u
This commit is contained in:
164
scratch/test-variants.js
Normal file
164
scratch/test-variants.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import LibDashStore from '../src/index.js';
|
||||
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||
|
||||
async function run() {
|
||||
console.log('--- Testing POS father/child variants and options ---');
|
||||
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'test-variants-'));
|
||||
const dbFile = join(tempDir, 'test.db');
|
||||
const storageDir = join(tempDir, 'storage');
|
||||
|
||||
const provider = new BetterSqlite3Provider({
|
||||
filename: dbFile,
|
||||
storageDir,
|
||||
});
|
||||
|
||||
await provider.initialize();
|
||||
const store = await new LibDashStore(provider);
|
||||
|
||||
try {
|
||||
const googleAuthId = await store.google_auth.add('tester@example.com');
|
||||
const company = await store.company.create({
|
||||
name: 'Variant Corp',
|
||||
iso639: 'de',
|
||||
taxrate: 19,
|
||||
reducedtaxrate: 7,
|
||||
googleAuthId,
|
||||
});
|
||||
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||
|
||||
// 1. Create a category
|
||||
const cat = await store.category.createUnderParent({
|
||||
googleAuthId,
|
||||
parentId: company.rootCategoryId,
|
||||
name: 'Kleidung',
|
||||
});
|
||||
|
||||
// 2. Create terminal and enable category
|
||||
const terminal = await store.posterminal.upsert(googleAuthId, {
|
||||
name: 'POS 1',
|
||||
pairingpin: '1234',
|
||||
});
|
||||
await store.posterminal.pair(terminal.id, {
|
||||
token: 'pos-token-xyz',
|
||||
clientserial: 'SN-001',
|
||||
pairedAt: new Date().toISOString(),
|
||||
});
|
||||
await store.posterminal.setOn(googleAuthId, terminal.id, cat.id);
|
||||
|
||||
// 3. Create attribute name "Farbe"
|
||||
const attrName = await store.attributename.create(googleAuthId, {
|
||||
name: 'Farbe',
|
||||
sort: 1,
|
||||
exclusive: true,
|
||||
});
|
||||
|
||||
// 4. Create base article and convert to parent
|
||||
const tShirt = await store.article.createUnderParent({
|
||||
googleAuthId,
|
||||
parentId: cat.id,
|
||||
name: 'T-Shirt Basic',
|
||||
sku: 'TSHIRT',
|
||||
});
|
||||
await store.article.toggleParent(googleAuthId, tShirt.sku, {
|
||||
isParent: true,
|
||||
attributeNameId: attrName.id,
|
||||
});
|
||||
|
||||
// 5. Add children "Rot" and "Blau"
|
||||
console.log('Adding children "Rot" and "Blau"...');
|
||||
const childRot = await store.article.addChild(googleAuthId, tShirt.sku, {
|
||||
value: 'Rot',
|
||||
name: 'T-Shirt Basic - Rot',
|
||||
});
|
||||
const childBlau = await store.article.addChild(googleAuthId, tShirt.sku, {
|
||||
value: 'Blau',
|
||||
name: 'T-Shirt Basic - Blau',
|
||||
});
|
||||
|
||||
// 6. Query POS products
|
||||
let prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
|
||||
let parentProd = prods.find((p) => p.sku === 'TSHIRT');
|
||||
let rotProd = prods.find((p) => p.sku === childRot.article.sku);
|
||||
let blauProd = prods.find((p) => p.sku === childBlau.article.sku);
|
||||
|
||||
console.log('Parent product:', {
|
||||
sku: parentProd.sku,
|
||||
is_parent: parentProd.is_parent,
|
||||
parent: parentProd.parent,
|
||||
variants: parentProd.variants,
|
||||
options: parentProd.options,
|
||||
});
|
||||
|
||||
assert.equal(parentProd.is_parent, '1');
|
||||
assert.equal(parentProd.parent, '0');
|
||||
assert.equal(parentProd.variants, '');
|
||||
assert.deepEqual(parentProd.options, [{ name: 'Farbe', value: 'Rot,Blau' }]);
|
||||
|
||||
console.log('Child Rot:', {
|
||||
sku: rotProd.sku,
|
||||
is_parent: rotProd.is_parent,
|
||||
parent: rotProd.parent,
|
||||
variants: rotProd.variants,
|
||||
sort: rotProd.sort,
|
||||
options: rotProd.options,
|
||||
});
|
||||
|
||||
assert.equal(rotProd.is_parent, '0');
|
||||
assert.equal(rotProd.parent, String(parentProd._id));
|
||||
assert.equal(rotProd.variants, 'Rot');
|
||||
assert.equal(rotProd.sort, '0');
|
||||
assert.equal(rotProd.options, null);
|
||||
|
||||
console.log('Child Blau:', {
|
||||
sku: blauProd.sku,
|
||||
is_parent: blauProd.is_parent,
|
||||
parent: blauProd.parent,
|
||||
variants: blauProd.variants,
|
||||
sort: blauProd.sort,
|
||||
options: blauProd.options,
|
||||
});
|
||||
|
||||
assert.equal(blauProd.is_parent, '0');
|
||||
assert.equal(blauProd.parent, String(parentProd._id));
|
||||
assert.equal(blauProd.variants, 'Blau');
|
||||
assert.equal(blauProd.sort, '1');
|
||||
assert.equal(blauProd.options, null);
|
||||
|
||||
// 7. Add third child "Grün" and verify options list update
|
||||
console.log('Adding third child "Grün"...');
|
||||
const childGruen = await store.article.addChild(googleAuthId, tShirt.sku, {
|
||||
value: 'Grün',
|
||||
name: 'T-Shirt Basic - Grün',
|
||||
});
|
||||
|
||||
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
|
||||
parentProd = prods.find((p) => p.sku === 'TSHIRT');
|
||||
assert.deepEqual(parentProd.options, [{ name: 'Farbe', value: 'Rot,Blau,Grün' }]);
|
||||
console.log('Updated parent options:', parentProd.options);
|
||||
|
||||
// 8. Remove child "Blau" and verify options list update
|
||||
console.log('Removing child "Blau"...');
|
||||
await store.article.removeChild(googleAuthId, tShirt.sku, childBlau.article.sku, { deleteArticle: true });
|
||||
|
||||
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
|
||||
parentProd = prods.find((p) => p.sku === 'TSHIRT');
|
||||
assert.deepEqual(parentProd.options, [{ name: 'Farbe', value: 'Rot,Grün' }]);
|
||||
console.log('Parent options after removal:', parentProd.options);
|
||||
|
||||
console.log('✅ ALL FATHER/CHILD VARIANT TESTS PASSED');
|
||||
} finally {
|
||||
try {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error('Test failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -117,7 +117,8 @@ WHERE id = @articleId
|
||||
export const TOGGLE_PARENT = `
|
||||
UPDATE article
|
||||
SET is_parent = @isParent,
|
||||
parentattributename_id = @parentAttributeNameId
|
||||
parentattributename_id = @parentAttributeNameId,
|
||||
updated_at = SYSUTCDATETIME()
|
||||
WHERE id = @articleId
|
||||
`;
|
||||
|
||||
|
||||
@@ -288,13 +288,15 @@ SELECT TOP (@limit)
|
||||
) AS barcode,
|
||||
a.is_parent AS isParent,
|
||||
a.parentarticle_id AS parentArticleId,
|
||||
a.parentsort AS parentsort,
|
||||
(
|
||||
SELECT TOP 1 CONCAT(fan.name, '|', av.value, '|', fan.sort, '|', a.parentsort)
|
||||
FROM article fa
|
||||
INNER JOIN attributename fan ON fan.id = fa.parentattributename_id
|
||||
INNER JOIN articleattributevalue aav ON aav.article_id = a.id
|
||||
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = fan.id
|
||||
WHERE a.parentarticle_id IS NOT NULL AND fa.id = a.parentarticle_id
|
||||
SELECT TOP 1 av.value
|
||||
FROM articleattributevalue aav
|
||||
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id
|
||||
LEFT JOIN article fa ON fa.id = a.parentarticle_id
|
||||
WHERE aav.article_id = a.id
|
||||
AND (fa.parentattributename_id IS NULL OR av.attribute_id = fa.parentattributename_id)
|
||||
ORDER BY CASE WHEN av.attribute_id = fa.parentattributename_id THEN 0 ELSE 1 END, aav.sort ASC, aav.id ASC
|
||||
) AS variantName,
|
||||
CASE
|
||||
WHEN a.articlelist_id IS NOT NULL AND EXISTS (
|
||||
@@ -502,6 +504,21 @@ LEFT JOIN tax opt_ctax ON opt_ctax.id = co.taxrate
|
||||
ORDER BY ola.sort ASC, ola.id ASC
|
||||
`;
|
||||
|
||||
export const LIST_POS_PARENT_OPTIONS = `
|
||||
SELECT
|
||||
fa.id AS parentArticleId,
|
||||
COALESCE(fan.name, 'Variante') AS attributeName,
|
||||
av.value AS variantValue
|
||||
FROM article fa
|
||||
INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = fa.company_id
|
||||
INNER JOIN article child ON child.parentarticle_id = fa.id
|
||||
LEFT JOIN attributename fan ON fan.id = fa.parentattributename_id
|
||||
LEFT JOIN articleattributevalue aav ON aav.article_id = child.id
|
||||
LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id AND (fa.parentattributename_id IS NULL OR av.attribute_id = fa.parentattributename_id)
|
||||
WHERE fa.is_parent = 1
|
||||
ORDER BY fa.id ASC, child.parentsort ASC, child.id ASC
|
||||
`;
|
||||
|
||||
export const RECORD_OBITUARY = `
|
||||
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
|
||||
SELECT pt.id, @entityType, @entityId
|
||||
|
||||
@@ -9,7 +9,7 @@ export const {
|
||||
FIND_BY_TOKEN, COUNT_PRODUCTS, COUNT_CATEGORIES, COUNT_CUSTOMERS,
|
||||
COUNT_CUSTOMER_GROUPS, COUNT_COMPOSITE_PRODUCTS, MAX_ORDER_ID,
|
||||
LIST_POS_PRODUCT_CATEGORIES, LIST_POS_CUSTOMER_GROUP_IDS, LIST_POS_PRODUCT_PRICES,
|
||||
LIST_POS_MANDATED_OPTIONS,
|
||||
LIST_POS_MANDATED_OPTIONS, LIST_POS_PARENT_OPTIONS,
|
||||
TOUCH_LASTSEEN, RECORD_OBITUARY, COUNT_DELETED_ENTITIES,
|
||||
} = queries;
|
||||
|
||||
@@ -176,13 +176,15 @@ SELECT
|
||||
) AS barcode,
|
||||
a.is_parent AS isParent,
|
||||
a.parentarticle_id AS parentArticleId,
|
||||
a.parentsort AS parentsort,
|
||||
(
|
||||
SELECT fan.name || '|' || av.value || '|' || fan.sort || '|' || a.parentsort
|
||||
FROM article fa
|
||||
INNER JOIN attributename fan ON fan.id = fa.parentattributename_id
|
||||
INNER JOIN articleattributevalue aav ON aav.article_id = a.id
|
||||
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = fan.id
|
||||
WHERE a.parentarticle_id IS NOT NULL AND fa.id = a.parentarticle_id
|
||||
SELECT av.value
|
||||
FROM articleattributevalue aav
|
||||
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id
|
||||
LEFT JOIN article fa ON fa.id = a.parentarticle_id
|
||||
WHERE aav.article_id = a.id
|
||||
AND (fa.parentattributename_id IS NULL OR av.attribute_id = fa.parentattributename_id)
|
||||
ORDER BY CASE WHEN av.attribute_id = fa.parentattributename_id THEN 0 ELSE 1 END, aav.sort ASC, aav.id ASC
|
||||
LIMIT 1
|
||||
) AS variantName,
|
||||
CASE
|
||||
|
||||
@@ -271,6 +271,9 @@ export default function addChild(provider) {
|
||||
await initArticleWarehouseSummary(runner, { companyId, articleId: childId });
|
||||
}
|
||||
|
||||
// Touch parent article updated_at so POS and other consumers refresh parent options
|
||||
await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId });
|
||||
|
||||
return load(googleAuthId, childSku, runner);
|
||||
};
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ export default function removeChild(provider) {
|
||||
childSku,
|
||||
});
|
||||
|
||||
// Touch parent article updated_at so POS and other consumers refresh parent options
|
||||
await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
parentSku,
|
||||
@@ -131,6 +134,9 @@ export default function removeChild(provider) {
|
||||
parentSku,
|
||||
});
|
||||
|
||||
// Touch parent article updated_at so POS and other consumers refresh parent options
|
||||
await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
parentSku,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
LIST_POS_CUSTOMER_GROUP_IDS as MSSQL_CG_IDS,
|
||||
LIST_POS_PRODUCT_PRICES as MSSQL_PRICES,
|
||||
LIST_POS_MANDATED_OPTIONS as MSSQL_MANDATED_OPTIONS,
|
||||
LIST_POS_PARENT_OPTIONS as MSSQL_PARENT_OPTIONS,
|
||||
} from '../../providers/mssql/queries/posterminal.js';
|
||||
import {
|
||||
LIST_POS_PRODUCTS as SQLITE_PRODUCTS,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
LIST_POS_CUSTOMER_GROUP_IDS as SQLITE_CG_IDS,
|
||||
LIST_POS_PRODUCT_PRICES as SQLITE_PRICES,
|
||||
LIST_POS_MANDATED_OPTIONS as SQLITE_MANDATED_OPTIONS,
|
||||
LIST_POS_PARENT_OPTIONS as SQLITE_PARENT_OPTIONS,
|
||||
} from '../../providers/sqlite3/queries/posterminal.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
@@ -39,6 +41,7 @@ export default function getProductList(provider) {
|
||||
const cgIdsSql = isSqlite ? SQLITE_CG_IDS : MSSQL_CG_IDS;
|
||||
const pricesSql = isSqlite ? SQLITE_PRICES : MSSQL_PRICES;
|
||||
const mandatedOptionsSql = isSqlite ? SQLITE_MANDATED_OPTIONS : MSSQL_MANDATED_OPTIONS;
|
||||
const parentOptionsSql = isSqlite ? SQLITE_PARENT_OPTIONS : MSSQL_PARENT_OPTIONS;
|
||||
|
||||
/**
|
||||
* Lists products for a POS terminal modified after the cursor.
|
||||
@@ -55,7 +58,7 @@ export default function getProductList(provider) {
|
||||
const runner = makeRunner(provider, tx);
|
||||
const pid = Number(posterminalId);
|
||||
|
||||
const [prodRes, catRes, cgRes, priceRes, optRes] = await Promise.all([
|
||||
const [prodRes, catRes, cgRes, priceRes, optRes, parentOptRes] = await Promise.all([
|
||||
runner.query(productsSql, {
|
||||
posterminalId: pid,
|
||||
cursor: normalizeCursor(cursor),
|
||||
@@ -65,6 +68,7 @@ export default function getProductList(provider) {
|
||||
runner.query(cgIdsSql, { posterminalId: pid }),
|
||||
runner.query(pricesSql, { posterminalId: pid }),
|
||||
runner.query(mandatedOptionsSql, { posterminalId: pid }),
|
||||
runner.query(parentOptionsSql, { posterminalId: pid }),
|
||||
]);
|
||||
|
||||
const categoriesByArticle = new Map();
|
||||
@@ -101,6 +105,21 @@ export default function getProductList(provider) {
|
||||
}
|
||||
}
|
||||
|
||||
const parentOptionsMap = new Map();
|
||||
for (const row of parentOptRes.rows) {
|
||||
if (!row.variantValue) continue;
|
||||
if (!parentOptionsMap.has(row.parentArticleId)) {
|
||||
parentOptionsMap.set(row.parentArticleId, {
|
||||
name: row.attributeName || 'Variante',
|
||||
values: [],
|
||||
});
|
||||
}
|
||||
const opt = parentOptionsMap.get(row.parentArticleId);
|
||||
if (!opt.values.includes(row.variantValue)) {
|
||||
opt.values.push(row.variantValue);
|
||||
}
|
||||
}
|
||||
|
||||
const customerGroupIds = cgRes.rows.map((row) => row.id);
|
||||
|
||||
return prodRes.rows.map((product) => {
|
||||
@@ -127,6 +146,24 @@ export default function getProductList(provider) {
|
||||
const depositName = mandatedOpt ? mandatedOpt.names.join(' + ') : '';
|
||||
const depositPrice = mandatedOpt ? mandatedOpt.totalPrice.toFixed(2) : '0.0';
|
||||
|
||||
const isParent = Boolean(product.isParent);
|
||||
const parentArticleId = Number(product.parentArticleId || 0);
|
||||
const isChild = parentArticleId > 0;
|
||||
|
||||
const parentOpt = parentOptionsMap.get(product.id);
|
||||
const options = isParent && parentOpt && parentOpt.values.length > 0
|
||||
? [
|
||||
{
|
||||
name: parentOpt.name,
|
||||
value: parentOpt.values.join(','),
|
||||
},
|
||||
]
|
||||
: null;
|
||||
|
||||
const sortValue = isChild
|
||||
? String(product.parentsort ?? 0)
|
||||
: String(product.sort ?? 0);
|
||||
|
||||
return {
|
||||
_id: String(product.id),
|
||||
imghash: product.imgHash ?? null,
|
||||
@@ -138,15 +175,16 @@ export default function getProductList(provider) {
|
||||
price: basePrice,
|
||||
created_at: formatDateTime(product.createdAt),
|
||||
lastChanged: String(product.lastChanged),
|
||||
sort: String(product.sort ?? 0),
|
||||
sort: sortValue,
|
||||
categories_id: categoryIds[0] ?? '0',
|
||||
categories: categoryIds.map((categoryId) => ({ categoryId })),
|
||||
prices,
|
||||
is_parent: product.isParent ? '1' : '0',
|
||||
parent: product.parentArticleId > 0 ? String(product.parentArticleId) : '0',
|
||||
variants: product.variantName ?? '',
|
||||
is_parent: isParent ? '1' : '0',
|
||||
parent: isChild ? String(parentArticleId) : '0',
|
||||
variants: isChild ? (product.variantName ?? '') : '',
|
||||
isCompositeProduct: product.isCompositeProduct,
|
||||
attributes: [],
|
||||
options,
|
||||
use_stock: product.trackStock ? '1' : '0',
|
||||
quantity: String(product.fBestand ?? 0),
|
||||
deposit: hasDeposit ? '1' : '0',
|
||||
|
||||
Reference in New Issue
Block a user