This commit is contained in:
@@ -2,13 +2,17 @@ import { HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { AdminAuthService } from './admin-auth.service';
|
||||
|
||||
/** Backend paths that require an active AdminWebSessionID per API-REFERENCE.md §0. */
|
||||
const ADMIN_GATED_PATH_SEGMENTS = ['/admin/', '/backoffice/', '/builder/', '/media/'];
|
||||
|
||||
/**
|
||||
* Attaches admin session/token headers only to admin API requests. Mirrors
|
||||
* apiHeadersInterceptor's self-guarding pattern but scoped to `/admin` so it
|
||||
* never touches customer requests and never reads AuthService's session.
|
||||
* apiHeadersInterceptor's self-guarding pattern but scoped to admin-gated
|
||||
* paths so it never touches customer requests and never reads AuthService's
|
||||
* session.
|
||||
*/
|
||||
export const adminAuthHeadersInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const isAdminRequest = req.url.includes('/admin/');
|
||||
const isAdminRequest = ADMIN_GATED_PATH_SEGMENTS.some(segment => req.url.includes(segment));
|
||||
if (!isAdminRequest) {
|
||||
return next(req);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from '../models/admin-analytics.model';
|
||||
import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway';
|
||||
import { AdminProductsLocalGateway } from '../../products/services/admin-products-local.gateway';
|
||||
import { AdminCategoriesLocalGateway } from '../../categories/services/admin-categories-local.gateway';
|
||||
import { ADMIN_CATEGORIES_GATEWAY } from '../../categories/services/admin-categories-gateway.token';
|
||||
import { AdminModerationLocalGateway } from '../../moderation/services/admin-moderation-local.gateway';
|
||||
import { AdminDashboardFacade } from '../../dashboard/facade/admin-dashboard.facade';
|
||||
import { AdminOrder } from '../../orders/models/admin-order.model';
|
||||
@@ -32,7 +32,7 @@ import { AdminReview } from '../../moderation/models/admin-review.model';
|
||||
export class AdminAnalyticsFacade {
|
||||
private readonly ordersGateway = inject(AdminOrdersLocalGateway);
|
||||
private readonly productsGateway = inject(AdminProductsLocalGateway);
|
||||
private readonly categoriesGateway = inject(AdminCategoriesLocalGateway);
|
||||
private readonly categoriesGateway = inject(ADMIN_CATEGORIES_GATEWAY);
|
||||
private readonly moderationGateway = inject(AdminModerationLocalGateway);
|
||||
private readonly dashboardFacade = inject(AdminDashboardFacade);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, computed, inject, signal } from '@angular/core';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { AdminCategory, AdminCategoryEditorMode, AdminCategoryListFilters } from '../models/admin-category.model';
|
||||
import { AdminCategoriesFormFactory } from '../services/admin-categories-form.factory';
|
||||
import { AdminCategoriesLocalGateway } from '../services/admin-categories-local.gateway';
|
||||
import { ADMIN_CATEGORIES_GATEWAY } from '../services/admin-categories-gateway.token';
|
||||
import { LocalStorageService } from '../../../../core/storage/local-storage.service';
|
||||
import { ProjectEditorFacade } from '../../../project-editor/facade/project-editor.facade';
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface AdminCategoriesDashboardStats {
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AdminCategoriesFacade {
|
||||
private readonly gateway = inject(AdminCategoriesLocalGateway);
|
||||
private readonly gateway = inject(ADMIN_CATEGORIES_GATEWAY);
|
||||
private readonly formFactory = inject(AdminCategoriesFormFactory);
|
||||
private readonly localStorage = inject(LocalStorageService);
|
||||
private readonly projectEditor = inject(ProjectEditorFacade);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError, map, of } from 'rxjs';
|
||||
import { ApiConfigService } from '../../../../core/config/api-config.service';
|
||||
import { AdminCategory, AdminCategoryListFilters } from '../models/admin-category.model';
|
||||
import { AdminCategoriesGateway } from './admin-categories-gateway.interface';
|
||||
|
||||
/**
|
||||
* Real HTTP implementation against /backoffice/categories per
|
||||
* docs/backend/BACKEND-INTEGRATION.md §6.9 and the backend's API-REFERENCE.md §3.
|
||||
* Categories have no hard delete - only soft delete/restore.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AdminCategoriesApiGateway implements AdminCategoriesGateway {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly apiConfig = inject(ApiConfigService);
|
||||
|
||||
private get baseUrl(): string {
|
||||
return `${this.apiConfig.getBaseUrl()}/backoffice/categories`;
|
||||
}
|
||||
|
||||
loadCategories(filters: AdminCategoryListFilters): Observable<AdminCategory[]> {
|
||||
const params = new HttpParams()
|
||||
.set('search', filters.search)
|
||||
.set('visibility', filters.visibility)
|
||||
.set('includeDeleted', String(filters.includeDeleted));
|
||||
return this.http.get<AdminCategory[]>(this.baseUrl, { params });
|
||||
}
|
||||
|
||||
loadCategory(id: string): Observable<AdminCategory | null> {
|
||||
return this.http.get<AdminCategory>(`${this.baseUrl}/${encodeURIComponent(id)}`).pipe(
|
||||
catchError(() => of(null))
|
||||
);
|
||||
}
|
||||
|
||||
createCategory(category: AdminCategory): Observable<AdminCategory> {
|
||||
const { id, itemsCount, deletedAt, createdAt, updatedAt, ...body } = category;
|
||||
return this.http.post<AdminCategory>(this.baseUrl, body);
|
||||
}
|
||||
|
||||
updateCategory(category: AdminCategory): Observable<AdminCategory> {
|
||||
return this.http.put<AdminCategory>(`${this.baseUrl}/${encodeURIComponent(category.id)}`, category);
|
||||
}
|
||||
|
||||
deleteCategory(id: string): Observable<void> {
|
||||
return this.http.delete<void>(`${this.baseUrl}/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
restoreCategory(id: string): Observable<AdminCategory | null> {
|
||||
return this.http.post<AdminCategory>(`${this.baseUrl}/${encodeURIComponent(id)}/restore`, {}).pipe(
|
||||
catchError(() => of(null))
|
||||
);
|
||||
}
|
||||
|
||||
isSlugTaken(slug: string, excludingId: string | null): Observable<boolean> {
|
||||
let params = new HttpParams().set('slug', slug);
|
||||
if (excludingId) {
|
||||
params = params.set('excludingId', excludingId);
|
||||
}
|
||||
return this.http.get<{ taken: boolean }>(`${this.baseUrl}/slug-taken`, { params }).pipe(
|
||||
map(response => response.taken),
|
||||
catchError(() => of(false))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { InjectionToken, inject } from '@angular/core';
|
||||
import { RuntimeProviderStrategyService } from '../../../../core/providers/runtime-provider-strategy.service';
|
||||
import { AdminCategoriesApiGateway } from './admin-categories-api.gateway';
|
||||
import { AdminCategoriesLocalGateway } from './admin-categories-local.gateway';
|
||||
import { AdminCategoriesGateway } from './admin-categories-gateway.interface';
|
||||
|
||||
export const ADMIN_CATEGORIES_GATEWAY = new InjectionToken<AdminCategoriesGateway>('ADMIN_CATEGORIES_GATEWAY', {
|
||||
providedIn: 'root',
|
||||
factory: () => {
|
||||
const strategy = inject(RuntimeProviderStrategyService);
|
||||
const mode = strategy.getBackofficeProviderMode();
|
||||
return mode === 'mock' ? inject(AdminCategoriesLocalGateway) : inject(AdminCategoriesApiGateway);
|
||||
}
|
||||
});
|
||||
@@ -333,8 +333,9 @@ export class CartComponent implements OnDestroy {
|
||||
this.closeTimeout = setTimeout(() => {
|
||||
this.autoSubmitPurchase();
|
||||
}, 5000);
|
||||
this.recordOrder();
|
||||
this.cartService.clearCart();
|
||||
|
||||
|
||||
|
||||
}
|
||||
// Continue checking for 3 minutes regardless of other statuses
|
||||
@@ -387,6 +388,36 @@ export class CartComponent implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the just-paid cart as a backoffice AdminOrder (POST /orders) so it shows up
|
||||
* in Backoffice → Orders/Transactions. Best-effort and fire-and-forget: a failure here
|
||||
* must never affect the already-confirmed payment or block autoSubmitPurchase.
|
||||
*/
|
||||
private recordOrder(): void {
|
||||
const email = this.userEmail().trim();
|
||||
const phone = this.userPhone().replace(/\D/g, '');
|
||||
|
||||
this.apiService.createOrder({
|
||||
items: this.paidItems.map((item: CartItem) => ({
|
||||
productId: String(item.itemID),
|
||||
name: item.name,
|
||||
quantity: item.quantity,
|
||||
price: item.discount > 0 ? item.price * (1 - item.discount / 100) : item.price,
|
||||
})),
|
||||
customer: {
|
||||
name: this.getTelegramUsername() || 'Guest',
|
||||
email,
|
||||
phone,
|
||||
},
|
||||
payment: {
|
||||
method: this.selectedPaymentMethod(),
|
||||
currency: 'RUB',
|
||||
},
|
||||
}).subscribe({
|
||||
error: (err) => console.error('Error recording order:', err),
|
||||
});
|
||||
}
|
||||
|
||||
private autoSubmitPurchase(): void {
|
||||
setTimeout(() => {
|
||||
const lang = this.langService.currentLanguage();
|
||||
|
||||
@@ -50,6 +50,21 @@ export interface CartPaymentRequest {
|
||||
items: Array<{ itemID: number; price: number; name: string; quantity?: number; delivery?: DeliveryOption[] }>;
|
||||
}
|
||||
|
||||
export interface CreateOrderRequest {
|
||||
items: Array<{ productId: string; name: string; quantity: number; price: number }>;
|
||||
customer: { name: string; email: string; phone: string };
|
||||
payment?: { method: string; currency: string };
|
||||
shipping?: { address: string; method: string; trackingNumber: string };
|
||||
}
|
||||
|
||||
export interface CreateOrderResponse {
|
||||
id: string;
|
||||
orderNumber: string;
|
||||
status: string;
|
||||
total: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface QrDynamicStatusResponse {
|
||||
additionalInfo: string;
|
||||
paymentPurpose: string;
|
||||
@@ -614,6 +629,15 @@ export class ApiService {
|
||||
return this.http.post<QrCreateResponse>(`${this.baseUrl}/cart`, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the just-paid cart as a backoffice order (POST /orders). Fire-and-forget
|
||||
* from the caller's perspective - a failure here must never block the existing
|
||||
* payment-confirmed flow, since payment itself is unaffected by this call.
|
||||
*/
|
||||
createOrder(payload: CreateOrderRequest): Observable<CreateOrderResponse> {
|
||||
return this.http.post<CreateOrderResponse>(`${this.baseUrl}/orders`, payload);
|
||||
}
|
||||
|
||||
checkCartPaymentStatus(qrId: string): Observable<QrDynamicStatusResponse> {
|
||||
return this.http.get<QrDynamicStatusResponse>(
|
||||
`${this.qrBaseUrl}/qr/dynamic/${this.cartPaymentPartnerId}/${encodeURIComponent(qrId)}`
|
||||
|
||||
@@ -14,7 +14,7 @@ export const environment = {
|
||||
brandFullName: 'Marketplace',
|
||||
theme: 'dexar',
|
||||
apiUrl: 'https://api.dexarmarket.ru:445',
|
||||
authApiUrl: 'https://users.vitanova.network:456',
|
||||
authApiUrl: 'https://api.dexarmarket.ru:445',
|
||||
qrApiUrl: 'https://qr.vitanova.network/api',
|
||||
logo: '/icons/icon-192x192.png',
|
||||
contactEmail: 'info@dexarmarket.ru',
|
||||
|
||||
@@ -15,7 +15,7 @@ export const environment = {
|
||||
brandFullName: 'Marketplace',
|
||||
theme: 'dexar',
|
||||
apiUrl: '/api',
|
||||
authApiUrl: 'https://users.vitanova.network:456',
|
||||
authApiUrl: 'https://api.dexarmarket.ru:445',
|
||||
qrApiUrl: 'https://qr.vitanova.network/api',
|
||||
logo: '/icons/icon-192x192.png',
|
||||
contactEmail: 'info@dexarmarket.ru',
|
||||
|
||||
Reference in New Issue
Block a user