diff --git a/web/dashboard/src/components/pos2/pos2-screen.tsx b/web/dashboard/src/components/pos2/pos2-screen.tsx index 75aca90..2394675 100644 --- a/web/dashboard/src/components/pos2/pos2-screen.tsx +++ b/web/dashboard/src/components/pos2/pos2-screen.tsx @@ -26,6 +26,7 @@ import { submitOrderToApi, orderAmountDue, isLocalOrder } from "@/lib/pos2/submi import { requestPosPayment, posDeviceErrorMessage } from "@/lib/api/pos-device"; import { printReceipt } from "@/lib/api/print"; import { PosCustomerPicker } from "@/components/pos2/pos-customer-picker"; +import { useConfirm } from "@/components/providers/confirm-provider"; import { Can } from "@/components/auth/can"; import { useHasPermission } from "@/lib/permissions"; import type { Customer, MenuItem, Order, TableBoardItem } from "@/lib/api/types"; @@ -116,6 +117,7 @@ export function Pos2Screen() { // a cashier must NOT be able to do it (send food, then erase it). Gated on the // VoidOrder permission; the unsent portion of a line stays freely editable. const canVoid = useHasPermission("VoidOrder"); + const confirm = useConfirm(); // local view state const [view, setView] = useState<"board" | "order">("board"); @@ -288,9 +290,31 @@ export function Pos2Screen() { try { const cardTotal = payments.filter((p) => p.method === "Card").reduce((s, p) => s + p.amount, 0); const payBranchId = payTarget.branchId ?? orderBranchId ?? undefined; + // Card leg: push the amount to the configured terminal and wait for it. A + // connected terminal that declines throws POS_DEVICE_* (caught below → + // nothing recorded). If no terminal is wired up the request is "skipped", + // so we have NO machine proof the card actually cleared. + let cardConfirmedByTerminal = false; if (cardTotal > 0 && payBranchId) { - // push the card amount to the configured terminal (no-op/skip if none) - await requestPosPayment(cafeId as string, payBranchId, payTarget.id, cardTotal); + const res = await requestPosPayment(cafeId as string, payBranchId, payTarget.id, cardTotal); + cardConfirmedByTerminal = res.sent && !res.skipped; + } + // No integrated-terminal confirmation → make the cashier confirm the card + // was approved before we book it as paid; otherwise a declined card gets + // recorded as revenue the café never received. + if (cardTotal > 0 && !cardConfirmedByTerminal) { + setBusy(false); // hide the processing overlay so the dialog is interactive + const approved = await confirm({ + title: "تأیید پرداخت کارتی", + description: `پرداخت کارتی ${fmt(cardTotal)} تومان روی دستگاه پوز با موفقیت انجام شد؟`, + confirmLabel: "بله، پرداخت شد", + cancelLabel: "خیر، لغو", + }); + if (!approved) { + notify.error("ثبت پرداخت لغو شد"); + return; // finally resets the guards; nothing recorded + } + setBusy(true); } await apiPost(`/api/cafes/${cafeId}/orders/${payTarget.id}/payments`, { payments, @@ -645,7 +669,15 @@ export function Pos2Screen() { {payTarget && ( setPayTarget(null)} onConfirm={confirmPay} diff --git a/web/dashboard/src/components/ui/alert-dialog.tsx b/web/dashboard/src/components/ui/alert-dialog.tsx index d33f031..160051d 100644 --- a/web/dashboard/src/components/ui/alert-dialog.tsx +++ b/web/dashboard/src/components/ui/alert-dialog.tsx @@ -16,7 +16,9 @@ const AlertDialogOverlay = React.forwardRef<