All files / src/hooks/common usePahReassignmentStatus.ts

100% Statements 17/17
100% Branches 9/9
100% Functions 4/4
100% Lines 16/16

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                                                                                                                    17x       144x   13x 13x       13x             17x                 17x     142x   17x 44x 44x   44x     2x                       44x 44x   44x    
import { useGetCurrentMerchantId } from "@hooks/common";
import { customInstance } from "@services/api";
import { useQuery } from "react-query";
import { QKEY_MERCHANT_OWNER_REASSIGNMENT } from "@constants/queryKeys";
import { GENERAL_STALE_TIME } from "@features/Merchants/MerchantSidePanel/constants";
 
/**
 * In-flight Primary Account Holder reassignment for the merchant Team tab.
 *
 * Reads `GET /merchants/{merchantId}/owner`, whose owner object carries a
 * `pendingChangeRequest` (same shape the acquirer side already reads off the
 * merchant-preview endpoint). The field is omitted entirely when no
 * reassignment is in flight. The account-domain `/accounts/{id}/members`
 * endpoint that feeds the team list does NOT carry this, so it lives here as a
 * separate query.
 */
export type PahReassignment = {
  state: "pending_reassignment" | "ready_for_approval";
  inviteId: number;
  newOwnerEmail: string;
  newOwnerUserAccID: number;
  inviteStatus: string;
  personStatus: string;
};
 
/**
 * Reassignment-related fields carried on a merchant `owner` object. Use this to
 * read them with keyof-checked access instead of untyped `any`.
 *
 * PAH006 SS-4 lifecycle note: `pendingChangeRequest` is present only while the
 * invite is unaccepted and is cleared at invite-accept (before the new PAH logs
 * in to do identity). Do NOT *require* it to enable identity-only re-onboarding
 * (`flag && pendingChangeRequest`) — that would disable the flow in exactly the
 * post-accept identity-under-review window. See
 * `resolveIsPahReassignmentIdentityOnly` for the safe use (suppress only).
 */
export interface MerchantOwnerReassignment {
  isOwnerReassignmentIdentityOnly?: boolean;
  pendingChangeRequest?: PahReassignment;
  statusName?: string;
}
 
/**
 * GB-21477: resolve whether the logged-in user should enter identity-only
 * re-onboarding.
 *
 * The BE flag `isOwnerReassignmentIdentityOnly` is merchant-scoped — it is true
 * whenever an owner identity task is open for the pending new PAH
 * (`account_read_handler` derives it from the merchant's identity-task status,
 * not the session). So every session viewing the merchant — including the
 * seated, already-approved OLD PAH while the invite is unaccepted — receives it
 * and was wrongly forced into identity-only onboarding.
 *
 * Identity-only must apply only to the INCOMING new PAH. While a reassignment is
 * pending to a DIFFERENT user than the one logged in, suppress it. Once the new
 * PAH accepts, `pendingChangeRequest` is cleared, so they correctly enter the
 * flow — and the suppression cannot misfire (no target email to compare).
 */
export const resolveIsPahReassignmentIdentityOnly = (
  owner: MerchantOwnerReassignment | undefined,
  currentUserEmail: string | undefined,
): boolean => {
  if (!owner?.isOwnerReassignmentIdentityOnly) return false;
 
  const targetEmail = owner.pendingChangeRequest?.newOwnerEmail;
  const isPendingToSomeoneElse = Boolean(
    targetEmail && targetEmail !== currentUserEmail,
  );
 
  return !isPendingToSomeoneElse;
};
 
// owner.statusName values meaning the PAH has finished submitting identity
// (both ID proof and selfie uploaded). The BE advances pending →
// ready_for_verification on submission, then → approved once underwriting
// verifies. See usePAHUploader / useGetMerchants for the same status reads.
const OWNER_IDENTITY_SUBMITTED_STATUSES = ["ready_for_verification", "approved"];
 
/**
 * PAH006 SS-4 follow-up: a reassigned identity-only PAH gets full portal access
 * as soon as they finish submitting identity — they should not be held on a
 * review screen waiting for underwriting approval. This reads the BE owner
 * status that flips on that submission, so the wizard and the portal gate stay
 * in sync off a single signal.
 */
export const isOwnerIdentitySubmitted = (
  owner?: MerchantOwnerReassignment,
): boolean =>
  OWNER_IDENTITY_SUBMITTED_STATUSES.includes(owner?.statusName ?? "");
 
export const usePahReassignmentStatus = (enabled = true) => {
  const { merchantId } = useGetCurrentMerchantId();
  const queryEnabled = enabled && Boolean(merchantId);
 
  const { data, isLoading } = useQuery(
    [QKEY_MERCHANT_OWNER_REASSIGNMENT, merchantId],
    () =>
      customInstance({
        url: `/merchants/${merchantId}/owner`,
        method: "GET",
      }),
    {
      staleTime: GENERAL_STALE_TIME,
      refetchOnWindowFocus: false,
      refetchOnMount: false,
      enabled: queryEnabled,
    },
  );
 
  const reassignment: PahReassignment | undefined = data?.pendingChangeRequest;
  const isPendingReassignment = Boolean(reassignment);
 
  return { reassignment, isPendingReassignment, isLoading };
};