package woocommerce import ( "fmt" "strings" "github.com/google/uuid" ) const ( maxSyncOnlyIDs = 500 maxCategoryFilterLen = 200 ) // ProductSyncScope is an optional one-shot filter for EnqueueSync / POST /sync. // Empty fields mean "no filter" (sync recent processed products up to SyncLimit). type ProductSyncScope struct { Limit int `json:"sync_limit,omitempty"` Status string `json:"status,omitempty"` Category string `json:"category,omitempty"` ProductIDs []string `json:"product_ids,omitempty"` } func clearOneShotSyncFilters(opt *SyncOptions) { if opt == nil { return } opt.SyncFilterStatus = "" opt.SyncFilterCategory = "" opt.SyncOnlyIDs = nil } func normalizeSyncFilterStatus(status string) (string, error) { s := strings.TrimSpace(strings.ToLower(status)) if s == "" { return "", nil } switch s { case "completed", "needs_review", "error", "processing", "processed": return s, nil default: return "", fmt.Errorf("%w: invalid status", ErrInvalidSyncScope) } } func applyProductSyncScope(opt *SyncOptions, scope ProductSyncScope) error { if opt == nil { return fmt.Errorf("%w: missing options", ErrInvalidSyncScope) } clearOneShotSyncFilters(opt) if scope.Limit > 0 { if scope.Limit > maxSyncLimit { return fmt.Errorf("%w: sync_limit max %d", ErrInvalidSyncScope, maxSyncLimit) } opt.SyncLimit = scope.Limit } status, err := normalizeSyncFilterStatus(scope.Status) if err != nil { return err } opt.SyncFilterStatus = status cat := strings.TrimSpace(scope.Category) if len(cat) > maxCategoryFilterLen { return fmt.Errorf("%w: category too long", ErrInvalidSyncScope) } opt.SyncFilterCategory = cat if len(scope.ProductIDs) > maxSyncOnlyIDs { return fmt.Errorf("%w: product_ids max %d", ErrInvalidSyncScope, maxSyncOnlyIDs) } cleaned := make([]string, 0, len(scope.ProductIDs)) seen := make(map[string]struct{}, len(scope.ProductIDs)) for _, raw := range scope.ProductIDs { id := strings.TrimSpace(raw) if id == "" { continue } parsed, err := uuid.Parse(id) if err != nil { return fmt.Errorf("%w: invalid product_ids", ErrInvalidSyncScope) } key := parsed.String() if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} cleaned = append(cleaned, key) } opt.SyncOnlyIDs = cleaned return nil } func pruneSyncOnlyIDs(ids []string, max int) []string { if max <= 0 || len(ids) <= max { return ids } return ids[:max] }