From f2618001597c44e0b1b0427308c389ca76545455 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Thu, 23 Jul 2026 00:26:35 +0400 Subject: [PATCH] feat(payment): add qrDescription/customerID fields, TTL-based QR polling - 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 --- docs/backend/BACKEND-INTEGRATION.md | 4 ++-- src/app/config/constants.ts | 2 +- src/app/pages/cart/cart.component.ts | 34 ++++++++++++++++++++++++---- src/app/services/api.service.ts | 3 +++ 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/docs/backend/BACKEND-INTEGRATION.md b/docs/backend/BACKEND-INTEGRATION.md index b96142a..605be16 100644 --- a/docs/backend/BACKEND-INTEGRATION.md +++ b/docs/backend/BACKEND-INTEGRATION.md @@ -1153,8 +1153,8 @@ Payment/QR types (`src/app/services/api.service.ts` lines 10–66, frozen — § ```ts interface QrCreateRequest { qrtype: 'QRDynamic'; amount: number; currency: 'RUB'; partnerqrID?: string; qrDescription?: string; Userid?: string; Reference?: string; RedirectUrl?: string; } -interface QrCreateResponse { qrId?: string; qrID?: string; nspkID?: string; nspkId?: string; nspkurl?: string; orderID?: string; url?: string; bankUrl?: string; status?: string; qrStatus?: string; qrExpirationDate?: string; payload?: string; Payload?: string; qrUrl?: string; partnerqrID?: string|number; partnerID?: string|number; partnerId?: string|number; PartnerID?: string|number; } -interface CartPaymentRequest { amount: number; currency: 'RUB'; siteuserID: string; siteorderID: string; redirectUrl: string; telegramUsername: string; paymentMethod: 'qr'|'card'; items: Array<{itemID: number; price: number; name: string; quantity?: number; delivery?: DeliveryOption[]}>; } +interface QrCreateResponse { qrId?: string; qrID?: string; nspkID?: string; nspkId?: string; nspkurl?: string; orderID?: string; url?: string; bankUrl?: string; status?: string; qrStatus?: string; qrExpirationDate?: string; qrTTL?: number; payload?: string; Payload?: string; qrUrl?: string; partnerqrID?: string|number; partnerID?: string|number; partnerId?: string|number; PartnerID?: string|number; } +interface CartPaymentRequest { amount: number; currency: 'RUB'; siteuserID: string; siteorderID: string; redirectUrl: string; telegramUsername: string; paymentMethod: 'qr'|'card'; qrDescription?: string; customerID?: string; items: Array<{itemID: number; price: number; name: string; quantity?: number; delivery?: DeliveryOption[]}>; } interface QrDynamicStatusResponse { additionalInfo: string; paymentPurpose: string; amount: number; code: string; createDate: string; currency: string; order: string; status: string; qrId: string; transactionDate: string; transactionId: number; qrExpirationDate: string; } ``` diff --git a/src/app/config/constants.ts b/src/app/config/constants.ts index 2359bd9..664d151 100644 --- a/src/app/config/constants.ts +++ b/src/app/config/constants.ts @@ -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; diff --git a/src/app/pages/cart/cart.component.ts b/src/app/pages/cart/cart.component.ts index a9f971b..48b2efb 100644 --- a/src/app/pages/cart/cart.component.ts +++ b/src/app/pages/cart/cart.component.ts @@ -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(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; + 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); diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 842e102..30b1b01 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -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[] }>; }