feat: add UOM category management with CRUD, import/export, and seed migration#47
Conversation
There was a problem hiding this comment.
Pull request overview
This PR replaces the hardcoded UOM category enum with a normalized mst_uom_category table and updates the Finance UOM APIs and infrastructure to use category UUID FKs plus denormalized category fields. It also introduces full CRUD + import/export + template support for managing UOM Categories, and seeds new IAM menu/permissions for the feature.
Changes:
- Add
mst_uom_categorytable + migration to convert existing UOMs from category string/enum touom_category_idFK. - Update UOM domain, repositories, caching, handlers, and OpenAPI/proto outputs to use
uom_category_idand return denormalizeduom_category_code/name. - Introduce UOM Category domain/application/infrastructure + gRPC/REST surface area (CRUD, list, import/export, template) and seed IAM navigation/permissions.
Reviewed changes
Copilot reviewed 46 out of 46 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| services/iam/migrations/postgres/000016_seed_uom_category_menu.up.sql | Seeds IAM permissions and menu entry for the new UOM Category module. |
| services/iam/migrations/postgres/000016_seed_uom_category_menu.down.sql | Rollback for the IAM seeds (menu + permissions + role assignments). |
| services/finance/seeds/main.go | Updates finance seeder to resolve category FK from mst_uom_category before inserting UOMs. |
| services/finance/migrations/postgres/000007_create_mst_uom_category.up.sql | Creates mst_uom_category, seeds initial categories, migrates UOMs to FK, drops legacy column/constraint. |
| services/finance/migrations/postgres/000007_create_mst_uom_category.down.sql | Rollback restoring legacy string category column + constraint and dropping category table/indexes. |
| services/finance/internal/infrastructure/redis/uom_cache.go | Updates cached UOM representation to store category id/code/name and reconstruct entities accordingly. |
| services/finance/internal/infrastructure/postgres/uom_repository.go | Migrates UOM persistence to FK and joins category table for reads + adds category sort support. |
| services/finance/internal/infrastructure/postgres/uom_repository_test.go | Updates repository integration tests to use mst_uom_category and FK. |
| services/finance/internal/infrastructure/postgres/uom_category_repository.go | Adds Postgres repository implementation for UOM Category CRUD/list/export and “in-use” checks. |
| services/finance/internal/domain/uomcategory/value_objects.go | Adds uomcategory.Code value object + validation rules. |
| services/finance/internal/domain/uomcategory/repository.go | Defines uomcategory.Repository interface and filter structs. |
| services/finance/internal/domain/uomcategory/errors.go | Adds domain error set for UOM Category operations. |
| services/finance/internal/domain/uomcategory/entity.go | Adds UOM Category aggregate with validation + update/soft-delete behavior. |
| services/finance/internal/domain/uomcategory/entity_test.go | Adds unit tests for UOM Category value object + entity behavior. |
| services/finance/internal/domain/uom/value_object.go | Replaces UOM category enum VO with CategoryInfo (id/code/name). |
| services/finance/internal/domain/uom/uom_test.go | Updates/extends UOM tests for FK-based category id validation and updates. |
| services/finance/internal/domain/uom/repository.go | Updates UOM filters to use CategoryID *uuid.UUID and introduces CategoryRepository for code→ID resolution. |
| services/finance/internal/domain/uom/errors.go | Updates invalid category error semantics for UUID-based categories. |
| services/finance/internal/domain/uom/entity.go | Updates UOM entity to store CategoryInfo and validate category via UUID presence. |
| services/finance/internal/domain/uom/entity_test.go | Updates unit tests to reflect UUID-based category handling and CategoryInfo. |
| services/finance/internal/delivery/grpc/uom_handler.go | Updates UOM gRPC handler to accept uom_category_id and return denormalized category fields. |
| services/finance/internal/delivery/grpc/uom_category_handler.go | Adds new gRPC handler exposing UOM Category CRUD/list/import/export/template endpoints. |
| services/finance/internal/delivery/grpc/metrics.go | Adds Prometheus counter for UOM Category operations. |
| services/finance/internal/application/uomcategory/create_handler.go | Adds application handler for creating UOM Categories. |
| services/finance/internal/application/uomcategory/get_handler.go | Adds application handler for retrieving a UOM Category by ID. |
| services/finance/internal/application/uomcategory/list_handler.go | Adds application handler for listing UOM Categories with pagination/filtering. |
| services/finance/internal/application/uomcategory/update_handler.go | Adds application handler for updating UOM Categories. |
| services/finance/internal/application/uomcategory/delete_handler.go | Adds application handler for soft-deleting UOM Categories with “in-use” protection. |
| services/finance/internal/application/uomcategory/export_handler.go | Adds UOM Category export-to-Excel handler. |
| services/finance/internal/application/uomcategory/import_handler.go | Adds UOM Category import-from-Excel handler with duplicate handling modes. |
| services/finance/internal/application/uomcategory/template_handler.go | Adds Excel template generation for UOM Category imports. |
| services/finance/internal/application/uom/create_handler.go | Updates UOM create handler to accept category UUID string. |
| services/finance/internal/application/uom/update_handler.go | Updates UOM update handler to accept optional category UUID string. |
| services/finance/internal/application/uom/list_handler.go | Updates UOM listing to filter by category UUID. |
| services/finance/internal/application/uom/import_handler.go | Updates UOM import to use category code→UUID resolution via category repository. |
| services/finance/internal/application/uom/export_handler.go | Updates UOM export to filter by category UUID and export category code. |
| services/finance/internal/application/uom/template_handler.go | Updates UOM import template to use “Category Code” column/instructions. |
| services/finance/internal/application/uom/handlers_test.go | Updates UOM application handler tests to pass category UUIDs. |
| services/finance/cmd/server/main.go | Wires up UOMCategory repository/handler and injects category repo into UOM handler/import. |
| gen/openapi/finance/v1/uom.swagger.json | Updates OpenAPI for UOM endpoints to use uomCategoryId and include category sort. |
| gen/openapi/finance/v1/uom_category.swagger.json | Adds OpenAPI spec for new UOM Category endpoints. |
| gen/finance/v1/uom.pb.go | Regenerates proto Go code removing UOMCategory enum and adding category id/code/name fields. |
| gen/finance/v1/uom_category.pb.gw.go | Adds grpc-gateway bindings for UOM Category service. |
| gen/finance/v1/uom_category_grpc.pb.go | Adds gRPC service/client stubs for UOM Category service. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func (s *UOMRepositorySuite) seedTestCategory() uuid.UUID { | ||
| id := uuid.New() | ||
| _, err := s.db.ExecContext(s.ctx, ` | ||
| INSERT INTO mst_uom_category (uom_category_id, category_code, category_name, created_by) | ||
| VALUES ($1, 'TEST_WEIGHT', 'Test Weight', 'test') | ||
| ON CONFLICT (category_code) DO UPDATE SET category_code = EXCLUDED.category_code | ||
| RETURNING uom_category_id | ||
| `, id) | ||
| if err != nil { | ||
| // If conflict, fetch existing | ||
| _ = s.db.QueryRowContext(s.ctx, | ||
| "SELECT uom_category_id FROM mst_uom_category WHERE category_code = 'TEST_WEIGHT'", | ||
| ).Scan(&id) | ||
| } | ||
| return id |
There was a problem hiding this comment.
seedTestCategory uses ExecContext with an INSERT ... RETURNING but never scans the returned row, and the ON CONFLICT DO UPDATE path will not error. This can cause the function to return a UUID that does not match the row’s actual uom_category_id, leading to FK failures when inserting UOMs. Use QueryRowContext(...).Scan(&id) (or a separate SELECT) so the returned ID is always the persisted one, regardless of conflicts.
| func entityToProto(entity *uomdomain.UOM) *financev1.UOM { | ||
| proto := &financev1.UOM{ | ||
| UomId: entity.ID().String(), | ||
| UomCode: entity.Code().String(), | ||
| UomName: entity.Name(), | ||
| UomCategory: categoryToProto(entity.Category().String()), | ||
| Description: entity.Description(), | ||
| IsActive: entity.IsActive(), | ||
| UomId: entity.ID().String(), | ||
| UomCode: entity.Code().String(), | ||
| UomName: entity.Name(), | ||
| UomCategoryId: entity.CategoryID().String(), | ||
| UomCategoryCode: entity.CategoryInfo().Code(), | ||
| UomCategoryName: entity.CategoryInfo().Name(), | ||
| Description: entity.Description(), |
There was a problem hiding this comment.
entityToProto expects CategoryInfo().Code() / .Name() to be populated, but NewUOM currently sets only the category UUID (code/name are empty) and Update replaces categoryInfo with an ID-only value when category changes. As a result, Create/Update responses (and any cache writes from these entities) can return empty uom_category_code / uom_category_name. Consider reloading the UOM from the repository after create/update (JOINing mst_uom_category), or resolving category details and setting a fully populated CategoryInfo before building the response.
| @@ -49,7 +51,7 @@ func (h *CreateHandler) Handle(ctx context.Context, cmd CreateCommand) (*uom.UOM | |||
| } | |||
|
|
|||
| // 3. Create domain entity | |||
| entity, err := uom.NewUOM(code, cmd.UOMName, category, cmd.Description, cmd.CreatedBy) | |||
| entity, err := uom.NewUOM(code, cmd.UOMName, categoryID, cmd.Description, cmd.CreatedBy) | |||
| if err != nil { | |||
There was a problem hiding this comment.
CreateHandler only validates that UOMCategoryID parses as a UUID. If the UUID is well-formed but does not exist (or refers to a deleted/inactive category), the repository call will fail with a FK violation which is likely surfaced as a 500 via domainErrorToBaseResponse. Add an explicit category existence/validity check (and return a domain-level invalid/not-found error) before attempting the insert.
| // 3. Prepare category ID if provided | ||
| var categoryID *uuid.UUID | ||
| if cmd.UOMCategoryID != nil { | ||
| parsed, parseErr := uuid.Parse(*cmd.UOMCategoryID) | ||
| if parseErr != nil { | ||
| return nil, uom.ErrInvalidCategory | ||
| } | ||
| category = &cat | ||
| categoryID = &parsed | ||
| } | ||
|
|
||
| // 4. Update domain entity | ||
| if err := entity.Update(cmd.UOMName, category, cmd.Description, cmd.IsActive, cmd.UpdatedBy); err != nil { | ||
| if err := entity.Update(cmd.UOMName, categoryID, cmd.Description, cmd.IsActive, cmd.UpdatedBy); err != nil { | ||
| return nil, err |
There was a problem hiding this comment.
When UOMCategoryID is provided, the handler parses the UUID but does not verify the category exists. A non-existent UUID will cause repo.Update to fail with a FK violation and may be mapped to a 500. Also, updating the category currently overwrites CategoryInfo with an ID-only value (code/name lost), which can lead to empty denormalized fields in the response unless the entity is reloaded. Validate category existence and/or reload the entity after update so CategoryInfo is fully populated.
| code, err := uom.NewCode(data.Code) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| category, err := uom.NewCategory(data.Category) | ||
| categoryID, err := uuid.Parse(data.CategoryID) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| categoryInfo := uom.NewCategoryInfo(categoryID, data.CategoryCode, data.CategoryName) | ||
|
|
There was a problem hiding this comment.
toEntity unconditionally parses CategoryID from cached JSON. Any pre-existing cache entries written with the old schema (e.g., only category field) will decode with CategoryID == "" and then fail uuid.Parse, breaking reads until Redis is flushed. Consider adding backward-compatible decoding (e.g., accept old category field), or treat parse failures as a cache miss by deleting the key and returning a not-found/miss error instead of propagating the parse error.
Description
This pull request updates the Unit of Measure (UOM) API to remove the
UOMCategoryenum and instead use category IDs and denormalized category fields throughout the UOM-related messages. This change affects the UOM model and all request/response types that previously used the category enum, making the API more flexible and better aligned with a normalized database structure.Type of Change
Service(s) Affected
Changes Made
The most important changes are:
Removal of UOMCategory Enum:
UOMCategoryenum and all related code were removed fromuom.pb.go, including all usages in UOM messages and requests. ([[1]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L27-L87),[[2]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L1318-R1333),[[3]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L1401-R1358))Model and Field Updates:
UOMstruct now includesuom_category_id,uom_category_code, anduom_category_namefields (all strings), replacing the previous enum-baseduom_categoryfield. ([[1]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L150-R100),[[2]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L213-L219),[[3]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3R177-R197))[gen/finance/v1/uom.pb.goR177-R197](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3R177-R197))Request/Response Changes:
CreateUOMRequest,UpdateUOMRequest,ListUOMsRequest, andExportUOMsRequestnow useuom_category_id(string) instead of the enum for specifying category. ([[1]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L249-R209),[[2]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L301-R267),[[3]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L480-R442),[[4]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L535-L541),[[5]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L712-R677),[[6]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L778-L784),[[7]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3R754-R760),[[8]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L873-R833),[[9]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L914-R879))sort_byfield inListUOMsRequestnow allows sorting by"category"(previously not supported). ([[1]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L712-R677),[[2]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L1318-R1333))Enum Index Updates:
ActiveFilterafter removingUOMCategory, ensuring correct enum handling in all relevant methods. ([[1]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L125-R68),[[2]](https://github.com/mutugading/goapps-backend/pull/47/files#diff-986b1624327c8294e9e970106273ee696034f3a94fd2d1b1a5e7b6840fec3bd3L138-R77))These changes modernize the UOM API for better database normalization and future extensibility.
Testing Performed
Unit Tests
Integration Tests
Lint & Build
golangci-lint run ./...passesgo build ./...succeedsgo test -race ./...passesDatabase (if applicable)
Documentation
Pre-merge Checklist