From 8d55da79b05f8454a0f606de4bdf8ebd998e6894 Mon Sep 17 00:00:00 2001 From: seb Date: Mon, 13 Jul 2026 02:30:04 +0200 Subject: [PATCH] close --- src/endpoints/order.js | 29 +- src/queries/create-order.js | 586 ++++++++++++++++++++++++++++++++++++ src/shop.js | 12 +- 3 files changed, 619 insertions(+), 8 deletions(-) create mode 100644 src/queries/create-order.js diff --git a/src/endpoints/order.js b/src/endpoints/order.js index aa608c2..126331f 100644 --- a/src/endpoints/order.js +++ b/src/endpoints/order.js @@ -1,5 +1,7 @@ import { sendJson } from '../http.js'; +import { logger } from '../logger.js'; import { logOrder } from '../order-log.js'; +import { createOrder } from '../queries/create-order.js'; export const method = 'POST'; export const path = '/v1/order'; @@ -33,16 +35,29 @@ export async function handle(req, res) { } const orders = getOrders(body); - const results = orders.map((order) => { + const results = []; + + for (const order of orders) { logOrder(order); const externalOrderId = String(order?.externalId ?? ''); - return { - status: 'OK', - externalOrderId, - message: '', - }; - }); + try { + const created = await createOrder(order); + logger.success(`order ${created.orderNumber} (kAuftrag=${created.orderId}) created for externalId=${externalOrderId}`); + results.push({ + status: 'OK', + externalOrderId, + message: '', + }); + } catch (err) { + logger.error(`order externalId=${externalOrderId} failed: ${err.message}`); + results.push({ + status: 'ERROR', + externalOrderId, + message: err.message, + }); + } + } return sendJson(res, 200, results); } diff --git a/src/queries/create-order.js b/src/queries/create-order.js new file mode 100644 index 0000000..9243b83 --- /dev/null +++ b/src/queries/create-order.js @@ -0,0 +1,586 @@ +import sql from 'mssql'; +import { getPool } from '../db.js'; +import { getActiveShopId, getActiveShopSubshopId } from '../shop.js'; + +const config = { + kBenutzer: Number(process.env.JTL_KBENUTZER) || 1, + kFirmaHistory: Number(process.env.JTL_KFIRMAHISTORY) || 0, + kSprache: Number(process.env.JTL_KSPRACHE) || 1, + kPlattform: Number(process.env.JTL_KPLATTFORM) || 7, + kVersandArt: Number(process.env.JTL_KVERSANDART) || 0, + kKundengruppe: Number(process.env.JTL_KKUNDENGRUPPE) || 0, + // kLaufendeNummer 3 = Auftrag, 6 = Kunde in dbo.tLaufendeNummern + orderNumberSequence: Number(process.env.JTL_ORDER_NUMBER_SEQUENCE) || 3, + customerNumberSequence: Number(process.env.JTL_CUSTOMER_NUMBER_SEQUENCE) || 6, +}; + +/** DB-dependent foreign keys, resolved once on first order. */ +let resolvedDefaults = null; + +async function getDefaults() { + if (resolvedDefaults) { + return resolvedDefaults; + } + + const result = await getPool().request().input('kPlattform', sql.Int, config.kPlattform).query(` + SELECT + (SELECT MAX(kFirmaHistory) FROM dbo.tFirmaHistory) AS kFirmaHistory, + (SELECT MIN(kVersandArt) FROM dbo.tVersandArt) AS kVersandArt, + (SELECT TOP 1 kKundenGruppe FROM dbo.tKundenGruppe ORDER BY nStandard DESC, kKundenGruppe) AS kKundengruppe, + (SELECT CASE WHEN EXISTS (SELECT 1 FROM dbo.tPlattform WHERE nPlattform = @kPlattform) THEN @kPlattform ELSE 1 END) AS kPlattform; + `); + const row = result.recordset[0] || {}; + + resolvedDefaults = { + kFirmaHistory: config.kFirmaHistory || row.kFirmaHistory || 1, + kVersandArt: config.kVersandArt || row.kVersandArt || 1, + kKundengruppe: config.kKundengruppe || row.kKundengruppe || 1, + kPlattform: row.kPlattform || 1, + }; + return resolvedDefaults; +} + +const COUNTRY_NAMES = { + DE: 'Deutschland', + AT: 'Österreich', + CH: 'Schweiz', +}; + +function toNumber(value, fallback = 0) { + const n = parseFloat(String(value ?? '').replace(',', '.')); + return Number.isFinite(n) ? n : fallback; +} + +function parseCreationDate(value) { + if (value) { + const d = new Date(String(value).replace(' ', 'T')); + if (!Number.isNaN(d.getTime())) { + return d; + } + } + return new Date(); +} + +// JTL maps VAT rates to Steuerklassen: 19% => 1 (voller Satz), 7% => 2 (ermäßigt) +function steuerklasseForVat(vat) { + if (vat >= 15) return 1; + if (vat > 0) return 2; + return 1; +} + +function isoWeek(date) { + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + const dayNum = d.getUTCDay() || 7; + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + return Math.ceil(((d - yearStart) / 86400000 + 1) / 7); +} + +/** + * Resolves the Admin > Nummernkreise placeholders used in tLaufendeNummern's + * cPrefix/cSuffix columns: J = Jahr, M = Monat, T = Tag, K = Kalenderwoche. + */ +function formatNumberPlaceholders(template, date) { + if (!template) return ''; + const pad2 = (n) => String(n).padStart(2, '0'); + return template + .replace(//g, String(date.getFullYear())) + .replace(//g, pad2(date.getMonth() + 1)) + .replace(//g, pad2(date.getDate())) + .replace(//g, pad2(isoWeek(date))); +} + +/** + * Atomically increments nNummer for a dbo.tLaufendeNummern row and returns + * the fully formatted number (cPrefix + nNummer + cSuffix, with placeholders + * resolved against `date`). + */ +async function nextNumberFromSequence(transaction, kLaufendeNummer, date) { + const result = await new sql.Request(transaction).input('kLaufendeNummer', sql.Int, kLaufendeNummer).query(` + DECLARE @n INT, @cPrefix NVARCHAR(50), @cSuffix NVARCHAR(50); + UPDATE dbo.tLaufendeNummern + SET @n = nNummer = nNummer + 1, @cPrefix = cPrefix, @cSuffix = cSuffix + WHERE kLaufendeNummer = @kLaufendeNummer; + SELECT @n AS nNummer, @cPrefix AS cPrefix, @cSuffix AS cSuffix; + `); + const row = result.recordset[0]; + if (!row || row.nNummer == null) { + throw new Error(`dbo.tLaufendeNummern has no row ${kLaufendeNummer}`); + } + const prefix = formatNumberPlaceholders(row.cPrefix, date); + const suffix = formatNumberPlaceholders(row.cSuffix, date); + return `${prefix}${row.nNummer}${suffix}`; +} + +/** + * Allocates the next primary key for a table via dbo.tpk + * (JTL tables like tZahlung/tZahlungsart have no identity column). + */ +async function allocatePk(transaction, tableName) { + const result = await new sql.Request(transaction).input('cName', sql.NVarChar, tableName).query(` + DECLARE @pk INT; + UPDATE dbo.tpk SET @pk = nummer, nummer = nummer + 1, dChanged = GETDATE() WHERE cName = @cName; + SELECT @pk AS pk; + `); + const pk = result.recordset[0]?.pk; + if (pk == null) { + throw new Error(`dbo.tpk has no row for table '${tableName}'`); + } + return pk; +} + +/** + * Resolves a Zahlungsart by name; creates it on the fly (including its PK + * from dbo.tpk) when it does not exist yet. + */ +async function resolveZahlungsart(transaction, name, cache) { + const key = String(name || 'Bar'); + const cached = cache.get(key.toLowerCase()); + if (cached) { + return cached; + } + + const existing = await new sql.Request(transaction) + .input('cName', sql.NVarChar, key) + .query('SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE cName = @cName'); + + let zahlungsart; + if (existing.recordset[0]) { + zahlungsart = { + kZahlungsart: existing.recordset[0].kZahlungsart, + cName: existing.recordset[0].cName, + }; + } else { + const kZahlungsart = await allocatePk(transaction, 'tZahlungsart'); + await new sql.Request(transaction) + .input('kZahlungsart', sql.Int, kZahlungsart) + .input('cName', sql.NVarChar, key) + .query(` + INSERT INTO dbo.tZahlungsart + (kZahlungsart, cName, cPrtString, nLastschrift, cPrtStringVor, cPaymentOption, cKonto, + nAusliefernVorZahlung, nPrioritaet, nMahnwesenAktiv, fSkontoWert, nSkontoZeitraum, + nMatchingOptionen, nIstStandard, nAktiv) + VALUES (@kZahlungsart, @cName, '', 0, '', '', '', 0, 0, 0, 0, 0, 0, 0, 1) + `); + zahlungsart = { kZahlungsart, cName: key }; + } + + cache.set(key.toLowerCase(), zahlungsart); + return zahlungsart; +} + +async function nextCustomerNumber(transaction) { + return nextNumberFromSequence(transaction, config.customerNumberSequence, new Date()); +} + +/** + * Creates a Kunde via Kunde.spKundeInsert (which fills tKunde, tAdresse and + * 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 }) { + const a = address || {}; + const iso = (a.countryIso || 'DE').toUpperCase(); + const kKundengruppe = Number(a.customerGroupId) || defaults.kKundengruppe; + + const result = await new sql.Request(transaction) + .input('cKundenNr', sql.NVarChar, customerNumber) + .input('cFirma', sql.NVarChar, a.company || '') + .input('cAnrede', sql.NVarChar, a.salutation || '') + .input('cTitel', sql.NVarChar, a.title || '') + .input('cVorname', sql.NVarChar, a.firstName || '') + .input('cName', sql.NVarChar, a.lastName || 'Laufkunde') + .input('cStrasse', sql.NVarChar, a.street || '-') + .input('cPLZ', sql.NVarChar, a.zipCode || '') + .input('cOrt', sql.NVarChar, a.city || '-') + .input('cLand', sql.NVarChar, COUNTRY_NAMES[iso] || iso) + .input('cTel', sql.NVarChar, a.phone || '') + .input('cFax', sql.NVarChar, a.fax || '') + .input('cEMail', sql.NVarChar, a.email || '') + .input('cMobil', sql.NVarChar, a.mobile || '') + .input('fRabatt', sql.Decimal(18, 13), toNumber(a.discount, 0)) + .input('cAdressZusatz', sql.NVarChar, a.addressAddition || '') + .input('cGeburtstag', sql.NVarChar, a.birthday || '') + .input('kKundenGruppe', sql.Int, kKundengruppe) + .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('nDebitorennr', sql.Int, Number(a.debtorNumber) || 0) + .query(` + DECLARE @returnValue INT; + DECLARE @p1 dbo.TYPE_spkundeInsert; + INSERT INTO @p1 + (kInetKunde, kKundenKategorie, cKundenNr, cFirma, cAnrede, cTitel, cVorname, cName, + cStrasse, cPLZ, cOrt, cLand, cTel, cFax, cEMail, dErstellt, cMobil, fRabatt, cUSTID, cNewsletter, + cZusatz, cEbayName, kBuyer, cAdressZusatz, cGeburtstag, cWWW, cSperre, cPostID, kKundenGruppe, + nZahlungsziel, kSprache, cISO, cBundesland, cHerkunft, cKassenKunde, cHRNr, kZahlungsart, + nDebitorennr, cSteuerNr, nKreditlimit, kKundenDrucktext, nMahnstopp, nMahnrhythmus, kFirma, + fProvision, nVertreter, fSkonto, nSkontoInTagen) + VALUES + (0, 0, @cKundenNr, @cFirma, @cAnrede, @cTitel, @cVorname, @cName, + @cStrasse, @cPLZ, @cOrt, @cLand, @cTel, @cFax, @cEMail, GETDATE(), @cMobil, @fRabatt, NULL, 'N', + N'', N'', 0, @cAdressZusatz, @cGeburtstag, N'', 'N', NULL, @kKundenGruppe, + 0, @kSprache, @cISO, @cBundesland, @cHerkunft, @cKassenKunde, N'', 0, + @nDebitorennr, N'', 0, 0, 0, 0, 0, + NULL, 0, 0, 0); + EXEC @returnValue = Kunde.spKundeInsert @daten = @p1; + SELECT @returnValue AS kKunde; + `); + + const kKunde = result.recordset[0]?.kKunde; + if (!kKunde || kKunde <= 0) { + throw new Error(`Kunde.spKundeInsert failed for '${customerNumber}'`); + } + return { kKunde, kKundengruppe }; +} + +/** + * 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). + */ +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, + }; + } + return createCustomer(transaction, { + customerNumber, + 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]) { + return { + kKunde: kassenKunde.recordset[0].kKunde, + kKundengruppe: kassenKunde.recordset[0].kKundenGruppe || defaults.kKundengruppe, + }; + } + + return createCustomer(transaction, { + customerNumber: await nextCustomerNumber(transaction), + address: order.billingAddress, + isPosCustomer: true, + defaults, + }); +} + +async function nextOrderNumber(transaction, orderDate) { + return nextNumberFromSequence(transaction, config.orderNumberSequence, orderDate); +} + +async function insertOrderAddress(transaction, kAuftrag, kKunde, address, nTyp) { + const a = address || {}; + const iso = (a.countryIso || 'DE').toUpperCase(); + await new sql.Request(transaction) + .input('kAuftrag', sql.Int, kAuftrag) + .input('kKunde', sql.Int, kKunde) + .input('cFirma', sql.NVarChar, a.company || '') + .input('cAnrede', sql.NVarChar, a.salutation || '') + .input('cTitel', sql.NVarChar, a.title || '') + .input('cVorname', sql.NVarChar, a.firstName || '') + .input('cName', sql.NVarChar, a.lastName || '-') + .input('cStrasse', sql.NVarChar, a.street || '-') + .input('cPLZ', sql.NVarChar, a.zipCode || '') + .input('cOrt', sql.NVarChar, a.city || '-') + .input('cLand', sql.NVarChar, COUNTRY_NAMES[iso] || iso) + .input('cTel', sql.NVarChar, a.phone || '') + .input('cZusatz', sql.NVarChar, a.extraAddressLine || '') + .input('cAdressZusatz', sql.NVarChar, a.addressAddition || '') + .input('cMobil', sql.NVarChar, a.mobile || '') + .input('cMail', sql.NVarChar, a.email || '') + .input('cFax', sql.NVarChar, a.fax || '') + .input('cBundesland', sql.NVarChar, a.state || '') + .input('cISO', sql.NVarChar, iso) + .input('nTyp', sql.Int, nTyp) + .query(` + INSERT INTO Verkauf.tAuftragAdresse + (kAuftrag, kKunde, cFirma, cAnrede, cTitel, cVorname, cName, cStrasse, cPLZ, cOrt, cLand, + cTel, cZusatz, cAdressZusatz, cMobil, cMail, cFax, cBundesland, cISO, nTyp, nZolldokumenteErforderlich) + VALUES (@kAuftrag, @kKunde, @cFirma, @cAnrede, @cTitel, @cVorname, @cName, @cStrasse, @cPLZ, @cOrt, @cLand, + @cTel, @cZusatz, @cAdressZusatz, @cMobil, @cMail, @cFax, @cBundesland, @cISO, @nTyp, 0) + `); +} + +async function insertOrderItem(transaction, kAuftrag, item) { + const vat = toNumber(item.vat, 19); + const quantity = toNumber(item.quantity, 1); + const priceNet = toNumber(item.priceNet, toNumber(item.priceGross) / (1 + vat / 100)); + const discount = toNumber(item.discountPercent, 0); + const kSteuerklasse = steuerklasseForVat(vat); + const sku = String(item.sku || '').trim(); + + let kArtikel = null; + if (sku) { + const result = await new sql.Request(transaction) + .input('cArtNr', sql.NVarChar, sku) + .query('SELECT TOP 1 kArtikel FROM dbo.tArtikel WHERE cArtNr = @cArtNr'); + kArtikel = result.recordset[0]?.kArtikel ?? null; + } + + const result = await new sql.Request(transaction) + .input('kArtikel', sql.Int, kArtikel) + .input('kAuftrag', sql.Int, kAuftrag) + .input('cArtNr', sql.NVarChar, kArtikel ? sku : null) + .input('cName', sql.NVarChar, item.name || sku || 'Position') + .input('cHinweis', sql.NVarChar, item.note || '') + .input('fAnzahl', sql.Float, quantity) + .input('fVkNetto', sql.Float, priceNet) + .input('fMwSt', sql.Float, vat) + .input('kSteuerklasse', sql.Int, kSteuerklasse) + .input('nType', sql.Int, kArtikel ? 1 : 0) + .input('cEinheit', sql.NVarChar, item.unit || '') + .input('fRabatt', sql.Float, discount) + .query(` + DECLARE @t TABLE ([kAuftragPosition] INT); + INSERT INTO Verkauf.tAuftragPosition + (kArtikel, kAuftrag, cArtNr, nReserviert, cName, cHinweis, fAnzahl, fVkNetto, fMwSt, + cNameStandard, kSteuerklasse, nType, cEinheit, fFaktor, kSteuerschluessel, fRabatt) + OUTPUT inserted.kAuftragPosition INTO @t + VALUES (@kArtikel, @kAuftrag, @cArtNr, 1, @cName, @cHinweis, @fAnzahl, @fVkNetto, @fMwSt, + @cName, @kSteuerklasse, @nType, @cEinheit, 1.0, 3, @fRabatt); + SELECT kAuftragPosition FROM @t; + `); + return result.recordset[0]?.kAuftragPosition ?? null; +} + +/** + * 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. + */ +async function insertPosOrderMapping(transaction, kAuftrag, kPosAuftrag) { + const kShopSubShop = getActiveShopSubshopId(); + if (!Number.isInteger(kPosAuftrag) || !kShopSubShop) { + return; + } + await new sql.Request(transaction) + .input('kAuftrag', sql.Int, kAuftrag) + .input('kPosAuftrag', sql.Int, kPosAuftrag) + .input('kShopSubShop', sql.Int, kShopSubShop) + .query(` + INSERT INTO Pos.tAuftragMapping (kAuftrag, kPosAuftrag, kShopSubShop) + VALUES (@kAuftrag, @kPosAuftrag, @kShopSubShop) + `); +} + +async function insertPosOrderPositionMapping(transaction, kAuftragPosition, kPosAuftragPosition) { + const kShopSubShop = getActiveShopSubshopId(); + if (!Number.isInteger(kAuftragPosition) || !Number.isInteger(kPosAuftragPosition) || !kShopSubShop) { + return; + } + await new sql.Request(transaction) + .input('kAuftragPosition', sql.Int, kAuftragPosition) + .input('kPosAuftragPosition', sql.Int, kPosAuftragPosition) + .input('kShopSubShop', sql.Int, kShopSubShop) + .query(` + INSERT INTO Pos.tAuftragPositionMapping (kAuftragPosition, kPosAuftragPosition, kShopSubShop) + VALUES (@kAuftragPosition, @kPosAuftragPosition, @kShopSubShop) + `); +} + +/** + * Mirrors JTL's own PosOrderCreationService -> DeliveryStep.Run(): after a POS + * order is created, `Settings.Deliver` (default true, see decompiled + * jtlCore.Classes.Sync.PosOrderSteps.DeliveryStep:199, + * `bool flag = orderEntity.Settings?.Deliver ?? true;`) decides whether the + * order is delivered immediately. JTL's real implementation books stock via + * the full Auslieferung/warehouse engine; we reproduce its DB end-state + * directly via the same stored procedures it ultimately relies on + * (Versand.spLieferscheinErstellen / spLieferscheinPosErstellen), which also + * call Verkauf.spAuftragEckdatenBerechnen and are what actually derive + * nKomplettAusgeliefert. Digital-voucher partial delivery (IstGutscheinDigital) + * is intentionally not reproduced. + */ +function isOrderDelivered(order) { + const deliver = order.settings?.deliver; + if (deliver === undefined || deliver === null) { + return true; + } + return deliver === true || String(deliver) === '1' || String(deliver).toLowerCase() === 'true'; +} + +async function createLieferschein(transaction, kAuftrag) { + const request = new sql.Request(transaction); + request.input('kBestellung', sql.Int, kAuftrag); + request.input('kBenutzer', sql.Int, config.kBenutzer); + request.input('cLieferscheinNr', sql.NVarChar(50), null); + request.input('cHinweis', sql.NVarChar(255), null); + request.input('dMailVersand', sql.DateTime, null); + request.input('dGedruckt', sql.DateTime, null); + request.input('nFulfillment', sql.Int, null); + request.input('kLieferantenBestellung', sql.Int, null); + request.input('kSessionId', sql.Int, null); + request.output('kLieferschein', sql.Int); + const result = await request.execute('Versand.spLieferscheinErstellen'); + return result.output.kLieferschein; +} + +async function createLieferscheinPos(transaction, kLieferschein, kAuftragPosition, quantity) { + const request = new sql.Request(transaction); + request.input('kLieferschein', sql.Int, kLieferschein); + request.input('kBestellPos', sql.Int, kAuftragPosition); + request.input('fAnzahl', sql.Decimal(25, 13), quantity); + request.input('cHinweis', sql.NVarChar(4000), ''); + request.output('kLieferscheinPos', sql.Int); + await request.execute('Versand.spLieferscheinPosErstellen'); +} + +async function setVersanddatum(transaction, kAuftrag, orderDate) { + await new sql.Request(transaction) + .input('kBestellung', sql.Int, kAuftrag) + .input('dVersanddatum', sql.DateTime, orderDate) + .query(` + MERGE dbo.tVersandInfo AS target + USING (SELECT @kBestellung AS kBestellung) AS src + ON target.kBestellung = src.kBestellung + WHEN MATCHED THEN + UPDATE SET dVersanddatum = @dVersanddatum + WHEN NOT MATCHED THEN + INSERT (kBestellung, dVersanddatum, nAnzahlVersendetePakete, nAnzahlPakete) + VALUES (@kBestellung, @dVersanddatum, 0, 0); + `); +} + +/** + * Delivers the full ordered quantity of every position (no partial delivery), + * mirroring DeliveryStep's non-voucher path. + */ +async function deliverOrder(transaction, kAuftrag, orderDate, deliveredItems) { + if (!deliveredItems.length) { + return; + } + const kLieferschein = await createLieferschein(transaction, kAuftrag); + for (const { kAuftragPosition, quantity } of deliveredItems) { + await createLieferscheinPos(transaction, kLieferschein, kAuftragPosition, quantity); + } + if (orderDate < new Date(new Date().toDateString())) { + await setVersanddatum(transaction, kAuftrag, orderDate); + } +} + +async function insertPayment(transaction, kAuftrag, payment, order, zahlungsartCache) { + const zahlungsart = await resolveZahlungsart(transaction, payment.paymentMethodName || order.paymentMethodName, zahlungsartCache); + const kZahlung = await allocatePk(transaction, 'tZahlung'); + + await new sql.Request(transaction) + .input('kZahlung', sql.Int, kZahlung) + .input('cName', sql.NVarChar, zahlungsart.cName) + .input('dDatum', sql.DateTime, parseCreationDate(order.creationDate)) + .input('fBetrag', sql.Float, toNumber(payment.amount, 0)) + .input('kBestellung', sql.Int, kAuftrag) + .input('kBenutzer', sql.Int, config.kBenutzer) + .input('kZahlungsart', sql.Int, zahlungsart.kZahlungsart) + .input('cExternalTransactionId', sql.NVarChar, order.externalOrderNumber || '') + .query(` + INSERT INTO dbo.tZahlung + (kZahlung, cName, dDatum, fBetrag, kBestellung, kBenutzer, nAnzahlung, cHinweis, kZahlungsart, + nKeinExport, cExternalTransactionId, nZuweisungstyp, nZahlungstyp, cZuweisungsinfo, nZuweisungswertung) + VALUES (@kZahlung, @cName, @dDatum, @fBetrag, @kBestellung, @kBenutzer, 0, '', @kZahlungsart, + 0, @cExternalTransactionId, 0, 0, '', 0) + `); +} + +export async function createOrder(order) { + const defaults = await getDefaults(); + const transaction = new sql.Transaction(getPool()); + await transaction.begin(sql.ISOLATION_LEVEL.READ_COMMITTED); + + try { + const zahlungsartCache = new Map(); + + const orderDate = parseCreationDate(order.creationDate); + const { kKunde, kKundengruppe } = await resolveCustomer(transaction, order, defaults); + const zahlungsart = await resolveZahlungsart(transaction, order.paymentMethodName, zahlungsartCache); + const cAuftragsNr = await nextOrderNumber(transaction, orderDate); + + const resultAuftrag = await new sql.Request(transaction) + .input('cAuftragsNr', sql.NVarChar, cAuftragsNr) + .input('dErstellt', sql.DateTime, orderDate) + .input('kBenutzer', sql.Int, config.kBenutzer) + .input('kKunde', sql.Int, kKunde) + .input('kFirmaHistory', sql.Int, defaults.kFirmaHistory) + .input('kSprache', sql.Int, config.kSprache) + .input('cWaehrung', sql.NVarChar, order.currencyIso || 'EUR') + .input('kPlattform', sql.Int, defaults.kPlattform) + .input('kShop', sql.Int, getActiveShopId() || null) + .input('cKundenNr', sql.NVarChar, String(order.customerNumber || '')) + .input('cVersandlandISO', sql.NVarChar, (order.shippingAddress?.countryIso || 'DE').toUpperCase()) + .input('kVersandArt', sql.Int, defaults.kVersandArt) + .input('kZahlungsart', sql.Int, zahlungsart.kZahlungsart) + .input('kKundengruppe', sql.Int, kKundengruppe) + .input('cExterneAuftragsnummer', sql.NVarChar, order.externalOrderNumber || '') + .query(` + DECLARE @t TABLE ([kAuftrag] INT); + INSERT INTO Verkauf.tAuftrag + (cAuftragsNr, dErstellt, nKomplettAusgeliefert, kBenutzer, kKunde, kBenutzerErstellt, nType, fFaktor, + kFirmaHistory, kSprache, cVersandlandWaehrung, fVersandlandWaehrungFaktor, fFinanzierungskosten, + cWaehrung, kPlattform, kShop, cKundenNr, cVersandlandISO, kVersandArt, kZahlungsart, kKundengruppe, + cExterneAuftragsnummer) + OUTPUT inserted.kAuftrag INTO @t + VALUES (@cAuftragsNr, @dErstellt, 0, @kBenutzer, @kKunde, @kBenutzer, 1, 1.0, + @kFirmaHistory, @kSprache, @cWaehrung, 1.0, 0.0, + @cWaehrung, @kPlattform, @kShop, @cKundenNr, @cVersandlandISO, @kVersandArt, @kZahlungsart, @kKundengruppe, + @cExterneAuftragsnummer); + SELECT kAuftrag FROM @t; + `); + + const kAuftrag = resultAuftrag.recordset[0].kAuftrag; + + const kPosAuftrag = Number.parseInt(order.externalId, 10); + await insertPosOrderMapping(transaction, kAuftrag, kPosAuftrag); + + await insertOrderAddress(transaction, kAuftrag, kKunde, order.shippingAddress, 0); + await insertOrderAddress(transaction, kAuftrag, kKunde, order.billingAddress, 1); + + const deliveredItems = []; + for (const item of order.orderItems || []) { + const kAuftragPosition = await insertOrderItem(transaction, kAuftrag, item); + const kPosAuftragPosition = Number.parseInt(item.externalId, 10); + await insertPosOrderPositionMapping(transaction, kAuftragPosition, kPosAuftragPosition); + if (kAuftragPosition != null) { + deliveredItems.push({ kAuftragPosition, quantity: toNumber(item.quantity, 1) }); + } + } + + for (const payment of order.payments || []) { + await insertPayment(transaction, kAuftrag, payment, order, zahlungsartCache); + } + + if (isOrderDelivered(order)) { + await deliverOrder(transaction, kAuftrag, orderDate, deliveredItems); + } + + await new sql.Request(transaction).input('kAuftrag', sql.Int, kAuftrag).query(` + DECLARE @p1 Verkauf.TYPE_spAuftragEckdatenBerechnen; + INSERT INTO @p1 VALUES (@kAuftrag); + EXEC Verkauf.spAuftragEckdatenBerechnen @auftrag = @p1; + `); + + await transaction.commit(); + + return { orderId: String(kAuftrag), orderNumber: cAuftragsNr }; + } catch (err) { + try { + await transaction.rollback(); + } catch { + // transaction may already be aborted by the server + } + throw err; + } +} diff --git a/src/shop.js b/src/shop.js index 808510a..ddbc8d6 100644 --- a/src/shop.js +++ b/src/shop.js @@ -1,6 +1,7 @@ import sql from 'mssql'; let activeShopId = 0; +let activeShopSubshopId = 0; export function setActiveShop(id) { activeShopId = id; @@ -10,11 +11,20 @@ export function getActiveShopId() { return activeShopId; } +export function setActiveShopSubshop(id) { + activeShopSubshopId = id; +} + +export function getActiveShopSubshopId() { + return activeShopSubshopId; +} + export async function fetchActiveShop(pool) { const result = await pool.request().query(` - SELECT TOP 1 kShop FROM dbo.tShopSubshop WHERE nGesperrt = 0 ORDER BY kShop + SELECT TOP 1 kShop, kShopSubshop FROM dbo.tShopSubshop WHERE nGesperrt = 0 ORDER BY kShop `); const id = result.recordset[0]?.kShop ?? 0; setActiveShop(id); + setActiveShopSubshop(result.recordset[0]?.kShopSubshop ?? 0); return id; }