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"
void handle_category(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
int64_t cursor = std::stoll(req.get_query_param("lastChangedCategory", "0"));
int limit = std::stoi(req.get_query_param("limit", "20"));
int64_t cursor = req.get_query_int64("lastChangedCategory");
int limit = req.get_query_int("limit", 20);
auto categories = get_category_list(cursor, limit);
resp.send_json(200, categories);
}

View File

@@ -6,16 +6,27 @@
#include "../tls_server.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) {
return {
{"authCode", nullptr},
{"authToken", config["authToken"]},
{"certificateFingerprint", config["certificateFingerprint"]},
{"certificateSerialNumber", config["certificateSerialNumber"]},
{"mandantId", config["mandantId"]},
{"authToken", config_string(config, "authToken")},
{"certificateFingerprint", config_string(config, "certificateFingerprint")},
{"certificateSerialNumber", config_string(config, "certificateSerialNumber")},
{"mandantId", config_string(config, "mandantId")},
{"mandantName", nullptr},
{"mandantDatabase", nullptr},
{"serverFingerprint", config["serverFingerprint"]},
{"serverFingerprint", config_string(config, "serverFingerprint")},
{"name", nullptr},
{"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) {
return {
{"authCode", auth_code},
{"authToken", config["authToken"]},
{"certificateFingerprint", config["certificateFingerprint"]},
{"certificateSerialNumber", config["certificateSerialNumber"]},
{"mandantId", config["mandantId"]},
{"mandantName", config["mandantName"]},
{"mandantDatabase", config["mandantDatabase"]},
{"authToken", config_string(config, "authToken")},
{"certificateFingerprint", config_string(config, "certificateFingerprint")},
{"certificateSerialNumber", config_string(config, "certificateSerialNumber")},
{"mandantId", config_string(config, "mandantId")},
{"mandantName", config_string(config, "mandantName")},
{"mandantDatabase", config_string(config, "mandantDatabase")},
{"serverFingerprint", nullptr},
{"name", nullptr},
{"serverTimestamp", server_timestamp()},
@@ -47,7 +58,7 @@ void handle_client(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
if (auth_code.size() == 6) {
if (ctx.pairing_store->has_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(400, {

View File

@@ -4,7 +4,7 @@
#include "../queries/customer_groups.hpp"
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);
resp.send_json(200, groups);
}

View File

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

View File

@@ -8,11 +8,11 @@
#include "../config.hpp"
void handle_init(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
int64_t product_cursor = std::stoll(req.get_query_param("lastChangedProduct", "0"));
int64_t category_cursor = std::stoll(req.get_query_param("lastChangedCategory", "0"));
int64_t cg_cursor = std::stoll(req.get_query_param("lastChangedCustomerGroup", "0"));
int64_t composite_cursor = std::stoll(req.get_query_param("lastChangedCompositeProduct", "0"));
int64_t deleted_cursor = std::stoll(req.get_query_param("lastChangedDeletedEntity", "0"));
int64_t product_cursor = req.get_query_int64("lastChangedProduct");
int64_t category_cursor = req.get_query_int64("lastChangedCategory");
int64_t cg_cursor = req.get_query_int64("lastChangedCustomerGroup");
int64_t composite_cursor = req.get_query_int64("lastChangedCompositeProduct");
int64_t deleted_cursor = req.get_query_int64("lastChangedDeletedEntity");
int root = config::get_int("ROOT_CATEGORY_ID", 1);
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);
deleted_count = get_deleted_count(deleted_cursor);
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 &&
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");

View File

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

View File

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

View File

@@ -7,6 +7,25 @@
#include <cstring>
#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
// ---------------------------------------------------------------------------
@@ -39,6 +58,14 @@ std::string HttpRequest::get_query_param(const std::string& key, const std::stri
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
// ---------------------------------------------------------------------------

View File

@@ -17,6 +17,11 @@ struct tls_session;
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
// ---------------------------------------------------------------------------
@@ -32,6 +37,8 @@ struct HttpRequest {
std::string query_string; // "authCode=xxx&name=yyy"
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();
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
const char* ip = sess->peer_ip.c_str();
if (resp_size > 0) {
@@ -132,7 +140,7 @@ int main(int /*argc*/, char* argv[]) {
// Initialize pairing store
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
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();
nlohmann::json result = nlohmann::json::array();
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({
{"_id", row[0].str},
{"imghash", row[4].str.empty() ? nullptr : nlohmann::json(row[4].str)},

View File

@@ -35,6 +35,11 @@ struct Config {
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<Defaults> g_resolved_defaults;
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);
}
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;
@@ -232,38 +243,158 @@ inline int allocate_pk(OdbcPool::Connection* c, const std::string& 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<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) {
std::string key = name.empty() ? "bar" : name;
std::transform(key.begin(), key.end(), key.begin(), [](unsigned char ch) { return std::tolower(ch); });
auto it = g_zahlungsart_cache.find(key);
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<char>(std::tolower(ch)); });
return key;
}();
auto it = g_zahlungsart_cache.find(cache_key);
if (it != g_zahlungsart_cache.end()) return it->second;
std::vector<Param> ps = {{ParamType::NVarChar, key, 0}};
ResultSet rs;
get_pool().execute(c, "SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE cName = ?", ps, rs);
auto fetch_row = [&](const char* sql) -> std::optional<std::pair<int, std::string>> {
std::vector<Param> 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);
};
if (!rs.empty() && !rs[0].empty()) {
nlohmann::json z = {{"kZahlungsart", std::stoi(rs[0][0].str)}, {"cName", rs[0][1].str}};
g_zahlungsart_cache[key] = z;
return z;
std::optional<std::pair<int, std::string>> 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(?)");
}
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<Param> ps2 = {
{ParamType::Int, "", kZahlungsart},
{ParamType::NVarChar, key, 0}
};
get_pool().execute(c, insert, ps2, rs);
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<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[key] = z;
return z;
g_zahlungsart_cache[cache_key] = zahlungsart;
return zahlungsart;
}
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", "");
}
// 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,
const nlohmann::json& address, const Defaults& defaults) {
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 =
"DECLARE @returnValue INT;"
"DECLARE @p1 dbo.TYPE_spkundeInsert;"
"INSERT INTO @p1"
"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,"
@@ -342,7 +458,7 @@ inline std::pair<int,int> create_customer(OdbcPool::Connection* c, const std::st
" 0, ?, ?, ?, ?, ?, N'', 0,"
" ?, N'', 0, 0, 0, 0, 0,"
" NULL, 0, 0, 0);"
"EXEC @returnValue = Kunde.spKundeInsert @daten = @p1;"
"EXEC @returnValue = Kunde.spKundeInsert @daten = @kunde_daten;"
"SELECT @returnValue AS kKunde";
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("email", ""), 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("birthday", ""), 0},
{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) {
double vat = to_number(item["vat"], 19);
double quantity = to_number(item["quantity"], 1);
double price_gross = to_number(item["priceGross"], 0);
double price_net = to_number(item["priceNet"], price_gross / (1 + vat / 100));
double discount = to_number(item["discountPercent"], 0);
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()) {
if (!sku.empty() && position_type != VERSANDPOSITION_TYPE) {
std::vector<Param> ps = {{ParamType::NVarChar, sku, 0}};
ResultSet 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 =
"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, ?, ?, ?, ?, ?,"
" 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);
@@ -484,6 +603,7 @@ inline int insert_order_item(OdbcPool::Connection* c, int kAuftrag, const nlohma
pk_art,
{ParamType::Int, "", kAuftrag},
cartnr,
{ParamType::Int, "", nReserviert},
{ParamType::NVarChar, name, 0},
{ParamType::NVarChar, item.value("note", ""), 0},
{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::NVarChar, name, 0},
{ParamType::Int, "", kSteuerklasse},
{ParamType::Int, "", has_artikel ? 1 : 0},
{ParamType::Int, "", nType},
{ParamType::NVarChar, item.value("unit", ""), 0},
{ParamType::Double, "", 0, discount}
};
@@ -608,6 +728,39 @@ inline bool is_order_delivered(const nlohmann::json& order) {
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,
const nlohmann::json& order, const std::tm& order_date_tm) {
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);
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 =
"INSERT INTO dbo.tZahlung"
" (kZahlung, cName, dDatum, fBetrag, kBestellung, kBenutzer, nAnzahlung, cHinweis, kZahlungsart,"
" nKeinExport, cExternalTransactionId, nZuweisungstyp, nZahlungstyp, cZuweisungsinfo, nZuweisungswertung)"
" VALUES (?, ?, ?, ?, ?, ?, 0, '', ?,"
" 0, ?, 0, 0, '', 0)";
"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<Param> 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, to_number(payment["amount"], 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::NVarChar, order.value("externalOrderNumber", ""), 0},
{ParamType::Int, "", ZAHLUNG_TYPE_ZAHLUNG},
};
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) {
@@ -678,7 +852,18 @@ inline nlohmann::json create_order(const nlohmann::json& order) {
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);"
@@ -700,7 +885,6 @@ inline nlohmann::json create_order(const nlohmann::json& order) {
std::string currency_iso = order.value("currencyIso", "EUR");
std::string cKundenNr = resolve_auftrag_c_kunden_nr(order);
int nIstExterneRechnung = resolve_n_ist_externe_rechnung(order);
std::vector<Param> ps = {
{ParamType::NVarChar, cAuftragsNr, 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::NVarChar, cKundenNr, 0},
{ParamType::NVarChar, shipping_iso, 0},
{ParamType::Int, "", defaults.kVersandArt},
{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, "", 2},
{ParamType::Int, "", nIstReadOnly},
};
if (active_shop == 0) {
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);
std::vector<delivery::DeliveredItem> delivered_items;
const nlohmann::json& items = order.contains("orderItems") ? order["orderItems"] : nlohmann::json::array();
for (const auto& item : items) {
for (const auto& item : order_items) {
int kAuftragPosition = insert_order_item(c, kAuftrag, item);
insert_pos_order_position_mapping(c, kAuftragPosition, item.value("externalId", ""));
if (kAuftragPosition > 0) {
delivered_items.push_back({kAuftragPosition, to_number(item["quantity"], 1)});
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, defaults.kVersandArt, delivered_items);
delivery::deliver_order(c, g_config.kBenutzer, kAuftrag, kVersandArt, delivered_items);
}
const char* calc =

View File

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

View File

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

View File

@@ -43,8 +43,8 @@ static const char* PRODUCT_LIST_SQL =
"ORDER BY lastChanged ASC";
static std::string gross_price(const std::string& net, const std::string& tax) {
double n = std::stod(net.empty() ? "0" : net);
double t = std::stod(tax.empty() ? "0" : tax);
double n = parse_double(net, 0);
double t = parse_double(tax, 0);
char buf[32];
std::snprintf(buf, sizeof(buf), "%.2f", n * (1.0 + t / 100.0));
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)},
{"sku", row[1].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},
{"created_at", row[5].str},
{"lastChanged", row[6].str},
@@ -115,7 +115,7 @@ inline nlohmann::json get_product_list(int64_t cursor, int limit) {
{"categories", cats},
{"prices", prices},
{"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},
{"isCompositeProduct", row[11].str},
{"attributes", nlohmann::json::array()},

View File

@@ -3,6 +3,7 @@
#include <chrono>
#include <algorithm>
#include <exception>
void Router::add_route(const std::string& method, const std::string& path, Handler handler) {
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.pairing_store = &pairing;
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;
}