diff --git a/scripts/seed/installments.ts b/scripts/seed/installments.ts new file mode 100644 index 0000000..4636441 --- /dev/null +++ b/scripts/seed/installments.ts @@ -0,0 +1,68 @@ +import type { Types } from "mongoose"; +import { createTransaction } from "@/features/transactions/service"; +import { AccountModel } from "@/models/account"; +import { CategoryModel } from "@/models/category"; +import { TransactionModel } from "@/models/transaction"; +import { todayIstanbul } from "@/shared/dates"; +import { addDays } from "@/shared/recurrence"; + +/** + * Örnek taksitli kart harcamaları (Faz 7b): bir kısmı gerçekleşmiş, kalanı planlı taksitler. + * İdempotent: aynı satıcıyla taksit planı varsa atlanır. + */ +const PURCHASES = [ + { + payee: "Hepsiburada", + description: "Çamaşır makinesi", + amountMinor: 18_000_00, + count: 9, + daysAgo: 75, + category: "ev", + by: "admin", + }, + { + payee: "Teknosa", + description: "Telefon", + amountMinor: 12_000_00, + count: 6, + daysAgo: 20, + category: "diger-gider", + by: "emre", + }, +]; + +export async function seedInstallments( + householdId: Types.ObjectId, + userIds: Map, +): Promise { + const today = todayIstanbul(); + const card = await AccountModel.findOne({ householdId, name: "Garanti Bonus" }).lean(); + if (!card) return 0; + let created = 0; + for (const p of PURCHASES) { + const exists = await TransactionModel.exists({ + householdId, + payeeName: p.payee, + "installment.planId": { $type: "objectId" }, + }); + if (exists) continue; + const category = await CategoryModel.findOne({ householdId, defaultKey: p.category }).lean(); + const r = await createTransaction(userIds.get(p.by)!, String(householdId), { + type: "expense", + amount: p.amountMinor, + date: addDays(today, -p.daysAgo), + accountId: String(card._id), + toAccountId: null, + categoryId: String(category!._id), + splits: [], + payee: p.payee, + description: p.description, + tags: [], + installments: p.count, + attachmentIds: [], + }); + if (!r.ok) throw new Error(`Seed taksiti oluşturulamadı (${p.payee}): ${r.error}`); + created++; + } + return created; +} diff --git a/scripts/seed/run.int.test.ts b/scripts/seed/run.int.test.ts index 26e97a3..c84b071 100644 --- a/scripts/seed/run.int.test.ts +++ b/scripts/seed/run.int.test.ts @@ -46,11 +46,27 @@ describe("seed", () => { accounts: 0, transactions: 0, recurringRules: 0, + installmentPlans: 0, }); expect(await getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1); expect(await HouseholdModel.countDocuments()).toBe(SEED_HOUSEHOLDS.length); }); + it("aynı aydaki transferlerin hepsi oluşur (seedRef çakışması yok)", async () => { + const tx = getDb().collection("transactions"); + const months = new Set( + (await tx.find({ description: "ATM" }).toArray()).map((t) => t.seedRef.split(":")[1]), + ); + expect(months.size).toBeGreaterThanOrEqual(5); + // Her ay ATM'nin yanında kart ödemesi ve birikim de var (ayın 7'si geçmişse). + expect(await tx.countDocuments({ description: "Kart borcu ödemesi" })).toBeGreaterThanOrEqual( + months.size - 1, + ); + expect(await tx.countDocuments({ description: "Aylık birikim" })).toBeGreaterThanOrEqual( + months.size - 1, + ); + }); + it("roller ve askı durumu doğru", async () => { const users = await getDb() .collection("user") diff --git a/scripts/seed/run.ts b/scripts/seed/run.ts index 348f7e2..d1ba35a 100644 --- a/scripts/seed/run.ts +++ b/scripts/seed/run.ts @@ -10,6 +10,7 @@ import { HouseholdModel } from "@/models/household"; import { HouseholdMemberModel } from "@/models/household-member"; import { storedEmail } from "@/shared/account-email"; import { runMigrations } from "../migrations"; +import { seedInstallments } from "./installments"; import { seedRecurring } from "./recurring"; import { seedTransactions } from "./transactions"; import { @@ -45,6 +46,7 @@ export type SeedSummary = { accounts: number; transactions: number; recurringRules: number; + installmentPlans: number; }; accounts: SeedAccountRow[]; }; @@ -61,6 +63,7 @@ const DOMAIN_COLLECTIONS = [ "attachments", "reconciliations", "recurringRules", + "statements", ]; type AuthApi = ReturnType["api"]; @@ -111,6 +114,7 @@ export async function runSeed({ accounts: 0, transactions: 0, recurringRules: 0, + installmentPlans: 0, }, accounts: [], }; @@ -279,6 +283,12 @@ export async function runSeed({ summary.created.recurringRules = await seedRecurring(householdIds, userIdMap); if (summary.created.recurringRules) log(`✓ Örnek düzenli işlemler: ${summary.created.recurringRules}`); + summary.created.installmentPlans = await seedInstallments( + householdIds.get("Çakır Ailesi")!, + userIdMap, + ); + if (summary.created.installmentPlans) + log(`✓ Örnek taksitli alışverişler: ${summary.created.installmentPlans}`); // 7) Geliştirme hesapları tablosu const all = [ diff --git a/scripts/seed/transactions.ts b/scripts/seed/transactions.ts index 4b22ee7..c59932f 100644 --- a/scripts/seed/transactions.ts +++ b/scripts/seed/transactions.ts @@ -166,7 +166,8 @@ function cakirAiles(): SeedTx[] { by: "emre", }); - // Transferler: nakit çekme, kart ödemesi, birikim. + // Transferler: nakit çekme, kart ödemesi, birikim. Satıcı/kategori olmadığından seedRef + // türden üretilir; çakışmasın diye sıra numarası verilir (ATM 0 — mevcut veriyle uyumlu). add({ type: "transfer", ymd: d(2), @@ -176,24 +177,31 @@ function cakirAiles(): SeedTx[] { description: "ATM", by: "admin", }); - add({ - type: "transfer", - ymd: d(9), - amountMinor: tl(11_000, 15_000), - account: "Ziraat Maaş", - toAccount: "Garanti Bonus", - description: "Kart borcu ödemesi", - by: "admin", - }); - add({ - type: "transfer", - ymd: d(16), - amountMinor: 5_000_00, - account: "Ziraat Maaş", - toAccount: "Birikim (Vadeli)", - description: "Aylık birikim", - by: "ayse", - }); + add( + { + type: "transfer", + // Kesim 29, son ödeme ~8: ödeme son ödeme gününden önce. + ymd: d(7), + amountMinor: tl(11_000, 15_000), + account: "Ziraat Maaş", + toAccount: "Garanti Bonus", + description: "Kart borcu ödemesi", + by: "admin", + }, + 1, + ); + add( + { + type: "transfer", + ymd: d(16), + amountMinor: 5_000_00, + account: "Ziraat Maaş", + toAccount: "Birikim (Vadeli)", + description: "Aylık birikim", + by: "ayse", + }, + 2, + ); // Market: haftada 1–2; bir kısmı bölünmüş. const marketCount = faker.number.int({ min: 6, max: 8 });