Skip to content

Commit

Permalink
Merge branch 'main' of github.com:mollie/commercetools-connector
Browse files Browse the repository at this point in the history
  • Loading branch information
Tung-Huynh-Shopmacher committed Jul 30, 2024
2 parents 97c535f + 188293d commit f01a6de
Show file tree
Hide file tree
Showing 33 changed files with 1,409 additions and 180 deletions.
4 changes: 4 additions & 0 deletions connect.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ deployAs:
description: Commercetools Composable Commerce API region
required: true
default: "europe-west1.gcp"
- key: ENABLE_MOLLIE_CARD_COMPONENT
description: Enable Mollie cart component (0 or 1)
required: true
default: "0"
- key: DEBUG
description: Debug mode (0 or 1)
required: false
Expand Down
146 changes: 146 additions & 0 deletions docs/CancelPaymentRefund.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Cancel Order

* [Parameters map](#parameters-map)
* [Representation: CT Payment](#representation-ct-payment)
* [Creating CommerceTools actions from Mollie's response](#creating-commercetools-actions-from-mollies-response)

## Overview
This functionality is used to cancel the pending refund which means it is created but not complete yet.

The target Mollie endpoint will be [Cancel Payment Refund](https://docs.mollie.com/reference/cancel-refund).

<br />

## Conditions

In order to use this functionality, the customer must have a charged-successfully payment and a refund created which is in-progress for that payment.
Technically, the CommerceTools Payment object needs to include 3 transactions:
- 1 transaction with type = `Charge`, state = `Success`. This transaction should also store the targeted Mollie Payment ID in `interactionId`.
- 1 transaction with type = `Refund`, state = `Pending`. This transaction should also store the targeted Mollie Refund ID in `interactionId`.
- 1 transaction with type = `CancelAuthorization`, state = `Initial`. This transaction is to point out that the customer is wanting to cancel the Refund.

<br />

## Parameters map

Target endpoint: `https://api.mollie.com/v2/payments/{paymentId}/refunds/{id}`

| CT SuccessCharge transaction | Parameter | Required |
|---------------------------------------------|---------------------------------------------|----------|
| `interactionId` | `paymentId` | YES |

| CT PendingRefund transaction | Parameter | Required |
|---------------------------------------------|---------------------------------------------|----------|
| `interactionId` | `id` | YES |

<br />

## Connector process

- Detect if the CommerceTools Payment object has satisfied the [conditions](#conditions) above
- If no, the connector should return success response an empty body (no updated actions)
- If yes:
- The connector will get the Refund ID from PendingRefund transaction, use it query to [Mollie Get Payment Refund API](#https://docs.mollie.com/reference/get-refund)
- Then, it will check whether the Refund status is `queued` or `pending`. If it is not `queued` nor `pending`, the connector will return error response along with some details message will be save into the App-Log
- If the Refund status is `queued` or `pending`, the connector will perform a call to the [Cancel Refund endpoint](https://docs.mollie.com/reference/cancel-refund) to cancel the refund.
- And finally, the connector will return a success response with a list of necessary updated actions including:
- Change PendingRefund transaction state from `Pending` to `Failure`
- Update PendingRefund transaction custom field `sctm_payment_cancel_refund`: store the reason of the cancelling from shop side, and a fixed message to point out that the cancelling was coming from the shop side

## Representation: CT Payment

<details>
<summary>Example Payment with initial CancelAuthorization transaction</summary>

```json
{
"id": "c0887a2d-bfbf-4f77-8f3d-fc33fb4c0920",
"version": 7,
"lastMessageSequenceNumber": 4,
"createdAt": "2021-12-16T08:21:02.813Z",
"lastModifiedAt": "2021-12-16T08:22:28.979Z",
"lastModifiedBy": {
"clientId": "A-7gCPuzUQnNSdDwlOCC",
"isPlatformClient": false
},
"createdBy": {
"clientId": "A-7gCPuzUQnNSdDwlOCC",
"isPlatformClient": false
},
"key": "ord_5h2f3w",
"amountPlanned": {
"type": "centPrecision",
"currencyCode": "EUR",
"centAmount": 1604,
"fractionDigits": 2
},
"paymentMethodInfo": {
"paymentInterface": "Mollie",
"method": "ideal"
},
"custom": {
"type": {
"typeId": "type",
"id": "c11764fa-4e07-4cc0-ba40-e7dfc8d67b4e"
},
"fields": {
"createPayment": "{\"redirectUrl\":\"https://www.redirect.com/\",\"webhookUrl\":\"https://webhook.com\",\"locale\":\"nl_NL\"}"
}
},
"paymentStatus": {},
"transactions": [
{
"id": "869ea4f0-b9f6-4006-bf04-d8306b5c9564",
"type": "Charge",
"interactionId": "tr_7UhSN1zuXS",
"amount": {
"type": "centPrecision",
"currencyCode": "EUR",
"centAmount": 1604,
"fractionDigits": 2
},
"state": "Success"
},
{
"id": "869ea4f0-b9f6-4006-bf04-d8306b5c1234",
"type": "Refund",
"interactionId": "re_4qqhO89gsT",
"amount": {
"type": "centPrecision",
"currencyCode": "EUR",
"centAmount": 1604,
"fractionDigits": 2
},
"state": "Pending",
"custom": {
"type": {
"key": "sctm_payment_cancel_refund"
},
"fields": {
"reasonText": "Cancel refund reason"
}
}
},
{
"id": "ad199f53-09be-43a5-ae73-aa97248239ad",
"type": "CancelAuthorization",
"amount": {
"centAmount": 1604,
"currencyCode": "EUR"
},
"state": "Initial"
}
],
}
```
</details>
<br />

## Creating CommerceTools actions from Mollie's response

When order is successfully cancelled on Mollie, we update commercetools payment with following actions

| Action name (CT) | Value |
| -------------------------------- | -------------------------------------------------------------------------- |
| `changeTransactionState` | `transactionId: <pendingRefundTransactionId>, state: 'Failure'` |
| `setTransactionCustomField` | `transactionId: <pendingRefundTransactionId>, name:sctm_payment_cancel_refund, value: "{\"reasonText\":\"Cancel refund reason\",\"statusText\":\"Cancel refund status\"}"` |
1 change: 1 addition & 0 deletions processor/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ CTP_API_URL=https://api.<YOUR_CTP_REGION>.commercetools.com
DEBUG=<INTEGER_VALUE> ## Either 1 for enable or 0 for disable
MOLLIE_API_KEY=<YOUR_MOLLIE_API_KEY>
MOLLIE_PROFILE_ID=<YOUR_MOLLIE_PROFILE_ID>
ENABLE_MOLLIE_CARD_COMPONENT=0 ## Either 1 for enable or 0 for disable

## NGROK
CONNECTOR_EXTENSION_TOKEN=<YOUR_NGROK_AUTH_TOKEN>
1 change: 1 addition & 0 deletions processor/.env.jest
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ CTP_REGION=europe-west1.gcp
MOLLIE_API_KEY=12345678901234567890123456789012
MOLLIE_PROFILE_ID=pfl_12345
DEBUG=0
ENABLE_MOLLIE_CARD_COMPONENT=0

CONNECT_SERVICE_URL=http://localhost:3000/processor
2 changes: 1 addition & 1 deletion processor/bin/ngrok.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ fi

# Start NGROK in background
echo "⚡️ Starting ngrok"
ngrok http 8889 --authtoken ${CONNECTOR_EXTENSION_TOKEN} > /dev/null &
ngrok http 8080 --authtoken ${CONNECTOR_EXTENSION_TOKEN} > /dev/null &

# Wait for ngrok to be available
while ! nc -z localhost 4040; do
Expand Down
4 changes: 2 additions & 2 deletions processor/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion processor/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "shopmacher-mollie-processor",
"description": "Integration between commercetools and mollie payment service provider",
"version": "0.0.18",
"version": "0.0.19",
"main": "index.js",
"private": true,
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions processor/src/commercetools/customFields.commercetools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,10 @@ export async function createCustomPaymentInterfaceInteractionType(): Promise<voi
}
}

export async function createCustomPaymentTransactionCancelRefundType(): Promise<void> {
export async function createCustomPaymentTransactionCancelReasonType(): Promise<void> {
const apiRoot = createApiRoot();

const customFieldName = CustomFields.paymentCancelRefund;
const customFieldName = CustomFields.paymentCancelReason;

const {
body: { results: types },
Expand Down
4 changes: 2 additions & 2 deletions processor/src/connector/post-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { assertError, assertString } from '../utils/assert.utils';
import { createPaymentExtension } from '../commercetools/extensions.commercetools';
import {
createCustomPaymentInterfaceInteractionType,
createCustomPaymentTransactionCancelRefundType,
createCustomPaymentTransactionCancelReasonType,
createCustomPaymentType,
} from '../commercetools/customFields.commercetools';

Expand All @@ -19,7 +19,7 @@ async function postDeploy(properties: Map<string, unknown>): Promise<void> {
await createPaymentExtension(applicationUrl);
await createCustomPaymentType();
await createCustomPaymentInterfaceInteractionType();
await createCustomPaymentTransactionCancelRefundType();
await createCustomPaymentTransactionCancelReasonType();
}

async function run(): Promise<void> {
Expand Down
3 changes: 3 additions & 0 deletions processor/src/controllers/payment.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { determinePaymentAction } from '../utils/paymentAction.utils';
import { ControllerResponseType } from '../types/controller.types';
import {
handleCancelPayment,
handleCreatePayment,
handleCreateRefund,
handleListPaymentMethodsByPayment,
Expand Down Expand Up @@ -47,6 +48,8 @@ export const paymentController = async (
case ConnectorActions.CreatePayment:
logger.debug('SCTM - payment processing - paymentController - handleCreatePayment');
return await handleCreatePayment(ctPayment);
case ConnectorActions.CancelPayment:
return await handleCancelPayment(ctPayment);
case ConnectorActions.CreateRefund:
logger.debug('SCTM - payment processing - paymentController - handleCreateRefund');
return await handleCreateRefund(ctPayment);
Expand Down
6 changes: 3 additions & 3 deletions processor/src/errors/mollie.error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { CTError, CTEnumErrors, CTErrorExtensionExtraInfo } from '../types/comme

// This is based on MollieApiError interface from Mollie's SDK
/* eslint-disable @typescript-eslint/no-explicit-any */
const getExtraInfo = ({ status, statusCode, links, title, field }: any): CTErrorExtensionExtraInfo => {
const orginalStatus = status || statusCode;
export const getExtraInfo = ({ status, statusCode, links, title, field }: any): CTErrorExtensionExtraInfo => {
const originalStatus = status || statusCode;
const extraInfo = Object.assign(
{},
orginalStatus && { originalStatusCode: orginalStatus },
originalStatus && { originalStatusCode: originalStatus },
links && { links },
title && { title },
field && { field },
Expand Down
18 changes: 18 additions & 0 deletions processor/src/mollie/payment.mollie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,21 @@ export const listPaymentMethods = async (options: MethodsListParams): Promise<Li
throw new CustomError(400, errorMessage);
}
};

export const cancelPayment = async (paymentId: string): Promise<Payment> => {
try {
return await initMollieClient().payments.cancel(paymentId);
} catch (error: unknown) {
let errorMessage;
if (error instanceof MollieApiError) {
errorMessage = `SCTM - cancelPayment - error: ${error.message}, field: ${error.field}`;
} else {
errorMessage = `SCTM - cancelPayment - Failed to cancel payments with unknown errors`;
}

logger.error(errorMessage, {
error,
});
throw new CustomError(400, errorMessage);
}
};
49 changes: 44 additions & 5 deletions processor/src/mollie/refund.mollie.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
CancelParameters,
CreateParameters,
GetParameters,
} from '@mollie/api-client/dist/types/src/binders/payments/refunds/parameters';
import { initMollieClient } from '../client/mollie.client';
import { MollieApiError } from '@mollie/api-client';
Expand All @@ -15,30 +16,68 @@ export const createPaymentRefund = async (params: CreateParameters): Promise<Ref
let errorMessage;

if (error instanceof MollieApiError) {
errorMessage = `SCTM - createMolliePaymentRefund - Calling Mollie API - error: ${error.message}`;
errorMessage = `SCTM - createPaymentRefund - Calling Mollie API - error: ${error.message}`;
} else {
errorMessage = `SCTM - createMolliePaymentRefund - Calling Mollie API - Failed to create refund with unknown errors`;
errorMessage = `SCTM - createPaymentRefund - Calling Mollie API - Failed to create refund with unknown errors`;
}

logger.error(errorMessage, {
paymentId: params.paymentId,
molliePaymentId: params.paymentId,
error,
});

throw new CustomError(400, errorMessage);
}
};

/**
* Retrieves a payment refund from the Mollie API.
*
* @param {string} refundId - The ID of the refund.
* @param {GetParameters} params - The parameters for the refund.
* @return {Promise<PaymentRefund>} A promise that resolves to the payment refund.
* @throws {CustomError} If there is an error retrieving the refund.
*/
export const getPaymentRefund = async (refundId: string, params: GetParameters) => {
try {
return await initMollieClient().paymentRefunds.get(refundId, params);
} catch (error: unknown) {
let errorMessage;

if (error instanceof MollieApiError) {
errorMessage = `SCTM - getPaymentRefund - Calling Mollie API - error: ${error.message}`;
} else {
errorMessage = `SCTM - getPaymentRefund - Calling Mollie API - Failed to cancel the refund with unknown errors`;
}

logger.error(errorMessage, {
molliePaymentId: params.paymentId,
mollieRefundId: refundId,
error,
});

throw new CustomError(400, errorMessage);
}
};

/**
* Cancels a payment refund by its ID using the provided parameters.
*
* @param {string} refundId - The ID of the payment refund to cancel.
* @param {CancelParameters} params - The parameters for cancelling the payment refund.
* @return {Promise<boolean>} A promise that resolves to true if the payment refund is successfully cancelled, or rejects with an error.
* @throws {CustomError} If there is an error cancelling the payment refund.
*/
export const cancelPaymentRefund = async (refundId: string, params: CancelParameters): Promise<boolean> => {
try {
return await initMollieClient().paymentRefunds.cancel(refundId, params);
} catch (error: unknown) {
let errorMessage;

if (error instanceof MollieApiError) {
errorMessage = `SCTM - cancelMolliePaymentRefund - Calling Mollie API - error: ${error.message}`;
errorMessage = `SCTM - cancelPaymentRefund - Calling Mollie API - error: ${error.message}`;
} else {
errorMessage = `SCTM - cancelMolliePaymentRefund - Calling Mollie API - Failed to cancel the refund with unknown errors`;
errorMessage = `SCTM - cancelPaymentRefund - Calling Mollie API - Failed to cancel the refund with unknown errors`;
}

logger.error(errorMessage, {
Expand Down
Loading

0 comments on commit f01a6de

Please sign in to comment.