@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 +20 -11
- package/dist/{chunk-OGNYHIOE.mjs → chunk-5QGVY5Z4.mjs} +33 -24
- package/dist/fixtures/index.d.ts +3 -4
- package/dist/index.js +262 -96
- package/dist/index.mjs +231 -74
- package/dist/schemas.d.ts +3 -1
- package/dist/table.d.ts +2 -3
- package/dist/table.js +32 -23
- package/dist/table.mjs +1 -1
- package/dist/types.d.ts +23 -23
- package/package.json +1 -1
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 +
|
|
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 +
|
|
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
|
|
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(
|
|
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
|
|
441
|
-
|
|
|
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
|
-
|
|
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.
|
|
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: "
|
|
129
|
-
note:
|
|
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
|
|
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/
|
|
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
|
-
|
|
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
|
-
"
|
|
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/
|
|
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
|
|
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-
|
|
503
|
+
//# sourceMappingURL=chunk-5QGVY5Z4.mjs.map
|
package/dist/fixtures/index.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* fiat-contract — recorded fixtures + loader.
|
|
3
3
|
*
|
|
4
|
-
* Every `.json` file in this directory
|
|
5
|
-
*
|
|
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;
|