api doc
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

This commit is contained in:
sdarbinyan
2026-07-20 01:02:36 +04:00
parent 08976de55a
commit fd5a436220
11 changed files with 930 additions and 10 deletions

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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))
);
}
}

View File

@@ -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);
}
});

View File

@@ -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();

View File

@@ -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)}`