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