init: M1 scaffolding + M2 organization/clients/services CRUD
- monorepo (npm workspaces): apps/api (Fastify+Prisma+TS), apps/web (Vite+React+TS), packages/shared (zod schemas) - SSO via auth.queo.ru: jose+JWKS plugin, requireDocPermission(viewer|user|admin) - DEV_BYPASS_AUTH for local development (hard-checked off in production) - M2: organization upsert, clients CRUD with search, services catalog with soft-delete - BigInt -> Number serializer for Prisma money columns - Embedded Postgres + npm run dev:demo for one-command local boot - Docker compose for queoserver: postgres + api + web (nginx as ingress proxying /api -> api:3030) - First migration 0_init committed (prisma migrate diff) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
// 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
|
||||
inn String
|
||||
kpp String?
|
||||
ogrn String?
|
||||
legalAddress String?
|
||||
bankName String?
|
||||
bankBik String?
|
||||
bankAccount String?
|
||||
signatoryName String?
|
||||
signatoryPosition String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
clients Client[]
|
||||
servicesCatalog ServiceCatalog[]
|
||||
templates DocumentTemplate[]
|
||||
documents Document[]
|
||||
payments Payment[]
|
||||
tochkaCredentials TochkaCredential[]
|
||||
auditLog AuditLog[]
|
||||
}
|
||||
|
||||
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])
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user