feat(payment): add qrDescription/customerID fields, TTL-based QR polling
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
- CartPaymentRequest gains qrDescription (brandName > hostname > fallback text) and customerID (telegram id) - QrCreateResponse gains qrTTL; polling window now derived from it (min 60s) instead of a fixed 3-minute/36-check cap - PAYMENT_MAX_CHECKS replaced by PAYMENT_MIN_POLL_SECONDS
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// Payment polling
|
||||
export const PAYMENT_POLL_INTERVAL_MS = 5000;
|
||||
export const PAYMENT_MAX_CHECKS = 36;
|
||||
export const PAYMENT_MIN_POLL_SECONDS = 60;
|
||||
export const PAYMENT_TIMEOUT_CLOSE_MS = 3000;
|
||||
export const LINK_COPIED_DURATION_MS = 2000;
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTran
|
||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
import { TranslateService } from '../../i18n/translate.service';
|
||||
import { PAYMENT_POLL_INTERVAL_MS, PAYMENT_MAX_CHECKS, PAYMENT_TIMEOUT_CLOSE_MS, LINK_COPIED_DURATION_MS } from '../../config/constants';
|
||||
import { PAYMENT_POLL_INTERVAL_MS, PAYMENT_MIN_POLL_SECONDS, PAYMENT_TIMEOUT_CLOSE_MS, LINK_COPIED_DURATION_MS } from '../../config/constants';
|
||||
import { IconComponent } from '../../shared/ui/icon/icon.component';
|
||||
import { ConfigService } from '../../core/config/config.service';
|
||||
import { TenantResolverService } from '../../core/config/tenant-resolver.service';
|
||||
|
||||
type PaymentMethod = 'qr' | 'card';
|
||||
|
||||
@@ -65,10 +67,13 @@ export class CartComponent implements OnDestroy {
|
||||
emailSubmitting = signal<boolean>(false);
|
||||
paidItems: CartItem[] = [];
|
||||
|
||||
maxChecks = PAYMENT_MAX_CHECKS;
|
||||
maxChecks = Math.ceil(PAYMENT_MIN_POLL_SECONDS / (PAYMENT_POLL_INTERVAL_MS / 1000));
|
||||
private pollingSubscription?: Subscription;
|
||||
private closeTimeout?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private configService = inject(ConfigService);
|
||||
private tenantResolver = inject(TenantResolverService);
|
||||
|
||||
constructor(
|
||||
private cartService: CartService,
|
||||
private apiService: ApiService,
|
||||
@@ -243,6 +248,8 @@ export class CartComponent implements OnDestroy {
|
||||
redirectUrl: '',
|
||||
telegramUsername: this.getTelegramUsername(),
|
||||
paymentMethod,
|
||||
qrDescription: this.getPaymentDescription(),
|
||||
customerID: this.getTelegramUserId() ?? undefined,
|
||||
items: this.buildPaymentItems(),
|
||||
};
|
||||
|
||||
@@ -266,7 +273,7 @@ export class CartComponent implements OnDestroy {
|
||||
this.bankPaymentUrl.set(bankUrl);
|
||||
|
||||
this.paymentStatus.set('waiting');
|
||||
this.startPolling();
|
||||
this.startPolling(response.qrTTL);
|
||||
if (paymentMethod === 'card') {
|
||||
this.openBankPaymentPopup();
|
||||
}
|
||||
@@ -278,16 +285,19 @@ export class CartComponent implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
startPolling(): void {
|
||||
startPolling(qrTTL?: number): void {
|
||||
this.stopPolling();
|
||||
if (!this.paymentId()) {
|
||||
this.setPaymentError();
|
||||
return;
|
||||
}
|
||||
|
||||
const pollSeconds = Math.max(PAYMENT_MIN_POLL_SECONDS, (qrTTL ?? 0) * 60);
|
||||
this.maxChecks = Math.ceil(pollSeconds / (PAYMENT_POLL_INTERVAL_MS / 1000));
|
||||
|
||||
this.pollingSubscription = interval(PAYMENT_POLL_INTERVAL_MS)
|
||||
.pipe(
|
||||
take(this.maxChecks), // maximum 36 checks (3 minutes)
|
||||
take(this.maxChecks), // qrTTL minutes from create response, minimum 1 minute
|
||||
exhaustMap(() => {
|
||||
const statusRequest = this.selectedPaymentMethod() === 'card'
|
||||
? this.apiService.checkCartCardPaymentStatus(this.paymentId())
|
||||
@@ -568,6 +578,20 @@ export class CartComponent implements OnDestroy {
|
||||
return this.getTelegramUserId() ?? `web_${Date.now()}`;
|
||||
}
|
||||
|
||||
private getPaymentDescription(): string {
|
||||
const brandName = this.configService.getBootstrapSnapshot()?.branding?.brandName?.trim();
|
||||
if (brandName) {
|
||||
return brandName;
|
||||
}
|
||||
|
||||
const hostname = this.tenantResolver.getHostname();
|
||||
if (hostname && !this.tenantResolver.isLocalhost()) {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
return 'Покупка на Маркетплейсе';
|
||||
}
|
||||
|
||||
private generateOrderId(): string {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface QrCreateResponse {
|
||||
status?: string;
|
||||
qrStatus?: string;
|
||||
qrExpirationDate?: string;
|
||||
qrTTL?: number;
|
||||
payload?: string;
|
||||
Payload?: string;
|
||||
qrUrl?: string;
|
||||
@@ -47,6 +48,8 @@ export interface CartPaymentRequest {
|
||||
redirectUrl: string;
|
||||
telegramUsername: string;
|
||||
paymentMethod: 'qr' | 'card';
|
||||
qrDescription?: string;
|
||||
customerID?: string;
|
||||
items: Array<{ itemID: number; price: number; name: string; quantity?: number; delivery?: DeliveryOption[] }>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user