mirror of
https://github.com/samsonjs/spirit-tracker.git
synced 2026-03-25 09:25:51 +00:00
link sku
This commit is contained in:
parent
91f6288b43
commit
5f7eeb5205
3 changed files with 106 additions and 3 deletions
78
viz/app/linker/price.js
Normal file
78
viz/app/linker/price.js
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
// viz/app/linker/price.js
|
||||||
|
export function buildPricePenaltyForPair({ allAgg, rules, kPerGroup = 6 }) {
|
||||||
|
// canonSku -> sorted array of up to kPerGroup lowest prices
|
||||||
|
const groupPrices = new Map();
|
||||||
|
|
||||||
|
function insertPrice(arr, p) {
|
||||||
|
// keep sorted ascending, cap length
|
||||||
|
let i = 0;
|
||||||
|
while (i < arr.length && arr[i] <= p) i++;
|
||||||
|
arr.splice(i, 0, p);
|
||||||
|
if (arr.length > kPerGroup) arr.length = kPerGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const it of allAgg || []) {
|
||||||
|
if (!it) continue;
|
||||||
|
const sku = String(it.sku || "");
|
||||||
|
if (!sku) continue;
|
||||||
|
|
||||||
|
const p = it.cheapestPriceNum;
|
||||||
|
if (p == null || !(p > 0)) continue;
|
||||||
|
|
||||||
|
const canon = String((rules && rules.canonicalSku && rules.canonicalSku(sku)) || sku);
|
||||||
|
let arr = groupPrices.get(canon);
|
||||||
|
if (!arr) groupPrices.set(canon, (arr = []));
|
||||||
|
insertPrice(arr, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bestRelativeGap(prA, prB) {
|
||||||
|
// min |a-b| / min(a,b)
|
||||||
|
let best = Infinity;
|
||||||
|
for (let i = 0; i < prA.length; i++) {
|
||||||
|
const a = prA[i];
|
||||||
|
for (let j = 0; j < prB.length; j++) {
|
||||||
|
const b = prB[j];
|
||||||
|
const gap = Math.abs(a - b) / Math.max(1e-9, Math.min(a, b));
|
||||||
|
if (gap < best) best = gap;
|
||||||
|
if (best <= 0.001) return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gapToMultiplier(gap) {
|
||||||
|
// gap = 0.40 => 40% relative difference
|
||||||
|
// <=35%: no penalty
|
||||||
|
// 35-50%: ease down to ~0.75
|
||||||
|
// >50%: continue down gently, floor at 0.35
|
||||||
|
if (!(gap >= 0)) return 1.0;
|
||||||
|
if (gap <= 0.35) return 1.0;
|
||||||
|
|
||||||
|
if (gap <= 0.50) {
|
||||||
|
const t = (gap - 0.35) / 0.15; // 0..1
|
||||||
|
return 1.0 - 0.25 * t; // 1.00 -> 0.75
|
||||||
|
}
|
||||||
|
|
||||||
|
const m = 0.75 * (0.5 / gap);
|
||||||
|
return Math.max(0.35, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function pricePenaltyForPair(aSku, bSku) {
|
||||||
|
const a = String(aSku || "");
|
||||||
|
const b = String(bSku || "");
|
||||||
|
if (!a || !b) return 1.0;
|
||||||
|
|
||||||
|
const aCanon = String((rules && rules.canonicalSku && rules.canonicalSku(a)) || a);
|
||||||
|
const bCanon = String((rules && rules.canonicalSku && rules.canonicalSku(b)) || b);
|
||||||
|
|
||||||
|
const prA = groupPrices.get(aCanon);
|
||||||
|
const prB = groupPrices.get(bCanon);
|
||||||
|
if (!prA || !prB || !prA.length || !prB.length) return 1.0;
|
||||||
|
|
||||||
|
const gap = bestRelativeGap(prA, prB);
|
||||||
|
if (!isFinite(gap)) return 1.0;
|
||||||
|
|
||||||
|
return gapToMultiplier(gap);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +63,7 @@ export function recommendSimilar(
|
||||||
mappedSkus,
|
mappedSkus,
|
||||||
isIgnoredPairFn,
|
isIgnoredPairFn,
|
||||||
sizePenaltyFn,
|
sizePenaltyFn,
|
||||||
|
pricePenaltyFn,
|
||||||
sameStoreFn,
|
sameStoreFn,
|
||||||
sameGroupFn
|
sameGroupFn
|
||||||
) {
|
) {
|
||||||
|
|
@ -169,6 +170,11 @@ export function recommendSimilar(
|
||||||
if (typeof sizePenaltyFn === "function") {
|
if (typeof sizePenaltyFn === "function") {
|
||||||
s0 *= sizePenaltyFn(pinnedSku, itSku);
|
s0 *= sizePenaltyFn(pinnedSku, itSku);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Price penalty early
|
||||||
|
if (typeof pricePenaltyFn === "function") {
|
||||||
|
s0 *= pricePenaltyFn(pinnedSku, itSku);
|
||||||
|
}
|
||||||
|
|
||||||
// Age handling early
|
// Age handling early
|
||||||
const itAge = extractAgeFromText(itNorm);
|
const itAge = extractAgeFromText(itNorm);
|
||||||
|
|
@ -215,6 +221,11 @@ export function recommendSimilar(
|
||||||
s *= sizePenaltyFn(pinnedSku, itSku);
|
s *= sizePenaltyFn(pinnedSku, itSku);
|
||||||
if (s <= 0) continue;
|
if (s <= 0) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof pricePenaltyFn === "function") {
|
||||||
|
s *= pricePenaltyFn(pinnedSku, itSku);
|
||||||
|
if (s <= 0) continue;
|
||||||
|
}
|
||||||
|
|
||||||
const itAge = extractAgeFromText(itNorm);
|
const itAge = extractAgeFromText(itNorm);
|
||||||
if (pinAge && itAge) {
|
if (pinAge && itAge) {
|
||||||
|
|
@ -263,7 +274,8 @@ export function recommendSimilar(
|
||||||
limitPairs,
|
limitPairs,
|
||||||
isIgnoredPairFn,
|
isIgnoredPairFn,
|
||||||
sameStoreFn,
|
sameStoreFn,
|
||||||
sizePenaltyFn // ✅ NEW: pass sizePenaltyForPair in
|
sizePenaltyFn, // ✅ NEW: pass sizePenaltyForPair in
|
||||||
|
pricePenaltyFn
|
||||||
) {
|
) {
|
||||||
const itemsAll = allAgg.filter((it) => !!it);
|
const itemsAll = allAgg.filter((it) => !!it);
|
||||||
|
|
||||||
|
|
@ -455,6 +467,8 @@ export function recommendSimilar(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof sizePenaltyFn === "function") s *= sizePenaltyFn(aSku, bSku);
|
if (typeof sizePenaltyFn === "function") s *= sizePenaltyFn(aSku, bSku);
|
||||||
|
|
||||||
|
if (typeof pricePenaltyFn === "function") s *= pricePenaltyFn(aSku, bSku);
|
||||||
|
|
||||||
const bAge = extractAgeFromText(bNorm);
|
const bAge = extractAgeFromText(bNorm);
|
||||||
if (aAge && bAge) {
|
if (aAge && bAge) {
|
||||||
|
|
@ -494,6 +508,11 @@ export function recommendSimilar(
|
||||||
s *= sizePenaltyFn(aSku, bSku);
|
s *= sizePenaltyFn(aSku, bSku);
|
||||||
if (s <= 0) continue;
|
if (s <= 0) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof pricePenaltyFn === "function") {
|
||||||
|
s *= pricePenaltyFn(aSku, bSku);
|
||||||
|
if (s <= 0) continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (aAge && x.bAge) {
|
if (aAge && x.bAge) {
|
||||||
if (aAge === x.bAge) s *= 2.0;
|
if (aAge === x.bAge) s *= 2.0;
|
||||||
|
|
@ -577,4 +596,3 @@ export function recommendSimilar(
|
||||||
}
|
}
|
||||||
return h >>> 0;
|
return h >>> 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import { buildCanonStoreCache, makeSameStoreCanonFn } from "./linker/store_cache
|
||||||
import { buildSizePenaltyForPair } from "./linker/size.js";
|
import { buildSizePenaltyForPair } from "./linker/size.js";
|
||||||
import { pickPreferredCanonical } from "./linker/canonical_pref.js";
|
import { pickPreferredCanonical } from "./linker/canonical_pref.js";
|
||||||
import { smwsKeyFromName } from "./linker/similarity.js";
|
import { smwsKeyFromName } from "./linker/similarity.js";
|
||||||
|
import { buildPricePenaltyForPair } from "./linker/price.js";
|
||||||
import {
|
import {
|
||||||
topSuggestions,
|
topSuggestions,
|
||||||
recommendSimilar,
|
recommendSimilar,
|
||||||
|
|
@ -124,10 +125,14 @@ export async function renderSkuLinker($app) {
|
||||||
// ✅ canonical-group size cache + helper
|
// ✅ canonical-group size cache + helper
|
||||||
let sizePenaltyForPair = buildSizePenaltyForPair({ allRows, allAgg, rules });
|
let sizePenaltyForPair = buildSizePenaltyForPair({ allRows, allAgg, rules });
|
||||||
|
|
||||||
|
// ✅ canonical-group price cache + helper
|
||||||
|
let pricePenaltyForPair = buildPricePenaltyForPair({ allAgg, rules });
|
||||||
|
|
||||||
function rebuildCachesAfterRulesReload() {
|
function rebuildCachesAfterRulesReload() {
|
||||||
CANON_STORE_CACHE = buildCanonStoreCache(allAgg, rules);
|
CANON_STORE_CACHE = buildCanonStoreCache(allAgg, rules);
|
||||||
sameStoreCanon = makeSameStoreCanonFn(rules, CANON_STORE_CACHE);
|
sameStoreCanon = makeSameStoreCanonFn(rules, CANON_STORE_CACHE);
|
||||||
sizePenaltyForPair = buildSizePenaltyForPair({ allRows, allAgg, rules });
|
sizePenaltyForPair = buildSizePenaltyForPair({ allRows, allAgg, rules });
|
||||||
|
pricePenaltyForPair = buildPricePenaltyForPair({ allAgg, rules });
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIgnoredPair(a, b) {
|
function isIgnoredPair(a, b) {
|
||||||
|
|
@ -156,7 +161,8 @@ export async function renderSkuLinker($app) {
|
||||||
28,
|
28,
|
||||||
isIgnoredPair,
|
isIgnoredPair,
|
||||||
sameStoreCanon,
|
sameStoreCanon,
|
||||||
sizePenaltyForPair // ✅ NEW
|
sizePenaltyForPair, // ✅ NEW
|
||||||
|
pricePenaltyForPair // ✅ NEW
|
||||||
);
|
);
|
||||||
|
|
||||||
return initialPairs;
|
return initialPairs;
|
||||||
|
|
@ -240,6 +246,7 @@ export async function renderSkuLinker($app) {
|
||||||
mappedSkus,
|
mappedSkus,
|
||||||
isIgnoredPair,
|
isIgnoredPair,
|
||||||
sizePenaltyForPair,
|
sizePenaltyForPair,
|
||||||
|
pricePenaltyForPair,
|
||||||
sameStoreCanon,
|
sameStoreCanon,
|
||||||
sameGroup
|
sameGroup
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue