feat: add internal finance ledger for accounts, claims, and payroll

Keep booked income and expense separate from cash balances, with installments, recurring items, and reports.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-18 18:49:49 +03:30
co-authored by Cursor
parent 61d17c5cea
commit ab538b8304
26 changed files with 3836 additions and 0 deletions
+243
View File
@@ -203,6 +203,14 @@ model Organization {
webhookEndpoints WebhookEndpoint[]
smsCampaigns SmsCampaign[]
outboundSmsLogs OutboundSmsLog[]
financialAccounts FinancialAccount[]
financialCategories FinancialCategory[]
financialDocuments FinancialDocument[]
financialMovements FinancialMovement[]
financialReceivables FinancialReceivable[]
financialPayrolls FinancialPayroll[]
financialRecurringItems FinancialRecurringItem[]
financialReconciliations FinancialReconciliation[]
wooCommerceConnections WooCommerceConnection[]
montifyConnections MontifyConnection[]
voipConnections VoipConnection[]
@@ -543,6 +551,8 @@ model Person {
trackingEvents TrackingEvent[]
routineTasks RoutineTask[]
outboundSmsLogs OutboundSmsLog[]
financialDocuments FinancialDocument[]
financialReceivables FinancialReceivable[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -1991,6 +2001,239 @@ model WebhookDelivery {
@@index([endpointId, createdAt])
}
/// دفتر مالی داخلی: حساب صندوق/بانک/تنخواه
model FinancialAccount {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
/// bank | cash | petty
type String
bankName String?
accountRef String?
responsibleName String?
openingBalance Decimal @default(0) @db.Decimal(18, 2)
openingDate DateTime
notes String?
isActive Boolean @default(true)
currency String @default("IRR")
createdById String?
movements FinancialMovement[]
documents FinancialDocument[] @relation("DocumentAccount")
reconciliations FinancialReconciliation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive])
}
model FinancialCategory {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
/// income | expense
kind String
isActive Boolean @default(true)
documents FinancialDocument[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, kind, name])
@@index([organizationId, kind])
}
/// درآمد یا هزینهٔ ثبت‌شده (تعهدی). موجودی حساب را خودش عوض نمی‌کند.
model FinancialDocument {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
/// income | expense
kind String
title String
amount Decimal @db.Decimal(18, 2)
bookedAt DateTime
dueAt DateTime?
categoryId String?
category FinancialCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
partyName String?
accountId String?
account FinancialAccount? @relation("DocumentAccount", fields: [accountId], references: [id], onDelete: SetNull)
/// cash | card_to_card | pos | transfer | other
method String?
/// open | partial | settled | void
status String @default("open")
reference String?
notes String?
createdById String?
voidedAt DateTime?
voidReason String?
movements FinancialMovement[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, kind, bookedAt])
@@index([organizationId, status])
@@index([personId])
}
/// گردش قطعی حساب. افتتاحیه، دریافت، پرداخت، انتقال.
model FinancialMovement {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
accountId String
account FinancialAccount @relation(fields: [accountId], references: [id], onDelete: Cascade)
documentId String?
document FinancialDocument? @relation(fields: [documentId], references: [id], onDelete: SetNull)
receivableId String?
receivable FinancialReceivable? @relation(fields: [receivableId], references: [id], onDelete: SetNull)
installmentId String?
installment FinancialInstallment? @relation(fields: [installmentId], references: [id], onDelete: SetNull)
payrollId String?
payroll FinancialPayroll? @relation(fields: [payrollId], references: [id], onDelete: SetNull)
/// in | out
direction String
amount Decimal @db.Decimal(18, 2)
/// opening | receipt | payment | transfer_in | transfer_out
kind String
transferGroupId String?
occurredAt DateTime
memo String?
reference String?
createdById String?
voidedAt DateTime?
createdAt DateTime @default(now())
@@index([organizationId, accountId, occurredAt])
@@index([transferGroupId])
@@index([documentId])
@@index([receivableId])
@@index([installmentId])
@@index([payrollId])
}
model FinancialPayroll {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
userId String
period String
baseAmount Decimal @db.Decimal(18, 2)
benefits Decimal @default(0) @db.Decimal(18, 2)
bonus Decimal @default(0) @db.Decimal(18, 2)
deductions Decimal @default(0) @db.Decimal(18, 2)
payable Decimal @db.Decimal(18, 2)
dueDate DateTime
notes String?
/// open | partial | settled | void
status String @default("open")
createdById String?
voidedAt DateTime?
movements FinancialMovement[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, userId, period])
@@index([organizationId, status])
}
model FinancialRecurringItem {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
title String
/// income | expense
kind String
amount Decimal @db.Decimal(18, 2)
/// weekly | monthly | yearly
interval String
startDate DateTime
endDate DateTime?
nextDue DateTime
accountId String?
categoryId String?
isActive Boolean @default(true)
createdById String?
occurrences FinancialRecurringOccurrence[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive, nextDue])
}
model FinancialRecurringOccurrence {
id String @id @default(cuid())
itemId String
item FinancialRecurringItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
periodKey String
dueDate DateTime
documentId String?
/// pending | confirmed
status String @default("pending")
createdAt DateTime @default(now())
@@unique([itemId, periodKey])
}
model FinancialReconciliation {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
accountId String
account FinancialAccount @relation(fields: [accountId], references: [id], onDelete: Cascade)
reconciledAt DateTime
statedBalance Decimal @db.Decimal(18, 2)
systemBalance Decimal @db.Decimal(18, 2)
difference Decimal @db.Decimal(18, 2)
notes String?
createdById String?
createdAt DateTime @default(now())
@@index([organizationId, accountId, reconciledAt])
}
/// قرارداد یا طلب اقساطی دفتر داخلی
model FinancialReceivable {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
title String
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
partyName String?
totalAmount Decimal @db.Decimal(18, 2)
notes String?
/// open | settled | void
status String @default("open")
createdById String?
voidedAt DateTime?
installments FinancialInstallment[]
movements FinancialMovement[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, status])
@@index([personId])
}
model FinancialInstallment {
id String @id @default(cuid())
receivableId String
receivable FinancialReceivable @relation(fields: [receivableId], references: [id], onDelete: Cascade)
position Int
dueDate DateTime
amount Decimal @db.Decimal(18, 2)
reminderNotifiedAt DateTime?
movements FinancialMovement[]
createdAt DateTime @default(now())
@@unique([receivableId, position])
@@index([dueDate])
}
/// لاگ یکپارچهٔ پیامک خروجی (دستی / کمپین / اتوماسیون)
model OutboundSmsLog {
id String @id @default(cuid())