@aglyn/tenant-data-admin 1.0.0-beta.202 → 1.0.0-beta.203
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/tenant-data-admin",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.203",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-util-fbserver": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-util-first-touch": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.203",
|
|
29
|
+
"@aglyn/shared-util-email": "1.0.0-beta.203",
|
|
30
|
+
"@aglyn/shared-util-fbserver": "1.0.0-beta.203",
|
|
31
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.203",
|
|
32
|
+
"@aglyn/shared-util-http": "1.0.0-beta.203",
|
|
33
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.203",
|
|
34
34
|
"@msgpack/msgpack": "^3.1.3",
|
|
35
35
|
"@swc/helpers": "0.5.23",
|
|
36
36
|
"sharp": "^0.35.3",
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
/** Why nothing was pulled back, when the answer is final. */
|
|
18
|
+
export type TransferReversalFailure = 'no-charge-on-cause' | 'charge-read-refused' | 'no-transfer-on-charge' | 'transfer-read-refused' | 'transfer-fully-reversed' | 'reversal-refused';
|
|
19
|
+
export type TransferReversalOutcome =
|
|
20
|
+
/** Pulled back now, or found already pulled back and adopted. */
|
|
21
|
+
{
|
|
22
|
+
kind: 'reversed';
|
|
23
|
+
cents: number;
|
|
24
|
+
reversalId: string;
|
|
25
|
+
adopted: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** Final: settle it and record why. `owedCents` when the share was known. */
|
|
28
|
+
| {
|
|
29
|
+
kind: 'not-reversed';
|
|
30
|
+
reason: TransferReversalFailure;
|
|
31
|
+
owedCents?: number;
|
|
32
|
+
}
|
|
33
|
+
/** Nothing was attempted and nothing should be settled: retry later. */
|
|
34
|
+
| {
|
|
35
|
+
kind: 'skipped';
|
|
36
|
+
reason: 'no-stripe-key';
|
|
37
|
+
};
|
|
38
|
+
export interface TransferReversalCause {
|
|
39
|
+
/** Which door: decides the metadata key and the idempotency key. */
|
|
40
|
+
kind: 'dispute' | 'refund';
|
|
41
|
+
/** The dispute's or refund's Stripe id. */
|
|
42
|
+
id: string;
|
|
43
|
+
/** Cents going back to the cardholder. */
|
|
44
|
+
amountCents: number;
|
|
45
|
+
/** The charge whose transfer is pulled back. */
|
|
46
|
+
chargeId: string;
|
|
47
|
+
/** More metadata for the reversal: the caller's record id. */
|
|
48
|
+
metadata?: Record<string, string>;
|
|
49
|
+
}
|
|
50
|
+
/** Stripe failures a redelivery can actually fix. */
|
|
51
|
+
export declare function isTransientStripeStatus(status: number): boolean;
|
|
52
|
+
type FetchLike = typeof fetch;
|
|
53
|
+
/** The metadata key a cause's reversal is stamped and found by. */
|
|
54
|
+
export declare function transferReversalMetadataKey(kind: TransferReversalCause['kind']): string;
|
|
55
|
+
/**
|
|
56
|
+
* Pull the seller's proportional share of `cause` back from the charge's
|
|
57
|
+
* transfer. See the module header for the rules.
|
|
58
|
+
*/
|
|
59
|
+
export declare function reverseDestinationTransfer(cause: TransferReversalCause, deps?: {
|
|
60
|
+
stripeKey?: string;
|
|
61
|
+
fetchImpl?: FetchLike;
|
|
62
|
+
}): Promise<TransferReversalOutcome>;
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /*==========================================
|
|
17
|
+
* A SELLER'S SHARE OF A DESTINATION CHARGE, PULLED BACK (AGL-1794, AGL-3363).
|
|
18
|
+
*
|
|
19
|
+
* Every tenant sale is a destination charge on the platform account, so a
|
|
20
|
+
* lost dispute debits the PLATFORM's balance while the seller keeps the
|
|
21
|
+
* transfer. The AGL-1794 policy: the seller returns their share by a
|
|
22
|
+
* `transfers/{id}/reversals` call; the platform keeps Stripe's dispute fee.
|
|
23
|
+
* This is the Stripe half of that, written once. It started as the commerce
|
|
24
|
+
* webhook's `reverseSellerShare`; the booking deposit's lost dispute uses it
|
|
25
|
+
* too (AGL-3363), so the two cannot drift. Each caller keeps its own record
|
|
26
|
+
* and its own settle marker — this module touches no Firestore.
|
|
27
|
+
*
|
|
28
|
+
* PROPORTIONAL, NEVER MORE: `cause × transfer.amount ÷ charge.amount`,
|
|
29
|
+
* FLOORED, capped at what the transfer has left (`amount − amount_reversed`).
|
|
30
|
+
* No `refund_application_fee`: on a chargeback the platform keeps its
|
|
31
|
+
* commission, a policy switch the AGL-1794 note says not to flip here.
|
|
32
|
+
*
|
|
33
|
+
* IDEMPOTENT at Stripe twice over: a reversal already on the transfer carrying
|
|
34
|
+
* this cause's id in its metadata is ADOPTED rather than repeated (the crash
|
|
35
|
+
* window between the POST and the caller's record), and the POST carries an
|
|
36
|
+
* `Idempotency-Key` derived from the cause, so a racing delivery gets Stripe's
|
|
37
|
+
* stored answer.
|
|
38
|
+
*
|
|
39
|
+
* TRANSIENT failures (a network reject, 429, 5xx) THROW, so the webhook's
|
|
40
|
+
* redelivery is the retry. DEFINITIVE ones answer `not-reversed` with a
|
|
41
|
+
* reason, for the caller to settle and record; no redelivery fixes them.
|
|
42
|
+
*=========================================*/ /** Why nothing was pulled back, when the answer is final. */ /** Stripe failures a redelivery can actually fix. */ export function isTransientStripeStatus(status) {
|
|
43
|
+
return status === 429 || status >= 500;
|
|
44
|
+
}
|
|
45
|
+
async function stripeGet(url, stripeKey, fetchImpl) {
|
|
46
|
+
const response = await fetchImpl(url, {
|
|
47
|
+
headers: {
|
|
48
|
+
Authorization: `Bearer ${stripeKey}`
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const body = await response.json().catch(()=>null);
|
|
52
|
+
return {
|
|
53
|
+
ok: response.ok,
|
|
54
|
+
status: response.status,
|
|
55
|
+
body
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** The metadata key a cause's reversal is stamped and found by. */ export function transferReversalMetadataKey(kind) {
|
|
59
|
+
return kind === 'dispute' ? 'disputeId' : 'refundId';
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pull the seller's proportional share of `cause` back from the charge's
|
|
63
|
+
* transfer. See the module header for the rules.
|
|
64
|
+
*/ export async function reverseDestinationTransfer(cause, deps = {}) {
|
|
65
|
+
var _deps_stripeKey, _deps_fetchImpl, _cause_id, _cause_chargeId, _ref, _ref1, _ref2, _ref3, _ref4, _cause_amountCents, _cause_metadata, _ref5, _ref6;
|
|
66
|
+
var _charge_body, _charge_body1, _transfer_body_reversals, _transfer_body, _transfer_body1, _transfer_body2;
|
|
67
|
+
const stripeKey = (_deps_stripeKey = deps.stripeKey) != null ? _deps_stripeKey : process.env.STRIPE_SECRET_KEY;
|
|
68
|
+
if (!stripeKey) return {
|
|
69
|
+
kind: 'skipped',
|
|
70
|
+
reason: 'no-stripe-key'
|
|
71
|
+
};
|
|
72
|
+
const fetchImpl = (_deps_fetchImpl = deps.fetchImpl) != null ? _deps_fetchImpl : fetch;
|
|
73
|
+
const causeId = String((_cause_id = cause.id) != null ? _cause_id : '');
|
|
74
|
+
const metadataKey = transferReversalMetadataKey(cause.kind);
|
|
75
|
+
const label = `${cause.kind} ${causeId}`;
|
|
76
|
+
const chargeId = String((_cause_chargeId = cause.chargeId) != null ? _cause_chargeId : '');
|
|
77
|
+
if (!chargeId) return {
|
|
78
|
+
kind: 'not-reversed',
|
|
79
|
+
reason: 'no-charge-on-cause'
|
|
80
|
+
};
|
|
81
|
+
const charge = await stripeGet(`https://api.stripe.com/v1/charges/${chargeId}`, stripeKey, fetchImpl);
|
|
82
|
+
if (!charge.ok) {
|
|
83
|
+
var _charge_body2;
|
|
84
|
+
if (isTransientStripeStatus(charge.status)) {
|
|
85
|
+
throw new Error(`Stripe charge read failed (${charge.status}) for ${label}`);
|
|
86
|
+
}
|
|
87
|
+
console.error('Stripe refused the charge read', (_charge_body2 = charge.body) == null ? void 0 : _charge_body2.error);
|
|
88
|
+
return {
|
|
89
|
+
kind: 'not-reversed',
|
|
90
|
+
reason: 'charge-read-refused'
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const transferId = String((_ref = (_charge_body = charge.body) == null ? void 0 : _charge_body.transfer) != null ? _ref : '');
|
|
94
|
+
const chargeAmountCents = Math.round(Number((_ref1 = (_charge_body1 = charge.body) == null ? void 0 : _charge_body1.amount) != null ? _ref1 : 0));
|
|
95
|
+
if (!transferId || !(chargeAmountCents > 0)) {
|
|
96
|
+
return {
|
|
97
|
+
kind: 'not-reversed',
|
|
98
|
+
reason: 'no-transfer-on-charge'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const transfer = await stripeGet(`https://api.stripe.com/v1/transfers/${transferId}`, stripeKey, fetchImpl);
|
|
102
|
+
if (!transfer.ok) {
|
|
103
|
+
var _transfer_body3;
|
|
104
|
+
if (isTransientStripeStatus(transfer.status)) {
|
|
105
|
+
throw new Error(`Stripe transfer read failed (${transfer.status}) for ${label}`);
|
|
106
|
+
}
|
|
107
|
+
console.error('Stripe refused the transfer read', (_transfer_body3 = transfer.body) == null ? void 0 : _transfer_body3.error);
|
|
108
|
+
return {
|
|
109
|
+
kind: 'not-reversed',
|
|
110
|
+
reason: 'transfer-read-refused'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// The crash window's backstop: the POST landed on a previous delivery and
|
|
114
|
+
// the caller's record did not. Adopt it rather than creating a second one.
|
|
115
|
+
const existing = ((_ref2 = (_transfer_body = transfer.body) == null ? void 0 : (_transfer_body_reversals = _transfer_body.reversals) == null ? void 0 : _transfer_body_reversals.data) != null ? _ref2 : []).find((item)=>{
|
|
116
|
+
var _ref;
|
|
117
|
+
var _item_metadata;
|
|
118
|
+
return String((_ref = item == null ? void 0 : (_item_metadata = item.metadata) == null ? void 0 : _item_metadata[metadataKey]) != null ? _ref : '') === causeId;
|
|
119
|
+
});
|
|
120
|
+
if (existing) {
|
|
121
|
+
var _existing_amount, _existing_id;
|
|
122
|
+
return {
|
|
123
|
+
kind: 'reversed',
|
|
124
|
+
cents: Math.round(Number((_existing_amount = existing.amount) != null ? _existing_amount : 0)),
|
|
125
|
+
reversalId: String((_existing_id = existing.id) != null ? _existing_id : ''),
|
|
126
|
+
adopted: true
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const transferCents = Math.round(Number((_ref3 = (_transfer_body1 = transfer.body) == null ? void 0 : _transfer_body1.amount) != null ? _ref3 : 0));
|
|
130
|
+
const alreadyReversedCents = Math.round(Number((_ref4 = (_transfer_body2 = transfer.body) == null ? void 0 : _transfer_body2.amount_reversed) != null ? _ref4 : 0));
|
|
131
|
+
const remainingCents = Math.max(0, transferCents - alreadyReversedCents);
|
|
132
|
+
const causeCents = Math.round(Number((_cause_amountCents = cause.amountCents) != null ? _cause_amountCents : 0));
|
|
133
|
+
const shareCents = Math.min(Math.floor(causeCents * transferCents / chargeAmountCents), remainingCents);
|
|
134
|
+
if (!(shareCents > 0)) {
|
|
135
|
+
return {
|
|
136
|
+
kind: 'not-reversed',
|
|
137
|
+
reason: 'transfer-fully-reversed',
|
|
138
|
+
owedCents: 0
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const params = new URLSearchParams({
|
|
142
|
+
amount: String(shareCents),
|
|
143
|
+
[`metadata[${metadataKey}]`]: causeId
|
|
144
|
+
});
|
|
145
|
+
for (const [key, value] of Object.entries((_cause_metadata = cause.metadata) != null ? _cause_metadata : {})){
|
|
146
|
+
params.set(`metadata[${key}]`, value);
|
|
147
|
+
}
|
|
148
|
+
const response = await fetchImpl(`https://api.stripe.com/v1/transfers/${transferId}/reversals`, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: {
|
|
151
|
+
Authorization: `Bearer ${stripeKey}`,
|
|
152
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
153
|
+
'Idempotency-Key': `${cause.kind}-reversal-${causeId}`
|
|
154
|
+
},
|
|
155
|
+
body: params.toString()
|
|
156
|
+
});
|
|
157
|
+
const reversal = await response.json().catch(()=>null);
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
if (isTransientStripeStatus(response.status)) {
|
|
160
|
+
throw new Error(`Stripe transfer reversal failed (${response.status}) for ${label}`);
|
|
161
|
+
}
|
|
162
|
+
console.error('Stripe refused the transfer reversal', reversal == null ? void 0 : reversal.error);
|
|
163
|
+
return {
|
|
164
|
+
kind: 'not-reversed',
|
|
165
|
+
reason: 'reversal-refused',
|
|
166
|
+
owedCents: shareCents
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
kind: 'reversed',
|
|
171
|
+
cents: Math.round(Number((_ref5 = reversal == null ? void 0 : reversal.amount) != null ? _ref5 : shareCents)),
|
|
172
|
+
reversalId: String((_ref6 = reversal == null ? void 0 : reversal.id) != null ? _ref6 : ''),
|
|
173
|
+
adopted: false
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
//# sourceMappingURL=stripe-transfer-reversal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/stripe-transfer-reversal.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * A SELLER'S SHARE OF A DESTINATION CHARGE, PULLED BACK (AGL-1794, AGL-3363).\n *\n * Every tenant sale is a destination charge on the platform account, so a\n * lost dispute debits the PLATFORM's balance while the seller keeps the\n * transfer. The AGL-1794 policy: the seller returns their share by a\n * `transfers/{id}/reversals` call; the platform keeps Stripe's dispute fee.\n * This is the Stripe half of that, written once. It started as the commerce\n * webhook's `reverseSellerShare`; the booking deposit's lost dispute uses it\n * too (AGL-3363), so the two cannot drift. Each caller keeps its own record\n * and its own settle marker — this module touches no Firestore.\n *\n * PROPORTIONAL, NEVER MORE: `cause × transfer.amount ÷ charge.amount`,\n * FLOORED, capped at what the transfer has left (`amount − amount_reversed`).\n * No `refund_application_fee`: on a chargeback the platform keeps its\n * commission, a policy switch the AGL-1794 note says not to flip here.\n *\n * IDEMPOTENT at Stripe twice over: a reversal already on the transfer carrying\n * this cause's id in its metadata is ADOPTED rather than repeated (the crash\n * window between the POST and the caller's record), and the POST carries an\n * `Idempotency-Key` derived from the cause, so a racing delivery gets Stripe's\n * stored answer.\n *\n * TRANSIENT failures (a network reject, 429, 5xx) THROW, so the webhook's\n * redelivery is the retry. DEFINITIVE ones answer `not-reversed` with a\n * reason, for the caller to settle and record; no redelivery fixes them.\n *=========================================*/\n\n/** Why nothing was pulled back, when the answer is final. */\nexport type TransferReversalFailure =\n | 'no-charge-on-cause'\n | 'charge-read-refused'\n | 'no-transfer-on-charge'\n | 'transfer-read-refused'\n | 'transfer-fully-reversed'\n | 'reversal-refused'\n\nexport type TransferReversalOutcome =\n /** Pulled back now, or found already pulled back and adopted. */\n | { kind: 'reversed'; cents: number; reversalId: string; adopted: boolean }\n /** Final: settle it and record why. `owedCents` when the share was known. */\n | { kind: 'not-reversed'; reason: TransferReversalFailure; owedCents?: number }\n /** Nothing was attempted and nothing should be settled: retry later. */\n | { kind: 'skipped'; reason: 'no-stripe-key' }\n\nexport interface TransferReversalCause {\n /** Which door: decides the metadata key and the idempotency key. */\n kind: 'dispute' | 'refund'\n /** The dispute's or refund's Stripe id. */\n id: string\n /** Cents going back to the cardholder. */\n amountCents: number\n /** The charge whose transfer is pulled back. */\n chargeId: string\n /** More metadata for the reversal: the caller's record id. */\n metadata?: Record<string, string>\n}\n\n/** Stripe failures a redelivery can actually fix. */\nexport function isTransientStripeStatus(status: number): boolean {\n return status === 429 || status >= 500\n}\n\ntype FetchLike = typeof fetch\n\nasync function stripeGet(\n url: string,\n stripeKey: string,\n fetchImpl: FetchLike,\n): Promise<{ ok: boolean; status: number; body: any }> {\n const response = await fetchImpl(url, {\n headers: { Authorization: `Bearer ${stripeKey}` },\n })\n const body = await response.json().catch(() => null)\n return { ok: response.ok, status: response.status, body }\n}\n\n/** The metadata key a cause's reversal is stamped and found by. */\nexport function transferReversalMetadataKey(kind: TransferReversalCause['kind']): string {\n return kind === 'dispute' ? 'disputeId' : 'refundId'\n}\n\n/**\n * Pull the seller's proportional share of `cause` back from the charge's\n * transfer. See the module header for the rules.\n */\nexport async function reverseDestinationTransfer(\n cause: TransferReversalCause,\n deps: { stripeKey?: string; fetchImpl?: FetchLike } = {},\n): Promise<TransferReversalOutcome> {\n const stripeKey = deps.stripeKey ?? process.env.STRIPE_SECRET_KEY\n if (!stripeKey) return { kind: 'skipped', reason: 'no-stripe-key' }\n const fetchImpl = deps.fetchImpl ?? fetch\n const causeId = String(cause.id ?? '')\n const metadataKey = transferReversalMetadataKey(cause.kind)\n const label = `${cause.kind} ${causeId}`\n\n const chargeId = String(cause.chargeId ?? '')\n if (!chargeId) return { kind: 'not-reversed', reason: 'no-charge-on-cause' }\n const charge = await stripeGet(\n `https://api.stripe.com/v1/charges/${chargeId}`,\n stripeKey,\n fetchImpl,\n )\n if (!charge.ok) {\n if (isTransientStripeStatus(charge.status)) {\n throw new Error(`Stripe charge read failed (${charge.status}) for ${label}`)\n }\n console.error('Stripe refused the charge read', charge.body?.error)\n return { kind: 'not-reversed', reason: 'charge-read-refused' }\n }\n const transferId = String(charge.body?.transfer ?? '')\n const chargeAmountCents = Math.round(Number(charge.body?.amount ?? 0))\n if (!transferId || !(chargeAmountCents > 0)) {\n return { kind: 'not-reversed', reason: 'no-transfer-on-charge' }\n }\n const transfer = await stripeGet(\n `https://api.stripe.com/v1/transfers/${transferId}`,\n stripeKey,\n fetchImpl,\n )\n if (!transfer.ok) {\n if (isTransientStripeStatus(transfer.status)) {\n throw new Error(`Stripe transfer read failed (${transfer.status}) for ${label}`)\n }\n console.error('Stripe refused the transfer read', transfer.body?.error)\n return { kind: 'not-reversed', reason: 'transfer-read-refused' }\n }\n // The crash window's backstop: the POST landed on a previous delivery and\n // the caller's record did not. Adopt it rather than creating a second one.\n const existing = ((transfer.body?.reversals?.data ?? []) as any[]).find(\n (item) => String(item?.metadata?.[metadataKey] ?? '') === causeId,\n )\n if (existing) {\n return {\n kind: 'reversed',\n cents: Math.round(Number(existing.amount ?? 0)),\n reversalId: String(existing.id ?? ''),\n adopted: true,\n }\n }\n const transferCents = Math.round(Number(transfer.body?.amount ?? 0))\n const alreadyReversedCents = Math.round(Number(transfer.body?.amount_reversed ?? 0))\n const remainingCents = Math.max(0, transferCents - alreadyReversedCents)\n const causeCents = Math.round(Number(cause.amountCents ?? 0))\n const shareCents = Math.min(\n Math.floor((causeCents * transferCents) / chargeAmountCents),\n remainingCents,\n )\n if (!(shareCents > 0)) {\n return { kind: 'not-reversed', reason: 'transfer-fully-reversed', owedCents: 0 }\n }\n const params = new URLSearchParams({\n amount: String(shareCents),\n [`metadata[${metadataKey}]`]: causeId,\n })\n for (const [key, value] of Object.entries(cause.metadata ?? {})) {\n params.set(`metadata[${key}]`, value)\n }\n const response = await fetchImpl(\n `https://api.stripe.com/v1/transfers/${transferId}/reversals`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${stripeKey}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'Idempotency-Key': `${cause.kind}-reversal-${causeId}`,\n },\n body: params.toString(),\n },\n )\n const reversal = await response.json().catch(() => null)\n if (!response.ok) {\n if (isTransientStripeStatus(response.status)) {\n throw new Error(`Stripe transfer reversal failed (${response.status}) for ${label}`)\n }\n console.error('Stripe refused the transfer reversal', reversal?.error)\n return { kind: 'not-reversed', reason: 'reversal-refused', owedCents: shareCents }\n }\n return {\n kind: 'reversed',\n cents: Math.round(Number(reversal?.amount ?? shareCents)),\n reversalId: String(reversal?.id ?? ''),\n adopted: false,\n }\n}\n"],"names":["isTransientStripeStatus","status","stripeGet","url","stripeKey","fetchImpl","response","headers","Authorization","body","json","catch","ok","transferReversalMetadataKey","kind","reverseDestinationTransfer","cause","deps","charge","transfer","process","env","STRIPE_SECRET_KEY","reason","fetch","causeId","String","id","metadataKey","label","chargeId","Error","console","error","transferId","chargeAmountCents","Math","round","Number","amount","existing","reversals","data","find","item","metadata","cents","reversalId","adopted","transferCents","alreadyReversedCents","amount_reversed","remainingCents","max","causeCents","amountCents","shareCents","min","floor","owedCents","params","URLSearchParams","key","value","Object","entries","set","method","toString","reversal"],"mappings":"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"}
|