Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(adyen-template): refactored operations to be included in an abstract service #25

Merged
merged 2 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading