[C06] Order Detail

Summary

Cross-cutting order view + admin-action surface. The full page at /orders/{orderId} is the canonical surface and owns all order-mutating action flows (cancel, mark-paid, refund, resend-receipt, add). Lightweight invocation surfaces (modal popover, hover tooltip) are gateways from callers such as E02 Event Participants — they signpost into the full page rather than executing mutations themselves.

Orders are not event-scoped — the same screen handles event participant orders, membership orders, and future cross-event purchases. The route deliberately omits any eventId segment.

Primary callers and consumers:

  • Caller from: E02 (chip click → modal → drill-in); future M02 membership-members; future Person screens; future finance / recon screens; deep-link from external systems (Slack, email, support tickets).

  • Caller to: action dialogs (cancel / mark-paid / refund) which post back to admin-service and re-render this screen; E09 from line-item EP links; (future) Membership detail screen from line-item membership links.

Actor & Context

Actor: event organiser, tenant admin, finance staff (future ROLE_FINANCE — see Out of Scope). Frequency: ad-hoc; high-frequency during reconciliation phases of an event or membership period. Precondition: the order exists; user has composite read access to BOTH the order’s organisation and the buyer’s person (per Order entity’s OrganizationalSecured + PersonalDataSecured interfaces). Entry point: chip click on a calling screen → modal → "View order" → /orders/{orderId}; OR direct route navigation /orders/{orderId} (deep-link); OR /orders/{orderId}?action=<action> to land directly in an action dialog.

Main Flow

  1. Operator arrives at /orders/{orderId}.

  2. Page renders, top to bottom:

    1. Sticky header band — order number, status pill, transaction date, billed person (with mailto: and tel: links when known), total amount, payment processor, WooCommerce external link (when external_id present), tenant name. Status-conditional action buttons sit top-right.

    2. Line items — one row per OrderLineItem: product, linked entity (EP name with race / Membership name with period), quantity, unit price, discount, line total. Linked-entity links route to E09 (EP) or future Membership detail (membership).

    3. Refunds & cancellations — collapsed if empty; populated by refundAmount / refundDateTime (per US #354) and by cancellation record once cancel_reason is persisted.

    4. Cross-referencesSibling orders panel (other orders for the same EP / Membership in the line items, resolved via the OrderCriteria extension; cancelled siblings hidden by default with a "Show cancelled (N)" toggle). Linked EP / Membership entities also surfaced here for navigation continuity.

  3. Operator triggers an action — either via the header button corresponding to current order status, or by arriving with ?action=<action> set.

  4. Action modal opens (modal-on-page, mirroring the C04 dialog pattern) with action-specific fields.

  5. On submit:

    1. Success — modal closes, page reloads order data, success banner surfaces the operation summary (status delta + cascade summary).

    2. Failure — modal stays open with error message; operator may retry or cancel.

Invocation Surfaces

The full page is reachable via three distinct surfaces. None are required — direct route navigation works on its own. The modal + tooltip are conveniences for callers that own a cluster of orders (e.g. an EP with multiple orders).

Hover tooltip — read-only preview (cluster-aware)

  • Triggered on chip mouseover (desktop); suppressed on touch.

  • Shows a popup panel (not a text-only tooltip) listing each non-cancelled order’s number, date, status, and total.

  • Bottom of the panel: a small info-icon hint suggesting "click for actions".

  • Click on the chip takes precedence — modal opens, tooltip dismisses.

Modal — cluster overview + action signposting

  • Triggered on chip click.

  • Always opens, even when the EP has only one non-cancelled order (consistency over single-row optimisation).

  • Lists each non-cancelled order as a row: number, date, status, total. Cancelled orders hidden by default; "Show cancelled (N)" toggle at the bottom of the modal reveals them inline (greyed treatment).

  • Per-row action buttons, contextual on that row’s status:

    • UNPAID / PENDING row → Mark as Paid, Cancel.

    • PAID row → Refund (visible only when at least one paid order in the cluster — gates the entire action class).

  • Action button click does NOT execute the mutation. It navigates to /orders/{orderId}?action=<action> — the full page opens with the action dialog auto-focused. This keeps the modal lightweight and the action flow consistent across surfaces.

  • Each row also exposes a View order link routing to /orders/{orderId} without an action focus.

Direct route — deep-link entry

  • /orders/{orderId} — view-only landing.

  • /orders/{orderId}?action=cancel|mark-paid|refund|resend-receipt — view + auto-open the corresponding action dialog.

  • /orders/new?epId=<id> or /orders/new?membershipId=<id> — new-order create flow (Future — see FU-2).

Action Flows

All action flows are modal-on-page dialogs reached via the header button (per current status) OR via ?action=<action> query. Same dialog, two entry paths.

Action Behaviour

Mark as Paid (UNPAID, PENDING)

Dialog captures: payment method (Cash / EFT / Other), payment processor (selector — typically the Office / Manual processor for cash), payment reference (string — useful for EFT), payment amount (defaults to order total; editable for partial), optional operator note. Submit → POST /api/orders/{id}/mark-paid → status transitions to PAID, paymentDateTime / paymentReference / paymentAmount / referenceCode persisted, downstream membership / EP activation cascade fires.

Cancel (UNPAID, PENDING)

Dialog opens with a conditional info box at the top:

* Multi-line cascade — when the order has more than one line item: "This order covers more than one membership/participant. Cancelling will cancel all of them." * Sole-paid-order cascade — when the affected person has no other paid orders: "This is the person’s only paid order. Cancelling could deactivate their participation/membership."

Both messages can be visible simultaneously when both conditions hold.

Dialog captures: cancel reason (reference-data picker — see Design Decisions); optional free-text note. Submit → POST /api/orders/{id}/cancel → status transitions to CANCELLED, cancelDateTime + cancel_reason persisted.

Refund (PAID)

Dialog captures: refund amount (full or partial — defaults to order total or paid total), refund mode (back to original PaymentProcessor vs office / manual), reason, deactivate-linked-entities flag (default true — surfaces the cascade explicitly to the operator). Submit → POST /api/orders/{id}/refundrefundAmount + refundDateTime persisted, downstream cascade per the flag. Depends on US #354 schema (in flight).

Resend receipt (PAID)

Dialog captures: recipient email (pre-filled from Order.email, editable). Submit → POST /api/orders/{id}/resend-receipt. Requires WooCommerce integration — Future, see FU-1.

Add order (zero-orders entry path)

Routed entry only — no header button. Reached via /orders/new?epId=<id> or ?membershipId=<id>. Full create flow with line-item authoring. Future, see FU-2; v1 stubs the entry point as a "Coming soon" tooltip on the calling screen’s empty cell.

Alternative Flows

  • AF-1 — CANCELLED order. Read-only banner across the top of the header band: "Cancelled on YYYY-MM-DD by X<reason>". No mutation affordances rendered. Refund and Mark-as-Paid buttons absent.

  • AF-2 — Refund pre-condition fails. Refund button is disabled when the order has no paid line items (or has already been fully refunded). Tooltip explains.

  • AF-3 — Permission denied. Header action button disabled with tooltip; dialog (when reached via ?action= query) opens with an inline error and a Close-only footer.

  • AF-4 — Order not found / no access. 404 page; back link to caller.

  • AF-5 — Action partial-failure. Operation succeeds on the order side but downstream cascade (membership activation, WC sync, receipt send) fails. Surface a warning banner explaining the partial state and offering retry where possible. Order-side mutation NOT rolled back.

  • AF-6 — Empty payment history. Section absent (not rendered) until the DTO carries payment events from the remote system. See FU-6.

Acceptance Criteria

  • Use-case page authored.

  • Status design-todo → in-design → handoff-ready after Claude Design pass.

  • :design-url: populated.

  • Route /orders/{orderId}not event-scoped.

  • ?action=cancel|mark-paid|refund|resend-receipt query auto-opens the corresponding action dialog on page load.

  • Sticky header band; collapsible sections.

  • Status-conditional header actions per the table in Action Flows.

  • Read-only display surface; mutations only via action dialogs.

  • Modal invocation surface implemented (per "Invocation Surfaces — Modal").

  • Hover tooltip invocation surface implemented (per "Invocation Surfaces — Hover tooltip"); suppressed on touch.

  • Sibling-orders panel powered by OrderCriteria extension (lineItemEventParticipantId / lineItemMembershipId filters).

  • Cancelled-orders default-hidden in modal + sibling panel; "Show cancelled (N)" toggles reveal them.

  • Browser-back returns to caller with caller’s filter state preserved (already supported by E02's URL-persistence convention; future callers must adopt the same).

  • Cross-references back to E02 (caller).

API Surface

Call Purpose

GET /api/orders/{id}

Order detail (existing). Returns OrderDTO with line items, computed totalAmount, security check. Reuse as-is; verify DTO surfaces paymentProcessor name + buyer person details (small projection check at impl time).

GET /api/orders?lineItemEventParticipantId.equals={id}
GET /api/orders?lineItemMembershipId.equals={id}

Sibling orders for the EP / Membership cluster. Requires OrderCriteria extension with two new LongFilter fields joining through lineItems.eventParticipant.id / lineItems.membership.id. Mirrors the existing lineItemId join. Composable with status / transactionDateTime / etc.

POST /api/orders/{id}/cancel

New endpoint. Body: { reasonId, note? }. Sets status to CANCELLED, persists cancelDateTime + cancel_reason_id. Liquibase: add cancel_reason_id FK to sales_order; create cancel_reason reference table.

POST /api/orders/{id}/mark-paid

New endpoint. Body: { paymentMethod, paymentProcessorId, paymentReference?, paymentAmount?, note? }. Sets status to PAID, persists payment fields, fires existing membership / EP activation cascade (mirroring the WC callback path).

POST /api/orders/{id}/refund

New endpoint. Body: { refundAmount, refundMode, reasonId, deactivateLinkedEntities? }. Persists refundAmount + refundDateTime. Predecessor: US #354 (refund schema + WC plugin sync). Reuses entity fields already on Order.

POST /api/orders/{id}/resend-receipt

New endpoint — requires WC integration. Future — see FU-1.

POST /api/orders + new convenience for `/orders/new?epId=…

membershipId=…`

Order create flow with caller context. Future — see FU-2.

GET /api/cancel-reasons

Out of Scope

  • Inline editing of order fields. v1 is read-only on the page itself — all mutations route through dedicated action dialogs.

  • Line-item-level mutations (edit qty, edit price, add/remove line items on existing orders). Edits to a paid order are refund-shaped operations; defer.

  • Field-level audit trail. Audit data isn’t tracked on Order today; not in v1, no future ADO ticket.

  • GL / journal entries drill-down. Finance-domain; lives in a future S01-class finance screen. Surface presence-only via the (future) payment-history section.

  • Recon record drill-down. As above.

  • Reassign customer (re-attach to a different billed person). Substantial flow (refund attribution, receipt re-issue); defer to a future use case if needed.

  • Customer-facing surfaces. C06 is admin-portal-only; customer notification flows live elsewhere.

  • Manual GL adjustments. Finance-grade; not C06’s job.

  • ROLE_FINANCE separation. v1 ships with all action affordances gated to ROLE_USER (org-scoped via composite security). A finer ROLE_FINANCE gate for refund / mark-paid is a future axis — captured but not actioned in v1.

Future Use Cases

Forward-looking. Not in v1. Captured here so the design intent isn’t lost and so v1 stubs in the right places.

FU-1 — Resend receipt (WC integration)

Resend the customer-facing receipt for a PAID order. Backend support required: a new admin-service endpoint that posts a receipt-resend request to the WC plugin, which uses its existing receipt-rendering path. Tracked as a separate Feature (Resend Receipt + WC Integration).

v1 stub: header button absent for PAID orders until the endpoint exists. (Or render disabled with a "Coming soon" tooltip if the visual presence is preferred.)

FU-2 — Add order create flow

Full create flow at /orders/new?epId=…|membershipId=…. Caller context drives initial line item population — opening this URL with epId=123 pre-selects the EP and seeds a line-item picker scoped to that EP’s event categories / products. Tracked as a separate Feature (Order Create Flow).

v1 stub: zero-orders cell on the calling screen renders an "Add order" affordance disabled with a "Coming soon" tooltip. Do not link to a placeholder route — a 404 destination is worse UX than a clearly-disabled button (mirrors E02's Add Participant + Substitute v1 stubs).

FU-3 — ROLE_FINANCE-gated refund elevation

Refund initiation is currently the most sensitive C06 action. When a ROLE_FINANCE role exists, the refund flow gates to it (vs ROLE_USER for cancel + mark-paid). Captured as a future axis; no ADO ticket today.

FU-4 — Audit log

If/when field-level audit logging is added to Order (currently absent), C06 surfaces it as a new collapsible section. Out of scope for v1 — no future ADO.

FU-5 — Reassign customer

Re-attach an order from one billed person to another, with refund attribution + receipt re-issue cascade. Substantial flow; deferred until a real operator workflow surfaces.

FU-6 — Payment history surfacing

Today, payment events live in remote systems (WooCommerce) and are not surfaced via the Order DTO. When backend sync brings these forward, C06 renders a Payment History section between Line Items and Refunds — timeline of initiation / gateway redirect / callback / status transitions. v1 hides the section when the DTO field is empty (which it always is in v1).

Design Anchors

  • Portal Pattern

  • UI Design Principles

  • design-journal/2026-04/admin-portal-screen-design-prompt-iteration.adoc — broader handoff workflow + 2026-04-30 carve-out for C06

  • design-journal/2026-03/woocommerce-order-reconciliation.adoc — order domain richness; office-payment / sync-failure cohorts that motivate the mark-paid action

  • design-journal/2026-03/payment-idempotency.adoc — duplicate-order context (#385); idempotency clues to be surfaced if/when payment history lands (FU-6)

  • design-journal/2026-01/financial-management.adoc — GL / recon background; explains why GL records are out of scope for C06

  • E02 (primary caller) — chip-cluster semantics + filter URL persistence

  • C04 — modal-on-page action-dialog pattern

  • C05 — "About this screen" info-panel pattern (candidate to apply on C06’s full page)

Design Decisions

  • Full page is canonical; modal + tooltip are gateways (2026-04-30). The full page at /orders/{orderId} owns all order-mutating action flows. Modal + hover tooltip are invocation surfaces offered to callers that own a cluster of orders (an EP with N orders); they signpost into the full page rather than executing mutations themselves. Rationale: orders are the financial source-of-truth and need a single, URL-shareable canonical surface that finance + ops + support can deep-link from external systems. Action flows are uniform across entry surfaces because they all converge on the full page’s ?action=<action> query.

  • Single route /orders/{orderId}, not event-scoped (2026-04-30). Orders cross event boundaries — memberships, future cross-event purchases — so the route omits any eventId segment. Sets the convention for cross-cutting financial entities and rules out the equivalent of a path-param event pin for orders.

  • Action route convention — ?action=<action> query, not sub-route (2026-04-30). The full page is the same screen across view-only and action modes; the action is a focused mode of the same screen, not a separate URL hierarchy. Refresh on ?action=cancel reopens the cancel dialog, which is the correct behaviour. Sub-routes (/orders/{id}/cancel) would imply different pages.

  • Single scrolling page; sticky header; collapsible sections (2026-04-30). Order detail is "what’s the state of this thing and what happened" — a tabbed layout fragments that narrative. Collapsible sections handle the audit / refund "could be long" cases without splitting the page. Sticky header keeps order # + status visible during scroll on long pages.

  • Read-only on the page itself; mutations only via action dialogs (2026-04-30). Order data is financially audit-sensitive. Inline editing gives the wrong affordance about consequences — operators who need a change generally need it to flow through a reason-bearing audit-trailed action. The Order entity has no note field today; if/when one lands, inline-editable becomes a candidate.

  • Modal-always on chip click (2026-04-30). Even when the EP has exactly one non-cancelled order, the chip click opens the modal (one row in it) rather than jumping straight to /orders/{orderId}. Rationale: consistent mental model; the modal still earns its keep (per-row action signposting + View-order link); operator can always bypass via the row’s View-order link.

  • Hover tooltip — rich popup panel; click takes precedence; touch-suppressed (2026-04-30). The chip’s hover tooltip is a popup panel (not a text-only tooltip) listing each non-cancelled order with status / date / total + an info-icon hint suggesting "click for actions". Click on the chip dismisses any open tooltip immediately and opens the modal. Tooltip suppressed on touch devices to avoid the touch-tap-as-hover confusion.

  • Per-row action affordances; no cluster-wide bulk (2026-04-30). Mark-as-Paid, Cancel, Refund are exposed per-row in the modal; there is no cluster-wide "Mark all as Paid" affordance. Rationale: each action has a follow-up dialog with its own fields (reason / amount / processor / note), and applying one input set across N orders has unclear UX semantics. Operators with cluster intent take the per-row path N times. Re-evaluate if a real cluster-wide workflow surfaces.

  • Header billed person ≠ line-item EP / Membership person (2026-04-30). The header surfaces the billed person (Order.buyer.person) — who paid for the order. Line-item linked entities (EP / Membership) carry their own persons via FK. These can differ — a parent buys a registration for a child; an organisation buys bulk membership. v1 surfaces both axes side-by-side without trying to collapse them.

  • Quantity always 1 when EP / Membership is set on a line item (2026-04-30). Line items linked to an entity are always qty=1. Qty > 1 only for non-entity products (raffle, generic items). Qty column always rendered for consistency.

  • PENDING folded into UNPAID for action set (2026-04-30). PENDING and UNPAID share the Mark-as-Paid + Cancel action set. No distinct "Cancel pending payment" action — operator’s intent is the same in both states.

  • Sibling resolution — extend OrderCriteria with line-item filters (2026-04-30). Add lineItemEventParticipantId and lineItemMembershipId filters joining through lineItems.eventParticipant.id / lineItems.membership.id, mirroring the existing lineItemId join. Single round-trip, returns OrderDTOs ready to render. Rejected alternative: querying OrderLineItemCriteria and deriving orders client-side — natural granularity mismatch (modal wants orders, response gives line items), forces dedup, two-call pattern likely needed for full order summary, doesn’t compose with future unpaid only / since date filter additions.

  • Cancel reason — reference-data picker + optional note (2026-04-30). New cancel_reason reference table; Order.cancel_reason_id FK; optional free-text note alongside. Initial seed values (Liquibase): Customer cancelled, Duplicate order, Operator override, Unfit event, Other. Free-text-only would cost less but breaks aggregate reporting on cancellation reasons — see the recon journal’s "Bug E pending revenue at risk" cohort triage.

  • Refund — dedicated POST /api/orders/{id}/refund endpoint; depends on US #354 (2026-04-30). Refund schema (refundAmount + refundDateTime) is part of US #354 (Phase 4 of Feature #350) which is in flight. Dedicated endpoint over PATCH/PUT for explicit operation params + clearer cascade-semantics audit. C06’s refund US has US #354 as predecessor.

  • Cluster status taxonomy on the chip — Paid / Unpaid / Various; cancelled excluded (2026-04-30; chip rendering belongs to the calling screen, captured here for cross-screen coherence). One chip per EP. Status is computed from non-cancelled orders only:

    Cluster shape Chip

    All non-cancelled paid (or single paid order)

    Paid

    All non-cancelled unpaid (or single unpaid order)

    Unpaid

    Mixed

    Various — visual variant: the dominant signal is the unpaid count (e.g. "1 of 3 unpaid")

    Zero non-cancelled orders (incl. all-cancelled case)

    "Add order" affordance replaces the chip — routes to `/orders/new?epId=…

    Cancelled orders are never displayed on the chip’s surface; they reach the modal via the "Show cancelled (N)" toggle.

  • v1 permissions — ROLE_USER read + write; composite security at entity level (2026-04-30). All action affordances gate to ROLE_USER, with org-scoped access enforced by the Order entity’s existing OrganizationalSecured + PersonalDataSecured interfaces. ROLE_FINANCE elevation for refund / mark-paid is a future axis (FU-3).

  • No pagination in v1 (2026-04-30). Line items typically ≤6; sibling orders typically ≤5; refund records bounded; payment history not surfaced in v1. Single-page-load is fine. If real production data ever pushes a section past a comfortable scroll, lazy-load that section.

  • User-facing terminology — "participant" / "member"; data discriminator stays EP / MEMBERSHIP (2026-05-01, design pass). All operator-visible copy says "participant" (not "EP", not "Event Participant") and "member" (not "Membership"). The data discriminator on OrderLineItem remains kind: 'EP' \| 'MEMBERSHIP' — unchanged at the API / DTO / criteria layer. Affects: cancel cascade copy, refund cascade flag copy, cross-refs heading, cluster modal field naming (personName / personContext, not epName / epContext), about-this-screen panel, locked-decisions card. Same convention propagates to E02’s chip cluster modal hand-off.

  • Mark-as-Paid cascade preview is data-driven from line items (2026-05-01, design pass). The dialog reads linked entities off order.lineItems, then renders contextual copy: EP-only → "linked participant <name>"; Membership-only → "linked member <name>"; Mixed → "linked participant X and member Y"; Multiple of one kind → "participants A, B and C"; raffle-only / zero linked entities → cascade card omitted entirely. Single source of truth (the cart) drives the message — no separate dialog-input parameter to keep in sync.

  • Mockup-only canvas annotations strip on hand-off (2026-05-01, design pass). The "Opened with `?action=cancel`" focus banner and the "From `GET /api/cancel-reasons`" hint under the cancel-reason picker were canvas-only annotations to make the contract legible during design discussion; explicitly stripped before the v1 hand-off and must not ship in the implementation. The action is still in the URL — just not announced in the page chrome.

Notes

Design returned 2026-05-01. Bundle archived at design-journal/claude-design/2026-05-01-egSK5m8U/; canonical screen folder at project/C06-order-detail/. Status moved in-design → handoff-ready. Two new design decisions surfaced during the design pass — terminology cleanup ("participant" / "member" on the user-facing surface; data discriminator unchanged) and a data-driven cascade preview pattern in Mark-as-Paid — captured in Design Decisions above. See admin-portal Screen Design Prompt Iteration for the broader handoff workflow.

C06 is the destination for E02's Order Number(s) chip click. The chip’s per-EP cluster-status semantics (Paid / Unpaid / Various; cancelled excluded; "Add order" affordance for zero-order EPs; rich popup-panel hover tooltip; click → modal) are captured under Design Decisions for cross-screen coherence; the chip rendering and tooltip live in E02’s spec under "Order Number(s) column". The implementation Claude Code session must update E02’s v2 prompt accordingly — see the delta-prompt hand-off in 2026-04/admin-portal-screen-design-prompt-iteration.adoc.

Backend prerequisites for v1:

  • OrderCriteria extension (sibling-orders filters) — small change.

  • cancel_reason reference table + Order.cancel_reason_id FK — new Liquibase migration.

  • POST /api/orders/{id}/cancel — new endpoint.

  • POST /api/orders/{id}/mark-paid — new endpoint.

  • POST /api/orders/{id}/refund — new endpoint, predecessor US #354.

Appendix A: Claude Design Prompts

Prompts persisted for audit trail. Most recent first. The active hand-off prompt is the topmost.

v1 — 2026-04-30 — derived from this .adoc

Status: handed off 2026-05-01; design returned in bundle 2026-05-01-egSK5m8U (chat12). 14 artboards covering all five status states, three live action dialogs (Mark Paid / Cancel × 2 cascade variants / Refund), the two E02 invocation surfaces, success / partial-failure result banners, and a 404. Design + spec aligned with two refinements folded back into Design Decisions. Source: this .adoc at :status: in-design.

I'm designing the **Order Detail** screen for the EMS admin portal — a
Spring Boot + Angular SPA admin tool used by event operators (race
directors), tenant admins, and finance staff to manage participants,
events, members, and order/payment data. Visual language matches the
existing portal screens you've designed in this project (C01 structure,
C03 public landing, C04 reassignment dialog, E01 event overview, E05
events control centre, E02 event participants — all handoff-ready or
in-design with v2 prompts). Match fonts, spacing, palette, density.

C06 is the **canonical order-detail surface** for the entire portal.
Every order — event-participant orders, membership orders, future cross-
event purchases — surfaces here. The route is deliberately *not* event-
scoped; orders cross event boundaries. Three invocation surfaces feed
into it: a hover popup panel on calling screens, a modal cluster
overview (also on calling screens), and direct route navigation.

============================================================
Route
============================================================

  /orders/{orderId}                       — view-only landing
  /orders/{orderId}?action=cancel
  /orders/{orderId}?action=mark-paid
  /orders/{orderId}?action=refund
  /orders/{orderId}?action=resend-receipt — auto-opens that action dialog
  /orders/new?epId={id}
  /orders/new?membershipId={id}           — Future (FU-2); v1 stubs only

The action is a **focused mode of the same screen**, not a separate
URL hierarchy. Refresh on `?action=cancel` should reopen the cancel
dialog — that's the desired behaviour. Sub-routes (`/orders/{id}/cancel`)
would imply different pages and are explicitly rejected.

============================================================
Invocation surfaces (rendered on caller screens — NOT on C06 itself)
============================================================

These three surfaces summon C06 from a caller. They're spec'd here for
cross-screen coherence — the actual rendering belongs to the caller's
template (E02 owns the chip + hover; this prompt describes what the
chip's modal contains).

[A] **Hover tooltip — rich popup panel** (desktop only; suppressed on
    touch). Triggered on chip mouseover. Shows a popup panel (NOT a
    text-only tooltip) listing each non-cancelled order as a row:
    order number, transaction date, status pill, total amount.
    Bottom of the panel: a small info-icon hint "click for actions".
    Click on the chip dismisses any open tooltip immediately and
    opens the modal.

[B] **Modal — cluster overview** (chip click). Always opens, even for
    a single-order EP (consistency over single-row optimisation).
    Lists each non-cancelled order as a row with:
      - Order number (left).
      - Transaction date.
      - Status pill (Paid / Unpaid / Pending / Cancelled — Cancelled
        rows hidden by default).
      - Total amount (right-aligned).
      - **Per-row action buttons**, contextual on row status:
          UNPAID / PENDING → "Mark as Paid", "Cancel"
          PAID            → "Refund" (visible only when ≥1 paid order
                                       in the cluster — gates entire
                                       action class)
      - **View order** link → /orders/{orderId} (no action focus).
    Bottom of modal: "Show cancelled (N)" toggle (default off — when
    on, cancelled orders render inline with greyed treatment).
    Action button click does NOT execute the mutation — it navigates
    to /orders/{orderId}?action=<action>. The full page opens with
    the action dialog auto-focused.

[C] **Direct route** — operators arriving via deep-link, share-link,
    or external integration (Slack / email / support tickets). Lands
    straight on the full page; no modal needed.

============================================================
Page anatomy (top to bottom — full page at /orders/{orderId})
============================================================

[1] Sticky header band (sticks on scroll)
   - Order number (left, prominent).
   - Status pill (Paid / Unpaid / Pending / Cancelled / Refunded).
   - Transaction date.
   - Billed person — name + linked `mailto:` (if email known) +
     linked `tel:` (if phone known). Note: this is `Order.buyer.person`
     — who PAID for the order. Line-item linked entities (EP /
     Membership) carry their own persons via FK and can differ
     (e.g., parent buys for child; org buys bulk membership).
     Surface both axes; don't try to collapse them.
   - Total amount.
   - Payment processor name.
   - WooCommerce external link (when `external_id` present) — small
     icon + "View on WooCommerce" link.
   - Tenant name.
   - **Status-conditional action buttons** (top-right of header band):
       UNPAID / PENDING → "Mark as Paid", "Cancel"
       PAID            → "Refund", "Resend receipt" (FU-1 — disabled
                          v1 with "Coming soon" tooltip)
       CANCELLED       → none
       REFUNDED        → none
     All buttons open their respective dialogs. The dialog also opens
     automatically if the URL carries ?action=<action>.

[2] CANCELLED-order banner (when status = CANCELLED, replaces action buttons)
   - "Cancelled on YYYY-MM-DD by *X* — *<reason>*"
   - Read-only across the whole page. No mutation affordances rendered.

[3] Line items section (collapsible — default expanded)
   - One row per `OrderLineItem`:
       - Product name.
       - Linked entity (EP name with race / Membership name with
         period; "—" when no linked entity, e.g. raffle items).
         Linked-entity links route to E09 (EP) / future Membership
         detail (membership).
       - Quantity. **Always 1 when EP / Membership is linked**;
         qty > 1 only for non-entity products. Always render the
         column for consistency.
       - Unit price.
       - Discount (when > 0; blank when 0).
       - Line total.
   - Footer row: subtotal / total breakdown.

[4] Refunds & cancellations section (collapsible — default collapsed
    if empty)
   - One row per refund event: amount, date, mode (back to original
     PaymentProcessor vs office / manual), reason, deactivate-linked-
     entities flag (for audit visibility), operator note.
   - For CANCELLED orders: cancellation record (date, reason, note).
   - Section absent (not rendered) when both lists are empty AND
     order is not CANCELLED.

[5] Cross-references panel (collapsible — default expanded)
   - **Sibling orders** — other orders for the same EP / Membership
     in the line items, resolved via OrderCriteria extension
     (`lineItemEventParticipantId.equals` / `lineItemMembershipId.
     equals`). Cancelled siblings hidden by default with a "Show
     cancelled (N)" toggle.
   - Linked EP / Membership entities — cards with name + a "View"
     link to their respective detail screens (E09 for EP; future
     Membership detail).

[6] Payment history section (FU-6 — Future)
   - Section ABSENT in v1 (DTO doesn't carry payment events yet).
   - When backend sync brings these forward (WooCommerce-side
     transaction history), this section renders between Line Items
     and Refunds — timeline of initiation / gateway redirect /
     callback / status transitions.
   - Don't draw a placeholder in v1.

============================================================
Action dialogs (modal-on-page; mirror the C04 pattern)
============================================================

All action flows are modal-on-page dialogs reached via the header
button (per current status) OR via ?action=<action> query. Same
dialog, two entry paths.

(a) **Mark as Paid** (UNPAID, PENDING)
    Dialog captures:
      - Payment method (Cash / EFT / Other) — radios.
      - Payment processor (selector — typically the Office / Manual
        processor for cash).
      - Payment reference (string; useful for EFT reference numbers).
      - Payment amount (defaults to order total; editable for partial).
      - Optional operator note (free-text).
    Submit → POST /api/orders/{id}/mark-paid → status transitions
    to PAID, payment fields persisted, downstream membership / EP
    activation cascade fires.

(b) **Cancel** (UNPAID, PENDING)
    Dialog opens with a **conditional info box at the top** when
    either condition holds (both can be visible simultaneously):

      Multi-line cascade (when order has > 1 line items):
        "This order covers more than one membership/participant.
         Cancelling will cancel all of them."

      Sole-paid-order cascade (when affected person has no other
      paid orders):
        "This is the person's only paid order. Cancelling could
         deactivate their participation/membership."

    Dialog captures:
      - Cancel reason (reference-data picker — values come from
        GET /api/cancel-reasons; initial seed: Customer cancelled,
        Duplicate order, Operator override, Unfit event, Other).
      - Optional free-text note.
    Submit → POST /api/orders/{id}/cancel → status transitions to
    CANCELLED, cancelDateTime + cancel_reason_id persisted.

(c) **Refund** (PAID)
    Dialog captures:
      - Refund amount (full or partial; defaults to order total or
        paid total).
      - Refund mode (back to original PaymentProcessor vs office /
        manual) — radios.
      - Reason (reference-data picker; same source as Cancel).
      - Deactivate-linked-entities flag (default TRUE — surfaces
        the cascade explicitly to the operator).
    Submit → POST /api/orders/{id}/refund → refundAmount +
    refundDateTime persisted, downstream cascade per the flag.
    Dependency: US #354 (refund schema + WC plugin sync — in flight).

(d) **Resend receipt** (PAID — FU-1, Future)
    v1 stub: button visually present but disabled with a "Coming
    soon" tooltip. Do NOT route to a placeholder URL. Dialog design
    can be sketched but not built.

(e) **Add order** (FU-2, Future)
    Routed entry only — no header button on C06. Reached via
    /orders/new?epId=… or ?membershipId=…. Full create flow with
    line-item authoring. v1 stubs the entry point on calling screens
    only (E02's "Add order" cell affordance for zero-order EPs).

============================================================
Action flow — submit / success / failure UX
============================================================

  Success: dialog closes, page reloads order data, success banner
           surfaces the operation summary (status delta + cascade
           summary, e.g. "Order #123 cancelled. 3 EPs deactivated.").
           Banner auto-dismisses after ~5 seconds.

  Failure: dialog stays open with inline error message; operator
           may retry or cancel.

  Partial-failure (AF-5): operation succeeds order-side but downstream
                          cascade fails (membership activation, WC
                          sync, receipt send). Surface a warning
                          banner explaining the partial state and
                          offering retry where possible. Order-side
                          mutation NOT rolled back.

============================================================
Empty + edge states
============================================================

- 404 (order not found / no access): full-page error with a back
  link to caller (use document.referrer when present; fall back to
  /events).
- AF-2: Refund button DISABLED when the order has no paid line items
  or has already been fully refunded. Tooltip explains.
- AF-3: Action affordance disabled with permission-explaining tooltip
  when the operator lacks the required role.
- Long lists (line items > 10, refunds > 5): no pagination in v1.
  The collapsible section header carries the count; if real
  production data ever pushes a section past a comfortable scroll,
  we lazy-load that section in a follow-up.

============================================================
"About this screen" info panel (C05/E06/C04 cross-cutting pattern)
============================================================

Persistent collapsible right-rail panel near the top of the page
with a short purpose paragraph + bulleted operational details:

  - What this screen shows (order detail; canonical order surface).
  - What the action buttons do at a high level (one line each).
  - Where mutations route (URL convention; ?action=).
  - Reference-data sources (cancel reasons; processors).

Default expanded on first open per browser, collapsed thereafter
(state persisted in localStorage). Same pattern applied symmetrically
on C04 / C05 / E06 — promote later to UI Design Principles.

============================================================
Style consistency
============================================================

- Sticky header band feel: similar weight to E01's title band.
- Status pills: same shape / saturation as the chips on E02.
- Action dialogs: same modal width / header / footer as C04.
- Section collapse affordance: chevron icon left of the heading,
  matches existing design system conventions.
- Tables (line items, refunds, sibling orders): plain-vanilla
  Bootstrap table density; no zebra striping on these counts.

============================================================
Output
============================================================

- HTML/JSX + matching styles for C06 covering:
  - The full page at /orders/{orderId} for each status state
    (Paid, Unpaid, Pending, Cancelled, Refunded).
  - The five action dialogs (Mark as Paid, Cancel, Refund —
    Resend Receipt and Add Order are FU stubs and only need a
    button-disabled + tooltip rendering).
  - The chip-summon modal (cluster overview) referenced from E02.
  - The hover popup panel referenced from E02.
- Screen README explaining:
  - The route + ?action= convention (canonical full page;
    sub-routes rejected).
  - The three invocation surfaces and how they relate.
  - Action flow ergonomics (modal-on-page; cascade banners;
    partial-failure handling).
  - The Cancel dialog's conditional info-box logic.
  - The "About this screen" info-panel pattern (cross-cutting
    candidate).
- Note any moments where a calling screen (E02) needs to update
  to render the chip / hover / modal correctly — that update lives
  in E02's delta prompt, but flag here if you spot ambiguity worth
  resolving before E02 lands its delta.