Skip to content

refactor(api): give check-in a service class and typed responses - #297

Open
harshtandiya wants to merge 5 commits into
refactor/api-thinfrom
refactor/api-checkin
Open

refactor(api): give check-in a service class and typed responses#297
harshtandiya wants to merge 5 commits into
refactor/api-thinfrom
refactor/api-checkin

Conversation

@harshtandiya

Copy link
Copy Markdown
Collaborator

Stacked on #296 — read that first. Diff here is checkin/ plus its scanner composable.

Both endpoints are two lines now; CheckinService owns the Frontdesk Manager guard, the ticket lookup and the cancelled / already-checked-in checks. checkin_ticket no longer calls validate_ticket_for_checkin just to reuse them.

Errors replace three generic 417s: TicketNotFound (404), TicketCancelled and AlreadyCheckedIn (409), each with its own copy.

The scanner was matching on error substrings. useTicketValidation.ts picked its toast by JSON.stringify(error).includes("Ticket not found") and friends — so the error text was an undeclared contract, and changing any message would have silently broken it. Now reads err.messages[0] like the rest of the dashboard. One branch was already dead: nothing in Python throws "not confirmed and cannot be used for check-in".

One intentional wire change: check_in_date is now always present, null before check-in, where it used to be absent. useTicketValidation.ts:20 already declared it check_in_date?: string | null. Same for payment_details on the check-in response — TicketDetailsModal.vue:85 guards it with v-if. Everything else byte-identical, key order included; diffed old vs new module in-process.

Schema field types match what Frappe actually returns — date, timedelta, datetime — so json_handler renders them exactly as before ("10:00:00", not RFC-3339).

Checks: 8 new tests, green. Full bench run-tests --app buzz, ruff, pre-commit, yarn typecheck clean.

Noticed, not fixed: only docstatus == 2 blocks check-in, so a draft ticket still scans through. Pre-existing; out of scope here.

🤖 Generated with Claude Code

Both endpoints are now two lines over CheckinService, which owns the
Frontdesk Manager guard, the ticket lookup and the cancelled/already-in
checks. checkin_ticket no longer calls validate_ticket_for_checkin to
reuse them.

Errors carry their own copy: TicketNotFound (404), TicketCancelled and
AlreadyCheckedIn (409), replacing three generic 417s.

The scanner used to branch on substrings of the serialized error to pick a
toast. That contract is gone — it reads err.messages[0] like the rest of
the dashboard. One of its branches was already dead: nothing throws "not
confirmed and cannot be used for check-in".

Response gains one key: check_in_date is now always present, null before
check-in. The scanner already declared it optional. Everything else is
byte-identical, key order included.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

get_cached_doc already caches, so cached_property only adds a second layer
that can go stale within a request. Plain properties read through the
framework cache each time.

The existence check moves to the constructor, which is where it belongs
now that the property no longer runs it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

Doctype controllers carry frappe's auto-generated field types, so the
properties can name them. Imported under TYPE_CHECKING: no runtime import,
so no risk of a cycle through buzz_event.py, which already imports from
buzz.api.forms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

The check-in refactor was only verified in-process: Python tests assert
the model, and payloads were diffed by importing old and new modules side
by side. Nothing exercised HTTP, the browser, or the components that read
the response.

Two things that only break in a browser are now covered: the response keys
TicketDetailsModal renders, and the client-side role gate, which reads the
User.roles child table out of get_user_info.

Seeding needed two new pieces. createUserWithRoles and saveLoginState let a
project run under an identity other than the shared one — check-in demands
Frontdesk Manager, which CI's System Manager user does not have. The
access-denied case gets its own roleless user rather than reusing
Administrator, who holds every role and would never see that screen.

Cleanup runs at the top of the setup, not in afterAll: the spec's context
carries the front-desk session, while the helpers sign writes with the
Administrator CSRF token.

extractTicketId moves out of QRScanner.vue into utils so it can be tested.
Camera capture stays uncovered; this is the only real logic behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@harshtandiya

Copy link
Copy Markdown
Collaborator Author

Added E2E coverage (f516496). 7 tests, green, and green again on a second consecutive run — which is the real check, since the first run consumes the seeded ticket for the day.

✓ front-desk user reaches the scanner
✓ a valid ticket opens the details modal
✓ an unknown ticket surfaces the server message
✓ checking in records the visit, and a second scan is refused
✓ a user without the Frontdesk Manager role is refused

The last three are the ones worth having: two assert that class-defined error copy reaches the toast now that the substring matching is gone, and the role test covers the client-side gate that reads the User.roles child table out of get_user_info — the shape #296 deliberately left untyped.

Three things worth knowing for the next spec:

  • CI's user can't check in. ui-tests.yml authenticates as a System Manager, and CheckinService calls frappe.only_for("Frontdesk Manager", True). New helpers createUserWithRoles / saveLoginState seed a front-desk user and persist its session, so the project runs under it. Nothing in e2e/ created users or roles before.
  • The access-denied case has its own roleless user. Administrator holds every role and would never see that screen.
  • Cleanup runs at the top of the setup, not in afterAll. The spec's context carries the front-desk session while the helpers sign writes with the Administrator CSRF token from e2e/.auth/csrf.json. Cleaning first is also the suite's existing idiom.

extractTicketId moved out of QRScanner.vue into utils/ticketQr.ts with 6 unit tests. Pinning it turned up that a bare lowercase ticket ID only resolves by falling through to the "any run of 10+ characters" branch — the first branch is uppercase-only and real names are lowercase. Behaviour pinned as-is, not fixed.

Camera capture and html5-qrcode decoding remain uncovered; the spec drives the manual-entry input, which calls the same validateTicket().

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@harshtandiya
harshtandiya force-pushed the refactor/api-checkin branch from 565c51a to f516496 Compare July 28, 2026 19:24

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

The check-in assertion raced the request it was meant to observe. It
waited on the "Last Scan Result" panel, which is already on screen from
the preceding validate call, so the Event Check In query ran before
checkin_ticket had landed. CI was slow enough to expose it; locally it
always passed.

confirmCheckIn now waits for the response and for the modal to close,
which is a state only check-in produces.

Retries failed differently — the modal never opened — because the first
attempt had by then consumed the shared ticket, leaving the retry to scan
an already checked-in one. Each test creates its own ticket, so the pair
of check-in tests no longer depend on running in order and a retry starts
clean. The seeded ticket is gone; setup keeps the ticket type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant