teamdigitale/italia-app

View on GitHub
ts/features/messages/saga/handleLoadMessageData.ts

Summary

Maintainability
D
2 days
Test Coverage
import { constUndefined, pipe } from "fp-ts/lib/function";
import * as B from "fp-ts/lib/boolean";
import * as O from "fp-ts/lib/Option";
import * as E from "fp-ts/lib/Either";
import * as pot from "@pagopa/ts-commons/lib/pot";
import { call, delay, put, race, select, take } from "typed-redux-saga/macro";
import { ActionType, isActionOf } from "typesafe-actions";
import { readableReport } from "@pagopa/ts-commons/lib/reporters";
import { ServiceId } from "../../../../definitions/backend/ServiceId";
import {
  RequestGetMessageDataActionType,
  cancelGetMessageDataAction,
  getMessageDataAction,
  loadMessageById,
  loadMessageDetails,
  loadThirdPartyMessage,
  upsertMessageStatusAttributes
} from "../store/actions";
import { getPaginatedMessageById } from "../store/reducers/paginatedById";
import { UIMessage, UIMessageDetails, UIMessageId } from "../types";
import { serviceByIdPotSelector } from "../../services/details/store/reducers";
import { loadServiceDetail } from "../../services/details/store/actions/details";
import { messageDetailsByIdSelector } from "../store/reducers/detailsById";
import { thirdPartyFromIdSelector } from "../store/reducers/thirdPartyById";
import { isLoadingOrUpdatingInbox } from "../store/reducers/allPaginated";
import { TagEnum } from "../../../../definitions/backend/MessageCategoryPN";
import { euCovidCertificateEnabled } from "../../../config";
import { isPnEnabledSelector } from "../../../store/reducers/backendStatus";
import { trackPNPushOpened } from "../../pn/analytics";
import { isTestEnv } from "../../../utils/environment";
import { ThirdPartyMessageWithContent } from "../../../../definitions/backend/ThirdPartyMessageWithContent";
import {
  trackMessageDataLoadFailure,
  trackMessageDataLoadPending,
  trackMessageDataLoadRequest,
  trackMessageDataLoadSuccess,
  trackRemoteContentMessageDecodingWarning
} from "../analytics";
import { RemoteContentDetails } from "../../../../definitions/backend/RemoteContentDetails";
import { MessageGetStatusFailurePhaseType } from "../store/reducers/messageGetStatus";
import { ServicePublic } from "../../../../definitions/backend/ServicePublic";

export function* handleLoadMessageData(
  action: ActionType<typeof getMessageDataAction.request>
) {
  yield* race({
    polling: call(loadMessageData, action.payload),
    cancelAction: take(cancelGetMessageDataAction)
  });
}

// eslint-disable-next-line sonarjs/cognitive-complexity
function* loadMessageData({
  messageId,
  fromPushNotification
}: RequestGetMessageDataActionType) {
  trackMessageDataLoadRequest(fromPushNotification);
  while (true) {
    // Make sure that the later `loadMessageById` and `setMessageReadState`
    // are called after any  update on the message list has ended, otherwise
    // we may change the local "read" state of a message and later have it
    // been rewritten by a server delayed response to the ongoing concurrent
    // message list retrieval (this is mostly happening when not coming from
    // an user tap on the message list, like selecting a push notification)
    const isSynchronizingInbox = yield* select(isLoadingOrUpdatingInbox);
    if (!isSynchronizingInbox) {
      break;
    }

    trackMessageDataLoadPending(fromPushNotification);

    yield* delay(500);
  }

  // Make sure that we have the basic message details
  const paginatedMessage = yield* call(getPaginatedMessage, messageId);
  if (!paginatedMessage) {
    yield* call(
      commonFailureHandling,
      "paginatedMessage",
      fromPushNotification
    );
    return;
  }

  // Make sure that we have the service details
  const serviceId = paginatedMessage.serviceId;
  const serviceDetails = yield* call(getServiceDetails, serviceId);
  if (!serviceDetails) {
    yield* call(commonFailureHandling, "serviceDetails", fromPushNotification);
    return;
  }

  // Make sure that we have the message details
  const messageDetails = yield* call(getMessageDetails, messageId);
  if (!messageDetails) {
    yield* call(commonFailureHandling, "messageDetails", fromPushNotification);
    return;
  }

  // If coming from a push notification, we must not show the message details
  // when there are messages preconditions that have to be accepted by the user
  // before reading the message's details. PN is a special case where there
  // are always preconditions and we cannot rely on the `hasPrecondition` flag
  // due to compatibility with older messages
  const isPNMessage = paginatedMessage.category.tag === "PN";
  if (
    fromPushNotification &&
    (isPNMessage || paginatedMessage.hasPrecondition)
  ) {
    if (isPNMessage) {
      trackPNPushOpened();
    }
    yield* call(
      commonFailureHandling,
      "preconditions",
      fromPushNotification,
      true
    );
    return;
  }

  // Make sure to download the third party data if the message has them
  // (PN is always a third party message so in that case make sure to
  // download it regardless of what is in messageDetails)
  const shouldDownloadThirdPartyData =
    messageDetails.hasThirdPartyData || isPNMessage;
  const thirdPartyMessageDetails = shouldDownloadThirdPartyData
    ? yield* call(
        getThirdPartyDataMessage,
        messageId,
        isPNMessage,
        serviceDetails,
        paginatedMessage.category.tag
      )
    : undefined;
  if (shouldDownloadThirdPartyData && !thirdPartyMessageDetails) {
    yield* call(
      commonFailureHandling,
      "thirdPartyMessageDetails",
      fromPushNotification
    );
    return;
  }

  const messageReadCheckSucceded = yield* call(
    setMessageReadIfNeeded,
    paginatedMessage
  );
  if (!messageReadCheckSucceded) {
    yield* call(
      commonFailureHandling,
      "readStatusUpdate",
      fromPushNotification
    );
    return;
  }

  trackMessageDataLoadSuccess(fromPushNotification);
  yield* call(
    dispatchSuccessAction,
    paginatedMessage,
    messageDetails,
    thirdPartyMessageDetails
  );
}

function* getPaginatedMessage(messageId: UIMessageId) {
  const initialMessagePot = yield* select(getPaginatedMessageById, messageId);
  if (!pot.isSome(initialMessagePot) || pot.isError(initialMessagePot)) {
    yield* put(loadMessageById.request({ id: messageId }));

    const outputAction = yield* take([
      loadMessageById.success,
      loadMessageById.failure
    ]);
    if (isActionOf(loadMessageById.failure, outputAction)) {
      return undefined;
    }

    const finalMessagePot = yield* select(getPaginatedMessageById, messageId);
    return pot.toUndefined(finalMessagePot);
  }

  return pot.toUndefined(initialMessagePot);
}

function* getServiceDetails(serviceId: ServiceId) {
  const initialServicePot = yield* select(serviceByIdPotSelector, serviceId);
  if (!pot.isSome(initialServicePot) || pot.isError(initialServicePot)) {
    yield* put(loadServiceDetail.request(serviceId));

    const outputAction = yield* take([
      loadServiceDetail.success,
      loadServiceDetail.failure
    ]);
    if (isActionOf(loadServiceDetail.failure, outputAction)) {
      return undefined;
    }

    const finalServicePot = yield* select(serviceByIdPotSelector, serviceId);
    return pot.toUndefined(finalServicePot);
  }

  return pot.toUndefined(initialServicePot);
}

function* getMessageDetails(messageId: UIMessageId) {
  const initialMessageDetailsPot = yield* select(
    messageDetailsByIdSelector,
    messageId
  );
  if (
    !pot.isSome(initialMessageDetailsPot) ||
    pot.isError(initialMessageDetailsPot)
  ) {
    yield* put(loadMessageDetails.request({ id: messageId }));

    const outputAction = yield* take([
      loadMessageDetails.success,
      loadMessageDetails.failure
    ]);
    if (isActionOf(loadMessageDetails.failure, outputAction)) {
      return undefined;
    }

    const finalMessageDetailsPot = yield* select(
      messageDetailsByIdSelector,
      messageId
    );
    return pot.toUndefined(finalMessageDetailsPot);
  }

  return pot.toUndefined(initialMessageDetailsPot);
}

function* getThirdPartyDataMessage(
  messageId: UIMessageId,
  isPNMessage: boolean,
  service: ServicePublic,
  tag: string
) {
  // Third party data may change anytime, so we must retrieve them on every request

  yield* put(
    loadThirdPartyMessage.request({
      id: messageId,
      serviceId: service.service_id,
      tag
    })
  );

  const outputAction = yield* take([
    loadThirdPartyMessage.success,
    loadThirdPartyMessage.failure
  ]);
  if (isActionOf(loadThirdPartyMessage.failure, outputAction)) {
    return undefined;
  }

  const thirdPartyMessagePot = yield* select(
    thirdPartyFromIdSelector,
    messageId
  );

  const thirdPartyMessageOrUndefined = pot.toUndefined(thirdPartyMessagePot);
  yield* call(
    decodeAndTrackThirdPartyMessageDetailsIfNeeded,
    isPNMessage,
    thirdPartyMessageOrUndefined,
    service,
    tag
  );

  return thirdPartyMessageOrUndefined;
}

function* setMessageReadIfNeeded(paginatedMessage: UIMessage) {
  const userHadReadMessage = paginatedMessage.isRead;
  if (!userHadReadMessage) {
    yield* put(
      upsertMessageStatusAttributes.request({
        message: paginatedMessage,
        update: { tag: "reading" }
      })
    );

    const outputAction = yield* take([
      upsertMessageStatusAttributes.success,
      upsertMessageStatusAttributes.failure
    ]);
    if (isActionOf(upsertMessageStatusAttributes.failure, outputAction)) {
      return undefined;
    }
  }

  return true;
}

function* dispatchSuccessAction(
  paginatedMessage: UIMessage,
  messageDetails: UIMessageDetails,
  thirdPartyMessage: ThirdPartyMessageWithContent | undefined
) {
  const isPNMessageCategory = paginatedMessage.category.tag === TagEnum.PN;
  const containsPayment = pipe(
    isPNMessageCategory,
    B.fold(
      () => pipe(messageDetails.paymentData, O.fromNullable, O.isSome),
      constUndefined
    )
  );
  const attachmentCount =
    thirdPartyMessage?.third_party_message.attachments?.length ?? 0;

  const isPnEnabled = yield* select(isPnEnabledSelector);

  yield* put(
    getMessageDataAction.success({
      containsAttachments: attachmentCount > 0,
      containsPayment,
      euCovidCerficateAuthCode: euCovidCertificateEnabled
        ? messageDetails.euCovidCertificate?.authCode
        : undefined,
      firstTimeOpening: !paginatedMessage.isRead,
      hasRemoteContent: !!thirdPartyMessage,
      isPNMessage: isPnEnabled && isPNMessageCategory,
      messageId: paginatedMessage.id,
      organizationFiscalCode: paginatedMessage.organizationFiscalCode,
      organizationName: paginatedMessage.organizationName,
      serviceId: paginatedMessage.serviceId,
      serviceName: paginatedMessage.serviceName
    })
  );
}

function* commonFailureHandling(
  phase: MessageGetStatusFailurePhaseType,
  loadingStartedFromPushNotification: boolean,
  blockedFromPushNotificationOpt: boolean | undefined = undefined
) {
  yield* call(
    trackMessageDataLoadFailure,
    loadingStartedFromPushNotification,
    phase
  );
  yield* put(
    getMessageDataAction.failure({ blockedFromPushNotificationOpt, phase })
  );
}

const decodeAndTrackThirdPartyMessageDetailsIfNeeded = (
  isPNMessage: boolean,
  thirdPartyMessageOrUndefined: ThirdPartyMessageWithContent | undefined,
  service: ServicePublic,
  tag: string
) =>
  pipe(
    thirdPartyMessageOrUndefined,
    O.fromNullable,
    O.filter(_ => !isPNMessage),
    O.chainNullableK(
      thirdPartyMessage => thirdPartyMessage.third_party_message.details
    ),
    O.map(details =>
      pipe(
        details,
        RemoteContentDetails.decode,
        E.mapLeft(errors =>
          pipe(errors, readableReport, reason =>
            trackRemoteContentMessageDecodingWarning(
              service.service_id,
              service.service_name,
              service.organization_name,
              service.organization_fiscal_code,
              tag,
              reason
            )
          )
        )
      )
    )
  );

export const testable = isTestEnv
  ? {
      commonFailureHandling,
      loadMessageData,
      decodeAndTrackThirdPartyMessageDetailsIfNeeded,
      dispatchSuccessAction,
      setMessageReadIfNeeded,
      getPaginatedMessage,
      getServiceDetails,
      getMessageDetails,
      getThirdPartyDataMessage
    }
  : undefined;