- Migration 029: local_partnumber_books, local_partnumber_book_items, vendor_spec TEXT column on local_configurations - Models: LocalPartnumberBook, LocalPartnumberBookItem, VendorSpec, VendorSpecItem with JSON Valuer/Scanner - Repository: PartnumberBookRepository (GetActiveBook, FindLotByPartnumber, SaveBook/Items, ListBooks, CountBookItems) - Service: VendorSpecResolver 3-step resolution (book → manual suggestion → unresolved) + AggregateLOTs with is_primary_pn qty logic - Sync: PullPartnumberBooks append-only pull from qt_partnumber_books - Handlers: VendorSpecHandler (GET/PUT/resolve/apply), PartnumberBooksHandler - Routes: /api/configs/:uuid/vendor-spec*, /api/partnumber-books, /api/sync/partnumber-books, /partnumber-books page - UI: 3 top-level tabs [Estimate][BOM вендора][Ценообразование]; Excel paste, PN resolution, inline LOT autocomplete, pricing table - Bible: 03-database.md updated, 09-vendor-spec.md added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
93 lines
3.0 KiB
Go
93 lines
3.0 KiB
Go
package sync
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
|
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
|
)
|
|
|
|
// PullPartnumberBooks synchronizes partnumber book snapshots from MariaDB to local SQLite.
|
|
// It only pulls books that don't exist locally yet (append-only).
|
|
func (s *Service) PullPartnumberBooks() (int, error) {
|
|
slog.Info("starting partnumber book pull")
|
|
|
|
mariaDB, err := s.getDB()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("database not available: %w", err)
|
|
}
|
|
|
|
localBookRepo := repository.NewPartnumberBookRepository(s.localDB.DB())
|
|
|
|
// Query server for all active partnumber books
|
|
type serverBook struct {
|
|
ID int `gorm:"column:id"`
|
|
Version string `gorm:"column:version"`
|
|
CreatedAt time.Time `gorm:"column:created_at"`
|
|
IsActive bool `gorm:"column:is_active"`
|
|
}
|
|
var serverBooks []serverBook
|
|
if err := mariaDB.Raw("SELECT id, version, created_at, is_active FROM qt_partnumber_books WHERE is_active = 1 ORDER BY created_at DESC, id DESC").Scan(&serverBooks).Error; err != nil {
|
|
return 0, fmt.Errorf("querying server partnumber books: %w", err)
|
|
}
|
|
|
|
pulled := 0
|
|
for _, sb := range serverBooks {
|
|
// Check if already exists locally
|
|
var existing localdb.LocalPartnumberBook
|
|
err := s.localDB.DB().Where("server_id = ?", sb.ID).First(&existing).Error
|
|
if err == nil {
|
|
// Already exists
|
|
continue
|
|
}
|
|
|
|
// Save the book
|
|
localBook := &localdb.LocalPartnumberBook{
|
|
ServerID: sb.ID,
|
|
Version: sb.Version,
|
|
CreatedAt: sb.CreatedAt,
|
|
IsActive: sb.IsActive,
|
|
}
|
|
if err := localBookRepo.SaveBook(localBook); err != nil {
|
|
slog.Warn("failed to save local partnumber book", "server_id", sb.ID, "error", err)
|
|
continue
|
|
}
|
|
|
|
// Pull items for this book
|
|
type serverItem struct {
|
|
Partnumber string `gorm:"column:partnumber"`
|
|
LotName string `gorm:"column:lot_name"`
|
|
IsPrimaryPN bool `gorm:"column:is_primary_pn"`
|
|
Description string `gorm:"column:description"`
|
|
}
|
|
var serverItems []serverItem
|
|
if err := mariaDB.Raw("SELECT partnumber, lot_name, is_primary_pn, description FROM qt_partnumber_book_items WHERE book_id = ?", sb.ID).Scan(&serverItems).Error; err != nil {
|
|
slog.Warn("failed to query server partnumber book items", "book_id", sb.ID, "error", err)
|
|
continue
|
|
}
|
|
|
|
localItems := make([]localdb.LocalPartnumberBookItem, 0, len(serverItems))
|
|
for _, si := range serverItems {
|
|
localItems = append(localItems, localdb.LocalPartnumberBookItem{
|
|
BookID: localBook.ID,
|
|
Partnumber: si.Partnumber,
|
|
LotName: si.LotName,
|
|
IsPrimaryPN: si.IsPrimaryPN,
|
|
Description: si.Description,
|
|
})
|
|
}
|
|
if err := localBookRepo.SaveBookItems(localItems); err != nil {
|
|
slog.Warn("failed to save local partnumber book items", "book_id", localBook.ID, "error", err)
|
|
continue
|
|
}
|
|
|
|
slog.Info("pulled partnumber book", "version", sb.Version, "items", len(localItems))
|
|
pulled++
|
|
}
|
|
|
|
slog.Info("partnumber book pull completed", "pulled", pulled)
|
|
return pulled, nil
|
|
}
|