@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,4 +1,4 @@
|
|
|
1
|
-
import { Credit,
|
|
1
|
+
import { Credit, DrizzleLedger } 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
|
import { DrizzleAffiliateFraudRepository } from "./affiliate-fraud-repository.js";
|
|
@@ -18,12 +18,17 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
18
18
|
});
|
|
19
19
|
beforeEach(async () => {
|
|
20
20
|
await truncateAllTables(pool);
|
|
21
|
-
ledger = new
|
|
21
|
+
ledger = new DrizzleLedger(db);
|
|
22
|
+
await ledger.seedSystemAccounts();
|
|
22
23
|
affiliateRepo = new DrizzleAffiliateRepository(db);
|
|
23
24
|
fraudRepo = new DrizzleAffiliateFraudRepository(db);
|
|
24
25
|
});
|
|
25
26
|
it("does nothing when tenant has no referral", async () => {
|
|
26
|
-
await ledger.credit("buyer", Credit.fromCents(1000), "purchase",
|
|
27
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
28
|
+
description: "first buy",
|
|
29
|
+
referenceId: "session-1",
|
|
30
|
+
fundingSource: "stripe",
|
|
31
|
+
});
|
|
27
32
|
const result = await processAffiliateCreditMatch({
|
|
28
33
|
tenantId: "buyer",
|
|
29
34
|
purchaseAmount: Credit.fromCents(1000),
|
|
@@ -34,8 +39,16 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
34
39
|
});
|
|
35
40
|
it("does nothing when tenant already has prior purchases", async () => {
|
|
36
41
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
37
|
-
await ledger.credit("buyer", Credit.fromCents(500), "purchase",
|
|
38
|
-
|
|
42
|
+
await ledger.credit("buyer", Credit.fromCents(500), "purchase", {
|
|
43
|
+
description: "old buy",
|
|
44
|
+
referenceId: "session-0",
|
|
45
|
+
fundingSource: "stripe",
|
|
46
|
+
});
|
|
47
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
48
|
+
description: "new buy",
|
|
49
|
+
referenceId: "session-1",
|
|
50
|
+
fundingSource: "stripe",
|
|
51
|
+
});
|
|
39
52
|
const result = await processAffiliateCreditMatch({
|
|
40
53
|
tenantId: "buyer",
|
|
41
54
|
purchaseAmount: Credit.fromCents(1000),
|
|
@@ -46,7 +59,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
46
59
|
});
|
|
47
60
|
it("credits referrer on first purchase with 100% match", async () => {
|
|
48
61
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
49
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
62
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
63
|
+
description: "first buy",
|
|
64
|
+
referenceId: "session-1",
|
|
65
|
+
fundingSource: "stripe",
|
|
66
|
+
});
|
|
50
67
|
const result = await processAffiliateCreditMatch({
|
|
51
68
|
tenantId: "buyer",
|
|
52
69
|
purchaseAmount: Credit.fromCents(2000),
|
|
@@ -65,7 +82,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
65
82
|
});
|
|
66
83
|
it("respects custom match rate", async () => {
|
|
67
84
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
68
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
85
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
86
|
+
description: "first buy",
|
|
87
|
+
referenceId: "session-1",
|
|
88
|
+
fundingSource: "stripe",
|
|
89
|
+
});
|
|
69
90
|
const result = await processAffiliateCreditMatch({
|
|
70
91
|
tenantId: "buyer",
|
|
71
92
|
purchaseAmount: Credit.fromCents(2000),
|
|
@@ -78,7 +99,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
78
99
|
});
|
|
79
100
|
it("is idempotent — second call returns null", async () => {
|
|
80
101
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
81
|
-
await ledger.credit("buyer", Credit.fromCents(1000), "purchase",
|
|
102
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
103
|
+
description: "first buy",
|
|
104
|
+
referenceId: "session-1",
|
|
105
|
+
fundingSource: "stripe",
|
|
106
|
+
});
|
|
82
107
|
const first = await processAffiliateCreditMatch({
|
|
83
108
|
tenantId: "buyer",
|
|
84
109
|
purchaseAmount: Credit.fromCents(1000),
|
|
@@ -99,7 +124,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
99
124
|
signupIp: "1.2.3.4",
|
|
100
125
|
signupEmail: "alice+ref@gmail.com",
|
|
101
126
|
});
|
|
102
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
127
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
128
|
+
description: "first buy",
|
|
129
|
+
referenceId: "session-1",
|
|
130
|
+
fundingSource: "stripe",
|
|
131
|
+
});
|
|
103
132
|
const result = await processAffiliateCreditMatch({
|
|
104
133
|
tenantId: "buyer",
|
|
105
134
|
purchaseAmount: Credit.fromCents(2000),
|
|
@@ -127,7 +156,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
127
156
|
}
|
|
128
157
|
// New referral
|
|
129
158
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
130
|
-
await ledger.credit("buyer", Credit.fromCents(1000), "purchase",
|
|
159
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
160
|
+
description: "first buy",
|
|
161
|
+
referenceId: "session-1",
|
|
162
|
+
fundingSource: "stripe",
|
|
163
|
+
});
|
|
131
164
|
const result = await processAffiliateCreditMatch({
|
|
132
165
|
tenantId: "buyer",
|
|
133
166
|
purchaseAmount: Credit.fromCents(1000),
|
|
@@ -151,7 +184,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
151
184
|
}
|
|
152
185
|
// New referral
|
|
153
186
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
154
|
-
await ledger.credit("buyer", Credit.fromCents(1000), "purchase",
|
|
187
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
188
|
+
description: "first buy",
|
|
189
|
+
referenceId: "session-1",
|
|
190
|
+
fundingSource: "stripe",
|
|
191
|
+
});
|
|
155
192
|
const result = await processAffiliateCreditMatch({
|
|
156
193
|
tenantId: "buyer",
|
|
157
194
|
purchaseAmount: Credit.fromCents(1000),
|
|
@@ -167,7 +204,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
167
204
|
});
|
|
168
205
|
it("allows payout when under both caps", async () => {
|
|
169
206
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
170
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
207
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
208
|
+
description: "first buy",
|
|
209
|
+
referenceId: "session-1",
|
|
210
|
+
fundingSource: "stripe",
|
|
211
|
+
});
|
|
171
212
|
const result = await processAffiliateCreditMatch({
|
|
172
213
|
tenantId: "buyer",
|
|
173
214
|
purchaseAmount: Credit.fromCents(2000),
|
|
@@ -184,7 +225,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
184
225
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123", {
|
|
185
226
|
signupIp: "1.2.3.4",
|
|
186
227
|
});
|
|
187
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
228
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
229
|
+
description: "first buy",
|
|
230
|
+
referenceId: "session-1",
|
|
231
|
+
fundingSource: "stripe",
|
|
232
|
+
});
|
|
188
233
|
const result = await processAffiliateCreditMatch({
|
|
189
234
|
tenantId: "buyer",
|
|
190
235
|
purchaseAmount: Credit.fromCents(2000),
|
|
@@ -1,10 +1,10 @@
|
|
|
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 { IAffiliateRepository } from "./drizzle-affiliate-repository.js";
|
|
4
4
|
/** Default bonus rate: 20% of purchase amount. Override with AFFILIATE_NEW_USER_BONUS_RATE env var. */
|
|
5
5
|
export declare const DEFAULT_BONUS_RATE: number;
|
|
6
6
|
export interface NewUserBonusParams {
|
|
7
|
-
ledger:
|
|
7
|
+
ledger: ILedger;
|
|
8
8
|
affiliateRepo: IAffiliateRepository;
|
|
9
9
|
referredTenantId: string;
|
|
10
10
|
purchaseAmount: Credit;
|
|
@@ -34,6 +34,9 @@ export async function grantNewUserBonus(params) {
|
|
|
34
34
|
// 5. Mark first purchase on the referral row (no-op if already set)
|
|
35
35
|
await affiliateRepo.markFirstPurchase(referredTenantId);
|
|
36
36
|
// 6. Credit the bonus
|
|
37
|
-
await ledger.credit(referredTenantId, bonus, "affiliate_bonus",
|
|
37
|
+
await ledger.credit(referredTenantId, bonus, "affiliate_bonus", {
|
|
38
|
+
description: `New user first-purchase bonus (${Math.round(rate * 100)}%)`,
|
|
39
|
+
referenceId: refId,
|
|
40
|
+
});
|
|
38
41
|
return { granted: true, bonus };
|
|
39
42
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Credit,
|
|
1
|
+
import { Credit, DrizzleLedger } 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
|
import { DrizzleAffiliateRepository } from "./drizzle-affiliate-repository.js";
|
|
@@ -16,7 +16,8 @@ describe("grantNewUserBonus", () => {
|
|
|
16
16
|
});
|
|
17
17
|
beforeEach(async () => {
|
|
18
18
|
await truncateAllTables(pool);
|
|
19
|
-
ledger = new
|
|
19
|
+
ledger = new DrizzleLedger(db);
|
|
20
|
+
await ledger.seedSystemAccounts();
|
|
20
21
|
affiliateRepo = new DrizzleAffiliateRepository(db);
|
|
21
22
|
});
|
|
22
23
|
it("DEFAULT_BONUS_RATE equals 0.20", () => {
|
|
@@ -37,7 +38,7 @@ describe("grantNewUserBonus", () => {
|
|
|
37
38
|
expect((await ledger.balance("referred-1")).toCents()).toBe(1000);
|
|
38
39
|
const txns = await ledger.history("referred-1");
|
|
39
40
|
expect(txns).toHaveLength(1);
|
|
40
|
-
expect(txns[0].
|
|
41
|
+
expect(txns[0].entryType).toBe("affiliate_bonus");
|
|
41
42
|
expect(txns[0].referenceId).toBe("affiliate-bonus:referred-1");
|
|
42
43
|
expect(txns[0].description).toContain("first-purchase bonus");
|
|
43
44
|
});
|
|
@@ -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 type { IAutoTopupEventLogRepository } from "./auto-topup-event-log-repository.js";
|
|
5
5
|
/** After this many consecutive Stripe failures, the auto-topup mode is disabled. */
|
|
@@ -7,7 +7,7 @@ export declare const MAX_CONSECUTIVE_FAILURES = 3;
|
|
|
7
7
|
export interface AutoTopupChargeDeps {
|
|
8
8
|
stripe: Stripe;
|
|
9
9
|
tenantRepo: ITenantCustomerRepository;
|
|
10
|
-
creditLedger:
|
|
10
|
+
creditLedger: ILedger;
|
|
11
11
|
eventLogRepo: IAutoTopupEventLogRepository;
|
|
12
12
|
}
|
|
13
13
|
export interface AutoTopupChargeResult {
|
|
@@ -113,7 +113,11 @@ export async function chargeAutoTopup(deps, tenantId, amount, source) {
|
|
|
113
113
|
// 5. Credit the ledger (idempotent via referenceId = PI ID)
|
|
114
114
|
try {
|
|
115
115
|
if (!(await deps.creditLedger.hasReferenceId(paymentIntent.id))) {
|
|
116
|
-
await deps.creditLedger.credit(tenantId, amount, "purchase",
|
|
116
|
+
await deps.creditLedger.credit(tenantId, amount, "purchase", {
|
|
117
|
+
description: `Auto-topup (${source})`,
|
|
118
|
+
referenceId: paymentIntent.id,
|
|
119
|
+
fundingSource: "stripe",
|
|
120
|
+
});
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
catch (err) {
|
|
@@ -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 Stripe from "stripe";
|
|
4
4
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { creditAutoTopup } from "../../db/schema/credit-auto-topup.js";
|
|
@@ -46,7 +46,8 @@ describe("chargeAutoTopup", () => {
|
|
|
46
46
|
});
|
|
47
47
|
beforeEach(async () => {
|
|
48
48
|
await truncateAllTables(pool);
|
|
49
|
-
ledger = new
|
|
49
|
+
ledger = new DrizzleLedger(db);
|
|
50
|
+
await ledger.seedSystemAccounts();
|
|
50
51
|
});
|
|
51
52
|
it("charges Stripe and credits ledger on success", async () => {
|
|
52
53
|
const stripe = mockStripe();
|
|
@@ -62,8 +63,8 @@ describe("chargeAutoTopup", () => {
|
|
|
62
63
|
expect(result.paymentReference).toEqual(expect.any(String));
|
|
63
64
|
expect((await ledger.balance("t1")).toCents()).toBe(500);
|
|
64
65
|
const history = await ledger.history("t1");
|
|
65
|
-
expect(history[0].
|
|
66
|
-
expect(history[0].fundingSource).toBe("stripe");
|
|
66
|
+
expect(history[0].entryType).toBe("purchase");
|
|
67
|
+
expect(history[0].metadata?.fundingSource).toBe("stripe");
|
|
67
68
|
});
|
|
68
69
|
it("writes success event to credit_auto_topup log", async () => {
|
|
69
70
|
const stripe = mockStripe();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Credit, IAutoTopupSettingsRepository,
|
|
1
|
+
import type { Credit, IAutoTopupSettingsRepository, ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import type { AutoTopupChargeResult } from "./auto-topup-charge.js";
|
|
3
3
|
export interface UsageTopupDeps {
|
|
4
4
|
settingsRepo: IAutoTopupSettingsRepository;
|
|
5
|
-
creditLedger:
|
|
5
|
+
creditLedger: ILedger;
|
|
6
6
|
/** Injected charge function (allows mocking in tests). */
|
|
7
7
|
chargeAutoTopup: (tenantId: string, amount: Credit, source: string) => Promise<AutoTopupChargeResult>;
|
|
8
8
|
/** Optional tenant status check. If provided and returns non-null, skip the charge. */
|
|
@@ -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) {
|