@12-apps/payments-frontend 3.17.0 → 3.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,244 @@
1
+ 'use client';
2
+
3
+ import { Button, Stack } from '@mui/material';
4
+ import type { JSX } from 'react';
5
+
6
+ import { BTN_PRIMARY_SX, BTN_SECONDARY_SX, LINKISH_SX } from '../../components/panel-tokens';
7
+
8
+ import { useActivationCopy } from './copy-context';
9
+ import { Notice, ProviderMessage } from './notice';
10
+
11
+ /**
12
+ * The settled outcomes of the activation charge, shared by both flows.
13
+ *
14
+ * They live apart from the panels that frame them because the panel is about
15
+ * layout and these are about what the owner is being TOLD — and every sentence
16
+ * here has cost real money to get right more than once. Three of the six are
17
+ * ways the link never got minted, and they are three screens rather than one
18
+ * because they are three different instructions: fix a setting, wait it out,
19
+ * read the provider's own words.
20
+ */
21
+
22
+ /** Settled-and-passed: the provider is on, and the cent is on its way back. */
23
+ export function PassedState({
24
+ amountLabel,
25
+ refunded,
26
+ onRetry,
27
+ }: {
28
+ /** What was charged. `null` only while the endpoint has not priced it yet. */
29
+ amountLabel: string | null;
30
+ refunded: boolean;
31
+ onRetry: () => void;
32
+ }): JSX.Element {
33
+ const { outcome, actions } = useActivationCopy();
34
+ const amount = amountLabel ?? outcome.someAmount;
35
+ return (
36
+ <Stack spacing={1} data-testid="verify-charge-passed">
37
+ <Notice
38
+ tone="ok"
39
+ title={outcome.approvedTitle}
40
+ description={refunded ? outcome.refundedBody(amount) : outcome.refundPendingBody(amount)}
41
+ />
42
+ <Stack direction="row">
43
+ <Button sx={LINKISH_SX} onClick={onRetry} data-testid="verify-charge-retry">
44
+ {actions.testAgain}
45
+ </Button>
46
+ </Stack>
47
+ </Stack>
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Settled-and-refused. The wording is the point: the owner IS connected, so
53
+ * telling them the connection failed would send them to reauthorize something
54
+ * that already works.
55
+ */
56
+ export function FailedState({
57
+ reason,
58
+ providerMessage,
59
+ onRetry,
60
+ }: {
61
+ reason: string;
62
+ providerMessage?: string;
63
+ onRetry: () => void;
64
+ }): JSX.Element {
65
+ const { outcome, actions } = useActivationCopy();
66
+ return (
67
+ <Stack spacing={1} data-testid="verify-charge-failed">
68
+ <Notice tone="bad" title={outcome.authenticatedNotActive} description={reason} />
69
+ {providerMessage ? (
70
+ <ProviderMessage message={providerMessage} label={outcome.providerSaid} />
71
+ ) : null}
72
+ <Stack direction="row">
73
+ <Button sx={BTN_PRIMARY_SX} onClick={onRetry} data-testid="verify-charge-retry">
74
+ {actions.retry}
75
+ </Button>
76
+ </Stack>
77
+ </Stack>
78
+ );
79
+ }
80
+
81
+ /**
82
+ * The provider would not CREATE the link. A different failure with a different
83
+ * owner, and it had been wearing the wrong one's clothes.
84
+ *
85
+ * Nothing was charged and nothing is outstanding — the request to mint a
86
+ * payment page was refused outright, which overwhelmingly means a provider-side
87
+ * switch is still off. That is a setup step rather than an error, and the
88
+ * screen it replaced ("authenticated but not active", with a retry button) sent
89
+ * the owner to re-check credentials that were fine and then to press retry
90
+ * against a setting that had not changed.
91
+ *
92
+ * `onDismiss` is not "try the same thing again": it clears the failed attempt
93
+ * and returns the step to its starting state, which is what the owner needs
94
+ * AFTER going to flip the setting. Safe here in a way it is not elsewhere in
95
+ * this flow, because nothing was charged — there is no outstanding payment for
96
+ * a second attempt to duplicate.
97
+ */
98
+ export function SetupIncompleteState({
99
+ displayName,
100
+ reason,
101
+ providerMessage,
102
+ onDismiss,
103
+ }: {
104
+ displayName: string;
105
+ reason: string;
106
+ providerMessage?: string;
107
+ onDismiss: () => void;
108
+ }): JSX.Element {
109
+ const { outcome, actions } = useActivationCopy();
110
+ return (
111
+ <Stack spacing={1} data-testid="verify-charge-setup-incomplete">
112
+ <Notice
113
+ tone="warn"
114
+ title={outcome.refusedTitle(displayName)}
115
+ description={outcome.refusedBody(displayName)}
116
+ />
117
+ {reason ? <Notice tone="info" title={reason} /> : null}
118
+ {providerMessage ? (
119
+ <ProviderMessage message={providerMessage} label={outcome.providerSaid} />
120
+ ) : null}
121
+ <Stack direction="row">
122
+ <Button sx={LINKISH_SX} onClick={onDismiss} data-testid="verify-charge-retry">
123
+ {actions.restart}
124
+ </Button>
125
+ </Stack>
126
+ </Stack>
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Already proven: the ONLY honest thing this step can render is that fact.
132
+ *
133
+ * It used to reset to the pay button on every reload, because the proof lives
134
+ * on the server and the screen never asked. An owner whose charge HAD landed
135
+ * was greeted by the pay button again — and one owner, reasonably reading that
136
+ * as "it did not work", paid four times.
137
+ *
138
+ * It is also the end of the flow, so it says where to go next. A terminal
139
+ * screen with no exit is how an owner who has just finished setting up payments
140
+ * ends up hunting the sidebar for the two things they now actually want: the
141
+ * order providers are tried in, and the storefront this was all for. Both sit
142
+ * INSIDE the panel, because they are what this outcome offers.
143
+ */
144
+ export function ProvenState({
145
+ storeUrl,
146
+ onProviderOrder,
147
+ }: {
148
+ /** The storefront this connection now takes money for. */
149
+ storeUrl: string;
150
+ /** Back to the provider list, which is where the failover chain lives. */
151
+ onProviderOrder: () => void;
152
+ }): JSX.Element {
153
+ const { outcome, actions } = useActivationCopy();
154
+ return (
155
+ <Notice
156
+ tone="ok"
157
+ title={outcome.provenTitle}
158
+ description={outcome.provenBody}
159
+ dataTestId="verify-charge-proven"
160
+ >
161
+ <Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap' }}>
162
+ <Button
163
+ sx={BTN_SECONDARY_SX}
164
+ onClick={onProviderOrder}
165
+ data-testid="verify-charge-provider-order"
166
+ >
167
+ {actions.setProviderOrder}
168
+ </Button>
169
+ <Button
170
+ sx={BTN_SECONDARY_SX}
171
+ onClick={() => window.open(storeUrl, '_blank', 'noopener')}
172
+ data-testid="verify-charge-open-store"
173
+ >
174
+ {actions.seePublishedStore}
175
+ </Button>
176
+ </Stack>
177
+ </Notice>
178
+ );
179
+ }
180
+
181
+ /**
182
+ * We could not reach the provider while MINTING the link.
183
+ *
184
+ * Deliberately not {@link SetupIncompleteState}, and the difference is the whole
185
+ * reason this exists: that screen tells the owner a provider-side switch is
186
+ * probably off and puts them back a step to fix it. A request that never
187
+ * arrived is no evidence of that at all — the provider refused nothing — so
188
+ * saying it would send someone whose connection blinked to change a setting
189
+ * that was already correct, and take a finished step away from them on the way.
190
+ *
191
+ * The only honest instruction is the one an outage deserves: try again.
192
+ */
193
+ export function UnreachableState({
194
+ reason,
195
+ providerMessage,
196
+ onRetry,
197
+ }: {
198
+ reason: string;
199
+ providerMessage?: string;
200
+ onRetry: () => void;
201
+ }): JSX.Element {
202
+ const { outcome, actions } = useActivationCopy();
203
+ return (
204
+ <Stack spacing={1} data-testid="verify-charge-unreachable">
205
+ <Notice tone="warn" title={outcome.unreachableTitle} description={reason} />
206
+ {providerMessage ? (
207
+ <ProviderMessage message={providerMessage} label={outcome.providerSaid} />
208
+ ) : null}
209
+ <Stack direction="row">
210
+ <Button sx={BTN_PRIMARY_SX} onClick={onRetry} data-testid="verify-charge-retry-create">
211
+ {actions.tryAgain}
212
+ </Button>
213
+ </Stack>
214
+ </Stack>
215
+ );
216
+ }
217
+
218
+ /**
219
+ * The link's window elapsed with nobody paying it.
220
+ *
221
+ * Nothing failed and nothing was charged, so the wording carries no blame and
222
+ * the offer is the only thing that helps: another link. The sentence it
223
+ * replaced read as a refusal, and an owner who believes a charge was refused
224
+ * does not press a button that looks like it charges again.
225
+ */
226
+ export function ExpiredState({
227
+ reason,
228
+ onRegenerate,
229
+ }: {
230
+ reason: string;
231
+ onRegenerate: () => void;
232
+ }): JSX.Element {
233
+ const { outcome, actions } = useActivationCopy();
234
+ return (
235
+ <Stack spacing={1} data-testid="verify-charge-expired">
236
+ <Notice tone="info" title={outcome.expiredTitle} description={reason} />
237
+ <Stack direction="row">
238
+ <Button sx={BTN_PRIMARY_SX} onClick={onRegenerate} data-testid="verify-charge-regenerate">
239
+ {actions.generateNewCharge}
240
+ </Button>
241
+ </Stack>
242
+ </Stack>
243
+ );
244
+ }
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ /**
6
+ * What the activation charge will actually cost, per provider.
7
+ *
8
+ * NOT always a cent. At least one provider refuses a one-cent total outright —
9
+ * InfinitePay answers `422 {"errors":{"items":["Total price must be greater
10
+ * than 1"]}}`, where the "1" is one REAL — so its verification charges more,
11
+ * and a button promising one figure while charging another would be exactly the
12
+ * kind of lie this flow exists to remove.
13
+ *
14
+ * Read from the host's endpoint rather than assumed here: the minimum is a fact
15
+ * about the provider's API, discovered from its own refusal, and a constant in
16
+ * this package would be wrong for the first adopter whose provider disagrees.
17
+ *
18
+ * `null` until it answers — the caller decides what a sentence says before
19
+ * there is an amount to name, because that is a sentence and sentences are the
20
+ * host's. The CARD flow does not need this at all: `useActivationCharge` reads
21
+ * the amount out of the same body it reads the store's card key from.
22
+ */
23
+ export function useVerificationAmount(verifyChargeUrl: string): number | null {
24
+ const [amountCents, setAmountCents] = useState<number | null>(null);
25
+
26
+ useEffect(() => {
27
+ let active = true;
28
+ void fetch(verifyChargeUrl)
29
+ .then((res) => (res.ok ? (res.json() as Promise<{ amountCents?: number }>) : null))
30
+ .then((body) => {
31
+ if (active && typeof body?.amountCents === 'number') setAmountCents(body.amountCents);
32
+ })
33
+ // A failed read leaves the amount unnamed rather than guessed — the
34
+ // screen still renders, and it is corrected the moment an answer lands.
35
+ .catch(() => undefined);
36
+ return () => {
37
+ active = false;
38
+ };
39
+ }, [verifyChargeUrl]);
40
+
41
+ return amountCents;
42
+ }
package/src/index.ts CHANGED
@@ -334,37 +334,35 @@ export { PT_BR_PLATFORM_HOMOLOGACAO_COPY } from './components/platform/pt-BR';
334
334
  export type { PaymentEnvironment } from '@12-apps/payments-backend';
335
335
 
336
336
  // ---------------------------------------------------------------------------
337
- // The REDIRECT ACTIVATION protocol (FUT-463, packaged by FUT-763) — proving a
338
- // connection can charge, for a provider whose payer pays on its own page.
337
+ // The ACTIVATION STEP (FUT-463, FUT-763, FUT-764) — proving a connection can
338
+ // actually charge, both protocols and the screens that render them.
339
339
  //
340
- // `renderVerification` above stays what it was: the package decides where the
341
- // step appears and the host owns the screen. What moved is the protocol behind
342
- // it resume-on-mount, the return trip's ids, the refusal/expiry/transport
343
- // distinctions, the bounded wait. Every one of those was learned from a payment
344
- // that went wrong, and no second host should have to learn them again.
340
+ // A connection is not a capability: a completed grant says the owner authorized
341
+ // us, not that the account can take money. The owner's own card or a real
342
+ // link they pay on the provider's page — goes through the SAME path a shopper's
343
+ // does, for a small amount, refunded or landing in their own account.
344
+ //
345
+ // Every branch in there was learned from a payment that went wrong: an owner
346
+ // who paid four times, a dead end blaming a store for a key that was never
347
+ // going to exist, a refusal wearing another failure's clothes. The SENTENCES
348
+ // stay the host's, required and defaultless, as everywhere here.
349
+ //
350
+ // Listed in `./activation/public` — see its header for why it is not inline.
345
351
  // ---------------------------------------------------------------------------
346
- export {
347
- useRedirectActivation,
348
- type RedirectActivation,
349
- type RedirectActivationOptions,
350
- } from './activation/use-redirect-activation';
351
- export { type RedirectActivationCopy } from './activation/copy';
352
- export {
353
- creationFailure,
354
- postActivation,
355
- refusedByProvider,
356
- settleActivationPoll,
357
- type ActivationClock,
358
- type ActivationPendingBody,
359
- type ActivationPollBody,
360
- type RedirectActivationState,
361
- type SettlePollIo,
362
- } from './activation/redirect-state';
363
- export {
364
- clearReturnedSettlement,
365
- takeReturnedSettlement,
366
- RETURNED_SETTLEMENT_KEY,
367
- } from './activation/returned-settlement';
352
+ export * from './activation/public';
353
+
354
+ // ---------------------------------------------------------------------------
355
+ // The two payment LEDGERS (FUT-764) — every charge raised against every order,
356
+ // and the subset where the provider captured LESS than the order was worth.
357
+ //
358
+ // UI-free on purpose. There is no component in that folder: a ledger is a
359
+ // table, every host already has one, and a grid slot wide enough to satisfy
360
+ // them all would be a worse contract than handing over rows. What moves is
361
+ // what a host was deriving twice and getting subtly wrong both times — which
362
+ // amount is "captured" when the audit diff and the payment row disagree, why a
363
+ // decision is shown INSTEAD of the order status, and what still counts as work.
364
+ // ---------------------------------------------------------------------------
365
+ export * from './ledger';
368
366
 
369
367
  // ---------------------------------------------------------------------------
370
368
  // The connect ROUND TRIP's other end (FUT-763): what the OAuth callback
@@ -378,23 +376,3 @@ export {
378
376
  type ConnectErrorCode,
379
377
  type ConnectReturn,
380
378
  } from './components/connect-return';
381
-
382
- // ---------------------------------------------------------------------------
383
- // The ACTIVATION CHARGE (FUT-463, packaged by FUT-763) — proving a connection
384
- // can charge, for a provider whose payer pays HERE.
385
- //
386
- // A connection is not a capability: a completed grant says the owner authorized
387
- // us, not that the account can take money. The owner's own card goes through
388
- // the SAME path a shopper's does — same fields, same validation, same
389
- // browser-side encryption — for one cent, refunded immediately.
390
- //
391
- // The sibling of `useRedirectActivation` for the other half of the same step.
392
- // As there, the SCREEN stays the host's.
393
- // ---------------------------------------------------------------------------
394
- export {
395
- useActivationCharge,
396
- type ActivationCharge,
397
- type ActivationChargeOptions,
398
- type ActivationChargeState,
399
- } from './activation/use-activation-charge';
400
- export { type ActivationChargeCopy } from './activation/charge-copy';
@@ -0,0 +1,17 @@
1
+ export {
2
+ toShortPaymentRows,
3
+ type ShortPaymentOutcomeCopy,
4
+ type ShortPaymentRow,
5
+ } from './rows';
6
+ export {
7
+ ledgerParams,
8
+ ledgerSearch,
9
+ ledgerSearchTerm,
10
+ ledgerSort,
11
+ type LedgerSort,
12
+ } from './query';
13
+ export {
14
+ type LedgerFormatters,
15
+ type PaymentLedgerWire,
16
+ type ShortPaymentWire,
17
+ } from './wire';
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The URL a ledger page is bookmarkable at, and the request it becomes.
3
+ *
4
+ * A server-driven list keeps its search, its sort and its page in the address
5
+ * bar so a link an operator sends to a colleague opens the same rows. Both
6
+ * ledgers do it identically, and both got the same two things subtly wrong
7
+ * when written twice:
8
+ *
9
+ * - a facet value from a stale bookmark forwarded straight into a 400, which
10
+ * replaces the WHOLE page — grid, selector and all — with an error whose only
11
+ * affordance re-requests the same bad URL;
12
+ * - the default facet included in the query, so the react-query key differed
13
+ * between an operator who arrived with `?view=all` and one who arrived with
14
+ * nothing, and the identical list was fetched twice and cached apart.
15
+ */
16
+
17
+ /** The params a ledger grid owns and forwards verbatim. */
18
+ const OWNED = ['q', 'page', 'sort'] as const;
19
+
20
+ /** A sort as the wire spells it, or nothing when the URL carries none. */
21
+ export interface LedgerSort {
22
+ id: string;
23
+ dir: 'asc' | 'desc';
24
+ }
25
+
26
+ /**
27
+ * The query string for the current URL.
28
+ *
29
+ * `facet` is passed already SANITIZED — a caller resolves an unknown value to
30
+ * its default first — and omitted when it IS the default, so the request and
31
+ * the cache key are identical however the operator arrived.
32
+ */
33
+ export function ledgerSearch(
34
+ params: URLSearchParams,
35
+ facet?: { key: string; value: string; fallback: string },
36
+ ): string {
37
+ const query = new URLSearchParams();
38
+ for (const key of OWNED) {
39
+ const value = params.get(key);
40
+ if (value) query.set(key, value);
41
+ }
42
+ if (facet && facet.value !== facet.fallback) query.set(facet.key, facet.value);
43
+ return query.toString();
44
+ }
45
+
46
+ /** The sort the URL is carrying, if it is carrying a well-formed one. */
47
+ export function ledgerSort(params: URLSearchParams): LedgerSort[] {
48
+ const [id, dir] = (params.get('sort') ?? '').split(':');
49
+ if (!id || !dir) return [];
50
+ return [{ id, dir: dir === 'desc' ? 'desc' : 'asc' }];
51
+ }
52
+
53
+ /** The search term the URL is carrying. */
54
+ export function ledgerSearchTerm(params: URLSearchParams): string {
55
+ return params.get('q') ?? '';
56
+ }
57
+
58
+ /**
59
+ * A grid query mapped back onto the params a ledger URL owns.
60
+ *
61
+ * `dir` accepts `null` as well as absent, because that is what a real grid
62
+ * produces: a column can be in the sort list with no direction chosen yet, and
63
+ * a data-grid library spells that `null` rather than by leaving the key out.
64
+ * The runtime always treated the two the same — an unsorted column contributes
65
+ * no `sort` param — so this is the TYPE catching up with the behaviour rather
66
+ * than a change to it. Typed the other way, every adopter writes the same
67
+ * `?? undefined` mapping at the call site, which is exactly the boilerplate
68
+ * this module exists to delete.
69
+ */
70
+ export function ledgerParams(query: {
71
+ search: string;
72
+ page: number;
73
+ sortBy: readonly { id: string; dir?: 'asc' | 'desc' | null }[];
74
+ }): Record<string, string | undefined> {
75
+ const sort = query.sortBy[0];
76
+ return {
77
+ q: query.search || undefined,
78
+ page: query.page > 1 ? String(query.page) : undefined,
79
+ sort: sort?.dir ? `${sort.id}:${sort.dir}` : undefined,
80
+ };
81
+ }
@@ -0,0 +1,85 @@
1
+ import type { LedgerFormatters, ShortPaymentWire } from './wire';
2
+
3
+ /**
4
+ * The short-payment queue's display projection.
5
+ *
6
+ * Three of these five decisions are the reason this is not a `map` a host
7
+ * writes for itself, and each one has been wrong in production:
8
+ *
9
+ * - **Which amount is "captured".** The audit diff can be missing it, and the
10
+ * payment row beside it is the same money seen by a second witness. Reading
11
+ * only the diff printed a dash where real money had landed.
12
+ * - **What the row's SITUATION is.** An operator's decision is shown INSTEAD of
13
+ * the order status, not beside it: a refunded shortfall leaves the order
14
+ * FAILED forever, so reading the status alone keeps calling finished work
15
+ * unreconciled — the same reason a sidebar badge has to subtract decisions.
16
+ * - **Whether the row is still WORK.** `pending` is what the row actions hang
17
+ * off, and a settled order with no decision recorded is not work either.
18
+ */
19
+
20
+ /** A wire row shaped for a table. Labels are formatted; `pending` is a fact. */
21
+ export interface ShortPaymentRow extends Record<string, unknown> {
22
+ id: string;
23
+ detectedAtLabel: string;
24
+ orderId: string;
25
+ expectedLabel: string;
26
+ capturedLabel: string;
27
+ shortfallLabel: string;
28
+ outcomeLabel: string;
29
+ methodLabel: string;
30
+ chargeLabel: string;
31
+ /** Drives the row actions: a decided shortfall offers none. */
32
+ pending: boolean;
33
+ }
34
+
35
+ /**
36
+ * What the SITUATION column can say, in the host's words.
37
+ *
38
+ * Two tables rather than one, because they answer different questions: what an
39
+ * operator DECIDED, and — when nobody has — what the order's status implies. A
40
+ * status or resolution with no entry falls through as its own raw value, which
41
+ * is the honest answer for a state this package has not met.
42
+ */
43
+ export interface ShortPaymentOutcomeCopy {
44
+ /** Keyed by `resolution` — `SETTLED`, `REFUNDED`. */
45
+ readonly resolution: Readonly<Record<string, string>>;
46
+ /** Keyed by `orderStatus`, for a row nobody has decided yet. */
47
+ readonly orderStatus: Readonly<Record<string, string>>;
48
+ }
49
+
50
+ /** The decision if there is one, else what the order's status implies. */
51
+ function outcomeLabel(
52
+ entry: ShortPaymentWire,
53
+ copy: ShortPaymentOutcomeCopy,
54
+ placeholder: string,
55
+ ): string {
56
+ if (entry.resolution) return copy.resolution[entry.resolution] ?? entry.resolution;
57
+ if (!entry.orderStatus) return placeholder;
58
+ return copy.orderStatus[entry.orderStatus] ?? entry.orderStatus;
59
+ }
60
+
61
+ export function toShortPaymentRows(
62
+ entries: ShortPaymentWire[],
63
+ format: LedgerFormatters,
64
+ copy: ShortPaymentOutcomeCopy,
65
+ ): ShortPaymentRow[] {
66
+ const money = (cents: number | null): string =>
67
+ cents === null ? format.placeholder : format.amount(cents);
68
+
69
+ return entries.map((entry) => ({
70
+ id: entry.id,
71
+ detectedAtLabel: format.dateTime(entry.detectedAt),
72
+ orderId: entry.orderId,
73
+ expectedLabel: money(entry.expectedCents),
74
+ // Same money, two witnesses: prefer the audit diff, fall back to the
75
+ // payment row it was joined to. Reading only the diff printed a dash over
76
+ // a capture that had really happened.
77
+ capturedLabel: money(entry.capturedCents ?? entry.payment?.amountCents ?? null),
78
+ shortfallLabel: money(entry.shortfallCents),
79
+ outcomeLabel: outcomeLabel(entry, copy, format.placeholder),
80
+ methodLabel: entry.method ?? format.placeholder,
81
+ chargeLabel: entry.providerChargeId ?? format.placeholder,
82
+ // Still work: nobody decided, and the order did not settle by itself.
83
+ pending: entry.resolution === null && entry.orderStatus !== 'PAID',
84
+ }));
85
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The two payment LEDGERS a store operator reads (FUT-764 burn-down).
3
+ *
4
+ * Every charge raised against every order, and the subset where the provider
5
+ * captured LESS than the order was worth. Both are server-driven lists over
6
+ * rows this package's own tables produce, and every host that mounts the
7
+ * payments platform has both — which is why the projection, the query mapping
8
+ * and the one write live here rather than being derived again per adopter.
9
+ *
10
+ * Deliberately UI-FREE. There is no component in this folder and no design
11
+ * system anywhere near it: a ledger is a table, every host already has one, and
12
+ * a grid slot wide enough to satisfy them all would be a worse contract than
13
+ * handing over rows. What a host renders is its own; WHAT IT MEANS is here.
14
+ */
15
+
16
+ /** One row of the store's payment ledger, as the API answers it. */
17
+ export interface PaymentLedgerWire {
18
+ id: string;
19
+ createdAt: string;
20
+ orderId: string | null;
21
+ status: string;
22
+ provider: string | null;
23
+ method: string | null;
24
+ amountCents: number;
25
+ /** What the order was worth, when this charge fell short of it. */
26
+ expectedCents?: number | null;
27
+ /** The gap, present only on a short capture. */
28
+ shortfallCents?: number | null;
29
+ providerChargeId: string | null;
30
+ }
31
+
32
+ /** One row of the short-payment reconciliation queue, as the API answers it. */
33
+ export interface ShortPaymentWire {
34
+ id: string;
35
+ detectedAt: string;
36
+ orderId: string;
37
+ orderStatus: string | null;
38
+ capturedCents: number | null;
39
+ expectedCents: number | null;
40
+ shortfallCents: number | null;
41
+ method: string | null;
42
+ providerChargeId: string | null;
43
+ /** The payment row the money actually landed on — the second witness. */
44
+ payment: { id: string; status: string; amountCents: number; createdAt: string } | null;
45
+ /** `SETTLED` / `REFUNDED` once an operator decided; null while waiting. */
46
+ resolution: string | null;
47
+ resolvedAt: string | null;
48
+ }
49
+
50
+ /** How a host writes the two things a ledger row is made of. */
51
+ export interface LedgerFormatters {
52
+ /** A cents amount, in the operator's language and this product's currency. */
53
+ amount: (cents: number) => string;
54
+ /** A wire timestamp, as the operator reads dates elsewhere in the console. */
55
+ dateTime: (iso: string) => string;
56
+ /** What stands in for a field the row does not carry. */
57
+ placeholder: string;
58
+ }