@fun-xyz/fiat-contract 0.7.0 → 0.8.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,9 @@ 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.
35
37
  Tracking: [Headless Fiat Onramp](https://linear.app/funxyz/project/headless-fiat-onramp-0147f4102399) ·
36
38
  this package is [ENG-5268](https://linear.app/funxyz/issue/ENG-5268).
37
39
 
@@ -52,6 +54,9 @@ this package is [ENG-5268](https://linear.app/funxyz/issue/ENG-5268).
52
54
  string; `GET /fiat/orders/:id` under `SUBMIT` is legal.
53
55
  5. **Fixtures stay synthetic.** `q_8f2`, `o_31c`, `eyJ…`, `"…"` — never paste a real session token,
54
56
  bank field, or PII value into a fixture. Redaction applies to fixtures too.
57
+ 6. **Surface kind names the renderer; provider selects the adapter.** Sumsub capture is
58
+ `{ kind: 'KYC_SDK', provider: 'SUMSUB' }`. It is not `URL_EMBED`: the client mounts an SDK,
59
+ not a URL or iframe.
55
60
 
56
61
  ## Versioning — the package version IS the table version
57
62
 
@@ -247,7 +252,7 @@ The table is data, so a runtime walk proves every state resolves to a screen —
247
252
  exhaustiveness alone, because it also fails when a *new* state is added to the contract.
248
253
 
249
254
  ```ts
250
- import { walkTable, isTerminal, TRANSITION_TABLE } from '@fun-xyz/fiat-contract';
255
+ import { assertFixture, walkTable, isTerminal, TRANSITION_TABLE } from '@fun-xyz/fiat-contract';
251
256
 
252
257
  it('every contract state resolves to a screen', () => {
253
258
  walkTable((entry) => {
@@ -256,7 +261,7 @@ it('every contract state resolves to a screen', () => {
256
261
  });
257
262
 
258
263
  // Terminality is table data — never `transitions.length === 0`
259
- isTerminal({ kind: 'KYC', phase: 'NO_ACTION_REQUIRED', reason: 'ON_HOLD' }); // false: empty, not over
264
+ isTerminal(assertFixture('screen-10-kyc-on-hold').stepResponse.state); // false: empty, not over
260
265
  isTerminal({ kind: 'ORDER', phase: 'CREATED', status: 'SETTLED' }); // true
261
266
  isTerminal({ kind: 'ORDER', phase: 'CREATED', status: 'PROCESSING' }); // false
262
267
 
@@ -437,17 +442,15 @@ The current documented shape is what ships; none of these are settled here.
437
442
  | --- | --- |
438
443
  | `orderId` placement | Both: optional on `PAYMENT{INSTRUCT}` (FE doc v0) **and** optional beside `state` (contract worked example). One fixture of each. |
439
444
  | `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. |
445
+ | `[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. |
446
+ | 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
447
  | 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
448
  | Cancel placement | `cancel` is legal on `PAYMENT{INSTRUCT}` and `ORDER{CREATED}`, marked conditional on the placement decision. |
444
449
  | `FUN_AUTH` shape | `challenge: Record<string, JsonValue>`; the table entry is `docStatus: 'UNSPECIFIED'`, so `assertLegalEmission` reports it unjudgeable instead of guessing. |
445
450
  | `statusHistory` element shape | The documented minimum (`{status}`) — no invented timestamps. |
446
451
  | `QR_IMAGE` instruction | In the union per §The fiat step response, flagged against OQ1's "deliberately not pre-declared". |
447
452
 
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.
453
+ `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
454
 
452
455
  ## Not decided here — needs a human
453
456
 
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.7.0";
2
+ var version = "0.8.0";
3
3
 
4
4
  // src/table.ts
5
5
  var TABLE_VERSION = version;
@@ -125,21 +125,23 @@ var TRANSITION_TABLE = {
125
125
  {
126
126
  id: "capture",
127
127
  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.'
128
+ endpoint: "POST /fiat/kyc/capture",
129
+ note: "Reports SDK submission; provider eligibility is re-evaluated through quote orchestration."
130
130
  }
131
131
  ],
132
132
  mayReturn: [
133
+ "KYC/CAPTURE",
133
134
  "KYC/INPUT_REQUIRED",
134
135
  "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
135
136
  "KYC/NO_ACTION_REQUIRED:ON_HOLD",
136
137
  "KYC/NO_ACTION_REQUIRED:REJECTED",
138
+ "QUOTE",
139
+ "PAYMENT/CAPTURE",
140
+ "PAYMENT/INSTRUCT",
137
141
  "ORDER/AWAITING_CONFIRMATION"
138
142
  ],
139
143
  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
- ]
144
+ notes: ["Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter."]
143
145
  },
144
146
  "KYC/INPUT_REQUIRED": {
145
147
  key: "KYC/INPUT_REQUIRED",
@@ -151,16 +153,20 @@ var TRANSITION_TABLE = {
151
153
  { id: "submit_round", mode: "SUBMIT", endpoint: "POST /fiat/kyc/form" }
152
154
  ],
153
155
  mayReturn: [
156
+ "KYC/CAPTURE",
154
157
  "KYC/INPUT_REQUIRED",
155
158
  "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
156
159
  "KYC/NO_ACTION_REQUIRED:ON_HOLD",
157
160
  "KYC/NO_ACTION_REQUIRED:REJECTED",
161
+ "QUOTE",
162
+ "PAYMENT/CAPTURE",
163
+ "PAYMENT/INSTRUCT",
158
164
  "ORDER/AWAITING_CONFIRMATION"
159
165
  ],
160
166
  terminal: false,
161
167
  notes: [
162
168
  "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.",
169
+ "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
170
  "Hosted KYC links arrive as HOSTED_LINK form fields, not as a Surface."
165
171
  ]
166
172
  },
@@ -171,12 +177,16 @@ var TRANSITION_TABLE = {
171
177
  reason: "IN_REVIEW",
172
178
  screens: [9],
173
179
  docStatus: "SPECIFIED",
174
- allowedTransitions: [{ id: "poll", mode: "AWAIT", endpoint: "GET /fiat/kyc" }],
180
+ allowedTransitions: [{ id: "poll", mode: "AWAIT", endpoint: "GET /fiat/quotes/:quoteRef" }],
175
181
  mayReturn: [
176
182
  "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
183
+ "KYC/CAPTURE",
177
184
  "KYC/INPUT_REQUIRED",
178
185
  "KYC/NO_ACTION_REQUIRED:ON_HOLD",
179
186
  "KYC/NO_ACTION_REQUIRED:REJECTED",
187
+ "QUOTE",
188
+ "PAYMENT/CAPTURE",
189
+ "PAYMENT/INSTRUCT",
180
190
  "ORDER/AWAITING_CONFIRMATION"
181
191
  ],
182
192
  terminal: false,
@@ -206,20 +216,11 @@ var TRANSITION_TABLE = {
206
216
  reason: "REJECTED",
207
217
  screens: [11],
208
218
  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,
219
+ allowedTransitions: [],
220
+ mayReturn: [],
221
+ terminal: true,
220
222
  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."
223
+ "Only explicit non-retryable outcomes reach REJECTED. Correctable failures return CAPTURE or INPUT_REQUIRED; provider-specific declines re-route before becoming user rejection."
223
224
  ]
224
225
  },
225
226
  PENDING_ORDER: {
@@ -381,8 +382,10 @@ var DOCUMENTED_ENDPOINTS = [
381
382
  "POST /fiat/payment-session",
382
383
  "POST /fiat/session",
383
384
  "POST /fiat/session/verify",
384
- "GET /fiat/kyc",
385
+ "GET /fiat/quotes/:quoteRef",
385
386
  "POST /fiat/kyc/form",
387
+ "POST /fiat/kyc/capture",
388
+ "POST /fiat/kyc/sdk-token",
386
389
  "POST /fiat/kyc/document",
387
390
  "POST /fiat/instruments",
388
391
  "POST /fiat/orders",
@@ -394,13 +397,17 @@ var DOCUMENTED_ENDPOINTS = [
394
397
  "POST /fiat/orders/:id/surface-result"
395
398
  ];
396
399
  var UNOFFERED_ENDPOINTS = [
400
+ {
401
+ endpoint: "POST /fiat/kyc/sdk-token",
402
+ reason: "SDK token refresh is adapter-driven rather than a flow transition."
403
+ },
397
404
  {
398
405
  endpoint: "GET /fiat/payment-methods",
399
406
  reason: "Discovery, called before any flow state exists \u2014 there is no state to hang it off."
400
407
  },
401
408
  {
402
409
  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)."
410
+ 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
411
  },
405
412
  {
406
413
  endpoint: "POST /fiat/instruments",
@@ -491,4 +498,4 @@ export {
491
498
  tableEntry,
492
499
  isTerminal
493
500
  };
494
- //# sourceMappingURL=chunk-OGNYHIOE.mjs.map
501
+ //# sourceMappingURL=chunk-LVVQ46YG.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;