fix(seed): çakışan transfer seedRef'leri; örnek taksitli alışverişler

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 11:19:17 +03:00
parent 0ad52757ad
commit 6ae98a1cb4
4 changed files with 121 additions and 19 deletions
+68
View File
@@ -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<string, string>,
): Promise<number> {
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;
}
+16
View File
@@ -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")
+10
View File
@@ -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<typeof getAuth>["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 = [
+27 -19
View File
@@ -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 });