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
This commit is contained in:
@@ -33,6 +33,7 @@ interface CreateQrResponse {
|
||||
nspkurl?: string; // actual field name in real responses
|
||||
qrUrl?: string;
|
||||
status?: string; // e.g. "REGISTERED"
|
||||
qrTTL?: number;
|
||||
}
|
||||
|
||||
interface QrStatusResponse {
|
||||
@@ -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<string, string> = {
|
||||
'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<number>(30);
|
||||
@@ -84,6 +87,9 @@ export class CreatePage {
|
||||
qrStatus = signal<string>('');
|
||||
paymentDone = signal<boolean>(false);
|
||||
private pollHandle: ReturnType<typeof setInterval> | 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 {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<QrStatusResponse>(`${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<QrStatusResponse>(
|
||||
`${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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user