feat(payment): add qrDescription/customerID fields, TTL-based QR polling
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:
sdarbinyan
2026-07-23 00:26:35 +04:00
parent 89ae50e9a4
commit f261800159
4 changed files with 35 additions and 8 deletions

View File

@@ -1153,8 +1153,8 @@ Payment/QR types (`src/app/services/api.service.ts` lines 1066, frozen — §
```ts ```ts
interface QrCreateRequest { qrtype: 'QRDynamic'; amount: number; currency: 'RUB'; partnerqrID?: string; qrDescription?: string; Userid?: string; Reference?: string; RedirectUrl?: string; } 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 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'; items: Array<{itemID: number; price: number; name: string; quantity?: number; delivery?: DeliveryOption[]}>; } 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; } 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; }
``` ```

View File

@@ -1,6 +1,6 @@
// Payment polling // Payment polling
export const PAYMENT_POLL_INTERVAL_MS = 5000; 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 PAYMENT_TIMEOUT_CLOSE_MS = 3000;
export const LINK_COPIED_DURATION_MS = 2000; export const LINK_COPIED_DURATION_MS = 2000;

View File

@@ -13,8 +13,10 @@ import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTran
import { LangRoutePipe } from '../../pipes/lang-route.pipe'; import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service'; 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 { 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'; type PaymentMethod = 'qr' | 'card';
@@ -65,10 +67,13 @@ export class CartComponent implements OnDestroy {
emailSubmitting = signal<boolean>(false); emailSubmitting = signal<boolean>(false);
paidItems: CartItem[] = []; paidItems: CartItem[] = [];
maxChecks = PAYMENT_MAX_CHECKS; maxChecks = Math.ceil(PAYMENT_MIN_POLL_SECONDS / (PAYMENT_POLL_INTERVAL_MS / 1000));
private pollingSubscription?: Subscription; private pollingSubscription?: Subscription;
private closeTimeout?: ReturnType<typeof setTimeout>; private closeTimeout?: ReturnType<typeof setTimeout>;
private configService = inject(ConfigService);
private tenantResolver = inject(TenantResolverService);
constructor( constructor(
private cartService: CartService, private cartService: CartService,
private apiService: ApiService, private apiService: ApiService,
@@ -243,6 +248,8 @@ export class CartComponent implements OnDestroy {
redirectUrl: '', redirectUrl: '',
telegramUsername: this.getTelegramUsername(), telegramUsername: this.getTelegramUsername(),
paymentMethod, paymentMethod,
qrDescription: this.getPaymentDescription(),
customerID: this.getTelegramUserId() ?? undefined,
items: this.buildPaymentItems(), items: this.buildPaymentItems(),
}; };
@@ -266,7 +273,7 @@ export class CartComponent implements OnDestroy {
this.bankPaymentUrl.set(bankUrl); this.bankPaymentUrl.set(bankUrl);
this.paymentStatus.set('waiting'); this.paymentStatus.set('waiting');
this.startPolling(); this.startPolling(response.qrTTL);
if (paymentMethod === 'card') { if (paymentMethod === 'card') {
this.openBankPaymentPopup(); this.openBankPaymentPopup();
} }
@@ -278,16 +285,19 @@ export class CartComponent implements OnDestroy {
}); });
} }
startPolling(): void { startPolling(qrTTL?: number): void {
this.stopPolling(); this.stopPolling();
if (!this.paymentId()) { if (!this.paymentId()) {
this.setPaymentError(); this.setPaymentError();
return; 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) this.pollingSubscription = interval(PAYMENT_POLL_INTERVAL_MS)
.pipe( .pipe(
take(this.maxChecks), // maximum 36 checks (3 minutes) take(this.maxChecks), // qrTTL minutes from create response, minimum 1 minute
exhaustMap(() => { exhaustMap(() => {
const statusRequest = this.selectedPaymentMethod() === 'card' const statusRequest = this.selectedPaymentMethod() === 'card'
? this.apiService.checkCartCardPaymentStatus(this.paymentId()) ? this.apiService.checkCartCardPaymentStatus(this.paymentId())
@@ -568,6 +578,20 @@ export class CartComponent implements OnDestroy {
return this.getTelegramUserId() ?? `web_${Date.now()}`; 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 { private generateOrderId(): string {
const timestamp = Date.now(); const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8); const random = Math.random().toString(36).substring(2, 8);

View File

@@ -30,6 +30,7 @@ export interface QrCreateResponse {
status?: string; status?: string;
qrStatus?: string; qrStatus?: string;
qrExpirationDate?: string; qrExpirationDate?: string;
qrTTL?: number;
payload?: string; payload?: string;
Payload?: string; Payload?: string;
qrUrl?: string; qrUrl?: string;
@@ -47,6 +48,8 @@ export interface CartPaymentRequest {
redirectUrl: string; redirectUrl: string;
telegramUsername: string; telegramUsername: string;
paymentMethod: 'qr' | 'card'; paymentMethod: 'qr' | 'card';
qrDescription?: string;
customerID?: string;
items: Array<{ itemID: number; price: number; name: string; quantity?: number; delivery?: DeliveryOption[] }>; items: Array<{ itemID: number; price: number; name: string; quantity?: number; delivery?: DeliveryOption[] }>;
} }