Skip to content

Commit f4a2faf

Browse files
authored
Merge pull request #41 from lambda-curry/fix/refunds-2
[MI-1340] fix: refunds and minor refactors for type safety
2 parents b782908 + 92bb346 commit f4a2faf

2 files changed

Lines changed: 36 additions & 27 deletions

File tree

plugins/braintree-payment/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lambdacurry/medusa-payment-braintree",
3-
"version": "0.1.5",
3+
"version": "0.1.7",
44
"description": "Braintree plugin for Medusa",
55
"author": "Lambda Curry (https://lambdacurry.dev)",
66
"license": "MIT",

plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,7 @@ const getBraintreeErrorMessage = (response: BraintreeErrorResponseLike): string
148148
const settlementResponseText = response.transaction?.processorSettlementResponseText?.trim();
149149
if (settlementResponseText) {
150150
const settlementResponseCode = response.transaction?.processorSettlementResponseCode?.trim();
151-
return settlementResponseCode
152-
? `${settlementResponseText} (${settlementResponseCode})`
153-
: settlementResponseText;
151+
return settlementResponseCode ? `${settlementResponseText} (${settlementResponseCode})` : settlementResponseText;
154152
}
155153

156154
const validationErrors = getBraintreeValidationErrors(response.errors).map(formatBraintreeValidationError);
@@ -178,9 +176,7 @@ export function throwOnBraintreeFailure(
178176
response.transaction?.gatewayRejectionReason ||
179177
response.transaction?.processorResponseText ||
180178
response.transaction?.processorSettlementResponseText;
181-
const type = hasProcessorSignal
182-
? MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR
183-
: MedusaError.Types.INVALID_DATA;
179+
const type = hasProcessorSignal ? MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR : MedusaError.Types.INVALID_DATA;
184180

185181
log(`${operation} failed`, new Error(message), {
186182
...context,
@@ -228,7 +224,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
228224
this.options_ = options;
229225
this.logger = container[ContainerRegistrationKeys.LOGGER];
230226
this.cache = container[Modules.CACHE];
231-
this.init();
227+
this.gateway = this.init();
232228
}
233229

234230
async saveClientTokenToCache(clientToken: string, customerId: string, expiresOnEpochSeconds: number): Promise<void> {
@@ -263,9 +259,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
263259
}
264260

265261
private isTestForceSettledEnabled(): boolean {
266-
return (
267-
process.env.TEST_FORCE_SETTLED === 'true' && this.options_.environment.toLowerCase() === 'sandbox'
268-
);
262+
return process.env.TEST_FORCE_SETTLED === 'true' && this.options_.environment.toLowerCase() === 'sandbox';
269263
}
270264

271265
async getValidClientToken(
@@ -314,7 +308,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
314308
return result.data as BraintreePaymentSessionData;
315309
}
316310

317-
init(): void {
311+
init(): Braintree.BraintreeGateway {
318312
const envKey = (this.options_.environment || 'sandbox').toLowerCase();
319313
const envMap: Record<string, Braintree.Environment> = {
320314
qa: Braintree.Environment.Qa,
@@ -334,13 +328,17 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
334328
});
335329

336330
this.logDebug(`Gateway initialized (environment: ${envKey})`);
331+
return this.gateway;
337332
}
338333

339334
static validateOptions(options: BraintreeOptions): void {
340335
const requiredFields = ['merchantId', 'publicKey', 'privateKey', 'webhookSecret', 'environment'];
341336

342337
for (const field of requiredFields) {
343-
if (!isDefined(options[field]) || typeof options[field] !== 'string') {
338+
if (
339+
!isDefined(options[field as keyof BraintreeOptions]) ||
340+
typeof options[field as keyof BraintreeOptions] !== 'string'
341+
) {
344342
throw new MedusaError(
345343
MedusaError.Types.INVALID_ARGUMENT,
346344
`Required option "${field}" is missing or invalid in Braintree plugin`,
@@ -364,7 +362,10 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
364362

365363
const booleanFields = ['enable3DSecure', 'savePaymentMethod', 'autoCapture', 'allowRefundOnRefunded', 'logging'];
366364
for (const field of booleanFields) {
367-
if (isDefined(options[field]) && typeof options[field] !== 'boolean') {
365+
if (
366+
isDefined(options[field as keyof BraintreeOptions]) &&
367+
typeof options[field as keyof BraintreeOptions] !== 'boolean'
368+
) {
368369
throw new MedusaError(
369370
MedusaError.Types.INVALID_ARGUMENT,
370371
`Option "${field}" must be a boolean in Braintree plugin`,
@@ -815,9 +816,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
815816

816817
if (process.env.TEST_FORCE_SETTLED === 'true') {
817818
if (!this.isTestForceSettledEnabled()) {
818-
this.logger.warn(
819-
'[Braintree refund] TEST_FORCE_SETTLED ignored — only supported when environment is sandbox',
820-
);
819+
this.logger.warn('[Braintree refund] TEST_FORCE_SETTLED ignored — only supported when environment is sandbox');
821820
} else {
822821
shouldVoid = false;
823822
await this.gateway.testing.settle(transaction.id);
@@ -839,16 +838,18 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
839838
}
840839

841840
const voidedTransaction = voidResponse?.transaction ?? (await this.retrieveTransaction(transaction.id));
842-
841+
const braintreeRefund = {
842+
success: true,
843+
transactionId: voidedTransaction?.id,
844+
type: 'void',
845+
};
846+
const priorRefunds = Array.isArray(input.data?.braintreeRefunds) ? input.data.braintreeRefunds : [];
847+
// we need to preserve the original transaction data and store the refund history separately
843848
const refundResult: RefundPaymentOutput = {
844849
data: {
845850
...input.data,
846-
transaction: voidedTransaction,
847-
braintreeRefund: {
848-
success: true,
849-
transactionId: voidedTransaction?.id,
850-
type: 'void',
851-
},
851+
transaction: transaction,
852+
braintreeRefunds: [...priorRefunds, braintreeRefund],
852853
},
853854
};
854855

@@ -861,7 +862,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
861862
);
862863
throw new MedusaError(
863864
MedusaError.Types.NOT_FOUND,
864-
`Braintree transaction with ID ${transaction.id} cannot be refunded`,
865+
`Braintree transaction with ID ${transaction.id} cannot be refunded because it's in status ${transaction.status}`,
865866
);
866867
}
867868

@@ -885,11 +886,19 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
885886

886887
const refundTransaction = refundResponse.transaction ?? (await this.retrieveTransaction(transaction.id));
887888

889+
const braintreeRefund = {
890+
success: true,
891+
transactionId: refundTransaction?.id,
892+
type: 'refund',
893+
};
894+
const priorRefunds = Array.isArray(input.data?.braintreeRefunds) ? input.data.braintreeRefunds : [];
895+
// we need to preserve the original transaction data and store the refund history separately. This is to support multiple partial refunds
896+
888897
const refundResult: RefundPaymentOutput = {
889898
data: {
890899
...input.data,
891-
transaction: refundTransaction,
892-
braintreeRefund: refundTransaction,
900+
transaction: transaction,
901+
braintreeRefunds: [...priorRefunds, braintreeRefund],
893902
},
894903
};
895904

0 commit comments

Comments
 (0)