Receipt Content Fields + Stringer Business-Identity Fields¶
This page specifies what content appears on the printed/PDF receipt and which Stringer-profile fields back the business-identity surface. ADR-0002 and docs/architecture/receipts.md pin how the receipt is rendered (WeasyPrint, A4, top-left quadrant carries the total, EN/DE templates) and when it is delivered (printed + emailed at Strung — M14). This page closes the field-list gap for Mira, Pax, and Stefan. Cross-cuts: V2 scope M13/M14/M19, data model — Stringer + Order + Person, the order-lifecycle requirements (filed separately as racket-book#80 — receipt re-emit triggers), client identity & sharing. Tracked in racket-book#82.
Stefan's defaults (already baked in)¶
These three are decisions, not open questions:
- Total placement: top-left quadrant (per ADR-0002).
- Receipt-numbering format:
YYYY-NNNN, sequential within (stringer, year). Resets to0001on January 1 of each year, per stringer. - Mandatory disclosure: "Not VAT-registered" line included on every receipt (the equivalent German line on DE-locale receipts).
Everything else on this page is Iris's draft for Stefan to confirm; open questions are flagged at the top of each affected section and consolidated at the bottom.
Quadrant-by-quadrant field list¶
The receipt is an A4 page divided into four quadrants by the page midlines (per ADR-0002 page-layout sketch). The customer sees only the top-left quadrant when the receipt is folded twice and clipped to the racket. Everything else is on the inside, visible when the receipt is unfolded.
Top-left quadrant (visible at handover)¶
The quadrant that survives the fold. Carries identity + the headline number.
| # | Field | Source | Required | Notes |
|---|---|---|---|---|
| TL-1 | Stringer logo | Stringer.logo (asset / blob) |
Optional | If absent, falls back to TL-2. Logo is rendered to fit a defined slot (Mira pins exact sizing). |
| TL-2 | Stringer display name | Stringer.display_name |
Required | Always rendered. The "who strung this" identifier visible at handover. |
| TL-3 | Stringer business name | Stringer.business_name |
Optional | Rendered below display name when set (e.g. "Stefan Wagen / RacketLab"). |
| TL-4 | TOTAL — CHF XX.XX |
Order.total |
Required | The 28pt typographic anchor of the receipt (per docs/architecture/receipts.md CSS skeleton). The reason the layout is inverted from a conventional invoice. |
| TL-5 | Player display name | order.client_profile.person.display_first_name + display_last_name |
Required | Small. The customer's name as shown on the receipt; backs the "is this mine?" check at pickup. Suppressed on self-jobs (where the stringer is the customer). |
| TL-6 | Racket model identifier | order.racket.make + model + (optional) version |
Required | Concise: e.g. "Babolat Pure Aero 98". Disambiguates which racket if the player has multiple. |
Top-right quadrant (inside the fold; visible when unfolded)¶
Carries job metadata and receipt identity.
| # | Field | Source | Required | Notes |
|---|---|---|---|---|
| TR-1 | Receipt number | Order.receipt_number |
Required | Format YYYY-NNNN (per Stefan's default; see Receipt-number format). |
| TR-2 | Strung date | Order.strung_at |
Required | The receipt's anchor date (the moment the receipt was finalized; see the order-lifecycle requirements — racket-book#80). |
| TR-3 | Ordered date | Order.ordered_at |
Optional | Shown when set. Suppressed when NULL (typical on self-jobs). |
| TR-4 | Player serial / racket instance ID | order.racket.serial_or_instance_id |
Optional | Rendered when set, useful when a player has multiple of the same model (e.g. PA98_2023_25). |
Per OQ-R-4 (closed 2026-05-04): the receipt shows only ordered_at and strung_at. Order.returned_at and Order.paid_at are never rendered on the receipt — they are admin/stringer-tracking fields, not customer-facing. A customer's emailed receipt does not change visually when the stringer toggles paid status; the paid status surface lives on the order page (stringer view) and, in V3, in the client portal. This keeps the receipt's role focused on "what was strung, what it cost" rather than "where this order is in the back-office workflow", and avoids the "did I actually pay this?" customer confusion that a re-emitted post-paid receipt could cause.
Bottom-left quadrant (inside the fold)¶
Carries the main string detail. Symmetric with bottom-right (cross side).
| # | Field | Source | Required | Notes |
|---|---|---|---|---|
| BL-1 | Section header "MAIN STRING" / locale equivalent | i18n string | Required | Localized per template. |
| BL-2 | Main string identification | order.main_string.manufacturer + model + gauge, or order.main_string_text if free-text override |
Required | E.g. "Luxilon ALU Power 1.25". |
| BL-3 | Main tension | Order.main_tension (kg) |
Required | E.g. "24 kg". |
| BL-4 | Main string price | Order.main_price |
Required (visible) | Displayed; excluded from sum if main_byo = TRUE (matches V1 behaviour). |
| BL-5 | Main BYO indicator | Order.main_byo |
Conditional | Rendered as "BYO" badge when TRUE; price still shown but visually marked as not-charged. |
| BL-6 | Main color | Order.main_color |
Optional | Shown when set. Absent on most client orders; common on self-orders. |
Bottom-right quadrant (inside the fold)¶
Cross string detail (symmetric) + footer fields.
| # | Field | Source | Required | Notes |
|---|---|---|---|---|
| BR-1 | Section header "CROSS STRING" / locale equivalent | i18n string | Required | |
| BR-2 | Cross string identification | order.cross_string.manufacturer + model + gauge, or order.cross_string_text |
Required | E.g. "same as main" rendering when cross matches main (see Cross-equals-main short form). |
| BR-3 | Cross tension | Order.cross_tension (kg) |
Required | |
| BR-4 | Cross string price | Order.cross_price |
Required (visible) | Same exclusion-from-sum rule as main when cross_byo = TRUE. |
| BR-5 | Cross BYO indicator | Order.cross_byo |
Conditional | Rendered as "BYO" badge when TRUE. |
| BR-6 | Cross color | Order.cross_color |
Optional | |
| BR-7 | Labor line | Order.labor (CHF) |
Required | Single-line item: e.g. "Labor — CHF 25.00". |
| BR-8 | Strings subtotal | Order.strings_subtotal |
Required | Computed from non-BYO sides; shown for transparency. |
| BR-9 | Total recap | Order.total |
Required | Same number as TL-4, but in the conventional invoice position; gives the unfolded receipt a familiar total-bottom-right reading. |
| BR-10 | Stringer signature line + thank-you message | Stringer.receipt_thankyou_text (per-stringer customizable) — falls back to a platform-default i18n string when NULL |
Optional | Per OQ-R-3 (closed 2026-05-04 with per-stringer customizable). When Stringer.receipt_thankyou_text is set, that string is rendered verbatim (locale-neutral — the stringer chooses which language they write it in, typically matching their default_locale). When unset, the receipt template falls back to the platform-default i18n string (receipt.thankyou.default — EN: "Thank you for your business." / DE: "Vielen Dank für Ihren Auftrag.") looked up against the receipt's resolved locale. |
Page-footer band (spans both bottom quadrants or runs as a footer)¶
Mandatory disclosures + stringer contact, rendered as a thin footer strip below the bottom quadrants.
| # | Field | Source | Required | Notes |
|---|---|---|---|---|
| F-1 | "Not VAT-registered" disclosure | i18n constant | Required | Per Stefan's default. EN: "Not VAT-registered." DE: "Nicht mehrwertsteuerpflichtig." Always rendered, every receipt, every locale. |
| F-2 | Stringer business address | Stringer.business_address |
Optional | Rendered when set; recommended at onboarding for receipt completeness. |
| F-3 | Stringer phone | Stringer.phone |
Optional | Rendered when set. |
| F-4 | Stringer email | Stringer.email |
Required | Always rendered (it is the platform-canonical stringer identifier). |
Comments band (per OQ-R-1, closed YES)¶
Order.comments IS rendered on the receipt as a comments band, sitting beneath the bottom quadrants (above the page-footer disclosure strip) when the field is non-empty. It is suppressed entirely when the field is empty so receipts without a comment do not show an empty box.
| # | Field | Source | Required | Notes |
|---|---|---|---|---|
| C-1 | "Comments" / "Anmerkungen" header | i18n constant | Conditional | Rendered only when comments is non-empty. |
| C-2 | Free-text comments | Order.comments |
Conditional | Rendered verbatim, line breaks preserved. |
Redaction caveat (Rule #1 share): under ADR-0004's Rule #1 (stringer-to-stringer per-job share), comments are redacted from the receiving stringer. The receipt is therefore rendered in two visibility modes:
| Audience | Comments visible on receipt? |
|---|---|
The originating stringer (the order's stringer_id) |
Yes — full comments band visible. |
The client (ClientProfile.person) on their own emailed/printed receipt |
Yes — full comments band visible. |
| A Rule #1 grantee (receiving stringer of a per-job share) viewing the receipt | No — comments band is redacted server-side and the receipt re-renders without rows C-1 and C-2. |
| A Rule #2 / Rule #3 grantee (client-initiated cross-stringer share) | N/A for V2 — Rules #2/#3 are V3-portal-gated per CC-2026-05-02-2. When Rules #2/#3 land, comments visibility follows the client-grant scope (the client controls what their grantees see; default = comments-visible because the client's comments are about themselves). |
The redaction is enforced at render time: the same chokepoint that gates comments reads on orders (per ADR-0004 write-protection symmetry — Rule #1 grantees are read-restricted on this column) is applied when assembling the receipt context for a Rule #1 grantee. A comments edit on a Strung+ order auto-re-emits the receipt to the client (and refreshes the originating-stringer view); the Rule #1 grantee's on-demand view is unaffected by re-emit (their view is regenerated without comments on every render).
Optional surfaces (not in V2 unless Stefan asks)¶
- Stringer logo on every page (already covered by TL-1; flagged here only because logos are an asset-management consideration: where the file lives, how it's uploaded, what formats are allowed — Pax's call, not requirements).
- QR code linking to the V3 client portal: out of V2 scope. V3 candidate.
Stringer business-identity fields (backing the receipt)¶
A new field group on the Stringer entity, scoped here so that:
- Receipt rendering has a stable shape to draw from.
- The stringer onboarding flow (issue #84) knows which fields to collect at first sign-in.
| Field | Required | Notes |
|---|---|---|
display_name |
Required | Already on Stringer. The "who strung this" name on the receipt (TL-2). |
business_name |
Optional | Trading name when different from display name (e.g. "RacketLab"). Rendered below display_name on the receipt (TL-3). |
email |
Required | Already on Stringer. The platform-canonical contact (F-4). |
phone |
Optional | Rendered in the receipt footer when set (F-3). |
business_address |
Optional but recommended | Rendered in receipt footer when set (F-2). Onboarding wizard prompts the stringer to fill this with a "your customers will see this on every receipt" hint. |
logo |
Optional | A binary asset (image). When set, replaces the text-only display_name area in TL-1. Image-handling specifics (allowed formats, size limits, storage location) are Pax's implementation call. Receipt template renders the logo in a defined-size slot (Mira pins). |
default_locale |
Required (already on Stringer) | EN | DE. Drives the receipt template variant when no per-Person locale is set. |
receipt_thankyou_text |
Optional | Per-stringer customizable thank-you message in the receipt's bottom-right quadrant (BR-10). Per OQ-R-3 (closed 2026-05-04 with per-stringer customizable), this field IS added to the Stringer entity. Fallback rule: when NULL, the receipt template falls back to a platform-default i18n string (receipt.thankyou.default — EN: "Thank you for your business." / DE: "Vielen Dank für Ihren Auftrag."). The field itself is locale-neutral free text — the stringer writes it in whichever language they prefer; if they want one EN and one DE version, they can keep the field NULL and rely on the platform i18n default, which IS locale-routed. |
These fields land on Stringer per data model; the entity gains them in V2.
Receipt-number format¶
| Aspect | Decision |
|---|---|
| Format | YYYY-NNNN — e.g. 2026-0001. |
| Uniqueness scope | Per (stringer, year). Each stringer has their own sequence; January 1 each year resets NNNN to 0001 for that stringer. |
| Generation moment | At receipt first emit (T2 in the order-lifecycle transition table — racket-book#80). The number is assigned to the Order on the Strung transition and never reassigned thereafter. Re-emits use the same number. |
| Storage | New field Order.receipt_number (string), populated on Strung; unique per (stringer, calendar-year-of-strung_at). |
| Concurrency | The next-number assignment must be safe under concurrent stringing-day sessions (rare in V2 with one active stringer, but the rule shouldn't break later). Implementation is Pax's call (DB sequence per stringer-year, or MAX(...) + 1 inside a transaction with a unique-index retry, or similar) — not advisory locks (per project_pgbouncer_constraint.md, no session-scoped Postgres features). |
| Display on UI | The receipt number is shown on the order page (with a "copy" affordance for stringer reference) and in the order list as a sortable / searchable column. |
What about un-Strung then re-Strung? When a Strung is reversed (T2-r in the order-lifecycle transition table — racket-book#80) the receipt_number stays attached to the order — it is not freed back into the pool. When the order is re-Strung, the existing number is reused for the new emit. This avoids a gap-in-sequence question if the order is never re-Strung (the number is "spent" but the order doesn't need a fresh one); it also keeps the link between the originally-emailed receipt number and the database row stable.
What about admin hard-delete? A hard-deleted order's number is left as a gap in the sequence (the row is gone, so its number is not re-assignable). This is the same behaviour as DB sequences in general; gaps are non-meaningful.
Locale handling (which template, which strings)¶
Per docs/architecture/receipts.md — draft, pending the i18n open question close: receipt language = Person.default_locale if set, else Stringer.default_locale. Per-order locale override is not in V2.
For receipt content fields:
- Stringer-sourced fields (display_name, business_name, business_address, phone, email, logo) are locale-neutral — they render identically in EN and DE templates.
- Person-sourced fields (display_first_name, display_last_name) are locale-neutral.
- Order data (numbers, dates, prices) is locale-formatted: dates are ISO
YYYY-MM-DDin both locales (no localization here — ISO is the platform default; if Stefan wantsDD.MM.YYYYin DE that's an OQ); prices areCHF X.XXin both locales. - Section headers, labels, BYO badge text, footer disclosure, fallback thank-you text are i18n strings, looked up per locale via Mira's design-system kit. Per OQ-R-3, the stringer-customizable thank-you (
Stringer.receipt_thankyou_text) is locale-neutral free text and overrides the i18n fallback when set.
The two template files (receipt_en.html, receipt_de.html) share the base.html CSS layout; per-locale variation is restricted to copy strings (per ADR-0002 decision).
Cross-equals-main short form¶
When main_string_id == cross_string_id (or both texts identical) AND main_tension == cross_tension AND main_byo == cross_byo AND main_color == cross_color, the bottom-right quadrant's string identification (BR-2) MAY render as "Same as main" instead of repeating the full string spec. This is a Mira UX decision; the requirement here is only that the two-side data is fully present in the underlying Order row (which it is, per the data model). Iris flags it for Mira's design pass; not a hard requirement of this page.
Paid-stamp affordance — dropped per OQ-R-4¶
Earlier draft contemplated a visual "PAID" indicator on the receipt when Order.paid_at is set. Per OQ-R-4 (closed 2026-05-04), paid_at is never on the receipt — paid status is admin/stringer-tracking only, not customer-facing. There is therefore no paid-stamp affordance on the receipt itself; paid status is surfaced on the order page (stringer view), and in V3 will be visible to the client through the client portal.
Re-emit triggers (cross-link to order lifecycle)¶
The list of fields whose post-Strung edits trigger an automatic receipt re-emit is owned by the order-lifecycle requirements (racket-book#80, parallel MR). Cross-checks against this page (updated for the OQ flips on 2026-05-04):
- All pricing fields (
labor,main_price,cross_price, derivedtotal) are on the receipt (BR-7, BL-4, BR-4, TL-4 / BR-9). Edit triggers re-emit. ✓ Per OQ-L-1 (closed with (a)), pricing edits are allowed in any post-Draft state and always re-emit. - All string-spec fields (
main_string_id/text/tension/byo/color, same for cross) are on the receipt. Edit triggers re-emit. ✓ - Racket FK and ClientProfile FK are on the receipt (TL-5, TL-6). Edit triggers re-emit (admin override only per the lifecycle rules). ✓
commentsIS on the receipt (per OQ-R-1, closed YES) — for client-bound emits. Edit triggers re-emit. ✓ Rule #1 redaction caveat: the auto-re-emit goes to the client; a Rule #1 grantee's on-demand view is rendered without comments (the redaction is at render time, not at re-emit time). See Comments band.- Optional self-fields (
method,dynamic_tension_after) are NOT on the receipt (they are stringer-internal — the receipt is for the customer). Edit does not trigger re-emit. ✓ - Lifecycle dates:
strung_atis on the receipt as the anchor (TR-2), so changing it is fundamentally a state regression (handled by lifecycle T2-r).ordered_atIS on the receipt (TR-3) per OQ-R-4, so its edit on Strung+ orders triggers re-emit.returned_atandpaid_atare never on the receipt (per OQ-R-4); their edits do not re-emit. - Stringer business-identity fields (display_name, business_name, address, phone, logo,
receipt_thankyou_text): editing them changes every receipt. Auto-re-emit is per-order on demand only, not bulk; per the lifecycle doc, a "regenerate affected receipts" button is flagged for Mira's V2.x design and is not in the V2 first cut.
Acceptance criteria¶
- A1. Every visible field on the receipt is enumerated in this page with its source (Order / ClientProfile / Person / Stringer / i18n constant) and required-vs-optional status.
- A2. The Stringer business-identity field group (display_name, business_name, email, phone, business_address, logo, default_locale, receipt_thankyou_text) is enumerated with required-vs-optional status.
- A3. Receipt-number format (
YYYY-NNNN, per-stringer-year reset) is specified, including assignment moment, storage, and re-emit behaviour. - A4. "Not VAT-registered" disclosure is specified including locale text.
- A5. Total placement is top-left quadrant (per ADR-0002), 28pt (per
docs/architecture/receipts.md), with a recap in bottom-right. - A6. Locale handling is specified: which fields are locale-neutral, which go through i18n lookup, which are stringer-customizable.
- A7. Cross-link with the order-lifecycle re-emit triggers (racket-book#80) confirms the re-emit field list matches the receipt content.
- A8. Open questions are flagged at the top of the affected sections and consolidated in Open questions.
Open questions¶
-
~~OQ-R-1~~ — Closed 2026-05-04 with YES:
Order.commentsIS on the receipt as a comments band (rendered only when non-empty). Rule #1 redaction caveat: comments are redacted server-side from receiving stringers (per ADR-0004) — only the originating stringer and the client see comments on the receipt. See Comments band. Comments edits on Strung+ orders therefore trigger an automatic receipt re-emit per the order-lifecycle re-emit triggers (racket-book#80). -
OQ-R-2 (Stefan to confirm): date format on receipt — ISO
YYYY-MM-DDin both locales (Iris's draft default), or locale-specific (YYYY-MM-DDin EN,DD.MM.YYYYin DE)? - Default applied: ISO
YYYY-MM-DDin both locales. Unambiguous, machine-readable, consistent with the timestamps elsewhere in the platform. -
One-line change in the templates if Stefan prefers locale-specific.
-
~~OQ-R-3~~ — Closed 2026-05-04 with per-stringer customizable:
Stringer.receipt_thankyou_textis added to the Stringer entity. Locale-neutral free text. Falls back to a platform-default i18n string (receipt.thankyou.default) when NULL. See Stringer business-identity fields and BR-10. -
~~OQ-R-4~~ — Closed 2026-05-04 with "only Ordered + Strung": the receipt shows
ordered_at(when set) andstrung_at(always); it never showsreturned_atorpaid_at. These two are admin/stringer-tracking only, not customer-facing. The TR quadrant table and the Paid-stamp affordance section are updated accordingly. -
OQ-R-5 (Stefan to decide): stringer logo upload — at onboarding (issue #84), as part of business-identity fields, or as a follow-up step in account settings?
- Default applied: account-settings only (V2 first cut), with onboarding nudge flagged for Mira. Avoids forcing a logo upload on first-login when the stringer just wants to record a job.
Cross-references¶
- Architecture: ADR-0002 (receipt PDF design),
docs/architecture/receipts.md(rendering pipeline),docs/architecture/i18n.md(locale strategy). - Sibling requirements: the order-lifecycle requirements (racket-book#80, parallel MR — re-emit triggers); data model — Stringer (business-identity fields land here); the stringer-lifecycle requirements (racket-book#84, parallel MR — onboarding wizard collects the new business-identity fields; recommends
business_addressupload). - UX: Mira owns the visual design and will resolve the four UX-shaped open questions (cross-equals-main short form rendering, paid-stamp visual, logo slot sizing, comments-panel design IF OQ-R-1 flips). Iris flags these as follow-up requirements.
- Implementation: Pax (after Mira's design lands) — receipt-number assignment must be PgBouncer-compatible (no session-scoped features); template structure is per ADR-0002.
- Issue tracking: racket-book#82 (this page).