From 171b8082cb2bb39748b8d1ed0d0767daf8abd7ec Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Thu, 16 Jan 2025 17:15:50 +0100 Subject: [PATCH 1/2] Paid payment badge from message details --- .../entities/__tests__/payments.test.ts | 94 +++++++++++++++++++ ts/store/reducers/entities/payments.ts | 33 +++++++ 2 files changed, 127 insertions(+) create mode 100644 ts/store/reducers/entities/__tests__/payments.test.ts 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..6e72946a562 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,13 @@ export const paymentByRptIdReducer = ( transactionId: undefined } }; + // This action is dispatched by the payment status update + // saga that is trigger upon entering message details + 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 +65,27 @@ export const paymentByRptIdReducer = ( } }; +const paymentByRptIdStateFromUpdatePaymentForMessageFailure = ( + payload: UpdatePaymentForMessageFailure, + state: PaymentByRptIdState +): PaymentByRptIdState => { + const isPaidPayment = isPaidPaymentFromDetailV2Enum(payload.details); + if (!isPaidPayment) { + return state; + } + const rptId = payload.paymentId; + const inMemoryPaymentData = state[rptId]; + if (inMemoryPaymentData != null) { + return state; + } + return { + ...state, + [rptId]: { + kind: "DUPLICATED" + } + }; +}; + // Selectors export const paymentsByRptIdSelector = ( From 23987cba16b7b8d1201ab2e9532c2c5e994ef52d Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Fri, 17 Jan 2025 10:39:17 +0100 Subject: [PATCH 2/2] Comments --- ts/store/reducers/entities/payments.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ts/store/reducers/entities/payments.ts b/ts/store/reducers/entities/payments.ts index 6e72946a562..da8e3eeb79f 100644 --- a/ts/store/reducers/entities/payments.ts +++ b/ts/store/reducers/entities/payments.ts @@ -49,8 +49,9 @@ export const paymentByRptIdReducer = ( transactionId: undefined } }; - // This action is dispatched by the payment status update - // saga that is trigger upon entering message details + // 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, @@ -69,15 +70,19 @@ 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]: {