All files / src/features/Settlements/ReconciliationHub GiveReconciliationHub.tsx

84.61% Statements 11/13
66.66% Branches 8/12
100% Functions 2/2
83.33% Lines 10/12

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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118                                                        1x 34x 33x       33x 33x 7x             33x 33x       33x   33x                                                                                                                                      
import { PortalRootContainerV2 } from "@components/Layout/LayoutV2";
import TechnicalErrorState from "componentsV2/Table/components/TechnicalErrorState";
import { Stack } from "@mui/material";
import { useGetCurrentMerchantId } from "@hooks/common";
import { StyledTitle } from "@shared/Banner/styles";
import GiveBreadcrumb from "@shared/Breadcrumbs/GiveBreadcrumb";
import { useAppTheme } from "@theme/v2/Provider";
import { useParams } from "react-router-dom";
import moment from "moment";
import { useMemo } from "react";
import { CANARY_TZ } from "@utils/date.helpers";
import CostBreakdownByBrand from "./components/CostBreakdownByBrand";
import HubCashPosition from "./components/HubCashPosition";
import HubExportButton from "./components/HubExportButton";
import HubFilterBar from "./components/HubFilterBar";
import HubFinancialDetails from "./components/HubFinancialDetails";
import HubKpiCards from "./components/HubKpiCards";
import HubSkeleton from "./components/HubSkeleton";
import PerMerchantBreakdown from "./components/PerMerchantBreakdown";
import { useReconciliationHub } from "./hooks/useReconciliationHub";
import { isProcessorResolved, useHubFilters } from "./useHubFilters";
 
/**
 * Reconciliation Hub — the primary acquirer-portal settlement view. Composes
 * the filter bar, KPI cards, cash position, financial details, cost-breakdown
 * matrix, and per-merchant breakdown around a shared period / processor /
 * merchant filter state.
 */
const GiveReconciliationHub = () => {
  const theme = useAppTheme();
  const { merchantId } = useGetCurrentMerchantId();
  // When opened for a settlement date picked from the listing, the route param
  // (`/acquirer/settlements/:id`) carries that date as an encoded ISO string —
  // seed the hub's anchor to it so the view scopes to the chosen day.
  const { id } = useParams();
  const initialAnchorDate = useMemo(() => {
    Eif (!id) return undefined;
    // Resolve the picked settlement date in the app's canonical timezone so the
    // day matches how the backend bounds it (see canaryToday in useHubFilters).
    const parsed = moment(decodeURIComponent(id)).tz(CANARY_TZ);
    return parsed.isValid() ? parsed.format("YYYY-MM-DD") : undefined;
  }, [id]);
  const { filters, setPeriodType, setAnchorDate, setProcessor, setMerchant } =
    useHubFilters({ initialAnchorDate });
  const { data, isLoading, isError } = useReconciliationHub(merchantId, filters);
  // The data hooks stay disabled until HubProcessorDropdown resolves the
  // acquirer's default processor, and a disabled react-query reports `idle`,
  // not `loading` — without this the sections would flash empty first.
  const isPending = !isProcessorResolved(filters) || isLoading;
 
  return (
    <PortalRootContainerV2>
      <Stack
        gap="20px"
        data-testid="reconciliation-hub"
        sx={{ padding: { xs: "16px", md: "20px 40px 24px", lg: "20px 64px 24px" } }}
      >
        <Stack justifyContent="center" gap="24px">
          <GiveBreadcrumb
            items={[
              { label: "Settlement", path: "/acquirer/settlements" },
              { label: "Reconciliation Hub" },
            ]}
            customStyle={{
              activeColor: "primary",
              defaultColor: theme.palette.text.secondary,
            }}
          />
          <Stack
            direction="row"
            justifyContent="space-between"
            alignItems="center"
            flexWrap="wrap"
            gap="16px"
          >
            <StyledTitle
              theme={theme}
              textColor={theme.palette.gradient?.["ocean-blue"]?.default ?? ""}
              variant="h1"
              data-testid="hub-page-title"
            >
              Reconciliation Hub
            </StyledTitle>
            <HubExportButton merchantId={merchantId} filters={filters} />
          </Stack>
        </Stack>
 
        {/* Renders unconditionally on purpose: HubProcessorDropdown inside it
            resolves the processor that ungates every query below. */}
        <HubFilterBar
          merchantId={merchantId}
          filters={filters}
          onPeriodTypeChange={setPeriodType}
          onAnchorDateChange={setAnchorDate}
          onProcessorChange={setProcessor}
          onMerchantChange={setMerchant}
        />
 
        {isPending ? (
          <HubSkeleton />
        ) : isError ? (
          <TechnicalErrorState />
        ) : (
          <>
            <HubKpiCards data={data} />
            <HubCashPosition data={data} />
            <HubFinancialDetails data={data} />
            <CostBreakdownByBrand data={data} />
            <PerMerchantBreakdown merchantId={merchantId} filters={filters} />
          </>
        )}
      </Stack>
    </PortalRootContainerV2>
  );
};
 
export default GiveReconciliationHub;