api changes
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user