@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,546 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { CAPTURED_BY_HOST_FIELD, checkCrmRecordsQuota, consentGroupScope, CONTACT_FACETS_FIELD, CONTACT_FORM_IDS_CAP, CONTACT_FORM_IDS_FIELD, marketingConsentFieldsForGroup, mergeContactInteraction, normalizeCampaignIds, readContactFacet, normalizeContactEmail, ORG_SCOPE_TOKEN } from "@aglyn/aglyn/server";
|
|
18
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
19
|
+
import { firebaseAdmin } from "./firebase-admin.js";
|
|
20
|
+
import { countCrmRecords } from "./crm-records.js";
|
|
21
|
+
import { attributeOrderToEmail } from "./email-revenue-attribution.js";
|
|
22
|
+
import { hostRefusesCaptureForErasure } from "./email-suppression.js";
|
|
23
|
+
import { attributeCampaignConversion } from "./campaign-conversion-attribution.js";
|
|
24
|
+
import { nameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
|
|
25
|
+
import { declineMarketingConsentFields, MARKETING_CONSENT_SOURCE_FIELD } from "@aglyn/aglyn/app-utils/marketing-consent";
|
|
26
|
+
/*
|
|
27
|
+
* The module paths, like `name-search` above, rather than the barrel: the
|
|
28
|
+
* pure helpers this door leans on are exactly the ones a spec of the door
|
|
29
|
+
* substitutes a fixture barrel for, and a fixture that has to re-export the
|
|
30
|
+
* whole of `@aglyn/aglyn` to keep a normalizer reachable is a fixture that
|
|
31
|
+
* drifts. A direct path is real in every harness.
|
|
32
|
+
*/ import { advanceContactLifecycleStage, CONTACT_COMPANY_IDS_FIELD, CONTACT_FIELD_KEY_PATTERN, CRM_COLLECTIONS, isContactLifecycleStage, planContactCompanyLink, readContactCompanyLink } from "@aglyn/aglyn/app-utils/crm";
|
|
33
|
+
import { normalizeAddress, normalizePhone } from "@aglyn/aglyn/foundation/definitions/contact.types";
|
|
34
|
+
import { contactCompanyMirrorValue, settleCompanyContactsCounts } from "./contact-company-link.js";
|
|
35
|
+
import { emailIndexBeside, findContactByEmail, writeContactEmailIndex } from "./contact-email-index.js";
|
|
36
|
+
import { consentGroupForSite, getOrgForHost, orgDataCollectionForHost } from "./organizations.js";
|
|
37
|
+
/**
|
|
38
|
+
* The profile as it may be STORED: every value normalized, every unusable
|
|
39
|
+
* one dropped, and nothing present that was not given.
|
|
40
|
+
*
|
|
41
|
+
* Only the keys given come back, which is what lets a merge write this
|
|
42
|
+
* straight into the facet: a door that knows the phone number and nothing
|
|
43
|
+
* else leaves the title, the owner and the stage exactly as another door
|
|
44
|
+
* left them. An address given as `null` is a deliberate clearing and is kept
|
|
45
|
+
* as `null`; one that normalizes to nothing is the same thing.
|
|
46
|
+
*/ function storableProfile(input) {
|
|
47
|
+
if (!input) return {};
|
|
48
|
+
const out = {};
|
|
49
|
+
if (input.phone !== undefined) {
|
|
50
|
+
const phone = normalizePhone(input.phone);
|
|
51
|
+
if (phone) out.phone = phone;
|
|
52
|
+
}
|
|
53
|
+
if (typeof input.jobTitle === 'string') {
|
|
54
|
+
const jobTitle = input.jobTitle.trim().slice(0, 120);
|
|
55
|
+
if (jobTitle) out.jobTitle = jobTitle;
|
|
56
|
+
}
|
|
57
|
+
if (input.address !== undefined) out.address = normalizeAddress(input.address);
|
|
58
|
+
if (typeof input.companyId === 'string' && input.companyId.trim()) {
|
|
59
|
+
out.companyId = input.companyId.trim().slice(0, 128);
|
|
60
|
+
}
|
|
61
|
+
if (typeof input.ownerUid === 'string' && input.ownerUid.trim()) {
|
|
62
|
+
out.ownerUid = input.ownerUid.trim().slice(0, 128);
|
|
63
|
+
}
|
|
64
|
+
if (isContactLifecycleStage(input.lifecycleStage)) {
|
|
65
|
+
out.lifecycleStage = input.lifecycleStage;
|
|
66
|
+
}
|
|
67
|
+
if (input.custom && typeof input.custom === 'object') {
|
|
68
|
+
/*
|
|
69
|
+
* Only a key a field definition could have, and only a value the field
|
|
70
|
+
* types can hold. A nested object here is a map the merge below would
|
|
71
|
+
* write as a subtree nobody can render; a key with a dot in it would be
|
|
72
|
+
* read as a PATH by the next dotted update to touch the facet. Nothing is
|
|
73
|
+
* coerced — a door that has a number should send one — and an empty map
|
|
74
|
+
* is left off rather than written as `{}` over a holder's values.
|
|
75
|
+
*/ const custom = {};
|
|
76
|
+
for (const [key, value] of Object.entries(input.custom)){
|
|
77
|
+
if (!CONTACT_FIELD_KEY_PATTERN.test(key)) continue;
|
|
78
|
+
if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
79
|
+
custom[key] = typeof value === 'string' ? value.slice(0, 2000) : value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (Object.keys(custom).length) out.custom = custom;
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The org's companies collection, beside its contacts one.
|
|
88
|
+
*
|
|
89
|
+
* Reached through the contacts reference's parent — the org document —
|
|
90
|
+
* because that is the one handle this function holds on the org. `parent`
|
|
91
|
+
* is `null` only for a root collection, which an org subcollection never
|
|
92
|
+
* is; answered as `null` rather than thrown so a count that has nowhere to
|
|
93
|
+
* land is skipped, and never costs the capture.
|
|
94
|
+
*/ function companiesBeside(contactsRef) {
|
|
95
|
+
const parent = contactsRef.parent;
|
|
96
|
+
return parent ? parent.collection(CRM_COLLECTIONS.companies) : null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Tags as the profile drawer stores them: trimmed, lowercased, deduplicated
|
|
100
|
+
* and capped at twenty — so an imported `VIP` and a typed `vip` are one tag.
|
|
101
|
+
*/ function normalizeTags(tags) {
|
|
102
|
+
return [
|
|
103
|
+
...new Set((tags != null ? tags : []).map((tag)=>String(tag != null ? tag : '').trim().toLowerCase()).filter(Boolean))
|
|
104
|
+
].slice(0, 20);
|
|
105
|
+
}
|
|
106
|
+
export async function upsertHostContact(options) {
|
|
107
|
+
try {
|
|
108
|
+
var _options_interaction_atMs, _options_campaignIds, _ref, _contactsRef_parent, _ref1, _ref2;
|
|
109
|
+
var _options_facet, _orgBilling_org;
|
|
110
|
+
const email = normalizeContactEmail(options.email);
|
|
111
|
+
if (!email) return {
|
|
112
|
+
refused: 'invalid-email'
|
|
113
|
+
};
|
|
114
|
+
const tags = normalizeTags(options.tags);
|
|
115
|
+
/*==========================================
|
|
116
|
+
* THE PURCHASE DOOR, AND THEREFORE THE ATTRIBUTION DOOR.
|
|
117
|
+
*
|
|
118
|
+
* Every way of buying something in this product — the cart, buy-now, the
|
|
119
|
+
* POS register, a draft order, a reservation, a subscription renewal, a
|
|
120
|
+
* booking — announces itself here, in exactly one shape: source `order`,
|
|
121
|
+
* a `purchaseCents` amount, and a `refId` naming what was bought. That
|
|
122
|
+
* shape IS the purchase chokepoint, which is why the revenue join hangs
|
|
123
|
+
* off it rather than off seven call sites in a webhook.
|
|
124
|
+
*
|
|
125
|
+
* ABOVE the audience-band gate below, and deliberately. Contact creation
|
|
126
|
+
* is band gated, so a Free org past its included count drops the CRM
|
|
127
|
+
* record — and an attribution written inside that branch would drop the
|
|
128
|
+
* revenue with it. The join keys on the address hash, exactly as the
|
|
129
|
+
* touch and the suppression list do, so it never needs a contact document
|
|
130
|
+
* to exist: a guest checkout by somebody who is not and never becomes a
|
|
131
|
+
* contact still credits the campaign whose link they clicked.
|
|
132
|
+
*
|
|
133
|
+
* Its own `catch`, inside a function that already swallows: this is the
|
|
134
|
+
* least important write on the path, and a failure here must not cost the
|
|
135
|
+
* contact capture below it.
|
|
136
|
+
*=========================================*/ if (options.source === 'order' && options.interaction.refId) {
|
|
137
|
+
var _options_purchaseCents, _options_interaction_atMs1;
|
|
138
|
+
await attributeOrderToEmail(_extends({
|
|
139
|
+
hostId: options.hostId,
|
|
140
|
+
orderId: String(options.interaction.refId),
|
|
141
|
+
email,
|
|
142
|
+
amountCents: Number((_options_purchaseCents = options.purchaseCents) != null ? _options_purchaseCents : 0)
|
|
143
|
+
}, options.purchaseCurrency ? {
|
|
144
|
+
currency: options.purchaseCurrency
|
|
145
|
+
} : {}, {
|
|
146
|
+
orderedAtMs: (_options_interaction_atMs1 = options.interaction.atMs) != null ? _options_interaction_atMs1 : Date.now()
|
|
147
|
+
})).catch(()=>null);
|
|
148
|
+
}
|
|
149
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
150
|
+
const hostRef = firestore.collection('hosts').doc(options.hostId);
|
|
151
|
+
// Contacts are org-scoped (AGL-237): every host in the org feeds one
|
|
152
|
+
// shared list.
|
|
153
|
+
const contactsRef = await orgDataCollectionForHost(options.hostId, 'contacts');
|
|
154
|
+
/*
|
|
155
|
+
* The consent group this capture belongs to — the sites declared to be
|
|
156
|
+
* one sender, or this site alone. Resolved once and used for three
|
|
157
|
+
* different decisions below, which must all agree: which controller the
|
|
158
|
+
* basis is recorded for, which sites the row becomes visible to, and
|
|
159
|
+
* whether the capture surface had to disclose anything.
|
|
160
|
+
*/ const group = await consentGroupForSite(options.hostId);
|
|
161
|
+
/*
|
|
162
|
+
* THE CONSENT ENTRY THIS CAPTURE WRITES, decided once for both branches.
|
|
163
|
+
*
|
|
164
|
+
* A grant outranks a refusal flag set beside it — one door cannot mean
|
|
165
|
+
* both — and neither writes anything when the door carried no act, so
|
|
166
|
+
* an unticked box stays the third state. The provenance rides inside the
|
|
167
|
+
* per-host entry, where the reader looks for it, never at the top of a
|
|
168
|
+
* document every brand in the org shares.
|
|
169
|
+
*/ const consentExtra = options.marketingConsentSource ? {
|
|
170
|
+
[MARKETING_CONSENT_SOURCE_FIELD]: options.marketingConsentSource
|
|
171
|
+
} : undefined;
|
|
172
|
+
const consentFields = options.marketingConsent ? marketingConsentFieldsForGroup(group, Date.now(), consentExtra) : options.declineMarketingConsent ? declineMarketingConsentFields(options.hostId, Date.now(), consentExtra) : {};
|
|
173
|
+
const interaction = _extends({
|
|
174
|
+
type: options.source,
|
|
175
|
+
atMs: (_options_interaction_atMs = options.interaction.atMs) != null ? _options_interaction_atMs : Date.now(),
|
|
176
|
+
// WHICH SITE this visit happened on. The row is shared; the history on
|
|
177
|
+
// it is not, and a timeline with no site cannot be split for an
|
|
178
|
+
// agency's client without showing them another client's activity.
|
|
179
|
+
hostId: options.hostId
|
|
180
|
+
}, options.interaction.refId ? {
|
|
181
|
+
refId: options.interaction.refId
|
|
182
|
+
} : {}, options.interaction.summary ? {
|
|
183
|
+
summary: options.interaction.summary.slice(0, 200)
|
|
184
|
+
} : {}, options.interaction.formId ? {
|
|
185
|
+
formId: String(options.interaction.formId).slice(0, 128)
|
|
186
|
+
} : {}, options.interaction.path ? {
|
|
187
|
+
path: String(options.interaction.path).slice(0, 500)
|
|
188
|
+
} : {});
|
|
189
|
+
/*
|
|
190
|
+
* THE CAMPAIGNS THIS CAPTURE FILES THE PERSON UNDER.
|
|
191
|
+
*
|
|
192
|
+
* Normalized here rather than trusted, because it reaches this function
|
|
193
|
+
* from a public endpoint's document read and every reader of the stored
|
|
194
|
+
* array goes through the same coercion.
|
|
195
|
+
*/ const campaignIds = normalizeCampaignIds((_options_campaignIds = options.campaignIds) != null ? _options_campaignIds : []);
|
|
196
|
+
/*
|
|
197
|
+
* THE CUSTOM FIELD VALUES THIS CAPTURE CARRIES, as one nested map.
|
|
198
|
+
*
|
|
199
|
+
* Only the keys the door resolved. Written in the NESTED form because both
|
|
200
|
+
* writes below are merge-sets, which deep-merge a map one key at a time:
|
|
201
|
+
* `custom: { tier: 'Gold' }` lands beside an existing `custom.vip` and
|
|
202
|
+
* leaves it standing. A dotted `facets.h1.custom.tier` path would be a
|
|
203
|
+
* literal field name to a `set`, and a `custom` written whole would take
|
|
204
|
+
* every other key with it.
|
|
205
|
+
*/ const customEntries = Object.entries((_ref = (_options_facet = options.facet) == null ? void 0 : _options_facet.custom) != null ? _ref : {});
|
|
206
|
+
const customFacet = customEntries.length ? {
|
|
207
|
+
custom: Object.fromEntries(customEntries)
|
|
208
|
+
} : {};
|
|
209
|
+
/*==========================================
|
|
210
|
+
* THE DEDUPE LOOKUP IS UNSCOPED, AND HAS TO BE.
|
|
211
|
+
*
|
|
212
|
+
* One human who touched two sites is ONE person. Narrowing this read to
|
|
213
|
+
* what the capturing site may already see would make a second submission
|
|
214
|
+
* on a sibling brand create a SECOND document for the same address —
|
|
215
|
+
* which loses the dedupe the shared address book exists for, and bills
|
|
216
|
+
* the org twice for one human.
|
|
217
|
+
*
|
|
218
|
+
* Recognizing somebody is not the same act as being allowed to read their
|
|
219
|
+
* row, and the two were the same query while every contact was stamped
|
|
220
|
+
* org-wide. They are separated here: this finds the person, and
|
|
221
|
+
* `visibleTo` below decides who may see them — widened by the capture
|
|
222
|
+
* that just happened, never by the lookup that found them.
|
|
223
|
+
*
|
|
224
|
+
* THROUGH THE ADDRESS INDEX FIRST (AGL-2625). A person whose two records
|
|
225
|
+
* were merged answers to two addresses, and only one of them is the
|
|
226
|
+
* document's `email`; the index is what lets a capture on the other one
|
|
227
|
+
* land on the survivor instead of minting the duplicate again. The query
|
|
228
|
+
* stays as the fallback, so a row the index has not seen costs one extra
|
|
229
|
+
* read once and needs no backfill.
|
|
230
|
+
*=========================================*/ const docSnapshot = await findContactByEmail(contactsRef, email);
|
|
231
|
+
if (docSnapshot) {
|
|
232
|
+
var _facet_name, _profile_lifecycleStage;
|
|
233
|
+
/*
|
|
234
|
+
* MERGED INTO THIS GROUP'S FACET, not into the top of the document.
|
|
235
|
+
*
|
|
236
|
+
* `sources`, `interactions`, the tags, the notes and every commercial
|
|
237
|
+
* figure are the HOLDER's own business records: a booking taken by one
|
|
238
|
+
* client of an agency is that client's, and while these lived at the
|
|
239
|
+
* top of a shared row every other client could read them. The identity
|
|
240
|
+
* — the address, and a canonical name for a holder that has set none of
|
|
241
|
+
* its own — stays shared, because that is what makes this one row.
|
|
242
|
+
*/ const facet = readContactFacet(docSnapshot.data(), group.groupId);
|
|
243
|
+
const merged = mergeContactInteraction({
|
|
244
|
+
name: (_facet_name = facet.name) != null ? _facet_name : undefined,
|
|
245
|
+
sources: facet.sources,
|
|
246
|
+
interactions: facet.interactions
|
|
247
|
+
}, {
|
|
248
|
+
source: options.source,
|
|
249
|
+
interaction,
|
|
250
|
+
name: options.name
|
|
251
|
+
});
|
|
252
|
+
/*
|
|
253
|
+
* THE PROFILE, as this door knows it (AGL-2596).
|
|
254
|
+
*
|
|
255
|
+
* Given keys only, so the merge below leaves untouched whatever another
|
|
256
|
+
* door wrote. The stage is the one field with a rule of its own: the
|
|
257
|
+
* door's `initialLifecycleStage` fills an empty stage and advances an
|
|
258
|
+
* earlier one, and never moves anybody back —
|
|
259
|
+
* `advanceContactLifecycleStage` is that rule, applied to the stage
|
|
260
|
+
* this door asked for outright or, failing that, the one already
|
|
261
|
+
* stored. Written only when it comes back with something: a door that
|
|
262
|
+
* named no stage and found none leaves the key absent, which reads as
|
|
263
|
+
* "no stage" rather than as a stage somebody picked.
|
|
264
|
+
*/ const profile = storableProfile(options.facet);
|
|
265
|
+
const advanced = advanceContactLifecycleStage((_profile_lifecycleStage = profile.lifecycleStage) != null ? _profile_lifecycleStage : facet.lifecycleStage, options.initialLifecycleStage);
|
|
266
|
+
if (advanced) profile.lifecycleStage = advanced;
|
|
267
|
+
/*
|
|
268
|
+
* THE FORM MIRROR, bounded (see `HostContact.formIds`).
|
|
269
|
+
*
|
|
270
|
+
* Read off the document the lookup already fetched, so the bound costs
|
|
271
|
+
* no extra read: a person who has come in through twenty forms keeps
|
|
272
|
+
* the twenty, and this capture's form stays on the interaction alone.
|
|
273
|
+
* `arrayUnion` for the add, so a concurrent capture on a sibling site
|
|
274
|
+
* cannot drop this one's id.
|
|
275
|
+
*/ const heldFormIds = Array.isArray(docSnapshot.get(CONTACT_FORM_IDS_FIELD)) ? docSnapshot.get(CONTACT_FORM_IDS_FIELD) : [];
|
|
276
|
+
const formIdToAdd = interaction.formId && !heldFormIds.includes(interaction.formId) && heldFormIds.length < CONTACT_FORM_IDS_CAP ? interaction.formId : null;
|
|
277
|
+
/*
|
|
278
|
+
* THE COMPANY LINK, when the door carried one (AGL-2613).
|
|
279
|
+
*
|
|
280
|
+
* The facet's `companyId` is written with the rest of the profile
|
|
281
|
+
* below, but the facet is not the whole association: the top-level
|
|
282
|
+
* `companyIds` mirror is what the company page queries, and the
|
|
283
|
+
* company's `contactsCount` is what the companies list shows. The
|
|
284
|
+
* planner reads the row as it stands — this holder's previous link,
|
|
285
|
+
* the mirror, the other holders' ids — and says what the mirror
|
|
286
|
+
* becomes and which counts move; the mirror change rides in this same
|
|
287
|
+
* merge-set, and the counts settle after it.
|
|
288
|
+
*/ const link = profile.companyId !== undefined ? planContactCompanyLink(readContactCompanyLink(docSnapshot.data(), group.groupId), profile.companyId) : null;
|
|
289
|
+
const mirror = link ? contactCompanyMirrorValue(link) : undefined;
|
|
290
|
+
await docSnapshot.ref.set(_extends({}, merged.name ? nameSearchFields(merged.name) : {}, profile.phone ? {
|
|
291
|
+
phone: profile.phone
|
|
292
|
+
} : {}, mirror !== undefined ? {
|
|
293
|
+
[CONTACT_COMPANY_IDS_FIELD]: mirror
|
|
294
|
+
} : {}, {
|
|
295
|
+
/*
|
|
296
|
+
* NESTED, not dot-pathed. This is a `set(…, { merge: true })`, and
|
|
297
|
+
* a merge-set treats a key containing dots as a literal field name
|
|
298
|
+
* — only `update()` reads them as paths. The nested form is what
|
|
299
|
+
* Firestore deep-merges, so this writes one holder's facet and
|
|
300
|
+
* leaves every other holder's untouched.
|
|
301
|
+
*/ [CONTACT_FACETS_FIELD]: {
|
|
302
|
+
[group.groupId]: _extends({
|
|
303
|
+
sources: merged.sources,
|
|
304
|
+
interactions: merged.interactions
|
|
305
|
+
}, merged.name ? {
|
|
306
|
+
name: merged.name
|
|
307
|
+
} : {}, campaignIds.length ? {
|
|
308
|
+
campaignIds: FieldValue.arrayUnion(...campaignIds)
|
|
309
|
+
} : {}, tags.length ? {
|
|
310
|
+
tags: FieldValue.arrayUnion(...tags)
|
|
311
|
+
} : {}, profile, options.purchaseCents ? _extends({
|
|
312
|
+
ltvCents: FieldValue.increment(options.purchaseCents),
|
|
313
|
+
ordersCount: FieldValue.increment(1),
|
|
314
|
+
lastPurchaseAtMs: Date.now()
|
|
315
|
+
}, facet.firstPurchaseAtMs ? {} : {
|
|
316
|
+
firstPurchaseAtMs: Date.now()
|
|
317
|
+
}) : {})
|
|
318
|
+
},
|
|
319
|
+
/*
|
|
320
|
+
* ATTRIBUTION GROWS ON THE MERGE BRANCH, which the create-only
|
|
321
|
+
* `hostId` beside it never did — so a person the first site
|
|
322
|
+
* captured and the second site later met read as the first site's
|
|
323
|
+
* alone, forever.
|
|
324
|
+
*
|
|
325
|
+
* `arrayUnion`, so the audience filter "everyone captured on A, B
|
|
326
|
+
* or C" answers with the sites that actually met this person.
|
|
327
|
+
*/ [CAPTURED_BY_HOST_FIELD]: FieldValue.arrayUnion(options.hostId)
|
|
328
|
+
}, formIdToAdd ? {
|
|
329
|
+
[CONTACT_FORM_IDS_FIELD]: FieldValue.arrayUnion(formIdToAdd)
|
|
330
|
+
} : {}, {
|
|
331
|
+
/*
|
|
332
|
+
* AND SO DOES VISIBILITY — by the capture, never by the lookup.
|
|
333
|
+
*
|
|
334
|
+
* This site just collected this person: it has its own relationship
|
|
335
|
+
* with them and may see the row. A site that has never captured
|
|
336
|
+
* them gains nothing here, which is what keeps an agency's clients
|
|
337
|
+
* apart on a document all of them share. The per-interaction
|
|
338
|
+
* `hostId` above is what keeps the HISTORY apart on the same row.
|
|
339
|
+
*/ visibleTo: FieldValue.arrayUnion(...consentGroupScope(group))
|
|
340
|
+
}, consentFields, {
|
|
341
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
342
|
+
}), {
|
|
343
|
+
merge: true
|
|
344
|
+
});
|
|
345
|
+
await settleCompanyContactsCounts(companiesBeside(contactsRef), link);
|
|
346
|
+
return {
|
|
347
|
+
contactId: docSnapshot.id,
|
|
348
|
+
created: false
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/*
|
|
352
|
+
* THE ERASED PERSON DOES NOT COME BACK (AGL-2623).
|
|
353
|
+
*
|
|
354
|
+
* Before the band, on the CREATE branch only — the one read this adds
|
|
355
|
+
* is paid by a capture that would otherwise make a new row, never by
|
|
356
|
+
* the merge above. An erasure wrote a row on this site's suppression
|
|
357
|
+
* list with no address on it; finding that row here is what keeps the
|
|
358
|
+
* next form fill or checkout from rebuilding the record the workspace
|
|
359
|
+
* just erased. The order or booking that triggered the capture still
|
|
360
|
+
* succeeds, exactly as it does when the band refuses: the person bought
|
|
361
|
+
* something, and the merchant's record of the sale is not the CRM's
|
|
362
|
+
* record of the person.
|
|
363
|
+
*/ if (await hostRefusesCaptureForErasure(options.hostId, email, firestore)) {
|
|
364
|
+
return {
|
|
365
|
+
refused: 'erased'
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
// New contact: records-band check via the aggregate counts (cheap; no
|
|
369
|
+
// doc reads) against the owning org's entitlements (AGL-238/890).
|
|
370
|
+
// Metered plans always pass (overage bills via the report-usage
|
|
371
|
+
// cron); only free's hard band drops the CRM record — visibly, via
|
|
372
|
+
// the counter and the console alert (AGL-891). The signup/order that
|
|
373
|
+
// triggered the capture always succeeds either way.
|
|
374
|
+
//
|
|
375
|
+
// THE RECORDS BAND, not the contacts headcount (AGL-2611): companies
|
|
376
|
+
// and deals share this band, so a capture is refused on a Free org
|
|
377
|
+
// whose hundred records are ninety companies and ten people — the same
|
|
378
|
+
// verdict the company drawer would give the ninety-first company.
|
|
379
|
+
// `contactsRef` is handed in so the contacts aggregate is the one this
|
|
380
|
+
// door already reads; the org reference is its parent.
|
|
381
|
+
const orgBilling = await getOrgForHost(options.hostId);
|
|
382
|
+
const orgRef = (_contactsRef_parent = contactsRef.parent) != null ? _contactsRef_parent : firestore.collection('orgs').doc(String((_ref1 = orgBilling == null ? void 0 : orgBilling.orgId) != null ? _ref1 : ''));
|
|
383
|
+
const { crmRecordsCount } = await countCrmRecords(orgRef, contactsRef);
|
|
384
|
+
const quota = checkCrmRecordsQuota((_ref2 = orgBilling == null ? void 0 : orgBilling.org) != null ? _ref2 : null, crmRecordsCount);
|
|
385
|
+
if (!quota.allowed) {
|
|
386
|
+
await hostRef.collection('counters').doc('contactsDropped').set({
|
|
387
|
+
total: FieldValue.increment(1)
|
|
388
|
+
}, {
|
|
389
|
+
merge: true
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
refused: 'band'
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/*
|
|
396
|
+
* The profile on a create, with the door's stage floor applied the same
|
|
397
|
+
* way as on a merge — there is no stored stage yet, so the floor is what
|
|
398
|
+
* a door that named one writes. A `null` address is dropped rather than
|
|
399
|
+
* written: there is nothing on a new document for it to clear.
|
|
400
|
+
*/ const profile = storableProfile(options.facet);
|
|
401
|
+
const advanced = advanceContactLifecycleStage(profile.lifecycleStage, options.initialLifecycleStage);
|
|
402
|
+
if (advanced) profile.lifecycleStage = advanced;
|
|
403
|
+
if (profile.address === null) delete profile.address;
|
|
404
|
+
const created = await contactsRef.add(_extends({
|
|
405
|
+
hostId: options.hostId,
|
|
406
|
+
/*
|
|
407
|
+
* WHICH SITES HAVE MET THIS PERSON — attribution, and separate from
|
|
408
|
+
* consent above.
|
|
409
|
+
*
|
|
410
|
+
* The scalar `hostId` beside it names the FIRST capturing site and is
|
|
411
|
+
* never rewritten, which is provenance for the ROW. This array is the
|
|
412
|
+
* one that grows and the one an audience query can filter on.
|
|
413
|
+
*/ [CAPTURED_BY_HOST_FIELD]: [
|
|
414
|
+
options.hostId
|
|
415
|
+
]
|
|
416
|
+
}, interaction.formId ? {
|
|
417
|
+
[CONTACT_FORM_IDS_FIELD]: [
|
|
418
|
+
interaction.formId
|
|
419
|
+
]
|
|
420
|
+
} : {}, {
|
|
421
|
+
/*
|
|
422
|
+
* THE CAPTURING GROUP, not the whole org.
|
|
423
|
+
*
|
|
424
|
+
* Stamping `['org']` here made every contact readable by every site in
|
|
425
|
+
* the account on the day it was created — so an agency's twelve clients
|
|
426
|
+
* shared one address book by default, and closing the missing-field
|
|
427
|
+
* fail-open would not have touched it, because the field was present
|
|
428
|
+
* and said so.
|
|
429
|
+
*
|
|
430
|
+
* A group of one — the default — is this site alone. A declared group
|
|
431
|
+
* is the sites that already present as one sender. Widening beyond that
|
|
432
|
+
* is available and is an ACT: an org may set `defaultResourceScope` to
|
|
433
|
+
* `'org'`, or a later capture on a sibling site unions that site in.
|
|
434
|
+
*/ visibleTo: (orgBilling == null ? void 0 : (_orgBilling_org = orgBilling.org) == null ? void 0 : _orgBilling_org.defaultResourceScope) === 'org' ? [
|
|
435
|
+
ORG_SCOPE_TOKEN
|
|
436
|
+
] : consentGroupScope(group),
|
|
437
|
+
email
|
|
438
|
+
}, options.name ? nameSearchFields(options.name.slice(0, 120)) : {}, profile.phone ? {
|
|
439
|
+
phone: profile.phone
|
|
440
|
+
} : {}, profile.companyId ? {
|
|
441
|
+
[CONTACT_COMPANY_IDS_FIELD]: [
|
|
442
|
+
profile.companyId
|
|
443
|
+
]
|
|
444
|
+
} : {}, {
|
|
445
|
+
// The facet this capture creates. Everything a holder owns lives under
|
|
446
|
+
// its own group id; the address and the canonical name above are the
|
|
447
|
+
// only shared identity.
|
|
448
|
+
[CONTACT_FACETS_FIELD]: {
|
|
449
|
+
[group.groupId]: _extends({
|
|
450
|
+
sources: {
|
|
451
|
+
[options.source]: true
|
|
452
|
+
},
|
|
453
|
+
interactions: [
|
|
454
|
+
interaction
|
|
455
|
+
],
|
|
456
|
+
tags
|
|
457
|
+
}, campaignIds.length ? {
|
|
458
|
+
campaignIds
|
|
459
|
+
} : {}, customFacet, options.name ? {
|
|
460
|
+
name: options.name.slice(0, 120)
|
|
461
|
+
} : {}, profile, options.purchaseCents ? {
|
|
462
|
+
ltvCents: options.purchaseCents,
|
|
463
|
+
ordersCount: 1,
|
|
464
|
+
lastPurchaseAtMs: Date.now(),
|
|
465
|
+
firstPurchaseAtMs: Date.now()
|
|
466
|
+
} : {})
|
|
467
|
+
}
|
|
468
|
+
}, consentFields, {
|
|
469
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
470
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
471
|
+
}));
|
|
472
|
+
// The address now resolves to this row (AGL-2625). Its own catch inside
|
|
473
|
+
// the writer, so an index the capture could not reach costs nothing.
|
|
474
|
+
await writeContactEmailIndex(emailIndexBeside(contactsRef), created.id, [
|
|
475
|
+
email
|
|
476
|
+
]);
|
|
477
|
+
// The company the door named now has one more contact naming it
|
|
478
|
+
// (AGL-2613) — the fresh row's plan is the trivial one, nothing held
|
|
479
|
+
// before and nothing held elsewhere.
|
|
480
|
+
if (profile.companyId) {
|
|
481
|
+
await settleCompanyContactsCounts(companiesBeside(contactsRef), planContactCompanyLink({
|
|
482
|
+
companyId: null,
|
|
483
|
+
companyIds: [],
|
|
484
|
+
heldElsewhere: []
|
|
485
|
+
}, profile.companyId));
|
|
486
|
+
}
|
|
487
|
+
/*
|
|
488
|
+
* ATTRIBUTED ON CREATION ONLY, and therefore below the band gate rather
|
|
489
|
+
* than above it — the opposite placement to the order join at the top of
|
|
490
|
+
* this function, deliberately.
|
|
491
|
+
*
|
|
492
|
+
* The order join sits above because it credits MONEY, which is real
|
|
493
|
+
* whether or not a CRM record was kept for the buyer, and a Free org past
|
|
494
|
+
* its included band would otherwise silently drop the revenue with the
|
|
495
|
+
* contact. This credits a contact, which does not exist when the band
|
|
496
|
+
* dropped it: attributing one above the gate would report a campaign as
|
|
497
|
+
* having produced people the customer cannot see anywhere in the console.
|
|
498
|
+
*
|
|
499
|
+
* The existing-contact branch above returns without reaching here, which
|
|
500
|
+
* is the same rule `addHostLead` applies — an interaction appended to
|
|
501
|
+
* somebody the site already held is another visit, not a new person, and
|
|
502
|
+
* crediting it would let the most recent campaign re-earn the whole list.
|
|
503
|
+
*/ if (options.campaignTouch) {
|
|
504
|
+
await attributeCampaignConversion({
|
|
505
|
+
hostId: options.hostId,
|
|
506
|
+
kind: 'contact',
|
|
507
|
+
refId: created.id,
|
|
508
|
+
touch: options.campaignTouch,
|
|
509
|
+
convertedAtMs: interaction.atMs
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
/*
|
|
513
|
+
* REPORTED ON CREATION ONLY, below the band gate for the reason the
|
|
514
|
+
* attribution above is: a contact the band dropped does not exist, and
|
|
515
|
+
* an automation told about it would act on a person the console cannot
|
|
516
|
+
* show. The merge branch returned long before this line, so a repeat
|
|
517
|
+
* visit by somebody already held is never announced as a new contact.
|
|
518
|
+
*/ if (options.onCreated) {
|
|
519
|
+
await Promise.resolve(options.onCreated(_extends({
|
|
520
|
+
contactId: created.id,
|
|
521
|
+
hostId: options.hostId,
|
|
522
|
+
email
|
|
523
|
+
}, options.name ? {
|
|
524
|
+
name: options.name.slice(0, 120)
|
|
525
|
+
} : {}, {
|
|
526
|
+
source: options.source,
|
|
527
|
+
campaignIds
|
|
528
|
+
}, profile.lifecycleStage ? {
|
|
529
|
+
lifecycleStage: profile.lifecycleStage
|
|
530
|
+
} : {}))).catch((error)=>{
|
|
531
|
+
console.error('upsertHostContact onCreated failed', error);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
contactId: created.id,
|
|
536
|
+
created: true
|
|
537
|
+
};
|
|
538
|
+
} catch (error) {
|
|
539
|
+
console.error('upsertHostContact failed', error);
|
|
540
|
+
return {
|
|
541
|
+
refused: 'error'
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
//# sourceMappingURL=upsert-contact.js.map
|