Skip to content

feat: add UOM category management with CRUD, import/export, and seed migration#47

Merged
ilramdhan merged 3 commits intomutugading:mainfrom
ilramdhan:feat/formula-master-be
Apr 13, 2026
Merged

feat: add UOM category management with CRUD, import/export, and seed migration#47
ilramdhan merged 3 commits intomutugading:mainfrom
ilramdhan:feat/formula-master-be

Conversation

@ilramdhan
Copy link
Copy Markdown
Member

Description

This pull request updates the Unit of Measure (UOM) API to remove the UOMCategory enum 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

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that changes existing API)
  • ♻️ Refactor (code change without new feature or bug fix)
  • 📚 Documentation update
  • 🧪 Test update
  • 🔧 Chore (dependencies, config, etc.)

Service(s) Affected

  • Finance Service
  • IAM Service
  • Shared Proto (gen/)
  • Root/Common

Changes Made

The most important changes are:

Removal of UOMCategory Enum:

  • The UOMCategory enum and all related code were removed from uom.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:

  • The UOM struct now includes uom_category_id, uom_category_code, and uom_category_name fields (all strings), replacing the previous enum-based uom_category field. ([[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))
  • Getter methods for the new fields have been added. ([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, and ExportUOMsRequest now use uom_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))
  • The sort_by field in ListUOMsRequest now 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:

  • Adjusted enum index references for ActiveFilter after removing UOMCategory, 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

  • New unit tests added
  • Existing unit tests pass
  • Coverage maintained/improved

Integration Tests

  • New integration tests added
  • Existing integration tests pass

Lint & Build

  • golangci-lint run ./... passes
  • go build ./... succeeds
  • go test -race ./... passes

Database (if applicable)

  • Migration added
  • Migration tested (up and down)
  • No breaking schema changes (or documented)

Documentation

  • README.md updated (if needed)
  • RULES.md updated (if needed)
  • Proto comments updated
  • OpenAPI regenerated

Pre-merge Checklist

  • I have read and followed RULES.md
  • I have read and followed CONTRIBUTING.md
  • Clean Architecture principles followed
  • All errors are properly handled
  • Context is passed appropriately
  • Structured logging is used
  • No hardcoded secrets
  • PR description is complete and clear
  • CI checks are passing

@ilramdhan ilramdhan requested a review from Copilot April 13, 2026 08:24
@ilramdhan ilramdhan self-assigned this Apr 13, 2026
@ilramdhan ilramdhan added documentation Improvements or additions to documentation enhancement New feature or request labels Apr 13, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_category table + migration to convert existing UOMs from category string/enum to uom_category_id FK.
  • Update UOM domain, repositories, caching, handlers, and OpenAPI/proto outputs to use uom_category_id and return denormalized uom_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.

Comment on lines +118 to +132
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
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 384 to +392
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(),
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 39 to 55
@@ -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 {
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to 58
// 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
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 169 to 179
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)

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@ilramdhan ilramdhan merged commit 29e4c43 into mutugading:main Apr 13, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants