From c42a14a437355eb86dbb7bbc4ae46c2ae905a79f Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 15 Aug 2026 04:11:54 +0200 Subject: [PATCH] u --- API.md | 2 +- jtlsrv-cpp/src/endpoints/customer.cpp | 3 +- jtlsrv-cpp/src/queries/counts.hpp | 3 +- jtlsrv-cpp/src/queries/customer_list.hpp | 7 ++-- src/queries/customer-count.js | 3 +- src/queries/customer-list.js | 5 +-- src/queries/product-list.js | 45 ++++++++++++++++++++++-- 7 files changed, 50 insertions(+), 18 deletions(-) diff --git a/API.md b/API.md index 9f963e3..092da6b 100644 --- a/API.md +++ b/API.md @@ -296,7 +296,7 @@ No `limit` — returns all groups newer than the cursor. ### `GET /v1/customer` -Customer deltas. Only customers of customer group `1` are returned, scoped to the active shop (`vCustomer.kShop = active kShop`) and its subshop (`tInetKundeShop.kSubShop = active kShopSubshop`). Rows are ordered by `bLastChanged` ascending, and only rows with `bLastChanged <= nMaxLastChanged` of the active subshop are returned. +Customer deltas. Customers are scoped to the active shop (`vCustomer.kShop = active kShop`) and its subshop (`tInetKundeShop.kSubShop = active kShopSubshop`). Rows are ordered by `bLastChanged` ascending, and only rows with `bLastChanged > lastChangedCustomer` are returned (matching the count semantics of `/v1/init`). | Param | Default | |---|---| diff --git a/jtlsrv-cpp/src/endpoints/customer.cpp b/jtlsrv-cpp/src/endpoints/customer.cpp index e9b68fc..d64a1ba 100644 --- a/jtlsrv-cpp/src/endpoints/customer.cpp +++ b/jtlsrv-cpp/src/endpoints/customer.cpp @@ -9,8 +9,7 @@ void handle_customer(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/ 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); + auto customers = get_customer_list(cursor, limit, shop, subshop); resp.send_json(200, customers); } diff --git a/jtlsrv-cpp/src/queries/counts.hpp b/jtlsrv-cpp/src/queries/counts.hpp index 1cf3587..19c13e9 100644 --- a/jtlsrv-cpp/src/queries/counts.hpp +++ b/jtlsrv-cpp/src/queries/counts.hpp @@ -43,8 +43,7 @@ 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 (? = 0 OR EXISTS (SELECT 1 FROM dbo.tInetKundeShop x " "WHERE x.kKunde = k.kKunde AND x.kShop = ?)) " "AND CONVERT(BIGINT, k.bRowversion) > ?"; diff --git a/jtlsrv-cpp/src/queries/customer_list.hpp b/jtlsrv-cpp/src/queries/customer_list.hpp index 03f891b..e191b18 100644 --- a/jtlsrv-cpp/src/queries/customer_list.hpp +++ b/jtlsrv-cpp/src/queries/customer_list.hpp @@ -1,7 +1,9 @@ #pragma once #include "../db/pool.hpp" +#include "../log.hpp" #include "nlohmann/json.hpp" #include "shop.hpp" +#include static const char* CUSTOMER_LIST_SQL = "SELECT TOP (?) " @@ -37,8 +39,6 @@ static const char* CUSTOMER_LIST_SQL = " 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) { @@ -56,13 +56,12 @@ inline std::string format_birthday(const std::string& c_date_of_birth) { return buf; } -inline nlohmann::json get_customer_list(int64_t cursor, int limit, int shop, int subshop, int64_t max_last_changed) { +inline nlohmann::json get_customer_list(int64_t cursor, int limit, int shop, int subshop) { std::vector 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)) { diff --git a/src/queries/customer-count.js b/src/queries/customer-count.js index 6461cd6..80ecb88 100644 --- a/src/queries/customer-count.js +++ b/src/queries/customer-count.js @@ -11,8 +11,7 @@ LEFT JOIN dbo.tInetKundeShop iks ON iks.kKunde = k.kKunde AND iks.kShop = @kShop AND iks.kSubShop = @SubShopId -WHERE k.kKundenGruppe = 1 - AND (@kShop = 0 OR EXISTS ( +WHERE (@kShop = 0 OR EXISTS ( SELECT 1 FROM dbo.tInetKundeShop x WHERE x.kKunde = k.kKunde AND x.kShop = @kShop )) diff --git a/src/queries/customer-list.js b/src/queries/customer-list.js index 9a048ae..d21d707 100644 --- a/src/queries/customer-list.js +++ b/src/queries/customer-list.js @@ -2,7 +2,7 @@ import sql from 'mssql'; import { isDemoMode } from '../demo/mode.js'; import { getDemoCustomerList } from '../demo/store.js'; import { getPool } from '../db.js'; -import { getActiveMaxLastChanged, getActiveShopId, getActiveShopSubshopId } from '../shop.js'; +import { getActiveShopId, getActiveShopSubshopId } from '../shop.js'; const CUSTOMER_LIST_SQL = ` SELECT TOP (@Limit) @@ -38,8 +38,6 @@ LEFT JOIN dbo.tInetKundeShop AND tInetKundeShop.kSubShop = @SubShopId WHERE vCustomer.kShop = @ShopId AND CONVERT(BIGINT, vCustomer.bLastChanged) > @bLastChanged - AND CONVERT(BIGINT, vCustomer.bLastChanged) <= @MaxLastChanged - AND vCustomer.kCustomerGroupId = 1 ORDER BY vCustomer.bLastChanged ASC; `; @@ -72,7 +70,6 @@ export async function getCustomerList({ cursor = 0, limit = 20 } = {}) { .input('ShopId', sql.Int, getActiveShopId()) .input('SubShopId', sql.Int, getActiveShopSubshopId()) .input('bLastChanged', sql.BigInt, cursor) - .input('MaxLastChanged', sql.BigInt, getActiveMaxLastChanged()) .query(CUSTOMER_LIST_SQL); return result.recordset.map((row) => ({ diff --git a/src/queries/product-list.js b/src/queries/product-list.js index 3f599d7..249ecdc 100644 --- a/src/queries/product-list.js +++ b/src/queries/product-list.js @@ -82,6 +82,26 @@ function priceOverridesSql(articleIds) { `; } +// Returns the first image hash (ordered by nNr) per article, matching the main +// query's imgHash selection. Used to resolve the parent's image for products +// without a picture of their own. +function imageHashesSql(articleIds) { + const idList = articleIds.join(','); + return ` + SELECT articleId, imgHash + FROM ( + SELECT + abp.kArtikel AS articleId, + img.cHash AS imgHash, + ROW_NUMBER() OVER (PARTITION BY abp.kArtikel ORDER BY abp.nNr) AS rn + FROM dbo.tArtikelbildPlattform abp + INNER JOIN dbo.tBild img ON img.kBild = abp.kBild + WHERE abp.kArtikel IN (${idList}) + ) t + WHERE rn = 1 AND imgHash IS NOT NULL AND imgHash <> ''; + `; +} + function formatDateTime(date) { if (!date) { return '0001-01-01 00:00:00'; @@ -118,10 +138,20 @@ export async function getProductList({ cursor = 0, limit = 20 } = {}) { const overridesByArticle = new Map(); let attributesByArticle = new Map(); + let parentImageHashes = new Map(); if (articleIds.length > 0) { - const [overrideResult, attributeMap] = await Promise.all([ + // Include any parent articles referenced by products in this batch so we + // can resolve the parent image for products without a picture of their own, + // even when the parent itself is not part of the batch. + const parentIds = products + .map((p) => (p.parentArticleId > 0 && !p.imgHash ? Number(p.parentArticleId) : null)) + .filter((id) => id !== null && !articleIds.includes(id)); + const hashIds = [...new Set([...articleIds, ...parentIds])]; + + const [overrideResult, attributeMap, imageHashResult] = await Promise.all([ pool.request().query(priceOverridesSql(articleIds)), getProductAttributes(pool, articleIds), + pool.request().query(imageHashesSql(hashIds)), ]); attributesByArticle = attributeMap; for (const row of overrideResult.recordset) { @@ -130,6 +160,9 @@ export async function getProductList({ cursor = 0, limit = 20 } = {}) { } overridesByArticle.get(row.articleId).set(row.customerGroupId, row.netPrice); } + for (const row of imageHashResult.recordset) { + parentImageHashes.set(row.articleId, row.imgHash); + } } return products.map((product) => { @@ -151,10 +184,16 @@ export async function getProductList({ cursor = 0, limit = 20 } = {}) { const categoryIds = product.categoryIds ? product.categoryIds.split(',') : []; const articleAttributes = attributesByArticle.get(product.id); + // If a product has a parent and no picture of its own, fall back to the + // parent's image hash. + const hasOwnImage = !!product.imgHash; + const parentHash = !hasOwnImage ? parentImageHashes.get(product.parentArticleId) : null; + const imageHash = hasOwnImage ? product.imgHash : parentHash; + return { _id: String(product.id), - imghash: product.imgHash ?? null, - imgsrc: product.imgHash ?? null, + imghash: imageHash ?? null, + imgsrc: imageHash ?? null, sku: product.sku, barcode: product.barcode ?? null, name: product.name,