Skip to content

Commit

Permalink
Merge pull request #22 from commercetools/dasanorct/SCC-2148_adyen_apis
Browse files Browse the repository at this point in the history
feat(adyen-template): added a first version of adyen APIs
  • Loading branch information
dasanorct authored Feb 27, 2024
2 parents 1c8be22 + a057632 commit ff194f3
Show file tree
Hide file tree
Showing 21 changed files with 727 additions and 87 deletions.
12 changes: 9 additions & 3 deletions processor/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ CTP_CLIENT_SECRET=[composable-commerce-client-secret]
CTP_PROJECT_KEY=[composable-commerce-project-key]

# Merchant website
SELLER_RETURN_URL=[Merchant-website-return-url]
SELLER_SEND_NOTIFICATION_ENABLED=[true/false]
SELLER_NOTIFICATION_URL=[Merchant-website-return-url]
MERCHANT_RETURN_URL=[Merchant-website-return-url]

# Adyen credentials
ADYEN_ENVIRONMENT=[Adyen-environment]
ADYEN_API_KEY=[Adyen-api-key]
ADYEN_CLIENT_KEY=[Adyen-client-key]
ADYEN_LIVE_URL_PREFIX=
ADYEN_NOTIFICATION_HMAC_KEY=[Adyen-hmac-key]
ADYEN_MERCHANT_ACCOUNT=[Adyen-merchant-account]
8 changes: 4 additions & 4 deletions processor/package-lock.json

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

4 changes: 2 additions & 2 deletions processor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"dependencies": {
"@adyen/api-library": "16.0.1",
"@commercetools-backend/loggers": "22.17.2",
"@commercetools/connect-payments-sdk": "0.1.0",
"@commercetools/connect-payments-sdk": "0.2.0",
"@commercetools/platform-sdk": "7.3.0",
"@commercetools/sdk-client-v2": "2.3.0",
"@fastify/autoload": "5.8.0",
Expand Down Expand Up @@ -56,4 +56,4 @@
"ts-node": "10.9.2",
"typescript": "5.3.3"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Client, CheckoutAPI } from '@adyen/api-library';
import { config } from '../../config/config';
import { config } from '../config/config';

export const AdyenAPI = (): CheckoutAPI => {
export const AdyenApi = (): CheckoutAPI => {
const apiClient = new Client({
apiKey: config.adyenApiKey,
environment: config.adyenEnvironment.toUpperCase() as Environment,
Expand Down
10 changes: 1 addition & 9 deletions processor/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,13 @@ export const config = {
// Required by logger
loggerLevel: process.env.LOGGER_LEVEL || 'info',

// Update with specific payment providers config
mockClientKey: process.env.MOCK_CLIENT_KEY,
mockEnvironment: process.env.MOCK_ENVIRONMENT,

// Payment Providers config
adyenEnvironment: process.env.ADYEN_ENVIRONMENT || '',
adyenClientKey: process.env.ADYEN_CLIENT_KEY || '',
adyenApiKey: process.env.ADYEN_API_KEY || '',
adyenHMACKey: process.env.ADYEN_NOTIFICATION_HMAC_KEY || '',
adyenLiveUrlPrefix: process.env.ADYEN_LIVE_URL_PREFIX || '',
adyenMerchantAccount: process.env.ADYEN_MERCHANT_ACCOUNT || '',
adyenReturnUrl: process.env.ADYEN_RETURN_URL || '',

// TODO review these configurations
// supportedUIElements: convertStringCommaSeparatedValuesToArray(process.env.SUPPORTED_UI_ELEMENTS),
// enableStoreDetails: process.env.ENABLE_STORE_DETAILS === 'true' ? true : false,
// sellerReturnUrl: process.env.SELLER_RETURN_URL || ''
merchantReturnUrl: process.env.MERCHANT_RETURN_URL || '',
};
65 changes: 65 additions & 0 deletions processor/src/dtos/adyen-payment.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { CreateCheckoutSessionRequest } from '@adyen/api-library/lib/src/typings/checkout/createCheckoutSessionRequest';
import { CreateCheckoutSessionResponse } from '@adyen/api-library/lib/src/typings/checkout/createCheckoutSessionResponse';
import { PaymentDetailsRequest } from '@adyen/api-library/lib/src/typings/checkout/paymentDetailsRequest';
import { PaymentDetailsResponse } from '@adyen/api-library/lib/src/typings/checkout/paymentDetailsResponse';
import { PaymentMethodsRequest } from '@adyen/api-library/lib/src/typings/checkout/paymentMethodsRequest';
import { PaymentMethodsResponse } from '@adyen/api-library/lib/src/typings/checkout/paymentMethodsResponse';
import { PaymentRequest } from '@adyen/api-library/lib/src/typings/checkout/paymentRequest';
import { PaymentResponse } from '@adyen/api-library/lib/src/typings/checkout/paymentResponse';
import { Notification } from '@adyen/api-library/lib/src/typings/notification/notification';

export type PaymentMethodsRequestDTO = Omit<PaymentMethodsRequest, 'amount' | 'merchantAccount' | 'countryCode'>;
export type PaymentMethodsResponseDTO = PaymentMethodsResponse;

export type CreateSessionRequestDTO = Omit<
CreateCheckoutSessionRequest,
| 'amount'
| 'merchantAccount'
| 'countryCode'
| 'returnUrl'
| 'reference'
| 'storePaymentMethod'
| 'shopperReference'
| 'recurringProcessingModel'
| 'storePaymentMethodMode'
>;

export type CreateSessionResponseDTO = {
sessionData: CreateCheckoutSessionResponse;
paymentReference: string;
};

export type CreatePaymentRequestDTO = Omit<
PaymentRequest,
| 'amount'
| 'additionalAmount'
| 'merchantAccount'
| 'countryCode'
| 'returnUrl'
| 'lineItems'
| 'reference'
| 'shopperReference'
| 'recurringProcessingModel'
> & {
paymentReference?: string;
};

export type CreatePaymentResponseDTO = Pick<
PaymentResponse,
'action' | 'resultCode' | 'threeDS2ResponseData' | 'threeDS2Result' | 'threeDSPaymentData'
> & {
paymentReference: string;
};

export type ConfirmPaymentRequestDTO = PaymentDetailsRequest & {
paymentReference: string;
};

export type ConfirmPaymentResponseDTO = Pick<
PaymentDetailsResponse,
'resultCode' | 'threeDS2ResponseData' | 'threeDS2Result' | 'threeDSPaymentData'
> & {
paymentReference: string;
};

export type NotificationRequestDTO = Notification;
3 changes: 0 additions & 3 deletions processor/src/dtos/adyen-payment.dts.ts

This file was deleted.

1 change: 0 additions & 1 deletion processor/src/dtos/operations/config.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Static, Type } from '@sinclair/typebox';
export const ConfigResponseSchema = Type.Object({
clientKey: Type.String(),
environment: Type.String(),
returnUrl: Type.String(),
});

export type ConfigResponseSchemaDTO = Static<typeof ConfigResponseSchema>;
10 changes: 10 additions & 0 deletions processor/src/libs/fastify/context/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ export const getAllowedPaymentMethodsFromContext = (): string[] => {
return authentication?.getPrincipal().allowedPaymentMethods;
};

export const getPaymentInterfaceFromContext = (): string | undefined => {
const authentication = getRequestContext().authentication as SessionAuthentication;
return authentication?.getPrincipal().paymentInterface;
};

export const getProcessorUrlFromContext = (): string => {
const authentication = getRequestContext().authentication as SessionAuthentication;
return authentication?.getPrincipal().processorUrl;
};

export const requestContextPlugin = fp(async (fastify: FastifyInstance) => {
// Enance the request object with a correlationId property
fastify.decorateRequest('correlationId', '');
Expand Down
25 changes: 25 additions & 0 deletions processor/src/libs/fastify/hooks/hmac-auth.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import HmacValidator from '@adyen/api-library/lib/src/utils/hmacValidator';
import { config } from '../../../config/config';
import { FastifyRequest } from 'fastify';
import { ErrorAuthErrorResponse } from '@commercetools/connect-payments-sdk';
import { NotificationRequestDTO } from '../../../dtos/adyen-payment.dto';

export class HmacAuthHook {
constructor() {}

public authenticate() {
return async (request: FastifyRequest) => {
const data = request.body as NotificationRequestDTO;
if (!data.notificationItems || data.notificationItems.length === 0) {
throw new ErrorAuthErrorResponse('Unexpected payload');
}

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

if (!validator.validateHMAC(item, config.adyenHMACKey)) {
throw new ErrorAuthErrorResponse('HMAC is not valid');
}
};
}
}
112 changes: 101 additions & 11 deletions processor/src/routes/adyen-payment.route.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,114 @@
import { SessionAuthenticationHook } from '@commercetools/connect-payments-sdk';
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
import { PaymentNotificationSchemaDTO } from '../dtos/adyen-payment.dts';
import {
ConfirmPaymentRequestDTO,
ConfirmPaymentResponseDTO,
CreatePaymentRequestDTO,
CreatePaymentResponseDTO,
CreateSessionRequestDTO,
CreateSessionResponseDTO,
NotificationRequestDTO,
PaymentMethodsRequestDTO,
PaymentMethodsResponseDTO,
} from '../dtos/adyen-payment.dto';
import { AdyenPaymentService } from '../services/adyen-payment.service';

const ACK_NOTIFICATION = '[accepted]';
import { config } from '../config/config';
import { HmacAuthHook } from '../libs/fastify/hooks/hmac-auth.hook';

type PaymentRoutesOptions = {
paymentService: AdyenPaymentService;
sessionAuthHook: SessionAuthenticationHook;
hmacAuthHook: HmacAuthHook;
};

export const paymentRoutes = async (fastify: FastifyInstance, opts: FastifyPluginOptions & PaymentRoutesOptions) => {
/**
* Listen to the notification from Adyen
*/
fastify.post<{ Body: PaymentNotificationSchemaDTO; Reply: any }>('/notifications', {}, async (request, reply) => {
await opts.notificationService.processNotification({
data: request.body,
export const adyenPaymentRoutes = async (
fastify: FastifyInstance,
opts: FastifyPluginOptions & PaymentRoutesOptions,
) => {
fastify.post<{ Body: PaymentMethodsRequestDTO; Reply: PaymentMethodsResponseDTO }>(
'/payment-methods',
{
preHandler: [opts.sessionAuthHook.authenticate()],
},
async (request, reply) => {
const resp = await opts.paymentService.getPaymentMethods({
data: request.body,
});

return reply.status(200).send(resp);
},
);

fastify.post<{ Body: CreateSessionRequestDTO; Reply: CreateSessionResponseDTO }>(
'/sessions',
{
preHandler: [opts.sessionAuthHook.authenticate()],
},
async (request, reply) => {
const resp = await opts.paymentService.createSession({
data: request.body,
});

return reply.status(200).send(resp);
},
);

fastify.post<{ Body: CreatePaymentRequestDTO; Reply: CreatePaymentResponseDTO }>(
'/payments',
{
preHandler: [opts.sessionAuthHook.authenticate()],
},
async (request, reply) => {
const resp = await opts.paymentService.createPayment({
data: request.body,
});

return reply.status(200).send(resp);
},
);

fastify.get<{ Reply: ConfirmPaymentResponseDTO }>('/payments/details', {}, async (request, reply) => {
const queryParams = request.query as any;
const res = await opts.paymentService.confirmPayment({
data: {
details: {
redirectResult: queryParams.redirectResult as string,
},
paymentReference: queryParams.paymentReference as string,
},
});

return reply.status(200).send(ACK_NOTIFICATION);
return reply.redirect(buildRedirectUrl(res.paymentReference));
});

fastify.post<{ Body: ConfirmPaymentRequestDTO; Reply: ConfirmPaymentResponseDTO }>(
'/payments/details',
{},
async (request, reply) => {
const res = await opts.paymentService.confirmPayment({
data: request.body,
});
return reply.status(200).send(res);
},
);

fastify.post<{ Body: NotificationRequestDTO }>(
'/notifications',
{
preHandler: [opts.hmacAuthHook.authenticate()],
},
async (request, reply) => {
await opts.notificationService.processNotification({
data: request.body,
});

return reply.status(200).send('[accepted]');
},
);
};

const buildRedirectUrl = (paymentReference: string) => {
const redirectUrl = new URL(config.merchantReturnUrl);
redirectUrl.searchParams.append('paymentReference', paymentReference);
return redirectUrl.toString();
};
12 changes: 7 additions & 5 deletions processor/src/server/plugins/adyen-payment.plugin.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { FastifyInstance } from 'fastify';
import { paymentSDK } from '../../payment-sdk';
import { paymentRoutes } from '../../routes/mock-payment.route';
import { MockPaymentService } from '../../services/mock-payment.service';
import { adyenPaymentRoutes } from '../../routes/adyen-payment.route';
import { AdyenPaymentService } from '../../services/adyen-payment.service';
import { HmacAuthHook } from '../../libs/fastify/hooks/hmac-auth.hook';

export default async function (server: FastifyInstance) {
const mockPaymentService = new MockPaymentService({
const paymentService = new AdyenPaymentService({
ctCartService: paymentSDK.ctCartService,
ctPaymentService: paymentSDK.ctPaymentService,
});

await server.register(paymentRoutes, {
paymentService: mockPaymentService,
await server.register(adyenPaymentRoutes, {
paymentService,
sessionAuthHook: paymentSDK.sessionAuthHookFn,
hmacAuthHook: new HmacAuthHook(),
});
}
Loading

0 comments on commit ff194f3

Please sign in to comment.