@fun-xyz/fiat-contract 0.7.0 → 0.9.0

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.
package/README.md CHANGED
@@ -10,7 +10,7 @@ Four things, zero runtime logic beyond validation:
10
10
  | `src/types.ts` | `FlowState` · `Transition` · `FailureReason` · `Surface` · `Instructions` · `FormDescriptor` · `OrderStatus` · `FiatStepResponse` |
11
11
  | `src/schemas.ts` | zod mirrors of every type — the single runtime validator |
12
12
  | `src/table.ts` | the transition table **as data**: per state, its legal transition set, the states any call from it may return, and `terminal: boolean` |
13
- | `src/assert.ts` + `src/fixtures/` | `assertFiatStepResponse` · `assertLegalEmission` · `assertLegalReturn` · `walkTable` · fixture loader + 16 recorded step responses (inlined as data — no filesystem, so React Native can bundle it) |
13
+ | `src/assert.ts` + `src/fixtures/` | `assertFiatStepResponse` · `assertLegalEmission` · `assertLegalReturn` · `walkTable` · fixture loader + 18 recorded step responses (inlined as data — no filesystem, so React Native can bundle it) |
14
14
 
15
15
  ## Three entry points — production vs test-time
16
16
 
@@ -18,11 +18,11 @@ Four things, zero runtime logic beyond validation:
18
18
  | --- | --- | --- | --- |
19
19
  | `@fun-xyz/fiat-contract/types` | **0.1 KB** (types erase) | every type; no runtime values | production, both repos |
20
20
  | `@fun-xyz/fiat-contract/table` | **13.8 KB**, zero deps | `TRANSITION_TABLE`, `stateKey`, `tableEntry`, `isTerminal`, `walkTable`, `TABLE_VERSION`, `TERMINAL_ORDER_STATUSES`, `DOCUMENTED_ENDPOINTS`, `UNOFFERED_ENDPOINTS`, `UNREACHABLE_STATES`, `unofferedEndpoints`, `unreachableStates` | **production frontend** + backend |
21
- | `@fun-xyz/fiat-contract` | 47.6 KB, needs zod | the above + 46 zod schemas + assertions + 16 fixtures | tests, and backend dev/test guards |
21
+ | `@fun-xyz/fiat-contract` | 47.6 KB, needs zod | the above + 46 zod schemas + assertions + 18 fixtures | tests, and backend dev/test guards |
22
22
 
23
23
  `./table` is not a micro-optimisation. Terminality is table data a **shipped** client must read
24
24
  (litmus rule 3 — clients never infer it), and Metro has no cross-module tree-shaking on by default,
25
- so importing `isTerminal` from the root would ship zod and all 16 fixtures into a React Native
25
+ so importing `isTerminal` from the root would ship zod and all 18 fixtures into a React Native
26
26
  bundle. Two CI jobs hold that line: `consumer` asserts requiring `./table` never loads zod into the
27
27
  process, and `metro` bundles `./table` with real Metro — in **both** package-exports modes, since
28
28
  Metro before RN 0.79 ignores `exports` entirely — then greps the emitted bundle for zod, fixtures and
@@ -31,7 +31,11 @@ schemas and executes it.
31
31
  Source of truth: [Fiat Client Contract](https://app.notion.com/p/3b9fc3b2a002815eb270fa4c818268cc)
32
32
  (§The fiat step response · §Conformance package · §split `InputSpec` — ACCEPTED) and
33
33
  [Fiat Frontend — State Machine & Screen Map](https://app.notion.com/p/3bbfc3b2a00281c994c2cebd17b1d6d3)
34
- (✅ Decisions · per-screen State details · Event bindings per flow state).
34
+ (✅ Decisions · per-screen State details · Event bindings per flow state), plus
35
+ [Fiat KYC — User Flow & API Interfaces](https://app.notion.com/p/3cefc3b2a00281c88e7fe771f0c64332)
36
+ for quote-bound KYC states and endpoints, and
37
+ [Fiat Routing Engine — Provider Selection, Ranking & Commit](https://app.notion.com/p/3d1fc3b2a00281f2b604c20869ac420a)
38
+ for quote-screen limits, provider auth channels, and blocked reasons.
35
39
  Tracking: [Headless Fiat Onramp](https://linear.app/funxyz/project/headless-fiat-onramp-0147f4102399) ·
36
40
  this package is [ENG-5268](https://linear.app/funxyz/issue/ENG-5268).
37
41
 
@@ -52,6 +56,13 @@ this package is [ENG-5268](https://linear.app/funxyz/issue/ENG-5268).
52
56
  string; `GET /fiat/orders/:id` under `SUBMIT` is legal.
53
57
  5. **Fixtures stay synthetic.** `q_8f2`, `o_31c`, `eyJ…`, `"…"` — never paste a real session token,
54
58
  bank field, or PII value into a fixture. Redaction applies to fixtures too.
59
+ 6. **Surface kind names the renderer; provider selects the adapter.** Sumsub capture is
60
+ `{ kind: 'KYC_SDK', provider: 'SUMSUB' }`. It is not `URL_EMBED`: the client mounts an SDK,
61
+ not a URL or iframe.
62
+ 7. **Quote-screen bounds are explicit exact-money strings.** `QUOTE.limits` carries the minimum and
63
+ current routing maximum; clients do not infer either from provider metadata or floating point.
64
+ 8. **Provider auth is capability-shaped.** SMS uses `SMS_OTP`; provider-owned auth uses
65
+ `PROVIDER_RENDERED` with an optional `AUTH_COMPONENT` surface rather than provider-specific channels.
55
66
 
56
67
  ## Versioning — the package version IS the table version
57
68
 
@@ -247,7 +258,7 @@ The table is data, so a runtime walk proves every state resolves to a screen —
247
258
  exhaustiveness alone, because it also fails when a *new* state is added to the contract.
248
259
 
249
260
  ```ts
250
- import { walkTable, isTerminal, TRANSITION_TABLE } from '@fun-xyz/fiat-contract';
261
+ import { assertFixture, walkTable, isTerminal, TRANSITION_TABLE } from '@fun-xyz/fiat-contract';
251
262
 
252
263
  it('every contract state resolves to a screen', () => {
253
264
  walkTable((entry) => {
@@ -256,7 +267,7 @@ it('every contract state resolves to a screen', () => {
256
267
  });
257
268
 
258
269
  // Terminality is table data — never `transitions.length === 0`
259
- isTerminal({ kind: 'KYC', phase: 'NO_ACTION_REQUIRED', reason: 'ON_HOLD' }); // false: empty, not over
270
+ isTerminal(assertFixture('screen-10-kyc-on-hold').stepResponse.state); // false: empty, not over
260
271
  isTerminal({ kind: 'ORDER', phase: 'CREATED', status: 'SETTLED' }); // true
261
272
  isTerminal({ kind: 'ORDER', phase: 'CREATED', status: 'PROCESSING' }); // false
262
273
 
@@ -437,17 +448,15 @@ The current documented shape is what ships; none of these are settled here.
437
448
  | --- | --- |
438
449
  | `orderId` placement | Both: optional on `PAYMENT{INSTRUCT}` (FE doc v0) **and** optional beside `state` (contract worked example). One fixture of each. |
439
450
  | `PENDING_ORDER` removal | Kind ships, with the removal proposal flagged on the type, the table entry, and the fixture. Screen 12 stays frozen. |
440
- | `[OQ7]` failure enumeration | The published `FailureReason` taxonomy only. Expired instructions, partial payment, per-rail cancel eligibility, and terminal-vs-escalating rejections are flagged unenumerated. |
441
- | Screen 11 escalation trigger | Encoded as published (`SUBMIT GET /fiat/kyc` + `params: {tier}`) with the doc's own warning that a GET carrying params is not a real shape. |
451
+ | `[OQ7]` failure enumeration | The published `FailureReason` taxonomy only. Expired instructions, partial payment, and per-rail cancel eligibility remain unenumerated; KYC rejection is terminal only for an explicit non-retryable outcome. |
452
+ | Pre-order KYC sequencing | KYC states carry their quote; SDK submission reports to `POST /fiat/kyc/capture`; pending review polls the quote resource. There is no `GET /fiat/kyc`. |
442
453
  | Card capture report target | Both topologies are legal in the table (`POST /fiat/orders` for capture-then-order; `POST /fiat/orders/:id/surface-result` otherwise) pending the Transak answer. |
443
454
  | Cancel placement | `cancel` is legal on `PAYMENT{INSTRUCT}` and `ORDER{CREATED}`, marked conditional on the placement decision. |
444
455
  | `FUN_AUTH` shape | `challenge: Record<string, JsonValue>`; the table entry is `docStatus: 'UNSPECIFIED'`, so `assertLegalEmission` reports it unjudgeable instead of guessing. |
445
456
  | `statusHistory` element shape | The documented minimum (`{status}`) — no invented timestamps. |
446
457
  | `QR_IMAGE` instruction | In the union per §The fiat step response, flagged against OQ1's "deliberately not pre-declared". |
447
458
 
448
- Two fixture gaps are declared in `FIXTURE_COVERAGE_GAPS` rather than filled with invented step responses:
449
- `FUN_AUTH` (shape owned by the auth spike) and `KYC/CAPTURE` (Screen 8 tombstone, dropped from v1).
450
- Every other state has a recorded step response.
459
+ `FUN_AUTH` is the only fixture gap declared in `FIXTURE_COVERAGE_GAPS`; its shape remains owned by the auth spike. Every other state has a recorded step response.
451
460
 
452
461
  ## Not decided here — needs a human
453
462
 
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.7.0";
2
+ var version = "0.9.0";
3
3
 
4
4
  // src/table.ts
5
5
  var TABLE_VERSION = version;
@@ -105,6 +105,8 @@ var TRANSITION_TABLE = {
105
105
  ],
106
106
  mayReturn: [
107
107
  "SESSION_AUTH",
108
+ "QUOTE",
109
+ "PAYMENT/CAPTURE",
108
110
  ...KYC_ANY,
109
111
  "ORDER/AWAITING_CONFIRMATION",
110
112
  "PENDING_ORDER"
@@ -125,21 +127,23 @@ var TRANSITION_TABLE = {
125
127
  {
126
128
  id: "capture",
127
129
  mode: "CLIENT_SURFACE",
128
- endpoint: "GET /fiat/kyc",
129
- note: 'report target per FE bindings ("POST surface report \u2192 GET /fiat/kyc") and contract Flow C.'
130
+ endpoint: "POST /fiat/kyc/capture",
131
+ note: "Reports SDK submission; provider eligibility is re-evaluated through quote orchestration."
130
132
  }
131
133
  ],
132
134
  mayReturn: [
135
+ "KYC/CAPTURE",
133
136
  "KYC/INPUT_REQUIRED",
134
137
  "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
135
138
  "KYC/NO_ACTION_REQUIRED:ON_HOLD",
136
139
  "KYC/NO_ACTION_REQUIRED:REJECTED",
140
+ "QUOTE",
141
+ "PAYMENT/CAPTURE",
142
+ "PAYMENT/INSTRUCT",
137
143
  "ORDER/AWAITING_CONFIRMATION"
138
144
  ],
139
145
  terminal: false,
140
- notes: [
141
- "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."
142
- ]
146
+ notes: ["Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter."]
143
147
  },
144
148
  "KYC/INPUT_REQUIRED": {
145
149
  key: "KYC/INPUT_REQUIRED",
@@ -151,16 +155,20 @@ var TRANSITION_TABLE = {
151
155
  { id: "submit_round", mode: "SUBMIT", endpoint: "POST /fiat/kyc/form" }
152
156
  ],
153
157
  mayReturn: [
158
+ "KYC/CAPTURE",
154
159
  "KYC/INPUT_REQUIRED",
155
160
  "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
156
161
  "KYC/NO_ACTION_REQUIRED:ON_HOLD",
157
162
  "KYC/NO_ACTION_REQUIRED:REJECTED",
163
+ "QUOTE",
164
+ "PAYMENT/CAPTURE",
165
+ "PAYMENT/INSTRUCT",
158
166
  "ORDER/AWAITING_CONFIRMATION"
159
167
  ],
160
168
  terminal: false,
161
169
  notes: [
162
170
  "One POST per round; conditional requirements may open another round.",
163
- "FILE fields stream to POST /fiat/kyc/document \u2014 an upload path the harness owns, not a transition.",
171
+ "FILE fields use POST /fiat/kyc/document to obtain or report a direct provider/vendor upload; document bytes never transit Fun. The harness owns this auxiliary path, not a transition.",
164
172
  "Hosted KYC links arrive as HOSTED_LINK form fields, not as a Surface."
165
173
  ]
166
174
  },
@@ -171,12 +179,16 @@ var TRANSITION_TABLE = {
171
179
  reason: "IN_REVIEW",
172
180
  screens: [9],
173
181
  docStatus: "SPECIFIED",
174
- allowedTransitions: [{ id: "poll", mode: "AWAIT", endpoint: "GET /fiat/kyc" }],
182
+ allowedTransitions: [{ id: "poll", mode: "AWAIT", endpoint: "GET /fiat/quotes/:quoteRef" }],
175
183
  mayReturn: [
176
184
  "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
185
+ "KYC/CAPTURE",
177
186
  "KYC/INPUT_REQUIRED",
178
187
  "KYC/NO_ACTION_REQUIRED:ON_HOLD",
179
188
  "KYC/NO_ACTION_REQUIRED:REJECTED",
189
+ "QUOTE",
190
+ "PAYMENT/CAPTURE",
191
+ "PAYMENT/INSTRUCT",
180
192
  "ORDER/AWAITING_CONFIRMATION"
181
193
  ],
182
194
  terminal: false,
@@ -206,20 +218,11 @@ var TRANSITION_TABLE = {
206
218
  reason: "REJECTED",
207
219
  screens: [11],
208
220
  docStatus: "SPECIFIED",
209
- allowedTransitions: [
210
- {
211
- id: "escalate",
212
- mode: "SUBMIT",
213
- endpoint: "GET /fiat/kyc",
214
- when: "an escalation round is offered",
215
- 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."
216
- }
217
- ],
218
- mayReturn: KYC_ANY,
219
- terminal: false,
221
+ allowedTransitions: [],
222
+ mayReturn: [],
223
+ terminal: true,
220
224
  notes: [
221
- "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.",
222
- "TODO(open-decision): contract OQ7 \u2014 terminal vs escalating rejections are not enumerated."
225
+ "Only explicit non-retryable outcomes reach REJECTED. Correctable failures return CAPTURE or INPUT_REQUIRED; provider-specific declines re-route before becoming user rejection."
223
226
  ]
224
227
  },
225
228
  PENDING_ORDER: {
@@ -381,8 +384,10 @@ var DOCUMENTED_ENDPOINTS = [
381
384
  "POST /fiat/payment-session",
382
385
  "POST /fiat/session",
383
386
  "POST /fiat/session/verify",
384
- "GET /fiat/kyc",
387
+ "GET /fiat/quotes/:quoteRef",
385
388
  "POST /fiat/kyc/form",
389
+ "POST /fiat/kyc/capture",
390
+ "POST /fiat/kyc/sdk-token",
386
391
  "POST /fiat/kyc/document",
387
392
  "POST /fiat/instruments",
388
393
  "POST /fiat/orders",
@@ -394,13 +399,17 @@ var DOCUMENTED_ENDPOINTS = [
394
399
  "POST /fiat/orders/:id/surface-result"
395
400
  ];
396
401
  var UNOFFERED_ENDPOINTS = [
402
+ {
403
+ endpoint: "POST /fiat/kyc/sdk-token",
404
+ reason: "SDK token refresh is adapter-driven rather than a flow transition."
405
+ },
397
406
  {
398
407
  endpoint: "GET /fiat/payment-methods",
399
408
  reason: "Discovery, called before any flow state exists \u2014 there is no state to hang it off."
400
409
  },
401
410
  {
402
411
  endpoint: "POST /fiat/kyc/document",
403
- reason: "FILE fields stream to it directly; the harness owns that upload path and it is not a transition (see KYC/INPUT_REQUIRED notes)."
412
+ reason: "FILE fields use it for a direct provider/vendor upload handshake and completion report; the harness owns that auxiliary path and it is not a transition (see KYC/INPUT_REQUIRED notes)."
404
413
  },
405
414
  {
406
415
  endpoint: "POST /fiat/instruments",
@@ -491,4 +500,4 @@ export {
491
500
  tableEntry,
492
501
  isTerminal
493
502
  };
494
- //# sourceMappingURL=chunk-OGNYHIOE.mjs.map
503
+ //# sourceMappingURL=chunk-5QGVY5Z4.mjs.map
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * fiat-contract — recorded fixtures + loader.
3
3
  *
4
- * Every `.json` file in this directory is a step response copied **verbatim** from the docs the
5
- * FE doc's per-screen "State details" responses plus the contract doc's worked example. The one
6
- * exception is `screen-04-order-review.json`, whose payload the doc publishes *by reference*
4
+ * Every `.json` file in this directory records a step response from the source named in its
5
+ * metadata. `screen-04-order-review.json` is the one payload published *by reference*
7
6
  * ("quote: <same shape as Screen 1, refreshed>"): its quote is Screen 1's payload verbatim.
8
7
  * They are
9
8
  * already synthetic (`q_8f2`, `o_31c`, `eyJ…`, `"…"` placeholders); the redaction rule applies to
@@ -15,7 +14,7 @@
15
14
  * wire, and the contract has no half-response shape.
16
15
  */
17
16
  import type { StateKey } from '../table';
18
- export type FixtureSource = 'FE_DOC' | 'CONTRACT_DOC';
17
+ export type FixtureSource = 'FE_DOC' | 'CONTRACT_DOC' | 'KYC_DOC';
19
18
  export interface FixtureMeta {
20
19
  id: string;
21
20
  file: string;