@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.
- package/dist/account/deletion-executor-repository.d.ts +2 -2
- package/dist/account/deletion-executor-repository.js +5 -5
- package/dist/{monetization/payram → billing/crypto}/cents-credits-boundary.test.js +14 -17
- package/dist/billing/crypto/charge-store.d.ts +68 -0
- package/dist/billing/crypto/charge-store.js +109 -0
- package/dist/billing/crypto/charge-store.test.js +120 -0
- package/dist/billing/crypto/checkout.d.ts +18 -0
- package/dist/billing/crypto/checkout.js +35 -0
- package/dist/billing/crypto/checkout.test.js +71 -0
- package/dist/billing/crypto/client.d.ts +39 -0
- package/dist/billing/crypto/client.js +72 -0
- package/dist/billing/crypto/client.test.js +100 -0
- package/dist/billing/crypto/evm/__tests__/address-gen.test.js +54 -0
- package/dist/billing/crypto/evm/__tests__/checkout.test.js +54 -0
- package/dist/billing/crypto/evm/__tests__/config.test.js +52 -0
- package/dist/billing/crypto/evm/__tests__/settler.test.js +196 -0
- package/dist/billing/crypto/evm/__tests__/watcher.test.js +109 -0
- package/dist/billing/crypto/evm/address-gen.d.ts +8 -0
- package/dist/billing/crypto/evm/address-gen.js +29 -0
- package/dist/billing/crypto/evm/checkout.d.ts +26 -0
- package/dist/billing/crypto/evm/checkout.js +57 -0
- package/dist/billing/crypto/evm/config.d.ts +13 -0
- package/dist/billing/crypto/evm/config.js +46 -0
- package/dist/billing/crypto/evm/index.d.ts +9 -0
- package/dist/billing/crypto/evm/index.js +5 -0
- package/dist/billing/crypto/evm/settler.d.ts +23 -0
- package/dist/billing/crypto/evm/settler.js +60 -0
- package/dist/billing/crypto/evm/types.d.ts +40 -0
- package/dist/billing/crypto/evm/types.js +1 -0
- package/dist/billing/crypto/evm/watcher.d.ts +31 -0
- package/dist/billing/crypto/evm/watcher.js +91 -0
- package/dist/billing/crypto/index.d.ts +10 -0
- package/dist/billing/crypto/index.js +6 -0
- package/dist/billing/crypto/types.d.ts +61 -0
- package/dist/billing/crypto/types.js +24 -0
- package/dist/billing/crypto/webhook.d.ts +34 -0
- package/dist/billing/crypto/webhook.js +107 -0
- package/dist/billing/crypto/webhook.test.d.ts +1 -0
- package/dist/billing/crypto/webhook.test.js +266 -0
- package/dist/billing/index.d.ts +1 -1
- package/dist/billing/index.js +2 -2
- package/dist/billing/payment-processor.d.ts +3 -3
- package/dist/credits/credit-ledger.d.ts +3 -3
- package/dist/credits/credit-ledger.js +3 -3
- package/dist/db/schema/credits.js +1 -1
- package/dist/db/schema/{payram.d.ts → crypto.d.ts} +85 -13
- package/dist/db/schema/crypto.js +32 -0
- package/dist/db/schema/index.d.ts +1 -1
- package/dist/db/schema/index.js +1 -1
- package/dist/monetization/crypto/__tests__/webhook.test.d.ts +1 -0
- package/dist/monetization/crypto/__tests__/webhook.test.js +249 -0
- package/dist/monetization/crypto/index.d.ts +4 -0
- package/dist/monetization/crypto/index.js +2 -0
- package/dist/monetization/crypto/webhook.d.ts +24 -0
- package/dist/monetization/crypto/webhook.js +88 -0
- package/dist/monetization/index.d.ts +3 -3
- package/dist/monetization/index.js +1 -1
- package/dist/monetization/repository-types.d.ts +1 -1
- package/dist/observability/pagerduty.test.js +1 -0
- package/docs/superpowers/plans/2026-03-14-stablecoin-phase1.md +1413 -0
- package/drizzle/migrations/0004_crypto_charges.sql +25 -0
- package/drizzle/migrations/0005_stablecoin_columns.sql +7 -0
- package/drizzle/migrations/meta/_journal.json +14 -0
- package/package.json +4 -3
- package/src/account/deletion-executor-repository.ts +6 -6
- package/src/billing/{payram → crypto}/cents-credits-boundary.test.ts +14 -17
- package/src/billing/crypto/charge-store.test.ts +142 -0
- package/src/billing/crypto/charge-store.ts +166 -0
- package/src/billing/crypto/checkout.test.ts +93 -0
- package/src/billing/crypto/checkout.ts +48 -0
- package/src/billing/crypto/client.test.ts +132 -0
- package/src/billing/crypto/client.ts +86 -0
- package/src/billing/crypto/evm/__tests__/address-gen.test.ts +63 -0
- package/src/billing/crypto/evm/__tests__/checkout.test.ts +83 -0
- package/src/billing/crypto/evm/__tests__/config.test.ts +63 -0
- package/src/billing/crypto/evm/__tests__/settler.test.ts +218 -0
- package/src/billing/crypto/evm/__tests__/watcher.test.ts +128 -0
- package/src/billing/crypto/evm/address-gen.ts +29 -0
- package/src/billing/crypto/evm/checkout.ts +82 -0
- package/src/billing/crypto/evm/config.ts +50 -0
- package/src/billing/crypto/evm/index.ts +16 -0
- package/src/billing/crypto/evm/settler.ts +79 -0
- package/src/billing/crypto/evm/types.ts +45 -0
- package/src/billing/crypto/evm/watcher.ts +126 -0
- package/src/billing/crypto/index.ts +16 -0
- package/src/billing/crypto/types.ts +83 -0
- package/src/billing/crypto/webhook.test.ts +340 -0
- package/src/billing/crypto/webhook.ts +136 -0
- package/src/billing/index.ts +2 -2
- package/src/billing/payment-processor.ts +3 -3
- package/src/credits/credit-ledger.ts +3 -3
- package/src/db/schema/credits.ts +1 -1
- package/src/db/schema/crypto.ts +37 -0
- package/src/db/schema/index.ts +1 -1
- package/src/monetization/crypto/__tests__/webhook.test.ts +327 -0
- package/src/monetization/crypto/index.ts +23 -0
- package/src/monetization/crypto/webhook.ts +115 -0
- package/src/monetization/index.ts +23 -21
- package/src/monetization/repository-types.ts +2 -2
- package/src/observability/pagerduty.test.ts +1 -0
- package/dist/billing/payram/cents-credits-boundary.test.js +0 -75
- package/dist/billing/payram/charge-store.d.ts +0 -41
- package/dist/billing/payram/charge-store.js +0 -72
- package/dist/billing/payram/charge-store.test.js +0 -64
- package/dist/billing/payram/checkout.d.ts +0 -15
- package/dist/billing/payram/checkout.js +0 -24
- package/dist/billing/payram/checkout.test.js +0 -74
- package/dist/billing/payram/client.d.ts +0 -7
- package/dist/billing/payram/client.js +0 -15
- package/dist/billing/payram/client.test.js +0 -52
- package/dist/billing/payram/index.d.ts +0 -8
- package/dist/billing/payram/index.js +0 -4
- package/dist/billing/payram/types.d.ts +0 -40
- package/dist/billing/payram/webhook.d.ts +0 -19
- package/dist/billing/payram/webhook.js +0 -71
- package/dist/billing/payram/webhook.test.d.ts +0 -7
- package/dist/billing/payram/webhook.test.js +0 -249
- package/dist/db/schema/payram.js +0 -21
- package/dist/monetization/payram/charge-store.test.js +0 -64
- package/dist/monetization/payram/checkout.test.js +0 -73
- package/dist/monetization/payram/client.test.js +0 -52
- package/dist/monetization/payram/index.d.ts +0 -4
- package/dist/monetization/payram/index.js +0 -2
- package/dist/monetization/payram/webhook.d.ts +0 -17
- package/dist/monetization/payram/webhook.js +0 -71
- package/dist/monetization/payram/webhook.test.d.ts +0 -7
- package/dist/monetization/payram/webhook.test.js +0 -247
- package/src/billing/payram/charge-store.test.ts +0 -84
- package/src/billing/payram/charge-store.ts +0 -109
- package/src/billing/payram/checkout.test.ts +0 -99
- package/src/billing/payram/checkout.ts +0 -40
- package/src/billing/payram/client.test.ts +0 -62
- package/src/billing/payram/client.ts +0 -21
- package/src/billing/payram/index.ts +0 -14
- package/src/billing/payram/types.ts +0 -44
- package/src/billing/payram/webhook.test.ts +0 -320
- package/src/billing/payram/webhook.ts +0 -94
- package/src/db/schema/payram.ts +0 -26
- package/src/monetization/payram/cents-credits-boundary.test.ts +0 -84
- package/src/monetization/payram/charge-store.test.ts +0 -84
- package/src/monetization/payram/checkout.test.ts +0 -98
- package/src/monetization/payram/client.test.ts +0 -62
- package/src/monetization/payram/index.ts +0 -20
- package/src/monetization/payram/webhook.test.ts +0 -327
- package/src/monetization/payram/webhook.ts +0 -97
- /package/dist/billing/{payram → crypto}/cents-credits-boundary.test.d.ts +0 -0
- /package/dist/billing/{payram → crypto}/charge-store.test.d.ts +0 -0
- /package/dist/billing/{payram → crypto}/checkout.test.d.ts +0 -0
- /package/dist/billing/{payram → crypto}/client.test.d.ts +0 -0
- /package/dist/billing/{payram/types.js → crypto/evm/__tests__/address-gen.test.d.ts} +0 -0
- /package/dist/{monetization/payram → billing/crypto/evm/__tests__}/checkout.test.d.ts +0 -0
- /package/dist/{monetization/payram/cents-credits-boundary.test.d.ts → billing/crypto/evm/__tests__/config.test.d.ts} +0 -0
- /package/dist/{monetization/payram/charge-store.test.d.ts → billing/crypto/evm/__tests__/settler.test.d.ts} +0 -0
- /package/dist/{monetization/payram/client.test.d.ts → billing/crypto/evm/__tests__/watcher.test.d.ts} +0 -0
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/** PayRam payment states (from PayRam API docs). */
|
|
2
|
-
export type PayRamPaymentState = "OPEN" | "VERIFYING" | "FILLED" | "OVER_FILLED" | "PARTIALLY_FILLED" | "CANCELLED";
|
|
3
|
-
|
|
4
|
-
/** Options for creating a PayRam payment session. */
|
|
5
|
-
export interface PayRamCheckoutOpts {
|
|
6
|
-
/** Internal tenant ID. */
|
|
7
|
-
tenant: string;
|
|
8
|
-
/** Amount in USD (minimum $10). */
|
|
9
|
-
amountUsd: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Webhook payload received from PayRam. */
|
|
13
|
-
export interface PayRamWebhookPayload {
|
|
14
|
-
/** Unique payment reference from session creation. */
|
|
15
|
-
reference_id: string;
|
|
16
|
-
/** Merchant invoice ID (echoed back if sent). */
|
|
17
|
-
invoice_id?: string;
|
|
18
|
-
/** Payment status. */
|
|
19
|
-
status: PayRamPaymentState;
|
|
20
|
-
/** Amount filled in this update. */
|
|
21
|
-
amount: string;
|
|
22
|
-
/** Currency symbol (ETH, USDC, USDT, etc.). */
|
|
23
|
-
currency: string;
|
|
24
|
-
/** Cumulative total filled so far. */
|
|
25
|
-
filled_amount: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Configuration for PayRam billing integration. */
|
|
29
|
-
export interface PayRamBillingConfig {
|
|
30
|
-
/** PayRam API key (from dashboard). */
|
|
31
|
-
apiKey: string;
|
|
32
|
-
/** PayRam self-hosted server base URL. */
|
|
33
|
-
baseUrl: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Result of processing a PayRam webhook event. */
|
|
37
|
-
export interface PayRamWebhookResult {
|
|
38
|
-
handled: boolean;
|
|
39
|
-
status: string;
|
|
40
|
-
tenant?: string;
|
|
41
|
-
creditedCents?: number;
|
|
42
|
-
reactivatedBots?: string[];
|
|
43
|
-
duplicate?: boolean;
|
|
44
|
-
}
|
|
@@ -1,320 +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 { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
-
import { DrizzleLedger } from "../../credits/ledger.js";
|
|
11
|
-
import type { PlatformDb } from "../../db/index.js";
|
|
12
|
-
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
13
|
-
import { DrizzleWebhookSeenRepository } from "../drizzle-webhook-seen-repository.js";
|
|
14
|
-
import { noOpReplayGuard } from "../webhook-seen-repository.js";
|
|
15
|
-
import { PayRamChargeRepository } from "./charge-store.js";
|
|
16
|
-
import type { PayRamWebhookDeps, PayRamWebhookPayload } from "./index.js";
|
|
17
|
-
import { handlePayRamWebhook } from "./webhook.js";
|
|
18
|
-
|
|
19
|
-
function makePayload(overrides: Partial<PayRamWebhookPayload> = {}): PayRamWebhookPayload {
|
|
20
|
-
return {
|
|
21
|
-
reference_id: "ref-test-001",
|
|
22
|
-
status: "FILLED",
|
|
23
|
-
amount: "25.00",
|
|
24
|
-
currency: "USDC",
|
|
25
|
-
filled_amount: "25.00",
|
|
26
|
-
...overrides,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// TOP OF FILE - shared across ALL describes
|
|
31
|
-
let pool: PGlite;
|
|
32
|
-
let db: PlatformDb;
|
|
33
|
-
|
|
34
|
-
beforeAll(async () => {
|
|
35
|
-
({ db, pool } = await createTestDb());
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
afterAll(async () => {
|
|
39
|
-
await pool.close();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe("handlePayRamWebhook", () => {
|
|
43
|
-
let chargeStore: PayRamChargeRepository;
|
|
44
|
-
let creditLedger: DrizzleLedger;
|
|
45
|
-
let deps: PayRamWebhookDeps;
|
|
46
|
-
|
|
47
|
-
beforeEach(async () => {
|
|
48
|
-
await truncateAllTables(pool);
|
|
49
|
-
chargeStore = new PayRamChargeRepository(db);
|
|
50
|
-
creditLedger = new DrizzleLedger(db);
|
|
51
|
-
|
|
52
|
-
await creditLedger.seedSystemAccounts();
|
|
53
|
-
deps = { chargeStore, creditLedger, replayGuard: noOpReplayGuard };
|
|
54
|
-
|
|
55
|
-
// Create a default test charge
|
|
56
|
-
await chargeStore.create("ref-test-001", "tenant-a", 2500);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// FILLED / OVER_FILLED — should credit ledger
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
describe("FILLED status", () => {
|
|
64
|
-
it("credits the ledger with the requested USD amount", async () => {
|
|
65
|
-
const result = await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
|
|
66
|
-
|
|
67
|
-
expect(result.handled).toBe(true);
|
|
68
|
-
expect(result.status).toBe("FILLED");
|
|
69
|
-
expect(result.tenant).toBe("tenant-a");
|
|
70
|
-
expect(result.creditedCents).toBe(2500);
|
|
71
|
-
|
|
72
|
-
const balance = await creditLedger.balance("tenant-a");
|
|
73
|
-
expect(balance.toCents()).toBe(2500);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("uses payram: prefix on reference ID in credit transaction", async () => {
|
|
77
|
-
await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
|
|
78
|
-
|
|
79
|
-
const history = await creditLedger.history("tenant-a");
|
|
80
|
-
expect(history).toHaveLength(1);
|
|
81
|
-
expect(history[0].referenceId).toBe("payram:ref-test-001");
|
|
82
|
-
expect(history[0].entryType).toBe("purchase");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("records fundingSource as payram", async () => {
|
|
86
|
-
await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
|
|
87
|
-
|
|
88
|
-
const history = await creditLedger.history("tenant-a");
|
|
89
|
-
expect(history[0].metadata?.fundingSource).toBe("payram");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("marks the charge as credited after FILLED", async () => {
|
|
93
|
-
await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
|
|
94
|
-
expect(await chargeStore.isCredited("ref-test-001")).toBe(true);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("is idempotent — duplicate FILLED webhook does not double-credit", async () => {
|
|
98
|
-
await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
|
|
99
|
-
const result2 = await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
|
|
100
|
-
|
|
101
|
-
expect(result2.handled).toBe(true);
|
|
102
|
-
expect(result2.creditedCents).toBe(0);
|
|
103
|
-
|
|
104
|
-
const balance = await creditLedger.balance("tenant-a");
|
|
105
|
-
expect(balance.toCents()).toBe(2500); // Only credited once
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe("OVER_FILLED status", () => {
|
|
110
|
-
it("credits the requested USD amount (not the overpayment)", async () => {
|
|
111
|
-
await chargeStore.create("ref-over-001", "tenant-b", 1000);
|
|
112
|
-
|
|
113
|
-
const result = await handlePayRamWebhook(
|
|
114
|
-
deps,
|
|
115
|
-
makePayload({
|
|
116
|
-
reference_id: "ref-over-001",
|
|
117
|
-
status: "OVER_FILLED",
|
|
118
|
-
filled_amount: "12.50", // Overpaid by $2.50
|
|
119
|
-
currency: "ETH",
|
|
120
|
-
}),
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
expect(result.handled).toBe(true);
|
|
124
|
-
expect(result.creditedCents).toBe(1000); // Only the requested amount
|
|
125
|
-
expect((await creditLedger.balance("tenant-b")).toCents()).toBe(1000);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
// Statuses that should NOT credit the ledger
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
|
|
133
|
-
describe("PARTIALLY_FILLED status", () => {
|
|
134
|
-
it("does NOT credit the ledger", async () => {
|
|
135
|
-
const result = await handlePayRamWebhook(deps, makePayload({ status: "PARTIALLY_FILLED" }));
|
|
136
|
-
|
|
137
|
-
expect(result.handled).toBe(true);
|
|
138
|
-
expect(result.tenant).toBe("tenant-a");
|
|
139
|
-
expect(result.creditedCents).toBeUndefined();
|
|
140
|
-
expect((await creditLedger.balance("tenant-a")).toCents()).toBe(0);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe("VERIFYING status", () => {
|
|
145
|
-
it("does NOT credit the ledger", async () => {
|
|
146
|
-
const result = await handlePayRamWebhook(deps, makePayload({ status: "VERIFYING" }));
|
|
147
|
-
|
|
148
|
-
expect(result.handled).toBe(true);
|
|
149
|
-
expect(result.creditedCents).toBeUndefined();
|
|
150
|
-
expect((await creditLedger.balance("tenant-a")).toCents()).toBe(0);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
describe("OPEN status", () => {
|
|
155
|
-
it("does NOT credit the ledger", async () => {
|
|
156
|
-
const result = await handlePayRamWebhook(deps, makePayload({ status: "OPEN" }));
|
|
157
|
-
|
|
158
|
-
expect(result.handled).toBe(true);
|
|
159
|
-
expect(result.creditedCents).toBeUndefined();
|
|
160
|
-
expect((await creditLedger.balance("tenant-a")).toCents()).toBe(0);
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe("CANCELLED status", () => {
|
|
165
|
-
it("does NOT credit the ledger", async () => {
|
|
166
|
-
const result = await handlePayRamWebhook(deps, makePayload({ status: "CANCELLED" }));
|
|
167
|
-
|
|
168
|
-
expect(result.handled).toBe(true);
|
|
169
|
-
expect(result.creditedCents).toBeUndefined();
|
|
170
|
-
expect((await creditLedger.balance("tenant-a")).toCents()).toBe(0);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
// Unknown reference ID
|
|
176
|
-
// ---------------------------------------------------------------------------
|
|
177
|
-
|
|
178
|
-
describe("unknown reference_id", () => {
|
|
179
|
-
it("returns handled:false when charge not found", async () => {
|
|
180
|
-
const result = await handlePayRamWebhook(deps, makePayload({ reference_id: "ref-unknown-999" }));
|
|
181
|
-
|
|
182
|
-
expect(result.handled).toBe(false);
|
|
183
|
-
expect(result.tenant).toBeUndefined();
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
// Charge store updates
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
|
|
191
|
-
describe("charge store updates", () => {
|
|
192
|
-
it("updates charge status on every webhook call", async () => {
|
|
193
|
-
await handlePayRamWebhook(deps, makePayload({ status: "VERIFYING" }));
|
|
194
|
-
|
|
195
|
-
const charge = await chargeStore.getByReferenceId("ref-test-001");
|
|
196
|
-
expect(charge?.status).toBe("VERIFYING");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("updates currency and filled_amount on FILLED", async () => {
|
|
200
|
-
await handlePayRamWebhook(deps, makePayload({ status: "FILLED", currency: "USDT", filled_amount: "25.00" }));
|
|
201
|
-
|
|
202
|
-
const charge = await chargeStore.getByReferenceId("ref-test-001");
|
|
203
|
-
expect(charge?.currency).toBe("USDT");
|
|
204
|
-
expect(charge?.filledAmount).toBe("25.00");
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
// Multiple tenants / reference IDs
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
|
|
212
|
-
describe("different reference IDs", () => {
|
|
213
|
-
it("processes multiple reference IDs independently", async () => {
|
|
214
|
-
await chargeStore.create("ref-b-001", "tenant-b", 5000);
|
|
215
|
-
await chargeStore.create("ref-c-001", "tenant-c", 1500);
|
|
216
|
-
|
|
217
|
-
await handlePayRamWebhook(deps, makePayload({ reference_id: "ref-b-001", status: "FILLED" }));
|
|
218
|
-
await handlePayRamWebhook(deps, makePayload({ reference_id: "ref-c-001", status: "FILLED" }));
|
|
219
|
-
|
|
220
|
-
expect((await creditLedger.balance("tenant-b")).toCents()).toBe(5000);
|
|
221
|
-
expect((await creditLedger.balance("tenant-c")).toCents()).toBe(1500);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
// Replay guard
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
|
|
229
|
-
describe("replay guard", () => {
|
|
230
|
-
it("blocks duplicate reference_id + status combos", async () => {
|
|
231
|
-
const replayGuard = new DrizzleWebhookSeenRepository(db);
|
|
232
|
-
const depsWithGuard: PayRamWebhookDeps = { ...deps, replayGuard };
|
|
233
|
-
|
|
234
|
-
const first = await handlePayRamWebhook(depsWithGuard, makePayload({ status: "FILLED" }));
|
|
235
|
-
expect(first.handled).toBe(true);
|
|
236
|
-
expect(first.creditedCents).toBe(2500);
|
|
237
|
-
expect(first.duplicate).toBeUndefined();
|
|
238
|
-
|
|
239
|
-
const second = await handlePayRamWebhook(depsWithGuard, makePayload({ status: "FILLED" }));
|
|
240
|
-
expect(second.handled).toBe(true);
|
|
241
|
-
expect(second.duplicate).toBe(true);
|
|
242
|
-
expect(second.creditedCents).toBeUndefined();
|
|
243
|
-
|
|
244
|
-
// Only credited once
|
|
245
|
-
expect((await creditLedger.balance("tenant-a")).toCents()).toBe(2500);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("same reference_id with different status is not blocked by replay guard", async () => {
|
|
249
|
-
const replayGuard = new DrizzleWebhookSeenRepository(db);
|
|
250
|
-
const depsWithGuard: PayRamWebhookDeps = { ...deps, replayGuard };
|
|
251
|
-
|
|
252
|
-
await handlePayRamWebhook(depsWithGuard, makePayload({ status: "VERIFYING" }));
|
|
253
|
-
const result = await handlePayRamWebhook(depsWithGuard, makePayload({ status: "FILLED" }));
|
|
254
|
-
|
|
255
|
-
expect(result.duplicate).toBeUndefined();
|
|
256
|
-
expect(result.creditedCents).toBe(2500);
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
// Bot reactivation
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
263
|
-
|
|
264
|
-
describe("resource reactivation via onCreditsPurchased", () => {
|
|
265
|
-
it("calls onCreditsPurchased on FILLED and includes reactivatedBots in result", async () => {
|
|
266
|
-
const mockOnCreditsPurchased = vi.fn().mockResolvedValue(["bot-1", "bot-2"]);
|
|
267
|
-
const depsWithCallback: PayRamWebhookDeps = {
|
|
268
|
-
...deps,
|
|
269
|
-
onCreditsPurchased: mockOnCreditsPurchased,
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const result = await handlePayRamWebhook(depsWithCallback, makePayload({ status: "FILLED" }));
|
|
273
|
-
|
|
274
|
-
expect(mockOnCreditsPurchased).toHaveBeenCalledWith("tenant-a", creditLedger);
|
|
275
|
-
expect(result.reactivatedBots).toEqual(["bot-1", "bot-2"]);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("does not include reactivatedBots when no resources reactivated", async () => {
|
|
279
|
-
const mockOnCreditsPurchased = vi.fn().mockResolvedValue([]);
|
|
280
|
-
const depsWithCallback: PayRamWebhookDeps = {
|
|
281
|
-
...deps,
|
|
282
|
-
onCreditsPurchased: mockOnCreditsPurchased,
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const result = await handlePayRamWebhook(depsWithCallback, makePayload({ status: "FILLED" }));
|
|
286
|
-
|
|
287
|
-
expect(result.reactivatedBots).toBeUndefined();
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// ---------------------------------------------------------------------------
|
|
293
|
-
// DrizzleWebhookSeenRepository unit tests (replaces PayRamReplayGuard)
|
|
294
|
-
// ---------------------------------------------------------------------------
|
|
295
|
-
|
|
296
|
-
describe("DrizzleWebhookSeenRepository (payram replay guard)", () => {
|
|
297
|
-
beforeEach(async () => {
|
|
298
|
-
await truncateAllTables(pool);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it("reports unseen keys as not duplicate", async () => {
|
|
302
|
-
const guard = new DrizzleWebhookSeenRepository(db);
|
|
303
|
-
expect(await guard.isDuplicate("ref-001:FILLED", "payram")).toBe(false);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it("reports seen keys as duplicate", async () => {
|
|
307
|
-
const guard = new DrizzleWebhookSeenRepository(db);
|
|
308
|
-
await guard.markSeen("ref-001:FILLED", "payram");
|
|
309
|
-
expect(await guard.isDuplicate("ref-001:FILLED", "payram")).toBe(true);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("purges expired entries via purgeExpired", async () => {
|
|
313
|
-
const guard = new DrizzleWebhookSeenRepository(db);
|
|
314
|
-
await guard.markSeen("ref-expire:FILLED", "payram");
|
|
315
|
-
expect(await guard.isDuplicate("ref-expire:FILLED", "payram")).toBe(true);
|
|
316
|
-
// Negative TTL pushes cutoff into the future — entry is expired
|
|
317
|
-
await guard.purgeExpired(-24 * 60 * 60 * 1000);
|
|
318
|
-
expect(await guard.isDuplicate("ref-expire:FILLED", "payram")).toBe(false);
|
|
319
|
-
});
|
|
320
|
-
});
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { Credit } from "../../credits/credit.js";
|
|
2
|
-
import type { ILedger } from "../../credits/ledger.js";
|
|
3
|
-
import type { IWebhookSeenRepository } from "../webhook-seen-repository.js";
|
|
4
|
-
import type { PayRamChargeRepository } from "./charge-store.js";
|
|
5
|
-
import type { PayRamWebhookPayload, PayRamWebhookResult } from "./types.js";
|
|
6
|
-
|
|
7
|
-
export interface PayRamWebhookDeps {
|
|
8
|
-
chargeStore: PayRamChargeRepository;
|
|
9
|
-
creditLedger: ILedger;
|
|
10
|
-
replayGuard: IWebhookSeenRepository;
|
|
11
|
-
/** Called after credits are purchased — consumer can reactivate suspended resources. Returns reactivated resource IDs. */
|
|
12
|
-
onCreditsPurchased?: (tenantId: string, ledger: ILedger) => Promise<string[]>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Process a PayRam webhook event.
|
|
17
|
-
*
|
|
18
|
-
* Only credits the ledger on FILLED or OVER_FILLED status.
|
|
19
|
-
* Uses the PayRam reference_id mapped to the stored charge record
|
|
20
|
-
* for tenant resolution and idempotency.
|
|
21
|
-
*/
|
|
22
|
-
export async function handlePayRamWebhook(
|
|
23
|
-
deps: PayRamWebhookDeps,
|
|
24
|
-
payload: PayRamWebhookPayload,
|
|
25
|
-
): Promise<PayRamWebhookResult> {
|
|
26
|
-
const { chargeStore, creditLedger } = deps;
|
|
27
|
-
|
|
28
|
-
// Replay guard: deduplicate by reference_id + status combination.
|
|
29
|
-
const dedupeKey = `${payload.reference_id}:${payload.status}`;
|
|
30
|
-
if (await deps.replayGuard.isDuplicate(dedupeKey, "payram")) {
|
|
31
|
-
return { handled: true, status: payload.status, duplicate: true };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Look up the charge record to find the tenant.
|
|
35
|
-
const charge = await chargeStore.getByReferenceId(payload.reference_id);
|
|
36
|
-
if (!charge) {
|
|
37
|
-
return { handled: false, status: payload.status };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Update charge status regardless of payment state.
|
|
41
|
-
await chargeStore.updateStatus(payload.reference_id, payload.status, payload.currency, payload.filled_amount);
|
|
42
|
-
|
|
43
|
-
let result: PayRamWebhookResult;
|
|
44
|
-
|
|
45
|
-
if (payload.status === "FILLED" || payload.status === "OVER_FILLED") {
|
|
46
|
-
// Idempotency: skip if already credited.
|
|
47
|
-
if (await chargeStore.isCredited(payload.reference_id)) {
|
|
48
|
-
result = {
|
|
49
|
-
handled: true,
|
|
50
|
-
status: payload.status,
|
|
51
|
-
tenant: charge.tenantId,
|
|
52
|
-
creditedCents: 0,
|
|
53
|
-
};
|
|
54
|
-
} else {
|
|
55
|
-
// Credit the original USD amount requested (not the crypto amount).
|
|
56
|
-
// For OVER_FILLED, we still credit the requested amount — the
|
|
57
|
-
// overpayment stays in the PayRam wallet as a buffer.
|
|
58
|
-
const creditCents = charge.amountUsdCents;
|
|
59
|
-
|
|
60
|
-
await creditLedger.credit(charge.tenantId, Credit.fromCents(creditCents), "purchase", {
|
|
61
|
-
description: `Crypto credit purchase via PayRam (ref: ${payload.reference_id}, ${payload.currency ?? "crypto"})`,
|
|
62
|
-
referenceId: `payram:${payload.reference_id}`,
|
|
63
|
-
fundingSource: "payram",
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
await chargeStore.markCredited(payload.reference_id);
|
|
67
|
-
|
|
68
|
-
// Reactivate suspended resources after credit purchase.
|
|
69
|
-
let reactivatedBots: string[] | undefined;
|
|
70
|
-
if (deps.onCreditsPurchased) {
|
|
71
|
-
reactivatedBots = await deps.onCreditsPurchased(charge.tenantId, creditLedger);
|
|
72
|
-
if (reactivatedBots.length === 0) reactivatedBots = undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
result = {
|
|
76
|
-
handled: true,
|
|
77
|
-
status: payload.status,
|
|
78
|
-
tenant: charge.tenantId,
|
|
79
|
-
creditedCents: creditCents,
|
|
80
|
-
reactivatedBots,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
} else {
|
|
84
|
-
// OPEN, VERIFYING, PARTIALLY_FILLED, CANCELLED — just track status.
|
|
85
|
-
result = {
|
|
86
|
-
handled: true,
|
|
87
|
-
status: payload.status,
|
|
88
|
-
tenant: charge.tenantId,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
await deps.replayGuard.markSeen(dedupeKey, "payram");
|
|
93
|
-
return result;
|
|
94
|
-
}
|
package/src/db/schema/payram.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { sql } from "drizzle-orm";
|
|
2
|
-
import { index, integer, pgTable, text } from "drizzle-orm/pg-core";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* PayRam payment sessions — tracks the lifecycle of each crypto payment.
|
|
6
|
-
* reference_id is the PayRam-assigned unique identifier.
|
|
7
|
-
*/
|
|
8
|
-
export const payramCharges = pgTable(
|
|
9
|
-
"payram_charges",
|
|
10
|
-
{
|
|
11
|
-
referenceId: text("reference_id").primaryKey(),
|
|
12
|
-
tenantId: text("tenant_id").notNull(),
|
|
13
|
-
amountUsdCents: integer("amount_usd_cents").notNull(),
|
|
14
|
-
status: text("status").notNull().default("OPEN"),
|
|
15
|
-
currency: text("currency"),
|
|
16
|
-
filledAmount: text("filled_amount"),
|
|
17
|
-
createdAt: text("created_at").notNull().default(sql`(now())`),
|
|
18
|
-
updatedAt: text("updated_at").notNull().default(sql`(now())`),
|
|
19
|
-
creditedAt: text("credited_at"),
|
|
20
|
-
},
|
|
21
|
-
(table) => [
|
|
22
|
-
index("idx_payram_charges_tenant").on(table.tenantId),
|
|
23
|
-
index("idx_payram_charges_status").on(table.status),
|
|
24
|
-
index("idx_payram_charges_created").on(table.createdAt),
|
|
25
|
-
],
|
|
26
|
-
);
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Regression tests for PayRam cents/credits boundary (WOP-1058).
|
|
5
|
-
*
|
|
6
|
-
* Verifies that the USD-to-cents conversion in checkout and
|
|
7
|
-
* the cents-to-credits flow in the webhook maintain correct units.
|
|
8
|
-
*
|
|
9
|
-
* If any of these tests fail after a rename/refactor, a _cents field was
|
|
10
|
-
* incorrectly changed to store Credit raw units (nanodollars) instead of
|
|
11
|
-
* USD cents. See src/monetization/credits/credit-ledger.ts for naming convention.
|
|
12
|
-
*/
|
|
13
|
-
describe("WOP-1058: PayRam cents/credits boundary", () => {
|
|
14
|
-
it("USD to cents conversion is correct (mirrors checkout.ts pattern)", () => {
|
|
15
|
-
// This mirrors the conversion in payram/checkout.ts: Math.round(opts.amountUsd * 100)
|
|
16
|
-
const amountUsd = 25;
|
|
17
|
-
const amountUsdCents = Math.round(amountUsd * 100);
|
|
18
|
-
expect(amountUsdCents).toBe(2500);
|
|
19
|
-
// Must NOT be nanodollar scale
|
|
20
|
-
expect(amountUsdCents).toBeLessThan(1_000_000);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("minimum payment amount converts to valid cents", () => {
|
|
24
|
-
const MIN_PAYMENT_USD = 10;
|
|
25
|
-
const cents = Math.round(MIN_PAYMENT_USD * 100);
|
|
26
|
-
expect(cents).toBe(1000);
|
|
27
|
-
expect(Number.isInteger(cents)).toBe(true);
|
|
28
|
-
// Sanity: $10 is 1000 cents, NOT 10_000_000_000 nanodollars
|
|
29
|
-
expect(cents).toBeLessThan(1_000_000);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("fractional USD amounts round correctly to cents", () => {
|
|
33
|
-
// Edge case: floating point conversion
|
|
34
|
-
const amountUsd = 10.99;
|
|
35
|
-
const cents = Math.round(amountUsd * 100);
|
|
36
|
-
expect(cents).toBe(1099);
|
|
37
|
-
expect(cents).toBeLessThan(1_000_000);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("amountUsdCents stored in charge record equals USD * 100 (not nanodollars)", () => {
|
|
41
|
-
// The core invariant: payram/checkout.ts stores Math.round(amountUsd * 100)
|
|
42
|
-
// as amountUsdCents. This test proves the conversion stays at cent scale.
|
|
43
|
-
const testCases: Array<{ usd: number; expectedCents: number }> = [
|
|
44
|
-
{ usd: 10, expectedCents: 1000 },
|
|
45
|
-
{ usd: 25, expectedCents: 2500 },
|
|
46
|
-
{ usd: 50, expectedCents: 5000 },
|
|
47
|
-
{ usd: 100, expectedCents: 10000 },
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
for (const { usd, expectedCents } of testCases) {
|
|
51
|
-
const amountUsdCents = Math.round(usd * 100);
|
|
52
|
-
expect(amountUsdCents).toBe(expectedCents);
|
|
53
|
-
// CREDIT SCALE = 1_000_000_000. If this value approaches that, unit confusion occurred.
|
|
54
|
-
expect(amountUsdCents).toBeLessThan(1_000_000);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("creditedCents in webhook equals amountUsdCents from charge store (1:1 for PayRam)", () => {
|
|
59
|
-
// payram/webhook.ts: const creditCents = charge.amountUsdCents;
|
|
60
|
-
// The credited amount always equals the stored USD cents — no bonus tiers for PayRam.
|
|
61
|
-
const chargeAmountUsdCents = 2500; // $25.00
|
|
62
|
-
const creditCents = chargeAmountUsdCents; // 1:1 for PayRam
|
|
63
|
-
expect(creditCents).toBe(2500);
|
|
64
|
-
// creditedCents must be 2500 (cents), not 25_000_000_000 (nanodollars)
|
|
65
|
-
expect(creditCents).toBeLessThan(1_000_000);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("cents-to-nanodollar scale difference is preserved as a sanity constant", () => {
|
|
69
|
-
// Credit.SCALE = 1_000_000_000 nanodollars per dollar
|
|
70
|
-
// 1 USD cent = 10_000_000 nanodollars (SCALE / 100)
|
|
71
|
-
// This test documents the relationship so future developers understand the gap.
|
|
72
|
-
const CREDIT_SCALE = 1_000_000_000;
|
|
73
|
-
const CENTS_PER_DOLLAR = 100;
|
|
74
|
-
const NANODOLLARS_PER_CENT = CREDIT_SCALE / CENTS_PER_DOLLAR;
|
|
75
|
-
|
|
76
|
-
expect(NANODOLLARS_PER_CENT).toBe(10_000_000);
|
|
77
|
-
|
|
78
|
-
// $25 in cents = 2500. $25 in nanodollars = 25_000_000_000.
|
|
79
|
-
// These are 10_000_000x apart — confirming that mixing the two is catastrophic.
|
|
80
|
-
const twentyFiveDollarsInCents = 2500;
|
|
81
|
-
const twentyFiveDollarsInNanodollars = 25 * CREDIT_SCALE;
|
|
82
|
-
expect(twentyFiveDollarsInNanodollars / twentyFiveDollarsInCents).toBe(NANODOLLARS_PER_CENT);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for PayRamChargeRepository (WOP-407).
|
|
3
|
-
*/
|
|
4
|
-
import type { PGlite } from "@electric-sql/pglite";
|
|
5
|
-
import { PayRamChargeRepository } from "@wopr-network/platform-core/billing";
|
|
6
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
7
|
-
import type { DrizzleDb } from "../../db/index.js";
|
|
8
|
-
import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../../test/db.js";
|
|
9
|
-
|
|
10
|
-
describe("PayRamChargeRepository", () => {
|
|
11
|
-
let pool: PGlite;
|
|
12
|
-
let db: DrizzleDb;
|
|
13
|
-
let store: PayRamChargeRepository;
|
|
14
|
-
|
|
15
|
-
beforeAll(async () => {
|
|
16
|
-
({ db, pool } = await createTestDb());
|
|
17
|
-
await beginTestTransaction(pool);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterAll(async () => {
|
|
21
|
-
await endTestTransaction(pool);
|
|
22
|
-
await pool.close();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
beforeEach(async () => {
|
|
26
|
-
await rollbackTestTransaction(pool);
|
|
27
|
-
store = new PayRamChargeRepository(db);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("create() stores a charge", async () => {
|
|
31
|
-
await store.create("ref-001", "tenant-1", 2500);
|
|
32
|
-
|
|
33
|
-
const charge = await store.getByReferenceId("ref-001");
|
|
34
|
-
expect(charge).not.toBeNull();
|
|
35
|
-
expect(charge?.referenceId).toBe("ref-001");
|
|
36
|
-
expect(charge?.tenantId).toBe("tenant-1");
|
|
37
|
-
expect(charge?.amountUsdCents).toBe(2500);
|
|
38
|
-
expect(charge?.status).toBe("OPEN");
|
|
39
|
-
expect(charge?.creditedAt).toBeNull();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("getByReferenceId() returns null when not found", async () => {
|
|
43
|
-
const charge = await store.getByReferenceId("ref-nonexistent");
|
|
44
|
-
expect(charge).toBeNull();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("updateStatus() updates status, currency and filled_amount", async () => {
|
|
48
|
-
await store.create("ref-002", "tenant-2", 5000);
|
|
49
|
-
await store.updateStatus("ref-002", "FILLED", "USDC", "50.00");
|
|
50
|
-
|
|
51
|
-
const charge = await store.getByReferenceId("ref-002");
|
|
52
|
-
expect(charge?.status).toBe("FILLED");
|
|
53
|
-
expect(charge?.currency).toBe("USDC");
|
|
54
|
-
expect(charge?.filledAmount).toBe("50.00");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("updateStatus() handles partial updates (no currency)", async () => {
|
|
58
|
-
await store.create("ref-003", "tenant-3", 1000);
|
|
59
|
-
await store.updateStatus("ref-003", "VERIFYING");
|
|
60
|
-
|
|
61
|
-
const charge = await store.getByReferenceId("ref-003");
|
|
62
|
-
expect(charge?.status).toBe("VERIFYING");
|
|
63
|
-
expect(charge?.currency).toBeNull();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("isCredited() returns false before markCredited", async () => {
|
|
67
|
-
await store.create("ref-004", "tenant-4", 1500);
|
|
68
|
-
expect(await store.isCredited("ref-004")).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("markCredited() sets creditedAt", async () => {
|
|
72
|
-
await store.create("ref-005", "tenant-5", 3000);
|
|
73
|
-
await store.markCredited("ref-005");
|
|
74
|
-
|
|
75
|
-
const charge = await store.getByReferenceId("ref-005");
|
|
76
|
-
expect(charge?.creditedAt).not.toBeNull();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("isCredited() returns true after markCredited", async () => {
|
|
80
|
-
await store.create("ref-006", "tenant-6", 2000);
|
|
81
|
-
await store.markCredited("ref-006");
|
|
82
|
-
expect(await store.isCredited("ref-006")).toBe(true);
|
|
83
|
-
});
|
|
84
|
-
});
|