# PriceForge - Claude Code Instructions ## Overview Администратор цен для работы с прайслистами, складскими справками и алертами. Источник данных: MariaDB. Локальная БД используется только для настроек и техданных приложения. ## Scope - Управление ценами и котировками компонентов - Управление прайслистами и их версиями - Импорт складских справок - Алерты по ценам/свежести данных - Подготовка к интеграции с API B2B площадок для автоматических котировок ## API Endpoints | Group | Endpoints | |-------|-----------| | Setup | GET/POST /setup, POST /setup/test, GET /setup/status | | System | GET /health, GET /api/ping, GET /api/db-status, GET /api/current-user | | Components | GET /api/components, GET /api/components/:lot_name, GET /api/categories | | Pricelists | CRUD /api/pricelists, GET /api/pricelists/latest | | Pricing Admin | /api/admin/pricing/* | | Sync (diagnostics/minimal) | GET /api/sync/status, GET /api/sync/info, POST /api/sync/components, POST /api/sync/pricelists | ## Commands ```bash # Development go run ./cmd/pfs make run # Build make build make build-release # Version ./bin/pfs -version ``` ## Tech Stack Go 1.22+ | Gin | GORM | MariaDB 11 | htmx + Tailwind CDN | excelize ## Pricelist Sources ### Estimate - Создается из snapshot текущих цен компонентов (`qt_lot_metadata`) - Сохраняет все настройки ценообразования: `price_period_days`, `price_coefficient`, `manual_price`, `meta_prices` - Это основной прайслист для расчетов и оценок ### Warehouse - Создается из складских справок (`stock_log`) - **ВАЖНО**: НЕ должен загружать настройки цен из `lot_metadata` - **КРИТИЧНО**: В warehouse прайслист попадают ТОЛЬКО сопоставленные partnumbers (есть в `lot_partnumbers`) - Несопоставленные partnumbers НЕ должны попадать в прайслист ни в каком виде - Использует только weighted median (взвешенная медиана по количеству) из stock_log - Поле `price_method` всегда содержит "weighted_median" - Не содержит `price_period_days`, `price_coefficient`, `manual_price`, `meta_prices` - Это фактические складские цены без дополнительной обработки ### Competitor - Зарезервировано для будущих интеграций с API B2B площадок ## Background Tasks (Фоновые задачи) **ВАЖНО**: Все долгие операции выполняются через Task Manager, НЕ через SSE (Server-Sent Events). ### Принцип работы: 1. **Backend**: Хендлер создает фоновую задачу через `taskManager.Submit()` и возвращает `task_id` 2. **Frontend**: Получает `task_id` и делает polling через `/api/tasks/:id` каждые 500ms 3. **Уведомления**: Система автоматически показывает toast-уведомления при завершении задачи ### Типы задач: - `TaskTypeRecalculate` - пересчет всех цен компонентов - `TaskTypeStockImport` - импорт складской справки из .mxl/.xlsx - `TaskTypePricelistCreate` - создание прайслиста (estimate/warehouse/competitor) ### Структура задачи: ```go type Task struct { ID string // UUID задачи Type TaskType // Тип задачи Status TaskStatus // running | completed | error Progress int // 0-100 Message string // Текущее сообщение для UI Result map[string]interface{} // Результат при completed Error string // Текст ошибки при error CreatedAt time.Time DoneAt *time.Time } ``` ### Пример использования: ```go taskID := h.taskManager.Submit(tasks.TaskTypeStockImport, func(ctx context.Context, progressCb func(int, string)) (map[string]interface{}, error) { // Выполнение задачи progressCb(50, "Половина выполнена") // Возврат результата return map[string]interface{}{ "rows_total": 100, "inserted": 95, }, nil }) c.JSON(http.StatusOK, gin.H{"task_id": taskID}) ``` ## Категории товаров (lot_category) ### Источник данных: Категории берутся из таблицы `lot` (поле `lot_category`), НЕ из `qt_lot_metadata`. ### При создании прайслистов: 1. **Estimate**: Загружаем категории из `lot.lot_category` для всех компонентов 2. **Warehouse**: Загружаем категории из `lot.lot_category` для всех позиций 3. Сохраняем в поле `lot_category` таблицы `qt_pricelist_items` ### В модели: ```go type PricelistItem struct { LotCategory *string `gorm:"column:lot_category;size:50" json:"category,omitempty"` // JSON поле называется "category" для фронтенда } ``` ### ВАЖНО: - Категория НЕ виртуальное поле - она сохраняется в БД при создании прайслиста - JOIN с таблицей `lot` нужен только для `lot_description`, категория уже есть в `qt_pricelist_items` - Никогда не получать категорию из имени элемента (lot name/названия). Всегда использовать категорию из `PricelistItem` (поле `lot_category` / JSON `category`). ## Notes - Главная страница должна вести на `/admin/pricing`. - В UI не должно быть ссылок на конфигуратор, проекты и экспорт. - **НЕ использовать SSE** - только фоновые задачи через Task Manager. ## Vendor Mapping Rules (2026-02-18) - `lot_partnumbers` is the canonical mapping contract for external configurator. - Do not reintroduce multi-row mapping for one key: one `(vendor, partnumber)` -> one `lot_name`. - Resolver behavior is fixed: - first exact `vendor + partnumber`, - then fallback `vendor='' + partnumber`. - Composite vendor partnumbers must be implemented via internal bundle tables: - `qt_lot_bundles` - `qt_lot_bundle_items` - Bundle LOT is internal and hidden in regular LOT UI by default. - Ignore behavior: - do not use `stock_ignore_rules` for new logic, - use `qt_vendor_partnumber_seen.is_ignored` (cross-source). - Client compatibility requirement: - client still consumes LOT-based pricelists, - bundle expansion/allocation happens only inside PriceForge warehouse calculations. ### Related modules - Migration: `migrations/023_vendor_partnumber_global_mapping.sql` - Service/API: `internal/services/vendor_mapping.go`, `internal/handlers/pricing.go` - Resolver: `internal/lotmatch/matcher.go` - Warehouse calc: `internal/warehouse/snapshot.go` - Stock import seen+ignore: `internal/services/stock_import.go`