2026-04-30 01:17:17 +04:00
|
|
|
|
import { Component, inject, signal } from '@angular/core';
|
|
|
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
|
|
|
import { HttpClient } from '@angular/common/http';
|
2026-05-07 00:13:45 +04:00
|
|
|
|
import { QR_VITANOVA_API } from '../../api';
|
2026-05-04 23:56:38 +04:00
|
|
|
|
import { TranslatePipe } from '../../translate/translate.pipe';
|
2026-05-05 00:52:03 +04:00
|
|
|
|
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;
|
2026-05-05 01:45:46 +04:00
|
|
|
|
nspkID?: string;
|
|
|
|
|
|
Payload?: string; // per API doc (capital P)
|
|
|
|
|
|
nspkurl?: string; // actual field name in real responses
|
|
|
|
|
|
qrUrl?: string;
|
2026-05-05 11:53:52 +04:00
|
|
|
|
status?: string; // e.g. "REGISTERED"
|
2026-05-05 00:52:03 +04:00
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface QrStatusResponse {
|
2026-05-05 11:53:52 +04:00
|
|
|
|
status?: string; // "REGISTERED" | "NEW" | "APPROVED" | "REJECTED" | "COMPLETED"
|
2026-05-05 01:45:46 +04:00
|
|
|
|
nspkurl?: string;
|
|
|
|
|
|
nspkID?: string;
|
2026-05-05 00:52:03 +04:00
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
|
}
|
2026-04-30 01:17:17 +04:00
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
|
selector: 'app-create-page',
|
2026-05-07 00:13:45 +04:00
|
|
|
|
imports: [FormsModule, TranslatePipe],
|
2026-04-30 01:17:17 +04:00
|
|
|
|
templateUrl: './create-page.html',
|
|
|
|
|
|
styleUrl: './create-page.scss'
|
|
|
|
|
|
})
|
|
|
|
|
|
export class CreatePage {
|
|
|
|
|
|
private http = inject(HttpClient);
|
2026-05-05 00:52:03 +04:00
|
|
|
|
private i18n = inject(TranslationService);
|
|
|
|
|
|
|
|
|
|
|
|
private t(key: string): string { return this.i18n.translate(key); }
|
2026-04-30 01:17:17 +04:00
|
|
|
|
|
2026-05-05 00:52:03 +04:00
|
|
|
|
// Limits – updated from settings API on init.
|
|
|
|
|
|
minAmount = signal<number>(30);
|
|
|
|
|
|
maxAmount = signal<number>(200_000);
|
|
|
|
|
|
|
2026-05-05 11:23:07 +04:00
|
|
|
|
amount = signal<number | null>(null);
|
2026-04-30 01:17:17 +04:00
|
|
|
|
note = signal<string>('');
|
|
|
|
|
|
error = signal<string>('');
|
|
|
|
|
|
loading = signal<boolean>(false);
|
2026-05-05 00:52:03 +04:00
|
|
|
|
settingsLoaded = signal<boolean>(false);
|
2026-04-30 01:17:17 +04:00
|
|
|
|
|
|
|
|
|
|
currency = signal<Currency>('RUB');
|
2026-05-05 00:52:03 +04:00
|
|
|
|
payment = signal<PaymentMethod>('sbp');
|
2026-04-30 01:17:17 +04:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 00:52:03 +04:00
|
|
|
|
// QR display state
|
|
|
|
|
|
qrImageUrl = signal<string | null>(null);
|
|
|
|
|
|
qrPolling = signal<boolean>(false);
|
2026-05-05 11:53:52 +04:00
|
|
|
|
qrStatus = signal<string>('');
|
2026-05-07 00:13:45 +04:00
|
|
|
|
paymentDone = signal<boolean>(false);
|
2026-05-05 00:52:03 +04:00
|
|
|
|
private pollHandle: ReturnType<typeof setInterval> | null = null;
|
|
|
|
|
|
private activeQrId = '';
|
|
|
|
|
|
|
|
|
|
|
|
/** Auth credentials passed by the host page as URL params. */
|
|
|
|
|
|
private get authKey(): string {
|
2026-05-05 10:46:49 +04:00
|
|
|
|
return new URLSearchParams(window.location.search).get('authorization-key') ?? '';
|
2026-05-05 00:52:03 +04:00
|
|
|
|
}
|
|
|
|
|
|
private get userId(): string {
|
2026-05-05 10:46:49 +04:00
|
|
|
|
return new URLSearchParams(window.location.search).get('userid-value') ?? '';
|
2026-05-05 00:52:03 +04:00
|
|
|
|
}
|
|
|
|
|
|
private get reference(): string {
|
|
|
|
|
|
return new URLSearchParams(window.location.search).get('ref') ?? window.location.hostname;
|
|
|
|
|
|
}
|
2026-05-08 18:58:25 +04:00
|
|
|
|
private get partnerqrID(): string {
|
|
|
|
|
|
return new URLSearchParams(window.location.search).get('id') ?? '';
|
|
|
|
|
|
}
|
2026-05-05 00:52:03 +04:00
|
|
|
|
|
2026-05-06 18:21:22 +04:00
|
|
|
|
get isMobile(): boolean {
|
|
|
|
|
|
return window.innerWidth < 768;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 00:52:03 +04:00
|
|
|
|
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
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 01:17:17 +04:00
|
|
|
|
createCheck(): void {
|
|
|
|
|
|
const val = this.amount();
|
2026-05-05 11:23:07 +04:00
|
|
|
|
if (val !== null && val < this.minAmount()) {
|
2026-05-05 00:52:03 +04:00
|
|
|
|
this.error.set(`${this.t('errors.invalid_amount')} (мин. ${this.minAmount()} ₽)`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-05 11:23:07 +04:00
|
|
|
|
if (val !== null && val > this.maxAmount()) {
|
2026-05-05 00:52:03 +04:00
|
|
|
|
this.error.set(`${this.t('errors.invalid_amount')} (макс. ${this.maxAmount().toLocaleString('ru')} ₽)`);
|
2026-04-30 01:17:17 +04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 18:58:25 +04:00
|
|
|
|
const partnerqrID = this.partnerqrID;
|
|
|
|
|
|
if (!partnerqrID) {
|
|
|
|
|
|
this.error.set(this.t('errors.lookup_failed'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 01:17:17 +04:00
|
|
|
|
this.error.set('');
|
|
|
|
|
|
this.loading.set(true);
|
|
|
|
|
|
|
|
|
|
|
|
const headers: Record<string, string> = {};
|
2026-05-05 00:52:03 +04:00
|
|
|
|
if (this.authKey) headers['authorization-key'] = this.authKey;
|
|
|
|
|
|
if (this.userId) headers['userid-value'] = this.userId;
|
|
|
|
|
|
|
|
|
|
|
|
this.http
|
|
|
|
|
|
.post<CreateQrResponse>(
|
|
|
|
|
|
`${QR_VITANOVA_API}/qr`,
|
|
|
|
|
|
{
|
|
|
|
|
|
qrtype: 'QRDynamic',
|
2026-05-05 11:23:07 +04:00
|
|
|
|
...(val !== null ? { amount: val } : {}),
|
2026-05-05 00:52:03 +04:00
|
|
|
|
currency: this.currency(),
|
|
|
|
|
|
partnerqrID,
|
|
|
|
|
|
qrDescription: this.note().trim(),
|
|
|
|
|
|
Userid: this.userId,
|
|
|
|
|
|
Reference: this.reference
|
|
|
|
|
|
},
|
|
|
|
|
|
{ headers }
|
|
|
|
|
|
)
|
|
|
|
|
|
.subscribe({
|
|
|
|
|
|
next: (res) => {
|
|
|
|
|
|
this.loading.set(false);
|
2026-05-05 01:45:46 +04:00
|
|
|
|
const qrId = res?.qrId ?? res?.nspkID ?? '';
|
|
|
|
|
|
// Real API uses 'nspkurl'; doc says 'Payload' — try both
|
|
|
|
|
|
const nspkUrl = res?.nspkurl ?? res?.Payload;
|
2026-05-05 11:53:52 +04:00
|
|
|
|
this.qrStatus.set(res?.status ?? '');
|
2026-05-06 18:21:22 +04:00
|
|
|
|
|
|
|
|
|
|
if (nspkUrl && this.isMobile) {
|
|
|
|
|
|
window.location.href = nspkUrl;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 01:45:46 +04:00
|
|
|
|
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);
|
2026-05-05 00:52:03 +04:00
|
|
|
|
this.qrImageUrl.set(qrData);
|
2026-05-05 01:45:46 +04:00
|
|
|
|
if (qrId) this.startPolling(qrId);
|
2026-05-05 00:52:03 +04:00
|
|
|
|
} 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) => {
|
2026-05-05 11:53:52 +04:00
|
|
|
|
const st = res?.status ?? '';
|
|
|
|
|
|
this.qrStatus.set(st);
|
|
|
|
|
|
if (st === 'COMPLETED' || st === 'APPROVED') {
|
2026-05-05 00:52:03 +04:00
|
|
|
|
this.stopPolling();
|
2026-05-07 00:13:45 +04:00
|
|
|
|
this.paymentDone.set(true);
|
2026-05-05 11:53:52 +04:00
|
|
|
|
} else if (st === 'REJECTED') {
|
2026-05-05 01:45:46 +04:00
|
|
|
|
this.stopPolling();
|
|
|
|
|
|
this.error.set(this.t('errors.payment_failed'));
|
|
|
|
|
|
this.qrImageUrl.set(null);
|
2026-05-05 00:52:03 +04:00
|
|
|
|
}
|
2026-05-05 11:53:52 +04:00
|
|
|
|
// REGISTERED / NEW / '' — keep polling
|
2026-05-05 00:52:03 +04:00
|
|
|
|
},
|
|
|
|
|
|
error: () => undefined
|
|
|
|
|
|
});
|
2026-05-05 11:53:52 +04:00
|
|
|
|
}, 5000);
|
2026-05-05 00:52:03 +04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private stopPolling(): void {
|
|
|
|
|
|
if (this.pollHandle !== null) {
|
|
|
|
|
|
clearInterval(this.pollHandle);
|
|
|
|
|
|
this.pollHandle = null;
|
2026-04-30 01:17:17 +04:00
|
|
|
|
}
|
2026-05-05 00:52:03 +04:00
|
|
|
|
this.qrPolling.set(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 11:23:07 +04:00
|
|
|
|
onAmountChange(value: number | null): void {
|
|
|
|
|
|
this.amount.set(value || null);
|
|
|
|
|
|
if (value && value > 0) this.error.set('');
|
2026-04-30 01:17:17 +04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onNoteChange(value: string): void {
|
|
|
|
|
|
this.note.set(value);
|
|
|
|
|
|
}
|
2026-05-05 11:31:51 +04:00
|
|
|
|
|
|
|
|
|
|
closeQr(): void {
|
|
|
|
|
|
this.qrImageUrl.set(null);
|
|
|
|
|
|
this.qrPolling.set(false);
|
2026-05-05 11:53:52 +04:00
|
|
|
|
this.qrStatus.set('');
|
2026-05-07 00:13:45 +04:00
|
|
|
|
this.paymentDone.set(false);
|
2026-05-05 11:31:51 +04:00
|
|
|
|
if (this.pollHandle !== null) {
|
|
|
|
|
|
clearInterval(this.pollHandle);
|
|
|
|
|
|
this.pollHandle = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-30 01:17:17 +04:00
|
|
|
|
}
|