diff --git a/processor/package.json b/processor/package.json index cbc51a8..a0a66af 100644 --- a/processor/package.json +++ b/processor/package.json @@ -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": { diff --git a/processor/src/clients/adyen.client.ts b/processor/src/clients/adyen.client.ts index 5e6cc55..35f3e13 100644 --- a/processor/src/clients/adyen.client.ts +++ b/processor/src/clients/adyen.client.ts @@ -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({ @@ -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; +}; diff --git a/processor/src/dtos/operations/payment-componets.dto.ts b/processor/src/dtos/operations/payment-componets.dto.ts index 666b3da..5e9d21c 100644 --- a/processor/src/dtos/operations/payment-componets.dto.ts +++ b/processor/src/dtos/operations/payment-componets.dto.ts @@ -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({ diff --git a/processor/src/errors/adyen-api.error.ts b/processor/src/errors/adyen-api.error.ts new file mode 100644 index 0000000..cc9a849 --- /dev/null +++ b/processor/src/errors/adyen-api.error.ts @@ -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, + }); + } +} diff --git a/processor/src/libs/fastify/dtos/error.dto.ts b/processor/src/libs/fastify/dtos/error.dto.ts new file mode 100644 index 0000000..6a3754e --- /dev/null +++ b/processor/src/libs/fastify/dtos/error.dto.ts @@ -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; +export type TErrorResponse = Static; +export type TAuthErrorResponse = Static; diff --git a/processor/src/libs/fastify/error-handler.spec.ts b/processor/src/libs/fastify/error-handler.spec.ts index 1eeea18..7e00536 100644 --- a/processor/src/libs/fastify/error-handler.spec.ts +++ b/processor/src/libs/fastify/error-handler.spec.ts @@ -29,9 +29,14 @@ describe('error-handler', () => { }); expect(response.json()).toStrictEqual({ - code: 'ErrorCode', message: 'someMessage', statusCode: 404, + errors: [ + { + code: 'ErrorCode', + message: 'someMessage', + }, + ], }); }); @@ -53,7 +58,6 @@ describe('error-handler', () => { }); expect(response.json()).toStrictEqual({ - code: 'ErrorCode', message: 'someMessage', statusCode: 404, errors: [ @@ -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.', + }, + ], }); }); }); diff --git a/processor/src/libs/fastify/error-handler.ts b/processor/src/libs/fastify/error-handler.ts index c056686..87f5f81 100644 --- a/processor/src/libs/fastify/error-handler.ts +++ b/processor/src/libs/fastify/error-handler.ts @@ -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; }; diff --git a/processor/src/routes/operation.route.ts b/processor/src/routes/operation.route.ts index 09b2a29..c1fa01d 100644 --- a/processor/src/routes/operation.route.ts +++ b/processor/src/routes/operation.route.ts @@ -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) => { @@ -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); }, ); @@ -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); }, ); @@ -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); }, ); @@ -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, }); diff --git a/processor/src/server/app.ts b/processor/src/server/app.ts new file mode 100644 index 0000000..40f0a04 --- /dev/null +++ b/processor/src/server/app.ts @@ -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(), + }, +}; diff --git a/processor/src/server/plugins/adyen-payment.plugin.ts b/processor/src/server/plugins/adyen-payment.plugin.ts index a84ce33..4def6ef 100644 --- a/processor/src/server/plugins/adyen-payment.plugin.ts +++ b/processor/src/server/plugins/adyen-payment.plugin.ts @@ -1,18 +1,12 @@ import { FastifyInstance } from 'fastify'; import { paymentSDK } from '../../payment-sdk'; import { adyenPaymentRoutes } from '../../routes/adyen-payment.route'; -import { AdyenPaymentService } from '../../services/adyen-payment.service'; -import { HmacAuthHook } from '../../libs/fastify/hooks/hmac-auth.hook'; +import { app } from '../app'; export default async function (server: FastifyInstance) { - const paymentService = new AdyenPaymentService({ - ctCartService: paymentSDK.ctCartService, - ctPaymentService: paymentSDK.ctPaymentService, - }); - await server.register(adyenPaymentRoutes, { - paymentService, + paymentService: app.services.paymentService, sessionAuthHook: paymentSDK.sessionAuthHookFn, - hmacAuthHook: new HmacAuthHook(), + hmacAuthHook: app.hooks.hmacAuthHook, }); } diff --git a/processor/src/server/plugins/operation.plugin.ts b/processor/src/server/plugins/operation.plugin.ts index 233a10a..1419b4a 100644 --- a/processor/src/server/plugins/operation.plugin.ts +++ b/processor/src/server/plugins/operation.plugin.ts @@ -1,21 +1,12 @@ import { FastifyInstance } from 'fastify'; import { paymentSDK } from '../../payment-sdk'; import { operationsRoute } from '../../routes/operation.route'; -import { DefaultOperationService } from '../../services/operation.service'; -import { AdyenOperationProcessor } from '../../services/processors/adyen-operation.processor'; +import { app } from '../app'; export default async function (server: FastifyInstance) { - const paymentProcessor = new AdyenOperationProcessor(); - - const operationService = new DefaultOperationService({ - ctCartService: paymentSDK.ctCartService, - ctPaymentService: paymentSDK.ctPaymentService, - operationProcessor: paymentProcessor, - }); - await server.register(operationsRoute, { prefix: '/operations', - operationService: operationService, + paymentService: app.services.paymentService, jwtAuthHook: paymentSDK.jwtAuthHookFn, oauth2AuthHook: paymentSDK.oauth2AuthHookFn, sessionAuthHook: paymentSDK.sessionAuthHookFn, diff --git a/processor/src/services/operation.service.ts b/processor/src/services/abstract-payment.service.ts similarity index 53% rename from processor/src/services/operation.service.ts rename to processor/src/services/abstract-payment.service.ts index 7de838c..144e40a 100644 --- a/processor/src/services/operation.service.ts +++ b/processor/src/services/abstract-payment.service.ts @@ -1,47 +1,71 @@ import { + CommercetoolsCartService, CommercetoolsPaymentService, ErrorInvalidJsonInput, ErrorInvalidOperation, } from '@commercetools/connect-payments-sdk'; -import { ModifyPayment, OperationService, OperationServiceOptions } from './types/operation.type'; - -import { ConfigResponseSchemaDTO } from '../dtos/operations/config.dto'; -import { SupportedPaymentComponentsSchemaDTO } from '../dtos/operations/payment-componets.dto'; +import { + CancelPaymentRequest, + CapturePaymentRequest, + ConfigResponse, + ModifyPayment, + PaymentProviderModificationResponse, + RefundPaymentRequest, + StatusResponse, +} from './types/operation.type'; import { AmountSchemaDTO, PaymentIntentResponseSchemaDTO, PaymentModificationStatus, } from '../dtos/operations/payment-intents.dto'; -import { StatusResponseSchemaDTO } from '../dtos/operations/status.dto'; -import { OperationProcessor } from './processors/operation.processor'; import { Payment } from '@commercetools/platform-sdk'; +import { SupportedPaymentComponentsSchemaDTO } from '../dtos/operations/payment-componets.dto'; -export class DefaultOperationService implements OperationService { - private ctPaymentService: CommercetoolsPaymentService; - private operationProcessor: OperationProcessor; - - constructor(opts: OperationServiceOptions) { - this.ctPaymentService = opts.ctPaymentService; - this.operationProcessor = opts.operationProcessor; - } - - public async getStatus(): Promise { - return this.operationProcessor.status(); - } - - public async getConfig(): Promise { - return this.operationProcessor.config(); +export abstract class AbstractPaymentService { + protected ctCartService: CommercetoolsCartService; + protected ctPaymentService: CommercetoolsPaymentService; + constructor(ctCartService: CommercetoolsCartService, ctPaymentService: CommercetoolsPaymentService) { + this.ctCartService = ctCartService; + this.ctPaymentService = ctPaymentService; } - public async getSupportedPaymentComponents(): Promise { - return { - components: [ - { - type: 'card', - }, - ], - }; - } + /** + * Get configuration information + * @returns + */ + abstract config(): Promise; + + /** + * Get stats information + * @returns + */ + abstract status(): Promise; + + /** + * Get supported payment components by the processor + */ + abstract getSupportedPaymentComponents(): Promise; + + /** + * Capture payment + * @param request + * @returns + */ + abstract capturePayment(request: CapturePaymentRequest): Promise; + + /** + * Cancel payment + * @param request + * @returns + */ + abstract cancelPayment(request: CancelPaymentRequest): Promise; + + /** + * Refund payment + * @param request + * @returns + */ + abstract refundPayment(request: RefundPaymentRequest): Promise; public async modifyPayment(opts: ModifyPayment): Promise { const ctPayment = await this.ctPaymentService.getPayment({ @@ -75,7 +99,7 @@ export class DefaultOperationService implements OperationService { transaction: { type: transactionType, amount: requestAmount, - interactionId: res?.pspReference, + interactionId: res.pspReference, state: res.outcome === PaymentModificationStatus.APPROVED ? 'Success' : 'Failure', }, }); @@ -85,7 +109,7 @@ export class DefaultOperationService implements OperationService { }; } - private getPaymentTransactionType(action: string): string { + protected getPaymentTransactionType(action: string): string { switch (action) { case 'cancelPayment': { return 'CancelAuthorization'; @@ -103,16 +127,20 @@ export class DefaultOperationService implements OperationService { } } - private async processPaymentModification(payment: Payment, transactionType: string, requestAmount: AmountSchemaDTO) { + protected async processPaymentModification( + payment: Payment, + transactionType: string, + requestAmount: AmountSchemaDTO, + ) { switch (transactionType) { case 'CancelAuthorization': { - return await this.operationProcessor.cancelPayment({ payment }); + return await this.cancelPayment({ payment }); } case 'Charge': { - return await this.operationProcessor.capturePayment({ amount: requestAmount, payment }); + return await this.capturePayment({ amount: requestAmount, payment }); } case 'Refund': { - return await this.operationProcessor.refundPayment({ amount: requestAmount, payment }); + return await this.refundPayment({ amount: requestAmount, payment }); } default: { throw new ErrorInvalidOperation(`Operation ${transactionType} not supported.`); diff --git a/processor/src/services/adyen-payment.service.ts b/processor/src/services/adyen-payment.service.ts index 7e5a1d0..d7adc1a 100644 --- a/processor/src/services/adyen-payment.service.ts +++ b/processor/src/services/adyen-payment.service.ts @@ -1,4 +1,9 @@ -import { CommercetoolsCartService, CommercetoolsPaymentService } from '@commercetools/connect-payments-sdk'; +import { + CommercetoolsCartService, + CommercetoolsPaymentService, + healthCheckCommercetoolsPermissions, + statusHandler, +} from '@commercetools/connect-payments-sdk'; import { ConfirmPaymentRequestDTO, ConfirmPaymentResponseDTO, @@ -10,31 +15,52 @@ import { PaymentMethodsRequestDTO, PaymentMethodsResponseDTO, } from '../dtos/adyen-payment.dto'; -import { AdyenApi } from '../clients/adyen.client'; +import { AdyenApi, wrapAdyenError } from '../clients/adyen.client'; import { getCartIdFromContext, 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'; import { NotificationConverter } from './converters/notification.converter'; import { PaymentMethodsConverter } from './converters/payment-methods.converter'; +import { PaymentComponentsConverter } from './converters/payment-components.converter'; +import { CapturePaymentConverter } from './converters/capture-payment.converter'; import { PaymentResponse } from '@adyen/api-library/lib/src/typings/checkout/paymentResponse'; -import { log } from 'console'; +import { + CancelPaymentRequest, + CapturePaymentRequest, + ConfigResponse, + PaymentProviderModificationResponse, + RefundPaymentRequest, + StatusResponse, +} from './types/operation.type'; +import { config } from '../config/config'; +import { paymentSDK } from '../payment-sdk'; +import { PaymentModificationStatus } from '../dtos/operations/payment-intents.dto'; +import { AbstractPaymentService } from './abstract-payment.service'; +import { SupportedPaymentComponentsSchemaDTO } from '../dtos/operations/payment-componets.dto'; +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'; +const packageJSON = require('../../package.json'); export type AdyenPaymentServiceOptions = { ctCartService: CommercetoolsCartService; ctPaymentService: CommercetoolsPaymentService; }; -export class AdyenPaymentService { - private ctCartService: CommercetoolsCartService; - private ctPaymentService: CommercetoolsPaymentService; +export class AdyenPaymentService extends AbstractPaymentService { private paymentMethodsConverter: PaymentMethodsConverter; private createSessionConverter: CreateSessionConverter; private createPaymentConverter: CreatePaymentConverter; private confirmPaymentConverter: ConfirmPaymentConverter; private notificationConverter: NotificationConverter; + private paymentComponentsConverter: PaymentComponentsConverter; + private cancelPaymentConverter: CancelPaymentConverter; + private capturePaymentConverter: CapturePaymentConverter; + private refundPaymentConverter: RefundPaymentConverter; constructor(opts: AdyenPaymentServiceOptions) { + super(opts.ctCartService, opts.ctPaymentService); this.ctCartService = opts.ctCartService; this.ctPaymentService = opts.ctPaymentService; this.paymentMethodsConverter = new PaymentMethodsConverter(this.ctCartService); @@ -42,6 +68,63 @@ export class AdyenPaymentService { this.createPaymentConverter = new CreatePaymentConverter(); this.confirmPaymentConverter = new ConfirmPaymentConverter(); this.notificationConverter = new NotificationConverter(); + this.paymentComponentsConverter = new PaymentComponentsConverter(); + this.cancelPaymentConverter = new CancelPaymentConverter(); + this.capturePaymentConverter = new CapturePaymentConverter(); + this.refundPaymentConverter = new RefundPaymentConverter(); + } + async config(): Promise { + return { + clientKey: config.adyenClientKey, + environment: config.adyenEnvironment, + }; + } + + async status(): Promise { + const handler = await statusHandler({ + timeout: config.healthCheckTimeout, + checks: [ + healthCheckCommercetoolsPermissions({ + requiredPermissions: ['manage_payments', 'view_sessions', 'view_api_clients'], + ctAuthorizationService: paymentSDK.ctAuthorizationService, + projectKey: config.projectKey, + }), + async () => { + try { + const result = await AdyenApi().PaymentsApi.paymentMethods({ + merchantAccount: config.adyenMerchantAccount, + }); + return { + name: 'Adyen Status check', + status: 'UP', + data: { + paymentMethods: result.paymentMethods, + }, + }; + } catch (e) { + return { + name: 'Adyen Status check', + status: 'DOWN', + data: { + error: e, + }, + }; + } + }, + ], + metadataFn: async () => ({ + name: packageJSON.name, + description: packageJSON.description, + '@commercetools/sdk-client-v2': packageJSON.dependencies['@commercetools/sdk-client-v2'], + '@adyen/api-library': packageJSON.dependencies['@adyen/api-library'], + }), + })(); + + return handler.body; + } + + async getSupportedPaymentComponents(): Promise { + return this.paymentComponentsConverter.convertResponse(); } async getPaymentMethods(opts: { data: PaymentMethodsRequestDTO }): Promise { @@ -55,8 +138,7 @@ export class AdyenPaymentService { data: res, }); } catch (e) { - log('Adyen getPaymentMethods error', e); - throw e; + throw wrapAdyenError(e); } } @@ -90,7 +172,7 @@ export class AdyenPaymentService { paymentId: ctPayment.id, }); - const adyenRequestData = await this.createSessionConverter.convert({ + const adyenRequestData = this.createSessionConverter.convertRequest({ data: opts.data, cart: updatedCart, payment: ctPayment, @@ -103,8 +185,7 @@ export class AdyenPaymentService { paymentReference: ctPayment.id, }; } catch (e) { - log('Adyen createSession error', e); - throw e; + throw wrapAdyenError(e); } } @@ -145,13 +226,18 @@ export class AdyenPaymentService { }); } - const data = await this.createPaymentConverter.convert({ + const data = this.createPaymentConverter.convertRequest({ data: opts.data, cart: ctCart, payment: ctPayment, }); - const res = await AdyenApi().PaymentsApi.payments(data); + let res!: PaymentResponse; + try { + res = await AdyenApi().PaymentsApi.payments(data); + } catch (e) { + throw wrapAdyenError(e); + } const updatedPayment = await this.ctPaymentService.updatePayment({ id: ctPayment.id, @@ -176,10 +262,16 @@ export class AdyenPaymentService { id: opts.data.paymentReference, }); - const data = await this.confirmPaymentConverter.convert({ + const data = this.confirmPaymentConverter.convertRequest({ data: opts.data, }); - const res = await AdyenApi().PaymentsApi.paymentsDetails(data); + + let res!: PaymentDetailsResponse; + try { + res = await AdyenApi().PaymentsApi.paymentsDetails(data); + } catch (e) { + throw wrapAdyenError(e); + } const updatedPayment = await this.ctPaymentService.updatePayment({ id: ctPayment.id, @@ -203,6 +295,46 @@ export class AdyenPaymentService { await this.ctPaymentService.updatePayment(updateData); } + async capturePayment(request: CapturePaymentRequest): Promise { + const interfaceId = request.payment.interfaceId as string; + try { + const res = await AdyenApi().ModificationsApi.captureAuthorisedPayment( + interfaceId, + this.capturePaymentConverter.convertRequest(request), + ); + + return { outcome: PaymentModificationStatus.RECEIVED, pspReference: res.pspReference }; + } catch (e) { + throw wrapAdyenError(e); + } + } + + async cancelPayment(request: CancelPaymentRequest): Promise { + const interfaceId = request.payment.interfaceId as string; + try { + const res = await AdyenApi().ModificationsApi.cancelAuthorisedPaymentByPspReference( + interfaceId, + this.cancelPaymentConverter.convertRequest(request), + ); + return { outcome: PaymentModificationStatus.RECEIVED, pspReference: res.pspReference }; + } catch (e) { + throw wrapAdyenError(e); + } + } + + async refundPayment(request: RefundPaymentRequest): Promise { + const interfaceId = request.payment.interfaceId as string; + try { + const res = await AdyenApi().ModificationsApi.refundCapturedPayment( + interfaceId, + this.refundPaymentConverter.convertRequest(request), + ); + return { outcome: PaymentModificationStatus.RECEIVED, pspReference: res.pspReference }; + } catch (e) { + throw wrapAdyenError(e); + } + } + private convertAdyenResultCode(resultCode: PaymentResponse.ResultCodeEnum): string { switch (resultCode) { case PaymentResponse.ResultCodeEnum.Authorised: diff --git a/processor/src/services/converters/cancel-payment.converter.ts b/processor/src/services/converters/cancel-payment.converter.ts new file mode 100644 index 0000000..ebd8e77 --- /dev/null +++ b/processor/src/services/converters/cancel-payment.converter.ts @@ -0,0 +1,14 @@ +import { config } from '../../config/config'; +import { PaymentCancelRequest } from '@adyen/api-library/lib/src/typings/checkout/paymentCancelRequest'; +import { CancelPaymentRequest } from '../types/operation.type'; + +export class CancelPaymentConverter { + constructor() {} + + public convertRequest(opts: CancelPaymentRequest): PaymentCancelRequest { + return { + merchantAccount: config.adyenMerchantAccount, + reference: opts.payment.id, + }; + } +} diff --git a/processor/src/services/converters/capture-payment.converter.ts b/processor/src/services/converters/capture-payment.converter.ts new file mode 100644 index 0000000..e925772 --- /dev/null +++ b/processor/src/services/converters/capture-payment.converter.ts @@ -0,0 +1,18 @@ +import { config } from '../../config/config'; +import { PaymentCaptureRequest } from '@adyen/api-library/lib/src/typings/checkout/paymentCaptureRequest'; +import { CapturePaymentRequest } from '../types/operation.type'; + +export class CapturePaymentConverter { + constructor() {} + + public convertRequest(opts: CapturePaymentRequest): PaymentCaptureRequest { + return { + merchantAccount: config.adyenMerchantAccount, + reference: opts.payment.id, + amount: { + currency: opts.amount.currencyCode, + value: opts.amount.centAmount, + }, + }; + } +} diff --git a/processor/src/services/converters/confirm-payment.converter.ts b/processor/src/services/converters/confirm-payment.converter.ts index ef72d57..b2317f9 100644 --- a/processor/src/services/converters/confirm-payment.converter.ts +++ b/processor/src/services/converters/confirm-payment.converter.ts @@ -4,7 +4,7 @@ import { PaymentDetailsRequest } from '@adyen/api-library/lib/src/typings/checko export class ConfirmPaymentConverter { constructor() {} - public async convert(opts: { data: ConfirmPaymentRequestDTO }): Promise { + public convertRequest(opts: { data: ConfirmPaymentRequestDTO }): PaymentDetailsRequest { return { ...opts.data, }; diff --git a/processor/src/services/converters/create-payment.converter.ts b/processor/src/services/converters/create-payment.converter.ts index b492308..d0ab291 100644 --- a/processor/src/services/converters/create-payment.converter.ts +++ b/processor/src/services/converters/create-payment.converter.ts @@ -8,7 +8,7 @@ import { CreatePaymentRequestDTO } from '../../dtos/adyen-payment.dto'; export class CreatePaymentConverter { constructor() {} - public async convert(opts: { data: CreatePaymentRequestDTO; cart: Cart; payment: Payment }): Promise { + public convertRequest(opts: { data: CreatePaymentRequestDTO; cart: Cart; payment: Payment }): PaymentRequest { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { paymentReference: _, ...requestData } = opts.data; return { diff --git a/processor/src/services/converters/create-session.converter.ts b/processor/src/services/converters/create-session.converter.ts index 79ed03d..9ca967d 100644 --- a/processor/src/services/converters/create-session.converter.ts +++ b/processor/src/services/converters/create-session.converter.ts @@ -7,12 +7,12 @@ import { Cart, Payment } from '@commercetools/platform-sdk'; export class CreateSessionConverter { constructor() {} - public async convert(opts: { + public convertRequest(opts: { data: CreateSessionRequestDTO; cart: Cart; payment: Payment; - }): Promise { - const data: CreateCheckoutSessionRequest = { + }): CreateCheckoutSessionRequest { + return { ...opts.data, amount: { value: opts.payment.amountPlanned.centAmount, @@ -33,7 +33,5 @@ export class CreateSessionConverter { }), shopperEmail: opts.cart.customerEmail, }; - - return data; } } diff --git a/processor/src/services/converters/notification.converter.ts b/processor/src/services/converters/notification.converter.ts index 0739dd5..86e76e3 100644 --- a/processor/src/services/converters/notification.converter.ts +++ b/processor/src/services/converters/notification.converter.ts @@ -6,7 +6,7 @@ import { TransactionData, UpdatePayment } from '@commercetools/connect-payments- export class NotificationConverter { constructor() {} - public async convert(opts: { data: NotificationRequestDTO }): Promise { + public convert(opts: { data: NotificationRequestDTO }): UpdatePayment { const item = opts.data.notificationItems[0].NotificationRequestItem; return { diff --git a/processor/src/services/converters/payment-components.converter.ts b/processor/src/services/converters/payment-components.converter.ts new file mode 100644 index 0000000..166d00b --- /dev/null +++ b/processor/src/services/converters/payment-components.converter.ts @@ -0,0 +1,24 @@ +import { SupportedPaymentComponentsSchemaDTO } from '../../dtos/operations/payment-componets.dto'; + +export class PaymentComponentsConverter { + constructor() {} + + public convertResponse(): SupportedPaymentComponentsSchemaDTO { + return { + components: [ + { + type: 'card', + }, + { + type: 'ideal', + }, + { + type: 'paypal', + }, + { + type: 'sofort', + }, + ], + }; + } +} diff --git a/processor/src/services/converters/refund-payment.converter.ts b/processor/src/services/converters/refund-payment.converter.ts new file mode 100644 index 0000000..30e9183 --- /dev/null +++ b/processor/src/services/converters/refund-payment.converter.ts @@ -0,0 +1,18 @@ +import { config } from '../../config/config'; +import { PaymentRefundRequest } from '@adyen/api-library/lib/src/typings/checkout/paymentRefundRequest'; +import { RefundPaymentRequest } from '../types/operation.type'; + +export class RefundPaymentConverter { + constructor() {} + + public convertRequest(opts: RefundPaymentRequest): PaymentRefundRequest { + return { + merchantAccount: config.adyenMerchantAccount, + reference: opts.payment.id, + amount: { + currency: opts.amount.currencyCode, + value: opts.amount.centAmount, + }, + }; + } +} diff --git a/processor/src/services/processors/adyen-operation.processor.ts b/processor/src/services/processors/adyen-operation.processor.ts deleted file mode 100644 index 36adce3..0000000 --- a/processor/src/services/processors/adyen-operation.processor.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { healthCheckCommercetoolsPermissions, statusHandler } from '@commercetools/connect-payments-sdk'; -import { AdyenApi } from '../../clients/adyen.client'; -import { config } from '../../config/config'; -import { PaymentModificationStatus } from '../../dtos/operations/payment-intents.dto'; -import { paymentSDK } from '../../payment-sdk'; -import { - CancelPaymentRequest, - CapturePaymentRequest, - ConfigResponse, - PaymentProviderModificationResponse, - RefundPaymentRequest, - StatusResponse, -} from '../types/operation.type'; -import { OperationProcessor } from './operation.processor'; -import { log } from '../../libs/logger'; -const packageJSON = require('../../../package.json'); - -export class AdyenOperationProcessor implements OperationProcessor { - async config(): Promise { - return { - clientKey: config.adyenClientKey, - environment: config.adyenEnvironment, - }; - } - - async status(): Promise { - const handler = await statusHandler({ - timeout: config.healthCheckTimeout, - checks: [ - healthCheckCommercetoolsPermissions({ - requiredPermissions: ['manage_payments', 'view_sessions', 'view_api_clients'], - ctAuthorizationService: paymentSDK.ctAuthorizationService, - projectKey: config.projectKey, - }), - async () => { - try { - const result = await AdyenApi().PaymentsApi.paymentMethods({ - merchantAccount: config.adyenMerchantAccount, - }); - return { - name: 'Adyen Status check', - status: 'UP', - data: { - paymentMethods: result.paymentMethods, - }, - }; - } catch (e) { - return { - name: 'Adyen Status check', - status: 'DOWN', - data: { - error: e, - }, - }; - } - }, - ], - metadataFn: async () => ({ - name: packageJSON.name, - description: packageJSON.description, - '@commercetools/sdk-client-v2': packageJSON.dependencies['@commercetools/sdk-client-v2'], - '@adyen/api-library': packageJSON.dependencies['@adyen/api-library'], - }), - })(); - - return handler.body; - } - - async capturePayment(request: CapturePaymentRequest): Promise { - const interfaceId = request.payment.interfaceId as string; - try { - await AdyenApi().ModificationsApi.captureAuthorisedPayment(interfaceId, { - amount: { - value: request.amount.centAmount, - currency: request.amount.currencyCode, - }, - merchantAccount: config.adyenMerchantAccount, - reference: interfaceId, - }); - return { outcome: PaymentModificationStatus.RECEIVED, pspReference: interfaceId }; - } catch (e) { - log.error('Error capturing payment', e); - return { outcome: PaymentModificationStatus.REJECTED, pspReference: interfaceId }; - } - } - - async cancelPayment(request: CancelPaymentRequest): Promise { - const interfaceId = request.payment.interfaceId as string; - try { - await AdyenApi().ModificationsApi.cancelAuthorisedPaymentByPspReference(interfaceId, { - merchantAccount: config.adyenMerchantAccount, - reference: interfaceId, - }); - return { outcome: PaymentModificationStatus.RECEIVED, pspReference: interfaceId }; - } catch (e) { - log.error('Error cancelling payment', e); - return { outcome: PaymentModificationStatus.REJECTED, pspReference: interfaceId }; - } - } - - async refundPayment(request: RefundPaymentRequest): Promise { - const interfaceId = request.payment.interfaceId as string; - try { - await AdyenApi().ModificationsApi.refundCapturedPayment(interfaceId, { - amount: { - value: request.amount.centAmount, - currency: request.amount.currencyCode, - }, - merchantAccount: config.adyenMerchantAccount, - reference: request.payment.id, - }); - return { outcome: PaymentModificationStatus.RECEIVED, pspReference: interfaceId }; - } catch (e) { - log.error('Error refunding payment', e); - return { outcome: PaymentModificationStatus.REJECTED, pspReference: interfaceId }; - } - } -} diff --git a/processor/src/services/processors/operation.processor.ts b/processor/src/services/processors/operation.processor.ts deleted file mode 100644 index c2d4769..0000000 --- a/processor/src/services/processors/operation.processor.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - CancelPaymentRequest, - CapturePaymentRequest, - ConfigResponse, - PaymentProviderModificationResponse, - RefundPaymentRequest, - StatusResponse, -} from '../types/operation.type'; - -export interface OperationProcessor { - /** - * Get configuration information - * @returns - */ - config: () => Promise; - - /** - * Get stats information - * @returns - */ - status: () => Promise; - - /** - * Capture payment - * @param request - * @returns - */ - capturePayment: (request: CapturePaymentRequest) => Promise; - - /** - * Cancel payment - * @param request - * @returns - */ - cancelPayment: (request: CancelPaymentRequest) => Promise; - - /** - * Refund payment - * @param request - * @returns - */ - refundPayment: (request: RefundPaymentRequest) => Promise; -} diff --git a/processor/src/services/types/operation.type.ts b/processor/src/services/types/operation.type.ts index 4a654e9..e8a64a7 100644 --- a/processor/src/services/types/operation.type.ts +++ b/processor/src/services/types/operation.type.ts @@ -1,14 +1,10 @@ -import { CommercetoolsCartService, CommercetoolsPaymentService } from '@commercetools/connect-payments-sdk'; import { ConfigResponseSchemaDTO } from '../../dtos/operations/config.dto'; -import { SupportedPaymentComponentsSchemaDTO } from '../../dtos/operations/payment-componets.dto'; import { AmountSchemaDTO, PaymentIntentRequestSchemaDTO, - PaymentIntentResponseSchemaDTO, PaymentModificationStatus, } from '../../dtos/operations/payment-intents.dto'; import { StatusResponseSchemaDTO } from '../../dtos/operations/status.dto'; -import { OperationProcessor } from '../processors/operation.processor'; import { Payment } from '@commercetools/platform-sdk'; export type CapturePaymentRequest = { @@ -38,16 +34,3 @@ export type ModifyPayment = { paymentId: string; data: PaymentIntentRequestSchemaDTO; }; - -export interface OperationService { - getStatus(): Promise; - getConfig(): Promise; - getSupportedPaymentComponents(): Promise; - modifyPayment(opts: ModifyPayment): Promise; -} - -export type OperationServiceOptions = { - operationProcessor: OperationProcessor; - ctCartService: CommercetoolsCartService; - ctPaymentService: CommercetoolsPaymentService; -};