@fun-xyz/fiat-contract 0.2.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,412 @@
1
+ // src/table.ts
2
+ var TABLE_VERSION = "0.2.1";
3
+ var TERMINAL_ORDER_STATUSES = [
4
+ "SETTLED",
5
+ "EXPIRED",
6
+ "CANCELLED",
7
+ "FAILED",
8
+ "REFUNDED"
9
+ ];
10
+ var KYC_ANY = [
11
+ "KYC/CAPTURE",
12
+ "KYC/INPUT_REQUIRED",
13
+ "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
14
+ "KYC/NO_ACTION_REQUIRED:ON_HOLD",
15
+ "KYC/NO_ACTION_REQUIRED:REJECTED"
16
+ ];
17
+ var TRANSITION_TABLE = {
18
+ QUOTE: {
19
+ key: "QUOTE",
20
+ kind: "QUOTE",
21
+ screens: [1],
22
+ docStatus: "SPECIFIED",
23
+ allowedTransitions: [
24
+ {
25
+ id: "continue",
26
+ mode: "SUBMIT",
27
+ endpoint: "POST /fiat/session",
28
+ when: "no valid provider session token (own-tables check)"
29
+ },
30
+ {
31
+ id: "continue",
32
+ mode: "SUBMIT",
33
+ endpoint: "POST /fiat/orders",
34
+ when: "returning user, session + KYC valid \u2014 the quote screen doubles as review"
35
+ },
36
+ {
37
+ id: "continue",
38
+ mode: "SUBMIT",
39
+ endpoint: "POST /fiat/auth",
40
+ when: "no ambient Fun identity (non-fomo surfaces only)"
41
+ }
42
+ ],
43
+ mayReturn: [
44
+ "SESSION_AUTH",
45
+ ...KYC_ANY,
46
+ "PENDING_ORDER",
47
+ "ORDER/AWAITING_CONFIRMATION",
48
+ "ORDER/CREATED",
49
+ "BLOCKED"
50
+ ],
51
+ terminal: false,
52
+ notes: [
53
+ "Re-quote on amount change is a client-local event, not a transition.",
54
+ 'If the PENDING_ORDER removal lands, QUOTE additionally returns PAYMENT{INSTRUCT} / ORDER{CREATED} directly. TODO(open-decision): contract \xA7"Proposal: remove PENDING_ORDER".'
55
+ ]
56
+ },
57
+ FUN_AUTH: {
58
+ key: "FUN_AUTH",
59
+ kind: "FUN_AUTH",
60
+ screens: [],
61
+ docStatus: "UNSPECIFIED",
62
+ allowedTransitions: [],
63
+ terminal: false,
64
+ notes: [
65
+ "TODO(open-decision): shape and transitions owned by the auth spike (contract \xA7Auth & ref binding, item 5).",
66
+ "Never renders in fomo \u2014 identity is ambient, so the FE doc gives it no screen and no bindings row."
67
+ ]
68
+ },
69
+ SESSION_AUTH: {
70
+ key: "SESSION_AUTH",
71
+ kind: "SESSION_AUTH",
72
+ screens: [2],
73
+ docStatus: "SPECIFIED",
74
+ allowedTransitions: [
75
+ { id: "verify", mode: "SUBMIT", endpoint: "POST /fiat/session/verify" }
76
+ ],
77
+ mayReturn: [
78
+ "SESSION_AUTH",
79
+ ...KYC_ANY,
80
+ "ORDER/AWAITING_CONFIRMATION",
81
+ "PENDING_ORDER"
82
+ ],
83
+ terminal: false,
84
+ notes: [
85
+ "A bad code re-enters this same state with error {category: USER_ERROR, code: BAD_CODE} \u2014 banner, not a screen change.",
86
+ "No resend affordance is specified in either doc (known gap, FE doc Screen 2)."
87
+ ]
88
+ },
89
+ "KYC/CAPTURE": {
90
+ key: "KYC/CAPTURE",
91
+ kind: "KYC",
92
+ phase: "CAPTURE",
93
+ screens: [8],
94
+ docStatus: "SPECIFIED",
95
+ allowedTransitions: [
96
+ {
97
+ id: "capture",
98
+ mode: "CLIENT_SURFACE",
99
+ endpoint: "GET /fiat/kyc",
100
+ note: 'report target per FE bindings ("POST surface report \u2192 GET /fiat/kyc") and contract Flow C.'
101
+ }
102
+ ],
103
+ mayReturn: [
104
+ "KYC/INPUT_REQUIRED",
105
+ "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
106
+ "KYC/NO_ACTION_REQUIRED:ON_HOLD",
107
+ "KYC/NO_ACTION_REQUIRED:REJECTED",
108
+ "ORDER/AWAITING_CONFIRMATION"
109
+ ],
110
+ terminal: false,
111
+ notes: [
112
+ "Dropped from v1 (no UK headless-module coverage) \u2014 FE Screen 8 is a tombstone. Kind retained as vocabulary; the handshake (supportedStepKinds) keeps the backend from routing v1 SDKs here."
113
+ ]
114
+ },
115
+ "KYC/INPUT_REQUIRED": {
116
+ key: "KYC/INPUT_REQUIRED",
117
+ kind: "KYC",
118
+ phase: "INPUT_REQUIRED",
119
+ screens: [3],
120
+ docStatus: "SPECIFIED",
121
+ allowedTransitions: [
122
+ { id: "submit_round", mode: "SUBMIT", endpoint: "POST /fiat/kyc/form" }
123
+ ],
124
+ mayReturn: [
125
+ "KYC/INPUT_REQUIRED",
126
+ "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
127
+ "KYC/NO_ACTION_REQUIRED:ON_HOLD",
128
+ "KYC/NO_ACTION_REQUIRED:REJECTED",
129
+ "ORDER/AWAITING_CONFIRMATION"
130
+ ],
131
+ terminal: false,
132
+ notes: [
133
+ "One POST per round; conditional requirements may open another round.",
134
+ "FILE fields stream to POST /fiat/kyc/document \u2014 an upload path the harness owns, not a transition.",
135
+ "Hosted KYC links arrive as HOSTED_LINK form fields, not as a Surface."
136
+ ]
137
+ },
138
+ "KYC/NO_ACTION_REQUIRED:IN_REVIEW": {
139
+ key: "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
140
+ kind: "KYC",
141
+ phase: "NO_ACTION_REQUIRED",
142
+ reason: "IN_REVIEW",
143
+ screens: [9],
144
+ docStatus: "SPECIFIED",
145
+ allowedTransitions: [{ id: "poll", mode: "AWAIT", endpoint: "GET /fiat/kyc" }],
146
+ mayReturn: [
147
+ "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
148
+ "KYC/INPUT_REQUIRED",
149
+ "KYC/NO_ACTION_REQUIRED:ON_HOLD",
150
+ "KYC/NO_ACTION_REQUIRED:REJECTED",
151
+ "ORDER/AWAITING_CONFIRMATION"
152
+ ],
153
+ terminal: false,
154
+ notes: [
155
+ "Documented poll shape: intervalMs 3000, backoffFactor 1.5, maxIntervalMs 10000, giveUpAfterMs 90000.",
156
+ "Pre-order giveUp destination is a design gap (FE doc Screen 9) \u2014 client-local, no contract impact."
157
+ ]
158
+ },
159
+ "KYC/NO_ACTION_REQUIRED:ON_HOLD": {
160
+ key: "KYC/NO_ACTION_REQUIRED:ON_HOLD",
161
+ kind: "KYC",
162
+ phase: "NO_ACTION_REQUIRED",
163
+ reason: "ON_HOLD",
164
+ screens: [10],
165
+ docStatus: "SPECIFIED",
166
+ allowedTransitions: [],
167
+ mayReturn: KYC_ANY,
168
+ terminal: false,
169
+ notes: [
170
+ "THE proof that terminality is not array emptiness: transitions [] and NOT terminal. Re-entry after retryAfter re-derives."
171
+ ]
172
+ },
173
+ "KYC/NO_ACTION_REQUIRED:REJECTED": {
174
+ key: "KYC/NO_ACTION_REQUIRED:REJECTED",
175
+ kind: "KYC",
176
+ phase: "NO_ACTION_REQUIRED",
177
+ reason: "REJECTED",
178
+ screens: [11],
179
+ docStatus: "SPECIFIED",
180
+ allowedTransitions: [
181
+ {
182
+ id: "escalate",
183
+ mode: "SUBMIT",
184
+ endpoint: "GET /fiat/kyc",
185
+ when: "an escalation round is offered",
186
+ note: "TODO(open-decision): placeholder shape \u2014 the FE doc flags that a GET carrying params is not a real request shape, and OQ7 owns which rejections escalate. FE doc open question 7."
187
+ }
188
+ ],
189
+ mayReturn: KYC_ANY,
190
+ terminal: false,
191
+ notes: [
192
+ "Terminal *variant*: failureReason {KYC_REJECTED, retryable: false, recovery: CONTACT_SUPPORT} + transitions [] \u2014 renders in place, never routes through ORDER{CREATED, FAILED}. Because escalation is also legal here, the entry is not flatly terminal.",
193
+ "TODO(open-decision): contract OQ7 \u2014 terminal vs escalating rejections are not enumerated."
194
+ ]
195
+ },
196
+ PENDING_ORDER: {
197
+ key: "PENDING_ORDER",
198
+ kind: "PENDING_ORDER",
199
+ screens: [12],
200
+ docStatus: "SPECIFIED",
201
+ allowedTransitions: [
202
+ { id: "resume", mode: "SUBMIT", endpoint: "GET /fiat/orders/:id" },
203
+ { id: "cancel", mode: "SUBMIT", endpoint: "POST /fiat/orders/:id/cancel" }
204
+ ],
205
+ mayReturn: ["ORDER/CREATED", "QUOTE"],
206
+ terminal: false,
207
+ notes: [
208
+ 'TODO(open-decision): removal proposed (contract \xA7"Proposal: remove PENDING_ORDER"); FE Screen 12 is frozen until it lands. The two-call resume-or-cancel shape is why transitions is an array.',
209
+ "SUBMIT is a user-fired request, not a POST: GET /fiat/orders/:id under SUBMIT is legal."
210
+ ]
211
+ },
212
+ BLOCKED: {
213
+ key: "BLOCKED",
214
+ kind: "BLOCKED",
215
+ screens: [16],
216
+ docStatus: "SPECIFIED",
217
+ allowedTransitions: [],
218
+ mayReturn: [],
219
+ terminal: true,
220
+ notes: [
221
+ "Region/compliance block \u2014 fires from anywhere (contract \xA7Global transitions). retryAfter renders a dated comeback; null = none.",
222
+ "TODO(open-decision): v1 scope assumed yes for FCA/UK \u2014 product confirm (FE doc open question 4)."
223
+ ]
224
+ },
225
+ "PAYMENT/CAPTURE": {
226
+ key: "PAYMENT/CAPTURE",
227
+ kind: "PAYMENT",
228
+ phase: "CAPTURE",
229
+ screens: [5],
230
+ docStatus: "SPECIFIED",
231
+ allowedTransitions: [
232
+ {
233
+ id: "capture",
234
+ mode: "CLIENT_SURFACE",
235
+ endpoint: "POST /fiat/orders",
236
+ when: "capture-then-order topology (Transak headless cards: the component yields requestId, an input to createOrder)",
237
+ note: 'TODO(open-decision): report target per card topology \u2014 Transak ask (contract Flow A: "confirm before freeze").'
238
+ },
239
+ {
240
+ id: "capture",
241
+ mode: "CLIENT_SURFACE",
242
+ endpoint: "POST /fiat/orders/:id/surface-result",
243
+ when: "the order already exists (order-then-capture topology)",
244
+ note: 'Endpoint per contract \xA7Endpoints ("client reports CLIENT_SURFACE outcome; backend reconciles vs webhooks").'
245
+ }
246
+ ],
247
+ mayReturn: ["ORDER/CREATED", "PAYMENT/CAPTURE", "QUOTE"],
248
+ terminal: false,
249
+ notes: [
250
+ "Decline re-entry: same state, fresh surface.session, error {category: PAYMENT_DECLINED, retryable: true, recovery: RETRY_PAYMENT}.",
251
+ "The Surface is carried in state, never in the transition (\u03942, single-carry)."
252
+ ]
253
+ },
254
+ "PAYMENT/INSTRUCT": {
255
+ key: "PAYMENT/INSTRUCT",
256
+ kind: "PAYMENT",
257
+ phase: "INSTRUCT",
258
+ screens: [6],
259
+ docStatus: "SPECIFIED",
260
+ allowedTransitions: [
261
+ {
262
+ id: "confirm_paid",
263
+ mode: "SUBMIT",
264
+ endpoint: "POST /fiat/orders/:id/confirm-payment"
265
+ },
266
+ {
267
+ id: "cancel",
268
+ mode: "SUBMIT",
269
+ endpoint: "POST /fiat/orders/:id/cancel",
270
+ when: "in-wizard cancel placement wins",
271
+ note: "TODO(open-decision): cancel placement \u2014 in-wizard vs an out-of-wizard pending-orders surface (FE doc open questions 1+2). Per-rail cancel eligibility is contract OQ7."
272
+ }
273
+ ],
274
+ mayReturn: ["ORDER/CREATED"],
275
+ terminal: false,
276
+ notes: [
277
+ "No instructions re-issue endpoint exists; expired-instructions recovery is unenumerated. TODO(open-decision): contract OQ7.",
278
+ "Async instructions (PREPARING) are deliberately not in v1 \u2014 Transak returns bank details synchronously."
279
+ ]
280
+ },
281
+ "ORDER/AWAITING_CONFIRMATION": {
282
+ key: "ORDER/AWAITING_CONFIRMATION",
283
+ kind: "ORDER",
284
+ phase: "AWAITING_CONFIRMATION",
285
+ screens: [4],
286
+ docStatus: "SPECIFIED",
287
+ allowedTransitions: [{ id: "confirm", mode: "SUBMIT", endpoint: "POST /fiat/orders" }],
288
+ mayReturn: [
289
+ "PAYMENT/CAPTURE",
290
+ "PAYMENT/INSTRUCT",
291
+ "ORDER/CREATED",
292
+ "QUOTE",
293
+ "PENDING_ORDER"
294
+ ],
295
+ terminal: false,
296
+ notes: [
297
+ "Bank rails only \u2014 card rails skip it, the pay sheet was the confirmation.",
298
+ "Carries a refreshed quote: the price may have drifted during KYC."
299
+ ]
300
+ },
301
+ "ORDER/CREATED": {
302
+ key: "ORDER/CREATED",
303
+ kind: "ORDER",
304
+ phase: "CREATED",
305
+ screens: [7, 13, 14, 15],
306
+ docStatus: "SPECIFIED",
307
+ allowedTransitions: [
308
+ {
309
+ id: "poll",
310
+ mode: "AWAIT",
311
+ endpoint: "GET /fiat/orders/:id",
312
+ when: "status is non-terminal",
313
+ note: "Documented giveUpAfterMs 300000 \u2014 on give-up the wizard exits and the durable order takes over."
314
+ },
315
+ {
316
+ id: "retry_payment",
317
+ mode: "SUBMIT",
318
+ endpoint: "POST /fiat/orders",
319
+ when: "failureReason.recovery === RETRY_PAYMENT"
320
+ },
321
+ {
322
+ id: "requote",
323
+ mode: "SUBMIT",
324
+ endpoint: "POST /fiat/quote",
325
+ when: "failureReason.recovery === REQUOTE"
326
+ },
327
+ {
328
+ id: "cancel",
329
+ mode: "SUBMIT",
330
+ endpoint: "POST /fiat/orders/:id/cancel",
331
+ when: "in-wizard cancel placement wins and the rail allows it",
332
+ note: "TODO(open-decision): cancel placement (FE doc open questions 1+2); per-rail eligibility is contract OQ7."
333
+ }
334
+ ],
335
+ mayReturn: ["ORDER/CREATED"],
336
+ terminal: false,
337
+ terminalStatuses: TERMINAL_ORDER_STATUSES,
338
+ notes: [
339
+ "Flow-terminality here rides status \u2208 terminalStatuses, not the state kind and not array emptiness.",
340
+ "A terminal status may still carry a recovery transition: FAILED + retryable failureReason emits it (FE doc Screen 14); non-retryable emits [].",
341
+ "ON_HOLD is a non-terminal status (resolves to PROCESSING or REFUNDED) \u2014 and is NOT the same thing as the KYC ON_HOLD reason; see the note on that field.",
342
+ "CAPTURE_ORPHANED is non-terminal pending the backend rank map (added from review 2026-08-14).",
343
+ "TODO(open-decision): contract OQ7 \u2014 under/over-payment is the biggest unenumerated branch."
344
+ ]
345
+ }
346
+ };
347
+ var STATE_KEYS = Object.keys(TRANSITION_TABLE);
348
+ var DOCUMENTED_ENDPOINTS = [
349
+ "GET /fiat/payment-methods",
350
+ "POST /fiat/quote",
351
+ "POST /fiat/auth",
352
+ "POST /fiat/session",
353
+ "POST /fiat/session/verify",
354
+ "GET /fiat/kyc",
355
+ "POST /fiat/kyc/form",
356
+ "POST /fiat/kyc/document",
357
+ "POST /fiat/instruments",
358
+ "POST /fiat/orders",
359
+ "GET /fiat/orders/:id",
360
+ "GET /fiat/orders/:id/instructions",
361
+ "POST /fiat/orders/:id/confirm-payment",
362
+ "POST /fiat/orders/:id/reference",
363
+ "POST /fiat/orders/:id/cancel",
364
+ "POST /fiat/orders/:id/surface-result"
365
+ ];
366
+ function stateKey(state) {
367
+ switch (state.kind) {
368
+ case "QUOTE":
369
+ return "QUOTE";
370
+ case "FUN_AUTH":
371
+ return "FUN_AUTH";
372
+ case "SESSION_AUTH":
373
+ return "SESSION_AUTH";
374
+ case "PENDING_ORDER":
375
+ return "PENDING_ORDER";
376
+ case "BLOCKED":
377
+ return "BLOCKED";
378
+ case "KYC": {
379
+ if (state.phase === "CAPTURE") return "KYC/CAPTURE";
380
+ if (state.phase === "INPUT_REQUIRED") return "KYC/INPUT_REQUIRED";
381
+ return `KYC/NO_ACTION_REQUIRED:${state.reason}`;
382
+ }
383
+ case "PAYMENT":
384
+ return state.phase === "CAPTURE" ? "PAYMENT/CAPTURE" : "PAYMENT/INSTRUCT";
385
+ case "ORDER":
386
+ return state.phase === "AWAITING_CONFIRMATION" ? "ORDER/AWAITING_CONFIRMATION" : "ORDER/CREATED";
387
+ default: {
388
+ const exhaustive = state;
389
+ throw new Error(`unknown FlowState: ${JSON.stringify(exhaustive)}`);
390
+ }
391
+ }
392
+ }
393
+ var tableEntry = (state) => TRANSITION_TABLE[stateKey(state)];
394
+ function isTerminal(state) {
395
+ const entry = tableEntry(state);
396
+ if (entry.terminalStatuses && state.kind === "ORDER" && state.phase === "CREATED") {
397
+ return entry.terminalStatuses.includes(state.status);
398
+ }
399
+ return entry.terminal;
400
+ }
401
+
402
+ export {
403
+ TABLE_VERSION,
404
+ TERMINAL_ORDER_STATUSES,
405
+ TRANSITION_TABLE,
406
+ STATE_KEYS,
407
+ DOCUMENTED_ENDPOINTS,
408
+ stateKey,
409
+ tableEntry,
410
+ isTerminal
411
+ };
412
+ //# sourceMappingURL=chunk-IUYT5BR2.mjs.map
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=chunk-KIQOUUEZ.mjs.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * GENERATED — do not edit. Run `npm run generate:fixtures` after changing any fixture .json.
3
+ *
4
+ * The .json files in this directory remain the verbatim record copied from the docs; this module
5
+ * inlines them so the package never touches the filesystem (React Native, browsers, CJS output).
6
+ */
7
+ export declare const FIXTURE_DATA: Readonly<Record<string, unknown>>;
8
+ //# sourceMappingURL=data.d.ts.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * fiat-contract — recorded fixtures + loader.
3
+ *
4
+ * Every `.json` file in this directory is an envelope copied **verbatim** from the docs — the FE
5
+ * doc's per-screen "State details" envelopes plus the contract doc's worked example. The one
6
+ * exception is `screen-04-order-review.json`, whose payload the doc publishes *by reference*
7
+ * ("quote: <same shape as Screen 1, refreshed>"): its quote is Screen 1's payload verbatim.
8
+ * They are
9
+ * already synthetic (`q_8f2`, `o_31c`, `eyJ…`, `"…"` placeholders); the redaction rule applies to
10
+ * fixtures too, so never replace a fake session token, bank field, or PII value with a real one.
11
+ *
12
+ * Two fixture shapes exist:
13
+ * - `complete: true` — a full envelope (`state` + `provider` + `transitions`) ⇒ `assertEnvelope`
14
+ * - `complete: false` — a doc excerpt that omits `provider` ⇒ `assertEnvelopeFragment`
15
+ * (`provider` is always present on the wire; the contract doc's worked example just elides it)
16
+ */
17
+ import type { StateKey } from '../table';
18
+ export type FixtureSource = 'FE_DOC' | 'CONTRACT_DOC';
19
+ export interface FixtureMeta {
20
+ id: string;
21
+ file: string;
22
+ source: FixtureSource;
23
+ /** Where in the source doc this envelope is published. */
24
+ docRef: string;
25
+ /** FE doc screen number, when the fixture comes from a screen card. */
26
+ screen?: number;
27
+ stateKey: StateKey;
28
+ /** `false` ⇒ the doc excerpt omits `provider`; validate as a fragment. */
29
+ complete: boolean;
30
+ notes?: readonly string[];
31
+ }
32
+ export declare const FIXTURES: readonly FixtureMeta[];
33
+ /**
34
+ * State entries with no published envelope to record. Declared so missing coverage is visible
35
+ * instead of silent — the fixture test asserts coverage equals (all state keys − these).
36
+ */
37
+ export declare const FIXTURE_COVERAGE_GAPS: readonly {
38
+ stateKey: StateKey;
39
+ reason: string;
40
+ }[];
41
+ export declare const fixtureMeta: (id: string) => FixtureMeta;
42
+ /**
43
+ * Raw envelope — deliberately `unknown`, so callers validate before use.
44
+ *
45
+ * Reads from the generated `data.ts`: no filesystem, so this works under React Native, in a
46
+ * browser, and in both CJS and ESM output. A fresh deep copy each call, so a consumer mutating a
47
+ * fixture in one test cannot poison another.
48
+ */
49
+ export declare function loadFixture(id: string): unknown;
50
+ export interface LoadedFixture {
51
+ meta: FixtureMeta;
52
+ json: unknown;
53
+ }
54
+ export declare function loadFixtures(): LoadedFixture[];
55
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * fiat-contract — the published contract between `fun-backend` (emits envelopes) and
3
+ * `funkit`/`connect-core` (renders them). Types + zod schemas + the transition table as data +
4
+ * recorded fixtures + assertion helpers. No runtime logic beyond validation and assertion.
5
+ *
6
+ * The package version IS the capability handshake's table version (`TABLE_VERSION`).
7
+ */
8
+ export * from './types';
9
+ export * from './schemas';
10
+ export * from './table';
11
+ export * from './assert';
12
+ export { FIXTURES, FIXTURE_COVERAGE_GAPS, fixtureMeta, loadFixture, loadFixtures, type FixtureMeta, type FixtureSource, type LoadedFixture, } from './fixtures/index';
13
+ //# sourceMappingURL=index.d.ts.map