@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
|
@@ -3,11 +3,11 @@ import type { AutoTopupChargeResult } from "./auto-topup-charge.js";
|
|
|
3
3
|
import { MAX_CONSECUTIVE_FAILURES } from "./auto-topup-charge.js";
|
|
4
4
|
import type { IAutoTopupSettingsRepository } from "./auto-topup-settings-repository.js";
|
|
5
5
|
import type { Credit } from "./credit.js";
|
|
6
|
-
import type {
|
|
6
|
+
import type { ILedger } from "./ledger.js";
|
|
7
7
|
|
|
8
8
|
export interface UsageTopupDeps {
|
|
9
9
|
settingsRepo: IAutoTopupSettingsRepository;
|
|
10
|
-
creditLedger:
|
|
10
|
+
creditLedger: ILedger;
|
|
11
11
|
/** Injected charge function (allows mocking in tests). */
|
|
12
12
|
chargeAutoTopup: (tenantId: string, amount: Credit, source: string) => Promise<AutoTopupChargeResult>;
|
|
13
13
|
/** Optional tenant status check. If provided and returns non-null, skip the charge. */
|
|
@@ -3,16 +3,16 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
|
3
3
|
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
4
4
|
import { Credit } from "./credit.js";
|
|
5
5
|
import { runCreditExpiryCron } from "./credit-expiry-cron.js";
|
|
6
|
-
import {
|
|
6
|
+
import { DrizzleLedger } from "./ledger.js";
|
|
7
7
|
|
|
8
8
|
describe("runCreditExpiryCron", () => {
|
|
9
9
|
let pool: PGlite;
|
|
10
|
-
let ledger:
|
|
10
|
+
let ledger: DrizzleLedger;
|
|
11
11
|
|
|
12
12
|
beforeAll(async () => {
|
|
13
13
|
const { db, pool: p } = await createTestDb();
|
|
14
14
|
pool = p;
|
|
15
|
-
ledger = new
|
|
15
|
+
ledger = new DrizzleLedger(db);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
afterAll(async () => {
|
|
@@ -21,6 +21,7 @@ describe("runCreditExpiryCron", () => {
|
|
|
21
21
|
|
|
22
22
|
beforeEach(async () => {
|
|
23
23
|
await truncateAllTables(pool);
|
|
24
|
+
await ledger.seedSystemAccounts();
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
// All tests pass an explicit `now` parameter — hardcoded dates are time-independent
|
|
@@ -33,16 +34,11 @@ describe("runCreditExpiryCron", () => {
|
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
it("debits expired promotional credit grant", async () => {
|
|
36
|
-
await ledger.credit(
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
"promo:tenant-1",
|
|
42
|
-
undefined,
|
|
43
|
-
undefined,
|
|
44
|
-
"2026-01-10T00:00:00Z",
|
|
45
|
-
);
|
|
37
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
38
|
+
description: "New user bonus",
|
|
39
|
+
referenceId: "promo:tenant-1",
|
|
40
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
41
|
+
});
|
|
46
42
|
|
|
47
43
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
48
44
|
expect(result.processed).toBe(1);
|
|
@@ -53,16 +49,11 @@ describe("runCreditExpiryCron", () => {
|
|
|
53
49
|
});
|
|
54
50
|
|
|
55
51
|
it("does not debit non-expired credits", async () => {
|
|
56
|
-
await ledger.credit(
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
"promo:tenant-1-future",
|
|
62
|
-
undefined,
|
|
63
|
-
undefined,
|
|
64
|
-
"2026-02-01T00:00:00Z",
|
|
65
|
-
);
|
|
52
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
53
|
+
description: "Future bonus",
|
|
54
|
+
referenceId: "promo:tenant-1-future",
|
|
55
|
+
expiresAt: "2026-02-01T00:00:00Z",
|
|
56
|
+
});
|
|
66
57
|
|
|
67
58
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
68
59
|
expect(result.processed).toBe(0);
|
|
@@ -72,7 +63,7 @@ describe("runCreditExpiryCron", () => {
|
|
|
72
63
|
});
|
|
73
64
|
|
|
74
65
|
it("does not debit credits without expires_at", async () => {
|
|
75
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "Top-up");
|
|
66
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "Top-up" });
|
|
76
67
|
|
|
77
68
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
78
69
|
expect(result.processed).toBe(0);
|
|
@@ -82,17 +73,12 @@ describe("runCreditExpiryCron", () => {
|
|
|
82
73
|
});
|
|
83
74
|
|
|
84
75
|
it("only debits up to available balance when partially consumed", async () => {
|
|
85
|
-
await ledger.credit(
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
undefined,
|
|
92
|
-
undefined,
|
|
93
|
-
"2026-01-10T00:00:00Z",
|
|
94
|
-
);
|
|
95
|
-
await ledger.debit("tenant-1", Credit.fromCents(300), "bot_runtime", "Runtime");
|
|
76
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
77
|
+
description: "Promo",
|
|
78
|
+
referenceId: "promo:partial",
|
|
79
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
80
|
+
});
|
|
81
|
+
await ledger.debit("tenant-1", Credit.fromCents(300), "bot_runtime", { description: "Runtime" });
|
|
96
82
|
|
|
97
83
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
98
84
|
expect(result.processed).toBe(1);
|
|
@@ -102,16 +88,11 @@ describe("runCreditExpiryCron", () => {
|
|
|
102
88
|
});
|
|
103
89
|
|
|
104
90
|
it("is idempotent -- does not double-debit on second run", async () => {
|
|
105
|
-
await ledger.credit(
|
|
106
|
-
"
|
|
107
|
-
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
"promo:idemp",
|
|
111
|
-
undefined,
|
|
112
|
-
undefined,
|
|
113
|
-
"2026-01-10T00:00:00Z",
|
|
114
|
-
);
|
|
91
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
92
|
+
description: "Promo",
|
|
93
|
+
referenceId: "promo:idemp",
|
|
94
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
95
|
+
});
|
|
115
96
|
|
|
116
97
|
await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
117
98
|
const balanceAfterFirst = await ledger.balance("tenant-1");
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { logger } from "../config/logger.js";
|
|
2
|
-
import type {
|
|
3
|
-
import { InsufficientBalanceError } from "./
|
|
2
|
+
import type { ILedger } from "./ledger.js";
|
|
3
|
+
import { InsufficientBalanceError } from "./ledger.js";
|
|
4
4
|
|
|
5
5
|
export interface CreditExpiryCronConfig {
|
|
6
|
-
ledger:
|
|
6
|
+
ledger: ILedger;
|
|
7
7
|
/** Current time as ISO-8601 string. */
|
|
8
8
|
now: string;
|
|
9
9
|
}
|
|
@@ -42,13 +42,10 @@ export async function runCreditExpiryCron(cfg: CreditExpiryCronConfig): Promise<
|
|
|
42
42
|
// Debit the lesser of the original grant amount or current balance
|
|
43
43
|
const debitAmount = balance.lessThan(grant.amount) ? balance : grant.amount;
|
|
44
44
|
|
|
45
|
-
await cfg.ledger.debit(
|
|
46
|
-
grant.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`Expired credit grant reclaimed: ${grant.id}`,
|
|
50
|
-
`expiry:${grant.id}`,
|
|
51
|
-
);
|
|
45
|
+
await cfg.ledger.debit(grant.tenantId, debitAmount, "credit_expiry", {
|
|
46
|
+
description: `Expired credit grant reclaimed: ${grant.entryId}`,
|
|
47
|
+
referenceId: `expiry:${grant.entryId}`,
|
|
48
|
+
});
|
|
52
49
|
|
|
53
50
|
result.processed++;
|
|
54
51
|
if (!result.expired.includes(grant.tenantId)) {
|
|
@@ -59,8 +56,8 @@ export async function runCreditExpiryCron(cfg: CreditExpiryCronConfig): Promise<
|
|
|
59
56
|
result.skippedZeroBalance++;
|
|
60
57
|
} else {
|
|
61
58
|
const msg = err instanceof Error ? err.message : String(err);
|
|
62
|
-
logger.error("Credit expiry failed", { tenantId: grant.tenantId,
|
|
63
|
-
result.errors.push(`${grant.tenantId}:${grant.
|
|
59
|
+
logger.error("Credit expiry failed", { tenantId: grant.tenantId, entryId: grant.entryId, error: msg });
|
|
60
|
+
result.errors.push(`${grant.tenantId}:${grant.entryId}: ${msg}`);
|
|
64
61
|
}
|
|
65
62
|
}
|
|
66
63
|
}
|
|
@@ -95,7 +95,7 @@ export class InsufficientBalanceError extends Error {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
export interface
|
|
98
|
+
export interface ILedger {
|
|
99
99
|
credit(
|
|
100
100
|
tenantId: string,
|
|
101
101
|
amount: Credit,
|
|
@@ -135,7 +135,7 @@ export interface ICreditLedger {
|
|
|
135
135
|
* creditBalances row is always consistent with the sum of creditTransactions.
|
|
136
136
|
* Zero raw SQL in application code.
|
|
137
137
|
*/
|
|
138
|
-
export class DrizzleCreditLedger implements
|
|
138
|
+
export class DrizzleCreditLedger implements ILedger {
|
|
139
139
|
constructor(private readonly db: PlatformDb) {}
|
|
140
140
|
|
|
141
141
|
/**
|
|
@@ -446,5 +446,5 @@ export class DrizzleCreditLedger implements ICreditLedger {
|
|
|
446
446
|
}
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
// Backward-compat alias — callers using 'new
|
|
449
|
+
// Backward-compat alias — callers using 'new DrizzleLedger(db)' continue to work.
|
|
450
450
|
export { DrizzleCreditLedger as CreditLedger };
|
|
@@ -1,44 +1,40 @@
|
|
|
1
1
|
import type { PGlite } from "@electric-sql/pglite";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
3
|
import type { PlatformDb } from "../db/index.js";
|
|
4
|
-
import { creditBalances, creditTransactions } from "../db/schema/credits.js";
|
|
5
4
|
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
6
5
|
import { Credit } from "./credit.js";
|
|
7
|
-
import { CreditLedger } from "./credit-ledger.js";
|
|
8
|
-
import { DrizzleCreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
9
6
|
import { type DividendCronConfig, runDividendCron } from "./dividend-cron.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
import { CREDIT_TYPE_ACCOUNT, DrizzleLedger } from "./ledger.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Insert a backdated purchase entry into the double-entry ledger.
|
|
11
|
+
* Uses post() with postedAt override to simulate historical purchases.
|
|
12
|
+
*/
|
|
13
|
+
async function insertPurchase(
|
|
14
|
+
ledger: DrizzleLedger,
|
|
15
|
+
tenantId: string,
|
|
16
|
+
amountCents: number,
|
|
17
|
+
postedAt: string,
|
|
18
|
+
): Promise<void> {
|
|
13
19
|
const amount = Credit.fromCents(amountCents);
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
// Purchase: DR cash (1000), CR unearned_revenue (2000:<tenantId>)
|
|
21
|
+
await ledger.post({
|
|
22
|
+
entryType: "purchase",
|
|
16
23
|
tenantId,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
description: `Test purchase ${amountCents}¢`,
|
|
25
|
+
referenceId: `test-purchase:${tenantId}:${postedAt}:${Math.random()}`,
|
|
26
|
+
postedAt,
|
|
27
|
+
lines: [
|
|
28
|
+
{ accountCode: CREDIT_TYPE_ACCOUNT.purchase, amount, side: "debit" },
|
|
29
|
+
{ accountCode: `2000:${tenantId}`, amount, side: "credit" },
|
|
30
|
+
],
|
|
21
31
|
});
|
|
22
|
-
// Upsert credit_balances
|
|
23
|
-
const existing = await db
|
|
24
|
-
.select()
|
|
25
|
-
.from(creditBalances)
|
|
26
|
-
.where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
|
|
27
|
-
if (existing.length > 0) {
|
|
28
|
-
await db
|
|
29
|
-
.update(creditBalances)
|
|
30
|
-
.set({ balance: existing[0].balance.add(amount) })
|
|
31
|
-
.where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
|
|
32
|
-
} else {
|
|
33
|
-
await db.insert(creditBalances).values({ tenantId, balance: amount });
|
|
34
|
-
}
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
describe("runDividendCron", () => {
|
|
38
35
|
let pool: PGlite;
|
|
39
36
|
let db: PlatformDb;
|
|
40
|
-
let ledger:
|
|
41
|
-
let creditTransactionRepo: DrizzleCreditTransactionRepository;
|
|
37
|
+
let ledger: DrizzleLedger;
|
|
42
38
|
|
|
43
39
|
beforeAll(async () => {
|
|
44
40
|
({ db, pool } = await createTestDb());
|
|
@@ -50,13 +46,12 @@ describe("runDividendCron", () => {
|
|
|
50
46
|
|
|
51
47
|
beforeEach(async () => {
|
|
52
48
|
await truncateAllTables(pool);
|
|
53
|
-
ledger = new
|
|
54
|
-
|
|
49
|
+
ledger = new DrizzleLedger(db);
|
|
50
|
+
await ledger.seedSystemAccounts();
|
|
55
51
|
});
|
|
56
52
|
|
|
57
53
|
function makeConfig(overrides?: Partial<DividendCronConfig>): DividendCronConfig {
|
|
58
54
|
return {
|
|
59
|
-
creditTransactionRepo,
|
|
60
55
|
ledger,
|
|
61
56
|
matchRate: 1.0,
|
|
62
57
|
targetDate: "2026-02-20",
|
|
@@ -65,7 +60,7 @@ describe("runDividendCron", () => {
|
|
|
65
60
|
}
|
|
66
61
|
|
|
67
62
|
it("distributes dividend to eligible tenants", async () => {
|
|
68
|
-
await insertPurchase(
|
|
63
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
69
64
|
|
|
70
65
|
const result = await runDividendCron(makeConfig());
|
|
71
66
|
|
|
@@ -76,7 +71,7 @@ describe("runDividendCron", () => {
|
|
|
76
71
|
});
|
|
77
72
|
|
|
78
73
|
it("is idempotent — skips if already ran for the date", async () => {
|
|
79
|
-
await insertPurchase(
|
|
74
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
80
75
|
|
|
81
76
|
const result1 = await runDividendCron(makeConfig());
|
|
82
77
|
expect(result1.distributed).toBe(1);
|
|
@@ -92,23 +87,22 @@ describe("runDividendCron", () => {
|
|
|
92
87
|
});
|
|
93
88
|
|
|
94
89
|
it("handles floor rounding — remainder is not distributed", async () => {
|
|
95
|
-
await insertPurchase(
|
|
96
|
-
await insertPurchase(
|
|
97
|
-
await insertPurchase(
|
|
90
|
+
await insertPurchase(ledger, "t1", 50, "2026-02-20 12:00:00");
|
|
91
|
+
await insertPurchase(ledger, "t2", 30, "2026-02-20 12:00:00");
|
|
92
|
+
await insertPurchase(ledger, "t3", 20, "2026-02-20 12:00:00");
|
|
98
93
|
|
|
99
94
|
const result = await runDividendCron(makeConfig());
|
|
100
95
|
|
|
101
96
|
expect(result.pool.toCents()).toBe(100);
|
|
102
97
|
expect(result.activeCount).toBe(3);
|
|
103
98
|
// Nanodollar precision: floor(1_000_000_000 raw / 3) = 333_333_333 raw each
|
|
104
|
-
// Remainder = 1 nanodollar (not 1 cent — far less wasted with higher scale)
|
|
105
99
|
expect(result.perUser.toRaw()).toBe(333_333_333);
|
|
106
100
|
expect(result.distributed).toBe(3);
|
|
107
101
|
});
|
|
108
102
|
|
|
109
103
|
it("skips distribution when pool is zero", async () => {
|
|
110
104
|
// Tenant purchased within 7 days but NOT on target date -> pool = 0
|
|
111
|
-
await insertPurchase(
|
|
105
|
+
await insertPurchase(ledger, "t1", 500, "2026-02-18 12:00:00");
|
|
112
106
|
|
|
113
107
|
const result = await runDividendCron(makeConfig());
|
|
114
108
|
|
|
@@ -121,9 +115,9 @@ describe("runDividendCron", () => {
|
|
|
121
115
|
it("distributes sub-cent amounts at nanodollar precision", async () => {
|
|
122
116
|
// 1 cent purchase, 3 active users: pool = 10_000_000 raw
|
|
123
117
|
// floor(10_000_000 / 3) = 3_333_333 raw each — non-zero, gets distributed
|
|
124
|
-
await insertPurchase(
|
|
125
|
-
await insertPurchase(
|
|
126
|
-
await insertPurchase(
|
|
118
|
+
await insertPurchase(ledger, "t1", 1, "2026-02-20 12:00:00");
|
|
119
|
+
await insertPurchase(ledger, "t2", 500, "2026-02-18 12:00:00");
|
|
120
|
+
await insertPurchase(ledger, "t3", 500, "2026-02-17 12:00:00");
|
|
127
121
|
|
|
128
122
|
const result = await runDividendCron(makeConfig({ matchRate: 1.0 }));
|
|
129
123
|
|
|
@@ -134,21 +128,20 @@ describe("runDividendCron", () => {
|
|
|
134
128
|
});
|
|
135
129
|
|
|
136
130
|
it("records transactions with correct type and referenceId", async () => {
|
|
137
|
-
await insertPurchase(
|
|
131
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
138
132
|
|
|
139
133
|
await runDividendCron(makeConfig());
|
|
140
134
|
|
|
141
135
|
const history = await ledger.history("t1", { type: "community_dividend" });
|
|
142
136
|
expect(history).toHaveLength(1);
|
|
143
|
-
expect(history[0].
|
|
137
|
+
expect(history[0].entryType).toBe("community_dividend");
|
|
144
138
|
expect(history[0].referenceId).toBe("dividend:2026-02-20:t1");
|
|
145
|
-
expect(history[0].amount.toCents()).toBe(1000);
|
|
146
139
|
expect(history[0].description).toContain("Community dividend");
|
|
147
140
|
});
|
|
148
141
|
|
|
149
142
|
it("collects errors without stopping distribution to other tenants", async () => {
|
|
150
|
-
await insertPurchase(
|
|
151
|
-
await insertPurchase(
|
|
143
|
+
await insertPurchase(ledger, "t1", 500, "2026-02-20 12:00:00");
|
|
144
|
+
await insertPurchase(ledger, "t2", 500, "2026-02-20 12:00:00");
|
|
152
145
|
|
|
153
146
|
const result = await runDividendCron(makeConfig());
|
|
154
147
|
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { logger } from "../config/logger.js";
|
|
2
2
|
import { Credit } from "./credit.js";
|
|
3
|
-
import type {
|
|
4
|
-
import type { ICreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
3
|
+
import type { ILedger } from "./ledger.js";
|
|
5
4
|
|
|
6
5
|
export interface DividendCronConfig {
|
|
7
|
-
|
|
8
|
-
ledger: ICreditLedger;
|
|
6
|
+
ledger: ILedger;
|
|
9
7
|
/** Fraction of daily purchases matched as dividend pool. Default 1.0 (100%). */
|
|
10
8
|
matchRate: number;
|
|
11
9
|
/** The date to compute dividend for, as YYYY-MM-DD string. Typically yesterday. */
|
|
@@ -25,8 +23,8 @@ export interface DividendCronResult {
|
|
|
25
23
|
* Compute and distribute the community dividend for a given day.
|
|
26
24
|
*
|
|
27
25
|
* 1. Check idempotency — skip if already run for this date.
|
|
28
|
-
* 2. Sum all 'purchase'
|
|
29
|
-
* 3. Find all tenants with a 'purchase'
|
|
26
|
+
* 2. Sum all 'purchase' entries for the target date.
|
|
27
|
+
* 3. Find all tenants with a 'purchase' entry in the last 7 days.
|
|
30
28
|
* 4. Compute pool = sum × matchRate, per-user share = floor(pool / activeCount).
|
|
31
29
|
* 5. Credit each active tenant with their share.
|
|
32
30
|
*/
|
|
@@ -40,10 +38,8 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
|
|
|
40
38
|
errors: [],
|
|
41
39
|
};
|
|
42
40
|
|
|
43
|
-
// Idempotency: check if any per-tenant dividend was already distributed for this date.
|
|
44
|
-
// We look for any referenceId matching "dividend:YYYY-MM-DD:*".
|
|
45
41
|
const sentinelPrefix = `dividend:${cfg.targetDate}:`;
|
|
46
|
-
const alreadyRan = await cfg.
|
|
42
|
+
const alreadyRan = await cfg.ledger.existsByReferenceIdLike(`${sentinelPrefix}%`);
|
|
47
43
|
|
|
48
44
|
if (alreadyRan) {
|
|
49
45
|
result.skippedAlreadyRun = true;
|
|
@@ -51,23 +47,18 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
|
|
|
51
47
|
return result;
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
// Step 1: Sum all purchase amounts for the target date.
|
|
55
50
|
const dayStart = `${cfg.targetDate} 00:00:00`;
|
|
56
51
|
const dayEnd = `${cfg.targetDate} 24:00:00`;
|
|
57
52
|
|
|
58
|
-
const
|
|
59
|
-
result.pool =
|
|
53
|
+
const dailyPurchaseTotal = await cfg.ledger.sumPurchasesForPeriod(dayStart, dayEnd);
|
|
54
|
+
result.pool = dailyPurchaseTotal.multiply(cfg.matchRate);
|
|
60
55
|
|
|
61
|
-
// Step 2: Find all active tenants (purchased in last 7 days from target date).
|
|
62
|
-
// The 7-day window is: [targetDate - 6 days 00:00:00, targetDate 24:00:00)
|
|
63
|
-
// This gives a full 7-day range ending at the end of targetDate.
|
|
64
56
|
const windowStart = subtractDays(cfg.targetDate, 6);
|
|
65
57
|
const windowStartTs = `${windowStart} 00:00:00`;
|
|
66
58
|
|
|
67
|
-
const activeTenantIds = await cfg.
|
|
59
|
+
const activeTenantIds = await cfg.ledger.getActiveTenantIdsInWindow(windowStartTs, dayEnd);
|
|
68
60
|
result.activeCount = activeTenantIds.length;
|
|
69
61
|
|
|
70
|
-
// Step 3: Compute per-user share.
|
|
71
62
|
if (result.pool.isZero() || result.activeCount <= 0) {
|
|
72
63
|
logger.info("Dividend cron: no pool or no active tenants", {
|
|
73
64
|
targetDate: cfg.targetDate,
|
|
@@ -88,17 +79,13 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
|
|
|
88
79
|
return result;
|
|
89
80
|
}
|
|
90
81
|
|
|
91
|
-
// Step 4: Distribute to each active tenant.
|
|
92
82
|
for (const tenantId of activeTenantIds) {
|
|
93
83
|
const perUserRef = `dividend:${cfg.targetDate}:${tenantId}`;
|
|
94
84
|
try {
|
|
95
|
-
await cfg.ledger.credit(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
`Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`,
|
|
100
|
-
perUserRef,
|
|
101
|
-
);
|
|
85
|
+
await cfg.ledger.credit(tenantId, result.perUser, "community_dividend", {
|
|
86
|
+
description: `Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`,
|
|
87
|
+
referenceId: perUserRef,
|
|
88
|
+
});
|
|
102
89
|
result.distributed++;
|
|
103
90
|
} catch (err) {
|
|
104
91
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -119,7 +106,6 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
|
|
|
119
106
|
return result;
|
|
120
107
|
}
|
|
121
108
|
|
|
122
|
-
/** Subtract N days from a YYYY-MM-DD date string, returning YYYY-MM-DD. */
|
|
123
109
|
function subtractDays(dateStr: string, days: number): string {
|
|
124
110
|
const d = new Date(`${dateStr}T00:00:00Z`);
|
|
125
111
|
d.setUTCDate(d.getUTCDate() - days);
|
|
@@ -6,8 +6,8 @@ import { adminUsers } from "../db/schema/admin-users.js";
|
|
|
6
6
|
import { dividendDistributions } from "../db/schema/dividend-distributions.js";
|
|
7
7
|
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
8
8
|
import { Credit } from "./credit.js";
|
|
9
|
-
import { CreditLedger } from "./credit-ledger.js";
|
|
10
9
|
import { DrizzleDividendRepository } from "./dividend-repository.js";
|
|
10
|
+
import { DrizzleLedger } from "./ledger.js";
|
|
11
11
|
|
|
12
12
|
let pool: PGlite;
|
|
13
13
|
let db: PlatformDb;
|
|
@@ -55,6 +55,7 @@ describe("DrizzleDividendRepository", () => {
|
|
|
55
55
|
|
|
56
56
|
beforeEach(async () => {
|
|
57
57
|
await truncateAllTables(pool);
|
|
58
|
+
await new DrizzleLedger(db).seedSystemAccounts();
|
|
58
59
|
repo = new DrizzleDividendRepository(db);
|
|
59
60
|
});
|
|
60
61
|
|
|
@@ -195,8 +196,8 @@ describe("DrizzleDividendRepository", () => {
|
|
|
195
196
|
});
|
|
196
197
|
|
|
197
198
|
it("marks user as eligible when they have a recent purchase", async () => {
|
|
198
|
-
const ledger = new
|
|
199
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase", "recent buy");
|
|
199
|
+
const ledger = new DrizzleLedger(db);
|
|
200
|
+
await ledger.credit("t1", Credit.fromCents(100), "purchase", { description: "recent buy" });
|
|
200
201
|
|
|
201
202
|
const stats = await repo.getStats("t1");
|
|
202
203
|
expect(stats.userEligible).toBe(true);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { and, desc, eq, gte, lt, sql } from "drizzle-orm";
|
|
2
2
|
import type { PlatformDb } from "../db/index.js";
|
|
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
|
import { Credit } from "./credit.js";
|
|
7
7
|
import type { DividendHistoryEntry, DividendStats } from "./repository-types.js";
|
|
8
8
|
|
|
@@ -30,35 +30,36 @@ export class DrizzleDividendRepository implements IDividendRepository {
|
|
|
30
30
|
constructor(private readonly db: PlatformDb) {}
|
|
31
31
|
|
|
32
32
|
async getStats(tenantId: string): Promise<DividendStats> {
|
|
33
|
-
// 1. Pool = sum of purchase amounts from yesterday UTC
|
|
33
|
+
// 1. Pool = sum of purchase credit amounts from yesterday UTC
|
|
34
|
+
// In double-entry: purchase entries have a credit line on the tenant liability account.
|
|
35
|
+
// Sum those credit line amounts for entries posted yesterday.
|
|
34
36
|
const poolRow = (
|
|
35
37
|
await this.db
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
.
|
|
38
|
+
.select({ total: sql<string>`COALESCE(SUM(${journalLines.amount}), 0)` })
|
|
39
|
+
.from(journalLines)
|
|
40
|
+
.innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
|
|
39
41
|
.where(
|
|
40
42
|
and(
|
|
41
|
-
eq(
|
|
43
|
+
eq(journalEntries.entryType, "purchase"),
|
|
44
|
+
eq(journalLines.side, "credit"),
|
|
42
45
|
// raw SQL: Drizzle cannot express date_trunc with interval arithmetic
|
|
43
|
-
sql`${
|
|
44
|
-
sql`${
|
|
46
|
+
sql`${journalEntries.postedAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`,
|
|
47
|
+
sql`${journalEntries.postedAt}::timestamp < date_trunc('day', timezone('UTC', now()))`,
|
|
45
48
|
),
|
|
46
49
|
)
|
|
47
50
|
)[0];
|
|
48
|
-
const
|
|
49
|
-
const pool = Credit.fromCents(poolCents);
|
|
51
|
+
const pool = Credit.fromRaw(Number(poolRow?.total ?? 0));
|
|
50
52
|
|
|
51
53
|
// 2. Active users = distinct tenants with a purchase in the last 7 days
|
|
52
54
|
const activeRow = (
|
|
53
55
|
await this.db
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
.from(creditTransactions)
|
|
56
|
+
.select({ count: sql<number>`COUNT(DISTINCT ${journalEntries.tenantId})` })
|
|
57
|
+
.from(journalEntries)
|
|
57
58
|
.where(
|
|
58
59
|
and(
|
|
59
|
-
eq(
|
|
60
|
+
eq(journalEntries.entryType, "purchase"),
|
|
60
61
|
// raw SQL: Drizzle cannot express timestamp comparison with interval arithmetic
|
|
61
|
-
sql`${
|
|
62
|
+
sql`${journalEntries.postedAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`,
|
|
62
63
|
),
|
|
63
64
|
)
|
|
64
65
|
)[0];
|
|
@@ -75,10 +76,10 @@ export class DrizzleDividendRepository implements IDividendRepository {
|
|
|
75
76
|
// 5. User eligibility — last purchase within 7 days
|
|
76
77
|
const userPurchaseRow = (
|
|
77
78
|
await this.db
|
|
78
|
-
.select({
|
|
79
|
-
.from(
|
|
80
|
-
.where(and(eq(
|
|
81
|
-
.orderBy(desc(
|
|
79
|
+
.select({ postedAt: journalEntries.postedAt })
|
|
80
|
+
.from(journalEntries)
|
|
81
|
+
.where(and(eq(journalEntries.tenantId, tenantId), eq(journalEntries.entryType, "purchase")))
|
|
82
|
+
.orderBy(desc(journalEntries.postedAt))
|
|
82
83
|
.limit(1)
|
|
83
84
|
)[0];
|
|
84
85
|
|
|
@@ -87,10 +88,7 @@ export class DrizzleDividendRepository implements IDividendRepository {
|
|
|
87
88
|
let userWindowExpiresAt: string | null = null;
|
|
88
89
|
|
|
89
90
|
if (userPurchaseRow) {
|
|
90
|
-
const
|
|
91
|
-
// Parse the timestamp directly. PGlite may return ISO strings with or without
|
|
92
|
-
// timezone suffix. JavaScript's Date constructor handles ISO 8601 strings natively.
|
|
93
|
-
const lastPurchase = new Date(rawTs);
|
|
91
|
+
const lastPurchase = new Date(userPurchaseRow.postedAt);
|
|
94
92
|
userLastPurchaseAt = lastPurchase.toISOString();
|
|
95
93
|
|
|
96
94
|
const windowExpiry = new Date(lastPurchase.getTime() + 7 * 24 * 60 * 60 * 1000);
|
package/src/credits/index.ts
CHANGED
|
@@ -12,14 +12,33 @@ export {
|
|
|
12
12
|
export { Credit } from "./credit.js";
|
|
13
13
|
export type { CreditExpiryCronConfig, CreditExpiryCronResult } from "./credit-expiry-cron.js";
|
|
14
14
|
export { runCreditExpiryCron } from "./credit-expiry-cron.js";
|
|
15
|
+
// -- Double-entry ledger (new) --
|
|
15
16
|
export type {
|
|
16
|
-
|
|
17
|
+
AccountType,
|
|
18
|
+
CreditOpts,
|
|
17
19
|
CreditType,
|
|
20
|
+
DebitOpts,
|
|
18
21
|
DebitType,
|
|
19
22
|
HistoryOptions,
|
|
20
|
-
|
|
23
|
+
ILedger,
|
|
24
|
+
JournalEntry,
|
|
25
|
+
JournalLine,
|
|
26
|
+
MemberUsageSummary,
|
|
27
|
+
PostEntryInput,
|
|
28
|
+
Side,
|
|
29
|
+
SystemAccount,
|
|
21
30
|
TransactionType,
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
TrialBalance,
|
|
32
|
+
} from "./ledger.js";
|
|
33
|
+
export {
|
|
34
|
+
CREDIT_TYPE_ACCOUNT,
|
|
35
|
+
DEBIT_TYPE_ACCOUNT,
|
|
36
|
+
DrizzleLedger,
|
|
37
|
+
InsufficientBalanceError,
|
|
38
|
+
Ledger,
|
|
39
|
+
SYSTEM_ACCOUNTS,
|
|
40
|
+
} from "./ledger.js";
|
|
24
41
|
export { grantSignupCredits, SIGNUP_GRANT } from "./signup-grant.js";
|
|
25
42
|
export type { ITenantCustomerRepository, TenantCustomerRow } from "./tenant-customer-repository.js";
|
|
43
|
+
export type { TrialBalanceCronConfig, TrialBalanceCronResult } from "./trial-balance-cron.js";
|
|
44
|
+
export { runTrialBalanceCron } from "./trial-balance-cron.js";
|