This commit is contained in:
seb
2026-07-15 03:11:30 +02:00
parent 056c9e18cd
commit 1de4144ae2
23 changed files with 921 additions and 229 deletions

View File

@@ -4,8 +4,8 @@
#include "../queries/category_list.hpp" #include "../queries/category_list.hpp"
void handle_category(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) { void handle_category(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
int64_t cursor = std::stoll(req.get_query_param("lastChangedCategory", "0")); int64_t cursor = req.get_query_int64("lastChangedCategory");
int limit = std::stoi(req.get_query_param("limit", "20")); int limit = req.get_query_int("limit", 20);
auto categories = get_category_list(cursor, limit); auto categories = get_category_list(cursor, limit);
resp.send_json(200, categories); resp.send_json(200, categories);
} }

View File

@@ -6,16 +6,27 @@
#include "../tls_server.hpp" #include "../tls_server.hpp"
#include "../router.hpp" #include "../router.hpp"
static std::string config_string(const json& config, const char* key,
const std::string& def = "") {
if (!config.is_object()) return def;
json v = config.value(key, json());
if (v.is_null()) return def;
if (v.is_string()) return v.get<std::string>();
if (v.is_number_integer()) return std::to_string(v.get<int64_t>());
if (v.is_number_unsigned()) return std::to_string(v.get<uint64_t>());
return def;
}
static json build_client_step1(const json& config) { static json build_client_step1(const json& config) {
return { return {
{"authCode", nullptr}, {"authCode", nullptr},
{"authToken", config["authToken"]}, {"authToken", config_string(config, "authToken")},
{"certificateFingerprint", config["certificateFingerprint"]}, {"certificateFingerprint", config_string(config, "certificateFingerprint")},
{"certificateSerialNumber", config["certificateSerialNumber"]}, {"certificateSerialNumber", config_string(config, "certificateSerialNumber")},
{"mandantId", config["mandantId"]}, {"mandantId", config_string(config, "mandantId")},
{"mandantName", nullptr}, {"mandantName", nullptr},
{"mandantDatabase", nullptr}, {"mandantDatabase", nullptr},
{"serverFingerprint", config["serverFingerprint"]}, {"serverFingerprint", config_string(config, "serverFingerprint")},
{"name", nullptr}, {"name", nullptr},
{"serverTimestamp", server_timestamp()}, {"serverTimestamp", server_timestamp()},
}; };
@@ -24,12 +35,12 @@ static json build_client_step1(const json& config) {
static json build_client_step2(const std::string& auth_code, const json& config) { static json build_client_step2(const std::string& auth_code, const json& config) {
return { return {
{"authCode", auth_code}, {"authCode", auth_code},
{"authToken", config["authToken"]}, {"authToken", config_string(config, "authToken")},
{"certificateFingerprint", config["certificateFingerprint"]}, {"certificateFingerprint", config_string(config, "certificateFingerprint")},
{"certificateSerialNumber", config["certificateSerialNumber"]}, {"certificateSerialNumber", config_string(config, "certificateSerialNumber")},
{"mandantId", config["mandantId"]}, {"mandantId", config_string(config, "mandantId")},
{"mandantName", config["mandantName"]}, {"mandantName", config_string(config, "mandantName")},
{"mandantDatabase", config["mandantDatabase"]}, {"mandantDatabase", config_string(config, "mandantDatabase")},
{"serverFingerprint", nullptr}, {"serverFingerprint", nullptr},
{"name", nullptr}, {"name", nullptr},
{"serverTimestamp", server_timestamp()}, {"serverTimestamp", server_timestamp()},
@@ -47,7 +58,7 @@ void handle_client(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
if (auth_code.size() == 6) { if (auth_code.size() == 6) {
if (ctx.pairing_store->has_pairing_code(auth_code)) { if (ctx.pairing_store->has_pairing_code(auth_code)) {
ctx.pairing_store->revoke_pairing_code(auth_code); ctx.pairing_store->revoke_pairing_code(auth_code);
ctx.pairing_store->register_device(ctx.config["authToken"].get<std::string>(), name); ctx.pairing_store->register_device(config_string(ctx.config, "authToken"), name);
return resp.send_json(200, build_client_step2(auth_code, ctx.config)); return resp.send_json(200, build_client_step2(auth_code, ctx.config));
} }
return resp.send_json(400, { return resp.send_json(400, {

View File

@@ -4,7 +4,7 @@
#include "../queries/customer_groups.hpp" #include "../queries/customer_groups.hpp"
void handle_customergroup(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) { void handle_customergroup(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
int64_t cursor = std::stoll(req.get_query_param("lastChangedCustomerGroup", "0")); int64_t cursor = req.get_query_int64("lastChangedCustomerGroup");
auto groups = get_customer_group_list(cursor); auto groups = get_customer_group_list(cursor);
resp.send_json(200, groups); resp.send_json(200, groups);
} }

View File

@@ -4,8 +4,8 @@
#include "../queries/deleted_entity_list.hpp" #include "../queries/deleted_entity_list.hpp"
void handle_deleted_entity(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) { void handle_deleted_entity(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
int64_t cursor = std::stoll(req.get_query_param("lastChangedDeletedEntity", "0")); int64_t cursor = req.get_query_int64("lastChangedDeletedEntity");
int limit = std::stoi(req.get_query_param("limit", "600")); int limit = req.get_query_int("limit", 600);
auto deleted = get_deleted_entity_list(cursor, limit); auto deleted = get_deleted_entity_list(cursor, limit);
resp.send_json(200, deleted); resp.send_json(200, deleted);
} }

View File

@@ -8,11 +8,11 @@
#include "../config.hpp" #include "../config.hpp"
void handle_init(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) { void handle_init(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
int64_t product_cursor = std::stoll(req.get_query_param("lastChangedProduct", "0")); int64_t product_cursor = req.get_query_int64("lastChangedProduct");
int64_t category_cursor = std::stoll(req.get_query_param("lastChangedCategory", "0")); int64_t category_cursor = req.get_query_int64("lastChangedCategory");
int64_t cg_cursor = std::stoll(req.get_query_param("lastChangedCustomerGroup", "0")); int64_t cg_cursor = req.get_query_int64("lastChangedCustomerGroup");
int64_t composite_cursor = std::stoll(req.get_query_param("lastChangedCompositeProduct", "0")); int64_t composite_cursor = req.get_query_int64("lastChangedCompositeProduct");
int64_t deleted_cursor = std::stoll(req.get_query_param("lastChangedDeletedEntity", "0")); int64_t deleted_cursor = req.get_query_int64("lastChangedDeletedEntity");
int root = config::get_int("ROOT_CATEGORY_ID", 1); int root = config::get_int("ROOT_CATEGORY_ID", 1);
int shop = get_active_shop_id(); int shop = get_active_shop_id();
@@ -27,9 +27,6 @@ void handle_init(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
composite_count = get_composite_count(shop, composite_cursor); composite_count = get_composite_count(shop, composite_cursor);
deleted_count = get_deleted_count(deleted_cursor); deleted_count = get_deleted_count(deleted_cursor);
max_order_id_count = get_max_order_id_count(get_active_shop_subshop_id()); max_order_id_count = get_max_order_id_count(get_active_shop_subshop_id());
logc::info("init counts: products=%lld categories=%lld cg=%lld composite=%lld deleted=%lld max_order=%lld root=%d shop=%d subshop=%d",
product_count, category_count, cg_count, composite_count, deleted_count, max_order_id_count,
root, shop, get_active_shop_subshop_id());
if (product_cursor == 0 && category_cursor == 0 && if (product_cursor == 0 && category_cursor == 0 &&
product_count == 0 && category_count == 0 && deleted_count == 0) { product_count == 0 && category_count == 0 && deleted_count == 0) {
logc::warn("init: all counts zero with cursors at 0 — check DB connectivity and shop/category config"); logc::warn("init: all counts zero with cursors at 0 — check DB connectivity and shop/category config");

View File

@@ -4,8 +4,8 @@
#include "../queries/product_list.hpp" #include "../queries/product_list.hpp"
void handle_product(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) { void handle_product(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
int64_t cursor = std::stoll(req.get_query_param("lastChangedProduct", "0")); int64_t cursor = req.get_query_int64("lastChangedProduct");
int limit = std::stoi(req.get_query_param("limit", "20")); int limit = req.get_query_int("limit", 20);
auto products = get_product_list(cursor, limit); auto products = get_product_list(cursor, limit);
resp.send_json(200, products); resp.send_json(200, products);
} }

View File

@@ -4,8 +4,8 @@
#include "../queries/composite_product_list.hpp" #include "../queries/composite_product_list.hpp"
void handle_productcomposite(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) { void handle_productcomposite(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
int64_t cursor = std::stoll(req.get_query_param("lastChangedCompositeProduct", "0")); int64_t cursor = req.get_query_int64("lastChangedCompositeProduct");
int limit = std::stoi(req.get_query_param("limit", "100")); int limit = req.get_query_int("limit", 100);
auto composites = get_composite_product_list(cursor, limit); auto composites = get_composite_product_list(cursor, limit);
resp.send_json(200, composites); resp.send_json(200, composites);
} }

View File

@@ -7,6 +7,25 @@
#include <cstring> #include <cstring>
#include <ctime> #include <ctime>
// ---------------------------------------------------------------------------
// Safe parsing
// ---------------------------------------------------------------------------
int parse_int(const std::string& s, int fallback) {
if (s.empty()) return fallback;
try { return std::stoi(s); } catch (...) { return fallback; }
}
int64_t parse_int64(const std::string& s, int64_t fallback) {
if (s.empty()) return fallback;
try { return std::stoll(s); } catch (...) { return fallback; }
}
double parse_double(const std::string& s, double fallback) {
if (s.empty()) return fallback;
try { return std::stod(s); } catch (...) { return fallback; }
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// HttpRequest // HttpRequest
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -39,6 +58,14 @@ std::string HttpRequest::get_query_param(const std::string& key, const std::stri
return decoded; return decoded;
} }
int HttpRequest::get_query_int(const std::string& key, int def) const {
return parse_int(get_query_param(key), def);
}
int64_t HttpRequest::get_query_int64(const std::string& key, int64_t def) const {
return parse_int64(get_query_param(key), def);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// HttpResponse // HttpResponse
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -17,6 +17,11 @@ struct tls_session;
using json = nlohmann::json; using json = nlohmann::json;
// Safe string-to-number parsing (never throws).
int parse_int(const std::string& s, int fallback = 0);
int64_t parse_int64(const std::string& s, int64_t fallback = 0);
double parse_double(const std::string& s, double fallback = 0.0);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Parsed HTTP request // Parsed HTTP request
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -32,6 +37,8 @@ struct HttpRequest {
std::string query_string; // "authCode=xxx&name=yyy" std::string query_string; // "authCode=xxx&name=yyy"
std::string get_query_param(const std::string& key, const std::string& def = "") const; std::string get_query_param(const std::string& key, const std::string& def = "") const;
int get_query_int(const std::string& key, int def = 0) const;
int64_t get_query_int64(const std::string& key, int64_t def = 0) const;
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -82,6 +82,14 @@ static void handle_request(tls_session* sess) {
} }
size_t resp_size = resp_body.size(); size_t resp_size = resp_body.size();
const bool is_init = (req.path == "/v1/init");
const bool log_request = !is_init
|| router.should_log_init(url, resp.status_code, static_cast<int>(elapsed), resp_body);
if (!log_request) {
return;
}
// Console log with response size and truncated body // Console log with response size and truncated body
const char* ip = sess->peer_ip.c_str(); const char* ip = sess->peer_ip.c_str();
if (resp_size > 0) { if (resp_size > 0) {
@@ -132,7 +140,7 @@ int main(int /*argc*/, char* argv[]) {
// Initialize pairing store // Initialize pairing store
pairing_store.set_pairing_code(config::get("PAIRING_CODE", "307018"), "JTL-POS"); pairing_store.set_pairing_code(config::get("PAIRING_CODE", "307018"), "JTL-POS");
pairing_store.register_device(server_config["authToken"].get<std::string>(), "JTL-POS"); pairing_store.register_device(server_config.value("authToken", std::string("df40ad2067954646abb0499548a52241")), "JTL-POS");
// Connect to MSSQL // Connect to MSSQL
if (get_pool().connect() == 0) { if (get_pool().connect() == 0) {

View File

@@ -41,7 +41,8 @@ inline nlohmann::json get_category_list(int64_t cursor, int limit) {
auto ts = server_timestamp(); auto ts = server_timestamp();
nlohmann::json result = nlohmann::json::array(); nlohmann::json result = nlohmann::json::array();
for (auto& row : rs) { for (auto& row : rs) {
std::string pid = (std::stoll(row[1].str) == root) ? "0" : row[1].str; int64_t parent_id = parse_int64(row[1].str, 0);
std::string pid = (parent_id == root) ? "0" : row[1].str;
result.push_back({ result.push_back({
{"_id", row[0].str}, {"_id", row[0].str},
{"imghash", row[4].str.empty() ? nullptr : nlohmann::json(row[4].str)}, {"imghash", row[4].str.empty() ? nullptr : nlohmann::json(row[4].str)},

View File

@@ -35,6 +35,11 @@ struct Config {
int customerNumberSequence = 6; 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; Config g_config;
std::unique_ptr<Defaults> g_resolved_defaults; std::unique_ptr<Defaults> g_resolved_defaults;
std::map<std::string, nlohmann::json> g_zahlungsart_cache; std::map<std::string, nlohmann::json> g_zahlungsart_cache;
@@ -128,6 +133,12 @@ inline int json_int(const nlohmann::json& obj, const char* key, int fallback = 0
return to_int(*it, 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) { inline int steuerklasse_for_vat(double vat) {
if (vat >= 15) return 1; if (vat >= 15) return 1;
if (vat > 0) return 2; if (vat > 0) return 2;
@@ -232,38 +243,158 @@ inline int allocate_pk(OdbcPool::Connection* c, const std::string& tableName) {
return std::stoi(rs[0][0].str); 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<VersandArtRow> lookup_versand_art(OdbcPool::Connection* c,
const std::string& shipping_name) {
auto try_lookup = [&](const std::string& cName) -> std::optional<VersandArtRow> {
std::vector<Param> 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) { inline nlohmann::json resolve_zahlungsart(OdbcPool::Connection* c, const std::string& name) {
std::string key = name.empty() ? "bar" : name; const std::string lookup_name = name.empty() ? "Bar" : name;
std::transform(key.begin(), key.end(), key.begin(), [](unsigned char ch) { return std::tolower(ch); }); const std::string cache_key = [&]() {
auto it = g_zahlungsart_cache.find(key); std::string key = lookup_name;
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
return key;
}();
auto it = g_zahlungsart_cache.find(cache_key);
if (it != g_zahlungsart_cache.end()) return it->second; if (it != g_zahlungsart_cache.end()) return it->second;
std::vector<Param> ps = {{ParamType::NVarChar, key, 0}}; auto fetch_row = [&](const char* sql) -> std::optional<std::pair<int, std::string>> {
ResultSet rs; std::vector<Param> ps = {{ParamType::NVarChar, lookup_name, 0}};
get_pool().execute(c, "SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE cName = ?", ps, rs); 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);
};
if (!rs.empty() && !rs[0].empty()) { std::optional<std::pair<int, std::string>> row =
nlohmann::json z = {{"kZahlungsart", std::stoi(rs[0][0].str)}, {"cName", rs[0][1].str}}; fetch_row("SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE cName = ?");
g_zahlungsart_cache[key] = z; if (!row) {
return z; 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(?)");
} }
int kZahlungsart = allocate_pk(c, "tZahlungsart"); nlohmann::json zahlungsart;
const char* insert = if (row) {
"INSERT INTO dbo.tZahlungsart" zahlungsart = {{"kZahlungsart", row->first}, {"cName", row->second}};
" (kZahlungsart, cName, cPrtString, nLastschrift, cPrtStringVor, cPaymentOption, cKonto," } else {
" nAusliefernVorZahlung, nPrioritaet, nMahnwesenAktiv, fSkontoWert, nSkontoZeitraum," int kZahlungsart = allocate_pk(c, "tZahlungsart");
" nMatchingOptionen, nIstStandard, nAktiv)" const char* insert =
" VALUES (?, ?, '', 0, '', '', '', 0, 0, 0, 0, 0, 0, 0, 1)"; "INSERT INTO dbo.tZahlungsart"
std::vector<Param> ps2 = { " (kZahlungsart, cName, cPrtString, nLastschrift, cPrtStringVor, cPaymentOption, cKonto,"
{ParamType::Int, "", kZahlungsart}, " nAusliefernVorZahlung, nPrioritaet, nMahnwesenAktiv, fSkontoWert, nSkontoZeitraum,"
{ParamType::NVarChar, key, 0} " nMatchingOptionen, nIstStandard, nAktiv)"
}; " VALUES (?, ?, '', 0, '', '', '', 0, 0, 0, 0, 0, 0, 0, 1)";
get_pool().execute(c, insert, ps2, rs); std::vector<Param> ps2 = {
{ParamType::Int, "", kZahlungsart},
{ParamType::NVarChar, lookup_name, 0}
};
ResultSet rs;
get_pool().execute(c, insert, ps2, rs);
zahlungsart = {{"kZahlungsart", kZahlungsart}, {"cName", lookup_name}};
}
nlohmann::json z = {{"kZahlungsart", kZahlungsart}, {"cName", key}}; g_zahlungsart_cache[cache_key] = zahlungsart;
g_zahlungsart_cache[key] = z; return zahlungsart;
return z;
} }
inline std::string next_customer_number(OdbcPool::Connection* c) { inline std::string next_customer_number(OdbcPool::Connection* c) {
@@ -303,21 +434,6 @@ inline std::string resolve_auftrag_c_kunden_nr(const nlohmann::json& order) {
return order.value("customerNumber", ""); return order.value("customerNumber", "");
} }
// POS settings.invoiceSetting: 0 = external/POS invoice (nIstExterneRechnung=1).
inline int resolve_n_ist_externe_rechnung(const nlohmann::json& order) {
if (!order.contains("settings") || order["settings"].is_null()) return 1;
const auto& settings = order["settings"];
if (!settings.contains("invoiceSetting")) return 1;
const auto& inv = settings["invoiceSetting"];
if (inv.is_boolean()) return inv.get<bool>() ? 0 : 1;
if (inv.is_number_integer()) return inv.get<int>() == 1 ? 0 : 1;
if (inv.is_string()) {
std::string s = inv.get<std::string>();
return (s == "1" || s == "true") ? 0 : 1;
}
return 1;
}
inline std::pair<int,int> create_customer(OdbcPool::Connection* c, const std::string& customer_number, inline std::pair<int,int> create_customer(OdbcPool::Connection* c, const std::string& customer_number,
const nlohmann::json& address, const Defaults& defaults) { const nlohmann::json& address, const Defaults& defaults) {
const nlohmann::json a = address.is_null() ? nlohmann::json::object() : address; const nlohmann::json a = address.is_null() ? nlohmann::json::object() : address;
@@ -328,8 +444,8 @@ inline std::pair<int,int> create_customer(OdbcPool::Connection* c, const std::st
const char* sql = const char* sql =
"DECLARE @returnValue INT;" "DECLARE @returnValue INT;"
"DECLARE @p1 dbo.TYPE_spkundeInsert;" "DECLARE @kunde_daten dbo.TYPE_spkundeInsert;"
"INSERT INTO @p1" "INSERT INTO @kunde_daten"
" (kInetKunde, kKundenKategorie, cKundenNr, cFirma, cAnrede, cTitel, cVorname, cName," " (kInetKunde, kKundenKategorie, cKundenNr, cFirma, cAnrede, cTitel, cVorname, cName,"
" cStrasse, cPLZ, cOrt, cLand, cTel, cFax, cEMail, dErstellt, cMobil, fRabatt, cUSTID, cNewsletter," " cStrasse, cPLZ, cOrt, cLand, cTel, cFax, cEMail, dErstellt, cMobil, fRabatt, cUSTID, cNewsletter,"
" cZusatz, cEbayName, kBuyer, cAdressZusatz, cGeburtstag, cWWW, cSperre, cPostID, kKundenGruppe," " cZusatz, cEbayName, kBuyer, cAdressZusatz, cGeburtstag, cWWW, cSperre, cPostID, kKundenGruppe,"
@@ -342,7 +458,7 @@ inline std::pair<int,int> create_customer(OdbcPool::Connection* c, const std::st
" 0, ?, ?, ?, ?, ?, N'', 0," " 0, ?, ?, ?, ?, ?, N'', 0,"
" ?, N'', 0, 0, 0, 0, 0," " ?, N'', 0, 0, 0, 0, 0,"
" NULL, 0, 0, 0);" " NULL, 0, 0, 0);"
"EXEC @returnValue = Kunde.spKundeInsert @daten = @p1;" "EXEC @returnValue = Kunde.spKundeInsert @daten = @kunde_daten;"
"SELECT @returnValue AS kKunde"; "SELECT @returnValue AS kKunde";
std::vector<Param> ps = { std::vector<Param> ps = {
@@ -360,7 +476,7 @@ inline std::pair<int,int> create_customer(OdbcPool::Connection* c, const std::st
{ParamType::NVarChar, a.value("fax", ""), 0}, {ParamType::NVarChar, a.value("fax", ""), 0},
{ParamType::NVarChar, a.value("email", ""), 0}, {ParamType::NVarChar, a.value("email", ""), 0},
{ParamType::NVarChar, a.value("mobile", ""), 0}, {ParamType::NVarChar, a.value("mobile", ""), 0},
{ParamType::Double, "", 0, to_number(a["discount"], 0)}, {ParamType::Double, "", 0, json_double(a, "discount", 0)},
{ParamType::NVarChar, a.value("addressAddition", ""), 0}, {ParamType::NVarChar, a.value("addressAddition", ""), 0},
{ParamType::NVarChar, a.value("birthday", ""), 0}, {ParamType::NVarChar, a.value("birthday", ""), 0},
{ParamType::Int, "", kKundengruppe}, {ParamType::Int, "", kKundengruppe},
@@ -445,17 +561,18 @@ inline void insert_order_address(OdbcPool::Connection* c, int kAuftrag, int kKun
} }
inline int insert_order_item(OdbcPool::Connection* c, int kAuftrag, const nlohmann::json& item) { inline int insert_order_item(OdbcPool::Connection* c, int kAuftrag, const nlohmann::json& item) {
double vat = to_number(item["vat"], 19); double vat = json_double(item, "vat", 19);
double quantity = to_number(item["quantity"], 1); double quantity = json_double(item, "quantity", 1);
double price_gross = to_number(item["priceGross"], 0); double price_gross = json_double(item, "priceGross", 0);
double price_net = to_number(item["priceNet"], price_gross / (1 + vat / 100)); double price_net = json_double(item, "priceNet", price_gross / (1 + vat / 100));
double discount = to_number(item["discountPercent"], 0); double discount = json_double(item, "discountPercent", 0);
int kSteuerklasse = steuerklasse_for_vat(vat); int kSteuerklasse = steuerklasse_for_vat(vat);
std::string sku = item.value("sku", ""); std::string sku = item.value("sku", "");
const int position_type = is_versandposition(item) ? VERSANDPOSITION_TYPE : json_int(item, "type", 0);
int kArtikel = 0; int kArtikel = 0;
bool has_artikel = false; bool has_artikel = false;
if (!sku.empty()) { if (!sku.empty() && position_type != VERSANDPOSITION_TYPE) {
std::vector<Param> ps = {{ParamType::NVarChar, sku, 0}}; std::vector<Param> ps = {{ParamType::NVarChar, sku, 0}};
ResultSet rs; ResultSet rs;
get_pool().execute(c, "SELECT TOP 1 kArtikel FROM dbo.tArtikel WHERE cArtNr = ?", ps, rs); get_pool().execute(c, "SELECT TOP 1 kArtikel FROM dbo.tArtikel WHERE cArtNr = ?", ps, rs);
@@ -465,18 +582,20 @@ inline int insert_order_item(OdbcPool::Connection* c, int kAuftrag, const nlohma
} }
} }
const int nType = position_type == VERSANDPOSITION_TYPE ? VERSANDPOSITION_TYPE : (has_artikel ? 1 : 0);
const int nReserviert = nType == VERSANDPOSITION_TYPE ? 0 : 1;
const char* sql = const char* sql =
"DECLARE @t TABLE ([kAuftragPosition] INT);" "DECLARE @t TABLE ([kAuftragPosition] INT);"
"INSERT INTO Verkauf.tAuftragPosition" "INSERT INTO Verkauf.tAuftragPosition"
" (kArtikel, kAuftrag, cArtNr, nReserviert, cName, cHinweis, fAnzahl, fVkNetto, fMwSt," " (kArtikel, kAuftrag, cArtNr, nReserviert, cName, cHinweis, fAnzahl, fVkNetto, fMwSt,"
" cNameStandard, kSteuerklasse, nType, cEinheit, fFaktor, kSteuerschluessel, fRabatt)" " cNameStandard, kSteuerklasse, nType, cEinheit, fFaktor, kSteuerschluessel, fRabatt)"
" OUTPUT inserted.kAuftragPosition INTO @t" " OUTPUT inserted.kAuftragPosition INTO @t"
" VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?," " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,"
" ?, ?, ?, ?, 1.0, 3, ?);" " ?, ?, ?, ?, 1.0, 3, ?);"
"SELECT kAuftragPosition FROM @t"; "SELECT kAuftragPosition FROM @t";
Param cartnr{ParamType::NVarChar, has_artikel ? sku : std::string(), 0, 0.0, !has_artikel}; Param cartnr{ParamType::NVarChar, has_artikel ? sku : std::string(), 0, 0.0, !has_artikel};
Param pk_art{ParamType::Int, "", kArtikel, 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::string name = item.value("name", sku.empty() ? std::string("Position") : sku);
@@ -484,6 +603,7 @@ inline int insert_order_item(OdbcPool::Connection* c, int kAuftrag, const nlohma
pk_art, pk_art,
{ParamType::Int, "", kAuftrag}, {ParamType::Int, "", kAuftrag},
cartnr, cartnr,
{ParamType::Int, "", nReserviert},
{ParamType::NVarChar, name, 0}, {ParamType::NVarChar, name, 0},
{ParamType::NVarChar, item.value("note", ""), 0}, {ParamType::NVarChar, item.value("note", ""), 0},
{ParamType::Double, "", 0, quantity}, {ParamType::Double, "", 0, quantity},
@@ -491,7 +611,7 @@ inline int insert_order_item(OdbcPool::Connection* c, int kAuftrag, const nlohma
{ParamType::Double, "", 0, vat}, {ParamType::Double, "", 0, vat},
{ParamType::NVarChar, name, 0}, {ParamType::NVarChar, name, 0},
{ParamType::Int, "", kSteuerklasse}, {ParamType::Int, "", kSteuerklasse},
{ParamType::Int, "", has_artikel ? 1 : 0}, {ParamType::Int, "", nType},
{ParamType::NVarChar, item.value("unit", ""), 0}, {ParamType::NVarChar, item.value("unit", ""), 0},
{ParamType::Double, "", 0, discount} {ParamType::Double, "", 0, discount}
}; };
@@ -608,6 +728,39 @@ inline bool is_order_delivered(const nlohmann::json& order) {
return true; 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<Param> 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<double> 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<Param> 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, inline void insert_payment(OdbcPool::Connection* c, int kAuftrag, const nlohmann::json& payment,
const nlohmann::json& order, const std::tm& order_date_tm) { const nlohmann::json& order, const std::tm& order_date_tm) {
std::string payment_name = payment.value("paymentMethodName", ""); std::string payment_name = payment.value("paymentMethodName", "");
@@ -615,25 +768,46 @@ inline void insert_payment(OdbcPool::Connection* c, int kAuftrag, const nlohmann
nlohmann::json zahlungsart = resolve_zahlungsart(c, payment_name); nlohmann::json zahlungsart = resolve_zahlungsart(c, payment_name);
int kZahlung = allocate_pk(c, "tZahlung"); 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 = const char* sql =
"INSERT INTO dbo.tZahlung" "IF EXISTS ("
" (kZahlung, cName, dDatum, fBetrag, kBestellung, kBenutzer, nAnzahlung, cHinweis, kZahlungsart," " SELECT 1 FROM Verkauf.tAuftragEckdaten"
" nKeinExport, cExternalTransactionId, nZuweisungstyp, nZahlungstyp, cZuweisungsinfo, nZuweisungswertung)" " WHERE kAuftrag = ?"
" VALUES (?, ?, ?, ?, ?, ?, 0, '', ?," " AND ROUND(fOffenerWertOhneStorno, 2) = ROUND(?, 2)"
" 0, ?, 0, 0, '', 0)"; ")"
"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<Param> ps = { std::vector<Param> ps = {
{ParamType::Int, "", kAuftrag},
{ParamType::Double, "", 0, *fOffenerWert},
{ParamType::Int, "", kZahlung}, {ParamType::Int, "", kZahlung},
{ParamType::NVarChar, zahlungsart.value("cName", ""), 0}, {ParamType::NVarChar, zahlungsart.value("cName", ""), 0},
{ParamType::NVarChar, order_date_sql(order_date_tm), 0}, {ParamType::NVarChar, order_date_sql(order_date_tm), 0},
{ParamType::Double, "", 0, to_number(payment["amount"], 0)}, {ParamType::Double, "", 0, json_double(payment, "amount", 0)},
{ParamType::Int, "", kAuftrag}, {ParamType::Int, "", kAuftrag},
{ParamType::Int, "", g_config.kBenutzer}, {ParamType::Int, "", g_config.kBenutzer},
{ParamType::Int, "", json_int(zahlungsart, "kZahlungsart", 0)}, {ParamType::Int, "", json_int(zahlungsart, "kZahlungsart", 0)},
{ParamType::NVarChar, order.value("externalOrderNumber", ""), 0} {ParamType::NVarChar, order.value("externalOrderNumber", ""), 0},
{ParamType::Int, "", ZAHLUNG_TYPE_ZAHLUNG},
}; };
ResultSet rs; ResultSet rs;
get_pool().execute(c, sql, ps, 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) { inline nlohmann::json create_order(const nlohmann::json& order) {
@@ -678,7 +852,18 @@ inline nlohmann::json create_order(const nlohmann::json& order) {
nlohmann::json zahlungsart = resolve_zahlungsart(c, order.value("paymentMethodName", "Bar")); nlohmann::json zahlungsart = resolve_zahlungsart(c, order.value("paymentMethodName", "Bar"));
std::string cAuftragsNr = next_order_number(c, order_date_tm); 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(); 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 = const char* insert_auftrag =
"DECLARE @t TABLE ([kAuftrag] INT);" "DECLARE @t TABLE ([kAuftrag] INT);"
@@ -700,7 +885,6 @@ inline nlohmann::json create_order(const nlohmann::json& order) {
std::string currency_iso = order.value("currencyIso", "EUR"); std::string currency_iso = order.value("currencyIso", "EUR");
std::string cKundenNr = resolve_auftrag_c_kunden_nr(order); std::string cKundenNr = resolve_auftrag_c_kunden_nr(order);
int nIstExterneRechnung = resolve_n_ist_externe_rechnung(order);
std::vector<Param> ps = { std::vector<Param> ps = {
{ParamType::NVarChar, cAuftragsNr, 0}, {ParamType::NVarChar, cAuftragsNr, 0},
{ParamType::NVarChar, order_date_sql(order_date_tm), 0}, {ParamType::NVarChar, order_date_sql(order_date_tm), 0},
@@ -715,13 +899,13 @@ inline nlohmann::json create_order(const nlohmann::json& order) {
{ParamType::Int, "", active_shop == 0 ? -1 : active_shop}, {ParamType::Int, "", active_shop == 0 ? -1 : active_shop},
{ParamType::NVarChar, cKundenNr, 0}, {ParamType::NVarChar, cKundenNr, 0},
{ParamType::NVarChar, shipping_iso, 0}, {ParamType::NVarChar, shipping_iso, 0},
{ParamType::Int, "", defaults.kVersandArt}, {ParamType::Int, "", kVersandArt},
{ParamType::Int, "", json_int(zahlungsart, "kZahlungsart", 0)}, {ParamType::Int, "", json_int(zahlungsart, "kZahlungsart", 0)},
{ParamType::Int, "", kKundengruppe}, {ParamType::Int, "", kKundengruppe},
{ParamType::NVarChar, order.value("externalOrderNumber", ""), 0}, {ParamType::NVarChar, order.value("externalOrderNumber", ""), 0},
{ParamType::Int, "", nIstExterneRechnung}, {ParamType::Int, "", nIstExterneRechnung},
{ParamType::NVarChar, "Y", 0}, {ParamType::NVarChar, "Y", 0},
{ParamType::Int, "", 2}, {ParamType::Int, "", nIstReadOnly},
}; };
if (active_shop == 0) { if (active_shop == 0) {
ps[11] = Param::null_int(); ps[11] = Param::null_int();
@@ -738,22 +922,25 @@ inline nlohmann::json create_order(const nlohmann::json& order) {
insert_order_address(c, kAuftrag, kKunde, order.value("billingAddress", nlohmann::json::object()), 1); insert_order_address(c, kAuftrag, kKunde, order.value("billingAddress", nlohmann::json::object()), 1);
std::vector<delivery::DeliveredItem> delivered_items; std::vector<delivery::DeliveredItem> delivered_items;
const nlohmann::json& items = order.contains("orderItems") ? order["orderItems"] : nlohmann::json::array(); for (const auto& item : order_items) {
for (const auto& item : items) {
int kAuftragPosition = insert_order_item(c, kAuftrag, item); int kAuftragPosition = insert_order_item(c, kAuftrag, item);
insert_pos_order_position_mapping(c, kAuftragPosition, item.value("externalId", "")); const std::string external_id = item.value("externalId", "");
if (kAuftragPosition > 0) { if (!external_id.empty()) {
delivered_items.push_back({kAuftragPosition, to_number(item["quantity"], 1)}); 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(); const nlohmann::json& payments = order.contains("payments") ? order["payments"] : nlohmann::json::array();
for (const auto& payment : payments) { for (const auto& payment : payments) {
if (!is_new_payment(payment)) continue;
insert_payment(c, kAuftrag, payment, order, order_date_tm); insert_payment(c, kAuftrag, payment, order, order_date_tm);
} }
if (is_order_delivered(order)) { if (is_order_delivered(order)) {
delivery::deliver_order(c, g_config.kBenutzer, kAuftrag, defaults.kVersandArt, delivered_items); delivery::deliver_order(c, g_config.kBenutzer, kAuftrag, kVersandArt, delivered_items);
} }
const char* calc = const char* calc =

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "../db/pool.hpp" #include "../db/pool.hpp"
#include "../log.hpp" #include "../log.hpp"
#include "../http.hpp"
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
static const char* CUSTOMER_GROUP_IDS_SQL = static const char* CUSTOMER_GROUP_IDS_SQL =
@@ -27,7 +28,7 @@ inline std::vector<int64_t> get_customer_group_ids() {
return {}; return {};
} }
for (auto& row : rs) { for (auto& row : rs) {
cached.push_back(std::stoll(row[0].str)); cached.push_back(parse_int64(row[0].str, 0));
} }
loaded = true; loaded = true;
return cached; return cached;
@@ -43,7 +44,7 @@ inline nlohmann::json get_customer_group_list(int64_t cursor = 0) {
{"customerGroupId", row[0].str}, {"customerGroupId", row[0].str},
{"name", row[1].str}, {"name", row[1].str},
{"standard", row[2].str}, {"standard", row[2].str},
{"discountPercent", std::to_string(std::stod(row[3].str))}, {"discountPercent", std::to_string(parse_double(row[3].str, 0))},
{"lastChanged", row[4].str} {"lastChanged", row[4].str}
}); });
} }

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "../db/pool.hpp" #include "../db/pool.hpp"
#include "../log.hpp" #include "../log.hpp"
#include "../http.hpp"
#include <string> #include <string>
#include <vector> #include <vector>
#include <algorithm> #include <algorithm>
@@ -83,10 +84,10 @@ inline ImageResult get_image_by_hash(const std::string& hash, const std::string&
auto& row = rs[0]; auto& row = rs[0];
int target = size.empty() ? 200 : std::stoi(size); int target = parse_int(size, 200);
std::string ct = content_type_for(row[6].str); std::string ct = content_type_for(row[6].str);
int preview_w = row[4].str.empty() ? 0 : std::stoi(row[4].str); int preview_w = row[4].str.empty() ? 0 : parse_int(row[4].str, 0);
int preview_h = row[5].str.empty() ? 0 : std::stoi(row[5].str); int preview_h = row[5].str.empty() ? 0 : parse_int(row[5].str, 0);
int preview_max = std::max(preview_w, preview_h); int preview_max = std::max(preview_w, preview_h);
bool has_full = !row[0].blob.empty(); bool has_full = !row[0].blob.empty();

View File

@@ -43,8 +43,8 @@ static const char* PRODUCT_LIST_SQL =
"ORDER BY lastChanged ASC"; "ORDER BY lastChanged ASC";
static std::string gross_price(const std::string& net, const std::string& tax) { static std::string gross_price(const std::string& net, const std::string& tax) {
double n = std::stod(net.empty() ? "0" : net); double n = parse_double(net, 0);
double t = std::stod(tax.empty() ? "0" : tax); double t = parse_double(tax, 0);
char buf[32]; char buf[32];
std::snprintf(buf, sizeof(buf), "%.2f", n * (1.0 + t / 100.0)); std::snprintf(buf, sizeof(buf), "%.2f", n * (1.0 + t / 100.0));
return buf; return buf;
@@ -107,7 +107,7 @@ inline nlohmann::json get_product_list(int64_t cursor, int limit) {
{"imgsrc", row[7].str.empty() ? nullptr : nlohmann::json(row[7].str)}, {"imgsrc", row[7].str.empty() ? nullptr : nlohmann::json(row[7].str)},
{"sku", row[1].str}, {"sku", row[1].str},
{"name", row[2].str}, {"name", row[2].str},
{"tax_rate", std::to_string((int)std::round(std::stod(row[4].str.empty()?"0":row[4].str)))}, {"tax_rate", std::to_string((int)std::round(parse_double(row[4].str, 0)))},
{"price", base_price}, {"price", base_price},
{"created_at", row[5].str}, {"created_at", row[5].str},
{"lastChanged", row[6].str}, {"lastChanged", row[6].str},
@@ -115,7 +115,7 @@ inline nlohmann::json get_product_list(int64_t cursor, int limit) {
{"categories", cats}, {"categories", cats},
{"prices", prices}, {"prices", prices},
{"is_parent", row[9].str == "1" ? "1" : "0"}, {"is_parent", row[9].str == "1" ? "1" : "0"},
{"parent", std::stoll(row[10].str) > 0 ? row[10].str : "0"}, {"parent", parse_int64(row[10].str, 0) > 0 ? row[10].str : "0"},
{"variants", row[12].str}, {"variants", row[12].str},
{"isCompositeProduct", row[11].str}, {"isCompositeProduct", row[11].str},
{"attributes", nlohmann::json::array()}, {"attributes", nlohmann::json::array()},

View File

@@ -3,6 +3,7 @@
#include <chrono> #include <chrono>
#include <algorithm> #include <algorithm>
#include <exception>
void Router::add_route(const std::string& method, const std::string& path, Handler handler) { void Router::add_route(const std::string& method, const std::string& path, Handler handler) {
std::string key = method + " " + path; std::string key = method + " " + path;
@@ -27,7 +28,21 @@ void Router::dispatch(tls_session* sess, PairingStore& pairing, const json& conf
ctx.full_url = full_url; ctx.full_url = full_url;
ctx.pairing_store = &pairing; ctx.pairing_store = &pairing;
ctx.config = config; ctx.config = config;
it->second(req, resp, ctx); try {
it->second(req, resp, ctx);
} catch (const std::exception& ex) {
logc::error("request handler error: %s %s — %s",
req.method.c_str(), full_url.c_str(), ex.what());
if (!resp.headers_sent) {
resp.send_json(500, {{"Message", "Internal server error"}});
}
} catch (...) {
logc::error("request handler error: %s %s — unknown exception",
req.method.c_str(), full_url.c_str());
if (!resp.headers_sent) {
resp.send_json(500, {{"Message", "Internal server error"}});
}
}
return; return;
} }

View File

@@ -9,6 +9,7 @@
"scripts": { "scripts": {
"cert": "node generate-cert.js", "cert": "node generate-cert.js",
"backup:s3": "node scripts/s3-backup/backup.mjs", "backup:s3": "node scripts/s3-backup/backup.mjs",
"backup:s3:quick": "node scripts/s3-backup/backup.mjs --skip-trust",
"start": "node --watch server.js", "start": "node --watch server.js",
"test:client": "node test-client.js" "test:client": "node test-client.js"
}, },

View File

@@ -0,0 +1,51 @@
# MSSQL backup via local S3 endpoint
Custom S3-compatible HTTPS server (no MinIO). SQL Server 2022+ backs up with `BACKUP TO URL`; files land on disk under `data/sqlbackups/`.
## Quick start
```bash
npm run backup:s3
```
First run installs the CA into the `mssql` Docker container (`/var/opt/mssql/security/ca-certificates/`) and restarts SQL Server — required on Linux.
## Commands
| Command | Description |
|---------|-------------|
| `npm run backup:s3` | Start S3 endpoint + backup `MSSQL_DATABASE` from `.env` |
| `npm run backup:s3:quick` | Same, but skip PAL CA install/restart (after first setup) |
| `node scripts/s3-backup/backup.mjs --all` | Backup `eazybusiness` and `Mandant_3` |
| `node scripts/s3-backup/backup.mjs --server-only` | Run endpoint only |
| `npm run backup:s3 -- --skip-trust` | Skip CA install (npm needs `--` before script args) |
## Layout
```
scripts/s3-backup/
server.mjs S3-compatible HTTPS server (SigV4, multipart upload)
backup.mjs Orchestrator: trust CA → start server → sqlcmd BACKUP
config.mjs Host, port, credentials
ensure-certs.mjs TLS certs + Docker MSSQL PAL trust
sigv4.mjs AWS Signature V4 verification
data/sqlbackups/ Backup files written here
```
## Configuration
Reads `MSSQL_*` from repo `.env`. Optional overrides:
| Variable | Default | Description |
|----------|---------|-------------|
| `S3_BACKUP_HOST` | Docker bridge IP (`172.17.0.1`) | Host SQL Server uses in `s3://` URL |
| `S3_BACKUP_PORT` | `19443` | HTTPS port |
| `S3_BACKUP_ACCESS_KEY` | `jtlsrv-s3` | S3 access key |
| `S3_BACKUP_SECRET_KEY` | `jtlsrv-s3-secret` | S3 secret key |
| `MSSQL_DOCKER_CONTAINER` | `mssql` | Container name for CA install |
## Notes
- MSSQL runs in Docker: the endpoint binds `0.0.0.0` and uses the docker bridge IP so the container can reach it.
- SQL Server on Linux uses **SQLPAL** for outbound TLS — the CA must be in `/var/opt/mssql/security/ca-certificates/`, not only the OS trust store.
- Regenerating certs requires re-running without `--skip-trust` so PAL stays in sync.

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
import { spawnSync } from 'node:child_process'; import { fork, spawn, spawnSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import https from 'node:https'; import https from 'node:https';
import path from 'node:path'; import path from 'node:path';
@@ -14,10 +14,10 @@ import {
HOST, HOST,
PORT, PORT,
SECRET_KEY, SECRET_KEY,
SERVER_CERT,
s3BaseUrl, s3BaseUrl,
} from './config.mjs'; } from './config.mjs';
import { ensureCerts, installCaTrust } from './ensure-certs.mjs'; import { caTrustStatus, ensureCerts, installCaTrust } from './ensure-certs.mjs';
import { startServer } from './server.mjs';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
dotenv.config({ path: path.join(root, '.env') }); dotenv.config({ path: path.join(root, '.env') });
@@ -35,60 +35,114 @@ function sqlcmd(query) {
const port = process.env.MSSQL_PORT || '1433'; const port = process.env.MSSQL_PORT || '1433';
const user = process.env.MSSQL_USER || 'sa'; const user = process.env.MSSQL_USER || 'sa';
const password = process.env.MSSQL_PASSWORD || ''; const password = process.env.MSSQL_PASSWORD || '';
const result = spawnSync( return new Promise((resolve, reject) => {
'sqlcmd', const child = spawn(
['-W', '-C', '-S', `${server},${port}`, '-U', user, '-P', password, '-Q', query], 'sqlcmd',
{ encoding: 'utf8' } ['-W', '-C', '-S', `${server},${port}`, '-U', user, '-P', password, '-Q', query],
); { encoding: 'utf8' }
const output = `${result.stdout || ''}${result.stderr || ''}`; );
if (result.status !== 0 || /^\s*Msg \d+,/m.test(output)) { let stdout = '';
throw new Error(output.trim() || 'sqlcmd failed'); let stderr = '';
} child.stdout?.on('data', (chunk) => {
return result.stdout; stdout += chunk;
process.stdout.write(chunk);
});
child.stderr?.on('data', (chunk) => {
stderr += chunk;
process.stderr.write(chunk);
});
child.on('close', (code) => {
const output = `${stdout}${stderr}`;
if (code !== 0 || /^\s*Msg \d+,/m.test(output)) {
reject(new Error(output.trim() || 'sqlcmd failed'));
} else {
resolve(stdout);
}
});
child.on('error', reject);
});
} }
function timestamp() { function timestamp() {
return new Date().toISOString().replace(/[:.]/g, '-'); return new Date().toISOString().replace(/[:.]/g, '-');
} }
async function isServerUp() {
const probeHost = /^\d+\./.test(HOST) ? '127.0.0.1' : HOST;
return new Promise((resolve) => {
const opts = {
host: probeHost,
port: PORT,
path: '/',
method: 'GET',
rejectUnauthorized: false,
};
if (!/^\d+\./.test(HOST)) {
opts.servername = HOST;
}
const req = https.request(opts, (res) => {
res.resume();
resolve(res.statusCode === 403 || res.statusCode === 200);
});
req.on('error', () => resolve(false));
req.setTimeout(1000, () => {
req.destroy();
resolve(false);
});
req.end();
});
}
async function waitForServer() { async function waitForServer() {
for (let i = 0; i < 40; i++) { for (let i = 0; i < 40; i++) {
const ok = await new Promise((resolve) => { if (await isServerUp()) return;
const req = https.request(
{ host: HOST, port: PORT, path: '/', method: 'GET', rejectUnauthorized: false },
(res) => {
res.resume();
resolve(res.statusCode === 403 || res.statusCode === 200);
}
);
req.on('error', () => resolve(false));
req.end();
});
if (ok) return;
await new Promise((r) => setTimeout(r, 250)); await new Promise((r) => setTimeout(r, 250));
} }
throw new Error(`S3 endpoint did not start on https://${HOST}:${PORT}`); throw new Error(`S3 endpoint did not start on https://${HOST}:${PORT}`);
} }
function pidOnPort(port) {
const result = spawnSync('ss', ['-tlnp'], { encoding: 'utf8' });
const match = result.stdout?.match(new RegExp(`:${port}\\s+.*?pid=(\\d+)`));
return match ? Number(match[1]) : null;
}
async function stopPortListener(port) {
const stale = pidOnPort(port);
if (!stale) return;
try {
process.kill(stale);
} catch {
spawnSync('fuser', ['-k', `${port}/tcp`], { stdio: 'pipe' });
}
await new Promise((r) => setTimeout(r, 200));
}
async function ensureServerProcess() {
await stopPortListener(PORT);
return startServerProcess();
}
function ensureCredential() { function ensureCredential() {
const cred = s3BaseUrl(); const cred = s3BaseUrl();
sqlcmd(` return sqlcmd(`
IF EXISTS (SELECT 1 FROM sys.credentials WHERE name = N'${cred}') IF NOT EXISTS (SELECT 1 FROM sys.credentials WHERE name = N'${cred}')
DROP CREDENTIAL [${cred}]; BEGIN
CREATE CREDENTIAL [${cred}] CREATE CREDENTIAL [${cred}]
WITH IDENTITY = 'S3 Access Key', WITH IDENTITY = 'S3 Access Key',
SECRET = '${ACCESS_KEY}:${SECRET_KEY}'; SECRET = '${ACCESS_KEY}:${SECRET_KEY}';
END
`); `);
} }
function backupDatabase(db) { async function backupDatabase(db) {
const file = `${db}-${timestamp()}.bak`; const file = `${db}-${timestamp()}.bak`;
const url = `${s3BaseUrl()}/${file}`; const url = `${s3BaseUrl()}/${file}`;
console.log(`Backing up ${db} -> ${url}`); console.log(`Backing up ${db} -> ${url}`);
sqlcmd(` await sqlcmd(`
BACKUP DATABASE [${db}] BACKUP DATABASE [${db}]
TO URL = '${url}' TO URL = '${url}'
WITH FORMAT, COMPRESSION, STATS = 10; WITH FORMAT, COMPRESSION, MAXTRANSFERSIZE = 20971520, STATS = 10;
`); `);
const onDisk = path.join(DATA_DIR, file); const onDisk = path.join(DATA_DIR, file);
if (!fs.existsSync(onDisk)) { if (!fs.existsSync(onDisk)) {
@@ -99,36 +153,95 @@ WITH FORMAT, COMPRESSION, STATS = 10;
return onDisk; return onDisk;
} }
function waitForSql() {
for (let i = 0; i < 60; i++) {
const result = spawnSync(
'sqlcmd',
[
'-W',
'-C',
'-S',
`${process.env.MSSQL_SERVER || 'localhost'},${process.env.MSSQL_PORT || '1433'}`,
'-U',
process.env.MSSQL_USER || 'sa',
'-P',
process.env.MSSQL_PASSWORD || '',
'-Q',
'SELECT 1',
],
{ encoding: 'utf8' }
);
if (result.status === 0 && !/Msg \d+,/.test(result.stdout || '')) {
return;
}
spawnSync('sleep', ['2']);
}
throw new Error('MSSQL did not become ready');
}
function startServerProcess() {
const child = fork(new URL('./server.mjs', import.meta.url), {
env: { ...process.env, S3_BACKUP_CHILD: '1' },
stdio: 'inherit',
});
return child;
}
async function main() { async function main() {
ensureCerts(HOST); if (skipTrust) {
if (!skipTrust) { const status = caTrustStatus();
const trusted = installCaTrust(); if (!status.inSync) {
if (!trusted) { throw new Error(
'PAL CA is out of sync with scripts/s3-backup/certs/ca.pem. Run: npm run backup:s3'
);
}
console.log('Skipping PAL CA install (--skip-trust)');
if (!fs.existsSync(SERVER_CERT)) {
throw new Error('No TLS certs found. Run: npm run backup:s3');
}
} else {
ensureCerts(HOST);
const trust = installCaTrust();
if (!trust.ok) {
console.warn('Could not install CA into MSSQL container. Run:'); console.warn('Could not install CA into MSSQL container. Run:');
console.warn(` docker cp scripts/s3-backup/certs/ca.pem mssql:/tmp/jtlsrv-s3-ca.pem`); console.warn(' docker exec -u root mssql mkdir -p /var/opt/mssql/security/ca-certificates');
console.warn(' docker exec mssql bash -lc "cp /tmp/jtlsrv-s3-ca.pem /usr/local/share/ca-certificates/jtlsrv-s3.crt && update-ca-certificates"'); console.warn(' docker cp scripts/s3-backup/certs/ca.pem mssql:/var/opt/mssql/security/ca-certificates/jtlsrv-s3-ca.pem');
console.warn(' docker exec -u root mssql chown mssql:mssql /var/opt/mssql/security/ca-certificates/jtlsrv-s3-ca.pem');
console.warn(' docker restart mssql');
} else { } else {
console.log('Installed S3 CA into MSSQL container'); console.log('Installed S3 CA into MSSQL PAL trust store');
if (trust.restarted) {
console.log('Waiting for MSSQL to restart...');
waitForSql();
}
} }
} }
await startServer(); const serverChild = await ensureServerProcess();
await waitForServer(); try {
await waitForServer();
if (serverOnly) { if (serverOnly) {
console.log('Server running (--server-only). Ctrl+C to stop.'); console.log('Server running (--server-only). Ctrl+C to stop.');
return; await new Promise((resolve) => serverChild.on('exit', resolve));
} return;
}
ensureCredential(); await ensureCredential();
const saved = []; const saved = [];
for (const db of databases) { for (const db of databases) {
saved.push(backupDatabase(db)); saved.push(await backupDatabase(db));
} }
console.log('\nBackups on disk:'); console.log('\nBackups on disk:');
for (const file of saved) { for (const file of saved) {
console.log(` ${file}`); console.log(` ${file}`);
}
} finally {
if (!serverOnly) {
serverChild.kill();
await stopPortListener(PORT);
}
} }
} }

View File

@@ -5,16 +5,18 @@ import path from 'node:path';
import { CA_CERT, CERTS_DIR, HOST, SERVER_CERT, SERVER_KEY } from './config.mjs'; import { CA_CERT, CERTS_DIR, HOST, SERVER_CERT, SERVER_KEY } from './config.mjs';
const SQL_CA_DIR = '/var/opt/mssql/security/ca-certificates';
export function certHosts(host) { export function certHosts(host) {
const hosts = new Set([host, '127.0.0.1', 'localhost']); const hosts = new Set([host, '127.0.0.1', 'localhost', 's3backup.local']);
for (const iface of Object.values(os.networkInterfaces())) { for (const iface of Object.values(os.networkInterfaces())) {
for (const addr of iface || []) { for (const addr of iface || []) {
if (addr.family === 'IPv4' && !addr.internal) { if (addr.family === 'IPv4' && !addr.internal && !addr.address.startsWith('169.254.')) {
hosts.add(addr.address); hosts.add(addr.address);
} }
} }
} }
return [...hosts]; return [...hosts].sort();
} }
export function ensureCerts(host = HOST) { export function ensureCerts(host = HOST) {
@@ -81,18 +83,99 @@ ${altNames}
fs.writeFileSync(marker, hostList); fs.writeFileSync(marker, hostList);
} }
export function caTrustStatus(container = process.env.MSSQL_DOCKER_CONTAINER || 'mssql') {
if (!fs.existsSync(CA_CERT)) {
return { ok: false, inSync: false, restarted: false };
}
const localFp = execSync(`openssl x509 -in "${CA_CERT}" -noout -fingerprint -sha256`, {
encoding: 'utf8',
}).trim();
const remoteFp = spawnSync(
'docker',
[
'exec',
container,
'bash',
'-lc',
`test -f ${SQL_CA_DIR}/jtlsrv-s3-ca.pem && openssl x509 -in ${SQL_CA_DIR}/jtlsrv-s3-ca.pem -noout -fingerprint -sha256`,
],
{ encoding: 'utf8' }
);
const inSync = remoteFp.status === 0 && remoteFp.stdout.trim() === localFp;
return { ok: remoteFp.status === 0, inSync, restarted: false };
}
export function installCaTrust(container = process.env.MSSQL_DOCKER_CONTAINER || 'mssql') { export function installCaTrust(container = process.env.MSSQL_DOCKER_CONTAINER || 'mssql') {
if (!fs.existsSync(CA_CERT)) { if (!fs.existsSync(CA_CERT)) {
ensureCerts(); ensureCerts();
} }
const copied = spawnSync('docker', ['cp', CA_CERT, `${container}:/tmp/jtlsrv-s3-ca.pem`], { const localFp = execSync(`openssl x509 -in "${CA_CERT}" -noout -fingerprint -sha256`, {
encoding: 'utf8',
}).trim();
const remoteFp = spawnSync(
'docker',
[
'exec',
container,
'bash',
'-lc',
`test -f ${SQL_CA_DIR}/jtlsrv-s3-ca.pem && openssl x509 -in ${SQL_CA_DIR}/jtlsrv-s3-ca.pem -noout -fingerprint -sha256`,
],
{ encoding: 'utf8' }
);
if (remoteFp.status === 0 && remoteFp.stdout.trim() === localFp) {
spawnSync('docker', [
'exec',
'-u',
'root',
container,
'bash',
'-lc',
'grep -q s3backup.local /etc/hosts || echo "172.17.0.1 s3backup.local" >> /etc/hosts',
]);
return { ok: true, restarted: false };
}
spawnSync('docker', ['exec', '-u', 'root', container, 'mkdir', '-p', SQL_CA_DIR], {
stdio: 'pipe', stdio: 'pipe',
}); });
if (copied.status !== 0) { spawnSync('docker', [
return false; 'exec',
'-u',
'root',
container,
'bash',
'-lc',
`rm -f ${SQL_CA_DIR}/*.pem ${SQL_CA_DIR}/*.crt`,
]);
spawnSync('docker', [
'exec',
'-u',
'root',
container,
'bash',
'-lc',
'grep -q s3backup.local /etc/hosts || echo "172.17.0.1 s3backup.local" >> /etc/hosts',
]);
try {
execSync('grep -q s3backup.local /etc/hosts || echo "172.17.0.1 s3backup.local" >> /etc/hosts', {
stdio: 'pipe',
});
} catch {
// optional on host
} }
const installed = spawnSync(
const copied = spawnSync(
'docker',
['cp', CA_CERT, `${container}:${SQL_CA_DIR}/jtlsrv-s3-ca.pem`],
{ stdio: 'pipe' }
);
if (copied.status !== 0) {
return { ok: false, restarted: false };
}
const perms = spawnSync(
'docker', 'docker',
[ [
'exec', 'exec',
@@ -101,9 +184,14 @@ export function installCaTrust(container = process.env.MSSQL_DOCKER_CONTAINER ||
container, container,
'bash', 'bash',
'-lc', '-lc',
'cp /tmp/jtlsrv-s3-ca.pem /usr/local/share/ca-certificates/jtlsrv-s3.crt && update-ca-certificates', `chown mssql:mssql ${SQL_CA_DIR}/jtlsrv-s3-ca.pem && chmod 644 ${SQL_CA_DIR}/jtlsrv-s3-ca.pem`,
], ],
{ stdio: 'pipe' } { stdio: 'pipe' }
); );
return installed.status === 0; if (perms.status !== 0) {
return { ok: false, restarted: false };
}
const restarted = spawnSync('docker', ['restart', container], { stdio: 'pipe' });
return { ok: true, restarted: restarted.status === 0 };
} }

View File

@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
import fs from 'node:fs'; import fs from 'node:fs';
import https from 'node:https'; import https from 'node:https';
import path from 'node:path'; import path from 'node:path';
import { URL } from 'node:url'; import { URL, pathToFileURL } from 'node:url';
import { import {
ACCESS_KEY, ACCESS_KEY,
@@ -32,6 +32,9 @@ function send(res, status, body = '', headers = {}) {
} }
function readBody(req) { function readBody(req) {
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'DELETE') {
return Promise.resolve(Buffer.alloc(0));
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const chunks = []; const chunks = [];
req.on('data', (chunk) => chunks.push(chunk)); req.on('data', (chunk) => chunks.push(chunk));
@@ -169,10 +172,17 @@ function abortMultipart(uploadId) {
return true; return true;
} }
function debug(...args) {
if (process.env.S3_BACKUP_DEBUG) {
console.error(...args);
}
}
async function handle(req, res) { async function handle(req, res) {
const body = await readBody(req); const body = await readBody(req);
debug(`${req.method} ${req.url} len=${body.length}`);
if (!authOk(req, body)) { if (!authOk(req, body)) {
console.error(`${req.method} ${req.url} -> 403 auth failed`); debug(`auth failed ${req.method} ${req.url}`);
return send(res, 403, xml('<Error><Code>AccessDenied</Code><Message>Access Denied</Message></Error>')); return send(res, 403, xml('<Error><Code>AccessDenied</Code><Message>Access Denied</Message></Error>'));
} }
@@ -195,6 +205,10 @@ async function handle(req, res) {
return send(res, 200, listBucketXml(query.get('prefix') || '')); return send(res, 200, listBucketXml(query.get('prefix') || ''));
} }
if (req.method === 'HEAD' && !key) {
return send(res, 200, '');
}
if (req.method === 'GET' && key) { if (req.method === 'GET' && key) {
const file = objectPath(key); const file = objectPath(key);
if (!fs.existsSync(file)) { if (!fs.existsSync(file)) {
@@ -258,7 +272,6 @@ async function handle(req, res) {
} }
export function startServer() { export function startServer() {
ensureCerts(HOST);
fs.mkdirSync(DATA_DIR, { recursive: true }); fs.mkdirSync(DATA_DIR, { recursive: true });
fs.mkdirSync(TMP_DIR, { recursive: true }); fs.mkdirSync(TMP_DIR, { recursive: true });
@@ -272,7 +285,17 @@ export function startServer() {
} }
); );
return new Promise((resolve) => { return new Promise((resolve, reject) => {
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
reject(new Error(`Port ${PORT} is already in use`));
return;
}
reject(err);
});
server.on('secureConnection', (tlsSocket) => {
debug(`tls ${tlsSocket.remoteAddress}:${tlsSocket.remotePort}`);
});
server.listen(PORT, BIND, () => { server.listen(PORT, BIND, () => {
console.log(`S3 endpoint https://${HOST}:${PORT}/${BUCKET} -> ${DATA_DIR}`); console.log(`S3 endpoint https://${HOST}:${PORT}/${BUCKET} -> ${DATA_DIR}`);
resolve(server); resolve(server);
@@ -280,6 +303,9 @@ export function startServer() {
}); });
} }
if (import.meta.url === `file://${process.argv[1]}`) { if (process.env.S3_BACKUP_CHILD || import.meta.url === pathToFileURL(process.argv[1]).href) {
startServer(); startServer().catch((err) => {
console.error(err.message || err);
process.exit(1);
});
} }

View File

@@ -22,7 +22,6 @@ function parseAuthHeader(header) {
accessKey: credential[0], accessKey: credential[0],
date: credential[1], date: credential[1],
region: credential[2], region: credential[2],
service: credential[3],
signedHeaders: parts.SignedHeaders.split(';'), signedHeaders: parts.SignedHeaders.split(';'),
signature: parts.Signature, signature: parts.Signature,
}; };
@@ -59,6 +58,7 @@ export function verifyRequest(req, body, { accessKey, secretKey, region = 'us-ea
return false; return false;
} }
const regionToUse = parsed.region || region;
const amzDate = getHeader(req, 'x-amz-date'); const amzDate = getHeader(req, 'x-amz-date');
const declaredPayload = getHeader(req, 'x-amz-content-sha256'); const declaredPayload = getHeader(req, 'x-amz-content-sha256');
const payloadHash = const payloadHash =
@@ -72,16 +72,16 @@ export function verifyRequest(req, body, { accessKey, secretKey, region = 'us-ea
payloadHash, payloadHash,
].join('\n'); ].join('\n');
const scope = `${parsed.date}/${region}/s3/aws4_request`; const scope = `${parsed.date}/${regionToUse}/s3/aws4_request`;
const stringToSign = ['AWS4-HMAC-SHA256', amzDate, scope, hash(canonical)].join('\n'); const stringToSign = ['AWS4-HMAC-SHA256', amzDate, scope, hash(canonical)].join('\n');
const signingKey = hmac( const signingKey = hmac(
hmac( hmac(hmac(hmac(`AWS4${secretKey}`, parsed.date), regionToUse), 's3'),
hmac(hmac(`AWS4${secretKey}`, parsed.date), region),
's3'
),
'aws4_request' 'aws4_request'
); );
const expected = hmac(signingKey, stringToSign, 'hex'); const expected = hmac(signingKey, stringToSign, 'hex');
if (expected.length !== parsed.signature.length) {
return false;
}
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parsed.signature, 'hex')); return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parsed.signature, 'hex'));
} }

View File

@@ -133,32 +133,134 @@ async function allocatePk(transaction, tableName) {
return pk; return pk;
} }
const VERSANDPOSITION_TYPE = 2;
const ZAHLUNG_TYPE_ZAHLUNG = 10;
const NIST_READONLY_NICHT_AENDERBAR = 2;
const NIST_EXTERNE_RECHNUNG_KEINE = 2;
function parseImportSetting(order) {
return toNumber(order.settings?.importSetting, 0);
}
function parseInvoiceSetting(order) {
return toNumber(order.settings?.invoiceSetting, 0);
}
/** PosOrderSettingsService: importSetting=0 → NichtAenderbar. */
function resolveNIstReadOnly(order) {
return parseImportSetting(order) === 0 ? NIST_READONLY_NICHT_AENDERBAR : 0;
}
/**
* PosOrderSettingsService: importSetting=0 → KeineRechnungKeineAusgabe;
* importSetting 25 or invoiceSetting Create flag → Wawi invoice.
*/
function resolveNIstExterneRechnung(order) {
const importSetting = parseImportSetting(order);
const invoiceSetting = parseInvoiceSetting(order);
if (importSetting >= 2 && importSetting <= 5) return 0;
if (invoiceSetting & 1) return 0;
if (importSetting === 0) return NIST_EXTERNE_RECHNUNG_KEINE;
return 0;
}
function isVersandposition(item) {
return toNumber(item.type, 0) === VERSANDPOSITION_TYPE;
}
function hasVersandposition(items) {
return items.some(isVersandposition);
}
function hasNonReturnSaleItems(items) {
return items.some((item) => toNumber(item.isReturn, 0) === 0);
}
function shouldInjectSelbstabholerShipping(items) {
if (hasVersandposition(items)) return false;
return hasNonReturnSaleItems(items);
}
async function lookupVersandArt(transaction, shippingName) {
const name = String(shippingName ?? '').trim() || 'Selbstabholer';
const tryLookup = async (cName) => {
const result = await new sql.Request(transaction)
.input('cName', sql.NVarChar, cName)
.query(`
SELECT TOP 1 kVersandArt, cName, fPrice, fMwSt
FROM dbo.tVersandArt
WHERE cName = @cName
`);
return result.recordset[0] ?? null;
};
let row = await tryLookup(name);
if (!row && name !== 'Selbstabholer') {
row = await tryLookup('Selbstabholer');
}
return row;
}
function syntheticShippingItem(versandArt) {
const vat = toNumber(versandArt.fMwSt, 19);
const gross = toNumber(versandArt.fPrice, 0);
const net = gross / (1 + vat / 100);
return {
type: String(VERSANDPOSITION_TYPE),
quantity: '1',
name: versandArt.cName,
priceGross: String(gross),
priceNet: String(net),
vat: String(vat),
isReturn: '0',
};
}
/** /**
* Resolves a Zahlungsart by name; creates it on the fly (including its PK * Resolves a Zahlungsart by name; creates it on the fly (including its PK
* from dbo.tpk) when it does not exist yet. * from dbo.tpk) when it does not exist yet.
*/ */
async function resolveZahlungsart(transaction, name, cache) { async function resolveZahlungsart(transaction, name, cache) {
const key = String(name || 'Bar'); const lookupName = String(name || 'Bar');
const cached = cache.get(key.toLowerCase()); const cacheKey = lookupName.toLowerCase();
const cached = cache.get(cacheKey);
if (cached) { if (cached) {
return cached; return cached;
} }
const existing = await new sql.Request(transaction) let row = null;
.input('cName', sql.NVarChar, key) const exact = await new sql.Request(transaction)
.input('cName', sql.NVarChar, lookupName)
.query('SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE cName = @cName'); .query('SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE cName = @cName');
row = exact.recordset[0] ?? null;
if (!row) {
const lang = await new sql.Request(transaction)
.input('cName', sql.NVarChar, lookupName)
.query(`
SELECT TOP 1 z.kZahlungsart, z.cName
FROM dbo.tZahlungsArtSprache zs
INNER JOIN dbo.tZahlungsart z ON z.kZahlungsart = zs.kZahlungsart
WHERE zs.cName = @cName
`);
row = lang.recordset[0] ?? null;
}
if (!row) {
const ci = await new sql.Request(transaction)
.input('cName', sql.NVarChar, lookupName)
.query('SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE UPPER(cName) = UPPER(@cName)');
row = ci.recordset[0] ?? null;
}
let zahlungsart; let zahlungsart;
if (existing.recordset[0]) { if (row) {
zahlungsart = { zahlungsart = { kZahlungsart: row.kZahlungsart, cName: row.cName };
kZahlungsart: existing.recordset[0].kZahlungsart,
cName: existing.recordset[0].cName,
};
} else { } else {
const kZahlungsart = await allocatePk(transaction, 'tZahlungsart'); const kZahlungsart = await allocatePk(transaction, 'tZahlungsart');
await new sql.Request(transaction) await new sql.Request(transaction)
.input('kZahlungsart', sql.Int, kZahlungsart) .input('kZahlungsart', sql.Int, kZahlungsart)
.input('cName', sql.NVarChar, key) .input('cName', sql.NVarChar, lookupName)
.query(` .query(`
INSERT INTO dbo.tZahlungsart INSERT INTO dbo.tZahlungsart
(kZahlungsart, cName, cPrtString, nLastschrift, cPrtStringVor, cPaymentOption, cKonto, (kZahlungsart, cName, cPrtString, nLastschrift, cPrtStringVor, cPaymentOption, cKonto,
@@ -166,10 +268,10 @@ async function resolveZahlungsart(transaction, name, cache) {
nMatchingOptionen, nIstStandard, nAktiv) nMatchingOptionen, nIstStandard, nAktiv)
VALUES (@kZahlungsart, @cName, '', 0, '', '', '', 0, 0, 0, 0, 0, 0, 0, 1) VALUES (@kZahlungsart, @cName, '', 0, '', '', '', 0, 0, 0, 0, 0, 0, 0, 1)
`); `);
zahlungsart = { kZahlungsart, cName: key }; zahlungsart = { kZahlungsart, cName: lookupName };
} }
cache.set(key.toLowerCase(), zahlungsart); cache.set(cacheKey, zahlungsart);
return zahlungsart; return zahlungsart;
} }
@@ -214,8 +316,8 @@ async function createCustomer(transaction, { customerNumber, address, defaults }
.input('nDebitorennr', sql.Int, Number(a.debtorNumber) || 0) .input('nDebitorennr', sql.Int, Number(a.debtorNumber) || 0)
.query(` .query(`
DECLARE @returnValue INT; DECLARE @returnValue INT;
DECLARE @p1 dbo.TYPE_spkundeInsert; DECLARE @kunde_daten dbo.TYPE_spkundeInsert;
INSERT INTO @p1 INSERT INTO @kunde_daten
(kInetKunde, kKundenKategorie, cKundenNr, cFirma, cAnrede, cTitel, cVorname, cName, (kInetKunde, kKundenKategorie, cKundenNr, cFirma, cAnrede, cTitel, cVorname, cName,
cStrasse, cPLZ, cOrt, cLand, cTel, cFax, cEMail, dErstellt, cMobil, fRabatt, cUSTID, cNewsletter, cStrasse, cPLZ, cOrt, cLand, cTel, cFax, cEMail, dErstellt, cMobil, fRabatt, cUSTID, cNewsletter,
cZusatz, cEbayName, kBuyer, cAdressZusatz, cGeburtstag, cWWW, cSperre, cPostID, kKundenGruppe, cZusatz, cEbayName, kBuyer, cAdressZusatz, cGeburtstag, cWWW, cSperre, cPostID, kKundenGruppe,
@@ -229,7 +331,7 @@ async function createCustomer(transaction, { customerNumber, address, defaults }
0, @kSprache, @cISO, @cBundesland, @cHerkunft, @cKassenKunde, N'', 0, 0, @kSprache, @cISO, @cBundesland, @cHerkunft, @cKassenKunde, N'', 0,
@nDebitorennr, N'', 0, 0, 0, 0, 0, @nDebitorennr, N'', 0, 0, 0, 0, 0,
NULL, 0, 0, 0); NULL, 0, 0, 0);
EXEC @returnValue = Kunde.spKundeInsert @daten = @p1; EXEC @returnValue = Kunde.spKundeInsert @daten = @kunde_daten;
SELECT @returnValue AS kKunde; SELECT @returnValue AS kKunde;
`); `);
@@ -252,14 +354,6 @@ function resolveAuftragCKundenNr(order) {
return isWalkInOrder(order) ? '0' : String(order.customerNumber || ''); return isWalkInOrder(order) ? '0' : String(order.customerNumber || '');
} }
/** POS settings.invoiceSetting: 0 = external/POS invoice (nIstExterneRechnung=1). */
function resolveNIstExterneRechnung(order) {
const inv = order.settings?.invoiceSetting;
if (inv === undefined || inv === null) return 1;
if (inv === true || inv === 1 || inv === '1' || String(inv).toLowerCase() === 'true') return 0;
return 1;
}
async function lookupKassenkunde(transaction, defaults) { async function lookupKassenkunde(transaction, defaults) {
const result = await new sql.Request(transaction).query( const result = await new sql.Request(transaction).query(
"SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKassenKunde = 'Y' ORDER BY kKunde" "SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKassenKunde = 'Y' ORDER BY kKunde"
@@ -349,15 +443,19 @@ async function insertOrderItem(transaction, kAuftrag, item) {
const discount = toNumber(item.discountPercent, 0); const discount = toNumber(item.discountPercent, 0);
const kSteuerklasse = steuerklasseForVat(vat); const kSteuerklasse = steuerklasseForVat(vat);
const sku = String(item.sku || '').trim(); const sku = String(item.sku || '').trim();
const positionType = isVersandposition(item) ? VERSANDPOSITION_TYPE : toNumber(item.type, 0);
let kArtikel = null; let kArtikel = null;
if (sku) { if (sku && positionType !== VERSANDPOSITION_TYPE) {
const result = await new sql.Request(transaction) const result = await new sql.Request(transaction)
.input('cArtNr', sql.NVarChar, sku) .input('cArtNr', sql.NVarChar, sku)
.query('SELECT TOP 1 kArtikel FROM dbo.tArtikel WHERE cArtNr = @cArtNr'); .query('SELECT TOP 1 kArtikel FROM dbo.tArtikel WHERE cArtNr = @cArtNr');
kArtikel = result.recordset[0]?.kArtikel ?? null; kArtikel = result.recordset[0]?.kArtikel ?? null;
} }
const nType = positionType === VERSANDPOSITION_TYPE ? VERSANDPOSITION_TYPE : (kArtikel ? 1 : 0);
const nReserviert = nType === VERSANDPOSITION_TYPE ? 0 : 1;
const result = await new sql.Request(transaction) const result = await new sql.Request(transaction)
.input('kArtikel', sql.Int, kArtikel) .input('kArtikel', sql.Int, kArtikel)
.input('kAuftrag', sql.Int, kAuftrag) .input('kAuftrag', sql.Int, kAuftrag)
@@ -368,7 +466,8 @@ async function insertOrderItem(transaction, kAuftrag, item) {
.input('fVkNetto', sql.Float, priceNet) .input('fVkNetto', sql.Float, priceNet)
.input('fMwSt', sql.Float, vat) .input('fMwSt', sql.Float, vat)
.input('kSteuerklasse', sql.Int, kSteuerklasse) .input('kSteuerklasse', sql.Int, kSteuerklasse)
.input('nType', sql.Int, kArtikel ? 1 : 0) .input('nType', sql.Int, nType)
.input('nReserviert', sql.Int, nReserviert)
.input('cEinheit', sql.NVarChar, item.unit || '') .input('cEinheit', sql.NVarChar, item.unit || '')
.input('fRabatt', sql.Float, discount) .input('fRabatt', sql.Float, discount)
.query(` .query(`
@@ -377,7 +476,7 @@ async function insertOrderItem(transaction, kAuftrag, item) {
(kArtikel, kAuftrag, cArtNr, nReserviert, cName, cHinweis, fAnzahl, fVkNetto, fMwSt, (kArtikel, kAuftrag, cArtNr, nReserviert, cName, cHinweis, fAnzahl, fVkNetto, fMwSt,
cNameStandard, kSteuerklasse, nType, cEinheit, fFaktor, kSteuerschluessel, fRabatt) cNameStandard, kSteuerklasse, nType, cEinheit, fFaktor, kSteuerschluessel, fRabatt)
OUTPUT inserted.kAuftragPosition INTO @t OUTPUT inserted.kAuftragPosition INTO @t
VALUES (@kArtikel, @kAuftrag, @cArtNr, 1, @cName, @cHinweis, @fAnzahl, @fVkNetto, @fMwSt, VALUES (@kArtikel, @kAuftrag, @cArtNr, @nReserviert, @cName, @cHinweis, @fAnzahl, @fVkNetto, @fMwSt,
@cName, @kSteuerklasse, @nType, @cEinheit, 1.0, 3, @fRabatt); @cName, @kSteuerklasse, @nType, @cEinheit, 1.0, 3, @fRabatt);
SELECT kAuftragPosition FROM @t; SELECT kAuftragPosition FROM @t;
`); `);
@@ -486,11 +585,43 @@ function isOrderDelivered(order) {
return deliver === true || String(deliver) === '1' || String(deliver).toLowerCase() === 'true'; return deliver === true || String(deliver) === '1' || String(deliver).toLowerCase() === 'true';
} }
async function recalculateAuftragEckdaten(transaction, kAuftrag) {
await new sql.Request(transaction).input('kAuftrag', sql.Int, kAuftrag).query(`
DECLARE @eckdaten_calc Verkauf.TYPE_spAuftragEckdatenBerechnen;
INSERT INTO @eckdaten_calc VALUES (@kAuftrag);
EXEC Verkauf.spAuftragEckdatenBerechnen @auftrag = @eckdaten_calc;
`);
}
async function getOffenerAuftragswert(transaction, kAuftrag) {
const result = await new sql.Request(transaction)
.input('kAuftrag', sql.Int, kAuftrag)
.query(`
SELECT ROUND(tAuftragEckdaten.fOffenerWertOhneStorno, 2) AS fOffenerAuftragswert
FROM Verkauf.tAuftrag
LEFT JOIN Verkauf.tAuftragEckdaten ON tAuftragEckdaten.kAuftrag = tAuftrag.kAuftrag
WHERE tAuftrag.kAuftrag = @kAuftrag
`);
const value = result.recordset[0]?.fOffenerAuftragswert;
return value == null ? null : toNumber(value, 0);
}
function isNewPayment(payment) {
const paymentId = toNumber(payment.paymentId, 0);
return paymentId <= 0;
}
async function insertPayment(transaction, kAuftrag, payment, order, orderDate, zahlungsartCache) { async function insertPayment(transaction, kAuftrag, payment, order, orderDate, zahlungsartCache) {
const zahlungsart = await resolveZahlungsart(transaction, payment.paymentMethodName || order.paymentMethodName, zahlungsartCache); const zahlungsart = await resolveZahlungsart(transaction, payment.paymentMethodName || order.paymentMethodName, zahlungsartCache);
const kZahlung = await allocatePk(transaction, 'tZahlung'); const kZahlung = await allocatePk(transaction, 'tZahlung');
await new sql.Request(transaction) await recalculateAuftragEckdaten(transaction, kAuftrag);
const fOffenerWert = await getOffenerAuftragswert(transaction, kAuftrag);
if (fOffenerWert == null) {
throw new Error(`no open order amount for kAuftrag=${kAuftrag}`);
}
const result = await new sql.Request(transaction)
.input('kZahlung', sql.Int, kZahlung) .input('kZahlung', sql.Int, kZahlung)
.input('cName', sql.NVarChar, zahlungsart.cName) .input('cName', sql.NVarChar, zahlungsart.cName)
.input('dDatum', sql.DateTime, orderDate) .input('dDatum', sql.DateTime, orderDate)
@@ -499,13 +630,26 @@ async function insertPayment(transaction, kAuftrag, payment, order, orderDate, z
.input('kBenutzer', sql.Int, config.kBenutzer) .input('kBenutzer', sql.Int, config.kBenutzer)
.input('kZahlungsart', sql.Int, zahlungsart.kZahlungsart) .input('kZahlungsart', sql.Int, zahlungsart.kZahlungsart)
.input('cExternalTransactionId', sql.NVarChar, order.externalOrderNumber || '') .input('cExternalTransactionId', sql.NVarChar, order.externalOrderNumber || '')
.input('fOffenerWert', sql.Float, fOffenerWert)
.query(` .query(`
INSERT INTO dbo.tZahlung IF EXISTS (
(kZahlung, cName, dDatum, fBetrag, kBestellung, kBenutzer, nAnzahlung, cHinweis, kZahlungsart, SELECT 1 FROM Verkauf.tAuftragEckdaten
nKeinExport, cExternalTransactionId, nZuweisungstyp, nZahlungstyp, cZuweisungsinfo, nZuweisungswertung) WHERE kAuftrag = @kBestellung
VALUES (@kZahlung, @cName, @dDatum, @fBetrag, @kBestellung, @kBenutzer, 0, '', @kZahlungsart, AND ROUND(fOffenerWertOhneStorno, 2) = ROUND(@fOffenerWert, 2)
0, @cExternalTransactionId, 0, 0, '', 0) )
BEGIN
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, ${ZAHLUNG_TYPE_ZAHLUNG}, '', 0);
END
SELECT @@ROWCOUNT AS inserted;
`); `);
if (!result.recordset[0]?.inserted) {
throw new Error(`payment insert skipped: open amount changed for kAuftrag=${kAuftrag}`);
}
} }
export async function createOrder(order) { export async function createOrder(order) {
@@ -537,6 +681,16 @@ export async function createOrder(order) {
const zahlungsart = await resolveZahlungsart(transaction, order.paymentMethodName, zahlungsartCache); const zahlungsart = await resolveZahlungsart(transaction, order.paymentMethodName, zahlungsartCache);
const cAuftragsNr = await nextOrderNumber(transaction, orderDate); const cAuftragsNr = await nextOrderNumber(transaction, orderDate);
const orderItems = [...(order.orderItems || [])];
const versandArt = await lookupVersandArt(transaction, order.shippingName);
let kVersandArt = versandArt?.kVersandArt ?? defaults.kVersandArt;
if (shouldInjectSelbstabholerShipping(orderItems) && versandArt) {
orderItems.push(syntheticShippingItem(versandArt));
}
const nIstReadOnly = resolveNIstReadOnly(order);
const nIstExterneRechnung = resolveNIstExterneRechnung(order);
const resultAuftrag = await new sql.Request(transaction) const resultAuftrag = await new sql.Request(transaction)
.input('cAuftragsNr', sql.NVarChar, cAuftragsNr) .input('cAuftragsNr', sql.NVarChar, cAuftragsNr)
.input('dErstellt', sql.DateTime, orderDate) .input('dErstellt', sql.DateTime, orderDate)
@@ -549,11 +703,12 @@ export async function createOrder(order) {
.input('kShop', sql.Int, getActiveShopId() || null) .input('kShop', sql.Int, getActiveShopId() || null)
.input('cKundenNr', sql.NVarChar, resolveAuftragCKundenNr(order)) .input('cKundenNr', sql.NVarChar, resolveAuftragCKundenNr(order))
.input('cVersandlandISO', sql.NVarChar, (order.shippingAddress?.countryIso || 'DE').toUpperCase()) .input('cVersandlandISO', sql.NVarChar, (order.shippingAddress?.countryIso || 'DE').toUpperCase())
.input('kVersandArt', sql.Int, defaults.kVersandArt) .input('kVersandArt', sql.Int, kVersandArt)
.input('kZahlungsart', sql.Int, zahlungsart.kZahlungsart) .input('kZahlungsart', sql.Int, zahlungsart.kZahlungsart)
.input('kKundengruppe', sql.Int, kKundengruppe) .input('kKundengruppe', sql.Int, kKundengruppe)
.input('cExterneAuftragsnummer', sql.NVarChar, order.externalOrderNumber || '') .input('cExterneAuftragsnummer', sql.NVarChar, order.externalOrderNumber || '')
.input('nIstExterneRechnung', sql.Int, resolveNIstExterneRechnung(order)) .input('nIstExterneRechnung', sql.Int, nIstExterneRechnung)
.input('nIstReadOnly', sql.Int, nIstReadOnly)
.query(` .query(`
DECLARE @t TABLE ([kAuftrag] INT); DECLARE @t TABLE ([kAuftrag] INT);
INSERT INTO Verkauf.tAuftrag INSERT INTO Verkauf.tAuftrag
@@ -565,7 +720,7 @@ export async function createOrder(order) {
VALUES (@cAuftragsNr, @dErstellt, 0, @kBenutzer, @kKunde, @kBenutzer, 1, 1.0, VALUES (@cAuftragsNr, @dErstellt, 0, @kBenutzer, @kKunde, @kBenutzer, 1, 1.0,
@kFirmaHistory, @kSprache, @cWaehrung, 1.0, 0.0, @kFirmaHistory, @kSprache, @cWaehrung, 1.0, 0.0,
@cWaehrung, @kPlattform, @kShop, @cKundenNr, @cVersandlandISO, @kVersandArt, @kZahlungsart, @kKundengruppe, @cWaehrung, @kPlattform, @kShop, @cKundenNr, @cVersandlandISO, @kVersandArt, @kZahlungsart, @kKundengruppe,
@cExterneAuftragsnummer, @nIstExterneRechnung, 'Y', 2, 0, 10); @cExterneAuftragsnummer, @nIstExterneRechnung, 'Y', @nIstReadOnly, 0, 10);
SELECT kAuftrag FROM @t; SELECT kAuftrag FROM @t;
`); `);
@@ -578,27 +733,30 @@ export async function createOrder(order) {
await insertOrderAddress(transaction, kAuftrag, kKunde, order.billingAddress, 1); await insertOrderAddress(transaction, kAuftrag, kKunde, order.billingAddress, 1);
const deliveredItems = []; const deliveredItems = [];
for (const item of order.orderItems || []) { for (const item of orderItems) {
const kAuftragPosition = await insertOrderItem(transaction, kAuftrag, item); const kAuftragPosition = await insertOrderItem(transaction, kAuftrag, item);
const kPosAuftragPosition = Number.parseInt(item.externalId, 10); const kPosAuftragPosition = Number.parseInt(item.externalId, 10);
await insertPosOrderPositionMapping(transaction, kAuftragPosition, kPosAuftragPosition); if (Number.isInteger(kPosAuftragPosition)) {
if (kAuftragPosition != null) { await insertPosOrderPositionMapping(transaction, kAuftragPosition, kPosAuftragPosition);
}
if (kAuftragPosition != null && !isVersandposition(item)) {
deliveredItems.push({ kAuftragPosition, quantity: toNumber(item.quantity, 1) }); deliveredItems.push({ kAuftragPosition, quantity: toNumber(item.quantity, 1) });
} }
} }
for (const payment of order.payments || []) { for (const payment of order.payments || []) {
if (!isNewPayment(payment)) continue;
await insertPayment(transaction, kAuftrag, payment, order, orderDate, zahlungsartCache); await insertPayment(transaction, kAuftrag, payment, order, orderDate, zahlungsartCache);
} }
if (isOrderDelivered(order)) { if (isOrderDelivered(order)) {
await deliverOrder(transaction, config.kBenutzer, kAuftrag, defaults.kVersandArt, deliveredItems); await deliverOrder(transaction, config.kBenutzer, kAuftrag, kVersandArt, deliveredItems);
} }
await new sql.Request(transaction).input('kAuftrag', sql.Int, kAuftrag).query(` await new sql.Request(transaction).input('kAuftrag', sql.Int, kAuftrag).query(`
DECLARE @p1 Verkauf.TYPE_spAuftragEckdatenBerechnen; DECLARE @eckdaten_calc Verkauf.TYPE_spAuftragEckdatenBerechnen;
INSERT INTO @p1 VALUES (@kAuftrag); INSERT INTO @eckdaten_calc VALUES (@kAuftrag);
EXEC Verkauf.spAuftragEckdatenBerechnen @auftrag = @p1; EXEC Verkauf.spAuftragEckdatenBerechnen @auftrag = @eckdaten_calc;
`); `);
await transaction.commit(); await transaction.commit();