Files
qr_vitanova/src/app/pages/create-page/create-page.ts
sdarbinyan e0df81c071 link
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 18:21:22 +04:00

275 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Component, inject, signal } from '@angular/core';
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, 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;
nspkID?: string;
Payload?: string; // per API doc (capital P)
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;
}
interface CreateFastcheckResponse {
id?: string; // real field name from server
fastcheck?: string; // per API doc fallback
expiration?: string;
code?: string;
amount?: number;
Status?: boolean;
}
/** 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',
imports: [FormsModule, RouterLink, TranslatePipe],
templateUrl: './create-page.html',
styleUrl: './create-page.scss'
})
export class CreatePage {
private http = inject(HttpClient);
private store = inject(FastcheckService);
private router = inject(Router);
private i18n = inject(TranslationService);
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 | null>(null);
note = signal<string>('');
error = signal<string>('');
loading = signal<boolean>(false);
settingsLoaded = signal<boolean>(false);
currency = signal<Currency>('RUB');
payment = signal<PaymentMethod>('sbp');
selectPayment(method: PaymentMethod, enabled: boolean): void {
if (!enabled) return;
this.payment.set(method);
}
selectCurrency(c: Currency, enabled: boolean): void {
if (!enabled) return;
this.currency.set(c);
}
// QR display state
qrImageUrl = signal<string | null>(null);
qrPolling = signal<boolean>(false);
qrStatus = signal<string>('');
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('authorization-key') ?? '';
}
private get userId(): string {
return new URLSearchParams(window.location.search).get('userid-value') ?? '';
}
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;
}
get isMobile(): boolean {
return window.innerWidth < 768;
}
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 !== null && val < this.minAmount()) {
this.error.set(`${this.t('errors.invalid_amount')} (мин. ${this.minAmount()} ₽)`);
return;
}
if (val !== null && val > this.maxAmount()) {
this.error.set(`${this.t('errors.invalid_amount')} (макс. ${this.maxAmount().toLocaleString('ru')} ₽)`);
return;
}
this.error.set('');
this.loading.set(true);
const headers: Record<string, string> = {};
if (this.authKey) headers['authorization-key'] = this.authKey;
if (this.userId) headers['userid-value'] = this.userId;
const partnerqrID = generateUUID();
this.http
.post<CreateQrResponse>(
`${QR_VITANOVA_API}/qr`,
{
qrtype: 'QRDynamic',
...(val !== null ? { amount: val } : {}),
currency: this.currency(),
partnerqrID,
qrDescription: this.note().trim(),
Userid: this.userId,
Reference: this.reference
},
{ headers }
)
.subscribe({
next: (res) => {
this.loading.set(false);
const qrId = res?.qrId ?? res?.nspkID ?? '';
// Real API uses 'nspkurl'; doc says 'Payload' — try both
const nspkUrl = res?.nspkurl ?? res?.Payload;
this.qrStatus.set(res?.status ?? '');
if (nspkUrl && this.isMobile) {
window.location.href = nspkUrl;
return;
}
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);
this.qrImageUrl.set(qrData);
if (qrId) this.startPolling(qrId);
} 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 st = res?.status ?? '';
this.qrStatus.set(st);
if (st === 'COMPLETED' || st === 'APPROVED') {
this.stopPolling();
this.createFastcheck();
} else if (st === 'REJECTED') {
this.stopPolling();
this.error.set(this.t('errors.payment_failed'));
this.qrImageUrl.set(null);
}
// REGISTERED / NEW / '' — keep polling
},
error: () => undefined
});
}, 5000);
}
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) => {
const fcNumber = res?.id ?? res?.fastcheck ?? '';
const payload = {
fastcheck: fcNumber,
code: res?.code ?? '',
amount: res?.amount ?? this.amount() ?? null,
expiration: res?.expiration
};
if (fcNumber) {
this.store.setCreated(payload);
}
this.router.navigate(['/'], { state: fcNumber ? payload : {} });
},
error: () => this.router.navigate(['/'])
});
}
onAmountChange(value: number | null): void {
this.amount.set(value || null);
if (value && value > 0) this.error.set('');
}
onNoteChange(value: string): void {
this.note.set(value);
}
closeQr(): void {
this.qrImageUrl.set(null);
this.qrPolling.set(false);
this.qrStatus.set('');
if (this.pollHandle !== null) {
clearInterval(this.pollHandle);
this.pollHandle = null;
}
}
}