diff --git a/CHANGELOG.md b/CHANGELOG.md index d462063..aede09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,35 @@ Added - Mollie custom application initialization +Updated + +- [getPaymentMethods](/docs/GetPaymentMethods.md) response has new returned format as follow + + ```Typescript + { + id: string, + name: Record + description: Record + image: string; + order: number; + } + + // e.g. + { + id: 'paypal', + name: { + 'en-GB': 'PayPal', + 'de-DE': 'PayPal', + }, + description: { + 'en-GB': '', + 'de-DE': '', + }, + image: 'https://example.img/paypal.svg', + order: 1 + } + ``` + ## v1.1.2 Added diff --git a/application/custom-application-config.ts b/application/custom-application-config.ts index 37fc7b4..95fe782 100644 --- a/application/custom-application-config.ts +++ b/application/custom-application-config.ts @@ -15,7 +15,7 @@ const config = { cloudIdentifier: CLOUD_IDENTIFIER, env: { development: { - initialProjectKey: 'shopm-adv-dev', + initialProjectKey: 'shopm-adv-windev', }, production: { applicationId: CUSTOM_APPLICATION_ID, diff --git a/application/cypress.config.ts b/application/cypress.config.ts index c229b72..de59fba 100644 --- a/application/cypress.config.ts +++ b/application/cypress.config.ts @@ -28,6 +28,7 @@ export default defineConfig({ LOGIN_PASSWORD: process.env.CYPRESS_LOGIN_PASSWORD, PROJECT_KEY: process.env.CYPRESS_PROJECT_KEY, PACKAGE_NAME: process.env.CYPRESS_PACKAGE_NAME, + LOCALE: process.env.CYPRESS_LOCALE || 'en-GB', }, }; }, diff --git a/application/cypress/.env.example b/application/cypress/.env.example index 7062bf0..1e18dc4 100644 --- a/application/cypress/.env.example +++ b/application/cypress/.env.example @@ -2,4 +2,5 @@ CYPRESS_LOGIN_USER= CYPRESS_LOGIN_PASSWORD= CYPRESS_PROJECT_KEY= CYPRESS_PACKAGE_NAME="application" -CYPRESS_BASE_URL="https://mc.europe-west1.gcp.commercetools.com/" \ No newline at end of file +CYPRESS_BASE_URL="https://mc.europe-west1.gcp.commercetools.com/" +CYPRESS_LOCALE="en-GB" \ No newline at end of file diff --git a/application/cypress/e2e/method-details.cy.ts b/application/cypress/e2e/method-details.cy.ts new file mode 100644 index 0000000..069e4b8 --- /dev/null +++ b/application/cypress/e2e/method-details.cy.ts @@ -0,0 +1,60 @@ +/// +/// +/// + +import { + entryPointUriPath, + APPLICATION_BASE_ROUTE, +} from '../support/constants'; +beforeEach(() => { + cy.loginToMerchantCenter({ + entryPointUriPath, + initialRoute: APPLICATION_BASE_ROUTE, + }); + + cy.fixture('forward-to').then((response) => { + cy.intercept('GET', '/proxy/forward-to', { + statusCode: 200, + body: response, + }); + }); +}); + +describe('Test welcome.cy.ts', () => { + it('should render method details page', () => { + const LOCALE = Cypress.env('LOCALE'); + const paymentMethods = [ + 'PayPal', + 'iDEAL Pay in 3 instalments, 0% interest', + 'iDEAL', + 'Bancontact', + 'Blik', + ]; + + cy.findByText(paymentMethods[0]).click(); + cy.url().should('contain', 'general'); + + cy.findByTestId('status-select').should('exist'); + + cy.findByTestId(`name-input-${LOCALE}`).should('exist'); + cy.findByTestId(`description-input-${LOCALE}`).should('exist'); + cy.findByTestId(`display-order-input`).should('exist'); + }); + + it('should update display order successfully', () => { + const paymentMethodIds = ['paypal', 'ideal', 'bancontact']; + + cy.findByTestId(`display-order-column-${paymentMethodIds[0]}`).click(); + cy.url().should('contain', 'general'); + + cy.findByTestId(`display-order-input`).should('exist'); + cy.findByTestId(`display-order-input`).clear(); + cy.findByTestId(`display-order-input`).type('20'); + cy.findByTestId(`save-button`).click(); + cy.findByLabelText('Go back').click(); + cy.findByTestId(`display-order-column-${paymentMethodIds[0]}`).should( + 'have.text', + 20 + ); + }); +}); diff --git a/application/cypress/fixtures/objects-paginated.json b/application/cypress/fixtures/objects-paginated.json new file mode 100644 index 0000000..4d2a73f --- /dev/null +++ b/application/cypress/fixtures/objects-paginated.json @@ -0,0 +1,345 @@ +{ + "results": [ + { + "id": "e768b373-f0c5-4122-a689-105e5382f6ee", + "container": "sctm-app-methods", + "key": "applepay", + "value": { + "id": "applepay", + "name": { + "en-GB": "Apple Pay" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/applepay.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "d8776b79-b5bf-4451-a14e-4852ecc8c0f7", + "container": "sctm-app-methods", + "key": "bancomatpay", + "value": { + "id": "bancomatpay", + "name": { + "en-GB": "Bancomat Pay" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/bancomatpay.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "1756102e-9656-4fec-a18c-2e5144ddd2ae", + "container": "sctm-app-methods", + "key": "bancontact", + "value": { + "id": "bancontact", + "name": { + "en-GB": "Bancontact" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/bancontact.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "ad7608bf-6cb7-4f2c-894e-a90cb4517bd3", + "container": "sctm-app-methods", + "key": "banktransfer", + "value": { + "id": "banktransfer", + "name": { + "en-GB": "Bank transfer" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/banktransfer.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "c2b0ed91-330f-48e2-8e7a-3b90d15b2340", + "container": "sctm-app-methods", + "key": "belfius", + "value": { + "id": "belfius", + "name": { + "en-GB": "Belfius Pay Button" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/belfius.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "adcc987d-c981-4906-a3ad-f5800d645775", + "container": "sctm-app-methods", + "key": "billie", + "value": { + "id": "billie", + "name": { + "en-GB": "Pay by Invoice for Businesses - Billie" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/billie.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "127d4d21-2b95-46f9-9a68-2490e5182ae5", + "container": "sctm-app-methods", + "key": "blik", + "value": { + "id": "blik", + "name": { + "en-GB": "Blik" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/blik.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "5fc3b28c-9282-4cb3-ba8a-3329c076509c", + "container": "sctm-app-methods", + "key": "creditcard", + "value": { + "id": "creditcard", + "name": { + "en-GB": "Card" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/creditcard.svg", + "status": "Inactive", + "displayOrder": 1 + }, + "__typename": "CustomObject" + }, + { + "id": "9882ab6d-c534-4a42-83f1-efc13d37e703", + "container": "sctm-app-methods", + "key": "directdebit", + "value": { + "id": "directdebit", + "name": { + "en-GB": "SEPA Direct Debit" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/directdebit.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "d414c2b2-6a88-4712-b143-ef463605c967", + "container": "sctm-app-methods", + "key": "eps", + "value": { + "id": "eps", + "name": { + "en-GB": "eps" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/eps.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "0b58a6db-affe-4cb1-97cf-e2e22de87f32", + "container": "sctm-app-methods", + "key": "giftcard", + "value": { + "id": "giftcard", + "name": { + "en-GB": "Gift cards" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/giftcard.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "1126f6b9-5e7b-4b74-9fad-9b47ed1f43bd", + "container": "sctm-app-methods", + "key": "ideal", + "value": { + "id": "ideal", + "name": { + "en-GB": "iDEAL" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/ideal.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "0fd7e27b-b6c4-4766-bf23-51deb2762155", + "container": "sctm-app-methods", + "key": "in3", + "value": { + "id": "in3", + "name": { + "en-GB": "iDEAL Pay in 3 instalments, 0% interest" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/in3.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "b26b5a51-4653-4dfd-aa61-3613605b1f2d", + "container": "sctm-app-methods", + "key": "kbc", + "value": { + "id": "kbc", + "name": { + "en-GB": "KBC/CBC Payment Button" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/kbc.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "37feebf9-be78-46d8-9f4e-eace2928f1cd", + "container": "sctm-app-methods", + "key": "klarna", + "value": { + "id": "klarna", + "name": { + "en-GB": "Pay with Klarna" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/klarna.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "1a2e480c-1a83-4d0d-ab4a-3859cf6a18e6", + "container": "sctm-app-methods", + "key": "paypal", + "value": { + "id": "paypal", + "name": { + "en-GB": "PayPal" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/paypal.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "eb982dd3-1b4f-4571-8ee1-ac1568cf5ec2", + "container": "sctm-app-methods", + "key": "przelewy24", + "value": { + "id": "przelewy24", + "name": { + "en-GB": "Przelewy24" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/przelewy24.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "cffb9dc2-7fe7-4577-80fa-c855bdde1a22", + "container": "sctm-app-methods", + "key": "trustly", + "value": { + "id": "trustly", + "name": { + "en-GB": "Trustly" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/trustly.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "09aa39b3-6c65-47d4-a3e9-32d7d1c86172", + "container": "sctm-app-methods", + "key": "twint", + "value": { + "id": "twint", + "name": { + "en-GB": "TWINT" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/twint.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + }, + { + "id": "4d9bbe29-0c27-4bde-bf03-3d73489ec9f0", + "container": "sctm-app-methods", + "key": "voucher", + "value": { + "id": "voucher", + "name": { + "en-GB": "Vouchers" + }, + "description": { + "en-GB": "" + }, + "imageUrl": "https://www.mollie.com/external/icons/payment-methods/voucher.svg", + "status": "Inactive" + }, + "__typename": "CustomObject" + } + ] +} diff --git a/application/package.json b/application/package.json index ee21bea..cecdd89 100644 --- a/application/package.json +++ b/application/package.json @@ -58,6 +58,7 @@ "@commercetools-uikit/localized-text-field": "^19.9.0", "@commercetools-uikit/localized-text-input": "^19.9.0", "@commercetools-uikit/notifications": "^19.9.0", + "@commercetools-uikit/number-field": "^19.11.0", "@commercetools-uikit/pagination": "^19.9.0", "@commercetools-uikit/select-field": "^19.9.0", "@commercetools-uikit/spacings": "^19.9.0", diff --git a/application/src/components/method-details/messages.ts b/application/src/components/method-details/messages.ts index 082d494..9030a5a 100644 --- a/application/src/components/method-details/messages.ts +++ b/application/src/components/method-details/messages.ts @@ -14,4 +14,40 @@ export default defineMessages({ id: 'MethodDetails.methodDetailsStatusUpdated', defaultMessage: '{methodName} {status}', }, + fieldMethodName: { + id: 'MethodDetails.fieldMethodName', + defaultMessage: 'Payment name', + }, + fieldMethodNameInvalidLength: { + id: 'MethodDetails.fieldMethodNameInvalidLength', + defaultMessage: 'Maximum 50 characters allowed.', + }, + fieldMethodNameDescription: { + id: 'MethodDetails.fieldMethodNameDescription', + defaultMessage: 'Enter payment name in their corresponding locals.', + }, + fieldMethodDescription: { + id: 'MethodDetails.fieldMethodDescription', + defaultMessage: 'Payment description', + }, + fieldMethodDescriptionInvalidLength: { + id: 'MethodDetails.fieldMethodDescriptionInvalidLength', + defaultMessage: 'Maximum 100 characters allowed.', + }, + fieldMethodDescriptionDescription: { + id: 'MethodDetails.fieldMethodDescriptionDescription', + defaultMessage: 'Describe payment method in their corresponding locals.', + }, + fieldMethodDisplayOrder: { + id: 'MethodDetails.fieldMethodDisplayOrder', + defaultMessage: 'Display order in checkout', + }, + fieldMethodDisplayOrderIsNotInteger: { + id: 'MethodDetails.fieldMethodDisplayOrderIsNotInteger', + defaultMessage: 'Choose natural integer between 0 and 100.', + }, + fieldMethodDisplayOrderInfoTitle: { + id: 'MethodDetails.fieldMethodDisplayOrderInfoTitle', + defaultMessage: 'Display order in checkout', + }, }); diff --git a/application/src/components/method-details/method-details-form.tsx b/application/src/components/method-details/method-details-form.tsx index 59a8e1a..4185d2c 100644 --- a/application/src/components/method-details/method-details-form.tsx +++ b/application/src/components/method-details/method-details-form.tsx @@ -2,6 +2,17 @@ import { useFormik, type FormikHelpers } from 'formik'; import { ReactElement } from 'react'; import { TMethodObjectValueFormValues } from '../../types'; import Spacings from '@commercetools-uikit/spacings'; +import TextField from '@commercetools-uikit/text-field'; +import NumberField from '@commercetools-uikit/number-field'; +import { useIntl } from 'react-intl'; +import messages from './messages'; +import LocalizedTextField from '@commercetools-uikit/localized-text-field'; +import { + InfoDialog, + useModalState, +} from '@commercetools-frontend/application-components'; +import Text from '@commercetools-uikit/text'; +import validate from './validate'; type Formik = ReturnType; type FormProps = { @@ -24,21 +35,143 @@ type TCustomObjectDetailsFormProps = { }; const MethodDetailsForm = (props: TCustomObjectDetailsFormProps) => { + const intl = useIntl(); const formik = useFormik({ initialValues: props.initialValues || ({} as TMethodObjectValueFormValues), onSubmit: props.onSubmit, - validate: () => {}, + validate, enableReinitialize: true, }); + const infoModalState = useModalState(); if (!props.initialValues) { return null; } const formElements = ( - -

Content will follow...

-
+ + (formik.errors) + .name + } + touched={Boolean(formik.touched.name)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + isReadOnly={props.isReadOnly} + horizontalConstraint={13} + isRequired={false} + renderError={(errorKey) => { + if (errorKey === 'invalidLength') { + return intl.formatMessage(messages.fieldMethodNameInvalidLength); + } + return null; + }} + data-testid="name-input" + /> + (formik.errors) + .description + } + touched={Boolean(formik.touched.description)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + isReadOnly={props.isReadOnly} + horizontalConstraint={13} + isRequired={false} + renderError={(errorKey) => { + if (errorKey === 'invalidLength') { + return intl.formatMessage( + messages.fieldMethodDescriptionInvalidLength + ); + } + return null; + }} + data-testid="description-input" + /> + (formik.errors) + .displayOrder + } + touched={Boolean(formik.touched.displayOrder)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + isReadOnly={props.isReadOnly} + horizontalConstraint={13} + step={1} + isRequired={true} + onInfoButtonClick={() => { + infoModalState.openModal(); + }} + renderError={(errorKey) => { + if (errorKey === 'isNotInteger') { + return intl.formatMessage( + messages.fieldMethodDisplayOrderIsNotInteger + ); + } + return null; + }} + data-testid="display-order-input" + > + + + + + Assigning an order number to payment methods will allow you to + more easily use sorting your payment methods. + + + + + Payment method order values can be a calue between 0 and 100. + + + + + The higher the value, the higher the position in the checkout. + + + + + For example: + + + + Payment Method A has rank value: 25 + + + Payment Method B has rank value: 80 + + + + Therefor Payment Method B will be displayed above A in the + checkout. + + + + + ); return props.children({ diff --git a/application/src/components/method-details/method-details.tsx b/application/src/components/method-details/method-details.tsx index 601ad1a..9f99c21 100644 --- a/application/src/components/method-details/method-details.tsx +++ b/application/src/components/method-details/method-details.tsx @@ -1,8 +1,10 @@ import { useIntl } from 'react-intl'; -import { useParams } from 'react-router-dom'; +import { Route, Switch, useParams, useRouteMatch } from 'react-router-dom'; import { PageNotFound, CustomFormModalPage, + TabularModalPage, + TabHeader, } from '@commercetools-frontend/application-components'; import { ContentNotification } from '@commercetools-uikit/notifications'; import Text from '@commercetools-uikit/text'; @@ -20,10 +22,14 @@ import { TMethodObjectValueFormValues } from '../../types'; import { useShowNotification } from '@commercetools-frontend/actions-global'; import { DOMAINS, + NO_VALUE_FALLBACK, NOTIFICATION_KINDS_SIDE, } from '@commercetools-frontend/constants'; import SelectField from '@commercetools-uikit/select-field'; import { ApplicationPageTitle } from '@commercetools-frontend/application-shell'; +import { useIsAuthorized } from '@commercetools-frontend/permissions'; +import { PERMISSIONS } from '../../constants'; +import { formatLocalizedString } from '@commercetools-frontend/l10n'; type TMethodDetailsProps = { onClose: () => void; @@ -31,15 +37,18 @@ type TMethodDetailsProps = { const MethodDetails = (props: TMethodDetailsProps) => { const intl = useIntl(); + const match = useRouteMatch(); const params = useParams<{ id: string }>(); const { loading, error, method } = useCustomObjectDetailsFetcher(params.id); - const { dataLocale } = useApplicationContext((context) => ({ + const { dataLocale, projectLanguages } = useApplicationContext((context) => ({ dataLocale: context.dataLocale ?? '', projectLanguages: context.project?.languages ?? [], })); const customObjectUpdater = useCustomObjectDetailsUpdater(); const showNotification = useShowNotification(); - const canManage = true; + const canManage = useIsAuthorized({ + demandedPermissions: [PERMISSIONS.Manage], + }); const handleSubmit = async (formikValues: TMethodObjectValueFormValues) => { try { @@ -53,7 +62,17 @@ const MethodDetails = (props: TMethodDetailsProps) => { kind: 'success', domain: DOMAINS.SIDE, text: intl.formatMessage(messages.methodDetailsUpdated, { - methodName: formikValues.description, + methodName: formatLocalizedString( + { + name: formikValues.name, + }, + { + key: 'name', + locale: dataLocale, + fallbackOrder: projectLanguages, + fallback: NO_VALUE_FALLBACK, + } + ), }), }); } @@ -68,22 +87,27 @@ const MethodDetails = (props: TMethodDetailsProps) => { ) => { try { if (method?.container && method?.key && formikValues) { + let clonedValues = { ...formikValues, ...{ status: status } }; await customObjectUpdater.execute({ container: method?.container, key: method?.key, - value: JSON.stringify({ - id: formikValues.id, - description: formikValues.description, - status: status, - imageUrl: formikValues.imageUrl, - displayOrder: formikValues.displayOrder, - }), + value: JSON.stringify(clonedValues), }); showNotification({ kind: NOTIFICATION_KINDS_SIDE.success, domain: DOMAINS.SIDE, text: intl.formatMessage(messages.methodDetailsStatusUpdated, { - methodName: formikValues.description, + methodName: formatLocalizedString( + { + name: formikValues.name, + }, + { + key: 'name', + locale: dataLocale, + fallbackOrder: projectLanguages, + fallback: NO_VALUE_FALLBACK, + } + ), status: status === 'Active' ? 'activated' : 'deactivated', }), }); @@ -94,16 +118,21 @@ const MethodDetails = (props: TMethodDetailsProps) => { }; const handleSubmitCallback = useCallback(handleSubmit, [ - method, + method?.container, + method?.key, customObjectUpdater, showNotification, intl, + dataLocale, + projectLanguages, ]); const handleChangeCallback = useCallback(handleChange, [ customObjectUpdater, + dataLocale, intl, method?.container, method?.key, + projectLanguages, showNotification, ]); @@ -115,11 +144,42 @@ const MethodDetails = (props: TMethodDetailsProps) => { dataLocale={dataLocale} > {(formProps) => { + const methodName = formatLocalizedString( + { + name: formProps.values?.name, + }, + { + key: 'name', + locale: dataLocale, + fallbackOrder: projectLanguages, + fallback: NO_VALUE_FALLBACK, + } + ); return ( - props.onClose()} + tabControls={ + <> + + + + + } formControls={ <> { ]} horizontalConstraint={4} controlShouldRenderValue={true} - data-cy={'active-select'} + data-testid={'status-select'} isSearchable={false} > formProps.submitForm()} + onClick={(event) => { + event.preventDefault(); + formProps.submitForm(); + }} isDisabled={ formProps.isSubmitting || !formProps.isDirty || !canManage } - dataAttributes={{ 'data-cy': 'save-button' }} + dataAttributes={{ 'data-testid': 'save-button' }} /> } @@ -174,14 +237,20 @@ const MethodDetails = (props: TMethodDetailsProps) => { )} - {method && formProps.formElements} - {method && ( - - )} + {method && } {method === null && } - + + + {method && formProps.formElements} + + +
Icon
+
+ +
Availability
+
+
+ ); }} diff --git a/application/src/components/method-details/validate.ts b/application/src/components/method-details/validate.ts new file mode 100644 index 0000000..6e58414 --- /dev/null +++ b/application/src/components/method-details/validate.ts @@ -0,0 +1,45 @@ +import omitEmpty from 'omit-empty-es'; +import type { FormikErrors } from 'formik'; +import type { TMethodObjectValueFormValues } from '../../types'; + +type TMethodObjectErrors = { + name: { invalidLength?: boolean }; + description: { invalidLength?: boolean }; + displayOrder: { isNotInteger?: boolean }; +}; + +const validate = ( + formikValues: TMethodObjectValueFormValues +): FormikErrors => { + const errors: TMethodObjectErrors = { + name: {}, + description: {}, + displayOrder: {}, + }; + + if ( + Object.keys(formikValues.name).some( + (language) => formikValues.name[language].length > 50 + ) + ) { + errors.name.invalidLength = true; + } + if ( + Object.keys(formikValues.description).some( + (language) => formikValues.description[language].length > 100 + ) + ) { + errors.description.invalidLength = true; + } + if ( + !Number.isInteger(formikValues.displayOrder) || + formikValues.displayOrder <= 0 || + formikValues.displayOrder > 100 + ) { + errors.displayOrder.isNotInteger = true; + } + + return omitEmpty(errors); +}; + +export default validate; diff --git a/application/src/components/welcome/messages.ts b/application/src/components/welcome/messages.ts index 57f4060..9aeec98 100644 --- a/application/src/components/welcome/messages.ts +++ b/application/src/components/welcome/messages.ts @@ -40,8 +40,8 @@ export default defineMessages({ id: 'Welcome.paymentMethodHeader', defaultMessage: 'Payment method', }, - descriptionHeader: { - id: 'Welcome.descriptionHeader', + nameHeader: { + id: 'Welcome.nameHeader', defaultMessage: 'Payment method', }, }); diff --git a/application/src/components/welcome/welcome.spec.tsx b/application/src/components/welcome/welcome.spec.tsx index 162edee..5f6f899 100644 --- a/application/src/components/welcome/welcome.spec.tsx +++ b/application/src/components/welcome/welcome.spec.tsx @@ -4,6 +4,7 @@ import { } from '@commercetools-frontend/application-shell/test-utils'; import { setupServer } from 'msw/node'; import ForwardToFixture from '../../../cypress/fixtures/forward-to.json'; +import ObjectsPaginated from '../../../cypress/fixtures/objects-paginated.json'; import messages from './messages'; import { useCustomObjectsFetcher, @@ -30,22 +31,23 @@ jest.mock('../../hooks/use-mollie-connector', () => ({ const mockMethods = ForwardToFixture._embedded.methods.map((method) => { return { id: method.id, - description: method.description, + description: { + 'en-GB': '', + }, + name: { + 'en-GB': method.description, + }, status: method.status === 'active' ? 'Active' : 'Inactive', imageUrl: method.image.svg, displayOrder: 0, }; }); -const mockMethodNames = ForwardToFixture._embedded.methods.map((method) => { - return method.description; -}); - const mockColumns = Object.values(messages) .filter((message) => [ - 'Welcome.descriptionHeader', 'Welcome.statusHeader', + 'Welcome.nameHeader', 'Welcome.iconHeader', 'Welcome.displayOrderHeader', ].includes(message.id) @@ -56,7 +58,7 @@ const mockServer = setupServer(); afterEach(() => mockServer.resetHandlers()); beforeEach(() => { (useCustomObjectsFetcher as jest.Mock).mockReturnValue({ - customObjectsPaginatedResult: { results: [] }, + customObjectsPaginatedResult: ObjectsPaginated, error: null, loading: false, }); @@ -71,7 +73,7 @@ beforeEach(() => { }); (useCustomObjectDetailsUpdater as jest.Mock).mockReturnValue({ - execute: jest.fn(), + execute: jest.fn().mockResolvedValue({}), }); }); beforeAll(() => @@ -108,8 +110,10 @@ describe('Test welcome.tsx', () => { expect(screen.getByTestId('status-tooltip')).toBeInTheDocument(); - mockMethodNames.forEach((name) => { - expect(screen.getByText(name)).toBeInTheDocument(); + mockMethods.forEach((method) => { + expect( + screen.getByTestId(`name-column-${method.id}`) + ).toBeInTheDocument(); }); }); diff --git a/application/src/components/welcome/welcome.tsx b/application/src/components/welcome/welcome.tsx index 1bfa987..d27cc16 100644 --- a/application/src/components/welcome/welcome.tsx +++ b/application/src/components/welcome/welcome.tsx @@ -30,15 +30,22 @@ import { getErrorMessage } from '../../helpers'; import { SuspendedRoute } from '@commercetools-frontend/application-shell'; import MethodDetails from '../method-details'; import { useIntl } from 'react-intl'; +import { formatLocalizedString } from '@commercetools-frontend/l10n'; +import { useApplicationContext } from '@commercetools-frontend/application-shell-connectors'; +import { NO_VALUE_FALLBACK } from '@commercetools-frontend/constants'; const Welcome = () => { const intl = useIntl(); const match = useRouteMatch(); const { push } = useHistory(); + const { dataLocale, projectLanguages } = useApplicationContext((context) => ({ + dataLocale: context.dataLocale ?? '', + projectLanguages: context.project?.languages ?? [], + })); const columns = [ { - key: 'description', - label: intl.formatMessage(messages.descriptionHeader), + key: 'name', + label: intl.formatMessage(messages.nameHeader), }, { key: 'status', @@ -58,7 +65,10 @@ const Welcome = () => { ), }, { key: 'image', label: intl.formatMessage(messages.iconHeader) }, - { key: 'order', label: intl.formatMessage(messages.displayOrderHeader) }, + { + key: 'order', + label: intl.formatMessage(messages.displayOrderHeader), + }, ]; const customObjectUpdater = useCustomObjectDetailsUpdater(); const { page, perPage } = usePaginationState(); @@ -78,7 +88,8 @@ const Welcome = () => { const [refresh, setRefresh] = useState(0); const { fetchedData, fetchedDataLoading } = usePaymentMethodsFetcher( - extension?.destination?.url + extension?.destination?.url, + projectLanguages ); const handleRefresh = useCallback(() => { @@ -94,11 +105,15 @@ const Welcome = () => { ); if (shouldCreate) { - await customObjectUpdater.execute({ - container: OBJECT_CONTAINER_NAME, - key: method.id, - value: JSON.stringify(method), - }); + await customObjectUpdater + .execute({ + container: OBJECT_CONTAINER_NAME, + key: method.id, + value: JSON.stringify(method), + }) + .catch((error) => { + console.error(`Error creating custom object: ${error}`); + }); return method; } else { return customObjectsPaginatedResult?.results.find( @@ -163,8 +178,20 @@ const Welcome = () => { ) : ( ); - case 'description': - return item.description; + case 'name': + return item.name + ? formatLocalizedString( + { + name: item.name, + }, + { + key: 'name', + locale: dataLocale, + fallbackOrder: projectLanguages, + fallback: NO_VALUE_FALLBACK, + } + ) + : item.description; case 'image': return ( { > ); case 'order': - return item.displayOrder; + return item.displayOrder ?? '-'; default: return null; } @@ -196,7 +223,7 @@ const Welcome = () => { customObjectsPaginatedResult?.results.filter( (obj) => obj.key === row.id )?.[0]?.id - }` + }/general` ); }} /> diff --git a/application/src/hooks/use-mollie-connector/use-mollie-connector.ts b/application/src/hooks/use-mollie-connector/use-mollie-connector.ts index 139083f..a5e17e2 100644 --- a/application/src/hooks/use-mollie-connector/use-mollie-connector.ts +++ b/application/src/hooks/use-mollie-connector/use-mollie-connector.ts @@ -14,6 +14,7 @@ import { MollieMethod, CustomMethodObject, MollieResult, + SupportedPaymentMethods, } from '../../types/app'; /** @@ -24,26 +25,37 @@ import { const config = { headers: { 'Content-Type': 'application/json', + 'ngrok-skip-browser-warning': 'true' }, }; const convertMollieMethodToCustomMethod = ( - results: MollieResult + results: MollieResult, + projectLanguages: string[] ): CustomMethodObject[] => { const methods = results['_embedded']['methods']; const availableMethods = methods.filter( - (method: MollieMethod) => method.status === 'activated' + (method: MollieMethod) => + method.status === 'activated' && + SupportedPaymentMethods[method.id as SupportedPaymentMethods] ); return availableMethods.map((method: MollieMethod) => ({ id: method.id, - description: method.description, + name: projectLanguages.reduce((acc, lang) => { + acc[lang] = method.description; + return acc; + }, {} as Record), + description: projectLanguages.reduce((acc, lang) => { + acc[lang] = ''; + return acc; + }, {} as Record), imageUrl: method.image.svg, status: 'Inactive', - displayOrder: 0, + displayOrder: undefined, })); }; -const getMethods = async (targetUrl?: string) => { +const getMethods = async (projectLanguages: string[], targetUrl?: string) => { if (!targetUrl) { logger.error('usePaymentMethodsFetcher - No target URL provided'); return []; @@ -72,18 +84,24 @@ const getMethods = async (targetUrl?: string) => { } ) .then((res) => - convertMollieMethodToCustomMethod(res as unknown as MollieResult) + convertMollieMethodToCustomMethod( + res as unknown as MollieResult, + projectLanguages + ) ) .catch((error) => logger.error(error)); }; -export const usePaymentMethodsFetcher = (url: string | undefined) => { +export const usePaymentMethodsFetcher = ( + url: string | undefined, + projectLanguages: string[] +) => { const [fetchedData, setFetchedData] = useState([]); const [fetchedDataLoading, setFetchedDataLoading] = useState(true); useEffect(() => { const fetchData = async () => { - const data = (await getMethods(url)) ?? []; + const data = (await getMethods(projectLanguages, url)) ?? []; setFetchedData(data); setFetchedDataLoading(false); }; @@ -91,7 +109,7 @@ export const usePaymentMethodsFetcher = (url: string | undefined) => { if (url) { fetchData(); } - }, [url]); + }, [url, projectLanguages]); return { fetchedData, fetchedDataLoading }; }; diff --git a/application/src/i18n/data/core.json b/application/src/i18n/data/core.json index 00dff87..8a885fb 100644 --- a/application/src/i18n/data/core.json +++ b/application/src/i18n/data/core.json @@ -1,12 +1,37 @@ { + "ChannelDetails.backToChannelsList": "Back to channels list", + "ChannelDetails.channelKeyLabel": "Channel key", + "ChannelDetails.channelNameLabel": "Channel name", + "ChannelDetails.channelRolesLabel": "Channel roles", + "ChannelDetails.channelUpdated": "Channel {channelName} updated", + "ChannelDetails.duplicateKey": "A channel with this key already exists.", + "ChannelDetails.errorMessage": "We were unable to fetch the channel details. Please check your connection, the provided channel ID and try again.", + "ChannelDetails.hint": "This page demonstrates for instance how to use forms, notifications and how to update data using GraphQL, etc.", + "ChannelDetails.modalTitle": "Edit channel", + "Channels.backToWelcome": "Back to Welcome page", + "Channels.demoHint": "This page demonstrates how you can develop a component following some of the Merchant Center UX Guidelines and development best practices. For instance, fetching data using GraphQL, displaying data in a paginated table, writing functional tests, etc.", "Channels.noResults": "There are no channels available in this project.", "Channels.title": "Channels list", + "MethodDetails.fieldMethodDescription": "Payment description", + "MethodDetails.fieldMethodDescriptionDescription": "Describe payment method in their corresponding locals.", + "MethodDetails.fieldMethodDescriptionInvalidLength": "Maximum 100 characters allowed.", + "MethodDetails.fieldMethodDisplayOrder": "Display order in checkout", + "MethodDetails.fieldMethodDisplayOrderInfoTitle": "Display order in checkout", + "MethodDetails.fieldMethodDisplayOrderIsNotInteger": "Choose natural integer between 0 and 100.", + "MethodDetails.fieldMethodName": "Payment name", + "MethodDetails.fieldMethodNameDescription": "Enter payment name in their corresponding locals.", + "MethodDetails.fieldMethodNameInvalidLength": "Maximum 50 characters allowed.", "MethodDetails.methodDetailsErrorMessage": "We were unable to fetch the custom object details. Please check your connection, the provided custom object ID and try again.", "MethodDetails.methodDetailsStatusUpdated": "{methodName} {status}", "MethodDetails.methodDetailsUpdated": "{methodName} updated", - "Welcome.activeHeader": "Payment method is only available for checkout if the status is set to “Active”. Please make sure that the payment method is also enabled in the Mollie Dashboard.", + "Welcome.displayOrderHeader": "Display order", + "Welcome.iconHeader": "Icon", + "Welcome.nameHeader": "Payment method", "Welcome.noData": "There are no active payment methods available. Please activate them in your mollie dashboard first.", "Welcome.notice": "Content will follow...", + "Welcome.paymentMethodHeader": "Payment method", + "Welcome.statusHeader": "Active", + "Welcome.statusHeaderHint": "Payment method is only available for checkout if the status is set to “Active”. Please make sure that the payment method is also enabled in the Mollie Dashboard.", "Welcome.subtitle": "Welcome to the Mollie Custom Application. This application allows you to manage your Mollie payments in the Commercetools Merchant Center.", "Welcome.title": "Mollie payment methods" } diff --git a/application/src/i18n/data/de.json b/application/src/i18n/data/de.json index 0967ef4..8a885fb 100644 --- a/application/src/i18n/data/de.json +++ b/application/src/i18n/data/de.json @@ -1 +1,37 @@ -{} +{ + "ChannelDetails.backToChannelsList": "Back to channels list", + "ChannelDetails.channelKeyLabel": "Channel key", + "ChannelDetails.channelNameLabel": "Channel name", + "ChannelDetails.channelRolesLabel": "Channel roles", + "ChannelDetails.channelUpdated": "Channel {channelName} updated", + "ChannelDetails.duplicateKey": "A channel with this key already exists.", + "ChannelDetails.errorMessage": "We were unable to fetch the channel details. Please check your connection, the provided channel ID and try again.", + "ChannelDetails.hint": "This page demonstrates for instance how to use forms, notifications and how to update data using GraphQL, etc.", + "ChannelDetails.modalTitle": "Edit channel", + "Channels.backToWelcome": "Back to Welcome page", + "Channels.demoHint": "This page demonstrates how you can develop a component following some of the Merchant Center UX Guidelines and development best practices. For instance, fetching data using GraphQL, displaying data in a paginated table, writing functional tests, etc.", + "Channels.noResults": "There are no channels available in this project.", + "Channels.title": "Channels list", + "MethodDetails.fieldMethodDescription": "Payment description", + "MethodDetails.fieldMethodDescriptionDescription": "Describe payment method in their corresponding locals.", + "MethodDetails.fieldMethodDescriptionInvalidLength": "Maximum 100 characters allowed.", + "MethodDetails.fieldMethodDisplayOrder": "Display order in checkout", + "MethodDetails.fieldMethodDisplayOrderInfoTitle": "Display order in checkout", + "MethodDetails.fieldMethodDisplayOrderIsNotInteger": "Choose natural integer between 0 and 100.", + "MethodDetails.fieldMethodName": "Payment name", + "MethodDetails.fieldMethodNameDescription": "Enter payment name in their corresponding locals.", + "MethodDetails.fieldMethodNameInvalidLength": "Maximum 50 characters allowed.", + "MethodDetails.methodDetailsErrorMessage": "We were unable to fetch the custom object details. Please check your connection, the provided custom object ID and try again.", + "MethodDetails.methodDetailsStatusUpdated": "{methodName} {status}", + "MethodDetails.methodDetailsUpdated": "{methodName} updated", + "Welcome.displayOrderHeader": "Display order", + "Welcome.iconHeader": "Icon", + "Welcome.nameHeader": "Payment method", + "Welcome.noData": "There are no active payment methods available. Please activate them in your mollie dashboard first.", + "Welcome.notice": "Content will follow...", + "Welcome.paymentMethodHeader": "Payment method", + "Welcome.statusHeader": "Active", + "Welcome.statusHeaderHint": "Payment method is only available for checkout if the status is set to “Active”. Please make sure that the payment method is also enabled in the Mollie Dashboard.", + "Welcome.subtitle": "Welcome to the Mollie Custom Application. This application allows you to manage your Mollie payments in the Commercetools Merchant Center.", + "Welcome.title": "Mollie payment methods" +} diff --git a/application/src/i18n/data/en.json b/application/src/i18n/data/en.json index 0967ef4..8a885fb 100644 --- a/application/src/i18n/data/en.json +++ b/application/src/i18n/data/en.json @@ -1 +1,37 @@ -{} +{ + "ChannelDetails.backToChannelsList": "Back to channels list", + "ChannelDetails.channelKeyLabel": "Channel key", + "ChannelDetails.channelNameLabel": "Channel name", + "ChannelDetails.channelRolesLabel": "Channel roles", + "ChannelDetails.channelUpdated": "Channel {channelName} updated", + "ChannelDetails.duplicateKey": "A channel with this key already exists.", + "ChannelDetails.errorMessage": "We were unable to fetch the channel details. Please check your connection, the provided channel ID and try again.", + "ChannelDetails.hint": "This page demonstrates for instance how to use forms, notifications and how to update data using GraphQL, etc.", + "ChannelDetails.modalTitle": "Edit channel", + "Channels.backToWelcome": "Back to Welcome page", + "Channels.demoHint": "This page demonstrates how you can develop a component following some of the Merchant Center UX Guidelines and development best practices. For instance, fetching data using GraphQL, displaying data in a paginated table, writing functional tests, etc.", + "Channels.noResults": "There are no channels available in this project.", + "Channels.title": "Channels list", + "MethodDetails.fieldMethodDescription": "Payment description", + "MethodDetails.fieldMethodDescriptionDescription": "Describe payment method in their corresponding locals.", + "MethodDetails.fieldMethodDescriptionInvalidLength": "Maximum 100 characters allowed.", + "MethodDetails.fieldMethodDisplayOrder": "Display order in checkout", + "MethodDetails.fieldMethodDisplayOrderInfoTitle": "Display order in checkout", + "MethodDetails.fieldMethodDisplayOrderIsNotInteger": "Choose natural integer between 0 and 100.", + "MethodDetails.fieldMethodName": "Payment name", + "MethodDetails.fieldMethodNameDescription": "Enter payment name in their corresponding locals.", + "MethodDetails.fieldMethodNameInvalidLength": "Maximum 50 characters allowed.", + "MethodDetails.methodDetailsErrorMessage": "We were unable to fetch the custom object details. Please check your connection, the provided custom object ID and try again.", + "MethodDetails.methodDetailsStatusUpdated": "{methodName} {status}", + "MethodDetails.methodDetailsUpdated": "{methodName} updated", + "Welcome.displayOrderHeader": "Display order", + "Welcome.iconHeader": "Icon", + "Welcome.nameHeader": "Payment method", + "Welcome.noData": "There are no active payment methods available. Please activate them in your mollie dashboard first.", + "Welcome.notice": "Content will follow...", + "Welcome.paymentMethodHeader": "Payment method", + "Welcome.statusHeader": "Active", + "Welcome.statusHeaderHint": "Payment method is only available for checkout if the status is set to “Active”. Please make sure that the payment method is also enabled in the Mollie Dashboard.", + "Welcome.subtitle": "Welcome to the Mollie Custom Application. This application allows you to manage your Mollie payments in the Commercetools Merchant Center.", + "Welcome.title": "Mollie payment methods" +} diff --git a/application/src/types.ts b/application/src/types.ts index 306f290..fa99d82 100644 --- a/application/src/types.ts +++ b/application/src/types.ts @@ -23,8 +23,9 @@ export type TMethodObjectFormValues = { export type TMethodObjectValueFormValues = { id: string; - description: string; + name: Record; + description: Record; imageUrl: string; status: string; - displayOrder?: number; + displayOrder: number; }; diff --git a/application/src/types/app.ts b/application/src/types/app.ts index b68e848..baadfcc 100644 --- a/application/src/types/app.ts +++ b/application/src/types/app.ts @@ -30,7 +30,8 @@ export type MollieMethod = { export type CustomMethodObject = { id: string; - description: string; + name: Record; + description?: Record; imageUrl: string; status: string; displayOrder?: number; @@ -41,3 +42,16 @@ export type MollieResult = { methods: MollieMethod[]; }; }; + +export enum SupportedPaymentMethods { + ideal = 'ideal', + creditcard = 'creditcard', + bancontact = 'bancontact', + banktransfer = 'banktransfer', + przelewy24 = 'przelewy24', + kbc = 'kbc', + blik = 'blik', + applepay = 'applepay', + paypal = 'paypal', + giftcard = 'giftcard', +} diff --git a/application/yarn.lock b/application/yarn.lock index bec47a6..80dacab 100644 --- a/application/yarn.lock +++ b/application/yarn.lock @@ -3179,6 +3179,26 @@ "@emotion/react" "^11.10.5" prop-types "15.8.1" +"@commercetools-uikit/number-field@^19.11.0": + version "19.11.0" + resolved "https://registry.yarnpkg.com/@commercetools-uikit/number-field/-/number-field-19.11.0.tgz#b92eea9eb8163eb37f84764ca8a1d904af213666" + integrity sha512-uYR8Pek6gp5emdlGdZkCleUTIkN8NOOtIveExkGFlJNMx+D/GNBGIs4PgJCbMa2rTuvQrMn3J6CTlRE4P8mxiQ== + dependencies: + "@babel/runtime" "^7.20.13" + "@babel/runtime-corejs3" "^7.20.13" + "@commercetools-uikit/constraints" "19.11.0" + "@commercetools-uikit/design-system" "19.11.0" + "@commercetools-uikit/field-errors" "19.11.0" + "@commercetools-uikit/field-label" "19.11.0" + "@commercetools-uikit/field-warnings" "19.11.0" + "@commercetools-uikit/number-input" "19.11.0" + "@commercetools-uikit/spacings-stack" "19.11.0" + "@commercetools-uikit/utils" "19.11.0" + "@emotion/react" "^11.10.5" + "@emotion/styled" "^11.10.5" + prop-types "15.8.1" + react-intl "^6.3.2" + "@commercetools-uikit/number-input@19.11.0": version "19.11.0" resolved "https://registry.npmjs.org/@commercetools-uikit/number-input/-/number-input-19.11.0.tgz" diff --git a/docs/GetPaymentMethods.md b/docs/GetPaymentMethods.md index 2db071c..337e986 100644 --- a/docs/GetPaymentMethods.md +++ b/docs/GetPaymentMethods.md @@ -69,17 +69,21 @@ _Body:_ ```json { - "actions": [ - { - "action": "setCustomField", - "name": "sctm_payment_methods_response", - "value": "{\"count\":11,\"methods\":[{\"resource\":\"method\",\"id\":\"applepay\",\"description\":\"Apple Pay\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"10000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/applepay.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/applepay%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/applepay.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/applepay\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"creditcard\",\"description\":\"Karte\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"10000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/creditcard.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/creditcard%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/creditcard.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/creditcard\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"paypal\",\"description\":\"PayPal\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":null,\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/paypal.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/paypal%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/paypal.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/paypal\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"banktransfer\",\"description\":\"Überweisung\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"1000000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/banktransfer.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/banktransfer%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/banktransfer.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/banktransfer\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"ideal\",\"description\":\"iDEAL\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"50000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/ideal.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/ideal%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/ideal.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/ideal\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"bancontact\",\"description\":\"Bancontact\",\"minimumAmount\":{\"value\":\"0.02\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"50000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/bancontact.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/bancontact%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/bancontact.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/bancontact\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"eps\",\"description\":\"eps\",\"minimumAmount\":{\"value\":\"1.00\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"50000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/eps.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/eps%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/eps.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/eps\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"przelewy24\",\"description\":\"Przelewy24\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"12815.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/przelewy24.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/przelewy24%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/przelewy24.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/przelewy24\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"kbc\",\"description\":\"KBC/CBC Zahlungsbutton\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"50000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/kbc.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/kbc%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/kbc.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/kbc\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"belfius\",\"description\":\"Belfius Pay Button\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"50000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/belfius.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/belfius%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/belfius.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/belfius\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"bancomatpay\",\"description\":\"Bancomat Pay\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"50000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/bancomatpay.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/bancomatpay%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/bancomatpay.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/bancomatpay\",\"type\":\"application/hal+json\"}}}]}" - }, - { - "action": "setCustomField", - "name": "sctm_mollie_profile_id", - "value": "pfl_SPkYGiEQjf" - } - ] + "actions": [ + { + "actions": [ + { + "action": "setCustomField", + "name": "sctm_mollie_profile_id", + "value": "pfl_SPkYGi***" + }, + { + "action": "setCustomField", + "name": "sctm_payment_methods_response", + "value": "{\"count\":7,\"methods\":[{\"id\":\"przelewy24\",\"name\":{\"en-GB\":\"Przelewy24\",\"de-DE\":\"Przelewy24\",\"pl-PL\":\"Przelewy24\"},\"description\":{\"en-GB\":\"\",\"de-DE\":\"Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of \",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/przelewy24.svg\",\"order\":4},{\"id\":\"banktransfer\",\"name\":{\"en-GB\":\"Bank transfer\",\"de-DE\":\"Bank transfer\",\"pl-PL\":\"Bank transfer\"},\"description\":{\"en-GB\":\"\",\"de-DE\":\"\",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/banktransfer.svg\",\"order\":3},{\"id\":\"applepay\",\"name\":{\"en-GB\":\"Apple Pay\",\"de-DE\":\"Apple Pay\",\"pl-PL\":\"Apple Pay\"},\"description\":{\"en-GB\":\"Apple Pay description\",\"de-DE\":\"Apple Pay description\",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/applepay.svg\",\"order\":2},{\"id\":\"paypal\",\"name\":{\"en-GB\":\"PayPal\",\"de-DE\":\"PayPal\",\"pl-PL\":\"PayPal\"},\"description\":{\"en-GB\":\"\",\"de-DE\":\"\",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/paypal.svg\",\"order\":0},{\"id\":\"ideal\",\"name\":{\"en-GB\":\"iDEAL\",\"de-DE\":\"iDEAL\",\"pl-PL\":\"iDEAL\"},\"description\":{\"en-GB\":\"\",\"de-DE\":\"\",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/ideal.svg\",\"order\":0},{\"id\":\"bancontact\",\"name\":{\"en-GB\":\"Bancontact\",\"de-DE\":\"Bancontact\",\"pl-PL\":\"Bancontact\"},\"description\":{\"en-GB\":\"\",\"de-DE\":\"\",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/bancontact.svg\",\"order\":0},{\"id\":\"kbc\",\"name\":{\"en-GB\":\"KBC/CBC Payment Button\",\"de-DE\":\"KBC/CBC Payment Button\",\"pl-PL\":\"KBC/CBC Payment Button\"},\"description\":{\"en-GB\":\"\",\"de-DE\":\"\",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/kbc.svg\",\"order\":0}]}" + } + ] + } + ] } ``` diff --git a/processor/src/service/payment.service.ts b/processor/src/service/payment.service.ts index 3866ef9..248e578 100644 --- a/processor/src/service/payment.service.ts +++ b/processor/src/service/payment.service.ts @@ -60,6 +60,80 @@ import { parseStringToJsonObject } from '../utils/app.utils'; import ApplePaySession from '@mollie/api-client/dist/types/src/data/applePaySession/ApplePaySession'; import { getMethodConfigObjects } from '../commercetools/customObjects.commercetools'; +type CustomMethod = { + id: string; + name: Record; + description: Record; + image: string; + order: number; +}; + +/** + * Validates and sorts the payment methods. + * + * @param {CustomMethod[]} methods - The list of payment methods. + * @param {CustomObject[]} configObjects - The configuration objects. + * @return {CustomMethod[]} - The validated and sorted payment methods. + */ +const validateAndSortMethods = (methods: CustomMethod[], configObjects: CustomObject[]): CustomMethod[] => { + if (!configObjects.length) { + return methods.filter( + (method: CustomMethod) => SupportedPaymentMethods[method.id.toString() as SupportedPaymentMethods], + ); + } + + return methods + .filter((method) => isValidMethod(method, configObjects)) + .map((method) => mapMethodToCustomMethod(method, configObjects)) + .sort((a, b) => b.order - a.order); // Descending order sort +}; + +/** + * Checks if a method is valid based on the configuration objects. + * + * @param {CustomMethod} method - The payment method. + * @param {CustomObject[]} configObjects - The configuration objects. + * @return {boolean} - True if the method is valid, false otherwise. + */ +const isValidMethod = (method: CustomMethod, configObjects: CustomObject[]): boolean => { + return ( + !!configObjects.find((config) => config.key === method.id && config.value.status === 'Active') && + !!SupportedPaymentMethods[method.id.toString() as SupportedPaymentMethods] + ); +}; + +/** + * Maps a payment method to a custom method. + * + * @param {CustomMethod} method - The payment method. + * @param {CustomObject[]} configObjects - The configuration objects. + * @return {CustomMethod} - The custom method. + */ +const mapMethodToCustomMethod = (method: CustomMethod, configObjects: CustomObject[]): CustomMethod => { + const config = configObjects.find((config) => config.key === method.id); + + return { + id: method.id, + name: config?.value?.name, + description: config?.value?.description, + image: config?.value?.imageUrl, + order: config?.value?.displayOrder || 0, + }; +}; + +/** + * Determines if the card component should be enabled. + * + * @param {CustomMethod[]} validatedMethods - The validated payment methods. + * @return {boolean} - True if the card component should be enabled, false otherwise. + */ +const shouldEnableCardComponent = (validatedMethods: CustomMethod[]): boolean => { + return ( + toBoolean(readConfiguration().mollie.cardComponent, true) && + validatedMethods.some((method) => method.id === PaymentMethod.creditcard) + ); +}; + /** * Handles listing payment methods by payment. * @@ -72,27 +146,21 @@ export const handleListPaymentMethodsByPayment = async (ctPayment: Payment): Pro const mollieOptions = await mapCommercetoolsPaymentCustomFieldsToMollieListParams(ctPayment); const methods: List = await listPaymentMethods(mollieOptions); const configObjects: CustomObject[] = await getMethodConfigObjects(); - let validatedMethods: Method[] = []; - if (configObjects.length) { - validatedMethods = methods.filter( - (method) => - !!configObjects.find((config) => config.key === method.id && config.value.status === 'Active') && - SupportedPaymentMethods[method.id.toString() as SupportedPaymentMethods], - ); - } else { - validatedMethods = methods.filter( - (method: Method) => SupportedPaymentMethods[method.id.toString() as SupportedPaymentMethods], - ); - } - - const enableCardComponent = - toBoolean(readConfiguration().mollie.cardComponent, true) && - validatedMethods.filter((method: Method) => method.id === PaymentMethod.creditcard).length > 0; + const customMethods = methods.map((method) => ({ + id: method.id, + name: { 'en-GB': method.description }, + description: { 'en-GB': '' }, + image: method.image.svg, + order: 0, + })); + const validatedMethods = validateAndSortMethods(customMethods, configObjects); + + const enableCardComponent = shouldEnableCardComponent(validatedMethods); const ctUpdateActions: UpdateAction[] = []; if (enableCardComponent) { validatedMethods.splice( - validatedMethods.findIndex((method: Method) => method.id === PaymentMethod.creditcard), + validatedMethods.findIndex((method) => method.id === PaymentMethod.creditcard), 1, ); }