@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,184 @@
|
|
|
1
|
+
import { and, eq, lte, sql } from "drizzle-orm";
|
|
2
|
+
import { creditAutoTopupSettings } from "../db/schema/credit-auto-topup-settings.js";
|
|
3
|
+
import { Credit } from "./credit.js";
|
|
4
|
+
export const ALLOWED_TOPUP_AMOUNTS = [500, 1000, 2000, 5000, 10000, 20000, 50000];
|
|
5
|
+
export const ALLOWED_THRESHOLDS = [200, 500, 1000];
|
|
6
|
+
export const ALLOWED_SCHEDULE_INTERVALS = ["daily", "weekly", "monthly"];
|
|
7
|
+
export function computeNextScheduleAt(interval, now = new Date()) {
|
|
8
|
+
if (!interval)
|
|
9
|
+
return null;
|
|
10
|
+
const next = new Date(now);
|
|
11
|
+
if (interval === "daily") {
|
|
12
|
+
next.setUTCDate(next.getUTCDate() + 1);
|
|
13
|
+
next.setUTCHours(0, 0, 0, 0);
|
|
14
|
+
}
|
|
15
|
+
else if (interval === "weekly") {
|
|
16
|
+
next.setUTCDate(next.getUTCDate() + 7);
|
|
17
|
+
next.setUTCHours(0, 0, 0, 0);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
next.setUTCMonth(next.getUTCMonth() + 1);
|
|
21
|
+
next.setUTCDate(1);
|
|
22
|
+
next.setUTCHours(0, 0, 0, 0);
|
|
23
|
+
}
|
|
24
|
+
return next.toISOString();
|
|
25
|
+
}
|
|
26
|
+
export class DrizzleAutoTopupSettingsRepository {
|
|
27
|
+
db;
|
|
28
|
+
constructor(db) {
|
|
29
|
+
this.db = db;
|
|
30
|
+
}
|
|
31
|
+
async getByTenant(tenantId) {
|
|
32
|
+
const row = (await this.db.select().from(creditAutoTopupSettings).where(eq(creditAutoTopupSettings.tenantId, tenantId)))[0];
|
|
33
|
+
return row ? mapRow(row) : null;
|
|
34
|
+
}
|
|
35
|
+
async upsert(tenantId, settings) {
|
|
36
|
+
const now = sql `now()`;
|
|
37
|
+
const values = {
|
|
38
|
+
tenantId,
|
|
39
|
+
updatedAt: now,
|
|
40
|
+
};
|
|
41
|
+
const updateSet = { updatedAt: now };
|
|
42
|
+
if (settings.usageEnabled !== undefined) {
|
|
43
|
+
values.usageEnabled = settings.usageEnabled;
|
|
44
|
+
updateSet.usageEnabled = values.usageEnabled;
|
|
45
|
+
}
|
|
46
|
+
if (settings.usageThreshold !== undefined) {
|
|
47
|
+
const cents = settings.usageThreshold.toCentsRounded();
|
|
48
|
+
values.usageThresholdCents = cents;
|
|
49
|
+
updateSet.usageThresholdCents = cents;
|
|
50
|
+
}
|
|
51
|
+
if (settings.usageTopup !== undefined) {
|
|
52
|
+
const cents = settings.usageTopup.toCentsRounded();
|
|
53
|
+
values.usageTopupCents = cents;
|
|
54
|
+
updateSet.usageTopupCents = cents;
|
|
55
|
+
}
|
|
56
|
+
if (settings.scheduleEnabled !== undefined) {
|
|
57
|
+
values.scheduleEnabled = settings.scheduleEnabled;
|
|
58
|
+
updateSet.scheduleEnabled = values.scheduleEnabled;
|
|
59
|
+
}
|
|
60
|
+
if (settings.scheduleAmount !== undefined) {
|
|
61
|
+
const cents = settings.scheduleAmount.toCentsRounded();
|
|
62
|
+
values.scheduleAmountCents = cents;
|
|
63
|
+
updateSet.scheduleAmountCents = cents;
|
|
64
|
+
}
|
|
65
|
+
if (settings.scheduleIntervalHours !== undefined) {
|
|
66
|
+
values.scheduleIntervalHours = settings.scheduleIntervalHours;
|
|
67
|
+
updateSet.scheduleIntervalHours = settings.scheduleIntervalHours;
|
|
68
|
+
}
|
|
69
|
+
if (settings.scheduleNextAt !== undefined) {
|
|
70
|
+
values.scheduleNextAt = settings.scheduleNextAt;
|
|
71
|
+
updateSet.scheduleNextAt = settings.scheduleNextAt;
|
|
72
|
+
}
|
|
73
|
+
await this.db
|
|
74
|
+
.insert(creditAutoTopupSettings)
|
|
75
|
+
.values(values)
|
|
76
|
+
.onConflictDoUpdate({ target: creditAutoTopupSettings.tenantId, set: updateSet });
|
|
77
|
+
}
|
|
78
|
+
async setUsageChargeInFlight(tenantId, inFlight) {
|
|
79
|
+
await this.db
|
|
80
|
+
.update(creditAutoTopupSettings)
|
|
81
|
+
.set({ usageChargeInFlight: inFlight, updatedAt: sql `now()` })
|
|
82
|
+
.where(eq(creditAutoTopupSettings.tenantId, tenantId));
|
|
83
|
+
}
|
|
84
|
+
async tryAcquireUsageInFlight(tenantId) {
|
|
85
|
+
const rows = await this.db
|
|
86
|
+
.update(creditAutoTopupSettings)
|
|
87
|
+
.set({ usageChargeInFlight: true, updatedAt: sql `now()` })
|
|
88
|
+
.where(and(eq(creditAutoTopupSettings.tenantId, tenantId), eq(creditAutoTopupSettings.usageChargeInFlight, false)))
|
|
89
|
+
.returning({ tenantId: creditAutoTopupSettings.tenantId });
|
|
90
|
+
return rows.length > 0;
|
|
91
|
+
}
|
|
92
|
+
async incrementUsageFailures(tenantId) {
|
|
93
|
+
await this.db
|
|
94
|
+
.update(creditAutoTopupSettings)
|
|
95
|
+
.set({
|
|
96
|
+
usageConsecutiveFailures: sql `${creditAutoTopupSettings.usageConsecutiveFailures} + 1`,
|
|
97
|
+
updatedAt: sql `now()`,
|
|
98
|
+
})
|
|
99
|
+
.where(eq(creditAutoTopupSettings.tenantId, tenantId));
|
|
100
|
+
const row = await this.getByTenant(tenantId);
|
|
101
|
+
return row?.usageConsecutiveFailures ?? 0;
|
|
102
|
+
}
|
|
103
|
+
async resetUsageFailures(tenantId) {
|
|
104
|
+
await this.db
|
|
105
|
+
.update(creditAutoTopupSettings)
|
|
106
|
+
.set({ usageConsecutiveFailures: 0, updatedAt: sql `now()` })
|
|
107
|
+
.where(eq(creditAutoTopupSettings.tenantId, tenantId));
|
|
108
|
+
}
|
|
109
|
+
async disableUsage(tenantId) {
|
|
110
|
+
await this.db
|
|
111
|
+
.update(creditAutoTopupSettings)
|
|
112
|
+
.set({ usageEnabled: false, updatedAt: sql `now()` })
|
|
113
|
+
.where(eq(creditAutoTopupSettings.tenantId, tenantId));
|
|
114
|
+
}
|
|
115
|
+
async incrementScheduleFailures(tenantId) {
|
|
116
|
+
await this.db
|
|
117
|
+
.update(creditAutoTopupSettings)
|
|
118
|
+
.set({
|
|
119
|
+
scheduleConsecutiveFailures: sql `${creditAutoTopupSettings.scheduleConsecutiveFailures} + 1`,
|
|
120
|
+
updatedAt: sql `now()`,
|
|
121
|
+
})
|
|
122
|
+
.where(eq(creditAutoTopupSettings.tenantId, tenantId));
|
|
123
|
+
const row = await this.getByTenant(tenantId);
|
|
124
|
+
return row?.scheduleConsecutiveFailures ?? 0;
|
|
125
|
+
}
|
|
126
|
+
async resetScheduleFailures(tenantId) {
|
|
127
|
+
await this.db
|
|
128
|
+
.update(creditAutoTopupSettings)
|
|
129
|
+
.set({ scheduleConsecutiveFailures: 0, updatedAt: sql `now()` })
|
|
130
|
+
.where(eq(creditAutoTopupSettings.tenantId, tenantId));
|
|
131
|
+
}
|
|
132
|
+
async disableSchedule(tenantId) {
|
|
133
|
+
await this.db
|
|
134
|
+
.update(creditAutoTopupSettings)
|
|
135
|
+
.set({ scheduleEnabled: false, updatedAt: sql `now()` })
|
|
136
|
+
.where(eq(creditAutoTopupSettings.tenantId, tenantId));
|
|
137
|
+
}
|
|
138
|
+
async advanceScheduleNextAt(tenantId) {
|
|
139
|
+
const settings = await this.getByTenant(tenantId);
|
|
140
|
+
if (!settings?.scheduleNextAt)
|
|
141
|
+
return;
|
|
142
|
+
const currentNext = new Date(settings.scheduleNextAt);
|
|
143
|
+
const newNext = new Date(currentNext.getTime() + settings.scheduleIntervalHours * 60 * 60 * 1000);
|
|
144
|
+
await this.db
|
|
145
|
+
.update(creditAutoTopupSettings)
|
|
146
|
+
.set({ scheduleNextAt: newNext.toISOString(), updatedAt: sql `now()` })
|
|
147
|
+
.where(eq(creditAutoTopupSettings.tenantId, tenantId));
|
|
148
|
+
}
|
|
149
|
+
async getMaxConsecutiveFailures() {
|
|
150
|
+
const result = await this.db
|
|
151
|
+
.select({
|
|
152
|
+
maxFailures: sql `GREATEST(
|
|
153
|
+
COALESCE(MAX(${creditAutoTopupSettings.usageConsecutiveFailures}), 0),
|
|
154
|
+
COALESCE(MAX(${creditAutoTopupSettings.scheduleConsecutiveFailures}), 0)
|
|
155
|
+
)`,
|
|
156
|
+
})
|
|
157
|
+
.from(creditAutoTopupSettings);
|
|
158
|
+
return result[0]?.maxFailures ?? 0;
|
|
159
|
+
}
|
|
160
|
+
async listDueScheduled(now) {
|
|
161
|
+
const rows = await this.db
|
|
162
|
+
.select()
|
|
163
|
+
.from(creditAutoTopupSettings)
|
|
164
|
+
.where(and(eq(creditAutoTopupSettings.scheduleEnabled, true), lte(creditAutoTopupSettings.scheduleNextAt, now)));
|
|
165
|
+
return rows.map(mapRow);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function mapRow(row) {
|
|
169
|
+
return {
|
|
170
|
+
tenantId: row.tenantId,
|
|
171
|
+
usageEnabled: row.usageEnabled,
|
|
172
|
+
usageThreshold: Credit.fromCents(row.usageThresholdCents),
|
|
173
|
+
usageTopup: Credit.fromCents(row.usageTopupCents),
|
|
174
|
+
usageConsecutiveFailures: row.usageConsecutiveFailures,
|
|
175
|
+
usageChargeInFlight: row.usageChargeInFlight,
|
|
176
|
+
scheduleEnabled: row.scheduleEnabled,
|
|
177
|
+
scheduleAmount: Credit.fromCents(row.scheduleAmountCents),
|
|
178
|
+
scheduleIntervalHours: row.scheduleIntervalHours,
|
|
179
|
+
scheduleNextAt: row.scheduleNextAt,
|
|
180
|
+
scheduleConsecutiveFailures: row.scheduleConsecutiveFailures,
|
|
181
|
+
createdAt: row.createdAt,
|
|
182
|
+
updatedAt: row.updatedAt,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../test/db.js";
|
|
3
|
+
import { Credit } from "./credit.js";
|
|
4
|
+
import { DrizzleAutoTopupSettingsRepository } from "./auto-topup-settings-repository.js";
|
|
5
|
+
describe("DrizzleAutoTopupSettingsRepository", () => {
|
|
6
|
+
let pool;
|
|
7
|
+
let db;
|
|
8
|
+
let repo;
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
({ db, pool } = await createTestDb());
|
|
11
|
+
await beginTestTransaction(pool);
|
|
12
|
+
});
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
await endTestTransaction(pool);
|
|
15
|
+
await pool.close();
|
|
16
|
+
});
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
await rollbackTestTransaction(pool);
|
|
19
|
+
repo = new DrizzleAutoTopupSettingsRepository(db);
|
|
20
|
+
});
|
|
21
|
+
it("returns null for unknown tenant", async () => {
|
|
22
|
+
expect(await repo.getByTenant("unknown")).toBeNull();
|
|
23
|
+
});
|
|
24
|
+
it("upsert creates settings and getByTenant retrieves them", async () => {
|
|
25
|
+
await repo.upsert("t1", {
|
|
26
|
+
usageEnabled: true,
|
|
27
|
+
usageThreshold: Credit.fromCents(200),
|
|
28
|
+
usageTopup: Credit.fromCents(1000),
|
|
29
|
+
});
|
|
30
|
+
const s = await repo.getByTenant("t1");
|
|
31
|
+
expect(s).not.toBeNull();
|
|
32
|
+
expect(s?.usageEnabled).toBe(true);
|
|
33
|
+
expect(s?.usageThreshold.toCents()).toBe(200);
|
|
34
|
+
expect(s?.usageTopup.toCents()).toBe(1000);
|
|
35
|
+
expect(s?.scheduleEnabled).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
it("upsert updates existing settings", async () => {
|
|
38
|
+
await repo.upsert("t1", { usageEnabled: true });
|
|
39
|
+
await repo.upsert("t1", { usageThreshold: Credit.fromCents(300) });
|
|
40
|
+
const s = await repo.getByTenant("t1");
|
|
41
|
+
expect(s?.usageEnabled).toBe(true);
|
|
42
|
+
expect(s?.usageThreshold.toCents()).toBe(300);
|
|
43
|
+
});
|
|
44
|
+
it("setUsageChargeInFlight toggles the flag", async () => {
|
|
45
|
+
await repo.upsert("t1", { usageEnabled: true });
|
|
46
|
+
await repo.setUsageChargeInFlight("t1", true);
|
|
47
|
+
expect((await repo.getByTenant("t1"))?.usageChargeInFlight).toBe(true);
|
|
48
|
+
await repo.setUsageChargeInFlight("t1", false);
|
|
49
|
+
expect((await repo.getByTenant("t1"))?.usageChargeInFlight).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
it("incrementUsageFailures increments and returns count", async () => {
|
|
52
|
+
await repo.upsert("t1", { usageEnabled: true });
|
|
53
|
+
expect(await repo.incrementUsageFailures("t1")).toBe(1);
|
|
54
|
+
expect(await repo.incrementUsageFailures("t1")).toBe(2);
|
|
55
|
+
expect(await repo.incrementUsageFailures("t1")).toBe(3);
|
|
56
|
+
});
|
|
57
|
+
it("resetUsageFailures resets to zero", async () => {
|
|
58
|
+
await repo.upsert("t1", { usageEnabled: true });
|
|
59
|
+
await repo.incrementUsageFailures("t1");
|
|
60
|
+
await repo.incrementUsageFailures("t1");
|
|
61
|
+
await repo.resetUsageFailures("t1");
|
|
62
|
+
expect((await repo.getByTenant("t1"))?.usageConsecutiveFailures).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
it("disableUsage sets usage_enabled to false", async () => {
|
|
65
|
+
await repo.upsert("t1", { usageEnabled: true });
|
|
66
|
+
await repo.disableUsage("t1");
|
|
67
|
+
expect((await repo.getByTenant("t1"))?.usageEnabled).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
it("incrementScheduleFailures increments and returns count", async () => {
|
|
70
|
+
await repo.upsert("t1", { scheduleEnabled: true });
|
|
71
|
+
expect(await repo.incrementScheduleFailures("t1")).toBe(1);
|
|
72
|
+
expect(await repo.incrementScheduleFailures("t1")).toBe(2);
|
|
73
|
+
});
|
|
74
|
+
it("disableSchedule sets schedule_enabled to false", async () => {
|
|
75
|
+
await repo.upsert("t1", { scheduleEnabled: true });
|
|
76
|
+
await repo.disableSchedule("t1");
|
|
77
|
+
expect((await repo.getByTenant("t1"))?.scheduleEnabled).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
it("getMaxConsecutiveFailures returns max across usage and schedule", async () => {
|
|
80
|
+
await repo.upsert("tenant-a", { usageEnabled: true });
|
|
81
|
+
await repo.upsert("tenant-b", { usageEnabled: true });
|
|
82
|
+
await repo.incrementUsageFailures("tenant-a"); // 1
|
|
83
|
+
await repo.incrementUsageFailures("tenant-a"); // 2
|
|
84
|
+
await repo.incrementScheduleFailures("tenant-b"); // 1
|
|
85
|
+
await repo.incrementScheduleFailures("tenant-b"); // 2
|
|
86
|
+
await repo.incrementScheduleFailures("tenant-b"); // 3
|
|
87
|
+
const max = await repo.getMaxConsecutiveFailures();
|
|
88
|
+
expect(max).toBe(3);
|
|
89
|
+
});
|
|
90
|
+
it("getMaxConsecutiveFailures returns 0 when no settings exist", async () => {
|
|
91
|
+
const max = await repo.getMaxConsecutiveFailures();
|
|
92
|
+
expect(max).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
it("listDueScheduled returns tenants with schedule_next_at <= now", async () => {
|
|
95
|
+
const past = "2026-02-20T00:00:00.000Z";
|
|
96
|
+
const future = "2026-12-31T00:00:00.000Z";
|
|
97
|
+
await repo.upsert("t1", { scheduleEnabled: true, scheduleAmount: Credit.fromCents(500), scheduleNextAt: past });
|
|
98
|
+
await repo.upsert("t2", { scheduleEnabled: true, scheduleAmount: Credit.fromCents(1000), scheduleNextAt: future });
|
|
99
|
+
await repo.upsert("t3", { scheduleEnabled: false, scheduleNextAt: past });
|
|
100
|
+
const due = await repo.listDueScheduled("2026-02-22T00:00:00.000Z");
|
|
101
|
+
expect(due).toHaveLength(1);
|
|
102
|
+
expect(due[0].tenantId).toBe("t1");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Credit } from "./credit.js";
|
|
2
|
+
import type { AutoTopupChargeResult } from "./auto-topup-charge.js";
|
|
3
|
+
import type { IAutoTopupSettingsRepository } from "./auto-topup-settings-repository.js";
|
|
4
|
+
import type { ICreditLedger } from "./credit-ledger.js";
|
|
5
|
+
export interface UsageTopupDeps {
|
|
6
|
+
settingsRepo: IAutoTopupSettingsRepository;
|
|
7
|
+
creditLedger: ICreditLedger;
|
|
8
|
+
/** Injected charge function (allows mocking in tests). */
|
|
9
|
+
chargeAutoTopup: (tenantId: string, amount: Credit, source: string) => Promise<AutoTopupChargeResult>;
|
|
10
|
+
/** Optional tenant status check. If provided and returns non-null, skip the charge. */
|
|
11
|
+
checkTenantStatus?: (tenantId: string) => Promise<{
|
|
12
|
+
error: string;
|
|
13
|
+
message: string;
|
|
14
|
+
} | null>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check whether a usage-based auto-topup should fire for a tenant.
|
|
18
|
+
*
|
|
19
|
+
* Called async after every credit debit. Fire-and-forget -- caller catches errors.
|
|
20
|
+
* Idempotent: skips if a charge is already in-flight for this tenant.
|
|
21
|
+
*/
|
|
22
|
+
export declare function maybeTriggerUsageTopup(deps: UsageTopupDeps, tenantId: string): Promise<void>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { logger } from "../config/logger.js";
|
|
2
|
+
import { MAX_CONSECUTIVE_FAILURES } from "./auto-topup-charge.js";
|
|
3
|
+
/**
|
|
4
|
+
* Check whether a usage-based auto-topup should fire for a tenant.
|
|
5
|
+
*
|
|
6
|
+
* Called async after every credit debit. Fire-and-forget -- caller catches errors.
|
|
7
|
+
* Idempotent: skips if a charge is already in-flight for this tenant.
|
|
8
|
+
*/
|
|
9
|
+
export async function maybeTriggerUsageTopup(deps, tenantId) {
|
|
10
|
+
// 1. Look up settings
|
|
11
|
+
const settings = await deps.settingsRepo.getByTenant(tenantId);
|
|
12
|
+
if (!settings || !settings.usageEnabled)
|
|
13
|
+
return;
|
|
14
|
+
// 1b. Check tenant status (skip banned/suspended accounts)
|
|
15
|
+
if (deps.checkTenantStatus) {
|
|
16
|
+
const statusErr = await deps.checkTenantStatus(tenantId);
|
|
17
|
+
if (statusErr)
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// 2. Check balance vs threshold
|
|
21
|
+
const balance = await deps.creditLedger.balance(tenantId);
|
|
22
|
+
if (!balance.lessThan(settings.usageThreshold))
|
|
23
|
+
return;
|
|
24
|
+
// 3. Atomic in-flight guard — compare-and-swap prevents duplicate charges
|
|
25
|
+
const acquired = await deps.settingsRepo.tryAcquireUsageInFlight(tenantId);
|
|
26
|
+
if (!acquired)
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
// 4. Execute charge
|
|
30
|
+
const result = await deps.chargeAutoTopup(tenantId, settings.usageTopup, "auto_topup_usage");
|
|
31
|
+
if (result.success) {
|
|
32
|
+
await deps.settingsRepo.resetUsageFailures(tenantId);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const failureCount = await deps.settingsRepo.incrementUsageFailures(tenantId);
|
|
36
|
+
if (failureCount >= MAX_CONSECUTIVE_FAILURES) {
|
|
37
|
+
await deps.settingsRepo.disableUsage(tenantId);
|
|
38
|
+
logger.warn("Usage auto-topup disabled after consecutive failures", { tenantId, failureCount });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const failureCount = await deps.settingsRepo.incrementUsageFailures(tenantId);
|
|
44
|
+
if (failureCount >= MAX_CONSECUTIVE_FAILURES) {
|
|
45
|
+
await deps.settingsRepo.disableUsage(tenantId);
|
|
46
|
+
}
|
|
47
|
+
logger.error("Usage auto-topup unexpected error", {
|
|
48
|
+
tenantId,
|
|
49
|
+
error: err instanceof Error ? err.message : String(err),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
// 6. Clear in-flight flag
|
|
54
|
+
await deps.settingsRepo.setUsageChargeInFlight(tenantId, false);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
3
|
+
import { Credit } from "./credit.js";
|
|
4
|
+
import { DrizzleAutoTopupSettingsRepository } from "./auto-topup-settings-repository.js";
|
|
5
|
+
import { maybeTriggerUsageTopup } from "./auto-topup-usage.js";
|
|
6
|
+
import { CreditLedger } from "./credit-ledger.js";
|
|
7
|
+
describe("maybeTriggerUsageTopup", () => {
|
|
8
|
+
let pool;
|
|
9
|
+
let db;
|
|
10
|
+
let ledger;
|
|
11
|
+
let settingsRepo;
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
({ db, pool } = await createTestDb());
|
|
14
|
+
});
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await pool.close();
|
|
17
|
+
});
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
await truncateAllTables(pool);
|
|
20
|
+
ledger = new CreditLedger(db);
|
|
21
|
+
settingsRepo = new DrizzleAutoTopupSettingsRepository(db);
|
|
22
|
+
});
|
|
23
|
+
it("does nothing when tenant has no auto-topup settings", async () => {
|
|
24
|
+
const mockCharge = vi.fn();
|
|
25
|
+
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
26
|
+
await maybeTriggerUsageTopup(deps, "t1");
|
|
27
|
+
expect(mockCharge).not.toHaveBeenCalled();
|
|
28
|
+
});
|
|
29
|
+
it("does nothing when usage_enabled is false", async () => {
|
|
30
|
+
await settingsRepo.upsert("t1", { usageEnabled: false });
|
|
31
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
|
|
32
|
+
const mockCharge = vi.fn();
|
|
33
|
+
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
34
|
+
await maybeTriggerUsageTopup(deps, "t1");
|
|
35
|
+
expect(mockCharge).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
it("does nothing when balance is above threshold", async () => {
|
|
38
|
+
await settingsRepo.upsert("t1", {
|
|
39
|
+
usageEnabled: true,
|
|
40
|
+
usageThreshold: Credit.fromCents(100),
|
|
41
|
+
usageTopup: Credit.fromCents(500),
|
|
42
|
+
});
|
|
43
|
+
await ledger.credit("t1", Credit.fromCents(200), "purchase", "buy", "ref-1", "stripe");
|
|
44
|
+
const mockCharge = vi.fn();
|
|
45
|
+
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
46
|
+
await maybeTriggerUsageTopup(deps, "t1");
|
|
47
|
+
expect(mockCharge).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
it("triggers charge when balance drops below threshold", async () => {
|
|
50
|
+
await settingsRepo.upsert("t1", {
|
|
51
|
+
usageEnabled: true,
|
|
52
|
+
usageThreshold: Credit.fromCents(100),
|
|
53
|
+
usageTopup: Credit.fromCents(500),
|
|
54
|
+
});
|
|
55
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
|
|
56
|
+
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
|
|
57
|
+
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
58
|
+
await maybeTriggerUsageTopup(deps, "t1");
|
|
59
|
+
expect(mockCharge).toHaveBeenCalledWith("t1", Credit.fromCents(500), "auto_topup_usage");
|
|
60
|
+
});
|
|
61
|
+
it("skips when charge is already in-flight", async () => {
|
|
62
|
+
await settingsRepo.upsert("t1", { usageEnabled: true, usageThreshold: Credit.fromCents(100) });
|
|
63
|
+
await settingsRepo.setUsageChargeInFlight("t1", true);
|
|
64
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
|
|
65
|
+
const mockCharge = vi.fn();
|
|
66
|
+
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
67
|
+
await maybeTriggerUsageTopup(deps, "t1");
|
|
68
|
+
expect(mockCharge).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
it("fires exactly one charge when two concurrent top-up triggers race", async () => {
|
|
71
|
+
// Setup: usage enabled, balance below threshold
|
|
72
|
+
await settingsRepo.upsert("t1", {
|
|
73
|
+
usageEnabled: true,
|
|
74
|
+
usageThreshold: Credit.fromCents(500),
|
|
75
|
+
usageTopup: Credit.fromCents(2000),
|
|
76
|
+
});
|
|
77
|
+
await ledger.credit("t1", Credit.fromCents(100), "purchase", "buy", "ref-1", "stripe");
|
|
78
|
+
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_race" });
|
|
79
|
+
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
80
|
+
// Fire two concurrent calls — both see balance < threshold,
|
|
81
|
+
// but only one wins the atomic tryAcquireUsageInFlight CAS
|
|
82
|
+
await Promise.all([maybeTriggerUsageTopup(deps, "t1"), maybeTriggerUsageTopup(deps, "t1")]);
|
|
83
|
+
// Exactly one charge — not zero, not two
|
|
84
|
+
expect(mockCharge).toHaveBeenCalledTimes(1);
|
|
85
|
+
expect(mockCharge).toHaveBeenCalledWith("t1", Credit.fromCents(2000), "auto_topup_usage");
|
|
86
|
+
// Flag should be cleared after the winner's finally block
|
|
87
|
+
const settings = await settingsRepo.getByTenant("t1");
|
|
88
|
+
expect(settings?.usageChargeInFlight).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
it("clears in-flight flag after successful charge (second call triggers new charge)", async () => {
|
|
91
|
+
await settingsRepo.upsert("t1", {
|
|
92
|
+
usageEnabled: true,
|
|
93
|
+
usageThreshold: Credit.fromCents(100),
|
|
94
|
+
usageTopup: Credit.fromCents(500),
|
|
95
|
+
});
|
|
96
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
|
|
97
|
+
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
|
|
98
|
+
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
99
|
+
// First call — triggers charge, flag set then cleared
|
|
100
|
+
await maybeTriggerUsageTopup(deps, "t1");
|
|
101
|
+
expect(mockCharge).toHaveBeenCalledTimes(1);
|
|
102
|
+
// Verify flag is cleared in the database
|
|
103
|
+
expect((await settingsRepo.getByTenant("t1"))?.usageChargeInFlight).toBe(false);
|
|
104
|
+
// Second call — if flag was cleared, this triggers another charge
|
|
105
|
+
await maybeTriggerUsageTopup(deps, "t1");
|
|
106
|
+
expect(mockCharge).toHaveBeenCalledTimes(2);
|
|
107
|
+
});
|
|
108
|
+
it("clears in-flight flag after charge throws error (second call triggers new charge)", async () => {
|
|
109
|
+
await settingsRepo.upsert("t1", {
|
|
110
|
+
usageEnabled: true,
|
|
111
|
+
usageThreshold: Credit.fromCents(100),
|
|
112
|
+
usageTopup: Credit.fromCents(500),
|
|
113
|
+
});
|
|
114
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
|
|
115
|
+
const mockCharge = vi
|
|
116
|
+
.fn()
|
|
117
|
+
.mockRejectedValueOnce(new Error("Stripe network error"))
|
|
118
|
+
.mockResolvedValueOnce({ success: true, paymentReference: "pi_456" });
|
|
119
|
+
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
120
|
+
// First call — charge throws, caught by catch block, finally clears flag
|
|
121
|
+
await maybeTriggerUsageTopup(deps, "t1");
|
|
122
|
+
expect(mockCharge).toHaveBeenCalledTimes(1);
|
|
123
|
+
// Verify flag is cleared in the database despite the error
|
|
124
|
+
expect((await settingsRepo.getByTenant("t1"))?.usageChargeInFlight).toBe(false);
|
|
125
|
+
// Second call — if flag was cleared, this triggers another charge
|
|
126
|
+
await maybeTriggerUsageTopup(deps, "t1");
|
|
127
|
+
expect(mockCharge).toHaveBeenCalledTimes(2);
|
|
128
|
+
});
|
|
129
|
+
it("resets failure counter on success", async () => {
|
|
130
|
+
await settingsRepo.upsert("t1", {
|
|
131
|
+
usageEnabled: true,
|
|
132
|
+
usageThreshold: Credit.fromCents(100),
|
|
133
|
+
usageTopup: Credit.fromCents(500),
|
|
134
|
+
});
|
|
135
|
+
await settingsRepo.incrementUsageFailures("t1");
|
|
136
|
+
await settingsRepo.incrementUsageFailures("t1");
|
|
137
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
|
|
138
|
+
const mockCharge = vi.fn().mockResolvedValue({ success: true });
|
|
139
|
+
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
140
|
+
await maybeTriggerUsageTopup(deps, "t1");
|
|
141
|
+
expect((await settingsRepo.getByTenant("t1"))?.usageConsecutiveFailures).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
it("increments failure counter on charge failure", async () => {
|
|
144
|
+
await settingsRepo.upsert("t1", {
|
|
145
|
+
usageEnabled: true,
|
|
146
|
+
usageThreshold: Credit.fromCents(100),
|
|
147
|
+
usageTopup: Credit.fromCents(500),
|
|
148
|
+
});
|
|
149
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
|
|
150
|
+
const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
|
|
151
|
+
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
152
|
+
await maybeTriggerUsageTopup(deps, "t1");
|
|
153
|
+
expect((await settingsRepo.getByTenant("t1"))?.usageConsecutiveFailures).toBe(1);
|
|
154
|
+
});
|
|
155
|
+
it("skips charge when tenant status check returns non-null (banned/suspended)", async () => {
|
|
156
|
+
// Setup: settings exist and balance is below threshold
|
|
157
|
+
await settingsRepo.upsert("t1", {
|
|
158
|
+
usageEnabled: true,
|
|
159
|
+
usageThreshold: Credit.fromCents(1000),
|
|
160
|
+
usageTopup: Credit.fromCents(2000),
|
|
161
|
+
});
|
|
162
|
+
const chargeAutoTopup = vi.fn();
|
|
163
|
+
const checkTenantStatus = vi.fn().mockResolvedValue({ error: "account_banned", message: "banned" });
|
|
164
|
+
await maybeTriggerUsageTopup({ settingsRepo, creditLedger: ledger, chargeAutoTopup, checkTenantStatus }, "t1");
|
|
165
|
+
expect(chargeAutoTopup).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
it("disables usage auto-topup after 3 consecutive failures", async () => {
|
|
168
|
+
await settingsRepo.upsert("t1", {
|
|
169
|
+
usageEnabled: true,
|
|
170
|
+
usageThreshold: Credit.fromCents(100),
|
|
171
|
+
usageTopup: Credit.fromCents(500),
|
|
172
|
+
});
|
|
173
|
+
await settingsRepo.incrementUsageFailures("t1");
|
|
174
|
+
await settingsRepo.incrementUsageFailures("t1");
|
|
175
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
|
|
176
|
+
const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
|
|
177
|
+
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
178
|
+
await maybeTriggerUsageTopup(deps, "t1");
|
|
179
|
+
expect((await settingsRepo.getByTenant("t1"))?.usageEnabled).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ICreditLedger } from "./credit-ledger.js";
|
|
2
|
+
export interface CreditExpiryCronConfig {
|
|
3
|
+
ledger: ICreditLedger;
|
|
4
|
+
/** Current time as ISO-8601 string. */
|
|
5
|
+
now: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CreditExpiryCronResult {
|
|
8
|
+
processed: number;
|
|
9
|
+
expired: string[];
|
|
10
|
+
errors: string[];
|
|
11
|
+
skippedZeroBalance: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Sweep expired credit grants and debit the original grant amount
|
|
15
|
+
* (or remaining balance if partially consumed).
|
|
16
|
+
*
|
|
17
|
+
* Idempotent: uses `expiry:<original_txn_id>` as referenceId.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runCreditExpiryCron(cfg: CreditExpiryCronConfig): Promise<CreditExpiryCronResult>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { logger } from "../config/logger.js";
|
|
2
|
+
import { InsufficientBalanceError } from "./credit-ledger.js";
|
|
3
|
+
/**
|
|
4
|
+
* Sweep expired credit grants and debit the original grant amount
|
|
5
|
+
* (or remaining balance if partially consumed).
|
|
6
|
+
*
|
|
7
|
+
* Idempotent: uses `expiry:<original_txn_id>` as referenceId.
|
|
8
|
+
*/
|
|
9
|
+
export async function runCreditExpiryCron(cfg) {
|
|
10
|
+
const result = {
|
|
11
|
+
processed: 0,
|
|
12
|
+
expired: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
skippedZeroBalance: 0,
|
|
15
|
+
};
|
|
16
|
+
const expiredGrants = await cfg.ledger.expiredCredits(cfg.now);
|
|
17
|
+
for (const grant of expiredGrants) {
|
|
18
|
+
try {
|
|
19
|
+
const balance = await cfg.ledger.balance(grant.tenantId);
|
|
20
|
+
if (balance.isZero()) {
|
|
21
|
+
result.skippedZeroBalance++;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
// Debit the lesser of the original grant amount or current balance
|
|
25
|
+
const debitAmount = balance.lessThan(grant.amount) ? balance : grant.amount;
|
|
26
|
+
await cfg.ledger.debit(grant.tenantId, debitAmount, "credit_expiry", `Expired credit grant reclaimed: ${grant.id}`, `expiry:${grant.id}`);
|
|
27
|
+
result.processed++;
|
|
28
|
+
if (!result.expired.includes(grant.tenantId)) {
|
|
29
|
+
result.expired.push(grant.tenantId);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err instanceof InsufficientBalanceError) {
|
|
34
|
+
result.skippedZeroBalance++;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
38
|
+
logger.error("Credit expiry failed", { tenantId: grant.tenantId, txnId: grant.id, error: msg });
|
|
39
|
+
result.errors.push(`${grant.tenantId}:${grant.id}: ${msg}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (result.processed > 0) {
|
|
44
|
+
logger.info(`Credit expiry cron: reclaimed ${result.processed} expired grants`, {
|
|
45
|
+
expired: result.expired,
|
|
46
|
+
skippedZeroBalance: result.skippedZeroBalance,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|