Files
butcem/src/models/transaction.ts
T
suphicakir bd5a20ce64 feat(shopping): alışverişi tamamla transaction'ı ve karşılaştırma motoru
- tek transaction: stok alımı, fiş kaynaklı fiyat, mağaza başına bölünmüş gider, fiş ekleri, kalanların taşınması
- muadil, markasız ürün tipi ve plansız ekstra satırları; stok → gider kategorisi eşlemesi ve yedek kategori
- compareShopping saf fonksiyonu: aynı/muadil/alınmadı/ekstra, farkın nedenlere tam ayrışması
- tamamlanan listede rapor görüntüsü; testler

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 23:57:50 +03:00

113 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 },
/** Alışverişi tamamlamadan gelen mağaza gideri (Faz 13b). */
shoppingListId: { type: Schema.Types.ObjectId, default: null },
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<typeof transactionSchema>;
export const TransactionModel: Model<Transaction> =
(models.Transaction as Model<Transaction> | undefined) ??
model<Transaction>("Transaction", transactionSchema);