@aglyn/plugins-commerce 1.0.0-beta.205 → 1.0.0-beta.207
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 +13 -13
- package/src/lib/constants/operator-alerts.d.ts +22 -0
- package/src/lib/constants/operator-alerts.js +85 -0
- package/src/lib/constants/operator-alerts.js.map +1 -0
- package/src/lib/declarations.server.d.ts +24 -0
- package/src/lib/declarations.server.js +33 -0
- package/src/lib/declarations.server.js.map +1 -0
- package/src/lib/server/billing-webhook.js +128 -25
- package/src/lib/server/billing-webhook.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-commerce",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.207",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
37
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.207",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.207",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.207",
|
|
31
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.207",
|
|
32
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.207",
|
|
33
|
+
"@aglyn/shared-util-email": "1.0.0-beta.207",
|
|
34
|
+
"@aglyn/shared-util-http": "1.0.0-beta.207",
|
|
35
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.207",
|
|
36
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.207",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.207",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.207",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.207",
|
|
40
40
|
"@stripe/react-stripe-js": "^3.10.0",
|
|
41
41
|
"@stripe/stripe-js": "^5.10.0",
|
|
42
42
|
"@swc/helpers": "0.5.23",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { OperatorAlertDefinition } from '@aglyn/aglyn/app-utils/operator-alerts';
|
|
18
|
+
export declare const COMMERCE_CHARGEBACK_UNROUTABLE: OperatorAlertDefinition;
|
|
19
|
+
export declare const COMMERCE_TAX_NOT_REVERSED: OperatorAlertDefinition;
|
|
20
|
+
export declare const COMMERCE_SELLER_SHARE_NOT_REVERSED: OperatorAlertDefinition;
|
|
21
|
+
export declare const COMMERCE_FEE_REPRICE_REFUSED: OperatorAlertDefinition;
|
|
22
|
+
export declare const COMMERCE_OPERATOR_ALERTS: readonly OperatorAlertDefinition[];
|
|
@@ -0,0 +1,85 @@
|
|
|
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 { BUNDLE_ID } from "./bundle-common.js";
|
|
17
|
+
/**
|
|
18
|
+
* The storefront's operator alerts (AGL-3377): the money faults only this
|
|
19
|
+
* plugin can see, declared in the registry's shape and registered from
|
|
20
|
+
* `declarations.server`, so core lists none of them and an install without
|
|
21
|
+
* commerce shows none of them on Staff → Operator alerts.
|
|
22
|
+
*
|
|
23
|
+
* Each is raised through `raiseOperatorAlert` with its definition, which
|
|
24
|
+
* also registers it in a process whose declarations have not loaded yet.
|
|
25
|
+
*/ const DAY = 24 * 60;
|
|
26
|
+
export const COMMERCE_CHARGEBACK_UNROUTABLE = {
|
|
27
|
+
type: 'commerce.chargebackUnroutable',
|
|
28
|
+
pluginId: BUNDLE_ID,
|
|
29
|
+
label: 'Storefront chargeback could not be routed',
|
|
30
|
+
description: 'A storefront chargeback matched no order, or more than one, so no order was flagged, no seller share reversed and no merchant told. A missing index means every storefront chargeback is being ignored.',
|
|
31
|
+
tier: 'must',
|
|
32
|
+
category: 'payments',
|
|
33
|
+
title: 'A {{amount}} chargeback could not be routed to an order',
|
|
34
|
+
body: '{{detail}}',
|
|
35
|
+
delivery: 'immediate',
|
|
36
|
+
dedupeWindowMinutes: DAY,
|
|
37
|
+
defaultEnabled: true
|
|
38
|
+
};
|
|
39
|
+
export const COMMERCE_TAX_NOT_REVERSED = {
|
|
40
|
+
type: 'commerce.taxNotReversed',
|
|
41
|
+
pluginId: BUNDLE_ID,
|
|
42
|
+
label: 'Subscription sales tax not reversed',
|
|
43
|
+
description: 'Sales tax collected on a storefront subscription stayed with the merchant instead of coming back to the platform, which files and remits it. The platform may owe tax it does not hold.',
|
|
44
|
+
tier: 'must',
|
|
45
|
+
category: 'payments',
|
|
46
|
+
title: 'Sales tax not reversed on invoice {{invoiceId}}',
|
|
47
|
+
body: '{{amount}} of sales tax on invoice {{invoiceId}} (site {{hostId}}) was not pulled back from the merchant: {{reason}}.',
|
|
48
|
+
delivery: 'immediate',
|
|
49
|
+
dedupeWindowMinutes: 7 * DAY,
|
|
50
|
+
defaultEnabled: true
|
|
51
|
+
};
|
|
52
|
+
export const COMMERCE_SELLER_SHARE_NOT_REVERSED = {
|
|
53
|
+
type: 'commerce.sellerShareNotReversed',
|
|
54
|
+
pluginId: BUNDLE_ID,
|
|
55
|
+
label: 'Seller share not reversed on a lost dispute',
|
|
56
|
+
description: 'A storefront dispute was lost and the merchant’s share could not be pulled back, so the platform is out the principal. Recover it by hand in Stripe.',
|
|
57
|
+
tier: 'must',
|
|
58
|
+
category: 'payments',
|
|
59
|
+
title: 'Seller share not reversed on dispute {{disputeId}}',
|
|
60
|
+
body: 'The lost dispute {{disputeId}} on order {{orderId}} (site {{hostId}}) cost {{amount}}, and the merchant’s share was not reversed: {{reason}}.',
|
|
61
|
+
delivery: 'immediate',
|
|
62
|
+
dedupeWindowMinutes: 30 * DAY,
|
|
63
|
+
defaultEnabled: true
|
|
64
|
+
};
|
|
65
|
+
export const COMMERCE_FEE_REPRICE_REFUSED = {
|
|
66
|
+
type: 'commerce.feeRepriceRefused',
|
|
67
|
+
pluginId: BUNDLE_ID,
|
|
68
|
+
label: 'Platform fee correction refused',
|
|
69
|
+
description: 'Stripe refused to change a storefront subscription’s platform fee, or to refund the part of a fee charged on the wrong base. The merchant is being charged a fee the plan does not set.',
|
|
70
|
+
tier: 'must',
|
|
71
|
+
category: 'payments',
|
|
72
|
+
title: 'Platform fee not corrected on {{subject}}',
|
|
73
|
+
body: 'The platform fee on {{subject}} (site {{hostId}}) could not be corrected: {{reason}}.',
|
|
74
|
+
delivery: 'immediate',
|
|
75
|
+
dedupeWindowMinutes: 7 * DAY,
|
|
76
|
+
defaultEnabled: true
|
|
77
|
+
};
|
|
78
|
+
export const COMMERCE_OPERATOR_ALERTS = [
|
|
79
|
+
COMMERCE_CHARGEBACK_UNROUTABLE,
|
|
80
|
+
COMMERCE_TAX_NOT_REVERSED,
|
|
81
|
+
COMMERCE_SELLER_SHARE_NOT_REVERSED,
|
|
82
|
+
COMMERCE_FEE_REPRICE_REFUSED
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=operator-alerts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/constants/operator-alerts.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 { OperatorAlertDefinition } from '@aglyn/aglyn/app-utils/operator-alerts'\nimport { BUNDLE_ID } from './bundle-common'\n\n/**\n * The storefront's operator alerts (AGL-3377): the money faults only this\n * plugin can see, declared in the registry's shape and registered from\n * `declarations.server`, so core lists none of them and an install without\n * commerce shows none of them on Staff → Operator alerts.\n *\n * Each is raised through `raiseOperatorAlert` with its definition, which\n * also registers it in a process whose declarations have not loaded yet.\n */\nconst DAY = 24 * 60\n\nexport const COMMERCE_CHARGEBACK_UNROUTABLE: OperatorAlertDefinition = {\n type: 'commerce.chargebackUnroutable',\n pluginId: BUNDLE_ID,\n label: 'Storefront chargeback could not be routed',\n description:\n 'A storefront chargeback matched no order, or more than one, so no order was flagged, no seller share reversed and no merchant told. A missing index means every storefront chargeback is being ignored.',\n tier: 'must',\n category: 'payments',\n title: 'A {{amount}} chargeback could not be routed to an order',\n body: '{{detail}}',\n delivery: 'immediate',\n dedupeWindowMinutes: DAY,\n defaultEnabled: true,\n}\n\nexport const COMMERCE_TAX_NOT_REVERSED: OperatorAlertDefinition = {\n type: 'commerce.taxNotReversed',\n pluginId: BUNDLE_ID,\n label: 'Subscription sales tax not reversed',\n description:\n 'Sales tax collected on a storefront subscription stayed with the merchant instead of coming back to the platform, which files and remits it. The platform may owe tax it does not hold.',\n tier: 'must',\n category: 'payments',\n title: 'Sales tax not reversed on invoice {{invoiceId}}',\n body:\n '{{amount}} of sales tax on invoice {{invoiceId}} (site {{hostId}}) was not pulled back from the merchant: {{reason}}.',\n delivery: 'immediate',\n dedupeWindowMinutes: 7 * DAY,\n defaultEnabled: true,\n}\n\nexport const COMMERCE_SELLER_SHARE_NOT_REVERSED: OperatorAlertDefinition = {\n type: 'commerce.sellerShareNotReversed',\n pluginId: BUNDLE_ID,\n label: 'Seller share not reversed on a lost dispute',\n description:\n 'A storefront dispute was lost and the merchant’s share could not be pulled back, so the platform is out the principal. Recover it by hand in Stripe.',\n tier: 'must',\n category: 'payments',\n title: 'Seller share not reversed on dispute {{disputeId}}',\n body:\n 'The lost dispute {{disputeId}} on order {{orderId}} (site {{hostId}}) cost {{amount}}, and the merchant’s share was not reversed: {{reason}}.',\n delivery: 'immediate',\n dedupeWindowMinutes: 30 * DAY,\n defaultEnabled: true,\n}\n\nexport const COMMERCE_FEE_REPRICE_REFUSED: OperatorAlertDefinition = {\n type: 'commerce.feeRepriceRefused',\n pluginId: BUNDLE_ID,\n label: 'Platform fee correction refused',\n description:\n 'Stripe refused to change a storefront subscription’s platform fee, or to refund the part of a fee charged on the wrong base. The merchant is being charged a fee the plan does not set.',\n tier: 'must',\n category: 'payments',\n title: 'Platform fee not corrected on {{subject}}',\n body: 'The platform fee on {{subject}} (site {{hostId}}) could not be corrected: {{reason}}.',\n delivery: 'immediate',\n dedupeWindowMinutes: 7 * DAY,\n defaultEnabled: true,\n}\n\nexport const COMMERCE_OPERATOR_ALERTS: readonly OperatorAlertDefinition[] = [\n COMMERCE_CHARGEBACK_UNROUTABLE,\n COMMERCE_TAX_NOT_REVERSED,\n COMMERCE_SELLER_SHARE_NOT_REVERSED,\n COMMERCE_FEE_REPRICE_REFUSED,\n]\n"],"names":["BUNDLE_ID","DAY","COMMERCE_CHARGEBACK_UNROUTABLE","type","pluginId","label","description","tier","category","title","body","delivery","dedupeWindowMinutes","defaultEnabled","COMMERCE_TAX_NOT_REVERSED","COMMERCE_SELLER_SHARE_NOT_REVERSED","COMMERCE_FEE_REPRICE_REFUSED","COMMERCE_OPERATOR_ALERTS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,SAAS,QAAQ,qBAAiB;AAE3C;;;;;;;;CAQC,GACD,MAAMC,MAAM,KAAK;AAEjB,OAAO,MAAMC,iCAA0D;IACrEC,MAAM;IACNC,UAAUJ;IACVK,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MAAM;IACNC,UAAU;IACVC,qBAAqBX;IACrBY,gBAAgB;AAClB,EAAC;AAED,OAAO,MAAMC,4BAAqD;IAChEX,MAAM;IACNC,UAAUJ;IACVK,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFC,UAAU;IACVC,qBAAqB,IAAIX;IACzBY,gBAAgB;AAClB,EAAC;AAED,OAAO,MAAME,qCAA8D;IACzEZ,MAAM;IACNC,UAAUJ;IACVK,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFC,UAAU;IACVC,qBAAqB,KAAKX;IAC1BY,gBAAgB;AAClB,EAAC;AAED,OAAO,MAAMG,+BAAwD;IACnEb,MAAM;IACNC,UAAUJ;IACVK,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MAAM;IACNC,UAAU;IACVC,qBAAqB,IAAIX;IACzBY,gBAAgB;AAClB,EAAC;AAED,OAAO,MAAMI,2BAA+D;IAC1Ef;IACAY;IACAC;IACAC;CACD,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
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 commerce plugin's server declarations: the light registrations core
|
|
19
|
+
* reads at boot, before any surface loads.
|
|
20
|
+
*
|
|
21
|
+
* Its operator alerts (AGL-3377), so Staff → Operator alerts lists them and
|
|
22
|
+
* staff can switch them before the first one is ever raised.
|
|
23
|
+
*/
|
|
24
|
+
export declare function registerCommerceServerDeclarations(): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
*/ // The seam from its own module, not the plugin-manager barrel: boot needs the
|
|
17
|
+
// registry and nothing else, and the barrel reaches the client contexts.
|
|
18
|
+
import { registerOperatorAlerts } from "@aglyn/aglyn/plugin-manager/operator-alerts";
|
|
19
|
+
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
20
|
+
import { COMMERCE_OPERATOR_ALERTS } from "./constants/operator-alerts.js";
|
|
21
|
+
/**
|
|
22
|
+
* The commerce plugin's server declarations: the light registrations core
|
|
23
|
+
* reads at boot, before any surface loads.
|
|
24
|
+
*
|
|
25
|
+
* Its operator alerts (AGL-3377), so Staff → Operator alerts lists them and
|
|
26
|
+
* staff can switch them before the first one is ever raised.
|
|
27
|
+
*/ export function registerCommerceServerDeclarations() {
|
|
28
|
+
registerOperatorAlerts(COMMERCE_OPERATOR_ALERTS, {
|
|
29
|
+
pluginId: BUNDLE_ID
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=declarations.server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/commerce/src/lib/declarations.server.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// The seam from its own module, not the plugin-manager barrel: boot needs the\n// registry and nothing else, and the barrel reaches the client contexts.\nimport { registerOperatorAlerts } from '@aglyn/aglyn/plugin-manager/operator-alerts'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { COMMERCE_OPERATOR_ALERTS } from './constants/operator-alerts'\n\n/**\n * The commerce plugin's server declarations: the light registrations core\n * reads at boot, before any surface loads.\n *\n * Its operator alerts (AGL-3377), so Staff → Operator alerts lists them and\n * staff can switch them before the first one is ever raised.\n */\nexport function registerCommerceServerDeclarations(): void {\n registerOperatorAlerts(COMMERCE_OPERATOR_ALERTS, { pluginId: BUNDLE_ID })\n}\n"],"names":["registerOperatorAlerts","BUNDLE_ID","COMMERCE_OPERATOR_ALERTS","registerCommerceServerDeclarations","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,yEAAyE;AACzE,SAASA,sBAAsB,QAAQ,8CAA6C;AACpF,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,wBAAwB,QAAQ,iCAA6B;AAEtE;;;;;;CAMC,GACD,OAAO,SAASC;IACdH,uBAAuBE,0BAA0B;QAAEE,UAAUH;IAAU;AACzE"}
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
17
|
import * as Aglyn from "@aglyn/aglyn/server";
|
|
18
|
-
import { findUserByUidAcrossPools, firebaseAdmin, getOrgForHost, meterHostEmail, notifyHostManagers, notifyRiskEvent,
|
|
18
|
+
import { findUserByUidAcrossPools, firebaseAdmin, getOrgForHost, meterHostEmail, notifyHostManagers, notifyRiskEvent, raiseOperatorAlert, renderHostEmailWithTokens, clearConnectPayoutFailure, recordConnectPayoutFailure, syncConnectAccountStatus, updateExisting, hostSendingIdentity } from "@aglyn/tenant-data-admin";
|
|
19
19
|
import { captureHostContact } from "@aglyn/tenant-runtime";
|
|
20
|
+
import { formatOperatorAlertAmount } from "@aglyn/aglyn/app-utils/operator-alerts";
|
|
21
|
+
import { COMMERCE_CHARGEBACK_UNROUTABLE, COMMERCE_FEE_REPRICE_REFUSED, COMMERCE_SELLER_SHARE_NOT_REVERSED, COMMERCE_TAX_NOT_REVERSED } from "../constants/operator-alerts.js";
|
|
20
22
|
import { createHmac } from "crypto";
|
|
21
23
|
import { isEmailConfigured, sendEmail } from "@aglyn/shared-util-email";
|
|
22
24
|
import * as CommerceModel from "../model/index.js";
|
|
@@ -274,21 +276,37 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
274
276
|
*
|
|
275
277
|
* Staff, not the merchant: on the missing-index path there is no merchant to
|
|
276
278
|
* tell — the whole point is that the order could not be found — and a missing
|
|
277
|
-
* platform index is a platform fault affecting every host at once.
|
|
278
|
-
* is the same channel the analytics and abuse routes use for exactly this
|
|
279
|
-
* class, and it never throws.
|
|
279
|
+
* platform index is a platform fault affecting every host at once.
|
|
280
280
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
281
|
+
* As this plugin's own operator alert (AGL-3377), which reaches the operator's
|
|
282
|
+
* inbox and webhook as well as the console. A missing index is one alert for
|
|
283
|
+
* every chargeback it drops, not one per chargeback.
|
|
284
284
|
*/ async function reportUnresolvedDispute(reason, paymentIntentId, dispute) {
|
|
285
285
|
var _dispute_amount;
|
|
286
|
-
const amount =
|
|
287
|
-
await
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
286
|
+
const amount = formatOperatorAlertAmount((_dispute_amount = dispute.amount) != null ? _dispute_amount : 0);
|
|
287
|
+
await raiseOperatorAlert(COMMERCE_CHARGEBACK_UNROUTABLE, {
|
|
288
|
+
dedupeKey: reason === 'missing-index' ? 'missing-index' : paymentIntentId,
|
|
289
|
+
url: '/admin',
|
|
290
|
+
context: {
|
|
291
|
+
amount,
|
|
292
|
+
detail: reason === 'missing-index' ? `A ${amount} chargeback (${paymentIntentId}) could not be looked ` + 'up: the collection-group index on orders.paymentIntentId is not ' + 'deployed, so EVERY commerce chargeback is currently being ignored ' + '— no flag, no seller-share reversal, no merchant notification. ' + 'Deploy cloud/firebase-firestore.indexes.json.' : `A ${amount} chargeback (${paymentIntentId}) matched more than one ` + 'order, so none was reversed. The dispute is durable in Stripe and ' + 'needs reconciling by hand.'
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Tells the operator that a subscription cycle's sales tax stayed with the
|
|
298
|
+
* merchant (AGL-3377): the platform files and remits it, so every one of
|
|
299
|
+
* these is tax it may owe and does not hold. Once per invoice and outcome, so
|
|
300
|
+
* Stripe's redeliveries of the same invoice say it once.
|
|
301
|
+
*/ async function reportTaxNotReversed(input) {
|
|
302
|
+
await raiseOperatorAlert(COMMERCE_TAX_NOT_REVERSED, {
|
|
303
|
+
dedupeKey: `${input.invoiceId}:${input.status}`,
|
|
304
|
+
context: {
|
|
305
|
+
invoiceId: input.invoiceId,
|
|
306
|
+
hostId: input.hostId,
|
|
307
|
+
amount: input.taxCents === undefined ? 'An unknown amount' : formatOperatorAlertAmount(input.taxCents),
|
|
308
|
+
reason: input.reason
|
|
309
|
+
}
|
|
292
310
|
});
|
|
293
311
|
}
|
|
294
312
|
/** The dispute record this event describes, before any outcome is known. */ function disputeFromEvent(dispute) {
|
|
@@ -713,10 +731,21 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
713
731
|
}).toString()
|
|
714
732
|
}).catch(()=>null);
|
|
715
733
|
if (!response || !response.ok) {
|
|
734
|
+
var _ref, _ref1;
|
|
735
|
+
var _subscriptionRef_parent_parent, _refusal_error;
|
|
716
736
|
if (!response || isTransientStripeStatus(response.status)) {
|
|
717
737
|
throw new Error(`Stripe refused to re-price subscription ${subscriptionId} ` + `(${response ? response.status : 'network'})`);
|
|
718
738
|
}
|
|
719
|
-
|
|
739
|
+
const refusal = await response.json().catch(()=>null);
|
|
740
|
+
console.error(`Stripe refused the fee re-price for ${subscriptionId} (AGL-2289)`, refusal);
|
|
741
|
+
await raiseOperatorAlert(COMMERCE_FEE_REPRICE_REFUSED, {
|
|
742
|
+
dedupeKey: `${subscriptionId}:${desiredFeePct}`,
|
|
743
|
+
context: {
|
|
744
|
+
subject: `subscription ${subscriptionId}`,
|
|
745
|
+
hostId: (_ref = (_subscriptionRef_parent_parent = subscriptionRef.parent.parent) == null ? void 0 : _subscriptionRef_parent_parent.id) != null ? _ref : '',
|
|
746
|
+
reason: `Stripe refused to set the fee to ${desiredFeePct}% (${(_ref1 = refusal == null ? void 0 : (_refusal_error = refusal.error) == null ? void 0 : _refusal_error.message) != null ? _ref1 : `HTTP ${response.status}`})`
|
|
747
|
+
}
|
|
748
|
+
});
|
|
720
749
|
return;
|
|
721
750
|
}
|
|
722
751
|
await subscriptionRef.set({
|
|
@@ -725,6 +754,20 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
725
754
|
merge: true
|
|
726
755
|
}).catch(()=>undefined);
|
|
727
756
|
}
|
|
757
|
+
/**
|
|
758
|
+
* Tells the operator a subscription cycle kept a fee charged on the wrong
|
|
759
|
+
* base (AGL-3377): the merchant paid more than the plan sets. Once per
|
|
760
|
+
* invoice.
|
|
761
|
+
*/ async function reportFeeNotCorrected(hostId, invoiceId, reason) {
|
|
762
|
+
await raiseOperatorAlert(COMMERCE_FEE_REPRICE_REFUSED, {
|
|
763
|
+
dedupeKey: invoiceId,
|
|
764
|
+
context: {
|
|
765
|
+
subject: `invoice ${invoiceId}`,
|
|
766
|
+
hostId,
|
|
767
|
+
reason
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
728
771
|
/**
|
|
729
772
|
* The charge a paid invoice settled against, on whichever spelling this
|
|
730
773
|
* endpoint's API version uses (AGL-2317).
|
|
@@ -848,6 +891,7 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
848
891
|
hostId,
|
|
849
892
|
invoiceId
|
|
850
893
|
});
|
|
894
|
+
await reportFeeNotCorrected(hostId, invoiceId, 'STRIPE_SECRET_KEY is not set on this deployment');
|
|
851
895
|
return chargedCents;
|
|
852
896
|
}
|
|
853
897
|
const chargeId = await resolveInvoiceChargeId(invoice, stripeKey);
|
|
@@ -856,6 +900,7 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
856
900
|
hostId,
|
|
857
901
|
invoiceId
|
|
858
902
|
});
|
|
903
|
+
await reportFeeNotCorrected(hostId, invoiceId, 'the invoice names no charge');
|
|
859
904
|
return chargedCents;
|
|
860
905
|
}
|
|
861
906
|
const fees = await stripeGet(`https://api.stripe.com/v1/application_fees?limit=1&charge=${encodeURIComponent(chargeId)}`, stripeKey);
|
|
@@ -870,6 +915,7 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
870
915
|
chargeId,
|
|
871
916
|
status: fees.status
|
|
872
917
|
});
|
|
918
|
+
await reportFeeNotCorrected(hostId, invoiceId, `no application fee was found on charge ${chargeId}`);
|
|
873
919
|
return chargedCents;
|
|
874
920
|
}
|
|
875
921
|
const feeAmount = Math.max(0, Math.round(Number((_fee_amount = fee.amount) != null ? _fee_amount : 0)));
|
|
@@ -890,6 +936,8 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
890
936
|
}).toString()
|
|
891
937
|
}).catch(()=>null);
|
|
892
938
|
if (!response || !response.ok) {
|
|
939
|
+
var _ref1;
|
|
940
|
+
var _refusal_error;
|
|
893
941
|
if (!response || isTransientStripeStatus(response.status)) {
|
|
894
942
|
// Let Stripe redeliver — the ledger write is keyed on the invoice id, so
|
|
895
943
|
// a retry re-runs this and nothing else (the AGL-1743 discipline).
|
|
@@ -897,7 +945,9 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
897
945
|
}
|
|
898
946
|
// Definitive: no redelivery fixes it, and throwing would have Stripe retry
|
|
899
947
|
// the whole invoice forever. Record what was actually taken.
|
|
900
|
-
|
|
948
|
+
const refusal = await response.json().catch(()=>null);
|
|
949
|
+
console.error(`Stripe refused the items-only fee correction for ${invoiceId} (AGL-2317)`, refusal);
|
|
950
|
+
await reportFeeNotCorrected(hostId, invoiceId, `Stripe refused to refund ${formatOperatorAlertAmount(refundCents)} of the fee (${(_ref1 = refusal == null ? void 0 : (_refusal_error = refusal.error) == null ? void 0 : _refusal_error.message) != null ? _ref1 : `HTTP ${response.status}`})`);
|
|
901
951
|
return chargedCents;
|
|
902
952
|
}
|
|
903
953
|
return desiredCents;
|
|
@@ -1102,11 +1152,12 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
1102
1152
|
status: 'unreadable',
|
|
1103
1153
|
reason: decision.reason
|
|
1104
1154
|
});
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1155
|
+
await reportTaxNotReversed({
|
|
1156
|
+
hostId,
|
|
1157
|
+
invoiceId,
|
|
1158
|
+
status: 'unreadable',
|
|
1159
|
+
reason: `the invoice enables automatic tax but states no readable tax field (${decision.reason})`
|
|
1160
|
+
});
|
|
1110
1161
|
return;
|
|
1111
1162
|
}
|
|
1112
1163
|
const taxCents = decision.taxCents;
|
|
@@ -1116,6 +1167,13 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
1116
1167
|
hostId,
|
|
1117
1168
|
invoiceId
|
|
1118
1169
|
});
|
|
1170
|
+
await reportTaxNotReversed({
|
|
1171
|
+
hostId,
|
|
1172
|
+
invoiceId,
|
|
1173
|
+
taxCents,
|
|
1174
|
+
status: 'no-key',
|
|
1175
|
+
reason: 'STRIPE_SECRET_KEY is not set on this deployment'
|
|
1176
|
+
});
|
|
1119
1177
|
return;
|
|
1120
1178
|
}
|
|
1121
1179
|
// GUARD 1. Every ordinary redelivery stops here, before any Stripe call, and
|
|
@@ -1168,6 +1226,13 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
1168
1226
|
status: 'charge-unreadable',
|
|
1169
1227
|
taxCents
|
|
1170
1228
|
});
|
|
1229
|
+
await reportTaxNotReversed({
|
|
1230
|
+
hostId,
|
|
1231
|
+
invoiceId,
|
|
1232
|
+
taxCents,
|
|
1233
|
+
status: 'charge-unreadable',
|
|
1234
|
+
reason: `Stripe refused to read charge ${chargeId}`
|
|
1235
|
+
});
|
|
1171
1236
|
return;
|
|
1172
1237
|
}
|
|
1173
1238
|
const transferId = String((_ref = (_charge_body = charge.body) == null ? void 0 : _charge_body.transfer) != null ? _ref : '');
|
|
@@ -1197,6 +1262,13 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
1197
1262
|
status: 'transfer-unreadable',
|
|
1198
1263
|
taxCents
|
|
1199
1264
|
});
|
|
1265
|
+
await reportTaxNotReversed({
|
|
1266
|
+
hostId,
|
|
1267
|
+
invoiceId,
|
|
1268
|
+
taxCents,
|
|
1269
|
+
status: 'transfer-unreadable',
|
|
1270
|
+
reason: `Stripe refused to read transfer ${transferId}`
|
|
1271
|
+
});
|
|
1200
1272
|
return;
|
|
1201
1273
|
}
|
|
1202
1274
|
// GUARD 2 — the crash window, and LOAD-BEARING now that the idempotency key
|
|
@@ -1258,6 +1330,13 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
1258
1330
|
transferId,
|
|
1259
1331
|
owedCents: taxCents
|
|
1260
1332
|
});
|
|
1333
|
+
await reportTaxNotReversed({
|
|
1334
|
+
hostId,
|
|
1335
|
+
invoiceId,
|
|
1336
|
+
taxCents,
|
|
1337
|
+
status: 'nothing-left',
|
|
1338
|
+
reason: `transfer ${transferId} had nothing left to reverse, so the tax must be reconciled by hand`
|
|
1339
|
+
});
|
|
1261
1340
|
return;
|
|
1262
1341
|
}
|
|
1263
1342
|
const response = await fetch(`https://api.stripe.com/v1/transfers/${encodeURIComponent(transferId)}/reversals`, {
|
|
@@ -1313,11 +1392,13 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
1313
1392
|
status: 'refused'
|
|
1314
1393
|
}, failureFields));
|
|
1315
1394
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1395
|
+
await reportTaxNotReversed({
|
|
1396
|
+
hostId,
|
|
1397
|
+
invoiceId,
|
|
1398
|
+
taxCents,
|
|
1399
|
+
status: insufficient ? 'insufficient' : 'refused',
|
|
1400
|
+
reason: insufficient ? 'the connected account had insufficient balance; it will retry on the next delivery' : `Stripe refused the reversal: ${String((_ref9 = body == null ? void 0 : (_body_error2 = body.error) == null ? void 0 : _body_error2.message) != null ? _ref9 : 'no reason given')}`
|
|
1401
|
+
});
|
|
1321
1402
|
return;
|
|
1322
1403
|
}
|
|
1323
1404
|
const reversedCents = Math.max(0, Math.round(Number((_ref4 = body == null ? void 0 : body.amount) != null ? _ref4 : reverseCents)));
|
|
@@ -1449,6 +1530,7 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
1449
1530
|
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
|
1450
1531
|
if (!stripeKey) {
|
|
1451
1532
|
console.error('Transfer reversal skipped: STRIPE_SECRET_KEY is not set (AGL-1794)');
|
|
1533
|
+
await reportSellerShareNotReversed(orderRef, disputeId, principalCents, 'STRIPE_SECRET_KEY is not set on this deployment');
|
|
1452
1534
|
return 0;
|
|
1453
1535
|
}
|
|
1454
1536
|
/**
|
|
@@ -1505,6 +1587,10 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
1505
1587
|
reason: outcome.reason
|
|
1506
1588
|
});
|
|
1507
1589
|
await settle(0, null, SELLER_SHARE_NOT_REVERSED_NOTE[outcome.reason]);
|
|
1590
|
+
// Already reversed on the transfer is money back, not money lost.
|
|
1591
|
+
if (outcome.reason !== 'transfer-fully-reversed') {
|
|
1592
|
+
await reportSellerShareNotReversed(orderRef, disputeId, principalCents, SELLER_SHARE_NOT_REVERSED_NOTE[outcome.reason]);
|
|
1593
|
+
}
|
|
1508
1594
|
return 0;
|
|
1509
1595
|
}
|
|
1510
1596
|
// Announced on adoption too: the delivery that created an adopted reversal
|
|
@@ -1513,6 +1599,23 @@ async function findOrderForDispute(paymentIntentId) {
|
|
|
1513
1599
|
const wrote = await settle(outcome.cents, outcome.reversalId, `$${(outcome.cents / 100).toFixed(2)} seller share reversed for lost dispute`);
|
|
1514
1600
|
return wrote ? outcome.cents : 0;
|
|
1515
1601
|
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Tells the operator the platform is out a lost dispute's principal because
|
|
1604
|
+
* the merchant's share could not be pulled back (AGL-3377). Once per dispute.
|
|
1605
|
+
*/ async function reportSellerShareNotReversed(orderRef, disputeId, principalCents, reason) {
|
|
1606
|
+
var _ref;
|
|
1607
|
+
var _orderRef_parent_parent;
|
|
1608
|
+
await raiseOperatorAlert(COMMERCE_SELLER_SHARE_NOT_REVERSED, {
|
|
1609
|
+
dedupeKey: disputeId,
|
|
1610
|
+
context: {
|
|
1611
|
+
disputeId,
|
|
1612
|
+
orderId: orderRef.id,
|
|
1613
|
+
hostId: (_ref = (_orderRef_parent_parent = orderRef.parent.parent) == null ? void 0 : _orderRef_parent_parent.id) != null ? _ref : '',
|
|
1614
|
+
amount: formatOperatorAlertAmount(principalCents),
|
|
1615
|
+
reason
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1516
1619
|
/** The order timeline's words for each final reason nothing was pulled back. */ const SELLER_SHARE_NOT_REVERSED_NOTE = {
|
|
1517
1620
|
'no-charge-on-cause': 'Seller share not reversed — no charge on the dispute',
|
|
1518
1621
|
'charge-read-refused': 'Seller share not reversed — charge not found at Stripe',
|