Compare commits

...

21 Commits

Author SHA1 Message Date
a52fd07273 spb 2026-06-21 21:02:53 +04:00
fe7fea151a spb2 2026-06-21 21:02:19 +04:00
e9acbd4898 fix 2026-06-01 15:33:23 +04:00
4841cdf90d important 2026-06-01 01:55:16 +04:00
c2a0675c79 change 2026-06-01 01:45:16 +04:00
e62afe07eb style changes 2026-06-01 00:27:37 +04:00
926afc5691 made default 2026-05-15 15:57:46 +04:00
45769ca817 redirect 2026-05-15 15:56:48 +04:00
02a33e9b14 param 2026-05-14 17:22:43 +04:00
9c96370235 location 2026-05-14 15:37:15 +04:00
9cbb6660f8 removed unused imports 2026-05-14 09:29:15 +04:00
b1ffd577c5 changes 2026-05-14 00:48:10 +04:00
bee56afedc qr check 2026-05-13 17:31:44 +04:00
ce2c9c42fe api change 2026-05-13 16:47:51 +04:00
17dfad5eaa added api 2026-05-13 16:05:17 +04:00
abb4f7b849 changed title 2026-05-13 14:50:44 +04:00
097064281a error 2026-05-13 14:42:02 +04:00
0330e0a212 dynamic + check for statuss 200 2026-05-13 14:35:50 +04:00
5147d05ea2 qr id 2026-05-13 11:26:28 +04:00
14d9642568 remove footer 2026-05-13 00:38:51 +04:00
6e7527cf1e some improvements 2026-05-08 23:55:07 +04:00
13 changed files with 96 additions and 449 deletions

4
.gitignore vendored
View File

@@ -5,8 +5,10 @@
/out-tsc
/bazel-out
/dist
# Local-only docs and scratch (not for publishing)
/docs/
changes.txt
api.txt
# Node
/node_modules

View File

@@ -148,7 +148,7 @@
<div class="card__header">
<div class="sbp-logo">
<img src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg" alt="СБП" />
<img src="public/sbp.svg" alt="СБП" />
</div>
<h1 class="card__title">Оплата через СБП</h1>
<p class="card__subtitle">Система быстрых платежей</p>

View File

@@ -1,142 +0,0 @@
eFastcheck.store
General Information
Information exchange with the Fastcheck server is realized via RESTful API. All requests to the server must be executed via HTTPS using GET||POST||PUT||DELETE requests to the given ROOT address. Body of requests must be in JSON format. All not public requests must be signed by the client and the public key must be sent to the server for client identification and sign checking.
Check if server is available
Client needs to periodically check if the server is available by sending “ping” to the client. On error corresponding message must be shown.
Protocol: https
Root Path: api.Fastcheck.store
Type GET
Path /ping
Request Parameters:
{
}
Response (OK):
{
"message": "pong",
}
________________
Create new websession
Creates a new websession for qr generation. By timeout a new websession must be requested, after the user shows some activity (click on qr).
Protocol: https
Root Path: api.Fastcheck.store
Type GET
Path /websession
Request Parameters:
{
}
Response (OK):
{
"sessionId": “1AF3781BF6B94604B771AEA1D44FA63A”
"userId" : "",
"expires" : "sessionId",
"userSessionId": "",
"Status": false
}
________________
Check websession status
Check if the user is already logged in. a new websession for qr generation. By timeout a new websession must be requested, after the user shows some activity (click on qr).
Protocol: https
Root Path: api.Fastcheck.store
Type GET
Path /websession/:webSessionID
Request Parameters:
{
}
Response (OK):
{
"sessionId": “1AF3781BF6B94604B771AEA1D44FA63A”,
"userId" : "kHaAe9roaC2uq63AKGE/8+Ti/t/iFro68QhEZ1dRGLo",
"expires" : "sessionId",
"userSessionId": "8A94EFEFD003426A9B456C48CAC99BE6",
"Status": true
}
________________
Delete websession status
Delete the session to log out from the system.
Protocol: https
Root Path: api.Fastcheck.store
Type DELETE
Path /websession/:webSessionID
Request Parameters:
{
"sessionId": “1AF3781BF6B94604B771AEA1D44FA63A”
}
Response (OK):
{
}
________________
Check Fastcheck status
Check if fastcheck exists and get the amount assigned to check.
Protocol: https
Root Path: api.Fastcheck.store
Type GET
Path /fastcheck
Request Parameters:
{
"fastcheck": “1234-5678-0001”,
}
Response (OK):
{
"fastcheck": "1234-5678-0001",
"expiration": 2021-07-07T09:08:18Z ,
"Status": true
}
________________
New Fastcheck
Create a fastcheck for a given amount. The Users must have a sufficient amount on the balance.
Protocol: https
Root Path: api.Fastcheck.store
Type POST
Path /fastcheck
HEADER: Authorization - {"sessionID": "1AF3781BF6B94604B771AEA1D44FA63A"}
Request Parameters:
{
"amount": 158000,
"currency": "RUB"
}
Response (OK):
{
"fastcheck": "1234-5678-0001",
"expiration": 2021-07-07T09:08:18Z ,
"code": "5864",
"Status": true
}
________________
Accept Fastcheck
Accept fastcheck to the user balance.
Protocol: https
Root Path: api.Fastcheck.store
Type POST
Path /fastcheck
HEADER: Authorization - {"sessionID": "1AF3781BF6B94604B771AEA1D44FA63A"}
Request Parameters:
{
"fastcheck": "1234-5678-0001",
"code": "5864"
}
Response (404-ERROR):
{
"message": "not authorized"
}
Response (200-OK):
{
"message": "ok"
}

View File

@@ -1,262 +0,0 @@
General Information
Information exchange with the SBP server is realized via RESTful API. All requests to the server must be executed via HTTPS using GET||POST||PUT||DELETE requests to the given ROOT address. Body of requests must be in JSON format. All not public requests must be signed by the client and the public key must be sent to the server for client identification and sign checking.
Header:
“Authorization”: {JSON WITH KEY AND PARTNERID}
Check if server is available
Client needs to periodically check if the server is available by sending “ping” to the client. On error corresponding message must be shown.
Protocol: https
Root Path: QR.VITANOVA.NETWORK
Type GET
Path /ping
Request Parameters:
{
}
Response (Error):
{
"message": "pong",
"status": "Wrong Header"
}
Response (OK):
{
"message": "pong",
"status": "Correct Header"
}
________________
Create New QR code
Create New QR for payment via SBP
Protocol: https
Root Path: QR.VITANOVA.NETWORK
Type POST
Path /qr
Request Parameters:
{
"amount": 10.00, //amount from 10Rub to 499.000 Rub
"qrDescription": "Item description",
"order": "540", //orderid at partners platform
"partnerID": 102 //same as in header
"Phonemask": 79xxxx66265 //User phone number mask, needed only for crypto based operations. Payment will be accepted only from phone numbers corresponding to the mask
"Namelastname": Hakxx Sargxxxx /Mask for User name, lastname in cyrilic, needed only for crypto based operations. Payment will be accepted only from the user corresponding to that mask.
}
Response !=200(Error):
{
"error": "wrong key"
}
Response =200(OK):
{
"qrId": "BD10002CI1V3JP1T8QR8TIQ8K35RBVQB",
"qrStatus": "NEW",
"qrExpirationDate": "2025-11-20T10:10:44Z",
"Payload": "https://qr.nspk.ru/BD10002CI1V3JP1T8QR8TIQ8K35RBVQB?type=02&bank=100000000007&sum=1000&cur=RUB&crc=8ACC",
"qrUrl": "https://e-commerce.raiffeisen.ru/api/sbp/v1/qr/BD10002CI1V3JP1T8QR8TIQ8K35RBVQB/image"
}
________________
Check Dynamic QR code
Check QR status
Protocol: https
Root Path: QR.VITANOVA.NETWORK
Type GET
Path /qr/dynamic/{qrId}
Request Parameters:
{
}
Response !=200(Error):
{
"error": "Error from the bank "
}
Response =200(OK):
{
`json:"nspkID"` //": "AD100060JFQF8FSB9Q28FFL88IH6SST0" `json:"amount"` // "1235"
`json:"currency" // "RUB"
`json:"order"` // "126" partner order id PaymentDetails
`json:"paymentDetails"` // "Назначение платежа 2",
`json:"qrType"` //"QRDynamic",
`json:"qrExpirationDate"` //: "2025-11-22T09:14:38+03:00" `json:"sbpBank"` // "raiffeisen"
`json:"sbpMerchant"` //"Dexar"
`json:"sbpMerchantId"` //"", uint64
`json:"sbpOperationId"` //0 Status
`json:"status"` //": "NEW", "APPROVED", "REJECTED", "COMPLETED"
`json:"nspkurl"` //"https://qr.nspk.ru/AD100060JFQF8FSB9Q28FFL88IH6SST0
`json:"statusurl"` // "https://partner.com/1234321/status" url for checking QR `json:"redirectUrl"` //"https://fastcheck.store/"
`json:"qrDescription"` //"QR для оплаты заказа"
`json:"additionalInfo"` // TTL
`json:"TTL"` //10 timeout in minutes
`json:"callbackUrl"` // https://partner.com/1234321 callback after QR get paid
`json:"retry"` //0 retry count for calling partner
`json:"partnerID"` //103 Partner created QR PartnerqrID `json:"partnerqrID"` //QR ID in partner system RequestIP
`json:"requestIP"` //IP address of client requested QR
}
________________
Check Static QR code
Get all qr-s paid by static QR for today, skipping already read qr codes
Protocol: https
Root Path: QR.VITANOVA.NETWORK
Type GET
Path /qr/static/{qrId}?skip=25
Request Parameters:
{
}
Response =200(OK):
[{
`json:"nspkID"` //": "AD100060JFQF8FSB9Q28FFL88IH6SST0" `json:"amount"` // "1235"
`json:"currency" // "RUB"
`json:"order"` // "126" partner order id PaymentDetails
`json:"paymentDetails"` // "Назначение платежа 2",
`json:"qrType"` //"QRDynamic",
`json:"qrExpirationDate"` //: "2025-11-22T09:14:38+03:00" `json:"sbpBank"` // "raiffeisen"
`json:"sbpMerchant"` //"Dexar"
`json:"sbpMerchantId"` //"", uint64
`json:"sbpOperationId"` //0 Status
`json:"status"` //": "NEW", "APPROVED", "REJECTED", "COMPLETED"
`json:"nspkurl"` //"https://qr.nspk.ru/AD100060JFQF8FSB9Q28FFL88IH6SST0
`json:"statusurl"` // "https://partner.com/1234321/status" url for checking QR `json:"redirectUrl"` //"https://fastcheck.store/"
`json:"qrDescription"` //"QR для оплаты заказа"
`json:"additionalInfo"` // TTL
`json:"TTL"` //10 timeout in minutes
`json:"callbackUrl"` // https://partner.com/1234321 callback after QR get paid
`json:"retry"` //0 retry count for calling partner
`json:"partnerID"` //103 Partner created QR PartnerqrID `json:"partnerqrID"` //QR ID in partner system RequestIP
`json:"requestIP"` //IP address of client requested QR
}]
________________
Delete QR
Delete unused QR. If QR is not paid until expiration time, it will be automatically deleted.
Protocol: https
Root Path: QR.VITANOVA.NETWORK
Type DELETE
Path /qr/{qrId}
Request Parameters:
{
}
Response !=200(Error):
{
"error": "Error from the bank "
}
Response =200(OK):
{
}
________________
Check Partner
Returns partner status, with balance and transactions. Each transaction id is QR code, which can be checked additionally.
Root Path: API.VITANOVA.NETWORK
Type Get
Path /partners/{partnerID}
Request Parameters:
{
}
Response !=200(Error):
{
"error": "Not authorized "
}
Response =200(OK):
{
"telegram_id": 8285633,
"username": "ZZZ",
"first_name": "АMAN",
"last_name": "",
"balance": 22,
"transaction": [
{
"additionalInfo": "Ручка",
"paymentPurpose": "Ручка",
"amount": 22,
"code": "SUCCESS",
"createDate": "2025-11-22T15:57:40.925104+03:00",
"currency": "RUB",
"order": "8285633735_301",
"paymentStatus": "SUCCESS",
"qrId": "AD10004C1K9N71MN907RD56UOA0BHIBR",
"transactionDate": "2025-11-22T15:58:14.814187+03:00",
"transactionId": 771515533,
"qrExpirationDate": "2025-11-22T16:12:40+03:00"
}
],
"inn": 0
}
________________
Withdraw
Get amount from balance and Creates fastcheck, which then can be for buying usdt, transferring to bank account and to bank card. Fastcheck can be checked on site or via API only by Id, but can be used only with code.
Root Path: QR.VITANOVA.NETWORK
Type POST
Path/partners/withdraw/{partnerID}
Request Parameters:
{
“amount”: 10600.00
“currency”: “RUB”
“partnerId: “1023454”
“wallet”: “TBia4uHnb3oSSZm5isP284cA7Np1v15Vhi”
“”
“rate”:79.50
}
Response !=200(Error):
{
"error": "Not enough amount on balance "
}
Response !=200(Error):
{
"error": "Rate is not correct "
}
Response =200(OK):
{
“trxID”:”T5Mv2v8n9L7jY4k1pW3QhUoZfE9R1X3s7rY6tB0pA2C4D6E8F5H”
}
________________
RATE
Get currency exchange rate.
Root Path: QR.VITANOVA.NETWORK
Type GET
Path/partners/rate
Request Parameters:
Response !=200(Error):
{
"error": "Not Authorized "
}
Response =200(OK):
{
"rate": 78.5
}

View File

@@ -148,7 +148,7 @@
<div class="card__header">
<div class="sbp-logo">
<img src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg" alt="СБП" />
<img src="sbp.svg" alt="СБП" />
</div>
<h1 class="card__title">Оплата через СБП</h1>
<p class="card__subtitle">Система быстрых платежей</p>

1
public/sbp.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,5 +1,5 @@
<app-site-header />
<!-- <app-site-header /> -->
<main class="app-main">
<router-outlet />
</main>
<app-site-footer />
<!-- <app-site-footer /> -->

View File

@@ -1,11 +1,9 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { SiteHeader } from './site-header/site-header';
import { SiteFooter } from './site-footer/site-footer';
@Component({
selector: 'app-root',
imports: [RouterOutlet, SiteHeader, SiteFooter],
imports: [RouterOutlet],
templateUrl: './app.html',
styleUrl: './app.scss'
})

View File

@@ -15,13 +15,13 @@
<button type="button" class="method" [class.method--active]="payment() === 'sbp'"
(click)="selectPayment('sbp', true)" aria-label="СБП">
<img class="method__logo"
src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg"
src="/sbp.svg"
alt="СБП" />
</button>
<button type="button" class="method method--disabled" disabled aria-label="WeChat Pay">
<img class="method__logo" src="/wechat-pay.svg" alt="WeChat Pay" />
</button>
<button type="button" class="method method--disabled" disabled aria-label="Alipay">
<button type="button" class="method method--disabled" disabled aria-label="Alipay">
<img class="method__logo" src="/alipay.svg" alt="Alipay" />
</button>
<button type="button" class="method method--disabled" disabled aria-label="Visa">

View File

@@ -234,11 +234,17 @@
}
&__label {
font-size: 13px;
font-weight: 600;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px !important;
font-weight: 500;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
margin: 0 auto 8px auto;
text-align: center;
}
&__img {

View File

@@ -9,9 +9,19 @@ type PaymentMethod = 'sbp';
type Currency = 'RUB';
interface SettingsResponse {
sbp?: boolean;
wechat?: boolean;
visa?: boolean;
mastercard?: boolean;
alipay?: boolean;
rubles?: boolean;
usd?: boolean;
euro?: boolean;
cny?: boolean;
dram?: boolean;
minAmount?: number;
maxAmount?: number;
[key: string]: unknown;
qrTTL?: number;
}
interface CreateQrResponse {
@@ -21,13 +31,10 @@ interface CreateQrResponse {
nspkurl?: string; // actual field name in real responses
qrUrl?: string;
status?: string; // e.g. "REGISTERED"
[key: string]: unknown;
}
interface QrStatusResponse {
status?: string; // "REGISTERED" | "NEW" | "APPROVED" | "REJECTED" | "COMPLETED"
nspkurl?: string;
nspkID?: string;
[key: string]: unknown;
}
@@ -40,6 +47,9 @@ interface QrStatusResponse {
export class CreatePage {
private http = inject(HttpClient);
private i18n = inject(TranslationService);
private readonly sites: Record<string, string> = {
'51': 'fastcheck.store'
};
private t(key: string): string { return this.i18n.translate(key); }
@@ -51,7 +61,6 @@ export class CreatePage {
note = signal<string>('');
error = signal<string>('');
loading = signal<boolean>(false);
settingsLoaded = signal<boolean>(false);
currency = signal<Currency>('RUB');
payment = signal<PaymentMethod>('sbp');
@@ -72,7 +81,6 @@ export class CreatePage {
qrStatus = signal<string>('');
paymentDone = signal<boolean>(false);
private pollHandle: ReturnType<typeof setInterval> | null = null;
private activeQrId = '';
/** Auth credentials passed by the host page as URL params. */
private get authKey(): string {
@@ -87,6 +95,9 @@ export class CreatePage {
private get partnerqrID(): string {
return new URLSearchParams(window.location.search).get('id') ?? '';
}
private get fromSite(): string {
return new URLSearchParams(window.location.search).get('from') ?? '';
}
get isMobile(): boolean {
return window.innerWidth < 768;
@@ -97,21 +108,13 @@ export class CreatePage {
}
private loadSettings(): void {
// The `id` query param is the user's id. Fetch per-user amount limits.
// If the call fails or omits a value, keep current defaults.
const userId = this.partnerqrID;
if (!userId) {
this.settingsLoaded.set(true);
return;
}
const url = `${QR_VITANOVA_API}/settings?id=${encodeURIComponent(userId)}`;
// Fetch limits from /qr/settings. If the call fails, keep defaults.
const url = `${QR_VITANOVA_API}/qr/settings`;
this.http.get<SettingsResponse>(url).subscribe({
next: (s) => {
if (typeof s?.minAmount === 'number') this.minAmount.set(s.minAmount);
if (typeof s?.maxAmount === 'number') this.maxAmount.set(s.maxAmount);
this.settingsLoaded.set(true);
},
error: () => this.settingsLoaded.set(true) // proceed with current defaults
}
});
}
@@ -149,7 +152,8 @@ export class CreatePage {
partnerqrID,
qrDescription: this.note().trim(),
Userid: this.userId,
Reference: this.reference
Reference: this.reference,
RedirectUrl: `https://fastcheck.store?id=fast-c202-4062-bcfb-8b4c8cc59adc`
},
{ headers }
)
@@ -167,7 +171,6 @@ export class CreatePage {
}
if (qrId || nspkUrl) {
this.activeQrId = qrId;
const qrData = nspkUrl
? `https://api.qrserver.com/v1/create-qr-code/?size=256x256&margin=8&data=${encodeURIComponent(nspkUrl)}`
: (res.qrUrl ?? null);
@@ -189,14 +192,13 @@ export class CreatePage {
this.stopPolling();
this.qrPolling.set(true);
this.pollHandle = setInterval(() => {
this.http.get<QrStatusResponse>(`${QR_VITANOVA_API}/qr/dynamic/${qrId}`)
this.http.get<QrStatusResponse>(`${QR_VITANOVA_API}/qr/dynamic/${encodeURIComponent(this.partnerqrID)}/${qrId}`)
.subscribe({
next: (res) => {
const st = res?.status ?? '';
this.qrStatus.set(st);
if (st === 'COMPLETED' || st === 'APPROVED') {
this.stopPolling();
this.paymentDone.set(true);
this.handlePaymentSuccess(res);
} else if (st === 'REJECTED') {
this.stopPolling();
this.error.set(this.t('errors.payment_failed'));
@@ -204,7 +206,10 @@ export class CreatePage {
}
// REGISTERED / NEW / '' — keep polling
},
error: () => undefined
error: () => {
this.closeQr();
this.error.set('оплата не прошла');
}
});
}, 5000);
}
@@ -226,14 +231,52 @@ export class CreatePage {
this.note.set(value);
}
closeQr(): void {
private handlePaymentSuccess(paidQr: QrStatusResponse): void {
this.stopPolling();
this.qrImageUrl.set(null);
this.qrPolling.set(false);
this.qrStatus.set('');
this.paymentDone.set(false);
if (this.pollHandle !== null) {
clearInterval(this.pollHandle);
this.pollHandle = null;
this.paymentDone.set(true);
const id = this.partnerqrID;
if (!id) {
this.redirectToSource();
return;
}
this.http
.post(`https://fastcheck.store/api/fastcheck/settings/${encodeURIComponent(id)}`, paidQr)
.subscribe({
next: () => this.redirectToSource(id),
error: () => this.redirectToSource(id)
});
}
private redirectToSource(id?: string): void {
const withId = (target: string): string => {
if (!id) return target;
const normalizedTarget = /^https?:\/\//i.test(target) ? target : `https://${target}`;
const url = new URL(normalizedTarget);
url.searchParams.set('id', id);
return url.toString();
};
const from = this.fromSite.trim();
const target = this.sites[from];
if (target) {
window.location.href = withId(target);
return;
}
if (window.history.length > 1) {
window.history.back();
}
}
closeQr(): void {
this.stopPolling();
this.qrImageUrl.set(null);
this.qrStatus.set('');
this.paymentDone.set(false);
}
}

View File

@@ -2,7 +2,7 @@
<html lang="ru">
<head>
<meta charset="utf-8">
<title>fastCHECK</title>
<title>QR Vitanova</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#2563eb">

View File

@@ -3,6 +3,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./out-tsc/app",
"types": []
},