2026-01-18 18:57:06 +04:00
|
|
|
import { Injectable } from '@angular/core';
|
|
|
|
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
|
|
|
import { Observable } from 'rxjs';
|
|
|
|
|
import { map } from 'rxjs/operators';
|
2026-02-20 10:44:03 +04:00
|
|
|
import { Category, Item, Subcategory } from '../models';
|
2026-01-18 18:57:06 +04:00
|
|
|
import { environment } from '../../environments/environment';
|
|
|
|
|
|
|
|
|
|
@Injectable({
|
|
|
|
|
providedIn: 'root'
|
|
|
|
|
})
|
|
|
|
|
export class ApiService {
|
|
|
|
|
private readonly baseUrl = environment.apiUrl;
|
|
|
|
|
|
|
|
|
|
constructor(private http: HttpClient) {}
|
|
|
|
|
|
2026-02-20 10:44:03 +04:00
|
|
|
/**
|
|
|
|
|
* 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;
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
|
|
|
|
|
2026-02-20 10:44:03 +04:00
|
|
|
private normalizeItems(items: any[] | null | undefined): Item[] {
|
2026-01-18 18:57:06 +04:00
|
|
|
if (!items || !Array.isArray(items)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
return items.map(item => this.normalizeItem(item));
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 10:44:03 +04:00
|
|
|
/**
|
|
|
|
|
* 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 ───────────────────────────
|
|
|
|
|
|
2026-01-18 18:57:06 +04:00
|
|
|
ping(): Observable<{ message: string }> {
|
|
|
|
|
return this.http.get<{ message: string }>(`${this.baseUrl}/ping`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getCategories(): Observable<Category[]> {
|
2026-02-20 10:44:03 +04:00
|
|
|
return this.http.get<any[]>(`${this.baseUrl}/category`)
|
|
|
|
|
.pipe(map(cats => this.normalizeCategories(cats)));
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getCategoryItems(categoryID: number, count: number = 50, skip: number = 0): Observable<Item[]> {
|
|
|
|
|
const params = new HttpParams()
|
|
|
|
|
.set('count', count.toString())
|
|
|
|
|
.set('skip', skip.toString());
|
2026-02-20 10:44:03 +04:00
|
|
|
return this.http.get<any[]>(`${this.baseUrl}/category/${categoryID}`, { params })
|
2026-01-18 18:57:06 +04:00
|
|
|
.pipe(map(items => this.normalizeItems(items)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getItem(itemID: number): Observable<Item> {
|
2026-02-20 10:44:03 +04:00
|
|
|
return this.http.get<any>(`${this.baseUrl}/item/${itemID}`)
|
2026-01-18 18:57:06 +04:00
|
|
|
.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());
|
2026-02-20 10:44:03 +04:00
|
|
|
return this.http.get<any>(`${this.baseUrl}/searchitems`, { params })
|
2026-01-18 18:57:06 +04:00
|
|
|
.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<Item[]> {
|
2026-02-20 10:44:03 +04:00
|
|
|
return this.http.get<any[]>(`${this.baseUrl}/cart`)
|
2026-01-18 18:57:06 +04:00
|
|
|
.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;
|
2026-01-22 23:52:59 +04:00
|
|
|
}>(`${this.baseUrl}/qr/payment/${qrId}`);
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<Item[]> {
|
|
|
|
|
let params = new HttpParams().set('count', count.toString());
|
|
|
|
|
if (categoryID) {
|
|
|
|
|
params = params.set('category', categoryID.toString());
|
|
|
|
|
}
|
2026-02-20 10:44:03 +04:00
|
|
|
return this.http.get<any[]>(`${this.baseUrl}/randomitems`, { params })
|
2026-01-18 18:57:06 +04:00
|
|
|
.pipe(map(items => this.normalizeItems(items)));
|
|
|
|
|
}
|
|
|
|
|
}
|