@wopr-network/platform-core 1.13.3 → 1.14.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/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/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/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,5 +1,5 @@
|
|
|
1
1
|
import type { ITenantCustomerRepository } from "@wopr-network/platform-core/billing";
|
|
2
|
-
import type { Credit,
|
|
2
|
+
import type { Credit, ILedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import Stripe from "stripe";
|
|
4
4
|
import { logger } from "../../config/logger.js";
|
|
5
5
|
import type { IAutoTopupEventLogRepository } from "./auto-topup-event-log-repository.js";
|
|
@@ -10,7 +10,7 @@ export const MAX_CONSECUTIVE_FAILURES = 3;
|
|
|
10
10
|
export interface AutoTopupChargeDeps {
|
|
11
11
|
stripe: Stripe;
|
|
12
12
|
tenantRepo: ITenantCustomerRepository;
|
|
13
|
-
creditLedger:
|
|
13
|
+
creditLedger: ILedger;
|
|
14
14
|
eventLogRepo: IAutoTopupEventLogRepository;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -136,14 +136,11 @@ export async function chargeAutoTopup(
|
|
|
136
136
|
// 5. Credit the ledger (idempotent via referenceId = PI ID)
|
|
137
137
|
try {
|
|
138
138
|
if (!(await deps.creditLedger.hasReferenceId(paymentIntent.id))) {
|
|
139
|
-
await deps.creditLedger.credit(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"
|
|
143
|
-
|
|
144
|
-
paymentIntent.id,
|
|
145
|
-
"stripe",
|
|
146
|
-
);
|
|
139
|
+
await deps.creditLedger.credit(tenantId, amount, "purchase", {
|
|
140
|
+
description: `Auto-topup (${source})`,
|
|
141
|
+
referenceId: paymentIntent.id,
|
|
142
|
+
fundingSource: "stripe",
|
|
143
|
+
});
|
|
147
144
|
}
|
|
148
145
|
} catch (err) {
|
|
149
146
|
const message = `Stripe charge ${paymentIntent.id} succeeded but credit grant failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
import { Credit,
|
|
2
|
+
import { Credit, DrizzleAutoTopupSettingsRepository, DrizzleLedger } 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";
|
|
@@ -8,7 +8,7 @@ import { maybeTriggerUsageTopup, type UsageTopupDeps } from "./auto-topup-usage.
|
|
|
8
8
|
describe("maybeTriggerUsageTopup", () => {
|
|
9
9
|
let pool: PGlite;
|
|
10
10
|
let db: DrizzleDb;
|
|
11
|
-
let ledger:
|
|
11
|
+
let ledger: DrizzleLedger;
|
|
12
12
|
let settingsRepo: DrizzleAutoTopupSettingsRepository;
|
|
13
13
|
|
|
14
14
|
beforeAll(async () => {
|
|
@@ -21,7 +21,9 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
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
|
settingsRepo = new DrizzleAutoTopupSettingsRepository(db);
|
|
26
28
|
});
|
|
27
29
|
|
|
@@ -35,7 +37,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
35
37
|
|
|
36
38
|
it("does nothing when usage_enabled is false", async () => {
|
|
37
39
|
await settingsRepo.upsert("t1", { usageEnabled: false });
|
|
38
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
40
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
41
|
+
description: "buy",
|
|
42
|
+
referenceId: "ref-1",
|
|
43
|
+
fundingSource: "stripe",
|
|
44
|
+
});
|
|
39
45
|
const mockCharge = vi.fn();
|
|
40
46
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
41
47
|
|
|
@@ -49,7 +55,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
49
55
|
usageThreshold: Credit.fromCents(100),
|
|
50
56
|
usageTopup: Credit.fromCents(500),
|
|
51
57
|
});
|
|
52
|
-
await ledger.credit("t1", Credit.fromCents(200), "purchase",
|
|
58
|
+
await ledger.credit("t1", Credit.fromCents(200), "purchase", {
|
|
59
|
+
description: "buy",
|
|
60
|
+
referenceId: "ref-1",
|
|
61
|
+
fundingSource: "stripe",
|
|
62
|
+
});
|
|
53
63
|
const mockCharge = vi.fn();
|
|
54
64
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
55
65
|
|
|
@@ -63,7 +73,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
63
73
|
usageThreshold: Credit.fromCents(100),
|
|
64
74
|
usageTopup: Credit.fromCents(500),
|
|
65
75
|
});
|
|
66
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
76
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
77
|
+
description: "buy",
|
|
78
|
+
referenceId: "ref-1",
|
|
79
|
+
fundingSource: "stripe",
|
|
80
|
+
});
|
|
67
81
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
|
|
68
82
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
69
83
|
|
|
@@ -74,7 +88,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
74
88
|
it("skips when charge is already in-flight", async () => {
|
|
75
89
|
await settingsRepo.upsert("t1", { usageEnabled: true, usageThreshold: Credit.fromCents(100) });
|
|
76
90
|
await settingsRepo.setUsageChargeInFlight("t1", true);
|
|
77
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
91
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
92
|
+
description: "buy",
|
|
93
|
+
referenceId: "ref-1",
|
|
94
|
+
fundingSource: "stripe",
|
|
95
|
+
});
|
|
78
96
|
const mockCharge = vi.fn();
|
|
79
97
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
80
98
|
|
|
@@ -89,7 +107,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
89
107
|
usageThreshold: Credit.fromCents(500),
|
|
90
108
|
usageTopup: Credit.fromCents(2000),
|
|
91
109
|
});
|
|
92
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase",
|
|
110
|
+
await ledger.credit("t1", Credit.fromCents(100), "purchase", {
|
|
111
|
+
description: "buy",
|
|
112
|
+
referenceId: "ref-1",
|
|
113
|
+
fundingSource: "stripe",
|
|
114
|
+
});
|
|
93
115
|
|
|
94
116
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_race" });
|
|
95
117
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
@@ -113,7 +135,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
113
135
|
usageThreshold: Credit.fromCents(100),
|
|
114
136
|
usageTopup: Credit.fromCents(500),
|
|
115
137
|
});
|
|
116
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
138
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
139
|
+
description: "buy",
|
|
140
|
+
referenceId: "ref-1",
|
|
141
|
+
fundingSource: "stripe",
|
|
142
|
+
});
|
|
117
143
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
|
|
118
144
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
119
145
|
|
|
@@ -135,7 +161,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
135
161
|
usageThreshold: Credit.fromCents(100),
|
|
136
162
|
usageTopup: Credit.fromCents(500),
|
|
137
163
|
});
|
|
138
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
164
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
165
|
+
description: "buy",
|
|
166
|
+
referenceId: "ref-1",
|
|
167
|
+
fundingSource: "stripe",
|
|
168
|
+
});
|
|
139
169
|
const mockCharge = vi
|
|
140
170
|
.fn()
|
|
141
171
|
.mockRejectedValueOnce(new Error("Stripe network error"))
|
|
@@ -162,7 +192,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
162
192
|
});
|
|
163
193
|
await settingsRepo.incrementUsageFailures("t1");
|
|
164
194
|
await settingsRepo.incrementUsageFailures("t1");
|
|
165
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
195
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
196
|
+
description: "buy",
|
|
197
|
+
referenceId: "ref-1",
|
|
198
|
+
fundingSource: "stripe",
|
|
199
|
+
});
|
|
166
200
|
const mockCharge = vi.fn().mockResolvedValue({ success: true });
|
|
167
201
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
168
202
|
|
|
@@ -176,7 +210,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
176
210
|
usageThreshold: Credit.fromCents(100),
|
|
177
211
|
usageTopup: Credit.fromCents(500),
|
|
178
212
|
});
|
|
179
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
213
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
214
|
+
description: "buy",
|
|
215
|
+
referenceId: "ref-1",
|
|
216
|
+
fundingSource: "stripe",
|
|
217
|
+
});
|
|
180
218
|
const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
|
|
181
219
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
182
220
|
|
|
@@ -208,7 +246,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
208
246
|
});
|
|
209
247
|
await settingsRepo.incrementUsageFailures("t1");
|
|
210
248
|
await settingsRepo.incrementUsageFailures("t1");
|
|
211
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
249
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
250
|
+
description: "buy",
|
|
251
|
+
referenceId: "ref-1",
|
|
252
|
+
fundingSource: "stripe",
|
|
253
|
+
});
|
|
212
254
|
const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
|
|
213
255
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
214
256
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { Credit, IAutoTopupSettingsRepository,
|
|
1
|
+
import type { Credit, IAutoTopupSettingsRepository, ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { logger } from "../../config/logger.js";
|
|
3
3
|
import type { AutoTopupChargeResult } from "./auto-topup-charge.js";
|
|
4
4
|
import { MAX_CONSECUTIVE_FAILURES } from "./auto-topup-charge.js";
|
|
5
5
|
|
|
6
6
|
export interface UsageTopupDeps {
|
|
7
7
|
settingsRepo: IAutoTopupSettingsRepository;
|
|
8
|
-
creditLedger:
|
|
8
|
+
creditLedger: ILedger;
|
|
9
9
|
/** Injected charge function (allows mocking in tests). */
|
|
10
10
|
chargeAutoTopup: (tenantId: string, amount: Credit, source: string) => Promise<AutoTopupChargeResult>;
|
|
11
11
|
/** Optional tenant status check. If provided and returns non-null, skip the charge. */
|
|
@@ -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 { sql } from "drizzle-orm";
|
|
4
4
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import type { DrizzleDb } from "../../db/index.js";
|
|
@@ -57,7 +57,7 @@ describe("BotBilling", () => {
|
|
|
57
57
|
let pool: PGlite;
|
|
58
58
|
let db: DrizzleDb;
|
|
59
59
|
let billing: BotBilling;
|
|
60
|
-
let ledger:
|
|
60
|
+
let ledger: DrizzleLedger;
|
|
61
61
|
|
|
62
62
|
beforeAll(async () => {
|
|
63
63
|
({ db, pool } = await createTestDb());
|
|
@@ -70,7 +70,9 @@ describe("BotBilling", () => {
|
|
|
70
70
|
beforeEach(async () => {
|
|
71
71
|
await truncateAllTables(pool);
|
|
72
72
|
billing = new BotBilling(new DrizzleBotInstanceRepository(db));
|
|
73
|
-
ledger = new
|
|
73
|
+
ledger = new DrizzleLedger(db);
|
|
74
|
+
|
|
75
|
+
await ledger.seedSystemAccounts();
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
describe("registerBot", () => {
|
|
@@ -211,7 +213,11 @@ describe("BotBilling", () => {
|
|
|
211
213
|
await billing.suspendBot("bot-1");
|
|
212
214
|
await billing.suspendBot("bot-2");
|
|
213
215
|
|
|
214
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase",
|
|
216
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
|
|
217
|
+
description: "test credit",
|
|
218
|
+
referenceId: "ref-1",
|
|
219
|
+
fundingSource: "stripe",
|
|
220
|
+
});
|
|
215
221
|
const reactivated = await billing.checkReactivation("tenant-1", ledger);
|
|
216
222
|
|
|
217
223
|
expect(reactivated.sort()).toEqual(["bot-1", "bot-2"]);
|
|
@@ -231,14 +237,22 @@ describe("BotBilling", () => {
|
|
|
231
237
|
await billing.registerBot("bot-1", "tenant-1", "bot-a");
|
|
232
238
|
await billing.destroyBot("bot-1");
|
|
233
239
|
|
|
234
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase",
|
|
240
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
|
|
241
|
+
description: "test credit",
|
|
242
|
+
referenceId: "ref-1",
|
|
243
|
+
fundingSource: "stripe",
|
|
244
|
+
});
|
|
235
245
|
const reactivated = await billing.checkReactivation("tenant-1", ledger);
|
|
236
246
|
|
|
237
247
|
expect(reactivated).toEqual([]);
|
|
238
248
|
});
|
|
239
249
|
|
|
240
250
|
it("returns empty array for tenant with no bots", async () => {
|
|
241
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase",
|
|
251
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
|
|
252
|
+
description: "test credit",
|
|
253
|
+
referenceId: "ref-1",
|
|
254
|
+
fundingSource: "stripe",
|
|
255
|
+
});
|
|
242
256
|
const reactivated = await billing.checkReactivation("tenant-1", ledger);
|
|
243
257
|
expect(reactivated).toEqual([]);
|
|
244
258
|
});
|
|
@@ -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 { logger } from "../../config/logger.js";
|
|
4
4
|
import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
|
|
@@ -16,7 +16,7 @@ export interface IBotBilling {
|
|
|
16
16
|
suspendBot(botId: string): Promise<void>;
|
|
17
17
|
suspendAllForTenant(tenantId: string): Promise<string[]>;
|
|
18
18
|
reactivateBot(botId: string): Promise<void>;
|
|
19
|
-
checkReactivation(tenantId: string, ledger:
|
|
19
|
+
checkReactivation(tenantId: string, ledger: ILedger): Promise<string[]>;
|
|
20
20
|
destroyBot(botId: string): Promise<void>;
|
|
21
21
|
destroyExpiredBots(): Promise<string[]>;
|
|
22
22
|
getBotBilling(botId: string): Promise<unknown>;
|
|
@@ -98,7 +98,7 @@ export class DrizzleBotBilling implements IBotBilling {
|
|
|
98
98
|
*
|
|
99
99
|
* @returns IDs of reactivated bots.
|
|
100
100
|
*/
|
|
101
|
-
async checkReactivation(tenantId: string, ledger:
|
|
101
|
+
async checkReactivation(tenantId: string, ledger: ILedger): Promise<string[]> {
|
|
102
102
|
const balance = await ledger.balance(tenantId);
|
|
103
103
|
if (balance.isNegative() || balance.isZero()) return [];
|
|
104
104
|
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
import { Credit,
|
|
2
|
+
import { Credit, DrizzleLedger, runCreditExpiryCron } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
5
5
|
|
|
6
6
|
describe("runCreditExpiryCron", () => {
|
|
7
7
|
let pool: PGlite;
|
|
8
|
-
let ledger:
|
|
8
|
+
let ledger: DrizzleLedger;
|
|
9
9
|
|
|
10
10
|
beforeAll(async () => {
|
|
11
11
|
const { db, pool: p } = await createTestDb();
|
|
12
12
|
pool = p;
|
|
13
|
-
ledger = new
|
|
13
|
+
ledger = new DrizzleLedger(db);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
afterAll(async () => {
|
|
@@ -19,6 +19,7 @@ describe("runCreditExpiryCron", () => {
|
|
|
19
19
|
|
|
20
20
|
beforeEach(async () => {
|
|
21
21
|
await truncateAllTables(pool);
|
|
22
|
+
await ledger.seedSystemAccounts();
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
// All tests pass an explicit `now` parameter — hardcoded dates are time-independent
|
|
@@ -31,16 +32,11 @@ describe("runCreditExpiryCron", () => {
|
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
it("debits expired promotional credit grant", async () => {
|
|
34
|
-
await ledger.credit(
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
"promo:tenant-1",
|
|
40
|
-
undefined,
|
|
41
|
-
undefined,
|
|
42
|
-
"2026-01-10T00:00:00Z",
|
|
43
|
-
);
|
|
35
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
36
|
+
description: "New user bonus",
|
|
37
|
+
referenceId: "promo:tenant-1",
|
|
38
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
39
|
+
});
|
|
44
40
|
|
|
45
41
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
46
42
|
expect(result.processed).toBe(1);
|
|
@@ -51,16 +47,11 @@ describe("runCreditExpiryCron", () => {
|
|
|
51
47
|
});
|
|
52
48
|
|
|
53
49
|
it("does not debit non-expired credits", async () => {
|
|
54
|
-
await ledger.credit(
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
"promo:tenant-1-future",
|
|
60
|
-
undefined,
|
|
61
|
-
undefined,
|
|
62
|
-
"2026-02-01T00:00:00Z",
|
|
63
|
-
);
|
|
50
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
51
|
+
description: "Future bonus",
|
|
52
|
+
referenceId: "promo:tenant-1-future",
|
|
53
|
+
expiresAt: "2026-02-01T00:00:00Z",
|
|
54
|
+
});
|
|
64
55
|
|
|
65
56
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
66
57
|
expect(result.processed).toBe(0);
|
|
@@ -70,7 +61,7 @@ describe("runCreditExpiryCron", () => {
|
|
|
70
61
|
});
|
|
71
62
|
|
|
72
63
|
it("does not debit credits without expires_at", async () => {
|
|
73
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "Top-up");
|
|
64
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "Top-up" });
|
|
74
65
|
|
|
75
66
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
76
67
|
expect(result.processed).toBe(0);
|
|
@@ -80,17 +71,12 @@ describe("runCreditExpiryCron", () => {
|
|
|
80
71
|
});
|
|
81
72
|
|
|
82
73
|
it("only debits up to available balance when partially consumed", async () => {
|
|
83
|
-
await ledger.credit(
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
undefined,
|
|
90
|
-
undefined,
|
|
91
|
-
"2026-01-10T00:00:00Z",
|
|
92
|
-
);
|
|
93
|
-
await ledger.debit("tenant-1", Credit.fromCents(300), "bot_runtime", "Runtime");
|
|
74
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
75
|
+
description: "Promo",
|
|
76
|
+
referenceId: "promo:partial",
|
|
77
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
78
|
+
});
|
|
79
|
+
await ledger.debit("tenant-1", Credit.fromCents(300), "bot_runtime", { description: "Runtime" });
|
|
94
80
|
|
|
95
81
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
96
82
|
expect(result.processed).toBe(1);
|
|
@@ -100,16 +86,11 @@ describe("runCreditExpiryCron", () => {
|
|
|
100
86
|
});
|
|
101
87
|
|
|
102
88
|
it("is idempotent -- does not double-debit on second run", async () => {
|
|
103
|
-
await ledger.credit(
|
|
104
|
-
"
|
|
105
|
-
|
|
106
|
-
"
|
|
107
|
-
|
|
108
|
-
"promo:idemp",
|
|
109
|
-
undefined,
|
|
110
|
-
undefined,
|
|
111
|
-
"2026-01-10T00:00:00Z",
|
|
112
|
-
);
|
|
89
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
90
|
+
description: "Promo",
|
|
91
|
+
referenceId: "promo:idemp",
|
|
92
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
93
|
+
});
|
|
113
94
|
|
|
114
95
|
await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
115
96
|
const balanceAfterFirst = await ledger.balance("tenant-1");
|
|
@@ -1,43 +1,34 @@
|
|
|
1
1
|
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
import { Credit,
|
|
2
|
+
import { CREDIT_TYPE_ACCOUNT, Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
-
import type {
|
|
5
|
-
import { creditBalances, creditTransactions } from "../../db/schema/credits.js";
|
|
4
|
+
import type { PlatformDb } from "../../db/index.js";
|
|
6
5
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
7
|
-
import { DrizzleCreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
8
6
|
import { type DividendCronConfig, runDividendCron } from "./dividend-cron.js";
|
|
9
7
|
|
|
10
|
-
async function insertPurchase(
|
|
11
|
-
|
|
8
|
+
async function insertPurchase(
|
|
9
|
+
ledger: DrizzleLedger,
|
|
10
|
+
tenantId: string,
|
|
11
|
+
amountCents: number,
|
|
12
|
+
postedAt: string,
|
|
13
|
+
): Promise<void> {
|
|
12
14
|
const amount = Credit.fromCents(amountCents);
|
|
13
|
-
await
|
|
14
|
-
|
|
15
|
+
await ledger.post({
|
|
16
|
+
entryType: "purchase",
|
|
15
17
|
tenantId,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
description: `Test purchase ${amountCents}¢`,
|
|
19
|
+
referenceId: `test-purchase:${tenantId}:${postedAt}:${Math.random()}`,
|
|
20
|
+
postedAt,
|
|
21
|
+
lines: [
|
|
22
|
+
{ accountCode: CREDIT_TYPE_ACCOUNT.purchase, amount, side: "debit" },
|
|
23
|
+
{ accountCode: `2000:${tenantId}`, amount, side: "credit" },
|
|
24
|
+
],
|
|
20
25
|
});
|
|
21
|
-
// Upsert credit_balances
|
|
22
|
-
const existing = await db
|
|
23
|
-
.select()
|
|
24
|
-
.from(creditBalances)
|
|
25
|
-
.where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
|
|
26
|
-
if (existing.length > 0) {
|
|
27
|
-
await db
|
|
28
|
-
.update(creditBalances)
|
|
29
|
-
.set({ balance: existing[0].balance.add(amount) })
|
|
30
|
-
.where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
|
|
31
|
-
} else {
|
|
32
|
-
await db.insert(creditBalances).values({ tenantId, balance: amount });
|
|
33
|
-
}
|
|
34
26
|
}
|
|
35
27
|
|
|
36
28
|
describe("runDividendCron", () => {
|
|
37
29
|
let pool: PGlite;
|
|
38
|
-
let db:
|
|
39
|
-
let ledger:
|
|
40
|
-
let creditTransactionRepo: DrizzleCreditTransactionRepository;
|
|
30
|
+
let db: PlatformDb;
|
|
31
|
+
let ledger: DrizzleLedger;
|
|
41
32
|
|
|
42
33
|
beforeAll(async () => {
|
|
43
34
|
({ db, pool } = await createTestDb());
|
|
@@ -49,13 +40,12 @@ describe("runDividendCron", () => {
|
|
|
49
40
|
|
|
50
41
|
beforeEach(async () => {
|
|
51
42
|
await truncateAllTables(pool);
|
|
52
|
-
ledger = new
|
|
53
|
-
|
|
43
|
+
ledger = new DrizzleLedger(db);
|
|
44
|
+
await ledger.seedSystemAccounts();
|
|
54
45
|
});
|
|
55
46
|
|
|
56
47
|
function makeConfig(overrides?: Partial<DividendCronConfig>): DividendCronConfig {
|
|
57
48
|
return {
|
|
58
|
-
creditTransactionRepo,
|
|
59
49
|
ledger,
|
|
60
50
|
matchRate: 1.0,
|
|
61
51
|
targetDate: "2026-02-20",
|
|
@@ -64,7 +54,7 @@ describe("runDividendCron", () => {
|
|
|
64
54
|
}
|
|
65
55
|
|
|
66
56
|
it("distributes dividend to eligible tenants", async () => {
|
|
67
|
-
await insertPurchase(
|
|
57
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
68
58
|
|
|
69
59
|
const result = await runDividendCron(makeConfig());
|
|
70
60
|
|
|
@@ -75,7 +65,7 @@ describe("runDividendCron", () => {
|
|
|
75
65
|
});
|
|
76
66
|
|
|
77
67
|
it("is idempotent — skips if already ran for the date", async () => {
|
|
78
|
-
await insertPurchase(
|
|
68
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
79
69
|
|
|
80
70
|
const result1 = await runDividendCron(makeConfig());
|
|
81
71
|
expect(result1.distributed).toBe(1);
|
|
@@ -91,23 +81,22 @@ describe("runDividendCron", () => {
|
|
|
91
81
|
});
|
|
92
82
|
|
|
93
83
|
it("handles floor rounding — remainder is not distributed", async () => {
|
|
94
|
-
await insertPurchase(
|
|
95
|
-
await insertPurchase(
|
|
96
|
-
await insertPurchase(
|
|
84
|
+
await insertPurchase(ledger, "t1", 50, "2026-02-20 12:00:00");
|
|
85
|
+
await insertPurchase(ledger, "t2", 30, "2026-02-20 12:00:00");
|
|
86
|
+
await insertPurchase(ledger, "t3", 20, "2026-02-20 12:00:00");
|
|
97
87
|
|
|
98
88
|
const result = await runDividendCron(makeConfig());
|
|
99
89
|
|
|
100
90
|
expect(result.pool.toCents()).toBe(100);
|
|
101
91
|
expect(result.activeCount).toBe(3);
|
|
102
92
|
// Nanodollar precision: floor(1_000_000_000 raw / 3) = 333_333_333 raw each
|
|
103
|
-
// Remainder = 1 nanodollar (not 1 cent — far less wasted with higher scale)
|
|
104
93
|
expect(result.perUser.toRaw()).toBe(333_333_333);
|
|
105
94
|
expect(result.distributed).toBe(3);
|
|
106
95
|
});
|
|
107
96
|
|
|
108
97
|
it("skips distribution when pool is zero", async () => {
|
|
109
98
|
// Tenant purchased within 7 days but NOT on target date -> pool = 0
|
|
110
|
-
await insertPurchase(
|
|
99
|
+
await insertPurchase(ledger, "t1", 500, "2026-02-18 12:00:00");
|
|
111
100
|
|
|
112
101
|
const result = await runDividendCron(makeConfig());
|
|
113
102
|
|
|
@@ -118,11 +107,9 @@ describe("runDividendCron", () => {
|
|
|
118
107
|
});
|
|
119
108
|
|
|
120
109
|
it("distributes sub-cent amounts at nanodollar precision", async () => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
await insertPurchase(
|
|
124
|
-
await insertPurchase(db, "t2", 500, "2026-02-18 12:00:00");
|
|
125
|
-
await insertPurchase(db, "t3", 500, "2026-02-17 12:00:00");
|
|
110
|
+
await insertPurchase(ledger, "t1", 1, "2026-02-20 12:00:00");
|
|
111
|
+
await insertPurchase(ledger, "t2", 500, "2026-02-18 12:00:00");
|
|
112
|
+
await insertPurchase(ledger, "t3", 500, "2026-02-17 12:00:00");
|
|
126
113
|
|
|
127
114
|
const result = await runDividendCron(makeConfig({ matchRate: 1.0 }));
|
|
128
115
|
|
|
@@ -133,21 +120,20 @@ describe("runDividendCron", () => {
|
|
|
133
120
|
});
|
|
134
121
|
|
|
135
122
|
it("records transactions with correct type and referenceId", async () => {
|
|
136
|
-
await insertPurchase(
|
|
123
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
137
124
|
|
|
138
125
|
await runDividendCron(makeConfig());
|
|
139
126
|
|
|
140
127
|
const history = await ledger.history("t1", { type: "community_dividend" });
|
|
141
128
|
expect(history).toHaveLength(1);
|
|
142
|
-
expect(history[0].
|
|
129
|
+
expect(history[0].entryType).toBe("community_dividend");
|
|
143
130
|
expect(history[0].referenceId).toBe("dividend:2026-02-20:t1");
|
|
144
|
-
expect(history[0].amount.toCents()).toBe(1000);
|
|
145
131
|
expect(history[0].description).toContain("Community dividend");
|
|
146
132
|
});
|
|
147
133
|
|
|
148
134
|
it("collects errors without stopping distribution to other tenants", async () => {
|
|
149
|
-
await insertPurchase(
|
|
150
|
-
await insertPurchase(
|
|
135
|
+
await insertPurchase(ledger, "t1", 500, "2026-02-20 12:00:00");
|
|
136
|
+
await insertPurchase(ledger, "t2", 500, "2026-02-20 12:00:00");
|
|
151
137
|
|
|
152
138
|
const result = await runDividendCron(makeConfig());
|
|
153
139
|
|
|
@@ -1,11 +1,9 @@
|
|
|
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 { logger } from "../../config/logger.js";
|
|
4
|
-
import type { ICreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
5
4
|
|
|
6
5
|
export interface DividendCronConfig {
|
|
7
|
-
|
|
8
|
-
ledger: ICreditLedger;
|
|
6
|
+
ledger: ILedger;
|
|
9
7
|
/** Fraction of daily purchases matched as dividend pool. Default 1.0 (100%). */
|
|
10
8
|
matchRate: number;
|
|
11
9
|
/** The date to compute dividend for, as YYYY-MM-DD string. Typically yesterday. */
|
|
@@ -43,7 +41,7 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
|
|
|
43
41
|
// Idempotency: check if any per-tenant dividend was already distributed for this date.
|
|
44
42
|
// We look for any referenceId matching "dividend:YYYY-MM-DD:*".
|
|
45
43
|
const sentinelPrefix = `dividend:${cfg.targetDate}:`;
|
|
46
|
-
const alreadyRan = await cfg.
|
|
44
|
+
const alreadyRan = await cfg.ledger.existsByReferenceIdLike(`${sentinelPrefix}%`);
|
|
47
45
|
|
|
48
46
|
if (alreadyRan) {
|
|
49
47
|
result.skippedAlreadyRun = true;
|
|
@@ -55,7 +53,7 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
|
|
|
55
53
|
const dayStart = `${cfg.targetDate} 00:00:00`;
|
|
56
54
|
const dayEnd = `${cfg.targetDate} 24:00:00`;
|
|
57
55
|
|
|
58
|
-
const dailyPurchaseTotalCredit = await cfg.
|
|
56
|
+
const dailyPurchaseTotalCredit = await cfg.ledger.sumPurchasesForPeriod(dayStart, dayEnd);
|
|
59
57
|
result.pool = dailyPurchaseTotalCredit.multiply(cfg.matchRate);
|
|
60
58
|
|
|
61
59
|
// Step 2: Find all active tenants (purchased in last 7 days from target date).
|
|
@@ -64,7 +62,7 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
|
|
|
64
62
|
const windowStart = subtractDays(cfg.targetDate, 6);
|
|
65
63
|
const windowStartTs = `${windowStart} 00:00:00`;
|
|
66
64
|
|
|
67
|
-
const activeTenantIds = await cfg.
|
|
65
|
+
const activeTenantIds = await cfg.ledger.getActiveTenantIdsInWindow(windowStartTs, dayEnd);
|
|
68
66
|
result.activeCount = activeTenantIds.length;
|
|
69
67
|
|
|
70
68
|
// Step 3: Compute per-user share.
|
|
@@ -92,13 +90,10 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
|
|
|
92
90
|
for (const tenantId of activeTenantIds) {
|
|
93
91
|
const perUserRef = `dividend:${cfg.targetDate}:${tenantId}`;
|
|
94
92
|
try {
|
|
95
|
-
await cfg.ledger.credit(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
`Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`,
|
|
100
|
-
perUserRef,
|
|
101
|
-
);
|
|
93
|
+
await cfg.ledger.credit(tenantId, result.perUser, "community_dividend", {
|
|
94
|
+
description: `Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`,
|
|
95
|
+
referenceId: perUserRef,
|
|
96
|
+
});
|
|
102
97
|
result.distributed++;
|
|
103
98
|
} catch (err) {
|
|
104
99
|
const msg = err instanceof Error ? err.message : String(err);
|