@commercetools/checkout-payments-processor-fastify 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # @commercetools/checkout-payments-processor-sdk
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - af90f7f: Sample change for all the packages
8
+ - Updated dependencies [af90f7f]
9
+ - @commercetools/checkout-payments-processor-sdk@0.0.2
10
+
11
+ ## 0.0.1
12
+
13
+ ### Patch Changes
14
+
15
+ - Initialising repository
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @commercetools/checkout-payments-processor-sdk
2
+
3
+ The Processor Module acts as the middleware between commercetools and the payment service provider, handling transaction processing and payment updates. It handles key operations such as payment authorization, capture, refund, and cancellation directly with the payment service provider while ensuring transactions are processed and saved accurately in commercetools composable commerce.
4
+
5
+ ## Key Responsibilities
6
+ * Receives payment requests from the enabler.
7
+ * Communicates with the PSP for authorization, capture, refund, and cancellation.
8
+ * Updates commercetools payment objects based on PSP responses.
9
+ * Ensures secure and idempotent transaction handling.
10
+
11
+ ## Prerequisites
12
+
13
+ commercetools Checkout has support for two component integration types (payment components and drop-in components)
14
+
15
+ The payment connector requires:
16
+
17
+ * A commercetools Composable Commerce API Client with permissions for:
18
+ * `manage_payments`
19
+ * `manage_orders`
20
+ * `view_sessions`
21
+ * `view_api_clients`
22
+ * `manage_checkout_payment_intents`
23
+ * `introspect_oauth_tokens`
24
+
25
+ * Additional API credentials capable of managing connnector-related operations. This includes tasks like:
26
+ * Installing the connector
27
+ * Deploying and updating the connector
28
+ * Performing connector-related maintenance operations
29
+
30
+ For more details on setting up and managing connectors, refer to the commercetools Connector Deployment Documentation.
31
+
32
+ # Access & Authentication
33
+ The integration uses several authentication methods to ensure secure communication between different components. These include:
34
+
35
+ ## Session Authentication
36
+ The integration will use session-based authentication for frontend-to-backend communication. The session information is passed via the X-Session-Id header, which is validated by the Processor to ensure the request is trusted and associated with the correct session.
37
+
38
+ ## JWT Validation
39
+ JSON Web Tokens (JWT) are used to validate operations performed within the commercetools Merchant Center. The Processor supports JWT validation to authenticate and authorize requests for internal operations.
40
+
41
+ ## OAuth Authentication
42
+ OAuth 2.0 is used for server-to-server communication to manage secure operations such as capture, cancel, and refund of payments. The OAuth credentials are validated via the introspect_oauth_tokens scope to ensure that the Processor has the necessary permissions for these operations.
package/biome.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
3
+ "vcs": {
4
+ "enabled": false,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": false
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false
10
+ },
11
+ "formatter": {
12
+ "enabled": true,
13
+ "indentStyle": "space",
14
+ "indentWidth": 2
15
+ },
16
+ "linter": {
17
+ "enabled": true,
18
+ "rules": {
19
+ "recommended": true,
20
+ "style": {
21
+ "useImportType": "error"
22
+ }
23
+ }
24
+ },
25
+ "javascript": {
26
+ "formatter": {
27
+ "quoteStyle": "single"
28
+ }
29
+ },
30
+ "assist": {
31
+ "enabled": true,
32
+ "actions": {
33
+ "source": {
34
+ "organizeImports": "on"
35
+ }
36
+ }
37
+ }
38
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@commercetools/checkout-payments-processor-fastify",
3
+ "version": "0.0.6",
4
+ "description": "Checkout payments SDK for processor",
5
+ "type": "module",
6
+ "main": "dist/plugin.js",
7
+ "types": "dist/plugin.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "keywords": [],
12
+ "author": "commercetools",
13
+ "license": "ISC",
14
+ "devDependencies": {
15
+ "@biomejs/biome": "2.2.5",
16
+ "@tsconfig/node24": "24.0.1",
17
+ "@types/node": "24.7.0",
18
+ "@vitest/coverage-istanbul": "^3.2.4",
19
+ "tsc-alias": "1.8.16",
20
+ "tsx": "4.20.6",
21
+ "typedoc": "0.28.13",
22
+ "typescript": "5.9.3",
23
+ "vite-tsconfig-paths": "5.1.4",
24
+ "vitest": "3.2.4"
25
+ },
26
+ "dependencies": {
27
+ "@fastify/request-context": "6.2.1",
28
+ "@sinclair/typebox": "0.34.41",
29
+ "fastify": "5.7.3",
30
+ "fastify-plugin": "5.1.0",
31
+ "schema": "link:fastify/types/schema",
32
+ "@commercetools/checkout-payments-processor-sdk": "0.0.6"
33
+ },
34
+ "scripts": {
35
+ "build": "rm -rf ./dist && tsc -p tsconfig.prod.json && tsc-alias",
36
+ "clean": "rm -rf ./dist ./coverage ./node_modules",
37
+ "format": "biome check --write ./src",
38
+ "lint": "biome check ./src",
39
+ "test": "vitest run --coverage",
40
+ "test:watch": "vitest",
41
+ "doc": "typedoc --options typedoc.json"
42
+ }
43
+ }
@@ -0,0 +1,69 @@
1
+ import * as paymentSdk from '@commercetools/checkout-payments-processor-sdk';
2
+ import { requestContext } from '@fastify/request-context';
3
+
4
+ export type ContextData = {
5
+ anonymousId?: string;
6
+ customerId?: string;
7
+ path?: string;
8
+ pathTemplate?: string;
9
+ // biome-ignore lint/suspicious/noExplicitAny: path params is any
10
+ pathParams?: any;
11
+ // biome-ignore lint/suspicious/noExplicitAny: query is any
12
+ query?: any;
13
+ correlationId: string;
14
+ requestId: string;
15
+ authentication?: paymentSdk.Authentication;
16
+ };
17
+
18
+ export const getRequestContext = (): Partial<ContextData> => {
19
+ return requestContext.get('request') ?? {};
20
+ };
21
+
22
+ export const setRequestContext = (ctx: ContextData) => {
23
+ requestContext.set('request', ctx);
24
+ };
25
+
26
+ export const updateRequestContext = (ctx: Partial<ContextData>) => {
27
+ const currentContext = getRequestContext();
28
+ setRequestContext({
29
+ ...(currentContext as ContextData),
30
+ ...ctx,
31
+ });
32
+ };
33
+
34
+ export const getCtSessionIdFromContext = (): string => {
35
+ const contextData = getRequestContext() as ContextData;
36
+ return paymentSdk.getCtSessionIdFromContext(contextData) as string;
37
+ };
38
+
39
+ export const getCartIdFromContext = (): string => {
40
+ const contextData = getRequestContext() as ContextData;
41
+ return paymentSdk.getCartIdFromContext(contextData) as string;
42
+ };
43
+
44
+ export const getAllowedPaymentMethodsFromContext = (): string[] => {
45
+ const contextData = getRequestContext() as ContextData;
46
+ return paymentSdk.getAllowedPaymentMethodsFromContext(
47
+ contextData,
48
+ ) as string[];
49
+ };
50
+
51
+ export const getPaymentInterfaceFromContext = (): string | undefined => {
52
+ const contextData = getRequestContext() as ContextData;
53
+ return paymentSdk.getPaymentInterfaceFromContext(contextData);
54
+ };
55
+
56
+ export const getProcessorUrlFromContext = (): string => {
57
+ const contextData = getRequestContext() as ContextData;
58
+ return paymentSdk.getProcessorUrlFromContext(contextData) as string;
59
+ };
60
+
61
+ export const getMerchantReturnUrlFromContext = (): string | undefined => {
62
+ const contextData = getRequestContext() as ContextData;
63
+ return paymentSdk.getMerchantReturnUrlFromContext(contextData);
64
+ };
65
+
66
+ export const getFutureOrderNumberFromContext = (): string | undefined => {
67
+ const contextData = getRequestContext() as ContextData;
68
+ return paymentSdk.getFutureOrderNumberFromContext(contextData);
69
+ };
@@ -0,0 +1,36 @@
1
+ import { type Static, Type } from '@sinclair/typebox';
2
+
3
+ /**
4
+ * Represents https://docs.commercetools.com/api/errors#errorobject
5
+ */
6
+ export const ErrorObject = Type.Object(
7
+ {
8
+ code: Type.String(),
9
+ message: Type.String(),
10
+ },
11
+ { additionalProperties: true },
12
+ );
13
+
14
+ /**
15
+ * Represents https://docs.commercetools.com/api/errors#errorresponse
16
+ */
17
+ export const ErrorResponse = Type.Object({
18
+ statusCode: Type.Integer(),
19
+ message: Type.String(),
20
+ errors: Type.Array(ErrorObject),
21
+ });
22
+
23
+ /**
24
+ * Represents https://docs.commercetools.com/api/errors#autherrorresponse
25
+ */
26
+ export const AuthErrorResponse = Type.Composite([
27
+ ErrorResponse,
28
+ Type.Object({
29
+ error: Type.String(),
30
+ error_description: Type.Optional(Type.String()),
31
+ }),
32
+ ]);
33
+
34
+ export type TErrorObject = Static<typeof ErrorObject>;
35
+ export type TErrorResponse = Static<typeof ErrorResponse>;
36
+ export type TAuthErrorResponse = Static<typeof AuthErrorResponse>;
@@ -0,0 +1,24 @@
1
+ import { type Static, Type } from '@sinclair/typebox';
2
+
3
+ const DropinType = Type.Enum({
4
+ EMBEDDED: 'embedded',
5
+ HPP: 'hpp',
6
+ });
7
+
8
+ export const SupportedPaymentDropinsData = Type.Object({
9
+ type: DropinType,
10
+ });
11
+
12
+ export const SupportedPaymentComponentsData = Type.Object({
13
+ type: Type.String(),
14
+ subtypes: Type.Optional(Type.Array(Type.String())),
15
+ });
16
+
17
+ export const SupportedPaymentComponentsSchema = Type.Object({
18
+ dropins: Type.Array(SupportedPaymentDropinsData),
19
+ components: Type.Array(SupportedPaymentComponentsData),
20
+ });
21
+
22
+ export type SupportedPaymentComponentsSchemaDTO = Static<
23
+ typeof SupportedPaymentComponentsSchema
24
+ >;
@@ -0,0 +1,74 @@
1
+ import { type Static, Type } from '@sinclair/typebox';
2
+
3
+ export const AmountSchema = Type.Object({
4
+ centAmount: Type.Integer(),
5
+ currencyCode: Type.String(),
6
+ });
7
+
8
+ export const ActionCapturePaymentSchema = Type.Composite([
9
+ Type.Object({
10
+ action: Type.Literal('capturePayment'),
11
+ }),
12
+ Type.Object({
13
+ amount: AmountSchema,
14
+ merchantReference: Type.Optional(Type.String()),
15
+ }),
16
+ ]);
17
+
18
+ export const ActionRefundPaymentSchema = Type.Composite([
19
+ Type.Object({
20
+ action: Type.Literal('refundPayment'),
21
+ }),
22
+ Type.Object({
23
+ amount: AmountSchema,
24
+ merchantReference: Type.Optional(Type.String()),
25
+ transactionId: Type.Optional(Type.String()),
26
+ }),
27
+ ]);
28
+
29
+ export const ActionCancelPaymentSchema = Type.Composite([
30
+ Type.Object({
31
+ action: Type.Literal('cancelPayment'),
32
+ merchantReference: Type.Optional(Type.String()),
33
+ }),
34
+ ]);
35
+
36
+ export const ActionReversePaymentSchema = Type.Composite([
37
+ Type.Object({
38
+ action: Type.Literal('reversePayment'),
39
+ merchantReference: Type.Optional(Type.String()),
40
+ }),
41
+ ]);
42
+
43
+ export const PaymentIntentRequestSchema = Type.Object({
44
+ actions: Type.Array(
45
+ Type.Union([
46
+ ActionCapturePaymentSchema,
47
+ ActionRefundPaymentSchema,
48
+ ActionCancelPaymentSchema,
49
+ ActionReversePaymentSchema,
50
+ ]),
51
+ {
52
+ maxItems: 1,
53
+ },
54
+ ),
55
+ });
56
+
57
+ export enum PaymentModificationStatus {
58
+ APPROVED = 'approved',
59
+ REJECTED = 'rejected',
60
+ RECEIVED = 'received',
61
+ }
62
+ const PaymentModificationSchema = Type.Enum(PaymentModificationStatus);
63
+
64
+ export const PaymentIntentResponseSchema = Type.Object({
65
+ outcome: PaymentModificationSchema,
66
+ });
67
+
68
+ export type PaymentIntentRequestSchemaDTO = Static<
69
+ typeof PaymentIntentRequestSchema
70
+ >;
71
+ export type PaymentIntentResponseSchemaDTO = Static<
72
+ typeof PaymentIntentResponseSchema
73
+ >;
74
+ export type AmountSchemaDTO = Static<typeof AmountSchema>;
@@ -0,0 +1,18 @@
1
+ import { type Static, Type } from '@sinclair/typebox';
2
+
3
+ export const StatusResponseSchema = Type.Object({
4
+ status: Type.String(),
5
+ timestamp: Type.String(),
6
+ version: Type.String(),
7
+ metadata: Type.Optional(Type.Any()),
8
+ checks: Type.Array(
9
+ Type.Object({
10
+ name: Type.String(),
11
+ status: Type.String(),
12
+ details: Type.Optional(Type.Any()),
13
+ message: Type.Optional(Type.String()),
14
+ }),
15
+ ),
16
+ });
17
+
18
+ export type StatusResponseSchemaDTO = Static<typeof StatusResponseSchema>;
@@ -0,0 +1,3 @@
1
+ test('dummy test', () => {
2
+ expect(true).toBe(true);
3
+ });
@@ -0,0 +1,149 @@
1
+ import type { Logger } from '@commercetools/checkout-payments-processor-sdk';
2
+ import {
3
+ ErrorAuthErrorResponse,
4
+ ErrorGeneral,
5
+ ErrorInvalidField,
6
+ ErrorInvalidJsonInput,
7
+ ErrorRequiredField,
8
+ Errorx,
9
+ MultiErrorx,
10
+ } from '@commercetools/checkout-payments-processor-sdk';
11
+ import type { FastifyError, FastifyReply, FastifyRequest } from 'fastify';
12
+ import type { FastifySchemaValidationError } from 'fastify/types/schema.js';
13
+ import type {
14
+ TAuthErrorResponse,
15
+ TErrorObject,
16
+ TErrorResponse,
17
+ } from './dtos/error.dto.js';
18
+
19
+ export const errorHandler = ({ logger }: { logger: Logger }) => {
20
+ function isFastifyValidationError(error: Error): error is FastifyError {
21
+ return (error as unknown as FastifyError).validation !== undefined;
22
+ }
23
+
24
+ const handleAuthError = (
25
+ error: ErrorAuthErrorResponse,
26
+ reply: FastifyReply,
27
+ ) => {
28
+ const transformedErrors: TErrorObject[] = transformErrorxToHTTPModel([
29
+ error,
30
+ ]);
31
+
32
+ const response: TAuthErrorResponse = {
33
+ message: error.message,
34
+ statusCode: error.httpErrorStatus,
35
+ errors: transformedErrors,
36
+ error: transformedErrors[0].code,
37
+ error_description: transformedErrors[0].message,
38
+ };
39
+
40
+ return reply.code(error.httpErrorStatus).send(response);
41
+ };
42
+
43
+ const handleErrors = (errorxList: Errorx[], reply: FastifyReply) => {
44
+ const transformedErrors: TErrorObject[] =
45
+ transformErrorxToHTTPModel(errorxList);
46
+
47
+ // 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.
48
+ const response: TErrorResponse = {
49
+ message: errorxList[0].message,
50
+ statusCode: errorxList[0].httpErrorStatus,
51
+ errors: transformedErrors,
52
+ };
53
+
54
+ return reply.code(errorxList[0].httpErrorStatus).send(response);
55
+ };
56
+
57
+ const transformErrorxToHTTPModel = (errors: Errorx[]): TErrorObject[] => {
58
+ const errorObjectList: TErrorObject[] = [];
59
+
60
+ for (const err of errors) {
61
+ if (err.skipLog) {
62
+ logger.debug({ err }, err.message);
63
+ } else {
64
+ logger.error({ err }, err.message);
65
+ }
66
+
67
+ const tErrObj: TErrorObject = {
68
+ code: err.code,
69
+ message: err.message,
70
+ ...(err.fields ? err.fields : {}), // Add any additional field to the response object (which will differ per type of error)
71
+ };
72
+
73
+ errorObjectList.push(tErrObj);
74
+ }
75
+
76
+ return errorObjectList;
77
+ };
78
+
79
+ const transformValidationErrors = (
80
+ errors: FastifySchemaValidationError[],
81
+ req: FastifyRequest,
82
+ ): Errorx[] => {
83
+ const errorxList: Errorx[] = [];
84
+
85
+ for (const err of errors) {
86
+ switch (err.keyword) {
87
+ case 'required':
88
+ errorxList.push(
89
+ new ErrorRequiredField(err.params.missingProperty as string),
90
+ );
91
+ break;
92
+ case 'enum':
93
+ errorxList.push(
94
+ new ErrorInvalidField(
95
+ getKeys(err.instancePath).join('.'),
96
+ getPropertyFromPath(err.instancePath, req.body),
97
+ err.params.allowedValues as string,
98
+ ),
99
+ );
100
+ break;
101
+ }
102
+ }
103
+
104
+ // If we cannot map the validation error to a CoCo error then return a general InvalidJsonError
105
+ if (errorxList.length === 0) {
106
+ errorxList.push(new ErrorInvalidJsonInput());
107
+ }
108
+
109
+ return errorxList;
110
+ };
111
+
112
+ const getKeys = (path: string) => path.replace(/^\//, '').split('/');
113
+
114
+ // biome-ignore lint/suspicious/noExplicitAny: obj is any
115
+ const getPropertyFromPath = (path: string, obj: any): any => {
116
+ const keys = getKeys(path);
117
+ let value = obj;
118
+ for (const key of keys) {
119
+ value = value[key];
120
+ }
121
+ return value;
122
+ };
123
+
124
+ return (error: Error, req: FastifyRequest, reply: FastifyReply) => {
125
+ if (isFastifyValidationError(error) && error.validation) {
126
+ return handleErrors(
127
+ transformValidationErrors(error.validation, req),
128
+ reply,
129
+ );
130
+ } else if (error instanceof ErrorAuthErrorResponse) {
131
+ return handleAuthError(error, reply);
132
+ } else if (error instanceof Errorx) {
133
+ return handleErrors([error], reply);
134
+ } else if (error instanceof MultiErrorx) {
135
+ return handleErrors(error.errors, reply);
136
+ }
137
+
138
+ // 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
139
+ return handleErrors(
140
+ [
141
+ new ErrorGeneral('Internal server error.', {
142
+ cause: error,
143
+ skipLog: false,
144
+ }),
145
+ ],
146
+ reply,
147
+ );
148
+ };
149
+ };
@@ -0,0 +1,23 @@
1
+ import '@fastify/request-context';
2
+ import {
3
+ ContextData,
4
+ SessionContextData,
5
+ } from './libs/fastify/context/context';
6
+
7
+ declare module '@fastify/request-context' {
8
+ interface RequestContextData {
9
+ request: ContextData;
10
+ session?: SessionContextData;
11
+ }
12
+ }
13
+
14
+ declare module 'fastify' {
15
+ interface FastifyInstance {
16
+ // biome-ignore lint/suspicious/noExplicitAny: vite is any
17
+ vite: any;
18
+ }
19
+
20
+ export interface FastifyRequest {
21
+ correlationId?: string;
22
+ }
23
+ }
package/src/ip/ip.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import fp from 'fastify-plugin';
3
+
4
+ const HEADERS_ORDER = [
5
+ 'x-client-ip', // Most common
6
+ 'x-forwarded-for', // Mostly used by proxies
7
+ 'cf-connecting-ip', // Cloudflare
8
+ 'Cf-Pseudo-IPv4', // Cloudflare
9
+ 'fastly-client-ip',
10
+ 'true-client-ip', // Akamai and Cloudflare
11
+ 'x-real-ip', // Nginx
12
+ 'x-cluser-client-ip', // Rackspace LB
13
+ 'forwarded-for',
14
+ 'x-forwarded',
15
+ 'forwarded',
16
+ 'x-appengine-user-ip', // GCP App Engine
17
+ ];
18
+
19
+ type HeaderKey = (typeof HEADERS_ORDER)[number];
20
+
21
+ declare module 'fastify' {
22
+ interface FastifyRequest {
23
+ clientIp: string;
24
+ }
25
+ }
26
+
27
+ function extractIpFromHeader(
28
+ value: string | string[] | undefined,
29
+ ): string | null {
30
+ if (!value) return null;
31
+ if (Array.isArray(value)) value = value[0];
32
+ return value.split(',')[0].trim();
33
+ }
34
+
35
+ export const requestIpPlugin = fp(async (fastify: FastifyInstance) => {
36
+ fastify.decorateRequest('clientIp', '');
37
+
38
+ fastify.addHook('onRequest', async (request) => {
39
+ let ip = '';
40
+
41
+ for (const header of HEADERS_ORDER) {
42
+ const value = request.headers[header as HeaderKey] as string | undefined;
43
+ const extracted = extractIpFromHeader(value);
44
+ if (extracted) {
45
+ ip = extracted;
46
+ break;
47
+ }
48
+ }
49
+
50
+ if (!ip) {
51
+ ip = request.ip;
52
+ }
53
+
54
+ request.clientIp = ip;
55
+ });
56
+ });
package/src/plugin.ts ADDED
@@ -0,0 +1,201 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type {
3
+ AuthType,
4
+ CheckoutPaymentsProcessor,
5
+ PaymentComponentsHandler,
6
+ PaymentComponentsIncomingHttpHeaders,
7
+ PaymentComponentsRequest,
8
+ PaymentComponentsResponse,
9
+ PaymentIntentsHandler,
10
+ PaymentIntentsRequest,
11
+ PaymentIntentsResponse,
12
+ StatusHandler,
13
+ StatusIncomingHttpHeaders,
14
+ StatusRequest,
15
+ StatusResponse,
16
+ TransactionHandler,
17
+ TransactionIncomingHttpHeaders,
18
+ TransactionRequest,
19
+ TransactionResponse,
20
+ } from '@commercetools/checkout-payments-processor-sdk';
21
+
22
+ import fastifyRequestContext from '@fastify/request-context';
23
+ import { Type } from '@sinclair/typebox';
24
+ import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';
25
+ import { SupportedPaymentComponentsSchema } from './dtos/payment-componets.dto.js';
26
+ import {
27
+ PaymentIntentRequestSchema,
28
+ PaymentIntentResponseSchema,
29
+ } from './dtos/payment-intents.dto.js';
30
+ import { StatusResponseSchema } from './dtos/status.dto.js';
31
+ import { errorHandler } from './error-handler.js';
32
+
33
+ interface CheckoutPaymentsProcessorFastifyPluginOptions {
34
+ processorSDK: CheckoutPaymentsProcessor;
35
+ paymentComponentsHandler: PaymentComponentsHandler;
36
+ paymentIntentsHandler: PaymentIntentsHandler;
37
+ statusHandler: StatusHandler;
38
+ transactionHandler: TransactionHandler;
39
+ }
40
+
41
+ type HookFn<Request, Response> = (
42
+ request: FastifyRequest<{ Body: Request; Reply: Response }>,
43
+ reply: FastifyReply,
44
+ ) => Promise<void>;
45
+
46
+ export const CheckoutPaymentsProcessorFastifyPluginOptions: FastifyPluginAsync<
47
+ CheckoutPaymentsProcessorFastifyPluginOptions
48
+ > = async (fastify, options) => {
49
+ const {
50
+ processorSDK,
51
+ statusHandler,
52
+ paymentComponentsHandler,
53
+ paymentIntentsHandler,
54
+ transactionHandler,
55
+ } = options;
56
+
57
+ fastify.setErrorHandler(errorHandler({ logger: processorSDK.getLogger() }));
58
+
59
+ fastify.decorateRequest('correlationId', '');
60
+
61
+ // Propagate the correlationId from the request header to the request object
62
+ fastify.addHook('onRequest', (req, _reply, done) => {
63
+ req.correlationId = req.headers['x-correlation-id']
64
+ ? (req.headers['x-correlation-id'] as string)
65
+ : undefined;
66
+ done();
67
+ });
68
+
69
+ // Register the request context
70
+ await fastify.register(fastifyRequestContext, {
71
+ defaultStoreValues: (req: FastifyRequest) => ({
72
+ request: {
73
+ path: req.url,
74
+ pathTemplate: req.routeOptions.url,
75
+ pathParams: req.params,
76
+ query: req.query,
77
+ correlationId: req.correlationId || randomUUID().toString(),
78
+ requestId: req.id,
79
+ },
80
+ }),
81
+ hook: 'onRequest',
82
+ });
83
+
84
+ const preHandlersMapper = <Request, Response>(
85
+ auths: AuthType[],
86
+ ): HookFn<Request, Response>[] => {
87
+ const result: HookFn<Request, Response>[] = [];
88
+ for (const auth of auths) {
89
+ if (auth.type === 'jwt') {
90
+ result.push(processorSDK.getJwtAuthHookFn().authenticate());
91
+ } else if (auth.type === 'oauth2') {
92
+ result.push(processorSDK.getOauth2AuthHookFn().authenticate());
93
+ result.push(
94
+ processorSDK
95
+ .getAuthorityAuthorizationHookFn()
96
+ .authorize(...auth.allowedScopes),
97
+ );
98
+ } else if (auth.type === 'session') {
99
+ result.push(processorSDK.getSessionHeaderAuthHookFn().authenticate());
100
+ }
101
+ }
102
+ return result;
103
+ };
104
+
105
+ fastify.route<{ Body: StatusRequest; Reply: StatusResponse }>({
106
+ method: statusHandler.method(),
107
+ url: statusHandler.endpoint(),
108
+ preHandler: preHandlersMapper<StatusRequest, StatusResponse>(
109
+ statusHandler.auth(),
110
+ ),
111
+ schema: {
112
+ response: {
113
+ 200: StatusResponseSchema,
114
+ },
115
+ },
116
+ handler: async (request, reply) => {
117
+ const response = await statusHandler.handler({
118
+ request: request.body as StatusRequest,
119
+ headers: request.headers as StatusIncomingHttpHeaders,
120
+ });
121
+ reply.code(200).send(response.body);
122
+ },
123
+ });
124
+
125
+ fastify.route<{
126
+ Body: PaymentIntentsRequest;
127
+ Reply: PaymentIntentsResponse;
128
+ Params: { id: string };
129
+ }>({
130
+ method: paymentIntentsHandler.method(),
131
+ url: paymentIntentsHandler.endpoint(),
132
+ preHandler: preHandlersMapper<
133
+ PaymentIntentsRequest,
134
+ PaymentIntentsResponse
135
+ >(paymentIntentsHandler.auth()),
136
+ schema: {
137
+ params: {
138
+ $id: 'paramsSchema',
139
+ type: 'object',
140
+ properties: {
141
+ id: Type.String(),
142
+ },
143
+ required: ['id'],
144
+ },
145
+ body: PaymentIntentRequestSchema,
146
+ response: {
147
+ 200: PaymentIntentResponseSchema,
148
+ },
149
+ },
150
+ handler: async (request, reply) => {
151
+ const { id } = request.params;
152
+ const body = request.body as PaymentIntentsRequest;
153
+ const response = await paymentIntentsHandler.handler({
154
+ request: {
155
+ id,
156
+ actions: body.actions,
157
+ },
158
+ });
159
+ reply.code(200).send(response.body);
160
+ },
161
+ });
162
+
163
+ fastify.route<{
164
+ Body: PaymentComponentsRequest;
165
+ Reply: PaymentComponentsResponse;
166
+ }>({
167
+ method: paymentComponentsHandler.method(),
168
+ url: paymentComponentsHandler.endpoint(),
169
+ preHandler: preHandlersMapper<
170
+ PaymentComponentsRequest,
171
+ PaymentComponentsResponse
172
+ >(paymentComponentsHandler.auth()),
173
+ schema: {
174
+ response: {
175
+ 200: SupportedPaymentComponentsSchema,
176
+ },
177
+ },
178
+ handler: async (request, reply) => {
179
+ const response = await paymentComponentsHandler.handler({
180
+ request: request.body as PaymentComponentsRequest,
181
+ headers: request.headers as PaymentComponentsIncomingHttpHeaders,
182
+ });
183
+ reply.code(200).send(response.body);
184
+ },
185
+ });
186
+
187
+ fastify.route<{ Body: TransactionRequest; Reply: TransactionResponse }>({
188
+ method: transactionHandler.method(),
189
+ url: transactionHandler.endpoint(),
190
+ preHandler: preHandlersMapper<TransactionRequest, TransactionResponse>(
191
+ transactionHandler.auth(),
192
+ ),
193
+ handler: async (request, reply) => {
194
+ const response = await transactionHandler.handler({
195
+ request: request.body as TransactionRequest,
196
+ headers: request.headers as TransactionIncomingHttpHeaders,
197
+ });
198
+ reply.code(response.status).send(response.body);
199
+ },
200
+ });
201
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "@tsconfig/node24/tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["vitest/globals"],
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "paths": {
10
+ "@/*": ["./src/*"],
11
+ }
12
+ },
13
+ "exclude": ["src/**/*.[test|spec|mock].ts"],
14
+ "include": ["src/**/*.ts"]
15
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "declarationMap": false
6
+ },
7
+ "exclude": ["./src/**/*.test.ts", "./src/**/*.spec.ts"],
8
+ "include": ["./src/**/*.ts"]
9
+ }
package/typedoc.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "entryPoints": ["src/index.ts"],
3
+ "out": "../../generated-doc/checkout-payments-processor-sdk",
4
+ "tsconfig": "tsconfig.json",
5
+ "projectDocuments": ["wiki/*.md", "CHANGELOG.md"]
6
+ }
@@ -0,0 +1,15 @@
1
+ import tsconfigPaths from "vite-tsconfig-paths";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ export default defineConfig({
5
+ plugins: [tsconfigPaths()],
6
+ test: {
7
+ globals: true,
8
+ coverage: {
9
+ provider: "istanbul", // or 'v8'
10
+ reporter: ["text", "json-summary"],
11
+ exclude: ["./generated-doc/**/*", "./dist/**/*", "./src/**/*.test.ts", "./src/**/*.spec.ts"],
12
+ },
13
+ include: ["./src/**/*.test.ts", "./src/**/*.spec.ts"],
14
+ },
15
+ });
@@ -0,0 +1,13 @@
1
+ ---
2
+ title: Getting started
3
+ group: Documents
4
+ category: Guides
5
+ ---
6
+
7
+ # Payment enabler
8
+
9
+ ```bash
10
+ npm install @commercerools/checkout-payments-enabler-sdk
11
+ ```
12
+
13
+