@siglume/direct-request-payment 0.4.7 → 0.4.9

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.
@@ -392,7 +392,10 @@ signer and verifier only.
392
392
  Use the webhook as the durable signal, not just the browser return path.
393
393
 
394
394
  ```ts
395
- import { verifyDirectRequestPaymentWebhook } from "@siglume/direct-request-payment";
395
+ import {
396
+ classifyDirectPaymentConfirmation,
397
+ verifyDirectRequestPaymentWebhook,
398
+ } from "@siglume/direct-request-payment";
396
399
 
397
400
  const { event } = await verifyDirectRequestPaymentWebhook(
398
401
  process.env.SIGLUME_WEBHOOK_SECRET!,
@@ -404,57 +407,57 @@ if (event.type !== "direct_payment.confirmed") {
404
407
  return new Response(null, { status: 204 });
405
408
  }
406
409
 
407
- const data = event.data;
410
+ const confirmation = classifyDirectPaymentConfirmation(event);
408
411
 
409
- if (data.mode === "metered_settlement_batch") {
412
+ if (confirmation.kind === "metered_batch_settled") {
410
413
  // Aggregated Micro/Nano settlement events do not carry an order challenge.
411
- if (data.settlement_status === "settled") {
412
- await orders.reconcileMeteredSettlementOnce({
413
- settlement_batch_id: String(data.settlement_batch_id ?? ""),
414
- chain_receipt_id: String(data.chain_receipt_id ?? ""),
415
- usage_event_digest: String(data.usage_event_digest ?? ""),
416
- settled_at: String(data.settled_at ?? ""),
417
- });
418
- }
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
+ });
419
420
  return new Response(null, { status: 204 });
420
421
  }
421
422
 
422
- if (
423
- data.pricing_band === "standard" &&
424
- data.finality === "per_payment_onchain" &&
425
- data.settlement_status === "settled"
426
- ) {
427
- const order = await orders.findByChallengeHash(String(data.challenge_hash ?? ""));
423
+ if (confirmation.kind === "standard_settled") {
424
+ const order = await orders.findByChallengeHash(confirmation.challenge_hash);
428
425
  if (!order) {
429
426
  await orders.flagForPaymentStateReview({
430
427
  reason: "unknown_challenge_hash",
431
- requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
428
+ requirement_id: confirmation.requirement_id,
432
429
  });
433
430
  return new Response(null, { status: 204 });
434
431
  }
435
432
  await orders.markPaidOnce(order.id, {
436
- siglume_requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
437
- chain_receipt_id: String(data.chain_receipt_id ?? ""),
433
+ siglume_requirement_id: confirmation.requirement_id,
434
+ chain_receipt_id: confirmation.chain_receipt_id,
438
435
  });
439
436
  return new Response(null, { status: 204 });
440
437
  }
441
438
 
442
- if (data.pricing_band === "micro" || data.pricing_band === "nano") {
443
- const order = await orders.findByChallengeHash(String(data.challenge_hash ?? ""));
444
- if (order) {
445
- await orders.markFulfilledButUnsettledOnce(order.id, {
446
- siglume_requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
447
- 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,
448
445
  });
446
+ return new Response(null, { status: 204 });
449
447
  }
448
+ await orders.markFulfilledButUnsettledOnce(order.id, {
449
+ siglume_requirement_id: confirmation.requirement_id,
450
+ pricing_band: confirmation.pricing_band,
451
+ });
450
452
  return new Response(null, { status: 204 });
451
453
  }
452
454
 
453
455
  // Missing or unknown machine fields: do not mark the order paid from the event
454
456
  // name alone. Fetch the requirement or route it to manual review.
455
457
  await orders.flagForPaymentStateReview({
456
- reason: "missing_settlement_machine_fields",
457
- 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,
458
461
  });
459
462
  return new Response(null, { status: 204 });
460
463
  ```
@@ -464,7 +467,10 @@ Python:
464
467
  ```py
465
468
  import os
466
469
 
467
- 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
+ )
468
474
 
469
475
  verified = verify_direct_request_payment_webhook(
470
476
  os.environ["SIGLUME_WEBHOOK_SECRET"],
@@ -475,53 +481,54 @@ verified = verify_direct_request_payment_webhook(
475
481
  if verified["event"]["type"] != "direct_payment.confirmed":
476
482
  return "", 204
477
483
 
478
- data = verified["event"]["data"]
484
+ confirmation = classify_direct_payment_confirmation(verified["event"])
479
485
 
480
- if data.get("mode") == "metered_settlement_batch":
486
+ if confirmation["kind"] == "metered_batch_settled":
481
487
  # Aggregated Micro/Nano settlement events do not carry an order challenge.
482
- if data.get("settlement_status") == "settled":
483
- orders.reconcile_metered_settlement_once(
484
- settlement_batch_id=str(data.get("settlement_batch_id") or ""),
485
- chain_receipt_id=str(data.get("chain_receipt_id") or ""),
486
- usage_event_digest=str(data.get("usage_event_digest") or ""),
487
- settled_at=str(data.get("settled_at") or ""),
488
- )
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
+ )
489
494
  return "", 204
490
495
 
491
- if (
492
- data.get("pricing_band") == "standard"
493
- and data.get("finality") == "per_payment_onchain"
494
- and data.get("settlement_status") == "settled"
495
- ):
496
- 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"])
497
498
  if not order:
498
499
  orders.flag_for_payment_state_review(
499
500
  reason="unknown_challenge_hash",
500
- requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
501
+ requirement_id=confirmation["requirement_id"],
501
502
  )
502
503
  return "", 204
503
504
  orders.mark_paid_once(
504
505
  order["id"],
505
- siglume_requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
506
- 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"],
507
508
  )
508
509
  return "", 204
509
510
 
510
- if data.get("pricing_band") in ("micro", "nano"):
511
- order = orders.find_by_challenge_hash(str(data.get("challenge_hash") or ""))
512
- if order:
513
- orders.mark_fulfilled_but_unsettled_once(
514
- order["id"],
515
- siglume_requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
516
- 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"],
517
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
+ )
518
524
  return "", 204
519
525
 
520
526
  # Missing or unknown machine fields: do not mark the order paid from the event
521
527
  # name alone. Fetch the requirement or route it to manual review.
522
528
  orders.flag_for_payment_state_review(
523
- reason="missing_settlement_machine_fields",
524
- 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"),
525
532
  )
526
533
  return "", 204
527
534
  ```
@@ -529,11 +536,12 @@ return "", 204
529
536
  ## Reconcile Micro / Nano Statements
530
537
 
531
538
  Standard Payment can be marked paid from the verified `direct_payment.confirmed`
532
- webhook only when `pricing_band === "standard"`,
533
- `finality === "per_payment_onchain"`, and `settlement_status === "settled"`.
534
- Micro Payment and Nano Payment are different: they are automatic amount bands
535
- and are settled later in aggregated on-chain batches. Use the statement APIs to
536
- 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:
537
545
 
538
546
  - how much Micro / Nano usage is open this week or month,
539
547
  - when the buyer's assigned period closes,
@@ -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.7",
3
+ "version": "0.4.9",
4
4
  "description": "SDK for the Siglume Direct Request Payment SDRP payment protocol",
5
5
  "keywords": [
6
6
  "siglume",