Skip to content

Commit

Permalink
feat(notification): setup adyen notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
ddeliziact committed Feb 19, 2024
1 parent 1160d76 commit 98ee30d
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 35 deletions.
3 changes: 3 additions & 0 deletions processor/src/dtos/adyen-payment.dts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Notification } from '@adyen/api-library/lib/src/typings/notification/notification';

export type PaymentNotificationSchemaDTO = Notification;
40 changes: 14 additions & 26 deletions processor/src/routes/adyen-payment.route.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,24 @@
import { SessionAuthenticationHook } from '@commercetools/connect-payments-sdk';
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
import {
PaymentRequestSchema,
PaymentRequestSchemaDTO,
PaymentResponseSchema,
PaymentResponseSchemaDTO,
} from '../dtos/mock-payment.dto';
import { MockPaymentService } from '../services/mock-payment.service';
import { PaymentNotificationSchemaDTO } from '../dtos/adyen-payment.dts';
import { AdyenPaymentService } from '../services/adyen-payment.service';

const ACK_NOTIFICATION = '[accepted]';

type PaymentRoutesOptions = {
paymentService: MockPaymentService;
paymentService: AdyenPaymentService;
sessionAuthHook: SessionAuthenticationHook;
};

export const paymentRoutes = async (fastify: FastifyInstance, opts: FastifyPluginOptions & PaymentRoutesOptions) => {
fastify.post<{ Body: PaymentRequestSchemaDTO; Reply: PaymentResponseSchemaDTO }>(
'/payments',
{
preHandler: [opts.sessionAuthHook.authenticate()],
schema: {
body: PaymentRequestSchema,
response: {
200: PaymentResponseSchema,
},
},
},
async (request, reply) => {
const resp = await opts.paymentService.createPayment({
data: request.body,
});
/**
* Listen to the notification from Adyen
*/
fastify.post<{ Body: PaymentNotificationSchemaDTO; Reply: any }>('/notifications', {}, async (request, reply) => {
await opts.notificationService.processNotification({
data: request.body,
});

return reply.status(200).send(resp);
},
);
return reply.status(200).send(ACK_NOTIFICATION);
});
};
34 changes: 27 additions & 7 deletions processor/src/services/adyen-payment.service.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
import { CommercetoolsCartService, CommercetoolsPaymentService } from '@commercetools/connect-payments-sdk';
import { NotificationConverter } from './converters/notification.converter';
import { ProcessNotification as ProcessNotificationRequest } from './types/adyen-payment.type';
import { hmacValidator } from '@adyen/api-library';
import { config } from '../config/config';

import { PaymentNotification } from './types/adyen-payment.type';

export type MockPaymentServiceOptions = {
export type AdyenPaymentServiceOptions = {
ctCartService: CommercetoolsCartService;
ctPaymentService: CommercetoolsPaymentService;
notificationConverter: NotificationConverter;
};

export class MockPaymentService {
export class AdyenPaymentService {
private ctCartService: CommercetoolsCartService;
private ctPaymentService: CommercetoolsPaymentService;
private notificationConverter: NotificationConverter;

constructor(opts: MockPaymentServiceOptions) {
constructor(opts: AdyenPaymentServiceOptions) {
this.ctCartService = opts.ctCartService;
this.ctPaymentService = opts.ctPaymentService;
this.notificationConverter = opts.notificationConverter;
}

public async processNotification(opts: ProcessNotificationRequest): Promise<void> {
await this.validateHmac(opts);
const updateData = await this.notificationConverter.convert(opts);
await this.ctPaymentService.updatePayment(updateData);
}

public async processNotification(opts: PaymentNotification): Promise<void> {
// TODO
private async validateHmac(opts: ProcessNotificationRequest): Promise<void> {
if (!opts.data.notificationItems || opts.data.notificationItems.length === 0) {
//TODO: throw an error 401
}

const validator = new hmacValidator();
const item = opts.data.notificationItems[0].NotificationRequestItem;

if (!validator.validateHMAC(item, config.adyenHMACKey)) {
//TODO: throw an error 401
}
}
}
84 changes: 84 additions & 0 deletions processor/src/services/converters/notification.converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Money } from '@commercetools/platform-sdk';

import { NotificationRequestItem } from '@adyen/api-library/lib/src/typings/notification/notificationRequestItem';

import { ProcessNotification } from '../types/adyen-payment.type';
import { TransactionData, UpdatePayment } from '@commercetools/connect-payments-sdk';

export class NotificationConverter {
constructor() {}

public async convert(opts: ProcessNotification): Promise<UpdatePayment> {
const item = opts.data.notificationItems[0].NotificationRequestItem;

return {
id: item.merchantReference,
pspReference: item.pspReference,
transaction: this.populateTransaction(item),
};
}

private populateTransaction(item: NotificationRequestItem): TransactionData {
switch (item.eventCode) {
case NotificationRequestItem.EventCodeEnum.Authorisation:
return {
type: 'Authorization',
state: item.success ? 'Success' : 'Failure',
amount: this.populateAmount(item),
interactionId: item.pspReference,
};
case NotificationRequestItem.EventCodeEnum.Capture:
return {
type: 'Charge',
state: 'Failure',
amount: this.populateAmount(item),
interactionId: item.pspReference,
};
case NotificationRequestItem.EventCodeEnum.CaptureFailed:
return {
type: 'Charge',
state: 'Failure',
amount: this.populateAmount(item),
interactionId: item.pspReference,
};
case NotificationRequestItem.EventCodeEnum.Cancellation:
return {
type: 'CancelAuthorization',
state: item.success ? 'Success' : 'Failure',
amount: this.populateAmount(item),
interactionId: item.pspReference,
};
case NotificationRequestItem.EventCodeEnum.Refund:
return {
type: 'Refund',
state: item.success ? 'Success' : 'Failure',
amount: this.populateAmount(item),
interactionId: item.pspReference,
};
case NotificationRequestItem.EventCodeEnum.RefundFailed:
return {
type: 'Refund',
state: 'Failure',
amount: this.populateAmount(item),
interactionId: item.pspReference,
};
case NotificationRequestItem.EventCodeEnum.Chargeback:
return {
type: 'Chargeback',
state: 'Success',
amount: this.populateAmount(item),
interactionId: item.pspReference,
};
default:
//TODO: throw unsupported notification error
throw new Error('Unsupported notification');
}
}

private populateAmount(item: NotificationRequestItem): Money {
return {
centAmount: item.amount.value as number,
currencyCode: item.amount.currency as string,
};
}
}
6 changes: 4 additions & 2 deletions processor/src/services/types/adyen-payment.type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Notification } from '@adyen/api-library/lib/src/typings/notification/notification';
import { PaymentNotificationSchemaDTO } from '../../dtos/adyen-payment.dts';

export type PaymentNotification = Notification;
export type ProcessNotification = {
data: PaymentNotificationSchemaDTO;
};

0 comments on commit 98ee30d

Please sign in to comment.