@wopr-network/platform-core 1.14.8 → 1.16.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.
Files changed (154) hide show
  1. package/dist/account/deletion-executor-repository.d.ts +2 -2
  2. package/dist/account/deletion-executor-repository.js +5 -5
  3. package/dist/{monetization/payram → billing/crypto}/cents-credits-boundary.test.js +14 -17
  4. package/dist/billing/crypto/charge-store.d.ts +68 -0
  5. package/dist/billing/crypto/charge-store.js +109 -0
  6. package/dist/billing/crypto/charge-store.test.js +120 -0
  7. package/dist/billing/crypto/checkout.d.ts +18 -0
  8. package/dist/billing/crypto/checkout.js +35 -0
  9. package/dist/billing/crypto/checkout.test.js +71 -0
  10. package/dist/billing/crypto/client.d.ts +39 -0
  11. package/dist/billing/crypto/client.js +72 -0
  12. package/dist/billing/crypto/client.test.js +100 -0
  13. package/dist/billing/crypto/evm/__tests__/address-gen.test.js +54 -0
  14. package/dist/billing/crypto/evm/__tests__/checkout.test.js +54 -0
  15. package/dist/billing/crypto/evm/__tests__/config.test.js +52 -0
  16. package/dist/billing/crypto/evm/__tests__/settler.test.js +196 -0
  17. package/dist/billing/crypto/evm/__tests__/watcher.test.js +109 -0
  18. package/dist/billing/crypto/evm/address-gen.d.ts +8 -0
  19. package/dist/billing/crypto/evm/address-gen.js +29 -0
  20. package/dist/billing/crypto/evm/checkout.d.ts +26 -0
  21. package/dist/billing/crypto/evm/checkout.js +57 -0
  22. package/dist/billing/crypto/evm/config.d.ts +13 -0
  23. package/dist/billing/crypto/evm/config.js +46 -0
  24. package/dist/billing/crypto/evm/index.d.ts +9 -0
  25. package/dist/billing/crypto/evm/index.js +5 -0
  26. package/dist/billing/crypto/evm/settler.d.ts +23 -0
  27. package/dist/billing/crypto/evm/settler.js +60 -0
  28. package/dist/billing/crypto/evm/types.d.ts +40 -0
  29. package/dist/billing/crypto/evm/types.js +1 -0
  30. package/dist/billing/crypto/evm/watcher.d.ts +31 -0
  31. package/dist/billing/crypto/evm/watcher.js +91 -0
  32. package/dist/billing/crypto/index.d.ts +10 -0
  33. package/dist/billing/crypto/index.js +6 -0
  34. package/dist/billing/crypto/types.d.ts +61 -0
  35. package/dist/billing/crypto/types.js +24 -0
  36. package/dist/billing/crypto/webhook.d.ts +34 -0
  37. package/dist/billing/crypto/webhook.js +107 -0
  38. package/dist/billing/crypto/webhook.test.d.ts +1 -0
  39. package/dist/billing/crypto/webhook.test.js +266 -0
  40. package/dist/billing/index.d.ts +1 -1
  41. package/dist/billing/index.js +2 -2
  42. package/dist/billing/payment-processor.d.ts +3 -3
  43. package/dist/credits/credit-ledger.d.ts +3 -3
  44. package/dist/credits/credit-ledger.js +3 -3
  45. package/dist/db/schema/credits.js +1 -1
  46. package/dist/db/schema/{payram.d.ts → crypto.d.ts} +85 -13
  47. package/dist/db/schema/crypto.js +32 -0
  48. package/dist/db/schema/index.d.ts +1 -1
  49. package/dist/db/schema/index.js +1 -1
  50. package/dist/monetization/crypto/__tests__/webhook.test.d.ts +1 -0
  51. package/dist/monetization/crypto/__tests__/webhook.test.js +249 -0
  52. package/dist/monetization/crypto/index.d.ts +4 -0
  53. package/dist/monetization/crypto/index.js +2 -0
  54. package/dist/monetization/crypto/webhook.d.ts +24 -0
  55. package/dist/monetization/crypto/webhook.js +88 -0
  56. package/dist/monetization/index.d.ts +3 -3
  57. package/dist/monetization/index.js +1 -1
  58. package/dist/monetization/repository-types.d.ts +1 -1
  59. package/dist/observability/pagerduty.test.js +1 -0
  60. package/docs/superpowers/plans/2026-03-14-stablecoin-phase1.md +1413 -0
  61. package/drizzle/migrations/0004_crypto_charges.sql +25 -0
  62. package/drizzle/migrations/0005_stablecoin_columns.sql +7 -0
  63. package/drizzle/migrations/meta/_journal.json +14 -0
  64. package/package.json +4 -3
  65. package/src/account/deletion-executor-repository.ts +6 -6
  66. package/src/billing/{payram → crypto}/cents-credits-boundary.test.ts +14 -17
  67. package/src/billing/crypto/charge-store.test.ts +142 -0
  68. package/src/billing/crypto/charge-store.ts +166 -0
  69. package/src/billing/crypto/checkout.test.ts +93 -0
  70. package/src/billing/crypto/checkout.ts +48 -0
  71. package/src/billing/crypto/client.test.ts +132 -0
  72. package/src/billing/crypto/client.ts +86 -0
  73. package/src/billing/crypto/evm/__tests__/address-gen.test.ts +63 -0
  74. package/src/billing/crypto/evm/__tests__/checkout.test.ts +83 -0
  75. package/src/billing/crypto/evm/__tests__/config.test.ts +63 -0
  76. package/src/billing/crypto/evm/__tests__/settler.test.ts +218 -0
  77. package/src/billing/crypto/evm/__tests__/watcher.test.ts +128 -0
  78. package/src/billing/crypto/evm/address-gen.ts +29 -0
  79. package/src/billing/crypto/evm/checkout.ts +82 -0
  80. package/src/billing/crypto/evm/config.ts +50 -0
  81. package/src/billing/crypto/evm/index.ts +16 -0
  82. package/src/billing/crypto/evm/settler.ts +79 -0
  83. package/src/billing/crypto/evm/types.ts +45 -0
  84. package/src/billing/crypto/evm/watcher.ts +126 -0
  85. package/src/billing/crypto/index.ts +16 -0
  86. package/src/billing/crypto/types.ts +83 -0
  87. package/src/billing/crypto/webhook.test.ts +340 -0
  88. package/src/billing/crypto/webhook.ts +136 -0
  89. package/src/billing/index.ts +2 -2
  90. package/src/billing/payment-processor.ts +3 -3
  91. package/src/credits/credit-ledger.ts +3 -3
  92. package/src/db/schema/credits.ts +1 -1
  93. package/src/db/schema/crypto.ts +37 -0
  94. package/src/db/schema/index.ts +1 -1
  95. package/src/monetization/crypto/__tests__/webhook.test.ts +327 -0
  96. package/src/monetization/crypto/index.ts +23 -0
  97. package/src/monetization/crypto/webhook.ts +115 -0
  98. package/src/monetization/index.ts +23 -21
  99. package/src/monetization/repository-types.ts +2 -2
  100. package/src/observability/pagerduty.test.ts +1 -0
  101. package/dist/billing/payram/cents-credits-boundary.test.js +0 -75
  102. package/dist/billing/payram/charge-store.d.ts +0 -41
  103. package/dist/billing/payram/charge-store.js +0 -72
  104. package/dist/billing/payram/charge-store.test.js +0 -64
  105. package/dist/billing/payram/checkout.d.ts +0 -15
  106. package/dist/billing/payram/checkout.js +0 -24
  107. package/dist/billing/payram/checkout.test.js +0 -74
  108. package/dist/billing/payram/client.d.ts +0 -7
  109. package/dist/billing/payram/client.js +0 -15
  110. package/dist/billing/payram/client.test.js +0 -52
  111. package/dist/billing/payram/index.d.ts +0 -8
  112. package/dist/billing/payram/index.js +0 -4
  113. package/dist/billing/payram/types.d.ts +0 -40
  114. package/dist/billing/payram/webhook.d.ts +0 -19
  115. package/dist/billing/payram/webhook.js +0 -71
  116. package/dist/billing/payram/webhook.test.d.ts +0 -7
  117. package/dist/billing/payram/webhook.test.js +0 -249
  118. package/dist/db/schema/payram.js +0 -21
  119. package/dist/monetization/payram/charge-store.test.js +0 -64
  120. package/dist/monetization/payram/checkout.test.js +0 -73
  121. package/dist/monetization/payram/client.test.js +0 -52
  122. package/dist/monetization/payram/index.d.ts +0 -4
  123. package/dist/monetization/payram/index.js +0 -2
  124. package/dist/monetization/payram/webhook.d.ts +0 -17
  125. package/dist/monetization/payram/webhook.js +0 -71
  126. package/dist/monetization/payram/webhook.test.d.ts +0 -7
  127. package/dist/monetization/payram/webhook.test.js +0 -247
  128. package/src/billing/payram/charge-store.test.ts +0 -84
  129. package/src/billing/payram/charge-store.ts +0 -109
  130. package/src/billing/payram/checkout.test.ts +0 -99
  131. package/src/billing/payram/checkout.ts +0 -40
  132. package/src/billing/payram/client.test.ts +0 -62
  133. package/src/billing/payram/client.ts +0 -21
  134. package/src/billing/payram/index.ts +0 -14
  135. package/src/billing/payram/types.ts +0 -44
  136. package/src/billing/payram/webhook.test.ts +0 -320
  137. package/src/billing/payram/webhook.ts +0 -94
  138. package/src/db/schema/payram.ts +0 -26
  139. package/src/monetization/payram/cents-credits-boundary.test.ts +0 -84
  140. package/src/monetization/payram/charge-store.test.ts +0 -84
  141. package/src/monetization/payram/checkout.test.ts +0 -98
  142. package/src/monetization/payram/client.test.ts +0 -62
  143. package/src/monetization/payram/index.ts +0 -20
  144. package/src/monetization/payram/webhook.test.ts +0 -327
  145. package/src/monetization/payram/webhook.ts +0 -97
  146. /package/dist/billing/{payram → crypto}/cents-credits-boundary.test.d.ts +0 -0
  147. /package/dist/billing/{payram → crypto}/charge-store.test.d.ts +0 -0
  148. /package/dist/billing/{payram → crypto}/checkout.test.d.ts +0 -0
  149. /package/dist/billing/{payram → crypto}/client.test.d.ts +0 -0
  150. /package/dist/billing/{payram/types.js → crypto/evm/__tests__/address-gen.test.d.ts} +0 -0
  151. /package/dist/{monetization/payram → billing/crypto/evm/__tests__}/checkout.test.d.ts +0 -0
  152. /package/dist/{monetization/payram/cents-credits-boundary.test.d.ts → billing/crypto/evm/__tests__/config.test.d.ts} +0 -0
  153. /package/dist/{monetization/payram/charge-store.test.d.ts → billing/crypto/evm/__tests__/settler.test.d.ts} +0 -0
  154. /package/dist/{monetization/payram/client.test.d.ts → billing/crypto/evm/__tests__/watcher.test.d.ts} +0 -0
@@ -1,98 +0,0 @@
1
- /**
2
- * Unit tests for createPayRamCheckout (WOP-407).
3
- */
4
- import type { PGlite } from "@electric-sql/pglite";
5
- import { createPayRamCheckout, MIN_PAYMENT_USD, PayRamChargeRepository } from "@wopr-network/platform-core/billing";
6
- import type { Payram } from "payram";
7
- import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
8
- import type { DrizzleDb } from "../../db/index.js";
9
- import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../../test/db.js";
10
-
11
- function createMockPayram(overrides: { initiatePayment?: ReturnType<typeof vi.fn> } = {}): Payram {
12
- return {
13
- payments: {
14
- initiatePayment:
15
- overrides.initiatePayment ??
16
- vi.fn().mockResolvedValue({
17
- reference_id: "ref-mock-001",
18
- url: "https://payram.example.com/pay/ref-mock-001",
19
- }),
20
- },
21
- } as unknown as Payram;
22
- }
23
-
24
- describe("createPayRamCheckout", () => {
25
- let pool: PGlite;
26
- let db: DrizzleDb;
27
- let chargeStore: PayRamChargeRepository;
28
- let payram: Payram;
29
-
30
- beforeAll(async () => {
31
- ({ db, pool } = await createTestDb());
32
- await beginTestTransaction(pool);
33
- });
34
-
35
- afterAll(async () => {
36
- await endTestTransaction(pool);
37
- await pool.close();
38
- });
39
-
40
- beforeEach(async () => {
41
- await rollbackTestTransaction(pool);
42
- chargeStore = new PayRamChargeRepository(db);
43
- payram = createMockPayram();
44
- });
45
-
46
- it("rejects amounts below $10 minimum", async () => {
47
- await expect(createPayRamCheckout(payram, chargeStore, { tenant: "t-1", amountUsd: 5 })).rejects.toThrow(
48
- `Minimum payment amount is $${MIN_PAYMENT_USD}`,
49
- );
50
- });
51
-
52
- it("rejects amounts of exactly $0", async () => {
53
- await expect(createPayRamCheckout(payram, chargeStore, { tenant: "t-1", amountUsd: 0 })).rejects.toThrow();
54
- });
55
-
56
- it("calls payram.payments.initiatePayment with correct params", async () => {
57
- const initiatePayment = vi.fn().mockResolvedValue({
58
- reference_id: "ref-abc",
59
- url: "https://payram.example.com/pay/ref-abc",
60
- });
61
- const mockPayram = createMockPayram({ initiatePayment });
62
-
63
- await createPayRamCheckout(mockPayram, chargeStore, { tenant: "t-test", amountUsd: 25 });
64
-
65
- expect(initiatePayment).toHaveBeenCalledWith({
66
- customerEmail: "t-test@wopr.bot",
67
- customerId: "t-test",
68
- amountInUSD: 25,
69
- });
70
- });
71
-
72
- it("stores the charge with correct amountUsdCents (converts from USD)", async () => {
73
- const initiatePayment = vi.fn().mockResolvedValue({
74
- reference_id: "ref-store-test",
75
- url: "https://payram.example.com/pay/ref-store-test",
76
- });
77
- const mockPayram = createMockPayram({ initiatePayment });
78
-
79
- await createPayRamCheckout(mockPayram, chargeStore, { tenant: "t-2", amountUsd: 25 });
80
-
81
- const charge = await chargeStore.getByReferenceId("ref-store-test");
82
- expect(charge).not.toBeNull();
83
- expect(charge?.tenantId).toBe("t-2");
84
- expect(charge?.amountUsdCents).toBe(2500); // $25.00 = 2500 cents
85
- expect(charge?.status).toBe("OPEN");
86
- });
87
-
88
- it("returns referenceId and url from PayRam response", async () => {
89
- const result = await createPayRamCheckout(payram, chargeStore, { tenant: "t-3", amountUsd: 10 });
90
-
91
- expect(result.referenceId).toBe("ref-mock-001");
92
- expect(result.url).toBe("https://payram.example.com/pay/ref-mock-001");
93
- });
94
-
95
- it("accepts exactly $10 (minimum boundary)", async () => {
96
- await expect(createPayRamCheckout(payram, chargeStore, { tenant: "t-4", amountUsd: 10 })).resolves.not.toBeNull();
97
- });
98
- });
@@ -1,62 +0,0 @@
1
- import { createPayRamClient, loadPayRamConfig } from "@wopr-network/platform-core/billing";
2
- import { Payram } from "payram";
3
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
-
5
- describe("createPayRamClient", () => {
6
- it("returns a Payram instance", () => {
7
- const client = createPayRamClient({ apiKey: "test-key", baseUrl: "https://api.payram.test" });
8
- expect(client).toBeInstanceOf(Payram);
9
- });
10
-
11
- it("passes config through to the Payram constructor", () => {
12
- const client = createPayRamClient({ apiKey: "pk_live_123", baseUrl: "https://api.payram.io" });
13
- expect(client).toBeInstanceOf(Payram);
14
- });
15
- });
16
-
17
- describe("loadPayRamConfig", () => {
18
- beforeEach(() => {
19
- delete process.env.PAYRAM_API_KEY;
20
- delete process.env.PAYRAM_BASE_URL;
21
- });
22
-
23
- afterEach(() => {
24
- vi.unstubAllEnvs();
25
- });
26
-
27
- it("returns null when PAYRAM_API_KEY is missing", () => {
28
- vi.stubEnv("PAYRAM_BASE_URL", "https://api.payram.test");
29
- expect(loadPayRamConfig()).toBeNull();
30
- });
31
-
32
- it("returns null when PAYRAM_BASE_URL is missing", () => {
33
- vi.stubEnv("PAYRAM_API_KEY", "pk_test_123");
34
- expect(loadPayRamConfig()).toBeNull();
35
- });
36
-
37
- it("returns null when PAYRAM_API_KEY is an empty string", () => {
38
- vi.stubEnv("PAYRAM_API_KEY", "");
39
- vi.stubEnv("PAYRAM_BASE_URL", "https://api.payram.test");
40
- expect(loadPayRamConfig()).toBeNull();
41
- });
42
-
43
- it("returns null when PAYRAM_BASE_URL is an empty string", () => {
44
- vi.stubEnv("PAYRAM_API_KEY", "pk_test_123");
45
- vi.stubEnv("PAYRAM_BASE_URL", "");
46
- expect(loadPayRamConfig()).toBeNull();
47
- });
48
-
49
- it("returns config when both env vars are set", () => {
50
- vi.stubEnv("PAYRAM_API_KEY", "pk_test_123");
51
- vi.stubEnv("PAYRAM_BASE_URL", "https://api.payram.test");
52
- const config = loadPayRamConfig();
53
- expect(config).toEqual({
54
- apiKey: "pk_test_123",
55
- baseUrl: "https://api.payram.test",
56
- });
57
- });
58
-
59
- it("returns null when both env vars are missing", () => {
60
- expect(loadPayRamConfig()).toBeNull();
61
- });
62
- });
@@ -1,20 +0,0 @@
1
- export type {
2
- IPayRamChargeRepository,
3
- PayRamBillingConfig,
4
- PayRamChargeRecord,
5
- PayRamCheckoutOpts,
6
- PayRamConfig,
7
- PayRamPaymentState,
8
- PayRamWebhookPayload,
9
- PayRamWebhookResult,
10
- } from "@wopr-network/platform-core/billing";
11
- export {
12
- createPayRamCheckout,
13
- createPayRamClient,
14
- DrizzlePayRamChargeRepository,
15
- loadPayRamConfig,
16
- MIN_PAYMENT_USD,
17
- PayRamChargeRepository,
18
- } from "@wopr-network/platform-core/billing";
19
- export type { PayRamWebhookDeps } from "./webhook.js";
20
- export { handlePayRamWebhook } from "./webhook.js";
@@ -1,327 +0,0 @@
1
- /**
2
- * Unit tests for PayRam webhook handler (WOP-407).
3
- *
4
- * Covers FILLED/OVER_FILLED crediting the ledger, PARTIALLY_FILLED/CANCELLED
5
- * no-op status, idempotency, replay guard, and bot reactivation.
6
- */
7
-
8
- import type { PGlite } from "@electric-sql/pglite";
9
- import type { PayRamWebhookPayload } from "@wopr-network/platform-core/billing";
10
- import {
11
- DrizzleWebhookSeenRepository,
12
- noOpReplayGuard,
13
- PayRamChargeRepository,
14
- } from "@wopr-network/platform-core/billing";
15
- import { DrizzleLedger } from "@wopr-network/platform-core/credits";
16
- import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
17
- import type { DrizzleDb } from "../../db/index.js";
18
- import { createTestDb, truncateAllTables } from "../../test/db.js";
19
- import type { PayRamWebhookDeps } from "./webhook.js";
20
- import { handlePayRamWebhook } from "./webhook.js";
21
-
22
- function makePayload(overrides: Partial<PayRamWebhookPayload> = {}): PayRamWebhookPayload {
23
- return {
24
- reference_id: "ref-test-001",
25
- status: "FILLED",
26
- amount: "25.00",
27
- currency: "USDC",
28
- filled_amount: "25.00",
29
- ...overrides,
30
- };
31
- }
32
-
33
- // TOP OF FILE - shared across ALL describes
34
- let pool: PGlite;
35
- let db: DrizzleDb;
36
-
37
- beforeAll(async () => {
38
- ({ db, pool } = await createTestDb());
39
- });
40
-
41
- afterAll(async () => {
42
- await pool.close();
43
- });
44
-
45
- describe("handlePayRamWebhook", () => {
46
- let chargeStore: PayRamChargeRepository;
47
- let creditLedger: DrizzleLedger;
48
- let deps: PayRamWebhookDeps;
49
-
50
- beforeEach(async () => {
51
- await truncateAllTables(pool);
52
- chargeStore = new PayRamChargeRepository(db);
53
- creditLedger = new DrizzleLedger(db);
54
-
55
- await creditLedger.seedSystemAccounts();
56
- deps = { chargeStore, creditLedger, replayGuard: noOpReplayGuard };
57
-
58
- // Create a default test charge
59
- await chargeStore.create("ref-test-001", "tenant-a", 2500);
60
- });
61
-
62
- // ---------------------------------------------------------------------------
63
- // FILLED / OVER_FILLED — should credit ledger
64
- // ---------------------------------------------------------------------------
65
-
66
- describe("FILLED status", () => {
67
- it("credits the ledger with the requested USD amount", async () => {
68
- const result = await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
69
-
70
- expect(result.handled).toBe(true);
71
- expect(result.status).toBe("FILLED");
72
- expect(result.tenant).toBe("tenant-a");
73
- expect(result.creditedCents).toBe(2500);
74
-
75
- const balance = await creditLedger.balance("tenant-a");
76
- expect(balance.toCents()).toBe(2500);
77
- });
78
-
79
- it("uses payram: prefix on reference ID in credit transaction", async () => {
80
- await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
81
-
82
- const history = await creditLedger.history("tenant-a");
83
- expect(history).toHaveLength(1);
84
- expect(history[0].referenceId).toBe("payram:ref-test-001");
85
- expect(history[0].entryType).toBe("purchase");
86
- });
87
-
88
- it("records fundingSource as payram", async () => {
89
- await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
90
-
91
- const history = await creditLedger.history("tenant-a");
92
- expect(history[0].metadata?.fundingSource).toBe("payram");
93
- });
94
-
95
- it("marks the charge as credited after FILLED", async () => {
96
- await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
97
- expect(await chargeStore.isCredited("ref-test-001")).toBe(true);
98
- });
99
-
100
- it("is idempotent — duplicate FILLED webhook does not double-credit", async () => {
101
- await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
102
- const result2 = await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
103
-
104
- expect(result2.handled).toBe(true);
105
- expect(result2.creditedCents).toBe(0);
106
-
107
- const balance = await creditLedger.balance("tenant-a");
108
- expect(balance.toCents()).toBe(2500); // Only credited once
109
- });
110
- });
111
-
112
- describe("OVER_FILLED status", () => {
113
- it("credits the requested USD amount (not the overpayment)", async () => {
114
- await chargeStore.create("ref-over-001", "tenant-b", 1000);
115
-
116
- const result = await handlePayRamWebhook(
117
- deps,
118
- makePayload({
119
- reference_id: "ref-over-001",
120
- status: "OVER_FILLED",
121
- filled_amount: "12.50", // Overpaid by $2.50
122
- currency: "ETH",
123
- }),
124
- );
125
-
126
- expect(result.handled).toBe(true);
127
- expect(result.creditedCents).toBe(1000); // Only the requested amount
128
- expect((await creditLedger.balance("tenant-b")).toCents()).toBe(1000);
129
- });
130
- });
131
-
132
- // ---------------------------------------------------------------------------
133
- // Statuses that should NOT credit the ledger
134
- // ---------------------------------------------------------------------------
135
-
136
- describe("PARTIALLY_FILLED status", () => {
137
- it("does NOT credit the ledger", async () => {
138
- const result = await handlePayRamWebhook(deps, makePayload({ status: "PARTIALLY_FILLED" }));
139
-
140
- expect(result.handled).toBe(true);
141
- expect(result.tenant).toBe("tenant-a");
142
- expect(result.creditedCents).toBeUndefined();
143
- expect((await creditLedger.balance("tenant-a")).toCents()).toBe(0);
144
- });
145
- });
146
-
147
- describe("VERIFYING status", () => {
148
- it("does NOT credit the ledger", async () => {
149
- const result = await handlePayRamWebhook(deps, makePayload({ status: "VERIFYING" }));
150
-
151
- expect(result.handled).toBe(true);
152
- expect(result.creditedCents).toBeUndefined();
153
- expect((await creditLedger.balance("tenant-a")).toCents()).toBe(0);
154
- });
155
- });
156
-
157
- describe("OPEN status", () => {
158
- it("does NOT credit the ledger", async () => {
159
- const result = await handlePayRamWebhook(deps, makePayload({ status: "OPEN" }));
160
-
161
- expect(result.handled).toBe(true);
162
- expect(result.creditedCents).toBeUndefined();
163
- expect((await creditLedger.balance("tenant-a")).toCents()).toBe(0);
164
- });
165
- });
166
-
167
- describe("CANCELLED status", () => {
168
- it("does NOT credit the ledger", async () => {
169
- const result = await handlePayRamWebhook(deps, makePayload({ status: "CANCELLED" }));
170
-
171
- expect(result.handled).toBe(true);
172
- expect(result.creditedCents).toBeUndefined();
173
- expect((await creditLedger.balance("tenant-a")).toCents()).toBe(0);
174
- });
175
- });
176
-
177
- // ---------------------------------------------------------------------------
178
- // Unknown reference ID
179
- // ---------------------------------------------------------------------------
180
-
181
- describe("unknown reference_id", () => {
182
- it("returns handled:false when charge not found", async () => {
183
- const result = await handlePayRamWebhook(deps, makePayload({ reference_id: "ref-unknown-999" }));
184
-
185
- expect(result.handled).toBe(false);
186
- expect(result.tenant).toBeUndefined();
187
- });
188
- });
189
-
190
- // ---------------------------------------------------------------------------
191
- // Charge store updates
192
- // ---------------------------------------------------------------------------
193
-
194
- describe("charge store updates", () => {
195
- it("updates charge status on every webhook call", async () => {
196
- await handlePayRamWebhook(deps, makePayload({ status: "VERIFYING" }));
197
-
198
- const charge = await chargeStore.getByReferenceId("ref-test-001");
199
- expect(charge?.status).toBe("VERIFYING");
200
- });
201
-
202
- it("updates currency and filled_amount on FILLED", async () => {
203
- await handlePayRamWebhook(deps, makePayload({ status: "FILLED", currency: "USDT", filled_amount: "25.00" }));
204
-
205
- const charge = await chargeStore.getByReferenceId("ref-test-001");
206
- expect(charge?.currency).toBe("USDT");
207
- expect(charge?.filledAmount).toBe("25.00");
208
- });
209
- });
210
-
211
- // ---------------------------------------------------------------------------
212
- // Multiple tenants / reference IDs
213
- // ---------------------------------------------------------------------------
214
-
215
- describe("different reference IDs", () => {
216
- it("processes multiple reference IDs independently", async () => {
217
- await chargeStore.create("ref-b-001", "tenant-b", 5000);
218
- await chargeStore.create("ref-c-001", "tenant-c", 1500);
219
-
220
- await handlePayRamWebhook(deps, makePayload({ reference_id: "ref-b-001", status: "FILLED" }));
221
- await handlePayRamWebhook(deps, makePayload({ reference_id: "ref-c-001", status: "FILLED" }));
222
-
223
- expect((await creditLedger.balance("tenant-b")).toCents()).toBe(5000);
224
- expect((await creditLedger.balance("tenant-c")).toCents()).toBe(1500);
225
- });
226
- });
227
-
228
- // ---------------------------------------------------------------------------
229
- // Replay guard
230
- // ---------------------------------------------------------------------------
231
-
232
- describe("replay guard", () => {
233
- it("blocks duplicate reference_id + status combos", async () => {
234
- const replayGuard = new DrizzleWebhookSeenRepository(db);
235
- const depsWithGuard: PayRamWebhookDeps = { ...deps, replayGuard };
236
-
237
- const first = await handlePayRamWebhook(depsWithGuard, makePayload({ status: "FILLED" }));
238
- expect(first.handled).toBe(true);
239
- expect(first.creditedCents).toBe(2500);
240
- expect(first.duplicate).toBeUndefined();
241
-
242
- const second = await handlePayRamWebhook(depsWithGuard, makePayload({ status: "FILLED" }));
243
- expect(second.handled).toBe(true);
244
- expect(second.duplicate).toBe(true);
245
- expect(second.creditedCents).toBeUndefined();
246
-
247
- // Only credited once
248
- expect((await creditLedger.balance("tenant-a")).toCents()).toBe(2500);
249
- });
250
-
251
- it("same reference_id with different status is not blocked by replay guard", async () => {
252
- const replayGuard = new DrizzleWebhookSeenRepository(db);
253
- const depsWithGuard: PayRamWebhookDeps = { ...deps, replayGuard };
254
-
255
- await handlePayRamWebhook(depsWithGuard, makePayload({ status: "VERIFYING" }));
256
- const result = await handlePayRamWebhook(depsWithGuard, makePayload({ status: "FILLED" }));
257
-
258
- expect(result.duplicate).toBeUndefined();
259
- expect(result.creditedCents).toBe(2500);
260
- });
261
- });
262
-
263
- // ---------------------------------------------------------------------------
264
- // Bot reactivation
265
- // ---------------------------------------------------------------------------
266
-
267
- describe("bot reactivation", () => {
268
- it("calls botBilling.checkReactivation on FILLED and includes reactivatedBots in result", async () => {
269
- const mockCheckReactivation = vi.fn().mockReturnValue(["bot-1", "bot-2"]);
270
- const depsWithBotBilling: PayRamWebhookDeps = {
271
- ...deps,
272
- botBilling: { checkReactivation: mockCheckReactivation } as unknown as Parameters<
273
- typeof handlePayRamWebhook
274
- >[0]["botBilling"],
275
- };
276
-
277
- const result = await handlePayRamWebhook(depsWithBotBilling, makePayload({ status: "FILLED" }));
278
-
279
- expect(mockCheckReactivation).toHaveBeenCalledWith("tenant-a", creditLedger);
280
- expect(result.reactivatedBots).toEqual(["bot-1", "bot-2"]);
281
- });
282
-
283
- it("does not include reactivatedBots when no bots reactivated", async () => {
284
- const mockCheckReactivation = vi.fn().mockReturnValue([]);
285
- const depsWithBotBilling: PayRamWebhookDeps = {
286
- ...deps,
287
- botBilling: { checkReactivation: mockCheckReactivation } as unknown as Parameters<
288
- typeof handlePayRamWebhook
289
- >[0]["botBilling"],
290
- };
291
-
292
- const result = await handlePayRamWebhook(depsWithBotBilling, makePayload({ status: "FILLED" }));
293
-
294
- expect(result.reactivatedBots).toBeUndefined();
295
- });
296
- });
297
- });
298
-
299
- // ---------------------------------------------------------------------------
300
- // DrizzleWebhookSeenRepository unit tests (replaces PayRamReplayGuard)
301
- // ---------------------------------------------------------------------------
302
-
303
- describe("DrizzleWebhookSeenRepository (payram replay guard)", () => {
304
- beforeEach(async () => {
305
- await truncateAllTables(pool);
306
- });
307
-
308
- it("reports unseen keys as not duplicate", async () => {
309
- const guard = new DrizzleWebhookSeenRepository(db);
310
- expect(await guard.isDuplicate("ref-001:FILLED", "payram")).toBe(false);
311
- });
312
-
313
- it("reports seen keys as duplicate", async () => {
314
- const guard = new DrizzleWebhookSeenRepository(db);
315
- await guard.markSeen("ref-001:FILLED", "payram");
316
- expect(await guard.isDuplicate("ref-001:FILLED", "payram")).toBe(true);
317
- });
318
-
319
- it("purges expired entries via purgeExpired", async () => {
320
- const guard = new DrizzleWebhookSeenRepository(db);
321
- await guard.markSeen("ref-expire:FILLED", "payram");
322
- expect(await guard.isDuplicate("ref-expire:FILLED", "payram")).toBe(true);
323
- // Negative TTL pushes cutoff into the future — entry is expired
324
- await guard.purgeExpired(-24 * 60 * 60 * 1000);
325
- expect(await guard.isDuplicate("ref-expire:FILLED", "payram")).toBe(false);
326
- });
327
- });
@@ -1,97 +0,0 @@
1
- import type {
2
- IWebhookSeenRepository,
3
- PayRamChargeRepository,
4
- PayRamWebhookPayload,
5
- PayRamWebhookResult,
6
- } from "@wopr-network/platform-core/billing";
7
- import type { ILedger } from "@wopr-network/platform-core/credits";
8
- import { Credit } from "@wopr-network/platform-core/credits";
9
- import type { BotBilling } from "../credits/bot-billing.js";
10
-
11
- export interface PayRamWebhookDeps {
12
- chargeStore: PayRamChargeRepository;
13
- creditLedger: ILedger;
14
- botBilling?: BotBilling;
15
- replayGuard: IWebhookSeenRepository;
16
- }
17
-
18
- /**
19
- * Process a PayRam webhook event.
20
- *
21
- * Only credits the ledger on FILLED or OVER_FILLED status.
22
- * Uses the PayRam reference_id mapped to the stored charge record
23
- * for tenant resolution and idempotency.
24
- */
25
- export async function handlePayRamWebhook(
26
- deps: PayRamWebhookDeps,
27
- payload: PayRamWebhookPayload,
28
- ): Promise<PayRamWebhookResult> {
29
- const { chargeStore, creditLedger } = deps;
30
-
31
- // Replay guard: deduplicate by reference_id + status combination.
32
- const dedupeKey = `${payload.reference_id}:${payload.status}`;
33
- if (await deps.replayGuard.isDuplicate(dedupeKey, "payram")) {
34
- return { handled: true, status: payload.status, duplicate: true };
35
- }
36
-
37
- // Look up the charge record to find the tenant.
38
- const charge = await chargeStore.getByReferenceId(payload.reference_id);
39
- if (!charge) {
40
- return { handled: false, status: payload.status };
41
- }
42
-
43
- // Update charge status regardless of payment state.
44
- await chargeStore.updateStatus(payload.reference_id, payload.status, payload.currency, payload.filled_amount);
45
-
46
- let result: PayRamWebhookResult;
47
-
48
- if (payload.status === "FILLED" || payload.status === "OVER_FILLED") {
49
- // Idempotency: skip if already credited.
50
- if (await chargeStore.isCredited(payload.reference_id)) {
51
- result = {
52
- handled: true,
53
- status: payload.status,
54
- tenant: charge.tenantId,
55
- creditedCents: 0,
56
- };
57
- } else {
58
- // Credit the original USD amount requested (not the crypto amount).
59
- // For OVER_FILLED, we still credit the requested amount — the
60
- // overpayment stays in the PayRam wallet as a buffer.
61
- const creditCents = charge.amountUsdCents;
62
-
63
- await creditLedger.credit(charge.tenantId, Credit.fromCents(creditCents), "purchase", {
64
- description: `Crypto credit purchase via PayRam (ref: ${payload.reference_id}, ${payload.currency ?? "crypto"})`,
65
- referenceId: `payram:${payload.reference_id}`,
66
- fundingSource: "payram",
67
- });
68
-
69
- await chargeStore.markCredited(payload.reference_id);
70
-
71
- // Reactivate suspended bots (same as Stripe webhook, WOP-447).
72
- let reactivatedBots: string[] | undefined;
73
- if (deps.botBilling) {
74
- reactivatedBots = await deps.botBilling.checkReactivation(charge.tenantId, creditLedger);
75
- if (reactivatedBots.length === 0) reactivatedBots = undefined;
76
- }
77
-
78
- result = {
79
- handled: true,
80
- status: payload.status,
81
- tenant: charge.tenantId,
82
- creditedCents: creditCents,
83
- reactivatedBots,
84
- };
85
- }
86
- } else {
87
- // OPEN, VERIFYING, PARTIALLY_FILLED, CANCELLED — just track status.
88
- result = {
89
- handled: true,
90
- status: payload.status,
91
- tenant: charge.tenantId,
92
- };
93
- }
94
-
95
- await deps.replayGuard.markSeen(dedupeKey, "payram");
96
- return result;
97
- }