@aglyn/tenant-data-admin 1.0.0-beta.218 → 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.
Files changed (156) hide show
  1. package/package.json +8 -8
  2. package/src/lib/server/api-http.js +5 -1
  3. package/src/lib/server/api-http.js.map +1 -1
  4. package/src/lib/server/api-v1-description.d.ts +112 -0
  5. package/src/lib/server/api-v1-description.js +126 -0
  6. package/src/lib/server/api-v1-description.js.map +1 -0
  7. package/src/lib/server/api-v1-kit.d.ts +163 -0
  8. package/src/lib/server/api-v1-kit.js +191 -0
  9. package/src/lib/server/api-v1-kit.js.map +1 -0
  10. package/src/lib/server/api-v1-resources.d.ts +143 -0
  11. package/src/lib/server/api-v1-resources.js +125 -0
  12. package/src/lib/server/api-v1-resources.js.map +1 -0
  13. package/src/lib/server/campaign-conversion-attribution.d.ts +1 -1
  14. package/src/lib/server/campaign-conversion-attribution.js +3 -3
  15. package/src/lib/server/campaign-conversion-attribution.js.map +1 -1
  16. package/src/lib/server/card-payment-velocity.d.ts +9 -0
  17. package/src/lib/server/card-payment-velocity.js +28 -2
  18. package/src/lib/server/card-payment-velocity.js.map +1 -1
  19. package/src/lib/server/consent-group-carry.js +1 -1
  20. package/src/lib/server/consent-group-carry.js.map +1 -1
  21. package/src/lib/server/consent-group-change.d.ts +2 -1
  22. package/src/lib/server/consent-group-change.js +43 -18
  23. package/src/lib/server/consent-group-change.js.map +1 -1
  24. package/src/lib/server/contact-email-index.d.ts +22 -0
  25. package/src/lib/server/contact-email-index.js +108 -0
  26. package/src/lib/server/contact-email-index.js.map +1 -1
  27. package/src/lib/server/crm-email-activity.d.ts +1 -1
  28. package/src/lib/server/crm-email-activity.js +1 -1
  29. package/src/lib/server/crm-email-activity.js.map +1 -1
  30. package/src/lib/server/crm-records.d.ts +9 -1
  31. package/src/lib/server/crm-records.js +19 -2
  32. package/src/lib/server/crm-records.js.map +1 -1
  33. package/src/lib/server/cron-auth.d.ts +44 -0
  34. package/src/lib/server/cron-auth.js +76 -0
  35. package/src/lib/server/cron-auth.js.map +1 -0
  36. package/src/lib/server/dataset-live-pages.js +6 -6
  37. package/src/lib/server/dataset-live-pages.js.map +1 -1
  38. package/src/lib/server/duplicate-resource.d.ts +26 -0
  39. package/src/lib/server/duplicate-resource.js +47 -25
  40. package/src/lib/server/duplicate-resource.js.map +1 -1
  41. package/src/lib/server/dynamic-list-materialize.js +6 -3
  42. package/src/lib/server/dynamic-list-materialize.js.map +1 -1
  43. package/src/lib/server/email-flow-gate.js +2 -2
  44. package/src/lib/server/email-flow-gate.js.map +1 -1
  45. package/src/lib/server/email-suppression.d.ts +11 -0
  46. package/src/lib/server/email-suppression.js +37 -2
  47. package/src/lib/server/email-suppression.js.map +1 -1
  48. package/src/lib/server/email-unsubscribe-link.d.ts +8 -5
  49. package/src/lib/server/email-unsubscribe-link.js +7 -7
  50. package/src/lib/server/email-unsubscribe-link.js.map +1 -1
  51. package/src/lib/server/erase.d.ts +19 -33
  52. package/src/lib/server/erase.js +37 -162
  53. package/src/lib/server/erase.js.map +1 -1
  54. package/src/lib/server/host-email-tokens.js +1 -1
  55. package/src/lib/server/host-email-tokens.js.map +1 -1
  56. package/src/lib/server/host-memberships.js +3 -3
  57. package/src/lib/server/host-memberships.js.map +1 -1
  58. package/src/lib/server/host-visitor-records.d.ts +32 -4
  59. package/src/lib/server/host-visitor-records.js +73 -14
  60. package/src/lib/server/host-visitor-records.js.map +1 -1
  61. package/src/lib/server/hosted-page-review.d.ts +27 -0
  62. package/src/lib/server/hosted-page-review.js +149 -12
  63. package/src/lib/server/hosted-page-review.js.map +1 -1
  64. package/src/lib/server/media-cdn-rate-limit.d.ts +6 -3
  65. package/src/lib/server/media-cdn-rate-limit.js +6 -3
  66. package/src/lib/server/media-cdn-rate-limit.js.map +1 -1
  67. package/src/lib/server/member-email-aliases.d.ts +10 -2
  68. package/src/lib/server/member-email-aliases.js +26 -2
  69. package/src/lib/server/member-email-aliases.js.map +1 -1
  70. package/src/lib/server/new-account-announcement.d.ts +62 -0
  71. package/src/lib/server/new-account-announcement.js +77 -0
  72. package/src/lib/server/new-account-announcement.js.map +1 -0
  73. package/src/lib/server/notifications.d.ts +22 -2
  74. package/src/lib/server/notifications.js +40 -3
  75. package/src/lib/server/notifications.js.map +1 -1
  76. package/src/lib/server/operator-alerts.d.ts +7 -1
  77. package/src/lib/server/operator-alerts.js +5 -3
  78. package/src/lib/server/operator-alerts.js.map +1 -1
  79. package/src/lib/server/org-containers.d.ts +66 -0
  80. package/src/lib/server/org-containers.js +95 -0
  81. package/src/lib/server/org-containers.js.map +1 -0
  82. package/src/lib/server/org-member-notice.js +3 -2
  83. package/src/lib/server/org-member-notice.js.map +1 -1
  84. package/src/lib/server/payment-fraud-signal.js +5 -1
  85. package/src/lib/server/payment-fraud-signal.js.map +1 -1
  86. package/src/lib/server/{connect-payout-failure.d.ts → payment-provider-stripe-connect-payouts.d.ts} +3 -0
  87. package/src/lib/server/{connect-payout-failure.js → payment-provider-stripe-connect-payouts.js} +36 -2
  88. package/src/lib/server/payment-provider-stripe-connect-payouts.js.map +1 -0
  89. package/src/lib/server/{connect-account-status.d.ts → payment-provider-stripe-connect-status.d.ts} +3 -0
  90. package/src/lib/server/{connect-account-status.js → payment-provider-stripe-connect-status.js} +1 -1
  91. package/src/lib/server/payment-provider-stripe-connect-status.js.map +1 -0
  92. package/src/lib/server/{stripe-account-mode.d.ts → payment-provider-stripe-connect.d.ts} +45 -70
  93. package/src/lib/server/{stripe-account-mode.js → payment-provider-stripe-connect.js} +78 -52
  94. package/src/lib/server/payment-provider-stripe-connect.js.map +1 -0
  95. package/src/lib/server/payment-provider.d.ts +173 -0
  96. package/src/lib/server/payment-provider.js +70 -0
  97. package/src/lib/server/payment-provider.js.map +1 -0
  98. package/src/lib/server/payment-risk-record.d.ts +1 -1
  99. package/src/lib/server/payment-risk-record.js +3 -1
  100. package/src/lib/server/payment-risk-record.js.map +1 -1
  101. package/src/lib/server/personal-data-export.js +1 -1
  102. package/src/lib/server/personal-data-export.js.map +1 -1
  103. package/src/lib/server/platform-marketing-consent.d.ts +1 -1
  104. package/src/lib/server/platform-marketing-consent.js +1 -1
  105. package/src/lib/server/platform-marketing-consent.js.map +1 -1
  106. package/src/lib/server/rate-limit-store.js +13 -3
  107. package/src/lib/server/rate-limit-store.js.map +1 -1
  108. package/src/lib/server/realm-plugins.js +8 -3
  109. package/src/lib/server/realm-plugins.js.map +1 -1
  110. package/src/lib/server/retained-refusals.d.ts +142 -0
  111. package/src/lib/server/retained-refusals.js +325 -0
  112. package/src/lib/server/retained-refusals.js.map +1 -0
  113. package/src/lib/server/risk-notice.d.ts +32 -1
  114. package/src/lib/server/risk-notice.js +90 -28
  115. package/src/lib/server/risk-notice.js.map +1 -1
  116. package/src/lib/server/sending-domains.d.ts +17 -0
  117. package/src/lib/server/sending-domains.js +28 -4
  118. package/src/lib/server/sending-domains.js.map +1 -1
  119. package/src/lib/server/sso-enforcement.js +7 -2
  120. package/src/lib/server/sso-enforcement.js.map +1 -1
  121. package/src/lib/server/staff-alert-email.js +4 -2
  122. package/src/lib/server/staff-alert-email.js.map +1 -1
  123. package/src/lib/server/test-firestore-queries.d.ts +7 -0
  124. package/src/lib/server/test-firestore-queries.js +70 -27
  125. package/src/lib/server/test-firestore-queries.js.map +1 -1
  126. package/src/lib/server/{email-topic-confirmation.js → topic-subscriptions.js} +15 -11
  127. package/src/lib/server/topic-subscriptions.js.map +1 -0
  128. package/src/lib/server/upsert-contact.d.ts +46 -1
  129. package/src/lib/server/upsert-contact.js +87 -20
  130. package/src/lib/server/upsert-contact.js.map +1 -1
  131. package/src/tenancy.d.ts +5 -8
  132. package/src/tenancy.js +12 -13
  133. package/src/tenancy.js.map +1 -1
  134. package/src/lib/server/connect-account-status.js.map +0 -1
  135. package/src/lib/server/connect-payout-failure.js.map +0 -1
  136. package/src/lib/server/contact-email-engagement.d.ts +0 -32
  137. package/src/lib/server/contact-email-engagement.js +0 -145
  138. package/src/lib/server/contact-email-engagement.js.map +0 -1
  139. package/src/lib/server/contact-lifecycle-floor.d.ts +0 -89
  140. package/src/lib/server/contact-lifecycle-floor.js +0 -91
  141. package/src/lib/server/contact-lifecycle-floor.js.map +0 -1
  142. package/src/lib/server/contact-merge.d.ts +0 -55
  143. package/src/lib/server/contact-merge.js +0 -288
  144. package/src/lib/server/contact-merge.js.map +0 -1
  145. package/src/lib/server/crm-booking-activity.d.ts +0 -86
  146. package/src/lib/server/crm-booking-activity.js +0 -257
  147. package/src/lib/server/crm-booking-activity.js.map +0 -1
  148. package/src/lib/server/crm-inbound-email.d.ts +0 -145
  149. package/src/lib/server/crm-inbound-email.js +0 -297
  150. package/src/lib/server/crm-inbound-email.js.map +0 -1
  151. package/src/lib/server/email-topic-confirmation.js.map +0 -1
  152. package/src/lib/server/form-dataset-binding-token.d.ts +0 -35
  153. package/src/lib/server/form-dataset-binding-token.js +0 -107
  154. package/src/lib/server/form-dataset-binding-token.js.map +0 -1
  155. package/src/lib/server/stripe-account-mode.js.map +0 -1
  156. /package/src/lib/server/{email-topic-confirmation.d.ts → topic-subscriptions.d.ts} +0 -0
package/src/tenancy.js CHANGED
@@ -19,9 +19,11 @@ export * from "./lib/server/api-keys.js";
19
19
  export * from "./lib/server/account-emails.js";
20
20
  export * from "./lib/server/auth-pools.js";
21
21
  export * from "./lib/server/client-error-report.js";
22
- export * from "./lib/server/connect-account-status.js";
23
- export * from "./lib/server/connect-payout-failure.js";
24
- export * from "./lib/server/stripe-account-mode.js";
22
+ // The merchant's payment account, as a contract. Its adapter is not exported:
23
+ // a plugin asks the contract, and the doors that only need the readiness rule
24
+ // import `@aglyn/tenant-data-admin/server/payment-provider` directly, which
25
+ // loads no Firebase.
26
+ export { merchantAccountIsReady, merchantAccountReadiness, paymentProvider } from "./lib/server/payment-provider.js";
25
27
  export * from "./lib/server/auth-handoff.js";
26
28
  export * from "./lib/server/console-domains.js";
27
29
  export * from "./lib/server/dns-probe.js";
@@ -44,7 +46,7 @@ export * from "./lib/server/email-suppression.js";
44
46
  // The double opt-in quarantine, after the module whose keying it shares and
45
47
  // whose `filterTopicSendable` is the reader that makes it a quarantine rather
46
48
  // than a stored fact.
47
- export * from "./lib/server/email-topic-confirmation.js";
49
+ export * from "./lib/server/topic-subscriptions.js";
48
50
  export * from "./lib/server/email-unsubscribe-link.js";
49
51
  export * from "./lib/server/email-flow-gate.js";
50
52
  // Side-effecting on purpose, for the reason `email-send-rate` below is:
@@ -56,6 +58,9 @@ export * from "./lib/server/email-marketing-gate.js";
56
58
  // and the executor that runs a change around them.
57
59
  export * from "./lib/server/consent-group-carry.js";
58
60
  export * from "./lib/server/consent-group-change.js";
61
+ // A person's refusal kept after the record it was written on is deleted
62
+ // (AGL-3338): the delete's write, the list gate's read, and the change's carry.
63
+ export * from "./lib/server/retained-refusals.js";
59
64
  export * from "./lib/server/sms-keywords.js";
60
65
  export * from "./lib/server/document-id.js";
61
66
  export * from "./lib/server/collection-preview-token.js";
@@ -135,10 +140,7 @@ export * from "./lib/server/sso-provisioning.js";
135
140
  export * from "./lib/server/update-existing.js";
136
141
  export * from "./lib/server/contact-company-link.js";
137
142
  export * from "./lib/server/contact-email-index.js";
138
- export * from "./lib/server/contact-merge.js";
139
143
  export * from "./lib/server/upsert-contact.js";
140
- // The stage floor a won deal applies to its contact (AGL-2641).
141
- export * from "./lib/server/contact-lifecycle-floor.js";
142
144
  export * from "./lib/server/crm-records.js";
143
145
  // The `nextTaskAtMs` a contact, a company and a deal carry, recomputed from
144
146
  // their open tasks by every server-side task writer (AGL-2661).
@@ -146,16 +148,13 @@ export * from "./lib/server/crm-next-activity.js";
146
148
  // Where a one-to-one email's activity row lives and how the delivery
147
149
  // webhook advances it (AGL-2615).
148
150
  export * from "./lib/server/crm-email-activity.js";
149
- // What a confirmed booking files on the CRM record — the meeting and the
150
- // follow-up (AGL-2660).
151
- export * from "./lib/server/crm-booking-activity.js";
152
- // The org's email capture token and the filing of one received message
153
- // on the record it was with (AGL-2657).
154
- export * from "./lib/server/crm-inbound-email.js";
155
151
  // A member's own addresses in a workspace, and the signed link that
156
152
  // confirms one (AGL-2975).
157
153
  export * from "./lib/server/member-email-aliases.js";
158
154
  export * from "./lib/server/user-profiles.js";
155
+ // The one "New account" staff notice per account, keyed on the auth
156
+ // record's creation (AGL-3225).
157
+ export * from "./lib/server/new-account-announcement.js";
159
158
  export * from "./lib/server/legal-acceptance.js";
160
159
  // The person's decision about the platform's own product email, on their
161
160
  // document and on the operator's marketing contact (AGL-3185).
@@ -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'\nexport * from './lib/server/connect-account-status'\nexport * from './lib/server/connect-payout-failure'\nexport * from './lib/server/stripe-account-mode'\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'\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/contact-merge'\nexport * from './lib/server/upsert-contact'\n// The stage floor a won deal applies to its contact (AGL-2641).\nexport * from './lib/server/contact-lifecycle-floor'\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'\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":["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 +0,0 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/connect-account-status.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { firebaseAdmin } from './firebase-admin'\nimport { sendGa4StripeConnected } from './ga4-measurement-protocol'\nimport { updateExisting } from './update-existing'\n\n/**\n * Connect readiness, refreshed from Stripe rather than from a cached flag\n * (AGL-1997).\n *\n * `stripeChargesEnabled` is the field every money route gates on — commerce\n * checkout, cart checkout, draft orders, reservations, POS — and until this it\n * was refreshed ONLY when the merchant happened to reopen the Connect route.\n * So an account Stripe later restricts (a verification deadline passing, a\n * capability revoked, documents going stale) kept a cached `true`, the store\n * kept selling, and the failure surfaced to the SHOPPER at payment time. The\n * merchant, who could have fixed it, was the last to know.\n *\n * `account.updated` is the event that carries this. Stripe sends it on every\n * change to a connected account, which is why the fix is a subscription and\n * not a poll.\n *\n * WHY IT WRITES ONLY WHAT STRIPE ACTUALLY SAID. Both flags are written only\n * when the payload states them as booleans. Coercing an absent field with\n * `Boolean(...)` would manufacture a `false` and lock out a merchant who is\n * fine — the over-restrictive direction, but still a wrong answer invented\n * from no evidence — and defaulting the other way would re-create the very\n * fail-open this closes. An event that states neither flag writes nothing at\n * all; it is not an Account payload.\n *\n * WHY `updateExisting` AND NOT A MERGE-SET. The refs come from a query, so the\n * documents existed a moment ago — but a merge-set would resurrect one erased\n * in between as a stub holding two payout booleans and nothing else, the\n * AGL-1763 phantom shape. `update()` rejects instead, and a rejection here\n * costs nothing: the next `account.updated` re-delivers the same state.\n *\n * IDEMPOTENT AND ORDER-INDEPENDENT by construction — it mirrors current state,\n * never a delta — so a Stripe redelivery and an out-of-order pair both\n * converge.\n */\nexport interface ConnectAccountStatusEvent {\n /** `event.data.object` for `account.updated` — the Stripe Account. */\n id?: unknown\n charges_enabled?: unknown\n payouts_enabled?: unknown\n}\n\n/**\n * Mirrors a connected account's charge/payout readiness onto whichever\n * documents in `collection` name it in `stripeAccountId`.\n *\n * @param eventLivemode `event.livemode` from the enclosing `account.updated`\n * event (AGL-2471) — NOT `event.data.object.livemode`, which Stripe's\n * Account object does not have. Anything other than a boolean writes\n * nothing, leaving the field absent, which every money gate reads as\n * unverified and refuses.\n * @param collection Firestore collection holding the payout binding —\n * `profiles` for storefront merchants, `publisherProfiles` for marketplace\n * publishers. Each plugin syncs its own; a non-matching account updates\n * nothing.\n * @returns the number of documents updated. 0 is the ordinary answer for an\n * account this collection does not bind, and the caller should not treat it\n * as a failure.\n */\nexport async function syncConnectAccountStatus(\n collection: string,\n account: ConnectAccountStatusEvent | null | undefined,\n eventLivemode?: unknown,\n): Promise<number> {\n const accountId = typeof account?.id === 'string' ? account.id.trim() : ''\n if (!accountId) return 0\n const patch: Record<string, boolean> = {}\n if (typeof account?.charges_enabled === 'boolean') {\n patch['stripeChargesEnabled'] = account.charges_enabled\n }\n if (typeof account?.payouts_enabled === 'boolean') {\n patch['stripePayoutsEnabled'] = account.payouts_enabled\n }\n // WHICH STRIPE WORLD (AGL-2471). The Account object has no `livemode`, but\n // the EVENT carrying it does, and it is Stripe's own statement about the\n // account being described — the strongest evidence available, and it\n // arrives on every change. This is what heals a linkage whose mode was\n // never recorded, without anyone touching the database by hand.\n //\n // Same doctrine as the two flags above: only a literal boolean is written.\n if (typeof eventLivemode === 'boolean') {\n patch['stripeAccountLivemode'] = eventLivemode\n }\n // Nothing stated, nothing written — see the doc comment.\n if (!Object.keys(patch).length) return 0\n\n const matches = await firebaseAdmin\n .app()\n .firestore()\n .collection(collection)\n .where('stripeAccountId', '==', accountId)\n .get()\n let updated = 0\n for (const doc of matches.docs) {\n // THE ACTIVATION TRANSITION, read BEFORE the write (AGL-1580).\n //\n // `stripe_connected` is one of the launch key events, and its two\n // client-side emitters are gated on the merchant's profile still reading\n // \"not connected\" when they click. This webhook is what makes that false:\n // it lands while the merchant is still on Stripe's hosted onboarding, so\n // the flag has already flipped by the time they get back and the browser\n // event can never fire. See `sendGa4StripeConnected` for why the two\n // guards cannot both open for the same account.\n //\n // `!== true` and not `=== false`: a profile that has never been asked has\n // the field ABSENT, and that is the commonest first-connect shape of all.\n const becameConnected =\n patch['stripeChargesEnabled'] === true &&\n doc.get('stripeChargesEnabled') !== true\n // `updateExisting` rethrows anything that is not \"the document is gone\",\n // so a permission or transport failure still reaches the webhook and gets\n // the redelivery it needs.\n if (await updateExisting(doc.ref, patch)) {\n updated += 1\n // Reported only after the write LANDED. A document erased between the\n // query and the update is not an activation, and reporting one would\n // put a merchant in the metric who no longer exists.\n //\n // Awaited, not floated: this runs inside the Stripe webhook's claim on\n // the event, and a floated promise in a serverless handler is frozen at\n // response and never runs — the AGL-2346 shape. The sender swallows\n // everything and cannot un-claim the event by throwing.\n if (becameConnected) await sendGa4StripeConnected({ accountId })\n }\n }\n return updated\n}\n"],"names":["firebaseAdmin","sendGa4StripeConnected","updateExisting","syncConnectAccountStatus","collection","account","eventLivemode","accountId","id","trim","patch","charges_enabled","payouts_enabled","Object","keys","length","matches","app","firestore","where","get","updated","doc","docs","becameConnected","ref"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,sBAAkB;AAChD,SAASC,sBAAsB,QAAQ,gCAA4B;AACnE,SAASC,cAAc,QAAQ,uBAAmB;AA2ClD;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,eAAeC,yBACpBC,UAAkB,EAClBC,OAAqD,EACrDC,aAAuB;IAEvB,MAAMC,YAAY,QAAOF,2BAAAA,QAASG,EAAE,MAAK,WAAWH,QAAQG,EAAE,CAACC,IAAI,KAAK;IACxE,IAAI,CAACF,WAAW,OAAO;IACvB,MAAMG,QAAiC,CAAC;IACxC,IAAI,QAAOL,2BAAAA,QAASM,eAAe,MAAK,WAAW;QACjDD,KAAK,CAAC,uBAAuB,GAAGL,QAAQM,eAAe;IACzD;IACA,IAAI,QAAON,2BAAAA,QAASO,eAAe,MAAK,WAAW;QACjDF,KAAK,CAAC,uBAAuB,GAAGL,QAAQO,eAAe;IACzD;IACA,2EAA2E;IAC3E,yEAAyE;IACzE,qEAAqE;IACrE,uEAAuE;IACvE,gEAAgE;IAChE,EAAE;IACF,2EAA2E;IAC3E,IAAI,OAAON,kBAAkB,WAAW;QACtCI,KAAK,CAAC,wBAAwB,GAAGJ;IACnC;IACA,yDAAyD;IACzD,IAAI,CAACO,OAAOC,IAAI,CAACJ,OAAOK,MAAM,EAAE,OAAO;IAEvC,MAAMC,UAAU,MAAMhB,cACnBiB,GAAG,GACHC,SAAS,GACTd,UAAU,CAACA,YACXe,KAAK,CAAC,mBAAmB,MAAMZ,WAC/Ba,GAAG;IACN,IAAIC,UAAU;IACd,KAAK,MAAMC,OAAON,QAAQO,IAAI,CAAE;QAC9B,+DAA+D;QAC/D,EAAE;QACF,kEAAkE;QAClE,yEAAyE;QACzE,0EAA0E;QAC1E,yEAAyE;QACzE,yEAAyE;QACzE,qEAAqE;QACrE,gDAAgD;QAChD,EAAE;QACF,0EAA0E;QAC1E,0EAA0E;QAC1E,MAAMC,kBACJd,KAAK,CAAC,uBAAuB,KAAK,QAClCY,IAAIF,GAAG,CAAC,4BAA4B;QACtC,yEAAyE;QACzE,0EAA0E;QAC1E,2BAA2B;QAC3B,IAAI,MAAMlB,eAAeoB,IAAIG,GAAG,EAAEf,QAAQ;YACxCW,WAAW;YACX,sEAAsE;YACtE,qEAAqE;YACrE,qDAAqD;YACrD,EAAE;YACF,uEAAuE;YACvE,wEAAwE;YACxE,oEAAoE;YACpE,wDAAwD;YACxD,IAAIG,iBAAiB,MAAMvB,uBAAuB;gBAAEM;YAAU;QAChE;IACF;IACA,OAAOc;AACT"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/connect-payout-failure.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { formatOperatorAlertAmount } from '@aglyn/aglyn/app-utils/operator-alerts'\nimport { firebaseAdmin } from './firebase-admin'\nimport { updateExisting } from './update-existing'\n\n/**\n * A PAYOUT THAT NEVER LANDED, RECORDED.\n *\n * ## What was invisible, and to whom\n *\n * Nothing in this codebase handled `payout.failed` or `transfer.failed`. The\n * consequences ran in opposite directions on the two sides of the same money:\n *\n * - **The merchant** saw a storefront that looked healthy — orders settling,\n * the payments card reporting enabled — while the funds accumulated in a\n * Connect account that could not release them. Aglyn stored no balance and\n * no payout schedule, so nothing in the console could contradict that.\n * - **The publisher** saw a seller ledger whose payout figure is derived from\n * `transferCents`, which is what Stripe was ASKED to move. A failed payout\n * leaves that number untouched, so the ledger actively asserted money had\n * arrived when it had not.\n *\n * And Aglyn held no record at all, so there was nothing to reconcile against,\n * no history, and no way to answer \"has this happened to this account before\".\n *\n * ## Recorded, surfaced — never retried\n *\n * This module only writes things down. It does NOT re-issue a payout or a\n * transfer, deliberately: Stripe runs its own retry schedule, and a second\n * transfer against an account that has just refused one is how a duplicate\n * lands. A human decides what happens next, which is the same posture the\n * refund-reversal recovery queue takes for the money Stripe refuses to pull\n * back — the sibling family of problem.\n *\n * ## Two records, because they answer different questions\n *\n * `connectPayoutFailures/{stripeId}` is the durable HISTORY, keyed by the\n * Stripe id so a redelivery converges rather than duplicating. It is written\n * even when no profile in the named collection binds the account, because\n * \"money did not arrive and we cannot say whose\" is precisely the state that\n * must not be silent.\n *\n * The profile documents get a MIRROR of the latest failure, because that is\n * the document the merchant and publisher surfaces already read. A card that\n * had to open a second collection would be a second, disagreeing read of the\n * same fact.\n */\n\n/** Which leg of the money movement failed. */\nexport type ConnectPayoutFailureKind = 'payout' | 'transfer'\n\nexport interface ConnectPayoutFailureInput {\n /**\n * `payout` is the connected account's balance reaching its bank, and its\n * account id comes from `event.account` — the Payout object's own\n * `destination` is the BANK, not the Connect account. `transfer` is the\n * platform's balance reaching the connected account, a platform event whose\n * `destination` IS the account.\n */\n kind: ConnectPayoutFailureKind\n /** `event.data.object` — the Payout or Transfer. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n object: any\n /** The connected account the money was headed for. */\n accountId: string\n /** `event.livemode`. */\n livemode?: unknown\n}\n\nfunction cents(value: unknown): number {\n const amount = Math.round(Number(value ?? 0))\n return Number.isFinite(amount) ? amount : 0\n}\n\nfunction text(value: unknown): string | null {\n return typeof value === 'string' && value.trim() ? value.trim() : null\n}\n\n/**\n * The sentence a merchant reads. Stripe's `failure_message` is written for a\n * developer and is often absent, so the code is kept beside it and a missing\n * pair still produces something a human can act on rather than an empty\n * warning that looks like a rendering fault.\n */\nexport function payoutFailureReason(object: unknown): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const source = object as any\n return (\n text(source?.failure_message) ??\n text(source?.failure_code) ??\n 'Stripe did not say why'\n )\n}\n\n/**\n * Records one failed payout or transfer, and mirrors it onto the profiles in\n * `collection` that bind the account.\n *\n * @returns how many profile documents were mirrored onto. `0` is an ordinary\n * answer — the other plugin's collection binds this account, or nothing\n * does — and never means the failure went unrecorded.\n */\nexport async function recordConnectPayoutFailure(\n collection: string,\n input: ConnectPayoutFailureInput,\n): Promise<number> {\n const accountId = String(input?.accountId ?? '').trim()\n const stripeId = text(input?.object?.id)\n if (!accountId || !stripeId) return 0\n const firestore = firebaseAdmin.app().firestore()\n const amountCents = cents(input.object?.amount)\n const reason = payoutFailureReason(input.object)\n const failedAtMs = Date.now()\n\n const matches = await firestore\n .collection(collection)\n .where('stripeAccountId', '==', accountId)\n .get()\n\n // The durable record FIRST, and unconditionally. An account no profile binds\n // is the case with the least visibility and the most need of a written\n // trace; making the history conditional on a successful join would lose\n // exactly the failures nobody could otherwise explain.\n await firestore\n .collection('connectPayoutFailures')\n .doc(stripeId)\n .set(\n {\n kind: input.kind,\n accountId,\n amountCents,\n currency: text(input.object?.currency) ?? 'usd',\n failureCode: text(input.object?.failure_code),\n failureMessage: text(input.object?.failure_message),\n reason,\n failedAtMs,\n ...(typeof input.livemode === 'boolean'\n ? { livemode: input.livemode }\n : {}),\n // A lead a human can search, accumulated across both plugins: the same\n // account can be bound by a storefront profile and a publisher profile,\n // and each plugin records only its own.\n ...(matches.docs.length\n ? {\n boundProfiles: firebaseAdmin.firestore.FieldValue.arrayUnion(\n ...matches.docs.map((doc) => `${collection}/${doc.id}`),\n ),\n }\n : {}),\n recordedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n\n // Money due to a merchant or seller did not arrive, and a human decides\n // what happens next (AGL-3377). Loaded lazily, like everything this module\n // only needs once a failure is real.\n const { raiseOperatorAlert } = await import('./operator-alerts')\n await raiseOperatorAlert('billing.connectPayoutFailed', {\n dedupeKey: stripeId,\n context: {\n kind: input.kind,\n stripeId,\n account: accountId,\n reason,\n amount: formatOperatorAlertAmount(amountCents, text(input.object?.currency) ?? 'usd'),\n },\n })\n\n // `updateExisting` rather than a merge-set, for the reason its sibling\n // states: the refs come from a query, and a merge-set would resurrect a\n // profile erased in between as a stub holding nothing but a payout warning.\n let mirrored = 0\n for (const doc of matches.docs) {\n if (\n await updateExisting(doc.ref, {\n lastPayoutFailureId: stripeId,\n lastPayoutFailureKind: input.kind,\n lastPayoutFailureAtMs: failedAtMs,\n lastPayoutFailureCents: amountCents,\n lastPayoutFailureReason: reason,\n })\n ) {\n mirrored += 1\n }\n }\n return mirrored\n}\n\n/**\n * Clears the mirrored warning when a later payout to the same account\n * succeeds.\n *\n * NOT a retry and not a correction of the history: `connectPayoutFailures`\n * keeps every failure, because \"has this account failed before\" is a question\n * the record exists to answer. This only stops the merchant's card asserting a\n * problem that has since resolved — a stale warning trains people to ignore\n * the surface, which costs more than it saves.\n */\nexport async function clearConnectPayoutFailure(\n collection: string,\n accountId: string,\n): Promise<number> {\n const id = String(accountId ?? '').trim()\n if (!id) return 0\n const firestore = firebaseAdmin.app().firestore()\n const matches = await firestore\n .collection(collection)\n .where('stripeAccountId', '==', id)\n .get()\n let cleared = 0\n for (const doc of matches.docs) {\n // Only touch a profile that actually carries a warning, so an ordinary\n // successful payout does not write to every profile on every event.\n if (doc.get('lastPayoutFailureAtMs') == null) continue\n const remove = firebaseAdmin.firestore.FieldValue.delete()\n if (\n await updateExisting(doc.ref, {\n lastPayoutFailureId: remove,\n lastPayoutFailureKind: remove,\n lastPayoutFailureAtMs: remove,\n lastPayoutFailureCents: remove,\n lastPayoutFailureReason: remove,\n })\n ) {\n cleared += 1\n }\n }\n return cleared\n}\n"],"names":["formatOperatorAlertAmount","firebaseAdmin","updateExisting","cents","value","amount","Math","round","Number","isFinite","text","trim","payoutFailureReason","object","source","failure_message","failure_code","recordConnectPayoutFailure","collection","input","accountId","String","stripeId","id","firestore","app","amountCents","reason","failedAtMs","Date","now","matches","where","get","doc","set","kind","currency","failureCode","failureMessage","livemode","docs","length","boundProfiles","FieldValue","arrayUnion","map","recordedAt","serverTimestamp","merge","raiseOperatorAlert","dedupeKey","context","account","mirrored","ref","lastPayoutFailureId","lastPayoutFailureKind","lastPayoutFailureAtMs","lastPayoutFailureCents","lastPayoutFailureReason","clearConnectPayoutFailure","cleared","remove","delete"],"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,89 +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
- /**
18
- * A person's lifecycle stage, FLOORED at what just happened to them
19
- * (AGL-2641).
20
- *
21
- * The capture doors floor a stage inside `upsertHostContact`: an order
22
- * names `customer` as its `initialLifecycleStage`, and the door fills an
23
- * empty stage or advances an earlier one and never moves anybody back. A
24
- * won deal is the same fact as a purchase — the business has decided this
25
- * person bought — but it arrives at a record that already exists, through a
26
- * writer that is not a capture door: the deal-stage route and the REST
27
- * deals resource. This is the door's rule for them, over a contact document
28
- * they already hold a reference to.
29
- *
30
- * The write goes into ONE holder's facet, because a stage is one business's
31
- * reading of a person and the row is shared by every site in the org. The
32
- * holder is the site the deal was made on, resolved to its consent group
33
- * the way every other facet writer resolves it; a deal that names no site
34
- * falls back to the site that captured the person, which is the one
35
- * holder a contact always has. A contact that names none either is left
36
- * alone and the caller told, rather than written into a facet nobody reads.
37
- *
38
- * The write is skipped when it would change nothing — a customer stays a
39
- * customer, an evangelist stays an evangelist, `other` is never overwritten
40
- * — so a caller can announce `contactStageChanged` exactly when `advanced`
41
- * says the stage moved, and an automation listening for the change never
42
- * hears one that did not happen.
43
- */
44
- import { type ContactLifecycleStage } from '@aglyn/aglyn/server';
45
- export type ContactLifecycleFloor = {
46
- /** The stage was raised to the floor. */
47
- outcome: 'advanced';
48
- contactId: string;
49
- /** The address on the row, for the event a caller announces. */
50
- email: string;
51
- /** The site whose facet was written. */
52
- hostId: string;
53
- groupId: string;
54
- /** The stage before the write, or `''` for a person who had none. */
55
- previousStage: ContactLifecycleStage | '';
56
- lifecycleStage: ContactLifecycleStage;
57
- } | {
58
- /** The person already held the floor or a later stage; nothing was written. */
59
- outcome: 'held';
60
- contactId: string;
61
- email: string;
62
- hostId: string;
63
- groupId: string;
64
- lifecycleStage: ContactLifecycleStage;
65
- } | {
66
- /** No such contact. */
67
- outcome: 'missing';
68
- contactId: string;
69
- } | {
70
- /** Neither the caller nor the contact names a site, so there is no facet to write. */
71
- outcome: 'unheld';
72
- contactId: string;
73
- };
74
- export interface ContactLifecycleFloorOptions {
75
- contactRef: FirebaseFirestore.DocumentReference;
76
- /**
77
- * The org the contact belongs to, for the consent groups — the same
78
- * document every facet writer resolves the holder through.
79
- */
80
- org: Record<string, unknown> | null | undefined;
81
- /**
82
- * The site the act happened on, whose facet takes the stage. `null` for a
83
- * record no site captured; the contact's own capturing site is used then.
84
- */
85
- hostId: string | null | undefined;
86
- /** The EARLIEST stage that describes what happened — see `advanceContactLifecycleStage`. */
87
- floor: ContactLifecycleStage;
88
- }
89
- export declare function floorContactLifecycleStage(options: ContactLifecycleFloorOptions): Promise<ContactLifecycleFloor>;
@@ -1,91 +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
- * A person's lifecycle stage, FLOORED at what just happened to them
18
- * (AGL-2641).
19
- *
20
- * The capture doors floor a stage inside `upsertHostContact`: an order
21
- * names `customer` as its `initialLifecycleStage`, and the door fills an
22
- * empty stage or advances an earlier one and never moves anybody back. A
23
- * won deal is the same fact as a purchase — the business has decided this
24
- * person bought — but it arrives at a record that already exists, through a
25
- * writer that is not a capture door: the deal-stage route and the REST
26
- * deals resource. This is the door's rule for them, over a contact document
27
- * they already hold a reference to.
28
- *
29
- * The write goes into ONE holder's facet, because a stage is one business's
30
- * reading of a person and the row is shared by every site in the org. The
31
- * holder is the site the deal was made on, resolved to its consent group
32
- * the way every other facet writer resolves it; a deal that names no site
33
- * falls back to the site that captured the person, which is the one
34
- * holder a contact always has. A contact that names none either is left
35
- * alone and the caller told, rather than written into a facet nobody reads.
36
- *
37
- * The write is skipped when it would change nothing — a customer stays a
38
- * customer, an evangelist stays an evangelist, `other` is never overwritten
39
- * — so a caller can announce `contactStageChanged` exactly when `advanced`
40
- * says the stage moved, and an automation listening for the change never
41
- * hears one that did not happen.
42
- */ import { consentGroupForHost, advanceContactLifecycleStage, contactFacetPath, isContactLifecycleStage, readContactFacet } from "@aglyn/aglyn/server";
43
- import { FieldValue } from "firebase-admin/firestore";
44
- import { restampCrmListFieldsAt } from "./crm-records.js";
45
- export async function floorContactLifecycleStage(options) {
46
- var _snapshot_data, _options_hostId, _data_hostId, _advanceContactLifecycleStage, _data_email;
47
- const { contactRef, org, floor } = options;
48
- const snapshot = await contactRef.get();
49
- if (!snapshot.exists) return {
50
- outcome: 'missing',
51
- contactId: contactRef.id
52
- };
53
- const data = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
54
- const hostId = String((_options_hostId = options.hostId) != null ? _options_hostId : '').trim() || String((_data_hostId = data['hostId']) != null ? _data_hostId : '').trim();
55
- if (!hostId) return {
56
- outcome: 'unheld',
57
- contactId: snapshot.id
58
- };
59
- const { groupId } = consentGroupForHost(org != null ? org : null, hostId);
60
- const facet = readContactFacet(data, groupId);
61
- const held = isContactLifecycleStage(facet.lifecycleStage) ? facet.lifecycleStage : undefined;
62
- const lifecycleStage = (_advanceContactLifecycleStage = advanceContactLifecycleStage(held, floor)) != null ? _advanceContactLifecycleStage : floor;
63
- const email = String((_data_email = data['email']) != null ? _data_email : '');
64
- if (lifecycleStage === held) {
65
- return {
66
- outcome: 'held',
67
- contactId: snapshot.id,
68
- email,
69
- hostId,
70
- groupId,
71
- lifecycleStage
72
- };
73
- }
74
- await snapshot.ref.update({
75
- [contactFacetPath(groupId, 'lifecycleStage')]: lifecycleStage,
76
- updatedAt: FieldValue.serverTimestamp()
77
- });
78
- // The stage is what the Contacts list filters by (AGL-3321).
79
- await restampCrmListFieldsAt(snapshot.ref, 'contacts');
80
- return {
81
- outcome: 'advanced',
82
- contactId: snapshot.id,
83
- email,
84
- hostId,
85
- groupId,
86
- previousStage: held != null ? held : '',
87
- lifecycleStage
88
- };
89
- }
90
-
91
- //# sourceMappingURL=contact-lifecycle-floor.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/contact-lifecycle-floor.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 * A person's lifecycle stage, FLOORED at what just happened to them\n * (AGL-2641).\n *\n * The capture doors floor a stage inside `upsertHostContact`: an order\n * names `customer` as its `initialLifecycleStage`, and the door fills an\n * empty stage or advances an earlier one and never moves anybody back. A\n * won deal is the same fact as a purchase — the business has decided this\n * person bought — but it arrives at a record that already exists, through a\n * writer that is not a capture door: the deal-stage route and the REST\n * deals resource. This is the door's rule for them, over a contact document\n * they already hold a reference to.\n *\n * The write goes into ONE holder's facet, because a stage is one business's\n * reading of a person and the row is shared by every site in the org. The\n * holder is the site the deal was made on, resolved to its consent group\n * the way every other facet writer resolves it; a deal that names no site\n * falls back to the site that captured the person, which is the one\n * holder a contact always has. A contact that names none either is left\n * alone and the caller told, rather than written into a facet nobody reads.\n *\n * The write is skipped when it would change nothing — a customer stays a\n * customer, an evangelist stays an evangelist, `other` is never overwritten\n * — so a caller can announce `contactStageChanged` exactly when `advanced`\n * says the stage moved, and an automation listening for the change never\n * hears one that did not happen.\n */\n\nimport {\n consentGroupForHost,\n advanceContactLifecycleStage,\n contactFacetPath,\n type ContactLifecycleStage,\n isContactLifecycleStage,\n readContactFacet,\n} from '@aglyn/aglyn/server'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { restampCrmListFieldsAt } from './crm-records'\n\nexport type ContactLifecycleFloor =\n | {\n /** The stage was raised to the floor. */\n outcome: 'advanced'\n contactId: string\n /** The address on the row, for the event a caller announces. */\n email: string\n /** The site whose facet was written. */\n hostId: string\n groupId: string\n /** The stage before the write, or `''` for a person who had none. */\n previousStage: ContactLifecycleStage | ''\n lifecycleStage: ContactLifecycleStage\n }\n | {\n /** The person already held the floor or a later stage; nothing was written. */\n outcome: 'held'\n contactId: string\n email: string\n hostId: string\n groupId: string\n lifecycleStage: ContactLifecycleStage\n }\n | {\n /** No such contact. */\n outcome: 'missing'\n contactId: string\n }\n | {\n /** Neither the caller nor the contact names a site, so there is no facet to write. */\n outcome: 'unheld'\n contactId: string\n }\n\nexport interface ContactLifecycleFloorOptions {\n contactRef: FirebaseFirestore.DocumentReference\n /**\n * The org the contact belongs to, for the consent groups — the same\n * document every facet writer resolves the holder through.\n */\n org: Record<string, unknown> | null | undefined\n /**\n * The site the act happened on, whose facet takes the stage. `null` for a\n * record no site captured; the contact's own capturing site is used then.\n */\n hostId: string | null | undefined\n /** The EARLIEST stage that describes what happened — see `advanceContactLifecycleStage`. */\n floor: ContactLifecycleStage\n}\n\nexport async function floorContactLifecycleStage(\n options: ContactLifecycleFloorOptions,\n): Promise<ContactLifecycleFloor> {\n const { contactRef, org, floor } = options\n const snapshot = await contactRef.get()\n if (!snapshot.exists) return { outcome: 'missing', contactId: contactRef.id }\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n const hostId = String(options.hostId ?? '').trim() || String(data['hostId'] ?? '').trim()\n if (!hostId) return { outcome: 'unheld', contactId: snapshot.id }\n\n const { groupId } = consentGroupForHost(org ?? null, hostId)\n const facet = readContactFacet(data, groupId)\n const held = isContactLifecycleStage(facet.lifecycleStage)\n ? facet.lifecycleStage\n : undefined\n const lifecycleStage = advanceContactLifecycleStage(held, floor) ?? floor\n const email = String(data['email'] ?? '')\n if (lifecycleStage === held) {\n return { outcome: 'held', contactId: snapshot.id, email, hostId, groupId, lifecycleStage }\n }\n await snapshot.ref.update({\n [contactFacetPath(groupId, 'lifecycleStage')]: lifecycleStage,\n updatedAt: FieldValue.serverTimestamp(),\n })\n // The stage is what the Contacts list filters by (AGL-3321).\n await restampCrmListFieldsAt(snapshot.ref, 'contacts')\n return {\n outcome: 'advanced',\n contactId: snapshot.id,\n email,\n hostId,\n groupId,\n previousStage: held ?? '',\n lifecycleStage,\n }\n}\n"],"names":["consentGroupForHost","advanceContactLifecycleStage","contactFacetPath","isContactLifecycleStage","readContactFacet","FieldValue","restampCrmListFieldsAt","floorContactLifecycleStage","options","snapshot","data","contactRef","org","floor","get","exists","outcome","contactId","id","hostId","String","trim","groupId","facet","held","lifecycleStage","undefined","email","ref","update","updatedAt","serverTimestamp","previousStage"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GAED,SACEA,mBAAmB,EACnBC,4BAA4B,EAC5BC,gBAAgB,EAEhBC,uBAAuB,EACvBC,gBAAgB,QACX,sBAAqB;AAC5B,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,sBAAsB,QAAQ,mBAAe;AAoDtD,OAAO,eAAeC,2BACpBC,OAAqC;QAKvBC,gBACQD,iBAAuCE,cAQtCT,+BACFS;IAbrB,MAAM,EAAEC,UAAU,EAAEC,GAAG,EAAEC,KAAK,EAAE,GAAGL;IACnC,MAAMC,WAAW,MAAME,WAAWG,GAAG;IACrC,IAAI,CAACL,SAASM,MAAM,EAAE,OAAO;QAAEC,SAAS;QAAWC,WAAWN,WAAWO,EAAE;IAAC;IAC5E,MAAMR,QAAQD,iBAAAA,SAASC,IAAI,cAAbD,iBAAmB,CAAC;IAClC,MAAMU,SAASC,QAAOZ,kBAAAA,QAAQW,MAAM,YAAdX,kBAAkB,IAAIa,IAAI,MAAMD,QAAOV,eAAAA,IAAI,CAAC,SAAS,YAAdA,eAAkB,IAAIW,IAAI;IACvF,IAAI,CAACF,QAAQ,OAAO;QAAEH,SAAS;QAAUC,WAAWR,SAASS,EAAE;IAAC;IAEhE,MAAM,EAAEI,OAAO,EAAE,GAAGtB,oBAAoBY,cAAAA,MAAO,MAAMO;IACrD,MAAMI,QAAQnB,iBAAiBM,MAAMY;IACrC,MAAME,OAAOrB,wBAAwBoB,MAAME,cAAc,IACrDF,MAAME,cAAc,GACpBC;IACJ,MAAMD,kBAAiBxB,gCAAAA,6BAA6BuB,MAAMX,kBAAnCZ,gCAA6CY;IACpE,MAAMc,QAAQP,QAAOV,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAiB;IACtC,IAAIe,mBAAmBD,MAAM;QAC3B,OAAO;YAAER,SAAS;YAAQC,WAAWR,SAASS,EAAE;YAAES;YAAOR;YAAQG;YAASG;QAAe;IAC3F;IACA,MAAMhB,SAASmB,GAAG,CAACC,MAAM,CAAC;QACxB,CAAC3B,iBAAiBoB,SAAS,kBAAkB,EAAEG;QAC/CK,WAAWzB,WAAW0B,eAAe;IACvC;IACA,6DAA6D;IAC7D,MAAMzB,uBAAuBG,SAASmB,GAAG,EAAE;IAC3C,OAAO;QACLZ,SAAS;QACTC,WAAWR,SAASS,EAAE;QACtBS;QACAR;QACAG;QACAU,aAAa,EAAER,eAAAA,OAAQ;QACvBC;IACF;AACF"}