Fairflow — GraphQL Backend Specification v1¶
Источник: fairflow-business-process-v5.2.md
Стек: GraphQL (Apollo Server), MongoDB
Принципы: projectId в каждом CRM-документе, ownerId обязателен, soft delete, модульность
1. Общие типы и утилиты¶
1.1 Пагинация¶
input PaginationInput {
first: Int = 25
after: String
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
interface Connection {
edges: [Edge!]!
pageInfo: PageInfo!
}
interface Edge {
cursor: String!
node: Node!
}
interface Node {
id: ID!
}
1.2 Ошибки и валидация¶
type ValidationError {
field: String!
message: String!
}
type BusinessError {
code: String!
message: String!
}
union MutationResult = Success | ValidationError | BusinessError
1.3 Временные метки¶
scalar DateTime # ISO 8601, хранится в UTC
type Timestamps {
createdAt: DateTime!
updatedAt: DateTime!
}
2. Auth (Авторизация и аутентификация)¶
2.1 Types¶
type User {
id: ID!
email: String!
userName: String!
avatar: String
emailVerified: Boolean!
twoFactorEnabled: Boolean!
canCreatePersonalProjects: Boolean!
createdAt: DateTime!
}
type Session {
accessToken: String!
refreshToken: String
expiresAt: DateTime!
user: User!
}
type Workspace {
id: ID!
type: WorkspaceType!
name: String!
organizationId: ID
orgRole: OrgRole
}
enum WorkspaceType {
PERSONAL
ORGANIZATION
}
enum OrgRole {
PLATFORM_OWNER
PLATFORM_ADMIN
EMPLOYEE
}
type ProjectInfo {
id: ID!
name: String!
color: String!
workspaceId: ID!
role: ProjectRole!
}
enum ProjectRole {
OWNER
ADMIN
MANAGER
MEMBER
VIEWER
}
2.2 Queries¶
type Query {
me: User
myWorkspaces: [Workspace!]!
myProjects(workspaceId: ID): [ProjectInfo!]!
}
2.3 Mutations¶
input SignInInput {
email: String!
password: String!
}
input SignUpInput {
userName: String!
email: String!
password: String!
}
input ForgotPasswordInput {
email: String!
}
input ResetPasswordInput {
token: String!
password: String!
}
input VerifyEmailInput {
token: String!
}
input InviteAcceptInput {
token: String!
password: String # для нового пользователя
}
type AuthPayload {
session: Session!
user: User!
workspaces: [Workspace!]!
projects: [ProjectInfo!]!
}
type Subscription {
# расширяется в доменах
}
type Mutation {
signIn(input: SignInInput!): AuthPayload!
signUp(input: SignUpInput!): AuthPayload!
signOut: Boolean!
forgotPassword(input: ForgotPasswordInput!): Boolean!
resetPassword(input: ResetPasswordInput!): Boolean!
verifyEmail(input: VerifyEmailInput!): AuthPayload!
acceptInvite(input: InviteAcceptInput!): AuthPayload!
refreshToken: Session!
}
3. Organizations (Организации)¶
3.1 Types¶
type Organization {
id: ID!
name: String!
inn: String
kpp: String
legalAddress: String
createdAt: DateTime!
updatedAt: DateTime!
}
type Department {
id: ID!
name: String!
parentId: ID
managerId: ID
organizationId: ID!
}
type Employee {
id: ID!
userId: ID!
userName: String!
email: String!
departmentId: ID
orgRole: OrgRole!
canCreatePersonalProjects: Boolean!
invitedAt: DateTime
joinedAt: DateTime
}
3.2 Queries¶
input EmployeeFilter {
departmentId: ID
orgRole: OrgRole
}
type EmployeeEdge implements Edge {
cursor: String!
node: Employee!
}
type EmployeeConnection implements Connection {
edges: [EmployeeEdge!]!
pageInfo: PageInfo!
}
extend type Query {
organization(id: ID!): Organization
organizationDepartments(organizationId: ID!): [Department!]!
organizationEmployees(organizationId: ID!, filter: EmployeeFilter, pagination: PaginationInput): EmployeeConnection!
}
3.3 Mutations¶
input CreateOrganizationInput {
name: String!
inn: String
kpp: String
legalAddress: String
}
input UpdateOrganizationInput {
name: String
inn: String
kpp: String
legalAddress: String
}
input InviteEmployeeInput {
organizationId: ID!
email: String!
departmentId: ID
orgRole: OrgRole!
}
extend type Mutation {
createOrganization(input: CreateOrganizationInput!): Organization!
updateOrganization(id: ID!, input: UpdateOrganizationInput!): Organization!
inviteEmployee(input: InviteEmployeeInput!): Employee!
removeEmployee(organizationId: ID!, userId: ID!): Boolean!
}
4. Projects (Проекты)¶
4.1 Types¶
type Project {
id: ID!
name: String!
description: String
workspaceId: ID!
workspaceType: WorkspaceType!
modules: [String!]!
status: ProjectStatus!
templateId: String
createdAt: DateTime!
updatedAt: DateTime!
}
enum ProjectStatus {
ACTIVE
ARCHIVED
}
type ProjectMember {
id: ID!
userId: ID!
userName: String!
email: String!
avatar: String
role: ProjectRole!
departmentId: ID
}
4.2 Queries¶
input ProjectFilter {
workspaceId: ID
status: ProjectStatus
}
extend type Query {
project(id: ID!): Project
projects(filter: ProjectFilter): [Project!]!
projectMembers(projectId: ID!): [ProjectMember!]!
}
4.3 Mutations¶
input CreateProjectInput {
workspaceId: ID!
name: String!
description: String
templateId: String
modules: [String!]!
}
input UpdateProjectInput {
name: String
description: String
modules: [String!]
}
input AddProjectMemberInput {
projectId: ID!
userId: ID!
role: ProjectRole!
}
extend type Mutation {
createProject(input: CreateProjectInput!): Project!
updateProject(id: ID!, input: UpdateProjectInput!): Project!
archiveProject(id: ID!): Project!
addProjectMember(input: AddProjectMemberInput!): ProjectMember!
updateProjectMemberRole(projectId: ID!, userId: ID!, role: ProjectRole!): ProjectMember!
removeProjectMember(projectId: ID!, userId: ID!): Boolean!
}
5. Contacts (Контакты)¶
5.1 Types¶
type Contact {
id: ID!
projectId: ID!
firstName: String!
lastName: String!
middleName: String
phone: String!
email: String!
position: String
source: String
ownerId: ID!
ownerType: OwnerType!
companyIds: [ID!]!
tags: [String!]
notes: String
identityHash: String # скрытое, для Org Master Data v2+
createdAt: DateTime!
updatedAt: DateTime!
deletedAt: DateTime
}
enum OwnerType {
USER
DEPARTMENT
}
5.2 Queries¶
input ContactFilter {
search: String
companyId: ID
ownerId: ID
source: String
hasDeals: Boolean
}
type ContactEdge implements Edge {
cursor: String!
node: Contact!
}
type ContactConnection implements Connection {
edges: [ContactEdge!]!
pageInfo: PageInfo!
}
extend type Query {
contact(projectId: ID!, id: ID!): Contact
contacts(projectId: ID!, filter: ContactFilter, pagination: PaginationInput): ContactConnection!
}
5.3 Mutations¶
input CreateContactInput {
projectId: ID!
firstName: String!
lastName: String!
phone: String
email: String
position: String
companyIds: [ID!]
ownerId: ID!
ownerType: OwnerType!
}
input UpdateContactInput {
firstName: String
lastName: String
middleName: String
phone: String
email: String
position: String
companyIds: [ID!]
ownerId: ID
ownerType: OwnerType
tags: [String!]
notes: String
}
extend type Mutation {
createContact(input: CreateContactInput!): Contact!
updateContact(projectId: ID!, id: ID!, input: UpdateContactInput!): Contact!
deleteContact(projectId: ID!, id: ID!): Contact! # soft delete
restoreContact(projectId: ID!, id: ID!): Contact!
mergeContacts(projectId: ID!, masterId: ID!, shadowIds: [ID!]!): Contact!
}
6. Companies (Компании / Клиенты)¶
6.1 Types¶
type Company {
id: ID!
projectId: ID!
name: String!
inn: String
kpp: String
ogrn: String
legalAddress: String
actualAddress: String
phone: String
email: String
website: String
industry: String
employeeCount: Int
contactIds: [ID!]!
ownerId: ID!
ownerType: OwnerType!
tags: [String!]
notes: String
identityHash: String
createdAt: DateTime!
updatedAt: DateTime!
deletedAt: DateTime
}
6.2 Queries¶
input CompanyFilter {
search: String
inn: String
contactId: ID
ownerId: ID
}
type CompanyEdge implements Edge {
cursor: String!
node: Company!
}
type CompanyConnection implements Connection {
edges: [CompanyEdge!]!
pageInfo: PageInfo!
}
extend type Query {
company(projectId: ID!, id: ID!): Company
companies(projectId: ID!, filter: CompanyFilter, pagination: PaginationInput): CompanyConnection!
}
6.3 Mutations¶
input CreateCompanyInput {
projectId: ID!
name: String!
inn: String
legalAddress: String
contactIds: [ID!]
ownerId: ID!
ownerType: OwnerType!
}
input UpdateCompanyInput {
name: String
inn: String
kpp: String
ogrn: String
legalAddress: String
actualAddress: String
phone: String
email: String
website: String
industry: String
employeeCount: Int
contactIds: [ID!]
ownerId: ID
ownerType: OwnerType
tags: [String!]
notes: String
}
extend type Mutation {
createCompany(input: CreateCompanyInput!): Company!
updateCompany(projectId: ID!, id: ID!, input: UpdateCompanyInput!): Company!
deleteCompany(projectId: ID!, id: ID!): Company!
restoreCompany(projectId: ID!, id: ID!): Company!
mergeCompanies(projectId: ID!, masterId: ID!, shadowIds: [ID!]!): Company!
}
7. Pipelines & Deal Sources (Воронки и источники)¶
7.1 Types¶
type PipelineStage {
id: ID!
name: String!
color: String!
order: Int!
}
type Pipeline {
id: ID!
projectId: ID!
name: String!
stages: [PipelineStage!]!
isDefault: Boolean!
}
type DealSource {
id: ID!
projectId: ID!
name: String!
color: String!
}
7.2 Queries¶
extend type Query {
pipeline(projectId: ID!, id: ID!): Pipeline
pipelines(projectId: ID!): [Pipeline!]!
dealSources(projectId: ID!): [DealSource!]!
}
7.3 Mutations¶
input CreatePipelineInput {
projectId: ID!
name: String!
stages: [PipelineStageInput!]!
isDefault: Boolean
}
input PipelineStageInput {
name: String!
color: String!
order: Int!
}
input UpdatePipelineInput {
name: String
stages: [PipelineStageInput!]
isDefault: Boolean
}
extend type Mutation {
createPipeline(input: CreatePipelineInput!): Pipeline!
updatePipeline(projectId: ID!, id: ID!, input: UpdatePipelineInput!): Pipeline!
deletePipeline(projectId: ID!, id: ID!): Boolean!
createDealSource(projectId: ID!, name: String!, color: String!): DealSource!
}
8. Deals (Сделки)¶
8.1 Types¶
type DealLightContact {
contactName: String
contactPhone: String
contactEmail: String
contactCompanyName: String
}
type DealLineage {
originProjectId: ID!
originDealId: ID!
transferredAt: DateTime!
transferredById: ID!
}
type StageLogEntry {
stageId: ID!
stageName: String!
enteredAt: DateTime!
exitedAt: DateTime
}
type Deal {
id: ID!
projectId: ID!
name: String!
amount: Float!
currency: String!
pipelineId: ID!
stageId: ID!
stageName: String!
stageEnteredAt: DateTime!
stageLog: [StageLogEntry!]!
lightContact: DealLightContact
contactId: ID
contactSnapshot: ContactSnapshot # drift detection
companyId: ID
companySnapshot: CompanySnapshot # drift detection
productId: ID
sourceId: ID
sourceName: String
ownerId: ID!
ownerType: OwnerType!
expectedCloseDate: DateTime
closedAt: DateTime
result: DealResult
lostReason: String
notes: String
lineage: DealLineage # если передана из другого проекта
createdAt: DateTime!
updatedAt: DateTime!
deletedAt: DateTime
}
enum DealResult {
ACTIVE
WON
LOST
TRANSFERRED
}
type ContactSnapshot {
name: String!
phone: String!
email: String!
snapshotAt: DateTime!
}
type CompanySnapshot {
name: String!
inn: String
phone: String
snapshotAt: DateTime!
}
8.2 Queries¶
input DealFilter {
pipelineId: ID
stageId: ID
ownerId: ID
contactId: ID
companyId: ID
productId: ID
sourceId: ID
result: DealResult
stageDaysMin: Int # на стадии более N дней
withoutOwner: Boolean
}
type DealEdge implements Edge {
cursor: String!
node: Deal!
}
type DealConnection implements Connection {
edges: [DealEdge!]!
pageInfo: PageInfo!
}
extend type Query {
deal(projectId: ID!, id: ID!): Deal
deals(projectId: ID!, filter: DealFilter, pagination: PaginationInput): DealConnection!
dealsByStage(projectId: ID!, pipelineId: ID!): [DealsByStage!]!
}
type DealsByStage {
stageId: ID!
stageName: String!
count: Int!
amount: Float!
}
8.3 Mutations¶
input CreateDealInput {
projectId: ID!
name: String!
pipelineId: ID!
stageId: ID!
amount: Float
currency: String
lightContact: DealLightContactInput
contactId: ID
companyId: ID
productId: ID
sourceId: ID
ownerId: ID!
ownerType: OwnerType!
expectedCloseDate: DateTime
}
input DealLightContactInput {
contactName: String
contactPhone: String
contactEmail: String
contactCompanyName: String
}
input UpdateDealInput {
name: String
amount: Float
currency: String
contactId: ID
companyId: ID
productId: ID
sourceId: ID
ownerId: ID
ownerType: OwnerType
expectedCloseDate: DateTime
notes: String
}
extend type Mutation {
createDeal(input: CreateDealInput!): Deal!
updateDeal(projectId: ID!, id: ID!, input: UpdateDealInput!): Deal!
moveDealToStage(projectId: ID!, id: ID!, stageId: ID!): Deal!
closeDeal(projectId: ID!, id: ID!, result: DealResult!, lostReason: String): Deal!
reopenDeal(projectId: ID!, id: ID!, stageId: ID!): Deal!
transferDeal(sourceProjectId: ID!, dealId: ID!, targetProjectId: ID!): Deal!
confirmContactSnapshot(projectId: ID!, dealId: ID!): Deal! # drift: принять изменения
bulkMoveDealsToStage(projectId: ID!, dealIds: [ID!]!, stageId: ID!): [Deal!]!
bulkAssignDeals(projectId: ID!, dealIds: [ID!]!, ownerId: ID!, ownerType: OwnerType!): [Deal!]!
bulkTransferDeals(sourceProjectId: ID!, dealIds: [ID!]!, targetProjectId: ID!): [Deal!]!
deleteDeal(projectId: ID!, id: ID!): Deal!
}
8.4 Subscriptions¶
extend type Subscription {
dealUpdated(projectId: ID!): Deal!
dealsByStageUpdated(projectId: ID!, pipelineId: ID!): DealsByStage!
}
9. Order Types (Типы продаж)¶
9.1 Types¶
type OrderTypeField {
key: String!
label: String!
type: OrderTypeFieldType!
required: Boolean!
options: [String!]
}
enum OrderTypeFieldType {
TEXT
NUMBER
DATE
SELECT
FILE
CHECKBOX
}
type OrderTypeStage {
id: ID!
name: String!
order: Int!
}
type OrderType {
id: ID!
projectId: ID!
name: String!
fields: [OrderTypeField!]!
stages: [OrderTypeStage!]!
schemaVersion: Int!
webhookEnabled: Boolean!
webhookUrl: String
retryPolicy: RetryPolicy
activeOrdersCount: Int!
}
type RetryPolicy {
maxAttempts: Int!
initialDelayMs: Int!
maxDelayMs: Int!
}
9.2 Queries¶
extend type Query {
orderType(projectId: ID!, id: ID!): OrderType
orderTypes(projectId: ID!): [OrderType!]!
}
9.3 Mutations¶
input CreateOrderTypeInput {
projectId: ID!
name: String!
fields: [OrderTypeFieldInput!]!
stages: [OrderTypeStageInput!]!
webhookEnabled: Boolean
webhookUrl: String
}
input OrderTypeFieldInput {
key: String!
label: String!
type: OrderTypeFieldType!
required: Boolean!
options: [String!]
}
input OrderTypeStageInput {
name: String!
order: Int!
}
input UpdateOrderTypeInput {
name: String
fields: [OrderTypeFieldInput!]
stages: [OrderTypeStageInput!]
webhookEnabled: Boolean
webhookUrl: String
}
extend type Mutation {
createOrderType(input: CreateOrderTypeInput!): OrderType!
updateOrderType(projectId: ID!, id: ID!, input: UpdateOrderTypeInput!): OrderType!
deleteOrderType(projectId: ID!, id: ID!): Boolean!
}
10. Orders (Продажи)¶
10.1 Types¶
type Order {
id: ID!
projectId: ID!
number: String!
typeId: ID!
typeName: String!
orderTypeVersion: Int!
dealId: ID
contactId: ID
contactSnapshot: ContactSnapshot
companyId: ID
companySnapshot: CompanySnapshot
stageId: ID!
stageName: String!
stageEnteredAt: DateTime!
stageLog: [StageLogEntry!]!
fields: JSON!
ownerId: ID!
ownerType: OwnerType!
status: OrderStatus!
dlqError: String
dlqAttempts: Int
createdAt: DateTime!
updatedAt: DateTime!
deletedAt: DateTime
}
enum OrderStatus {
ACTIVE
COMPLETED
ERROR # DLQ: ошибка финального действия
CANCELLED
}
scalar JSON
10.2 Queries¶
input OrderFilter {
typeId: ID
stageId: ID
dealId: ID
ownerId: ID
status: OrderStatus
hasDlqError: Boolean
}
type OrderEdge implements Edge {
cursor: String!
node: Order!
}
type OrderConnection implements Connection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
}
extend type Query {
order(projectId: ID!, id: ID!): Order
orders(projectId: ID!, filter: OrderFilter, pagination: PaginationInput): OrderConnection!
}
10.3 Mutations¶
input CreateOrderInput {
projectId: ID!
dealId: ID!
typeId: ID!
contactId: ID
companyId: ID
fields: JSON!
ownerId: ID!
ownerType: OwnerType!
}
input UpdateOrderInput {
contactId: ID
companyId: ID
fields: JSON
ownerId: ID
ownerType: OwnerType
}
extend type Mutation {
createOrder(input: CreateOrderInput!): Order!
updateOrder(projectId: ID!, id: ID!, input: UpdateOrderInput!): Order!
moveOrderToStage(projectId: ID!, id: ID!, stageId: ID!): Order!
completeOrder(projectId: ID!, id: ID!): Order!
retryOrderWebhook(projectId: ID!, id: ID!): Order!
confirmOrderSnapshot(projectId: ID!, orderId: ID!): Order!
deleteOrder(projectId: ID!, id: ID!): Order!
}
11. Products (Продукты)¶
11.1 Types¶
type Product {
id: ID!
projectId: ID!
name: String!
description: String
category: String
price: Float!
unit: ProductUnit!
orderTypeId: ID
orderTypeName: String
dealsCount: Int!
ordersCount: Int!
createdAt: DateTime!
updatedAt: DateTime!
}
enum ProductUnit {
ONE_TIME
MONTHLY
YEARLY
}
11.2 Queries¶
input ProductFilter {
category: String
orderTypeId: ID
}
extend type Query {
product(projectId: ID!, id: ID!): Product
products(projectId: ID!, filter: ProductFilter): [Product!]!
}
11.3 Mutations¶
input CreateProductInput {
projectId: ID!
name: String!
description: String
category: String
price: Float!
unit: ProductUnit!
orderTypeId: ID
}
input UpdateProductInput {
name: String
description: String
category: String
price: Float
unit: ProductUnit
orderTypeId: ID
}
extend type Mutation {
createProduct(input: CreateProductInput!): Product!
updateProduct(projectId: ID!, id: ID!, input: UpdateProductInput!): Product!
deleteProduct(projectId: ID!, id: ID!): Product!
}
12. Activities (Активности)¶
12.1 Types¶
type Activity {
id: ID!
projectId: ID!
type: ActivityType!
title: String!
description: String
status: ActivityStatus!
priority: ActivityPriority!
dueDate: DateTime
startDate: DateTime
endDate: DateTime
assigneeId: ID!
dealId: ID
contactId: ID
companyId: ID
orderId: ID
location: String
direction: ActivityDirection
result: String
duration: Int
reminderAt: DateTime
overdue: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
}
enum ActivityType {
TASK
CALL
MEETING
NOTE
}
enum ActivityStatus {
PLANNED
IN_PROGRESS
COMPLETED
CANCELLED
}
enum ActivityPriority {
LOW
MEDIUM
HIGH
URGENT
}
enum ActivityDirection {
INCOMING
OUTGOING
}
12.2 Queries¶
input ActivityFilter {
type: ActivityType
status: ActivityStatus
assigneeId: ID
dealId: ID
contactId: ID
overdue: Boolean
dateFrom: DateTime
dateTo: DateTime
}
type ActivityEdge implements Edge {
cursor: String!
node: Activity!
}
type ActivityConnection implements Connection {
edges: [ActivityEdge!]!
pageInfo: PageInfo!
}
extend type Query {
activity(projectId: ID!, id: ID!): Activity
activities(projectId: ID!, filter: ActivityFilter, pagination: PaginationInput): ActivityConnection!
activitiesCalendar(projectId: ID!, start: DateTime!, end: DateTime!, filter: ActivityFilter): [Activity!]!
}
12.3 Mutations¶
input CreateActivityInput {
projectId: ID!
type: ActivityType!
title: String!
description: String
status: ActivityStatus
priority: ActivityPriority
dueDate: DateTime
startDate: DateTime
endDate: DateTime
assigneeId: ID!
dealId: ID
contactId: ID
companyId: ID
orderId: ID
reminderAt: DateTime
}
input UpdateActivityInput {
title: String
description: String
status: ActivityStatus
priority: ActivityPriority
dueDate: DateTime
startDate: DateTime
endDate: DateTime
assigneeId: ID
reminderAt: DateTime
result: String
duration: Int
}
extend type Mutation {
createActivity(input: CreateActivityInput!): Activity!
updateActivity(projectId: ID!, id: ID!, input: UpdateActivityInput!): Activity!
deleteActivity(projectId: ID!, id: ID!): Activity!
}
13. Documents (Документы)¶
13.1 Types¶
type DocumentTemplate {
id: ID!
projectId: ID!
name: String!
context: DocumentContext!
format: String!
orderTypeId: ID
fileUrl: String!
variables: [String!]!
}
enum DocumentContext {
ORDER
DEAL
CONTACT
COMPANY
}
type Document {
id: ID!
projectId: ID!
templateId: ID!
entityType: String!
entityId: ID!
version: Int!
fileUrl: String!
generatedAt: DateTime!
generatedById: ID!
}
13.2 Queries¶
extend type Query {
documentTemplates(projectId: ID!): [DocumentTemplate!]!
document(projectId: ID!, id: ID!): Document
documents(projectId: ID!, entityType: String, entityId: ID): [Document!]!
}
13.3 Mutations¶
input CreateDocumentTemplateInput {
name: String!
context: DocumentContext!
format: String!
orderTypeId: ID
fileUrl: String!
}
extend type Mutation {
createDocumentTemplate(projectId: ID!, input: CreateDocumentTemplateInput!): DocumentTemplate!
generateDocument(projectId: ID!, templateId: ID!, entityType: String!, entityId: ID!): Document!
}
14. Reports (Отчёты)¶
14.1 Types¶
type ReportSales {
newDeals: Int!
wonDeals: Int!
lostDeals: Int!
totalAmount: Float!
avgCheck: Float!
conversionRate: Float!
timeline: [ReportTimelinePoint!]!
}
type ReportFunnel {
stages: [ReportFunnelStage!]!
avgTimeOnStage: [ReportStageTime!]!
stuckDeals: Int!
}
type ReportSources {
sources: [ReportSourcePoint!]!
}
type ReportActivity {
byType: [ReportActivityByType!]!
overdue: Int!
byManager: [ReportActivityByManager!]!
}
type ReportTimelinePoint {
date: String!
won: Int!
lost: Int!
new: Int!
}
type ReportFunnelStage {
stageId: ID!
stageName: String!
count: Int!
conversionRate: Float!
}
type ReportStageTime {
stageId: ID!
avgDays: Float!
}
type ReportSourcePoint {
sourceId: ID!
sourceName: String!
count: Int!
amount: Float!
conversionRate: Float!
}
type ReportActivityByType {
type: ActivityType!
count: Int!
}
type ReportActivityByManager {
userId: ID!
userName: String!
count: Int!
}
type DealsTimelinePoint {
date: String!
won: Int!
lost: Int!
new: Int!
}
type DealsBySourcePoint {
sourceId: ID!
sourceName: String!
count: Int!
}
type TopManager {
userId: ID!
userName: String!
dealsCount: Int!
amount: Float!
conversionRate: Float!
}
14.2 Queries¶
input ReportPeriodInput {
from: DateTime!
to: DateTime!
}
extend type Query {
reportSales(projectId: ID!, period: ReportPeriodInput!): ReportSales!
reportFunnel(projectId: ID!, pipelineId: ID!, period: ReportPeriodInput!): ReportFunnel!
reportSources(projectId: ID!, period: ReportPeriodInput!): ReportSources!
reportActivity(projectId: ID!, period: ReportPeriodInput!): ReportActivity!
}
15. Dashboard (Дашборд)¶
15.1 Types¶
type DashboardStatistic {
key: String!
label: String!
value: Float!
previousValue: Float!
growthRate: Float!
}
type DashboardData {
statistics: [DashboardStatistic!]!
dealsByStage: [DealsByStage!]!
dealsTimeline: [DealsTimelinePoint!]!
dealsBySource: [DealsBySourcePoint!]
topManagers: [TopManager!]!
recentDeals: [Deal!]!
overdueActivities: [Activity!]!
upcomingActivities: [Activity!]!
}
15.2 Queries¶
extend type Query {
dashboard(projectId: ID!, period: ReportPeriodInput!): DashboardData!
}
16. Search (Глобальный поиск)¶
16.1 Types¶
type SearchResult {
type: String!
id: ID!
title: String!
subtitle: String
url: String!
}
type SearchResults {
deals: [SearchResult!]!
contacts: [SearchResult!]!
companies: [SearchResult!]!
orders: [SearchResult!]!
activities: [SearchResult!]!
products: [SearchResult!]!
}
16.2 Queries¶
extend type Query {
search(projectId: ID!, query: String!): SearchResults!
}
17. Audit (Аудит)¶
17.1 Types¶
type AuditEvent {
id: ID!
type: AuditEventType!
timestamp: DateTime!
userId: ID!
userName: String!
entityType: String!
entityId: ID!
projectId: ID
changes: JSON # JSON Patch RFC 6902
metadata: JSON
}
enum AuditEventType {
CREATE
UPDATE
DELETE
TRANSFER
STATUS_CHANGE
LOGIN
LOGOUT
MERGE
SHARE
}
17.2 Queries¶
input AuditFilter {
entityType: String
entityId: ID
userId: ID
projectId: ID
type: AuditEventType
from: DateTime
to: DateTime
}
type AuditEventEdge implements Edge {
cursor: String!
node: AuditEvent!
}
type AuditEventConnection implements Connection {
edges: [AuditEventEdge!]!
pageInfo: PageInfo!
}
type NotificationEdge implements Edge {
cursor: String!
node: Notification!
}
type NotificationConnection implements Connection {
edges: [NotificationEdge!]!
pageInfo: PageInfo!
}
extend type Query {
projectAudit(projectId: ID!, filter: AuditFilter, pagination: PaginationInput): AuditEventConnection!
organizationAudit(organizationId: ID!, filter: AuditFilter, pagination: PaginationInput): AuditEventConnection!
entityHistory(projectId: ID!, entityType: String!, entityId: ID!): [AuditEvent!]!
}
18. Notifications (Уведомления)¶
18.1 Types¶
type Notification {
id: ID!
userId: ID!
type: String!
title: String!
body: String
read: Boolean!
entityType: String
entityId: ID
projectId: ID
createdAt: DateTime!
}
18.2 Queries & Mutations¶
extend type Query {
notifications(pagination: PaginationInput): NotificationConnection!
unreadNotificationsCount: Int!
}
extend type Mutation {
markNotificationRead(id: ID!): Notification!
markAllNotificationsRead: Boolean!
}
extend type Subscription {
notificationReceived: Notification!
}
19. Import (Импорт данных)¶
19.1 Mutations¶
input ImportMapping {
columnIndex: Int!
fieldKey: String!
}
type ImportResult {
created: Int!
updated: Int!
skipped: Int!
errors: [ImportError!]!
}
type ImportError {
row: Int!
message: String!
}
input ImportOptions {
updateExisting: Boolean
skipDuplicates: Boolean
}
extend type Mutation {
importContacts(projectId: ID!, fileUrl: String!, mapping: [ImportMapping!]!, options: ImportOptions): ImportResult!
importCompanies(projectId: ID!, fileUrl: String!, mapping: [ImportMapping!]!, options: ImportOptions): ImportResult!
importProducts(projectId: ID!, fileUrl: String!, mapping: [ImportMapping!]!, options: ImportOptions): ImportResult!
}
20. MongoDB — коллекции и индексы¶
20.1 Коллекции¶
| Коллекция | Назначение |
|---|---|
| users | Пользователи, профили |
| organizations | Организации (юрлица) |
| departments | Подразделения организаций |
| workspaces | Пространства (личные/орг) |
| projects | Проекты |
| project_members | Участники проектов |
| contacts | Контакты (projectId, ownerId) |
| companies | Компании (projectId, ownerId) |
| contact_company_links | M:M связь контакт ↔ компания |
| pipelines | Воронки |
| deal_sources | Источники сделок |
| deals | Сделки (projectId, stage_log embedded, lineage embedded) |
| order_types | Типы продаж |
| orders | Продажи (projectId, fields JSON, snapshots embedded) |
| products | Продукты |
| activities | Активности |
| document_templates | Шаблоны документов |
| documents | Сгенерированные документы |
| audit_events | Журнал аудита |
| notifications | Уведомления |
20.2 Ключевые индексы¶
contacts: { projectId: 1, deletedAt: 1 }, { projectId: 1, ownerId: 1 }, { projectId: 1, email: 1 } (partial: deletedAt null)
companies: { projectId: 1, deletedAt: 1 }, { projectId: 1, inn: 1 } (partial)
deals: { projectId: 1, stageId: 1 }, { projectId: 1, ownerId: 1 }, { projectId: 1, pipelineId: 1, result: 1 }
orders: { projectId: 1, typeId: 1 }, { projectId: 1, stageId: 1 }, { projectId: 1, status: 1 }
activities: { projectId: 1, assigneeId: 1, dueDate: 1 }
audit_events: { projectId: 1, timestamp: -1 }, { entityType: 1, entityId: 1 }
20.3 Embedding vs References¶
- Embedding: stage_log, lineage, lightContact, contactSnapshot, companySnapshot, OrderType fields/stages
- References: contactId, companyId, dealId, orderId, ownerId, pipelineId, typeId
21. Бизнес-правила в резолверах¶
- projectId — все CRM queries/mutations проверяют доступ к проекту и фильтруют по projectId.
- ownerId — обязателен при создании; при удалении сотрудника — переназначение по проекту.
- Модули — резолверы проверяют
project.modulesперед операциями (contacts, deals, orders и т.д.). - Дубли — при создании контакта/компании проверка по email/phone/inn; предложение merge.
- Soft delete —
deletedAt; при создании с дублем в корзине — предложить восстановить. - Передача сделки — только между проектами одной организации; копируется deal + lineage + lightContact.
- DLQ — retry по RetryPolicy; после исчерпания — status ERROR; retryOrderWebhook для повторной отправки.
Спецификация v1. Детализация сценариев, edge cases и производительности — в следующих итерациях.