api changes

This commit is contained in:
sdarbinyan
2026-06-06 22:38:01 +04:00
parent 9b5c2dd95c
commit 80cc90d347
2 changed files with 45 additions and 75 deletions

View File

@@ -5,7 +5,7 @@ import { FormsModule } from '@angular/forms';
import { CartService, ApiService, LanguageService, AuthService } from '../../services'; import { CartService, ApiService, LanguageService, AuthService } from '../../services';
import { Item, CartItem } from '../../models'; import { Item, CartItem } from '../../models';
import { interval, of, Subscription } from 'rxjs'; 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 { EmptyCartIconComponent } from '../../components/empty-cart-icon/empty-cart-icon.component';
import { TelegramLoginComponent } from '../../components/telegram-login/telegram-login.component'; import { TelegramLoginComponent } from '../../components/telegram-login/telegram-login.component';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
@@ -13,7 +13,7 @@ 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, 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({ @Component({
selector: 'app-cart', selector: 'app-cart',
@@ -58,7 +58,6 @@ export class CartComponent implements OnDestroy {
maxChecks = PAYMENT_MAX_CHECKS; maxChecks = PAYMENT_MAX_CHECKS;
private pollingSubscription?: Subscription; private pollingSubscription?: Subscription;
private closeTimeout?: ReturnType<typeof setTimeout>; private closeTimeout?: ReturnType<typeof setTimeout>;
private qrPartnerId = '';
constructor( constructor(
private cartService: CartService, private cartService: CartService,
@@ -155,7 +154,6 @@ export class CartComponent implements OnDestroy {
openPaymentPopup(): void { openPaymentPopup(): void {
this.showPaymentPopup.set(true); this.showPaymentPopup.set(true);
this.paymentStatus.set('creating'); this.paymentStatus.set('creating');
this.qrPartnerId = '';
this.paymentId.set(''); this.paymentId.set('');
this.qrCodeUrl.set(''); this.qrCodeUrl.set('');
this.paymentUrl.set(''); this.paymentUrl.set('');
@@ -187,7 +185,6 @@ export class CartComponent implements OnDestroy {
} }
this.paymentStatus.set('creating'); this.paymentStatus.set('creating');
this.qrPartnerId = '';
this.paymentId.set(''); this.paymentId.set('');
this.qrCodeUrl.set(''); this.qrCodeUrl.set('');
this.paymentUrl.set(''); this.paymentUrl.set('');
@@ -196,40 +193,18 @@ export class CartComponent implements OnDestroy {
} }
createPayment(): void { createPayment(): void {
const sessionId = this.authService.session()?.sessionId || ''; const orderId = this.generateOrderId();
const partnerQrId = this.getPartnerQrId(); const paymentPayload = {
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,
amount: Number(this.totalPrice()), amount: Number(this.totalPrice()),
currency: 'RUB' as const, currency: 'RUB' as const,
...(partnerQrId && { partnerqrID: partnerQrId }), siteuserID: this.getPaymentUserId(),
qrDescription: this.buildQrDescription(this.generateOrderId()), siteorderID: orderId,
redirectUrl: '',
telegramUsername: this.getTelegramUsername(),
items: this.buildPaymentItems(),
}; };
syncCart$ this.apiService.createCartPayment(paymentPayload)
.pipe(
switchMap(() => this.apiService.createSbpPayment(qrPayload))
)
.subscribe({ .subscribe({
next: (response) => { next: (response) => {
const qrId = this.apiService.resolvePaymentQrId(response); const qrId = this.apiService.resolvePaymentQrId(response);
@@ -242,7 +217,6 @@ export class CartComponent implements OnDestroy {
return; return;
} }
this.qrPartnerId = partnerQrId;
this.paymentId.set(qrId); this.paymentId.set(qrId);
this.qrCodeUrl.set(qrUrl); this.qrCodeUrl.set(qrUrl);
this.paymentUrl.set(paymentLink); this.paymentUrl.set(paymentLink);
@@ -268,7 +242,7 @@ export class CartComponent implements OnDestroy {
.pipe( .pipe(
take(this.maxChecks), // maximum 36 checks (3 minutes) take(this.maxChecks), // maximum 36 checks (3 minutes)
exhaustMap(() => { exhaustMap(() => {
return this.apiService.checkSbpPaymentStatus(this.paymentId()).pipe( return this.apiService.checkCartPaymentStatus(this.paymentId()).pipe(
timeout(8000), timeout(8000),
catchError((err) => { catchError((err) => {
console.error('Error checking payment status:', err); console.error('Error checking payment status:', err);
@@ -283,7 +257,8 @@ export class CartComponent implements OnDestroy {
return; 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') { if (paymentStatus === 'FAILED' || paymentStatus === 'EXPIRED' || paymentStatus === 'CANCELLED' || paymentStatus === 'REJECTED') {
this.paymentStatus.set('timeout'); this.paymentStatus.set('timeout');
@@ -296,7 +271,7 @@ export class CartComponent implements OnDestroy {
} }
// Check if payment is successful // 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.paymentStatus.set('success');
this.stopPolling(); this.stopPolling();
// Clear cart but don't close popup - wait for email submission // Clear cart but don't close popup - wait for email submission
@@ -329,7 +304,6 @@ export class CartComponent implements OnDestroy {
private setPaymentError(): void { private setPaymentError(): void {
this.paymentStatus.set('error'); this.paymentStatus.set('error');
this.stopPolling(); this.stopPolling();
this.qrPartnerId = '';
if (this.closeTimeout) { if (this.closeTimeout) {
clearTimeout(this.closeTimeout); clearTimeout(this.closeTimeout);
this.closeTimeout = undefined; this.closeTimeout = undefined;
@@ -433,36 +407,27 @@ export class CartComponent implements OnDestroy {
return this.getTelegramUserId() ?? `web_${Date.now()}`; 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 { 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);
return `order_${timestamp}_${random}`; return `order_${timestamp}_${random}`;
} }
private buildQrDescription(orderId: string): string { private buildPaymentItems(): Array<{ itemID: number; price: number; name: string }> {
const items = this.items() return this.items().map((item: CartItem) => {
.map((item: CartItem) => { const unitPrice = item.discount > 0
const name = this.itemName(item).trim() || `Item ${item.itemID}`; ? item.price * (1 - item.discount / 100)
: item.price;
const details = [item.colour, item.size].filter(Boolean).join(', '); const details = [item.colour, item.size].filter(Boolean).join(', ');
const label = details ? `${name} (${details})` : name; const translatedName = this.itemName(item).trim() || `Item ${item.itemID}`;
return `${item.quantity} x ${label}`; const name = details ? `${item.quantity} x ${translatedName} (${details})` : `${item.quantity} x ${translatedName}`;
})
.join('; ');
const description = `Order ${orderId}; items: ${items}; total: ${this.totalPrice().toFixed(2)} RUB`; return {
return description.length > 500 ? `${description.slice(0, 497)}...` : description; itemID: item.itemID,
price: unitPrice * item.quantity,
name,
};
});
} }
onPhoneInput(event: Event): void { onPhoneInput(event: Event): void {

View File

@@ -34,6 +34,16 @@ export interface QrCreateResponse {
PartnerID?: string | number; 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 { export interface QrDynamicStatusResponse {
additionalInfo: string; additionalInfo: string;
paymentPurpose: string; paymentPurpose: string;
@@ -49,19 +59,13 @@ export interface QrDynamicStatusResponse {
qrExpirationDate: string; qrExpirationDate: string;
} }
export interface QrPaymentStatusResponse {
status?: string;
paymentStatus?: string;
code?: string;
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ApiService { export class ApiService {
private readonly baseUrl = environment.apiUrl; private readonly baseUrl = environment.apiUrl;
private readonly qrBaseUrl = (environment as any).qrApiUrl as string; 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 = { private readonly retryConfig = {
count: 2, count: 2,
@@ -411,13 +415,14 @@ export class ApiService {
return this.http.post<QrCreateResponse>(`${this.qrBaseUrl}/qr`, payload, { headers: httpHeaders }); return this.http.post<QrCreateResponse>(`${this.qrBaseUrl}/qr`, payload, { headers: httpHeaders });
} }
createSbpPayment(payload: QrCreateRequest): Observable<QrCreateResponse> { createCartPayment(payload: CartPaymentRequest): Observable<QrCreateResponse> {
return this.http.post<QrCreateResponse>(this.sbpQrUrl, payload); return this.http.post<QrCreateResponse>(`${this.baseUrl}/cart`, payload);
} }
checkSbpPaymentStatus(paymentId: string): Observable<QrPaymentStatusResponse> { checkCartPaymentStatus(qrId: string): Observable<QrDynamicStatusResponse> {
const params = new HttpParams().set('id', paymentId); return this.http.get<QrDynamicStatusResponse>(
return this.http.get<QrPaymentStatusResponse>(this.sbpQrUrl, { params }); `${this.qrBaseUrl}/qr/dynamic/${this.cartPaymentPartnerId}/${encodeURIComponent(qrId)}`
);
} }
checkPaymentStatus(partnerQrId: string, qrId: string): Observable<QrDynamicStatusResponse> { checkPaymentStatus(partnerQrId: string, qrId: string): Observable<QrDynamicStatusResponse> {