This commit is contained in:
seb
2026-07-14 23:30:16 +02:00
parent 245fb047d7
commit 262fa63f1b
109 changed files with 21566 additions and 367 deletions

View File

@@ -1,6 +1,6 @@
import { sendJson } from '../http.js';
import { getMaxExternalId } from '../order-log.js';
import { getCategoryCount } from '../queries/category-count.js';
import { getMaxOrderIdCount } from '../queries/max-order-id.js';
import { getCompositeProductCount } from '../queries/composite-product-count.js';
import { getCustomerGroupCount } from '../queries/customer-groups.js';
import { getDeletedEntityCount } from '../queries/deleted-entity-count.js';
@@ -16,12 +16,13 @@ export async function handle(_req, res, { url }) {
const compositeProductCursor = Number(url.searchParams.get('lastChangedCompositeProduct')) || 0;
const deletedEntityCursor = Number(url.searchParams.get('lastChangedDeletedEntity')) || 0;
const [productCount, categoryCount, customerGroupCount, compositeProductCount, deletedEntityCount] = await Promise.all([
const [productCount, categoryCount, customerGroupCount, compositeProductCount, deletedEntityCount, maxOrderIdCount] = await Promise.all([
getProductCount({ cursor: productCursor }),
getCategoryCount({ cursor: categoryCursor }),
getCustomerGroupCount({ cursor: customerGroupCursor }),
getCompositeProductCount({ cursor: compositeProductCursor }),
getDeletedEntityCount({ cursor: deletedEntityCursor }),
getMaxOrderIdCount(),
]);
return sendJson(res, 200, {
@@ -34,6 +35,6 @@ export async function handle(_req, res, { url }) {
configurationGroup_count: '0',
configurationItem_count: '0',
deletedEntity_count: String(deletedEntityCount),
max_orderId_count: getMaxExternalId(),
max_orderId_count: String(maxOrderIdCount),
});
}

View File

@@ -7,20 +7,13 @@ fs.mkdirSync(path.dirname(ORDER_LOG_FILE), { recursive: true });
const stream = fs.createWriteStream(ORDER_LOG_FILE, { flags: 'a' });
let orderSequence = 0;
let maxExternalId = 0;
export function logOrder(order) {
orderSequence += 1;
maxExternalId += 1;
const externalId = order?.externalId ?? '';
const line = `${new Date().toISOString()} #${orderSequence} externalId=${maxExternalId} ${JSON.stringify(order)}\n`;
const line = `${new Date().toISOString()} #${orderSequence} externalId=${externalId} ${JSON.stringify(order)}\n`;
stream.write(line);
return maxExternalId;
}
export function getMaxExternalId() {
return String(maxExternalId);
}
export function closeOrderLog() {

View File

@@ -182,7 +182,7 @@ async function nextCustomerNumber(transaction) {
* the search tables). Columns of the table type are named explicitly because
* their order differs between JTL versions.
*/
async function createCustomer(transaction, { customerNumber, address, isPosCustomer, defaults }) {
async function createCustomer(transaction, { customerNumber, address, defaults }) {
const a = address || {};
const iso = (a.countryIso || 'DE').toUpperCase();
const kKundengruppe = Number(a.customerGroupId) || defaults.kKundengruppe;
@@ -209,8 +209,8 @@ async function createCustomer(transaction, { customerNumber, address, isPosCusto
.input('kSprache', sql.Int, config.kSprache)
.input('cISO', sql.NVarChar, iso)
.input('cBundesland', sql.NVarChar, a.state || '')
.input('cHerkunft', sql.NVarChar, isPosCustomer ? 'Kasse' : 'POS-API')
.input('cKassenKunde', sql.Char(1), isPosCustomer ? 'Y' : 'N')
.input('cHerkunft', sql.NVarChar, 'Kasse')
.input('cKassenKunde', sql.Char(1), 'Y')
.input('nDebitorennr', sql.Int, Number(a.debtorNumber) || 0)
.query(`
DECLARE @returnValue INT;
@@ -240,46 +240,55 @@ async function createCustomer(transaction, { customerNumber, address, isPosCusto
return { kKunde, kKundengruppe };
}
function isWalkInOrder(order) {
const customerNumber = String(order.customerNumber || '').trim();
if (!customerNumber || customerNumber === '0') return true;
const billing = order.billingAddress || {};
return (billing.lastName || '') === 'Laufkunde' && !(billing.firstName || '').trim();
}
async function lookupKassenkunde(transaction, defaults) {
const result = await new sql.Request(transaction).query(
"SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKassenKunde = 'Y' ORDER BY kKunde"
);
if (result.recordset[0]) {
return {
kKunde: result.recordset[0].kKunde,
kKundengruppe: result.recordset[0].kKundenGruppe || defaults.kKundengruppe,
};
}
return null;
}
/**
* Resolves the customer by cKundenNr; creates the Kunde on the fly when it
* does not exist. Orders without a customer number use (or create) the
* Kassenkunde (walk-in customer).
* does not exist. Walk-in orders use (or create) the Kassenkunde.
*/
async function resolveCustomer(transaction, order, defaults) {
const customerNumber = String(order.customerNumber || '').trim();
if (customerNumber) {
const result = await new sql.Request(transaction)
.input('cKundenNr', sql.NVarChar, customerNumber)
.query('SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKundenNr = @cKundenNr');
if (result.recordset[0]) {
return {
kKunde: result.recordset[0].kKunde,
kKundengruppe: result.recordset[0].kKundenGruppe || defaults.kKundengruppe,
};
}
if (isWalkInOrder(order)) {
const kassenKunde = await lookupKassenkunde(transaction, defaults);
if (kassenKunde) return kassenKunde;
return createCustomer(transaction, {
customerNumber,
customerNumber: await nextCustomerNumber(transaction),
address: order.billingAddress,
isPosCustomer: false,
defaults,
});
}
const kassenKunde = await new sql.Request(transaction).query(
"SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKassenKunde = 'Y' ORDER BY kKunde"
);
if (kassenKunde.recordset[0]) {
const customerNumber = String(order.customerNumber || '').trim();
const result = await new sql.Request(transaction)
.input('cKundenNr', sql.NVarChar, customerNumber)
.query('SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKundenNr = @cKundenNr');
if (result.recordset[0]) {
return {
kKunde: kassenKunde.recordset[0].kKunde,
kKundengruppe: kassenKunde.recordset[0].kKundenGruppe || defaults.kKundengruppe,
kKunde: result.recordset[0].kKunde,
kKundengruppe: result.recordset[0].kKundenGruppe || defaults.kKundengruppe,
};
}
return createCustomer(transaction, {
customerNumber: await nextCustomerNumber(transaction),
customerNumber,
address: order.billingAddress,
isPosCustomer: true,
defaults,
});
}
@@ -364,11 +373,41 @@ async function insertOrderItem(transaction, kAuftrag, item) {
}
/**
* Links a JTL-Wawi order (and its positions) back to the originating
* POS order/position ids via Pos.tAuftragMapping / Pos.tAuftragPositionMapping,
* so the POS system can recognise orders it already pushed into JTL-Wawi.
* JTL PosOrderCreationService.CheckIfOrderExists — skip re-import when the POS
* order id is already mapped to a Wawi order for this subshop.
*/
async function insertPosOrderMapping(transaction, kAuftrag, kPosAuftrag) {
async function findExistingPosOrderMapping(kPosAuftrag) {
const kShopSubShop = getActiveShopSubshopId();
if (!Number.isInteger(kPosAuftrag) || !kShopSubShop) {
return null;
}
const result = await getPool()
.request()
.input('kPosAuftrag', sql.Int, kPosAuftrag)
.input('kShopSubShop', sql.Int, kShopSubShop)
.query(`
SELECT TOP 1 m.kAuftrag, a.cAuftragsNr
FROM Pos.tAuftragMapping m
LEFT JOIN Verkauf.tAuftrag a ON a.kAuftrag = m.kAuftrag
WHERE m.kPosAuftrag = @kPosAuftrag
AND m.kShopSubShop = @kShopSubShop
AND m.kAuftrag IS NOT NULL
ORDER BY m.kAuftrag DESC
`);
const row = result.recordset[0];
if (!row?.kAuftrag) {
return null;
}
return row;
}
/**
* Links a JTL-Wawi order back to the originating POS order id.
* MERGE matches JTL CreateOrUpdate outcome for the final committed row.
*/
async function upsertPosOrderMapping(transaction, kAuftrag, kPosAuftrag) {
const kShopSubShop = getActiveShopSubshopId();
if (!Number.isInteger(kPosAuftrag) || !kShopSubShop) {
return;
@@ -378,8 +417,15 @@ async function insertPosOrderMapping(transaction, kAuftrag, kPosAuftrag) {
.input('kPosAuftrag', sql.Int, kPosAuftrag)
.input('kShopSubShop', sql.Int, kShopSubShop)
.query(`
INSERT INTO Pos.tAuftragMapping (kAuftrag, kPosAuftrag, kShopSubShop)
VALUES (@kAuftrag, @kPosAuftrag, @kShopSubShop)
MERGE INTO Pos.tAuftragMapping WITH (HOLDLOCK) AS Target
USING (SELECT @kAuftrag AS kAuftrag, @kPosAuftrag AS kPosAuftrag, @kShopSubShop AS kShopSubShop) AS Source
ON Target.kPosAuftrag = Source.kPosAuftrag
AND Target.kShopSubShop = Source.kShopSubShop
WHEN MATCHED THEN
UPDATE SET Target.kAuftrag = Source.kAuftrag
WHEN NOT MATCHED BY TARGET THEN
INSERT (kAuftrag, kPosAuftrag, kShopSubShop)
VALUES (Source.kAuftrag, Source.kPosAuftrag, Source.kShopSubShop);
`);
}
@@ -441,6 +487,17 @@ async function insertPayment(transaction, kAuftrag, payment, order, orderDate, z
}
export async function createOrder(order) {
const kPosAuftrag = Number.parseInt(order.externalId, 10);
if (Number.isInteger(kPosAuftrag) && kPosAuftrag > 0) {
const existing = await findExistingPosOrderMapping(kPosAuftrag);
if (existing) {
return {
orderId: String(existing.kAuftrag),
orderNumber: existing.cAuftragsNr || '',
};
}
}
const defaults = await getDefaults();
const transaction = new sql.Transaction(getPool());
await transaction.begin(sql.ISOLATION_LEVEL.READ_COMMITTED);
@@ -490,7 +547,7 @@ export async function createOrder(order) {
const kAuftrag = resultAuftrag.recordset[0].kAuftrag;
const kPosAuftrag = Number.parseInt(order.externalId, 10);
await insertPosOrderMapping(transaction, kAuftrag, kPosAuftrag);
await upsertPosOrderMapping(transaction, kAuftrag, kPosAuftrag);
await insertOrderAddress(transaction, kAuftrag, kKunde, order.shippingAddress, 0);
await insertOrderAddress(transaction, kAuftrag, kKunde, order.billingAddress, 1);

View File

@@ -0,0 +1,23 @@
import sql from 'mssql';
import { getPool } from '../db.js';
import { getActiveShopSubshopId } from '../shop.js';
const MAX_ORDER_ID_SQL = `
SELECT ISNULL(MAX(kPosAuftrag), 0) AS MaxOrderId
FROM Pos.tAuftragMapping
WHERE kShopSubShop = @kShopSubShop;
`;
export async function getMaxOrderIdCount() {
const kShopSubShop = getActiveShopSubshopId();
if (!kShopSubShop) {
return 0;
}
const result = await getPool()
.request()
.input('kShopSubShop', sql.Int, kShopSubShop)
.query(MAX_ORDER_ID_SQL);
return result.recordset[0]?.MaxOrderId ?? 0;
}