feat(shopping): alışveriş listeleri, stok eşiği önerileri ve akıllı mağaza önerisi motoru
- shoppingLists koleksiyonu (ürün tipi / marka / serbest metin kalemleri), migration 015 - planShopping saf fonksiyonu: paket yuvarlama, zahmet ve hacimli cezası, teslimat, yemek kartı önceliği, ek mağaza eşiği - eşik önerileri onay/red (red stok yenilenene kadar), ürün tipine hacimli işareti - fiyat servisinde ortak loadTypeOffers; seed listeleri ve testler Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -293,17 +293,35 @@ export async function getTypePriceComparison(
|
||||
};
|
||||
}
|
||||
|
||||
/** Ürün tipi birimine çevrilebilen ürünlerin mağaza başına geçerli fiyatı (planlayıcı girdisi). */
|
||||
export type TypeOffer = {
|
||||
productId: string;
|
||||
productTypeId: string;
|
||||
storeId: string;
|
||||
priceMinor: number;
|
||||
inTypeUnit: number;
|
||||
unitPriceMinor: number;
|
||||
date: string;
|
||||
stale: boolean;
|
||||
isPromo: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ürün tipi başına son bilinen birim fiyat (en yeni geçerli fiyat kaydı; tüm marka/mağazalar).
|
||||
* Tüketim analizinde aylık maliyet tahmini için. Dönüşümü eksik ürünler atlanır.
|
||||
* Ürün tiplerinin tüm (arşivsiz) ürünlerinin mağaza başına geçerli fiyatları. Dönüşümü eksik
|
||||
* ürünler atlanır. `activeOnly`: arşivli ürün ve mağazaları dışarıda bırak.
|
||||
*/
|
||||
export async function latestTypeUnitPrices(
|
||||
export async function loadTypeOffers(
|
||||
householdId: Types.ObjectId,
|
||||
productTypeIds: Types.ObjectId[],
|
||||
today = todayIstanbul(),
|
||||
): Promise<Map<string, { unitPriceMinor: number; date: string }>> {
|
||||
const [products, types] = await Promise.all([
|
||||
ProductModel.find({ householdId, productTypeId: { $in: productTypeIds } })
|
||||
{ today = todayIstanbul(), activeOnly = false } = {},
|
||||
): Promise<TypeOffer[]> {
|
||||
if (!productTypeIds.length) return [];
|
||||
const [products, types, archivedStores] = await Promise.all([
|
||||
ProductModel.find({
|
||||
householdId,
|
||||
productTypeId: { $in: productTypeIds },
|
||||
...(activeOnly ? { archivedAt: null } : {}),
|
||||
})
|
||||
.select({ productTypeId: 1, packageQuantity: 1, packageUnit: 1, typeQuantity: 1 })
|
||||
.lean<
|
||||
{
|
||||
@@ -317,7 +335,13 @@ export async function latestTypeUnitPrices(
|
||||
ProductTypeModel.find({ householdId, _id: { $in: productTypeIds } })
|
||||
.select({ unit: 1 })
|
||||
.lean<{ _id: Types.ObjectId; unit: string }[]>(),
|
||||
activeOnly
|
||||
? StoreModel.find({ householdId, archivedAt: { $ne: null } })
|
||||
.select({ _id: 1 })
|
||||
.lean<{ _id: Types.ObjectId }[]>()
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
const skipStore = new Set(archivedStores.map((x) => String(x._id)));
|
||||
const unitOf = new Map(types.map((x) => [String(x._id), x.unit]));
|
||||
const prices = await loadPrices(
|
||||
householdId,
|
||||
@@ -325,11 +349,12 @@ export async function latestTypeUnitPrices(
|
||||
);
|
||||
const groups = new Map<string, PriceLean[]>();
|
||||
for (const r of prices) {
|
||||
if (skipStore.has(String(r.storeId))) continue;
|
||||
const key = `${r.productId}:${r.storeId}`;
|
||||
groups.set(key, [...(groups.get(key) ?? []), r]);
|
||||
}
|
||||
const productById = new Map(products.map((p) => [String(p._id), p]));
|
||||
const out = new Map<string, { unitPriceMinor: number; date: string }>();
|
||||
const out: TypeOffer[] = [];
|
||||
for (const records of groups.values()) {
|
||||
const p = productById.get(String(records[0]!.productId))!;
|
||||
const typeId = String(p.productTypeId);
|
||||
@@ -341,12 +366,35 @@ export async function latestTypeUnitPrices(
|
||||
});
|
||||
if (!inType) continue;
|
||||
const eff = effectivePrice(records, today)!;
|
||||
const prev = out.get(typeId);
|
||||
if (!prev || eff.record.date > prev.date)
|
||||
out.set(typeId, {
|
||||
unitPriceMinor: unitPrice(eff.record.priceMinor, inType),
|
||||
date: eff.record.date,
|
||||
});
|
||||
out.push({
|
||||
productId: String(p._id),
|
||||
productTypeId: typeId,
|
||||
storeId: String(records[0]!.storeId),
|
||||
priceMinor: eff.record.priceMinor,
|
||||
inTypeUnit: inType,
|
||||
unitPriceMinor: unitPrice(eff.record.priceMinor, inType),
|
||||
date: eff.record.date,
|
||||
stale: eff.stale,
|
||||
isPromo: eff.record.isPromo && !eff.promoEnded,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ürün tipi başına son bilinen birim fiyat (en yeni geçerli fiyat kaydı; tüm marka/mağazalar).
|
||||
* Tüketim analizinde aylık maliyet tahmini için. Dönüşümü eksik ürünler atlanır.
|
||||
*/
|
||||
export async function latestTypeUnitPrices(
|
||||
householdId: Types.ObjectId,
|
||||
productTypeIds: Types.ObjectId[],
|
||||
today = todayIstanbul(),
|
||||
): Promise<Map<string, { unitPriceMinor: number; date: string }>> {
|
||||
const out = new Map<string, { unitPriceMinor: number; date: string }>();
|
||||
for (const o of await loadTypeOffers(householdId, productTypeIds, { today })) {
|
||||
const prev = out.get(o.productTypeId);
|
||||
if (!prev || o.date > prev.date)
|
||||
out.set(o.productTypeId, { unitPriceMinor: o.unitPriceMinor, date: o.date });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user