@aglyn/tenant-data-admin 1.0.0-beta.219 → 1.0.0-beta.220
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/package.json +8 -8
- package/src/lib/server/api-http.js +5 -1
- package/src/lib/server/api-http.js.map +1 -1
- package/src/lib/server/api-v1-kit.d.ts +7 -0
- package/src/lib/server/api-v1-kit.js.map +1 -1
- package/src/lib/server/api-v1-resources.d.ts +1 -2
- package/src/lib/server/api-v1-resources.js +0 -1
- package/src/lib/server/api-v1-resources.js.map +1 -1
- package/src/lib/server/campaign-conversion-attribution.d.ts +1 -1
- package/src/lib/server/campaign-conversion-attribution.js +3 -3
- package/src/lib/server/campaign-conversion-attribution.js.map +1 -1
- package/src/lib/server/card-payment-velocity.d.ts +9 -0
- package/src/lib/server/card-payment-velocity.js +28 -2
- package/src/lib/server/card-payment-velocity.js.map +1 -1
- package/src/lib/server/consent-group-carry.js +1 -1
- package/src/lib/server/consent-group-carry.js.map +1 -1
- package/src/lib/server/consent-group-change.js +2 -2
- package/src/lib/server/consent-group-change.js.map +1 -1
- package/src/lib/server/dynamic-list-materialize.js +3 -3
- package/src/lib/server/dynamic-list-materialize.js.map +1 -1
- package/src/lib/server/email-flow-gate.js +2 -2
- package/src/lib/server/email-flow-gate.js.map +1 -1
- package/src/lib/server/email-suppression.js +1 -1
- package/src/lib/server/email-suppression.js.map +1 -1
- package/src/lib/server/host-visitor-records.d.ts +32 -4
- package/src/lib/server/host-visitor-records.js +73 -14
- package/src/lib/server/host-visitor-records.js.map +1 -1
- package/src/lib/server/hosted-page-review.d.ts +27 -0
- package/src/lib/server/hosted-page-review.js +149 -12
- package/src/lib/server/hosted-page-review.js.map +1 -1
- package/src/lib/server/media-cdn-rate-limit.d.ts +6 -3
- package/src/lib/server/media-cdn-rate-limit.js +6 -3
- package/src/lib/server/media-cdn-rate-limit.js.map +1 -1
- package/src/lib/server/member-email-aliases.d.ts +9 -1
- package/src/lib/server/member-email-aliases.js +25 -1
- package/src/lib/server/member-email-aliases.js.map +1 -1
- package/src/lib/server/notifications.d.ts +14 -0
- package/src/lib/server/notifications.js +29 -0
- package/src/lib/server/notifications.js.map +1 -1
- package/src/lib/server/operator-alerts.d.ts +7 -1
- package/src/lib/server/operator-alerts.js +5 -3
- package/src/lib/server/operator-alerts.js.map +1 -1
- package/src/lib/server/org-containers.d.ts +66 -0
- package/src/lib/server/org-containers.js +95 -0
- package/src/lib/server/org-containers.js.map +1 -0
- package/src/lib/server/org-member-notice.js +3 -2
- package/src/lib/server/org-member-notice.js.map +1 -1
- package/src/lib/server/payment-fraud-signal.js +5 -1
- package/src/lib/server/payment-fraud-signal.js.map +1 -1
- package/src/lib/server/payment-provider-stripe-connect-payouts.js +35 -1
- package/src/lib/server/payment-provider-stripe-connect-payouts.js.map +1 -1
- package/src/lib/server/payment-risk-record.d.ts +1 -1
- package/src/lib/server/payment-risk-record.js +3 -1
- package/src/lib/server/payment-risk-record.js.map +1 -1
- package/src/lib/server/platform-marketing-consent.d.ts +1 -1
- package/src/lib/server/platform-marketing-consent.js +1 -1
- package/src/lib/server/platform-marketing-consent.js.map +1 -1
- package/src/lib/server/risk-notice.d.ts +32 -1
- package/src/lib/server/risk-notice.js +90 -28
- package/src/lib/server/risk-notice.js.map +1 -1
- package/src/lib/server/sending-domains.d.ts +17 -0
- package/src/lib/server/sending-domains.js +28 -4
- package/src/lib/server/sending-domains.js.map +1 -1
- package/src/lib/server/sso-enforcement.js +7 -2
- package/src/lib/server/sso-enforcement.js.map +1 -1
- package/src/lib/server/staff-alert-email.js +4 -2
- package/src/lib/server/staff-alert-email.js.map +1 -1
- package/src/lib/server/{email-topic-confirmation.js → topic-subscriptions.js} +15 -11
- package/src/lib/server/topic-subscriptions.js.map +1 -0
- package/src/lib/server/upsert-contact.js +2 -2
- package/src/lib/server/upsert-contact.js.map +1 -1
- package/src/tenancy.d.ts +1 -3
- package/src/tenancy.js +1 -7
- package/src/tenancy.js.map +1 -1
- package/src/lib/server/contact-email-engagement.d.ts +0 -32
- package/src/lib/server/contact-email-engagement.js +0 -145
- package/src/lib/server/contact-email-engagement.js.map +0 -1
- package/src/lib/server/crm-booking-activity.d.ts +0 -86
- package/src/lib/server/crm-booking-activity.js +0 -257
- package/src/lib/server/crm-booking-activity.js.map +0 -1
- package/src/lib/server/crm-inbound-email.d.ts +0 -168
- package/src/lib/server/crm-inbound-email.js +0 -282
- package/src/lib/server/crm-inbound-email.js.map +0 -1
- package/src/lib/server/email-topic-confirmation.js.map +0 -1
- /package/src/lib/server/{email-topic-confirmation.d.ts → topic-subscriptions.d.ts} +0 -0
package/src/tenancy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/tenant/data/admin/src/tenancy.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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\nexport * from './lib/server/analytics-retention'\nexport * from './lib/server/api-http'\nexport * from './lib/server/api-keys'\nexport * from './lib/server/account-emails'\nexport * from './lib/server/auth-pools'\nexport * from './lib/server/client-error-report'\n// The merchant's payment account, as a contract. Its adapter is not exported:\n// a plugin asks the contract, and the doors that only need the readiness rule\n// import `@aglyn/tenant-data-admin/server/payment-provider` directly, which\n// loads no Firebase.\nexport {\n merchantAccountIsReady,\n merchantAccountReadiness,\n paymentProvider,\n} from './lib/server/payment-provider'\nexport type {\n MerchantAccountReadiness,\n MerchantAccountReadinessInput,\n PaymentMode,\n PaymentProvider,\n PaymentProviderEvent,\n} from './lib/server/payment-provider'\nexport * from './lib/server/auth-handoff'\nexport * from './lib/server/console-domains'\nexport * from './lib/server/dns-probe'\nexport * from './lib/server/sending-domains'\nexport * from './lib/server/sender-readiness'\nexport * from './lib/server/host-sending-domain'\nexport * from './lib/server/sending-domain-debt'\nexport * from './lib/server/sending-domain-recheck'\n// The hosting seam itself, so a door can ask whether this deployment is able\n// to register a name at all before it promises the caller one.\n// `workspace-domains` re-exports the pieces its own callers share with it; the\n// provider behind it is not among them.\nexport { domainProvider } from './lib/server/domain-provider'\nexport type {\n DomainProvider,\n DomainProviderId,\n DomainScope,\n} from './lib/server/domain-provider'\nexport * from './lib/server/platform-domain-names'\nexport * from './lib/server/consume-once'\nexport * from './lib/server/csp-aggregate'\nexport * from './lib/server/data-storage-gate'\nexport * from './lib/server/contact-suppression'\nexport * from './lib/server/email-suppression'\n// The double opt-in quarantine, after the module whose keying it shares and\n// whose `filterTopicSendable` is the reader that makes it a quarantine rather\n// than a stored fact.\nexport * from './lib/server/email-topic-confirmation'\nexport * from './lib/server/email-unsubscribe-link'\nexport * from './lib/server/email-flow-gate'\n// Side-effecting on purpose, for the reason `email-send-rate` below is:\n// evaluating this module INSTALLS the marketing gate on `sendEmail`, so the\n// unsubscribe header, the suppression check and the frequency ceiling reach\n// every bulk sender without a call at each server entrypoint.\nexport * from './lib/server/email-marketing-gate'\n// A consent group change (AGL-3320): the carries of the three stores above,\n// and the executor that runs a change around them.\nexport * from './lib/server/consent-group-carry'\nexport * from './lib/server/consent-group-change'\n// A person's refusal kept after the record it was written on is deleted\n// (AGL-3338): the delete's write, the list gate's read, and the change's carry.\nexport * from './lib/server/retained-refusals'\nexport * from './lib/server/sms-keywords'\nexport * from './lib/server/document-id'\nexport * from './lib/server/collection-preview-token'\nexport * from './lib/server/edit-access-authz'\nexport * from './lib/server/edit-access-token'\nexport * from './lib/server/edit-hint-token'\nexport * from './lib/server/email-metering'\n// Side-effecting on purpose (AGL-2409): evaluating this module INSTALLS the\n// platform send-rate governor on `sendEmail`. Every server surface already\n// imports this barrel, so nothing has to remember to call an installer.\nexport * from './lib/server/campaign-conversion-attribution'\nexport * from './lib/server/email-send-rate'\n// Side-effecting on purpose, for the same reason (AGL-3328): evaluating this\n// module INSTALLS the deliverability preflight on `sendEmail`, so a recipient\n// whose domain takes no mail is refused, and bulk mail behind a gateway that\n// refused its sending domain twice is held, on every send path at once.\nexport * from './lib/server/email-deliverability'\nexport * from './lib/server/email-sender-reputation'\nexport * from './lib/server/erase'\nexport * from './lib/server/erase-person'\nexport * from './lib/server/dynamic-list-materialize'\nexport * from './lib/server/firebase-admin'\nexport * from './lib/server/free-workspace-cap'\nexport * from './lib/server/ga4-measurement-protocol'\nexport * from './lib/server/host-memberships'\nexport * from './lib/server/host-visitor-records'\nexport * from './lib/server/list-members'\nexport * from './lib/server/lockdown'\nexport * from './lib/server/token-revocation'\nexport * from './lib/server/tenant-write-lockdown'\nexport * from './lib/server/outbound-send-review'\n// The same review for a published page, read where it is composed (AGL-3356).\nexport * from './lib/server/hosted-page-review'\n// Every hold, flag, lock and pause tells owners and staff through one seam (AGL-3368).\nexport * from './lib/server/risk-notice'\nexport * from './lib/server/host-email-tokens'\nexport * from './lib/server/notifications'\n// Every operator alert raises through one entry point, and every health check\n// remembers its state (AGL-3377).\nexport * from './lib/server/operator-alerts'\nexport * from './lib/server/operator-health'\nexport * from './lib/server/org-billing'\nexport * from './lib/server/organizations'\nexport * from './lib/server/duplicate-activity'\nexport * from './lib/server/duplicate-resource'\nexport * from './lib/server/workspace-domains'\nexport * from './lib/server/membership-recover-throttle'\nexport * from './lib/server/password-reset-throttle'\nexport * from './lib/server/rate-limit-store'\nexport * from './lib/server/verify-email-cooldown'\nexport * from './lib/server/vercel-drain-signature'\nexport * from './lib/server/vercel-log-drain'\nexport * from './lib/server/visitor-write-rate-limit'\nexport * from './lib/server/card-payment-velocity'\nexport * from './lib/server/console-api-rate-limit'\nexport * from './lib/server/realm-plugins'\nexport * from './lib/server/release-flags'\nexport * from './lib/server/resolve-people'\nexport * from './lib/server/member-photo'\nexport * from './lib/server/personal-data-export'\nexport * from './lib/server/media-delivery'\nexport * from './lib/server/media-download-tokens'\nexport * from './lib/server/media-quarantine'\nexport * from './lib/server/media-signing'\nexport * from './lib/server/media-storage-path'\nexport * from './lib/server/safe-equal'\nexport * from './lib/server/media-strong-digest'\nexport * from './lib/server/media-tombstone'\nexport * from './lib/server/media-variants'\nexport * from './lib/server/serve-media-cdn'\nexport * from './lib/server/serve-plugin-fetch'\nexport * from './lib/server/configured-url-fetch'\nexport * from './lib/server/sso-break-glass-owners'\nexport * from './lib/server/sso-domain-policy'\nexport * from './lib/server/sso-enforcement'\nexport * from './lib/server/sso-provisioning'\nexport * from './lib/server/update-existing'\nexport * from './lib/server/contact-company-link'\nexport * from './lib/server/contact-email-index'\nexport * from './lib/server/upsert-contact'\nexport * from './lib/server/crm-records'\n// The `nextTaskAtMs` a contact, a company and a deal carry, recomputed from\n// their open tasks by every server-side task writer (AGL-2661).\nexport * from './lib/server/crm-next-activity'\n// Where a one-to-one email's activity row lives and how the delivery\n// webhook advances it (AGL-2615).\nexport * from './lib/server/crm-email-activity'\n// What a confirmed booking files on the CRM record — the meeting and the\n// follow-up (AGL-2660).\nexport * from './lib/server/crm-booking-activity'\n// The org's email capture token and the filing of one received message\n// on the record it was with (AGL-2657).\nexport * from './lib/server/crm-inbound-email'\n// A member's own addresses in a workspace, and the signed link that\n// confirms one (AGL-2975).\nexport * from './lib/server/member-email-aliases'\nexport * from './lib/server/user-profiles'\n// The one \"New account\" staff notice per account, keyed on the auth\n// record's creation (AGL-3225).\nexport * from './lib/server/new-account-announcement'\nexport * from './lib/server/legal-acceptance'\n// The person's decision about the platform's own product email, on their\n// document and on the operator's marketing contact (AGL-3185).\nexport * from './lib/server/platform-marketing-consent'\n"],"names":["merchantAccountIsReady","merchantAccountReadiness","paymentProvider","domainProvider"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/data/admin/src/tenancy.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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\nexport * from './lib/server/analytics-retention'\nexport * from './lib/server/api-http'\nexport * from './lib/server/api-keys'\nexport * from './lib/server/account-emails'\nexport * from './lib/server/auth-pools'\nexport * from './lib/server/client-error-report'\n// The merchant's payment account, as a contract. Its adapter is not exported:\n// a plugin asks the contract, and the doors that only need the readiness rule\n// import `@aglyn/tenant-data-admin/server/payment-provider` directly, which\n// loads no Firebase.\nexport {\n merchantAccountIsReady,\n merchantAccountReadiness,\n paymentProvider,\n} from './lib/server/payment-provider'\nexport type {\n MerchantAccountReadiness,\n MerchantAccountReadinessInput,\n PaymentMode,\n PaymentProvider,\n PaymentProviderEvent,\n} from './lib/server/payment-provider'\nexport * from './lib/server/auth-handoff'\nexport * from './lib/server/console-domains'\nexport * from './lib/server/dns-probe'\nexport * from './lib/server/sending-domains'\nexport * from './lib/server/sender-readiness'\nexport * from './lib/server/host-sending-domain'\nexport * from './lib/server/sending-domain-debt'\nexport * from './lib/server/sending-domain-recheck'\n// The hosting seam itself, so a door can ask whether this deployment is able\n// to register a name at all before it promises the caller one.\n// `workspace-domains` re-exports the pieces its own callers share with it; the\n// provider behind it is not among them.\nexport { domainProvider } from './lib/server/domain-provider'\nexport type {\n DomainProvider,\n DomainProviderId,\n DomainScope,\n} from './lib/server/domain-provider'\nexport * from './lib/server/platform-domain-names'\nexport * from './lib/server/consume-once'\nexport * from './lib/server/csp-aggregate'\nexport * from './lib/server/data-storage-gate'\nexport * from './lib/server/contact-suppression'\nexport * from './lib/server/email-suppression'\n// The double opt-in quarantine, after the module whose keying it shares and\n// whose `filterTopicSendable` is the reader that makes it a quarantine rather\n// than a stored fact.\nexport * from './lib/server/topic-subscriptions'\nexport * from './lib/server/email-unsubscribe-link'\nexport * from './lib/server/email-flow-gate'\n// Side-effecting on purpose, for the reason `email-send-rate` below is:\n// evaluating this module INSTALLS the marketing gate on `sendEmail`, so the\n// unsubscribe header, the suppression check and the frequency ceiling reach\n// every bulk sender without a call at each server entrypoint.\nexport * from './lib/server/email-marketing-gate'\n// A consent group change (AGL-3320): the carries of the three stores above,\n// and the executor that runs a change around them.\nexport * from './lib/server/consent-group-carry'\nexport * from './lib/server/consent-group-change'\n// A person's refusal kept after the record it was written on is deleted\n// (AGL-3338): the delete's write, the list gate's read, and the change's carry.\nexport * from './lib/server/retained-refusals'\nexport * from './lib/server/sms-keywords'\nexport * from './lib/server/document-id'\nexport * from './lib/server/collection-preview-token'\nexport * from './lib/server/edit-access-authz'\nexport * from './lib/server/edit-access-token'\nexport * from './lib/server/edit-hint-token'\nexport * from './lib/server/email-metering'\n// Side-effecting on purpose (AGL-2409): evaluating this module INSTALLS the\n// platform send-rate governor on `sendEmail`. Every server surface already\n// imports this barrel, so nothing has to remember to call an installer.\nexport * from './lib/server/campaign-conversion-attribution'\nexport * from './lib/server/email-send-rate'\n// Side-effecting on purpose, for the same reason (AGL-3328): evaluating this\n// module INSTALLS the deliverability preflight on `sendEmail`, so a recipient\n// whose domain takes no mail is refused, and bulk mail behind a gateway that\n// refused its sending domain twice is held, on every send path at once.\nexport * from './lib/server/email-deliverability'\nexport * from './lib/server/email-sender-reputation'\nexport * from './lib/server/erase'\nexport * from './lib/server/erase-person'\nexport * from './lib/server/dynamic-list-materialize'\nexport * from './lib/server/firebase-admin'\nexport * from './lib/server/free-workspace-cap'\nexport * from './lib/server/ga4-measurement-protocol'\nexport * from './lib/server/host-memberships'\nexport * from './lib/server/host-visitor-records'\nexport * from './lib/server/list-members'\nexport * from './lib/server/lockdown'\nexport * from './lib/server/token-revocation'\nexport * from './lib/server/tenant-write-lockdown'\nexport * from './lib/server/outbound-send-review'\n// The same review for a published page, read where it is composed (AGL-3356).\nexport * from './lib/server/hosted-page-review'\n// Every hold, flag, lock and pause tells owners and staff through one seam (AGL-3368).\nexport * from './lib/server/risk-notice'\nexport * from './lib/server/host-email-tokens'\nexport * from './lib/server/notifications'\n// Every operator alert raises through one entry point, and every health check\n// remembers its state (AGL-3377).\nexport * from './lib/server/operator-alerts'\nexport * from './lib/server/operator-health'\nexport * from './lib/server/org-billing'\nexport * from './lib/server/organizations'\nexport * from './lib/server/duplicate-activity'\nexport * from './lib/server/duplicate-resource'\nexport * from './lib/server/workspace-domains'\nexport * from './lib/server/membership-recover-throttle'\nexport * from './lib/server/password-reset-throttle'\nexport * from './lib/server/rate-limit-store'\nexport * from './lib/server/verify-email-cooldown'\nexport * from './lib/server/vercel-drain-signature'\nexport * from './lib/server/vercel-log-drain'\nexport * from './lib/server/visitor-write-rate-limit'\nexport * from './lib/server/card-payment-velocity'\nexport * from './lib/server/console-api-rate-limit'\nexport * from './lib/server/realm-plugins'\nexport * from './lib/server/release-flags'\nexport * from './lib/server/resolve-people'\nexport * from './lib/server/member-photo'\nexport * from './lib/server/personal-data-export'\nexport * from './lib/server/media-delivery'\nexport * from './lib/server/media-download-tokens'\nexport * from './lib/server/media-quarantine'\nexport * from './lib/server/media-signing'\nexport * from './lib/server/media-storage-path'\nexport * from './lib/server/safe-equal'\nexport * from './lib/server/media-strong-digest'\nexport * from './lib/server/media-tombstone'\nexport * from './lib/server/media-variants'\nexport * from './lib/server/serve-media-cdn'\nexport * from './lib/server/serve-plugin-fetch'\nexport * from './lib/server/configured-url-fetch'\nexport * from './lib/server/sso-break-glass-owners'\nexport * from './lib/server/sso-domain-policy'\nexport * from './lib/server/sso-enforcement'\nexport * from './lib/server/sso-provisioning'\nexport * from './lib/server/update-existing'\nexport * from './lib/server/contact-company-link'\nexport * from './lib/server/contact-email-index'\nexport * from './lib/server/upsert-contact'\nexport * from './lib/server/crm-records'\n// The `nextTaskAtMs` a contact, a company and a deal carry, recomputed from\n// their open tasks by every server-side task writer (AGL-2661).\nexport * from './lib/server/crm-next-activity'\n// Where a one-to-one email's activity row lives and how the delivery\n// webhook advances it (AGL-2615).\nexport * from './lib/server/crm-email-activity'\n// A member's own addresses in a workspace, and the signed link that\n// confirms one (AGL-2975).\nexport * from './lib/server/member-email-aliases'\nexport * from './lib/server/user-profiles'\n// The one \"New account\" staff notice per account, keyed on the auth\n// record's creation (AGL-3225).\nexport * from './lib/server/new-account-announcement'\nexport * from './lib/server/legal-acceptance'\n// The person's decision about the platform's own product email, on their\n// document and on the operator's marketing contact (AGL-3185).\nexport * from './lib/server/platform-marketing-consent'\n"],"names":["merchantAccountIsReady","merchantAccountReadiness","paymentProvider","domainProvider"],"mappings":"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"}
|
|
@@ -1,32 +0,0 @@
|
|
|
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 type { EmailDeliveryEventOutcome } from './email-delivery-log';
|
|
18
|
-
/** The facet field the stamp lives under. */
|
|
19
|
-
export declare const CONTACT_EMAIL_ENGAGEMENT_FIELD = "lastEmailEngagementAtMs";
|
|
20
|
-
/**
|
|
21
|
-
* Stamps the sending site's contact facet for every person these outcomes
|
|
22
|
-
* say engaged for the first time with a message.
|
|
23
|
-
*
|
|
24
|
-
* @param hostId the site the campaign went out from — the `hostId` tag the
|
|
25
|
-
* send stamped, which is the only tenant identity a delivery event carries.
|
|
26
|
-
* @returns how many contact documents were written.
|
|
27
|
-
*/
|
|
28
|
-
export declare function recordContactEmailEngagement(args: {
|
|
29
|
-
hostId: string;
|
|
30
|
-
outcomes: readonly EmailDeliveryEventOutcome[];
|
|
31
|
-
firestore?: any;
|
|
32
|
-
}): Promise<number>;
|
|
@@ -1,145 +0,0 @@
|
|
|
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 CONTACT'S OWN ENGAGEMENT STAMP (AGL-2616):
|
|
18
|
-
* `facets.{groupId}.lastEmailEngagementAtMs` on `orgs/{orgId}/contacts`.
|
|
19
|
-
*
|
|
20
|
-
* ## What it answers
|
|
21
|
-
*
|
|
22
|
-
* "When did this person last read one of OUR campaigns?" — on the record
|
|
23
|
-
* page as "Last engaged", as a column on the list, and as the audience rule
|
|
24
|
-
* `engagedWithinDays`. The per-person rollup on `emailDeliveries/{key}`
|
|
25
|
-
* already answers "when did this address last read anything from anybody",
|
|
26
|
-
* which is the right question for a sunset that refuses to mail cold
|
|
27
|
-
* addresses and the wrong one for a re-engagement audience: it moves when
|
|
28
|
-
* the person opens a receipt, an invite, or a sibling business's newsletter
|
|
29
|
-
* on the same shared row. This stamp moves only when they open or click a
|
|
30
|
-
* campaign this holder's own sites sent.
|
|
31
|
-
*
|
|
32
|
-
* ## What one webhook event costs
|
|
33
|
-
*
|
|
34
|
-
* The same bound the rollup keeps: only an event that is the FIRST of its
|
|
35
|
-
* type for its message reaches here, which `recordEmailDeliveryEvent`'s
|
|
36
|
-
* transaction already decided. A reader opening one newsletter six times is
|
|
37
|
-
* one transaction, and a replayed event finds its type already recorded and
|
|
38
|
-
* contributes nothing. Inside that bound the cost per person is one org
|
|
39
|
-
* resolution and one group resolution per batch, then one keyed query and
|
|
40
|
-
* at most one update per person.
|
|
41
|
-
*
|
|
42
|
-
* ## Forward-only, in a transaction
|
|
43
|
-
*
|
|
44
|
-
* Provider events are unordered and at-least-once, so an old instant
|
|
45
|
-
* arriving late must not overwrite a fresh stamp — the same reasoning
|
|
46
|
-
* `recordPersonEngagement` gives. The query for the contact rides inside
|
|
47
|
-
* the transaction so the compare-and-write is atomic against a second event
|
|
48
|
-
* for the same person landing in another instance.
|
|
49
|
-
*
|
|
50
|
-
* ## Only a contact this site may see, and only this holder's facet
|
|
51
|
-
*
|
|
52
|
-
* The contact row is shared by every site in the org. The stamp is written
|
|
53
|
-
* under the sending site's consent group, never at the top of the document,
|
|
54
|
-
* and only onto a row whose `visibleTo` admits the site — a person another
|
|
55
|
-
* holder captured and this site never met is not this site's contact, and a
|
|
56
|
-
* write onto their row would mint a facet for a holder that does not hold
|
|
57
|
-
* them. `updatedAt` is left alone: an open is something the person did, not
|
|
58
|
-
* an edit the team made, and a list sorted on recency must not reshuffle on
|
|
59
|
-
* every mailbox prefetch.
|
|
60
|
-
*
|
|
61
|
-
* ## Never throws
|
|
62
|
-
*
|
|
63
|
-
* Best-effort for the reason everything on the webhook path is: a stamp that
|
|
64
|
-
* failed loses a fact a page can live without, and a stamp that threw would
|
|
65
|
-
* lose the provider's acknowledgement and teach it to retry the whole event.
|
|
66
|
-
*/ import { contactFacetPath, normalizeContactEmail, readContactFacet, visibleToHost } from "@aglyn/aglyn/server";
|
|
67
|
-
import { findContactByEmail } from "./contact-email-index.js";
|
|
68
|
-
import { firebaseAdmin } from "./firebase-admin.js";
|
|
69
|
-
import { consentGroupForSite, orgDataCollectionForHost } from "./organizations.js";
|
|
70
|
-
/** The facet field the stamp lives under. */ export const CONTACT_EMAIL_ENGAGEMENT_FIELD = 'lastEmailEngagementAtMs';
|
|
71
|
-
/** The event types that count as a person engaging with a campaign. */ const ENGAGEMENT_TYPES = new Set([
|
|
72
|
-
'opened',
|
|
73
|
-
'clicked'
|
|
74
|
-
]);
|
|
75
|
-
/**
|
|
76
|
-
* Stamps the sending site's contact facet for every person these outcomes
|
|
77
|
-
* say engaged for the first time with a message.
|
|
78
|
-
*
|
|
79
|
-
* @param hostId the site the campaign went out from — the `hostId` tag the
|
|
80
|
-
* send stamped, which is the only tenant identity a delivery event carries.
|
|
81
|
-
* @returns how many contact documents were written.
|
|
82
|
-
*/ export async function recordContactEmailEngagement(args) {
|
|
83
|
-
var _args_hostId, _args_firestore;
|
|
84
|
-
const hostId = String((_args_hostId = args.hostId) != null ? _args_hostId : '');
|
|
85
|
-
if (!hostId) return 0;
|
|
86
|
-
/** Address → the newest engagement instant in this batch. */ const byEmail = new Map();
|
|
87
|
-
for (const outcome of args.outcomes){
|
|
88
|
-
var _byEmail_get;
|
|
89
|
-
if (!outcome.firstOfType) continue;
|
|
90
|
-
if (!ENGAGEMENT_TYPES.has(outcome.type)) continue;
|
|
91
|
-
const email = normalizeContactEmail(outcome.to);
|
|
92
|
-
const at = Number(outcome.at);
|
|
93
|
-
if (!email || !Number.isFinite(at) || at <= 0) continue;
|
|
94
|
-
byEmail.set(email, Math.max((_byEmail_get = byEmail.get(email)) != null ? _byEmail_get : 0, at));
|
|
95
|
-
}
|
|
96
|
-
if (!byEmail.size) return 0;
|
|
97
|
-
let contactsRef;
|
|
98
|
-
let groupId;
|
|
99
|
-
try {
|
|
100
|
-
contactsRef = await orgDataCollectionForHost(hostId, 'contacts');
|
|
101
|
-
groupId = (await consentGroupForSite(hostId)).groupId;
|
|
102
|
-
} catch (error) {
|
|
103
|
-
console.error('[contact-email-engagement] site could not be resolved', hostId, error);
|
|
104
|
-
return 0;
|
|
105
|
-
}
|
|
106
|
-
const db = (_args_firestore = args.firestore) != null ? _args_firestore : firebaseAdmin.app().firestore();
|
|
107
|
-
const field = contactFacetPath(groupId, CONTACT_EMAIL_ENGAGEMENT_FIELD);
|
|
108
|
-
let written = 0;
|
|
109
|
-
for (const [email, at] of byEmail){
|
|
110
|
-
try {
|
|
111
|
-
await db.runTransaction(async (transaction)=>{
|
|
112
|
-
var _snapshot_data, _readContactFacet_CONTACT_EMAIL_ENGAGEMENT_FIELD;
|
|
113
|
-
/*
|
|
114
|
-
* The same unscoped lookup the capture door makes — one human is one
|
|
115
|
-
* row whichever site met them, and through the address index
|
|
116
|
-
* (AGL-2633) the row is found under an address a merge folded into
|
|
117
|
-
* it too — followed by the scope check the capture door's
|
|
118
|
-
* `visibleTo` write is the source of. Read THROUGH the transaction:
|
|
119
|
-
* the stamp below is a compare-and-set against the instant this
|
|
120
|
-
* read saw.
|
|
121
|
-
*/ const snapshot = await findContactByEmail(contactsRef, email, {
|
|
122
|
-
transaction
|
|
123
|
-
});
|
|
124
|
-
if (!snapshot) return;
|
|
125
|
-
const data = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
|
|
126
|
-
if (!visibleToHost(data['visibleTo'], hostId)) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const stored = Number((_readContactFacet_CONTACT_EMAIL_ENGAGEMENT_FIELD = readContactFacet(data, groupId)[CONTACT_EMAIL_ENGAGEMENT_FIELD]) != null ? _readContactFacet_CONTACT_EMAIL_ENGAGEMENT_FIELD : 0);
|
|
130
|
-
// Nothing moved forward, so nothing is written — the out-of-order
|
|
131
|
-
// and the replayed event are the ordinary cases this skips.
|
|
132
|
-
if (Number.isFinite(stored) && stored >= at) return;
|
|
133
|
-
transaction.update(snapshot.ref, {
|
|
134
|
-
[field]: at
|
|
135
|
-
});
|
|
136
|
-
written += 1;
|
|
137
|
-
});
|
|
138
|
-
} catch (error) {
|
|
139
|
-
console.error('[contact-email-engagement] stamp failed', hostId, error);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return written;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
//# sourceMappingURL=contact-email-engagement.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/contact-email-engagement.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 * THE CONTACT'S OWN ENGAGEMENT STAMP (AGL-2616):\n * `facets.{groupId}.lastEmailEngagementAtMs` on `orgs/{orgId}/contacts`.\n *\n * ## What it answers\n *\n * \"When did this person last read one of OUR campaigns?\" — on the record\n * page as \"Last engaged\", as a column on the list, and as the audience rule\n * `engagedWithinDays`. The per-person rollup on `emailDeliveries/{key}`\n * already answers \"when did this address last read anything from anybody\",\n * which is the right question for a sunset that refuses to mail cold\n * addresses and the wrong one for a re-engagement audience: it moves when\n * the person opens a receipt, an invite, or a sibling business's newsletter\n * on the same shared row. This stamp moves only when they open or click a\n * campaign this holder's own sites sent.\n *\n * ## What one webhook event costs\n *\n * The same bound the rollup keeps: only an event that is the FIRST of its\n * type for its message reaches here, which `recordEmailDeliveryEvent`'s\n * transaction already decided. A reader opening one newsletter six times is\n * one transaction, and a replayed event finds its type already recorded and\n * contributes nothing. Inside that bound the cost per person is one org\n * resolution and one group resolution per batch, then one keyed query and\n * at most one update per person.\n *\n * ## Forward-only, in a transaction\n *\n * Provider events are unordered and at-least-once, so an old instant\n * arriving late must not overwrite a fresh stamp — the same reasoning\n * `recordPersonEngagement` gives. The query for the contact rides inside\n * the transaction so the compare-and-write is atomic against a second event\n * for the same person landing in another instance.\n *\n * ## Only a contact this site may see, and only this holder's facet\n *\n * The contact row is shared by every site in the org. The stamp is written\n * under the sending site's consent group, never at the top of the document,\n * and only onto a row whose `visibleTo` admits the site — a person another\n * holder captured and this site never met is not this site's contact, and a\n * write onto their row would mint a facet for a holder that does not hold\n * them. `updatedAt` is left alone: an open is something the person did, not\n * an edit the team made, and a list sorted on recency must not reshuffle on\n * every mailbox prefetch.\n *\n * ## Never throws\n *\n * Best-effort for the reason everything on the webhook path is: a stamp that\n * failed loses a fact a page can live without, and a stamp that threw would\n * lose the provider's acknowledgement and teach it to retry the whole event.\n */\n\nimport {\n contactFacetPath,\n normalizeContactEmail,\n readContactFacet,\n visibleToHost,\n} from '@aglyn/aglyn/server'\nimport { findContactByEmail } from './contact-email-index'\nimport type { EmailDeliveryEventOutcome } from './email-delivery-log'\nimport { firebaseAdmin } from './firebase-admin'\nimport { consentGroupForSite, orgDataCollectionForHost } from './organizations'\n\n/** The facet field the stamp lives under. */\nexport const CONTACT_EMAIL_ENGAGEMENT_FIELD = 'lastEmailEngagementAtMs'\n\n/** The event types that count as a person engaging with a campaign. */\nconst ENGAGEMENT_TYPES: ReadonlySet<EmailDeliveryEventOutcome['type']> = new Set([\n 'opened',\n 'clicked',\n])\n\n/**\n * Stamps the sending site's contact facet for every person these outcomes\n * say engaged for the first time with a message.\n *\n * @param hostId the site the campaign went out from — the `hostId` tag the\n * send stamped, which is the only tenant identity a delivery event carries.\n * @returns how many contact documents were written.\n */\nexport async function recordContactEmailEngagement(args: {\n hostId: string\n outcomes: readonly EmailDeliveryEventOutcome[]\n firestore?: any\n}): Promise<number> {\n const hostId = String(args.hostId ?? '')\n if (!hostId) return 0\n\n /** Address → the newest engagement instant in this batch. */\n const byEmail = new Map<string, number>()\n for (const outcome of args.outcomes) {\n if (!outcome.firstOfType) continue\n if (!ENGAGEMENT_TYPES.has(outcome.type)) continue\n const email = normalizeContactEmail(outcome.to)\n const at = Number(outcome.at)\n if (!email || !Number.isFinite(at) || at <= 0) continue\n byEmail.set(email, Math.max(byEmail.get(email) ?? 0, at))\n }\n if (!byEmail.size) return 0\n\n let contactsRef: FirebaseFirestore.CollectionReference\n let groupId: string\n try {\n contactsRef = await orgDataCollectionForHost(hostId, 'contacts')\n groupId = (await consentGroupForSite(hostId)).groupId\n } catch (error) {\n console.error('[contact-email-engagement] site could not be resolved', hostId, error)\n return 0\n }\n\n const db = args.firestore ?? firebaseAdmin.app().firestore()\n const field = contactFacetPath(groupId, CONTACT_EMAIL_ENGAGEMENT_FIELD)\n let written = 0\n for (const [email, at] of byEmail) {\n try {\n await db.runTransaction(async (transaction: any) => {\n /*\n * The same unscoped lookup the capture door makes — one human is one\n * row whichever site met them, and through the address index\n * (AGL-2633) the row is found under an address a merge folded into\n * it too — followed by the scope check the capture door's\n * `visibleTo` write is the source of. Read THROUGH the transaction:\n * the stamp below is a compare-and-set against the instant this\n * read saw.\n */\n const snapshot = await findContactByEmail(contactsRef, email, { transaction })\n if (!snapshot) return\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n if (!visibleToHost(data['visibleTo'] as string[] | undefined, hostId)) {\n return\n }\n const stored = Number(\n readContactFacet(data, groupId)[CONTACT_EMAIL_ENGAGEMENT_FIELD] ?? 0,\n )\n // Nothing moved forward, so nothing is written — the out-of-order\n // and the replayed event are the ordinary cases this skips.\n if (Number.isFinite(stored) && stored >= at) return\n transaction.update(snapshot.ref, { [field]: at })\n written += 1\n })\n } catch (error) {\n console.error('[contact-email-engagement] stamp failed', hostId, error)\n }\n }\n return written\n}\n"],"names":["contactFacetPath","normalizeContactEmail","readContactFacet","visibleToHost","findContactByEmail","firebaseAdmin","consentGroupForSite","orgDataCollectionForHost","CONTACT_EMAIL_ENGAGEMENT_FIELD","ENGAGEMENT_TYPES","Set","recordContactEmailEngagement","args","hostId","String","byEmail","Map","outcome","outcomes","firstOfType","has","type","email","to","at","Number","isFinite","set","Math","max","get","size","contactsRef","groupId","error","console","db","firestore","app","field","written","runTransaction","transaction","snapshot","data","stored","update","ref"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDC,GAED,SACEA,gBAAgB,EAChBC,qBAAqB,EACrBC,gBAAgB,EAChBC,aAAa,QACR,sBAAqB;AAC5B,SAASC,kBAAkB,QAAQ,2BAAuB;AAE1D,SAASC,aAAa,QAAQ,sBAAkB;AAChD,SAASC,mBAAmB,EAAEC,wBAAwB,QAAQ,qBAAiB;AAE/E,2CAA2C,GAC3C,OAAO,MAAMC,iCAAiC,0BAAyB;AAEvE,qEAAqE,GACrE,MAAMC,mBAAmE,IAAIC,IAAI;IAC/E;IACA;CACD;AAED;;;;;;;CAOC,GACD,OAAO,eAAeC,6BAA6BC,IAIlD;QACuBA,cAyBXA;IAzBX,MAAMC,SAASC,QAAOF,eAAAA,KAAKC,MAAM,YAAXD,eAAe;IACrC,IAAI,CAACC,QAAQ,OAAO;IAEpB,2DAA2D,GAC3D,MAAME,UAAU,IAAIC;IACpB,KAAK,MAAMC,WAAWL,KAAKM,QAAQ,CAAE;YAMPH;QAL5B,IAAI,CAACE,QAAQE,WAAW,EAAE;QAC1B,IAAI,CAACV,iBAAiBW,GAAG,CAACH,QAAQI,IAAI,GAAG;QACzC,MAAMC,QAAQrB,sBAAsBgB,QAAQM,EAAE;QAC9C,MAAMC,KAAKC,OAAOR,QAAQO,EAAE;QAC5B,IAAI,CAACF,SAAS,CAACG,OAAOC,QAAQ,CAACF,OAAOA,MAAM,GAAG;QAC/CT,QAAQY,GAAG,CAACL,OAAOM,KAAKC,GAAG,EAACd,eAAAA,QAAQe,GAAG,CAACR,kBAAZP,eAAsB,GAAGS;IACvD;IACA,IAAI,CAACT,QAAQgB,IAAI,EAAE,OAAO;IAE1B,IAAIC;IACJ,IAAIC;IACJ,IAAI;QACFD,cAAc,MAAMzB,yBAAyBM,QAAQ;QACrDoB,UAAU,AAAC,CAAA,MAAM3B,oBAAoBO,OAAM,EAAGoB,OAAO;IACvD,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,yDAAyDrB,QAAQqB;QAC/E,OAAO;IACT;IAEA,MAAME,MAAKxB,kBAAAA,KAAKyB,SAAS,YAAdzB,kBAAkBP,cAAciC,GAAG,GAAGD,SAAS;IAC1D,MAAME,QAAQvC,iBAAiBiC,SAASzB;IACxC,IAAIgC,UAAU;IACd,KAAK,MAAM,CAAClB,OAAOE,GAAG,IAAIT,QAAS;QACjC,IAAI;YACF,MAAMqB,GAAGK,cAAc,CAAC,OAAOC;oBAYfC,gBAKZzC;gBAhBF;;;;;;;;SAQC,GACD,MAAMyC,WAAW,MAAMvC,mBAAmB4B,aAAaV,OAAO;oBAAEoB;gBAAY;gBAC5E,IAAI,CAACC,UAAU;gBACf,MAAMC,QAAQD,iBAAAA,SAASC,IAAI,cAAbD,iBAAmB,CAAC;gBAClC,IAAI,CAACxC,cAAcyC,IAAI,CAAC,YAAY,EAA0B/B,SAAS;oBACrE;gBACF;gBACA,MAAMgC,SAASpB,QACbvB,mDAAAA,iBAAiB0C,MAAMX,QAAQ,CAACzB,+BAA+B,YAA/DN,mDAAmE;gBAErE,kEAAkE;gBAClE,4DAA4D;gBAC5D,IAAIuB,OAAOC,QAAQ,CAACmB,WAAWA,UAAUrB,IAAI;gBAC7CkB,YAAYI,MAAM,CAACH,SAASI,GAAG,EAAE;oBAAE,CAACR,MAAM,EAAEf;gBAAG;gBAC/CgB,WAAW;YACb;QACF,EAAE,OAAON,OAAO;YACdC,QAAQD,KAAK,CAAC,2CAA2CrB,QAAQqB;QACnE;IACF;IACA,OAAOM;AACT"}
|
|
@@ -1,86 +0,0 @@
|
|
|
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 { type CrmActivityLink } from '@aglyn/aglyn/server';
|
|
18
|
-
export interface CrmBookingRecordInput {
|
|
19
|
-
hostId: string;
|
|
20
|
-
/**
|
|
21
|
-
* The owning org, as `getOrgForHost` returns it — the plugin set, the
|
|
22
|
-
* consent groups and the scope default are all read off it.
|
|
23
|
-
*/
|
|
24
|
-
org: Record<string, unknown> | null | undefined;
|
|
25
|
-
orgId: string;
|
|
26
|
-
/** The booking as its row was written. */
|
|
27
|
-
booking: {
|
|
28
|
-
id: string;
|
|
29
|
-
serviceId?: string;
|
|
30
|
-
serviceName?: string;
|
|
31
|
-
email?: string;
|
|
32
|
-
startsAtMs: number;
|
|
33
|
-
endsAtMs: number;
|
|
34
|
-
/** The `crm` query value the link carried, kept on the row. */
|
|
35
|
-
crmRef?: string | null;
|
|
36
|
-
};
|
|
37
|
-
/** The service as stored: its two CRM switches and its timezone. */
|
|
38
|
-
service: {
|
|
39
|
-
name?: string;
|
|
40
|
-
timezone?: string;
|
|
41
|
-
crmFollowUpTask?: boolean;
|
|
42
|
-
crmMeetingActivity?: boolean;
|
|
43
|
-
} | null | undefined;
|
|
44
|
-
/**
|
|
45
|
-
* The host document's plugin fields, when the caller already holds them;
|
|
46
|
-
* read here otherwise. The deny-list is per site, and a site that has
|
|
47
|
-
* switched the CRM off must file nothing.
|
|
48
|
-
*/
|
|
49
|
-
host?: {
|
|
50
|
-
disabledPlugins?: string[];
|
|
51
|
-
enabledPlugins?: string[];
|
|
52
|
-
} | null;
|
|
53
|
-
}
|
|
54
|
-
export type CrmBookingRecordOutcome = {
|
|
55
|
-
filed: false;
|
|
56
|
-
reason:
|
|
57
|
-
/** The CRM plugin does not run on this site. */
|
|
58
|
-
'crm-off'
|
|
59
|
-
/**
|
|
60
|
-
* The org's plan does not carry the CRM suite, whose activities and
|
|
61
|
-
* tasks these are (AGL-2787).
|
|
62
|
-
*/
|
|
63
|
-
| 'not-entitled'
|
|
64
|
-
/** Neither the reference nor the address named a record. */
|
|
65
|
-
| 'no-record'
|
|
66
|
-
/** A meeting for this booking is already on the timeline. */
|
|
67
|
-
| 'already-filed'
|
|
68
|
-
/** The service asks for neither a meeting nor a follow-up. */
|
|
69
|
-
| 'nothing-to-file'
|
|
70
|
-
/** A read or a write threw; logged, and nothing is promised. */
|
|
71
|
-
| 'failed';
|
|
72
|
-
} | {
|
|
73
|
-
filed: true;
|
|
74
|
-
/** How the record was found. */
|
|
75
|
-
matchedBy: 'crmRef' | 'email';
|
|
76
|
-
link: CrmActivityLink;
|
|
77
|
-
/** The meeting's id, or `null` when the service files no meeting. */
|
|
78
|
-
activityId: string | null;
|
|
79
|
-
/** The follow-up's id, or `null` when the service asks for none. */
|
|
80
|
-
taskId: string | null;
|
|
81
|
-
};
|
|
82
|
-
/**
|
|
83
|
-
* Files what a confirmed booking owes the CRM: the meeting, and the
|
|
84
|
-
* follow-up when the service asks for one. See the module comment.
|
|
85
|
-
*/
|
|
86
|
-
export declare function recordCrmBooking(firestore: FirebaseFirestore.Firestore, input: CrmBookingRecordInput): Promise<CrmBookingRecordOutcome>;
|
|
@@ -1,257 +0,0 @@
|
|
|
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 { checkEntitlement, consentGroupForHost, CRM_COLLECTIONS, crmTaskListFields, crmActivityLogHasRoom, crmBookingFollowUpDueMs, crmBookingFollowUpTitle, crmBookingMeetingBody, crmScopeTokens, isHostPluginEnabled, parseCrmBookingRef, readContactFacet, visibleToHost } from "@aglyn/aglyn/server";
|
|
18
|
-
import { FieldValue } from "firebase-admin/firestore";
|
|
19
|
-
import { findContactByEmail } from "./contact-email-index.js";
|
|
20
|
-
import { readLeadForHost } from "./host-visitor-records.js";
|
|
21
|
-
import { countCrmActivitiesForRecord } from "./crm-records.js";
|
|
22
|
-
/**
|
|
23
|
-
* WHAT A BOOKING FILES ON THE CRM RECORD (AGL-2660).
|
|
24
|
-
*
|
|
25
|
-
* A confirmed booking is a meeting the record has on its calendar, and the
|
|
26
|
-
* timeline should say so without a rep logging it by hand. So once a
|
|
27
|
-
* booking's row is written — at creation for a free service, on payment
|
|
28
|
-
* for a paid one — the record is found and a `meeting` activity filed under
|
|
29
|
-
* it, with a follow-up task beside it when the service asks for one.
|
|
30
|
-
*
|
|
31
|
-
* ## Which record
|
|
32
|
-
*
|
|
33
|
-
* The reference the booking link carried wins: `crmRef` names the contact,
|
|
34
|
-
* lead or deal the rep dropped the link from, and a booking made through
|
|
35
|
-
* that link belongs to that record even when the booker typed a different
|
|
36
|
-
* address. Without one — a booking taken off the widget cold — the booker's
|
|
37
|
-
* address is looked up through the org's address index, narrowed to this
|
|
38
|
-
* site, exactly as the automation steps resolve an event's contact. A
|
|
39
|
-
* reference to a record the site cannot see, or that no longer exists,
|
|
40
|
-
* falls through to the address rather than filing under nobody.
|
|
41
|
-
*
|
|
42
|
-
* ## Where it lives
|
|
43
|
-
*
|
|
44
|
-
* In the admin library rather than the Bookings plugin, because the row is
|
|
45
|
-
* a CRM record: its scope, its ceiling and its collections are the CRM's,
|
|
46
|
-
* and a plugin that stamped its own would be a second creator to keep in
|
|
47
|
-
* step with `crmScopeTokens`. The Bookings plugin passes what it already
|
|
48
|
-
* holds and reads the outcome; the CRM plugin is never imported.
|
|
49
|
-
*
|
|
50
|
-
* Firestore is a parameter, as in `crm-records.ts`: the callers hold one,
|
|
51
|
-
* and a module that reached for the Admin app itself would drag it into
|
|
52
|
-
* every spec that only wants the arithmetic.
|
|
53
|
-
*
|
|
54
|
-
* ## Failure posture
|
|
55
|
-
*
|
|
56
|
-
* **Never throws.** A booking that could not be filed on the CRM is still
|
|
57
|
-
* a booking — the guest has the slot and the confirmation — so a failure
|
|
58
|
-
* here is logged and reported in the outcome, never raised into the
|
|
59
|
-
* request that took the booking or the webhook that confirmed it.
|
|
60
|
-
*/ /** The CRM plugin's id, as `enabledPlugins` and `disabledPlugins` name it. */ const CRM_PLUGIN_ID = 'crm';
|
|
61
|
-
/**
|
|
62
|
-
* The record a booking belongs to — the reference first, the address second.
|
|
63
|
-
*/ async function resolveBookingRecord(firestore, input, groupId) {
|
|
64
|
-
var _hit_data;
|
|
65
|
-
const { hostId, orgId } = input;
|
|
66
|
-
const orgRef = firestore.collection('orgs').doc(orgId);
|
|
67
|
-
const contactsRef = orgRef.collection('contacts');
|
|
68
|
-
const ref = parseCrmBookingRef(input.booking.crmRef);
|
|
69
|
-
if ((ref == null ? void 0 : ref.kind) === 'contact') {
|
|
70
|
-
const contact = await contactsRef.doc(ref.id).get();
|
|
71
|
-
if (contact.exists && visibleToHost(contact.get('visibleTo'), hostId)) {
|
|
72
|
-
var _contact_data;
|
|
73
|
-
const facet = readContactFacet((_contact_data = contact.data()) != null ? _contact_data : {}, groupId);
|
|
74
|
-
return _extends({
|
|
75
|
-
link: _extends({
|
|
76
|
-
contactId: contact.id
|
|
77
|
-
}, facet.companyId ? {
|
|
78
|
-
companyId: facet.companyId
|
|
79
|
-
} : {}),
|
|
80
|
-
matchedBy: 'crmRef'
|
|
81
|
-
}, facet.ownerUid ? {
|
|
82
|
-
ownerUid: facet.ownerUid
|
|
83
|
-
} : {});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if ((ref == null ? void 0 : ref.kind) === 'deal') {
|
|
87
|
-
const deal = await orgRef.collection(CRM_COLLECTIONS.deals).doc(ref.id).get();
|
|
88
|
-
if (deal.exists && visibleToHost(deal.get('visibleTo'), hostId)) {
|
|
89
|
-
var _deal_get, _deal_get1, _deal_get2;
|
|
90
|
-
const contactId = String((_deal_get = deal.get('contactId')) != null ? _deal_get : '');
|
|
91
|
-
const companyId = String((_deal_get1 = deal.get('companyId')) != null ? _deal_get1 : '');
|
|
92
|
-
const ownerUid = String((_deal_get2 = deal.get('ownerUid')) != null ? _deal_get2 : '');
|
|
93
|
-
return _extends({
|
|
94
|
-
link: _extends({
|
|
95
|
-
dealId: deal.id
|
|
96
|
-
}, contactId ? {
|
|
97
|
-
contactId
|
|
98
|
-
} : {}, companyId ? {
|
|
99
|
-
companyId
|
|
100
|
-
} : {}),
|
|
101
|
-
matchedBy: 'crmRef'
|
|
102
|
-
}, ownerUid ? {
|
|
103
|
-
ownerUid
|
|
104
|
-
} : {});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if ((ref == null ? void 0 : ref.kind) === 'lead') {
|
|
108
|
-
/*
|
|
109
|
-
* A lead carries its own `visibleTo` now (AGL-3275) — it used to be
|
|
110
|
-
* host-scoped by path, and the comment here said so. The seam reads the
|
|
111
|
-
* org row, and the site has to be allowed to see it before a booking is
|
|
112
|
-
* filed against it, or one brand's booking would name another's lead.
|
|
113
|
-
*/ const lead = await readLeadForHost(hostId, ref.id);
|
|
114
|
-
if (lead && visibleToHost(lead.get('visibleTo'), hostId)) {
|
|
115
|
-
var _lead_get;
|
|
116
|
-
const ownerUid = String((_lead_get = lead.get('ownerUid')) != null ? _lead_get : '');
|
|
117
|
-
return _extends({
|
|
118
|
-
link: {
|
|
119
|
-
leadId: lead.id
|
|
120
|
-
},
|
|
121
|
-
matchedBy: 'crmRef'
|
|
122
|
-
}, ownerUid ? {
|
|
123
|
-
ownerUid
|
|
124
|
-
} : {});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const hit = await findContactByEmail(contactsRef, input.booking.email, {
|
|
128
|
-
hostId
|
|
129
|
-
});
|
|
130
|
-
if (!hit) return null;
|
|
131
|
-
const facet = readContactFacet((_hit_data = hit.data()) != null ? _hit_data : {}, groupId);
|
|
132
|
-
return _extends({
|
|
133
|
-
link: _extends({
|
|
134
|
-
contactId: hit.id
|
|
135
|
-
}, facet.companyId ? {
|
|
136
|
-
companyId: facet.companyId
|
|
137
|
-
} : {}),
|
|
138
|
-
matchedBy: 'email'
|
|
139
|
-
}, facet.ownerUid ? {
|
|
140
|
-
ownerUid: facet.ownerUid
|
|
141
|
-
} : {});
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Files what a confirmed booking owes the CRM: the meeting, and the
|
|
145
|
-
* follow-up when the service asks for one. See the module comment.
|
|
146
|
-
*/ export async function recordCrmBooking(firestore, input) {
|
|
147
|
-
const { hostId, orgId, booking, service } = input;
|
|
148
|
-
try {
|
|
149
|
-
var _data, _input_org, _input_org1, _input_org2, _ref, _ref1;
|
|
150
|
-
if (!hostId || !orgId || !(booking == null ? void 0 : booking.id)) return {
|
|
151
|
-
filed: false,
|
|
152
|
-
reason: 'failed'
|
|
153
|
-
};
|
|
154
|
-
const wantsMeeting = (service == null ? void 0 : service.crmMeetingActivity) !== false;
|
|
155
|
-
const wantsTask = (service == null ? void 0 : service.crmFollowUpTask) === true;
|
|
156
|
-
if (!wantsMeeting && !wantsTask) return {
|
|
157
|
-
filed: false,
|
|
158
|
-
reason: 'nothing-to-file'
|
|
159
|
-
};
|
|
160
|
-
const host = input.host === undefined ? (_data = (await firestore.collection('hosts').doc(hostId).get()).data()) != null ? _data : null : input.host;
|
|
161
|
-
if (!isHostPluginEnabled((_input_org = input.org) != null ? _input_org : null, host, CRM_PLUGIN_ID)) {
|
|
162
|
-
return {
|
|
163
|
-
filed: false,
|
|
164
|
-
reason: 'crm-off'
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
if (!checkEntitlement(input.org, 'crm')) {
|
|
168
|
-
return {
|
|
169
|
-
filed: false,
|
|
170
|
-
reason: 'not-entitled'
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
const orgRef = firestore.collection('orgs').doc(orgId);
|
|
174
|
-
const activities = orgRef.collection(CRM_COLLECTIONS.activities);
|
|
175
|
-
// One meeting per booking, whatever path re-enters: the paid webhook is
|
|
176
|
-
// redelivered for days after any 500, and its own guard is the status,
|
|
177
|
-
// which this row is not part of.
|
|
178
|
-
const already = await activities.where('bookingId', '==', booking.id).limit(1).get();
|
|
179
|
-
if (!already.empty) return {
|
|
180
|
-
filed: false,
|
|
181
|
-
reason: 'already-filed'
|
|
182
|
-
};
|
|
183
|
-
const group = consentGroupForHost((_input_org1 = input.org) != null ? _input_org1 : null, hostId);
|
|
184
|
-
const record = await resolveBookingRecord(firestore, input, group.groupId);
|
|
185
|
-
if (!record) return {
|
|
186
|
-
filed: false,
|
|
187
|
-
reason: 'no-record'
|
|
188
|
-
};
|
|
189
|
-
const visibleTo = crmScopeTokens((_input_org2 = input.org) != null ? _input_org2 : null, group);
|
|
190
|
-
const serviceName = String((_ref = (_ref1 = service == null ? void 0 : service.name) != null ? _ref1 : booking.serviceName) != null ? _ref : '').trim();
|
|
191
|
-
const timezone = (service == null ? void 0 : service.timezone) || undefined;
|
|
192
|
-
let activityId = null;
|
|
193
|
-
if (wantsMeeting) {
|
|
194
|
-
// The per-record ceiling (AGL-2611): a full log refuses the meeting
|
|
195
|
-
// but not the follow-up — a task is owed either way.
|
|
196
|
-
const logged = await countCrmActivitiesForRecord(orgRef, record.link);
|
|
197
|
-
if (crmActivityLogHasRoom(logged)) {
|
|
198
|
-
const activity = _extends({
|
|
199
|
-
kind: 'meeting',
|
|
200
|
-
body: crmBookingMeetingBody({
|
|
201
|
-
serviceName,
|
|
202
|
-
startsAtMs: booking.startsAtMs,
|
|
203
|
-
timezone
|
|
204
|
-
}),
|
|
205
|
-
// When it HAPPENS, which is the slot — not when it was booked.
|
|
206
|
-
atMs: booking.startsAtMs,
|
|
207
|
-
byUid: ''
|
|
208
|
-
}, record.link, {
|
|
209
|
-
bookingId: booking.id,
|
|
210
|
-
hostId,
|
|
211
|
-
visibleTo,
|
|
212
|
-
createdAt: FieldValue.serverTimestamp(),
|
|
213
|
-
updatedAt: FieldValue.serverTimestamp()
|
|
214
|
-
});
|
|
215
|
-
const written = await activities.add(activity);
|
|
216
|
-
activityId = written.id;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
let taskId = null;
|
|
220
|
-
if (wantsTask) {
|
|
221
|
-
const task = _extends({
|
|
222
|
-
title: crmBookingFollowUpTitle(serviceName),
|
|
223
|
-
kind: 'todo',
|
|
224
|
-
priority: 'normal',
|
|
225
|
-
status: 'open',
|
|
226
|
-
dueAtMs: crmBookingFollowUpDueMs(booking.endsAtMs, timezone)
|
|
227
|
-
}, record.ownerUid ? {
|
|
228
|
-
assigneeUid: record.ownerUid
|
|
229
|
-
} : {}, {
|
|
230
|
-
createdByUid: ''
|
|
231
|
-
}, record.link, {
|
|
232
|
-
hostId,
|
|
233
|
-
visibleTo,
|
|
234
|
-
createdAt: FieldValue.serverTimestamp(),
|
|
235
|
-
updatedAt: FieldValue.serverTimestamp()
|
|
236
|
-
});
|
|
237
|
-
// What the Tasks list searches by (AGL-3321), beside the task.
|
|
238
|
-
const written = await orgRef.collection(CRM_COLLECTIONS.tasks).add(_extends({}, task, crmTaskListFields(task)));
|
|
239
|
-
taskId = written.id;
|
|
240
|
-
}
|
|
241
|
-
return {
|
|
242
|
-
filed: true,
|
|
243
|
-
matchedBy: record.matchedBy,
|
|
244
|
-
link: record.link,
|
|
245
|
-
activityId,
|
|
246
|
-
taskId
|
|
247
|
-
};
|
|
248
|
-
} catch (error) {
|
|
249
|
-
console.error('[crm] booking could not be filed', hostId, booking == null ? void 0 : booking.id, error);
|
|
250
|
-
return {
|
|
251
|
-
filed: false,
|
|
252
|
-
reason: 'failed'
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
//# sourceMappingURL=crm-booking-activity.js.map
|