> ## Documentation Index
> Fetch the complete documentation index at: https://helloluciallc.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Payment truth — current

> Real Stripe in test mode behind a declared STRIPE_MODE, one truth function over the ledger and owner-recorded rows, real doors that run on the owner's tap, and a payment item that closes only when the truth says confirmed_paid.

<Note>
  This is the current payment-truth contract on the Development Engine. The
  ownership doctrine is unchanged from the
  [Payment Truth Foundation](/lucia/system-architecture/lucia-payment-truth-foundation)
  (dated 2026-06-19): the Engine owns payment truth, Stripe is what actually
  moved, the durable ledger is what survives a restart, and the Admin renders
  Engine truth. What is new since September is that the Admin now executes real
  Stripe doors on the owner's tap, and the truth closes the step.
</Note>

Quinn Salas, named in the examples below, is a synthetic guest of the private
test world that plays Villa Valentin against Development, not a real person.
The Stripe objects behind that guest are real test-mode objects.

The founder's direction that opened this lane (2026-09-06): "Test now → flip
to live." and "unless we tackle this head-on we're going to be beating around
the bush and 'pretending' when in reality, we should be perfecting the UX
around how the property owner/manager/employee actually works the 'payment.'"

***

## Real Stripe, declared mode

```text theme={"dark"}
STRIPE_MODE = test | live        declared, never inferred from a key prefix
test reads  STRIPE_SECRET_KEY_TEST  + STRIPE_WEBHOOK_SECRET_TEST
live reads  STRIPE_SECRET_KEY_LIVE  + STRIPE_WEBHOOK_SECRET_LIVE
a key whose prefix contradicts the declared mode refuses at startup
a webhook event whose livemode disagrees with the declared mode is refused
ledger rows carry the mode; the ledger is partitioned by mode
served meta.mode is read from config, never a literal
```

Development runs in **test mode** with real Stripe test-mode objects (Quinn
Salas's deposit and declined balance are real PaymentIntents in the test
account). **Live becomes possible, never on**: the live flip is one variable on
a production service set by the founder's hand, plus the live webhook endpoint
registered by the founder; no code path differs between modes (LUCI-215 phase
01, 2026-09-06). No production service exists and no live flip is evidenced.
The Admin shows a quiet "Test mode" footer while in test.

The word "sandbox" left the product with this contract; the historical ledger
value stays readable so old rows keep their meaning.

## One truth function

Payment truth is resolved **per obligation** (deposit, balance) from the Stripe
ledger and the owner's own rows, by one function that every reader uses: the
Algorithm, the Focus house pack, the payment-status read and every money door
(LUCI-215 phase 01b, engine dev `e0c869f` and `5af7703`, 2026-09-06).

```text theme={"dark"}
owner-recorded rows ADD to what is covered
Stripe money REPLACES the mirror: once Stripe has ever held money for the stay, the ledger is authoritative
an obligation is failed only while its latest Stripe attempt failed and it is uncovered
a later succeeded or owner-recorded payment on it clears it regardless of event order
a dispute wins
```

The event-order rule closes the defect where a paid deposit read as fully paid
(Deposit paid reads as fully paid, ALGO-24, engine dev `31fce1c`, 2026-09-03;
founder: "ship"). ALGO-24's "round two" (the Postgres ledger as the only
financial truth in durable mode) is named on that record as **the production
gate** and is not started.

A lying balance field on a booking record cannot move a charge: every money
door computes its amount from the ledger.

## The money story, in owner words

The Engine serves the record's words; the Admin reads them, never composes
them (Payment page speaks as one, LUCI-242, engine dev `1f7d773`,
2026-09-09):

```text theme={"dark"}
money.headline      "Paid in full on <date>."  ·  "$X still owed — the card didn't go through <date> at <time>."  ·  "$X still owed, due <day>."  ·  "Nothing still owed."
reason line         "Quinn's card didn't go through on Thu, Sep 3 at 8:24 PM, and Quinn arrives tomorrow at 2 PM."
closing sentence    "This marks itself paid when the money lands: a paid link, or a payment you record."
history rows        { at_label, text, kind }  in obligation order
```

**History rows carry kinds** (`payment-history-kinds/v1`, LUCI-245, engine
dev `e2166b8`): `card_failed`, `money_in`, `asked`, `refund`, `dispute`,
`recorded_by_owner`, `other`. The kind is attached from the ledger event that
produced the row, never inferred from the sentence (no keyword guardrails); an
unmapped row is `other`, never null. The founder's ask (2026-09-09): "for
anything historical or timeline-ish like this, we should copy Linear with this
flowing, connected, vertical element."

Every time word is the property's own (America/Bahia\_Banderas), never a zone
name, never a year.

## The doors

Each door is a real Stripe action executed on the owner's tap and never
before it. Every tap carries a fresh `client_reference`, which is the
idempotency identity; a network retry of the same tap reuses the same Stripe
idempotency key and never mints a second object.

| Door                                                                 | What it does                                                                                                                                                                                                              | Truth lands                                          |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| **Send a payment link** (`POST /admin/bookings/:id/payment-request`) | Creates or reuses a Checkout Session for the outstanding balance, amount computed server-side from the ledger; the guest's card becomes the card on file (`customer_creation: always`, `setup_future_usage: off_session`) | via the webhook, `sent` until paid                   |
| **Charge the card on file** (`charge-card`)                          | Off-session PaymentIntent on the saved payment method; rendered only when the Engine serves `card_on_file`; the attempt is reserved on the booking before Stripe is called so two taps cannot mint two intents            | via `payment_intent.succeeded`, never the API reply  |
| **Record a payment taken another way** (`confirm-manual-payment`)    | Writes an owner-recorded row covering the obligation; no longer sets a status directly                                                                                                                                    | at once; the only door that closes without a webhook |
| **Refund** (`refund`)                                                | Real Stripe refund; `refund.failed` and `refund.updated` handled                                                                                                                                                          | via `charge.refunded`, which re-opens a ticked item  |

The generic `create-checkout-session` endpoint is **retired** (LUCI-215 round
2\): it trusted the client's amount and became live-capable with the mode
contract. Only the payment-status reads remain on that router.

When there is no card on file, the record says so in words: "There's no card
on file for Quinn, so this link asks for one and keeps it for next time."

## Lucia's draft, one brain

The payment request is drafted by one gateway task, `payment_request_draft`,
pinned to `claude-fable-5-1` on Development with **no fallback** (Lucia asks
Quinn for a new card, LUCI-210, 2026-09-06): if Fable cannot draft, the step
says so and offers **Copy link** only. The draft names the guest, the amount
and the moment, carries the live checkout link, never a year, never an engine
word; the owner's own edits are never rewritten. Three buttons, no Save:
**Send email** (Resend), **Send text** (Twilio, only when the record has a
phone), **Copy link**.

**The draft reports its stages** (`payment-draft-stages/v1`, LUCI-233,
Completed 2026-09-09): `reading → linking → composing → ready`, streamed as
data-only server-sent events when the caller asks for them; a reused link
omits `linking`; never a timer, never a percentage. Founder: "Narration during
loader is MUCH more helpful."

The draft's honest face was proved and fixed on 2026-09-07 (Lucia couldn't
write the payment ask, LUCI-220, engine dev `e2dc3d8`): the composer had been
refusing before any provider was asked because of a string-versus-number
booking id at the hand-off. The per-task no-fallback env line for this task
(`LUCIA_FALLBACK_MODEL_PAYMENT_REQUEST_DRAFT=none`) is **not evidenced set**
on api-dev and remains an open founder item on that record; the source default
already yields no fallback for the task at `e2166b8`.

## The truth closes the step

A payment item on the arrival readiness feed is `done` only when the applied
truth for the stay reads `confirmed_paid`, owner-recorded rows included; it
re-opens when the truth leaves `confirmed_paid`. "Nothing else closes it — not
a send, not a note, not a tap." (Payment step is the payment record, LUCI-219,
shape accepted 2026-09-07; engine dev `5c3ca1b` and `63b5546`, admin dev
`814d90f`.)

```text theme={"dark"}
payment_request_status   none | sent | paid | failed_again      derived at read time; the admin polls nothing
sub_state                sent | failed_again | charging
door                     doors["payment:N"] → /ops/payments/N, surface payment_record, step_key from the row's own next steps
```

The payment record **is** the step: the arrival flow's "payment check" opens
the record with its doors, wearing the arrival frame ("Back to Quinn's
arrival"), and Lucia stands beside it (LUCI-211). The founder's ruling that
set this (2026-09-05): "the truth closes the step, not a Save button."

## Doctrine, unchanged

```text theme={"dark"}
Property policy = what should be due (the property's own terms; see the property pack)
Stripe = what the guest actually attempted or paid
the durable ledger = restart-safe financial record and event storage, partitioned by mode
the Algorithm = what deserves attention now (a payment row ranks from the same truth function)
Admin = renders Engine truth and executes real doors only on the owner's tap
no synthesized payment state, ever
```

## Not current

* **Live mode**: possible by contract, not on. No production service, no live
  keys evidenced, no live webhook registered.
* **ALGO-24 round two** (the Postgres ledger as sole financial truth in durable
  mode): the production gate, not started.
* **Engine migrations ride the deploy** (LUCI-146): Planned; the September
  Stripe migrations were applied by hand before their code was pushed.
* **Founder proofs still open on the record**: the rebuilt payment record
  (LUCI-215 phase 02, admin dev `81ab08f`), the self-closing step (LUCI-219),
  the history rail's icons (LUCI-245).
* The admin surface's own words and layout are documented separately (wave 2).

## See also

* [Payment Truth Foundation](/lucia/system-architecture/lucia-payment-truth-foundation) (historical, 2026-06-19)
* [Resend](/lucia/infrastructure/resend)
* [Model Layer](/lucia/infrastructure/model-layer)
* [Supabase](/lucia/infrastructure/supabase)
* [Focus Ops Intelligence](/lucia/intelligence/focus-ops-intelligence)
* [LIEA doctrine](/lucia/intelligence/lucia-intelligence-engine-algorithm)
