diff --git a/src/app/pages/cart/cart.component.ts b/src/app/pages/cart/cart.component.ts index 470a9ce..66cda26 100644 --- a/src/app/pages/cart/cart.component.ts +++ b/src/app/pages/cart/cart.component.ts @@ -5,7 +5,7 @@ import { FormsModule } from '@angular/forms'; import { CartService, ApiService, LanguageService, AuthService } from '../../services'; import { Item, CartItem } from '../../models'; import { interval, of, Subscription } from 'rxjs'; -import { catchError, exhaustMap, switchMap, take, timeout } from 'rxjs/operators'; +import { catchError, exhaustMap, take, timeout } from 'rxjs/operators'; import { EmptyCartIconComponent } from '../../components/empty-cart-icon/empty-cart-icon.component'; import { TelegramLoginComponent } from '../../components/telegram-login/telegram-login.component'; import { environment } from '../../../environments/environment'; @@ -13,7 +13,7 @@ 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, PAYMENT_ERROR_CLOSE_MS, LINK_COPIED_DURATION_MS } from '../../config/constants'; +import { PAYMENT_POLL_INTERVAL_MS, PAYMENT_MAX_CHECKS, PAYMENT_TIMEOUT_CLOSE_MS, LINK_COPIED_DURATION_MS } from '../../config/constants'; @Component({ selector: 'app-cart', @@ -58,7 +58,6 @@ export class CartComponent implements OnDestroy { maxChecks = PAYMENT_MAX_CHECKS; private pollingSubscription?: Subscription; private closeTimeout?: ReturnType; - private qrPartnerId = ''; constructor( private cartService: CartService, @@ -155,7 +154,6 @@ export class CartComponent implements OnDestroy { openPaymentPopup(): void { this.showPaymentPopup.set(true); this.paymentStatus.set('creating'); - this.qrPartnerId = ''; this.paymentId.set(''); this.qrCodeUrl.set(''); this.paymentUrl.set(''); @@ -187,7 +185,6 @@ export class CartComponent implements OnDestroy { } this.paymentStatus.set('creating'); - this.qrPartnerId = ''; this.paymentId.set(''); this.qrCodeUrl.set(''); this.paymentUrl.set(''); @@ -196,40 +193,18 @@ export class CartComponent implements OnDestroy { } createPayment(): void { - const sessionId = this.authService.session()?.sessionId || ''; - const partnerQrId = this.getPartnerQrId(); - - const cartItems = this.items().map((item: CartItem) => ({ - itemID: item.itemID, - quantity: item.quantity, - colour: item.colour || '', - size: item.size || '', - price: item.discount > 0 - ? item.price * (1 - item.discount / 100) - : item.price, - })); - - const syncCart$ = sessionId - ? this.apiService.addToCart(sessionId, cartItems).pipe( - catchError((err) => { - console.error('Error syncing cart:', err); - return of(null); - }) - ) - : of(null); - - const qrPayload = { - qrtype: 'QRDynamic' as const, + const orderId = this.generateOrderId(); + const paymentPayload = { amount: Number(this.totalPrice()), currency: 'RUB' as const, - ...(partnerQrId && { partnerqrID: partnerQrId }), - qrDescription: this.buildQrDescription(this.generateOrderId()), + siteuserID: this.getPaymentUserId(), + siteorderID: orderId, + redirectUrl: '', + telegramUsername: this.getTelegramUsername(), + items: this.buildPaymentItems(), }; - syncCart$ - .pipe( - switchMap(() => this.apiService.createSbpPayment(qrPayload)) - ) + this.apiService.createCartPayment(paymentPayload) .subscribe({ next: (response) => { const qrId = this.apiService.resolvePaymentQrId(response); @@ -242,7 +217,6 @@ export class CartComponent implements OnDestroy { return; } - this.qrPartnerId = partnerQrId; this.paymentId.set(qrId); this.qrCodeUrl.set(qrUrl); this.paymentUrl.set(paymentLink); @@ -268,7 +242,7 @@ export class CartComponent implements OnDestroy { .pipe( take(this.maxChecks), // maximum 36 checks (3 minutes) exhaustMap(() => { - return this.apiService.checkSbpPaymentStatus(this.paymentId()).pipe( + return this.apiService.checkCartPaymentStatus(this.paymentId()).pipe( timeout(8000), catchError((err) => { console.error('Error checking payment status:', err); @@ -283,7 +257,8 @@ export class CartComponent implements OnDestroy { return; } - const paymentStatus = (response.paymentStatus || response.status || response.code || '').toUpperCase(); + const paymentStatus = response.paymentStatus?.toUpperCase() || ''; + const paymentCode = response.code?.toUpperCase() || ''; if (paymentStatus === 'FAILED' || paymentStatus === 'EXPIRED' || paymentStatus === 'CANCELLED' || paymentStatus === 'REJECTED') { this.paymentStatus.set('timeout'); @@ -296,7 +271,7 @@ export class CartComponent implements OnDestroy { } // Check if payment is successful - if (paymentStatus === 'COMPLETED' || paymentStatus === 'APPROVED' || paymentStatus === 'PAID') { + if (paymentStatus === 'COMPLETED' || paymentStatus === 'APPROVED' || paymentStatus === 'PAID' || paymentCode === 'SUCCESS') { this.paymentStatus.set('success'); this.stopPolling(); // Clear cart but don't close popup - wait for email submission @@ -329,7 +304,6 @@ export class CartComponent implements OnDestroy { private setPaymentError(): void { this.paymentStatus.set('error'); this.stopPolling(); - this.qrPartnerId = ''; if (this.closeTimeout) { clearTimeout(this.closeTimeout); this.closeTimeout = undefined; @@ -433,36 +407,27 @@ export class CartComponent implements OnDestroy { return this.getTelegramUserId() ?? `web_${Date.now()}`; } - private getPartnerQrId(): string { - const fromQuery = this.getUrlParam('id'); - if (fromQuery) return fromQuery; - const envValue = (environment as any)['partnerqrID']; - return typeof envValue === 'string' ? envValue : ''; - } - - private getUrlParam(name: string): string | null { - if (typeof window === 'undefined') return null; - return new URLSearchParams(window.location.search).get(name); - } - private generateOrderId(): string { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return `order_${timestamp}_${random}`; } - private buildQrDescription(orderId: string): string { - const items = this.items() - .map((item: CartItem) => { - const name = this.itemName(item).trim() || `Item ${item.itemID}`; - const details = [item.colour, item.size].filter(Boolean).join(', '); - const label = details ? `${name} (${details})` : name; - return `${item.quantity} x ${label}`; - }) - .join('; '); + private buildPaymentItems(): Array<{ itemID: number; price: number; name: string }> { + return this.items().map((item: CartItem) => { + const unitPrice = item.discount > 0 + ? item.price * (1 - item.discount / 100) + : item.price; + const details = [item.colour, item.size].filter(Boolean).join(', '); + const translatedName = this.itemName(item).trim() || `Item ${item.itemID}`; + const name = details ? `${item.quantity} x ${translatedName} (${details})` : `${item.quantity} x ${translatedName}`; - const description = `Order ${orderId}; items: ${items}; total: ${this.totalPrice().toFixed(2)} RUB`; - return description.length > 500 ? `${description.slice(0, 497)}...` : description; + return { + itemID: item.itemID, + price: unitPrice * item.quantity, + name, + }; + }); } onPhoneInput(event: Event): void { diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 7732f69..82e15ac 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -34,6 +34,16 @@ export interface QrCreateResponse { PartnerID?: string | number; } +export interface CartPaymentRequest { + amount: number; + currency: 'RUB'; + siteuserID: string; + siteorderID: string; + redirectUrl: string; + telegramUsername: string; + items: Array<{ itemID: number; price: number; name: string }>; +} + export interface QrDynamicStatusResponse { additionalInfo: string; paymentPurpose: string; @@ -49,19 +59,13 @@ export interface QrDynamicStatusResponse { qrExpirationDate: string; } -export interface QrPaymentStatusResponse { - status?: string; - paymentStatus?: string; - code?: string; -} - @Injectable({ providedIn: 'root' }) export class ApiService { private readonly baseUrl = environment.apiUrl; private readonly qrBaseUrl = (environment as any).qrApiUrl as string; - private readonly sbpQrUrl = 'https://qr.vitanova.network/api/qr'; + private readonly cartPaymentPartnerId = 'web-97ec-9c57-4dde-9037-3a68f7f83750'; private readonly retryConfig = { count: 2, @@ -411,13 +415,14 @@ export class ApiService { return this.http.post(`${this.qrBaseUrl}/qr`, payload, { headers: httpHeaders }); } - createSbpPayment(payload: QrCreateRequest): Observable { - return this.http.post(this.sbpQrUrl, payload); + createCartPayment(payload: CartPaymentRequest): Observable { + return this.http.post(`${this.baseUrl}/cart`, payload); } - checkSbpPaymentStatus(paymentId: string): Observable { - const params = new HttpParams().set('id', paymentId); - return this.http.get(this.sbpQrUrl, { params }); + checkCartPaymentStatus(qrId: string): Observable { + return this.http.get( + `${this.qrBaseUrl}/qr/dynamic/${this.cartPaymentPartnerId}/${encodeURIComponent(qrId)}` + ); } checkPaymentStatus(partnerQrId: string, qrId: string): Observable {