@siglume/direct-request-payment 0.4.23 → 0.4.25

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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.25 - 2026-06-20
4
+
5
+ - Added a real PostgreSQL/MySQL ORM matrix for the Express SQL order store,
6
+ covering Prisma + PostgreSQL, TypeORM + PostgreSQL, Sequelize + MySQL,
7
+ Drizzle + PostgreSQL, and Drizzle + MySQL.
8
+ - The matrix verifies concurrent Hosted Checkout starts create one session,
9
+ expired checkout attempts create a new attempt, webhook handler failures stay
10
+ retryable, duplicate webhook events stay idempotent, and paid status reaches
11
+ the merchant order table.
12
+ - Hardened the TypeORM and Drizzle SQL executors so ORM-specific affected-row
13
+ and row-return shapes are normalized before checkout/webhook safety decisions.
14
+ - Added PostgreSQL and MySQL service-backed CI coverage, and made npm release
15
+ publish only after the ORM matrix passes.
16
+
17
+ ## 0.4.24 - 2026-06-20
18
+
19
+ - Split CLI checks into `preflight` for pre-mount setup checks and `verify` for
20
+ full Hosted Checkout plus signed webhook delivery verification, so the
21
+ 10-minute guide no longer asks users to verify a webhook route before it
22
+ exists.
23
+ - Made merchant account status fail-closed in readiness checks; only `active`
24
+ and `ready` pass.
25
+ - Reworked Express and FastAPI checkout attempts to support attempt generations,
26
+ expiry/failure recovery, and one active checkout attempt per order enforced by
27
+ a database unique key.
28
+ - Added Express E2E coverage for 50 concurrent checkout starts creating exactly
29
+ one Hosted Checkout session, new attempt creation after expiry, and stale
30
+ non-transactional webhook `processing` recovery.
31
+ - Added FastAPI SQLAlchemy expiry retry coverage and made the adapter configurable
32
+ for existing product order table/column names.
33
+ - Added a FastAPI `AsyncSession` SQLAlchemy adapter and E2E coverage for async
34
+ checkout concurrency, webhook idempotency, and expired-session retry.
35
+ - Marked readiness probe webhooks so generated routes ignore them instead of
36
+ writing manual-review records.
37
+ - Updated sandbox checkout to follow the returned success redirect after
38
+ confirmation and expose metered summary responses with seller-borne Micro /
39
+ Nano accounting fields.
40
+
3
41
  ## 0.4.23 - 2026-06-20
4
42
 
5
43
  - Made the local SDRP sandbox reject invalid checkout input early, including
package/README.md CHANGED
@@ -86,10 +86,11 @@ CLI-first:
86
86
 
87
87
  ```bash
88
88
  npm install @siglume/direct-request-payment
89
- npx siglume-sdrp sandbox --webhook-url http://localhost:3000/payments/webhooks/siglume
90
- npx siglume-check readiness --sandbox
91
- npx siglume-check readiness
92
89
  npx siglume-sdrp init express --target src/siglume
90
+ # mount the routes, start your app, then:
91
+ npx siglume-sdrp sandbox --webhook-url http://localhost:3000/payments/webhooks/siglume
92
+ npx siglume-check verify --sandbox
93
+ npx siglume-check verify
93
94
  ```
94
95
 
95
96
  or:
@@ -97,14 +98,22 @@ or:
97
98
  ```bash
98
99
  pip install siglume-direct-request-payment
99
100
  siglume-sdrp init fastapi --target app/siglume
101
+ # mount the routes, start your app, then use the npm sandbox for local checkout:
102
+ npx siglume-sdrp sandbox --webhook-url http://localhost:3000/payments/webhooks/siglume
103
+ siglume-check verify --sandbox
100
104
  ```
101
105
 
102
106
  The sandbox command starts a local Siglume-compatible API that creates fake
103
107
  checkout sessions and sends signed webhooks to your product. It never charges a
104
- wallet; see [SDRP Sandbox](./docs/sandbox.md). The readiness command checks
105
- account, billing, origin, webhook, and Hosted Checkout availability before you
106
- write checkout code. It also confirms the webhook subscription and signed test
107
- delivery when API probes are enabled.
108
+ wallet; see [SDRP Sandbox](./docs/sandbox.md). `siglume-check preflight`
109
+ checks account, billing, origin, webhook subscription metadata, and Hosted
110
+ Checkout availability before route mounting. `siglume-check verify` additionally
111
+ requires a signed webhook test delivery, so run it only after your webhook route
112
+ is mounted and your app is running.
113
+
114
+ The FastAPI templates include both sync `Session` and async `AsyncSession`
115
+ SQLAlchemy adapters. The sandbox also exposes metered summary endpoints so you
116
+ can verify Micro / Nano seller-borne fee fields before using live credentials.
108
117
 
109
118
  Before implementation, confirm Hosted Checkout readiness in
110
119
  [Troubleshooting](./docs/troubleshooting.md#hosted-checkout-readiness). For
@@ -125,7 +134,7 @@ fulfilling orders.
125
134
 
126
135
  | Use case | Recommended path | 10-minute integration path? | Production work still required |
127
136
  | --- | --- | --- | --- |
128
- | EC one-time Standard payment | Hosted Checkout | Yes, with `siglume-check readiness` and `siglume-sdrp init` | Product DB adapter, refund/support process, monitoring |
137
+ | EC one-time Standard payment | Hosted Checkout | Yes, with `siglume-sdrp init`, sandbox, and `siglume-check verify` | Product DB adapter, refund/support process, monitoring |
129
138
  | Game consumables | Hosted Checkout or agent/API | Conditional | Idempotent entitlement grants, disconnect recovery, Micro / Nano settlement reconciliation and past-due handling |
130
139
  | Paid API / AtoA | Direct API or Siglume marketplace tool | Conditional | Request idempotency, buyer auth context, reconciliation |
131
140
  | SaaS subscription | Recurring challenge plus raw API | No | Renewal, cancellation, failed renewal, plan-change lifecycle |
@@ -139,7 +148,7 @@ case `createCheckoutSession(...)` / `getCheckoutSession(...)` raises
139
148
  `HostedCheckoutNotAvailableError` instead of exposing the raw rollout 404/409.
140
149
  Keep the signed `direct_payment.confirmed` webhook as the durable signal, and
141
150
  inspect its settlement machine fields before marking any order paid.
142
- Check readiness before you build the flow; see
151
+ Run preflight before route mounting and verify after your webhook is live; see
143
152
  [Hosted Checkout readiness](./docs/troubleshooting.md#hosted-checkout-readiness).
144
153
 
145
154
  Hosted Checkout is a Siglume-hosted page that turns a "Pay with Siglume" button
@@ -265,9 +274,11 @@ transaction. Micro / Nano settlement batches are aggregated on-chain after the
265
274
  weekly or monthly close, or earlier when the fixed amount threshold is reached.
266
275
 
267
276
  Micro Payment and Nano Payment are not separate products you opt into; they are
268
- amount bands Siglume applies on your behalf. Your integration code is the same
269
- regardless of which band a payment falls into. The full fee table and the exact
270
- weekly / monthly settlement schedule plus early threshold settlement rule are in
277
+ amount bands Siglume applies on your behalf. Payment initiation is the same
278
+ across amount bands. Fulfillment, revenue recognition, reconciliation, past-due
279
+ handling, and terminal write-off handling differ for Micro / Nano. The full fee
280
+ table and the exact weekly / monthly settlement schedule plus early threshold
281
+ settlement rule are in
271
282
  [docs/pricing.md](./docs/pricing.md).
272
283
  Provider revenue in the Micro and Nano bands is not settled revenue until the
273
284
  aggregated on-chain settlement succeeds. Siglume keeps outstanding failed
@@ -27,8 +27,14 @@ async function main() {
27
27
  printHelp();
28
28
  return;
29
29
  }
30
- if (command === "readiness" || command === "doctor") {
31
- await readiness(parseArgs(args));
30
+ if (command === "readiness" || command === "verify" || command === "doctor") {
31
+ await readiness(parseArgs(args), { requireProbe: true, label: command === "verify" ? "verify" : "readiness" });
32
+ return;
33
+ }
34
+ if (command === "preflight") {
35
+ const options = parseArgs(args);
36
+ options.probe = false;
37
+ await readiness(options, { requireProbe: false, label: "preflight" });
32
38
  return;
33
39
  }
34
40
  if (command === "sandbox") {
@@ -46,7 +52,9 @@ function printHelp() {
46
52
  console.log(`Siglume SDRP integration CLI
47
53
 
48
54
  Usage:
55
+ siglume-check preflight --merchant <key> --origin <https://shop.example> --webhook-url <https://api.example/siglume/webhook>
49
56
  siglume-check readiness --merchant <key> --origin <https://shop.example> --webhook-url <https://api.example/siglume/webhook>
57
+ siglume-check verify --merchant <key> --origin <https://shop.example> --webhook-url <https://api.example/siglume/webhook>
50
58
  siglume-sdrp sandbox --webhook-url <http://localhost:3000/payments/webhooks/siglume>
51
59
  siglume-sdrp init express --target src/siglume
52
60
  siglume-sdrp init fastapi --target app/siglume
@@ -60,7 +68,7 @@ Readiness options:
60
68
  --base-url <url> Siglume API base URL. Defaults to SIGLUME_API_BASE or production.
61
69
  --sandbox Use the local sandbox default API base (http://127.0.0.1:8787/v1).
62
70
  --no-api Validate local config only; do not call Siglume.
63
- --no-probe Partial API check only; readiness will not be reported as ready.
71
+ --no-probe Partial API check only; readiness/verify will not be reported as ready. preflight sets this automatically.
64
72
  --json Print machine-readable JSON.
65
73
 
66
74
  Sandbox options:
@@ -101,7 +109,7 @@ function parseArgs(args) {
101
109
  return out;
102
110
  }
103
111
 
104
- async function readiness(options) {
112
+ async function readiness(options, mode = { requireProbe: true, label: "readiness" }) {
105
113
  const checks = [];
106
114
  const sandboxMode = Boolean(options.sandbox) || String(process.env.SIGLUME_ENV || "").toLowerCase() === "sandbox";
107
115
  const merchant = options.merchant || process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT || "";
@@ -133,7 +141,7 @@ async function readiness(options) {
133
141
  check(checks, "merchant_exists", Boolean(account.merchant), "Run merchant setup before checkout.");
134
142
  check(checks, "billing_mandate", Boolean(account.billing_mandate_id), "Complete the merchant billing mandate wallet approval.");
135
143
  check(checks, "billing_status_active", activeLike(account.billing_status), `Billing status is ${account.billing_status || "unknown"}; it must be active before accepting payments.`);
136
- warnIf(checks, "merchant_status", account.status && !activeLike(account.status), `Merchant status is ${account.status}; confirm it is allowed to accept payments.`);
144
+ check(checks, "merchant_status_active", merchantStatusAllowed(account.status), `Merchant status is ${account.status || "unknown"}; it must be active or ready before accepting payments.`);
137
145
  } catch (error) {
138
146
  check(checks, "merchant_api", false, apiErrorMessage(error, "Could not read the merchant account."));
139
147
  }
@@ -162,8 +170,10 @@ async function readiness(options) {
162
170
  }
163
171
  }
164
172
 
165
- if (!options.probe && !hasFailures(checks)) {
173
+ if (!options.probe && mode.requireProbe && !hasFailures(checks)) {
166
174
  check(checks, "hosted_checkout_probe", false, "--no-probe skips Hosted Checkout and webhook delivery probes. Remove --no-probe for readiness.");
175
+ } else if (!options.probe && !mode.requireProbe && !hasFailures(checks)) {
176
+ check(checks, "delivery_probe_skipped", true, "preflight only; run siglume-check verify after mounting and starting the webhook route.");
167
177
  }
168
178
 
169
179
  if (options.probe && !hasFailures(checks)) {
@@ -205,7 +215,11 @@ async function readiness(options) {
205
215
  if (ok && !options.api) {
206
216
  console.log("Local config checks passed. API, Hosted Checkout, and webhook delivery readiness were not verified.");
207
217
  } else {
208
- console.log(ok ? `Ready for 10-minute SDRP integration (${sandboxMode ? "sandbox" : "live"}).` : "Not ready. Fix the FAIL items before coding checkout.");
218
+ if (ok && !options.probe && !mode.requireProbe) {
219
+ console.log(`Preflight passed (${sandboxMode ? "sandbox" : "live"}). Mount the routes, start your app, then run siglume-check verify.`);
220
+ } else {
221
+ console.log(ok ? `Ready for 10-minute SDRP integration (${sandboxMode ? "sandbox" : "live"}).` : `Not ready. Fix the FAIL items before ${mode.label === "preflight" ? "mounting checkout" : "opening checkout"}.`);
222
+ }
209
223
  }
210
224
  }
211
225
  if (!ok) {
@@ -233,7 +247,7 @@ async function init(args) {
233
247
  }
234
248
  await copyDir(from, to, Boolean(parsed.force));
235
249
  console.log(`Copied ${framework} SDRP integration files to ${to}`);
236
- console.log("Wire the exported router into your app, then run siglume-check readiness before opening checkout.");
250
+ console.log("Wire the exported router into your app, start it, then run siglume-check verify before opening checkout.");
237
251
  }
238
252
 
239
253
  async function sandbox(options) {
@@ -260,6 +274,7 @@ async function sandbox(options) {
260
274
  subscriptionId: "whsub_sandbox_local",
261
275
  sessions: new Map(),
262
276
  deliveries: [],
277
+ meteredUsageEvents: [],
263
278
  };
264
279
 
265
280
  const server = createServer(async (req, res) => {
@@ -294,7 +309,7 @@ async function sandbox(options) {
294
309
  console.log(`SHOP_PUBLIC_ORIGIN=${origin}`);
295
310
  console.log(`SHOP_WEBHOOK_URL=${webhookUrl}`);
296
311
  console.log("");
297
- console.log(`Then run: siglume-check readiness --sandbox`);
312
+ console.log(`Then run after your app is running: siglume-check verify --sandbox`);
298
313
  }
299
314
  }
300
315
 
@@ -417,6 +432,35 @@ async function handleSandboxRequest(req, res, state, port) {
417
432
  return;
418
433
  }
419
434
 
435
+ if (req.method === "GET" && url.pathname === "/v1/sdrp/metered/my-summary") {
436
+ sendEnvelope(res, 200, sandboxBuyerMeteredSummary(state, url));
437
+ return;
438
+ }
439
+
440
+ if (req.method === "GET" && url.pathname === "/v1/sdrp/metered/provider/summary") {
441
+ sendEnvelope(res, 200, sandboxProviderMeteredSummary(state, url));
442
+ return;
443
+ }
444
+
445
+ if (req.method === "GET" && (
446
+ url.pathname === "/v1/sdrp/metered/my-usage-events"
447
+ || url.pathname === "/v1/sdrp/metered/provider/usage-events"
448
+ )) {
449
+ sendEnvelope(res, 200, {
450
+ items: filterSandboxMeteredUsage(state, url),
451
+ next_cursor: null,
452
+ });
453
+ return;
454
+ }
455
+
456
+ if (req.method === "GET" && url.pathname === "/v1/sdrp/metered/provider/settlement-batches") {
457
+ sendEnvelope(res, 200, {
458
+ items: sandboxSettlementBatches(state, url),
459
+ next_cursor: null,
460
+ });
461
+ return;
462
+ }
463
+
420
464
  if (req.method === "GET" && url.pathname.startsWith("/pay/")) {
421
465
  const sessionId = decodeURIComponent(url.pathname.split("/").pop() || "");
422
466
  const session = state.sessions.get(sessionId);
@@ -444,6 +488,10 @@ async function handleSandboxRequest(req, res, state, port) {
444
488
  session.requirement_id = `dpr_sandbox_${sessionId}`;
445
489
  const event = sandboxPaymentConfirmedEvent(session);
446
490
  session.confirmation_event = event;
491
+ const usageEvent = sandboxMeteredUsageEvent(session, event);
492
+ if (usageEvent) {
493
+ state.meteredUsageEvents.unshift(usageEvent);
494
+ }
447
495
  await deliverSandboxWebhook(state, event);
448
496
  sendEnvelope(res, 200, sandboxConfirmResponse(session, event));
449
497
  return;
@@ -487,6 +535,7 @@ function sandboxEvent({ event_type, data }) {
487
535
  function sandboxPaymentConfirmedEvent(session) {
488
536
  const pricingBand = classifySandboxAmount(session.currency, Number(session.amount_minor));
489
537
  const metered = pricingBand === "micro" || pricingBand === "nano";
538
+ const accounting = metered ? sandboxMeteredAccounting(session, pricingBand) : null;
490
539
  return sandboxEvent({
491
540
  event_type: "direct_payment.confirmed",
492
541
  data: {
@@ -503,10 +552,188 @@ function sandboxPaymentConfirmedEvent(session) {
503
552
  settlement_status: metered ? "pending_settlement" : "settled",
504
553
  chain_receipt_id: metered ? undefined : `chain_sandbox_${session.session_id}`,
505
554
  environment: "sandbox",
555
+ ...(accounting ? {
556
+ provider_gross_amount_minor: accounting.provider_gross_amount_minor,
557
+ provider_usage_amount_minor: accounting.provider_usage_amount_minor,
558
+ protocol_fee_minor: accounting.protocol_fee_minor,
559
+ provider_receivable_minor: accounting.provider_receivable_minor,
560
+ gross_buyer_debit_minor: accounting.gross_buyer_debit_minor,
561
+ buyer_debit_minor: accounting.buyer_debit_minor,
562
+ rounding_delta_minor: accounting.rounding_delta_minor,
563
+ settlement_threshold_minor: accounting.settlement_threshold_minor,
564
+ } : {}),
506
565
  },
507
566
  });
508
567
  }
509
568
 
569
+ function sandboxMeteredUsageEvent(session, event) {
570
+ const pricingBand = classifySandboxAmount(session.currency, Number(session.amount_minor));
571
+ if (pricingBand !== "micro" && pricingBand !== "nano") return null;
572
+ const accounting = sandboxMeteredAccounting(session, pricingBand);
573
+ return {
574
+ metered_usage_id: `mu_sandbox_${session.session_id}`,
575
+ created_at: new Date().toISOString(),
576
+ plan_type: pricingBand,
577
+ pricing_band: pricingBand,
578
+ settlement_cadence: pricingBand === "micro" ? "weekly" : "monthly",
579
+ period_start: new Date().toISOString(),
580
+ period_end: null,
581
+ listing_id: session.metadata_jsonb?.listing_id || "sandbox_listing",
582
+ capability_key: session.metadata_jsonb?.capability_key || "sandbox_checkout",
583
+ operation_key: session.metadata_jsonb?.operation_key || "checkout.confirm",
584
+ currency: session.currency,
585
+ token_symbol: session.token_symbol,
586
+ status: "open",
587
+ settlement_batch_id: null,
588
+ buyer_period_ref: `buyer_sandbox:${session.merchant}:${session.token_symbol}:${pricingBand}`,
589
+ requirement_id: session.requirement_id,
590
+ challenge_hash: session.challenge_hash,
591
+ event_id: event.id,
592
+ ...accounting,
593
+ };
594
+ }
595
+
596
+ function sandboxMeteredAccounting(session, pricingBand) {
597
+ const currency = String(session.currency || "").toUpperCase();
598
+ const providerGrossTenths = Number(session.amount_minor) * 10;
599
+ const protocolFeeTenths = sandboxProtocolFeeTenths(currency, pricingBand);
600
+ const providerReceivableTenths = providerGrossTenths - protocolFeeTenths;
601
+ return {
602
+ provider_gross_amount_minor: formatTenths(providerGrossTenths),
603
+ provider_usage_amount_minor: formatTenths(providerGrossTenths),
604
+ protocol_fee_minor: formatTenths(protocolFeeTenths),
605
+ provider_receivable_minor: formatTenths(providerReceivableTenths),
606
+ gross_buyer_debit_minor: formatTenths(providerGrossTenths),
607
+ buyer_debit_minor: formatTenths(providerGrossTenths),
608
+ rounding_delta_minor: "0",
609
+ settlement_threshold_minor: "10000",
610
+ };
611
+ }
612
+
613
+ function sandboxProtocolFeeTenths(currency, pricingBand) {
614
+ if (pricingBand === "micro") return currency === "USD" ? 10 : 20;
615
+ return currency === "USD" ? 1 : 2;
616
+ }
617
+
618
+ function sandboxBuyerMeteredSummary(state, url) {
619
+ const events = filterSandboxMeteredUsage(state, url);
620
+ const batches = sandboxSettlementBatches(state, url);
621
+ return {
622
+ role: "buyer",
623
+ open_periods: sandboxOpenPeriods(events),
624
+ settlement_batches: batches,
625
+ past_due_blocks: [],
626
+ balance_sufficiency: { sufficient: true },
627
+ };
628
+ }
629
+
630
+ function sandboxProviderMeteredSummary(state, url) {
631
+ const events = filterSandboxMeteredUsage(state, url);
632
+ const totals = sandboxProviderTotals(events);
633
+ return {
634
+ role: "provider",
635
+ timezone: "UTC",
636
+ filters: Object.fromEntries(url.searchParams.entries()),
637
+ open_periods: sandboxOpenPeriods(events),
638
+ periods: sandboxSettlementBatches(state, url),
639
+ totals,
640
+ };
641
+ }
642
+
643
+ function filterSandboxMeteredUsage(state, url) {
644
+ const planType = url.searchParams.get("plan_type");
645
+ const tokenSymbol = url.searchParams.get("token_symbol");
646
+ const status = url.searchParams.get("status");
647
+ return state.meteredUsageEvents.filter((event) => {
648
+ if (planType && event.plan_type !== planType) return false;
649
+ if (tokenSymbol && event.token_symbol !== tokenSymbol) return false;
650
+ if (status && event.status !== status) return false;
651
+ return true;
652
+ });
653
+ }
654
+
655
+ function sandboxOpenPeriods(events) {
656
+ return Object.values(groupSandboxMeteredEvents(events)).map((group) => {
657
+ const grossTenths = sumTenths(group.events, "provider_gross_amount_minor");
658
+ const protocolFeeTenths = sumTenths(group.events, "protocol_fee_minor");
659
+ const receivableTenths = sumTenths(group.events, "provider_receivable_minor");
660
+ const buyerDebitTenths = sumTenths(group.events, "buyer_debit_minor");
661
+ const thresholdTenths = 10000 * 10;
662
+ const thresholdReached = grossTenths >= thresholdTenths;
663
+ return {
664
+ plan_type: group.plan_type,
665
+ settlement_cadence: group.plan_type === "micro" ? "weekly" : "monthly",
666
+ currency: group.currency,
667
+ token_symbol: group.token_symbol,
668
+ period_start: group.events[group.events.length - 1]?.created_at ?? null,
669
+ period_end: null,
670
+ close_at: null,
671
+ settlement_trigger: thresholdReached ? "amount_threshold" : null,
672
+ settlement_threshold_minor: "10000",
673
+ threshold_reached_at: thresholdReached ? group.events[0]?.created_at ?? null : null,
674
+ provider_gross_amount_minor: formatTenths(grossTenths),
675
+ provider_usage_amount_minor: formatTenths(grossTenths),
676
+ protocol_fee_minor: formatTenths(protocolFeeTenths),
677
+ provider_receivable_minor: formatTenths(receivableTenths),
678
+ buyer_debit_minor: formatTenths(buyerDebitTenths),
679
+ total_unsettled_exposure_minor: formatTenths(grossTenths),
680
+ };
681
+ });
682
+ }
683
+
684
+ function sandboxSettlementBatches(state, url) {
685
+ const events = filterSandboxMeteredUsage(state, url);
686
+ return sandboxOpenPeriods(events)
687
+ .filter((period) => period.settlement_trigger === "amount_threshold")
688
+ .map((period) => ({
689
+ settlement_batch_id: `batch_sandbox_${period.plan_type}_${period.currency}_${period.token_symbol}`,
690
+ status: "notice_pending",
691
+ notice_status: "pending",
692
+ not_before_attempt_at: new Date(Date.now() + 72 * 60 * 60 * 1000).toISOString(),
693
+ expected_scheduled_debit_at: new Date(Date.now() + 72 * 60 * 60 * 1000).toISOString(),
694
+ ...period,
695
+ }));
696
+ }
697
+
698
+ function sandboxProviderTotals(events) {
699
+ return {
700
+ settled_provider_receivable_minor: "0",
701
+ unsettled_provider_receivable_minor: formatTenths(sumTenths(events, "provider_receivable_minor")),
702
+ past_due_provider_receivable_minor: "0",
703
+ terminal_provider_receivable_minor: "0",
704
+ uncollectible_provider_receivable_minor: "0",
705
+ written_off_provider_receivable_minor: "0",
706
+ };
707
+ }
708
+
709
+ function groupSandboxMeteredEvents(events) {
710
+ const groups = {};
711
+ for (const event of events) {
712
+ const key = `${event.plan_type}:${event.currency}:${event.token_symbol}`;
713
+ groups[key] ||= {
714
+ plan_type: event.plan_type,
715
+ currency: event.currency,
716
+ token_symbol: event.token_symbol,
717
+ events: [],
718
+ };
719
+ groups[key].events.push(event);
720
+ }
721
+ return groups;
722
+ }
723
+
724
+ function sumTenths(items, field) {
725
+ return items.reduce((total, item) => total + parseTenths(item[field]), 0);
726
+ }
727
+
728
+ function parseTenths(value) {
729
+ return Math.round(Number(value || 0) * 10);
730
+ }
731
+
732
+ function formatTenths(value) {
733
+ if (value % 10 === 0) return String(value / 10);
734
+ return (value / 10).toFixed(1);
735
+ }
736
+
510
737
  function sandboxConfirmResponse(session, event) {
511
738
  return {
512
739
  status: "paid",
@@ -567,6 +794,9 @@ function sandboxCheckoutHtml(session) {
567
794
  const body = await response.json();
568
795
  document.getElementById("status").textContent = body.data?.status || "failed";
569
796
  document.getElementById("output").textContent = JSON.stringify(body, null, 2);
797
+ if (body.data?.redirect_url) {
798
+ window.setTimeout(() => window.location.assign(body.data.redirect_url), 400);
799
+ }
570
800
  });
571
801
  </script>
572
802
  </body>`;
@@ -781,6 +1011,10 @@ function activeLike(value) {
781
1011
  return /^(active|ready|current|ok|enabled|paid|complete|completed)$/i.test(String(value || ""));
782
1012
  }
783
1013
 
1014
+ function merchantStatusAllowed(value) {
1015
+ return /^(active|ready)$/i.test(String(value || ""));
1016
+ }
1017
+
784
1018
  function includesEventType(eventTypes, eventType) {
785
1019
  if (!Array.isArray(eventTypes) || eventTypes.length === 0) return true;
786
1020
  return eventTypes.map((item) => String(item)).includes(eventType);
@@ -812,6 +1046,7 @@ async function checkWebhookDeliveryProbe(checks, merchantClient, { merchant, sub
812
1046
  subscription_ids: [id],
813
1047
  data: {
814
1048
  mode: "readiness_probe",
1049
+ readiness_probe: true,
815
1050
  merchant,
816
1051
  direct_payment_requirement_id: `dpr_readiness_${Date.now()}`,
817
1052
  requirement_id: `dpr_readiness_${Date.now()}`,
package/dist/index.cjs CHANGED
@@ -85,7 +85,7 @@ var DIRECT_REQUEST_PAYMENT_RECEIPT_KIND = "sdrp_direct_payment";
85
85
  var DIRECT_REQUEST_PAYMENT_ALLOWANCE_RECEIPT_KIND = "sdrp_direct_payment_allowance";
86
86
  var DIRECT_REQUEST_PAYMENT_REFERENCE_TYPE = "sdrp_direct_payment_requirement";
87
87
  var DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
88
- var DIRECT_REQUEST_PAYMENT_SDK_VERSION = "0.4.23";
88
+ var DIRECT_REQUEST_PAYMENT_SDK_VERSION = "0.4.25";
89
89
  var DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS = "settled";
90
90
  var DIRECT_REQUEST_PAYMENT_METERED_ACCEPTED_STATUS = "pending_settlement";
91
91
  var DIRECT_REQUEST_PAYMENT_STANDARD_FINALITY = "per_payment_onchain";