@aglyn/tenant-data-admin 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/jest.integration.config.d.ts +39 -0
- package/jest.integration.setup.d.ts +17 -0
- package/package.json +51 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/lib/render-cache.d.ts +184 -0
- package/src/lib/render-cache.js +198 -0
- package/src/lib/render-cache.js.map +1 -0
- package/src/lib/server/account-addresses.d.ts +160 -0
- package/src/lib/server/account-addresses.js +311 -0
- package/src/lib/server/account-addresses.js.map +1 -0
- package/src/lib/server/account-emails.d.ts +219 -0
- package/src/lib/server/account-emails.js +640 -0
- package/src/lib/server/account-emails.js.map +1 -0
- package/src/lib/server/admin-audit.d.ts +180 -0
- package/src/lib/server/admin-audit.js +233 -0
- package/src/lib/server/admin-audit.js.map +1 -0
- package/src/lib/server/analytics-retention.d.ts +63 -0
- package/src/lib/server/analytics-retention.js +66 -0
- package/src/lib/server/analytics-retention.js.map +1 -0
- package/src/lib/server/api-http.d.ts +159 -0
- package/src/lib/server/api-http.js +230 -0
- package/src/lib/server/api-http.js.map +1 -0
- package/src/lib/server/api-keys.d.ts +117 -0
- package/src/lib/server/api-keys.js +298 -0
- package/src/lib/server/api-keys.js.map +1 -0
- package/src/lib/server/auth-handoff.d.ts +165 -0
- package/src/lib/server/auth-handoff.js +482 -0
- package/src/lib/server/auth-handoff.js.map +1 -0
- package/src/lib/server/auth-pools.d.ts +246 -0
- package/src/lib/server/auth-pools.js +551 -0
- package/src/lib/server/auth-pools.js.map +1 -0
- package/src/lib/server/billing-addons.d.ts +171 -0
- package/src/lib/server/billing-addons.js +277 -0
- package/src/lib/server/billing-addons.js.map +1 -0
- package/src/lib/server/campaign-attribution-store.d.ts +55 -0
- package/src/lib/server/campaign-attribution-store.js +95 -0
- package/src/lib/server/campaign-attribution-store.js.map +1 -0
- package/src/lib/server/campaign-conversion-attribution.d.ts +205 -0
- package/src/lib/server/campaign-conversion-attribution.js +279 -0
- package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
- package/src/lib/server/client-error-report.d.ts +301 -0
- package/src/lib/server/client-error-report.js +755 -0
- package/src/lib/server/client-error-report.js.map +1 -0
- package/src/lib/server/connect-account-status.d.ts +74 -0
- package/src/lib/server/connect-account-status.js +94 -0
- package/src/lib/server/connect-account-status.js.map +1 -0
- package/src/lib/server/connect-payout-failure.d.ts +103 -0
- package/src/lib/server/connect-payout-failure.js +127 -0
- package/src/lib/server/connect-payout-failure.js.map +1 -0
- package/src/lib/server/console-api-rate-limit.d.ts +81 -0
- package/src/lib/server/console-api-rate-limit.js +111 -0
- package/src/lib/server/console-api-rate-limit.js.map +1 -0
- package/src/lib/server/console-domains.d.ts +326 -0
- package/src/lib/server/console-domains.js +779 -0
- package/src/lib/server/console-domains.js.map +1 -0
- package/src/lib/server/consume-once.d.ts +91 -0
- package/src/lib/server/consume-once.js +96 -0
- package/src/lib/server/consume-once.js.map +1 -0
- package/src/lib/server/contact-company-link.d.ts +85 -0
- package/src/lib/server/contact-company-link.js +105 -0
- package/src/lib/server/contact-company-link.js.map +1 -0
- package/src/lib/server/contact-email-engagement.d.ts +32 -0
- package/src/lib/server/contact-email-engagement.js +145 -0
- package/src/lib/server/contact-email-engagement.js.map +1 -0
- package/src/lib/server/contact-email-index.d.ts +56 -0
- package/src/lib/server/contact-email-index.js +158 -0
- package/src/lib/server/contact-email-index.js.map +1 -0
- package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
- package/src/lib/server/contact-lifecycle-floor.js +88 -0
- package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
- package/src/lib/server/contact-merge.d.ts +55 -0
- package/src/lib/server/contact-merge.js +279 -0
- package/src/lib/server/contact-merge.js.map +1 -0
- package/src/lib/server/contact-suppression.d.ts +153 -0
- package/src/lib/server/contact-suppression.js +259 -0
- package/src/lib/server/contact-suppression.js.map +1 -0
- package/src/lib/server/crm-booking-activity.d.ts +86 -0
- package/src/lib/server/crm-booking-activity.js +251 -0
- package/src/lib/server/crm-booking-activity.js.map +1 -0
- package/src/lib/server/crm-email-activity.d.ts +112 -0
- package/src/lib/server/crm-email-activity.js +143 -0
- package/src/lib/server/crm-email-activity.js.map +1 -0
- package/src/lib/server/crm-inbound-email.d.ts +145 -0
- package/src/lib/server/crm-inbound-email.js +287 -0
- package/src/lib/server/crm-inbound-email.js.map +1 -0
- package/src/lib/server/crm-next-activity.d.ts +74 -0
- package/src/lib/server/crm-next-activity.js +194 -0
- package/src/lib/server/crm-next-activity.js.map +1 -0
- package/src/lib/server/crm-records.d.ts +144 -0
- package/src/lib/server/crm-records.js +218 -0
- package/src/lib/server/crm-records.js.map +1 -0
- package/src/lib/server/csp-aggregate.d.ts +62 -0
- package/src/lib/server/csp-aggregate.js +276 -0
- package/src/lib/server/csp-aggregate.js.map +1 -0
- package/src/lib/server/data-storage-gate.d.ts +66 -0
- package/src/lib/server/data-storage-gate.js +48 -0
- package/src/lib/server/data-storage-gate.js.map +1 -0
- package/src/lib/server/dataset-live-pages.d.ts +179 -0
- package/src/lib/server/dataset-live-pages.js +421 -0
- package/src/lib/server/dataset-live-pages.js.map +1 -0
- package/src/lib/server/dns-probe.d.ts +53 -0
- package/src/lib/server/dns-probe.js +184 -0
- package/src/lib/server/dns-probe.js.map +1 -0
- package/src/lib/server/document-id.d.ts +29 -0
- package/src/lib/server/document-id.js +100 -0
- package/src/lib/server/document-id.js.map +1 -0
- package/src/lib/server/domain-provider-vercel.d.ts +33 -0
- package/src/lib/server/domain-provider-vercel.js +298 -0
- package/src/lib/server/domain-provider-vercel.js.map +1 -0
- package/src/lib/server/domain-provider-webhook.d.ts +73 -0
- package/src/lib/server/domain-provider-webhook.js +223 -0
- package/src/lib/server/domain-provider-webhook.js.map +1 -0
- package/src/lib/server/domain-provider-wildcard.d.ts +54 -0
- package/src/lib/server/domain-provider-wildcard.js +163 -0
- package/src/lib/server/domain-provider-wildcard.js.map +1 -0
- package/src/lib/server/domain-provider.d.ts +197 -0
- package/src/lib/server/domain-provider.js +221 -0
- package/src/lib/server/domain-provider.js.map +1 -0
- package/src/lib/server/duplicate-activity.d.ts +54 -0
- package/src/lib/server/duplicate-activity.js +67 -0
- package/src/lib/server/duplicate-activity.js.map +1 -0
- package/src/lib/server/duplicate-resource.d.ts +55 -0
- package/src/lib/server/duplicate-resource.js +474 -0
- package/src/lib/server/duplicate-resource.js.map +1 -0
- package/src/lib/server/dynamic-list-materialize.d.ts +153 -0
- package/src/lib/server/dynamic-list-materialize.js +581 -0
- package/src/lib/server/dynamic-list-materialize.js.map +1 -0
- package/src/lib/server/edit-access-authz.d.ts +56 -0
- package/src/lib/server/edit-access-authz.js +107 -0
- package/src/lib/server/edit-access-authz.js.map +1 -0
- package/src/lib/server/edit-access-token.d.ts +72 -0
- package/src/lib/server/edit-access-token.js +116 -0
- package/src/lib/server/edit-access-token.js.map +1 -0
- package/src/lib/server/edit-hint-token.d.ts +83 -0
- package/src/lib/server/edit-hint-token.js +119 -0
- package/src/lib/server/edit-hint-token.js.map +1 -0
- package/src/lib/server/email-delivery-log.d.ts +509 -0
- package/src/lib/server/email-delivery-log.js +1043 -0
- package/src/lib/server/email-delivery-log.js.map +1 -0
- package/src/lib/server/email-flow-gate.d.ts +80 -0
- package/src/lib/server/email-flow-gate.js +153 -0
- package/src/lib/server/email-flow-gate.js.map +1 -0
- package/src/lib/server/email-marketing-gate.d.ts +333 -0
- package/src/lib/server/email-marketing-gate.js +639 -0
- package/src/lib/server/email-marketing-gate.js.map +1 -0
- package/src/lib/server/email-metering.d.ts +293 -0
- package/src/lib/server/email-metering.js +359 -0
- package/src/lib/server/email-metering.js.map +1 -0
- package/src/lib/server/email-revenue-attribution.d.ts +185 -0
- package/src/lib/server/email-revenue-attribution.js +300 -0
- package/src/lib/server/email-revenue-attribution.js.map +1 -0
- package/src/lib/server/email-send-rate.d.ts +232 -0
- package/src/lib/server/email-send-rate.js +417 -0
- package/src/lib/server/email-send-rate.js.map +1 -0
- package/src/lib/server/email-sender-reputation.d.ts +317 -0
- package/src/lib/server/email-sender-reputation.js +464 -0
- package/src/lib/server/email-sender-reputation.js.map +1 -0
- package/src/lib/server/email-suppression.d.ts +490 -0
- package/src/lib/server/email-suppression.js +648 -0
- package/src/lib/server/email-suppression.js.map +1 -0
- package/src/lib/server/email-topic-confirmation.d.ts +150 -0
- package/src/lib/server/email-topic-confirmation.js +272 -0
- package/src/lib/server/email-topic-confirmation.js.map +1 -0
- package/src/lib/server/email-unsubscribe-link.d.ts +174 -0
- package/src/lib/server/email-unsubscribe-link.js +236 -0
- package/src/lib/server/email-unsubscribe-link.js.map +1 -0
- package/src/lib/server/erase-person.d.ts +116 -0
- package/src/lib/server/erase-person.js +272 -0
- package/src/lib/server/erase-person.js.map +1 -0
- package/src/lib/server/erase.d.ts +458 -0
- package/src/lib/server/erase.js +1506 -0
- package/src/lib/server/erase.js.map +1 -0
- package/src/lib/server/firebase-admin.d.ts +71 -0
- package/src/lib/server/firebase-admin.js +343 -0
- package/src/lib/server/firebase-admin.js.map +1 -0
- package/src/lib/server/form-dataset-binding-token.d.ts +35 -0
- package/src/lib/server/form-dataset-binding-token.js +107 -0
- package/src/lib/server/form-dataset-binding-token.js.map +1 -0
- package/src/lib/server/free-workspace-cap.d.ts +201 -0
- package/src/lib/server/free-workspace-cap.js +404 -0
- package/src/lib/server/free-workspace-cap.js.map +1 -0
- package/src/lib/server/ga4-measurement-protocol.d.ts +256 -0
- package/src/lib/server/ga4-measurement-protocol.js +558 -0
- package/src/lib/server/ga4-measurement-protocol.js.map +1 -0
- package/src/lib/server/host-email-tokens.d.ts +47 -0
- package/src/lib/server/host-email-tokens.js +72 -0
- package/src/lib/server/host-email-tokens.js.map +1 -0
- package/src/lib/server/host-memberships.d.ts +54 -0
- package/src/lib/server/host-memberships.js +194 -0
- package/src/lib/server/host-memberships.js.map +1 -0
- package/src/lib/server/host-sending-domain.d.ts +292 -0
- package/src/lib/server/host-sending-domain.js +451 -0
- package/src/lib/server/host-sending-domain.js.map +1 -0
- package/src/lib/server/host-visitor-records.d.ts +150 -0
- package/src/lib/server/host-visitor-records.js +262 -0
- package/src/lib/server/host-visitor-records.js.map +1 -0
- package/src/lib/server/id-token-refusal.d.ts +28 -0
- package/src/lib/server/id-token-refusal.js +111 -0
- package/src/lib/server/id-token-refusal.js.map +1 -0
- package/src/lib/server/legal-acceptance.d.ts +214 -0
- package/src/lib/server/legal-acceptance.js +305 -0
- package/src/lib/server/legal-acceptance.js.map +1 -0
- package/src/lib/server/list-members.d.ts +211 -0
- package/src/lib/server/list-members.js +176 -0
- package/src/lib/server/list-members.js.map +1 -0
- package/src/lib/server/live-page-usage.d.ts +202 -0
- package/src/lib/server/live-page-usage.js +300 -0
- package/src/lib/server/live-page-usage.js.map +1 -0
- package/src/lib/server/lockdown.d.ts +313 -0
- package/src/lib/server/lockdown.js +643 -0
- package/src/lib/server/lockdown.js.map +1 -0
- package/src/lib/server/media-cdn-rate-limit.d.ts +188 -0
- package/src/lib/server/media-cdn-rate-limit.js +204 -0
- package/src/lib/server/media-cdn-rate-limit.js.map +1 -0
- package/src/lib/server/media-delivery.d.ts +267 -0
- package/src/lib/server/media-delivery.js +485 -0
- package/src/lib/server/media-delivery.js.map +1 -0
- package/src/lib/server/media-download-tokens.d.ts +161 -0
- package/src/lib/server/media-download-tokens.js +386 -0
- package/src/lib/server/media-download-tokens.js.map +1 -0
- package/src/lib/server/media-quarantine.d.ts +127 -0
- package/src/lib/server/media-quarantine.js +170 -0
- package/src/lib/server/media-quarantine.js.map +1 -0
- package/src/lib/server/media-signing.d.ts +126 -0
- package/src/lib/server/media-signing.js +159 -0
- package/src/lib/server/media-signing.js.map +1 -0
- package/src/lib/server/media-storage-path.d.ts +96 -0
- package/src/lib/server/media-storage-path.js +111 -0
- package/src/lib/server/media-storage-path.js.map +1 -0
- package/src/lib/server/media-strong-digest.d.ts +85 -0
- package/src/lib/server/media-strong-digest.js +190 -0
- package/src/lib/server/media-strong-digest.js.map +1 -0
- package/src/lib/server/media-tombstone.d.ts +276 -0
- package/src/lib/server/media-tombstone.js +421 -0
- package/src/lib/server/media-tombstone.js.map +1 -0
- package/src/lib/server/media-variants.d.ts +309 -0
- package/src/lib/server/media-variants.js +428 -0
- package/src/lib/server/media-variants.js.map +1 -0
- package/src/lib/server/member-email-aliases.d.ts +180 -0
- package/src/lib/server/member-email-aliases.js +420 -0
- package/src/lib/server/member-email-aliases.js.map +1 -0
- package/src/lib/server/member-photo.d.ts +88 -0
- package/src/lib/server/member-photo.js +185 -0
- package/src/lib/server/member-photo.js.map +1 -0
- package/src/lib/server/membership-recover-throttle.d.ts +211 -0
- package/src/lib/server/membership-recover-throttle.js +231 -0
- package/src/lib/server/membership-recover-throttle.js.map +1 -0
- package/src/lib/server/notifications.d.ts +55 -0
- package/src/lib/server/notifications.js +134 -0
- package/src/lib/server/notifications.js.map +1 -0
- package/src/lib/server/org-billing.d.ts +97 -0
- package/src/lib/server/org-billing.js +236 -0
- package/src/lib/server/org-billing.js.map +1 -0
- package/src/lib/server/organizations.d.ts +837 -0
- package/src/lib/server/organizations.js +1814 -0
- package/src/lib/server/organizations.js.map +1 -0
- package/src/lib/server/password-reset-throttle.d.ts +84 -0
- package/src/lib/server/password-reset-throttle.js +102 -0
- package/src/lib/server/password-reset-throttle.js.map +1 -0
- package/src/lib/server/personal-data-export.d.ts +241 -0
- package/src/lib/server/personal-data-export.js +858 -0
- package/src/lib/server/personal-data-export.js.map +1 -0
- package/src/lib/server/platform-domain-names.d.ts +62 -0
- package/src/lib/server/platform-domain-names.js +197 -0
- package/src/lib/server/platform-domain-names.js.map +1 -0
- package/src/lib/server/platform-marketing-consent.d.ts +133 -0
- package/src/lib/server/platform-marketing-consent.js +209 -0
- package/src/lib/server/platform-marketing-consent.js.map +1 -0
- package/src/lib/server/rate-limit-store.d.ts +702 -0
- package/src/lib/server/rate-limit-store.js +1220 -0
- package/src/lib/server/rate-limit-store.js.map +1 -0
- package/src/lib/server/realm-plugins.d.ts +67 -0
- package/src/lib/server/realm-plugins.js +250 -0
- package/src/lib/server/realm-plugins.js.map +1 -0
- package/src/lib/server/recaptcha-allowlist.d.ts +90 -0
- package/src/lib/server/recaptcha-allowlist.js +409 -0
- package/src/lib/server/recaptcha-allowlist.js.map +1 -0
- package/src/lib/server/release-flags.d.ts +105 -0
- package/src/lib/server/release-flags.js +183 -0
- package/src/lib/server/release-flags.js.map +1 -0
- package/src/lib/server/replay-erasures.d.ts +126 -0
- package/src/lib/server/replay-erasures.js +217 -0
- package/src/lib/server/replay-erasures.js.map +1 -0
- package/src/lib/server/resolve-people.d.ts +66 -0
- package/src/lib/server/resolve-people.js +112 -0
- package/src/lib/server/resolve-people.js.map +1 -0
- package/src/lib/server/safe-equal.d.ts +45 -0
- package/src/lib/server/safe-equal.js +58 -0
- package/src/lib/server/safe-equal.js.map +1 -0
- package/src/lib/server/sending-domain-debt.d.ts +127 -0
- package/src/lib/server/sending-domain-debt.js +268 -0
- package/src/lib/server/sending-domain-debt.js.map +1 -0
- package/src/lib/server/sending-domain-recheck.d.ts +151 -0
- package/src/lib/server/sending-domain-recheck.js +253 -0
- package/src/lib/server/sending-domain-recheck.js.map +1 -0
- package/src/lib/server/sending-domains.d.ts +364 -0
- package/src/lib/server/sending-domains.js +683 -0
- package/src/lib/server/sending-domains.js.map +1 -0
- package/src/lib/server/serve-media-cdn.d.ts +433 -0
- package/src/lib/server/serve-media-cdn.js +1370 -0
- package/src/lib/server/serve-media-cdn.js.map +1 -0
- package/src/lib/server/serve-plugin-fetch.d.ts +61 -0
- package/src/lib/server/serve-plugin-fetch.js +294 -0
- package/src/lib/server/serve-plugin-fetch.js.map +1 -0
- package/src/lib/server/sms-keywords.d.ts +63 -0
- package/src/lib/server/sms-keywords.js +146 -0
- package/src/lib/server/sms-keywords.js.map +1 -0
- package/src/lib/server/sso-break-glass-owners.d.ts +91 -0
- package/src/lib/server/sso-break-glass-owners.js +227 -0
- package/src/lib/server/sso-break-glass-owners.js.map +1 -0
- package/src/lib/server/sso-domain-policy.d.ts +140 -0
- package/src/lib/server/sso-domain-policy.js +239 -0
- package/src/lib/server/sso-domain-policy.js.map +1 -0
- package/src/lib/server/sso-drift-logic.d.ts +106 -0
- package/src/lib/server/sso-drift-logic.js +95 -0
- package/src/lib/server/sso-drift-logic.js.map +1 -0
- package/src/lib/server/sso-enforcement.d.ts +204 -0
- package/src/lib/server/sso-enforcement.js +309 -0
- package/src/lib/server/sso-enforcement.js.map +1 -0
- package/src/lib/server/sso-provisioning.d.ts +320 -0
- package/src/lib/server/sso-provisioning.js +715 -0
- package/src/lib/server/sso-provisioning.js.map +1 -0
- package/src/lib/server/staff-alert-email.d.ts +41 -0
- package/src/lib/server/staff-alert-email.js +63 -0
- package/src/lib/server/staff-alert-email.js.map +1 -0
- package/src/lib/server/stripe-account-mode.d.ts +156 -0
- package/src/lib/server/stripe-account-mode.js +162 -0
- package/src/lib/server/stripe-account-mode.js.map +1 -0
- package/src/lib/server/tenant-write-lockdown.d.ts +131 -0
- package/src/lib/server/tenant-write-lockdown.js +176 -0
- package/src/lib/server/tenant-write-lockdown.js.map +1 -0
- package/src/lib/server/test-firestore.d.ts +45 -0
- package/src/lib/server/test-firestore.js +124 -0
- package/src/lib/server/test-firestore.js.map +1 -0
- package/src/lib/server/token-revocation.d.ts +140 -0
- package/src/lib/server/token-revocation.js +255 -0
- package/src/lib/server/token-revocation.js.map +1 -0
- package/src/lib/server/update-existing.d.ts +32 -0
- package/src/lib/server/update-existing.js +73 -0
- package/src/lib/server/update-existing.js.map +1 -0
- package/src/lib/server/upload-cors-reconcile.d.ts +118 -0
- package/src/lib/server/upload-cors-reconcile.js +234 -0
- package/src/lib/server/upload-cors-reconcile.js.map +1 -0
- package/src/lib/server/upsert-contact.d.ts +262 -0
- package/src/lib/server/upsert-contact.js +546 -0
- package/src/lib/server/upsert-contact.js.map +1 -0
- package/src/lib/server/usage-invoice.d.ts +203 -0
- package/src/lib/server/usage-invoice.js +397 -0
- package/src/lib/server/usage-invoice.js.map +1 -0
- package/src/lib/server/user-profiles.d.ts +151 -0
- package/src/lib/server/user-profiles.js +364 -0
- package/src/lib/server/user-profiles.js.map +1 -0
- package/src/lib/server/vercel-drain-signature.d.ts +30 -0
- package/src/lib/server/vercel-drain-signature.js +61 -0
- package/src/lib/server/vercel-drain-signature.js.map +1 -0
- package/src/lib/server/vercel-log-drain.d.ts +259 -0
- package/src/lib/server/vercel-log-drain.js +479 -0
- package/src/lib/server/vercel-log-drain.js.map +1 -0
- package/src/lib/server/verify-email-cooldown.d.ts +102 -0
- package/src/lib/server/verify-email-cooldown.js +114 -0
- package/src/lib/server/verify-email-cooldown.js.map +1 -0
- package/src/lib/server/visitor-write-rate-limit.d.ts +87 -0
- package/src/lib/server/visitor-write-rate-limit.js +117 -0
- package/src/lib/server/visitor-write-rate-limit.js.map +1 -0
- package/src/lib/server/workspace-domains.d.ts +156 -0
- package/src/lib/server/workspace-domains.js +271 -0
- package/src/lib/server/workspace-domains.js.map +1 -0
- package/src/tenancy.d.ts +115 -0
- package/src/tenancy.js +142 -0
- package/src/tenancy.js.map +1 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { resolveOrgEntitlements, type OrgPlan, type OrgSeatAddons } from '@aglyn/aglyn/server';
|
|
18
|
+
/**
|
|
19
|
+
* Self-serve add-on price map (AGL-525): the bridge between
|
|
20
|
+
* `STRIPE_PRICE_*` env price ids, `org.seatAddons` keys, and
|
|
21
|
+
* `PLAN_PRICING` USD amounts. Add-ons bill as extra items on the org's
|
|
22
|
+
* one subscription, so every kind has a monthly and a yearly price
|
|
23
|
+
* (Stripe allows a single interval per subscription; annual orgs attach
|
|
24
|
+
* the `_YEARLY` variants). Seat/member/dataset/host prices and the Aglyn AI
|
|
25
|
+
* add-on (AGL-2896) are per-plan; POS registers and the Event Calendar are
|
|
26
|
+
* flat. Prices are minted by `tools/scripts/setup-stripe.mjs` — keep the
|
|
27
|
+
* three in sync.
|
|
28
|
+
*/
|
|
29
|
+
export type AddonKind = keyof Required<OrgSeatAddons>;
|
|
30
|
+
export declare const ADDON_KINDS: readonly AddonKind[];
|
|
31
|
+
export type BillingInterval = 'month' | 'year';
|
|
32
|
+
/**
|
|
33
|
+
* Every plan that is sold with a Stripe price — DERIVED, never hand-listed
|
|
34
|
+
* (AGL-1340).
|
|
35
|
+
*
|
|
36
|
+
* The literal that used to sit here listed starter, pro, business and
|
|
37
|
+
* advanced only — it predated the Scale and Agency tiers, so
|
|
38
|
+
* `addonKindFromPriceId()` could not recognise a single Scale/Agency add-on
|
|
39
|
+
* price coming back from Stripe. `addonPriceId()` still SOLD them (it only
|
|
40
|
+
* uppercases the plan), so the money moved and the read-back returned
|
|
41
|
+
* nothing: the webhook wrote `seatAddons: {all zeros}` for the
|
|
42
|
+
* highest-paying orgs on the next subscription event, and `seatAddons` is an
|
|
43
|
+
* ENTITLEMENT INPUT (`plan-entitlements.ts` — it raises `hostLimit` and
|
|
44
|
+
* `posRegisters`, and flips the `eventCalendar` feature).
|
|
45
|
+
*
|
|
46
|
+
* Deriving from `SELF_SERVE_PLANS` is what stops the next tier from
|
|
47
|
+
* reintroducing it: `free` has nothing to sell and `enterprise` is quoted
|
|
48
|
+
* per deal (no `STRIPE_PRICE_ENTERPRISE_*` exists), so everything else is
|
|
49
|
+
* by definition a paid, priced plan.
|
|
50
|
+
*/
|
|
51
|
+
export declare const PAID_PLANS: readonly OrgPlan[];
|
|
52
|
+
/**
|
|
53
|
+
* The Stripe price id for an add-on kind on a plan at a billing interval;
|
|
54
|
+
* null when the plan doesn't sell it (free) or the env isn't configured.
|
|
55
|
+
*/
|
|
56
|
+
export declare function addonPriceId(kind: AddonKind, plan: OrgPlan, interval: BillingInterval): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Reverse map: which add-on kind a subscription item's price id sells.
|
|
59
|
+
* Null for non-add-on prices (the base plan item, unknown prices).
|
|
60
|
+
*/
|
|
61
|
+
export declare function addonKindFromPriceId(priceId: string | null | undefined): AddonKind | null;
|
|
62
|
+
/**
|
|
63
|
+
* The BASE plan's Stripe price id at an interval
|
|
64
|
+
* (`STRIPE_PRICE_{PLAN}[_YEARLY]`); null for `free`, for the custom-priced
|
|
65
|
+
* `enterprise`, and for an env that isn't configured. Read at call time, so
|
|
66
|
+
* it never freezes a stale value at module load the way the routes'
|
|
67
|
+
* `PRICE_ENV` maps do.
|
|
68
|
+
*/
|
|
69
|
+
export declare function planPriceId(plan: OrgPlan, interval: BillingInterval): string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Which plan a subscription item's price sells AND on which interval; null for
|
|
72
|
+
* an add-on price, the metered price, an ad-hoc enterprise price, and anything
|
|
73
|
+
* unknown.
|
|
74
|
+
*
|
|
75
|
+
* The interval half exists because a schedule phase read back from Stripe
|
|
76
|
+
* carries price IDS and nothing else (AGL-2150): rebuilding that phase's item
|
|
77
|
+
* list needs to know which interval it is priced on, and guessing it from the
|
|
78
|
+
* live subscription is wrong exactly when the pending change is also an
|
|
79
|
+
* interval change.
|
|
80
|
+
*/
|
|
81
|
+
export declare function planAndIntervalFromPriceId(priceId: string | null | undefined): {
|
|
82
|
+
plan: OrgPlan;
|
|
83
|
+
interval: BillingInterval;
|
|
84
|
+
} | null;
|
|
85
|
+
/**
|
|
86
|
+
* Which plan a subscription item's price sells; null for an add-on price,
|
|
87
|
+
* the metered price, an ad-hoc enterprise price, and anything unknown.
|
|
88
|
+
*/
|
|
89
|
+
export declare function planFromPriceId(priceId: string | null | undefined): OrgPlan | null;
|
|
90
|
+
/**
|
|
91
|
+
* The shared metered-usage price (AGL-635) that makes reported usage
|
|
92
|
+
* billable, for a given billing interval.
|
|
93
|
+
*
|
|
94
|
+
* Stripe rejects a subscription whose items mix `recurring.interval`, so the
|
|
95
|
+
* metered item has to recur the same way the plan does. There are two prices
|
|
96
|
+
* — `aglyn_metered_usage` (monthly) and `aglyn_metered_usage_yearly` — and
|
|
97
|
+
* both bill $0.01 per aggregated unit against the SAME meter, because the
|
|
98
|
+
* rollup posts an already-computed value in CENTS. The interval is therefore
|
|
99
|
+
* the only thing that differs, and it is the only thing a caller must get
|
|
100
|
+
* right.
|
|
101
|
+
*
|
|
102
|
+
* `interval` is REQUIRED on purpose (AGL-1280). It used to be monthly-only
|
|
103
|
+
* with no argument, so the failure mode of forgetting it was attaching a
|
|
104
|
+
* monthly price to a yearly subscription — the exact mixed-interval crash
|
|
105
|
+
* this signature now makes unrepresentable. A default would hand that
|
|
106
|
+
* footgun straight back.
|
|
107
|
+
*/
|
|
108
|
+
export declare function meteredPriceId(interval: 'month' | 'year'): string | null;
|
|
109
|
+
/**
|
|
110
|
+
* Whether a price id is EITHER shared metered-usage price.
|
|
111
|
+
*
|
|
112
|
+
* Interval-agnostic by design: callers ask this to exclude the metered item
|
|
113
|
+
* from plan/add-on reasoning, and an annual subscription's metered item must
|
|
114
|
+
* be excluded just as thoroughly as a monthly one.
|
|
115
|
+
*/
|
|
116
|
+
export declare function isMeteredPriceId(priceId: string | null | undefined): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* The subscription's BASE PLAN item, identified POSITIVELY (AGL-1340).
|
|
119
|
+
*
|
|
120
|
+
* This used to be "whatever no add-on price claims" — identification by
|
|
121
|
+
* elimination, which quietly returns the wrong item the moment a
|
|
122
|
+
* subscription carries anything the add-on map doesn't recognise: the
|
|
123
|
+
* metered item, a grandfathered v1 price, a coupon-era SKU, an item added
|
|
124
|
+
* by hand in the Stripe dashboard. Picking an add-on item as the plan item
|
|
125
|
+
* means a plan switch re-prices the ADD-ON, and reads the billing interval
|
|
126
|
+
* off it.
|
|
127
|
+
*
|
|
128
|
+
* So: match a known `STRIPE_PRICE_{PLAN}[_YEARLY]` first. Only when nothing
|
|
129
|
+
* matches — an enterprise deal bills on an ad-hoc price, and grandfathered
|
|
130
|
+
* subscriptions predate the current SKUs — fall back to the old rule,
|
|
131
|
+
* minus the metered item, and finally to `items[0]` so behaviour for those
|
|
132
|
+
* subscriptions is unchanged.
|
|
133
|
+
*/
|
|
134
|
+
export declare function findPlanItem<T extends {
|
|
135
|
+
price?: {
|
|
136
|
+
id?: string;
|
|
137
|
+
} | null;
|
|
138
|
+
}>(items: readonly T[] | null | undefined): T | null;
|
|
139
|
+
/**
|
|
140
|
+
* Monthly USD per unit for an add-on on a plan (yearly prices are ×12 of
|
|
141
|
+
* this); null when the plan doesn't sell the kind. Single pricing source
|
|
142
|
+
* of truth: `PLAN_PRICING` + the two flat add-on constants.
|
|
143
|
+
*/
|
|
144
|
+
export declare function addonUnitUsd(kind: AddonKind, plan: OrgPlan): number | null;
|
|
145
|
+
/**
|
|
146
|
+
* Full add-on quantity map (explicit zeros for absent kinds) from a
|
|
147
|
+
* subscription's items — the shape written to `org.seatAddons` so
|
|
148
|
+
* removals and dashboard edits converge on sync (AGL-527).
|
|
149
|
+
*/
|
|
150
|
+
export declare function addonQuantitiesFromItems(items: Array<{
|
|
151
|
+
price?: {
|
|
152
|
+
id?: string;
|
|
153
|
+
} | null;
|
|
154
|
+
quantity?: number | null;
|
|
155
|
+
}> | null | undefined): Record<AddonKind, number>;
|
|
156
|
+
/**
|
|
157
|
+
* Max purchasable quantity per kind, from a purchases-free entitlement
|
|
158
|
+
* resolution (plan defaults + staff overrides only) so the ceiling does not
|
|
159
|
+
* drift as the org buys: seat/dataset kinds stop at the plan's hard max,
|
|
160
|
+
* hosts/registers use flat ceilings, the Event Calendar and Aglyn AI are 0/1
|
|
161
|
+
* toggles. POS registers additionally require the `pos` feature.
|
|
162
|
+
*
|
|
163
|
+
* Lives here rather than inside `/api/billing/addons` because a PLAN CHANGE
|
|
164
|
+
* needs the same ceiling for a different plan. The addons route asks "how many
|
|
165
|
+
* may this org buy on the plan it is on"; `buildTargetItems` asks "how many of
|
|
166
|
+
* what it already bought can the plan it is moving to actually deliver". Two
|
|
167
|
+
* readers, one definition — a second copy is how they answer differently.
|
|
168
|
+
*/
|
|
169
|
+
export declare function addonMaxForBaseline(kind: AddonKind, baseline: ReturnType<typeof resolveOrgEntitlements>): number;
|
|
170
|
+
/** The ceiling a PLAN would impose on an add-on kind, purchases excluded. */
|
|
171
|
+
export declare function addonMaxForPlan(kind: AddonKind, plan: OrgPlan): number;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { EVENT_CALENDAR_ADDON_MONTHLY_USD, EXTRA_HOSTS_ADDON_MAX, PLAN_PRICING, POS_REGISTER_ADDON_MONTHLY_USD, POS_REGISTERS_ADDON_MAX, resolveOrgEntitlements, SELF_SERVE_PLANS, UNLIMITED } from "@aglyn/aglyn/server";
|
|
17
|
+
export const ADDON_KINDS = [
|
|
18
|
+
'managers',
|
|
19
|
+
'members',
|
|
20
|
+
'datasets',
|
|
21
|
+
'hosts',
|
|
22
|
+
'posRegisters',
|
|
23
|
+
'eventCalendar',
|
|
24
|
+
'aiAddon'
|
|
25
|
+
];
|
|
26
|
+
/** Kinds priced per plan (env `STRIPE_PRICE_{PLAN}_{SUFFIX}[_YEARLY]`). */ const PER_PLAN_ENV_SUFFIX = {
|
|
27
|
+
managers: 'EXTRA_SEAT',
|
|
28
|
+
members: 'EXTRA_MEMBER',
|
|
29
|
+
datasets: 'EXTRA_DATASET',
|
|
30
|
+
hosts: 'EXTRA_HOST',
|
|
31
|
+
aiAddon: 'AI_ADDON'
|
|
32
|
+
};
|
|
33
|
+
/** Kinds priced flat across plans (env `STRIPE_PRICE_{SUFFIX}[_YEARLY]`). */ const FLAT_ENV_NAME = {
|
|
34
|
+
posRegisters: 'POS_REGISTER',
|
|
35
|
+
eventCalendar: 'EVENT_CALENDAR'
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Every plan that is sold with a Stripe price — DERIVED, never hand-listed
|
|
39
|
+
* (AGL-1340).
|
|
40
|
+
*
|
|
41
|
+
* The literal that used to sit here listed starter, pro, business and
|
|
42
|
+
* advanced only — it predated the Scale and Agency tiers, so
|
|
43
|
+
* `addonKindFromPriceId()` could not recognise a single Scale/Agency add-on
|
|
44
|
+
* price coming back from Stripe. `addonPriceId()` still SOLD them (it only
|
|
45
|
+
* uppercases the plan), so the money moved and the read-back returned
|
|
46
|
+
* nothing: the webhook wrote `seatAddons: {all zeros}` for the
|
|
47
|
+
* highest-paying orgs on the next subscription event, and `seatAddons` is an
|
|
48
|
+
* ENTITLEMENT INPUT (`plan-entitlements.ts` — it raises `hostLimit` and
|
|
49
|
+
* `posRegisters`, and flips the `eventCalendar` feature).
|
|
50
|
+
*
|
|
51
|
+
* Deriving from `SELF_SERVE_PLANS` is what stops the next tier from
|
|
52
|
+
* reintroducing it: `free` has nothing to sell and `enterprise` is quoted
|
|
53
|
+
* per deal (no `STRIPE_PRICE_ENTERPRISE_*` exists), so everything else is
|
|
54
|
+
* by definition a paid, priced plan.
|
|
55
|
+
*/ export const PAID_PLANS = SELF_SERVE_PLANS.filter((plan)=>plan !== 'free');
|
|
56
|
+
function env(name) {
|
|
57
|
+
const value = process.env[name];
|
|
58
|
+
return value ? value : null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The Stripe price id for an add-on kind on a plan at a billing interval;
|
|
62
|
+
* null when the plan doesn't sell it (free) or the env isn't configured.
|
|
63
|
+
*/ export function addonPriceId(kind, plan, interval) {
|
|
64
|
+
const yearly = interval === 'year' ? '_YEARLY' : '';
|
|
65
|
+
const flat = FLAT_ENV_NAME[kind];
|
|
66
|
+
if (flat) return env(`STRIPE_PRICE_${flat}${yearly}`);
|
|
67
|
+
const suffix = PER_PLAN_ENV_SUFFIX[kind];
|
|
68
|
+
if (!suffix || plan === 'free') return null;
|
|
69
|
+
return env(`STRIPE_PRICE_${plan.toUpperCase()}_${suffix}${yearly}`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Reverse map: which add-on kind a subscription item's price id sells.
|
|
73
|
+
* Null for non-add-on prices (the base plan item, unknown prices).
|
|
74
|
+
*/ export function addonKindFromPriceId(priceId) {
|
|
75
|
+
if (!priceId) return null;
|
|
76
|
+
for (const kind of ADDON_KINDS){
|
|
77
|
+
for (const interval of [
|
|
78
|
+
'month',
|
|
79
|
+
'year'
|
|
80
|
+
]){
|
|
81
|
+
if (FLAT_ENV_NAME[kind]) {
|
|
82
|
+
if (addonPriceId(kind, 'starter', interval) === priceId) return kind;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (const plan of PAID_PLANS){
|
|
86
|
+
if (addonPriceId(kind, plan, interval) === priceId) return kind;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The BASE plan's Stripe price id at an interval
|
|
94
|
+
* (`STRIPE_PRICE_{PLAN}[_YEARLY]`); null for `free`, for the custom-priced
|
|
95
|
+
* `enterprise`, and for an env that isn't configured. Read at call time, so
|
|
96
|
+
* it never freezes a stale value at module load the way the routes'
|
|
97
|
+
* `PRICE_ENV` maps do.
|
|
98
|
+
*/ export function planPriceId(plan, interval) {
|
|
99
|
+
if (!PAID_PLANS.includes(plan)) return null;
|
|
100
|
+
const yearly = interval === 'year' ? '_YEARLY' : '';
|
|
101
|
+
return env(`STRIPE_PRICE_${plan.toUpperCase()}${yearly}`);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Which plan a subscription item's price sells AND on which interval; null for
|
|
105
|
+
* an add-on price, the metered price, an ad-hoc enterprise price, and anything
|
|
106
|
+
* unknown.
|
|
107
|
+
*
|
|
108
|
+
* The interval half exists because a schedule phase read back from Stripe
|
|
109
|
+
* carries price IDS and nothing else (AGL-2150): rebuilding that phase's item
|
|
110
|
+
* list needs to know which interval it is priced on, and guessing it from the
|
|
111
|
+
* live subscription is wrong exactly when the pending change is also an
|
|
112
|
+
* interval change.
|
|
113
|
+
*/ export function planAndIntervalFromPriceId(priceId) {
|
|
114
|
+
if (!priceId) return null;
|
|
115
|
+
for (const plan of PAID_PLANS){
|
|
116
|
+
for (const interval of [
|
|
117
|
+
'month',
|
|
118
|
+
'year'
|
|
119
|
+
]){
|
|
120
|
+
if (planPriceId(plan, interval) === priceId) return {
|
|
121
|
+
plan,
|
|
122
|
+
interval
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Which plan a subscription item's price sells; null for an add-on price,
|
|
130
|
+
* the metered price, an ad-hoc enterprise price, and anything unknown.
|
|
131
|
+
*/ export function planFromPriceId(priceId) {
|
|
132
|
+
var _ref;
|
|
133
|
+
var _planAndIntervalFromPriceId;
|
|
134
|
+
return (_ref = (_planAndIntervalFromPriceId = planAndIntervalFromPriceId(priceId)) == null ? void 0 : _planAndIntervalFromPriceId.plan) != null ? _ref : null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The shared metered-usage price (AGL-635) that makes reported usage
|
|
138
|
+
* billable, for a given billing interval.
|
|
139
|
+
*
|
|
140
|
+
* Stripe rejects a subscription whose items mix `recurring.interval`, so the
|
|
141
|
+
* metered item has to recur the same way the plan does. There are two prices
|
|
142
|
+
* — `aglyn_metered_usage` (monthly) and `aglyn_metered_usage_yearly` — and
|
|
143
|
+
* both bill $0.01 per aggregated unit against the SAME meter, because the
|
|
144
|
+
* rollup posts an already-computed value in CENTS. The interval is therefore
|
|
145
|
+
* the only thing that differs, and it is the only thing a caller must get
|
|
146
|
+
* right.
|
|
147
|
+
*
|
|
148
|
+
* `interval` is REQUIRED on purpose (AGL-1280). It used to be monthly-only
|
|
149
|
+
* with no argument, so the failure mode of forgetting it was attaching a
|
|
150
|
+
* monthly price to a yearly subscription — the exact mixed-interval crash
|
|
151
|
+
* this signature now makes unrepresentable. A default would hand that
|
|
152
|
+
* footgun straight back.
|
|
153
|
+
*/ export function meteredPriceId(interval) {
|
|
154
|
+
return env(interval === 'year' ? 'STRIPE_PRICE_METERED_YEARLY' : 'STRIPE_PRICE_METERED');
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Whether a price id is EITHER shared metered-usage price.
|
|
158
|
+
*
|
|
159
|
+
* Interval-agnostic by design: callers ask this to exclude the metered item
|
|
160
|
+
* from plan/add-on reasoning, and an annual subscription's metered item must
|
|
161
|
+
* be excluded just as thoroughly as a monthly one.
|
|
162
|
+
*/ export function isMeteredPriceId(priceId) {
|
|
163
|
+
if (!priceId) return false;
|
|
164
|
+
return priceId === meteredPriceId('month') || priceId === meteredPriceId('year');
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The subscription's BASE PLAN item, identified POSITIVELY (AGL-1340).
|
|
168
|
+
*
|
|
169
|
+
* This used to be "whatever no add-on price claims" — identification by
|
|
170
|
+
* elimination, which quietly returns the wrong item the moment a
|
|
171
|
+
* subscription carries anything the add-on map doesn't recognise: the
|
|
172
|
+
* metered item, a grandfathered v1 price, a coupon-era SKU, an item added
|
|
173
|
+
* by hand in the Stripe dashboard. Picking an add-on item as the plan item
|
|
174
|
+
* means a plan switch re-prices the ADD-ON, and reads the billing interval
|
|
175
|
+
* off it.
|
|
176
|
+
*
|
|
177
|
+
* So: match a known `STRIPE_PRICE_{PLAN}[_YEARLY]` first. Only when nothing
|
|
178
|
+
* matches — an enterprise deal bills on an ad-hoc price, and grandfathered
|
|
179
|
+
* subscriptions predate the current SKUs — fall back to the old rule,
|
|
180
|
+
* minus the metered item, and finally to `items[0]` so behaviour for those
|
|
181
|
+
* subscriptions is unchanged.
|
|
182
|
+
*/ export function findPlanItem(items) {
|
|
183
|
+
var _ref;
|
|
184
|
+
const list = items != null ? items : [];
|
|
185
|
+
const byPlanPrice = list.find((item)=>{
|
|
186
|
+
var _item_price;
|
|
187
|
+
return planFromPriceId(item == null ? void 0 : (_item_price = item.price) == null ? void 0 : _item_price.id);
|
|
188
|
+
});
|
|
189
|
+
if (byPlanPrice) return byPlanPrice;
|
|
190
|
+
const notAnAddon = list.find((item)=>{
|
|
191
|
+
var _item_price, _item_price1;
|
|
192
|
+
return !addonKindFromPriceId(item == null ? void 0 : (_item_price = item.price) == null ? void 0 : _item_price.id) && !isMeteredPriceId(item == null ? void 0 : (_item_price1 = item.price) == null ? void 0 : _item_price1.id);
|
|
193
|
+
});
|
|
194
|
+
return (_ref = notAnAddon != null ? notAnAddon : list[0]) != null ? _ref : null;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Monthly USD per unit for an add-on on a plan (yearly prices are ×12 of
|
|
198
|
+
* this); null when the plan doesn't sell the kind. Single pricing source
|
|
199
|
+
* of truth: `PLAN_PRICING` + the two flat add-on constants.
|
|
200
|
+
*/ export function addonUnitUsd(kind, plan) {
|
|
201
|
+
const pricing = PLAN_PRICING[plan];
|
|
202
|
+
switch(kind){
|
|
203
|
+
case 'managers':
|
|
204
|
+
return pricing.extraSeatMonthlyUsd;
|
|
205
|
+
case 'members':
|
|
206
|
+
return pricing.extraCollaboratorMonthlyUsd;
|
|
207
|
+
case 'datasets':
|
|
208
|
+
return pricing.extraDatasetMonthlyUsd;
|
|
209
|
+
case 'hosts':
|
|
210
|
+
return pricing.extraHostMonthlyUsd;
|
|
211
|
+
case 'posRegisters':
|
|
212
|
+
return POS_REGISTER_ADDON_MONTHLY_USD;
|
|
213
|
+
case 'eventCalendar':
|
|
214
|
+
return EVENT_CALENDAR_ADDON_MONTHLY_USD;
|
|
215
|
+
case 'aiAddon':
|
|
216
|
+
return pricing.aiAddonMonthlyUsd;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Full add-on quantity map (explicit zeros for absent kinds) from a
|
|
221
|
+
* subscription's items — the shape written to `org.seatAddons` so
|
|
222
|
+
* removals and dashboard edits converge on sync (AGL-527).
|
|
223
|
+
*/ export function addonQuantitiesFromItems(items) {
|
|
224
|
+
const quantities = Object.fromEntries(ADDON_KINDS.map((kind)=>[
|
|
225
|
+
kind,
|
|
226
|
+
0
|
|
227
|
+
]));
|
|
228
|
+
for (const item of items != null ? items : []){
|
|
229
|
+
var _item_price;
|
|
230
|
+
const kind = addonKindFromPriceId(item == null ? void 0 : (_item_price = item.price) == null ? void 0 : _item_price.id);
|
|
231
|
+
if (kind) {
|
|
232
|
+
var _ref;
|
|
233
|
+
quantities[kind] += Math.max(0, Math.floor(Number((_ref = item == null ? void 0 : item.quantity) != null ? _ref : 0)));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return quantities;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Max purchasable quantity per kind, from a purchases-free entitlement
|
|
240
|
+
* resolution (plan defaults + staff overrides only) so the ceiling does not
|
|
241
|
+
* drift as the org buys: seat/dataset kinds stop at the plan's hard max,
|
|
242
|
+
* hosts/registers use flat ceilings, the Event Calendar and Aglyn AI are 0/1
|
|
243
|
+
* toggles. POS registers additionally require the `pos` feature.
|
|
244
|
+
*
|
|
245
|
+
* Lives here rather than inside `/api/billing/addons` because a PLAN CHANGE
|
|
246
|
+
* needs the same ceiling for a different plan. The addons route asks "how many
|
|
247
|
+
* may this org buy on the plan it is on"; `buildTargetItems` asks "how many of
|
|
248
|
+
* what it already bought can the plan it is moving to actually deliver". Two
|
|
249
|
+
* readers, one definition — a second copy is how they answer differently.
|
|
250
|
+
*/ export function addonMaxForBaseline(kind, baseline) {
|
|
251
|
+
const bounded = (included, max)=>Number.isFinite(max) ? Math.max(0, max - included) : EXTRA_HOSTS_ADDON_MAX;
|
|
252
|
+
switch(kind){
|
|
253
|
+
case 'managers':
|
|
254
|
+
return bounded(baseline.managersPerOrg, baseline.maxManagersPerOrg);
|
|
255
|
+
case 'members':
|
|
256
|
+
return bounded(baseline.membersPerHost, baseline.maxMembersPerHost);
|
|
257
|
+
case 'datasets':
|
|
258
|
+
return bounded(baseline.datasetsPerOrg, baseline.maxDatasetsPerOrg);
|
|
259
|
+
case 'hosts':
|
|
260
|
+
return baseline.hostLimit === UNLIMITED ? 0 : EXTRA_HOSTS_ADDON_MAX;
|
|
261
|
+
case 'posRegisters':
|
|
262
|
+
return baseline.features.pos ? POS_REGISTERS_ADDON_MAX : 0;
|
|
263
|
+
case 'eventCalendar':
|
|
264
|
+
return 1;
|
|
265
|
+
case 'aiAddon':
|
|
266
|
+
// Org-wide like the Event Calendar: one purchase, and the price map
|
|
267
|
+
// (null on Free and Enterprise) says whether the plan sells it at all.
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** The ceiling a PLAN would impose on an add-on kind, purchases excluded. */ export function addonMaxForPlan(kind, plan) {
|
|
272
|
+
return addonMaxForBaseline(kind, resolveOrgEntitlements({
|
|
273
|
+
plan
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
//# sourceMappingURL=billing-addons.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/billing-addons.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n EVENT_CALENDAR_ADDON_MONTHLY_USD,\n EXTRA_HOSTS_ADDON_MAX,\n PLAN_PRICING,\n POS_REGISTER_ADDON_MONTHLY_USD,\n POS_REGISTERS_ADDON_MAX,\n resolveOrgEntitlements,\n SELF_SERVE_PLANS,\n UNLIMITED,\n type OrgPlan,\n type OrgSeatAddons,\n} from '@aglyn/aglyn/server'\n\n/**\n * Self-serve add-on price map (AGL-525): the bridge between\n * `STRIPE_PRICE_*` env price ids, `org.seatAddons` keys, and\n * `PLAN_PRICING` USD amounts. Add-ons bill as extra items on the org's\n * one subscription, so every kind has a monthly and a yearly price\n * (Stripe allows a single interval per subscription; annual orgs attach\n * the `_YEARLY` variants). Seat/member/dataset/host prices and the Aglyn AI\n * add-on (AGL-2896) are per-plan; POS registers and the Event Calendar are\n * flat. Prices are minted by `tools/scripts/setup-stripe.mjs` — keep the\n * three in sync.\n */\nexport type AddonKind = keyof Required<OrgSeatAddons>\n\nexport const ADDON_KINDS: readonly AddonKind[] = [\n 'managers',\n 'members',\n 'datasets',\n 'hosts',\n 'posRegisters',\n 'eventCalendar',\n 'aiAddon',\n]\n\nexport type BillingInterval = 'month' | 'year'\n\n/** Kinds priced per plan (env `STRIPE_PRICE_{PLAN}_{SUFFIX}[_YEARLY]`). */\nconst PER_PLAN_ENV_SUFFIX: Partial<Record<AddonKind, string>> = {\n managers: 'EXTRA_SEAT',\n members: 'EXTRA_MEMBER',\n datasets: 'EXTRA_DATASET',\n hosts: 'EXTRA_HOST',\n aiAddon: 'AI_ADDON',\n}\n\n/** Kinds priced flat across plans (env `STRIPE_PRICE_{SUFFIX}[_YEARLY]`). */\nconst FLAT_ENV_NAME: Partial<Record<AddonKind, string>> = {\n posRegisters: 'POS_REGISTER',\n eventCalendar: 'EVENT_CALENDAR',\n}\n\n/**\n * Every plan that is sold with a Stripe price — DERIVED, never hand-listed\n * (AGL-1340).\n *\n * The literal that used to sit here listed starter, pro, business and\n * advanced only — it predated the Scale and Agency tiers, so\n * `addonKindFromPriceId()` could not recognise a single Scale/Agency add-on\n * price coming back from Stripe. `addonPriceId()` still SOLD them (it only\n * uppercases the plan), so the money moved and the read-back returned\n * nothing: the webhook wrote `seatAddons: {all zeros}` for the\n * highest-paying orgs on the next subscription event, and `seatAddons` is an\n * ENTITLEMENT INPUT (`plan-entitlements.ts` — it raises `hostLimit` and\n * `posRegisters`, and flips the `eventCalendar` feature).\n *\n * Deriving from `SELF_SERVE_PLANS` is what stops the next tier from\n * reintroducing it: `free` has nothing to sell and `enterprise` is quoted\n * per deal (no `STRIPE_PRICE_ENTERPRISE_*` exists), so everything else is\n * by definition a paid, priced plan.\n */\nexport const PAID_PLANS: readonly OrgPlan[] = SELF_SERVE_PLANS.filter(\n (plan) => plan !== 'free',\n)\n\nfunction env(name: string): string | null {\n const value = process.env[name]\n return value ? value : null\n}\n\n/**\n * The Stripe price id for an add-on kind on a plan at a billing interval;\n * null when the plan doesn't sell it (free) or the env isn't configured.\n */\nexport function addonPriceId(\n kind: AddonKind,\n plan: OrgPlan,\n interval: BillingInterval,\n): string | null {\n const yearly = interval === 'year' ? '_YEARLY' : ''\n const flat = FLAT_ENV_NAME[kind]\n if (flat) return env(`STRIPE_PRICE_${flat}${yearly}`)\n const suffix = PER_PLAN_ENV_SUFFIX[kind]\n if (!suffix || plan === 'free') return null\n return env(`STRIPE_PRICE_${plan.toUpperCase()}_${suffix}${yearly}`)\n}\n\n/**\n * Reverse map: which add-on kind a subscription item's price id sells.\n * Null for non-add-on prices (the base plan item, unknown prices).\n */\nexport function addonKindFromPriceId(\n priceId: string | null | undefined,\n): AddonKind | null {\n if (!priceId) return null\n for (const kind of ADDON_KINDS) {\n for (const interval of ['month', 'year'] as const) {\n if (FLAT_ENV_NAME[kind]) {\n if (addonPriceId(kind, 'starter', interval) === priceId) return kind\n continue\n }\n for (const plan of PAID_PLANS) {\n if (addonPriceId(kind, plan, interval) === priceId) return kind\n }\n }\n }\n return null\n}\n\n/**\n * The BASE plan's Stripe price id at an interval\n * (`STRIPE_PRICE_{PLAN}[_YEARLY]`); null for `free`, for the custom-priced\n * `enterprise`, and for an env that isn't configured. Read at call time, so\n * it never freezes a stale value at module load the way the routes'\n * `PRICE_ENV` maps do.\n */\nexport function planPriceId(\n plan: OrgPlan,\n interval: BillingInterval,\n): string | null {\n if (!PAID_PLANS.includes(plan)) return null\n const yearly = interval === 'year' ? '_YEARLY' : ''\n return env(`STRIPE_PRICE_${plan.toUpperCase()}${yearly}`)\n}\n\n/**\n * Which plan a subscription item's price sells AND on which interval; null for\n * an add-on price, the metered price, an ad-hoc enterprise price, and anything\n * unknown.\n *\n * The interval half exists because a schedule phase read back from Stripe\n * carries price IDS and nothing else (AGL-2150): rebuilding that phase's item\n * list needs to know which interval it is priced on, and guessing it from the\n * live subscription is wrong exactly when the pending change is also an\n * interval change.\n */\nexport function planAndIntervalFromPriceId(\n priceId: string | null | undefined,\n): { plan: OrgPlan; interval: BillingInterval } | null {\n if (!priceId) return null\n for (const plan of PAID_PLANS) {\n for (const interval of ['month', 'year'] as const) {\n if (planPriceId(plan, interval) === priceId) return { plan, interval }\n }\n }\n return null\n}\n\n/**\n * Which plan a subscription item's price sells; null for an add-on price,\n * the metered price, an ad-hoc enterprise price, and anything unknown.\n */\nexport function planFromPriceId(\n priceId: string | null | undefined,\n): OrgPlan | null {\n return planAndIntervalFromPriceId(priceId)?.plan ?? null\n}\n\n/**\n * The shared metered-usage price (AGL-635) that makes reported usage\n * billable, for a given billing interval.\n *\n * Stripe rejects a subscription whose items mix `recurring.interval`, so the\n * metered item has to recur the same way the plan does. There are two prices\n * — `aglyn_metered_usage` (monthly) and `aglyn_metered_usage_yearly` — and\n * both bill $0.01 per aggregated unit against the SAME meter, because the\n * rollup posts an already-computed value in CENTS. The interval is therefore\n * the only thing that differs, and it is the only thing a caller must get\n * right.\n *\n * `interval` is REQUIRED on purpose (AGL-1280). It used to be monthly-only\n * with no argument, so the failure mode of forgetting it was attaching a\n * monthly price to a yearly subscription — the exact mixed-interval crash\n * this signature now makes unrepresentable. A default would hand that\n * footgun straight back.\n */\nexport function meteredPriceId(interval: 'month' | 'year'): string | null {\n return env(\n interval === 'year' ? 'STRIPE_PRICE_METERED_YEARLY' : 'STRIPE_PRICE_METERED',\n )\n}\n\n/**\n * Whether a price id is EITHER shared metered-usage price.\n *\n * Interval-agnostic by design: callers ask this to exclude the metered item\n * from plan/add-on reasoning, and an annual subscription's metered item must\n * be excluded just as thoroughly as a monthly one.\n */\nexport function isMeteredPriceId(priceId: string | null | undefined): boolean {\n if (!priceId) return false\n return (\n priceId === meteredPriceId('month') || priceId === meteredPriceId('year')\n )\n}\n\n/**\n * The subscription's BASE PLAN item, identified POSITIVELY (AGL-1340).\n *\n * This used to be \"whatever no add-on price claims\" — identification by\n * elimination, which quietly returns the wrong item the moment a\n * subscription carries anything the add-on map doesn't recognise: the\n * metered item, a grandfathered v1 price, a coupon-era SKU, an item added\n * by hand in the Stripe dashboard. Picking an add-on item as the plan item\n * means a plan switch re-prices the ADD-ON, and reads the billing interval\n * off it.\n *\n * So: match a known `STRIPE_PRICE_{PLAN}[_YEARLY]` first. Only when nothing\n * matches — an enterprise deal bills on an ad-hoc price, and grandfathered\n * subscriptions predate the current SKUs — fall back to the old rule,\n * minus the metered item, and finally to `items[0]` so behaviour for those\n * subscriptions is unchanged.\n */\nexport function findPlanItem<T extends { price?: { id?: string } | null }>(\n items: readonly T[] | null | undefined,\n): T | null {\n const list = items ?? []\n const byPlanPrice = list.find((item) => planFromPriceId(item?.price?.id))\n if (byPlanPrice) return byPlanPrice\n const notAnAddon = list.find(\n (item) =>\n !addonKindFromPriceId(item?.price?.id) &&\n !isMeteredPriceId(item?.price?.id),\n )\n return notAnAddon ?? list[0] ?? null\n}\n\n/**\n * Monthly USD per unit for an add-on on a plan (yearly prices are ×12 of\n * this); null when the plan doesn't sell the kind. Single pricing source\n * of truth: `PLAN_PRICING` + the two flat add-on constants.\n */\nexport function addonUnitUsd(kind: AddonKind, plan: OrgPlan): number | null {\n const pricing = PLAN_PRICING[plan]\n switch (kind) {\n case 'managers':\n return pricing.extraSeatMonthlyUsd\n case 'members':\n return pricing.extraCollaboratorMonthlyUsd\n case 'datasets':\n return pricing.extraDatasetMonthlyUsd\n case 'hosts':\n return pricing.extraHostMonthlyUsd\n case 'posRegisters':\n return POS_REGISTER_ADDON_MONTHLY_USD\n case 'eventCalendar':\n return EVENT_CALENDAR_ADDON_MONTHLY_USD\n case 'aiAddon':\n return pricing.aiAddonMonthlyUsd\n }\n}\n\n/**\n * Full add-on quantity map (explicit zeros for absent kinds) from a\n * subscription's items — the shape written to `org.seatAddons` so\n * removals and dashboard edits converge on sync (AGL-527).\n */\nexport function addonQuantitiesFromItems(\n items:\n | Array<{ price?: { id?: string } | null; quantity?: number | null }>\n | null\n | undefined,\n): Record<AddonKind, number> {\n const quantities = Object.fromEntries(\n ADDON_KINDS.map((kind) => [kind, 0]),\n ) as Record<AddonKind, number>\n for (const item of items ?? []) {\n const kind = addonKindFromPriceId(item?.price?.id)\n if (kind) {\n quantities[kind] += Math.max(0, Math.floor(Number(item?.quantity ?? 0)))\n }\n }\n return quantities\n}\n\n/**\n * Max purchasable quantity per kind, from a purchases-free entitlement\n * resolution (plan defaults + staff overrides only) so the ceiling does not\n * drift as the org buys: seat/dataset kinds stop at the plan's hard max,\n * hosts/registers use flat ceilings, the Event Calendar and Aglyn AI are 0/1\n * toggles. POS registers additionally require the `pos` feature.\n *\n * Lives here rather than inside `/api/billing/addons` because a PLAN CHANGE\n * needs the same ceiling for a different plan. The addons route asks \"how many\n * may this org buy on the plan it is on\"; `buildTargetItems` asks \"how many of\n * what it already bought can the plan it is moving to actually deliver\". Two\n * readers, one definition — a second copy is how they answer differently.\n */\nexport function addonMaxForBaseline(\n kind: AddonKind,\n baseline: ReturnType<typeof resolveOrgEntitlements>,\n): number {\n const bounded = (included: number, max: number) =>\n Number.isFinite(max) ? Math.max(0, max - included) : EXTRA_HOSTS_ADDON_MAX\n switch (kind) {\n case 'managers':\n return bounded(baseline.managersPerOrg, baseline.maxManagersPerOrg)\n case 'members':\n return bounded(baseline.membersPerHost, baseline.maxMembersPerHost)\n case 'datasets':\n return bounded(baseline.datasetsPerOrg, baseline.maxDatasetsPerOrg)\n case 'hosts':\n return baseline.hostLimit === UNLIMITED ? 0 : EXTRA_HOSTS_ADDON_MAX\n case 'posRegisters':\n return baseline.features.pos ? POS_REGISTERS_ADDON_MAX : 0\n case 'eventCalendar':\n return 1\n case 'aiAddon':\n // Org-wide like the Event Calendar: one purchase, and the price map\n // (null on Free and Enterprise) says whether the plan sells it at all.\n return 1\n }\n}\n\n/** The ceiling a PLAN would impose on an add-on kind, purchases excluded. */\nexport function addonMaxForPlan(kind: AddonKind, plan: OrgPlan): number {\n return addonMaxForBaseline(kind, resolveOrgEntitlements({ plan } as never))\n}\n"],"names":["EVENT_CALENDAR_ADDON_MONTHLY_USD","EXTRA_HOSTS_ADDON_MAX","PLAN_PRICING","POS_REGISTER_ADDON_MONTHLY_USD","POS_REGISTERS_ADDON_MAX","resolveOrgEntitlements","SELF_SERVE_PLANS","UNLIMITED","ADDON_KINDS","PER_PLAN_ENV_SUFFIX","managers","members","datasets","hosts","aiAddon","FLAT_ENV_NAME","posRegisters","eventCalendar","PAID_PLANS","filter","plan","env","name","value","process","addonPriceId","kind","interval","yearly","flat","suffix","toUpperCase","addonKindFromPriceId","priceId","planPriceId","includes","planAndIntervalFromPriceId","planFromPriceId","meteredPriceId","isMeteredPriceId","findPlanItem","items","notAnAddon","list","byPlanPrice","find","item","price","id","addonUnitUsd","pricing","extraSeatMonthlyUsd","extraCollaboratorMonthlyUsd","extraDatasetMonthlyUsd","extraHostMonthlyUsd","aiAddonMonthlyUsd","addonQuantitiesFromItems","quantities","Object","fromEntries","map","Math","max","floor","Number","quantity","addonMaxForBaseline","baseline","bounded","included","isFinite","managersPerOrg","maxManagersPerOrg","membersPerHost","maxMembersPerHost","datasetsPerOrg","maxDatasetsPerOrg","hostLimit","features","pos","addonMaxForPlan"],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/** The per-host collection of conversion attribution records. */
|
|
18
|
+
export declare const CAMPAIGN_ATTRIBUTIONS_COLLECTION = "campaignAttributions";
|
|
19
|
+
/**
|
|
20
|
+
* Removes every conversion attributed to one person, across every site.
|
|
21
|
+
*
|
|
22
|
+
* ## Why an erasure reaches these at all
|
|
23
|
+
*
|
|
24
|
+
* An attribution is a claim about a person: "this human came from that
|
|
25
|
+
* campaign and then did this". It is the same personal fact as the click
|
|
26
|
+
* stamp it was derived from, and the erasure path already deletes those —
|
|
27
|
+
* `eraseEmailDeliveriesForAddresses` drops `campaignTouches` alongside the
|
|
28
|
+
* engagement stamps and says why. A summary that outlived its source would
|
|
29
|
+
* leave an erasure that removed the evidence and kept the conclusion.
|
|
30
|
+
*
|
|
31
|
+
* ## Why a collection-group query
|
|
32
|
+
*
|
|
33
|
+
* The record is per HOST, because that is where a site's own reporting reads
|
|
34
|
+
* it. An erasure arrives as an ADDRESS and knows nothing about which sites
|
|
35
|
+
* that person ever visited. The collection group is the join between the two,
|
|
36
|
+
* and `personKey` is on the record precisely so one exists — declared in
|
|
37
|
+
* `cloud/firebase-firestore.indexes.json` with `COLLECTION_GROUP` scope,
|
|
38
|
+
* because a collection-group query gets no automatic single-field index and
|
|
39
|
+
* would otherwise fail with FAILED_PRECONDITION at the moment somebody asked
|
|
40
|
+
* to be forgotten.
|
|
41
|
+
*
|
|
42
|
+
* ## The order's own record is NOT touched
|
|
43
|
+
*
|
|
44
|
+
* `emailAttributions` holds a commercial fact about a sale, keyed by the
|
|
45
|
+
* order's id and carrying no address — the revenue join says so where it
|
|
46
|
+
* writes it. This holds a record of a person's behavior under a key derived
|
|
47
|
+
* from their address. Different facts, different answers, and the split is
|
|
48
|
+
* why both are defensible.
|
|
49
|
+
*
|
|
50
|
+
* Never throws: an erasure that fails on one leg must still complete the
|
|
51
|
+
* others, and the caller reports what it removed.
|
|
52
|
+
*
|
|
53
|
+
* @returns how many records were removed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function eraseCampaignAttributionsForPersonKey(key: string | null | undefined, firestore?: any): Promise<number>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* Where conversion attributions are kept, and how an erasure reaches them.
|
|
18
|
+
*
|
|
19
|
+
* ## Why this is a leaf module and not part of the join
|
|
20
|
+
*
|
|
21
|
+
* The erasure runs from `email-delivery-log.ts`, in the same function that
|
|
22
|
+
* drops a person's campaign touches — which is the point, because an
|
|
23
|
+
* attribution is a conclusion drawn from those touches and a conclusion that
|
|
24
|
+
* outlives its evidence is an erasure that did not happen. But the join
|
|
25
|
+
* itself READS that module (`readEmailCampaignTouch` is one half of the
|
|
26
|
+
* last-touch comparison), so putting the erasure beside the join would make
|
|
27
|
+
* the two modules import each other. Under jest that cycle resolves to
|
|
28
|
+
* `undefined` at import time rather than failing loudly, which is how an
|
|
29
|
+
* erasure comes to silently do nothing while its test passes.
|
|
30
|
+
*
|
|
31
|
+
* So the collection name and the sweep live here, importing nothing but
|
|
32
|
+
* Firestore, and both sides import this.
|
|
33
|
+
*/ import firebaseAdmin from "./firebase-admin.js";
|
|
34
|
+
const defaultFirestore = ()=>firebaseAdmin.app().firestore();
|
|
35
|
+
/** The per-host collection of conversion attribution records. */ export const CAMPAIGN_ATTRIBUTIONS_COLLECTION = 'campaignAttributions';
|
|
36
|
+
/** How many records one erasure pass removes per batch. */ const ERASURE_BATCH = 400;
|
|
37
|
+
/**
|
|
38
|
+
* Removes every conversion attributed to one person, across every site.
|
|
39
|
+
*
|
|
40
|
+
* ## Why an erasure reaches these at all
|
|
41
|
+
*
|
|
42
|
+
* An attribution is a claim about a person: "this human came from that
|
|
43
|
+
* campaign and then did this". It is the same personal fact as the click
|
|
44
|
+
* stamp it was derived from, and the erasure path already deletes those —
|
|
45
|
+
* `eraseEmailDeliveriesForAddresses` drops `campaignTouches` alongside the
|
|
46
|
+
* engagement stamps and says why. A summary that outlived its source would
|
|
47
|
+
* leave an erasure that removed the evidence and kept the conclusion.
|
|
48
|
+
*
|
|
49
|
+
* ## Why a collection-group query
|
|
50
|
+
*
|
|
51
|
+
* The record is per HOST, because that is where a site's own reporting reads
|
|
52
|
+
* it. An erasure arrives as an ADDRESS and knows nothing about which sites
|
|
53
|
+
* that person ever visited. The collection group is the join between the two,
|
|
54
|
+
* and `personKey` is on the record precisely so one exists — declared in
|
|
55
|
+
* `cloud/firebase-firestore.indexes.json` with `COLLECTION_GROUP` scope,
|
|
56
|
+
* because a collection-group query gets no automatic single-field index and
|
|
57
|
+
* would otherwise fail with FAILED_PRECONDITION at the moment somebody asked
|
|
58
|
+
* to be forgotten.
|
|
59
|
+
*
|
|
60
|
+
* ## The order's own record is NOT touched
|
|
61
|
+
*
|
|
62
|
+
* `emailAttributions` holds a commercial fact about a sale, keyed by the
|
|
63
|
+
* order's id and carrying no address — the revenue join says so where it
|
|
64
|
+
* writes it. This holds a record of a person's behavior under a key derived
|
|
65
|
+
* from their address. Different facts, different answers, and the split is
|
|
66
|
+
* why both are defensible.
|
|
67
|
+
*
|
|
68
|
+
* Never throws: an erasure that fails on one leg must still complete the
|
|
69
|
+
* others, and the caller reports what it removed.
|
|
70
|
+
*
|
|
71
|
+
* @returns how many records were removed.
|
|
72
|
+
*/ export async function eraseCampaignAttributionsForPersonKey(key, firestore) {
|
|
73
|
+
if (!key || typeof key !== 'string') return 0;
|
|
74
|
+
const db = firestore != null ? firestore : defaultFirestore();
|
|
75
|
+
let removed = 0;
|
|
76
|
+
try {
|
|
77
|
+
// Bounded rather than `while (true)`: a pathological volume must not be
|
|
78
|
+
// able to hold an erasure request open indefinitely, and the pass that
|
|
79
|
+
// runs next picks up whatever a bounded one left.
|
|
80
|
+
for(let pass = 0; pass < 20; pass += 1){
|
|
81
|
+
const snapshot = await db.collectionGroup(CAMPAIGN_ATTRIBUTIONS_COLLECTION).where('personKey', '==', key).limit(ERASURE_BATCH).get();
|
|
82
|
+
if (snapshot.empty) break;
|
|
83
|
+
const batch = db.batch();
|
|
84
|
+
snapshot.docs.forEach((doc)=>batch.delete(doc.ref));
|
|
85
|
+
await batch.commit();
|
|
86
|
+
removed += snapshot.size;
|
|
87
|
+
if (snapshot.size < ERASURE_BATCH) break;
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('eraseCampaignAttributionsForPersonKey failed', error);
|
|
91
|
+
}
|
|
92
|
+
return removed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=campaign-attribution-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/campaign-attribution-store.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Where conversion attributions are kept, and how an erasure reaches them.\n *\n * ## Why this is a leaf module and not part of the join\n *\n * The erasure runs from `email-delivery-log.ts`, in the same function that\n * drops a person's campaign touches — which is the point, because an\n * attribution is a conclusion drawn from those touches and a conclusion that\n * outlives its evidence is an erasure that did not happen. But the join\n * itself READS that module (`readEmailCampaignTouch` is one half of the\n * last-touch comparison), so putting the erasure beside the join would make\n * the two modules import each other. Under jest that cycle resolves to\n * `undefined` at import time rather than failing loudly, which is how an\n * erasure comes to silently do nothing while its test passes.\n *\n * So the collection name and the sweep live here, importing nothing but\n * Firestore, and both sides import this.\n */\n\nimport firebaseAdmin from './firebase-admin'\n\nconst defaultFirestore = () => firebaseAdmin.app().firestore()\n\n/** The per-host collection of conversion attribution records. */\nexport const CAMPAIGN_ATTRIBUTIONS_COLLECTION = 'campaignAttributions'\n\n/** How many records one erasure pass removes per batch. */\nconst ERASURE_BATCH = 400\n\n/**\n * Removes every conversion attributed to one person, across every site.\n *\n * ## Why an erasure reaches these at all\n *\n * An attribution is a claim about a person: \"this human came from that\n * campaign and then did this\". It is the same personal fact as the click\n * stamp it was derived from, and the erasure path already deletes those —\n * `eraseEmailDeliveriesForAddresses` drops `campaignTouches` alongside the\n * engagement stamps and says why. A summary that outlived its source would\n * leave an erasure that removed the evidence and kept the conclusion.\n *\n * ## Why a collection-group query\n *\n * The record is per HOST, because that is where a site's own reporting reads\n * it. An erasure arrives as an ADDRESS and knows nothing about which sites\n * that person ever visited. The collection group is the join between the two,\n * and `personKey` is on the record precisely so one exists — declared in\n * `cloud/firebase-firestore.indexes.json` with `COLLECTION_GROUP` scope,\n * because a collection-group query gets no automatic single-field index and\n * would otherwise fail with FAILED_PRECONDITION at the moment somebody asked\n * to be forgotten.\n *\n * ## The order's own record is NOT touched\n *\n * `emailAttributions` holds a commercial fact about a sale, keyed by the\n * order's id and carrying no address — the revenue join says so where it\n * writes it. This holds a record of a person's behavior under a key derived\n * from their address. Different facts, different answers, and the split is\n * why both are defensible.\n *\n * Never throws: an erasure that fails on one leg must still complete the\n * others, and the caller reports what it removed.\n *\n * @returns how many records were removed.\n */\nexport async function eraseCampaignAttributionsForPersonKey(\n key: string | null | undefined,\n firestore?: any,\n): Promise<number> {\n if (!key || typeof key !== 'string') return 0\n const db = firestore ?? defaultFirestore()\n let removed = 0\n try {\n // Bounded rather than `while (true)`: a pathological volume must not be\n // able to hold an erasure request open indefinitely, and the pass that\n // runs next picks up whatever a bounded one left.\n for (let pass = 0; pass < 20; pass += 1) {\n const snapshot = await db\n .collectionGroup(CAMPAIGN_ATTRIBUTIONS_COLLECTION)\n .where('personKey', '==', key)\n .limit(ERASURE_BATCH)\n .get()\n if (snapshot.empty) break\n const batch = db.batch()\n snapshot.docs.forEach((doc: any) => batch.delete(doc.ref))\n await batch.commit()\n removed += snapshot.size\n if (snapshot.size < ERASURE_BATCH) break\n }\n } catch (error) {\n console.error('eraseCampaignAttributionsForPersonKey failed', error)\n }\n return removed\n}\n"],"names":["firebaseAdmin","defaultFirestore","app","firestore","CAMPAIGN_ATTRIBUTIONS_COLLECTION","ERASURE_BATCH","eraseCampaignAttributionsForPersonKey","key","db","removed","pass","snapshot","collectionGroup","where","limit","get","empty","batch","docs","forEach","doc","delete","ref","commit","size","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;CAiBC,GAED,OAAOA,mBAAmB,sBAAkB;AAE5C,MAAMC,mBAAmB,IAAMD,cAAcE,GAAG,GAAGC,SAAS;AAE5D,+DAA+D,GAC/D,OAAO,MAAMC,mCAAmC,uBAAsB;AAEtE,yDAAyD,GACzD,MAAMC,gBAAgB;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCC,GACD,OAAO,eAAeC,sCACpBC,GAA8B,EAC9BJ,SAAe;IAEf,IAAI,CAACI,OAAO,OAAOA,QAAQ,UAAU,OAAO;IAC5C,MAAMC,KAAKL,oBAAAA,YAAaF;IACxB,IAAIQ,UAAU;IACd,IAAI;QACF,wEAAwE;QACxE,uEAAuE;QACvE,kDAAkD;QAClD,IAAK,IAAIC,OAAO,GAAGA,OAAO,IAAIA,QAAQ,EAAG;YACvC,MAAMC,WAAW,MAAMH,GACpBI,eAAe,CAACR,kCAChBS,KAAK,CAAC,aAAa,MAAMN,KACzBO,KAAK,CAACT,eACNU,GAAG;YACN,IAAIJ,SAASK,KAAK,EAAE;YACpB,MAAMC,QAAQT,GAAGS,KAAK;YACtBN,SAASO,IAAI,CAACC,OAAO,CAAC,CAACC,MAAaH,MAAMI,MAAM,CAACD,IAAIE,GAAG;YACxD,MAAML,MAAMM,MAAM;YAClBd,WAAWE,SAASa,IAAI;YACxB,IAAIb,SAASa,IAAI,GAAGnB,eAAe;QACrC;IACF,EAAE,OAAOoB,OAAO;QACdC,QAAQD,KAAK,CAAC,gDAAgDA;IAChE;IACA,OAAOhB;AACT"}
|