5d370a062e
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
5.0 KiB
TypeScript
99 lines
5.0 KiB
TypeScript
import { model, models, Schema, type InferSchemaType, type Model } from "mongoose";
|
||
import { CONTACT_ENTRY_KINDS } from "@/shared/contacts";
|
||
import { TRANSACTION_STATUSES, TRANSACTION_TYPES } from "@/shared/transactions";
|
||
|
||
/**
|
||
* İşlem (Bölüm 6.3). `amountMinor` her zaman pozitif; işaret türden gelir (transactionEffects).
|
||
* Bakiyenin tek doğruluk kaynağıdır. Silme yumuşaktır (`deletedAt`).
|
||
*/
|
||
const splitSchema = new Schema(
|
||
{
|
||
categoryId: { type: Schema.Types.ObjectId, required: true },
|
||
amountMinor: { type: Number, required: true, min: 1 },
|
||
note: { type: String, default: "", maxlength: 120 },
|
||
},
|
||
{ _id: true },
|
||
);
|
||
|
||
const transactionSchema = new Schema(
|
||
{
|
||
householdId: { type: Schema.Types.ObjectId, required: true },
|
||
type: { type: String, enum: TRANSACTION_TYPES, required: true },
|
||
status: { type: String, enum: TRANSACTION_STATUSES, default: "posted" },
|
||
/** İstanbul gününün başlangıç anı (UTC). */
|
||
date: { type: Date, required: true },
|
||
/** Kaynak hesap. Cariden girişte (borç alma, tahsilat) null; hedef `toAccountId`. */
|
||
accountId: { type: Schema.Types.ObjectId, default: null },
|
||
toAccountId: { type: Schema.Types.ObjectId, default: null },
|
||
amountMinor: { type: Number, required: true, min: 1 },
|
||
/** Bölünmemiş gelir/giderde kategori; bölünmüşte null ve kalemler `splits`te. */
|
||
categoryId: { type: Schema.Types.ObjectId, default: null },
|
||
splits: { type: [splitSchema], default: [] },
|
||
/** Kategori filtresi/raporlar için: categoryId veya kalem kategorileri (tekilleştirilmiş). */
|
||
effectiveCategoryIds: { type: [Schema.Types.ObjectId], default: [] },
|
||
payeeId: { type: Schema.Types.ObjectId, default: null },
|
||
payeeName: { type: String, default: "", maxlength: 80 },
|
||
description: { type: String, default: "", maxlength: 500 },
|
||
/** Arama için normalize metin (açıklama + satıcı + kalem notları). */
|
||
searchText: { type: String, default: "" },
|
||
tags: { type: [String], default: [] },
|
||
/** Bağlı (silinmemiş) ek dosya sayısı; liste ikonu için (asıl kayıt attachments'ta). */
|
||
attachmentCount: { type: Number, default: 0 },
|
||
// İleri fazlar: planlı işlem kuralı (7), taksit (7), kart dönemi (7), cari (8).
|
||
recurringRuleId: { type: Schema.Types.ObjectId, default: null },
|
||
/** Serideki sıra (0'dan); kuralla birlikte benzersiz (tekrar üretimi idempotent). */
|
||
occurrenceIndex: { type: Number, default: null },
|
||
/** Tek başına düzenlendi/ertelendi: "tümü" güncellemesinde korunur. */
|
||
detached: { type: Boolean, default: false },
|
||
/** Planlı tutar tahmini (ör. fatura); onaylarken kesinleşir. */
|
||
isEstimate: { type: Boolean, default: false },
|
||
installment: {
|
||
type: new Schema(
|
||
{ planId: Schema.Types.ObjectId, index: Number, count: Number },
|
||
{ _id: false },
|
||
),
|
||
default: null,
|
||
},
|
||
statementId: { type: Schema.Types.ObjectId, default: null },
|
||
/** Cari transferi: karşı taraf kişi (hesabın biri boş). */
|
||
contactId: { type: Schema.Types.ObjectId, default: null },
|
||
contactKind: { type: String, enum: [...CONTACT_ENTRY_KINDS, null], default: null },
|
||
/**
|
||
* Mutabakatı yapılmış hesaplar (Bölüm 6.1). Transfer iki hesaba dokunduğu için hesap bazında
|
||
* tutulur. Tutar, tarih, tür veya hesap değişirse işaret kalkar.
|
||
*/
|
||
reconciledAccountIds: { type: [Schema.Types.ObjectId], default: [] },
|
||
/** Mutabakat düzeltme işlemi: kategorisiz; düzenlenemez, yalnızca silinebilir. */
|
||
isAdjustment: { type: Boolean, default: false },
|
||
deletedAt: { type: Date, default: null },
|
||
/** Seed verisinin idempotent yüklenmesi için sabit anahtar. */
|
||
seedRef: { type: String, default: null },
|
||
createdBy: { type: Schema.Types.ObjectId, required: true },
|
||
updatedBy: { type: Schema.Types.ObjectId, required: true },
|
||
},
|
||
{ timestamps: true, collection: "transactions" },
|
||
);
|
||
|
||
transactionSchema.index({ householdId: 1, deletedAt: 1, date: -1, createdAt: -1 });
|
||
transactionSchema.index({ householdId: 1, status: 1, date: 1 });
|
||
transactionSchema.index({ householdId: 1, accountId: 1, date: -1 });
|
||
transactionSchema.index({ householdId: 1, toAccountId: 1, date: -1 });
|
||
transactionSchema.index({ householdId: 1, effectiveCategoryIds: 1, date: -1 });
|
||
transactionSchema.index({ householdId: 1, tags: 1 });
|
||
transactionSchema.index({ householdId: 1, contactId: 1, date: 1 }, { sparse: true });
|
||
transactionSchema.index({ "installment.planId": 1 }, { sparse: true });
|
||
transactionSchema.index(
|
||
{ recurringRuleId: 1, occurrenceIndex: 1 },
|
||
{ unique: true, partialFilterExpression: { recurringRuleId: { $type: "objectId" } } },
|
||
);
|
||
transactionSchema.index(
|
||
{ householdId: 1, seedRef: 1 },
|
||
{ unique: true, partialFilterExpression: { seedRef: { $type: "string" } } },
|
||
);
|
||
|
||
export type Transaction = InferSchemaType<typeof transactionSchema>;
|
||
|
||
export const TransactionModel: Model<Transaction> =
|
||
(models.Transaction as Model<Transaction> | undefined) ??
|
||
model<Transaction>("Transaction", transactionSchema);
|