Files
dashback/src/services/companyImport/pfandImporter.js
2026-10-16 00:59:31 +02:00

323 lines
9.8 KiB
JavaScript

// MARK: - Imports
// #region Imports
import { cleanValue } from './csvParser.js';
import { createLogger } from '../../util/logger.js';
// #endregion
const log = createLogger('import');
const PFAND_REGEX = /pfand\s*:\s*([0-9]+(?:[.,][0-9]+)?)/i;
// MARK: - Helpers
// #region Helpers
/**
* Parses the Pfand deposit value from an attributes string.
* E.g. "pfand:0.08" -> 0.08, "pfand:0,25" -> 0.25.
*
* @param {string} attrStr
* @returns {number|null}
*/
export function extractPfandValue(attrStr) {
if (!attrStr) return null;
const match = String(attrStr).match(PFAND_REGEX);
if (!match) return null;
const num = parseFloat(match[1].replace(',', '.'));
return Number.isFinite(num) && num > 0 ? num : null;
}
/**
* Formats standard Pfand SKU: e.g. PFAND-0.08, PFAND-0.15, PFAND-0.25.
*
* @param {number} val
* @returns {string}
*/
export function formatPfandSku(val) {
return `PFAND-${Number(val).toFixed(2)}`;
}
/**
* Formats standard Pfand article name: e.g. "Pfand 0,08 €", "Pfand 0,15 €", "Pfand 0,25 €".
*
* @param {number} val
* @returns {string}
*/
export function formatPfandName(val) {
return `Pfand ${Number(val).toFixed(2).replace('.', ',')} €`;
}
// #endregion
// MARK: - Pfand Setup & Linking Helpers
// #region Pfand Setup & Linking Helpers
/**
* Pre-creates the Pfand category and Pfand articles upfront.
*
* @param {object} params
* @param {object} params.store - Store singleton
* @param {number} params.googleAuthId
* @param {number} params.hauptDashId
* @param {Array<Record<string, string>>} params.includedArticles
* @returns {Promise<{ pfandArticlesCreated: number, pfandArticlesUpdated: number, pfandCatId: number|null, uniqueValues: number[] }>}
*/
export async function setupPfandArticles({
store,
googleAuthId,
hauptDashId,
includedArticles,
}) {
// 1. Discover unique Pfand values from all included articles (checking attributes)
const uniqueValuesSet = new Set();
for (const a of includedArticles) {
const val = extractPfandValue(a.attributes);
if (val != null) {
uniqueValuesSet.add(val);
}
}
if (uniqueValuesSet.size === 0) {
log.info('No pfand attributes detected among articles.');
return {
pfandArticlesCreated: 0,
pfandArticlesUpdated: 0,
pfandCatId: null,
uniqueValues: [],
};
}
const uniqueValues = Array.from(uniqueValuesSet).sort((a, b) => a - b);
log.info(`Discovered ${uniqueValues.length} unique Pfand values: ${uniqueValues.join(', ')}`);
// 2. Resolve or create "Pfand" subcategory under "Haupt"
let pfandCatId = null;
try {
const treeResult = await store.category.getTreeForCompany(googleAuthId);
const existingTree = treeResult.tree || [];
const existingPfandCat = existingTree.find(
(c) => c.parentId === hauptDashId && String(c.name || '').trim().toLowerCase() === 'pfand'
);
if (existingPfandCat) {
pfandCatId = existingPfandCat.id;
} else {
const created = await store.category.createUnderParent({
googleAuthId,
parentId: hauptDashId,
name: 'Pfand',
});
pfandCatId = created.id;
log.info(`Created "Pfand" category under Haupt (id: ${pfandCatId})`);
}
} catch (err) {
log.warn('Could not resolve or create Pfand category under Haupt, falling back to Haupt:', err.message);
pfandCatId = hauptDashId;
}
// 3. Create or update Pfand articles
let pfandArticlesCreated = 0;
let pfandArticlesUpdated = 0;
for (const val of uniqueValues) {
const pfandSku = formatPfandSku(val);
const pfandName = formatPfandName(val);
let existingArticle = null;
try {
const res = await store.article.getBySku(googleAuthId, pfandSku);
existingArticle = res.article;
} catch {
existingArticle = null;
}
try {
if (existingArticle) {
await store.article.updateBySku(googleAuthId, pfandSku, {
name: pfandName,
track_stock: 0,
});
await store.article.setPricesBySku(googleAuthId, pfandSku, [
{ gross: val, tax: 19 },
]);
pfandArticlesUpdated++;
log.info(`Updated existing Pfand article: ${pfandSku} ("${pfandName}")`);
} else {
await store.article.createUnderParent({
googleAuthId,
parentId: pfandCatId,
name: pfandName,
sku: pfandSku,
track_stock: 0,
});
await store.article.setPricesBySku(googleAuthId, pfandSku, [
{ gross: val, tax: 19 },
]);
pfandArticlesCreated++;
log.info(`Created new Pfand article: ${pfandSku} ("${pfandName}")`);
}
} catch (err) {
log.warn(`Failed to create or update Pfand article ${pfandSku}:`, err.message);
}
}
return {
pfandArticlesCreated,
pfandArticlesUpdated,
pfandCatId,
uniqueValues,
};
}
/**
* Links an article to its mandatory Pfand deposit option.
*
* @param {object} params
* @param {object} params.store
* @param {number} params.googleAuthId
* @param {string} params.sku
* @param {number} params.pfandVal
* @returns {Promise<boolean>}
*/
export async function linkArticlePfand({
store,
googleAuthId,
sku,
pfandVal,
}) {
if (!sku || pfandVal == null) return false;
const pfandSku = formatPfandSku(pfandVal);
let existingOptions = [];
try {
existingOptions = (await store.article.getOptionslist(googleAuthId, sku)) || [];
} catch {
existingOptions = [];
}
const otherOptions = existingOptions.filter((o) => !o.sku?.startsWith('PFAND-'));
const newOptions = [
...otherOptions.map((o, idx) => ({
sku: o.sku,
articleId: o.articleId,
count: o.count || 1,
sort: idx,
mandated: Boolean(o.mandated),
})),
{
sku: pfandSku,
count: 1,
sort: otherOptions.length,
mandated: true,
},
];
try {
await store.article.setOptionslist(googleAuthId, sku, newOptions);
return true;
} catch (err) {
log.warn(`Failed to link Pfand option ${pfandSku} to article ${sku}:`, err.message);
return false;
}
}
// #endregion
// MARK: - Pfand Importer
// #region Pfand Importer
/**
* Imports Pfand deposit articles and links them as mandatory options to beverages/articles.
*
* @param {object} params
* @param {object} params.store - Store singleton
* @param {number} params.googleAuthId
* @param {Array<Record<string, string>>} params.includedArticles
* @param {number} params.hauptDashId
* @param {Set<string>} [params.parentSkus] - Set of SKUs that are parent articles (which cannot have options)
* @returns {Promise<{ pfandArticlesCreated: number, pfandArticlesUpdated: number, pfandOptionsLinked: number }>}
*/
export async function importPfand({
store,
googleAuthId,
includedArticles,
hauptDashId,
parentSkus = new Set(),
}) {
// 1. Identify parent articles and map their Pfand values so child variants can inherit
const parentPfandMap = new Map();
for (const a of includedArticles) {
const sku = cleanValue(a.sku ?? a.cArtNr);
const isParent = a.is_parent === '1' || a.nIstVater === '1' || (sku && parentSkus.has(sku));
if (isParent) {
const val = extractPfandValue(a.attributes);
if (val != null) {
parentPfandMap.set(String(a.id ?? a.kArtikel), val);
}
}
}
// 2. Discover target articles (purchasable standalone or variant items) and their Pfand values
const articleTargets = [];
for (const a of includedArticles) {
const sku = cleanValue(a.sku ?? a.cArtNr);
if (!sku) continue;
const isParent = a.is_parent === '1' || a.nIstVater === '1' || parentSkus.has(sku);
if (isParent) {
// Parent articles cannot have options in Dash
continue;
}
let pfandVal = extractPfandValue(a.attributes);
const parentId = cleanValue(a.parentid ?? a.kVaterArtikel);
if (pfandVal == null && parentId && parentId !== '0') {
if (parentPfandMap.has(parentId)) {
pfandVal = parentPfandMap.get(parentId);
}
}
if (pfandVal != null) {
articleTargets.push({ sku, pfandVal });
}
}
if (articleTargets.length === 0) {
log.info('No pfand attributes detected among articles.');
return {
pfandArticlesCreated: 0,
pfandArticlesUpdated: 0,
pfandOptionsLinked: 0,
};
}
// 3. Setup Pfand category and Pfand articles
const setupResult = await setupPfandArticles({
store,
googleAuthId,
hauptDashId,
includedArticles,
});
// 4. Link each target article to its Pfand article as a mandatory option
let pfandOptionsLinked = 0;
for (const { sku, pfandVal } of articleTargets) {
const linked = await linkArticlePfand({
store,
googleAuthId,
sku,
pfandVal,
});
if (linked) pfandOptionsLinked++;
}
log.info(`Pfand import complete: ${setupResult.pfandArticlesCreated} created, ${setupResult.pfandArticlesUpdated} updated, ${pfandOptionsLinked} linked as mandatory options.`);
return {
pfandArticlesCreated: setupResult.pfandArticlesCreated,
pfandArticlesUpdated: setupResult.pfandArticlesUpdated,
pfandOptionsLinked,
};
}
// #endregion