diff --git a/buf.gen.yaml b/buf.gen.yaml index 5c44e8c..f8f3a0e 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -15,29 +15,32 @@ managed: path: . plugins: # =========================================================================== - # Go Backend Generation + # Go Backend Generation (LOCAL binaries installed via `go install`). + # buf.build remote plugins were unreachable during S7; local binaries remove + # the runtime dependency on the BSR. To revert, swap each + # `local: protoc-gen-XXX` back to `remote: buf.build//`. # =========================================================================== # Go protobuf structs - - remote: buf.build/protocolbuffers/go + - local: protoc-gen-go out: ../goapps-backend/gen opt: - paths=source_relative # gRPC service stubs - - remote: buf.build/grpc/go + - local: protoc-gen-go-grpc out: ../goapps-backend/gen opt: - paths=source_relative # gRPC-Gateway for REST API - - remote: buf.build/grpc-ecosystem/gateway + - local: protoc-gen-grpc-gateway out: ../goapps-backend/gen opt: - paths=source_relative # OpenAPI/Swagger documentation - - remote: buf.build/grpc-ecosystem/openapiv2 + - local: protoc-gen-openapiv2 out: ../goapps-backend/gen/openapi inputs: diff --git a/finance/v1/cost_attachment.proto b/finance/v1/cost_attachment.proto new file mode 100644 index 0000000..c3b9baf --- /dev/null +++ b/finance/v1/cost_attachment.proto @@ -0,0 +1,111 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// CostAttachment — PRD Phase A §7.1.10 (CA_). +// Generic file attachment that hangs off either a request OR a comment (XOR). +// Storage: MinIO via finance.internal/infrastructure/storage. The proto carries +// raw bytes on upload (max 25MB per FR-5); download is via presigned URL. +// ============================================================================= + +message CostAttachment { + int64 attachment_id = 1; + int64 request_id = 2; // set when request-level + int64 comment_id = 3; // set when comment-level + string filename = 4; + string mime_type = 5; + int64 size_bytes = 6; + string storage_key = 7; + string uploaded_by = 8; + string uploaded_at = 9; +} + +// ============================================================================= +// Upload +// ============================================================================= + +message UploadCostAttachmentRequest { + // Exactly one of (request_id, comment_id) must be > 0. + int64 request_id = 1; + int64 comment_id = 2; + string filename = 3 [(buf.validate.field).string = { + min_len: 1 + max_len: 255 + }]; + string mime_type = 4 [(buf.validate.field).string = { + min_len: 1 + max_len: 100 + }]; + // Max 25 MB per FR-5. + bytes file_content = 5 [(buf.validate.field).bytes = { + min_len: 1 + max_len: 26214400 + }]; +} + +message UploadCostAttachmentResponse { + common.v1.BaseResponse base = 1; + CostAttachment data = 2; +} + +message ListCostAttachmentsByRequestRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; +} + +message ListCostAttachmentsByRequestResponse { + common.v1.BaseResponse base = 1; + repeated CostAttachment data = 2; +} + +message ListCostAttachmentsByCommentRequest { + int64 comment_id = 1 [(buf.validate.field).int64.gte = 1]; +} + +message ListCostAttachmentsByCommentResponse { + common.v1.BaseResponse base = 1; + repeated CostAttachment data = 2; +} + +message GetCostAttachmentDownloadURLRequest { + int64 attachment_id = 1 [(buf.validate.field).int64.gte = 1]; +} + +message GetCostAttachmentDownloadURLResponse { + common.v1.BaseResponse base = 1; + string url = 2; + int32 valid_seconds = 3; +} + +message DeleteCostAttachmentRequest { + int64 attachment_id = 1 [(buf.validate.field).int64.gte = 1]; +} + +message DeleteCostAttachmentResponse { + common.v1.BaseResponse base = 1; +} + +service CostAttachmentService { + rpc UploadCostAttachment(UploadCostAttachmentRequest) returns (UploadCostAttachmentResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-attachments/upload" + body: "*" + }; + } + rpc ListCostAttachmentsByRequest(ListCostAttachmentsByRequestRequest) returns (ListCostAttachmentsByRequestResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-requests/{request_id}/attachments"}; + } + rpc ListCostAttachmentsByComment(ListCostAttachmentsByCommentRequest) returns (ListCostAttachmentsByCommentResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-request-comments/{comment_id}/attachments"}; + } + rpc GetCostAttachmentDownloadURL(GetCostAttachmentDownloadURLRequest) returns (GetCostAttachmentDownloadURLResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-attachments/{attachment_id}/download-url"}; + } + rpc DeleteCostAttachment(DeleteCostAttachmentRequest) returns (DeleteCostAttachmentResponse) { + option (google.api.http) = {delete: "/api/v1/finance/cost-attachments/{attachment_id}"}; + } +} diff --git a/finance/v1/cost_audit_log.proto b/finance/v1/cost_audit_log.proto new file mode 100644 index 0000000..244f3cb --- /dev/null +++ b/finance/v1/cost_audit_log.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// CostAuditLog — PRD Phase A §7.1.14 (CAL_). +// Append-only audit trail. Writes happen inside the business handlers (e.g., +// CostProductRequestService.DecideFeasibility appends a CAL_ row). This service +// exposes READ-ONLY queries for admin viewing — there is no CreateAuditLog RPC. +// ============================================================================= + +message CostAuditLog { + int64 log_id = 1; + string entity_type = 2; + int64 entity_id = 3; + string operation = 4; + // before/after data as JSON-stringified objects. + string before_data = 5; + string after_data = 6; + string user_id = 7; + string performed_at = 8; +} + +message ListCostAuditLogsRequest { + // Optional filters; all return everything when empty. + string entity_type = 1 [(buf.validate.field).string.max_len = 50]; + int64 entity_id = 2; + string user_id = 3 [(buf.validate.field).string.max_len = 64]; + string operation = 4 [(buf.validate.field).string.max_len = 30]; + // ISO date YYYY-MM-DD inclusive. + string from_date = 5 [(buf.validate.field).string.max_len = 10]; + string to_date = 6 [(buf.validate.field).string.max_len = 10]; + common.v1.PaginationRequest pagination = 7; +} + +message ListCostAuditLogsResponse { + common.v1.BaseResponse base = 1; + repeated CostAuditLog data = 2; + common.v1.PaginationResponse pagination = 3; +} + +service CostAuditLogService { + rpc ListCostAuditLogs(ListCostAuditLogsRequest) returns (ListCostAuditLogsResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-audit-logs"}; + } +} diff --git a/finance/v1/cost_calc.proto b/finance/v1/cost_calc.proto new file mode 100644 index 0000000..b11c334 --- /dev/null +++ b/finance/v1/cost_calc.proto @@ -0,0 +1,719 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; + +// CalculationType selects the costing flavor for a job/result. +enum CalculationType { + // Default - invalid for create/update; ignored as filter. + CALCULATION_TYPE_UNSPECIFIED = 0; + // Actual cost based on realized RM rates. + CALCULATION_TYPE_ACTUAL = 1; + // Forecast cost using forward-looking rates. + CALCULATION_TYPE_FORECAST = 2; + // Selling-side cost projection. + CALCULATION_TYPE_SELLING = 3; +} + +// CalcJobStatus is the lifecycle status of a calc job batch. +enum CalcJobStatus { + // Default - invalid for create/update; ignored as filter. + CALC_JOB_STATUS_UNSPECIFIED = 0; + // Job created, waiting for planner. + CALC_JOB_STATUS_QUEUED = 1; + // Planner is expanding products + chunks. + CALC_JOB_STATUS_PLANNING = 2; + // Chunks dispatched, workers running. + CALC_JOB_STATUS_PROCESSING = 3; + // All products calculated successfully. + CALC_JOB_STATUS_SUCCESS = 4; + // Some products failed, others succeeded. + CALC_JOB_STATUS_PARTIAL_FAILED = 5; + // Job failed at planning or all chunks failed. + CALC_JOB_STATUS_FAILED = 6; + // Operator cancelled the job. + CALC_JOB_STATUS_CANCELLED = 7; +} + +// CalcJobScope describes what set of products the job covers. +enum CalcJobScope { + // Default - invalid for trigger. + CALC_JOB_SCOPE_UNSPECIFIED = 0; + // All eligible products for the period. + CALC_JOB_SCOPE_ALL = 1; + // Subset selected by product_filter_json. + CALC_JOB_SCOPE_FILTERED = 2; + // A single product target. + CALC_JOB_SCOPE_SINGLE_PRODUCT = 3; + // A single route head target. + CALC_JOB_SCOPE_SINGLE_ROUTE = 4; +} + +// ChunkStatus is the lifecycle status of an individual chunk. +enum ChunkStatus { + // Default - invalid for create/update. + CHUNK_STATUS_UNSPECIFIED = 0; + // Chunk queued, not yet dispatched. + CHUNK_STATUS_QUEUED = 1; + // Chunk dispatched to a worker. + CHUNK_STATUS_DISPATCHED = 2; + // Worker is processing the chunk. + CHUNK_STATUS_PROCESSING = 3; + // All products in chunk succeeded. + CHUNK_STATUS_SUCCESS = 4; + // Some products in chunk failed. + CHUNK_STATUS_PARTIAL_FAILED = 5; + // All products in chunk failed. + CHUNK_STATUS_FAILED = 6; +} + +// JobProductStatus is the per-product execution status inside a job. +enum JobProductStatus { + // Default - invalid for create/update. + JOB_PRODUCT_STATUS_UNSPECIFIED = 0; + // Awaiting upstream waves to finish. + JOB_PRODUCT_STATUS_PENDING = 1; + // Inputs ready, awaiting worker. + JOB_PRODUCT_STATUS_READY = 2; + // Worker calculating now. + JOB_PRODUCT_STATUS_CALCULATING = 3; + // Cost calculated successfully. + JOB_PRODUCT_STATUS_SUCCESS = 4; + // Calculation failed for this product. + JOB_PRODUCT_STATUS_FAILED = 5; + // Blocked by upstream failure or missing inputs. + JOB_PRODUCT_STATUS_BLOCKED = 6; + // Skipped (e.g., no active route). + JOB_PRODUCT_STATUS_SKIPPED = 7; +} + +// CostResultStatus is the lifecycle status of a stored cost result. +enum CostResultStatus { + // Default - invalid for create/update. + COST_RESULT_STATUS_UNSPECIFIED = 0; + // Freshly calculated, awaiting review. + COST_RESULT_STATUS_CALCULATED = 1; + // Verified by a reviewer. + COST_RESULT_STATUS_VERIFIED = 2; + // Approved for downstream use. + COST_RESULT_STATUS_APPROVED = 3; + // Replaced by a newer version. + COST_RESULT_STATUS_SUPERSEDED = 4; +} + +// CalJob represents a calculation job batch. +message CalJob { + // Auto-increment job id. + int64 job_id = 1; + // Human-readable job code (e.g., CJ-202604-ACTUAL-001). + string job_code = 2; + // Period in YYYYMM format. + string period = 3; + // Calculation flavor. + CalculationType calculation_type = 4; + // Scope of products this job covers. + CalcJobScope scope = 5; + // Raw JSON of cj_product_filter (filter criteria when scope=FILTERED). + string product_filter_json = 6; + // Current lifecycle status. + CalcJobStatus status = 7; + // Higher value = higher priority for dispatcher. + int32 priority = 8; + // Total products targeted by this job. + int32 total_products = 9; + // Total chunks planned. + int32 total_chunks = 10; + // Total waves (dependency levels) planned. + int32 total_waves = 11; + // Chunks completed so far. + int32 processed_chunks = 12; + // Successful product calculations. + int32 success_count = 13; + // Failed product calculations. + int32 failed_count = 14; + // Blocked products count. + int32 blocked_count = 15; + // Raw JSON summarizing errors (top error categories + counts). + string error_summary_json = 16; + // Trigger source: MANUAL | CRON | API. + string triggered_by = 17; + // When job was queued. + google.protobuf.Timestamp queued_at = 18; + // When planner started. + google.protobuf.Timestamp started_at = 19; + // When job ended (success/fail/cancelled). + google.protobuf.Timestamp completed_at = 20; + // Total duration in milliseconds. + int64 duration_ms = 21; + // User who triggered the job (resolved label, not UUID). + string created_by = 22; +} + +// CalJobChunk is one batch of products dispatched to a worker. +message CalJobChunk { + // Auto-increment chunk id. + int64 chunk_id = 1; + // Parent job id. + int64 job_id = 2; + // Sequential chunk number within the job. + int32 chunk_number = 3; + // Wave (dependency level) this chunk belongs to. + int32 wave_no = 4; + // Product ids contained in this chunk. + repeated int64 product_ids = 5; + // Count of products in this chunk. + int32 product_count = 6; + // Current chunk status. + ChunkStatus status = 7; + // Worker pod/id that picked up the chunk. + string worker_id = 8; + // When chunk was queued. + google.protobuf.Timestamp queued_at = 9; + // When dispatched to a worker. + google.protobuf.Timestamp dispatched_at = 10; + // When worker started processing. + google.protobuf.Timestamp started_at = 11; + // When worker finished. + google.protobuf.Timestamp completed_at = 12; + // Duration in milliseconds. + int32 duration_ms = 13; + // Successful products in chunk. + int32 success_count = 14; + // Failed products in chunk. + int32 failed_count = 15; + // Chunk-level error message (planning/dispatch). + string error_message = 16; + // Retry attempt count. + int32 retry_count = 17; + // Max retries allowed before giving up. + int32 max_retries = 18; +} + +// CalJobProduct is per-product execution row inside a job. +message CalJobProduct { + // Auto-increment job-product id. + int64 job_product_id = 1; + // Parent job id. + int64 job_id = 2; + // Chunk this product was assigned to. + int64 chunk_id = 3; + // Product sys id (numeric internal id). + int64 product_sys_id = 4; + // Resolved product code (NEVER expose UUID — see UX rule). + string product_code = 5; + // Resolved product name for UI. + string product_name = 6; + // Route head used for this calculation. + int64 route_head_id = 7; + // Wave (dependency level). + int32 wave_no = 8; + // Current product status. + JobProductStatus status = 9; + // Reason if status=BLOCKED. + string block_reason = 10; + // When calculation started. + google.protobuf.Timestamp started_at = 11; + // When calculation finished. + google.protobuf.Timestamp completed_at = 12; + // Duration in milliseconds. + int32 duration_ms = 13; + // Cost result id if status=SUCCESS. + int64 cost_id = 14; + // Error message if status=FAILED. + string error_message = 15; + // Raw JSON calculation log (formula trace + intermediate values). + string calculation_log_json = 16; +} + +// CostResult is the persisted cost summary for a product/period/type. +message CostResult { + // Auto-increment cost id. + int64 cost_id = 1; + // Product sys id. + int64 product_sys_id = 2; + // Resolved product code. + string product_code = 3; + // Resolved product name. + string product_name = 4; + // Period in YYYYMM format. + string period = 5; + // Calculation flavor for this row. + CalculationType calculation_type = 6; + // Route head used for the calculation. + int64 route_head_id = 7; + // Version (auto-increment per (product, period, type)). + int32 version = 8; + // Cost per unit (NUMERIC(20,6) serialized as string). + string cost_per_unit = 9; + // Aggregate RM cost for this product (sum of unit × ratio across stages). + string total_rm_cost = 10; + // Aggregate conversion cost across stages. + string total_conversion = 11; + // Total cost (rm + conversion). + string total_cost = 12; + // UOM id of the unit. + int32 uom_id = 13; + // Resolved UOM code. + string uom_code = 14; + // Currency ISO code. + string currency_code = 15; + // Result lifecycle status. + CostResultStatus status = 16; + // Job id that produced this row. + int64 job_id = 17; + // When calculation completed. + google.protobuf.Timestamp calculated_at = 18; + // User who triggered the calc (resolved label). + string calculated_by = 19; + // When verified (if applicable). + google.protobuf.Timestamp verified_at = 20; + // User who verified (resolved label). + string verified_by = 21; +} + +// CostBreakdown is the full drill-down for one CostResult. +message CostBreakdown { + // Summary row. + CostResult summary = 1; + // Cost contribution per route level. + repeated LevelBreakdown by_level = 2; + // RM input details. + repeated CostRMDetail rm_details = 3; + // Formula evaluation trace. + repeated FormulaEval formula_trace = 4; + // Snapshot of parameters used (key = param_code, val = stringified float). + map param_snapshot = 5; +} + +// LevelBreakdown is the cost contribution at one route level. +message LevelBreakdown { + // Route level (1 = FG; 2..N upstream). + int32 level = 1; + // Product sys id at this level. + int64 product_sys_id = 2; + // Resolved product code. + string product_code = 3; + // Resolved product name. + string product_name = 4; + // Cost contribution (stringified NUMERIC). + string cost_contribution = 5; + // Ratio of this level to total cost. + string ratio = 6; +} + +// CostRMDetail is one RM input edge contribution to the cost. +message CostRMDetail { + // RM type: PRODUCT | ITEM | GROUP. + string rm_type = 1; + // Reference code: product_code for PRODUCT, item_code for ITEM, group_code for GROUP. + string ref_code = 2; + // Resolved display label for the ref. + string ref_label = 3; + // Shade code (if applicable). + string shade_code = 4; + // Unit cost (stringified NUMERIC). + string unit_cost = 5; + // Consumption ratio. + string ratio = 6; + // Contribution = unit_cost × ratio. + string contribution = 7; +} + +// FormulaEval traces one formula evaluation step. +message FormulaEval { + // Formula code. + string formula_code = 1; + // Formula human name. + string formula_name = 2; + // Expression evaluated. + string expression = 3; + // Inputs (key = param_code, val = stringified value). + map inputs = 4; + // Output parameter code this formula assigns. + string output_param_code = 5; + // Output value (stringified NUMERIC). + string output_value = 6; +} + +// CostHistoryEntry is one row in the versioned cost history. +message CostHistoryEntry { + // Cost id of this history entry. + int64 cost_id = 1; + // Period in YYYYMM format. + string period = 2; + // Calculation flavor. + CalculationType calculation_type = 3; + // Version number. + int32 version = 4; + // Cost per unit (stringified NUMERIC). + string cost_per_unit = 5; + // Variance percentage vs the previous version (sign included, stringified). + string variance_pct_from_previous = 6; + // Lifecycle status. + CostResultStatus status = 7; + // Job id that produced this row. + int64 job_id = 8; + // When calculation completed. + google.protobuf.Timestamp calculated_at = 9; + // User who triggered the calc (resolved label). + string calculated_by = 10; +} + +// ---- Requests / responses ---- + +// TriggerCalcJobRequest creates a new calc job batch. +message TriggerCalcJobRequest { + // Period in YYYYMM format (e.g., 202604). + string period = 1 [(buf.validate.field).string.pattern = "^[0-9]{6}$"]; + // Calculation flavor; cannot be UNSPECIFIED. + CalculationType calculation_type = 2 [(buf.validate.field).enum = { + not_in: [0] + }]; + // Scope; cannot be UNSPECIFIED. + CalcJobScope scope = 3 [(buf.validate.field).enum = { + not_in: [0] + }]; + // For SINGLE_PRODUCT, set product_sys_id. Otherwise leave 0. + int64 product_sys_id = 4; + // For SINGLE_ROUTE, set route_head_id. Otherwise leave 0. + int64 route_head_id = 5; + // For FILTERED, set product_type_id_filter. Otherwise leave 0. + int32 product_type_id_filter = 6; +} + +// TriggerCalcJobResponse returns the created job. +message TriggerCalcJobResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Created job. + CalJob job = 2; +} + +// GetCalcJobRequest fetches one job by id. +message GetCalcJobRequest { + // Job id; must be > 0. + int64 job_id = 1 [(buf.validate.field).int64.gt = 0]; +} + +// GetCalcJobResponse returns the requested job. +message GetCalcJobResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Requested job. + CalJob job = 2; +} + +// ListCalcJobsRequest lists jobs with filters and pagination. +message ListCalcJobsRequest { + // Pagination params. + common.v1.PaginationRequest pagination = 1; + // Optional period filter (YYYYMM). + string period = 2; + // Optional calculation type filter (UNSPECIFIED = no filter). + CalculationType calculation_type = 3; + // Optional status filter (UNSPECIFIED = no filter). + CalcJobStatus status = 4; + // Optional triggered_by filter (e.g., MANUAL/CRON/API). + string triggered_by = 5; +} + +// ListCalcJobsResponse returns a page of jobs. +message ListCalcJobsResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Page of jobs. + repeated CalJob items = 2; + // Pagination metadata. + common.v1.PaginationResponse pagination = 3; +} + +// ListCalcJobChunksRequest lists chunks for one job. +message ListCalcJobChunksRequest { + // Parent job id. + int64 job_id = 1 [(buf.validate.field).int64.gt = 0]; + // Pagination params. + common.v1.PaginationRequest pagination = 2; + // Optional wave filter (0 = no filter). + int32 wave_no = 3; + // Optional chunk status filter (UNSPECIFIED = no filter). + ChunkStatus status = 4; +} + +// ListCalcJobChunksResponse returns a page of chunks. +message ListCalcJobChunksResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Page of chunks. + repeated CalJobChunk items = 2; + // Pagination metadata. + common.v1.PaginationResponse pagination = 3; +} + +// ListCalcJobProductsRequest lists per-product execution rows for one job. +message ListCalcJobProductsRequest { + // Parent job id. + int64 job_id = 1 [(buf.validate.field).int64.gt = 0]; + // Pagination params. + common.v1.PaginationRequest pagination = 2; + // Optional product status filter (UNSPECIFIED = no filter). + JobProductStatus status = 3; +} + +// ListCalcJobProductsResponse returns a page of product-level execution rows. +message ListCalcJobProductsResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Page of product execution rows. + repeated CalJobProduct items = 2; + // Pagination metadata. + common.v1.PaginationResponse pagination = 3; +} + +// CancelCalcJobRequest cancels a running job. +message CancelCalcJobRequest { + // Job id; must be > 0. + int64 job_id = 1 [(buf.validate.field).int64.gt = 0]; + // Optional human reason for the cancellation. + string reason = 2; +} + +// CancelCalcJobResponse returns the cancelled job. +message CancelCalcJobResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Cancelled job. + CalJob job = 2; +} + +// GetCostResultRequest fetches the active cost result for a product/period/type. +message GetCostResultRequest { + // Product sys id; must be > 0. + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; + // Period in YYYYMM format. + string period = 2 [(buf.validate.field).string.pattern = "^[0-9]{6}$"]; + // Calculation flavor; cannot be UNSPECIFIED. + CalculationType calculation_type = 3 [(buf.validate.field).enum = { + not_in: [0] + }]; +} + +// GetCostResultResponse returns the requested cost result. +message GetCostResultResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Active cost result row. + CostResult result = 2; +} + +// ListCostResultsRequest lists active (non-superseded) cost results across +// products for a period + calc type, with optional product search + status. +// All filters are optional: empty period means "latest available period". +message ListCostResultsRequest { + // Pagination params. + common.v1.PaginationRequest pagination = 1; + // Period in YYYYMM (empty = latest period present in cst_product_cost). + string period = 2 [(buf.validate.field).string = {pattern: "^$|^[0-9]{6}$"}]; + // Calculation type filter (UNSPECIFIED = no filter). + CalculationType calculation_type = 3; + // Product code/name search (case-insensitive, optional). + string search = 4 [(buf.validate.field).string.max_len = 100]; + // Result status filter (UNSPECIFIED = active only, i.e. exclude SUPERSEDED). + CostResultStatus status = 5; +} + +// ListCostResultsResponse returns a page of cost results across products. +message ListCostResultsResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Page of cost result rows (resolved product code/name/uom; no UUIDs). + repeated CostResult items = 2; + // Pagination metadata. + common.v1.PaginationResponse pagination = 3; + // The period actually used (resolved when request period was empty). + string resolved_period = 4; +} + +// GetCostBreakdownRequest fetches the full drill-down for a cost. +message GetCostBreakdownRequest { + // Product sys id; must be > 0. + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; + // Period in YYYYMM format. + string period = 2 [(buf.validate.field).string.pattern = "^[0-9]{6}$"]; + // Calculation flavor; cannot be UNSPECIFIED. + CalculationType calculation_type = 3 [(buf.validate.field).enum = { + not_in: [0] + }]; +} + +// GetCostBreakdownResponse returns the cost breakdown. +message GetCostBreakdownResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Full breakdown: summary + by-level + rm details + formula trace. + CostBreakdown breakdown = 2; +} + +// ListCostHistoryRequest lists versioned cost history for a product. +message ListCostHistoryRequest { + // Product sys id; must be > 0. + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; + // Pagination params. + common.v1.PaginationRequest pagination = 2; + // Optional calculation type filter (UNSPECIFIED = no filter). + CalculationType calculation_type = 3; +} + +// ListCostHistoryResponse returns a page of cost history rows. +message ListCostHistoryResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Page of history rows (includes superseded). + repeated CostHistoryEntry items = 2; + // Pagination metadata. + common.v1.PaginationResponse pagination = 3; +} + +// VerifyCostResultRequest marks a cost result as verified. +message VerifyCostResultRequest { + // Cost id; must be > 0. + int64 cost_id = 1 [(buf.validate.field).int64.gt = 0]; +} + +// VerifyCostResultResponse returns the verified cost result. +message VerifyCostResultResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Updated cost result. + CostResult result = 2; +} + +// ApproveCostResultRequest marks a cost result as approved. +message ApproveCostResultRequest { + // Cost id; must be > 0. + int64 cost_id = 1 [(buf.validate.field).int64.gt = 0]; +} + +// ApproveCostResultResponse returns the approved cost result. +message ApproveCostResultResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Updated cost result. + CostResult result = 2; +} + +// ProcessChunkInternalRequest carries the data the worker needs to compute +// one chunk of products. Internal RPC: only finance-cost-worker invokes it. +message ProcessChunkInternalRequest { + // Job id this chunk belongs to; must be > 0. + int64 job_id = 1 [(buf.validate.field).int64.gt = 0]; + // Chunk id (cal_job_chunk.cjc_chunk_id) -- the row this chunk updates. + int64 chunk_id = 2 [(buf.validate.field).int64.gt = 0]; + // Period in YYYYMM format (e.g., 202604). + string period = 3 [(buf.validate.field).string.pattern = "^[0-9]{6}$"]; + // Calculation flavor; cannot be UNSPECIFIED. + CalculationType calculation_type = 4 [(buf.validate.field).enum = { + not_in: [0] + }]; + // Product sys ids to compute; at least one required. + repeated int64 product_ids = 5 [(buf.validate.field).repeated.min_items = 1]; + // Actor (worker id) recorded in audit + result rows. + string actor = 6 [(buf.validate.field).string.min_len = 1]; +} + +// ProcessChunkInternalResponse summarizes per-status counts. +message ProcessChunkInternalResponse { + // Standard response envelope. + common.v1.BaseResponse base = 1; + // Number of products that computed + persisted successfully. + int32 success_count = 2; + // Number of products that failed with non-recoverable errors. + int32 failed_count = 3; + // Number of products that were BLOCKED (missing CAPP, missing RM cost, etc.). + int32 blocked_count = 4; +} + +// CostCalcService runs the cost calculation engine and exposes results. +service CostCalcService { + // TriggerCalcJob creates a new calc job batch. + // Scope determines whether it covers a single product, a route, a filtered + // set, or ALL products. Required permission: finance.cost.caljob.trigger. + rpc TriggerCalcJob(TriggerCalcJobRequest) returns (TriggerCalcJobResponse) { + option (google.api.http) = { + post: "/api/v1/finance/calc-jobs/trigger" + body: "*" + }; + } + // GetCalcJob fetches one calc job by id. + // Required permission: finance.cost.caljob.view. + rpc GetCalcJob(GetCalcJobRequest) returns (GetCalcJobResponse) { + option (google.api.http) = {get: "/api/v1/finance/calc-jobs/{job_id}"}; + } + // ListCalcJobs returns a paginated list of calc jobs with optional filters. + // Required permission: finance.cost.caljob.view. + rpc ListCalcJobs(ListCalcJobsRequest) returns (ListCalcJobsResponse) { + option (google.api.http) = {get: "/api/v1/finance/calc-jobs"}; + } + // ListCalcJobChunks returns chunks belonging to one job. + // Required permission: finance.cost.caljob.view. + rpc ListCalcJobChunks(ListCalcJobChunksRequest) returns (ListCalcJobChunksResponse) { + option (google.api.http) = {get: "/api/v1/finance/calc-jobs/{job_id}/chunks"}; + } + // ListCalcJobProducts returns per-product execution rows for one job. + // Required permission: finance.cost.caljob.view. + rpc ListCalcJobProducts(ListCalcJobProductsRequest) returns (ListCalcJobProductsResponse) { + option (google.api.http) = {get: "/api/v1/finance/calc-jobs/{job_id}/products"}; + } + // CancelCalcJob cancels a running calc job. + // Required permission: finance.cost.caljob.cancel. + rpc CancelCalcJob(CancelCalcJobRequest) returns (CancelCalcJobResponse) { + option (google.api.http) = { + post: "/api/v1/finance/calc-jobs/{job_id}/cancel" + body: "*" + }; + } + // GetCostResult returns the active cost result for a product/period/type. + // Required permission: finance.cost.result.view. + rpc GetCostResult(GetCostResultRequest) returns (GetCostResultResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-results/{product_sys_id}/{period}/{calculation_type}"}; + } + // ListCostResults lists active cost results across products for a period. + // Required permission: finance.cost.result.view. + rpc ListCostResults(ListCostResultsRequest) returns (ListCostResultsResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-results"}; + } + // GetCostBreakdown returns by-level + rm-details + formula-trace for a cost. + // Required permission: finance.cost.result.view. + rpc GetCostBreakdown(GetCostBreakdownRequest) returns (GetCostBreakdownResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-results/{product_sys_id}/{period}/{calculation_type}/breakdown"}; + } + // ListCostHistory returns versioned cost history (incl. superseded) for a product. + // Required permission: finance.cost.history.view. + rpc ListCostHistory(ListCostHistoryRequest) returns (ListCostHistoryResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-results/{product_sys_id}/history"}; + } + // VerifyCostResult marks a cost result as verified. + // Required permission: finance.cost.result.verify. + rpc VerifyCostResult(VerifyCostResultRequest) returns (VerifyCostResultResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-results/{cost_id}/verify" + body: "*" + }; + } + // ApproveCostResult marks a cost result as approved. + // Required permission: finance.cost.result.approve. + rpc ApproveCostResult(ApproveCostResultRequest) returns (ApproveCostResultResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-results/{cost_id}/approve" + body: "*" + }; + } + // ProcessChunkInternal computes one chunk of products synchronously. + // Invoked by finance-cost-worker after consuming a chunk message from RMQ. + // NOT exposed via REST gateway; intended for service-to-service traffic only. + // Required permission: finance.cost.caljob.trigger. + rpc ProcessChunkInternal(ProcessChunkInternalRequest) returns (ProcessChunkInternalResponse); +} diff --git a/finance/v1/cost_erp.proto b/finance/v1/cost_erp.proto new file mode 100644 index 0000000..f00b422 --- /dev/null +++ b/finance/v1/cost_erp.proto @@ -0,0 +1,135 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// CostErp{Item,Grade,Shade} — PRD Phase B §7.3. +// READ-ONLY replicas of Oracle ERP master data. Sync mechanism (CDC / scheduled +// job) is infrastructure-level and not exposed here. Only Get / List for lookup +// pickers (ProductMaster ERP linkage, BOM Store-Rate components, Phase A shade +// autocomplete). NO create/update/delete RPCs. +// ============================================================================= + +message CostErpItem { + int64 item_id = 1; + string item_code = 2; + string item_name = 3; + string item_type = 4; // POY/PTY/etc + bool is_active = 5; + string synced_at = 6; +} + +message CostErpGrade { + int32 grade_id = 1; + string grade_code = 2; + string grade_name = 3; + bool is_active = 4; + string synced_at = 5; +} + +message CostErpShade { + int32 shade_id = 1; + string shade_code = 2; + string shade_name = 3; + bool is_active = 4; + string synced_at = 5; +} + +// ============================================================================= +// ERP Item +// ============================================================================= + +message ListCostErpItemsRequest { + string search = 1 [(buf.validate.field).string.max_len = 200]; // matches code OR name + string item_type = 2 [(buf.validate.field).string.max_len = 10]; + string active_filter = 3 [(buf.validate.field).string = { + in: [ + "", + "all", + "active", + "inactive" + ] + }]; + common.v1.PaginationRequest pagination = 4; +} + +message ListCostErpItemsResponse { + common.v1.BaseResponse base = 1; + repeated CostErpItem data = 2; + common.v1.PaginationResponse pagination = 3; +} + +message GetCostErpItemRequest { + int64 item_id = 1 [(buf.validate.field).int64.gte = 1]; +} + +message GetCostErpItemResponse { + common.v1.BaseResponse base = 1; + CostErpItem data = 2; +} + +// ============================================================================= +// ERP Grade +// ============================================================================= + +message ListCostErpGradesRequest { + string search = 1 [(buf.validate.field).string.max_len = 200]; + string active_filter = 2 [(buf.validate.field).string = { + in: [ + "", + "all", + "active", + "inactive" + ] + }]; + common.v1.PaginationRequest pagination = 3; +} + +message ListCostErpGradesResponse { + common.v1.BaseResponse base = 1; + repeated CostErpGrade data = 2; + common.v1.PaginationResponse pagination = 3; +} + +// ============================================================================= +// ERP Shade +// ============================================================================= + +message ListCostErpShadesRequest { + string search = 1 [(buf.validate.field).string.max_len = 200]; + string active_filter = 2 [(buf.validate.field).string = { + in: [ + "", + "all", + "active", + "inactive" + ] + }]; + common.v1.PaginationRequest pagination = 3; +} + +message ListCostErpShadesResponse { + common.v1.BaseResponse base = 1; + repeated CostErpShade data = 2; + common.v1.PaginationResponse pagination = 3; +} + +// CostErpLookupService — read-only lookup for ERP replica tables. +service CostErpLookupService { + rpc ListCostErpItems(ListCostErpItemsRequest) returns (ListCostErpItemsResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-erp/items"}; + } + rpc GetCostErpItem(GetCostErpItemRequest) returns (GetCostErpItemResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-erp/items/{item_id}"}; + } + rpc ListCostErpGrades(ListCostErpGradesRequest) returns (ListCostErpGradesResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-erp/grades"}; + } + rpc ListCostErpShades(ListCostErpShadesRequest) returns (ListCostErpShadesResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-erp/shades"}; + } +} diff --git a/finance/v1/cost_notification.proto b/finance/v1/cost_notification.proto new file mode 100644 index 0000000..6b3737c --- /dev/null +++ b/finance/v1/cost_notification.proto @@ -0,0 +1,83 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// CostNotification — PRD Phase A §7.1.12 (CN_) + §7.1.13 (CNP_). +// In-app notification + read-state management. Email dispatch is a downstream +// concern handled by a worker that consumes cn_email_sent_at IS NULL rows. +// ============================================================================= + +message CostNotification { + int64 notification_id = 1; + string recipient_user_id = 2; + // STATUS_CHANGE | MENTION | ASSIGNED | FEASIBILITY | COMMENT_ADDED | + // ROUTING_PROMOTED | REQUEST_REJECTED | REQUEST_CLOSED. + string trigger_type = 3; + int64 request_id = 4; // 0 when not tied to a specific request + string payload = 5; // JSON-stringified + bool is_read = 6; + string email_sent_at = 7; + string created_at = 8; +} + +message ListMyCostNotificationsRequest { + // When true, only unread; else include all. + bool unread_only = 1; + common.v1.PaginationRequest pagination = 2; +} + +message ListMyCostNotificationsResponse { + common.v1.BaseResponse base = 1; + repeated CostNotification data = 2; + common.v1.PaginationResponse pagination = 3; + int32 unread_count = 4; +} + +message GetMyCostNotificationUnreadCountRequest {} + +message GetMyCostNotificationUnreadCountResponse { + common.v1.BaseResponse base = 1; + int32 unread_count = 2; +} + +message MarkCostNotificationReadRequest { + int64 notification_id = 1 [(buf.validate.field).int64.gte = 1]; +} + +message MarkCostNotificationReadResponse { + common.v1.BaseResponse base = 1; + CostNotification data = 2; +} + +message MarkAllMyCostNotificationsReadRequest {} + +message MarkAllMyCostNotificationsReadResponse { + common.v1.BaseResponse base = 1; + int32 updated_count = 2; +} + +service CostNotificationService { + rpc ListMyCostNotifications(ListMyCostNotificationsRequest) returns (ListMyCostNotificationsResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-notifications"}; + } + rpc GetMyCostNotificationUnreadCount(GetMyCostNotificationUnreadCountRequest) returns (GetMyCostNotificationUnreadCountResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-notifications/unread-count"}; + } + rpc MarkCostNotificationRead(MarkCostNotificationReadRequest) returns (MarkCostNotificationReadResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-notifications/{notification_id}/read" + body: "*" + }; + } + rpc MarkAllMyCostNotificationsRead(MarkAllMyCostNotificationsReadRequest) returns (MarkAllMyCostNotificationsReadResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-notifications/mark-all-read" + body: "*" + }; + } +} diff --git a/finance/v1/cost_paper_tube_type.proto b/finance/v1/cost_paper_tube_type.proto new file mode 100644 index 0000000..54c681d --- /dev/null +++ b/finance/v1/cost_paper_tube_type.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// CostPaperTubeType — PRD Phase A (CPTT_). +// Lookup used by cost_product_spec for paper-tube selection. +// ============================================================================= + +message CostPaperTubeType { + int32 paper_tube_type_id = 1; + string code = 2; + string display_name = 3; + bool is_active = 4; +} + +message ListCostPaperTubeTypesRequest { + string search = 1 [(buf.validate.field).string.max_len = 200]; + string active_filter = 2 [(buf.validate.field).string = { + in: [ + "", + "all", + "active", + "inactive" + ] + }]; + common.v1.PaginationRequest pagination = 3; +} + +message ListCostPaperTubeTypesResponse { + common.v1.BaseResponse base = 1; + repeated CostPaperTubeType data = 2; + common.v1.PaginationResponse pagination = 3; +} + +service CostPaperTubeTypeService { + rpc ListCostPaperTubeTypes(ListCostPaperTubeTypesRequest) returns (ListCostPaperTubeTypesResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-paper-tube-types"}; + } +} diff --git a/finance/v1/cost_product_master.proto b/finance/v1/cost_product_master.proto new file mode 100644 index 0000000..d8b9bdd --- /dev/null +++ b/finance/v1/cost_product_master.proto @@ -0,0 +1,203 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// CostProductMaster — PRD Phase B §7.4.1 (CPM_). +// Costing-system product identity, separate from ERP. Code auto-generated by +// server: CST + product_type.type_code + YYMM + LPAD(seq, 6, '0'). +// ERP linkage fields (erp_item_code / erp_grade_code_1 / erp_grade_code_2) are +// informational — populated AFTER the product appears in ERP. +// ============================================================================= + +message CostProductMaster { + int64 product_sys_id = 1; + string product_code = 2; // auto-generated, read-only + int32 product_type_id = 3; + string product_type_code = 4; // denormalized for UI display + string product_type_name = 5; + string product_name = 6; + string shade_code = 7; + string grade_code = 8; // default 'AX' + string description = 9; + string erp_item_code = 10; + string erp_grade_code_1 = 11; + string erp_grade_code_2 = 12; + string erp_linked_at = 13; + string erp_linked_by = 14; + bool is_active = 15; + common.v1.AuditInfo audit = 16; +} + +// ============================================================================= +// Create — server auto-generates product_code; client supplies type + name + shade + grade. +// ============================================================================= + +message CreateCostProductMasterRequest { + int32 product_type_id = 1 [(buf.validate.field).int32.gte = 1]; + string product_name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 500 + }]; + string shade_code = 3 [(buf.validate.field).string.max_len = 50]; + string grade_code = 4 [(buf.validate.field).string = { + min_len: 1 + max_len: 20 + }]; + string description = 5 [(buf.validate.field).string.max_len = 1000]; +} + +message CreateCostProductMasterResponse { + common.v1.BaseResponse base = 1; + CostProductMaster data = 2; +} + +message GetCostProductMasterRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gte = 1]; +} + +message GetCostProductMasterResponse { + common.v1.BaseResponse base = 1; + CostProductMaster data = 2; +} + +message GetCostProductMasterByCodeRequest { + string product_code = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 20 + }]; +} + +message GetCostProductMasterByCodeResponse { + common.v1.BaseResponse base = 1; + CostProductMaster data = 2; +} + +// ============================================================================= +// Update — product_code + product_type_id are immutable. +// ============================================================================= + +message UpdateCostProductMasterRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gte = 1]; + string product_name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 500 + }]; + string shade_code = 3 [(buf.validate.field).string.max_len = 50]; + string grade_code = 4 [(buf.validate.field).string = { + min_len: 1 + max_len: 20 + }]; + string description = 5 [(buf.validate.field).string.max_len = 1000]; +} + +message UpdateCostProductMasterResponse { + common.v1.BaseResponse base = 1; + CostProductMaster data = 2; +} + +// ============================================================================= +// Update ERP linkage — separate RPC because it's typically called later after +// the product appears in ERP. Records erp_linked_at / erp_linked_by. +// ============================================================================= + +message UpdateCostProductMasterErpLinkageRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gte = 1]; + string erp_item_code = 2 [(buf.validate.field).string.max_len = 20]; + string erp_grade_code_1 = 3 [(buf.validate.field).string.max_len = 20]; + string erp_grade_code_2 = 4 [(buf.validate.field).string.max_len = 20]; +} + +message UpdateCostProductMasterErpLinkageResponse { + common.v1.BaseResponse base = 1; + CostProductMaster data = 2; +} + +// ============================================================================= +// Deactivate — soft delete. Rejected if used as component in active product orders. +// ============================================================================= + +message DeactivateCostProductMasterRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gte = 1]; +} + +message DeactivateCostProductMasterResponse { + common.v1.BaseResponse base = 1; +} + +// ============================================================================= +// List — paginated browse with rich filters. +// ============================================================================= + +message ListCostProductMastersRequest { + string search = 1 [(buf.validate.field).string.max_len = 200]; // matches product_code OR product_name OR erp_item_code + int32 product_type_id = 2; + string shade_code = 3 [(buf.validate.field).string.max_len = 50]; + string active_filter = 4 [(buf.validate.field).string = { + in: [ + "", + "all", + "active", + "inactive" + ] + }]; + common.v1.PaginationRequest pagination = 5; + string sort_by = 6 [(buf.validate.field).string = { + in: [ + "", + "product_code", + "product_name", + "created_at" + ] + }]; + string sort_order = 7 [(buf.validate.field).string = { + in: [ + "", + "asc", + "desc" + ] + }]; +} + +message ListCostProductMastersResponse { + common.v1.BaseResponse base = 1; + repeated CostProductMaster data = 2; + common.v1.PaginationResponse pagination = 3; +} + +service CostProductMasterService { + rpc CreateCostProductMaster(CreateCostProductMasterRequest) returns (CreateCostProductMasterResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-master" + body: "*" + }; + } + rpc GetCostProductMaster(GetCostProductMasterRequest) returns (GetCostProductMasterResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-master/{product_sys_id}"}; + } + rpc GetCostProductMasterByCode(GetCostProductMasterByCodeRequest) returns (GetCostProductMasterByCodeResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-master/by-code/{product_code}"}; + } + rpc UpdateCostProductMaster(UpdateCostProductMasterRequest) returns (UpdateCostProductMasterResponse) { + option (google.api.http) = { + put: "/api/v1/finance/cost-product-master/{product_sys_id}" + body: "*" + }; + } + rpc UpdateCostProductMasterErpLinkage(UpdateCostProductMasterErpLinkageRequest) returns (UpdateCostProductMasterErpLinkageResponse) { + option (google.api.http) = { + put: "/api/v1/finance/cost-product-master/{product_sys_id}/erp-linkage" + body: "*" + }; + } + rpc DeactivateCostProductMaster(DeactivateCostProductMasterRequest) returns (DeactivateCostProductMasterResponse) { + option (google.api.http) = {delete: "/api/v1/finance/cost-product-master/{product_sys_id}"}; + } + rpc ListCostProductMasters(ListCostProductMastersRequest) returns (ListCostProductMastersResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-master"}; + } +} diff --git a/finance/v1/cost_product_parameter.proto b/finance/v1/cost_product_parameter.proto new file mode 100644 index 0000000..2462759 --- /dev/null +++ b/finance/v1/cost_product_parameter.proto @@ -0,0 +1,285 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// MESSAGES — Entity +// ============================================================================= + +// CostProductParameterValue is a single param value bound to a product. +// Exactly one of value_numeric / value_text / value_flag is populated per row, +// matching the data_type of the referenced parameter. +message CostProductParameterValue { + int64 value_id = 1; + int64 product_sys_id = 2; + // Parameter UUID from mst_parameter.id. + string param_id = 3; + // Resolved metadata (read-only join from mst_parameter). + string param_code = 4; + string param_name = 5; + string param_short_name = 6; + // NUMBER / TEXT / BOOLEAN — matches mst_parameter.data_type. + string data_type = 7; + string param_category = 8; + string uom_code = 9; + string owner_department = 10; + bool is_required_for_costing = 11; + string lookup_master_code = 12; + int32 display_order = 13; + string display_group = 14; + // Value columns — only one is populated per row. + string value_numeric = 20; + string value_text = 21; + bool value_flag = 22; + // Tracks who filled the value last. + string filled_at = 30; + string filled_by = 31; +} + +// RequiredParamEntry surfaces a required mst_parameter row that may or may not +// yet have a value bound for the product (used by ListProductRequiredParams). +message RequiredParamEntry { + string param_id = 1; + string param_code = 2; + string param_name = 3; + string param_short_name = 4; + string data_type = 5; + string param_category = 6; + string uom_code = 7; + string owner_department = 8; + bool is_required_for_costing = 9; + string lookup_master_code = 10; + int32 display_order = 11; + string display_group = 12; + // Existing value (zero/empty when not yet bound). + bool has_value = 20; + string value_numeric = 21; + string value_text = 22; + bool value_flag = 23; + string filled_at = 30; + string filled_by = 31; +} + +// ============================================================================= +// MESSAGES — List Required Params (per product) +// ============================================================================= + +message ListProductRequiredParamsRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; + // When TRUE, only is_required_for_costing rows are returned. Default FALSE = all. + bool required_only = 2; +} + +message ListProductRequiredParamsResponse { + common.v1.BaseResponse base = 1; + repeated RequiredParamEntry data = 2; +} + +// ============================================================================= +// MESSAGES — Upsert a single value +// ============================================================================= + +message UpsertProductParamValueRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; + string param_id = 2 [(buf.validate.field).string.uuid = true]; + // Value as string (decimals as text for precision). One of these must be set. + string value_numeric = 3; + string value_text = 4; + // value_flag is a real bool — we cannot distinguish "unset" from FALSE, so + // callers must use has_value_flag to opt in. + bool value_flag = 5; + bool has_value_flag = 6; +} + +message UpsertProductParamValueResponse { + common.v1.BaseResponse base = 1; + CostProductParameterValue data = 2; +} + +// ============================================================================= +// MESSAGES — Batch upsert (Save All from the Parameters tab) +// ============================================================================= + +message UpsertProductParamValuesBatchRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; + repeated UpsertProductParamValueRequest values = 2 [(buf.validate.field).repeated.min_items = 1]; +} + +message UpsertProductParamValuesBatchResponse { + common.v1.BaseResponse base = 1; + int32 upserted_count = 2; + int32 failed_count = 3; + repeated string failed_param_codes = 4; +} + +// ============================================================================= +// MESSAGES — Delete a value (clear back to "unbound") +// ============================================================================= + +message DeleteProductParamValueRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; + string param_id = 2 [(buf.validate.field).string.uuid = true]; +} + +message DeleteProductParamValueResponse { + common.v1.BaseResponse base = 1; +} + +// ============================================================================= +// MESSAGES — Missing required params check +// ============================================================================= + +message CheckMissingRequiredParamsRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; +} + +message MissingParam { + string param_id = 1; + string param_code = 2; + string param_name = 3; + string display_group = 4; +} + +message CheckMissingRequiredParamsResponse { + common.v1.BaseResponse base = 1; + // Empty slice = all required params are filled. + repeated MissingParam data = 2; +} + +// ============================================================================= +// MESSAGES — Applicability (CAPP_) per-product param subset +// ============================================================================= + +message AvailableParamEntry { + string param_id = 1; + string param_code = 2; + string param_name = 3; + string param_short_name = 4; + string data_type = 5; + string param_category = 6; + string uom_code = 7; + string owner_department = 8; + bool is_required_for_costing = 9; // global default + string lookup_master_code = 10; + int32 display_order = 11; + string display_group = 12; +} + +message ListAvailableParamsRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; +} + +message ListAvailableParamsResponse { + common.v1.BaseResponse base = 1; + repeated AvailableParamEntry data = 2; +} + +message AddApplicableParamRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; + string param_id = 2 [(buf.validate.field).string.uuid = true]; + bool is_required = 3; + // 0 means inherit mst_parameter.display_order. + int32 display_order = 4 [(buf.validate.field).int32.gte = 0]; +} + +message AddApplicableParamResponse { + common.v1.BaseResponse base = 1; +} + +message RemoveApplicableParamRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; + string param_id = 2 [(buf.validate.field).string.uuid = true]; +} + +message RemoveApplicableParamResponse { + common.v1.BaseResponse base = 1; +} + +message UpdateApplicableParamRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; + string param_id = 2 [(buf.validate.field).string.uuid = true]; + optional bool is_required = 3; + optional int32 display_order = 4 [(buf.validate.field).int32.gte = 0]; +} + +message UpdateApplicableParamResponse { + common.v1.BaseResponse base = 1; +} + +// ============================================================================= +// SERVICE +// ============================================================================= + +// CostProductParameterService manages per-product static parameter values. +service CostProductParameterService { + // ListProductRequiredParams returns the parameter form contents for a + // product — every active mst_parameter row (optionally filtered to + // is_required_for_costing) joined with the existing CPP value when present. + rpc ListProductRequiredParams(ListProductRequiredParamsRequest) returns (ListProductRequiredParamsResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-parameters/products/{product_sys_id}"}; + } + + // UpsertProductParamValue creates or updates a single param value for a product. + rpc UpsertProductParamValue(UpsertProductParamValueRequest) returns (UpsertProductParamValueResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-parameters/upsert" + body: "*" + }; + } + + // UpsertProductParamValuesBatch upserts multiple values atomically. + rpc UpsertProductParamValuesBatch(UpsertProductParamValuesBatchRequest) returns (UpsertProductParamValuesBatchResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-parameters/upsert-batch" + body: "*" + }; + } + + // DeleteProductParamValue clears a single value (back to unbound state). + rpc DeleteProductParamValue(DeleteProductParamValueRequest) returns (DeleteProductParamValueResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-parameters/delete" + body: "*" + }; + } + + // CheckMissingRequiredParams returns required params with no value yet. + // Used by the request state machine to gate PARAMETER_PENDING → PARAMETER_COMPLETE. + rpc CheckMissingRequiredParams(CheckMissingRequiredParamsRequest) returns (CheckMissingRequiredParamsResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-parameters/products/{product_sys_id}/missing"}; + } + + // ListAvailableParams returns mst_parameter rows NOT yet applicable for the + // product — feeds the "Add parameter" picker on the Parameters tab. + rpc ListAvailableParams(ListAvailableParamsRequest) returns (ListAvailableParamsResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-parameters/products/{product_sys_id}/available"}; + } + + // AddApplicableParam marks a parameter as applicable to the product. + rpc AddApplicableParam(AddApplicableParamRequest) returns (AddApplicableParamResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-parameters/applicable/add" + body: "*" + }; + } + + // RemoveApplicableParam removes a parameter from a product (deletes value too). + rpc RemoveApplicableParam(RemoveApplicableParamRequest) returns (RemoveApplicableParamResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-parameters/applicable/remove" + body: "*" + }; + } + + // UpdateApplicableParam patches per-product override fields (is_required, display_order). + rpc UpdateApplicableParam(UpdateApplicableParamRequest) returns (UpdateApplicableParamResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-parameters/applicable/update" + body: "*" + }; + } +} diff --git a/finance/v1/cost_product_request.proto b/finance/v1/cost_product_request.proto new file mode 100644 index 0000000..eef0687 --- /dev/null +++ b/finance/v1/cost_product_request.proto @@ -0,0 +1,504 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// CostProductRequest — PRD Phase A §7.1.1 (CPR_) + §7.1.2 (CPS_). +// State machine HARD-CODED per G3 (no IAM workflow template). The aggregate +// root is the request; the spec is an embedded 1:1 conditional value object. +// ============================================================================= + +message CostProductSpec { + int64 spec_id = 1; + int64 request_id = 2; + // POY_BOUGHTOUT | CHIPS_SD | CHIPS_BRT | CHIPS_RECYCLE. + string raw_material_type = 3; + string product_description = 4; + int32 shade_id = 5; + string shade_custom_text = 6; + int32 paper_tube_type_id = 7; + string paper_tube_label = 8; // denormalized + string weight_per_bobbin_kg = 9; // decimal stringified to preserve precision + // JUMBO | NORMAL | PALLET. + string box_type = 10; + string created_at = 11; + string created_by = 12; +} + +message CostProductRequest { + int64 request_id = 1; + string request_no = 2; // REQ-YYYYMM-NNNN + int32 request_type_id = 3; + string request_type_code = 4; // denormalized + string title = 5; + string description = 6; + string customer_name = 7; + string customer_code = 8; + string product_classification = 9; // existing | new + string verified_classification = 10; + string classification_override_reason = 11; + string target_volume = 12; // decimal stringified + string target_price_range = 13; + string urgency_level = 14; // low | medium | high + string needed_by_date = 15; // YYYY-MM-DD + string status = 16; + string closed_substatus = 17; + string feasibility_decision = 18; // FEASIBLE | NOT_FEASIBLE + string feasibility_note = 19; + string feasibility_by = 20; + string feasibility_at = 21; + string reject_reason = 22; + string cancel_reason = 23; + string assigned_to_user_id = 24; + string requester_user_id = 25; + common.v1.AuditInfo audit = 26; + CostProductSpec spec = 27; + // When UseExistingCosting was invoked, points to the product master whose costing is reused. 0 = none. + int64 existing_product_sys_id = 28; + // Active route head linked to this request (many requests → one route head when spec matches). + int64 linked_route_head_id = 29; +} + +// ============================================================================= +// CRUD +// ============================================================================= + +message SpecInput { + string raw_material_type = 1 [(buf.validate.field).string = { + in: [ + "POY_BOUGHTOUT", + "CHIPS_SD", + "CHIPS_BRT", + "CHIPS_RECYCLE" + ] + }]; + string product_description = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 5000 + }]; + int32 shade_id = 3; + string shade_custom_text = 4 [(buf.validate.field).string.max_len = 100]; + int32 paper_tube_type_id = 5 [(buf.validate.field).int32.gte = 1]; + string weight_per_bobbin_kg = 6 [(buf.validate.field).string = { + min_len: 1 + max_len: 20 + }]; + string box_type = 7 [(buf.validate.field).string = { + in: [ + "JUMBO", + "NORMAL", + "PALLET" + ] + }]; +} + +message CreateCostProductRequestRequest { + int32 request_type_id = 1 [(buf.validate.field).int32.gte = 1]; + string title = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 255 + }]; + string description = 3 [(buf.validate.field).string.max_len = 10000]; + string customer_name = 4 [(buf.validate.field).string = { + min_len: 1 + max_len: 255 + }]; + string customer_code = 5 [(buf.validate.field).string.max_len = 50]; + string product_classification = 6 [(buf.validate.field).string = { + in: [ + "existing", + "new" + ] + }]; + string target_volume = 7 [(buf.validate.field).string.max_len = 30]; + string target_price_range = 8 [(buf.validate.field).string.max_len = 50]; + string urgency_level = 9 [(buf.validate.field).string = { + in: [ + "", + "low", + "medium", + "high" + ] + }]; + string needed_by_date = 10 [(buf.validate.field).string.max_len = 10]; + SpecInput spec = 11; +} + +message CreateCostProductRequestResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +message GetCostProductRequestRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; +} + +message GetCostProductRequestResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +message GetCostProductRequestByNoRequest { + string request_no = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 30 + }]; +} + +message GetCostProductRequestByNoResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +message UpdateCostProductRequestRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; + string title = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 255 + }]; + string description = 3 [(buf.validate.field).string.max_len = 10000]; + string customer_name = 4 [(buf.validate.field).string = { + min_len: 1 + max_len: 255 + }]; + string customer_code = 5 [(buf.validate.field).string.max_len = 50]; + string product_classification = 6 [(buf.validate.field).string = { + in: [ + "existing", + "new" + ] + }]; + string target_volume = 7 [(buf.validate.field).string.max_len = 30]; + string target_price_range = 8 [(buf.validate.field).string.max_len = 50]; + string urgency_level = 9 [(buf.validate.field).string = { + in: [ + "", + "low", + "medium", + "high" + ] + }]; + string needed_by_date = 10 [(buf.validate.field).string.max_len = 10]; + SpecInput spec = 11; +} + +message UpdateCostProductRequestResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +message ListCostProductRequestsRequest { + string search = 1 [(buf.validate.field).string.max_len = 200]; + string status = 2 [(buf.validate.field).string.max_len = 30]; + int32 request_type_id = 3; + string requester_user_id = 4 [(buf.validate.field).string.max_len = 64]; + string assignee_user_id = 5 [(buf.validate.field).string.max_len = 64]; + common.v1.PaginationRequest pagination = 6; + string sort_by = 7 [(buf.validate.field).string = { + in: [ + "", + "request_no", + "created_at", + "updated_at", + "status" + ] + }]; + string sort_order = 8 [(buf.validate.field).string = { + in: [ + "", + "asc", + "desc" + ] + }]; +} + +message ListCostProductRequestsResponse { + common.v1.BaseResponse base = 1; + repeated CostProductRequest data = 2; + common.v1.PaginationResponse pagination = 3; +} + +// ============================================================================= +// State transitions (hard-coded per G3). Each RPC has a dedicated req+resp pair. +// ============================================================================= + +message SubmitCostProductRequestRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; +} +message SubmitCostProductRequestResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +message StartCostProductRequestReviewRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; +} +message StartCostProductRequestReviewResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +message VerifyCostProductRequestClassificationRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; + string verified_classification = 2 [(buf.validate.field).string = { + in: [ + "existing", + "new" + ] + }]; + string override_reason = 3 [(buf.validate.field).string.max_len = 2000]; +} +message VerifyCostProductRequestClassificationResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +message DecideCostProductRequestFeasibilityRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; + string decision = 2 [(buf.validate.field).string = { + in: [ + "FEASIBLE", + "NOT_FEASIBLE" + ] + }]; + string note = 3 [(buf.validate.field).string.max_len = 5000]; +} +message DecideCostProductRequestFeasibilityResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +message UseExistingCostingForCostProductRequestRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; + // Existing product the request reuses costing from — recorded on the + // request so QUOTE_READY traces back to a concrete product master. + int64 existing_product_sys_id = 2 [(buf.validate.field).int64.gte = 1]; +} +message UseExistingCostingForCostProductRequestResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +message RejectCostProductRequestRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; + string reason = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 5000 + }]; +} +message RejectCostProductRequestResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +message ReviseCostProductRequestRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; +} +message ReviseCostProductRequestResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +// Reopen moves a terminal CLOSED request back to DRAFT so it can re-enter the +// lifecycle. Restricted to admins at the delivery layer. +message ReopenCostProductRequestRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; +} +message ReopenCostProductRequestResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +// MarkParameterComplete moves PARAMETER_PENDING → PARAMETER_COMPLETE. +// The handler verifies all promoted products have their required CAPP params +// filled before allowing the transition. +message MarkParameterCompleteRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; +} +message MarkParameterCompleteResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; + // missing_products lists product_sys_ids that still have required params unfilled + // when the transition is blocked. Empty when the response is_success = true. + repeated MissingProductParams missing_products = 3; +} + +message MissingProductParams { + int64 product_sys_id = 1; + string product_code = 2; + string product_name = 3; + repeated string missing_param_codes = 4; +} + +message CancelCostProductRequestRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; + string reason = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 5000 + }]; +} +message CancelCostProductRequestResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +message CloseCostProductRequestRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; + string closed_substatus = 2 [(buf.validate.field).string = { + in: [ + "won", + "lost", + "cancelled", + "on_hold" + ] + }]; +} +message CloseCostProductRequestResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +message AssignCostProductRequestRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; + string assignee_user_id = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 64 + }]; +} +message AssignCostProductRequestResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} + +service CostProductRequestService { + rpc CreateCostProductRequest(CreateCostProductRequestRequest) returns (CreateCostProductRequestResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests" + body: "*" + }; + } + rpc GetCostProductRequest(GetCostProductRequestRequest) returns (GetCostProductRequestResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-requests/{request_id}"}; + } + rpc GetCostProductRequestByNo(GetCostProductRequestByNoRequest) returns (GetCostProductRequestByNoResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-requests/by-no/{request_no}"}; + } + rpc UpdateCostProductRequest(UpdateCostProductRequestRequest) returns (UpdateCostProductRequestResponse) { + option (google.api.http) = { + put: "/api/v1/finance/cost-product-requests/{request_id}" + body: "*" + }; + } + rpc ListCostProductRequests(ListCostProductRequestsRequest) returns (ListCostProductRequestsResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-requests"}; + } + rpc SubmitCostProductRequest(SubmitCostProductRequestRequest) returns (SubmitCostProductRequestResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/submit" + body: "*" + }; + } + rpc StartCostProductRequestReview(StartCostProductRequestReviewRequest) returns (StartCostProductRequestReviewResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/start-review" + body: "*" + }; + } + rpc VerifyCostProductRequestClassification(VerifyCostProductRequestClassificationRequest) returns (VerifyCostProductRequestClassificationResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/verify-classification" + body: "*" + }; + } + rpc DecideCostProductRequestFeasibility(DecideCostProductRequestFeasibilityRequest) returns (DecideCostProductRequestFeasibilityResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/decide-feasibility" + body: "*" + }; + } + rpc UseExistingCostingForCostProductRequest(UseExistingCostingForCostProductRequestRequest) returns (UseExistingCostingForCostProductRequestResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/use-existing-costing" + body: "*" + }; + } + rpc RejectCostProductRequest(RejectCostProductRequestRequest) returns (RejectCostProductRequestResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/reject" + body: "*" + }; + } + rpc ReviseCostProductRequest(ReviseCostProductRequestRequest) returns (ReviseCostProductRequestResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/revise" + body: "*" + }; + } + // ReopenCostProductRequest moves a CLOSED request back to DRAFT. + rpc ReopenCostProductRequest(ReopenCostProductRequestRequest) returns (ReopenCostProductRequestResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/reopen" + body: "*" + }; + } + // MarkParameterComplete advances PARAMETER_PENDING → PARAMETER_COMPLETE + // after verifying all promoted products have required params filled. + rpc MarkParameterComplete(MarkParameterCompleteRequest) returns (MarkParameterCompleteResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/mark-parameter-complete" + body: "*" + }; + } + rpc CancelCostProductRequest(CancelCostProductRequestRequest) returns (CancelCostProductRequestResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/cancel" + body: "*" + }; + } + rpc CloseCostProductRequest(CloseCostProductRequestRequest) returns (CloseCostProductRequestResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/close" + body: "*" + }; + } + rpc AssignCostProductRequest(AssignCostProductRequestRequest) returns (AssignCostProductRequestResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/assign" + body: "*" + }; + } + // LinkExistingRoute attaches an existing route_head to the request. + rpc LinkExistingRoute(LinkExistingRouteRequest) returns (LinkExistingRouteResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/link-route" + body: "*" + }; + } + // UnlinkRoute clears both existing-product and linked-route fields. + rpc UnlinkRoute(UnlinkRouteRequest) returns (UnlinkRouteResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-requests/{request_id}/unlink-route" + body: "*" + }; + } +} + +message LinkExistingRouteRequest { + int64 request_id = 1 [(buf.validate.field).int64.gt = 0]; + int64 route_head_id = 2 [(buf.validate.field).int64.gt = 0]; +} +message LinkExistingRouteResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} +message UnlinkRouteRequest { + int64 request_id = 1 [(buf.validate.field).int64.gt = 0]; +} +message UnlinkRouteResponse { + common.v1.BaseResponse base = 1; + CostProductRequest data = 2; +} diff --git a/finance/v1/cost_product_type.proto b/finance/v1/cost_product_type.proto new file mode 100644 index 0000000..fa26085 --- /dev/null +++ b/finance/v1/cost_product_type.proto @@ -0,0 +1,120 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// CostProductType — PRD Phase B §7.2.1 (CPT_). +// Master of product types (POY/PTY/TTY/etc) that drives auto-generated product +// code prefix. DB columns: "CPT_type_id", "CPT_type_code", "CPT_type_name", ... +// Proto/API surface uses plain snake_case (no CPT_ prefix on the wire). +// ============================================================================= + +message CostProductType { + int32 type_id = 1; + string type_code = 2; // max 5 chars uppercase + string type_name = 3; + bool is_active = 4; + common.v1.AuditInfo audit = 16; +} + +message CreateCostProductTypeRequest { + string type_code = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 5 + pattern: "^[A-Z][A-Z0-9]*$" + }]; + string type_name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 100 + }]; +} + +message CreateCostProductTypeResponse { + common.v1.BaseResponse base = 1; + CostProductType data = 2; +} + +message GetCostProductTypeRequest { + int32 type_id = 1 [(buf.validate.field).int32.gte = 1]; +} + +message GetCostProductTypeResponse { + common.v1.BaseResponse base = 1; + CostProductType data = 2; +} + +message UpdateCostProductTypeRequest { + int32 type_id = 1 [(buf.validate.field).int32.gte = 1]; + string type_name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 100 + }]; + bool is_active = 3; +} + +message UpdateCostProductTypeResponse { + common.v1.BaseResponse base = 1; + CostProductType data = 2; +} + +message ListCostProductTypesRequest { + // Free-text search across type_code and type_name (case-insensitive). + string search = 1 [(buf.validate.field).string.max_len = 200]; + // "all" | "active" | "inactive" — empty defaults to "all". + string active_filter = 2 [(buf.validate.field).string = { + in: [ + "", + "all", + "active", + "inactive" + ] + }]; + common.v1.PaginationRequest pagination = 3; + string sort_by = 4 [(buf.validate.field).string = { + in: [ + "", + "type_code", + "type_name", + "created_at" + ] + }]; + string sort_order = 5 [(buf.validate.field).string = { + in: [ + "", + "asc", + "desc" + ] + }]; +} + +message ListCostProductTypesResponse { + common.v1.BaseResponse base = 1; + repeated CostProductType data = 2; + common.v1.PaginationResponse pagination = 3; +} + +// CostProductTypeService — CRUD master for canonical product types. +service CostProductTypeService { + rpc CreateCostProductType(CreateCostProductTypeRequest) returns (CreateCostProductTypeResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-product-types" + body: "*" + }; + } + rpc GetCostProductType(GetCostProductTypeRequest) returns (GetCostProductTypeResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-types/{type_id}"}; + } + rpc UpdateCostProductType(UpdateCostProductTypeRequest) returns (UpdateCostProductTypeResponse) { + option (google.api.http) = { + put: "/api/v1/finance/cost-product-types/{type_id}" + body: "*" + }; + } + rpc ListCostProductTypes(ListCostProductTypesRequest) returns (ListCostProductTypesResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-types"}; + } +} diff --git a/finance/v1/cost_request_comment.proto b/finance/v1/cost_request_comment.proto new file mode 100644 index 0000000..5fd4138 --- /dev/null +++ b/finance/v1/cost_request_comment.proto @@ -0,0 +1,169 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// CostRequestComment — PRD Phase A §7.1.7 (CRC_) + §7.1.8 (CCEH_) + §7.1.9 (CRM_). +// Rich-text comment thread on a product request. body_richtext is a JSON tree +// (Tiptap/Lexical) serialized as a string on the wire; body_plaintext is the +// searchable/notification copy. @mentions are extracted from body_plaintext +// (pattern @[username]) and persisted to cost_request_mention. +// ============================================================================= + +message CostRequestComment { + int64 comment_id = 1; + int64 request_id = 2; + int64 parent_comment_id = 3; + string author_user_id = 4; + // JSON serialized Tiptap/Lexical document. + string body_richtext = 5; + string body_plaintext = 6; + bool is_edited = 7; + bool is_hidden = 8; + string hidden_reason = 9; + string created_at = 10; + string updated_at = 11; + // Mentioned user_ids extracted from body_plaintext on create/update. + repeated string mentioned_user_ids = 12; +} + +message CostCommentEditHistory { + int64 edit_id = 1; + int64 comment_id = 2; + string body_richtext = 3; + string body_plaintext = 4; + string edited_by = 5; + string edited_at = 6; +} + +// ============================================================================= +// CRUD +// ============================================================================= + +message CreateCostRequestCommentRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; + int64 parent_comment_id = 2; + string body_richtext = 3 [(buf.validate.field).string = { + min_len: 1 + max_len: 200000 + }]; + string body_plaintext = 4 [(buf.validate.field).string = { + min_len: 1 + max_len: 100000 + }]; + // Optional @mentions extracted client-side; backend re-extracts to be safe. + repeated string mentioned_user_ids = 5; +} + +message CreateCostRequestCommentResponse { + common.v1.BaseResponse base = 1; + CostRequestComment data = 2; +} + +message UpdateCostRequestCommentRequest { + int64 comment_id = 1 [(buf.validate.field).int64.gte = 1]; + string body_richtext = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 200000 + }]; + string body_plaintext = 3 [(buf.validate.field).string = { + min_len: 1 + max_len: 100000 + }]; + repeated string mentioned_user_ids = 4; +} + +message UpdateCostRequestCommentResponse { + common.v1.BaseResponse base = 1; + CostRequestComment data = 2; +} + +message HideCostRequestCommentRequest { + int64 comment_id = 1 [(buf.validate.field).int64.gte = 1]; + string hidden_reason = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 1000 + }]; +} + +message HideCostRequestCommentResponse { + common.v1.BaseResponse base = 1; + CostRequestComment data = 2; +} + +message UnhideCostRequestCommentRequest { + int64 comment_id = 1 [(buf.validate.field).int64.gte = 1]; +} + +message UnhideCostRequestCommentResponse { + common.v1.BaseResponse base = 1; + CostRequestComment data = 2; +} + +message DeleteCostRequestCommentRequest { + int64 comment_id = 1 [(buf.validate.field).int64.gte = 1]; +} + +message DeleteCostRequestCommentResponse { + common.v1.BaseResponse base = 1; +} + +message ListCostRequestCommentsRequest { + int64 request_id = 1 [(buf.validate.field).int64.gte = 1]; + // When true, include CRC_is_hidden = true (admin view). Default false. + bool include_hidden = 2; +} + +message ListCostRequestCommentsResponse { + common.v1.BaseResponse base = 1; + repeated CostRequestComment data = 2; +} + +message ListCostCommentEditHistoryRequest { + int64 comment_id = 1 [(buf.validate.field).int64.gte = 1]; +} + +message ListCostCommentEditHistoryResponse { + common.v1.BaseResponse base = 1; + repeated CostCommentEditHistory data = 2; +} + +service CostRequestCommentService { + rpc CreateCostRequestComment(CreateCostRequestCommentRequest) returns (CreateCostRequestCommentResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-request-comments" + body: "*" + }; + } + rpc UpdateCostRequestComment(UpdateCostRequestCommentRequest) returns (UpdateCostRequestCommentResponse) { + option (google.api.http) = { + put: "/api/v1/finance/cost-request-comments/{comment_id}" + body: "*" + }; + } + rpc HideCostRequestComment(HideCostRequestCommentRequest) returns (HideCostRequestCommentResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-request-comments/{comment_id}/hide" + body: "*" + }; + } + rpc UnhideCostRequestComment(UnhideCostRequestCommentRequest) returns (UnhideCostRequestCommentResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-request-comments/{comment_id}/unhide" + body: "*" + }; + } + rpc DeleteCostRequestComment(DeleteCostRequestCommentRequest) returns (DeleteCostRequestCommentResponse) { + option (google.api.http) = {delete: "/api/v1/finance/cost-request-comments/{comment_id}"}; + } + rpc ListCostRequestComments(ListCostRequestCommentsRequest) returns (ListCostRequestCommentsResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-product-requests/{request_id}/comments"}; + } + rpc ListCostCommentEditHistory(ListCostCommentEditHistoryRequest) returns (ListCostCommentEditHistoryResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-request-comments/{comment_id}/edit-history"}; + } +} diff --git a/finance/v1/cost_request_type.proto b/finance/v1/cost_request_type.proto new file mode 100644 index 0000000..7f956a3 --- /dev/null +++ b/finance/v1/cost_request_type.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// CostRequestType — PRD Phase A §7.1.3 (CRT_). +// Lookup: QUOTE / DEVELOPMENT. Read-only via API for S4; admin CRUD deferred. +// ============================================================================= + +message CostRequestType { + int32 type_id = 1; + string code = 2; + string display_name = 3; + // FULL | SHORTCUT_CAPABLE. + string state_machine_variant = 4; + string default_urgency = 5; + bool is_active = 6; +} + +message ListCostRequestTypesRequest { + string search = 1 [(buf.validate.field).string.max_len = 200]; + string active_filter = 2 [(buf.validate.field).string = { + in: [ + "", + "all", + "active", + "inactive" + ] + }]; + common.v1.PaginationRequest pagination = 3; +} + +message ListCostRequestTypesResponse { + common.v1.BaseResponse base = 1; + repeated CostRequestType data = 2; + common.v1.PaginationResponse pagination = 3; +} + +service CostRequestTypeService { + rpc ListCostRequestTypes(ListCostRequestTypesRequest) returns (ListCostRequestTypesResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-request-types"}; + } +} diff --git a/finance/v1/cost_rm_type.proto b/finance/v1/cost_rm_type.proto new file mode 100644 index 0000000..57680cf --- /dev/null +++ b/finance/v1/cost_rm_type.proto @@ -0,0 +1,122 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// CostRmType — PRD Phase B §7.2.3 (CRMT_). +// User-definable RM type master. reference_target controls which FK is required +// on cost_product_order_component: +// PRODUCT → CPOC_rm_product_sys_id (cost_product_master, Captive-Cost-like) +// MASTER → CPOC_rm_master_item_id (cost_erp_item, Store-Rate-like) +// allow_sub_sequence enables Multi-Yarn-like types (multiple rows per seq). +// ============================================================================= + +message CostRmType { + int32 type_id = 1; + string type_code = 2; + string type_name = 3; + string reference_target = 4; // PRODUCT | MASTER + bool allow_sub_sequence = 5; + bool is_active = 6; + common.v1.AuditInfo audit = 16; +} + +message CreateCostRmTypeRequest { + string type_code = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 30 + pattern: "^[A-Z][A-Z0-9_]*$" + }]; + string type_name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 100 + }]; + string reference_target = 3 [(buf.validate.field).string = { + in: [ + "PRODUCT", + "MASTER" + ] + }]; + bool allow_sub_sequence = 4; +} + +message CreateCostRmTypeResponse { + common.v1.BaseResponse base = 1; + CostRmType data = 2; +} + +message GetCostRmTypeRequest { + int32 type_id = 1 [(buf.validate.field).int32.gte = 1]; +} + +message GetCostRmTypeResponse { + common.v1.BaseResponse base = 1; + CostRmType data = 2; +} + +message UpdateCostRmTypeRequest { + int32 type_id = 1 [(buf.validate.field).int32.gte = 1]; + // reference_target + allow_sub_sequence are IMMUTABLE after create (would break + // existing components that depend on them); only display + active flag editable. + string type_name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 100 + }]; + bool is_active = 3; +} + +message UpdateCostRmTypeResponse { + common.v1.BaseResponse base = 1; + CostRmType data = 2; +} + +message ListCostRmTypesRequest { + string search = 1 [(buf.validate.field).string.max_len = 200]; + string reference_target = 2 [(buf.validate.field).string = { + in: [ + "", + "PRODUCT", + "MASTER" + ] + }]; + string active_filter = 3 [(buf.validate.field).string = { + in: [ + "", + "all", + "active", + "inactive" + ] + }]; + common.v1.PaginationRequest pagination = 4; +} + +message ListCostRmTypesResponse { + common.v1.BaseResponse base = 1; + repeated CostRmType data = 2; + common.v1.PaginationResponse pagination = 3; +} + +service CostRmTypeService { + rpc CreateCostRmType(CreateCostRmTypeRequest) returns (CreateCostRmTypeResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-rm-types" + body: "*" + }; + } + rpc GetCostRmType(GetCostRmTypeRequest) returns (GetCostRmTypeResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-rm-types/{type_id}"}; + } + rpc UpdateCostRmType(UpdateCostRmTypeRequest) returns (UpdateCostRmTypeResponse) { + option (google.api.http) = { + put: "/api/v1/finance/cost-rm-types/{type_id}" + body: "*" + }; + } + rpc ListCostRmTypes(ListCostRmTypesRequest) returns (ListCostRmTypesResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-rm-types"}; + } +} diff --git a/finance/v1/cost_route.proto b/finance/v1/cost_route.proto new file mode 100644 index 0000000..28356be --- /dev/null +++ b/finance/v1/cost_route.proto @@ -0,0 +1,263 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// CostRouteHead is the per-product released routing aggregate. +// One non-LOCKED head per product (UK by cpm_product_sys_id). +message CostRouteHead { + int64 head_id = 1; + int64 product_sys_id = 2; + string product_code = 3; // denorm for list views + string product_name = 4; // denorm + // DRAFT | COMPLETE | LOCKED + string routing_status = 5; + int32 version = 6; + int64 promoted_from_draft_id = 7; + int32 cyl_type_id = 8; + string notes = 9; + common.v1.AuditInfo audit = 16; +} + +// CostRouteSeq is one stage of the route at a given level + ordering. +message CostRouteSeq { + int64 seq_id = 1; + int64 head_id = 2; + int64 product_sys_id = 3; + string product_code = 4; // denorm + string product_name = 5; // denorm + int32 route_level = 6; // 1 = FG; 2..N upstream + int32 route_seq = 7; // ordering within level + string route_name = 8; + string route_item_code = 9; + string route_shade_code = 10; + string route_shade_name = 11; + double position_x = 12; + double position_y = 13; + // RMs that feed this stage (rendered inline on graph fetch). + repeated CostRouteRm rms = 14; +} + +// CostRouteRm is one input edge feeding a SEQ. +// Exactly one of (rm_product_sys_id, rm_item_code, rm_group_code) is set, +// matching rm_type. +message CostRouteRm { + int64 rm_id = 1; + int64 seq_id = 2; + int64 parent_product_sys_id = 3; + // PRODUCT | ITEM | GROUP + string rm_type = 4; + int64 rm_product_sys_id = 5; + string rm_item_code = 6; + string rm_group_code = 7; + string route_rm_name = 8; + string route_rm_item_code = 9; + string route_rm_shade_code = 10; + string route_rm_shade_name = 11; + double route_rm_ratio = 12; + int32 uom_id = 13; + string sub_type = 14; + string notes = 15; +} + +// RouteGraph bundles the head + all seqs (with rms inline). +message RouteGraph { + CostRouteHead head = 1; + repeated CostRouteSeq seqs = 2; +} + +// ---- Requests / responses ---- + +message GetRouteByProductRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; +} +message GetRouteByProductResponse { + common.v1.BaseResponse base = 1; + CostRouteHead data = 2; +} + +message GetRouteGraphRequest { + int64 head_id = 1 [(buf.validate.field).int64.gt = 0]; +} +message GetRouteGraphResponse { + common.v1.BaseResponse base = 1; + RouteGraph data = 2; +} + +message SaveRouteGraphRequest { + int64 head_id = 1 [(buf.validate.field).int64.gt = 0]; + RouteGraph graph = 2; +} +message SaveRouteGraphResponse { + common.v1.BaseResponse base = 1; + RouteGraph data = 2; +} + +message CompleteRouteRequest { + int64 head_id = 1 [(buf.validate.field).int64.gt = 0]; +} +message CompleteRouteResponse { + common.v1.BaseResponse base = 1; + CostRouteHead data = 2; +} + +message LockRouteRequest { + int64 head_id = 1 [(buf.validate.field).int64.gt = 0]; +} +message LockRouteResponse { + common.v1.BaseResponse base = 1; + CostRouteHead data = 2; +} + +message UnlockRouteRequest { + int64 head_id = 1 [(buf.validate.field).int64.gt = 0]; +} +message UnlockRouteResponse { + common.v1.BaseResponse base = 1; + CostRouteHead data = 2; +} + +message DeleteRouteRequest { + int64 head_id = 1 [(buf.validate.field).int64.gt = 0]; +} +message DeleteRouteResponse { + common.v1.BaseResponse base = 1; +} + +message ListRoutesRequest { + int32 page = 1 [(buf.validate.field).int32.gte = 1]; + int32 page_size = 2 [(buf.validate.field).int32 = { + gte: 1 + lte: 100 + }]; + string search = 3 [(buf.validate.field).string.max_len = 100]; + // Empty / DRAFT / COMPLETE / LOCKED + string status = 4 [(buf.validate.field).string = { + in: [ + "", + "DRAFT", + "COMPLETE", + "LOCKED" + ] + }]; + string sort_by = 5 [(buf.validate.field).string = { + in: [ + "", + "created_at", + "product_code", + "status" + ] + }]; + string sort_order = 6 [(buf.validate.field).string = { + in: [ + "", + "asc", + "desc" + ] + }]; +} +message ListRoutesResponse { + common.v1.BaseResponse base = 1; + repeated CostRouteHead data = 2; + common.v1.PaginationResponse pagination = 3; +} + +// CostRouteService manages persisted routings (replaces CostProductOrderService). +service CostRouteService { + rpc GetRouteByProduct(GetRouteByProductRequest) returns (GetRouteByProductResponse) { + option (google.api.http) = {get: "/api/v1/finance/routes/by-product/{product_sys_id}"}; + } + rpc GetRouteGraph(GetRouteGraphRequest) returns (GetRouteGraphResponse) { + option (google.api.http) = {get: "/api/v1/finance/routes/{head_id}/graph"}; + } + rpc SaveRouteGraph(SaveRouteGraphRequest) returns (SaveRouteGraphResponse) { + option (google.api.http) = { + post: "/api/v1/finance/routes/{head_id}/graph" + body: "*" + }; + } + rpc CompleteRoute(CompleteRouteRequest) returns (CompleteRouteResponse) { + option (google.api.http) = { + post: "/api/v1/finance/routes/{head_id}/complete" + body: "*" + }; + } + rpc LockRoute(LockRouteRequest) returns (LockRouteResponse) { + option (google.api.http) = { + post: "/api/v1/finance/routes/{head_id}/lock" + body: "*" + }; + } + rpc UnlockRoute(UnlockRouteRequest) returns (UnlockRouteResponse) { + option (google.api.http) = { + post: "/api/v1/finance/routes/{head_id}/unlock" + body: "*" + }; + } + rpc DeleteRoute(DeleteRouteRequest) returns (DeleteRouteResponse) { + option (google.api.http) = {delete: "/api/v1/finance/routes/{head_id}"}; + } + rpc ListRoutes(ListRoutesRequest) returns (ListRoutesResponse) { + option (google.api.http) = {get: "/api/v1/finance/routes"}; + } + rpc DuplicateRoute(DuplicateRouteRequest) returns (DuplicateRouteResponse) { + option (google.api.http) = { + post: "/api/v1/finance/routes/{head_id}/duplicate" + body: "*" + }; + } + rpc ListLinkedRequests(ListLinkedRequestsRequest) returns (ListLinkedRequestsResponse) { + option (google.api.http) = {get: "/api/v1/finance/routes/{head_id}/linked-requests"}; + } + rpc CreateRouteFromProduct(CreateRouteFromProductRequest) returns (CreateRouteFromProductResponse) { + option (google.api.http) = { + post: "/api/v1/finance/routes/from-product" + body: "*" + }; + } +} + +message DuplicateRouteRequest { + int64 head_id = 1 [(buf.validate.field).int64.gt = 0]; + bool include_routing = 2; + bool include_upstream = 3; + bool include_applicability = 4; + bool include_values = 5; + string new_code_prefix = 6 [(buf.validate.field).string.max_len = 40]; + int64 linked_request_id = 7; +} +message DuplicateRouteResponse { + common.v1.BaseResponse base = 1; + int64 new_head_id = 2; + int64 new_product_sys_id = 3; + string new_product_code = 4; +} + +message ListLinkedRequestsRequest { + int64 head_id = 1 [(buf.validate.field).int64.gt = 0]; +} +message LinkedRequest { + int64 request_id = 1; + string request_no = 2; + string status = 3; + string product_top_2 = 4; + string created_by = 5; + string created_at = 6; +} +message ListLinkedRequestsResponse { + common.v1.BaseResponse base = 1; + repeated LinkedRequest data = 2; +} + +message CreateRouteFromProductRequest { + int64 product_sys_id = 1 [(buf.validate.field).int64.gt = 0]; + int64 linked_request_id = 2; // optional, atomically links the request on success. + int32 cyl_type_id = 3; +} +message CreateRouteFromProductResponse { + common.v1.BaseResponse base = 1; + int64 head_id = 2; +} diff --git a/finance/v1/cost_routing_rule.proto b/finance/v1/cost_routing_rule.proto new file mode 100644 index 0000000..005b9cf --- /dev/null +++ b/finance/v1/cost_routing_rule.proto @@ -0,0 +1,127 @@ +syntax = "proto3"; + +package finance.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// CostRoutingRule — PRD Phase A §7.1.4 (CRR_). +// Admin-managed first-match-wins rules evaluated on cost_product_request submit. +// condition is a JSON predicate tree; backend evaluates it against the request. +// ============================================================================= + +message CostRoutingRule { + int32 rule_id = 1; + int32 priority = 2; + string condition = 3; // serialized JSON predicate + // AUTO_ASSIGN | TO_TRIAGE. + string action_type = 4; + // user_id (for AUTO_ASSIGN to specific user) or functional_role (e.g. "Engineering"). + string action_target = 5; + bool is_active = 6; + string created_by = 7; + string created_at = 8; +} + +message CreateCostRoutingRuleRequest { + int32 priority = 1 [(buf.validate.field).int32.gte = 1]; + string condition = 2 [(buf.validate.field).string = { + min_len: 2 + max_len: 20000 + }]; + string action_type = 3 [(buf.validate.field).string = { + in: [ + "AUTO_ASSIGN", + "TO_TRIAGE" + ] + }]; + string action_target = 4 [(buf.validate.field).string.max_len = 100]; +} + +message CreateCostRoutingRuleResponse { + common.v1.BaseResponse base = 1; + CostRoutingRule data = 2; +} + +message GetCostRoutingRuleRequest { + int32 rule_id = 1 [(buf.validate.field).int32.gte = 1]; +} + +message GetCostRoutingRuleResponse { + common.v1.BaseResponse base = 1; + CostRoutingRule data = 2; +} + +message UpdateCostRoutingRuleRequest { + int32 rule_id = 1 [(buf.validate.field).int32.gte = 1]; + int32 priority = 2 [(buf.validate.field).int32.gte = 1]; + string condition = 3 [(buf.validate.field).string = { + min_len: 2 + max_len: 20000 + }]; + string action_type = 4 [(buf.validate.field).string = { + in: [ + "AUTO_ASSIGN", + "TO_TRIAGE" + ] + }]; + string action_target = 5 [(buf.validate.field).string.max_len = 100]; + bool is_active = 6; +} + +message UpdateCostRoutingRuleResponse { + common.v1.BaseResponse base = 1; + CostRoutingRule data = 2; +} + +message DeleteCostRoutingRuleRequest { + int32 rule_id = 1 [(buf.validate.field).int32.gte = 1]; +} + +message DeleteCostRoutingRuleResponse { + common.v1.BaseResponse base = 1; +} + +message ListCostRoutingRulesRequest { + string active_filter = 1 [(buf.validate.field).string = { + in: [ + "", + "all", + "active", + "inactive" + ] + }]; + common.v1.PaginationRequest pagination = 2; +} + +message ListCostRoutingRulesResponse { + common.v1.BaseResponse base = 1; + repeated CostRoutingRule data = 2; + common.v1.PaginationResponse pagination = 3; +} + +service CostRoutingRuleService { + rpc CreateCostRoutingRule(CreateCostRoutingRuleRequest) returns (CreateCostRoutingRuleResponse) { + option (google.api.http) = { + post: "/api/v1/finance/cost-routing-rules" + body: "*" + }; + } + rpc GetCostRoutingRule(GetCostRoutingRuleRequest) returns (GetCostRoutingRuleResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-routing-rules/{rule_id}"}; + } + rpc UpdateCostRoutingRule(UpdateCostRoutingRuleRequest) returns (UpdateCostRoutingRuleResponse) { + option (google.api.http) = { + put: "/api/v1/finance/cost-routing-rules/{rule_id}" + body: "*" + }; + } + rpc DeleteCostRoutingRule(DeleteCostRoutingRuleRequest) returns (DeleteCostRoutingRuleResponse) { + option (google.api.http) = {delete: "/api/v1/finance/cost-routing-rules/{rule_id}"}; + } + rpc ListCostRoutingRules(ListCostRoutingRulesRequest) returns (ListCostRoutingRulesResponse) { + option (google.api.http) = {get: "/api/v1/finance/cost-routing-rules"}; + } +} diff --git a/finance/v1/parameter.proto b/finance/v1/parameter.proto index 503d911..042c4d8 100644 --- a/finance/v1/parameter.proto +++ b/finance/v1/parameter.proto @@ -71,6 +71,18 @@ message Parameter { string uom_code = 22; // Resolved UOM name (read-only, populated from mst_uom join). string uom_name = 23; + // Responsible department (Engineering, Production, Finance, RND). Optional. + string owner_department = 24; + // Whether this parameter must be filled per product before the request can leave PARAMETER_PENDING. + bool is_required_for_costing = 25; + // FALSE = stored in cost_product_parameter (Phase B static). TRUE = stored per period in Phase C (deferred). + bool is_period_dependent = 26; + // When NOT empty the UI renders a combobox sourced from the named master (e.g. YARN_TYPE). Free-text fallback while master is not yet built. + string lookup_master_code = 27; + // Render order within display_group. + int32 display_order = 28; + // Form section: Spec / Machine / Grade / Packing / Cost / etc. + string display_group = 29; } // ============================================================================= @@ -112,6 +124,18 @@ message CreateParameterRequest { string min_value = 8 [(buf.validate.field).string.max_len = 50]; // Maximum value (optional, decimal as string). string max_value = 9 [(buf.validate.field).string.max_len = 50]; + // Owner department (optional, max 30 chars). + string owner_department = 10 [(buf.validate.field).string.max_len = 30]; + // Whether required for costing (default false). + bool is_required_for_costing = 11; + // Period-dependent flag (default false). TRUE = Phase C period-storage (deferred). + bool is_period_dependent = 12; + // Lookup master code (optional, max 30 chars). E.g. 'YARN_TYPE'. + string lookup_master_code = 13 [(buf.validate.field).string.max_len = 30]; + // Display order within display group (default 0). + int32 display_order = 14 [(buf.validate.field).int32.gte = 0]; + // Display group (optional, max 50 chars). E.g. 'Spec', 'Machine'. + string display_group = 15 [(buf.validate.field).string.max_len = 50]; } // CreateParameterResponse is the response for creating a parameter. @@ -171,6 +195,18 @@ message UpdateParameterRequest { optional string max_value = 9; // New active status (optional). optional bool is_active = 10; + // New owner department (optional). + optional string owner_department = 11 [(buf.validate.field).string.max_len = 30]; + // New is_required_for_costing flag (optional). + optional bool is_required_for_costing = 12; + // New is_period_dependent flag (optional). + optional bool is_period_dependent = 13; + // New lookup master code (optional). Set to empty string to clear. + optional string lookup_master_code = 14 [(buf.validate.field).string.max_len = 30]; + // New display order (optional). + optional int32 display_order = 15 [(buf.validate.field).int32.gte = 0]; + // New display group (optional). Set to empty string to clear. + optional string display_group = 16 [(buf.validate.field).string.max_len = 50]; } // UpdateParameterResponse is the response for updating a parameter. diff --git a/iam/v1/auth.proto b/iam/v1/auth.proto index 963c37a..98daa57 100644 --- a/iam/v1/auth.proto +++ b/iam/v1/auth.proto @@ -204,6 +204,10 @@ message AuthUser { bool two_factor_enabled = 8; // Whether the user has verified their email address (true if email_verified_at IS NOT NULL). bool email_verified = 9; + // Section UUID assigned to the user via user_detail (empty if not assigned). + string section_id = 10; + // Department UUID derived from the user's section (empty if no section assigned). + string department_id = 11; } // ============================================================================= diff --git a/iam/v1/company_mapping.proto b/iam/v1/company_mapping.proto new file mode 100644 index 0000000..727e878 --- /dev/null +++ b/iam/v1/company_mapping.proto @@ -0,0 +1,217 @@ +syntax = "proto3"; + +package iam.v1; + +// Note: go_package is managed by buf.gen.yaml managed mode + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; +import "iam/v1/user.proto"; + +// ============================================================================= +// MESSAGES - Entity +// ============================================================================= + +// CompanyMapping represents a denormalized organizational path +// (Company → Division → Department → optional Section) bundled under a single +// human-friendly code. Section is optional. Used to assign users to one or +// more organizational positions. +message CompanyMapping { + // Unique identifier (UUID). + string company_mapping_id = 1; + // Unique business code (uppercase, digits, hyphen). Immutable after creation. + string code = 2; + // Display name (1-200 chars). + string name = 3; + + // Company reference (required) and denormalized fields for read-side display. + string company_id = 4; + string company_code = 5; + string company_name = 6; + + // Division reference (required) and denormalized fields. + string division_id = 7; + string division_code = 8; + string division_name = 9; + + // Department reference (required) and denormalized fields. + string department_id = 10; + string department_code = 11; + string department_name = 12; + + // Section reference (optional) and denormalized fields. Empty when no section. + optional string section_id = 13; + string section_code = 14; + string section_name = 15; + + // Whether the mapping is active. + bool is_active = 16; + + // Audit information. + common.v1.AuditInfo audit = 20; +} + +// ============================================================================= +// MESSAGES - Create +// ============================================================================= + +// CreateCompanyMappingRequest is the request for creating a new mapping. +message CreateCompanyMappingRequest { + // Unique code (1-50 chars, uppercase letters, digits and hyphens). + // Immutable after creation. + string code = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 50 + pattern: "^[A-Z][A-Z0-9-]*$" + }]; + // Display name (1-200 chars). + string name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 200 + }]; + // Company ID (UUID, required). + string company_id = 3 [(buf.validate.field).string.uuid = true]; + // Division ID (UUID, required). + string division_id = 4 [(buf.validate.field).string.uuid = true]; + // Department ID (UUID, required). + string department_id = 5 [(buf.validate.field).string.uuid = true]; + // Section ID (UUID, optional). + optional string section_id = 6 [(buf.validate.field).string.uuid = true]; +} + +// CreateCompanyMappingResponse is the response for creating a mapping. +message CreateCompanyMappingResponse { + common.v1.BaseResponse base = 1; + CompanyMapping data = 2; +} + +// ============================================================================= +// MESSAGES - Get +// ============================================================================= + +// GetCompanyMappingRequest fetches a mapping by ID. +message GetCompanyMappingRequest { + string company_mapping_id = 1 [(buf.validate.field).string.uuid = true]; +} + +// GetCompanyMappingResponse is the response for fetching a mapping. +message GetCompanyMappingResponse { + common.v1.BaseResponse base = 1; + CompanyMapping data = 2; +} + +// ============================================================================= +// MESSAGES - Update +// ============================================================================= + +// UpdateCompanyMappingRequest updates an existing mapping. Code is immutable. +message UpdateCompanyMappingRequest { + string company_mapping_id = 1 [(buf.validate.field).string.uuid = true]; + optional string name = 2 [(buf.validate.field).string = {max_len: 200}]; + optional string company_id = 3 [(buf.validate.field).string.uuid = true]; + optional string division_id = 4 [(buf.validate.field).string.uuid = true]; + optional string department_id = 5 [(buf.validate.field).string.uuid = true]; + optional string section_id = 6 [(buf.validate.field).string.uuid = true]; + optional bool is_active = 7; +} + +// UpdateCompanyMappingResponse is the response for updating a mapping. +message UpdateCompanyMappingResponse { + common.v1.BaseResponse base = 1; + CompanyMapping data = 2; +} + +// ============================================================================= +// MESSAGES - Delete +// ============================================================================= + +// DeleteCompanyMappingRequest soft-deletes a mapping by ID. +message DeleteCompanyMappingRequest { + string company_mapping_id = 1 [(buf.validate.field).string.uuid = true]; +} + +// DeleteCompanyMappingResponse is the response for deleting a mapping. +message DeleteCompanyMappingResponse { + common.v1.BaseResponse base = 1; +} + +// ============================================================================= +// MESSAGES - List +// ============================================================================= + +// ListCompanyMappingsRequest lists mappings with search, filters, and pagination. +message ListCompanyMappingsRequest { + int32 page = 1 [(buf.validate.field).int32.gte = 1]; + int32 page_size = 2 [(buf.validate.field).int32 = { + gte: 1 + lte: 100 + }]; + // Search across code, name, and denormalized hierarchy names. + string search = 3 [(buf.validate.field).string.max_len = 200]; + optional string company_id = 4 [(buf.validate.field).string.uuid = true]; + optional string division_id = 5 [(buf.validate.field).string.uuid = true]; + optional string department_id = 6 [(buf.validate.field).string.uuid = true]; + optional string section_id = 7 [(buf.validate.field).string.uuid = true]; + ActiveFilter active_filter = 8; + string sort_by = 9 [(buf.validate.field).string = { + in: [ + "", + "code", + "name", + "created_at" + ] + }]; + string sort_order = 10 [(buf.validate.field).string = { + in: [ + "", + "asc", + "desc" + ] + }]; +} + +// ListCompanyMappingsResponse is the response for listing mappings. +message ListCompanyMappingsResponse { + common.v1.BaseResponse base = 1; + repeated CompanyMapping data = 2; + common.v1.PaginationResponse pagination = 3; +} + +// ============================================================================= +// SERVICE DEFINITION +// ============================================================================= + +// CompanyMappingService provides CRUD for CompanyMapping master records. +service CompanyMappingService { + // CreateCompanyMapping creates a new mapping. + rpc CreateCompanyMapping(CreateCompanyMappingRequest) returns (CreateCompanyMappingResponse) { + option (google.api.http) = { + post: "/api/v1/iam/company-mappings" + body: "*" + }; + } + + // GetCompanyMapping fetches a mapping by ID. + rpc GetCompanyMapping(GetCompanyMappingRequest) returns (GetCompanyMappingResponse) { + option (google.api.http) = {get: "/api/v1/iam/company-mappings/{company_mapping_id}"}; + } + + // UpdateCompanyMapping updates an existing mapping. Code is immutable. + rpc UpdateCompanyMapping(UpdateCompanyMappingRequest) returns (UpdateCompanyMappingResponse) { + option (google.api.http) = { + put: "/api/v1/iam/company-mappings/{company_mapping_id}" + body: "*" + }; + } + + // DeleteCompanyMapping soft-deletes a mapping. + rpc DeleteCompanyMapping(DeleteCompanyMappingRequest) returns (DeleteCompanyMappingResponse) { + option (google.api.http) = {delete: "/api/v1/iam/company-mappings/{company_mapping_id}"}; + } + + // ListCompanyMappings lists mappings with search, filter, and pagination. + rpc ListCompanyMappings(ListCompanyMappingsRequest) returns (ListCompanyMappingsResponse) { + option (google.api.http) = {get: "/api/v1/iam/company-mappings"}; + } +} diff --git a/iam/v1/user.proto b/iam/v1/user.proto index c30d172..5d32348 100644 --- a/iam/v1/user.proto +++ b/iam/v1/user.proto @@ -135,6 +135,27 @@ service UserService { body: "*" }; } + + // AssignUserCompanyMapping assigns a company mapping to a user. + rpc AssignUserCompanyMapping(AssignUserCompanyMappingRequest) returns (AssignUserCompanyMappingResponse) { + option (google.api.http) = { + post: "/api/v1/iam/users/{user_id}/company-mappings" + body: "*" + }; + } + + // RemoveUserCompanyMapping removes a company mapping from a user. + rpc RemoveUserCompanyMapping(RemoveUserCompanyMappingRequest) returns (RemoveUserCompanyMappingResponse) { + option (google.api.http) = { + post: "/api/v1/iam/users/{user_id}/company-mappings/remove" + body: "*" + }; + } + + // GetUserCompanyMappings lists all company mappings assigned to a user. + rpc GetUserCompanyMappings(GetUserCompanyMappingsRequest) returns (GetUserCompanyMappingsResponse) { + option (google.api.http) = {get: "/api/v1/iam/users/{user_id}/company-mappings"}; + } } // ============================================================================= @@ -159,6 +180,24 @@ message User { optional string last_login_at = 7; // Audit information. common.v1.AuditInfo audit = 8; + + // Employee level reference (optional). + string employee_level_id = 9; + // Employee level code (denormalized for read). + string employee_level_code = 10; + // Employee group reference (optional). + string employee_group_id = 11; + // Employee group code (denormalized for read). + string employee_group_code = 12; + + // Primary company-mapping reference (optional). + string primary_company_mapping_id = 13; + // Primary company-mapping denormalized fields (empty if no primary mapping). + string primary_company_name = 14; + string primary_division_name = 15; + string primary_department_name = 16; + // Primary section name (empty when the primary mapping has no section). + string primary_section_name = 17; } // UserDetail represents employee details. @@ -271,6 +310,12 @@ message CreateUserRequest { optional string address = 12 [(buf.validate.field).string.max_len = 500]; // Initial role IDs to assign. repeated string role_ids = 13; + // Employee level ID to assign (optional, UUID). + optional string employee_level_id = 14 [(buf.validate.field).string.uuid = true]; + // Employee group ID to assign (optional, UUID). + optional string employee_group_id = 15 [(buf.validate.field).string.uuid = true]; + // Primary company-mapping ID to assign on creation (optional, UUID). + optional string company_mapping_id = 16 [(buf.validate.field).string.uuid = true]; } // CreateUserResponse is the response for creating a user. @@ -336,6 +381,12 @@ message UpdateUserRequest { optional bool is_active = 4; // Unlock account (optional). optional bool unlock_account = 5; + // Employee level ID (optional, UUID; clears when omitted from request body). + optional string employee_level_id = 6 [(buf.validate.field).string.uuid = true]; + // Employee group ID (optional, UUID). + optional string employee_group_id = 7 [(buf.validate.field).string.uuid = true]; + // Primary company-mapping ID (optional, UUID). + optional string company_mapping_id = 8 [(buf.validate.field).string.uuid = true]; } // UpdateUserResponse is the response for updating a user. @@ -681,3 +732,59 @@ message UploadProfilePictureResponse { // The public URL of the uploaded profile picture. string profile_picture_url = 2; } + +// ============================================================================= +// MESSAGES - User Company Mapping Assignment +// ============================================================================= + +// UserCompanyMappingRef is a thin reference to a CompanyMapping with the +// denormalized hierarchy and the user-junction is_primary flag for display. +message UserCompanyMappingRef { + string company_mapping_id = 1; + string code = 2; + string name = 3; + string company_name = 4; + string division_name = 5; + string department_name = 6; + // Section name (empty if mapping has no section). + string section_name = 7; + bool is_primary = 8; +} + +// AssignUserCompanyMappingRequest assigns a mapping to a user. +message AssignUserCompanyMappingRequest { + string user_id = 1 [(buf.validate.field).string.uuid = true]; + string company_mapping_id = 2 [(buf.validate.field).string.uuid = true]; + // If true, the mapping becomes the user's primary mapping (any existing + // primary is unset transactionally). + bool is_primary = 3; +} + +// AssignUserCompanyMappingResponse is the response for assigning a mapping. +message AssignUserCompanyMappingResponse { + common.v1.BaseResponse base = 1; +} + +// RemoveUserCompanyMappingRequest removes a mapping from a user. +message RemoveUserCompanyMappingRequest { + string user_id = 1 [(buf.validate.field).string.uuid = true]; + string company_mapping_id = 2 [(buf.validate.field).string.uuid = true]; +} + +// RemoveUserCompanyMappingResponse is the response for removing a mapping. +message RemoveUserCompanyMappingResponse { + common.v1.BaseResponse base = 1; +} + +// GetUserCompanyMappingsRequest fetches the mappings assigned to a user. +message GetUserCompanyMappingsRequest { + string user_id = 1 [(buf.validate.field).string.uuid = true]; +} + +// GetUserCompanyMappingsResponse contains a user's mapping list. +message GetUserCompanyMappingsResponse { + common.v1.BaseResponse base = 1; + repeated UserCompanyMappingRef data = 2; + // Primary mapping ID (empty if user has no primary mapping). + string primary_company_mapping_id = 3; +} diff --git a/iam/v1/workflow.proto b/iam/v1/workflow.proto new file mode 100644 index 0000000..afae2f3 --- /dev/null +++ b/iam/v1/workflow.proto @@ -0,0 +1,416 @@ +syntax = "proto3"; + +package iam.v1; + +import "buf/validate/validate.proto"; +import "common/v1/common.proto"; +import "google/api/annotations.proto"; + +// ============================================================================= +// MESSAGES — Template + Step +// ============================================================================= + +// WorkflowTemplate is a versioned configuration of approval steps. +// kind discriminates use cases (PRODUCT_COSTING, PARAM_FILL). +message WorkflowTemplate { + string template_id = 1; + string kind = 2; + string name = 3; + int32 version = 4; + bool is_active = 5; + string description = 6; + repeated WorkflowTemplateStep steps = 7; + common.v1.AuditInfo audit = 16; +} + +// WorkflowTemplateStep is one ordered step inside a template. +message WorkflowTemplateStep { + string template_step_id = 1; + string template_id = 2; + int32 step_no = 3; + string step_name = 4; + string approver_resolution_type = 5; // ROLE | USER | DEPT + string approver_resolution_value = 6; + int32 sla_hours = 7; + bool allow_reject = 8; + bool allow_reassign = 9; + bool require_password_on_unlock = 10; + int32 reject_to_step_no = 11; // 0 = restart, N-1 = back one +} + +// ============================================================================= +// Step input (shared by Create / Update) +// ============================================================================= + +message WorkflowTemplateStepInput { + int32 step_no = 1 [(buf.validate.field).int32.gte = 1]; + string step_name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 200 + }]; + string approver_resolution_type = 3 [(buf.validate.field).string = { + in: [ + "ROLE", + "USER", + "DEPT" + ] + }]; + string approver_resolution_value = 4 [(buf.validate.field).string = { + min_len: 1 + max_len: 200 + }]; + int32 sla_hours = 5 [(buf.validate.field).int32.gte = 0]; + bool allow_reject = 6; + bool allow_reassign = 7; + bool require_password_on_unlock = 8; + int32 reject_to_step_no = 9 [(buf.validate.field).int32.gte = 0]; +} + +// ============================================================================= +// MESSAGES — Create +// ============================================================================= + +message CreateWorkflowTemplateRequest { + string kind = 1 [(buf.validate.field).string = { + in: [ + "PRODUCT_COSTING", + "PARAM_FILL" + ] + }]; + string name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 200 + }]; + string description = 3 [(buf.validate.field).string = {max_len: 1000}]; + repeated WorkflowTemplateStepInput steps = 4 [(buf.validate.field).repeated.min_items = 1]; +} + +message CreateWorkflowTemplateResponse { + common.v1.BaseResponse base = 1; + WorkflowTemplate data = 2; +} + +// ============================================================================= +// MESSAGES — Get +// ============================================================================= + +message GetWorkflowTemplateRequest { + string template_id = 1 [(buf.validate.field).string.uuid = true]; +} + +message GetWorkflowTemplateResponse { + common.v1.BaseResponse base = 1; + WorkflowTemplate data = 2; +} + +// ============================================================================= +// MESSAGES — Update +// ============================================================================= +// +// Update creates a new version of the template (immutable history). The current +// active version is left untouched on disk; activate the new version via Activate. +message UpdateWorkflowTemplateRequest { + string template_id = 1 [(buf.validate.field).string.uuid = true]; + string name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 200 + }]; + string description = 3 [(buf.validate.field).string = {max_len: 1000}]; + repeated WorkflowTemplateStepInput steps = 4 [(buf.validate.field).repeated.min_items = 1]; +} + +message UpdateWorkflowTemplateResponse { + common.v1.BaseResponse base = 1; + WorkflowTemplate data = 2; +} + +// ============================================================================= +// MESSAGES — Activate +// ============================================================================= + +message ActivateWorkflowTemplateRequest { + string template_id = 1 [(buf.validate.field).string.uuid = true]; +} + +message ActivateWorkflowTemplateResponse { + common.v1.BaseResponse base = 1; + WorkflowTemplate data = 2; +} + +// ============================================================================= +// MESSAGES — Delete +// ============================================================================= + +message DeleteWorkflowTemplateRequest { + string template_id = 1 [(buf.validate.field).string.uuid = true]; +} + +message DeleteWorkflowTemplateResponse { + common.v1.BaseResponse base = 1; +} + +// ============================================================================= +// MESSAGES — List +// ============================================================================= + +message ListWorkflowTemplatesRequest { + string search = 1 [(buf.validate.field).string.max_len = 200]; + // Filter by kind (PRODUCT_COSTING|PARAM_FILL). Empty = all. + string kind = 2 [(buf.validate.field).string = { + in: [ + "", + "PRODUCT_COSTING", + "PARAM_FILL" + ] + }]; + // "all" | "active" | "inactive". Empty = all. + string active_filter = 3 [(buf.validate.field).string = { + in: [ + "", + "all", + "active", + "inactive" + ] + }]; + common.v1.PaginationRequest pagination = 4; + string sort_by = 5 [(buf.validate.field).string = { + in: [ + "", + "kind", + "name", + "version", + "created_at" + ] + }]; + string sort_order = 6 [(buf.validate.field).string = { + in: [ + "", + "asc", + "desc" + ] + }]; +} + +message ListWorkflowTemplatesResponse { + common.v1.BaseResponse base = 1; + repeated WorkflowTemplate data = 2; + common.v1.PaginationResponse pagination = 3; +} + +// ============================================================================= +// SERVICE DEFINITION +// ============================================================================= + +// WorkflowTemplateService provides CRUD for workflow templates (PRD v1.3 §0.1 D6). +service WorkflowTemplateService { + rpc CreateWorkflowTemplate(CreateWorkflowTemplateRequest) returns (CreateWorkflowTemplateResponse) { + option (google.api.http) = { + post: "/api/v1/iam/workflow-templates" + body: "*" + }; + } + + rpc GetWorkflowTemplate(GetWorkflowTemplateRequest) returns (GetWorkflowTemplateResponse) { + option (google.api.http) = {get: "/api/v1/iam/workflow-templates/{template_id}"}; + } + + // Update creates a new version row; the prior version remains in history. + rpc UpdateWorkflowTemplate(UpdateWorkflowTemplateRequest) returns (UpdateWorkflowTemplateResponse) { + option (google.api.http) = { + put: "/api/v1/iam/workflow-templates/{template_id}" + body: "*" + }; + } + + // Activate marks this version active and deactivates all other versions of the same kind. + rpc ActivateWorkflowTemplate(ActivateWorkflowTemplateRequest) returns (ActivateWorkflowTemplateResponse) { + option (google.api.http) = { + post: "/api/v1/iam/workflow-templates/{template_id}/activate" + body: "*" + }; + } + + rpc DeleteWorkflowTemplate(DeleteWorkflowTemplateRequest) returns (DeleteWorkflowTemplateResponse) { + option (google.api.http) = {delete: "/api/v1/iam/workflow-templates/{template_id}"}; + } + + rpc ListWorkflowTemplates(ListWorkflowTemplatesRequest) returns (ListWorkflowTemplatesResponse) { + option (google.api.http) = {get: "/api/v1/iam/workflow-templates"}; + } +} + +// ============================================================================= +// WORKFLOW INSTANCE — runtime state machine +// ============================================================================= + +// WorkflowInstance is a live execution of a workflow template against an entity. +message WorkflowInstance { + string instance_id = 1; + string template_id = 2; + int32 template_version = 3; + string kind = 4; // PRODUCT_COSTING | PARAM_FILL + string entity_kind = 5; // PRD_REQUEST | CST_PRODUCT | PARAM_FILL + string entity_id = 6; + int32 current_step_no = 7; + string status = 8; // IN_PROGRESS | APPROVED | REJECTED | LOCKED | UNLOCKED + string started_by = 9; + string started_at = 10; + string completed_at = 11; + repeated WorkflowInstanceStep steps = 12; +} + +// WorkflowInstanceStep is one snapshot row inside a running instance. +message WorkflowInstanceStep { + string instance_step_id = 1; + string instance_id = 2; + int32 step_no = 3; + string step_name = 4; + string approver_resolution_type = 5; + string approver_resolution_value = 6; + int32 sla_hours = 7; + string assigned_at = 8; + string actor_user_id = 9; + string decision = 10; // "" | APPROVED | REJECTED | REASSIGNED | SKIPPED + string decided_at = 11; + string comment = 12; + string stuck_since = 13; +} + +// ============================================================================= +// MESSAGES — Start +// ============================================================================= + +message StartWorkflowInstanceRequest { + // Template id to instantiate. Must be active. + string template_id = 1 [(buf.validate.field).string.uuid = true]; + string entity_kind = 2 [(buf.validate.field).string = { + in: [ + "PRD_REQUEST", + "CST_PRODUCT", + "PARAM_FILL" + ] + }]; + string entity_id = 3 [(buf.validate.field).string.uuid = true]; +} + +message StartWorkflowInstanceResponse { + common.v1.BaseResponse base = 1; + WorkflowInstance data = 2; +} + +// ============================================================================= +// MESSAGES — Advance (Approve current step) +// ============================================================================= + +message AdvanceWorkflowInstanceRequest { + string instance_id = 1 [(buf.validate.field).string.uuid = true]; + string comment = 2 [(buf.validate.field).string.max_len = 1000]; +} + +message AdvanceWorkflowInstanceResponse { + common.v1.BaseResponse base = 1; + WorkflowInstance data = 2; +} + +// ============================================================================= +// MESSAGES — Reject +// ============================================================================= + +message RejectWorkflowInstanceRequest { + string instance_id = 1 [(buf.validate.field).string.uuid = true]; + string comment = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 1000 + }]; +} + +message RejectWorkflowInstanceResponse { + common.v1.BaseResponse base = 1; + WorkflowInstance data = 2; +} + +// ============================================================================= +// MESSAGES — Get +// ============================================================================= + +message GetWorkflowInstanceRequest { + string instance_id = 1 [(buf.validate.field).string.uuid = true]; +} + +message GetWorkflowInstanceResponse { + common.v1.BaseResponse base = 1; + WorkflowInstance data = 2; +} + +// ============================================================================= +// MESSAGES — List +// ============================================================================= + +message ListWorkflowInstancesRequest { + string entity_kind = 1 [(buf.validate.field).string = { + in: [ + "", + "PRD_REQUEST", + "CST_PRODUCT", + "PARAM_FILL" + ] + }]; + string entity_id = 2; + string status = 3 [(buf.validate.field).string = { + in: [ + "", + "IN_PROGRESS", + "APPROVED", + "REJECTED", + "LOCKED", + "UNLOCKED" + ] + }]; + common.v1.PaginationRequest pagination = 4; +} + +message ListWorkflowInstancesResponse { + common.v1.BaseResponse base = 1; + repeated WorkflowInstance data = 2; + common.v1.PaginationResponse pagination = 3; +} + +// ============================================================================= +// SERVICE DEFINITION — runtime +// ============================================================================= + +// WorkflowInstanceService drives the runtime state machine. +service WorkflowInstanceService { + // StartWorkflowInstance snapshots the active template and creates an instance + // with its first step pre-assigned. + rpc StartWorkflowInstance(StartWorkflowInstanceRequest) returns (StartWorkflowInstanceResponse) { + option (google.api.http) = { + post: "/api/v1/iam/workflow-instances" + body: "*" + }; + } + + rpc GetWorkflowInstance(GetWorkflowInstanceRequest) returns (GetWorkflowInstanceResponse) { + option (google.api.http) = {get: "/api/v1/iam/workflow-instances/{instance_id}"}; + } + + rpc ListWorkflowInstances(ListWorkflowInstancesRequest) returns (ListWorkflowInstancesResponse) { + option (google.api.http) = {get: "/api/v1/iam/workflow-instances"}; + } + + // Advance approves the current step. If it was the last step, status becomes LOCKED. + rpc AdvanceWorkflowInstance(AdvanceWorkflowInstanceRequest) returns (AdvanceWorkflowInstanceResponse) { + option (google.api.http) = { + post: "/api/v1/iam/workflow-instances/{instance_id}/advance" + body: "*" + }; + } + + // Reject sets status REJECTED with the supplied comment. + rpc RejectWorkflowInstance(RejectWorkflowInstanceRequest) returns (RejectWorkflowInstanceResponse) { + option (google.api.http) = { + post: "/api/v1/iam/workflow-instances/{instance_id}/reject" + body: "*" + }; + } +}