This commit is contained in:
seb
2026-07-13 02:30:04 +02:00
parent 53a7924c7f
commit 8d55da79b0
3 changed files with 619 additions and 8 deletions

View File

@@ -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);
}

586
src/queries/create-order.js Normal file
View File

@@ -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(/<J>/g, String(date.getFullYear()))
.replace(/<M>/g, pad2(date.getMonth() + 1))
.replace(/<T>/g, pad2(date.getDate()))
.replace(/<K>/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;
}
}

View File

@@ -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;
}