2026-01-18 18:57:06 +04:00
|
|
|
import { Injectable } from '@angular/core';
|
2026-06-05 18:23:24 +04:00
|
|
|
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
2026-03-06 18:40:58 +04:00
|
|
|
import { Observable, timer } from 'rxjs';
|
2026-06-02 01:46:12 +04:00
|
|
|
import { map, retry } 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';
|
|
|
|
|
|
2026-06-05 18:23:24 +04:00
|
|
|
export interface QrCreateRequest {
|
|
|
|
|
qrtype: 'QRDynamic';
|
2026-06-02 00:57:36 +04:00
|
|
|
amount: number;
|
2026-06-05 18:23:24 +04:00
|
|
|
currency: 'RUB';
|
2026-06-06 16:16:37 +04:00
|
|
|
partnerqrID?: string;
|
2026-06-05 18:23:24 +04:00
|
|
|
qrDescription?: string;
|
|
|
|
|
Userid?: string;
|
|
|
|
|
Reference?: string;
|
|
|
|
|
RedirectUrl?: string;
|
2026-06-02 00:57:36 +04:00
|
|
|
}
|
|
|
|
|
|
2026-06-02 01:46:12 +04:00
|
|
|
export interface QrCreateResponse {
|
2026-06-02 00:57:36 +04:00
|
|
|
qrId?: string;
|
|
|
|
|
qrID?: string;
|
2026-06-02 01:46:12 +04:00
|
|
|
nspkID?: string;
|
|
|
|
|
nspkId?: string;
|
|
|
|
|
nspkurl?: string;
|
|
|
|
|
status?: string;
|
2026-06-02 00:57:36 +04:00
|
|
|
qrStatus?: string;
|
|
|
|
|
qrExpirationDate?: string;
|
|
|
|
|
payload?: string;
|
|
|
|
|
Payload?: string;
|
|
|
|
|
qrUrl?: string;
|
2026-06-02 01:46:12 +04:00
|
|
|
partnerqrID?: string | number;
|
2026-06-02 00:57:36 +04:00
|
|
|
partnerID?: string | number;
|
|
|
|
|
partnerId?: string | number;
|
|
|
|
|
PartnerID?: string | number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 18:23:24 +04:00
|
|
|
export interface QrDynamicStatusResponse {
|
2026-06-02 00:57:36 +04:00
|
|
|
additionalInfo: string;
|
|
|
|
|
paymentPurpose: string;
|
|
|
|
|
amount: number;
|
|
|
|
|
code: string;
|
|
|
|
|
createDate: string;
|
|
|
|
|
currency: string;
|
|
|
|
|
order: string;
|
|
|
|
|
paymentStatus: string;
|
|
|
|
|
qrId: string;
|
|
|
|
|
transactionDate: string;
|
|
|
|
|
transactionId: number;
|
|
|
|
|
qrExpirationDate: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 16:16:37 +04:00
|
|
|
export interface QrPaymentStatusResponse {
|
|
|
|
|
status?: string;
|
|
|
|
|
paymentStatus?: string;
|
|
|
|
|
code?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 18:57:06 +04:00
|
|
|
@Injectable({
|
|
|
|
|
providedIn: 'root'
|
|
|
|
|
})
|
|
|
|
|
export class ApiService {
|
|
|
|
|
private readonly baseUrl = environment.apiUrl;
|
2026-06-05 18:23:24 +04:00
|
|
|
private readonly qrBaseUrl = (environment as any).qrApiUrl as string;
|
2026-06-06 19:25:00 +04:00
|
|
|
private readonly sbpQrUrl = 'https://qr.vitanova.network/api/qr';
|
2026-01-18 18:57:06 +04:00
|
|
|
|
2026-03-06 18:40:58 +04:00
|
|
|
private readonly retryConfig = {
|
|
|
|
|
count: 2,
|
|
|
|
|
delay: (error: unknown, retryCount: number) => timer(Math.pow(2, retryCount) * 500)
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-18 18:57:06 +04:00
|
|
|
constructor(private http: HttpClient) {}
|
|
|
|
|
|
2026-03-24 02:25:50 +04:00
|
|
|
/** Map API language codes (RU/EN/AM) → frontend codes (ru/en/hy) */
|
|
|
|
|
private normalizeLang(apiLang: string): string {
|
|
|
|
|
const map: Record<string, string> = { 'RU': 'ru', 'EN': 'en', 'AM': 'hy' };
|
|
|
|
|
return map[apiLang] || apiLang.toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 03:12:04 +04:00
|
|
|
/** Convert Go-style hex colour (0xfffca0) → CSS hex (#fffca0) */
|
|
|
|
|
private normalizeColor(c: string): string {
|
|
|
|
|
if (!c) return '';
|
|
|
|
|
return c.startsWith('0x') ? '#' + c.slice(2) : c;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 02:46:58 +04:00
|
|
|
/** Resolve relative image URLs (e.g. ./images/x.webp) against site origin */
|
2026-03-24 02:25:50 +04:00
|
|
|
private resolveImageUrl(url: string): string {
|
|
|
|
|
if (!url) return '';
|
|
|
|
|
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/')) return url;
|
2026-03-24 02:46:58 +04:00
|
|
|
const origin = `https://${environment.domain}`;
|
|
|
|
|
if (url.startsWith('./')) return `${origin}/${url.slice(2)}`;
|
|
|
|
|
return `${origin}/${url}`;
|
2026-03-24 02:25:50 +04:00
|
|
|
}
|
|
|
|
|
|
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 {
|
2026-03-24 00:09:11 +04:00
|
|
|
const { partnerID, ...rest } = raw;
|
|
|
|
|
const item: Item = { ...rest };
|
2026-02-20 10:44:03 +04:00
|
|
|
|
2026-03-24 02:25:50 +04:00
|
|
|
// Extract price/currency/remaining/colour/size from itemDetails[]
|
|
|
|
|
// Note: Go struct tag is "itemdetails" but actual API may send "itemDetails"
|
|
|
|
|
const details = raw.itemDetails || raw.itemdetails;
|
|
|
|
|
if (details && Array.isArray(details) && details.length > 0) {
|
|
|
|
|
const detail = details[0];
|
2026-03-24 03:12:04 +04:00
|
|
|
item.itemDetails = details.map((d: any) => ({
|
|
|
|
|
...d,
|
|
|
|
|
colour: this.normalizeColor(d.colour || d.color || ''),
|
|
|
|
|
color: undefined,
|
|
|
|
|
}));
|
2026-03-24 02:25:50 +04:00
|
|
|
if (item.price == null || item.price === 0) item.price = detail.price;
|
|
|
|
|
if (!item.currency) item.currency = detail.currency;
|
2026-03-24 03:12:04 +04:00
|
|
|
if (!item.colour) item.colour = this.normalizeColor(detail.colour || detail.color || '');
|
2026-03-24 02:25:50 +04:00
|
|
|
if (!item.size) item.size = detail.size || '';
|
|
|
|
|
// Use remaining from detail for stock level
|
|
|
|
|
if (raw.remaining == null && detail.remaining != null) {
|
|
|
|
|
(raw as any).remaining = detail.remaining;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 10:44:03 +04:00
|
|
|
// 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 }));
|
|
|
|
|
}
|
2026-03-24 00:09:11 +04:00
|
|
|
// Normalize photo type: API sends type='video'|'photo', template checks .video
|
2026-03-24 02:25:50 +04:00
|
|
|
// Also resolve relative URLs (e.g. ./images/x.webp) against API base
|
2026-03-24 00:09:11 +04:00
|
|
|
if (item.photos) {
|
|
|
|
|
item.photos = item.photos.map((p: any) => ({
|
|
|
|
|
...p,
|
2026-03-24 02:25:50 +04:00
|
|
|
url: this.resolveImageUrl(p.url),
|
2026-03-24 00:09:11 +04:00
|
|
|
video: p.video || (p.type === 'video' ? p.url : undefined),
|
|
|
|
|
}));
|
|
|
|
|
}
|
2026-03-24 02:25:50 +04:00
|
|
|
item.imgs = raw.imgs?.map((u: string) => this.resolveImageUrl(u))
|
|
|
|
|
|| item.photos?.map((p: any) => p.url) || [];
|
2026-02-20 10:44:03 +04:00
|
|
|
|
|
|
|
|
// 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 || '';
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 00:09:11 +04:00
|
|
|
// Map backend names[] → translations (multi-lang name support)
|
2026-03-24 02:25:50 +04:00
|
|
|
// Note: API has typo "valuue" in some responses, handle both
|
2026-03-24 00:09:11 +04:00
|
|
|
if (raw.names && Array.isArray(raw.names)) {
|
|
|
|
|
item.names = raw.names;
|
|
|
|
|
if (!item.translations) item.translations = {};
|
|
|
|
|
for (const entry of raw.names) {
|
2026-03-24 02:25:50 +04:00
|
|
|
const lang = this.normalizeLang(entry.language);
|
|
|
|
|
const val = entry.value || entry.valuue || '';
|
|
|
|
|
if (val) {
|
|
|
|
|
if (!item.translations[lang]) item.translations[lang] = {};
|
|
|
|
|
item.translations[lang].name = val;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Fallback: if top-level name is missing, use first available translation
|
|
|
|
|
if (!item.name && raw.names.length > 0) {
|
|
|
|
|
const ruName = raw.names.find((n: any) => n.language === 'RU' || n.language === 'ru');
|
|
|
|
|
item.name = ruName?.value || ruName?.valuue || raw.names[0].value || raw.names[0].valuue || '';
|
2026-03-24 00:09:11 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Preserve attributes from backend
|
|
|
|
|
item.attributes = raw.attributes || [];
|
|
|
|
|
|
2026-03-24 02:25:50 +04:00
|
|
|
// Preserve colour & size (only if not already set from itemDetails)
|
2026-03-24 03:12:04 +04:00
|
|
|
if (!item.colour) item.colour = this.normalizeColor(raw.colour || '');
|
2026-03-24 02:25:50 +04:00
|
|
|
if (!item.size) item.size = raw.size || '';
|
2026-03-24 00:09:11 +04:00
|
|
|
|
2026-02-20 10:44:03 +04:00
|
|
|
// 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
|
2026-03-24 02:25:50 +04:00
|
|
|
item.name = item.name || '';
|
|
|
|
|
item.price = item.price ?? 0;
|
2026-02-20 10:44:03 +04:00
|
|
|
item.discount = item.discount || 0;
|
2026-03-24 02:25:50 +04:00
|
|
|
item.remainings = item.remainings || (raw.remaining != null
|
|
|
|
|
? (raw.remaining <= 0 ? 'out' : raw.remaining <= 5 ? 'low' : raw.remaining <= 20 ? 'medium' : 'high')
|
|
|
|
|
: raw.quantity != null
|
2026-02-20 10:44:03 +04:00
|
|
|
? (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 || '';
|
2026-03-24 00:09:11 +04:00
|
|
|
item.translations = item.translations || raw.translations || {};
|
2026-02-20 10:44:03 +04:00
|
|
|
item.visible = raw.visible ?? true;
|
|
|
|
|
item.priority = raw.priority ?? 0;
|
2026-03-24 02:25:50 +04:00
|
|
|
item.visits = raw.visits ?? 0;
|
|
|
|
|
|
|
|
|
|
// Map question like/dislike → upvotes/downvotes
|
|
|
|
|
if (item.questions) {
|
|
|
|
|
item.questions = item.questions.map((q: any) => ({
|
|
|
|
|
...q,
|
|
|
|
|
upvotes: q.upvotes ?? q.like ?? 0,
|
|
|
|
|
downvotes: q.downvotes ?? q.dislike ?? 0,
|
|
|
|
|
}));
|
|
|
|
|
}
|
2026-02-20 10:44:03 +04:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-03-24 02:25:50 +04:00
|
|
|
// Resolve relative icon/image URLs
|
|
|
|
|
if (cat.icon) cat.icon = this.resolveImageUrl(cat.icon);
|
|
|
|
|
if (cat.img) cat.img = this.resolveImageUrl(cat.img);
|
|
|
|
|
|
|
|
|
|
// Map backend wideicon → wideBanner
|
|
|
|
|
if (raw.wideicon && !cat.wideBanner) {
|
|
|
|
|
cat.wideBanner = raw.wideicon;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 10:44:03 +04:00
|
|
|
cat.parentID = raw.parentID ?? 0;
|
|
|
|
|
cat.visible = raw.visible ?? true;
|
|
|
|
|
cat.priority = raw.priority ?? 0;
|
2026-03-24 02:25:50 +04:00
|
|
|
cat.itemCount = raw.itemCount ?? raw.ItemsCount ?? 0;
|
|
|
|
|
cat.categoriesCount = raw.categoriesCount ?? raw.CategoriesCount ?? 0;
|
|
|
|
|
|
|
|
|
|
// Map backend names[] → translations (multi-lang name support)
|
|
|
|
|
// Note: API has typo "valuue" in some responses, handle both
|
|
|
|
|
if (raw.names && Array.isArray(raw.names)) {
|
|
|
|
|
cat.names = raw.names;
|
|
|
|
|
cat.translations = cat.translations || {};
|
|
|
|
|
for (const entry of raw.names) {
|
|
|
|
|
const lang = this.normalizeLang(entry.language);
|
|
|
|
|
const val = entry.value || entry.valuue || '';
|
|
|
|
|
if (val) {
|
|
|
|
|
if (!cat.translations[lang]) cat.translations[lang] = {};
|
|
|
|
|
cat.translations[lang].name = val;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Fallback: if top-level name is missing, use first available translation
|
|
|
|
|
if (!cat.name && raw.names.length > 0) {
|
|
|
|
|
const ruName = raw.names.find((n: any) => n.language === 'RU' || n.language === 'ru');
|
|
|
|
|
cat.name = ruName?.value || ruName?.valuue || raw.names[0].value || raw.names[0].valuue || '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cat.name = cat.name || '';
|
2026-02-20 10:44:03 +04:00
|
|
|
|
|
|
|
|
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`)
|
2026-03-24 00:18:13 +04:00
|
|
|
.pipe(retry(this.retryConfig), 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-03-06 18:40:58 +04:00
|
|
|
.pipe(retry(this.retryConfig), map(items => this.normalizeItems(items)));
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getItem(itemID: number): Observable<Item> {
|
2026-03-24 02:25:50 +04:00
|
|
|
return this.http.get<any>(`${this.baseUrl}/items/${itemID}`)
|
2026-03-06 18:40:58 +04:00
|
|
|
.pipe(retry(this.retryConfig), map(item => this.normalizeItem(item)));
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 02:25:50 +04:00
|
|
|
searchItems(
|
|
|
|
|
search: string,
|
|
|
|
|
count: number = 50,
|
|
|
|
|
skip: number = 0,
|
|
|
|
|
options?: {
|
|
|
|
|
categoryIDs?: number[];
|
|
|
|
|
minPrice?: number;
|
|
|
|
|
maxPrice?: number;
|
|
|
|
|
tag?: string;
|
|
|
|
|
sort?: 'relevance' | 'price_asc' | 'price_desc' | 'popular' | 'rating';
|
|
|
|
|
}
|
|
|
|
|
): Observable<{ items: Item[], total: number }> {
|
|
|
|
|
let params = new HttpParams()
|
2026-01-18 18:57:06 +04:00
|
|
|
.set('search', search)
|
|
|
|
|
.set('count', count.toString())
|
|
|
|
|
.set('skip', skip.toString());
|
2026-03-24 02:25:50 +04:00
|
|
|
if (options?.categoryIDs?.length) {
|
|
|
|
|
params = params.set('categoryIDs', options.categoryIDs.join(','));
|
|
|
|
|
}
|
|
|
|
|
if (options?.minPrice != null) {
|
|
|
|
|
params = params.set('minPrice', options.minPrice.toString());
|
|
|
|
|
}
|
|
|
|
|
if (options?.maxPrice != null) {
|
|
|
|
|
params = params.set('maxPrice', options.maxPrice.toString());
|
|
|
|
|
}
|
|
|
|
|
if (options?.tag) {
|
|
|
|
|
params = params.set('tag', options.tag);
|
|
|
|
|
}
|
|
|
|
|
if (options?.sort) {
|
|
|
|
|
params = params.set('sort', options.sort);
|
|
|
|
|
}
|
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(
|
2026-03-06 18:40:58 +04:00
|
|
|
retry(this.retryConfig),
|
2026-01-18 18:57:06 +04:00
|
|
|
map(response => ({
|
|
|
|
|
items: this.normalizeItems(response?.items || []),
|
|
|
|
|
total: response?.total || 0
|
|
|
|
|
}))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 02:25:50 +04:00
|
|
|
// Cart operations — spec uses websession-based paths
|
|
|
|
|
addToCart(sessionId: string, items: Array<{ itemID: number; quantity: number; colour?: string; size?: string; price?: number }>): Observable<any> {
|
|
|
|
|
return this.http.post<any>(`${this.baseUrl}/websession/${sessionId}`, items);
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Review submission
|
|
|
|
|
submitReview(reviewData: {
|
|
|
|
|
itemID: number;
|
|
|
|
|
rating: number;
|
|
|
|
|
comment: string;
|
2026-03-24 02:25:50 +04:00
|
|
|
sessionID: string;
|
2026-01-18 18:57:06 +04:00
|
|
|
timestamp: string;
|
|
|
|
|
}): Observable<{ message: string }> {
|
2026-03-24 02:25:50 +04:00
|
|
|
const { itemID, ...body } = reviewData;
|
|
|
|
|
return this.http.post<{ message: string }>(`${this.baseUrl}/items/${itemID}/callback`, body);
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 02:25:50 +04:00
|
|
|
// Question submission — spec path has typo "questiion"
|
|
|
|
|
submitQuestion(questionData: {
|
|
|
|
|
itemID: number;
|
|
|
|
|
question: string;
|
|
|
|
|
sessionID: string;
|
|
|
|
|
timestamp: string;
|
|
|
|
|
}): Observable<{ message: string }> {
|
|
|
|
|
const { itemID, ...body } = questionData;
|
|
|
|
|
return this.http.post<{ message: string }>(`${this.baseUrl}/items/${itemID}/questiion`, body);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 18:23:24 +04:00
|
|
|
createPayment(payload: QrCreateRequest, headers?: { authorizationKey?: string; userIdValue?: string }): Observable<QrCreateResponse> {
|
|
|
|
|
let httpHeaders = new HttpHeaders();
|
|
|
|
|
if (headers?.authorizationKey) {
|
|
|
|
|
httpHeaders = httpHeaders.set('authorization-key', headers.authorizationKey);
|
|
|
|
|
}
|
|
|
|
|
if (headers?.userIdValue) {
|
|
|
|
|
httpHeaders = httpHeaders.set('userid-value', headers.userIdValue);
|
|
|
|
|
}
|
|
|
|
|
return this.http.post<QrCreateResponse>(`${this.qrBaseUrl}/qr`, payload, { headers: httpHeaders });
|
2026-06-02 00:57:36 +04:00
|
|
|
}
|
|
|
|
|
|
2026-06-06 16:16:37 +04:00
|
|
|
createSbpPayment(payload: QrCreateRequest): Observable<QrCreateResponse> {
|
|
|
|
|
return this.http.post<QrCreateResponse>(this.sbpQrUrl, payload);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
checkSbpPaymentStatus(paymentId: string): Observable<QrPaymentStatusResponse> {
|
|
|
|
|
const params = new HttpParams().set('id', paymentId);
|
|
|
|
|
return this.http.get<QrPaymentStatusResponse>(this.sbpQrUrl, { params });
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 18:23:24 +04:00
|
|
|
checkPaymentStatus(partnerQrId: string, qrId: string): Observable<QrDynamicStatusResponse> {
|
|
|
|
|
return this.http.get<QrDynamicStatusResponse>(
|
|
|
|
|
`${this.qrBaseUrl}/qr/dynamic/${encodeURIComponent(partnerQrId)}/${encodeURIComponent(qrId)}`
|
|
|
|
|
);
|
2026-06-02 00:57:36 +04:00
|
|
|
}
|
|
|
|
|
|
2026-06-02 01:46:12 +04:00
|
|
|
resolvePaymentQrId(response: QrCreateResponse): string {
|
|
|
|
|
return response.qrId ?? response.qrID ?? response.nspkID ?? response.nspkId ?? '';
|
|
|
|
|
}
|
2026-06-02 00:57:36 +04:00
|
|
|
|
2026-06-02 01:46:12 +04:00
|
|
|
resolvePaymentLink(response: QrCreateResponse): string {
|
|
|
|
|
return response.nspkurl ?? response.Payload ?? response.payload ?? response.qrUrl ?? '';
|
|
|
|
|
}
|
2026-06-02 00:57:36 +04:00
|
|
|
|
2026-06-02 01:46:12 +04:00
|
|
|
resolvePaymentQrUrl(response: QrCreateResponse): string {
|
|
|
|
|
const paymentLink = this.resolvePaymentLink(response);
|
|
|
|
|
if (paymentLink) {
|
|
|
|
|
return `https://api.qrserver.com/v1/create-qr-code/?size=256x256&margin=8&data=${encodeURIComponent(paymentLink)}`;
|
2026-06-02 00:57:36 +04:00
|
|
|
}
|
|
|
|
|
|
2026-06-02 01:46:12 +04:00
|
|
|
return response.qrUrl ?? '';
|
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-03-24 02:58:51 +04:00
|
|
|
return this.http.get<any[]>(`${this.baseUrl}/items/randomitems`, { params })
|
2026-03-06 18:40:58 +04:00
|
|
|
.pipe(retry(this.retryConfig), map(items => this.normalizeItems(items)));
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
|
|
|
|
}
|