@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,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Email Service — deduplication + sending for billing-triggered emails.
|
|
3
|
+
*
|
|
4
|
+
* Uses the emailNotifications table to ensure at most one email of each type
|
|
5
|
+
* per tenant per day. All queries use Drizzle — zero raw SQL.
|
|
6
|
+
*/
|
|
7
|
+
import type { EmailClient } from "./client.js";
|
|
8
|
+
import type { IBillingEmailRepository } from "./drizzle-billing-email-repository.js";
|
|
9
|
+
export type BillingEmailType = "credit-purchase" | "low-balance" | "bot-suspended" | "bot-destruction" | "data-deleted" | "spend-alert";
|
|
10
|
+
export interface BillingEmailServiceConfig {
|
|
11
|
+
billingEmailRepo: IBillingEmailRepository;
|
|
12
|
+
emailClient: EmailClient;
|
|
13
|
+
/** Base URL for CTA links (e.g. "https://app.wopr.bot"). */
|
|
14
|
+
appBaseUrl: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class BillingEmailService {
|
|
17
|
+
private readonly billingEmailRepo;
|
|
18
|
+
private readonly emailClient;
|
|
19
|
+
private readonly appBaseUrl;
|
|
20
|
+
constructor(config: BillingEmailServiceConfig);
|
|
21
|
+
/**
|
|
22
|
+
* Check if an email of this type was already sent today for this tenant.
|
|
23
|
+
*/
|
|
24
|
+
shouldSendEmail(tenantId: string, emailType: BillingEmailType): Promise<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* Record that an email was sent.
|
|
27
|
+
*/
|
|
28
|
+
recordEmailSent(tenantId: string, emailType: BillingEmailType): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Send a purchase receipt email.
|
|
31
|
+
* Always sends (no daily dedup — receipts are per-transaction).
|
|
32
|
+
*/
|
|
33
|
+
sendPurchaseReceipt(email: string, tenantId: string, amountDollars: string, newBalanceDollars: string): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Send a low balance warning. Deduped: max once per day.
|
|
36
|
+
*/
|
|
37
|
+
sendLowBalanceWarning(email: string, tenantId: string, balanceDollars: string, estimatedDaysRemaining: number): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Send a bot suspended notification. Deduped: max once per day.
|
|
40
|
+
*/
|
|
41
|
+
sendBotSuspendedNotice(email: string, tenantId: string, botNames: string[]): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Send a destruction warning (5 days left). Deduped: max once per day.
|
|
44
|
+
*/
|
|
45
|
+
sendDestructionWarning(email: string, tenantId: string, botNames: string[]): Promise<boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* Send a data deleted confirmation. Deduped: max once per day.
|
|
48
|
+
*/
|
|
49
|
+
sendDataDeletedNotice(email: string, tenantId: string): Promise<boolean>;
|
|
50
|
+
private creditsUrl;
|
|
51
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Email Service — deduplication + sending for billing-triggered emails.
|
|
3
|
+
*
|
|
4
|
+
* Uses the emailNotifications table to ensure at most one email of each type
|
|
5
|
+
* per tenant per day. All queries use Drizzle — zero raw SQL.
|
|
6
|
+
*/
|
|
7
|
+
import { logger } from "../config/logger.js";
|
|
8
|
+
import { botDestructionTemplate, botSuspendedTemplate, creditPurchaseTemplate, dataDeletedTemplate, lowBalanceTemplate, } from "./templates.js";
|
|
9
|
+
export class BillingEmailService {
|
|
10
|
+
billingEmailRepo;
|
|
11
|
+
emailClient;
|
|
12
|
+
appBaseUrl;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.billingEmailRepo = config.billingEmailRepo;
|
|
15
|
+
this.emailClient = config.emailClient;
|
|
16
|
+
this.appBaseUrl = config.appBaseUrl;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Check if an email of this type was already sent today for this tenant.
|
|
20
|
+
*/
|
|
21
|
+
async shouldSendEmail(tenantId, emailType) {
|
|
22
|
+
return this.billingEmailRepo.shouldSend(tenantId, emailType);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Record that an email was sent.
|
|
26
|
+
*/
|
|
27
|
+
async recordEmailSent(tenantId, emailType) {
|
|
28
|
+
await this.billingEmailRepo.recordSent(tenantId, emailType);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Send a purchase receipt email.
|
|
32
|
+
* Always sends (no daily dedup — receipts are per-transaction).
|
|
33
|
+
*/
|
|
34
|
+
async sendPurchaseReceipt(email, tenantId, amountDollars, newBalanceDollars) {
|
|
35
|
+
try {
|
|
36
|
+
const template = creditPurchaseTemplate(email, amountDollars, newBalanceDollars, this.creditsUrl());
|
|
37
|
+
await this.emailClient.send({
|
|
38
|
+
to: email,
|
|
39
|
+
...template,
|
|
40
|
+
userId: tenantId,
|
|
41
|
+
templateName: "credit-purchase",
|
|
42
|
+
});
|
|
43
|
+
await this.recordEmailSent(tenantId, "credit-purchase");
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
logger.error("Failed to send purchase receipt", {
|
|
48
|
+
tenantId,
|
|
49
|
+
error: err instanceof Error ? err.message : String(err),
|
|
50
|
+
});
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Send a low balance warning. Deduped: max once per day.
|
|
56
|
+
*/
|
|
57
|
+
async sendLowBalanceWarning(email, tenantId, balanceDollars, estimatedDaysRemaining) {
|
|
58
|
+
if (!(await this.shouldSendEmail(tenantId, "low-balance"))) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const template = lowBalanceTemplate(email, balanceDollars, estimatedDaysRemaining, this.creditsUrl());
|
|
63
|
+
await this.emailClient.send({
|
|
64
|
+
to: email,
|
|
65
|
+
...template,
|
|
66
|
+
userId: tenantId,
|
|
67
|
+
templateName: "low-balance",
|
|
68
|
+
});
|
|
69
|
+
await this.recordEmailSent(tenantId, "low-balance");
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
logger.error("Failed to send low balance warning", {
|
|
74
|
+
tenantId,
|
|
75
|
+
error: err instanceof Error ? err.message : String(err),
|
|
76
|
+
});
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Send a bot suspended notification. Deduped: max once per day.
|
|
82
|
+
*/
|
|
83
|
+
async sendBotSuspendedNotice(email, tenantId, botNames) {
|
|
84
|
+
if (!(await this.shouldSendEmail(tenantId, "bot-suspended"))) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const botsDisplay = botNames.length > 0 ? botNames.join(", ") : "your bot(s)";
|
|
89
|
+
const template = botSuspendedTemplate(email, botsDisplay, "Insufficient credits", this.creditsUrl());
|
|
90
|
+
await this.emailClient.send({
|
|
91
|
+
to: email,
|
|
92
|
+
...template,
|
|
93
|
+
userId: tenantId,
|
|
94
|
+
templateName: "bot-suspended",
|
|
95
|
+
});
|
|
96
|
+
await this.recordEmailSent(tenantId, "bot-suspended");
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
logger.error("Failed to send bot suspended notice", {
|
|
101
|
+
tenantId,
|
|
102
|
+
error: err instanceof Error ? err.message : String(err),
|
|
103
|
+
});
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Send a destruction warning (5 days left). Deduped: max once per day.
|
|
109
|
+
*/
|
|
110
|
+
async sendDestructionWarning(email, tenantId, botNames) {
|
|
111
|
+
if (!(await this.shouldSendEmail(tenantId, "bot-destruction"))) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const botsDisplay = botNames.length > 0 ? botNames.join(", ") : "your bot(s)";
|
|
116
|
+
const template = botDestructionTemplate(email, botsDisplay, 5, this.creditsUrl());
|
|
117
|
+
await this.emailClient.send({
|
|
118
|
+
to: email,
|
|
119
|
+
...template,
|
|
120
|
+
userId: tenantId,
|
|
121
|
+
templateName: "bot-destruction",
|
|
122
|
+
});
|
|
123
|
+
await this.recordEmailSent(tenantId, "bot-destruction");
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
logger.error("Failed to send destruction warning", {
|
|
128
|
+
tenantId,
|
|
129
|
+
error: err instanceof Error ? err.message : String(err),
|
|
130
|
+
});
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Send a data deleted confirmation. Deduped: max once per day.
|
|
136
|
+
*/
|
|
137
|
+
async sendDataDeletedNotice(email, tenantId) {
|
|
138
|
+
if (!(await this.shouldSendEmail(tenantId, "data-deleted"))) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const template = dataDeletedTemplate(email, this.creditsUrl());
|
|
143
|
+
await this.emailClient.send({
|
|
144
|
+
to: email,
|
|
145
|
+
...template,
|
|
146
|
+
userId: tenantId,
|
|
147
|
+
templateName: "data-deleted",
|
|
148
|
+
});
|
|
149
|
+
await this.recordEmailSent(tenantId, "data-deleted");
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
logger.error("Failed to send data deleted notice", {
|
|
154
|
+
tenantId,
|
|
155
|
+
error: err instanceof Error ? err.message : String(err),
|
|
156
|
+
});
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
creditsUrl() {
|
|
161
|
+
return `${this.appBaseUrl}/credits`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import { drizzle } from "drizzle-orm/pglite";
|
|
3
|
+
import { migrate } from "drizzle-orm/pglite/migrator";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { emailNotifications } from "../db/schema/email-notifications.js";
|
|
6
|
+
import * as schema from "../db/schema/index.js";
|
|
7
|
+
import { BillingEmailService } from "./billing-emails.js";
|
|
8
|
+
import { EmailClient } from "./client.js";
|
|
9
|
+
import { DrizzleBillingEmailRepository } from "./drizzle-billing-email-repository.js";
|
|
10
|
+
vi.mock("resend", () => ({
|
|
11
|
+
Resend: class MockResend {
|
|
12
|
+
emails = { send: vi.fn().mockResolvedValue({ data: { id: "email-123" }, error: null }) };
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
vi.mock("../config/logger.js", () => ({
|
|
16
|
+
logger: {
|
|
17
|
+
info: vi.fn(),
|
|
18
|
+
error: vi.fn(),
|
|
19
|
+
warn: vi.fn(),
|
|
20
|
+
debug: vi.fn(),
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
describe("BillingEmailService", () => {
|
|
24
|
+
let db;
|
|
25
|
+
let pool;
|
|
26
|
+
let emailClient;
|
|
27
|
+
let service;
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
pool = new PGlite();
|
|
30
|
+
db = drizzle(pool, { schema });
|
|
31
|
+
await migrate(drizzle(pool, { schema }), { migrationsFolder: "./drizzle/migrations" });
|
|
32
|
+
emailClient = new EmailClient({
|
|
33
|
+
apiKey: "test-key",
|
|
34
|
+
from: "noreply@wopr.bot",
|
|
35
|
+
});
|
|
36
|
+
service = new BillingEmailService({
|
|
37
|
+
billingEmailRepo: new DrizzleBillingEmailRepository(db),
|
|
38
|
+
emailClient,
|
|
39
|
+
appBaseUrl: "https://app.wopr.bot",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
await pool.close();
|
|
44
|
+
});
|
|
45
|
+
describe("shouldSendEmail", () => {
|
|
46
|
+
it("should return true when no email was sent today", async () => {
|
|
47
|
+
expect(await service.shouldSendEmail("tenant-1", "low-balance")).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it("should return false when email was already sent today", async () => {
|
|
50
|
+
await service.recordEmailSent("tenant-1", "low-balance");
|
|
51
|
+
expect(await service.shouldSendEmail("tenant-1", "low-balance")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
it("should allow different email types for same tenant same day", async () => {
|
|
54
|
+
await service.recordEmailSent("tenant-1", "low-balance");
|
|
55
|
+
expect(await service.shouldSendEmail("tenant-1", "bot-suspended")).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it("should allow same email type for different tenants", async () => {
|
|
58
|
+
await service.recordEmailSent("tenant-1", "low-balance");
|
|
59
|
+
expect(await service.shouldSendEmail("tenant-2", "low-balance")).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("recordEmailSent", () => {
|
|
63
|
+
it("should insert a record into the database", async () => {
|
|
64
|
+
await service.recordEmailSent("tenant-1", "low-balance");
|
|
65
|
+
const rows = await db.select().from(emailNotifications);
|
|
66
|
+
expect(rows).toHaveLength(1);
|
|
67
|
+
expect(rows[0].tenantId).toBe("tenant-1");
|
|
68
|
+
expect(rows[0].emailType).toBe("low-balance");
|
|
69
|
+
});
|
|
70
|
+
it("should throw on duplicate insert (same tenant, type, date)", async () => {
|
|
71
|
+
await service.recordEmailSent("tenant-1", "low-balance");
|
|
72
|
+
await expect(service.recordEmailSent("tenant-1", "low-balance")).rejects.toThrow();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe("sendPurchaseReceipt", () => {
|
|
76
|
+
it("should send and record purchase receipt", async () => {
|
|
77
|
+
const sent = await service.sendPurchaseReceipt("user@test.com", "tenant-1", "$10.00", "$15.00");
|
|
78
|
+
expect(sent).toBe(true);
|
|
79
|
+
const rows = await db.select().from(emailNotifications);
|
|
80
|
+
expect(rows).toHaveLength(1);
|
|
81
|
+
expect(rows[0].emailType).toBe("credit-purchase");
|
|
82
|
+
});
|
|
83
|
+
it("should allow multiple purchase receipts same day (per-transaction)", async () => {
|
|
84
|
+
await service.sendPurchaseReceipt("user@test.com", "tenant-1", "$10.00", "$15.00");
|
|
85
|
+
// Second purchase receipt for same tenant - the dedup is per record,
|
|
86
|
+
// but credit-purchase uses recordEmailSent which will throw on dupe.
|
|
87
|
+
// Purchase receipts always send but record for audit only.
|
|
88
|
+
// The unique constraint will prevent a second insert with the same date.
|
|
89
|
+
// This is by design - 1 receipt email per day per tenant is sufficient.
|
|
90
|
+
const rows = await db.select().from(emailNotifications);
|
|
91
|
+
expect(rows).toHaveLength(1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("sendLowBalanceWarning", () => {
|
|
95
|
+
it("should send low balance warning when not already sent", async () => {
|
|
96
|
+
const sent = await service.sendLowBalanceWarning("user@test.com", "tenant-1", "$1.50", 3);
|
|
97
|
+
expect(sent).toBe(true);
|
|
98
|
+
const rows = await db.select().from(emailNotifications);
|
|
99
|
+
expect(rows).toHaveLength(1);
|
|
100
|
+
expect(rows[0].emailType).toBe("low-balance");
|
|
101
|
+
});
|
|
102
|
+
it("should skip when already sent today", async () => {
|
|
103
|
+
await service.recordEmailSent("tenant-1", "low-balance");
|
|
104
|
+
const sent = await service.sendLowBalanceWarning("user@test.com", "tenant-1", "$1.50", 3);
|
|
105
|
+
expect(sent).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("sendBotSuspendedNotice", () => {
|
|
109
|
+
it("should send bot suspended notice", async () => {
|
|
110
|
+
const sent = await service.sendBotSuspendedNotice("user@test.com", "tenant-1", ["MyBot"]);
|
|
111
|
+
expect(sent).toBe(true);
|
|
112
|
+
const rows = await db.select().from(emailNotifications);
|
|
113
|
+
expect(rows).toHaveLength(1);
|
|
114
|
+
expect(rows[0].emailType).toBe("bot-suspended");
|
|
115
|
+
});
|
|
116
|
+
it("should handle empty bot names list", async () => {
|
|
117
|
+
const sent = await service.sendBotSuspendedNotice("user@test.com", "tenant-1", []);
|
|
118
|
+
expect(sent).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
it("should skip when already sent today", async () => {
|
|
121
|
+
await service.recordEmailSent("tenant-1", "bot-suspended");
|
|
122
|
+
const sent = await service.sendBotSuspendedNotice("user@test.com", "tenant-1", ["MyBot"]);
|
|
123
|
+
expect(sent).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe("sendDestructionWarning", () => {
|
|
127
|
+
it("should send destruction warning", async () => {
|
|
128
|
+
const sent = await service.sendDestructionWarning("user@test.com", "tenant-1", ["MyBot"]);
|
|
129
|
+
expect(sent).toBe(true);
|
|
130
|
+
const rows = await db.select().from(emailNotifications);
|
|
131
|
+
expect(rows).toHaveLength(1);
|
|
132
|
+
expect(rows[0].emailType).toBe("bot-destruction");
|
|
133
|
+
});
|
|
134
|
+
it("should skip when already sent today", async () => {
|
|
135
|
+
await service.recordEmailSent("tenant-1", "bot-destruction");
|
|
136
|
+
const sent = await service.sendDestructionWarning("user@test.com", "tenant-1", ["MyBot"]);
|
|
137
|
+
expect(sent).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe("sendDataDeletedNotice", () => {
|
|
141
|
+
it("should send data deleted notice", async () => {
|
|
142
|
+
const sent = await service.sendDataDeletedNotice("user@test.com", "tenant-1");
|
|
143
|
+
expect(sent).toBe(true);
|
|
144
|
+
const rows = await db.select().from(emailNotifications);
|
|
145
|
+
expect(rows).toHaveLength(1);
|
|
146
|
+
expect(rows[0].emailType).toBe("data-deleted");
|
|
147
|
+
});
|
|
148
|
+
it("should skip when already sent today", async () => {
|
|
149
|
+
await service.recordEmailSent("tenant-1", "data-deleted");
|
|
150
|
+
const sent = await service.sendDataDeletedNotice("user@test.com", "tenant-1");
|
|
151
|
+
expect(sent).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe("error handling", () => {
|
|
155
|
+
it("should return false and log when email sending fails", async () => {
|
|
156
|
+
// Override the emailClient.send to throw
|
|
157
|
+
vi.spyOn(emailClient, "send").mockRejectedValueOnce(new Error("Resend error"));
|
|
158
|
+
const sent = await service.sendLowBalanceWarning("user@test.com", "tenant-1", "$1.50", 3);
|
|
159
|
+
expect(sent).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Client — Template-based transactional email sender.
|
|
3
|
+
*
|
|
4
|
+
* Wraps Resend SDK and provides a typed interface for sending emails
|
|
5
|
+
* using the platform's templates. Every email is logged for audit.
|
|
6
|
+
*/
|
|
7
|
+
export interface EmailClientConfig {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
from: string;
|
|
10
|
+
replyTo?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SendTemplateEmailOpts {
|
|
13
|
+
to: string;
|
|
14
|
+
subject: string;
|
|
15
|
+
html: string;
|
|
16
|
+
text: string;
|
|
17
|
+
/** Audit metadata: who triggered this email */
|
|
18
|
+
userId?: string;
|
|
19
|
+
/** Audit metadata: which template was used */
|
|
20
|
+
templateName?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface EmailSendResult {
|
|
23
|
+
id: string;
|
|
24
|
+
success: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Transactional email client backed by Resend.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* ```ts
|
|
31
|
+
* const client = new EmailClient({ apiKey: "re_xxx", from: "noreply@wopr.bot" });
|
|
32
|
+
* const template = verifyEmailTemplate(url, email);
|
|
33
|
+
* await client.send({ to: email, ...template, userId: "user-123", templateName: "verify-email" });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare class EmailClient {
|
|
37
|
+
private resend;
|
|
38
|
+
private from;
|
|
39
|
+
private replyTo;
|
|
40
|
+
private onSend;
|
|
41
|
+
constructor(config: EmailClientConfig);
|
|
42
|
+
/** Register a callback invoked after each successful send (for audit logging). */
|
|
43
|
+
onEmailSent(callback: (opts: SendTemplateEmailOpts, result: EmailSendResult) => void): void;
|
|
44
|
+
/** Send a transactional email. */
|
|
45
|
+
send(opts: SendTemplateEmailOpts): Promise<EmailSendResult>;
|
|
46
|
+
}
|
|
47
|
+
export declare function getEmailClient(): EmailClient;
|
|
48
|
+
/** Reset the singleton (for testing). */
|
|
49
|
+
export declare function resetEmailClient(): void;
|
|
50
|
+
/** Replace the singleton (for testing). */
|
|
51
|
+
export declare function setEmailClient(client: EmailClient): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Client — Template-based transactional email sender.
|
|
3
|
+
*
|
|
4
|
+
* Wraps Resend SDK and provides a typed interface for sending emails
|
|
5
|
+
* using the platform's templates. Every email is logged for audit.
|
|
6
|
+
*/
|
|
7
|
+
import { Resend } from "resend";
|
|
8
|
+
import { logger } from "../config/logger.js";
|
|
9
|
+
/**
|
|
10
|
+
* Transactional email client backed by Resend.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* ```ts
|
|
14
|
+
* const client = new EmailClient({ apiKey: "re_xxx", from: "noreply@wopr.bot" });
|
|
15
|
+
* const template = verifyEmailTemplate(url, email);
|
|
16
|
+
* await client.send({ to: email, ...template, userId: "user-123", templateName: "verify-email" });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class EmailClient {
|
|
20
|
+
resend;
|
|
21
|
+
from;
|
|
22
|
+
replyTo;
|
|
23
|
+
onSend = null;
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.resend = new Resend(config.apiKey);
|
|
26
|
+
this.from = config.from;
|
|
27
|
+
this.replyTo = config.replyTo;
|
|
28
|
+
}
|
|
29
|
+
/** Register a callback invoked after each successful send (for audit logging). */
|
|
30
|
+
onEmailSent(callback) {
|
|
31
|
+
this.onSend = callback;
|
|
32
|
+
}
|
|
33
|
+
/** Send a transactional email. */
|
|
34
|
+
async send(opts) {
|
|
35
|
+
const { data, error } = await this.resend.emails.send({
|
|
36
|
+
from: this.from,
|
|
37
|
+
replyTo: this.replyTo,
|
|
38
|
+
to: opts.to,
|
|
39
|
+
subject: opts.subject,
|
|
40
|
+
html: opts.html,
|
|
41
|
+
text: opts.text,
|
|
42
|
+
});
|
|
43
|
+
if (error) {
|
|
44
|
+
logger.error("Failed to send email", {
|
|
45
|
+
to: opts.to,
|
|
46
|
+
template: opts.templateName,
|
|
47
|
+
error: error.message,
|
|
48
|
+
});
|
|
49
|
+
throw new Error(`Failed to send email: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
const result = {
|
|
52
|
+
id: data?.id || "",
|
|
53
|
+
success: true,
|
|
54
|
+
};
|
|
55
|
+
logger.info("Email sent", {
|
|
56
|
+
emailId: result.id,
|
|
57
|
+
to: opts.to,
|
|
58
|
+
template: opts.templateName,
|
|
59
|
+
userId: opts.userId,
|
|
60
|
+
});
|
|
61
|
+
if (this.onSend) {
|
|
62
|
+
try {
|
|
63
|
+
this.onSend(opts, result);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Audit callback failure should not break email sending
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create a lazily-initialized singleton EmailClient from environment variables.
|
|
74
|
+
*
|
|
75
|
+
* Env vars:
|
|
76
|
+
* - RESEND_API_KEY (required)
|
|
77
|
+
* - RESEND_FROM (default: "noreply@wopr.bot")
|
|
78
|
+
* - RESEND_REPLY_TO (default: "support@wopr.bot")
|
|
79
|
+
*/
|
|
80
|
+
let _client = null;
|
|
81
|
+
export function getEmailClient() {
|
|
82
|
+
if (!_client) {
|
|
83
|
+
const apiKey = process.env.RESEND_API_KEY;
|
|
84
|
+
if (!apiKey) {
|
|
85
|
+
throw new Error("RESEND_API_KEY environment variable is required");
|
|
86
|
+
}
|
|
87
|
+
_client = new EmailClient({
|
|
88
|
+
apiKey,
|
|
89
|
+
from: process.env.RESEND_FROM || "noreply@wopr.bot",
|
|
90
|
+
replyTo: process.env.RESEND_REPLY_TO || "support@wopr.bot",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return _client;
|
|
94
|
+
}
|
|
95
|
+
/** Reset the singleton (for testing). */
|
|
96
|
+
export function resetEmailClient() {
|
|
97
|
+
_client = null;
|
|
98
|
+
}
|
|
99
|
+
/** Replace the singleton (for testing). */
|
|
100
|
+
export function setEmailClient(client) {
|
|
101
|
+
_client = client;
|
|
102
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { EmailClient, getEmailClient, resetEmailClient, setEmailClient } from "./client.js";
|
|
3
|
+
const mockSend = vi.fn();
|
|
4
|
+
vi.mock("resend", () => ({
|
|
5
|
+
Resend: class MockResend {
|
|
6
|
+
emails = { send: mockSend };
|
|
7
|
+
},
|
|
8
|
+
}));
|
|
9
|
+
vi.mock("../config/logger.js", () => ({
|
|
10
|
+
logger: {
|
|
11
|
+
info: vi.fn(),
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
warn: vi.fn(),
|
|
14
|
+
debug: vi.fn(),
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
describe("EmailClient", () => {
|
|
18
|
+
let client;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
mockSend.mockResolvedValue({ data: { id: "email-123" }, error: null });
|
|
22
|
+
client = new EmailClient({
|
|
23
|
+
apiKey: "test-api-key",
|
|
24
|
+
from: "noreply@wopr.bot",
|
|
25
|
+
replyTo: "support@wopr.bot",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it("should send email with correct parameters", async () => {
|
|
29
|
+
const result = await client.send({
|
|
30
|
+
to: "user@test.com",
|
|
31
|
+
subject: "Test Subject",
|
|
32
|
+
html: "<p>Test</p>",
|
|
33
|
+
text: "Test",
|
|
34
|
+
});
|
|
35
|
+
expect(result).toEqual({ id: "email-123", success: true });
|
|
36
|
+
expect(mockSend).toHaveBeenCalledWith({
|
|
37
|
+
from: "noreply@wopr.bot",
|
|
38
|
+
replyTo: "support@wopr.bot",
|
|
39
|
+
to: "user@test.com",
|
|
40
|
+
subject: "Test Subject",
|
|
41
|
+
html: "<p>Test</p>",
|
|
42
|
+
text: "Test",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
it("should throw on Resend API error", async () => {
|
|
46
|
+
mockSend.mockResolvedValueOnce({ data: null, error: { message: "Rate limit" } });
|
|
47
|
+
await expect(client.send({ to: "user@test.com", subject: "Test", html: "<p>T</p>", text: "T" })).rejects.toThrow("Failed to send email: Rate limit");
|
|
48
|
+
});
|
|
49
|
+
it("should invoke onEmailSent callback after successful send", async () => {
|
|
50
|
+
const callback = vi.fn();
|
|
51
|
+
client.onEmailSent(callback);
|
|
52
|
+
const result = await client.send({
|
|
53
|
+
to: "user@test.com",
|
|
54
|
+
subject: "Test",
|
|
55
|
+
html: "<p>T</p>",
|
|
56
|
+
text: "T",
|
|
57
|
+
userId: "user-1",
|
|
58
|
+
templateName: "verify-email",
|
|
59
|
+
});
|
|
60
|
+
expect(callback).toHaveBeenCalledWith(expect.objectContaining({ to: "user@test.com", templateName: "verify-email" }), result);
|
|
61
|
+
});
|
|
62
|
+
it("should not throw if onEmailSent callback errors", async () => {
|
|
63
|
+
const callback = vi.fn().mockImplementation(() => {
|
|
64
|
+
throw new Error("callback failed");
|
|
65
|
+
});
|
|
66
|
+
client.onEmailSent(callback);
|
|
67
|
+
const result = await client.send({
|
|
68
|
+
to: "user@test.com",
|
|
69
|
+
subject: "Test",
|
|
70
|
+
html: "<p>T</p>",
|
|
71
|
+
text: "T",
|
|
72
|
+
});
|
|
73
|
+
expect(result.success).toBe(true);
|
|
74
|
+
expect(callback).toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
it("should handle empty id from Resend", async () => {
|
|
77
|
+
mockSend.mockResolvedValueOnce({ data: { id: undefined }, error: null });
|
|
78
|
+
const result = await client.send({
|
|
79
|
+
to: "user@test.com",
|
|
80
|
+
subject: "Test",
|
|
81
|
+
html: "<p>T</p>",
|
|
82
|
+
text: "T",
|
|
83
|
+
});
|
|
84
|
+
expect(result).toEqual({ id: "", success: true });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("getEmailClient / setEmailClient / resetEmailClient", () => {
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
resetEmailClient();
|
|
90
|
+
delete process.env.RESEND_API_KEY;
|
|
91
|
+
delete process.env.RESEND_FROM;
|
|
92
|
+
delete process.env.RESEND_REPLY_TO;
|
|
93
|
+
});
|
|
94
|
+
it("should throw if RESEND_API_KEY is not set", () => {
|
|
95
|
+
expect(() => getEmailClient()).toThrow("RESEND_API_KEY environment variable is required");
|
|
96
|
+
});
|
|
97
|
+
it("should create client from env vars", () => {
|
|
98
|
+
process.env.RESEND_API_KEY = "re_test123";
|
|
99
|
+
const client = getEmailClient();
|
|
100
|
+
expect(client).toBeInstanceOf(EmailClient);
|
|
101
|
+
});
|
|
102
|
+
it("should return singleton", () => {
|
|
103
|
+
process.env.RESEND_API_KEY = "re_test123";
|
|
104
|
+
const a = getEmailClient();
|
|
105
|
+
const b = getEmailClient();
|
|
106
|
+
expect(a).toBe(b);
|
|
107
|
+
});
|
|
108
|
+
it("should allow replacing singleton for testing", () => {
|
|
109
|
+
const mock = new EmailClient({ apiKey: "mock", from: "mock@test.com" });
|
|
110
|
+
setEmailClient(mock);
|
|
111
|
+
expect(getEmailClient()).toBe(mock);
|
|
112
|
+
});
|
|
113
|
+
it("should reset singleton", () => {
|
|
114
|
+
process.env.RESEND_API_KEY = "re_test123";
|
|
115
|
+
const a = getEmailClient();
|
|
116
|
+
resetEmailClient();
|
|
117
|
+
const b = getEmailClient();
|
|
118
|
+
expect(a).not.toBe(b);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Email Repository — deduplication record storage for billing emails.
|
|
3
|
+
*
|
|
4
|
+
* Implements IBillingEmailRepository using Drizzle ORM.
|
|
5
|
+
* BillingEmailService depends on this interface, not on PlatformDb directly.
|
|
6
|
+
*/
|
|
7
|
+
import type { PlatformDb } from "../db/index.js";
|
|
8
|
+
import type { BillingEmailType } from "./billing-emails.js";
|
|
9
|
+
/** Repository interface for billing email deduplication records. */
|
|
10
|
+
export interface IBillingEmailRepository {
|
|
11
|
+
/** Returns true if no email of this type was sent to this tenant today. */
|
|
12
|
+
shouldSend(tenantId: string, emailType: BillingEmailType): Promise<boolean>;
|
|
13
|
+
/** Records that an email was sent. */
|
|
14
|
+
recordSent(tenantId: string, emailType: BillingEmailType): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare class DrizzleBillingEmailRepository implements IBillingEmailRepository {
|
|
17
|
+
private readonly db;
|
|
18
|
+
constructor(db: PlatformDb);
|
|
19
|
+
shouldSend(tenantId: string, emailType: BillingEmailType): Promise<boolean>;
|
|
20
|
+
recordSent(tenantId: string, emailType: BillingEmailType): Promise<void>;
|
|
21
|
+
}
|