From 86043f00b5625c0b8e9e169d9b237595c66d3c17 Mon Sep 17 00:00:00 2001 From: Emanuele Dall'Ara <71103219+LeleDallas@users.noreply.github.com> Date: Wed, 11 Dec 2024 17:45:21 +0100 Subject: [PATCH] chore: [IOBP-964] Add zendesk payment subcategories (#6522) > [!WARNING] > this PR depends on https://github.com/pagopa/io-services-metadata/pull/892 > this PR depends on https://github.com/pagopa/io-dev-api-server/pull/446 ## Short description This pull request includes the addition of a new API request type for fetching `Zendesk` payment configuration, updates to the Redux store and sagas to handle this new configuration, and modifications to the payment failure support modal to utilize the new configuration. ## List of changes proposed in this pull request - Added a new type `GetZendeskPaymentConfigT` and a corresponding API request `getZendeskPaymentConfig` to fetch the Zendesk payment configuration - `handleGetZendeskPaymentConfig` to manage the side effects of fetching the Zendesk payment configuration and integrated it into the `watchZendeskSupportSaga` - Updated the payment failure support modal to dispatch the `getZendeskPaymentConfig` action and utilize the fetched configuration to set custom fields for Zendesk tickets ## How to test Ensure that the outcomes are accurately mapped to the corresponding subcategory when opening a ticket --------- Co-authored-by: Alessandro --- package.json | 2 +- ts/api/content.ts | 23 ++++++++- .../hooks/usePaymentFailureSupportModal.tsx | 45 ++++++++++++++---- .../checkout/utils/__tests__/index.test.ts | 28 +++++++++++ ts/features/payments/checkout/utils/index.ts | 20 ++++++++ ts/features/zendesk/analytics/index.ts | 4 ++ ts/features/zendesk/saga/index.ts | 10 +++- .../handleGetZendeskPaymentConfig.ts | 47 +++++++++++++++++++ .../screens/ZendeskSupportHelpCenter.tsx | 2 + ts/features/zendesk/store/actions/index.ts | 13 ++++- .../store/reducers/__tests__/index.test.ts | 3 +- ts/features/zendesk/store/reducers/index.ts | 36 +++++++++++++- ts/utils/supportAssistance.ts | 6 ++- 13 files changed, 221 insertions(+), 18 deletions(-) create mode 100644 ts/features/zendesk/saga/networking/handleGetZendeskPaymentConfig.ts diff --git a/package.json b/package.json index 89395ddc819..6248762bd7b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "io_session_manager_api": "https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/io-session-manager@1.0.0/apps/io-session-manager/api/internal.yaml", "io_session_manager_public_api": "https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/io-session-manager@1.0.0/apps/io-session-manager/api/public.yaml", "io_public_api": "https://raw.githubusercontent.com/pagopa/io-backend/v16.4.0-RELEASE/api_public.yaml", - "io_content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.49/definitions.yml", + "io_content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.50/definitions.yml", "io_cgn_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v16.4.0-RELEASE/api_cgn.yaml", "io_cgn_merchants_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v16.4.0-RELEASE/api_cgn_operator_search.yaml", "api_fci": "https://raw.githubusercontent.com/pagopa/io-backend/v16.4.0-RELEASE/api_io_sign.yaml", diff --git a/ts/api/content.ts b/ts/api/content.ts index 315c9d5c8f7..399ced6f022 100644 --- a/ts/api/content.ts +++ b/ts/api/content.ts @@ -14,6 +14,7 @@ import { Municipality as MunicipalityMedadata } from "../../definitions/content/ import { SpidIdps } from "../../definitions/content/SpidIdps"; import { VersionInfo } from "../../definitions/content/VersionInfo"; import { Zendesk } from "../../definitions/content/Zendesk"; +import { ZendeskSubcategoriesErrors } from "../../definitions/content/ZendeskSubcategoriesErrors"; import { CoBadgeServices } from "../../definitions/pagopa/cobadge/configuration/CoBadgeServices"; import { AbiListResponse } from "../../definitions/pagopa/walletv2/AbiListResponse"; import { contentRepoUrl } from "../config"; @@ -140,6 +141,22 @@ const getZendeskConfigT: GetZendeskConfigT = { headers: () => ({}), response_decoder: basicResponseDecoder(Zendesk) }; + +type GetZendeskPaymentConfigT = IGetApiRequestType< + void, + never, + never, + BasicResponseType +>; + +const getZendeskPaymentConfig: GetZendeskPaymentConfigT = { + method: "get", + url: () => "/assistanceTools/payment/zendeskOutcomeMapping.json", + query: _ => ({}), + headers: () => ({}), + response_decoder: basicResponseDecoder(ZendeskSubcategoriesErrors) +}; + /** * A client for the static content */ @@ -157,6 +174,10 @@ export function ContentClient(fetchApi: typeof fetch = defaultRetryingFetch()) { getCobadgeServices: createFetchRequestForApi(getCobadgeServicesT, options), getVersionInfo: createFetchRequestForApi(getVersionInfoT, options), getIdps: createFetchRequestForApi(getIdpsT, options), - getZendeskConfig: createFetchRequestForApi(getZendeskConfigT, options) + getZendeskConfig: createFetchRequestForApi(getZendeskConfigT, options), + getZendeskPaymentConfig: createFetchRequestForApi( + getZendeskPaymentConfig, + options + ) }; } diff --git a/ts/features/payments/checkout/hooks/usePaymentFailureSupportModal.tsx b/ts/features/payments/checkout/hooks/usePaymentFailureSupportModal.tsx index 543b745f488..94b33c0ab2d 100644 --- a/ts/features/payments/checkout/hooks/usePaymentFailureSupportModal.tsx +++ b/ts/features/payments/checkout/hooks/usePaymentFailureSupportModal.tsx @@ -11,7 +11,7 @@ import { import { OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import React from "react"; +import React, { useEffect } from "react"; import { Linking } from "react-native"; import { ToolEnum } from "../../../../../definitions/content/AssistanceToolConfig"; import I18n from "../../../../i18n"; @@ -24,30 +24,34 @@ import { addTicketCustomField, appendLog, assistanceToolRemoteConfig, + defaultZendeskPaymentCategory, resetCustomFields, zendeskCategoryId, - zendeskPaymentCategory, zendeskPaymentFailure, zendeskPaymentNav, zendeskPaymentOrgFiscalCode, zendeskPaymentStartOrigin } from "../../../../utils/supportAssistance"; import { + getZendeskPaymentConfig, zendeskSelectedCategory, zendeskSupportStart } from "../../../zendesk/store/actions"; +import { zendeskMapSelector } from "../../../zendesk/store/reducers"; +import { formatPaymentNoticeNumber } from "../../common/utils"; import { selectOngoingPaymentHistory } from "../../history/store/selectors"; +import { + WalletOnboardingOutcome, + getWalletOnboardingOutcomeEnumByValue +} from "../../onboarding/types/OnboardingOutcomeEnum"; import { walletPaymentRptIdSelector } from "../store/selectors"; import { WalletPaymentOutcome, getWalletPaymentOutcomeEnumByValue } from "../types/PaymentOutcomeEnum"; import { WalletPaymentFailure } from "../types/WalletPaymentFailure"; -import { formatPaymentNoticeNumber } from "../../common/utils"; -import { - getWalletOnboardingOutcomeEnumByValue, - WalletOnboardingOutcome -} from "../../onboarding/types/OnboardingOutcomeEnum"; +import { getSubCategoryFromFaultCode } from "../utils"; +import { isReady } from "../../../../common/model/RemoteValue"; type PaymentFailureSupportModalParams = { failure?: WalletPaymentFailure; @@ -71,8 +75,14 @@ const usePaymentFailureSupportModal = ({ const choosenTool = assistanceToolRemoteConfig(assistanceToolConfig); const rptId = useIOSelector(walletPaymentRptIdSelector); const paymentHistory = useIOSelector(selectOngoingPaymentHistory); + const zendeskPaymentCategory = useIOSelector(zendeskMapSelector); + const dispatch = useIODispatch(); + useEffect(() => { + dispatch(getZendeskPaymentConfig.request()); + }, [dispatch]); + const faultCodeDetail = failure?.faultCodeDetail || (outcome && @@ -82,8 +92,25 @@ const usePaymentFailureSupportModal = ({ ""; const zendeskAssistanceLogAndStart = () => { + if (!isReady(zendeskPaymentCategory)) { + return; + } + const { payments } = zendeskPaymentCategory.value; + const subCategory = getSubCategoryFromFaultCode(payments, faultCodeDetail); + resetCustomFields(); - addTicketCustomField(zendeskCategoryId, zendeskPaymentCategory.value); + // attach the main zendesk category to the ticket + addTicketCustomField( + zendeskCategoryId, + defaultZendeskPaymentCategory.value + ); + + if (subCategory) { + // if a subcategory is found, we attach its id and value to the ticket + const { value, zendeskSubCategoryId } = subCategory; + addTicketCustomField(zendeskSubCategoryId, value); + } + addTicketCustomField(zendeskPaymentOrgFiscalCode, organizationFiscalCode); addTicketCustomField(zendeskPaymentNav, paymentNoticeNumber); addTicketCustomField(zendeskPaymentFailure, faultCodeDetail); @@ -100,7 +127,7 @@ const usePaymentFailureSupportModal = ({ assistanceForFci: false }) ); - dispatch(zendeskSelectedCategory(zendeskPaymentCategory)); + dispatch(zendeskSelectedCategory(defaultZendeskPaymentCategory)); }; const handleAskAssistance = () => { diff --git a/ts/features/payments/checkout/utils/__tests__/index.test.ts b/ts/features/payments/checkout/utils/__tests__/index.test.ts index d5981b08bfa..c15aee94415 100644 --- a/ts/features/payments/checkout/utils/__tests__/index.test.ts +++ b/ts/features/payments/checkout/utils/__tests__/index.test.ts @@ -1,11 +1,21 @@ import { getPaymentPhaseFromStep, getPspFlagType, + getSubCategoryFromFaultCode, isDueDateValid, trimAndLimitValue } from ".."; +import { ZendeskSubCategoriesMap } from "../../../../../../definitions/content/ZendeskSubCategoriesMap"; import { WalletPaymentStepEnum } from "../../types"; +const mockCategories: ZendeskSubCategoriesMap = { + subcategories: { + "12345": ["subcategory1"], + "67890": ["subcategory2"] + }, + subcategoryId: "313" +}; + describe("trimAndLimitValue", () => { it("should remove all spaces from the string", () => { const input = "a b c d e"; @@ -136,3 +146,21 @@ describe("getPaymentPhaseFromStep", () => { expect(result).toBe("verifica"); }); }); + +describe("getSubCategoryFromFaultCode", () => { + it("should return the subcategory if the fault code is in the map", () => { + const result = getSubCategoryFromFaultCode(mockCategories, "subcategory1"); + expect(result).toStrictEqual({ + value: "12345", + zendeskSubCategoryId: "313" + }); + }); + + it("should return nullable if the fault code is not in the map or is empty string", () => { + const result = getSubCategoryFromFaultCode(mockCategories, "subcategory3"); + expect(result).toStrictEqual(null); + + const emptyResult = getSubCategoryFromFaultCode(mockCategories, ""); + expect(emptyResult).toStrictEqual(null); + }); +}); diff --git a/ts/features/payments/checkout/utils/index.ts b/ts/features/payments/checkout/utils/index.ts index d468a23f6af..e5c44a0ba5b 100644 --- a/ts/features/payments/checkout/utils/index.ts +++ b/ts/features/payments/checkout/utils/index.ts @@ -1,4 +1,5 @@ import _ from "lodash"; +import { ZendeskSubCategoriesMap } from "../../../../../definitions/content/ZendeskSubCategoriesMap"; import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; import { PaymentMethodManagementTypeEnum } from "../../../../../definitions/pagopa/ecommerce/PaymentMethodManagementType"; import { PaymentMethodResponse } from "../../../../../definitions/pagopa/ecommerce/PaymentMethodResponse"; @@ -78,3 +79,22 @@ export const isDueDateValid = (date: string): string | undefined => { tenYearsFromNow.setFullYear(tenYearsFromNow.getFullYear() + YEARS_TO_EXPIRE); return new Date(date) > tenYearsFromNow ? undefined : formattedDate; }; + +export const getSubCategoryFromFaultCode = ( + data: ZendeskSubCategoriesMap, + statusCode: string +) => { + // check if there is a subcategory array that includes passed element + const subcategoryKey = Object.keys(data.subcategories).find(key => + data.subcategories[key].includes(statusCode) + ); + // if there is, return the mapped subcategory with the zendesk category id + if (subcategoryKey) { + return { + value: subcategoryKey, + zendeskSubCategoryId: data.subcategoryId + }; + } + // if not, return nullable + return null; +}; diff --git a/ts/features/zendesk/analytics/index.ts b/ts/features/zendesk/analytics/index.ts index 9acc4628f76..f91ae63282e 100644 --- a/ts/features/zendesk/analytics/index.ts +++ b/ts/features/zendesk/analytics/index.ts @@ -7,6 +7,7 @@ import { zendeskEnabled } from "../../../config"; import { getNetworkErrorMessage } from "../../../utils/errors"; import { getZendeskConfig, + getZendeskPaymentConfig, zendeskSelectedCategory, zendeskSupportCancel, zendeskSupportCompleted, @@ -22,6 +23,8 @@ const trackZendesk = case getType(zendeskSupportCancel): case getType(getZendeskConfig.request): case getType(getZendeskConfig.success): + case getType(getZendeskPaymentConfig.success): + case getType(getZendeskPaymentConfig.request): return mp.track(action.type); case getType(zendeskSupportStart): return mp.track(action.type, { @@ -38,6 +41,7 @@ const trackZendesk = category: action.payload.value }); case getType(getZendeskConfig.failure): + case getType(getZendeskPaymentConfig.failure): return mp.track(action.type, { reason: getNetworkErrorMessage(action.payload) }); diff --git a/ts/features/zendesk/saga/index.ts b/ts/features/zendesk/saga/index.ts index bd614ca57b9..b9ab66a01f3 100644 --- a/ts/features/zendesk/saga/index.ts +++ b/ts/features/zendesk/saga/index.ts @@ -11,7 +11,8 @@ import { zendeskStartPolling, zendeskSupportCompleted, zendeskSupportStart, - getZendeskToken + getZendeskToken, + getZendeskPaymentConfig } from "../store/actions"; import { ContentClient } from "../../../api/content"; import { dismissSupport } from "../../../utils/supportAssistance"; @@ -33,6 +34,7 @@ import { isDevEnv } from "./../../../utils/environment"; import { zendeskSupport } from "./orchestration"; import { handleGetZendeskConfig } from "./networking/handleGetZendeskConfig"; import { handleHasOpenedTickets } from "./networking/handleHasOpenedTickets"; +import { handleGetZendeskPaymentConfig } from "./networking/handleGetZendeskPaymentConfig"; const ZENDESK_GET_SESSION_POLLING_INTERVAL = ((isDevEnv ? 10 : 60) * 1000) as Millisecond; @@ -89,6 +91,12 @@ export function* watchZendeskSupportSaga() { contentClient.getZendeskConfig ); + yield* takeLatest( + getZendeskPaymentConfig.request, + handleGetZendeskPaymentConfig, + contentClient.getZendeskPaymentConfig + ); + yield* takeLatest(zendeskRequestTicketNumber.request, handleHasOpenedTickets); // close the Zendesk support UI when the identification is requested // this is due since there is a modal clash (iOS only) see https://pagopa.atlassian.net/browse/IABT-1348?filter=10121 diff --git a/ts/features/zendesk/saga/networking/handleGetZendeskPaymentConfig.ts b/ts/features/zendesk/saga/networking/handleGetZendeskPaymentConfig.ts new file mode 100644 index 00000000000..bc31c9e27f0 --- /dev/null +++ b/ts/features/zendesk/saga/networking/handleGetZendeskPaymentConfig.ts @@ -0,0 +1,47 @@ +import { readableReport } from "@pagopa/ts-commons/lib/reporters"; +import * as E from "fp-ts/lib/Either"; +import { call, put } from "typed-redux-saga/macro"; +import { ContentClient } from "../../../../api/content"; +import { SagaCallReturnType } from "../../../../types/utils"; +import { getGenericError, getNetworkError } from "../../../../utils/errors"; +import { getZendeskPaymentConfig } from "../../store/actions"; + +// retrieve the zendesk config from the CDN +export function* handleGetZendeskPaymentConfig( + getZendeskPaymentConfigClient: ReturnType< + typeof ContentClient + >["getZendeskPaymentConfig"] +) { + try { + const getZendeskPaymentConfigResult: SagaCallReturnType< + typeof getZendeskPaymentConfigClient + > = yield* call(getZendeskPaymentConfigClient); + if (E.isRight(getZendeskPaymentConfigResult)) { + if (getZendeskPaymentConfigResult.right.status === 200) { + yield* put( + getZendeskPaymentConfig.success( + getZendeskPaymentConfigResult.right.value + ) + ); + } else { + yield* put( + getZendeskPaymentConfig.failure( + getGenericError( + Error( + `response status ${getZendeskPaymentConfigResult.right.status}` + ) + ) + ) + ); + } + } else { + getZendeskPaymentConfig.failure( + getGenericError( + Error(readableReport(getZendeskPaymentConfigResult.left)) + ) + ); + } + } catch (e) { + yield* put(getZendeskPaymentConfig.failure(getNetworkError(e))); + } +} diff --git a/ts/features/zendesk/screens/ZendeskSupportHelpCenter.tsx b/ts/features/zendesk/screens/ZendeskSupportHelpCenter.tsx index d2810658499..077487db6e3 100644 --- a/ts/features/zendesk/screens/ZendeskSupportHelpCenter.tsx +++ b/ts/features/zendesk/screens/ZendeskSupportHelpCenter.tsx @@ -63,6 +63,7 @@ import ZENDESK_ROUTES from "../navigation/routes"; import { ZendeskStartPayload, getZendeskConfig, + getZendeskPaymentConfig, getZendeskToken, zendeskSupportCancel } from "../store/actions"; @@ -330,6 +331,7 @@ const ZendeskSupportHelpCenter = () => { */ useEffect(() => { dispatch(getZendeskConfig.request()); + dispatch(getZendeskPaymentConfig.request()); }, [dispatch]); // add the signatureRequestId to the ticket custom fields diff --git a/ts/features/zendesk/store/actions/index.ts b/ts/features/zendesk/store/actions/index.ts index b818305565f..1150bb1a8a5 100644 --- a/ts/features/zendesk/store/actions/index.ts +++ b/ts/features/zendesk/store/actions/index.ts @@ -6,6 +6,7 @@ import { import { Zendesk } from "../../../../../definitions/content/Zendesk"; import { ZendeskCategory } from "../../../../../definitions/content/ZendeskCategory"; import { ZendeskSubCategory } from "../../../../../definitions/content/ZendeskSubCategory"; +import { ZendeskSubcategoriesErrors } from "../../../../../definitions/content/ZendeskSubcategoriesErrors"; import { ContextualHelpProps, ContextualHelpPropsMarkdown @@ -95,6 +96,15 @@ export const getZendeskConfig = createAsyncAction( "ZENDESK_CONFIG_FAILURE" )(); +/** + * Request the zendesk payment config + */ +export const getZendeskPaymentConfig = createAsyncAction( + "ZENDESK_PAYMENT_CONFIG_REQUEST", + "ZENDESK_PAYMENT_CONFIG_SUCCESS", + "ZENDESK_PAYMENT_CONFIG_FAILURE" +)(); + // user selected a category export const zendeskSelectedCategory = createStandardAction( "ZENDESK_SELECTED_CATEGORY" @@ -127,4 +137,5 @@ export type ZendeskSupportActions = | ActionType | ActionType | ActionType - | ActionType; + | ActionType + | ActionType; diff --git a/ts/features/zendesk/store/reducers/__tests__/index.test.ts b/ts/features/zendesk/store/reducers/__tests__/index.test.ts index 29128a9d0f5..fd10dd2a10f 100644 --- a/ts/features/zendesk/store/reducers/__tests__/index.test.ts +++ b/ts/features/zendesk/store/reducers/__tests__/index.test.ts @@ -24,7 +24,8 @@ import { ZendeskState } from "../index"; const INITIAL_STATE: ZendeskState = { zendeskConfig: remoteUndefined, - ticketNumber: pot.none + ticketNumber: pot.none, + zendeskSubcategoriesErrorMap: remoteUndefined }; const mockCategory: ZendeskCategory = { diff --git a/ts/features/zendesk/store/reducers/index.ts b/ts/features/zendesk/store/reducers/index.ts index 2a2643aa600..29142ae533a 100644 --- a/ts/features/zendesk/store/reducers/index.ts +++ b/ts/features/zendesk/store/reducers/index.ts @@ -20,10 +20,12 @@ import { zendeskStartPolling, zendeskStopPolling, zendeskSupportStart, - getZendeskToken + getZendeskToken, + getZendeskPaymentConfig } from "../actions"; import { GlobalState } from "../../../../store/reducers/types"; import { ZendeskSubCategory } from "../../../../../definitions/content/ZendeskSubCategory"; +import { ZendeskSubcategoriesErrors } from "../../../../../definitions/content/ZendeskSubcategoriesErrors"; type ZendeskValue = { panicMode: boolean; @@ -34,6 +36,11 @@ type ZendeskValue = { }; export type ZendeskConfig = RemoteValue; +export type ZendeskSubcategoriesErrorsConfig = RemoteValue< + ZendeskSubcategoriesErrors, + NetworkError +>; + export enum ZendeskTokenStatusEnum { SUCCESS = "success", ERROR = "error", @@ -47,11 +54,13 @@ export type ZendeskState = { ticketNumber: pot.Pot; getSessionPollingRunning?: boolean; getZendeskTokenStatus?: ZendeskTokenStatusEnum | "401"; + zendeskSubcategoriesErrorMap: ZendeskSubcategoriesErrorsConfig; }; const INITIAL_STATE: ZendeskState = { zendeskConfig: remoteUndefined, - ticketNumber: pot.none + ticketNumber: pot.none, + zendeskSubcategoriesErrorMap: remoteUndefined }; const reducer = ( @@ -135,6 +144,21 @@ const reducer = ( ...state, ticketNumber: pot.toError(state.ticketNumber, action.payload) }; + case getType(getZendeskPaymentConfig.request): + return { + ...state, + zendeskSubcategoriesErrorMap: remoteLoading + }; + case getType(getZendeskPaymentConfig.success): + return { + ...state, + zendeskSubcategoriesErrorMap: remoteReady(action.payload) + }; + case getType(getZendeskPaymentConfig.failure): + return { + ...state, + zendeskSubcategoriesErrorMap: remoteError(action.payload) + }; } return state; }; @@ -168,4 +192,12 @@ export const zendeskTicketNumberSelector = createSelector( (ticketNumber: pot.Pot): pot.Pot => ticketNumber ); +export const zendeskMapSelector = createSelector( + [ + (state: GlobalState) => + state.assistanceTools.zendesk.zendeskSubcategoriesErrorMap + ], + (zendeskMap): ZendeskSubcategoriesErrorsConfig => zendeskMap +); + export default reducer; diff --git a/ts/utils/supportAssistance.ts b/ts/utils/supportAssistance.ts index 85bdb50293f..395105c2ecc 100644 --- a/ts/utils/supportAssistance.ts +++ b/ts/utils/supportAssistance.ts @@ -115,14 +115,16 @@ export const zendeskidentityProviderId = "4414310934673"; export const zendeskCurrentAppVersionId = "4414316660369"; export const zendeskVersionsHistoryId = "4419641151505"; export const zendeskFciId = "14874226407825"; -export const zendeskPaymentCategory: ZendeskCategory = { - value: "pagamenti_pagopa", + +export const defaultZendeskPaymentCategory: ZendeskCategory = { + value: "io_pagamenti_pagopa", description: { "it-IT": "Pagamento pagoPA", "en-EN": "pagoPA payment", "de-DE": "pagoPA-Zahlung" } }; + export const zendeskPaymentMethodCategory: ZendeskCategory = { value: "metodo_di_pagamento", description: {