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

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