diff --git a/driver-pwa/frontend/src/api/driver.ts b/driver-pwa/frontend/src/api/driver.ts
index 9babf3f..31ce636 100644
--- a/driver-pwa/frontend/src/api/driver.ts
+++ b/driver-pwa/frontend/src/api/driver.ts
@@ -104,8 +104,11 @@ export const getBalance = async () =>
BalanceSchema.parse(await api.get("driver/balance").json());
export const getBuckets = async () =>
BucketsSchema.parse(await api.get("driver/buckets").json());
-export const topup = async (bucket: string, amount: number, method: "qr" | "card") =>
- TopupSchema.parse(await api.post("driver/topup", { json: { bucket, amount, method } }).json());
+export const topup = async (bucket: string, amount: number, method: "qr" | "card",
+ live = false) =>
+ TopupSchema.parse(await api.post("driver/topup", {
+ json: { bucket, amount, method, live },
+ }).json());
export const getPaymentState = async (orderId: string) =>
PaymentStateSchema.parse(await api.get(`driver/payment/${orderId}`).json());
export const getPayments = async () =>
diff --git a/driver-pwa/frontend/src/pages/DebugPage.tsx b/driver-pwa/frontend/src/pages/DebugPage.tsx
index 2d4b731..a70d52b 100644
--- a/driver-pwa/frontend/src/pages/DebugPage.tsx
+++ b/driver-pwa/frontend/src/pages/DebugPage.tsx
@@ -68,10 +68,30 @@ export function DebugPage() {
)}
+ {impersonated && (
+
Сведения
|
+ {impersonated && (
+
|
+ )}
|
diff --git a/driver-pwa/frontend/src/pages/TopupPage.test.tsx b/driver-pwa/frontend/src/pages/TopupPage.test.tsx
index 2fdd690..464b284 100644
--- a/driver-pwa/frontend/src/pages/TopupPage.test.tsx
+++ b/driver-pwa/frontend/src/pages/TopupPage.test.tsx
@@ -5,7 +5,8 @@ import { MemoryRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
vi.mock("@/api/driver", () => ({
- topup: vi.fn(async () => ({ order_id: "o9", pay_url: "https://qr.nspk.ru/x", commission: 52, total: 1052 })),
+ topup: vi.fn(async () => ({ order_id: "o9", pay_url: "https://qr.nspk.ru/x", commission: 52,
+ total: 1052, mock: false })),
getCommission: vi.fn(async () => ({ rate: 0.052 })),
getBuckets: vi.fn(async () => ({
buckets: ["Долг аренда", "Долг по штрафам"], with_debt: ["Долг аренда"],
@@ -34,7 +35,7 @@ describe("TopupPage", () => {
expect(screen.getByTestId("amount").textContent).toBe("1 000 ₽");
expect(screen.getByTestId("total").textContent).toContain("1 052");
await userEvent.click(screen.getByRole("button", { name: /Оплатить/i }));
- expect(driver.topup).toHaveBeenCalledWith("Долг аренда", 1000, "qr");
+ expect(driver.topup).toHaveBeenCalledWith("Долг аренда", 1000, "qr", false);
});
it("decimal key builds kopecks and they reach the backend", async () => {
@@ -46,7 +47,7 @@ describe("TopupPage", () => {
}
expect(screen.getByTestId("amount").textContent).toBe("100,52 ₽");
await userEvent.click(screen.getByRole("button", { name: /Оплатить/i }));
- expect(driver.topup).toHaveBeenCalledWith("Долг аренда", 100.52, "qr");
+ expect(driver.topup).toHaveBeenCalledWith("Долг аренда", 100.52, "qr", false);
});
it("lets the driver choose the debt category instead of hardcoding rent", async () => {
@@ -57,6 +58,32 @@ describe("TopupPage", () => {
await userEvent.click(screen.getByRole("button", { name: "0" }));
await userEvent.click(screen.getByRole("button", { name: "0" }));
await userEvent.click(screen.getByRole("button", { name: /Оплатить/i }));
- expect(driver.topup).toHaveBeenCalledWith("Долг по штрафам", 500, "qr");
+ expect(driver.topup).toHaveBeenCalledWith("Долг по штрафам", 500, "qr", false);
+ });
+});
+
+describe("боевая оплата под просмотром", () => {
+ it("по умолчанию платёж учебный — флаг в запрос уходит выключенным", async () => {
+ const { useStaff } = await import("@/store/staff");
+ useStaff.getState().clear();
+ wrap();
+ await userEvent.click(screen.getByRole("button", { name: "5" }));
+ await userEvent.click(screen.getByRole("button", { name: "0" }));
+ await userEvent.click(screen.getByRole("button", { name: "0" }));
+ await userEvent.click(screen.getByRole("button", { name: /Оплатить/i }));
+ expect(driver.topup).toHaveBeenLastCalledWith("Долг аренда", 500, "qr", false);
+ });
+
+ it("включённая боевая оплата предупреждает и уходит с флагом", async () => {
+ const { useStaff } = await import("@/store/staff");
+ useStaff.getState().setLivePay(true);
+ wrap();
+ expect(screen.getByText(/деньги спишутся по-настоящему/i)).toBeInTheDocument();
+ await userEvent.click(screen.getByRole("button", { name: "5" }));
+ await userEvent.click(screen.getByRole("button", { name: "0" }));
+ await userEvent.click(screen.getByRole("button", { name: "0" }));
+ await userEvent.click(screen.getByRole("button", { name: /Оплатить/i }));
+ expect(driver.topup).toHaveBeenLastCalledWith("Долг аренда", 500, "qr", true);
+ useStaff.getState().clear();
});
});
diff --git a/driver-pwa/frontend/src/pages/TopupPage.tsx b/driver-pwa/frontend/src/pages/TopupPage.tsx
index e2b2e85..b741099 100644
--- a/driver-pwa/frontend/src/pages/TopupPage.tsx
+++ b/driver-pwa/frontend/src/pages/TopupPage.tsx
@@ -6,6 +6,7 @@ import { toast } from "sonner";
import { apiErrorText, topup, getCommission, getBuckets } from "@/api/driver";
import { keypadReduce, parseAmount, withCommission } from "@/lib/amount";
import { formatMoney } from "@/lib/format";
+import { useStaff } from "@/store/staff";
import { Segment } from "@/components/Segment";
import { Spinner } from "@/components/Spinner";
@@ -14,6 +15,7 @@ const KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", ",", "0", "back"];
export function TopupPage() {
const nav = useNavigate();
+ const livePay = useStaff((s) => s.livePay);
const loc = useLocation();
const fromBalance = (loc.state as { bucket?: string } | null)?.bucket;
// Список категорий приходит с сервера — там же он и проверяется. Хардкод
@@ -40,7 +42,7 @@ export function TopupPage() {
if (base <= 0 || !bucket || busy) return;
setBusy(true);
try {
- const r = await topup(bucket, base, method as "qr" | "card");
+ const r = await topup(bucket, base, method as "qr" | "card", livePay);
// Учебный платёж (сотрудник смотрит от чужого лица): банка нет, платить
// негде — сразу на экран выбора исхода, дальше всё как у водителя.
if (r.mock) {
@@ -113,6 +115,13 @@ export function TopupPage() {
+ {livePay && (
+ // Предупреждение прямо над кнопкой: под чужим лицом ошибиться легко, а
+ // отыграть боевой платёж — это возврат в банке и правка в 1С.
+
+ Боевая оплата: деньги спишутся по-настоящему и зачислятся водителю
+
+ )}