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 }, /** * Aynî borca bağlı para hareketi: birikim katkısı (hesap → birikim hesabı) veya ödeme. * `contactId` görünürlük için doludur ama `contactKind` boştur: cari TL bakiyesine girmez. */ inKindDebtId: { type: Schema.Types.ObjectId, default: null }, inKindRole: { type: String, enum: ["contribution", "payment", null], default: null }, /** * Varlık alımı (hesap → varlık) veya satışı (varlık → hesap): karşı hesap boş transfer. * Hareketi varlık sayfasından yönetilir; görünürlük varlığa bağlıdır. */ assetId: { type: Schema.Types.ObjectId, 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({ householdId: 1, assetId: 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; export const TransactionModel: Model = (models.Transaction as Model | undefined) ?? model("Transaction", transactionSchema);