package feeds import ( "context" "errors" "fmt" "strings" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) // requiredStandardField is an enabled, required company standard field used for sync preflight. type requiredStandardField struct { Key string Name string } // activeMappings returns rows with a non-empty source and a real target (not "none"). // Mirrors the frontend activeMappingRows filter. func activeMappings(mappings []FieldMapping) []FieldMapping { out := make([]FieldMapping, 0, len(mappings)) for _, m := range mappings { src := m.sourceKey() tgt := m.targetKey() if src == "" || tgt == "" || strings.EqualFold(tgt, "none") { continue } out = append(out, m) } return out } // validateMappingsForSync returns a ClientMsg when mappings are empty or required targets are missing. // Optional (non-required) standard fields may remain unmapped. func validateMappingsForSync(mappings []FieldMapping, required []requiredStandardField) error { active := activeMappings(mappings) if len(active) == 0 { return ClientMsg("Map at least one source field before syncing.") } if len(required) == 0 { return nil } mapped := make(map[string]struct{}, len(active)) for _, m := range active { mapped[m.targetKey()] = struct{}{} } missing := make([]string, 0) for _, r := range required { if _, ok := mapped[r.Key]; ok { continue } label := strings.TrimSpace(r.Name) if label == "" { label = r.Key } missing = append(missing, label) } if len(missing) == 0 { return nil } return ClientMsg(fmt.Sprintf("Map required fields before syncing: %s.", strings.Join(missing, ", "))) } // loadRequiredStandardFields returns enabled+required standard field keys for the company. func (s *Service) loadRequiredStandardFields(ctx context.Context, companyID uuid.UUID) ([]requiredStandardField, error) { if s == nil || s.Pool == nil { return nil, nil } rows, err := s.Pool.Query(ctx, ` SELECT key, COALESCE(NULLIF(TRIM(name), ''), key) AS name FROM standard_fields WHERE company_id = $1 AND enabled = true AND is_required = true ORDER BY sort_order ASC, key ASC`, companyID) if err != nil { return nil, err } defer rows.Close() out := make([]requiredStandardField, 0) for rows.Next() { var key, name string if err := rows.Scan(&key, &name); err != nil { return nil, err } key = strings.TrimSpace(key) if key == "" { continue } out = append(out, requiredStandardField{Key: key, Name: strings.TrimSpace(name)}) } return out, rows.Err() } // ensureMappingsReadyForSync loads active mappings and required standard fields, then validates. // Called before creating a sync job so clients get a clear 400 without a failed job row. func (s *Service) ensureMappingsReadyForSync(ctx context.Context, companyID, feedID uuid.UUID) error { raw, err := s.loadMappingsRaw(ctx, companyID, feedID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return ClientMsg("Map at least one source field before syncing.") } return err } required, err := s.loadRequiredStandardFields(ctx, companyID) if err != nil { return err } return validateMappingsForSync(parseMappings(raw), required) }