Specification: Direct Email Send (Email Orders)
Overview
Section titled “Overview”This is the driving document for implementing direct email sending in
arda-frontend-app (PDEV-969). It turns the order-queue EmailPanel from a
copy-only composer into a real sender, behind a per-tenant capability toggle.
Read alongside:
requirements.md— REQ-ES-* functional behavior.verification.md— requirement ↔ test traceability.design.md— cross-system design, contract, security (DQ-001–011).ui-implementation-analysis.md— codebase gap analysis (G1–G13) and the target module decomposition. This serves as the Modification-project analysis; no separateanalysis.mdis produced.implementation-changes.md— per-file changes.
Project type: Modification (single repo, arda-frontend-app). The complex
design/decision phases are complete; this is the project-planning output.
In scope: the capability toggle; the redesigned composer (recipients,
subject, editable body, note); HTML/plain-text split-button Send; dual-format
Copy; client validation; shared-BFF body sanitization; the config-status and
send BFF routes + a reusable backend proxy; MSW handlers; unit + E2E tests.
Out of scope: recipient pre-population (separate ticket — “Integrate Email Orders with Supplier Data”); send authorization/permission gating (handled separately); backend body hardening (PDEV-976); PO-Line emails (PDEV-970); config provisioning (PDEV-971); attachments/BCC.
Conventions (must hold)
Section titled “Conventions (must hold)”Per ui-implementation-analysis.md §3.0/§4: the reuse-before-build procedure;
ux-prototype component rules; no raw/inline styles (semantic tokens / cn();
new styles escalate to Sebrand Warren @nail60); SPA / BFF / shared tier
separation (eslint-enforced); co-location of feature-specific code; canary-first
(Button/IconButton/SplitButton confirmed present); ≤ ~500-line files;
strict TypeScript; coverage 84/84/81/72; no content/PII logging in BFF routes.
Phased Implementation Plan
Section titled “Phased Implementation Plan”Phases map to reviewable PRs (see PR and Delivery Strategy). All work lands on the feature branch and is reviewed via PR before merge — no per-file approval gate is needed to edit existing files. Each phase is a coherent, independently mergeable unit.
Phase 1 — Foundation (contracts + mocks)
Section titled “Phase 1 — Foundation (contracts + mocks)”lib/email/email-constants.ts<<shared>>—PROCUREMENT_EMAIL_SLUG_TOKEN, subject/greeting/sign-off default builders.types/email.ts<<shared>>—EmailJobInput,EmailRecipients,EmailConfigStatus, configuration-query result.- MSW handlers for
config-statusandsend(extendmocks/handlers/email.ts).
Acceptance: typecheck + lint clean; MSW returns the new shapes in mock mode.
Phase 2 — Pure utilities (TDD)
Section titled “Phase 2 — Pure utilities (TDD)”Self-contained, independently tested; consumers (BFF, UI) come later.
lib/email/validate-email-order.ts<<shared>>— ≥1 To, RFC addresses, non-empty subject, no CR/LF / control chars.lib/email/sanitize-email-html.ts<<BFF>>— thinsanitize-htmlwrapper (server-only): allow-list tags/attributes;http/https/mailtolinks only; drops scripts/styles/iframes/images/event handlers. Returns the sanitized HTML (sanitize-and-send; no reject — DQ-010 revised R7).lib/email/compose-email-html.ts<<shared>>— inline-styledhtmlBody+ plaintextBodyfrom structured content; escapes user content. Shared tier (DQ-020): imported by the BFF (authoritative compose on send) and the SPA (live preview). Includes the per-line Notes rendering (REQ-ES-062).
Acceptance: REQ-ES-040/041/042 unit tests green.
Phase 3 — BFF + capability toggle
Section titled “Phase 3 — BFF + capability toggle”- One explicit proxy class per backend email endpoint (
<<BFF>>), each in its own file underlib/arda/email/, owning that endpoint’s URL, typed request/response, and theAuthorization/X-Tenant-Id/X-Author/X-oidc-subject/Idempotency-Keyheaders +cache:'no-store':EmailConfigurationProxy—POST /v1/shop-access/email/configuration/query.EmailJobProxy—POST /v1/shop-access/email/job.
app/api/arda/email/config-status/route.ts<<BFF>>— viaEmailConfigurationProxy; applies the Operational + slug rule. Called on composer open; no caching in the initial implementation (DQ-002, revised) — so noemailConfigSlice/thunks/selectors and no sign-in/tenant-switch dispatch.app/api/arda/email/send/route.ts<<BFF>>— accepts the structured payload, composes the body viacompose-email-html(DQ-020) →sanitize-email-html(sanitize-and-send backstop) →EmailJobProxy.
Acceptance: BFF unit tests (REQ-ES-002/022/023/041/042/043) — incl. the route composing from structured input and config-status resolving on open.
Phase 4 — Compose the UI module
Section titled “Phase 4 — Compose the UI module”- Create
components/email-order-panel/(orchestrator +email-body-preview+ co-locateduse-email-composer). Run the §3.0 reuse search forrecipient-chips/editable-text(general-use candidates →src/components/, added to the promotion ticket; else feature-local), designing the shell as a reusable seam (recipient fields / toggle / footer reusable; Body content is the feature slot — REQ-ES-060). Apply semantic tokens + canaryButton/IconButton/SplitButton. - Editable fields use the grid inline-edit pattern (click-to-edit, no pencil badge — REQ-ES-061); the items table includes the editable per-line Notes column alongside the retained global Note (REQ-ES-062).
Acceptance: the new panel renders full vs restricted; REQ-ES-010/011/012/014/015/060/061/062.
Phase 5 — Send path + cut-over
Section titled “Phase 5 — Send path + cut-over”use-email-send(format → body; accept-after-send); wire the toggle; mount the new panel fromorder-queue; retire the/api/email/send-orderstub.- Persist the last-used send format as the session default (REQ-ES-063): the
composer opens on the last preview after the first send (Redux/
sessionStorage).
Acceptance: REQ-ES-020/021/024/050/063; restricted mode = today’s copy-only.
Phase 6 — E2E
Section titled “Phase 6 — E2E”e2e/pages/email-order-panel.page.ts+ the three scenarios inverification.md.
Acceptance: E2E green in mock mode with Istanbul coverage.
Phase 7 — Cleanup & handoff
Section titled “Phase 7 — Cleanup & handoff”- Remove Spanish comments and the dead
deliveryAddresswiring; confirm file sizes ≤ target; compile the ux-prototype promotion ticket (assignedsebrand@arda.cards) listing promotion candidates; runmake ci-replicate.
Acceptance: make ci-replicate green; coverage thresholds met.
PR and Delivery Strategy
Section titled “PR and Delivery Strategy”Recommendation: a stack of 5 small, ordered PRs — each independently
mergeable to main without leaving an unstable state, and collapsible into one
delivery PR.
Why incremental merges to main are safe. The feature is dark-launched
behind the capability toggle (REQ-ES-001/002): the direct-send form appears
only for a tenant with an Operational procurement EmailConfiguration, and
provisioning those configs is a separate effort (PDEV-971). Until then every
tenant sees the restricted = copy-only form, which is byte-for-byte today’s
behavior. So PR 1–4 are purely additive (new, not-yet-consumed files) and PR 5
(the cut-over) preserves current behavior for every tenant. Nothing destabilizes
main.
| PR | Phases | Contents | Safe because |
|---|---|---|---|
| PR1 | 1 | constants, types, MSW handlers | additive; no runtime path |
| PR2 | 2 | validate / sanitize / compose utilities + tests | pure functions, unconsumed |
| PR3 | 3 | per-endpoint proxies, config-status + send routes, toggle slice/thunk | new routes unconsumed by UI; toggle only caches a flag |
| PR4 | 4 | email-order-panel/ + subcomponents + composer + tests | new components, not yet mounted |
| PR5 | 5–7 | use-email-send, cut-over (mount panel, retire stub), E2E, cleanup | restricted/copy-only == today; toggle off in prod (PDEV-971 pending) |
Ordering: utilities (PR2) precede their consumers (PR3 BFF, PR4 UI); the cut-over (PR5) is last. Each PR targets the previous branch in the stack (or all target a shared integration branch).
Flexibility (as required):
- Merge in order: land PR1 →
main, retarget PR2 →main, etc. (gh-stack rebase). Each is green and non-disruptive on its own. - Single delivery PR: point the stack at the integration branch
(
jmpicnic/email-order-sending-fe) and open one PR from it tomain.
Coupling note: the cut-over (PR5) should be functionally complete before any tenant is provisioned via PDEV-971, so the toggle never turns on against a half-built UI. Provisioning is gated and separate, so this is easily sequenced.
If fewer, larger reviews are preferred, PR1–PR3 (all additive backend/utilities) can be combined into one, giving a 3-PR shape (backend+utils / UI / cut-over) with the same safety properties.
Incremental Commit Workflow
Section titled “Incremental Commit Workflow”Implementation within a PR follows an incremental-commit cadence — a bounded look-ahead pipeline that keeps the agent close to the reviewer. Increments are not pre-listed: each is the smallest self-contained unit that stays green (compiles, lints, touched tests pass) and is therefore always safe to commit and push.
Per-increment gate: tsc --noEmit + eslint + Jest for the touched files
(fast gate). The full make ci-replicate runs on the PR / CI, not per increment.
Cadence:
- Implement increment 1 → fast gate → commit, push, open the PR; run the touched-area tests in the background while CI runs.
- In parallel, while the reviewer reviews the PR:
- implement the next increment and commit locally (do not push);
- if no go-signal yet, implement the following increment and stop before committing (held in the working tree);
- buffer ceiling = 2 (one committed-unpushed + one uncommitted-held); when full with no signal, stop and report state.
- The reviewer leaves comments (in the PR and/or directly) and sends a direct go-signal — the cue for the agent to read the PR comments and proceed.
- On the go-signal: address review feedback first (fix-forward — pushed history is never rewritten; the unpushed buffer is rebased/amended to fold feedback in), then push the oldest buffered commit (FIFO, one per signal), promote the held increment to a local commit, and begin the next increment uncommitted.
- Repeat 2–4 until the PR is complete.
Push discipline: the agent pushes only (a) the PR-creating increment and (b) one increment per go-signal — never otherwise.
State reporting: every turn, report the pipeline state —
pushed HEAD · N local commits ahead · uncommitted-held (yes/no).
Why it’s safe: each increment is independently green and the PR stays
mergeable throughout (the feature is dark behind the capability toggle), so a
pushed mid-PR increment never destabilizes main. The workflow assumes a
single implementing agent and a single intra-PR reviewer coordinating
closely; it is per-PR and restarts when the next PR in the stack opens.
Open Questions and Decisions
Section titled “Open Questions and Decisions”| # | Question | Options | Recommendation | Decision |
|---|---|---|---|---|
| D1 | Recipient pre-population | empty / prefill from supplier | empty for V1 | Resolved — To/Cc empty with placeholder cues; prefill = separate ticket |
| D2 | Authorization to send | existing access / new permission | — | Deferred — handled separately; not in this plan |
| D3 | Reply-To policy | free-form / constrained | free-form for V1 | Resolved — free-form (editable From → Reply-To) |
| D4 | Rate / volume limits | FE / backend | backend | Resolved — backend, PDEV-976 |
| OQ-2 | BFF route namespace | /api/email/* / /api/arda/email/* | proxy convention | Resolved — /api/arda/email/*; retire old stub |
| OQ-8 | Toggle cache durability | session / redux-persist / none | none | Resolved — no cache in the initial implementation; resolved on composer open (DQ-002 revised) |
| OQ-10 | File naming | kebab / Pascal | kebab per ux-prototype rules | Resolved — kebab-case for new email-order-panel/ files |
| OQ-11 | Validation placement | SPA-only / shared | shared | Resolved — shared; BFF re-validates |
No open blockers remain. Full rationale:
decision-log.md.
Worktree Strategy
Section titled “Worktree Strategy”Single agent, single repo, sequential writes — no parallel worktrees needed.
Implementation happens in the existing arda-frontend-app worktree
(projects/email-integration-worktrees/phase-6/arda-frontend-app) on branch
jmpicnic/email-order-sending-fe; documentation changes in the paired
documentation worktree. One PR per repo.
Acceptance Criteria Checklist
Section titled “Acceptance Criteria Checklist”- Toggle drives full vs restricted form, resolved on composer open with no cache (REQ-ES-001–003).
- To/Cc empty with placeholders; From→Reply-To; editable Subject default (REQ-ES-011/012/013).
- Editable body + note; Revert all (dirty-gated); ⌘Z undo (REQ-ES-010/014/015).
- Send split button: default HTML, menu HTML/plain text; ⌘↵ default (REQ-ES-020/021).
- Send builds
EmailJobInputwithX-Tenant-Id+ stableIdempotency-Key; accept-after-send on success only (REQ-ES-022/023/024). - Copy writes both
text/htmlandtext/plain(REQ-ES-030). - Validation (≥1 To, RFC, non-empty subject, no CR/LF) blocks send (REQ-ES-040).
- BFF sanitizes the composed body with
sanitize-html(sanitize-and-send); no content logging (REQ-ES-041/042/043). - Outcomes via toast; backend errors surfaced (REQ-ES-050).
- No raw styles; canary components; tier separation; ≤500-line files; coverage 84/84/81/72.
- Unit + E2E per
verification.md;make ci-replicategreen.
Copyright: © Arda Systems 2025-2026, All rights reserved