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:
@@ -0,0 +1,156 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "AccountingConnection" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL DEFAULT 'tahesab',
|
||||
"label" TEXT NOT NULL,
|
||||
"baseUrl" TEXT NOT NULL,
|
||||
"dbName" TEXT NOT NULL,
|
||||
"tokenCiphertext" TEXT NOT NULL,
|
||||
"tokenIv" TEXT NOT NULL,
|
||||
"tokenTag" TEXT NOT NULL,
|
||||
"tokenLast4" TEXT NOT NULL DEFAULT '',
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||
"allowInsecureTls" BOOLEAN NOT NULL DEFAULT false,
|
||||
"lastHealthAt" TIMESTAMP(3),
|
||||
"lastHealthOk" BOOLEAN,
|
||||
"lastError" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "AccountingConnection_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PersonAccountingLink" (
|
||||
"id" TEXT NOT NULL,
|
||||
"personId" TEXT NOT NULL,
|
||||
"connectionId" TEXT NOT NULL,
|
||||
"moshtariCode" INTEGER NOT NULL,
|
||||
"matchMethod" TEXT NOT NULL,
|
||||
"linkedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastBalanceSyncedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "PersonAccountingLink_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AccountingDocumentLog" (
|
||||
"id" TEXT NOT NULL,
|
||||
"connectionId" TEXT NOT NULL,
|
||||
"personId" TEXT,
|
||||
"docType" TEXT NOT NULL,
|
||||
"factorCode" TEXT NOT NULL,
|
||||
"requestPayload" JSONB NOT NULL,
|
||||
"responsePayload" JSONB,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"tahesabOk" TEXT,
|
||||
"tahesabFactorId" INTEGER,
|
||||
"errorMessage" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "AccountingDocumentLog_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AccountingBalanceCache" (
|
||||
"id" TEXT NOT NULL,
|
||||
"linkId" TEXT NOT NULL,
|
||||
"connectionId" TEXT NOT NULL,
|
||||
"goldBalance" DOUBLE PRECISION,
|
||||
"rialBalance" DOUBLE PRECISION,
|
||||
"currencyBalance" DOUBLE PRECISION,
|
||||
"totalGoldEquiv" DOUBLE PRECISION,
|
||||
"mandehDetail" JSONB NOT NULL DEFAULT '[]',
|
||||
"rawResponse" JSONB,
|
||||
"fetchedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "AccountingBalanceCache_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AccountingSyncJob" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"connectionId" TEXT,
|
||||
"kind" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"request" JSONB NOT NULL DEFAULT '{}',
|
||||
"result" JSONB,
|
||||
"errorMessage" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "AccountingSyncJob_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AccountingConnection_organizationId_enabled_idx" ON "AccountingConnection"("organizationId", "enabled");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AccountingConnection_organizationId_isDefault_idx" ON "AccountingConnection"("organizationId", "isDefault");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AccountingConnection_organizationId_baseUrl_dbName_key" ON "AccountingConnection"("organizationId", "baseUrl", "dbName");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PersonAccountingLink_personId_idx" ON "PersonAccountingLink"("personId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PersonAccountingLink_connectionId_idx" ON "PersonAccountingLink"("connectionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PersonAccountingLink_personId_connectionId_key" ON "PersonAccountingLink"("personId", "connectionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PersonAccountingLink_connectionId_moshtariCode_key" ON "PersonAccountingLink"("connectionId", "moshtariCode");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AccountingDocumentLog_connectionId_status_idx" ON "AccountingDocumentLog"("connectionId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AccountingDocumentLog_personId_idx" ON "AccountingDocumentLog"("personId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AccountingDocumentLog_docType_idx" ON "AccountingDocumentLog"("docType");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AccountingDocumentLog_connectionId_factorCode_key" ON "AccountingDocumentLog"("connectionId", "factorCode");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AccountingBalanceCache_linkId_key" ON "AccountingBalanceCache"("linkId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AccountingBalanceCache_connectionId_idx" ON "AccountingBalanceCache"("connectionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AccountingSyncJob_organizationId_createdAt_idx" ON "AccountingSyncJob"("organizationId", "createdAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AccountingConnection" ADD CONSTRAINT "AccountingConnection_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PersonAccountingLink" ADD CONSTRAINT "PersonAccountingLink_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PersonAccountingLink" ADD CONSTRAINT "PersonAccountingLink_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "AccountingConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AccountingDocumentLog" ADD CONSTRAINT "AccountingDocumentLog_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "AccountingConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AccountingDocumentLog" ADD CONSTRAINT "AccountingDocumentLog_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AccountingBalanceCache" ADD CONSTRAINT "AccountingBalanceCache_linkId_fkey" FOREIGN KEY ("linkId") REFERENCES "PersonAccountingLink"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AccountingBalanceCache" ADD CONSTRAINT "AccountingBalanceCache_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "AccountingConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AccountingSyncJob" ADD CONSTRAINT "AccountingSyncJob_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user