@wopr-network/platform-core 0.1.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/biome.json +61 -0
- package/dist/admin/admin-audit-log-repository.d.ts +33 -0
- package/dist/admin/admin-audit-log-repository.js +102 -0
- package/dist/admin/audit-log.d.ts +49 -0
- package/dist/admin/audit-log.js +63 -0
- package/dist/admin/index.d.ts +6 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/role-store.d.ts +37 -0
- package/dist/admin/role-store.js +106 -0
- package/dist/auth/api-key-repository.d.ts +11 -0
- package/dist/auth/api-key-repository.js +33 -0
- package/dist/auth/api-key-repository.test.d.ts +1 -0
- package/dist/auth/api-key-repository.test.js +46 -0
- package/dist/auth/auth.test.d.ts +1 -0
- package/dist/auth/auth.test.js +140 -0
- package/dist/auth/better-auth.d.ts +42 -0
- package/dist/auth/better-auth.js +196 -0
- package/dist/auth/index.d.ts +186 -0
- package/dist/auth/index.js +422 -0
- package/dist/auth/login-history-repository.d.ts +14 -0
- package/dist/auth/login-history-repository.js +15 -0
- package/dist/auth/login-history-repository.test.d.ts +1 -0
- package/dist/auth/login-history-repository.test.js +47 -0
- package/dist/auth/middleware.d.ts +55 -0
- package/dist/auth/middleware.js +101 -0
- package/dist/auth/middleware.test.d.ts +1 -0
- package/dist/auth/middleware.test.js +213 -0
- package/dist/auth/scoped-tokens.test.d.ts +1 -0
- package/dist/auth/scoped-tokens.test.js +306 -0
- package/dist/auth/tenant-access.test.d.ts +1 -0
- package/dist/auth/tenant-access.test.js +62 -0
- package/dist/auth/user-creator.d.ts +9 -0
- package/dist/auth/user-creator.js +47 -0
- package/dist/auth/user-creator.test.d.ts +1 -0
- package/dist/auth/user-creator.test.js +78 -0
- package/dist/auth/user-role-repository.d.ts +31 -0
- package/dist/auth/user-role-repository.js +53 -0
- package/dist/auth/user-role-repository.test.d.ts +1 -0
- package/dist/auth/user-role-repository.test.js +122 -0
- package/dist/billing/drizzle-webhook-seen-repository.d.ts +10 -0
- package/dist/billing/drizzle-webhook-seen-repository.js +28 -0
- package/dist/billing/index.d.ts +7 -0
- package/dist/billing/index.js +7 -0
- package/dist/billing/payment-processor.d.ts +127 -0
- package/dist/billing/payment-processor.js +8 -0
- package/dist/billing/payment-processor.test.d.ts +1 -0
- package/dist/billing/payment-processor.test.js +71 -0
- package/dist/billing/payram/cents-credits-boundary.test.d.ts +1 -0
- package/dist/billing/payram/cents-credits-boundary.test.js +75 -0
- package/dist/billing/payram/charge-store.d.ts +41 -0
- package/dist/billing/payram/charge-store.js +72 -0
- package/dist/billing/payram/charge-store.test.d.ts +1 -0
- package/dist/billing/payram/charge-store.test.js +64 -0
- package/dist/billing/payram/checkout.d.ts +15 -0
- package/dist/billing/payram/checkout.js +24 -0
- package/dist/billing/payram/checkout.test.d.ts +1 -0
- package/dist/billing/payram/checkout.test.js +74 -0
- package/dist/billing/payram/client.d.ts +7 -0
- package/dist/billing/payram/client.js +15 -0
- package/dist/billing/payram/client.test.d.ts +1 -0
- package/dist/billing/payram/client.test.js +52 -0
- package/dist/billing/payram/index.d.ts +8 -0
- package/dist/billing/payram/index.js +4 -0
- package/dist/billing/payram/types.d.ts +40 -0
- package/dist/billing/payram/types.js +1 -0
- package/dist/billing/payram/webhook.d.ts +19 -0
- package/dist/billing/payram/webhook.js +67 -0
- package/dist/billing/payram/webhook.test.d.ts +7 -0
- package/dist/billing/payram/webhook.test.js +248 -0
- package/dist/billing/stripe/cents-credits-boundary.test.d.ts +1 -0
- package/dist/billing/stripe/cents-credits-boundary.test.js +62 -0
- package/dist/billing/stripe/checkout.d.ts +20 -0
- package/dist/billing/stripe/checkout.js +63 -0
- package/dist/billing/stripe/checkout.test.d.ts +1 -0
- package/dist/billing/stripe/checkout.test.js +148 -0
- package/dist/billing/stripe/client.d.ts +14 -0
- package/dist/billing/stripe/client.js +33 -0
- package/dist/billing/stripe/client.test.d.ts +1 -0
- package/dist/billing/stripe/client.test.js +58 -0
- package/dist/billing/stripe/credit-prices.d.ts +63 -0
- package/dist/billing/stripe/credit-prices.js +81 -0
- package/dist/billing/stripe/credit-prices.test.d.ts +1 -0
- package/dist/billing/stripe/credit-prices.test.js +87 -0
- package/dist/billing/stripe/index.d.ts +14 -0
- package/dist/billing/stripe/index.js +8 -0
- package/dist/billing/stripe/payment-methods-detach-all.test.d.ts +1 -0
- package/dist/billing/stripe/payment-methods-detach-all.test.js +40 -0
- package/dist/billing/stripe/payment-methods.d.ts +25 -0
- package/dist/billing/stripe/payment-methods.js +53 -0
- package/dist/billing/stripe/payment-methods.test.d.ts +1 -0
- package/dist/billing/stripe/payment-methods.test.js +122 -0
- package/dist/billing/stripe/portal.d.ts +10 -0
- package/dist/billing/stripe/portal.js +16 -0
- package/dist/billing/stripe/portal.test.d.ts +1 -0
- package/dist/billing/stripe/portal.test.js +48 -0
- package/dist/billing/stripe/setup-intent.d.ts +16 -0
- package/dist/billing/stripe/setup-intent.js +22 -0
- package/dist/billing/stripe/setup-intent.test.d.ts +1 -0
- package/dist/billing/stripe/setup-intent.test.js +58 -0
- package/dist/billing/stripe/stripe-payment-processor.d.ts +49 -0
- package/dist/billing/stripe/stripe-payment-processor.js +166 -0
- package/dist/billing/stripe/stripe-payment-processor.test.d.ts +1 -0
- package/dist/billing/stripe/stripe-payment-processor.test.js +413 -0
- package/dist/billing/stripe/tenant-store.d.ts +56 -0
- package/dist/billing/stripe/tenant-store.js +119 -0
- package/dist/billing/stripe/tenant-store.test.d.ts +1 -0
- package/dist/billing/stripe/tenant-store.test.js +97 -0
- package/dist/billing/stripe/types.d.ts +49 -0
- package/dist/billing/stripe/types.js +1 -0
- package/dist/billing/webhook-seen-repository.d.ts +14 -0
- package/dist/billing/webhook-seen-repository.js +13 -0
- package/dist/config/billing-env.test.d.ts +1 -0
- package/dist/config/billing-env.test.js +48 -0
- package/dist/config/index.d.ts +46 -0
- package/dist/config/index.js +38 -0
- package/dist/config/logger.d.ts +2 -0
- package/dist/config/logger.js +11 -0
- package/dist/config/provider-endpoints.d.ts +6 -0
- package/dist/config/provider-endpoints.js +12 -0
- package/dist/credits/auto-topup-charge.d.ts +27 -0
- package/dist/credits/auto-topup-charge.js +139 -0
- package/dist/credits/auto-topup-charge.test.d.ts +1 -0
- package/dist/credits/auto-topup-charge.test.js +242 -0
- package/dist/credits/auto-topup-event-log-repository.d.ts +16 -0
- package/dist/credits/auto-topup-event-log-repository.js +18 -0
- package/dist/credits/auto-topup-event-log-repository.test.d.ts +1 -0
- package/dist/credits/auto-topup-event-log-repository.test.js +83 -0
- package/dist/credits/auto-topup-schedule.d.ts +27 -0
- package/dist/credits/auto-topup-schedule.js +66 -0
- package/dist/credits/auto-topup-schedule.test.d.ts +1 -0
- package/dist/credits/auto-topup-schedule.test.js +145 -0
- package/dist/credits/auto-topup-settings-repository.d.ts +54 -0
- package/dist/credits/auto-topup-settings-repository.js +184 -0
- package/dist/credits/auto-topup-settings-repository.test.d.ts +1 -0
- package/dist/credits/auto-topup-settings-repository.test.js +104 -0
- package/dist/credits/auto-topup-usage.d.ts +22 -0
- package/dist/credits/auto-topup-usage.js +56 -0
- package/dist/credits/auto-topup-usage.test.d.ts +1 -0
- package/dist/credits/auto-topup-usage.test.js +181 -0
- package/dist/credits/credit-expiry-cron.d.ts +19 -0
- package/dist/credits/credit-expiry-cron.js +50 -0
- package/dist/credits/credit-expiry-cron.test.d.ts +1 -0
- package/dist/credits/credit-expiry-cron.test.js +67 -0
- package/dist/credits/credit-ledger-extra.test.d.ts +1 -0
- package/dist/credits/credit-ledger-extra.test.js +40 -0
- package/dist/credits/credit-ledger.bench.d.ts +1 -0
- package/dist/credits/credit-ledger.bench.js +33 -0
- package/dist/credits/credit-ledger.d.ts +130 -0
- package/dist/credits/credit-ledger.js +293 -0
- package/dist/credits/credit-ledger.test.d.ts +4 -0
- package/dist/credits/credit-ledger.test.js +203 -0
- package/dist/credits/credit-transaction-repository.d.ts +17 -0
- package/dist/credits/credit-transaction-repository.js +35 -0
- package/dist/credits/credit-transaction-repository.test.d.ts +1 -0
- package/dist/credits/credit-transaction-repository.test.js +232 -0
- package/dist/credits/credit.d.ts +75 -0
- package/dist/credits/credit.js +139 -0
- package/dist/credits/credit.test.d.ts +1 -0
- package/dist/credits/credit.test.js +196 -0
- package/dist/credits/dividend-cron.d.ts +29 -0
- package/dist/credits/dividend-cron.js +88 -0
- package/dist/credits/dividend-cron.test.d.ts +1 -0
- package/dist/credits/dividend-cron.test.js +128 -0
- package/dist/credits/dividend-repository.d.ts +29 -0
- package/dist/credits/dividend-repository.js +126 -0
- package/dist/credits/dividend-repository.test.d.ts +1 -0
- package/dist/credits/dividend-repository.test.js +176 -0
- package/dist/credits/index.d.ts +9 -0
- package/dist/credits/index.js +5 -0
- package/dist/credits/repository-types.d.ts +29 -0
- package/dist/credits/repository-types.js +1 -0
- package/dist/credits/signup-grant.d.ts +12 -0
- package/dist/credits/signup-grant.js +35 -0
- package/dist/credits/signup-grant.test.d.ts +1 -0
- package/dist/credits/signup-grant.test.js +51 -0
- package/dist/credits/tenant-customer-repository.d.ts +30 -0
- package/dist/credits/tenant-customer-repository.js +5 -0
- package/dist/db/auth-user-repository.d.ts +46 -0
- package/dist/db/auth-user-repository.js +90 -0
- package/dist/db/credit-column.d.ts +27 -0
- package/dist/db/credit-column.js +13 -0
- package/dist/db/index.d.ts +14 -0
- package/dist/db/index.js +8 -0
- package/dist/db/schema/account-deletion-requests.d.ts +203 -0
- package/dist/db/schema/account-deletion-requests.js +36 -0
- package/dist/db/schema/account-export-requests.d.ts +148 -0
- package/dist/db/schema/account-export-requests.js +19 -0
- package/dist/db/schema/admin-audit.d.ts +194 -0
- package/dist/db/schema/admin-audit.js +21 -0
- package/dist/db/schema/admin-users.d.ts +177 -0
- package/dist/db/schema/admin-users.js +23 -0
- package/dist/db/schema/affiliate-fraud.d.ts +160 -0
- package/dist/db/schema/affiliate-fraud.js +18 -0
- package/dist/db/schema/affiliate.d.ts +277 -0
- package/dist/db/schema/affiliate.js +32 -0
- package/dist/db/schema/coupon-codes.d.ts +143 -0
- package/dist/db/schema/coupon-codes.js +17 -0
- package/dist/db/schema/credit-auto-topup-settings.d.ts +232 -0
- package/dist/db/schema/credit-auto-topup-settings.js +27 -0
- package/dist/db/schema/credit-auto-topup.d.ts +130 -0
- package/dist/db/schema/credit-auto-topup.js +21 -0
- package/dist/db/schema/credits.d.ts +283 -0
- package/dist/db/schema/credits.js +38 -0
- package/dist/db/schema/dividend-distributions.d.ts +130 -0
- package/dist/db/schema/dividend-distributions.js +19 -0
- package/dist/db/schema/email-notifications.d.ts +99 -0
- package/dist/db/schema/email-notifications.js +21 -0
- package/dist/db/schema/index.d.ts +33 -0
- package/dist/db/schema/index.js +33 -0
- package/dist/db/schema/meter-events.d.ts +599 -0
- package/dist/db/schema/meter-events.js +55 -0
- package/dist/db/schema/notification-preferences.d.ts +165 -0
- package/dist/db/schema/notification-preferences.js +18 -0
- package/dist/db/schema/notification-queue.d.ts +236 -0
- package/dist/db/schema/notification-queue.js +40 -0
- package/dist/db/schema/org-memberships.d.ts +63 -0
- package/dist/db/schema/org-memberships.js +15 -0
- package/dist/db/schema/organization-members.d.ts +235 -0
- package/dist/db/schema/organization-members.js +27 -0
- package/dist/db/schema/payram.d.ts +164 -0
- package/dist/db/schema/payram.js +21 -0
- package/dist/db/schema/platform-api-keys.d.ts +143 -0
- package/dist/db/schema/platform-api-keys.js +20 -0
- package/dist/db/schema/promotion-redemptions.d.ts +143 -0
- package/dist/db/schema/promotion-redemptions.js +18 -0
- package/dist/db/schema/promotions.d.ts +445 -0
- package/dist/db/schema/promotions.js +48 -0
- package/dist/db/schema/provider-credentials.d.ts +201 -0
- package/dist/db/schema/provider-credentials.js +36 -0
- package/dist/db/schema/rate-limit-entries.d.ts +75 -0
- package/dist/db/schema/rate-limit-entries.js +7 -0
- package/dist/db/schema/secret-audit-log.d.ts +109 -0
- package/dist/db/schema/secret-audit-log.js +15 -0
- package/dist/db/schema/session-usage.d.ts +194 -0
- package/dist/db/schema/session-usage.js +19 -0
- package/dist/db/schema/spending-limits.d.ts +92 -0
- package/dist/db/schema/spending-limits.js +8 -0
- package/dist/db/schema/tenant-addons.d.ts +58 -0
- package/dist/db/schema/tenant-addons.js +9 -0
- package/dist/db/schema/tenant-api-keys.d.ts +131 -0
- package/dist/db/schema/tenant-api-keys.js +21 -0
- package/dist/db/schema/tenant-capability-settings.d.ts +79 -0
- package/dist/db/schema/tenant-capability-settings.js +12 -0
- package/dist/db/schema/tenant-customers.d.ts +303 -0
- package/dist/db/schema/tenant-customers.js +25 -0
- package/dist/db/schema/tenants.d.ts +126 -0
- package/dist/db/schema/tenants.js +18 -0
- package/dist/db/schema/user-roles.d.ts +98 -0
- package/dist/db/schema/user-roles.js +18 -0
- package/dist/db/schema/webhook-seen-events.d.ts +58 -0
- package/dist/db/schema/webhook-seen-events.js +9 -0
- package/dist/email/billing-emails.d.ts +51 -0
- package/dist/email/billing-emails.js +163 -0
- package/dist/email/billing-emails.test.d.ts +1 -0
- package/dist/email/billing-emails.test.js +162 -0
- package/dist/email/client.d.ts +51 -0
- package/dist/email/client.js +102 -0
- package/dist/email/client.test.d.ts +1 -0
- package/dist/email/client.test.js +120 -0
- package/dist/email/drizzle-billing-email-repository.d.ts +21 -0
- package/dist/email/drizzle-billing-email-repository.js +36 -0
- package/dist/email/drizzle-billing-email-repository.test.d.ts +1 -0
- package/dist/email/drizzle-billing-email-repository.test.js +42 -0
- package/dist/email/index.d.ts +33 -0
- package/dist/email/index.js +22 -0
- package/dist/email/notification-preferences-store.d.ts +12 -0
- package/dist/email/notification-preferences-store.js +82 -0
- package/dist/email/notification-preferences-store.test.d.ts +1 -0
- package/dist/email/notification-preferences-store.test.js +86 -0
- package/dist/email/notification-queue-store.d.ts +25 -0
- package/dist/email/notification-queue-store.js +97 -0
- package/dist/email/notification-queue-store.test.d.ts +1 -0
- package/dist/email/notification-queue-store.test.js +177 -0
- package/dist/email/notification-repository-types.d.ts +70 -0
- package/dist/email/notification-repository-types.js +6 -0
- package/dist/email/notification-service.d.ts +41 -0
- package/dist/email/notification-service.js +196 -0
- package/dist/email/notification-service.test.d.ts +1 -0
- package/dist/email/notification-service.test.js +160 -0
- package/dist/email/notification-templates.d.ts +18 -0
- package/dist/email/notification-templates.js +574 -0
- package/dist/email/notification-templates.test.d.ts +1 -0
- package/dist/email/notification-templates.test.js +238 -0
- package/dist/email/notification-worker.d.ts +24 -0
- package/dist/email/notification-worker.js +109 -0
- package/dist/email/notification-worker.test.d.ts +1 -0
- package/dist/email/notification-worker.test.js +153 -0
- package/dist/email/require-verified.d.ts +25 -0
- package/dist/email/require-verified.js +52 -0
- package/dist/email/require-verified.test.d.ts +1 -0
- package/dist/email/require-verified.test.js +62 -0
- package/dist/email/resend-adapter.d.ts +47 -0
- package/dist/email/resend-adapter.js +137 -0
- package/dist/email/resend-adapter.test.d.ts +1 -0
- package/dist/email/resend-adapter.test.js +190 -0
- package/dist/email/templates.d.ts +22 -0
- package/dist/email/templates.js +359 -0
- package/dist/email/templates.test.d.ts +1 -0
- package/dist/email/templates.test.js +170 -0
- package/dist/email/verification.d.ts +42 -0
- package/dist/email/verification.js +83 -0
- package/dist/email/verification.test.d.ts +1 -0
- package/dist/email/verification.test.js +141 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +23 -0
- package/dist/metering/aggregator.d.ts +54 -0
- package/dist/metering/aggregator.js +123 -0
- package/dist/metering/aggregator.test.d.ts +1 -0
- package/dist/metering/aggregator.test.js +179 -0
- package/dist/metering/dlq.d.ts +31 -0
- package/dist/metering/dlq.js +82 -0
- package/dist/metering/dlq.test.d.ts +1 -0
- package/dist/metering/dlq.test.js +117 -0
- package/dist/metering/drizzle-usage-summary-repository.d.ts +67 -0
- package/dist/metering/drizzle-usage-summary-repository.js +98 -0
- package/dist/metering/emitter.d.ts +66 -0
- package/dist/metering/emitter.js +185 -0
- package/dist/metering/emitter.test.d.ts +1 -0
- package/dist/metering/emitter.test.js +171 -0
- package/dist/metering/index.d.ts +11 -0
- package/dist/metering/index.js +5 -0
- package/dist/metering/load-test.bench.d.ts +1 -0
- package/dist/metering/load-test.bench.js +103 -0
- package/dist/metering/meter-event-repository.d.ts +33 -0
- package/dist/metering/meter-event-repository.js +58 -0
- package/dist/metering/meter-repositories.test.d.ts +1 -0
- package/dist/metering/meter-repositories.test.js +419 -0
- package/dist/metering/metering.test.d.ts +1 -0
- package/dist/metering/metering.test.js +1046 -0
- package/dist/metering/reconciliation-cron.d.ts +37 -0
- package/dist/metering/reconciliation-cron.js +85 -0
- package/dist/metering/reconciliation-cron.test.d.ts +1 -0
- package/dist/metering/reconciliation-cron.test.js +162 -0
- package/dist/metering/reconciliation-repository.d.ts +27 -0
- package/dist/metering/reconciliation-repository.js +43 -0
- package/dist/metering/reconciliation-repository.test.d.ts +1 -0
- package/dist/metering/reconciliation-repository.test.js +160 -0
- package/dist/metering/types.d.ts +88 -0
- package/dist/metering/types.js +1 -0
- package/dist/metering/wal.d.ts +49 -0
- package/dist/metering/wal.js +124 -0
- package/dist/metering/wal.test.d.ts +1 -0
- package/dist/metering/wal.test.js +175 -0
- package/dist/middleware/csrf.d.ts +24 -0
- package/dist/middleware/csrf.js +80 -0
- package/dist/middleware/csrf.test.d.ts +1 -0
- package/dist/middleware/csrf.test.js +152 -0
- package/dist/middleware/drizzle-rate-limit-repository.d.ts +9 -0
- package/dist/middleware/drizzle-rate-limit-repository.js +52 -0
- package/dist/middleware/drizzle-rate-limit-repository.test.d.ts +1 -0
- package/dist/middleware/drizzle-rate-limit-repository.test.js +74 -0
- package/dist/middleware/get-client-ip.d.ts +22 -0
- package/dist/middleware/get-client-ip.js +51 -0
- package/dist/middleware/get-client-ip.test.d.ts +1 -0
- package/dist/middleware/get-client-ip.test.js +40 -0
- package/dist/middleware/index.d.ts +5 -0
- package/dist/middleware/index.js +4 -0
- package/dist/middleware/rate-limit-repository.d.ts +19 -0
- package/dist/middleware/rate-limit-repository.js +1 -0
- package/dist/middleware/rate-limit.d.ts +57 -0
- package/dist/middleware/rate-limit.js +109 -0
- package/dist/middleware/rate-limit.test.d.ts +1 -0
- package/dist/middleware/rate-limit.test.js +247 -0
- package/dist/security/credential-vault/audit-repository.d.ts +27 -0
- package/dist/security/credential-vault/audit-repository.js +42 -0
- package/dist/security/credential-vault/audit-repository.test.d.ts +1 -0
- package/dist/security/credential-vault/audit-repository.test.js +78 -0
- package/dist/security/credential-vault/credential-repository.d.ts +94 -0
- package/dist/security/credential-vault/credential-repository.js +145 -0
- package/dist/security/credential-vault/credential-repository.test.d.ts +1 -0
- package/dist/security/credential-vault/credential-repository.test.js +206 -0
- package/dist/security/credential-vault/index.d.ts +12 -0
- package/dist/security/credential-vault/index.js +6 -0
- package/dist/security/credential-vault/key-rotation.d.ts +18 -0
- package/dist/security/credential-vault/key-rotation.js +52 -0
- package/dist/security/credential-vault/key-rotation.test.d.ts +1 -0
- package/dist/security/credential-vault/key-rotation.test.js +95 -0
- package/dist/security/credential-vault/migrate-plaintext.d.ts +15 -0
- package/dist/security/credential-vault/migrate-plaintext.js +80 -0
- package/dist/security/credential-vault/migrate-plaintext.test.d.ts +1 -0
- package/dist/security/credential-vault/migrate-plaintext.test.js +111 -0
- package/dist/security/credential-vault/migration-check.d.ts +15 -0
- package/dist/security/credential-vault/migration-check.js +71 -0
- package/dist/security/credential-vault/migration-check.test.d.ts +1 -0
- package/dist/security/credential-vault/migration-check.test.js +457 -0
- package/dist/security/credential-vault/store.d.ts +106 -0
- package/dist/security/credential-vault/store.js +181 -0
- package/dist/security/credential-vault/store.test.d.ts +1 -0
- package/dist/security/credential-vault/store.test.js +482 -0
- package/dist/security/encryption.d.ts +22 -0
- package/dist/security/encryption.js +53 -0
- package/dist/security/encryption.test.d.ts +1 -0
- package/dist/security/encryption.test.js +95 -0
- package/dist/security/host-validation.d.ts +11 -0
- package/dist/security/host-validation.js +108 -0
- package/dist/security/host-validation.test.d.ts +1 -0
- package/dist/security/host-validation.test.js +106 -0
- package/dist/security/index.d.ts +11 -0
- package/dist/security/index.js +11 -0
- package/dist/security/key-audit.d.ts +16 -0
- package/dist/security/key-audit.js +35 -0
- package/dist/security/key-audit.test.d.ts +1 -0
- package/dist/security/key-audit.test.js +50 -0
- package/dist/security/key-injection.d.ts +28 -0
- package/dist/security/key-injection.js +57 -0
- package/dist/security/key-injection.test.d.ts +1 -0
- package/dist/security/key-injection.test.js +97 -0
- package/dist/security/key-validation.d.ts +16 -0
- package/dist/security/key-validation.js +78 -0
- package/dist/security/key-validation.test.d.ts +1 -0
- package/dist/security/key-validation.test.js +87 -0
- package/dist/security/redirect-allowlist.d.ts +6 -0
- package/dist/security/redirect-allowlist.js +36 -0
- package/dist/security/redirect-allowlist.test.d.ts +1 -0
- package/dist/security/redirect-allowlist.test.js +55 -0
- package/dist/security/tenant-keys/capability-settings-store.d.ts +22 -0
- package/dist/security/tenant-keys/capability-settings-store.js +33 -0
- package/dist/security/tenant-keys/capability-settings-store.test.d.ts +1 -0
- package/dist/security/tenant-keys/capability-settings-store.test.js +77 -0
- package/dist/security/tenant-keys/index.d.ts +10 -0
- package/dist/security/tenant-keys/index.js +5 -0
- package/dist/security/tenant-keys/key-resolution-repository.d.ts +15 -0
- package/dist/security/tenant-keys/key-resolution-repository.js +18 -0
- package/dist/security/tenant-keys/key-resolution-repository.test.d.ts +1 -0
- package/dist/security/tenant-keys/key-resolution-repository.test.js +72 -0
- package/dist/security/tenant-keys/key-resolution.d.ts +39 -0
- package/dist/security/tenant-keys/key-resolution.js +59 -0
- package/dist/security/tenant-keys/key-resolution.test.d.ts +1 -0
- package/dist/security/tenant-keys/key-resolution.test.js +97 -0
- package/dist/security/tenant-keys/org-key-resolution.d.ts +30 -0
- package/dist/security/tenant-keys/org-key-resolution.js +50 -0
- package/dist/security/tenant-keys/org-key-resolution.test.d.ts +1 -0
- package/dist/security/tenant-keys/org-key-resolution.test.js +103 -0
- package/dist/security/tenant-keys/tenant-key-repository.d.ts +36 -0
- package/dist/security/tenant-keys/tenant-key-repository.js +96 -0
- package/dist/security/tenant-keys/tenant-key-repository.test.d.ts +1 -0
- package/dist/security/tenant-keys/tenant-key-repository.test.js +114 -0
- package/dist/security/types.d.ts +35 -0
- package/dist/security/types.js +15 -0
- package/dist/tenancy/drizzle-org-repository.d.ts +40 -0
- package/dist/tenancy/drizzle-org-repository.js +126 -0
- package/dist/tenancy/index.d.ts +6 -0
- package/dist/tenancy/index.js +3 -0
- package/dist/tenancy/org-member-repository.d.ts +57 -0
- package/dist/tenancy/org-member-repository.js +99 -0
- package/dist/tenancy/org-repository.test.d.ts +1 -0
- package/dist/tenancy/org-repository.test.js +143 -0
- package/dist/tenancy/org-service.d.ts +70 -0
- package/dist/tenancy/org-service.js +223 -0
- package/dist/tenancy/org-service.test.d.ts +1 -0
- package/dist/tenancy/org-service.test.js +550 -0
- package/dist/test/db.d.ts +33 -0
- package/dist/test/db.js +65 -0
- package/dist/trpc/index.d.ts +1 -0
- package/dist/trpc/index.js +1 -0
- package/dist/trpc/init.d.ts +49 -0
- package/dist/trpc/init.js +108 -0
- package/dist/trpc/init.test.d.ts +1 -0
- package/dist/trpc/init.test.js +154 -0
- package/drizzle/migrations/0000_slippery_mandrill.sql +559 -0
- package/drizzle/migrations/meta/0000_snapshot.json +4374 -0
- package/drizzle/migrations/meta/_journal.json +13 -0
- package/drizzle.config.ts +41 -0
- package/package.json +64 -0
- package/src/admin/admin-audit-log-repository.ts +135 -0
- package/src/admin/audit-log.ts +111 -0
- package/src/admin/index.ts +6 -0
- package/src/admin/role-store.ts +134 -0
- package/src/auth/api-key-repository.test.ts +63 -0
- package/src/auth/api-key-repository.ts +46 -0
- package/src/auth/auth.test.ts +166 -0
- package/src/auth/better-auth.ts +216 -0
- package/src/auth/index.ts +520 -0
- package/src/auth/login-history-repository.test.ts +54 -0
- package/src/auth/login-history-repository.ts +28 -0
- package/src/auth/middleware.test.ts +264 -0
- package/src/auth/middleware.ts +117 -0
- package/src/auth/scoped-tokens.test.ts +362 -0
- package/src/auth/tenant-access.test.ts +69 -0
- package/src/auth/user-creator.test.ts +98 -0
- package/src/auth/user-creator.ts +54 -0
- package/src/auth/user-role-repository.test.ts +149 -0
- package/src/auth/user-role-repository.ts +67 -0
- package/src/billing/drizzle-webhook-seen-repository.ts +34 -0
- package/src/billing/index.ts +22 -0
- package/src/billing/payment-processor.test.ts +93 -0
- package/src/billing/payment-processor.ts +150 -0
- package/src/billing/payram/cents-credits-boundary.test.ts +84 -0
- package/src/billing/payram/charge-store.test.ts +84 -0
- package/src/billing/payram/charge-store.ts +109 -0
- package/src/billing/payram/checkout.test.ts +99 -0
- package/src/billing/payram/checkout.ts +40 -0
- package/src/billing/payram/client.test.ts +62 -0
- package/src/billing/payram/client.ts +21 -0
- package/src/billing/payram/index.ts +14 -0
- package/src/billing/payram/types.ts +44 -0
- package/src/billing/payram/webhook.test.ts +318 -0
- package/src/billing/payram/webhook.ts +97 -0
- package/src/billing/stripe/cents-credits-boundary.test.ts +70 -0
- package/src/billing/stripe/checkout.test.ts +186 -0
- package/src/billing/stripe/checkout.ts +82 -0
- package/src/billing/stripe/client.test.ts +64 -0
- package/src/billing/stripe/client.ts +39 -0
- package/src/billing/stripe/credit-prices.test.ts +114 -0
- package/src/billing/stripe/credit-prices.ts +113 -0
- package/src/billing/stripe/index.ts +14 -0
- package/src/billing/stripe/payment-methods-detach-all.test.ts +53 -0
- package/src/billing/stripe/payment-methods.test.ts +157 -0
- package/src/billing/stripe/payment-methods.ts +76 -0
- package/src/billing/stripe/portal.test.ts +63 -0
- package/src/billing/stripe/portal.ts +25 -0
- package/src/billing/stripe/setup-intent.test.ts +78 -0
- package/src/billing/stripe/setup-intent.ts +34 -0
- package/src/billing/stripe/stripe-payment-processor.test.ts +517 -0
- package/src/billing/stripe/stripe-payment-processor.ts +255 -0
- package/src/billing/stripe/tenant-store.test.ts +124 -0
- package/src/billing/stripe/tenant-store.ts +151 -0
- package/src/billing/stripe/types.ts +53 -0
- package/src/billing/webhook-seen-repository.ts +24 -0
- package/src/config/billing-env.test.ts +54 -0
- package/src/config/index.ts +44 -0
- package/src/config/logger.ts +12 -0
- package/src/config/provider-endpoints.ts +14 -0
- package/src/credits/auto-topup-charge.test.ts +292 -0
- package/src/credits/auto-topup-charge.ts +171 -0
- package/src/credits/auto-topup-event-log-repository.test.ts +99 -0
- package/src/credits/auto-topup-event-log-repository.ts +30 -0
- package/src/credits/auto-topup-schedule.test.ts +179 -0
- package/src/credits/auto-topup-schedule.ts +93 -0
- package/src/credits/auto-topup-settings-repository.test.ts +123 -0
- package/src/credits/auto-topup-settings-repository.ts +245 -0
- package/src/credits/auto-topup-usage.test.ts +220 -0
- package/src/credits/auto-topup-usage.ts +68 -0
- package/src/credits/credit-expiry-cron.test.ts +125 -0
- package/src/credits/credit-expiry-cron.ts +76 -0
- package/src/credits/credit-ledger-extra.test.ts +57 -0
- package/src/credits/credit-ledger.bench.ts +56 -0
- package/src/credits/credit-ledger.test.ts +276 -0
- package/src/credits/credit-ledger.ts +450 -0
- package/src/credits/credit-transaction-repository.test.ts +274 -0
- package/src/credits/credit-transaction-repository.ts +62 -0
- package/src/credits/credit.test.ts +234 -0
- package/src/credits/credit.ts +160 -0
- package/src/credits/dividend-cron.test.ts +158 -0
- package/src/credits/dividend-cron.ts +127 -0
- package/src/credits/dividend-repository.test.ts +223 -0
- package/src/credits/dividend-repository.ts +182 -0
- package/src/credits/index.ts +25 -0
- package/src/credits/repository-types.ts +33 -0
- package/src/credits/signup-grant.test.ts +63 -0
- package/src/credits/signup-grant.ts +44 -0
- package/src/credits/tenant-customer-repository.ts +28 -0
- package/src/db/auth-user-repository.ts +124 -0
- package/src/db/credit-column.ts +17 -0
- package/src/db/index.ts +21 -0
- package/src/db/schema/account-deletion-requests.ts +41 -0
- package/src/db/schema/account-export-requests.ts +24 -0
- package/src/db/schema/admin-audit.ts +26 -0
- package/src/db/schema/admin-users.ts +31 -0
- package/src/db/schema/affiliate-fraud.ts +23 -0
- package/src/db/schema/affiliate.ts +38 -0
- package/src/db/schema/coupon-codes.ts +22 -0
- package/src/db/schema/credit-auto-topup-settings.ts +32 -0
- package/src/db/schema/credit-auto-topup.ts +26 -0
- package/src/db/schema/credits.ts +44 -0
- package/src/db/schema/dividend-distributions.ts +24 -0
- package/src/db/schema/email-notifications.ts +26 -0
- package/src/db/schema/index.ts +33 -0
- package/src/db/schema/meter-events.ts +70 -0
- package/src/db/schema/notification-preferences.ts +19 -0
- package/src/db/schema/notification-queue.ts +45 -0
- package/src/db/schema/org-memberships.ts +20 -0
- package/src/db/schema/organization-members.ts +37 -0
- package/src/db/schema/payram.ts +26 -0
- package/src/db/schema/platform-api-keys.ts +25 -0
- package/src/db/schema/promotion-redemptions.ts +23 -0
- package/src/db/schema/promotions.ts +57 -0
- package/src/db/schema/provider-credentials.ts +41 -0
- package/src/db/schema/rate-limit-entries.ts +12 -0
- package/src/db/schema/secret-audit-log.ts +20 -0
- package/src/db/schema/session-usage.ts +24 -0
- package/src/db/schema/spending-limits.ts +9 -0
- package/src/db/schema/tenant-addons.ts +14 -0
- package/src/db/schema/tenant-api-keys.ts +26 -0
- package/src/db/schema/tenant-capability-settings.ts +17 -0
- package/src/db/schema/tenant-customers.ts +35 -0
- package/src/db/schema/tenants.ts +23 -0
- package/src/db/schema/user-roles.ts +23 -0
- package/src/db/schema/webhook-seen-events.ts +14 -0
- package/src/email/billing-emails.test.ts +198 -0
- package/src/email/billing-emails.ts +211 -0
- package/src/email/client.test.ts +149 -0
- package/src/email/client.ts +137 -0
- package/src/email/drizzle-billing-email-repository.test.ts +52 -0
- package/src/email/drizzle-billing-email-repository.ts +59 -0
- package/src/email/index.ts +57 -0
- package/src/email/notification-preferences-store.test.ts +102 -0
- package/src/email/notification-preferences-store.ts +90 -0
- package/src/email/notification-queue-store.test.ts +215 -0
- package/src/email/notification-queue-store.ts +127 -0
- package/src/email/notification-repository-types.ts +101 -0
- package/src/email/notification-service.test.ts +178 -0
- package/src/email/notification-service.ts +265 -0
- package/src/email/notification-templates.test.ts +261 -0
- package/src/email/notification-templates.ts +727 -0
- package/src/email/notification-worker.test.ts +189 -0
- package/src/email/notification-worker.ts +133 -0
- package/src/email/require-verified.ts +65 -0
- package/src/email/resend-adapter.test.ts +253 -0
- package/src/email/resend-adapter.ts +157 -0
- package/src/email/templates.test.ts +217 -0
- package/src/email/templates.ts +469 -0
- package/src/email/verification.test.ts +185 -0
- package/src/email/verification.ts +110 -0
- package/src/index.ts +51 -0
- package/src/metering/aggregator.test.ts +239 -0
- package/src/metering/aggregator.ts +160 -0
- package/src/metering/dlq.test.ts +134 -0
- package/src/metering/dlq.ts +102 -0
- package/src/metering/drizzle-usage-summary-repository.ts +167 -0
- package/src/metering/emitter.test.ts +202 -0
- package/src/metering/emitter.ts +227 -0
- package/src/metering/index.ts +21 -0
- package/src/metering/load-test.bench.ts +130 -0
- package/src/metering/meter-event-repository.ts +87 -0
- package/src/metering/meter-repositories.test.ts +491 -0
- package/src/metering/metering.test.ts +1317 -0
- package/src/metering/reconciliation-cron.test.ts +202 -0
- package/src/metering/reconciliation-cron.ts +134 -0
- package/src/metering/reconciliation-repository.test.ts +196 -0
- package/src/metering/reconciliation-repository.ts +83 -0
- package/src/metering/types.ts +93 -0
- package/src/metering/wal.test.ts +222 -0
- package/src/metering/wal.ts +139 -0
- package/src/middleware/csrf.test.ts +178 -0
- package/src/middleware/csrf.ts +101 -0
- package/src/middleware/drizzle-rate-limit-repository.test.ts +97 -0
- package/src/middleware/drizzle-rate-limit-repository.ts +57 -0
- package/src/middleware/get-client-ip.test.ts +49 -0
- package/src/middleware/get-client-ip.ts +62 -0
- package/src/middleware/index.ts +12 -0
- package/src/middleware/rate-limit-repository.ts +22 -0
- package/src/middleware/rate-limit.test.ts +338 -0
- package/src/middleware/rate-limit.ts +169 -0
- package/src/security/credential-vault/audit-repository.test.ts +91 -0
- package/src/security/credential-vault/audit-repository.ts +64 -0
- package/src/security/credential-vault/credential-repository.test.ts +264 -0
- package/src/security/credential-vault/credential-repository.ts +233 -0
- package/src/security/credential-vault/index.ts +26 -0
- package/src/security/credential-vault/key-rotation.test.ts +139 -0
- package/src/security/credential-vault/key-rotation.ts +70 -0
- package/src/security/credential-vault/migrate-plaintext.test.ts +138 -0
- package/src/security/credential-vault/migrate-plaintext.ts +101 -0
- package/src/security/credential-vault/migration-check.test.ts +533 -0
- package/src/security/credential-vault/migration-check.ts +88 -0
- package/src/security/credential-vault/store.test.ts +569 -0
- package/src/security/credential-vault/store.ts +284 -0
- package/src/security/encryption.test.ts +114 -0
- package/src/security/encryption.ts +65 -0
- package/src/security/host-validation.test.ts +136 -0
- package/src/security/host-validation.ts +116 -0
- package/src/security/index.ts +59 -0
- package/src/security/key-audit.test.ts +57 -0
- package/src/security/key-audit.ts +45 -0
- package/src/security/key-injection.test.ts +131 -0
- package/src/security/key-injection.ts +71 -0
- package/src/security/key-validation.test.ts +111 -0
- package/src/security/key-validation.ts +84 -0
- package/src/security/redirect-allowlist.test.ts +70 -0
- package/src/security/redirect-allowlist.ts +35 -0
- package/src/security/tenant-keys/capability-settings-store.test.ts +98 -0
- package/src/security/tenant-keys/capability-settings-store.ts +53 -0
- package/src/security/tenant-keys/index.ts +10 -0
- package/src/security/tenant-keys/key-resolution-repository.test.ts +95 -0
- package/src/security/tenant-keys/key-resolution-repository.ts +31 -0
- package/src/security/tenant-keys/key-resolution.test.ts +173 -0
- package/src/security/tenant-keys/key-resolution.ts +87 -0
- package/src/security/tenant-keys/org-key-resolution.test.ts +217 -0
- package/src/security/tenant-keys/org-key-resolution.ts +76 -0
- package/src/security/tenant-keys/tenant-key-repository.test.ts +143 -0
- package/src/security/tenant-keys/tenant-key-repository.ts +130 -0
- package/src/security/types.ts +43 -0
- package/src/tenancy/drizzle-org-repository.ts +169 -0
- package/src/tenancy/index.ts +6 -0
- package/src/tenancy/org-member-repository.ts +159 -0
- package/src/tenancy/org-repository.test.ts +172 -0
- package/src/tenancy/org-service.test.ts +634 -0
- package/src/tenancy/org-service.ts +290 -0
- package/src/test/db.ts +97 -0
- package/src/trpc/index.ts +11 -0
- package/src/trpc/init.test.ts +196 -0
- package/src/trpc/init.ts +138 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { logger } from "../config/logger.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Credit value object with sub-cent precision.
|
|
5
|
+
*
|
|
6
|
+
* SCALE = 1,000,000,000 raw units per dollar (nano-dollars).
|
|
7
|
+
* All arithmetic operates on integer raw units -- no floating point
|
|
8
|
+
* in arithmetic paths. Math.round() is used only at input boundaries
|
|
9
|
+
* (fromDollars, fromCents, multiply).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const charge = Credit.fromDollars(0.001); // 1,000,000 raw units
|
|
14
|
+
* const balance = Credit.fromCents(500); // 5,000,000,000 raw units
|
|
15
|
+
* const remaining = balance.subtract(charge);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export class Credit {
|
|
19
|
+
static readonly SCALE = 1_000_000_000;
|
|
20
|
+
|
|
21
|
+
private constructor(private readonly raw: number) {}
|
|
22
|
+
|
|
23
|
+
/** Zero credit (static readonly instance to avoid allocation per access). */
|
|
24
|
+
static readonly ZERO = new Credit(0);
|
|
25
|
+
|
|
26
|
+
/** Create from dollar amount. Rounds to nearest raw unit. */
|
|
27
|
+
static fromDollars(dollars: number): Credit {
|
|
28
|
+
return new Credit(Math.round(dollars * Credit.SCALE));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Create from cent amount. Rounds to nearest raw unit. */
|
|
32
|
+
static fromCents(cents: number): Credit {
|
|
33
|
+
return new Credit(Math.round(cents * (Credit.SCALE / 100)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Tiered balance thresholds (in raw units) for observability warnings.
|
|
37
|
+
private static readonly WARN_10K = 10_000 * Credit.SCALE; // $10,000
|
|
38
|
+
private static readonly WARN_100K = 100_000 * Credit.SCALE; // $100,000
|
|
39
|
+
private static readonly WARN_1M = 1_000_000 * Credit.SCALE; // $1,000,000
|
|
40
|
+
|
|
41
|
+
/** Create from raw integer units. Throws TypeError if not integer. */
|
|
42
|
+
static fromRaw(raw: number): Credit {
|
|
43
|
+
if (!Number.isInteger(raw)) {
|
|
44
|
+
throw new TypeError(`Credit.fromRaw requires an integer, got ${raw}`);
|
|
45
|
+
}
|
|
46
|
+
if (raw > Number.MAX_SAFE_INTEGER) {
|
|
47
|
+
throw new RangeError(`Credit.fromRaw value ${raw} exceeds MAX_SAFE_INTEGER — bigint migration required`);
|
|
48
|
+
}
|
|
49
|
+
const dollars = raw / Credit.SCALE;
|
|
50
|
+
if (raw >= Credit.WARN_1M) {
|
|
51
|
+
logger.warn("Credit balance WTF threshold reached — consider bigint migration", { dollars: dollars.toFixed(2) });
|
|
52
|
+
} else if (raw >= Credit.WARN_100K) {
|
|
53
|
+
logger.warn("Credit balance HIGH threshold reached — monitor for overflow", { dollars: dollars.toFixed(2) });
|
|
54
|
+
} else if (raw >= Credit.WARN_10K) {
|
|
55
|
+
logger.info("Credit balance large threshold reached", { dollars: dollars.toFixed(2) });
|
|
56
|
+
}
|
|
57
|
+
return new Credit(raw);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Zero credit (factory method alias for Credit.ZERO). */
|
|
61
|
+
static zero(): Credit {
|
|
62
|
+
return Credit.ZERO;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Convert to dollars (floating point, for display only). */
|
|
66
|
+
toDollars(): number {
|
|
67
|
+
return this.raw / Credit.SCALE;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Convert to cents (floating point, for display only). */
|
|
71
|
+
toCents(): number {
|
|
72
|
+
return this.raw / (Credit.SCALE / 100);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert to cents, rounded to nearest integer (Math.round).
|
|
77
|
+
* Use for display values and API responses where exact cent is needed.
|
|
78
|
+
*/
|
|
79
|
+
toCentsRounded(): number {
|
|
80
|
+
return Math.round(this.raw / (Credit.SCALE / 100));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Convert to cents, floored to integer (Math.floor).
|
|
85
|
+
* Use when sending amounts to Stripe/Payram — floor avoids charging
|
|
86
|
+
* more than the displayed amount.
|
|
87
|
+
*/
|
|
88
|
+
toCentsFloor(): number {
|
|
89
|
+
return Math.floor(this.raw / (Credit.SCALE / 100));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Display as dollar amount with two decimal places. */
|
|
93
|
+
toDisplayString(): string {
|
|
94
|
+
return `$${this.toDollars().toFixed(2)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Raw integer units (what gets stored in the database). */
|
|
98
|
+
toRaw(): number {
|
|
99
|
+
return this.raw;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Add another Credit, returning a new Credit. */
|
|
103
|
+
add(other: Credit): Credit {
|
|
104
|
+
return new Credit(this.raw + other.raw);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Subtract another Credit, returning a new Credit (may be negative). */
|
|
108
|
+
subtract(other: Credit): Credit {
|
|
109
|
+
return new Credit(this.raw - other.raw);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Multiply by a factor, rounding to nearest raw unit. */
|
|
113
|
+
multiply(factor: number): Credit {
|
|
114
|
+
return new Credit(Math.round(this.raw * factor));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** True if this credit is negative. */
|
|
118
|
+
isNegative(): boolean {
|
|
119
|
+
return this.raw < 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** True if this credit is exactly zero. */
|
|
123
|
+
isZero(): boolean {
|
|
124
|
+
return this.raw === 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** True if this credit is greater than other. */
|
|
128
|
+
greaterThan(other: Credit): boolean {
|
|
129
|
+
return this.raw > other.raw;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** True if this credit is less than other. */
|
|
133
|
+
lessThan(other: Credit): boolean {
|
|
134
|
+
return this.raw < other.raw;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** True if this credit equals other. */
|
|
138
|
+
equals(other: Credit): boolean {
|
|
139
|
+
return this.raw === other.raw;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** True if this credit is greater than or equal to other. */
|
|
143
|
+
greaterThanOrEqual(other: Credit): boolean {
|
|
144
|
+
return this.raw >= other.raw;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** True if this credit is less than or equal to other. */
|
|
148
|
+
lessThanOrEqual(other: Credit): boolean {
|
|
149
|
+
return this.raw <= other.raw;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
toString(): string {
|
|
153
|
+
return `Credit(raw=${this.raw})`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Serialize to raw integer for JSON (API responses, database). */
|
|
157
|
+
toJSON(): number {
|
|
158
|
+
return this.raw;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import type { PlatformDb } from "../db/index.js";
|
|
4
|
+
import { creditBalances, creditTransactions } from "../db/schema/credits.js";
|
|
5
|
+
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
6
|
+
import { Credit } from "./credit.js";
|
|
7
|
+
import { CreditLedger } from "./credit-ledger.js";
|
|
8
|
+
import { DrizzleCreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
9
|
+
import { type DividendCronConfig, runDividendCron } from "./dividend-cron.js";
|
|
10
|
+
|
|
11
|
+
async function insertPurchase(db: PlatformDb, tenantId: string, amountCents: number, createdAt: string): Promise<void> {
|
|
12
|
+
const id = `test-${tenantId}-${Date.now()}-${Math.random()}`;
|
|
13
|
+
const amount = Credit.fromCents(amountCents);
|
|
14
|
+
await db.insert(creditTransactions).values({
|
|
15
|
+
id,
|
|
16
|
+
tenantId,
|
|
17
|
+
amount,
|
|
18
|
+
balanceAfter: amount,
|
|
19
|
+
type: "purchase",
|
|
20
|
+
createdAt,
|
|
21
|
+
});
|
|
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
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("runDividendCron", () => {
|
|
38
|
+
let pool: PGlite;
|
|
39
|
+
let db: PlatformDb;
|
|
40
|
+
let ledger: CreditLedger;
|
|
41
|
+
let creditTransactionRepo: DrizzleCreditTransactionRepository;
|
|
42
|
+
|
|
43
|
+
beforeAll(async () => {
|
|
44
|
+
({ db, pool } = await createTestDb());
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(async () => {
|
|
48
|
+
await pool.close();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
await truncateAllTables(pool);
|
|
53
|
+
ledger = new CreditLedger(db);
|
|
54
|
+
creditTransactionRepo = new DrizzleCreditTransactionRepository(db);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function makeConfig(overrides?: Partial<DividendCronConfig>): DividendCronConfig {
|
|
58
|
+
return {
|
|
59
|
+
creditTransactionRepo,
|
|
60
|
+
ledger,
|
|
61
|
+
matchRate: 1.0,
|
|
62
|
+
targetDate: "2026-02-20",
|
|
63
|
+
...overrides,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
it("distributes dividend to eligible tenants", async () => {
|
|
68
|
+
await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
|
|
69
|
+
|
|
70
|
+
const result = await runDividendCron(makeConfig());
|
|
71
|
+
|
|
72
|
+
expect(result.distributed).toBe(1);
|
|
73
|
+
expect(result.pool.toCents()).toBe(1000);
|
|
74
|
+
expect(result.perUser.toCents()).toBe(1000);
|
|
75
|
+
expect(result.activeCount).toBe(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("is idempotent — skips if already ran for the date", async () => {
|
|
79
|
+
await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
|
|
80
|
+
|
|
81
|
+
const result1 = await runDividendCron(makeConfig());
|
|
82
|
+
expect(result1.distributed).toBe(1);
|
|
83
|
+
expect(result1.skippedAlreadyRun).toBe(false);
|
|
84
|
+
|
|
85
|
+
const balanceAfterFirst = await ledger.balance("t1");
|
|
86
|
+
|
|
87
|
+
const result2 = await runDividendCron(makeConfig());
|
|
88
|
+
expect(result2.skippedAlreadyRun).toBe(true);
|
|
89
|
+
expect(result2.distributed).toBe(0);
|
|
90
|
+
|
|
91
|
+
expect((await ledger.balance("t1")).equals(balanceAfterFirst)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("handles floor rounding — remainder is not distributed", async () => {
|
|
95
|
+
await insertPurchase(db, "t1", 50, "2026-02-20 12:00:00");
|
|
96
|
+
await insertPurchase(db, "t2", 30, "2026-02-20 12:00:00");
|
|
97
|
+
await insertPurchase(db, "t3", 20, "2026-02-20 12:00:00");
|
|
98
|
+
|
|
99
|
+
const result = await runDividendCron(makeConfig());
|
|
100
|
+
|
|
101
|
+
expect(result.pool.toCents()).toBe(100);
|
|
102
|
+
expect(result.activeCount).toBe(3);
|
|
103
|
+
// 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
|
+
expect(result.perUser.toRaw()).toBe(333_333_333);
|
|
106
|
+
expect(result.distributed).toBe(3);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("skips distribution when pool is zero", async () => {
|
|
110
|
+
// Tenant purchased within 7 days but NOT on target date -> pool = 0
|
|
111
|
+
await insertPurchase(db, "t1", 500, "2026-02-18 12:00:00");
|
|
112
|
+
|
|
113
|
+
const result = await runDividendCron(makeConfig());
|
|
114
|
+
|
|
115
|
+
expect(result.pool.toCents()).toBe(0);
|
|
116
|
+
expect(result.activeCount).toBe(1);
|
|
117
|
+
expect(result.perUser.toCents()).toBe(0);
|
|
118
|
+
expect(result.distributed).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("distributes sub-cent amounts at nanodollar precision", async () => {
|
|
122
|
+
// 1 cent purchase, 3 active users: pool = 10_000_000 raw
|
|
123
|
+
// floor(10_000_000 / 3) = 3_333_333 raw each — non-zero, gets distributed
|
|
124
|
+
await insertPurchase(db, "t1", 1, "2026-02-20 12:00:00");
|
|
125
|
+
await insertPurchase(db, "t2", 500, "2026-02-18 12:00:00");
|
|
126
|
+
await insertPurchase(db, "t3", 500, "2026-02-17 12:00:00");
|
|
127
|
+
|
|
128
|
+
const result = await runDividendCron(makeConfig({ matchRate: 1.0 }));
|
|
129
|
+
|
|
130
|
+
expect(result.pool.toCents()).toBe(1);
|
|
131
|
+
expect(result.activeCount).toBe(3);
|
|
132
|
+
expect(result.perUser.toRaw()).toBe(3_333_333);
|
|
133
|
+
expect(result.distributed).toBe(3);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("records transactions with correct type and referenceId", async () => {
|
|
137
|
+
await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
|
|
138
|
+
|
|
139
|
+
await runDividendCron(makeConfig());
|
|
140
|
+
|
|
141
|
+
const history = await ledger.history("t1", { type: "community_dividend" });
|
|
142
|
+
expect(history).toHaveLength(1);
|
|
143
|
+
expect(history[0].type).toBe("community_dividend");
|
|
144
|
+
expect(history[0].referenceId).toBe("dividend:2026-02-20:t1");
|
|
145
|
+
expect(history[0].amount.toCents()).toBe(1000);
|
|
146
|
+
expect(history[0].description).toContain("Community dividend");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("collects errors without stopping distribution to other tenants", async () => {
|
|
150
|
+
await insertPurchase(db, "t1", 500, "2026-02-20 12:00:00");
|
|
151
|
+
await insertPurchase(db, "t2", 500, "2026-02-20 12:00:00");
|
|
152
|
+
|
|
153
|
+
const result = await runDividendCron(makeConfig());
|
|
154
|
+
|
|
155
|
+
expect(result.distributed).toBe(2);
|
|
156
|
+
expect(result.errors).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { logger } from "../config/logger.js";
|
|
2
|
+
import { Credit } from "./credit.js";
|
|
3
|
+
import type { ICreditLedger } from "./credit-ledger.js";
|
|
4
|
+
import type { ICreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
5
|
+
|
|
6
|
+
export interface DividendCronConfig {
|
|
7
|
+
creditTransactionRepo: ICreditTransactionRepository;
|
|
8
|
+
ledger: ICreditLedger;
|
|
9
|
+
/** Fraction of daily purchases matched as dividend pool. Default 1.0 (100%). */
|
|
10
|
+
matchRate: number;
|
|
11
|
+
/** The date to compute dividend for, as YYYY-MM-DD string. Typically yesterday. */
|
|
12
|
+
targetDate: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DividendCronResult {
|
|
16
|
+
pool: Credit;
|
|
17
|
+
activeCount: number;
|
|
18
|
+
perUser: Credit;
|
|
19
|
+
distributed: number;
|
|
20
|
+
skippedAlreadyRun: boolean;
|
|
21
|
+
errors: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compute and distribute the community dividend for a given day.
|
|
26
|
+
*
|
|
27
|
+
* 1. Check idempotency — skip if already run for this date.
|
|
28
|
+
* 2. Sum all 'purchase' transactions for the target date.
|
|
29
|
+
* 3. Find all tenants with a 'purchase' transaction in the last 7 days.
|
|
30
|
+
* 4. Compute pool = sum × matchRate, per-user share = floor(pool / activeCount).
|
|
31
|
+
* 5. Credit each active tenant with their share.
|
|
32
|
+
*/
|
|
33
|
+
export async function runDividendCron(cfg: DividendCronConfig): Promise<DividendCronResult> {
|
|
34
|
+
const result: DividendCronResult = {
|
|
35
|
+
pool: Credit.ZERO,
|
|
36
|
+
activeCount: 0,
|
|
37
|
+
perUser: Credit.ZERO,
|
|
38
|
+
distributed: 0,
|
|
39
|
+
skippedAlreadyRun: false,
|
|
40
|
+
errors: [],
|
|
41
|
+
};
|
|
42
|
+
|
|
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
|
+
const sentinelPrefix = `dividend:${cfg.targetDate}:`;
|
|
46
|
+
const alreadyRan = await cfg.creditTransactionRepo.existsByReferenceIdLike(`${sentinelPrefix}%`);
|
|
47
|
+
|
|
48
|
+
if (alreadyRan) {
|
|
49
|
+
result.skippedAlreadyRun = true;
|
|
50
|
+
logger.info("Dividend cron already ran for this date", { targetDate: cfg.targetDate });
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Step 1: Sum all purchase amounts for the target date.
|
|
55
|
+
const dayStart = `${cfg.targetDate} 00:00:00`;
|
|
56
|
+
const dayEnd = `${cfg.targetDate} 24:00:00`;
|
|
57
|
+
|
|
58
|
+
const dailyPurchaseTotalCredit = await cfg.creditTransactionRepo.sumPurchasesForPeriod(dayStart, dayEnd);
|
|
59
|
+
result.pool = dailyPurchaseTotalCredit.multiply(cfg.matchRate);
|
|
60
|
+
|
|
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
|
+
const windowStart = subtractDays(cfg.targetDate, 6);
|
|
65
|
+
const windowStartTs = `${windowStart} 00:00:00`;
|
|
66
|
+
|
|
67
|
+
const activeTenantIds = await cfg.creditTransactionRepo.getActiveTenantIdsInWindow(windowStartTs, dayEnd);
|
|
68
|
+
result.activeCount = activeTenantIds.length;
|
|
69
|
+
|
|
70
|
+
// Step 3: Compute per-user share.
|
|
71
|
+
if (result.pool.isZero() || result.activeCount <= 0) {
|
|
72
|
+
logger.info("Dividend cron: no pool or no active tenants", {
|
|
73
|
+
targetDate: cfg.targetDate,
|
|
74
|
+
pool: result.pool.toCents(),
|
|
75
|
+
activeCount: result.activeCount,
|
|
76
|
+
});
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
result.perUser = Credit.fromRaw(Math.floor(result.pool.toRaw() / result.activeCount));
|
|
81
|
+
|
|
82
|
+
if (result.perUser.isZero()) {
|
|
83
|
+
logger.info("Dividend cron: per-user share rounds to zero", {
|
|
84
|
+
targetDate: cfg.targetDate,
|
|
85
|
+
pool: result.pool.toCents(),
|
|
86
|
+
activeCount: result.activeCount,
|
|
87
|
+
});
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Step 4: Distribute to each active tenant.
|
|
92
|
+
for (const tenantId of activeTenantIds) {
|
|
93
|
+
const perUserRef = `dividend:${cfg.targetDate}:${tenantId}`;
|
|
94
|
+
try {
|
|
95
|
+
await cfg.ledger.credit(
|
|
96
|
+
tenantId,
|
|
97
|
+
result.perUser,
|
|
98
|
+
"community_dividend",
|
|
99
|
+
`Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`,
|
|
100
|
+
perUserRef,
|
|
101
|
+
);
|
|
102
|
+
result.distributed++;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
105
|
+
logger.error("Dividend distribution failed for tenant", { tenantId, error: msg });
|
|
106
|
+
result.errors.push(`${tenantId}: ${msg}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
logger.info("Dividend cron complete", {
|
|
111
|
+
targetDate: cfg.targetDate,
|
|
112
|
+
pool: result.pool.toCents(),
|
|
113
|
+
activeCount: result.activeCount,
|
|
114
|
+
perUser: result.perUser.toCents(),
|
|
115
|
+
distributed: result.distributed,
|
|
116
|
+
errors: result.errors.length,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Subtract N days from a YYYY-MM-DD date string, returning YYYY-MM-DD. */
|
|
123
|
+
function subtractDays(dateStr: string, days: number): string {
|
|
124
|
+
const d = new Date(`${dateStr}T00:00:00Z`);
|
|
125
|
+
d.setUTCDate(d.getUTCDate() - days);
|
|
126
|
+
return d.toISOString().slice(0, 10);
|
|
127
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { PGlite } from "@electric-sql/pglite";
|
|
3
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import type { PlatformDb } from "../db/index.js";
|
|
5
|
+
import { adminUsers } from "../db/schema/admin-users.js";
|
|
6
|
+
import { dividendDistributions } from "../db/schema/dividend-distributions.js";
|
|
7
|
+
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
8
|
+
import { Credit } from "./credit.js";
|
|
9
|
+
import { CreditLedger } from "./credit-ledger.js";
|
|
10
|
+
import { DrizzleDividendRepository } from "./dividend-repository.js";
|
|
11
|
+
|
|
12
|
+
let pool: PGlite;
|
|
13
|
+
let db: PlatformDb;
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
({ db, pool } = await createTestDb());
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterAll(async () => {
|
|
20
|
+
await pool.close();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** Insert a dividend_distributions row directly (bypassing repo — it has no write method). */
|
|
24
|
+
async function seedDividend(opts: {
|
|
25
|
+
tenantId: string;
|
|
26
|
+
date: string;
|
|
27
|
+
amountCents: number;
|
|
28
|
+
poolCents: number;
|
|
29
|
+
activeUsers: number;
|
|
30
|
+
}): Promise<void> {
|
|
31
|
+
await db.insert(dividendDistributions).values({
|
|
32
|
+
id: crypto.randomUUID(),
|
|
33
|
+
tenantId: opts.tenantId,
|
|
34
|
+
date: opts.date,
|
|
35
|
+
amountCents: opts.amountCents,
|
|
36
|
+
poolCents: opts.poolCents,
|
|
37
|
+
activeUsers: opts.activeUsers,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Insert an admin_users row for getTenantEmail tests. */
|
|
42
|
+
async function seedAdminUser(tenantId: string, email: string): Promise<void> {
|
|
43
|
+
await db.insert(adminUsers).values({
|
|
44
|
+
id: crypto.randomUUID(),
|
|
45
|
+
email,
|
|
46
|
+
tenantId,
|
|
47
|
+
status: "active",
|
|
48
|
+
role: "tenant_admin",
|
|
49
|
+
createdAt: Date.now(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("DrizzleDividendRepository", () => {
|
|
54
|
+
let repo: DrizzleDividendRepository;
|
|
55
|
+
|
|
56
|
+
beforeEach(async () => {
|
|
57
|
+
await truncateAllTables(pool);
|
|
58
|
+
repo = new DrizzleDividendRepository(db);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// --- getHistory() ---
|
|
62
|
+
|
|
63
|
+
describe("getHistory()", () => {
|
|
64
|
+
it("returns empty array when tenant has no distributions", async () => {
|
|
65
|
+
const history = await repo.getHistory("nonexistent", 50, 0);
|
|
66
|
+
expect(history).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns distributions in reverse chronological order", async () => {
|
|
70
|
+
await seedDividend({ tenantId: "t1", date: "2026-01-01", amountCents: 10, poolCents: 1000, activeUsers: 5 });
|
|
71
|
+
await seedDividend({ tenantId: "t1", date: "2026-01-02", amountCents: 20, poolCents: 2000, activeUsers: 10 });
|
|
72
|
+
await seedDividend({ tenantId: "t1", date: "2026-01-03", amountCents: 30, poolCents: 3000, activeUsers: 15 });
|
|
73
|
+
|
|
74
|
+
const history = await repo.getHistory("t1", 50, 0);
|
|
75
|
+
expect(history).toHaveLength(3);
|
|
76
|
+
expect(history[0].date).toBe("2026-01-03");
|
|
77
|
+
expect(history[1].date).toBe("2026-01-02");
|
|
78
|
+
expect(history[2].date).toBe("2026-01-01");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("converts amountCents and poolCents to Credit objects", async () => {
|
|
82
|
+
await seedDividend({ tenantId: "t1", date: "2026-01-15", amountCents: 42, poolCents: 5000, activeUsers: 8 });
|
|
83
|
+
|
|
84
|
+
const history = await repo.getHistory("t1", 50, 0);
|
|
85
|
+
expect(history).toHaveLength(1);
|
|
86
|
+
expect(history[0].amount.toCents()).toBe(42);
|
|
87
|
+
expect(history[0].pool.toCents()).toBe(5000);
|
|
88
|
+
expect(history[0].activeUsers).toBe(8);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("respects limit and offset", async () => {
|
|
92
|
+
for (let i = 1; i <= 5; i++) {
|
|
93
|
+
await seedDividend({
|
|
94
|
+
tenantId: "t1",
|
|
95
|
+
date: `2026-01-0${i}`,
|
|
96
|
+
amountCents: i * 10,
|
|
97
|
+
poolCents: 1000,
|
|
98
|
+
activeUsers: 5,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const page1 = await repo.getHistory("t1", 2, 0);
|
|
103
|
+
expect(page1).toHaveLength(2);
|
|
104
|
+
expect(page1[0].date).toBe("2026-01-05");
|
|
105
|
+
expect(page1[1].date).toBe("2026-01-04");
|
|
106
|
+
|
|
107
|
+
const page2 = await repo.getHistory("t1", 2, 2);
|
|
108
|
+
expect(page2).toHaveLength(2);
|
|
109
|
+
expect(page2[0].date).toBe("2026-01-03");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("is tenant-isolated", async () => {
|
|
113
|
+
await seedDividend({ tenantId: "t1", date: "2026-01-01", amountCents: 10, poolCents: 1000, activeUsers: 5 });
|
|
114
|
+
await seedDividend({ tenantId: "t2", date: "2026-01-01", amountCents: 99, poolCents: 9000, activeUsers: 50 });
|
|
115
|
+
|
|
116
|
+
const t1History = await repo.getHistory("t1", 50, 0);
|
|
117
|
+
expect(t1History).toHaveLength(1);
|
|
118
|
+
expect(t1History[0].amount.toCents()).toBe(10);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// --- getLifetimeTotal() ---
|
|
123
|
+
|
|
124
|
+
describe("getLifetimeTotal()", () => {
|
|
125
|
+
it("returns Credit.ZERO when tenant has no distributions", async () => {
|
|
126
|
+
const total = await repo.getLifetimeTotal("nonexistent");
|
|
127
|
+
expect(total.toCents()).toBe(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("sums all distributions for a tenant", async () => {
|
|
131
|
+
await seedDividend({ tenantId: "t1", date: "2026-01-01", amountCents: 10, poolCents: 1000, activeUsers: 5 });
|
|
132
|
+
await seedDividend({ tenantId: "t1", date: "2026-01-02", amountCents: 25, poolCents: 2000, activeUsers: 10 });
|
|
133
|
+
|
|
134
|
+
const total = await repo.getLifetimeTotal("t1");
|
|
135
|
+
expect(total.toCents()).toBe(35);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("is tenant-isolated", async () => {
|
|
139
|
+
await seedDividend({ tenantId: "t1", date: "2026-01-01", amountCents: 10, poolCents: 1000, activeUsers: 5 });
|
|
140
|
+
await seedDividend({ tenantId: "t2", date: "2026-01-01", amountCents: 999, poolCents: 9000, activeUsers: 50 });
|
|
141
|
+
|
|
142
|
+
const total = await repo.getLifetimeTotal("t1");
|
|
143
|
+
expect(total.toCents()).toBe(10);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// --- getDigestTenantAggregates() ---
|
|
148
|
+
|
|
149
|
+
describe("getDigestTenantAggregates()", () => {
|
|
150
|
+
it("returns empty array when no distributions in window", async () => {
|
|
151
|
+
const result = await repo.getDigestTenantAggregates("2026-02-01", "2026-02-28");
|
|
152
|
+
expect(result).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("aggregates distributions per tenant within [windowStart, windowEnd)", async () => {
|
|
156
|
+
// In window
|
|
157
|
+
await seedDividend({ tenantId: "t1", date: "2026-02-01", amountCents: 10, poolCents: 1000, activeUsers: 5 });
|
|
158
|
+
await seedDividend({ tenantId: "t1", date: "2026-02-02", amountCents: 20, poolCents: 2000, activeUsers: 10 });
|
|
159
|
+
// Out of window
|
|
160
|
+
await seedDividend({ tenantId: "t1", date: "2026-03-01", amountCents: 999, poolCents: 9000, activeUsers: 50 });
|
|
161
|
+
|
|
162
|
+
const result = await repo.getDigestTenantAggregates("2026-02-01", "2026-03-01");
|
|
163
|
+
expect(result).toHaveLength(1);
|
|
164
|
+
expect(result[0].tenantId).toBe("t1");
|
|
165
|
+
expect(result[0].total.toCents()).toBe(30); // 10 + 20
|
|
166
|
+
expect(result[0].distributionCount).toBe(2);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("returns multiple tenants", async () => {
|
|
170
|
+
await seedDividend({ tenantId: "t1", date: "2026-02-01", amountCents: 10, poolCents: 1000, activeUsers: 5 });
|
|
171
|
+
await seedDividend({ tenantId: "t2", date: "2026-02-01", amountCents: 50, poolCents: 5000, activeUsers: 20 });
|
|
172
|
+
|
|
173
|
+
const result = await repo.getDigestTenantAggregates("2026-02-01", "2026-03-01");
|
|
174
|
+
expect(result).toHaveLength(2);
|
|
175
|
+
|
|
176
|
+
const t1 = result.find((r) => r.tenantId === "t1");
|
|
177
|
+
const t2 = result.find((r) => r.tenantId === "t2");
|
|
178
|
+
expect(t1?.total.toCents()).toBe(10);
|
|
179
|
+
expect(t2?.total.toCents()).toBe(50);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// --- getStats() ---
|
|
184
|
+
|
|
185
|
+
describe("getStats()", () => {
|
|
186
|
+
it("returns zero pool and zero activeUsers when no purchase transactions exist", async () => {
|
|
187
|
+
const stats = await repo.getStats("t1");
|
|
188
|
+
expect(stats.pool.toCents()).toBe(0);
|
|
189
|
+
expect(stats.activeUsers).toBe(0);
|
|
190
|
+
expect(stats.perUser.toCents()).toBe(0);
|
|
191
|
+
expect(stats.userEligible).toBe(false);
|
|
192
|
+
expect(stats.userLastPurchaseAt).toBeNull();
|
|
193
|
+
expect(stats.userWindowExpiresAt).toBeNull();
|
|
194
|
+
expect(stats.nextDistributionAt).toEqual(expect.any(String));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("marks user as eligible when they have a recent purchase", async () => {
|
|
198
|
+
const ledger = new CreditLedger(db);
|
|
199
|
+
await ledger.credit("t1", Credit.fromCents(100), "purchase", "recent buy");
|
|
200
|
+
|
|
201
|
+
const stats = await repo.getStats("t1");
|
|
202
|
+
expect(stats.userEligible).toBe(true);
|
|
203
|
+
expect(stats.userLastPurchaseAt).toEqual(expect.any(String));
|
|
204
|
+
expect(stats.userWindowExpiresAt).toEqual(expect.any(String));
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// --- getTenantEmail() ---
|
|
209
|
+
|
|
210
|
+
describe("getTenantEmail()", () => {
|
|
211
|
+
it("returns undefined when tenant has no admin user", async () => {
|
|
212
|
+
const email = await repo.getTenantEmail("nonexistent");
|
|
213
|
+
expect(email).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("returns the email for the tenant's admin user", async () => {
|
|
217
|
+
await seedAdminUser("t1", "alice@example.com");
|
|
218
|
+
|
|
219
|
+
const email = await repo.getTenantEmail("t1");
|
|
220
|
+
expect(email).toBe("alice@example.com");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|