Skip to content

Commit

Permalink
chore: [IOBP-964] Add zendesk payment subcategories (#6522)
Browse files Browse the repository at this point in the history
> [!WARNING]
> this PR depends on
pagopa/io-services-metadata#892
> this PR depends on
pagopa/io-dev-api-server#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 <[email protected]>
  • Loading branch information
LeleDallas and Hantex9 authored Dec 11, 2024
1 parent c7eca8a commit 86043f0
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 18 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"io_session_manager_api": "https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/[email protected]/apps/io-session-manager/api/internal.yaml",
"io_session_manager_public_api": "https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/[email protected]/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",
Expand Down
23 changes: 22 additions & 1 deletion ts/api/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -140,6 +141,22 @@ const getZendeskConfigT: GetZendeskConfigT = {
headers: () => ({}),
response_decoder: basicResponseDecoder(Zendesk)
};

type GetZendeskPaymentConfigT = IGetApiRequestType<
void,
never,
never,
BasicResponseType<ZendeskSubcategoriesErrors>
>;

const getZendeskPaymentConfig: GetZendeskPaymentConfigT = {
method: "get",
url: () => "/assistanceTools/payment/zendeskOutcomeMapping.json",
query: _ => ({}),
headers: () => ({}),
response_decoder: basicResponseDecoder(ZendeskSubcategoriesErrors)
};

/**
* A client for the static content
*/
Expand All @@ -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
)
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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 &&
Expand All @@ -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);
Expand All @@ -100,7 +127,7 @@ const usePaymentFailureSupportModal = ({
assistanceForFci: false
})
);
dispatch(zendeskSelectedCategory(zendeskPaymentCategory));
dispatch(zendeskSelectedCategory(defaultZendeskPaymentCategory));
};

const handleAskAssistance = () => {
Expand Down
28 changes: 28 additions & 0 deletions ts/features/payments/checkout/utils/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
});
});
20 changes: 20 additions & 0 deletions ts/features/payments/checkout/utils/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
};
4 changes: 4 additions & 0 deletions ts/features/zendesk/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { zendeskEnabled } from "../../../config";
import { getNetworkErrorMessage } from "../../../utils/errors";
import {
getZendeskConfig,
getZendeskPaymentConfig,
zendeskSelectedCategory,
zendeskSupportCancel,
zendeskSupportCompleted,
Expand All @@ -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, {
Expand All @@ -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)
});
Expand Down
10 changes: 9 additions & 1 deletion ts/features/zendesk/saga/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
zendeskStartPolling,
zendeskSupportCompleted,
zendeskSupportStart,
getZendeskToken
getZendeskToken,
getZendeskPaymentConfig
} from "../store/actions";
import { ContentClient } from "../../../api/content";
import { dismissSupport } from "../../../utils/supportAssistance";
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)));
}
}
2 changes: 2 additions & 0 deletions ts/features/zendesk/screens/ZendeskSupportHelpCenter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import ZENDESK_ROUTES from "../navigation/routes";
import {
ZendeskStartPayload,
getZendeskConfig,
getZendeskPaymentConfig,
getZendeskToken,
zendeskSupportCancel
} from "../store/actions";
Expand Down Expand Up @@ -330,6 +331,7 @@ const ZendeskSupportHelpCenter = () => {
*/
useEffect(() => {
dispatch(getZendeskConfig.request());
dispatch(getZendeskPaymentConfig.request());
}, [dispatch]);

// add the signatureRequestId to the ticket custom fields
Expand Down
13 changes: 12 additions & 1 deletion ts/features/zendesk/store/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -95,6 +96,15 @@ export const getZendeskConfig = createAsyncAction(
"ZENDESK_CONFIG_FAILURE"
)<void, Zendesk, NetworkError>();

/**
* Request the zendesk payment config
*/
export const getZendeskPaymentConfig = createAsyncAction(
"ZENDESK_PAYMENT_CONFIG_REQUEST",
"ZENDESK_PAYMENT_CONFIG_SUCCESS",
"ZENDESK_PAYMENT_CONFIG_FAILURE"
)<void, ZendeskSubcategoriesErrors, NetworkError>();

// user selected a category
export const zendeskSelectedCategory = createStandardAction(
"ZENDESK_SELECTED_CATEGORY"
Expand Down Expand Up @@ -127,4 +137,5 @@ export type ZendeskSupportActions =
| ActionType<typeof getZendeskConfig>
| ActionType<typeof zendeskSelectedCategory>
| ActionType<typeof zendeskRequestTicketNumber>
| ActionType<typeof zendeskSelectedSubcategory>;
| ActionType<typeof zendeskSelectedSubcategory>
| ActionType<typeof getZendeskPaymentConfig>;
3 changes: 2 additions & 1 deletion ts/features/zendesk/store/reducers/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading

0 comments on commit 86043f0

Please sign in to comment.