From 400c0232a94dec2503ee2982f281f0d97ff342fd Mon Sep 17 00:00:00 2001 From: Nate Barlow Date: Fri, 15 May 2026 15:08:48 -0600 Subject: [PATCH 01/11] M19 restructure: add Phase 4 (Visual Identity Polish); reopen 19.1 for verification pass; renumber phases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Phase 4 — Visual Identity Polish (new): favicon overhaul (theme-cohesive yet purpose-distinguishable across surfaces), og:image / social previews, and the "open beta" copy refactor (moved here from former Phase 4 / now Phase 5 §5). Slotted at Phase 4 so subsequent phases review their surfaces in their final visual state. No dependency on Phase 3 (LLC). - Phase 1 reopened for end-to-end verification: §1–§4 audit work stays closed and untouched; §5–§7 appended for scenario walkthroughs (incl. time-advanced past-due flows), §6 finding resolution, §7 best-effort stress check. Catch-all alias (`MAIL_ALIAS_ENABLED`) flipped off early in §5.0 so the rest of MS19 emits mail under prod-like routing. Tasks tagged `[Agent]` / `[Subagent]` / `[User]` per the x17.5 convention. - Phases 4–7 → 5–8: Legal Layer (was 4 → 5), Content Promises (5 → 6), Contributor Docs (6 → 7), 2FA (7 → 8). Phase 8 stays positionally last by design. Strategy doc filenames renamed to match. - Phase 3 (LLC) and Phase 5 (Legal Layer) corrected to independent parallel tracks. Stripped the "Prerequisite for Phase 4" assertion from the milestone doc — drafting/UI work has no LLC dependency; only the deploy-time entity-name swap at MS21 needs the formed entity. - `.ics` updated to reflect actual M19 start (2026-05-08), expanded scope (M19 end → 2026-07-04), and downstream cascade (M20: 2026-07-03 → 2026-07-16; M21: 2026-07-15 → 2026-07-28). - Cross-reference updates: `docs/milestones/21_RC_DEPLOYMENT.md` and `docs/strategies/x18.3_SITE_ARCHITECTURE_AND_PUBLISHING.md` updated to point at the new phase numbers. Co-Authored-By: Claude Opus 4.7 --- docs/milestones.ics | 12 +- docs/milestones/19_RC_HARDENING_OPS.md | 41 +-- docs/milestones/21_RC_DEPLOYMENT.md | 6 +- .../19.1_NOTIFICATION_COVERAGE_AUDIT.md | 237 ++++++++++++++++++ docs/strategies/19.2_AUTH_HARDENING.md | 4 +- docs/strategies/19.3_LLC_FORMATION.md | 6 +- .../strategies/19.4_VISUAL_IDENTITY_POLISH.md | 76 ++++++ ...9.4_LEGAL_LAYER.md => 19.5_LEGAL_LAYER.md} | 17 +- ...> 19.6_CONTENT_PROMISES_RECONCILIATION.md} | 2 +- ...BUTOR_DOCS.md => 19.7_CONTRIBUTOR_DOCS.md} | 2 +- ...N.md => 19.8_TWO_FACTOR_AUTHENTICATION.md} | 6 +- .../x18.3_SITE_ARCHITECTURE_AND_PUBLISHING.md | 2 +- .../x19.1_NOTIFICATION_COVERAGE_AUDIT.md | 114 --------- 13 files changed, 363 insertions(+), 162 deletions(-) create mode 100644 docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md create mode 100644 docs/strategies/19.4_VISUAL_IDENTITY_POLISH.md rename docs/strategies/{19.4_LEGAL_LAYER.md => 19.5_LEGAL_LAYER.md} (89%) rename docs/strategies/{19.5_CONTENT_PROMISES_RECONCILIATION.md => 19.6_CONTENT_PROMISES_RECONCILIATION.md} (96%) rename docs/strategies/{19.6_CONTRIBUTOR_DOCS.md => 19.7_CONTRIBUTOR_DOCS.md} (97%) rename docs/strategies/{19.7_TWO_FACTOR_AUTHENTICATION.md => 19.8_TWO_FACTOR_AUTHENTICATION.md} (96%) delete mode 100644 docs/strategies/x19.1_NOTIFICATION_COVERAGE_AUDIT.md diff --git a/docs/milestones.ics b/docs/milestones.ics index 0f8a1738..46d2fb39 100644 --- a/docs/milestones.ics +++ b/docs/milestones.ics @@ -12,22 +12,22 @@ DESCRIPTION:Extend the site from a single placeholder to a lightweight content s UID:cz-ms18@cryptozing.app END:VEVENT BEGIN:VEVENT -DTSTART;VALUE=DATE:20260516 -DTEND;VALUE=DATE:20260612 +DTSTART;VALUE=DATE:20260508 +DTEND;VALUE=DATE:20260704 SUMMARY:MS19 — RC Hardening & Ops DESCRIPTION:RC hardening: notification coverage, auth/password policy, LLC formation, legal layer (ToS, Privacy Policy, disclaimers, monetization-neutral copy), content promises reconciliation, contributor docs, 2FA (email baseline, optional TOTP). UID:cz-ms19@cryptozing.app END:VEVENT BEGIN:VEVENT -DTSTART;VALUE=DATE:20260611 -DTEND;VALUE=DATE:20260624 +DTSTART;VALUE=DATE:20260703 +DTEND;VALUE=DATE:20260716 SUMMARY:MS20 — Mainnet Cutover Preparation DESCRIPTION:Define and rehearse env flips, wallet validation, mail sanity, and backout steps for mainnet cutover. UID:cz-ms20@cryptozing.app END:VEVENT BEGIN:VEVENT -DTSTART;VALUE=DATE:20260623 -DTEND;VALUE=DATE:20260706 +DTSTART;VALUE=DATE:20260715 +DTEND;VALUE=DATE:20260728 SUMMARY:MS21 — CryptoZing.app Deployment (RC) DESCRIPTION:Deploy the RC under cryptozing.app, replace GitHub Pages placeholder, remove temporary mail aliasing, complete rollout verification. UID:cz-ms21@cryptozing.app diff --git a/docs/milestones/19_RC_HARDENING_OPS.md b/docs/milestones/19_RC_HARDENING_OPS.md index 5967b9b8..46b36b5c 100644 --- a/docs/milestones/19_RC_HARDENING_OPS.md +++ b/docs/milestones/19_RC_HARDENING_OPS.md @@ -1,6 +1,6 @@ # MS19 - RC Hardening & Ops -Status: Active — running in parallel with MS18 (no hard dependencies between the two; MS18 is blocked on Rachel's video through the 2026-05-31 hard cap, MS19 phases are independent of that work). Phase strategy doc skeletons in place; decisions pending before flesh-out. +Status: Active — running in parallel with MS18 (no hard dependencies between the two; MS18 is blocked on Rachel's video through the 2026-05-31 hard cap, MS19 phases are independent of that work). Phase 1 reopened 2026-05-15 for end-to-end verification (audit closed; verification pass appended). Phase 4 (Visual Identity Polish) added 2026-05-15 — favicon overhaul + open-beta copy refactor + og:image bundled as one visual pass; renumbered downstream phases accordingly. Phase 3 / Phase 5 corrected to independent parallel tracks (no LLC → Legal Layer prerequisite). `.ics` updated to reflect the expanded scope. Parent execution doc: [`docs/PLAN.md`](../PLAN.md) Supporting ops doc: [`docs/ops/DOCS_DX.md`](../ops/DOCS_DX.md) @@ -12,6 +12,7 @@ Supporting ops doc: [`docs/ops/DOCS_DX.md`](../ops/DOCS_DX.md) - Put a minimum legal layer in place before mainnet cutover: Terms of Service draft, Privacy Policy draft, disclaimer copy at key user touchpoints, monetization-neutral copy review across existing UI and mail, and UI placement (disclaimer surfaces + footer ToS/Privacy Policy links). - Reconcile the content promises catalog against the finished product — confirm every open entry is honored or trigger a content/product revision. - Refactor public-facing copy from "pre-release" / "Release Candidate" to "open beta" across all published pages. +- Land a coherent visual identity polish before open beta — favicon set across all surfaces (theme-cohesive yet purpose-distinguishable), og:image / social previews, bundled with the open-beta copy refactor. - Add 2FA capability for RC: email-based 2FA as the baseline, TOTP opportunistically if MS19 time allows, with a recommendation surface for users without 2FA enabled. ## Decisions recorded @@ -21,51 +22,57 @@ Supporting ops doc: [`docs/ops/DOCS_DX.md`](../ops/DOCS_DX.md) - **Findings tracking trial:** Through M19, new findings/bugs/todos go to GitHub Issues (closed via `Fixes #N` on the merging PR) instead of new `docs/qa/Finding*.md` docs. Existing finding docs stay put. M20 kickoff decides whether to keep, revert, or hybridize. See [`docs/DOC_ROLES.md`](../DOC_ROLES.md#findings-conventions). ## Current Focus -- Active phase: _(Pre-flight — strategy skeletons drafted; flesh-out begins once decisions are answered.)_ +- Active phase: Phase 1 (reopened — verification pass) and Phase 2 (next-up auth hardening). Other phases pre-flight. - Phase 1: [`docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md`](../strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md) - Phase 2: [`docs/strategies/19.2_AUTH_HARDENING.md`](../strategies/19.2_AUTH_HARDENING.md) - Phase 3: [`docs/strategies/19.3_LLC_FORMATION.md`](../strategies/19.3_LLC_FORMATION.md) -- Phase 4: [`docs/strategies/19.4_LEGAL_LAYER.md`](../strategies/19.4_LEGAL_LAYER.md) -- Phase 5: [`docs/strategies/19.5_CONTENT_PROMISES_RECONCILIATION.md`](../strategies/19.5_CONTENT_PROMISES_RECONCILIATION.md) -- Phase 6: [`docs/strategies/19.6_CONTRIBUTOR_DOCS.md`](../strategies/19.6_CONTRIBUTOR_DOCS.md) -- Phase 7: [`docs/strategies/19.7_TWO_FACTOR_AUTHENTICATION.md`](../strategies/19.7_TWO_FACTOR_AUTHENTICATION.md) +- Phase 4: [`docs/strategies/19.4_VISUAL_IDENTITY_POLISH.md`](../strategies/19.4_VISUAL_IDENTITY_POLISH.md) +- Phase 5: [`docs/strategies/19.5_LEGAL_LAYER.md`](../strategies/19.5_LEGAL_LAYER.md) +- Phase 6: [`docs/strategies/19.6_CONTENT_PROMISES_RECONCILIATION.md`](../strategies/19.6_CONTENT_PROMISES_RECONCILIATION.md) +- Phase 7: [`docs/strategies/19.7_CONTRIBUTOR_DOCS.md`](../strategies/19.7_CONTRIBUTOR_DOCS.md) +- Phase 8: [`docs/strategies/19.8_TWO_FACTOR_AUTHENTICATION.md`](../strategies/19.8_TWO_FACTOR_AUTHENTICATION.md) ## Phase Rollup -### [x] Phase 1 — Notification Coverage Audit -Document every outbound mail type — trigger, recipient, delivery-log behavior — so the full mail surface is explicitly accounted for before RC. Closed 2026-05-09 — see [`x19.1_NOTIFICATION_COVERAGE_AUDIT.md`](../strategies/x19.1_NOTIFICATION_COVERAGE_AUDIT.md). +### [ ] Phase 1 — Notification Coverage Audit & Verification +Document every outbound mail type — trigger, recipient, delivery-log behavior — so the full mail surface is explicitly accounted for before open beta. Audit (§1–§4) closed 2026-05-09. Reopened 2026-05-15 to add §5–§7 end-to-end verification (every notice class triggered in the running stack against realistic scenarios including time-advanced past-due flows; rendered email confirmed at intended recipients; catch-all alias flipped off so the rest of MS19 emits mail under prod-like routing). See [`19.1_NOTIFICATION_COVERAGE_AUDIT.md`](../strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md). ### [ ] Phase 2 — Auth/Password Policy Hardening Implement 419-to-login redirect and site-wide session-expiry logout. ### [ ] Phase 3 — LLC Formation -Form a single-member LLC in Arizona; obtain EIN; open a business bank account; draft and sign an operating agreement; update CryptoZing references to reflect the entity. Provides the entity backing required for the legal layer's ToS protections to actually shield the operator personally. **Prerequisite for Phase 4.** +Form a single-member LLC in Arizona; obtain EIN; open a business bank account; draft and sign an operating agreement; update CryptoZing references to reflect the entity. Provides the entity backing the Phase 5 legal-layer ToS protections need to actually shield the operator personally. Phase 3 (LLC) and Phase 5 (Legal Layer) run as **independent parallel tracks** — drafting/UI work in Phase 5 does not gate on LLC status; only the deploy-time entity-name swap at MS21 needs the formed entity. Both must land before MS21. -### [ ] Phase 4 — Legal Layer -Draft ToS, Privacy Policy, disclaimer copy; review existing UI/mail copy for monetization-neutral language; place all in the UI. +### [ ] Phase 4 — Visual Identity Polish +Land the visual/brand polish pass before open beta: favicon set across all surfaces (theme-cohesive yet purpose-distinguishable), og:image / social previews, and the open-beta copy refactor (formerly Phase 4 §5 / now Phase 5 §5 of the prior layout — moved here so it bundles with the favicon and social-preview work as one coherent visual pass). No dependency on Phase 3; slotted here so subsequent phases review their surfaces in their final visual state. -### [ ] Phase 5 — Content Promises Reconciliation +### [ ] Phase 5 — Legal Layer +Draft ToS, Privacy Policy, disclaimer copy; review existing UI/mail copy for monetization-neutral language; place all in the UI. Drafting + UI work has no dependency on Phase 3 (LLC) — runs in parallel. Final entity-name swap and publication is deferred to MS21 deploy time. + +### [ ] Phase 6 — Content Promises Reconciliation Walk every open entry in `CONTENT_PROMISES.md` against the finished product; resolve each as honored, content-revised, or product-revised. -### [ ] Phase 6 — Contributor Docs Review +### [ ] Phase 7 — Contributor Docs Review Refresh AGENTS.md, CLAUDE.md, AgentRoles/, and contributor-facing ops docs for currency before RC. -### [ ] Phase 7 — Two-Factor Authentication +### [ ] Phase 8 — Two-Factor Authentication Add 2FA to the RC. Email-based 2FA as the baseline; TOTP / authenticator-app 2FA opportunistically if MS19 time allows (deferred to the 2028 release otherwise). Includes a non-blocking recommendation surface for users without 2FA enabled. **Positionally last by design** — if additional phases are ever added to MS19, this one stays at the end. ## Exit Criteria -_(To be detailed when active.)_ -- [ ] Notification coverage documented: every outbound mail type accounted for with intended trigger, recipient, and delivery log behavior. +- [ ] Notification coverage documented AND verified end-to-end: every outbound mail type accounted for with intended trigger, recipient, and delivery-log behavior, and every notice class observed firing correctly in the running stack against realistic scenarios (including time-advanced past-due flows). Catch-all alias disabled; later MS19 phases run under prod-like mail routing. - [ ] 419-to-login redirect implemented and tested. - [ ] Site-wide session expiry logout implemented and tested. - [ ] LLC formed in Arizona; EIN obtained; business bank account opened; operating agreement signed; CryptoZing references updated to reflect the entity. +- [ ] Favicon set generated and wired across all surfaces (Laravel app, marketing site, any other distinct surface) with theme-cohesive yet purpose-distinguishable per-surface variants. +- [ ] og:image / social-preview meta tags wired and validated against major platform preview tools. +- [ ] User-facing "pre-release" / "Release Candidate" copy replaced with "open beta" framing; internal docs unchanged. - [ ] ToS and Privacy Policy drafted and published to the live site. - [ ] Disclaimer copy present at signup, wallet onboarding, and invoice/payment surfaces; footer links to ToS and Privacy Policy on every page. - [ ] Existing UI and mail copy reviewed for overstatements, financial advice language, and pricing commitments — issues resolved. - [ ] Monetization-safe language guide produced for future copy decisions. - [ ] Content promises catalog reconciled — every open entry confirmed honored or resolved (content revised or product adjusted). - [ ] Contributor docs reviewed and current. -- [ ] Email 2FA available as opt-in; recovery flow per the Phase 7 decision in place. +- [ ] Email 2FA available as opt-in; recovery flow per the Phase 8 decision in place. - [ ] Recommendation surface for users without 2FA enabled is shipped. - [ ] TOTP shipped if MS19 time-cutoff met; otherwise explicitly deferred to the 2028 release. diff --git a/docs/milestones/21_RC_DEPLOYMENT.md b/docs/milestones/21_RC_DEPLOYMENT.md index 4de89f35..09c4b60a 100644 --- a/docs/milestones/21_RC_DEPLOYMENT.md +++ b/docs/milestones/21_RC_DEPLOYMENT.md @@ -11,7 +11,7 @@ Supporting ops doc: [`docs/ops/RC_ROLLOUT_CHECKLIST.md`](../ops/RC_ROLLOUT_CHECK - Replace the GitHub Pages placeholder at `/` with the live app landing page without breaking the SEO baseline established in MS15 and extended in MS18. - Remove temporary mail aliasing. - Complete rollout verification per the RC rollout checklist. -- Activate the legal layer drafted in MS19 Phase 4: swap placeholder entity name for the actual LLC name (formed in MS19 Phase 3), publish ToS and Privacy Policy to live URLs, and wire footer ToS/Privacy links. +- Activate the legal layer drafted in MS19 Phase 5: swap placeholder entity name for the actual LLC name (formed in MS19 Phase 3), publish ToS and Privacy Policy to live URLs, and wire footer ToS/Privacy links. ## Decisions recorded (during MS18 Phase 1) - **Content site architecture:** Static content files served by nginx directly alongside the Laravel app — same domain, no PHP involved for content routes. CMS is Eleventy (selected in MS18 Phase 1); evaluate at RC deployment whether to keep Eleventy or migrate — static output means migration is never a rework. @@ -20,7 +20,7 @@ Supporting ops doc: [`docs/ops/RC_ROLLOUT_CHECKLIST.md`](../ops/RC_ROLLOUT_CHECK - **Staging:** Dev server (`public/content/` via Sail) is the staging environment during MS18–MS20. At RC deployment, the built `public/content/` output is what nginx serves. Post-RC staging options to be decided post-RC. - **GitHub Pages retirement:** GitHub Pages is retired at DNS cutover — not deleted, just no longer the DNS target. No redirects needed; URLs are preserved by the nginx serving the same paths. - **GitHub nav link:** Remove the GitHub link from the site nav before RC deployment — it's pre-release framing. Keep the footer link as-is; consider updating copy post-RC if it no longer fits. -- **Legal-layer activation:** ToS, Privacy Policy, disclaimer copy, and footer ToS/Privacy link scaffolding are drafted in MS19 Phase 4 with placeholder entity names. Final entity-name swap (using the LLC formed in MS19 Phase 3) and publication to live URLs happen at deploy time within Phase 2 of this milestone — not as a separate phase. Treat as a finishing step in the deploy/cutover work. +- **Legal-layer activation:** ToS, Privacy Policy, disclaimer copy, and footer ToS/Privacy link scaffolding are drafted in MS19 Phase 5 with placeholder entity names. Final entity-name swap (using the LLC formed in MS19 Phase 3) and publication to live URLs happen at deploy time within Phase 2 of this milestone — not as a separate phase. Treat as a finishing step in the deploy/cutover work. ## Phases _(Phase strategy docs to be written when this milestone becomes active.)_ @@ -36,6 +36,6 @@ _(To be detailed when active.)_ - [ ] Live app landing page replaces GitHub Pages placeholder at `/`; SEO baseline intact (canonical, sitemap, robots, indexed URLs). - [ ] Temporary mail aliasing removed; outbound mail routes through production config. - [ ] Self-host deployment verified — a clean instance can be stood up independently from the production environment. -- [ ] Legal layer activated: drafted ToS and Privacy Policy from MS19 Phase 4 published with the actual LLC entity name; footer ToS/Privacy links functional on every page; disclaimer copy live at signup, wallet onboarding, and invoice/payment surfaces. +- [ ] Legal layer activated: drafted ToS and Privacy Policy from MS19 Phase 5 published with the actual LLC entity name; footer ToS/Privacy links functional on every page; disclaimer copy live at signup, wallet onboarding, and invoice/payment surfaces. - [ ] Content promises catalog checked — no work in this milestone introduced or violated an entry. - [ ] All RC rollout checklist items completed and signed off. diff --git a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md new file mode 100644 index 00000000..25806822 --- /dev/null +++ b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md @@ -0,0 +1,237 @@ +# MS19 Phase 1 Strategy — Notification Coverage Audit + +Status: Reopened — verification pass appended (2026-05-15). §1–§4 closed and untouched (audit work shipped); §5–§7 added to verify the documented coverage actually behaves correctly end-to-end before open beta. + +Parent milestone doc: [`docs/milestones/19_RC_HARDENING_OPS.md`](../milestones/19_RC_HARDENING_OPS.md) + +Owner tags: `[Agent]` = critical-path code/doc/verification work I drive directly. `[Subagent]` = safe delegated sidecar work. `[User]` = work Nate drives directly (receiving real email, eyeballing rendered output, judging copy/branding quality). + +**Original goal (§1–§4):** Finish the coverage matrix that [`docs/specs/NOTIFICATIONS.md`](../specs/NOTIFICATIONS.md) flagged as an MS17 deliverable but never received. Confirm every active outbound mail class has a documented row covering audience, trigger, code reference, status, feature test, and delivery-log type. Tweak the spec narrative inline where code has drifted from documented behavior. + +**Expanded goal (§5–§7):** Prove the documented mail surface actually works end-to-end. Exercise every notice class in the running stack against realistic scenarios (including time-advanced past-due flows), confirm rendered email lands at intended recipients, fix anything that surfaces. Last-pass-before-open-beta confidence — the bar is "would I bet open-beta credibility on this," not "did the assertion pass." + +## Decisions made + +- **Inventory location:** Extend `NOTIFICATIONS.md`. The matrix slot already exists at the bottom of the spec under `## Coverage & Status (MS17 deliverable)`. +- **Coverage format:** Use the column set the spec already specifies — Audience, Trigger, Mailable class, Status (`live` / `stubbed` / `planned`), Feature test(s), Delivery log type. +- **Gap-closure scope:** Fixes land in this phase by default. If a finding is small-consequence relative to its work cost, route it to [`docs/BACKLOG.md`](../BACKLOG.md) — but only when MS19's phase goal and exit criteria still hold without the deferred work. + +## Carried-in known finding (resolved) + +`Tests\Feature\InvoiceNotificationTest` was failing across every recent PR back to MS17 — past-due delivery assertions expecting `queued` observed `skipped`/`sent`. Root cause: `.env.example` shipped `MAIL_OUTBOUND_ENABLED=false` and the PR Tests workflow copied it verbatim into `.env`, so `InvoiceDeliveryService::skipReason()` marked every queued row as skipped. Resolved via [#68](https://github.com/n8bar/CryptoZing/issues/68) / [#69](https://github.com/n8bar/CryptoZing/pull/69) by pinning the env var in `phpunit.xml`. First Issue under the M19 GitHub Issues trial (see milestone doc Decisions recorded). + +## 1. Enumerate the live mail surface + +1. [x] Re-list every Mail class under `app/Mail/` directly from disk; do not trust the snapshot in the Reference section without re-verifying. +2. [x] List every Notification class under `app/Notifications/` (if any). +3. [x] Identify any in-app outbound paths that bypass Mail/Notification classes (raw mailer calls in Jobs, Commands, or Console scheduled tasks). +4. [x] Cross-check the resulting set against the notice classes named in `NOTIFICATIONS.md` Sections 3–5 — flag anything in code without a spec entry, or anything in the spec without code. + +**§1 results (2026-05-09):** + +- **Mail classes (14):** `app/Mail/` matches the Reference snapshot — no drift since drafting. +- **Notification classes (0):** No `app/Notifications/` directory. +- **Outbound paths (2 callsites, both via Mail classes — no raw-mailer bypass):** + - `App\Jobs\DeliverInvoiceMail::handle` — canonical delivery-log-driven job; renders the right Mailable per `delivery->type` (13 mappings + `InvoiceReadyMail` default for manual sends) and dispatches via `Mail::to(...)->send(...)`. + - `App\Http\Controllers\NotificationSettingsController::send` — owner-only branding preview; sends `NotificationBrandingPreviewMail` directly without creating a delivery-log row (per spec §5.14.4.1). +- **Drift flags for §3:** + - **Spec narrative gap — overpay/underpay paired classes (resolved):** Code has `InvoiceOverpaymentIssuerMail` and `InvoiceUnderpaymentIssuerMail` paired with their client siblings; spec §4.3 and §4.4 originally titled these alerts as "(Client)" only. Pulled forward from §3 and applied: §4.3 and §4.4 retitled as "(Owner + Client)" with an owner-alert sub-bullet derived from the actual issuer mail bodies (overpay disposition prompt; underpay outstanding-balance follow-up). §5.4 paired-pattern and §5.12.2 "Underpayment alert (owner)" language are now consistent with §4. + - **Partial-warning status — stubbed by design:** `InvoicePartialWarningClientMail` and `InvoicePartialWarningIssuerMail` are wired into `DeliverInvoiceMail`'s type map, but no service code queues new `*_partial_warning` deliveries; `InvoiceAlertService::skipInvalidQueuedDeliveries()` only skips legacy queued rows. Tests `test_repeated_partial_payment_detection_no_longer_creates_a_separate_partial_warning_family` and `..._does_not_enqueue_legacy_partial_warnings_on_repeat_runs` assert zero new partial-warning rows. Matches spec §4.4.3 (deprecating the family) and §5.12.3 (legacy display only). To be recorded as `stubbed` in the §2 matrix. + +## 2. Build the coverage matrix in `NOTIFICATIONS.md` + +1. [x] Replace the placeholder under `## Coverage & Status (MS17 deliverable)` with a Markdown table using the spec's column set: Audience | Trigger | Mailable class | Status | Feature test(s) | Delivery log type. +2. [x] Populate one row per notice class identified in §1. +3. [x] For each row's **Status** field, record `live` (in production code, exercising correctly), `stubbed` (class exists but doesn't behave per spec), or `planned` (named in spec, not implemented). +4. [x] For each row's **Feature test(s)** field, link to the corresponding test under `tests/Feature/**` if one exists; record `none` if not. +5. [x] For each row's **Delivery log type** field, record the log-type identifier the class uses when creating delivery-log entries. +6. [x] Drop the "(MS17 deliverable)" suffix from the section heading once the matrix is populated. + +**§2 results (2026-05-09):** 14 rows populated — 12 `live`, 2 `stubbed` (partial-warning legacy classes). All Mailables have feature-test coverage. Delivery log types match `DeliverInvoiceMail`'s type→Mailable map plus `send` (manual default) and `—` for the branding preview (which deliberately bypasses the delivery log). + +## 3. Reconcile drift and tweak the narrative + +1. [x] For each matrix row, compare the actual class behavior to the documented behavior in `NOTIFICATIONS.md` Sections 3–5. +2. [x] Where the spec is right and the code drifted, capture a finding (audience, class, what the spec says vs. what the code does) for §4. +3. [x] Where the code is right and the spec is stale, tweak the spec narrative inline — small edits only. (Pulled forward earlier for the overpay/underpay paired-class titling — see §1 drift flags.) +4. [x] If a tweak feels larger than "small" (rewrites a paragraph, changes a documented invariant, contradicts a §3–§5 commitment), stop and surface for confirmation before applying. + +**§3 results (2026-05-09):** 11 rows aligned with spec; 3 drift findings raised for §4 (one needing a user decision on direction, two small-scoped). 14th-row check (overpay/underpay narrative) was applied earlier in this phase. + +### Findings raised for §4 + +**F1 — Manual send does not transition invoice out of `draft` (decision needed).** +- Spec §3.1.2 says: "Sending the invoice queues outbound delivery, records the attempt in the delivery log, **and issues the invoice out of `draft`**." +- Code: `InvoiceDeliveryController::store` queues the `send` delivery row but does not update `Invoice::status`. `Invoice::refreshPaymentState()` explicitly skips `draft`/`void` invoices, so a draft that gets sent stays at `status='draft'` until something else moves it. +- This is bigger than a "small inline tweak" per §3.4 — needs a deliberate decision: either the spec is over-specifying (drop the draft-transition clause) or the code is missing a status update on successful queue (small but real code change). + +**F2 — Spec uses "(owner)" in display-label examples; shipped labels use "(issuer)".** +- Spec §5.12.2 example: `Underpayment alert (owner)`. Spec §5.12.3 example: `Partial payment warning (client|owner)`. +- Code (`InvoiceDelivery::typeLabel()`): consistently uses `(issuer)` after the MS17 owner→issuer terminology sweep — `Underpayment alert (issuer)`, `Partial payment warning (issuer)`, etc. +- Resolution: small spec tweak — replace "(owner)" with "(issuer)" in §5.12.2 and §5.12.3 examples so the spec matches the shipped labels. + +**F3 — Client underpayment alert view omits BTC outstanding amount (decision needed, small either way).** +- Spec §4.4.2: client alert should "include the outstanding USD/BTC amounts." +- Code (`resources/views/mail/invoice-underpayment-client.blade.php`): shows the outstanding USD amount only; no BTC line. +- The legacy `invoice-partial-warning-client.blade.php` view does show both USD and BTC, so the project pattern supports either interpretation. +- Two reasonable resolutions: (a) add a BTC line to the underpay-client view for spec compliance, or (b) drop "/BTC" from §4.4.2 since the linked invoice page already surfaces full BTC details. + +## 4. Resolve findings + +1. [x] For each in-phase finding from §3, ship the fix (code change or test) within Phase 1 and check off the finding here. +2. [x] For each backlog-bound finding (small consequence, high effort), add an entry to `docs/BACKLOG.md` with enough context to pick it up later. List the deferred finding here with a `[deferred → backlog]` note. +3. [x] Confirm the Phase 1 Exit Criteria still hold after any deferrals. + +**§4 results (2026-05-09):** All three §3 findings resolved in-phase; no backlog deferrals. + +- **F1 — Manual send draft transition (Option C, applied):** Spec §3.1.2 reworded to make the transition rule explicit and conditional with a no-regression guarantee. `InvoiceDeliveryController::store` updated to transition `draft` → `sent` after a successful queue. Two new tests in `InvoiceDeliveryTest` (`test_sending_draft_invoice_transitions_status_to_sent`, `test_sending_does_not_regress_status_for_non_draft_invoice`). +- **F2 — Owner → Issuer in spec display labels:** §5.12.2 and §5.12.3 examples replaced with `(issuer)` to match shipped `InvoiceDelivery::typeLabel()` output. Catches the spec up to the MS17 terminology sweep. +- **F3 — Client underpay alert USD/BTC (b, applied):** §4.4.2 narrowed to "outstanding USD amount." Keeps the spec loose; the linked invoice page surfaces full BTC details, and the email body stays simple/USD-focused for the typical recipient. Code is free to add a BTC line or current-rate hint as a future enhancement without a re-spec round. + +--- + +## 5. End-to-end scenario verification + +Bar: every notice type observed at least once end-to-end — from real trigger in the running stack, through queue + delivery-log + actual mail-server submission, to landing in a real inbox at the intended address with subject/branding/links/copy passing eyeball QA. "Feature test passed" is not enough; that was already true at §1 close. + +### 5.0 Catch-all flip (Option A — early in §5) + +The catch-all alias (`MAIL_ALIAS_ENABLED=true` rewriting recipients to `mailer.cryptozing.app`) is disabled at the start of §5 so all subsequent scenarios exercise real recipient routing. Aliasing **stays off** for the remainder of MS19 — Phases 2–8 then emit mail under prod-like routing, so any later regression is caught in the right state. + +1. [ ] [Agent] Set `MAIL_ALIAS_ENABLED=false` in `.env`. Confirm `MAIL_ALIAS_DOMAIN` left intact for documentation but inactive. +2. [ ] [Agent] Trigger a single low-stakes test mail (manual-send `InvoiceReadyMail` to a controlled recipient) and verify via Mailgun logs + recipient inbox that no rewrite occurred. +3. [ ] [User] Confirm receipt of the un-aliased mail at the intended address. Sign off on §5.0 before §5.1 begins. +4. [ ] [Agent] Document the active test addresses in this section — agent-suggested list below; [User] may override at §5.0 sign-off: + - **Issuer persona:** `issuer-test@nateTheProgrammer.com` + - **Client persona:** `client-test@nospam.site` + - **Owner-alert recipient:** `owner-alerts@cybercreek.us` + - **Real eyeball-QA inbox:** `nate@nateTheProgrammer.com` + +### 5.1 Manual issue (`InvoiceReadyMail` + draft→sent transition) + +1. [ ] [Agent] Create a draft invoice via the app; set client recipient to the §5.0 client persona. +2. [ ] [Agent] Trigger send; confirm `InvoiceReadyMail` queued, `delivery_log` row of type `send` created, invoice transitioned `draft` → `sent`. +3. [ ] [Agent] Confirm Mailgun accepted the message (check provider logs / message-id round-trip). +4. [ ] [User] Receive the mail at the client persona inbox; verify subject/branding/links/copy. Click invoice link, confirm public invoice view loads correctly. + +### 5.2 Pay full (`InvoicePaidReceiptMail` client + `InvoiceIssuerPaidNoticeMail` issuer) + +1. [ ] [Agent] Take an issued invoice, simulate a confirmed full payment (signet/regtest payment or fixture-based payment-state advance). +2. [ ] [Agent] Verify both Mailables fire; confirm both delivery-log rows created (`paid_client`, `paid_issuer`). +3. [ ] [User] Receive both at client + issuer personas. Verify TXID renders cleanly on narrow viewports (Finding 1 from MS17 — regression check), copy is appropriate, paid receipt looks like a paid receipt. + +### 5.3 Partial payment acknowledgment (`InvoicePaymentAcknowledgmentClientMail` + `InvoicePaymentAcknowledgmentIssuerMail`) + +1. [ ] [Agent] Take an issued invoice, simulate a partial confirmed payment. +2. [ ] [Agent] Verify both acknowledgment Mailables fire; confirm delivery-log rows. +3. [ ] [User] Receive both. Verify the acknowledgment framing is honest about "we received some" vs. final settlement. + +### 5.4 Underpayment alert (`InvoiceUnderpaymentClientMail` + `InvoiceUnderpaymentIssuerMail`) + +1. [ ] [Agent] Take an issued invoice; simulate payment below threshold; advance scheduler past the underpay-alert cooldown so the alert is eligible to fire. +2. [ ] [Agent] Run the alert pass; verify both underpayment Mailables fire; confirm delivery-log rows. +3. [ ] [User] Receive both. Verify client view shows the outstanding USD amount per spec §4.4.2 (post-F3 narrowing); issuer view surfaces the outstanding-balance follow-up per spec §4.4. +4. [ ] [Agent] Re-run the alert pass within cooldown; verify NO duplicate row is written (regression check on Findings 7 & 8 from MS17). + +### 5.5 Overpayment alert (`InvoiceOverpaymentClientMail` + `InvoiceOverpaymentIssuerMail`) + +1. [ ] [Agent] Take an issued invoice; simulate payment above threshold. +2. [ ] [Agent] Run the alert pass; verify both overpayment Mailables fire; confirm delivery-log rows. +3. [ ] [User] Receive both. Verify the issuer view's overpay disposition prompt per spec §4.3. +4. [ ] [Agent] Re-run within cooldown; verify no duplicate row. + +### 5.6 Past-due (force-set due date) (`InvoicePastDueClientMail` + `InvoicePastDueIssuerMail`) + +This one matters most — Nate flagged late-payment notifications specifically as critical to verify thoroughly. Use real time-advanced scenarios, not just unit-level "if past due, send mail" assertions. + +1. [ ] [Agent] Create an issued, unpaid invoice. Force-set its `due_at` (or equivalent) to ≥1 day in the past via a tinker statement or database update. +2. [ ] [Agent] Run `InvoiceAlertService::sendPastDueAlerts()` (via `php artisan schedule:run` or direct service call). +3. [ ] [Agent] Verify slot 1 fires for both client and issuer; confirm delivery-log rows; verify slot 2 and slot 3 do NOT fire on the same run. +4. [ ] [User] Receive both. Verify outstanding-USD copy is not falsely qualified as "approximate" (regression check on Finding 2 from MS17); links resolve. +5. [ ] [Agent] Force-set `due_at` to 7+ days in the past; re-run; verify slot 2 fires. +6. [ ] [Agent] Force-set `due_at` to 14+ days in the past; re-run; verify slot 3 fires. +7. [ ] [Agent] Run `sendPastDueAlerts` again with all three slots already sent; verify NO new delivery-log rows appear (regression check on Findings 3 & 4 from MS17). +8. [ ] [User] Receive each slot at issuer + client personas; spot-check that escalation tone or content actually escalates between slots if that's the spec'd behavior. + +### 5.7 Branding preview (`NotificationBrandingPreviewMail`) + +1. [ ] [Agent] As an authenticated owner, trigger the branding preview from `NotificationSettingsController::send`. +2. [ ] [Agent] Confirm the preview Mailable fires and is sent directly without creating a delivery-log row (per spec §5.14.4.1). +3. [ ] [User] Receive the preview; verify branding renders as expected. + +### 5.8 Stubbed-by-design (`InvoicePartialWarningClientMail` + `InvoicePartialWarningIssuerMail`) + +These are wired into the type map but no service code queues new `*_partial_warning` deliveries (per §1 drift findings). Verify they stay quiet. + +1. [ ] [Agent] Run a full pass of `InvoiceAlertService` and observe the delivery log: zero new `*_partial_warning` rows should be written for any invoice. +2. [ ] [Agent] Verify the existing tests (`test_repeated_partial_payment_detection_no_longer_creates_a_separate_partial_warning_family`, `..._does_not_enqueue_legacy_partial_warnings_on_repeat_runs`) still pass. + +### 5.9 Edge cases + +1. [ ] [Agent] Void an invoice after send; verify no further mail fires for that invoice (no past-due, no underpay, no overpay). +2. [ ] [Agent] Create two invoices with the same client recipient address; verify each invoice's mails route correctly and no cross-contamination of delivery-log entries. +3. [ ] [Agent] Manual-resend `InvoicePaidReceiptMail` within the resend cooldown window; verify the resend is blocked with no new delivery row. +4. [ ] [Agent] Wait past the cooldown; manual-resend; verify a new `resend_*` context-keyed delivery row is created and the mail lands. + +### 5.10 Mail-client rendering spot-check + +1. [ ] [User] For at least one received mail per audience class (client-facing receipt, issuer-facing alert), open in two distinct mail clients (e.g., Gmail web + iOS Mail). Note any rendering breakage — broken images, broken layout, broken links, blocked-content prompts. + +## 6. Resolve §5 findings + +Same in-phase-fix-vs-backlog rule §4 used. + +1. [ ] [Agent or User] For each in-phase finding from §5, ship the fix (code change, template change, test) within Phase 1 and check off the finding here. +2. [ ] [Agent] For each backlog-bound finding (small consequence, high effort), add an entry to `docs/BACKLOG.md` with enough context to pick it up later. List the deferred finding here with a `[deferred → backlog]` note. +3. [ ] [User] Confirm the expanded Phase 1 Exit Criteria still hold after any deferrals. + +**§6 findings tracking:** + +_(Populated as §5 surfaces issues. Empty until §5 runs.)_ + +## 7. Stress-readiness check (best-effort) + +We don't have dedicated load-test infrastructure before open beta. This section is honest about that — it records what we CAN exercise and what we explicitly aren't testing. + +1. [ ] [Agent] Burst-fire ~25–50 issued invoices in a tight loop; verify the queue drains all `DeliverInvoiceMail` jobs without any double-sending or `failed_jobs` accumulation. +2. [ ] [Agent] Trigger a queue worker restart mid-burst; verify in-flight jobs resume without duplicate sends and the delivery log stays truthful. +3. [ ] [Agent] Force one job to fail (e.g., temporarily invalid mail config); confirm Laravel's retry logic handles it; on success after retry, no duplicate delivery-log rows are written. +4. [ ] [Agent] Document what was NOT tested (so post-launch ops knows what's untested rather than silently assumed): no real-volume stress (>1,000 simultaneous invoices), no Mailgun rate-limit testing, no bounce/spam-folder simulation, no extended-duration soak. + +**§7 results:** _(Populated when §7 runs. Include a short "untested risks" summary so the open-beta cutover doesn't inherit hidden assumptions.)_ + +## Exit Criteria + +- [x] `NOTIFICATIONS.md` Coverage & Status section contains a populated matrix with one row per active notice class. +- [x] Every Mail/Notification class in code is represented in the matrix. +- [x] Every notice class named in spec Sections 3–5 has a matching matrix row. +- [x] Drift findings are either fixed in this phase or recorded in `BACKLOG.md` with context. +- [x] `(MS17 deliverable)` placeholder text is removed from the spec heading. +- [x] CI flakiness in `Tests\Feature\InvoiceNotificationTest` (past-due delivery status: expected `queued`, observed `skipped`/`sent`) investigated and resolved. Tests pass green on a Phase-1-close PR. (Resolved via [#68](https://github.com/n8bar/CryptoZing/issues/68).) +- [ ] Catch-all alias disabled (`MAIL_ALIAS_ENABLED=false`); test mail confirmed reaching real intended addresses without rewrite. +- [ ] Every notice class observed end-to-end at least once via §5 scenarios — trigger fired, Mailable sent, delivery-log row truthful, mail received at intended recipient, eyeball QA passed. +- [ ] Late-payment / past-due notification flow (§5.6) verified across all three escalation slots with force-set due dates. +- [ ] Stubbed-by-design partial-warning classes (§5.8) confirmed quiet under live alert passes. +- [ ] All §5 findings resolved per §6 — in-phase fixes shipped or backlog-deferred with context. +- [ ] §7 stress-readiness check executed; "what was NOT tested" risks documented for post-launch ops. + +## Reference + +Mail classes present in `app/Mail/` at skeleton drafting (2026-05-08). Re-enumerate in §1.1; this list is a starting point, not authoritative. + +- `InvoiceIssuerPaidNoticeMail.php` +- `InvoiceOverpaymentClientMail.php` +- `InvoiceOverpaymentIssuerMail.php` +- `InvoicePaidReceiptMail.php` +- `InvoicePartialWarningClientMail.php` +- `InvoicePartialWarningIssuerMail.php` +- `InvoicePastDueClientMail.php` +- `InvoicePastDueIssuerMail.php` +- `InvoicePaymentAcknowledgmentClientMail.php` +- `InvoicePaymentAcknowledgmentIssuerMail.php` +- `InvoiceReadyMail.php` +- `InvoiceUnderpaymentClientMail.php` +- `InvoiceUnderpaymentIssuerMail.php` +- `NotificationBrandingPreviewMail.php` + +`NOTIFICATIONS.md` self-flagged the gap at lines 99–100 (the `## Coverage & Status (MS17 deliverable)` heading exists; the matrix below it does not). diff --git a/docs/strategies/19.2_AUTH_HARDENING.md b/docs/strategies/19.2_AUTH_HARDENING.md index e97b8a49..fcf58f7e 100644 --- a/docs/strategies/19.2_AUTH_HARDENING.md +++ b/docs/strategies/19.2_AUTH_HARDENING.md @@ -5,14 +5,14 @@ Parent milestone doc: [`docs/milestones/19_RC_HARDENING_OPS.md`](../milestones/1 **Goal:** Implement 419-to-login redirect and site-wide session-expiry logout so expired sessions land users somewhere predictable and recoverable instead of dropping a raw 419 page. -(2FA was originally considered for this phase but has been split into [Phase 7 — Two-Factor Authentication](19.7_TWO_FACTOR_AUTHENTICATION.md). This phase stays narrow.) +(2FA was originally considered for this phase but has been split into [Phase 8 — Two-Factor Authentication](19.8_TWO_FACTOR_AUTHENTICATION.md). This phase stays narrow.) ## Decisions made - **419 redirect target:** Investigation-then-decide. Section 1's survey enumerates the intents that can land at 419; if multiple distinct intents exist beyond login/logout, ship intent-return (capture intended target, re-auth, return). If the population is mostly login/logout-adjacent, straight-to-login is the cheaper correct answer. - **Session expiry duration:** Keep the Laravel default — 120-minute rolling idle timeout. Fits the risk profile (sensitive invoice data, non-custodial wallet); revisit post-launch only if support feedback or threat assessment surfaces a reason to tighten. - **Logout-on-expiry UX:** Surface an inline message on the login page explaining the session expired. Silent redirects are confusing for users. -- **Scope:** Limited to 419-redirect and session-expiry logout. Other auth surfaces (rate limiting, password policy, 2FA) are out of scope for this phase. 2FA → Phase 7. +- **Scope:** Limited to 419-redirect and session-expiry logout. Other auth surfaces (rate limiting, password policy, 2FA) are out of scope for this phase. 2FA → Phase 8. ## 1. Survey current 419 / session behavior diff --git a/docs/strategies/19.3_LLC_FORMATION.md b/docs/strategies/19.3_LLC_FORMATION.md index 1bac4d58..c1c58429 100644 --- a/docs/strategies/19.3_LLC_FORMATION.md +++ b/docs/strategies/19.3_LLC_FORMATION.md @@ -11,7 +11,7 @@ Parent milestone doc: [`docs/milestones/19_RC_HARDENING_OPS.md`](../milestones/1 Personal-budget cap (Dave Ramsey principles, no credit cards) means the $50 AZCC filing fee can't ship until June 1 when the day-job income refreshes. May work is therefore $0 prep only — name research, bank shortlist, operating agreement template draft. Filing and downstream execution begin June 1. -This timing fits cleanly with the cross-phase dependency: Phase 4 (Legal Layer) drafts ToS/Privacy Policy with placeholder entity names in parallel during May/June. The actual entity-name swap and publication is a deploy-time finishing step in **MS21** so MS19 close doesn't wait on LLC formation completion. +This timing fits cleanly with the parallel-track approach: Phase 5 (Legal Layer) drafts ToS/Privacy Policy with placeholder entity names in parallel during May/June. LLC formation and Legal Layer drafting do not gate each other — drafting + UI work can proceed regardless of LLC status. The actual entity-name swap and publication is a deploy-time finishing step in **MS21** so MS19 close doesn't wait on LLC formation completion. ## Decisions made @@ -59,7 +59,7 @@ This timing fits cleanly with the cross-phase dependency: Phase 4 (Legal Layer) 1. [ ] Update `cryptozing.app` site footer / contact info to reflect the LLC name. 2. [ ] Update places in the app where the operator/issuer is named (settings, mail headers, copyright notices, etc.) to use the LLC name where appropriate. 3. [ ] Update `docs/PRODUCT_SPEC.md`, `README.md`, and other repo docs that name the operator. -4. [ ] Notify Phase 4 (Legal Layer) that the placeholder entity name in the drafted ToS/Privacy Policy can be swapped for the actual LLC name. (The final swap-and-publish step lives at MS21 deploy time, not in MS19.) +4. [ ] Notify Phase 5 (Legal Layer) that the placeholder entity name in the drafted ToS/Privacy Policy can be swapped for the actual LLC name. (The final swap-and-publish step lives at MS21 deploy time, not in MS19.) ## 7. E&O Insurance (deferred — revenue-gated) @@ -76,7 +76,7 @@ E&O insurance is deferred until CryptoZing generates revenue that justifies the - [ ] Operating Agreement signed. - [ ] AFCU business checking account opened in the LLC's name; CryptoZing income/expenses routing through it exclusively. - [ ] CryptoZing references updated to reflect the LLC entity (site, app, repo docs). -- [ ] Phase 4 (Legal Layer) notified of final entity name; the deploy-time ToS/Privacy swap-and-publish step in MS21 has the data it needs. +- [ ] Phase 5 (Legal Layer) notified of final entity name; the deploy-time ToS/Privacy swap-and-publish step in MS21 has the data it needs. - [ ] E&O insurance status formally recorded as revenue-gated deferral in operating-expense plan. ## Reference diff --git a/docs/strategies/19.4_VISUAL_IDENTITY_POLISH.md b/docs/strategies/19.4_VISUAL_IDENTITY_POLISH.md new file mode 100644 index 00000000..1d0bb5b6 --- /dev/null +++ b/docs/strategies/19.4_VISUAL_IDENTITY_POLISH.md @@ -0,0 +1,76 @@ +# MS19 Phase 4 Strategy — Visual Identity Polish + +Status: Not started. +Parent milestone doc: [`docs/milestones/19_RC_HARDENING_OPS.md`](../milestones/19_RC_HARDENING_OPS.md) + +**Goal:** Land the visual/brand polish pass before open beta — favicon set across all surfaces (theme-cohesive yet purpose-distinguishable), open-beta copy refactor across user-facing surfaces, and og:image/social-preview consistency. Three deliverables in one phase because they share surfaces, share approval taste, and benefit from one coherent design pass instead of three drift-prone ones. + +This phase has **no dependency on Phase 3 (LLC)** — it is pure visual/copy work. Slotted at Phase 4 so subsequent phases (Legal Layer, Content Promises, Contributor Docs) review their surfaces in their final visual state instead of pre-polish. + +## Decisions to confirm before flesh-out + +- **Favicon design direction:** Lift existing CryptoZing brand colors/glyph and produce variants, or commission/draft a fresh icon system. _Answer:_ _(pending)_ +- **"Same theme yet distinctive" mechanism:** How do per-surface favicons differ — color shifts on a shared glyph, glyph variations on a shared color, badge overlays, or mixed approach. _Answer:_ _(pending)_ +- **Surface inventory:** Confirmed surfaces needing distinct favicons. Working list: (a) Laravel app — main `cryptozing.app`, (b) marketing/Pages site under `site/`, (c) GitHub Pages placeholder if it stays distinct from the marketing site, (d) any per-area distinction (wallet vs. invoice vs. settings) we want to surface. _Answer:_ _(pending)_ +- **og:image style:** Match the favicon system, or distinct visual language for social cards (since they're consumed at much larger size). _Answer:_ _(pending)_ + +## Decisions made + +- **Open-beta refactor placement:** Folded into this phase (§3). Bundled here rather than Phase 5 (Legal Layer) per the same "one coherent visual/brand pass" reasoning that bundled favicons + og:images. +- **"RC" terminology in internal docs:** Preserved. Refactor in §3 targets user-facing surfaces only — AGENTS.md, milestone docs, strategy docs keep "RC" terminology so internal planning language stays stable. + +## 1. Favicon set + +1. [ ] [User] Lock the design direction per "Decisions to confirm" (color/glyph approach + per-surface mechanism). +2. [ ] [User] Confirm final surface inventory (which surfaces ship which favicon variant). +3. [ ] [Agent or User] Generate the favicon assets at all standard sizes per surface — `favicon.ico` (multi-resolution), `favicon-16x16.png`, `favicon-32x32.png`, `apple-touch-icon.png` (180x180), `android-chrome-192x192.png`, `android-chrome-512x512.png`, `safari-pinned-tab.svg` (monochrome), and a `site.webmanifest` per surface. Tooling choice (RealFaviconGenerator, custom script, manual export) decided at execution time. +4. [ ] [Agent] Wire `` and related meta tags into the Laravel app's base layout(s). +5. [ ] [Agent] Wire favicon assets into the marketing/Pages site under `site/`. +6. [ ] [Agent] Wire favicon assets into the GitHub Pages placeholder if it remains distinct. +7. [ ] [User] Eyeball verification across browsers — Chrome, Firefox, Safari, mobile Safari, mobile Chrome. Pinned-tab rendering checked in Safari specifically. +8. [ ] [User] Verify on a high-resolution display that the icon renders cleanly at small sizes; not just visually correct at preview-large. + +## 2. og:image / social previews + +1. [ ] [User] Lock og:image visual style per "Decisions to confirm." +2. [ ] [Agent or User] Generate 1200×630 og:image assets for primary surfaces (homepage, marketing landing, key article pages if applicable). +3. [ ] [Agent] Wire `og:image`, `og:title`, `og:description`, `twitter:card` meta tags into the relevant layouts/pages. +4. [ ] [User] Validate rendering using each platform's preview tool — Twitter Card Validator, LinkedIn Post Inspector, Facebook Sharing Debugger. +5. [ ] [User] Confirm fallback rendering is acceptable on platforms that don't pull og:image perfectly (Slack, Discord, iMessage previews). + +## 3. "Pre-release" / "Release Candidate" → "open beta" refactor + +(Lifted from former Phase 5 §5; bundled here so the open-beta look-and-feel ships in one pass with favicons and social previews.) + +1. [ ] [Agent] Find all "pre-release" / "Release Candidate" / "RC" mentions in user-facing copy: `site/`, articles, public invoice views, signup/onboarding flows, mail copy, error pages, footer. +2. [ ] [Agent] Replace with "open beta" framing where appropriate. Some instances may need contextual rewrite rather than direct substitution — flag those for [User] review rather than auto-replacing. +3. [ ] [User] Review the replacement set; approve or revise context-specific cases. +4. [ ] [Agent] Apply approved replacements. +5. Internal docs (AGENTS.md, milestone docs, strategy docs, AgentRoles/) keep "RC" terminology — this refactor is for user-facing surfaces only. + +## 4. Verification + +1. [ ] [Agent] Run `php artisan view:clear` and verify all changed Blade templates render. +2. [ ] [User] Walk a fresh-eyes path through the public surfaces with a clean browser cache: marketing site, signup, dashboard, invoice creation, public invoice view. Favicon, copy, and social-preview elements all reflect the open-beta brand state. +3. [ ] [Agent] Grep the codebase one more time for stragglers: any "pre-release" / "RC" / "Release Candidate" in user-facing files that escaped §3. +4. [ ] [User] Quick mobile check on the same path — favicon shows correctly in mobile browser tabs; copy renders without overflow/awkward wrapping under the new framing. + +## 5. Tests + +1. [ ] [Agent] Add a feature test asserting the base layout includes the expected favicon link tags + og:image meta tags, so future template refactors don't silently strip them. +2. [ ] [Agent] If reasonable, add a content test that no user-facing Blade template contains "pre-release" / "Release Candidate" / "RC" string after the refactor — guards against regression. + +## Exit Criteria + +- [ ] Favicon set generated and wired across all in-scope surfaces (Laravel app, marketing site, any other distinct surface). +- [ ] Per-surface favicon variants follow the locked theme-cohesive-yet-distinguishable system. +- [ ] og:image / social-preview meta tags wired on primary surfaces and validated against major platform preview tools. +- [ ] User-facing "pre-release" / "Release Candidate" copy replaced with "open beta" framing across all surfaces; internal docs unchanged. +- [ ] Eyeball QA pass complete across desktop and mobile browsers. +- [ ] Tests in §5 added so layout regressions don't silently strip the work. + +## Reference + +- Standard favicon set sizes: 16x16, 32x32, 48x48 (in .ico), 180x180 (apple-touch-icon), 192x192 + 512x512 (android-chrome), monochrome SVG (safari-pinned-tab). +- og:image: 1200×630 is the consensus size for cross-platform social preview cards. +- Useful tooling (decision left to execution time): RealFaviconGenerator (favicon.io), Squoosh (image optimization), Twitter Card Validator, LinkedIn Post Inspector, Facebook Sharing Debugger. diff --git a/docs/strategies/19.4_LEGAL_LAYER.md b/docs/strategies/19.5_LEGAL_LAYER.md similarity index 89% rename from docs/strategies/19.4_LEGAL_LAYER.md rename to docs/strategies/19.5_LEGAL_LAYER.md index 42ac0aaf..e227d909 100644 --- a/docs/strategies/19.4_LEGAL_LAYER.md +++ b/docs/strategies/19.5_LEGAL_LAYER.md @@ -1,10 +1,12 @@ -# MS19 Phase 4 Strategy — Legal Layer +# MS19 Phase 5 Strategy — Legal Layer Status: Not started. Parent milestone doc: [`docs/milestones/19_RC_HARDENING_OPS.md`](../milestones/19_RC_HARDENING_OPS.md) **Goal:** Draft ToS, Privacy Policy, and disclaimer copy with placeholder entity names; review existing UI/mail copy for monetization-neutral language; scaffold UI placement so the legal layer is staged for activation. Drafting and scaffolding land in this phase. Final entity-name swap (using the LLC formed in Phase 3) and publication to the live site is the deploy-time finishing step in MS21 — *not* in this phase. +Note on phase ordering: Phase 3 (LLC) and Phase 5 (Legal Layer) run as independent parallel tracks. Drafting/UI work here has no dependency on LLC formation status — only the deploy-time entity-name swap at MS21 needs the formed entity. Both must land before MS21; neither gates the other phase-by-phase. + Decisions inherited from the milestone doc that flow into this phase: - **Legal approach:** No lawyer for RC1. Self-drafted ToS and Privacy Policy covering essentials. - **Disclaimer surfaces:** Account signup, wallet onboarding, invoice/payment screens. Footer links to ToS and Privacy Policy on every page. @@ -17,7 +19,7 @@ Decisions inherited from the milestone doc that flow into this phase: - **Pages location:** Both — `/terms` and `/privacy` on the public GitHub Pages site (`site/`) AND in the Laravel app. App needs them reachable from authenticated views; the public site needs them for unauthenticated visitors and SEO. - **Monetization-safe language guide location:** Section in [`docs/UX_GUARDRAILS.md`](../UX_GUARDRAILS.md). Fits the existing copy-rules section; consulted during copy work, which is exactly when this guide matters. - **Existing-copy review scope:** Sweep all UI and mail copy — not limited to public-facing surfaces. -- **"Open beta" copy refactor placement:** Folded into this phase (§5). +- **"Open beta" copy refactor placement:** Moved to Phase 4 (Visual Identity Polish) — bundled with favicon overhaul and og:image work as one coherent visual/brand pass. ## 1. Draft Terms of Service @@ -47,19 +49,13 @@ Decisions inherited from the milestone doc that flow into this phase: 3. [ ] Flag language that forecloses pricing options (e.g., "always free," "no fees ever"). 4. [ ] Revise flagged items in place. -## 5. "Pre-release" / "Release Candidate" → "open beta" refactor - -1. [ ] Find all "pre-release"/"Release Candidate" mentions in user-facing copy: site/, articles, public invoice views, signup/onboarding, mail copy. -2. [ ] Replace with "open beta" framing where appropriate. -3. [ ] Internal docs (AGENTS.md, milestone docs) keep "RC" terminology — this refactor is for user-facing surfaces only. - -## 6. Monetization-safe language guide +## 5. Monetization-safe language guide 1. [ ] Add new section to [`docs/UX_GUARDRAILS.md`](../UX_GUARDRAILS.md): "Monetization-neutral language." 2. [ ] Include concrete rules (don't promise no fees / always free; do leave room for future paid tiers) and 3–5 do/don't examples. 3. [ ] Reference from `CONTENT_PROMISES.md` when adding new entries that touch pricing language. -## 7. UI placement +## 6. UI placement 1. [ ] Add disclaimer copy at signup, wallet onboarding, invoice/payment surfaces. 2. [ ] Add footer ToS / Privacy Policy link scaffolding across all pages (placeholder URLs to `/terms` and `/privacy`; final URLs activate at MS21 deploy). @@ -72,5 +68,4 @@ Decisions inherited from the milestone doc that flow into this phase: - [ ] Disclaimer copy present at signup, wallet onboarding, and invoice/payment surfaces (entity-agnostic where possible; placeholder where not). - [ ] Footer ToS / Privacy Policy link scaffolding in place across all pages; final wiring to live URLs activates at MS21 deploy. - [ ] All UI and mail copy reviewed for monetization-neutral language; issues resolved. -- [ ] "Pre-release"/"RC" → "open beta" refactor complete across user-facing surfaces. - [ ] Monetization-safe language guide section added to `UX_GUARDRAILS.md`. diff --git a/docs/strategies/19.5_CONTENT_PROMISES_RECONCILIATION.md b/docs/strategies/19.6_CONTENT_PROMISES_RECONCILIATION.md similarity index 96% rename from docs/strategies/19.5_CONTENT_PROMISES_RECONCILIATION.md rename to docs/strategies/19.6_CONTENT_PROMISES_RECONCILIATION.md index e7cda003..2c41f4fd 100644 --- a/docs/strategies/19.5_CONTENT_PROMISES_RECONCILIATION.md +++ b/docs/strategies/19.6_CONTENT_PROMISES_RECONCILIATION.md @@ -1,4 +1,4 @@ -# MS19 Phase 5 Strategy — Content Promises Reconciliation +# MS19 Phase 6 Strategy — Content Promises Reconciliation Status: Not started. Parent milestone doc: [`docs/milestones/19_RC_HARDENING_OPS.md`](../milestones/19_RC_HARDENING_OPS.md) diff --git a/docs/strategies/19.6_CONTRIBUTOR_DOCS.md b/docs/strategies/19.7_CONTRIBUTOR_DOCS.md similarity index 97% rename from docs/strategies/19.6_CONTRIBUTOR_DOCS.md rename to docs/strategies/19.7_CONTRIBUTOR_DOCS.md index 041245e1..42797190 100644 --- a/docs/strategies/19.6_CONTRIBUTOR_DOCS.md +++ b/docs/strategies/19.7_CONTRIBUTOR_DOCS.md @@ -1,4 +1,4 @@ -# MS19 Phase 6 Strategy — Contributor Docs Review +# MS19 Phase 7 Strategy — Contributor Docs Review Status: Not started. Parent milestone doc: [`docs/milestones/19_RC_HARDENING_OPS.md`](../milestones/19_RC_HARDENING_OPS.md) diff --git a/docs/strategies/19.7_TWO_FACTOR_AUTHENTICATION.md b/docs/strategies/19.8_TWO_FACTOR_AUTHENTICATION.md similarity index 96% rename from docs/strategies/19.7_TWO_FACTOR_AUTHENTICATION.md rename to docs/strategies/19.8_TWO_FACTOR_AUTHENTICATION.md index cf09890e..226ad63e 100644 --- a/docs/strategies/19.7_TWO_FACTOR_AUTHENTICATION.md +++ b/docs/strategies/19.8_TWO_FACTOR_AUTHENTICATION.md @@ -1,15 +1,15 @@ -# MS19 Phase 7 Strategy — Two-Factor Authentication +# MS19 Phase 8 Strategy — Two-Factor Authentication Status: Not started. Parent milestone doc: [`docs/milestones/19_RC_HARDENING_OPS.md`](../milestones/19_RC_HARDENING_OPS.md) -**Goal:** Add 2FA capability for RC. Email-based 2FA is the baseline (mandatory deliverable). TOTP / authenticator-app 2FA is an opportunistic add — included if Phase 7 begins with meaningful time remaining before MS19 close, otherwise deferred to the 2028 release. A lightweight recommendation surface points users without 2FA toward enabling it. +**Goal:** Add 2FA capability for RC. Email-based 2FA is the baseline (mandatory deliverable). TOTP / authenticator-app 2FA is an opportunistic add — included if Phase 8 begins with meaningful time remaining before MS19 close, otherwise deferred to the 2028 release. A lightweight recommendation surface points users without 2FA toward enabling it. This phase is **positionally last by design** — if additional phases are ever added to MS19, 2FA stays at the end of the rollup so it can ship on the latest feedback or be deferred most cleanly. ## Decisions made -- **TOTP cutoff trigger:** If Phase 7 begins with at least 3 working days remaining before MS19 close, attempt TOTP. Otherwise email-only and defer TOTP to the 2028 release. +- **TOTP cutoff trigger:** If Phase 8 begins with at least 3 working days remaining before MS19 close, attempt TOTP. Otherwise email-only and defer TOTP to the 2028 release. - **Recovery for email 2FA:** Option A — recovery codes. Generate ~10 single-use codes at 2FA enable time, display once, store hashed, single-use. User saves them out-of-band (password manager, paper, etc.). If both email access AND all recovery codes are lost, account recreation is the answer (documented honestly in user help). No SMS fallback — NIST-deprecated as 2FA, requires paid vendor, weak against SIM-swap. - **Code TTL and attempt lockout:** 10-minute TTL on the 6-digit code; 5 failed attempts before account locks for 15 minutes. - **Recommendation surface:** Non-blocking dashboard banner only. No settings-page hint or soft-block modal — keep beta UX un-naggy. diff --git a/docs/strategies/x18.3_SITE_ARCHITECTURE_AND_PUBLISHING.md b/docs/strategies/x18.3_SITE_ARCHITECTURE_AND_PUBLISHING.md index 6c33b188..3fef3d02 100644 --- a/docs/strategies/x18.3_SITE_ARCHITECTURE_AND_PUBLISHING.md +++ b/docs/strategies/x18.3_SITE_ARCHITECTURE_AND_PUBLISHING.md @@ -22,7 +22,7 @@ This is the review pass that MS19 reconciliation builds on. Small copy tweaks ar 1. [x] Scan all published content for promises and update [`docs/CONTENT_PROMISES.md`](../CONTENT_PROMISES.md) with any missing entries. 2. [x] Walk each open entry against current product behavior. 3. [x] Fix minor copy issues in place. _(None found — all claims verified against codebase.)_ -4. [x] Add any heavier items to the MS19 milestone stub as reconciliation work. _(MS19 Phase 4 already covers catalog reconciliation. Major entries with open status feed directly into it.)_ +4. [x] Add any heavier items to the MS19 milestone stub as reconciliation work. _(MS19 Phase 6 covers catalog reconciliation per the post-2026-05-15 phase renumber. Major entries with open status feed directly into it.)_ ## 3. Video production diff --git a/docs/strategies/x19.1_NOTIFICATION_COVERAGE_AUDIT.md b/docs/strategies/x19.1_NOTIFICATION_COVERAGE_AUDIT.md deleted file mode 100644 index f22be040..00000000 --- a/docs/strategies/x19.1_NOTIFICATION_COVERAGE_AUDIT.md +++ /dev/null @@ -1,114 +0,0 @@ -# MS19 Phase 1 Strategy — Notification Coverage Audit - -Status: Complete. -Parent milestone doc: [`docs/milestones/19_RC_HARDENING_OPS.md`](../milestones/19_RC_HARDENING_OPS.md) - -**Goal:** Finish the coverage matrix that [`docs/specs/NOTIFICATIONS.md`](../specs/NOTIFICATIONS.md) flagged as an MS17 deliverable but never received. Confirm every active outbound mail class has a documented row covering audience, trigger, code reference, status, feature test, and delivery-log type. Tweak the spec narrative inline where code has drifted from documented behavior. - -## Decisions made - -- **Inventory location:** Extend `NOTIFICATIONS.md`. The matrix slot already exists at the bottom of the spec under `## Coverage & Status (MS17 deliverable)`. -- **Coverage format:** Use the column set the spec already specifies — Audience, Trigger, Mailable class, Status (`live` / `stubbed` / `planned`), Feature test(s), Delivery log type. -- **Gap-closure scope:** Fixes land in this phase by default. If a finding is small-consequence relative to its work cost, route it to [`docs/BACKLOG.md`](../BACKLOG.md) — but only when MS19's phase goal and exit criteria still hold without the deferred work. - -## Carried-in known finding (resolved) - -`Tests\Feature\InvoiceNotificationTest` was failing across every recent PR back to MS17 — past-due delivery assertions expecting `queued` observed `skipped`/`sent`. Root cause: `.env.example` shipped `MAIL_OUTBOUND_ENABLED=false` and the PR Tests workflow copied it verbatim into `.env`, so `InvoiceDeliveryService::skipReason()` marked every queued row as skipped. Resolved via [#68](https://github.com/n8bar/CryptoZing/issues/68) / [#69](https://github.com/n8bar/CryptoZing/pull/69) by pinning the env var in `phpunit.xml`. First Issue under the M19 GitHub Issues trial (see milestone doc Decisions recorded). - -## 1. Enumerate the live mail surface - -1. [x] Re-list every Mail class under `app/Mail/` directly from disk; do not trust the snapshot in the Reference section without re-verifying. -2. [x] List every Notification class under `app/Notifications/` (if any). -3. [x] Identify any in-app outbound paths that bypass Mail/Notification classes (raw mailer calls in Jobs, Commands, or Console scheduled tasks). -4. [x] Cross-check the resulting set against the notice classes named in `NOTIFICATIONS.md` Sections 3–5 — flag anything in code without a spec entry, or anything in the spec without code. - -**§1 results (2026-05-09):** - -- **Mail classes (14):** `app/Mail/` matches the Reference snapshot — no drift since drafting. -- **Notification classes (0):** No `app/Notifications/` directory. -- **Outbound paths (2 callsites, both via Mail classes — no raw-mailer bypass):** - - `App\Jobs\DeliverInvoiceMail::handle` — canonical delivery-log-driven job; renders the right Mailable per `delivery->type` (13 mappings + `InvoiceReadyMail` default for manual sends) and dispatches via `Mail::to(...)->send(...)`. - - `App\Http\Controllers\NotificationSettingsController::send` — owner-only branding preview; sends `NotificationBrandingPreviewMail` directly without creating a delivery-log row (per spec §5.14.4.1). -- **Drift flags for §3:** - - **Spec narrative gap — overpay/underpay paired classes (resolved):** Code has `InvoiceOverpaymentIssuerMail` and `InvoiceUnderpaymentIssuerMail` paired with their client siblings; spec §4.3 and §4.4 originally titled these alerts as "(Client)" only. Pulled forward from §3 and applied: §4.3 and §4.4 retitled as "(Owner + Client)" with an owner-alert sub-bullet derived from the actual issuer mail bodies (overpay disposition prompt; underpay outstanding-balance follow-up). §5.4 paired-pattern and §5.12.2 "Underpayment alert (owner)" language are now consistent with §4. - - **Partial-warning status — stubbed by design:** `InvoicePartialWarningClientMail` and `InvoicePartialWarningIssuerMail` are wired into `DeliverInvoiceMail`'s type map, but no service code queues new `*_partial_warning` deliveries; `InvoiceAlertService::skipInvalidQueuedDeliveries()` only skips legacy queued rows. Tests `test_repeated_partial_payment_detection_no_longer_creates_a_separate_partial_warning_family` and `..._does_not_enqueue_legacy_partial_warnings_on_repeat_runs` assert zero new partial-warning rows. Matches spec §4.4.3 (deprecating the family) and §5.12.3 (legacy display only). To be recorded as `stubbed` in the §2 matrix. - -## 2. Build the coverage matrix in `NOTIFICATIONS.md` - -1. [x] Replace the placeholder under `## Coverage & Status (MS17 deliverable)` with a Markdown table using the spec's column set: Audience | Trigger | Mailable class | Status | Feature test(s) | Delivery log type. -2. [x] Populate one row per notice class identified in §1. -3. [x] For each row's **Status** field, record `live` (in production code, exercising correctly), `stubbed` (class exists but doesn't behave per spec), or `planned` (named in spec, not implemented). -4. [x] For each row's **Feature test(s)** field, link to the corresponding test under `tests/Feature/**` if one exists; record `none` if not. -5. [x] For each row's **Delivery log type** field, record the log-type identifier the class uses when creating delivery-log entries. -6. [x] Drop the "(MS17 deliverable)" suffix from the section heading once the matrix is populated. - -**§2 results (2026-05-09):** 14 rows populated — 12 `live`, 2 `stubbed` (partial-warning legacy classes). All Mailables have feature-test coverage. Delivery log types match `DeliverInvoiceMail`'s type→Mailable map plus `send` (manual default) and `—` for the branding preview (which deliberately bypasses the delivery log). - -## 3. Reconcile drift and tweak the narrative - -1. [x] For each matrix row, compare the actual class behavior to the documented behavior in `NOTIFICATIONS.md` Sections 3–5. -2. [x] Where the spec is right and the code drifted, capture a finding (audience, class, what the spec says vs. what the code does) for §4. -3. [x] Where the code is right and the spec is stale, tweak the spec narrative inline — small edits only. (Pulled forward earlier for the overpay/underpay paired-class titling — see §1 drift flags.) -4. [x] If a tweak feels larger than "small" (rewrites a paragraph, changes a documented invariant, contradicts a §3–§5 commitment), stop and surface for confirmation before applying. - -**§3 results (2026-05-09):** 11 rows aligned with spec; 3 drift findings raised for §4 (one needing a user decision on direction, two small-scoped). 14th-row check (overpay/underpay narrative) was applied earlier in this phase. - -### Findings raised for §4 - -**F1 — Manual send does not transition invoice out of `draft` (decision needed).** -- Spec §3.1.2 says: "Sending the invoice queues outbound delivery, records the attempt in the delivery log, **and issues the invoice out of `draft`**." -- Code: `InvoiceDeliveryController::store` queues the `send` delivery row but does not update `Invoice::status`. `Invoice::refreshPaymentState()` explicitly skips `draft`/`void` invoices, so a draft that gets sent stays at `status='draft'` until something else moves it. -- This is bigger than a "small inline tweak" per §3.4 — needs a deliberate decision: either the spec is over-specifying (drop the draft-transition clause) or the code is missing a status update on successful queue (small but real code change). - -**F2 — Spec uses "(owner)" in display-label examples; shipped labels use "(issuer)".** -- Spec §5.12.2 example: `Underpayment alert (owner)`. Spec §5.12.3 example: `Partial payment warning (client|owner)`. -- Code (`InvoiceDelivery::typeLabel()`): consistently uses `(issuer)` after the MS17 owner→issuer terminology sweep — `Underpayment alert (issuer)`, `Partial payment warning (issuer)`, etc. -- Resolution: small spec tweak — replace "(owner)" with "(issuer)" in §5.12.2 and §5.12.3 examples so the spec matches the shipped labels. - -**F3 — Client underpayment alert view omits BTC outstanding amount (decision needed, small either way).** -- Spec §4.4.2: client alert should "include the outstanding USD/BTC amounts." -- Code (`resources/views/mail/invoice-underpayment-client.blade.php`): shows the outstanding USD amount only; no BTC line. -- The legacy `invoice-partial-warning-client.blade.php` view does show both USD and BTC, so the project pattern supports either interpretation. -- Two reasonable resolutions: (a) add a BTC line to the underpay-client view for spec compliance, or (b) drop "/BTC" from §4.4.2 since the linked invoice page already surfaces full BTC details. - -## 4. Resolve findings - -1. [x] For each in-phase finding from §3, ship the fix (code change or test) within Phase 1 and check off the finding here. -2. [x] For each backlog-bound finding (small consequence, high effort), add an entry to `docs/BACKLOG.md` with enough context to pick it up later. List the deferred finding here with a `[deferred → backlog]` note. -3. [x] Confirm the Phase 1 Exit Criteria still hold after any deferrals. - -**§4 results (2026-05-09):** All three §3 findings resolved in-phase; no backlog deferrals. - -- **F1 — Manual send draft transition (Option C, applied):** Spec §3.1.2 reworded to make the transition rule explicit and conditional with a no-regression guarantee. `InvoiceDeliveryController::store` updated to transition `draft` → `sent` after a successful queue. Two new tests in `InvoiceDeliveryTest` (`test_sending_draft_invoice_transitions_status_to_sent`, `test_sending_does_not_regress_status_for_non_draft_invoice`). -- **F2 — Owner → Issuer in spec display labels:** §5.12.2 and §5.12.3 examples replaced with `(issuer)` to match shipped `InvoiceDelivery::typeLabel()` output. Catches the spec up to the MS17 terminology sweep. -- **F3 — Client underpay alert USD/BTC (b, applied):** §4.4.2 narrowed to "outstanding USD amount." Keeps the spec loose; the linked invoice page surfaces full BTC details, and the email body stays simple/USD-focused for the typical recipient. Code is free to add a BTC line or current-rate hint as a future enhancement without a re-spec round. - -## Exit Criteria - -- [x] `NOTIFICATIONS.md` Coverage & Status section contains a populated matrix with one row per active notice class. -- [x] Every Mail/Notification class in code is represented in the matrix. -- [x] Every notice class named in spec Sections 3–5 has a matching matrix row. -- [x] Drift findings are either fixed in this phase or recorded in `BACKLOG.md` with context. -- [x] `(MS17 deliverable)` placeholder text is removed from the spec heading. -- [x] CI flakiness in `Tests\Feature\InvoiceNotificationTest` (past-due delivery status: expected `queued`, observed `skipped`/`sent`) investigated and resolved. Tests pass green on a Phase-1-close PR. (Resolved via [#68](https://github.com/n8bar/CryptoZing/issues/68).) - -## Reference - -Mail classes present in `app/Mail/` at skeleton drafting (2026-05-08). Re-enumerate in §1.1; this list is a starting point, not authoritative. - -- `InvoiceIssuerPaidNoticeMail.php` -- `InvoiceOverpaymentClientMail.php` -- `InvoiceOverpaymentIssuerMail.php` -- `InvoicePaidReceiptMail.php` -- `InvoicePartialWarningClientMail.php` -- `InvoicePartialWarningIssuerMail.php` -- `InvoicePastDueClientMail.php` -- `InvoicePastDueIssuerMail.php` -- `InvoicePaymentAcknowledgmentClientMail.php` -- `InvoicePaymentAcknowledgmentIssuerMail.php` -- `InvoiceReadyMail.php` -- `InvoiceUnderpaymentClientMail.php` -- `InvoiceUnderpaymentIssuerMail.php` -- `NotificationBrandingPreviewMail.php` - -`NOTIFICATIONS.md` self-flagged the gap at lines 99–100 (the `## Coverage & Status (MS17 deliverable)` heading exists; the matrix below it does not). From a3e9fb8d95459acb41c35706604819f3565c6963 Mon Sep 17 00:00:00 2001 From: Nate Barlow Date: Fri, 15 May 2026 18:17:26 -0600 Subject: [PATCH 02/11] =?UTF-8?q?M19.1=20=C2=A75:=20renumber=20sub-section?= =?UTF-8?q?s=20per=20x14.5/x17.1=20convention?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace dotted notation (### 5.0, ### 5.1, ..., ### 5.10) with restart-at-1 numbering (### 1, ### 2, ..., ### 11) to match the H3 sub-section pattern established in x14.5_BQA_FINDINGS_AND_FIXES.md and x17.1_ISSUER_SWEEP.md. The dotted scheme was unique to my §5 and inconsistent with the rest of the project. Internal back-refs and Exit Criteria parentheticals updated to use prose descriptions instead of dotted-section numbers. Co-Authored-By: Claude Opus 4.7 --- .../19.1_NOTIFICATION_COVERAGE_AUDIT.md | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md index 25806822..0d5e81ff 100644 --- a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md +++ b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md @@ -94,53 +94,53 @@ Owner tags: `[Agent]` = critical-path code/doc/verification work I drive directl Bar: every notice type observed at least once end-to-end — from real trigger in the running stack, through queue + delivery-log + actual mail-server submission, to landing in a real inbox at the intended address with subject/branding/links/copy passing eyeball QA. "Feature test passed" is not enough; that was already true at §1 close. -### 5.0 Catch-all flip (Option A — early in §5) +### 1. Catch-all flip (Option A) The catch-all alias (`MAIL_ALIAS_ENABLED=true` rewriting recipients to `mailer.cryptozing.app`) is disabled at the start of §5 so all subsequent scenarios exercise real recipient routing. Aliasing **stays off** for the remainder of MS19 — Phases 2–8 then emit mail under prod-like routing, so any later regression is caught in the right state. 1. [ ] [Agent] Set `MAIL_ALIAS_ENABLED=false` in `.env`. Confirm `MAIL_ALIAS_DOMAIN` left intact for documentation but inactive. 2. [ ] [Agent] Trigger a single low-stakes test mail (manual-send `InvoiceReadyMail` to a controlled recipient) and verify via Mailgun logs + recipient inbox that no rewrite occurred. -3. [ ] [User] Confirm receipt of the un-aliased mail at the intended address. Sign off on §5.0 before §5.1 begins. -4. [ ] [Agent] Document the active test addresses in this section — agent-suggested list below; [User] may override at §5.0 sign-off: +3. [ ] [User] Confirm receipt of the un-aliased mail at the intended address. Sign off on this sub-section before the next one begins. +4. [ ] [Agent] Document the active test addresses in this section — agent-suggested list below; [User] may override at sign-off: - **Issuer persona:** `issuer-test@nateTheProgrammer.com` - **Client persona:** `client-test@nospam.site` - **Owner-alert recipient:** `owner-alerts@cybercreek.us` - **Real eyeball-QA inbox:** `nate@nateTheProgrammer.com` -### 5.1 Manual issue (`InvoiceReadyMail` + draft→sent transition) +### 2. Manual issue (`InvoiceReadyMail` + draft→sent transition) -1. [ ] [Agent] Create a draft invoice via the app; set client recipient to the §5.0 client persona. +1. [ ] [Agent] Create a draft invoice via the app; set client recipient to the client persona established in the catch-all flip sub-section. 2. [ ] [Agent] Trigger send; confirm `InvoiceReadyMail` queued, `delivery_log` row of type `send` created, invoice transitioned `draft` → `sent`. 3. [ ] [Agent] Confirm Mailgun accepted the message (check provider logs / message-id round-trip). 4. [ ] [User] Receive the mail at the client persona inbox; verify subject/branding/links/copy. Click invoice link, confirm public invoice view loads correctly. -### 5.2 Pay full (`InvoicePaidReceiptMail` client + `InvoiceIssuerPaidNoticeMail` issuer) +### 3. Pay full (`InvoicePaidReceiptMail` client + `InvoiceIssuerPaidNoticeMail` issuer) 1. [ ] [Agent] Take an issued invoice, simulate a confirmed full payment (signet/regtest payment or fixture-based payment-state advance). 2. [ ] [Agent] Verify both Mailables fire; confirm both delivery-log rows created (`paid_client`, `paid_issuer`). 3. [ ] [User] Receive both at client + issuer personas. Verify TXID renders cleanly on narrow viewports (Finding 1 from MS17 — regression check), copy is appropriate, paid receipt looks like a paid receipt. -### 5.3 Partial payment acknowledgment (`InvoicePaymentAcknowledgmentClientMail` + `InvoicePaymentAcknowledgmentIssuerMail`) +### 4. Partial payment acknowledgment (`InvoicePaymentAcknowledgmentClientMail` + `InvoicePaymentAcknowledgmentIssuerMail`) 1. [ ] [Agent] Take an issued invoice, simulate a partial confirmed payment. 2. [ ] [Agent] Verify both acknowledgment Mailables fire; confirm delivery-log rows. 3. [ ] [User] Receive both. Verify the acknowledgment framing is honest about "we received some" vs. final settlement. -### 5.4 Underpayment alert (`InvoiceUnderpaymentClientMail` + `InvoiceUnderpaymentIssuerMail`) +### 5. Underpayment alert (`InvoiceUnderpaymentClientMail` + `InvoiceUnderpaymentIssuerMail`) 1. [ ] [Agent] Take an issued invoice; simulate payment below threshold; advance scheduler past the underpay-alert cooldown so the alert is eligible to fire. 2. [ ] [Agent] Run the alert pass; verify both underpayment Mailables fire; confirm delivery-log rows. 3. [ ] [User] Receive both. Verify client view shows the outstanding USD amount per spec §4.4.2 (post-F3 narrowing); issuer view surfaces the outstanding-balance follow-up per spec §4.4. 4. [ ] [Agent] Re-run the alert pass within cooldown; verify NO duplicate row is written (regression check on Findings 7 & 8 from MS17). -### 5.5 Overpayment alert (`InvoiceOverpaymentClientMail` + `InvoiceOverpaymentIssuerMail`) +### 6. Overpayment alert (`InvoiceOverpaymentClientMail` + `InvoiceOverpaymentIssuerMail`) 1. [ ] [Agent] Take an issued invoice; simulate payment above threshold. 2. [ ] [Agent] Run the alert pass; verify both overpayment Mailables fire; confirm delivery-log rows. 3. [ ] [User] Receive both. Verify the issuer view's overpay disposition prompt per spec §4.3. 4. [ ] [Agent] Re-run within cooldown; verify no duplicate row. -### 5.6 Past-due (force-set due date) (`InvoicePastDueClientMail` + `InvoicePastDueIssuerMail`) +### 7. Past-due (force-set due date) (`InvoicePastDueClientMail` + `InvoicePastDueIssuerMail`) This one matters most — Nate flagged late-payment notifications specifically as critical to verify thoroughly. Use real time-advanced scenarios, not just unit-level "if past due, send mail" assertions. @@ -153,27 +153,27 @@ This one matters most — Nate flagged late-payment notifications specifically a 7. [ ] [Agent] Run `sendPastDueAlerts` again with all three slots already sent; verify NO new delivery-log rows appear (regression check on Findings 3 & 4 from MS17). 8. [ ] [User] Receive each slot at issuer + client personas; spot-check that escalation tone or content actually escalates between slots if that's the spec'd behavior. -### 5.7 Branding preview (`NotificationBrandingPreviewMail`) +### 8. Branding preview (`NotificationBrandingPreviewMail`) 1. [ ] [Agent] As an authenticated owner, trigger the branding preview from `NotificationSettingsController::send`. 2. [ ] [Agent] Confirm the preview Mailable fires and is sent directly without creating a delivery-log row (per spec §5.14.4.1). 3. [ ] [User] Receive the preview; verify branding renders as expected. -### 5.8 Stubbed-by-design (`InvoicePartialWarningClientMail` + `InvoicePartialWarningIssuerMail`) +### 9. Stubbed-by-design (`InvoicePartialWarningClientMail` + `InvoicePartialWarningIssuerMail`) These are wired into the type map but no service code queues new `*_partial_warning` deliveries (per §1 drift findings). Verify they stay quiet. 1. [ ] [Agent] Run a full pass of `InvoiceAlertService` and observe the delivery log: zero new `*_partial_warning` rows should be written for any invoice. 2. [ ] [Agent] Verify the existing tests (`test_repeated_partial_payment_detection_no_longer_creates_a_separate_partial_warning_family`, `..._does_not_enqueue_legacy_partial_warnings_on_repeat_runs`) still pass. -### 5.9 Edge cases +### 10. Edge cases 1. [ ] [Agent] Void an invoice after send; verify no further mail fires for that invoice (no past-due, no underpay, no overpay). 2. [ ] [Agent] Create two invoices with the same client recipient address; verify each invoice's mails route correctly and no cross-contamination of delivery-log entries. 3. [ ] [Agent] Manual-resend `InvoicePaidReceiptMail` within the resend cooldown window; verify the resend is blocked with no new delivery row. 4. [ ] [Agent] Wait past the cooldown; manual-resend; verify a new `resend_*` context-keyed delivery row is created and the mail lands. -### 5.10 Mail-client rendering spot-check +### 11. Mail-client rendering spot-check 1. [ ] [User] For at least one received mail per audience class (client-facing receipt, issuer-facing alert), open in two distinct mail clients (e.g., Gmail web + iOS Mail). Note any rendering breakage — broken images, broken layout, broken links, blocked-content prompts. @@ -210,8 +210,8 @@ We don't have dedicated load-test infrastructure before open beta. This section - [x] CI flakiness in `Tests\Feature\InvoiceNotificationTest` (past-due delivery status: expected `queued`, observed `skipped`/`sent`) investigated and resolved. Tests pass green on a Phase-1-close PR. (Resolved via [#68](https://github.com/n8bar/CryptoZing/issues/68).) - [ ] Catch-all alias disabled (`MAIL_ALIAS_ENABLED=false`); test mail confirmed reaching real intended addresses without rewrite. - [ ] Every notice class observed end-to-end at least once via §5 scenarios — trigger fired, Mailable sent, delivery-log row truthful, mail received at intended recipient, eyeball QA passed. -- [ ] Late-payment / past-due notification flow (§5.6) verified across all three escalation slots with force-set due dates. -- [ ] Stubbed-by-design partial-warning classes (§5.8) confirmed quiet under live alert passes. +- [ ] Late-payment / past-due notification flow verified across all three escalation slots with force-set due dates. +- [ ] Stubbed-by-design partial-warning classes confirmed quiet under live alert passes. - [ ] All §5 findings resolved per §6 — in-phase fixes shipped or backlog-deferred with context. - [ ] §7 stress-readiness check executed; "what was NOT tested" risks documented for post-launch ops. From 0c354c69b8457297635e477ccc45e883523811c0 Mon Sep 17 00:00:00 2001 From: Nate Barlow Date: Sat, 23 May 2026 02:43:42 -0600 Subject: [PATCH 03/11] =?UTF-8?q?M19.1:=20=C2=A75=20restructure=20+=20Issu?= =?UTF-8?q?e=20content=20conventions=20in=20DOC=5FROLES?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §5/§6/§7 collapsed to per-sub-section action lists; old §6 (findings collector) retired and prior §7 (stress-readiness) renumbered to §6 with Mailgun free-tier cap recorded. §5.4 carries action items for #75/#76/#77; §5.9 converted from dormant-verification to removal items per #82; §6 references future-stress trigger #81. Top of strategy doc + milestone Status/Phase 1/Active phase lines stripped of "reopened/added/restructured" cruft (state, not history). §3/§4 audit findings filed retroactively as closed Issues #78/#79/#80; in-doc writeups trimmed to pointers. DOC_ROLES: new Issue content conventions section (required problem, optional repro/refs/fix-direction/scope/test plan, never journaling or internal shorthand, always link the strategy doc). Co-Authored-By: Claude Opus 4.7 --- docs/DOC_ROLES.md | 10 ++ docs/milestones/19_RC_HARDENING_OPS.md | 6 +- .../19.1_NOTIFICATION_COVERAGE_AUDIT.md | 122 ++++++++---------- 3 files changed, 65 insertions(+), 73 deletions(-) diff --git a/docs/DOC_ROLES.md b/docs/DOC_ROLES.md index bda3621f..52f2f10c 100644 --- a/docs/DOC_ROLES.md +++ b/docs/DOC_ROLES.md @@ -48,3 +48,13 @@ Keep these in sync with every merge or scope change. - **Through M19, new findings go to GitHub Issues; existing finding docs are unchanged. M20 kickoff decides whether this doctype reopens, retires, or stays as-is.** New findings/bugs/todos surfaced during M19 are opened as GitHub Issues and closed via `Fixes #N` on the merging PR. Strategy docs reference the Issue number rather than spawning a new `docs/qa/Finding*.md`. - Each existing finding under `docs/qa/Finding*.md` records `Date:` (when reported) near the top, and adds a `Date fixed:` line once resolved with a brief reference to the milestone, PR, or commit that resolved it. - A finding without a `Date fixed:` line is treated as still open. + +## Issue content conventions + +Applies to any GitHub Issue we file. + +- **Required**: the problem — what's wrong, what's happening vs. expected, or what's missing. +- **Optional**: reproduction steps, file/line refs, fix direction, scope, test plan. Include when it helps the doer. +- **Never**: decision-journaling (`## How surfaced`, `## Reversibility`, `## Why X over Y`), internal shorthand (`Path A`), who-said-what narrative. +- **Always link** the strategy doc / spec section the work traces to (`Tracked in: …`). +- Write for a reader without the originating conversation; body + links should be enough to act. diff --git a/docs/milestones/19_RC_HARDENING_OPS.md b/docs/milestones/19_RC_HARDENING_OPS.md index 46b36b5c..b2c6de00 100644 --- a/docs/milestones/19_RC_HARDENING_OPS.md +++ b/docs/milestones/19_RC_HARDENING_OPS.md @@ -1,6 +1,6 @@ # MS19 - RC Hardening & Ops -Status: Active — running in parallel with MS18 (no hard dependencies between the two; MS18 is blocked on Rachel's video through the 2026-05-31 hard cap, MS19 phases are independent of that work). Phase 1 reopened 2026-05-15 for end-to-end verification (audit closed; verification pass appended). Phase 4 (Visual Identity Polish) added 2026-05-15 — favicon overhaul + open-beta copy refactor + og:image bundled as one visual pass; renumbered downstream phases accordingly. Phase 3 / Phase 5 corrected to independent parallel tracks (no LLC → Legal Layer prerequisite). `.ics` updated to reflect the expanded scope. +Status: Active — running in parallel with MS18 (no hard dependencies; MS18 is blocked on Rachel's video through the 2026-05-31 hard cap). Phase 3 (LLC) and Phase 5 (Legal Layer) run as independent parallel tracks. Phase 8 (2FA) is positionally last by design. Parent execution doc: [`docs/PLAN.md`](../PLAN.md) Supporting ops doc: [`docs/ops/DOCS_DX.md`](../ops/DOCS_DX.md) @@ -22,7 +22,7 @@ Supporting ops doc: [`docs/ops/DOCS_DX.md`](../ops/DOCS_DX.md) - **Findings tracking trial:** Through M19, new findings/bugs/todos go to GitHub Issues (closed via `Fixes #N` on the merging PR) instead of new `docs/qa/Finding*.md` docs. Existing finding docs stay put. M20 kickoff decides whether to keep, revert, or hybridize. See [`docs/DOC_ROLES.md`](../DOC_ROLES.md#findings-conventions). ## Current Focus -- Active phase: Phase 1 (reopened — verification pass) and Phase 2 (next-up auth hardening). Other phases pre-flight. +- Active phase: Phase 1 (verification pass) and Phase 2 (next-up auth hardening). Other phases pre-flight. - Phase 1: [`docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md`](../strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md) - Phase 2: [`docs/strategies/19.2_AUTH_HARDENING.md`](../strategies/19.2_AUTH_HARDENING.md) - Phase 3: [`docs/strategies/19.3_LLC_FORMATION.md`](../strategies/19.3_LLC_FORMATION.md) @@ -35,7 +35,7 @@ Supporting ops doc: [`docs/ops/DOCS_DX.md`](../ops/DOCS_DX.md) ## Phase Rollup ### [ ] Phase 1 — Notification Coverage Audit & Verification -Document every outbound mail type — trigger, recipient, delivery-log behavior — so the full mail surface is explicitly accounted for before open beta. Audit (§1–§4) closed 2026-05-09. Reopened 2026-05-15 to add §5–§7 end-to-end verification (every notice class triggered in the running stack against realistic scenarios including time-advanced past-due flows; rendered email confirmed at intended recipients; catch-all alias flipped off so the rest of MS19 emits mail under prod-like routing). See [`19.1_NOTIFICATION_COVERAGE_AUDIT.md`](../strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md). +Document every outbound mail type — trigger, recipient, delivery-log behavior — so the full mail surface is explicitly accounted for before open beta. Includes end-to-end verification of every notice class in the running stack against realistic scenarios (time-advanced past-due flows included) and a stress-readiness check; catch-all alias stays flipped off so the rest of MS19 emits mail under prod-like routing. See [`19.1_NOTIFICATION_COVERAGE_AUDIT.md`](../strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md). ### [ ] Phase 2 — Auth/Password Policy Hardening Implement 419-to-login redirect and site-wide session-expiry logout. diff --git a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md index 0d5e81ff..2bd528b9 100644 --- a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md +++ b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md @@ -1,14 +1,14 @@ # MS19 Phase 1 Strategy — Notification Coverage Audit -Status: Reopened — verification pass appended (2026-05-15). §1–§4 closed and untouched (audit work shipped); §5–§7 added to verify the documented coverage actually behaves correctly end-to-end before open beta. +Status: Active — in §5 (end-to-end verification scenarios). §1–§4 (audit) closed; §6 (stress-readiness check) pending. Parent milestone doc: [`docs/milestones/19_RC_HARDENING_OPS.md`](../milestones/19_RC_HARDENING_OPS.md) Owner tags: `[Agent]` = critical-path code/doc/verification work I drive directly. `[Subagent]` = safe delegated sidecar work. `[User]` = work Nate drives directly (receiving real email, eyeballing rendered output, judging copy/branding quality). -**Original goal (§1–§4):** Finish the coverage matrix that [`docs/specs/NOTIFICATIONS.md`](../specs/NOTIFICATIONS.md) flagged as an MS17 deliverable but never received. Confirm every active outbound mail class has a documented row covering audience, trigger, code reference, status, feature test, and delivery-log type. Tweak the spec narrative inline where code has drifted from documented behavior. +**Coverage audit (§1–§4):** Finish the coverage matrix that [`docs/specs/NOTIFICATIONS.md`](../specs/NOTIFICATIONS.md) flagged as an MS17 deliverable but never received. Confirm every active outbound mail class has a documented row covering audience, trigger, code reference, status, feature test, and delivery-log type. Tweak the spec narrative inline where code has drifted from documented behavior. -**Expanded goal (§5–§7):** Prove the documented mail surface actually works end-to-end. Exercise every notice class in the running stack against realistic scenarios (including time-advanced past-due flows), confirm rendered email lands at intended recipients, fix anything that surfaces. Last-pass-before-open-beta confidence — the bar is "would I bet open-beta credibility on this," not "did the assertion pass." +**End-to-end verification (§5–§6):** Prove the documented mail surface actually works end-to-end. Exercise every notice class in the running stack against realistic scenarios (including time-advanced past-due flows), confirm rendered email lands at intended recipients, fix anything that surfaces. Last-pass-before-open-beta confidence — the bar is "would I bet open-beta credibility on this," not "did the assertion pass." ## Decisions made @@ -16,9 +16,9 @@ Owner tags: `[Agent]` = critical-path code/doc/verification work I drive directl - **Coverage format:** Use the column set the spec already specifies — Audience, Trigger, Mailable class, Status (`live` / `stubbed` / `planned`), Feature test(s), Delivery log type. - **Gap-closure scope:** Fixes land in this phase by default. If a finding is small-consequence relative to its work cost, route it to [`docs/BACKLOG.md`](../BACKLOG.md) — but only when MS19's phase goal and exit criteria still hold without the deferred work. -## Carried-in known finding (resolved) +## Carried-in finding (resolved) -`Tests\Feature\InvoiceNotificationTest` was failing across every recent PR back to MS17 — past-due delivery assertions expecting `queued` observed `skipped`/`sent`. Root cause: `.env.example` shipped `MAIL_OUTBOUND_ENABLED=false` and the PR Tests workflow copied it verbatim into `.env`, so `InvoiceDeliveryService::skipReason()` marked every queued row as skipped. Resolved via [#68](https://github.com/n8bar/CryptoZing/issues/68) / [#69](https://github.com/n8bar/CryptoZing/pull/69) by pinning the env var in `phpunit.xml`. First Issue under the M19 GitHub Issues trial (see milestone doc Decisions recorded). +`InvoiceNotificationTest` CI flakiness — resolved via [#68](https://github.com/n8bar/CryptoZing/issues/68) / [#69](https://github.com/n8bar/CryptoZing/pull/69). ## 1. Enumerate the live mail surface @@ -27,7 +27,7 @@ Owner tags: `[Agent]` = critical-path code/doc/verification work I drive directl 3. [x] Identify any in-app outbound paths that bypass Mail/Notification classes (raw mailer calls in Jobs, Commands, or Console scheduled tasks). 4. [x] Cross-check the resulting set against the notice classes named in `NOTIFICATIONS.md` Sections 3–5 — flag anything in code without a spec entry, or anything in the spec without code. -**§1 results (2026-05-09):** +**§1 outcomes:** - **Mail classes (14):** `app/Mail/` matches the Reference snapshot — no drift since drafting. - **Notification classes (0):** No `app/Notifications/` directory. @@ -47,7 +47,7 @@ Owner tags: `[Agent]` = critical-path code/doc/verification work I drive directl 5. [x] For each row's **Delivery log type** field, record the log-type identifier the class uses when creating delivery-log entries. 6. [x] Drop the "(MS17 deliverable)" suffix from the section heading once the matrix is populated. -**§2 results (2026-05-09):** 14 rows populated — 12 `live`, 2 `stubbed` (partial-warning legacy classes). All Mailables have feature-test coverage. Delivery log types match `DeliverInvoiceMail`'s type→Mailable map plus `send` (manual default) and `—` for the branding preview (which deliberately bypasses the delivery log). +**§2 outcomes:** 14 rows populated — 12 `live`, 2 `stubbed` (partial-warning legacy classes). All Mailables have feature-test coverage. Delivery log types match `DeliverInvoiceMail`'s type→Mailable map plus `send` (manual default) and `—` for the branding preview (which deliberately bypasses the delivery log). ## 3. Reconcile drift and tweak the narrative @@ -56,25 +56,7 @@ Owner tags: `[Agent]` = critical-path code/doc/verification work I drive directl 3. [x] Where the code is right and the spec is stale, tweak the spec narrative inline — small edits only. (Pulled forward earlier for the overpay/underpay paired-class titling — see §1 drift flags.) 4. [x] If a tweak feels larger than "small" (rewrites a paragraph, changes a documented invariant, contradicts a §3–§5 commitment), stop and surface for confirmation before applying. -**§3 results (2026-05-09):** 11 rows aligned with spec; 3 drift findings raised for §4 (one needing a user decision on direction, two small-scoped). 14th-row check (overpay/underpay narrative) was applied earlier in this phase. - -### Findings raised for §4 - -**F1 — Manual send does not transition invoice out of `draft` (decision needed).** -- Spec §3.1.2 says: "Sending the invoice queues outbound delivery, records the attempt in the delivery log, **and issues the invoice out of `draft`**." -- Code: `InvoiceDeliveryController::store` queues the `send` delivery row but does not update `Invoice::status`. `Invoice::refreshPaymentState()` explicitly skips `draft`/`void` invoices, so a draft that gets sent stays at `status='draft'` until something else moves it. -- This is bigger than a "small inline tweak" per §3.4 — needs a deliberate decision: either the spec is over-specifying (drop the draft-transition clause) or the code is missing a status update on successful queue (small but real code change). - -**F2 — Spec uses "(owner)" in display-label examples; shipped labels use "(issuer)".** -- Spec §5.12.2 example: `Underpayment alert (owner)`. Spec §5.12.3 example: `Partial payment warning (client|owner)`. -- Code (`InvoiceDelivery::typeLabel()`): consistently uses `(issuer)` after the MS17 owner→issuer terminology sweep — `Underpayment alert (issuer)`, `Partial payment warning (issuer)`, etc. -- Resolution: small spec tweak — replace "(owner)" with "(issuer)" in §5.12.2 and §5.12.3 examples so the spec matches the shipped labels. - -**F3 — Client underpayment alert view omits BTC outstanding amount (decision needed, small either way).** -- Spec §4.4.2: client alert should "include the outstanding USD/BTC amounts." -- Code (`resources/views/mail/invoice-underpayment-client.blade.php`): shows the outstanding USD amount only; no BTC line. -- The legacy `invoice-partial-warning-client.blade.php` view does show both USD and BTC, so the project pattern supports either interpretation. -- Two reasonable resolutions: (a) add a BTC line to the underpay-client view for spec compliance, or (b) drop "/BTC" from §4.4.2 since the linked invoice page already surfaces full BTC details. +**§3 outcomes:** 11 rows aligned with spec; 3 drift findings raised for §4 — tracked in [#78](https://github.com/n8bar/CryptoZing/issues/78), [#79](https://github.com/n8bar/CryptoZing/issues/79), [#80](https://github.com/n8bar/CryptoZing/issues/80). 14th-row check (overpay/underpay narrative) was applied earlier in this phase. ## 4. Resolve findings @@ -82,11 +64,7 @@ Owner tags: `[Agent]` = critical-path code/doc/verification work I drive directl 2. [x] For each backlog-bound finding (small consequence, high effort), add an entry to `docs/BACKLOG.md` with enough context to pick it up later. List the deferred finding here with a `[deferred → backlog]` note. 3. [x] Confirm the Phase 1 Exit Criteria still hold after any deferrals. -**§4 results (2026-05-09):** All three §3 findings resolved in-phase; no backlog deferrals. - -- **F1 — Manual send draft transition (Option C, applied):** Spec §3.1.2 reworded to make the transition rule explicit and conditional with a no-regression guarantee. `InvoiceDeliveryController::store` updated to transition `draft` → `sent` after a successful queue. Two new tests in `InvoiceDeliveryTest` (`test_sending_draft_invoice_transitions_status_to_sent`, `test_sending_does_not_regress_status_for_non_draft_invoice`). -- **F2 — Owner → Issuer in spec display labels:** §5.12.2 and §5.12.3 examples replaced with `(issuer)` to match shipped `InvoiceDelivery::typeLabel()` output. Catches the spec up to the MS17 terminology sweep. -- **F3 — Client underpay alert USD/BTC (b, applied):** §4.4.2 narrowed to "outstanding USD amount." Keeps the spec loose; the linked invoice page surfaces full BTC details, and the email body stays simple/USD-focused for the typical recipient. Code is free to add a BTC line or current-rate hint as a future enhancement without a re-spec round. +**§4 outcomes:** All three §3 findings resolved in-phase ([#78](https://github.com/n8bar/CryptoZing/issues/78), [#79](https://github.com/n8bar/CryptoZing/issues/79), [#80](https://github.com/n8bar/CryptoZing/issues/80)); no backlog deferrals. --- @@ -94,43 +72,55 @@ Owner tags: `[Agent]` = critical-path code/doc/verification work I drive directl Bar: every notice type observed at least once end-to-end — from real trigger in the running stack, through queue + delivery-log + actual mail-server submission, to landing in a real inbox at the intended address with subject/branding/links/copy passing eyeball QA. "Feature test passed" is not enough; that was already true at §1 close. +Findings file as GitHub issues per the M19 trial; the corresponding fix and re-verify steps append to the parent sub-section as normal numbered items. Same in-phase-fix-vs-backlog rule §4 used: fixes land in this phase by default; small-consequence/high-effort items may route to `docs/BACKLOG.md` with a `[deferred → backlog]` note on the relevant numbered item. + ### 1. Catch-all flip (Option A) The catch-all alias (`MAIL_ALIAS_ENABLED=true` rewriting recipients to `mailer.cryptozing.app`) is disabled at the start of §5 so all subsequent scenarios exercise real recipient routing. Aliasing **stays off** for the remainder of MS19 — Phases 2–8 then emit mail under prod-like routing, so any later regression is caught in the right state. -1. [ ] [Agent] Set `MAIL_ALIAS_ENABLED=false` in `.env`. Confirm `MAIL_ALIAS_DOMAIN` left intact for documentation but inactive. -2. [ ] [Agent] Trigger a single low-stakes test mail (manual-send `InvoiceReadyMail` to a controlled recipient) and verify via Mailgun logs + recipient inbox that no rewrite occurred. -3. [ ] [User] Confirm receipt of the un-aliased mail at the intended address. Sign off on this sub-section before the next one begins. -4. [ ] [Agent] Document the active test addresses in this section — agent-suggested list below; [User] may override at sign-off: +1. [x] [Agent] Set `MAIL_ALIAS_ENABLED=false` in `.env`. Confirm `MAIL_ALIAS_DOMAIN` left intact for documentation but inactive. (Applied 2026-05-21; queue worker restarted; `MailAlias::isEnabled()` now reports `false` and `convert("client-test@nospam.site")` returns the address unchanged.) +2. [x] [Agent] Trigger a single low-stakes test mail (manual-send `InvoiceReadyMail` to a controlled recipient) and verify via Mailgun logs + recipient inbox that no rewrite occurred. (Sent 2026-05-21 via tinker against `Invoice` #95 / `delivery_log` #72606 to `client-test@nospam.site`; envelope captured `to=client-test@nospam.site` directly with no rewrite to `*.mailer.cryptozing.app`; Mailgun message-id ``; no `invoice_delivery.mailgun_failed` warning. Note: 2 sends — initial send did not capture the provider message-id, so the send was repeated to record it; both delivered to the same controlled recipient.) +3. [x] [User] Confirm receipt of the un-aliased mail at the intended address. Sign off on this sub-section before the next one begins. (Signed off 2026-05-22: both smoke-test mails received at `client-test@nospam.site` with no aliasing.) +4. [x] [Agent] Document the active test addresses in this section. Signed-off persona set (2026-05-21): - **Issuer persona:** `issuer-test@nateTheProgrammer.com` - **Client persona:** `client-test@nospam.site` - **Owner-alert recipient:** `owner-alerts@cybercreek.us` - **Real eyeball-QA inbox:** `nate@nateTheProgrammer.com` + - **§5.1 smoke-test recipient:** `client-test@nospam.site` ### 2. Manual issue (`InvoiceReadyMail` + draft→sent transition) -1. [ ] [Agent] Create a draft invoice via the app; set client recipient to the client persona established in the catch-all flip sub-section. -2. [ ] [Agent] Trigger send; confirm `InvoiceReadyMail` queued, `delivery_log` row of type `send` created, invoice transitioned `draft` → `sent`. -3. [ ] [Agent] Confirm Mailgun accepted the message (check provider logs / message-id round-trip). -4. [ ] [User] Receive the mail at the client persona inbox; verify subject/branding/links/copy. Click invoice link, confirm public invoice view loads correctly. +1. [x] [Agent] Create a draft invoice via the app; set client recipient to the client persona established in the catch-all flip sub-section. (2026-05-22: Invoice #96 `INV-M191-S52-1123` created as draft with client #45 / email `client-test@nospam.site`; public share enabled.) +2. [x] [Agent] Trigger send; confirm `InvoiceReadyMail` queued, `delivery_log` row of type `send` created, invoice transitioned `draft` → `sent`. (2026-05-22: `InvoiceDeliveryController::store` invoked under `Auth::login($issuer)`; returned 302 with session status `Invoice email queued.`; `delivery_log` row #72607 created with `type=send`, `recipient=client-test@nospam.site`, initial `status=queued`; invoice transitioned `draft` → `sent` — §4 draft-transition fix confirmed live in real flow.) +3. [x] [Agent] Confirm Mailgun accepted the message (check provider logs / message-id round-trip). (2026-05-22: queue worker drained within 2s; `delivery_log` #72607 transitioned to `status=sent` with `provider_message_id=<140a19f766f3427f2a200322bdc33a50@cryptozing.app>`; `invoice_delivery.sent` log line emitted with recipient un-aliased; no `mailgun_failed` warning.) +4. [x] [User] Receive the mail at the client persona inbox; verify subject/branding/links/copy. Click invoice link, confirm public invoice view loads correctly. (Signed off 2026-05-22: inbox view + public invoice link both clean.) ### 3. Pay full (`InvoicePaidReceiptMail` client + `InvoiceIssuerPaidNoticeMail` issuer) -1. [ ] [Agent] Take an issued invoice, simulate a confirmed full payment (signet/regtest payment or fixture-based payment-state advance). -2. [ ] [Agent] Verify both Mailables fire; confirm both delivery-log rows created (`paid_client`, `paid_issuer`). -3. [ ] [User] Receive both at client + issuer personas. Verify TXID renders cleanly on narrow viewports (Finding 1 from MS17 — regression check), copy is appropriate, paid receipt looks like a paid receipt. +1. [x] [Agent] Take an issued invoice, simulate a confirmed full payment (signet/regtest payment or fixture-based payment-state advance). (2026-05-22: created User `issuer-test@nateTheProgrammer.com` (#18), Client `client-test@nospam.site` (#46), Invoice #97 `INV-M191-S53-1127` in `sent` status with public share enabled; injected fixture `InvoicePayment` with confirmed full payment matching `amount_usd=50.00` at 76923 sats / synthetic 64-hex txid; `refreshPaymentLedger` transitioned the invoice to `paid`.) +2. [x] [Agent] Verify both Mailables fire; confirm both delivery-log rows created (`paid_client`, `paid_issuer`). (2026-05-22: `InvoicePaid` event listener auto-queued `issuer_paid_notice` #72608 to `issuer-test@nateTheProgrammer.com` (sent within 1s, msgid ``); owner-initiated `storeReceipt` queued client `receipt` #72609 to `client-test@nospam.site` (sent within 1s, msgid ``). Both un-aliased; no `mailgun_failed` warning.) +3. [x] [User] Receive both at client + issuer personas. Verify TXID renders cleanly on narrow viewports (Finding 1 from MS17 — regression check), copy is appropriate, paid receipt looks like a paid receipt. (Signed off 2026-05-22: paid notification and receipt acknowledging 1 confirmation both read clean.) ### 4. Partial payment acknowledgment (`InvoicePaymentAcknowledgmentClientMail` + `InvoicePaymentAcknowledgmentIssuerMail`) -1. [ ] [Agent] Take an issued invoice, simulate a partial confirmed payment. -2. [ ] [Agent] Verify both acknowledgment Mailables fire; confirm delivery-log rows. -3. [ ] [User] Receive both. Verify the acknowledgment framing is honest about "we received some" vs. final settlement. +1. [x] [Agent] Take an issued invoice, simulate a partial confirmed payment. (2026-05-22: Invoice #98 `INV-M191-S54-1134` created in `sent` status with public share enabled for issuer-test/client-test personas; injected confirmed `InvoicePayment` #109 at 50% of expected (46154/92308 sats); `refreshPaymentLedger` landed status `partial` with outstanding 46154 sats.) +2. [x] [Agent] Verify both acknowledgment Mailables fire; confirm delivery-log rows. (2026-05-22: `InvoiceAlertService::sendDetectedPaymentAcknowledgments` queued `payment_acknowledgment_client` #72610 → `client-test@nospam.site` (sent within 1s, msgid `<4542cd81b3e858615c70ab204f1ce259@cryptozing.app>`) and `payment_acknowledgment_issuer` #72611 → `issuer-test@nateTheProgrammer.com` (sent within 1s, msgid ``). No `mailgun_failed` warning.) +3. [x] [User] Receive both. Verify the acknowledgment framing is honest about "we received some" vs. final settlement. (Signed off 2026-05-23: framing read as borderline — three findings filed against the ack pair + delivery-log surface: [#75](https://github.com/n8bar/CryptoZing/issues/75) issuer subject awkwardness (`Review detected payment …`), [#76](https://github.com/n8bar/CryptoZing/issues/76) client body's `No action is needed right now` reads as completion in partial scenarios, [#77](https://github.com/n8bar/CryptoZing/issues/77) per-invoice delivery log defaults need filter + toggle. Not blocking eyeball QA; fix + re-verify items appended below.) +4. [ ] [Agent] Ship [#75](https://github.com/n8bar/CryptoZing/issues/75): update `app/Mail/InvoicePaymentAcknowledgmentIssuerMail.php` subject + `resources/views/mail/invoice-payment-acknowledgment-issuer.blade.php` heading to `Review payment activity on Invoice INV-…`. +5. [ ] [Agent] Re-fire §5.4 mail pair against invoice #98; confirm rendered subject + heading match the new wording. +6. [ ] [User] Eyeball QA: issuer-inbox subject reads clean. +7. [ ] [Agent] Ship [#76](https://github.com/n8bar/CryptoZing/issues/76): tighten `resources/views/mail/invoice-payment-acknowledgment-client.blade.php` body so the partial scenario doesn't read as completion. +8. [ ] [Agent] Re-fire client ack against invoice #98; confirm rendered body opens with the new direction. +9. [ ] [User] Eyeball QA: client body no longer reads as completion in a partial scenario. +10. [ ] [Agent] Ship [#77](https://github.com/n8bar/CryptoZing/issues/77): default per-invoice delivery log to client-facing rows; add "Include emails sent to me" toggle that restores the unfiltered list; add feature test covering default-hidden vs. toggle-visible behavior. +11. [ ] [Agent] Run `./vendor/bin/sail artisan test` and confirm the new feature test passes alongside the existing suite. +12. [ ] [User] Browser QA: log in as `issuer-test@nateTheProgrammer.com` (password `password`), open invoice #98's page, confirm default-filtered delivery log + toggle reveals all rows. ### 5. Underpayment alert (`InvoiceUnderpaymentClientMail` + `InvoiceUnderpaymentIssuerMail`) 1. [ ] [Agent] Take an issued invoice; simulate payment below threshold; advance scheduler past the underpay-alert cooldown so the alert is eligible to fire. 2. [ ] [Agent] Run the alert pass; verify both underpayment Mailables fire; confirm delivery-log rows. -3. [ ] [User] Receive both. Verify client view shows the outstanding USD amount per spec §4.4.2 (post-F3 narrowing); issuer view surfaces the outstanding-balance follow-up per spec §4.4. +3. [ ] [User] Receive both. Verify client view shows the outstanding USD amount per spec §4.4.2 (USD-only per §4 narrowing); issuer view surfaces the outstanding-balance follow-up per spec §4.4. 4. [ ] [Agent] Re-run the alert pass within cooldown; verify NO duplicate row is written (regression check on Findings 7 & 8 from MS17). ### 6. Overpayment alert (`InvoiceOverpaymentClientMail` + `InvoiceOverpaymentIssuerMail`) @@ -159,12 +149,16 @@ This one matters most — Nate flagged late-payment notifications specifically a 2. [ ] [Agent] Confirm the preview Mailable fires and is sent directly without creating a delivery-log row (per spec §5.14.4.1). 3. [ ] [User] Receive the preview; verify branding renders as expected. -### 9. Stubbed-by-design (`InvoicePartialWarningClientMail` + `InvoicePartialWarningIssuerMail`) +### 9. Remove deprecated partial-warning mail family (`InvoicePartialWarningClientMail` + `InvoicePartialWarningIssuerMail`) -These are wired into the type map but no service code queues new `*_partial_warning` deliveries (per §1 drift findings). Verify they stay quiet. +§1 surfaced these as dormant Mailables — wired into the type map but never queued by service code. They were kept around under a "preserve for historical delivery-log rendering" premise. Pre-launch the database holds only seed/test data (8 `*_partial_warning` rows from 2026-03), so the premise collapses. Ship the full removal per [#82](https://github.com/n8bar/CryptoZing/issues/82). -1. [ ] [Agent] Run a full pass of `InvoiceAlertService` and observe the delivery log: zero new `*_partial_warning` rows should be written for any invoice. -2. [ ] [Agent] Verify the existing tests (`test_repeated_partial_payment_detection_no_longer_creates_a_separate_partial_warning_family`, `..._does_not_enqueue_legacy_partial_warnings_on_repeat_runs`) still pass. +1. [ ] [Agent] Code changes: delete the two Mailable classes + two blade templates; strip imports + type-map entries from `DeliverInvoiceMail`; remove the two `typeLabel()` entries from `InvoiceDelivery`; remove the two skip-handler branches (`DeliverInvoiceMail::skipReason`, `InvoiceAlertService::skipInvalidQueuedDeliveries`); remove `Invoice::shouldWarnAboutPartialPayments()` (no other callers); remove `last_partial_warning_sent_at` from `Invoice` `$fillable` and `$casts`. +2. [ ] [Agent] Migration: drop `last_partial_warning_sent_at` column from `invoices`; delete `*_partial_warning` rows from `invoice_deliveries`. +3. [ ] [Agent] Test fixture surgery in `tests/Feature/InvoicePaymentCorrectionTest.php` (lines 201-216): substitute the two `*_partial_warning` delivery fixtures with another valid type (e.g., `past_due_client` / `past_due_issuer`); preserve the "4 queued, all skipped on payment restore" assertion. +4. [ ] [Agent] Delete regression tests `test_repeated_partial_payment_detection_no_longer_creates_a_separate_partial_warning_family` and `test_repeated_partial_payment_detection_does_not_enqueue_legacy_partial_warnings_on_repeat_runs` from `tests/Feature/Wallet/WatchPaymentsCommandTest.php` (lines 525-641) — they assert absence of removed types. +5. [ ] [Agent] Spec update: strip `docs/specs/NOTIFICATIONS.md` line 88 (legacy-rows-render clause in §5 sub-item 12.3) and matrix rows 118-119 (Coverage & Status drops from 14 to 12 live rows). +6. [ ] [Agent] Run `./vendor/bin/sail artisan test` — full suite passes. ### 10. Edge cases @@ -175,30 +169,18 @@ These are wired into the type map but no service code queues new `*_partial_warn ### 11. Mail-client rendering spot-check -1. [ ] [User] For at least one received mail per audience class (client-facing receipt, issuer-facing alert), open in two distinct mail clients (e.g., Gmail web + iOS Mail). Note any rendering breakage — broken images, broken layout, broken links, blocked-content prompts. - -## 6. Resolve §5 findings - -Same in-phase-fix-vs-backlog rule §4 used. - -1. [ ] [Agent or User] For each in-phase finding from §5, ship the fix (code change, template change, test) within Phase 1 and check off the finding here. -2. [ ] [Agent] For each backlog-bound finding (small consequence, high effort), add an entry to `docs/BACKLOG.md` with enough context to pick it up later. List the deferred finding here with a `[deferred → backlog]` note. -3. [ ] [User] Confirm the expanded Phase 1 Exit Criteria still hold after any deferrals. - -**§6 findings tracking:** - -_(Populated as §5 surfaces issues. Empty until §5 runs.)_ +1. [ ] [User] For at least one received mail per audience class (client-facing receipt, issuer-facing alert), open in two distinct mail clients (e.g., a PC web client + an Android mail app). Note any rendering breakage — broken images, broken layout, broken links, blocked-content prompts. -## 7. Stress-readiness check (best-effort) +## 6. Stress-readiness check (best-effort) -We don't have dedicated load-test infrastructure before open beta. This section is honest about that — it records what we CAN exercise and what we explicitly aren't testing. +We don't have dedicated load-test infrastructure before open beta, and Mailgun's free tier caps total outbound at **100 emails/day**. A single invoice flow can produce 5-6 emails across notice classes, so meaningful provider-level stress isn't feasible on the free tier. This section exercises what we CAN under those constraints (queue mechanics + retry behavior) and explicitly names what we aren't testing. Larger-scale stress is deferred to [#81](https://github.com/n8bar/CryptoZing/issues/81) — to be triggered when the mailer service is upgraded or switched. -1. [ ] [Agent] Burst-fire ~25–50 issued invoices in a tight loop; verify the queue drains all `DeliverInvoiceMail` jobs without any double-sending or `failed_jobs` accumulation. +1. [ ] [Agent] Burst-fire ~10–15 issued invoices in a tight loop (kept under the Mailgun daily cap); verify the queue drains all `DeliverInvoiceMail` jobs without any double-sending or `failed_jobs` accumulation. 2. [ ] [Agent] Trigger a queue worker restart mid-burst; verify in-flight jobs resume without duplicate sends and the delivery log stays truthful. 3. [ ] [Agent] Force one job to fail (e.g., temporarily invalid mail config); confirm Laravel's retry logic handles it; on success after retry, no duplicate delivery-log rows are written. -4. [ ] [Agent] Document what was NOT tested (so post-launch ops knows what's untested rather than silently assumed): no real-volume stress (>1,000 simultaneous invoices), no Mailgun rate-limit testing, no bounce/spam-folder simulation, no extended-duration soak. +4. [ ] [Agent] Record what was NOT tested under free-tier constraints (so post-launch ops knows what's deferred to [#81](https://github.com/n8bar/CryptoZing/issues/81) rather than silently assumed): no real-volume burst (>100 emails/day, blocked by daily cap), no sub-daily Mailgun rate-limit testing, no bounce/spam-folder simulation, no extended-duration soak. -**§7 results:** _(Populated when §7 runs. Include a short "untested risks" summary so the open-beta cutover doesn't inherit hidden assumptions.)_ +**§6 outcomes:** _(Populated when §6 runs. Include a short "untested risks" summary so the open-beta cutover doesn't inherit hidden assumptions; the canonical follow-up plan lives in [#81](https://github.com/n8bar/CryptoZing/issues/81).)_ ## Exit Criteria @@ -212,8 +194,8 @@ We don't have dedicated load-test infrastructure before open beta. This section - [ ] Every notice class observed end-to-end at least once via §5 scenarios — trigger fired, Mailable sent, delivery-log row truthful, mail received at intended recipient, eyeball QA passed. - [ ] Late-payment / past-due notification flow verified across all three escalation slots with force-set due dates. - [ ] Stubbed-by-design partial-warning classes confirmed quiet under live alert passes. -- [ ] All §5 findings resolved per §6 — in-phase fixes shipped or backlog-deferred with context. -- [ ] §7 stress-readiness check executed; "what was NOT tested" risks documented for post-launch ops. +- [ ] All §5 findings resolved inline per sub-section — in-phase fixes shipped or backlog-deferred with context. +- [ ] §6 stress-readiness check executed; "what was NOT tested" risks documented for post-launch ops. ## Reference From db4dea7a2ecee442b4c85eedadc5ac80893281d2 Mon Sep 17 00:00:00 2001 From: Nate Barlow Date: Sat, 23 May 2026 05:08:48 -0600 Subject: [PATCH 04/11] =?UTF-8?q?M19.1=20=C2=A75.4:=20resolve=20#75/#76/#7?= =?UTF-8?q?7=20partial-payment=20ack=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #75: rewrite issuer subject + heading to "Review payment activity on Invoice …" — replaces ambiguous "Review detected payment …" - #76: tighten client ack body so partial scenario reads clearly as partial. Final wording names the scenario explicitly and reframes the outstanding dollar figure as a post-confirmation value: "This appears to be a partial payment — once confirmations settle, $Y will still be due on this invoice." Per-payment rate lock per PARTIAL_PAYMENTS.md:73-74 makes the figure stable across the confirmation interval. - #77: per-invoice delivery log defaults to client-facing rows (filter by recipient, not by type enumeration, so new *_issuer types stay hidden automatically). "Include emails sent to me" checkbox toggles the unfiltered view; section is anchored so reload returns to the log instead of the page top. - Restructure §5.4 of the strategy doc to nest verifications under each finding's ship item. Concision pass on completed-item notes. Fixes #75 Fixes #76 Fixes #77 Co-Authored-By: Claude Opus 4.7 --- app/Http/Controllers/InvoiceController.php | 14 +++++ ...InvoicePaymentAcknowledgmentIssuerMail.php | 2 +- .../19.1_NOTIFICATION_COVERAGE_AUDIT.md | 27 ++++----- resources/views/invoices/show.blade.php | 15 ++++- ...ce-payment-acknowledgment-client.blade.php | 6 +- ...ce-payment-acknowledgment-issuer.blade.php | 2 +- tests/Feature/InvoiceShowEditFlowTest.php | 58 +++++++++++++++++++ 7 files changed, 105 insertions(+), 19 deletions(-) diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index f9f19012..5cb1bafc 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -224,12 +224,26 @@ public function show(\Illuminate\Http\Request $request, \App\Models\Invoice $inv allowFallback: $btcForUri !== null ); + $includeSelf = $request->boolean('include_self'); + $issuerEmail = (string) ($invoice->user?->email ?? ''); + $visibleDeliveries = $invoice->deliveries + ->where('status', '!=', 'skipped') + ->when( + !$includeSelf && $issuerEmail !== '', + fn ($collection) => $collection->filter( + fn ($delivery) => strcasecmp((string) $delivery->recipient, $issuerEmail) !== 0 + ) + ) + ->values(); + return view('invoices.show', [ 'invoice' => $invoice, 'rate' => $rate, 'paymentSummary' => $summary, 'paymentHistory' => $paymentHistory, 'reattributeDestinations' => $reattributeDestinations, + 'includeSelf' => $includeSelf, + 'visibleDeliveries' => $visibleDeliveries, 'gettingStartedStrip' => $request->boolean('getting_started') ? (static function () use ($gettingStartedFlow, $request, $invoice): array { $snapshot = $gettingStartedFlow->snapshot($request->user()); diff --git a/app/Mail/InvoicePaymentAcknowledgmentIssuerMail.php b/app/Mail/InvoicePaymentAcknowledgmentIssuerMail.php index 05b89ea5..b4ad473c 100644 --- a/app/Mail/InvoicePaymentAcknowledgmentIssuerMail.php +++ b/app/Mail/InvoicePaymentAcknowledgmentIssuerMail.php @@ -24,7 +24,7 @@ public function __construct( public function envelope(): Envelope { return new Envelope( - subject: 'Review detected payment for Invoice ' . ($this->invoice->number ?? $this->invoice->id), + subject: 'Review payment activity on Invoice ' . ($this->invoice->number ?? $this->invoice->id), ); } diff --git a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md index 2bd528b9..3005d2b9 100644 --- a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md +++ b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md @@ -74,7 +74,7 @@ Bar: every notice type observed at least once end-to-end — from real trigger i Findings file as GitHub issues per the M19 trial; the corresponding fix and re-verify steps append to the parent sub-section as normal numbered items. Same in-phase-fix-vs-backlog rule §4 used: fixes land in this phase by default; small-consequence/high-effort items may route to `docs/BACKLOG.md` with a `[deferred → backlog]` note on the relevant numbered item. -### 1. Catch-all flip (Option A) +### 1. Catch-all flip The catch-all alias (`MAIL_ALIAS_ENABLED=true` rewriting recipients to `mailer.cryptozing.app`) is disabled at the start of §5 so all subsequent scenarios exercise real recipient routing. Aliasing **stays off** for the remainder of MS19 — Phases 2–8 then emit mail under prod-like routing, so any later regression is caught in the right state. @@ -103,18 +103,19 @@ The catch-all alias (`MAIL_ALIAS_ENABLED=true` rewriting recipients to `mailer.c ### 4. Partial payment acknowledgment (`InvoicePaymentAcknowledgmentClientMail` + `InvoicePaymentAcknowledgmentIssuerMail`) -1. [x] [Agent] Take an issued invoice, simulate a partial confirmed payment. (2026-05-22: Invoice #98 `INV-M191-S54-1134` created in `sent` status with public share enabled for issuer-test/client-test personas; injected confirmed `InvoicePayment` #109 at 50% of expected (46154/92308 sats); `refreshPaymentLedger` landed status `partial` with outstanding 46154 sats.) -2. [x] [Agent] Verify both acknowledgment Mailables fire; confirm delivery-log rows. (2026-05-22: `InvoiceAlertService::sendDetectedPaymentAcknowledgments` queued `payment_acknowledgment_client` #72610 → `client-test@nospam.site` (sent within 1s, msgid `<4542cd81b3e858615c70ab204f1ce259@cryptozing.app>`) and `payment_acknowledgment_issuer` #72611 → `issuer-test@nateTheProgrammer.com` (sent within 1s, msgid ``). No `mailgun_failed` warning.) -3. [x] [User] Receive both. Verify the acknowledgment framing is honest about "we received some" vs. final settlement. (Signed off 2026-05-23: framing read as borderline — three findings filed against the ack pair + delivery-log surface: [#75](https://github.com/n8bar/CryptoZing/issues/75) issuer subject awkwardness (`Review detected payment …`), [#76](https://github.com/n8bar/CryptoZing/issues/76) client body's `No action is needed right now` reads as completion in partial scenarios, [#77](https://github.com/n8bar/CryptoZing/issues/77) per-invoice delivery log defaults need filter + toggle. Not blocking eyeball QA; fix + re-verify items appended below.) -4. [ ] [Agent] Ship [#75](https://github.com/n8bar/CryptoZing/issues/75): update `app/Mail/InvoicePaymentAcknowledgmentIssuerMail.php` subject + `resources/views/mail/invoice-payment-acknowledgment-issuer.blade.php` heading to `Review payment activity on Invoice INV-…`. -5. [ ] [Agent] Re-fire §5.4 mail pair against invoice #98; confirm rendered subject + heading match the new wording. -6. [ ] [User] Eyeball QA: issuer-inbox subject reads clean. -7. [ ] [Agent] Ship [#76](https://github.com/n8bar/CryptoZing/issues/76): tighten `resources/views/mail/invoice-payment-acknowledgment-client.blade.php` body so the partial scenario doesn't read as completion. -8. [ ] [Agent] Re-fire client ack against invoice #98; confirm rendered body opens with the new direction. -9. [ ] [User] Eyeball QA: client body no longer reads as completion in a partial scenario. -10. [ ] [Agent] Ship [#77](https://github.com/n8bar/CryptoZing/issues/77): default per-invoice delivery log to client-facing rows; add "Include emails sent to me" toggle that restores the unfiltered list; add feature test covering default-hidden vs. toggle-visible behavior. -11. [ ] [Agent] Run `./vendor/bin/sail artisan test` and confirm the new feature test passes alongside the existing suite. -12. [ ] [User] Browser QA: log in as `issuer-test@nateTheProgrammer.com` (password `password`), open invoice #98's page, confirm default-filtered delivery log + toggle reveals all rows. +1. [x] [Agent] Take an issued invoice, simulate a partial confirmed payment. (Invoice #98 `INV-M191-S54-1134`; payment #109 at 46154/92308 sats; landed `partial`.) +2. [x] [Agent] Verify both acknowledgment Mailables fire; confirm delivery-log rows. (Delivery rows #72610 client-side, #72611 issuer-side; both `sent` with provider msgids.) +3. [x] [User] Receive both. Verify the acknowledgment framing is honest about "we received some" vs. final settlement. (Surfaced [#75](https://github.com/n8bar/CryptoZing/issues/75), [#76](https://github.com/n8bar/CryptoZing/issues/76), [#77](https://github.com/n8bar/CryptoZing/issues/77); fix items below.) +4. [x] [Agent] Ship [#75](https://github.com/n8bar/CryptoZing/issues/75): update `app/Mail/InvoicePaymentAcknowledgmentIssuerMail.php` subject + `resources/views/mail/invoice-payment-acknowledgment-issuer.blade.php` heading to `Review payment activity on Invoice INV-…`. + 1. [x] [Agent] Re-fire §5.4 mail pair against invoice #98; confirm rendered subject + heading match the new wording. (Queue-path dedupe blocks same-txid re-queue, so used direct `Mail::send`.) + 2. [x] [User] Eyeball QA: issuer-inbox subject reads clean. +5. [x] [Agent] Ship [#76](https://github.com/n8bar/CryptoZing/issues/76): tighten `resources/views/mail/invoice-payment-acknowledgment-client.blade.php` so the partial scenario reads clearly as partial without false completion. + 1. [x] Round 1: replaced `No action is needed right now.` with `We're still waiting for the network to confirm — we'll follow up if anything needs your attention.` QA: not completion ✓, but didn't read as *partial* either — drove round 2. + 2. [x] Round 2: added a conditional branch on `$invoice->outstanding_usd > 0` (mirrors issuer ack's outstanding-balance treatment). Partial branch read `Your invoice currently shows an outstanding balance of **$Y**. Confirmations are still settling — we'll follow up if anything else needs your attention.` QA: figure unclear pre/post-confirmation — drove round 3. + 3. [x] Round 3: refined partial branch to `This appears to be a partial payment — once confirmations settle, **$Y** will still be due on this invoice. We'll follow up if anything else needs your attention.` Spec-verified `outstanding_usd` stable across confirmation interval (per `docs/specs/PARTIAL_PAYMENTS.md:73-74`). QA: reads as partial ✓. Closes #76 on PR merge via `Fixes #76`. +6. [x] [Agent] Ship [#77](https://github.com/n8bar/CryptoZing/issues/77): default per-invoice delivery log to client-facing rows; add "Include emails sent to me" toggle that restores the unfiltered list; add feature test covering default-hidden vs. toggle-visible behavior. (Filter by `recipient` not by type enumeration (per #77's robustness note) — new `*_issuer` types stay hidden automatically. Section anchored as `
`; checkbox form submits to `…#delivery-log` so reload lands back at the section. Feature test asserts on the `Paid notice (issuer)` typeLabel as a stable target.) + 1. [x] [Agent] Run `./vendor/bin/sail artisan test` and confirm the new feature test passes alongside the existing suite. + 2. [x] [User] Browser QA: log in as `issuer-test@nateTheProgrammer.com` (password `password`), open invoice #98's page, confirm default-filtered delivery log + toggle reveals all rows. ### 5. Underpayment alert (`InvoiceUnderpaymentClientMail` + `InvoiceUnderpaymentIssuerMail`) diff --git a/resources/views/invoices/show.blade.php b/resources/views/invoices/show.blade.php index 14452fb7..ee5d7ae8 100644 --- a/resources/views/invoices/show.blade.php +++ b/resources/views/invoices/show.blade.php @@ -625,7 +625,7 @@ class="rounded-lg border border-gray-200 bg-white p-2">--}}
@if ($invoice->deliveries->isNotEmpty()) @php - $displayDeliveries = $invoice->deliveries->where('status', '!=', 'skipped'); + $displayDeliveries = $visibleDeliveries; $deliveryCount = $displayDeliveries->count(); @endphp -
+
@@ -644,6 +644,15 @@ class="rounded-lg border border-gray-200 bg-white p-2">--}} Hide
+
+ +
@@ -710,7 +719,7 @@ class="rounded-lg border border-gray-200 bg-white p-2">--}} @else -
+

Delivery log

No delivery attempts yet. Enable the public link and send the invoice to create the first log entry. diff --git a/resources/views/mail/invoice-payment-acknowledgment-client.blade.php b/resources/views/mail/invoice-payment-acknowledgment-client.blade.php index 5baed9d8..a814b6e9 100644 --- a/resources/views/mail/invoice-payment-acknowledgment-client.blade.php +++ b/resources/views/mail/invoice-payment-acknowledgment-client.blade.php @@ -5,7 +5,11 @@ A Bitcoin payment of **{{ $invoice->formatBitcoinAmount(($payment?->sats_received ?? 0) / \App\Models\Invoice::SATS_PER_BTC) ?? '0' }} BTC** was detected. -No action is needed right now. +@if ($invoice->outstanding_usd > 0) +This appears to be a partial payment — once confirmations settle, **${{ number_format($invoice->outstanding_usd, 2) }}** will still be due on this invoice. We'll follow up if anything else needs your attention. +@else +We're still waiting for the network to confirm — we'll follow up if anything needs your attention. +@endif The invoice issuer has been notified to review it promptly. diff --git a/resources/views/mail/invoice-payment-acknowledgment-issuer.blade.php b/resources/views/mail/invoice-payment-acknowledgment-issuer.blade.php index 7bf6d417..c6f4ce4f 100644 --- a/resources/views/mail/invoice-payment-acknowledgment-issuer.blade.php +++ b/resources/views/mail/invoice-payment-acknowledgment-issuer.blade.php @@ -1,5 +1,5 @@ @component('mail::message', ['invoice' => $invoice]) -# Review detected payment for Invoice {{ $invoice->number ?? $invoice->id }} +# Review payment activity on Invoice {{ $invoice->number ?? $invoice->id }} A Bitcoin payment of **{{ $invoice->formatBitcoinAmount(($payment?->sats_received ?? 0) / \App\Models\Invoice::SATS_PER_BTC) ?? '0' }} BTC** was detected for this invoice. diff --git a/tests/Feature/InvoiceShowEditFlowTest.php b/tests/Feature/InvoiceShowEditFlowTest.php index 68fd054c..fdbdbd3b 100644 --- a/tests/Feature/InvoiceShowEditFlowTest.php +++ b/tests/Feature/InvoiceShowEditFlowTest.php @@ -671,4 +671,62 @@ public function test_expired_public_link_shows_expired_label_and_reactivation_st false ); } + + public function test_delivery_log_defaults_to_client_facing_rows_and_include_self_toggle_reveals_issuer_rows(): void + { + $owner = User::factory()->create(['email' => 'issuer-log-test@example.com']); + $client = Client::create([ + 'user_id' => $owner->id, + 'name' => 'Log Filter Client', + 'email' => 'client-log-test@example.com', + ]); + + $invoice = Invoice::create([ + 'user_id' => $owner->id, + 'client_id' => $client->id, + 'number' => 'INV-LOGFILTER-1', + 'amount_usd' => 100, + 'btc_rate' => 50_000, + 'amount_btc' => 0.002, + 'payment_address' => 'tb1qq0logfilter', + 'status' => 'sent', + 'invoice_date' => now()->toDateString(), + ]); + + $invoice->deliveries()->create([ + 'user_id' => $owner->id, + 'type' => 'send', + 'status' => 'sent', + 'recipient' => $client->email, + 'queued_at' => now()->subMinute(), + 'sent_at' => now()->subMinute(), + ]); + + $invoice->deliveries()->create([ + 'user_id' => $owner->id, + 'type' => 'issuer_paid_notice', + 'status' => 'sent', + 'recipient' => $owner->email, + 'queued_at' => now()->subMinute(), + 'sent_at' => now()->subMinute(), + ]); + + $defaultResponse = $this + ->actingAs($owner) + ->get(route('invoices.show', $invoice)); + + $defaultResponse->assertOk(); + $defaultResponse->assertSee($client->email); + $defaultResponse->assertDontSee('Paid notice (issuer)'); + $defaultResponse->assertSee('Include emails sent to me'); + $defaultResponse->assertSee('id="delivery-log"', false); + + $includeSelfResponse = $this + ->actingAs($owner) + ->get(route('invoices.show', ['invoice' => $invoice, 'include_self' => 1])); + + $includeSelfResponse->assertOk(); + $includeSelfResponse->assertSee($client->email); + $includeSelfResponse->assertSee('Paid notice (issuer)'); + } } From 3ce9d38d76af005743b66a604d442f61f8605555 Mon Sep 17 00:00:00 2001 From: Nate Barlow Date: Sat, 23 May 2026 12:33:01 -0600 Subject: [PATCH 05/11] =?UTF-8?q?M19.1=20=C2=A75.5:=20resolve=20#84=20conf?= =?UTF-8?q?irmation-framing=20across=20underpay/overpay=20mails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reframes the post-confirmation alert mails so they're clearly distinguishable from the detection-time acknowledgment. The detection-time ack reads "Bitcoin payment detected … we'll follow up if anything needs your attention"; the confirmation-time alert was reading like a duplicate of that promise rather than the status update it actually is. All four templates (`InvoiceUnderpaymentClientMail`, `InvoiceUnderpaymentIssuerMail`, `InvoiceOverpaymentClientMail`, `InvoiceOverpaymentIssuerMail`) now carry a consistent "Payment confirmed — …" subject prefix and a body opener that explicitly names the confirmation event ("Your payment is now confirmed on the network …" / "The client's payment is now confirmed on the network …") so the reader can't mistake this for a duplicate of the detection-time ack. Also restructures §5.4 of the strategy doc so ship items nest as sub-items of the QA action that surfaced them, rather than sitting as top-level siblings. Fixes #84 Co-Authored-By: Claude Opus 4.7 --- app/Mail/InvoiceOverpaymentClientMail.php | 2 +- app/Mail/InvoiceOverpaymentIssuerMail.php | 2 +- app/Mail/InvoiceUnderpaymentClientMail.php | 2 +- app/Mail/InvoiceUnderpaymentIssuerMail.php | 2 +- .../19.1_NOTIFICATION_COVERAGE_AUDIT.md | 33 ++++++++++--------- .../mail/invoice-overpayment-client.blade.php | 4 +-- .../mail/invoice-overpayment-issuer.blade.php | 4 +-- .../invoice-underpayment-client.blade.php | 4 +-- .../invoice-underpayment-issuer.blade.php | 4 +-- 9 files changed, 30 insertions(+), 27 deletions(-) diff --git a/app/Mail/InvoiceOverpaymentClientMail.php b/app/Mail/InvoiceOverpaymentClientMail.php index 5a64c494..f443e116 100644 --- a/app/Mail/InvoiceOverpaymentClientMail.php +++ b/app/Mail/InvoiceOverpaymentClientMail.php @@ -21,7 +21,7 @@ public function __construct(public Invoice $invoice, public InvoiceDelivery $del public function envelope(): Envelope { return new Envelope( - subject: 'Invoice ' . ($this->invoice->number ?? $this->invoice->id) . ' was overpaid', + subject: 'Payment confirmed — Invoice ' . ($this->invoice->number ?? $this->invoice->id) . ' overpaid', replyTo: [new Address($this->invoice->user->email, $this->invoice->user->name)], ); } diff --git a/app/Mail/InvoiceOverpaymentIssuerMail.php b/app/Mail/InvoiceOverpaymentIssuerMail.php index 5c3c1ab2..729c150d 100644 --- a/app/Mail/InvoiceOverpaymentIssuerMail.php +++ b/app/Mail/InvoiceOverpaymentIssuerMail.php @@ -20,7 +20,7 @@ public function __construct(public Invoice $invoice, public InvoiceDelivery $del public function envelope(): Envelope { return new Envelope( - subject: 'Client overpaid invoice ' . ($this->invoice->number ?? $this->invoice->id), + subject: 'Payment confirmed — client overpaid Invoice ' . ($this->invoice->number ?? $this->invoice->id), ); } diff --git a/app/Mail/InvoiceUnderpaymentClientMail.php b/app/Mail/InvoiceUnderpaymentClientMail.php index 46980608..9a619c04 100644 --- a/app/Mail/InvoiceUnderpaymentClientMail.php +++ b/app/Mail/InvoiceUnderpaymentClientMail.php @@ -21,7 +21,7 @@ public function __construct(public Invoice $invoice, public InvoiceDelivery $del public function envelope(): Envelope { return new Envelope( - subject: 'Invoice ' . ($this->invoice->number ?? $this->invoice->id) . ' has a balance due', + subject: 'Payment confirmed — balance still due on Invoice ' . ($this->invoice->number ?? $this->invoice->id), replyTo: [new Address($this->invoice->user->email, $this->invoice->user->name)], ); } diff --git a/app/Mail/InvoiceUnderpaymentIssuerMail.php b/app/Mail/InvoiceUnderpaymentIssuerMail.php index 426d10a8..48076ba4 100644 --- a/app/Mail/InvoiceUnderpaymentIssuerMail.php +++ b/app/Mail/InvoiceUnderpaymentIssuerMail.php @@ -20,7 +20,7 @@ public function __construct(public Invoice $invoice, public InvoiceDelivery $del public function envelope(): Envelope { return new Envelope( - subject: 'Client underpayment alert for invoice ' . ($this->invoice->number ?? $this->invoice->id), + subject: 'Payment confirmed — client underpaid Invoice ' . ($this->invoice->number ?? $this->invoice->id), ); } diff --git a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md index 3005d2b9..0de54f26 100644 --- a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md +++ b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md @@ -105,24 +105,27 @@ The catch-all alias (`MAIL_ALIAS_ENABLED=true` rewriting recipients to `mailer.c 1. [x] [Agent] Take an issued invoice, simulate a partial confirmed payment. (Invoice #98 `INV-M191-S54-1134`; payment #109 at 46154/92308 sats; landed `partial`.) 2. [x] [Agent] Verify both acknowledgment Mailables fire; confirm delivery-log rows. (Delivery rows #72610 client-side, #72611 issuer-side; both `sent` with provider msgids.) -3. [x] [User] Receive both. Verify the acknowledgment framing is honest about "we received some" vs. final settlement. (Surfaced [#75](https://github.com/n8bar/CryptoZing/issues/75), [#76](https://github.com/n8bar/CryptoZing/issues/76), [#77](https://github.com/n8bar/CryptoZing/issues/77); fix items below.) -4. [x] [Agent] Ship [#75](https://github.com/n8bar/CryptoZing/issues/75): update `app/Mail/InvoicePaymentAcknowledgmentIssuerMail.php` subject + `resources/views/mail/invoice-payment-acknowledgment-issuer.blade.php` heading to `Review payment activity on Invoice INV-…`. - 1. [x] [Agent] Re-fire §5.4 mail pair against invoice #98; confirm rendered subject + heading match the new wording. (Queue-path dedupe blocks same-txid re-queue, so used direct `Mail::send`.) - 2. [x] [User] Eyeball QA: issuer-inbox subject reads clean. -5. [x] [Agent] Ship [#76](https://github.com/n8bar/CryptoZing/issues/76): tighten `resources/views/mail/invoice-payment-acknowledgment-client.blade.php` so the partial scenario reads clearly as partial without false completion. - 1. [x] Round 1: replaced `No action is needed right now.` with `We're still waiting for the network to confirm — we'll follow up if anything needs your attention.` QA: not completion ✓, but didn't read as *partial* either — drove round 2. - 2. [x] Round 2: added a conditional branch on `$invoice->outstanding_usd > 0` (mirrors issuer ack's outstanding-balance treatment). Partial branch read `Your invoice currently shows an outstanding balance of **$Y**. Confirmations are still settling — we'll follow up if anything else needs your attention.` QA: figure unclear pre/post-confirmation — drove round 3. - 3. [x] Round 3: refined partial branch to `This appears to be a partial payment — once confirmations settle, **$Y** will still be due on this invoice. We'll follow up if anything else needs your attention.` Spec-verified `outstanding_usd` stable across confirmation interval (per `docs/specs/PARTIAL_PAYMENTS.md:73-74`). QA: reads as partial ✓. Closes #76 on PR merge via `Fixes #76`. -6. [x] [Agent] Ship [#77](https://github.com/n8bar/CryptoZing/issues/77): default per-invoice delivery log to client-facing rows; add "Include emails sent to me" toggle that restores the unfiltered list; add feature test covering default-hidden vs. toggle-visible behavior. (Filter by `recipient` not by type enumeration (per #77's robustness note) — new `*_issuer` types stay hidden automatically. Section anchored as `

`; checkbox form submits to `…#delivery-log` so reload lands back at the section. Feature test asserts on the `Paid notice (issuer)` typeLabel as a stable target.) - 1. [x] [Agent] Run `./vendor/bin/sail artisan test` and confirm the new feature test passes alongside the existing suite. - 2. [x] [User] Browser QA: log in as `issuer-test@nateTheProgrammer.com` (password `password`), open invoice #98's page, confirm default-filtered delivery log + toggle reveals all rows. +3. [x] [User] Receive both. Verify the acknowledgment framing is honest about "we received some" vs. final settlement. + 1. [x] [Agent] Ship [#75](https://github.com/n8bar/CryptoZing/issues/75): update `app/Mail/InvoicePaymentAcknowledgmentIssuerMail.php` subject + `resources/views/mail/invoice-payment-acknowledgment-issuer.blade.php` heading to `Review payment activity on Invoice INV-…`. + 1. [x] [Agent] Re-fire §5.4 mail pair against invoice #98; confirm rendered subject + heading match the new wording. (Queue-path dedupe blocks same-txid re-queue, so used direct `Mail::send`.) + 2. [x] [User] Eyeball QA: issuer-inbox subject reads clean. + 2. [x] [Agent] Ship [#76](https://github.com/n8bar/CryptoZing/issues/76): tighten `resources/views/mail/invoice-payment-acknowledgment-client.blade.php` so the partial scenario reads clearly as partial without false completion. + 1. [x] Round 1: replaced `No action is needed right now.` with `We're still waiting for the network to confirm — we'll follow up if anything needs your attention.` QA: not completion ✓, but didn't read as *partial* either — drove round 2. + 2. [x] Round 2: added a conditional branch on `$invoice->outstanding_usd > 0` (mirrors issuer ack's outstanding-balance treatment). Partial branch read `Your invoice currently shows an outstanding balance of **$Y**. Confirmations are still settling — we'll follow up if anything else needs your attention.` QA: figure unclear pre/post-confirmation — drove round 3. + 3. [x] Round 3: refined partial branch to `This appears to be a partial payment — once confirmations settle, **$Y** will still be due on this invoice. We'll follow up if anything else needs your attention.` Spec-verified `outstanding_usd` stable across confirmation interval (per `docs/specs/PARTIAL_PAYMENTS.md:73-74`). QA: reads as partial ✓. Closes #76 on PR merge via `Fixes #76`. + 3. [x] [Agent] Ship [#77](https://github.com/n8bar/CryptoZing/issues/77): default per-invoice delivery log to client-facing rows; add "Include emails sent to me" toggle that restores the unfiltered list; add feature test covering default-hidden vs. toggle-visible behavior. (Filter by `recipient` not by type enumeration (per #77's robustness note) — new `*_issuer` types stay hidden automatically. Section anchored as `
`; checkbox form submits to `…#delivery-log` so reload lands back at the section. Feature test asserts on the `Paid notice (issuer)` typeLabel as a stable target.) + 1. [x] [Agent] Run `./vendor/bin/sail artisan test` and confirm the new feature test passes alongside the existing suite. + 2. [x] [User] Browser QA: log in as `issuer-test@nateTheProgrammer.com` (password `password`), open invoice #98's page, confirm default-filtered delivery log + toggle reveals all rows. ### 5. Underpayment alert (`InvoiceUnderpaymentClientMail` + `InvoiceUnderpaymentIssuerMail`) -1. [ ] [Agent] Take an issued invoice; simulate payment below threshold; advance scheduler past the underpay-alert cooldown so the alert is eligible to fire. -2. [ ] [Agent] Run the alert pass; verify both underpayment Mailables fire; confirm delivery-log rows. -3. [ ] [User] Receive both. Verify client view shows the outstanding USD amount per spec §4.4.2 (USD-only per §4 narrowing); issuer view surfaces the outstanding-balance follow-up per spec §4.4. -4. [ ] [Agent] Re-run the alert pass within cooldown; verify NO duplicate row is written (regression check on Findings 7 & 8 from MS17). +1. [x] [Agent] Take an issued invoice; simulate payment below threshold; advance scheduler past the underpay-alert cooldown so the alert is eligible to fire. (Reused invoice #98 — already `partial` at 50% underpaid (well above `CLIENT_ALERT_PERCENT=15.0`); `last_underpayment_alert_at` was null, so no cooldown advance needed for the first fire.) +2. [x] [Agent] Run the alert pass; verify both underpayment Mailables fire; confirm delivery-log rows. (Direct `InvoiceAlertService::checkPaymentThresholds($inv)`; delivery rows #72614 client + #72615 issuer queued and both `sent` with provider msgids.) +3. [x] [User] Receive both. Verify client view shows the outstanding USD amount per spec §4.4.2 (USD-only per §4 narrowing); issuer view surfaces the outstanding-balance follow-up per spec §4.4. + 1. [x] [Agent] Ship [#84](https://github.com/n8bar/CryptoZing/issues/84): reframe `invoice-underpayment-client.blade.php` opening so the confirmation event is named explicitly (candidate: `Your payment is now confirmed on the network, but **$Y** remains outstanding on this invoice.`); update subject line to match. Scope-check the issuer underpay + both overpay templates for the same pattern. (Applied across all four templates with consistent `Payment confirmed — …` subject prefix and `[Your | The client's] payment is now confirmed on the network` body opener. 66/66 tests passing.) + 1. [x] [Agent] Re-fire §5.5 underpay pair against invoice #98 (direct `Mail::send` per the §5.4 dedupe pattern); confirm rendered opening + subject reflect the new wording. (All four templates rendered against invoice #98 — subject + heading + opener match the new wording. Underpay pair sent live to `client-test@nospam.site` / `issuer-test@nateTheProgrammer.com`.) + 2. [x] [User] Eyeball QA: client mail clearly reads as a confirmation-time status update, not a duplicate of the detection-time ack already in the inbox. Closes #84 on PR merge via `Fixes #84`. +4. [x] [Agent] Re-run the alert pass within cooldown; verify NO duplicate row is written (regression check on Findings 7 & 8 from MS17). (Re-fired immediately after first send — `shouldSend()` returned false against the freshly-set `last_underpayment_alert_at`; row count held at 2.) ### 6. Overpayment alert (`InvoiceOverpaymentClientMail` + `InvoiceOverpaymentIssuerMail`) diff --git a/resources/views/mail/invoice-overpayment-client.blade.php b/resources/views/mail/invoice-overpayment-client.blade.php index 5631d4a9..0808b2a5 100644 --- a/resources/views/mail/invoice-overpayment-client.blade.php +++ b/resources/views/mail/invoice-overpayment-client.blade.php @@ -1,7 +1,7 @@ @component('mail::message', ['invoice' => $invoice]) -# Invoice {{ $invoice->number ?? $invoice->id }} was overpaid +# Payment confirmed — Invoice {{ $invoice->number ?? $invoice->id }} overpaid -We detected that the payment we received is about **{{ number_format($invoice->overpaymentPercent() ?? 0, 1) }}%** above the invoice total. +Your payment is now confirmed on the network and came in about **{{ number_format($invoice->overpaymentPercent() ?? 0, 1) }}%** above the invoice total. Overpayments are treated as gratuities by default, so please reply if this was accidental and we’ll coordinate a refund or credit. diff --git a/resources/views/mail/invoice-overpayment-issuer.blade.php b/resources/views/mail/invoice-overpayment-issuer.blade.php index b2731625..bd203be6 100644 --- a/resources/views/mail/invoice-overpayment-issuer.blade.php +++ b/resources/views/mail/invoice-overpayment-issuer.blade.php @@ -1,7 +1,7 @@ @component('mail::message', ['invoice' => $invoice]) -# Overpayment flagged on invoice {{ $invoice->number ?? $invoice->id }} +# Payment confirmed — Invoice {{ $invoice->number ?? $invoice->id }} overpaid -The latest payment puts this invoice roughly **{{ number_format($invoice->overpaymentPercent() ?? 0, 1) }}%** above the total. +The client's payment is now confirmed on the network, about **{{ number_format($invoice->overpaymentPercent() ?? 0, 1) }}%** above the invoice total. Decide whether to keep it as a tip, credit the client, or record a manual adjustment/refund. diff --git a/resources/views/mail/invoice-underpayment-client.blade.php b/resources/views/mail/invoice-underpayment-client.blade.php index f8781c10..e8cbea86 100644 --- a/resources/views/mail/invoice-underpayment-client.blade.php +++ b/resources/views/mail/invoice-underpayment-client.blade.php @@ -1,7 +1,7 @@ @component('mail::message', ['invoice' => $invoice]) -# Invoice {{ $invoice->number ?? $invoice->id }} still has a balance +# Payment confirmed — balance still due on Invoice {{ $invoice->number ?? $invoice->id }} -We received a payment, but **${{ number_format($invoice->outstanding_usd, 2) }}** remains outstanding on this invoice. +Your payment is now confirmed on the network, but **${{ number_format($invoice->outstanding_usd, 2) }}** remains outstanding on this invoice. Please use the button below to view the invoice and send the remaining amount. If you believe this is in error, reply to this email. diff --git a/resources/views/mail/invoice-underpayment-issuer.blade.php b/resources/views/mail/invoice-underpayment-issuer.blade.php index a96fd948..e3a7c10c 100644 --- a/resources/views/mail/invoice-underpayment-issuer.blade.php +++ b/resources/views/mail/invoice-underpayment-issuer.blade.php @@ -1,7 +1,7 @@ @component('mail::message', ['invoice' => $invoice]) -# Underpayment alert for invoice {{ $invoice->number ?? $invoice->id }} +# Payment confirmed — Invoice {{ $invoice->number ?? $invoice->id }} underpaid -The latest payment left **${{ number_format($invoice->outstanding_usd, 2) }}** still outstanding. +The client's payment is now confirmed on the network, leaving **${{ number_format($invoice->outstanding_usd, 2) }}** still outstanding. Consider following up with the client or recording a manual adjustment if you've already reconciled it elsewhere. From ecdf86a8d961074adafff739f397487359af9a70 Mon Sep 17 00:00:00 2001 From: Nate Barlow Date: Sat, 23 May 2026 13:56:09 -0600 Subject: [PATCH 06/11] =?UTF-8?q?M19.1=20=C2=A75.7:=20resolve=20#85=20past?= =?UTF-8?q?-due=20slot-aware=20framing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The past-due alert pipeline fires three escalation slots (1d / 7d / 14d) on independent context keys (past_due_1/_2/_3), but the rendered mail carried no slot indicator — three nudges arrived looking identical and the system's graduated escalation never surfaced to the recipient who's supposed to act on it. Now both Mailables read `$delivery->context_key` and render slot-aware subject + body across client + issuer audiences: - Slot 1 stays "Reminder" framing — one-day-late is often an oversight; calling it the first notice would treat soft contact as already-failed. - Slots 2/3 surface "2nd past-due notice" / "3rd past-due notice" anchored to past-due specifically (not counting any pre-due contact), plus a "1 week overdue" / "2 weeks overdue" time anchor. - Body copy escalates with the count — slot 2 opens with a follow- up acknowledgment; slot 3 names the delinquency timeline and hints at escalation outside the platform (the platform is watch-only; it can't pursue payment for the issuer). Also carries §5.6 (overpay alert) verification checkoffs — agent- side flow + cooldown regression all clean; user-side eyeball QA signed off. No code changes for §5.6. Fixes #85 Co-Authored-By: Claude Opus 4.7 --- app/Mail/InvoicePastDueClientMail.php | 19 +++++++++++- app/Mail/InvoicePastDueIssuerMail.php | 19 +++++++++++- .../19.1_NOTIFICATION_COVERAGE_AUDIT.md | 27 +++++++++-------- .../mail/invoice-past-due-client.blade.php | 18 ++++++++++- .../mail/invoice-past-due-issuer.blade.php | 30 +++++++++++++++++-- 5 files changed, 95 insertions(+), 18 deletions(-) diff --git a/app/Mail/InvoicePastDueClientMail.php b/app/Mail/InvoicePastDueClientMail.php index 263902ee..d1eb640d 100644 --- a/app/Mail/InvoicePastDueClientMail.php +++ b/app/Mail/InvoicePastDueClientMail.php @@ -20,8 +20,15 @@ public function __construct(public Invoice $invoice, public InvoiceDelivery $del public function envelope(): Envelope { + $number = $this->invoice->number ?? $this->invoice->id; + $subject = match ($this->slot()) { + 2 => "2nd past-due notice — invoice {$number} is 1 week overdue", + 3 => "3rd past-due notice — invoice {$number} is 2 weeks overdue", + default => "Reminder: invoice {$number} is past due", + }; + return new Envelope( - subject: 'Reminder: invoice ' . ($this->invoice->number ?? $this->invoice->id) . ' is past due', + subject: $subject, replyTo: [new Address($this->invoice->user->email, $this->invoice->user->name)], ); } @@ -32,6 +39,7 @@ public function content(): Content markdown: 'mail.invoice-past-due-client', with: [ 'invoice' => $this->invoice, + 'slot' => $this->slot(), ], ); } @@ -40,4 +48,13 @@ public function attachments(): array { return []; } + + private function slot(): int + { + if (preg_match('/^past_due_(\d+)$/', (string) ($this->delivery->context_key ?? ''), $m)) { + return (int) $m[1]; + } + + return 1; + } } diff --git a/app/Mail/InvoicePastDueIssuerMail.php b/app/Mail/InvoicePastDueIssuerMail.php index b5f094c6..4cb7884c 100644 --- a/app/Mail/InvoicePastDueIssuerMail.php +++ b/app/Mail/InvoicePastDueIssuerMail.php @@ -19,8 +19,15 @@ public function __construct(public Invoice $invoice, public InvoiceDelivery $del public function envelope(): Envelope { + $number = $this->invoice->number ?? $this->invoice->id; + $subject = match ($this->slot()) { + 2 => "2nd past-due notice sent — invoice {$number} 1 week overdue", + 3 => "3rd past-due notice sent — invoice {$number} 2 weeks overdue", + default => "Reminder sent: invoice {$number} past due", + }; + return new Envelope( - subject: 'Invoice ' . ($this->invoice->number ?? $this->invoice->id) . ' is past due', + subject: $subject, ); } @@ -30,6 +37,7 @@ public function content(): Content markdown: 'mail.invoice-past-due-issuer', with: [ 'invoice' => $this->invoice, + 'slot' => $this->slot(), ], ); } @@ -38,4 +46,13 @@ public function attachments(): array { return []; } + + private function slot(): int + { + if (preg_match('/^past_due_(\d+)$/', (string) ($this->delivery->context_key ?? ''), $m)) { + return (int) $m[1]; + } + + return 1; + } } diff --git a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md index 0de54f26..59a4771a 100644 --- a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md +++ b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md @@ -129,23 +129,26 @@ The catch-all alias (`MAIL_ALIAS_ENABLED=true` rewriting recipients to `mailer.c ### 6. Overpayment alert (`InvoiceOverpaymentClientMail` + `InvoiceOverpaymentIssuerMail`) -1. [ ] [Agent] Take an issued invoice; simulate payment above threshold. -2. [ ] [Agent] Run the alert pass; verify both overpayment Mailables fire; confirm delivery-log rows. -3. [ ] [User] Receive both. Verify the issuer view's overpay disposition prompt per spec §4.3. -4. [ ] [Agent] Re-run within cooldown; verify no duplicate row. +1. [x] [Agent] Take an issued invoice; simulate payment above threshold. (Invoice #99 `INV-M191-S56-a278` for the §5.4 issuer/client personas; $40 USD, locked at $50k/BTC. Injected confirmed payment #110 at 120,000 sats / $60 = 150% of total. `refreshPaymentLedger` landed `paid` (system treats overpay as paid-with-flag); `overpaymentPercent()` = 50%, well above `CLIENT_ALERT_PERCENT=15.0`.) +2. [x] [Agent] Run the alert pass; verify both overpayment Mailables fire; confirm delivery-log rows. (Direct `InvoiceAlertService::checkPaymentThresholds($inv)`; delivery rows #72617 client + #72618 issuer queued and both `sent` with provider msgids.) +3. [x] [User] Receive both. Verify the issuer view's overpay disposition prompt per spec §4.3. +4. [x] [Agent] Re-run within cooldown; verify no duplicate row. (Re-fired; row count held at 2.) ### 7. Past-due (force-set due date) (`InvoicePastDueClientMail` + `InvoicePastDueIssuerMail`) This one matters most — Nate flagged late-payment notifications specifically as critical to verify thoroughly. Use real time-advanced scenarios, not just unit-level "if past due, send mail" assertions. -1. [ ] [Agent] Create an issued, unpaid invoice. Force-set its `due_at` (or equivalent) to ≥1 day in the past via a tinker statement or database update. -2. [ ] [Agent] Run `InvoiceAlertService::sendPastDueAlerts()` (via `php artisan schedule:run` or direct service call). -3. [ ] [Agent] Verify slot 1 fires for both client and issuer; confirm delivery-log rows; verify slot 2 and slot 3 do NOT fire on the same run. -4. [ ] [User] Receive both. Verify outstanding-USD copy is not falsely qualified as "approximate" (regression check on Finding 2 from MS17); links resolve. -5. [ ] [Agent] Force-set `due_at` to 7+ days in the past; re-run; verify slot 2 fires. -6. [ ] [Agent] Force-set `due_at` to 14+ days in the past; re-run; verify slot 3 fires. -7. [ ] [Agent] Run `sendPastDueAlerts` again with all three slots already sent; verify NO new delivery-log rows appear (regression check on Findings 3 & 4 from MS17). -8. [ ] [User] Receive each slot at issuer + client personas; spot-check that escalation tone or content actually escalates between slots if that's the spec'd behavior. +1. [x] [Agent] Create an issued, unpaid invoice. Force-set its `due_at` (or equivalent) to ≥1 day in the past via a tinker statement or database update. (Invoice #100 `INV-M191-S57-a434` for the §5.4 personas; $80 USD, status `sent`, no payments; `due_date` set to yesterday.) +2. [x] [Agent] Run `InvoiceAlertService::sendPastDueAlerts()` (via `php artisan schedule:run` or direct service call). (Direct service call.) +3. [x] [Agent] Verify slot 1 fires for both client and issuer; confirm delivery-log rows; verify slot 2 and slot 3 do NOT fire on the same run. (Slot 1 fired: delivery rows #72619 issuer + #72620 client, both `sent` with ctx=`past_due_1`. Only those two rows existed after — slot 2/3 correctly held until their thresholds.) +4. [x] [User] Receive both. Verify outstanding-USD copy is not falsely qualified as "approximate" (regression check on Finding 2 from MS17); links resolve. +5. [x] [Agent] Force-set `due_at` to 7+ days in the past; re-run; verify slot 2 fires. (Set to 8 days; delivery rows #72621 issuer + #72622 client with ctx=`past_due_2`.) +6. [x] [Agent] Force-set `due_at` to 14+ days in the past; re-run; verify slot 3 fires. (Set to 15 days; delivery rows #72623 issuer + #72624 client with ctx=`past_due_3`.) +7. [x] [Agent] Run `sendPastDueAlerts` again with all three slots already sent; verify NO new delivery-log rows appear (regression check on Findings 3 & 4 from MS17). (Re-fired; row count held at 6.) +8. [x] [User] Receive each slot at issuer + client personas; spot-check that escalation tone or content actually escalates between slots if that's the spec'd behavior. + 1. [x] [Agent] Ship [#85](https://github.com/n8bar/CryptoZing/issues/85): slot-aware subject + body across both `InvoicePastDueClientMail` / `InvoicePastDueIssuerMail` and their templates. Slot derived from `$delivery->context_key` (`past_due_1`/`_2`/`_3`); slot 1 reads as a friendly reminder, slot 2/3 surface "2nd past-due notice" / "3rd past-due notice" + "1 week overdue" / "2 weeks overdue" anchors. 66/66 tests passing. + 1. [x] [Agent] Re-fire all six (3 client × 3 issuer × slot 1/2/3) via direct `Mail::send` against invoice #100; subject + H1 match per slot. Live copies in `client-test@nospam.site` (3) and `issuer-test@nateTheProgrammer.com` (3). + 2. [x] [User] Eyeball QA: three slots side-by-side read as a clear escalation arc, not three identical mails. Closes #85 on PR merge via `Fixes #85`. ### 8. Branding preview (`NotificationBrandingPreviewMail`) diff --git a/resources/views/mail/invoice-past-due-client.blade.php b/resources/views/mail/invoice-past-due-client.blade.php index 3f2e1428..cd908e41 100644 --- a/resources/views/mail/invoice-past-due-client.blade.php +++ b/resources/views/mail/invoice-past-due-client.blade.php @@ -1,14 +1,30 @@ @php $summary = $invoice->paymentSummary(\App\Services\BtcRate::current()); $outstandingUsd = $summary['outstanding_usd'] ?? 0; + $slot = $slot ?? 1; + $number = $invoice->number ?? $invoice->id; @endphp @component('mail::message', ['invoice' => $invoice]) -# Invoice {{ $invoice->number ?? $invoice->id }} is past due +@if ($slot === 3) +# 3rd past-due notice — invoice {{ $number }} is 2 weeks overdue + +This is the third past-due notice. Invoice {{ $number }} has been overdue for two weeks with an outstanding balance of **${{ number_format($outstandingUsd, 2) }} USD** that remains unresolved. + +Please remit payment promptly. If you've already paid or there's a reason for the delay, reply to this email so we can resolve it. +@elseif ($slot === 2) +# 2nd past-due notice — invoice {{ $number }} is 1 week overdue + +Following up — invoice {{ $number }} has now been past due for a week with an outstanding balance of **${{ number_format($outstandingUsd, 2) }} USD**. + +Please settle the balance or reply to this email to confirm payment status. +@else +# Reminder: invoice {{ $number }} is past due Our records show an outstanding balance of **${{ number_format($outstandingUsd, 2) }} USD**. Please review the invoice and settle the remaining amount. If you already paid, just reply to this email so we can reconcile it. +@endif @component('mail::button', ['url' => $invoice->public_url]) View invoice diff --git a/resources/views/mail/invoice-past-due-issuer.blade.php b/resources/views/mail/invoice-past-due-issuer.blade.php index 40ed7b8d..ac0d59e6 100644 --- a/resources/views/mail/invoice-past-due-issuer.blade.php +++ b/resources/views/mail/invoice-past-due-issuer.blade.php @@ -1,18 +1,42 @@ @php $summary = $invoice->paymentSummary(\App\Services\BtcRate::current()); $outstandingUsd = $summary['outstanding_usd'] ?? 0; + $slot = $slot ?? 1; + $number = $invoice->number ?? $invoice->id; @endphp @component('mail::message', ['invoice' => $invoice]) -# Invoice {{ $invoice->number ?? $invoice->id }} is past due +@if ($slot === 3) +# 3rd past-due notice sent — invoice {{ $number }} 2 weeks overdue -This invoice is now past its due date and still has an outstanding balance. +Your client has now received the third past-due notice. The invoice has been overdue for two weeks with no payment recorded. - **Client:** {{ $invoice->client->name ?? 'N/A' }} - **Due date:** {{ optional($invoice->due_date)->toDateString() ?? '—' }} - **Outstanding:** ${{ number_format($outstandingUsd, 2) }} -Consider nudging the client or recording a manual adjustment if you've already reconciled it. +This may warrant direct outreach, a manual adjustment, or escalation outside the platform. +@elseif ($slot === 2) +# 2nd past-due notice sent — invoice {{ $number }} 1 week overdue + +Your client has now received a second past-due notice. The invoice has been overdue for a week. + +- **Client:** {{ $invoice->client->name ?? 'N/A' }} +- **Due date:** {{ optional($invoice->due_date)->toDateString() ?? '—' }} +- **Outstanding:** ${{ number_format($outstandingUsd, 2) }} + +Consider following up with the client directly or recording a manual adjustment if you've already reconciled this elsewhere. +@else +# Reminder sent: invoice {{ $number }} past due + +A past-due reminder went out to your client. + +- **Client:** {{ $invoice->client->name ?? 'N/A' }} +- **Due date:** {{ optional($invoice->due_date)->toDateString() ?? '—' }} +- **Outstanding:** ${{ number_format($outstandingUsd, 2) }} + +Consider nudging the client directly if you haven't already. +@endif @component('mail::button', ['url' => route('invoices.show', $invoice)]) Open invoice From 26c4f1c073d7a1acd26e3c595cc11c00e5166e1a Mon Sep 17 00:00:00 2001 From: Nate Barlow Date: Sat, 23 May 2026 14:01:45 -0600 Subject: [PATCH 07/11] =?UTF-8?q?M19.1=20=C2=A75.8:=20branding=20preview?= =?UTF-8?q?=20verification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Branding preview Mailable fires via NotificationSettingsController's direct Mail::to path; no invoice_deliveries row is created (bypass per spec §5.14.4.1 confirmed). Rendered preview lands at the owner's inbox and reads as expected. No code changes — verification-only. Co-Authored-By: Claude Opus 4.7 --- docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md index 59a4771a..dee8495a 100644 --- a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md +++ b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md @@ -152,9 +152,9 @@ This one matters most — Nate flagged late-payment notifications specifically a ### 8. Branding preview (`NotificationBrandingPreviewMail`) -1. [ ] [Agent] As an authenticated owner, trigger the branding preview from `NotificationSettingsController::send`. -2. [ ] [Agent] Confirm the preview Mailable fires and is sent directly without creating a delivery-log row (per spec §5.14.4.1). -3. [ ] [User] Receive the preview; verify branding renders as expected. +1. [x] [Agent] As an authenticated owner, trigger the branding preview from `NotificationSettingsController::send`. (Invoked the same `Mail::to($user->email)->send(new NotificationBrandingPreviewMail($user))` path the controller uses, against the issuer-test user.) +2. [x] [Agent] Confirm the preview Mailable fires and is sent directly without creating a delivery-log row (per spec §5.14.4.1). (`invoice_deliveries` row count held at 215 across the send.) +3. [x] [User] Receive the preview; verify branding renders as expected. ### 9. Remove deprecated partial-warning mail family (`InvoicePartialWarningClientMail` + `InvoicePartialWarningIssuerMail`) From 45319f8afeab54630852f0ee9e305c30cf743355 Mon Sep 17 00:00:00 2001 From: Nate Barlow Date: Sat, 23 May 2026 14:39:18 -0600 Subject: [PATCH 08/11] =?UTF-8?q?M19.1=20=C2=A75.9:=20remove=20deprecated?= =?UTF-8?q?=20partial-warning=20mail=20family=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The InvoicePartialWarning{Client,Issuer}Mail family was wired into DeliverInvoiceMail's type map but never queued by service code — dormant Mailables held around under a "preserve for historical delivery-log rendering" premise. Pre-launch the database holds only seed/test data, so that premise no longer applies. Removed: - app/Mail/InvoicePartialWarning{Client,Issuer}Mail.php (deleted) - resources/views/mail/invoice-partial-warning-{client,issuer}.blade.php (deleted) - DeliverInvoiceMail: type-map entries, imports, and the partial- warning skip-handler branch in skipReason() - InvoiceDelivery::typeLabel: the two partial-warning entries - InvoiceAlertService::skipInvalidQueuedDeliveries: the partial- warning skip branch (no longer reachable) - Invoice::shouldWarnAboutPartialPayments() (no remaining callers) - Invoice $fillable + $casts: last_partial_warning_sent_at - Schema: dropped invoices.last_partial_warning_sent_at; deleted *_partial_warning rows from invoice_deliveries - Spec NOTIFICATIONS.md: §5.12.3 (legacy-rows-render clause) and the two matrix rows in Coverage & Status - Test surgery: - InvoicePaymentCorrectionTest: fixture rows substituted with overpay-alert types (still in skipInvalidQueuedDeliveries scope); test method renamed accordingly - WatchPaymentsCommandTest: deleted the two regression tests asserting absence of removed types §4.4.3 of the spec stays — useful historical context for why the underpay alert exists in place of the partial-warning family. Full suite green: 335 passed / 1639 assertions / 184s. Fixes #82 Co-Authored-By: Claude Opus 4.7 --- app/Jobs/DeliverInvoiceMail.php | 9 -- app/Mail/InvoicePartialWarningClientMail.php | 43 ------- app/Mail/InvoicePartialWarningIssuerMail.php | 41 ------ app/Models/Invoice.php | 18 --- app/Models/InvoiceDelivery.php | 2 - app/Services/InvoiceAlertService.php | 8 -- ...t_and_clean_partial_warning_deliveries.php | 27 ++++ docs/specs/NOTIFICATIONS.md | 7 +- .../19.1_NOTIFICATION_COVERAGE_AUDIT.md | 13 +- .../invoice-partial-warning-client.blade.php | 25 ---- .../invoice-partial-warning-issuer.blade.php | 25 ---- .../Feature/InvoicePaymentCorrectionTest.php | 6 +- .../Wallet/WatchPaymentsCommandTest.php | 118 ------------------ 13 files changed, 38 insertions(+), 304 deletions(-) delete mode 100644 app/Mail/InvoicePartialWarningClientMail.php delete mode 100644 app/Mail/InvoicePartialWarningIssuerMail.php create mode 100644 database/migrations/2026_05_23_140808_drop_last_partial_warning_sent_at_and_clean_partial_warning_deliveries.php delete mode 100644 resources/views/mail/invoice-partial-warning-client.blade.php delete mode 100644 resources/views/mail/invoice-partial-warning-issuer.blade.php diff --git a/app/Jobs/DeliverInvoiceMail.php b/app/Jobs/DeliverInvoiceMail.php index 60a7196f..58ba0a47 100644 --- a/app/Jobs/DeliverInvoiceMail.php +++ b/app/Jobs/DeliverInvoiceMail.php @@ -14,8 +14,6 @@ use App\Mail\InvoiceUnderpaymentIssuerMail; use App\Mail\InvoiceIssuerPaidNoticeMail; use App\Models\Invoice; -use App\Mail\InvoicePartialWarningClientMail; -use App\Mail\InvoicePartialWarningIssuerMail; use App\Models\InvoiceDelivery; use App\Models\InvoicePayment; use App\Services\InvoiceDeliveryService; @@ -140,8 +138,6 @@ public function handle(MailAlias $mailAlias, InvoiceDeliveryService $deliveries) 'issuer_overpay_alert' => new InvoiceOverpaymentIssuerMail($invoice, $delivery), 'client_underpay_alert' => new InvoiceUnderpaymentClientMail($invoice, $delivery), 'issuer_underpay_alert' => new InvoiceUnderpaymentIssuerMail($invoice, $delivery), - 'client_partial_warning' => new InvoicePartialWarningClientMail($invoice, $delivery), - 'issuer_partial_warning' => new InvoicePartialWarningIssuerMail($invoice, $delivery), default => new InvoiceReadyMail($invoice, $delivery), }; @@ -288,11 +284,6 @@ private function shouldSkipDelivery( return 'Underpayment resolved before send.'; } - $partialTypes = ['client_partial_warning', 'issuer_partial_warning']; - if (in_array($delivery->type, $partialTypes, true) && !$invoice->shouldWarnAboutPartialPayments()) { - return 'Partial-payment warning no longer applicable.'; - } - $pastDueTypes = ['past_due_issuer', 'past_due_client']; if (in_array($delivery->type, $pastDueTypes, true) && in_array($invoice->status, ['paid', 'void'], true)) { return 'Invoice settled before past-due alert.'; diff --git a/app/Mail/InvoicePartialWarningClientMail.php b/app/Mail/InvoicePartialWarningClientMail.php deleted file mode 100644 index 55c8ee95..00000000 --- a/app/Mail/InvoicePartialWarningClientMail.php +++ /dev/null @@ -1,43 +0,0 @@ -invoice->number ?? $this->invoice->id) . ' payment reminder', - replyTo: [new Address($this->invoice->user->email, $this->invoice->user->name)], - ); - } - - public function content(): Content - { - return new Content( - markdown: 'mail.invoice-partial-warning-client', - with: [ - 'invoice' => $this->invoice, - ], - ); - } - - public function attachments(): array - { - return []; - } -} diff --git a/app/Mail/InvoicePartialWarningIssuerMail.php b/app/Mail/InvoicePartialWarningIssuerMail.php deleted file mode 100644 index 4eb94821..00000000 --- a/app/Mail/InvoicePartialWarningIssuerMail.php +++ /dev/null @@ -1,41 +0,0 @@ -invoice->number ?? $this->invoice->id), - ); - } - - public function content(): Content - { - return new Content( - markdown: 'mail.invoice-partial-warning-issuer', - with: [ - 'invoice' => $this->invoice, - ], - ); - } - - public function attachments(): array - { - return []; - } -} diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index a6bfd225..f149d514 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -29,7 +29,6 @@ class Invoice extends Model 'billing_address_override','invoice_footer_note_override','branding_heading_override', 'last_overpayment_alert_at','last_underpayment_alert_at', 'last_past_due_issuer_alert_at','last_past_due_client_alert_at', - 'last_partial_warning_sent_at', ]; protected $casts = [ @@ -58,7 +57,6 @@ class Invoice extends Model 'last_underpayment_alert_at' => 'datetime', 'last_past_due_issuer_alert_at' => 'datetime', 'last_past_due_client_alert_at' => 'datetime', - 'last_partial_warning_sent_at' => 'datetime', ]; public const SATS_PER_BTC = 100_000_000; public const PAYMENT_SAT_TOLERANCE = 100; @@ -690,22 +688,6 @@ public function getPublicUrlAttribute(): ?string return $base . $path; } - public function shouldWarnAboutPartialPayments(): bool - { - if (in_array($this->status, ['paid','void'])) { - return false; - } - - $outstanding = $this->outstanding_sats; - if ($outstanding === null || $outstanding <= 0) { - return false; - } - - $payments = $this->activePayments(); - - return $payments->count() >= 2; - } - public function sumPaymentsUsd(bool $confirmedOnly = false): float { $payments = $this->activePayments(); diff --git a/app/Models/InvoiceDelivery.php b/app/Models/InvoiceDelivery.php index fc0a9001..928c9fa7 100644 --- a/app/Models/InvoiceDelivery.php +++ b/app/Models/InvoiceDelivery.php @@ -58,8 +58,6 @@ public function typeLabel(): string 'issuer_overpay_alert' => 'Overpayment alert (issuer)', 'client_underpay_alert' => 'Underpayment alert (client)', 'issuer_underpay_alert' => 'Underpayment alert (issuer)', - 'client_partial_warning' => 'Partial payment warning (client)', - 'issuer_partial_warning' => 'Partial payment warning (issuer)', default => Str::of($this->type)->replace('_', ' ')->headline()->toString(), }; } diff --git a/app/Services/InvoiceAlertService.php b/app/Services/InvoiceAlertService.php index 57297b95..95f18464 100644 --- a/app/Services/InvoiceAlertService.php +++ b/app/Services/InvoiceAlertService.php @@ -98,14 +98,6 @@ public function skipInvalidQueuedDeliveries(Invoice $invoice, string $reasonPref "{$reasonPrefix} Overpayment alert no longer applies." ); } - - if (! $invoice->shouldWarnAboutPartialPayments()) { - $this->skipQueuedDeliveries( - $invoice, - ['client_partial_warning', 'issuer_partial_warning'], - "{$reasonPrefix} Partial-payment warning no longer applies." - ); - } } public function sendPastDueAlerts(Invoice $invoice): void diff --git a/database/migrations/2026_05_23_140808_drop_last_partial_warning_sent_at_and_clean_partial_warning_deliveries.php b/database/migrations/2026_05_23_140808_drop_last_partial_warning_sent_at_and_clean_partial_warning_deliveries.php new file mode 100644 index 00000000..6342b405 --- /dev/null +++ b/database/migrations/2026_05_23_140808_drop_last_partial_warning_sent_at_and_clean_partial_warning_deliveries.php @@ -0,0 +1,27 @@ +whereIn('type', ['client_partial_warning', 'issuer_partial_warning']) + ->delete(); + + Schema::table('invoices', function (Blueprint $table) { + $table->dropColumn('last_partial_warning_sent_at'); + }); + } + + public function down(): void + { + Schema::table('invoices', function (Blueprint $table) { + $table->timestamp('last_partial_warning_sent_at')->nullable(); + }); + } +}; diff --git a/docs/specs/NOTIFICATIONS.md b/docs/specs/NOTIFICATIONS.md index 053ab751..fa1dbf50 100644 --- a/docs/specs/NOTIFICATIONS.md +++ b/docs/specs/NOTIFICATIONS.md @@ -85,9 +85,8 @@ 12. The delivery history should use concise, human-friendly labels for communication classes and outcomes. 1. Manual invoice sends should display as `Invoice email`, not a raw storage key. 2. Paired issuer/client notification rows should keep the audience explicit in the label, such as `Past-due reminder (client)` and `Underpayment alert (issuer)`. - 3. While the legacy repeated-partial warning rows still exist in stored history, they should display honestly as `Partial payment warning (client|issuer)` rather than being hidden behind renamed copy. - 4. Payment-triggered follow-up should keep the acknowledgment-versus-receipt split visible in history once those rows ship, using labels such as `Payment acknowledgment (client)`, `Payment acknowledgment (owner)`, and `Receipt (client)` for the later higher-certainty follow-up. - 5. Outcome labels should display as `Queued`, `Sending`, `Sent`, `Skipped`, and `Failed`. + 3. Payment-triggered follow-up should keep the acknowledgment-versus-receipt split visible in history once those rows ship, using labels such as `Payment acknowledgment (client)`, `Payment acknowledgment (owner)`, and `Receipt (client)` for the later higher-certainty follow-up. + 4. Outcome labels should display as `Queued`, `Sending`, `Sent`, `Skipped`, and `Failed`. 13. Outbound mail copy should stay concise and actionable. 14. Owner-facing mail-branding settings for RC may expose only constrained brand-shell controls, not arbitrary message editing. 1. Allowed MS16 fields are limited to simple mail chrome values such as brand name, short tagline, footer blurb, and whether the default CryptoZing logo is shown in the shared mail header. @@ -115,6 +114,4 @@ One row per outbound notice class. `Status`: `live` = in production code, behavi | Owner | Overpayment ≥15% threshold | `InvoiceOverpaymentIssuerMail` | live | [`InvoiceNotificationTest`](../../tests/Feature/InvoiceNotificationTest.php) | `issuer_overpay_alert` | | Client | Underpayment ≥15% remaining | `InvoiceUnderpaymentClientMail` | live | [`InvoiceNotificationTest`](../../tests/Feature/InvoiceNotificationTest.php) | `client_underpay_alert` | | Owner | Underpayment ≥15% remaining | `InvoiceUnderpaymentIssuerMail` | live | [`InvoiceNotificationTest`](../../tests/Feature/InvoiceNotificationTest.php) | `issuer_underpay_alert` | -| Client | (deprecated — superseded by underpay alert per §4.4.3; legacy rows render in history per §5.12.3) | `InvoicePartialWarningClientMail` | stubbed | [`WatchPaymentsCommandTest`](../../tests/Feature/Wallet/WatchPaymentsCommandTest.php) (asserts zero new queued) | `client_partial_warning` | -| Owner | (deprecated — superseded by underpay alert per §4.4.3; legacy rows render in history per §5.12.3) | `InvoicePartialWarningIssuerMail` | stubbed | [`WatchPaymentsCommandTest`](../../tests/Feature/Wallet/WatchPaymentsCommandTest.php) (asserts zero new queued) | `issuer_partial_warning` | | Owner (self) | Manual "send me a test email" preview action (§5.14.4) | `NotificationBrandingPreviewMail` | live | [`MailBrandingTest`](../../tests/Feature/MailBrandingTest.php) | — (no delivery-log row, per §5.14.4.1) | diff --git a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md index dee8495a..4ac7a236 100644 --- a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md +++ b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md @@ -160,12 +160,12 @@ This one matters most — Nate flagged late-payment notifications specifically a §1 surfaced these as dormant Mailables — wired into the type map but never queued by service code. They were kept around under a "preserve for historical delivery-log rendering" premise. Pre-launch the database holds only seed/test data (8 `*_partial_warning` rows from 2026-03), so the premise collapses. Ship the full removal per [#82](https://github.com/n8bar/CryptoZing/issues/82). -1. [ ] [Agent] Code changes: delete the two Mailable classes + two blade templates; strip imports + type-map entries from `DeliverInvoiceMail`; remove the two `typeLabel()` entries from `InvoiceDelivery`; remove the two skip-handler branches (`DeliverInvoiceMail::skipReason`, `InvoiceAlertService::skipInvalidQueuedDeliveries`); remove `Invoice::shouldWarnAboutPartialPayments()` (no other callers); remove `last_partial_warning_sent_at` from `Invoice` `$fillable` and `$casts`. -2. [ ] [Agent] Migration: drop `last_partial_warning_sent_at` column from `invoices`; delete `*_partial_warning` rows from `invoice_deliveries`. -3. [ ] [Agent] Test fixture surgery in `tests/Feature/InvoicePaymentCorrectionTest.php` (lines 201-216): substitute the two `*_partial_warning` delivery fixtures with another valid type (e.g., `past_due_client` / `past_due_issuer`); preserve the "4 queued, all skipped on payment restore" assertion. -4. [ ] [Agent] Delete regression tests `test_repeated_partial_payment_detection_no_longer_creates_a_separate_partial_warning_family` and `test_repeated_partial_payment_detection_does_not_enqueue_legacy_partial_warnings_on_repeat_runs` from `tests/Feature/Wallet/WatchPaymentsCommandTest.php` (lines 525-641) — they assert absence of removed types. -5. [ ] [Agent] Spec update: strip `docs/specs/NOTIFICATIONS.md` line 88 (legacy-rows-render clause in §5 sub-item 12.3) and matrix rows 118-119 (Coverage & Status drops from 14 to 12 live rows). -6. [ ] [Agent] Run `./vendor/bin/sail artisan test` — full suite passes. +1. [x] [Agent] Code changes: delete the two Mailable classes + two blade templates; strip imports + type-map entries from `DeliverInvoiceMail`; remove the two `typeLabel()` entries from `InvoiceDelivery`; remove the two skip-handler branches (`DeliverInvoiceMail::skipReason`, `InvoiceAlertService::skipInvalidQueuedDeliveries`); remove `Invoice::shouldWarnAboutPartialPayments()` (no other callers); remove `last_partial_warning_sent_at` from `Invoice` `$fillable` and `$casts`. +2. [x] [Agent] Migration: drop `last_partial_warning_sent_at` column from `invoices`; delete `*_partial_warning` rows from `invoice_deliveries`. (Migration `2026_05_23_140808_drop_last_partial_warning_sent_at_and_clean_partial_warning_deliveries.php`; ran in 291ms.) +3. [x] [Agent] Test fixture surgery in `tests/Feature/InvoicePaymentCorrectionTest.php` (lines 201-216): substitute the two `*_partial_warning` delivery fixtures with another valid type (e.g., `past_due_client` / `past_due_issuer`); preserve the "4 queued, all skipped on payment restore" assertion. (Substituted with `client_overpay_alert` / `issuer_overpay_alert` instead of past_due — overpay alerts are still in `skipInvalidQueuedDeliveries` scope, so the restore-to-paid transition skips them via the existing branch. Past-due types would have required adding new skip-handler scope. Renamed the test method to `_skip_stale_underpay_and_overpay_deliveries` to match.) +4. [x] [Agent] Delete regression tests `test_repeated_partial_payment_detection_no_longer_creates_a_separate_partial_warning_family` and `test_repeated_partial_payment_detection_does_not_enqueue_legacy_partial_warnings_on_repeat_runs` from `tests/Feature/Wallet/WatchPaymentsCommandTest.php` (lines 525-641) — they assert absence of removed types. +5. [x] [Agent] Spec update: strip `docs/specs/NOTIFICATIONS.md` line 88 (legacy-rows-render clause in §5 sub-item 12.3) and matrix rows 118-119 (Coverage & Status drops from 14 to 12 live rows). (Sub-item 12.3 stripped and 12.4 / 12.5 renumbered to 12.3 / 12.4; matrix now 12 live + 1 branding row. §4.4.3 left intact — still useful historical context for why underpay alert exists.) +6. [x] [Agent] Run `./vendor/bin/sail artisan test` — full suite passes. (335 passed / 1639 assertions / 184s.) ### 10. Edge cases @@ -200,7 +200,6 @@ We don't have dedicated load-test infrastructure before open beta, and Mailgun's - [ ] Catch-all alias disabled (`MAIL_ALIAS_ENABLED=false`); test mail confirmed reaching real intended addresses without rewrite. - [ ] Every notice class observed end-to-end at least once via §5 scenarios — trigger fired, Mailable sent, delivery-log row truthful, mail received at intended recipient, eyeball QA passed. - [ ] Late-payment / past-due notification flow verified across all three escalation slots with force-set due dates. -- [ ] Stubbed-by-design partial-warning classes confirmed quiet under live alert passes. - [ ] All §5 findings resolved inline per sub-section — in-phase fixes shipped or backlog-deferred with context. - [ ] §6 stress-readiness check executed; "what was NOT tested" risks documented for post-launch ops. diff --git a/resources/views/mail/invoice-partial-warning-client.blade.php b/resources/views/mail/invoice-partial-warning-client.blade.php deleted file mode 100644 index 47e92573..00000000 --- a/resources/views/mail/invoice-partial-warning-client.blade.php +++ /dev/null @@ -1,25 +0,0 @@ -@php - $summary = $invoice->paymentSummary(\App\Services\BtcRate::current()); - $outstandingUsd = $summary['outstanding_usd'] ?? 0; - $outstandingBtc = $summary['outstanding_btc_formatted'] ?? null; -@endphp - -@component('mail::message', ['invoice' => $invoice]) -# Quick reminder for Invoice {{ $invoice->number ?? $invoice->id }} - -We noticed more than one payment attempt for this invoice. Splitting a single invoice across multiple Bitcoin transactions usually means **paying extra miner fees** and might slow down processing. - -@component('mail::panel') -**Outstanding balance:** ${{ number_format($outstandingUsd, 2) }} USD -**BTC equivalent:** {{ $outstandingBtc ?? '—' }} BTC -**Due date:** {{ optional($invoice->due_date)->toFormattedDateString() ?? '—' }} -@endcomponent - -@component('mail::button', ['url' => $invoice->public_url]) -Open invoice -@endcomponent - -If you intentionally split the payment or have questions, just reply to this email and we’ll help square it up. - -Thank you! -@endcomponent diff --git a/resources/views/mail/invoice-partial-warning-issuer.blade.php b/resources/views/mail/invoice-partial-warning-issuer.blade.php deleted file mode 100644 index 1b687bfe..00000000 --- a/resources/views/mail/invoice-partial-warning-issuer.blade.php +++ /dev/null @@ -1,25 +0,0 @@ -@php - $summary = $invoice->paymentSummary(\App\Services\BtcRate::current()); - $outstandingUsd = $summary['outstanding_usd'] ?? 0; - $outstandingBtc = $summary['outstanding_btc_formatted'] ?? null; -@endphp - -@component('mail::message', ['invoice' => $invoice]) -# Heads up: Invoice {{ $invoice->number ?? $invoice->id }} received multiple payments - -We just emailed the client reminding them that splitting payments adds miner fees and can slow settlement. This usually happens when they try to “top up” the invoice across several transactions. - -@component('mail::panel') -**Outstanding balance:** ${{ number_format($outstandingUsd, 2) }} USD -**BTC equivalent:** {{ $outstandingBtc ?? '—' }} BTC -**Client:** {{ $invoice->client->name ?? 'N/A' }} -@endcomponent - -@component('mail::button', ['url' => route('invoices.show', $invoice)]) -View invoice -@endcomponent - -No action is required unless you’d like to follow up with the client or confirm whether the split payment was intentional. - -Thanks! -@endcomponent diff --git a/tests/Feature/InvoicePaymentCorrectionTest.php b/tests/Feature/InvoicePaymentCorrectionTest.php index 29d51d68..dab269c6 100644 --- a/tests/Feature/InvoicePaymentCorrectionTest.php +++ b/tests/Feature/InvoicePaymentCorrectionTest.php @@ -133,7 +133,7 @@ public function test_owner_can_ignore_payment_reopen_invoice_and_hide_ignored_ro ->once(); } - public function test_owner_can_restore_ignored_payment_and_skip_stale_underpay_and_partial_deliveries(): void + public function test_owner_can_restore_ignored_payment_and_skip_stale_underpay_and_overpay_deliveries(): void { Carbon::setTestNow(Carbon::parse('2025-01-11 09:00:00', 'UTC')); Log::spy(); @@ -201,7 +201,7 @@ public function test_owner_can_restore_ignored_payment_and_skip_stale_underpay_a $invoice->deliveries()->create([ 'invoice_id' => $invoice->id, 'user_id' => $owner->id, - 'type' => 'client_partial_warning', + 'type' => 'client_overpay_alert', 'status' => 'queued', 'recipient' => $client->email, 'dispatched_at' => now(), @@ -209,7 +209,7 @@ public function test_owner_can_restore_ignored_payment_and_skip_stale_underpay_a $invoice->deliveries()->create([ 'invoice_id' => $invoice->id, 'user_id' => $owner->id, - 'type' => 'issuer_partial_warning', + 'type' => 'issuer_overpay_alert', 'status' => 'queued', 'recipient' => $owner->email, 'dispatched_at' => now(), diff --git a/tests/Feature/Wallet/WatchPaymentsCommandTest.php b/tests/Feature/Wallet/WatchPaymentsCommandTest.php index e695160b..75ff85c8 100644 --- a/tests/Feature/Wallet/WatchPaymentsCommandTest.php +++ b/tests/Feature/Wallet/WatchPaymentsCommandTest.php @@ -522,124 +522,6 @@ public function test_command_records_multiple_partial_transactions(): void ]); } - public function test_repeated_partial_payment_detection_no_longer_creates_a_separate_partial_warning_family(): void - { - Carbon::setTestNow(Carbon::parse('2025-01-05 08:00:00', 'UTC')); - $invoice = $this->makeInvoice(); - - $base = config('blockchain.mempool.testnet_base'); - - Cache::put(BtcRate::CACHE_KEY, [ - 'rate_usd' => 48_000, - 'as_of' => Carbon::now(), - 'source' => 'test', - ], BtcRate::TTL); - - Http::fake([ - "{$base}/address/{$invoice->payment_address}/txs" => Http::response([ - [ - 'txid' => 'warn-aaa', - 'status' => [ - 'confirmed' => false, - 'block_height' => null, - 'block_time' => null, - ], - 'vout' => [ - [ - 'scriptpubkey_address' => $invoice->payment_address, - 'value' => 300_000, - ], - ], - ], - [ - 'txid' => 'warn-bbb', - 'status' => [ - 'confirmed' => false, - 'block_height' => null, - 'block_time' => null, - ], - 'vout' => [ - [ - 'scriptpubkey_address' => $invoice->payment_address, - 'value' => 200_000, - ], - ], - ], - ], 200), - ]); - - $this->artisan('wallet:watch-payments')->assertExitCode(0); - - $invoice->refresh(); - $this->assertSame('pending', $invoice->status); - $this->assertNull($invoice->last_partial_warning_sent_at); - - $this->assertDatabaseMissing('invoice_deliveries', [ - 'invoice_id' => $invoice->id, - 'type' => 'client_partial_warning', - ]); - - $this->assertDatabaseMissing('invoice_deliveries', [ - 'invoice_id' => $invoice->id, - 'type' => 'issuer_partial_warning', - ]); - } - - public function test_repeated_partial_payment_detection_does_not_enqueue_legacy_partial_warnings_on_repeat_runs(): void - { - Carbon::setTestNow(Carbon::parse('2025-01-06 08:00:00', 'UTC')); - $invoice = $this->makeInvoice(); - - $base = config('blockchain.mempool.testnet_base'); - - Cache::put(BtcRate::CACHE_KEY, [ - 'rate_usd' => 46_000, - 'as_of' => Carbon::now(), - 'source' => 'test', - ], BtcRate::TTL); - - $payload = [ - [ - 'txid' => 'warn-ccc', - 'status' => [ - 'confirmed' => false, - 'block_height' => null, - 'block_time' => null, - ], - 'vout' => [ - [ - 'scriptpubkey_address' => $invoice->payment_address, - 'value' => 350_000, - ], - ], - ], - [ - 'txid' => 'warn-ddd', - 'status' => [ - 'confirmed' => false, - 'block_height' => null, - 'block_time' => null, - ], - 'vout' => [ - [ - 'scriptpubkey_address' => $invoice->payment_address, - 'value' => 350_000, - ], - ], - ], - ]; - - Http::fake([ - "{$base}/address/{$invoice->payment_address}/txs" => Http::response($payload, 200), - ]); - - $this->artisan('wallet:watch-payments')->assertExitCode(0); - $this->artisan('wallet:watch-payments')->assertExitCode(0); - - $this->assertEquals(0, \App\Models\InvoiceDelivery::where('invoice_id', $invoice->id)->where('type', 'client_partial_warning')->count()); - $this->assertEquals(0, \App\Models\InvoiceDelivery::where('invoice_id', $invoice->id)->where('type', 'issuer_partial_warning')->count()); - } - public function test_watcher_keeps_payment_acknowledgment_deduped_per_txid_but_allows_a_second_txid(): void { Queue::fake(); From f3fc90614bdad8161d27f9f3afc4454a3e9ea385 Mon Sep 17 00:00:00 2001 From: Nate Barlow Date: Sat, 23 May 2026 15:29:40 -0600 Subject: [PATCH 09/11] =?UTF-8?q?M19.1=20=C2=A75.10:=20resolve=20#86=20voi?= =?UTF-8?q?d-gate;=20close=20=C2=A75.11;=20queue=20#87=20in=20=C2=A75.12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §5.10 walk surfaced one finding: InvoiceAlertService::sendPastDueAlerts correctly early-returns on `paid` or `void` status, but checkPaymentThresholds did not — voiding an invoice that was still in `partial` state and then triggering any caller (payment sync, correction action, manual service call) would queue and send underpay/overpay alerts to live recipients on a defunct invoice. Fix: mirror the existing past-due gate in checkPaymentThresholds. Gate only on `void` — paid is still allowed since overpay alerts legitimately fire post-paid when overpaymentPercent exceeds the threshold. Verified: voided invoice #100 (which had previously leaked the alerts) now correctly produces zero new rows on a re-fired checkPaymentThresholds call. Full suite 335/335. Also: - §5.11 (mail-client rendering spot-check) signed off. - §5.12 added to track #87 (settlement mails — client receipt + issuer paid notice — drop on-chain evidence for multi-payment invoices). Doc-only addition; the ship lands in a separate commit. Fixes #86 Co-Authored-By: Claude Opus 4.7 --- app/Services/InvoiceAlertService.php | 4 ++++ .../19.1_NOTIFICATION_COVERAGE_AUDIT.md | 23 +++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/Services/InvoiceAlertService.php b/app/Services/InvoiceAlertService.php index 95f18464..ca0a8087 100644 --- a/app/Services/InvoiceAlertService.php +++ b/app/Services/InvoiceAlertService.php @@ -64,6 +64,10 @@ public function sendDetectedPaymentAcknowledgments(Invoice $invoice, InvoicePaym public function checkPaymentThresholds(Invoice $invoice): void { + if ($invoice->status === 'void') { + return; + } + if ($invoice->requiresClientOverpayAlert()) { $this->maybeSendOverpayAlert($invoice); } diff --git a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md index 4ac7a236..1cbfc928 100644 --- a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md +++ b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md @@ -169,14 +169,27 @@ This one matters most — Nate flagged late-payment notifications specifically a ### 10. Edge cases -1. [ ] [Agent] Void an invoice after send; verify no further mail fires for that invoice (no past-due, no underpay, no overpay). -2. [ ] [Agent] Create two invoices with the same client recipient address; verify each invoice's mails route correctly and no cross-contamination of delivery-log entries. -3. [ ] [Agent] Manual-resend `InvoicePaidReceiptMail` within the resend cooldown window; verify the resend is blocked with no new delivery row. -4. [ ] [Agent] Wait past the cooldown; manual-resend; verify a new `resend_*` context-keyed delivery row is created and the mail lands. +1. [x] [Agent] Void an invoice after send; verify no further mail fires for that invoice (no past-due, no underpay, no overpay). (Voided invoice #100. `sendPastDueAlerts` correctly returned without queuing. `checkPaymentThresholds` did NOT gate on void — queued + sent `client_underpay_alert` #72625 + `issuer_underpay_alert` #72626 to live recipients.) + 1. [x] [Agent] Ship [#86](https://github.com/n8bar/CryptoZing/issues/86): add `if ($invoice->status === 'void') return;` gate at the top of `InvoiceAlertService::checkPaymentThresholds`. Mirror the existing gate in `sendPastDueAlerts`. Closes #86 on PR merge via `Fixes #86`. + 1. [x] [Agent] Run `./vendor/bin/sail artisan test` — full suite passes. (335 passed / 1639 assertions.) + 2. [x] [Agent] Re-fire the void test from item 1 against invoice #100 (already void); confirm `checkPaymentThresholds` queues zero new rows. (Row count held at 8.) +2. [x] [Agent] Create two invoices with the same client recipient address; verify each invoice's mails route correctly and no cross-contamination of delivery-log entries. (Invoices #97/#98/#99/#100 all share `client-test@nospam.site` as the client; 19 delivery rows across them, all properly scoped via `invoice_id`, none leak between invoices.) +3. [x] [Agent] Manual-resend `InvoicePaidReceiptMail` within the resend cooldown window; verify the resend is blocked with no new delivery row. (Second `queueResend` call against invoice #97 within 60-min cooldown of the just-created resend row returned null; no new row written.) +4. [x] [Agent] Wait past the cooldown; manual-resend; verify a new `resend_*` context-keyed delivery row is created and the mail lands. (Invoice #97's original receipt (#72609) was sent 2026-05-22 — well past the 60-min cooldown. `queueResend` returned delivery #72627 with `context_key=resend_`.) ### 11. Mail-client rendering spot-check -1. [ ] [User] For at least one received mail per audience class (client-facing receipt, issuer-facing alert), open in two distinct mail clients (e.g., a PC web client + an Android mail app). Note any rendering breakage — broken images, broken layout, broken links, blocked-content prompts. +1. [x] [User] For at least one received mail per audience class (client-facing receipt, issuer-facing alert), open in two distinct mail clients (e.g., a PC web client + an Android mail app). Note any rendering breakage — broken images, broken layout, broken links, blocked-content prompts. + +### 12. Settlement mail txid completeness ([#87](https://github.com/n8bar/CryptoZing/issues/87)) + +Settlement mails are accounting documents — they should list every confirmed on-chain payment that supports the "paid" claim. The client receipt currently shows only `$invoice->txid` (the latest confirmed payment), and the issuer paid notice shows no txid at all. Multi-partial-payment invoices lose the full evidence trail in both mails. + +1. [ ] [Agent] Ship [#87](https://github.com/n8bar/CryptoZing/issues/87): iterate `$invoice->activeOnChainPayments()` confirmed entries and render a per-payment list (txid + sats + fiat_amount) in both `resources/views/mail/invoice-paid.blade.php` (client receipt) and `resources/views/mail/invoice-issuer-paid.blade.php` (issuer paid notice). Use the locked-at-detection-time `fiat_amount` per payment (per `docs/specs/PARTIAL_PAYMENTS.md:73-74`). + 1. [ ] [Agent] Feature test: create an invoice with 2+ confirmed payments contributing to a paid settlement; assert both rendered templates contain every payment's txid + fiat_amount. Single-payment case still renders cleanly. + 2. [ ] [Agent] Run `./vendor/bin/sail artisan test` — full suite passes. + 3. [ ] [Agent] Re-fire both mails against a multi-payment invoice; verify the rendered list reads as an accounting record. + 4. [ ] [User] Eyeball QA: both mails read as honest accounting records with the full settlement evidence. ## 6. Stress-readiness check (best-effort) From 49e7dbbf4b690488b0207fb4da595abab377b3d6 Mon Sep 17 00:00:00 2001 From: Nate Barlow Date: Sat, 23 May 2026 16:59:06 -0600 Subject: [PATCH 10/11] =?UTF-8?q?M19.1=20=C2=A75.12:=20resolve=20#87=20set?= =?UTF-8?q?tlement-mail=20txid=20completeness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settlement mails are accounting documents — they should list every confirmed on-chain payment that supports the "paid" claim. Both fell short for multi-payment invoices: - Client receipt previously rendered only $invoice->txid (the latest confirmed payment per refreshPaymentLedger), silently dropping any earlier confirmed payments that contributed to settlement. - Issuer paid notice rendered no txid at all. Both Mailables now query confirmed, non-ignored, non-adjustment payments via the existing payments() relation and pass them to the view. Each payment renders with its locked-at-detection fiat_amount (per docs/specs/PARTIAL_PAYMENTS.md:73-74), sats received, and confirmation time (client side) — full on-chain evidence in one place. Single-payment invoices render one entry without awkward "1 of 1" framing; multi-payment renders the list with an "across N on-chain payments" anchor in the receipt opener. New tests in tests/Feature/SettlementMailEvidenceTest.php verify both Mailables across multi-payment + single-payment scenarios. Full suite green: 338 passed / 1662 assertions. Fixes #87 Co-Authored-By: Claude Opus 4.7 --- app/Mail/InvoiceIssuerPaidNoticeMail.php | 6 + app/Mail/InvoicePaidReceiptMail.php | 6 + .../19.1_NOTIFICATION_COVERAGE_AUDIT.md | 8 +- .../views/mail/invoice-issuer-paid.blade.php | 12 ++ resources/views/mail/invoice-paid.blade.php | 18 +- tests/Feature/SettlementMailEvidenceTest.php | 165 ++++++++++++++++++ 6 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 tests/Feature/SettlementMailEvidenceTest.php diff --git a/app/Mail/InvoiceIssuerPaidNoticeMail.php b/app/Mail/InvoiceIssuerPaidNoticeMail.php index d076db7d..2d63a195 100644 --- a/app/Mail/InvoiceIssuerPaidNoticeMail.php +++ b/app/Mail/InvoiceIssuerPaidNoticeMail.php @@ -30,6 +30,12 @@ public function content(): Content markdown: 'mail.invoice-issuer-paid', with: [ 'invoice' => $this->invoice, + 'settlementPayments' => $this->invoice->payments() + ->whereNotNull('confirmed_at') + ->whereNull('ignored_at') + ->where('is_adjustment', false) + ->orderBy('confirmed_at') + ->get(), ], ); } diff --git a/app/Mail/InvoicePaidReceiptMail.php b/app/Mail/InvoicePaidReceiptMail.php index 856ed35b..8f6f3b7d 100644 --- a/app/Mail/InvoicePaidReceiptMail.php +++ b/app/Mail/InvoicePaidReceiptMail.php @@ -41,6 +41,12 @@ public function content(): Content 'delivery' => $this->delivery, 'client' => $this->invoice->client, 'publicUrl' => $this->invoice->public_url, + 'settlementPayments' => $this->invoice->payments() + ->whereNotNull('confirmed_at') + ->whereNull('ignored_at') + ->where('is_adjustment', false) + ->orderBy('confirmed_at') + ->get(), ], ); } diff --git a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md index 1cbfc928..aaaa0187 100644 --- a/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md +++ b/docs/strategies/19.1_NOTIFICATION_COVERAGE_AUDIT.md @@ -185,10 +185,10 @@ This one matters most — Nate flagged late-payment notifications specifically a Settlement mails are accounting documents — they should list every confirmed on-chain payment that supports the "paid" claim. The client receipt currently shows only `$invoice->txid` (the latest confirmed payment), and the issuer paid notice shows no txid at all. Multi-partial-payment invoices lose the full evidence trail in both mails. -1. [ ] [Agent] Ship [#87](https://github.com/n8bar/CryptoZing/issues/87): iterate `$invoice->activeOnChainPayments()` confirmed entries and render a per-payment list (txid + sats + fiat_amount) in both `resources/views/mail/invoice-paid.blade.php` (client receipt) and `resources/views/mail/invoice-issuer-paid.blade.php` (issuer paid notice). Use the locked-at-detection-time `fiat_amount` per payment (per `docs/specs/PARTIAL_PAYMENTS.md:73-74`). - 1. [ ] [Agent] Feature test: create an invoice with 2+ confirmed payments contributing to a paid settlement; assert both rendered templates contain every payment's txid + fiat_amount. Single-payment case still renders cleanly. - 2. [ ] [Agent] Run `./vendor/bin/sail artisan test` — full suite passes. - 3. [ ] [Agent] Re-fire both mails against a multi-payment invoice; verify the rendered list reads as an accounting record. +1. [x] [Agent] Ship [#87](https://github.com/n8bar/CryptoZing/issues/87): iterate `$invoice->activeOnChainPayments()` confirmed entries and render a per-payment list (txid + sats + fiat_amount) in both `resources/views/mail/invoice-paid.blade.php` (client receipt) and `resources/views/mail/invoice-issuer-paid.blade.php` (issuer paid notice). Use the locked-at-detection-time `fiat_amount` per payment (per `docs/specs/PARTIAL_PAYMENTS.md:73-74`). Closes #87 on PR merge via `Fixes #87`. + 1. [x] [Agent] Feature test: create an invoice with 2+ confirmed payments contributing to a paid settlement; assert both rendered templates contain every payment's txid + fiat_amount. Single-payment case still renders cleanly. (New file `tests/Feature/SettlementMailEvidenceTest.php`; 3 tests, 23 assertions.) + 2. [x] [Agent] Run `./vendor/bin/sail artisan test` — full suite passes. (338 passed / 1662 assertions / 300s.) + 3. [x] [Agent] Re-fire both mails against a multi-payment invoice; verify the rendered list reads as an accounting record. (Created invoice #101 `INV-M191-S512-8d4b` with three confirmed payments at 60k sats / $30 each; live samples sent to `client-test@nospam.site` (receipt) and `issuer-test@nateTheProgrammer.com` (paid notice).) 4. [ ] [User] Eyeball QA: both mails read as honest accounting records with the full settlement evidence. ## 6. Stress-readiness check (best-effort) diff --git a/resources/views/mail/invoice-issuer-paid.blade.php b/resources/views/mail/invoice-issuer-paid.blade.php index 5ae17f90..664a65b3 100644 --- a/resources/views/mail/invoice-issuer-paid.blade.php +++ b/resources/views/mail/invoice-issuer-paid.blade.php @@ -1,3 +1,7 @@ +@php + $settlementPayments = $settlementPayments ?? collect(); +@endphp + @component('mail::message', ['invoice' => $invoice]) # Invoice {{ $invoice->number ?? $invoice->id }} paid @@ -7,6 +11,14 @@ - **Amount:** ${{ number_format($invoice->amount_usd ?? 0, 2) }} ({{ $invoice->amount_btc ?? '—' }} BTC) - **Paid at:** {{ optional($invoice->paid_at)->toDayDateTimeString() ?? now()->toDayDateTimeString() }} +@if ($settlementPayments->isNotEmpty()) +**On-chain settlement:** + +@foreach ($settlementPayments as $payment) +- {{ $payment->txid }} — {{ number_format($payment->sats_received) }} sats / ${{ number_format((float) $payment->fiat_amount, 2) }} +@endforeach +@endif + @component('mail::button', ['url' => route('invoices.show', $invoice)]) Review invoice @endcomponent diff --git a/resources/views/mail/invoice-paid.blade.php b/resources/views/mail/invoice-paid.blade.php index 320c9d0a..6792737b 100644 --- a/resources/views/mail/invoice-paid.blade.php +++ b/resources/views/mail/invoice-paid.blade.php @@ -1,16 +1,24 @@ +@php + $settlementPayments = $settlementPayments ?? collect(); + $multiplePayments = $settlementPayments->count() > 1; +@endphp + # Receipt for Invoice {{ $invoice->number ?? $invoice->id }} Hi {{ $client->name ?? 'there' }}, -Thanks for your payment. We detected funds for **${{ number_format($invoice->amount_usd, 2) }}** on {{ optional($invoice->payment_detected_at)->toDayDateTimeString() ?? 'N/A' }}. +Your payment is confirmed. **${{ number_format((float) $invoice->amount_usd, 2) }} USD** received{{ $multiplePayments ? ' across ' . $settlementPayments->count() . ' on-chain payments' : '' }}. +@if ($settlementPayments->isNotEmpty()) -**Amount received:** {{ $invoice->payment_amount_formatted ?? '—' }} BTC -**USD total:** ${{ number_format((float) $invoice->amount_usd, 2) }} -**TXID:** {{ $invoice->txid ?? '—' }} -**Confirmations:** {{ $invoice->payment_confirmations ?? '0' }} +@foreach ($settlementPayments as $payment) +**TXID:** {{ $payment->txid }} +{{ number_format($payment->sats_received) }} sats / ${{ number_format((float) $payment->fiat_amount, 2) }} (confirmed {{ optional($payment->confirmed_at)->toDayDateTimeString() }}) + +@endforeach +@endif @if ($publicUrl) diff --git a/tests/Feature/SettlementMailEvidenceTest.php b/tests/Feature/SettlementMailEvidenceTest.php new file mode 100644 index 00000000..044157cd --- /dev/null +++ b/tests/Feature/SettlementMailEvidenceTest.php @@ -0,0 +1,165 @@ +makeMultiPaymentPaidInvoice(); + + $html = (new InvoicePaidReceiptMail($invoice->fresh(['client', 'user', 'payments']), $delivery))->render(); + + foreach ($payments as $payment) { + $this->assertStringContainsString($payment->txid, $html, "Receipt missing txid {$payment->txid}"); + $this->assertStringContainsString('$' . number_format((float) $payment->fiat_amount, 2), $html); + $this->assertStringContainsString(number_format($payment->sats_received) . ' sats', $html); + } + + $this->assertStringContainsString('across 3 on-chain payments', $html); + } + + public function test_issuer_paid_notice_renders_all_confirmed_txids_for_multi_payment_invoice(): void + { + [$invoice, $delivery, $payments] = $this->makeMultiPaymentPaidInvoice(); + + $html = (new InvoiceIssuerPaidNoticeMail($invoice->fresh(['client', 'user', 'payments']), $delivery))->render(); + + $this->assertStringContainsString('On-chain settlement:', $html); + foreach ($payments as $payment) { + $this->assertStringContainsString($payment->txid, $html, "Paid notice missing txid {$payment->txid}"); + $this->assertStringContainsString('$' . number_format((float) $payment->fiat_amount, 2), $html); + $this->assertStringContainsString(number_format($payment->sats_received) . ' sats', $html); + } + } + + public function test_single_payment_paid_invoice_still_renders_one_txid_cleanly(): void + { + [$invoice, $delivery, $payment] = $this->makeSinglePaymentPaidInvoice(); + + $receiptHtml = (new InvoicePaidReceiptMail($invoice->fresh(['client', 'user', 'payments']), $delivery))->render(); + $issuerHtml = (new InvoiceIssuerPaidNoticeMail($invoice->fresh(['client', 'user', 'payments']), $delivery))->render(); + + foreach ([$receiptHtml, $issuerHtml] as $html) { + $this->assertStringContainsString($payment->txid, $html); + } + + $this->assertStringNotContainsString('across 1 on-chain payments', $receiptHtml); + } + + private function makeMultiPaymentPaidInvoice(): array + { + $owner = User::factory()->create([ + 'email' => 'multi-pay-owner@example.com', + 'name' => 'Multi Owner', + ]); + + $client = Client::create([ + 'user_id' => $owner->id, + 'name' => 'Multi Pay Client', + 'email' => 'multi-pay-client@example.com', + ]); + + $invoice = Invoice::create([ + 'user_id' => $owner->id, + 'client_id' => $client->id, + 'number' => 'INV-MULTI-PAY', + 'amount_usd' => 90, + 'btc_rate' => 50_000, + 'amount_btc' => 0.0018, + 'payment_address' => 'tb1qq0multipayinvoice', + 'status' => 'paid', + 'invoice_date' => Carbon::now()->toDateString(), + ]); + $invoice->enablePublicShare(); + + $payments = collect(); + foreach (range(1, 3) as $i) { + $payments->push(InvoicePayment::create([ + 'invoice_id' => $invoice->id, + 'accounting_invoice_id' => $invoice->id, + 'txid' => str_pad('mp' . $i, 64, '0', STR_PAD_RIGHT), + 'sats_received' => 60_000, + 'detected_at' => Carbon::now()->subMinutes(10 - $i), + 'confirmed_at' => Carbon::now()->subMinutes(9 - $i), + 'usd_rate' => 50_000, + 'fiat_amount' => 30.00, + ])); + } + + $delivery = InvoiceDelivery::create([ + 'invoice_id' => $invoice->id, + 'user_id' => $owner->id, + 'type' => 'receipt', + 'status' => 'sent', + 'recipient' => $client->email, + 'dispatched_at' => Carbon::now(), + 'sent_at' => Carbon::now(), + ]); + + return [$invoice, $delivery, $payments]; + } + + private function makeSinglePaymentPaidInvoice(): array + { + $owner = User::factory()->create([ + 'email' => 'single-pay-owner@example.com', + 'name' => 'Single Owner', + ]); + + $client = Client::create([ + 'user_id' => $owner->id, + 'name' => 'Single Pay Client', + 'email' => 'single-pay-client@example.com', + ]); + + $invoice = Invoice::create([ + 'user_id' => $owner->id, + 'client_id' => $client->id, + 'number' => 'INV-SINGLE-PAY', + 'amount_usd' => 50, + 'btc_rate' => 50_000, + 'amount_btc' => 0.001, + 'payment_address' => 'tb1qq0singlepayinvoice', + 'status' => 'paid', + 'invoice_date' => Carbon::now()->toDateString(), + ]); + $invoice->enablePublicShare(); + + $payment = InvoicePayment::create([ + 'invoice_id' => $invoice->id, + 'accounting_invoice_id' => $invoice->id, + 'txid' => str_pad('sp1', 64, '0', STR_PAD_RIGHT), + 'sats_received' => 100_000, + 'detected_at' => Carbon::now()->subMinute(), + 'confirmed_at' => Carbon::now(), + 'usd_rate' => 50_000, + 'fiat_amount' => 50.00, + ]); + + $delivery = InvoiceDelivery::create([ + 'invoice_id' => $invoice->id, + 'user_id' => $owner->id, + 'type' => 'receipt', + 'status' => 'sent', + 'recipient' => $client->email, + 'dispatched_at' => Carbon::now(), + 'sent_at' => Carbon::now(), + ]); + + return [$invoice, $delivery, $payment]; + } +} From fa884ecce190f3cc0bc310a5b8655d062f6f1d73 Mon Sep 17 00:00:00 2001 From: Nate Barlow Date: Sat, 30 May 2026 18:39:47 -0600 Subject: [PATCH 11/11] M19.1: drop stale "blocked on Rachel" framing from MS19 doc MS18 video footage is captured and MS18 target is extended to 2026-06-07 (see PR #89). MS19 doc no longer needs the parenthetical flagging MS18 as Rachel-blocked. Co-Authored-By: Claude Opus 4.7 --- docs/milestones/19_RC_HARDENING_OPS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/milestones/19_RC_HARDENING_OPS.md b/docs/milestones/19_RC_HARDENING_OPS.md index b2c6de00..c9aa2c10 100644 --- a/docs/milestones/19_RC_HARDENING_OPS.md +++ b/docs/milestones/19_RC_HARDENING_OPS.md @@ -1,6 +1,6 @@ # MS19 - RC Hardening & Ops -Status: Active — running in parallel with MS18 (no hard dependencies; MS18 is blocked on Rachel's video through the 2026-05-31 hard cap). Phase 3 (LLC) and Phase 5 (Legal Layer) run as independent parallel tracks. Phase 8 (2FA) is positionally last by design. +Status: Active — running in parallel with MS18 (no hard dependencies). Phase 3 (LLC) and Phase 5 (Legal Layer) run as independent parallel tracks. Phase 8 (2FA) is positionally last by design. Parent execution doc: [`docs/PLAN.md`](../PLAN.md) Supporting ops doc: [`docs/ops/DOCS_DX.md`](../ops/DOCS_DX.md)