]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Документация
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Mon, 17 Nov 2025 06:41:00 +0000 (09:41 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Mon, 17 Nov 2025 06:41:00 +0000 (09:41 +0300)
docs/README.md [new file with mode: 0644]
docs/api/api2/README.md [new file with mode: 0644]
docs/architecture/api-architecture.md [new file with mode: 0644]
docs/architecture/system-overview.md [new file with mode: 0644]
docs/database/schema-overview.md [new file with mode: 0644]
docs/modules/bonus.md [new file with mode: 0644]
docs/modules/dashboard.md [new file with mode: 0644]
docs/modules/payroll.md [new file with mode: 0644]
docs/modules/rating.md [new file with mode: 0644]
docs/modules/timetable.md [new file with mode: 0644]

diff --git a/docs/README.md b/docs/README.md
new file mode 100644 (file)
index 0000000..76fc8fd
--- /dev/null
@@ -0,0 +1,330 @@
+# ERP24 System Documentation
+
+> **Comprehensive documentation for the ERP24 Yii2 enterprise resource planning system**
+
+## Overview
+
+ERP24 is a large-scale enterprise resource planning system built on Yii2 framework, designed for managing flower retail business operations. The system handles complex workflows including customer loyalty programs, payroll processing, shipment tracking, employee scheduling, sales analytics, and multi-channel marketplace integrations.
+
+### System Statistics
+
+- **Total PHP Files**: ~3,771
+- **Controllers**: 161
+- **ActiveRecord Models**: 389
+- **Services**: 51 business logic services
+- **Actions**: 223 standalone action classes
+- **API Controllers**: 33 (across 3 API layers)
+- **Database Migrations**: 278
+- **Console Commands**: 16
+- **Background Jobs**: 7
+- **View Templates**: 830+
+- **Business Modules**: 82 legacy modules
+
+### Technology Stack
+
+- **Framework**: Yii2 (PHP framework)
+- **Database**: PostgreSQL
+- **Message Queue**: RabbitMQ
+- **Frontend**: jQuery, ES6+, SASS (via esbuild)
+- **Infrastructure**: Docker, Nginx, PHP-FPM, Supervisor
+
+---
+
+## Documentation Structure
+
+### 📐 [Architecture Documentation](./architecture/)
+System design patterns, component interactions, and architectural decisions
+- [System Overview](./architecture/system-overview.md)
+- [Three-Layer API Architecture](./architecture/api-architecture.md)
+- [Service Layer Pattern](./architecture/service-layer.md)
+- [Database Architecture](./architecture/database-architecture.md)
+- [Frontend Architecture](./architecture/frontend-architecture.md)
+
+### 🔌 [API Documentation](./api/)
+Complete API reference for all three API layers
+- [API1 - Legacy API](./api/api1/) (Port 4444)
+- [API2 - Modern REST API](./api/api2/) (Port 5555)
+- [API3 - Advanced API](./api/api3/) (Port 8888)
+
+### 📦 [Business Modules](./modules/)
+Domain-specific business logic documentation
+- [Bonus System](./modules/bonus.md) - Customer loyalty and bonus points
+- [Payroll Module](./modules/payroll.md) - Salary calculations and payment processing
+- [Shipment Module](./modules/shipment.md) - Order fulfillment and delivery tracking
+- [Timetable Module](./modules/timetable.md) - Employee scheduling and shift management
+- [Dashboard Module](./modules/dashboard.md) - Analytics and KPI visualization
+- [Rating System](./modules/rating.md) - Employee performance evaluation
+- [Notifications](./modules/notifications.md) - Push notifications and alerts
+- [Marketplace Integration](./modules/marketplace.md) - Flowwow, Yandex Market
+- [KIK Feedback](./modules/kik.md) - Customer feedback system
+- [Regulations](./modules/regulations.md) - Company policies and regulations
+- [Write-offs](./modules/write-offs.md) - Inventory write-off management
+- [Grade System](./modules/grade.md) - Employee grade management
+- [Lesson System](./modules/lesson.md) - Training and education
+
+### 🗄️ [Database Documentation](./database/)
+Database schema, relationships, and migration guides
+- [Schema Overview](./database/schema-overview.md)
+- [ActiveRecord Models Reference](./database/models-reference.md)
+- [Table Relationships](./database/relationships.md)
+- [Migration Guide](./database/migrations.md)
+
+### ⚙️ [Services Documentation](./services/)
+Business logic services (51 service classes)
+- [Services Overview](./services/overview.md)
+- [Service Index](./services/index.md)
+- [Creating New Services](./services/creating-services.md)
+
+### 📚 [Developer Guides](./guides/)
+Setup, development workflows, and best practices
+- [Getting Started](./guides/getting-started.md)
+- [Development Environment Setup](./guides/environment-setup.md)
+- [Creating Controllers](./guides/creating-controllers.md)
+- [Using the Action Pattern](./guides/action-pattern.md)
+- [Working with ActiveRecord](./guides/activerecord.md)
+- [Background Jobs & Queue System](./guides/background-jobs.md)
+- [Frontend Development](./guides/frontend-development.md)
+- [Testing Guidelines](./guides/testing.md)
+- [Security & RBAC](./guides/security-rbac.md)
+- [Configuration Management](./guides/configuration.md)
+- [Integration Guides](./guides/integrations/)
+
+### ❌ [Error Reference](./errors/)
+Common errors, troubleshooting, and solutions
+- [Error Codes](./errors/error-codes.md)
+- [Troubleshooting Guide](./errors/troubleshooting.md)
+- [Database Issues](./errors/database-issues.md)
+- [API Debugging](./errors/api-debugging.md)
+
+---
+
+## Quick Start
+
+### Prerequisites
+
+- Docker and Docker Compose
+- PHP 7.4+ (for local development)
+- PostgreSQL 12+ (via Docker)
+- Node.js 14+ (for frontend builds)
+
+### Installation
+
+```bash
+# Clone the repository
+git clone <repository-url>
+cd yii-erp24
+
+# Start Docker containers
+docker-compose up -d
+
+# Install PHP dependencies
+composer install
+
+# Run migrations
+php erp24/yii migrate
+
+# Install frontend dependencies
+npm install
+
+# Build frontend assets
+npm run dev
+```
+
+### Access Points
+
+- **Main Application**: http://localhost:81
+- **API1 (Legacy)**: http://localhost:4444
+- **API2 (REST)**: http://localhost:5555
+- **API3 (Advanced)**: http://localhost:8888
+- **Media Server**: http://localhost:9999
+- **RabbitMQ Console**: http://localhost:15672
+
+---
+
+## Architecture Overview
+
+### Three-Layer API Design
+
+ERP24 implements a unique three-layer API architecture to support different integration needs:
+
+```mermaid
+graph TB
+    subgraph "Client Layer"
+        WEB[Web Application]
+        MOBILE[Mobile Apps]
+        CRON[Cron Jobs]
+        EXTERNAL[External Systems]
+    end
+
+    subgraph "API Layer"
+        API1[API1 - Legacy<br/>Port 4444]
+        API2[API2 - REST<br/>Port 5555]
+        API3[API3 - Advanced<br/>Port 8888]
+    end
+
+    subgraph "Business Layer"
+        SERVICES[51 Service Classes]
+        ACTIONS[223 Action Classes]
+    end
+
+    subgraph "Data Layer"
+        RECORDS[389 ActiveRecord Models]
+        DB[(PostgreSQL Database)]
+    end
+
+    subgraph "Integration Layer"
+        QUEUE[RabbitMQ Queue]
+        AMOCRM[AmoCRM]
+        TELEGRAM[Telegram Bot]
+        WHATSAPP[WhatsApp]
+        YANDEX[Yandex Market]
+    end
+
+    WEB --> API2
+    MOBILE --> API2
+    MOBILE --> API3
+    CRON --> API1
+    EXTERNAL --> API2
+    EXTERNAL --> API3
+
+    API1 --> SERVICES
+    API2 --> SERVICES
+    API3 --> SERVICES
+
+    API1 --> ACTIONS
+    API2 --> ACTIONS
+    API3 --> ACTIONS
+
+    SERVICES --> RECORDS
+    ACTIONS --> RECORDS
+
+    RECORDS --> DB
+
+    SERVICES --> QUEUE
+    QUEUE --> TELEGRAM
+    QUEUE --> WHATSAPP
+
+    API2 --> AMOCRM
+    API2 --> YANDEX
+```
+
+### Core Components
+
+1. **Controllers** (161 files): Handle HTTP requests and route to appropriate services
+2. **Services** (51 files): Business logic layer with single-responsibility services
+3. **Actions** (223 files): Standalone action classes for specific operations
+4. **ActiveRecord Models** (389 files): Data access layer representing database tables
+5. **Helpers** (20 files): Utility functions and common operations
+6. **Forms** (23 files): Input validation and data transformation
+7. **Commands** (16 files): Console commands for background tasks
+8. **Jobs** (7 files): Queue-based asynchronous job processing
+
+---
+
+## Key Features
+
+### Business Capabilities
+
+- **Customer Loyalty**: Comprehensive bonus point system with multiple earning and redemption rules
+- **Payroll Management**: Automated salary calculation based on performance metrics
+- **Order Fulfillment**: End-to-end shipment tracking with delivery optimization
+- **Employee Scheduling**: Shift management with conflict detection
+- **Performance Analytics**: Real-time dashboards and KPI tracking
+- **Multi-Channel Sales**: Integration with Flowwow, Yandex Market, and other platforms
+- **Task Management**: Assignment, tracking, and reporting system
+- **Training System**: Employee onboarding and continuous education
+
+### Technical Features
+
+- **RESTful APIs**: Clean, versioned APIs with comprehensive error handling
+- **Queue System**: Asynchronous processing for notifications and heavy operations
+- **RBAC**: Role-based access control with fine-grained permissions
+- **Soft Deletes**: Logical deletion with history tracking
+- **Change Tracking**: Audit trail for critical business entities
+- **File Management**: Organized media storage with thumbnail generation
+- **Real-time Notifications**: Telegram and WhatsApp integration
+- **Multi-Database Support**: Primary and replica database connections
+
+---
+
+## Development Principles
+
+### Code Organization
+
+- **MVC Pattern**: Strict separation of concerns
+- **Service Layer**: Business logic isolated from controllers
+- **Action Pattern**: Single-responsibility action classes
+- **Repository Pattern**: Data access through ActiveRecord
+- **DTO Pattern**: Request/Response objects in API3
+
+### Best Practices
+
+- Dependency injection via Yii2 DI container
+- Interface-based programming for services
+- Trait-based behavior composition
+- PSR-4 autoloading
+- Comprehensive error handling
+- Logging at appropriate levels
+- Transaction management for data integrity
+
+---
+
+## Contributing
+
+### Documentation Updates
+
+When updating documentation:
+
+1. Follow the established structure
+2. Include code examples for all methods
+3. Add mermaid diagrams for complex flows
+4. Cross-reference related components
+5. Update the main index when adding new documents
+
+### Code Documentation Standards
+
+- PHPDoc blocks for all public methods
+- Type hints for parameters and return values
+- Usage examples in doc comments
+- Link to related classes and methods
+
+---
+
+## Support & Resources
+
+### Internal Resources
+
+- [Wiki System](./modules/wiki.md) - Internal knowledge base
+- [Lesson System](./modules/lesson.md) - Training materials
+- [Task System](./modules/task.md) - Issue tracking
+
+### External Links
+
+- [Yii2 Documentation](https://www.yiiframework.com/doc/guide/2.0/en)
+- [PostgreSQL Documentation](https://www.postgresql.org/docs/)
+- [RabbitMQ Documentation](https://www.rabbitmq.com/documentation.html)
+- [Docker Documentation](https://docs.docker.com/)
+
+---
+
+## Version History
+
+- **Current Version**: Production (2025)
+- **Framework**: Yii2 2.0.x
+- **PHP Version**: 7.4+
+- **Database**: PostgreSQL 12+
+- **Last Updated**: January 2025
+
+---
+
+## License
+
+Proprietary - Internal Use Only
+
+---
+
+## Maintainers
+
+This documentation is maintained by the ERP24 development team.
+
+For questions or issues, please contact the development team or create a task in the internal task management system.
diff --git a/docs/api/api2/README.md b/docs/api/api2/README.md
new file mode 100644 (file)
index 0000000..721b6c9
--- /dev/null
@@ -0,0 +1,1464 @@
+# API2 - Modern REST API Documentation
+
+> **Primary REST API for ERP24 system - Port 5555**
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Getting Started](#getting-started)
+- [Authentication](#authentication)
+- [Core Endpoints](#core-endpoints)
+- [Business Endpoints](#business-endpoints)
+- [Integration Endpoints](#integration-endpoints)
+- [Error Handling](#error-handling)
+- [Rate Limiting](#rate-limiting)
+
+---
+
+## Overview
+
+API2 is the **primary REST API** for the ERP24 system, serving mobile applications, web clients, and external integrations.
+
+### Key Features
+
+- ✅ RESTful design principles
+- ✅ JSON request/response format
+- ✅ Token-based authentication
+- ✅ Comprehensive error handling
+- ✅ Swagger documentation support
+- ✅ Mobile-optimized responses
+- ✅ Real-time data access
+
+### Base URL
+
+```
+Production: https://api2.bazacvetov24.ru
+Development: http://localhost:5555
+```
+
+### API Statistics
+
+| Metric | Count |
+|--------|-------|
+| **Controllers** | 21 |
+| **Total Endpoints** | 60+ |
+| **Authentication Methods** | Token-based |
+| **Response Format** | JSON |
+| **Status** | Active Development |
+
+---
+
+## Getting Started
+
+### Quick Start Example
+
+```bash
+# 1. Authenticate
+curl -X POST https://api2.bazacvetov24.ru/auth/login \
+  -H "Content-Type: application/json" \
+  -d '{"login": "api_user", "password": "secret"}'
+
+# Response:
+# {"access-token": "abc123xyz..."}
+
+# 2. Use the token for subsequent requests
+curl -X GET https://api2.bazacvetov24.ru/client/get-info \
+  -H "Authorization: Bearer abc123xyz..." \
+  -H "Content-Type: application/json" \
+  -d '{"phone": "79001234567"}'
+```
+
+### Request Headers
+
+All API requests should include:
+
+```http
+Content-Type: application/json
+Authorization: Bearer <access-token>
+Accept: application/json
+```
+
+### Response Format
+
+**Success Response**:
+```json
+{
+  "success": true,
+  "data": { ...actual data... },
+  "timestamp": "2025-01-14T12:43:32Z"
+}
+```
+
+**Error Response**:
+```json
+{
+  "success": false,
+  "error": {
+    "code": "ERROR_CODE",
+    "message": "Human-readable error message",
+    "details": {...optional details...}
+  },
+  "timestamp": "2025-01-14T12:43:32Z"
+}
+```
+
+---
+
+## Authentication
+
+### POST /auth/login
+
+Authenticate user and receive access token.
+
+**File**: `erp24/api2/controllers/AuthController.php:9`
+
+**Request**:
+```json
+{
+  "login": "api_user",
+  "password": "your_password"
+}
+```
+
+**Success Response**:
+```json
+{
+  "access-token": "eyJ0eXAiOiJKV1QiLCJhbGc..."
+}
+```
+
+**Error Response**:
+```json
+{
+  "errors": "Wrong login or password"
+}
+```
+
+**Example**:
+```bash
+curl -X POST http://localhost:5555/auth/login \
+  -H "Content-Type: application/json" \
+  -d '{
+    "login": "admin",
+    "password": "secret123"
+  }'
+```
+
+---
+
+## Core Endpoints
+
+### Client Controller
+
+**File**: `erp24/api2/controllers/ClientController.php`
+
+Client management endpoints for customer operations.
+
+#### POST /client/add
+
+Register new client or update existing client.
+
+**Request Parameters**:
+```json
+{
+  "phone": "79001234567",        // Required
+  "client_id": 12345,           // Optional (for Salebot)
+  "client_type": "1",            // Optional
+  "platform_id": 123,            // Optional
+  "name": "Иван Иванов",        // Required
+  "avatar": "https://...",       // Optional
+  "messenger": "telegram",       // Optional
+  "message_id": "msg_123"        // Optional
+}
+```
+
+**Response**:
+```json
+{
+  "result": true,
+  "result_edit": " {...} 79001234567 ",
+  "editDates": true
+}
+```
+
+**Error Response**:
+```json
+{
+  "error_id": 1,
+  "error": "phone is required"
+}
+```
+
+---
+
+#### POST /client/balance
+
+Get client bonus balance and keycode.
+
+**Request**:
+```json
+{
+  "phone": "79001234567"
+}
+```
+
+**Response**:
+```json
+{
+  "balance": 1250.50,
+  "keycode": "1234",
+  "editDates": true
+}
+```
+
+---
+
+#### POST /client/get-info
+
+Get comprehensive client information.
+
+**Request**:
+```json
+{
+  "phone": "79001234567"
+  // OR
+  "ref_code": "ABC123XYZ"
+}
+```
+
+**Response**:
+```json
+{
+  "response": {
+    "id": 12345,
+    "card": "1234567890",
+    "first_name": "Иван",
+    "second_name": "Иванов",
+    "sex": "male",
+    "birth_day": "1990-05-15",
+    "comment": "VIP клиент",
+    "balance": 1250.50,
+    "burn_balans": 200.00,
+    "bonus_level": "gold",
+    "total_price": 45000.00,
+    "total_price_rejected": 500.00,
+    "referral_count_all": 5,
+    "referral_count_get_bonus_already": 3,
+    "ref_code": "ABC123",
+    "referral_id": 98765,
+    "events": [
+      {
+        "date": "2025-01-15",
+        "event_id": 1,
+        "event_tip": "День рождения",
+        "number": 1
+      }
+    ],
+    "platform": {
+      "telegram": {
+        "is_subscribed": 1,
+        "created_at": "2024-06-15 10:30:00"
+      }
+    }
+  }
+}
+```
+
+---
+
+#### POST /client/check-details
+
+Get client purchase history with details.
+
+**Request**:
+```json
+{
+  "phone": "79001234567"
+}
+```
+
+**Response**:
+```json
+{
+  "response": {
+    "checks": [
+      {
+        "id": 123456,
+        "store": {
+          "id": "guid-store-1",
+          "name": "Магазин на Ленина"
+        },
+        "number": "00123",
+        "payment": [
+          {"type": "cash"},
+          {"type": "card"}
+        ],
+        "date": "2025-01-14T15:30:00+00:00",
+        "sum": 2500.00,
+        "discount": 250.00,
+        "order_id": "ORD-123",
+        "seller_id": "SELLER-456",
+        "products": [
+          {
+            "product_id": "PROD-789",
+            "quantity": 5.0,
+            "price": 500.00,
+            "discount": 50.00
+          }
+        ],
+        "bonuses": [
+          {
+            "name": "Начисление бонусов за покупку",
+            "amount": 250
+          },
+          {
+            "name": "Списание бонусов",
+            "amount": -100
+          }
+        ]
+      }
+    ],
+    "pages": {
+      "totalCount": 45,
+      "page": 0,
+      "per-page": 20
+    }
+  }
+}
+```
+
+---
+
+#### POST /client/check-detail
+
+Get details of a specific check/sale.
+
+**Request**:
+```json
+{
+  "check_id": 123456
+}
+```
+
+**Response**: Same structure as single check in `/client/check-details`
+
+---
+
+#### POST /client/bonus-write-off
+
+Get bonus write-off history for client.
+
+**Request**:
+```json
+{
+  "phone": "79001234567"
+}
+```
+
+**Response**:
+```json
+{
+  "response": {
+    "bonuses": [
+      {
+        "name": "Списание бонусов за покупку",
+        "amount": -100,
+        "check_id": "123456",
+        "date": "2025-01-14T15:30:00+00:00"
+      },
+      {
+        "name": "Начисление за регистрацию",
+        "amount": 500,
+        "check_id": null,
+        "date": "2024-12-01T10:00:00+00:00"
+      }
+    ],
+    "pages": {
+      "totalCount": 25,
+      "page": 0,
+      "per-page": 20
+    }
+  }
+}
+```
+
+---
+
+#### POST /client/use-bonuses
+
+Write off bonuses for an order.
+
+**Request**:
+```json
+{
+  "order_id": "ORDER-123",
+  "phone": "79001234567",
+  "points_to_use": 150,
+  "date": 1705243200,
+  "price": 2500.00
+}
+```
+
+**Success Response**:
+```json
+{
+  "response": {
+    "code": 200,
+    "status": "success",
+    "data": {
+      "client_id": 12345,
+      "order_id": "ORDER-123",
+      "addedPoints": 150,
+      "remainingPoints": 1100
+    }
+  }
+}
+```
+
+**Error Response**:
+```json
+{
+  "error": {
+    "code": 403,
+    "message": "Недостаточно бонусов для списания"
+  }
+}
+```
+
+---
+
+#### POST /client/add-bonus
+
+Add bonus points for an order.
+
+**Request**:
+```json
+{
+  "order_id": "ORDER-123",
+  "phone": "79001234567",
+  "points_to_add": 250,
+  "date": 1705243200,
+  "price": 2500.00
+}
+```
+
+**Response**:
+```json
+{
+  "response": {
+    "code": 200,
+    "status": "success",
+    "data": {
+      "phone": "79001234567",
+      "order_id": "ORDER-123",
+      "addedPoints": 250,
+      "totalPoints": 1500
+    }
+  }
+}
+```
+
+---
+
+#### POST /client/bonus-status
+
+Get client's bonus level status.
+
+**Request**:
+```json
+{
+  "phone": "79001234567"
+}
+```
+
+**Response**:
+```json
+{
+  "response": {
+    "phone": "79001234567",
+    "alias": "gold",
+    "bonus_level": "Золотой",
+    "current_points": 5000,
+    "next_points": 10000,
+    "discount_percent": 15,
+    "cashback_rate": 10
+  }
+}
+```
+
+---
+
+#### POST /client/event-edit
+
+Add or update memorable dates for client.
+
+**Request**:
+```json
+{
+  "phone": "79001234567",
+  "channel": "salebot",
+  "events": [
+    {
+      "number": 1,
+      "date": "15.05.1990",
+      "tip": "День рождения"
+    },
+    {
+      "number": 2,
+      "date": "14.02.2015",
+      "tip": "День свадьбы"
+    }
+  ]
+}
+```
+
+**Response**:
+```json
+{
+  "response": true
+}
+```
+
+---
+
+#### POST /client/memorable-dates
+
+Get client's memorable dates.
+
+**Request**:
+```json
+{
+  "phone": "79001234567"
+}
+```
+
+**Response**:
+```json
+{
+  "response": [
+    {
+      "date": "15.05.1990",
+      "number": 1,
+      "tip": "День рождения"
+    },
+    {
+      "date": "14.02.2015",
+      "number": 2,
+      "tip": "День свадьбы"
+    }
+  ]
+}
+```
+
+---
+
+#### POST /client/show-keycode
+
+Send QR code with keycode to client via Telegram.
+
+**Request**:
+```json
+{
+  "phone": "79001234567",
+  "platform_id": 123456
+}
+```
+
+**Response**: Telegram API response from sending QR code message
+
+---
+
+#### POST /client/store-geo
+
+Send store locations based on client geolocation.
+
+**Request**:
+```json
+{
+  "location": "55.7558,37.6173",
+  "platform_id": 123456
+}
+```
+
+**Response**: Telegram API response with stores near location
+
+---
+
+#### POST /client/apply-promo-code
+
+Apply promotional code to client's account.
+
+**Request**:
+```json
+{
+  "phone": "79001234567",
+  "code": "SPRING2025"
+}
+```
+
+**Success Response**:
+```json
+{
+  "response": ["ok"]
+}
+```
+
+**Error Response**:
+```json
+{
+  "error_id": 2,
+  "error": "Промокод не известен"
+}
+```
+
+```json
+{
+  "error_id": 3,
+  "error": "Промокод уже использован"
+}
+```
+
+---
+
+#### POST /client/get-user-info
+
+Get detailed user statistics and platform subscription info.
+
+**Request**:
+```json
+{
+  "phone": "79001234567"
+}
+```
+
+**Response**:
+```json
+{
+  "response": {
+    "name": "Иван Иванов",
+    "sex": "man",
+    "sale_avg_price": 2500.00,
+    "total_price": 45000.00,
+    "registration_date": "2024-01-15",
+    "bonus_balance": 1250,
+    "bonus_minus": 300,
+    "date_first_sale": "2024-01-20 14:30:00",
+    "date_last_sale": "2025-01-10 16:45:00",
+    "sale_cnt": 18,
+    "total_price_rejected": 500.00,
+    "events": [...],
+    "platform": {
+      "telegram": {
+        "is_subscribed": 1,
+        "created_at": "2024-06-15 10:30:00"
+      }
+    }
+  }
+}
+```
+
+---
+
+#### POST /client/change-user-subscription
+
+Update client's Telegram subscription status.
+
+**Request**:
+```json
+{
+  "phone": "79001234567",
+  "telegram_is_subscribed": 1  // 1 = subscribed, 0 = unsubscribed
+}
+```
+
+**Response**:
+```json
+{
+  "response": true
+}
+```
+
+---
+
+#### GET /client/get-stores
+
+Get list of all active stores.
+
+**Response**:
+```json
+{
+  "response": [
+    {
+      "id": 1,
+      "name": "Магазин на Ленина"
+    },
+    {
+      "id": 2,
+      "name": "Магазин в ТЦ Галерея"
+    }
+  ]
+}
+```
+
+---
+
+#### POST /client/phone-keycode-by-card
+
+Get phone and keycode by card number.
+
+**Request**:
+```json
+{
+  "card": "1234567890"
+}
+```
+
+**Response**:
+```json
+{
+  "response": {
+    "phone": "79001234567",
+    "keycode": "1234"
+  }
+}
+```
+
+---
+
+#### POST /client/social-ids
+
+Get client's social platform IDs.
+
+**Request**:
+```json
+{
+  "phone": "79001234567"
+}
+```
+
+**Response**:
+```json
+{
+  "response": [
+    {
+      "platform": "telegram",
+      "user_id": 123456789
+    }
+  ]
+}
+```
+
+---
+
+### Employee Controller
+
+**File**: `erp24/api2/controllers/EmployeeController.php`
+
+Employee and shift management endpoints.
+
+#### POST /employee/is-admin-on-shift
+
+Check if employee is currently on shift.
+
+**Request**:
+```json
+{
+  "store_guid": "guid-store-1",
+  "seller_id": "guid-seller-1"
+}
+```
+
+**Response**:
+```json
+{
+  "response": {
+    "is_admin_on_shift": true
+  }
+}
+```
+
+**Logic**: Checks if employee has active shift within the last 12 hours.
+
+---
+
+#### POST /employee/is-admin-end-shift
+
+Check if employee's shift has ended.
+
+**Request**:
+```json
+{
+  "store_guid": "guid-store-1",
+  "seller_id": "guid-seller-1"
+}
+```
+
+**Response**:
+```json
+{
+  "response": {
+    "is_admin_on_shift": false
+  }
+}
+```
+
+**Logic**: Checks if current time is between shift start and end time.
+
+---
+
+#### GET /employee/get-all-admins
+
+Get all employees with valid GUIDs and phones.
+
+**Response**:
+```json
+[
+  {
+    "id": 123,
+    "guid": "admin-guid-1",
+    "name": "Петрова Мария Ивановна",
+    "phone": "79001234567"
+  },
+  {
+    "id": 124,
+    "guid": "admin-guid-2",
+    "name": "Сидоров Иван Петрович",
+    "phone": "79007654321"
+  }
+]
+```
+
+---
+
+#### POST /employee/at-store
+
+Get employees currently checked in at a specific store.
+
+**Request**:
+```json
+{
+  "guid": "guid-store-1"
+}
+```
+
+**Response**:
+```json
+["admin-guid-1", "admin-guid-2"]
+```
+
+**Logic**: Returns GUIDs of employees who checked in once today (not checked out).
+
+---
+
+## Business Endpoints
+
+### Bonus Controller
+
+**File**: `erp24/api2/controllers/BonusController.php`
+
+Bonus and loyalty program endpoints.
+
+#### POST /bonus/get-bonuses
+
+Calculate available bonuses for a purchase.
+
+**Request**:
+```json
+{
+  "store_id": "guid-store-1",
+  "seller_id": "guid-seller-1",
+  "phone": "79001234567",
+  "check_amount": 2500,
+  "items": [
+    {
+      "product_id": "guid-product-1",
+      "price": 500,
+      "quantity": 5
+    }
+  ]
+}
+```
+
+**Success Response**:
+```json
+{
+  "result": true,
+  "auth_code": "1234",
+  "name": "Иван Иванов",
+  "total_bonuses": 1250,
+  "bonus_level": "gold",
+  "burn_balans": 200,
+  "available_bonuses": 250,
+  "will_be_credited_bonuses": 250,
+  "message_cashier": "Клиент Иван Иванов найден 1250"
+}
+```
+
+**Key Logic**:
+- Excludes products in `unused_nomenclature` from bonus accrual
+- Excludes products in `non_bonusable_goods` from bonus write-off
+- Maximum write-off is determined by bonus level percentage
+- Special handling for new Telegram subscribers (30% bonus on first purchase)
+
+---
+
+#### POST /bonus/send-message
+
+Initiate phone call verification for bonus program.
+
+**Request**:
+```json
+{
+  "store_id": "guid-store-1",
+  "seller_id": "guid-seller-1",
+  "phone": "79001234567"
+}
+```
+
+**Response**:
+```json
+{
+  "auth_code": "5472",
+  "message_cashier": "Попытка:1 Звонок клиенту! последние 4 цифры номера",
+  "timeout": 15
+}
+```
+
+**Logic**:
+- Uses SMS.ru call verification service
+- Limits to 2 call attempts within 10 minutes
+- Generates 4-digit keycode as last 4 digits of incoming call
+
+---
+
+#### POST /bonus/save-client-info
+
+Save or update client information for bonus program.
+
+**Request**:
+```json
+{
+  "store_id": "guid-store-1",
+  "seller_id": "guid-seller-1",
+  "phone": "79001234567",
+  "first_name": "Иван",
+  "second_name": "Иванов",
+  "sex": "male",              // "male" | "female"
+  "birth_day": "1990-05-15",
+  "referral_id": 98765,
+  "comment": "VIP клиент",
+  "events": [
+    {
+      "number": 1,
+      "date": "15.05.1990",
+      "tip": "День рождения"
+    }
+  ],
+  "source": 1                 // Optional: traffic source
+}
+```
+
+**Response**: Similar to `/bonus/get-bonuses`
+
+---
+
+### Orders Controller
+
+**File**: `erp24/api2/controllers/OrdersController.php`
+
+Marketplace order management endpoints.
+
+#### POST /orders/change-status
+
+Update marketplace order status (called from 1C).
+
+**Request**:
+```json
+{
+  "order": [
+    {
+      "order_id": "order-guid-1",
+      "status": 5,
+      "seller_id": "seller-guid-1"
+    },
+    {
+      "order_id": "order-guid-2",
+      "status": 10
+    }
+  ]
+}
+```
+
+**Response**:
+```json
+[
+  {
+    "order_id": "order-guid-1",
+    "result": true,
+    "message": "Статус обновлён",
+    "status": 12
+  },
+  {
+    "order_id": "order-guid-2",
+    "result": "error",
+    "message": "Заказ не найден"
+  }
+]
+```
+
+**Key Logic**:
+- Maps 1C statuses to marketplace statuses (Flowwow, Yandex Market)
+- Automatically updates Yandex Market via API for certain statuses
+- Tracks order cancellation source and date
+- Creates status change history
+
+---
+
+#### POST /orders/get-orders
+
+Get orders for a specific store (last 24 hours).
+
+**Request**:
+```json
+{
+  "store_id": "guid-store-1"
+}
+```
+
+**Response**:
+```json
+{
+  "success": true,
+  "result": [
+    {
+      "order_id": "order-guid-1",
+      "status": 5,
+      "items": [
+        {
+          "product_id": "prod-guid-1",
+          "quantity": 2.0,
+          "price": 1500.00
+        }
+      ]
+    }
+  ]
+}
+```
+
+---
+
+## Integration Endpoints
+
+### Telegram Controller
+
+**File**: `erp24/api2/controllers/TelegramController.php`
+
+Telegram bot webhook and messaging endpoints.
+
+#### GET /telegram/set-webhook
+
+Configure Telegram webhook URL.
+
+**Parameters**: `on=1` (enable) or `on=0` (disable)
+
+**Response**: `"OK"` or `"NOT OK"`
+
+---
+
+#### POST /telegram/webhook
+
+Telegram bot webhook handler.
+
+**Handles**:
+- Incoming messages from Telegram users
+- Callback queries (button presses)
+- Bot commands
+- User registration and authentication
+
+**Auto-processed** - not called directly by external systems.
+
+---
+
+#### GET /telegram/send-message
+
+Send message to employee via Telegram.
+
+**Parameters**:
+- `admin_id` - Employee ID
+- `message` - Message text (HTML supported)
+- `reply_markup` - Optional JSON keyboard markup
+
+**Response**: JSON array of send results for all employee chats
+
+**Example**:
+```bash
+curl "http://localhost:5555/telegram/send-message? \
+  admin_id=123&message=<b>Новая задача!</b>"
+```
+
+---
+
+### Marketplace Controller
+
+**File**: `erp24/api2/controllers/MarketplaceController.php`
+
+Marketplace integration management.
+
+#### GET /marketplace/statuses
+
+Get all marketplace statuses.
+
+**Response**:
+```json
+{
+  "response": [
+    {
+      "id": 1,
+      "code": "NEW",
+      "name": "Новый заказ",
+      "marketplace_id": 1
+    },
+    {
+      "id": 2,
+      "code": "PROCESSING",
+      "name": "В обработке",
+      "marketplace_id": 1
+    }
+  ]
+}
+```
+
+---
+
+#### POST /marketplace/get-new-order-count
+
+Get count of new unprocessed orders for a store.
+
+**Request**:
+```json
+{
+  "store_guid": "guid-store-1"
+}
+```
+
+**Response**:
+```json
+{
+  "response": 5
+}
+```
+
+**Logic**: Counts orders with `status_1c=1`, `status_id=1`, created within last 3 days.
+
+---
+
+#### POST /marketplace/instruction-dictionary
+
+Get status transition instructions for marketplace orders.
+
+**Request**:
+```json
+{
+  "guid": "order-guid-1"
+}
+```
+
+**Response**:
+```json
+{
+  "response": [
+    {
+      "marketplace": "ФлауВау",
+      "status": "Новый",
+      "status_id": 1,
+      "status_instruction": "Принять заказ в работу",
+      "status_relations": [
+        {
+          "status": "В работе",
+          "status_id": 2,
+          "description": "Заказ принят флористом",
+          "button_text": "Принять",
+          "order": 1
+        },
+        {
+          "status": "Отменен",
+          "status_id": 99,
+          "description": "Отменить заказ",
+          "button_text": "Отменить",
+          "order": 2
+        }
+      ]
+    }
+  ]
+}
+```
+
+---
+
+## Error Handling
+
+### Standard Error Codes
+
+| Error ID | Description | HTTP Status |
+|----------|-------------|-------------|
+| `0` | Missing required parameter | 400 |
+| `0.1` | Invalid parameter format | 400 |
+| `0.2` | Phone validation failed | 400 |
+| `1` | Database validation error | 400 |
+| `1.2` | Phone format invalid | 400 |
+| `2` | Entity not found | 404 |
+| `3` | Permission denied / Blacklist | 403 |
+| `400` | Bad request | 400 |
+| `403` | Forbidden | 403 |
+| `404` | Not found | 404 |
+| `500` | Internal server error | 500 |
+
+### Error Response Examples
+
+**Missing Parameter**:
+```json
+{
+  "error_id": 0,
+  "error": "phone is required"
+}
+```
+
+**Invalid Phone**:
+```json
+{
+  "error_id": 1.2,
+  "error": "phone is required"
+}
+```
+
+**Blacklisted Client**:
+```json
+{
+  "error": "Этот номер в черном списке"
+}
+```
+
+**Not Found**:
+```json
+{
+  "error": {
+    "code": 404,
+    "message": "Пользователь не найден"
+  }
+}
+```
+
+**Validation Error**:
+```json
+{
+  "error": {
+    "code": 400,
+    "message": "Ошибка валидации",
+    "details": {
+      "phone": ["Phone cannot be blank"],
+      "amount": ["Amount must be greater than 0"]
+    }
+  }
+}
+```
+
+---
+
+## Rate Limiting
+
+Currently **no rate limiting** is enforced on API2.
+
+**Recommendations**:
+- Implement rate limiting in production
+- Suggested limits:
+  - Authentication endpoints: 5 req/min per IP
+  - Read endpoints: 60 req/min per token
+  - Write endpoints: 30 req/min per token
+
+---
+
+## Phone Number Format
+
+All endpoints accepting phone numbers expect **Russian phone format**:
+
+- **Format**: `79001234567` (11 digits, starts with 7)
+- **Invalid**: `+7 900 123 45 67`, `8 900 123 45 67`, `9001234567`
+
+**Phone Validation** (`ClientHelper::phoneClear` + `ClientHelper::phoneVerify`):
+```php
+// Accepted formats (all converted to 79001234567):
+"+7 900 123 45 67"
+"8 900 123 45 67"
+"7 900 123 45 67"
+"900 123 45 67"
+"9001234567"
+
+// All normalize to: "79001234567"
+```
+
+---
+
+## Data Controller
+
+Additional data endpoints:
+
+### GET /data/get-stores
+Get all active city stores
+
+### GET /data/get-cities
+Get all cities with stores
+
+### GET /data/get-bonus-levels
+Get bonus level configurations
+
+---
+
+## Testing
+
+### Postman Collection
+
+Available at: `/erp24/api2/swagger/postman_collection.json`
+
+### Test Endpoints
+
+```bash
+# Health check
+curl http://localhost:5555/site/index
+
+# Get stores
+curl http://localhost:5555/client/get-stores
+
+# Test authentication
+curl -X POST http://localhost:5555/auth/login \
+  -H "Content-Type: application/json" \
+  -d '{"login": "test", "password": "test123"}'
+```
+
+---
+
+## Swagger Documentation
+
+Access interactive API documentation:
+
+```
+http://localhost:5555/swagger/
+```
+
+Swagger YAML location: `/erp24/api2/swagger/swagger.yaml`
+
+---
+
+## Migration from API1
+
+If you're currently using API1, see [API1 to API2 Migration Guide](../api1/migration-to-api2.md).
+
+**Key Differences**:
+- JSON-only (no plain text responses)
+- Consistent error format
+- Token authentication instead of session
+- Proper HTTP status codes
+
+---
+
+## Additional Controllers
+
+### Task Controller
+Task management endpoints (implementation details TBD)
+
+### Universal Catalog Controller
+Product catalog endpoints (implementation details TBD)
+
+### Chatbot Action Controller
+Chatbot integration endpoints (implementation details TBD)
+
+### Telegram Salebot Controller
+Sales bot automation endpoints (implementation details TBD)
+
+### Yandex Market Controller
+Yandex Market specific integration endpoints (implementation details TBD)
+
+### Delivery Controller
+Delivery tracking and management endpoints (implementation details TBD)
+
+### KIK Controller
+Customer feedback (KIK) endpoints (implementation details TBD)
+
+### Store Controller
+Store management endpoints (implementation details TBD)
+
+### Balance Controller
+Financial balance endpoints (implementation details TBD)
+
+### Data Buh Controller
+Accounting data endpoints (implementation details TBD)
+
+---
+
+## Best Practices
+
+### 1. Always Validate Phone Numbers
+```javascript
+function normalizePhone(phone) {
+  // Remove all non-digits
+  phone = phone.replace(/\D/g, '');
+
+  // Handle 8xxx format
+  if (phone.startsWith('8') && phone.length === 11) {
+    phone = '7' + phone.substring(1);
+  }
+
+  // Handle xxx format (no country code)
+  if (phone.length === 10) {
+    phone = '7' + phone;
+  }
+
+  return phone;
+}
+```
+
+### 2. Handle Errors Gracefully
+```javascript
+try {
+  const response = await fetch('/api2/client/balance', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+      'Authorization': `Bearer ${token}`
+    },
+    body: JSON.stringify({ phone: normalizePhone(phone) })
+  });
+
+  const data = await response.json();
+
+  if (data.error || data.error_id) {
+    console.error('API Error:', data.error);
+    // Handle error
+  } else {
+    // Success
+    console.log('Balance:', data.balance);
+  }
+} catch (error) {
+  console.error('Network Error:', error);
+}
+```
+
+### 3. Use Async Operations
+For heavy operations like `/client/retrieve`, consider implementing queue-based processing.
+
+### 4. Cache Reference Data
+Cache responses from endpoints like `/client/get-stores`, `/data/get-bonus-levels`.
+
+---
+
+## Support
+
+For API2 issues:
+- Check logs: `/erp24/api2/runtime/logs/app.log`
+- Review JSON debug files: `/erp24/api2/json/`
+- Contact: development team
+
+---
+
+**Last Updated**: January 2025
+**API Version**: 2.0 (Active Development)
+**Maintained By**: ERP24 Development Team
diff --git a/docs/architecture/api-architecture.md b/docs/architecture/api-architecture.md
new file mode 100644 (file)
index 0000000..74b4eb7
--- /dev/null
@@ -0,0 +1,1242 @@
+# Three-Layer API Architecture
+
+> **Comprehensive guide to ERP24's unique three-layer API design**
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Architecture Rationale](#architecture-rationale)
+- [API Layer Comparison](#api-layer-comparison)
+- [API1 - Legacy Layer](#api1---legacy-layer)
+- [API2 - Modern REST Layer](#api2---modern-rest-layer)
+- [API3 - Advanced Layer](#api3---advanced-layer)
+- [Request/Response Flow](#requestresponse-flow)
+- [Authentication & Authorization](#authentication--authorization)
+- [Error Handling](#error-handling)
+- [Best Practices](#best-practices)
+
+---
+
+## Overview
+
+ERP24 implements a unique **three-layer API architecture** to serve different integration needs while maintaining backward compatibility and enabling modern API patterns.
+
+```mermaid
+graph TB
+    subgraph "Client Types"
+        CRON[Cron Jobs<br/>& Scripts]
+        LEGACY[Legacy<br/>Integrations]
+        MOBILE[Mobile<br/>Apps]
+        WEB[Web<br/>Clients]
+        PARTNER[Partner<br/>Systems]
+        MODERN[Modern<br/>Clients]
+    end
+
+    subgraph "API Layers"
+        API1[API1 - Legacy<br/>Port 4444<br/>Cron & Compatibility]
+        API2[API2 - REST<br/>Port 5555<br/>Primary API]
+        API3[API3 - Advanced<br/>Port 8888<br/>Versioned & Clean]
+    end
+
+    subgraph "Shared Business Layer"
+        SVC[Services<br/>51 Business Logic Classes]
+        ACT[Actions<br/>223 Operation Classes]
+        AR[ActiveRecord<br/>389 Data Models]
+    end
+
+    CRON --> API1
+    LEGACY --> API1
+    LEGACY --> API2
+
+    MOBILE --> API2
+    WEB --> API2
+    PARTNER --> API2
+
+    MODERN --> API3
+    MOBILE --> API3
+
+    API1 --> SVC
+    API2 --> SVC
+    API3 --> SVC
+
+    API1 --> ACT
+    API2 --> ACT
+    API3 --> ACT
+
+    SVC --> AR
+    ACT --> AR
+
+    style API1 fill:#ffcdd2
+    style API2 fill:#c8e6c9
+    style API3 fill:#bbdefb
+    style SVC fill:#fff9c4
+```
+
+### Key Characteristics
+
+| Feature | API1 | API2 | API3 |
+|---------|------|------|------|
+| **Port** | 4444 | 5555 | 8888 |
+| **Purpose** | Legacy, Cron | Primary REST | Advanced, Versioned |
+| **Design** | Mixed patterns | RESTful | Clean Architecture |
+| **Controllers** | 3 | 21 | 11+ (v1 module) |
+| **Versioning** | None | None | Module-based (v1, v2...) |
+| **Documentation** | Minimal | Swagger available | OpenAPI planned |
+| **Primary Users** | Internal systems | Mobile, Web, Partners | Modern clients |
+| **Authentication** | Token/Session | Token/Session | Token/JWT |
+| **Status** | Maintenance mode | Active development | Future-focused |
+
+---
+
+## Architecture Rationale
+
+### Why Three APIs?
+
+#### 1. **Backward Compatibility** (API1)
+- Maintain legacy cron jobs without breaking changes
+- Support old integration code during migration period
+- Isolate deprecated endpoints from active development
+
+#### 2. **Primary Operations** (API2)
+- Serve current mobile applications
+- Handle external integrations (Telegram, marketplaces)
+- Provide stable REST interface for web clients
+
+#### 3. **Future-Proof Design** (API3)
+- Implement clean architecture patterns
+- Enable API versioning strategy
+- Experiment with modern patterns before migrating API2
+
+### Design Principles
+
+```mermaid
+graph LR
+    A[Separation of Concerns] --> B[Independent Evolution]
+    B --> C[Gradual Migration]
+    C --> D[Risk Mitigation]
+
+    E[Shared Business Logic] --> F[Code Reuse]
+    F --> G[Consistency]
+    G --> H[Maintainability]
+
+    style A fill:#e3f2fd
+    style E fill:#fff3e0
+```
+
+**Benefits**:
+- ✅ No breaking changes for existing integrations
+- ✅ Freedom to innovate in API3 without affecting stability
+- ✅ Shared business logic ensures consistency
+- ✅ Clear migration path from API1 → API2 → API3
+
+**Trade-offs**:
+- ❌ Multiple codebases to maintain
+- ❌ Potential for duplicated logic
+- ❌ Requires clear documentation of which API to use
+
+---
+
+## API Layer Comparison
+
+### Detailed Feature Comparison
+
+| Aspect | API1 | API2 | API3 |
+|--------|------|------|------|
+| **Location** | `/erp24/api1/` | `/erp24/api2/` | `/erp24/api3/` |
+| **Entry Point** | `api1/index.php` | `api2/index.php` | `api3/web/index.php` |
+| **Config** | `api1/config/api1.config.php` | `api2/config/api2.config.php` | `api3/config/main.php` |
+| **URL Format** | `/api1/<controller>/<action>` | `/api2/<controller>/<action>` | `/api3/v1/<module>/<resource>` |
+| **Request Format** | Query params / Form data | JSON / Form data | JSON only |
+| **Response Format** | JSON / Plain text | JSON | JSON (structured) |
+| **Error Format** | Mixed | JSON with status codes | Standardized JSON |
+| **CORS** | Limited | Enabled | Full support |
+| **Rate Limiting** | None | Basic | Advanced |
+| **Caching** | None | Basic | ETags, Cache-Control |
+| **Pagination** | Manual | Basic (page/per-page) | Cursor-based available |
+| **Filtering** | Limited | Query parameters | Advanced filters |
+| **Sorting** | Limited | Query parameters | Multi-field sorting |
+
+---
+
+## API1 - Legacy Layer
+
+### Purpose & Use Cases
+
+**Primary Purpose**: Backward compatibility and scheduled tasks
+
+**Use Cases**:
+- ✅ Cron jobs (bonus synchronization, data cleanup)
+- ✅ Legacy integration scripts
+- ✅ Administrative automation
+- ✅ Internal system communication
+
+**Status**: 🟡 Maintenance mode - No new features, bug fixes only
+
+### Architecture
+
+**Location**: `/erp24/api1/`
+
+**Structure**:
+```
+api1/
+├── actions/
+│   └── cron/                   # Cron job actions (9 files)
+│       ├── RunUpdateAction.php
+│       ├── RunAmoCRMAction.php
+│       └── ...
+├── controllers/
+│   ├── AuthController.php      # Authentication
+│   ├── BaseController.php      # Base functionality
+│   └── CronController.php      # Cron job endpoints
+├── records/                     # Legacy-specific models
+│   ├── amocrm/
+│   └── cloudpayments/
+├── config/
+│   └── api1.config.php         # API1 configuration
+└── index.php                   # Entry point
+```
+
+### Controllers
+
+#### 1. AuthController
+**File**: `erp24/api1/controllers/AuthController.php`
+
+**Purpose**: Handle authentication for API1 endpoints
+
+**Endpoints**:
+- `POST /api1/auth/login` - Authenticate user
+- `POST /api1/auth/logout` - End session
+- `GET /api1/auth/verify` - Verify token
+
+#### 2. CronController
+**File**: `erp24/api1/controllers/CronController.php`
+
+**Purpose**: Execute scheduled background tasks
+
+**Endpoints**:
+- `GET /api1/cron/update` - Run system updates
+- `GET /api1/cron/amocrm` - Synchronize AmoCRM data
+- `GET /api1/cron/cloudpayments` - Process payments
+- `GET /api1/cron/bonus` - Sync bonus points
+
+**Example Usage**:
+```bash
+# Cron job configuration
+0 */4 * * * curl http://localhost:4444/api1/cron/amocrm
+0 1 * * * curl http://localhost:4444/api1/cron/bonus
+```
+
+#### 3. BaseController
+**File**: `erp24/api1/controllers/BaseController.php`
+
+**Purpose**: Common functionality for API1 controllers
+
+**Features**:
+- Request validation
+- Error handling
+- Response formatting
+- Authentication checks
+
+### Cron Actions
+
+Located in `/erp24/api1/actions/cron/`
+
+**Available Actions**:
+```php
+RunUpdateAction.php              // System updates
+RunAmoCRMAction.php             // AmoCRM synchronization
+RunCloudPaymentsAction.php      // Payment processing
+RunBonusAction.php              // Bonus point sync
+RunCleanupAction.php            // Database cleanup
+RunReportAction.php             // Generate reports
+RunNotificationAction.php       // Send pending notifications
+RunBackupAction.php             // Database backups
+RunAnalyticsAction.php          // Analytics aggregation
+```
+
+### Migration Strategy from API1
+
+**Timeline**: API1 → Deprecated (2026) → Removed (2027)
+
+**Steps**:
+1. ✅ Document all API1 endpoints
+2. ✅ Create equivalent API2 endpoints
+3. 🔄 Migrate cron jobs to console commands
+4. 🔄 Update client integrations
+5. ⏳ Monitor API1 usage (logging)
+6. ⏳ Deprecation notice (12 months before removal)
+7. ⏳ Remove API1 code
+
+---
+
+## API2 - Modern REST Layer
+
+### Purpose & Use Cases
+
+**Primary Purpose**: Main REST API for mobile apps and integrations
+
+**Use Cases**:
+- ✅ Mobile applications (iOS, Android)
+- ✅ Web application backend
+- ✅ Partner integrations
+- ✅ Marketplace connectors (Yandex, Flowwow)
+- ✅ Telegram bot webhooks
+- ✅ Real-time data access
+
+**Status**: 🟢 Active development - Primary API layer
+
+### Architecture
+
+**Location**: `/erp24/api2/`
+
+**Structure**:
+```
+api2/
+├── controllers/                # 21 REST controllers
+│   ├── AuthController.php
+│   ├── BalanceController.php
+│   ├── BonusController.php
+│   ├── ClientController.php
+│   ├── DataController.php
+│   ├── DeliveryController.php
+│   ├── EmployeeController.php
+│   ├── KikController.php
+│   ├── MarketplaceController.php
+│   ├── OrdersController.php
+│   ├── StoreController.php
+│   ├── TaskController.php
+│   ├── TelegramController.php
+│   └── ...
+├── swagger/                    # API documentation
+│   └── swagger.yaml
+├── config/
+│   ├── api2.config.php
+│   ├── dev.api2.config.php
+│   └── env.php
+├── runtime/                    # Runtime files
+├── cache/                      # Cache directory
+└── index.php                   # Entry point
+```
+
+### Controller Overview
+
+#### Core Controllers (10)
+
+| Controller | Purpose | Key Endpoints |
+|------------|---------|---------------|
+| **AuthController** | Authentication & sessions | `/login`, `/logout`, `/refresh` |
+| **ClientController** | Client management | `/clients`, `/clients/{id}` |
+| **EmployeeController** | Employee operations | `/employees`, `/employees/{id}/schedule` |
+| **StoreController** | Store data | `/stores`, `/stores/{id}/products` |
+| **DataController** | Reference data | `/cities`, `/grades`, `/roles` |
+| **BalanceController** | Financial balances | `/balance/client/{id}`, `/balance/store/{id}` |
+| **BonusController** | Loyalty program | `/bonus/accrue`, `/bonus/history/{clientId}` |
+| **TaskController** | Task management | `/tasks`, `/tasks/{id}/complete` |
+| **OrdersController** | Order processing | `/orders`, `/orders/{id}/status` |
+| **DeliveryController** | Delivery tracking | `/delivery/track/{id}`, `/delivery/assign` |
+
+#### Integration Controllers (6)
+
+| Controller | Purpose | Integration |
+|------------|---------|-------------|
+| **TelegramController** | Telegram bot | Telegram Bot API |
+| **TelegramSalebotController** | Sales bot | Automated sales bot |
+| **MarketplaceController** | Marketplace ops | Flowwow |
+| **YandexMarketController** | Yandex integration | Yandex Market API |
+| **KikController** | Feedback system | KIK feedback |
+| **WhatsAppController** | WhatsApp messaging | WhatsApp Business API |
+
+#### Domain Controllers (5)
+
+| Controller | Purpose | Domain |
+|------------|---------|---------|
+| **PayrollController** | Payroll operations | HR & Finance |
+| **ShipmentController** | Shipment tracking | Logistics |
+| **TimetableController** | Scheduling | HR |
+| **RatingController** | Employee ratings | HR |
+| **LessonController** | Training system | HR |
+
+### Authentication Flow
+
+```mermaid
+sequenceDiagram
+    participant C as Client
+    participant API as API2
+    participant DB as Database
+    participant Cache as Cache
+
+    C->>API: POST /api2/auth/login
+    Note over C,API: {phone: "79001234567", password: "***"}
+
+    API->>DB: Validate credentials
+    DB-->>API: User record
+
+    alt Valid credentials
+        API->>Cache: Store session
+        Cache-->>API: Session created
+        API-->>C: 200 OK
+        Note over API,C: {token: "abc123", user: {...}}
+    else Invalid credentials
+        API-->>C: 401 Unauthorized
+        Note over API,C: {error: "Invalid credentials"}
+    end
+
+    Note over C: Subsequent requests
+
+    C->>API: GET /api2/client/profile
+    Note over C,API: Header: Authorization: Bearer abc123
+
+    API->>Cache: Validate token
+    Cache-->>API: Session data
+
+    alt Valid token
+        API->>DB: Fetch profile
+        DB-->>API: Profile data
+        API-->>C: 200 OK + data
+    else Invalid token
+        API-->>C: 401 Unauthorized
+    end
+```
+
+### Request/Response Examples
+
+#### Example 1: Get Client Bonus Balance
+
+**Request**:
+```http
+GET /api2/bonus/balance/12345 HTTP/1.1
+Host: localhost:5555
+Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
+Accept: application/json
+```
+
+**Response**:
+```json
+{
+  "success": true,
+  "data": {
+    "client_id": 12345,
+    "bonus_balance": 1250.50,
+    "pending_bonuses": 200.00,
+    "lifetime_earned": 5430.75,
+    "lifetime_spent": 4180.25,
+    "tier": "gold",
+    "next_tier_points": 249.50
+  },
+  "timestamp": "2025-01-14T12:43:32Z"
+}
+```
+
+#### Example 2: Accrue Bonus Points
+
+**Request**:
+```http
+POST /api2/bonus/accrue HTTP/1.1
+Host: localhost:5555
+Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
+Content-Type: application/json
+
+{
+  "client_id": 12345,
+  "amount": 150.00,
+  "source": "purchase",
+  "reference_id": "ORDER-2025-001234",
+  "description": "Bonus for order #1234"
+}
+```
+
+**Response**:
+```json
+{
+  "success": true,
+  "data": {
+    "history_id": 67890,
+    "client_id": 12345,
+    "amount": 150.00,
+    "new_balance": 1400.50,
+    "accrued_at": "2025-01-14T12:45:00Z",
+    "expires_at": "2026-01-14T23:59:59Z"
+  },
+  "timestamp": "2025-01-14T12:45:00Z"
+}
+```
+
+#### Example 3: Get Employee Schedule
+
+**Request**:
+```http
+GET /api2/employee/schedule/456?start_date=2025-01-14&end_date=2025-01-20 HTTP/1.1
+Host: localhost:5555
+Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
+Accept: application/json
+```
+
+**Response**:
+```json
+{
+  "success": true,
+  "data": {
+    "employee_id": 456,
+    "employee_name": "Иванова Мария",
+    "period": {
+      "start": "2025-01-14",
+      "end": "2025-01-20"
+    },
+    "schedule": [
+      {
+        "date": "2025-01-14",
+        "shifts": [
+          {
+            "shift_id": 1001,
+            "store_id": 5,
+            "store_name": "Магазин на Ленина",
+            "start_time": "09:00",
+            "end_time": "18:00",
+            "break_minutes": 60,
+            "type": "regular",
+            "status": "confirmed"
+          }
+        ]
+      },
+      {
+        "date": "2025-01-15",
+        "shifts": [
+          {
+            "shift_id": 1002,
+            "store_id": 5,
+            "store_name": "Магазин на Ленина",
+            "start_time": "10:00",
+            "end_time": "19:00",
+            "break_minutes": 60,
+            "type": "regular",
+            "status": "confirmed"
+          }
+        ]
+      },
+      {
+        "date": "2025-01-16",
+        "shifts": []
+      }
+    ],
+    "total_hours": 16.0,
+    "total_shifts": 2
+  },
+  "timestamp": "2025-01-14T12:46:00Z"
+}
+```
+
+### Error Handling
+
+**Standard Error Response**:
+```json
+{
+  "success": false,
+  "error": {
+    "code": "VALIDATION_ERROR",
+    "message": "Validation failed",
+    "details": [
+      {
+        "field": "amount",
+        "message": "Amount must be greater than 0"
+      }
+    ]
+  },
+  "timestamp": "2025-01-14T12:47:00Z"
+}
+```
+
+**HTTP Status Codes**:
+- `200 OK` - Success
+- `201 Created` - Resource created
+- `400 Bad Request` - Invalid input
+- `401 Unauthorized` - Authentication required
+- `403 Forbidden` - Insufficient permissions
+- `404 Not Found` - Resource not found
+- `422 Unprocessable Entity` - Validation failed
+- `500 Internal Server Error` - Server error
+
+### Swagger Documentation
+
+**Location**: `/erp24/api2/swagger/swagger.yaml`
+
+**Access URL**: `http://localhost:5555/api2/swagger`
+
+**Features**:
+- Interactive API explorer
+- Request/response examples
+- Authentication flow documentation
+- Model schemas
+
+---
+
+## API3 - Advanced Layer
+
+### Purpose & Use Cases
+
+**Primary Purpose**: Future-focused, clean architecture API
+
+**Use Cases**:
+- ✅ New mobile app features
+- ✅ Modern single-page applications
+- ✅ Microservice integrations
+- ✅ Third-party developer APIs
+- ✅ API versioning experiments
+
+**Status**: 🔵 Future-focused - Selective feature implementation
+
+### Architecture
+
+**Location**: `/erp24/api3/`
+
+**Structure**:
+```
+api3/
+├── core/                       # Core utilities
+│   ├── BaseController.php
+│   ├── BaseService.php
+│   └── validators/
+├── modules/
+│   └── v1/                     # Version 1 module
+│       ├── controllers/        # 11+ controllers
+│       │   ├── AdminController.php
+│       │   ├── BonusController.php
+│       │   ├── ClientController.php
+│       │   ├── EmployeeController.php
+│       │   ├── claim/          # Claim subdomain
+│       │   ├── orders/         # Orders subdomain
+│       │   ├── search/         # Search subdomain
+│       │   └── timetable/      # Timetable subdomain
+│       ├── models/             # Request/Response DTOs
+│       │   ├── ClientModel.php
+│       │   └── ...
+│       ├── requests/           # Input validation
+│       │   ├── BonusRequest.php
+│       │   └── ...
+│       ├── services/           # Business logic
+│       │   ├── BonusService.php
+│       │   ├── ClientService.php
+│       │   └── ...
+│       └── Module.php          # V1 module configuration
+├── config/
+│   ├── main.php               # Main configuration
+│   ├── bootstrap.php          # Bootstrap events
+│   └── params.php             # Parameters
+├── web/
+│   └── index.php              # Entry point
+└── EventBootstrap.php         # Event system
+```
+
+### Clean Architecture Principles
+
+```mermaid
+graph TB
+    subgraph "API3 Layers"
+        CTRL[Controllers<br/>HTTP Handlers]
+        REQ[Request DTOs<br/>Input Validation]
+        SVC[Services<br/>Business Logic]
+        MOD[Models<br/>Domain Entities]
+        RESP[Response DTOs<br/>Output Formatting]
+    end
+
+    HTTP[HTTP Request] --> CTRL
+    CTRL --> REQ
+    REQ --> |Valid| SVC
+    REQ --> |Invalid| ERR[Error Response]
+    SVC --> MOD
+    MOD --> SVC
+    SVC --> RESP
+    RESP --> CTRL
+    CTRL --> JSON[JSON Response]
+
+    style REQ fill:#fff9c4
+    style SVC fill:#c8e6c9
+    style RESP fill:#bbdefb
+```
+
+**Key Differences from API1/API2**:
+
+1. **Request/Response DTOs**: Separate input and output models
+2. **Service Layer**: Dedicated service classes per domain
+3. **Validators**: Custom validation logic separated from models
+4. **Events**: Event-driven architecture with EventBootstrap
+5. **Versioning**: Module-based versioning (v1, v2, v3...)
+
+### Versioning Strategy
+
+**URL Structure**: `/api3/{version}/{module}/{resource}`
+
+**Examples**:
+```
+GET  /api3/v1/clients/12345
+POST /api3/v1/bonus/accrue
+GET  /api3/v1/timetable/employee/456/schedule
+PUT  /api3/v1/orders/7890/status
+```
+
+**Version Management**:
+```php
+// Route configuration in api3/config/main.php
+'modules' => [
+    'v1' => [
+        'class' => 'app\modules\v1\Module',
+    ],
+    'v2' => [  // Future version
+        'class' => 'app\modules\v2\Module',
+    ],
+],
+```
+
+### Controller Structure
+
+#### Core Controllers (v1)
+
+| Controller | File | Purpose |
+|------------|------|---------|
+| **AdminController** | `modules/v1/controllers/AdminController.php` | Admin operations |
+| **BonusController** | `modules/v1/controllers/BonusController.php` | Bonus system |
+| **ClientController** | `modules/v1/controllers/ClientController.php` | Client management |
+| **EmployeeController** | `modules/v1/controllers/EmployeeController.php` | Employee data |
+| **IncomeController** | `modules/v1/controllers/IncomeController.php` | Income tracking |
+| **KikController** | `modules/v1/controllers/KikController.php` | Feedback system |
+| **NotifiableController** | `modules/v1/controllers/NotifiableController.php` | Notifications |
+| **ProductController** | `modules/v1/controllers/ProductController.php` | Product catalog |
+| **ReportController** | `modules/v1/controllers/ReportController.php` | Reports |
+| **StoreController** | `modules/v1/controllers/StoreController.php` | Store management |
+| **TgController** | `modules/v1/controllers/TgController.php` | Telegram integration |
+
+#### Subdomain Controllers
+
+**Claim Module** (`modules/v1/controllers/claim/`):
+- Manage customer claims and complaints
+
+**Orders Module** (`modules/v1/controllers/orders/`):
+- Order processing workflows
+
+**Search Module** (`modules/v1/controllers/search/`):
+- Advanced search functionality
+
+**Timetable Module** (`modules/v1/controllers/timetable/`):
+- Schedule management
+
+### Request/Response Pattern
+
+#### Example: BonusController with DTOs
+
+**Request DTO** (`modules/v1/requests/BonusRequest.php`):
+```php
+namespace app\modules\v1\requests;
+
+use yii\base\Model;
+
+class BonusRequest extends Model
+{
+    public $client_id;
+    public $amount;
+    public $source;
+    public $reference_id;
+    public $description;
+
+    public function rules()
+    {
+        return [
+            [['client_id', 'amount', 'source'], 'required'],
+            ['client_id', 'integer', 'min' => 1],
+            ['amount', 'number', 'min' => 0.01],
+            ['source', 'in', 'range' => ['purchase', 'manual', 'promotion', 'referral']],
+            ['reference_id', 'string', 'max' => 100],
+            ['description', 'string', 'max' => 500],
+        ];
+    }
+}
+```
+
+**Response DTO** (`modules/v1/models/BonusResponse.php`):
+```php
+namespace app\modules\v1\models;
+
+class BonusResponse
+{
+    public $history_id;
+    public $client_id;
+    public $amount;
+    public $new_balance;
+    public $accrued_at;
+    public $expires_at;
+
+    public function __construct($historyModel, $clientModel)
+    {
+        $this->history_id = $historyModel->id;
+        $this->client_id = $clientModel->id;
+        $this->amount = $historyModel->amount;
+        $this->new_balance = $clientModel->bonus_balance;
+        $this->accrued_at = $historyModel->created_at;
+        $this->expires_at = $historyModel->expires_at;
+    }
+
+    public function toArray()
+    {
+        return [
+            'history_id' => $this->history_id,
+            'client_id' => $this->client_id,
+            'amount' => $this->amount,
+            'new_balance' => $this->new_balance,
+            'accrued_at' => $this->accrued_at,
+            'expires_at' => $this->expires_at,
+        ];
+    }
+}
+```
+
+**Controller** (`modules/v1/controllers/BonusController.php`):
+```php
+namespace app\modules\v1\controllers;
+
+use app\core\BaseController;
+use app\modules\v1\requests\BonusRequest;
+use app\modules\v1\models\BonusResponse;
+use app\modules\v1\services\BonusService;
+use yii\web\BadRequestHttpException;
+
+class BonusController extends BaseController
+{
+    private $bonusService;
+
+    public function __construct($id, $module, BonusService $bonusService, $config = [])
+    {
+        $this->bonusService = $bonusService;
+        parent::__construct($id, $module, $config);
+    }
+
+    public function actionAccrue()
+    {
+        $request = new BonusRequest();
+        $request->load(\Yii::$app->request->post(), '');
+
+        if (!$request->validate()) {
+            throw new BadRequestHttpException(json_encode($request->errors));
+        }
+
+        $result = $this->bonusService->accrueBonus(
+            $request->client_id,
+            $request->amount,
+            $request->source,
+            $request->reference_id,
+            $request->description
+        );
+
+        $response = new BonusResponse($result['history'], $result['client']);
+
+        return $this->asJson([
+            'success' => true,
+            'data' => $response->toArray(),
+            'timestamp' => date('c'),
+        ]);
+    }
+}
+```
+
+### Service Layer (API3)
+
+**Location**: `/erp24/api3/modules/v1/services/`
+
+**Available Services**:
+```
+BonusService.php                // Bonus calculations
+ClientService.php               // Client operations
+ClaimService.php                // Claim management
+EmployeeService.php             // Employee operations
+IncomeService.php               // Income tracking
+KikService.php                  // Feedback processing
+NotifiableService.php           // Notification delivery
+ReportService.php               // Report generation
+StoreService.php                // Store operations
+TimetableService.php            // Scheduling logic
+```
+
+**Service Trait**: Shared functionality across services
+
+**Location**: `/erp24/api3/core/ServiceTrait.php`
+
+```php
+trait ServiceTrait
+{
+    protected function beginTransaction()
+    {
+        return \Yii::$app->db->beginTransaction();
+    }
+
+    protected function logError($message, $exception)
+    {
+        \Yii::error([
+            'message' => $message,
+            'exception' => $exception->getMessage(),
+            'trace' => $exception->getTraceAsString(),
+        ], 'api3');
+    }
+
+    protected function sendNotification($userId, $message)
+    {
+        // Queue notification job
+    }
+}
+```
+
+### Custom Validators
+
+**Location**: `/erp24/api3/core/validators/`
+
+**PhoneValidator** - Russian phone number validation:
+```php
+namespace app\core\validators;
+
+use yii\validators\Validator;
+
+class PhoneValidator extends Validator
+{
+    public function validateAttribute($model, $attribute)
+    {
+        $value = $model->$attribute;
+
+        // Russian phone format: 7XXXXXXXXXX
+        if (!preg_match('/^7\d{10}$/', $value)) {
+            $this->addError($model, $attribute, 'Invalid phone number format. Expected: 7XXXXXXXXXX');
+        }
+    }
+}
+```
+
+**SexValidator** - Gender validation:
+```php
+namespace app\core\validators;
+
+use yii\validators\Validator;
+
+class SexValidator extends Validator
+{
+    public function validateAttribute($model, $attribute)
+    {
+        $value = $model->$attribute;
+
+        if (!in_array($value, ['male', 'female', 'other'], true)) {
+            $this->addError($model, $attribute, 'Invalid sex value. Allowed: male, female, other');
+        }
+    }
+}
+```
+
+---
+
+## Request/Response Flow
+
+### Complete API Flow Diagram
+
+```mermaid
+sequenceDiagram
+    participant C as Client
+    participant NG as Nginx
+    participant PHP as PHP-FPM
+    participant CTRL as Controller
+    participant REQ as Request DTO
+    participant SVC as Service
+    participant AR as ActiveRecord
+    participant DB as PostgreSQL
+    participant Q as RabbitMQ
+
+    C->>NG: HTTPS Request
+    NG->>PHP: Forward to PHP-FPM
+    PHP->>CTRL: Route to Controller
+
+    CTRL->>REQ: Create Request DTO
+    REQ->>REQ: Validate Input
+
+    alt Validation Failed
+        REQ-->>CTRL: Validation Errors
+        CTRL-->>C: 400 Bad Request
+    else Validation Passed
+        CTRL->>SVC: Call Service Method
+        SVC->>AR: Query/Modify Data
+        AR->>DB: Execute SQL
+        DB-->>AR: Return Results
+        AR-->>SVC: Return Models
+
+        opt Async Operation
+            SVC->>Q: Push Job
+            Q-->>SVC: Job Queued
+        end
+
+        SVC-->>CTRL: Return Result
+        CTRL->>RESP: Format Response
+        RESP-->>C: JSON Response
+    end
+```
+
+### Middleware Pipeline
+
+```mermaid
+graph LR
+    REQ[Request] --> AUTH[Authentication]
+    AUTH --> |Authenticated| RBAC[Authorization]
+    AUTH --> |Not Authenticated| ERR401[401 Error]
+    RBAC --> |Authorized| RATE[Rate Limiting]
+    RBAC --> |Not Authorized| ERR403[403 Error]
+    RATE --> |Under Limit| CORS[CORS Headers]
+    RATE --> |Over Limit| ERR429[429 Error]
+    CORS --> CTRL[Controller]
+    CTRL --> RESP[Response]
+
+    style AUTH fill:#fff9c4
+    style RBAC fill:#c8e6c9
+    style RATE fill:#bbdefb
+```
+
+---
+
+## Authentication & Authorization
+
+### Authentication Methods
+
+#### 1. Token-Based Authentication (API2, API3)
+
+**Flow**:
+```mermaid
+sequenceDiagram
+    C->>API: POST /auth/login (credentials)
+    API->>DB: Validate user
+    DB-->>API: User data
+    API->>API: Generate token
+    API-->>C: Return token
+
+    Note over C: Store token
+
+    C->>API: GET /resource (Bearer token)
+    API->>API: Validate token
+    API->>DB: Fetch resource
+    DB-->>API: Resource data
+    API-->>C: Return resource
+```
+
+**Implementation**:
+```php
+// In controller
+public function behaviors()
+{
+    $behaviors = parent::behaviors();
+    $behaviors['authenticator'] = [
+        'class' => HttpBearerAuth::class,
+    ];
+    return $behaviors;
+}
+```
+
+#### 2. Session-Based Authentication (API1, Web)
+
+**Flow**: Traditional cookie-based sessions
+
+---
+
+## Error Handling
+
+### Error Response Structure
+
+**API1** (Legacy):
+```json
+{
+  "error": "Invalid client ID",
+  "code": 400
+}
+```
+
+**API2** (Standard):
+```json
+{
+  "success": false,
+  "error": {
+    "code": "INVALID_CLIENT",
+    "message": "Client with ID 12345 not found",
+    "field": "client_id"
+  },
+  "timestamp": "2025-01-14T12:50:00Z"
+}
+```
+
+**API3** (Structured):
+```json
+{
+  "success": false,
+  "error": {
+    "type": "ValidationError",
+    "code": "ERR_VALIDATION_001",
+    "message": "Request validation failed",
+    "details": [
+      {
+        "field": "amount",
+        "code": "ERR_FIELD_MIN",
+        "message": "Amount must be greater than 0",
+        "value": -10
+      }
+    ],
+    "documentation": "/docs/errors/ERR_VALIDATION_001"
+  },
+  "request_id": "req_abc123xyz",
+  "timestamp": "2025-01-14T12:50:00Z"
+}
+```
+
+### Error Code Catalog
+
+See [Error Reference](../errors/error-codes.md) for complete list.
+
+---
+
+## Best Practices
+
+### API Selection Guide
+
+**Use API1 when**:
+- ❌ Don't use for new development
+- ✅ Maintaining existing cron jobs (migration planned)
+- ✅ Supporting legacy integrations (temporary)
+
+**Use API2 when**:
+- ✅ Building mobile applications
+- ✅ Creating web frontends
+- ✅ Integrating with external services
+- ✅ Rapid prototyping
+- ✅ Need stable, battle-tested endpoints
+
+**Use API3 when**:
+- ✅ Building new modern clients
+- ✅ Requiring API versioning
+- ✅ Implementing complex validation
+- ✅ Need clean request/response contracts
+- ✅ Experimenting with new features
+
+### Request Best Practices
+
+1. **Always include Accept header**:
+   ```http
+   Accept: application/json
+   ```
+
+2. **Use proper HTTP methods**:
+   - `GET` - Read data
+   - `POST` - Create resource
+   - `PUT` - Update entire resource
+   - `PATCH` - Partial update
+   - `DELETE` - Remove resource
+
+3. **Include authentication**:
+   ```http
+   Authorization: Bearer <token>
+   ```
+
+4. **Set Content-Type for body requests**:
+   ```http
+   Content-Type: application/json
+   ```
+
+5. **Handle errors gracefully**:
+   ```javascript
+   try {
+     const response = await fetch('/api2/bonus/accrue', {
+       method: 'POST',
+       headers: {
+         'Content-Type': 'application/json',
+         'Authorization': `Bearer ${token}`
+       },
+       body: JSON.stringify(data)
+     });
+
+     if (!response.ok) {
+       const error = await response.json();
+       console.error('API Error:', error.error.message);
+     }
+   } catch (error) {
+     console.error('Network Error:', error);
+   }
+   ```
+
+### Response Best Practices
+
+1. **Always include `success` flag**
+2. **Provide timestamp**
+3. **Use consistent data envelope**
+4. **Include pagination metadata**
+5. **Return appropriate HTTP status codes**
+
+---
+
+## Performance Optimization
+
+### Caching Strategy
+
+```php
+// Controller level caching
+public function behaviors()
+{
+    return [
+        [
+            'class' => 'yii\filters\HttpCache',
+            'only' => ['index', 'view'],
+            'lastModified' => function ($action, $params) {
+                return Model::find()->max('updated_at');
+            },
+        ],
+    ];
+}
+```
+
+### Rate Limiting
+
+```php
+// API2/API3 rate limiting
+public function behaviors()
+{
+    return [
+        'rateLimiter' => [
+            'class' => RateLimiter::class,
+            'enableRateLimitHeaders' => true,
+        ],
+    ];
+}
+```
+
+---
+
+## Monitoring & Debugging
+
+### Request Logging
+
+All API requests are logged with:
+- Request ID
+- Endpoint
+- Method
+- User ID
+- Response time
+- Status code
+
+**Log Location**: `/erp24/api{1,2,3}/runtime/logs/app.log`
+
+### Debug Mode
+
+**API2 Development**:
+```php
+// api2/config/dev.api2.config.php
+defined('YII_DEBUG') or define('YII_DEBUG', true);
+defined('YII_ENV') or define('YII_ENV', 'dev');
+```
+
+---
+
+## Next Steps
+
+- [API1 Detailed Reference](../api/api1/README.md)
+- [API2 Detailed Reference](../api/api2/README.md)
+- [API3 Detailed Reference](../api/api3/README.md)
+- [Error Reference](../errors/error-codes.md)
+- [Integration Guides](../guides/integrations/)
+
+---
+
+**Last Updated**: January 2025
+**Maintained By**: ERP24 Development Team
diff --git a/docs/architecture/system-overview.md b/docs/architecture/system-overview.md
new file mode 100644 (file)
index 0000000..e6fb029
--- /dev/null
@@ -0,0 +1,938 @@
+# ERP24 System Overview
+
+> **High-level architectural overview of the ERP24 enterprise system**
+
+## Table of Contents
+
+- [Introduction](#introduction)
+- [System Purpose](#system-purpose)
+- [Architectural Layers](#architectural-layers)
+- [Component Overview](#component-overview)
+- [Data Flow](#data-flow)
+- [Technology Stack](#technology-stack)
+- [Deployment Architecture](#deployment-architecture)
+- [Key Design Patterns](#key-design-patterns)
+
+---
+
+## Introduction
+
+ERP24 is a comprehensive enterprise resource planning system built on the Yii2 framework, specifically designed for managing flower retail business operations. The system handles end-to-end business processes from customer engagement to order fulfillment, employee management, and financial operations.
+
+### Business Context
+
+- **Industry**: Retail (Flower Shop Chain)
+- **Scale**: Multi-store operations across multiple cities
+- **Users**: Employees, managers, administrators, customers (via integrations)
+- **Integration**: Multi-channel sales (web, mobile, marketplaces)
+
+### System Characteristics
+
+- **Type**: Monolithic with modular architecture
+- **Scale**: ~3,771 PHP files, 389 database models
+- **API Strategy**: Three-layer API design (Legacy, REST, Advanced)
+- **Data Volume**: High-transaction environment with real-time requirements
+
+---
+
+## System Purpose
+
+### Primary Functions
+
+1. **Customer Management**
+   - Client registration and profile management
+   - Bonus/loyalty program administration
+   - Communication via Telegram and WhatsApp
+   - Purchase history tracking
+
+2. **Order & Sales Management**
+   - Order processing and tracking
+   - Product catalog management
+   - Multi-channel sales integration (Flowwow, Yandex Market)
+   - Sales analytics and reporting
+
+3. **Inventory & Logistics**
+   - Store inventory management
+   - Shipment tracking and delivery optimization
+   - Write-off management
+   - Product matrix and pricing
+
+4. **Human Resources**
+   - Employee records and profiles
+   - Payroll calculation and processing
+   - Work schedule management (timetable)
+   - Performance rating system
+   - Training and certification (lessons)
+
+5. **Financial Operations**
+   - Payment processing (CloudPayments integration)
+   - Payroll disbursement
+   - Financial reporting
+   - Bonus point accounting
+
+6. **Analytics & Business Intelligence**
+   - Dashboard system with KPIs
+   - Custom reports
+   - Performance metrics
+   - Sales analytics
+
+---
+
+## Architectural Layers
+
+### Layered Architecture Diagram
+
+```mermaid
+graph TB
+    subgraph "Presentation Layer"
+        UI[Web UI<br/>Views & Templates]
+        API1_P[API1 Endpoints]
+        API2_P[API2 REST Endpoints]
+        API3_P[API3 Advanced Endpoints]
+    end
+
+    subgraph "Application Layer"
+        CTRL[Controllers<br/>161 files]
+        ACT[Actions<br/>223 files]
+        FORMS[Forms<br/>23 files]
+    end
+
+    subgraph "Business Logic Layer"
+        SVC[Services<br/>51 files]
+        HELP[Helpers<br/>20 files]
+        VALID[Validators]
+    end
+
+    subgraph "Data Access Layer"
+        AR[ActiveRecord<br/>389 models]
+        QUERY[Query Builders]
+    end
+
+    subgraph "Data Layer"
+        DB[(PostgreSQL<br/>Database)]
+        CACHE[(Cache<br/>Redis/File)]
+        QUEUE[(RabbitMQ<br/>Message Queue)]
+    end
+
+    subgraph "External Systems"
+        AMOCRM[AmoCRM]
+        TELEGRAM[Telegram API]
+        WHATSAPP[WhatsApp API]
+        YANDEX[Yandex Market]
+        FLOWWOW[Flowwow]
+        PAYMENT[CloudPayments]
+    end
+
+    UI --> CTRL
+    API1_P --> CTRL
+    API2_P --> CTRL
+    API3_P --> CTRL
+
+    CTRL --> ACT
+    CTRL --> SVC
+    CTRL --> FORMS
+
+    ACT --> SVC
+    FORMS --> SVC
+
+    SVC --> HELP
+    SVC --> AR
+
+    AR --> DB
+    AR --> CACHE
+
+    SVC --> QUEUE
+
+    QUEUE --> TELEGRAM
+    QUEUE --> WHATSAPP
+
+    SVC --> AMOCRM
+    SVC --> YANDEX
+    SVC --> FLOWWOW
+    SVC --> PAYMENT
+
+    style DB fill:#e1f5ff
+    style CACHE fill:#e1f5ff
+    style QUEUE fill:#e1f5ff
+    style SVC fill:#fff3e0
+    style AR fill:#f3e5f5
+```
+
+### Layer Responsibilities
+
+#### 1. Presentation Layer
+**Purpose**: Handle user interactions and API requests
+
+- **Web UI**: Server-side rendered views using PHP templates
+- **API1**: Legacy API for cron jobs and old integrations
+- **API2**: Modern REST API for mobile apps and external systems
+- **API3**: Advanced API with versioning and clean architecture
+
+**Key Components**:
+- View templates (`/erp24/views/`)
+- API controllers (`/erp24/api1/`, `/erp24/api2/`, `/erp24/api3/`)
+- Asset bundles (`/erp24/assets/`)
+
+#### 2. Application Layer
+**Purpose**: Request routing and orchestration
+
+- **Controllers**: Receive HTTP requests, validate input, delegate to services
+- **Actions**: Standalone action classes for specific operations
+- **Forms**: Input validation and data transformation
+
+**Key Files**:
+- `/erp24/controllers/` - 161 controller files
+- `/erp24/actions/` - 223 action classes
+- `/erp24/forms/` - 23 form models
+
+**Responsibilities**:
+- Request validation
+- Authentication and authorization checks
+- Response formatting
+- Error handling
+
+#### 3. Business Logic Layer
+**Purpose**: Core business rules and operations
+
+- **Services**: Business logic implementation (51 services)
+- **Helpers**: Utility functions and common operations
+- **Validators**: Custom validation rules
+
+**Key Files**:
+- `/erp24/services/` - BonusService, PayrollService, ShipmentService, etc.
+- `/erp24/helpers/` - DataHelper, FormatHelper, SalaryHelper, etc.
+
+**Design Principles**:
+- Single Responsibility Principle
+- Service classes encapsulate business domains
+- Testable and reusable logic
+- Transaction management
+
+#### 4. Data Access Layer
+**Purpose**: Database abstraction and data persistence
+
+- **ActiveRecord Models**: ORM models representing database tables (389 models)
+- **Query Builders**: Complex query construction
+- **Migrations**: Database schema versioning
+
+**Key Files**:
+- `/erp24/records/` - Admin, Bonus, CheckConduct, Client, Employee, etc.
+- `/erp24/migrations/` - 278 migration files
+
+**Features**:
+- Relationship definitions (hasOne, hasMany, belongsTo)
+- Validation rules
+- Scopes and query methods
+- Soft delete support (via SoftDeleteTrait)
+- Change tracking (via HistoryModelTrait)
+
+#### 5. Data Layer
+**Purpose**: Data storage and messaging
+
+- **PostgreSQL**: Primary data store
+- **Cache**: Performance optimization (file-based or Redis)
+- **RabbitMQ**: Message queue for async operations
+
+---
+
+## Component Overview
+
+### Controllers (161 files)
+
+Controllers handle HTTP requests and coordinate application flow.
+
+**Location**: `/erp24/controllers/`
+
+**Major Controllers**:
+```php
+// Example: BonusController
+erp24/controllers/BonusController.php           // Bonus system management
+erp24/controllers/PayrollController.php         // Payroll operations
+erp24/controllers/ShipmentController.php        // Shipment tracking
+erp24/controllers/TimetableController.php       // Schedule management
+erp24/controllers/DashboardController.php       // Analytics dashboard
+erp24/controllers/RatingController.php          // Employee ratings
+erp24/controllers/NotificationController.php    // Notifications
+```
+
+**Controller Responsibilities**:
+- Request handling
+- Input validation
+- Service coordination
+- Response rendering
+- Access control
+
+### Services (51 files)
+
+Services contain business logic and orchestrate complex operations.
+
+**Location**: `/erp24/services/`
+
+**Core Services**:
+```
+BonusService                    // Loyalty program logic
+PayrollService                  // Salary calculations
+ShipmentService                 // Delivery management
+TimetableService                // Scheduling logic
+DashboardService                // Analytics aggregation
+RatingService                   // Performance evaluation
+NotificationService             // Push notifications
+MarketplaceService              // Marketplace integrations
+```
+
+**Service Patterns**:
+- Dependency injection
+- Interface contracts
+- Transaction management
+- Error handling and logging
+
+### Actions (223 files)
+
+Standalone action classes implementing single operations.
+
+**Location**: `/erp24/actions/`
+
+**Action Organization**:
+```
+actions/admin/                  // Administrative actions
+actions/bonus/                  // Bonus-related actions
+actions/cabinet/                // User cabinet actions
+actions/dashboard/              // Dashboard actions
+actions/lesson/                 // Training actions
+actions/payroll/                // Payroll actions
+actions/rating/                 // Rating actions
+actions/shipment/               // Shipment actions
+actions/timetable/              // Scheduling actions
+```
+
+**Action Pattern Benefits**:
+- Single Responsibility Principle
+- Testability
+- Reusability across controllers
+- Clear operation boundaries
+
+### ActiveRecord Models (389 files)
+
+ORM models representing database entities.
+
+**Location**: `/erp24/records/`
+
+**Model Categories**:
+```
+Core Entities:
+- Admin, AdminPayroll, AdminPayrollDays
+- Client, ClientBonus, ClientBonusHistory
+- Employee, EmployeePayment
+- Store, StoreProduct
+
+Operational:
+- CheckConduct, CheckType
+- Shipment, ShipmentProduct
+- Timetable, TimetableTemplate
+- Task, TaskTemplate
+
+Financial:
+- Payment, PaymentType
+- Balances, WriteOffs
+
+System:
+- Notification, NotificationHistory
+- Rating, Grade
+- Lesson, LessonPoll
+```
+
+### Helpers (20 files)
+
+Utility functions for common operations.
+
+**Location**: `/erp24/helpers/`
+
+**Key Helpers**:
+```
+AppArrayHelper                  // Array manipulations
+DataHelper                      // Data transformations
+DateHelper                      // Date/time operations
+FormatHelper                    // Formatting utilities
+SalaryHelper                    // Salary calculations
+HtmlHelper                      // HTML generation
+ImageHelper                     // Image processing
+EmployeePaymentHelper           // Payment utilities
+```
+
+### Forms (23 files)
+
+Form models for validation and data binding.
+
+**Location**: `/erp24/forms/`
+
+**Form Categories**:
+```
+forms/dashboard/                // Dashboard configuration forms
+forms/device/                   // Device management
+forms/lesson/                   // Training forms
+forms/payroll/                  // Payroll forms
+forms/timetable/                // Scheduling forms
+forms/writeOffsErp/             // Write-off forms
+```
+
+---
+
+## Data Flow
+
+### Request Processing Flow
+
+```mermaid
+sequenceDiagram
+    participant C as Client
+    participant R as Route
+    participant Ctrl as Controller
+    participant Act as Action/Form
+    participant Svc as Service
+    participant AR as ActiveRecord
+    participant DB as Database
+    participant Q as Queue
+
+    C->>R: HTTP Request
+    R->>Ctrl: Route to Controller
+
+    alt Using Action Pattern
+        Ctrl->>Act: Delegate to Action
+        Act->>Svc: Call Service Method
+    else Using Form Pattern
+        Ctrl->>Act: Validate with Form
+        Act->>Svc: Call Service if Valid
+    else Direct Service Call
+        Ctrl->>Svc: Call Service Method
+    end
+
+    Svc->>AR: Query/Modify Data
+    AR->>DB: Execute SQL
+    DB-->>AR: Return Results
+    AR-->>Svc: Return Models
+
+    opt Async Operation
+        Svc->>Q: Push Job to Queue
+        Q-->>Svc: Job Queued
+    end
+
+    Svc-->>Ctrl: Return Result
+    Ctrl-->>C: HTTP Response
+```
+
+### Example: Bonus Accrual Flow
+
+```mermaid
+graph LR
+    A[Client Makes Purchase] --> B[CheckConduct Created]
+    B --> C[BonusService::accrueBonus]
+    C --> D{Check Eligibility}
+    D -->|Eligible| E[Calculate Bonus Points]
+    D -->|Not Eligible| F[Skip]
+    E --> G[Create ClientBonusHistory]
+    E --> H[Update Client Balance]
+    G --> I[Log Transaction]
+    H --> I
+    I --> J{Send Notification?}
+    J -->|Yes| K[Queue Telegram Job]
+    J -->|No| L[End]
+    K --> L
+```
+
+---
+
+## Technology Stack
+
+### Backend Technologies
+
+| Component | Technology | Purpose |
+|-----------|------------|---------|
+| **Framework** | Yii2 2.0.x | PHP MVC framework |
+| **Language** | PHP 7.4+ | Server-side logic |
+| **Database** | PostgreSQL 12+ | Primary data store |
+| **ORM** | Active Record | Database abstraction |
+| **Queue** | RabbitMQ | Async job processing |
+| **Cache** | File/Redis | Performance optimization |
+| **Web Server** | Nginx | HTTP server |
+| **PHP Runtime** | PHP-FPM | PHP execution |
+| **Process Manager** | Supervisor | Service management |
+
+### Frontend Technologies
+
+| Component | Technology | Purpose |
+|-----------|------------|---------|
+| **JavaScript** | jQuery 3.6.0 | DOM manipulation |
+| **Transpiler** | Babel | ES6+ to ES5 |
+| **Bundler** | esbuild | Fast build system |
+| **Styling** | SASS | CSS preprocessing |
+| **Templates** | PHP | Server-side rendering |
+
+### Development & DevOps
+
+| Component | Technology | Purpose |
+|-----------|------------|---------|
+| **Containerization** | Docker | Environment consistency |
+| **Orchestration** | Docker Compose | Multi-container management |
+| **Version Control** | Git | Source control |
+| **Dependency Manager** | Composer | PHP packages |
+| **Package Manager** | npm | Frontend packages |
+| **Build Tool** | esbuild | Frontend builds |
+
+### External Integrations
+
+| Service | Purpose |
+|---------|---------|
+| **AmoCRM** | CRM integration |
+| **Telegram Bot API** | Customer notifications |
+| **WhatsApp API** | Customer communications |
+| **CloudPayments** | Payment processing |
+| **Yandex Market** | Marketplace integration |
+| **Flowwow** | Marketplace integration |
+
+---
+
+## Deployment Architecture
+
+### Docker Services
+
+```mermaid
+graph TB
+    subgraph "Docker Compose Environment"
+        subgraph "Web Layer"
+            NGINX_MAIN[nginx-yii_erp24<br/>Port 81/7443]
+            NGINX_API1[nginx_api1<br/>Port 4444/4443]
+            NGINX_API2[nginx_api2<br/>Port 5555/9443]
+            NGINX_API3[nginx_api3<br/>Port 8888]
+            NGINX_MEDIA[nginx_media<br/>Port 9999/8443]
+        end
+
+        subgraph "Application Layer"
+            PHP[php-yii_erp24<br/>PHP-FPM]
+            SUPERVISOR[supervisor<br/>Process Manager]
+        end
+
+        subgraph "Data Layer"
+            POSTGRES[db-pgsql-yii_erp24<br/>PostgreSQL]
+            RABBITMQ[rabbitmq-yii_erp24<br/>Port 5672/15672]
+        end
+    end
+
+    NGINX_MAIN --> PHP
+    NGINX_API1 --> PHP
+    NGINX_API2 --> PHP
+    NGINX_API3 --> PHP
+    NGINX_MEDIA --> PHP
+
+    PHP --> POSTGRES
+    PHP --> RABBITMQ
+
+    SUPERVISOR --> PHP
+
+    style POSTGRES fill:#4fc3f7
+    style RABBITMQ fill:#ff9800
+    style PHP fill:#8e44ad
+```
+
+### Port Allocation
+
+| Service | Port | Purpose |
+|---------|------|---------|
+| Main Web | 81 (HTTP), 7443 (HTTPS) | Primary web interface |
+| API1 | 4444 (HTTP), 4443 (HTTPS) | Legacy API |
+| API2 | 5555 (HTTP), 9443 (HTTPS) | Modern REST API |
+| API3 | 8888 (HTTP) | Advanced API |
+| Media Server | 9999 (HTTP), 8443 (HTTPS) | File uploads/downloads |
+| RabbitMQ | 5672 (AMQP), 15672 (UI) | Message queue |
+| PostgreSQL | 5432 (Internal) | Database |
+
+### Directory Structure
+
+```
+/Users/vladfo/development/yii-erp24/
+├── docker/                     # Docker configuration files
+├── erp24/                      # Main application
+│   ├── api1/                   # Legacy API application
+│   ├── api2/                   # REST API application
+│   ├── api3/                   # Advanced API application
+│   ├── config/                 # Main config files
+│   ├── controllers/            # Web controllers
+│   ├── services/               # Business logic services
+│   ├── records/                # ActiveRecord models
+│   ├── actions/                # Action classes
+│   ├── views/                  # View templates
+│   ├── web/                    # Web entry point
+│   ├── migrations/             # Database migrations
+│   └── ...
+├── docs/                       # Documentation (this)
+└── docker-compose.yml          # Docker orchestration
+```
+
+---
+
+## Key Design Patterns
+
+### 1. Model-View-Controller (MVC)
+
+**Implementation**: Yii2 framework pattern
+
+```php
+// Controller (erp24/controllers/BonusController.php)
+class BonusController extends Controller
+{
+    public function actionIndex()
+    {
+        $service = new BonusService();
+        $bonuses = $service->getAllBonuses();
+
+        return $this->render('index', ['bonuses' => $bonuses]);
+    }
+}
+
+// View (erp24/views/bonus/index.php)
+<?php foreach ($bonuses as $bonus): ?>
+    <div><?= $bonus->amount ?></div>
+<?php endforeach; ?>
+```
+
+### 2. Service Layer Pattern
+
+**Purpose**: Encapsulate business logic
+
+```php
+// Service (erp24/services/BonusService.php)
+class BonusService
+{
+    public function accrueBonus(Client $client, float $amount): ClientBonusHistory
+    {
+        $transaction = Yii::$app->db->beginTransaction();
+        try {
+            $bonus = new ClientBonusHistory();
+            $bonus->client_id = $client->id;
+            $bonus->amount = $amount;
+            $bonus->save();
+
+            $client->bonus_balance += $amount;
+            $client->save();
+
+            $transaction->commit();
+            return $bonus;
+        } catch (\Exception $e) {
+            $transaction->rollBack();
+            throw $e;
+        }
+    }
+}
+```
+
+### 3. Action Pattern
+
+**Purpose**: Single-responsibility action classes
+
+```php
+// Action (erp24/actions/bonus/AccrueBonusAction.php)
+class AccrueBonusAction extends Action
+{
+    public function run(int $clientId, float $amount)
+    {
+        $client = Client::findOne($clientId);
+        if (!$client) {
+            throw new NotFoundHttpException('Client not found');
+        }
+
+        $service = new BonusService();
+        $bonus = $service->accrueBonus($client, $amount);
+
+        return $this->controller->asJson(['success' => true, 'bonus' => $bonus]);
+    }
+}
+```
+
+### 4. Active Record Pattern
+
+**Purpose**: Object-relational mapping
+
+```php
+// Model (erp24/records/ClientBonusHistory.php)
+class ClientBonusHistory extends ActiveRecord
+{
+    public static function tableName()
+    {
+        return 'client_bonus_history';
+    }
+
+    public function rules()
+    {
+        return [
+            [['client_id', 'amount'], 'required'],
+            ['amount', 'number'],
+        ];
+    }
+
+    public function getClient()
+    {
+        return $this->hasOne(Client::class, ['id' => 'client_id']);
+    }
+}
+```
+
+### 5. Repository Pattern (via ActiveRecord)
+
+**Purpose**: Data access abstraction
+
+```php
+// Usage in Service
+class BonusService
+{
+    public function findByClient(int $clientId): array
+    {
+        return ClientBonusHistory::find()
+            ->where(['client_id' => $clientId])
+            ->orderBy(['created_at' => SORT_DESC])
+            ->all();
+    }
+}
+```
+
+### 6. Dependency Injection
+
+**Purpose**: Loose coupling and testability
+
+```php
+class PayrollService
+{
+    private $bonusService;
+    private $timetableService;
+
+    public function __construct(
+        BonusService $bonusService,
+        TimetableService $timetableService
+    ) {
+        $this->bonusService = $bonusService;
+        $this->timetableService = $timetableService;
+    }
+}
+```
+
+### 7. Queue/Job Pattern
+
+**Purpose**: Asynchronous task processing
+
+```php
+// Job (erp24/jobs/SendTelegramMessageJob.php)
+class SendTelegramMessageJob extends BaseObject implements JobInterface
+{
+    public $userId;
+    public $message;
+
+    public function execute($queue)
+    {
+        $telegramService = new TelegramService();
+        $telegramService->sendMessage($this->userId, $this->message);
+    }
+}
+
+// Usage in Service
+Yii::$app->queue->push(new SendTelegramMessageJob([
+    'userId' => $user->telegram_id,
+    'message' => 'Your bonus has been accrued!'
+]));
+```
+
+### 8. Trait-based Behavior Composition
+
+**Purpose**: Reusable behaviors across models
+
+```php
+// Trait (erp24/traits/SoftDeleteTrait.php)
+trait SoftDeleteTrait
+{
+    public function softDelete()
+    {
+        $this->deleted_at = time();
+        return $this->save(false);
+    }
+
+    public static function find()
+    {
+        return parent::find()->where(['deleted_at' => null]);
+    }
+}
+
+// Usage in Model
+class Client extends ActiveRecord
+{
+    use SoftDeleteTrait;
+}
+```
+
+---
+
+## System Boundaries
+
+### What ERP24 IS
+
+- ✅ Comprehensive ERP for flower retail operations
+- ✅ Multi-channel sales management system
+- ✅ Employee management and payroll system
+- ✅ Customer loyalty program platform
+- ✅ Order fulfillment and logistics system
+- ✅ Analytics and reporting platform
+
+### What ERP24 IS NOT
+
+- ❌ E-commerce storefront (integrates with external platforms)
+- ❌ Accounting system (provides data to accounting software)
+- ❌ General-purpose CRM (specialized for flower retail)
+- ❌ Public API platform (APIs for internal/partner use only)
+
+---
+
+## Performance Considerations
+
+### Optimization Strategies
+
+1. **Database Optimization**
+   - Proper indexing on frequently queried columns
+   - Query optimization using ActiveRecord scopes
+   - Connection pooling
+   - Read replicas for reporting
+
+2. **Caching**
+   - File-based caching for configuration
+   - Redis for session and frequently accessed data
+   - HTTP caching headers for static assets
+
+3. **Asynchronous Processing**
+   - RabbitMQ for heavy operations
+   - Background jobs for notifications
+   - Queue-based email/SMS sending
+
+4. **Frontend Optimization**
+   - Asset bundling with esbuild
+   - SASS compilation
+   - Lazy loading for images
+   - Minification in production
+
+---
+
+## Security Architecture
+
+### Security Layers
+
+```mermaid
+graph TB
+    A[Incoming Request] --> B{HTTPS?}
+    B -->|Yes| C{Authenticated?}
+    B -->|No| X[Reject]
+
+    C -->|Yes| D{Authorized?}
+    C -->|No| Y[Redirect to Login]
+
+    D -->|Yes| E[RBAC Check]
+    D -->|No| Z[403 Forbidden]
+
+    E --> F{Has Permission?}
+    F -->|Yes| G[Process Request]
+    F -->|No| Z
+
+    G --> H{Input Valid?}
+    H -->|Yes| I[Execute Business Logic]
+    H -->|No| W[400 Bad Request]
+
+    I --> J[Return Response]
+```
+
+### Security Features
+
+1. **Authentication**: Session-based with secure cookies
+2. **Authorization**: RBAC with hierarchical roles
+3. **Input Validation**: Server-side validation on all inputs
+4. **SQL Injection Prevention**: ActiveRecord parameter binding
+5. **XSS Protection**: Output escaping in views
+6. **CSRF Protection**: CSRF tokens on forms
+7. **Password Hashing**: Bcrypt for password storage
+8. **HTTPS**: SSL/TLS for all API communications
+
+---
+
+## Scalability Considerations
+
+### Current Architecture Scalability
+
+**Vertical Scaling** (Current Approach):
+- Single application server
+- Single database server
+- Suitable for current load
+
+**Horizontal Scaling** (Future):
+- Load balancer for multiple app servers
+- Database replication (master-slave)
+- Distributed cache (Redis cluster)
+- Microservices extraction for high-load domains
+
+### Bottleneck Analysis
+
+| Component | Current Limit | Scaling Strategy |
+|-----------|---------------|------------------|
+| Database | Single instance | Read replicas, connection pooling |
+| App Server | Single container | Load balancing, multiple instances |
+| Queue | Single RabbitMQ | Cluster mode, multiple workers |
+| File Storage | Local disk | Object storage (S3-compatible) |
+
+---
+
+## Monitoring & Observability
+
+### Logging Strategy
+
+```php
+// Application Logging
+Yii::info('Bonus accrued for client: ' . $clientId, 'bonus');
+Yii::error('Failed to process payment: ' . $e->getMessage(), 'payment');
+
+// Custom Telegram Logging Target
+Yii::getLogger()->dispatch([
+    new LogRecord('error', 'Critical error occurred', 'application')
+]);
+```
+
+### Metrics to Monitor
+
+1. **Application Metrics**
+   - Request rate
+   - Response time
+   - Error rate
+   - Queue length
+
+2. **Business Metrics**
+   - Orders per hour
+   - Bonus accruals
+   - Payroll processing time
+   - API response times
+
+3. **Infrastructure Metrics**
+   - CPU usage
+   - Memory usage
+   - Disk I/O
+   - Database connections
+
+---
+
+## Next Steps
+
+For detailed information on specific components:
+
+1. [Three-Layer API Architecture](./api-architecture.md)
+2. [Service Layer Documentation](./service-layer.md)
+3. [Database Architecture](./database-architecture.md)
+4. [Frontend Architecture](./frontend-architecture.md)
+
+For implementation guides:
+
+- [Getting Started Guide](../guides/getting-started.md)
+- [Development Environment Setup](../guides/environment-setup.md)
+- [Security & RBAC Guide](../guides/security-rbac.md)
+
+---
+
+**Last Updated**: January 2025
+**Maintained By**: ERP24 Development Team
diff --git a/docs/database/schema-overview.md b/docs/database/schema-overview.md
new file mode 100644 (file)
index 0000000..988084a
--- /dev/null
@@ -0,0 +1,873 @@
+# Database Schema Overview
+
+> **Comprehensive database documentation for ERP24 PostgreSQL schema**
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Database Statistics](#database-statistics)
+- [Entity Relationship Diagram](#entity-relationship-diagram)
+- [Core Entities](#core-entities)
+- [Business Domain Tables](#business-domain-tables)
+- [System Tables](#system-tables)
+- [Data Flow](#data-flow)
+- [Naming Conventions](#naming-conventions)
+- [Migration History](#migration-history)
+
+---
+
+## Overview
+
+ERP24 uses a **PostgreSQL** database with a comprehensive schema supporting flower retail operations including employee management, customer loyalty programs, sales tracking, inventory management, and financial operations.
+
+### Database Information
+
+| Property | Value |
+|----------|-------|
+| **DBMS** | PostgreSQL 12+ |
+| **Schema** | public, erp24 |
+| **Total Tables** | 389+ (via ActiveRecord models) |
+| **Migrations** | 278 migration files |
+| **Primary Keys** | Integer (auto-increment) and UUID (GUID) |
+| **Foreign Keys** | Soft references via application layer |
+| **Indexing** | Strategic indexes on high-traffic columns |
+
+---
+
+## Database Statistics
+
+### Model Count by Domain
+
+| Domain | Model Count | Purpose |
+|--------|-------------|---------|
+| **Admin & HR** | 80+ | Employee management, payroll, timetable |
+| **Sales & Orders** | 60+ | Sales, checks, products, marketplace orders |
+| **Customer Management** | 40+ | Users, bonus program, events |
+| **Inventory** | 35+ | Products, stores, write-offs |
+| **Task Management** | 25+ | Tasks, templates, types |
+| **Analytics** | 20+ | Dashboard, reports, metrics |
+| **Lessons & Training** | 15+ | Employee education system |
+| **System** | 30+ | Auth, logs, configurations |
+| **Marketplace** | 15+ | Flowwow, Yandex Market integration |
+| **Regulations** | 10+ | Company policies, polls |
+
+### Table Size Estimates
+
+| Table | Est. Rows | Growth Rate | Notes |
+|-------|-----------|-------------|-------|
+| **sales** | 500K+ | High (daily) | Main sales records |
+| **users** | 100K+ | Medium | Customer database |
+| **users_bonus** | 1M+ | High (daily) | Bonus transactions |
+| **timetable** | 200K+ | Medium | Employee schedules |
+| **admin_payroll_days** | 150K+ | Medium | Daily payroll records |
+| **sales_products** | 2M+ | High (daily) | Sale line items |
+| **task** | 50K+ | Medium | Task management |
+| **logs (various)** | 10M+ | Very High | System logs |
+
+---
+
+## Entity Relationship Diagram
+
+### Core Entity Relationships
+
+```mermaid
+erDiagram
+    ADMIN ||--o{ SALES : creates
+    ADMIN ||--o{ TIMETABLE : has_schedule
+    ADMIN ||--o{ ADMIN_PAYROLL : receives
+    ADMIN ||--o{ TASK : assigned_to
+    ADMIN }o--|| ADMIN_GROUP : belongs_to
+    ADMIN }o--|| CITY_STORE : works_at
+
+    USERS ||--o{ SALES : purchases
+    USERS ||--o{ USERS_BONUS : has_bonuses
+    USERS ||--o{ USERS_EVENTS : has_events
+
+    SALES ||--o{ SALES_PRODUCTS : contains
+    SALES ||--o{ USERS_BONUS : generates_bonus
+    SALES }o--|| CITY_STORE : sold_at
+    SALES }o--|| ADMIN : sold_by
+
+    CITY_STORE ||--o{ ADMIN : employs
+    CITY_STORE ||--o{ SALES : records
+    CITY_STORE ||--o{ TIMETABLE : schedules
+    CITY_STORE }o--|| CITY : located_in
+
+    ADMIN_PAYROLL ||--|| ADMIN : for_employee
+    ADMIN_PAYROLL ||--o{ ADMIN_PAYROLL_DAYS : has_daily_records
+
+    TIMETABLE }o--|| ADMIN : for_employee
+    TIMETABLE }o--|| CITY_STORE : at_store
+    TIMETABLE }o--|| SHIFT : in_shift
+
+    TASK }o--|| ADMIN : assigned_to
+    TASK }o--|| ADMIN : created_by
+    TASK }o--|| TASK_TYPE : has_type
+    TASK }o--|| TASK_STATUS : has_status
+
+    MARKETPLACE_ORDERS ||--o{ MARKETPLACE_ORDER_ITEMS : contains
+    MARKETPLACE_ORDERS }o--|| CITY_STORE : for_store
+    MARKETPLACE_ORDERS }o--|| MARKETPLACE_STATUS : has_status
+```
+
+---
+
+## Core Entities
+
+### 1. Admin (Employees)
+
+**Table**: `admin`
+**Primary Key**: `id` (integer)
+**File**: `erp24/records/Admin.php`
+
+#### Schema
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `guid` | VARCHAR(36) | 1C UUID identifier |
+| `name` | VARCHAR(55) | Short name |
+| `name_full` | VARCHAR(200) | Full name |
+| `group_id` | INTEGER | FK to admin_group |
+| `mobile` | VARCHAR(25) | Phone number (unique) |
+| `store_id` | INTEGER | Default store |
+| `store_arr` | TEXT | Array of accessible stores |
+| `login_user` | VARCHAR(29) | Login (unique) |
+| `pass_user` | VARCHAR(120) | Password hash |
+| `work_status` | INTEGER | 1=active, 4=fired |
+| `work_rate` | INTEGER | 1=5/2, 2=2/2, 3=3/3 |
+| `birthdate` | DATE | Date of birth |
+| `lasttime` | TIMESTAMP | Last login |
+| `access_token` | VARCHAR(512) | API auth token |
+
+#### Relationships
+
+```php
+// Admin.php relationships
+public function getAdminGroup() {
+    return $this->hasOne(AdminGroup::class, ['id' => 'group_id']);
+}
+
+public function getStore() {
+    return $this->hasOne(CityStore::class, ['id' => 'store_id']);
+}
+
+public function getTimetables() {
+    return $this->hasMany(Timetable::class, ['admin_id' => 'id']);
+}
+
+public function getPayrolls() {
+    return $this->hasMany(AdminPayroll::class, ['admin_id' => 'id']);
+}
+
+public function getCreatedTasks() {
+    return $this->hasMany(Task::class, ['created_by' => 'id']);
+}
+
+public function getAssignedTasks() {
+    return $this->hasMany(Task::class, ['updated_by' => 'id']);
+}
+```
+
+#### Indexes
+
+- PRIMARY KEY on `id`
+- UNIQUE on `mobile`
+- UNIQUE on `login_user`
+- UNIQUE on `guid`
+- INDEX on `group_id`
+- INDEX on `work_status`
+
+---
+
+### 2. Users (Customers)
+
+**Table**: `users`
+**Primary Key**: `id` (integer)
+**File**: `erp24/records/Users.php`
+
+#### Schema
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `phone` | VARCHAR(13) | Phone number (key) |
+| `name` | VARCHAR | Full name |
+| `card` | VARCHAR | Loyalty card number |
+| `password` | VARCHAR | Password hash |
+| `keycode` | VARCHAR | 4-digit verification code |
+| `pol` | VARCHAR | Gender (man/women) |
+| `bdate` | DATE | Birth date |
+| `email` | VARCHAR | Email address |
+| `balans` | DECIMAL | Current bonus balance (deprecated) |
+| `burn_balans` | DECIMAL | Soon-to-expire bonus |
+| `bonus_minus` | DECIMAL | Total spent bonuses |
+| `bonus_level` | VARCHAR | Tier (silver/gold/platinum) |
+| `sale_cnt` | INTEGER | Total purchase count |
+| `sale_price` | INTEGER | Lifetime value (LTV) |
+| `sale_avg_price` | INTEGER | Average check |
+| `date_last_sale` | TIMESTAMP | Last purchase date |
+| `date_first_sale` | TIMESTAMP | First purchase date |
+| `ref_code` | VARCHAR | Referral code |
+| `referral_id` | INTEGER | Referred by user ID |
+| `source` | INTEGER | 0=1C, 1=1C→TG, 2=TG |
+| `telegram_is_subscribed` | INTEGER | 0=no, 1=yes |
+| `telegram_created_at` | TIMESTAMP | TG registration date |
+| `black_list` | INTEGER | Blocked flag |
+
+#### Relationships
+
+```php
+// Users.php relationships
+public function getBonuses() {
+    return $this->hasMany(UsersBonus::class, ['phone' => 'phone']);
+}
+
+public function getEvents() {
+    return $this->hasMany(UsersEvents::class, ['phone' => 'phone']);
+}
+
+public function getSales() {
+    return $this->hasMany(Sales::class, ['phone' => 'phone']);
+}
+
+public function getReferrer() {
+    return $this->hasOne(Users::class, ['id' => 'referral_id']);
+}
+
+public function getReferrals() {
+    return $this->hasMany(Users::class, ['referral_id' => 'id']);
+}
+```
+
+#### Indexes
+
+- PRIMARY KEY on `id`
+- INDEX on `phone`
+- INDEX on `card`
+- INDEX on `bonus_level`
+- INDEX on `telegram_is_subscribed`
+
+---
+
+### 3. Sales (Check/Receipt)
+
+**Table**: `sales`
+**Primary Key**: `id` (UUID/GUID)
+**File**: `erp24/records/Sales.php`
+
+#### Schema
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | VARCHAR(36) | 1C check GUID (PK) |
+| `date` | TIMESTAMP | Check date/time |
+| `operation` | VARCHAR(35) | "Продажа" or "Возврат" |
+| `status` | VARCHAR(45) | Check status |
+| `summ` | DECIMAL | Total amount |
+| `skidka` | DECIMAL | Discount amount |
+| `number` | VARCHAR(225) | Check number |
+| `admin_id` | INTEGER | FK to admin |
+| `seller_id` | VARCHAR(36) | 1C seller GUID |
+| `store_id_1c` | VARCHAR(36) | 1C store GUID |
+| `store_id` | INTEGER | FK to city_store |
+| `phone` | BIGINT | Customer phone |
+| `payments` | JSON | Payment details |
+| `pay_arr` | VARCHAR(15) | Payment type IDs |
+| `sales_check` | VARCHAR(36) | Return check ID |
+| `order_id` | VARCHAR(36) | Online order ID |
+| `matrix` | INTEGER | Matrix bouquet % |
+| `delivery_date` | TIMESTAMP | Delivery date |
+| `pickup` | INTEGER | Pickup flag |
+
+#### Relationships
+
+```php
+// Sales.php relationships
+public function getAdmin() {
+    return $this->hasOne(Admin::class, ['id' => 'admin_id']);
+}
+
+public function getStore() {
+    return $this->hasOne(CityStore::class, ['id' => 'store_id']);
+}
+
+public function getStoreByGuid() {
+    return $this->hasOne(Products1c::class, ['id' => 'store_id_1c']);
+}
+
+public function getUsers() {
+    return $this->hasOne(Users::class, ['phone' => 'phone']);
+}
+
+public function getBonuses() {
+    return $this->hasMany(UsersBonus::class, ['check_id' => 'id']);
+}
+
+public function getProducts() {
+    return $this->hasMany(SalesProducts::class, ['check_id' => 'id']);
+}
+
+public function getSaleCheck() {  // Return reference
+    return $this->hasOne(Sales::class, ['id' => 'sales_check']);
+}
+```
+
+#### Indexes
+
+- PRIMARY KEY on `id`
+- UNIQUE on `(date, operation, store_id_1c, id)`
+- INDEX on `date`
+- INDEX on `phone`
+- INDEX on `store_id`
+- INDEX on `operation`
+
+---
+
+### 4. UsersBonus (Bonus Transactions)
+
+**Table**: `users_bonus`
+**Primary Key**: `id` (integer)
+**File**: `erp24/records/UsersBonus.php`
+
+#### Schema
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `phone` | VARCHAR(13) | Customer phone |
+| `name` | VARCHAR(155) | Transaction name |
+| `date` | TIMESTAMP | Transaction date |
+| `user_id` | INTEGER | FK to users |
+| `store_id` | INTEGER | FK to city_store |
+| `check_id` | VARCHAR(45) | FK to sales |
+| `tip` | TEXT | plus/minus/burn |
+| `tip_sale` | TEXT | Transaction type detail |
+| `price` | DECIMAL | Purchase amount |
+| `price_skidka` | DECIMAL | Discount applied |
+| `bonus` | DECIMAL | Bonus amount |
+| `date_start` | TIMESTAMP | Bonus valid from |
+| `date_end` | TIMESTAMP | Bonus expires at |
+| `admin_id` | INTEGER | Added by admin |
+| `referal_id` | INTEGER | Referral ID |
+| `store_id_1c` | VARCHAR(36) | 1C store GUID |
+| `seller_id_1c` | VARCHAR(36) | 1C seller GUID |
+
+#### Transaction Types (`tip_sale`)
+
+- `sale` - Accrued from purchase
+- `minus` - Written off for purchase
+- `burn` - Expired bonuses
+- `memorable300` - 5 memorable dates bonus
+- `p_PROMOCODE` - Promotional code
+- `referral` - Referral bonus
+- `manual` - Manual adjustment
+
+#### Relationships
+
+```php
+// UsersBonus.php relationships
+public function getUser() {
+    return $this->hasOne(Users::class, ['id' => 'user_id']);
+}
+
+public function getStore() {
+    return $this->hasOne(CityStore::class, ['id' => 'store_id']);
+}
+
+public function getSale() {
+    return $this->hasOne(Sales::class, ['id' => 'check_id']);
+}
+
+public function getAdmin() {
+    return $this->hasOne(Admin::class, ['id' => 'admin_id']);
+}
+```
+
+---
+
+### 5. CityStore (Stores)
+
+**Table**: `city_store`
+**Primary Key**: `id` (integer)
+**File**: `erp24/records/CityStore.php`
+
+#### Schema
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `f_id` | INTEGER | FloraPoint ID |
+| `name` | VARCHAR | Short store name |
+| `name_full` | VARCHAR | Full store name |
+| `city_id` | INTEGER | FK to city |
+| `adress` | TEXT | Street address |
+| `gps` | VARCHAR | GPS coordinates |
+| `email` | VARCHAR | Store email |
+| `tg_chat_id` | VARCHAR | Telegram chat ID |
+| `visible` | INTEGER | Display on site |
+| `administrator_id` | INTEGER | Store manager |
+| `sale_plan_avg` | INTEGER | Avg sales plan |
+| `visitor_day_avg` | INTEGER | Avg daily visitors |
+| `open_date` | DATE | Opening date |
+
+#### Relationships
+
+```php
+// CityStore.php relationships
+public function getCity() {
+    return $this->hasOne(City::class, ['id' => 'city_id']);
+}
+
+public function getAdministrator() {
+    return $this->hasOne(Admin::class, ['id' => 'administrator_id']);
+}
+
+public function getEmployees() {
+    return $this->hasMany(Admin::class, ['store_id' => 'id']);
+}
+
+public function getSales() {
+    return $this->hasMany(Sales::class, ['store_id' => 'id']);
+}
+
+public function getTimetable() {
+    return $this->hasMany(Timetable::class, ['store_id' => 'id']);
+}
+```
+
+---
+
+### 6. Timetable (Employee Schedule)
+
+**Table**: `timetable`
+**Primary Key**: `id` (integer)
+**File**: `erp24/records/Timetable.php`
+**Traits**: `SoftDeleteTrait`
+
+#### Schema
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `admin_id` | INTEGER | FK to admin |
+| `store_id` | INTEGER | FK to city_store |
+| `shift_id` | INTEGER | FK to shift |
+| `admin_group_id` | INTEGER | Position group |
+| `tabel` | INTEGER | 0=plan, 1=fact |
+| `date` | DATE | Shift date |
+| `time_start` | TIME | Shift start |
+| `time_end` | TIME | Shift end |
+| `datetime_start` | TIMESTAMP | Full start datetime |
+| `datetime_end` | TIMESTAMP | Full end datetime |
+| `work_time` | DECIMAL | Hours worked |
+| `salary_shift` | INTEGER | Shift salary |
+| `slot_type_id` | INTEGER | Type (work/vacation) |
+| `status` | INTEGER | 0=pending, 1=verified |
+| `active` | INTEGER | Soft delete flag |
+| `deleted_at` | TIMESTAMP | Deletion time |
+| `deleted_by` | INTEGER | Deleted by admin |
+
+#### Slot Types
+
+- `1` - TIMESLOT_WORK (regular work shift)
+- `2` - TIMESLOT_VACATION (vacation)
+- `3` - TIMESLOT_ADMINISTRATIVE (admin leave)
+- `4` - TIMESLOT_SICK_LEAVE (sick leave)
+- `5` - TIMESLOT_INTERNSHIP (training)
+- `6` - TIMESLOT_WEEKEND (weekend)
+- `7` - TIMESLOT_FREELANCE (temporary worker)
+
+#### Relationships
+
+```php
+// Timetable.php relationships
+public function getAdmin() {
+    return $this->hasOne(Admin::class, ['id' => 'admin_id']);
+}
+
+public function getStore() {
+    return $this->hasOne(CityStore::class, ['id' => 'store_id']);
+}
+
+public function getShift() {
+    return $this->hasOne(Shift::class, ['id' => 'shift_id']);
+}
+
+public function getPosition() {
+    return $this->hasOne(AdminGroup::class, ['id' => 'admin_group_id']);
+}
+
+public function getDeletedBy() {
+    return $this->hasOne(Admin::class, ['id' => 'deleted_by']);
+}
+```
+
+---
+
+### 7. AdminPayroll (Payroll Summary)
+
+**Table**: `admin_payroll`
+**Primary Key**: `id` (integer)
+**File**: `erp24/records/AdminPayroll.php`
+
+#### Schema
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `admin_id` | INTEGER | FK to admin |
+| `store_id` | INTEGER | FK to city_store |
+| `year` | INTEGER | Year |
+| `month` | INTEGER | Month (1-12) |
+| `date` | VARCHAR(100) | Month description |
+| `date_time` | TIMESTAMP | Created at |
+| `delete_status` | INTEGER | Deletion flag |
+| `date_delete` | TIMESTAMP | Deleted at |
+
+#### Unique Constraint
+
+- UNIQUE on `(admin_id, year, month)` - One payroll per employee per month
+
+#### Relationships
+
+```php
+// AdminPayroll.php relationships
+public function getAdmin() {
+    return $this->hasOne(Admin::class, ['id' => 'admin_id']);
+}
+
+public function getStore() {
+    return $this->hasOne(CityStore::class, ['id' => 'store_id']);
+}
+
+public function getDays() {
+    return $this->hasMany(AdminPayrollDays::class, ['admin_payroll_id' => 'id']);
+}
+
+public function getHistory() {
+    return $this->hasMany(AdminPayrollHistory::class, ['admin_payroll_id' => 'id']);
+}
+
+public function getMonthInfo() {
+    return $this->hasOne(AdminPayrollMonthInfo::class, ['admin_payroll_id' => 'id']);
+}
+```
+
+---
+
+## Business Domain Tables
+
+### Bonus & Loyalty Program
+
+| Table | Purpose | Records |
+|-------|---------|---------|
+| `users` | Customer profiles | 100K+ |
+| `users_bonus` | Bonus transactions | 1M+ |
+| `users_bonus_levels` | Customer tier assignments | 100K+ |
+| `bonus_levels` | Tier configuration | ~10 |
+| `users_events` | Memorable dates | 200K+ |
+| `promocode` | Promotional codes | 1K+ |
+| `referral_status` | Referral tracking | 10K+ |
+
+**Data Flow**:
+```
+Sales → BonusService → UsersBonus (accrual) → Users.bonus_level update
+```
+
+---
+
+### Payroll & HR
+
+| Table | Purpose | Records |
+|-------|---------|---------|
+| `admin` | Employees | 500+ |
+| `admin_group` | Positions/roles | 50+ |
+| `admin_payroll` | Monthly payroll | 10K+ |
+| `admin_payroll_days` | Daily payroll records | 150K+ |
+| `admin_payroll_values` | Payment components | 50K+ |
+| `admin_payroll_values_dict` | Component types | 30+ |
+| `admin_rating` | Performance ratings | 20K+ |
+| `grade` | Employee grades | 10 |
+| `holiday` | Holiday calendar | 500+ |
+
+**Data Flow**:
+```
+Timetable → PayrollService → AdminPayrollDays → AdminPayroll → Payment
+```
+
+---
+
+### Sales & Inventory
+
+| Table | Purpose | Records |
+|-------|---------|---------|
+| `sales` | Sale checks | 500K+ |
+| `sales_products` | Sale line items | 2M+ |
+| `sales_history` | Sales history log | 500K+ |
+| `products_1c` | Product catalog from 1C | 50K+ |
+| `write_offs_erp` | Inventory write-offs | 30K+ |
+| `write_offs_products_erp` | Write-off line items | 100K+ |
+| `matrix_bouquet` | Matrix bouquet definitions | 1K+ |
+
+---
+
+### Task Management
+
+| Table | Purpose | Records |
+|-------|---------|---------|
+| `task` | Tasks | 50K+ |
+| `task_template` | Task templates | 200+ |
+| `task_type` | Task types | 30+ |
+| `task_status` | Task statuses | 10+ |
+| `task_logs` | Task change history | 200K+ |
+| `task_motivation` | Task rewards | 10K+ |
+| `task_viewers` | Task watchers | 50K+ |
+
+---
+
+### Marketplace Integration
+
+| Table | Purpose | Records |
+|-------|---------|---------|
+| `marketplace_orders` | Marketplace orders | 50K+ |
+| `marketplace_order_items` | Order line items | 150K+ |
+| `marketplace_order_status_history` | Status changes | 200K+ |
+| `marketplace_order_1c_statuses` | Status mappings | 50+ |
+| `marketplace_status` | Status definitions | 20+ |
+
+---
+
+### Learning & Training
+
+| Table | Purpose | Records |
+|-------|---------|---------|
+| `lesson` | Training lessons | 500+ |
+| `lesson_group` | Lesson groups | 50+ |
+| `lesson_poll` | Lesson quizzes | 200+ |
+| `lesson_poll_answers` | Quiz answers | 5K+ |
+| `regulations` | Company regulations | 100+ |
+| `regulations_poll` | Regulation quizzes | 50+ |
+| `regulations_passed` | Completion tracking | 10K+ |
+
+---
+
+## System Tables
+
+### Authentication & Authorization (RBAC)
+
+| Table | Purpose |
+|-------|---------|
+| `auth_assignment` | User role assignments |
+| `auth_item` | Roles and permissions |
+| `auth_item_child` | Role hierarchy |
+| `auth_rule` | Custom auth rules |
+
+### Logging & Monitoring
+
+| Table | Purpose | Size |
+|-------|---------|------|
+| `api_logs` | API request logs | Very Large |
+| `api_error_log` | API errors | Large |
+| `error_log` | Application errors | Large |
+| `info_log` | Information logs | Very Large |
+| `task_logs` | Task change logs | Large |
+
+### Configuration & Reference
+
+| Table | Purpose |
+|-------|---------|
+| `export_import_table` | 1C mapping (ID ↔ GUID) |
+| `universal_catalog` | Configurable catalogs |
+| `universal_catalog_item` | Catalog items |
+| `dashboard_fields` | Dashboard configurations |
+| `crm_menu` | Menu structure |
+
+---
+
+## Data Flow
+
+### Sales Bonus Accrual Flow
+
+```mermaid
+flowchart TD
+    A[1C: Create Sale] --> B[Sales Table]
+    B --> C{Customer in<br/>bonus program?}
+    C -->|No| D[End]
+    C -->|Yes| E[Calculate Bonus]
+    E --> F{Check Products}
+    F -->|Exclude non-bonus items| E
+    F -->|Calculate base| G[Apply Tier Rate]
+    G --> H[Create UsersBonus<br/>tip=plus]
+    H --> I[Update User Stats]
+    I --> J{Update Tier?}
+    J -->|Yes| K[Update bonus_level]
+    J -->|No| L[End]
+    K --> L
+```
+
+### Payroll Calculation Flow
+
+```mermaid
+flowchart TD
+    A[Month End] --> B[Gather Timetable Data]
+    B --> C[Calculate Daily Hours]
+    C --> D[Create AdminPayrollDays]
+    D --> E[Apply Payment Components]
+    E --> F[Sum AdminPayrollValues]
+    F --> G[Create AdminPayrollMonthInfo]
+    G --> H[Generate AdminPayroll]
+    H --> I[Review & Approve]
+    I --> J{Approved?}
+    J -->|Yes| K[Mark for Payment]
+    J -->|No| L[Edit Components]
+    L --> F
+    K --> M[Export to 1C]
+```
+
+---
+
+## Naming Conventions
+
+### Table Names
+
+- **snake_case**: All table names use underscore separation
+- **Singular**: Table names are singular (`admin`, not `admins`)
+- **Descriptive**: Clear, descriptive names
+
+### Column Names
+
+- **snake_case**: Column names use underscores
+- **Suffixes**:
+  - `_id`: Integer foreign key (e.g., `admin_id`)
+  - `_id_1c`: UUID from 1C (e.g., `store_id_1c`)
+  - `_guid`: UUID identifier
+  - `_at`: Timestamp (e.g., `created_at`, `deleted_at`)
+  - `_arr`: Serialized array/JSON
+  - `_date`: Date field
+  - `_time`: Time field
+  - `_datetime`: Timestamp field
+
+### Relationship Patterns
+
+```php
+// hasOne: singular method name
+public function getAdmin()
+
+// hasMany: plural method name
+public function getBonuses()
+
+// Through another table: descriptive name
+public function getStoreByGuid()
+```
+
+---
+
+## Migration History
+
+### Migration File Pattern
+
+```
+m{YYMMDD}_{HHMMSS}_{description}.php
+```
+
+**Examples**:
+- `m230220_095139_function_regulations.php`
+- `m230306_064243_create_table_write_offs_erp.php`
+- `m230301_122735_add_access_token_column_to_admin_table.php`
+
+### Recent Migrations (2023-2025)
+
+| Date | Migration | Purpose |
+|------|-----------|---------|
+| 2024-12-28 | m241228_092653 | Add target_date to sent_kogort |
+| 2024-11-15 | Various | Marketplace enhancements |
+| 2024-08-20 | Various | Bonus system improvements |
+| 2023-03-06 | m230306_064243 | Create write_offs_erp |
+| 2023-03-01 | m230301_122735 | Add access_token to admin |
+
+### Migration Commands
+
+```bash
+# Apply migrations
+php erp24/yii migrate
+
+# Create new migration
+php erp24/yii migrate/create migration_name
+
+# Rollback last migration
+php erp24/yii migrate/down
+
+# View migration history
+php erp24/yii migrate/history
+```
+
+---
+
+## Best Practices
+
+### 1. Always Use ActiveRecord Relationships
+
+```php
+// Good: Use relationship
+$sales = $user->getSales()->where(['operation' => Sales::OPERATION_SALE])->all();
+
+// Bad: Manual join
+$sales = Sales::find()->where(['phone' => $user->phone])->all();
+```
+
+### 2. Use Transactions for Multi-Table Operations
+
+```php
+$transaction = Yii::$app->db->beginTransaction();
+try {
+    $sale->save();
+    $bonus->save();
+    $user->save();
+    $transaction->commit();
+} catch (\Exception $e) {
+    $transaction->rollBack();
+    throw $e;
+}
+```
+
+### 3. Index High-Traffic Columns
+
+- Foreign keys
+- Date/timestamp columns used in WHERE clauses
+- Phone numbers, email addresses
+- Status/type columns used for filtering
+
+### 4. Use Soft Deletes Where Appropriate
+
+```php
+// Models with SoftDeleteTrait
+$timetable->softDelete(); // Sets deleted_at instead of removing row
+```
+
+---
+
+## Database Diagrams
+
+For visual ER diagrams, see:
+- [Core Entities Diagram](./diagrams/core-entities.md)
+- [Bonus System Diagram](./diagrams/bonus-system.md)
+- [Payroll System Diagram](./diagrams/payroll-system.md)
+- [Sales Flow Diagram](./diagrams/sales-flow.md)
+
+---
+
+## Related Documentation
+
+- [ActiveRecord Models Reference](./models-reference.md)
+- [Table Relationships](./relationships.md)
+- [Migration Guide](./migrations.md)
+- [Data Dictionary](./data-dictionary.md)
+
+---
+
+**Last Updated**: January 2025
+**Database Version**: PostgreSQL 12+
+**Total Models**: 389
+**Total Migrations**: 278
+**Maintained By**: ERP24 Development Team
diff --git a/docs/modules/bonus.md b/docs/modules/bonus.md
new file mode 100644 (file)
index 0000000..7048819
--- /dev/null
@@ -0,0 +1,1805 @@
+# Bonus & Loyalty System
+
+> **Complete documentation of the customer bonus and loyalty program in ERP24**
+
+## Table of Contents
+
+- [Overview](#overview)
+- [System Architecture](#system-architecture)
+- [Bonus Tiers & Levels](#bonus-tiers--levels)
+- [Bonus Accrual Rules](#bonus-accrual-rules)
+- [Bonus Write-off Rules](#bonus-write-off-rules)
+- [Bonus Expiration & Burn](#bonus-expiration--burn)
+- [Database Schema](#database-schema)
+- [Service Layer](#service-layer)
+- [API Endpoints](#api-endpoints)
+- [Console Commands](#console-commands)
+- [Business Logic](#business-logic)
+- [Integration Points](#integration-points)
+
+---
+
+## Overview
+
+The Bonus System is a comprehensive customer loyalty program that manages bonus point accrual, write-off, expiration, and tier progression. It incentivizes repeat purchases, referrals, and customer engagement through a multi-tier cashback structure.
+
+### Key Features
+
+- **Multi-tier loyalty program** (Silver, Gold, Platinum)
+- **Automatic bonus accrual** from purchases (10-20% cashback)
+- **Bonus write-off** during checkout (up to 50% of purchase)
+- **Time-limited bonuses** with automatic expiration
+- **Referral bonus system**
+- **Promotional campaigns** (memorable dates, contests, special events)
+- **Integration with 1C** for sales synchronization
+- **Telegram bot integration** for notifications
+
+### Statistics
+
+- **2 Main Database Tables**: `users`, `users_bonus`
+- **1 Configuration Table**: `bonus_levels`
+- **3 API Layers**: API1 (cron), API2 (primary), API3 (advanced)
+- **1 Service Class**: `BonusService`
+- **7+ Console Commands**: Automated bonus management
+- **20+ API2 Endpoints**: Client bonus operations
+
+---
+
+## System Architecture
+
+```mermaid
+graph TB
+    subgraph "Client Layer"
+        MOBILE[Mobile App]
+        WEB[Web App]
+        TG[Telegram Bot]
+        POS[1C POS System]
+    end
+
+    subgraph "API Layer"
+        API2_CLIENT[API2 ClientController]
+        API2_BONUS[API2 BonusController]
+        API3_BONUS[API3 BonusController]
+    end
+
+    subgraph "Business Logic"
+        CLIENT_HELPER[ClientHelper]
+        BONUS_SERVICE[BonusService]
+        PAYMENT_HELPER[Bonus Payment Helper]
+    end
+
+    subgraph "Data Layer"
+        USERS[Users Model]
+        USERS_BONUS[UsersBonus Model]
+        BONUS_LEVELS[BonusLevels Model]
+        SALES[Sales Model]
+    end
+
+    subgraph "Background Jobs"
+        CRON_ADD[BonusController::add]
+        CRON_DELL[BonusController::dell]
+        CRON_SYNC[BonusController::addUserAndBonus]
+        CRON_BALANCE[BonusController::updateUserBonusBalance]
+    end
+
+    MOBILE --> API2_CLIENT
+    WEB --> API2_CLIENT
+    TG --> API2_CLIENT
+    POS --> API3_BONUS
+
+    API2_CLIENT --> CLIENT_HELPER
+    API2_BONUS --> BONUS_SERVICE
+    API3_BONUS --> BONUS_SERVICE
+
+    CLIENT_HELPER --> USERS
+    CLIENT_HELPER --> USERS_BONUS
+    BONUS_SERVICE --> USERS_BONUS
+    PAYMENT_HELPER --> USERS_BONUS
+
+    SALES --> CRON_SYNC
+    CRON_SYNC --> USERS
+    CRON_SYNC --> USERS_BONUS
+
+    CRON_ADD --> USERS_BONUS
+    CRON_DELL --> USERS_BONUS
+    CRON_BALANCE --> USERS
+```
+
+---
+
+## Bonus Tiers & Levels
+
+The system implements a three-tier loyalty program based on customer lifetime value.
+
+### Tier Configuration
+
+**Table: `bonus_levels`**
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `name` | VARCHAR(255) | Tier name (e.g., "Серебряный") |
+| `alias` | VARCHAR(255) | Tier alias (silver/gold/platinum) |
+| `threshold` | INTEGER | Minimum lifetime purchases (rubles) |
+| `cashback_rate` | INTEGER | Bonus accrual percentage (%) |
+| `referal_rate` | INTEGER | Referral bonus percentage (%) |
+| `bonus_rate` | INTEGER | Maximum bonus write-off (%) |
+| `active` | INTEGER | Active status (1=active, 0=inactive) |
+| `date_start` | TIMESTAMP | Effective start date |
+| `date_end` | TIMESTAMP | Effective end date |
+
+### Default Tier Structure
+
+```php
+// Based on Users.bonus_level field
+const TIER_SILVER = 'silver';     // Default tier
+const TIER_GOLD = 'gold';         // Mid tier
+const TIER_PLATINUM = 'platinum'; // Premium tier
+
+// Typical configuration (as of 2025):
+// Silver:   threshold: 0      cashback: 10%  write-off: 50%
+// Gold:     threshold: 25000  cashback: 15%  write-off: 50%
+// Platinum: threshold: 50000  cashback: 20%  write-off: 50%
+```
+
+### Tier Progression
+
+Customers automatically upgrade tiers based on their `sale_price` (lifetime value) in the `users` table:
+
+```php
+// Pseudo-logic for tier assignment
+if ($user->sale_price >= 50000) {
+    $user->bonus_level = 'platinum';
+} elseif ($user->sale_price >= 25000) {
+    $user->bonus_level = 'gold';
+} else {
+    $user->bonus_level = 'silver';
+}
+```
+
+---
+
+## Bonus Accrual Rules
+
+### Standard Purchase Bonus
+
+**Rule**: Customers receive bonus points from every purchase based on their tier.
+
+**Calculation**:
+```php
+$bonusAmount = $checkSum * ($tier->cashback_rate / 100);
+```
+
+**Exclusions**:
+1. Products from `unused_nomenclature` catalog (excluded from accrual)
+2. Products from `non_bonusable_goods` (excluded from write-off calculations)
+
+**Implementation**: `BonusController::addUserAndBonus()` (console command)
+
+```php
+// From erp24/commands/BonusController.php:492-496
+$percentBonus = 10; // Base rate
+$daysActiveBonus = 366; // 1 year validity
+self::addUserBonus($row, $percentBonus, $admin_id, $store_id, $daysActiveBonus);
+```
+
+**Bonus Record**:
+```php
+$userBonus = new UsersBonus;
+$userBonus->tip = 'plus';          // Accrual type
+$userBonus->tip_sale = 'sale';     // From purchase
+$userBonus->date = $row["date"];   // Purchase date
+$userBonus->date_start = date('Y-m-d H:i:s', strtotime('+1 day', strtotime($userBonus->date)));
+$userBonus->date_end = date('Y-m-d H:i:s', strtotime('+366 day', strtotime($userBonus->date)));
+$userBonus->phone = strval($row["phone"]);
+$userBonus->check_id = $row["id"]; // Link to sales check
+$userBonus->bonus = $back;         // Calculated amount
+```
+
+### First Purchase Bonus
+
+**Rule**: New customers receive additional 20% bonus on first purchase (valid for 90 days).
+
+```php
+// From erp24/commands/BonusController.php:498-503
+if ($addFirstSaleBonus) {
+    $percentFirstBonus = 20;
+    $daysActiveFirstBonus = 90;
+    self::addUserBonus($row, $percentFirstBonus, $admin_id, $store_id, $daysActiveFirstBonus);
+}
+```
+
+### Telegram Subscriber Bonus
+
+**Rule**: New Telegram subscribers receive 30% bonus on first purchase.
+
+**API Endpoint**: `POST /client/get-info` calculates this special rate.
+
+```php
+// From API2 ClientController logic
+if ($user->telegram_is_subscribed && $isNewSubscriber) {
+    $bonusRate = 30; // Special rate
+}
+```
+
+### Referral Bonus
+
+**Rule**: Referrer receives bonus when referred customer makes purchases.
+
+**Fields**:
+- `users.referral_id` - Phone of referrer
+- `users_bonus.tip_sale = 'referral'` - Referral bonus type
+- `users_bonus.referal_id` - Referrer user ID
+
+### Memorable Dates Bonus
+
+**Rule**: Customers with 5+ memorable dates receive 300 bonus points.
+
+**Trigger**: Special memorable dates bonus campaign.
+
+```php
+// From users_bonus.tip_sale
+'memorable300' // 300 points for 5 memorable dates
+```
+
+**Console Command**: `BonusController::actionAdd()`
+
+Automatically accrues 200 bonus points 2 days before memorable dates:
+
+```php
+const DAYS_BEFORE = 2;
+const DAYS_AFTER = 1;
+
+// From erp24/commands/BonusController.php:88-92
+$tip = "plus";
+$bonus = 200;
+$tip_sale = "date";
+$date_end = date("Y-m-d 23:59:59",
+    strtotime($event->date_year . "-" . $event->date_month . "-" . $event->date_day)
+    + 86400 * self::DAYS_AFTER);
+```
+
+### Promotional Bonuses
+
+**Types** (identified by `tip_sale` field):
+
+| Type | Description | Typical Amount | Validity |
+|------|-------------|----------------|----------|
+| `p_PROMOCODE` | Promotional code bonus | Varies | Campaign-specific |
+| `podarok` | Gift bonus | Varies | 15 years (permanent) |
+| `14feb` | Valentine's Day | 300 | 2 days |
+| `contest202310` | iPhone contest | 100 | 366 days |
+
+**Example - Valentine's Day**:
+```php
+// From BonusController::actionPlus300On14Feb()
+$userBonus->tip_sale = '14feb';
+$userBonus->bonus = 300;
+$userBonus->date_end = date('Y-m-d H:i:s', strtotime('+2 day', strtotime($userBonus->date)));
+```
+
+---
+
+## Bonus Write-off Rules
+
+### Maximum Write-off Percentage
+
+**Rule**: Customers can write off up to 50% of purchase amount using bonuses.
+
+```php
+// From API2 BonusController::actionGetBonuses()
+$maxWriteOffPercent = 50; // Configured per tier in bonus_levels
+$maxWriteOffAmount = $checkSum * ($maxWriteOffPercent / 100);
+```
+
+### Product Exclusions
+
+**Non-bonusable goods**: Products in the `non_bonusable_goods` catalog cannot have bonuses applied.
+
+```php
+// From API2 BonusController logic
+$excludedProducts = Products1c::find()
+    ->where(['catalog' => 'non_bonusable_goods'])
+    ->all();
+```
+
+### Write-off Transaction
+
+```php
+$userBonus = new UsersBonus;
+$userBonus->tip = 'minus';         // Write-off type
+$userBonus->tip_sale = 'sale';     // Used in purchase
+$userBonus->bonus = $writeOffAmount;
+$userBonus->check_id = $saleId;    // Link to sales check
+$userBonus->date = date('Y-m-d H:i:s');
+```
+
+### Balance Calculation
+
+**Formula**:
+```php
+$balance = SUM(bonus WHERE tip='plus' AND date_start <= NOW())
+         - SUM(bonus WHERE tip='minus');
+```
+
+**Implementation**: Calculated dynamically or cached in `users.balans` field.
+
+```php
+// From BonusController::updateBonusBalance()
+$plus = UsersBonus::find()
+    ->select(['SUM(bonus) as plus'])
+    ->where(['phone' => $phone, 'tip' => 'plus'])
+    ->andWhere(['<=', 'date_start', date('Y-m-d')])
+    ->scalar();
+
+$minus = UsersBonus::find()
+    ->select(['SUM(bonus) as minus'])
+    ->where(['phone' => $phone, 'tip' => 'minus'])
+    ->scalar();
+
+$balance = $plus - $minus;
+```
+
+---
+
+## Bonus Expiration & Burn
+
+### Expiration Logic
+
+**Rule**: Bonuses expire based on their `date_end` timestamp. Expired bonuses are automatically "burned" (removed from balance).
+
+### Burn Types
+
+| `tip` | `tip_sale` | Description |
+|-------|-----------|-------------|
+| `minus` | `date` | Memorable date bonus expired |
+| `minus` | [promo code] | Promotional bonus expired |
+| `burn` | Various | General expiration |
+
+### Automatic Burn - Memorable Dates
+
+**Console Command**: `BonusController::actionDell()`
+
+```php
+// Finds bonuses that expired today
+$userBonus1 = UsersBonus::find()
+    ->where(['tip' => 'plus', 'tip_sale' => 'date'])
+    ->andWhere(['<=', 'date_end', date('Y-m-d H:i:s')])
+    ->all();
+
+foreach ($userBonus1 as $userBonus) {
+    // Check if bonus was used in purchases
+    $sale = UsersBonus::find()
+        ->where(['tip' => 'minus', 'tip_sale' => 'sale', 'phone' => $userBonus->phone])
+        ->andWhere(['>=', 'date', $userBonus->date_start])
+        ->andWhere(['<=', 'date', $userBonus->date_end])
+        ->sum('bonus');
+
+    // Burn unused portion
+    $burnAmount = max(0, $userBonus->bonus - $sale);
+
+    $userBonus2 = new UsersBonus;
+    $userBonus2->tip = 'minus';
+    $userBonus2->tip_sale = 'date';
+    $userBonus2->bonus = $burnAmount;
+    $userBonus2->name = "Автоматическое сгорание бонусов на дату " . $userBonus->date_start;
+    $userBonus2->save();
+}
+```
+
+### Automatic Burn - Promotional Bonuses
+
+**Console Command**: `BonusController::actionDellPromo()`
+
+Same logic as memorable dates, but applies to all promotional bonuses (excluding `date` and `off` types):
+
+```php
+$userBonus1 = UsersBonus::find()
+    ->where(['tip' => 'plus'])
+    ->andWhere(['not in', 'tip_sale', ['date', 'off']])
+    ->andWhere(['<=', 'date_end', date('Y-m-d 00:00:00')])
+    ->all();
+```
+
+### Validity Periods
+
+| Bonus Type | Validity | Implementation |
+|------------|----------|----------------|
+| Standard purchase (10%) | 366 days | `date_end = +366 days` |
+| First purchase (20%) | 90 days | `date_end = +90 days` |
+| Memorable dates | 1-3 days | `date_end = event_date + DAYS_AFTER` |
+| Promotional | Campaign-specific | Varies (2-366 days) |
+| Gift bonuses | Permanent | `date_end = +15 years` |
+
+---
+
+## Database Schema
+
+### Table: `users` (Customers)
+
+**File**: `erp24/records/Users.php`
+
+**Key Bonus-Related Fields**:
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `phone` | VARCHAR(13) | Primary identifier (79001234567) |
+| `card` | VARCHAR | Loyalty card number |
+| `keycode` | VARCHAR(4) | 4-digit verification code |
+| `bonus_level` | VARCHAR | Tier: silver/gold/platinum |
+| `balans` | DECIMAL | Cached bonus balance |
+| `sale_cnt` | INTEGER | Total purchase count |
+| `sale_price` | INTEGER | Lifetime value (for tier) |
+| `referral_id` | INTEGER | Referrer's phone |
+| `telegram_is_subscribed` | INTEGER | Telegram subscription (0/1) |
+| `source` | INTEGER | 0=1C, 1=1C→TG, 2=TG |
+
+**Relationships**:
+```php
+public function getBonuses() {
+    return $this->hasMany(UsersBonus::class, ['phone' => 'phone']);
+}
+
+public function getSales() {
+    return $this->hasMany(Sales::class, ['phone' => 'phone']);
+}
+```
+
+### Table: `users_bonus` (Bonus Transactions)
+
+**File**: `erp24/records/UsersBonus.php`
+
+**Schema**:
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `phone` | VARCHAR(13) | Customer phone |
+| `tip` | TEXT | Transaction type: plus/minus/burn |
+| `tip_sale` | TEXT | Bonus source (see types below) |
+| `bonus` | DECIMAL | Bonus amount (positive) |
+| `date` | TIMESTAMP | Transaction timestamp |
+| `date_start` | TIMESTAMP | Bonus becomes active |
+| `date_end` | TIMESTAMP | Bonus expires |
+| `check_id` | VARCHAR(45) | FK to sales.id (1C GUID) |
+| `name` | TEXT | Transaction description |
+| `store_id` | INTEGER | Store ID (internal) |
+| `store_id_1c` | VARCHAR | Store GUID (1C) |
+| `seller_id_1c` | VARCHAR | Seller GUID (1C) |
+| `admin_id` | INTEGER | FK to admin.id |
+| `price` | DECIMAL | Original check amount |
+| `price_skidka` | DECIMAL | Discount amount |
+| `referal_id` | INTEGER | Referrer user ID |
+| `dell` | INTEGER | Burn flag (1=burned) |
+| `date_dell` | TIMESTAMP | Burn timestamp |
+
+**Transaction Types** (`tip` field):
+
+| Value | Description |
+|-------|-------------|
+| `plus` | Bonus accrual |
+| `minus` | Bonus write-off or burn |
+| `burn` | Explicit burn/expiration |
+
+**Bonus Sources** (`tip_sale` field):
+
+| Value | Description |
+|-------|-------------|
+| `sale` | From purchase or used in purchase |
+| `date` | Memorable date bonus |
+| `p_PROMOCODE` | Promotional code (e.g., `p_NEWYEAR`) |
+| `referral` | Referral bonus |
+| `memorable300` | 5 memorable dates bonus |
+| `podarok` | Gift bonus |
+| `14feb` | Valentine's Day |
+| `contest202310` | Contest campaign |
+| `off` | Special exclusion type |
+
+**Relationships**:
+```php
+public function getSale() {
+    return $this->hasOne(Sales::class, ['id' => 'check_id']);
+}
+
+public function getUser() {
+    return $this->hasOne(Users::class, ['phone' => 'phone']);
+}
+```
+
+### Table: `bonus_levels` (Tier Configuration)
+
+**File**: `erp24/records/BonusLevels.php`
+
+See [Bonus Tiers & Levels](#bonus-tiers--levels) section for full schema.
+
+---
+
+## Service Layer
+
+### BonusService
+
+**File**: `erp24/services/BonusService.php`
+
+**Purpose**: Employee payroll bonus calculations (NOT customer loyalty bonuses).
+
+This service calculates performance-based bonuses for employees based on:
+- Sales metrics (average check, conversion, matrix percentage)
+- Quality metrics (80-100% quality ratings)
+- Write-off percentages
+- Team bonuses
+- Store performance
+
+**Note**: Despite the name, `BonusService` is primarily used for **employee payroll bonuses**, not customer loyalty bonuses. Customer bonus logic is implemented in:
+- `ClientHelper` (erp24/helpers/ClientHelper.php)
+- API2 `ClientController` and `BonusController`
+- Console `BonusController`
+
+### Key Methods (Employee Payroll Context)
+
+#### 1. Matrix Bonus Calculation
+```php
+public function getMatrixBonusCoefficient($rowDate): float
+{
+    $oldMatrixBonusCoefficient = 0.025;
+    $newMatrixBonusCoefficient = 2/115;
+    $newTwoMatrixBonusCoefficient = 0.02;
+
+    // Historical date-based coefficient
+    if ($rowDate < '2022-11-16 00:00:00') {
+        return 0.025;
+    } elseif ($rowDate < '2022-12-01 00:00:00') {
+        return 2/115;
+    } else {
+        return 0.02;
+    }
+}
+```
+
+#### 2. Quality Bonus
+```php
+public function getBonusForQuality(float $percent): int
+{
+    $levels = [
+        "80" => 3000,
+        "90" => 4000,
+        "100" => 5000,
+    ];
+
+    return $this->getValueByLavelsEqualAndMore($percent, $levels);
+}
+```
+
+#### 3. Conversion Bonus
+```php
+public function getGameBonusConversionStore(
+    float $conversionPercent,
+    bool $isAdministrator,
+    $storeId = null,
+    $date = null
+): int {
+    $gameBonus = 0;
+    $gameBonusSuccess = $isAdministrator ? 5 : 3;
+    $comparePercent = 80;
+
+    // Special case for store 4 (Aerodrmnaya 28)
+    if ($storeId == 4 && $date >= '2023-04-01') {
+        $comparePercent = 40;
+    }
+
+    if ($conversionPercent >= $comparePercent) {
+        $gameBonus = $gameBonusSuccess;
+    }
+
+    return $gameBonus;
+}
+```
+
+#### 4. Team Bonus Calculation
+```php
+public function getTeamBonus(
+    $adminId,
+    $storeId,
+    $storeGuid,
+    $dateFrom,
+    $dateTo
+): array {
+    // Complex calculation:
+    // 1. Get store FOT (payroll fund): salaries + bonuses
+    // 2. Get store write-offs
+    // 3. Get store sales
+    // 4. Calculate: (sales * 20%) - (FOT + write-offs) = team bonus pool
+    // 5. Distribute pool by shift count
+
+    $percentTeamBonusInMonth = self::getPercentTeamBonusInMonth($storeId, $dateFrom);
+    $salesByStorePart = $salesByStore * ($percentTeamBonusInMonth / 100);
+    $primeFondStore = $salesByStorePart - $fotStoreAndWriteOff;
+
+    return [
+        'primeFondStore' => $primeFondStore,
+        'personPrimeFondStore' => $primeFondStoreOneShift * $personShiftCount,
+        // ... detailed breakdown
+    ];
+}
+```
+
+#### 5. Level-Based Bonus Helper
+```php
+public function getValueByLavelsEqualAndMore(float $value, array $levels)
+{
+    $gameBonus = 0;
+    $minLevel = empty($levels) ? -1 : min(array_keys($levels));
+
+    if ($value >= $minLevel) {
+        ksort($levels);
+        foreach ($levels as $compareValue => $sum) {
+            if ($value >= $compareValue) {
+                $gameBonus = $sum;
+            }
+        }
+    }
+
+    return $gameBonus;
+}
+```
+
+### Bonus Payment Helper
+
+**File**: `erp24/helpers/payment/stage/Bonus.php`
+
+**Purpose**: Calculate employee bonus payment for payroll processing.
+
+**Interface**: Implements `PaymentStageInterface` for payroll pipeline.
+
+```php
+public function run(PaymentEvent $event): bool
+{
+    $factsByStore = ArrayHelper::index(
+        $event->factsByEmployee[$event->employee->id],
+        null,
+        ['store_id']
+    );
+
+    $workDaysByStore = array_map('count', $factsByStore);
+    $bonusPayment = 0;
+
+    foreach ($workDaysByStore as $storeId => $workDays) {
+        $bonusByStore = $this->getBonusParams($event->employee, $storeId);
+
+        $dailyBonus = $bonusByStore['bonus']
+                    * $bonusByStore['coefficient']
+                    / $event->workDaysInMonth;
+
+        $bonusPaymentForStore = $dailyBonus * $workDays;
+        $bonusPayment += $bonusPaymentForStore;
+    }
+
+    $event->pay += $bonusPayment;
+    return true;
+}
+```
+
+**Bonus Parameters by Position**:
+
+```php
+// Senior Florist (group_id: 30, 35, 45)
+$bonusByWriteOff = [
+    ['writeOff' => 0.03, 'bonus' => 10000],
+    ['writeOff' => 0.05, 'bonus' => 8000],
+    ['writeOff' => 0.1,  'bonus' => 6000],
+    ['writeOff' => 0.2,  'bonus' => 0],
+];
+
+// Administrator (group_id: 7, 20, 50)
+$bonusByWriteOff = [
+    ['writeOff' => 0.03, 'bonus' => 15000],
+    ['writeOff' => 0.05, 'bonus' => 12000],
+    ['writeOff' => 0.1,  'bonus' => 10000],
+    ['writeOff' => 0.2,  'bonus' => 0],
+];
+
+// Coefficient by plan achievement
+$coefficientByPlanRatio = [
+    ['planRatio' => 1,    'coeff' => 1.1],  // 100%+ plan = 110%
+    ['planRatio' => 0.95, 'coeff' => 1],    // 95-100% = 100%
+    ['planRatio' => 0.90, 'coeff' => 0.7],  // 90-95% = 70%
+    ['planRatio' => 0.85, 'coeff' => 0.5],  // 85-90% = 50%
+    ['planRatio' => 0,    'coeff' => 0],    // <85% = 0%
+];
+```
+
+---
+
+## API Endpoints
+
+### API2 - ClientController
+
+See full documentation at [docs/api/api2/README.md](../api/api2/README.md#clientcontroller-client-management)
+
+**Bonus-Related Endpoints**:
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/client/balance` | POST | Get current bonus balance |
+| `/client/get-info` | POST | Full client info including bonuses |
+| `/client/bonus-write-off` | POST | Get bonus transaction history |
+| `/client/use-bonuses` | POST | Write off bonuses for order |
+| `/client/add-bonus` | POST | Manually add bonus points |
+| `/client/bonus-status` | POST | Get tier status and requirements |
+| `/client/apply-promo-code` | POST | Apply promotional code |
+
+### API2 - BonusController
+
+**File**: `erp24/api2/controllers/BonusController.php`
+
+#### POST `/bonus/get-bonuses` - Calculate Available Bonuses
+
+**Purpose**: Calculate available bonuses for a check, considering product exclusions and tier rates.
+
+**Request**:
+```json
+{
+  "phone": "79001234567",
+  "store_id": 5,
+  "products": [
+    {
+      "nomenclature_id": "guid-1",
+      "price": 1500
+    }
+  ]
+}
+```
+
+**Response**:
+```json
+{
+  "result": true,
+  "total_bonuses": 1250,
+  "available_bonuses": 250,
+  "will_be_credited_bonuses": 250,
+  "bonus_level": "gold",
+  "cashback_rate": 15,
+  "max_write_off_rate": 50
+}
+```
+
+**Business Logic**:
+1. Exclude products from `unused_nomenclature` catalog
+2. Exclude products from `non_bonusable_goods` for write-off
+3. Apply tier-based cashback rate (10/15/20%)
+4. Apply 30% special rate for new Telegram subscribers
+5. Calculate maximum write-off (50% of check)
+
+#### POST `/bonus/send-message` - Send Verification Code
+
+**Purpose**: Send SMS verification code via SMS.ru API.
+
+**Request**:
+```json
+{
+  "phone": "79001234567"
+}
+```
+
+**Response**:
+```json
+{
+  "result": true,
+  "message": "Код отправлен"
+}
+```
+
+#### POST `/bonus/save-client-info` - Save Client Data
+
+**Purpose**: Save customer information after verification.
+
+**Request**:
+```json
+{
+  "phone": "79001234567",
+  "name": "Иван Иванов",
+  "birth_date": "1990-05-15"
+}
+```
+
+**Response**:
+```json
+{
+  "result": true,
+  "user_id": 12345
+}
+```
+
+### API3 - BonusController
+
+**File**: `erp24/api3/modules/v1/controllers/BonusController.php`
+
+Advanced API with DTO pattern (Request/Response objects).
+
+**Request DTOs**:
+- `BonusAddInput` - Add bonus request
+- `BonusWriteOffInput` - Write-off bonus request
+
+---
+
+## Console Commands
+
+### BonusController
+
+**File**: `erp24/commands/BonusController.php`
+
+**Namespace**: `yii_app\commands\BonusController`
+
+All commands are executed via CLI:
+```bash
+php erp24/yii bonus/[action-name]
+```
+
+#### 1. `bonus/add-user-and-bonus` - Sync Sales and Accrual Bonuses
+
+**Purpose**: Main cron job that synchronizes sales from 1C and accrues bonuses.
+
+**Schedule**: Runs every 5 minutes (typical setup)
+
+**Process**:
+1. Fetch sales from last 5-7 days
+2. Check for returns (exclude returned sales)
+3. Create new users if needed
+4. Accrual 10% standard bonus (366 days validity)
+5. Accrual 20% first purchase bonus (90 days validity)
+6. Update user statistics
+
+**Code Flow**:
+```php
+public function addUserAndBonus()
+{
+    $dateCompare = date('Y-m-d H:i:s', strtotime('-7 day'));
+
+    // Get sales
+    $sales = Sales::find()
+        ->where(['!=', 'phone', 0])
+        ->andWhere(['>', 'date', $dateCompare])
+        ->andWhere(['operation' => Sales::OPERATION_SALE])
+        ->all();
+
+    foreach ($sales as $row) {
+        // Skip if already has return
+        if (in_array($row["id"], $returnSalesIds)) continue;
+
+        // Create user if new
+        if (!array_key_exists($row["phone"], $usersSalesPhones)) {
+            $newUser = new Users();
+            // ... set fields
+            $newUser->save();
+        }
+
+        // Check if bonus already accrued
+        $existing = UsersBonus::find()
+            ->where(['phone' => $row["phone"], 'check_id' => $row["id"]])
+            ->exists();
+
+        if (!$existing) {
+            // Accrual 10% standard
+            self::addUserBonus($row, 10, $admin_id, $store_id, 366);
+
+            // Accrual 20% first purchase bonus
+            if ($isFirstPurchase) {
+                self::addUserBonus($row, 20, $admin_id, $store_id, 90);
+            }
+        }
+    }
+}
+```
+
+#### 2. `bonus/add` - Memorable Dates Bonus
+
+**Purpose**: Accrual 200 bonus points for memorable dates (2 days before event).
+
+**Schedule**: Runs daily
+
+**Process**:
+1. Find events from `users_events` table
+2. Select dates: today + next 2 days
+3. Check if bonus already accrued
+4. Create bonus record with 1-3 day validity
+
+**Code**:
+```php
+public function actionAdd()
+{
+    const DAYS_BEFORE = 2;
+    const DAYS_AFTER = 1;
+
+    $date_day_now = (int)date("d");
+    $date_month_now = (int)date("m");
+
+    $userEvents = UsersEvents::find()
+        ->where(['date_day' => $date_day_now, 'date_month' => $date_month_now])
+        ->all();
+
+    foreach ($userEvents as $event) {
+        $date_end = date("Y-m-d 23:59:59",
+            strtotime($event->date_year . "-" . $event->date_month . "-" . $event->date_day)
+            + 86400 * self::DAYS_AFTER);
+
+        $userBonus = new UsersBonus;
+        $userBonus->phone = $event->phone;
+        $userBonus->tip = 'plus';
+        $userBonus->tip_sale = 'date';
+        $userBonus->bonus = 200;
+        $userBonus->date_start = date('Y-m-d');
+        $userBonus->date_end = $date_end;
+        $userBonus->save();
+    }
+}
+```
+
+#### 3. `bonus/dell` - Burn Expired Memorable Date Bonuses
+
+**Purpose**: Automatically burn (expire) unused memorable date bonuses.
+
+**Schedule**: Runs daily
+
+**Process**:
+1. Find bonuses where `date_end` passed
+2. Check if bonus was used (partially or fully)
+3. Create "minus" transaction for unused portion
+4. Mark original bonus as burned (`dell = 1`)
+
+**Code**:
+```php
+public function actionDell()
+{
+    $userBonuses = UsersBonus::find()
+        ->where(['tip' => 'plus', 'tip_sale' => 'date'])
+        ->andWhere(['<=', 'date_end', date('Y-m-d H:i:s')])
+        ->all();
+
+    foreach ($userBonuses as $userBonus) {
+        if ($userBonus->dell > 0) continue; // Already burned
+
+        // Check if used in purchases
+        $usedAmount = UsersBonus::find()
+            ->where(['tip' => 'minus', 'tip_sale' => 'sale'])
+            ->andWhere(['phone' => $userBonus->phone])
+            ->andWhere(['>=', 'date', $userBonus->date_start])
+            ->andWhere(['<=', 'date', $userBonus->date_end])
+            ->sum('bonus');
+
+        $burnAmount = max(0, $userBonus->bonus - $usedAmount);
+
+        if ($burnAmount > 0) {
+            $burn = new UsersBonus;
+            $burn->phone = $userBonus->phone;
+            $burn->tip = 'minus';
+            $burn->tip_sale = 'date';
+            $burn->bonus = $burnAmount;
+            $burn->name = "Автоматическое сгорание бонусов";
+            $burn->save();
+
+            $userBonus->dell = 1;
+            $userBonus->date_dell = date('Y-m-d H:i:s');
+            $userBonus->save();
+        }
+    }
+}
+```
+
+#### 4. `bonus/dell-promo` - Burn Expired Promotional Bonuses
+
+**Purpose**: Burn all promotional bonuses (excluding memorable dates).
+
+**Schedule**: Runs daily
+
+**Process**: Same as `bonus/dell`, but applies to all promotional types.
+
+```php
+public function actionDellPromo()
+{
+    $userBonuses = UsersBonus::find()
+        ->where(['tip' => 'plus'])
+        ->andWhere(['not in', 'tip_sale', ['date', 'off']])
+        ->andWhere(['<=', 'date_end', date('Y-m-d 00:00:00')])
+        ->all();
+
+    // Same burn logic as actionDell()
+}
+```
+
+#### 5. `bonus/update-user-bonus-balance` - Update Cached Balance
+
+**Purpose**: Recalculate and update `users.balans` cached field.
+
+**Schedule**: Runs daily or after major bonus operations
+
+**Code**:
+```php
+public function actionUpdateUserBonusBalance()
+{
+    $dateCompare = date('Y-m-d H:i:s', strtotime('-7 day'));
+
+    $sales = Sales::find()
+        ->where(['operation' => Sales::OPERATION_SALE])
+        ->andWhere(['>', 'date', $dateCompare])
+        ->all();
+
+    foreach ($sales as $sale) {
+        self::updateBonusBalance($sale['phone']);
+    }
+}
+
+public static function updateBonusBalance($phone): void
+{
+    $user = Users::findOne(['phone' => $phone]);
+
+    $plus = UsersBonus::find()
+        ->where(['phone' => $phone, 'tip' => 'plus'])
+        ->andWhere(['<=', 'date_start', date('Y-m-d')])
+        ->sum('bonus');
+
+    $minus = UsersBonus::find()
+        ->where(['phone' => $phone, 'tip' => 'minus'])
+        ->sum('bonus');
+
+    $balance = $plus - $minus;
+
+    if ($balance != $user->balans) {
+        $user->balans = $balance;
+        $user->save();
+    }
+}
+```
+
+#### 6. `bonus/balance-correction` - Fix Negative Balances
+
+**Purpose**: One-time correction for users with negative balance.
+
+**Usage**: Manual execution when data inconsistencies found.
+
+```php
+public function actionBalanceCorrection()
+{
+    // Calculate current balance
+    $resultMap = [];
+    foreach ($plusQuery as $plus) {
+        $resultMap[$plus->phone] = $plus->sum;
+    }
+    foreach ($minusQuery as $minus) {
+        $resultMap[$minus->phone] = ($resultMap[$minus->phone] ?? 0) - $minus->sum;
+    }
+
+    // Add correction bonus for negative balances
+    foreach ($resultMap as $phone => $sum) {
+        if ($sum < 0) {
+            $correction = new UsersBonus;
+            $correction->tip = 'plus';
+            $correction->tip_sale = 'podarok';
+            $correction->bonus = -$sum;
+            $correction->name = "Корректировка баланса. Выход из минуса.";
+            $correction->date_end = date('Y-m-d H:i:s', strtotime('+15 years'));
+            $correction->save();
+        }
+    }
+}
+```
+
+#### 7. `bonus/bonus-remove-from-return-sales` - Remove Bonuses from Returns
+
+**Purpose**: Delete bonus records when sales are returned.
+
+**Schedule**: Runs with `add-user-and-bonus` (every 5 minutes)
+
+```php
+public function actionBonusRemoveFromReturnSales()
+{
+    $dateCompare = date('Y-m-d H:i:s', strtotime('-7 day'));
+
+    $salesReturn = Sales::find()
+        ->where(['operation' => Sales::OPERATION_RETURN])
+        ->andWhere(['>', 'date', $dateCompare])
+        ->all();
+
+    foreach ($salesReturn as $row) {
+        UsersBonus::deleteAll([
+            'check_id' => $row["id"],
+            'phone' => $row["phone"]
+        ]);
+    }
+}
+```
+
+#### 8. Special Campaigns
+
+**`bonus/plus-100-contest`** - iPhone Contest Bonus
+```php
+public function actionPlus100Contest()
+{
+    // Read phones from file
+    $arr = preg_split("/\r\n|\n|\r/", file_get_contents('plus100rubles.txt'));
+
+    foreach ($arr as $phone) {
+        $userBonus = new UsersBonus;
+        $userBonus->phone = $phone;
+        $userBonus->name = "100 бонусов за участие в розыгрыше iPhone";
+        $userBonus->tip = 'plus';
+        $userBonus->tip_sale = 'contest202310';
+        $userBonus->bonus = 100;
+        $userBonus->date_end = date('Y-m-d H:i:s', strtotime('+366 day'));
+        $userBonus->save();
+    }
+}
+```
+
+**`bonus/plus-300-on-14-feb`** - Valentine's Day Bonus
+```php
+public function actionPlus300On14Feb()
+{
+    foreach (Users::find()->where([
+        'pol' => 'man',
+        'telegram_is_subscribed' => 1
+    ])->all() as $user) {
+        $userBonus = new UsersBonus;
+        $userBonus->phone = $user->phone;
+        $userBonus->name = "Ко Дню Влюблённых";
+        $userBonus->tip = 'plus';
+        $userBonus->tip_sale = '14feb';
+        $userBonus->bonus = 300;
+        $userBonus->date_end = date('Y-m-d H:i:s', strtotime('+2 day'));
+        $userBonus->save();
+    }
+}
+```
+
+---
+
+## Business Logic
+
+### Complete Bonus Lifecycle
+
+```mermaid
+sequenceDiagram
+    participant Customer
+    participant POS as 1C POS
+    participant Cron as Bonus Cron
+    participant DB as Database
+    participant TG as Telegram Bot
+
+    Customer->>POS: Makes purchase (1500₽)
+    POS->>DB: Create Sales record
+
+    Note over Cron: Runs every 5 min
+    Cron->>DB: Fetch new sales
+    Cron->>DB: Check user exists
+    alt New Customer
+        Cron->>DB: Create Users record
+        Cron->>DB: Accrual 10% (150₽, 366 days)
+        Cron->>DB: Accrual 20% (300₽, 90 days)
+    else Existing Customer
+        Cron->>DB: Accrual 10-20% based on tier
+    end
+
+    Cron->>TG: Send notification
+    TG->>Customer: "Начислено 150₽"
+
+    Note over Cron: Next day
+    Cron->>DB: Check memorable dates
+    alt Has event in 2 days
+        Cron->>DB: Accrual 200₽ (date bonus)
+        Cron->>TG: Notify customer
+    end
+
+    Note over Customer: 90 days later
+    Customer->>POS: Makes purchase (2000₽)
+    Customer->>POS: Use 300₽ bonuses
+    POS->>DB: Create Sales record
+    POS->>DB: Create UsersBonus (minus, 300₽)
+
+    Note over Cron: After 366 days
+    Cron->>DB: Find expired bonuses
+    Cron->>DB: Check if used
+    alt Partially used
+        Cron->>DB: Burn unused portion
+    else Fully unused
+        Cron->>DB: Burn all 150₽
+    end
+```
+
+### Balance Calculation Flow
+
+```mermaid
+graph TD
+    A[Client Requests Balance] --> B{API2 /client/balance}
+    B --> C[Fetch Users record]
+    C --> D{Use cached balans?}
+    D -->|Yes| E[Return users.balans]
+    D -->|No| F[Calculate from users_bonus]
+    F --> G[SUM tip=plus WHERE date_start <= NOW]
+    G --> H[SUM tip=minus]
+    H --> I[Balance = Plus - Minus]
+    I --> J[Update users.balans cache]
+    J --> K[Return balance]
+```
+
+### Bonus Accrual Decision Tree
+
+```mermaid
+graph TD
+    A[New Sale] --> B{Return sale?}
+    B -->|Yes| C[Skip, remove bonuses]
+    B -->|No| D{User exists?}
+    D -->|No| E[Create user]
+    E --> F{First purchase?}
+    D -->|Yes| G{Already accrued?}
+    G -->|Yes| H[Skip]
+    G -->|No| F
+    F -->|Yes| I[Accrual 10% + 20%]
+    F -->|No| J{Telegram new subscriber?}
+    J -->|Yes| K[Accrual 30%]
+    J -->|No| L{Get tier}
+    L --> M{Silver 10%}
+    L --> N{Gold 15%}
+    L --> O{Platinum 20%}
+    M --> P[Accrual 10% bonus]
+    N --> Q[Accrual 15% bonus]
+    O --> R[Accrual 20% bonus]
+```
+
+### Write-off Validation Flow
+
+```mermaid
+graph TD
+    A[Client Wants to Use Bonuses] --> B{Check current balance}
+    B --> C{Balance >= Requested?}
+    C -->|No| D[Error: Insufficient bonuses]
+    C -->|Yes| E{Check max write-off %}
+    E --> F{Requested <= 50% of check?}
+    F -->|No| G[Error: Max 50% write-off]
+    F -->|Yes| H{Check products}
+    H --> I{Contains non-bonusable?}
+    I -->|Yes| J[Exclude from write-off amount]
+    I -->|No| K{Calculate final amount}
+    J --> K
+    K --> L[Create UsersBonus minus record]
+    L --> M[Link to Sales check_id]
+    M --> N[Update users.balans]
+    N --> O[Success]
+```
+
+---
+
+## Integration Points
+
+### 1C Integration
+
+**Sales Synchronization**:
+- 1C POS creates `Sales` records with phone numbers
+- Bonus cron reads sales every 5 minutes
+- Bonuses accrued based on 1C check data
+- Returns handled via `sales.operation = 'Возврат'`
+
+**Data Mapping**:
+```php
+// 1C → ERP24 mapping
+$admin_id = ArrayHelper::getValue($exportAdmin, $row["seller_id"]);  // 1C seller GUID → admin.id
+$store_id = ArrayHelper::getValue($exportCityStore, $row["store_id_1c"]);  // 1C store GUID → city_store.id
+```
+
+### Telegram Bot Integration
+
+**Notifications**:
+- Bonus accrual notifications
+- Memorable date reminders
+- Tier upgrade notifications
+- Balance inquiries
+
+**Implementation**: Via API2 `/telegram/send-message` endpoint
+
+**User Subscription**:
+- `users.telegram_is_subscribed = 1`
+- Special 30% bonus for new subscribers
+- Stored in `users.telegram_id`
+
+### Mobile App Integration
+
+**API2 Endpoints Used**:
+- `/client/balance` - Show balance in app
+- `/client/get-info` - Display full bonus history
+- `/client/bonus-status` - Show tier progress
+- `/client/use-bonuses` - Apply bonuses at checkout
+
+### Web Application Integration
+
+**Controllers**:
+- `BonusController` - Admin interface for bonus management
+- `BonusLevelsController` - Tier configuration
+
+**Actions**:
+- `StatAction` - Bonus statistics
+- `VozvratStatsAction` - Return statistics
+- `AjaxShowCheckAction` - Show check details
+- `AjaxBonusRemoveAction` - Manual bonus removal
+
+### Queue System Integration
+
+While not heavily used in bonus system, potential queue jobs:
+- Mass bonus accrual campaigns
+- Telegram notification delivery
+- Balance recalculation for large user sets
+
+---
+
+## Example Scenarios
+
+### Scenario 1: New Customer First Purchase
+
+**Context**: Anna makes her first purchase of 2000₽ at store #5.
+
+**Flow**:
+1. POS creates `Sales` record:
+   ```
+   id: "1c-guid-12345"
+   phone: 79001234567
+   summ: 2000
+   operation: "Продажа"
+   store_id_1c: "store-guid-5"
+   ```
+
+2. Cron `bonus/add-user-and-bonus` runs:
+   - Checks if user exists → No
+   - Creates `Users` record:
+     ```php
+     phone: 79001234567
+     name: "Новый"
+     bonus_level: "silver"
+     sale_cnt: 1
+     sale_price: 2000
+     ```
+
+3. Accrues standard 10% bonus:
+   ```php
+   UsersBonus:
+     phone: 79001234567
+     tip: "plus"
+     tip_sale: "sale"
+     bonus: 200  // 2000 * 10%
+     date_start: tomorrow
+     date_end: +366 days
+     check_id: "1c-guid-12345"
+   ```
+
+4. Accrues first purchase 20% bonus:
+   ```php
+   UsersBonus:
+     phone: 79001234567
+     tip: "plus"
+     tip_sale: "sale"
+     bonus: 400  // 2000 * 20%
+     date_start: tomorrow
+     date_end: +90 days
+     check_id: "1c-guid-12345"
+   ```
+
+5. Updates balance:
+   ```php
+   users.balans = 600
+   ```
+
+6. Sends Telegram notification: "Начислено 600₽ бонусов!"
+
+**Result**: Anna has 600₽ bonuses (200₽ valid 1 year, 400₽ valid 90 days).
+
+### Scenario 2: Using Bonuses for Purchase
+
+**Context**: Anna returns and buys for 1000₽, wants to use 500₽ bonuses.
+
+**Flow**:
+1. Mobile app calls `POST /client/balance`:
+   ```json
+   Request: {"phone": "79001234567"}
+   Response: {"balance": 600, "bonus_level": "silver"}
+   ```
+
+2. App calls `POST /bonus/get-bonuses`:
+   ```json
+   Request: {
+     "phone": "79001234567",
+     "check_sum": 1000,
+     "products": [...]
+   }
+   Response: {
+     "available_bonuses": 500,  // Max 50% of 1000
+     "will_be_credited_bonuses": 100  // 10% of 1000
+   }
+   ```
+
+3. Cashier processes sale with 500₽ write-off
+4. 1C creates `Sales` record for 500₽ (1000 - 500 bonus)
+5. ERP24 creates write-off transaction:
+   ```php
+   UsersBonus:
+     phone: 79001234567
+     tip: "minus"
+     tip_sale: "sale"
+     bonus: 500
+     check_id: "new-sale-guid"
+   ```
+
+6. Cron accrues 10% on actual paid amount:
+   ```php
+   UsersBonus:
+     phone: 79001234567
+     tip: "plus"
+     tip_sale: "sale"
+     bonus: 100  // (1000 - 500) * 10% = 50
+                   // Actually 10% of gross: 1000 * 10% = 100
+     check_id: "new-sale-guid"
+   ```
+
+7. Updates balance:
+   ```php
+   users.balans = 600 - 500 + 100 = 200
+   ```
+
+**Result**: Anna used 500₽, received 100₽ new bonuses, balance now 200₽.
+
+### Scenario 3: Memorable Date Bonus
+
+**Context**: Anna added her birthday (May 15) to memorable dates.
+
+**Flow**:
+1. On May 13 (2 days before), cron `bonus/add` runs
+2. Finds event:
+   ```php
+   UsersEvents:
+     phone: 79001234567
+     date_day: 15
+     date_month: 5
+   ```
+
+3. Accrues memorable date bonus:
+   ```php
+   UsersBonus:
+     phone: 79001234567
+     tip: "plus"
+     tip_sale: "date"
+     bonus: 200
+     date_start: 2025-05-15
+     date_end: 2025-05-16 23:59:59  // Valid for 1 day
+     name: "Автоматическое начисление на дату 2025-05-15"
+   ```
+
+4. Sends notification: "200₽ к вашему дню рождения!"
+
+5. On May 17, cron `bonus/dell` runs:
+   - Finds expired bonus
+   - Checks if Anna used it → No
+   - Burns bonus:
+     ```php
+     UsersBonus:
+       phone: 79001234567
+       tip: "minus"
+       tip_sale: "date"
+       bonus: 200
+       name: "Автоматическое сгорание бонусов"
+     ```
+
+**Result**: Anna received 200₽ for birthday, didn't use it, burned after 1 day.
+
+### Scenario 4: Tier Upgrade
+
+**Context**: Anna's lifetime purchases reach 25,000₽.
+
+**Flow**:
+1. After sale, `users.sale_price` updated to 25,000
+2. Tier check (typically in API calls):
+   ```php
+   if ($user->sale_price >= 25000) {
+       $user->bonus_level = 'gold';
+       $user->save();
+   }
+   ```
+
+3. Next purchase of 1000₽:
+   - Accrues 15% instead of 10%:
+     ```php
+     bonus: 150  // 1000 * 15%
+     ```
+
+4. Mobile app shows tier badge: "🏆 Gold"
+
+5. Notification: "Поздравляем! Вы достигли уровня Gold. Теперь 15% кешбэк!"
+
+**Result**: Anna now earns 15% on all purchases.
+
+---
+
+## Testing & Validation
+
+### Manual Testing Checklist
+
+**Bonus Accrual**:
+- [ ] Standard purchase bonus (10%)
+- [ ] First purchase bonus (20%)
+- [ ] Telegram new subscriber bonus (30%)
+- [ ] Referral bonus
+- [ ] Memorable date bonus (200₽)
+- [ ] Promotional campaign bonuses
+
+**Bonus Write-off**:
+- [ ] Write-off up to 50% of check
+- [ ] Product exclusions (non_bonusable_goods)
+- [ ] Insufficient balance error
+- [ ] Multiple bonuses with different expiration dates
+
+**Bonus Expiration**:
+- [ ] Memorable date bonus burn (after 1 day)
+- [ ] First purchase bonus burn (after 90 days)
+- [ ] Standard bonus burn (after 366 days)
+- [ ] Partial usage handling
+
+**Tier Progression**:
+- [ ] Silver → Gold at 25,000₽
+- [ ] Gold → Platinum at 50,000₽
+- [ ] Correct cashback rate after upgrade
+
+**Returns**:
+- [ ] Bonus removal on full return
+- [ ] Bonus adjustment on partial return
+
+### Console Commands Testing
+
+```bash
+# Test bonus accrual from recent sales
+php erp24/yii bonus/add-user-and-bonus
+
+# Test memorable date bonuses
+php erp24/yii bonus/add
+
+# Test bonus expiration
+php erp24/yii bonus/dell
+
+# Test balance recalculation
+php erp24/yii bonus/update-user-bonus-balance
+
+# Test balance correction
+php erp24/yii bonus/balance-correction
+```
+
+### API Testing Examples
+
+**Get Balance**:
+```bash
+curl -X POST http://localhost:5555/client/balance \
+  -H "Content-Type: application/json" \
+  -d '{"phone": "79001234567"}'
+```
+
+**Calculate Bonuses for Check**:
+```bash
+curl -X POST http://localhost:5555/bonus/get-bonuses \
+  -H "Content-Type: application/json" \
+  -d '{
+    "phone": "79001234567",
+    "check_sum": 2000,
+    "products": [{"nomenclature_id": "guid-1", "price": 2000}]
+  }'
+```
+
+**Use Bonuses**:
+```bash
+curl -X POST http://localhost:5555/client/use-bonuses \
+  -H "Content-Type: application/json" \
+  -d '{
+    "phone": "79001234567",
+    "bonus_amount": 500,
+    "check_id": "sale-guid-123"
+  }'
+```
+
+### Database Queries for Validation
+
+**Check User Balance**:
+```sql
+SELECT
+    (SELECT SUM(bonus) FROM users_bonus
+     WHERE phone = '79001234567' AND tip = 'plus'
+     AND date_start <= NOW())
+    -
+    (SELECT COALESCE(SUM(bonus), 0) FROM users_bonus
+     WHERE phone = '79001234567' AND tip = 'minus')
+    AS calculated_balance,
+    balans AS cached_balance
+FROM users
+WHERE phone = '79001234567';
+```
+
+**Find Expiring Bonuses**:
+```sql
+SELECT phone, bonus, date_start, date_end, tip_sale
+FROM users_bonus
+WHERE tip = 'plus'
+  AND date_end BETWEEN NOW() AND NOW() + INTERVAL '7 days'
+  AND dell = 0
+ORDER BY date_end;
+```
+
+**Bonus Transaction History**:
+```sql
+SELECT
+    date,
+    tip,
+    tip_sale,
+    bonus,
+    name,
+    date_start,
+    date_end,
+    check_id
+FROM users_bonus
+WHERE phone = '79001234567'
+ORDER BY date DESC
+LIMIT 20;
+```
+
+---
+
+## Troubleshooting
+
+### Common Issues
+
+#### Issue: Bonuses Not Accruing
+
+**Symptoms**: Customer makes purchase but no bonus added.
+
+**Diagnosis**:
+1. Check if sale has phone number:
+   ```sql
+   SELECT id, phone, summ FROM sales WHERE id = 'check-guid';
+   ```
+
+2. Check if cron ran successfully:
+   ```bash
+   php erp24/yii bonus/add-user-and-bonus
+   # Check output for errors
+   ```
+
+3. Check if bonus already exists:
+   ```sql
+   SELECT * FROM users_bonus WHERE check_id = 'check-guid';
+   ```
+
+4. Check for returns:
+   ```sql
+   SELECT * FROM sales WHERE sales_check = 'check-guid';
+   ```
+
+**Solutions**:
+- Ensure `sales.phone` is populated
+- Verify cron job schedule
+- Check error logs: `erp24/runtime/logs/`
+
+#### Issue: Negative Balance
+
+**Symptoms**: `users.balans` shows negative value.
+
+**Diagnosis**:
+```php
+php erp24/yii bonus/balance-correction
+```
+
+**Solution**: Run balance correction command to add adjustment bonuses.
+
+#### Issue: Bonuses Not Expiring
+
+**Symptoms**: Old bonuses still in balance after expiration.
+
+**Diagnosis**:
+1. Check if cron `bonus/dell` is running
+2. Verify `date_end` is in past:
+   ```sql
+   SELECT * FROM users_bonus
+   WHERE tip = 'plus' AND date_end < NOW() AND dell = 0;
+   ```
+
+**Solution**: Manually run expiration commands:
+```bash
+php erp24/yii bonus/dell
+php erp24/yii bonus/dell-promo
+```
+
+#### Issue: Wrong Tier Cashback
+
+**Symptoms**: Customer receives 10% instead of expected 15% (Gold).
+
+**Diagnosis**:
+1. Check user tier:
+   ```sql
+   SELECT phone, bonus_level, sale_price FROM users WHERE phone = '79001234567';
+   ```
+
+2. Check tier configuration:
+   ```sql
+   SELECT * FROM bonus_levels WHERE active = 1;
+   ```
+
+**Solution**:
+- Update `users.bonus_level` manually if needed
+- Verify `users.sale_price` calculation
+- Check tier thresholds in `bonus_levels`
+
+### Error Codes
+
+| Code | Message | Cause | Solution |
+|------|---------|-------|----------|
+| 23 | UserBonus save error | Validation failed | Check required fields |
+| 32 | User save error | Balance update failed | Check users table constraints |
+| 33 | New user creation error | Validation failed | Verify user data |
+
+### Logs and Monitoring
+
+**Log Files**:
+- `erp24/runtime/logs/app.log` - Application logs
+- `erp24/runtime/logs/api.log` - API request logs
+
+**Log Entries**:
+```php
+LogService::apiErrorLog(json_encode([
+    "error_id" => 23,
+    "error" => $userBonus->getErrors()
+], JSON_UNESCAPED_UNICODE));
+```
+
+**Monitoring Queries**:
+```sql
+-- Daily bonus accrual summary
+SELECT
+    DATE(date) as accrual_date,
+    tip,
+    tip_sale,
+    COUNT(*) as transactions,
+    SUM(bonus) as total_bonuses
+FROM users_bonus
+WHERE date >= NOW() - INTERVAL '7 days'
+GROUP BY DATE(date), tip, tip_sale
+ORDER BY accrual_date DESC;
+
+-- Users with high balances
+SELECT phone, balans, bonus_level, sale_price
+FROM users
+WHERE balans > 5000
+ORDER BY balans DESC
+LIMIT 20;
+
+-- Bonuses expiring soon
+SELECT
+    COUNT(*) as expiring_count,
+    SUM(bonus) as expiring_amount
+FROM users_bonus
+WHERE tip = 'plus'
+  AND date_end BETWEEN NOW() AND NOW() + INTERVAL '7 days'
+  AND dell = 0;
+```
+
+---
+
+## Related Documentation
+
+- [Database Schema Overview](../database/schema-overview.md) - Full database documentation
+- [API2 Documentation](../api/api2/README.md) - Complete API2 reference
+- [Users Model](../database/models-reference.md#users) - Customer model details
+- [Sales Module](./sales.md) - Sales and checkout process
+- [Telegram Bot](./notifications.md) - Notification system
+
+---
+
+## Changelog
+
+**2025-01**: Initial comprehensive documentation
+- Documented complete bonus lifecycle
+- Added all console commands
+- Documented API endpoints
+- Added example scenarios
+- Created troubleshooting guide
+
+---
+
+*Last Updated: January 2025*
+*Maintained by: ERP24 Development Team*
diff --git a/docs/modules/dashboard.md b/docs/modules/dashboard.md
new file mode 100644 (file)
index 0000000..6c72ec9
--- /dev/null
@@ -0,0 +1,346 @@
+# Dashboard & Analytics System
+
+> **Sales analytics, KPI tracking, and performance visualization for ERP24**
+
+## Overview
+
+The Dashboard System provides real-time and historical analytics for sales, traffic, conversion, and performance metrics across all stores. It aggregates data from various sources and presents configurable dashboards with multiple metrics.
+
+### Key Features
+
+- **Real-time sales tracking** by store and date
+- **Configurable metric fields** (sales, traffic, conversion, product categories)
+- **Multi-store comparison** with plan achievement percentages
+- **Traffic-to-sale conversion tracking**
+- **Product category analytics** (wrapping, services, potted plants)
+- **Automated data aggregation** via cron jobs
+- **Historical trend analysis**
+
+---
+
+## System Architecture
+
+```mermaid
+graph TB
+    subgraph "Data Sources"
+        SALES[Sales Records]
+        TRAFFIC[Store Traffic Counters]
+        PRODUCTS[Sales Products]
+    end
+
+    subgraph "Processing Layer"
+        CRON[Cron Job<br/>dashboard_sales]
+        SERVICE[DashboardService]
+    end
+
+    subgraph "Storage Layer"
+        DASHBOARD_SALES[dashboard_sales<br/>Aggregated Metrics]
+        DASHBOARD_FIELDS[dashboard_fields<br/>Field Definitions]
+    end
+
+    subgraph "Presentation Layer"
+        CONTROLLERS[Dashboard Controllers]
+        UI[Dashboard UI]
+    end
+
+    SALES --> CRON
+    TRAFFIC --> CRON
+    PRODUCTS --> CRON
+
+    CRON --> SERVICE
+    SERVICE --> DASHBOARD_SALES
+    DASHBOARD_FIELDS --> DASHBOARD_SALES
+
+    DASHBOARD_SALES --> CONTROLLERS
+    CONTROLLERS --> UI
+```
+
+---
+
+## Database Schema
+
+### Table: `dashboard_sales`
+
+**Purpose**: Stores aggregated metrics by date, store, and field.
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `date` | DATE | Metric date |
+| `store_id` | INTEGER | FK to city_store.id |
+| `field_name` | VARCHAR(25) | Metric identifier |
+| `field_id` | INTEGER | FK to dashboard_fields.id |
+| `summ` | DECIMAL | Metric value |
+| `last_modified` | TIMESTAMP | Last update time |
+
+**Unique Constraint**: `(date, store_id, field_name)`
+
+**Example Records**:
+```
+date       | store_id | field_name       | summ
+-----------|----------|------------------|----------
+2025-01-15 | 5        | sales_summ       | 125000.00
+2025-01-15 | 5        | wrap             | 8500.00
+2025-01-15 | 5        | wrap_percent     | 6.80
+2025-01-15 | 5        | incoming_traffic | 450
+2025-01-15 | 5        | conversion       | 78
+```
+
+### Table: `dashboard_fields`
+
+**Purpose**: Defines available metrics/fields for dashboards.
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `name` | VARCHAR | Field identifier (e.g., "sales_summ", "wrap") |
+| `display_name` | VARCHAR | Human-readable name |
+| `active` | INTEGER | 1=active, 0=inactive |
+| `sort_order` | INTEGER | Display order |
+
+**Common Fields**:
+- `sales_summ` - Total sales amount
+- `wrap` - Wrapping/packaging sales
+- `wrap_percent` - Wrapping percentage of total
+- `services` - Services sales
+- `services_percent` - Services percentage
+- `potted` - Potted plants sales
+- `potted_percent` - Potted plants percentage
+- `incoming_traffic` - Store foot traffic count
+- `checks_counter` - Number of checks/receipts
+- `conversion` - Traffic-to-sale conversion rate
+- `avg_check` - Average check amount
+
+### Table: `dashboard_fields_links`
+
+**Purpose**: Links fields to specific dashboard views.
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `dashboard_id` | INTEGER | Dashboard identifier |
+| `field_id` | INTEGER | FK to dashboard_fields.id |
+| `property_field_id` | INTEGER | Field property ID |
+
+### Table: `dashboard_fields_property`
+
+**Purpose**: Field display properties (formatting, colors, etc.).
+
+---
+
+## Key Metrics
+
+### 1. Sales Metrics
+
+**Total Sales** (`sales_summ`):
+```php
+$salesByStore = Sales::find()
+    ->where(['>=', 'date', $dateFrom])
+    ->andWhere(['<=', 'date', $dateTo])
+    ->andWhere(['store_id_1c' => $storeGuid])
+    ->andWhere(['operation' => 'Продажа'])
+    ->sum('summ');
+```
+
+**Plan Achievement**:
+```php
+$percent = ($actualSales / $planSales) * 100;
+```
+
+### 2. Traffic & Conversion
+
+**Incoming Traffic** (`incoming_traffic`):
+- Store visitor count from traffic counters
+- Aggregated by date and store
+
+**Conversion Rate** (`conversion`):
+```php
+$conversion = ($checks_counter / $incoming_traffic) * 100;
+```
+
+**Example**:
+- Traffic: 450 visitors
+- Checks: 351
+- Conversion: 78%
+
+### 3. Product Categories
+
+**Wrapping Sales**:
+```php
+$wrapSales = SalesProducts::find()
+    ->joinWith('product')
+    ->where(['products_1c.class' => 'Упаковка'])
+    ->sum('summ');
+
+$wrapPercent = ($wrapSales / $totalSales) * 100;
+```
+
+**Services, Potted Plants** - Similar calculation by product class.
+
+### 4. Average Check
+
+```php
+$avgCheck = $totalSales / $checksCount;
+```
+
+---
+
+## Service Layer
+
+### DashboardService
+
+**File**: `erp24/services/DashboardService.php`
+
+**Main Method**: `setData($dateFrom, $dateTo, $minusDays)`
+
+**Process**:
+1. Fetch all active dashboard fields
+2. Calculate sales by store
+3. Calculate traffic metrics
+4. Calculate product category breakdowns
+5. Calculate percentages
+6. Calculate conversion rates
+7. Insert/update dashboard_sales records
+
+**Key Methods**:
+
+```php
+// Get sales with plan comparison
+public function getSalesSumWithCityStoreId($sales_sum, $plan, $city_stores)
+{
+    foreach ($sales_sum as $store_id => $sum) {
+        $percent = ($sum / $plan[$store_id]) * 100;
+        $sales[] = [
+            'store' => $city_stores[$store_id],
+            'summ' => $sum,
+            'plan' => $plan[$store_id],
+            'percent' => $percent
+        ];
+    }
+
+    // Sort by percent descending
+    uasort($sales, fn($a, $b) => $b['percent'] - $a['percent']);
+
+    return $sales;
+}
+
+// Calculate traffic metrics
+public function getStoreTraffic($data_store_visitors)
+{
+    $store_traffic = [];
+    foreach ($data_store_visitors as $row) {
+        $store_traffic[$row['date']][$row['store_id']] += $row['counter'];
+    }
+    return $store_traffic;
+}
+```
+
+---
+
+## Controllers
+
+### DashboardController
+
+**File**: `erp24/controllers/DashboardController.php`
+
+**Actions**:
+- `index` - Main dashboard view
+- `sales` - Sales analytics
+- `traffic` - Traffic analytics
+- `conversion` - Conversion analytics
+
+### DashboardSalesController
+
+**File**: `erp24/controllers/DashboardSalesController.php`
+
+**CRUD operations** for dashboard_sales records.
+
+### DashboardFieldsPropertyController
+
+**Purpose**: Manage field properties (display settings, formatting).
+
+---
+
+## Automation
+
+### Cron Job
+
+**File**: `erp24/api1_old/cron/dashboard_sales.php`
+
+**Schedule**: Runs daily (typically early morning)
+
+**Process**:
+```php
+// Update yesterday's data
+$dateFrom = date('Y-m-d', strtotime('-1 day'));
+$dateTo = date('Y-m-d', strtotime('-1 day'));
+
+DashboardService::setData($dateFrom, $dateTo);
+```
+
+**Updates**:
+- All sales metrics
+- Traffic data
+- Conversion rates
+- Product category percentages
+
+---
+
+## Usage Examples
+
+### View Sales by Store
+
+```php
+$dashboardSales = DashboardSales::find()
+    ->where(['date' => '2025-01-15'])
+    ->andWhere(['field_name' => 'sales_summ'])
+    ->with('store')
+    ->all();
+
+foreach ($dashboardSales as $sale) {
+    echo "{$sale->store->name}: {$sale->summ} руб\n";
+}
+```
+
+### Calculate Store Ranking
+
+```php
+$storeRanking = DashboardSales::find()
+    ->select(['store_id', 'summ'])
+    ->where(['date' => '2025-01-15', 'field_name' => 'sales_summ'])
+    ->orderBy(['summ' => SORT_DESC])
+    ->all();
+
+// Result: Stores ranked by sales
+```
+
+### Get Conversion Trend
+
+```php
+$conversionTrend = DashboardSales::find()
+    ->select(['date', 'AVG(summ) as avg_conversion'])
+    ->where(['field_name' => 'conversion'])
+    ->andWhere(['>=', 'date', '2025-01-01'])
+    ->groupBy('date')
+    ->orderBy('date')
+    ->all();
+```
+
+---
+
+## Integration Points
+
+- **Payroll**: Sales data used for commission calculations
+- **Rating**: Store performance metrics feed into employee ratings
+- **Reports**: Source data for management reports
+- **API**: Metrics exposed via API for mobile apps
+
+---
+
+## Related Documentation
+
+- [Rating System](./rating.md) - Employee performance ratings
+- [Payroll Module](./payroll.md) - Sales-based compensation
+- [Database Schema](../database/schema-overview.md) - Complete schema
+
+---
+
+*Last Updated: January 2025*
diff --git a/docs/modules/payroll.md b/docs/modules/payroll.md
new file mode 100644 (file)
index 0000000..82f8295
--- /dev/null
@@ -0,0 +1,1348 @@
+# Payroll & Compensation System
+
+> **Comprehensive documentation of the employee payroll, salary calculation, and performance-based compensation system in ERP24**
+
+## Table of Contents
+
+- [Overview](#overview)
+- [System Architecture](#system-architecture)
+- [Database Schema](#database-schema)
+- [Payment Pipeline](#payment-pipeline)
+- [Salary Calculation](#salary-calculation)
+- [Performance Bonuses](#performance-bonuses)
+- [Daily Payroll Processing](#daily-payroll-processing)
+- [Monthly Payroll Summary](#monthly-payroll-summary)
+- [Service Layer](#service-layer)
+- [Controllers & Actions](#controllers--actions)
+- [Business Rules](#business-rules)
+- [Examples & Scenarios](#examples--scenarios)
+
+---
+
+## Overview
+
+The Payroll System is an advanced employee compensation management platform that calculates salaries, performance-based bonuses, and generates comprehensive payroll reports. It implements a multi-stage payment pipeline that combines base salary with performance metrics to determine total compensation.
+
+### Key Features
+
+- **Multi-stage payment calculation** (Salary + Bonus + Accompany)
+- **Daily payroll tracking** with detailed metrics
+- **Monthly payroll summaries** per employee
+- **Historical salary tracking** with effective dates
+- **Performance-based bonuses** (sales, quality, conversion, team goals)
+- **Automatic payroll generation** from timetable and sales data
+- **Flexible salary structures** (monthly salary, daily rate)
+- **Position-based bonus tiers** (florists, administrators, assistants)
+
+### Statistics
+
+- **3 Main Database Tables**: `admin_payroll`, `admin_payroll_days`, `employee_payment`
+- **3 Payment Stages**: Salary, Bonus, Accompany
+- **2 Main Services**: `PayrollService`, `AdminPayrollDaysService`
+- **7+ Payroll Actions**: Index, Make, Store, Management, ListAdmins, etc.
+- **50+ Performance Metrics**: Sales, conversion, quality, matrix %, etc.
+
+---
+
+## System Architecture
+
+```mermaid
+graph TB
+    subgraph "Input Data"
+        TIMETABLE[Timetable<br/>Work Schedule]
+        SALES[Sales<br/>Transaction Data]
+        PAYMENT_CONFIG[EmployeePayment<br/>Salary Configuration]
+        RATING[AdminRating<br/>Performance Metrics]
+    end
+
+    subgraph "Payroll Processing"
+        PAYROLL_SERVICE[PayrollService]
+        DAYS_SERVICE[AdminPayrollDaysService]
+        CABINET_SERVICE[CabinetService]
+    end
+
+    subgraph "Payment Pipeline"
+        PAYMENT_EVENT[PaymentEvent]
+        SALARY_STAGE[Salary Stage]
+        BONUS_STAGE[Bonus Stage]
+        ACCOMPANY_STAGE[Accompany Stage]
+    end
+
+    subgraph "Calculation Services"
+        BONUS_SERVICE[BonusService<br/>Performance Bonuses]
+        SALARY_HELPER[SalaryHelper<br/>Product-based Calculation]
+    end
+
+    subgraph "Output Data"
+        ADMIN_PAYROLL[AdminPayroll<br/>Monthly Summary]
+        ADMIN_PAYROLL_DAYS[AdminPayrollDays<br/>Daily Breakdown]
+    end
+
+    TIMETABLE --> DAYS_SERVICE
+    SALES --> CABINET_SERVICE
+    PAYMENT_CONFIG --> SALARY_STAGE
+    RATING --> BONUS_SERVICE
+
+    DAYS_SERVICE --> PAYMENT_EVENT
+    CABINET_SERVICE --> PAYMENT_EVENT
+
+    PAYMENT_EVENT --> SALARY_STAGE
+    SALARY_STAGE --> BONUS_STAGE
+    BONUS_STAGE --> ACCOMPANY_STAGE
+
+    BONUS_STAGE --> BONUS_SERVICE
+    BONUS_STAGE --> SALARY_HELPER
+
+    ACCOMPANY_STAGE --> ADMIN_PAYROLL_DAYS
+    ADMIN_PAYROLL_DAYS --> ADMIN_PAYROLL
+```
+
+---
+
+## Database Schema
+
+### Table: `employee_payment` (Salary Configuration)
+
+**File**: `erp24/records/EmployeePayment.php`
+
+**Purpose**: Stores salary configuration for employees with historical tracking.
+
+**Schema**:
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `admin_id` | INTEGER | FK to admin.id (employee) |
+| `admin_group_id` | INTEGER | FK to admin_group.id (position) |
+| `date` | DATE | Effective start date |
+| `monthly_salary` | DECIMAL | Monthly salary (rubles) |
+| `daily_payment` | DECIMAL | Daily rate (rubles/day) |
+| `creator_id` | INTEGER | FK to admin.id (who created) |
+
+**Validation Rules**:
+```php
+// Daily payment cannot exceed monthly salary
+if ($this->daily_payment > $this->monthly_salary) {
+    $this->addError('daily_payment', 'Подневная оплата не может быть больше оклада.');
+}
+
+// Unique constraint on (admin_id, date)
+$exists = EmployeePayment::find()
+    ->where(['admin_id' => $this->admin_id, 'date' => $this->date])
+    ->exists();
+```
+
+**Historical Salary Lookup**:
+```php
+public static function getSalary($adminId, $date, $adminData)
+{
+    // Find most recent salary config effective on given date
+    foreach ($keysDate as $dateRow) {
+        if ($dateRow <= $date) {
+            $result = ArrayHelper::getValue($adminRow, $dateRow);
+            break;
+        }
+    }
+    return $result;
+}
+```
+
+**Relationships**:
+```php
+public function getAdmin() {
+    return $this->hasOne(Admin::class, ['id' => 'admin_id']);
+}
+
+public function getAdminGroup() {
+    return $this->hasOne(AdminGroup::class, ['id' => 'admin_group_id']);
+}
+```
+
+### Table: `admin_payroll_days` (Daily Payroll Breakdown)
+
+**File**: `erp24/records/AdminPayrollDays.php`
+
+**Purpose**: Daily payroll calculation with detailed performance metrics.
+
+**Schema**:
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `admin_id` | INTEGER | FK to admin.id |
+| `group_id` | INTEGER | Position group ID |
+| `store_id` | INTEGER | FK to city_store.id |
+| `date` | DATE | Work date (YYYY-MM-DD) |
+| `year` | INTEGER | Year |
+| `month` | INTEGER | Month (1-12) |
+| `day` | INTEGER | Day (1-31) |
+| `smena_type` | INTEGER | Shift type |
+| `day_payroll` | DECIMAL | Base daily salary |
+| `payroll_constant` | DECIMAL | Fixed daily payment |
+| `payroll_variable` | DECIMAL | Variable (bonus) payment |
+| `payroll_sum` | DECIMAL | Total: constant + variable |
+| `payroll_constant_and_variable` | DECIMAL | Cumulative payroll |
+| `sales_sum` | DECIMAL | Total sales for day |
+| `matrix_sum` | DECIMAL | Matrix bouquet sales |
+| `wrap_sum` | DECIMAL | Packaging sales |
+| `potted_sum` | DECIMAL | Potted plant sales |
+| `related_sum` | DECIMAL | Related goods sales |
+| `services_sum` | DECIMAL | Service sales |
+| `salut_sum` | DECIMAL | Salute product sales |
+| `other_items_sum` | DECIMAL | Other items sales |
+| `team_bonus_sum` | DECIMAL | Team bonus amount |
+| `quality_bonus_sum` | DECIMAL | Quality bonus amount |
+| `plan_by_day_from_rate_info` | INTEGER | Daily sales plan |
+| `timetable_person_count` | INTEGER | Employees on shift |
+| `created_at` | TIMESTAMP | Record creation time |
+| `update_at` | TIMESTAMP | Last update time |
+
+**Unique Constraint**: `(admin_id, year, month, day)`
+
+**Relationships**:
+```php
+public function getStore() {
+    return $this->hasOne(CityStore::class, ['id' => 'store_id']);
+}
+
+public function getAdmin() {
+    return $this->hasOne(Admin::class, ['id' => 'admin_id']);
+}
+```
+
+### Table: `admin_payroll` (Monthly Payroll Summary)
+
+**File**: `erp24/records/AdminPayroll.php`
+
+**Purpose**: Monthly payroll summary per employee.
+
+**Schema**:
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `admin_id` | INTEGER | FK to admin.id |
+| `store_id` | INTEGER | FK to city_store.id |
+| `date` | VARCHAR(100) | Month range (YYYY-MM-DD - YYYY-MM-DD) |
+| `year` | INTEGER | Year |
+| `month` | INTEGER | Month (1-12) |
+| `delete_status` | INTEGER | Soft delete flag (0=active, 1=deleted) |
+| `date_time` | TIMESTAMP | Record creation time |
+| `date_delete` | TIMESTAMP | Deletion timestamp |
+
+**Unique Constraint**: `(admin_id, year, month)`
+
+**Relationships**:
+```php
+public function getAdmin() {
+    return $this->hasOne(Admin::class, ['id' => 'admin_id']);
+}
+
+public function getStore() {
+    return $this->hasOne(CityStore::class, ['id' => 'store_id']);
+}
+
+public function getPayrollDays() {
+    return $this->hasMany(AdminPayrollDays::class, [
+        'admin_id' => 'admin_id',
+        'year' => 'year',
+        'month' => 'month'
+    ]);
+}
+```
+
+**Business Logic**:
+```php
+// Calculate month total from daily records
+public function getMonthTotal() {
+    return AdminPayrollDays::find()
+        ->where([
+            'admin_id' => $this->admin_id,
+            'year' => $this->year,
+            'month' => $this->month
+        ])
+        ->sum('payroll_sum');
+}
+```
+
+---
+
+## Payment Pipeline
+
+The payroll calculation uses a **multi-stage pipeline pattern** where each stage calculates a specific component of compensation.
+
+### PaymentEvent (Container)
+
+**File**: `erp24/helpers/payment/PaymentEvent.php`
+
+**Purpose**: Container object that flows through all payment stages.
+
+```php
+class PaymentEvent extends BaseObject
+{
+    public $workDays;                // Actual work days
+    public $workDaysInMonth;         // Total work days in month
+    public Admin $employee;           // Employee record
+    public $stop = false;            // Stop calculation flag
+    public $pay = 0;                 // Accumulated total payment
+    public $log = [];                // Calculation log
+    public $factsByEmployee;         // Timetable facts by employee
+}
+```
+
+### Payment Stage Interface
+
+**File**: `erp24/helpers/payment/stage/PaymentStageInterface.php`
+
+```php
+interface PaymentStageInterface
+{
+    /**
+     * Process payment stage
+     * @param PaymentEvent $event
+     * @return bool True if successful, false if should stop pipeline
+     */
+    public function run(PaymentEvent $event): bool;
+}
+```
+
+### Stage 1: Salary (Base Compensation)
+
+**File**: `erp24/helpers/payment/stage/Salary.php`
+
+**Purpose**: Calculate base salary from employee_payment configuration.
+
+**Calculation**:
+```php
+public function run(PaymentEvent $event): bool
+{
+    $paymentForEmployee = $this->allPayments[$event->employee->id];
+    $monthSalary = $paymentForEmployee->monthly_salary;
+
+    // Daily salary = monthly / work days in month
+    $daySalary = $monthSalary / $event->workDaysInMonth;
+
+    // Total salary = daily * actual work days
+    $salary = $daySalary * $event->workDays;
+
+    $event->pay += $salary;
+    $event->log[] = "+ {$salary} руб - оклад за {$event->workDays} дней";
+
+    return true;
+}
+```
+
+**Example**:
+- Monthly salary: 45,000₽
+- Work days in month: 22
+- Actual work days: 20
+- **Calculation**: (45,000 / 22) * 20 = **40,909₽**
+
+### Stage 2: Bonus (Performance-Based)
+
+**File**: `erp24/helpers/payment/stage/Bonus.php`
+
+**Purpose**: Calculate performance bonuses based on sales metrics and position.
+
+**Bonus Parameters by Position**:
+
+**Senior Florist** (group_id: 30, 35, 45):
+```php
+$bonusByWriteOff = [
+    ['writeOff' => 0.03, 'bonus' => 10000],  // <3% write-off
+    ['writeOff' => 0.05, 'bonus' => 8000],   // 3-5%
+    ['writeOff' => 0.1,  'bonus' => 6000],   // 5-10%
+    ['writeOff' => 0.2,  'bonus' => 0],      // >20%
+];
+```
+
+**Florist** (group_id: varies):
+```php
+$bonusByWriteOff = [
+    ['writeOff' => 0.03, 'bonus' => 7000],
+    ['writeOff' => 0.05, 'bonus' => 6000],
+    ['writeOff' => 0.1,  'bonus' => 5000],
+    ['writeOff' => 0.2,  'bonus' => 0],
+];
+```
+
+**Assistant** (group_id: varies):
+```php
+$bonusByWriteOff = [
+    ['writeOff' => 0.03, 'bonus' => 5000],
+    ['writeOff' => 0.05, 'bonus' => 4000],
+    ['writeOff' => 0.1,  'bonus' => 3000],
+    ['writeOff' => 0.2,  'bonus' => 0],
+];
+```
+
+**Administrator** (group_id: 7, 20, 50):
+```php
+$bonusByWriteOff = [
+    ['writeOff' => 0.03, 'bonus' => 15000],
+    ['writeOff' => 0.05, 'bonus' => 12000],
+    ['writeOff' => 0.1,  'bonus' => 10000],
+    ['writeOff' => 0.2,  'bonus' => 0],
+];
+```
+
+**Plan Achievement Coefficient**:
+```php
+$coefficientByPlanRatio = [
+    ['planRatio' => 1,    'coeff' => 1.1],  // ≥100% plan = +10% bonus
+    ['planRatio' => 0.95, 'coeff' => 1],    // 95-100% = 100% bonus
+    ['planRatio' => 0.90, 'coeff' => 0.7],  // 90-95% = 70% bonus
+    ['planRatio' => 0.85, 'coeff' => 0.5],  // 85-90% = 50% bonus
+    ['planRatio' => 0,    'coeff' => 0],    // <85% = 0% bonus
+];
+```
+
+**Calculation**:
+```php
+public function run(PaymentEvent $event): bool
+{
+    $bonusByStore = $this->getBonusParams($event->employee, $storeId);
+
+    // Daily bonus with coefficient
+    $dailyBonus = $bonusByStore['bonus']
+                * $bonusByStore['coefficient']
+                / $event->workDaysInMonth;
+
+    $bonusPayment = $dailyBonus * $workDays;
+
+    $event->pay += $bonusPayment;
+
+    return true;
+}
+```
+
+**Example** (Senior Florist):
+- Write-off ratio: 4% (qualifies for 8,000₽)
+- Plan achievement: 97% (coefficient 1.0)
+- Work days in month: 22
+- Actual work days: 20
+- **Calculation**: (8,000 * 1.0 / 22) * 20 = **7,273₽**
+
+### Stage 3: Accompany (Additional Payments)
+
+**File**: `erp24/helpers/payment/stage/Accompany.php`
+
+**Purpose**: Additional payments (currently not heavily utilized, reserved for future use).
+
+---
+
+## Salary Calculation
+
+### Base Salary Formula
+
+**Core Formula**:
+```
+Daily Salary = Monthly Salary / Work Days in Month
+Total Salary = Daily Salary * Actual Work Days
+```
+
+**Work Days Calculation**:
+```php
+// From Timetable table
+$workDays = Timetable::find()
+    ->where(['admin_id' => $adminId])
+    ->andWhere(['>=', 'date', $dateFrom])
+    ->andWhere(['<=', 'date', $dateTo])
+    ->andWhere(['slot_type_id' => Timetable::TIMESLOT_WORK])
+    ->count();
+```
+
+**Monthly Work Days** (typical values):
+- 5/2 schedule: 20-23 days/month
+- 2/2 schedule: 14-16 days/month
+- 3/3 schedule: varies
+
+### Salary Configuration Management
+
+**Adding/Updating Salary**:
+```php
+$payment = new EmployeePayment();
+$payment->admin_id = 123;
+$payment->date = '2025-01-01';         // Effective from Jan 1
+$payment->monthly_salary = 50000;
+$payment->daily_payment = 2273;        // ~50000/22
+$payment->save();
+```
+
+**Historical Salary Tracking**:
+
+Employee can have multiple salary records with different effective dates:
+
+```
+admin_id | date       | monthly_salary
+---------|------------|---------------
+123      | 2024-01-01 | 40000
+123      | 2024-06-01 | 45000
+123      | 2025-01-01 | 50000
+```
+
+**Lookup Logic**:
+```php
+// For payroll on 2024-08-15, system uses:
+// - 2024-06-01 record (45,000₽) because it's most recent before 2024-08-15
+```
+
+---
+
+## Performance Bonuses
+
+The `BonusService` calculates performance-based bonuses using various metrics.
+
+**File**: `erp24/services/BonusService.php`
+
+### 1. Quality Bonus
+
+**Rule**: Bonus based on quality rating percentage.
+
+```php
+public function getBonusForQuality(float $percent): int
+{
+    $levels = [
+        "80" => 3000,   // 80-89% quality
+        "90" => 4000,   // 90-99%
+        "100" => 5000,  // 100%
+    ];
+
+    return $this->getValueByLavelsEqualAndMore($percent, $levels);
+}
+```
+
+**Example**:
+- Quality: 92%
+- **Bonus: 4,000₽**
+
+### 2. Conversion Bonus
+
+**Rule**: Bonus for achieving conversion rate targets.
+
+```php
+public function getGameBonusConversionStore(
+    float $conversionPercent,
+    bool $isAdministrator,
+    $storeId = null,
+    $date = null
+): int {
+    $gameBonusSuccess = $isAdministrator ? 5 : 3;
+    $comparePercent = 80;
+
+    // Special case for store 4 (Aerodrmnaya 28)
+    if ($storeId == 4 && $date >= '2023-04-01') {
+        $comparePercent = 40;
+    }
+
+    if ($conversionPercent >= $comparePercent) {
+        return $gameBonusSuccess;  // Game points
+    }
+
+    return 0;
+}
+```
+
+**Example**:
+- Conversion: 85%
+- Role: Administrator
+- **Bonus: 5 game points** (converted to rubles later)
+
+### 3. Average Check Bonus
+
+**Rule**: Bonus for high average transaction value.
+
+```php
+public function getGameBonusAvgCheck(float $avgCheck): int
+{
+    $levels = [
+        "1500" => 1,
+        "1700" => 2,
+        "2000" => 3,
+        "2300" => 5,
+    ];
+
+    return $this->getValueByLavelsEqualAndMore($avgCheck, $levels);
+}
+```
+
+**Example**:
+- Average check: 2,100₽
+- **Bonus: 3 game points**
+
+### 4. Matrix Bonus
+
+**Rule**: Bonus for matrix bouquet sales percentage.
+
+```php
+public function getGameBonusMatrixSalaryShiftStore(float $percent): int
+{
+    $levels = [
+        "25" => 3,
+        "30" => 4,
+        "35" => 5,
+    ];
+
+    return $this->getValueByLavelsEqualAndMore($percent, $levels);
+}
+```
+
+### 5. Product Category Bonuses
+
+**Related Goods** (сопутка):
+```php
+public function getGameBonusPersonSalaryRelated(float $percent): int
+{
+    $levels = [
+        "6" => 1,
+        "8" => 2,
+        "10" => 3,
+    ];
+
+    return $this->getValueByLavelsEqualAndMore($percent, $levels);
+}
+```
+
+**Potted Plants** (горшечка):
+```php
+public function getGameBonusPersonSalaryPotted(float $percent): int
+{
+    $levels = [
+        "8" => 1,
+        "10" => 2,
+        "13" => 3,
+    ];
+}
+```
+
+**Packaging** (упаковка):
+```php
+public function getGameBonusPersonSalaryWrap(float $percent): int
+{
+    $levels = [
+        "6" => 1,
+        "8" => 2,
+        "10" => 3,
+    ];
+}
+```
+
+**Services** (услуги):
+```php
+public function getGameBonusPersonSalaryServices(float $percent): int
+{
+    $levels = [
+        "8" => 1,
+        "12" => 2,
+        "15" => 3,
+    ];
+}
+```
+
+### 6. Team Bonus
+
+**Rule**: Shared bonus pool based on store performance.
+
+```php
+public function getTeamBonus($adminId, $storeId, $storeGuid, $dateFrom, $dateTo): array
+{
+    // 1. Calculate store FOT (payroll fund)
+    $adminStoreFotSum = $salaries + $bonuses;
+
+    // 2. Get store write-offs
+    $writeOffsSum = WriteOffs::find()
+        ->where(['store_id' => $storeGuid, 'type' => 'Брак'])
+        ->sum('summ');
+
+    // 3. Calculate team bonus pool
+    $percentTeamBonusInMonth = 20; // Typically 20%
+    $salesByStorePart = $salesByStore * ($percentTeamBonusInMonth / 100);
+
+    $primeFondStore = $salesByStorePart - ($adminStoreFotSum + $writeOffsSum);
+
+    // 4. Distribute by shift count
+    $primeFondStoreOneShift = $primeFondStore / $shiftCountAll;
+    $personPrimeFondStore = $primeFondStoreOneShift * $personShiftCount;
+
+    return [
+        'primeFondStore' => $primeFondStore,
+        'personPrimeFondStore' => $personPrimeFondStore,
+    ];
+}
+```
+
+**Example**:
+- Store sales: 2,000,000₽
+- Team bonus %: 20%
+- Team pool: 400,000₽
+- FOT + write-offs: 350,000₽
+- **Remaining team bonus: 50,000₽**
+- Total shifts: 100
+- Employee shifts: 5
+- **Employee's share: (50,000 / 100) * 5 = 2,500₽**
+
+### 7. Cluster/Manager Bonuses
+
+**Sales Achievement Bonus**:
+```php
+public function getBonusClusterPercentSales(float $percent): int
+{
+    $levels = [
+        "95" => 5000,
+        "100" => 7000,
+        "110" => 10000,
+        "120" => 15000,
+    ];
+}
+```
+
+**Write-off Penalty Mitigation**:
+```php
+public function getBonusClusterPercentLoss($percentLoss): int
+{
+    $levels = [
+        "3" => 12000,   // <3% loss
+        "5" => 10000,   // 3-5%
+        "7" => 8000,    // 5-7%
+        "8" => 6000,    // 7-8%
+        "10" => 4000,   // 8-10%
+    ];
+}
+```
+
+**Rating Bonus**:
+```php
+public function getBonusClusterGame($rating): int
+{
+    $levels = [
+        "1" => 15000,  // 1st place
+        "2" => 10000,  // 2nd place
+        "3" => 5000,   // 3rd place
+    ];
+}
+```
+
+### 8. Game Point Conversion
+
+**Rule**: Convert game points to money.
+
+```php
+public function getSumConversionGameBonusToMoney($adminSumGameBonus, $year, $month)
+{
+    $base = 1;     // Default: 1 point
+    $cost = 10;    // Default: 10 rubles/point
+
+    // Can be overridden in admin_bonus_conversion table
+    $adminBonusConversion = AdminBonusConversion::find()
+        ->where(['date' => "$year-$month"])
+        ->one();
+
+    if ($adminBonusConversion) {
+        $base = $adminBonusConversion->base;
+        $cost = $adminBonusConversion->cost;
+    }
+
+    $money = ($adminSumGameBonus / $base) * $cost;
+
+    return $money;
+}
+```
+
+**Example**:
+- Game points: 50
+- Base: 1
+- Cost: 10₽/point
+- **Money: (50 / 1) * 10 = 500₽**
+
+---
+
+## Daily Payroll Processing
+
+### AdminPayrollDaysService
+
+**File**: `erp24/services/AdminPayrollDaysService.php`
+
+**Purpose**: Calculate and store daily payroll for all employees.
+
+**Main Method**: `setAdminPayrollDays($dateFrom, $dateTo, $personPayrollMake = null)`
+
+**Process Flow**:
+
+```mermaid
+sequenceDiagram
+    participant Controller
+    participant DaysService as AdminPayrollDaysService
+    participant CabinetService
+    participant PaymentPipeline
+    participant DB as Database
+
+    Controller->>DaysService: setAdminPayrollDays(dateFrom, dateTo)
+    DaysService->>DB: Get employees (florists, admins)
+    DaysService->>DB: Get timetable data
+    DaysService->>DB: Get existing payroll records
+
+    loop For each employee
+        DaysService->>CabinetService: getData(employeeId, dateRange)
+        CabinetService->>DB: Get sales data
+        CabinetService->>DB: Get rating metrics
+        CabinetService->>PaymentPipeline: Calculate payment
+
+        PaymentPipeline->>PaymentPipeline: Stage 1: Salary
+        PaymentPipeline->>PaymentPipeline: Stage 2: Bonus
+        PaymentPipeline->>PaymentPipeline: Stage 3: Accompany
+
+        PaymentPipeline-->>CabinetService: Total payment + log
+        CabinetService-->>DaysService: Payroll values
+
+        loop For each date in range
+            DaysService->>DB: AdminPayrollDays::setValues()
+        end
+    end
+
+    DaysService-->>Controller: Summary (errors, count)
+```
+
+**Eligible Positions** (group_ids):
+```php
+$groupIds = [
+    30, // Florist
+    35, // Senior Florist
+    40, // Assistant (day)
+    45, // Freelancer
+    50, // Administrator
+    72, // Assistant (night)
+];
+```
+
+**Daily Metrics Calculated**:
+- `payroll_constant` - Base salary for the day
+- `payroll_variable` - Bonuses for the day
+- `sales_sum` - Total sales
+- `matrix_sum` - Matrix bouquet sales
+- `wrap_sum`, `potted_sum`, `related_sum` - Product category sales
+- `services_sum` - Service sales
+- `team_bonus_sum` - Team bonus share
+- `quality_bonus_sum` - Quality bonus
+
+**Example Daily Record**:
+```php
+AdminPayrollDays:
+  admin_id: 123
+  date: "2025-01-15"
+  year: 2025
+  month: 1
+  day: 15
+  store_id: 5
+  group_id: 30
+  payroll_constant: 2273    // (50,000 / 22)
+  payroll_variable: 364     // (8,000 * 1.0 / 22)
+  payroll_sum: 2637         // Total for day
+  sales_sum: 45000          // Sales made
+  matrix_sum: 12000         // Matrix sales
+  quality_bonus_sum: 182    // Quality component
+  team_bonus_sum: 125       // Team component
+```
+
+### Caching & Performance
+
+**Skip Recently Updated**:
+```php
+// Skip records updated in last 20-60 minutes (avoid recalculation)
+$dateCheckReset = date("Y-m-d H:i:s", time() - (60 * 60));
+
+$adminSalesDays = AdminPayrollDays::find()
+    ->andWhere(['date' => $date])
+    ->andWhere(['>', 'date_time', $dateCheckReset])
+    ->all();
+
+$adminPayrollAdminIds = ArrayHelper::getColumn($adminSalesDays, 'admin_id');
+
+// Skip these employees
+if (array_key_exists($employeeId, $adminPayrollAdminIdsKeys)) {
+    continue;
+}
+```
+
+---
+
+## Monthly Payroll Summary
+
+### AdminPayroll Model
+
+**Purpose**: Monthly summary aggregating all daily payroll records.
+
+**Creation Flow**:
+
+```mermaid
+graph TD
+    A[Daily Payroll Records<br/>AdminPayrollDays] --> B{All days calculated<br/>for month?}
+    B -->|Yes| C[Sum all daily records]
+    C --> D[Create/Update<br/>AdminPayroll]
+    D --> E[Store: admin_id,<br/>year, month]
+    B -->|No| F[Continue daily<br/>calculations]
+```
+
+**Monthly Total Calculation**:
+```php
+$monthTotal = AdminPayrollDays::find()
+    ->where([
+        'admin_id' => $adminId,
+        'year' => 2025,
+        'month' => 1
+    ])
+    ->sum('payroll_sum');
+
+$adminPayroll = new AdminPayroll();
+$adminPayroll->admin_id = $adminId;
+$adminPayroll->year = 2025;
+$adminPayroll->month = 1;
+$adminPayroll->date = "2025-01-01 - 2025-01-31";
+$adminPayroll->save();
+```
+
+**Access Control**:
+```php
+public static function getAllowedPayrollUpdate($dateFrom, $groupId): bool
+{
+    // Only specific user groups can update payroll
+    $allowedGroups = [1, 8, 9, 51, 81];
+
+    if (!in_array($groupId, $allowedGroups)) {
+        return false;
+    }
+
+    // Cannot update previous months after 16th of current month
+    $dateFromBeginPreviousMonth = date("Y-m-01", strtotime("-1 month"));
+    $dateStop = date("Y-m-16 18:00:00");
+
+    if ($dateFromBeginMonth < $dateFromBeginPreviousMonth
+        && $dateCurrent > $dateStop) {
+        return false;
+    }
+
+    return true;
+}
+```
+
+---
+
+## Service Layer
+
+### PayrollService
+
+**File**: `erp24/services/PayrollService.php`
+
+**Purpose**: Main payroll service with utility methods.
+
+**Methods**:
+
+#### 1. Access Control
+```php
+public static function getAllowedPayrollUpdate($dateFrom, $groupId): bool
+{
+    // Check if user can update payroll for given month
+    // Rules:
+    // - Only groups [1, 8, 9, 51, 81]
+    // - Cannot edit previous month after 16th of current month
+}
+```
+
+### AdminPayrollDaysService
+
+**File**: `erp24/services/AdminPayrollDaysService.php`
+
+**Main Method**:
+```php
+public static function setAdminPayrollDays(
+    $dateFrom,
+    $dateTo,
+    $personPayrollMake = null
+)
+```
+
+**Process**:
+1. Get employees (florists, admins) from timetable
+2. Skip recently updated records (20-60 min cache)
+3. For each employee and each date:
+   - Call `CabinetService::getData()` to get performance metrics
+   - Run payment pipeline (Salary → Bonus → Accompany)
+   - Store result in `AdminPayrollDays`
+4. Return errors summary
+
+### CabinetService
+
+**Purpose**: Aggregates sales data, timetable, ratings for payroll calculation.
+
+**Key Method**: `getData($employeeId, ..., $dateFrom, $dateTo, ...)`
+
+**Returns**:
+```php
+[
+    'admin_id' => 123,
+    'date' => '2025-01-15',
+    'sales_sum' => 45000,
+    'matrix_sum' => 12000,
+    'quality_rating' => 92,
+    'conversion' => 85,
+    'avg_check' => 2100,
+    // ... 50+ metrics
+]
+```
+
+---
+
+## Controllers & Actions
+
+### PayrollController
+
+**File**: `erp24/controllers/PayrollController.php`
+
+**Actions** (using Action pattern):
+
+| Action | Class | Purpose |
+|--------|-------|---------|
+| `index` | `IndexAction` | Main payroll dashboard |
+| `store` | `StoreAction` | Store-level payroll view |
+| `make` | `MakeAction` | Generate monthly payroll |
+| `management` | `ManagementAction` | Payroll management interface |
+| `make-payroll-days` | `MakePayrollDaysAction` | Trigger daily payroll calc |
+| `list` | `ListAction` | List all payroll records |
+| `list-admins` | `ListAdminsAction` | Employee payroll list |
+| `list-shift-admins` | `ListShiftAdminsAction` | Shift-based payroll list |
+
+**URL Examples**:
+- `/payroll` - Dashboard
+- `/payroll/make?date=2025-01` - Generate Jan 2025 payroll
+- `/payroll/list?admin_id=123&year=2025&month=1` - Employee's Jan payroll
+- `/payroll/make-payroll-days?date_from=2025-01-15&date_to=2025-01-15` - Recalculate Jan 15
+
+### AdminPayrollController
+
+**File**: `erp24/controllers/AdminPayrollController.php`
+
+**Purpose**: CRUD operations for payroll records.
+
+**Actions**:
+- `index` - List all payrolls
+- `view` - View specific payroll
+- `create` - Create new payroll
+- `update` - Update payroll
+- `delete` - Soft delete payroll
+
+---
+
+## Business Rules
+
+### 1. Payroll Calculation Timing
+
+**Rule**: Payroll is calculated after the month ends, typically between 1st-15th of next month.
+
+**Enforcement**:
+```php
+// Cannot update payroll for previous months after 16th
+if ($dateFromBeginMonth < $dateFromBeginPreviousMonth
+    && $dateCurrent > date("Y-m-16 18:00:00")) {
+    throw new Exception('Payroll editing closed for this month');
+}
+```
+
+### 2. Salary Effective Dates
+
+**Rule**: Salary changes take effect from the specified date, not retroactively.
+
+**Example**:
+- Current salary (Jan 1): 40,000₽
+- New salary (Feb 1): 45,000₽
+- **January payroll**: Uses 40,000₽
+- **February payroll**: Uses 45,000₽
+
+### 3. Daily Payment Constraint
+
+**Rule**: `daily_payment` cannot exceed `monthly_salary`.
+
+**Validation**:
+```php
+if ($this->daily_payment > $this->monthly_salary) {
+    $this->addError('daily_payment', 'Подневная оплата не может быть больше оклада.');
+}
+```
+
+### 4. Bonus Qualification
+
+**Rule**: Bonuses only apply to active work shifts (slot_type_id = 1).
+
+**Implementation**:
+```php
+$timeTable = Timetable::find()
+    ->where(['admin_id' => $adminId])
+    ->andWhere(['slot_type_id' => Timetable::TIMESLOT_WORK]) // Only work shifts
+    ->andWhere(['tabel' => 0]) // Plan, not fact
+    ->all();
+```
+
+### 5. Write-off Penalty
+
+**Rule**: High write-off percentage reduces or eliminates bonus.
+
+**Tiers**:
+- <3%: Maximum bonus
+- 3-5%: Reduced bonus
+- 5-10%: Further reduced
+- 10-20%: Minimal/zero bonus
+- >20%: Penalty (potential negative motivation)
+
+### 6. Team Bonus Distribution
+
+**Rule**: Team bonus pool distributed proportionally to shift count.
+
+**Formula**:
+```
+Team Pool = (Sales * 20%) - (FOT + Write-offs)
+Per-Shift Bonus = Team Pool / Total Shifts
+Employee Bonus = Per-Shift Bonus * Employee's Shifts
+```
+
+### 7. Position-Based Bonus Tiers
+
+**Rule**: Different positions have different bonus structures.
+
+**Hierarchy**:
+1. Administrator: 15,000₽ (highest)
+2. Senior Florist: 10,000₽
+3. Florist: 7,000₽
+4. Assistant: 5,000₽ (lowest)
+
+---
+
+## Examples & Scenarios
+
+### Scenario 1: Simple Florist Payroll
+
+**Context**: Maria, a florist, worked 20 days in January.
+
+**Configuration**:
+```php
+EmployeePayment:
+  admin_id: 456
+  monthly_salary: 45000
+  date: 2025-01-01
+```
+
+**Timetable**:
+- Work days in January (total): 22
+- Maria's actual work days: 20
+
+**Performance**:
+- Write-off ratio: 4%
+- Plan achievement: 96%
+- Quality: 88%
+- No additional bonuses
+
+**Calculation**:
+
+**Stage 1 - Salary**:
+```
+Daily Salary = 45,000 / 22 = 2,045₽/day
+Total Salary = 2,045 * 20 = 40,909₽
+```
+
+**Stage 2 - Bonus** (Florist, 4% write-off):
+```
+Base Bonus = 6,000₽ (4% qualifies for tier 2)
+Coefficient = 1.0 (96% qualifies for 100%)
+Daily Bonus = (6,000 * 1.0) / 22 = 273₽/day
+Total Bonus = 273 * 20 = 5,455₽
+```
+
+**Total Monthly Payroll**:
+```
+Salary:  40,909₽
+Bonus:    5,455₽
+-------------------
+Total:   46,364₽
+```
+
+**Daily Breakdown** (example day):
+```php
+AdminPayrollDays (Jan 15):
+  payroll_constant: 2045
+  payroll_variable: 273
+  payroll_sum: 2318
+```
+
+### Scenario 2: Administrator with Team Bonus
+
+**Context**: Alexey, store administrator, worked 22 days in January.
+
+**Configuration**:
+```php
+EmployeePayment:
+  admin_id: 789
+  monthly_salary: 60000
+  date: 2025-01-01
+```
+
+**Performance**:
+- Write-off ratio: 2.5%
+- Plan achievement: 105%
+- Conversion: 87%
+- Quality: 95%
+- Team bonus pool: 50,000₽
+- Total store shifts: 100
+- Alexey's shifts: 22
+
+**Calculation**:
+
+**Stage 1 - Salary**:
+```
+Daily Salary = 60,000 / 22 = 2,727₽/day
+Total Salary = 2,727 * 22 = 60,000₽
+```
+
+**Stage 2 - Position Bonus** (Administrator, 2.5% write-off):
+```
+Base Bonus = 15,000₽ (<3% qualifies for maximum)
+Coefficient = 1.1 (105% qualifies for +10%)
+Daily Bonus = (15,000 * 1.1) / 22 = 750₽/day
+Total Bonus = 750 * 22 = 16,500₽
+```
+
+**Stage 2 - Team Bonus**:
+```
+Per-Shift Bonus = 50,000 / 100 = 500₽/shift
+Alexey's Team Bonus = 500 * 22 = 11,000₽
+```
+
+**Stage 2 - Quality Bonus**:
+```
+Quality: 95% → 4,000₽
+```
+
+**Stage 2 - Conversion Bonus**:
+```
+Conversion: 87% (Administrator) → 5 game points
+Money: 5 * 10 = 50₽
+```
+
+**Total Monthly Payroll**:
+```
+Salary:           60,000₽
+Position Bonus:   16,500₽
+Team Bonus:       11,000₽
+Quality Bonus:     4,000₽
+Conversion:           50₽
+----------------------------
+Total:            91,550₽
+```
+
+### Scenario 3: Salary Change Mid-Month
+
+**Context**: Ivan's salary increased from 40,000₽ to 50,000₽ on Jan 15.
+
+**Configuration**:
+```php
+// Before Jan 15
+EmployeePayment (id=1):
+  admin_id: 321
+  date: 2024-12-01
+  monthly_salary: 40000
+
+// From Jan 15
+EmployeePayment (id=2):
+  admin_id: 321
+  date: 2025-01-15
+  monthly_salary: 50000
+```
+
+**Work Schedule**:
+- Jan 1-14: 10 days
+- Jan 15-31: 12 days
+- Total: 22 days
+
+**Calculation**:
+
+**Jan 1-14** (old salary):
+```
+Daily Salary = 40,000 / 22 = 1,818₽/day
+Salary for 10 days = 1,818 * 10 = 18,182₽
+```
+
+**Jan 15-31** (new salary):
+```
+Daily Salary = 50,000 / 22 = 2,273₽/day
+Salary for 12 days = 2,273 * 12 = 27,273₽
+```
+
+**Total**:
+```
+Jan 1-14:   18,182₽
+Jan 15-31:  27,273₽
+-------------------
+Total:      45,455₽
+```
+
+**Note**: The system uses the `EmployeePayment::getSalary()` method to automatically select the correct salary config for each day based on effective dates.
+
+### Scenario 4: Part-Time Freelancer
+
+**Context**: Olga, freelance florist, worked only 8 days in January.
+
+**Configuration**:
+```php
+EmployeePayment:
+  admin_id: 555
+  monthly_salary: 0          // No monthly salary
+  daily_payment: 2500        // Fixed daily rate
+```
+
+**Calculation**:
+
+For freelancers with `monthly_salary = 0`, the system uses `daily_payment` directly:
+
+```
+Daily Payment = 2,500₽ (fixed)
+Total Salary = 2,500 * 8 = 20,000₽
+```
+
+**Bonuses**:
+```
+Write-off: 5% → 6,000₽ base
+Coefficient: 1.0
+Daily Bonus = 6,000 / 22 = 273₽
+Total Bonus = 273 * 8 = 2,182₽
+```
+
+**Total**:
+```
+Salary:  20,000₽
+Bonus:    2,182₽
+------------------
+Total:   22,182₽
+```
+
+---
+
+## Related Documentation
+
+- [Bonus System](./bonus.md) - Customer loyalty bonuses (different from employee payroll bonuses)
+- [Timetable Module](./timetable.md) - Work scheduling and shift management
+- [Rating System](./rating.md) - Employee performance evaluation
+- [Database Schema](../database/schema-overview.md) - Complete database documentation
+- [Services Overview](../services/overview.md) - All service classes
+
+---
+
+## Changelog
+
+**2025-01**: Initial comprehensive documentation
+- Documented complete payroll calculation pipeline
+- Added payment stages (Salary, Bonus, Accompany)
+- Documented all performance bonus types
+- Added daily and monthly payroll processing
+- Created example scenarios
+- Documented business rules and access control
+
+---
+
+*Last Updated: January 2025*
+*Maintained by: ERP24 Development Team*
diff --git a/docs/modules/rating.md b/docs/modules/rating.md
new file mode 100644 (file)
index 0000000..51cb61e
--- /dev/null
@@ -0,0 +1,485 @@
+# Rating & Performance Evaluation System
+
+> **Employee performance tracking and evaluation based on sales, quality, and service metrics**
+
+## Overview
+
+The Rating System evaluates employee performance on a daily/monthly basis using quantifiable metrics from sales, timetable, and quality data. Ratings are calculated automatically and used for payroll bonuses, performance reviews, and management decisions.
+
+### Key Features
+
+- **Automated rating calculation** based on sales performance
+- **Multiple rating types** (florist, administrator, etc.)
+- **Daily and monthly aggregation**
+- **Integration with payroll** for performance bonuses
+- **Historical tracking** for performance trends
+- **Store-level comparison** of employee performance
+
+---
+
+## System Architecture
+
+```mermaid
+graph TB
+    subgraph "Data Sources"
+        SALES[Sales Records]
+        TIMETABLE[Timetable Facts]
+        WRITEOFFS[Write-offs]
+        TRAFFIC[Store Traffic]
+    end
+
+    subgraph "Processing Layer"
+        RATING_SERVICE[RatingService]
+        CABINET_SERVICE[CabinetService]
+        BONUS_SERVICE[BonusService]
+    end
+
+    subgraph "Storage"
+        ADMIN_RATING[admin_rating<br/>Performance Scores]
+    end
+
+    subgraph "Outputs"
+        PAYROLL[Payroll Calculation]
+        REPORTS[Performance Reports]
+        DASHBOARD[Dashboard Metrics]
+    end
+
+    SALES --> RATING_SERVICE
+    TIMETABLE --> RATING_SERVICE
+    WRITEOFFS --> RATING_SERVICE
+    TRAFFIC --> CABINET_SERVICE
+
+    CABINET_SERVICE --> RATING_SERVICE
+    BONUS_SERVICE --> RATING_SERVICE
+
+    RATING_SERVICE --> ADMIN_RATING
+
+    ADMIN_RATING --> PAYROLL
+    ADMIN_RATING --> REPORTS
+    ADMIN_RATING --> DASHBOARD
+```
+
+---
+
+## Database Schema
+
+### Table: `admin_rating`
+
+**Purpose**: Stores calculated performance ratings for employees.
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `admin_id` | INTEGER | FK to admin.id (employee) |
+| `rating_id` | INTEGER | Rating type identifier |
+| `rating` | INTEGER | Calculated rating score |
+| `administrators_count` | INTEGER | Number of employees in comparison |
+| `date` | DATE | Rating date |
+| `year` | INTEGER | Year |
+| `month` | INTEGER | Month (1-12) |
+| `value` | DECIMAL | Metric value used for rating |
+| `count_shift` | INTEGER | Number of shifts worked |
+| `avg_value` | DECIMAL | Average value per shift |
+| `date_time` | TIMESTAMP | Calculation timestamp |
+
+**Example Records**:
+```
+admin_id | rating_id | rating | date       | value     | count_shift | avg_value
+---------|-----------|--------|------------|-----------|-------------|----------
+123      | 1         | 85     | 2025-01-15 | 42500.00  | 1           | 42500.00
+123      | 1         | 92     | 2025-01    | 850000.00 | 20          | 42500.00
+```
+
+**Relationships**:
+```php
+public function getAdmin() {
+    return $this->hasOne(Admin::class, ['id' => 'admin_id']);
+}
+
+public function getStore() {
+    return $this->hasOne(CityStore::class, ['id' => 'store_id'])->via('admin');
+}
+```
+
+---
+
+## Rating Types
+
+### 1. Florist Rating (rating_id = 1)
+
+**Based on**:
+- Sales volume per shift
+- Average check amount
+- Product category percentages (matrix, wrapping, services)
+- Quality metrics
+
+**Calculation**:
+```php
+$salesPerShift = $totalSales / $shiftCount;
+$rating = calculateRatingScore($salesPerShift, $avgCheck, $quality);
+```
+
+**Score Range**: 0-100
+
+**Thresholds** (example):
+- 90-100: Excellent
+- 80-89: Good
+- 70-79: Satisfactory
+- Below 70: Needs improvement
+
+### 2. Administrator Rating (rating_id = 2)
+
+**Based on**:
+- Store sales vs plan
+- Write-off percentage
+- Conversion rate
+- Team performance
+
+**Calculation**:
+```php
+$planAchievement = ($actualSales / $planSales) * 100;
+$writeOffPenalty = $writeOffPercent > 5 ? -10 : 0;
+$rating = $planAchievement + $conversionBonus + $writeOffPenalty;
+```
+
+---
+
+## Service Layer
+
+### RatingService
+
+**File**: `erp24/services/RatingService.php`
+
+**Main Method**: `getData($employeeId, $employeeSelect, ..., $dateFrom, $dateTo, ...)`
+
+**Process**:
+1. **Validate employee data**
+   - Check if employee has store assigned
+   - Check if employee has 1C GUID mapping
+   - Check if employee has salary configured
+
+2. **Fetch performance data**
+   - Get sales by employee
+   - Get timetable facts (actual shifts)
+   - Get write-off data for store
+   - Get traffic/conversion metrics
+
+3. **Calculate metrics**
+   - Sales per shift
+   - Average check
+   - Product category percentages
+   - Quality scores
+   - Team performance
+
+4. **Generate rating score**
+   - Apply position-specific algorithms
+   - Compare against thresholds
+   - Calculate rank among peers
+
+5. **Store results**
+   - Save to admin_rating table
+   - Link to date and employee
+
+**Error Handling**:
+
+```php
+// No store assigned
+if (empty($employeeSelectStoreId)) {
+    return ['errorText' => 'У сотрудника не указан магазин'];
+}
+
+// No 1C mapping
+if (!array_key_exists($employeeId, $exportAdmin)) {
+    return ['errorText' => 'В ERP нет данных GUID из 1с'];
+}
+
+// No salary configured
+if (empty($monthlySalary)) {
+    return ['errorText' => 'У сотрудника не указан оклад'];
+}
+
+// No shifts in timetable
+if (empty($timetable)) {
+    return ['errorText' => 'В графике смен не найдено'];
+}
+```
+
+**Key Calculations**:
+
+```php
+// Write-off percentage
+$percentLoss = 0;
+if (!empty($amountWriteOffByStore) && !empty($salesByStore)) {
+    $percentLoss = round(100 * $amountWriteOffByStore / $salesByStore, 1);
+}
+
+// Sales per administrator
+$salaryByAdmin = $this->cabinetService->getSumByAdmin(
+    $adminGuid,
+    $dateFrom,
+    $dateTo,
+    $isAdministrator
+);
+
+// Plan achievement
+$planAchievement = ($salesByStore / $planMonthByStore) * 100;
+```
+
+---
+
+## Rating Calculation Examples
+
+### Example 1: Florist Daily Rating
+
+**Employee**: Maria (Florist)
+**Date**: 2025-01-15
+**Shift**: 10:00-22:00 (12 hours)
+
+**Data**:
+- Sales: 42,500₽
+- Checks: 18
+- Average check: 2,361₽
+- Matrix percentage: 32%
+- Services percentage: 12%
+- Quality score: 4.5/5
+
+**Calculation**:
+```php
+$baseScore = 70; // Starting point
+
+// Sales performance (+10 points if > 40,000)
+if ($sales > 40000) {
+    $baseScore += 10;
+}
+
+// Average check (+5 points if > 2,000)
+if ($avgCheck > 2000) {
+    $baseScore += 5;
+}
+
+// Matrix bouquets (+5 points if > 30%)
+if ($matrixPercent > 30) {
+    $baseScore += 5;
+}
+
+// Quality (+10 points if > 4.0)
+if ($qualityScore > 4.0) {
+    $baseScore += 10;
+}
+
+$rating = min($baseScore, 100); // Cap at 100
+// Result: 70 + 10 + 5 + 5 + 10 = 100
+```
+
+**Stored**:
+```php
+AdminRating:
+  admin_id: 456
+  rating_id: 1
+  rating: 100
+  date: 2025-01-15
+  value: 42500
+  count_shift: 1
+  avg_value: 42500
+```
+
+### Example 2: Administrator Monthly Rating
+
+**Employee**: Alexey (Administrator)
+**Month**: January 2025
+
+**Data**:
+- Store sales: 1,850,000₽
+- Plan: 1,800,000₽
+- Write-offs: 87,000₽ (4.7%)
+- Conversion: 82%
+- Shifts worked: 22
+
+**Calculation**:
+```php
+$baseScore = 0;
+
+// Plan achievement (103%)
+$planAchievement = (1850000 / 1800000) * 100; // 103%
+$baseScore += min($planAchievement, 120); // +103 points
+
+// Write-off penalty (< 5% is good, no penalty)
+if ($writeOffPercent < 5) {
+    $baseScore += 5; // Bonus
+}
+
+// Conversion bonus (> 80% is good)
+if ($conversion > 80) {
+    $baseScore += 5;
+}
+
+$rating = $baseScore; // 103 + 5 + 5 = 113
+```
+
+**Stored**:
+```php
+AdminRating:
+  admin_id: 789
+  rating_id: 2
+  rating: 113
+  date: 2025-01
+  value: 1850000
+  count_shift: 22
+  avg_value: 84091
+```
+
+---
+
+## Access Control
+
+### Viewing Permissions
+
+**Own ratings**: All employees can view their own ratings
+
+**Team ratings**: Managers can view their team's ratings
+
+**All ratings**: HR, Directors can view all ratings
+
+```php
+// Check if user can view rating
+public static function canViewRating($viewerId, $targetEmployeeId)
+{
+    $viewer = Admin::findOne($viewerId);
+
+    // Own rating
+    if ($viewerId == $targetEmployeeId) {
+        return true;
+    }
+
+    // Manager viewing team
+    if ($viewer->isManager()) {
+        return true;
+    }
+
+    // HR/Director viewing all
+    if (in_array($viewer->group_id, [1, 8, 9, 51])) {
+        return true;
+    }
+
+    return false;
+}
+```
+
+---
+
+## Integration with Payroll
+
+**Bonus Calculation**:
+
+```php
+// From BonusService
+$ratingBonus = 0;
+
+if ($rating >= 90) {
+    $ratingBonus = 5000; // Excellent performance
+} elseif ($rating >= 80) {
+    $ratingBonus = 3000; // Good performance
+} elseif ($rating >= 70) {
+    $ratingBonus = 1000; // Satisfactory
+}
+
+$totalPay = $baseSalary + $ratingBonus;
+```
+
+**Monthly Performance Review**:
+
+```php
+$monthlyRating = AdminRating::find()
+    ->where([
+        'admin_id' => $adminId,
+        'year' => 2025,
+        'month' => 1
+    ])
+    ->one();
+
+// Use rating for payroll calculation
+$performanceMultiplier = $monthlyRating->rating / 100;
+$bonus = $baseBonus * $performanceMultiplier;
+```
+
+---
+
+## Controllers
+
+### RatingController
+
+**File**: `erp24/controllers/RatingController.php`
+
+**Actions**:
+- `index` - Rating overview
+- `view` - View specific rating
+- `calculate` - Trigger rating calculation
+- `report` - Rating reports
+
+### Rating2Controller
+
+**Purpose**: Alternative rating views/calculations (possibly legacy or experimental).
+
+---
+
+## Reports & Analytics
+
+### Individual Performance Trend
+
+```php
+$ratingTrend = AdminRating::find()
+    ->select(['date', 'rating', 'value'])
+    ->where(['admin_id' => $adminId])
+    ->andWhere(['>=', 'date', '2024-01-01'])
+    ->orderBy('date')
+    ->all();
+```
+
+### Team Comparison
+
+```php
+$teamRatings = AdminRating::find()
+    ->with('admin')
+    ->where([
+        'year' => 2025,
+        'month' => 1,
+        'rating_id' => 1
+    ])
+    ->orderBy(['rating' => SORT_DESC])
+    ->all();
+```
+
+### Store Performance
+
+```php
+$storeAvgRating = AdminRating::find()
+    ->select(['AVG(rating) as avg_rating'])
+    ->joinWith('admin')
+    ->where(['admin.store_id' => 5])
+    ->andWhere(['year' => 2025, 'month' => 1])
+    ->scalar();
+```
+
+---
+
+## Best Practices
+
+1. **Calculate ratings daily** for timely feedback
+2. **Monthly aggregation** for payroll and reviews
+3. **Transparent criteria** communicated to employees
+4. **Regular recalibration** of thresholds based on business goals
+5. **Combine quantitative and qualitative** metrics
+6. **Link to development plans** for low performers
+
+---
+
+## Related Documentation
+
+- [Payroll Module](./payroll.md) - Rating-based bonuses
+- [Dashboard Module](./dashboard.md) - Performance visualization
+- [Timetable Module](./timetable.md) - Shift tracking
+- [Database Schema](../database/schema-overview.md) - Complete schema
+
+---
+
+*Last Updated: January 2025*
diff --git a/docs/modules/timetable.md b/docs/modules/timetable.md
new file mode 100644 (file)
index 0000000..166e91a
--- /dev/null
@@ -0,0 +1,1331 @@
+# Timetable & Shift Management System
+
+> **Complete documentation of the employee scheduling, shift tracking, and attendance management system in ERP24**
+
+## Table of Contents
+
+- [Overview](#overview)
+- [System Architecture](#system-architecture)
+- [Core Concepts](#core-concepts)
+- [Database Schema](#database-schema)
+- [Plan vs Fact Model](#plan-vs-fact-model)
+- [Shift Types & Schedules](#shift-types--schedules)
+- [Check-in System](#check-in-system)
+- [Business Logic](#business-logic)
+- [Controllers & Actions](#controllers--actions)
+- [Service Layer](#service-layer)
+- [Access Control & Permissions](#access-control--permissions)
+- [Integration Points](#integration-points)
+- [Examples & Scenarios](#examples--scenarios)
+
+---
+
+## Overview
+
+The Timetable System is a sophisticated employee scheduling and attendance tracking platform that manages work schedules (plan), actual work hours (fact), shift assignments, and employee check-ins. It implements a dual-record system that separates planned schedules from actual attendance, enabling precise tracking of late arrivals, early departures, and schedule adherence.
+
+### Key Features
+
+- **Plan vs Fact tracking** - Separate records for scheduled vs actual work
+- **7 slot types** - Work, vacation, sick leave, administrative, internship, weekend, freelance
+- **Shift management** - Day/night shifts with configurable durations
+- **Check-in/check-out system** - GPS-tracked attendance with photo verification
+- **Soft delete** - Logical deletion with history tracking
+- **Conflict detection** - Prevents overlapping shifts
+- **Auto-fact generation** - Automatic fact creation from check-ins
+- **Payroll integration** - Direct integration with payroll calculation
+
+### Statistics
+
+- **3 Main Models**: `Timetable` (base), `TimetablePlan`, `TimetableFact`
+- **2 Supporting Models**: `Shift`, `AdminCheckin`
+- **7 Slot Types**: Work, vacation, administrative, sick leave, internship, weekend, freelance
+- **2 Shift Types**: Day shift (#1), Night shift (#2)
+- **3 Check-in Types**: Start, end, appearance
+
+---
+
+## System Architecture
+
+```mermaid
+graph TB
+    subgraph "Planning Layer"
+        PLAN_UI[Timetable Planning UI]
+        PLAN_ACTION[TimetablePlan Actions]
+    end
+
+    subgraph "Execution Layer"
+        CHECKIN_UI[Mobile Check-in]
+        CHECKIN_ACTION[Check-in Actions]
+    end
+
+    subgraph "Data Layer"
+        TIMETABLE_PLAN[TimetablePlan<br/>Scheduled Work]
+        ADMIN_CHECKIN[AdminCheckin<br/>Clock-in/out Events]
+        TIMETABLE_FACT[TimetableFact<br/>Actual Work]
+        SHIFT[Shift<br/>Shift Templates]
+    end
+
+    subgraph "Integration Layer"
+        PAYROLL[Payroll Service]
+        RATING[Rating Service]
+        CABINET[Cabinet Service]
+    end
+
+    PLAN_UI --> PLAN_ACTION
+    PLAN_ACTION --> TIMETABLE_PLAN
+    TIMETABLE_PLAN --> SHIFT
+
+    CHECKIN_UI --> CHECKIN_ACTION
+    CHECKIN_ACTION --> ADMIN_CHECKIN
+    ADMIN_CHECKIN --> TIMETABLE_PLAN
+
+    TIMETABLE_PLAN --> TIMETABLE_FACT
+    ADMIN_CHECKIN --> TIMETABLE_FACT
+
+    TIMETABLE_FACT --> PAYROLL
+    TIMETABLE_FACT --> RATING
+    TIMETABLE_PLAN --> CABINET
+```
+
+---
+
+## Core Concepts
+
+### 1. Plan (График)
+
+**Planned work schedule** created in advance (usually for the month).
+
+**Characteristics**:
+- Created by administrators/managers
+- Defines intended work schedule
+- Can be edited before fact is created
+- Serves as baseline for comparison
+
+**Example**:
+```
+Employee: Maria
+Date: 2025-01-15
+Shift: Day (10:00-22:00)
+Store: Store #5
+Type: Work
+```
+
+### 2. Fact (Табель)
+
+**Actual work performed** based on check-ins or manual entry.
+
+**Characteristics**:
+- Created after work is done
+- Reflects real attendance
+- Cannot modify plan once fact exists
+- Used for payroll calculation
+
+**Example**:
+```
+Employee: Maria
+Date: 2025-01-15
+Actual: 10:15-21:50 (late start, early leave)
+Based on plan_id: 12345
+Work time: 11.58 hours (instead of 12)
+```
+
+### 3. Check-in (Явка)
+
+**Timestamped attendance events** captured via mobile app or web interface.
+
+**Types**:
+1. **Start** (TYPE_START = 1) - Shift opening
+2. **End** (TYPE_END = 2) - Shift closing
+3. **Appear** (TYPE_APPEAR = 3) - Mid-shift appearance
+
+**Characteristics**:
+- GPS coordinates captured
+- Photo optional
+- Mood/rating optional (1-5 scale)
+- Links to plan via plan_id
+
+---
+
+## Database Schema
+
+### Table: `timetable` (Base Table)
+
+**File**: `erp24/records/Timetable.php`
+
+**Purpose**: Base table for both plan and fact records (single table inheritance).
+
+**Schema**:
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `tabel` | INTEGER | Record type: 0=Plan, 1=Fact, 2=Fact New |
+| `admin_id` | INTEGER | FK to admin.id (employee) |
+| `admin_group_id` | INTEGER | Position group ID |
+| `store_id` | INTEGER | FK to city_store.id |
+| `shift_id` | INTEGER | FK to timetable_shift.id |
+| `date` | DATE | Work date (YYYY-MM-DD) |
+| `datetime_start` | TIMESTAMP | Shift start date & time |
+| `datetime_end` | TIMESTAMP | Shift end date & time |
+| `time_start` | TIME | Shift start time (HH:MM:SS) |
+| `time_end` | TIME | Shift end time (HH:MM:SS) |
+| `work_time` | DECIMAL | Work hours (decimal) |
+| `salary_shift` | INTEGER | Daily salary for shift |
+| `slot_type_id` | INTEGER | Slot type (1-7) |
+| `d_id` | INTEGER | Position being worked |
+| `admin_id_add` | INTEGER | FK to admin.id (creator) |
+| `comment` | TEXT | Notes |
+| `status` | INTEGER | 0=Pending, 1=Verified |
+| `date_add` | TIMESTAMP | Creation timestamp |
+| `active` | INTEGER | Soft delete flag (1=active, 0=deleted) |
+| `deleted_at` | TIMESTAMP | Deletion timestamp |
+| `deleted_by` | INTEGER | FK to admin.id (who deleted) |
+
+**Constants**:
+
+```php
+// Record types
+const TABLE_PLAN = 0;
+const TABLE_FACT = 1;
+const TABLE_FACT_NEW = 2;
+
+// Slot types
+const TIMESLOT_WORK = 1;
+const TIMESLOT_VACATION = 2;
+const TIMESLOT_ADMINISTRATIVE = 3;
+const TIMESLOT_SICK_LEAVE = 4;
+const TIMESLOT_INTERNSHIP = 5;
+const TIMESLOT_WEEKEND = 6;
+const TIMESLOT_FREELANCE = 7;
+
+// Status
+const STATUS_PENDING = 0;
+const STATUS_VERIFIED = 1;
+```
+
+**Traits**:
+```php
+use SoftDeleteTrait;  // Provides soft delete functionality
+```
+
+**Single Table Inheritance**:
+```php
+public static function instantiate($row): self
+{
+    if ($row['tabel'] == self::TABLE_PLAN) {
+        return new TimetablePlan();
+    }
+    if ($row['tabel'] == self::TABLE_FACT) {
+        return new TimetableFact();
+    }
+    throw new \Exception('Unknown timetable type');
+}
+```
+
+**Relationships**:
+```php
+public function getShift() {
+    return $this->hasOne(Shift::class, ['id' => 'shift_id']);
+}
+
+public function getStore() {
+    return $this->hasOne(CityStore::class, ['id' => 'store_id']);
+}
+
+public function getAdmin() {
+    return $this->hasOne(Admin::class, ['id' => 'admin_id']);
+}
+
+public function getPosition() {
+    return $this->hasOne(AdminGroup::class, ['id' => 'admin_group_id']);
+}
+```
+
+### Model: `TimetablePlan` (Planned Schedule)
+
+**File**: `erp24/records/TimetablePlan.php`
+
+**Purpose**: Planned work schedule created in advance.
+
+**Extends**: `Timetable` (with `tabel = 0`)
+
+**Additional Rules**:
+```php
+public function rules()
+{
+    return array_merge([
+        [['tabel'], 'default', 'value' => self::TABLE_PLAN],
+        [['work_time'], 'number', 'min' => 1, 'max' => 24],
+        [['time_end'], function () {
+            // Validate shift duration >= 1 hour
+            $start = new \DateTime($this->datetime_start);
+            $end = new \DateTime($this->datetime_end);
+            if ($start->diff($end)->h < 1) {
+                $this->addError('datetime_end', 'Смена длится меньше часа');
+            }
+
+            // Cannot edit plan if fact already exists
+            if ($this->id) {
+                $fact = TimetableFact::find()->andWhere(['plan_id' => $this->id])->one();
+                if ($fact) {
+                    $this->addError('fact', 'Нельзя редактировать смену, для которой уже создан факт');
+                }
+            }
+        }],
+    ], parent::rules());
+}
+```
+
+**Query Scope**:
+```php
+public static function find()
+{
+    return parent::find()->andWhere(['tabel' => self::TABLE_PLAN]);
+}
+```
+
+**Relationships**:
+```php
+public function getFact() {
+    return $this->hasOne(TimetableFact::class, ['plan_id' => 'id']);
+}
+
+public function getCheckins() {
+    return $this->hasMany(AdminCheckin::class, ['plan_id' => 'id']);
+}
+```
+
+**Auto-Fact Generation**:
+```php
+public function makeFact()
+{
+    $checkins = $this->checkins;
+
+    if (count($checkins) === 0) {
+        // No check-ins = absent
+        return new TimetableFact([
+            'plan_id' => $this->id,
+            'datetime_start' => $this->datetime_end,
+            'datetime_end' => $this->datetime_end,
+            'work_time' => 0,
+            'comment' => 'fakt checkins === 0',
+        ] + $this->toArray());
+    }
+
+    // Use first and last check-in
+    $firstCheckin = reset($checkins);
+    $lastCheckin = end($checkins);
+
+    $boundStart = $this->bound($firstCheckin->dateTime());
+    $boundEnd = $this->bound($lastCheckin->dateTime());
+
+    return new TimetableFact([
+        'plan_id' => $this->id,
+        'admin_id' => $firstCheckin->admin_id,
+        'datetime_start' => $boundStart->format('Y-m-d H:i:s'),
+        'datetime_end' => $boundEnd->format('Y-m-d H:i:s'),
+        'comment' => 'fakt checkins != 0',
+    ] + $this->toArray());
+}
+
+public function bound(\DateTime $date): \DateTime
+{
+    // Constrain check-in time to plan boundaries
+    if ($date > $this->getDateTimeEnd()) {
+        return $this->getDateTimeEnd();
+    }
+    if ($date < $this->getDateTimeStart()) {
+        return $this->getDateTimeStart();
+    }
+    return $date;
+}
+```
+
+### Model: `TimetableFact` (Actual Work)
+
+**File**: `erp24/records/TimetableFact.php`
+
+**Purpose**: Actual work performed based on check-ins.
+
+**Extends**: `Timetable` (with `tabel = 1`)
+
+**Additional Fields**:
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `plan_id` | INTEGER | FK to timetable.id (plan record) |
+
+**Additional Rules**:
+```php
+public function rules()
+{
+    return array_merge([
+        [['plan_id'], 'exist', 'targetClass' => TimetablePlan::class, 'targetAttribute' => 'id'],
+        [['tabel'], 'default', 'value' => self::TABLE_FACT],
+        [['datetime_start', 'datetime_end'], function () {
+            $now = new \DateTime();
+            $datetimeStart = new \DateTime($this->datetime_start);
+            $datetimeEnd = new \DateTime($this->datetime_end);
+
+            // Cannot create fact for future shifts
+            if ($datetimeStart > $now) {
+                $this->addError('datetime_start', 'Смена ещё не началась');
+            }
+            if ($datetimeEnd > $now) {
+                $this->addError('datetime_start', 'Смена ещё не закончилась');
+            }
+        }]
+    ], parent::rules());
+}
+```
+
+**Query Scope**:
+```php
+public static function find()
+{
+    return parent::find()->andWhere(['tabel' => self::TABLE_FACT]);
+}
+```
+
+**Soft Delete**:
+```php
+public static function hasSoftDelete(): bool
+{
+    return false;  // Facts are never soft-deleted
+}
+```
+
+**Absence Detection**:
+```php
+public function isAbsent()
+{
+    return $this->datetime_start === $this->datetime_end;
+}
+```
+
+**Relationships**:
+```php
+public function getPlan() {
+    return $this->hasOne(TimetablePlan::class, ['id' => 'plan_id']);
+}
+```
+
+**Variance Calculation**:
+```php
+public function getSkippedHours()
+{
+    return $this->plan->work_time - $this->work_time;
+}
+
+public function getLate()
+{
+    // How late employee arrived
+    return $this->getDateTimeStart()->diff($this->plan->getDateTimeStart());
+}
+
+public function getEarly()
+{
+    // How early employee left
+    return $this->getDateTimeEnd()->diff($this->plan->getDateTimeEnd());
+}
+```
+
+### Table: `timetable_shift` (Shift Templates)
+
+**File**: `erp24/records/Shift.php`
+
+**Purpose**: Predefined shift templates (e.g., "Day 10:00-22:00", "Night 22:00-10:00").
+
+**Schema**:
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `name` | VARCHAR | Full shift name |
+| `short_name` | VARCHAR | Short name (e.g., "День", "Ночь") |
+| `start_time` | TIME | Shift start time (HH:MM:SS) |
+| `duration` | DECIMAL | Shift duration (hours, decimal) |
+| `work_time` | DECIMAL | Actual work hours (excluding breaks) |
+| `end_time` | TIME | Shift end time (calculated) |
+
+**Constants**:
+```php
+const DAY = 1;
+const NIGHT = 2;
+```
+
+**Example Records**:
+```
+id | name        | short_name | start_time | duration | work_time | end_time
+---|-------------|------------|------------|----------|-----------|----------
+1  | День        | День       | 10:00:00   | 12.0     | 12.0      | 22:00:00
+2  | Ночь        | Ночь       | 22:00:00   | 12.0     | 12.0      | 10:00:00
+3  | Утро        | Утро       | 08:00:00   | 8.0      | 8.0       | 16:00:00
+```
+
+**Calculated Fields**:
+```php
+public function getDurationInterval()
+{
+    $hours = (int) $this->duration;
+    $minutes = (int) (60 * ($this->duration - floor($this->duration)));
+    return new \DateInterval('PT' . $hours . 'H' . $minutes . 'M');
+}
+
+public function getHours(): array
+{
+    // Get all hours covered by shift
+    $startHour = (int) substr($this->start_time, 0, 2);
+    for ($delta = 0; $delta <= $this->duration; $delta++) {
+        $hours[] = ($startHour + $delta) % 24;
+    }
+    return $hours;
+}
+```
+
+**Helpers**:
+```php
+public static function isNight($shiftId): bool
+{
+    return Shift::NIGHT === (int) $shiftId;
+}
+```
+
+### Table: `admin_checkin` (Check-in Events)
+
+**File**: `erp24/records/AdminCheckin.php`
+
+**Purpose**: Clock-in/clock-out events with GPS and photo tracking.
+
+**Schema**:
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | INTEGER | Primary key |
+| `admin_id` | INTEGER | FK to admin.id (employee) |
+| `replaced_admin_id` | INTEGER | FK to admin.id (replacement) |
+| `type_id` | INTEGER | 1=Start, 2=End, 3=Appear |
+| `date` | DATE | Check-in date |
+| `time` | TIMESTAMP | Check-in timestamp |
+| `store_id` | INTEGER | FK to city_store.id |
+| `d_id` | INTEGER | Position ID |
+| `plan_id` | INTEGER | FK to timetable.id (plan) |
+| `device_id` | INTEGER | Device identifier |
+| `lat` | DECIMAL | GPS latitude |
+| `lon` | DECIMAL | GPS longitude |
+| `ball` | INTEGER | Mood/rating (1-5) |
+| `comment` | TEXT | Optional notes |
+| `photo` | VARCHAR | Photo filename |
+| `status` | INTEGER | 0=Pending, 1=Verified |
+
+**Constants**:
+```php
+const TYPE_START = 1;
+const TYPE_END = 2;
+const TYPE_APPEAR = 3;
+
+const STATUS_PENDING = 0;
+const STATUS_VERIFIED = 1;
+```
+
+**Type Helpers**:
+```php
+public function isStart()
+{
+    return $this->type_id == self::TYPE_START;
+}
+
+public function isEnd()
+{
+    return $this->type_id == self::TYPE_END;
+}
+
+public function checkinType()
+{
+    return [
+        self::TYPE_START => 'Открытие смены',
+        self::TYPE_END => 'Закрытие смены',
+        self::TYPE_APPEAR => 'Появление',
+    ];
+}
+```
+
+**Relationships**:
+```php
+public function getPlan() {
+    return $this->hasOne(TimetablePlan::class, ['id' => 'plan_id']);
+}
+
+public function getUser() {
+    return $this->hasOne(Admin::class, ['id' => 'admin_id']);
+}
+
+public function getStore() {
+    return $this->hasOne(CityStore::class, ['id' => 'store_id']);
+}
+```
+
+---
+
+## Plan vs Fact Model
+
+### Lifecycle
+
+```mermaid
+sequenceDiagram
+    participant Manager
+    participant PlanUI as Plan UI
+    participant Plan as TimetablePlan
+    participant Employee
+    participant Mobile as Mobile App
+    participant Checkin as AdminCheckin
+    participant Fact as TimetableFact
+
+    Manager->>PlanUI: Create monthly schedule
+    PlanUI->>Plan: Save plan records
+    Plan->>Plan: tabel = 0
+
+    Note over Employee,Mobile: On shift day
+
+    Employee->>Mobile: Start shift
+    Mobile->>Checkin: Create TYPE_START check-in
+    Checkin->>Checkin: Save GPS, photo, time
+    Checkin->>Plan: Link via plan_id
+
+    Employee->>Mobile: End shift
+    Mobile->>Checkin: Create TYPE_END check-in
+
+    Note over Plan,Fact: End of day or period
+
+    Plan->>Checkin: Get all check-ins
+    Plan->>Fact: makeFact()
+    Fact->>Fact: tabel = 1
+    Fact->>Fact: datetime_start = first check-in
+    Fact->>Fact: datetime_end = last check-in
+    Fact->>Fact: plan_id = plan.id
+```
+
+### Comparison Logic
+
+**Planned vs Actual**:
+
+```php
+// Plan
+$plan = TimetablePlan::findOne($planId);
+$plan->datetime_start = '2025-01-15 10:00:00';
+$plan->datetime_end = '2025-01-15 22:00:00';
+$plan->work_time = 12.0;
+
+// Fact (created from check-ins)
+$fact = $plan->fact;
+$fact->datetime_start = '2025-01-15 10:15:00';  // 15 min late
+$fact->datetime_end = '2025-01-15 21:50:00';    // 10 min early
+$fact->work_time = 11.58;  // 0.42 hours short
+
+// Calculate variance
+$late = $fact->late;                // DateInterval: 0h 15m
+$early = $fact->early;              // DateInterval: 0h 10m
+$skippedHours = $fact->skippedHours; // 0.42 hours
+```
+
+### Editing Rules
+
+**Plan Editing**:
+- ✅ Can edit before fact is created
+- ❌ Cannot edit after fact is created
+- ✅ Can soft-delete (sets active=0)
+
+**Fact Editing**:
+- ❌ Cannot edit once created
+- ❌ Cannot soft-delete (permanent records)
+- ✅ Can create manually if no check-ins
+
+**Code Enforcement**:
+```php
+// In TimetablePlan::rules()
+[['time_end'], function () {
+    if ($this->id) {
+        $fact = TimetableFact::find()->andWhere(['plan_id' => $this->id])->one();
+        if ($fact) {
+            $this->addError('fact', 'Нельзя редактировать смену, для которой уже создан факт');
+        }
+    }
+}]
+```
+
+---
+
+## Shift Types & Schedules
+
+### Slot Types
+
+**7 Types of Time Slots**:
+
+| ID | Type | Letter | Description | Paid? | CSS Class |
+|----|------|--------|-------------|-------|-----------|
+| 1 | Работа (Work) | Р | Regular work shift | ✅ | bg-success (green) |
+| 2 | Отпуск (Vacation) | О | Paid vacation | ✅ | bg-info (blue) |
+| 3 | Административный (Administrative) | А | Administrative leave | ✅ | bg-danger (red) |
+| 4 | Больничный (Sick Leave) | Б | Sick leave | ✅ | bg-danger (red) |
+| 5 | Стажировка (Internship) | С | Unpaid internship (2 shifts) | ❌ | bg-warning (yellow) |
+| 6 | Выходной (Weekend) | В | Day off | ❌ | bg-danger (red) |
+| 7 | Подработчик (Freelance) | П | Freelancer | ✅ | (not used) |
+
+**Constants**:
+```php
+const TIMESLOT_WORK = 1;
+const TIMESLOT_VACATION = 2;
+const TIMESLOT_ADMINISTRATIVE = 3;
+const TIMESLOT_SICK_LEAVE = 4;
+const TIMESLOT_INTERNSHIP = 5;
+const TIMESLOT_WEEKEND = 6;
+const TIMESLOT_FREELANCE = 7;
+```
+
+**Work Slot Check**:
+```php
+public function isWorkSlot()
+{
+    return $this->slot_type_id === Timetable::TIMESLOT_WORK
+        || $this->slot_type_id === Timetable::TIMESLOT_INTERNSHIP;
+}
+```
+
+### Work Schedules
+
+**Common Schedules** (via `admin.work_rate`):
+
+| work_rate | Schedule | Description |
+|-----------|----------|-------------|
+| 1 | 5/2 | 5 days work, 2 days off (typical office) |
+| 2 | 2/2 | 2 days work, 2 days off (florists) |
+| 3 | 3/3 | 3 days work, 3 days off |
+
+**Monthly Work Days** (typical):
+- 5/2: 20-23 days/month
+- 2/2: 14-16 days/month
+- 3/3: varies
+
+---
+
+## Check-in System
+
+### Check-in Flow
+
+**1. Starting a Shift**:
+
+```php
+$checkin = new AdminCheckin();
+$checkin->admin_id = 123;
+$checkin->type_id = AdminCheckin::TYPE_START;
+$checkin->date = '2025-01-15';
+$checkin->time = '2025-01-15 10:15:00';
+$checkin->store_id = 5;
+$checkin->d_id = 30; // Position: Florist
+$checkin->lat = 55.7558;
+$checkin->lon = 37.6173;
+$checkin->ball = 4; // Mood: Good
+$checkin->photo = 'checkin_123_20250115_101500.jpg';
+$checkin->plan_id = 456; // Link to plan
+$checkin->device_id = 789;
+$checkin->save();
+```
+
+**2. Ending a Shift**:
+
+```php
+$checkin = new AdminCheckin();
+$checkin->admin_id = 123;
+$checkin->type_id = AdminCheckin::TYPE_END;
+$checkin->date = '2025-01-15';
+$checkin->time = '2025-01-15 21:50:00';
+$checkin->store_id = 5;
+$checkin->lat = 55.7558;
+$checkin->lon = 37.6173;
+$checkin->plan_id = 456;
+$checkin->device_id = 789;
+$checkin->save();
+```
+
+**3. Auto-Fact Creation**:
+
+```php
+$plan = TimetablePlan::findOne(456);
+$fact = $plan->makeFact();
+$fact->save();
+
+// Result:
+// fact->datetime_start = '2025-01-15 10:15:00' (first check-in)
+// fact->datetime_end = '2025-01-15 21:50:00' (last check-in)
+// fact->work_time = 11.58 hours
+```
+
+### GPS Verification
+
+**Tolerance** (typical implementation):
+- Store radius: ~100-500 meters
+- GPS accuracy: ±10-50 meters
+- Validation occurs on mobile app
+
+**Storage**:
+- Latitude: `lat` (DECIMAL)
+- Longitude: `lon` (DECIMAL)
+- Coordinates stored with each check-in
+
+### Mood/Rating
+
+**Scale**: 1-5
+
+| Value | Meaning |
+|-------|---------|
+| 1 | Very bad mood |
+| 2 | Bad mood |
+| 3 | Neutral |
+| 4 | Good mood |
+| 5 | Excellent mood |
+
+**Usage**: Analytics, identifying employee satisfaction trends.
+
+---
+
+## Business Logic
+
+### 1. Shift Editing Rules
+
+**Access Control**:
+
+```php
+public static function getAllowEditShift($slotDate, $days = 5): bool
+{
+    return (
+        // Future shifts
+        date('Y-m-d', strtotime($slotDate)) >= date('Y-m-d')
+        ||
+        // Current month shifts
+        date('n', strtotime($slotDate)) == date('n')
+        ||
+        // Previous month shifts (first 5 days only)
+        (
+            date('n', strtotime($slotDate)) == date('n', strtotime("-1 month"))
+            &&
+            date('j') <= $days
+        )
+    );
+}
+```
+
+**Admin Permissions**:
+
+```php
+public static function getCountDaysAllowEditShift($groupId): int
+{
+    $numDay = 5;  // Default: 5 days
+
+    $accessArray = [
+        1,  // Директор
+        7,  // Кустовые директора
+        8,  // Руководитель HR
+        9,  // Главный бухгалтер
+        20, // Администратор платформы (HR)
+        51, // Операционный директор
+    ];
+
+    if (in_array($groupId, $accessArray)) {
+        $numDay = 13;  // Extended: 13 days
+    }
+
+    return $numDay;
+}
+```
+
+**Rules**:
+- Regular users: Can edit previous month shifts for first 5 days of current month
+- Managers/HR: Can edit previous month shifts for first 13 days
+
+### 2. Conflict Detection
+
+**Overlapping Shifts**:
+
+```php
+// Check for conflicting shifts
+$conflict = Timetable::find()
+    ->andWhere(['admin_id' => $adminId])
+    ->andWhere(['date' => $date])
+    ->andWhere(['active' => 1])
+    ->andWhere(['tabel' => Timetable::TABLE_PLAN])
+    ->exists();
+
+if ($conflict) {
+    throw new \Exception('Shift already exists for this date');
+}
+```
+
+### 3. Plan-Checkin Linking
+
+**Auto-Link on Check-in**:
+
+```php
+// Find matching plan when checking in
+$plan = TimetablePlan::find()
+    ->andWhere(['admin_id' => $adminId])
+    ->andWhere(['date' => $date])
+    ->andWhere(['store_id' => $storeId])
+    ->one();
+
+if ($plan) {
+    $checkin->plan_id = $plan->id;
+}
+```
+
+**Manual Linking** (for orphaned check-ins):
+
+```php
+// From TimetableController::actionLinkPlans()
+$factsBatch = TimetableFact::find()
+    ->with('plan')
+    ->andWhere('plan_id IS NULL')
+    ->batch();
+
+foreach ($factsBatch as $facts) {
+    $plans = TimetablePlan::find()
+        ->andWhere(['IN', ['admin_id', 'date'], $tuples])
+        ->all();
+
+    foreach ($plans as $key => $plan) {
+        $fact = $facts[$key];
+        $fact->updateAttributes(['plan_id' => $plan->id]);
+    }
+}
+```
+
+### 4. Absence Handling
+
+**Zero-Hour Fact for Absence**:
+
+```php
+// From TimetablePlan::makeFact()
+if (count($checkins) === 0) {
+    return new TimetableFact([
+        'plan_id' => $this->id,
+        'datetime_start' => $this->datetime_end,  // Same time
+        'datetime_end' => $this->datetime_end,    // = absence
+        'work_time' => 0,
+        'comment' => 'fakt checkins === 0',
+    ] + $this->toArray());
+}
+```
+
+**Absence Detection**:
+
+```php
+public function isAbsent()
+{
+    return $this->datetime_start === $this->datetime_end;
+}
+```
+
+---
+
+## Controllers & Actions
+
+### TimetableController
+
+**File**: `erp24/controllers/TimetableController.php`
+
+**Actions** (using Action pattern):
+
+| Action | Class | Purpose |
+|--------|-------|---------|
+| `plan` | `PlanAction` | View/edit planned schedule |
+| `edit_plan` | `EditPlanAction` | Edit specific plan record |
+| `add` | `AddAction` | Add new plan shift |
+| `fact` | `FactAction` | View actual work facts |
+| `fact_overview` | `FactOverviewAction` | Fact summary report |
+| `edit_fact` | `EditFactAction` | Edit fact record |
+| `fact-overview-item` | `FactOverviewItemAction` | Single fact details |
+| `start` | `StartAction` | Start shift (check-in) |
+| `start-shift-step-one` | `StartShiftStepOneAction` | Check-in step 1 |
+| `start-shift-step-two` | `StartShiftStepTwoAction` | Check-in step 2 |
+| `start-shift-step-three` | `StartShiftStepThreeAction` | Check-in step 3 |
+| `checkins` | `CheckinsAction` | View all check-ins |
+| `holidays` | `HolidaysAction` | Manage holidays |
+| `admin_stores` | `AdminStores` | Employee store assignments |
+
+**Utility Actions**:
+
+| Action | Purpose |
+|--------|---------|
+| `link-plans` | Link orphaned facts/check-ins to plans |
+| `unlink-checkins` | Unlink check-ins with mismatched employees |
+| `join-missing-shifts-with-checkins` | Auto-create facts from unlinked check-ins |
+| `add-fact-hand` | Manually add fact (no check-ins) |
+
+**URL Examples**:
+- `/timetable/plan?date=2025-01` - January plan view
+- `/timetable/fact?date=2025-01-15` - Facts for Jan 15
+- `/timetable/add?admin_id=123&date=2025-01-15` - Add shift
+- `/timetable/checkins?admin_id=123&date=2025-01-15` - View check-ins
+- `/timetable/link-plans` - Auto-link orphaned records
+
+### TimetableFactController
+
+**File**: `erp24/controllers/TimetableFactController.php`
+
+**Purpose**: Dedicated controller for fact-related operations.
+
+---
+
+## Service Layer
+
+### TimetableService
+
+**File**: `erp24/services/TimetableService.php`
+
+**Methods**:
+
+#### 1. Get Timetable
+```php
+public static function getTimetable($date1, $date2_smen): array
+{
+    return Timetable::find()
+        ->select(['admin_id', 'd_id', 'store_id', 'slot_type_id', 'date', 'shift_id'])
+        ->andWhere(['tabel' => 0])  // Plan only
+        ->andWhere(['>=', 'datetime_start', $date1])
+        ->andWhere(['<=', 'datetime_end', $date2])
+        ->andWhere(['slot_type_id' => [
+            Timetable::TIMESLOT_WORK,
+            Timetable::TIMESLOT_INTERNSHIP,
+            8  // Additional type
+        ]])
+        ->orderBy(['d_id' => SORT_ASC, 'shift_id' => SORT_ASC])
+        ->asArray()
+        ->all();
+}
+```
+
+#### 2. Get Allowed Stores
+```php
+public static function getAllowedStoreId($adminId, $groupId): array
+{
+    $admin = Admin::findOne($adminId);
+    $storeIds = explode(',', $admin->store_arr);
+
+    // Special case: single-store restriction groups
+    if (in_array($groupId, Admin::ADMIN_WRITE_OFFS_SINGLE_STORE_GROUP_IDS)) {
+        $timeTable = Timetable::find()
+            ->andWhere(['admin_id' => $adminId])
+            ->andWhere(['date' => date("Y-m-d")])
+            ->andWhere(['slot_type_id' => Timetable::TIMESLOT_WORK])
+            ->andWhere(['tabel' => 0])
+            ->one();
+
+        if ($timeTable) {
+            $storeIds = [$timeTable->store_id];
+        }
+    }
+
+    return $storeIds;
+}
+```
+
+---
+
+## Access Control & Permissions
+
+### Editing Permissions
+
+**By Time Period**:
+
+| User Role | Can Edit Current Month | Can Edit Previous Month |
+|-----------|------------------------|-------------------------|
+| Regular | ✅ Until end of month | ✅ First 5 days only |
+| Manager/HR | ✅ Until end of month | ✅ First 13 days only |
+| Past months | ❌ Locked | ❌ Locked |
+
+**By Record State**:
+
+| Record Type | Create | Update | Delete | Conditions |
+|-------------|--------|--------|--------|------------|
+| Plan (no fact) | ✅ | ✅ | ✅ (soft) | Within time window |
+| Plan (has fact) | N/A | ❌ | ❌ | Immutable |
+| Fact | ✅ | ❌ | ❌ | Permanent record |
+| Check-in | ✅ | ❌ | ❌ | Permanent record |
+
+**Privileged Groups**:
+
+```php
+$accessArray = [
+    1,  // Директор
+    7,  // Кустовые директора
+    8,  // Руководитель HR
+    9,  // Главный бухгалтер
+    20, // Администратор платформы (HR)
+    51, // Операционный директор
+];
+```
+
+### Store Restrictions
+
+**Multi-Store Access**:
+- Stored in `admin.store_arr` (comma-separated IDs)
+- Example: `"5,7,12"` = access to stores 5, 7, 12
+
+**Single-Store Restriction**:
+- Some positions limited to current shift store only
+- Determined by `Admin::ADMIN_WRITE_OFFS_SINGLE_STORE_GROUP_IDS`
+
+---
+
+## Integration Points
+
+### 1. Payroll System
+
+**Integration**: `AdminPayrollDaysService` uses timetable for salary calculation.
+
+```php
+// From AdminPayrollDaysService
+$slotTypeId = [Timetable::TIMESLOT_WORK];
+
+$timeTable = Timetable::find()
+    ->andWhere(['>=', 'date', $dateFrom])
+    ->andWhere(['<=', 'date', $dateTo])
+    ->andWhere(['admin_id' => $adminId])
+    ->andWhere(['slot_type_id' => $slotTypeId])
+    ->andWhere(['tabel' => 0])  // Use plan
+    ->all();
+
+$workDays = count($timeTable);
+```
+
+**Fact Usage**:
+- Actual work hours from `TimetableFact.work_time`
+- Late/early penalties calculated from variance
+- Absence detection via `isAbsent()`
+
+### 2. Rating System
+
+**Integration**: Performance metrics tracked per shift.
+
+```php
+// Link rating to specific shift
+$rating = new AdminRating();
+$rating->admin_id = $adminId;
+$rating->date = $date;
+$rating->timetable_id = $timetableId;  // Link to fact
+```
+
+### 3. Cabinet Service
+
+**Integration**: Dashboard metrics pulled from timetable.
+
+```php
+// Get working employees for date range
+$timetableData = (new CabinetService())->getTimetableDataList($dateFrom, $dateTo);
+$activeEmployees = ArrayHelper::getColumn($timetableData, 'admin_id');
+```
+
+### 4. Mobile App
+
+**Check-in API**:
+- Start shift: POST to API with GPS coordinates
+- End shift: POST with final GPS coordinates
+- Photo upload optional
+
+**Endpoints**:
+- `/timetable/start-shift-step-one` - Select shift
+- `/timetable/start-shift-step-two` - Enter details
+- `/timetable/start-shift-step-three` - Confirm and save
+
+---
+
+## Examples & Scenarios
+
+### Scenario 1: Creating Monthly Schedule
+
+**Context**: HR manager creates January schedule for florist Maria.
+
+**Process**:
+
+```php
+// Create plan for January 1-31
+$dates = ['2025-01-01', '2025-01-03', '2025-01-05', ...]; // 2/2 schedule
+
+foreach ($dates as $date) {
+    $plan = new TimetablePlan();
+    $plan->admin_id = 456; // Maria
+    $plan->admin_group_id = 30; // Florist
+    $plan->store_id = 5;
+    $plan->shift_id = 1; // Day shift
+    $plan->date = $date;
+    $plan->datetime_start = $date . ' 10:00:00';
+    $plan->datetime_end = $date . ' 22:00:00';
+    $plan->time_start = '10:00:00';
+    $plan->time_end = '22:00:00';
+    $plan->work_time = 12.0;
+    $plan->slot_type_id = Timetable::TIMESLOT_WORK;
+    $plan->admin_id_add = $_SESSION['admin_id'];
+    $plan->save();
+}
+```
+
+**Result**: 15-16 plan records created for January (2/2 schedule).
+
+### Scenario 2: Employee Check-in/Check-out
+
+**Context**: Maria works her shift on January 15, arriving slightly late.
+
+**Flow**:
+
+**Morning Check-in** (10:15 AM):
+```php
+$checkin = new AdminCheckin();
+$checkin->admin_id = 456;
+$checkin->type_id = AdminCheckin::TYPE_START;
+$checkin->date = '2025-01-15';
+$checkin->time = '2025-01-15 10:15:00';  // 15 min late
+$checkin->store_id = 5;
+$checkin->lat = 55.7558;
+$checkin->lon = 37.6173;
+$checkin->ball = 4; // Good mood
+$checkin->plan_id = 12345; // Auto-linked
+$checkin->device_id = 789;
+$checkin->save();
+```
+
+**Evening Check-out** (9:50 PM):
+```php
+$checkin = new AdminCheckin();
+$checkin->admin_id = 456;
+$checkin->type_id = AdminCheckin::TYPE_END;
+$checkin->date = '2025-01-15';
+$checkin->time = '2025-01-15 21:50:00';  // 10 min early
+$checkin->store_id = 5;
+$checkin->lat = 55.7560;
+$checkin->lon = 37.6175;
+$checkin->plan_id = 12345;
+$checkin->device_id = 789;
+$checkin->save();
+```
+
+**Auto-Fact Creation** (end of day):
+```php
+$plan = TimetablePlan::findOne(12345);
+$fact = $plan->makeFact();
+$fact->save();
+
+// Result:
+// fact->datetime_start = '2025-01-15 10:15:00'
+// fact->datetime_end = '2025-01-15 21:50:00'
+// fact->work_time = 11.58 hours
+// fact->late = 15 minutes
+// fact->early = 10 minutes
+// fact->skippedHours = 0.42 hours
+```
+
+### Scenario 3: Employee Absence
+
+**Context**: Maria scheduled for January 16 but doesn't show up.
+
+**Plan**:
+```php
+$plan = TimetablePlan::findOne(['admin_id' => 456, 'date' => '2025-01-16']);
+// Exists: planned for 10:00-22:00
+```
+
+**Check-ins**: None
+
+**Fact Creation**:
+```php
+$fact = $plan->makeFact();
+// datetime_start = '2025-01-16 22:00:00'
+// datetime_end = '2025-01-16 22:00:00'
+// work_time = 0
+// isAbsent() = true
+```
+
+**Payroll Impact**:
+- Zero work hours for the day
+- No salary for this shift
+- Flagged for HR review
+
+### Scenario 4: Vacation Day
+
+**Context**: Maria requests vacation for January 20.
+
+**Creation**:
+```php
+$plan = new TimetablePlan();
+$plan->admin_id = 456;
+$plan->date = '2025-01-20';
+$plan->slot_type_id = Timetable::TIMESLOT_VACATION;
+$plan->store_id = 5;
+$plan->work_time = 0;
+$plan->comment = 'Оплачиваемый отпуск';
+$plan->save();
+```
+
+**Payroll Impact**:
+- Counted as paid day
+- No check-in/check-out required
+- No fact record needed
+
+### Scenario 5: Shift Replacement
+
+**Context**: Maria can't work January 18, Anna covers.
+
+**Original Plan**:
+```php
+$plan = TimetablePlan::findOne(['admin_id' => 456, 'date' => '2025-01-18']);
+// Maria's shift
+```
+
+**Anna Checks In**:
+```php
+$checkin = new AdminCheckin();
+$checkin->admin_id = 789; // Anna
+$checkin->replaced_admin_id = 456; // Maria (original)
+$checkin->type_id = AdminCheckin::TYPE_START;
+$checkin->date = '2025-01-18';
+$checkin->plan_id = 12345; // Maria's plan
+$checkin->save();
+```
+
+**Fact Creation**:
+```php
+$fact = $plan->makeFact();
+// fact->admin_id = 789 (Anna actually worked)
+// fact->plan_id = 12345 (Maria's original plan)
+```
+
+**Payroll Impact**:
+- Maria: No pay for this shift
+- Anna: Paid for this shift (as replacement)
+
+---
+
+## Related Documentation
+
+- [Payroll Module](./payroll.md) - Salary calculation using timetable data
+- [Rating System](./rating.md) - Performance tracking per shift
+- [Dashboard Module](./dashboard.md) - Timetable analytics and reports
+- [Database Schema](../database/schema-overview.md) - Complete database documentation
+- [Services Overview](../services/overview.md) - Service layer documentation
+
+---
+
+## Changelog
+
+**2025-01**: Initial comprehensive documentation
+- Documented plan vs fact model
+- Added complete database schema
+- Documented 7 slot types and shift templates
+- Added check-in system with GPS tracking
+- Created 5 complete scenarios
+- Documented business logic and access control
+- Added integration points with payroll and rating systems
+
+---
+
+*Last Updated: January 2025*
+*Maintained by: ERP24 Development Team*