@siglume/direct-request-payment 0.4.6 → 0.4.8

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.
@@ -16,11 +16,15 @@ order challenge; the buyer-facing Siglume payment flow pays it.
16
16
 
17
17
  This quickstart uses Standard-band example amounts. Micro Payment and Nano
18
18
  Payment are applied automatically by amount through the same Hosted Checkout or
19
- agent/API flow; you do not create a separate Micro/Nano object or pass a
20
- "force Standard" flag. Micro/Nano are settled on account-assigned weekly /
19
+ agent/API flow; you do not create a separate Micro/Nano object or manually
20
+ select the amount band. Micro/Nano are settled on account-assigned weekly /
21
21
  monthly slots after the final notice and close-plus-3-day window (see
22
22
  [Pricing](./pricing.md#settlement-schedule)), and provider revenue remains
23
- unsettled until the later on-chain settlement succeeds.
23
+ unsettled until the later on-chain settlement succeeds. Completing merchant
24
+ setup and the billing mandate means accepting this Micro/Nano delayed aggregated
25
+ settlement model for low-price items. If your product requires immediate
26
+ on-chain settlement, keep its price above the Micro/Nano thresholds instead of
27
+ offering JPY 500-and-under or USD 3-and-under amounts.
24
28
 
25
29
  ## Two Buyer Systems
26
30
 
@@ -388,7 +392,10 @@ signer and verifier only.
388
392
  Use the webhook as the durable signal, not just the browser return path.
389
393
 
390
394
  ```ts
391
- import { verifyDirectRequestPaymentWebhook } from "@siglume/direct-request-payment";
395
+ import {
396
+ classifyDirectPaymentConfirmation,
397
+ verifyDirectRequestPaymentWebhook,
398
+ } from "@siglume/direct-request-payment";
392
399
 
393
400
  const { event } = await verifyDirectRequestPaymentWebhook(
394
401
  process.env.SIGLUME_WEBHOOK_SECRET!,
@@ -400,57 +407,57 @@ if (event.type !== "direct_payment.confirmed") {
400
407
  return new Response(null, { status: 204 });
401
408
  }
402
409
 
403
- const data = event.data;
410
+ const confirmation = classifyDirectPaymentConfirmation(event);
404
411
 
405
- if (data.mode === "metered_settlement_batch") {
412
+ if (confirmation.kind === "metered_batch_settled") {
406
413
  // Aggregated Micro/Nano settlement events do not carry an order challenge.
407
- if (data.settlement_status === "settled") {
408
- await orders.reconcileMeteredSettlementOnce({
409
- settlement_batch_id: String(data.settlement_batch_id ?? ""),
410
- chain_receipt_id: String(data.chain_receipt_id ?? ""),
411
- usage_event_digest: String(data.usage_event_digest ?? ""),
412
- settled_at: String(data.settled_at ?? ""),
413
- });
414
- }
414
+ await orders.reconcileMeteredSettlementOnce({
415
+ settlement_batch_id: confirmation.settlement_batch_id,
416
+ chain_receipt_id: confirmation.chain_receipt_id,
417
+ usage_event_digest: confirmation.usage_event_digest,
418
+ settled_at: confirmation.settled_at ?? null,
419
+ });
415
420
  return new Response(null, { status: 204 });
416
421
  }
417
422
 
418
- if (
419
- data.pricing_band === "standard" &&
420
- data.finality === "per_payment_onchain" &&
421
- data.settlement_status === "settled"
422
- ) {
423
- const order = await orders.findByChallengeHash(String(data.challenge_hash ?? ""));
423
+ if (confirmation.kind === "standard_settled") {
424
+ const order = await orders.findByChallengeHash(confirmation.challenge_hash);
424
425
  if (!order) {
425
426
  await orders.flagForPaymentStateReview({
426
427
  reason: "unknown_challenge_hash",
427
- requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
428
+ requirement_id: confirmation.requirement_id,
428
429
  });
429
430
  return new Response(null, { status: 204 });
430
431
  }
431
432
  await orders.markPaidOnce(order.id, {
432
- siglume_requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
433
- chain_receipt_id: String(data.chain_receipt_id ?? ""),
433
+ siglume_requirement_id: confirmation.requirement_id,
434
+ chain_receipt_id: confirmation.chain_receipt_id,
434
435
  });
435
436
  return new Response(null, { status: 204 });
436
437
  }
437
438
 
438
- if (data.pricing_band === "micro" || data.pricing_band === "nano") {
439
- const order = await orders.findByChallengeHash(String(data.challenge_hash ?? ""));
440
- if (order) {
441
- await orders.markFulfilledButUnsettledOnce(order.id, {
442
- siglume_requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
443
- pricing_band: String(data.pricing_band),
439
+ if (confirmation.kind === "metered_usage_accepted") {
440
+ const order = await orders.findByChallengeHash(confirmation.challenge_hash);
441
+ if (!order) {
442
+ await orders.flagForPaymentStateReview({
443
+ reason: "unknown_metered_challenge_hash",
444
+ requirement_id: confirmation.requirement_id,
444
445
  });
446
+ return new Response(null, { status: 204 });
445
447
  }
448
+ await orders.markFulfilledButUnsettledOnce(order.id, {
449
+ siglume_requirement_id: confirmation.requirement_id,
450
+ pricing_band: confirmation.pricing_band,
451
+ });
446
452
  return new Response(null, { status: 204 });
447
453
  }
448
454
 
449
455
  // Missing or unknown machine fields: do not mark the order paid from the event
450
456
  // name alone. Fetch the requirement or route it to manual review.
451
457
  await orders.flagForPaymentStateReview({
452
- reason: "missing_settlement_machine_fields",
453
- requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
458
+ reason: confirmation.reason,
459
+ requirement_id: confirmation.requirement_id ?? "",
460
+ settlement_batch_id: confirmation.settlement_batch_id ?? null,
454
461
  });
455
462
  return new Response(null, { status: 204 });
456
463
  ```
@@ -460,7 +467,10 @@ Python:
460
467
  ```py
461
468
  import os
462
469
 
463
- from siglume_direct_request_payment import verify_direct_request_payment_webhook
470
+ from siglume_direct_request_payment import (
471
+ classify_direct_payment_confirmation,
472
+ verify_direct_request_payment_webhook,
473
+ )
464
474
 
465
475
  verified = verify_direct_request_payment_webhook(
466
476
  os.environ["SIGLUME_WEBHOOK_SECRET"],
@@ -471,53 +481,54 @@ verified = verify_direct_request_payment_webhook(
471
481
  if verified["event"]["type"] != "direct_payment.confirmed":
472
482
  return "", 204
473
483
 
474
- data = verified["event"]["data"]
484
+ confirmation = classify_direct_payment_confirmation(verified["event"])
475
485
 
476
- if data.get("mode") == "metered_settlement_batch":
486
+ if confirmation["kind"] == "metered_batch_settled":
477
487
  # Aggregated Micro/Nano settlement events do not carry an order challenge.
478
- if data.get("settlement_status") == "settled":
479
- orders.reconcile_metered_settlement_once(
480
- settlement_batch_id=str(data.get("settlement_batch_id") or ""),
481
- chain_receipt_id=str(data.get("chain_receipt_id") or ""),
482
- usage_event_digest=str(data.get("usage_event_digest") or ""),
483
- settled_at=str(data.get("settled_at") or ""),
484
- )
488
+ orders.reconcile_metered_settlement_once(
489
+ settlement_batch_id=confirmation["settlement_batch_id"],
490
+ chain_receipt_id=confirmation["chain_receipt_id"],
491
+ usage_event_digest=confirmation["usage_event_digest"],
492
+ settled_at=confirmation.get("settled_at"),
493
+ )
485
494
  return "", 204
486
495
 
487
- if (
488
- data.get("pricing_band") == "standard"
489
- and data.get("finality") == "per_payment_onchain"
490
- and data.get("settlement_status") == "settled"
491
- ):
492
- order = orders.find_by_challenge_hash(str(data.get("challenge_hash") or ""))
496
+ if confirmation["kind"] == "standard_settled":
497
+ order = orders.find_by_challenge_hash(confirmation["challenge_hash"])
493
498
  if not order:
494
499
  orders.flag_for_payment_state_review(
495
500
  reason="unknown_challenge_hash",
496
- requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
501
+ requirement_id=confirmation["requirement_id"],
497
502
  )
498
503
  return "", 204
499
504
  orders.mark_paid_once(
500
505
  order["id"],
501
- siglume_requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
502
- chain_receipt_id=str(data.get("chain_receipt_id") or ""),
506
+ siglume_requirement_id=confirmation["requirement_id"],
507
+ chain_receipt_id=confirmation["chain_receipt_id"],
503
508
  )
504
509
  return "", 204
505
510
 
506
- if data.get("pricing_band") in ("micro", "nano"):
507
- order = orders.find_by_challenge_hash(str(data.get("challenge_hash") or ""))
508
- if order:
509
- orders.mark_fulfilled_but_unsettled_once(
510
- order["id"],
511
- siglume_requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
512
- pricing_band=str(data.get("pricing_band") or ""),
511
+ if confirmation["kind"] == "metered_usage_accepted":
512
+ order = orders.find_by_challenge_hash(confirmation["challenge_hash"])
513
+ if not order:
514
+ orders.flag_for_payment_state_review(
515
+ reason="unknown_metered_challenge_hash",
516
+ requirement_id=confirmation["requirement_id"],
513
517
  )
518
+ return "", 204
519
+ orders.mark_fulfilled_but_unsettled_once(
520
+ order["id"],
521
+ siglume_requirement_id=confirmation["requirement_id"],
522
+ pricing_band=confirmation["pricing_band"],
523
+ )
514
524
  return "", 204
515
525
 
516
526
  # Missing or unknown machine fields: do not mark the order paid from the event
517
527
  # name alone. Fetch the requirement or route it to manual review.
518
528
  orders.flag_for_payment_state_review(
519
- reason="missing_settlement_machine_fields",
520
- requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
529
+ reason=confirmation["reason"],
530
+ requirement_id=confirmation.get("requirement_id") or "",
531
+ settlement_batch_id=confirmation.get("settlement_batch_id"),
521
532
  )
522
533
  return "", 204
523
534
  ```
@@ -525,11 +536,12 @@ return "", 204
525
536
  ## Reconcile Micro / Nano Statements
526
537
 
527
538
  Standard Payment can be marked paid from the verified `direct_payment.confirmed`
528
- webhook only when `pricing_band === "standard"`,
529
- `finality === "per_payment_onchain"`, and `settlement_status === "settled"`.
530
- Micro Payment and Nano Payment are different: they are automatic amount bands
531
- and are settled later in aggregated on-chain batches. Use the statement APIs to
532
- answer:
539
+ webhook only when `classifyDirectPaymentConfirmation(event)` returns
540
+ `standard_settled`, which requires Standard pricing, per-payment on-chain
541
+ finality, settled status, a challenge hash, a requirement id, and a chain
542
+ receipt id. Micro Payment and Nano Payment are different: they are automatic
543
+ amount bands and are settled later in aggregated on-chain batches. Use the
544
+ statement APIs to answer:
533
545
 
534
546
  - how much Micro / Nano usage is open this week or month,
535
547
  - when the buyer's assigned period closes,
package/docs/pricing.md CHANGED
@@ -7,7 +7,9 @@ fee data returned at runtime.
7
7
 
8
8
  Pricing has one structure: a merchant selects the Standard Payment plan during
9
9
  setup, then Siglume applies the fee for each payment by amount. Micro Payment
10
- and Nano Payment are automatic amount bands, not separate choices.
10
+ and Nano Payment are automatic amount bands, not separate choices. Merchant
11
+ setup and the billing mandate terms assume the merchant accepts Micro / Nano
12
+ delayed aggregated settlement whenever they offer amounts in those bands.
11
13
 
12
14
  ## Settlement Currencies
13
15
 
@@ -36,15 +38,17 @@ aggregated and settled in account-assigned weekly / monthly slots - see
36
38
  pre-debit notice window elapses, when revenue becomes settled, and how rejected
37
39
  requests behave.
38
40
 
39
- The current public API chooses the band from `amount_minor`; it does not expose
40
- `settlement_mode: "immediate"` or `require_immediate_finality: true`. If a
41
- merchant needs immediate on-chain finality, the payment amount must be in the
42
- Standard band or the merchant must have a separately agreed platform contract.
43
- For public one-time Direct Payment / Hosted Checkout, `amount_minor` is a
44
- positive integer in minor currency units. That means the smallest public
45
- one-time checkout amount is JPY 1 or USD 0.01. Nano Payment on this public path
46
- therefore means JPY 1-49 or USD 0.01-0.30. Sub-minor Nano protocol fees are
47
- settlement-accounting amounts, not externally submitted one-time item prices.
41
+ The current public API chooses the band from `amount_minor`; JPY 500-and-under /
42
+ USD 3-and-under payments are routed to Micro / Nano delayed aggregated
43
+ settlement. If a merchant needs immediate on-chain finality, the payment amount
44
+ must be in the Standard band. In practice, do not offer JPY 500-and-under or
45
+ USD 3-and-under items for a product that cannot accept Micro / Nano delayed
46
+ aggregated settlement. For public one-time Direct Payment / Hosted Checkout,
47
+ `amount_minor` is a positive integer in minor currency units. That means the
48
+ smallest public one-time checkout amount is JPY 1 or USD 0.01. Nano Payment on
49
+ this public path therefore means JPY 1-49 or USD 0.01-0.30. Sub-minor Nano
50
+ protocol fees are settlement-accounting amounts, not externally submitted
51
+ one-time item prices.
48
52
 
49
53
  For the operational statement APIs, CSV export, buyer past-due blocks, and the
50
54
  field-by-field meaning of `scheduled_debit_at`, `not_before_attempt_at`,
@@ -1,5 +1,6 @@
1
1
  import express from "express";
2
2
  import {
3
+ classifyDirectPaymentConfirmation,
3
4
  DirectRequestPaymentMerchantClient,
4
5
  verifyDirectRequestPaymentWebhook,
5
6
  } from "@siglume/direct-request-payment";
@@ -22,45 +23,54 @@ app.use((req, res, next) => {
22
23
 
23
24
  const orders = new Map<string, any>();
24
25
 
25
- async function handleDirectPaymentConfirmed(data: Record<string, any>): Promise<void> {
26
- if (data.mode === "metered_settlement_batch") {
26
+ async function flagForPaymentStateReview(payload: Record<string, any>): Promise<void> {
27
+ console.warn("payment state review required", payload);
28
+ }
29
+
30
+ async function handleDirectPaymentConfirmed(event: any): Promise<void> {
31
+ const classification = classifyDirectPaymentConfirmation(event);
32
+
33
+ if (classification.kind === "metered_batch_settled") {
27
34
  // Aggregated Micro/Nano settlement events do not carry an order challenge.
28
35
  // Reconcile them against statement / settlement batch data instead.
29
- if (data.settlement_status === "settled") {
30
- console.log("settled metered batch", data.settlement_batch_id || data.usage_event_digest);
31
- }
36
+ console.log("settled metered batch", classification.settlement_batch_id, classification.chain_receipt_id);
32
37
  return;
33
38
  }
34
39
 
35
- if (
36
- data.pricing_band === "standard" &&
37
- data.finality === "per_payment_onchain" &&
38
- data.settlement_status === "settled"
39
- ) {
40
- const challengeHash = String(data.challenge_hash || "");
41
- const order = [...orders.values()].find((item) => item.siglume_challenge_hash === challengeHash);
40
+ if (classification.kind === "standard_settled") {
41
+ const order = [...orders.values()].find((item) => item.siglume_challenge_hash === classification.challenge_hash);
42
42
  if (order) {
43
43
  order.siglume_payment_status = "paid";
44
- order.siglume_requirement_id = data.requirement_id || data.direct_payment_requirement_id;
45
- order.siglume_chain_receipt_id = data.chain_receipt_id || null;
44
+ order.siglume_requirement_id = classification.requirement_id;
45
+ order.siglume_chain_receipt_id = classification.chain_receipt_id;
46
+ } else {
47
+ await flagForPaymentStateReview({
48
+ reason: "unknown_challenge_hash",
49
+ requirement_id: classification.requirement_id,
50
+ });
46
51
  }
47
52
  return;
48
53
  }
49
54
 
50
- if (data.pricing_band === "micro" || data.pricing_band === "nano") {
51
- const challengeHash = String(data.challenge_hash || "");
52
- const order = [...orders.values()].find((item) => item.siglume_challenge_hash === challengeHash);
55
+ if (classification.kind === "metered_usage_accepted") {
56
+ const order = [...orders.values()].find((item) => item.siglume_challenge_hash === classification.challenge_hash);
53
57
  if (order) {
54
58
  order.siglume_payment_status = "fulfilled_unsettled";
55
- order.siglume_requirement_id = data.requirement_id || data.direct_payment_requirement_id;
59
+ order.siglume_requirement_id = classification.requirement_id;
60
+ } else {
61
+ await flagForPaymentStateReview({
62
+ reason: "unknown_metered_challenge_hash",
63
+ requirement_id: classification.requirement_id,
64
+ });
56
65
  }
57
66
  return;
58
67
  }
59
68
 
60
69
  // Unknown or older payload shape: do not mark paid from the event name alone.
61
- console.warn("direct_payment.confirmed missing settlement machine fields", {
62
- id: data.id,
63
- requirement_id: data.requirement_id || data.direct_payment_requirement_id,
70
+ await flagForPaymentStateReview({
71
+ reason: classification.reason,
72
+ requirement_id: classification.requirement_id,
73
+ settlement_batch_id: classification.settlement_batch_id,
64
74
  });
65
75
  }
66
76
 
@@ -111,7 +121,7 @@ app.post("/siglume/webhook", express.raw({ type: "application/json" }), asyncRou
111
121
  );
112
122
 
113
123
  if (event.type === "direct_payment.confirmed") {
114
- await handleDirectPaymentConfirmed(event.data);
124
+ await handleDirectPaymentConfirmed(event);
115
125
  }
116
126
 
117
127
  res.status(204).send();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siglume/direct-request-payment",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "SDK for the Siglume Direct Request Payment SDRP payment protocol",
5
5
  "keywords": [
6
6
  "siglume",