Add Tahesab jewelry accounting integration with multi-DB connections.

Encrypt tokens server-side, sync customers both ways, show balances/documents on the person Assets tab, and push sanads with idempotent Factor_Code logging.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-04 22:00:23 +03:30
co-authored by Cursor
parent d2f66d11fc
commit 4b47b753c3
26 changed files with 13784 additions and 8 deletions
+129 -6
View File
@@ -162,9 +162,11 @@ model Organization {
projectBoards ProjectBoard[]
projectCards ProjectCard[]
loyaltyTiers LoyaltyTier[]
automationRules AutomationRule[]
memoryItems MemoryItem[]
noteAiSuggestions NoteAiSuggestion[]
automationRules AutomationRule[]
memoryItems MemoryItem[]
noteAiSuggestions NoteAiSuggestion[]
accountingConnections AccountingConnection[]
accountingSyncJobs AccountingSyncJob[]
}
model User {
@@ -257,9 +259,11 @@ model Person {
owner User? @relation("PersonOwner", fields: [ownerId], references: [id], onDelete: SetNull)
tickets Ticket[]
projectCards ProjectCard[]
memoryItems MemoryItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
memoryItems MemoryItem[]
accountingLinks PersonAccountingLink[]
accountingDocuments AccountingDocumentLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@index([organizationId, email])
@@ -929,3 +933,122 @@ model AutomationRule {
@@index([organizationId, isActive, trigger])
}
/// اتصال به نرم‌افزار حسابداری خارجی (ته‌حساب) — چند اتصال/DB به‌ازای هر سازمان
model AccountingConnection {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
/// فعلاً فقط tahesab
provider String @default("tahesab")
label String
baseUrl String
dbName String
/// AES-256-GCM ciphertext (base64)
tokenCiphertext String
tokenIv String
tokenTag String
/// چهار کاراکتر آخر توکن برای نمایش ماسک‌شده
tokenLast4 String @default("")
enabled Boolean @default(true)
isDefault Boolean @default(false)
/// اگر true باشد verify SSL نادیده گرفته می‌شود (فقط برای نصب‌های داخلی با گواهی خودامضا)
allowInsecureTls Boolean @default(false)
lastHealthAt DateTime?
lastHealthOk Boolean?
lastError String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
personLinks PersonAccountingLink[]
documentLogs AccountingDocumentLog[]
balanceCaches AccountingBalanceCache[]
@@unique([organizationId, baseUrl, dbName])
@@index([organizationId, enabled])
@@index([organizationId, isDefault])
}
model PersonAccountingLink {
id String @id @default(cuid())
personId String
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
connectionId String
connection AccountingConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
/// کد مشتری در ته‌حساب (Moshtari_Code)
moshtariCode Int
/// manual | mobile | name | created
matchMethod String
linkedAt DateTime @default(now())
lastBalanceSyncedAt DateTime?
balanceCache AccountingBalanceCache?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([personId, connectionId])
@@unique([connectionId, moshtariCode])
@@index([personId])
@@index([connectionId])
}
model AccountingDocumentLog {
id String @id @default(cuid())
connectionId String
connection AccountingConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
/// مثلاً DoNewSanadBuySaleGOLD
docType String
/// کد یکتای ≥۲۰ کاراکتر — idempotency
factorCode String
requestPayload Json
responsePayload Json?
/// pending | success | failed
status String @default("pending")
tahesabOk String?
tahesabFactorId Int?
errorMessage String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([connectionId, factorCode])
@@index([connectionId, status])
@@index([personId])
@@index([docType])
}
model AccountingBalanceCache {
id String @id @default(cuid())
linkId String @unique
link PersonAccountingLink @relation(fields: [linkId], references: [id], onDelete: Cascade)
connectionId String
connection AccountingConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
goldBalance Float?
rialBalance Float?
currencyBalance Float?
/// مانده هم‌ارز طلا (MandeyeKolBeTALA) در صورت وجود
totalGoldEquiv Float?
/// جزئیات گروه‌بندی‌شده برای UI
mandehDetail Json @default("[]")
rawResponse Json?
fetchedAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([connectionId])
}
model AccountingSyncJob {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
connectionId String?
/// search | import | match
kind String
status String @default("pending")
request Json @default("{}")
result Json?
errorMessage String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, createdAt])
}