← HMT — review hub

_system / plans · subplan companion to beneficiary-care-workflow · source: donor-and-finance-subplan.md

Donor Relationships & Financial Transparency — subplan

Research-level draft · 2026-05-29; trustee decisions logged 2026-05-30 · Areas 3 & 4.

Roadmap position: step 3 (Financial transparency) & step 4 (Donor relationships) per HMT-Foundation-Ops-design-v0. Built after the storytelling pilot lands and beneficiary-care is operationally settled.

Trustee decisions logged 2026-05-30 — this page summarises; the markdown (_system/plans/donor-and-finance-subplan.md) is the up-to-date design. Decisions taken: See §15 (open-questions resolutions) and §16 (full decisions log) below.

1 · Where this fits in the four-area model

The Foundation's operational design (HMT-Foundation-Ops-design-v0) names four areas. Area 1 (Beneficiary care) is code-complete and awaiting deploy; Area 2 (Storytelling & media) is in trial. This subplan covers the next two; per trustee decision §16.a (2026-05-30), the donor and finance passes build in parallel sharing F1 as the common substrate.

Roadmap step 3 · built first

Area 4 · Financial transparency

Honesty about money is a core value. The Foundation will publish monthly reports of what came in (cash + in-kind, anonymous or named per the donor's preference) and where it went (programmes, schools, specific children, operating costs). Internal practice runs cleanly for two or three months before any version reaches the public site.

Today: internal accounting; the practice exists, the publication does not. Built first so the donor system has something real to reference.

Roadmap step 4 · built second

Area 3 · Donor relationships

Every contribution — large or small — followed by a real thank-you, not a form letter. Donors who wish to stay close receive ongoing updates. The donation rail is live (VietQR via NCB); what is not yet built is the donor record, the per-donor code, the thank-you composition, and the opt-in update list.

Today: donation page live; thank-yous hand-written when remembered. Next: a simple donor record so we never lose track, and a personal thank-you for every gift.

Sponsorship (roadmap step 5) is substrate-only here. "Đồng hành cùng một Hành trình" (Option C from the Ops-v0 sketch) is opened only after Areas 3 + 4 are stable. This subplan builds the sponsorship_match table so the donor ↔ journey-NNNN bridge is unblocked, but the per-sponsor renderer and matching workflow are out of scope.

2 · What is already there, honestly

The donation rail itself works. The Foundation's NCB institutional account is the registered receiver, scanned by donors as a VietQR / NAPAS 247 transfer. Per Nghị định 93, this institutional account is the only legitimate receiver for a registered charity's contributions; the workspace never holds the credentials.

What is not yet built is everything after a transfer lands: parsing the bank statement, attaching the transfer to a donor, thanking the donor personally, reconciling the month, and publishing the monthly transparency report. That is this subplan's surface.

Honest operational read of today: the Foundation has not lost track of any contribution, but the record is in the bank statement plus the Account Custodian's notebook plus a few personal thank-you messages the Coordinator has sent. There is no single retrievable answer to "who has given, how much, when, and have we said thanks." Small enough to hold in someone's head today; big enough to drop something next quarter if it grows.

3 · The contribution — the shared atom

The load-bearing insight

One contribution is one fact, surfaced two ways.

A row in the donor's "what they have given" history is the same row as the finance side's "what came in this month." The data model below makes that explicit: one contribution table on the private side, projected up to the donor stream and the finance stream as views.

Supporter donor, partner, friend VietQR scan NCB · 100899899899 Foundation account monthly statement Bank statement NCB CSV export import + match contribution · con-2026-NNNNN one row · two views · the shared substrate donor_id · received_on · amount · intent · audit Donor stream — Area 3 thank-you · donor record · annual receipt private — never reaches the public Finance stream — Area 4 reconciliation · published report aggregate · opt-in named
One contribution row, two streams. Donor side stays private; finance side aggregates and publishes.

4 · The donor record — Block 1, 2, 3, 4

One markdown file per donor on the private Foundation Drive, named donor-D<NNNN>.md. The donor code is HMT-D<NNNN>; D<NNNN> in the filename for brevity. Monotonic, never reused. D0001D0099 are reserved for the Foundation's standing institutional donors named on initialisation; ordinary donors start at D0100.

BlockWhat's in itUsed by
1 — IdentityDisplay name (always real), public-anonymity flag, email, phone, preferred contact, postal address.Thank-you composer; annual receipt; published report (only when opt_in_public_recognition: true).
2 — Relationshipdonor_since, who introduced them, relationship notes, primary intent (programme slug or general).Thank-you composer (for warmth + context); Coordinator's prep for the next conversation.
3 — Consent observedopt_in_updates, opt_in_public_recognition, opt_in_annual_report, observed_at, observed_by, observed_method (verbal-confirmation / written-reply / inferred-no-objection).Send-list filter (D4); anonymisation linter (F5); receipt sender (F6). Silent-rule default is false for every opt-in until explicitly recorded.
4 — Contributions indexPointer to Lich-su-dong-gop/by-donor/D<NNNN>/ — the per-donor view of their contributions.Read-only index; never edited by hand. Rebuilt nightly from the canonical by-month/ store.
Naming the silent-rule default. Opt-in flags default to false when a donor's preference has not been recorded. A donor whose conversation has not happened yet gets no updates and no public recognition until the Coordinator notes it. This is the workspace-ethics.md §3 posture — the workspace observes and records, it does not adjudicate or assume.

5 · The contribution record — one file, one row, one audit chain

Each contribution is one file under by-month/<YYYY-MM>/con-YYYY-NNNNN.md; the per-donor view (by-donor/D<NNNN>/) is a nightly-generated read-only index. The frontmatter carries every field a reconciliation, a thank-you, and an annual receipt could need:

contribution_id: con-2026-00043
donor_id: HMT-D0142
received_on: 2026-03-14
channel: vietqr-bank
amount_vnd: 2000000
bank_txn_ref: "NCB-2026-03-14-7Y2X9"
donor_addinfo: "HMT-D0142 ung ho"
intent: cung-em-tien-buoc
thanked_at: 2026-03-15T10:24:00+07:00
thank_you_channel: email
thank_you_artifact: ../../Thu-cam-on/2026-03/D0142__con-2026-00043__cam-on.md
receipted_at: null
audit:
  recorded_by: "Cô Thuỷ"
  recorded_at: 2026-03-14T16:30:00+07:00
  source: bank-statement-import

In-kind contributions take the same shape: channel: inkind, amount_vnd: null, amount_inkind_summary: "300 bộ đồ dùng học tập".

Why one contribution = one file = one row. One file is the unit of audit. A trustee can ask "show me contribution N" and get exactly one file with the bank ref, the donor link, the thank-you trail, and the receipt trail. This is the discipline Decree 93 expects for a registered charity account; the workspace records it in a form that survives staff changes.

6 · In-kind ledger — separate from the financial statements

Updated 2026-05-30 (§16.2b). In-kind contributions are no longer rolled into the cash financial statements; they live in their own inkind_contribution table on the same SQLite, parallel to contribution. Cash and in-kind never sum on a published report. Disposal bridge stays; donor thank-you + per-handover PDF receipt still ship together. See the markdown §4.1 for the full design.

An in-kind contribution is not "spent" the moment it arrives; it is held in Foundation stock until it is delivered to a beneficiary as a benefit (Area 1). Honest transparency tracks which incoming contributions paid for which delivered benefits, including in-kind — otherwise "Anh Bình's 300 school-supply sets" and the 220 sets delivered to children this term are two unconnected facts on opposite sides of the ledger.

The inkind_disposal table records the bridge. One in-kind contribution can have many disposals (one per benefit it ultimately reaches); each benefit row that consumes in-kind stock points back via funding_source to the donor code that contributed it. The contribution.inkind_disposal_status field tracks the rollup (pendingpartialdisposed).

For cash contributions the bridge is implicit: benefit.funding_source directly references the donor code, and the monthly reconciliation sums them. No disposal row is written for cash.

7 · The daily pipeline — five steps, Coordinator-driven

Updated 2026-05-30 (§16.g + §16.e). Cadence changed from monthly to daily preferred, weekly fallback; the Account Custodian uploads end-of-day NCB statements, the Coordinator runs import + thank-you + per-contribution PDF receipt the same day. Filename is <YYYY-MM>/<YYYY-MM-DD>__ncb-statement.csv. Each thank-you ships with its per-contribution PDF receipt attached. SLA target: next-business-day donor turnaround. See markdown §7 for the five-step shape with the new cadence.

Same operational rhythm as Area-1 intake. Custodian holds the bank credentials; Coordinator runs everything downstream.

1 · Download Custodian exports NCB CSV 2 · Import draft matched / unknown / non-donation 3 · Resolve unknown → register or assign or anon 4 · Promote rows committed contribution table 5 · Thank-you draft + send Coordinator's voice Nothing reaches a donor without the Coordinator reading the contribution first.
The monthly pipeline. No automation between steps 2 and 5 — each is an explicit human action.

Step-by-step

1DownloadCustodian exports the month's statement from NCB web banking; places the CSV in 07_Quan-tri/Tai-chinh/Sao-ke-ngan-hang/<YYYY>/. Only step that touches bank credentials.

2Import draftCoordinator runs contribution_import_statement.py; produces a draft batch under 2026-03__draft-contributions/ with three buckets: matched/ (addInfo recognised a donor code), unknown-donor/ (no match), non-donation/ (bank fees, interest, transfers out). Nothing committed yet.

3Resolve unknowns — For each unknown-donor row, the Coordinator chooses: assign to existing donor; register a new donor (donor_register.py); attach to the anonymous bucket HMT-D0000; or reclassify as non-donation.

4Promotecontribution_promote.py writes each row as a con-YYYY-NNNNN.md file, inserts the SQL row, regenerates by-donor/ indexes. Idempotent.

5Personal thank-you — For each new contribution, the Coordinator runs thank_you_draft.py (Claude with a Coordinator-voice system prompt, distinct from the brand-voice corpus — this is interpersonal correspondence, not public-channel copy). Edits, sends from personal mail client. Phase A: no SMTP, no auto-send. thank_you_record.py stamps thanked_at after sending.

8 · The monthly cash report + side-by-side in-kind report

Updated 2026-05-30 (§16.b + §16.2b). Two changes:
  1. The cash monthly report (§8.1) is now cash only; the in-kind side has its own monthly report (§8.4 in the markdown). The published transparency package shows the two sections side-by-side, never summed.
  2. F5 (the first published transparency report) ships at confidence — when the Finance trustee + Editor sign off — not at a fixed N-months threshold.

The Area-4 deliverable. Built first so the workspace runs an internal version cleanly for two or three months before any version reaches the public site.

What's in the internal monthly report (F4)

SectionWhat it contains
Income summaryTotal VND in. By channel (VietQR / cash / in-kind / other). By donor type (named / anonymous). By intent (general / per programme / per sponsorship). Count of unique donors, count of new donors.
In-kind disposalsOpening stock, received this month, disposed (delivered as benefits), closing stock. Per-contribution disposal trace.
ExpenditureBenefits delivered (joined to Area-1) grouped by programme / category / petal / school / funding-source. Operating costs by category.
ReconciliationBank closing balance vs (opening + income − benefits paid in cash − operating costs paid in cash). Discrepancies surfaced as findings, never silently rounded.
8-petal coverageWhere funds went across the holistic-development model. Same anchors the storytelling pipeline uses.
Per-school totalsIncoming vs outgoing per partner school. Surfaces under-reported activity to the trustee read.
NarrativeClaude-drafted paragraph against the structured sections. Editor edits before any publication.

Audience filter (what each variant includes)

AudienceFull donor namesIn-kind detailOp-cost detailReconciliationNarrative tone
internalyesfullfullfullfactual
trusteeyessummaryfullsummaryfactual
published-namedonly opt-insummarycategory-onlyaggregatenarrative
published-anonnosummarycategory-onlyaggregatenarrative

The published-* variants feed F5 (the published transparency report), which ships as a post-bundle through the existing media-line approval chain. A new stream identifier D (for Đóng góp / financial) slots alongside the existing A / B / C in bundle_validate.py.

9 · The donor anonymisation membrane

The principle that does not move. A donor whose opt_in_public_recognition is false never appears by name in any published Foundation output. The membrane is enforced in code: finance_anon_check.py reads the published draft and the donor table and refuses any leakage. There is no Editor override on this gate — donor opt-in is the donor's decision, not the Editor's. This is the financial-side analogue of Area-1's journey_leakage_check.py.
PRIVATE — inside the Foundation WHAT THE PUBLIC REPORT SHOWS A donor's record Real name · email · phone · address Bank ref · transfer note · payer detail Per-donor totals · history · intent Donor → child link (sponsorship) Visible only to Coordinator, trustees, Editor. AGGREGATE + opt-in named The published report Total raised · count of supporters Where it went, by programme + petal "Special thanks to ___" (opt-in only) Story moments + Editor reflection No donor's name without their explicit opt-in.
The donor-side anonymisation membrane. Enforced by finance_anon_check.py — a blocking gate on the publish bundle.

10 · Per-donor VietQR — the donor side of the rail

The donation rail today is one static VietQR for the institutional account; donors scan and type whatever they like in the transfer note. A per-donor QR (HMT-D<NNNN> pre-filled in addInfo) makes the bank-import match deterministic and lets the thank-you flow auto-attach to the right donor on every return contribution.

py tools/qr_donor.py --donor D0142
py tools/qr_donor.py --donor D0142 --amount 2000000          # optional fixed amount
py tools/qr_donor.py --donor D0142 --output png|svg|both

The generator is offline: the NAPAS-247 QR is a CRC-protected string of (field_id, length, value) tuples per the EMV Co spec, ~120 LOC implemented directly to avoid a third-party dependency. The institutional account number is already public; no credentials involved. The chromed canvas output (5:4 landscape, matching the Stream-A photo chrome composite) is the Coordinator's send-to-the-donor artefact.

No payment-aggregator integration in Phase A. payOS, SePay and Casso would webhook the workspace on every incoming transfer and remove the bank-statement CSV step, shortening thank-you-latency to seconds. Deferred because Phase A volume does not justify the operational complexity, and routing the transaction stream through a third party needs a Foundation-side data-sharing decision (workspace-ethics.md §3). The aggregator path is a later-stage option, on the same shelf as Stage-2 API posting.

11 · Per-contribution PDF receipts (sent with each thank-you)

Updated 2026-05-30 (§16.e), replacing the annual-only model. Every cash contribution and every in-kind handover gets a designed per-contribution PDF receipt generated at promote-time and attached to the thank-you email sent within the next business day. The year-end aggregate becomes an optional roll-up on request (audit, donor preference). Cash and in-kind in two separate tables in the annual roll-up, never summed. Template lives at Knowledge/Brand/templates/receipt-per-contribution.html. See markdown §9 for the full design.

The Ops-v0 design parks tax-deductibility as "the Foundation is reviewing how contributions can be receipted." This subplan scopes the artefact; the legal posture (which receipts qualify, what Vietnamese tax law requires) stays Foundation-external per workspace-ethics.md §3.

For each donor with at least one contribution in the calendar year, contribution_receipt.py generates a per-donor PDF: Foundation header, donor identification, per-contribution rows (date, channel, amount or in-kind summary, intent, bank ref), totals, signatory line, and a Foundation-supplied tax-deductibility note. PDFs are written to 02_Quan-he-nha-tai-tro/Bien-nhan-nam/<YYYY>/D<NNNN>__<YYYY>.pdf; the Coordinator personally sends them (Phase A: no auto-send). Donors with opt_in_annual_report: false have their PDF generated and filed, never sent — useful for an auditor read, not spam-bait.

12 · Build sequence — finance & donor passes in parallel

Updated 2026-05-30 (§16.a). Passes now run in parallel sharing F1 as the substrate. New tasks: F-receipts (per-contribution PDF, replaces the old F6 annual receipt), F2-inkind (in-kind handover record), F4-inkind (in-kind monthly report), D1-form (donor input form + Apps Script + Cloud Run donor-input Service + returning-donor detector). Critical path: F1 → F2 → F-receipts → F4 → F5; donor pass runs alongside; D3 (thank-you) depends on F-receipts so thank-yous can ship the PDF together. F5 ships at confidence (§16.b), not at a fixed-month trigger. See markdown §11 for the full task list.

The Finance pass (F1, F2, F-receipts, F4, F5 — plus the new F2-inkind and F4-inkind for the in-kind split) and the Donor pass (D1, D1-form, D2, D3, D4, D5) build in parallel after F1. F-zero (the cash + in-kind reconciliation pilot) and D-zero (the thank-you + receipt pilot) are hand-rolled operational pilots that precede code, same crawl-walk-run discipline as Areas 1 and 2. The summarised timeline below is the pre-2026-05-30 draft for context only — the canonical task list lives in the markdown §11.

Show the original pre-2026-05-30 finance-first / donor-second draft timeline

Finance pass — built first (original draft)

F0
Pilot

Operational pilot — hand-roll one month

Before any code at scale. Confirms the practice survives before the schema is locked.

  • Export an NCB statement.
  • List every contribution by hand.
  • Match against the Area-1 benefit table for the same window.
  • Note operating costs.
  • Draft the internal monthly report in markdown.
Deliverable: Editor + Finance trustee + Coordinator have read one real reconciliation end-to-end. Findings feed F1's last-mile schema adjustments. Blocks F5.
F1
3–5 days

Schema + validators

Six tables on the private Foundation Drive.

  • tools/finance_registry_migrate.py — creates the six tables on 07_Quan-tri/Tai-chinh/so-tai-chinh.sqlite. Idempotent. Pytest against a temp file (no live Drive in tests).
  • tools/finance_validate.py — schema check on contribution rows + donor profiles (frontmatter shape, opt-in coherence, valid enums, donor-side funding_source ↔ donor-code cross-check).
  • Pydantic models for each table; schema fixtures + tests; 2 synthetic donors + 4 synthetic contributions in tests/fixtures/finance/.
F2
4–6 days

Bank-statement import

The CSV → draft batch → resolved batch → committed rows pipeline.

  • tools/contribution_import_statement.py — parses NCB CSV; produces draft batch + _review.md; classifies into matched / unknown-donor / non-donation. Schema-detection guard surfaces "unknown CSV shape" rather than silently miscategorising.
  • tools/contribution_promote.py — walks a resolved batch, writes con-YYYY-NNNNN.md files + inserts SQL rows, regenerates indexes. Idempotent.
  • tools/contribution_record.py — manual entry tool for cash + in-kind contributions (the path that does not go through bank statement).
F3
1–2 days

Operating-cost ledger

The outgoing-side analogue of Area-1's benefit_record.py.

  • tools/operating_cost_record.py — manual entry. Categories enum-validated (platform-fees | hosting | logistics | volunteer-reimbursement | stationery | other). Funding-source validated against the shared nguon-tai-tro.yaml.
F4
4–6 days

Internal monthly report

Joins Area-1 benefits + finance contributions + ops costs into the internal reconciliation.

  • tools/finance_report_monthly.py — generates the internal report. Joins Area-1 benefit rows by date window at the application layer (Python opens both SQLites; no SQL ATTACH). Audience filter internal.
  • Claude composes the narrative paragraph against the structured sections; Editor edits before any publication.
Deliverable: A signed-off internal monthly report at 07_Quan-tri/Tai-chinh/Bao-cao-noi-bo/<YYYY-MM>__bao-cao-noi-bo.md. Run for two or three months before F5.
F5
3–5 days

Published transparency report

Internal report → public-facing bundle through the existing media-line approval chain.

  • tools/finance_report_publish.py — produces a post-bundle scaffolded under Sandbox/posts/, traveling through the same QC chain as Stream-A (different content source). Audience filter published-named or published-anon.
  • tools/finance_anon_check.py — the anonymisation linter (blocking gate; no Editor override).
  • New stream identifier D slots alongside A / B / C in bundle_validate.py.
Deliverable: The first published monthly transparency report on Facebook + website. Every donor thank-you can now point at it.
F6
3–5 days

Annual receipts

Per-donor PDF; reconciles against contribution only. Tax-deductibility wording stays Foundation-supplied.

  • tools/contribution_receipt.py — per-donor annual summary; markdown + weasyprint PDF. Stamps receipted_at on every row included.
  • Output: 02_Quan-he-nha-tai-tro/Bien-nhan-nam/<YYYY>/D<NNNN>__<YYYY>.pdf.

Donor pass — built second

D0
Pilot

Practice the personal thank-you

The Coordinator hand-writes thank-you messages for one month of contributions, before automation lands.

  • Confirms the "warm not form letter" feel survives templating.
  • Provides 5–10 real exemplars to seed Knowledge/Donor/thank-you-corpus.md (the Coordinator-voice few-shot file).
Deliverable: First-pass exemplar corpus; the D3 system prompt has something honest to mimic.
D1
3–4 days

Donor record + onboarding

One atomic write across SQL + markdown + the shared funding-source enum.

  • tools/donor_register.py — creates the SQL row + writes the markdown profile + appends to 01_Ho-so-thu-huong/nguon-tai-tro.yaml. Rolls back on any partial failure.
  • tools/donor_validate.py — frontmatter shape + opt-in coherence (no opt_in_*: true without observed_at).
D2
2–3 days

Per-donor VietQR

Offline generator + brand-chrome canvas.

  • tools/qr_donor.py — NAPAS 247 spec implemented directly (~120 LOC) + ~60 LOC for the 5:4 Stream-A-style canvas. Pillow-only; no third-party QR dependency.
  • Pytest: golden-file render against a fixed donor + known addInfo payload; CRC bytes are deterministic.
D3
4–6 days

Thank-you composer

Claude with a Coordinator-voice system prompt, distinct from brand-voice. The Coordinator edits + sends.

  • tools/thank_you_draft.pyThankYouEngine Protocol seam + FakeThankYouEngine for tests.
  • thank-you-composer agent (markdown prompt) — same shape as beneficiary-recorder from Area 1.
  • /thank-you skill — wraps the agent + the tool.
  • tools/thank_you_record.py — stamps thanked_at + thank_you_channel after the Coordinator sends.
  • Knowledge/Donor/thank-you-corpus.md — few-shot exemplars seeded by D0; ratchets up over time.
D4
2–3 days

Opt-in update lists

Subscribe / unsubscribe + batch send-list CSV. No SMTP.

  • tools/update_recipient_register.py — subscribe a donor to a list; regenerates the list markdown.
  • tools/update_recipient_unsubscribe.py — sets unsubscribed_at + appends a note to the donor profile.
  • tools/update_send_batch.py — produces the CSV the Coordinator pastes into the mail client. Filters on unsubscribed_at IS NULL.
D5
4–6 days

Donor dashboard view

Extends Area-1's FastAPI dashboard with donor + contribution routes.

  • /donors — list of all donors; filter on opt-in flags.
  • /donors/<donor-id> — donor profile + their contributions.
  • /contributions — recent contributions with "needs thank-you" badge.
  • /finance/monthly/<YYYY-MM> — internal monthly report view.
  • Reads so-tai-chinh.sqlite directly; cross-area joins to Area-1 happen at the application layer.
  • Login boundary is the Drive's membership list (same as Area-1).

13 · Failure modes — what kills this build

Failure modeMitigation
Coordinator stops running the monthly bank-statement import.F4's internal monthly report is the trigger for the Coordinator's monthly cadence; the Editor's read of the report is the second-line check. If a month is missed, the next run includes both months and surfaces the gap.
A donor's name leaks into the published transparency report.finance_anon_check.py is a blocking gate on the publish bundle; no Editor override. Donor opt-in is the donor's decision, not the Editor's.
A contribution lands in the bank with no identifiable donor.The anonymous bucket HMT-D0000 exists exactly for this case; the contribution is still a row, the thank-you flow stamps thank_you_channel: none-by-request automatically. No money lost; relationship simply does not start.
A donor unsubscribes; the next batch send goes out without filtering them.update_send_batch.py filters on unsubscribed_at IS NULL; an explicit --include-opt-out flag exists for narrow Foundation-initiated reach-out (e.g. annual receipt) and logs the override.
In-kind valuation drift (double-counting a 300-set donation against benefit rows that exceed 300).inkind_disposal rows sum against the contribution's quantity; finance_validate.py cross-checks per contribution and surfaces over-disposal as a finding.
NCB CSV format changes silently.contribution_import_statement.py pins the column header set in tools/_ncb_csv_schema.yaml and hard-fails on mismatch. A schema change is a 1-line PR; a silently miscategorised import is a trust failure that takes months to surface.
Two donors share a display name.The donor code is the primary key, not the name. The dashboard always shows the code alongside the name; the thank-you composer always references both. Differentiation handled at registration via the introducer's note.
A trustee leaves and still has access to the private Drive.Quarterly membership audit by the Account Custodian (same audit as Area-1); the Drive's membership is the only access boundary on the donor PII, so this audit is load-bearing.
The institutional NCB account access is lost (Custodian unavailable).Backup Custodian named in SECURITY.md; bank credentials are held by the institution per Decree 93, not by an individual. Tools degrade gracefully (manual entry path) until access returns.
Annual receipts arithmetic disagrees with the bank.Every contribution row has a bank_txn_ref traceable back to the original statement; the receipt generator emits a per-donor sub-statement an auditor can verify line-by-line.

14 · Cross-area touchpoints

The finance registry is the downstream of Area 1 (benefits delivered flow into the monthly reconciliation through the existing benefit table's funding_source enum and date window) and upstream of Area 2 (the published transparency report ships as a post-bundle through the existing media-line approval chain, with a new stream identifier D).

The donor side is the substrate for Area 3 sponsorship — the sponsorship_match table bridges donor_id ↔ Area-1's journey_id when the future renderer comes online; the consent observation lives on the match row itself.

The shared file 01_Ho-so-thu-huong/nguon-tai-tro.yaml is the only cross-area coupling in code today; this subplan formalises its maintenance: donor_register.py appends, nothing removes — the file grows monotonically so historical benefit rows never orphan their funding-source reference.

None of the four areas writes into another's SQLite at the SQL layer; cross-area joins are at the application layer in Python, which keeps each area's storage independently re-locatable (a future Cloud Run lift can lift one area at a time, the same shape as Cloud Run Đợt 3 for Care).

15 · Open questions — resolved 2026-05-30

The seven open questions in the prior draft were taken to the trustees on 2026-05-30. Each was either resolved (and is now operationalised in the body / markdown) or explicitly parked. Two new asks came back from the same pass; both are also reflected below.

# Question Status Decision — §16
1Finance first vs parallel build?Resolved§16.a — parallel
2F5: wait 2 or 3 internal months?Resolved§16.b — ships at confidence, no fixed N
3D0001–D0099 reserved range?Parked§16.c — ordinary donors start at D0001
4Thank-you few-shot seeding?Follow-up§16.d — 5–10 templates as a separate file; voice principles recorded
5Annual receipt tax wording?Resolved§16.e — per-contribution PDF + thank-you, ASAP; annual is optional roll-up
6Sponsorship as separate subplan?Parked§16.f — substrate stays; workflow later
7NCB CSV pin maintenance?Resolved§16.g — Account Custodian; daily upload cadence (weekly fallback)
2aDonor input form + returning-donor detection?New, resolved§16.2a — web form, province (post-July-2025 + Overseas, dropdown), email/phone/name+province match cascade
2bIn-kind in a separate ledger?New, resolved§16.2b — split tables; reports show side-by-side, never summed

16 · Trustee decisions log — 2026-05-30

The Editor walked the draft with the trustees on 2026-05-30. Their responses to the §15 open questions, plus two new asks, became the decisions below. Each links back to where it is operationalised in the markdown body.

16.a — Donor and finance passes run in parallel

"Any reason why finance first, donor second? We can go donor first unless there is a strong reason not."

The "prove the practice before publishing" rationale only constrains F5 (the first published transparency report), not the donor-side machinery. D1–D5 build alongside F1–F4 sharing F1 as the substrate. F5 keeps its own confidence gate (§16.b).

16.b — F5 publication ships at confidence

"No specific timeline, we can try until full confidence."

The Finance trustee + Editor decide when the practice is stable enough to publish. F5 has no fixed-month threshold; F-zero pilot is the precondition, but no months-running counter gates the first release.

16.c — Reserved D0001–D0099 range stays parked

"Park for now."

No standing-institutional-donor list is named on this pass. Ordinary donors start at D0001. When the trustees raise the question, the range is reserved retroactively.

16.d — Thank-you templates are a follow-up artefact; voice principles recorded

"Can you prepare 5–10 templates and I will review and provide comments/revisions. Ideal is the message voice: personal, appreciation, shared values and acknowledgement, pride and equal roles, dependency."

Voice principles recorded in markdown §7 step 5 (the thank-you composer context) and feed D3's system prompt + few-shot corpus. The 5–10 concrete templates ship as a separate file (_system/plans/donor-thank-you-templates-v0.md) for trustee review, seeding Knowledge/Donor/thank-you-corpus.md.

PrincipleWhat it means in the letter
PersonalAddressed to the donor by their preferred name; references the specific contribution + intent. Never "Dear donor".
AppreciationDirect, heartfelt thanks for the specific act. Not generic.
Shared values + acknowledgementNames the value the donor's act reflects (education, opportunity, the dignity of the child) and acknowledges it as value the donor + the Foundation hold together.
Pride + equal rolesThe Foundation is proud of the work; the donor is an equal partner, not a benefactor and a recipient. Voice is between equals.
DependencyHonest, plainly stated: the Foundation depends on this support to keep doing the work. Acknowledgement, not flattery.

16.e — Per-contribution PDF receipts replace annual-only model; sent with thank-you

"Annual receipt should be sent in pdf file (you design a template) together with each thank you letter which should be sent to the donors as soon as after the system can confirm receipt of fund."

Every cash contribution and every in-kind handover gets a designed PDF receipt generated at promote-time (markdown §7 step 4) and attached to the thank-you email sent within the next business day (markdown §7 step 5). The year-end aggregate becomes an optional roll-up (markdown §9.3) for donors or auditors who ask.

16.f — Sponsorship workflow stays parked

"Parked for now."

The sponsorship_match table is built so the bridge exists; the matching workflow + the per-sponsor renderer stay parked until the four operational areas are stable and the trustees raise it again.

16.g — Daily statement upload (weekly fallback); Account Custodian owns the CSV pin

"NCB: We are thinking of more regular updates. At this stage, daily uploads of bank statements/transactions. If not, weekly."

The Account Custodian uploads end-of-business-day daily; weekly is the documented fallback. The same role owns the NCB CSV schema pin (the Custodian sees the export format change before anyone else does, on the next day's upload).

16.2a — Donor input form (province, privacy, returning-donor detection)

"Donor input forms should catch enough information such as phone number, email address, full names, province, option of privacy (anything else just enough, no extra). Should be verified if this is a regular donor (repeated after the first donation), to acknowledge their returning/additional connections/relationships."
Follow-ups: "Provinces: Should be dropdowns and add one more option for Overseas." · "Use the new list of provinces after 1 July 2025."

Markdown §3a defines the form: full_name, phone, email, province (from vn-provinces-2025-07-plus-overseas — the 34 post-July-2025 administrative units + the Overseas option, as a strict dropdown), privacy choice, opt-in updates, optional notes. Markdown §3a.2 specifies the three-step returning-donor detector (email → phone → name+province), which surfaces a "merge candidate" in the Coordinator inbox before promoting.

16.2b — In-kind contributions live in a separate ledger, outside the financial statements

"Let's keep in kind contributions separately in a separate ledger (outside the financial statements)."

Cash (contribution) and in-kind (inkind_contribution) are split at the schema level so a join cannot accidentally sum them. The monthly cash reconciliation (markdown §8.1) is cash-only; the in-kind monthly report (markdown §8.4) is its own document; the published transparency package (markdown §8.2) shows the two sections side-by-side, never summed.

17 · What this subplan deliberately is not