The /sponsor page (learn.spikersoft.com/sponsor) has three issues:
Wasted width / only 3 columns..profiles-section is capped at max-width: 1200px and .profiles-grid maxes at repeat(3, 1fr), so laptop/desktop screens leave large empty margins. Family and solo cards render in two separate stacked grids, not one row.
Donate-button clutter on family cards. A 4-member family shows ~5 donate-ish CTAs (one per member row + spotlight + "Sponsor this family"). We want one "Sponsor / Donate" button per card, with the per-member donate hidden until a member is clicked (the member spotlight already has it).
"Sponsor family" isn't a real donation. Today every family "Donate" is just a routerLink to an individual's /sponsor/:profileId checkout — there is no family-scoped donation, no split, and no way to keep a gift made to the family from being spent on one member's solo trip.
Solution
Layout: one unified full-width 4-column grid; family cards span 2 columns, individual cards span 1.
Declutter: one "Sponsor / Donate family" button per card; individual-member donation lives in the member spotlight.
Family shared-trip fund (the real feature): a donation to a family is pooled into a family fund (keyed by familyKey, mirroring the LocationFund subsystem — atomic guarded debit + append-only ledger). Staff disburse from the pool into members' trip funds, but only earmarked to a destination the whole family shares (SharedDestinationCodes), never a member's solo/individual destination. Donations are always allowed; if a family has no shared destination yet, the money waits in the pool.
Key architectural fact: the backend has no per-traveler wallet and no spend step — totalRaisedCents is a computed sum of completed Donation rows attributed by RecipientProfileId + earmarked via TargetCountry. So the anti-abuse guard is enforced at attribution time: a family disbursement's TargetCountry must be a shared destination.
Locked decisions
Held family pool + staff-controlled disbursement (mirror LocationFund).
Strict shared-only spend rule; donations always allowed (money waits in pool if no shared destination yet).
## Problem
The `/sponsor` page (learn.spikersoft.com/sponsor) has three issues:
1. **Wasted width / only 3 columns.** `.profiles-section` is capped at `max-width: 1200px` and `.profiles-grid` maxes at `repeat(3, 1fr)`, so laptop/desktop screens leave large empty margins. Family and solo cards render in two separate stacked grids, not one row.
2. **Donate-button clutter on family cards.** A 4-member family shows ~5 donate-ish CTAs (one per member row + spotlight + "Sponsor this family"). We want **one** "Sponsor / Donate" button per card, with the per-member donate hidden until a member is clicked (the member spotlight already has it).
3. **"Sponsor family" isn't a real donation.** Today every family "Donate" is just a routerLink to an individual's `/sponsor/:profileId` checkout — there is no family-scoped donation, no split, and no way to keep a gift made to the family from being spent on one member's **solo** trip.
## Solution
- **Layout:** one unified full-width 4-column grid; family cards span 2 columns, individual cards span 1.
- **Declutter:** one "Sponsor / Donate family" button per card; individual-member donation lives in the member spotlight.
- **Family shared-trip fund (the real feature):** a donation to a family is pooled into a **family fund** (keyed by `familyKey`, mirroring the `LocationFund` subsystem — atomic guarded debit + append-only ledger). Staff disburse from the pool into members' trip funds, but **only earmarked to a destination the whole family shares** (`SharedDestinationCodes`), never a member's solo/individual destination. Donations are always allowed; if a family has no shared destination yet, the money waits in the pool.
**Key architectural fact:** the backend has no per-traveler wallet and no spend step — `totalRaisedCents` is a computed sum of completed `Donation` rows attributed by `RecipientProfileId` + earmarked via `TargetCountry`. So the anti-abuse guard is enforced at *attribution time*: a family disbursement's `TargetCountry` must be a shared destination.
## Locked decisions
- Held family pool + **staff-controlled** disbursement (mirror LocationFund).
- **Strict shared-only** spend rule; donations always allowed (money waits in pool if no shared destination yet).
## Sub-tasks
- Backend: `DonationType.Family`/`FamilyDisbursement`, `FamilyFund` + `FamilyFundTransaction` entities, `IFamilyFundLedger`, checkout branch, webhook credit, `FamilyFundsController` (staff disburse with shared-only guard, ledger, adjust).
- Angular: unified 4-col grid + declutter family card; domain models; `FamilyDonateComponent` + route; rewire the family button; admin disburse panel; i18n.
Cross-repo: spikersoft-backend + spikersoft-angular.
Both are draft/WIP pending the runtime end-to-end walk (checkout → Stripe webhook → pool credit → staff disburse) once the backend is deployed with test Stripe.
Design as locked with the requester: held family pool + staff-controlled disbursement, strict shared-only spend rule, donations always allowed (money waits in the pool if the family has no shared trip yet).
WIP PRs opened (land together):
- **Backend:** spikerj/spikersoft-backend#473 — `DonationType.Family`/`FamilyDisbursement`, `FamilyFund` + ledger, checkout branch, webhook credit, `FamilyFundsController` with the shared-only disburse guard. Unit tests green (`SpikerSoft.UnitTests.slnf` builds clean).
- **Angular:** spikerj/spikersoft-angular#564 — unified 4-col grid + family-card declutter, `FamilyDonateComponent` + route, admin "Family Funds" disburse tab, domain models + i18n. `nx build` of domain-sponsor / feature-sponsor / spikersoft (app) all green; lint clean.
Both are draft/WIP pending the runtime end-to-end walk (checkout → Stripe webhook → pool credit → staff disburse) once the backend is deployed with test Stripe.
Design as locked with the requester: **held family pool + staff-controlled disbursement**, **strict shared-only** spend rule, donations always allowed (money waits in the pool if the family has no shared trip yet).
Both PRs merged to master (2026-07-24): backend #473 and angular #564 (merge 2cad2c2d). Deploying master for the manual end-to-end walk (checkout → Stripe webhook → pool credit → staff disburse). Keeping this epic open until that runtime pass is done + the follow-ups (prune orphaned i18n keys, optional auto-disbursement rules).
Both PRs **merged to master** (2026-07-24): backend #473 and angular #564 (merge `2cad2c2d`). Deploying master for the manual end-to-end walk (checkout → Stripe webhook → pool credit → staff disburse). Keeping this epic open until that runtime pass is done + the follow-ups (prune orphaned i18n keys, optional auto-disbursement rules).
The strict shared-only rule is enforced exactly as the epic locked it — GUARD 1 rejects any TargetCountry not in SharedDestinationCodes, and the disbursement writes a completed FamilyDisbursement row earmarked to that shared destination so the existing raised-total math credits only the shared trip.
Live production check (backend image published and rolled):
GET https://api.spikersoft.com/api/sponsor/family-funds/f8fe93afc8cd25ff13d86a49 -> 200
{"fund":{"balanceCents":0,"totalDonatedCents":0,"totalDisbursedCents":0,"currency":"USD","isActive":true},
"sharedDestinationCodes":["US-LA"],"members":[Joseph S. (parent), Todd S. (child)],"recentActivity":[]}
Angular — merged to master, NOT deployed ⚠️
spikersoft-angular PRs #564, #566 (i18n orphan), #567 (test fix) are merged. Verified on master:
Layout: one unified grid — families and solos now flow through a single .profiles-grid; .profiles-section is max-width: min(100% - 4rem, 1760px) (was 1200px); columns 1 → 2 (640) → 3 (1024) → 4 (1440); app-sponsor-family-card { grid-column: span 2 } from 640px up. Matches "family spans 2, solo spans 1".
Declutter: the family card now has exactly one CTA — sponsor-family-donate-link → /sponsor/donate-family/:familyKey. The per-member donate (sponsor.family.donateFor) lives only inside .spotlight-actions, i.e. hidden until a member is clicked.
Family fund UI:FamilyDonateComponent + route, family-fund-management admin disburse panel wired into sponsorship-management, sponsor domain models, en/es i18n.
But production is still running a pre-#564 build. The deployed bundle main-VSL3ATV3.js on learn.spikersoft.com contains sponsor/family/:family, sponsor/fund/:code, sponsor/donate-org — and no sponsor/donate-family. Cause: e2e-anonymous has failed on every master run since the #564 merge (run 18112, 17:52Z), and publish is gated on it, so publish has been skipped on every master run since 17:47Z.
The e2e failure is a direct miss in #564 itself: the new route was added to routes.ts but never classified in e2e/support/route-manifest.ts, which also dipped the anonymous ratchet 29.5% → 29.2%. Filed as #828, fix up in spikersoft-angular PR #568 (classify as public + skipWalk, re-derive both baselines per the documented exception (2)).
Remaining to close this epic
Land PR #568 → master goes green → publish runs → frontend deploys.
Re-verify on learn.spikersoft.com: 4-column grid at ≥1440px, single donate CTA per family card, /sponsor/donate-family/f8fe93afc8cd25ff13d86a49 renders and checks out.
Sanity-check one real family donation end-to-end (pool credit on webhook) and one staff disbursement earmarked to US-LA (the only shared destination on prod today) — plus confirm the guard rejects a disbursement toward Todd's solo US-ID.
— Opus 5 Agent
**Status check — code complete on both sides, but the frontend has NOT reached production yet. Keeping this open.**
### Backend — merged AND live ✅
spikersoft-backend PR #473 (`feat/sponsor-family-fund` → master, `e7a244bc`) landed all of the epic's backend sub-tasks, verified on master:
- `DonationType.Family = 5` / `FamilyDisbursement = 6` (`SpikerSoft.Data/Mongos/Donation.cs`)
- `FamilyFund` + `FamilyFundTransaction` (`SpikerSoft.Data/Mongos/FamilyFund.cs`)
- `IFamilyFundLedger` / `FamilyFundLedger` with atomic guarded debit + append-only ledger (`SpikerSoft.Business/Domain/Funds/Services/`)
- checkout branch + idempotent webhook credit (`StripeService`, `SponsorController`)
- `FamilyFundsController` (`api/sponsor/family-funds`): public summary, staff `disburse` (shared-only + membership + child-approval guards), `ledger`, `adjust`
- `SponsorFamilyBuilder.ResolveParentKeycloakId`
The strict shared-only rule is enforced exactly as the epic locked it — GUARD 1 rejects any `TargetCountry` not in `SharedDestinationCodes`, and the disbursement writes a completed `FamilyDisbursement` row earmarked to that shared destination so the existing raised-total math credits only the shared trip.
Live production check (backend image published and rolled):
```
GET https://api.spikersoft.com/api/sponsor/family-funds/f8fe93afc8cd25ff13d86a49 -> 200
{"fund":{"balanceCents":0,"totalDonatedCents":0,"totalDisbursedCents":0,"currency":"USD","isActive":true},
"sharedDestinationCodes":["US-LA"],"members":[Joseph S. (parent), Todd S. (child)],"recentActivity":[]}
```
### Angular — merged to master, NOT deployed ⚠️
spikersoft-angular PRs #564, #566 (i18n orphan), #567 (test fix) are merged. Verified on master:
- **Layout:** one unified grid — families and solos now flow through a single `.profiles-grid`; `.profiles-section` is `max-width: min(100% - 4rem, 1760px)` (was 1200px); columns 1 → 2 (640) → 3 (1024) → **4 (1440)**; `app-sponsor-family-card { grid-column: span 2 }` from 640px up. Matches "family spans 2, solo spans 1".
- **Declutter:** the family card now has exactly one CTA — `sponsor-family-donate-link` → `/sponsor/donate-family/:familyKey`. The per-member donate (`sponsor.family.donateFor`) lives only inside `.spotlight-actions`, i.e. hidden until a member is clicked.
- **Family fund UI:** `FamilyDonateComponent` + route, `family-fund-management` admin disburse panel wired into `sponsorship-management`, sponsor domain models, en/es i18n.
**But production is still running a pre-#564 build.** The deployed bundle `main-VSL3ATV3.js` on learn.spikersoft.com contains `sponsor/family/:family`, `sponsor/fund/:code`, `sponsor/donate-org` — and no `sponsor/donate-family`. Cause: `e2e-anonymous` has failed on every master run since the #564 merge (run 18112, 17:52Z), and `publish` is gated on it, so `publish` has been **skipped** on every master run since 17:47Z.
The e2e failure is a direct miss in #564 itself: the new route was added to `routes.ts` but never classified in `e2e/support/route-manifest.ts`, which also dipped the anonymous ratchet 29.5% → 29.2%. Filed as **#828**, fix up in **spikersoft-angular PR #568** (classify as public + `skipWalk`, re-derive both baselines per the documented exception (2)).
### Remaining to close this epic
1. Land PR #568 → master goes green → `publish` runs → frontend deploys.
2. Re-verify on learn.spikersoft.com: 4-column grid at ≥1440px, single donate CTA per family card, `/sponsor/donate-family/f8fe93afc8cd25ff13d86a49` renders and checks out.
3. Sanity-check one real family donation end-to-end (pool credit on webhook) and one staff disbursement earmarked to `US-LA` (the only shared destination on prod today) — plus confirm the guard rejects a disbursement toward Todd's solo `US-ID`.
— Opus 5 Agent
Shipped — closing. The deploy blocker (#828) is cleared and all three parts of this epic are now live in production.
Deploy confirmed
spikersoft-angular master run 18124 (dd34b0a4) is green end to end and publish ran successfully. learn.spikersoft.com now serves main-SRPMVC66.js (was main-VSL3ATV3.js), and the new bundle contains sponsor/donate-family/:family.
Live verification
Check
Result
GET /api/sponsor/family-funds/{familyKey} (public summary)
200 — pool + sharedDestinationCodes: ["US-LA"] + members
POST /api/sponsor/family-funds/admin/{familyKey}/disburse anonymous
401 — staff gate live
GET /api/sponsor/family-funds/admin/{familyKey}/ledger anonymous
401 — staff gate live
sponsor/donate-family/:familyKey in deployed bundle
present
Against the epic's three problems
Wasted width / 3 columns — fixed. Families and solos now flow through one shared .profiles-grid; .profiles-section is max-width: min(100% - 4rem, 1760px) (was 1200px); columns step 1 → 2 (640) → 3 (1024) → 4 (1440), with app-sponsor-family-card { grid-column: span 2 } from 640px up. Family cards span 2 tracks, solo cards 1, exactly as specified.
Donate-button clutter — fixed. One CTA per family card (sponsor-family-donate-link); the per-member donate (sponsor.family.donateFor) exists only inside .spotlight-actions, so it appears after a member is clicked.
"Sponsor family" isn't a real donation — fixed. DonationType.Family / FamilyDisbursement, FamilyFund + FamilyFundTransaction, IFamilyFundLedger / FamilyFundLedger (atomic guarded debit + append-only ledger on the raw driver), checkout branch, idempotent webhook credit, and FamilyFundsController with the locked decisions enforced: donations always allowed (money waits in the pool), disbursement rejected unless TargetCountry is in SharedDestinationCodes, plus membership and child-approval guards. The disbursement writes a completed FamilyDisbursement row earmarked to the shared destination, so the existing computed totalRaisedCents math credits the shared trip only — the attribution-time guard the epic called for.
Two acceptance steps need real money or staff credentials, so I did not exercise them:
A real family donation through Stripe checkout crediting the pool on webhook completion (unit-tested, not prod-exercised).
A staff disbursement in the admin panel — the happy path to US-LA, and the guard rejecting a disbursement toward Todd's solo US-ID. Prod currently has exactly one family (f8fe93afc8cd25ff13d86a49, spikerj + toddcan) with US-LA as the sole shared destination, so it is a clean test case for both.
Also note the pool balance is 0 and trip costs are still unset for this family, so the funding bar has nothing to show yet — that is data, not a code gap.
— Opus 5 Agent
**Shipped — closing.** The deploy blocker (#828) is cleared and all three parts of this epic are now live in production.
### Deploy confirmed
spikersoft-angular master run 18124 (`dd34b0a4`) is green end to end and `publish` ran successfully. learn.spikersoft.com now serves `main-SRPMVC66.js` (was `main-VSL3ATV3.js`), and the new bundle contains `sponsor/donate-family/:family`.
### Live verification
| Check | Result |
|---|---|
| `GET /api/sponsor/family-funds/{familyKey}` (public summary) | 200 — pool + `sharedDestinationCodes: ["US-LA"]` + members |
| `POST /api/sponsor/family-funds/admin/{familyKey}/disburse` anonymous | **401** — staff gate live |
| `GET /api/sponsor/family-funds/admin/{familyKey}/ledger` anonymous | **401** — staff gate live |
| `sponsor/donate-family/:familyKey` in deployed bundle | present |
### Against the epic's three problems
1. **Wasted width / 3 columns** — fixed. Families and solos now flow through one shared `.profiles-grid`; `.profiles-section` is `max-width: min(100% - 4rem, 1760px)` (was 1200px); columns step 1 → 2 (640) → 3 (1024) → 4 (1440), with `app-sponsor-family-card { grid-column: span 2 }` from 640px up. Family cards span 2 tracks, solo cards 1, exactly as specified.
2. **Donate-button clutter** — fixed. One CTA per family card (`sponsor-family-donate-link`); the per-member donate (`sponsor.family.donateFor`) exists only inside `.spotlight-actions`, so it appears after a member is clicked.
3. **"Sponsor family" isn't a real donation** — fixed. `DonationType.Family` / `FamilyDisbursement`, `FamilyFund` + `FamilyFundTransaction`, `IFamilyFundLedger` / `FamilyFundLedger` (atomic guarded debit + append-only ledger on the raw driver), checkout branch, idempotent webhook credit, and `FamilyFundsController` with the locked decisions enforced: donations always allowed (money waits in the pool), disbursement rejected unless `TargetCountry` is in `SharedDestinationCodes`, plus membership and child-approval guards. The disbursement writes a completed `FamilyDisbursement` row earmarked to the shared destination, so the existing computed `totalRaisedCents` math credits the shared trip only — the attribution-time guard the epic called for.
Both repos' sub-task lists are complete: backend PR #473; angular PRs #564, #566 (i18n orphan), #567 (test fix), #568 (#828 deploy unblock).
### Not verified by me — worth a human pass
Two acceptance steps need real money or staff credentials, so I did not exercise them:
1. A real family donation through Stripe checkout crediting the pool on webhook completion (unit-tested, not prod-exercised).
2. A staff disbursement in the admin panel — the happy path to `US-LA`, and the guard rejecting a disbursement toward Todd's solo `US-ID`. Prod currently has exactly one family (`f8fe93afc8cd25ff13d86a49`, spikerj + toddcan) with `US-LA` as the sole shared destination, so it is a clean test case for both.
Also note the pool balance is 0 and trip costs are still unset for this family, so the funding bar has nothing to show yet — that is data, not a code gap.
— Opus 5 Agent
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
The
/sponsorpage (learn.spikersoft.com/sponsor) has three issues:.profiles-sectionis capped atmax-width: 1200pxand.profiles-gridmaxes atrepeat(3, 1fr), so laptop/desktop screens leave large empty margins. Family and solo cards render in two separate stacked grids, not one row./sponsor/:profileIdcheckout — there is no family-scoped donation, no split, and no way to keep a gift made to the family from being spent on one member's solo trip.Solution
familyKey, mirroring theLocationFundsubsystem — atomic guarded debit + append-only ledger). Staff disburse from the pool into members' trip funds, but only earmarked to a destination the whole family shares (SharedDestinationCodes), never a member's solo/individual destination. Donations are always allowed; if a family has no shared destination yet, the money waits in the pool.Key architectural fact: the backend has no per-traveler wallet and no spend step —
totalRaisedCentsis a computed sum of completedDonationrows attributed byRecipientProfileId+ earmarked viaTargetCountry. So the anti-abuse guard is enforced at attribution time: a family disbursement'sTargetCountrymust be a shared destination.Locked decisions
Sub-tasks
DonationType.Family/FamilyDisbursement,FamilyFund+FamilyFundTransactionentities,IFamilyFundLedger, checkout branch, webhook credit,FamilyFundsController(staff disburse with shared-only guard, ledger, adjust).FamilyDonateComponent+ route; rewire the family button; admin disburse panel; i18n.Cross-repo: spikersoft-backend + spikersoft-angular.
WIP PRs opened (land together):
DonationType.Family/FamilyDisbursement,FamilyFund+ ledger, checkout branch, webhook credit,FamilyFundsControllerwith the shared-only disburse guard. Unit tests green (SpikerSoft.UnitTests.slnfbuilds clean).FamilyDonateComponent+ route, admin "Family Funds" disburse tab, domain models + i18n.nx buildof domain-sponsor / feature-sponsor / spikersoft (app) all green; lint clean.Both are draft/WIP pending the runtime end-to-end walk (checkout → Stripe webhook → pool credit → staff disburse) once the backend is deployed with test Stripe.
Design as locked with the requester: held family pool + staff-controlled disbursement, strict shared-only spend rule, donations always allowed (money waits in the pool if the family has no shared trip yet).
Both PRs merged to master (2026-07-24): backend #473 and angular #564 (merge
2cad2c2d). Deploying master for the manual end-to-end walk (checkout → Stripe webhook → pool credit → staff disburse). Keeping this epic open until that runtime pass is done + the follow-ups (prune orphaned i18n keys, optional auto-disbursement rules).Status check — code complete on both sides, but the frontend has NOT reached production yet. Keeping this open.
Backend — merged AND live ✅
spikersoft-backend PR #473 (
feat/sponsor-family-fund→ master,e7a244bc) landed all of the epic's backend sub-tasks, verified on master:DonationType.Family = 5/FamilyDisbursement = 6(SpikerSoft.Data/Mongos/Donation.cs)FamilyFund+FamilyFundTransaction(SpikerSoft.Data/Mongos/FamilyFund.cs)IFamilyFundLedger/FamilyFundLedgerwith atomic guarded debit + append-only ledger (SpikerSoft.Business/Domain/Funds/Services/)StripeService,SponsorController)FamilyFundsController(api/sponsor/family-funds): public summary, staffdisburse(shared-only + membership + child-approval guards),ledger,adjustSponsorFamilyBuilder.ResolveParentKeycloakIdThe strict shared-only rule is enforced exactly as the epic locked it — GUARD 1 rejects any
TargetCountrynot inSharedDestinationCodes, and the disbursement writes a completedFamilyDisbursementrow earmarked to that shared destination so the existing raised-total math credits only the shared trip.Live production check (backend image published and rolled):
Angular — merged to master, NOT deployed ⚠️
spikersoft-angular PRs #564, #566 (i18n orphan), #567 (test fix) are merged. Verified on master:
.profiles-grid;.profiles-sectionismax-width: min(100% - 4rem, 1760px)(was 1200px); columns 1 → 2 (640) → 3 (1024) → 4 (1440);app-sponsor-family-card { grid-column: span 2 }from 640px up. Matches "family spans 2, solo spans 1".sponsor-family-donate-link→/sponsor/donate-family/:familyKey. The per-member donate (sponsor.family.donateFor) lives only inside.spotlight-actions, i.e. hidden until a member is clicked.FamilyDonateComponent+ route,family-fund-managementadmin disburse panel wired intosponsorship-management, sponsor domain models, en/es i18n.But production is still running a pre-#564 build. The deployed bundle
main-VSL3ATV3.json learn.spikersoft.com containssponsor/family/:family,sponsor/fund/:code,sponsor/donate-org— and nosponsor/donate-family. Cause:e2e-anonymoushas failed on every master run since the #564 merge (run 18112, 17:52Z), andpublishis gated on it, sopublishhas been skipped on every master run since 17:47Z.The e2e failure is a direct miss in #564 itself: the new route was added to
routes.tsbut never classified ine2e/support/route-manifest.ts, which also dipped the anonymous ratchet 29.5% → 29.2%. Filed as #828, fix up in spikersoft-angular PR #568 (classify as public +skipWalk, re-derive both baselines per the documented exception (2)).Remaining to close this epic
publishruns → frontend deploys./sponsor/donate-family/f8fe93afc8cd25ff13d86a49renders and checks out.US-LA(the only shared destination on prod today) — plus confirm the guard rejects a disbursement toward Todd's soloUS-ID.— Opus 5 Agent
Shipped — closing. The deploy blocker (#828) is cleared and all three parts of this epic are now live in production.
Deploy confirmed
spikersoft-angular master run 18124 (
dd34b0a4) is green end to end andpublishran successfully. learn.spikersoft.com now servesmain-SRPMVC66.js(wasmain-VSL3ATV3.js), and the new bundle containssponsor/donate-family/:family.Live verification
GET /api/sponsor/family-funds/{familyKey}(public summary)sharedDestinationCodes: ["US-LA"]+ membersPOST /api/sponsor/family-funds/admin/{familyKey}/disburseanonymousGET /api/sponsor/family-funds/admin/{familyKey}/ledgeranonymoussponsor/donate-family/:familyKeyin deployed bundleAgainst the epic's three problems
.profiles-grid;.profiles-sectionismax-width: min(100% - 4rem, 1760px)(was 1200px); columns step 1 → 2 (640) → 3 (1024) → 4 (1440), withapp-sponsor-family-card { grid-column: span 2 }from 640px up. Family cards span 2 tracks, solo cards 1, exactly as specified.sponsor-family-donate-link); the per-member donate (sponsor.family.donateFor) exists only inside.spotlight-actions, so it appears after a member is clicked.DonationType.Family/FamilyDisbursement,FamilyFund+FamilyFundTransaction,IFamilyFundLedger/FamilyFundLedger(atomic guarded debit + append-only ledger on the raw driver), checkout branch, idempotent webhook credit, andFamilyFundsControllerwith the locked decisions enforced: donations always allowed (money waits in the pool), disbursement rejected unlessTargetCountryis inSharedDestinationCodes, plus membership and child-approval guards. The disbursement writes a completedFamilyDisbursementrow earmarked to the shared destination, so the existing computedtotalRaisedCentsmath credits the shared trip only — the attribution-time guard the epic called for.Both repos' sub-task lists are complete: backend PR #473; angular PRs #564, #566 (i18n orphan), #567 (test fix), #568 (#828 deploy unblock).
Not verified by me — worth a human pass
Two acceptance steps need real money or staff credentials, so I did not exercise them:
US-LA, and the guard rejecting a disbursement toward Todd's soloUS-ID. Prod currently has exactly one family (f8fe93afc8cd25ff13d86a49, spikerj + toddcan) withUS-LAas the sole shared destination, so it is a clean test case for both.Also note the pool balance is 0 and trip costs are still unset for this family, so the funding bar has nothing to show yet — that is data, not a code gap.
— Opus 5 Agent