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:
@@ -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;
|
||||||
|
}
|
||||||
@@ -46,11 +46,27 @@ describe("seed", () => {
|
|||||||
accounts: 0,
|
accounts: 0,
|
||||||
transactions: 0,
|
transactions: 0,
|
||||||
recurringRules: 0,
|
recurringRules: 0,
|
||||||
|
installmentPlans: 0,
|
||||||
});
|
});
|
||||||
expect(await getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1);
|
expect(await getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1);
|
||||||
expect(await HouseholdModel.countDocuments()).toBe(SEED_HOUSEHOLDS.length);
|
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 () => {
|
it("roller ve askı durumu doğru", async () => {
|
||||||
const users = await getDb()
|
const users = await getDb()
|
||||||
.collection("user")
|
.collection("user")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { HouseholdModel } from "@/models/household";
|
|||||||
import { HouseholdMemberModel } from "@/models/household-member";
|
import { HouseholdMemberModel } from "@/models/household-member";
|
||||||
import { storedEmail } from "@/shared/account-email";
|
import { storedEmail } from "@/shared/account-email";
|
||||||
import { runMigrations } from "../migrations";
|
import { runMigrations } from "../migrations";
|
||||||
|
import { seedInstallments } from "./installments";
|
||||||
import { seedRecurring } from "./recurring";
|
import { seedRecurring } from "./recurring";
|
||||||
import { seedTransactions } from "./transactions";
|
import { seedTransactions } from "./transactions";
|
||||||
import {
|
import {
|
||||||
@@ -45,6 +46,7 @@ export type SeedSummary = {
|
|||||||
accounts: number;
|
accounts: number;
|
||||||
transactions: number;
|
transactions: number;
|
||||||
recurringRules: number;
|
recurringRules: number;
|
||||||
|
installmentPlans: number;
|
||||||
};
|
};
|
||||||
accounts: SeedAccountRow[];
|
accounts: SeedAccountRow[];
|
||||||
};
|
};
|
||||||
@@ -61,6 +63,7 @@ const DOMAIN_COLLECTIONS = [
|
|||||||
"attachments",
|
"attachments",
|
||||||
"reconciliations",
|
"reconciliations",
|
||||||
"recurringRules",
|
"recurringRules",
|
||||||
|
"statements",
|
||||||
];
|
];
|
||||||
|
|
||||||
type AuthApi = ReturnType<typeof getAuth>["api"];
|
type AuthApi = ReturnType<typeof getAuth>["api"];
|
||||||
@@ -111,6 +114,7 @@ export async function runSeed({
|
|||||||
accounts: 0,
|
accounts: 0,
|
||||||
transactions: 0,
|
transactions: 0,
|
||||||
recurringRules: 0,
|
recurringRules: 0,
|
||||||
|
installmentPlans: 0,
|
||||||
},
|
},
|
||||||
accounts: [],
|
accounts: [],
|
||||||
};
|
};
|
||||||
@@ -279,6 +283,12 @@ export async function runSeed({
|
|||||||
summary.created.recurringRules = await seedRecurring(householdIds, userIdMap);
|
summary.created.recurringRules = await seedRecurring(householdIds, userIdMap);
|
||||||
if (summary.created.recurringRules)
|
if (summary.created.recurringRules)
|
||||||
log(`✓ Örnek düzenli işlemler: ${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
|
// 7) Geliştirme hesapları tablosu
|
||||||
const all = [
|
const all = [
|
||||||
|
|||||||
@@ -166,7 +166,8 @@ function cakirAiles(): SeedTx[] {
|
|||||||
by: "emre",
|
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({
|
add({
|
||||||
type: "transfer",
|
type: "transfer",
|
||||||
ymd: d(2),
|
ymd: d(2),
|
||||||
@@ -176,16 +177,21 @@ function cakirAiles(): SeedTx[] {
|
|||||||
description: "ATM",
|
description: "ATM",
|
||||||
by: "admin",
|
by: "admin",
|
||||||
});
|
});
|
||||||
add({
|
add(
|
||||||
|
{
|
||||||
type: "transfer",
|
type: "transfer",
|
||||||
ymd: d(9),
|
// Kesim 29, son ödeme ~8: ödeme son ödeme gününden önce.
|
||||||
|
ymd: d(7),
|
||||||
amountMinor: tl(11_000, 15_000),
|
amountMinor: tl(11_000, 15_000),
|
||||||
account: "Ziraat Maaş",
|
account: "Ziraat Maaş",
|
||||||
toAccount: "Garanti Bonus",
|
toAccount: "Garanti Bonus",
|
||||||
description: "Kart borcu ödemesi",
|
description: "Kart borcu ödemesi",
|
||||||
by: "admin",
|
by: "admin",
|
||||||
});
|
},
|
||||||
add({
|
1,
|
||||||
|
);
|
||||||
|
add(
|
||||||
|
{
|
||||||
type: "transfer",
|
type: "transfer",
|
||||||
ymd: d(16),
|
ymd: d(16),
|
||||||
amountMinor: 5_000_00,
|
amountMinor: 5_000_00,
|
||||||
@@ -193,7 +199,9 @@ function cakirAiles(): SeedTx[] {
|
|||||||
toAccount: "Birikim (Vadeli)",
|
toAccount: "Birikim (Vadeli)",
|
||||||
description: "Aylık birikim",
|
description: "Aylık birikim",
|
||||||
by: "ayse",
|
by: "ayse",
|
||||||
});
|
},
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
// Market: haftada 1–2; bir kısmı bölünmüş.
|
// Market: haftada 1–2; bir kısmı bölünmüş.
|
||||||
const marketCount = faker.number.int({ min: 6, max: 8 });
|
const marketCount = faker.number.int({ min: 6, max: 8 });
|
||||||
|
|||||||
Reference in New Issue
Block a user