diff --git a/.env.example b/.env.example index b803662..5767c84 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,12 @@ TURNSTILE_SECRET_KEY="" # (ym init snippet) ставится в admin → Settings → headCode, отдельно от этого ID. NEXT_PUBLIC_METRIKA_COUNTER_ID="" +# Адрес payment-router для чекаута витрины (/pay/order, /pay/methods). Build-time. +# Пусто: dev → локальная заглушка /dev/pay-stub, production-сборка → боевой +# https://obsidian.second-brain.ru. Для локальной `next build && next start` +# задать явно (например http://localhost:9), иначе форма уйдёт на прод. +NEXT_PUBLIC_PAY_BASE_URL="" + # Kinescope API KINESCOPE_API_KEY="" diff --git a/CLAUDE.md b/CLAUDE.md index 997e804..f687e8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -220,6 +220,7 @@ S3_REGION="eu-central" ### Env +- **Чекаут витрины ходит в боевой payment-router из любой production-сборки.** Адрес `/pay/order` и `/pay/methods` выбирает `resolvePayBase` (`src/lib/pay-base.ts`): `NEXT_PUBLIC_PAY_BASE_URL`, иначе при `NODE_ENV=production` — `https://obsidian.second-brain.ru`, иначе — локальная заглушка `/dev/pay-stub` (в проде 404, ничего не сохраняет и не шлёт). `next dev` безопасен, а **локальная `next build && next start` без `NEXT_PUBLIC_PAY_BASE_URL` отправит форму на прод**: реальный заказ, pending в леджере, Telegram владельцу (случай 20261111, заказ 1794380127). Для такой сборки задавать переменную явно, например `http://localhost:9`. Браузерные проверки — только с сетевой блокировкой внешних доменов и без отправки форм. - `NEXT_PUBLIC_*` инлайнятся **на сборке**. `NEXT_PUBLIC_APP_URL` задан только как runtime-env в `docker-compose.prod.yml`, в образ не попадает → работает захардкоженный фолбэк на прод-адрес. Следствие: на staging студенту выдаются Zotero-скрипт и curl-пример с **прод**-адресом. Правишь — передавай через `--build-arg`, как `NEXT_PUBLIC_TURNSTILE_SITE_KEY`. ### Тариф — это отдельный курс со своей копией уроков diff --git a/src/app/dev/pay-stub/pay/methods/route.ts b/src/app/dev/pay-stub/pay/methods/route.ts new file mode 100644 index 0000000..f7f5b01 --- /dev/null +++ b/src/app/dev/pay-stub/pay/methods/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; + +// Local stand-in for payment-router GET /pay/methods (see src/lib/pay-base.ts). +// Production: 404. No outbound calls. +export function GET() { + if (process.env.NODE_ENV === "production") { + return new NextResponse(null, { status: 404 }); + } + return NextResponse.json({ + methods: [ + { id: "stub-card", label: "Заглушка: карта", provider: "dev-stub", kind: "online" }, + { id: "stub-invoice", label: "Заглушка: счёт", provider: "dev-stub", kind: "manual" }, + ], + }); +} diff --git a/src/app/dev/pay-stub/pay/order/route.ts b/src/app/dev/pay-stub/pay/order/route.ts new file mode 100644 index 0000000..713c9fb --- /dev/null +++ b/src/app/dev/pay-stub/pay/order/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; + +// Local stand-in for payment-router POST /pay/order (see src/lib/pay-base.ts). +// Echoes the submitted fields back as a page. Stores nothing, sends nothing. +// Production: 404. + +function escapeHtml(s: string) { + return s.replace(/[&<>"']/g, (c) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] as string, + ); +} + +export async function POST(request: NextRequest) { + if (process.env.NODE_ENV === "production") { + return new NextResponse(null, { status: 404 }); + } + const form = await request.formData(); + const rows = [...form.entries()] + .map(([k, v]) => `
Заказ не создан, данные никуда не отправлены. Так выглядела бы отправка на payment-router:
+