This commit is contained in:
seb
2026-08-15 03:04:14 +02:00
parent 6abe2032e8
commit e1a009ba5c
21 changed files with 751 additions and 10 deletions

View File

@@ -32,6 +32,7 @@ CPP_SRCS := \
src/endpoints/productcomposite.cpp \
src/endpoints/deleted_entity.cpp \
src/endpoints/customergroup.cpp \
src/endpoints/customer.cpp \
src/endpoints/order.cpp \
src/endpoints/pimage.cpp \
src/endpoints/cimage.cpp \

View File

@@ -110,6 +110,7 @@ Place `certs/cert.pem` and `certs/key.pem` relative to the working directory whe
| GET | `/v1/productcomposite` | Composite product sync |
| GET | `/v1/deletedentity` | Deleted entity sync |
| GET | `/v1/customergroup` | Customer group sync |
| GET | `/v1/customer` | Customer sync |
| POST | `/v1/order` | Submit orders |
| GET | `/v1/pimage` | Product image (resized) |
| GET | `/v1/cimage` | Category image (resized) |

View File

@@ -0,0 +1,16 @@
#include "../http.hpp"
#include "../tls_server.hpp"
#include "../router.hpp"
#include "../queries/customer_list.hpp"
#include "../queries/shop.hpp"
void handle_customer(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
int64_t cursor = req.get_query_int64("lastChangedCustomer");
int limit = req.get_query_int("limit", 20);
int shop = get_active_shop_id();
int subshop = get_active_shop_subshop_id();
int64_t max_last_changed = get_active_max_last_changed();
auto customers = get_customer_list(cursor, limit, shop, subshop, max_last_changed);
resp.send_json(200, customers);
}

View File

@@ -10,23 +10,26 @@
void handle_init(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
int64_t product_cursor = req.get_query_int64("lastChangedProduct");
int64_t category_cursor = req.get_query_int64("lastChangedCategory");
int64_t customer_cursor = req.get_query_int64("lastChangedCustomer");
int64_t cg_cursor = req.get_query_int64("lastChangedCustomerGroup");
int64_t composite_cursor = req.get_query_int64("lastChangedCompositeProduct");
int64_t deleted_cursor = req.get_query_int64("lastChangedDeletedEntity");
int root = config::get_int("ROOT_CATEGORY_ID", 1);
int shop = get_active_shop_id();
int subshop = get_active_shop_subshop_id();
int64_t product_count = 0, category_count = 0, cg_count = 0, composite_count = 0, deleted_count = 0;
int64_t product_count = 0, category_count = 0, customer_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);
category_count = get_category_count(root, shop, category_cursor);
customer_count = get_customer_count(shop, subshop, customer_cursor);
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());
max_order_id_count = get_max_order_id_count(subshop);
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");
@@ -39,7 +42,7 @@ void handle_init(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
{"version", "1.10.12.0"},
{"product_count", std::to_string(product_count)},
{"category_count", std::to_string(category_count)},
{"customer_count", "0"},
{"customer_count", std::to_string(customer_count)},
{"customerGroup_count", std::to_string(cg_count)},
{"compositeProduct_count", std::to_string(composite_count)},
{"configurationGroup_count", "0"},

View File

@@ -107,6 +107,7 @@ extern void handle_product(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_productcomposite(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_deleted_entity(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_customergroup(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_customer(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_order(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_pimage(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_cimage(HttpRequest&, HttpResponse&, RouteContext&);
@@ -188,6 +189,7 @@ int main(int /*argc*/, char* argv[]) {
router.add_route("GET", "/v1/productcomposite", handle_productcomposite);
router.add_route("GET", "/v1/deletedentity", handle_deleted_entity);
router.add_route("GET", "/v1/customergroup", handle_customergroup);
router.add_route("GET", "/v1/customer", handle_customer);
router.add_route("POST", "/v1/order", handle_order);
router.add_route("GET", "/v1/pimage", handle_pimage);
router.add_route("GET", "/v1/cimage", handle_cimage);

View File

@@ -39,6 +39,15 @@ static const char* COMPOSITE_PRODUCT_COUNT_SQL =
"AND ks.kShop = ? WHERE ka.kArtikel = a.kArtikel)) "
"AND CONVERT(BIGINT, a.bRowversion) > ?";
static const char* CUSTOMER_COUNT_SQL =
"SELECT COUNT(DISTINCT k.kKunde) AS cnt FROM dbo.tkunde k "
"LEFT JOIN dbo.tInetKundeShop iks ON iks.kKunde = k.kKunde "
"AND iks.kShop = ? AND iks.kSubShop = ? "
"WHERE k.kKundenGruppe = 1 "
"AND (? = 0 OR EXISTS (SELECT 1 FROM dbo.tInetKundeShop x "
"WHERE x.kKunde = k.kKunde AND x.kShop = ?)) "
"AND CONVERT(BIGINT, k.bRowversion) > ?";
static const char* DELETED_ENTITY_COUNT_SQL =
"SELECT COUNT(*) AS cnt FROM Pos.vDeletedEntity "
"WHERE CONVERT(BIGINT, vDeletedEntity.bLastChanged) > ?";
@@ -63,6 +72,12 @@ inline int64_t get_composite_count(int k_shop, int64_t cursor) {
{{ParamType::Int,"",k_shop},{ParamType::Int,"",k_shop},
{ParamType::BigInt,"",cursor}});
}
inline int64_t get_customer_count(int k_shop, int k_subshop, int64_t cursor) {
return get_pool().execute_scalar(CUSTOMER_COUNT_SQL,
{{ParamType::Int,"",k_shop},{ParamType::Int,"",k_subshop},
{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}});

View File

@@ -0,0 +1,101 @@
#pragma once
#include "../db/pool.hpp"
#include "nlohmann/json.hpp"
#include "shop.hpp"
static const char* CUSTOMER_LIST_SQL =
"SELECT TOP (?) "
"vCustomer.kId, "
"ISNULL(tInetKundeShop.cShopKundenNr, vCustomer.cCustomerNumber) AS cCustomerNumber, "
"vCustomer.cFirstName, "
"vCustomer.cLastName, "
"vCustomer.cTitle, "
"vCustomer.cCompany, "
"vCustomer.cAddress, "
"vCustomer.cAddressSupplement, "
"vCustomer.cCity, "
"vCustomer.cPostalCode, "
"vCustomer.cState, "
"vCustomer.cCountry, "
"vCustomer.cPhone, "
"vCustomer.cEmailAddress, "
"ISNULL(tInetKundeShop.kKundenGruppe, vCustomer.kCustomerGroupId) AS kCustomerGroupId, "
"vCustomer.cSalutation, "
"vCustomer.cDateOfBirth, "
"vCustomer.fDiscount, "
"vCustomer.cFederalTaxId, "
"CONVERT(BIGINT, vCustomer.bLastChanged) AS lastChanged, "
"vCustomer.kShop, "
"vCustomer.dLastModified, "
"vCustomer.dActive, "
"vCustomer.dInactive, "
"vCustomer.nDebtorNumber "
"FROM Pos.vCustomer "
"LEFT JOIN dbo.tInetKundeShop "
" ON vCustomer.kId = tInetKundeShop.kKunde "
" AND tInetKundeShop.kShop = vCustomer.kShop "
" AND tInetKundeShop.kSubShop = ? "
"WHERE vCustomer.kShop = ? "
" AND CONVERT(BIGINT, vCustomer.bLastChanged) > ? "
" AND CONVERT(BIGINT, vCustomer.bLastChanged) <= ? "
" AND vCustomer.kCustomerGroupId = 1 "
"ORDER BY vCustomer.bLastChanged ASC";
inline std::string format_birthday(const std::string& c_date_of_birth) {
if (c_date_of_birth.empty()) return "";
// cDateOfBirth is expected as "dd.MM.yyyy"
int d = 0, m = 0, y = 0;
if (std::sscanf(c_date_of_birth.c_str(), "%d.%d.%d", &d, &m, &y) != 3) {
return "";
}
if (y < 1900 || y > 2100 || m < 1 || m > 12 || d < 1 || d > 31) {
return "";
}
char buf[32];
std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d 00:00:00", y, m, d);
return buf;
}
inline nlohmann::json get_customer_list(int64_t cursor, int limit, int shop, int subshop, int64_t max_last_changed) {
std::vector<Param> ps = {
{ParamType::Int, "", limit},
{ParamType::Int, "", subshop},
{ParamType::Int, "", shop},
{ParamType::BigInt, "", cursor},
{ParamType::BigInt, "", max_last_changed},
};
ResultSet rs;
if (!get_pool().execute(CUSTOMER_LIST_SQL, ps, rs)) {
logc::warn("customer list query failed (cursor=%lld limit=%d shop=%d subshop=%d)",
(long long)cursor, limit, shop, subshop);
return nlohmann::json::array();
}
nlohmann::json result = nlohmann::json::array();
for (auto& row : rs) {
result.push_back({
{"id", row[0].str},
{"customerNumber", row[1].type == CellType::Null ? "" : row[1].str},
{"firstname", row[2].type == CellType::Null ? "" : row[2].str},
{"lastname", row[3].type == CellType::Null ? "" : row[3].str},
{"title", row[4].type == CellType::Null ? "" : row[4].str},
{"company", row[5].type == CellType::Null ? "" : row[5].str},
{"address", row[6].type == CellType::Null ? "" : row[6].str},
{"addressSupplement", row[7].type == CellType::Null ? "" : row[7].str},
{"city", row[8].type == CellType::Null ? "" : row[8].str},
{"postalCode", row[9].type == CellType::Null ? "" : row[9].str},
{"state", row[10].type == CellType::Null ? "" : row[10].str},
{"country", row[11].type == CellType::Null ? "" : row[11].str},
{"phone", row[12].type == CellType::Null ? "" : row[12].str},
{"email", row[13].type == CellType::Null ? "" : row[13].str},
{"customerGroupId", row[14].type == CellType::Null ? "1" : row[14].str},
{"salutation", row[15].type == CellType::Null ? "" : row[15].str},
{"birthday", format_birthday(row[16].str)},
{"discount", row[17].type == CellType::Null ? "0.00" : row[17].str},
{"taxIdNumber", row[18].type == CellType::Null ? "" : row[18].str},
{"lastChanged", row[19].str},
{"debtorNumber", row[24].type == CellType::Null ? "0" : row[24].str},
});
}
return result;
}

View File

@@ -1,9 +1,11 @@
#include "shop.hpp"
#include "../log.hpp"
#include "../http.hpp"
#include <stdexcept>
int g_active_shop_id = 0;
int g_active_shop_subshop_id = 0;
int64_t g_active_max_last_changed = 0;
static int cell_to_int(const Cell& cell) {
if (cell.type == CellType::Int64) return static_cast<int>(cell.i64);
@@ -11,10 +13,16 @@ static int cell_to_int(const Cell& cell) {
return 0;
}
static int64_t cell_to_int64(const Cell& cell) {
if (cell.type == CellType::Int64) return cell.i64;
if (cell.type == CellType::String && !cell.str.empty()) return parse_int64(cell.str, 0);
return 0;
}
bool fetch_active_shop() {
ResultSet rs;
if (!get_pool().execute(
"SELECT TOP 1 kShop, kShopSubshop FROM dbo.tShopSubshop "
"SELECT TOP 1 kShop, kShopSubshop, nMaxLastChanged FROM dbo.tShopSubshop "
"WHERE nGesperrt = 0 ORDER BY kShop",
rs) || rs.empty()) {
logc::warn("failed to load active shop from dbo.tShopSubshop");
@@ -23,6 +31,9 @@ bool fetch_active_shop() {
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);
g_active_max_last_changed = rs[0].size() > 2 ? cell_to_int64(rs[0][2]) : 0;
logc::info("Active shop: kShop=%d kShopSubshop=%d nMaxLastChanged=%lld",
g_active_shop_id, g_active_shop_subshop_id,
(long long)g_active_max_last_changed);
return g_active_shop_id > 0;
}

View File

@@ -3,8 +3,10 @@
extern int g_active_shop_id;
extern int g_active_shop_subshop_id;
extern int64_t g_active_max_last_changed;
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 int64_t get_active_max_last_changed() { return g_active_max_last_changed; }
bool fetch_active_shop();