@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.
- package/package.json +8 -8
- package/src/lib/server/api-http.js +5 -1
- package/src/lib/server/api-http.js.map +1 -1
- package/src/lib/server/api-v1-description.d.ts +112 -0
- package/src/lib/server/api-v1-description.js +126 -0
- package/src/lib/server/api-v1-description.js.map +1 -0
- package/src/lib/server/api-v1-kit.d.ts +163 -0
- package/src/lib/server/api-v1-kit.js +191 -0
- package/src/lib/server/api-v1-kit.js.map +1 -0
- package/src/lib/server/api-v1-resources.d.ts +143 -0
- package/src/lib/server/api-v1-resources.js +125 -0
- package/src/lib/server/api-v1-resources.js.map +1 -0
- package/src/lib/server/campaign-conversion-attribution.d.ts +1 -1
- package/src/lib/server/campaign-conversion-attribution.js +3 -3
- package/src/lib/server/campaign-conversion-attribution.js.map +1 -1
- package/src/lib/server/card-payment-velocity.d.ts +9 -0
- package/src/lib/server/card-payment-velocity.js +28 -2
- package/src/lib/server/card-payment-velocity.js.map +1 -1
- package/src/lib/server/consent-group-carry.js +1 -1
- package/src/lib/server/consent-group-carry.js.map +1 -1
- package/src/lib/server/consent-group-change.d.ts +2 -1
- package/src/lib/server/consent-group-change.js +43 -18
- package/src/lib/server/consent-group-change.js.map +1 -1
- package/src/lib/server/contact-email-index.d.ts +22 -0
- package/src/lib/server/contact-email-index.js +108 -0
- package/src/lib/server/contact-email-index.js.map +1 -1
- package/src/lib/server/crm-email-activity.d.ts +1 -1
- package/src/lib/server/crm-email-activity.js +1 -1
- package/src/lib/server/crm-email-activity.js.map +1 -1
- package/src/lib/server/crm-records.d.ts +9 -1
- package/src/lib/server/crm-records.js +19 -2
- package/src/lib/server/crm-records.js.map +1 -1
- package/src/lib/server/cron-auth.d.ts +44 -0
- package/src/lib/server/cron-auth.js +76 -0
- package/src/lib/server/cron-auth.js.map +1 -0
- package/src/lib/server/dataset-live-pages.js +6 -6
- package/src/lib/server/dataset-live-pages.js.map +1 -1
- package/src/lib/server/duplicate-resource.d.ts +26 -0
- package/src/lib/server/duplicate-resource.js +47 -25
- package/src/lib/server/duplicate-resource.js.map +1 -1
- package/src/lib/server/dynamic-list-materialize.js +6 -3
- package/src/lib/server/dynamic-list-materialize.js.map +1 -1
- package/src/lib/server/email-flow-gate.js +2 -2
- package/src/lib/server/email-flow-gate.js.map +1 -1
- package/src/lib/server/email-suppression.d.ts +11 -0
- package/src/lib/server/email-suppression.js +37 -2
- package/src/lib/server/email-suppression.js.map +1 -1
- package/src/lib/server/email-unsubscribe-link.d.ts +8 -5
- package/src/lib/server/email-unsubscribe-link.js +7 -7
- package/src/lib/server/email-unsubscribe-link.js.map +1 -1
- package/src/lib/server/erase.d.ts +19 -33
- package/src/lib/server/erase.js +37 -162
- package/src/lib/server/erase.js.map +1 -1
- package/src/lib/server/host-email-tokens.js +1 -1
- package/src/lib/server/host-email-tokens.js.map +1 -1
- package/src/lib/server/host-memberships.js +3 -3
- package/src/lib/server/host-memberships.js.map +1 -1
- package/src/lib/server/host-visitor-records.d.ts +32 -4
- package/src/lib/server/host-visitor-records.js +73 -14
- package/src/lib/server/host-visitor-records.js.map +1 -1
- package/src/lib/server/hosted-page-review.d.ts +27 -0
- package/src/lib/server/hosted-page-review.js +149 -12
- package/src/lib/server/hosted-page-review.js.map +1 -1
- package/src/lib/server/media-cdn-rate-limit.d.ts +6 -3
- package/src/lib/server/media-cdn-rate-limit.js +6 -3
- package/src/lib/server/media-cdn-rate-limit.js.map +1 -1
- package/src/lib/server/member-email-aliases.d.ts +10 -2
- package/src/lib/server/member-email-aliases.js +26 -2
- package/src/lib/server/member-email-aliases.js.map +1 -1
- package/src/lib/server/new-account-announcement.d.ts +62 -0
- package/src/lib/server/new-account-announcement.js +77 -0
- package/src/lib/server/new-account-announcement.js.map +1 -0
- package/src/lib/server/notifications.d.ts +22 -2
- package/src/lib/server/notifications.js +40 -3
- package/src/lib/server/notifications.js.map +1 -1
- package/src/lib/server/operator-alerts.d.ts +7 -1
- package/src/lib/server/operator-alerts.js +5 -3
- package/src/lib/server/operator-alerts.js.map +1 -1
- package/src/lib/server/org-containers.d.ts +66 -0
- package/src/lib/server/org-containers.js +95 -0
- package/src/lib/server/org-containers.js.map +1 -0
- package/src/lib/server/org-member-notice.js +3 -2
- package/src/lib/server/org-member-notice.js.map +1 -1
- package/src/lib/server/payment-fraud-signal.js +5 -1
- package/src/lib/server/payment-fraud-signal.js.map +1 -1
- package/src/lib/server/{connect-payout-failure.d.ts → payment-provider-stripe-connect-payouts.d.ts} +3 -0
- package/src/lib/server/{connect-payout-failure.js → payment-provider-stripe-connect-payouts.js} +36 -2
- package/src/lib/server/payment-provider-stripe-connect-payouts.js.map +1 -0
- package/src/lib/server/{connect-account-status.d.ts → payment-provider-stripe-connect-status.d.ts} +3 -0
- package/src/lib/server/{connect-account-status.js → payment-provider-stripe-connect-status.js} +1 -1
- package/src/lib/server/payment-provider-stripe-connect-status.js.map +1 -0
- package/src/lib/server/{stripe-account-mode.d.ts → payment-provider-stripe-connect.d.ts} +45 -70
- package/src/lib/server/{stripe-account-mode.js → payment-provider-stripe-connect.js} +78 -52
- package/src/lib/server/payment-provider-stripe-connect.js.map +1 -0
- package/src/lib/server/payment-provider.d.ts +173 -0
- package/src/lib/server/payment-provider.js +70 -0
- package/src/lib/server/payment-provider.js.map +1 -0
- package/src/lib/server/payment-risk-record.d.ts +1 -1
- package/src/lib/server/payment-risk-record.js +3 -1
- package/src/lib/server/payment-risk-record.js.map +1 -1
- package/src/lib/server/personal-data-export.js +1 -1
- package/src/lib/server/personal-data-export.js.map +1 -1
- package/src/lib/server/platform-marketing-consent.d.ts +1 -1
- package/src/lib/server/platform-marketing-consent.js +1 -1
- package/src/lib/server/platform-marketing-consent.js.map +1 -1
- package/src/lib/server/rate-limit-store.js +13 -3
- package/src/lib/server/rate-limit-store.js.map +1 -1
- package/src/lib/server/realm-plugins.js +8 -3
- package/src/lib/server/realm-plugins.js.map +1 -1
- package/src/lib/server/retained-refusals.d.ts +142 -0
- package/src/lib/server/retained-refusals.js +325 -0
- package/src/lib/server/retained-refusals.js.map +1 -0
- package/src/lib/server/risk-notice.d.ts +32 -1
- package/src/lib/server/risk-notice.js +90 -28
- package/src/lib/server/risk-notice.js.map +1 -1
- package/src/lib/server/sending-domains.d.ts +17 -0
- package/src/lib/server/sending-domains.js +28 -4
- package/src/lib/server/sending-domains.js.map +1 -1
- package/src/lib/server/sso-enforcement.js +7 -2
- package/src/lib/server/sso-enforcement.js.map +1 -1
- package/src/lib/server/staff-alert-email.js +4 -2
- package/src/lib/server/staff-alert-email.js.map +1 -1
- package/src/lib/server/test-firestore-queries.d.ts +7 -0
- package/src/lib/server/test-firestore-queries.js +70 -27
- package/src/lib/server/test-firestore-queries.js.map +1 -1
- package/src/lib/server/{email-topic-confirmation.js → topic-subscriptions.js} +15 -11
- package/src/lib/server/topic-subscriptions.js.map +1 -0
- package/src/lib/server/upsert-contact.d.ts +46 -1
- package/src/lib/server/upsert-contact.js +87 -20
- package/src/lib/server/upsert-contact.js.map +1 -1
- package/src/tenancy.d.ts +5 -8
- package/src/tenancy.js +12 -13
- package/src/tenancy.js.map +1 -1
- package/src/lib/server/connect-account-status.js.map +0 -1
- package/src/lib/server/connect-payout-failure.js.map +0 -1
- package/src/lib/server/contact-email-engagement.d.ts +0 -32
- package/src/lib/server/contact-email-engagement.js +0 -145
- package/src/lib/server/contact-email-engagement.js.map +0 -1
- package/src/lib/server/contact-lifecycle-floor.d.ts +0 -89
- package/src/lib/server/contact-lifecycle-floor.js +0 -91
- package/src/lib/server/contact-lifecycle-floor.js.map +0 -1
- package/src/lib/server/contact-merge.d.ts +0 -55
- package/src/lib/server/contact-merge.js +0 -288
- package/src/lib/server/contact-merge.js.map +0 -1
- package/src/lib/server/crm-booking-activity.d.ts +0 -86
- package/src/lib/server/crm-booking-activity.js +0 -257
- package/src/lib/server/crm-booking-activity.js.map +0 -1
- package/src/lib/server/crm-inbound-email.d.ts +0 -145
- package/src/lib/server/crm-inbound-email.js +0 -297
- package/src/lib/server/crm-inbound-email.js.map +0 -1
- package/src/lib/server/email-topic-confirmation.js.map +0 -1
- package/src/lib/server/form-dataset-binding-token.d.ts +0 -35
- package/src/lib/server/form-dataset-binding-token.js +0 -107
- package/src/lib/server/form-dataset-binding-token.js.map +0 -1
- package/src/lib/server/stripe-account-mode.js.map +0 -1
- /package/src/lib/server/{email-topic-confirmation.d.ts → topic-subscriptions.d.ts} +0 -0
|
@@ -14,7 +14,17 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ /**
|
|
17
|
-
*
|
|
17
|
+
* The Stripe Connect adapter behind the payment-provider contract
|
|
18
|
+
* (`payment-provider.ts`): a tenant's merchant account is a Stripe Connect
|
|
19
|
+
* account, charged and paid out through the platform's Stripe key.
|
|
20
|
+
*
|
|
21
|
+
* This module is the part that is Stripe's — which mode a key states, how the
|
|
22
|
+
* API is asked, which webhook events describe a connected account and where
|
|
23
|
+
* each one keeps the account id. What an event WRITES lives in the two modules
|
|
24
|
+
* it dispatches to, loaded only when such an event arrives, so the money doors
|
|
25
|
+
* that import the contract for its readiness rule load no Firebase.
|
|
26
|
+
*
|
|
27
|
+
* ## Whether a stored Connect linkage can actually take money HERE (AGL-2471)
|
|
18
28
|
*
|
|
19
29
|
* THE DEFECT. Production Firestore held three Connect linkages and all three
|
|
20
30
|
* named TEST-mode accounts. One — `profiles/7AVEMtDa…`, `stripeAccountId:
|
|
@@ -49,20 +59,10 @@
|
|
|
49
59
|
* (above), so the account's mode IS the key's mode; the key's mode is
|
|
50
60
|
* then confirmed against the API via `resolvePlatformStripeMode`.
|
|
51
61
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* `account.updated`. Everywhere else — a test-key deployment, a developer
|
|
57
|
-
* machine, a self-hosted install still in sandbox, the whole test suite — an
|
|
58
|
-
* unverified linkage keeps the behaviour it had before, because no real money
|
|
59
|
-
* can move there and Stripe enforces the mode boundary itself. Refusing
|
|
60
|
-
* everywhere would have bought production nothing extra and broken every
|
|
61
|
-
* non-production deployment.
|
|
62
|
-
*
|
|
63
|
-
* A PROVEN mismatch (`stripeAccountLivemode` recorded and disagreeing) is
|
|
64
|
-
* refused in BOTH directions, because that costs nothing to detect and is
|
|
65
|
-
* never right.
|
|
62
|
+
* The profile field is `stripeAccountLivemode`, and the rule that reads it —
|
|
63
|
+
* refused on a live deployment when absent, refused in both directions when
|
|
64
|
+
* it disagrees — is the contract's `merchantAccountReadiness`, which says why
|
|
65
|
+
* it is asymmetric.
|
|
66
66
|
*
|
|
67
67
|
* WHAT IS SNIFFED, AND WHY THAT ONE IS FAIR. The SECRET KEY states its own
|
|
68
68
|
* mode — `sk_live_`, `sk_test_`, `rk_live_`, `rk_test_` — and that is a
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
* and writes it into the production database. Nothing about that is going to
|
|
79
79
|
* change — the split key is the right call — so the gate has to be the thing
|
|
80
80
|
* that notices.
|
|
81
|
-
*/ /**
|
|
81
|
+
*/ /**
|
|
82
82
|
* The mode a Stripe secret key states about itself, or `undefined` when the
|
|
83
83
|
* string is not a Stripe secret key.
|
|
84
84
|
*
|
|
@@ -120,43 +120,69 @@
|
|
|
120
120
|
return fromKey;
|
|
121
121
|
}
|
|
122
122
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
123
|
+
* A Stripe webhook event about a connected account, applied to the profiles in
|
|
124
|
+
* `collection` that store it — the contract's `applyAccountEvent`.
|
|
125
|
+
*
|
|
126
|
+
* Three events describe a connected account, and each keeps the account id
|
|
127
|
+
* somewhere different:
|
|
128
|
+
*
|
|
129
|
+
* - `account.updated` — its readiness changed (AGL-1997). The object IS the
|
|
130
|
+
* Account, and the EVENT carries `livemode` (AGL-2471): the Account object
|
|
131
|
+
* has no such field, and the event's is what lets a linkage whose mode was
|
|
132
|
+
* never recorded heal itself instead of staying refused forever.
|
|
133
|
+
* - `payout.failed` / `transfer.failed` — money headed for the account never
|
|
134
|
+
* landed. A payout is the connected account's balance failing to reach its
|
|
135
|
+
* bank, so its account id is `event.account`: the Payout's own
|
|
136
|
+
* `destination` names the BANK. A transfer is the platform's balance
|
|
137
|
+
* failing to reach the connected account, a platform event whose
|
|
138
|
+
* `destination` IS the account. Recorded and surfaced, never retried:
|
|
139
|
+
* Stripe runs its own retry schedule, and a second transfer against an
|
|
140
|
+
* account that has just refused one is how a duplicate lands.
|
|
141
|
+
* - `payout.paid` — a later payout landed, which retires the warning the
|
|
142
|
+
* failure left on the profile. The history is kept.
|
|
143
|
+
*
|
|
144
|
+
* Every other event answers `false` without loading anything. A write that
|
|
145
|
+
* fails throws, and the webhook's 500 earns the redelivery; every write
|
|
146
|
+
* mirrors current state or is keyed by Stripe's id, so a redelivery converges.
|
|
147
|
+
*/ export async function applyStripeConnectAccountEvent(collection, delivered) {
|
|
148
|
+
const type = delivered == null ? void 0 : delivered.type;
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
|
+
const object = delivered == null ? void 0 : delivered.object;
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
152
|
+
const event = delivered == null ? void 0 : delivered.event;
|
|
153
|
+
if (type === 'account.updated') {
|
|
154
|
+
const { syncConnectAccountStatus } = await import("./payment-provider-stripe-connect-status.js");
|
|
155
|
+
await syncConnectAccountStatus(collection, object, event == null ? void 0 : event.livemode);
|
|
156
|
+
return true;
|
|
143
157
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
if (type === 'payout.failed' || type === 'transfer.failed') {
|
|
159
|
+
var _ref, _ref1, _ref2;
|
|
160
|
+
var _object_destination;
|
|
161
|
+
const failedAccountId = type === 'payout.failed' ? String((_ref = event == null ? void 0 : event.account) != null ? _ref : '') : String((_ref1 = (_ref2 = object == null ? void 0 : (_object_destination = object.destination) == null ? void 0 : _object_destination.id) != null ? _ref2 : object == null ? void 0 : object.destination) != null ? _ref1 : '');
|
|
162
|
+
const { recordConnectPayoutFailure } = await import("./payment-provider-stripe-connect-payouts.js");
|
|
163
|
+
await recordConnectPayoutFailure(collection, {
|
|
164
|
+
kind: type === 'payout.failed' ? 'payout' : 'transfer',
|
|
165
|
+
object,
|
|
166
|
+
accountId: failedAccountId,
|
|
167
|
+
livemode: event == null ? void 0 : event.livemode
|
|
168
|
+
});
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (type === 'payout.paid') {
|
|
172
|
+
var _ref3;
|
|
173
|
+
const { clearConnectPayoutFailure } = await import("./payment-provider-stripe-connect-payouts.js");
|
|
174
|
+
await clearConnectPayoutFailure(collection, String((_ref3 = event == null ? void 0 : event.account) != null ? _ref3 : ''));
|
|
175
|
+
return true;
|
|
158
176
|
}
|
|
159
|
-
return
|
|
177
|
+
return false;
|
|
160
178
|
}
|
|
179
|
+
/** The adapter the contract returns. */ export const STRIPE_CONNECT_PAYMENT_PROVIDER = {
|
|
180
|
+
id: 'stripe-connect',
|
|
181
|
+
// Called with no argument, so the key is read when asked — not when this
|
|
182
|
+
// module was first evaluated.
|
|
183
|
+
platformMode: ()=>platformStripeMode(),
|
|
184
|
+
resolvePlatformMode: ()=>resolvePlatformStripeMode(),
|
|
185
|
+
applyAccountEvent: applyStripeConnectAccountEvent
|
|
186
|
+
};
|
|
161
187
|
|
|
162
|
-
//# sourceMappingURL=stripe-
|
|
188
|
+
//# sourceMappingURL=payment-provider-stripe-connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/payment-provider-stripe-connect.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 type { PaymentMode, PaymentProvider, PaymentProviderEvent } from './payment-provider'\n\n/**\n * The Stripe Connect adapter behind the payment-provider contract\n * (`payment-provider.ts`): a tenant's merchant account is a Stripe Connect\n * account, charged and paid out through the platform's Stripe key.\n *\n * This module is the part that is Stripe's — which mode a key states, how the\n * API is asked, which webhook events describe a connected account and where\n * each one keeps the account id. What an event WRITES lives in the two modules\n * it dispatches to, loaded only when such an event arrives, so the money doors\n * that import the contract for its readiness rule load no Firebase.\n *\n * ## Whether a stored Connect linkage can actually take money HERE (AGL-2471)\n *\n * THE DEFECT. Production Firestore held three Connect linkages and all three\n * named TEST-mode accounts. One — `profiles/7AVEMtDa…`, `stripeAccountId:\n * acct_1TulDeRbL3B9Ioqz` — also carried `stripeChargesEnabled: true`, which\n * was the entire payments-readiness test every money door made:\n *\n * if (!accountId || !ownerProfile.get('stripeChargesEnabled')) …\n *\n * So three storefronts presented as payments-ready, minted a LIVE Checkout\n * session with a TEST-mode `payment_intent_data[transfer_data][destination]`,\n * and were refused by Stripe as a generic 502 at the shopper's checkout. A\n * merchant told they are ready to take money, who silently cannot.\n *\n * WHY THE ACCOUNT ID IS NOT THE EVIDENCE. `acct_1TulDeRbL3B9Ioqz` is a real\n * production value naming a TEST account. Stripe's account ids carry no mode\n * marker — live and test ids are the same shape — so no amount of reading the\n * string can tell them apart. What DOES tell them apart is Stripe itself:\n * retrieving that id with the live key answers\n *\n * 400 The account acct_1TulDeRbL3B9Ioqz was a test account created with a\n * testmode key, and therefore can only be used with testmode keys.\n *\n * The Account object, notably, has NO `livemode` field of its own (verified\n * against the live API: the payload carries `charges_enabled`,\n * `payouts_enabled`, `capabilities` and no `livemode`). So the mode is\n * recorded at the two moments Stripe does state it:\n *\n * 1. `account.updated` — the EVENT carries `livemode`, and it is Stripe's\n * own statement about the account the event is describing;\n * 2. onboarding — the account was just created or retrieved successfully\n * with the platform key, and Stripe hard-refuses cross-mode retrieval\n * (above), so the account's mode IS the key's mode; the key's mode is\n * then confirmed against the API via `resolvePlatformStripeMode`.\n *\n * The profile field is `stripeAccountLivemode`, and the rule that reads it —\n * refused on a live deployment when absent, refused in both directions when\n * it disagrees — is the contract's `merchantAccountReadiness`, which says why\n * it is asymmetric.\n *\n * WHAT IS SNIFFED, AND WHY THAT ONE IS FAIR. The SECRET KEY states its own\n * mode — `sk_live_`, `sk_test_`, `rk_live_`, `rk_test_` — and that is a\n * documented, stable Stripe invariant, unlike the account id. It is used only\n * to answer \"which mode is this deployment\", never \"which mode is this\n * account\", and even there `resolvePlatformStripeMode` prefers the API.\n *\n * HOW IT GOT THERE. A developer machine runs against PRODUCTION Firestore\n * (`FIREBASE_PROJECT_ID=aglyn-main` in every dev env file) while\n * `apps/console/.env.development.local` sets `STRIPE_SECRET_KEY` to the\n * `sk_test_` key by deliberate policy (AGL-1137: localhost must never touch\n * live Stripe). The connect route then creates a test-mode Express account\n * and writes it into the production database. Nothing about that is going to\n * change — the split key is the right call — so the gate has to be the thing\n * that notices.\n */\n\n/**\n * The mode a Stripe secret key states about itself, or `undefined` when the\n * string is not a Stripe secret key.\n *\n * Deliberately narrow: it matches `sk_`/`rk_` followed by `live`/`test`, and\n * refuses everything else rather than guessing. Handing it an `acct_…` id\n * returns `undefined` — the whole point is that account ids say nothing.\n */\nexport function platformStripeMode(\n key: string | undefined = process.env.STRIPE_SECRET_KEY,\n): PaymentMode | undefined {\n const match = /^[sr]k_(live|test)_/.exec(String(key ?? '').trim())\n return match ? (match[1] as PaymentMode) : undefined\n}\n\n/**\n * The platform's mode, asked of Stripe and only then of the key.\n *\n * `GET /v1/balance` is a singleton that carries `livemode`, so it states the\n * mode of whatever key made the call — no object has to be created to find\n * out. The key prefix is the fallback for a restricted key that cannot read\n * balance; without it, a self-hosted install with a scoped key could not\n * finish onboarding at all.\n */\nexport async function resolvePlatformStripeMode(\n key: string | undefined = process.env.STRIPE_SECRET_KEY,\n fetchImpl: typeof fetch = fetch,\n): Promise<PaymentMode | undefined> {\n const fromKey = platformStripeMode(key)\n if (!key) return fromKey\n try {\n const response = await fetchImpl('https://api.stripe.com/v1/balance', {\n method: 'GET',\n headers: { Authorization: `Bearer ${key}` },\n })\n if (response.ok) {\n const payload = await response.json()\n if (typeof payload?.livemode === 'boolean') {\n return payload.livemode ? 'live' : 'test'\n }\n }\n } catch {\n // Network or transport failure — fall through to the key, which is the\n // same answer we would have recorded before this existed.\n }\n return fromKey\n}\n\n/**\n * A Stripe webhook event about a connected account, applied to the profiles in\n * `collection` that store it — the contract's `applyAccountEvent`.\n *\n * Three events describe a connected account, and each keeps the account id\n * somewhere different:\n *\n * - `account.updated` — its readiness changed (AGL-1997). The object IS the\n * Account, and the EVENT carries `livemode` (AGL-2471): the Account object\n * has no such field, and the event's is what lets a linkage whose mode was\n * never recorded heal itself instead of staying refused forever.\n * - `payout.failed` / `transfer.failed` — money headed for the account never\n * landed. A payout is the connected account's balance failing to reach its\n * bank, so its account id is `event.account`: the Payout's own\n * `destination` names the BANK. A transfer is the platform's balance\n * failing to reach the connected account, a platform event whose\n * `destination` IS the account. Recorded and surfaced, never retried:\n * Stripe runs its own retry schedule, and a second transfer against an\n * account that has just refused one is how a duplicate lands.\n * - `payout.paid` — a later payout landed, which retires the warning the\n * failure left on the profile. The history is kept.\n *\n * Every other event answers `false` without loading anything. A write that\n * fails throws, and the webhook's 500 earns the redelivery; every write\n * mirrors current state or is keyed by Stripe's id, so a redelivery converges.\n */\nexport async function applyStripeConnectAccountEvent(\n collection: string,\n delivered: PaymentProviderEvent,\n): Promise<boolean> {\n const type = delivered?.type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const object = delivered?.object as any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const event = delivered?.event as any\n if (type === 'account.updated') {\n const { syncConnectAccountStatus } = await import(\n './payment-provider-stripe-connect-status'\n )\n await syncConnectAccountStatus(collection, object, event?.livemode)\n return true\n }\n if (type === 'payout.failed' || type === 'transfer.failed') {\n const failedAccountId =\n type === 'payout.failed'\n ? String(event?.account ?? '')\n : String(object?.destination?.id ?? object?.destination ?? '')\n const { recordConnectPayoutFailure } = await import(\n './payment-provider-stripe-connect-payouts'\n )\n await recordConnectPayoutFailure(collection, {\n kind: type === 'payout.failed' ? 'payout' : 'transfer',\n object,\n accountId: failedAccountId,\n livemode: event?.livemode,\n })\n return true\n }\n if (type === 'payout.paid') {\n const { clearConnectPayoutFailure } = await import(\n './payment-provider-stripe-connect-payouts'\n )\n await clearConnectPayoutFailure(collection, String(event?.account ?? ''))\n return true\n }\n return false\n}\n\n/** The adapter the contract returns. */\nexport const STRIPE_CONNECT_PAYMENT_PROVIDER: PaymentProvider = {\n id: 'stripe-connect',\n // Called with no argument, so the key is read when asked — not when this\n // module was first evaluated.\n platformMode: () => platformStripeMode(),\n resolvePlatformMode: () => resolvePlatformStripeMode(),\n applyAccountEvent: applyStripeConnectAccountEvent,\n}\n"],"names":["platformStripeMode","key","process","env","STRIPE_SECRET_KEY","match","exec","String","trim","undefined","resolvePlatformStripeMode","fetchImpl","fetch","fromKey","response","method","headers","Authorization","ok","payload","json","livemode","applyStripeConnectAccountEvent","collection","delivered","type","object","event","syncConnectAccountStatus","failedAccountId","account","destination","id","recordConnectPayoutFailure","kind","accountId","clearConnectPayoutFailure","STRIPE_CONNECT_PAYMENT_PROVIDER","platformMode","resolvePlatformMode","applyAccountEvent"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiEC,GAED;;;;;;;CAOC,GACD,OAAO,SAASA,mBACdC,MAA0BC,QAAQC,GAAG,CAACC,iBAAiB;IAEvD,MAAMC,QAAQ,sBAAsBC,IAAI,CAACC,OAAON,cAAAA,MAAO,IAAIO,IAAI;IAC/D,OAAOH,QAASA,KAAK,CAAC,EAAE,GAAmBI;AAC7C;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeC,0BACpBT,MAA0BC,QAAQC,GAAG,CAACC,iBAAiB,EACvDO,YAA0BC,KAAK;IAE/B,MAAMC,UAAUb,mBAAmBC;IACnC,IAAI,CAACA,KAAK,OAAOY;IACjB,IAAI;QACF,MAAMC,WAAW,MAAMH,UAAU,qCAAqC;YACpEI,QAAQ;YACRC,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEhB,KAAK;YAAC;QAC5C;QACA,IAAIa,SAASI,EAAE,EAAE;YACf,MAAMC,UAAU,MAAML,SAASM,IAAI;YACnC,IAAI,QAAOD,2BAAAA,QAASE,QAAQ,MAAK,WAAW;gBAC1C,OAAOF,QAAQE,QAAQ,GAAG,SAAS;YACrC;QACF;IACF,EAAE,eAAM;IACN,uEAAuE;IACvE,0DAA0D;IAC5D;IACA,OAAOR;AACT;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,eAAeS,+BACpBC,UAAkB,EAClBC,SAA+B;IAE/B,MAAMC,OAAOD,6BAAAA,UAAWC,IAAI;IAC5B,8DAA8D;IAC9D,MAAMC,SAASF,6BAAAA,UAAWE,MAAM;IAChC,8DAA8D;IAC9D,MAAMC,QAAQH,6BAAAA,UAAWG,KAAK;IAC9B,IAAIF,SAAS,mBAAmB;QAC9B,MAAM,EAAEG,wBAAwB,EAAE,GAAG,MAAM,MAAM,CAC/C;QAEF,MAAMA,yBAAyBL,YAAYG,QAAQC,yBAAAA,MAAON,QAAQ;QAClE,OAAO;IACT;IACA,IAAII,SAAS,mBAAmBA,SAAS,mBAAmB;kBAI7CC;YAAAA;QAHb,MAAMG,kBACJJ,SAAS,kBACLlB,eAAOoB,yBAAAA,MAAOG,OAAO,mBAAI,MACzBvB,QAAOmB,iBAAAA,2BAAAA,sBAAAA,OAAQK,WAAW,qBAAnBL,oBAAqBM,EAAE,oBAAIN,0BAAAA,OAAQK,WAAW,YAA9CL,QAAkD;QAC/D,MAAM,EAAEO,0BAA0B,EAAE,GAAG,MAAM,MAAM,CACjD;QAEF,MAAMA,2BAA2BV,YAAY;YAC3CW,MAAMT,SAAS,kBAAkB,WAAW;YAC5CC;YACAS,WAAWN;YACXR,QAAQ,EAAEM,yBAAAA,MAAON,QAAQ;QAC3B;QACA,OAAO;IACT;IACA,IAAII,SAAS,eAAe;;QAC1B,MAAM,EAAEW,yBAAyB,EAAE,GAAG,MAAM,MAAM,CAChD;QAEF,MAAMA,0BAA0Bb,YAAYhB,gBAAOoB,yBAAAA,MAAOG,OAAO,oBAAI;QACrE,OAAO;IACT;IACA,OAAO;AACT;AAEA,sCAAsC,GACtC,OAAO,MAAMO,kCAAmD;IAC9DL,IAAI;IACJ,yEAAyE;IACzE,8BAA8B;IAC9BM,cAAc,IAAMtC;IACpBuC,qBAAqB,IAAM7B;IAC3B8B,mBAAmBlB;AACrB,EAAC"}
|
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
* The merchant's payment account — the one seam the payment vendor lives
|
|
19
|
+
* behind when a TENANT takes money.
|
|
20
|
+
*
|
|
21
|
+
* ## Which money this is, and which it is not
|
|
22
|
+
*
|
|
23
|
+
* Two different accounts move money on this platform and only one of them is
|
|
24
|
+
* here. The platform BILLS its customers — plans, add-ons, metered usage — on
|
|
25
|
+
* its own account; that rail is infrastructure (`org-billing.ts`, the billing
|
|
26
|
+
* webhook route, `api-idempotency.ts`, `stripe-deployment-mode.ts`) and names
|
|
27
|
+
* its vendor where it runs. A tenant SELLS — a storefront order, a booked
|
|
28
|
+
* appointment, a marketplace listing — into a connected merchant account of
|
|
29
|
+
* its own, and every plugin that takes money reaches that account through
|
|
30
|
+
* this contract: whether the account may be charged, which world (live or
|
|
31
|
+
* test) the platform charges in, and what the provider says when the account
|
|
32
|
+
* changes or a payout fails.
|
|
33
|
+
*
|
|
34
|
+
* Commerce's `PaymentProviderId` names the provider a CHECKOUT is created
|
|
35
|
+
* with. This is the other half: the account the checkout pays into.
|
|
36
|
+
*
|
|
37
|
+
* ## One adapter, compiled in, and no registry — by failure direction
|
|
38
|
+
*
|
|
39
|
+
* Stripe Connect is the only adapter
|
|
40
|
+
* (`payment-provider-stripe-connect.ts`), and {@link paymentProvider} returns
|
|
41
|
+
* it without consulting any registry, the way the domain driver's built-ins
|
|
42
|
+
* and the mail rail's are compiled into their libraries. A provider a plugin
|
|
43
|
+
* registered could be ABSENT where a door reads it — a declaration that threw
|
|
44
|
+
* part-way, a route bundle that evaluated the registry apart from boot's —
|
|
45
|
+
* and every answer an absent provider could give at a money door is wrong: a
|
|
46
|
+
* refusal takes every sale on every site down, and a default lets a checkout
|
|
47
|
+
* proceed against an account nobody checked. Compiled in, it cannot be
|
|
48
|
+
* absent, so neither can happen.
|
|
49
|
+
*
|
|
50
|
+
* It lives in this library rather than in a plugin because three plugins take
|
|
51
|
+
* money into the same account — a storefront, a calendar and the marketplace
|
|
52
|
+
* — and one plugin may not import another. A plugin that owned the adapter
|
|
53
|
+
* would make the other two's sales depend on its install and on its
|
|
54
|
+
* registration having run; the adapter itself sells nothing and holds no
|
|
55
|
+
* product, only the platform's own connected-account integration.
|
|
56
|
+
*
|
|
57
|
+
* A second adapter is one module beside the first and a selector here. There
|
|
58
|
+
* is no selector today because there is nothing to select: every plugin's
|
|
59
|
+
* checkout is created against the same vendor.
|
|
60
|
+
*
|
|
61
|
+
* ## The contract every adapter owes
|
|
62
|
+
*
|
|
63
|
+
* 1. **An account the provider did not describe is never ready.** Every
|
|
64
|
+
* {@link MerchantAccountReadiness} other than `'ready'` refuses the charge,
|
|
65
|
+
* and only a literal boolean is a recorded answer.
|
|
66
|
+
* 2. **Only what the provider stated is written.** An account event that
|
|
67
|
+
* states nothing writes nothing, and a written record never invents a
|
|
68
|
+
* `false` or a `true` from an absent field.
|
|
69
|
+
* 3. **Failures propagate.** {@link PaymentProvider.applyAccountEvent} runs
|
|
70
|
+
* inside the platform's billing webhook, and a throw is what earns the
|
|
71
|
+
* redelivery; every write it makes is idempotent so that redelivery
|
|
72
|
+
* converges.
|
|
73
|
+
*/
|
|
74
|
+
/** Which world a platform credential or a merchant account charges in. */
|
|
75
|
+
export type PaymentMode = 'live' | 'test';
|
|
76
|
+
/**
|
|
77
|
+
* Why a stored merchant account may not be charged against. Every value
|
|
78
|
+
* other than `'ready'` is a refusal.
|
|
79
|
+
*/
|
|
80
|
+
export type MerchantAccountReadiness =
|
|
81
|
+
/** Charge away. */
|
|
82
|
+
'ready'
|
|
83
|
+
/** No merchant account is stored at all — the onboarding call to action. */
|
|
84
|
+
| 'not-connected'
|
|
85
|
+
/** The provider will not let this account take charges. */
|
|
86
|
+
| 'charges-disabled'
|
|
87
|
+
/**
|
|
88
|
+
* The account's mode was never recorded (AGL-2471) AND this deployment is
|
|
89
|
+
* LIVE. The onboarding route and the provider's account event both record
|
|
90
|
+
* it, so a genuine merchant self-heals on the next of either.
|
|
91
|
+
*/
|
|
92
|
+
| 'mode-unverified'
|
|
93
|
+
/** The account belongs to the OTHER world. The AGL-2471 defect. */
|
|
94
|
+
| 'mode-mismatch';
|
|
95
|
+
export interface MerchantAccountReadinessInput {
|
|
96
|
+
/** The stored account id, whatever shape the document has. */
|
|
97
|
+
accountId?: unknown;
|
|
98
|
+
/** Whether the provider said the account may take charges. */
|
|
99
|
+
chargesEnabled?: unknown;
|
|
100
|
+
/** Whether the provider said the account is live — absent before AGL-2471. */
|
|
101
|
+
accountLivemode?: unknown;
|
|
102
|
+
/** Defaults to {@link PaymentProvider.platformMode}. */
|
|
103
|
+
platformMode?: PaymentMode | undefined;
|
|
104
|
+
}
|
|
105
|
+
/** One event the platform's billing webhook received, as the provider sent it. */
|
|
106
|
+
export interface PaymentProviderEvent {
|
|
107
|
+
/** The provider's own name for the event. */
|
|
108
|
+
type: string;
|
|
109
|
+
/** The object the event describes. */
|
|
110
|
+
object: unknown;
|
|
111
|
+
/** The whole event as delivered. */
|
|
112
|
+
event: unknown;
|
|
113
|
+
}
|
|
114
|
+
export interface PaymentProvider {
|
|
115
|
+
/** Stable id, for log lines. Never a display name. */
|
|
116
|
+
readonly id: string;
|
|
117
|
+
/**
|
|
118
|
+
* The mode this deployment's platform credential states about itself, read
|
|
119
|
+
* without a network call; `undefined` when there is no credential or it
|
|
120
|
+
* states nothing.
|
|
121
|
+
*/
|
|
122
|
+
platformMode(): PaymentMode | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* The platform's mode, asked of the provider first and the credential
|
|
125
|
+
* second — what an onboarding route records against a merchant account it
|
|
126
|
+
* has just retrieved.
|
|
127
|
+
*/
|
|
128
|
+
resolvePlatformMode(): Promise<PaymentMode | undefined>;
|
|
129
|
+
/**
|
|
130
|
+
* Applies an event about a connected merchant account — its readiness
|
|
131
|
+
* changed, a payout to it failed, a later payout landed — to the documents
|
|
132
|
+
* in `collection` that store it. A plugin passes the collection holding ITS
|
|
133
|
+
* merchant profiles; a non-matching account updates nothing.
|
|
134
|
+
*
|
|
135
|
+
* @returns `true` when the event was about a merchant account and has been
|
|
136
|
+
* applied, so the caller's own sections have nothing to do with it;
|
|
137
|
+
* `false` for every other event.
|
|
138
|
+
*/
|
|
139
|
+
applyAccountEvent(collection: string, delivered: PaymentProviderEvent): Promise<boolean>;
|
|
140
|
+
}
|
|
141
|
+
/** The provider every merchant account on this deployment is held at. */
|
|
142
|
+
export declare function paymentProvider(): PaymentProvider;
|
|
143
|
+
/**
|
|
144
|
+
* Decides whether a stored merchant account may be charged against.
|
|
145
|
+
*
|
|
146
|
+
* Order matters: the two refusals that existed before AGL-2471 are asked
|
|
147
|
+
* first, so an unconnected or restricted merchant still gets the answer they
|
|
148
|
+
* always got, and only an account that WOULD have passed reaches the mode
|
|
149
|
+
* question.
|
|
150
|
+
*
|
|
151
|
+
* WHY THE MODE RULE IS ASYMMETRIC, AND NOT "REFUSE ANYTHING UNPROVEN". An
|
|
152
|
+
* unrecorded mode refuses only on a LIVE deployment. That is where real money
|
|
153
|
+
* moves, and a live deployment can always re-establish the field: the
|
|
154
|
+
* merchant reconnects, or the provider sends one account event. Everywhere
|
|
155
|
+
* else — a test deployment, a developer machine, a self-hosted install still
|
|
156
|
+
* in sandbox, the whole test suite — an unverified account keeps the
|
|
157
|
+
* behavior it had before, because no real money can move there and the
|
|
158
|
+
* provider enforces the mode boundary itself. A PROVEN mismatch is refused in
|
|
159
|
+
* both directions, because it costs nothing to detect and is never right.
|
|
160
|
+
*/
|
|
161
|
+
export declare function merchantAccountReadiness(input: MerchantAccountReadinessInput): MerchantAccountReadiness;
|
|
162
|
+
/**
|
|
163
|
+
* The form every money door calls: `true` only when the account may be
|
|
164
|
+
* charged, with the two mode refusals reported to the server log.
|
|
165
|
+
*
|
|
166
|
+
* The log line is the part the shopper's generic 409 cannot carry. Before
|
|
167
|
+
* this, a mode mismatch surfaced as a 502 from the provider with nothing
|
|
168
|
+
* anywhere naming the cause; the sale is still refused the same way, but now
|
|
169
|
+
* somebody can find out why.
|
|
170
|
+
*/
|
|
171
|
+
export declare function merchantAccountIsReady(input: MerchantAccountReadinessInput, context?: {
|
|
172
|
+
subject?: string;
|
|
173
|
+
}): boolean;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { STRIPE_CONNECT_PAYMENT_PROVIDER } from "./payment-provider-stripe-connect.js";
|
|
17
|
+
/** The provider every merchant account on this deployment is held at. */ export function paymentProvider() {
|
|
18
|
+
return STRIPE_CONNECT_PAYMENT_PROVIDER;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decides whether a stored merchant account may be charged against.
|
|
22
|
+
*
|
|
23
|
+
* Order matters: the two refusals that existed before AGL-2471 are asked
|
|
24
|
+
* first, so an unconnected or restricted merchant still gets the answer they
|
|
25
|
+
* always got, and only an account that WOULD have passed reaches the mode
|
|
26
|
+
* question.
|
|
27
|
+
*
|
|
28
|
+
* WHY THE MODE RULE IS ASYMMETRIC, AND NOT "REFUSE ANYTHING UNPROVEN". An
|
|
29
|
+
* unrecorded mode refuses only on a LIVE deployment. That is where real money
|
|
30
|
+
* moves, and a live deployment can always re-establish the field: the
|
|
31
|
+
* merchant reconnects, or the provider sends one account event. Everywhere
|
|
32
|
+
* else — a test deployment, a developer machine, a self-hosted install still
|
|
33
|
+
* in sandbox, the whole test suite — an unverified account keeps the
|
|
34
|
+
* behavior it had before, because no real money can move there and the
|
|
35
|
+
* provider enforces the mode boundary itself. A PROVEN mismatch is refused in
|
|
36
|
+
* both directions, because it costs nothing to detect and is never right.
|
|
37
|
+
*/ export function merchantAccountReadiness(input) {
|
|
38
|
+
const accountId = typeof input.accountId === 'string' ? input.accountId.trim() : '';
|
|
39
|
+
if (!accountId) return 'not-connected';
|
|
40
|
+
if (input.chargesEnabled !== true) return 'charges-disabled';
|
|
41
|
+
const platformMode = 'platformMode' in input ? input.platformMode : paymentProvider().platformMode();
|
|
42
|
+
// Three-valued on purpose, exactly as AGL-1997 reads `payoutsEnabled`: only
|
|
43
|
+
// a literal boolean is a recorded answer. A string `'true'` or a `1` is a
|
|
44
|
+
// field somebody else wrote, and inventing `true` from it would re-open the
|
|
45
|
+
// hole this closes.
|
|
46
|
+
if (typeof input.accountLivemode === 'boolean') {
|
|
47
|
+
// A PROVEN mismatch is refused in either direction. It costs nothing to
|
|
48
|
+
// detect and it is always wrong.
|
|
49
|
+
if (!platformMode) return 'ready';
|
|
50
|
+
return input.accountLivemode === (platformMode === 'live') ? 'ready' : 'mode-mismatch';
|
|
51
|
+
}
|
|
52
|
+
return platformMode === 'live' ? 'mode-unverified' : 'ready';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The form every money door calls: `true` only when the account may be
|
|
56
|
+
* charged, with the two mode refusals reported to the server log.
|
|
57
|
+
*
|
|
58
|
+
* The log line is the part the shopper's generic 409 cannot carry. Before
|
|
59
|
+
* this, a mode mismatch surfaced as a 502 from the provider with nothing
|
|
60
|
+
* anywhere naming the cause; the sale is still refused the same way, but now
|
|
61
|
+
* somebody can find out why.
|
|
62
|
+
*/ export function merchantAccountIsReady(input, context) {
|
|
63
|
+
const readiness = merchantAccountReadiness(input);
|
|
64
|
+
if (readiness === 'mode-mismatch' || readiness === 'mode-unverified') {
|
|
65
|
+
console.error(`[AGL-2471] Refusing a charge against ${paymentProvider().id} account ` + `${String(input.accountId)}${(context == null ? void 0 : context.subject) ? ` (${context.subject})` : ''}: ${readiness}. The stored account is not verified for this ` + `deployment's payment mode; the merchant must re-onboard.`);
|
|
66
|
+
}
|
|
67
|
+
return readiness === 'ready';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=payment-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/payment-provider.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 { STRIPE_CONNECT_PAYMENT_PROVIDER } from './payment-provider-stripe-connect'\n\n/**\n * The merchant's payment account — the one seam the payment vendor lives\n * behind when a TENANT takes money.\n *\n * ## Which money this is, and which it is not\n *\n * Two different accounts move money on this platform and only one of them is\n * here. The platform BILLS its customers — plans, add-ons, metered usage — on\n * its own account; that rail is infrastructure (`org-billing.ts`, the billing\n * webhook route, `api-idempotency.ts`, `stripe-deployment-mode.ts`) and names\n * its vendor where it runs. A tenant SELLS — a storefront order, a booked\n * appointment, a marketplace listing — into a connected merchant account of\n * its own, and every plugin that takes money reaches that account through\n * this contract: whether the account may be charged, which world (live or\n * test) the platform charges in, and what the provider says when the account\n * changes or a payout fails.\n *\n * Commerce's `PaymentProviderId` names the provider a CHECKOUT is created\n * with. This is the other half: the account the checkout pays into.\n *\n * ## One adapter, compiled in, and no registry — by failure direction\n *\n * Stripe Connect is the only adapter\n * (`payment-provider-stripe-connect.ts`), and {@link paymentProvider} returns\n * it without consulting any registry, the way the domain driver's built-ins\n * and the mail rail's are compiled into their libraries. A provider a plugin\n * registered could be ABSENT where a door reads it — a declaration that threw\n * part-way, a route bundle that evaluated the registry apart from boot's —\n * and every answer an absent provider could give at a money door is wrong: a\n * refusal takes every sale on every site down, and a default lets a checkout\n * proceed against an account nobody checked. Compiled in, it cannot be\n * absent, so neither can happen.\n *\n * It lives in this library rather than in a plugin because three plugins take\n * money into the same account — a storefront, a calendar and the marketplace\n * — and one plugin may not import another. A plugin that owned the adapter\n * would make the other two's sales depend on its install and on its\n * registration having run; the adapter itself sells nothing and holds no\n * product, only the platform's own connected-account integration.\n *\n * A second adapter is one module beside the first and a selector here. There\n * is no selector today because there is nothing to select: every plugin's\n * checkout is created against the same vendor.\n *\n * ## The contract every adapter owes\n *\n * 1. **An account the provider did not describe is never ready.** Every\n * {@link MerchantAccountReadiness} other than `'ready'` refuses the charge,\n * and only a literal boolean is a recorded answer.\n * 2. **Only what the provider stated is written.** An account event that\n * states nothing writes nothing, and a written record never invents a\n * `false` or a `true` from an absent field.\n * 3. **Failures propagate.** {@link PaymentProvider.applyAccountEvent} runs\n * inside the platform's billing webhook, and a throw is what earns the\n * redelivery; every write it makes is idempotent so that redelivery\n * converges.\n */\n\n/** Which world a platform credential or a merchant account charges in. */\nexport type PaymentMode = 'live' | 'test'\n\n/**\n * Why a stored merchant account may not be charged against. Every value\n * other than `'ready'` is a refusal.\n */\nexport type MerchantAccountReadiness =\n /** Charge away. */\n | 'ready'\n /** No merchant account is stored at all — the onboarding call to action. */\n | 'not-connected'\n /** The provider will not let this account take charges. */\n | 'charges-disabled'\n /**\n * The account's mode was never recorded (AGL-2471) AND this deployment is\n * LIVE. The onboarding route and the provider's account event both record\n * it, so a genuine merchant self-heals on the next of either.\n */\n | 'mode-unverified'\n /** The account belongs to the OTHER world. The AGL-2471 defect. */\n | 'mode-mismatch'\n\nexport interface MerchantAccountReadinessInput {\n /** The stored account id, whatever shape the document has. */\n accountId?: unknown\n /** Whether the provider said the account may take charges. */\n chargesEnabled?: unknown\n /** Whether the provider said the account is live — absent before AGL-2471. */\n accountLivemode?: unknown\n /** Defaults to {@link PaymentProvider.platformMode}. */\n platformMode?: PaymentMode | undefined\n}\n\n/** One event the platform's billing webhook received, as the provider sent it. */\nexport interface PaymentProviderEvent {\n /** The provider's own name for the event. */\n type: string\n /** The object the event describes. */\n object: unknown\n /** The whole event as delivered. */\n event: unknown\n}\n\nexport interface PaymentProvider {\n /** Stable id, for log lines. Never a display name. */\n readonly id: string\n /**\n * The mode this deployment's platform credential states about itself, read\n * without a network call; `undefined` when there is no credential or it\n * states nothing.\n */\n platformMode(): PaymentMode | undefined\n /**\n * The platform's mode, asked of the provider first and the credential\n * second — what an onboarding route records against a merchant account it\n * has just retrieved.\n */\n resolvePlatformMode(): Promise<PaymentMode | undefined>\n /**\n * Applies an event about a connected merchant account — its readiness\n * changed, a payout to it failed, a later payout landed — to the documents\n * in `collection` that store it. A plugin passes the collection holding ITS\n * merchant profiles; a non-matching account updates nothing.\n *\n * @returns `true` when the event was about a merchant account and has been\n * applied, so the caller's own sections have nothing to do with it;\n * `false` for every other event.\n */\n applyAccountEvent(\n collection: string,\n delivered: PaymentProviderEvent,\n ): Promise<boolean>\n}\n\n/** The provider every merchant account on this deployment is held at. */\nexport function paymentProvider(): PaymentProvider {\n return STRIPE_CONNECT_PAYMENT_PROVIDER\n}\n\n/**\n * Decides whether a stored merchant account may be charged against.\n *\n * Order matters: the two refusals that existed before AGL-2471 are asked\n * first, so an unconnected or restricted merchant still gets the answer they\n * always got, and only an account that WOULD have passed reaches the mode\n * question.\n *\n * WHY THE MODE RULE IS ASYMMETRIC, AND NOT \"REFUSE ANYTHING UNPROVEN\". An\n * unrecorded mode refuses only on a LIVE deployment. That is where real money\n * moves, and a live deployment can always re-establish the field: the\n * merchant reconnects, or the provider sends one account event. Everywhere\n * else — a test deployment, a developer machine, a self-hosted install still\n * in sandbox, the whole test suite — an unverified account keeps the\n * behavior it had before, because no real money can move there and the\n * provider enforces the mode boundary itself. A PROVEN mismatch is refused in\n * both directions, because it costs nothing to detect and is never right.\n */\nexport function merchantAccountReadiness(\n input: MerchantAccountReadinessInput,\n): MerchantAccountReadiness {\n const accountId =\n typeof input.accountId === 'string' ? input.accountId.trim() : ''\n if (!accountId) return 'not-connected'\n if (input.chargesEnabled !== true) return 'charges-disabled'\n const platformMode =\n 'platformMode' in input\n ? input.platformMode\n : paymentProvider().platformMode()\n // Three-valued on purpose, exactly as AGL-1997 reads `payoutsEnabled`: only\n // a literal boolean is a recorded answer. A string `'true'` or a `1` is a\n // field somebody else wrote, and inventing `true` from it would re-open the\n // hole this closes.\n if (typeof input.accountLivemode === 'boolean') {\n // A PROVEN mismatch is refused in either direction. It costs nothing to\n // detect and it is always wrong.\n if (!platformMode) return 'ready'\n return input.accountLivemode === (platformMode === 'live')\n ? 'ready'\n : 'mode-mismatch'\n }\n return platformMode === 'live' ? 'mode-unverified' : 'ready'\n}\n\n/**\n * The form every money door calls: `true` only when the account may be\n * charged, with the two mode refusals reported to the server log.\n *\n * The log line is the part the shopper's generic 409 cannot carry. Before\n * this, a mode mismatch surfaced as a 502 from the provider with nothing\n * anywhere naming the cause; the sale is still refused the same way, but now\n * somebody can find out why.\n */\nexport function merchantAccountIsReady(\n input: MerchantAccountReadinessInput,\n context?: { subject?: string },\n): boolean {\n const readiness = merchantAccountReadiness(input)\n if (readiness === 'mode-mismatch' || readiness === 'mode-unverified') {\n console.error(\n `[AGL-2471] Refusing a charge against ${paymentProvider().id} account ` +\n `${String(input.accountId)}${\n context?.subject ? ` (${context.subject})` : ''\n }: ${readiness}. The stored account is not verified for this ` +\n `deployment's payment mode; the merchant must re-onboard.`,\n )\n }\n return readiness === 'ready'\n}\n"],"names":["STRIPE_CONNECT_PAYMENT_PROVIDER","paymentProvider","merchantAccountReadiness","input","accountId","trim","chargesEnabled","platformMode","accountLivemode","merchantAccountIsReady","context","readiness","console","error","id","String","subject"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,+BAA+B,QAAQ,uCAAmC;AAuInF,uEAAuE,GACvE,OAAO,SAASC;IACd,OAAOD;AACT;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASE,yBACdC,KAAoC;IAEpC,MAAMC,YACJ,OAAOD,MAAMC,SAAS,KAAK,WAAWD,MAAMC,SAAS,CAACC,IAAI,KAAK;IACjE,IAAI,CAACD,WAAW,OAAO;IACvB,IAAID,MAAMG,cAAc,KAAK,MAAM,OAAO;IAC1C,MAAMC,eACJ,kBAAkBJ,QACdA,MAAMI,YAAY,GAClBN,kBAAkBM,YAAY;IACpC,4EAA4E;IAC5E,0EAA0E;IAC1E,4EAA4E;IAC5E,oBAAoB;IACpB,IAAI,OAAOJ,MAAMK,eAAe,KAAK,WAAW;QAC9C,wEAAwE;QACxE,iCAAiC;QACjC,IAAI,CAACD,cAAc,OAAO;QAC1B,OAAOJ,MAAMK,eAAe,KAAMD,CAAAA,iBAAiB,MAAK,IACpD,UACA;IACN;IACA,OAAOA,iBAAiB,SAAS,oBAAoB;AACvD;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASE,uBACdN,KAAoC,EACpCO,OAA8B;IAE9B,MAAMC,YAAYT,yBAAyBC;IAC3C,IAAIQ,cAAc,mBAAmBA,cAAc,mBAAmB;QACpEC,QAAQC,KAAK,CACX,CAAC,qCAAqC,EAAEZ,kBAAkBa,EAAE,CAAC,SAAS,CAAC,GACrE,GAAGC,OAAOZ,MAAMC,SAAS,IACvBM,CAAAA,2BAAAA,QAASM,OAAO,IAAG,CAAC,EAAE,EAAEN,QAAQM,OAAO,CAAC,CAAC,CAAC,GAAG,GAC9C,EAAE,EAAEL,UAAU,8CAA8C,CAAC,GAC9D,CAAC,wDAAwD,CAAC;IAEhE;IACA,OAAOA,cAAc;AACvB"}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type PaymentRiskSignal, type PaymentRiskSignalKind } from '@aglyn/aglyn/app-utils/payment-risk';
|
|
18
18
|
import type { AglynNotificationType } from '@aglyn/aglyn/app-utils/notifications';
|
|
19
|
-
import type
|
|
19
|
+
import { type RiskEventKind } from '@aglyn/shared-util-email/risk-notice-catalog';
|
|
20
20
|
import type { RiskEventInput } from './risk-notice';
|
|
21
21
|
/**
|
|
22
22
|
* The notifier's shape: `notifyRiskEvent` from the admin barrel (AGL-3368),
|
|
@@ -30,6 +30,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
30
30
|
* Dependencies are passed in — the Firestore handle and the notifier — so a
|
|
31
31
|
* plugin's spec that substitutes the admin barrel substitutes these too.
|
|
32
32
|
*=========================================*/ import { addPaymentRiskSignal, PAYMENT_RISK_FIELD, settlePaymentRiskDispute } from "@aglyn/aglyn/app-utils/payment-risk";
|
|
33
|
+
import { RISK_PAYMENT_EVENTS } from "@aglyn/shared-util-email/risk-notice-catalog";
|
|
33
34
|
/** The risk notice each Stripe signal on a sale is told as. */ export const PAYMENT_RISK_NOTICE_KINDS = {
|
|
34
35
|
'early-fraud-warning': 'sale-fraud-warning',
|
|
35
36
|
'radar-review': 'sale-payment-review',
|
|
@@ -54,7 +55,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
54
55
|
return true;
|
|
55
56
|
});
|
|
56
57
|
if (written && (input.hostId || input.orgId)) {
|
|
57
|
-
var _input_noticeKind, _input_orgId, _input_amount, _input_evidenceDueByMs;
|
|
58
|
+
var _input_noticeKind, _input_orgId, _input_amount, _RISK_PAYMENT_EVENTS_input_signal_kind, _input_evidenceDueByMs;
|
|
58
59
|
await deps.notifyRisk({
|
|
59
60
|
kind: (_input_noticeKind = input.noticeKind) != null ? _input_noticeKind : PAYMENT_RISK_NOTICE_KINDS[input.signal.kind],
|
|
60
61
|
orgId: (_input_orgId = input.orgId) != null ? _input_orgId : null,
|
|
@@ -65,6 +66,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
65
66
|
},
|
|
66
67
|
occurredAtMs: input.signal.atMs,
|
|
67
68
|
amount: (_input_amount = input.amount) != null ? _input_amount : null,
|
|
69
|
+
paymentEvent: (_RISK_PAYMENT_EVENTS_input_signal_kind = RISK_PAYMENT_EVENTS[input.signal.kind]) != null ? _RISK_PAYMENT_EVENTS_input_signal_kind : null,
|
|
68
70
|
evidenceDueByMs: (_input_evidenceDueByMs = input.evidenceDueByMs) != null ? _input_evidenceDueByMs : null,
|
|
69
71
|
dedupeKey: `sale:${input.signal.stripeObjectId}`,
|
|
70
72
|
staffEvidence: input.signal.detail ? `Stripe says: ${input.signal.detail.replace(/_/g, ' ')}.` : null
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/payment-risk-record.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 * STAMP A STRIPE FRAUD SIGNAL ON A MERCHANT'S RECORD (AGL-3360).\n *\n * The server half of `@aglyn/aglyn/app-utils/payment-risk`. A plugin that\n * finds its own record behind a charge — an order, a booking — hands the\n * document here, and this writes the `paymentRisk` field once per signal and\n * tells the site's managers and the workspace's owners once, through the\n * risk notice seam (AGL-3368). Any plugin that sells through Stripe uses\n * the same two calls; nothing about the record's kind is known here.\n *\n * NOTHING IS REFUNDED OR CANCELED. The merchant decides for their own\n * customer; the notification says so.\n *\n * Dependencies are passed in — the Firestore handle and the notifier — so a\n * plugin's spec that substitutes the admin barrel substitutes these too.\n *=========================================*/\n\nimport {\n addPaymentRiskSignal,\n PAYMENT_RISK_FIELD,\n type PaymentRisk,\n type PaymentRiskSignal,\n type PaymentRiskSignalKind,\n settlePaymentRiskDispute,\n} from '@aglyn/aglyn/app-utils/payment-risk'\nimport type { AglynNotificationType } from '@aglyn/aglyn/app-utils/notifications'\nimport type
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/payment-risk-record.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 * STAMP A STRIPE FRAUD SIGNAL ON A MERCHANT'S RECORD (AGL-3360).\n *\n * The server half of `@aglyn/aglyn/app-utils/payment-risk`. A plugin that\n * finds its own record behind a charge — an order, a booking — hands the\n * document here, and this writes the `paymentRisk` field once per signal and\n * tells the site's managers and the workspace's owners once, through the\n * risk notice seam (AGL-3368). Any plugin that sells through Stripe uses\n * the same two calls; nothing about the record's kind is known here.\n *\n * NOTHING IS REFUNDED OR CANCELED. The merchant decides for their own\n * customer; the notification says so.\n *\n * Dependencies are passed in — the Firestore handle and the notifier — so a\n * plugin's spec that substitutes the admin barrel substitutes these too.\n *=========================================*/\n\nimport {\n addPaymentRiskSignal,\n PAYMENT_RISK_FIELD,\n type PaymentRisk,\n type PaymentRiskSignal,\n type PaymentRiskSignalKind,\n settlePaymentRiskDispute,\n} from '@aglyn/aglyn/app-utils/payment-risk'\nimport type { AglynNotificationType } from '@aglyn/aglyn/app-utils/notifications'\nimport {\n RISK_PAYMENT_EVENTS,\n type RiskEventKind,\n} from '@aglyn/shared-util-email/risk-notice-catalog'\nimport type { RiskEventInput } from './risk-notice'\n\n/**\n * The notifier's shape: `notifyRiskEvent` from the admin barrel (AGL-3368),\n * which tells the site's managers and the workspace's owners and admins what\n * arrived, what it means, and how to refund, keep or answer it.\n */\nexport type PaymentRiskNotifier = (input: RiskEventInput) => Promise<unknown>\n\n/** The risk notice each Stripe signal on a sale is told as. */\nexport const PAYMENT_RISK_NOTICE_KINDS: Record<PaymentRiskSignalKind, RiskEventKind> = {\n 'early-fraud-warning': 'sale-fraud-warning',\n 'radar-review': 'sale-payment-review',\n dispute: 'sale-dispute',\n}\n\nexport interface RecordPaymentRiskInput {\n ref: FirebaseFirestore.DocumentReference\n signal: PaymentRiskSignal\n /** The site whose managers are told. Empty tells only `orgId`'s owners. */\n hostId: string\n /**\n * The workspace told when the record belongs to no site — a marketplace\n * sale's publisher. Resolved from the site otherwise.\n */\n orgId?: string | null\n /**\n * The notice to send, when the record's owner cannot act the way a shop\n * does on its own sale (a marketplace publisher cannot refund a charge the\n * platform is merchant of record for). Defaults by signal kind.\n */\n noticeKind?: RiskEventKind\n /** What the record is, as the merchant calls it: `Order 1042`, `Booking`. */\n subjectLabel: string\n /**\n * The console path the notice's actions open, in the stored-notification\n * shape (`/{hostId}/…`) — the order's own dialog, the Bookings page.\n */\n link: string\n /**\n * The plugin's own notification type. Kept for the record's callers; the\n * notice itself is always a `system.riskNotice` now.\n */\n notificationType?: AglynNotificationType\n /** `$56.00 USD`, when the plugin knows the amount. */\n amount?: string | null\n /** A dispute's evidence deadline, when the plugin knows it. */\n evidenceDueByMs?: number | null\n /**\n * More fields to write in the same transaction, from the record as read —\n * a plugin's own activity line (an order's timeline) rides here.\n */\n extraUpdate?: (data: Record<string, unknown>) => Record<string, unknown>\n}\n\n/**\n * Stamp `signal` on `input.ref` and notify, once. Returns true when this\n * delivery wrote it; false for a redelivery or a record that is gone.\n * Throws only when the write throws, which the webhook answers with a 500 and\n * Stripe's redelivery.\n */\nexport async function recordPaymentRiskOnRecord(\n input: RecordPaymentRiskInput,\n deps: {\n firestore: FirebaseFirestore.Firestore\n notifyRisk: PaymentRiskNotifier\n },\n): Promise<boolean> {\n const written = await deps.firestore.runTransaction(async (transaction) => {\n const fresh = await transaction.get(input.ref)\n if (!fresh.exists) return false\n const data = (fresh.data() ?? {}) as Record<string, unknown>\n const next = addPaymentRiskSignal(\n data[PAYMENT_RISK_FIELD] as PaymentRisk | undefined,\n input.signal,\n )\n if (!next) return false\n transaction.update(input.ref, {\n ...(input.extraUpdate ? input.extraUpdate(data) : {}),\n [PAYMENT_RISK_FIELD]: next,\n })\n return true\n })\n if (written && (input.hostId || input.orgId)) {\n await deps\n .notifyRisk({\n kind: input.noticeKind ?? PAYMENT_RISK_NOTICE_KINDS[input.signal.kind],\n orgId: input.orgId ?? null,\n hostId: input.hostId || null,\n item: { label: input.subjectLabel, path: input.link },\n occurredAtMs: input.signal.atMs,\n amount: input.amount ?? null,\n paymentEvent: RISK_PAYMENT_EVENTS[input.signal.kind] ?? null,\n evidenceDueByMs: input.evidenceDueByMs ?? null,\n dedupeKey: `sale:${input.signal.stripeObjectId}`,\n staffEvidence: input.signal.detail\n ? `Stripe says: ${input.signal.detail.replace(/_/g, ' ')}.`\n : null,\n })\n .catch(() => undefined)\n }\n return written\n}\n\n/**\n * Record a dispute's outcome on the signal already stamped for it. Silent:\n * the plugin that owns money movement on a lost dispute says so itself.\n */\nexport async function settlePaymentRiskDisputeOnRecord(\n ref: FirebaseFirestore.DocumentReference,\n disputeId: string,\n outcome: string,\n deps: { firestore: FirebaseFirestore.Firestore },\n): Promise<boolean> {\n return deps.firestore.runTransaction(async (transaction) => {\n const fresh = await transaction.get(ref)\n if (!fresh.exists) return false\n const next = settlePaymentRiskDispute(\n fresh.get(PAYMENT_RISK_FIELD) as PaymentRisk | undefined,\n disputeId,\n outcome,\n )\n if (!next) return false\n transaction.update(ref, { [PAYMENT_RISK_FIELD]: next })\n return true\n })\n}\n"],"names":["addPaymentRiskSignal","PAYMENT_RISK_FIELD","settlePaymentRiskDispute","RISK_PAYMENT_EVENTS","PAYMENT_RISK_NOTICE_KINDS","dispute","recordPaymentRiskOnRecord","input","deps","written","firestore","runTransaction","transaction","fresh","get","ref","exists","data","next","signal","update","extraUpdate","hostId","orgId","notifyRisk","kind","noticeKind","item","label","subjectLabel","path","link","occurredAtMs","atMs","amount","paymentEvent","evidenceDueByMs","dedupeKey","stripeObjectId","staffEvidence","detail","replace","catch","undefined","settlePaymentRiskDisputeOnRecord","disputeId","outcome"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;2CAe2C,GAE3C,SACEA,oBAAoB,EACpBC,kBAAkB,EAIlBC,wBAAwB,QACnB,sCAAqC;AAE5C,SACEC,mBAAmB,QAEd,+CAA8C;AAUrD,6DAA6D,GAC7D,OAAO,MAAMC,4BAA0E;IACrF,uBAAuB;IACvB,gBAAgB;IAChBC,SAAS;AACX,EAAC;AAyCD;;;;;CAKC,GACD,OAAO,eAAeC,0BACpBC,KAA6B,EAC7BC,IAGC;IAED,MAAMC,UAAU,MAAMD,KAAKE,SAAS,CAACC,cAAc,CAAC,OAAOC;YAG3CC;QAFd,MAAMA,QAAQ,MAAMD,YAAYE,GAAG,CAACP,MAAMQ,GAAG;QAC7C,IAAI,CAACF,MAAMG,MAAM,EAAE,OAAO;QAC1B,MAAMC,QAAQJ,cAAAA,MAAMI,IAAI,cAAVJ,cAAgB,CAAC;QAC/B,MAAMK,OAAOlB,qBACXiB,IAAI,CAAChB,mBAAmB,EACxBM,MAAMY,MAAM;QAEd,IAAI,CAACD,MAAM,OAAO;QAClBN,YAAYQ,MAAM,CAACb,MAAMQ,GAAG,EAAE,aACxBR,MAAMc,WAAW,GAAGd,MAAMc,WAAW,CAACJ,QAAQ,CAAC;YACnD,CAAChB,mBAAmB,EAAEiB;;QAExB,OAAO;IACT;IACA,IAAIT,WAAYF,CAAAA,MAAMe,MAAM,IAAIf,MAAMgB,KAAK,AAAD,GAAI;YAGlChB,mBACCA,cAICA,eACMJ,wCACGI;QATrB,MAAMC,KACHgB,UAAU,CAAC;YACVC,IAAI,GAAElB,oBAAAA,MAAMmB,UAAU,YAAhBnB,oBAAoBH,yBAAyB,CAACG,MAAMY,MAAM,CAACM,IAAI,CAAC;YACtEF,KAAK,GAAEhB,eAAAA,MAAMgB,KAAK,YAAXhB,eAAe;YACtBe,QAAQf,MAAMe,MAAM,IAAI;YACxBK,MAAM;gBAAEC,OAAOrB,MAAMsB,YAAY;gBAAEC,MAAMvB,MAAMwB,IAAI;YAAC;YACpDC,cAAczB,MAAMY,MAAM,CAACc,IAAI;YAC/BC,MAAM,GAAE3B,gBAAAA,MAAM2B,MAAM,YAAZ3B,gBAAgB;YACxB4B,YAAY,GAAEhC,yCAAAA,mBAAmB,CAACI,MAAMY,MAAM,CAACM,IAAI,CAAC,YAAtCtB,yCAA0C;YACxDiC,eAAe,GAAE7B,yBAAAA,MAAM6B,eAAe,YAArB7B,yBAAyB;YAC1C8B,WAAW,CAAC,KAAK,EAAE9B,MAAMY,MAAM,CAACmB,cAAc,EAAE;YAChDC,eAAehC,MAAMY,MAAM,CAACqB,MAAM,GAC9B,CAAC,aAAa,EAAEjC,MAAMY,MAAM,CAACqB,MAAM,CAACC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,GACzD;QACN,GACCC,KAAK,CAAC,IAAMC;IACjB;IACA,OAAOlC;AACT;AAEA;;;CAGC,GACD,OAAO,eAAemC,iCACpB7B,GAAwC,EACxC8B,SAAiB,EACjBC,OAAe,EACftC,IAAgD;IAEhD,OAAOA,KAAKE,SAAS,CAACC,cAAc,CAAC,OAAOC;QAC1C,MAAMC,QAAQ,MAAMD,YAAYE,GAAG,CAACC;QACpC,IAAI,CAACF,MAAMG,MAAM,EAAE,OAAO;QAC1B,MAAME,OAAOhB,yBACXW,MAAMC,GAAG,CAACb,qBACV4C,WACAC;QAEF,IAAI,CAAC5B,MAAM,OAAO;QAClBN,YAAYQ,MAAM,CAACL,KAAK;YAAE,CAACd,mBAAmB,EAAEiB;QAAK;QACrD,OAAO;IACT;AACF"}
|
|
@@ -118,7 +118,7 @@ const firestoreApp = ()=>firebaseAdmin.app().firestore();
|
|
|
118
118
|
'org'
|
|
119
119
|
],
|
|
120
120
|
exported: true,
|
|
121
|
-
note: 'Each site tree the org owns —
|
|
121
|
+
note: 'Each site tree the org owns — pages, layouts, orders, form submissions. Secrets within are redacted, not omitted, so the export still says a webhook exists.'
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
collection: 'hostIndex',
|