Skip to content

feat/fix: 6-Sheet Bulk Import/Export Engine for Product Routing#127

Merged
ilramdhan merged 22 commits into
mutugading:mainfrom
ilramdhan:fix/seed-master-data-product-cost
Jun 22, 2026
Merged

feat/fix: 6-Sheet Bulk Import/Export Engine for Product Routing#127
ilramdhan merged 22 commits into
mutugading:mainfrom
ilramdhan:fix/seed-master-data-product-cost

Conversation

@ilramdhan

Copy link
Copy Markdown
Member

Description

This PR introduces the Bulk Product Routing Engine, a comprehensive, highly scalable feature to handle bulk importing and exporting of complex costing data via a 6-sheet Excel file.

The engine supports asynchronous bulk processing for Product Masters, Applicable Params (CAPP), Parameter Overrides (CPP), Route Heads, Route Sequences, and Route Raw Materials (RM). It includes synchronous dry-run validation, extensive error reporting generation (downloadable as Excel), and integration with the existing RabbitMQ + MinIO worker architecture.

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

📊 6-Sheet Bulk Import Engine (Finance)

  • Foundation & Parsers: Added costbulkimport package with ImportMaps (for cross-sheet ID resolution) and a robust ParseSheet utility with header validation and empty-row skipping.
  • Bulk Repositories: Added bulk Upsert and Replace methods to CostProductMaster and CostRoute repositories leveraging ON CONFLICT clauses for high performance. Batched operations (e.g., 200 rows per batch) are utilized to prevent memory exhaustion.
  • Asynchronous Workflow: Built BulkImportHandler to parse the 6 sheets (Product Master, CAPP, CPP, Route Head, Route Seq, Route RM) asynchronously. Job status transitions through PENDING → RUNNING → DONE/PARTIAL/FAILED.
  • Validation & Error Reporting: Built ValidateHandler for synchronous dry-runs with a 20-error-per-sheet limit, and GenerateErrorReport to create downloadable multi-sheet Excel files detailing import failures.
  • Worker Fixes: Fixed the RabbitMQ worker to bypass MinIO fetchFile() for export jobs (which store JSON params instead of object paths). Fixed chk_cij_entity constraint to accept the new bulk entity constants.

📤 Bulk Export & Template Handlers (Finance)

  • Export Engine: Built ExportHandler to dynamically generate the 6-sheet Excel file from current DB state (supporting ProductType filtering), pushing the artifact to MinIO.
  • Template Download: Added BulkImportTemplateHandler for users to download an empty, correctly formatted 6-sheet template.

🐛 SQL, Linting & Domain Fixes

  • Fixed ON CONFLICT predicates and generate_cost_product_code arity bugs.
  • Modernized min/max clamping logic using built-in min() / max() (Go 1.21+).
  • Cleaned up multiple linter warnings (goconst, unparam, gocritic/ifElseChain, etc.).
  • Fixed ListAllParams missing uom_code by adding a correct LEFT JOIN to mst_uom.
  • Corrected Export RM sheet to accurately pull RouteLevel and RouteSeq.

🔐 IAM Service

  • Added Migration 000061 to seed the FINANCE_IMPORT_JOBS menu under FINANCE_PRODUCT_COSTING. Since it has no permission entries, it is intentionally visible to all authenticated Finance users.

Related Issues

Fixes #
Related to #

API Changes (if applicable)

Proto Changes

+ // Added: ImportBulkProductRouting, ValidateBulkProductRoutingFile, ExportBulkProductRouting, DownloadBulkProductRoutingTemplate

Breaking Changes

None.

Testing Performed

Unit Tests

  • New unit tests added (Coverage for GenerateErrorReport and limits).
  • Existing unit tests pass
  • Coverage maintained/improved

Integration Tests

  • New integration tests added
  • Existing integration tests pass

Manual Testing

# Deployed and ran migrations for both services
./scripts/finance-setup.sh goapps-production migrate
./scripts/iam-setup.sh goapps-production migrate

# Verified 6-sheet async bulk upload completes and resolves dependencies correctly
# Verified dry-run validation limits errors to 20 per sheet
# Verified Export job successfully bypasses fetchFile and uploads generated Excel

Lint & Build

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

Database (if applicable)

  • Migration added
    • Finance: uk_cpm_flex02 adjustments, extended chk_cij_entity constraint.
    • IAM: 000061 (Import Jobs Menu).
  • 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

Rollback Plan

  • Rollback backend deployments for finance-service and finance-worker.
  • Revert IAM Migration 000061 to remove the Import Jobs menu.
  • Note: Dropping the bulk routing entities from the chk_cij_entity enum is non-trivial in Postgres, so any queued export/import jobs in the DB will simply fail to process if the code is rolled back.

Screenshots/Logs (if applicable)


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

Reviewer Notes

  • Export Jobs & MinIO: Export jobs store the request payload as JSON in the file_key column rather than pointing to a pre-existing MinIO file. The worker now catches this case early and routes it directly to ExportHandler.
  • Conflict Keys: Bulk UPSERT operations rely heavily on cpm_flex_02 as the legacy ID conflict key. Any manual modifications to this column could disrupt subsequent bulk imports.

ilramdhan and others added 19 commits June 22, 2026 14:17
…k_cpm_flex02, and regenerated proto code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…interfaces + postgres implementations

- New package costbulkimport: ImportMaps struct (5 maps for sheet-by-sheet ID resolution)
  and ParseSheet utility for header-validated Excel sheet parsing with empty-row skipping.
- costproductmaster.Repository: add ProductUpsertInput/ProductUpsertResult types and
  BulkUpsertByLegacyID method using cpm_flex_02 as upsert conflict key, batched at 200.
- costroute.Repository: add HeadUpsertInput/Result, SeqUpsertInput/Result, RMInput types
  and BulkUpsertHeads/BulkUpsertSeqs/BulkReplaceRMs methods for import-driven bulk writes.
- All three new repo methods implemented in postgres layer; existing fakeRepoForDup test
  stub extended to satisfy the updated interface.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…terfaces

- parser.go: simplify header search loop using slices.Contains (gocritic/intrange lint)
- cost_product_master_repository.go: replace manual min-clamp with min() builtin (gocritic/ifElseChain lint)

go build + go vet: 0 errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cost_product_code arity, ON CONFLICT predicates, and Skipped logic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pp, capp (task-2b)

- Add CPPUpsertInput and CAPPUpsertInput types to costproductparameter domain
- Add BulkUpsertValues and BulkUpsertApplicable to Repository interface
- Implement both bulk upsert methods in postgres repo (batches of 200, xmax insert detection)
- Create sheet_product_master.go, sheet_cpp.go, sheet_capp.go processors
- Add stub methods to fakeRepo in handlers_test.go

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add sheet_route_head.go, sheet_route_seq.go, and sheet_route_rm.go to the
costbulkimport package, implementing processRouteHead, processRouteSeq, and
processRouteRM for Sheets 4–6 of the 6-sheet legacy Oracle import format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move repeated column header literals into a shared constants.go file in
the costbulkimport package to satisfy the goconst linter (3+ occurrences).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…and repo additions for bulk import (Task 2d)

- Add ListAllParams to costproductparameter.Repository + postgres impl
- Add ListAllActive to costproducttype.Repository + postgres impl
- Add ExportRouteHead/Seq/RM types + ListAllHeadsForExport/SeqsForExport/RMsForExport to costroute.Repository + postgres impl
- Add error_report.go: GenerateErrorReport builds multi-sheet Excel error report
- Add handler.go: BulkImportHandler.Handle processes 6-sheet import lifecycle PENDING→RUNNING→DONE/PARTIAL/FAILED
- Add validate_handler.go: ValidateHandler.Validate does synchronous dry-run with max 20 sample errors per sheet
- Add export_handler.go: ExportHandler.Handle generates 6-sheet Excel export from DB data, uploads to MinIO
- Add handler_test.go: tests for GenerateErrorReport, countErrors, appendIfUnderLimit
- Update test fakes in costroute, costproducttype, costproductparameter to implement new interface methods

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…unused fileName param

- ListAllParams: replace non-existent p.uom_code column reference with LEFT JOIN
  mst_uom u ON u.uom_id = p.uom_id, consistent with all other queries in the file
- ExportHandler: add typeRepo field + param to NewExportHandler; build typeIDToCode
  reverse map from ListAllActive; write product_type_code string instead of
  product_type_id int32 in product_master sheet header and data rows
- ExportHandler: apply ProductTypeCodes filter using typeCodeToID map when non-empty
- BulkImportHandler.Handle: log fileName at start to eliminate unparam CI lint failure
- ValidateHandler.validateProductMaster: remove unused _ time.Time parameter and
  remove time import + now := time.Now() in Validate() to fix unparam

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nto gRPC delivery and worker

- cost_import_handler.go: add ImportBulkProductRouting, ValidateBulkProductRoutingFile, ExportBulkProductRouting methods; add validateBulkHandler field; add toProtoBulkSheetResult and marshalExportRequestKey helpers
- costing_import_handler.go: add bulkImportHandler/bulkExportHandler fields; add EntityBulkProductRouting and EntityBulkProductRoutingExport dispatch cases; add unmarshalExportRequest helper
- cmd/server/main.go: instantiate bulkValidateH and pass to NewCostDataImportHandler
- cmd/worker/main.go: instantiate bulkImportHandler, bulkExportHandler and pass to NewCostingImportHandler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…uards in gRPC handler

Dead bulkValidateHandler instantiation in cmd/worker/main.go was created
only to be suppressed with _ = — it is wired by the server, not the worker.
Nil-guards on validateBulkHandler and importPublisher in cost_import_handler.go
masked programming errors that should fail loudly at startup or return a real error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…emove dead rowNums

- Add RouteLevel/RouteSeq fields to ExportRouteRM struct
- JOIN cost_route_seq in ListAllRMsForExport to populate those fields
- Write rm.RouteLevel/rm.RouteSeq in writeRouteRMSheet instead of empty strings
- Add lowercase JSON tags to ExportRequest struct
- Remove dead rowNums slice in processProductMaster (built but never used)

Fix 2 (partial unique index on crh_product_sys_id): already exists as
uk_cost_route_head_active_per_product in migration 000222 — no action needed.
Fix 3 (MarkPartial): MarkDone() already handles PARTIAL status internally
when j.failed > 0 — no separate MarkPartial method needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… download

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ities

The original constraint (migration 000377) only allowed 5 values and did
not include the two bulk routing entities added later — causing INSERT
failures when queuing a bulk export or import job.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Export jobs store request parameters as JSON in the file_key column
(not a MinIO object path). The worker was calling fetchFile() for all
entity types unconditionally, causing a "stat object: key does not
exist" error for every bulk export job.

Fix: early-dispatch EntityBulkProductRoutingExport via a dedicated
handleExport() method before the fetchFile path. Import jobs are
unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…seed

- cost_import_handler: presigned URL now uses entity-specific filename;
  export jobs download as bulk_product_routing_export.xlsx instead of
  the misleading import_errors.xlsx
- IAM migration 000061: seed FINANCE_IMPORT_JOBS menu (level-3 under
  FINANCE_PRODUCT_COSTING, no permission entries = visible to all
  authenticated users)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ilramdhan ilramdhan added this to the Costing Release Milestone milestone Jun 22, 2026
@ilramdhan ilramdhan self-assigned this Jun 22, 2026
@ilramdhan ilramdhan added bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request feat labels Jun 22, 2026
@ilramdhan ilramdhan moved this from Todo to In progress in Goapps Roadmap [Backend] Jun 22, 2026
ilramdhan and others added 2 commits June 22, 2026 23:01
- errcheck: expand all `_ = f.Close()` defers to `if err := f.Close(); err != nil { _ = err }`
- errcheck: propagate SetCellValue/DeleteSheet/CoordinatesToCellName errors instead of blank-assigning
- goconst: add boolTrueStr const and replace 4 occurrences of "true" literal
- gocognit: extract filterProductsByTypeCode helper to reduce loadExportData complexity 25→≤20
- gocognit: add nolint annotations to 3 cohesive row-validation pipelines (21-23)
- unparam: annotate appendIfUnderLimit limit param with nolint
- gofmt: fix formatting in costroute/types.go, cost_route_repository.go, costing_import_handler.go

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- gofmt: fix formatting in sheet_route_rm, sheet_route_head, export_handler,
  sheet_route_seq, validate_handler
- gocyclo: add nolint annotation to processRouteRM (cyclomatic 16 > threshold 15)
- gocognit: extract populateTemplateSheet helper to reduce TemplateHandler.Handle
  complexity from 23 to ≤20

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ilramdhan ilramdhan merged commit a6daa67 into mutugading:main Jun 22, 2026
19 checks passed
@github-project-automation github-project-automation Bot moved this from In progress to Done in Goapps Roadmap [Backend] Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request feat

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant