teamdigitale/italia-app

View on GitHub
ts/features/messages/components/MessageDetail/MessagePaymentItem.tsx

Summary

Maintainability
A
1 hr
Test Coverage
import {
  ModulePaymentNotice,
  PaymentNoticeStatus,
  VSpacer,
  useIOToast
} from "@pagopa/io-app-design-system";
import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";
import React, { useCallback, useEffect } from "react";
import { View } from "react-native";
import { PaymentAmount } from "../../../../../definitions/backend/PaymentAmount";
import I18n from "../../../../i18n";
import {
  useIODispatch,
  useIOSelector,
  useIOStore
} from "../../../../store/hooks";
import { updatePaymentForMessage } from "../../store/actions";
import {
  canNavigateToPaymentFromMessageSelector,
  paymentStatusForUISelector,
  shouldUpdatePaymentSelector
} from "../../store/reducers/payments";
import { UIMessageId } from "../../types";
import { Detail_v2Enum } from "../../../../../definitions/backend/PaymentProblemJson";
import { PaymentRequestsGetResponse } from "../../../../../definitions/backend/PaymentRequestsGetResponse";
import {
  RemoteValue,
  fold,
  isError
} from "../../../../common/model/RemoteValue";
import { format } from "../../../../utils/dates";
import {
  cleanTransactionDescription,
  getV2ErrorMainType
} from "../../../../utils/payment";
import {
  centsToAmount,
  formatNumberAmount
} from "../../../../utils/stringBuilder";
import { initializeAndNavigateToWalletForPayment } from "../../utils";
import { getBadgeTextByPaymentNoticeStatus } from "../../utils/strings";
import { formatPaymentNoticeNumber } from "../../../payments/common/utils";
import { isNewPaymentSectionEnabledSelector } from "../../../../store/reducers/backendStatus";
import { ServiceId } from "../../../../../definitions/backend/ServiceId";
import { trackPNPaymentStart } from "../../../pn/analytics";
import { computeAndTrackPaymentStart } from "./detailsUtils";

type MessagePaymentItemProps = {
  hideExpirationDate?: boolean;
  index?: number;
  isPNPayment?: boolean;
  messageId: UIMessageId;
  noSpaceOnTop?: boolean;
  noticeNumber: string;
  paymentAmount?: PaymentAmount;
  rptId: string;
  serviceId: ServiceId;
  willNavigateToPayment?: () => void;
};

type ProcessedPaymentUIData = {
  paymentNoticeStatus: Exclude<PaymentNoticeStatus, "default">;
  badgeText: string;
};

const paymentNoticeStatusFromDetailV2Enum = (
  detail: Detail_v2Enum
): Exclude<PaymentNoticeStatus, "default"> => {
  const errorType = getV2ErrorMainType(detail);
  switch (errorType) {
    case "REVOKED":
      return "revoked";
    case "EXPIRED":
      return "expired";
    case "ONGOING":
      return "in-progress";
    case "DUPLICATED":
      return "paid";
  }
  // Here EC (an error on the ente-side) is treated like a generic
  // ERROR since it is later specialized in the payment flow
  return "error";
};

const processedUIPaymentFromDetailV2Enum = (
  detail: Detail_v2Enum
): ProcessedPaymentUIData =>
  pipe(detail, paymentNoticeStatusFromDetailV2Enum, paymentNoticeStatus => ({
    paymentNoticeStatus,
    badgeText: getBadgeTextByPaymentNoticeStatus(paymentNoticeStatus)
  }));

const modulePaymentNoticeForUndefinedOrLoadingPayment = () => (
  <ModulePaymentNotice
    isLoading={true}
    title={""}
    subtitle={""}
    onPress={_ => undefined}
    paymentNoticeStatus={"error"}
    badgeText={""}
  />
);

const modulePaymentNoticeFromPaymentStatus = (
  hideExpirationDate: boolean,
  noticeNumber: string,
  paymentStatus: RemoteValue<PaymentRequestsGetResponse, Detail_v2Enum>,
  paymentCallback: () => void
) =>
  fold(
    paymentStatus,
    modulePaymentNoticeForUndefinedOrLoadingPayment,
    modulePaymentNoticeForUndefinedOrLoadingPayment,
    payablePayment => {
      const dueDateOrUndefined = pipe(
        payablePayment.dueDate,
        O.fromNullable,
        O.filter(_ => !hideExpirationDate),
        O.map(
          dueDate =>
            `${I18n.t("wallet.firstTransactionSummary.dueDate")} ${format(
              dueDate,
              "DD/MM/YYYY"
            )}`
        ),
        O.toUndefined
      );
      const description = cleanTransactionDescription(
        payablePayment.causaleVersamento
      );
      const formattedAmount = pipe(
        payablePayment.importoSingoloVersamento,
        centsToAmount,
        formatNumberAmount,
        formattedAmountNumber => `${formattedAmountNumber} €`
      );
      return (
        <ModulePaymentNotice
          title={dueDateOrUndefined}
          subtitle={description}
          paymentNoticeStatus="default"
          paymentNoticeAmount={formattedAmount}
          onPress={paymentCallback}
        />
      );
    },
    processedPaymentDetails => {
      const formattedPaymentNoticeNumber =
        formatPaymentNoticeNumber(noticeNumber);
      const { paymentNoticeStatus, badgeText } =
        processedUIPaymentFromDetailV2Enum(processedPaymentDetails);
      return (
        <ModulePaymentNotice
          title={I18n.t("features.messages.payments.noticeCode")}
          subtitle={formattedPaymentNoticeNumber}
          onPress={paymentCallback}
          paymentNoticeStatus={paymentNoticeStatus}
          badgeText={badgeText}
        />
      );
    }
  );

export const MessagePaymentItem = ({
  hideExpirationDate = false,
  index = 0,
  isPNPayment = false,
  messageId,
  noSpaceOnTop = false,
  noticeNumber,
  paymentAmount = undefined,
  rptId,
  serviceId,
  willNavigateToPayment = undefined
}: MessagePaymentItemProps) => {
  const dispatch = useIODispatch();
  const store = useIOStore();
  const toast = useIOToast();

  const shouldUpdatePayment = shouldUpdatePaymentSelector(
    store.getState(),
    messageId,
    rptId
  );
  const paymentStatusForUI = useIOSelector(state =>
    paymentStatusForUISelector(state, messageId, rptId)
  );

  const canNavigateToPayment = useIOSelector(state =>
    canNavigateToPaymentFromMessageSelector(state)
  );

  // Checks if the new wallet section is enabled
  const isNewWalletSectionEnabled = useIOSelector(
    isNewPaymentSectionEnabledSelector
  );
  const startPaymentCallback = useCallback(() => {
    initializeAndNavigateToWalletForPayment(
      isNewWalletSectionEnabled,
      messageId,
      rptId,
      isError(paymentStatusForUI),
      paymentAmount,
      canNavigateToPayment,
      dispatch,
      () => {
        if (isPNPayment) {
          trackPNPaymentStart();
        } else {
          computeAndTrackPaymentStart(serviceId, store.getState());
        }
      },
      () => toast.error(I18n.t("genericError")),
      () => willNavigateToPayment?.()
    );
  }, [
    canNavigateToPayment,
    dispatch,
    isNewWalletSectionEnabled,
    isPNPayment,
    messageId,
    paymentAmount,
    paymentStatusForUI,
    rptId,
    serviceId,
    store,
    toast,
    willNavigateToPayment
  ]);
  useEffect(() => {
    if (shouldUpdatePayment) {
      const updateAction = updatePaymentForMessage.request({
        messageId,
        paymentId: rptId,
        serviceId
      });
      dispatch(updateAction);
    }
  }, [dispatch, messageId, isPNPayment, rptId, serviceId, shouldUpdatePayment]);
  return (
    <View>
      {!noSpaceOnTop && <VSpacer size={index > 0 ? 8 : 24} />}
      {modulePaymentNoticeFromPaymentStatus(
        hideExpirationDate,
        noticeNumber,
        paymentStatusForUI,
        startPaymentCallback
      )}
    </View>
  );
};