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
|
nspkurl?: string; // actual field name in real responses
|
||||||
qrUrl?: string;
|
qrUrl?: string;
|
||||||
status?: string; // e.g. "REGISTERED"
|
status?: string; // e.g. "REGISTERED"
|
||||||
|
qrTTL?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QrStatusResponse {
|
interface QrStatusResponse {
|
||||||
@@ -44,16 +45,18 @@ interface QrStatusResponse {
|
|||||||
selector: 'app-create-page',
|
selector: 'app-create-page',
|
||||||
imports: [FormsModule, TranslatePipe],
|
imports: [FormsModule, TranslatePipe],
|
||||||
templateUrl: './create-page.html',
|
templateUrl: './create-page.html',
|
||||||
styleUrl: './create-page.scss'
|
styleUrl: './create-page.scss',
|
||||||
})
|
})
|
||||||
export class CreatePage {
|
export class CreatePage {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private i18n = inject(TranslationService);
|
private i18n = inject(TranslationService);
|
||||||
private readonly sites: Record<string, string> = {
|
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.
|
// Limits – updated from settings API on init.
|
||||||
minAmount = signal<number>(30);
|
minAmount = signal<number>(30);
|
||||||
@@ -84,6 +87,9 @@ export class CreatePage {
|
|||||||
qrStatus = signal<string>('');
|
qrStatus = signal<string>('');
|
||||||
paymentDone = signal<boolean>(false);
|
paymentDone = signal<boolean>(false);
|
||||||
private pollHandle: ReturnType<typeof setInterval> | null = null;
|
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. */
|
/** Auth credentials passed by the host page as URL params. */
|
||||||
private get authKey(): string {
|
private get authKey(): string {
|
||||||
@@ -126,7 +132,7 @@ export class CreatePage {
|
|||||||
} else {
|
} else {
|
||||||
this.lockedAmount.set(null);
|
this.lockedAmount.set(null);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +143,9 @@ export class CreatePage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (val !== null && val > this.maxAmount()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,12 +170,13 @@ export class CreatePage {
|
|||||||
...(val !== null ? { amount: val } : {}),
|
...(val !== null ? { amount: val } : {}),
|
||||||
currency: this.currency(),
|
currency: this.currency(),
|
||||||
partnerqrID,
|
partnerqrID,
|
||||||
qrDescription: this.note().trim(),
|
qrDescription: this.note().trim() || 'Покупка на Маркетплейсе',
|
||||||
|
customerID: this.userId,
|
||||||
Userid: this.userId,
|
Userid: this.userId,
|
||||||
Reference: this.reference,
|
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({
|
.subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
@@ -187,7 +196,7 @@ export class CreatePage {
|
|||||||
? `https://api.qrserver.com/v1/create-qr-code/?size=256x256&margin=8&data=${encodeURIComponent(nspkUrl)}`
|
? `https://api.qrserver.com/v1/create-qr-code/?size=256x256&margin=8&data=${encodeURIComponent(nspkUrl)}`
|
||||||
: (res.qrUrl ?? null);
|
: (res.qrUrl ?? null);
|
||||||
this.qrImageUrl.set(qrData);
|
this.qrImageUrl.set(qrData);
|
||||||
if (qrId) this.startPolling(qrId);
|
if (qrId) this.startPolling(qrId, res?.qrTTL);
|
||||||
} else {
|
} else {
|
||||||
this.error.set(this.t('errors.payment_failed'));
|
this.error.set(this.t('errors.payment_failed'));
|
||||||
}
|
}
|
||||||
@@ -196,15 +205,30 @@ export class CreatePage {
|
|||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
const msg: string | undefined = err?.error?.message;
|
const msg: string | undefined = err?.error?.message;
|
||||||
this.error.set(msg ?? this.t('errors.lookup_failed'));
|
this.error.set(msg ?? this.t('errors.lookup_failed'));
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private startPolling(qrId: string): void {
|
private startPolling(qrId: string, qrTTL?: number): void {
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
this.qrPolling.set(true);
|
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.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({
|
.subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
const st = res?.status ?? '';
|
const st = res?.status ?? '';
|
||||||
@@ -221,9 +245,9 @@ export class CreatePage {
|
|||||||
error: () => {
|
error: () => {
|
||||||
this.closeQr();
|
this.closeQr();
|
||||||
this.error.set('оплата не прошла');
|
this.error.set('оплата не прошла');
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}, 5000);
|
}, CreatePage.POLL_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopPolling(): void {
|
private stopPolling(): void {
|
||||||
@@ -268,7 +292,7 @@ export class CreatePage {
|
|||||||
.post(`https://fastcheck.store/api/fastcheck/settings/${encodeURIComponent(id)}`, paidQr)
|
.post(`https://fastcheck.store/api/fastcheck/settings/${encodeURIComponent(id)}`, paidQr)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => this.redirectToSource(id),
|
next: () => this.redirectToSource(id),
|
||||||
error: () => this.redirectToSource(id)
|
error: () => this.redirectToSource(id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user