#pragma once // Port of src/queries/product-attributes.js — article attributes + Pfand deposit fields. #include "../db/pool.hpp" #include "../http.hpp" #include "../log.hpp" #include "nlohmann/json.hpp" #include #include #include #include #include #include struct ProductAttributeBundle { nlohmann::json attributes = nlohmann::json::array(); // null when no Pfand attributes present; otherwise deposit / deposit_name / d_price nlohmann::json deposit = nullptr; }; struct AttrRow { int64_t article_id = 0; int64_t k_attribut = 0; int k_shop = 0; int n_sortierung = 0; int k_feld_typ = 0; std::string c_gruppe_name; std::string c_attribut_id; std::string c_name; std::string c_wert_varchar; bool has_n_wert_int = false; int n_wert_int = 0; bool has_f_wert_decimal = false; double f_wert_decimal = 0.0; }; inline std::string join_int64s(const std::vector& ids) { if (ids.empty()) return "NULL"; std::ostringstream oss; for (size_t i = 0; i < ids.size(); ++i) { if (i) oss << ','; oss << ids[i]; } return oss.str(); } inline const std::vector& get_pfand_k_attribute_ids() { static std::vector cached; static bool loaded = false; if (loaded) return cached; const char* sql = "SELECT a.kAttribut " "FROM dbo.tAttribut a " "INNER JOIN dbo.tAttributSprache s ON s.kAttribut = a.kAttribut AND s.kSprache = 0 " "WHERE a.cGruppeName = 'JTL-POS' " "AND a.cAttributId IN ('Pfandartikel', 'Pfandart (Bezeichnung auf Ausdruck)', 'Pfandbetrag')"; ResultSet rs; if (!get_pool().execute(sql, rs)) { logc::warn("failed to load Pfand attribute metadata"); return cached; } for (auto& row : rs) { if (!row.empty()) { cached.push_back(parse_int64(row[0].str, 0)); } } loaded = true; return cached; } inline const std::vector& get_active_shop_ids_for_attrs() { static std::vector cached; static bool loaded = false; if (loaded) return cached; const char* sql = "WITH ActiveShops AS (" " SELECT ss.kShop, s.kKategorie AS rootKategorie " " FROM dbo.tShopSubshop ss " " INNER JOIN dbo.tShop s ON s.kShop = ss.kShop " " WHERE ss.nGesperrt = 0" ") SELECT DISTINCT ash.kShop FROM ActiveShops ash"; ResultSet rs; if (!get_pool().execute(sql, rs)) { logc::warn("failed to load active shops for attributes"); return cached; } for (auto& row : rs) { if (!row.empty()) { cached.push_back(parse_int64(row[0].str, 0)); } } loaded = true; return cached; } inline nlohmann::json to_pos_attribute(const AttrRow& row) { return { {"aname", row.c_name}, {"aprice", "0.0"}, {"asort", std::to_string(row.n_sortierung)}, {"atype", std::to_string(row.k_feld_typ)}, {"agroup", row.c_gruppe_name}, }; } inline nlohmann::json derive_deposit_fields(const std::vector& rows) { const AttrRow* pfand_artikel = nullptr; const AttrRow* pfand_art = nullptr; const AttrRow* pfand_betrag = nullptr; for (const auto& row : rows) { if (row.c_attribut_id == "Pfandartikel") pfand_artikel = &row; else if (row.c_attribut_id == "Pfandart (Bezeichnung auf Ausdruck)") pfand_art = &row; else if (row.c_attribut_id == "Pfandbetrag") pfand_betrag = &row; } if (!pfand_artikel && !pfand_art && !pfand_betrag) { return nullptr; } std::string d_price = "0.0"; if (pfand_betrag && pfand_betrag->has_f_wert_decimal) { char buf[32]; std::snprintf(buf, sizeof(buf), "%.2f", pfand_betrag->f_wert_decimal); d_price = buf; } return { {"deposit", (pfand_artikel && pfand_artikel->has_n_wert_int && pfand_artikel->n_wert_int == 1) ? "1" : "0"}, {"deposit_name", pfand_art ? pfand_art->c_wert_varchar : ""}, {"d_price", d_price}, }; } inline std::vector merge_article_attributes(std::vector rows) { // Later rows for the same kAttribut win (shop-specific over global), matching JS Map set order. std::map by_attr; for (auto& row : rows) { by_attr[row.k_attribut] = std::move(row); } std::vector merged; merged.reserve(by_attr.size()); for (auto& [_, row] : by_attr) { merged.push_back(std::move(row)); } std::sort(merged.begin(), merged.end(), [](const AttrRow& a, const AttrRow& b) { return a.n_sortierung < b.n_sortierung; }); return merged; } inline AttrRow parse_attr_row(const Row& row) { AttrRow a; if (row.size() < 11) return a; a.article_id = parse_int64(row[0].str, 0); a.k_attribut = parse_int64(row[1].str, 0); a.k_shop = parse_int(row[2].str, 0); a.n_sortierung = parse_int(row[3].str, 0); a.k_feld_typ = parse_int(row[4].str, 0); a.c_gruppe_name = row[5].type == CellType::Null ? "" : row[5].str; a.c_attribut_id = row[6].type == CellType::Null ? "" : row[6].str; a.c_name = row[7].type == CellType::Null ? "" : row[7].str; a.c_wert_varchar = row[8].type == CellType::Null ? "" : row[8].str; if (row[9].type != CellType::Null) { a.has_n_wert_int = true; a.n_wert_int = parse_int(row[9].str, 0); } if (row[10].type != CellType::Null) { a.has_f_wert_decimal = true; a.f_wert_decimal = parse_double(row[10].str, 0); } return a; } inline std::map get_product_attributes( const std::vector& article_ids) { std::map out; if (article_ids.empty()) return out; const auto& pfand_ids = get_pfand_k_attribute_ids(); const auto& active_shops = get_active_shop_ids_for_attrs(); std::string sql = "SELECT " " aa.kArtikel AS articleId, " " aa.kAttribut, " " aa.kShop, " " at.nSortierung, " " at.kFeldTyp, " " at.cGruppeName, " " at.cAttributId, " " ats.cName, " " aas.cWertVarchar, " " aas.nWertInt, " " aas.fWertDecimal " "FROM dbo.tArtikelAttribut aa " "INNER JOIN dbo.tArtikelAttributSprache aas " " ON aas.kArtikelAttribut = aa.kArtikelAttribut AND aas.kSprache = 0 " "INNER JOIN dbo.tAttribut at ON at.kAttribut = aa.kAttribut " "INNER JOIN dbo.tAttributSprache ats ON ats.kAttribut = at.kAttribut AND ats.kSprache = 0 " "WHERE aa.kArtikel IN (" + join_int64s(article_ids) + ") " " AND (" " aa.kShop = 0 " " OR (" " aa.kShop IN (" + join_int64s(active_shops) + ") " " AND aa.kAttribut IN (" + join_int64s(pfand_ids) + ") " " )" " ) " "ORDER BY aa.kArtikel, at.nSortierung, aa.kShop"; ResultSet rs; if (!get_pool().execute(sql, rs)) { logc::warn("product attributes query failed (%zu articles)", article_ids.size()); return out; } std::map> rows_by_article; for (const auto& row : rs) { AttrRow a = parse_attr_row(row); if (a.article_id == 0) continue; rows_by_article[a.article_id].push_back(std::move(a)); } for (auto& [article_id, rows] : rows_by_article) { auto merged = merge_article_attributes(std::move(rows)); ProductAttributeBundle bundle; for (const auto& row : merged) { bundle.attributes.push_back(to_pos_attribute(row)); } bundle.deposit = derive_deposit_fields(merged); out[article_id] = std::move(bundle); } return out; }