@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,139 @@
|
|
|
1
|
+
import { logger } from "../config/logger.js";
|
|
2
|
+
/**
|
|
3
|
+
* Credit value object with sub-cent precision.
|
|
4
|
+
*
|
|
5
|
+
* SCALE = 1,000,000,000 raw units per dollar (nano-dollars).
|
|
6
|
+
* All arithmetic operates on integer raw units -- no floating point
|
|
7
|
+
* in arithmetic paths. Math.round() is used only at input boundaries
|
|
8
|
+
* (fromDollars, fromCents, multiply).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const charge = Credit.fromDollars(0.001); // 1,000,000 raw units
|
|
13
|
+
* const balance = Credit.fromCents(500); // 5,000,000,000 raw units
|
|
14
|
+
* const remaining = balance.subtract(charge);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class Credit {
|
|
18
|
+
raw;
|
|
19
|
+
static SCALE = 1_000_000_000;
|
|
20
|
+
constructor(raw) {
|
|
21
|
+
this.raw = raw;
|
|
22
|
+
}
|
|
23
|
+
/** Zero credit (static readonly instance to avoid allocation per access). */
|
|
24
|
+
static ZERO = new Credit(0);
|
|
25
|
+
/** Create from dollar amount. Rounds to nearest raw unit. */
|
|
26
|
+
static fromDollars(dollars) {
|
|
27
|
+
return new Credit(Math.round(dollars * Credit.SCALE));
|
|
28
|
+
}
|
|
29
|
+
/** Create from cent amount. Rounds to nearest raw unit. */
|
|
30
|
+
static fromCents(cents) {
|
|
31
|
+
return new Credit(Math.round(cents * (Credit.SCALE / 100)));
|
|
32
|
+
}
|
|
33
|
+
// Tiered balance thresholds (in raw units) for observability warnings.
|
|
34
|
+
static WARN_10K = 10_000 * Credit.SCALE; // $10,000
|
|
35
|
+
static WARN_100K = 100_000 * Credit.SCALE; // $100,000
|
|
36
|
+
static WARN_1M = 1_000_000 * Credit.SCALE; // $1,000,000
|
|
37
|
+
/** Create from raw integer units. Throws TypeError if not integer. */
|
|
38
|
+
static fromRaw(raw) {
|
|
39
|
+
if (!Number.isInteger(raw)) {
|
|
40
|
+
throw new TypeError(`Credit.fromRaw requires an integer, got ${raw}`);
|
|
41
|
+
}
|
|
42
|
+
if (raw > Number.MAX_SAFE_INTEGER) {
|
|
43
|
+
throw new RangeError(`Credit.fromRaw value ${raw} exceeds MAX_SAFE_INTEGER — bigint migration required`);
|
|
44
|
+
}
|
|
45
|
+
const dollars = raw / Credit.SCALE;
|
|
46
|
+
if (raw >= Credit.WARN_1M) {
|
|
47
|
+
logger.warn("Credit balance WTF threshold reached — consider bigint migration", { dollars: dollars.toFixed(2) });
|
|
48
|
+
}
|
|
49
|
+
else if (raw >= Credit.WARN_100K) {
|
|
50
|
+
logger.warn("Credit balance HIGH threshold reached — monitor for overflow", { dollars: dollars.toFixed(2) });
|
|
51
|
+
}
|
|
52
|
+
else if (raw >= Credit.WARN_10K) {
|
|
53
|
+
logger.info("Credit balance large threshold reached", { dollars: dollars.toFixed(2) });
|
|
54
|
+
}
|
|
55
|
+
return new Credit(raw);
|
|
56
|
+
}
|
|
57
|
+
/** Zero credit (factory method alias for Credit.ZERO). */
|
|
58
|
+
static zero() {
|
|
59
|
+
return Credit.ZERO;
|
|
60
|
+
}
|
|
61
|
+
/** Convert to dollars (floating point, for display only). */
|
|
62
|
+
toDollars() {
|
|
63
|
+
return this.raw / Credit.SCALE;
|
|
64
|
+
}
|
|
65
|
+
/** Convert to cents (floating point, for display only). */
|
|
66
|
+
toCents() {
|
|
67
|
+
return this.raw / (Credit.SCALE / 100);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Convert to cents, rounded to nearest integer (Math.round).
|
|
71
|
+
* Use for display values and API responses where exact cent is needed.
|
|
72
|
+
*/
|
|
73
|
+
toCentsRounded() {
|
|
74
|
+
return Math.round(this.raw / (Credit.SCALE / 100));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Convert to cents, floored to integer (Math.floor).
|
|
78
|
+
* Use when sending amounts to Stripe/Payram — floor avoids charging
|
|
79
|
+
* more than the displayed amount.
|
|
80
|
+
*/
|
|
81
|
+
toCentsFloor() {
|
|
82
|
+
return Math.floor(this.raw / (Credit.SCALE / 100));
|
|
83
|
+
}
|
|
84
|
+
/** Display as dollar amount with two decimal places. */
|
|
85
|
+
toDisplayString() {
|
|
86
|
+
return `$${this.toDollars().toFixed(2)}`;
|
|
87
|
+
}
|
|
88
|
+
/** Raw integer units (what gets stored in the database). */
|
|
89
|
+
toRaw() {
|
|
90
|
+
return this.raw;
|
|
91
|
+
}
|
|
92
|
+
/** Add another Credit, returning a new Credit. */
|
|
93
|
+
add(other) {
|
|
94
|
+
return new Credit(this.raw + other.raw);
|
|
95
|
+
}
|
|
96
|
+
/** Subtract another Credit, returning a new Credit (may be negative). */
|
|
97
|
+
subtract(other) {
|
|
98
|
+
return new Credit(this.raw - other.raw);
|
|
99
|
+
}
|
|
100
|
+
/** Multiply by a factor, rounding to nearest raw unit. */
|
|
101
|
+
multiply(factor) {
|
|
102
|
+
return new Credit(Math.round(this.raw * factor));
|
|
103
|
+
}
|
|
104
|
+
/** True if this credit is negative. */
|
|
105
|
+
isNegative() {
|
|
106
|
+
return this.raw < 0;
|
|
107
|
+
}
|
|
108
|
+
/** True if this credit is exactly zero. */
|
|
109
|
+
isZero() {
|
|
110
|
+
return this.raw === 0;
|
|
111
|
+
}
|
|
112
|
+
/** True if this credit is greater than other. */
|
|
113
|
+
greaterThan(other) {
|
|
114
|
+
return this.raw > other.raw;
|
|
115
|
+
}
|
|
116
|
+
/** True if this credit is less than other. */
|
|
117
|
+
lessThan(other) {
|
|
118
|
+
return this.raw < other.raw;
|
|
119
|
+
}
|
|
120
|
+
/** True if this credit equals other. */
|
|
121
|
+
equals(other) {
|
|
122
|
+
return this.raw === other.raw;
|
|
123
|
+
}
|
|
124
|
+
/** True if this credit is greater than or equal to other. */
|
|
125
|
+
greaterThanOrEqual(other) {
|
|
126
|
+
return this.raw >= other.raw;
|
|
127
|
+
}
|
|
128
|
+
/** True if this credit is less than or equal to other. */
|
|
129
|
+
lessThanOrEqual(other) {
|
|
130
|
+
return this.raw <= other.raw;
|
|
131
|
+
}
|
|
132
|
+
toString() {
|
|
133
|
+
return `Credit(raw=${this.raw})`;
|
|
134
|
+
}
|
|
135
|
+
/** Serialize to raw integer for JSON (API responses, database). */
|
|
136
|
+
toJSON() {
|
|
137
|
+
return this.raw;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { Credit } from "./credit.js";
|
|
3
|
+
describe("Credit", () => {
|
|
4
|
+
describe("factory methods", () => {
|
|
5
|
+
it("fromDollars creates correct raw value", () => {
|
|
6
|
+
const c = Credit.fromDollars(1);
|
|
7
|
+
expect(c.toRaw()).toBe(1_000_000_000);
|
|
8
|
+
});
|
|
9
|
+
it("fromDollars handles sub-cent precision", () => {
|
|
10
|
+
const c = Credit.fromDollars(0.001); // 1 mill
|
|
11
|
+
expect(c.toRaw()).toBe(1_000_000);
|
|
12
|
+
});
|
|
13
|
+
it("fromCents creates correct raw value", () => {
|
|
14
|
+
const c = Credit.fromCents(1);
|
|
15
|
+
expect(c.toRaw()).toBe(10_000_000);
|
|
16
|
+
});
|
|
17
|
+
it("fromCents handles fractional cents", () => {
|
|
18
|
+
const c = Credit.fromCents(0.5);
|
|
19
|
+
expect(c.toRaw()).toBe(5_000_000);
|
|
20
|
+
});
|
|
21
|
+
it("fromRaw stores exact value", () => {
|
|
22
|
+
const c = Credit.fromRaw(42);
|
|
23
|
+
expect(c.toRaw()).toBe(42);
|
|
24
|
+
});
|
|
25
|
+
it("fromRaw rejects non-integer", () => {
|
|
26
|
+
expect(() => Credit.fromRaw(1.5)).toThrow(TypeError);
|
|
27
|
+
});
|
|
28
|
+
it("zero creates zero credit", () => {
|
|
29
|
+
expect(Credit.zero().toRaw()).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
it("ZERO has raw value 0", () => {
|
|
32
|
+
expect(Credit.ZERO.toRaw()).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe("conversion", () => {
|
|
36
|
+
it("toDollars converts correctly", () => {
|
|
37
|
+
const c = Credit.fromRaw(1_000_000_000);
|
|
38
|
+
expect(c.toDollars()).toBe(1);
|
|
39
|
+
});
|
|
40
|
+
it("toCents converts correctly", () => {
|
|
41
|
+
const c = Credit.fromRaw(10_000_000);
|
|
42
|
+
expect(c.toCents()).toBe(1);
|
|
43
|
+
});
|
|
44
|
+
it("sub-cent value round-trips through fromCents/toCents", () => {
|
|
45
|
+
const c = Credit.fromCents(0.123);
|
|
46
|
+
// 0.123 cents * 10_000_000 raw-per-cent = 1_230_000
|
|
47
|
+
expect(c.toRaw()).toBe(1_230_000);
|
|
48
|
+
expect(c.toCents()).toBeCloseTo(0.123, 10);
|
|
49
|
+
});
|
|
50
|
+
it("toDisplayString formats as currency", () => {
|
|
51
|
+
expect(Credit.fromDollars(1.5).toDisplayString()).toBe("$1.50");
|
|
52
|
+
});
|
|
53
|
+
it("toCentsRounded rounds to nearest integer cent", () => {
|
|
54
|
+
// 0.123 cents -> rounds to 0
|
|
55
|
+
expect(Credit.fromCents(0.123).toCentsRounded()).toBe(0);
|
|
56
|
+
// 0.5 cents -> rounds to 1 (banker's rounding irrelevant, Math.round rounds 0.5 up)
|
|
57
|
+
expect(Credit.fromCents(0.5).toCentsRounded()).toBe(1);
|
|
58
|
+
// 0.7 cents -> rounds to 1
|
|
59
|
+
expect(Credit.fromCents(0.7).toCentsRounded()).toBe(1);
|
|
60
|
+
// exact integer cents pass through unchanged
|
|
61
|
+
expect(Credit.fromCents(500).toCentsRounded()).toBe(500);
|
|
62
|
+
});
|
|
63
|
+
it("toCentsFloor truncates toward negative infinity", () => {
|
|
64
|
+
// 0.9 cents -> floors to 0
|
|
65
|
+
expect(Credit.fromCents(0.9).toCentsFloor()).toBe(0);
|
|
66
|
+
// exact integer cents pass through unchanged
|
|
67
|
+
expect(Credit.fromCents(500).toCentsFloor()).toBe(500);
|
|
68
|
+
// negative: -0.1 cents -> floors to -1
|
|
69
|
+
expect(Credit.fromCents(-0.1).toCentsFloor()).toBe(-1);
|
|
70
|
+
});
|
|
71
|
+
it("toDisplayString rounds sub-cent to two decimals", () => {
|
|
72
|
+
// 1 raw unit = $0.000000001, should display as $0.00
|
|
73
|
+
expect(Credit.fromRaw(1).toDisplayString()).toBe("$0.00");
|
|
74
|
+
// $0.999 should display as $1.00
|
|
75
|
+
expect(Credit.fromRaw(999_000_000).toDisplayString()).toBe("$1.00");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe("arithmetic", () => {
|
|
79
|
+
it("add combines two credits", () => {
|
|
80
|
+
const a = Credit.fromCents(10);
|
|
81
|
+
const b = Credit.fromCents(20);
|
|
82
|
+
expect(a.add(b).toRaw()).toBe(300_000_000);
|
|
83
|
+
});
|
|
84
|
+
it("subtract removes credits", () => {
|
|
85
|
+
const a = Credit.fromCents(30);
|
|
86
|
+
const b = Credit.fromCents(10);
|
|
87
|
+
expect(a.subtract(b).toRaw()).toBe(200_000_000);
|
|
88
|
+
});
|
|
89
|
+
it("subtract can produce negative", () => {
|
|
90
|
+
const a = Credit.fromCents(5);
|
|
91
|
+
const b = Credit.fromCents(10);
|
|
92
|
+
expect(a.subtract(b).isNegative()).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it("multiply scales by factor", () => {
|
|
95
|
+
const c = Credit.fromDollars(1);
|
|
96
|
+
expect(c.multiply(2.5).toRaw()).toBe(2_500_000_000);
|
|
97
|
+
});
|
|
98
|
+
it("multiply rounds to integer raw", () => {
|
|
99
|
+
const c = Credit.fromRaw(3);
|
|
100
|
+
// 3 * 0.5 = 1.5, rounds to 2
|
|
101
|
+
expect(c.multiply(0.5).toRaw()).toBe(2);
|
|
102
|
+
});
|
|
103
|
+
it("multiply by zero gives zero", () => {
|
|
104
|
+
expect(Credit.fromDollars(5).multiply(0).isZero()).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe("comparison", () => {
|
|
108
|
+
it("isNegative returns true for negative", () => {
|
|
109
|
+
expect(Credit.fromRaw(-1).isNegative()).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
it("isNegative returns false for zero", () => {
|
|
112
|
+
expect(Credit.zero().isNegative()).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
it("isZero returns true for zero", () => {
|
|
115
|
+
expect(Credit.zero().isZero()).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
it("isZero returns false for non-zero", () => {
|
|
118
|
+
expect(Credit.fromRaw(1).isZero()).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
it("greaterThan compares correctly", () => {
|
|
121
|
+
const a = Credit.fromRaw(10);
|
|
122
|
+
const b = Credit.fromRaw(5);
|
|
123
|
+
expect(a.greaterThan(b)).toBe(true);
|
|
124
|
+
expect(b.greaterThan(a)).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
it("lessThan compares correctly", () => {
|
|
127
|
+
const a = Credit.fromRaw(5);
|
|
128
|
+
const b = Credit.fromRaw(10);
|
|
129
|
+
expect(a.lessThan(b)).toBe(true);
|
|
130
|
+
expect(b.lessThan(a)).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
it("equals compares correctly", () => {
|
|
133
|
+
const a = Credit.fromRaw(42);
|
|
134
|
+
const b = Credit.fromRaw(42);
|
|
135
|
+
const c = Credit.fromRaw(43);
|
|
136
|
+
expect(a.equals(b)).toBe(true);
|
|
137
|
+
expect(a.equals(c)).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
it("equals: fromDollars(1) equals fromCents(100)", () => {
|
|
140
|
+
expect(Credit.fromDollars(1).equals(Credit.fromCents(100))).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
it("lessThanOrEqual compares correctly", () => {
|
|
143
|
+
expect(Credit.fromCents(5).lessThanOrEqual(Credit.fromCents(5))).toBe(true);
|
|
144
|
+
expect(Credit.fromCents(5).lessThanOrEqual(Credit.fromCents(10))).toBe(true);
|
|
145
|
+
expect(Credit.fromCents(10).lessThanOrEqual(Credit.fromCents(5))).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
it("greaterThanOrEqual compares correctly", () => {
|
|
148
|
+
expect(Credit.fromCents(10).greaterThanOrEqual(Credit.fromCents(5))).toBe(true);
|
|
149
|
+
expect(Credit.fromCents(5).greaterThanOrEqual(Credit.fromCents(5))).toBe(true);
|
|
150
|
+
expect(Credit.fromCents(5).greaterThanOrEqual(Credit.fromCents(10))).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe("toString", () => {
|
|
154
|
+
it("returns debug representation", () => {
|
|
155
|
+
const c = Credit.fromRaw(123456);
|
|
156
|
+
expect(c.toString()).toBe("Credit(raw=123456)");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe("toJSON", () => {
|
|
160
|
+
it("toJSON returns raw value", () => {
|
|
161
|
+
const c = Credit.fromDollars(1);
|
|
162
|
+
expect(c.toJSON()).toBe(1_000_000_000);
|
|
163
|
+
expect(JSON.stringify({ amount: c })).toBe('{"amount":1000000000}');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe("sub-cent precision proof", () => {
|
|
167
|
+
it("$0.001 charge does not round to zero", () => {
|
|
168
|
+
const charge = Credit.fromDollars(0.001);
|
|
169
|
+
expect(charge.isZero()).toBe(false);
|
|
170
|
+
expect(charge.toRaw()).toBe(1_000_000);
|
|
171
|
+
});
|
|
172
|
+
it("$0.000000001 charge (1 raw unit) does not round to zero", () => {
|
|
173
|
+
const charge = Credit.fromDollars(0.000000001);
|
|
174
|
+
expect(charge.isZero()).toBe(false);
|
|
175
|
+
expect(charge.toRaw()).toBe(1);
|
|
176
|
+
});
|
|
177
|
+
it("old integer-cents would lose $0.001 but Credit preserves it", () => {
|
|
178
|
+
// In old system: Math.round(0.001 * 100) = 0 cents (lost!)
|
|
179
|
+
const oldCents = Math.round(0.001 * 100);
|
|
180
|
+
expect(oldCents).toBe(0);
|
|
181
|
+
// In new system: preserved
|
|
182
|
+
const credit = Credit.fromDollars(0.001);
|
|
183
|
+
expect(credit.toRaw()).toBe(1_000_000);
|
|
184
|
+
expect(credit.isZero()).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
it("accumulates sub-cent charges without loss", () => {
|
|
187
|
+
const tiny = Credit.fromDollars(0.0001); // 100_000 raw
|
|
188
|
+
let total = Credit.ZERO;
|
|
189
|
+
for (let i = 0; i < 10_000; i++) {
|
|
190
|
+
total = total.add(tiny);
|
|
191
|
+
}
|
|
192
|
+
expect(total.toDollars()).toBeCloseTo(1.0);
|
|
193
|
+
expect(total.toRaw()).toBe(1_000_000_000);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Credit } from "./credit.js";
|
|
2
|
+
import type { ICreditLedger } from "./credit-ledger.js";
|
|
3
|
+
import type { ICreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
4
|
+
export interface DividendCronConfig {
|
|
5
|
+
creditTransactionRepo: ICreditTransactionRepository;
|
|
6
|
+
ledger: ICreditLedger;
|
|
7
|
+
/** Fraction of daily purchases matched as dividend pool. Default 1.0 (100%). */
|
|
8
|
+
matchRate: number;
|
|
9
|
+
/** The date to compute dividend for, as YYYY-MM-DD string. Typically yesterday. */
|
|
10
|
+
targetDate: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DividendCronResult {
|
|
13
|
+
pool: Credit;
|
|
14
|
+
activeCount: number;
|
|
15
|
+
perUser: Credit;
|
|
16
|
+
distributed: number;
|
|
17
|
+
skippedAlreadyRun: boolean;
|
|
18
|
+
errors: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Compute and distribute the community dividend for a given day.
|
|
22
|
+
*
|
|
23
|
+
* 1. Check idempotency — skip if already run for this date.
|
|
24
|
+
* 2. Sum all 'purchase' transactions for the target date.
|
|
25
|
+
* 3. Find all tenants with a 'purchase' transaction in the last 7 days.
|
|
26
|
+
* 4. Compute pool = sum × matchRate, per-user share = floor(pool / activeCount).
|
|
27
|
+
* 5. Credit each active tenant with their share.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runDividendCron(cfg: DividendCronConfig): Promise<DividendCronResult>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { logger } from "../config/logger.js";
|
|
2
|
+
import { Credit } from "./credit.js";
|
|
3
|
+
/**
|
|
4
|
+
* Compute and distribute the community dividend for a given day.
|
|
5
|
+
*
|
|
6
|
+
* 1. Check idempotency — skip if already run for this date.
|
|
7
|
+
* 2. Sum all 'purchase' transactions for the target date.
|
|
8
|
+
* 3. Find all tenants with a 'purchase' transaction in the last 7 days.
|
|
9
|
+
* 4. Compute pool = sum × matchRate, per-user share = floor(pool / activeCount).
|
|
10
|
+
* 5. Credit each active tenant with their share.
|
|
11
|
+
*/
|
|
12
|
+
export async function runDividendCron(cfg) {
|
|
13
|
+
const result = {
|
|
14
|
+
pool: Credit.ZERO,
|
|
15
|
+
activeCount: 0,
|
|
16
|
+
perUser: Credit.ZERO,
|
|
17
|
+
distributed: 0,
|
|
18
|
+
skippedAlreadyRun: false,
|
|
19
|
+
errors: [],
|
|
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
|
+
const sentinelPrefix = `dividend:${cfg.targetDate}:`;
|
|
24
|
+
const alreadyRan = await cfg.creditTransactionRepo.existsByReferenceIdLike(`${sentinelPrefix}%`);
|
|
25
|
+
if (alreadyRan) {
|
|
26
|
+
result.skippedAlreadyRun = true;
|
|
27
|
+
logger.info("Dividend cron already ran for this date", { targetDate: cfg.targetDate });
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
// Step 1: Sum all purchase amounts for the target date.
|
|
31
|
+
const dayStart = `${cfg.targetDate} 00:00:00`;
|
|
32
|
+
const dayEnd = `${cfg.targetDate} 24:00:00`;
|
|
33
|
+
const dailyPurchaseTotalCredit = await cfg.creditTransactionRepo.sumPurchasesForPeriod(dayStart, dayEnd);
|
|
34
|
+
result.pool = dailyPurchaseTotalCredit.multiply(cfg.matchRate);
|
|
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.
|
|
38
|
+
const windowStart = subtractDays(cfg.targetDate, 6);
|
|
39
|
+
const windowStartTs = `${windowStart} 00:00:00`;
|
|
40
|
+
const activeTenantIds = await cfg.creditTransactionRepo.getActiveTenantIdsInWindow(windowStartTs, dayEnd);
|
|
41
|
+
result.activeCount = activeTenantIds.length;
|
|
42
|
+
// Step 3: Compute per-user share.
|
|
43
|
+
if (result.pool.isZero() || result.activeCount <= 0) {
|
|
44
|
+
logger.info("Dividend cron: no pool or no active tenants", {
|
|
45
|
+
targetDate: cfg.targetDate,
|
|
46
|
+
pool: result.pool.toCents(),
|
|
47
|
+
activeCount: result.activeCount,
|
|
48
|
+
});
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
result.perUser = Credit.fromRaw(Math.floor(result.pool.toRaw() / result.activeCount));
|
|
52
|
+
if (result.perUser.isZero()) {
|
|
53
|
+
logger.info("Dividend cron: per-user share rounds to zero", {
|
|
54
|
+
targetDate: cfg.targetDate,
|
|
55
|
+
pool: result.pool.toCents(),
|
|
56
|
+
activeCount: result.activeCount,
|
|
57
|
+
});
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
// Step 4: Distribute to each active tenant.
|
|
61
|
+
for (const tenantId of activeTenantIds) {
|
|
62
|
+
const perUserRef = `dividend:${cfg.targetDate}:${tenantId}`;
|
|
63
|
+
try {
|
|
64
|
+
await cfg.ledger.credit(tenantId, result.perUser, "community_dividend", `Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`, perUserRef);
|
|
65
|
+
result.distributed++;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
69
|
+
logger.error("Dividend distribution failed for tenant", { tenantId, error: msg });
|
|
70
|
+
result.errors.push(`${tenantId}: ${msg}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
logger.info("Dividend cron complete", {
|
|
74
|
+
targetDate: cfg.targetDate,
|
|
75
|
+
pool: result.pool.toCents(),
|
|
76
|
+
activeCount: result.activeCount,
|
|
77
|
+
perUser: result.perUser.toCents(),
|
|
78
|
+
distributed: result.distributed,
|
|
79
|
+
errors: result.errors.length,
|
|
80
|
+
});
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
/** Subtract N days from a YYYY-MM-DD date string, returning YYYY-MM-DD. */
|
|
84
|
+
function subtractDays(dateStr, days) {
|
|
85
|
+
const d = new Date(`${dateStr}T00:00:00Z`);
|
|
86
|
+
d.setUTCDate(d.getUTCDate() - days);
|
|
87
|
+
return d.toISOString().slice(0, 10);
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { creditBalances, creditTransactions } from "../db/schema/credits.js";
|
|
3
|
+
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
4
|
+
import { Credit } from "./credit.js";
|
|
5
|
+
import { CreditLedger } from "./credit-ledger.js";
|
|
6
|
+
import { DrizzleCreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
7
|
+
import { runDividendCron } from "./dividend-cron.js";
|
|
8
|
+
async function insertPurchase(db, tenantId, amountCents, createdAt) {
|
|
9
|
+
const id = `test-${tenantId}-${Date.now()}-${Math.random()}`;
|
|
10
|
+
const amount = Credit.fromCents(amountCents);
|
|
11
|
+
await db.insert(creditTransactions).values({
|
|
12
|
+
id,
|
|
13
|
+
tenantId,
|
|
14
|
+
amount,
|
|
15
|
+
balanceAfter: amount,
|
|
16
|
+
type: "purchase",
|
|
17
|
+
createdAt,
|
|
18
|
+
});
|
|
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
|
+
}
|
|
34
|
+
describe("runDividendCron", () => {
|
|
35
|
+
let pool;
|
|
36
|
+
let db;
|
|
37
|
+
let ledger;
|
|
38
|
+
let creditTransactionRepo;
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
({ db, pool } = await createTestDb());
|
|
41
|
+
});
|
|
42
|
+
afterAll(async () => {
|
|
43
|
+
await pool.close();
|
|
44
|
+
});
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
await truncateAllTables(pool);
|
|
47
|
+
ledger = new CreditLedger(db);
|
|
48
|
+
creditTransactionRepo = new DrizzleCreditTransactionRepository(db);
|
|
49
|
+
});
|
|
50
|
+
function makeConfig(overrides) {
|
|
51
|
+
return {
|
|
52
|
+
creditTransactionRepo,
|
|
53
|
+
ledger,
|
|
54
|
+
matchRate: 1.0,
|
|
55
|
+
targetDate: "2026-02-20",
|
|
56
|
+
...overrides,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
it("distributes dividend to eligible tenants", async () => {
|
|
60
|
+
await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
|
|
61
|
+
const result = await runDividendCron(makeConfig());
|
|
62
|
+
expect(result.distributed).toBe(1);
|
|
63
|
+
expect(result.pool.toCents()).toBe(1000);
|
|
64
|
+
expect(result.perUser.toCents()).toBe(1000);
|
|
65
|
+
expect(result.activeCount).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
it("is idempotent — skips if already ran for the date", async () => {
|
|
68
|
+
await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
|
|
69
|
+
const result1 = await runDividendCron(makeConfig());
|
|
70
|
+
expect(result1.distributed).toBe(1);
|
|
71
|
+
expect(result1.skippedAlreadyRun).toBe(false);
|
|
72
|
+
const balanceAfterFirst = await ledger.balance("t1");
|
|
73
|
+
const result2 = await runDividendCron(makeConfig());
|
|
74
|
+
expect(result2.skippedAlreadyRun).toBe(true);
|
|
75
|
+
expect(result2.distributed).toBe(0);
|
|
76
|
+
expect((await ledger.balance("t1")).equals(balanceAfterFirst)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
it("handles floor rounding — remainder is not distributed", async () => {
|
|
79
|
+
await insertPurchase(db, "t1", 50, "2026-02-20 12:00:00");
|
|
80
|
+
await insertPurchase(db, "t2", 30, "2026-02-20 12:00:00");
|
|
81
|
+
await insertPurchase(db, "t3", 20, "2026-02-20 12:00:00");
|
|
82
|
+
const result = await runDividendCron(makeConfig());
|
|
83
|
+
expect(result.pool.toCents()).toBe(100);
|
|
84
|
+
expect(result.activeCount).toBe(3);
|
|
85
|
+
// 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
|
+
expect(result.perUser.toRaw()).toBe(333_333_333);
|
|
88
|
+
expect(result.distributed).toBe(3);
|
|
89
|
+
});
|
|
90
|
+
it("skips distribution when pool is zero", async () => {
|
|
91
|
+
// Tenant purchased within 7 days but NOT on target date -> pool = 0
|
|
92
|
+
await insertPurchase(db, "t1", 500, "2026-02-18 12:00:00");
|
|
93
|
+
const result = await runDividendCron(makeConfig());
|
|
94
|
+
expect(result.pool.toCents()).toBe(0);
|
|
95
|
+
expect(result.activeCount).toBe(1);
|
|
96
|
+
expect(result.perUser.toCents()).toBe(0);
|
|
97
|
+
expect(result.distributed).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
it("distributes sub-cent amounts at nanodollar precision", async () => {
|
|
100
|
+
// 1 cent purchase, 3 active users: pool = 10_000_000 raw
|
|
101
|
+
// floor(10_000_000 / 3) = 3_333_333 raw each — non-zero, gets distributed
|
|
102
|
+
await insertPurchase(db, "t1", 1, "2026-02-20 12:00:00");
|
|
103
|
+
await insertPurchase(db, "t2", 500, "2026-02-18 12:00:00");
|
|
104
|
+
await insertPurchase(db, "t3", 500, "2026-02-17 12:00:00");
|
|
105
|
+
const result = await runDividendCron(makeConfig({ matchRate: 1.0 }));
|
|
106
|
+
expect(result.pool.toCents()).toBe(1);
|
|
107
|
+
expect(result.activeCount).toBe(3);
|
|
108
|
+
expect(result.perUser.toRaw()).toBe(3_333_333);
|
|
109
|
+
expect(result.distributed).toBe(3);
|
|
110
|
+
});
|
|
111
|
+
it("records transactions with correct type and referenceId", async () => {
|
|
112
|
+
await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
|
|
113
|
+
await runDividendCron(makeConfig());
|
|
114
|
+
const history = await ledger.history("t1", { type: "community_dividend" });
|
|
115
|
+
expect(history).toHaveLength(1);
|
|
116
|
+
expect(history[0].type).toBe("community_dividend");
|
|
117
|
+
expect(history[0].referenceId).toBe("dividend:2026-02-20:t1");
|
|
118
|
+
expect(history[0].amount.toCents()).toBe(1000);
|
|
119
|
+
expect(history[0].description).toContain("Community dividend");
|
|
120
|
+
});
|
|
121
|
+
it("collects errors without stopping distribution to other tenants", async () => {
|
|
122
|
+
await insertPurchase(db, "t1", 500, "2026-02-20 12:00:00");
|
|
123
|
+
await insertPurchase(db, "t2", 500, "2026-02-20 12:00:00");
|
|
124
|
+
const result = await runDividendCron(makeConfig());
|
|
125
|
+
expect(result.distributed).toBe(2);
|
|
126
|
+
expect(result.errors).toEqual([]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PlatformDb } from "../db/index.js";
|
|
2
|
+
import { Credit } from "./credit.js";
|
|
3
|
+
import type { DividendHistoryEntry, DividendStats } from "./repository-types.js";
|
|
4
|
+
export type { DividendHistoryEntry, DividendStats };
|
|
5
|
+
export interface DigestTenantRow {
|
|
6
|
+
tenantId: string;
|
|
7
|
+
total: Credit;
|
|
8
|
+
distributionCount: number;
|
|
9
|
+
avgPool: Credit;
|
|
10
|
+
avgActiveUsers: number;
|
|
11
|
+
}
|
|
12
|
+
export interface IDividendRepository {
|
|
13
|
+
getStats(tenantId: string): Promise<DividendStats>;
|
|
14
|
+
getHistory(tenantId: string, limit: number, offset: number): Promise<DividendHistoryEntry[]>;
|
|
15
|
+
getLifetimeTotal(tenantId: string): Promise<Credit>;
|
|
16
|
+
/** Aggregate dividend distributions per tenant for a date window [windowStart, windowEnd). */
|
|
17
|
+
getDigestTenantAggregates(windowStart: string, windowEnd: string): Promise<DigestTenantRow[]>;
|
|
18
|
+
/** Resolve email for a tenant from admin_users. Returns undefined if no row exists. */
|
|
19
|
+
getTenantEmail(tenantId: string): Promise<string | undefined>;
|
|
20
|
+
}
|
|
21
|
+
export declare class DrizzleDividendRepository implements IDividendRepository {
|
|
22
|
+
private readonly db;
|
|
23
|
+
constructor(db: PlatformDb);
|
|
24
|
+
getStats(tenantId: string): Promise<DividendStats>;
|
|
25
|
+
getHistory(tenantId: string, limit: number, offset: number): Promise<DividendHistoryEntry[]>;
|
|
26
|
+
getLifetimeTotal(tenantId: string): Promise<Credit>;
|
|
27
|
+
getDigestTenantAggregates(windowStart: string, windowEnd: string): Promise<DigestTenantRow[]>;
|
|
28
|
+
getTenantEmail(tenantId: string): Promise<string | undefined>;
|
|
29
|
+
}
|