From a5d42d22e7e61be2b55a631612c681f76e8de909 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Thu, 23 Jul 2026 00:26:50 +0400 Subject: [PATCH] feat(payment): add customerID field, TTL-based QR polling - POST /qr body gains customerID (telegram id), qrDescription now guaranteed non-empty (fallback text when note is blank) - CreateQrResponse gains qrTTL; polling now bounded by it (min 60s) instead of running unbounded via setInterval --- src/app/pages/create-page/create-page.ts | 66 ++++++++++++++++-------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/src/app/pages/create-page/create-page.ts b/src/app/pages/create-page/create-page.ts index 8e4c9ac..4aa5aaa 100644 --- a/src/app/pages/create-page/create-page.ts +++ b/src/app/pages/create-page/create-page.ts @@ -29,14 +29,15 @@ interface SettingsResponse { interface CreateQrResponse { qrId?: string; nspkID?: string; - Payload?: string; // per API doc (capital P) - nspkurl?: string; // actual field name in real responses + Payload?: string; // per API doc (capital P) + nspkurl?: string; // actual field name in real responses qrUrl?: string; - status?: string; // e.g. "REGISTERED" + status?: string; // e.g. "REGISTERED" + qrTTL?: number; } interface QrStatusResponse { - status?: string; // "REGISTERED" | "NEW" | "APPROVED" | "REJECTED" | "COMPLETED" + status?: string; // "REGISTERED" | "NEW" | "APPROVED" | "REJECTED" | "COMPLETED" [key: string]: unknown; } @@ -44,16 +45,18 @@ interface QrStatusResponse { selector: 'app-create-page', imports: [FormsModule, TranslatePipe], templateUrl: './create-page.html', - styleUrl: './create-page.scss' + styleUrl: './create-page.scss', }) export class CreatePage { private http = inject(HttpClient); private i18n = inject(TranslationService); private readonly sites: Record = { - '51': 'fastcheck.store' + '51': 'fastcheck.store', }; - private t(key: string): string { return this.i18n.translate(key); } + private t(key: string): string { + return this.i18n.translate(key); + } // Limits – updated from settings API on init. minAmount = signal(30); @@ -84,6 +87,9 @@ export class CreatePage { qrStatus = signal(''); paymentDone = signal(false); private pollHandle: ReturnType | null = null; + private static readonly POLL_INTERVAL_MS = 5000; + private static readonly MIN_POLL_SECONDS = 60; + private pollChecksRemaining = 0; /** Auth credentials passed by the host page as URL params. */ private get authKey(): string { @@ -102,7 +108,7 @@ export class CreatePage { return new URLSearchParams(window.location.search).get('from') ?? ''; } - get isMobile(): boolean { + get isMobile(): boolean { return window.innerWidth < 768; } @@ -126,7 +132,7 @@ export class CreatePage { } else { this.lockedAmount.set(null); } - } + }, }); } @@ -137,7 +143,9 @@ export class CreatePage { return; } if (val !== null && val > this.maxAmount()) { - this.error.set(`${this.t('errors.invalid_amount')} (макс. ${this.maxAmount().toLocaleString('ru')} ₽)`); + this.error.set( + `${this.t('errors.invalid_amount')} (макс. ${this.maxAmount().toLocaleString('ru')} ₽)`, + ); return; } @@ -152,7 +160,7 @@ export class CreatePage { const headers: Record = {}; if (this.authKey) headers['authorization-key'] = this.authKey; - if (this.userId) headers['userid-value'] = this.userId; + if (this.userId) headers['userid-value'] = this.userId; this.http .post( @@ -162,12 +170,13 @@ export class CreatePage { ...(val !== null ? { amount: val } : {}), currency: this.currency(), partnerqrID, - qrDescription: this.note().trim(), + qrDescription: this.note().trim() || 'Покупка на Маркетплейсе', + customerID: this.userId, Userid: this.userId, Reference: this.reference, - RedirectUrl: `https://fastcheck.store?id=fast-c202-4062-bcfb-8b4c8cc59adc` + RedirectUrl: `https://fastcheck.store?id=fast-c202-4062-bcfb-8b4c8cc59adc`, }, - { headers } + { headers }, ) .subscribe({ next: (res) => { @@ -187,7 +196,7 @@ export class CreatePage { ? `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); + if (qrId) this.startPolling(qrId, res?.qrTTL); } else { this.error.set(this.t('errors.payment_failed')); } @@ -196,15 +205,30 @@ export class CreatePage { 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 { + private startPolling(qrId: string, qrTTL?: number): void { this.stopPolling(); this.qrPolling.set(true); + + const pollSeconds = Math.max(CreatePage.MIN_POLL_SECONDS, (qrTTL ?? 0) * 60); + this.pollChecksRemaining = Math.ceil(pollSeconds / (CreatePage.POLL_INTERVAL_MS / 1000)); + this.pollHandle = setInterval(() => { - this.http.get(`${QR_VITANOVA_API}/qr/dynamic/${encodeURIComponent(this.partnerqrID)}/${qrId}`) + if (this.pollChecksRemaining <= 0) { + this.stopPolling(); + this.error.set(this.t('errors.payment_failed')); + this.qrImageUrl.set(null); + return; + } + this.pollChecksRemaining -= 1; + + this.http + .get( + `${QR_VITANOVA_API}/qr/dynamic/${encodeURIComponent(this.partnerqrID)}/${qrId}`, + ) .subscribe({ next: (res) => { const st = res?.status ?? ''; @@ -221,9 +245,9 @@ export class CreatePage { error: () => { this.closeQr(); this.error.set('оплата не прошла'); - } + }, }); - }, 5000); + }, CreatePage.POLL_INTERVAL_MS); } private stopPolling(): void { @@ -268,7 +292,7 @@ export class CreatePage { .post(`https://fastcheck.store/api/fastcheck/settings/${encodeURIComponent(id)}`, paidQr) .subscribe({ next: () => this.redirectToSource(id), - error: () => this.redirectToSource(id) + error: () => this.redirectToSource(id), }); }