package handlers import ( "errors" "net/http" "strings" "git.mchus.pro/mchus/quoteforge/internal/localdb" "git.mchus.pro/mchus/quoteforge/internal/repository" "git.mchus.pro/mchus/quoteforge/internal/services" "github.com/gin-gonic/gin" ) // VendorSpecHandler handles vendor BOM spec operations for a configuration. type VendorSpecHandler struct { localDB *localdb.LocalDB configService *services.LocalConfigurationService } func NewVendorSpecHandler(localDB *localdb.LocalDB) *VendorSpecHandler { return &VendorSpecHandler{ localDB: localDB, configService: services.NewLocalConfigurationService(localDB, nil, nil, func() bool { return false }), } } // lookupConfig finds an active configuration by UUID using the standard localDB method. func (h *VendorSpecHandler) lookupConfig(uuid string) (*localdb.LocalConfiguration, error) { cfg, err := h.localDB.GetConfigurationByUUID(uuid) if err != nil { return nil, err } if !cfg.IsActive { return nil, errors.New("not active") } return cfg, nil } // GetVendorSpec returns the vendor spec (BOM) for a configuration. // GET /api/configs/:uuid/vendor-spec func (h *VendorSpecHandler) GetVendorSpec(c *gin.Context) { cfg, err := h.lookupConfig(c.Param("uuid")) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "configuration not found"}) return } spec := cfg.VendorSpec if spec == nil { spec = localdb.VendorSpec{} } c.JSON(http.StatusOK, gin.H{"vendor_spec": spec}) } // PutVendorSpec saves (replaces) the vendor spec for a configuration. // PUT /api/configs/:uuid/vendor-spec func (h *VendorSpecHandler) PutVendorSpec(c *gin.Context) { cfg, err := h.lookupConfig(c.Param("uuid")) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "configuration not found"}) return } var body struct { VendorSpec []localdb.VendorSpecItem `json:"vendor_spec"` } if err := c.ShouldBindJSON(&body); err != nil { RespondError(c, http.StatusBadRequest, "invalid request", err) return } for i := range body.VendorSpec { if body.VendorSpec[i].SortOrder == 0 { body.VendorSpec[i].SortOrder = (i + 1) * 10 } // Persist canonical LOT mapping only. body.VendorSpec[i].LotMappings = normalizeLotMappings(body.VendorSpec[i].LotMappings) body.VendorSpec[i].ResolvedLotName = "" body.VendorSpec[i].ResolutionSource = "" body.VendorSpec[i].ManualLotSuggestion = "" body.VendorSpec[i].LotQtyPerPN = 0 body.VendorSpec[i].LotAllocations = nil } spec := localdb.VendorSpec(body.VendorSpec) if _, err := h.configService.UpdateVendorSpecNoAuth(cfg.UUID, spec); err != nil { RespondError(c, http.StatusInternalServerError, "internal server error", err) return } c.JSON(http.StatusOK, gin.H{"vendor_spec": spec}) } func normalizeLotMappings(in []localdb.VendorSpecLotMapping) []localdb.VendorSpecLotMapping { if len(in) == 0 { return nil } merged := make(map[string]int, len(in)) order := make([]string, 0, len(in)) for _, m := range in { lot := strings.TrimSpace(m.LotName) if lot == "" { continue } qty := m.QuantityPerPN if qty < 1 { qty = 1 } if _, exists := merged[lot]; !exists { order = append(order, lot) } merged[lot] += qty } out := make([]localdb.VendorSpecLotMapping, 0, len(order)) for _, lot := range order { out = append(out, localdb.VendorSpecLotMapping{ LotName: lot, QuantityPerPN: merged[lot], }) } if len(out) == 0 { return nil } return out } // ResolveVendorSpec resolves vendor PN → LOT without modifying the cart. // POST /api/configs/:uuid/vendor-spec/resolve func (h *VendorSpecHandler) ResolveVendorSpec(c *gin.Context) { if _, err := h.lookupConfig(c.Param("uuid")); err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "configuration not found"}) return } var body struct { VendorSpec []localdb.VendorSpecItem `json:"vendor_spec"` } if err := c.ShouldBindJSON(&body); err != nil { RespondError(c, http.StatusBadRequest, "invalid request", err) return } bookRepo := repository.NewPartnumberBookRepository(h.localDB.DB()) resolver := services.NewVendorSpecResolver(bookRepo) resolved, err := resolver.Resolve(body.VendorSpec) if err != nil { RespondError(c, http.StatusInternalServerError, "internal server error", err) return } book, _ := bookRepo.GetActiveBook() aggregated, err := services.AggregateLOTs(resolved, book, bookRepo) if err != nil { RespondError(c, http.StatusInternalServerError, "internal server error", err) return } c.JSON(http.StatusOK, gin.H{ "resolved": resolved, "aggregated": aggregated, }) } // ApplyVendorSpec applies the resolved BOM to the cart (Estimate items). // POST /api/configs/:uuid/vendor-spec/apply func (h *VendorSpecHandler) ApplyVendorSpec(c *gin.Context) { cfg, err := h.lookupConfig(c.Param("uuid")) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "configuration not found"}) return } var body struct { Items []struct { LotName string `json:"lot_name"` Quantity int `json:"quantity"` UnitPrice float64 `json:"unit_price"` } `json:"items"` } if err := c.ShouldBindJSON(&body); err != nil { RespondError(c, http.StatusBadRequest, "invalid request", err) return } newItems := make(localdb.LocalConfigItems, 0, len(body.Items)) for _, it := range body.Items { newItems = append(newItems, localdb.LocalConfigItem{ LotName: it.LotName, Quantity: it.Quantity, UnitPrice: it.UnitPrice, }) } if _, err := h.configService.ApplyVendorSpecItemsNoAuth(cfg.UUID, newItems); err != nil { RespondError(c, http.StatusInternalServerError, "internal server error", err) return } c.JSON(http.StatusOK, gin.H{"items": newItems}) }