import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { Category, Item, Subcategory } from '../models'; import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root' }) export class ApiService { private readonly baseUrl = environment.apiUrl; constructor(private http: HttpClient) {} /** * Normalize an item from the API response — supports both * legacy marketplace format and the new backOffice API format. */ private normalizeItem(raw: any): Item { const item: Item = { ...raw }; // Map backOffice string id → legacy numeric itemID if (raw.id != null && raw.itemID == null) { item.id = String(raw.id); item.itemID = typeof raw.id === 'number' ? raw.id : 0; } // Map backOffice imgs[] → legacy photos[] if (raw.imgs && (!raw.photos || raw.photos.length === 0)) { item.photos = raw.imgs.map((url: string) => ({ url })); } item.imgs = raw.imgs || raw.photos?.map((p: any) => p.url) || []; // Map backOffice description (key-value array) → legacy description string if (Array.isArray(raw.description)) { item.descriptionFields = raw.description; item.description = raw.description.map((d: any) => `${d.key}: ${d.value}`).join('\n'); } else { item.description = raw.description || raw.simpleDescription || ''; } // Map backOffice comments → legacy callbacks if (raw.comments && (!raw.callbacks || raw.callbacks.length === 0)) { item.callbacks = raw.comments.map((c: any) => ({ rating: c.stars, content: c.text, userID: c.author, timestamp: c.createdAt, })); } item.comments = raw.comments || raw.callbacks?.map((c: any) => ({ id: c.userID, text: c.content, author: c.userID, stars: c.rating, createdAt: c.timestamp, })) || []; // Compute average rating from comments if not present if (raw.rating == null && item.comments && item.comments.length > 0) { const rated = item.comments.filter(c => c.stars != null); item.rating = rated.length > 0 ? rated.reduce((sum, c) => sum + (c.stars || 0), 0) / rated.length : 0; } item.rating = item.rating || 0; // Defaults item.discount = item.discount || 0; item.remainings = item.remainings || (raw.quantity != null ? (raw.quantity <= 0 ? 'out' : raw.quantity <= 5 ? 'low' : raw.quantity <= 20 ? 'medium' : 'high') : 'high'); item.currency = item.currency || 'RUB'; // Preserve new backOffice fields item.badges = raw.badges || []; item.tags = raw.tags || []; item.simpleDescription = raw.simpleDescription || ''; item.translations = raw.translations || {}; item.visible = raw.visible ?? true; item.priority = raw.priority ?? 0; return item; } private normalizeItems(items: any[] | null | undefined): Item[] { if (!items || !Array.isArray(items)) { return []; } return items.map(item => this.normalizeItem(item)); } /** * Normalize a category from the API response — supports both * the flat legacy format and nested backOffice format. */ private normalizeCategory(raw: any): Category { const cat: Category = { ...raw }; if (raw.id != null && raw.categoryID == null) { cat.id = String(raw.id); cat.categoryID = typeof raw.id === 'number' ? raw.id : 0; } // Map backOffice img → legacy icon if (raw.img && !raw.icon) { cat.icon = raw.img; } cat.img = raw.img || raw.icon; cat.parentID = raw.parentID ?? 0; cat.visible = raw.visible ?? true; cat.priority = raw.priority ?? 0; if (raw.subcategories && Array.isArray(raw.subcategories)) { cat.subcategories = raw.subcategories; } return cat; } private normalizeCategories(cats: any[] | null | undefined): Category[] { if (!cats || !Array.isArray(cats)) return []; return cats.map(c => this.normalizeCategory(c)); } // ─── Core Marketplace Endpoints ─────────────────────────── ping(): Observable<{ message: string }> { return this.http.get<{ message: string }>(`${this.baseUrl}/ping`); } getCategories(): Observable { return this.http.get(`${this.baseUrl}/category`) .pipe(map(cats => this.normalizeCategories(cats))); } getCategoryItems(categoryID: number, count: number = 50, skip: number = 0): Observable { const params = new HttpParams() .set('count', count.toString()) .set('skip', skip.toString()); return this.http.get(`${this.baseUrl}/category/${categoryID}`, { params }) .pipe(map(items => this.normalizeItems(items))); } getItem(itemID: number): Observable { return this.http.get(`${this.baseUrl}/item/${itemID}`) .pipe(map(item => this.normalizeItem(item))); } searchItems(search: string, count: number = 50, skip: number = 0): Observable<{ items: Item[], total: number }> { const params = new HttpParams() .set('search', search) .set('count', count.toString()) .set('skip', skip.toString()); return this.http.get(`${this.baseUrl}/searchitems`, { params }) .pipe( map(response => ({ items: this.normalizeItems(response?.items || []), total: response?.total || 0 })) ); } addToCart(itemID: number, quantity: number = 1): Observable<{ message: string }> { return this.http.post<{ message: string }>(`${this.baseUrl}/cart`, { itemID, quantity }); } updateCartQuantity(itemID: number, quantity: number): Observable<{ message: string }> { return this.http.patch<{ message: string }>(`${this.baseUrl}/cart`, { itemID, quantity }); } removeFromCart(itemIDs: number[]): Observable<{ message: string }> { return this.http.delete<{ message: string }>(`${this.baseUrl}/cart`, { body: itemIDs }); } getCart(): Observable { return this.http.get(`${this.baseUrl}/cart`) .pipe(map(items => this.normalizeItems(items))); } // Review submission submitReview(reviewData: { itemID: number; rating: number; comment: string; username: string | null; userId: number | null; timestamp: string; }): Observable<{ message: string }> { return this.http.post<{ message: string }>(`${this.baseUrl}/comment`, reviewData); } // Payment - SBP Integration createPayment(paymentData: { amount: number; currency: string; siteuserID: string; siteorderID: string; redirectUrl: string; telegramUsername: string; items: Array<{ itemID: number; price: number; name: string }>; }): Observable<{ qrId: string; qrStatus: string; qrExpirationDate: string; payload: string; qrUrl: string; }> { return this.http.post<{ qrId: string; qrStatus: string; qrExpirationDate: string; payload: string; qrUrl: string; }>(`${this.baseUrl}/cart`, paymentData); } checkPaymentStatus(qrId: string): Observable<{ additionalInfo: string; paymentPurpose: string; amount: number; code: string; createDate: string; currency: string; order: string; paymentStatus: string; qrId: string; transactionDate: string; transactionId: number; qrExpirationDate: string; phoneNumber: string; }> { return this.http.get<{ additionalInfo: string; paymentPurpose: string; amount: number; code: string; createDate: string; currency: string; order: string; paymentStatus: string; qrId: string; transactionDate: string; transactionId: number; qrExpirationDate: string; phoneNumber: string; }>(`${this.baseUrl}/qr/payment/${qrId}`); } submitPurchaseEmail(emailData: { email: string; telegramUserId: string | null; items: Array<{ itemID: number; name: string; price: number; currency: string }>; }): Observable<{ message: string }> { return this.http.post<{ message: string }>(`${this.baseUrl}/purchase-email`, emailData); } getRandomItems(count: number = 5, categoryID?: number): Observable { let params = new HttpParams().set('count', count.toString()); if (categoryID) { params = params.set('category', categoryID.toString()); } return this.http.get(`${this.baseUrl}/randomitems`, { params }) .pipe(map(items => this.normalizeItems(items))); } }