c2fcdec85d
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>
403 lines
13 KiB
Plaintext
403 lines
13 KiB
Plaintext
// 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])
|
||
}
|