Repository navigation
feat(app-space): checkpoint prize barcode + capped scanner handover - #2861
Merged
Merged
Conversation
Adds GET /api/my-prize-barcode?eventId=<legacyId> — a session-authenticated
endpoint that mints a QR PNG encoding { uid: <Users.ID>, payload: { eventId } }
for the logged-in visitor. Identity is resolved from the XSUAA session via
resolveDbUserScoped; client cannot inject a uid via query string.
Cache-Control: private, no-store (identity-bearing; no CDN/browser cache).
401 when no DB user resolves; 500 on QR generation failure.
Route registered immediately after app.use(basicAuthMiddleware) in server.js,
alongside the existing /api/qrcode handler.
Tasks 3 + 4: - Add pure helpers checkpointBarcodeUrl / shouldShowCheckpointBarcode in checkpoint-barcode.ts (testable without mounting the component) - Wire authenticated ref from getAppSpaceProgress data.authenticated - Replace dead PRIZE/EARNED QR block with CHECKPOINT barcode section: shows <img src="/api/my-prize-barcode?eventId=..."> when track complete + authenticated; shows sign-in hint when not authenticated - Remove dead qrCodeUrl helper (no other callers confirmed via grep) - Add 10 tests covering URL shape, gate conditions, and render paths
…helper shouldShowCheckpointBarcode now takes (item, unlocked, authenticated) instead of (track, index, authenticated), eliminating the copy of the CHECKPOINT unlock logic that was already in isItemUnlocked(). The component wrapper passes isItemUnlocked(track, index) as the unlocked arg — single source of truth. Tests updated to the flat signature.
- getContestant now accepts accountNumber as a UUID string (Users.ID), dropping the parseInt / legacyId lookup path. - Adds eventId: Integer param; resolves Events by legacyId then scopes PrizeRecords count to that event_ID. Degrades gracefully (no event filter) when eventId is absent or unresolvable. - Returns claimedCount: Integer + prizeRecords: '<N> of 2 prizes claimed' replacing the per-prize-title enumeration. Completion counts unchanged. - claimPrize is untouched; all existing ownership tests still pass. - New test: test/unit/scanner-contestant-count.test.js (4 cases: scoped count, event isolation, completion fields present, unknown UUID → 404).
Add ScannerService.claimPrizeForContestant(accountNumber, eventId):
- Inserts a CLAIMED PrizeRecords row for (user UUID, event legacyId) iff
current CLAIMED count < 2; returns { claimedCount, message }.
- Atomicity: uses SELECT.forUpdate() on the user row to acquire an exclusive
row-level write lock before reading the prize count. On HANA this
serializes concurrent calls for the same user. On SQLite (unit tests)
forUpdate() is a no-op but Node.js single-threaded event loop serializes
the async chain within one cds.tx.
- Old claimPrize kept in place (still works on legacy pre-existing rows).
- legacyId left null on new rows — nullable on HANA, no @mandatory in CDS.
Tests: test/unit/scanner-claim-capped.test.js (5 tests, all pass)
(a) 1 CLAIMED → claim → claimedCount===2, new row exists
(b) 2 CLAIMED → claim → cap message, no new row
(c) Promise.all two concurrent claims from count 1 → final count 2
…5 regression)
scanner-service-auth.test.js seeded { ID: 'u-alice', legacyId: 8001 } but
called getContestant({ accountNumber: '8001' }) — the legacyId. Task 5 changed
getContestant to resolve by Users.ID (UUID), so '8001' no longer matched.
Changed all three call sites to accountNumber: 'u-alice' (the UUID). Auth
assertions (MobileApp scope guard) are unchanged.
- types.ts: add eventId to ScanResult.payload; add claimedCount to ContestantData
- useScannerApi.ts: getContestant(uid, eventId) passes eventId in URL;
replace claimPrize with claimPrizeForContestant(uid, eventId) returning
{claimedCount, message}; old claimPrize wrapper removed (was dead code)
- ScannerApp.vue: parseScanData reads payload.eventId; loadContestant passes
eventId to getContestant and stores claimedCount; template shows
"Prizes claimed: N of 2"; handover button disabled at claimedCount >= 2;
on success shows returned message and refreshes via getContestant;
manual placeholder updated to new QR shape
- test: scanner-claim.test.ts covers URL shape for both new functions (7/7 pass)
…(Task 5 contract)
…ybrid test Finding #1 — UI5 App at /scanner-ui/ still used old payload.recordId shape and called legacy claimPrize(). Updated to new contract: - CONTESTANT_DEFAULTS: recordId/status -> eventId/claimedCount - _getContestant: now accepts (uid, eventId) and sets /claimedCount from result - _processScanResult: reads payload.eventId (Number), drops recordId/status - onClaimPrize: calls /claimPrizeForContestant(...) with accountNumber + eventId; caps at claimedCount >= 2 (shows MessageBox.warning); shows returned message - View: primary prize display is "N of 2" counter; button label "Record prize handover"; button disabled when claimedCount >= 2 or not loaded; removed stale recordId/status sub-line from identity header Finding #2 — hybrid test scanner.getContestant call passed seed.userLegacyId (numeric legacyId); getContestant now resolves Users by ID (UUID). Fixed to pass seed.userId (UUID). No assertions weakened. Needs npm run test:hybrid to confirm on real HANA.
…ckpoint-qr # Conflicts: # srv/developer-service.js
jung-thomas
marked this pull request as ready for review
October 20, 2026 19:11
jung-thomas
added a commit
that referenced
this pull request
Oct 20, 2026
…e-401 fix(#2861): App Space prize-barcode 401 for logged-in users
jung-thomas
added a commit
that referenced
this pull request
Oct 20, 2026
…arcode fix(#2861): prize-barcode 401 — resolve identity from cds.context.user
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
App Space Checkpoint Prize Barcode
Makes a track's Checkpoint render a scannable identity barcode once the track is complete; the instructor's scanner resolves it to a "prizes claimed: N of 2" count and records a handover, server-capped at 2 per visitor per event.
Fixes the reported bug (no QR at the checkpoint for
?eventId=10000015) — root cause was that the per-track barcode flow was never wired up in the backend, plus the checkpoint rendered blank because the resolver ignoredcheckpointTitle.What changed
getAppSpaceProgress/getEventProgressnow fall back toCompletionPathItems.checkpointTitle(bug fix).GET /api/my-prize-barcode?eventId=mints the QR from the logged-in session, encoding{ uid: <user UUID>, payload: { eventId } }. Identity never appears in a client URL;Cache-Control: private, no-store.AppSpace.vue) — renders the checkpoint barcode when the track is complete and authenticated; sign-in hint otherwise; dead PRIZE/EARNED block retired.ScannerService) —getContestant(accountNumber, eventId)resolves by user UUID, returns event-scopedclaimedCount; newclaimPrizeForContestant(accountNumber, eventId)inserts a CLAIMEDPrizeRecordsrow atomically (.forUpdate()lock on the Users row, held across count+insert), capped at 2./scanner-vue/) and the UI5 scanner (/scanner-ui/) both show "N of 2" and a cap-gated "Record prize handover" button on the new contract.Design & plan
docs/superpowers/specs/2026-10-20-app-space-checkpoint-prize-barcode-design.mddocs/superpowers/plans/2026-10-20-app-space-checkpoint-prize-barcode.mdDecisions made during build (please sanity-check)
Users.ID), not sapId/legacyId — works for any login provider; legacyId resolution dropped (no working legacy flow existed).forUpdatelock.^/api/(.*)$XSUAA catch-all already authenticates/api/my-prize-barcode.Known / deferred
forUpdate(); the mechanism is correct and documented, but the true multi-connection proof needsnpm run test:hybridon real HANA (see post-deploy below).page-fallback/page-serveunit failures are not from this branch (fail identically at merge-base79e59da5).getContestantsilently falls back to an unscoped count on an unresolvedeventId(the paired claim stays event-scoped, so no over-claim risk).Post-deploy verification (DEV) — required before calling done
Local
cds bindcan't reach the app's HDI container, so verify on the deployed DEV app:npm run test:hybrid(withcf login) to exercise the atomic cap on real HANA.🤖 Generated with Claude Code