Skip to content

Commit

Permalink
Merge pull request #49 from commercetools/dasanorct/support_session_m…
Browse files Browse the repository at this point in the history
…erchant_return_url

feat(payments): added support for retrieving the merchant return url from the session
  • Loading branch information
dasanorct authored Mar 12, 2024
2 parents e0cf38a + 59d290e commit e10d300
Show file tree
Hide file tree
Showing 11 changed files with 2,681 additions and 3,013 deletions.
5,544 changes: 2,577 additions & 2,967 deletions processor/package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions processor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "payment-integration-adyen",
"version": "1.0.1",
"version": "1.0.2",
"description": "Payment integration with Adyen",
"main": "dist/server.js",
"scripts": {
Expand All @@ -20,7 +20,7 @@
"dependencies": {
"@adyen/api-library": "16.1.0",
"@commercetools-backend/loggers": "22.20.0",
"@commercetools/connect-payments-sdk": "0.2.0",
"@commercetools/connect-payments-sdk": "0.3.0",
"@commercetools/platform-sdk": "7.4.0",
"@commercetools/sdk-client-v2": "2.3.0",
"@fastify/autoload": "5.8.0",
Expand Down
2 changes: 2 additions & 0 deletions processor/src/dtos/adyen-payment.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export type CreatePaymentResponseDTO = Pick<
'action' | 'resultCode' | 'threeDS2ResponseData' | 'threeDS2Result' | 'threeDSPaymentData'
> & {
paymentReference: string;
merchantReturnUrl?: string;
};

export type ConfirmPaymentRequestDTO = PaymentDetailsRequest & {
Expand All @@ -60,6 +61,7 @@ export type ConfirmPaymentResponseDTO = Pick<
'resultCode' | 'threeDS2ResponseData' | 'threeDS2Result' | 'threeDSPaymentData'
> & {
paymentReference: string;
merchantReturnUrl: string;
};

export type NotificationRequestDTO = Notification;
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 @@ -32,6 +32,11 @@ export const updateRequestContext = (ctx: Partial<ContextData>) => {
});
};

export const getCtSessionIdFromContext = (): string => {
const authentication = getRequestContext().authentication as SessionAuthentication;
return authentication?.getCredentials();
};

export const getCartIdFromContext = (): string => {
const authentication = getRequestContext().authentication as SessionAuthentication;
return authentication?.getPrincipal().cartId;
Expand All @@ -52,6 +57,11 @@ export const getProcessorUrlFromContext = (): string => {
return authentication?.getPrincipal().processorUrl;
};

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

export const requestContextPlugin = fp(async (fastify: FastifyInstance) => {
// Enance the request object with a correlationId property
fastify.decorateRequest('correlationId', '');
Expand Down
53 changes: 29 additions & 24 deletions processor/src/routes/adyen-payment.route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { SessionAuthenticationHook } from '@commercetools/connect-payments-sdk';
import {
SessionHeaderAuthenticationHook,
SessionQueryParamAuthenticationHook,
} from '@commercetools/connect-payments-sdk';
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
import {
ConfirmPaymentRequestDTO,
Expand All @@ -12,12 +15,12 @@ import {
PaymentMethodsResponseDTO,
} from '../dtos/adyen-payment.dto';
import { AdyenPaymentService } from '../services/adyen-payment.service';
import { config } from '../config/config';
import { HmacAuthHook } from '../libs/fastify/hooks/hmac-auth.hook';

type PaymentRoutesOptions = {
paymentService: AdyenPaymentService;
sessionAuthHook: SessionAuthenticationHook;
sessionHeaderAuthHook: SessionHeaderAuthenticationHook;
sessionQueryParamAuthHook: SessionQueryParamAuthenticationHook;
hmacAuthHook: HmacAuthHook;
};

Expand All @@ -28,7 +31,7 @@ export const adyenPaymentRoutes = async (
fastify.post<{ Body: PaymentMethodsRequestDTO; Reply: PaymentMethodsResponseDTO }>(
'/payment-methods',
{
preHandler: [opts.sessionAuthHook.authenticate()],
preHandler: [opts.sessionHeaderAuthHook.authenticate()],
},
async (request, reply) => {
const resp = await opts.paymentService.getPaymentMethods({
Expand All @@ -42,7 +45,7 @@ export const adyenPaymentRoutes = async (
fastify.post<{ Body: CreateSessionRequestDTO; Reply: CreateSessionResponseDTO }>(
'/sessions',
{
preHandler: [opts.sessionAuthHook.authenticate()],
preHandler: [opts.sessionHeaderAuthHook.authenticate()],
},
async (request, reply) => {
const resp = await opts.paymentService.createSession({
Expand All @@ -56,7 +59,7 @@ export const adyenPaymentRoutes = async (
fastify.post<{ Body: CreatePaymentRequestDTO; Reply: CreatePaymentResponseDTO }>(
'/payments',
{
preHandler: [opts.sessionAuthHook.authenticate()],
preHandler: [opts.sessionHeaderAuthHook.authenticate()],
},
async (request, reply) => {
const resp = await opts.paymentService.createPayment({
Expand All @@ -75,23 +78,31 @@ export const adyenPaymentRoutes = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};
}>('/payments/details', {}, async (request, reply) => {
const queryParams = request.query as any;
const res = await opts.paymentService.confirmPayment({
data: {
details: {
redirectResult: queryParams.redirectResult as string,
}>(
'/payments/details',
{
preHandler: [opts.sessionQueryParamAuthHook.authenticate()],
},
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,
},
paymentReference: queryParams.paymentReference as string,
},
});
});

return reply.redirect(buildRedirectUrl(res.paymentReference));
});
return reply.redirect(res.merchantReturnUrl);
},
);

fastify.post<{ Body: ConfirmPaymentRequestDTO; Reply: ConfirmPaymentResponseDTO }>(
'/payments/details',
{},
{
preHandler: [opts.sessionHeaderAuthHook.authenticate()],
},
async (request, reply) => {
const res = await opts.paymentService.confirmPayment({
data: request.body,
Expand All @@ -114,9 +125,3 @@ export const adyenPaymentRoutes = async (
},
);
};

const buildRedirectUrl = (paymentReference: string) => {
const redirectUrl = new URL(config.merchantReturnUrl);
redirectUrl.searchParams.append('paymentReference', paymentReference);
return redirectUrl.toString();
};
6 changes: 3 additions & 3 deletions processor/src/routes/operation.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
AuthorityAuthorizationHook,
JWTAuthenticationHook,
Oauth2AuthenticationHook,
SessionAuthenticationHook,
SessionHeaderAuthenticationHook,
} from '@commercetools/connect-payments-sdk';
import { Type } from '@sinclair/typebox';
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
Expand All @@ -18,7 +18,7 @@ import { StatusResponseSchema, StatusResponseSchemaDTO } from '../dtos/operation
import { AbstractPaymentService } from '../services/abstract-payment.service';

type OperationRouteOptions = {
sessionAuthHook: SessionAuthenticationHook;
sessionHeaderAuthHook: SessionHeaderAuthenticationHook;
oauth2AuthHook: Oauth2AuthenticationHook;
jwtAuthHook: JWTAuthenticationHook;
authorizationHook: AuthorityAuthorizationHook;
Expand All @@ -29,7 +29,7 @@ export const operationsRoute = async (fastify: FastifyInstance, opts: FastifyPlu
fastify.get<{ Reply: ConfigResponseSchemaDTO }>(
'/config',
{
preHandler: [opts.sessionAuthHook.authenticate()],
preHandler: [opts.sessionHeaderAuthHook.authenticate()],
schema: {
response: {
200: ConfigResponseSchema,
Expand Down
3 changes: 2 additions & 1 deletion processor/src/server/plugins/adyen-payment.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { app } from '../app';
export default async function (server: FastifyInstance) {
await server.register(adyenPaymentRoutes, {
paymentService: app.services.paymentService,
sessionAuthHook: paymentSDK.sessionAuthHookFn,
sessionHeaderAuthHook: paymentSDK.sessionHeaderAuthHookFn,
sessionQueryParamAuthHook: paymentSDK.sessionQueryParamAuthHookFn,
hmacAuthHook: app.hooks.hmacAuthHook,
});
}
2 changes: 1 addition & 1 deletion processor/src/server/plugins/operation.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default async function (server: FastifyInstance) {
paymentService: app.services.paymentService,
jwtAuthHook: paymentSDK.jwtAuthHookFn,
oauth2AuthHook: paymentSDK.oauth2AuthHookFn,
sessionAuthHook: paymentSDK.sessionAuthHookFn,
sessionHeaderAuthHook: paymentSDK.sessionHeaderAuthHookFn,
authorizationHook: paymentSDK.authorityAuthorizationHookFn,
});
}
49 changes: 42 additions & 7 deletions processor/src/services/adyen-payment.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
CommercetoolsCartService,
CommercetoolsPaymentService,
ErrorInvalidOperation,
healthCheckCommercetoolsPermissions,
statusHandler,
} from '@commercetools/connect-payments-sdk';
Expand All @@ -16,7 +17,11 @@ import {
PaymentMethodsResponseDTO,
} from '../dtos/adyen-payment.dto';
import { AdyenApi, wrapAdyenError } from '../clients/adyen.client';
import { getCartIdFromContext, getPaymentInterfaceFromContext } from '../libs/fastify/context/context';
import {
getCartIdFromContext,
getMerchantReturnUrlFromContext,
getPaymentInterfaceFromContext,
} from '../libs/fastify/context/context';
import { CreateSessionConverter } from './converters/create-session.converter';
import { CreatePaymentConverter } from './converters/create-payment.converter';
import { ConfirmPaymentConverter } from './converters/confirm-payment.converter';
Expand All @@ -41,6 +46,7 @@ import { SupportedPaymentComponentsSchemaDTO } from '../dtos/operations/payment-
import { PaymentDetailsResponse } from '@adyen/api-library/lib/src/typings/checkout/paymentDetailsResponse';
import { CancelPaymentConverter } from './converters/cancel-payment.converter';
import { RefundPaymentConverter } from './converters/refund-payment.converter';
import { Cart, Payment } from '@commercetools/platform-sdk';
const packageJSON = require('../../package.json');

export type AdyenPaymentServiceOptions = {
Expand Down Expand Up @@ -199,6 +205,15 @@ export class AdyenPaymentService extends AbstractPaymentService {
ctPayment = await this.ctPaymentService.getPayment({
id: opts.data.paymentReference,
});

if (await this.hasPaymentAmountChanged(ctCart, ctPayment)) {
throw new ErrorInvalidOperation('The payment amount does not fulfill the remaining amount of the cart', {
fields: {
cartId: ctCart.id,
paymentId: ctPayment.id,
},
});
}
} else {
const amountPlanned = await this.ctCartService.getPaymentAmount({ cart: ctCart });
ctPayment = await this.ctPaymentService.createPayment({
Expand Down Expand Up @@ -239,24 +254,28 @@ export class AdyenPaymentService extends AbstractPaymentService {
throw wrapAdyenError(e);
}

const txState = this.convertAdyenResultCode(
res.resultCode as PaymentResponse.ResultCodeEnum,
this.isActionRequired(res),
);
const updatedPayment = await this.ctPaymentService.updatePayment({
id: ctPayment.id,
pspReference: res.pspReference,
paymentMethod: res.paymentMethod?.type, //TODO: review
paymentMethod: res.paymentMethod?.type, //TODO: should be converted to a standard format? i.e scheme to card
transaction: {
type: 'Authorization', //TODO: review
type: 'Authorization', //TODO: is there any case where this could be a direct charge?
amount: ctPayment.amountPlanned,
interactionId: res.pspReference,
state: this.convertAdyenResultCode(
res.resultCode as PaymentResponse.ResultCodeEnum,
this.isActionRequired(res),
),
state: txState,
},
});

return {
...res,
paymentReference: updatedPayment.id,
...(txState === 'Success' || txState === 'Pending'
? { merchantReturnUrl: this.buildRedirectMerchantUrl(updatedPayment.id) }
: {}),
} as CreatePaymentResponseDTO;
}

Expand Down Expand Up @@ -290,6 +309,7 @@ export class AdyenPaymentService extends AbstractPaymentService {
return {
...res,
paymentReference: updatedPayment.id,
merchantReturnUrl: this.buildRedirectMerchantUrl(updatedPayment.id),
} as ConfirmPaymentResponseDTO;
}

Expand Down Expand Up @@ -357,4 +377,19 @@ export class AdyenPaymentService extends AbstractPaymentService {
private isActionRequired(data: PaymentResponse): boolean {
return data.action?.type !== undefined;
}

private async hasPaymentAmountChanged(cart: Cart, ctPayment: Payment): Promise<boolean> {
const amountPlanned = await this.ctCartService.getPaymentAmount({ cart });
return (
ctPayment.amountPlanned.centAmount !== amountPlanned.centAmount ||
ctPayment.amountPlanned.currencyCode !== amountPlanned.currencyCode
);
}

private buildRedirectMerchantUrl(paymentReference: string): string {
const merchantReturnUrl = getMerchantReturnUrlFromContext() || config.merchantReturnUrl;
const redirectUrl = new URL(merchantReturnUrl);
redirectUrl.searchParams.append('paymentReference', paymentReference);
return redirectUrl.toString();
}
}
7 changes: 6 additions & 1 deletion processor/src/services/converters/helper.converter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Address } from '@adyen/api-library/lib/src/typings/checkout/address';
import { LineItem } from '@adyen/api-library/lib/src/typings/checkout/lineItem';
import { Cart, LineItem as CoCoLineItem, CustomLineItem, Address as CartAddress } from '@commercetools/platform-sdk';
import { getAllowedPaymentMethodsFromContext, getProcessorUrlFromContext } from '../../libs/fastify/context/context';
import {
getAllowedPaymentMethodsFromContext,
getCtSessionIdFromContext,
getProcessorUrlFromContext,
} from '../../libs/fastify/context/context';

export const populateLineItems = (cart: Cart): LineItem[] => {
const lineItems: LineItem[] = [];
Expand Down Expand Up @@ -81,6 +85,7 @@ export const convertPaymentMethodFromAdyenFormat = (paymentMethod: string): stri
export const buildReturnUrl = (paymentReference: string): string => {
const url = new URL('/payments/details', getProcessorUrlFromContext());
url.searchParams.append('paymentReference', paymentReference);
url.searchParams.append('ctsid', getCtSessionIdFromContext());
return url.toString();
};

Expand Down
14 changes: 7 additions & 7 deletions processor/test/routes.test/operations.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fastify from 'fastify';
import { describe, beforeAll, afterAll, test, expect } from '@jest/globals';
import { describe, beforeAll, afterAll, test, expect, jest, afterEach } from '@jest/globals';
import {
AuthorityAuthorizationHook,
AuthorityAuthorizationManager,
Expand All @@ -11,8 +11,8 @@ import {
Oauth2AuthenticationHook,
Oauth2AuthenticationManager,
RequestContextData,
SessionAuthenticationHook,
SessionAuthenticationManager,
SessionHeaderAuthenticationHook,
SessionHeaderAuthenticationManager,
} from '@commercetools/connect-payments-sdk';
import { IncomingHttpHeaders } from 'node:http';
import { operationsRoute } from '../../src/routes/operation.route';
Expand All @@ -37,7 +37,7 @@ describe('/operations APIs', () => {
});

const spyAuthenticateSession = jest
.spyOn(SessionAuthenticationHook.prototype, 'authenticate')
.spyOn(SessionHeaderAuthenticationHook.prototype, 'authenticate')
.mockImplementationOnce(() => async (request: { headers: IncomingHttpHeaders }) => {
expect(request.headers['x-session-id']).toContain('session-id');
});
Expand All @@ -52,8 +52,8 @@ describe('/operations APIs', () => {
contextProvider: jest.fn() as unknown as ContextProvider<RequestContextData>,
});

const spiedSessionAuthenticationHook = new SessionAuthenticationHook({
authenticationManager: jest.fn() as unknown as SessionAuthenticationManager,
const spiedSessionHeaderAuthenticationHook = new SessionHeaderAuthenticationHook({
authenticationManager: jest.fn() as unknown as SessionHeaderAuthenticationManager,
contextProvider: jest.fn() as unknown as ContextProvider<RequestContextData>,
});

Expand All @@ -72,7 +72,7 @@ describe('/operations APIs', () => {
prefix: '/operations',
oauth2AuthHook: spiedOauth2AuthenticationHook,
jwtAuthHook: spiedJwtAuthenticationHook,
sessionAuthHook: spiedSessionAuthenticationHook,
sessionHeaderAuthHook: spiedSessionHeaderAuthenticationHook,
authorizationHook: spiedAuthorityAuthorizationHook,
paymentService: spiedPaymentService,
});
Expand Down

0 comments on commit e10d300

Please sign in to comment.