@fun-xyz/fiat-contract 0.5.0 → 0.7.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 +34 -34
- package/dist/assert.d.ts +2 -2
- package/dist/{chunk-3R5GGUAK.mjs → chunk-OGNYHIOE.mjs} +84 -5
- package/dist/fixtures/index.d.ts +7 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +203 -22
- package/dist/index.mjs +130 -20
- package/dist/schemas.d.ts +5 -2
- package/dist/table.d.ts +41 -6
- package/dist/table.js +84 -5
- package/dist/table.mjs +11 -3
- package/dist/types.d.ts +60 -33
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# fiat-contract
|
|
2
2
|
|
|
3
|
-
The **published contract** between `fun-backend` (emits
|
|
3
|
+
The **published contract** between `fun-backend` (emits step responses) and `funkit`/`connect-core`
|
|
4
4
|
(renders them). Both repos test against it; neither owns it.
|
|
5
5
|
|
|
6
6
|
Four things, zero runtime logic beyond validation:
|
|
@@ -10,14 +10,14 @@ 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
|
|
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) |
|
|
14
14
|
|
|
15
15
|
## Three entry points — production vs test-time
|
|
16
16
|
|
|
17
17
|
| Import | Weight | Contains | Used by |
|
|
18
18
|
| --- | --- | --- | --- |
|
|
19
19
|
| `@fun-xyz/fiat-contract/types` | **0.1 KB** (types erase) | every type; no runtime values | production, both repos |
|
|
20
|
-
| `@fun-xyz/fiat-contract/table` | **13.8 KB**, zero deps | `TRANSITION_TABLE`, `stateKey`, `tableEntry`, `isTerminal`, `walkTable`, `TABLE_VERSION`, `TERMINAL_ORDER_STATUSES`, `DOCUMENTED_ENDPOINTS` | **production frontend** + backend |
|
|
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
21
|
| `@fun-xyz/fiat-contract` | 47.6 KB, needs zod | the above + 46 zod schemas + assertions + 16 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
|
|
@@ -29,7 +29,7 @@ Metro before RN 0.79 ignores `exports` entirely — then greps the emitted bundl
|
|
|
29
29
|
schemas and executes it.
|
|
30
30
|
|
|
31
31
|
Source of truth: [Fiat Client Contract](https://app.notion.com/p/3b9fc3b2a002815eb270fa4c818268cc)
|
|
32
|
-
(§The
|
|
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
34
|
(✅ Decisions · per-screen State details · Event bindings per flow state).
|
|
35
35
|
Tracking: [Headless Fiat Onramp](https://linear.app/funxyz/project/headless-fiat-onramp-0147f4102399) ·
|
|
@@ -39,7 +39,7 @@ this package is [ENG-5268](https://linear.app/funxyz/issue/ENG-5268).
|
|
|
39
39
|
|
|
40
40
|
1. **Errors are fields, never states.** Every fallible state carries `error?: FailureReason`.
|
|
41
41
|
Stay-on-screen ⇒ an error field. Change-screen ⇒ a different state (session expiry just returns
|
|
42
|
-
a `SESSION_AUTH`
|
|
42
|
+
a `SESSION_AUTH` step response — there is no error routing table).
|
|
43
43
|
2. **`params` = server literals · `inputs` = collected specs · `expects` = injected surface
|
|
44
44
|
results.** `body = {…params, …collected(inputs), …injected(expects)}`; a key collision across
|
|
45
45
|
the three is a contract violation, not last-write-wins. zod rejects a `FieldSpec` hiding in
|
|
@@ -74,11 +74,11 @@ removals also ride a minor, with no deprecated aliases kept — consumers pin an
|
|
|
74
74
|
import { assertFiatStepResponse, assertLegalEmission, walkTable } from '@fun-xyz/fiat-contract';
|
|
75
75
|
|
|
76
76
|
assertLegalEmission(state, transitions); // per emission: adapter conformance
|
|
77
|
-
assertFiatStepResponse(outgoing); // outgoing-
|
|
77
|
+
assertFiatStepResponse(outgoing); // outgoing step-response validation in dev/test
|
|
78
78
|
const owed = walkTable((entry) => entry.allowedTransitions); // what the adapter must emit
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Its own suites: adapter conformance (fixture-driven + property-generated states), outgoing-
|
|
81
|
+
Its own suites: adapter conformance (fixture-driven + property-generated states), outgoing step-response
|
|
82
82
|
validation, and the scheduled provider-sandbox drift run diffed against `src/fixtures`.
|
|
83
83
|
|
|
84
84
|
**`connect-core`** — the harness obeys `transitions` for sequencing and owns rendering.
|
|
@@ -91,7 +91,7 @@ FIXTURES.forEach(({ id }) => renderCold(loadFixture(id))); // stale-rule
|
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
Its own suites: exhaustive `computePage` table-walk, fixture-driven cold-render tests, harness units
|
|
94
|
-
(any-state handling, same-state error re-entry retains form values, one-live-
|
|
94
|
+
(any-state handling, same-state error re-entry retains form values, one-live-response focus gating,
|
|
95
95
|
idempotency-key reuse).
|
|
96
96
|
|
|
97
97
|
## Usage
|
|
@@ -101,7 +101,7 @@ so these examples cannot drift from the API.
|
|
|
101
101
|
|
|
102
102
|
### Production: a backend route handler
|
|
103
103
|
|
|
104
|
-
The backend *builds*
|
|
104
|
+
The backend *builds* step responses, so its production use is entirely compile-time — the types are the
|
|
105
105
|
verification. Runtime assertions stay behind a dev/test guard.
|
|
106
106
|
|
|
107
107
|
```ts
|
|
@@ -142,17 +142,17 @@ production-safe: types erase, `./table` is 13.8 KB with no zod.
|
|
|
142
142
|
import { isTerminal, stateKey } from '@fun-xyz/fiat-contract/table';
|
|
143
143
|
import type { FiatStepResponse } from '@fun-xyz/fiat-contract/types';
|
|
144
144
|
|
|
145
|
-
export function FiatScreen({
|
|
146
|
-
const page = computePage(stateKey(
|
|
147
|
-
const done = isTerminal(
|
|
145
|
+
export function FiatScreen({ stepResponse }: { stepResponse: FiatStepResponse }) {
|
|
146
|
+
const page = computePage(stateKey(stepResponse.state), clientLocal); // client owns state → screen
|
|
147
|
+
const done = isTerminal(stepResponse.state); // table data, never inferred
|
|
148
148
|
|
|
149
|
-
if (
|
|
150
|
-
return render(page,
|
|
149
|
+
if (stepResponse.state.kind === 'PAYMENT' && stepResponse.state.phase === 'INSTRUCT') {
|
|
150
|
+
return render(page, stepResponse.state.instructions); // narrowed: instructions exists here
|
|
151
151
|
}
|
|
152
|
-
if (
|
|
153
|
-
return render(page, { status:
|
|
152
|
+
if (stepResponse.state.kind === 'ORDER' && stepResponse.state.phase === 'CREATED') {
|
|
153
|
+
return render(page, { status: stepResponse.state.status, done });
|
|
154
154
|
}
|
|
155
|
-
return render(page,
|
|
155
|
+
return render(page, stepResponse.state);
|
|
156
156
|
}
|
|
157
157
|
```
|
|
158
158
|
|
|
@@ -165,11 +165,11 @@ re-derive that rule locally.
|
|
|
165
165
|
```ts
|
|
166
166
|
import type { FiatStepResponse, Transition } from '@fun-xyz/fiat-contract/types';
|
|
167
167
|
|
|
168
|
-
function useTransitions(
|
|
168
|
+
function useTransitions(stepResponse: FiatStepResponse) {
|
|
169
169
|
return {
|
|
170
|
-
ctas:
|
|
171
|
-
poll:
|
|
172
|
-
surface:
|
|
170
|
+
ctas: stepResponse.transitions.filter((t) => t.mode === 'SUBMIT'), // render buttons
|
|
171
|
+
poll: stepResponse.transitions.find((t) => t.mode === 'AWAIT'), // harness schedules
|
|
172
|
+
surface: stepResponse.transitions.find((t) => t.mode === 'CLIENT_SURFACE'), // harness mounts
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
|
|
@@ -191,7 +191,7 @@ Screens never inspect the array themselves; they receive `ctas` and bind labels
|
|
|
191
191
|
|
|
192
192
|
These import from the root, which carries zod. Test-time only.
|
|
193
193
|
|
|
194
|
-
### Validate
|
|
194
|
+
### Validate a step response at the boundary
|
|
195
195
|
|
|
196
196
|
`assertFiatStepResponse` parses and returns a typed step response, or throws `ContractViolation`
|
|
197
197
|
listing every problem. Use it on the way out of `fun-backend` (dev/test) and on the way in to
|
|
@@ -201,10 +201,10 @@ listing every problem. Use it on the way out of `fun-backend` (dev/test) and on
|
|
|
201
201
|
import { assertFiatStepResponse, ContractViolation } from '@fun-xyz/fiat-contract';
|
|
202
202
|
|
|
203
203
|
try {
|
|
204
|
-
const
|
|
204
|
+
const stepResponse = assertFiatStepResponse(await res.json());
|
|
205
205
|
// ^? FiatStepResponse — state is a narrowable discriminated union from here on
|
|
206
|
-
if (
|
|
207
|
-
render(
|
|
206
|
+
if (stepResponse.state.kind === 'PAYMENT' && stepResponse.state.phase === 'INSTRUCT') {
|
|
207
|
+
render(stepResponse.state.instructions); // narrowed: instructions exists, quote does not
|
|
208
208
|
}
|
|
209
209
|
} catch (err) {
|
|
210
210
|
if (err instanceof ContractViolation) console.error(err.issues); // ['state.quote: Required', …]
|
|
@@ -265,16 +265,16 @@ TRANSITION_TABLE['PAYMENT/INSTRUCT'].mayReturn; // ['ORDER/CREATED']
|
|
|
265
265
|
|
|
266
266
|
### Render every fixture cold (stale-rule survival)
|
|
267
267
|
|
|
268
|
-
Any call may return any state, so every screen must render from a cold
|
|
269
|
-
context. The fixtures are the FE doc's own
|
|
268
|
+
Any call may return any state, so every screen must render from a cold step response with no
|
|
269
|
+
prior context. The fixtures are the FE doc's own responses, so this is a test against the spec.
|
|
270
270
|
|
|
271
271
|
```ts
|
|
272
272
|
import { FIXTURES, loadFixture, assertFixture } from '@fun-xyz/fiat-contract';
|
|
273
273
|
|
|
274
274
|
FIXTURES.forEach(({ id, stateKey, docRef }) => {
|
|
275
275
|
it(`${id} renders cold (${docRef})`, () => {
|
|
276
|
-
const {
|
|
277
|
-
expect(() => renderCold(
|
|
276
|
+
const { stepResponse } = assertFixture(id); // validated + emission-legality checked
|
|
277
|
+
expect(() => renderCold(stepResponse)).not.toThrow();
|
|
278
278
|
expect(computePage(stateKey)).toBeDefined();
|
|
279
279
|
});
|
|
280
280
|
});
|
|
@@ -284,7 +284,7 @@ loadFixture('screen-10-kyc-on-hold'); // raw JSON, fresh deep copy, `unknown`
|
|
|
284
284
|
|
|
285
285
|
### Use a schema directly
|
|
286
286
|
|
|
287
|
-
All 46 schemas are exported when you need to validate a fragment rather than a whole
|
|
287
|
+
All 46 schemas are exported when you need to validate a fragment rather than a whole step response.
|
|
288
288
|
They are typed `z.ZodType<T>`, so you get `.parse` / `.safeParse` / `.optional()` — not `.shape` or
|
|
289
289
|
`.extend`, deliberately.
|
|
290
290
|
|
|
@@ -435,7 +435,7 @@ The current documented shape is what ships; none of these are settled here.
|
|
|
435
435
|
|
|
436
436
|
| Open item | How 0.1.0 encodes it |
|
|
437
437
|
| --- | --- |
|
|
438
|
-
| `orderId` placement | Both: optional on `PAYMENT{INSTRUCT}` (FE doc v0) **and** optional
|
|
438
|
+
| `orderId` placement | Both: optional on `PAYMENT{INSTRUCT}` (FE doc v0) **and** optional beside `state` (contract worked example). One fixture of each. |
|
|
439
439
|
| `PENDING_ORDER` removal | Kind ships, with the removal proposal flagged on the type, the table entry, and the fixture. Screen 12 stays frozen. |
|
|
440
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
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. |
|
|
@@ -443,11 +443,11 @@ The current documented shape is what ships; none of these are settled here.
|
|
|
443
443
|
| Cancel placement | `cancel` is legal on `PAYMENT{INSTRUCT}` and `ORDER{CREATED}`, marked conditional on the placement decision. |
|
|
444
444
|
| `FUN_AUTH` shape | `challenge: Record<string, JsonValue>`; the table entry is `docStatus: 'UNSPECIFIED'`, so `assertLegalEmission` reports it unjudgeable instead of guessing. |
|
|
445
445
|
| `statusHistory` element shape | The documented minimum (`{status}`) — no invented timestamps. |
|
|
446
|
-
| `QR_IMAGE` instruction | In the union per §The
|
|
446
|
+
| `QR_IMAGE` instruction | In the union per §The fiat step response, flagged against OQ1's "deliberately not pre-declared". |
|
|
447
447
|
|
|
448
|
-
Two fixture gaps are declared in `FIXTURE_COVERAGE_GAPS` rather than filled with invented
|
|
448
|
+
Two fixture gaps are declared in `FIXTURE_COVERAGE_GAPS` rather than filled with invented step responses:
|
|
449
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
|
|
450
|
+
Every other state has a recorded step response.
|
|
451
451
|
|
|
452
452
|
## Not decided here — needs a human
|
|
453
453
|
|
package/dist/assert.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* exactly two things: validate shapes (zod) and judge emissions against the table.
|
|
4
4
|
*
|
|
5
5
|
* Test *suites* live in the consumer repos next to the code they test:
|
|
6
|
-
* - `fun-backend`: adapter conformance (every emission ∈ the legal set), outgoing-
|
|
6
|
+
* - `fun-backend`: adapter conformance (every emission ∈ the legal set), outgoing step-response
|
|
7
7
|
* validation in dev/test, the scheduled provider drift run.
|
|
8
8
|
* - `connect-core`: exhaustive `computePage` table-walk, fixture-driven cold-render tests,
|
|
9
9
|
* harness units.
|
|
@@ -49,7 +49,7 @@ export declare function assertLegalReturn(from: FlowState, to: FlowState): void;
|
|
|
49
49
|
export declare function walkTable<T>(fn: (entry: TableEntry, key: StateKey) => T): T[];
|
|
50
50
|
export interface ValidatedFixture {
|
|
51
51
|
meta: FixtureMeta;
|
|
52
|
-
|
|
52
|
+
stepResponse: FiatStepResponse;
|
|
53
53
|
}
|
|
54
54
|
/** Load one fixture, validate its shape, and assert its emission is legal for its state. */
|
|
55
55
|
export declare function assertFixture(id: string): ValidatedFixture;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// package.json
|
|
2
|
-
var version = "0.
|
|
2
|
+
var version = "0.7.0";
|
|
3
3
|
|
|
4
4
|
// src/table.ts
|
|
5
5
|
var TABLE_VERSION = version;
|
|
@@ -30,22 +30,46 @@ var TRANSITION_TABLE = {
|
|
|
30
30
|
endpoint: "POST /fiat/session",
|
|
31
31
|
when: "no valid provider session token (own-tables check)"
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
id: "open_session",
|
|
35
|
+
mode: "SUBMIT",
|
|
36
|
+
endpoint: "POST /fiat/payment-session",
|
|
37
|
+
when: "returning user, session + KYC valid, capture-first rail \u2014 the method authorises before any order exists",
|
|
38
|
+
note: "Returns PAYMENT{CAPTURE} carrying the Surface. Without this entry the capture-first rail has no legal way in: a transition-following client takes POST /fiat/orders instead, and the backend refuses that when the provider declares the capture-first capability and no authorisation is present. Named open_session per the Fiat Contract 0.7 doc, which adds quote-time-minted surface rows beside it."
|
|
39
|
+
},
|
|
33
40
|
{
|
|
34
41
|
id: "continue",
|
|
35
42
|
mode: "SUBMIT",
|
|
36
43
|
endpoint: "POST /fiat/orders",
|
|
37
|
-
when: "returning user, session + KYC valid \u2014 the quote screen doubles as review"
|
|
44
|
+
when: "returning user, session + KYC valid, order-first rail \u2014 the quote screen doubles as review",
|
|
45
|
+
note: "Narrowed to the order-first rail: on a capture-first method the order cannot be created before the payment is authorised, so the sibling payment-session entry is the one that applies."
|
|
38
46
|
},
|
|
39
47
|
{
|
|
40
48
|
id: "continue",
|
|
41
49
|
mode: "SUBMIT",
|
|
42
50
|
endpoint: "POST /fiat/auth",
|
|
43
51
|
when: "no ambient Fun identity (non-fomo surfaces only)"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "capture",
|
|
55
|
+
mode: "CLIENT_SURFACE",
|
|
56
|
+
endpoint: "POST /fiat/orders",
|
|
57
|
+
when: "capture-then-order rail whose session is minted at quote time (inline pay sheet)",
|
|
58
|
+
note: "The 0.7 row that makes the single-screen Apple Pay design legal: the surface rides QUOTE, the provider component IS the primary CTA, and its result creates the order. Apple policy forbids a visible step between an Apple-branded tap and the sheet, and TransakApplePay exposes no programmatic trigger, so a two-step via payment-session cannot serve this rail. Requires state.surface (\u03942)."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "capture",
|
|
62
|
+
mode: "CLIENT_SURFACE",
|
|
63
|
+
endpoint: "POST /fiat/orders/:id/surface-result",
|
|
64
|
+
when: "order-then-capture rail whose provider order was created with the quote-time session",
|
|
65
|
+
note: "Same quote-time placement, other topology: the order already exists, so the surface reports against it rather than creating one."
|
|
44
66
|
}
|
|
45
67
|
],
|
|
46
68
|
mayReturn: [
|
|
47
69
|
"SESSION_AUTH",
|
|
48
70
|
...KYC_ANY,
|
|
71
|
+
"PAYMENT/CAPTURE",
|
|
72
|
+
"PAYMENT/INSTRUCT",
|
|
49
73
|
"PENDING_ORDER",
|
|
50
74
|
"ORDER/AWAITING_CONFIRMATION",
|
|
51
75
|
"ORDER/CREATED",
|
|
@@ -53,8 +77,10 @@ var TRANSITION_TABLE = {
|
|
|
53
77
|
],
|
|
54
78
|
terminal: false,
|
|
55
79
|
notes: [
|
|
56
|
-
"Re-quote on amount change is a client-local event, not a transition.",
|
|
57
|
-
|
|
80
|
+
"Re-quote on amount change is a client-local event, not a transition. A new quote carries a new surface; the client remounts the adapter rather than reusing the old session.",
|
|
81
|
+
"A surface on QUOTE and a SUBMIT to the same endpoint may not coexist \u2014 two paths to one order. assertLegalEmission enforces it; the server picks one per rail.",
|
|
82
|
+
`Both PAYMENT states are reachable directly from QUOTE: the FE doc canvas draws S1 \u2192 S5 (card, capture-first) and S1 \u2192 S6 (bank). This entry was first ported from the FE doc's bindings table, whose "May receive back" column omits both \u2014 the canvas is the correct source of the two.`,
|
|
83
|
+
'TODO(open-decision): contract \xA7"Proposal: remove PENDING_ORDER" would additionally have QUOTE return ORDER{CREATED} for an active order. That decision does not gate the PAYMENT edges above.'
|
|
58
84
|
]
|
|
59
85
|
},
|
|
60
86
|
FUN_AUTH: {
|
|
@@ -352,6 +378,7 @@ var DOCUMENTED_ENDPOINTS = [
|
|
|
352
378
|
"GET /fiat/payment-methods",
|
|
353
379
|
"POST /fiat/quote",
|
|
354
380
|
"POST /fiat/auth",
|
|
381
|
+
"POST /fiat/payment-session",
|
|
355
382
|
"POST /fiat/session",
|
|
356
383
|
"POST /fiat/session/verify",
|
|
357
384
|
"GET /fiat/kyc",
|
|
@@ -366,6 +393,54 @@ var DOCUMENTED_ENDPOINTS = [
|
|
|
366
393
|
"POST /fiat/orders/:id/cancel",
|
|
367
394
|
"POST /fiat/orders/:id/surface-result"
|
|
368
395
|
];
|
|
396
|
+
var UNOFFERED_ENDPOINTS = [
|
|
397
|
+
{
|
|
398
|
+
endpoint: "GET /fiat/payment-methods",
|
|
399
|
+
reason: "Discovery, called before any flow state exists \u2014 there is no state to hang it off."
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
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)."
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
endpoint: "POST /fiat/instruments",
|
|
407
|
+
reason: "No state offers it and no published doc explains why. TODO(open-decision): saved instruments may simply be unreached in v1 \u2014 confirm, or wire it to the state that should offer it."
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
endpoint: "GET /fiat/orders/:id/instructions",
|
|
411
|
+
reason: "PAYMENT{INSTRUCT} carries its instructions in state and offers no fetch. TODO(open-decision): that state also notes no re-issue endpoint exists, which sits oddly beside this one being published \u2014 confirm or retire it."
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
endpoint: "POST /fiat/orders/:id/reference",
|
|
415
|
+
reason: "The bank-reference submission is offered by no state. TODO(open-decision): PAYMENT{INSTRUCT} is the plausible home \u2014 confirm or wire it."
|
|
416
|
+
}
|
|
417
|
+
];
|
|
418
|
+
var UNREACHABLE_STATES = [
|
|
419
|
+
{
|
|
420
|
+
stateKey: "FUN_AUTH",
|
|
421
|
+
reason: "No bindings row published (docStatus UNSPECIFIED) and never rendered in fomo, where identity is ambient. Entered out-of-band, not by a mayReturn edge."
|
|
422
|
+
}
|
|
423
|
+
];
|
|
424
|
+
function unofferedEndpoints() {
|
|
425
|
+
const offered = /* @__PURE__ */ new Set();
|
|
426
|
+
for (const key of STATE_KEYS) {
|
|
427
|
+
for (const allowed of TRANSITION_TABLE[key].allowedTransitions) offered.add(allowed.endpoint);
|
|
428
|
+
}
|
|
429
|
+
return DOCUMENTED_ENDPOINTS.filter((endpoint) => !offered.has(endpoint));
|
|
430
|
+
}
|
|
431
|
+
function unreachableStates(from = "QUOTE") {
|
|
432
|
+
const seen = /* @__PURE__ */ new Set([from]);
|
|
433
|
+
const queue = [from];
|
|
434
|
+
while (queue.length > 0) {
|
|
435
|
+
for (const target of TRANSITION_TABLE[queue.pop()].mayReturn ?? []) {
|
|
436
|
+
if (!seen.has(target)) {
|
|
437
|
+
seen.add(target);
|
|
438
|
+
queue.push(target);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return STATE_KEYS.filter((key) => !seen.has(key));
|
|
443
|
+
}
|
|
369
444
|
function stateKey(state) {
|
|
370
445
|
switch (state.kind) {
|
|
371
446
|
case "QUOTE":
|
|
@@ -408,8 +483,12 @@ export {
|
|
|
408
483
|
TRANSITION_TABLE,
|
|
409
484
|
STATE_KEYS,
|
|
410
485
|
DOCUMENTED_ENDPOINTS,
|
|
486
|
+
UNOFFERED_ENDPOINTS,
|
|
487
|
+
UNREACHABLE_STATES,
|
|
488
|
+
unofferedEndpoints,
|
|
489
|
+
unreachableStates,
|
|
411
490
|
stateKey,
|
|
412
491
|
tableEntry,
|
|
413
492
|
isTerminal
|
|
414
493
|
};
|
|
415
|
-
//# sourceMappingURL=chunk-
|
|
494
|
+
//# sourceMappingURL=chunk-OGNYHIOE.mjs.map
|
package/dist/fixtures/index.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* fiat-contract — recorded fixtures + loader.
|
|
3
3
|
*
|
|
4
|
-
* Every `.json` file in this directory is
|
|
5
|
-
* doc's per-screen "State details"
|
|
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
6
|
* exception is `screen-04-order-review.json`, whose payload the doc publishes *by reference*
|
|
7
7
|
* ("quote: <same shape as Screen 1, refreshed>"): its quote is Screen 1's payload verbatim.
|
|
8
8
|
* They are
|
|
9
9
|
* already synthetic (`q_8f2`, `o_31c`, `eyJ…`, `"…"` placeholders); the redaction rule applies to
|
|
10
10
|
* fixtures too, so never replace a fake session token, bank field, or PII value with a real one.
|
|
11
11
|
*
|
|
12
|
-
* Every fixture is a full
|
|
12
|
+
* Every fixture is a full step response (`state` + `provider` + `transitions`). The contract doc's worked
|
|
13
13
|
* example elides `provider` in its prose; the three `worked-example-*` fixtures add
|
|
14
14
|
* `"provider": "TRANSAK"` so they validate as real responses — `provider` is always present on the
|
|
15
|
-
* wire, and the contract has no half-
|
|
15
|
+
* wire, and the contract has no half-response shape.
|
|
16
16
|
*/
|
|
17
17
|
import type { StateKey } from '../table';
|
|
18
18
|
export type FixtureSource = 'FE_DOC' | 'CONTRACT_DOC';
|
|
@@ -20,7 +20,7 @@ export interface FixtureMeta {
|
|
|
20
20
|
id: string;
|
|
21
21
|
file: string;
|
|
22
22
|
source: FixtureSource;
|
|
23
|
-
/** Where in the source doc this
|
|
23
|
+
/** Where in the source doc this step response is published. */
|
|
24
24
|
docRef: string;
|
|
25
25
|
/** FE doc screen number, when the fixture comes from a screen card. */
|
|
26
26
|
screen?: number;
|
|
@@ -29,7 +29,7 @@ export interface FixtureMeta {
|
|
|
29
29
|
}
|
|
30
30
|
export declare const FIXTURES: readonly FixtureMeta[];
|
|
31
31
|
/**
|
|
32
|
-
* State entries with no published
|
|
32
|
+
* State entries with no published step response to record. Declared so missing coverage is visible
|
|
33
33
|
* instead of silent — the fixture test asserts coverage equals (all state keys − these).
|
|
34
34
|
*/
|
|
35
35
|
export declare const FIXTURE_COVERAGE_GAPS: readonly {
|
|
@@ -38,7 +38,7 @@ export declare const FIXTURE_COVERAGE_GAPS: readonly {
|
|
|
38
38
|
}[];
|
|
39
39
|
export declare const fixtureMeta: (id: string) => FixtureMeta;
|
|
40
40
|
/**
|
|
41
|
-
* Raw
|
|
41
|
+
* Raw step response — deliberately `unknown`, so callers validate before use.
|
|
42
42
|
*
|
|
43
43
|
* Reads from the generated `data.ts`: no filesystem, so this works under React Native, in a
|
|
44
44
|
* browser, and in both CJS and ESM output. A fresh deep copy each call, so a consumer mutating a
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* fiat-contract — the published contract between `fun-backend` (emits
|
|
2
|
+
* fiat-contract — the published contract between `fun-backend` (emits step responses) and
|
|
3
3
|
* `funkit`/`connect-core` (renders them). Types + zod schemas + the transition table as data +
|
|
4
4
|
* recorded fixtures + assertion helpers. No runtime logic beyond validation and assertion.
|
|
5
5
|
*
|