This commit is contained in:
2026-05-05 00:52:03 +04:00
parent cf634f766f
commit 59bda137e5
29 changed files with 1347 additions and 118 deletions

View File

@@ -3,8 +3,32 @@ import { FormsModule } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { FastcheckService } from '../../fastcheck.service';
import { FASTCHECK_API } from '../../api';
import { FASTCHECK_API, QR_VITANOVA_API } from '../../api';
import { TranslatePipe } from '../../translate/translate.pipe';
import { TranslationService } from '../../translate/translation.service';
type PaymentMethod = 'sbp';
type Currency = 'RUB';
interface SettingsResponse {
minAmount?: number;
maxAmount?: number;
[key: string]: unknown;
}
interface CreateQrResponse {
qrId?: string;
payload?: string; // raw QR data string to encode
qrUrl?: string; // pre-rendered image URL (if provided)
Status?: boolean;
[key: string]: unknown;
}
interface QrStatusResponse {
qrStatus?: string; // e.g. "PAID"
Status?: boolean;
[key: string]: unknown;
}
interface CreateFastcheckResponse {
fastcheck: string;
@@ -13,8 +37,13 @@ interface CreateFastcheckResponse {
Status: boolean;
}
type PaymentMethod = 'sbp' | 'wechat' | 'visa' | 'master';
type Currency = 'RUB' | 'CNY' | 'USD' | 'EUR' | 'AMD';
/** Generate a v4-like UUID without crypto dependency. */
function generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
});
}
@Component({
selector: 'app-create-page',
@@ -26,19 +55,22 @@ export class CreatePage {
private http = inject(HttpClient);
private store = inject(FastcheckService);
private router = inject(Router);
private i18n = inject(TranslationService);
amount = signal<number>(10);
private t(key: string): string { return this.i18n.translate(key); }
// Limits updated from settings API on init.
minAmount = signal<number>(30);
maxAmount = signal<number>(200_000);
amount = signal<number>(100);
note = signal<string>('');
error = signal<string>('');
loading = signal<boolean>(false);
settingsLoaded = signal<boolean>(false);
payment = signal<PaymentMethod>('sbp');
currency = signal<Currency>('RUB');
/** sessionID for the Authorization header. Comes from ?session=... or websession. */
private get sessionId(): string {
return new URLSearchParams(window.location.search).get('session') ?? '';
}
payment = signal<PaymentMethod>('sbp');
selectPayment(method: PaymentMethod, enabled: boolean): void {
if (!enabled) return;
@@ -50,10 +82,49 @@ export class CreatePage {
this.currency.set(c);
}
// QR display state
qrImageUrl = signal<string | null>(null);
qrPolling = 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 {
return new URLSearchParams(window.location.search).get('auth-key') ?? '';
}
private get userId(): string {
return new URLSearchParams(window.location.search).get('user-id') ?? '';
}
private get sessionId(): string {
return new URLSearchParams(window.location.search).get('session') ?? '';
}
private get reference(): string {
return new URLSearchParams(window.location.search).get('ref') ?? window.location.hostname;
}
constructor() {
this.loadSettings();
}
private loadSettings(): void {
this.http.get<SettingsResponse>(`${QR_VITANOVA_API}/settings`).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 defaults
});
}
createCheck(): void {
const val = this.amount();
if (!val || val <= 0) {
this.error.set('Введите корректную сумму');
if (!val || val < this.minAmount()) {
this.error.set(`${this.t('errors.invalid_amount')} (мин. ${this.minAmount()} ₽)`);
return;
}
if (val > this.maxAmount()) {
this.error.set(`${this.t('errors.invalid_amount')} (макс. ${this.maxAmount().toLocaleString('ru')} ₽)`);
return;
}
@@ -61,35 +132,97 @@ export class CreatePage {
this.loading.set(true);
const headers: Record<string, string> = {};
if (this.sessionId) {
headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId });
}
if (this.authKey) headers['authorization-key'] = this.authKey;
if (this.userId) headers['userid-value'] = this.userId;
const partnerqrID = generateUUID();
this.http
.post<CreateFastcheckResponse>(
`${FASTCHECK_API}/fastcheck`,
{ amount: val, currency: this.currency() },
.post<CreateQrResponse>(
`${QR_VITANOVA_API}/qr`,
{
qrtype: 'QRDynamic',
amount: val,
currency: this.currency(),
partnerqrID,
qrDescription: this.note().trim(),
Userid: this.userId,
Reference: this.reference
},
{ headers }
)
.subscribe({
next: (res) => {
this.loading.set(false);
if (res?.qrId || res?.payload || res?.qrUrl) {
this.activeQrId = res.qrId ?? '';
// Use server-provided image URL or generate one from payload string.
const qrData = res.qrUrl ?? (res.payload
? `https://api.qrserver.com/v1/create-qr-code/?size=240x240&margin=8&data=${encodeURIComponent(res.payload)}`
: null);
this.qrImageUrl.set(qrData);
if (this.activeQrId) this.startPolling(this.activeQrId);
} else {
this.error.set(this.t('errors.payment_failed'));
}
},
error: (err) => {
this.loading.set(false);
const msg: string | undefined = err?.error?.message;
this.error.set(msg ?? this.t('errors.lookup_failed'));
}
});
}
private startPolling(qrId: string): void {
this.stopPolling();
this.qrPolling.set(true);
this.pollHandle = setInterval(() => {
this.http.get<QrStatusResponse>(`${QR_VITANOVA_API}/qr/dynamic/${qrId}`)
.subscribe({
next: (res) => {
const paid = res?.qrStatus === 'PAID' || res?.Status === true;
if (paid) {
this.stopPolling();
this.createFastcheck();
}
},
error: () => undefined
});
}, 3000);
}
private stopPolling(): void {
if (this.pollHandle !== null) {
clearInterval(this.pollHandle);
this.pollHandle = null;
}
this.qrPolling.set(false);
}
private createFastcheck(): void {
const headers: Record<string, string> = {};
if (this.sessionId) headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId });
this.http
.post<CreateFastcheckResponse>(
`${FASTCHECK_API}/fastcheck`,
{ amount: this.amount(), currency: this.currency() },
{ headers }
)
.subscribe({
next: (res) => {
if (res?.fastcheck) {
this.store.setCreated({
fastcheck: res.fastcheck,
code: res.code,
amount: val,
amount: this.amount(),
expiration: res.expiration
});
this.router.navigate(['/']);
} else {
this.error.set('Не удалось создать платёж.');
}
this.router.navigate(['/']);
},
error: () => {
this.loading.set(false);
this.error.set('Ошибка при создании платежа. Попробуйте ещё раз.');
}
error: () => this.router.navigate(['/'])
});
}