@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
|
@@ -4,8 +4,8 @@ import { Credit } from "./credit.js";
|
|
|
4
4
|
* Compute and distribute the community dividend for a given day.
|
|
5
5
|
*
|
|
6
6
|
* 1. Check idempotency — skip if already run for this date.
|
|
7
|
-
* 2. Sum all 'purchase'
|
|
8
|
-
* 3. Find all tenants with a 'purchase'
|
|
7
|
+
* 2. Sum all 'purchase' entries for the target date.
|
|
8
|
+
* 3. Find all tenants with a 'purchase' entry in the last 7 days.
|
|
9
9
|
* 4. Compute pool = sum × matchRate, per-user share = floor(pool / activeCount).
|
|
10
10
|
* 5. Credit each active tenant with their share.
|
|
11
11
|
*/
|
|
@@ -18,28 +18,21 @@ export async function runDividendCron(cfg) {
|
|
|
18
18
|
skippedAlreadyRun: false,
|
|
19
19
|
errors: [],
|
|
20
20
|
};
|
|
21
|
-
// Idempotency: check if any per-tenant dividend was already distributed for this date.
|
|
22
|
-
// We look for any referenceId matching "dividend:YYYY-MM-DD:*".
|
|
23
21
|
const sentinelPrefix = `dividend:${cfg.targetDate}:`;
|
|
24
|
-
const alreadyRan = await cfg.
|
|
22
|
+
const alreadyRan = await cfg.ledger.existsByReferenceIdLike(`${sentinelPrefix}%`);
|
|
25
23
|
if (alreadyRan) {
|
|
26
24
|
result.skippedAlreadyRun = true;
|
|
27
25
|
logger.info("Dividend cron already ran for this date", { targetDate: cfg.targetDate });
|
|
28
26
|
return result;
|
|
29
27
|
}
|
|
30
|
-
// Step 1: Sum all purchase amounts for the target date.
|
|
31
28
|
const dayStart = `${cfg.targetDate} 00:00:00`;
|
|
32
29
|
const dayEnd = `${cfg.targetDate} 24:00:00`;
|
|
33
|
-
const
|
|
34
|
-
result.pool =
|
|
35
|
-
// Step 2: Find all active tenants (purchased in last 7 days from target date).
|
|
36
|
-
// The 7-day window is: [targetDate - 6 days 00:00:00, targetDate 24:00:00)
|
|
37
|
-
// This gives a full 7-day range ending at the end of targetDate.
|
|
30
|
+
const dailyPurchaseTotal = await cfg.ledger.sumPurchasesForPeriod(dayStart, dayEnd);
|
|
31
|
+
result.pool = dailyPurchaseTotal.multiply(cfg.matchRate);
|
|
38
32
|
const windowStart = subtractDays(cfg.targetDate, 6);
|
|
39
33
|
const windowStartTs = `${windowStart} 00:00:00`;
|
|
40
|
-
const activeTenantIds = await cfg.
|
|
34
|
+
const activeTenantIds = await cfg.ledger.getActiveTenantIdsInWindow(windowStartTs, dayEnd);
|
|
41
35
|
result.activeCount = activeTenantIds.length;
|
|
42
|
-
// Step 3: Compute per-user share.
|
|
43
36
|
if (result.pool.isZero() || result.activeCount <= 0) {
|
|
44
37
|
logger.info("Dividend cron: no pool or no active tenants", {
|
|
45
38
|
targetDate: cfg.targetDate,
|
|
@@ -57,11 +50,13 @@ export async function runDividendCron(cfg) {
|
|
|
57
50
|
});
|
|
58
51
|
return result;
|
|
59
52
|
}
|
|
60
|
-
// Step 4: Distribute to each active tenant.
|
|
61
53
|
for (const tenantId of activeTenantIds) {
|
|
62
54
|
const perUserRef = `dividend:${cfg.targetDate}:${tenantId}`;
|
|
63
55
|
try {
|
|
64
|
-
await cfg.ledger.credit(tenantId, result.perUser, "community_dividend",
|
|
56
|
+
await cfg.ledger.credit(tenantId, result.perUser, "community_dividend", {
|
|
57
|
+
description: `Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`,
|
|
58
|
+
referenceId: perUserRef,
|
|
59
|
+
});
|
|
65
60
|
result.distributed++;
|
|
66
61
|
}
|
|
67
62
|
catch (err) {
|
|
@@ -80,7 +75,6 @@ export async function runDividendCron(cfg) {
|
|
|
80
75
|
});
|
|
81
76
|
return result;
|
|
82
77
|
}
|
|
83
|
-
/** Subtract N days from a YYYY-MM-DD date string, returning YYYY-MM-DD. */
|
|
84
78
|
function subtractDays(dateStr, days) {
|
|
85
79
|
const d = new Date(`${dateStr}T00:00:00Z`);
|
|
86
80
|
d.setUTCDate(d.getUTCDate() - days);
|
|
@@ -1,41 +1,31 @@
|
|
|
1
1
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
-
import { creditBalances, creditTransactions } from "../db/schema/credits.js";
|
|
3
2
|
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
4
3
|
import { Credit } from "./credit.js";
|
|
5
|
-
import { CreditLedger } from "./credit-ledger.js";
|
|
6
|
-
import { DrizzleCreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
7
4
|
import { runDividendCron } from "./dividend-cron.js";
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import { CREDIT_TYPE_ACCOUNT, DrizzleLedger } from "./ledger.js";
|
|
6
|
+
/**
|
|
7
|
+
* Insert a backdated purchase entry into the double-entry ledger.
|
|
8
|
+
* Uses post() with postedAt override to simulate historical purchases.
|
|
9
|
+
*/
|
|
10
|
+
async function insertPurchase(ledger, tenantId, amountCents, postedAt) {
|
|
10
11
|
const amount = Credit.fromCents(amountCents);
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
// Purchase: DR cash (1000), CR unearned_revenue (2000:<tenantId>)
|
|
13
|
+
await ledger.post({
|
|
14
|
+
entryType: "purchase",
|
|
13
15
|
tenantId,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
description: `Test purchase ${amountCents}¢`,
|
|
17
|
+
referenceId: `test-purchase:${tenantId}:${postedAt}:${Math.random()}`,
|
|
18
|
+
postedAt,
|
|
19
|
+
lines: [
|
|
20
|
+
{ accountCode: CREDIT_TYPE_ACCOUNT.purchase, amount, side: "debit" },
|
|
21
|
+
{ accountCode: `2000:${tenantId}`, amount, side: "credit" },
|
|
22
|
+
],
|
|
18
23
|
});
|
|
19
|
-
// Upsert credit_balances
|
|
20
|
-
const existing = await db
|
|
21
|
-
.select()
|
|
22
|
-
.from(creditBalances)
|
|
23
|
-
.where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
|
|
24
|
-
if (existing.length > 0) {
|
|
25
|
-
await db
|
|
26
|
-
.update(creditBalances)
|
|
27
|
-
.set({ balance: existing[0].balance.add(amount) })
|
|
28
|
-
.where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
await db.insert(creditBalances).values({ tenantId, balance: amount });
|
|
32
|
-
}
|
|
33
24
|
}
|
|
34
25
|
describe("runDividendCron", () => {
|
|
35
26
|
let pool;
|
|
36
27
|
let db;
|
|
37
28
|
let ledger;
|
|
38
|
-
let creditTransactionRepo;
|
|
39
29
|
beforeAll(async () => {
|
|
40
30
|
({ db, pool } = await createTestDb());
|
|
41
31
|
});
|
|
@@ -44,12 +34,11 @@ describe("runDividendCron", () => {
|
|
|
44
34
|
});
|
|
45
35
|
beforeEach(async () => {
|
|
46
36
|
await truncateAllTables(pool);
|
|
47
|
-
ledger = new
|
|
48
|
-
|
|
37
|
+
ledger = new DrizzleLedger(db);
|
|
38
|
+
await ledger.seedSystemAccounts();
|
|
49
39
|
});
|
|
50
40
|
function makeConfig(overrides) {
|
|
51
41
|
return {
|
|
52
|
-
creditTransactionRepo,
|
|
53
42
|
ledger,
|
|
54
43
|
matchRate: 1.0,
|
|
55
44
|
targetDate: "2026-02-20",
|
|
@@ -57,7 +46,7 @@ describe("runDividendCron", () => {
|
|
|
57
46
|
};
|
|
58
47
|
}
|
|
59
48
|
it("distributes dividend to eligible tenants", async () => {
|
|
60
|
-
await insertPurchase(
|
|
49
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
61
50
|
const result = await runDividendCron(makeConfig());
|
|
62
51
|
expect(result.distributed).toBe(1);
|
|
63
52
|
expect(result.pool.toCents()).toBe(1000);
|
|
@@ -65,7 +54,7 @@ describe("runDividendCron", () => {
|
|
|
65
54
|
expect(result.activeCount).toBe(1);
|
|
66
55
|
});
|
|
67
56
|
it("is idempotent — skips if already ran for the date", async () => {
|
|
68
|
-
await insertPurchase(
|
|
57
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
69
58
|
const result1 = await runDividendCron(makeConfig());
|
|
70
59
|
expect(result1.distributed).toBe(1);
|
|
71
60
|
expect(result1.skippedAlreadyRun).toBe(false);
|
|
@@ -76,20 +65,19 @@ describe("runDividendCron", () => {
|
|
|
76
65
|
expect((await ledger.balance("t1")).equals(balanceAfterFirst)).toBe(true);
|
|
77
66
|
});
|
|
78
67
|
it("handles floor rounding — remainder is not distributed", async () => {
|
|
79
|
-
await insertPurchase(
|
|
80
|
-
await insertPurchase(
|
|
81
|
-
await insertPurchase(
|
|
68
|
+
await insertPurchase(ledger, "t1", 50, "2026-02-20 12:00:00");
|
|
69
|
+
await insertPurchase(ledger, "t2", 30, "2026-02-20 12:00:00");
|
|
70
|
+
await insertPurchase(ledger, "t3", 20, "2026-02-20 12:00:00");
|
|
82
71
|
const result = await runDividendCron(makeConfig());
|
|
83
72
|
expect(result.pool.toCents()).toBe(100);
|
|
84
73
|
expect(result.activeCount).toBe(3);
|
|
85
74
|
// Nanodollar precision: floor(1_000_000_000 raw / 3) = 333_333_333 raw each
|
|
86
|
-
// Remainder = 1 nanodollar (not 1 cent — far less wasted with higher scale)
|
|
87
75
|
expect(result.perUser.toRaw()).toBe(333_333_333);
|
|
88
76
|
expect(result.distributed).toBe(3);
|
|
89
77
|
});
|
|
90
78
|
it("skips distribution when pool is zero", async () => {
|
|
91
79
|
// Tenant purchased within 7 days but NOT on target date -> pool = 0
|
|
92
|
-
await insertPurchase(
|
|
80
|
+
await insertPurchase(ledger, "t1", 500, "2026-02-18 12:00:00");
|
|
93
81
|
const result = await runDividendCron(makeConfig());
|
|
94
82
|
expect(result.pool.toCents()).toBe(0);
|
|
95
83
|
expect(result.activeCount).toBe(1);
|
|
@@ -99,9 +87,9 @@ describe("runDividendCron", () => {
|
|
|
99
87
|
it("distributes sub-cent amounts at nanodollar precision", async () => {
|
|
100
88
|
// 1 cent purchase, 3 active users: pool = 10_000_000 raw
|
|
101
89
|
// floor(10_000_000 / 3) = 3_333_333 raw each — non-zero, gets distributed
|
|
102
|
-
await insertPurchase(
|
|
103
|
-
await insertPurchase(
|
|
104
|
-
await insertPurchase(
|
|
90
|
+
await insertPurchase(ledger, "t1", 1, "2026-02-20 12:00:00");
|
|
91
|
+
await insertPurchase(ledger, "t2", 500, "2026-02-18 12:00:00");
|
|
92
|
+
await insertPurchase(ledger, "t3", 500, "2026-02-17 12:00:00");
|
|
105
93
|
const result = await runDividendCron(makeConfig({ matchRate: 1.0 }));
|
|
106
94
|
expect(result.pool.toCents()).toBe(1);
|
|
107
95
|
expect(result.activeCount).toBe(3);
|
|
@@ -109,18 +97,17 @@ describe("runDividendCron", () => {
|
|
|
109
97
|
expect(result.distributed).toBe(3);
|
|
110
98
|
});
|
|
111
99
|
it("records transactions with correct type and referenceId", async () => {
|
|
112
|
-
await insertPurchase(
|
|
100
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
113
101
|
await runDividendCron(makeConfig());
|
|
114
102
|
const history = await ledger.history("t1", { type: "community_dividend" });
|
|
115
103
|
expect(history).toHaveLength(1);
|
|
116
|
-
expect(history[0].
|
|
104
|
+
expect(history[0].entryType).toBe("community_dividend");
|
|
117
105
|
expect(history[0].referenceId).toBe("dividend:2026-02-20:t1");
|
|
118
|
-
expect(history[0].amount.toCents()).toBe(1000);
|
|
119
106
|
expect(history[0].description).toContain("Community dividend");
|
|
120
107
|
});
|
|
121
108
|
it("collects errors without stopping distribution to other tenants", async () => {
|
|
122
|
-
await insertPurchase(
|
|
123
|
-
await insertPurchase(
|
|
109
|
+
await insertPurchase(ledger, "t1", 500, "2026-02-20 12:00:00");
|
|
110
|
+
await insertPurchase(ledger, "t2", 500, "2026-02-20 12:00:00");
|
|
124
111
|
const result = await runDividendCron(makeConfig());
|
|
125
112
|
expect(result.distributed).toBe(2);
|
|
126
113
|
expect(result.errors).toEqual([]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { and, desc, eq, gte, lt, sql } from "drizzle-orm";
|
|
2
2
|
import { adminUsers } from "../db/schema/admin-users.js";
|
|
3
|
-
import { creditTransactions } from "../db/schema/credits.js";
|
|
4
3
|
import { dividendDistributions } from "../db/schema/dividend-distributions.js";
|
|
4
|
+
import { journalEntries, journalLines } from "../db/schema/ledger.js";
|
|
5
5
|
import { Credit } from "./credit.js";
|
|
6
6
|
export class DrizzleDividendRepository {
|
|
7
7
|
db;
|
|
@@ -9,24 +9,24 @@ export class DrizzleDividendRepository {
|
|
|
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
|
+
// In double-entry: purchase entries have a credit line on the tenant liability account.
|
|
14
|
+
// Sum those credit line amounts for entries posted yesterday.
|
|
13
15
|
const poolRow = (await this.db
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
.where(and(eq(
|
|
16
|
+
.select({ total: sql `COALESCE(SUM(${journalLines.amount}), 0)` })
|
|
17
|
+
.from(journalLines)
|
|
18
|
+
.innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
|
|
19
|
+
.where(and(eq(journalEntries.entryType, "purchase"), eq(journalLines.side, "credit"),
|
|
18
20
|
// raw SQL: Drizzle cannot express date_trunc with interval arithmetic
|
|
19
|
-
sql `${
|
|
20
|
-
const
|
|
21
|
-
const pool = Credit.fromCents(poolCents);
|
|
21
|
+
sql `${journalEntries.postedAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`, sql `${journalEntries.postedAt}::timestamp < date_trunc('day', timezone('UTC', now()))`)))[0];
|
|
22
|
+
const pool = Credit.fromRaw(Number(poolRow?.total ?? 0));
|
|
22
23
|
// 2. Active users = distinct tenants with a purchase in the last 7 days
|
|
23
24
|
const activeRow = (await this.db
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
.where(and(eq(creditTransactions.type, "purchase"),
|
|
25
|
+
.select({ count: sql `COUNT(DISTINCT ${journalEntries.tenantId})` })
|
|
26
|
+
.from(journalEntries)
|
|
27
|
+
.where(and(eq(journalEntries.entryType, "purchase"),
|
|
28
28
|
// raw SQL: Drizzle cannot express timestamp comparison with interval arithmetic
|
|
29
|
-
sql `${
|
|
29
|
+
sql `${journalEntries.postedAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`)))[0];
|
|
30
30
|
const activeUsers = activeRow?.count ?? 0;
|
|
31
31
|
// 3. Per-user projection (avoid division by zero)
|
|
32
32
|
const perUser = activeUsers > 0 ? Credit.fromRaw(Math.floor(pool.toRaw() / activeUsers)) : Credit.ZERO;
|
|
@@ -36,19 +36,16 @@ export class DrizzleDividendRepository {
|
|
|
36
36
|
const nextDistributionAt = nextMidnight.toISOString();
|
|
37
37
|
// 5. User eligibility — last purchase within 7 days
|
|
38
38
|
const userPurchaseRow = (await this.db
|
|
39
|
-
.select({
|
|
40
|
-
.from(
|
|
41
|
-
.where(and(eq(
|
|
42
|
-
.orderBy(desc(
|
|
39
|
+
.select({ postedAt: journalEntries.postedAt })
|
|
40
|
+
.from(journalEntries)
|
|
41
|
+
.where(and(eq(journalEntries.tenantId, tenantId), eq(journalEntries.entryType, "purchase")))
|
|
42
|
+
.orderBy(desc(journalEntries.postedAt))
|
|
43
43
|
.limit(1))[0];
|
|
44
44
|
let userEligible = false;
|
|
45
45
|
let userLastPurchaseAt = null;
|
|
46
46
|
let userWindowExpiresAt = null;
|
|
47
47
|
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);
|
|
48
|
+
const lastPurchase = new Date(userPurchaseRow.postedAt);
|
|
52
49
|
userLastPurchaseAt = lastPurchase.toISOString();
|
|
53
50
|
const windowExpiry = new Date(lastPurchase.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
54
51
|
userWindowExpiresAt = windowExpiry.toISOString();
|
|
@@ -4,8 +4,8 @@ import { adminUsers } from "../db/schema/admin-users.js";
|
|
|
4
4
|
import { dividendDistributions } from "../db/schema/dividend-distributions.js";
|
|
5
5
|
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
6
6
|
import { Credit } from "./credit.js";
|
|
7
|
-
import { CreditLedger } from "./credit-ledger.js";
|
|
8
7
|
import { DrizzleDividendRepository } from "./dividend-repository.js";
|
|
8
|
+
import { DrizzleLedger } from "./ledger.js";
|
|
9
9
|
let pool;
|
|
10
10
|
let db;
|
|
11
11
|
beforeAll(async () => {
|
|
@@ -40,6 +40,7 @@ describe("DrizzleDividendRepository", () => {
|
|
|
40
40
|
let repo;
|
|
41
41
|
beforeEach(async () => {
|
|
42
42
|
await truncateAllTables(pool);
|
|
43
|
+
await new DrizzleLedger(db).seedSystemAccounts();
|
|
43
44
|
repo = new DrizzleDividendRepository(db);
|
|
44
45
|
});
|
|
45
46
|
// --- getHistory() ---
|
|
@@ -153,8 +154,8 @@ describe("DrizzleDividendRepository", () => {
|
|
|
153
154
|
expect(stats.nextDistributionAt).toEqual(expect.any(String));
|
|
154
155
|
});
|
|
155
156
|
it("marks user as eligible when they have a recent purchase", async () => {
|
|
156
|
-
const ledger = new
|
|
157
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase", "recent buy");
|
|
157
|
+
const ledger = new DrizzleLedger(db);
|
|
158
|
+
await ledger.credit("t1", Credit.fromCents(100), "purchase", { description: "recent buy" });
|
|
158
159
|
const stats = await repo.getStats("t1");
|
|
159
160
|
expect(stats.userEligible).toBe(true);
|
|
160
161
|
expect(stats.userLastPurchaseAt).toEqual(expect.any(String));
|
package/dist/credits/index.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS,
|
|
|
3
3
|
export { Credit } from "./credit.js";
|
|
4
4
|
export type { CreditExpiryCronConfig, CreditExpiryCronResult } from "./credit-expiry-cron.js";
|
|
5
5
|
export { runCreditExpiryCron } from "./credit-expiry-cron.js";
|
|
6
|
-
export type {
|
|
7
|
-
export {
|
|
6
|
+
export type { AccountType, CreditOpts, CreditType, DebitOpts, DebitType, HistoryOptions, ILedger, JournalEntry, JournalLine, MemberUsageSummary, PostEntryInput, Side, SystemAccount, TransactionType, TrialBalance, } from "./ledger.js";
|
|
7
|
+
export { CREDIT_TYPE_ACCOUNT, DEBIT_TYPE_ACCOUNT, DrizzleLedger, InsufficientBalanceError, Ledger, SYSTEM_ACCOUNTS, } from "./ledger.js";
|
|
8
8
|
export { grantSignupCredits, SIGNUP_GRANT } from "./signup-grant.js";
|
|
9
9
|
export type { ITenantCustomerRepository, TenantCustomerRow } from "./tenant-customer-repository.js";
|
|
10
|
+
export type { TrialBalanceCronConfig, TrialBalanceCronResult } from "./trial-balance-cron.js";
|
|
11
|
+
export { runTrialBalanceCron } from "./trial-balance-cron.js";
|
package/dist/credits/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS, computeNextScheduleAt, DrizzleAutoTopupSettingsRepository, } from "./auto-topup-settings-repository.js";
|
|
2
2
|
export { Credit } from "./credit.js";
|
|
3
3
|
export { runCreditExpiryCron } from "./credit-expiry-cron.js";
|
|
4
|
-
export {
|
|
4
|
+
export { CREDIT_TYPE_ACCOUNT, DEBIT_TYPE_ACCOUNT, DrizzleLedger, InsufficientBalanceError, Ledger, SYSTEM_ACCOUNTS, } from "./ledger.js";
|
|
5
5
|
export { grantSignupCredits, SIGNUP_GRANT } from "./signup-grant.js";
|
|
6
|
+
export { runTrialBalanceCron } from "./trial-balance-cron.js";
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Double-entry credit ledger.
|
|
3
|
+
*
|
|
4
|
+
* Every mutation posts a balanced journal entry: sum(debits) === sum(credits).
|
|
5
|
+
* A tenant's "credit balance" is the balance of their unearned_revenue liability account.
|
|
6
|
+
*
|
|
7
|
+
* Account model:
|
|
8
|
+
* ASSETS — cash, stripe_receivable
|
|
9
|
+
* LIABILITIES — unearned_revenue:<tenant_id> (the "credit balance")
|
|
10
|
+
* REVENUE — revenue:bot_runtime, revenue:adapter_usage, etc.
|
|
11
|
+
* EXPENSES — expense:signup_grant, expense:admin_grant, expense:promo, etc.
|
|
12
|
+
* EQUITY — retained_earnings
|
|
13
|
+
*/
|
|
14
|
+
import type { PlatformDb } from "../db/index.js";
|
|
15
|
+
import { Credit } from "./credit.js";
|
|
16
|
+
export type CreditType = "signup_grant" | "admin_grant" | "purchase" | "bounty" | "referral" | "promo" | "community_dividend" | "affiliate_bonus" | "affiliate_match" | "correction";
|
|
17
|
+
export type DebitType = "bot_runtime" | "adapter_usage" | "addon" | "refund" | "correction" | "resource_upgrade" | "storage_upgrade" | "onboarding_llm" | "credit_expiry";
|
|
18
|
+
export type TransactionType = CreditType | DebitType;
|
|
19
|
+
export type AccountType = "asset" | "liability" | "equity" | "revenue" | "expense";
|
|
20
|
+
export type Side = "debit" | "credit";
|
|
21
|
+
export interface JournalLine {
|
|
22
|
+
accountCode: string;
|
|
23
|
+
amount: Credit;
|
|
24
|
+
side: Side;
|
|
25
|
+
}
|
|
26
|
+
export interface PostEntryInput {
|
|
27
|
+
entryType: string;
|
|
28
|
+
tenantId: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
referenceId?: string;
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
createdBy?: string;
|
|
33
|
+
/** Override the posted_at timestamp (useful in tests to backdate entries). */
|
|
34
|
+
postedAt?: string;
|
|
35
|
+
lines: JournalLine[];
|
|
36
|
+
/**
|
|
37
|
+
* When set, verifies inside the transaction (after acquiring row locks) that
|
|
38
|
+
* the tenant's balance >= amount. Throws InsufficientBalanceError otherwise.
|
|
39
|
+
* Use this instead of a pre-check outside the transaction (TOCTOU-safe).
|
|
40
|
+
*/
|
|
41
|
+
balanceCheck?: {
|
|
42
|
+
tenantId: string;
|
|
43
|
+
amount: Credit;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export interface JournalEntry {
|
|
47
|
+
id: string;
|
|
48
|
+
postedAt: string;
|
|
49
|
+
entryType: string;
|
|
50
|
+
tenantId: string;
|
|
51
|
+
description: string | null;
|
|
52
|
+
referenceId: string | null;
|
|
53
|
+
metadata: Record<string, unknown> | null;
|
|
54
|
+
lines: Array<{
|
|
55
|
+
accountCode: string;
|
|
56
|
+
amount: Credit;
|
|
57
|
+
side: Side;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
/** Thrown when a debit would exceed a tenant's credit balance. */
|
|
61
|
+
export declare class InsufficientBalanceError extends Error {
|
|
62
|
+
currentBalance: Credit;
|
|
63
|
+
requestedAmount: Credit;
|
|
64
|
+
constructor(currentBalance: Credit, requestedAmount: Credit);
|
|
65
|
+
}
|
|
66
|
+
export interface HistoryOptions {
|
|
67
|
+
limit?: number;
|
|
68
|
+
offset?: number;
|
|
69
|
+
type?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface MemberUsageSummary {
|
|
72
|
+
userId: string;
|
|
73
|
+
totalDebit: Credit;
|
|
74
|
+
transactionCount: number;
|
|
75
|
+
}
|
|
76
|
+
export interface TrialBalance {
|
|
77
|
+
totalDebits: Credit;
|
|
78
|
+
totalCredits: Credit;
|
|
79
|
+
balanced: boolean;
|
|
80
|
+
difference: Credit;
|
|
81
|
+
}
|
|
82
|
+
export interface CreditOpts {
|
|
83
|
+
description?: string;
|
|
84
|
+
referenceId?: string;
|
|
85
|
+
fundingSource?: string;
|
|
86
|
+
stripeFingerprint?: string;
|
|
87
|
+
attributedUserId?: string;
|
|
88
|
+
expiresAt?: string;
|
|
89
|
+
createdBy?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface DebitOpts {
|
|
92
|
+
description?: string;
|
|
93
|
+
referenceId?: string;
|
|
94
|
+
allowNegative?: boolean;
|
|
95
|
+
attributedUserId?: string;
|
|
96
|
+
createdBy?: string;
|
|
97
|
+
}
|
|
98
|
+
/** Maps credit (money-in) types to the debit-side account code. */
|
|
99
|
+
export declare const CREDIT_TYPE_ACCOUNT: Record<CreditType, string>;
|
|
100
|
+
/** Maps debit (money-out) types to the credit-side account code. */
|
|
101
|
+
export declare const DEBIT_TYPE_ACCOUNT: Record<DebitType, string>;
|
|
102
|
+
export interface SystemAccount {
|
|
103
|
+
code: string;
|
|
104
|
+
name: string;
|
|
105
|
+
type: AccountType;
|
|
106
|
+
normalSide: Side;
|
|
107
|
+
}
|
|
108
|
+
export declare const SYSTEM_ACCOUNTS: SystemAccount[];
|
|
109
|
+
export interface ILedger {
|
|
110
|
+
/** Post a balanced journal entry. The primitive. Everything else calls this. */
|
|
111
|
+
post(input: PostEntryInput): Promise<JournalEntry>;
|
|
112
|
+
/** Add credits to a tenant (posts balanced entry: DR source, CR unearned_revenue). */
|
|
113
|
+
credit(tenantId: string, amount: Credit, type: CreditType, opts?: CreditOpts): Promise<JournalEntry>;
|
|
114
|
+
/** Deduct credits from a tenant (posts balanced entry: DR unearned_revenue, CR revenue). */
|
|
115
|
+
debit(tenantId: string, amount: Credit, type: DebitType, opts?: DebitOpts): Promise<JournalEntry>;
|
|
116
|
+
/** Tenant's credit balance (= their unearned_revenue liability account balance). */
|
|
117
|
+
balance(tenantId: string): Promise<Credit>;
|
|
118
|
+
/** Check if a reference ID has already been posted (idempotency). */
|
|
119
|
+
hasReferenceId(referenceId: string): Promise<boolean>;
|
|
120
|
+
/** Journal entries for a tenant, newest first. */
|
|
121
|
+
history(tenantId: string, opts?: HistoryOptions): Promise<JournalEntry[]>;
|
|
122
|
+
/** All tenants with positive credit balance. */
|
|
123
|
+
tenantsWithBalance(): Promise<Array<{
|
|
124
|
+
tenantId: string;
|
|
125
|
+
balance: Credit;
|
|
126
|
+
}>>;
|
|
127
|
+
/** Per-member debit totals for a tenant. */
|
|
128
|
+
memberUsage(tenantId: string): Promise<MemberUsageSummary[]>;
|
|
129
|
+
/** Sum of all debits for a tenant (absolute value). */
|
|
130
|
+
lifetimeSpend(tenantId: string): Promise<Credit>;
|
|
131
|
+
/** Batch lifetimeSpend for multiple tenants. */
|
|
132
|
+
lifetimeSpendBatch(tenantIds: string[]): Promise<Map<string, Credit>>;
|
|
133
|
+
/** Expired credit grants not yet clawed back. */
|
|
134
|
+
expiredCredits(now: string): Promise<Array<{
|
|
135
|
+
entryId: string;
|
|
136
|
+
tenantId: string;
|
|
137
|
+
amount: Credit;
|
|
138
|
+
}>>;
|
|
139
|
+
/** Verify the books balance: total debits === total credits across all lines. */
|
|
140
|
+
trialBalance(): Promise<TrialBalance>;
|
|
141
|
+
/** Balance of any account by code. */
|
|
142
|
+
accountBalance(accountCode: string): Promise<Credit>;
|
|
143
|
+
/** Ensure system accounts exist (idempotent, called at startup). */
|
|
144
|
+
seedSystemAccounts(): Promise<void>;
|
|
145
|
+
/** Check if any journal entry has a referenceId matching a LIKE pattern (for dividend idempotency). */
|
|
146
|
+
existsByReferenceIdLike(pattern: string): Promise<boolean>;
|
|
147
|
+
/** Sum all purchase-type entry amounts credited to tenant accounts in [startTs, endTs). */
|
|
148
|
+
sumPurchasesForPeriod(startTs: string, endTs: string): Promise<Credit>;
|
|
149
|
+
/** Get distinct tenantIds with a purchase entry in [startTs, endTs). */
|
|
150
|
+
getActiveTenantIdsInWindow(startTs: string, endTs: string): Promise<string[]>;
|
|
151
|
+
}
|
|
152
|
+
export declare class DrizzleLedger implements ILedger {
|
|
153
|
+
private readonly db;
|
|
154
|
+
constructor(db: PlatformDb);
|
|
155
|
+
seedSystemAccounts(): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Get or create the per-tenant unearned_revenue liability account, then lock
|
|
158
|
+
* it for the duration of the surrounding transaction.
|
|
159
|
+
* Code format: `2000:<tenantId>`
|
|
160
|
+
*
|
|
161
|
+
* Uses INSERT ON CONFLICT DO NOTHING so concurrent first-time calls for the
|
|
162
|
+
* same tenant are idempotent (no unique-constraint crash on the second writer).
|
|
163
|
+
*/
|
|
164
|
+
private ensureTenantAccountLocked;
|
|
165
|
+
/**
|
|
166
|
+
* Resolve account code → account id.
|
|
167
|
+
* Acquires FOR UPDATE locks on both the accounts row and the account_balances
|
|
168
|
+
* row so concurrent transactions are fully serialized on balance reads/writes.
|
|
169
|
+
*/
|
|
170
|
+
private resolveAccountLocked;
|
|
171
|
+
post(input: PostEntryInput): Promise<JournalEntry>;
|
|
172
|
+
credit(tenantId: string, amount: Credit, type: CreditType, opts?: CreditOpts): Promise<JournalEntry>;
|
|
173
|
+
debit(tenantId: string, amount: Credit, type: DebitType, opts?: DebitOpts): Promise<JournalEntry>;
|
|
174
|
+
balance(tenantId: string): Promise<Credit>;
|
|
175
|
+
accountBalance(accountCode: string): Promise<Credit>;
|
|
176
|
+
hasReferenceId(referenceId: string): Promise<boolean>;
|
|
177
|
+
history(tenantId: string, opts?: HistoryOptions): Promise<JournalEntry[]>;
|
|
178
|
+
tenantsWithBalance(): Promise<Array<{
|
|
179
|
+
tenantId: string;
|
|
180
|
+
balance: Credit;
|
|
181
|
+
}>>;
|
|
182
|
+
memberUsage(tenantId: string): Promise<MemberUsageSummary[]>;
|
|
183
|
+
lifetimeSpend(tenantId: string): Promise<Credit>;
|
|
184
|
+
lifetimeSpendBatch(tenantIds: string[]): Promise<Map<string, Credit>>;
|
|
185
|
+
expiredCredits(now: string): Promise<Array<{
|
|
186
|
+
entryId: string;
|
|
187
|
+
tenantId: string;
|
|
188
|
+
amount: Credit;
|
|
189
|
+
}>>;
|
|
190
|
+
existsByReferenceIdLike(pattern: string): Promise<boolean>;
|
|
191
|
+
sumPurchasesForPeriod(startTs: string, endTs: string): Promise<Credit>;
|
|
192
|
+
getActiveTenantIdsInWindow(startTs: string, endTs: string): Promise<string[]>;
|
|
193
|
+
trialBalance(): Promise<TrialBalance>;
|
|
194
|
+
}
|
|
195
|
+
export { DrizzleLedger as Ledger };
|