Browse Source

feat: implement marriage profile normalization logic and request-accepted client component with tests

staging
parent
commit
eee46c1199
  1. 73
      src/app/request-accepted/request-accepted-client.test.tsx
  2. 152
      src/app/request-accepted/request-accepted-client.tsx
  3. 1
      src/hooks/marriage/types.ts
  4. 26
      src/lib/marriage-profile-contract.ts

73
src/app/request-accepted/request-accepted-client.test.tsx

@ -339,4 +339,77 @@ describe("RequestAcceptedClient", () => {
expect(screen.queryByText(/acquaintance concluded/i)).not.toBeInTheDocument(); expect(screen.queryByText(/acquaintance concluded/i)).not.toBeInTheDocument();
expect(sessionStorage.getItem("recent_declined_outcome")).toBeNull(); expect(sessionStorage.getItem("recent_declined_outcome")).toBeNull();
}); });
it("handles female confirming 'Contact Received', invokes mutation with contacted action, and transitions cleanly to outcome reporting state", async () => {
mockContactStatusMutateAsync.mockResolvedValueOnce({ success: true });
mockProfileState.data = {
id: 2,
gender: "female",
is_registering_for_self: true,
status: "in_case",
active_case: {
case_id: 45,
status: "female_accepted",
has_outcome_reported: false,
},
};
render(<RequestAcceptedClient locale="en" />);
// Click "Contact Received" button
const contactReceivedBtn = screen.getByRole("button", {
name: "Contact Received",
});
expect(contactReceivedBtn).toBeInTheDocument();
await userEvent.click(contactReceivedBtn);
// Confirmation modal should appear
expect(
screen.getByText("Are you sure contact has been made?"),
).toBeInTheDocument();
// Confirm button in the modal
const confirmBtn = screen.getByRole("button", { name: "Confirm" });
await userEvent.click(confirmBtn);
// Mutation should have been called with action: 'contacted'
expect(mockContactStatusMutateAsync).toHaveBeenCalledWith({
action: "contacted",
custom_note: "Contact received confirmed by female candidate",
});
// Modal should be closed and outcome state should be rendered
expect(
screen.queryByText("Are you sure contact has been made?"),
).not.toBeInTheDocument();
expect(screen.getByText("Share Result")).toBeInTheDocument();
});
it("persists 'Acquaintance Concluded' across reload when active_case has has_outcome_reported: true in post-contact stage", () => {
mockProfileState.data = {
id: 2,
gender: "female",
is_registering_for_self: true,
status: "in_case",
active_case: {
case_id: 45,
status: "contacted",
has_outcome_reported: true,
outcome_reason: "personal_withdrawal",
},
};
render(<RequestAcceptedClient locale="en" />);
expect(screen.getByText(/acquaintance concluded/i)).toBeInTheDocument();
expect(
screen.getByText(
"This acquaintance has not reached a conclusion and the process has been stopped. Please contact support for information on the next steps.",
),
).toBeInTheDocument();
expect(screen.getByText("Contact Support")).toBeInTheDocument();
expect(screen.getByText("View all detail")).toBeInTheDocument();
expect(screen.queryByText("Share Result")).not.toBeInTheDocument();
});
}); });

152
src/app/request-accepted/request-accepted-client.tsx

@ -628,6 +628,7 @@ export default function RequestAcceptedClient() {
"Contact received confirmed by female candidate", "Contact received confirmed by female candidate",
}); });
setHasConfirmedFemaleContact(true); setHasConfirmedFemaleContact(true);
await refetchProfile();
setIsContactReceivedConfirmOpen(false); setIsContactReceivedConfirmOpen(false);
} catch (error) { } catch (error) {
console.error("Unable to persist received contact", error); console.error("Unable to persist received contact", error);
@ -756,6 +757,7 @@ export default function RequestAcceptedClient() {
onSuccess={async () => { onSuccess={async () => {
try { try {
await handleNoContactReport(); await handleNoContactReport();
await refetchProfile();
setIsNoContactConfirmOpen(false); setIsNoContactConfirmOpen(false);
} catch (err) { } catch (err) {
console.error("Failed to report no contact", err); console.error("Failed to report no contact", err);
@ -825,36 +827,30 @@ export default function RequestAcceptedClient() {
} }
</p> </p>
</div> </div>
) : caseStatus === "contacted" ||
isFemaleContactConfirmed ||
(isFemaleProfile && contactStatusMutation.isPending) ? (
) : caseStatus === "contacted" || isFemaleContactConfirmed ? (
<div className="w-full border border-[#E2E8F0] bg-white/90 backdrop-blur-sm rounded-[20px] mt-6 px-5 py-4 text-center shadow-sm max-w-[340px] flex flex-col items-center justify-center min-h-[90px]"> <div className="w-full border border-[#E2E8F0] bg-white/90 backdrop-blur-sm rounded-[20px] mt-6 px-5 py-4 text-center shadow-sm max-w-[340px] flex flex-col items-center justify-center min-h-[90px]">
{isFemaleProfile && contactStatusMutation.isPending ? (
<LoadingThreeDot className="text-[#E03950]" />
) : (
<p className="text-[#475569] text-[14px] font-medium leading-relaxed">
{isFemaleProfile ? (
t[
"Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized."
]
) : (
<>
{t[
"Thank you for giving us feedback, we would be very happy if you also let us know the final result."
]}
{!canReportOutcome && (
<>
{" "}
{t[
"The 'Share Result' option will become active after 48 hours."
] ||
"The 'Share Result' option will become active after 48 hours."}
</>
)}
</>
)}
</p>
)}
<p className="text-[#475569] text-[14px] font-medium leading-relaxed">
{isFemaleProfile ? (
t[
"Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized."
]
) : (
<>
{t[
"Thank you for giving us feedback, we would be very happy if you also let us know the final result."
]}
{!canReportOutcome && (
<>
{" "}
{t[
"The 'Share Result' option will become active after 48 hours."
] ||
"The 'Share Result' option will become active after 48 hours."}
</>
)}
</>
)}
</p>
</div> </div>
) : noContactReportedSuccess ? ( ) : noContactReportedSuccess ? (
<p className="mt-4 max-w-[340px] text-[14.5px] leading-[1.6] font-semibold text-[#10B981]"> <p className="mt-4 max-w-[340px] text-[14.5px] leading-[1.6] font-semibold text-[#10B981]">
@ -896,56 +892,49 @@ export default function RequestAcceptedClient() {
{t["Contact Support"] || "Contact Support"} {t["Contact Support"] || "Contact Support"}
</button> </button>
</div> </div>
) : caseStatus === "contacted" ||
isFemaleContactConfirmed ||
(isFemaleProfile && contactStatusMutation.isPending) ? (
) : caseStatus === "contacted" || isFemaleContactConfirmed ? (
<div className="flex mt-8 w-full gap-3 justify-center max-w-[350px] mx-auto"> <div className="flex mt-8 w-full gap-3 justify-center max-w-[350px] mx-auto">
{isFemaleProfile &&
contactStatusMutation.isPending ? null : (
<>
<button
type="button"
disabled={isOpeningProfile}
onClick={handleOpenProfile}
className="flex-1 h-[50px] px-3 rounded-[14px] border border-[#E2E8F0] bg-white/95 backdrop-blur-sm text-[#334155] font-bold text-[14px] shadow-sm flex items-center justify-center transition-transform active:scale-[0.98] cursor-pointer hover:bg-[#F8FAFC]"
>
{isOpeningProfile ? (
<LoadingThreeDot />
) : (
t["View all detail"] || "View all detail"
)}
</button>
<button
type="button"
disabled={isOpeningProfile}
onClick={handleOpenProfile}
className="flex-1 h-[50px] px-3 rounded-[14px] border border-[#E2E8F0] bg-white/95 backdrop-blur-sm text-[#334155] font-bold text-[14px] shadow-sm flex items-center justify-center transition-transform active:scale-[0.98] cursor-pointer hover:bg-[#F8FAFC]"
>
{isOpeningProfile ? (
<LoadingThreeDot />
) : (
t["View all detail"] || "View all detail"
)}
</button>
<button
type="button"
onClick={() => {
if (isOutcomeDisabled) return;
setIsOutcomeSheetOpen(true);
}}
disabled={isOutcomeDisabled}
aria-disabled={isOutcomeDisabled}
title={
!canReportOutcome && !isFemaleProfile
? t[
"This option will become active 48 hours after contact details are shared."
] ||
"This option will become active 48 hours after contact details are shared."
: undefined
}
className={`flex-1 h-[50px] px-3 rounded-[14px] bg-gradient-to-r from-[#FF6687] to-[#FF456C] text-white font-bold text-[14px] flex items-center justify-center transition-all ${
isOutcomeDisabled
? "opacity-50 cursor-not-allowed shadow-none"
: "hover:opacity-95 active:scale-[0.98] cursor-pointer shadow-sm"
}`}
>
{outcomeMutation.isPending ? (
<LoadingThreeDot className="text-white" />
) : (
t["Share Result"] || "Share Result"
)}
</button>
</>
)}
<button
type="button"
onClick={() => {
if (isOutcomeDisabled) return;
setIsOutcomeSheetOpen(true);
}}
disabled={isOutcomeDisabled}
aria-disabled={isOutcomeDisabled}
title={
!canReportOutcome && !isFemaleProfile
? t[
"This option will become active 48 hours after contact details are shared."
] ||
"This option will become active 48 hours after contact details are shared."
: undefined
}
className={`flex-1 h-[50px] px-3 rounded-[14px] bg-gradient-to-r from-[#FF6687] to-[#FF456C] text-white font-bold text-[14px] flex items-center justify-center transition-all ${
isOutcomeDisabled
? "opacity-50 cursor-not-allowed shadow-none"
: "hover:opacity-95 active:scale-[0.98] cursor-pointer shadow-sm"
}`}
>
{outcomeMutation.isPending ? (
<LoadingThreeDot className="text-white" />
) : (
t["Share Result"] || "Share Result"
)}
</button>
</div> </div>
) : noContactReportedSuccess ? ( ) : noContactReportedSuccess ? (
<div className="flex flex-col mt-8 w-full gap-3.5 max-w-md mx-auto"> <div className="flex flex-col mt-8 w-full gap-3.5 max-w-md mx-auto">
@ -1009,11 +998,7 @@ export default function RequestAcceptedClient() {
: "border-[#E2E8F0] bg-white/95 backdrop-blur-sm text-[#475569] shadow-sm cursor-pointer hover:bg-[#F8FAFC] active:scale-[0.98]" : "border-[#E2E8F0] bg-white/95 backdrop-blur-sm text-[#475569] shadow-sm cursor-pointer hover:bg-[#F8FAFC] active:scale-[0.98]"
}`} }`}
> >
{contactStatusMutation.isPending ? (
<LoadingThreeDot />
) : (
primaryActionText
)}
{primaryActionText}
</button> </button>
) : ( ) : (
<button <button
@ -1050,8 +1035,7 @@ export default function RequestAcceptedClient() {
{caseStatus !== "contacted" && {caseStatus !== "contacted" &&
!isFemaleContactConfirmed && !isFemaleContactConfirmed &&
!noContactReportedSuccess &&
!(isFemaleProfile && contactStatusMutation.isPending) ? (
!noContactReportedSuccess ? (
<div className="border border-[#FDA4AF]/60 bg-[#FFF1F2]/80 backdrop-blur-sm rounded-[16px] mt-6 p-4 max-w-md mx-auto shadow-sm"> <div className="border border-[#FDA4AF]/60 bg-[#FFF1F2]/80 backdrop-blur-sm rounded-[16px] mt-6 p-4 max-w-md mx-auto shadow-sm">
<p className="text-[#BE123C] text-[12px] font-medium leading-[1.65] whitespace-pre-line text-justify"> <p className="text-[#BE123C] text-[12px] font-medium leading-[1.65] whitespace-pre-line text-justify">
{ {

1
src/hooks/marriage/types.ts

@ -82,6 +82,7 @@ export type MarriageActiveCase = {
status: MarriageCaseStatus; status: MarriageCaseStatus;
introduced_at: string | null; introduced_at: string | null;
contact_shared_at?: string | null; contact_shared_at?: string | null;
payment_done_at?: string | null;
minutes_since_contact_shared?: number | null; minutes_since_contact_shared?: number | null;
can_report_no_contact?: boolean; can_report_no_contact?: boolean;
can_report_outcome?: boolean; can_report_outcome?: boolean;

26
src/lib/marriage-profile-contract.ts

@ -116,6 +116,10 @@ export function normalizeMarriageProfile(
rawActiveCase.contact_shared_at ?? rawActiveCase.contact_shared_at ??
rawActiveCase.contactSharedAt ?? rawActiveCase.contactSharedAt ??
null, null,
payment_done_at:
rawActiveCase.payment_done_at ??
rawActiveCase.paymentDoneAt ??
null,
minutes_since_contact_shared: minutes_since_contact_shared:
rawActiveCase.minutes_since_contact_shared ?? rawActiveCase.minutes_since_contact_shared ??
rawActiveCase.minutesSinceContactShared ?? rawActiveCase.minutesSinceContactShared ??
@ -125,6 +129,28 @@ export function normalizeMarriageProfile(
rawActiveCase.canReportNoContact ?? rawActiveCase.canReportNoContact ??
false, false,
), ),
can_report_outcome: Boolean(
rawActiveCase.can_report_outcome ??
rawActiveCase.canReportOutcome ??
false,
),
has_outcome_reported: Boolean(
rawActiveCase.has_outcome_reported ??
rawActiveCase.hasOutcomeReported ??
false,
),
outcome_reason:
rawActiveCase.outcome_reason ??
rawActiveCase.outcomeReason ??
null,
outcome_note:
rawActiveCase.outcome_note ??
rawActiveCase.outcomeNote ??
null,
outcome_reported_by:
rawActiveCase.outcome_reported_by ??
rawActiveCase.outcomeReportedBy ??
null,
my_action: my_action:
rawActiveCase.my_action ?? rawActiveCase.myAction ?? "pending", rawActiveCase.my_action ?? rawActiveCase.myAction ?? "pending",
other_action: other_action:

Loading…
Cancel
Save