@volter/twin-stripe 2.0.0 → 2.0.2

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.
Files changed (67) hide show
  1. package/README.md +33 -1
  2. package/dist/src/index.js +6 -4
  3. package/dist/src/manifest.js +8 -3
  4. package/dist/src/screens/checkout.js +20 -6
  5. package/dist/src/screens/connect-oauth.d.ts +27 -0
  6. package/dist/src/screens/connect-oauth.js +414 -0
  7. package/dist/src/screens/connect-settings.d.ts +22 -0
  8. package/dist/src/screens/connect-settings.js +103 -0
  9. package/dist/src/screens/portal.js +2 -0
  10. package/dist/src/semantics/after-payment.d.ts +1 -1
  11. package/dist/src/semantics/after-payment.js +6 -0
  12. package/dist/src/semantics/charges.js +10 -2
  13. package/dist/src/semantics/checkout.js +19 -6
  14. package/dist/src/semantics/connect.js +20 -3
  15. package/dist/src/semantics/invoices.js +4 -0
  16. package/dist/src/semantics/issuing.js +7 -2
  17. package/dist/src/semantics/ledger.d.ts +11 -6
  18. package/dist/src/semantics/ledger.js +40 -21
  19. package/dist/src/semantics/payment-methods.js +2 -0
  20. package/dist/src/semantics/shared.d.ts +5 -1
  21. package/dist/src/semantics/shared.js +14 -3
  22. package/dist/src/semantics/test-cards.d.ts +4 -0
  23. package/dist/src/semantics/test-cards.js +7 -0
  24. package/dist/src/semantics/transfers.js +1 -1
  25. package/dist/src/stripe-capabilities.js +829 -186
  26. package/dist/src/stripe-conformance.d.ts +2 -0
  27. package/dist/src/stripe-conformance.js +11 -2
  28. package/dist/src/stripe-emit.js +2 -2
  29. package/dist/src/stripe-events.js +14 -10
  30. package/dist/src/stripe-mirror-ui.js +3 -3
  31. package/dist/src/stripe-server.js +97 -24
  32. package/dist/src/stripe-shared.d.ts +3 -0
  33. package/dist/src/stripe-shared.js +3 -0
  34. package/dist/src/stripe-twin.js +7 -1
  35. package/dist/src/stripe-version.d.ts +2 -0
  36. package/dist/src/stripe-version.js +2 -0
  37. package/dist/test-fixtures/stripe-known-deviations.json +6 -1
  38. package/dist/test-fixtures/stripe-schemas.json +85 -12
  39. package/package.json +4 -4
  40. package/src/index.ts +6 -4
  41. package/src/manifest.ts +8 -3
  42. package/src/screens/checkout.tsx +21 -6
  43. package/src/screens/connect-oauth.tsx +400 -0
  44. package/src/screens/connect-settings.tsx +121 -0
  45. package/src/screens/portal.tsx +2 -0
  46. package/src/semantics/after-payment.ts +6 -1
  47. package/src/semantics/charges.ts +11 -2
  48. package/src/semantics/checkout.ts +19 -6
  49. package/src/semantics/connect.ts +19 -3
  50. package/src/semantics/invoices.ts +4 -0
  51. package/src/semantics/issuing.ts +7 -2
  52. package/src/semantics/ledger.ts +60 -23
  53. package/src/semantics/payment-methods.ts +2 -0
  54. package/src/semantics/shared.ts +14 -3
  55. package/src/semantics/test-cards.ts +7 -0
  56. package/src/semantics/transfers.ts +1 -1
  57. package/src/stripe-capabilities.ts +826 -182
  58. package/src/stripe-conformance.ts +13 -2
  59. package/src/stripe-emit.ts +2 -2
  60. package/src/stripe-events.ts +14 -10
  61. package/src/stripe-mirror-ui.ts +3 -3
  62. package/src/stripe-server.ts +85 -24
  63. package/src/stripe-shared.ts +3 -0
  64. package/src/stripe-twin.ts +6 -1
  65. package/src/stripe-version.ts +3 -0
  66. package/test-fixtures/stripe-known-deviations.json +6 -1
  67. package/test-fixtures/stripe-schemas.json +85 -12
@@ -297,6 +297,10 @@
297
297
  "status"
298
298
  ],
299
299
  "properties": {
300
+ "allowed_payment_method_types": {
301
+ "type": "array",
302
+ "nullable": true
303
+ },
300
304
  "amount": {
301
305
  "type": "integer"
302
306
  },
@@ -419,6 +423,9 @@
419
423
  "payment_method_types": {
420
424
  "type": "array"
421
425
  },
426
+ "payment_record": {
427
+ "nullable": true
428
+ },
422
429
  "presentment_details": {},
423
430
  "processing": {
424
431
  "nullable": true
@@ -495,6 +502,13 @@
495
502
  "currency": {
496
503
  "type": "string"
497
504
  },
505
+ "customer": {
506
+ "nullable": true
507
+ },
508
+ "customer_account": {
509
+ "type": "string",
510
+ "nullable": true
511
+ },
498
512
  "description": {
499
513
  "type": "string"
500
514
  },
@@ -523,6 +537,9 @@
523
537
  "payment_intent": {
524
538
  "nullable": true
525
539
  },
540
+ "payment_method": {
541
+ "nullable": true
542
+ },
526
543
  "pending_reason": {
527
544
  "enum": [
528
545
  "charge_pending",
@@ -726,6 +743,7 @@
726
743
  ],
727
744
  "type": "string"
728
745
  },
746
+ "status_details": {},
729
747
  "test_clock": {
730
748
  "nullable": true
731
749
  },
@@ -757,6 +775,10 @@
757
775
  "usage"
758
776
  ],
759
777
  "properties": {
778
+ "allowed_payment_method_types": {
779
+ "type": "array",
780
+ "nullable": true
781
+ },
760
782
  "application": {
761
783
  "nullable": true
762
784
  },
@@ -953,6 +975,7 @@
953
975
  "payco": {},
954
976
  "paynow": {},
955
977
  "paypal": {},
978
+ "paypay": {},
956
979
  "payto": {},
957
980
  "pix": {},
958
981
  "promptpay": {},
@@ -962,6 +985,7 @@
962
985
  "satispay": {},
963
986
  "scalapay": {},
964
987
  "sepa_debit": {},
988
+ "sequra": {},
965
989
  "sofort": {},
966
990
  "sunbit": {},
967
991
  "swish": {},
@@ -1204,6 +1228,9 @@
1204
1228
  "tax_code": {
1205
1229
  "nullable": true
1206
1230
  },
1231
+ "tax_details": {
1232
+ "nullable": true
1233
+ },
1207
1234
  "unit_label": {
1208
1235
  "type": "string",
1209
1236
  "nullable": true
@@ -1500,6 +1527,7 @@
1500
1527
  "type": "string",
1501
1528
  "nullable": true
1502
1529
  },
1530
+ "status_details": {},
1503
1531
  "status_transitions": {},
1504
1532
  "subtotal": {
1505
1533
  "type": "integer"
@@ -1579,12 +1607,18 @@
1579
1607
  "type": "array",
1580
1608
  "nullable": true
1581
1609
  },
1610
+ "frozen_fields": {
1611
+ "type": "array"
1612
+ },
1582
1613
  "id": {
1583
1614
  "type": "string"
1584
1615
  },
1585
1616
  "invoice": {
1586
1617
  "nullable": true
1587
1618
  },
1619
+ "invoicing_rules": {
1620
+ "type": "array"
1621
+ },
1588
1622
  "livemode": {
1589
1623
  "type": "boolean"
1590
1624
  },
@@ -2045,6 +2079,10 @@
2045
2079
  "type": "boolean",
2046
2080
  "nullable": true
2047
2081
  },
2082
+ "allowed_payment_method_types": {
2083
+ "type": "array",
2084
+ "nullable": true
2085
+ },
2048
2086
  "amount_subtotal": {
2049
2087
  "type": "integer",
2050
2088
  "nullable": true
@@ -2575,7 +2613,12 @@
2575
2613
  },
2576
2614
  "livemode": {
2577
2615
  "type": "boolean"
2578
- }
2616
+ },
2617
+ "company": {},
2618
+ "groups": {
2619
+ "nullable": true
2620
+ },
2621
+ "individual": {}
2579
2622
  }
2580
2623
  },
2581
2624
  "transfer": {
@@ -2583,7 +2626,6 @@
2583
2626
  "required": [
2584
2627
  "amount",
2585
2628
  "amount_reversed",
2586
- "balance_transaction",
2587
2629
  "created",
2588
2630
  "currency",
2589
2631
  "id",
@@ -2645,13 +2687,13 @@
2645
2687
  "tax_rate": {
2646
2688
  "type": "object",
2647
2689
  "required": [
2648
- "id",
2649
- "object",
2650
2690
  "active",
2651
2691
  "created",
2652
2692
  "display_name",
2693
+ "id",
2653
2694
  "inclusive",
2654
2695
  "livemode",
2696
+ "object",
2655
2697
  "percentage"
2656
2698
  ],
2657
2699
  "properties": {
@@ -2709,20 +2751,36 @@
2709
2751
  "tax_type": {
2710
2752
  "type": "string",
2711
2753
  "nullable": true
2754
+ },
2755
+ "effective_percentage": {
2756
+ "type": "number",
2757
+ "nullable": true
2758
+ },
2759
+ "flat_amount": {
2760
+ "nullable": true
2761
+ },
2762
+ "rate_type": {
2763
+ "enum": [
2764
+ "flat_amount",
2765
+ "percentage"
2766
+ ],
2767
+ "type": "string",
2768
+ "nullable": true
2712
2769
  }
2713
2770
  }
2714
2771
  },
2715
2772
  "tax.calculation": {
2716
2773
  "type": "object",
2717
2774
  "required": [
2718
- "object",
2719
2775
  "amount_total",
2720
2776
  "currency",
2721
2777
  "customer_details",
2722
- "line_items",
2778
+ "livemode",
2779
+ "object",
2723
2780
  "tax_amount_exclusive",
2724
2781
  "tax_amount_inclusive",
2725
- "tax_breakdown"
2782
+ "tax_breakdown",
2783
+ "tax_date"
2726
2784
  ],
2727
2785
  "properties": {
2728
2786
  "id": {
@@ -2772,18 +2830,24 @@
2772
2830
  },
2773
2831
  "tax_date": {
2774
2832
  "type": "integer"
2833
+ },
2834
+ "ship_from_details": {
2835
+ "nullable": true
2775
2836
  }
2776
2837
  }
2777
2838
  },
2778
2839
  "tax.calculation_line_item": {
2779
2840
  "type": "object",
2780
2841
  "required": [
2781
- "id",
2782
- "object",
2783
2842
  "amount",
2784
2843
  "amount_tax",
2844
+ "id",
2785
2845
  "livemode",
2786
- "tax_behavior"
2846
+ "object",
2847
+ "quantity",
2848
+ "reference",
2849
+ "tax_behavior",
2850
+ "tax_code"
2787
2851
  ],
2788
2852
  "properties": {
2789
2853
  "id": {
@@ -2827,18 +2891,27 @@
2827
2891
  },
2828
2892
  "tax_code": {
2829
2893
  "type": "string"
2894
+ },
2895
+ "metadata": {
2896
+ "type": "object",
2897
+ "nullable": true
2898
+ },
2899
+ "performance_location": {
2900
+ "type": "string",
2901
+ "nullable": true
2830
2902
  }
2831
2903
  }
2832
2904
  },
2833
2905
  "tax.registration": {
2834
2906
  "type": "object",
2835
2907
  "required": [
2836
- "id",
2837
- "object",
2838
2908
  "active_from",
2839
2909
  "country",
2840
2910
  "country_options",
2911
+ "created",
2912
+ "id",
2841
2913
  "livemode",
2914
+ "object",
2842
2915
  "status"
2843
2916
  ],
2844
2917
  "properties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/twin-stripe",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Local Stripe twin — a faithful, stateful local Stripe API your real `stripe` SDK talks to unmodified. Mirror, simulate, and fork. Built on @volter/world-core.",
5
5
  "keywords": [
6
6
  "twin",
@@ -56,15 +56,15 @@
56
56
  "postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
57
57
  },
58
58
  "dependencies": {
59
- "@volter/world-ui": "0.1.0",
59
+ "@volter/world-ui": "0.1.2",
60
60
  "react": "^19.2.7",
61
61
  "react-dom": "^19.2.7"
62
62
  },
63
63
  "peerDependencies": {
64
- "@volter/world-core": "2.0.0"
64
+ "@volter/world-core": "2.0.2"
65
65
  },
66
66
  "devDependencies": {
67
- "@volter/world-core": "2.0.0",
67
+ "@volter/world-core": "2.0.2",
68
68
  "@volter/world-tooling": "0.1.0",
69
69
  "@types/bun": "^1.2.20",
70
70
  "@types/node": "^24.0.0",
package/src/index.ts CHANGED
@@ -93,7 +93,7 @@ export const pack: TwinPack = {
93
93
  // one live API host — declared HERE, not in the central maps (descriptor-first exemplar (adding-a-twin.md §3);
94
94
  // the pack-facts artifact carries them to covers/inspect-project and inject.cjs).
95
95
  // NEXT_PRIVATE_STRIPE_* joined the stem list in the adoption-facts sweep 2026-08-31.
96
- // `stripeconnect` is deliberately NOT claimed: the Connect area is unmodeled, so a repo
96
+ // `stripeconnect` is deliberately NOT claimed: no Connect-only credential stem is modeled, so a repo
97
97
  // holding only that credential is build-surface, not coverage this twin can honor.
98
98
  adoption: {
99
99
  // Stripe's official Python bindings.
@@ -101,15 +101,17 @@ export const pack: TwinPack = {
101
101
  sdks: ['stripe'], envStems: ['STRIPE', 'NEXTPRIVATESTRIPE'],
102
102
  },
103
103
  // The API and the hosted flows (src/screens): checkout.stripe.com's payment page, billing.stripe.com's customer
104
- // portal, connect.stripe.com's onboarding, verify.stripe.com's identity check and the bank-linking flow Stripe.js
104
+ // portal, connect.stripe.com's onboarding and its OAuth endpoints (/oauth/authorize, /oauth/token, /oauth/deauthorize:
105
+ // src/screens/connect-oauth.tsx), verify.stripe.com's identity check and the bank-linking flow Stripe.js
105
106
  // opens: an application that sends a person to one of their urls lands in the World. dashboard.stripe.com serves the
106
- // Dashboard's Public details page (src/screens/public-details.tsx), where the operator sets the name customers see;
107
+ // Dashboard's Public details page (src/screens/public-details.tsx), where the operator sets the name customers see,
108
+ // and its Connect OAuth settings (src/screens/connect-settings.tsx: the client_id, OAuth on, the redirect URIs);
107
109
  // claiming the host makes every other dashboard.stripe.com path an unclaimed path on a twinned host, refused in a
108
110
  // World rather than sent to the real Dashboard. js.stripe.com also serves
109
111
  // Stripe.js itself, the client library an application's page loads (stripe-js.ts): /v3/, /v3/stripe.js and /<release train>/stripe.js.
110
112
  hosts: [
111
113
  { host: 'api.stripe.com' }, { host: 'checkout.stripe.com', pathPattern: '^/c/pay/' }, { host: 'billing.stripe.com', pathPattern: '^/p/session/' },
112
- { host: 'connect.stripe.com', pathPattern: '^/setup/' }, { host: 'dashboard.stripe.com', pathPattern: '^/settings/public/?$' }, { host: 'verify.stripe.com', pathPattern: '^/start/' }, { host: 'js.stripe.com', pathPattern: '^/v3/financial-connections/|^/v3/?$|^/(v3|[a-z]+)/stripe\\.js$' },
114
+ { host: 'connect.stripe.com', pathPattern: '^/setup/|^/oauth/(authorize|token|deauthorize)/?$' }, { host: 'dashboard.stripe.com', pathPattern: '^/settings/public/?$|^/(test/)?settings/connect(/onboarding-options/oauth)?/?$' }, { host: 'verify.stripe.com', pathPattern: '^/start/' }, { host: 'js.stripe.com', pathPattern: '^/v3/financial-connections/|^/v3/?$|^/(v3|[a-z]+)/stripe\\.js$' },
113
115
  ],
114
116
  // DELIVER support: signed event synthesis from twin state (`world-stripe emit`).
115
117
  emitter: stripeEmitter,
package/src/manifest.ts CHANGED
@@ -567,6 +567,11 @@ export const manifest: DerivedManifest = {
567
567
  demand: '3 of 85 applications onboard connected accounts with Account Links', controls: ['Agree and submit', '← Return to platform'],
568
568
  source: 'https://docs.stripe.com/connect/hosted-onboarding',
569
569
  },
570
+ {
571
+ id: 'connect-oauth', kind: 'flow', host: 'connect.stripe.com', path: '/oauth/authorize', status: 'done',
572
+ demand: "Cal.com's Stripe app connects each organizer's Standard account through Connect OAuth", controls: ['Skip this form', 'Deny access'],
573
+ source: 'https://docs.stripe.com/connect/oauth-reference',
574
+ },
570
575
  {
571
576
  id: 'identity-verification', kind: 'flow', host: 'verify.stripe.com', path: '/start/{verification_session}', status: 'done',
572
577
  demand: 'a verification report is written only when a person completes this page', controls: ['Verify', 'Fail verification'],
@@ -578,9 +583,9 @@ export const manifest: DerivedManifest = {
578
583
  source: 'https://docs.stripe.com/js/financial_connections/collect_financial_connections_accounts',
579
584
  },
580
585
  {
581
- id: 'dashboard', kind: 'workspace', host: 'dashboard.stripe.com', path: '/test/{section}; /settings/public', status: 'done',
582
- demand: 'testers and agents inspect payments, customers and subscriptions; an operator sets the business name Checkout shows (Public details)',
583
- controls: ['Business name', 'Save'], source: 'https://docs.stripe.com/dashboard/basics',
586
+ id: 'dashboard', kind: 'workspace', host: 'dashboard.stripe.com', path: '/test/{section}; /settings/public; /settings/connect/onboarding-options/oauth', status: 'done',
587
+ demand: 'testers and agents inspect payments, customers and subscriptions; an operator sets the business name Checkout shows (Public details) and a Connect platform its OAuth client_id and redirect URIs',
588
+ controls: ['Business name', 'Save', 'Enable OAuth for Standard accounts', 'Redirect URIs'], source: 'https://docs.stripe.com/dashboard/basics',
584
589
  },
585
590
  ],
586
591
  resources: {
@@ -111,6 +111,15 @@ function afterTrial(session: Row): number {
111
111
  return subtotal - (coupon && coupon.duration !== 'once' ? applyCouponDiscount(subtotal, coupon) : 0);
112
112
  }
113
113
 
114
+ const EMAIL = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
115
+
116
+ /** The email of the session's customer when it has a valid one: "If the Customer already has a valid email set, the email
117
+ * will be prefilled and not editable in Checkout" (docs.stripe.com/api/checkout/sessions/create#create_checkout_session-customer). */
118
+ function customerEmailOf(session: Row): string | undefined {
119
+ const email = (session._customer as Row | undefined)?.email;
120
+ return typeof email === 'string' && EMAIL.test(email) ? email : undefined;
121
+ }
122
+
114
123
  function page(session: Row, id: string, now: number, values: Record<string, string> = {}, error?: string): Response {
115
124
  const lines = (((session.line_items as { data?: Row[] } | undefined)?.data) ?? []).map((item) => ({
116
125
  name: String(item.description ?? 'Item'),
@@ -122,9 +131,12 @@ function page(session: Row, id: string, now: number, values: Record<string, stri
122
131
  const trial = trialOf(session, now);
123
132
  const submit = mode === 'setup' ? 'Save card' : mode === 'subscription' ? (trial ? 'Start trial' : 'Subscribe') : 'Pay';
124
133
  const trialText = trial ? trialSummary(session, trial) : undefined;
125
- const contact: PaymentField[] = typeof session.customer_email === 'string'
126
- ? [{ id: 'email', label: 'Email', type: 'email', autoComplete: 'email', value: session.customer_email }]
127
- : [{ id: 'email', label: 'Email', type: 'email', autoComplete: 'email', placeholder: 'email@example.com', value: values.email ?? '' }];
134
+ const onFile = customerEmailOf(session);
135
+ const contact: PaymentField[] = onFile
136
+ ? [{ id: 'email', label: 'Email', type: 'email', autoComplete: 'email', value: onFile, readOnly: true }]
137
+ : typeof session.customer_email === 'string'
138
+ ? [{ id: 'email', label: 'Email', type: 'email', autoComplete: 'email', value: session.customer_email }]
139
+ : [{ id: 'email', label: 'Email', type: 'email', autoComplete: 'email', placeholder: 'email@example.com', value: values.email ?? '' }];
128
140
  const card: PaymentField[] = [
129
141
  { id: 'cardNumber', label: 'Card number', autoComplete: 'cc-number', placeholder: '1234 1234 1234 1234', value: values.cardNumber ?? '' },
130
142
  { id: 'cardExpiry', label: 'Expiration', autoComplete: 'cc-exp', placeholder: 'MM / YY', value: values.cardExpiry ?? '', format: 'card-expiry' },
@@ -191,9 +203,10 @@ export function cardAnswer(v: Record<string, string>, nowSeconds: number, charge
191
203
  }
192
204
 
193
205
  async function pay(ctx: SemanticsContext, id: string, session: Row, v: Record<string, string>): Promise<Response> {
194
- const email = typeof session.customer_email === 'string' ? session.customer_email : (v.email ?? '').trim();
206
+ const onFile = customerEmailOf(session);
207
+ const email = onFile ?? (typeof session.customer_email === 'string' ? session.customer_email : (v.email ?? '').trim());
195
208
  if (session.mode !== 'setup' || !session.customer) {
196
- if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return page(session, id, Number(ctx.now()), v, 'Your email address is incomplete.');
209
+ if (!EMAIL.test(email)) return page(session, id, Number(ctx.now()), v, 'Your email address is incomplete.');
197
210
  }
198
211
  // a trial charges nothing now unless a one-time price is on the first invoice
199
212
  const oneTimeDue = (((session.line_items as { data?: Row[] } | undefined)?.data) ?? []).some((it, i) => isOneTime(it, inlineOf(session, i)));
@@ -207,6 +220,8 @@ async function pay(ctx: SemanticsContext, id: string, session: Row, v: Record<st
207
220
  const customer = await created(ctx, 'customer', { id: cid, email, ...(v.billingName ? { name: v.billingName } : {}) }, { livemode: false, ...newCustomer(cid) });
208
221
  existing = { ...session, customer: customer.id };
209
222
  }
223
+ // "If the Customer does not have a valid email, Checkout will set the email entered during the session on the Customer" (the same page)
224
+ if (typeof session.customer === 'string' && !onFile && EMAIL.test(email)) await ctx.write('customer', session.customer, { email }, 'customer.update');
210
225
  const details: Row = {
211
226
  customer_details: { email, name: v.billingName || null, address: { country: v.billingCountry || null, postal_code: v.billingPostalCode || null, city: null, line1: null, line2: null, state: null }, phone: null, tax_exempt: 'none', tax_ids: [] },
212
227
  ...(existing.customer !== session.customer ? { customer: existing.customer } : {}),
@@ -246,7 +261,7 @@ export function stripeCheckoutFlow(scope: { root?: string; clock?: () => string
246
261
  if (session.status !== 'open' && (request.method === 'GET' || ctx.legal(CS, 'status', COMPLETE_OPERATION.id, String(session.status), 'complete', id, 'external'))) return gone();
247
262
  const stored = ctx.row(CS, id) ?? {};
248
263
  const coupon = (stored._discount as Row | undefined)?.coupon;
249
- const full = { ...session, _subscription_data: stored._subscription_data, _price_data: stored._price_data, _merchant: merchantOf(ctx, stored), _coupon: typeof coupon === 'string' ? ctx.get('coupon', coupon) : undefined };
264
+ const full = { ...session, _subscription_data: stored._subscription_data, _price_data: stored._price_data, _merchant: merchantOf(ctx, stored), _coupon: typeof coupon === 'string' ? ctx.get('coupon', coupon) : undefined, _customer: typeof session.customer === 'string' ? ctx.get('customer', session.customer) : undefined };
250
265
  return request.method === 'GET' ? page(full, id, Number(ctx.now())) : pay(ctx, id, full, values);
251
266
  };
252
267
  }