Перейти к содержанию

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.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. Бизнес-правила в резолверах

  1. projectId — все CRM queries/mutations проверяют доступ к проекту и фильтруют по projectId.
  2. ownerId — обязателен при создании; при удалении сотрудника — переназначение по проекту.
  3. Модули — резолверы проверяют project.modules перед операциями (contacts, deals, orders и т.д.).
  4. Дубли — при создании контакта/компании проверка по email/phone/inn; предложение merge.
  5. Soft deletedeletedAt; при создании с дублем в корзине — предложить восстановить.
  6. Передача сделки — только между проектами одной организации; копируется deal + lineage + lightContact.
  7. DLQ — retry по RetryPolicy; после исчерпания — status ERROR; retryOrderWebhook для повторной отправки.

Спецификация v1. Детализация сценариев, edge cases и производительности — в следующих итерациях.