u
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)},
|
||||||
|
|||||||
@@ -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,22 +243,140 @@ 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>> {
|
||||||
|
std::vector<Param> ps = {{ParamType::NVarChar, lookup_name, 0}};
|
||||||
ResultSet rs;
|
ResultSet rs;
|
||||||
get_pool().execute(c, "SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE cName = ?", ps, 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(?)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nlohmann::json zahlungsart;
|
||||||
|
if (row) {
|
||||||
|
zahlungsart = {{"kZahlungsart", row->first}, {"cName", row->second}};
|
||||||
|
} else {
|
||||||
int kZahlungsart = allocate_pk(c, "tZahlungsart");
|
int kZahlungsart = allocate_pk(c, "tZahlungsart");
|
||||||
const char* insert =
|
const char* insert =
|
||||||
"INSERT INTO dbo.tZahlungsart"
|
"INSERT INTO dbo.tZahlungsart"
|
||||||
@@ -257,13 +386,15 @@ inline nlohmann::json resolve_zahlungsart(OdbcPool::Connection* c, const std::st
|
|||||||
" VALUES (?, ?, '', 0, '', '', '', 0, 0, 0, 0, 0, 0, 0, 1)";
|
" VALUES (?, ?, '', 0, '', '', '', 0, 0, 0, 0, 0, 0, 0, 1)";
|
||||||
std::vector<Param> ps2 = {
|
std::vector<Param> ps2 = {
|
||||||
{ParamType::Int, "", kZahlungsart},
|
{ParamType::Int, "", kZahlungsart},
|
||||||
{ParamType::NVarChar, key, 0}
|
{ParamType::NVarChar, lookup_name, 0}
|
||||||
};
|
};
|
||||||
|
ResultSet rs;
|
||||||
get_pool().execute(c, insert, ps2, 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 ("
|
||||||
|
" 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,"
|
" (kZahlung, cName, dDatum, fBetrag, kBestellung, kBenutzer, nAnzahlung, cHinweis, kZahlungsart,"
|
||||||
" nKeinExport, cExternalTransactionId, nZuweisungstyp, nZahlungstyp, cZuweisungsinfo, nZuweisungswertung)"
|
" nKeinExport, cExternalTransactionId, nZuweisungstyp, nZahlungstyp, cZuweisungsinfo, nZuweisungswertung)"
|
||||||
" VALUES (?, ?, ?, ?, ?, ?, 0, '', ?,"
|
" VALUES (?, ?, ?, ?, ?, ?, 0, '', ?,"
|
||||||
" 0, ?, 0, 0, '', 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 =
|
||||||
|
|||||||
@@ -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}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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()},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
try {
|
||||||
it->second(req, resp, ctx);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
51
scripts/s3-backup/README.md
Normal file
51
scripts/s3-backup/README.md
Normal 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.
|
||||||
@@ -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) => {
|
||||||
|
const child = spawn(
|
||||||
'sqlcmd',
|
'sqlcmd',
|
||||||
['-W', '-C', '-S', `${server},${port}`, '-U', user, '-P', password, '-Q', query],
|
['-W', '-C', '-S', `${server},${port}`, '-U', user, '-P', password, '-Q', query],
|
||||||
{ encoding: 'utf8' }
|
{ encoding: 'utf8' }
|
||||||
);
|
);
|
||||||
const output = `${result.stdout || ''}${result.stderr || ''}`;
|
let stdout = '';
|
||||||
if (result.status !== 0 || /^\s*Msg \d+,/m.test(output)) {
|
let stderr = '';
|
||||||
throw new Error(output.trim() || 'sqlcmd failed');
|
child.stdout?.on('data', (chunk) => {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
return result.stdout;
|
});
|
||||||
|
child.on('error', reject);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function timestamp() {
|
function timestamp() {
|
||||||
return new Date().toISOString().replace(/[:.]/g, '-');
|
return new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForServer() {
|
async function isServerUp() {
|
||||||
for (let i = 0; i < 40; i++) {
|
const probeHost = /^\d+\./.test(HOST) ? '127.0.0.1' : HOST;
|
||||||
const ok = await new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const req = https.request(
|
const opts = {
|
||||||
{ host: HOST, port: PORT, path: '/', method: 'GET', rejectUnauthorized: false },
|
host: probeHost,
|
||||||
(res) => {
|
port: PORT,
|
||||||
|
path: '/',
|
||||||
|
method: 'GET',
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
};
|
||||||
|
if (!/^\d+\./.test(HOST)) {
|
||||||
|
opts.servername = HOST;
|
||||||
|
}
|
||||||
|
const req = https.request(opts, (res) => {
|
||||||
res.resume();
|
res.resume();
|
||||||
resolve(res.statusCode === 403 || res.statusCode === 200);
|
resolve(res.statusCode === 403 || res.statusCode === 200);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
req.on('error', () => resolve(false));
|
req.on('error', () => resolve(false));
|
||||||
|
req.setTimeout(1000, () => {
|
||||||
|
req.destroy();
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
req.end();
|
req.end();
|
||||||
});
|
});
|
||||||
if (ok) return;
|
}
|
||||||
|
|
||||||
|
async function waitForServer() {
|
||||||
|
for (let i = 0; i < 40; i++) {
|
||||||
|
if (await isServerUp()) 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,37 +153,96 @@ 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(
|
||||||
console.warn('Could not install CA into MSSQL container. Run:');
|
'PAL CA is out of sync with scripts/s3-backup/certs/ca.pem. Run: npm run backup:s3'
|
||||||
console.warn(` docker cp scripts/s3-backup/certs/ca.pem mssql:/tmp/jtlsrv-s3-ca.pem`);
|
);
|
||||||
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.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 {
|
} else {
|
||||||
console.log('Installed S3 CA into MSSQL container');
|
ensureCerts(HOST);
|
||||||
|
const trust = installCaTrust();
|
||||||
|
if (!trust.ok) {
|
||||||
|
console.warn('Could not install CA into MSSQL container. Run:');
|
||||||
|
console.warn(' docker exec -u root mssql mkdir -p /var/opt/mssql/security/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 {
|
||||||
|
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();
|
||||||
|
try {
|
||||||
await waitForServer();
|
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.');
|
||||||
|
await new Promise((resolve) => serverChild.on('exit', resolve));
|
||||||
return;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.has('--help')) {
|
if (args.has('--help')) {
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 2–5 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(`
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM Verkauf.tAuftragEckdaten
|
||||||
|
WHERE kAuftrag = @kBestellung
|
||||||
|
AND ROUND(fOffenerWertOhneStorno, 2) = ROUND(@fOffenerWert, 2)
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
INSERT INTO dbo.tZahlung
|
INSERT INTO dbo.tZahlung
|
||||||
(kZahlung, cName, dDatum, fBetrag, kBestellung, kBenutzer, nAnzahlung, cHinweis, kZahlungsart,
|
(kZahlung, cName, dDatum, fBetrag, kBestellung, kBenutzer, nAnzahlung, cHinweis, kZahlungsart,
|
||||||
nKeinExport, cExternalTransactionId, nZuweisungstyp, nZahlungstyp, cZuweisungsinfo, nZuweisungswertung)
|
nKeinExport, cExternalTransactionId, nZuweisungstyp, nZahlungstyp, cZuweisungsinfo, nZuweisungswertung)
|
||||||
VALUES (@kZahlung, @cName, @dDatum, @fBetrag, @kBestellung, @kBenutzer, 0, '', @kZahlungsart,
|
VALUES (@kZahlung, @cName, @dDatum, @fBetrag, @kBestellung, @kBenutzer, 0, '', @kZahlungsart,
|
||||||
0, @cExternalTransactionId, 0, 0, '', 0)
|
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);
|
||||||
|
if (Number.isInteger(kPosAuftragPosition)) {
|
||||||
await insertPosOrderPositionMapping(transaction, kAuftragPosition, kPosAuftragPosition);
|
await insertPosOrderPositionMapping(transaction, kAuftragPosition, kPosAuftragPosition);
|
||||||
if (kAuftragPosition != null) {
|
}
|
||||||
|
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user