package adminstore import ( "context" "errors" "fmt" "strings" "github.com/jackc/pgx/v5" ) // Brand-management errors, mapped to client statuses by the handler. var ( // ErrDuplicateBrand is returned when a brand name already exists. ErrDuplicateBrand = errors.New("duplicate brand name") // ErrBrandInUse blocks deleting a brand still referenced by products. ErrBrandInUse = errors.New("brand in use") // ErrInvalidMerge is returned when a merge target is missing or equal to // the source. ErrInvalidMerge = errors.New("invalid merge target") ) // CreateBrand inserts a new brand. Names are unique by normalized form. func (s *Store) CreateBrand(ctx context.Context, actor, name string) (*Brand, error) { name = strings.TrimSpace(name) if name == "" { return nil, errors.New("name required") } var b Brand err := s.pool.QueryRow(ctx, `INSERT INTO brand (name, normalized_name) VALUES ($1, $2) RETURNING id, name, 0`, name, normBrand(name)).Scan(&b.ID, &b.Name, &b.ProductCount) if isUniqueViolation(err) { return nil, ErrDuplicateBrand } if err != nil { return nil, err } _ = s.writeAudit(ctx, actor, "create", "brand", &b.ID, []string{"name"}, nil, b) return &b, nil } // UpdateBrand renames a brand, keeping the normalized name in sync. func (s *Store) UpdateBrand(ctx context.Context, id, actor, name string) (*Brand, error) { name = strings.TrimSpace(name) if name == "" { return nil, errors.New("name required") } ct, err := s.pool.Exec(ctx, "UPDATE brand SET name = $1, normalized_name = $2 WHERE id = $3", name, normBrand(name), id) if isUniqueViolation(err) { return nil, ErrDuplicateBrand } if err != nil { return nil, err } if ct.RowsAffected() == 0 { return nil, ErrNotFound } out, err := s.getBrand(ctx, id) if err != nil { return nil, err } _ = s.writeAudit(ctx, actor, "update", "brand", &id, []string{"name"}, nil, out) return out, nil } // MergeBrands reassigns every product of src to dst, then deletes src. Useful // for collapsing duplicate brands (e.g. "可口可乐" and "Coca-Cola"). func (s *Store) MergeBrands(ctx context.Context, srcID, dstID, actor string) (*Brand, error) { if srcID == dstID { return nil, ErrInvalidMerge } tx, err := s.pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) var dstName string err = tx.QueryRow(ctx, "SELECT name FROM brand WHERE id = $1", dstID).Scan(&dstName) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrInvalidMerge } if err != nil { return nil, err } var srcName string err = tx.QueryRow(ctx, "SELECT name FROM brand WHERE id = $1", srcID).Scan(&srcName) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, err } if _, err := tx.Exec(ctx, "UPDATE product SET brand_id = $1 WHERE brand_id = $2", dstID, srcID); err != nil { return nil, err } if _, err := tx.Exec(ctx, "DELETE FROM brand WHERE id = $1", srcID); err != nil { return nil, err } if err := tx.Commit(ctx); err != nil { return nil, err } out, err := s.getBrand(ctx, dstID) if err != nil { return nil, err } _ = s.writeAudit(ctx, actor, "merge", "brand", &srcID, []string{"name"}, map[string]string{"name": srcName}, map[string]string{"merged_into": dstName, "merged_into_id": dstID}) return out, nil } // DeleteBrand removes a brand not referenced by any product. func (s *Store) DeleteBrand(ctx context.Context, id, actor string) error { before, err := s.getBrand(ctx, id) if err != nil { return err } if before.ProductCount > 0 { return fmt.Errorf("%w: %d products", ErrBrandInUse, before.ProductCount) } ct, err := s.pool.Exec(ctx, "DELETE FROM brand WHERE id = $1", id) if err != nil { if isForeignKeyViolation(err) { return ErrBrandInUse } return err } if ct.RowsAffected() == 0 { return ErrNotFound } _ = s.writeAudit(ctx, actor, "delete", "brand", &id, []string{"name"}, before, nil) return nil } func (s *Store) getBrand(ctx context.Context, id string) (*Brand, error) { var b Brand err := s.pool.QueryRow(ctx, ` SELECT b.id, b.name, (SELECT count(*) FROM product p WHERE p.brand_id = b.id) FROM brand b WHERE b.id = $1`, id).Scan(&b.ID, &b.Name, &b.ProductCount) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, err } return &b, nil }