Implementation Changes: Direct Email Send (Email Orders)
Purpose
Section titled “Purpose”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.
Files to create
Section titled “Files to create”| Phase | File | Tier | Purpose |
|---|---|---|---|
| 1 | src/lib/email/email-constants.ts | shared | PROCUREMENT_EMAIL_SLUG_TOKEN = "procurement"; subject/greeting/sign-off default builders |
| 1 | src/types/email.ts | shared | EmailJobInput, EmailRecipients, EmailConfigStatus, configuration-query result types |
| 2 | src/lib/email/validate-email-order.ts | shared | ≥1 To, RFC To/Cc/Reply-To, non-empty subject, no CR/LF/control chars → structured errors |
| 2 | src/lib/email/sanitize-email-html.ts | BFF | thin 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) |
| 2 | src/lib/email/compose-email-html.ts | shared | inline-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 |
| 3 | src/lib/arda/email/configuration-proxy.ts | BFF | EmailConfigurationProxy — POST /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 |
| 3 | src/lib/arda/email/job-proxy.ts | BFF | EmailJobProxy — POST /job; same header set plus Idempotency-Key; typed EmailJobInput → outcome |
| 3 | src/app/api/arda/email/config-status/route.ts | BFF | GET → EmailConfigurationProxy → select Operational + slug-token config → { directSendEnabled, configurationEId, senderAddress } |
| 3 | src/app/api/arda/email/send/route.ts | BFF | POST structured payload → compose-email-html (DQ-020) → sanitize-email-html (sanitize-and-send) → build EmailJobInput → EmailJobProxy → 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) |
| 4 | src/components/email-order-panel/email-order-panel.tsx | SPA | orchestrator: full/restricted layout; footer (Cancel / Revert all / Copy / Send SplitButton) |
| 4 | src/components/email-order-panel/email-body-preview.tsx | SPA | on-screen body card |
| 4 | src/components/email-order-panel/use-email-composer.ts | SPA | composer state, isDirty, revertAll, address undo |
| 4 | src/components/<recipient-chips>/, src/components/<editable-text>/ | SPA | only 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 |
| 5 | src/components/email-order-panel/use-email-send.ts | SPA | format → body; validate → compose → send → accept-after-send → toast; stable Idempotency-Key per attempt |
| 5 | src/lib/ardaClient.ts additions | SPA | getEmailConfigStatus(), sendEmailOrder() (via getBffAuthHeaders + handleApiResponse) — new functions in an existing file (modify) |
| 6 | e2e/pages/email-order-panel.page.ts + spec | — | page object + the 3 scenarios in verification.md |
| 1–6 | *.test.ts(x) co-located | — | per verification.md |
Files to modify (reviewed via PR)
Section titled “Files to modify (reviewed via PR)”| Phase | File | Change |
|---|---|---|
| 1 | src/mocks/handlers/email.ts + handlers/index.ts | add/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 |
| 5 | src/store (session-default format) | Persist the last send format as the session default (REQ-ES-063, DQ-019) — small UI/session slice or sessionStorage |
| 4 | src/app/globals.css | only if a genuinely new semantic token is required (else none; escalate new styles to @nail60) |
| 5 | src/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 |
| 5 | src/components/EmailPanel.tsx | replaced by email-order-panel/ — remove once call sites are migrated |
| 5 | src/app/api/email/send-order/route.ts + route.test.ts | remove the stub (superseded by api/arda/email/send) |
| 7 | src/components/email-order-panel/* | remove Spanish comments; drop dead deliveryAddress wiring |
- Idempotency-Key is minted once per Send attempt in
use-email-sendand reused on retry; the BFF forwards it (does not regenerate). - Format mapping: HTML send ⇒
htmlBody+textBodyalternate; plain-text send ⇒textBodyonly. configurationEIdcomes from the cached toggle; the backend must still verify tenant ownership (PDEV-976).compose-email-html(shared, escapes viaescape-html) andsanitize-email-html(BFF, wrapssanitize-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-textcomponent 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-composerholds alineNotes: string[];compose-email-htmlrenders it as a table column (HTML) and an indentedNote: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.
uiSliceoremailConfigSlice+sessionStorage);use-email-composerinitializes the Body preview to it (falling back to Compose on first use).
Copyright: © Arda Systems 2025-2026, All rights reserved