@wopr-network/platform-core 1.13.3 → 1.14.1
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/.github/workflows/dependabot-auto-merge.yml +1 -2
- package/dist/api/routes/admin-credits.d.ts +2 -2
- package/dist/api/routes/admin-credits.js +9 -4
- package/dist/api/routes/quota.d.ts +2 -2
- package/dist/api/routes/verify-email.d.ts +3 -3
- package/dist/backup/on-demand-snapshot-service.d.ts +2 -2
- package/dist/billing/payram/webhook.d.ts +3 -3
- package/dist/billing/payram/webhook.js +5 -1
- package/dist/billing/payram/webhook.test.js +5 -4
- package/dist/billing/stripe/stripe-payment-processor.d.ts +2 -2
- package/dist/billing/stripe/stripe-payment-processor.test.js +7 -0
- package/dist/billing/stripe/tenant-store.d.ts +1 -1
- package/dist/billing/stripe/tenant-store.js +1 -1
- package/dist/credits/auto-topup-charge.d.ts +2 -2
- package/dist/credits/auto-topup-charge.js +5 -1
- package/dist/credits/auto-topup-charge.test.js +5 -4
- package/dist/credits/auto-topup-usage.d.ts +2 -2
- package/dist/credits/auto-topup-usage.test.js +53 -12
- package/dist/credits/credit-expiry-cron.d.ts +2 -2
- package/dist/credits/credit-expiry-cron.js +7 -4
- package/dist/credits/credit-expiry-cron.test.js +25 -8
- package/dist/credits/credit-ledger.d.ts +2 -2
- package/dist/credits/credit-ledger.js +1 -1
- package/dist/credits/dividend-cron.d.ts +4 -6
- package/dist/credits/dividend-cron.js +10 -16
- package/dist/credits/dividend-cron.test.js +31 -44
- package/dist/credits/dividend-repository.js +19 -22
- package/dist/credits/dividend-repository.test.js +4 -3
- package/dist/credits/index.d.ts +4 -2
- package/dist/credits/index.js +2 -1
- package/dist/credits/ledger.d.ts +195 -0
- package/dist/credits/ledger.js +561 -0
- package/dist/credits/ledger.test.js +418 -0
- package/dist/credits/signup-grant.d.ts +2 -2
- package/dist/credits/signup-grant.js +4 -4
- package/dist/credits/signup-grant.test.js +5 -3
- package/dist/credits/trial-balance-cron.d.ts +19 -0
- package/dist/credits/trial-balance-cron.js +30 -0
- package/dist/credits/trial-balance-cron.test.js +55 -0
- package/dist/db/schema/index.d.ts +1 -0
- package/dist/db/schema/index.js +1 -0
- package/dist/db/schema/ledger.d.ts +442 -0
- package/dist/db/schema/ledger.js +76 -0
- package/dist/gateway/credit-gate.d.ts +2 -2
- package/dist/gateway/credit-gate.js +5 -1
- package/dist/gateway/credit-gate.test.js +35 -33
- package/dist/gateway/protocol/deps.d.ts +2 -2
- package/dist/gateway/protocol/handlers.test.js +461 -0
- package/dist/gateway/proxy.d.ts +2 -2
- package/dist/gateway/types.d.ts +2 -2
- package/dist/metering/reconciliation-cron.test.js +9 -8
- package/dist/metering/reconciliation-repository.js +12 -10
- package/dist/metering/reconciliation-repository.test.js +9 -8
- package/dist/monetization/affiliate/affiliate-admin-repository.js +10 -8
- package/dist/monetization/affiliate/affiliate-admin-repository.test.js +32 -13
- package/dist/monetization/affiliate/credit-match.d.ts +2 -2
- package/dist/monetization/affiliate/credit-match.js +4 -1
- package/dist/monetization/affiliate/credit-match.test.js +58 -13
- package/dist/monetization/affiliate/new-user-bonus.d.ts +2 -2
- package/dist/monetization/affiliate/new-user-bonus.js +4 -1
- package/dist/monetization/affiliate/new-user-bonus.test.js +4 -3
- package/dist/monetization/credits/auto-topup-charge.d.ts +2 -2
- package/dist/monetization/credits/auto-topup-charge.js +5 -1
- package/dist/monetization/credits/auto-topup-charge.test.js +5 -4
- package/dist/monetization/credits/auto-topup-usage.d.ts +2 -2
- package/dist/monetization/credits/auto-topup-usage.test.js +53 -12
- package/dist/monetization/credits/bot-billing.d.ts +3 -3
- package/dist/monetization/credits/bot-billing.test.js +18 -5
- package/dist/monetization/credits/credit-expiry-cron.test.js +25 -8
- package/dist/monetization/credits/dividend-cron.d.ts +2 -4
- package/dist/monetization/credits/dividend-cron.js +7 -4
- package/dist/monetization/credits/dividend-cron.test.js +26 -46
- package/dist/monetization/credits/dividend-repository.js +15 -24
- package/dist/monetization/credits/dividend-repository.test.js +4 -3
- package/dist/monetization/credits/index.d.ts +2 -2
- package/dist/monetization/credits/index.js +1 -1
- package/dist/monetization/credits/member-usage.test.js +23 -10
- package/dist/monetization/credits/phone-billing.d.ts +2 -2
- package/dist/monetization/credits/phone-billing.js +5 -1
- package/dist/monetization/credits/phone-billing.test.js +9 -12
- package/dist/monetization/credits/runtime-cron.d.ts +2 -2
- package/dist/monetization/credits/runtime-cron.js +32 -8
- package/dist/monetization/credits/runtime-cron.test.js +28 -27
- package/dist/monetization/credits/runtime-scheduler.d.ts +2 -2
- package/dist/monetization/credits/runtime-scheduler.test.js +1 -1
- package/dist/monetization/credits/signup-grant.test.js +5 -3
- package/dist/monetization/credits/storage-tier-cron.test.js +3 -2
- package/dist/monetization/credits/trial-balance-cron.test.js +42 -0
- package/dist/monetization/feature-gate.d.ts +3 -3
- package/dist/monetization/index.d.ts +3 -3
- package/dist/monetization/index.js +1 -1
- package/dist/monetization/metering/reconciliation-cron.test.js +9 -8
- package/dist/monetization/metering/reconciliation-repository.js +11 -10
- package/dist/monetization/metering/reconciliation-repository.test.js +9 -8
- package/dist/monetization/payram/webhook.d.ts +2 -2
- package/dist/monetization/payram/webhook.js +5 -1
- package/dist/monetization/payram/webhook.test.js +5 -4
- package/dist/monetization/promotions/engine.d.ts +2 -2
- package/dist/monetization/promotions/engine.js +4 -1
- package/dist/monetization/promotions/engine.test.js +3 -1
- package/dist/monetization/repository-types.d.ts +1 -1
- package/dist/monetization/stripe/stripe-payment-processor.d.ts +2 -2
- package/dist/monetization/stripe/stripe-payment-processor.test.js +7 -0
- package/dist/monetization/stripe/webhook.d.ts +2 -2
- package/dist/monetization/stripe/webhook.js +70 -6
- package/dist/monetization/stripe/webhook.test.js +20 -15
- package/dist/onboarding/onboarding-service.d.ts +2 -2
- package/dist/onboarding/onboarding-service.js +6 -2
- package/drizzle/migrations/0003_double_entry_ledger.sql +82 -0
- package/drizzle/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/api/routes/admin-credits.ts +11 -14
- package/src/api/routes/quota.ts +2 -2
- package/src/api/routes/verify-email.ts +4 -4
- package/src/backup/on-demand-snapshot-service.test.ts +3 -3
- package/src/backup/on-demand-snapshot-service.ts +3 -3
- package/src/billing/payram/webhook.test.ts +7 -5
- package/src/billing/payram/webhook.ts +8 -11
- package/src/billing/stripe/stripe-payment-processor.test.ts +10 -3
- package/src/billing/stripe/stripe-payment-processor.ts +3 -3
- package/src/billing/stripe/tenant-store.ts +1 -1
- package/src/credits/auto-topup-charge.test.ts +7 -5
- package/src/credits/auto-topup-charge.ts +7 -10
- package/src/credits/auto-topup-usage.test.ts +55 -13
- package/src/credits/auto-topup-usage.ts +2 -2
- package/src/credits/credit-expiry-cron.test.ts +26 -45
- package/src/credits/credit-expiry-cron.ts +9 -12
- package/src/credits/credit-ledger.ts +3 -3
- package/src/credits/dividend-cron.test.ts +38 -45
- package/src/credits/dividend-cron.ts +12 -26
- package/src/credits/dividend-repository.test.ts +4 -3
- package/src/credits/dividend-repository.ts +21 -23
- package/src/credits/index.ts +23 -4
- package/src/credits/ledger.test.ts +514 -0
- package/src/credits/ledger.ts +851 -0
- package/src/credits/signup-grant.test.ts +7 -4
- package/src/credits/signup-grant.ts +6 -12
- package/src/credits/trial-balance-cron.test.ts +68 -0
- package/src/credits/trial-balance-cron.ts +46 -0
- package/src/db/schema/index.ts +1 -0
- package/src/db/schema/ledger.ts +94 -0
- package/src/gateway/credit-gate-wiring.test.ts +3 -3
- package/src/gateway/credit-gate.test.ts +35 -33
- package/src/gateway/credit-gate.ts +6 -10
- package/src/gateway/gateway-routes.test.ts +5 -5
- package/src/gateway/protocol/deps.ts +2 -2
- package/src/gateway/protocol/handlers.test.ts +549 -1
- package/src/gateway/proxy.ts +2 -2
- package/src/gateway/route-mounting.test.ts +2 -2
- package/src/gateway/types.ts +2 -2
- package/src/metering/reconciliation-cron.test.ts +10 -9
- package/src/metering/reconciliation-repository.test.ts +10 -9
- package/src/metering/reconciliation-repository.ts +14 -11
- package/src/monetization/affiliate/affiliate-admin-repository.test.ts +32 -19
- package/src/monetization/affiliate/affiliate-admin-repository.ts +16 -8
- package/src/monetization/affiliate/credit-match.test.ts +60 -14
- package/src/monetization/affiliate/credit-match.ts +6 -9
- package/src/monetization/affiliate/new-user-bonus.test.ts +6 -4
- package/src/monetization/affiliate/new-user-bonus.ts +6 -9
- package/src/monetization/credits/auto-topup-charge.test.ts +7 -5
- package/src/monetization/credits/auto-topup-charge.ts +7 -10
- package/src/monetization/credits/auto-topup-usage.test.ts +55 -13
- package/src/monetization/credits/auto-topup-usage.ts +2 -2
- package/src/monetization/credits/bot-billing.test.ts +20 -6
- package/src/monetization/credits/bot-billing.ts +3 -3
- package/src/monetization/credits/credit-expiry-cron.test.ts +26 -45
- package/src/monetization/credits/dividend-cron.test.ts +34 -48
- package/src/monetization/credits/dividend-cron.ts +9 -14
- package/src/monetization/credits/dividend-repository.test.ts +4 -3
- package/src/monetization/credits/dividend-repository.ts +19 -25
- package/src/monetization/credits/index.ts +4 -4
- package/src/monetization/credits/member-usage.test.ts +25 -11
- package/src/monetization/credits/phone-billing.test.ts +18 -26
- package/src/monetization/credits/phone-billing.ts +7 -10
- package/src/monetization/credits/runtime-cron.test.ts +29 -28
- package/src/monetization/credits/runtime-cron.ts +34 -58
- package/src/monetization/credits/runtime-scheduler.test.ts +1 -1
- package/src/monetization/credits/runtime-scheduler.ts +2 -2
- package/src/monetization/credits/signup-grant.test.ts +7 -4
- package/src/monetization/credits/storage-tier-cron.test.ts +5 -3
- package/src/monetization/credits/trial-balance-cron.test.ts +52 -0
- package/src/monetization/feature-gate.ts +3 -3
- package/src/monetization/index.ts +4 -4
- package/src/monetization/metering/reconciliation-cron.test.ts +10 -9
- package/src/monetization/metering/reconciliation-repository.test.ts +11 -9
- package/src/monetization/metering/reconciliation-repository.ts +13 -11
- package/src/monetization/payram/webhook.test.ts +7 -5
- package/src/monetization/payram/webhook.ts +7 -10
- package/src/monetization/promotions/engine.test.ts +6 -5
- package/src/monetization/promotions/engine.ts +6 -3
- package/src/monetization/repository-types.ts +1 -1
- package/src/monetization/stripe/stripe-payment-processor.test.ts +10 -3
- package/src/monetization/stripe/stripe-payment-processor.ts +3 -3
- package/src/monetization/stripe/webhook.test.ts +22 -16
- package/src/monetization/stripe/webhook.ts +75 -50
- package/src/onboarding/onboarding-service.ts +8 -11
- package/dist/credits/credit-ledger-extra.test.js +0 -40
- package/dist/credits/credit-ledger.bench.js +0 -33
- package/dist/credits/credit-ledger.test.d.ts +0 -4
- package/dist/credits/credit-ledger.test.js +0 -203
- package/dist/credits/credit-transaction-repository.test.js +0 -232
- package/dist/monetization/credits/credit-ledger-extra.test.d.ts +0 -1
- package/dist/monetization/credits/credit-ledger-extra.test.js +0 -39
- package/dist/monetization/credits/credit-ledger.bench.d.ts +0 -1
- package/dist/monetization/credits/credit-ledger.bench.js +0 -32
- package/dist/monetization/credits/credit-ledger.test.d.ts +0 -4
- package/dist/monetization/credits/credit-ledger.test.js +0 -202
- package/dist/monetization/credits/credit-transaction-repository.test.d.ts +0 -1
- package/dist/monetization/credits/credit-transaction-repository.test.js +0 -232
- package/src/credits/credit-ledger-extra.test.ts +0 -57
- package/src/credits/credit-ledger.bench.ts +0 -56
- package/src/credits/credit-ledger.test.ts +0 -276
- package/src/credits/credit-transaction-repository.test.ts +0 -274
- package/src/monetization/credits/credit-ledger-extra.test.ts +0 -56
- package/src/monetization/credits/credit-ledger.bench.ts +0 -55
- package/src/monetization/credits/credit-ledger.test.ts +0 -275
- package/src/monetization/credits/credit-transaction-repository.test.ts +0 -274
- /package/dist/credits/{credit-ledger-extra.test.d.ts → ledger.test.d.ts} +0 -0
- /package/dist/credits/{credit-ledger.bench.d.ts → trial-balance-cron.test.d.ts} +0 -0
- /package/dist/{credits/credit-transaction-repository.test.d.ts → monetization/credits/trial-balance-cron.test.d.ts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { Credit, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { logger } from "../../config/logger.js";
|
|
4
4
|
import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
|
|
@@ -20,7 +20,7 @@ export type GetActiveBotCount = (tenantId: string) => number | Promise<number>;
|
|
|
20
20
|
export const LOW_BALANCE_THRESHOLD = Credit.fromCents(100);
|
|
21
21
|
|
|
22
22
|
export interface RuntimeCronConfig {
|
|
23
|
-
ledger:
|
|
23
|
+
ledger: ILedger;
|
|
24
24
|
getActiveBotCount: GetActiveBotCount;
|
|
25
25
|
/** The date being billed, as YYYY-MM-DD. Used for idempotency. */
|
|
26
26
|
date: string;
|
|
@@ -122,13 +122,10 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
122
122
|
|
|
123
123
|
if (!balance.lessThan(totalCost)) {
|
|
124
124
|
// Full deduction
|
|
125
|
-
await cfg.ledger.debit(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
`Daily runtime: ${botCount} bot(s) x $${DAILY_BOT_COST.toDollars().toFixed(2)}`,
|
|
130
|
-
runtimeRef,
|
|
131
|
-
);
|
|
125
|
+
await cfg.ledger.debit(tenantId, totalCost, "bot_runtime", {
|
|
126
|
+
description: `Daily runtime: ${botCount} bot(s) x $${DAILY_BOT_COST.toDollars().toFixed(2)}`,
|
|
127
|
+
referenceId: runtimeRef,
|
|
128
|
+
});
|
|
132
129
|
|
|
133
130
|
// Debit resource tier surcharges (if any)
|
|
134
131
|
if (cfg.getResourceTierCosts) {
|
|
@@ -136,21 +133,15 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
136
133
|
if (!tierCost.isZero()) {
|
|
137
134
|
const balanceAfterRuntime = await cfg.ledger.balance(tenantId);
|
|
138
135
|
if (!balanceAfterRuntime.lessThan(tierCost)) {
|
|
139
|
-
await cfg.ledger.debit(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"Daily resource tier surcharge",
|
|
144
|
-
`runtime-tier:${cfg.date}:${tenantId}`,
|
|
145
|
-
);
|
|
136
|
+
await cfg.ledger.debit(tenantId, tierCost, "resource_upgrade", {
|
|
137
|
+
description: "Daily resource tier surcharge",
|
|
138
|
+
referenceId: `runtime-tier:${cfg.date}:${tenantId}`,
|
|
139
|
+
});
|
|
146
140
|
} else if (balanceAfterRuntime.greaterThan(Credit.ZERO)) {
|
|
147
|
-
await cfg.ledger.debit(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
"Partial resource tier surcharge (balance exhausted)",
|
|
152
|
-
`runtime-tier:${cfg.date}:${tenantId}`,
|
|
153
|
-
);
|
|
141
|
+
await cfg.ledger.debit(tenantId, balanceAfterRuntime, "resource_upgrade", {
|
|
142
|
+
description: "Partial resource tier surcharge (balance exhausted)",
|
|
143
|
+
referenceId: `runtime-tier:${cfg.date}:${tenantId}`,
|
|
144
|
+
});
|
|
154
145
|
}
|
|
155
146
|
}
|
|
156
147
|
}
|
|
@@ -190,23 +181,17 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
190
181
|
if (!storageCost.isZero()) {
|
|
191
182
|
const currentBalance = await cfg.ledger.balance(tenantId);
|
|
192
183
|
if (!currentBalance.lessThan(storageCost)) {
|
|
193
|
-
await cfg.ledger.debit(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"Daily storage tier surcharge",
|
|
198
|
-
`runtime-storage:${cfg.date}:${tenantId}`,
|
|
199
|
-
);
|
|
184
|
+
await cfg.ledger.debit(tenantId, storageCost, "storage_upgrade", {
|
|
185
|
+
description: "Daily storage tier surcharge",
|
|
186
|
+
referenceId: `runtime-storage:${cfg.date}:${tenantId}`,
|
|
187
|
+
});
|
|
200
188
|
} else {
|
|
201
189
|
// Partial debit — take what's left, then suspend
|
|
202
190
|
if (currentBalance.greaterThan(Credit.ZERO)) {
|
|
203
|
-
await cfg.ledger.debit(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"Partial storage tier surcharge (balance exhausted)",
|
|
208
|
-
`runtime-storage:${cfg.date}:${tenantId}`,
|
|
209
|
-
);
|
|
191
|
+
await cfg.ledger.debit(tenantId, currentBalance, "storage_upgrade", {
|
|
192
|
+
description: "Partial storage tier surcharge (balance exhausted)",
|
|
193
|
+
referenceId: `runtime-storage:${cfg.date}:${tenantId}`,
|
|
194
|
+
});
|
|
210
195
|
}
|
|
211
196
|
if (!result.suspended.includes(tenantId)) {
|
|
212
197
|
result.suspended.push(tenantId);
|
|
@@ -222,23 +207,17 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
222
207
|
if (!addonCost.isZero()) {
|
|
223
208
|
const currentBalance = await cfg.ledger.balance(tenantId);
|
|
224
209
|
if (!currentBalance.lessThan(addonCost)) {
|
|
225
|
-
await cfg.ledger.debit(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
"Daily infrastructure add-on charges",
|
|
230
|
-
`runtime-addon:${cfg.date}:${tenantId}`,
|
|
231
|
-
);
|
|
210
|
+
await cfg.ledger.debit(tenantId, addonCost, "addon", {
|
|
211
|
+
description: "Daily infrastructure add-on charges",
|
|
212
|
+
referenceId: `runtime-addon:${cfg.date}:${tenantId}`,
|
|
213
|
+
});
|
|
232
214
|
} else {
|
|
233
215
|
// Partial debit — take what's left, then suspend
|
|
234
216
|
if (currentBalance.greaterThan(Credit.ZERO)) {
|
|
235
|
-
await cfg.ledger.debit(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
"Partial add-on charges (balance exhausted)",
|
|
240
|
-
`runtime-addon:${cfg.date}:${tenantId}`,
|
|
241
|
-
);
|
|
217
|
+
await cfg.ledger.debit(tenantId, currentBalance, "addon", {
|
|
218
|
+
description: "Partial add-on charges (balance exhausted)",
|
|
219
|
+
referenceId: `runtime-addon:${cfg.date}:${tenantId}`,
|
|
220
|
+
});
|
|
242
221
|
}
|
|
243
222
|
if (!result.suspended.includes(tenantId)) {
|
|
244
223
|
result.suspended.push(tenantId);
|
|
@@ -250,13 +229,10 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
250
229
|
} else {
|
|
251
230
|
// Partial deduction — debit remaining balance, then suspend
|
|
252
231
|
if (balance.greaterThan(Credit.ZERO)) {
|
|
253
|
-
await cfg.ledger.debit(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
`Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
|
|
258
|
-
runtimeRef,
|
|
259
|
-
);
|
|
232
|
+
await cfg.ledger.debit(tenantId, balance, "bot_runtime", {
|
|
233
|
+
description: `Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
|
|
234
|
+
referenceId: runtimeRef,
|
|
235
|
+
});
|
|
260
236
|
}
|
|
261
237
|
|
|
262
238
|
if (cfg.onCreditsExhausted) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { RUNTIME_INTERVAL_MS, startRuntimeScheduler } from "./runtime-scheduler.js";
|
|
3
3
|
|
|
4
|
-
// Minimal
|
|
4
|
+
// Minimal ILedger stub — only the methods runRuntimeDeductions calls.
|
|
5
5
|
function makeLedger() {
|
|
6
6
|
return {
|
|
7
7
|
tenantsWithBalance: vi.fn().mockResolvedValue([]),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { logger } from "../../config/logger.js";
|
|
3
3
|
import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
|
|
4
4
|
import { buildAddonCosts } from "../addons/addon-cron.js";
|
|
@@ -6,7 +6,7 @@ import type { ITenantAddonRepository } from "../addons/addon-repository.js";
|
|
|
6
6
|
import { buildResourceTierCosts, runRuntimeDeductions } from "./runtime-cron.js";
|
|
7
7
|
|
|
8
8
|
export interface RuntimeSchedulerDeps {
|
|
9
|
-
ledger:
|
|
9
|
+
ledger: ILedger;
|
|
10
10
|
botInstanceRepo: IBotInstanceRepository;
|
|
11
11
|
tenantAddonRepo: ITenantAddonRepository;
|
|
12
12
|
onSuspend?: (tenantId: string) => void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
import {
|
|
2
|
+
import { DrizzleLedger, grantSignupCredits, SIGNUP_GRANT } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import type { DrizzleDb } from "../../db/index.js";
|
|
5
5
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
@@ -7,7 +7,7 @@ import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
|
7
7
|
describe("grantSignupCredits", () => {
|
|
8
8
|
let pool: PGlite;
|
|
9
9
|
let db: DrizzleDb;
|
|
10
|
-
let ledger:
|
|
10
|
+
let ledger: DrizzleLedger;
|
|
11
11
|
|
|
12
12
|
beforeAll(async () => {
|
|
13
13
|
({ db, pool } = await createTestDb());
|
|
@@ -19,7 +19,9 @@ describe("grantSignupCredits", () => {
|
|
|
19
19
|
|
|
20
20
|
beforeEach(async () => {
|
|
21
21
|
await truncateAllTables(pool);
|
|
22
|
-
ledger = new
|
|
22
|
+
ledger = new DrizzleLedger(db);
|
|
23
|
+
|
|
24
|
+
await ledger.seedSystemAccounts();
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
it("grants credits to a new tenant and returns true", async () => {
|
|
@@ -52,7 +54,8 @@ describe("grantSignupCredits", () => {
|
|
|
52
54
|
const uniqueErr = Object.assign(new Error("duplicate key value violates unique constraint"), {
|
|
53
55
|
code: "23505",
|
|
54
56
|
});
|
|
55
|
-
const racingLedger = new
|
|
57
|
+
const racingLedger = new DrizzleLedger(db);
|
|
58
|
+
await racingLedger.seedSystemAccounts();
|
|
56
59
|
vi.spyOn(racingLedger, "hasReferenceId").mockResolvedValue(false);
|
|
57
60
|
vi.spyOn(racingLedger, "credit").mockRejectedValue(uniqueErr);
|
|
58
61
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
import { Credit,
|
|
2
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import type { DrizzleDb } from "../../db/index.js";
|
|
5
5
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
@@ -9,7 +9,7 @@ describe("runtime cron with storage tiers", () => {
|
|
|
9
9
|
const TODAY = "2025-01-01";
|
|
10
10
|
let pool: PGlite;
|
|
11
11
|
let db: DrizzleDb;
|
|
12
|
-
let ledger:
|
|
12
|
+
let ledger: DrizzleLedger;
|
|
13
13
|
|
|
14
14
|
beforeAll(async () => {
|
|
15
15
|
({ db, pool } = await createTestDb());
|
|
@@ -21,7 +21,9 @@ describe("runtime cron with storage tiers", () => {
|
|
|
21
21
|
|
|
22
22
|
beforeEach(async () => {
|
|
23
23
|
await truncateAllTables(pool);
|
|
24
|
-
ledger = new
|
|
24
|
+
ledger = new DrizzleLedger(db);
|
|
25
|
+
|
|
26
|
+
await ledger.seedSystemAccounts();
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
it("debits base cost plus storage surcharge for pro tier", async () => {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import { Credit, DrizzleLedger, runTrialBalanceCron } from "@wopr-network/platform-core/credits";
|
|
3
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
5
|
+
|
|
6
|
+
describe("runTrialBalanceCron", () => {
|
|
7
|
+
let pool: PGlite;
|
|
8
|
+
let ledger: DrizzleLedger;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
const { db, pool: p } = await createTestDb();
|
|
12
|
+
pool = p;
|
|
13
|
+
ledger = new DrizzleLedger(db);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
await pool.close();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
await truncateAllTables(pool);
|
|
22
|
+
await ledger.seedSystemAccounts();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns balanced when no entries exist", async () => {
|
|
26
|
+
const result = await runTrialBalanceCron({ ledger });
|
|
27
|
+
expect(result.balanced).toBe(true);
|
|
28
|
+
expect(result.differenceRaw).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns balanced after normal credit and debit", async () => {
|
|
32
|
+
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
33
|
+
await ledger.debit("t1", Credit.fromCents(200), "bot_runtime");
|
|
34
|
+
|
|
35
|
+
const result = await runTrialBalanceCron({ ledger });
|
|
36
|
+
expect(result.balanced).toBe(true);
|
|
37
|
+
expect(result.differenceRaw).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("logs an error on imbalance without throwing", async () => {
|
|
41
|
+
vi.spyOn(ledger, "trialBalance").mockResolvedValueOnce({
|
|
42
|
+
totalDebits: Credit.fromCents(1000),
|
|
43
|
+
totalCredits: Credit.fromCents(900),
|
|
44
|
+
balanced: false,
|
|
45
|
+
difference: Credit.fromCents(100),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const result = await runTrialBalanceCron({ ledger });
|
|
49
|
+
expect(result.balanced).toBe(false);
|
|
50
|
+
expect(result.differenceRaw).toBe(Credit.fromCents(100).toRaw());
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { Credit } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import type { Context, Next } from "hono";
|
|
4
4
|
import { DAILY_BOT_COST } from "./credits/runtime-cron.js";
|
|
@@ -74,7 +74,7 @@ export function createFeatureGate(cfg: FeatureGateConfig) {
|
|
|
74
74
|
/**
|
|
75
75
|
* Convenience factory that creates a requireBalance middleware from a CreditLedger instance.
|
|
76
76
|
*/
|
|
77
|
-
export function createBalanceGate(ledger:
|
|
77
|
+
export function createBalanceGate(ledger: ILedger, userKey?: string, userIdField?: string) {
|
|
78
78
|
return createFeatureGate({
|
|
79
79
|
getUserBalance: (tenantId) => ledger.balance(tenantId),
|
|
80
80
|
userKey,
|
|
@@ -94,7 +94,7 @@ export type ResolveTenantId = (c: Context) => string | undefined | Promise<strin
|
|
|
94
94
|
|
|
95
95
|
export interface CreditGateConfig {
|
|
96
96
|
/** CreditLedger instance used to check balance. */
|
|
97
|
-
ledger:
|
|
97
|
+
ledger: ILedger;
|
|
98
98
|
/** Resolve the tenant ID from the request context. */
|
|
99
99
|
resolveTenantId: ResolveTenantId;
|
|
100
100
|
}
|
|
@@ -134,11 +134,12 @@ export { BudgetChecker, DrizzleBudgetChecker } from "./budget/index.js";
|
|
|
134
134
|
// Credit ledger (WOP-384)
|
|
135
135
|
export type {
|
|
136
136
|
BillingState,
|
|
137
|
-
CreditTransaction,
|
|
138
137
|
CreditType,
|
|
139
138
|
DebitType,
|
|
140
139
|
GetActiveBotCount,
|
|
141
140
|
HistoryOptions,
|
|
141
|
+
ILedger,
|
|
142
|
+
JournalEntry,
|
|
142
143
|
OnSuspend,
|
|
143
144
|
RuntimeCronConfig,
|
|
144
145
|
RuntimeCronResult,
|
|
@@ -147,12 +148,12 @@ export type {
|
|
|
147
148
|
export {
|
|
148
149
|
BotBilling,
|
|
149
150
|
buildResourceTierCosts,
|
|
150
|
-
CreditLedger,
|
|
151
151
|
DAILY_BOT_COST,
|
|
152
152
|
DrizzleBotBilling,
|
|
153
|
-
|
|
153
|
+
DrizzleLedger,
|
|
154
154
|
grantSignupCredits,
|
|
155
155
|
InsufficientBalanceError,
|
|
156
|
+
Ledger,
|
|
156
157
|
runRuntimeDeductions,
|
|
157
158
|
SIGNUP_GRANT,
|
|
158
159
|
SUSPENSION_GRACE_DAYS,
|
|
@@ -215,7 +216,6 @@ export {
|
|
|
215
216
|
export type {
|
|
216
217
|
IBotBilling,
|
|
217
218
|
IBudgetChecker,
|
|
218
|
-
ICreditLedger,
|
|
219
219
|
IMeterAggregator,
|
|
220
220
|
IMeterEmitter,
|
|
221
221
|
IPayRamChargeRepository,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import type { PGlite } from "@electric-sql/pglite";
|
|
3
|
-
import { Credit,
|
|
3
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
4
4
|
import { runReconciliation } from "@wopr-network/platform-core/metering";
|
|
5
5
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import type { DrizzleDb } from "../../db/index.js";
|
|
@@ -17,7 +17,7 @@ const DAY_END = DAY_START + 24 * 60 * 60 * 1000;
|
|
|
17
17
|
describe("runReconciliation", () => {
|
|
18
18
|
let pool: PGlite;
|
|
19
19
|
let db: DrizzleDb;
|
|
20
|
-
let ledger:
|
|
20
|
+
let ledger: DrizzleLedger;
|
|
21
21
|
let usageSummaryRepo: DrizzleUsageSummaryRepository;
|
|
22
22
|
let adapterUsageRepo: DrizzleAdapterUsageRepository;
|
|
23
23
|
|
|
@@ -25,7 +25,7 @@ describe("runReconciliation", () => {
|
|
|
25
25
|
const t = await createTestDb();
|
|
26
26
|
pool = t.pool;
|
|
27
27
|
db = t.db;
|
|
28
|
-
ledger = new
|
|
28
|
+
ledger = new DrizzleLedger(db);
|
|
29
29
|
usageSummaryRepo = new DrizzleUsageSummaryRepository(db);
|
|
30
30
|
adapterUsageRepo = new DrizzleAdapterUsageRepository(db);
|
|
31
31
|
});
|
|
@@ -36,6 +36,7 @@ describe("runReconciliation", () => {
|
|
|
36
36
|
|
|
37
37
|
beforeEach(async () => {
|
|
38
38
|
await truncateAllTables(pool);
|
|
39
|
+
await ledger.seedSystemAccounts();
|
|
39
40
|
});
|
|
40
41
|
|
|
41
42
|
/** Insert a usage_summaries row directly. */
|
|
@@ -74,7 +75,7 @@ describe("runReconciliation", () => {
|
|
|
74
75
|
await insertSummary({ tenant: "t1", totalCharge: charge.toRaw() });
|
|
75
76
|
|
|
76
77
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
77
|
-
await ledger.debit("t1", charge, "adapter_usage", "chat usage");
|
|
78
|
+
await ledger.debit("t1", charge, "adapter_usage", { description: "chat usage" });
|
|
78
79
|
|
|
79
80
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
80
81
|
expect(result.tenantsChecked).toBe(1);
|
|
@@ -85,7 +86,7 @@ describe("runReconciliation", () => {
|
|
|
85
86
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(100).toRaw() });
|
|
86
87
|
|
|
87
88
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
88
|
-
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
|
|
89
|
+
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
|
|
89
90
|
|
|
90
91
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
91
92
|
expect(result.tenantsChecked).toBe(1);
|
|
@@ -117,7 +118,7 @@ describe("runReconciliation", () => {
|
|
|
117
118
|
|
|
118
119
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
119
120
|
// Debit as bot_runtime — should NOT count toward reconciliation
|
|
120
|
-
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "daily runtime");
|
|
121
|
+
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "daily runtime" });
|
|
121
122
|
|
|
122
123
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
123
124
|
// Metered 20c, ledger adapter_usage = 0 => drift = 20c
|
|
@@ -149,12 +150,12 @@ describe("runReconciliation", () => {
|
|
|
149
150
|
// t1: balanced
|
|
150
151
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
|
|
151
152
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
152
|
-
await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", "chat");
|
|
153
|
+
await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", { description: "chat" });
|
|
153
154
|
|
|
154
155
|
// t2: drifted
|
|
155
156
|
await insertSummary({ tenant: "t2", totalCharge: Credit.fromCents(100).toRaw() });
|
|
156
157
|
await ledger.credit("t2", Credit.fromCents(500), "purchase");
|
|
157
|
-
await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", "chat");
|
|
158
|
+
await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", { description: "chat" });
|
|
158
159
|
|
|
159
160
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
160
161
|
expect(result.tenantsChecked).toBe(2);
|
|
@@ -192,7 +193,7 @@ describe("runReconciliation", () => {
|
|
|
192
193
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
|
|
193
194
|
|
|
194
195
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
195
|
-
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
|
|
196
|
+
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
|
|
196
197
|
|
|
197
198
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
198
199
|
expect(result.discrepancies).toHaveLength(1);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import type { PGlite } from "@electric-sql/pglite";
|
|
3
|
-
import { Credit,
|
|
3
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
4
4
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
5
5
|
import type { DrizzleDb } from "../../db/index.js";
|
|
6
6
|
import { createTestDb, seedUsageSummary, truncateAllTables } from "../../test/db.js";
|
|
@@ -125,12 +125,14 @@ describe("DrizzleUsageSummaryRepository", () => {
|
|
|
125
125
|
|
|
126
126
|
describe("DrizzleAdapterUsageRepository", () => {
|
|
127
127
|
let repo: DrizzleAdapterUsageRepository;
|
|
128
|
-
let ledger:
|
|
128
|
+
let ledger: DrizzleLedger;
|
|
129
129
|
|
|
130
130
|
beforeEach(async () => {
|
|
131
131
|
await truncateAllTables(pool);
|
|
132
132
|
repo = new DrizzleAdapterUsageRepository(db);
|
|
133
|
-
ledger = new
|
|
133
|
+
ledger = new DrizzleLedger(db);
|
|
134
|
+
|
|
135
|
+
await ledger.seedSystemAccounts();
|
|
134
136
|
});
|
|
135
137
|
|
|
136
138
|
it("returns empty array when no adapter_usage debits exist", async () => {
|
|
@@ -146,9 +148,9 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
146
148
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
147
149
|
await ledger.credit("t2", Credit.fromCents(1000), "purchase");
|
|
148
150
|
|
|
149
|
-
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "t1-debit-1");
|
|
150
|
-
await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", "t1-debit-2");
|
|
151
|
-
await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", "t2-debit-1");
|
|
151
|
+
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "t1-debit-1" });
|
|
152
|
+
await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", { description: "t1-debit-2" });
|
|
153
|
+
await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", { description: "t2-debit-1" });
|
|
152
154
|
|
|
153
155
|
// Query window covering today
|
|
154
156
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -167,8 +169,8 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
167
169
|
|
|
168
170
|
it("excludes non-adapter_usage debit types", async () => {
|
|
169
171
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
170
|
-
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "adapter debit");
|
|
171
|
-
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "runtime debit");
|
|
172
|
+
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "adapter debit" });
|
|
173
|
+
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "runtime debit" });
|
|
172
174
|
|
|
173
175
|
const today = new Date().toISOString().slice(0, 10);
|
|
174
176
|
const startIso = `${today}T00:00:00Z`;
|
|
@@ -181,7 +183,7 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
181
183
|
|
|
182
184
|
it("excludes credit transactions (positive amounts are not debits)", async () => {
|
|
183
185
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
184
|
-
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "real debit");
|
|
186
|
+
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "real debit" });
|
|
185
187
|
|
|
186
188
|
const today = new Date().toISOString().slice(0, 10);
|
|
187
189
|
const startIso = `${today}T00:00:00Z`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { and, eq, gte, lt, ne, sql } from "drizzle-orm";
|
|
2
2
|
import type { DrizzleDb } from "../../db/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { journalEntries, journalLines } from "../../db/schema/ledger.js";
|
|
4
4
|
import { usageSummaries } from "../../db/schema/meter-events.js";
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
@@ -59,24 +59,26 @@ export class DrizzleAdapterUsageRepository implements IAdapterUsageRepository {
|
|
|
59
59
|
constructor(private readonly db: DrizzleDb) {}
|
|
60
60
|
|
|
61
61
|
async getAggregatedAdapterUsageDebits(startIso: string, endIso: string): Promise<AggregatedDebit[]> {
|
|
62
|
+
// Sum the debit-side journal line amounts for adapter_usage entries.
|
|
63
|
+
// In double-entry: DR tenant liability (2000:<tenantId>), CR revenue:adapter_usage (4010).
|
|
62
64
|
const rows = await this.db
|
|
63
65
|
.select({
|
|
64
|
-
tenantId:
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
// raw SQL: Drizzle cannot express ABS with COALESCE and SUM
|
|
68
|
-
totalDebitRaw: sql<number>`COALESCE(SUM(ABS(amount_credits)), 0)`,
|
|
66
|
+
tenantId: journalEntries.tenantId,
|
|
67
|
+
// raw SQL: Drizzle cannot express COALESCE with SUM aggregation
|
|
68
|
+
totalDebitRaw: sql<number>`COALESCE(SUM(${journalLines.amount}), 0)`,
|
|
69
69
|
})
|
|
70
|
-
.from(
|
|
70
|
+
.from(journalLines)
|
|
71
|
+
.innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
|
|
71
72
|
.where(
|
|
72
73
|
and(
|
|
73
|
-
eq(
|
|
74
|
+
eq(journalEntries.entryType, "adapter_usage"),
|
|
75
|
+
eq(journalLines.side, "debit"),
|
|
74
76
|
// raw SQL: Drizzle cannot express timestamptz cast for text column date comparison
|
|
75
|
-
sql`${
|
|
76
|
-
sql`${
|
|
77
|
+
sql`${journalEntries.postedAt}::timestamptz >= ${startIso}::timestamptz`,
|
|
78
|
+
sql`${journalEntries.postedAt}::timestamptz < ${endIso}::timestamptz`,
|
|
77
79
|
),
|
|
78
80
|
)
|
|
79
|
-
.groupBy(
|
|
81
|
+
.groupBy(journalEntries.tenantId);
|
|
80
82
|
|
|
81
83
|
return rows.map((r) => ({ tenantId: r.tenantId, totalDebitRaw: Number(r.totalDebitRaw) }));
|
|
82
84
|
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
noOpReplayGuard,
|
|
13
13
|
PayRamChargeRepository,
|
|
14
14
|
} from "@wopr-network/platform-core/billing";
|
|
15
|
-
import {
|
|
15
|
+
import { DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
16
16
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
17
17
|
import type { DrizzleDb } from "../../db/index.js";
|
|
18
18
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
@@ -44,13 +44,15 @@ afterAll(async () => {
|
|
|
44
44
|
|
|
45
45
|
describe("handlePayRamWebhook", () => {
|
|
46
46
|
let chargeStore: PayRamChargeRepository;
|
|
47
|
-
let creditLedger:
|
|
47
|
+
let creditLedger: DrizzleLedger;
|
|
48
48
|
let deps: PayRamWebhookDeps;
|
|
49
49
|
|
|
50
50
|
beforeEach(async () => {
|
|
51
51
|
await truncateAllTables(pool);
|
|
52
52
|
chargeStore = new PayRamChargeRepository(db);
|
|
53
|
-
creditLedger = new
|
|
53
|
+
creditLedger = new DrizzleLedger(db);
|
|
54
|
+
|
|
55
|
+
await creditLedger.seedSystemAccounts();
|
|
54
56
|
deps = { chargeStore, creditLedger, replayGuard: noOpReplayGuard };
|
|
55
57
|
|
|
56
58
|
// Create a default test charge
|
|
@@ -80,14 +82,14 @@ describe("handlePayRamWebhook", () => {
|
|
|
80
82
|
const history = await creditLedger.history("tenant-a");
|
|
81
83
|
expect(history).toHaveLength(1);
|
|
82
84
|
expect(history[0].referenceId).toBe("payram:ref-test-001");
|
|
83
|
-
expect(history[0].
|
|
85
|
+
expect(history[0].entryType).toBe("purchase");
|
|
84
86
|
});
|
|
85
87
|
|
|
86
88
|
it("records fundingSource as payram", async () => {
|
|
87
89
|
await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
|
|
88
90
|
|
|
89
91
|
const history = await creditLedger.history("tenant-a");
|
|
90
|
-
expect(history[0].fundingSource).toBe("payram");
|
|
92
|
+
expect(history[0].metadata?.fundingSource).toBe("payram");
|
|
91
93
|
});
|
|
92
94
|
|
|
93
95
|
it("marks the charge as credited after FILLED", async () => {
|
|
@@ -4,13 +4,13 @@ import type {
|
|
|
4
4
|
PayRamWebhookPayload,
|
|
5
5
|
PayRamWebhookResult,
|
|
6
6
|
} from "@wopr-network/platform-core/billing";
|
|
7
|
-
import type {
|
|
7
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
8
8
|
import { Credit } from "@wopr-network/platform-core/credits";
|
|
9
9
|
import type { BotBilling } from "../credits/bot-billing.js";
|
|
10
10
|
|
|
11
11
|
export interface PayRamWebhookDeps {
|
|
12
12
|
chargeStore: PayRamChargeRepository;
|
|
13
|
-
creditLedger:
|
|
13
|
+
creditLedger: ILedger;
|
|
14
14
|
botBilling?: BotBilling;
|
|
15
15
|
replayGuard: IWebhookSeenRepository;
|
|
16
16
|
}
|
|
@@ -60,14 +60,11 @@ export async function handlePayRamWebhook(
|
|
|
60
60
|
// overpayment stays in the PayRam wallet as a buffer.
|
|
61
61
|
const creditCents = charge.amountUsdCents;
|
|
62
62
|
|
|
63
|
-
await creditLedger.credit(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
`payram:${payload.reference_id}`,
|
|
69
|
-
"payram",
|
|
70
|
-
);
|
|
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
|
+
});
|
|
71
68
|
|
|
72
69
|
await chargeStore.markCredited(payload.reference_id);
|
|
73
70
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { Credit } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import type { ICouponRepository } from "./coupon-repository.js";
|
|
@@ -40,7 +40,7 @@ describe("PromotionEngine", () => {
|
|
|
40
40
|
let promotionRepo: IPromotionRepository;
|
|
41
41
|
let couponRepo: ICouponRepository;
|
|
42
42
|
let redemptionRepo: IRedemptionRepository;
|
|
43
|
-
let ledger:
|
|
43
|
+
let ledger: ILedger;
|
|
44
44
|
let engine: PromotionEngine;
|
|
45
45
|
|
|
46
46
|
beforeEach(() => {
|
|
@@ -95,7 +95,7 @@ describe("PromotionEngine", () => {
|
|
|
95
95
|
createdAt: new Date().toISOString(),
|
|
96
96
|
}),
|
|
97
97
|
hasReferenceId: vi.fn().mockResolvedValue(false),
|
|
98
|
-
} as unknown as
|
|
98
|
+
} as unknown as ILedger;
|
|
99
99
|
|
|
100
100
|
engine = new PromotionEngine({ promotionRepo, couponRepo, redemptionRepo, ledger });
|
|
101
101
|
});
|
|
@@ -111,8 +111,9 @@ describe("PromotionEngine", () => {
|
|
|
111
111
|
"tenant-1",
|
|
112
112
|
expect.any(Object), // Credit instance
|
|
113
113
|
"promo",
|
|
114
|
-
expect.
|
|
115
|
-
|
|
114
|
+
expect.objectContaining({
|
|
115
|
+
referenceId: "promo:promo-1:tenant-1:1",
|
|
116
|
+
}),
|
|
116
117
|
);
|
|
117
118
|
});
|
|
118
119
|
|