Update paiments
All checks were successful
Deploy / deploy (push) Successful in 17s

This commit is contained in:
2026-03-12 10:18:08 +00:00
parent d7e1fe31ff
commit eaf0891905
2 changed files with 45 additions and 24 deletions

View File

@@ -106,20 +106,26 @@ export async function lockSeatApi(
return { ticketId: response.data.ticket_id ?? seatId };
}
// ─── Payment webhook ──────────────────────────────────────────────────────────
// ─── Acquring / booking ───────────────────────────────────────────────────────
export interface BookTicketResponse {
ticket_id: number;
/** Redirect URL returned by the payment gateway (YooKassa, etc.) */
payment_url: string;
}
/**
* POST /api/webhooks/payment
* Simulates a payment gateway callback for the given ticket.
* Uses a random idempotency key to satisfy the backend's idempotency check.
* POST /api/tickets/book
* Books a seat and initiates an acquiring session.
* On success, caller must redirect to `payment_url`.
* Throws AxiosError 400/404 if the seat is taken, locked, or not found.
* Requires a valid Bearer token (injected by the request interceptor).
*/
export async function processPaymentWebhook(ticketId: number): Promise<void> {
const idempotencyKey = "req-" + Math.random().toString(36).substring(2, 9);
await apiClient.post("/webhooks/payment", {
ticket_id: ticketId,
idempotency_key: idempotencyKey,
status: "success",
export async function bookTicket(seatId: number): Promise<BookTicketResponse> {
const response = await apiClient.post<BookTicketResponse>("/tickets/book", {
seat_id: seatId,
});
return response.data;
}
// ─── Ticket scanner ───────────────────────────────────────────────────────────

View File

@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import {
ArrowLeft,
@@ -11,8 +11,9 @@ import {
Loader2,
} from "lucide-react";
import Image from "next/image";
import type { AxiosError } from "axios";
import { useCartStore, type CartSeat } from "@/store/cartStore";
import { processPaymentWebhook } from "@/api/client";
import { bookTicket } from "@/api/client";
// ─── Mock event data (real data would come from route param / API) ─────────────
@@ -97,26 +98,40 @@ export default function CheckoutPage() {
const finalTotal = rawTotal - bonusDiscount;
const seatGroups = groupByRow(seats);
function showToast(message: string, isError = false) {
const showToast = useCallback((message: string, isError = false) => {
setToastMsg(message);
setToastIsError(isError);
setTimeout(() => setToastMsg(null), 3000);
}, []);
const handlePay = useCallback(async () => {
// Для MVP жестко ограничиваем: один заказ = один билет
if (seats.length === 0 || isLoading) return;
if (seats.length > 1) {
showToast("В MVP-версии можно оплатить только одно место за раз. Оставьте в корзине один билет.", true);
return;
}
async function handlePay() {
if (seats.length === 0 || isLoading) return;
setIsLoading(true);
try {
await Promise.all(seats.map((s: CartSeat) => processPaymentWebhook(s.ticketId)));
clearCart();
showToast("Оплата прошла успешно!");
setTimeout(() => router.push("/tickets"), 1400);
} catch {
showToast("Ошибка оплаты. Попробуйте снова.", true);
} finally {
setIsLoading(false);
}
// Берем строго единственное место из корзины
const targetSeat = seats[0];
const response = await bookTicket(targetSeat.seatId);
// Корзину пока НЕ чистим (оставим это на совесть успешного возврата)
// Уходим на шлюз. Кнопка остается заблокированной (isLoading: true).
window.location.href = response.payment_url;
} catch (err) {
const axiosErr = err as AxiosError<{ detail: string }>;
const detail = axiosErr.response?.data?.detail;
const msg = typeof detail === "string" ? detail : "Ошибка связи с платежным шлюзом.";
showToast(msg, true);
setIsLoading(false); // Разблокируем кнопку только при ошибке
}
}, [seats, isLoading, showToast]);
// ── Empty cart guard ──
if (seats.length === 0) {