change
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user