#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include "delivery.hpp" #include "shop.hpp" #include "../db/pool.hpp" #include "../config.hpp" #include "nlohmann/json.hpp" namespace order { struct Defaults; namespace { struct Config { int kBenutzer = 1; int kFirmaHistory = 0; int kSprache = 1; int kPlattform = 7; int kVersandArt = 0; int kKundengruppe = 0; int orderNumberSequence = 3; int customerNumberSequence = 6; }; constexpr int VERSANDPOSITION_TYPE = 2; constexpr int ZAHLUNG_TYPE_ZAHLUNG = 10; constexpr int NIST_READONLY_NICHT_AENDERBAR = 2; constexpr int NIST_EXTERNE_RECHNUNG_KEINE = 2; Config g_config; std::unique_ptr g_resolved_defaults; std::map g_zahlungsart_cache; } // namespace struct Defaults { int kFirmaHistory = 1; int kVersandArt = 1; int kKundengruppe = 1; int kPlattform = 1; }; inline void load_config() { g_config.kBenutzer = config::get_int("JTL_KBENUTZER", 1); g_config.kFirmaHistory = config::get_int("JTL_KFIRMAHISTORY", 0); g_config.kSprache = config::get_int("JTL_KSPRACHE", 1); g_config.kPlattform = config::get_int("JTL_KPLATTFORM", 7); g_config.kVersandArt = config::get_int("JTL_KVERSANDART", 0); g_config.kKundengruppe = config::get_int("JTL_KKUNDENGRUPPE", 0); g_config.orderNumberSequence = config::get_int("JTL_ORDER_NUMBER_SEQUENCE", 3); g_config.customerNumberSequence = config::get_int("JTL_CUSTOMER_NUMBER_SEQUENCE", 6); } inline const Defaults& get_defaults(OdbcPool::Connection* c) { if (g_resolved_defaults) return *g_resolved_defaults; Defaults d; d.kFirmaHistory = g_config.kFirmaHistory; d.kVersandArt = g_config.kVersandArt; d.kKundengruppe = g_config.kKundengruppe; d.kPlattform = g_config.kPlattform; const char* sql = "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 = ?) THEN ? ELSE 1 END) AS kPlattform"; std::vector ps = { {ParamType::Int, "", g_config.kPlattform}, {ParamType::Int, "", g_config.kPlattform} }; ResultSet rs; if (get_pool().execute(c, sql, ps, rs) && !rs.empty() && !rs[0].empty()) { if (!rs[0][0].str.empty()) d.kFirmaHistory = std::stoi(rs[0][0].str); if (!rs[0][1].str.empty()) d.kVersandArt = std::stoi(rs[0][1].str); if (!rs[0][2].str.empty()) d.kKundengruppe = std::stoi(rs[0][2].str); if (!rs[0][3].str.empty()) d.kPlattform = std::stoi(rs[0][3].str); } // Apply environment overrides if (g_config.kFirmaHistory > 0) d.kFirmaHistory = g_config.kFirmaHistory; if (g_config.kVersandArt > 0) d.kVersandArt = g_config.kVersandArt; if (g_config.kKundengruppe > 0) d.kKundengruppe = g_config.kKundengruppe; g_resolved_defaults = std::make_unique(d); return *g_resolved_defaults; } inline double to_number(const nlohmann::json& value, double fallback = 0) { if (value.is_null()) return fallback; if (value.is_number()) return value.get(); if (value.is_string()) { std::string s = value.get(); if (s.empty()) return fallback; std::replace(s.begin(), s.end(), ',', '.'); try { return std::stod(s); } catch (...) {} } return fallback; } inline int to_int(const nlohmann::json& value, int fallback = 0) { if (value.is_null()) return fallback; if (value.is_number_integer()) return value.get(); if (value.is_number_unsigned()) return static_cast(value.get()); if (value.is_number_float()) return static_cast(value.get()); if (value.is_boolean()) return value.get() ? 1 : 0; if (value.is_string()) { std::string s = value.get(); if (s.empty()) return fallback; try { return std::stoi(s); } catch (...) {} } return fallback; } inline int json_int(const nlohmann::json& obj, const char* key, int fallback = 0) { auto it = obj.find(key); if (it == obj.end()) return fallback; return to_int(*it, fallback); } inline double json_double(const nlohmann::json& obj, const char* key, double fallback = 0) { auto it = obj.find(key); if (it == obj.end()) return fallback; return to_number(*it, fallback); } inline int steuerklasse_for_vat(double vat) { if (vat >= 15) return 1; if (vat > 0) return 2; return 1; } inline int iso_week(const std::tm& tm) { // JTL placeholders: J = Jahr, M = Monat, T = Tag, K = Kalenderwoche. // ISO week date std::tm t0 = tm; int yday = t0.tm_yday; int wday = t0.tm_wday; if (wday == 0) wday = 7; int week = (yday - wday + 10) / 7; if (week < 1) week = 1; if (week > 53) week = 53; return week; } inline std::string format_number_placeholders(const std::string& tpl, const std::tm& tm) { if (tpl.empty()) return ""; std::string r = tpl; auto replace = [&r](const std::string& from, const std::string& to) { size_t start = 0; while ((start = r.find(from, start)) != std::string::npos) { r.replace(start, from.size(), to); start += to.size(); } }; std::ostringstream mm, dd, kk; mm << std::setw(2) << std::setfill('0') << (tm.tm_mon + 1); dd << std::setw(2) << std::setfill('0') << tm.tm_mday; kk << std::setw(2) << std::setfill('0') << iso_week(tm); replace("", std::to_string(tm.tm_year + 1900)); replace("", mm.str()); replace("", dd.str()); replace("", kk.str()); return r; } inline std::tm parse_order_date(const std::string& s) { std::tm tm{}; std::string iso = s; std::replace(iso.begin(), iso.end(), ' ', 'T'); std::istringstream ss(iso); ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); if (ss.fail()) { auto now = std::chrono::system_clock::now(); std::time_t t = std::chrono::system_clock::to_time_t(now); localtime_r(&t, &tm); } return tm; } inline std::string order_date_sql(const std::tm& tm) { char buf[32]; std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); return buf; } inline std::string country_name(const std::string& iso) { static const std::map names = { {"DE", "Deutschland"}, {"AT", "Oesterreich"}, {"CH", "Schweiz"} }; auto it = names.find(iso); return it != names.end() ? it->second : iso; } inline std::string next_number_from_sequence(OdbcPool::Connection* c, int kLaufendeNummer, const std::tm& date) { const char* sql = "DECLARE @n INT, @cPrefix NVARCHAR(50), @cSuffix NVARCHAR(50);" "UPDATE dbo.tLaufendeNummern" " SET @n = nNummer = nNummer + 1, @cPrefix = cPrefix, @cSuffix = cSuffix" " WHERE kLaufendeNummer = ?;" "SELECT @n AS nNummer, @cPrefix AS cPrefix, @cSuffix AS cSuffix"; std::vector ps = {{ParamType::Int, "", kLaufendeNummer}}; ResultSet rs; if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].size() < 3) { throw std::runtime_error("dbo.tLaufendeNummern has no row " + std::to_string(kLaufendeNummer)); } int n = std::stoi(rs[0][0].str); std::string prefix = format_number_placeholders(rs[0][1].str, date); std::string suffix = format_number_placeholders(rs[0][2].str, date); return prefix + std::to_string(n) + suffix; } inline int allocate_pk(OdbcPool::Connection* c, const std::string& tableName) { const char* sql = "DECLARE @pk INT;" "UPDATE dbo.tpk SET @pk = nummer, nummer = nummer + 1, dChanged = GETDATE() WHERE cName = ?;" "SELECT @pk AS pk"; std::vector ps = {{ParamType::NVarChar, tableName, 0}}; ResultSet rs; if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].empty()) { throw std::runtime_error("dbo.tpk has no row for table '" + tableName + "'"); } return std::stoi(rs[0][0].str); } inline int parse_import_setting(const nlohmann::json& order) { if (!order.contains("settings") || order["settings"].is_null()) return 0; return json_int(order["settings"], "importSetting", 0); } inline int parse_invoice_setting(const nlohmann::json& order) { if (!order.contains("settings") || order["settings"].is_null()) return 0; return json_int(order["settings"], "invoiceSetting", 0); } inline int resolve_n_ist_readonly(const nlohmann::json& order) { return parse_import_setting(order) == 0 ? NIST_READONLY_NICHT_AENDERBAR : 0; } inline int resolve_n_ist_externe_rechnung(const nlohmann::json& order) { const int import_setting = parse_import_setting(order); const int invoice_setting = parse_invoice_setting(order); if (import_setting >= 2 && import_setting <= 5) return 0; if (invoice_setting & 1) return 0; if (import_setting == 0) return NIST_EXTERNE_RECHNUNG_KEINE; return 0; } struct VersandArtRow { int kVersandArt = 0; std::string cName; double fPrice = 0; double fMwSt = 19; }; inline bool is_versandposition(const nlohmann::json& item) { return json_int(item, "type", 0) == VERSANDPOSITION_TYPE; } inline bool has_versandposition(const nlohmann::json& items) { if (!items.is_array()) return false; for (const auto& item : items) { if (is_versandposition(item)) return true; } return false; } inline bool has_non_return_sale_items(const nlohmann::json& items) { if (!items.is_array()) return false; for (const auto& item : items) { if (json_double(item, "isReturn", 0) == 0) return true; } return false; } inline bool should_inject_selbstabholer_shipping(const nlohmann::json& items) { if (has_versandposition(items)) return false; return has_non_return_sale_items(items); } inline std::optional lookup_versand_art(OdbcPool::Connection* c, const std::string& shipping_name) { auto try_lookup = [&](const std::string& cName) -> std::optional { std::vector ps = {{ParamType::NVarChar, cName, 0}}; ResultSet rs; if (!get_pool().execute(c, "SELECT TOP 1 kVersandArt, cName, fPrice, fMwSt " "FROM dbo.tVersandArt WHERE cName = ?", ps, rs) || rs.empty() || rs[0].empty()) { return std::nullopt; } VersandArtRow row; row.kVersandArt = std::stoi(rs[0][0].str); row.cName = rs[0][1].str; row.fPrice = rs[0].size() > 2 ? to_number(nlohmann::json(rs[0][2].str), 0) : 0; row.fMwSt = rs[0].size() > 3 ? to_number(nlohmann::json(rs[0][3].str), 19) : 19; return row; }; std::string name = shipping_name; if (name.empty()) name = "Selbstabholer"; if (auto row = try_lookup(name)) return row; if (name != "Selbstabholer") return try_lookup("Selbstabholer"); return std::nullopt; } inline nlohmann::json synthetic_shipping_item(const VersandArtRow& versand_art) { const double vat = versand_art.fMwSt; const double gross = versand_art.fPrice; const double net = gross / (1 + vat / 100); return { {"type", std::to_string(VERSANDPOSITION_TYPE)}, {"quantity", "1"}, {"name", versand_art.cName}, {"priceGross", std::to_string(gross)}, {"priceNet", std::to_string(net)}, {"vat", std::to_string(vat)}, {"isReturn", "0"} }; } inline nlohmann::json resolve_zahlungsart(OdbcPool::Connection* c, const std::string& name) { const std::string lookup_name = name.empty() ? "Bar" : name; const std::string cache_key = [&]() { std::string key = lookup_name; std::transform(key.begin(), key.end(), key.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); return key; }(); auto it = g_zahlungsart_cache.find(cache_key); if (it != g_zahlungsart_cache.end()) return it->second; auto fetch_row = [&](const char* sql) -> std::optional> { std::vector ps = {{ParamType::NVarChar, lookup_name, 0}}; ResultSet rs; if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].empty()) { return std::nullopt; } return std::make_pair(std::stoi(rs[0][0].str), rs[0][1].str); }; std::optional> row = fetch_row("SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE cName = ?"); if (!row) { row = fetch_row( "SELECT TOP 1 z.kZahlungsart, z.cName " "FROM dbo.tZahlungsArtSprache zs " "INNER JOIN dbo.tZahlungsart z ON z.kZahlungsart = zs.kZahlungsart " "WHERE zs.cName = ?"); } if (!row) { row = fetch_row( "SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE UPPER(cName) = UPPER(?)"); } nlohmann::json zahlungsart; if (row) { zahlungsart = {{"kZahlungsart", row->first}, {"cName", row->second}}; } else { int kZahlungsart = allocate_pk(c, "tZahlungsart"); const char* insert = "INSERT INTO dbo.tZahlungsart" " (kZahlungsart, cName, cPrtString, nLastschrift, cPrtStringVor, cPaymentOption, cKonto," " nAusliefernVorZahlung, nPrioritaet, nMahnwesenAktiv, fSkontoWert, nSkontoZeitraum," " nMatchingOptionen, nIstStandard, nAktiv)" " VALUES (?, ?, '', 0, '', '', '', 0, 0, 0, 0, 0, 0, 0, 1)"; std::vector ps2 = { {ParamType::Int, "", kZahlungsart}, {ParamType::NVarChar, lookup_name, 0} }; ResultSet rs; get_pool().execute(c, insert, ps2, rs); zahlungsart = {{"kZahlungsart", kZahlungsart}, {"cName", lookup_name}}; } g_zahlungsart_cache[cache_key] = zahlungsart; return zahlungsart; } inline std::string next_customer_number(OdbcPool::Connection* c) { std::tm date; { auto now = std::chrono::system_clock::now(); std::time_t t = std::chrono::system_clock::to_time_t(now); localtime_r(&t, &date); } return next_number_from_sequence(c, g_config.customerNumberSequence, date); } inline std::pair lookup_kassenkunde(OdbcPool::Connection* c, const Defaults& defaults) { ResultSet rs; get_pool().execute(c, "SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKassenKunde = 'Y' ORDER BY kKunde", rs); if (!rs.empty() && !rs[0].empty()) { int kKunde = std::stoi(rs[0][0].str); int grp = defaults.kKundengruppe; if (rs[0].size() > 1 && !rs[0][1].str.empty()) grp = std::stoi(rs[0][1].str); return {kKunde, grp}; } return {0, defaults.kKundengruppe}; } inline bool is_walk_in_order(const nlohmann::json& order) { std::string customer_number = order.value("customerNumber", ""); if (customer_number.empty() || customer_number == "0") return true; const auto billing = order.value("billingAddress", nlohmann::json::object()); return billing.value("lastName", "") == "Laufkunde" && billing.value("firstName", "").empty(); } inline std::string resolve_auftrag_c_kunden_nr(const nlohmann::json& order) { if (is_walk_in_order(order)) return "0"; return order.value("customerNumber", ""); } inline std::pair create_customer(OdbcPool::Connection* c, const std::string& customer_number, const nlohmann::json& address, const Defaults& defaults) { const nlohmann::json a = address.is_null() ? nlohmann::json::object() : address; std::string iso = a.value("countryIso", "DE"); std::transform(iso.begin(), iso.end(), iso.begin(), [](unsigned char ch) { return std::toupper(ch); }); int kKundengruppe = json_int(a, "customerGroupId", 0); if (kKundengruppe <= 0) kKundengruppe = defaults.kKundengruppe; const char* sql = "DECLARE @returnValue INT;" "DECLARE @kunde_daten dbo.TYPE_spkundeInsert;" "INSERT INTO @kunde_daten" " (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, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, GETDATE(), ?, ?, NULL, 'N'," " N'', N'', 0, ?, ?, N'', 'N', NULL, ?," " 0, ?, ?, ?, ?, ?, N'', 0," " ?, N'', 0, 0, 0, 0, 0," " NULL, 0, 0, 0);" "EXEC @returnValue = Kunde.spKundeInsert @daten = @kunde_daten;" "SELECT @returnValue AS kKunde"; std::vector ps = { {ParamType::NVarChar, customer_number, 0}, {ParamType::NVarChar, a.value("company", ""), 0}, {ParamType::NVarChar, a.value("salutation", ""), 0}, {ParamType::NVarChar, a.value("title", ""), 0}, {ParamType::NVarChar, a.value("firstName", ""), 0}, {ParamType::NVarChar, a.value("lastName", "Laufkunde"), 0}, {ParamType::NVarChar, a.value("street", "-"), 0}, {ParamType::NVarChar, a.value("zipCode", ""), 0}, {ParamType::NVarChar, a.value("city", "-"), 0}, {ParamType::NVarChar, country_name(iso), 0}, {ParamType::NVarChar, a.value("phone", ""), 0}, {ParamType::NVarChar, a.value("fax", ""), 0}, {ParamType::NVarChar, a.value("email", ""), 0}, {ParamType::NVarChar, a.value("mobile", ""), 0}, {ParamType::Double, "", 0, json_double(a, "discount", 0)}, {ParamType::NVarChar, a.value("addressAddition", ""), 0}, {ParamType::NVarChar, a.value("birthday", ""), 0}, {ParamType::Int, "", kKundengruppe}, {ParamType::Int, "", g_config.kSprache}, {ParamType::NVarChar, iso, 0}, {ParamType::NVarChar, a.value("state", ""), 0}, {ParamType::NVarChar, std::string("Kasse"), 0}, {ParamType::NVarChar, std::string("Y"), 0}, {ParamType::Int, "", json_int(a, "debtorNumber", 0)} }; ResultSet rs; if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].empty()) { throw std::runtime_error("Kunde.spKundeInsert failed for '" + customer_number + "'"); } int kKunde = std::stoi(rs[0][0].str); if (kKunde <= 0) throw std::runtime_error("Kunde.spKundeInsert returned invalid kKunde"); return {kKunde, kKundengruppe}; } inline std::pair resolve_customer(OdbcPool::Connection* c, const nlohmann::json& order, const Defaults& defaults) { if (is_walk_in_order(order)) { auto [kKunde, grp] = lookup_kassenkunde(c, defaults); if (kKunde > 0) return {kKunde, grp}; return create_customer(c, next_customer_number(c), order.value("billingAddress", nlohmann::json::object()), defaults); } std::string customer_number = order.value("customerNumber", ""); std::vector ps = {{ParamType::NVarChar, customer_number, 0}}; ResultSet rs; get_pool().execute(c, "SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKundenNr = ?", ps, rs); if (!rs.empty() && !rs[0].empty()) { int kKunde = std::stoi(rs[0][0].str); int grp = defaults.kKundengruppe; if (rs[0].size() > 1 && !rs[0][1].str.empty()) grp = std::stoi(rs[0][1].str); return {kKunde, grp}; } return create_customer(c, customer_number, order.value("billingAddress", nlohmann::json::object()), defaults); } inline std::string next_order_number(OdbcPool::Connection* c, const std::tm& date) { return next_number_from_sequence(c, g_config.orderNumberSequence, date); } inline void insert_order_address(OdbcPool::Connection* c, int kAuftrag, int kKunde, const nlohmann::json& address, int nTyp) { const nlohmann::json a = address.is_null() ? nlohmann::json::object() : address; std::string iso = a.value("countryIso", "DE"); std::transform(iso.begin(), iso.end(), iso.begin(), [](unsigned char ch) { return std::toupper(ch); }); const char* sql = "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)"; std::vector ps = { {ParamType::Int, "", kAuftrag}, {ParamType::Int, "", kKunde}, {ParamType::NVarChar, a.value("company", ""), 0}, {ParamType::NVarChar, a.value("salutation", ""), 0}, {ParamType::NVarChar, a.value("title", ""), 0}, {ParamType::NVarChar, a.value("firstName", ""), 0}, {ParamType::NVarChar, a.value("lastName", "-"), 0}, {ParamType::NVarChar, a.value("street", "-"), 0}, {ParamType::NVarChar, a.value("zipCode", ""), 0}, {ParamType::NVarChar, a.value("city", "-"), 0}, {ParamType::NVarChar, country_name(iso), 0}, {ParamType::NVarChar, a.value("phone", ""), 0}, {ParamType::NVarChar, a.value("extraAddressLine", ""), 0}, {ParamType::NVarChar, a.value("addressAddition", ""), 0}, {ParamType::NVarChar, a.value("mobile", ""), 0}, {ParamType::NVarChar, a.value("email", ""), 0}, {ParamType::NVarChar, a.value("fax", ""), 0}, {ParamType::NVarChar, a.value("state", ""), 0}, {ParamType::NVarChar, iso, 0}, {ParamType::Int, "", nTyp} }; ResultSet rs; get_pool().execute(c, sql, ps, rs); } inline int insert_order_item(OdbcPool::Connection* c, int kAuftrag, const nlohmann::json& item) { double vat = json_double(item, "vat", 19); double quantity = json_double(item, "quantity", 1); double price_gross = json_double(item, "priceGross", 0); double price_net = json_double(item, "priceNet", price_gross / (1 + vat / 100)); double discount = json_double(item, "discountPercent", 0); int kSteuerklasse = steuerklasse_for_vat(vat); std::string sku = item.value("sku", ""); const int position_type = is_versandposition(item) ? VERSANDPOSITION_TYPE : json_int(item, "type", 0); int kArtikel = 0; bool has_artikel = false; if (!sku.empty() && position_type != VERSANDPOSITION_TYPE) { std::vector ps = {{ParamType::NVarChar, sku, 0}}; ResultSet rs; get_pool().execute(c, "SELECT TOP 1 kArtikel FROM dbo.tArtikel WHERE cArtNr = ?", ps, rs); if (!rs.empty() && !rs[0].empty()) { kArtikel = std::stoi(rs[0][0].str); has_artikel = true; } } const int nType = position_type == VERSANDPOSITION_TYPE ? VERSANDPOSITION_TYPE : (has_artikel ? 1 : 0); const int nReserviert = nType == VERSANDPOSITION_TYPE ? 0 : 1; const char* sql = "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 (?, ?, ?, ?, ?, ?, ?, ?, ?," " ?, ?, ?, ?, 1.0, 3, ?);" "SELECT kAuftragPosition FROM @t"; Param cartnr{ParamType::NVarChar, has_artikel ? sku : std::string(), 0, 0.0, !has_artikel}; Param pk_art{ParamType::Int, "", kArtikel, 0.0, !has_artikel}; std::string name = item.value("name", sku.empty() ? std::string("Position") : sku); std::vector ps = { pk_art, {ParamType::Int, "", kAuftrag}, cartnr, {ParamType::Int, "", nReserviert}, {ParamType::NVarChar, name, 0}, {ParamType::NVarChar, item.value("note", ""), 0}, {ParamType::Double, "", 0, quantity}, {ParamType::Double, "", 0, price_net}, {ParamType::Double, "", 0, vat}, {ParamType::NVarChar, name, 0}, {ParamType::Int, "", kSteuerklasse}, {ParamType::Int, "", nType}, {ParamType::NVarChar, item.value("unit", ""), 0}, {ParamType::Double, "", 0, discount} }; ResultSet rs; if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].empty()) return 0; return std::stoi(rs[0][0].str); } inline int parse_pos_auftrag_id(const std::string& external_id) { if (external_id.empty()) return 0; try { return std::stoi(external_id); } catch (...) { return 0; } } inline bool external_order_numbers_match(const std::string& mapped, const std::string& incoming) { if (incoming.empty()) return true; if (mapped.size() != incoming.size()) return false; for (size_t i = 0; i < mapped.size(); ++i) { if (std::tolower(static_cast(mapped[i])) != std::tolower(static_cast(incoming[i]))) { return false; } } return true; } // PosOrderCreationService.CheckIfOrderExists — already-imported POS order. inline std::optional> find_existing_pos_order( OdbcPool::Connection* c, int kPosAuftrag, const std::string& external_order_number) { int kShopSubShop = get_active_shop_subshop_id(); if (kPosAuftrag <= 0 || kShopSubShop <= 0) return std::nullopt; const char* sql = "SELECT TOP 1 m.kAuftrag, a.cAuftragsNr, ISNULL(a.cExterneAuftragsnummer, '') AS cExterneAuftragsnummer " "FROM Pos.tAuftragMapping m " "LEFT JOIN Verkauf.tAuftrag a ON a.kAuftrag = m.kAuftrag " "WHERE m.kPosAuftrag = ? AND m.kShopSubShop = ? AND m.kAuftrag IS NOT NULL " "ORDER BY m.kAuftrag DESC"; ResultSet rs; std::vector ps = { {ParamType::Int, "", kPosAuftrag}, {ParamType::Int, "", kShopSubShop}, }; if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].empty() || rs[0][0].type == CellType::Null) { return std::nullopt; } int kAuftrag = std::stoi(rs[0][0].str); if (kAuftrag <= 0) return std::nullopt; std::string order_number = rs[0].size() > 1 ? rs[0][1].str : ""; std::string mapped_external = rs[0].size() > 2 ? rs[0][2].str : ""; if (!external_order_numbers_match(mapped_external, external_order_number)) { return std::nullopt; } return std::make_pair(kAuftrag, order_number); } inline void upsert_pos_order_mapping(OdbcPool::Connection* c, int kAuftrag, int kPosAuftrag) { int kShopSubShop = get_active_shop_subshop_id(); if (kPosAuftrag <= 0 || kShopSubShop <= 0) return; ResultSet rs; std::vector del_ps = { {ParamType::Int, "", kPosAuftrag}, {ParamType::Int, "", kShopSubShop}, }; get_pool().execute(c, "DELETE FROM Pos.tAuftragMapping WHERE kPosAuftrag = ? AND kShopSubShop = ?", del_ps, rs); std::vector ins_ps = { {ParamType::Int, "", kAuftrag}, {ParamType::Int, "", kPosAuftrag}, {ParamType::Int, "", kShopSubShop}, }; get_pool().execute(c, "INSERT INTO Pos.tAuftragMapping (kAuftrag, kPosAuftrag, kShopSubShop) VALUES (?, ?, ?)", ins_ps, rs); } inline void insert_pos_order_position_mapping(OdbcPool::Connection* c, int kAuftragPosition, const std::string& external_id) { int kPosAuftragPosition = 0; try { kPosAuftragPosition = std::stoi(external_id); } catch (...) { return; } int kShopSubShop = get_active_shop_subshop_id(); if (kAuftragPosition <= 0 || kPosAuftragPosition <= 0 || kShopSubShop <= 0) return; const char* sql = "INSERT INTO Pos.tAuftragPositionMapping (kAuftragPosition, kPosAuftragPosition, kShopSubShop) VALUES (?, ?, ?)"; std::vector ps = { {ParamType::Int, "", kAuftragPosition}, {ParamType::Int, "", kPosAuftragPosition}, {ParamType::Int, "", kShopSubShop} }; ResultSet rs; get_pool().execute(c, sql, ps, rs); } inline bool is_order_delivered(const nlohmann::json& order) { auto it = order.find("settings"); if (it != order.end() && !it->is_null()) { auto del = it->find("deliver"); if (del != it->end()) { if (del->is_boolean()) return del->get(); if (del->is_number()) return del->get() != 0; if (del->is_string()) { std::string s = *del; std::transform(s.begin(), s.end(), s.begin(), [](unsigned char ch) { return std::tolower(ch); }); return s == "true" || s == "1"; } return false; } } return true; } // ODBC maps ? placeholders to @p1, @p2, … — never DECLARE @p1 in the same batch. inline void recalculate_auftrag_eckdaten(OdbcPool::Connection* c, int kAuftrag) { const char* sql = "DECLARE @eckdaten_calc Verkauf.TYPE_spAuftragEckdatenBerechnen;" "INSERT INTO @eckdaten_calc VALUES (?);" "EXEC Verkauf.spAuftragEckdatenBerechnen @auftrag = @eckdaten_calc"; std::vector ps = {{ParamType::Int, "", kAuftrag}}; ResultSet rs; if (!get_pool().execute(c, sql, ps, rs)) { throw std::runtime_error("spAuftragEckdatenBerechnen failed for kAuftrag=" + std::to_string(kAuftrag)); } } inline std::optional get_offener_auftragswert(OdbcPool::Connection* c, int kAuftrag) { const char* sql = "SELECT ROUND(tAuftragEckdaten.fOffenerWertOhneStorno, 2) AS fOffenerAuftragswert " "FROM Verkauf.tAuftrag " "LEFT JOIN Verkauf.tAuftragEckdaten ON tAuftragEckdaten.kAuftrag = tAuftrag.kAuftrag " "WHERE tAuftrag.kAuftrag = ?"; std::vector ps = {{ParamType::Int, "", kAuftrag}}; ResultSet rs; if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].empty() || rs[0][0].type == CellType::Null) { return std::nullopt; } return to_number(nlohmann::json(rs[0][0].str), 0); } inline bool is_new_payment(const nlohmann::json& payment) { return json_int(payment, "paymentId", 0) <= 0; } inline void insert_payment(OdbcPool::Connection* c, int kAuftrag, const nlohmann::json& payment, const nlohmann::json& order, const std::tm& order_date_tm) { std::string payment_name = payment.value("paymentMethodName", ""); if (payment_name.empty()) payment_name = order.value("paymentMethodName", "Bar"); nlohmann::json zahlungsart = resolve_zahlungsart(c, payment_name); int kZahlung = allocate_pk(c, "tZahlung"); recalculate_auftrag_eckdaten(c, kAuftrag); auto fOffenerWert = get_offener_auftragswert(c, kAuftrag); if (!fOffenerWert) { throw std::runtime_error("no open order amount for kAuftrag=" + std::to_string(kAuftrag)); } const char* sql = "IF EXISTS (" " SELECT 1 FROM Verkauf.tAuftragEckdaten" " WHERE kAuftrag = ?" " AND ROUND(fOffenerWertOhneStorno, 2) = ROUND(?, 2)" ")" "BEGIN" " INSERT INTO dbo.tZahlung" " (kZahlung, cName, dDatum, fBetrag, kBestellung, kBenutzer, nAnzahlung, cHinweis, kZahlungsart," " nKeinExport, cExternalTransactionId, nZuweisungstyp, nZahlungstyp, cZuweisungsinfo, nZuweisungswertung)" " VALUES (?, ?, ?, ?, ?, ?, 0, '', ?," " 0, ?, 0, ?, '', 0);" "END;" "SELECT @@ROWCOUNT AS inserted"; std::vector ps = { {ParamType::Int, "", kAuftrag}, {ParamType::Double, "", 0, *fOffenerWert}, {ParamType::Int, "", kZahlung}, {ParamType::NVarChar, zahlungsart.value("cName", ""), 0}, {ParamType::NVarChar, order_date_sql(order_date_tm), 0}, {ParamType::Double, "", 0, json_double(payment, "amount", 0)}, {ParamType::Int, "", kAuftrag}, {ParamType::Int, "", g_config.kBenutzer}, {ParamType::Int, "", json_int(zahlungsart, "kZahlungsart", 0)}, {ParamType::NVarChar, order.value("externalOrderNumber", ""), 0}, {ParamType::Int, "", ZAHLUNG_TYPE_ZAHLUNG}, }; ResultSet rs; if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].empty() || std::stoi(rs[0][0].str) == 0) { throw std::runtime_error("payment insert skipped: open amount changed for kAuftrag=" + std::to_string(kAuftrag)); } } inline nlohmann::json create_order(const nlohmann::json& order) { load_config(); auto guard = get_pool().checkout(); if (!guard) throw std::runtime_error("no ODBC connection available"); auto* c = guard.get(); int kPosAuftrag = parse_pos_auftrag_id(order.value("externalId", "")); std::string external_order_number = order.value("externalOrderNumber", ""); if (kPosAuftrag > 0) { if (auto existing = find_existing_pos_order(c, kPosAuftrag, external_order_number)) { return { {"orderId", std::to_string(existing->first)}, {"orderNumber", existing->second}, {"alreadyExists", true} }; } } const Defaults& defaults = get_defaults(c); if (!get_pool().begin(c)) throw std::runtime_error("failed to begin transaction"); try { g_zahlungsart_cache.clear(); std::string creation_date = order.value("creationDate", ""); std::tm order_date_tm = creation_date.empty() ? []() { std::tm tm{}; auto now = std::chrono::system_clock::now(); std::time_t t = std::chrono::system_clock::to_time_t(now); localtime_r(&t, &tm); return tm; }() : parse_order_date(creation_date); auto [kKunde, kKundengruppe] = resolve_customer(c, order, defaults); nlohmann::json zahlungsart = resolve_zahlungsart(c, order.value("paymentMethodName", "Bar")); std::string cAuftragsNr = next_order_number(c, order_date_tm); nlohmann::json order_items = order.contains("orderItems") ? order["orderItems"] : nlohmann::json::array(); if (!order_items.is_array()) order_items = nlohmann::json::array(); auto versand_art = lookup_versand_art(c, order.value("shippingName", "")); int kVersandArt = versand_art ? versand_art->kVersandArt : defaults.kVersandArt; if (should_inject_selbstabholer_shipping(order_items) && versand_art) { order_items.push_back(synthetic_shipping_item(*versand_art)); } int active_shop = get_active_shop_id(); const int nIstReadOnly = resolve_n_ist_readonly(order); const int nIstExterneRechnung = resolve_n_ist_externe_rechnung(order); const char* insert_auftrag = "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, nIstExterneRechnung, cInet, nIstReadOnly, kShopauftrag, nLieferPrioritaet)" " OUTPUT inserted.kAuftrag INTO @t" " VALUES (?, ?, 0, ?, ?, ?, 1, 1.0," " ?, ?, ?, 1.0, 0.0," " ?, ?, ?, ?, ?, ?, ?, ?," " ?, ?, ?, ?, 0, 10);" "SELECT kAuftrag FROM @t"; std::string shipping_iso = order.value("shippingAddress", nlohmann::json::object()).value("countryIso", "DE"); std::transform(shipping_iso.begin(), shipping_iso.end(), shipping_iso.begin(), [](unsigned char ch) { return std::toupper(ch); }); std::string currency_iso = order.value("currencyIso", "EUR"); std::string cKundenNr = resolve_auftrag_c_kunden_nr(order); std::vector ps = { {ParamType::NVarChar, cAuftragsNr, 0}, {ParamType::NVarChar, order_date_sql(order_date_tm), 0}, {ParamType::Int, "", g_config.kBenutzer}, {ParamType::Int, "", kKunde}, {ParamType::Int, "", g_config.kBenutzer}, {ParamType::Int, "", defaults.kFirmaHistory}, {ParamType::Int, "", g_config.kSprache}, {ParamType::NVarChar, currency_iso, 0}, {ParamType::NVarChar, currency_iso, 0}, {ParamType::Int, "", defaults.kPlattform}, {ParamType::Int, "", active_shop == 0 ? -1 : active_shop}, {ParamType::NVarChar, cKundenNr, 0}, {ParamType::NVarChar, shipping_iso, 0}, {ParamType::Int, "", kVersandArt}, {ParamType::Int, "", json_int(zahlungsart, "kZahlungsart", 0)}, {ParamType::Int, "", kKundengruppe}, {ParamType::NVarChar, order.value("externalOrderNumber", ""), 0}, {ParamType::Int, "", nIstExterneRechnung}, {ParamType::NVarChar, "Y", 0}, {ParamType::Int, "", nIstReadOnly}, }; if (active_shop == 0) { ps[11] = Param::null_int(); } ResultSet rs; if (!get_pool().execute(c, insert_auftrag, ps, rs) || rs.empty() || rs[0].empty()) { throw std::runtime_error("failed to insert Verkauf.tAuftrag"); } int kAuftrag = std::stoi(rs[0][0].str); upsert_pos_order_mapping(c, kAuftrag, kPosAuftrag); insert_order_address(c, kAuftrag, kKunde, order.value("shippingAddress", nlohmann::json::object()), 0); insert_order_address(c, kAuftrag, kKunde, order.value("billingAddress", nlohmann::json::object()), 1); std::vector delivered_items; for (const auto& item : order_items) { int kAuftragPosition = insert_order_item(c, kAuftrag, item); const std::string external_id = item.value("externalId", ""); if (!external_id.empty()) { insert_pos_order_position_mapping(c, kAuftragPosition, external_id); } if (kAuftragPosition > 0 && !is_versandposition(item)) { delivered_items.push_back({kAuftragPosition, json_double(item, "quantity", 1)}); } } const nlohmann::json& payments = order.contains("payments") ? order["payments"] : nlohmann::json::array(); for (const auto& payment : payments) { if (!is_new_payment(payment)) continue; insert_payment(c, kAuftrag, payment, order, order_date_tm); } if (is_order_delivered(order)) { delivery::deliver_order(c, g_config.kBenutzer, kAuftrag, kVersandArt, delivered_items); } const char* calc = "DECLARE @auftrag_calc Verkauf.TYPE_spAuftragEckdatenBerechnen;" "INSERT INTO @auftrag_calc VALUES (?);" "EXEC Verkauf.spAuftragEckdatenBerechnen @auftrag = @auftrag_calc"; std::vector ps_calc = {{ParamType::Int, "", kAuftrag}}; get_pool().execute(c, calc, ps_calc, rs); if (!get_pool().commit(c)) throw std::runtime_error("failed to commit transaction"); return { {"orderId", std::to_string(kAuftrag)}, {"orderNumber", cAuftragsNr}, {"alreadyExists", false} }; } catch (const std::exception&) { try { get_pool().rollback(c); } catch (...) {} throw; } } } // namespace order