@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 { Credit,
|
|
1
|
+
import { Credit, DrizzleAutoTopupSettingsRepository, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
4
4
|
import { maybeTriggerUsageTopup } from "./auto-topup-usage.js";
|
|
@@ -15,7 +15,8 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
15
15
|
});
|
|
16
16
|
beforeEach(async () => {
|
|
17
17
|
await truncateAllTables(pool);
|
|
18
|
-
ledger = new
|
|
18
|
+
ledger = new DrizzleLedger(db);
|
|
19
|
+
await ledger.seedSystemAccounts();
|
|
19
20
|
settingsRepo = new DrizzleAutoTopupSettingsRepository(db);
|
|
20
21
|
});
|
|
21
22
|
it("does nothing when tenant has no auto-topup settings", async () => {
|
|
@@ -26,7 +27,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
26
27
|
});
|
|
27
28
|
it("does nothing when usage_enabled is false", async () => {
|
|
28
29
|
await settingsRepo.upsert("t1", { usageEnabled: false });
|
|
29
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
30
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
31
|
+
description: "buy",
|
|
32
|
+
referenceId: "ref-1",
|
|
33
|
+
fundingSource: "stripe",
|
|
34
|
+
});
|
|
30
35
|
const mockCharge = vi.fn();
|
|
31
36
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
32
37
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -38,7 +43,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
38
43
|
usageThreshold: Credit.fromCents(100),
|
|
39
44
|
usageTopup: Credit.fromCents(500),
|
|
40
45
|
});
|
|
41
|
-
await ledger.credit("t1", Credit.fromCents(200), "purchase",
|
|
46
|
+
await ledger.credit("t1", Credit.fromCents(200), "purchase", {
|
|
47
|
+
description: "buy",
|
|
48
|
+
referenceId: "ref-1",
|
|
49
|
+
fundingSource: "stripe",
|
|
50
|
+
});
|
|
42
51
|
const mockCharge = vi.fn();
|
|
43
52
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
44
53
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -50,7 +59,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
50
59
|
usageThreshold: Credit.fromCents(100),
|
|
51
60
|
usageTopup: Credit.fromCents(500),
|
|
52
61
|
});
|
|
53
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
62
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
63
|
+
description: "buy",
|
|
64
|
+
referenceId: "ref-1",
|
|
65
|
+
fundingSource: "stripe",
|
|
66
|
+
});
|
|
54
67
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
|
|
55
68
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
56
69
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -59,7 +72,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
59
72
|
it("skips when charge is already in-flight", async () => {
|
|
60
73
|
await settingsRepo.upsert("t1", { usageEnabled: true, usageThreshold: Credit.fromCents(100) });
|
|
61
74
|
await settingsRepo.setUsageChargeInFlight("t1", true);
|
|
62
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
75
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
76
|
+
description: "buy",
|
|
77
|
+
referenceId: "ref-1",
|
|
78
|
+
fundingSource: "stripe",
|
|
79
|
+
});
|
|
63
80
|
const mockCharge = vi.fn();
|
|
64
81
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
65
82
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -72,7 +89,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
72
89
|
usageThreshold: Credit.fromCents(500),
|
|
73
90
|
usageTopup: Credit.fromCents(2000),
|
|
74
91
|
});
|
|
75
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase",
|
|
92
|
+
await ledger.credit("t1", Credit.fromCents(100), "purchase", {
|
|
93
|
+
description: "buy",
|
|
94
|
+
referenceId: "ref-1",
|
|
95
|
+
fundingSource: "stripe",
|
|
96
|
+
});
|
|
76
97
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_race" });
|
|
77
98
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
78
99
|
// Fire two concurrent calls — both see balance < threshold,
|
|
@@ -91,7 +112,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
91
112
|
usageThreshold: Credit.fromCents(100),
|
|
92
113
|
usageTopup: Credit.fromCents(500),
|
|
93
114
|
});
|
|
94
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
115
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
116
|
+
description: "buy",
|
|
117
|
+
referenceId: "ref-1",
|
|
118
|
+
fundingSource: "stripe",
|
|
119
|
+
});
|
|
95
120
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
|
|
96
121
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
97
122
|
// First call — triggers charge, flag set then cleared
|
|
@@ -109,7 +134,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
109
134
|
usageThreshold: Credit.fromCents(100),
|
|
110
135
|
usageTopup: Credit.fromCents(500),
|
|
111
136
|
});
|
|
112
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
137
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
138
|
+
description: "buy",
|
|
139
|
+
referenceId: "ref-1",
|
|
140
|
+
fundingSource: "stripe",
|
|
141
|
+
});
|
|
113
142
|
const mockCharge = vi
|
|
114
143
|
.fn()
|
|
115
144
|
.mockRejectedValueOnce(new Error("Stripe network error"))
|
|
@@ -132,7 +161,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
132
161
|
});
|
|
133
162
|
await settingsRepo.incrementUsageFailures("t1");
|
|
134
163
|
await settingsRepo.incrementUsageFailures("t1");
|
|
135
|
-
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
|
+
});
|
|
136
169
|
const mockCharge = vi.fn().mockResolvedValue({ success: true });
|
|
137
170
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
138
171
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -144,7 +177,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
144
177
|
usageThreshold: Credit.fromCents(100),
|
|
145
178
|
usageTopup: Credit.fromCents(500),
|
|
146
179
|
});
|
|
147
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
180
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
181
|
+
description: "buy",
|
|
182
|
+
referenceId: "ref-1",
|
|
183
|
+
fundingSource: "stripe",
|
|
184
|
+
});
|
|
148
185
|
const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
|
|
149
186
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
150
187
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -170,7 +207,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
170
207
|
});
|
|
171
208
|
await settingsRepo.incrementUsageFailures("t1");
|
|
172
209
|
await settingsRepo.incrementUsageFailures("t1");
|
|
173
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
210
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
211
|
+
description: "buy",
|
|
212
|
+
referenceId: "ref-1",
|
|
213
|
+
fundingSource: "stripe",
|
|
214
|
+
});
|
|
174
215
|
const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
|
|
175
216
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
176
217
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -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 { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
|
|
4
4
|
import type { INodeCommandBus } from "../../fleet/node-command-bus.js";
|
|
@@ -11,7 +11,7 @@ export interface IBotBilling {
|
|
|
11
11
|
suspendBot(botId: string): Promise<void>;
|
|
12
12
|
suspendAllForTenant(tenantId: string): Promise<string[]>;
|
|
13
13
|
reactivateBot(botId: string): Promise<void>;
|
|
14
|
-
checkReactivation(tenantId: string, ledger:
|
|
14
|
+
checkReactivation(tenantId: string, ledger: ILedger): Promise<string[]>;
|
|
15
15
|
destroyBot(botId: string): Promise<void>;
|
|
16
16
|
destroyExpiredBots(): Promise<string[]>;
|
|
17
17
|
getBotBilling(botId: string): Promise<unknown>;
|
|
@@ -56,7 +56,7 @@ export declare class DrizzleBotBilling implements IBotBilling {
|
|
|
56
56
|
*
|
|
57
57
|
* @returns IDs of reactivated bots.
|
|
58
58
|
*/
|
|
59
|
-
checkReactivation(tenantId: string, ledger:
|
|
59
|
+
checkReactivation(tenantId: string, ledger: ILedger): Promise<string[]>;
|
|
60
60
|
/**
|
|
61
61
|
* Mark a bot as destroyed.
|
|
62
62
|
* Sets billingState='destroyed'. Actual Docker cleanup is handled by the caller.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Credit,
|
|
1
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { sql } from "drizzle-orm";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { botInstances } from "../../db/schema/bot-instances.js";
|
|
@@ -61,7 +61,8 @@ describe("BotBilling", () => {
|
|
|
61
61
|
beforeEach(async () => {
|
|
62
62
|
await truncateAllTables(pool);
|
|
63
63
|
billing = new BotBilling(new DrizzleBotInstanceRepository(db));
|
|
64
|
-
ledger = new
|
|
64
|
+
ledger = new DrizzleLedger(db);
|
|
65
|
+
await ledger.seedSystemAccounts();
|
|
65
66
|
});
|
|
66
67
|
describe("registerBot", () => {
|
|
67
68
|
it("registers a bot in active billing state", async () => {
|
|
@@ -178,7 +179,11 @@ describe("BotBilling", () => {
|
|
|
178
179
|
await billing.registerBot("bot-2", "tenant-1", "bot-b");
|
|
179
180
|
await billing.suspendBot("bot-1");
|
|
180
181
|
await billing.suspendBot("bot-2");
|
|
181
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase",
|
|
182
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
|
|
183
|
+
description: "test credit",
|
|
184
|
+
referenceId: "ref-1",
|
|
185
|
+
fundingSource: "stripe",
|
|
186
|
+
});
|
|
182
187
|
const reactivated = await billing.checkReactivation("tenant-1", ledger);
|
|
183
188
|
expect(reactivated.sort()).toEqual(["bot-1", "bot-2"]);
|
|
184
189
|
expect(await billing.getActiveBotCount("tenant-1")).toBe(2);
|
|
@@ -193,12 +198,20 @@ describe("BotBilling", () => {
|
|
|
193
198
|
it("does not reactivate destroyed bots", async () => {
|
|
194
199
|
await billing.registerBot("bot-1", "tenant-1", "bot-a");
|
|
195
200
|
await billing.destroyBot("bot-1");
|
|
196
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase",
|
|
201
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
|
|
202
|
+
description: "test credit",
|
|
203
|
+
referenceId: "ref-1",
|
|
204
|
+
fundingSource: "stripe",
|
|
205
|
+
});
|
|
197
206
|
const reactivated = await billing.checkReactivation("tenant-1", ledger);
|
|
198
207
|
expect(reactivated).toEqual([]);
|
|
199
208
|
});
|
|
200
209
|
it("returns empty array for tenant with no bots", async () => {
|
|
201
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase",
|
|
210
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
|
|
211
|
+
description: "test credit",
|
|
212
|
+
referenceId: "ref-1",
|
|
213
|
+
fundingSource: "stripe",
|
|
214
|
+
});
|
|
202
215
|
const reactivated = await billing.checkReactivation("tenant-1", ledger);
|
|
203
216
|
expect(reactivated).toEqual([]);
|
|
204
217
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Credit,
|
|
1
|
+
import { Credit, DrizzleLedger, runCreditExpiryCron } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
3
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
4
4
|
describe("runCreditExpiryCron", () => {
|
|
@@ -7,13 +7,14 @@ describe("runCreditExpiryCron", () => {
|
|
|
7
7
|
beforeAll(async () => {
|
|
8
8
|
const { db, pool: p } = await createTestDb();
|
|
9
9
|
pool = p;
|
|
10
|
-
ledger = new
|
|
10
|
+
ledger = new DrizzleLedger(db);
|
|
11
11
|
});
|
|
12
12
|
afterAll(async () => {
|
|
13
13
|
await pool.close();
|
|
14
14
|
});
|
|
15
15
|
beforeEach(async () => {
|
|
16
16
|
await truncateAllTables(pool);
|
|
17
|
+
await ledger.seedSystemAccounts();
|
|
17
18
|
});
|
|
18
19
|
// All tests pass an explicit `now` parameter — hardcoded dates are time-independent
|
|
19
20
|
// because runCreditExpiryCron never reads the system clock.
|
|
@@ -24,7 +25,11 @@ describe("runCreditExpiryCron", () => {
|
|
|
24
25
|
expect(result.errors).toEqual([]);
|
|
25
26
|
});
|
|
26
27
|
it("debits expired promotional credit grant", async () => {
|
|
27
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "promo",
|
|
28
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
29
|
+
description: "New user bonus",
|
|
30
|
+
referenceId: "promo:tenant-1",
|
|
31
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
32
|
+
});
|
|
28
33
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
29
34
|
expect(result.processed).toBe(1);
|
|
30
35
|
expect(result.expired).toContain("tenant-1");
|
|
@@ -32,29 +37,41 @@ describe("runCreditExpiryCron", () => {
|
|
|
32
37
|
expect(balance.toCents()).toBe(0);
|
|
33
38
|
});
|
|
34
39
|
it("does not debit non-expired credits", async () => {
|
|
35
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "promo",
|
|
40
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
41
|
+
description: "Future bonus",
|
|
42
|
+
referenceId: "promo:tenant-1-future",
|
|
43
|
+
expiresAt: "2026-02-01T00:00:00Z",
|
|
44
|
+
});
|
|
36
45
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
37
46
|
expect(result.processed).toBe(0);
|
|
38
47
|
const balance = await ledger.balance("tenant-1");
|
|
39
48
|
expect(balance.toCents()).toBe(500);
|
|
40
49
|
});
|
|
41
50
|
it("does not debit credits without expires_at", async () => {
|
|
42
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "Top-up");
|
|
51
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "Top-up" });
|
|
43
52
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
44
53
|
expect(result.processed).toBe(0);
|
|
45
54
|
const balance = await ledger.balance("tenant-1");
|
|
46
55
|
expect(balance.toCents()).toBe(500);
|
|
47
56
|
});
|
|
48
57
|
it("only debits up to available balance when partially consumed", async () => {
|
|
49
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "promo",
|
|
50
|
-
|
|
58
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
59
|
+
description: "Promo",
|
|
60
|
+
referenceId: "promo:partial",
|
|
61
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
62
|
+
});
|
|
63
|
+
await ledger.debit("tenant-1", Credit.fromCents(300), "bot_runtime", { description: "Runtime" });
|
|
51
64
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
52
65
|
expect(result.processed).toBe(1);
|
|
53
66
|
const balance = await ledger.balance("tenant-1");
|
|
54
67
|
expect(balance.toCents()).toBe(0);
|
|
55
68
|
});
|
|
56
69
|
it("is idempotent -- does not double-debit on second run", async () => {
|
|
57
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "promo",
|
|
70
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
71
|
+
description: "Promo",
|
|
72
|
+
referenceId: "promo:idemp",
|
|
73
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
74
|
+
});
|
|
58
75
|
await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
59
76
|
const balanceAfterFirst = await ledger.balance("tenant-1");
|
|
60
77
|
const result2 = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { Credit } from "@wopr-network/platform-core/credits";
|
|
3
|
-
import type { ICreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
4
3
|
export interface DividendCronConfig {
|
|
5
|
-
|
|
6
|
-
ledger: ICreditLedger;
|
|
4
|
+
ledger: ILedger;
|
|
7
5
|
/** Fraction of daily purchases matched as dividend pool. Default 1.0 (100%). */
|
|
8
6
|
matchRate: number;
|
|
9
7
|
/** The date to compute dividend for, as YYYY-MM-DD string. Typically yesterday. */
|
|
@@ -21,7 +21,7 @@ export async function runDividendCron(cfg) {
|
|
|
21
21
|
// Idempotency: check if any per-tenant dividend was already distributed for this date.
|
|
22
22
|
// We look for any referenceId matching "dividend:YYYY-MM-DD:*".
|
|
23
23
|
const sentinelPrefix = `dividend:${cfg.targetDate}:`;
|
|
24
|
-
const alreadyRan = await cfg.
|
|
24
|
+
const alreadyRan = await cfg.ledger.existsByReferenceIdLike(`${sentinelPrefix}%`);
|
|
25
25
|
if (alreadyRan) {
|
|
26
26
|
result.skippedAlreadyRun = true;
|
|
27
27
|
logger.info("Dividend cron already ran for this date", { targetDate: cfg.targetDate });
|
|
@@ -30,14 +30,14 @@ export async function runDividendCron(cfg) {
|
|
|
30
30
|
// Step 1: Sum all purchase amounts for the target date.
|
|
31
31
|
const dayStart = `${cfg.targetDate} 00:00:00`;
|
|
32
32
|
const dayEnd = `${cfg.targetDate} 24:00:00`;
|
|
33
|
-
const dailyPurchaseTotalCredit = await cfg.
|
|
33
|
+
const dailyPurchaseTotalCredit = await cfg.ledger.sumPurchasesForPeriod(dayStart, dayEnd);
|
|
34
34
|
result.pool = dailyPurchaseTotalCredit.multiply(cfg.matchRate);
|
|
35
35
|
// Step 2: Find all active tenants (purchased in last 7 days from target date).
|
|
36
36
|
// The 7-day window is: [targetDate - 6 days 00:00:00, targetDate 24:00:00)
|
|
37
37
|
// This gives a full 7-day range ending at the end of targetDate.
|
|
38
38
|
const windowStart = subtractDays(cfg.targetDate, 6);
|
|
39
39
|
const windowStartTs = `${windowStart} 00:00:00`;
|
|
40
|
-
const activeTenantIds = await cfg.
|
|
40
|
+
const activeTenantIds = await cfg.ledger.getActiveTenantIdsInWindow(windowStartTs, dayEnd);
|
|
41
41
|
result.activeCount = activeTenantIds.length;
|
|
42
42
|
// Step 3: Compute per-user share.
|
|
43
43
|
if (result.pool.isZero() || result.activeCount <= 0) {
|
|
@@ -61,7 +61,10 @@ export async function runDividendCron(cfg) {
|
|
|
61
61
|
for (const tenantId of activeTenantIds) {
|
|
62
62
|
const perUserRef = `dividend:${cfg.targetDate}:${tenantId}`;
|
|
63
63
|
try {
|
|
64
|
-
await cfg.ledger.credit(tenantId, result.perUser, "community_dividend",
|
|
64
|
+
await cfg.ledger.credit(tenantId, result.perUser, "community_dividend", {
|
|
65
|
+
description: `Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`,
|
|
66
|
+
referenceId: perUserRef,
|
|
67
|
+
});
|
|
65
68
|
result.distributed++;
|
|
66
69
|
}
|
|
67
70
|
catch (err) {
|
|
@@ -1,40 +1,25 @@
|
|
|
1
|
-
import { Credit,
|
|
1
|
+
import { CREDIT_TYPE_ACCOUNT, Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
-
import { creditBalances, creditTransactions } from "../../db/schema/credits.js";
|
|
4
3
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
5
|
-
import { DrizzleCreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
6
4
|
import { runDividendCron } from "./dividend-cron.js";
|
|
7
|
-
async function insertPurchase(
|
|
8
|
-
const id = `test-${tenantId}-${Date.now()}-${Math.random()}`;
|
|
5
|
+
async function insertPurchase(ledger, tenantId, amountCents, postedAt) {
|
|
9
6
|
const amount = Credit.fromCents(amountCents);
|
|
10
|
-
await
|
|
11
|
-
|
|
7
|
+
await ledger.post({
|
|
8
|
+
entryType: "purchase",
|
|
12
9
|
tenantId,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
description: `Test purchase ${amountCents}¢`,
|
|
11
|
+
referenceId: `test-purchase:${tenantId}:${postedAt}:${Math.random()}`,
|
|
12
|
+
postedAt,
|
|
13
|
+
lines: [
|
|
14
|
+
{ accountCode: CREDIT_TYPE_ACCOUNT.purchase, amount, side: "debit" },
|
|
15
|
+
{ accountCode: `2000:${tenantId}`, amount, side: "credit" },
|
|
16
|
+
],
|
|
17
17
|
});
|
|
18
|
-
// Upsert credit_balances
|
|
19
|
-
const existing = await db
|
|
20
|
-
.select()
|
|
21
|
-
.from(creditBalances)
|
|
22
|
-
.where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
|
|
23
|
-
if (existing.length > 0) {
|
|
24
|
-
await db
|
|
25
|
-
.update(creditBalances)
|
|
26
|
-
.set({ balance: existing[0].balance.add(amount) })
|
|
27
|
-
.where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
await db.insert(creditBalances).values({ tenantId, balance: amount });
|
|
31
|
-
}
|
|
32
18
|
}
|
|
33
19
|
describe("runDividendCron", () => {
|
|
34
20
|
let pool;
|
|
35
21
|
let db;
|
|
36
22
|
let ledger;
|
|
37
|
-
let creditTransactionRepo;
|
|
38
23
|
beforeAll(async () => {
|
|
39
24
|
({ db, pool } = await createTestDb());
|
|
40
25
|
});
|
|
@@ -43,12 +28,11 @@ describe("runDividendCron", () => {
|
|
|
43
28
|
});
|
|
44
29
|
beforeEach(async () => {
|
|
45
30
|
await truncateAllTables(pool);
|
|
46
|
-
ledger = new
|
|
47
|
-
|
|
31
|
+
ledger = new DrizzleLedger(db);
|
|
32
|
+
await ledger.seedSystemAccounts();
|
|
48
33
|
});
|
|
49
34
|
function makeConfig(overrides) {
|
|
50
35
|
return {
|
|
51
|
-
creditTransactionRepo,
|
|
52
36
|
ledger,
|
|
53
37
|
matchRate: 1.0,
|
|
54
38
|
targetDate: "2026-02-20",
|
|
@@ -56,7 +40,7 @@ describe("runDividendCron", () => {
|
|
|
56
40
|
};
|
|
57
41
|
}
|
|
58
42
|
it("distributes dividend to eligible tenants", async () => {
|
|
59
|
-
await insertPurchase(
|
|
43
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
60
44
|
const result = await runDividendCron(makeConfig());
|
|
61
45
|
expect(result.distributed).toBe(1);
|
|
62
46
|
expect(result.pool.toCents()).toBe(1000);
|
|
@@ -64,7 +48,7 @@ describe("runDividendCron", () => {
|
|
|
64
48
|
expect(result.activeCount).toBe(1);
|
|
65
49
|
});
|
|
66
50
|
it("is idempotent — skips if already ran for the date", async () => {
|
|
67
|
-
await insertPurchase(
|
|
51
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
68
52
|
const result1 = await runDividendCron(makeConfig());
|
|
69
53
|
expect(result1.distributed).toBe(1);
|
|
70
54
|
expect(result1.skippedAlreadyRun).toBe(false);
|
|
@@ -75,20 +59,19 @@ describe("runDividendCron", () => {
|
|
|
75
59
|
expect((await ledger.balance("t1")).equals(balanceAfterFirst)).toBe(true);
|
|
76
60
|
});
|
|
77
61
|
it("handles floor rounding — remainder is not distributed", async () => {
|
|
78
|
-
await insertPurchase(
|
|
79
|
-
await insertPurchase(
|
|
80
|
-
await insertPurchase(
|
|
62
|
+
await insertPurchase(ledger, "t1", 50, "2026-02-20 12:00:00");
|
|
63
|
+
await insertPurchase(ledger, "t2", 30, "2026-02-20 12:00:00");
|
|
64
|
+
await insertPurchase(ledger, "t3", 20, "2026-02-20 12:00:00");
|
|
81
65
|
const result = await runDividendCron(makeConfig());
|
|
82
66
|
expect(result.pool.toCents()).toBe(100);
|
|
83
67
|
expect(result.activeCount).toBe(3);
|
|
84
68
|
// Nanodollar precision: floor(1_000_000_000 raw / 3) = 333_333_333 raw each
|
|
85
|
-
// Remainder = 1 nanodollar (not 1 cent — far less wasted with higher scale)
|
|
86
69
|
expect(result.perUser.toRaw()).toBe(333_333_333);
|
|
87
70
|
expect(result.distributed).toBe(3);
|
|
88
71
|
});
|
|
89
72
|
it("skips distribution when pool is zero", async () => {
|
|
90
73
|
// Tenant purchased within 7 days but NOT on target date -> pool = 0
|
|
91
|
-
await insertPurchase(
|
|
74
|
+
await insertPurchase(ledger, "t1", 500, "2026-02-18 12:00:00");
|
|
92
75
|
const result = await runDividendCron(makeConfig());
|
|
93
76
|
expect(result.pool.toCents()).toBe(0);
|
|
94
77
|
expect(result.activeCount).toBe(1);
|
|
@@ -96,11 +79,9 @@ describe("runDividendCron", () => {
|
|
|
96
79
|
expect(result.distributed).toBe(0);
|
|
97
80
|
});
|
|
98
81
|
it("distributes sub-cent amounts at nanodollar precision", async () => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
await insertPurchase(
|
|
102
|
-
await insertPurchase(db, "t2", 500, "2026-02-18 12:00:00");
|
|
103
|
-
await insertPurchase(db, "t3", 500, "2026-02-17 12:00:00");
|
|
82
|
+
await insertPurchase(ledger, "t1", 1, "2026-02-20 12:00:00");
|
|
83
|
+
await insertPurchase(ledger, "t2", 500, "2026-02-18 12:00:00");
|
|
84
|
+
await insertPurchase(ledger, "t3", 500, "2026-02-17 12:00:00");
|
|
104
85
|
const result = await runDividendCron(makeConfig({ matchRate: 1.0 }));
|
|
105
86
|
expect(result.pool.toCents()).toBe(1);
|
|
106
87
|
expect(result.activeCount).toBe(3);
|
|
@@ -108,18 +89,17 @@ describe("runDividendCron", () => {
|
|
|
108
89
|
expect(result.distributed).toBe(3);
|
|
109
90
|
});
|
|
110
91
|
it("records transactions with correct type and referenceId", async () => {
|
|
111
|
-
await insertPurchase(
|
|
92
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
112
93
|
await runDividendCron(makeConfig());
|
|
113
94
|
const history = await ledger.history("t1", { type: "community_dividend" });
|
|
114
95
|
expect(history).toHaveLength(1);
|
|
115
|
-
expect(history[0].
|
|
96
|
+
expect(history[0].entryType).toBe("community_dividend");
|
|
116
97
|
expect(history[0].referenceId).toBe("dividend:2026-02-20:t1");
|
|
117
|
-
expect(history[0].amount.toCents()).toBe(1000);
|
|
118
98
|
expect(history[0].description).toContain("Community dividend");
|
|
119
99
|
});
|
|
120
100
|
it("collects errors without stopping distribution to other tenants", async () => {
|
|
121
|
-
await insertPurchase(
|
|
122
|
-
await insertPurchase(
|
|
101
|
+
await insertPurchase(ledger, "t1", 500, "2026-02-20 12:00:00");
|
|
102
|
+
await insertPurchase(ledger, "t2", 500, "2026-02-20 12:00:00");
|
|
123
103
|
const result = await runDividendCron(makeConfig());
|
|
124
104
|
expect(result.distributed).toBe(2);
|
|
125
105
|
expect(result.errors).toEqual([]);
|
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
import { Credit } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { and, desc, eq, gte, lt, sql } from "drizzle-orm";
|
|
3
3
|
import { adminUsers } from "../../db/schema/admin-users.js";
|
|
4
|
-
import { creditTransactions } from "../../db/schema/credits.js";
|
|
5
4
|
import { dividendDistributions } from "../../db/schema/dividend-distributions.js";
|
|
5
|
+
import { journalEntries, journalLines } from "../../db/schema/ledger.js";
|
|
6
6
|
export class DrizzleDividendRepository {
|
|
7
7
|
db;
|
|
8
8
|
constructor(db) {
|
|
9
9
|
this.db = db;
|
|
10
10
|
}
|
|
11
11
|
async getStats(tenantId) {
|
|
12
|
-
// 1. Pool = sum of purchase amounts from yesterday UTC
|
|
12
|
+
// 1. Pool = sum of purchase credit amounts from yesterday UTC
|
|
13
13
|
const poolRow = (await this.db
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
.where(and(eq(
|
|
18
|
-
|
|
19
|
-
sql `${creditTransactions.createdAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`, sql `${creditTransactions.createdAt}::timestamp < date_trunc('day', timezone('UTC', now()))`)))[0];
|
|
20
|
-
const poolCents = poolRow?.total ?? 0;
|
|
21
|
-
const pool = Credit.fromCents(poolCents);
|
|
14
|
+
.select({ total: sql `COALESCE(SUM(${journalLines.amount}), 0)` })
|
|
15
|
+
.from(journalLines)
|
|
16
|
+
.innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
|
|
17
|
+
.where(and(eq(journalEntries.entryType, "purchase"), eq(journalLines.side, "credit"), sql `${journalEntries.postedAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`, sql `${journalEntries.postedAt}::timestamp < date_trunc('day', timezone('UTC', now()))`)))[0];
|
|
18
|
+
const pool = Credit.fromRaw(Number(poolRow?.total ?? 0));
|
|
22
19
|
// 2. Active users = distinct tenants with a purchase in the last 7 days
|
|
23
20
|
const activeRow = (await this.db
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
.where(and(eq(creditTransactions.type, "purchase"),
|
|
28
|
-
// raw SQL: Drizzle cannot express timestamp comparison with interval arithmetic
|
|
29
|
-
sql `${creditTransactions.createdAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`)))[0];
|
|
21
|
+
.select({ count: sql `COUNT(DISTINCT ${journalEntries.tenantId})` })
|
|
22
|
+
.from(journalEntries)
|
|
23
|
+
.where(and(eq(journalEntries.entryType, "purchase"), sql `${journalEntries.postedAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`)))[0];
|
|
30
24
|
const activeUsers = activeRow?.count ?? 0;
|
|
31
25
|
// 3. Per-user projection (avoid division by zero)
|
|
32
26
|
const perUser = activeUsers > 0 ? Credit.fromRaw(Math.floor(pool.toRaw() / activeUsers)) : Credit.ZERO;
|
|
@@ -36,19 +30,16 @@ export class DrizzleDividendRepository {
|
|
|
36
30
|
const nextDistributionAt = nextMidnight.toISOString();
|
|
37
31
|
// 5. User eligibility — last purchase within 7 days
|
|
38
32
|
const userPurchaseRow = (await this.db
|
|
39
|
-
.select({
|
|
40
|
-
.from(
|
|
41
|
-
.where(and(eq(
|
|
42
|
-
.orderBy(desc(
|
|
33
|
+
.select({ postedAt: journalEntries.postedAt })
|
|
34
|
+
.from(journalEntries)
|
|
35
|
+
.where(and(eq(journalEntries.tenantId, tenantId), eq(journalEntries.entryType, "purchase")))
|
|
36
|
+
.orderBy(desc(journalEntries.postedAt))
|
|
43
37
|
.limit(1))[0];
|
|
44
38
|
let userEligible = false;
|
|
45
39
|
let userLastPurchaseAt = null;
|
|
46
40
|
let userWindowExpiresAt = null;
|
|
47
41
|
if (userPurchaseRow) {
|
|
48
|
-
const
|
|
49
|
-
// Parse the timestamp directly. PGlite may return ISO strings with or without
|
|
50
|
-
// timezone suffix. JavaScript's Date constructor handles ISO 8601 strings natively.
|
|
51
|
-
const lastPurchase = new Date(rawTs);
|
|
42
|
+
const lastPurchase = new Date(userPurchaseRow.postedAt);
|
|
52
43
|
userLastPurchaseAt = lastPurchase.toISOString();
|
|
53
44
|
const windowExpiry = new Date(lastPurchase.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
54
45
|
userWindowExpiresAt = windowExpiry.toISOString();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
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 { adminUsers } from "../../db/schema/admin-users.js";
|
|
5
5
|
import { dividendDistributions } from "../../db/schema/dividend-distributions.js";
|
|
@@ -39,6 +39,7 @@ describe("DrizzleDividendRepository", () => {
|
|
|
39
39
|
let repo;
|
|
40
40
|
beforeEach(async () => {
|
|
41
41
|
await truncateAllTables(pool);
|
|
42
|
+
await new DrizzleLedger(db).seedSystemAccounts();
|
|
42
43
|
repo = new DrizzleDividendRepository(db);
|
|
43
44
|
});
|
|
44
45
|
// --- getHistory() ---
|
|
@@ -152,8 +153,8 @@ describe("DrizzleDividendRepository", () => {
|
|
|
152
153
|
expect(stats.nextDistributionAt).toEqual(expect.any(String));
|
|
153
154
|
});
|
|
154
155
|
it("marks user as eligible when they have a recent purchase", async () => {
|
|
155
|
-
const ledger = new
|
|
156
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase", "recent buy");
|
|
156
|
+
const ledger = new DrizzleLedger(db);
|
|
157
|
+
await ledger.credit("t1", Credit.fromCents(100), "purchase", { description: "recent buy" });
|
|
157
158
|
const stats = await repo.getStats("t1");
|
|
158
159
|
expect(stats.userEligible).toBe(true);
|
|
159
160
|
expect(stats.userLastPurchaseAt).toEqual(expect.any(String));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { AutoTopupSettings, CreditExpiryCronConfig, CreditExpiryCronResult,
|
|
2
|
-
export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS,
|
|
1
|
+
export type { AutoTopupSettings, CreditExpiryCronConfig, CreditExpiryCronResult, CreditType, DebitType, HistoryOptions, IAutoTopupSettingsRepository, ILedger, JournalEntry, TransactionType, } from "@wopr-network/platform-core/credits";
|
|
2
|
+
export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS, computeNextScheduleAt, DrizzleAutoTopupSettingsRepository, DrizzleLedger, grantSignupCredits, InsufficientBalanceError, Ledger, runCreditExpiryCron, SIGNUP_GRANT, } from "@wopr-network/platform-core/credits";
|
|
3
3
|
export type { BillingState, IBotBilling } from "./bot-billing.js";
|
|
4
4
|
export { BotBilling, DrizzleBotBilling, SUSPENSION_GRACE_DAYS } from "./bot-billing.js";
|
|
5
5
|
export type { DividendDigestConfig, DividendDigestResult } from "./dividend-digest-cron.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS,
|
|
1
|
+
export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS, computeNextScheduleAt, DrizzleAutoTopupSettingsRepository, DrizzleLedger, grantSignupCredits, InsufficientBalanceError, Ledger, runCreditExpiryCron, SIGNUP_GRANT, } from "@wopr-network/platform-core/credits";
|
|
2
2
|
export { BotBilling, DrizzleBotBilling, SUSPENSION_GRACE_DAYS } from "./bot-billing.js";
|
|
3
3
|
export { runDividendDigestCron } from "./dividend-digest-cron.js";
|
|
4
4
|
export { buildResourceTierCosts, DAILY_BOT_COST, runRuntimeDeductions } from "./runtime-cron.js";
|