Skip to content

Commit

Permalink
Merge pull request #294 from commercetools/dasanorct/SCC-2774_merchan…
Browse files Browse the repository at this point in the history
…t_reference

feat(payments): accept a merchant reference in payment intents API
  • Loading branch information
dasanorct authored Jan 17, 2025
2 parents a0814e8 + de7ce29 commit 530840a
Show file tree
Hide file tree
Showing 16 changed files with 238 additions and 43 deletions.
34 changes: 17 additions & 17 deletions processor/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion processor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"dependencies": {
"@adyen/api-library": "22.1.0",
"@commercetools-backend/loggers": "22.38.1",
"@commercetools/connect-payments-sdk": "0.15.0",
"@commercetools/connect-payments-sdk": "0.16.0",
"@fastify/autoload": "6.0.3",
"@fastify/cors": "10.0.2",
"@fastify/formbody": "8.0.2",
Expand Down
3 changes: 3 additions & 0 deletions processor/src/dtos/operations/payment-intents.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const ActionCapturePaymentSchema = Type.Composite([
}),
Type.Object({
amount: AmountSchema,
merchantReference: Type.Optional(Type.String()),
}),
]);

Expand All @@ -20,12 +21,14 @@ export const ActionRefundPaymentSchema = Type.Composite([
}),
Type.Object({
amount: AmountSchema,
merchantReference: Type.Optional(Type.String()),
}),
]);

export const ActionCancelPaymentSchema = Type.Composite([
Type.Object({
action: Type.Literal('cancelPayment'),
merchantReference: Type.Optional(Type.String()),
}),
]);

Expand Down
14 changes: 10 additions & 4 deletions processor/src/services/abstract-payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,12 @@ export abstract class AbstractPaymentService {
action: request.action,
});

const res = await this.processPaymentModification(updatedPayment, transactionType, requestAmount);
const res = await this.processPaymentModification(
updatedPayment,
transactionType,
requestAmount,
request.merchantReference,
);

updatedPayment = await this.ctPaymentService.updatePayment({
id: ctPayment.id,
Expand Down Expand Up @@ -151,16 +156,17 @@ export abstract class AbstractPaymentService {
payment: Payment,
transactionType: string,
requestAmount: AmountSchemaDTO,
merchantReference?: string,
) {
switch (transactionType) {
case 'CancelAuthorization': {
return await this.cancelPayment({ payment });
return await this.cancelPayment({ payment, merchantReference });
}
case 'Charge': {
return await this.capturePayment({ amount: requestAmount, payment });
return await this.capturePayment({ amount: requestAmount, payment, merchantReference });
}
case 'Refund': {
return await this.refundPayment({ amount: requestAmount, payment });
return await this.refundPayment({ amount: requestAmount, payment, merchantReference });
}
default: {
throw new ErrorInvalidOperation(`Operation ${transactionType} not supported.`);
Expand Down
34 changes: 33 additions & 1 deletion processor/src/services/adyen-payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { RefundPaymentConverter } from './converters/refund-payment.converter';
import { log } from '../libs/logger';
import { ApplePayPaymentSessionError, UnsupportedNotificationError } from '../errors/adyen-api.error';
import { fetch as undiciFetch, Agent, Dispatcher } from 'undici';
import { NotificationUpdatePayment } from './types/service.type';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const packageJSON = require('../../package.json');

Expand Down Expand Up @@ -380,10 +381,11 @@ export class AdyenPaymentService extends AbstractPaymentService {
log.info('Processing notification', { notification: JSON.stringify(opts.data) });
try {
const updateData = this.notificationConverter.convert(opts);
const payment = await this.getPaymentFromNotification(updateData);

for (const tx of updateData.transactions) {
const updatedPayment = await this.ctPaymentService.updatePayment({
id: updateData.id,
id: payment.id,
pspReference: updateData.pspReference,
transaction: tx,
});
Expand Down Expand Up @@ -564,4 +566,34 @@ export class AdyenPaymentService extends AbstractPaymentService {
}
return redirectUrl.toString();
}

/**
* Retrieves a payment instance from the notification data
* First, it tries to find the payment by the interfaceId (PSP reference)
* As a fallback, it tries to find the payment by the merchantReference which unless the merchant overrides it, it's the payment ID
* @param data
* @returns A payment instance
*/
private async getPaymentFromNotification(data: NotificationUpdatePayment): Promise<Payment> {
const interfaceId = data.pspReference;
let payment!: Payment;

if (interfaceId) {
const results = await this.ctPaymentService.findPaymentsByInterfaceId({
interfaceId,
});

if (results.length > 0) {
payment = results[0];
}
}

if (!payment) {
return await this.ctPaymentService.getPayment({
id: data.merchantReference,
});
}

return payment;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class CancelPaymentConverter {
public convertRequest(opts: CancelPaymentRequest): PaymentCancelRequest {
return {
merchantAccount: config.adyenMerchantAccount,
reference: opts.payment.id,
reference: opts.merchantReference || opts.payment.id,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class CapturePaymentConverter {

return {
merchantAccount: config.adyenMerchantAccount,
reference: opts.payment.id,
reference: opts.merchantReference || opts.payment.id,
amount: {
currency: opts.amount.currencyCode,
value: CurrencyConverters.convertWithMapping({
Expand All @@ -47,7 +47,7 @@ export class CapturePaymentConverter {
currencyCode: opts.amount.currencyCode,
}),
},
lineItems: adyenLineItems,
...(adyenLineItems && { lineItems: adyenLineItems }),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class NotificationConverter {
const item = opts.data.notificationItems[0].NotificationRequestItem;

return {
id: item.merchantReference,
merchantReference: item.merchantReference,
pspReference: item.originalReference || item.pspReference,
paymentMethod: item.paymentMethod,
transactions: this.populateTransactions(item),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class RefundPaymentConverter {
public convertRequest(opts: RefundPaymentRequest): PaymentRefundRequest {
return {
merchantAccount: config.adyenMerchantAccount,
reference: opts.payment.id,
reference: opts.merchantReference || opts.payment.id,
amount: {
currency: opts.amount.currencyCode,
value: CurrencyConverters.convertWithMapping({
Expand Down
3 changes: 3 additions & 0 deletions processor/src/services/types/operation.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ import { Payment } from '@commercetools/connect-payments-sdk';
export type CapturePaymentRequest = {
amount: AmountSchemaDTO;
payment: Payment;
merchantReference?: string;
};

export type CancelPaymentRequest = {
payment: Payment;
merchantReference?: string;
};

export type RefundPaymentRequest = {
amount: AmountSchemaDTO;
payment: Payment;
merchantReference?: string;
};

export type PaymentProviderModificationResponse = {
Expand Down
2 changes: 1 addition & 1 deletion processor/src/services/types/service.type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TransactionData } from '@commercetools/connect-payments-sdk';
export type NotificationUpdatePayment = {
id: string;
merchantReference: string;
pspReference?: string;
transactions: TransactionData[];
paymentMethod?: string;
Expand Down
5 changes: 4 additions & 1 deletion processor/test/services/adyen-payment.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,14 +711,17 @@ describe('adyen-payment.service', () => {
],
};

jest
.spyOn(DefaultPaymentService.prototype, 'findPaymentsByInterfaceId')
.mockResolvedValue([mockUpdatePaymentResult]);
jest.spyOn(DefaultPaymentService.prototype, 'updatePayment').mockResolvedValue(mockUpdatePaymentResult);

// When
await paymentService.processNotification({ data: notification });

// Then
expect(DefaultPaymentService.prototype.updatePayment).toHaveBeenCalledWith({
id: merchantReference,
id: '123456',
pspReference,
transaction: {
amount: {
Expand Down
43 changes: 43 additions & 0 deletions processor/test/services/converters/cancel.converter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, test, expect } from '@jest/globals';
import { mockGetPaymentResult } from '../../utils/mock-payment-data';
import { config } from '../../../src/config/config';
import { CancelPaymentConverter } from '../../../src/services/converters/cancel-payment.converter';

describe('cancel.converter', () => {
const converter = new CancelPaymentConverter();

test('convert with checkout merchant reference', async () => {
// Arrange
const payment = mockGetPaymentResult;
const data = {
payment,
};

// Act
const result = converter.convertRequest(data);

// Assert
expect(result).toEqual({
merchantAccount: config.adyenMerchantAccount,
reference: mockGetPaymentResult.id,
});
});

test('convert with custom merchant reference', async () => {
// Arrange
const payment = mockGetPaymentResult;
const data = {
payment,
merchantReference: 'merchantReference',
};

// Act
const result = converter.convertRequest(data);

// Assert
expect(result).toEqual({
merchantAccount: config.adyenMerchantAccount,
reference: 'merchantReference',
});
});
});
Loading

0 comments on commit 530840a

Please sign in to comment.