feat(reports): cari, birikim, alışveriş, birim fiyat ve fire rapor servisleri

- mağaza adı eşleşmesi ve aylık birim fiyat özeti (saf, birim testli)
- tamamlanan listelerin plan/gerçekleşen özeti, mağaza bazlı harcama
- ürün tipi fiyat geçmişi yükleyicisi; fire oranı ve tahmini maliyet; testler

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 00:53:02 +03:00
parent d9a22551e1
commit 8623c5a02e
7 changed files with 920 additions and 4 deletions
+51
View File
@@ -381,6 +381,57 @@ export async function loadTypeOffers(
return out;
}
/** Bir ürün tipinin tüm fiyat kayıtları, ürün tipi biriminde birim fiyatla (dönüşümsüzler hariç). */
export async function loadTypePriceHistory(
householdId: Types.ObjectId,
productTypeId: Types.ObjectId,
): Promise<{ date: string; productId: string; storeId: string; unitPriceMinor: number }[]> {
const [products, type] = await Promise.all([
ProductModel.find({ householdId, productTypeId })
.select({ packageQuantity: 1, packageUnit: 1, typeQuantity: 1 })
.lean<
{
_id: Types.ObjectId;
packageQuantity: number;
packageUnit: string;
typeQuantity: number | null;
}[]
>(),
ProductTypeModel.findOne({ householdId, _id: productTypeId })
.select({ unit: 1 })
.lean<{ unit: string }>(),
]);
if (!type) return [];
const inType = new Map(
products.map((p) => [
String(p._id),
packageInTypeUnit({
packageQuantity: p.packageQuantity,
packageUnit: p.packageUnit,
typeUnit: type.unit,
typeQuantity: p.typeQuantity,
}),
]),
);
const prices = await loadPrices(
householdId,
products.map((p) => p._id),
);
return prices.flatMap((r) => {
const q = inType.get(String(r.productId));
return q
? [
{
date: r.date,
productId: String(r.productId),
storeId: String(r.storeId),
unitPriceMinor: unitPrice(r.priceMinor, q),
},
]
: [];
});
}
/**
* Ü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.