visual changes
3
.gitignore
vendored
@@ -4,6 +4,9 @@
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
/dist
|
||||
changes.txt
|
||||
api.txt
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
|
||||
150
BACKEND_CHANGES.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# Fastcheck Backend — изменения сверх api.txt
|
||||
|
||||
Документ с **дельтой** — что нужно добавить/изменить в спеке `public/api.txt`,
|
||||
чтобы фронт полноценно заработал. Базовая спека остаётся в силе, здесь только
|
||||
правки.
|
||||
|
||||
> Полный референс с примерами — см. `BACKEND.md` в этом же репозитории.
|
||||
|
||||
---
|
||||
|
||||
## 1. `POST /fastcheck` (создание) — расширить тело
|
||||
|
||||
Добавить поля `orderId`, `note`, `returnUrl` (все опциональные):
|
||||
|
||||
```diff
|
||||
POST /fastcheck
|
||||
HEADER: Authorization: {"sessionID": "..."}
|
||||
Body:
|
||||
{
|
||||
"amount": 158000,
|
||||
"currency": "RUB",
|
||||
+ "orderId": "merchant-order-uuid", // id заказа на стороне мерчанта
|
||||
+ "note": "Оплата заказа №123", // комментарий, видит получатель
|
||||
+ "returnUrl": "https://shop.example.com/thanks" // куда вернуть юзера после оплаты
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"fastcheck": "1234-5678-0001",
|
||||
"expiration": "2026-07-07T09:08:18Z",
|
||||
"code": "5864",
|
||||
"Status": true,
|
||||
+ "orderId": "merchant-order-uuid" // эхо обратно
|
||||
}
|
||||
```
|
||||
|
||||
`note` также возвращать в `GET /fastcheck`, чтобы получатель видел причину
|
||||
платежа перед приёмом.
|
||||
|
||||
---
|
||||
|
||||
## 2. Зафиксировать единицу `amount`
|
||||
|
||||
В `api.txt` пример `"amount": 158000` неоднозначен. Зафиксировать:
|
||||
|
||||
> `amount` — **целое число в основной единице валюты** (для RUB — рубли,
|
||||
> не копейки). Минимум 1.
|
||||
|
||||
Если бэкенд считает в копейках — сообщить, фронт изменит формат.
|
||||
|
||||
---
|
||||
|
||||
## 3. Merchant webhook (новое)
|
||||
|
||||
После успешного `POST /fastcheck` (accept), сервер шлёт `POST` на
|
||||
`webhookUrl`, привязанный к `orderId`/мерчанту:
|
||||
|
||||
```
|
||||
POST <merchant_webhook_url>
|
||||
Headers:
|
||||
Content-Type: application/json
|
||||
X-Fastcheck-Signature: <hmac-sha256 от тела с секретом мерчанта>
|
||||
|
||||
Body:
|
||||
{
|
||||
"event": "fastcheck.paid",
|
||||
"fastcheck": "1234-5678-0001",
|
||||
"orderId": "merchant-order-uuid",
|
||||
"amount": 158000,
|
||||
"currency": "RUB",
|
||||
"paidAt": "2026-04-30T12:34:56Z"
|
||||
}
|
||||
```
|
||||
|
||||
Мерчант проверяет подпись и помечает заказ оплаченным. Ретраи (минимум
|
||||
3 попытки с экспоненциальной задержкой) при не-2xx ответах.
|
||||
|
||||
---
|
||||
|
||||
## 4. Развести create и accept на разные пути (рекомендация)
|
||||
|
||||
Сейчас `POST /fastcheck` делает оба действия — отличается только формой тела
|
||||
(`amount` vs `code`). Это хрупко.
|
||||
|
||||
```diff
|
||||
- POST /fastcheck (создание)
|
||||
- POST /fastcheck (приём)
|
||||
+ POST /fastcheck (создание)
|
||||
+ POST /fastcheck/accept (приём)
|
||||
```
|
||||
|
||||
Фронт правится в одну строку. Если оставляете один путь — оставляем как есть.
|
||||
|
||||
---
|
||||
|
||||
## 5. Гранулярные ошибки `POST /fastcheck` (accept)
|
||||
|
||||
В `api.txt` любая ошибка = `404 { "message": "not authorized" }`. Юзер не
|
||||
понимает, что пошло не так. Различать:
|
||||
|
||||
```
|
||||
401 { "message": "not authorized" } — нет/невалидная сессия
|
||||
404 { "message": "fastcheck not found" } — нет такого номера
|
||||
403 { "message": "wrong code" } — код неверный
|
||||
410 { "message": "already used" } — чек уже погашен
|
||||
410 { "message": "expired" } — просрочен
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. CORS + HTTPS + DNS (блокер)
|
||||
|
||||
Сейчас `https://api.fastcheck.store` даёт `ERR_NAME_NOT_RESOLVED` —
|
||||
домен не резолвится. Без этого тестировать нечего.
|
||||
|
||||
Минимально:
|
||||
- Поднять DNS A-запись на `api.fastcheck.store`.
|
||||
- Валидный TLS-сертификат (Let's Encrypt подойдёт).
|
||||
- На все эндпоинты + `OPTIONS` отвечать заголовками:
|
||||
```
|
||||
Access-Control-Allow-Origin: https://<домен-фронта>
|
||||
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
|
||||
Access-Control-Allow-Headers: Content-Type, Authorization
|
||||
Access-Control-Max-Age: 86400
|
||||
```
|
||||
`OPTIONS` → `204 No Content` без тела.
|
||||
|
||||
Подробности — `BACKEND.md` §1.2.
|
||||
|
||||
---
|
||||
|
||||
## Что **не меняется**
|
||||
|
||||
- `GET /ping`
|
||||
- `GET /websession`, `GET /websession/:id`, `DELETE /websession/:id`
|
||||
- `GET /fastcheck`
|
||||
- Формат заголовка `Authorization: {"sessionID":"..."}`
|
||||
- Telegram-логин через бот `@DexarSupport_bot` с `?start=<sessionId>`
|
||||
|
||||
---
|
||||
|
||||
## Чеклист для бэкенда
|
||||
|
||||
- [ ] DNS + HTTPS + CORS (блокер)
|
||||
- [ ] `orderId`, `note`, `returnUrl` в `POST /fastcheck` (create)
|
||||
- [ ] `note` возвращается в `GET /fastcheck`
|
||||
- [ ] Зафиксировать `amount` в основной единице (рубли)
|
||||
- [ ] Webhook на `fastcheck.paid` с HMAC-подписью
|
||||
- [ ] Гранулярные ошибки accept
|
||||
- [ ] (опц.) Развести create / accept на разные пути
|
||||
52
dist/3rdpartylicenses.txt
vendored
@@ -1,4 +1,30 @@
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/forms
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/core
|
||||
License: "MIT"
|
||||
@@ -327,29 +353,3 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/forms
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
BIN
dist/favicon.ico
vendored
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 4.2 KiB |
4
dist/favicon.svg
vendored
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="7" fill="#2563eb"/>
|
||||
<text x="16" y="23" font-family="Arial, sans-serif" font-size="20" font-weight="bold" fill="white" text-anchor="middle">₽</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 256 B |
2
dist/index.html
vendored
@@ -13,5 +13,5 @@
|
||||
<style>*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body{height:100%}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#1e40af}</style><link rel="stylesheet" href="styles-4STSJS4C.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-4STSJS4C.css"></noscript></head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<script src="main-S3GKETUH.js" type="module"></script></body>
|
||||
<link rel="modulepreload" href="chunk-FBABAKVO.js"><script src="main-XKBOOIAP.js" type="module"></script></body>
|
||||
</html>
|
||||
|
||||
5
dist/main-S3GKETUH.js
vendored
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/logo_big.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/logo_small.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
@@ -8,7 +8,10 @@
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="card__title">Новый Фастчек</h1>
|
||||
<h1 class="card__title">
|
||||
Новый
|
||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||
</h1>
|
||||
<p class="card__subtitle">Укажите сумму для пополнения</p>
|
||||
</div>
|
||||
|
||||
@@ -111,7 +114,12 @@
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ loading() ? 'Создание…' : 'Создать Фастчек' }}
|
||||
@if (loading()) {
|
||||
Создание…
|
||||
} @else {
|
||||
Создать
|
||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -82,12 +82,12 @@ export class CreatePage {
|
||||
});
|
||||
this.router.navigate(['/']);
|
||||
} else {
|
||||
this.error.set('Не удалось создать Фастчек.');
|
||||
this.error.set('Не удалось создать платёж.');
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.loading.set(false);
|
||||
this.error.set('Ошибка при создании Фастчека. Попробуйте ещё раз.');
|
||||
this.error.set('Ошибка при создании платежа. Попробуйте ещё раз.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,15 +2,23 @@
|
||||
<div class="card">
|
||||
|
||||
<div class="card__header">
|
||||
<h1 class="card__title">Оплата Фастчеком</h1>
|
||||
<p class="card__subtitle">Введите данные Фастчека или создайте новый</p>
|
||||
<img class="card__brand" src="/logo_big.png"
|
||||
alt="fastCHECK" width="220" height="60" />
|
||||
<p class="card__subtitle">
|
||||
Введите данные
|
||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||
или создайте новый
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card__body">
|
||||
|
||||
<!-- Fastcheck number + new -->
|
||||
<div class="field">
|
||||
<label class="field__label" for="fcNumber">Номер Фастчека</label>
|
||||
<label class="field__label" for="fcNumber">
|
||||
Номер
|
||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||
</label>
|
||||
<div class="row">
|
||||
<input
|
||||
id="fcNumber"
|
||||
@@ -22,7 +30,7 @@
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<a class="btn btn--ghost" routerLink="/new" aria-label="Создать новый Фастчек">Новый</a>
|
||||
<a class="btn btn--ghost" routerLink="/new" aria-label="Создать новый fastCHECK">Новый</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -101,9 +109,14 @@
|
||||
<path d="M20 6L9 17l-5-5" />
|
||||
</svg>
|
||||
<h2 class="modal__title">Оплачено</h2>
|
||||
<p class="modal__sub">Фастчек успешно принят.</p>
|
||||
<p class="modal__sub">
|
||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||
успешно принят.
|
||||
</p>
|
||||
</div>
|
||||
} @else {
|
||||
<img class="brand-logo brand-logo--small" src="/logo_small.png"
|
||||
alt="fastCHECK" width="32" height="32" />
|
||||
<h2 class="modal__title">Войти через Telegram</h2>
|
||||
<p class="modal__sub">Отсканируйте QR или откройте ссылку</p>
|
||||
|
||||
|
||||
@@ -63,11 +63,11 @@ export class FastcheckPage {
|
||||
|
||||
pay(): void {
|
||||
if (!this.fastcheckNumber().trim()) {
|
||||
this.error.set('Введите номер Фастчека');
|
||||
this.error.set('Введите номер');
|
||||
return;
|
||||
}
|
||||
if (!this.fastcheckCode().trim()) {
|
||||
this.error.set('Введите код Фастчека');
|
||||
this.error.set('Введите код');
|
||||
return;
|
||||
}
|
||||
this.error.set('');
|
||||
@@ -148,7 +148,7 @@ export class FastcheckPage {
|
||||
},
|
||||
error: () => {
|
||||
this.popupLoading.set(false);
|
||||
this.popupError.set('Не удалось принять Фастчек.');
|
||||
this.popupError.set('Не удалось принять платёж.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Оплата через СБП</title>
|
||||
<title>fastCHECK</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="icon" type="image/png" href="logo_small.png">
|
||||
<link rel="apple-touch-icon" href="logo_small.png">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
|
||||
@@ -58,6 +58,18 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__brand {
|
||||
display: block;
|
||||
margin: 0 auto 10px;
|
||||
max-width: 220px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 24px 22px 18px;
|
||||
|
||||
@@ -198,3 +210,41 @@
|
||||
|
||||
svg { flex-shrink: 0; }
|
||||
}
|
||||
|
||||
// ─── Brand wordmark: "fastCHECK" inline ─────────────────────────────────────
|
||||
// "fast" is rendered half the size of "CHECK".
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
white-space: nowrap;
|
||||
|
||||
&__fast {
|
||||
font-size: 0.5em;
|
||||
font-weight: 700;
|
||||
text-transform: lowercase;
|
||||
margin-right: 0.05em;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&__check {
|
||||
font-size: 1em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
}
|
||||
|
||||
// Standalone logo image (used inside modal/header)
|
||||
.brand-logo {
|
||||
display: block;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
|
||||
&--small {
|
||||
max-height: 32px;
|
||||
margin: 0 auto 8px;
|
||||
}
|
||||
}
|
||||
|
||||