This commit is contained in:
sdarbinyan
2026-06-02 01:46:12 +04:00
parent 63b0e18396
commit c6bc05560e
3 changed files with 128 additions and 113 deletions

View File

@@ -800,6 +800,14 @@ export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
}, 300); }, 300);
} }
// ── GET /qr/settings
if (url.endsWith('/qr/settings') && req.method === 'GET') {
return respond({
minAmount: 30,
maxAmount: 200000,
});
}
// ── POST /qr (create payment QR directly) // ── POST /qr (create payment QR directly)
if (url.endsWith('/qr') && req.method === 'POST') { if (url.endsWith('/qr') && req.method === 'POST') {
return respond({ return respond({
@@ -858,6 +866,26 @@ export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
}, 500); }, 500);
} }
// ── GET /qr/dynamic/:partnerID/:qrID (dynamic QR status)
if (url.match(/\/qr\/dynamic\/[^/]+\/[^/]+$/) && req.method === 'GET') {
return respond({
status: 'APPROVED',
paymentStatus: 'COMPLETED',
code: 'SUCCESS',
amount: 0,
currency: 'RUB',
qrId: 'mock',
transactionId: 999,
transactionDate: new Date().toISOString(),
additionalInfo: '',
paymentPurpose: '',
createDate: new Date().toISOString(),
order: 'mock-order',
qrExpirationDate: new Date().toISOString(),
phoneNumber: '+70000000000'
}, 500);
}
// ── GET /qr/payment/:qrId (legacy/direct payment status) // ── GET /qr/payment/:qrId (legacy/direct payment status)
if (url.match(/\/qr\/payment\/[^/]+$/) && req.method === 'GET') { if (url.match(/\/qr\/payment\/[^/]+$/) && req.method === 'GET') {
return respond({ return respond({

View File

@@ -58,7 +58,7 @@ 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 paymentBackend: 'websession' | 'qr' | 'cart' | null = null; private qrPartnerId = '';
constructor( constructor(
private cartService: CartService, private cartService: CartService,
@@ -160,7 +160,7 @@ 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.paymentBackend = null; this.qrPartnerId = '';
this.paymentId.set(''); this.paymentId.set('');
this.qrCodeUrl.set(''); this.qrCodeUrl.set('');
this.paymentUrl.set(''); this.paymentUrl.set('');
@@ -192,7 +192,7 @@ export class CartComponent implements OnDestroy {
} }
this.paymentStatus.set('creating'); this.paymentStatus.set('creating');
this.paymentBackend = null; this.qrPartnerId = '';
this.paymentId.set(''); this.paymentId.set('');
this.qrCodeUrl.set(''); this.qrCodeUrl.set('');
this.paymentUrl.set(''); this.paymentUrl.set('');
@@ -202,6 +202,12 @@ export class CartComponent implements OnDestroy {
createPayment(): void { createPayment(): void {
const sessionId = this.authService.session()?.sessionId || ''; const sessionId = this.authService.session()?.sessionId || '';
const partnerQrId = this.getPartnerQrId();
if (!partnerQrId) {
this.setPaymentError();
return;
}
const cartItems = this.items().map((item: CartItem) => ({ const cartItems = this.items().map((item: CartItem) => ({
itemID: item.itemID, itemID: item.itemID,
quantity: item.quantity, quantity: item.quantity,
@@ -212,23 +218,6 @@ export class CartComponent implements OnDestroy {
: item.price, : item.price,
})); }));
const paymentData = {
amount: this.totalPrice(),
currency: this.currentCurrency,
siteuserID: this.getPaymentUserId(),
siteorderID: this.generateOrderId(),
redirectUrl: '',
telegramUsername: this.getTelegramUsername(),
items: this.items().map((item: CartItem) => ({
itemID: item.itemID,
price: item.discount > 0
? item.price * (1 - item.discount / 100)
: item.price,
name: item.name,
quantity: item.quantity,
}))
};
const syncCart$ = sessionId const syncCart$ = sessionId
? this.apiService.addToCart(sessionId, cartItems).pipe( ? this.apiService.addToCart(sessionId, cartItems).pipe(
catchError((err) => { catchError((err) => {
@@ -238,25 +227,45 @@ export class CartComponent implements OnDestroy {
) )
: of(null); : of(null);
const userIdValue = this.getUrlParam('userid-value') || undefined;
const authorizationKey = this.getUrlParam('authorization-key') || undefined;
const qrPayload = {
qrtype: 'QRDynamic' as const,
amount: Number(this.totalPrice()),
currency: 'RUB' as const,
partnerqrID: partnerQrId,
qrDescription: `Order ${this.generateOrderId()}, total: ${this.totalPrice().toFixed(2)} ${this.currentCurrency}`,
Userid: userIdValue ?? this.getPaymentUserId(),
Reference: this.getUrlParam('ref') || (typeof window !== 'undefined' ? window.location.hostname : ''),
RedirectUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
};
syncCart$ syncCart$
.pipe( .pipe(
switchMap(() => this.apiService.createPayment(paymentData, sessionId || undefined)) switchMap(() => this.apiService.createPayment(qrPayload, { authorizationKey, userIdValue }))
) )
.subscribe({ .subscribe({
next: (response) => { next: (response) => {
const qrId = this.apiService.resolvePaymentQrId(response.response); const qrId = this.apiService.resolvePaymentQrId(response);
const qrUrl = this.apiService.resolvePaymentQrUrl(response.response); const qrUrl = this.apiService.resolvePaymentQrUrl(response);
const paymentLink = this.apiService.resolvePaymentLink(response);
if (!qrId || !qrUrl) { if (!qrId || !qrUrl) {
console.error('Payment response missing qr fields:', response.response); console.error('Payment response missing qr fields:', response);
this.setPaymentError(); this.setPaymentError();
return; return;
} }
this.paymentBackend = response.backend; this.qrPartnerId = partnerQrId;
this.paymentId.set(qrId); this.paymentId.set(qrId);
this.qrCodeUrl.set(qrUrl); this.qrCodeUrl.set(qrUrl);
this.paymentUrl.set(this.apiService.resolvePaymentLink(response.response)); this.paymentUrl.set(paymentLink);
if (paymentLink && typeof window !== 'undefined' && window.innerWidth < 768) {
window.location.href = paymentLink;
return;
}
this.paymentStatus.set('waiting'); this.paymentStatus.set('waiting');
this.startPolling(); this.startPolling();
}, },
@@ -269,15 +278,16 @@ export class CartComponent implements OnDestroy {
startPolling(): void { startPolling(): void {
this.stopPolling(); this.stopPolling();
if (!this.qrPartnerId || !this.paymentId()) {
this.setPaymentError();
return;
}
this.pollingSubscription = interval(PAYMENT_POLL_INTERVAL_MS) this.pollingSubscription = interval(PAYMENT_POLL_INTERVAL_MS)
.pipe( .pipe(
take(this.maxChecks), // maximum 36 checks (3 minutes) take(this.maxChecks), // maximum 36 checks (3 minutes)
switchMap(() => { switchMap(() => {
const sessionId = this.authService.session()?.sessionId || ''; return this.apiService.checkPaymentStatus(this.qrPartnerId, this.paymentId()).pipe(
return this.apiService.checkPaymentStatus(this.paymentId(), {
sessionId: sessionId || undefined,
backend: this.paymentBackend ?? undefined,
}).pipe(
catchError((err) => { catchError((err) => {
console.error('Error checking payment status:', err); console.error('Error checking payment status:', err);
return of(null); return of(null);
@@ -338,7 +348,7 @@ export class CartComponent implements OnDestroy {
private setPaymentError(): void { private setPaymentError(): void {
this.paymentStatus.set('error'); this.paymentStatus.set('error');
this.stopPolling(); this.stopPolling();
this.paymentBackend = null; this.qrPartnerId = '';
if (this.closeTimeout) { if (this.closeTimeout) {
clearTimeout(this.closeTimeout); clearTimeout(this.closeTimeout);
this.closeTimeout = undefined; this.closeTimeout = undefined;
@@ -442,6 +452,24 @@ 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 unknown as Record<string, unknown>)['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);

View File

@@ -1,48 +1,41 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable, timer } from 'rxjs'; import { Observable, timer } from 'rxjs';
import { catchError, map, retry } from 'rxjs/operators'; import { map, retry } from 'rxjs/operators';
import { Category, Item, Subcategory } from '../models'; import { Category, Item, Subcategory } from '../models';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
type PaymentBackend = 'websession' | 'qr' | 'cart'; export interface QrCreateRequest {
qrtype: 'QRDynamic';
interface PaymentRequestItem {
itemID: number;
price: number;
name: string;
quantity?: number;
}
export interface PaymentRequest {
amount: number; amount: number;
currency: string; currency: 'RUB';
siteuserID: string; partnerqrID: string;
siteorderID: string; qrDescription?: string;
redirectUrl: string; Userid?: string;
telegramUsername: string; Reference?: string;
items: PaymentRequestItem[]; RedirectUrl?: string;
} }
export interface PaymentCreateResponse { export interface QrCreateResponse {
qrId?: string; qrId?: string;
qrID?: string; qrID?: string;
nspkID?: string;
nspkId?: string;
nspkurl?: string;
status?: string;
qrStatus?: string; qrStatus?: string;
qrExpirationDate?: string; qrExpirationDate?: string;
payload?: string; payload?: string;
Payload?: string; Payload?: string;
qrUrl?: string; qrUrl?: string;
partnerqrID?: string | number;
partnerID?: string | number; partnerID?: string | number;
partnerId?: string | number; partnerId?: string | number;
PartnerID?: string | number; PartnerID?: string | number;
} }
export interface PaymentCreateResult { export interface QrStatusResponse {
backend: PaymentBackend; status?: string;
response: PaymentCreateResponse;
}
export interface PaymentStatusResponse {
additionalInfo: string; additionalInfo: string;
paymentPurpose: string; paymentPurpose: string;
amount: number; amount: number;
@@ -400,74 +393,40 @@ export class ApiService {
return this.http.post<{ message: string }>(`${this.baseUrl}/items/${itemID}/questiion`, body); return this.http.post<{ message: string }>(`${this.baseUrl}/items/${itemID}/questiion`, body);
} }
createPayment(paymentData: PaymentRequest, sessionId?: string): Observable<PaymentCreateResult> { createPayment(payload: QrCreateRequest, headers?: { authorizationKey?: string; userIdValue?: string }): Observable<QrCreateResponse> {
const directQrPayment$ = this.http.post<PaymentCreateResponse>(`${this.baseUrl}/qr`, paymentData).pipe( let httpHeaders = new HttpHeaders();
map(response => ({ backend: 'qr' as const, response }))
);
const legacyCartPayment$ = this.http.post<PaymentCreateResponse>(`${this.baseUrl}/cart`, paymentData).pipe(
map(response => ({ backend: 'cart' as const, response }))
);
const directPayment$ = directQrPayment$.pipe(
catchError(() => legacyCartPayment$)
);
if (!sessionId) { if (headers?.authorizationKey) {
return directPayment$; httpHeaders = httpHeaders.set('authorization-key', headers.authorizationKey);
}
if (headers?.userIdValue) {
httpHeaders = httpHeaders.set('userid-value', headers.userIdValue);
} }
return this.http.post<PaymentCreateResponse>(`${this.baseUrl}/websession/${sessionId}/qr`, {}).pipe( return this.http.post<QrCreateResponse>(`${this.baseUrl}/qr`, payload, { headers: httpHeaders });
map(response => ({ backend: 'websession' as const, response })), }
catchError(() => directPayment$)
checkPaymentStatus(partnerQrId: string, qrId: string): Observable<QrStatusResponse> {
return this.http.get<QrStatusResponse>(
`${this.baseUrl}/qr/dynamic/${encodeURIComponent(partnerQrId)}/${encodeURIComponent(qrId)}`
); );
} }
checkPaymentStatus(qrId: string, options?: { sessionId?: string; backend?: PaymentBackend }): Observable<PaymentStatusResponse> { resolvePaymentQrId(response: QrCreateResponse): string {
const legacyStatus$ = this.http.get<PaymentStatusResponse>(`${this.baseUrl}/qr/payment/${qrId}`); return response.qrId ?? response.qrID ?? response.nspkID ?? response.nspkId ?? '';
if (options?.backend === 'websession' && options.sessionId) {
return this.http.get<PaymentStatusResponse>(`${this.baseUrl}/websession/${options.sessionId}/${qrId}`).pipe(
catchError(() => legacyStatus$)
);
}
if (options?.backend === 'qr' || options?.backend === 'cart') {
return legacyStatus$;
}
if (options?.sessionId) {
return this.http.get<PaymentStatusResponse>(`${this.baseUrl}/websession/${options.sessionId}/${qrId}`).pipe(
catchError(() => legacyStatus$)
);
}
return legacyStatus$;
} }
resolvePaymentQrId(response: PaymentCreateResponse): string { resolvePaymentLink(response: QrCreateResponse): string {
return response.qrId ?? response.qrID ?? ''; return response.nspkurl ?? response.Payload ?? response.payload ?? response.qrUrl ?? '';
} }
resolvePaymentQrUrl(response: PaymentCreateResponse): string { resolvePaymentQrUrl(response: QrCreateResponse): string {
if (response.qrUrl) { const paymentLink = this.resolvePaymentLink(response);
return response.qrUrl; if (paymentLink) {
return `https://api.qrserver.com/v1/create-qr-code/?size=256x256&margin=8&data=${encodeURIComponent(paymentLink)}`;
} }
const qrId = this.resolvePaymentQrId(response); return response.qrUrl ?? '';
const partnerId = response.partnerID ?? response.partnerId ?? response.PartnerID;
if (!qrId) {
return '';
}
if (partnerId != null) {
return `${this.baseUrl}/qr/dynamic/${encodeURIComponent(String(partnerId))}/${encodeURIComponent(qrId)}`;
}
return `${this.baseUrl}/qr/static/${encodeURIComponent(qrId)}`;
}
resolvePaymentLink(response: PaymentCreateResponse): string {
return response.payload ?? response.Payload ?? this.resolvePaymentQrUrl(response);
} }
submitPurchaseEmail(emailData: { submitPurchaseEmail(emailData: {