refactor: reorganize component files and update import paths; add PsItem and PsProduct interfaces
This commit is contained in:
726
client/src/app/services/prestashop.serivce.ts
Normal file
726
client/src/app/services/prestashop.serivce.ts
Normal file
@@ -0,0 +1,726 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
|
||||
import {forkJoin, map, of, switchMap, Observable} from 'rxjs';
|
||||
import {PsItem} from '../interfaces/ps-item';
|
||||
import {PsProduct} from '../interfaces/ps-product';
|
||||
import {ProductListItem} from '../interfaces/product-list-item';
|
||||
|
||||
type Resource = 'categories' | 'manufacturers' | 'suppliers';
|
||||
|
||||
const UPDATE_CFG: Record<Resource, {
|
||||
root: 'category' | 'manufacturer' | 'supplier';
|
||||
needsDefaultLang?: boolean;
|
||||
keepFields?: string[];
|
||||
nameIsMultilang?: boolean;
|
||||
}> = {
|
||||
categories: {
|
||||
root: 'category',
|
||||
needsDefaultLang: true,
|
||||
keepFields: ['active', 'id_parent', 'link_rewrite'],
|
||||
nameIsMultilang: true
|
||||
},
|
||||
manufacturers: {root: 'manufacturer', needsDefaultLang: false, keepFields: ['active'], nameIsMultilang: false},
|
||||
suppliers: {root: 'supplier', needsDefaultLang: false, keepFields: ['active'], nameIsMultilang: false},
|
||||
};
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class PrestashopService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = '/ps'; // proxy Angular -> https://.../api
|
||||
|
||||
// -------- Utils
|
||||
/** Id de la catégorie d’accueil (Home) de la boutique: PS_HOME_CATEGORY */
|
||||
private getHomeCategoryId() {
|
||||
const params = new HttpParams()
|
||||
.set('display', '[value]')
|
||||
.set('filter[name]', 'PS_HOME_CATEGORY')
|
||||
.set('output_format', 'JSON');
|
||||
return this.http.get<any>(`${this.base}/configurations`, {params}).pipe(
|
||||
map(r => +r?.configurations?.[0]?.value || 2) // fallback 2
|
||||
);
|
||||
}
|
||||
|
||||
/** Id de la catégorie racine (PS_ROOT_CATEGORY) */
|
||||
private getRootCategoryId() {
|
||||
const params = new HttpParams()
|
||||
.set('display', '[value]')
|
||||
.set('filter[name]', 'PS_ROOT_CATEGORY')
|
||||
.set('output_format', 'JSON');
|
||||
return this.http.get<any>(`${this.base}/configurations`, {params}).pipe(
|
||||
map(r => +r?.configurations?.[0]?.value || 1) // fallback 1
|
||||
);
|
||||
}
|
||||
|
||||
private readonly headersXml = new HttpHeaders({'Content-Type': 'application/xml', 'Accept': 'application/xml'});
|
||||
|
||||
private escapeXml(v: string) {
|
||||
return String(v)
|
||||
.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
||||
.replaceAll('"', '"').replaceAll("'", ''');
|
||||
}
|
||||
|
||||
/** Extrait un <id> y compris avec CDATA et attributs éventuels */
|
||||
private extractIdFromXml(xml: string): number | null {
|
||||
const s = String(xml);
|
||||
|
||||
// 1) prioritaire: id du noeud <product> (avant <associations>)
|
||||
const mProduct = s.match(
|
||||
/<product[\s\S]*?<id[^>]*>\s*(?:<!\[CDATA\[)?(\d+)(?:\]\]>)?\s*<\/id>[\s\S]*?(?:<associations>|<\/product>)/i
|
||||
);
|
||||
if (mProduct) return +mProduct[1];
|
||||
|
||||
// 2) racine quelconque (tolère CDATA)
|
||||
const mRoot = s.match(
|
||||
/<prestashop[\s\S]*?<([a-z_]+)[^>]*>[\s\S]*?<id[^>]*>\s*(?:<!\[CDATA\[)?(\d+)(?:\]\]>)?\s*<\/id>/i
|
||||
);
|
||||
if (mRoot) return +mRoot[2];
|
||||
|
||||
// 3) fallback: premier <id> numérique
|
||||
const mAny = s.match(/<id[^>]*>\s*(?:<!\[CDATA\[)?(\d+)(?:\]\]>)?\s*<\/id>/i);
|
||||
if (mAny) return +mAny[1];
|
||||
|
||||
console.warn('[Presta] Impossible d’extraire <id> depuis la réponse:', xml);
|
||||
return null;
|
||||
}
|
||||
|
||||
private slug(str: string) {
|
||||
return String(str).toLowerCase().normalize('NFD')
|
||||
.replaceAll(/[\u0300-\u036f]/g, '')
|
||||
.replaceAll(/[^a-z0-9]+/g, '-')
|
||||
.replaceAll(/(^-|-$)/g, '');
|
||||
}
|
||||
|
||||
private toLangBlock(tag: string, entries: Array<{ id: number; value: string }>) {
|
||||
const inner = entries
|
||||
.map(e => `<language id="${e.id}">${this.escapeXml(e.value)}</language>`)
|
||||
.join('');
|
||||
return `<${tag}>${inner}</${tag}>`;
|
||||
}
|
||||
|
||||
private ensureArrayLang(v: any): Array<{ id: number; value: string }> {
|
||||
if (Array.isArray(v)) return v.map(x => ({id: +x.id, value: String(x.value ?? '')}));
|
||||
return [];
|
||||
}
|
||||
|
||||
private round2(n: number) {
|
||||
return Math.round((n + Number.EPSILON) * 100) / 100;
|
||||
}
|
||||
|
||||
private ttcToHt(priceTtc: number, rate = 0.2) {
|
||||
return this.round2(priceTtc / (1 + rate));
|
||||
}
|
||||
|
||||
// -------- Contexte, langues, fiscalité
|
||||
/** Langue par défaut (PS_LANG_DEFAULT) */
|
||||
getDefaultLangId() {
|
||||
const params = new HttpParams()
|
||||
.set('display', '[value]')
|
||||
.set('filter[name]', 'PS_LANG_DEFAULT')
|
||||
.set('output_format', 'JSON');
|
||||
return this.http.get<any>(`${this.base}/configurations`, {params}).pipe(
|
||||
map(r => +r?.configurations?.[0]?.value || 1)
|
||||
);
|
||||
}
|
||||
|
||||
/** Premier tax_rule_group actif (fallback TVA) */
|
||||
private getFirstActiveTaxRulesGroupId() {
|
||||
const params = new HttpParams()
|
||||
.set('display', '[id,active]')
|
||||
.set('filter[active]', '1')
|
||||
.set('output_format', 'JSON');
|
||||
|
||||
return this.http.get<any>(`${this.base}/tax_rule_groups`, {params}).pipe(
|
||||
map(r => {
|
||||
const g = (r?.tax_rule_groups ?? [])[0];
|
||||
return g ? +g.id : 0;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/** Groupe de taxes par défaut (PS_TAX_DEFAULT_RULES_GROUP) avec fallback sur un groupe actif */
|
||||
private getDefaultTaxRulesGroupId() {
|
||||
const params = new HttpParams()
|
||||
.set('display', '[value]')
|
||||
.set('filter[name]', 'PS_TAX_DEFAULT_RULES_GROUP')
|
||||
.set('output_format', 'JSON');
|
||||
|
||||
return this.http.get<any>(`${this.base}/configurations`, {params}).pipe(
|
||||
switchMap(r => {
|
||||
const id = +r?.configurations?.[0]?.value;
|
||||
if (Number.isFinite(id) && id > 0) return of(id);
|
||||
return this.getFirstActiveTaxRulesGroupId();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/** Devise par défaut (PS_CURRENCY_DEFAULT) */
|
||||
private getDefaultCurrencyId() {
|
||||
const params = new HttpParams()
|
||||
.set('display', '[value]')
|
||||
.set('filter[name]', 'PS_CURRENCY_DEFAULT')
|
||||
.set('output_format', 'JSON');
|
||||
|
||||
return this.http.get<any>(`${this.base}/configurations`, {params}).pipe(
|
||||
map(r => +r?.configurations?.[0]?.value || 1)
|
||||
);
|
||||
}
|
||||
|
||||
/** IDs des langues actives (pour création catégorie multilangue) */
|
||||
getActiveLangIds() {
|
||||
const params = new HttpParams().set('display', '[id,active]').set('filter[active]', '1').set('output_format', 'JSON');
|
||||
return this.http.get<any>(`${this.base}/languages`, {params}).pipe(
|
||||
map(r => (r?.languages ?? []).map((l: any) => +l.id))
|
||||
);
|
||||
}
|
||||
|
||||
/** Contexte boutique actif (1ère boutique active) */
|
||||
private getDefaultShopContext() {
|
||||
const params = new HttpParams()
|
||||
.set('display', '[id,id_shop_group,active]')
|
||||
.set('filter[active]', '1')
|
||||
.set('output_format', 'JSON');
|
||||
return this.http.get<any>(`${this.base}/shops`, {params}).pipe(
|
||||
map(r => {
|
||||
const s = (r?.shops ?? [])[0];
|
||||
return {idShop: s ? +s.id : 1, idShopGroup: s ? +s.id_shop_group : 1};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/** Objet complet pour update sûr (cat/manu/supplier) */
|
||||
private getOne(resource: Resource, id: number) {
|
||||
const params = new HttpParams().set('output_format', 'JSON').set('display', 'full');
|
||||
return this.http.get<any>(`${this.base}/${resource}/${id}`, {params}).pipe(
|
||||
map(r => r?.category ?? r?.manufacturer ?? r?.supplier ?? r)
|
||||
);
|
||||
}
|
||||
|
||||
// -------- CRUD générique (categories/manufacturers/suppliers)
|
||||
list(resource: Resource) {
|
||||
const params = new HttpParams().set('display', '[id,name,active]').set('output_format', 'JSON');
|
||||
return this.http.get<any>(`${this.base}/${resource}`, {params}).pipe(
|
||||
map(r => (r?.[resource] ?? []).map((x: any) => ({
|
||||
id: +x.id,
|
||||
name: Array.isArray(x.name) ? (x.name[0]?.value ?? '') : (x.name ?? ''),
|
||||
active: x.active === undefined ? undefined : !!+x.active
|
||||
}) as PsItem))
|
||||
);
|
||||
}
|
||||
|
||||
create(resource: Resource, name: string) {
|
||||
const safeName = this.escapeXml(name);
|
||||
|
||||
if (resource === 'categories') {
|
||||
return this.getActiveLangIds().pipe(
|
||||
switchMap(langIds => {
|
||||
const xml = `<prestashop xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<category>
|
||||
<id_parent>2</id_parent>
|
||||
<active>1</active>
|
||||
${this.toLangBlock('name', langIds.map((id: number) => ({id, value: safeName})))}
|
||||
${this.toLangBlock('link_rewrite', langIds.map((id: number) => ({id, value: this.slug(name)})))}
|
||||
</category>
|
||||
</prestashop>`;
|
||||
return this.http.post(`${this.base}/categories`, xml, {headers: this.headersXml, responseType: 'text'});
|
||||
}),
|
||||
map(res => this.extractIdFromXml(res))
|
||||
);
|
||||
}
|
||||
|
||||
const xml = resource === 'manufacturers'
|
||||
? `<prestashop xmlns:xlink="http://www.w3.org/1999/xlink"><manufacturer><active>1</active><name>${safeName}</name></manufacturer></prestashop>`
|
||||
: `<prestashop xmlns:xlink="http://www.w3.org/1999/xlink"><supplier><active>1</active><name>${safeName}</name></supplier></prestashop>`;
|
||||
|
||||
return this.http.post(`${this.base}/${resource}`, xml, {headers: this.headersXml, responseType: 'text'})
|
||||
.pipe(map((res: string) => this.extractIdFromXml(res)));
|
||||
}
|
||||
|
||||
update(resource: Resource, id: number, newName: string) {
|
||||
const cfg = UPDATE_CFG[resource];
|
||||
const safeName = this.escapeXml(newName);
|
||||
|
||||
const defaultLangOr1$ = (cfg.needsDefaultLang ? this.getDefaultLangId() : this.getDefaultLangId().pipe(map(() => 1)));
|
||||
return defaultLangOr1$.pipe(
|
||||
switchMap((idLang: number) => this.getOne(resource, id).pipe(
|
||||
switchMap(obj => {
|
||||
const root = cfg.root;
|
||||
const active = obj?.active === undefined ? 1 : +obj.active;
|
||||
|
||||
let linkRewriteXml = '', idParentXml = '';
|
||||
if (resource === 'categories') {
|
||||
const lr = this.ensureArrayLang(obj?.link_rewrite);
|
||||
linkRewriteXml = lr.length ? this.toLangBlock('link_rewrite', lr)
|
||||
: this.toLangBlock('link_rewrite', [{id: idLang, value: this.slug(newName)}]);
|
||||
if (obj?.id_parent) idParentXml = `<id_parent>${+obj.id_parent}</id_parent>`;
|
||||
}
|
||||
|
||||
const nameXml = cfg.nameIsMultilang
|
||||
? `<name><language id="${idLang}">${safeName}</language></name>`
|
||||
: `<name>${safeName}</name>`;
|
||||
|
||||
const body = `<prestashop xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<${root}>
|
||||
<id>${id}</id>
|
||||
<active>${active}</active>
|
||||
${idParentXml}
|
||||
${nameXml}
|
||||
${linkRewriteXml}
|
||||
</${root}>
|
||||
</prestashop>`;
|
||||
|
||||
return this.http.put(`${this.base}/${resource}/${id}`, body, {
|
||||
headers: this.headersXml,
|
||||
responseType: 'text'
|
||||
});
|
||||
})
|
||||
)),
|
||||
map(() => true)
|
||||
);
|
||||
}
|
||||
|
||||
delete(resource: Resource, id: number) {
|
||||
return this.http.delete(`${this.base}/${resource}/${id}`, {responseType: 'text'}).pipe(map(() => true));
|
||||
}
|
||||
|
||||
getXml(resource: Resource, id: number) {
|
||||
return this.http.get(`${this.base}/${resource}/${id}`, {responseType: 'text'});
|
||||
}
|
||||
|
||||
// -------- Produits (liste / détails)
|
||||
listProducts(query?: string) {
|
||||
let params = new HttpParams()
|
||||
.set('display', '[id,name,id_manufacturer,id_supplier,id_category_default,price]')
|
||||
.set('output_format', 'JSON');
|
||||
if (query?.trim()) params = params.set('filter[name]', `%[${query.trim()}]%`);
|
||||
return this.http.get<any>(`${this.base}/products`, {params}).pipe(
|
||||
map(r => (r?.products ?? []).map((p: any) => ({
|
||||
id: +p.id,
|
||||
name: Array.isArray(p.name) ? (p.name[0]?.value ?? '') : (p.name ?? ''),
|
||||
id_manufacturer: p?.id_manufacturer ? +p.id_manufacturer : undefined,
|
||||
id_supplier: p?.id_supplier ? +p.id_supplier : undefined,
|
||||
id_category_default: p?.id_category_default ? +p.id_category_default : undefined,
|
||||
priceHt: p?.price ? +p.price : 0
|
||||
}) as ProductListItem & { priceHt?: number }))
|
||||
);
|
||||
}
|
||||
|
||||
/** Détails produit (JSON full) + fallback XML pour description si vide */
|
||||
getProductDetails(id: number) {
|
||||
const params = new HttpParams().set('output_format', 'JSON').set('display', 'full');
|
||||
return this.http.get<any>(`${this.base}/products/${id}`, {params}).pipe(
|
||||
map(r => r?.product ?? r),
|
||||
switchMap(p => {
|
||||
let description = Array.isArray(p?.description) ? (p.description[0]?.value ?? '') : (p?.description ?? '');
|
||||
if (description && typeof description === 'string') {
|
||||
return of({
|
||||
id: +p.id,
|
||||
name: Array.isArray(p.name) ? (p.name[0]?.value ?? '') : (p.name ?? ''),
|
||||
description,
|
||||
id_manufacturer: p?.id_manufacturer ? +p.id_manufacturer : undefined,
|
||||
id_supplier: p?.id_supplier ? +p.id_supplier : undefined,
|
||||
id_category_default: p?.id_category_default ? +p.id_category_default : undefined,
|
||||
priceHt: p?.price ? +p.price : 0
|
||||
});
|
||||
}
|
||||
// Fallback XML : extraire <description>…</description> et enlever CDATA si présent
|
||||
return this.http.get(`${this.base}/products/${id}`, {responseType: 'text'}).pipe(
|
||||
map((xml: string) => {
|
||||
const m = xml.match(/<description>[\s\S]*?<language[^>]*>([\s\S]*?)<\/language>[\s\S]*?<\/description>/i);
|
||||
let descXml = m ? m[1] : '';
|
||||
if (descXml.startsWith('<![CDATA[') && descXml.endsWith(']]>')) {
|
||||
descXml = descXml.slice(9, -3);
|
||||
}
|
||||
return {
|
||||
id: +p.id,
|
||||
name: Array.isArray(p.name) ? (p.name[0]?.value ?? '') : (p.name ?? ''),
|
||||
description: descXml,
|
||||
id_manufacturer: p?.id_manufacturer ? +p.id_manufacturer : undefined,
|
||||
id_supplier: p?.id_supplier ? +p.id_supplier : undefined,
|
||||
id_category_default: p?.id_category_default ? +p.id_category_default : undefined,
|
||||
priceHt: p?.price ? +p.price : 0
|
||||
};
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// --- Images
|
||||
private getProductImageIds(productId: number) {
|
||||
return this.http.get<any>(`${this.base}/images/products/${productId}`, {responseType: 'json' as any}).pipe(
|
||||
map(r => {
|
||||
const arr = (r?.image ?? r?.images ?? []) as Array<any>;
|
||||
return Array.isArray(arr) ? arr.map(x => +x.id) : [];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getProductImageUrls(productId: number) {
|
||||
return this.getProductImageIds(productId).pipe(
|
||||
map(ids => ids.map(idImg => `${this.base}/images/products/${productId}/${idImg}`))
|
||||
);
|
||||
}
|
||||
|
||||
uploadProductImage(productId: number, file: File) {
|
||||
const fd = new FormData();
|
||||
fd.append('image', file);
|
||||
return this.http.post(`${this.base}/images/products/${productId}`, fd);
|
||||
}
|
||||
|
||||
// --- Stock (quantité) — sans POST, uniquement PUT
|
||||
/** Lis la quantité (prend exactement la ligne stock_available utilisée par Presta) */
|
||||
getProductQuantity(productId: number) {
|
||||
// 1) on essaie d’abord de récupérer l’id SA via les associations du produit (le plus fiable)
|
||||
return this.http.get(`${this.base}/products/${productId}`, {responseType: 'text'}).pipe(
|
||||
switchMap(xml => {
|
||||
const m = xml.match(
|
||||
/<stock_availables[\s\S]*?<stock_available[^>]*>[\s\S]*?<id[^>]*>\s*(?:<!\[CDATA\[)?(\d+)(?:\]\]>)?\s*<\/id>[\s\S]*?<\/stock_availables>/i
|
||||
);
|
||||
const saId = m ? +m[1] : null;
|
||||
if (saId) {
|
||||
// lire la ligne SA par son id → on lit quantity
|
||||
const params = new HttpParams()
|
||||
.set('display', '[id,quantity]')
|
||||
.set('filter[id]', `${saId}`)
|
||||
.set('output_format', 'JSON');
|
||||
return this.http.get<any>(`${this.base}/stock_availables`, {params}).pipe(
|
||||
map(r => {
|
||||
const sa = (r?.stock_availables ?? [])[0];
|
||||
return sa?.quantity != null ? +sa.quantity : 0;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 2) fallback: rechercher la ligne par id_product_attribute=0 (sans filtre shop → puis avec)
|
||||
let p1 = new HttpParams()
|
||||
.set('display', '[id,quantity,id_product_attribute]')
|
||||
.set('filter[id_product]', `${productId}`)
|
||||
.set('filter[id_product_attribute]', '0')
|
||||
.set('output_format', 'JSON');
|
||||
return this.http.get<any>(`${this.base}/stock_availables`, {params: p1}).pipe(
|
||||
switchMap(r => {
|
||||
const sa = (r?.stock_availables ?? [])[0];
|
||||
if (sa?.quantity != null) return of(+sa.quantity);
|
||||
|
||||
return this.getDefaultShopContext().pipe(
|
||||
switchMap(({idShop, idShopGroup}) => {
|
||||
const p2 = new HttpParams()
|
||||
.set('display', '[id,quantity,id_product_attribute]')
|
||||
.set('filter[id_product]', `${productId}`)
|
||||
.set('filter[id_product_attribute]', '0')
|
||||
.set('filter[id_shop]', `${idShop}`)
|
||||
.set('filter[id_shop_group]', `${idShopGroup}`)
|
||||
.set('output_format', 'JSON');
|
||||
|
||||
return this.http.get<any>(`${this.base}/stock_availables`, {params: p2}).pipe(
|
||||
map(r2 => {
|
||||
const sa2 = (r2?.stock_availables ?? [])[0];
|
||||
return sa2?.quantity != null ? +sa2.quantity : 0;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/** PUT quantité : on cible précisément la ligne SA (avec id_shop/id_shop_group réels) */
|
||||
private setProductQuantity(productId: number, quantity: number) {
|
||||
const q = Math.max(0, Math.trunc(quantity));
|
||||
|
||||
// Helper: fait un PUT en reprenant tous les champs utiles de la ligne SA existante
|
||||
const putFromRow = (row: any) => {
|
||||
const saId = +row.id;
|
||||
const idShop = row.id_shop ? +row.id_shop : undefined;
|
||||
const idShopGroup = row.id_shop_group ? +row.id_shop_group : undefined;
|
||||
|
||||
const extraShop =
|
||||
(idShop != null ? `<id_shop>${idShop}</id_shop>` : '') +
|
||||
(idShopGroup != null ? `<id_shop_group>${idShopGroup}</id_shop_group>` : '');
|
||||
|
||||
const xml = `<prestashop xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<stock_available>
|
||||
<id>${saId}</id>
|
||||
<id_product>${productId}</id_product>
|
||||
<id_product_attribute>0</id_product_attribute>
|
||||
${extraShop}
|
||||
<quantity>${q}</quantity>
|
||||
<depends_on_stock>0</depends_on_stock>
|
||||
<out_of_stock>0</out_of_stock>
|
||||
</stock_available>
|
||||
</prestashop>`;
|
||||
|
||||
return this.http.put(`${this.base}/stock_availables/${saId}`, xml, {
|
||||
headers: this.headersXml,
|
||||
responseType: 'text'
|
||||
}).pipe(map(() => true));
|
||||
};
|
||||
|
||||
// 1) récupérer l’id SA via les associations du produit (le plus fiable)
|
||||
return this.http.get(`${this.base}/products/${productId}`, {responseType: 'text'}).pipe(
|
||||
switchMap(xml => {
|
||||
const m = xml.match(
|
||||
/<stock_availables[\s\S]*?<stock_available[^>]*>[\s\S]*?<id[^>]*>\s*(?:<!\[CDATA\[)?(\d+)(?:\]\]>)?\s*<\/id>[\s\S]*?<\/stock_availables>/i
|
||||
);
|
||||
const saId = m ? +m[1] : null;
|
||||
if (saId) {
|
||||
const params = new HttpParams()
|
||||
.set('display', '[id,id_product,id_product_attribute,id_shop,id_shop_group,quantity]')
|
||||
.set('filter[id]', `${saId}`)
|
||||
.set('output_format', 'JSON');
|
||||
return this.http.get<any>(`${this.base}/stock_availables`, {params}).pipe(
|
||||
switchMap(r => {
|
||||
const row = (r?.stock_availables ?? [])[0];
|
||||
if (row?.id) return putFromRow(row);
|
||||
// si l’id renvoyé n’est pas lisible (peu probable), on bascule sur le fallback ci-dessous
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 2) fallback: rechercher la ligne par id_product_attribute=0 (sans filtre shop → puis avec)
|
||||
let p1 = new HttpParams()
|
||||
.set('display', '[id,id_product,id_product_attribute,id_shop,id_shop_group,quantity]')
|
||||
.set('filter[id_product]', `${productId}`)
|
||||
.set('filter[id_product_attribute]', '0')
|
||||
.set('output_format', 'JSON');
|
||||
|
||||
return this.http.get<any>(`${this.base}/stock_availables`, {params: p1}).pipe(
|
||||
switchMap(r => {
|
||||
const row = (r?.stock_availables ?? [])[0];
|
||||
if (row?.id) return putFromRow(row);
|
||||
|
||||
return this.getDefaultShopContext().pipe(
|
||||
switchMap(({idShop, idShopGroup}) => {
|
||||
const p2 = new HttpParams()
|
||||
.set('display', '[id,id_product,id_product_attribute,id_shop,id_shop_group,quantity]')
|
||||
.set('filter[id_product]', `${productId}`)
|
||||
.set('filter[id_product_attribute]', '0')
|
||||
.set('filter[id_shop]', `${idShop}`)
|
||||
.set('filter[id_shop_group]', `${idShopGroup}`)
|
||||
.set('output_format', 'JSON');
|
||||
|
||||
return this.http.get<any>(`${this.base}/stock_availables`, {params: p2}).pipe(
|
||||
switchMap(r2 => {
|
||||
const row2 = (r2?.stock_availables ?? [])[0];
|
||||
if (row2?.id) return putFromRow(row2);
|
||||
|
||||
console.warn('[Presta] Aucune ligne stock_available PUTtable trouvée pour product:', productId);
|
||||
return of(true); // on ne bloque pas le flux si la ligne est introuvable
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// --- Association fournisseur (product_suppliers)
|
||||
private findProductSupplierId(productId: number, supplierId: number) {
|
||||
const params = new HttpParams()
|
||||
.set('display', '[id,id_product,id_supplier,id_product_attribute]')
|
||||
.set('filter[id_product]', `${productId}`)
|
||||
.set('filter[id_supplier]', `${supplierId}`)
|
||||
.set('filter[id_product_attribute]', '0')
|
||||
.set('output_format', 'JSON');
|
||||
|
||||
return this.http.get<any>(`${this.base}/product_suppliers`, {params}).pipe(
|
||||
map(r => {
|
||||
const row = (r?.product_suppliers ?? [])[0];
|
||||
return row?.id ? +row.id : null;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private upsertProductSupplier(productId: number, supplierId: number) {
|
||||
if (!supplierId) return of(true);
|
||||
|
||||
return forkJoin({
|
||||
curId: this.getDefaultCurrencyId(),
|
||||
psId: this.findProductSupplierId(productId, supplierId),
|
||||
}).pipe(
|
||||
switchMap(({curId, psId}) => {
|
||||
const body = (id?: number) => `<prestashop xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<product_supplier>
|
||||
${id ? `<id>${id}</id>` : ''}
|
||||
<id_product>${productId}</id_product>
|
||||
<id_product_attribute>0</id_product_attribute>
|
||||
<id_supplier>${supplierId}</id_supplier>
|
||||
<id_currency>${curId}</id_currency>
|
||||
<product_supplier_reference></product_supplier_reference>
|
||||
<product_supplier_price_te>0</product_supplier_price_te>
|
||||
</product_supplier>
|
||||
</prestashop>`;
|
||||
|
||||
if (psId) {
|
||||
return this.http.put(`${this.base}/product_suppliers/${psId}`, body(psId), {
|
||||
headers: this.headersXml, responseType: 'text'
|
||||
}).pipe(map(() => true));
|
||||
} else {
|
||||
return this.http.post(`${this.base}/product_suppliers`, body(), {
|
||||
headers: this.headersXml, responseType: 'text'
|
||||
}).pipe(map(() => true));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// --- Create & Update produit
|
||||
private getProductForUpdate(id: number) {
|
||||
const params = new HttpParams().set('output_format', 'JSON').set('display', 'full');
|
||||
return this.http.get<any>(`${this.base}/products/${id}`, {params}).pipe(map(r => r?.product ?? r));
|
||||
}
|
||||
|
||||
createProduct(dto: PsProduct) {
|
||||
const priceHt = this.ttcToHt(dto.priceTtc, dto.vatRate);
|
||||
|
||||
return forkJoin({
|
||||
idLang: this.getDefaultLangId(),
|
||||
idTaxGroup: this.getDefaultTaxRulesGroupId(),
|
||||
shop: this.getDefaultShopContext(),
|
||||
homeCat: this.getHomeCategoryId(),
|
||||
rootCat: this.getRootCategoryId(),
|
||||
}).pipe(
|
||||
switchMap(({idLang, idTaxGroup, shop, homeCat, rootCat}) => {
|
||||
const desc = this.buildAugmentedDescription(dto);
|
||||
|
||||
const xml = `<prestashop xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<product>
|
||||
<id_manufacturer>${dto.manufacturerId}</id_manufacturer>
|
||||
<id_supplier>${dto.supplierId}</id_supplier>
|
||||
<id_category_default>${dto.categoryId}</id_category_default>
|
||||
<id_shop_default>${shop.idShop}</id_shop_default>
|
||||
<id_tax_rules_group>${idTaxGroup}</id_tax_rules_group>
|
||||
|
||||
<type>standard</type>
|
||||
<product_type>standard</product_type>
|
||||
<state>1</state>
|
||||
<minimal_quantity>1</minimal_quantity>
|
||||
|
||||
<price>${priceHt}</price>
|
||||
<active>1</active>
|
||||
<visibility>both</visibility>
|
||||
<available_for_order>1</available_for_order>
|
||||
<show_price>1</show_price>
|
||||
<indexed>1</indexed>
|
||||
|
||||
<name><language id="${idLang}">${this.escapeXml(dto.name)}</language></name>
|
||||
<link_rewrite><language id="${idLang}">${this.escapeXml(this.slug(dto.name))}</language></link_rewrite>
|
||||
<description><language id="${idLang}">${this.escapeXml(desc)}</language></description>
|
||||
|
||||
<associations>
|
||||
<categories>
|
||||
<category><id>${dto.categoryId}</id></category>
|
||||
</categories>
|
||||
</associations>
|
||||
</product>
|
||||
</prestashop>`;
|
||||
|
||||
return this.http.post(`${this.base}/products`, xml, {headers: this.headersXml, responseType: 'text'})
|
||||
.pipe(map((res: string) => this.extractIdFromXml(res)));
|
||||
}),
|
||||
switchMap((productId) => {
|
||||
if (!productId) return of(null);
|
||||
const ops: Array<Observable<unknown>> = [];
|
||||
if (dto.images?.length) ops.push(forkJoin(dto.images.map(f => this.uploadProductImage(productId, f))));
|
||||
ops.push(this.setProductQuantity(productId, dto.quantity));
|
||||
if (dto.supplierId) ops.push(this.upsertProductSupplier(productId, dto.supplierId));
|
||||
return forkJoin(ops).pipe(map(() => productId));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updateProduct(id: number, dto: PsProduct) {
|
||||
const priceHt = this.ttcToHt(dto.priceTtc, dto.vatRate);
|
||||
|
||||
return forkJoin({
|
||||
idLang: this.getDefaultLangId(),
|
||||
prod: this.getProductForUpdate(id),
|
||||
idTaxGroup: this.getDefaultTaxRulesGroupId(),
|
||||
shop: this.getDefaultShopContext(),
|
||||
homeCat: this.getHomeCategoryId(),
|
||||
rootCat: this.getRootCategoryId(),
|
||||
}).pipe(
|
||||
switchMap(({idLang, prod, idTaxGroup, shop, homeCat, rootCat}) => {
|
||||
const active = +prod?.active || 1;
|
||||
|
||||
const lr = Array.isArray(prod?.link_rewrite)
|
||||
? prod.link_rewrite.map((l: any) => ({id: +l.id, value: String(l.value ?? '')}))
|
||||
: [{id: idLang, value: this.slug(dto.name)}];
|
||||
|
||||
const lrXml = `<link_rewrite>${
|
||||
lr.map((e: {
|
||||
id: any;
|
||||
value: string;
|
||||
}) => `<language id="${e.id}">${this.escapeXml(e.value)}</language>`).join('')
|
||||
}</link_rewrite>`;
|
||||
|
||||
const desc = this.buildAugmentedDescription(dto);
|
||||
|
||||
const xml = `<prestashop xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<product>
|
||||
<id>${id}</id>
|
||||
<active>${active}</active>
|
||||
|
||||
<id_manufacturer>${dto.manufacturerId}</id_manufacturer>
|
||||
<id_supplier>${dto.supplierId}</id_supplier>
|
||||
<id_category_default>${dto.categoryId}</id_category_default>
|
||||
<id_shop_default>${shop.idShop}</id_shop_default>
|
||||
<id_tax_rules_group>${idTaxGroup}</id_tax_rules_group>
|
||||
|
||||
<type>standard</type> <!-- NEW -->
|
||||
<product_type>standard</product_type><!-- NEW -->
|
||||
<state>1</state> <!-- NEW -->
|
||||
<minimal_quantity>1</minimal_quantity><!-- NEW -->
|
||||
|
||||
<price>${priceHt}</price>
|
||||
<visibility>both</visibility>
|
||||
<available_for_order>1</available_for_order>
|
||||
<show_price>1</show_price>
|
||||
<indexed>1</indexed>
|
||||
|
||||
<name><language id="${idLang}">${this.escapeXml(dto.name)}</language></name>
|
||||
${lrXml}
|
||||
<description><language id="${idLang}">${this.escapeXml(desc)}</language></description>
|
||||
|
||||
<associations>
|
||||
<categories>
|
||||
<category><id>${dto.categoryId}</id></category>
|
||||
</categories>
|
||||
</associations>
|
||||
</product>
|
||||
</prestashop>`;
|
||||
|
||||
return this.http.put(`${this.base}/products/${id}`, xml, {headers: this.headersXml, responseType: 'text'})
|
||||
.pipe(map(() => id));
|
||||
}),
|
||||
switchMap(productId => {
|
||||
const ops: Array<Observable<unknown>> = [this.setProductQuantity(productId, dto.quantity)];
|
||||
if (dto.supplierId) ops.push(this.upsertProductSupplier(productId, dto.supplierId));
|
||||
return forkJoin(ops).pipe(map(() => true));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deleteProduct(id: number) {
|
||||
return this.http.delete(`${this.base}/products/${id}`, {responseType: 'text'}).pipe(map(() => true));
|
||||
}
|
||||
|
||||
// -------- Description augmentée
|
||||
private buildAugmentedDescription(dto: PsProduct): string {
|
||||
const parts: string[] = [];
|
||||
if (dto.description?.trim()) parts.push(dto.description.trim());
|
||||
|
||||
const flags: string[] = [];
|
||||
flags.push(`Complet: ${dto.complete ? 'Oui' : 'Non'}`);
|
||||
flags.push(`Notice: ${dto.hasManual ? 'Oui' : 'Non'}`);
|
||||
if (dto.conditionLabel) flags.push(`État: ${dto.conditionLabel}`);
|
||||
|
||||
parts.push('', flags.join(' | '));
|
||||
return parts.join('\n').trim();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user