diff --git a/ts/store/reducers/entities/__tests__/payments.test.ts b/ts/store/reducers/entities/__tests__/payments.test.ts new file mode 100644 index 00000000000..a55847a5a57 --- /dev/null +++ b/ts/store/reducers/entities/__tests__/payments.test.ts @@ -0,0 +1,94 @@ +import { Detail_v2Enum } from "../../../../../definitions/backend/PaymentProblemJson"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; +import { + updatePaymentForMessage, + UpdatePaymentForMessageFailure +} from "../../../../features/messages/store/actions"; +import { UIMessageId } from "../../../../features/messages/types"; +import { isPaidPaymentFromDetailV2Enum } from "../../../../utils/payment"; +import { paymentByRptIdReducer, PaymentByRptIdState } from "../payments"; + +describe("payments", () => { + describe("paymentByRptIdReducer", () => { + const rptId1 = "01234567890012345678912345610"; + const rptId2 = "01234567890012345678912345620"; + const initialState: PaymentByRptIdState = { + [rptId1]: { + kind: "COMPLETED", + transactionId: 15 + }, + [rptId2]: { + kind: "DUPLICATED" + } + }; + const payload: UpdatePaymentForMessageFailure = { + details: Detail_v2Enum.PAA_PAGAMENTO_DUPLICATO, + messageId: "5a15aba4-7cd6-490b-b3fe-cf766f731a2f" as UIMessageId, + paymentId: "01234567890012345678912345630", + serviceId: "75c046cf-77a7-4d33-9c3f-578e00379b55" as ServiceId + }; + + Object.values(Detail_v2Enum) + .filter(detailV2Enum => !isPaidPaymentFromDetailV2Enum(detailV2Enum)) + .forEach(detailV2Enum => { + it(`should return the input state if the failure details are '${detailV2Enum}'`, () => { + const outputState = paymentByRptIdReducer( + initialState, + updatePaymentForMessage.failure({ + ...payload, + details: detailV2Enum + }) + ); + expect(outputState).toBe(initialState); + }); + }); + it(`should return the input state if failure details are 'PAA_PAGAMENTO_DUPLICATO' but there is already a record in the state for the input payment`, () => { + const outputState = paymentByRptIdReducer( + initialState, + updatePaymentForMessage.failure({ + ...payload, + paymentId: rptId1, + details: Detail_v2Enum.PAA_PAGAMENTO_DUPLICATO + }) + ); + expect(outputState).toBe(initialState); + }); + it(`should return the input state if failure details are 'PPT_PAGAMENTO_DUPLICATO' but there is already a record in the state for the input payment`, () => { + const outputState = paymentByRptIdReducer( + initialState, + updatePaymentForMessage.failure({ + ...payload, + paymentId: rptId1, + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }) + ); + expect(outputState).toBe(initialState); + }); + it(`should return the updated state if failure details are 'PAA_PAGAMENTO_DUPLICATO' and there is no record for the input payment`, () => { + const outputState = paymentByRptIdReducer( + initialState, + updatePaymentForMessage.failure({ + ...payload, + details: Detail_v2Enum.PAA_PAGAMENTO_DUPLICATO + }) + ); + expect(outputState).toEqual({ + ...initialState, + [payload.paymentId]: { kind: "DUPLICATED" } + }); + }); + it(`should return the updated state if failure details are 'PPT_PAGAMENTO_DUPLICATO' and there is no record for the input payment`, () => { + const outputState = paymentByRptIdReducer( + initialState, + updatePaymentForMessage.failure({ + ...payload, + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }) + ); + expect(outputState).toEqual({ + ...initialState, + [payload.paymentId]: { kind: "DUPLICATED" } + }); + }); + }); +}); diff --git a/ts/store/reducers/entities/payments.ts b/ts/store/reducers/entities/payments.ts index 9c7cbe52a99..da8e3eeb79f 100644 --- a/ts/store/reducers/entities/payments.ts +++ b/ts/store/reducers/entities/payments.ts @@ -8,6 +8,11 @@ import { Action } from "../../actions/types"; import { paymentCompletedSuccess } from "../../../features/payments/checkout/store/actions/orchestration"; import { GlobalState } from "../types"; import { differentProfileLoggedIn } from "../../actions/crossSessions"; +import { + updatePaymentForMessage, + UpdatePaymentForMessageFailure +} from "../../../features/messages/store/actions"; +import { isPaidPaymentFromDetailV2Enum } from "../../../utils/payment"; export type PaidReason = Readonly< | { @@ -44,6 +49,14 @@ export const paymentByRptIdReducer = ( transactionId: undefined } }; + // This action is dispatched by the payment status update saga that is triggered upon + // entering message details. Be aware that the status of a paid payment can never change, + // so there is no need to handle the removal of a no-more-paid payment from the state + case getType(updatePaymentForMessage.failure): + return paymentByRptIdStateFromUpdatePaymentForMessageFailure( + action.payload, + state + ); // clear state if the current profile is different from the previous one case getType(differentProfileLoggedIn): return INITIAL_STATE; @@ -53,6 +66,31 @@ export const paymentByRptIdReducer = ( } }; +const paymentByRptIdStateFromUpdatePaymentForMessageFailure = ( + payload: UpdatePaymentForMessageFailure, + state: PaymentByRptIdState +): PaymentByRptIdState => { + // Only paid payments are tracked from the reducer, ignore the others + const isPaidPayment = isPaidPaymentFromDetailV2Enum(payload.details); + if (!isPaidPayment) { + return state; + } + const rptId = payload.paymentId; + // Make sure not to overwrite any existing data (since it may + // have come from a payment flow, where data are more detailed) + const inMemoryPaymentData = state[rptId]; + if (inMemoryPaymentData != null) { + return state; + } + // Paid payment was not tracked, add it to the reducer's state + return { + ...state, + [rptId]: { + kind: "DUPLICATED" + } + }; +}; + // Selectors export const paymentsByRptIdSelector = (