Skip to content

Commit

Permalink
feat(adyen-template): refactored operations to be included in an abst…
Browse files Browse the repository at this point in the history
…ract service
  • Loading branch information
dasanorct committed Feb 28, 2024
1 parent 83faac0 commit f2df02c
Show file tree
Hide file tree
Showing 24 changed files with 497 additions and 337 deletions.
2 changes: 1 addition & 1 deletion processor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "payment-integration-adyen",
"version": "0.0.2",
"version": "0.0.5",
"description": "Payment integration with Adyen",
"main": "dist/server.js",
"scripts": {
Expand Down
12 changes: 12 additions & 0 deletions processor/src/clients/adyen.client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Client, CheckoutAPI } from '@adyen/api-library';
import { config } from '../config/config';
import { log } from '../libs/logger';
import { AdyenApiError, AdyenApiErrorData } from '../errors/adyen-api.error';

export const AdyenApi = (): CheckoutAPI => {
const apiClient = new Client({
Expand All @@ -12,3 +14,13 @@ export const AdyenApi = (): CheckoutAPI => {

return new CheckoutAPI(apiClient);
};

export const wrapAdyenError = (e: any): Error => {
if (e?.responseBody) {
const errorData = JSON.parse(e.responseBody) as AdyenApiErrorData;
return new AdyenApiError(errorData, { cause: e });
}

log.error('Unexpected error calling Adyen', e);
return e;
};
1 change: 1 addition & 0 deletions processor/src/dtos/operations/payment-componets.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Static, Type } from '@sinclair/typebox';

export const SupportedPaymentComponentsData = Type.Object({
type: Type.String(),
subtypes: Type.Optional(Type.Array(Type.String())),
});

export const SupportedPaymentComponentsSchema = Type.Object({
Expand Down
19 changes: 19 additions & 0 deletions processor/src/errors/adyen-api.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Errorx, ErrorxAdditionalOpts } from '@commercetools/connect-payments-sdk';

export type AdyenApiErrorData = {
status: number;
errorCode: string;
message: string;
errorType?: string;
};

export class AdyenApiError extends Errorx {
constructor(errorData: AdyenApiErrorData, additionalOpts?: ErrorxAdditionalOpts) {
super({
code: `AdyenError-${errorData.errorCode}`,
httpErrorStatus: errorData.status,
message: errorData.message,
...additionalOpts,
});
}
}
36 changes: 36 additions & 0 deletions processor/src/libs/fastify/dtos/error.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Static, Type } from '@sinclair/typebox';

/**
* Represents https://docs.commercetools.com/api/errors#errorobject
*/
export const ErrorObject = Type.Object(
{
code: Type.String(),
message: Type.String(),
},
{ additionalProperties: true },
);

/**
* Represents https://docs.commercetools.com/api/errors#errorresponse
*/
export const ErrorResponse = Type.Object({
statusCode: Type.Integer(),
message: Type.String(),
errors: Type.Array(ErrorObject),
});

/**
* Represents https://docs.commercetools.com/api/errors#autherrorresponse
*/
export const AuthErrorResponse = Type.Composite([
ErrorResponse,
Type.Object({
error: Type.String(),
error_description: Type.Optional(Type.String()),
}),
]);

export type TErrorObject = Static<typeof ErrorObject>;
export type TErrorResponse = Static<typeof ErrorResponse>;
export type TAuthErrorResponse = Static<typeof AuthErrorResponse>;
15 changes: 12 additions & 3 deletions processor/src/libs/fastify/error-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,14 @@ describe('error-handler', () => {
});

expect(response.json()).toStrictEqual({
code: 'ErrorCode',
message: 'someMessage',
statusCode: 404,
errors: [
{
code: 'ErrorCode',
message: 'someMessage',
},
],
});
});

Expand All @@ -53,7 +58,6 @@ describe('error-handler', () => {
});

expect(response.json()).toStrictEqual({
code: 'ErrorCode',
message: 'someMessage',
statusCode: 404,
errors: [
Expand All @@ -77,9 +81,14 @@ describe('error-handler', () => {
});

expect(response.json()).toStrictEqual({
code: 'General',
message: 'Internal server error.',
statusCode: 500,
errors: [
{
code: 'General',
message: 'Internal server error.',
},
],
});
});
});
170 changes: 100 additions & 70 deletions processor/src/libs/fastify/error-handler.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,122 @@
import { type FastifyReply, type FastifyRequest } from 'fastify';
import { FastifyError, type FastifyReply, type FastifyRequest } from 'fastify';

import { ErrorInvalidField, ErrorRequiredField, Errorx, MultiErrorx } from '@commercetools/connect-payments-sdk';
import { FastifySchemaValidationError } from 'fastify/types/schema';
import { log } from '../logger';
import {
ErrorAuthErrorResponse,
ErrorGeneral,
ErrorInvalidField,
ErrorInvalidJsonInput,
ErrorRequiredField,
Errorx,
MultiErrorx,
} from '@commercetools/connect-payments-sdk';
import { TAuthErrorResponse, TErrorObject, TErrorResponse } from './dtos/error.dto';

const getKeys = (path: string) => path.replace(/^\//, '').split('/');
function isFastifyValidationError(error: Error): error is FastifyError {
return (error as unknown as FastifyError).validation != undefined;
}

const getPropertyFromPath = (path: string, obj: any): any => {
const keys = getKeys(path);
let value = obj;
for (const key of keys) {
value = value[key];
export const errorHandler = (error: Error, req: FastifyRequest, reply: FastifyReply) => {
if (isFastifyValidationError(error) && error.validation) {
return handleErrors(transformValidationErrors(error.validation, req), reply);
} else if (error instanceof ErrorAuthErrorResponse) {
return handleAuthError(error, reply);
} else if (error instanceof Errorx) {
return handleErrors([error], reply);
} else if (error instanceof MultiErrorx) {
return handleErrors(error.errors, reply);
}
return value;

// If it isn't any of the cases above (for example a normal Error is thrown) then fallback to a general 500 internal server error
return handleErrors([new ErrorGeneral('Internal server error.', { cause: error, skipLog: false })], reply);
};

type ValidationObject = {
validation: object;
const handleAuthError = (error: ErrorAuthErrorResponse, reply: FastifyReply) => {
const transformedErrors: TErrorObject[] = transformErrorxToHTTPModel([error]);

const response: TAuthErrorResponse = {
message: error.message,
statusCode: error.httpErrorStatus,
errors: transformedErrors,
error: transformedErrors[0].code,
error_description: transformedErrors[0].message,
};

return reply.code(error.httpErrorStatus).send(response);
};

type TError = {
statusCode: number;
code: string;
message: string;
errors?: object[];
const handleErrors = (errorxList: Errorx[], reply: FastifyReply) => {
const transformedErrors: TErrorObject[] = transformErrorxToHTTPModel(errorxList);

// Based on CoCo specs, the root level message attribute is always set to the values from the first error. MultiErrorx enforces the same HTTP status code.
const response: TErrorResponse = {
message: errorxList[0].message,
statusCode: errorxList[0].httpErrorStatus,
errors: transformedErrors,
};

return reply.code(errorxList[0].httpErrorStatus).send(response);
};

export const errorHandler = (error: Error, req: FastifyRequest, reply: FastifyReply) => {
if (error instanceof Object && (error as unknown as ValidationObject).validation) {
const errorsList: Errorx[] = [];

// Transforming the validation errors
for (const err of (error as any).validation) {
switch (err.keyword) {
case 'required':
errorsList.push(new ErrorRequiredField(err.params.missingProperty));
break;
case 'enum':
errorsList.push(
new ErrorInvalidField(
getKeys(err.instancePath).join('.'),
getPropertyFromPath(err.instancePath, req.body),
err.params.allowedValues,
),
);
}

if (errorsList.length > 1) {
error = new MultiErrorx(errorsList);
} else {
error = errorsList[0];
}
const transformErrorxToHTTPModel = (errors: Errorx[]): TErrorObject[] => {
const errorObjectList: TErrorObject[] = [];

for (const err of errors) {
if (err.skipLog) {
log.debug(err.message, err);
} else {
log.error(err.message, err);
}
}

if (error instanceof Errorx) {
return handleErrorx(error, reply);
} else {
const { message, ...meta } = error;
log.error(message, meta);
return reply.code(500).send({
code: 'General',
message: 'Internal server error.',
statusCode: 500,
});
const tErrObj: TErrorObject = {
code: err.code,
message: err.message,
...(err.fields ? err.fields : {}), // Add any additional field to the response object (which will differ per type of error)
};

errorObjectList.push(tErrObj);
}

return errorObjectList;
};

const handleErrorx = (error: Errorx, reply: FastifyReply) => {
const { message, ...meta } = error;
log.error(message, meta);
const errorBuilder: TError = {
statusCode: error.httpErrorStatus,
code: error.code,
message: error.message,
};
const transformValidationErrors = (errors: FastifySchemaValidationError[], req: FastifyRequest): Errorx[] => {
const errorxList: Errorx[] = [];

const errors: object[] = [];
if (error.fields) {
errors.push({
code: error.code,
message: error.message,
...error.fields,
});
for (const err of errors) {
switch (err.keyword) {
case 'required':
errorxList.push(new ErrorRequiredField(err.params.missingProperty as string));
break;
case 'enum':
errorxList.push(
new ErrorInvalidField(
getKeys(err.instancePath).join('.'),
getPropertyFromPath(err.instancePath, req.body),
err.params.allowedValues as string,
),
);
break;
}
}

if (errors.length > 0) {
errorBuilder.errors = errors;
// If we cannot map the validation error to a CoCo error then return a general InvalidJsonError
if (errorxList.length === 0) {
errorxList.push(new ErrorInvalidJsonInput());
}

return reply.code(error.httpErrorStatus).send(errorBuilder);
return errorxList;
};

const getKeys = (path: string) => path.replace(/^\//, '').split('/');

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getPropertyFromPath = (path: string, obj: any): any => {
const keys = getKeys(path);
let value = obj;
for (const key of keys) {
value = value[key];
}
return value;
};
12 changes: 6 additions & 6 deletions processor/src/routes/operation.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import {
PaymentIntentResponseSchemaDTO,
} from '../dtos/operations/payment-intents.dto';
import { StatusResponseSchema, StatusResponseSchemaDTO } from '../dtos/operations/status.dto';
import { OperationService } from '../services/types/operation.type';
import { AbstractPaymentService } from '../services/abstract-payment.service';

type OperationRouteOptions = {
sessionAuthHook: SessionAuthenticationHook;
oauth2AuthHook: Oauth2AuthenticationHook;
jwtAuthHook: JWTAuthenticationHook;
authorizationHook: AuthorityAuthorizationHook;
operationService: OperationService;
paymentService: AbstractPaymentService;
};

export const operationsRoute = async (fastify: FastifyInstance, opts: FastifyPluginOptions & OperationRouteOptions) => {
Expand All @@ -37,7 +37,7 @@ export const operationsRoute = async (fastify: FastifyInstance, opts: FastifyPlu
},
},
async (request, reply) => {
const config = await opts.operationService.getConfig();
const config = await opts.paymentService.config();
reply.code(200).send(config);
},
);
Expand All @@ -53,7 +53,7 @@ export const operationsRoute = async (fastify: FastifyInstance, opts: FastifyPlu
},
},
async (request, reply) => {
const status = await opts.operationService.getStatus();
const status = await opts.paymentService.status();
reply.code(200).send(status);
},
);
Expand All @@ -69,7 +69,7 @@ export const operationsRoute = async (fastify: FastifyInstance, opts: FastifyPlu
},
},
async (request, reply) => {
const result = await opts.operationService.getSupportedPaymentComponents();
const result = await opts.paymentService.getSupportedPaymentComponents();
reply.code(200).send(result);
},
);
Expand All @@ -93,7 +93,7 @@ export const operationsRoute = async (fastify: FastifyInstance, opts: FastifyPlu
},
async (request, reply) => {
const { id } = request.params;
const resp = await opts.operationService.modifyPayment({
const resp = await opts.paymentService.modifyPayment({
paymentId: id,
data: request.body,
});
Expand Down
17 changes: 17 additions & 0 deletions processor/src/server/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { HmacAuthHook } from '../libs/fastify/hooks/hmac-auth.hook';
import { paymentSDK } from '../payment-sdk';
import { AdyenPaymentService } from '../services/adyen-payment.service';

const paymentService = new AdyenPaymentService({
ctCartService: paymentSDK.ctCartService,
ctPaymentService: paymentSDK.ctPaymentService,
});

export const app = {
services: {
paymentService,
},
hooks: {
hmacAuthHook: new HmacAuthHook(),
},
};
Loading

0 comments on commit f2df02c

Please sign in to comment.