package catalog import ( "context" "fmt" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) const maxResetProductIDs = 5000 // ResetProductsToUnprocessed resets selected products so they reappear as unprocessed raw items. // kind "raw" updates raw_products by id; kind "processed" (default) deletes processed rows and // resets linked raw rows (by raw_product_id and shared product_id/gtin), matching legacy behavior. func (s *Service) ResetProductsToUnprocessed(ctx context.Context, companyID uuid.UUID, productIDs []uuid.UUID, kind string) (map[string]any, error) { if len(productIDs) == 0 { return nil, ClientMsg("product_ids is required") } if len(productIDs) > maxResetProductIDs { return nil, ClientMsg(fmt.Sprintf("at most %d product_ids allowed", maxResetProductIDs)) } tx, err := s.Pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) var resetCount int64 switch kind { case "raw": resetCount, err = resetRawProducts(ctx, tx, companyID, productIDs) default: resetCount, err = resetProcessedProducts(ctx, tx, companyID, productIDs) } if err != nil { return nil, err } if err := tx.Commit(ctx); err != nil { return nil, err } return map[string]any{ "success": true, "reset_count": resetCount, "message": fmt.Sprintf("%d product(s) returned to unprocessed state", resetCount), }, nil } func resetRawProducts(ctx context.Context, tx pgx.Tx, companyID uuid.UUID, ids []uuid.UUID) (int64, error) { ct, err := tx.Exec(ctx, ` UPDATE raw_products SET is_processed = false, processing_status = 'unprocessed', updated_at = now() WHERE company_id = $1 AND id = ANY($2::uuid[])`, companyID, ids) if err != nil { return 0, err } _, err = tx.Exec(ctx, ` DELETE FROM processed_products WHERE company_id = $1 AND raw_product_id = ANY($2::uuid[])`, companyID, ids) if err != nil { return 0, err } return ct.RowsAffected(), nil } func resetProcessedProducts(ctx context.Context, tx pgx.Tx, companyID uuid.UUID, ids []uuid.UUID) (int64, error) { rows, err := tx.Query(ctx, ` SELECT id, raw_product_id, product_id FROM processed_products WHERE company_id = $1 AND id = ANY($2::uuid[])`, companyID, ids) if err != nil { return 0, err } defer rows.Close() processedIDs := make([]uuid.UUID, 0, len(ids)) rawIDs := make([]uuid.UUID, 0) gtins := make([]string, 0) seenGTIN := map[string]struct{}{} for rows.Next() { var id uuid.UUID var rawID *uuid.UUID var productID *string if err := rows.Scan(&id, &rawID, &productID); err != nil { return 0, err } processedIDs = append(processedIDs, id) if rawID != nil { rawIDs = append(rawIDs, *rawID) } if productID != nil { g := *productID if g != "" { if _, ok := seenGTIN[g]; !ok { seenGTIN[g] = struct{}{} gtins = append(gtins, g) } } } } if err := rows.Err(); err != nil { return 0, err } if len(processedIDs) == 0 { return 0, ClientMsg("no products found to return to unprocessed state") } if len(gtins) > 0 { _, err = tx.Exec(ctx, ` UPDATE raw_products SET is_processed = false, processing_status = 'unprocessed', updated_at = now() WHERE company_id = $1 AND gtin = ANY($2::text[])`, companyID, gtins) if err != nil { return 0, err } } if len(rawIDs) > 0 { _, err = tx.Exec(ctx, ` UPDATE raw_products SET is_processed = false, processing_status = 'unprocessed', updated_at = now() WHERE company_id = $1 AND id = ANY($2::uuid[])`, companyID, rawIDs) if err != nil { return 0, err } } ct, err := tx.Exec(ctx, ` DELETE FROM processed_products WHERE company_id = $1 AND id = ANY($2::uuid[])`, companyID, processedIDs) if err != nil { return 0, err } return ct.RowsAffected(), nil }