@aglyn/tenant-data-admin 1.0.0-beta.202 → 1.0.0-beta.204

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 (51) hide show
  1. package/package.json +7 -7
  2. package/src/lib/server/capture-email-check.d.ts +109 -0
  3. package/src/lib/server/capture-email-check.js +362 -0
  4. package/src/lib/server/capture-email-check.js.map +1 -0
  5. package/src/lib/server/card-payment-velocity.d.ts +11 -16
  6. package/src/lib/server/card-payment-velocity.js +24 -28
  7. package/src/lib/server/card-payment-velocity.js.map +1 -1
  8. package/src/lib/server/email-deliverability.d.ts +19 -3
  9. package/src/lib/server/email-deliverability.js +34 -16
  10. package/src/lib/server/email-deliverability.js.map +1 -1
  11. package/src/lib/server/held-page-subject.d.ts +93 -0
  12. package/src/lib/server/held-page-subject.js +206 -0
  13. package/src/lib/server/held-page-subject.js.map +1 -0
  14. package/src/lib/server/host-email-tokens.d.ts +42 -1
  15. package/src/lib/server/host-email-tokens.js +100 -12
  16. package/src/lib/server/host-email-tokens.js.map +1 -1
  17. package/src/lib/server/host-visitor-records.js +21 -5
  18. package/src/lib/server/host-visitor-records.js.map +1 -1
  19. package/src/lib/server/hosted-page-review.d.ts +16 -5
  20. package/src/lib/server/hosted-page-review.js +242 -61
  21. package/src/lib/server/hosted-page-review.js.map +1 -1
  22. package/src/lib/server/list-members.js +16 -0
  23. package/src/lib/server/list-members.js.map +1 -1
  24. package/src/lib/server/outbound-send-review.d.ts +22 -0
  25. package/src/lib/server/outbound-send-review.js +74 -6
  26. package/src/lib/server/outbound-send-review.js.map +1 -1
  27. package/src/lib/server/payment-fraud-signal.d.ts +20 -12
  28. package/src/lib/server/payment-fraud-signal.js +41 -11
  29. package/src/lib/server/payment-fraud-signal.js.map +1 -1
  30. package/src/lib/server/payment-risk-record.d.ts +37 -13
  31. package/src/lib/server/payment-risk-record.js +23 -8
  32. package/src/lib/server/payment-risk-record.js.map +1 -1
  33. package/src/lib/server/render-system-email.d.ts +3 -0
  34. package/src/lib/server/render-system-email.js +39 -11
  35. package/src/lib/server/render-system-email.js.map +1 -1
  36. package/src/lib/server/risk-notice.d.ts +327 -0
  37. package/src/lib/server/risk-notice.js +1240 -0
  38. package/src/lib/server/risk-notice.js.map +1 -0
  39. package/src/lib/server/sender-readiness.d.ts +36 -0
  40. package/src/lib/server/sender-readiness.js +99 -0
  41. package/src/lib/server/sender-readiness.js.map +1 -0
  42. package/src/lib/server/stripe-transfer-reversal.d.ts +63 -0
  43. package/src/lib/server/stripe-transfer-reversal.js +177 -0
  44. package/src/lib/server/stripe-transfer-reversal.js.map +1 -0
  45. package/src/lib/server/test-firestore.js +1 -0
  46. package/src/lib/server/test-firestore.js.map +1 -1
  47. package/src/lib/server/upsert-contact.js +19 -3
  48. package/src/lib/server/upsert-contact.js.map +1 -1
  49. package/src/tenancy.d.ts +2 -0
  50. package/src/tenancy.js +3 -0
  51. package/src/tenancy.js.map +1 -1
@@ -0,0 +1,177 @@
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 SELLER'S SHARE OF A DESTINATION CHARGE, PULLED BACK (AGL-1794, AGL-3363).
18
+ *
19
+ * Every tenant sale is a destination charge on the platform account, so a
20
+ * lost dispute debits the PLATFORM's balance while the seller keeps the
21
+ * transfer. The AGL-1794 policy: the seller returns their share by a
22
+ * `transfers/{id}/reversals` call; the platform keeps Stripe's dispute fee.
23
+ * This is the Stripe half of that, written once. It started as the commerce
24
+ * webhook's `reverseSellerShare`; the booking deposit's lost dispute uses it
25
+ * too (AGL-3363), so the two cannot drift. Each caller keeps its own record
26
+ * and its own settle marker — this module touches no Firestore.
27
+ *
28
+ * PROPORTIONAL, NEVER MORE: `cause × transfer.amount ÷ charge.amount`,
29
+ * FLOORED, capped at what the transfer has left (`amount − amount_reversed`).
30
+ * No `refund_application_fee`: on a chargeback the platform keeps its
31
+ * commission, a policy switch the AGL-1794 note says not to flip here.
32
+ *
33
+ * IDEMPOTENT at Stripe twice over: a reversal already on the transfer carrying
34
+ * this cause's id in its metadata is ADOPTED rather than repeated (the crash
35
+ * window between the POST and the caller's record), and the POST carries an
36
+ * `Idempotency-Key` derived from the cause, so a racing delivery gets Stripe's
37
+ * stored answer.
38
+ *
39
+ * TRANSIENT failures (a network reject, 429, 5xx) THROW, so the webhook's
40
+ * redelivery is the retry. DEFINITIVE ones answer `not-reversed` with a
41
+ * reason, for the caller to settle and record; no redelivery fixes them.
42
+ *=========================================*/ /** Why nothing was pulled back, when the answer is final. */ /** Stripe failures a redelivery can actually fix. */ export function isTransientStripeStatus(status) {
43
+ return status === 429 || status >= 500;
44
+ }
45
+ async function stripeGet(url, stripeKey, fetchImpl) {
46
+ const response = await fetchImpl(url, {
47
+ headers: {
48
+ Authorization: `Bearer ${stripeKey}`
49
+ }
50
+ });
51
+ const body = await response.json().catch(()=>null);
52
+ return {
53
+ ok: response.ok,
54
+ status: response.status,
55
+ body
56
+ };
57
+ }
58
+ /** The metadata key a cause's reversal is stamped and found by. */ export function transferReversalMetadataKey(kind) {
59
+ return kind === 'dispute' ? 'disputeId' : 'refundId';
60
+ }
61
+ /**
62
+ * Pull the seller's proportional share of `cause` back from the charge's
63
+ * transfer. See the module header for the rules.
64
+ */ export async function reverseDestinationTransfer(cause, deps = {}) {
65
+ var _deps_stripeKey, _deps_fetchImpl, _cause_id, _cause_chargeId, _ref, _ref1, _ref2, _ref3, _ref4, _cause_amountCents, _cause_metadata, _ref5, _ref6;
66
+ var _charge_body, _charge_body1, _transfer_body_reversals, _transfer_body, _transfer_body1, _transfer_body2;
67
+ const stripeKey = (_deps_stripeKey = deps.stripeKey) != null ? _deps_stripeKey : process.env.STRIPE_SECRET_KEY;
68
+ if (!stripeKey) return {
69
+ kind: 'skipped',
70
+ reason: 'no-stripe-key'
71
+ };
72
+ const fetchImpl = (_deps_fetchImpl = deps.fetchImpl) != null ? _deps_fetchImpl : fetch;
73
+ const causeId = String((_cause_id = cause.id) != null ? _cause_id : '');
74
+ const metadataKey = transferReversalMetadataKey(cause.kind);
75
+ const label = `${cause.kind} ${causeId}`;
76
+ const chargeId = String((_cause_chargeId = cause.chargeId) != null ? _cause_chargeId : '');
77
+ if (!chargeId) return {
78
+ kind: 'not-reversed',
79
+ reason: 'no-charge-on-cause'
80
+ };
81
+ const charge = await stripeGet(`https://api.stripe.com/v1/charges/${chargeId}`, stripeKey, fetchImpl);
82
+ if (!charge.ok) {
83
+ var _charge_body2;
84
+ if (isTransientStripeStatus(charge.status)) {
85
+ throw new Error(`Stripe charge read failed (${charge.status}) for ${label}`);
86
+ }
87
+ console.error('Stripe refused the charge read', (_charge_body2 = charge.body) == null ? void 0 : _charge_body2.error);
88
+ return {
89
+ kind: 'not-reversed',
90
+ reason: 'charge-read-refused'
91
+ };
92
+ }
93
+ const transferId = String((_ref = (_charge_body = charge.body) == null ? void 0 : _charge_body.transfer) != null ? _ref : '');
94
+ const chargeAmountCents = Math.round(Number((_ref1 = (_charge_body1 = charge.body) == null ? void 0 : _charge_body1.amount) != null ? _ref1 : 0));
95
+ if (!transferId || !(chargeAmountCents > 0)) {
96
+ return {
97
+ kind: 'not-reversed',
98
+ reason: 'no-transfer-on-charge'
99
+ };
100
+ }
101
+ const transfer = await stripeGet(`https://api.stripe.com/v1/transfers/${transferId}`, stripeKey, fetchImpl);
102
+ if (!transfer.ok) {
103
+ var _transfer_body3;
104
+ if (isTransientStripeStatus(transfer.status)) {
105
+ throw new Error(`Stripe transfer read failed (${transfer.status}) for ${label}`);
106
+ }
107
+ console.error('Stripe refused the transfer read', (_transfer_body3 = transfer.body) == null ? void 0 : _transfer_body3.error);
108
+ return {
109
+ kind: 'not-reversed',
110
+ reason: 'transfer-read-refused'
111
+ };
112
+ }
113
+ // The crash window's backstop: the POST landed on a previous delivery and
114
+ // the caller's record did not. Adopt it rather than creating a second one.
115
+ const existing = ((_ref2 = (_transfer_body = transfer.body) == null ? void 0 : (_transfer_body_reversals = _transfer_body.reversals) == null ? void 0 : _transfer_body_reversals.data) != null ? _ref2 : []).find((item)=>{
116
+ var _ref;
117
+ var _item_metadata;
118
+ return String((_ref = item == null ? void 0 : (_item_metadata = item.metadata) == null ? void 0 : _item_metadata[metadataKey]) != null ? _ref : '') === causeId;
119
+ });
120
+ if (existing) {
121
+ var _existing_amount, _existing_id;
122
+ return {
123
+ kind: 'reversed',
124
+ cents: Math.round(Number((_existing_amount = existing.amount) != null ? _existing_amount : 0)),
125
+ reversalId: String((_existing_id = existing.id) != null ? _existing_id : ''),
126
+ adopted: true
127
+ };
128
+ }
129
+ const transferCents = Math.round(Number((_ref3 = (_transfer_body1 = transfer.body) == null ? void 0 : _transfer_body1.amount) != null ? _ref3 : 0));
130
+ const alreadyReversedCents = Math.round(Number((_ref4 = (_transfer_body2 = transfer.body) == null ? void 0 : _transfer_body2.amount_reversed) != null ? _ref4 : 0));
131
+ const remainingCents = Math.max(0, transferCents - alreadyReversedCents);
132
+ const causeCents = Math.round(Number((_cause_amountCents = cause.amountCents) != null ? _cause_amountCents : 0));
133
+ const shareCents = Math.min(Math.floor(causeCents * transferCents / chargeAmountCents), remainingCents);
134
+ if (!(shareCents > 0)) {
135
+ return {
136
+ kind: 'not-reversed',
137
+ reason: 'transfer-fully-reversed',
138
+ owedCents: 0
139
+ };
140
+ }
141
+ const params = new URLSearchParams({
142
+ amount: String(shareCents),
143
+ [`metadata[${metadataKey}]`]: causeId
144
+ });
145
+ for (const [key, value] of Object.entries((_cause_metadata = cause.metadata) != null ? _cause_metadata : {})){
146
+ params.set(`metadata[${key}]`, value);
147
+ }
148
+ const response = await fetchImpl(`https://api.stripe.com/v1/transfers/${transferId}/reversals`, {
149
+ method: 'POST',
150
+ headers: {
151
+ Authorization: `Bearer ${stripeKey}`,
152
+ 'Content-Type': 'application/x-www-form-urlencoded',
153
+ 'Idempotency-Key': `${cause.kind}-reversal-${causeId}`
154
+ },
155
+ body: params.toString()
156
+ });
157
+ const reversal = await response.json().catch(()=>null);
158
+ if (!response.ok) {
159
+ if (isTransientStripeStatus(response.status)) {
160
+ throw new Error(`Stripe transfer reversal failed (${response.status}) for ${label}`);
161
+ }
162
+ console.error('Stripe refused the transfer reversal', reversal == null ? void 0 : reversal.error);
163
+ return {
164
+ kind: 'not-reversed',
165
+ reason: 'reversal-refused',
166
+ owedCents: shareCents
167
+ };
168
+ }
169
+ return {
170
+ kind: 'reversed',
171
+ cents: Math.round(Number((_ref5 = reversal == null ? void 0 : reversal.amount) != null ? _ref5 : shareCents)),
172
+ reversalId: String((_ref6 = reversal == null ? void 0 : reversal.id) != null ? _ref6 : ''),
173
+ adopted: false
174
+ };
175
+ }
176
+
177
+ //# sourceMappingURL=stripe-transfer-reversal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/stripe-transfer-reversal.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 SELLER'S SHARE OF A DESTINATION CHARGE, PULLED BACK (AGL-1794, AGL-3363).\n *\n * Every tenant sale is a destination charge on the platform account, so a\n * lost dispute debits the PLATFORM's balance while the seller keeps the\n * transfer. The AGL-1794 policy: the seller returns their share by a\n * `transfers/{id}/reversals` call; the platform keeps Stripe's dispute fee.\n * This is the Stripe half of that, written once. It started as the commerce\n * webhook's `reverseSellerShare`; the booking deposit's lost dispute uses it\n * too (AGL-3363), so the two cannot drift. Each caller keeps its own record\n * and its own settle marker — this module touches no Firestore.\n *\n * PROPORTIONAL, NEVER MORE: `cause × transfer.amount ÷ charge.amount`,\n * FLOORED, capped at what the transfer has left (`amount − amount_reversed`).\n * No `refund_application_fee`: on a chargeback the platform keeps its\n * commission, a policy switch the AGL-1794 note says not to flip here.\n *\n * IDEMPOTENT at Stripe twice over: a reversal already on the transfer carrying\n * this cause's id in its metadata is ADOPTED rather than repeated (the crash\n * window between the POST and the caller's record), and the POST carries an\n * `Idempotency-Key` derived from the cause, so a racing delivery gets Stripe's\n * stored answer.\n *\n * TRANSIENT failures (a network reject, 429, 5xx) THROW, so the webhook's\n * redelivery is the retry. DEFINITIVE ones answer `not-reversed` with a\n * reason, for the caller to settle and record; no redelivery fixes them.\n *=========================================*/\n\n/** Why nothing was pulled back, when the answer is final. */\nexport type TransferReversalFailure =\n | 'no-charge-on-cause'\n | 'charge-read-refused'\n | 'no-transfer-on-charge'\n | 'transfer-read-refused'\n | 'transfer-fully-reversed'\n | 'reversal-refused'\n\nexport type TransferReversalOutcome =\n /** Pulled back now, or found already pulled back and adopted. */\n | { kind: 'reversed'; cents: number; reversalId: string; adopted: boolean }\n /** Final: settle it and record why. `owedCents` when the share was known. */\n | { kind: 'not-reversed'; reason: TransferReversalFailure; owedCents?: number }\n /** Nothing was attempted and nothing should be settled: retry later. */\n | { kind: 'skipped'; reason: 'no-stripe-key' }\n\nexport interface TransferReversalCause {\n /** Which door: decides the metadata key and the idempotency key. */\n kind: 'dispute' | 'refund'\n /** The dispute's or refund's Stripe id. */\n id: string\n /** Cents going back to the cardholder. */\n amountCents: number\n /** The charge whose transfer is pulled back. */\n chargeId: string\n /** More metadata for the reversal: the caller's record id. */\n metadata?: Record<string, string>\n}\n\n/** Stripe failures a redelivery can actually fix. */\nexport function isTransientStripeStatus(status: number): boolean {\n return status === 429 || status >= 500\n}\n\ntype FetchLike = typeof fetch\n\nasync function stripeGet(\n url: string,\n stripeKey: string,\n fetchImpl: FetchLike,\n): Promise<{ ok: boolean; status: number; body: any }> {\n const response = await fetchImpl(url, {\n headers: { Authorization: `Bearer ${stripeKey}` },\n })\n const body = await response.json().catch(() => null)\n return { ok: response.ok, status: response.status, body }\n}\n\n/** The metadata key a cause's reversal is stamped and found by. */\nexport function transferReversalMetadataKey(kind: TransferReversalCause['kind']): string {\n return kind === 'dispute' ? 'disputeId' : 'refundId'\n}\n\n/**\n * Pull the seller's proportional share of `cause` back from the charge's\n * transfer. See the module header for the rules.\n */\nexport async function reverseDestinationTransfer(\n cause: TransferReversalCause,\n deps: { stripeKey?: string; fetchImpl?: FetchLike } = {},\n): Promise<TransferReversalOutcome> {\n const stripeKey = deps.stripeKey ?? process.env.STRIPE_SECRET_KEY\n if (!stripeKey) return { kind: 'skipped', reason: 'no-stripe-key' }\n const fetchImpl = deps.fetchImpl ?? fetch\n const causeId = String(cause.id ?? '')\n const metadataKey = transferReversalMetadataKey(cause.kind)\n const label = `${cause.kind} ${causeId}`\n\n const chargeId = String(cause.chargeId ?? '')\n if (!chargeId) return { kind: 'not-reversed', reason: 'no-charge-on-cause' }\n const charge = await stripeGet(\n `https://api.stripe.com/v1/charges/${chargeId}`,\n stripeKey,\n fetchImpl,\n )\n if (!charge.ok) {\n if (isTransientStripeStatus(charge.status)) {\n throw new Error(`Stripe charge read failed (${charge.status}) for ${label}`)\n }\n console.error('Stripe refused the charge read', charge.body?.error)\n return { kind: 'not-reversed', reason: 'charge-read-refused' }\n }\n const transferId = String(charge.body?.transfer ?? '')\n const chargeAmountCents = Math.round(Number(charge.body?.amount ?? 0))\n if (!transferId || !(chargeAmountCents > 0)) {\n return { kind: 'not-reversed', reason: 'no-transfer-on-charge' }\n }\n const transfer = await stripeGet(\n `https://api.stripe.com/v1/transfers/${transferId}`,\n stripeKey,\n fetchImpl,\n )\n if (!transfer.ok) {\n if (isTransientStripeStatus(transfer.status)) {\n throw new Error(`Stripe transfer read failed (${transfer.status}) for ${label}`)\n }\n console.error('Stripe refused the transfer read', transfer.body?.error)\n return { kind: 'not-reversed', reason: 'transfer-read-refused' }\n }\n // The crash window's backstop: the POST landed on a previous delivery and\n // the caller's record did not. Adopt it rather than creating a second one.\n const existing = ((transfer.body?.reversals?.data ?? []) as any[]).find(\n (item) => String(item?.metadata?.[metadataKey] ?? '') === causeId,\n )\n if (existing) {\n return {\n kind: 'reversed',\n cents: Math.round(Number(existing.amount ?? 0)),\n reversalId: String(existing.id ?? ''),\n adopted: true,\n }\n }\n const transferCents = Math.round(Number(transfer.body?.amount ?? 0))\n const alreadyReversedCents = Math.round(Number(transfer.body?.amount_reversed ?? 0))\n const remainingCents = Math.max(0, transferCents - alreadyReversedCents)\n const causeCents = Math.round(Number(cause.amountCents ?? 0))\n const shareCents = Math.min(\n Math.floor((causeCents * transferCents) / chargeAmountCents),\n remainingCents,\n )\n if (!(shareCents > 0)) {\n return { kind: 'not-reversed', reason: 'transfer-fully-reversed', owedCents: 0 }\n }\n const params = new URLSearchParams({\n amount: String(shareCents),\n [`metadata[${metadataKey}]`]: causeId,\n })\n for (const [key, value] of Object.entries(cause.metadata ?? {})) {\n params.set(`metadata[${key}]`, value)\n }\n const response = await fetchImpl(\n `https://api.stripe.com/v1/transfers/${transferId}/reversals`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${stripeKey}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'Idempotency-Key': `${cause.kind}-reversal-${causeId}`,\n },\n body: params.toString(),\n },\n )\n const reversal = await response.json().catch(() => null)\n if (!response.ok) {\n if (isTransientStripeStatus(response.status)) {\n throw new Error(`Stripe transfer reversal failed (${response.status}) for ${label}`)\n }\n console.error('Stripe refused the transfer reversal', reversal?.error)\n return { kind: 'not-reversed', reason: 'reversal-refused', owedCents: shareCents }\n }\n return {\n kind: 'reversed',\n cents: Math.round(Number(reversal?.amount ?? shareCents)),\n reversalId: String(reversal?.id ?? ''),\n adopted: false,\n }\n}\n"],"names":["isTransientStripeStatus","status","stripeGet","url","stripeKey","fetchImpl","response","headers","Authorization","body","json","catch","ok","transferReversalMetadataKey","kind","reverseDestinationTransfer","cause","deps","charge","transfer","process","env","STRIPE_SECRET_KEY","reason","fetch","causeId","String","id","metadataKey","label","chargeId","Error","console","error","transferId","chargeAmountCents","Math","round","Number","amount","existing","reversals","data","find","item","metadata","cents","reversalId","adopted","transferCents","alreadyReversedCents","amount_reversed","remainingCents","max","causeCents","amountCents","shareCents","min","floor","owedCents","params","URLSearchParams","key","value","Object","entries","set","method","toString","reversal"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;2CA0B2C,GAE3C,2DAA2D,GA8B3D,mDAAmD,GACnD,OAAO,SAASA,wBAAwBC,MAAc;IACpD,OAAOA,WAAW,OAAOA,UAAU;AACrC;AAIA,eAAeC,UACbC,GAAW,EACXC,SAAiB,EACjBC,SAAoB;IAEpB,MAAMC,WAAW,MAAMD,UAAUF,KAAK;QACpCI,SAAS;YAAEC,eAAe,CAAC,OAAO,EAAEJ,WAAW;QAAC;IAClD;IACA,MAAMK,OAAO,MAAMH,SAASI,IAAI,GAAGC,KAAK,CAAC,IAAM;IAC/C,OAAO;QAAEC,IAAIN,SAASM,EAAE;QAAEX,QAAQK,SAASL,MAAM;QAAEQ;IAAK;AAC1D;AAEA,iEAAiE,GACjE,OAAO,SAASI,4BAA4BC,IAAmC;IAC7E,OAAOA,SAAS,YAAY,cAAc;AAC5C;AAEA;;;CAGC,GACD,OAAO,eAAeC,2BACpBC,KAA4B,EAC5BC,OAAsD,CAAC,CAAC;QAEtCA,iBAEAA,iBACKD,WAICA,mDA+CaA,oBAYKA;QA7ChBE,cACkBA,eAkBzBC,0BAAAA,gBAWqBA,iBACOA;IApD/C,MAAMf,aAAYa,kBAAAA,KAAKb,SAAS,YAAda,kBAAkBG,QAAQC,GAAG,CAACC,iBAAiB;IACjE,IAAI,CAAClB,WAAW,OAAO;QAAEU,MAAM;QAAWS,QAAQ;IAAgB;IAClE,MAAMlB,aAAYY,kBAAAA,KAAKZ,SAAS,YAAdY,kBAAkBO;IACpC,MAAMC,UAAUC,QAAOV,YAAAA,MAAMW,EAAE,YAARX,YAAY;IACnC,MAAMY,cAAcf,4BAA4BG,MAAMF,IAAI;IAC1D,MAAMe,QAAQ,GAAGb,MAAMF,IAAI,CAAC,CAAC,EAAEW,SAAS;IAExC,MAAMK,WAAWJ,QAAOV,kBAAAA,MAAMc,QAAQ,YAAdd,kBAAkB;IAC1C,IAAI,CAACc,UAAU,OAAO;QAAEhB,MAAM;QAAgBS,QAAQ;IAAqB;IAC3E,MAAML,SAAS,MAAMhB,UACnB,CAAC,kCAAkC,EAAE4B,UAAU,EAC/C1B,WACAC;IAEF,IAAI,CAACa,OAAON,EAAE,EAAE;YAIkCM;QAHhD,IAAIlB,wBAAwBkB,OAAOjB,MAAM,GAAG;YAC1C,MAAM,IAAI8B,MAAM,CAAC,2BAA2B,EAAEb,OAAOjB,MAAM,CAAC,MAAM,EAAE4B,OAAO;QAC7E;QACAG,QAAQC,KAAK,CAAC,mCAAkCf,gBAAAA,OAAOT,IAAI,qBAAXS,cAAae,KAAK;QAClE,OAAO;YAAEnB,MAAM;YAAgBS,QAAQ;QAAsB;IAC/D;IACA,MAAMW,aAAaR,gBAAOR,eAAAA,OAAOT,IAAI,qBAAXS,aAAaC,QAAQ,mBAAI;IACnD,MAAMgB,oBAAoBC,KAAKC,KAAK,CAACC,iBAAOpB,gBAAAA,OAAOT,IAAI,qBAAXS,cAAaqB,MAAM,oBAAI;IACnE,IAAI,CAACL,cAAc,CAAEC,CAAAA,oBAAoB,CAAA,GAAI;QAC3C,OAAO;YAAErB,MAAM;YAAgBS,QAAQ;QAAwB;IACjE;IACA,MAAMJ,WAAW,MAAMjB,UACrB,CAAC,oCAAoC,EAAEgC,YAAY,EACnD9B,WACAC;IAEF,IAAI,CAACc,SAASP,EAAE,EAAE;YAIkCO;QAHlD,IAAInB,wBAAwBmB,SAASlB,MAAM,GAAG;YAC5C,MAAM,IAAI8B,MAAM,CAAC,6BAA6B,EAAEZ,SAASlB,MAAM,CAAC,MAAM,EAAE4B,OAAO;QACjF;QACAG,QAAQC,KAAK,CAAC,qCAAoCd,kBAAAA,SAASV,IAAI,qBAAbU,gBAAec,KAAK;QACtE,OAAO;YAAEnB,MAAM;YAAgBS,QAAQ;QAAwB;IACjE;IACA,0EAA0E;IAC1E,2EAA2E;IAC3E,MAAMiB,WAAW,WAAErB,iBAAAA,SAASV,IAAI,sBAAbU,2BAAAA,eAAesB,SAAS,qBAAxBtB,yBAA0BuB,IAAI,oBAAI,EAAE,EAAYC,IAAI,CACrE,CAACC;;YAAgBA;eAAPlB,eAAOkB,yBAAAA,iBAAAA,KAAMC,QAAQ,qBAAdD,cAAgB,CAAChB,YAAY,mBAAI,QAAQH;;IAE5D,IAAIe,UAAU;YAGeA,kBACNA;QAHrB,OAAO;YACL1B,MAAM;YACNgC,OAAOV,KAAKC,KAAK,CAACC,QAAOE,mBAAAA,SAASD,MAAM,YAAfC,mBAAmB;YAC5CO,YAAYrB,QAAOc,eAAAA,SAASb,EAAE,YAAXa,eAAe;YAClCQ,SAAS;QACX;IACF;IACA,MAAMC,gBAAgBb,KAAKC,KAAK,CAACC,iBAAOnB,kBAAAA,SAASV,IAAI,qBAAbU,gBAAeoB,MAAM,oBAAI;IACjE,MAAMW,uBAAuBd,KAAKC,KAAK,CAACC,iBAAOnB,kBAAAA,SAASV,IAAI,qBAAbU,gBAAegC,eAAe,oBAAI;IACjF,MAAMC,iBAAiBhB,KAAKiB,GAAG,CAAC,GAAGJ,gBAAgBC;IACnD,MAAMI,aAAalB,KAAKC,KAAK,CAACC,QAAOtB,qBAAAA,MAAMuC,WAAW,YAAjBvC,qBAAqB;IAC1D,MAAMwC,aAAapB,KAAKqB,GAAG,CACzBrB,KAAKsB,KAAK,CAAC,AAACJ,aAAaL,gBAAiBd,oBAC1CiB;IAEF,IAAI,CAAEI,CAAAA,aAAa,CAAA,GAAI;QACrB,OAAO;YAAE1C,MAAM;YAAgBS,QAAQ;YAA2BoC,WAAW;QAAE;IACjF;IACA,MAAMC,SAAS,IAAIC,gBAAgB;QACjCtB,QAAQb,OAAO8B;QACf,CAAC,CAAC,SAAS,EAAE5B,YAAY,CAAC,CAAC,CAAC,EAAEH;IAChC;IACA,KAAK,MAAM,CAACqC,KAAKC,MAAM,IAAIC,OAAOC,OAAO,EAACjD,kBAAAA,MAAM6B,QAAQ,YAAd7B,kBAAkB,CAAC,GAAI;QAC/D4C,OAAOM,GAAG,CAAC,CAAC,SAAS,EAAEJ,IAAI,CAAC,CAAC,EAAEC;IACjC;IACA,MAAMzD,WAAW,MAAMD,UACrB,CAAC,oCAAoC,EAAE6B,WAAW,UAAU,CAAC,EAC7D;QACEiC,QAAQ;QACR5D,SAAS;YACPC,eAAe,CAAC,OAAO,EAAEJ,WAAW;YACpC,gBAAgB;YAChB,mBAAmB,GAAGY,MAAMF,IAAI,CAAC,UAAU,EAAEW,SAAS;QACxD;QACAhB,MAAMmD,OAAOQ,QAAQ;IACvB;IAEF,MAAMC,WAAW,MAAM/D,SAASI,IAAI,GAAGC,KAAK,CAAC,IAAM;IACnD,IAAI,CAACL,SAASM,EAAE,EAAE;QAChB,IAAIZ,wBAAwBM,SAASL,MAAM,GAAG;YAC5C,MAAM,IAAI8B,MAAM,CAAC,iCAAiC,EAAEzB,SAASL,MAAM,CAAC,MAAM,EAAE4B,OAAO;QACrF;QACAG,QAAQC,KAAK,CAAC,wCAAwCoC,4BAAAA,SAAUpC,KAAK;QACrE,OAAO;YAAEnB,MAAM;YAAgBS,QAAQ;YAAoBoC,WAAWH;QAAW;IACnF;IACA,OAAO;QACL1C,MAAM;QACNgC,OAAOV,KAAKC,KAAK,CAACC,gBAAO+B,4BAAAA,SAAU9B,MAAM,oBAAIiB;QAC7CT,YAAYrB,gBAAO2C,4BAAAA,SAAU1C,EAAE,oBAAI;QACnCqB,SAAS;IACX;AACF"}
@@ -101,6 +101,7 @@ export function fakeFirestore(seed = {}) {
101
101
  }),
102
102
  // Ordering and limits are not what these specs are about; the queue
103
103
  // read just needs to return everything it stored.
104
+ where: ()=>api,
104
105
  orderBy: ()=>api,
105
106
  limit: ()=>api,
106
107
  get: async ()=>({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/test-firestore.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 Firestore stand-in for specs that span MORE THAN ONE COLLECTION\n * (AGL-1592). Test-only; deliberately not exported from the barrel.\n *\n * The one-collection fakes elsewhere in this directory return the same\n * document whatever collection name they are handed, which is fine when the\n * code under test touches one document and actively misleading when it does\n * not: the phone opt-out path reads `users/{uid}` and `contactSuppressions/{n}`\n * and the whole point is that they are different records. A fake that conflated\n * them would pass while the real thing wrote the marker on top of the\n * suppression.\n *\n * `FieldValue.delete()` is honoured for real, because \"the field is gone\" and\n * \"the field holds a sentinel object\" are different states and only the first\n * one makes `seedUserProfile`'s blank check fire.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\n\nconst DELETE = FieldValue.delete()\n\nexport interface FakeFirestore {\n /**\n * Every document in a collection, keyed by id.\n *\n * A SUBcollection is addressed by its full path — `hosts/host-1/suppressions`\n * — which is also how it is seeded.\n */\n docs: (collection: string) => Record<string, any>\n collection: (name: string) => any\n /**\n * Several documents in one round trip, the shape the real `getAll` has:\n * the results come back POSITIONALLY, one per reference, so a caller that\n * zips them back against its own input list gets what it asked for.\n */\n getAll: (...refs: any[]) => Promise<any[]>\n /**\n * A transaction, real enough for a read-then-write body.\n *\n * Reads pass straight through and writes are held until the body resolves,\n * which is the property the code under test depends on: a writer that\n * decides what to store FROM what it read must not observe its own\n * half-finished write. It does not model contention or retries — nothing in\n * this repo's transaction bodies branches on a retry, and a fake that\n * pretended to would be asserting the fake.\n */\n runTransaction: <T>(body: (transaction: any) => Promise<T>) => Promise<T>\n}\n\nexport function fakeFirestore(\n seed: Record<string, Record<string, any>> = {},\n): FakeFirestore {\n const store: Record<string, Record<string, any>> = JSON.parse(\n JSON.stringify(seed),\n )\n\n const snapshotFor = (collection: string, id: string) => {\n const data = store[collection]?.[id]\n return {\n id,\n exists: data !== undefined,\n get: (field: string) => data?.[field],\n data: () => data,\n }\n }\n\n const fake: FakeFirestore = {\n docs: (collection: string) => store[collection] ?? {},\n getAll: async (...refs: any[]) =>\n refs.map((ref) => snapshotFor(ref.collectionPath, ref.id)),\n async runTransaction<T>(body: (transaction: any) => Promise<T>): Promise<T> {\n const pending: Array<() => Promise<void>> = []\n const transaction = {\n get: (ref: any) => ref.get(),\n set: (ref: any, data: Record<string, any>, options?: { merge?: boolean }) => {\n pending.push(() => ref.set(data, options))\n return transaction\n },\n }\n const result = await body(transaction)\n for (const write of pending) await write()\n return result\n },\n collection(name: string) {\n store[name] = store[name] ?? {}\n const api: any = {\n doc: (id: string) => ({\n id,\n /**\n * Which collection this reference came from, so `getAll` can resolve\n * a bare reference the way the real client does.\n */\n collectionPath: name,\n /**\n * A subcollection is a collection at the joined path, so the fake\n * stays one flat map. Nesting matters here because a per-site list\n * lives under the site — a fake that returned the same documents for\n * `hosts/a/suppressions` and `hosts/b/suppressions` would certify a\n * sender that leaks one site's unsubscribes into another's.\n */\n collection: (sub: string) => fake.collection(`${name}/${id}/${sub}`),\n get: async () => snapshotFor(name, id),\n set: async (data: Record<string, any>, options?: { merge?: boolean }) => {\n const base = options?.merge ? (store[name][id] ?? {}) : {}\n const next: Record<string, any> = { ...base }\n for (const [key, value] of Object.entries(data)) {\n // A real delete, not a stored sentinel — see the header.\n if (value && typeof value === 'object' && DELETE.isEqual(value as any)) {\n delete next[key]\n continue\n }\n // Server timestamps arrive as sentinels too. Freeze them into\n // something a spec can assert on and a date can be read from.\n next[key] =\n value && typeof value === 'object' && typeof (value as any).isEqual === 'function'\n ? { seconds: Math.floor(Date.now() / 1000) }\n : value\n }\n store[name][id] = next\n },\n }),\n // Ordering and limits are not what these specs are about; the queue\n // read just needs to return everything it stored.\n orderBy: () => api,\n limit: () => api,\n get: async () => ({\n docs: Object.keys(store[name]).map((id) => snapshotFor(name, id)),\n }),\n add: async (data: Record<string, any>) => {\n const id = `auto-${Object.keys(store[name]).length + 1}`\n store[name][id] = data\n return { id }\n },\n }\n return api\n },\n }\n return fake\n}\n\nexport default fakeFirestore\n"],"names":["FieldValue","DELETE","delete","fakeFirestore","seed","store","JSON","parse","stringify","snapshotFor","collection","id","data","exists","undefined","get","field","fake","docs","getAll","refs","map","ref","collectionPath","runTransaction","body","pending","transaction","set","options","push","result","write","name","api","doc","sub","base","merge","next","key","value","Object","entries","isEqual","seconds","Math","floor","Date","now","orderBy","limit","keys","add","length"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/test-firestore.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 Firestore stand-in for specs that span MORE THAN ONE COLLECTION\n * (AGL-1592). Test-only; deliberately not exported from the barrel.\n *\n * The one-collection fakes elsewhere in this directory return the same\n * document whatever collection name they are handed, which is fine when the\n * code under test touches one document and actively misleading when it does\n * not: the phone opt-out path reads `users/{uid}` and `contactSuppressions/{n}`\n * and the whole point is that they are different records. A fake that conflated\n * them would pass while the real thing wrote the marker on top of the\n * suppression.\n *\n * `FieldValue.delete()` is honoured for real, because \"the field is gone\" and\n * \"the field holds a sentinel object\" are different states and only the first\n * one makes `seedUserProfile`'s blank check fire.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\n\nconst DELETE = FieldValue.delete()\n\nexport interface FakeFirestore {\n /**\n * Every document in a collection, keyed by id.\n *\n * A SUBcollection is addressed by its full path — `hosts/host-1/suppressions`\n * — which is also how it is seeded.\n */\n docs: (collection: string) => Record<string, any>\n collection: (name: string) => any\n /**\n * Several documents in one round trip, the shape the real `getAll` has:\n * the results come back POSITIONALLY, one per reference, so a caller that\n * zips them back against its own input list gets what it asked for.\n */\n getAll: (...refs: any[]) => Promise<any[]>\n /**\n * A transaction, real enough for a read-then-write body.\n *\n * Reads pass straight through and writes are held until the body resolves,\n * which is the property the code under test depends on: a writer that\n * decides what to store FROM what it read must not observe its own\n * half-finished write. It does not model contention or retries — nothing in\n * this repo's transaction bodies branches on a retry, and a fake that\n * pretended to would be asserting the fake.\n */\n runTransaction: <T>(body: (transaction: any) => Promise<T>) => Promise<T>\n}\n\nexport function fakeFirestore(\n seed: Record<string, Record<string, any>> = {},\n): FakeFirestore {\n const store: Record<string, Record<string, any>> = JSON.parse(\n JSON.stringify(seed),\n )\n\n const snapshotFor = (collection: string, id: string) => {\n const data = store[collection]?.[id]\n return {\n id,\n exists: data !== undefined,\n get: (field: string) => data?.[field],\n data: () => data,\n }\n }\n\n const fake: FakeFirestore = {\n docs: (collection: string) => store[collection] ?? {},\n getAll: async (...refs: any[]) =>\n refs.map((ref) => snapshotFor(ref.collectionPath, ref.id)),\n async runTransaction<T>(body: (transaction: any) => Promise<T>): Promise<T> {\n const pending: Array<() => Promise<void>> = []\n const transaction = {\n get: (ref: any) => ref.get(),\n set: (ref: any, data: Record<string, any>, options?: { merge?: boolean }) => {\n pending.push(() => ref.set(data, options))\n return transaction\n },\n }\n const result = await body(transaction)\n for (const write of pending) await write()\n return result\n },\n collection(name: string) {\n store[name] = store[name] ?? {}\n const api: any = {\n doc: (id: string) => ({\n id,\n /**\n * Which collection this reference came from, so `getAll` can resolve\n * a bare reference the way the real client does.\n */\n collectionPath: name,\n /**\n * A subcollection is a collection at the joined path, so the fake\n * stays one flat map. Nesting matters here because a per-site list\n * lives under the site — a fake that returned the same documents for\n * `hosts/a/suppressions` and `hosts/b/suppressions` would certify a\n * sender that leaks one site's unsubscribes into another's.\n */\n collection: (sub: string) => fake.collection(`${name}/${id}/${sub}`),\n get: async () => snapshotFor(name, id),\n set: async (data: Record<string, any>, options?: { merge?: boolean }) => {\n const base = options?.merge ? (store[name][id] ?? {}) : {}\n const next: Record<string, any> = { ...base }\n for (const [key, value] of Object.entries(data)) {\n // A real delete, not a stored sentinel — see the header.\n if (value && typeof value === 'object' && DELETE.isEqual(value as any)) {\n delete next[key]\n continue\n }\n // Server timestamps arrive as sentinels too. Freeze them into\n // something a spec can assert on and a date can be read from.\n next[key] =\n value && typeof value === 'object' && typeof (value as any).isEqual === 'function'\n ? { seconds: Math.floor(Date.now() / 1000) }\n : value\n }\n store[name][id] = next\n },\n }),\n // Ordering and limits are not what these specs are about; the queue\n // read just needs to return everything it stored.\n where: () => api,\n orderBy: () => api,\n limit: () => api,\n get: async () => ({\n docs: Object.keys(store[name]).map((id) => snapshotFor(name, id)),\n }),\n add: async (data: Record<string, any>) => {\n const id = `auto-${Object.keys(store[name]).length + 1}`\n store[name][id] = data\n return { id }\n },\n }\n return api\n },\n }\n return fake\n}\n\nexport default fakeFirestore\n"],"names":["FieldValue","DELETE","delete","fakeFirestore","seed","store","JSON","parse","stringify","snapshotFor","collection","id","data","exists","undefined","get","field","fake","docs","getAll","refs","map","ref","collectionPath","runTransaction","body","pending","transaction","set","options","push","result","write","name","api","doc","sub","base","merge","next","key","value","Object","entries","isEqual","seconds","Math","floor","Date","now","where","orderBy","limit","keys","add","length"],"mappings":"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"}
@@ -20,6 +20,7 @@ import { firebaseAdmin } from "./firebase-admin.js";
20
20
  import { countCrmRecords, restampCrmListFieldsAt } from "./crm-records.js";
21
21
  import { attributeOrderToEmail } from "./email-revenue-attribution.js";
22
22
  import { hostRefusesCaptureForErasure } from "./email-suppression.js";
23
+ import { scheduleCapturedEmailCheck } from "./capture-email-check.js";
23
24
  import { attributeCampaignConversion } from "./campaign-conversion-attribution.js";
24
25
  import { nameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
25
26
  import { consentGroupForGrant } from "@aglyn/aglyn/app-utils/consent-groups";
@@ -117,8 +118,8 @@ import { consentGroupForSite, getOrgForHost, orgDataCollectionForHost } from "./
117
118
  }
118
119
  export async function upsertHostContact(options) {
119
120
  try {
120
- var _options_interaction_atMs, _options_campaignIds, _ref, _contactsRef_parent, _ref1, _ref2;
121
- var _options_facet, _orgBilling_org;
121
+ var _options_interaction_atMs, _options_campaignIds, _ref, _contactsRef_parent, _ref1, _ref2, _ref3;
122
+ var _options_facet, _orgBilling_org, _contactsRef_parent1;
122
123
  const email = normalizeContactEmail(options.email);
123
124
  if (!email) return {
124
125
  refused: 'invalid-email'
@@ -246,7 +247,8 @@ export async function upsertHostContact(options) {
246
247
  * read once and needs no backfill.
247
248
  *=========================================*/ const docSnapshot = await findContactByEmail(contactsRef, email);
248
249
  if (docSnapshot) {
249
- var _facet_name, _profile_lifecycleStage;
250
+ var _facet_name, _profile_lifecycleStage, _ref4;
251
+ var _contactsRef_parent2;
250
252
  /*
251
253
  * MERGED INTO THIS GROUP'S FACET, not into the top of the document.
252
254
  *
@@ -365,6 +367,13 @@ export async function upsertHostContact(options) {
365
367
  // The merged facet, name and scope are what the Contacts list
366
368
  // searches and filters by (AGL-3321): restamped from the stored row.
367
369
  await restampCrmListFieldsAt(docSnapshot.ref, 'contacts');
370
+ // The address's domain, checked after the response (AGL-3328).
371
+ scheduleCapturedEmailCheck({
372
+ orgId: (_ref4 = (_contactsRef_parent2 = contactsRef.parent) == null ? void 0 : _contactsRef_parent2.id) != null ? _ref4 : null,
373
+ hostId: options.hostId,
374
+ email,
375
+ predicted: docSnapshot.get('emailStatus') === 'undeliverable'
376
+ });
368
377
  return {
369
378
  contactId: docSnapshot.id,
370
379
  created: false
@@ -556,6 +565,13 @@ export async function upsertHostContact(options) {
556
565
  console.error('upsertHostContact onCreated failed', error);
557
566
  });
558
567
  }
568
+ // A new person's address, checked after the response (AGL-3328): a
569
+ // domain that takes no mail reads "Would bounce" on the record.
570
+ scheduleCapturedEmailCheck({
571
+ orgId: (_ref3 = (_contactsRef_parent1 = contactsRef.parent) == null ? void 0 : _contactsRef_parent1.id) != null ? _ref3 : null,
572
+ hostId: options.hostId,
573
+ email
574
+ });
559
575
  return {
560
576
  contactId: created.id,
561
577
  created: true