ClientsFlow · Email-System Overhaul · Workstream W1
FINAL · 2026-07-12 — owner comment round folded in (comments override round-3 wherever they conflict). The target behavior after W1 ships — every send made provably match the locked auto-send whitelist, every send-click idempotent, every send outcome honestly recorded (pending → accepted → failed) so "the screen said sent" can never again mean "nothing left the building." Walked click-by-click on the staging dashboard. Awaiting owner signature.
accepted (relay took it), never delivered; pre-call reminders and booking confirmations were never wired to the global gate and must stay that way; W1 owns the send path only — W2 owns how the ledger + frozen rows are rendered in touchpoint history; W0 owns the alarm/banner plumbing the failure surfaces ride on. Operator UI strings are English (Hungarian appears only inside client-facing email copy).| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás clicks "Send Emails" on a lead card | The CURRENT modal — unchanged (owner: the current UI is nice, keep it exactly as-is) — opens showing the copy for EVERY email in the sequence, most fixed templates, a few AI-filled, each with its scheduled send-day. NOTHING has been sent yet | Copy: full body of each step incl. resolved greeting + canonical sign-off (client-facing copy stays Hungarian: "Üdv, Sarudi Mátyás…") · Look: the existing stacked preview cards, one per step, Send button at the bottom — no redesign · Where: modal over the card |
Copy generated for all steps and held in the modal; no ledger row yet; no network send | Must NOT send anything on modal-open; must NOT show raw {{greeting}}/{{token}} placeholders; must NOT show only the first email; must NOT restyle or restructure the existing modal |
No touchpoint yet — previewing is not a send. History rows appear only when the background send actually completes (step 3). |
| 2 | Mátyás clicks Send | The modal closes immediately — he is free to do other work at once. No spinner-modal, no waiting screen. The board is fully usable | Copy: — · Look: modal gone on click; board interactive · Where: board | The send batch is handed to a reliable background worker BEFORE the modal closes: a pending ledger row for step 0 + all remaining steps stored frozen (subject + body + send-day exactly as previewed); client single-flight absorbs any second click |
Must NOT block the owner in a spinner modal; must NOT lose the batch if he navigates away or closes the tab right after; must NOT re-generate/alter the copy at send time — what he saw is what freezes | Still no committed "sent" row — the pending ledger entry is internal until the background send resolves. No claim before proof. |
| 3 | (Automatic, seconds later) the background worker completes the first send | Without any owner action, the card gains a 📤 sent row in its history and the clockwise-circling dashed border (active sequence). No toast is required — the history row + border ARE the proof | Copy: history row "First sequence email sent" + subject · Look: dashed circling border on the card · Where: board card + its history panel | Step 0 ledger row flips pending → accepted with the relay's message id; steps 1–3 sit as frozen scheduled rows; the arm is idempotent — re-arming is a logged no-op |
Must NOT mark accepted without a real 2xx + message id from the relay; must NOT double-send step 0; the ledger must NOT record delivered; the email must actually arrive in the test inbox |
NEW ENTRY Type: Email sent · icon: 📤 · label: "First sequence email sent" · desc: subject line + "3 more scheduled" · timestamp: send-completion time · by: operator. The 3 frozen future rows render as scheduled (W2 owns the styling). |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás clicks Send twice in quick succession | The modal closes on the first click; the second click lands on nothing. Exactly one email goes out in the background | Copy: — · Look: one modal-close, no double anything · Where: board | Client single-flight swallows click 2; even if a second request reaches the server, the deterministic idempotency key (deal · category · step) makes it a no-op via put-if-absent — one ledger row, one send | Must NOT fire two sends; must NOT create two ledger rows for the same step; must NOT deliver two copies to the test inbox | Exactly ONE 📤 "Email sent" entry — never a duplicate pair. A duplicated history row is itself the visible symptom of a double-send bug. |
| 2 | Mátyás re-opens "Send Emails" on the already-armed deal and tries to send again | No second batch goes out — the modal shows the sequence is already running and Send is disabled | Copy: notice "This sequence is already running" · Look: already-armed state, Send disabled · Where: re-opened modal | Idempotency key already present for step 0; arm() sees an armed deal → logged no-op; frozen future rows untouched | Must NOT re-arm or re-schedule; must NOT reset the frozen copy or send-days; must NOT double-fire step 0 | No new entry — the re-submit produces nothing. The original 📤 entry stands alone. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | (Automatic) the background send is rejected by the relay | No fake success anywhere — the card's history gains a ⛔ failed row with a plain-language reason; the board reflects the failure without the owner having done anything | Copy: history row "Email send failed" + reason in plain words (e.g. "recipient address invalid — check the lead's details"), raw relay code kept in a foldout · Look: ⛔ failed row · Where: card history | Step 0 ledger row goes pending → failed with the captured reason; the arm is NOT left half-done (either fully armed with step 0 marked failed, or not armed — never "board says armed, nothing scheduled") |
Must NOT record accepted; must NOT show any "sent" claim in history; must NOT swallow the failure with no record; must NOT leave a half-armed sequence |
FAILURE ENTRY Type: Send failed · icon: ⛔ · label: "Email send failed" · desc: subject + plain-language reason · timestamp: failure time · by: system |
| 2 | Mátyás glances at the board | The affected card wears a red dashed ring and a board banner reads "1 failed send — review". That is the WHOLE failure surface: no popup when he opens the card, no separate Failures list | Copy: banner "⛔ 1 failed send — review" · Look: red dashed ring around the card + banner notification · Where: board card + board header | The failed ledger row keeps the ring + banner up until the failure is handled; banner count = open failed rows; a retry (from the ⛔ history row) re-enters the send path with the SAME idempotency key + frozen copy — success flips failed → accepted and clears ring + banner |
Must NOT pop a dialog when the card is opened; must NOT render a Failures list/panel anywhere; must NOT let the failure fade silently (ring stays until handled); a retry must NOT double-send if the first actually went | The ⛔ failure entry persists on the timeline — it is the durable record behind the ring. ON RETRY-SUCCESS it gains a follow-up 📤 "Resend succeeded" entry (the failure stays in history — resolved, not erased). |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás arms a sequence while the relay is down (modal closes instantly as always) | The first send fails honestly in the background and surfaces exactly like S3 (red dashed ring + banner); the remaining steps stay frozen + scheduled, not thrown away | Copy: banner reason "mail service unavailable" · Look: red dashed ring + banner · Where: board card + board header | Step 0 ledger row = failed (reason: relay unreachable); frozen steps 1–N remain as pending scheduled rows, untouched |
Must NOT report the batch as sent; must NOT discard the scheduled steps because step 0 failed; must NOT infinite-retry-hammer the dead relay | FAILURE ENTRY ⛔ "Email send failed — mail service unavailable". The scheduled future steps still render as pending (W2 styling). |
| 2 | (Automatic) a previously-scheduled frozen step comes due while the relay is still down | That step attempts its send, fails honestly, and surfaces as its own failed row — it does not silently skip its slot | Copy: banner count increments · Look: red dashed ring on the affected card · Where: board | The due frozen step writes a pending → failed ledger row (never a phantom "accepted"); it stays reviewable/retryable from its ⛔ history row |
Must NOT mark a step "accepted" because its scheduled time passed; must NOT drop the step off the schedule with no record | FAILURE ENTRY ⛔ per due step that couldn't send — each keeps its own reason + timestamp so the timeline shows exactly which scheduled email didn't make it. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | (Automatic) a frozen step's send-day arrives, still no reply | The email auto-sends exactly as it was frozen; the card's history gains a 📤 sent row that byte-matches the preview AND carries a small "⚙️ auto" marker so an autopilot send never masquerades as a hand-sent one | Copy: the frozen subject + body, unchanged since arm; history row tagged "⚙️ auto" · Look: scheduled row flips to sent, muted-grey system styling + the auto marker · Where: touchpoint history (W2 rendering) | Continuation is a whitelisted category (rides the existing arm — no fresh click); ledger row pending → accepted; the sent copy = the frozen copy (100% parity, Q7) |
Must NOT re-generate or re-resolve the copy at send time; must NOT require a new owner click for a continuation; must NOT render the auto-send identically to an operator send (the ⚙️ marker is required) | NEW ENTRY Type: Email sent · icon: 📤 + ⚙️ auto marker · label: "Sequence step {n} sent (auto)" · desc: subject · timestamp: send time · by: system (muted grey — auto, but whitelisted) |
| 2 | The lead replies before the next scheduled step | The sequence stops itself; remaining frozen steps are cancelled and the circling dashed border clears | Copy: history "Sequence stopped — reply received" · Look: dashed border removed; scheduled rows shown cancelled · Where: card + history | Reply detected → sequence teardown; pending scheduled ledger rows move to a cancelled/superseded terminal state (not left as ghost pending) | Must NOT keep sending after a reply; must NOT leave orphan scheduled rows firing; must NOT mark cancelled rows as failures | NEW ENTRY Type: Sequence stopped · icon: 🛑 · label: "Sequence stopped (reply)" · desc: "The lead replied — remaining emails cancelled" · timestamp: reply time · by: system |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | The lead's data changes after arming but before a frozen step's send-day | That scheduled row turns amber and is put on HOLD — it will NOT auto-send on its day until re-approved | Copy: row badge "⏸ Review needed — lead data changed" · Look: amber scheduled row · Where: touchpoint history / scheduled rows (W2 rendering) | Drift detected between frozen snapshot and current lead data → step marked needs-reconfirm; the auto-send gate refuses this step while held (its approved flag is not set for the new data) | Must NOT auto-send the stale frozen copy; must NOT silently re-resolve to the new data and send without the owner seeing it; must NOT drop the step | HOLD ENTRY Type: Scheduled email held · icon: ⏸ · label: "Scheduled email held (data changed)" · desc: "Lead data changed — re-approval needed" · timestamp: drift-detect time · by: system |
| 2 | Mátyás opens the held row and clicks re-approve (accepting or editing the refreshed copy) | The row leaves amber and returns to a normal scheduled state; it will fire on its day with the approved copy | Copy: row back to normal scheduled styling · Look: amber cleared · Where: scheduled rows | Re-approval writes the fresh frozen copy + sets the approved/owner_edited flag → the auto-send gate now permits this step (owner-edited scheduled email = whitelisted) |
Must NOT require re-arming the whole sequence; must NOT lose the step's original send-day; must NOT send before the owner's explicit re-approve | NEW ENTRY Type: Scheduled email approved · icon: ✅ · label: "Held email re-approved" · desc: "Updated copy approved — schedule resumes" · timestamp: approval time · by: operator |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | (Automatic) DocuSeal reports the client has signed the owner-pre-signed contract | The existing automatic email goes out to the client immediately — no owner click — containing all four pieces: ① the pre-existing copy already set up in the system, ② confirmation with the signed contract, ③ the contract download link, ④ the payment reminder with the Stripe link + bank-transfer details | Copy: the system's existing signed-contract email (client-facing, Hungarian) with signed-PDF + download link + Stripe link + transfer details · Look: 📤 sent row in history · Where: touchpoint history + the client's inbox | Send passes the whitelist gate as the signed-contract category; ledger pending → accepted with message id; per-deal idempotency flag prevents a double-send on a repeated webhook |
Must NOT wait for an owner click; must NOT double-send on a duplicate webhook; must NOT go out with any of the four pieces missing (no contract attachment/link, no payment details); must NOT be blocked by any global flag — the whitelist permits it by category | NEW ENTRY Type: Email sent · icon: 📤 · label: "Signed-contract email sent" · desc: "Client signed — confirmation + signed contract + download link + payment details sent automatically" · timestamp: signing time · by: system |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | (Automatic) 1 day after signing, no payment has arrived | The first payment-reminder email auto-sends — no button, no owner click anywhere. The card's history shows it as an automatic send | Copy: the payment-reminder email (client-facing, Hungarian — Stripe link + transfer details) · Look: ⏰ reminder row in history with the ⚙️ auto marker · Where: touchpoint history | The payment-reminder chase arms itself off the signing event with a 1-day fuse; the reminder is a whitelisted auto-send category; ledger pending → accepted; payment state checked immediately before send |
Must NOT render any "Send payment link" button or owner task; must NOT send if payment already arrived within the first day; must NOT send more than one reminder in a day | NEW ENTRY Type: Payment reminder sent · icon: ⏰ + ⚙️ auto · label: "Payment reminder 1 sent" · desc: "No payment 1 day after signing — automatic reminder" · timestamp: send time · by: system |
| 2 | (Automatic) the client still doesn't pay on the following days | One reminder per day fires — day 2, day 3, capped at 3–4 reminders total. The moment a payment arrives (Stripe or bank transfer), the chase ends silently — no further reminders | Copy: daily reminder emails · Look: one ⏰ row per day in history; on payment the chase rows simply stop · Where: touchpoint history | A daily cron checks payment state before each reminder; hard cap at the 3–4-reminder ceiling; payment-arrival flips the chase to a terminal completed state and cancels any remaining reminders |
Must NOT send a reminder after payment has arrived; must NOT exceed the reminder cap; must NOT send two reminders on one day; must NOT keep the chase alive forever past the cap | One ⏰ "Payment reminder {n} sent" entry per day, each through the ledger. After payment: no further chase entries (the payment itself is logged by the money path, not by W1). |
| 3 | Mátyás logs a phone call on the deal mid-chase (he called the client about the payment) | The chase stops immediately — the logged call is the stop signal; no more payment reminders go out | Copy: the existing Log-Call panel, unchanged · Look: call touchpoint appears; scheduled reminders shown cancelled · Where: card history | Logging a call on a deal with an active payment chase cancels the remaining reminders (terminal stopped_by_call state); the call touchpoint itself is logged as today |
Must NOT keep sending reminders after a logged call; must NOT require any extra "stop chase" action beyond logging the call; must NOT delete the already-sent reminder history | NEW ENTRY Type: Chase stopped · icon: 🛑 · label: "Payment chase stopped (call logged)" · desc: "Owner logged a phone call — remaining reminders cancelled" · timestamp: call-log time · by: operator |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | A lead self-books a call slot | The booking-confirmation email goes out instantly, automatically — exactly as it does today; no owner click, no gate, no behavior change | Copy: the existing booking confirmation (time, Meet link — client-facing Hungarian, unchanged) · Look: 📅 confirmation row in history · Where: touchpoint history | Send passes the whitelist as the booking-confirmation category; ledger pending → accepted; the rep is on the invite as an attendee (B16 re-verify — W2/owner) |
Must NOT be blocked by any global send flag; must NOT require a click; must NOT change the existing email's content or timing (verify-only, no redesign) | NEW ENTRY Type: Booking confirmation sent · icon: 📅 (booking-lifecycle family — distinct from sequence 📤 rows) · label: "Booking confirmation sent" · desc: "Slot confirmed: {time}" · timestamp: booking time · by: system |
| 2 | (Automatic, cron) the pre-call reminder becomes due | The reminder auto-sends on schedule, exactly as today; in history it reads clearly as a reminder, visually distinct from the confirmation row (owner-approved distinction) | Copy: the existing pre-call reminder body (unchanged) · Look: ⏰ reminder row, same 📅 booking-lifecycle family, distinct label/icon from the confirmation · Where: touchpoint history | Send passes the whitelist as the reminder category; per-(deal,idx) claim + quiet-window suppression already in place; ledger pending → accepted |
Must NOT fire inside the quiet window; must NOT double-send the same reminder index; must NOT render confirmation and reminder as two indistinguishable generic rows | NEW ENTRY Type: Call reminder sent · icon: ⏰ (booking-lifecycle family) · label: "Pre-call reminder sent" · desc: "Reminder about the upcoming call" · timestamp: cron time · by: system — clearly distinct from the 📅 confirmation row above it |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | A lead sends an angry / hostile reply | A card appears in the Negative Replies column, tagged as hostile — and NO automated reply is sent back | Copy: card tag "😠 hostile" · Look: silent card in Negative Replies (no notification blast) · Where: Negative Replies column | Inbound classified hostile → a Negative Replies card is minted with a hostile tag; the send/no-send decision is NO auto-reply; any armed sequence stops | Must NOT auto-reply / antagonize; must NOT drop the reply with no card (round-3 requires the card); must NOT keep the outreach sequence running after a hostile reply | NEW ENTRY Type: Hostile reply · icon: 😠 · label: "Hostile reply — silent card" · desc: "The lead replied hostilely — card minted in Negative Replies, no automated response" · timestamp: reply time · by: system |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás opens an already-SENT email in touchpoint history, edits the text, and saves | The stored record updates and the row gains an "edited" note; NOTHING is re-sent to the lead | Copy: row badge "✎ edited" + edit timestamp · Look: updated body, "edited" marker · Where: the sent row in touchpoint history | The sent record's stored copy is updated for internal accuracy; an "edited" note (who/when) is attached; no send call is made — the ledger's original accepted send is untouched |
Must NOT re-send to the lead; must NOT overwrite the record with no "edited" note (that would break history honesty); must NOT alter the original send's ledger row | NEW ENTRY Type: Record edited · icon: ✎ · label: "Sent-email record edited" · desc: "Internal record corrected — NO email re-sent to the lead" · timestamp: edit time · by: operator |
delivered (no bounce webhook can prove it). W2 owns the visual rendering of these rows; W1 owns their truth.One row per W1 work item — which scenario steps prove it. The QA skill walks only the listed steps per item. Owner comment-round (2026-07-12) overrides noted where they reshape an item.
| W1 work item | What it delivers | Proven by (scenario · step) | Ruling note |
|---|---|---|---|
| W1-1 Verify the existing signed-contract auto-send | When the client signs the owner-pre-signed contract, the EXISTING automatic email (system copy + signed-contract confirmation + download link + Stripe/transfer payment details) fires through the ledger, idempotent on duplicate webhooks | S7·1 | REWRITTEN by owner comments — the email already exists; W1 verifies it (all four content pieces present), it does not redesign or gate it. |
| W1-2 Automatic daily payment-reminder chase | No payment 1 day after signing → one reminder per day (cap 3–4), stopped by payment arrival or a logged phone call; no button, no owner click | S8·1, S8·2, S8·3 | REVERSED by owner comments — the old click-only "Send payment link" button is deleted; the chase is fully automatic with two stop conditions. |
| W1-3 Per-category whitelist gate (replaces the global boolean) | Only whitelisted categories auto-send; a flipped flag can't re-open a leak | S5·1, S7·1, S8·1, S9·1, S9·2 | Whitelist = booking confirmations · pre-call reminders · signed-contract email · owner-edited scheduled sequence emails · post-signing payment reminders; sequence continuation rides the arm. |
| W1-4 Instant modal-close + reliable background send | Send closes the modal immediately; a background worker reliably fires the send and the proof (ledger + history row) appears on the card without owner action; every send button single-flight | S1·1, S1·2, S1·3, S2·1, S3·1, S3·2 | REWRITTEN by owner comments — replaces the old "modal stays open until 2xx + success toast" design. Failure surface reduced to red dashed ring + banner only (no popup, no Failures list). |
| W1-5 Send ledger (pending → accepted → failed) | Every send recorded honestly; failures never swallowed; never "delivered" | S1·3, S3·1, S4·1, S4·2, S5·1, S9·1, S9·2 | Terminal states pending/accepted/failed; the board ring + banner read this ledger. |
| W1-6 Deterministic idempotency key per intended send | N clicks / duplicate webhooks = exactly 1 send | S2·1, S2·2, S7·1 | Unchanged. |
| A3 Angry-reply routing (was deferred owner-decision) | Hostile reply → silent card in Negative Replies, no auto-reply | S10·1 | RESOLVED by Q8=A — silent card; sequence also stops. |
| Q2=B gate Drift hold on frozen scheduled emails | Drifted frozen step holds amber; won't auto-send until re-approved | S6·1, S6·2 | W2 owns the amber-row UI; W1 owns the gate that refuses an un-re-approved drifted step (owner-edited flag). |
| Q7=A Sequences fire until reply/stop | Continuations auto-send with frozen-copy parity + a visible ⚙️ auto marker; reply auto-stops | S5·1, S5·2 | 100% parity between history copy and sent copy is the hard requirement; the auto marker is owner-approved. |
| Q9=A Edit a sent email | Internal record fix + "edited" note, never a re-send | S11·1 | Touches the send record but makes no send call; original ledger row untouched. |
main before this signature and a green staging QA pass captured in SHIP_LEDGER.md.EBO_W1_SEND_INTEGRITY.html.signed)