Skip to content

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 to 0001 on 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.

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:

  1. Receipt rendering has a stable shape to draw from.
  2. 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.mddraft, 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-DD in both locales (no localization here — ISO is the platform default; if Stefan wants DD.MM.YYYY in DE that's an OQ); prices are CHF X.XX in 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.

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.

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, derived total) 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). ✓
  • comments IS 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_at is on the receipt as the anchor (TR-2), so changing it is fundamentally a state regression (handled by lifecycle T2-r). ordered_at IS on the receipt (TR-3) per OQ-R-4, so its edit on Strung+ orders triggers re-emit. returned_at and paid_at are 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.comments IS 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-DD in both locales (Iris's draft default), or locale-specific (YYYY-MM-DD in EN, DD.MM.YYYY in DE)?

  • Default applied: ISO YYYY-MM-DD in 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_text is 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) and strung_at (always); it never shows returned_at or paid_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_address upload).
  • 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).