@baruk/core 0.1.0 → 0.2.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/CHANGELOG.md +59 -0
- package/dist/auth/password-reset.d.ts.map +1 -1
- package/dist/auth/password-reset.js +2 -4
- package/dist/auth/password-reset.js.map +1 -1
- package/dist/auth/password.d.ts +2 -18
- package/dist/auth/password.d.ts.map +1 -1
- package/dist/auth/password.js +12 -51
- package/dist/auth/password.js.map +1 -1
- package/dist/auth/service.d.ts +0 -12
- package/dist/auth/service.d.ts.map +1 -1
- package/dist/auth/service.js +3 -36
- package/dist/auth/service.js.map +1 -1
- package/dist/auth/session-token.d.ts +5 -2
- package/dist/auth/session-token.d.ts.map +1 -1
- package/dist/auth/session-token.js +7 -12
- package/dist/auth/session-token.js.map +1 -1
- package/dist/billing/dunning.d.ts +33 -0
- package/dist/billing/dunning.d.ts.map +1 -0
- package/dist/billing/dunning.js +49 -0
- package/dist/billing/dunning.js.map +1 -0
- package/dist/billing/provider/asaas.d.ts +15 -0
- package/dist/billing/provider/asaas.d.ts.map +1 -0
- package/dist/billing/provider/asaas.js +121 -0
- package/dist/billing/provider/asaas.js.map +1 -0
- package/dist/billing/provider/manual.d.ts +14 -0
- package/dist/billing/provider/manual.d.ts.map +1 -0
- package/dist/billing/provider/manual.js +24 -0
- package/dist/billing/provider/manual.js.map +1 -0
- package/dist/billing/provider/registry.d.ts +12 -0
- package/dist/billing/provider/registry.d.ts.map +1 -0
- package/dist/billing/provider/registry.js +22 -0
- package/dist/billing/provider/registry.js.map +1 -0
- package/dist/billing/provider/types.d.ts +76 -0
- package/dist/billing/provider/types.d.ts.map +1 -0
- package/dist/billing/provider/types.js +9 -0
- package/dist/billing/provider/types.js.map +1 -0
- package/dist/billing/receivable.d.ts +108 -0
- package/dist/billing/receivable.d.ts.map +1 -0
- package/dist/billing/receivable.js +2 -0
- package/dist/billing/receivable.js.map +1 -0
- package/dist/billing/repository.d.ts +97 -0
- package/dist/billing/repository.d.ts.map +1 -0
- package/dist/billing/repository.js +167 -0
- package/dist/billing/repository.js.map +1 -0
- package/dist/cjs/audit/audit.js +31 -0
- package/dist/cjs/audit/audit.js.map +1 -0
- package/dist/cjs/audit/repository.js +32 -0
- package/dist/cjs/audit/repository.js.map +1 -0
- package/dist/cjs/auth/password-reset.js +58 -0
- package/dist/cjs/auth/password-reset.js.map +1 -0
- package/dist/cjs/auth/password.js +32 -0
- package/dist/cjs/auth/password.js.map +1 -0
- package/dist/cjs/auth/service.js +191 -0
- package/dist/cjs/auth/service.js.map +1 -0
- package/dist/cjs/auth/session-token.js +18 -0
- package/dist/cjs/auth/session-token.js.map +1 -0
- package/dist/cjs/billing/dunning.js +54 -0
- package/dist/cjs/billing/dunning.js.map +1 -0
- package/dist/cjs/billing/provider/asaas.js +125 -0
- package/dist/cjs/billing/provider/asaas.js.map +1 -0
- package/dist/cjs/billing/provider/manual.js +28 -0
- package/dist/cjs/billing/provider/manual.js.map +1 -0
- package/dist/cjs/billing/provider/registry.js +27 -0
- package/dist/cjs/billing/provider/registry.js.map +1 -0
- package/dist/cjs/billing/provider/types.js +10 -0
- package/dist/cjs/billing/provider/types.js.map +1 -0
- package/dist/cjs/billing/receivable.js +3 -0
- package/dist/cjs/billing/receivable.js.map +1 -0
- package/dist/cjs/billing/repository.js +180 -0
- package/dist/cjs/billing/repository.js.map +1 -0
- package/dist/cjs/clients/repository.js +123 -0
- package/dist/cjs/clients/repository.js.map +1 -0
- package/dist/cjs/commercial-catalog/catalog.js +108 -0
- package/dist/cjs/commercial-catalog/catalog.js.map +1 -0
- package/dist/cjs/commercial-catalog/consumption-contracts.js +10 -0
- package/dist/cjs/commercial-catalog/consumption-contracts.js.map +1 -0
- package/dist/cjs/commercial-catalog/coupon.js +53 -0
- package/dist/cjs/commercial-catalog/coupon.js.map +1 -0
- package/dist/cjs/commercial-catalog/index.js +16 -0
- package/dist/cjs/commercial-catalog/index.js.map +1 -0
- package/dist/cjs/commercial-catalog/offer-application.js +53 -0
- package/dist/cjs/commercial-catalog/offer-application.js.map +1 -0
- package/dist/cjs/commercial-catalog/types.js +26 -0
- package/dist/cjs/commercial-catalog/types.js.map +1 -0
- package/dist/cjs/crm/repository.js +195 -0
- package/dist/cjs/crm/repository.js.map +1 -0
- package/dist/cjs/dashboard/repository.js +39 -0
- package/dist/cjs/dashboard/repository.js.map +1 -0
- package/dist/cjs/db/client.js +27 -0
- package/dist/cjs/db/client.js.map +1 -0
- package/dist/cjs/db/context.js +43 -0
- package/dist/cjs/db/context.js.map +1 -0
- package/dist/cjs/db/load-env.js +47 -0
- package/dist/cjs/db/load-env.js.map +1 -0
- package/dist/cjs/db/seed.js +69 -0
- package/dist/cjs/db/seed.js.map +1 -0
- package/dist/cjs/entitlements/decision.js +203 -0
- package/dist/cjs/entitlements/decision.js.map +1 -0
- package/dist/cjs/entitlements/derive.js +46 -0
- package/dist/cjs/entitlements/derive.js.map +1 -0
- package/dist/cjs/entitlements/index.js +23 -0
- package/dist/cjs/entitlements/index.js.map +1 -0
- package/dist/cjs/entitlements/sources.js +60 -0
- package/dist/cjs/entitlements/sources.js.map +1 -0
- package/dist/cjs/entitlements/standing.js +42 -0
- package/dist/cjs/entitlements/standing.js.map +1 -0
- package/dist/cjs/entitlements/types.js +16 -0
- package/dist/cjs/entitlements/types.js.map +1 -0
- package/dist/cjs/import/clients-import.js +147 -0
- package/dist/cjs/import/clients-import.js.map +1 -0
- package/dist/cjs/index.js +335 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/mail/dedup-store.js +56 -0
- package/dist/cjs/mail/dedup-store.js.map +1 -0
- package/dist/cjs/mail/index.js +24 -0
- package/dist/cjs/mail/index.js.map +1 -0
- package/dist/cjs/mail/provider.js +251 -0
- package/dist/cjs/mail/provider.js.map +1 -0
- package/dist/cjs/mail/resend-client.js +103 -0
- package/dist/cjs/mail/resend-client.js.map +1 -0
- package/dist/cjs/mail/resend.js +21 -0
- package/dist/cjs/mail/resend.js.map +1 -0
- package/dist/cjs/mail/sanitize.js +56 -0
- package/dist/cjs/mail/sanitize.js.map +1 -0
- package/dist/cjs/mail/sender-resolver.js +15 -0
- package/dist/cjs/mail/sender-resolver.js.map +1 -0
- package/dist/cjs/mail/template-renderer.js +21 -0
- package/dist/cjs/mail/template-renderer.js.map +1 -0
- package/dist/cjs/mail/types.js +38 -0
- package/dist/cjs/mail/types.js.map +1 -0
- package/dist/cjs/notifications/repository.js +49 -0
- package/dist/cjs/notifications/repository.js.map +1 -0
- package/dist/cjs/onboarding/domain.js +370 -0
- package/dist/cjs/onboarding/domain.js.map +1 -0
- package/dist/cjs/onboarding/interaction-review.js +425 -0
- package/dist/cjs/onboarding/interaction-review.js.map +1 -0
- package/dist/cjs/onboarding/persistence.js +444 -0
- package/dist/cjs/onboarding/persistence.js.map +1 -0
- package/dist/cjs/onboarding/portal-actions.js +157 -0
- package/dist/cjs/onboarding/portal-actions.js.map +1 -0
- package/dist/cjs/onboarding/portal.js +177 -0
- package/dist/cjs/onboarding/portal.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/portal/repository.js +37 -0
- package/dist/cjs/portal/repository.js.map +1 -0
- package/dist/cjs/pricing-engine/bundle.js +49 -0
- package/dist/cjs/pricing-engine/bundle.js.map +1 -0
- package/dist/cjs/pricing-engine/errors.js +48 -0
- package/dist/cjs/pricing-engine/errors.js.map +1 -0
- package/dist/cjs/pricing-engine/index.js +30 -0
- package/dist/cjs/pricing-engine/index.js.map +1 -0
- package/dist/cjs/pricing-engine/money.js +69 -0
- package/dist/cjs/pricing-engine/money.js.map +1 -0
- package/dist/cjs/pricing-engine/pipeline.js +330 -0
- package/dist/cjs/pricing-engine/pipeline.js.map +1 -0
- package/dist/cjs/pricing-engine/proration.js +53 -0
- package/dist/cjs/pricing-engine/proration.js.map +1 -0
- package/dist/cjs/pricing-engine/shadow.js +58 -0
- package/dist/cjs/pricing-engine/shadow.js.map +1 -0
- package/dist/cjs/pricing-engine/types.js +34 -0
- package/dist/cjs/pricing-engine/types.js.map +1 -0
- package/dist/cjs/products/catalog.js +10 -0
- package/dist/cjs/products/catalog.js.map +1 -0
- package/dist/cjs/products/repository.js +61 -0
- package/dist/cjs/products/repository.js.map +1 -0
- package/dist/cjs/rbac/authorize.js +36 -0
- package/dist/cjs/rbac/authorize.js.map +1 -0
- package/dist/cjs/rbac/catalog.js +455 -0
- package/dist/cjs/rbac/catalog.js.map +1 -0
- package/dist/cjs/requests/repository.js +114 -0
- package/dist/cjs/requests/repository.js.map +1 -0
- package/dist/cjs/subscription-billing/dunning.js +26 -0
- package/dist/cjs/subscription-billing/dunning.js.map +1 -0
- package/dist/cjs/subscription-billing/events.js +45 -0
- package/dist/cjs/subscription-billing/events.js.map +1 -0
- package/dist/cjs/subscription-billing/index.js +62 -0
- package/dist/cjs/subscription-billing/index.js.map +1 -0
- package/dist/cjs/subscription-billing/plans.js +88 -0
- package/dist/cjs/subscription-billing/plans.js.map +1 -0
- package/dist/cjs/subscription-billing/product-config.js +54 -0
- package/dist/cjs/subscription-billing/product-config.js.map +1 -0
- package/dist/cjs/subscription-billing/provider/asaas.js +233 -0
- package/dist/cjs/subscription-billing/provider/asaas.js.map +1 -0
- package/dist/cjs/subscription-billing/provider/disabled.js +46 -0
- package/dist/cjs/subscription-billing/provider/disabled.js.map +1 -0
- package/dist/cjs/subscription-billing/provider/registry.js +29 -0
- package/dist/cjs/subscription-billing/provider/registry.js.map +1 -0
- package/dist/cjs/subscription-billing/reconcile.js +198 -0
- package/dist/cjs/subscription-billing/reconcile.js.map +1 -0
- package/dist/cjs/subscription-billing/repository.js +359 -0
- package/dist/cjs/subscription-billing/repository.js.map +1 -0
- package/dist/cjs/subscription-billing/service.js +838 -0
- package/dist/cjs/subscription-billing/service.js.map +1 -0
- package/dist/cjs/subscription-billing/types.js +28 -0
- package/dist/cjs/subscription-billing/types.js.map +1 -0
- package/dist/cjs/tenancy/provisioning.js +126 -0
- package/dist/cjs/tenancy/provisioning.js.map +1 -0
- package/dist/cjs/users/repository.js +83 -0
- package/dist/cjs/users/repository.js.map +1 -0
- package/dist/cjs/vault/crypto.js +94 -0
- package/dist/cjs/vault/crypto.js.map +1 -0
- package/dist/cjs/vault/repository.js +120 -0
- package/dist/cjs/vault/repository.js.map +1 -0
- package/dist/commercial-catalog/catalog.d.ts +27 -0
- package/dist/commercial-catalog/catalog.d.ts.map +1 -0
- package/dist/commercial-catalog/catalog.js +104 -0
- package/dist/commercial-catalog/catalog.js.map +1 -0
- package/dist/commercial-catalog/consumption-contracts.d.ts +33 -0
- package/dist/commercial-catalog/consumption-contracts.d.ts.map +1 -0
- package/dist/commercial-catalog/consumption-contracts.js +9 -0
- package/dist/commercial-catalog/consumption-contracts.js.map +1 -0
- package/dist/commercial-catalog/coupon.d.ts +27 -0
- package/dist/commercial-catalog/coupon.d.ts.map +1 -0
- package/dist/commercial-catalog/coupon.js +47 -0
- package/dist/commercial-catalog/coupon.js.map +1 -0
- package/dist/commercial-catalog/index.d.ts +10 -0
- package/dist/commercial-catalog/index.d.ts.map +1 -0
- package/dist/commercial-catalog/index.js +5 -0
- package/dist/commercial-catalog/index.js.map +1 -0
- package/dist/commercial-catalog/offer-application.d.ts +39 -0
- package/dist/commercial-catalog/offer-application.d.ts.map +1 -0
- package/dist/commercial-catalog/offer-application.js +50 -0
- package/dist/commercial-catalog/offer-application.js.map +1 -0
- package/dist/commercial-catalog/types.d.ts +155 -0
- package/dist/commercial-catalog/types.d.ts.map +1 -0
- package/dist/commercial-catalog/types.js +23 -0
- package/dist/commercial-catalog/types.js.map +1 -0
- package/dist/crm/repository.d.ts +109 -0
- package/dist/crm/repository.d.ts.map +1 -0
- package/dist/crm/repository.js +181 -0
- package/dist/crm/repository.js.map +1 -0
- package/dist/db/context.d.ts +2 -0
- package/dist/db/context.d.ts.map +1 -1
- package/dist/db/context.js +3 -0
- package/dist/db/context.js.map +1 -1
- package/dist/db/migrate.js +7 -2
- package/dist/db/migrate.js.map +1 -1
- package/dist/db/seed.js +4 -1
- package/dist/db/seed.js.map +1 -1
- package/dist/entitlements/decision.d.ts +86 -0
- package/dist/entitlements/decision.d.ts.map +1 -0
- package/dist/entitlements/decision.js +196 -0
- package/dist/entitlements/decision.js.map +1 -0
- package/dist/entitlements/derive.d.ts +30 -0
- package/dist/entitlements/derive.d.ts.map +1 -0
- package/dist/entitlements/derive.js +42 -0
- package/dist/entitlements/derive.js.map +1 -0
- package/dist/entitlements/index.d.ts +10 -0
- package/dist/entitlements/index.d.ts.map +1 -0
- package/dist/entitlements/index.js +7 -0
- package/dist/entitlements/index.js.map +1 -0
- package/dist/entitlements/sources.d.ts +30 -0
- package/dist/entitlements/sources.d.ts.map +1 -0
- package/dist/entitlements/sources.js +55 -0
- package/dist/entitlements/sources.js.map +1 -0
- package/dist/entitlements/standing.d.ts +22 -0
- package/dist/entitlements/standing.d.ts.map +1 -0
- package/dist/entitlements/standing.js +38 -0
- package/dist/entitlements/standing.js.map +1 -0
- package/dist/entitlements/types.d.ts +26 -0
- package/dist/entitlements/types.d.ts.map +1 -0
- package/dist/entitlements/types.js +15 -0
- package/dist/entitlements/types.js.map +1 -0
- package/dist/index.d.ts +74 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -4
- package/dist/index.js.map +1 -1
- package/dist/mail/dedup-store.d.ts +25 -0
- package/dist/mail/dedup-store.d.ts.map +1 -0
- package/dist/mail/dedup-store.js +51 -0
- package/dist/mail/dedup-store.js.map +1 -0
- package/dist/mail/index.d.ts +10 -0
- package/dist/mail/index.d.ts.map +1 -0
- package/dist/mail/index.js +9 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/mail/provider.d.ts +31 -0
- package/dist/mail/provider.d.ts.map +1 -0
- package/dist/mail/provider.js +248 -0
- package/dist/mail/provider.js.map +1 -0
- package/dist/mail/resend-client.d.ts +16 -0
- package/dist/mail/resend-client.d.ts.map +1 -0
- package/dist/mail/resend-client.js +100 -0
- package/dist/mail/resend-client.js.map +1 -0
- package/dist/mail/resend.d.ts.map +1 -1
- package/dist/mail/resend.js +2 -0
- package/dist/mail/resend.js.map +1 -1
- package/dist/mail/sanitize.d.ts +17 -0
- package/dist/mail/sanitize.d.ts.map +1 -0
- package/dist/mail/sanitize.js +51 -0
- package/dist/mail/sanitize.js.map +1 -0
- package/dist/mail/sender-resolver.d.ts +8 -0
- package/dist/mail/sender-resolver.d.ts.map +1 -0
- package/dist/mail/sender-resolver.js +11 -0
- package/dist/mail/sender-resolver.js.map +1 -0
- package/dist/mail/template-renderer.d.ts +10 -0
- package/dist/mail/template-renderer.d.ts.map +1 -0
- package/dist/mail/template-renderer.js +17 -0
- package/dist/mail/template-renderer.js.map +1 -0
- package/dist/mail/types.d.ts +143 -0
- package/dist/mail/types.d.ts.map +1 -0
- package/dist/mail/types.js +34 -0
- package/dist/mail/types.js.map +1 -0
- package/dist/onboarding/domain.d.ts +146 -0
- package/dist/onboarding/domain.d.ts.map +1 -0
- package/dist/onboarding/domain.js +348 -0
- package/dist/onboarding/domain.js.map +1 -0
- package/dist/onboarding/interaction-review.d.ts +123 -0
- package/dist/onboarding/interaction-review.d.ts.map +1 -0
- package/dist/onboarding/interaction-review.js +413 -0
- package/dist/onboarding/interaction-review.js.map +1 -0
- package/dist/onboarding/persistence.d.ts +106 -0
- package/dist/onboarding/persistence.d.ts.map +1 -0
- package/dist/onboarding/persistence.js +430 -0
- package/dist/onboarding/persistence.js.map +1 -0
- package/dist/onboarding/portal-actions.d.ts +48 -0
- package/dist/onboarding/portal-actions.d.ts.map +1 -0
- package/dist/onboarding/portal-actions.js +153 -0
- package/dist/onboarding/portal-actions.js.map +1 -0
- package/dist/onboarding/portal.d.ts +58 -0
- package/dist/onboarding/portal.d.ts.map +1 -0
- package/dist/onboarding/portal.js +170 -0
- package/dist/onboarding/portal.js.map +1 -0
- package/dist/pricing-engine/bundle.d.ts +22 -0
- package/dist/pricing-engine/bundle.d.ts.map +1 -0
- package/dist/pricing-engine/bundle.js +44 -0
- package/dist/pricing-engine/bundle.js.map +1 -0
- package/dist/pricing-engine/errors.d.ts +32 -0
- package/dist/pricing-engine/errors.d.ts.map +1 -0
- package/dist/pricing-engine/errors.js +44 -0
- package/dist/pricing-engine/errors.js.map +1 -0
- package/dist/pricing-engine/index.d.ts +12 -0
- package/dist/pricing-engine/index.d.ts.map +1 -0
- package/dist/pricing-engine/index.js +8 -0
- package/dist/pricing-engine/index.js.map +1 -0
- package/dist/pricing-engine/money.d.ts +32 -0
- package/dist/pricing-engine/money.d.ts.map +1 -0
- package/dist/pricing-engine/money.js +58 -0
- package/dist/pricing-engine/money.js.map +1 -0
- package/dist/pricing-engine/pipeline.d.ts +3 -0
- package/dist/pricing-engine/pipeline.d.ts.map +1 -0
- package/dist/pricing-engine/pipeline.js +327 -0
- package/dist/pricing-engine/pipeline.js.map +1 -0
- package/dist/pricing-engine/proration.d.ts +3 -0
- package/dist/pricing-engine/proration.d.ts.map +1 -0
- package/dist/pricing-engine/proration.js +50 -0
- package/dist/pricing-engine/proration.js.map +1 -0
- package/dist/pricing-engine/shadow.d.ts +51 -0
- package/dist/pricing-engine/shadow.d.ts.map +1 -0
- package/dist/pricing-engine/shadow.js +54 -0
- package/dist/pricing-engine/shadow.js.map +1 -0
- package/dist/pricing-engine/types.d.ts +184 -0
- package/dist/pricing-engine/types.d.ts.map +1 -0
- package/dist/pricing-engine/types.js +31 -0
- package/dist/pricing-engine/types.js.map +1 -0
- package/dist/rbac/authorize.d.ts +3 -6
- package/dist/rbac/authorize.d.ts.map +1 -1
- package/dist/rbac/authorize.js +4 -8
- package/dist/rbac/authorize.js.map +1 -1
- package/dist/rbac/catalog.d.ts.map +1 -1
- package/dist/rbac/catalog.js +414 -21
- package/dist/rbac/catalog.js.map +1 -1
- package/dist/subscription-billing/dunning.d.ts +18 -0
- package/dist/subscription-billing/dunning.d.ts.map +1 -0
- package/dist/subscription-billing/dunning.js +28 -0
- package/dist/subscription-billing/dunning.js.map +1 -0
- package/dist/subscription-billing/events.d.ts +58 -0
- package/dist/subscription-billing/events.d.ts.map +1 -0
- package/dist/subscription-billing/events.js +38 -0
- package/dist/subscription-billing/events.js.map +1 -0
- package/dist/subscription-billing/index.d.ts +18 -0
- package/dist/subscription-billing/index.d.ts.map +1 -0
- package/dist/subscription-billing/index.js +16 -0
- package/dist/subscription-billing/index.js.map +1 -0
- package/dist/subscription-billing/plans.d.ts +34 -0
- package/dist/subscription-billing/plans.d.ts.map +1 -0
- package/dist/subscription-billing/plans.js +85 -0
- package/dist/subscription-billing/plans.js.map +1 -0
- package/dist/subscription-billing/product-config.d.ts +61 -0
- package/dist/subscription-billing/product-config.d.ts.map +1 -0
- package/dist/subscription-billing/product-config.js +50 -0
- package/dist/subscription-billing/product-config.js.map +1 -0
- package/dist/subscription-billing/provider/asaas.d.ts +26 -0
- package/dist/subscription-billing/provider/asaas.d.ts.map +1 -0
- package/dist/subscription-billing/provider/asaas.js +229 -0
- package/dist/subscription-billing/provider/asaas.js.map +1 -0
- package/dist/subscription-billing/provider/disabled.d.ts +30 -0
- package/dist/subscription-billing/provider/disabled.d.ts.map +1 -0
- package/dist/subscription-billing/provider/disabled.js +42 -0
- package/dist/subscription-billing/provider/disabled.js.map +1 -0
- package/dist/subscription-billing/provider/registry.d.ts +10 -0
- package/dist/subscription-billing/provider/registry.d.ts.map +1 -0
- package/dist/subscription-billing/provider/registry.js +26 -0
- package/dist/subscription-billing/provider/registry.js.map +1 -0
- package/dist/subscription-billing/reconcile.d.ts +45 -0
- package/dist/subscription-billing/reconcile.d.ts.map +1 -0
- package/dist/subscription-billing/reconcile.js +192 -0
- package/dist/subscription-billing/reconcile.js.map +1 -0
- package/dist/subscription-billing/repository.d.ts +168 -0
- package/dist/subscription-billing/repository.d.ts.map +1 -0
- package/dist/subscription-billing/repository.js +336 -0
- package/dist/subscription-billing/repository.js.map +1 -0
- package/dist/subscription-billing/service.d.ts +175 -0
- package/dist/subscription-billing/service.d.ts.map +1 -0
- package/dist/subscription-billing/service.js +818 -0
- package/dist/subscription-billing/service.js.map +1 -0
- package/dist/subscription-billing/types.d.ts +161 -0
- package/dist/subscription-billing/types.d.ts.map +1 -0
- package/dist/subscription-billing/types.js +24 -0
- package/dist/subscription-billing/types.js.map +1 -0
- package/dist/vault/crypto.d.ts +22 -0
- package/dist/vault/crypto.d.ts.map +1 -0
- package/dist/vault/crypto.js +88 -0
- package/dist/vault/crypto.js.map +1 -0
- package/dist/vault/repository.d.ts +45 -0
- package/dist/vault/repository.d.ts.map +1 -0
- package/dist/vault/repository.js +111 -0
- package/dist/vault/repository.js.map +1 -0
- package/package.json +23 -13
- package/src/db/migrations/0012_vault.sql +51 -0
- package/src/db/migrations/0013_crm.sql +67 -0
- package/src/db/migrations/0014_billing.sql +146 -0
- package/src/db/migrations/0015_merchant_billing_ar.sql +201 -0
- package/src/db/migrations/0016_office_onboarding.sql +162 -0
- package/src/db/migrations/0017_office_onboarding_api.sql +21 -0
- package/src/db/migrations/0018_office_onboarding_portal_actions.sql +60 -0
- package/src/db/migrations/0019_office_onboarding_interaction_review.sql +134 -0
- package/src/db/migrations/0020_saas_billing.sql +157 -0
- package/src/db/migrations/0021_saas_billing_indexes.sql +23 -0
- package/src/db/migrations/0022_saas_billing_multi_product.sql +75 -0
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
import { ConflictError, NotFoundError, ValidationError, uuidv7 } from '@baruk/shared';
|
|
2
|
+
import { withContext } from '../db/context.js';
|
|
3
|
+
import { appendAudit } from '../audit/audit.js';
|
|
4
|
+
import { hashPassword } from '../auth/password.js';
|
|
5
|
+
import { assignSystemRole, ensureMembership, ensureOrganization, ensureTenant, } from '../tenancy/provisioning.js';
|
|
6
|
+
import { ensureProductCatalog, ensureTenantProduct } from '../products/repository.js';
|
|
7
|
+
import { deriveEntitlements } from '../entitlements/derive.js';
|
|
8
|
+
import { sendEmail } from '../mail/resend.js';
|
|
9
|
+
import { applySubscriptionEvent, cancelSubscription, changePlan, startTrial } from './reconcile.js';
|
|
10
|
+
import { dunningStepsAsEvents } from './dunning.js';
|
|
11
|
+
import { resolveSubscriptionBillingProvider } from './provider/registry.js';
|
|
12
|
+
import { appendDomainEvents, backofficeSummary, ensureSaasPlanCatalog, getPlan, getSubscription, insertWebhookEvent, listAllSubscriptions, listBillingEvents, listExecutedDunningSteps, listFailedWebhookEvents, listInvoices, listPublicPlans, listSubscriptionsForCron, lockTenantBilling, markWebhookEvent, recordDunningStep, setSubscriptionProviderRefs, upsertInvoice, upsertSubscription, withOperatorContext, } from './repository.js';
|
|
13
|
+
import { DEFAULT_PRODUCT_KEY, SAAS_PLANS } from './plans.js';
|
|
14
|
+
import { singleProductRegistry } from './product-config.js';
|
|
15
|
+
function asRegistry(source) {
|
|
16
|
+
return 'resolve' in source ? source : singleProductRegistry(source);
|
|
17
|
+
}
|
|
18
|
+
function providerFor(cfg) {
|
|
19
|
+
return resolveSubscriptionBillingProvider(cfg.provider, {
|
|
20
|
+
apiKey: cfg.apiKey,
|
|
21
|
+
webhookSecret: cfg.webhookSecret,
|
|
22
|
+
mode: cfg.mode,
|
|
23
|
+
baseUrl: cfg.baseUrl,
|
|
24
|
+
fetchImpl: cfg.fetchImpl,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function todayOf(nowIso) {
|
|
28
|
+
return nowIso.slice(0, 10);
|
|
29
|
+
}
|
|
30
|
+
function addDays(nowIso, days) {
|
|
31
|
+
const d = new Date(nowIso);
|
|
32
|
+
d.setUTCDate(d.getUTCDate() + days);
|
|
33
|
+
return d.toISOString().slice(0, 10);
|
|
34
|
+
}
|
|
35
|
+
function addMonths(nowIso, months) {
|
|
36
|
+
const d = new Date(nowIso);
|
|
37
|
+
d.setUTCMonth(d.getUTCMonth() + months);
|
|
38
|
+
return d.toISOString().slice(0, 10);
|
|
39
|
+
}
|
|
40
|
+
function daysBetween(fromDate, toDate) {
|
|
41
|
+
return Math.floor((new Date(`${toDate}T00:00:00Z`).getTime() - new Date(`${fromDate}T00:00:00Z`).getTime()) /
|
|
42
|
+
86_400_000);
|
|
43
|
+
}
|
|
44
|
+
/** Mapeia o status da assinatura para o status do tenant (suspensão é decisão do cron). */
|
|
45
|
+
function tenantStatusFor(status) {
|
|
46
|
+
switch (status) {
|
|
47
|
+
case 'trialing':
|
|
48
|
+
return 'trial';
|
|
49
|
+
case 'active':
|
|
50
|
+
return 'active';
|
|
51
|
+
case 'past_due':
|
|
52
|
+
return 'active'; // acesso segue no grace period; cron decide suspender
|
|
53
|
+
case 'canceled':
|
|
54
|
+
return 'churned';
|
|
55
|
+
default:
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function syncTenantStatus(tx, tenantId, status) {
|
|
60
|
+
const next = tenantStatusFor(status);
|
|
61
|
+
if (!next)
|
|
62
|
+
return;
|
|
63
|
+
await tx `UPDATE tenant SET status = ${next}, updated_at = now() WHERE id = ${tenantId} AND status <> 'suspended'`;
|
|
64
|
+
}
|
|
65
|
+
// ── Catálogo ─────────────────────────────────────────────────────────────────
|
|
66
|
+
export { listPublicPlans, getPlan, ensureSaasPlanCatalog };
|
|
67
|
+
/** Seed do catálogo padrão (chamado pelo seed do core). */
|
|
68
|
+
export async function seedSaasPlans(db) {
|
|
69
|
+
await ensureProductCatalog(db);
|
|
70
|
+
await ensureSaasPlanCatalog(db, SAAS_PLANS);
|
|
71
|
+
}
|
|
72
|
+
function slugify(name) {
|
|
73
|
+
return name
|
|
74
|
+
.normalize('NFD')
|
|
75
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
78
|
+
.replace(/^-+|-+$/g, '')
|
|
79
|
+
.slice(0, 40);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Cadastro self-service (Fase 17): cria usuário + tenant em TRIAL + organização +
|
|
83
|
+
* membership com papel `owner` + entitlement do produto + assinatura `trialing`.
|
|
84
|
+
* Idempotência: e-mail já cadastrado → 409 (nunca sobrescreve credencial existente).
|
|
85
|
+
*/
|
|
86
|
+
export async function signupSelfService(db, input, nowIso) {
|
|
87
|
+
const email = input.email.trim().toLowerCase();
|
|
88
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email))
|
|
89
|
+
throw new ValidationError('E-mail inválido');
|
|
90
|
+
if (input.password.length < 8)
|
|
91
|
+
throw new ValidationError('Senha deve ter ao menos 8 caracteres');
|
|
92
|
+
if (!input.companyName.trim())
|
|
93
|
+
throw new ValidationError('Nome da empresa é obrigatório');
|
|
94
|
+
const plan = await getPlan(db, input.planKey);
|
|
95
|
+
if (!plan || !plan.isActive || !plan.isPublic) {
|
|
96
|
+
throw new ValidationError(`Plano indisponível: ${input.planKey}`);
|
|
97
|
+
}
|
|
98
|
+
const existing = await db `
|
|
99
|
+
SELECT id FROM "user" WHERE email = ${email} AND deleted_at IS NULL
|
|
100
|
+
`;
|
|
101
|
+
if (existing[0]) {
|
|
102
|
+
throw new ConflictError('Já existe uma conta com este e-mail. Faça login para continuar.');
|
|
103
|
+
}
|
|
104
|
+
// Código único do tenant a partir do nome (sufixo em caso de colisão).
|
|
105
|
+
const base = slugify(input.companyName) || 'escritorio';
|
|
106
|
+
let code = base;
|
|
107
|
+
for (let i = 2;; i += 1) {
|
|
108
|
+
const clash = await db `
|
|
109
|
+
SELECT id FROM tenant WHERE lower(code) = lower(${code}) AND deleted_at IS NULL
|
|
110
|
+
`;
|
|
111
|
+
if (!clash[0])
|
|
112
|
+
break;
|
|
113
|
+
code = `${base}-${i}`;
|
|
114
|
+
if (i > 50) {
|
|
115
|
+
code = `${base}-${uuidv7().slice(-6)}`;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// INSERT puro (não upsert): o papel de app pode CRIAR credencial, nunca
|
|
120
|
+
// sobrescrever (REVOKE UPDATE em user_credential — anti account-takeover).
|
|
121
|
+
// A checagem de e-mail acima já garante que o usuário é novo.
|
|
122
|
+
const userRows = await db `
|
|
123
|
+
INSERT INTO "user" (id, email, full_name, user_type, auth_provider)
|
|
124
|
+
VALUES (${uuidv7()}, ${email}, ${input.fullName.trim()}, 'staff', 'password')
|
|
125
|
+
RETURNING id
|
|
126
|
+
`;
|
|
127
|
+
const user = { id: userRows[0].id, email };
|
|
128
|
+
await db `
|
|
129
|
+
INSERT INTO user_credential (user_id, password_hash)
|
|
130
|
+
VALUES (${user.id}, ${hashPassword(input.password)})
|
|
131
|
+
`;
|
|
132
|
+
const tenantId = await ensureTenant(db, {
|
|
133
|
+
code,
|
|
134
|
+
displayName: input.companyName.trim(),
|
|
135
|
+
status: 'trial',
|
|
136
|
+
primaryProduct: 'contabil',
|
|
137
|
+
});
|
|
138
|
+
await ensureOrganization(db, tenantId, input.companyName.trim());
|
|
139
|
+
await ensureMembership(db, tenantId, user.id);
|
|
140
|
+
await assignSystemRole(db, tenantId, user.id, 'owner');
|
|
141
|
+
await ensureProductCatalog(db);
|
|
142
|
+
await ensureTenantProduct(db, tenantId, 'contabil');
|
|
143
|
+
const trialEndsAt = addDays(nowIso, plan.trialDays > 0 ? plan.trialDays : 14);
|
|
144
|
+
const { subscription, domainEvents } = startTrial(tenantId, plan.key, trialEndsAt, nowIso);
|
|
145
|
+
await withContext(db, { tenantId, userId: user.id }, async (tx) => {
|
|
146
|
+
const row = await upsertSubscription(tx, { tenantId, planKey: plan.key, subscription });
|
|
147
|
+
await appendDomainEvents(tx, row.rowId, domainEvents);
|
|
148
|
+
await appendAudit(tx, {
|
|
149
|
+
actorId: user.id,
|
|
150
|
+
action: 'signup',
|
|
151
|
+
resourceType: 'saas_subscription',
|
|
152
|
+
resourceId: row.rowId,
|
|
153
|
+
metadata: { planKey: plan.key, trialEndsAt },
|
|
154
|
+
ip: input.ip ?? null,
|
|
155
|
+
userAgent: input.userAgent ?? null,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
return { tenantId, tenantCode: code, userId: user.id, planKey: plan.key, trialEndsAt };
|
|
159
|
+
}
|
|
160
|
+
export function standingFor(sub) {
|
|
161
|
+
if (!sub)
|
|
162
|
+
return { ok: false, reason: 'none' };
|
|
163
|
+
switch (sub.status) {
|
|
164
|
+
case 'active':
|
|
165
|
+
return { ok: true, reason: 'ok' };
|
|
166
|
+
case 'trialing':
|
|
167
|
+
return { ok: true, reason: 'trialing' };
|
|
168
|
+
case 'past_due':
|
|
169
|
+
return { ok: false, reason: 'past_due' };
|
|
170
|
+
case 'paused':
|
|
171
|
+
return { ok: false, reason: 'paused' };
|
|
172
|
+
default:
|
|
173
|
+
return { ok: false, reason: 'canceled' };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** Entitlements finos derivados do plano (EntitlementSource 'PLAN', v0.1 stateless). */
|
|
177
|
+
export function entitlementsForPlan(sub, plan, nowIso) {
|
|
178
|
+
if (!sub || !plan)
|
|
179
|
+
return [];
|
|
180
|
+
const standing = standingFor(sub);
|
|
181
|
+
const targetGrants = standing.ok
|
|
182
|
+
? plan.modules.map((moduleCode) => ({ moduleCode, source: 'PLAN', expiresAt: null }))
|
|
183
|
+
: [];
|
|
184
|
+
return deriveEntitlements({
|
|
185
|
+
tenantId: sub.tenantId,
|
|
186
|
+
subscriptionId: sub.subscriptionId,
|
|
187
|
+
targetGrants,
|
|
188
|
+
current: [],
|
|
189
|
+
now: nowIso,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
export async function subscriptionOverview(db, ctx, nowIso) {
|
|
193
|
+
const { sub, invoices, events } = await withContext(db, ctx, async (tx) => {
|
|
194
|
+
const sub0 = await getSubscription(tx);
|
|
195
|
+
return {
|
|
196
|
+
sub: sub0,
|
|
197
|
+
invoices: sub0 ? await listInvoices(tx) : [],
|
|
198
|
+
events: sub0 ? await listBillingEvents(tx, 20) : [],
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
// Fora da transação: getPlan usa outra conexão do pool — dentro dela seria
|
|
202
|
+
// um segundo checkout de conexão com transação aberta (deadlock de pool).
|
|
203
|
+
const plan = sub ? await getPlan(db, sub.planKey) : null;
|
|
204
|
+
return {
|
|
205
|
+
subscription: sub,
|
|
206
|
+
plan,
|
|
207
|
+
standing: standingFor(sub),
|
|
208
|
+
invoices,
|
|
209
|
+
entitlements: entitlementsForPlan(sub, plan, nowIso),
|
|
210
|
+
events,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Inicia o checkout do plano no gateway da plataforma. A atribuição do webhook
|
|
215
|
+
* de volta ao tenant usa `externalReference = "<tenantId>:<planKey>"` (RFC §9).
|
|
216
|
+
* Em modo 'disabled' não há gateway: o fluxo comercial segue e a ativação é
|
|
217
|
+
* manual (backoffice) — nunca cobra por falta de configuração.
|
|
218
|
+
*/
|
|
219
|
+
export async function startCheckout(db, ctx, source, planKey, _nowIso, productKey = DEFAULT_PRODUCT_KEY) {
|
|
220
|
+
const plan = await getPlan(db, planKey, productKey);
|
|
221
|
+
if (!plan || !plan.isActive || !plan.isPublic) {
|
|
222
|
+
throw new ValidationError(`Plano indisponível: ${planKey}`);
|
|
223
|
+
}
|
|
224
|
+
if (plan.periodicity !== 'monthly') {
|
|
225
|
+
// O checkout certificado cria link RECURRENT MENSAL; vender outra
|
|
226
|
+
// periodicidade cobraria errado. Suporte a ciclo anual é v0.2.
|
|
227
|
+
throw new ValidationError('Este plano ainda não está disponível para contratação online.');
|
|
228
|
+
}
|
|
229
|
+
// A conta de gateway e a URL de retorno saem do PRODUTO do plano — nunca de
|
|
230
|
+
// uma configuração global. É isso que impede o checkout de um produto voltar
|
|
231
|
+
// para o domínio de outro, ou cobrar na conta errada.
|
|
232
|
+
const cfg = asRegistry(source).resolve({ productKey: plan.productKey, tenantId: ctx.tenantId });
|
|
233
|
+
const provider = providerFor(cfg);
|
|
234
|
+
if (provider.mode === 'disabled') {
|
|
235
|
+
return { mode: 'disabled', provider: provider.name, url: null };
|
|
236
|
+
}
|
|
237
|
+
// 1) Leitura curta (transação própria) — nada de rede com transação aberta.
|
|
238
|
+
const snapshot = await withContext(db, ctx, async (tx) => {
|
|
239
|
+
const tenantRows = await tx `
|
|
240
|
+
SELECT display_name FROM tenant WHERE id = ${ctx.tenantId}
|
|
241
|
+
`;
|
|
242
|
+
const userRows = await tx `
|
|
243
|
+
SELECT email FROM "user" WHERE id = ${ctx.userId}
|
|
244
|
+
`;
|
|
245
|
+
const sub = await getSubscription(tx);
|
|
246
|
+
return {
|
|
247
|
+
tenantName: tenantRows[0]?.display_name ?? 'Tenant Baruk',
|
|
248
|
+
email: userRows[0]?.email ?? null,
|
|
249
|
+
existingCustomerId: sub?.providerCustomerId ?? null,
|
|
250
|
+
subRowId: sub?.rowId ?? null,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
// 2) Gateway (até ~24s de rede) — sem transação e sem conexão do pool presa.
|
|
254
|
+
const customer = snapshot.existingCustomerId
|
|
255
|
+
? { externalId: snapshot.existingCustomerId }
|
|
256
|
+
: await provider.ensureCustomer({
|
|
257
|
+
tenantId: ctx.tenantId,
|
|
258
|
+
name: snapshot.tenantName,
|
|
259
|
+
email: snapshot.email,
|
|
260
|
+
});
|
|
261
|
+
const checkout = await provider.createCheckout({
|
|
262
|
+
tenantId: ctx.tenantId,
|
|
263
|
+
planKey: plan.key,
|
|
264
|
+
amountBRL: plan.amountCents / 100,
|
|
265
|
+
customerExternalId: customer.externalId,
|
|
266
|
+
returnUrl: `${cfg.webBaseUrl}/checkout/retorno`,
|
|
267
|
+
description: `Assinatura Baruk — plano ${plan.name}`,
|
|
268
|
+
});
|
|
269
|
+
// 3) Persistência curta: SOMENTE as referências de provider (nunca o estado
|
|
270
|
+
// da assinatura — um webhook pode tê-la mudado durante a chamada de rede).
|
|
271
|
+
await withContext(db, ctx, async (tx) => {
|
|
272
|
+
await lockTenantBilling(tx, ctx.tenantId);
|
|
273
|
+
await setSubscriptionProviderRefs(tx, ctx.tenantId, provider.name, customer.externalId);
|
|
274
|
+
await appendAudit(tx, {
|
|
275
|
+
actorId: ctx.userId,
|
|
276
|
+
action: 'checkout_started',
|
|
277
|
+
resourceType: 'saas_subscription',
|
|
278
|
+
resourceId: snapshot.subRowId,
|
|
279
|
+
metadata: { planKey: plan.key, provider: provider.name, mode: provider.mode },
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
return { mode: provider.mode, provider: provider.name, url: checkout.url };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Lê o `externalReference` que voltou do gateway.
|
|
286
|
+
*
|
|
287
|
+
* Formato canônico: `"<tenantId>:<productKey>:<planKey>"`. O formato antigo de
|
|
288
|
+
* duas partes (`"<tenantId>:<planKey>"`) continua aceito para os payment links
|
|
289
|
+
* emitidos antes do multi-produto — nesses casos o produto é o Contábil, o
|
|
290
|
+
* único que existia. Sem o produto explícito, um evento de um produto poderia
|
|
291
|
+
* reconciliar a assinatura de outro no mesmo tenant.
|
|
292
|
+
*/
|
|
293
|
+
function parseAttribution(event) {
|
|
294
|
+
const ref = event.externalReference;
|
|
295
|
+
if (!ref)
|
|
296
|
+
return null;
|
|
297
|
+
const parts = ref.split(':');
|
|
298
|
+
if (parts.length === 3) {
|
|
299
|
+
const [tenantId, productKey, planKey] = parts;
|
|
300
|
+
if (!tenantId || !productKey || !planKey)
|
|
301
|
+
return null;
|
|
302
|
+
return { tenantId, productKey, planKey };
|
|
303
|
+
}
|
|
304
|
+
if (parts.length === 2) {
|
|
305
|
+
const [tenantId, planKey] = parts;
|
|
306
|
+
if (!tenantId || !planKey)
|
|
307
|
+
return null;
|
|
308
|
+
return { tenantId, productKey: DEFAULT_PRODUCT_KEY, planKey };
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Processa um webhook do gateway: verifica autenticidade → dedup por
|
|
314
|
+
* `externalEventId` → resolve tenant via `externalReference` → reconcilia via
|
|
315
|
+
* domínio puro → espelha fatura → sincroniza tenant.status → audita.
|
|
316
|
+
* A rota SEMPRE responde 200 após o dedup (regra dos 15 não-200 do ASAAS).
|
|
317
|
+
*/
|
|
318
|
+
export async function processWebhook(db, source, headers, rawBody, payload, nowIso, productKey = DEFAULT_PRODUCT_KEY) {
|
|
319
|
+
// O segredo do webhook é da CONTA do produto. Como a verificação acontece
|
|
320
|
+
// antes de qualquer parsing, o produto tem de vir de fora — na prática, da
|
|
321
|
+
// própria URL cadastrada no gateway (uma por conta).
|
|
322
|
+
const cfg = asRegistry(source).resolve({ productKey });
|
|
323
|
+
const provider = providerFor(cfg);
|
|
324
|
+
if (!provider.verifyWebhook(headers, rawBody)) {
|
|
325
|
+
return { outcome: 'unauthorized' };
|
|
326
|
+
}
|
|
327
|
+
const event = provider.parseWebhookEvent(payload);
|
|
328
|
+
if (!event)
|
|
329
|
+
return { outcome: 'ignored' };
|
|
330
|
+
const webhookId = await insertWebhookEvent(db, {
|
|
331
|
+
provider: provider.name,
|
|
332
|
+
externalEventId: event.externalEventId,
|
|
333
|
+
eventType: event.eventType,
|
|
334
|
+
payload,
|
|
335
|
+
});
|
|
336
|
+
if (!webhookId)
|
|
337
|
+
return { outcome: 'duplicate' };
|
|
338
|
+
return reconcileStoredWebhookEvent(db, provider, event, webhookId, nowIso, productKey);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Reconcilia um evento JÁ persistido em `saas_webhook_event` (caminho comum do
|
|
342
|
+
* webhook inbound e do reprocessamento de eventos `failed` pelo cron).
|
|
343
|
+
*/
|
|
344
|
+
async function reconcileStoredWebhookEvent(db, provider, event, webhookId, nowIso, expectedProductKey = DEFAULT_PRODUCT_KEY) {
|
|
345
|
+
const attribution = parseAttribution(event);
|
|
346
|
+
if (!attribution) {
|
|
347
|
+
await markWebhookEvent(db, webhookId, 'ignored', null, 'externalReference ausente/inválida');
|
|
348
|
+
return { outcome: 'ignored' };
|
|
349
|
+
}
|
|
350
|
+
// ISOLAMENTO FINANCEIRO: a conta que assinou o webhook é a conta de UM
|
|
351
|
+
// produto. Se o evento alega pertencer a outro, alguém configurou a URL
|
|
352
|
+
// errada no gateway — ou é tentativa de mover dinheiro entre produtos.
|
|
353
|
+
// Recusar é obrigatório: reconciliar aqui escreveria na assinatura errada.
|
|
354
|
+
if (attribution.productKey !== expectedProductKey) {
|
|
355
|
+
await markWebhookEvent(db, webhookId, 'ignored', null, `produto divergente: evento=${attribution.productKey} conta=${expectedProductKey}`);
|
|
356
|
+
return { outcome: 'ignored' };
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
const plan = await getPlan(db, attribution.planKey, attribution.productKey);
|
|
360
|
+
const { tenantId } = attribution;
|
|
361
|
+
const applied = await withContext(db, { tenantId }, async (tx) => {
|
|
362
|
+
await lockTenantBilling(tx, tenantId);
|
|
363
|
+
const current = await getSubscription(tx);
|
|
364
|
+
// Guarda de ordenação (webhook atrasado): um OVERDUE que chega DEPOIS do
|
|
365
|
+
// pagamento do mesmo ciclo (período corrente ainda vigente) é obsoleto —
|
|
366
|
+
// aplicá-lo marcaria past_due uma assinatura em dia. Política de
|
|
367
|
+
// orquestração; o domínio puro certificado permanece intocado.
|
|
368
|
+
if (event.invoice?.status === 'overdue' &&
|
|
369
|
+
current?.status === 'active' &&
|
|
370
|
+
current.currentPeriodEnd != null &&
|
|
371
|
+
current.currentPeriodEnd >= todayOf(nowIso)) {
|
|
372
|
+
return 'overdue obsoleto — período corrente já pago';
|
|
373
|
+
}
|
|
374
|
+
// Sem plano conhecido E sem assinatura existente não há o que reconciliar —
|
|
375
|
+
// persistir criaria uma linha órfã com plano arbitrário.
|
|
376
|
+
if (!plan && !current) {
|
|
377
|
+
return `plano desconhecido (${attribution.planKey}) sem assinatura existente`;
|
|
378
|
+
}
|
|
379
|
+
// Guarda anti-ressurreição: pagamento ANTERIOR ao cancelamento não desfaz
|
|
380
|
+
// um cancelamento explícito — reativação exige checkout novo.
|
|
381
|
+
if (current?.status === 'canceled' &&
|
|
382
|
+
current.canceledAt != null &&
|
|
383
|
+
(event.invoice?.status === 'received' || event.invoice?.status === 'confirmed') &&
|
|
384
|
+
event.invoice.paidAt != null &&
|
|
385
|
+
new Date(event.invoice.paidAt).getTime() < new Date(current.canceledAt).getTime()) {
|
|
386
|
+
return 'pagamento anterior ao cancelamento — reativação exige novo checkout';
|
|
387
|
+
}
|
|
388
|
+
// Atribuição de plano: em assinatura ATIVA o plano vem do estado interno
|
|
389
|
+
// (fonte de verdade) — payment link antigo com externalReference de plano
|
|
390
|
+
// anterior NÃO reverte um upgrade. Em ativação (trial/dormente), vale o
|
|
391
|
+
// plano efetivamente pago (externalReference).
|
|
392
|
+
const effectivePlanKey = current?.status === 'active' ? current.planKey : (plan?.key ?? null);
|
|
393
|
+
const { subscription, domainEvents } = applySubscriptionEvent({
|
|
394
|
+
current,
|
|
395
|
+
tenantId,
|
|
396
|
+
planKey: effectivePlanKey,
|
|
397
|
+
event,
|
|
398
|
+
now: nowIso,
|
|
399
|
+
});
|
|
400
|
+
// Evento informativo (ex.: PENDING) para tenant sem assinatura: nada a
|
|
401
|
+
// persistir — gravar o placeholder criaria uma assinatura 'canceled'
|
|
402
|
+
// fantasma e marcaria o tenant como churned.
|
|
403
|
+
if (!current && domainEvents.length === 0) {
|
|
404
|
+
return 'evento sem transição para tenant sem assinatura';
|
|
405
|
+
}
|
|
406
|
+
let next = subscription;
|
|
407
|
+
// Backfill: past_due nascido de trial não tem currentPeriodEnd — a régua
|
|
408
|
+
// ancora no fim do trial (senão ficaria em limbo, sem régua nem suspensão).
|
|
409
|
+
if (next.status === 'past_due' && next.currentPeriodEnd == null) {
|
|
410
|
+
next = { ...next, currentPeriodEnd: current?.trialEndsAt ?? todayOf(nowIso) };
|
|
411
|
+
}
|
|
412
|
+
// Periodicidade: o domínio v0.1 renova sempre +1 mês; planos não mensais
|
|
413
|
+
// (ativação manual/backoffice) seguem o ciclo do plano.
|
|
414
|
+
if (next.status === 'active' &&
|
|
415
|
+
plan &&
|
|
416
|
+
plan.periodicity !== 'monthly' &&
|
|
417
|
+
next.currentPeriodEnd != null) {
|
|
418
|
+
next = {
|
|
419
|
+
...next,
|
|
420
|
+
currentPeriodEnd: addMonths(nowIso, plan.periodicity === 'yearly' ? 12 : 3),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const row = await upsertSubscription(tx, {
|
|
424
|
+
tenantId,
|
|
425
|
+
planKey: effectivePlanKey ?? current?.planKey ?? next.items[0]?.planKey ?? 'starter',
|
|
426
|
+
subscription: next,
|
|
427
|
+
provider: provider.name,
|
|
428
|
+
providerCustomerId: event.externalCustomerId ?? null,
|
|
429
|
+
providerSubscriptionId: event.subscription?.externalId ?? null,
|
|
430
|
+
});
|
|
431
|
+
await appendDomainEvents(tx, row.rowId, domainEvents);
|
|
432
|
+
if (event.invoice) {
|
|
433
|
+
await upsertInvoice(tx, {
|
|
434
|
+
tenantId,
|
|
435
|
+
subscriptionRowId: row.rowId,
|
|
436
|
+
provider: provider.name,
|
|
437
|
+
externalId: event.invoice.externalId,
|
|
438
|
+
status: event.invoice.status,
|
|
439
|
+
amountCents: Math.abs(Math.round((event.invoice.value ?? 0) * 100)),
|
|
440
|
+
dueDate: event.invoice.dueDate ?? null,
|
|
441
|
+
paidAt: event.invoice.paidAt ?? null,
|
|
442
|
+
invoiceUrl: event.invoice.invoiceUrl ?? null,
|
|
443
|
+
description: event.invoice.description ?? null,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
await syncTenantStatus(tx, tenantId, next.status);
|
|
447
|
+
await appendAudit(tx, {
|
|
448
|
+
actorType: 'system',
|
|
449
|
+
action: `webhook_${event.eventType}`,
|
|
450
|
+
resourceType: 'saas_subscription',
|
|
451
|
+
resourceId: row.rowId,
|
|
452
|
+
metadata: {
|
|
453
|
+
provider: provider.name,
|
|
454
|
+
externalEventId: event.externalEventId,
|
|
455
|
+
status: next.status,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
return null;
|
|
459
|
+
});
|
|
460
|
+
if (applied !== null) {
|
|
461
|
+
await markWebhookEvent(db, webhookId, 'ignored', attribution.tenantId, applied);
|
|
462
|
+
return { outcome: 'ignored', tenantId: attribution.tenantId };
|
|
463
|
+
}
|
|
464
|
+
await markWebhookEvent(db, webhookId, 'processed', attribution.tenantId);
|
|
465
|
+
return { outcome: 'processed', tenantId: attribution.tenantId };
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
await markWebhookEvent(db, webhookId, 'failed', attribution.tenantId, err instanceof Error ? err.message : String(err));
|
|
469
|
+
return { outcome: 'failed', tenantId: attribution.tenantId };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// ── Gestão da assinatura (tenant) ────────────────────────────────────────────
|
|
473
|
+
export async function changePlanForTenant(db, ctx, newPlanKey, nowIso) {
|
|
474
|
+
const plan = await getPlan(db, newPlanKey);
|
|
475
|
+
if (!plan || !plan.isActive)
|
|
476
|
+
throw new ValidationError(`Plano desconhecido: ${newPlanKey}`);
|
|
477
|
+
return withContext(db, ctx, async (tx) => {
|
|
478
|
+
await lockTenantBilling(tx, ctx.tenantId);
|
|
479
|
+
const current = await getSubscription(tx);
|
|
480
|
+
if (!current)
|
|
481
|
+
throw new NotFoundError('Nenhuma assinatura para este tenant');
|
|
482
|
+
if (current.status === 'canceled') {
|
|
483
|
+
throw new ValidationError('Assinatura cancelada — reative antes de trocar de plano.');
|
|
484
|
+
}
|
|
485
|
+
if (current.planKey === plan.key)
|
|
486
|
+
return current;
|
|
487
|
+
const { subscription, domainEvents } = changePlan(current, plan.key, nowIso);
|
|
488
|
+
const row = await upsertSubscription(tx, {
|
|
489
|
+
tenantId: ctx.tenantId,
|
|
490
|
+
planKey: plan.key,
|
|
491
|
+
subscription,
|
|
492
|
+
});
|
|
493
|
+
await appendDomainEvents(tx, row.rowId, domainEvents);
|
|
494
|
+
await appendAudit(tx, {
|
|
495
|
+
actorId: ctx.userId,
|
|
496
|
+
action: 'plan_changed',
|
|
497
|
+
resourceType: 'saas_subscription',
|
|
498
|
+
resourceId: row.rowId,
|
|
499
|
+
diff: { from: current.planKey, to: plan.key },
|
|
500
|
+
});
|
|
501
|
+
return row;
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
export async function cancelForTenant(db, ctx, nowIso) {
|
|
505
|
+
return withContext(db, ctx, async (tx) => {
|
|
506
|
+
await lockTenantBilling(tx, ctx.tenantId);
|
|
507
|
+
const current = await getSubscription(tx);
|
|
508
|
+
if (!current)
|
|
509
|
+
throw new NotFoundError('Nenhuma assinatura para este tenant');
|
|
510
|
+
if (current.status === 'canceled')
|
|
511
|
+
return current;
|
|
512
|
+
const { subscription, domainEvents } = cancelSubscription(current, nowIso);
|
|
513
|
+
const row = await upsertSubscription(tx, {
|
|
514
|
+
tenantId: ctx.tenantId,
|
|
515
|
+
planKey: current.planKey,
|
|
516
|
+
subscription,
|
|
517
|
+
});
|
|
518
|
+
await appendDomainEvents(tx, row.rowId, domainEvents);
|
|
519
|
+
await syncTenantStatus(tx, ctx.tenantId, subscription.status);
|
|
520
|
+
await appendAudit(tx, {
|
|
521
|
+
actorId: ctx.userId,
|
|
522
|
+
action: 'subscription_canceled',
|
|
523
|
+
resourceType: 'saas_subscription',
|
|
524
|
+
resourceId: row.rowId,
|
|
525
|
+
});
|
|
526
|
+
return row;
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Reativação: assinatura cancelada/past_due volta pelo PAGAMENTO (checkout →
|
|
531
|
+
* webhook `received` reativa via reconcile). Este método só valida o estado e
|
|
532
|
+
* devolve o checkout; em modo 'disabled' a reativação é manual (backoffice).
|
|
533
|
+
*/
|
|
534
|
+
export async function reactivateForTenant(db, ctx, source, nowIso, productKey = DEFAULT_PRODUCT_KEY) {
|
|
535
|
+
const sub = await withContext(db, ctx, (tx) => getSubscription(tx, productKey));
|
|
536
|
+
if (!sub)
|
|
537
|
+
throw new NotFoundError('Nenhuma assinatura para este tenant');
|
|
538
|
+
if (sub.status === 'active' || sub.status === 'trialing') {
|
|
539
|
+
throw new ValidationError('Assinatura já está ativa.');
|
|
540
|
+
}
|
|
541
|
+
return startCheckout(db, ctx, source, sub.planKey, nowIso, sub.productKey);
|
|
542
|
+
}
|
|
543
|
+
// ── Backoffice (operadora) ───────────────────────────────────────────────────
|
|
544
|
+
export async function backofficeListSubscriptions(db, filter = {}) {
|
|
545
|
+
return withOperatorContext(db, (tx) => listAllSubscriptions(tx, filter));
|
|
546
|
+
}
|
|
547
|
+
export async function backofficeOverview(db) {
|
|
548
|
+
return withOperatorContext(db, (tx) => backofficeSummary(tx));
|
|
549
|
+
}
|
|
550
|
+
export async function backofficeListWebhookEvents(db, limit = 100) {
|
|
551
|
+
const rows = await db `
|
|
552
|
+
SELECT id, tenant_id, provider, external_event_id, event_type, status, error, processed_at, created_at
|
|
553
|
+
FROM saas_webhook_event ORDER BY created_at DESC LIMIT ${limit}
|
|
554
|
+
`;
|
|
555
|
+
return rows.map((r) => ({
|
|
556
|
+
id: r.id,
|
|
557
|
+
tenantId: r.tenant_id ?? null,
|
|
558
|
+
provider: r.provider,
|
|
559
|
+
externalEventId: r.external_event_id,
|
|
560
|
+
eventType: r.event_type,
|
|
561
|
+
status: r.status,
|
|
562
|
+
error: r.error ?? null,
|
|
563
|
+
processedAt: r.processed_at ? new Date(r.processed_at).toISOString() : null,
|
|
564
|
+
createdAt: new Date(r.created_at).toISOString(),
|
|
565
|
+
}));
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Ativação manual pela operadora (modo 'disabled' ou pagamento fora do gateway):
|
|
569
|
+
* simula uma fatura `received` e reconcilia pelo MESMO caminho do webhook —
|
|
570
|
+
* nenhuma transição de estado fora do domínio puro.
|
|
571
|
+
*/
|
|
572
|
+
export async function backofficeActivateSubscription(db, operatorEmail, tenantId, planKey, nowIso) {
|
|
573
|
+
const plan = await getPlan(db, planKey);
|
|
574
|
+
if (!plan)
|
|
575
|
+
throw new ValidationError(`Plano desconhecido: ${planKey}`);
|
|
576
|
+
return withContext(db, { tenantId }, async (tx) => {
|
|
577
|
+
await lockTenantBilling(tx, tenantId);
|
|
578
|
+
const current = await getSubscription(tx);
|
|
579
|
+
const manualEvent = {
|
|
580
|
+
externalEventId: `manual_${uuidv7()}`,
|
|
581
|
+
eventType: 'manual.activation',
|
|
582
|
+
kind: 'invoice',
|
|
583
|
+
invoice: {
|
|
584
|
+
externalId: `manual_${uuidv7()}`,
|
|
585
|
+
status: 'received',
|
|
586
|
+
value: plan.amountCents / 100,
|
|
587
|
+
paidAt: nowIso,
|
|
588
|
+
description: `Ativação manual (${operatorEmail})`,
|
|
589
|
+
},
|
|
590
|
+
externalReference: `${tenantId}:${plan.productKey ?? DEFAULT_PRODUCT_KEY}:${plan.key}`,
|
|
591
|
+
};
|
|
592
|
+
const { subscription, domainEvents } = applySubscriptionEvent({
|
|
593
|
+
current,
|
|
594
|
+
tenantId,
|
|
595
|
+
planKey: plan.key,
|
|
596
|
+
event: manualEvent,
|
|
597
|
+
now: nowIso,
|
|
598
|
+
});
|
|
599
|
+
const row = await upsertSubscription(tx, {
|
|
600
|
+
tenantId,
|
|
601
|
+
planKey: plan.key,
|
|
602
|
+
subscription,
|
|
603
|
+
provider: 'manual',
|
|
604
|
+
});
|
|
605
|
+
await appendDomainEvents(tx, row.rowId, domainEvents);
|
|
606
|
+
await upsertInvoice(tx, {
|
|
607
|
+
tenantId,
|
|
608
|
+
subscriptionRowId: row.rowId,
|
|
609
|
+
provider: 'manual',
|
|
610
|
+
externalId: manualEvent.invoice.externalId,
|
|
611
|
+
status: 'received',
|
|
612
|
+
amountCents: plan.amountCents,
|
|
613
|
+
paidAt: nowIso,
|
|
614
|
+
description: manualEvent.invoice.description ?? null,
|
|
615
|
+
});
|
|
616
|
+
await syncTenantStatus(tx, tenantId, subscription.status);
|
|
617
|
+
await appendAudit(tx, {
|
|
618
|
+
actorType: 'user',
|
|
619
|
+
action: 'backoffice_manual_activation',
|
|
620
|
+
resourceType: 'saas_subscription',
|
|
621
|
+
resourceId: row.rowId,
|
|
622
|
+
metadata: { operatorEmail, planKey: plan.key },
|
|
623
|
+
});
|
|
624
|
+
return row;
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Reprocessa eventos de webhook `failed` recentes (rede de segurança para o
|
|
629
|
+
* caso "falhou após o dedup e respondemos 200"). Idempotente: reconciliar um
|
|
630
|
+
* efeito já refletido é no-op de domínio; o passo é chamado pelo cron.
|
|
631
|
+
*/
|
|
632
|
+
export async function retryFailedWebhookEvents(db, source, nowIso, limit = 25, productKey = DEFAULT_PRODUCT_KEY) {
|
|
633
|
+
const cfg = asRegistry(source).resolve({ productKey });
|
|
634
|
+
const provider = providerFor(cfg);
|
|
635
|
+
const failed = await listFailedWebhookEvents(db, limit);
|
|
636
|
+
let recovered = 0;
|
|
637
|
+
for (const row of failed) {
|
|
638
|
+
if (row.provider !== provider.name)
|
|
639
|
+
continue;
|
|
640
|
+
const event = provider.parseWebhookEvent(row.payload);
|
|
641
|
+
if (!event) {
|
|
642
|
+
await markWebhookEvent(db, row.id, 'ignored', null, 'payload não parseável no reprocessamento');
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const result = await reconcileStoredWebhookEvent(db, provider, event, row.id, nowIso, productKey);
|
|
646
|
+
if (result.outcome === 'processed' || result.outcome === 'ignored')
|
|
647
|
+
recovered += 1;
|
|
648
|
+
}
|
|
649
|
+
return { retried: failed.length, recovered };
|
|
650
|
+
}
|
|
651
|
+
async function ownerEmailForTenant(tx, tenantId) {
|
|
652
|
+
const rows = await tx `
|
|
653
|
+
SELECT u.email FROM membership m
|
|
654
|
+
JOIN "user" u ON u.id = m.user_id
|
|
655
|
+
WHERE m.tenant_id = ${tenantId} AND m.deleted_at IS NULL AND m.status = 'active'
|
|
656
|
+
ORDER BY m.created_at ASC LIMIT 1
|
|
657
|
+
`;
|
|
658
|
+
return rows[0]?.email ?? null;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Executor de política temporal — idempotente, seguro para rodar a cada hora
|
|
662
|
+
* (Render Cron). Trial vencido → `past_due`; período vencido → `past_due`;
|
|
663
|
+
* `past_due` → régua de inadimplência (stepKey idempotente) + e-mail best-effort;
|
|
664
|
+
* atraso ≥ `suspendAfterDays` → tenant `suspended`.
|
|
665
|
+
*/
|
|
666
|
+
export async function runBillingCron(db, source, nowIso) {
|
|
667
|
+
const registry = asRegistry(source);
|
|
668
|
+
const today = todayOf(nowIso);
|
|
669
|
+
const report = {
|
|
670
|
+
examined: 0,
|
|
671
|
+
trialsExpired: 0,
|
|
672
|
+
periodsExpired: 0,
|
|
673
|
+
dunningStepsExecuted: 0,
|
|
674
|
+
tenantsSuspended: 0,
|
|
675
|
+
emailsSent: 0,
|
|
676
|
+
webhooksRetried: 0,
|
|
677
|
+
webhooksRecovered: 0,
|
|
678
|
+
};
|
|
679
|
+
// Rede de segurança: reprocessa webhooks que falharam após o dedup/200.
|
|
680
|
+
// Um passe por produto configurado — cada conta de gateway tem a sua fila.
|
|
681
|
+
for (const productKey of registry.productKeys()) {
|
|
682
|
+
const retry = await retryFailedWebhookEvents(db, registry, nowIso, 25, productKey);
|
|
683
|
+
report.webhooksRetried += retry.retried;
|
|
684
|
+
report.webhooksRecovered += retry.recovered;
|
|
685
|
+
}
|
|
686
|
+
const subs = await withOperatorContext(db, (tx) => listSubscriptionsForCron(tx, today));
|
|
687
|
+
report.examined = subs.length;
|
|
688
|
+
for (const sub of subs) {
|
|
689
|
+
// Isolamento de falha: um tenant quebrado não pode derrubar a régua dos demais.
|
|
690
|
+
try {
|
|
691
|
+
let dunningEmailTo = null;
|
|
692
|
+
// Política de suspensão, remetente e base pública são DO PRODUTO desta
|
|
693
|
+
// assinatura — nunca de uma configuração global compartilhada.
|
|
694
|
+
const cfg = registry.resolve({ productKey: sub.productKey, tenantId: sub.tenantId });
|
|
695
|
+
await withContext(db, { tenantId: sub.tenantId }, async (tx) => {
|
|
696
|
+
await lockTenantBilling(tx, sub.tenantId);
|
|
697
|
+
const loaded = await getSubscription(tx, sub.productKey);
|
|
698
|
+
if (!loaded)
|
|
699
|
+
return; // sumiu entre o scan e o lock (ex.: tenant removido)
|
|
700
|
+
let current = loaded;
|
|
701
|
+
// 1. Trial vencido ou período vencido → past_due (evento canônico).
|
|
702
|
+
const trialExpired = current.status === 'trialing' &&
|
|
703
|
+
current.trialEndsAt != null &&
|
|
704
|
+
current.trialEndsAt < today;
|
|
705
|
+
const periodExpired = current.status === 'active' &&
|
|
706
|
+
current.currentPeriodEnd != null &&
|
|
707
|
+
current.currentPeriodEnd < today;
|
|
708
|
+
if (trialExpired || periodExpired) {
|
|
709
|
+
const overdueEvent = {
|
|
710
|
+
externalEventId: `cron_${today}_${current.subscriptionId}_overdue`,
|
|
711
|
+
eventType: trialExpired ? 'cron.trial_expired' : 'cron.period_expired',
|
|
712
|
+
kind: 'invoice',
|
|
713
|
+
invoice: {
|
|
714
|
+
externalId: `cron_${today}_${current.subscriptionId}`,
|
|
715
|
+
status: 'overdue',
|
|
716
|
+
value: 0,
|
|
717
|
+
},
|
|
718
|
+
externalReference: `${current.tenantId}:${current.productKey}:${current.planKey}`,
|
|
719
|
+
};
|
|
720
|
+
// Trial não tem `current` past_due-ável no reconcile? Tem: overdue exige assinatura existente — ok.
|
|
721
|
+
const { subscription, domainEvents } = applySubscriptionEvent({
|
|
722
|
+
current,
|
|
723
|
+
tenantId: current.tenantId,
|
|
724
|
+
planKey: current.planKey,
|
|
725
|
+
event: overdueEvent,
|
|
726
|
+
now: nowIso,
|
|
727
|
+
});
|
|
728
|
+
if (subscription.status === 'past_due' && current.status !== 'past_due') {
|
|
729
|
+
if (trialExpired)
|
|
730
|
+
report.trialsExpired += 1;
|
|
731
|
+
else
|
|
732
|
+
report.periodsExpired += 1;
|
|
733
|
+
// Referência da régua: fim do trial ou do período.
|
|
734
|
+
const reference = current.trialEndsAt ?? current.currentPeriodEnd ?? today;
|
|
735
|
+
const next = {
|
|
736
|
+
...subscription,
|
|
737
|
+
currentPeriodEnd: subscription.currentPeriodEnd ?? reference,
|
|
738
|
+
};
|
|
739
|
+
const row = await upsertSubscription(tx, {
|
|
740
|
+
tenantId: current.tenantId,
|
|
741
|
+
planKey: current.planKey,
|
|
742
|
+
subscription: next,
|
|
743
|
+
});
|
|
744
|
+
await appendDomainEvents(tx, row.rowId, domainEvents);
|
|
745
|
+
await appendAudit(tx, {
|
|
746
|
+
actorType: 'automation',
|
|
747
|
+
action: trialExpired ? 'trial_expired' : 'period_expired',
|
|
748
|
+
resourceType: 'saas_subscription',
|
|
749
|
+
resourceId: row.rowId,
|
|
750
|
+
});
|
|
751
|
+
current = (await getSubscription(tx));
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// 2. Régua de inadimplência (idempotente por stepKey + reference_date).
|
|
755
|
+
if (current.status === 'past_due') {
|
|
756
|
+
const reference = current.currentPeriodEnd ?? current.trialEndsAt ?? today;
|
|
757
|
+
const executed = await listExecutedDunningSteps(tx, current.rowId, reference);
|
|
758
|
+
const events = dunningStepsAsEvents({ ...current, currentPeriodEnd: reference }, today, executed);
|
|
759
|
+
for (const ev of events) {
|
|
760
|
+
const stepKey = ev.data.dunningStep?.stepKey ??
|
|
761
|
+
`step_${ev.sequence}`;
|
|
762
|
+
await recordDunningStep(tx, {
|
|
763
|
+
tenantId: current.tenantId,
|
|
764
|
+
subscriptionRowId: current.rowId,
|
|
765
|
+
stepKey,
|
|
766
|
+
referenceDate: reference,
|
|
767
|
+
});
|
|
768
|
+
report.dunningStepsExecuted += 1;
|
|
769
|
+
}
|
|
770
|
+
if (events.length > 0) {
|
|
771
|
+
await appendDomainEvents(tx, current.rowId, events);
|
|
772
|
+
dunningEmailTo = await ownerEmailForTenant(tx, current.tenantId);
|
|
773
|
+
await upsertSubscription(tx, {
|
|
774
|
+
tenantId: current.tenantId,
|
|
775
|
+
planKey: current.planKey,
|
|
776
|
+
subscription: { ...current, sequence: current.sequence + events.length },
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
// 3. Suspensão automática após o grace period.
|
|
780
|
+
const overdueDays = daysBetween(reference, today);
|
|
781
|
+
if (overdueDays >= cfg.suspendAfterDays) {
|
|
782
|
+
const updated = await tx `
|
|
783
|
+
UPDATE tenant SET status = 'suspended', updated_at = now()
|
|
784
|
+
WHERE id = ${current.tenantId} AND status <> 'suspended'
|
|
785
|
+
`;
|
|
786
|
+
if (updated.count > 0) {
|
|
787
|
+
report.tenantsSuspended += 1;
|
|
788
|
+
await appendAudit(tx, {
|
|
789
|
+
actorType: 'automation',
|
|
790
|
+
action: 'tenant_suspended_overdue',
|
|
791
|
+
resourceType: 'tenant',
|
|
792
|
+
resourceId: current.tenantId,
|
|
793
|
+
metadata: { overdueDays },
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
// Envio FORA da transação (best-effort com timeout): rede nunca segura o banco.
|
|
800
|
+
if (dunningEmailTo) {
|
|
801
|
+
const sent = await sendEmail(cfg.resendApiKey, {
|
|
802
|
+
from: cfg.emailFrom,
|
|
803
|
+
to: dunningEmailTo,
|
|
804
|
+
subject: 'Sua assinatura Baruk está com pagamento pendente',
|
|
805
|
+
html: `<p>Olá,</p><p>Identificamos uma pendência na assinatura da sua conta Baruk.</p><p>Regularize para manter seus serviços ativos: <a href="${cfg.webBaseUrl}/assinatura">gerenciar assinatura</a>.</p><p>Se o pagamento já foi feito, desconsidere este aviso.</p>`,
|
|
806
|
+
});
|
|
807
|
+
if (sent)
|
|
808
|
+
report.emailsSent += 1;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
// Registrado e seguimos: a assinatura problemática fica para a próxima execução.
|
|
813
|
+
console.error(`billing-cron: falha ao processar tenant ${sub.tenantId}:`, err instanceof Error ? err.message : err);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return report;
|
|
817
|
+
}
|
|
818
|
+
//# sourceMappingURL=service.js.map
|