Skip to content

Implementation Changes: Direct Email Send (Email Orders)

The file-level change list for the specification.md phases. Paths are relative to arda-frontend-app/. Tier tags: <<SPA>> browser, <<BFF>> server, <<shared>> both (import-safe for each). Design rationale per file is in ui-implementation-analysis.md §3.

PhaseFileTierPurpose
1src/lib/email/email-constants.tssharedPROCUREMENT_EMAIL_SLUG_TOKEN = "procurement"; subject/greeting/sign-off default builders
1src/types/email.tssharedEmailJobInput, EmailRecipients, EmailConfigStatus, configuration-query result types
2src/lib/email/validate-email-order.tsshared≥1 To, RFC To/Cc/Reply-To, non-empty subject, no CR/LF/control chars → structured errors
2src/lib/email/sanitize-email-html.tsBFFthin sanitize-html wrapper (server-only) — allow-list tags/attributes; http/https/mailto only; drops scripts/styles/iframes/images/event handlers; returns sanitized HTML (sanitize-and-send, no reject — DQ-010 R7)
2src/lib/email/compose-email-html.tssharedinline-styled htmlBody + plain textBody from structured content (incl. per-line Notes); escapes user content. Imported by the BFF (authoritative compose) and the SPA (live preview) — DQ-020
3src/lib/arda/email/configuration-proxy.tsBFFEmailConfigurationProxyPOST /configuration/query; owns the URL, typed req/resp, ARDA_API_KEY + X-Tenant-Id/X-Author/X-oidc-subject headers, cache:'no-store', response parsing
3src/lib/arda/email/job-proxy.tsBFFEmailJobProxyPOST /job; same header set plus Idempotency-Key; typed EmailJobInput → outcome
3src/app/api/arda/email/config-status/route.tsBFFGET → EmailConfigurationProxy → select Operational + slug-token config → { directSendEnabled, configurationEId, senderAddress }
3src/app/api/arda/email/send/route.tsBFFPOST structured payloadcompose-email-html (DQ-020) → sanitize-email-html (sanitize-and-send) → build EmailJobInputEmailJobProxy → map outcome; never logs recipients/body
3(dropped — DQ-002 revised)No emailConfigSlice / emailConfigThunks / selectors: the composer fetches config-status on open via a small hook; no caching (initial implementation)
4src/components/email-order-panel/email-order-panel.tsxSPAorchestrator: full/restricted layout; footer (Cancel / Revert all / Copy / Send SplitButton)
4src/components/email-order-panel/email-body-preview.tsxSPAon-screen body card
4src/components/email-order-panel/use-email-composer.tsSPAcomposer state, isDirty, revertAll, address undo
4src/components/<recipient-chips>/, src/components/<editable-text>/SPAonly if the §3.0 reuse search confirms no canary equivalent and they are general-use candidates (else feature-local under email-order-panel/); add to the promotion ticket
5src/components/email-order-panel/use-email-send.tsSPAformat → body; validate → compose → send → accept-after-send → toast; stable Idempotency-Key per attempt
5src/lib/ardaClient.ts additionsSPAgetEmailConfigStatus(), sendEmailOrder() (via getBffAuthHeaders + handleApiResponse) — new functions in an existing file (modify)
6e2e/pages/email-order-panel.page.ts + specpage object + the 3 scenarios in verification.md
1–6*.test.ts(x) co-locatedper verification.md
PhaseFileChange
1src/mocks/handlers/email.ts + handlers/index.tsadd/register config-status + send handlers returning the new shapes
3(none — DQ-002 revised)No sign-in / tenant-switch dispatch and no rootReducer registration — config-status is fetched on composer open
5src/store (session-default format)Persist the last send format as the session default (REQ-ES-063, DQ-019) — small UI/session slice or sessionStorage
4src/app/globals.cssonly if a genuinely new semantic token is required (else none; escalate new styles to @nail60)
5src/app/order-queue/OrderQueuePanels.tsx (+ OrderQueueGroupedView.tsx, orderQueueHandlers.ts)mount EmailOrderPanel in place of EmailPanel; wire the toggle; reuse the existing accept-after-action handler
5src/components/EmailPanel.tsxreplaced by email-order-panel/ — remove once call sites are migrated
5src/app/api/email/send-order/route.ts + route.test.tsremove the stub (superseded by api/arda/email/send)
7src/components/email-order-panel/*remove Spanish comments; drop dead deliveryAddress wiring
  • Idempotency-Key is minted once per Send attempt in use-email-send and reused on retry; the BFF forwards it (does not regenerate).
  • Format mapping: HTML send ⇒ htmlBody + textBody alternate; plain-text send ⇒ textBody only.
  • configurationEId comes from the cached toggle; the backend must still verify tenant ownership (PDEV-976).
  • compose-email-html (shared, escapes via escape-html) and sanitize-email-html (BFF, wraps sanitize-html) are distinct: the BFF composes the authoritative body then runs the sanitizer as a defense-in-depth backstop (sanitize-and-send). New deps: sanitize-html, escape-html (+ @types).

Round 5 stakeholder additions (DQ-016–019)

Section titled “Round 5 stakeholder additions (DQ-016–019)”
  • Reusable seam (REQ-ES-060, DQ-016): email-order-panel/ exposes the shell (recipient fields, Body toggle, footer/send controls) with the Body content as a slot, so invitations/other email features can reuse it. Reuse candidates (recipient-chips, editable-text) follow §3.0 + the promotion ticket.
  • Grid inline-edit (REQ-ES-061, DQ-017): the shared editable-text component is click-to-edit with a hover highlight (no pencil badge), matching the AG-grid cells; keep per-field revert + global Revert all / ⌘Z.
  • Per-line Notes (REQ-ES-062, DQ-018): use-email-composer holds a lineNotes: string[]; compose-email-html renders it as a table column (HTML) and an indented Note: sub-line (plain text). The global Note field is retained.
  • Session-default format (REQ-ES-063, DQ-019): persist the last send format in a small slice of client session state (e.g. uiSlice or emailConfigSlice + sessionStorage); use-email-composer initializes the Body preview to it (falling back to Compose on first use).