feat(shopping): mağazalar, zahmet skoru ve ürünler (marka/paket, dönüşüm, görseller)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,8 @@ describe("seed", () => {
|
||||
expect(summary.created.assets).toBe(4);
|
||||
expect(summary.created.budgets).toBe(2);
|
||||
expect(summary.created.stock).toBe(19);
|
||||
expect(summary.created.stores).toBe(8);
|
||||
expect(summary.created.products).toBe(22);
|
||||
expect(await getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1);
|
||||
expect(await HouseholdMemberModel.countDocuments()).toBe(memberCount);
|
||||
});
|
||||
@@ -55,6 +57,8 @@ describe("seed", () => {
|
||||
assets: 0,
|
||||
budgets: 0,
|
||||
stock: 0,
|
||||
stores: 0,
|
||||
products: 0,
|
||||
});
|
||||
expect(await getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1);
|
||||
expect(await HouseholdModel.countDocuments()).toBe(SEED_HOUSEHOLDS.length);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { runMigrations } from "../migrations";
|
||||
import { seedContacts } from "./contacts";
|
||||
import { seedAssets } from "./assets";
|
||||
import { seedBudgets } from "./budgets";
|
||||
import { seedShopping } from "./shopping";
|
||||
import { seedStock } from "./stock";
|
||||
import { seedInKind } from "./in-kind";
|
||||
import { seedInstallments } from "./installments";
|
||||
@@ -57,6 +58,8 @@ export type SeedSummary = {
|
||||
assets: number;
|
||||
budgets: number;
|
||||
stock: number;
|
||||
stores: number;
|
||||
products: number;
|
||||
};
|
||||
accounts: SeedAccountRow[];
|
||||
};
|
||||
@@ -84,6 +87,8 @@ const DOMAIN_COLLECTIONS = [
|
||||
"productTypes",
|
||||
"stockMovements",
|
||||
"stockCounts",
|
||||
"stores",
|
||||
"products",
|
||||
];
|
||||
|
||||
type AuthApi = ReturnType<typeof getAuth>["api"];
|
||||
@@ -140,6 +145,8 @@ export async function runSeed({
|
||||
assets: 0,
|
||||
budgets: 0,
|
||||
stock: 0,
|
||||
stores: 0,
|
||||
products: 0,
|
||||
},
|
||||
accounts: [],
|
||||
};
|
||||
@@ -324,6 +331,11 @@ export async function runSeed({
|
||||
if (summary.created.budgets) log(`✓ Örnek bütçeler: ${summary.created.budgets}`);
|
||||
summary.created.stock = await seedStock(householdIds, userIdMap);
|
||||
if (summary.created.stock) log(`✓ Örnek stok ürün tipleri: ${summary.created.stock}`);
|
||||
const shopping = await seedShopping(householdIds, userIdMap);
|
||||
summary.created.stores = shopping.stores;
|
||||
summary.created.products = shopping.products;
|
||||
if (shopping.stores || shopping.products)
|
||||
log(`✓ Örnek mağazalar ve ürünler: ${shopping.stores} / ${shopping.products}`);
|
||||
|
||||
// 7) Geliştirme hesapları tablosu
|
||||
const all = [
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
import { Types } from "mongoose";
|
||||
import { ProductModel } from "@/models/product";
|
||||
import { ProductTypeModel } from "@/models/product-type";
|
||||
import { StoreModel } from "@/models/store";
|
||||
import type { MealCardProvider } from "@/shared/accounts";
|
||||
import { autoConvertible } from "@/shared/products";
|
||||
import { foldTr } from "@/shared/search";
|
||||
import type { StoreKind } from "@/shared/stores";
|
||||
|
||||
/**
|
||||
* Örnek mağazalar ve ürünler (Faz 12). Çakır Ailesi (Kadıköy) için marketler, pazar ve online
|
||||
* platform; stok ürün tiplerine bağlı marka/paketler. Elle dönüşüm gereken örnekler: deterjan
|
||||
* (kg → yıkama), tablet paketi (paket → tablet), diş macunu/şampuan (ml → adet). Görsel yok.
|
||||
* Idempotent (`seedRef`). Miktarlar binde bir, tutarlar kuruş.
|
||||
*/
|
||||
|
||||
type SeedStore = {
|
||||
ref: string;
|
||||
name: string;
|
||||
kind: StoreKind;
|
||||
branch?: string;
|
||||
distanceM?: number;
|
||||
carry: number;
|
||||
deliveryFee?: number;
|
||||
freeDeliveryMin?: number;
|
||||
mealCards?: MealCardProvider[];
|
||||
note?: string;
|
||||
};
|
||||
|
||||
export const STORES: SeedStore[] = [
|
||||
{
|
||||
ref: "migros",
|
||||
name: "Migros",
|
||||
kind: "market",
|
||||
branch: "Bahariye",
|
||||
distanceM: 1_200,
|
||||
carry: 2,
|
||||
mealCards: ["pluxee", "multinet", "edenred", "setcard"],
|
||||
},
|
||||
{ ref: "a101", name: "A101", kind: "market", branch: "Moda", distanceM: 400, carry: 1 },
|
||||
{ ref: "bim", name: "BİM", kind: "market", branch: "Yeldeğirmeni", distanceM: 650, carry: 2 },
|
||||
{
|
||||
ref: "sok",
|
||||
name: "Şok",
|
||||
kind: "market",
|
||||
branch: "Rasimpaşa",
|
||||
distanceM: 300,
|
||||
carry: 1,
|
||||
mealCards: ["multinet", "pluxee"],
|
||||
},
|
||||
{
|
||||
ref: "carrefoursa",
|
||||
name: "CarrefourSA",
|
||||
kind: "market",
|
||||
branch: "Acıbadem",
|
||||
distanceM: 3_800,
|
||||
carry: 4,
|
||||
mealCards: ["pluxee", "multinet", "edenred", "setcard", "metropol"],
|
||||
note: "Hafta sonu arabayla; büyük alışveriş.",
|
||||
},
|
||||
{
|
||||
ref: "sali-pazari",
|
||||
name: "Salı Pazarı",
|
||||
kind: "bazaar",
|
||||
branch: "Kadıköy",
|
||||
distanceM: 1_900,
|
||||
carry: 4,
|
||||
note: "Salı ve cuma; sebze-meyve.",
|
||||
},
|
||||
{
|
||||
ref: "getir-buyuk",
|
||||
name: "Getir Büyük",
|
||||
kind: "online",
|
||||
carry: 1,
|
||||
deliveryFee: 39_90,
|
||||
freeDeliveryMin: 750_00,
|
||||
mealCards: ["multinet"],
|
||||
},
|
||||
{
|
||||
ref: "metro",
|
||||
name: "Metro Toptancı",
|
||||
kind: "market",
|
||||
branch: "Kozyatağı",
|
||||
distanceM: 9_500,
|
||||
carry: 5,
|
||||
},
|
||||
];
|
||||
|
||||
type SeedProduct = {
|
||||
ref: string;
|
||||
/** Stok seed'indeki ürün tipi `seedRef`'i. */
|
||||
type: string;
|
||||
brand: string;
|
||||
name?: string;
|
||||
barcode?: string;
|
||||
qty: number;
|
||||
unit: string;
|
||||
/** Elle dönüşüm (bir paket = bu kadar ürün tipi birimi). */
|
||||
typeQty?: number;
|
||||
};
|
||||
|
||||
const K = 1000;
|
||||
|
||||
export const PRODUCTS: SeedProduct[] = [
|
||||
{
|
||||
ref: "ariel-5kg",
|
||||
type: "toz-deterjan",
|
||||
brand: "Ariel",
|
||||
name: "Matik Dağ Esintisi",
|
||||
qty: 5 * K,
|
||||
unit: "kg",
|
||||
typeQty: 33 * K,
|
||||
barcode: "8690000100015",
|
||||
},
|
||||
{
|
||||
ref: "omo-6kg",
|
||||
type: "toz-deterjan",
|
||||
brand: "Omo",
|
||||
name: "Matik Active Fresh",
|
||||
qty: 6 * K,
|
||||
unit: "kg",
|
||||
typeQty: 40 * K,
|
||||
},
|
||||
{
|
||||
ref: "bingo-4kg",
|
||||
type: "toz-deterjan",
|
||||
brand: "Bingo",
|
||||
name: "Matik",
|
||||
qty: 4 * K,
|
||||
unit: "kg",
|
||||
typeQty: 26 * K,
|
||||
},
|
||||
{
|
||||
ref: "fairy-50",
|
||||
type: "tablet-deterjan",
|
||||
brand: "Fairy",
|
||||
name: "Platinum Plus",
|
||||
qty: 1 * K,
|
||||
unit: "paket",
|
||||
typeQty: 50 * K,
|
||||
barcode: "8690000100022",
|
||||
},
|
||||
{
|
||||
ref: "finish-40",
|
||||
type: "tablet-deterjan",
|
||||
brand: "Finish",
|
||||
name: "Quantum",
|
||||
qty: 40 * K,
|
||||
unit: "tablet",
|
||||
},
|
||||
{
|
||||
ref: "yayla-mercimek",
|
||||
type: "kirmizi-mercimek",
|
||||
brand: "Yayla",
|
||||
name: "Kırmızı Mercimek",
|
||||
qty: 1 * K,
|
||||
unit: "kg",
|
||||
},
|
||||
{
|
||||
ref: "reis-mercimek",
|
||||
type: "kirmizi-mercimek",
|
||||
brand: "Reis",
|
||||
name: "Kırmızı Mercimek",
|
||||
qty: 1 * K,
|
||||
unit: "kg",
|
||||
},
|
||||
{
|
||||
ref: "duru-mercimek",
|
||||
type: "kirmizi-mercimek",
|
||||
brand: "Duru",
|
||||
name: "Kırmızı Mercimek",
|
||||
qty: 2_500,
|
||||
unit: "kg",
|
||||
},
|
||||
{ ref: "yayla-baldo", type: "pirinc", brand: "Yayla", name: "Baldo", qty: 2_500, unit: "kg" },
|
||||
{ ref: "reis-baldo", type: "pirinc", brand: "Reis", name: "Baldo", qty: 1 * K, unit: "kg" },
|
||||
{
|
||||
ref: "pinar-sut",
|
||||
type: "sut",
|
||||
brand: "Pınar",
|
||||
name: "Tam Yağlı Süt",
|
||||
qty: 1 * K,
|
||||
unit: "lt",
|
||||
barcode: "8690000100039",
|
||||
},
|
||||
{ ref: "sutas-sut", type: "sut", brand: "Sütaş", name: "Tam Yağlı Süt", qty: 1 * K, unit: "lt" },
|
||||
{
|
||||
ref: "icim-sut-500",
|
||||
type: "sut",
|
||||
brand: "İçim",
|
||||
name: "Yarım Yağlı Süt",
|
||||
qty: 500 * K,
|
||||
unit: "ml",
|
||||
},
|
||||
{
|
||||
ref: "keskinoglu-30",
|
||||
type: "yumurta",
|
||||
brand: "Keskinoğlu",
|
||||
name: "L Boy 30'lu",
|
||||
qty: 30 * K,
|
||||
unit: "adet",
|
||||
},
|
||||
{
|
||||
ref: "koy-yumurta-15",
|
||||
type: "yumurta",
|
||||
brand: "Köy Yumurtası",
|
||||
name: "15'li",
|
||||
qty: 15 * K,
|
||||
unit: "adet",
|
||||
},
|
||||
{
|
||||
ref: "pinar-peynir",
|
||||
type: "beyaz-peynir",
|
||||
brand: "Pınar",
|
||||
name: "Beyaz Peynir",
|
||||
qty: 500 * K,
|
||||
unit: "g",
|
||||
},
|
||||
{
|
||||
ref: "yumos-1440",
|
||||
type: "yumusatici",
|
||||
brand: "Yumoş",
|
||||
name: "Extra Lilyum",
|
||||
qty: 1_440 * K,
|
||||
unit: "ml",
|
||||
},
|
||||
{
|
||||
ref: "selpak-32",
|
||||
type: "tuvalet-kagidi",
|
||||
brand: "Selpak",
|
||||
name: "32'li",
|
||||
qty: 32 * K,
|
||||
unit: "rulo",
|
||||
},
|
||||
{
|
||||
ref: "solo-24",
|
||||
type: "tuvalet-kagidi",
|
||||
brand: "Solo",
|
||||
name: "24'lü",
|
||||
qty: 24 * K,
|
||||
unit: "rulo",
|
||||
},
|
||||
{
|
||||
ref: "colgate-75",
|
||||
type: "dis-macunu",
|
||||
brand: "Colgate",
|
||||
name: "Total 75 ml",
|
||||
qty: 75 * K,
|
||||
unit: "ml",
|
||||
typeQty: 1 * K,
|
||||
},
|
||||
{
|
||||
ref: "sensodyne-75",
|
||||
type: "dis-macunu",
|
||||
brand: "Sensodyne",
|
||||
name: "Repair 75 ml",
|
||||
qty: 75 * K,
|
||||
unit: "ml",
|
||||
typeQty: 1 * K,
|
||||
},
|
||||
{
|
||||
ref: "elidor-500",
|
||||
type: "sampuan",
|
||||
brand: "Elidor",
|
||||
name: "500 ml",
|
||||
qty: 500 * K,
|
||||
unit: "ml",
|
||||
typeQty: 1 * K,
|
||||
},
|
||||
];
|
||||
|
||||
export async function seedShopping(
|
||||
householdIds: Map<string, Types.ObjectId>,
|
||||
users: Map<string, string>,
|
||||
): Promise<{ stores: number; products: number }> {
|
||||
const actor = new Types.ObjectId(users.get("ayse")!);
|
||||
const householdId = householdIds.get("Çakır Ailesi")!;
|
||||
let stores = 0;
|
||||
for (const s of STORES) {
|
||||
if (await StoreModel.exists({ householdId, seedRef: s.ref })) continue;
|
||||
await StoreModel.create({
|
||||
householdId,
|
||||
name: s.name,
|
||||
nameKey: foldTr(s.name),
|
||||
kind: s.kind,
|
||||
branch: s.branch ?? "",
|
||||
distanceM: s.kind === "online" ? null : (s.distanceM ?? null),
|
||||
carryDifficulty: s.carry,
|
||||
deliveryFeeMinor: s.kind === "online" ? (s.deliveryFee ?? null) : null,
|
||||
freeDeliveryMinMinor: s.kind === "online" ? (s.freeDeliveryMin ?? null) : null,
|
||||
mealCardAccepted: Boolean(s.mealCards?.length),
|
||||
mealCardProviders: s.mealCards ?? [],
|
||||
note: s.note ?? "",
|
||||
seedRef: s.ref,
|
||||
createdBy: actor,
|
||||
updatedBy: actor,
|
||||
});
|
||||
stores++;
|
||||
}
|
||||
|
||||
const types = await ProductTypeModel.find({ householdId, seedRef: { $type: "string" } })
|
||||
.select({ seedRef: 1, unit: 1 })
|
||||
.lean<{ _id: Types.ObjectId; seedRef: string; unit: string }[]>();
|
||||
const typeByRef = new Map(types.map((x) => [x.seedRef, x]));
|
||||
let products = 0;
|
||||
for (const p of PRODUCTS) {
|
||||
const type = typeByRef.get(p.type);
|
||||
if (!type || (await ProductModel.exists({ householdId, seedRef: p.ref }))) continue;
|
||||
if (!autoConvertible(p.unit, type.unit) && !p.typeQty)
|
||||
throw new Error(`Seed ürünü ${p.ref}: ${p.unit} → ${type.unit} dönüşümü eksik`);
|
||||
await ProductModel.create({
|
||||
householdId,
|
||||
productTypeId: type._id,
|
||||
brand: p.brand,
|
||||
name: p.name ?? "",
|
||||
searchKey: foldTr(`${p.brand} ${p.name ?? ""}`),
|
||||
barcode: p.barcode ?? null,
|
||||
packageQuantity: p.qty,
|
||||
packageUnit: p.unit,
|
||||
typeQuantity: autoConvertible(p.unit, type.unit) ? null : p.typeQty,
|
||||
seedRef: p.ref,
|
||||
createdBy: actor,
|
||||
updatedBy: actor,
|
||||
});
|
||||
products++;
|
||||
}
|
||||
return { stores, products };
|
||||
}
|
||||
Reference in New Issue
Block a user