@fun-xyz/fiat-contract 0.6.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/dist/index.js CHANGED
@@ -51,6 +51,7 @@ __export(index_exports, {
51
51
  InstructionsSchema: () => InstructionsSchema,
52
52
  InstrumentSchema: () => InstrumentSchema,
53
53
  JsonSchema: () => JsonSchema,
54
+ KYCProviderSchema: () => KYCProviderSchema,
54
55
  KycCaptureStateSchema: () => KycCaptureStateSchema,
55
56
  KycInputRequiredStateSchema: () => KycInputRequiredStateSchema,
56
57
  KycNoActionRequiredStateSchema: () => KycNoActionRequiredStateSchema,
@@ -73,6 +74,8 @@ __export(index_exports, {
73
74
  SelectOptionSchema: () => SelectOptionSchema,
74
75
  SessionAuthStateSchema: () => SessionAuthStateSchema,
75
76
  StatusHistoryEntrySchema: () => StatusHistoryEntrySchema,
77
+ SurfaceKindSchema: () => SurfaceKindSchema,
78
+ SurfaceProviderSchema: () => SurfaceProviderSchema,
76
79
  SurfaceSchema: () => SurfaceSchema,
77
80
  TABLE_VERSION: () => TABLE_VERSION,
78
81
  TERMINAL_ORDER_STATUSES: () => TERMINAL_ORDER_STATUSES,
@@ -81,6 +84,8 @@ __export(index_exports, {
81
84
  TransitionParamsSchema: () => TransitionParamsSchema,
82
85
  TransitionSchema: () => TransitionSchema,
83
86
  TxSchema: () => TxSchema,
87
+ UNOFFERED_ENDPOINTS: () => UNOFFERED_ENDPOINTS,
88
+ UNREACHABLE_STATES: () => UNREACHABLE_STATES,
84
89
  assertAllFixtures: () => assertAllFixtures,
85
90
  assertFiatStepResponse: () => assertFiatStepResponse,
86
91
  assertFixture: () => assertFixture,
@@ -95,6 +100,8 @@ __export(index_exports, {
95
100
  looksLikeFieldSpec: () => looksLikeFieldSpec,
96
101
  stateKey: () => stateKey,
97
102
  tableEntry: () => tableEntry,
103
+ unofferedEndpoints: () => unofferedEndpoints,
104
+ unreachableStates: () => unreachableStates,
98
105
  walkTable: () => walkTable
99
106
  });
100
107
  module.exports = __toCommonJS(index_exports);
@@ -380,8 +387,12 @@ var HTTP_VERBS = ["GET", "POST"];
380
387
  var ENDPOINT_PATTERN = /^(GET|POST) \/fiat\/[^\s]*$/;
381
388
  var $endpoint = import_zod.z.string().regex(ENDPOINT_PATTERN, 'endpoint must read "<GET|POST> /fiat/\u2026"');
382
389
  var FiatEndpointSchema = $endpoint;
383
- var $provider = import_zod.z.enum(["TRANSAK", "SWAPPED"]);
390
+ var $provider = import_zod.z.enum(["TRANSAK", "SWAPPED", "BANXA", "MOONPAY", "STRIPE"]);
384
391
  var FiatProviderSchema = $provider;
392
+ var $kycProvider = import_zod.z.enum(["SUMSUB"]);
393
+ var KYCProviderSchema = $kycProvider;
394
+ var $surfaceProvider = import_zod.z.enum([...$provider.options, ...$kycProvider.options]);
395
+ var SurfaceProviderSchema = $surfaceProvider;
385
396
  var $orderStatus = import_zod.z.enum([
386
397
  "CREATING",
387
398
  "AWAITING_PAYMENT",
@@ -473,13 +484,22 @@ var $refund = obj({
473
484
  expectedBy: import_zod.z.string()
474
485
  });
475
486
  var RefundSchema = $refund;
476
- var $surface = import_zod.z.union([
477
- obj({ kind: import_zod.z.literal("URL_REDIRECT"), url: import_zod.z.string() }),
478
- obj({ kind: import_zod.z.literal("URL_EMBED"), url: import_zod.z.string() }),
479
- obj({ kind: import_zod.z.literal("PCI_COMPONENT"), session: import_zod.z.string() }),
480
- obj({ kind: import_zod.z.literal("PAY_SHEET"), session: import_zod.z.string() }),
481
- obj({ kind: import_zod.z.literal("ACH_COMPONENT"), session: import_zod.z.string() })
482
- ]);
487
+ var $surfaceKind = import_zod.z.enum(["URL_REDIRECT", "KYC_SDK", "PCI_COMPONENT", "PAY_SHEET", "ACH_COMPONENT"]);
488
+ var SurfaceKindSchema = $surfaceKind;
489
+ var $surface = obj({
490
+ kind: $surfaceKind,
491
+ provider: $surfaceProvider,
492
+ /**
493
+ * The one deliberate non-strict leaf in the contract. Provider inputs are the adapter's
494
+ * vocabulary, so typing them here would drag every provider field change through a contract
495
+ * release and both consumers — and a normalised `environment` could only be lossy across
496
+ * Transak's four values and Banxa's two. Both ends narrow instead: fun-backend emits from a
497
+ * typed provider session, the client adapter validates with its own schema, and a recorded
498
+ * fixture per `(provider, kind)` keeps the two honest.
499
+ */
500
+ params: import_zod.z.record(import_zod.z.string(), JsonSchema),
501
+ expiresAt: import_zod.z.string().optional()
502
+ });
483
503
  var SurfaceSchema = $surface;
484
504
  var $instructionField = obj({
485
505
  id: import_zod.z.string(),
@@ -609,6 +629,8 @@ var AuthChallengeSchema = $authChallenge;
609
629
  var $quoteState = obj({
610
630
  kind: import_zod.z.literal("QUOTE"),
611
631
  quote: $quote,
632
+ /** Present when the provider's component is the primary CTA, inline on the entry screen. */
633
+ surface: $surface.optional(),
612
634
  error: $failureReason.optional()
613
635
  });
614
636
  var QuoteStateSchema = $quoteState;
@@ -627,6 +649,7 @@ var SessionAuthStateSchema = $sessionAuthState;
627
649
  var $kycCaptureState = obj({
628
650
  kind: import_zod.z.literal("KYC"),
629
651
  phase: import_zod.z.literal("CAPTURE"),
652
+ quote: $quote,
630
653
  surface: $surface,
631
654
  error: $failureReason.optional()
632
655
  });
@@ -634,6 +657,7 @@ var KycCaptureStateSchema = $kycCaptureState;
634
657
  var $kycInputRequiredState = obj({
635
658
  kind: import_zod.z.literal("KYC"),
636
659
  phase: import_zod.z.literal("INPUT_REQUIRED"),
660
+ quote: $quote,
637
661
  form: $formDescriptor,
638
662
  error: $failureReason.optional()
639
663
  });
@@ -641,6 +665,7 @@ var KycInputRequiredStateSchema = $kycInputRequiredState;
641
665
  var $kycNoActionRequiredState = obj({
642
666
  kind: import_zod.z.literal("KYC"),
643
667
  phase: import_zod.z.literal("NO_ACTION_REQUIRED"),
668
+ quote: $quote,
644
669
  reason: import_zod.z.enum(["IN_REVIEW", "ON_HOLD", "REJECTED"]),
645
670
  retryAfter: import_zod.z.string().optional(),
646
671
  failureReason: $failureReason.optional(),
@@ -718,7 +743,7 @@ var $stepResponse = obj({
718
743
  var FiatStepResponseSchema = $stepResponse;
719
744
 
720
745
  // package.json
721
- var version = "0.6.0";
746
+ var version = "0.8.0";
722
747
 
723
748
  // src/table.ts
724
749
  var TABLE_VERSION = version;
@@ -749,22 +774,46 @@ var TRANSITION_TABLE = {
749
774
  endpoint: "POST /fiat/session",
750
775
  when: "no valid provider session token (own-tables check)"
751
776
  },
777
+ {
778
+ id: "open_session",
779
+ mode: "SUBMIT",
780
+ endpoint: "POST /fiat/payment-session",
781
+ when: "returning user, session + KYC valid, capture-first rail \u2014 the method authorises before any order exists",
782
+ 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."
783
+ },
752
784
  {
753
785
  id: "continue",
754
786
  mode: "SUBMIT",
755
787
  endpoint: "POST /fiat/orders",
756
- when: "returning user, session + KYC valid \u2014 the quote screen doubles as review"
788
+ when: "returning user, session + KYC valid, order-first rail \u2014 the quote screen doubles as review",
789
+ 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."
757
790
  },
758
791
  {
759
792
  id: "continue",
760
793
  mode: "SUBMIT",
761
794
  endpoint: "POST /fiat/auth",
762
795
  when: "no ambient Fun identity (non-fomo surfaces only)"
796
+ },
797
+ {
798
+ id: "capture",
799
+ mode: "CLIENT_SURFACE",
800
+ endpoint: "POST /fiat/orders",
801
+ when: "capture-then-order rail whose session is minted at quote time (inline pay sheet)",
802
+ 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)."
803
+ },
804
+ {
805
+ id: "capture",
806
+ mode: "CLIENT_SURFACE",
807
+ endpoint: "POST /fiat/orders/:id/surface-result",
808
+ when: "order-then-capture rail whose provider order was created with the quote-time session",
809
+ note: "Same quote-time placement, other topology: the order already exists, so the surface reports against it rather than creating one."
763
810
  }
764
811
  ],
765
812
  mayReturn: [
766
813
  "SESSION_AUTH",
767
814
  ...KYC_ANY,
815
+ "PAYMENT/CAPTURE",
816
+ "PAYMENT/INSTRUCT",
768
817
  "PENDING_ORDER",
769
818
  "ORDER/AWAITING_CONFIRMATION",
770
819
  "ORDER/CREATED",
@@ -772,8 +821,10 @@ var TRANSITION_TABLE = {
772
821
  ],
773
822
  terminal: false,
774
823
  notes: [
775
- "Re-quote on amount change is a client-local event, not a transition.",
776
- 'If the PENDING_ORDER removal lands, QUOTE additionally returns PAYMENT{INSTRUCT} / ORDER{CREATED} directly. TODO(open-decision): contract \xA7"Proposal: remove PENDING_ORDER".'
824
+ "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.",
825
+ "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.",
826
+ `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.`,
827
+ '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.'
777
828
  ]
778
829
  },
779
830
  FUN_AUTH: {
@@ -818,21 +869,23 @@ var TRANSITION_TABLE = {
818
869
  {
819
870
  id: "capture",
820
871
  mode: "CLIENT_SURFACE",
821
- endpoint: "GET /fiat/kyc",
822
- note: 'report target per FE bindings ("POST surface report \u2192 GET /fiat/kyc") and contract Flow C.'
872
+ endpoint: "POST /fiat/kyc/capture",
873
+ note: "Reports SDK submission; provider eligibility is re-evaluated through quote orchestration."
823
874
  }
824
875
  ],
825
876
  mayReturn: [
877
+ "KYC/CAPTURE",
826
878
  "KYC/INPUT_REQUIRED",
827
879
  "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
828
880
  "KYC/NO_ACTION_REQUIRED:ON_HOLD",
829
881
  "KYC/NO_ACTION_REQUIRED:REJECTED",
882
+ "QUOTE",
883
+ "PAYMENT/CAPTURE",
884
+ "PAYMENT/INSTRUCT",
830
885
  "ORDER/AWAITING_CONFIRMATION"
831
886
  ],
832
887
  terminal: false,
833
- notes: [
834
- "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."
835
- ]
888
+ notes: ["Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter."]
836
889
  },
837
890
  "KYC/INPUT_REQUIRED": {
838
891
  key: "KYC/INPUT_REQUIRED",
@@ -844,16 +897,20 @@ var TRANSITION_TABLE = {
844
897
  { id: "submit_round", mode: "SUBMIT", endpoint: "POST /fiat/kyc/form" }
845
898
  ],
846
899
  mayReturn: [
900
+ "KYC/CAPTURE",
847
901
  "KYC/INPUT_REQUIRED",
848
902
  "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
849
903
  "KYC/NO_ACTION_REQUIRED:ON_HOLD",
850
904
  "KYC/NO_ACTION_REQUIRED:REJECTED",
905
+ "QUOTE",
906
+ "PAYMENT/CAPTURE",
907
+ "PAYMENT/INSTRUCT",
851
908
  "ORDER/AWAITING_CONFIRMATION"
852
909
  ],
853
910
  terminal: false,
854
911
  notes: [
855
912
  "One POST per round; conditional requirements may open another round.",
856
- "FILE fields stream to POST /fiat/kyc/document \u2014 an upload path the harness owns, not a transition.",
913
+ "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.",
857
914
  "Hosted KYC links arrive as HOSTED_LINK form fields, not as a Surface."
858
915
  ]
859
916
  },
@@ -864,12 +921,16 @@ var TRANSITION_TABLE = {
864
921
  reason: "IN_REVIEW",
865
922
  screens: [9],
866
923
  docStatus: "SPECIFIED",
867
- allowedTransitions: [{ id: "poll", mode: "AWAIT", endpoint: "GET /fiat/kyc" }],
924
+ allowedTransitions: [{ id: "poll", mode: "AWAIT", endpoint: "GET /fiat/quotes/:quoteRef" }],
868
925
  mayReturn: [
869
926
  "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
927
+ "KYC/CAPTURE",
870
928
  "KYC/INPUT_REQUIRED",
871
929
  "KYC/NO_ACTION_REQUIRED:ON_HOLD",
872
930
  "KYC/NO_ACTION_REQUIRED:REJECTED",
931
+ "QUOTE",
932
+ "PAYMENT/CAPTURE",
933
+ "PAYMENT/INSTRUCT",
873
934
  "ORDER/AWAITING_CONFIRMATION"
874
935
  ],
875
936
  terminal: false,
@@ -899,20 +960,11 @@ var TRANSITION_TABLE = {
899
960
  reason: "REJECTED",
900
961
  screens: [11],
901
962
  docStatus: "SPECIFIED",
902
- allowedTransitions: [
903
- {
904
- id: "escalate",
905
- mode: "SUBMIT",
906
- endpoint: "GET /fiat/kyc",
907
- when: "an escalation round is offered",
908
- 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."
909
- }
910
- ],
911
- mayReturn: KYC_ANY,
912
- terminal: false,
963
+ allowedTransitions: [],
964
+ mayReturn: [],
965
+ terminal: true,
913
966
  notes: [
914
- "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.",
915
- "TODO(open-decision): contract OQ7 \u2014 terminal vs escalating rejections are not enumerated."
967
+ "Only explicit non-retryable outcomes reach REJECTED. Correctable failures return CAPTURE or INPUT_REQUIRED; provider-specific declines re-route before becoming user rejection."
916
968
  ]
917
969
  },
918
970
  PENDING_ORDER: {
@@ -1071,10 +1123,13 @@ var DOCUMENTED_ENDPOINTS = [
1071
1123
  "GET /fiat/payment-methods",
1072
1124
  "POST /fiat/quote",
1073
1125
  "POST /fiat/auth",
1126
+ "POST /fiat/payment-session",
1074
1127
  "POST /fiat/session",
1075
1128
  "POST /fiat/session/verify",
1076
- "GET /fiat/kyc",
1129
+ "GET /fiat/quotes/:quoteRef",
1077
1130
  "POST /fiat/kyc/form",
1131
+ "POST /fiat/kyc/capture",
1132
+ "POST /fiat/kyc/sdk-token",
1078
1133
  "POST /fiat/kyc/document",
1079
1134
  "POST /fiat/instruments",
1080
1135
  "POST /fiat/orders",
@@ -1085,6 +1140,58 @@ var DOCUMENTED_ENDPOINTS = [
1085
1140
  "POST /fiat/orders/:id/cancel",
1086
1141
  "POST /fiat/orders/:id/surface-result"
1087
1142
  ];
1143
+ var UNOFFERED_ENDPOINTS = [
1144
+ {
1145
+ endpoint: "POST /fiat/kyc/sdk-token",
1146
+ reason: "SDK token refresh is adapter-driven rather than a flow transition."
1147
+ },
1148
+ {
1149
+ endpoint: "GET /fiat/payment-methods",
1150
+ reason: "Discovery, called before any flow state exists \u2014 there is no state to hang it off."
1151
+ },
1152
+ {
1153
+ endpoint: "POST /fiat/kyc/document",
1154
+ 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)."
1155
+ },
1156
+ {
1157
+ endpoint: "POST /fiat/instruments",
1158
+ 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."
1159
+ },
1160
+ {
1161
+ endpoint: "GET /fiat/orders/:id/instructions",
1162
+ 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."
1163
+ },
1164
+ {
1165
+ endpoint: "POST /fiat/orders/:id/reference",
1166
+ reason: "The bank-reference submission is offered by no state. TODO(open-decision): PAYMENT{INSTRUCT} is the plausible home \u2014 confirm or wire it."
1167
+ }
1168
+ ];
1169
+ var UNREACHABLE_STATES = [
1170
+ {
1171
+ stateKey: "FUN_AUTH",
1172
+ 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."
1173
+ }
1174
+ ];
1175
+ function unofferedEndpoints() {
1176
+ const offered = /* @__PURE__ */ new Set();
1177
+ for (const key of STATE_KEYS) {
1178
+ for (const allowed of TRANSITION_TABLE[key].allowedTransitions) offered.add(allowed.endpoint);
1179
+ }
1180
+ return DOCUMENTED_ENDPOINTS.filter((endpoint) => !offered.has(endpoint));
1181
+ }
1182
+ function unreachableStates(from = "QUOTE") {
1183
+ const seen = /* @__PURE__ */ new Set([from]);
1184
+ const queue = [from];
1185
+ while (queue.length > 0) {
1186
+ for (const target of TRANSITION_TABLE[queue.pop()].mayReturn ?? []) {
1187
+ if (!seen.has(target)) {
1188
+ seen.add(target);
1189
+ queue.push(target);
1190
+ }
1191
+ }
1192
+ }
1193
+ return STATE_KEYS.filter((key) => !seen.has(key));
1194
+ }
1088
1195
  function stateKey(state) {
1089
1196
  switch (state.kind) {
1090
1197
  case "QUOTE":
@@ -1167,6 +1274,64 @@ var FIXTURE_DATA = {
1167
1274
  }
1168
1275
  ]
1169
1276
  },
1277
+ "screen-01b-quote-with-pay-sheet": {
1278
+ "state": {
1279
+ "kind": "QUOTE",
1280
+ "quote": {
1281
+ "quoteRef": "q_8f2",
1282
+ "fiat": {
1283
+ "currency": "GBP",
1284
+ "amount": "100.00"
1285
+ },
1286
+ "crypto": {
1287
+ "currency": "USDC",
1288
+ "network": "polygon",
1289
+ "amount": "98.41"
1290
+ },
1291
+ "rate": "0.9841",
1292
+ "totalFee": "1.59",
1293
+ "feeBreakdown": [
1294
+ {
1295
+ "id": "provider_fee",
1296
+ "labelFallback": "Processing fee",
1297
+ "amount": "1.09"
1298
+ },
1299
+ {
1300
+ "id": "network_fee",
1301
+ "labelFallback": "Network fee",
1302
+ "amount": "0.50"
1303
+ }
1304
+ ],
1305
+ "paymentMethod": "bank_transfer",
1306
+ "expiresAt": "\u2026"
1307
+ },
1308
+ "surface": {
1309
+ "kind": "PAY_SHEET",
1310
+ "provider": "TRANSAK",
1311
+ "params": {
1312
+ "sessionId": "txs_\u2026",
1313
+ "environment": "STAGING"
1314
+ }
1315
+ }
1316
+ },
1317
+ "provider": "TRANSAK",
1318
+ "transitions": [
1319
+ {
1320
+ "id": "capture",
1321
+ "mode": "CLIENT_SURFACE",
1322
+ "report": {
1323
+ "endpoint": "POST /fiat/orders",
1324
+ "params": {
1325
+ "quoteRef": "q_8f2",
1326
+ "depositAddress": "\u2026"
1327
+ },
1328
+ "expects": [
1329
+ "paymentAuthorisationId"
1330
+ ]
1331
+ }
1332
+ }
1333
+ ]
1334
+ },
1170
1335
  "screen-02-session-auth": {
1171
1336
  "state": {
1172
1337
  "kind": "SESSION_AUTH",
@@ -1194,45 +1359,43 @@ var FIXTURE_DATA = {
1194
1359
  "state": {
1195
1360
  "kind": "KYC",
1196
1361
  "phase": "INPUT_REQUIRED",
1362
+ "quote": {
1363
+ "quoteRef": "q_8f2",
1364
+ "fiat": {
1365
+ "currency": "GBP",
1366
+ "amount": "100.00"
1367
+ },
1368
+ "crypto": {
1369
+ "currency": "USDC",
1370
+ "network": "polygon",
1371
+ "amount": "98.41"
1372
+ },
1373
+ "rate": "0.9841",
1374
+ "totalFee": "1.59",
1375
+ "feeBreakdown": [
1376
+ {
1377
+ "id": "provider_fee",
1378
+ "labelFallback": "Processing fee",
1379
+ "amount": "1.59"
1380
+ }
1381
+ ],
1382
+ "paymentMethod": "bank_transfer",
1383
+ "expiresAt": "\u2026"
1384
+ },
1197
1385
  "form": {
1198
- "formId": "personal_details",
1386
+ "formId": "kyc_q_8f2_1",
1199
1387
  "fields": [
1200
1388
  {
1201
- "id": "firstName",
1389
+ "id": "tin",
1202
1390
  "type": "TEXT",
1203
- "labelFallback": "First name",
1204
- "required": true
1205
- },
1206
- {
1207
- "id": "dob",
1208
- "type": "DATE",
1209
- "labelFallback": "Date of birth",
1391
+ "labelFallback": "Tax ID",
1210
1392
  "required": true
1211
1393
  },
1212
1394
  {
1213
- "id": "country",
1214
- "type": "COUNTRY",
1215
- "labelFallback": "Country",
1395
+ "id": "address",
1396
+ "type": "TEXT",
1397
+ "labelFallback": "Home address",
1216
1398
  "required": true
1217
- },
1218
- {
1219
- "id": "sourceOfFunds",
1220
- "type": "SELECT",
1221
- "labelFallback": "Source of funds",
1222
- "required": true,
1223
- "options": [
1224
- {
1225
- "id": "salary",
1226
- "labelFallback": "Salary"
1227
- }
1228
- ]
1229
- },
1230
- {
1231
- "id": "idProof",
1232
- "type": "HOSTED_LINK",
1233
- "labelFallback": "Verify your ID",
1234
- "required": true,
1235
- "url": "\u2026"
1236
1399
  }
1237
1400
  ]
1238
1401
  }
@@ -1244,7 +1407,8 @@ var FIXTURE_DATA = {
1244
1407
  "mode": "SUBMIT",
1245
1408
  "endpoint": "POST /fiat/kyc/form",
1246
1409
  "params": {
1247
- "formId": "personal_details"
1410
+ "quoteRef": "q_8f2",
1411
+ "formId": "kyc_q_8f2_1"
1248
1412
  },
1249
1413
  "inputs": {
1250
1414
  "values": {
@@ -1305,7 +1469,11 @@ var FIXTURE_DATA = {
1305
1469
  "phase": "CAPTURE",
1306
1470
  "surface": {
1307
1471
  "kind": "PCI_COMPONENT",
1308
- "session": "eyJ\u2026"
1472
+ "provider": "TRANSAK",
1473
+ "params": {
1474
+ "sessionId": "eyJ\u2026",
1475
+ "environment": "STAGING"
1476
+ }
1309
1477
  }
1310
1478
  },
1311
1479
  "provider": "TRANSAK",
@@ -1325,11 +1493,84 @@ var FIXTURE_DATA = {
1325
1493
  }
1326
1494
  ]
1327
1495
  },
1496
+ "screen-08-kyc-capture": {
1497
+ "state": {
1498
+ "kind": "KYC",
1499
+ "phase": "CAPTURE",
1500
+ "quote": {
1501
+ "quoteRef": "q_8f2",
1502
+ "fiat": {
1503
+ "currency": "GBP",
1504
+ "amount": "100.00"
1505
+ },
1506
+ "crypto": {
1507
+ "currency": "USDC",
1508
+ "network": "polygon",
1509
+ "amount": "98.41"
1510
+ },
1511
+ "rate": "0.9841",
1512
+ "totalFee": "1.59",
1513
+ "feeBreakdown": [
1514
+ {
1515
+ "id": "provider_fee",
1516
+ "labelFallback": "Processing fee",
1517
+ "amount": "1.59"
1518
+ }
1519
+ ],
1520
+ "paymentMethod": "bank_transfer",
1521
+ "expiresAt": "\u2026"
1522
+ },
1523
+ "surface": {
1524
+ "kind": "KYC_SDK",
1525
+ "provider": "SUMSUB",
1526
+ "params": {
1527
+ "accessToken": "eyJ\u2026"
1528
+ },
1529
+ "expiresAt": "\u2026"
1530
+ }
1531
+ },
1532
+ "provider": "TRANSAK",
1533
+ "transitions": [
1534
+ {
1535
+ "id": "capture",
1536
+ "mode": "CLIENT_SURFACE",
1537
+ "report": {
1538
+ "endpoint": "POST /fiat/kyc/capture",
1539
+ "params": {
1540
+ "quoteRef": "q_8f2"
1541
+ }
1542
+ }
1543
+ }
1544
+ ]
1545
+ },
1328
1546
  "screen-09-kyc-in-review": {
1329
1547
  "state": {
1330
1548
  "kind": "KYC",
1331
1549
  "phase": "NO_ACTION_REQUIRED",
1332
- "reason": "IN_REVIEW"
1550
+ "reason": "IN_REVIEW",
1551
+ "quote": {
1552
+ "quoteRef": "q_8f2",
1553
+ "fiat": {
1554
+ "currency": "GBP",
1555
+ "amount": "100.00"
1556
+ },
1557
+ "crypto": {
1558
+ "currency": "USDC",
1559
+ "network": "polygon",
1560
+ "amount": "98.41"
1561
+ },
1562
+ "rate": "0.9841",
1563
+ "totalFee": "1.59",
1564
+ "feeBreakdown": [
1565
+ {
1566
+ "id": "provider_fee",
1567
+ "labelFallback": "Processing fee",
1568
+ "amount": "1.59"
1569
+ }
1570
+ ],
1571
+ "paymentMethod": "bank_transfer",
1572
+ "expiresAt": "\u2026"
1573
+ }
1333
1574
  },
1334
1575
  "provider": "TRANSAK",
1335
1576
  "transitions": [
@@ -1337,10 +1578,7 @@ var FIXTURE_DATA = {
1337
1578
  "id": "poll",
1338
1579
  "mode": "AWAIT",
1339
1580
  "poll": {
1340
- "endpoint": "GET /fiat/kyc",
1341
- "params": {
1342
- "quoteRef": "q_8f2"
1343
- },
1581
+ "endpoint": "GET /fiat/quotes/q_8f2",
1344
1582
  "intervalMs": 3e3,
1345
1583
  "backoffFactor": 1.5,
1346
1584
  "maxIntervalMs": 1e4,
@@ -1354,28 +1592,70 @@ var FIXTURE_DATA = {
1354
1592
  "kind": "KYC",
1355
1593
  "phase": "NO_ACTION_REQUIRED",
1356
1594
  "reason": "ON_HOLD",
1357
- "retryAfter": "2026-08-20T09:00:00Z"
1595
+ "retryAfter": "2026-08-20T09:00:00Z",
1596
+ "quote": {
1597
+ "quoteRef": "q_8f2",
1598
+ "fiat": {
1599
+ "currency": "GBP",
1600
+ "amount": "100.00"
1601
+ },
1602
+ "crypto": {
1603
+ "currency": "USDC",
1604
+ "network": "polygon",
1605
+ "amount": "98.41"
1606
+ },
1607
+ "rate": "0.9841",
1608
+ "totalFee": "1.59",
1609
+ "feeBreakdown": [
1610
+ {
1611
+ "id": "provider_fee",
1612
+ "labelFallback": "Processing fee",
1613
+ "amount": "1.59"
1614
+ }
1615
+ ],
1616
+ "paymentMethod": "bank_transfer",
1617
+ "expiresAt": "\u2026"
1618
+ }
1358
1619
  },
1359
1620
  "provider": "TRANSAK",
1360
1621
  "transitions": []
1361
1622
  },
1362
- "screen-11-kyc-rejected-escalation": {
1623
+ "screen-11-kyc-rejected": {
1363
1624
  "state": {
1364
1625
  "kind": "KYC",
1365
1626
  "phase": "NO_ACTION_REQUIRED",
1366
- "reason": "REJECTED"
1627
+ "reason": "REJECTED",
1628
+ "quote": {
1629
+ "quoteRef": "q_8f2",
1630
+ "fiat": {
1631
+ "currency": "GBP",
1632
+ "amount": "100.00"
1633
+ },
1634
+ "crypto": {
1635
+ "currency": "USDC",
1636
+ "network": "polygon",
1637
+ "amount": "98.41"
1638
+ },
1639
+ "rate": "0.9841",
1640
+ "totalFee": "1.59",
1641
+ "feeBreakdown": [
1642
+ {
1643
+ "id": "provider_fee",
1644
+ "labelFallback": "Processing fee",
1645
+ "amount": "1.59"
1646
+ }
1647
+ ],
1648
+ "paymentMethod": "bank_transfer",
1649
+ "expiresAt": "\u2026"
1650
+ },
1651
+ "failureReason": {
1652
+ "category": "KYC_REJECTED",
1653
+ "retryable": false,
1654
+ "recovery": "CONTACT_SUPPORT"
1655
+ }
1367
1656
  },
1368
1657
  "provider": "TRANSAK",
1369
- "transitions": [
1370
- {
1371
- "id": "escalate",
1372
- "mode": "SUBMIT",
1373
- "endpoint": "GET /fiat/kyc",
1374
- "params": {
1375
- "tier": "escalated"
1376
- }
1377
- }
1378
- ]
1658
+ "transitions": []
1379
1659
  },
1380
1660
  "screen-12-pending-order": {
1381
1661
  "state": {
@@ -1630,6 +1910,18 @@ var FIXTURES = [
1630
1910
  screen: 1,
1631
1911
  stateKey: "QUOTE"
1632
1912
  },
1913
+ {
1914
+ id: "screen-01b-quote-with-pay-sheet",
1915
+ file: "screen-01b-quote-with-pay-sheet.json",
1916
+ source: "CONTRACT_DOC",
1917
+ docRef: "Fiat Contract 0.7 \xA7The contract diff \xB7 example envelopes",
1918
+ screen: 1,
1919
+ stateKey: "QUOTE",
1920
+ notes: [
1921
+ "The 0.7 single-screen shape: the quote carries its own PAY_SHEET, so the provider component is the primary CTA rather than something a later state reveals.",
1922
+ "Quote payload is screen-01-quote verbatim; only the surface and the transition differ."
1923
+ ]
1924
+ },
1633
1925
  {
1634
1926
  id: "screen-02-session-auth",
1635
1927
  file: "screen-02-session-auth.json",
@@ -1641,8 +1933,8 @@ var FIXTURES = [
1641
1933
  {
1642
1934
  id: "screen-03-kyc-input-required",
1643
1935
  file: "screen-03-kyc-input-required.json",
1644
- source: "FE_DOC",
1645
- docRef: "Screen 3 \xB7 Identity form (KYC)",
1936
+ source: "KYC_DOC",
1937
+ docRef: "Fiat KYC \xB7 L1 input round",
1646
1938
  screen: 3,
1647
1939
  stateKey: "KYC/INPUT_REQUIRED",
1648
1940
  notes: ["Carries the FORM_VALUES input spec \u2014 the no-double-carry case of the accepted split."]
@@ -1670,34 +1962,40 @@ var FIXTURES = [
1670
1962
  "TODO(open-decision): report target per card topology is a Transak ask (contract Flow A)."
1671
1963
  ]
1672
1964
  },
1965
+ {
1966
+ id: "screen-08-kyc-capture",
1967
+ file: "screen-08-kyc-capture.json",
1968
+ source: "KYC_DOC",
1969
+ docRef: "Fiat KYC \xB7 embedded Sumsub WebSDK",
1970
+ screen: 8,
1971
+ stateKey: "KYC/CAPTURE",
1972
+ notes: ["The client mounts the Sumsub adapter; no URL or iframe is emitted."]
1973
+ },
1673
1974
  {
1674
1975
  id: "screen-09-kyc-in-review",
1675
1976
  file: "screen-09-kyc-in-review.json",
1676
- source: "FE_DOC",
1677
- docRef: "Screen 9 \xB7 Verifying identity",
1977
+ source: "KYC_DOC",
1978
+ docRef: "Fiat KYC \xB7 pending quote",
1678
1979
  screen: 9,
1679
1980
  stateKey: "KYC/NO_ACTION_REQUIRED:IN_REVIEW"
1680
1981
  },
1681
1982
  {
1682
1983
  id: "screen-10-kyc-on-hold",
1683
1984
  file: "screen-10-kyc-on-hold.json",
1684
- source: "FE_DOC",
1685
- docRef: "Screen 10 \xB7 Verification on hold",
1985
+ source: "KYC_DOC",
1986
+ docRef: "Fiat KYC \xB7 verification on hold",
1686
1987
  screen: 10,
1687
1988
  stateKey: "KYC/NO_ACTION_REQUIRED:ON_HOLD",
1688
1989
  notes: ["transitions: [] and NOT terminal \u2014 the fixture that proves terminality is table data."]
1689
1990
  },
1690
1991
  {
1691
- id: "screen-11-kyc-rejected-escalation",
1692
- file: "screen-11-kyc-rejected-escalation.json",
1693
- source: "FE_DOC",
1694
- docRef: "Screen 11 \xB7 Verification unsuccessful",
1992
+ id: "screen-11-kyc-rejected",
1993
+ file: "screen-11-kyc-rejected.json",
1994
+ source: "KYC_DOC",
1995
+ docRef: "Fiat KYC \xB7 non-retryable rejection",
1695
1996
  screen: 11,
1696
1997
  stateKey: "KYC/NO_ACTION_REQUIRED:REJECTED",
1697
- notes: [
1698
- "TODO(open-decision): the escalation transition is a flagged placeholder \u2014 a GET carrying params is not a real request shape (FE doc Screen 11 + open question 7).",
1699
- "The terminal REJECTED variant (failureReason + transitions []) is described in prose, not published as JSON \u2014 no fixture."
1700
- ]
1998
+ notes: ["Only an explicit non-retryable provider or identity-vendor outcome reaches this state."]
1701
1999
  },
1702
2000
  {
1703
2001
  id: "screen-12-pending-order",
@@ -1742,7 +2040,7 @@ var FIXTURES = [
1742
2040
  docRef: "Screen 16 \xB7 Not available",
1743
2041
  screen: 16,
1744
2042
  stateKey: "BLOCKED",
1745
- notes: ["The only flatly terminal state entry in the table."]
2043
+ notes: ["A flatly terminal availability state; explicit non-retryable KYC rejection is terminal too."]
1746
2044
  },
1747
2045
  {
1748
2046
  id: "worked-example-01-quote",
@@ -1774,10 +2072,6 @@ var FIXTURE_COVERAGE_GAPS = [
1774
2072
  {
1775
2073
  stateKey: "FUN_AUTH",
1776
2074
  reason: "No step response exists in either doc \u2014 shape owned by the auth spike, and FUN_AUTH never renders in fomo. TODO(open-decision): contract \xA7Auth & ref binding, item 5."
1777
- },
1778
- {
1779
- stateKey: "KYC/CAPTURE",
1780
- reason: "Dropped from v1 (FE Screen 8 tombstone, no UK module coverage); no step response was ever published. Kind retained as handshake-gated vocabulary."
1781
2075
  }
1782
2076
  ];
1783
2077
  var fixtureMeta = (id) => {
@@ -1867,6 +2161,16 @@ function checkLegalEmission(state, transitions) {
1867
2161
  `${entry.key} is terminal (table) and must carry transitions: [] \u2014 got ${transitions.length}`
1868
2162
  );
1869
2163
  }
2164
+ const surfaceTargets = new Set(
2165
+ transitions.filter((t) => t.mode === "CLIENT_SURFACE").map((t) => t.report.endpoint)
2166
+ );
2167
+ for (const transition of transitions) {
2168
+ if (transition.mode === "SUBMIT" && surfaceTargets.has(transition.endpoint)) {
2169
+ issues.push(
2170
+ `transition "${transition.id}": SUBMIT ${transition.endpoint} coexists with a CLIENT_SURFACE reporting to it`
2171
+ );
2172
+ }
2173
+ }
1870
2174
  for (const transition of transitions) {
1871
2175
  issues.push(...bodyKeyIssues(transition));
1872
2176
  if (transition.mode === "CLIENT_SURFACE" && !("surface" in state)) {