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>
This commit is contained in:
@@ -61,6 +61,15 @@ enum ProjectStatus {
|
||||
cancelled
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
new // только пришла с сайта
|
||||
accepted // менеджер принял (или клиент акцептовал оферту)
|
||||
invoiced // выставлен счёт
|
||||
paid // оплачено
|
||||
fulfilled // услуга оказана, документы закрыты
|
||||
cancelled
|
||||
}
|
||||
|
||||
model Organization {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
name String
|
||||
@@ -89,6 +98,81 @@ model Organization {
|
||||
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 {
|
||||
@@ -110,6 +194,7 @@ model Project {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
documents Document[]
|
||||
orders Order[]
|
||||
|
||||
@@index([organizationId, archivedAt])
|
||||
@@index([organizationId, status])
|
||||
@@ -173,6 +258,7 @@ model ServiceCatalog {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
lines DocumentLine[]
|
||||
orderItems OrderItem[]
|
||||
|
||||
@@index([organizationId])
|
||||
@@index([organizationId, archivedAt])
|
||||
@@ -189,6 +275,7 @@ model DocumentTemplate {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
defaultForProjects Project[]
|
||||
defaultForSites Site[] @relation("OfferTemplate")
|
||||
|
||||
@@index([organizationId, docType])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user