Files
doc-manager/apps/api/prisma/schema.prisma
T
admin c2fcdec85d feat(orders): Site/Order/OrderItem + S2S incoming endpoint + Tochka webhook receiver
Schema:
- Site (organizationId, name, slug, domain, apiKey, defaultOfferTemplateId)
- Order (full customer fields, status enum, totalCents/vatCents, projectId link, rawPayload)
- OrderItem (orderId, position, name, serviceId, qty, unit, price, vat, eventDate)
- Migration 3_orders + OrderStatus enum

API:
- /api/sites — CRUD with apiKey shown only on create/regenerate
- /api/orders — list/get/convert-to-project (creates project + matches/creates client by INN)
- POST /api/incoming/orders — S2S, X-Site-Key header → resolves Site → creates Order
- POST /webhooks/tochka/<secret> — receives raw, dedupes, parses paymentId+purpose,
  matches by document number regex, creates Payment, updates Document status
  (paid/partially_paid), propagates Order.status=paid when fully covered

Web:
- /sites page: list + add site (paste-friendly modal with API key + curl example
  shown once after create/regenerate)
- /orders page: filterable list, link to project
- /orders/🆔 view with items + "Перевести в проект" button (creates project,
  upserts client by INN, links project<-order)
- Nav: «Заявки» and «Сайты» added

Manual demo flow:
1. /sites → add «Голосования» slug=voting → save the apiKey
2. curl POST /api/incoming/orders with X-Site-Key → order appears in /orders
3. Open order → «Перевести в проект» → project created with client+default
4. Create invoice document in project → «Выставить через Точку»
5. Webhook from sandbox/prod → document.status=paid → order.status=paid

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-16 15:00:24 +03:00

403 lines
13 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
}
enum OrderStatus {
new // только пришла с сайта
accepted // менеджер принял (или клиент акцептовал оферту)
invoiced // выставлен счёт
paid // оплачено
fulfilled // услуга оказана, документы закрыты
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[]
sites Site[]
orders Order[]
}
model Site {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String // напр. "Голосования", "PrezLoad"
slug String // напр. "voting", "prezload" — используется в URL и человекочитаемых местах
domain String? // напр. "voting.queo.ru" — для верификации/CORS
apiKey String @unique // S2S ключ (длинная строка)
defaultOfferTemplateId String? @db.Uuid
defaultOfferTemplate DocumentTemplate? @relation("OfferTemplate", fields: [defaultOfferTemplateId], references: [id])
archivedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
@@unique([organizationId, slug])
@@index([organizationId])
}
model Order {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
siteId String? @db.Uuid
site Site? @relation(fields: [siteId], references: [id])
projectId String? @db.Uuid // когда сконвертирована в проект
project Project? @relation(fields: [projectId], references: [id])
status OrderStatus @default(new)
// данные клиента из заявки (могут не совпадать с записью Client — пока не привязан)
customerName String
customerInn String?
customerKpp String?
customerEmail String?
customerPhone String?
customerAddress String?
customerKind ClientKind @default(ul)
totalCents BigInt @default(0)
vatCents BigInt @default(0)
currency String @default("RUB")
acceptedOfferAt DateTime? // когда клиент принял оферту на сайте
notes String?
rawPayload Json? // что прислал сайт целиком — на случай неполного парсинга
archivedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items OrderItem[]
@@index([organizationId, status])
@@index([organizationId, siteId])
@@index([projectId])
}
model OrderItem {
id String @id @default(uuid()) @db.Uuid
orderId String @db.Uuid
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
position Int
name String // отображаемое название позиции
serviceId String? @db.Uuid // привязка к каталогу, если совпало
service ServiceCatalog? @relation(fields: [serviceId], references: [id])
qtyMilli BigInt @default(1000)
unit String @default("шт")
priceCents BigInt
vat VatRate @default(none)
sumCents BigInt
eventDate DateTime? // для услуг с привязкой к дате мероприятия (голосование 14 мая и т.д.)
@@index([orderId])
@@index([serviceId])
}
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[]
orders Order[]
@@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[]
orderItems OrderItem[]
@@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[]
defaultForSites Site[] @relation("OfferTemplate")
@@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])
}