diff --git a/driver-pwa/frontend/src/pages/TopupPage.test.tsx b/driver-pwa/frontend/src/pages/TopupPage.test.tsx
new file mode 100644
index 0000000..01b367a
--- /dev/null
+++ b/driver-pwa/frontend/src/pages/TopupPage.test.tsx
@@ -0,0 +1,26 @@
+import { describe, it, expect, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { MemoryRouter } from "react-router-dom";
+
+vi.mock("@/api/driver", () => ({
+ topup: vi.fn(async () => ({ order_id: "o9", pay_url: "https://qr.nspk.ru/x", commission: 52, total: 1052 })),
+}));
+import { TopupPage } from "./TopupPage";
+import * as driver from "@/api/driver";
+
+const wrap = () => render();
+
+describe("TopupPage", () => {
+ it("keypad builds amount, shows total, and submits topup", async () => {
+ wrap();
+ await userEvent.click(screen.getByRole("button", { name: "1" }));
+ await userEvent.click(screen.getByRole("button", { name: "0" }));
+ await userEvent.click(screen.getByRole("button", { name: "0" }));
+ await userEvent.click(screen.getByRole("button", { name: "0" }));
+ 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");
+ });
+});
diff --git a/driver-pwa/frontend/src/pages/TopupPage.tsx b/driver-pwa/frontend/src/pages/TopupPage.tsx
index 5e3b567..a213614 100644
--- a/driver-pwa/frontend/src/pages/TopupPage.tsx
+++ b/driver-pwa/frontend/src/pages/TopupPage.tsx
@@ -1 +1,82 @@
-export function TopupPage() { return null; }
+import { useState } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+import { Delete } from "lucide-react";
+import { toast } from "sonner";
+import { topup } from "@/api/driver";
+import { keypadReduce, withCommission } from "@/lib/amount";
+import { formatMoney } from "@/lib/format";
+import { Segment } from "@/components/Segment";
+import { Spinner } from "@/components/Spinner";
+
+const RATE = 0.052;
+const QUICK = ["500", "1000", "2000"];
+const KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "·", "0", "back"];
+
+export function TopupPage() {
+ const nav = useNavigate();
+ const loc = useLocation();
+ const bucket = (loc.state as { bucket?: string } | null)?.bucket ?? "Долг аренда";
+ const [raw, setRaw] = useState("");
+ const [method, setMethod] = useState("qr");
+ const [busy, setBusy] = useState(false);
+
+ const base = raw ? parseInt(raw, 10) : 0;
+ const { commission, total } = withCommission(base, RATE);
+
+ function press(k: string) {
+ if (k === "·") return;
+ setRaw((r) => keypadReduce(r, k));
+ }
+
+ async function pay() {
+ if (base <= 0) return;
+ setBusy(true);
+ try {
+ const r = await topup(bucket, base, method as "qr" | "card");
+ nav(`/pay/${r.order_id}`, { state: { payUrl: r.pay_url, total: r.total, bucket, base } });
+ } catch {
+ toast.error("Не удалось создать платёж");
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ return (
+
+
+
{formatMoney(base)}
+
+ к оплате {formatMoney(total)} (комиссия {formatMoney(commission)})
+
+
+
+ {QUICK.map((q) => (
+
+ ))}
+
+
+
+ {KEYS.map((k) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}