28 KiB
Backend Phase 13 — Weekly nurse payouts (mocked bank transfer)
Mission: pay nurses what they have earned. Build the weekly payout engine that aggregates payout-eligible, unpaid bookings/sessions into a
nurse_payout_batchesrun, fans them out to onenurse_payoutsrow per nurse (netting any pending clawback the nurse owes back), links each booking under aUNIQUEguard so it can never be paid twice, snapshots the nurse's verified primary IBAN, submits the transfers through a mocked PAYA/SATNA bank rail, and posts the outboundnurse_payableledger movement — all holiday-aware so a Nowruz-landing batch shifts off bank-closed days. This is the last money-out phase; after this a nurse's earnings are real.Track: backend · Depends on: b10 (ledger /
nurse_payable), b11 (clawbacks), b9 (booking/session eligibility, dispute window), b3 (nurse bank accounts), b1 (iranian_holidays) · Unlocks: nurse earnings; frontend f12-b13 Before you start, read../_shared/agent-operating-rules.md. It is not optional.
1. Context — where this sits
This is backend phase b13, the final money-out leg of the payments arc (b10 ledger → b11
refunds/clawbacks/invoices → b12 BNPL → b13 payouts). The platform never custodies cash: "escrow" is
an internal double-entry ledger state (product/payments/escrow-ledger.md),
and a nurse's owed balance lives in ledger_entries as the nurse_payable account. This phase drains
that accrual to a real bank transfer, once per booking, only after the dispute window has closed — the
one irreversible step in the whole money flow. Because an Iranian PAYA/SATNA transfer cannot be charged
back, eligibility gating and the clawback fallback (b11) are what protect the platform from overpaying.
What already exists (do not rebuild) — built by prior phases:
- The ledger &
nurse_payableaccrual — b10 builtledger_entries(append-only, balanced,transaction_group_id, the sixaccount_types incl.escrow_held,nurse_payable,nurse_clawback_receivable),payment_transactions,payment_webhook_events, the card-capture posting (DEBIT escrow_held/CREDIT platform_revenue+nurse_payable), theIDistributedLockRedis-lock pattern on the money path, andGetNursePayableBalance(sum ofnurse_payablelegs). Reuse the ledger posting helper and the lock — do not re-implement them. - Clawbacks — b11 built
nurse_clawbacks(nurse_id,booking_id,refund_id,original_payout_id,amount_irr,status∈pending|recovered|written_off,recovered_in_payout_id,resolved_at) and thenurse_clawback_receivableledger leg. This phase netspendingclawbacks into a payout and marks themrecovered— it does not create them. - Bookings, sessions & the dispute window — b9 built
bookings(the three-amount splitgross_price_irr = balinyaar_commission_irr + nurse_payout_amount),booking_sessions(per-visitvisit_payout_amount,payout_eligible_at),visit_verifications(EVV), and thedispute_window_ends_atset on completion (completed_at + config(dispute_window_hours, 72)). Thepayout_releasedboolean was deliberately CUT — never reintroduce it. - Nurse bank accounts — b3 built
nurse_bank_accounts(ibanenc,iban_hashUNIQUE,is_primaryfiltered-UNIQUE per nurse,is_verified,matched_national_id,account_holder_from_bank,ownership_vendor_ref) and theIBankAccountOwnershipVerifierseam. This phase reads the verified primary account and snapshots its IBAN — it does not register or verify. iranian_holidays— b1 seeded the holiday calendar (holiday_date,is_bank_closed) behind theIHolidayCalendarseam. ReuseIHolidayCalendarfor date shifting.- The platform config accessor — b1's typed, cached
platform_configsreader. Readdispute_window_hours(already used by b9) and any payout-window config through it; never hardcode. - The b0 foundation: REST surface,
BaseController,OperationResult<T>, CQRS viamartinothamar/Mediator,IFieldEncryptor(foriban_snapshot),ICurrentUser+ audit interceptor, rate limiting,IDateTimeProvider.
What this phase introduces: the three payout tables, the eligibility/build/link/execute capabilities,
and one new seam — IBankTransferProvider (the mocked PAYA/SATNA rail). The weekly cron scheduler is
DEFERRED — batches are triggered manually by an admin endpoint now (see §3, SchedulePayoutJob DEFERRED).
2. Required reading (do this first)
../_shared/agent-operating-rules.mdand../_shared/backend-conventions-checklist.md— especially the Performance, caching, money, idempotency block (IRRBIGINT, append-only balanced ledger, idempotent money writes, Redis lock on the money path).product/business/10-payouts.md— the business rules: weekly batches, EVV + dispute-window gating, one-payout-per-booking, clawback netting, holiday-aware scheduling, verified-primary-IBAN destination, MVP vs DEFERRED (no on-demand withdrawal).product/payments/cancellation-and-payout.md— Q2 "who pays the nurse, and when": the nurse payout isgross_price_irr − balinyaar_commission_irr, identical and on the identical weekly timing whether the family paid by card or BNPL; the BNPL provider's commission never touches the nurse; the optionalsettled_attiming guard.product/data-model/07-payouts.md— the canonical schema fornurse_payout_batches,nurse_payouts(incl. thegross_earnings_irr/clawback_applied_irr/net_amount_irradditions), andnurse_payout_booking_links(thebooking_idUNIQUE guard). Mirror these field names exactly.product/payments/escrow-ledger.md— the payout ledger posting (DEBIT nurse_payable/CREDIT escrow_heldfornurse_payout_amount) and the clawback leg.- Code to mirror: b10's ledger posting helper +
IDistributedLockusage, thepayment_webhook_eventsidempotency pattern, and anyFeatures/Payments/**command structure; b11'snurse_clawbacksconfig &Features/Refunds/**; b9'sbookings/booking_sessionsconfigs and the eligibility columns; b3'snurse_bank_accountsconfig; b1'sIHolidayCalendarand the typed config accessor. - Contract conventions:
../../contracts/conventions/api-conventions.mdandmoney-and-types.md(IRRBIGINT, envelope). - Prior handoffs:
dev/shared-working-context/backend/handoff/after-backend-phase-10.md,…-11.md,…-9.md,…-3.md,…-1.md, andreports/mocks-registry.md(seam rows you reuse/add).
3. Scope — build this
All money is IRR long / BIGINT. Features live under
Baya.Application/Features/Payouts/{Commands|Queries}/<Name>/; entities in
Baya.Domain/Entities/Payouts/; one IEntityTypeConfiguration<T> per entity in
Persistence/Configuration/PayoutsConfig/; one EF migration for the three tables.
3.1 Entities + migration
nurse_payout_batches [CORE] — weekly aggregation, admin/job-initiated, holiday-aware.
- Fields:
id,period_start,period_end(holiday-shifted offis_bank_closeddays),processing_date(holiday-shifted),total_amount(BIGINT),payout_count(int),status(enum, see below),initiated_by_admin_id(FKusers),processed_at(nullable),failure_notes(nullable), audit fields. - CHECK / invariant:
total_amount = Σ(nurse_payouts.net_amount_irr)for the batch — enforce in the handler when materializing rows; add a DB CHECK where SQL Server allows (else a verified invariant inExecutePayoutBatch).payout_count = COUNT(nurse_payouts). - Relations: 1:N →
nurse_payouts.
nurse_payouts [CORE] — one row per nurse per batch.
- Fields:
id,batch_id(FK),nurse_id(FKnurse_profiles),bank_account_id(FKnurse_bank_accounts),iban_snapshot(encrypted viaIFieldEncryptor, frozen at build time),gross_earnings_irr(BIGINT — Σ eligible booking/session payouts),clawback_applied_irr(BIGINT — pending clawbacks netted this batch, ≥ 0),net_amount_irr(BIGINT —gross_earnings_irr − clawback_applied_irr),amount(BIGINT — actually transferred net; equalsnet_amount_irron success),booking_count(int),status(enum),transfer_reference(nullable — the bank track id),paid_at(nullable),failure_reason(nullable), audit fields. - Invariant (handler + CHECK where possible):
net_amount_irr = gross_earnings_irr − clawback_applied_irr; all amounts ≥ 0;net_amount_irr ≥ 0(a nurse whose clawback exceeds earnings nets to zero this batch with the remainder stayingpending— see §5; never produce a negative transfer). - Relations: N:1 →
nurse_payout_batches,nurse_profiles,nurse_bank_accounts; 1:N →nurse_payout_booking_links; referenced bynurse_clawbacks.recovered_in_payout_id.
nurse_payout_booking_links [CORE] — the structural anti-double-pay guard.
- Fields:
id,payout_id(FKnurse_payouts),booking_id(FKbookings)UNIQUE,session_id(nullable FKbooking_sessions— set when paying per-session accrual),payout_amount_irr(BIGINT — the portion of this booking/session in this payout), audit fields. - The
booking_idUNIQUE index is the hard guard — a booking can be linked to exactly one payout across all batches, ever. Do not drop it. (When paying per-session, the unique guard is on(booking_id, session_id)so each session is paid once — confirm against b9's session model; the booking-levelbooking_idUNIQUE still holds for single-session bookings.) - Relations: N:1 →
nurse_payouts; 1:1 →bookings(and per-session →booking_sessions).
Status enums (define as proper enums, persist as string/byte per project convention):
PayoutBatchStatus:draft|processing|partially_failed|completed|failed.PayoutStatus:pending|submitted|paid|failed.
3.2 Commands & queries (CQRS, OperationResult, never throw for expected failures)
| Capability | Type | Route (admin/nurse) | What it does |
|---|---|---|---|
ComputeEligibleEarningsQuery |
Query | GET api/v1/admin_payouts/eligible?period_start=&period_end= |
Projects (AsNoTracking + .Select) the payout-eligible, unpaid bookings/sessions for the window and groups them by nurse, returning a preview (per-nurse gross_earnings_irr, pending clawback_applied_irr, net_amount_irr, booking count). Eligible = status='completed' AND dispute_window_ends_at < now (per-session: payout_eligible_at < now) AND no open dispute AND not already in a nurse_payout_booking_links row. Paginated. |
GeneratePayoutBatchCommand |
Command | POST api/v1/admin_payouts/batches |
Opens a nurse_payout_batches row in draft. Computes period_end/processing_date and shifts them off is_bank_closed days via IHolidayCalendar to the next business day. Selects the eligible set (same predicate as the query) under a lock(payout:batch) so two runs can't grab the same bookings. Orchestrates BuildNursePayouts + LinkPayoutBookings inside one unit of work. Returns the draft batch with materialized payouts for admin preview. Idempotent: re-running for an overlapping window cannot re-select an already-linked booking (the UNIQUE link is the backstop). |
BuildNursePayouts |
Command (internal step) | — | Groups the eligible bookings per nurse; computes gross_earnings_irr = Σ(gross_price_irr − balinyaar_commission_irr) (per-session: Σ visit_payout_amount); reads the nurse's pending nurse_clawbacks, nets them into clawback_applied_irr (capped at gross_earnings_irr), sets net_amount_irr and amount; snapshots iban_snapshot from the nurse's verified primary nurse_bank_accounts (is_primary=1 AND is_verified=1 AND matched_national_id=1). A nurse with no verified primary account is skipped with a recorded reason (not silently dropped). |
LinkPayoutBookings |
Command (internal step) | — | Inserts nurse_payout_booking_links rows under the booking_id UNIQUE constraint. A duplicate-key violation is the already-paid guard — catch it, treat that booking as not-eligible, and continue (never let it abort the batch or double-pay). |
ExecutePayoutBatchCommand |
Command | POST api/v1/admin_payouts/batches/{id}/process |
Transitions draft → processing. Under lock(payout:batch) (and lock(nurse:{id}:payout) per row), submits the batch to IBankTransferProvider.SubmitPayoutBatchAsync with one PayoutInstruction per payout (nurse IBAN, net_amount_irr, PAYA/SATNA method), stores each transfer_reference, transitions payouts to submitted then paid, posts the payout ledger group (DEBIT nurse_payable / CREDIT escrow_held for nurse_payout_amount, per payout, balanced, append-only) via b10's helper, and marks each netted clawback recovered with recovered_in_payout_id + resolved_at. Batch ends completed (all paid) or partially_failed (some failed). Idempotent: carries an idempotencyKey so a retried call never re-submits an already-paid payout or double-posts the ledger. |
RetryFailedPayoutCommand |
Command | POST api/v1/admin_payouts/{payout_id}/retry |
Re-submits a single failed payout (holiday-aware: won't submit on a closed day), updating transfer_reference/status. Idempotent on the same key. |
MarkPayoutFailedCommand |
Command | POST api/v1/admin_payouts/{payout_id}/mark_failed |
Records failure_reason/failure_notes, sets status='failed'. Used on a reconciled bank rejection. Does not post a ledger movement (no money left). |
GetBatchDetailQuery |
Query | GET api/v1/admin_payouts/batches/{id} |
Batch header + its payouts (status, net, transfer_reference) + per-payout linked bookings. Projected, paginated. |
ListPayoutBatchesQuery |
Query | GET api/v1/admin_payouts/batches?status=&page=&page_size= |
Admin reconciliation list. Projected + paginated. |
GetNursePayoutHistoryQuery |
Query | GET api/v1/nurse_payouts/history?page=&page_size= |
The nurse's own payouts (tenancy-scoped to ICurrentUser): status, net_amount_irr, transfer_reference, paid_at, masked IBAN, any clawback applied. Projected + paginated. Feeds f12's earnings screen. |
- Controllers:
AdminPayoutsController(admin policy, payout-sensitive endpoints rate-limited) andNursePayoutsController(nurse policy, tenancy-scoped). Bothsealed : BaseController, injectISender, returnbase.OperationResult(...), snake_case[controller]/[action]routes,CancellationTokenthreaded. - Validators: FluentValidation on
GeneratePayoutBatchCommand(period_start ≤ period_end, not in the future) and the id-bearing commands.
3.3 DEFERRED (build the seam/flag, not the feature)
SchedulePayoutJob— the recurring weekly cron trigger (PAYA-aligned). DEFERRED: batches are admin-triggered now. Leave a clean entry point (theGeneratePayoutBatchCommandthe cron will call) and a config key for the cadence; note it in the report. (Roadmap: a hosted scheduler later.)- On-demand / instant nurse withdrawal, per-nurse configurable payout frequency, automated
clawback recovery beyond next-batch netting — DEFERRED per
product/business/10-payouts.md§(c). - The optional BNPL
settled_attiming guard (don't pay before BNPL cash is actually received) — expose it as a config flag (require_bnpl_settlement_for_payout, default off) and apply it in the eligibility predicate when set; do not hard-couple payouts to BNPL settlement. Note in the report.
4. Mocks & seams in this phase
| Seam | Owner | Mock behaviour | Registry |
|---|---|---|---|
IBankTransferProvider |
introduced here | SubmitPayoutBatchAsync(PayoutBatchId, IReadOnlyList<PayoutInstruction>, idempotencyKey, ct) returns a deterministic externalBatchRef + a per-instruction transfer_reference, status Submitted then Paid for all rows (no money moves); GetPayoutStatusAsync(externalBatchRef, ct) echoes Paid. PAYA vs SATNA selection: mock honours the method on each PayoutInstruction (choose SATNA for high-value rows above a config threshold, else PAYA) and records it. A config switch can force a deterministic failure (closed-day / insufficient-provider-balance) so partially_failed/retry paths are testable. |
add a new row (🟡) |
IHolidayCalendar |
reuse from b1 | static seeded iranian_holidays; used to shift period_end/processing_date off is_bank_closed days. |
reuse row |
IFieldEncryptor |
reuse from b0 | local symmetric key; encrypts iban_snapshot, never logs plaintext. |
reuse row |
IDistributedLock |
reuse from b10 | in-memory mock lock; lock(payout:batch) + lock(nurse:{id}:payout). |
reuse row |
ICacheService |
reuse from b0/b1 | in-memory; behind the typed config accessor. | reuse row |
The mock lives behind a DI-registered interface in Infrastructure (real impl is a drop-in later); a
real JibitBankTransferProvider / VandarPayoutProvider selection is config-driven, never an
if (mock) branch in a handler. Append the IBankTransferProvider row to
../../shared-working-context/reports/mocks-registry.md
(seam, file, what's faked, config keys, step-by-step how to make it real — Jibit transferor / Vandar
payout API, registered source settlement account, each nurse's verified Sheba, PAYA-vs-SATNA selection,
batch caps/minimums, the reconciliation callback that flips submitted → paid/failed).
5. Critical rules you must not get wrong
Money correctness is sacred — the following must hold verbatim:
- Money is IRR
BIGINT, no floats, ever. Every amount (gross_earnings_irr,clawback_applied_irr,net_amount_irr,amount,payout_amount_irr,total_amount) islong/BIGINT. No float path. - One payout per booking —
nurse_payout_booking_links.booking_idUNIQUE is the hard guard; never pay a booking in two batches. A duplicate insert is the already-paid signal; treat it as not-eligible and continue. Do not bypass the constraint across batches. - Payout eligibility requires
dispute_window_ends_at(or per-sessionpayout_eligible_at) passed AND no open dispute — never pay oncompletedalone. EVV completion alone is not enough; the dispute window must have closed. - Net prior clawbacks before transfer:
net_amount = gross_earnings − clawback, and mark recovered clawbacks (status='recovered',recovered_in_payout_id,resolved_at). Don't overpay a nurse who owes money back. If pending clawbacks exceed this batch's earnings, net to zero (never negative) and leave the remainderpendingfor the next batch. 'paid'derives from anurse_payout_booking_linkslink row + a ledger movement out ofnurse_payable— thepayout_releasedboolean is gone. Never reintroduce it; never infer paid-state from a status flag alone.- Append-only, balanced ledger. The payout posts
DEBIT nurse_payable nurse_payout_amount/CREDIT escrow_held nurse_payout_amountper payout, in onetransaction_group_id, Σdebit = Σcredit, via b10's helper. Never UPDATE/DELETE a ledger row; corrections are new balancing postings. The nurse's payable balance is derived from the ledger and may go negative (clawbacks) — don't clamp it to zero. - Holiday-aware shifting: a Nowruz batch must move off bank-closed days. Shift
period_endandprocessing_dateto the nextis_bank_closed=0day viaIHolidayCalendar, or PAYA/SATNA fails. total_amount = Σ payoutsmust hold per batch (CHECK / verified invariant);payout_count = COUNT(payouts).- Gross = commission + payout: the payout amount is
gross_price_irr − balinyaar_commission_irr(the booking's own split), never a BNPL provider'ssettled_amount_irr;bnpl_commission_irris a platform expense and never touches the nurse. The nurse's pay is invariant to payment method. - Webhook / transfer idempotency: the bank submit carries an
idempotencyKeyand the payoutstatusstate machine (pending → submitted → paid) is forward-only, so a retriedExecutePayoutBatchCommandnever double-sends an irreversible transfer or double-posts the ledger. Redislock(payout:batch)is the fast first line; the status state machine + the link UNIQUE are the authoritative backstop. - First payout is gated on the nurse's
matched_national_id(b3): only a verified primary IBAN (is_primary=1 AND is_verified=1 AND matched_national_id=1) may receive a transfer. Snapshot that IBAN intoiban_snapshot(encrypted) and store thetransfer_referencefor reconciliation. - Real bank transfers are effectively irreversible — which is why payout is dispute-window-gated and refund-after-payout falls back to a clawback (b11), not a transfer reversal. Treat the execute step as the point of no return.
- Tenancy:
GetNursePayoutHistoryQueryis scoped to the authenticated nurse viaICurrentUser; a nurse can never read another nurse's payouts. Admin endpoints sit behind the admin policy and are rate-limited.
6. Definition of Done
The shared definition-of-done.md, plus:
- The three tables (
nurse_payout_batches,nurse_payouts,nurse_payout_booking_links) exist via one migration, each with itsIEntityTypeConfiguration<T>, thebooking_idUNIQUE index, thenet_amount_irr = gross_earnings_irr − clawback_applied_irrandtotal_amount = Σ payoutsinvariants, encryptediban_snapshot, and soft-delete/audit wiring per conventions. - All §3.2 commands/queries implemented (CQRS,
OperationResult, projected + paginated reads, validators), withAdminPayoutsController+NursePayoutsController. IBankTransferProviderintroduced (Application interface, Infrastructure mock, DI registration via aServiceConfiguration/extension, config-selected). Noif (mock)in handlers.- Eligibility predicate is correct (completed + dispute-window/
payout_eligible_atpassed + no open dispute + not already linked); clawback netting +recoveredmarking works; the payout ledger group posts balanced out ofnurse_payable; holiday shifting works. - Handler unit tests (NSubstitute) for eligibility selection, clawback netting, the duplicate-link
guard, ledger posting, and holiday shifting; ≥1
WebApplicationFactoryintegration test per controller (happy path, 401, validation 400).dotnet build Baya.slnzero new warnings;dotnet test Baya.slngreen. - The
Baya.Application/Features/Payouts/**area is reflected in the Project map inserver/CLAUDE.md; theIBankTransferProviderseam noted where seams are documented. - The contract
dev/contracts/domains/payouts.mdwritten and theswagger.jsonsnapshot republished.
7. How to test (what a human can verify after this phase)
Seed (or reuse from prior phases) a few completed bookings: some with dispute_window_ends_at in the
past (eligible), some in the future (not yet), one disputed, and one with a pending
clawback on the nurse. Ensure one nurse has a verified primary IBAN and one does not.
- Eligibility preview —
GET api/v1/admin_payouts/eligible?period_start=…&period_end=…→ only the completed-and-dispute-window-closed, unpaid bookings appear, grouped by nurse; the future-window and disputed bookings are excluded; the nurse without a verified IBAN is flagged. - Generate a batch —
POST api/v1/admin_payouts/batches→ adraftbatch with onenurse_payoutsrow per eligible nurse; the nurse with a pending clawback showsclawback_applied_irr > 0andnet_amount_irr = gross_earnings_irr − clawback_applied_irr;total_amount = Σ net_amount_irr;iban_snapshotpopulated (encrypted). - Double-pay guard — attempt to generate a second batch covering the same bookings → those
bookings are not re-selected (the
booking_idUNIQUE link blocks it); no booking appears in two payouts. - Holiday shift — set
processing_dateto land on a seededis_bank_closed=1Nowruz day → the batchperiod_end/processing_dateis shifted to the next business day. - Execute —
POST api/v1/admin_payouts/batches/{id}/process→ payouts gosubmitted → paidwith atransfer_reference; the ledger shows a balancedDEBIT nurse_payable/CREDIT escrow_heldper payout (verifyGetNursePayableBalancedrops by the paid amount); the netted clawback is markedrecoveredwithrecovered_in_payout_idset. - Idempotency — re-
processthe same batch → no second transfer, no second ledger posting (statuses alreadypaid). - Failure / retry — flip the mock to force a failure → batch ends
partially_failed;POST …/{payout_id}/retryre-submits and (with the mock back to success) flips topaid. - Nurse history —
GET api/v1/nurse_payouts/historyas the nurse → their payouts with masked IBAN, net amount, transfer reference, and clawback explanation; another nurse's payouts are not visible.
8. Hand off & document (close the phase)
- Docs to update: the Project map in
server/CLAUDE.md(add theFeatures/Payouts/**area + theIBankTransferProviderseam); if you discover/confirm a rule the product docs don't capture (e.g. the clawback-exceeds-earnings → net-to-zero behaviour, or therequire_bnpl_settlement_for_payoutflag default), record it inproduct/business/10-payouts.md— don't invent rules. - Contract to write:
dev/contracts/domains/payouts.md(per../../contracts/domains/_TEMPLATE.md) — the admin payout endpoints (eligible/preview, create batch, process, retry, mark-failed, batch detail, list) and the nurse…/historyendpoint; thePayoutBatchStatus/PayoutStatusenums; the batch/payout/link DTO shapes (IRRBIGINT, maskediban_snapshot); auth/rate-limit/idempotency notes; the one-payout-per-booking and dispute-window-gating side-effects. Republish theswagger.jsonsnapshot per../../contracts/openapi/README.md. This is what f12-b13 consumes. - Handoff & report: write
dev/shared-working-context/backend/handoff/after-backend-phase-13.md(the payout engine is live, what f12 can now build — nurse earnings/payout history, admin payout console — which endpoints/contracts are live, that the bank rail is mocked behindIBankTransferProvider), append tobackend/STATUS.md, writedev/shared-working-context/reports/backend-phase-13-report.md(what was built, what is now testable and exactly how per §7, what is mocked + how to make it real, contracts produced, follow-ups: the cron scheduler, the BNPLsettled_atguard, on-demand withdrawal), and updatedev/shared-working-context/reports/mocks-registry.md(theIBankTransferProviderrow → 🟡). - Memory: save a
projectmemory note for the non-obvious decisions this phase fixes — the one-payout-per-booking UNIQUE guard, the clawback-netting +recovered_in_payout_idflow, the'paid'-derives-from-link+ledger rule (nopayout_released), holiday-aware shifting, and theIBankTransferProviderseam — with a one-line pointer inMEMORY.md.