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
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; }
```

View File

@@ -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;

View File

@@ -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);

View File

@@ -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[] }>;
}