Files
doc-manager/apps/api/prisma/schema.prisma
T
admin 524789facc feat: multi-organization support + bank accounts
- Renamed singular /api/organization → CRUD /api/organizations
- New BankAccount table with CRUD under /api/organizations/:orgId/bank-accounts
- TochkaCredential gets optional bankAccountId for future per-account bank API config
- Active organization stored in cookie dm_org, /api/active-organization GET/POST
- activeOrgPlugin resolves req._orgId from cookie (or first non-archived as fallback)
- Migration 1_multiorg: BankAccount table + data backfill from legacy Organization.bank* fields
- Web: new /companies list + /companies/:id with tabs (Реквизиты, Банки и счета, Интеграции stub)
- Web: OrgSwitcher dropdown in header (active org + management link)
- Removed nav "Реквизиты", "Банк" — replaced by "Компании"
- Per-field error highlighting wired up on new forms

Existing organization data backfills cleanly: legacy bank* fields stay readable, but new
BankAccount becomes the source of truth going forward.

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

278 lines
8.0 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
}
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[]
}
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[]
@@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[]
@@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
@@index([organizationId, docType])
}
model Document {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], 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([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])
}