All files / src/features/Merchants/MerchantSidePanel/Modals/MatchReport/hooks useMatchTranslation.tsx

92.3% Statements 24/26
88.23% Branches 15/17
100% Functions 5/5
100% Lines 21/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103                                                                          5x       7x             5x             73x       7x       73x                 73x 17x 17x       73x 73x   73x   7x 7x 7x 7x     73x 73x 62x 58x   73x                
import { customInstance } from "@services/api";
import { AxiosError } from "axios";
import { useCallback, useEffect, useRef } from "react";
import { useMutation } from "react-query";
 
import {
  MATCH_TRANSLATION_MAX_ATTEMPTS,
  matchTranslationUrl,
} from "../matchTranslation.constants";
 
// Response contract for POST /merchants/{id}/mastercard-match/translation
// (Story 1 / BE). The backend proxies OpenAI, persists the result keyed to the
// MATCH request, and returns the stored summary on repeat calls.
export interface MatchTranslationResponse {
  summary: string;
}
 
export type MatchTranslationStatus = "idle" | "loading" | "success" | "error";
 
interface UseMatchTranslationArgs {
  merchantID: number;
  // The Mastercard termination-inquiry JSON to translate. Undefined while the
  // live inquiry is still loading, on error, or when there is no result.
  matchResult?: Record<string, unknown>;
  // A summary already persisted for this MATCH (surfaced on saved reports via
  // the report read-back). When present the panel shows it with no new call.
  existingSummary?: string | null;
}
 
export interface UseMatchTranslationResult {
  status: MatchTranslationStatus;
  summary?: string;
  translate: () => void;
  canTranslate: boolean;
  canRetry: boolean;
}
 
const generateMatchTranslation = async (
  merchantID: number,
  matchResult: Record<string, unknown>,
): Promise<MatchTranslationResponse | null> => {
  return customInstance({
    url: matchTranslationUrl(merchantID),
    method: "POST",
    data: { matchResult },
  });
};
 
export const useMatchTranslation = ({
  merchantID,
  matchResult,
  existingSummary,
}: UseMatchTranslationArgs): UseMatchTranslationResult => {
  // react-query's own retry is disabled; retries are user-initiated and
  // bounded by MATCH_TRANSLATION_MAX_ATTEMPTS (AC011).
  const mutation = useMutation<
    MatchTranslationResponse | null,
    AxiosError,
    Record<string, unknown>
  >((payload) => generateMatchTranslation(merchantID, payload), {
    retry: false,
  });
 
  const attemptsRef = useRef(0);
 
  // When the underlying MATCH result changes (the live inquiry re-fires with a
  // new ref on every open, or the user reruns the check), clear any prior
  // error/success and restore the retry budget — otherwise, once the bounded
  // attempts are exhausted against one result, the panel stays stuck in its
  // error state and the new result can never be translated. `mutation.reset` is
  // bound to a stable observer, so a change of `matchResult` is the only trigger
  // we need to depend on.
  useEffect(() => {
    attemptsRef.current = 0;
    mutation.reset();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [matchResult]);
 
  const generatedSummary = mutation.data?.summary;
  const summary = existingSummary ?? generatedSummary ?? undefined;
 
  const translate = useCallback(() => {
    // AC005 — never re-request once a summary exists; AC011 — cap attempts.
    Iif (!matchResult || summary) return;
    Iif (attemptsRef.current >= MATCH_TRANSLATION_MAX_ATTEMPTS) return;
    attemptsRef.current += 1;
    mutation.mutate(matchResult);
  }, [matchResult, summary, mutation]);
 
  let status: MatchTranslationStatus = "idle";
  if (mutation.isLoading) status = "loading";
  else if (summary) status = "success";
  else if (mutation.isError) status = "error";
 
  return {
    status,
    summary,
    translate,
    canTranslate: !!matchResult && !summary,
    canRetry: attemptsRef.current < MATCH_TRANSLATION_MAX_ATTEMPTS,
  };
};