u
This commit is contained in:
@@ -3,7 +3,10 @@
|
||||
#include "../log.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
static OdbcPool g_pool;
|
||||
|
||||
@@ -11,6 +14,28 @@ OdbcPool& get_pool() { return g_pool; }
|
||||
|
||||
OdbcPool::~OdbcPool() { disconnect(); }
|
||||
|
||||
static void odbc_log_diag(SQLSMALLINT handle_type, SQLHANDLE handle, const char* ctx) {
|
||||
if (!handle) {
|
||||
logc::warn("ODBC %s failed (no handle)", ctx);
|
||||
return;
|
||||
}
|
||||
SQLSMALLINT rec = 0;
|
||||
bool any = false;
|
||||
while (true) {
|
||||
SQLCHAR state[6], msg[SQL_MAX_MESSAGE_LENGTH];
|
||||
SQLINTEGER native;
|
||||
SQLSMALLINT msg_len;
|
||||
SQLRETURN diag_rc = SQLGetDiagRec(handle_type, handle, ++rec,
|
||||
state, &native, msg, sizeof(msg), &msg_len);
|
||||
if (diag_rc != SQL_SUCCESS && diag_rc != SQL_SUCCESS_WITH_INFO) break;
|
||||
any = true;
|
||||
logc::warn("ODBC %s: %s - %s (%d)", ctx, state, msg, (int)native);
|
||||
}
|
||||
if (!any) {
|
||||
logc::warn("ODBC %s failed (no diag)", ctx);
|
||||
}
|
||||
}
|
||||
|
||||
int OdbcPool::connect() {
|
||||
SQLRETURN rc;
|
||||
|
||||
@@ -31,7 +56,6 @@ int OdbcPool::connect() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Connection string
|
||||
std::string conn_str =
|
||||
"DRIVER={ODBC Driver 18 for SQL Server};"
|
||||
"SERVER=" + server + "," + std::to_string(port) + ";"
|
||||
@@ -43,8 +67,7 @@ int OdbcPool::connect() {
|
||||
|
||||
logc::info("ODBC connecting to %s:%d/%s as %s", server.c_str(), port, database.c_str(), user.c_str());
|
||||
|
||||
// Create pool of 4 connections
|
||||
const int POOL_SIZE = 4;
|
||||
const int POOL_SIZE = config::get_int("MSSQL_POOL_SIZE", 1);
|
||||
conns_.resize(POOL_SIZE);
|
||||
int connected = 0;
|
||||
|
||||
@@ -58,14 +81,14 @@ int OdbcPool::connect() {
|
||||
|
||||
if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
|
||||
SQLAllocHandle(SQL_HANDLE_STMT, conns_[i].hdbc, &conns_[i].hstmt);
|
||||
SQLSetConnectAttr(conns_[i].hdbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0);
|
||||
connected++;
|
||||
} else {
|
||||
SQLCHAR state[6], msg[SQL_MAX_MESSAGE_LENGTH];
|
||||
SQLINTEGER native;
|
||||
SQLSMALLINT msg_len;
|
||||
SQLGetDiagRec(SQL_HANDLE_DBC, conns_[i].hdbc, 1,
|
||||
state, &native, msg, sizeof(msg), &msg_len);
|
||||
logc::warn("ODBC connect[%d] failed: %s - %s", i, state, msg);
|
||||
char ctx[32];
|
||||
std::snprintf(ctx, sizeof(ctx), "connect[%d]", i);
|
||||
odbc_log_diag(SQL_HANDLE_DBC, conns_[i].hdbc, ctx);
|
||||
SQLFreeHandle(SQL_HANDLE_DBC, conns_[i].hdbc);
|
||||
conns_[i].hdbc = SQL_NULL_HDBC;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,44 +110,52 @@ void OdbcPool::disconnect() {
|
||||
if (henv_ != SQL_NULL_HENV) { SQLFreeHandle(SQL_HANDLE_ENV, henv_); henv_ = SQL_NULL_HENV; }
|
||||
}
|
||||
|
||||
OdbcPool::Connection* OdbcPool::checkout() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
for (auto& c : conns_) {
|
||||
if (!c.in_use) { c.in_use = true; return &c; }
|
||||
OdbcPool::Connection* OdbcPool::checkout_raw() {
|
||||
for (int attempt = 0; attempt < 300; attempt++) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
for (auto& c : conns_) {
|
||||
if (!c.in_use && c.hdbc != SQL_NULL_HDBC) {
|
||||
c.in_use = true;
|
||||
return &c;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
OdbcPool::ConnGuard OdbcPool::checkout() {
|
||||
return ConnGuard(checkout_raw(), this);
|
||||
}
|
||||
|
||||
void OdbcPool::release(Connection* c) {
|
||||
if (!c) return;
|
||||
if (c->in_transaction) {
|
||||
SQLEndTran(SQL_HANDLE_DBC, c->hdbc, SQL_ROLLBACK);
|
||||
SQLSetConnectAttr(c->hdbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0);
|
||||
c->in_transaction = false;
|
||||
logc::warn("ODBC: rolled back uncommitted transaction on connection release");
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
c->in_use = false;
|
||||
}
|
||||
|
||||
bool OdbcPool::execute(const std::string& sql, const std::vector<Param>& params, ResultSet& out) {
|
||||
Connection* c = checkout();
|
||||
if (!c) return false;
|
||||
static bool bind_params(SQLHSTMT hstmt, const std::vector<Param>& params, std::vector<SQLLEN>& indicators) {
|
||||
indicators.assign(params.size(), 0);
|
||||
|
||||
SQLRETURN rc;
|
||||
// Stable placeholder for null numeric parameters.
|
||||
static const int64_t null_placeholder = 0;
|
||||
|
||||
// Prepare statement
|
||||
rc = SQLPrepare(c->hstmt, (SQLCHAR*)sql.c_str(), SQL_NTS);
|
||||
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
|
||||
release(c);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wide buffer storage for NVarChar parameters (must outlive SQLExecute)
|
||||
std::vector<std::vector<SQLWCHAR>> wbufs;
|
||||
|
||||
// Bind parameters
|
||||
for (size_t i = 0; i < params.size(); i++) {
|
||||
const auto& p = params[i];
|
||||
SQLUSMALLINT param_num = static_cast<SQLUSMALLINT>(i + 1);
|
||||
SQLPOINTER val_ptr = nullptr;
|
||||
SQLPOINTER val_ptr = (SQLPOINTER)&null_placeholder;
|
||||
SQLLEN buf_len = 0;
|
||||
SQLLEN indicator = 0;
|
||||
SQLSMALLINT c_type = SQL_C_CHAR;
|
||||
SQLSMALLINT sql_type = SQL_VARCHAR;
|
||||
SQLULEN column_size = 1;
|
||||
|
||||
switch (p.type) {
|
||||
case ParamType::Int:
|
||||
@@ -132,12 +163,16 @@ bool OdbcPool::execute(const std::string& sql, const std::vector<Param>& params,
|
||||
sql_type = SQL_INTEGER;
|
||||
val_ptr = (SQLPOINTER)&p.int_val;
|
||||
buf_len = sizeof(SQLINTEGER);
|
||||
column_size = sizeof(SQLINTEGER);
|
||||
indicators[i] = buf_len;
|
||||
break;
|
||||
case ParamType::BigInt:
|
||||
c_type = SQL_C_SBIGINT;
|
||||
sql_type = SQL_BIGINT;
|
||||
val_ptr = (SQLPOINTER)&p.int_val;
|
||||
buf_len = sizeof(SQLBIGINT);
|
||||
column_size = sizeof(SQLBIGINT);
|
||||
indicators[i] = buf_len;
|
||||
break;
|
||||
case ParamType::Float:
|
||||
case ParamType::Double:
|
||||
@@ -145,128 +180,167 @@ bool OdbcPool::execute(const std::string& sql, const std::vector<Param>& params,
|
||||
sql_type = SQL_DOUBLE;
|
||||
val_ptr = (SQLPOINTER)&p.dbl_val;
|
||||
buf_len = sizeof(SQLDOUBLE);
|
||||
column_size = sizeof(SQLDOUBLE);
|
||||
indicators[i] = buf_len;
|
||||
break;
|
||||
case ParamType::NVarChar: {
|
||||
// Convert narrow string to SQLWCHAR (unsigned short = UTF-16)
|
||||
// SQLWCHAR is unsigned short (2 bytes) on this platform
|
||||
std::vector<SQLWCHAR> wbuf(p.str_val.size());
|
||||
for (size_t k = 0; k < p.str_val.size(); k++)
|
||||
wbuf[k] = static_cast<SQLWCHAR>((unsigned char)p.str_val[k]);
|
||||
wbufs.push_back(std::move(wbuf));
|
||||
auto& wb = wbufs.back();
|
||||
c_type = SQL_C_WCHAR;
|
||||
case ParamType::NVarChar:
|
||||
case ParamType::DateTime:
|
||||
// UTF-8 SQL_C_CHAR + SQL_WVARCHAR works with msodbcsql18 for nvarchar columns.
|
||||
c_type = SQL_C_CHAR;
|
||||
sql_type = SQL_WVARCHAR;
|
||||
val_ptr = (SQLPOINTER)wb.data();
|
||||
buf_len = (SQLLEN)(wb.size() * sizeof(SQLWCHAR));
|
||||
indicator = wb.empty() ? SQL_NULL_DATA : (SQLLEN)(wb.size() * sizeof(SQLWCHAR));
|
||||
column_size = p.str_val.empty() ? 1 : p.str_val.size();
|
||||
if (!p.is_null) {
|
||||
val_ptr = (SQLPOINTER)p.str_val.c_str();
|
||||
buf_len = static_cast<SQLLEN>(p.str_val.size());
|
||||
indicators[i] = SQL_NTS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ParamType::Bit:
|
||||
c_type = SQL_C_BIT;
|
||||
sql_type = SQL_BIT;
|
||||
val_ptr = (SQLPOINTER)&p.int_val;
|
||||
buf_len = 1;
|
||||
column_size = 1;
|
||||
indicators[i] = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
rc = SQLBindParameter(c->hstmt, param_num, SQL_PARAM_INPUT,
|
||||
c_type, sql_type, p.str_val.size() + 1, 0, val_ptr, buf_len, &indicator);
|
||||
if (p.is_null) {
|
||||
indicators[i] = SQL_NULL_DATA;
|
||||
}
|
||||
|
||||
SQLRETURN rc = SQLBindParameter(hstmt, param_num, SQL_PARAM_INPUT,
|
||||
c_type, sql_type, column_size, 0, val_ptr, buf_len, &indicators[i]);
|
||||
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
|
||||
release(c);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Execute
|
||||
rc = SQLExecute(c->hstmt);
|
||||
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO && rc != SQL_NO_DATA) {
|
||||
release(c);
|
||||
static bool fetch_results(SQLHSTMT hstmt, ResultSet& out) {
|
||||
out.clear();
|
||||
|
||||
while (true) {
|
||||
SQLSMALLINT col_count = 0;
|
||||
SQLNumResultCols(hstmt, &col_count);
|
||||
|
||||
if (col_count > 0) {
|
||||
std::vector<SQLSMALLINT> col_types(col_count);
|
||||
for (SQLSMALLINT col = 0; col < col_count; col++) {
|
||||
SQLSMALLINT data_type;
|
||||
SQLDescribeCol(hstmt, col + 1, nullptr, 0, nullptr, &data_type, nullptr, nullptr, nullptr);
|
||||
col_types[col] = data_type;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
SQLRETURN rc = SQLFetch(hstmt);
|
||||
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) break;
|
||||
|
||||
Row row;
|
||||
for (SQLSMALLINT col = 0; col < col_count; col++) {
|
||||
Cell cell;
|
||||
SQLLEN ind;
|
||||
SQLSMALLINT sql_type = col_types[col];
|
||||
|
||||
if (sql_type == SQL_BINARY || sql_type == SQL_VARBINARY ||
|
||||
sql_type == SQL_LONGVARBINARY) {
|
||||
std::vector<uint8_t> blob_data;
|
||||
unsigned char chunk[8192];
|
||||
while (true) {
|
||||
rc = SQLGetData(hstmt, col + 1, SQL_C_BINARY, chunk, sizeof(chunk), &ind);
|
||||
if (ind == SQL_NULL_DATA) { break; }
|
||||
if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
|
||||
SQLLEN copy_len = std::min(ind, (SQLLEN)sizeof(chunk));
|
||||
blob_data.insert(blob_data.end(), chunk, chunk + copy_len);
|
||||
}
|
||||
if (rc == SQL_SUCCESS) break;
|
||||
if (rc != SQL_SUCCESS_WITH_INFO) break;
|
||||
}
|
||||
if (!blob_data.empty()) {
|
||||
cell.type = CellType::Blob;
|
||||
cell.blob = std::move(blob_data);
|
||||
}
|
||||
} else {
|
||||
char buf[4096];
|
||||
rc = SQLGetData(hstmt, col + 1, SQL_C_CHAR, buf, sizeof(buf) - 1, &ind);
|
||||
if (ind == SQL_NULL_DATA) {
|
||||
cell.type = CellType::Null;
|
||||
} else if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
|
||||
cell.type = CellType::String;
|
||||
cell.str = std::string(buf, std::min((SQLLEN)(sizeof(buf)-1), ind));
|
||||
}
|
||||
}
|
||||
row.push_back(std::move(cell));
|
||||
}
|
||||
out.push_back(std::move(row));
|
||||
}
|
||||
if (!out.empty()) return true;
|
||||
}
|
||||
|
||||
SQLRETURN rc = SQLMoreResults(hstmt);
|
||||
if (rc == SQL_NO_DATA) break;
|
||||
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OdbcPool::execute(Connection* c, const std::string& sql, const std::vector<Param>& params, ResultSet& out) {
|
||||
if (!c) {
|
||||
logc::warn("ODBC execute: no connection");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch results if it's a SELECT
|
||||
out.clear();
|
||||
SQLSMALLINT col_count = 0;
|
||||
SQLNumResultCols(c->hstmt, &col_count);
|
||||
|
||||
if (col_count > 0) {
|
||||
// Discover column types
|
||||
std::vector<SQLSMALLINT> col_types(col_count);
|
||||
bool has_blobs = false;
|
||||
for (SQLSMALLINT col = 0; col < col_count; col++) {
|
||||
SQLSMALLINT data_type;
|
||||
SQLDescribeCol(c->hstmt, col + 1, nullptr, 0, nullptr, &data_type, nullptr, nullptr, nullptr);
|
||||
col_types[col] = data_type;
|
||||
if (data_type == SQL_BINARY || data_type == SQL_VARBINARY || data_type == SQL_LONGVARBINARY)
|
||||
has_blobs = true;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
rc = SQLFetch(c->hstmt);
|
||||
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) break;
|
||||
|
||||
Row row;
|
||||
for (SQLSMALLINT col = 0; col < col_count; col++) {
|
||||
Cell cell;
|
||||
SQLLEN ind;
|
||||
SQLSMALLINT sql_type = col_types[col];
|
||||
|
||||
// Binary columns: read as binary
|
||||
if (sql_type == SQL_BINARY || sql_type == SQL_VARBINARY ||
|
||||
sql_type == SQL_LONGVARBINARY) {
|
||||
std::vector<uint8_t> blob_data;
|
||||
unsigned char chunk[8192];
|
||||
int chunk_count = 0;
|
||||
while (true) {
|
||||
rc = SQLGetData(c->hstmt, col + 1, SQL_C_BINARY, chunk, sizeof(chunk), &ind);
|
||||
if (ind == SQL_NULL_DATA) { break; }
|
||||
if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
|
||||
SQLLEN copy_len = std::min(ind, (SQLLEN)sizeof(chunk));
|
||||
if (has_blobs && chunk_count == 0)
|
||||
logc::info(" blob col=%d rc=%d ind=%d copy=%d", col, rc, (int)ind, (int)copy_len);
|
||||
blob_data.insert(blob_data.end(), chunk, chunk + copy_len);
|
||||
chunk_count++;
|
||||
}
|
||||
if (rc == SQL_SUCCESS) break;
|
||||
if (rc != SQL_SUCCESS_WITH_INFO) break;
|
||||
}
|
||||
if (chunk_count > 0) {
|
||||
cell.type = CellType::Blob;
|
||||
cell.blob = std::move(blob_data);
|
||||
if (has_blobs)
|
||||
logc::info(" blob col=%d total=%zu chunks=%d", col, cell.blob.size(), chunk_count);
|
||||
}
|
||||
} else {
|
||||
// String/numeric columns: read as char
|
||||
char buf[4096];
|
||||
rc = SQLGetData(c->hstmt, col + 1, SQL_C_CHAR, buf, sizeof(buf) - 1, &ind);
|
||||
if (ind == SQL_NULL_DATA) {
|
||||
cell.type = CellType::Null;
|
||||
} else if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
|
||||
cell.type = CellType::String;
|
||||
cell.str = std::string(buf, std::min((SQLLEN)(sizeof(buf)-1), ind));
|
||||
}
|
||||
}
|
||||
row.push_back(std::move(cell));
|
||||
}
|
||||
out.push_back(std::move(row));
|
||||
}
|
||||
SQLRETURN rc = SQLPrepare(c->hstmt, (SQLCHAR*)sql.c_str(), SQL_NTS);
|
||||
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
|
||||
odbc_log_diag(SQL_HANDLE_STMT, c->hstmt, "prepare");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reset statement for reuse
|
||||
std::vector<SQLLEN> indicators;
|
||||
if (!params.empty() && !bind_params(c->hstmt, params, indicators)) {
|
||||
odbc_log_diag(SQL_HANDLE_STMT, c->hstmt, "bind");
|
||||
return false;
|
||||
}
|
||||
|
||||
rc = SQLExecute(c->hstmt);
|
||||
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO && rc != SQL_NO_DATA) {
|
||||
odbc_log_diag(SQL_HANDLE_STMT, c->hstmt, "execute");
|
||||
return false;
|
||||
}
|
||||
|
||||
fetch_results(c->hstmt, out);
|
||||
|
||||
SQLFreeStmt(c->hstmt, SQL_UNBIND);
|
||||
SQLFreeStmt(c->hstmt, SQL_CLOSE);
|
||||
release(c);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OdbcPool::execute(Connection* c, const std::string& sql, ResultSet& out) {
|
||||
return execute(c, sql, {}, out);
|
||||
}
|
||||
|
||||
bool OdbcPool::execute(const std::string& sql, const std::vector<Param>& params, ResultSet& out) {
|
||||
ConnGuard g = checkout();
|
||||
if (!g) {
|
||||
logc::warn("ODBC execute: pool exhausted");
|
||||
return false;
|
||||
}
|
||||
bool ok = execute(g.get(), sql, params, out);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool OdbcPool::execute(const std::string& sql, ResultSet& out) {
|
||||
return execute(sql, {}, out);
|
||||
}
|
||||
|
||||
int64_t OdbcPool::execute_scalar(const std::string& sql, const std::vector<Param>& params, int64_t fallback) {
|
||||
ResultSet rs;
|
||||
if (!execute(sql, params, rs) || rs.empty() || rs[0].empty()) return fallback;
|
||||
if (!execute(sql, params, rs)) {
|
||||
logc::warn("ODBC scalar query failed");
|
||||
return fallback;
|
||||
}
|
||||
if (rs.empty() || rs[0].empty()) return fallback;
|
||||
const auto& cell = rs[0][0];
|
||||
if (cell.type == CellType::Int64) return cell.i64;
|
||||
if (cell.type == CellType::String) {
|
||||
@@ -274,3 +348,42 @@ int64_t OdbcPool::execute_scalar(const std::string& sql, const std::vector<Param
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
bool OdbcPool::begin(Connection* c) {
|
||||
if (!c) return false;
|
||||
if (c->in_transaction) {
|
||||
logc::warn("ODBC begin: connection already in transaction");
|
||||
return false;
|
||||
}
|
||||
SQLRETURN rc = SQLSetConnectAttr(c->hdbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0);
|
||||
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
|
||||
odbc_log_diag(SQL_HANDLE_DBC, c->hdbc, "begin");
|
||||
return false;
|
||||
}
|
||||
c->in_transaction = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OdbcPool::commit(Connection* c) {
|
||||
if (!c || !c->in_transaction) return false;
|
||||
SQLRETURN rc = SQLEndTran(SQL_HANDLE_DBC, c->hdbc, SQL_COMMIT);
|
||||
SQLSetConnectAttr(c->hdbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0);
|
||||
c->in_transaction = false;
|
||||
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
|
||||
odbc_log_diag(SQL_HANDLE_DBC, c->hdbc, "commit");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OdbcPool::rollback(Connection* c) {
|
||||
if (!c || !c->in_transaction) return true;
|
||||
SQLRETURN rc = SQLEndTran(SQL_HANDLE_DBC, c->hdbc, SQL_ROLLBACK);
|
||||
SQLSetConnectAttr(c->hdbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0);
|
||||
c->in_transaction = false;
|
||||
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
|
||||
odbc_log_diag(SQL_HANDLE_DBC, c->hdbc, "rollback");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -21,35 +21,75 @@ struct Cell {
|
||||
using Row = std::vector<Cell>;
|
||||
using ResultSet = std::vector<Row>;
|
||||
|
||||
enum class ParamType { Int, BigInt, Float, Double, NVarChar, Bit };
|
||||
enum class ParamType { Int, BigInt, Float, Double, NVarChar, Bit, DateTime };
|
||||
|
||||
struct Param {
|
||||
ParamType type;
|
||||
std::string str_val;
|
||||
int64_t int_val = 0;
|
||||
double dbl_val = 0.0;
|
||||
bool is_null = false;
|
||||
|
||||
static Param null_int() { Param p; p.type = ParamType::Int; p.is_null = true; return p; }
|
||||
static Param null_bigint() { Param p; p.type = ParamType::BigInt; p.is_null = true; return p; }
|
||||
static Param null_nvarchar() { Param p; p.type = ParamType::NVarChar; p.is_null = true; return p; }
|
||||
};
|
||||
|
||||
class OdbcPool {
|
||||
public:
|
||||
~OdbcPool();
|
||||
int connect();
|
||||
void disconnect();
|
||||
bool execute(const std::string& sql, const std::vector<Param>& params, ResultSet& out);
|
||||
bool execute(const std::string& sql, ResultSet& out);
|
||||
int64_t execute_scalar(const std::string& sql, const std::vector<Param>& params = {}, int64_t fallback = 0);
|
||||
|
||||
private:
|
||||
struct Connection {
|
||||
SQLHDBC hdbc = SQL_NULL_HDBC;
|
||||
SQLHSTMT hstmt = SQL_NULL_HSTMT;
|
||||
bool in_use = false;
|
||||
bool in_transaction = false;
|
||||
};
|
||||
|
||||
// RAII checkout guard. Returned by checkout().
|
||||
struct ConnGuard {
|
||||
Connection* conn = nullptr;
|
||||
OdbcPool* pool = nullptr;
|
||||
ConnGuard() = default;
|
||||
ConnGuard(Connection* c, OdbcPool* p) : conn(c), pool(p) {}
|
||||
~ConnGuard() { if (conn && pool) pool->release(conn); }
|
||||
ConnGuard(const ConnGuard&) = delete;
|
||||
ConnGuard(ConnGuard&& other) noexcept : conn(other.conn), pool(other.pool) { other.conn = nullptr; }
|
||||
ConnGuard& operator=(ConnGuard&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (conn && pool) pool->release(conn);
|
||||
conn = other.conn;
|
||||
pool = other.pool;
|
||||
other.conn = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
Connection* operator->() const { return conn; }
|
||||
Connection* get() const { return conn; }
|
||||
explicit operator bool() const { return conn != nullptr; }
|
||||
};
|
||||
|
||||
~OdbcPool();
|
||||
int connect();
|
||||
void disconnect();
|
||||
|
||||
bool execute(const std::string& sql, const std::vector<Param>& params, ResultSet& out);
|
||||
bool execute(const std::string& sql, ResultSet& out);
|
||||
int64_t execute_scalar(const std::string& sql, const std::vector<Param>& params = {}, int64_t fallback = 0);
|
||||
|
||||
// Transactional execution on an explicitly checked-out connection.
|
||||
ConnGuard checkout();
|
||||
bool execute(Connection* conn, const std::string& sql, const std::vector<Param>& params, ResultSet& out);
|
||||
bool execute(Connection* conn, const std::string& sql, ResultSet& out);
|
||||
bool begin(Connection* conn);
|
||||
bool commit(Connection* conn);
|
||||
bool rollback(Connection* conn);
|
||||
|
||||
void release(Connection* c);
|
||||
|
||||
private:
|
||||
SQLHENV henv_ = SQL_NULL_HENV;
|
||||
std::vector<Connection> conns_;
|
||||
std::mutex mutex_;
|
||||
Connection* checkout();
|
||||
void release(Connection* c);
|
||||
Connection* checkout_raw();
|
||||
};
|
||||
|
||||
OdbcPool& get_pool();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "../router.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"));
|
||||
int limit = std::stoi(req.get_query_param("limit", "20"));
|
||||
auto categories = get_category_list(cursor, limit);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "../log.hpp"
|
||||
#include "../queries/image.hpp"
|
||||
|
||||
void handle_cimage(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
|
||||
void handle_cimage(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
|
||||
std::string path = req.get_query_param("path");
|
||||
if (path.empty()) {
|
||||
resp.send_json(400, {{"Message", "Missing required query parameter 'path'."}});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "../router.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"));
|
||||
auto groups = get_customer_group_list(cursor);
|
||||
resp.send_json(200, groups);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "../router.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"));
|
||||
int limit = std::stoi(req.get_query_param("limit", "600"));
|
||||
auto deleted = get_deleted_entity_list(cursor, limit);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#include "../http.hpp"
|
||||
#include "../tls_server.hpp"
|
||||
#include "../router.hpp"
|
||||
#include "../log.hpp"
|
||||
#include "../queries/counts.hpp"
|
||||
#include "../queries/customer_groups.hpp"
|
||||
#include "../queries/shop.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 category_cursor = std::stoll(req.get_query_param("lastChangedCategory", "0"));
|
||||
int64_t cg_cursor = std::stoll(req.get_query_param("lastChangedCustomerGroup", "0"));
|
||||
@@ -17,6 +18,7 @@ void handle_init(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
|
||||
int shop = get_active_shop_id();
|
||||
|
||||
int64_t product_count = 0, category_count = 0, cg_count = 0, composite_count = 0, deleted_count = 0;
|
||||
int64_t max_order_id_count = 0;
|
||||
|
||||
if (get_pool().execute_scalar("SELECT 1") != 0) {
|
||||
product_count = get_product_count(root, shop, product_cursor);
|
||||
@@ -24,6 +26,16 @@ void handle_init(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
|
||||
cg_count = get_customer_group_count(cg_cursor);
|
||||
composite_count = get_composite_count(shop, composite_cursor);
|
||||
deleted_count = get_deleted_count(deleted_cursor);
|
||||
max_order_id_count = get_max_order_id_count(get_active_shop_subshop_id());
|
||||
logc::info("init counts: products=%lld categories=%lld cg=%lld composite=%lld deleted=%lld max_order=%lld root=%d shop=%d subshop=%d",
|
||||
product_count, category_count, cg_count, composite_count, deleted_count, max_order_id_count,
|
||||
root, shop, get_active_shop_subshop_id());
|
||||
if (product_cursor == 0 && category_cursor == 0 &&
|
||||
product_count == 0 && category_count == 0 && deleted_count == 0) {
|
||||
logc::warn("init: all counts zero with cursors at 0 — check DB connectivity and shop/category config");
|
||||
}
|
||||
} else {
|
||||
logc::warn("init: DB connection check failed, returning zero counts");
|
||||
}
|
||||
|
||||
resp.send_json(200, {
|
||||
@@ -36,6 +48,6 @@ void handle_init(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
|
||||
{"configurationGroup_count", "0"},
|
||||
{"configurationItem_count", "0"},
|
||||
{"deletedEntity_count", std::to_string(deleted_count)},
|
||||
{"max_orderId_count", "0"}
|
||||
{"max_orderId_count", std::to_string(max_order_id_count)}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
// POST /v1/order — creates one or more POS orders.
|
||||
// Port of src/endpoints/order.js and src/queries/create-order.js.
|
||||
|
||||
#include "../http.hpp"
|
||||
#include "../tls_server.hpp"
|
||||
#include "../router.hpp"
|
||||
#include "../log.hpp"
|
||||
#include "../order_log.hpp"
|
||||
#include "../queries/create_order.hpp"
|
||||
|
||||
void handle_order(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
|
||||
// TODO: full order creation (Milestone 5/6)
|
||||
// For now, parse the JSON body and acknowledge
|
||||
#include <string>
|
||||
|
||||
static nlohmann::json get_orders(const nlohmann::json& body) {
|
||||
if (!body.is_object()) return nlohmann::json::array();
|
||||
auto it = body.find("orders");
|
||||
if (it == body.end() || !it->is_array()) return nlohmann::json::array();
|
||||
return *it;
|
||||
}
|
||||
|
||||
void handle_order(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
|
||||
nlohmann::json body;
|
||||
try {
|
||||
body = nlohmann::json::parse(req.body);
|
||||
@@ -14,15 +25,44 @@ void handle_order(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
|
||||
}
|
||||
|
||||
nlohmann::json results = nlohmann::json::array();
|
||||
if (body.contains("orders") && body["orders"].is_array()) {
|
||||
for (auto& order : body["orders"]) {
|
||||
std::string ext_id = order.value("externalId", "");
|
||||
nlohmann::json orders = get_orders(body);
|
||||
|
||||
int successful = 0;
|
||||
int failed = 0;
|
||||
|
||||
for (const auto& order : orders) {
|
||||
std::string externalOrderId = order.value("externalId", "");
|
||||
g_order_log.log_order(order.dump(), externalOrderId);
|
||||
|
||||
try {
|
||||
nlohmann::json created = order::create_order(order);
|
||||
logc::success("order %s (kAuftrag=%s) created for externalId=%s",
|
||||
created.value("orderNumber", "").c_str(),
|
||||
created.value("orderId", "").c_str(),
|
||||
externalOrderId.c_str());
|
||||
results.push_back({
|
||||
{"status", "OK"},
|
||||
{"externalOrderId", ext_id},
|
||||
{"externalOrderId", externalOrderId},
|
||||
{"message", ""}
|
||||
});
|
||||
++successful;
|
||||
} catch (const std::exception& ex) {
|
||||
logc::error("order externalId=%s failed: %s", externalOrderId.c_str(), ex.what());
|
||||
results.push_back({
|
||||
{"status", "ERROR"},
|
||||
{"externalOrderId", externalOrderId},
|
||||
{"message", ex.what()}
|
||||
});
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
|
||||
resp.send_json(200, results);
|
||||
|
||||
if (orders.empty()) {
|
||||
logc::info("POST /v1/order: no orders in body");
|
||||
} else {
|
||||
logc::info("POST /v1/order: %d order(s), %d OK, %d ERROR",
|
||||
static_cast<int>(orders.size()), successful, failed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "../log.hpp"
|
||||
#include "../queries/image.hpp"
|
||||
|
||||
void handle_pimage(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
|
||||
void handle_pimage(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
|
||||
std::string path = req.get_query_param("path");
|
||||
if (path.empty()) {
|
||||
resp.send_json(400, {{"Message", "Missing required query parameter 'path'."}});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "../router.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"));
|
||||
int limit = std::stoi(req.get_query_param("limit", "20"));
|
||||
auto products = get_product_list(cursor, limit);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "../router.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"));
|
||||
int limit = std::stoi(req.get_query_param("limit", "100"));
|
||||
auto composites = get_composite_product_list(cursor, limit);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <vips/vips.h>
|
||||
#include "queries/shop.hpp"
|
||||
#include "queries/customer_groups.hpp"
|
||||
#include "request_log.hpp"
|
||||
#include "order_log.hpp"
|
||||
|
||||
@@ -27,7 +28,6 @@ static uv_loop_t* loop = nullptr;
|
||||
static Router router;
|
||||
static PairingStore pairing_store;
|
||||
static RequestLog request_log;
|
||||
static OrderLog order_log;
|
||||
|
||||
static json build_config() {
|
||||
return {
|
||||
@@ -85,7 +85,7 @@ static void handle_request(tls_session* sess) {
|
||||
// Console log with response size and truncated body
|
||||
const char* ip = sess->peer_ip.c_str();
|
||||
if (resp_size > 0) {
|
||||
std::string preview = resp_body.substr(0, std::min(resp_size, (size_t)200));
|
||||
std::string preview = resp_body.substr(0, std::min(resp_size, (size_t)220));
|
||||
logc::info("%s %s %s %d %dms [%zu bytes] %s",
|
||||
ip, req.method.c_str(), url.c_str(), resp.status_code,
|
||||
(int)elapsed, resp_size, preview.c_str());
|
||||
@@ -102,7 +102,7 @@ static void handle_request(tls_session* sess) {
|
||||
// Entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
int main(int /*argc*/, char* argv[]) {
|
||||
config::load(".env");
|
||||
|
||||
if (VIPS_INIT(argv[0])) {
|
||||
@@ -110,6 +110,7 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
int port = config::get_int("PORT", 4443);
|
||||
std::string bind_address = config::get("BIND_ADDRESS", "0.0.0.0");
|
||||
std::string cert_path = "certs/cert.pem";
|
||||
std::string key_path = "certs/key.pem";
|
||||
|
||||
@@ -140,7 +141,10 @@ int main(int argc, char* argv[]) {
|
||||
config::get("MSSQL_DATABASE").c_str());
|
||||
if (fetch_active_shop()) {
|
||||
logc::info("Active shop ID: %d", get_active_shop_id());
|
||||
} else {
|
||||
logc::warn("Active shop not loaded — sync filters and order mapping may be wrong");
|
||||
}
|
||||
(void)get_customer_group_ids();
|
||||
} else {
|
||||
logc::warn("MSSQL connection skipped");
|
||||
logc::warn("POS handshake will still work; sync from database is not available yet.");
|
||||
@@ -150,9 +154,9 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
// Open log files
|
||||
request_log.open(config::get("LOG_FILE", "logs/requests.log"));
|
||||
order_log.open(config::get("ORDER_LOG_FILE", "logs/orders.log"));
|
||||
g_order_log.open(config::get("ORDER_LOG_FILE", "logs/orders.log"));
|
||||
|
||||
int r = tls_server_init(loop, "0.0.0.0", port,
|
||||
int r = tls_server_init(loop, bind_address.c_str(), port,
|
||||
cert_path.c_str(), key_path.c_str());
|
||||
if (r != 0) {
|
||||
logc::error("failed to start TLS server");
|
||||
@@ -166,7 +170,7 @@ int main(int argc, char* argv[]) {
|
||||
uv_run(loop, UV_RUN_DEFAULT);
|
||||
|
||||
request_log.close();
|
||||
order_log.close();
|
||||
g_order_log.close();
|
||||
get_pool().disconnect();
|
||||
vips_shutdown();
|
||||
logc::info("shutdown complete.");
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <ctime>
|
||||
#include <sys/stat.h>
|
||||
|
||||
OrderLog g_order_log;
|
||||
|
||||
static void ensure_parent_dir(const std::string& path) {
|
||||
size_t pos = path.rfind('/');
|
||||
if (pos != std::string::npos) {
|
||||
@@ -19,10 +21,9 @@ void OrderLog::close() {
|
||||
if (fp_) { std::fclose(fp_); fp_ = nullptr; }
|
||||
}
|
||||
|
||||
int OrderLog::log_order(const std::string& order_json) {
|
||||
void OrderLog::log_order(const std::string& order_json, const std::string& external_id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
sequence_++;
|
||||
max_external_id_++;
|
||||
|
||||
if (fp_) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
@@ -32,13 +33,8 @@ int OrderLog::log_order(const std::string& order_json) {
|
||||
char ts[32];
|
||||
std::strftime(ts, sizeof(ts), "%Y-%m-%dT%H:%M:%S", &tm_buf);
|
||||
|
||||
std::fprintf(fp_, "%s #%d externalId=%d %s\n",
|
||||
ts, sequence_, max_external_id_, order_json.c_str());
|
||||
std::fprintf(fp_, "%s #%d externalId=%s %s\n",
|
||||
ts, sequence_, external_id.c_str(), order_json.c_str());
|
||||
std::fflush(fp_);
|
||||
}
|
||||
return max_external_id_;
|
||||
}
|
||||
|
||||
std::string OrderLog::get_max_external_id() const {
|
||||
return std::to_string(max_external_id_);
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ class OrderLog {
|
||||
public:
|
||||
void open(const std::string& path);
|
||||
void close();
|
||||
int log_order(const std::string& order_json);
|
||||
std::string get_max_external_id() const;
|
||||
void log_order(const std::string& order_json, const std::string& external_id = "");
|
||||
private:
|
||||
FILE* fp_ = nullptr;
|
||||
std::mutex mutex_;
|
||||
int sequence_ = 0;
|
||||
int max_external_id_ = 0;
|
||||
};
|
||||
|
||||
extern OrderLog g_order_log;
|
||||
|
||||
32
jtlsrv-cpp/src/queries/commit.hpp
Normal file
32
jtlsrv-cpp/src/queries/commit.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include "xml.hpp"
|
||||
#include "../db/pool.hpp"
|
||||
|
||||
namespace delivery {
|
||||
|
||||
inline void commit_picklists(OdbcPool::Connection* c, int kBenutzer, int kSessionId, int kAuftrag) {
|
||||
std::string bestellungen = xml::element("Bestellung", xml::tag("kBestellung", int64_t(kAuftrag)));
|
||||
|
||||
const char* sql =
|
||||
"DECLARE @xBestellungen XML = CONVERT(XML, ?);"
|
||||
"DECLARE @xResult XML;"
|
||||
"EXEC Auslieferung.spPicklistenUebernehmen"
|
||||
" @Bestellungen = @xBestellungen,"
|
||||
" @kBenutzer = ?,"
|
||||
" @nTeillieferung = 0,"
|
||||
" @kSessionId = ?,"
|
||||
" @xResult = @xResult OUTPUT";
|
||||
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::NVarChar, bestellungen, 0},
|
||||
{ParamType::Int, "", kBenutzer},
|
||||
{ParamType::Int, "", kSessionId}
|
||||
};
|
||||
ResultSet rs;
|
||||
if (!get_pool().execute(c, sql, ps, rs)) {
|
||||
throw std::runtime_error("commit_picklists failed");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace delivery
|
||||
@@ -43,23 +43,32 @@ static const char* DELETED_ENTITY_COUNT_SQL =
|
||||
"SELECT COUNT(*) AS cnt FROM Pos.vDeletedEntity "
|
||||
"WHERE CONVERT(BIGINT, vDeletedEntity.bLastChanged) > ?";
|
||||
|
||||
static const char* MAX_ORDER_ID_SQL =
|
||||
"SELECT ISNULL(MAX(kPosAuftrag), 0) AS cnt FROM Pos.tAuftragMapping "
|
||||
"WHERE kShopSubShop = ?";
|
||||
|
||||
inline int64_t get_category_count(int64_t root_cat, int k_shop, int64_t cursor) {
|
||||
return get_pool().execute_scalar(CATEGORY_COUNT_SQL,
|
||||
{{ParamType::BigInt,"",root_cat},{ParamType::BigInt,"",k_shop},
|
||||
{ParamType::BigInt,"",k_shop},{ParamType::BigInt,"",cursor}});
|
||||
{{ParamType::BigInt,"",root_cat},{ParamType::Int,"",k_shop},
|
||||
{ParamType::Int,"",k_shop},{ParamType::BigInt,"",cursor}});
|
||||
}
|
||||
inline int64_t get_product_count(int64_t root_cat, int k_shop, int64_t cursor) {
|
||||
return get_pool().execute_scalar(PRODUCT_COUNT_SQL,
|
||||
{{ParamType::BigInt,"",root_cat},{ParamType::BigInt,"",k_shop},
|
||||
{ParamType::BigInt,"",k_shop},{ParamType::BigInt,"",cursor},
|
||||
{ParamType::BigInt,"",k_shop},{ParamType::BigInt,"",cursor}});
|
||||
{{ParamType::BigInt,"",root_cat},{ParamType::Int,"",k_shop},
|
||||
{ParamType::Int,"",k_shop},{ParamType::BigInt,"",cursor},
|
||||
{ParamType::Int,"",k_shop},{ParamType::BigInt,"",cursor}});
|
||||
}
|
||||
inline int64_t get_composite_count(int k_shop, int64_t cursor) {
|
||||
return get_pool().execute_scalar(COMPOSITE_PRODUCT_COUNT_SQL,
|
||||
{{ParamType::BigInt,"",k_shop},{ParamType::BigInt,"",k_shop},
|
||||
{{ParamType::Int,"",k_shop},{ParamType::Int,"",k_shop},
|
||||
{ParamType::BigInt,"",cursor}});
|
||||
}
|
||||
inline int64_t get_deleted_count(int64_t cursor) {
|
||||
return get_pool().execute_scalar(DELETED_ENTITY_COUNT_SQL,
|
||||
{{ParamType::BigInt,"",cursor}});
|
||||
}
|
||||
inline int64_t get_max_order_id_count(int k_shop_subshop) {
|
||||
if (k_shop_subshop <= 0) return 0;
|
||||
return get_pool().execute_scalar(MAX_ORDER_ID_SQL,
|
||||
{{ParamType::Int,"",k_shop_subshop}});
|
||||
}
|
||||
|
||||
729
jtlsrv-cpp/src/queries/create_order.hpp
Normal file
729
jtlsrv-cpp/src/queries/create_order.hpp
Normal file
@@ -0,0 +1,729 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "delivery.hpp"
|
||||
#include "shop.hpp"
|
||||
#include "../db/pool.hpp"
|
||||
#include "../config.hpp"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
namespace order {
|
||||
|
||||
struct Defaults;
|
||||
|
||||
namespace {
|
||||
|
||||
struct Config {
|
||||
int kBenutzer = 1;
|
||||
int kFirmaHistory = 0;
|
||||
int kSprache = 1;
|
||||
int kPlattform = 7;
|
||||
int kVersandArt = 0;
|
||||
int kKundengruppe = 0;
|
||||
int orderNumberSequence = 3;
|
||||
int customerNumberSequence = 6;
|
||||
};
|
||||
|
||||
Config g_config;
|
||||
std::unique_ptr<Defaults> g_resolved_defaults;
|
||||
std::map<std::string, nlohmann::json> g_zahlungsart_cache;
|
||||
|
||||
} // namespace
|
||||
|
||||
struct Defaults {
|
||||
int kFirmaHistory = 1;
|
||||
int kVersandArt = 1;
|
||||
int kKundengruppe = 1;
|
||||
int kPlattform = 1;
|
||||
};
|
||||
|
||||
inline void load_config() {
|
||||
g_config.kBenutzer = config::get_int("JTL_KBENUTZER", 1);
|
||||
g_config.kFirmaHistory = config::get_int("JTL_KFIRMAHISTORY", 0);
|
||||
g_config.kSprache = config::get_int("JTL_KSPRACHE", 1);
|
||||
g_config.kPlattform = config::get_int("JTL_KPLATTFORM", 7);
|
||||
g_config.kVersandArt = config::get_int("JTL_KVERSANDART", 0);
|
||||
g_config.kKundengruppe = config::get_int("JTL_KKUNDENGRUPPE", 0);
|
||||
g_config.orderNumberSequence = config::get_int("JTL_ORDER_NUMBER_SEQUENCE", 3);
|
||||
g_config.customerNumberSequence = config::get_int("JTL_CUSTOMER_NUMBER_SEQUENCE", 6);
|
||||
}
|
||||
|
||||
inline const Defaults& get_defaults(OdbcPool::Connection* c) {
|
||||
if (g_resolved_defaults) return *g_resolved_defaults;
|
||||
|
||||
Defaults d;
|
||||
d.kFirmaHistory = g_config.kFirmaHistory;
|
||||
d.kVersandArt = g_config.kVersandArt;
|
||||
d.kKundengruppe = g_config.kKundengruppe;
|
||||
d.kPlattform = g_config.kPlattform;
|
||||
|
||||
const char* sql =
|
||||
"SELECT"
|
||||
" (SELECT MAX(kFirmaHistory) FROM dbo.tFirmaHistory) AS kFirmaHistory,"
|
||||
" (SELECT MIN(kVersandArt) FROM dbo.tVersandArt) AS kVersandArt,"
|
||||
" (SELECT TOP 1 kKundenGruppe FROM dbo.tKundenGruppe ORDER BY nStandard DESC, kKundenGruppe) AS kKundengruppe,"
|
||||
" (SELECT CASE WHEN EXISTS (SELECT 1 FROM dbo.tPlattform WHERE nPlattform = ?) THEN ? ELSE 1 END) AS kPlattform";
|
||||
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::Int, "", g_config.kPlattform},
|
||||
{ParamType::Int, "", g_config.kPlattform}
|
||||
};
|
||||
ResultSet rs;
|
||||
if (get_pool().execute(c, sql, ps, rs) && !rs.empty() && !rs[0].empty()) {
|
||||
if (!rs[0][0].str.empty()) d.kFirmaHistory = std::stoi(rs[0][0].str);
|
||||
if (!rs[0][1].str.empty()) d.kVersandArt = std::stoi(rs[0][1].str);
|
||||
if (!rs[0][2].str.empty()) d.kKundengruppe = std::stoi(rs[0][2].str);
|
||||
if (!rs[0][3].str.empty()) d.kPlattform = std::stoi(rs[0][3].str);
|
||||
}
|
||||
|
||||
// Apply environment overrides
|
||||
if (g_config.kFirmaHistory > 0) d.kFirmaHistory = g_config.kFirmaHistory;
|
||||
if (g_config.kVersandArt > 0) d.kVersandArt = g_config.kVersandArt;
|
||||
if (g_config.kKundengruppe > 0) d.kKundengruppe = g_config.kKundengruppe;
|
||||
|
||||
g_resolved_defaults = std::make_unique<Defaults>(d);
|
||||
return *g_resolved_defaults;
|
||||
}
|
||||
|
||||
inline double to_number(const nlohmann::json& value, double fallback = 0) {
|
||||
if (value.is_null()) return fallback;
|
||||
if (value.is_number()) return value.get<double>();
|
||||
if (value.is_string()) {
|
||||
std::string s = value.get<std::string>();
|
||||
if (s.empty()) return fallback;
|
||||
std::replace(s.begin(), s.end(), ',', '.');
|
||||
try { return std::stod(s); } catch (...) {}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
inline int to_int(const nlohmann::json& value, int fallback = 0) {
|
||||
if (value.is_null()) return fallback;
|
||||
if (value.is_number_integer()) return value.get<int>();
|
||||
if (value.is_number_unsigned()) return static_cast<int>(value.get<unsigned>());
|
||||
if (value.is_number_float()) return static_cast<int>(value.get<double>());
|
||||
if (value.is_boolean()) return value.get<bool>() ? 1 : 0;
|
||||
if (value.is_string()) {
|
||||
std::string s = value.get<std::string>();
|
||||
if (s.empty()) return fallback;
|
||||
try { return std::stoi(s); } catch (...) {}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
inline int json_int(const nlohmann::json& obj, const char* key, int fallback = 0) {
|
||||
auto it = obj.find(key);
|
||||
if (it == obj.end()) return fallback;
|
||||
return to_int(*it, fallback);
|
||||
}
|
||||
|
||||
inline int steuerklasse_for_vat(double vat) {
|
||||
if (vat >= 15) return 1;
|
||||
if (vat > 0) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
inline int iso_week(const std::tm& tm) {
|
||||
// JTL placeholders: J = Jahr, M = Monat, T = Tag, K = Kalenderwoche.
|
||||
// ISO week date
|
||||
std::tm t0 = tm;
|
||||
int yday = t0.tm_yday;
|
||||
int wday = t0.tm_wday;
|
||||
if (wday == 0) wday = 7;
|
||||
int week = (yday - wday + 10) / 7;
|
||||
if (week < 1) week = 1;
|
||||
if (week > 53) week = 53;
|
||||
return week;
|
||||
}
|
||||
|
||||
inline std::string format_number_placeholders(const std::string& tpl, const std::tm& tm) {
|
||||
if (tpl.empty()) return "";
|
||||
std::string r = tpl;
|
||||
auto replace = [&r](const std::string& from, const std::string& to) {
|
||||
size_t start = 0;
|
||||
while ((start = r.find(from, start)) != std::string::npos) {
|
||||
r.replace(start, from.size(), to);
|
||||
start += to.size();
|
||||
}
|
||||
};
|
||||
std::ostringstream mm, dd, kk;
|
||||
mm << std::setw(2) << std::setfill('0') << (tm.tm_mon + 1);
|
||||
dd << std::setw(2) << std::setfill('0') << tm.tm_mday;
|
||||
kk << std::setw(2) << std::setfill('0') << iso_week(tm);
|
||||
replace("<J>", std::to_string(tm.tm_year + 1900));
|
||||
replace("<M>", mm.str());
|
||||
replace("<T>", dd.str());
|
||||
replace("<K>", kk.str());
|
||||
return r;
|
||||
}
|
||||
|
||||
inline std::tm parse_order_date(const std::string& s) {
|
||||
std::tm tm{};
|
||||
std::string iso = s;
|
||||
std::replace(iso.begin(), iso.end(), ' ', 'T');
|
||||
std::istringstream ss(iso);
|
||||
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
|
||||
if (ss.fail()) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
std::time_t t = std::chrono::system_clock::to_time_t(now);
|
||||
localtime_r(&t, &tm);
|
||||
}
|
||||
return tm;
|
||||
}
|
||||
|
||||
inline std::string order_date_sql(const std::tm& tm) {
|
||||
char buf[32];
|
||||
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
|
||||
return buf;
|
||||
}
|
||||
|
||||
inline std::string country_name(const std::string& iso) {
|
||||
static const std::map<std::string, std::string> names = {
|
||||
{"DE", "Deutschland"},
|
||||
{"AT", "Oesterreich"},
|
||||
{"CH", "Schweiz"}
|
||||
};
|
||||
auto it = names.find(iso);
|
||||
return it != names.end() ? it->second : iso;
|
||||
}
|
||||
|
||||
inline std::string next_number_from_sequence(OdbcPool::Connection* c, int kLaufendeNummer,
|
||||
const std::tm& date) {
|
||||
const char* sql =
|
||||
"DECLARE @n INT, @cPrefix NVARCHAR(50), @cSuffix NVARCHAR(50);"
|
||||
"UPDATE dbo.tLaufendeNummern"
|
||||
" SET @n = nNummer = nNummer + 1, @cPrefix = cPrefix, @cSuffix = cSuffix"
|
||||
" WHERE kLaufendeNummer = ?;"
|
||||
"SELECT @n AS nNummer, @cPrefix AS cPrefix, @cSuffix AS cSuffix";
|
||||
|
||||
std::vector<Param> ps = {{ParamType::Int, "", kLaufendeNummer}};
|
||||
ResultSet rs;
|
||||
if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].size() < 3) {
|
||||
throw std::runtime_error("dbo.tLaufendeNummern has no row " + std::to_string(kLaufendeNummer));
|
||||
}
|
||||
int n = std::stoi(rs[0][0].str);
|
||||
std::string prefix = format_number_placeholders(rs[0][1].str, date);
|
||||
std::string suffix = format_number_placeholders(rs[0][2].str, date);
|
||||
return prefix + std::to_string(n) + suffix;
|
||||
}
|
||||
|
||||
inline int allocate_pk(OdbcPool::Connection* c, const std::string& tableName) {
|
||||
const char* sql =
|
||||
"DECLARE @pk INT;"
|
||||
"UPDATE dbo.tpk SET @pk = nummer, nummer = nummer + 1, dChanged = GETDATE() WHERE cName = ?;"
|
||||
"SELECT @pk AS pk";
|
||||
|
||||
std::vector<Param> ps = {{ParamType::NVarChar, tableName, 0}};
|
||||
ResultSet rs;
|
||||
if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].empty()) {
|
||||
throw std::runtime_error("dbo.tpk has no row for table '" + tableName + "'");
|
||||
}
|
||||
return std::stoi(rs[0][0].str);
|
||||
}
|
||||
|
||||
inline nlohmann::json resolve_zahlungsart(OdbcPool::Connection* c, const std::string& name) {
|
||||
std::string key = name.empty() ? "bar" : name;
|
||||
std::transform(key.begin(), key.end(), key.begin(), [](unsigned char ch) { return std::tolower(ch); });
|
||||
auto it = g_zahlungsart_cache.find(key);
|
||||
if (it != g_zahlungsart_cache.end()) return it->second;
|
||||
|
||||
std::vector<Param> ps = {{ParamType::NVarChar, key, 0}};
|
||||
ResultSet rs;
|
||||
get_pool().execute(c, "SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE cName = ?", ps, rs);
|
||||
|
||||
if (!rs.empty() && !rs[0].empty()) {
|
||||
nlohmann::json z = {{"kZahlungsart", std::stoi(rs[0][0].str)}, {"cName", rs[0][1].str}};
|
||||
g_zahlungsart_cache[key] = z;
|
||||
return z;
|
||||
}
|
||||
|
||||
int kZahlungsart = allocate_pk(c, "tZahlungsart");
|
||||
const char* insert =
|
||||
"INSERT INTO dbo.tZahlungsart"
|
||||
" (kZahlungsart, cName, cPrtString, nLastschrift, cPrtStringVor, cPaymentOption, cKonto,"
|
||||
" nAusliefernVorZahlung, nPrioritaet, nMahnwesenAktiv, fSkontoWert, nSkontoZeitraum,"
|
||||
" nMatchingOptionen, nIstStandard, nAktiv)"
|
||||
" VALUES (?, ?, '', 0, '', '', '', 0, 0, 0, 0, 0, 0, 0, 1)";
|
||||
std::vector<Param> ps2 = {
|
||||
{ParamType::Int, "", kZahlungsart},
|
||||
{ParamType::NVarChar, key, 0}
|
||||
};
|
||||
get_pool().execute(c, insert, ps2, rs);
|
||||
|
||||
nlohmann::json z = {{"kZahlungsart", kZahlungsart}, {"cName", key}};
|
||||
g_zahlungsart_cache[key] = z;
|
||||
return z;
|
||||
}
|
||||
|
||||
inline std::string next_customer_number(OdbcPool::Connection* c) {
|
||||
std::tm date;
|
||||
{
|
||||
auto now = std::chrono::system_clock::now();
|
||||
std::time_t t = std::chrono::system_clock::to_time_t(now);
|
||||
localtime_r(&t, &date);
|
||||
}
|
||||
return next_number_from_sequence(c, g_config.customerNumberSequence, date);
|
||||
}
|
||||
|
||||
inline std::pair<int,int> lookup_kassenkunde(OdbcPool::Connection* c, const Defaults& defaults) {
|
||||
ResultSet rs;
|
||||
get_pool().execute(c,
|
||||
"SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKassenKunde = 'Y' ORDER BY kKunde", rs);
|
||||
if (!rs.empty() && !rs[0].empty()) {
|
||||
int kKunde = std::stoi(rs[0][0].str);
|
||||
int grp = defaults.kKundengruppe;
|
||||
if (rs[0].size() > 1 && !rs[0][1].str.empty()) grp = std::stoi(rs[0][1].str);
|
||||
return {kKunde, grp};
|
||||
}
|
||||
return {0, defaults.kKundengruppe};
|
||||
}
|
||||
|
||||
inline bool is_walk_in_order(const nlohmann::json& order) {
|
||||
std::string customer_number = order.value("customerNumber", "");
|
||||
if (customer_number.empty() || customer_number == "0") return true;
|
||||
|
||||
const auto billing = order.value("billingAddress", nlohmann::json::object());
|
||||
return billing.value("lastName", "") == "Laufkunde"
|
||||
&& billing.value("firstName", "").empty();
|
||||
}
|
||||
|
||||
inline std::pair<int,int> create_customer(OdbcPool::Connection* c, const std::string& customer_number,
|
||||
const nlohmann::json& address, const Defaults& defaults) {
|
||||
const nlohmann::json a = address.is_null() ? nlohmann::json::object() : address;
|
||||
std::string iso = a.value("countryIso", "DE");
|
||||
std::transform(iso.begin(), iso.end(), iso.begin(), [](unsigned char ch) { return std::toupper(ch); });
|
||||
int kKundengruppe = json_int(a, "customerGroupId", 0);
|
||||
if (kKundengruppe <= 0) kKundengruppe = defaults.kKundengruppe;
|
||||
|
||||
const char* sql =
|
||||
"DECLARE @returnValue INT;"
|
||||
"DECLARE @p1 dbo.TYPE_spkundeInsert;"
|
||||
"INSERT INTO @p1"
|
||||
" (kInetKunde, kKundenKategorie, cKundenNr, cFirma, cAnrede, cTitel, cVorname, cName,"
|
||||
" cStrasse, cPLZ, cOrt, cLand, cTel, cFax, cEMail, dErstellt, cMobil, fRabatt, cUSTID, cNewsletter,"
|
||||
" cZusatz, cEbayName, kBuyer, cAdressZusatz, cGeburtstag, cWWW, cSperre, cPostID, kKundenGruppe,"
|
||||
" nZahlungsziel, kSprache, cISO, cBundesland, cHerkunft, cKassenKunde, cHRNr, kZahlungsart,"
|
||||
" nDebitorennr, cSteuerNr, nKreditlimit, kKundenDrucktext, nMahnstopp, nMahnrhythmus, kFirma,"
|
||||
" fProvision, nVertreter, fSkonto, nSkontoInTagen)"
|
||||
" VALUES"
|
||||
" (0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, GETDATE(), ?, ?, NULL, 'N',"
|
||||
" N'', N'', 0, ?, ?, N'', 'N', NULL, ?,"
|
||||
" 0, ?, ?, ?, ?, ?, N'', 0,"
|
||||
" ?, N'', 0, 0, 0, 0, 0,"
|
||||
" NULL, 0, 0, 0);"
|
||||
"EXEC @returnValue = Kunde.spKundeInsert @daten = @p1;"
|
||||
"SELECT @returnValue AS kKunde";
|
||||
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::NVarChar, customer_number, 0},
|
||||
{ParamType::NVarChar, a.value("company", ""), 0},
|
||||
{ParamType::NVarChar, a.value("salutation", ""), 0},
|
||||
{ParamType::NVarChar, a.value("title", ""), 0},
|
||||
{ParamType::NVarChar, a.value("firstName", ""), 0},
|
||||
{ParamType::NVarChar, a.value("lastName", "Laufkunde"), 0},
|
||||
{ParamType::NVarChar, a.value("street", "-"), 0},
|
||||
{ParamType::NVarChar, a.value("zipCode", ""), 0},
|
||||
{ParamType::NVarChar, a.value("city", "-"), 0},
|
||||
{ParamType::NVarChar, country_name(iso), 0},
|
||||
{ParamType::NVarChar, a.value("phone", ""), 0},
|
||||
{ParamType::NVarChar, a.value("fax", ""), 0},
|
||||
{ParamType::NVarChar, a.value("email", ""), 0},
|
||||
{ParamType::NVarChar, a.value("mobile", ""), 0},
|
||||
{ParamType::Double, "", 0, to_number(a["discount"], 0)},
|
||||
{ParamType::NVarChar, a.value("addressAddition", ""), 0},
|
||||
{ParamType::NVarChar, a.value("birthday", ""), 0},
|
||||
{ParamType::Int, "", kKundengruppe},
|
||||
{ParamType::Int, "", g_config.kSprache},
|
||||
{ParamType::NVarChar, iso, 0},
|
||||
{ParamType::NVarChar, a.value("state", ""), 0},
|
||||
{ParamType::NVarChar, std::string("Kasse"), 0},
|
||||
{ParamType::NVarChar, std::string("Y"), 0},
|
||||
{ParamType::Int, "", json_int(a, "debtorNumber", 0)}
|
||||
};
|
||||
ResultSet rs;
|
||||
if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].empty()) {
|
||||
throw std::runtime_error("Kunde.spKundeInsert failed for '" + customer_number + "'");
|
||||
}
|
||||
int kKunde = std::stoi(rs[0][0].str);
|
||||
if (kKunde <= 0) throw std::runtime_error("Kunde.spKundeInsert returned invalid kKunde");
|
||||
return {kKunde, kKundengruppe};
|
||||
}
|
||||
|
||||
inline std::pair<int,int> resolve_customer(OdbcPool::Connection* c, const nlohmann::json& order,
|
||||
const Defaults& defaults) {
|
||||
if (is_walk_in_order(order)) {
|
||||
auto [kKunde, grp] = lookup_kassenkunde(c, defaults);
|
||||
if (kKunde > 0) return {kKunde, grp};
|
||||
return create_customer(c, next_customer_number(c),
|
||||
order.value("billingAddress", nlohmann::json::object()), defaults);
|
||||
}
|
||||
|
||||
std::string customer_number = order.value("customerNumber", "");
|
||||
std::vector<Param> ps = {{ParamType::NVarChar, customer_number, 0}};
|
||||
ResultSet rs;
|
||||
get_pool().execute(c, "SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKundenNr = ?", ps, rs);
|
||||
if (!rs.empty() && !rs[0].empty()) {
|
||||
int kKunde = std::stoi(rs[0][0].str);
|
||||
int grp = defaults.kKundengruppe;
|
||||
if (rs[0].size() > 1 && !rs[0][1].str.empty()) grp = std::stoi(rs[0][1].str);
|
||||
return {kKunde, grp};
|
||||
}
|
||||
return create_customer(c, customer_number, order.value("billingAddress", nlohmann::json::object()), defaults);
|
||||
}
|
||||
|
||||
inline std::string next_order_number(OdbcPool::Connection* c, const std::tm& date) {
|
||||
return next_number_from_sequence(c, g_config.orderNumberSequence, date);
|
||||
}
|
||||
|
||||
inline void insert_order_address(OdbcPool::Connection* c, int kAuftrag, int kKunde,
|
||||
const nlohmann::json& address, int nTyp) {
|
||||
const nlohmann::json a = address.is_null() ? nlohmann::json::object() : address;
|
||||
std::string iso = a.value("countryIso", "DE");
|
||||
std::transform(iso.begin(), iso.end(), iso.begin(), [](unsigned char ch) { return std::toupper(ch); });
|
||||
|
||||
const char* sql =
|
||||
"INSERT INTO Verkauf.tAuftragAdresse"
|
||||
" (kAuftrag, kKunde, cFirma, cAnrede, cTitel, cVorname, cName, cStrasse, cPLZ, cOrt, cLand,"
|
||||
" cTel, cZusatz, cAdressZusatz, cMobil, cMail, cFax, cBundesland, cISO, nTyp, nZolldokumenteErforderlich)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)";
|
||||
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::Int, "", kAuftrag},
|
||||
{ParamType::Int, "", kKunde},
|
||||
{ParamType::NVarChar, a.value("company", ""), 0},
|
||||
{ParamType::NVarChar, a.value("salutation", ""), 0},
|
||||
{ParamType::NVarChar, a.value("title", ""), 0},
|
||||
{ParamType::NVarChar, a.value("firstName", ""), 0},
|
||||
{ParamType::NVarChar, a.value("lastName", "-"), 0},
|
||||
{ParamType::NVarChar, a.value("street", "-"), 0},
|
||||
{ParamType::NVarChar, a.value("zipCode", ""), 0},
|
||||
{ParamType::NVarChar, a.value("city", "-"), 0},
|
||||
{ParamType::NVarChar, country_name(iso), 0},
|
||||
{ParamType::NVarChar, a.value("phone", ""), 0},
|
||||
{ParamType::NVarChar, a.value("extraAddressLine", ""), 0},
|
||||
{ParamType::NVarChar, a.value("addressAddition", ""), 0},
|
||||
{ParamType::NVarChar, a.value("mobile", ""), 0},
|
||||
{ParamType::NVarChar, a.value("email", ""), 0},
|
||||
{ParamType::NVarChar, a.value("fax", ""), 0},
|
||||
{ParamType::NVarChar, a.value("state", ""), 0},
|
||||
{ParamType::NVarChar, iso, 0},
|
||||
{ParamType::Int, "", nTyp}
|
||||
};
|
||||
ResultSet rs;
|
||||
get_pool().execute(c, sql, ps, rs);
|
||||
}
|
||||
|
||||
inline int insert_order_item(OdbcPool::Connection* c, int kAuftrag, const nlohmann::json& item) {
|
||||
double vat = to_number(item["vat"], 19);
|
||||
double quantity = to_number(item["quantity"], 1);
|
||||
double price_gross = to_number(item["priceGross"], 0);
|
||||
double price_net = to_number(item["priceNet"], price_gross / (1 + vat / 100));
|
||||
double discount = to_number(item["discountPercent"], 0);
|
||||
int kSteuerklasse = steuerklasse_for_vat(vat);
|
||||
std::string sku = item.value("sku", "");
|
||||
|
||||
int kArtikel = 0;
|
||||
bool has_artikel = false;
|
||||
if (!sku.empty()) {
|
||||
std::vector<Param> ps = {{ParamType::NVarChar, sku, 0}};
|
||||
ResultSet rs;
|
||||
get_pool().execute(c, "SELECT TOP 1 kArtikel FROM dbo.tArtikel WHERE cArtNr = ?", ps, rs);
|
||||
if (!rs.empty() && !rs[0].empty()) {
|
||||
kArtikel = std::stoi(rs[0][0].str);
|
||||
has_artikel = true;
|
||||
}
|
||||
}
|
||||
|
||||
const char* sql =
|
||||
"DECLARE @t TABLE ([kAuftragPosition] INT);"
|
||||
"INSERT INTO Verkauf.tAuftragPosition"
|
||||
" (kArtikel, kAuftrag, cArtNr, nReserviert, cName, cHinweis, fAnzahl, fVkNetto, fMwSt,"
|
||||
" cNameStandard, kSteuerklasse, nType, cEinheit, fFaktor, kSteuerschluessel, fRabatt)"
|
||||
" OUTPUT inserted.kAuftragPosition INTO @t"
|
||||
" VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?,"
|
||||
" ?, ?, ?, ?, 1.0, 3, ?);"
|
||||
"SELECT kAuftragPosition FROM @t";
|
||||
|
||||
Param cartnr{ParamType::NVarChar, has_artikel ? sku : std::string(), 0, 0.0, !has_artikel};
|
||||
|
||||
Param pk_art{ParamType::Int, "", kArtikel, 0.0, !has_artikel};
|
||||
|
||||
std::string name = item.value("name", sku.empty() ? std::string("Position") : sku);
|
||||
std::vector<Param> ps = {
|
||||
pk_art,
|
||||
{ParamType::Int, "", kAuftrag},
|
||||
cartnr,
|
||||
{ParamType::NVarChar, name, 0},
|
||||
{ParamType::NVarChar, item.value("note", ""), 0},
|
||||
{ParamType::Double, "", 0, quantity},
|
||||
{ParamType::Double, "", 0, price_net},
|
||||
{ParamType::Double, "", 0, vat},
|
||||
{ParamType::NVarChar, name, 0},
|
||||
{ParamType::Int, "", kSteuerklasse},
|
||||
{ParamType::Int, "", has_artikel ? 1 : 0},
|
||||
{ParamType::NVarChar, item.value("unit", ""), 0},
|
||||
{ParamType::Double, "", 0, discount}
|
||||
};
|
||||
ResultSet rs;
|
||||
if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].empty()) return 0;
|
||||
return std::stoi(rs[0][0].str);
|
||||
}
|
||||
|
||||
inline int parse_pos_auftrag_id(const std::string& external_id) {
|
||||
if (external_id.empty()) return 0;
|
||||
try { return std::stoi(external_id); } catch (...) { return 0; }
|
||||
}
|
||||
|
||||
// PosOrderCreationService.CheckIfOrderExists — already-imported POS order.
|
||||
inline std::optional<std::pair<int, std::string>> find_existing_pos_order(
|
||||
OdbcPool::Connection* c, int kPosAuftrag) {
|
||||
int kShopSubShop = get_active_shop_subshop_id();
|
||||
if (kPosAuftrag <= 0 || kShopSubShop <= 0) return std::nullopt;
|
||||
|
||||
const char* sql =
|
||||
"SELECT TOP 1 m.kAuftrag, a.cAuftragsNr FROM Pos.tAuftragMapping m "
|
||||
"LEFT JOIN Verkauf.tAuftrag a ON a.kAuftrag = m.kAuftrag "
|
||||
"WHERE m.kPosAuftrag = ? AND m.kShopSubShop = ? AND m.kAuftrag IS NOT NULL "
|
||||
"ORDER BY m.kAuftrag DESC";
|
||||
|
||||
ResultSet rs;
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::Int, "", kPosAuftrag},
|
||||
{ParamType::Int, "", kShopSubShop},
|
||||
};
|
||||
if (!get_pool().execute(c, sql, ps, rs)
|
||||
|| rs.empty() || rs[0].empty() || rs[0][0].type == CellType::Null) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int kAuftrag = std::stoi(rs[0][0].str);
|
||||
if (kAuftrag <= 0) return std::nullopt;
|
||||
std::string order_number = rs[0].size() > 1 ? rs[0][1].str : "";
|
||||
return std::make_pair(kAuftrag, order_number);
|
||||
}
|
||||
|
||||
inline void upsert_pos_order_mapping(OdbcPool::Connection* c, int kAuftrag, int kPosAuftrag) {
|
||||
int kShopSubShop = get_active_shop_subshop_id();
|
||||
if (kPosAuftrag <= 0 || kShopSubShop <= 0) return;
|
||||
|
||||
const char* sql =
|
||||
"MERGE INTO Pos.tAuftragMapping WITH (HOLDLOCK) AS Target "
|
||||
"USING (SELECT ? AS kAuftrag, ? AS kPosAuftrag, ? AS kShopSubShop) AS Source "
|
||||
"ON Target.kPosAuftrag = Source.kPosAuftrag AND Target.kShopSubShop = Source.kShopSubShop "
|
||||
"WHEN MATCHED THEN UPDATE SET Target.kAuftrag = Source.kAuftrag "
|
||||
"WHEN NOT MATCHED BY TARGET THEN "
|
||||
"INSERT (kAuftrag, kPosAuftrag, kShopSubShop) "
|
||||
"VALUES (Source.kAuftrag, Source.kPosAuftrag, Source.kShopSubShop)";
|
||||
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::Int, "", kAuftrag},
|
||||
{ParamType::Int, "", kPosAuftrag},
|
||||
{ParamType::Int, "", kShopSubShop}
|
||||
};
|
||||
ResultSet rs;
|
||||
get_pool().execute(c, sql, ps, rs);
|
||||
}
|
||||
|
||||
inline void insert_pos_order_position_mapping(OdbcPool::Connection* c, int kAuftragPosition,
|
||||
const std::string& external_id) {
|
||||
int kPosAuftragPosition = 0;
|
||||
try { kPosAuftragPosition = std::stoi(external_id); } catch (...) { return; }
|
||||
int kShopSubShop = get_active_shop_subshop_id();
|
||||
if (kAuftragPosition <= 0 || kPosAuftragPosition <= 0 || kShopSubShop <= 0) return;
|
||||
|
||||
const char* sql = "INSERT INTO Pos.tAuftragPositionMapping (kAuftragPosition, kPosAuftragPosition, kShopSubShop) VALUES (?, ?, ?)";
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::Int, "", kAuftragPosition},
|
||||
{ParamType::Int, "", kPosAuftragPosition},
|
||||
{ParamType::Int, "", kShopSubShop}
|
||||
};
|
||||
ResultSet rs;
|
||||
get_pool().execute(c, sql, ps, rs);
|
||||
}
|
||||
|
||||
inline bool is_order_delivered(const nlohmann::json& order) {
|
||||
auto it = order.find("settings");
|
||||
if (it != order.end() && !it->is_null()) {
|
||||
auto del = it->find("deliver");
|
||||
if (del != it->end()) {
|
||||
if (del->is_boolean()) return del->get<bool>();
|
||||
if (del->is_number()) return del->get<int>() != 0;
|
||||
if (del->is_string()) {
|
||||
std::string s = *del;
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char ch) { return std::tolower(ch); });
|
||||
return s == "true" || s == "1";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void insert_payment(OdbcPool::Connection* c, int kAuftrag, const nlohmann::json& payment,
|
||||
const nlohmann::json& order, const std::tm& order_date_tm) {
|
||||
std::string payment_name = payment.value("paymentMethodName", "");
|
||||
if (payment_name.empty()) payment_name = order.value("paymentMethodName", "Bar");
|
||||
nlohmann::json zahlungsart = resolve_zahlungsart(c, payment_name);
|
||||
int kZahlung = allocate_pk(c, "tZahlung");
|
||||
|
||||
const char* sql =
|
||||
"INSERT INTO dbo.tZahlung"
|
||||
" (kZahlung, cName, dDatum, fBetrag, kBestellung, kBenutzer, nAnzahlung, cHinweis, kZahlungsart,"
|
||||
" nKeinExport, cExternalTransactionId, nZuweisungstyp, nZahlungstyp, cZuweisungsinfo, nZuweisungswertung)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, 0, '', ?,"
|
||||
" 0, ?, 0, 0, '', 0)";
|
||||
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::Int, "", kZahlung},
|
||||
{ParamType::NVarChar, zahlungsart.value("cName", ""), 0},
|
||||
{ParamType::NVarChar, order_date_sql(order_date_tm), 0},
|
||||
{ParamType::Double, "", 0, to_number(payment["amount"], 0)},
|
||||
{ParamType::Int, "", kAuftrag},
|
||||
{ParamType::Int, "", g_config.kBenutzer},
|
||||
{ParamType::Int, "", json_int(zahlungsart, "kZahlungsart", 0)},
|
||||
{ParamType::NVarChar, order.value("externalOrderNumber", ""), 0}
|
||||
};
|
||||
ResultSet rs;
|
||||
get_pool().execute(c, sql, ps, rs);
|
||||
}
|
||||
|
||||
inline nlohmann::json create_order(const nlohmann::json& order) {
|
||||
load_config();
|
||||
|
||||
auto guard = get_pool().checkout();
|
||||
if (!guard) throw std::runtime_error("no ODBC connection available");
|
||||
|
||||
auto* c = guard.get();
|
||||
|
||||
int kPosAuftrag = parse_pos_auftrag_id(order.value("externalId", ""));
|
||||
if (kPosAuftrag > 0) {
|
||||
if (auto existing = find_existing_pos_order(c, kPosAuftrag)) {
|
||||
return {
|
||||
{"orderId", std::to_string(existing->first)},
|
||||
{"orderNumber", existing->second}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const Defaults& defaults = get_defaults(c);
|
||||
|
||||
if (!get_pool().begin(c)) throw std::runtime_error("failed to begin transaction");
|
||||
|
||||
try {
|
||||
g_zahlungsart_cache.clear();
|
||||
|
||||
std::string creation_date = order.value("creationDate", "");
|
||||
std::tm order_date_tm = creation_date.empty()
|
||||
? []() {
|
||||
std::tm tm{};
|
||||
auto now = std::chrono::system_clock::now();
|
||||
std::time_t t = std::chrono::system_clock::to_time_t(now);
|
||||
localtime_r(&t, &tm);
|
||||
return tm;
|
||||
}()
|
||||
: parse_order_date(creation_date);
|
||||
|
||||
auto [kKunde, kKundengruppe] = resolve_customer(c, order, defaults);
|
||||
nlohmann::json zahlungsart = resolve_zahlungsart(c, order.value("paymentMethodName", "Bar"));
|
||||
std::string cAuftragsNr = next_order_number(c, order_date_tm);
|
||||
|
||||
int active_shop = get_active_shop_id();
|
||||
|
||||
const char* insert_auftrag =
|
||||
"DECLARE @t TABLE ([kAuftrag] INT);"
|
||||
"INSERT INTO Verkauf.tAuftrag"
|
||||
" (cAuftragsNr, dErstellt, nKomplettAusgeliefert, kBenutzer, kKunde, kBenutzerErstellt, nType, fFaktor,"
|
||||
" kFirmaHistory, kSprache, cVersandlandWaehrung, fVersandlandWaehrungFaktor, fFinanzierungskosten,"
|
||||
" cWaehrung, kPlattform, kShop, cKundenNr, cVersandlandISO, kVersandArt, kZahlungsart, kKundengruppe,"
|
||||
" cExterneAuftragsnummer)"
|
||||
" OUTPUT inserted.kAuftrag INTO @t"
|
||||
" VALUES (?, ?, 0, ?, ?, ?, 1, 1.0,"
|
||||
" ?, ?, ?, 1.0, 0.0,"
|
||||
" ?, ?, ?, ?, ?, ?, ?, ?,"
|
||||
" ?);"
|
||||
"SELECT kAuftrag FROM @t";
|
||||
|
||||
std::string shipping_iso = order.value("shippingAddress", nlohmann::json::object()).value("countryIso", "DE");
|
||||
std::transform(shipping_iso.begin(), shipping_iso.end(), shipping_iso.begin(),
|
||||
[](unsigned char ch) { return std::toupper(ch); });
|
||||
|
||||
std::string currency_iso = order.value("currencyIso", "EUR");
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::NVarChar, cAuftragsNr, 0},
|
||||
{ParamType::NVarChar, order_date_sql(order_date_tm), 0},
|
||||
{ParamType::Int, "", g_config.kBenutzer},
|
||||
{ParamType::Int, "", kKunde},
|
||||
{ParamType::Int, "", g_config.kBenutzer},
|
||||
{ParamType::Int, "", defaults.kFirmaHistory},
|
||||
{ParamType::Int, "", g_config.kSprache},
|
||||
{ParamType::NVarChar, currency_iso, 0},
|
||||
{ParamType::NVarChar, currency_iso, 0},
|
||||
{ParamType::Int, "", defaults.kPlattform},
|
||||
{ParamType::Int, "", active_shop == 0 ? -1 : active_shop},
|
||||
{ParamType::NVarChar, order.value("customerNumber", ""), 0},
|
||||
{ParamType::NVarChar, shipping_iso, 0},
|
||||
{ParamType::Int, "", defaults.kVersandArt},
|
||||
{ParamType::Int, "", json_int(zahlungsart, "kZahlungsart", 0)},
|
||||
{ParamType::Int, "", kKundengruppe},
|
||||
{ParamType::NVarChar, order.value("externalOrderNumber", ""), 0}
|
||||
};
|
||||
if (active_shop == 0) {
|
||||
ps[11] = Param::null_int();
|
||||
}
|
||||
|
||||
ResultSet rs;
|
||||
if (!get_pool().execute(c, insert_auftrag, ps, rs) || rs.empty() || rs[0].empty()) {
|
||||
throw std::runtime_error("failed to insert Verkauf.tAuftrag");
|
||||
}
|
||||
int kAuftrag = std::stoi(rs[0][0].str);
|
||||
|
||||
upsert_pos_order_mapping(c, kAuftrag, kPosAuftrag);
|
||||
insert_order_address(c, kAuftrag, kKunde, order.value("shippingAddress", nlohmann::json::object()), 0);
|
||||
insert_order_address(c, kAuftrag, kKunde, order.value("billingAddress", nlohmann::json::object()), 1);
|
||||
|
||||
std::vector<delivery::DeliveredItem> delivered_items;
|
||||
const nlohmann::json& items = order.contains("orderItems") ? order["orderItems"] : nlohmann::json::array();
|
||||
for (const auto& item : items) {
|
||||
int kAuftragPosition = insert_order_item(c, kAuftrag, item);
|
||||
insert_pos_order_position_mapping(c, kAuftragPosition, item.value("externalId", ""));
|
||||
if (kAuftragPosition > 0) {
|
||||
delivered_items.push_back({kAuftragPosition, to_number(item["quantity"], 1)});
|
||||
}
|
||||
}
|
||||
|
||||
const nlohmann::json& payments = order.contains("payments") ? order["payments"] : nlohmann::json::array();
|
||||
for (const auto& payment : payments) {
|
||||
insert_payment(c, kAuftrag, payment, order, order_date_tm);
|
||||
}
|
||||
|
||||
if (is_order_delivered(order)) {
|
||||
delivery::deliver_order(c, g_config.kBenutzer, kAuftrag, defaults.kVersandArt, delivered_items);
|
||||
}
|
||||
|
||||
const char* calc =
|
||||
"DECLARE @auftrag_calc Verkauf.TYPE_spAuftragEckdatenBerechnen;"
|
||||
"INSERT INTO @auftrag_calc VALUES (?);"
|
||||
"EXEC Verkauf.spAuftragEckdatenBerechnen @auftrag = @auftrag_calc";
|
||||
std::vector<Param> ps_calc = {{ParamType::Int, "", kAuftrag}};
|
||||
get_pool().execute(c, calc, ps_calc, rs);
|
||||
|
||||
if (!get_pool().commit(c)) throw std::runtime_error("failed to commit transaction");
|
||||
|
||||
return { {"orderId", std::to_string(kAuftrag)}, {"orderNumber", cAuftragsNr} };
|
||||
} catch (const std::exception&) {
|
||||
try { get_pool().rollback(c); } catch (...) {}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace order
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "../db/pool.hpp"
|
||||
#include "../log.hpp"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
static const char* CUSTOMER_GROUP_IDS_SQL =
|
||||
@@ -16,13 +17,20 @@ static const char* CUSTOMER_GROUP_COUNT_SQL =
|
||||
"WHERE CONVERT(BIGINT, bRowversion) > ?";
|
||||
|
||||
inline std::vector<int64_t> get_customer_group_ids() {
|
||||
static std::vector<int64_t> cached;
|
||||
static bool loaded = false;
|
||||
if (loaded) return cached;
|
||||
|
||||
ResultSet rs;
|
||||
get_pool().execute(CUSTOMER_GROUP_IDS_SQL, rs);
|
||||
std::vector<int64_t> ids;
|
||||
for (auto& row : rs) {
|
||||
ids.push_back(std::stoll(row[0].str));
|
||||
if (!get_pool().execute(CUSTOMER_GROUP_IDS_SQL, rs)) {
|
||||
logc::warn("failed to load customer group ids");
|
||||
return {};
|
||||
}
|
||||
return ids;
|
||||
for (auto& row : rs) {
|
||||
cached.push_back(std::stoll(row[0].str));
|
||||
}
|
||||
loaded = true;
|
||||
return cached;
|
||||
}
|
||||
|
||||
inline nlohmann::json get_customer_group_list(int64_t cursor = 0) {
|
||||
|
||||
42
jtlsrv-cpp/src/queries/deliver.hpp
Normal file
42
jtlsrv-cpp/src/queries/deliver.hpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include "xml.hpp"
|
||||
#include "../db/pool.hpp"
|
||||
|
||||
namespace delivery {
|
||||
|
||||
inline void deliver_picklists(OdbcPool::Connection* c, int kBenutzer, int kSessionId,
|
||||
int kAuftrag, int kVersandArt) {
|
||||
std::string pakete = xml::element("Paket",
|
||||
xml::tag("kBestellung", int64_t(kAuftrag)) +
|
||||
xml::tag("kVersandart", int64_t(kVersandArt)) +
|
||||
xml::tag("fGewicht", 0.0));
|
||||
|
||||
const int AUSLIEFERN_OPTIONS = 0x002;
|
||||
|
||||
const char* sql =
|
||||
"DECLARE @xHinweise XML = NULL;"
|
||||
"DECLARE @xPakete XML = CONVERT(XML, ?);"
|
||||
"DECLARE @xResult XML;"
|
||||
"EXEC Auslieferung.spPicklistenAusliefern"
|
||||
" @xHinweise = @xHinweise,"
|
||||
" @Pakete = @xPakete,"
|
||||
" @nOptions = ?,"
|
||||
" @kBenutzer = ?,"
|
||||
" @kSessionId = ?,"
|
||||
" @xResult = @xResult OUTPUT;"
|
||||
"SELECT @xResult AS xResult";
|
||||
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::NVarChar, pakete, 0},
|
||||
{ParamType::Int, "", AUSLIEFERN_OPTIONS},
|
||||
{ParamType::Int, "", kBenutzer},
|
||||
{ParamType::Int, "", kSessionId}
|
||||
};
|
||||
ResultSet rs;
|
||||
if (!get_pool().execute(c, sql, ps, rs)) {
|
||||
throw std::runtime_error("deliver_picklists failed");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace delivery
|
||||
33
jtlsrv-cpp/src/queries/delivery.hpp
Normal file
33
jtlsrv-cpp/src/queries/delivery.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include "xml.hpp"
|
||||
#include "session.hpp"
|
||||
#include "warehouse.hpp"
|
||||
#include "reserve.hpp"
|
||||
#include "commit.hpp"
|
||||
#include "deliver.hpp"
|
||||
#include "../db/pool.hpp"
|
||||
|
||||
namespace delivery {
|
||||
|
||||
inline void deliver_order(OdbcPool::Connection* c, int kBenutzer, int kAuftrag,
|
||||
int kVersandArt, const std::vector<DeliveredItem>& items) {
|
||||
if (items.empty()) return;
|
||||
|
||||
int kWarenLager = resolve_outgoing_warehouse(c);
|
||||
int kSessionId = open_session(c, kBenutzer);
|
||||
|
||||
try {
|
||||
reserve_positions(c, kBenutzer, kSessionId, kWarenLager, items);
|
||||
commit_picklists(c, kBenutzer, kSessionId, kAuftrag);
|
||||
deliver_picklists(c, kBenutzer, kSessionId, kAuftrag, kVersandArt);
|
||||
} catch (...) {
|
||||
try { discard_session(c, kBenutzer, kSessionId); } catch (...) {}
|
||||
try { close_session(c, kSessionId); } catch (...) {}
|
||||
throw;
|
||||
}
|
||||
|
||||
try { discard_session(c, kBenutzer, kSessionId); } catch (...) {}
|
||||
try { close_session(c, kSessionId); } catch (...) {}
|
||||
}
|
||||
|
||||
} // namespace delivery
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "../log.hpp"
|
||||
#include "../db/pool.hpp"
|
||||
#include "nlohmann/json.hpp"
|
||||
#include "shop.hpp"
|
||||
@@ -41,13 +42,6 @@ static const char* PRODUCT_LIST_SQL =
|
||||
"OR (ir.maxImageRV IS NOT NULL AND ir.maxImageRV > ?)) "
|
||||
"ORDER BY lastChanged ASC";
|
||||
|
||||
static const char* PRICE_OVERRIDES_SQL =
|
||||
"SELECT p.kArtikel AS articleId, p.kKundenGruppe AS customerGroupId, "
|
||||
"MIN(pd.fNettoPreis) AS netPrice FROM dbo.tPreis p "
|
||||
"INNER JOIN dbo.tPreisDetail pd ON pd.kPreis = p.kPreis "
|
||||
"WHERE p.kArtikel IN (%s) AND p.kShop = 0 AND pd.nAnzahlAb = 0 "
|
||||
"GROUP BY p.kArtikel, p.kKundenGruppe";
|
||||
|
||||
static std::string gross_price(const std::string& net, const std::string& tax) {
|
||||
double n = std::stod(net.empty() ? "0" : net);
|
||||
double t = std::stod(tax.empty() ? "0" : tax);
|
||||
@@ -72,7 +66,11 @@ inline nlohmann::json get_product_list(int64_t cursor, int limit) {
|
||||
{ParamType::BigInt,"",cursor}
|
||||
};
|
||||
ResultSet rs;
|
||||
get_pool().execute(PRODUCT_LIST_SQL, ps, rs);
|
||||
if (!get_pool().execute(PRODUCT_LIST_SQL, ps, rs)) {
|
||||
logc::warn("product list query failed (cursor=%lld limit=%d shop=%d)",
|
||||
(long long)cursor, limit, shop);
|
||||
return nlohmann::json::array();
|
||||
}
|
||||
|
||||
auto cg_ids = get_customer_group_ids();
|
||||
nlohmann::json result = nlohmann::json::array();
|
||||
|
||||
57
jtlsrv-cpp/src/queries/reserve.hpp
Normal file
57
jtlsrv-cpp/src/queries/reserve.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "xml.hpp"
|
||||
#include "../db/pool.hpp"
|
||||
|
||||
namespace delivery {
|
||||
|
||||
struct DeliveredItem {
|
||||
int kAuftragPosition;
|
||||
double quantity;
|
||||
};
|
||||
|
||||
inline void reserve_positions(OdbcPool::Connection* c, int kBenutzer, int kSessionId,
|
||||
int kWarenLager, const std::vector<DeliveredItem>& items) {
|
||||
if (items.empty()) return;
|
||||
|
||||
std::string bestellpositionen;
|
||||
for (const auto& it : items) {
|
||||
bestellpositionen += xml::element("Bestellposition",
|
||||
xml::tag("kBestellPos", int64_t(it.kAuftragPosition)) +
|
||||
xml::tag("fAnzahl", it.quantity));
|
||||
}
|
||||
|
||||
std::string laeger = xml::element("Lager",
|
||||
xml::tag("kWarenlager", int64_t(kWarenLager)) +
|
||||
xml::tag("nPrio", int64_t(0)) +
|
||||
xml::tag("kLieferant", int64_t(0)) +
|
||||
xml::tag("kAnsprechpartner", int64_t(0)));
|
||||
|
||||
const int RESERVIERE_OPTIONS = 0x102;
|
||||
|
||||
const char* sql =
|
||||
"DECLARE @xBestellpositionen XML = CONVERT(XML, ?);"
|
||||
"DECLARE @xLaeger XML = CONVERT(XML, ?);"
|
||||
"EXEC Auslieferung.spReserviereBestellpositionen"
|
||||
" @Bestellpositionen = @xBestellpositionen,"
|
||||
" @Laeger = @xLaeger,"
|
||||
" @Warenlagereingaenge = NULL,"
|
||||
" @nOptions = ?,"
|
||||
" @kBenutzer = ?,"
|
||||
" @kSessionId = ?";
|
||||
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::NVarChar, bestellpositionen, 0},
|
||||
{ParamType::NVarChar, laeger, 0},
|
||||
{ParamType::Int, "", RESERVIERE_OPTIONS},
|
||||
{ParamType::Int, "", kBenutzer},
|
||||
{ParamType::Int, "", kSessionId}
|
||||
};
|
||||
ResultSet rs;
|
||||
if (!get_pool().execute(c, sql, ps, rs)) {
|
||||
throw std::runtime_error("reserve_positions failed");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace delivery
|
||||
42
jtlsrv-cpp/src/queries/session.hpp
Normal file
42
jtlsrv-cpp/src/queries/session.hpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include "../db/pool.hpp"
|
||||
|
||||
namespace delivery {
|
||||
|
||||
inline int open_session(OdbcPool::Connection* c, int kBenutzer,
|
||||
const std::string& hostname = "jtlsrv") {
|
||||
const char* sql =
|
||||
"DECLARE @t TABLE ([kSessionId] INT);"
|
||||
"INSERT INTO dbo.tSessionId (cRechnername, kBenutzer, dLastAction)"
|
||||
" OUTPUT inserted.kSessionId INTO @t"
|
||||
" VALUES (?, ?, DATEADD(day, 10, GETDATE()));"
|
||||
"SELECT kSessionId FROM @t;";
|
||||
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::NVarChar, hostname, 0},
|
||||
{ParamType::Int, "", kBenutzer}
|
||||
};
|
||||
ResultSet rs;
|
||||
if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].empty()) {
|
||||
throw std::runtime_error("failed to open delivery session");
|
||||
}
|
||||
return std::stoi(rs[0][0].str);
|
||||
}
|
||||
|
||||
inline void discard_session(OdbcPool::Connection* c, int kBenutzer, int kSessionId) {
|
||||
std::vector<Param> ps = {
|
||||
{ParamType::Int, "", kBenutzer},
|
||||
{ParamType::Int, "", kSessionId}
|
||||
};
|
||||
ResultSet rs;
|
||||
get_pool().execute(c, "{CALL Auslieferung.spPicklistenVerwerfen(?, ?)}", ps, rs);
|
||||
}
|
||||
|
||||
inline void close_session(OdbcPool::Connection* c, int kSessionId) {
|
||||
std::vector<Param> ps = {{ParamType::Int, "", kSessionId}};
|
||||
ResultSet rs;
|
||||
get_pool().execute(c, "DELETE FROM dbo.tSessionId WHERE kSessionId = ?", ps, rs);
|
||||
}
|
||||
|
||||
} // namespace delivery
|
||||
28
jtlsrv-cpp/src/queries/shop.cpp
Normal file
28
jtlsrv-cpp/src/queries/shop.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include "shop.hpp"
|
||||
#include "../log.hpp"
|
||||
#include <stdexcept>
|
||||
|
||||
int g_active_shop_id = 0;
|
||||
int g_active_shop_subshop_id = 0;
|
||||
|
||||
static int cell_to_int(const Cell& cell) {
|
||||
if (cell.type == CellType::Int64) return static_cast<int>(cell.i64);
|
||||
if (cell.type == CellType::String && !cell.str.empty()) return std::stoi(cell.str);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool fetch_active_shop() {
|
||||
ResultSet rs;
|
||||
if (!get_pool().execute(
|
||||
"SELECT TOP 1 kShop, kShopSubshop FROM dbo.tShopSubshop "
|
||||
"WHERE nGesperrt = 0 ORDER BY kShop",
|
||||
rs) || rs.empty()) {
|
||||
logc::warn("failed to load active shop from dbo.tShopSubshop");
|
||||
return false;
|
||||
}
|
||||
|
||||
g_active_shop_id = cell_to_int(rs[0][0]);
|
||||
g_active_shop_subshop_id = rs[0].size() > 1 ? cell_to_int(rs[0][1]) : 0;
|
||||
logc::info("Active shop: kShop=%d kShopSubshop=%d", g_active_shop_id, g_active_shop_subshop_id);
|
||||
return g_active_shop_id > 0;
|
||||
}
|
||||
@@ -1,21 +1,10 @@
|
||||
#pragma once
|
||||
#include "../db/pool.hpp"
|
||||
#include "../config.hpp"
|
||||
|
||||
static int g_active_shop_id = 0;
|
||||
static int g_active_shop_subshop_id = 0;
|
||||
extern int g_active_shop_id;
|
||||
extern int g_active_shop_subshop_id;
|
||||
|
||||
inline int get_active_shop_id() { return g_active_shop_id; }
|
||||
inline int get_active_shop_subshop_id() { return g_active_shop_subshop_id; }
|
||||
|
||||
inline bool fetch_active_shop() {
|
||||
ResultSet rs;
|
||||
if (!get_pool().execute(
|
||||
"SELECT TOP 1 kShop, kShopSubshop FROM dbo.tShopSubshop "
|
||||
"WHERE nGesperrt = 0 ORDER BY kShop", rs) || rs.empty()) {
|
||||
return false;
|
||||
}
|
||||
g_active_shop_id = std::stoi(rs[0][0].str);
|
||||
g_active_shop_subshop_id = rs[0].size() > 1 ? std::stoi(rs[0][1].str) : 0;
|
||||
return true;
|
||||
}
|
||||
bool fetch_active_shop();
|
||||
|
||||
21
jtlsrv-cpp/src/queries/warehouse.hpp
Normal file
21
jtlsrv-cpp/src/queries/warehouse.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include "../db/pool.hpp"
|
||||
#include "../config.hpp"
|
||||
|
||||
namespace delivery {
|
||||
|
||||
inline int resolve_outgoing_warehouse(OdbcPool::Connection* c) {
|
||||
int configured = config::get_int("JTL_KWARENLAGER", 0);
|
||||
if (configured > 0) return configured;
|
||||
|
||||
ResultSet rs;
|
||||
if (!get_pool().execute(c,
|
||||
"SELECT TOP 1 kWarenLager FROM dbo.tWarenLager "
|
||||
"WHERE nFulfillment = 0 AND ISNULL(nAktiv, 1) = 1 "
|
||||
"ORDER BY nAuslieferungsPrio, kWarenLager", rs) || rs.empty() || rs[0].empty()) {
|
||||
throw std::runtime_error("No local warehouse (dbo.tWarenLager.nFulfillment = 0) found; set JTL_KWARENLAGER explicitly.");
|
||||
}
|
||||
return std::stoi(rs[0][0].str);
|
||||
}
|
||||
|
||||
} // namespace delivery
|
||||
40
jtlsrv-cpp/src/queries/xml.hpp
Normal file
40
jtlsrv-cpp/src/queries/xml.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
namespace xml {
|
||||
|
||||
inline std::string escape(const std::string& s) {
|
||||
std::string r;
|
||||
r.reserve(s.size());
|
||||
for (char c : s) {
|
||||
switch (c) {
|
||||
case '<': r += "<"; break;
|
||||
case '>': r += ">"; break;
|
||||
case '&': r += "&"; break;
|
||||
case '\'': r += "'"; break;
|
||||
case '"': r += """; break;
|
||||
default: r += c; break;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
inline std::string tag(const std::string& name, const std::string& value) {
|
||||
return "<" + name + ">" + escape(value) + "</" + name + ">";
|
||||
}
|
||||
|
||||
inline std::string tag(const std::string& name, int64_t value) {
|
||||
return "<" + name + ">" + std::to_string(value) + "</" + name + ">";
|
||||
}
|
||||
|
||||
inline std::string tag(const std::string& name, double value) {
|
||||
char buf[32];
|
||||
std::snprintf(buf, sizeof(buf), "%.6f", value);
|
||||
return "<" + name + ">" + buf + "</" + name + ">";
|
||||
}
|
||||
|
||||
inline std::string element(const std::string& name, const std::string& children) {
|
||||
return "<" + name + ">" + children + "</" + name + ">";
|
||||
}
|
||||
|
||||
} // namespace xml
|
||||
@@ -340,6 +340,7 @@ static void tls_server_shutdown() {
|
||||
uv_close(reinterpret_cast<uv_handle_t*>(&g_sigint), nullptr);
|
||||
uv_close(reinterpret_cast<uv_handle_t*>(&g_sigterm), nullptr);
|
||||
uv_close(reinterpret_cast<uv_handle_t*>(&g_server), nullptr);
|
||||
uv_stop(g_loop);
|
||||
}
|
||||
|
||||
void tls_server_install_signals(uv_loop_t* loop) {
|
||||
|
||||
Reference in New Issue
Block a user