Files
doc-manager/apps/api/prisma/schema.prisma
T
admin b2c221e643 feat: Projects + stronger LLM placeholder substitution
Backend:
- Project model with defaults: defaultClient, defaultTemplate, defaultBankAccount
- Document gets optional projectId (FK + index)
- Migration 2_projects: enum ProjectStatus + Project table + Document.projectId
- API: /api/projects CRUD, GET /api/projects/:id/documents
- documents/routes filter by projectId

LLM prompt:
- Concrete preamble example: «ООО «...», в лице директора ... , ИП ..., ОГРНИП ...»
  → {{customer.name}}, {{customer.signatoryPosition}} {{customer.signatoryName}},
    {{executor.name}}, {{executor.ogrn}}
- Expanded placeholder list (signatoryName/Position для customer, ogrn etc)
- Side-role detection: «Заказчик» vs «Исполнитель» map to customer/executor

Frontend:
- /projects (list) and /projects/:id (defaults form + documents list under project)
- Nav: «Проекты» first
- DocumentEdit reads projectId from URL, presets default client from project,
  saves projectId with the document

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 13:17:07 +03:00

316 lines
9.5 KiB
Plaintext
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.
// Doc_manager — модель данных
// Полиморфная таблица documents для contract/invoice/act/upd.
// organization_id присутствует на всех owner-scoped таблицах (single-tenant v1, готово к multi-tenant).
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum DocType {
contract
invoice
act
upd
}
enum DocStatus {
draft
issued
sent
partially_paid
paid
cancelled
signed
}
enum VatRate {
none
vat_0 @map("0")
vat_5 @map("5")
vat_7 @map("7")
vat_10 @map("10")
vat_20 @map("20")
}
enum ClientKind {
ul // юр.лицо
ip // ИП
fl // физ.лицо
}
enum TochkaEnv {
sandbox
prod
}
enum PaymentKind {
incoming
incoming_sbp
incoming_sbp_b2b
outgoing
}
enum ProjectStatus {
active
completed
cancelled
}
model Organization {
id String @id @default(uuid()) @db.Uuid
name String
shortName String? // короткое имя для бейджей в UI / селектора
inn String
kpp String?
ogrn String?
legalAddress String?
// Поля bankName/bankBik/bankAccount устарели после введения BankAccount.
// Оставлены для обратной совместимости с уже сохранёнными данными.
bankName String?
bankBik String?
bankAccount String?
signatoryName String?
signatoryPosition String?
archivedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
clients Client[]
servicesCatalog ServiceCatalog[]
templates DocumentTemplate[]
documents Document[]
payments Payment[]
tochkaCredentials TochkaCredential[]
auditLog AuditLog[]
bankAccounts BankAccount[]
projects Project[]
}
model Project {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
status ProjectStatus @default(active)
// Дефолты — подставляются при создании документов внутри проекта
defaultClientId String? @db.Uuid
defaultClient Client? @relation(fields: [defaultClientId], references: [id])
defaultTemplateId String? @db.Uuid
defaultTemplate DocumentTemplate? @relation(fields: [defaultTemplateId], references: [id])
defaultBankAccountId String? @db.Uuid
defaultBankAccount BankAccount? @relation(fields: [defaultBankAccountId], references: [id])
notes String?
archivedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
documents Document[]
@@index([organizationId, archivedAt])
@@index([organizationId, status])
}
model BankAccount {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String // отображаемое имя, напр. "Точка — основной"
bankName String?
bankBik String?
accountNumber String? // р/счёт (20 цифр)
corrAccount String? // к/счёт (20 цифр)
currency String @default("RUB")
isPrimary Boolean @default(false)
archivedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tochkaCredentials TochkaCredential[]
defaultForProjects Project[]
@@index([organizationId])
}
model Client {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id])
kind ClientKind
name String
inn String?
kpp String?
address String?
email String?
phone String?
contactPerson String?
requisitesJson Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
documents Document[]
defaultForProjects Project[]
@@index([organizationId])
@@index([organizationId, name])
}
model ServiceCatalog {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id])
name String
unit String // "шт", "час", "мес"
defaultPriceCents BigInt @default(0)
defaultVat VatRate @default(none)
notes String?
archivedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lines DocumentLine[]
@@index([organizationId])
@@index([organizationId, archivedAt])
}
model DocumentTemplate {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id])
docType DocType
name String
body Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
defaultForProjects Project[]
@@index([organizationId, docType])
}
model Document {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id])
projectId String? @db.Uuid
project Project? @relation(fields: [projectId], references: [id])
docType DocType
number String
issuedAt DateTime?
status DocStatus @default(draft)
clientId String? @db.Uuid
client Client? @relation(fields: [clientId], references: [id])
parentDocumentId String? @db.Uuid
parent Document? @relation("DocumentChildren", fields: [parentDocumentId], references: [id])
children Document[] @relation("DocumentChildren")
body Json
totalCents BigInt @default(0)
vatCents BigInt @default(0)
currency String @default("RUB")
tochkaDocumentId String?
tochkaEnvironment TochkaEnv?
pdfPath String?
createdBy String? // sub из JWT auth.queo.ru
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lines DocumentLine[]
payments Payment[]
@@unique([organizationId, docType, number])
@@index([organizationId, clientId, issuedAt(sort: Desc)])
@@index([organizationId, status])
@@index([organizationId, projectId])
@@index([tochkaDocumentId])
}
model DocumentLine {
id String @id @default(uuid()) @db.Uuid
documentId String @db.Uuid
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
position Int
serviceId String? @db.Uuid
service ServiceCatalog? @relation(fields: [serviceId], references: [id])
name String
// Количество хранится как milli-units (тысячные), чтобы поддержать дробные количества без float.
qtyMilli BigInt @default(1000)
unit String
priceCents BigInt
vat VatRate @default(none)
sumCents BigInt
@@index([documentId])
@@index([serviceId])
}
model Payment {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id])
documentId String? @db.Uuid
document Document? @relation(fields: [documentId], references: [id])
tochkaPaymentId String @unique
kind PaymentKind
amountCents BigInt
payerInn String?
payerName String?
purpose String?
paidAt DateTime?
raw Json
createdAt DateTime @default(now())
@@index([organizationId, paidAt(sort: Desc)])
@@index([documentId])
}
model TochkaCredential {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id])
bankAccountId String? @db.Uuid
bankAccount BankAccount? @relation(fields: [bankAccountId], references: [id])
environment TochkaEnv
// AES-256-GCM ciphertext (iv|tag|ct), base64
jwtEncrypted String
customerCode String
accountCode String?
expiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, environment])
}
model WebhookEvent {
id String @id @default(uuid()) @db.Uuid
receivedAt DateTime @default(now())
source String // 'tochka'
eventType String
raw Json
processedAt DateTime?
error String?
dedupeKey String @unique
@@index([source, eventType, receivedAt])
}
model AuditLog {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id])
actorSub String?
action String
entity String
entityId String
diff Json?
at DateTime @default(now())
@@index([organizationId, at(sort: Desc)])
@@index([entity, entityId])
}