diff --git a/.golangci.yml b/.golangci.yml index 072f75b..331d39d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -64,6 +64,9 @@ linters: - G104 misspell: locale: US + ignore-rules: + - colourant + - colour nestif: min-complexity: 4 exhaustive: diff --git a/gen/finance/v1/rm_cost.pb.go b/gen/finance/v1/rm_cost.pb.go new file mode 100644 index 0000000..b68d12c --- /dev/null +++ b/gen/finance/v1/rm_cost.pb.go @@ -0,0 +1,1961 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: finance/v1/rm_cost.proto + +package financev1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + v1 "github.com/mutugading/goapps-backend/gen/common/v1" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// RMCostType discriminates a group-level cost row from an item-level cost row. +type RMCostType int32 + +const ( + // Default zero value. Means "no filter" in list requests. + RMCostType_RM_COST_TYPE_UNSPECIFIED RMCostType = 0 + // Cost row aggregates a whole RM group. + RMCostType_RM_COST_TYPE_GROUP RMCostType = 1 + // Cost row is computed for a single item (future phase). + RMCostType_RM_COST_TYPE_ITEM RMCostType = 2 +) + +// Enum value maps for RMCostType. +var ( + RMCostType_name = map[int32]string{ + 0: "RM_COST_TYPE_UNSPECIFIED", + 1: "RM_COST_TYPE_GROUP", + 2: "RM_COST_TYPE_ITEM", + } + RMCostType_value = map[string]int32{ + "RM_COST_TYPE_UNSPECIFIED": 0, + "RM_COST_TYPE_GROUP": 1, + "RM_COST_TYPE_ITEM": 2, + } +) + +func (x RMCostType) Enum() *RMCostType { + p := new(RMCostType) + *p = x + return p +} + +func (x RMCostType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RMCostType) Descriptor() protoreflect.EnumDescriptor { + return file_finance_v1_rm_cost_proto_enumTypes[0].Descriptor() +} + +func (RMCostType) Type() protoreflect.EnumType { + return &file_finance_v1_rm_cost_proto_enumTypes[0] +} + +func (x RMCostType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RMCostType.Descriptor instead. +func (RMCostType) EnumDescriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{0} +} + +// RMCostTriggerReason enumerates the reasons a calculation was run. Mirrors +// `rmcost.HistoryTriggerReason`. +type RMCostTriggerReason int32 + +const ( + // Default zero value. Rejected on trigger requests via not_in: [0]. + RMCostTriggerReason_RM_COST_TRIGGER_REASON_UNSPECIFIED RMCostTriggerReason = 0 + // Auto-chained after a successful Oracle sync for the synced period. + RMCostTriggerReason_RM_COST_TRIGGER_REASON_ORACLE_SYNC_CHAIN RMCostTriggerReason = 1 + // Group header changed. + RMCostTriggerReason_RM_COST_TRIGGER_REASON_GROUP_UPDATE RMCostTriggerReason = 2 + // An item was added/removed/toggled in the group. + RMCostTriggerReason_RM_COST_TRIGGER_REASON_DETAIL_CHANGE RMCostTriggerReason = 3 + // Explicit user request from the UI. + RMCostTriggerReason_RM_COST_TRIGGER_REASON_MANUAL_UI RMCostTriggerReason = 4 +) + +// Enum value maps for RMCostTriggerReason. +var ( + RMCostTriggerReason_name = map[int32]string{ + 0: "RM_COST_TRIGGER_REASON_UNSPECIFIED", + 1: "RM_COST_TRIGGER_REASON_ORACLE_SYNC_CHAIN", + 2: "RM_COST_TRIGGER_REASON_GROUP_UPDATE", + 3: "RM_COST_TRIGGER_REASON_DETAIL_CHANGE", + 4: "RM_COST_TRIGGER_REASON_MANUAL_UI", + } + RMCostTriggerReason_value = map[string]int32{ + "RM_COST_TRIGGER_REASON_UNSPECIFIED": 0, + "RM_COST_TRIGGER_REASON_ORACLE_SYNC_CHAIN": 1, + "RM_COST_TRIGGER_REASON_GROUP_UPDATE": 2, + "RM_COST_TRIGGER_REASON_DETAIL_CHANGE": 3, + "RM_COST_TRIGGER_REASON_MANUAL_UI": 4, + } +) + +func (x RMCostTriggerReason) Enum() *RMCostTriggerReason { + p := new(RMCostTriggerReason) + *p = x + return p +} + +func (x RMCostTriggerReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RMCostTriggerReason) Descriptor() protoreflect.EnumDescriptor { + return file_finance_v1_rm_cost_proto_enumTypes[1].Descriptor() +} + +func (RMCostTriggerReason) Type() protoreflect.EnumType { + return &file_finance_v1_rm_cost_proto_enumTypes[1] +} + +func (x RMCostTriggerReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RMCostTriggerReason.Descriptor instead. +func (RMCostTriggerReason) EnumDescriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{1} +} + +// RMCostRates is the per-stage snapshot of aggregated rates captured at calc time. +type RMCostRates struct { + state protoimpl.MessageState `protogen:"open.v1"` + // CONS stage rate. + Cons float64 `protobuf:"fixed64,1,opt,name=cons,proto3" json:"cons,omitempty"` + // STORES stage rate. + Stores float64 `protobuf:"fixed64,2,opt,name=stores,proto3" json:"stores,omitempty"` + // DEPT stage rate. + Dept float64 `protobuf:"fixed64,3,opt,name=dept,proto3" json:"dept,omitempty"` + // First PO stage rate. + Po_1 float64 `protobuf:"fixed64,4,opt,name=po_1,json=po1,proto3" json:"po_1,omitempty"` + // Second PO stage rate. + Po_2 float64 `protobuf:"fixed64,5,opt,name=po_2,json=po2,proto3" json:"po_2,omitempty"` + // Third PO stage rate. + Po_3 float64 `protobuf:"fixed64,6,opt,name=po_3,json=po3,proto3" json:"po_3,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RMCostRates) Reset() { + *x = RMCostRates{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RMCostRates) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RMCostRates) ProtoMessage() {} + +func (x *RMCostRates) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RMCostRates.ProtoReflect.Descriptor instead. +func (*RMCostRates) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{0} +} + +func (x *RMCostRates) GetCons() float64 { + if x != nil { + return x.Cons + } + return 0 +} + +func (x *RMCostRates) GetStores() float64 { + if x != nil { + return x.Stores + } + return 0 +} + +func (x *RMCostRates) GetDept() float64 { + if x != nil { + return x.Dept + } + return 0 +} + +func (x *RMCostRates) GetPo_1() float64 { + if x != nil { + return x.Po_1 + } + return 0 +} + +func (x *RMCostRates) GetPo_2() float64 { + if x != nil { + return x.Po_2 + } + return 0 +} + +func (x *RMCostRates) GetPo_3() float64 { + if x != nil { + return x.Po_3 + } + return 0 +} + +// RMCost is the landed cost computed for a single (period, rm_code) pair. +type RMCost struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Cost row UUID. + RmCostId string `protobuf:"bytes,1,opt,name=rm_cost_id,json=rmCostId,proto3" json:"rm_cost_id,omitempty"` + // Period (YYYYMM). + Period string `protobuf:"bytes,2,opt,name=period,proto3" json:"period,omitempty"` + // Group code (rm_type=GROUP) or item code (rm_type=ITEM). + RmCode string `protobuf:"bytes,3,opt,name=rm_code,json=rmCode,proto3" json:"rm_code,omitempty"` + // Discriminator. + RmType RMCostType `protobuf:"varint,4,opt,name=rm_type,json=rmType,proto3,enum=finance.v1.RMCostType" json:"rm_type,omitempty"` + // Owning group head UUID (set when rm_type=GROUP). + GroupHeadId *string `protobuf:"bytes,5,opt,name=group_head_id,json=groupHeadId,proto3,oneof" json:"group_head_id,omitempty"` + // Item code (set when rm_type=ITEM). + ItemCode *string `protobuf:"bytes,6,opt,name=item_code,json=itemCode,proto3,oneof" json:"item_code,omitempty"` + // Display name of the RM (group or item). + RmName string `protobuf:"bytes,7,opt,name=rm_name,json=rmName,proto3" json:"rm_name,omitempty"` + // UOM code. + UomCode string `protobuf:"bytes,8,opt,name=uom_code,json=uomCode,proto3" json:"uom_code,omitempty"` + // Per-stage rate snapshot. + Rates *RMCostRates `protobuf:"bytes,9,opt,name=rates,proto3" json:"rates,omitempty"` + // Computed valuation landed cost (nil when never calculated). + CostValuation *float64 `protobuf:"fixed64,10,opt,name=cost_valuation,json=costValuation,proto3,oneof" json:"cost_valuation,omitempty"` + // Computed marketing landed cost. + CostMarketing *float64 `protobuf:"fixed64,11,opt,name=cost_marketing,json=costMarketing,proto3,oneof" json:"cost_marketing,omitempty"` + // Computed simulation landed cost. + CostSimulation *float64 `protobuf:"fixed64,12,opt,name=cost_simulation,json=costSimulation,proto3,oneof" json:"cost_simulation,omitempty"` + // Flags configured on the group header at calc time. + FlagValuation RMGroupFlag `protobuf:"varint,13,opt,name=flag_valuation,json=flagValuation,proto3,enum=finance.v1.RMGroupFlag" json:"flag_valuation,omitempty"` + // Flag configured on the group header at calc time. + FlagMarketing RMGroupFlag `protobuf:"varint,14,opt,name=flag_marketing,json=flagMarketing,proto3,enum=finance.v1.RMGroupFlag" json:"flag_marketing,omitempty"` + // Flag configured on the group header at calc time. + FlagSimulation RMGroupFlag `protobuf:"varint,15,opt,name=flag_simulation,json=flagSimulation,proto3,enum=finance.v1.RMGroupFlag" json:"flag_simulation,omitempty"` + // Stage actually used after cascade / INIT resolution. + FlagValuationUsed RMGroupFlag `protobuf:"varint,17,opt,name=flag_valuation_used,json=flagValuationUsed,proto3,enum=finance.v1.RMGroupFlag" json:"flag_valuation_used,omitempty"` + // Stage actually used after cascade / INIT resolution. + FlagMarketingUsed RMGroupFlag `protobuf:"varint,18,opt,name=flag_marketing_used,json=flagMarketingUsed,proto3,enum=finance.v1.RMGroupFlag" json:"flag_marketing_used,omitempty"` + // Stage actually used after cascade / INIT resolution. + FlagSimulationUsed RMGroupFlag `protobuf:"varint,19,opt,name=flag_simulation_used,json=flagSimulationUsed,proto3,enum=finance.v1.RMGroupFlag" json:"flag_simulation_used,omitempty"` + // Last calculation timestamp (RFC3339, empty when never calculated). + CalculatedAt string `protobuf:"bytes,20,opt,name=calculated_at,json=calculatedAt,proto3" json:"calculated_at,omitempty"` + // Last calculator (empty when never calculated). + CalculatedBy string `protobuf:"bytes,21,opt,name=calculated_by,json=calculatedBy,proto3" json:"calculated_by,omitempty"` + // Audit metadata. + Audit *v1.AuditInfo `protobuf:"bytes,16,opt,name=audit,proto3" json:"audit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RMCost) Reset() { + *x = RMCost{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RMCost) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RMCost) ProtoMessage() {} + +func (x *RMCost) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RMCost.ProtoReflect.Descriptor instead. +func (*RMCost) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{1} +} + +func (x *RMCost) GetRmCostId() string { + if x != nil { + return x.RmCostId + } + return "" +} + +func (x *RMCost) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *RMCost) GetRmCode() string { + if x != nil { + return x.RmCode + } + return "" +} + +func (x *RMCost) GetRmType() RMCostType { + if x != nil { + return x.RmType + } + return RMCostType_RM_COST_TYPE_UNSPECIFIED +} + +func (x *RMCost) GetGroupHeadId() string { + if x != nil && x.GroupHeadId != nil { + return *x.GroupHeadId + } + return "" +} + +func (x *RMCost) GetItemCode() string { + if x != nil && x.ItemCode != nil { + return *x.ItemCode + } + return "" +} + +func (x *RMCost) GetRmName() string { + if x != nil { + return x.RmName + } + return "" +} + +func (x *RMCost) GetUomCode() string { + if x != nil { + return x.UomCode + } + return "" +} + +func (x *RMCost) GetRates() *RMCostRates { + if x != nil { + return x.Rates + } + return nil +} + +func (x *RMCost) GetCostValuation() float64 { + if x != nil && x.CostValuation != nil { + return *x.CostValuation + } + return 0 +} + +func (x *RMCost) GetCostMarketing() float64 { + if x != nil && x.CostMarketing != nil { + return *x.CostMarketing + } + return 0 +} + +func (x *RMCost) GetCostSimulation() float64 { + if x != nil && x.CostSimulation != nil { + return *x.CostSimulation + } + return 0 +} + +func (x *RMCost) GetFlagValuation() RMGroupFlag { + if x != nil { + return x.FlagValuation + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMCost) GetFlagMarketing() RMGroupFlag { + if x != nil { + return x.FlagMarketing + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMCost) GetFlagSimulation() RMGroupFlag { + if x != nil { + return x.FlagSimulation + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMCost) GetFlagValuationUsed() RMGroupFlag { + if x != nil { + return x.FlagValuationUsed + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMCost) GetFlagMarketingUsed() RMGroupFlag { + if x != nil { + return x.FlagMarketingUsed + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMCost) GetFlagSimulationUsed() RMGroupFlag { + if x != nil { + return x.FlagSimulationUsed + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMCost) GetCalculatedAt() string { + if x != nil { + return x.CalculatedAt + } + return "" +} + +func (x *RMCost) GetCalculatedBy() string { + if x != nil { + return x.CalculatedBy + } + return "" +} + +func (x *RMCost) GetAudit() *v1.AuditInfo { + if x != nil { + return x.Audit + } + return nil +} + +// RMCostHistory is one row of the append-only audit trail written alongside every +// calculation pass. +type RMCostHistory struct { + state protoimpl.MessageState `protogen:"open.v1"` + // History row UUID. + HistoryId string `protobuf:"bytes,1,opt,name=history_id,json=historyId,proto3" json:"history_id,omitempty"` + // Cost row this history refers to (nil if the cost row was later deleted). + RmCostId *string `protobuf:"bytes,2,opt,name=rm_cost_id,json=rmCostId,proto3,oneof" json:"rm_cost_id,omitempty"` + // Job that produced this history row (nil when no job context). + JobId *string `protobuf:"bytes,3,opt,name=job_id,json=jobId,proto3,oneof" json:"job_id,omitempty"` + // Period. + Period string `protobuf:"bytes,4,opt,name=period,proto3" json:"period,omitempty"` + // RM code. + RmCode string `protobuf:"bytes,5,opt,name=rm_code,json=rmCode,proto3" json:"rm_code,omitempty"` + // Discriminator. + RmType RMCostType `protobuf:"varint,6,opt,name=rm_type,json=rmType,proto3,enum=finance.v1.RMCostType" json:"rm_type,omitempty"` + // Owning group head UUID (when rm_type=GROUP). + GroupHeadId *string `protobuf:"bytes,7,opt,name=group_head_id,json=groupHeadId,proto3,oneof" json:"group_head_id,omitempty"` + // Rates captured for this calc pass. + Rates *RMCostRates `protobuf:"bytes,8,opt,name=rates,proto3" json:"rates,omitempty"` + // Snapshot of head.cost_percentage at calc time. + CostPercentage float64 `protobuf:"fixed64,9,opt,name=cost_percentage,json=costPercentage,proto3" json:"cost_percentage,omitempty"` + // Snapshot of head.cost_per_kg at calc time. + CostPerKg float64 `protobuf:"fixed64,10,opt,name=cost_per_kg,json=costPerKg,proto3" json:"cost_per_kg,omitempty"` + // Configured flags at calc time. + FlagValuation RMGroupFlag `protobuf:"varint,11,opt,name=flag_valuation,json=flagValuation,proto3,enum=finance.v1.RMGroupFlag" json:"flag_valuation,omitempty"` + // Configured flags at calc time. + FlagMarketing RMGroupFlag `protobuf:"varint,12,opt,name=flag_marketing,json=flagMarketing,proto3,enum=finance.v1.RMGroupFlag" json:"flag_marketing,omitempty"` + // Configured flags at calc time. + FlagSimulation RMGroupFlag `protobuf:"varint,13,opt,name=flag_simulation,json=flagSimulation,proto3,enum=finance.v1.RMGroupFlag" json:"flag_simulation,omitempty"` + // Init-value overrides at calc time. + InitValValuation *float64 `protobuf:"fixed64,14,opt,name=init_val_valuation,json=initValValuation,proto3,oneof" json:"init_val_valuation,omitempty"` + // Init-value overrides at calc time. + InitValMarketing *float64 `protobuf:"fixed64,15,opt,name=init_val_marketing,json=initValMarketing,proto3,oneof" json:"init_val_marketing,omitempty"` + // Init-value overrides at calc time. + InitValSimulation *float64 `protobuf:"fixed64,16,opt,name=init_val_simulation,json=initValSimulation,proto3,oneof" json:"init_val_simulation,omitempty"` + // Computed costs at calc time. + CostValuation *float64 `protobuf:"fixed64,17,opt,name=cost_valuation,json=costValuation,proto3,oneof" json:"cost_valuation,omitempty"` + // Computed costs at calc time. + CostMarketing *float64 `protobuf:"fixed64,18,opt,name=cost_marketing,json=costMarketing,proto3,oneof" json:"cost_marketing,omitempty"` + // Computed costs at calc time. + CostSimulation *float64 `protobuf:"fixed64,19,opt,name=cost_simulation,json=costSimulation,proto3,oneof" json:"cost_simulation,omitempty"` + // Stages resolved at calc time. + FlagValuationUsed RMGroupFlag `protobuf:"varint,20,opt,name=flag_valuation_used,json=flagValuationUsed,proto3,enum=finance.v1.RMGroupFlag" json:"flag_valuation_used,omitempty"` + // Stages resolved at calc time. + FlagMarketingUsed RMGroupFlag `protobuf:"varint,21,opt,name=flag_marketing_used,json=flagMarketingUsed,proto3,enum=finance.v1.RMGroupFlag" json:"flag_marketing_used,omitempty"` + // Stages resolved at calc time. + FlagSimulationUsed RMGroupFlag `protobuf:"varint,22,opt,name=flag_simulation_used,json=flagSimulationUsed,proto3,enum=finance.v1.RMGroupFlag" json:"flag_simulation_used,omitempty"` + // Count of source items aggregated. + SourceItemCount int32 `protobuf:"varint,23,opt,name=source_item_count,json=sourceItemCount,proto3" json:"source_item_count,omitempty"` + // Why this calc was triggered. + TriggerReason RMCostTriggerReason `protobuf:"varint,24,opt,name=trigger_reason,json=triggerReason,proto3,enum=finance.v1.RMCostTriggerReason" json:"trigger_reason,omitempty"` + // When the calc ran (RFC3339). + CalculatedAt string `protobuf:"bytes,25,opt,name=calculated_at,json=calculatedAt,proto3" json:"calculated_at,omitempty"` + // Who ran the calc. + CalculatedBy string `protobuf:"bytes,26,opt,name=calculated_by,json=calculatedBy,proto3" json:"calculated_by,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RMCostHistory) Reset() { + *x = RMCostHistory{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RMCostHistory) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RMCostHistory) ProtoMessage() {} + +func (x *RMCostHistory) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RMCostHistory.ProtoReflect.Descriptor instead. +func (*RMCostHistory) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{2} +} + +func (x *RMCostHistory) GetHistoryId() string { + if x != nil { + return x.HistoryId + } + return "" +} + +func (x *RMCostHistory) GetRmCostId() string { + if x != nil && x.RmCostId != nil { + return *x.RmCostId + } + return "" +} + +func (x *RMCostHistory) GetJobId() string { + if x != nil && x.JobId != nil { + return *x.JobId + } + return "" +} + +func (x *RMCostHistory) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *RMCostHistory) GetRmCode() string { + if x != nil { + return x.RmCode + } + return "" +} + +func (x *RMCostHistory) GetRmType() RMCostType { + if x != nil { + return x.RmType + } + return RMCostType_RM_COST_TYPE_UNSPECIFIED +} + +func (x *RMCostHistory) GetGroupHeadId() string { + if x != nil && x.GroupHeadId != nil { + return *x.GroupHeadId + } + return "" +} + +func (x *RMCostHistory) GetRates() *RMCostRates { + if x != nil { + return x.Rates + } + return nil +} + +func (x *RMCostHistory) GetCostPercentage() float64 { + if x != nil { + return x.CostPercentage + } + return 0 +} + +func (x *RMCostHistory) GetCostPerKg() float64 { + if x != nil { + return x.CostPerKg + } + return 0 +} + +func (x *RMCostHistory) GetFlagValuation() RMGroupFlag { + if x != nil { + return x.FlagValuation + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMCostHistory) GetFlagMarketing() RMGroupFlag { + if x != nil { + return x.FlagMarketing + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMCostHistory) GetFlagSimulation() RMGroupFlag { + if x != nil { + return x.FlagSimulation + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMCostHistory) GetInitValValuation() float64 { + if x != nil && x.InitValValuation != nil { + return *x.InitValValuation + } + return 0 +} + +func (x *RMCostHistory) GetInitValMarketing() float64 { + if x != nil && x.InitValMarketing != nil { + return *x.InitValMarketing + } + return 0 +} + +func (x *RMCostHistory) GetInitValSimulation() float64 { + if x != nil && x.InitValSimulation != nil { + return *x.InitValSimulation + } + return 0 +} + +func (x *RMCostHistory) GetCostValuation() float64 { + if x != nil && x.CostValuation != nil { + return *x.CostValuation + } + return 0 +} + +func (x *RMCostHistory) GetCostMarketing() float64 { + if x != nil && x.CostMarketing != nil { + return *x.CostMarketing + } + return 0 +} + +func (x *RMCostHistory) GetCostSimulation() float64 { + if x != nil && x.CostSimulation != nil { + return *x.CostSimulation + } + return 0 +} + +func (x *RMCostHistory) GetFlagValuationUsed() RMGroupFlag { + if x != nil { + return x.FlagValuationUsed + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMCostHistory) GetFlagMarketingUsed() RMGroupFlag { + if x != nil { + return x.FlagMarketingUsed + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMCostHistory) GetFlagSimulationUsed() RMGroupFlag { + if x != nil { + return x.FlagSimulationUsed + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMCostHistory) GetSourceItemCount() int32 { + if x != nil { + return x.SourceItemCount + } + return 0 +} + +func (x *RMCostHistory) GetTriggerReason() RMCostTriggerReason { + if x != nil { + return x.TriggerReason + } + return RMCostTriggerReason_RM_COST_TRIGGER_REASON_UNSPECIFIED +} + +func (x *RMCostHistory) GetCalculatedAt() string { + if x != nil { + return x.CalculatedAt + } + return "" +} + +func (x *RMCostHistory) GetCalculatedBy() string { + if x != nil { + return x.CalculatedBy + } + return "" +} + +// TriggerRMCostCalculation enqueues a recalculation job. Returns immediately +// with a job ID the caller can poll via the job-execution endpoint. +type TriggerRMCostCalculationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Period to recalculate (YYYYMM). + Period string `protobuf:"bytes,1,opt,name=period,proto3" json:"period,omitempty"` + // When set, scope the calc to a single group. Empty = all active groups. + GroupHeadId *string `protobuf:"bytes,2,opt,name=group_head_id,json=groupHeadId,proto3,oneof" json:"group_head_id,omitempty"` + // Why the recalc was requested. + TriggerReason RMCostTriggerReason `protobuf:"varint,3,opt,name=trigger_reason,json=triggerReason,proto3,enum=finance.v1.RMCostTriggerReason" json:"trigger_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TriggerRMCostCalculationRequest) Reset() { + *x = TriggerRMCostCalculationRequest{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TriggerRMCostCalculationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TriggerRMCostCalculationRequest) ProtoMessage() {} + +func (x *TriggerRMCostCalculationRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TriggerRMCostCalculationRequest.ProtoReflect.Descriptor instead. +func (*TriggerRMCostCalculationRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{3} +} + +func (x *TriggerRMCostCalculationRequest) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *TriggerRMCostCalculationRequest) GetGroupHeadId() string { + if x != nil && x.GroupHeadId != nil { + return *x.GroupHeadId + } + return "" +} + +func (x *TriggerRMCostCalculationRequest) GetTriggerReason() RMCostTriggerReason { + if x != nil { + return x.TriggerReason + } + return RMCostTriggerReason_RM_COST_TRIGGER_REASON_UNSPECIFIED +} + +// Trigger response. +type TriggerRMCostCalculationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Enqueued job UUID. + JobId string `protobuf:"bytes,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TriggerRMCostCalculationResponse) Reset() { + *x = TriggerRMCostCalculationResponse{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TriggerRMCostCalculationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TriggerRMCostCalculationResponse) ProtoMessage() {} + +func (x *TriggerRMCostCalculationResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TriggerRMCostCalculationResponse.ProtoReflect.Descriptor instead. +func (*TriggerRMCostCalculationResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{4} +} + +func (x *TriggerRMCostCalculationResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *TriggerRMCostCalculationResponse) GetJobId() string { + if x != nil { + return x.JobId + } + return "" +} + +// CalculateRMCost runs a calculation synchronously and returns the produced rows. +// Intended for admin/troubleshooting use — production traffic should go through +// TriggerRMCostCalculation. +type CalculateRMCostRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Period (YYYYMM). + Period string `protobuf:"bytes,1,opt,name=period,proto3" json:"period,omitempty"` + // Optional single-group scope. + GroupHeadId *string `protobuf:"bytes,2,opt,name=group_head_id,json=groupHeadId,proto3,oneof" json:"group_head_id,omitempty"` + // Why this calc was run. + TriggerReason RMCostTriggerReason `protobuf:"varint,3,opt,name=trigger_reason,json=triggerReason,proto3,enum=finance.v1.RMCostTriggerReason" json:"trigger_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CalculateRMCostRequest) Reset() { + *x = CalculateRMCostRequest{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CalculateRMCostRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CalculateRMCostRequest) ProtoMessage() {} + +func (x *CalculateRMCostRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CalculateRMCostRequest.ProtoReflect.Descriptor instead. +func (*CalculateRMCostRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{5} +} + +func (x *CalculateRMCostRequest) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *CalculateRMCostRequest) GetGroupHeadId() string { + if x != nil && x.GroupHeadId != nil { + return *x.GroupHeadId + } + return "" +} + +func (x *CalculateRMCostRequest) GetTriggerReason() RMCostTriggerReason { + if x != nil { + return x.TriggerReason + } + return RMCostTriggerReason_RM_COST_TRIGGER_REASON_UNSPECIFIED +} + +// Calculate response. +type CalculateRMCostResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Number of group heads processed. + Processed int32 `protobuf:"varint,2,opt,name=processed,proto3" json:"processed,omitempty"` + // Number of group heads skipped (no active details / no source rows). + Skipped int32 `protobuf:"varint,3,opt,name=skipped,proto3" json:"skipped,omitempty"` + // Period that was calculated (echoes request). + Period string `protobuf:"bytes,4,opt,name=period,proto3" json:"period,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CalculateRMCostResponse) Reset() { + *x = CalculateRMCostResponse{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CalculateRMCostResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CalculateRMCostResponse) ProtoMessage() {} + +func (x *CalculateRMCostResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CalculateRMCostResponse.ProtoReflect.Descriptor instead. +func (*CalculateRMCostResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{6} +} + +func (x *CalculateRMCostResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *CalculateRMCostResponse) GetProcessed() int32 { + if x != nil { + return x.Processed + } + return 0 +} + +func (x *CalculateRMCostResponse) GetSkipped() int32 { + if x != nil { + return x.Skipped + } + return 0 +} + +func (x *CalculateRMCostResponse) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +// GetRMCost fetches a single cost row by (period, rm_code). +type GetRMCostRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Period (YYYYMM). + Period string `protobuf:"bytes,1,opt,name=period,proto3" json:"period,omitempty"` + // RM code (group or item). + RmCode string `protobuf:"bytes,2,opt,name=rm_code,json=rmCode,proto3" json:"rm_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRMCostRequest) Reset() { + *x = GetRMCostRequest{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRMCostRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRMCostRequest) ProtoMessage() {} + +func (x *GetRMCostRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRMCostRequest.ProtoReflect.Descriptor instead. +func (*GetRMCostRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{7} +} + +func (x *GetRMCostRequest) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *GetRMCostRequest) GetRmCode() string { + if x != nil { + return x.RmCode + } + return "" +} + +// Get response. +type GetRMCostResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Cost data. + Data *RMCost `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRMCostResponse) Reset() { + *x = GetRMCostResponse{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRMCostResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRMCostResponse) ProtoMessage() {} + +func (x *GetRMCostResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRMCostResponse.ProtoReflect.Descriptor instead. +func (*GetRMCostResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{8} +} + +func (x *GetRMCostResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *GetRMCostResponse) GetData() *RMCost { + if x != nil { + return x.Data + } + return nil +} + +// ListRMCosts lists cost rows with filter + pagination. +type ListRMCostsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Page number (1-indexed). + Page int32 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` + // Page size (1-100). + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Period filter (YYYYMM). Empty = all periods. + Period string `protobuf:"bytes,3,opt,name=period,proto3" json:"period,omitempty"` + // Filter by RM type. UNSPECIFIED = all. + RmType RMCostType `protobuf:"varint,4,opt,name=rm_type,json=rmType,proto3,enum=finance.v1.RMCostType" json:"rm_type,omitempty"` + // Optional group scope. + GroupHeadId *string `protobuf:"bytes,5,opt,name=group_head_id,json=groupHeadId,proto3,oneof" json:"group_head_id,omitempty"` + // Free-text search on rm_code + rm_name. + Search string `protobuf:"bytes,6,opt,name=search,proto3" json:"search,omitempty"` + // Sort field. + SortBy string `protobuf:"bytes,7,opt,name=sort_by,json=sortBy,proto3" json:"sort_by,omitempty"` + // Sort order. + SortOrder string `protobuf:"bytes,8,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRMCostsRequest) Reset() { + *x = ListRMCostsRequest{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRMCostsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRMCostsRequest) ProtoMessage() {} + +func (x *ListRMCostsRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRMCostsRequest.ProtoReflect.Descriptor instead. +func (*ListRMCostsRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{9} +} + +func (x *ListRMCostsRequest) GetPage() int32 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ListRMCostsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListRMCostsRequest) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *ListRMCostsRequest) GetRmType() RMCostType { + if x != nil { + return x.RmType + } + return RMCostType_RM_COST_TYPE_UNSPECIFIED +} + +func (x *ListRMCostsRequest) GetGroupHeadId() string { + if x != nil && x.GroupHeadId != nil { + return *x.GroupHeadId + } + return "" +} + +func (x *ListRMCostsRequest) GetSearch() string { + if x != nil { + return x.Search + } + return "" +} + +func (x *ListRMCostsRequest) GetSortBy() string { + if x != nil { + return x.SortBy + } + return "" +} + +func (x *ListRMCostsRequest) GetSortOrder() string { + if x != nil { + return x.SortOrder + } + return "" +} + +// List response. +type ListRMCostsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Cost rows. + Data []*RMCost `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty"` + // Pagination metadata. + Pagination *v1.PaginationResponse `protobuf:"bytes,3,opt,name=pagination,proto3" json:"pagination,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRMCostsResponse) Reset() { + *x = ListRMCostsResponse{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRMCostsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRMCostsResponse) ProtoMessage() {} + +func (x *ListRMCostsResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRMCostsResponse.ProtoReflect.Descriptor instead. +func (*ListRMCostsResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{10} +} + +func (x *ListRMCostsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ListRMCostsResponse) GetData() []*RMCost { + if x != nil { + return x.Data + } + return nil +} + +func (x *ListRMCostsResponse) GetPagination() *v1.PaginationResponse { + if x != nil { + return x.Pagination + } + return nil +} + +// ListRMCostHistory lists audit-history rows with filter + pagination. +type ListRMCostHistoryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Page number (1-indexed). + Page int32 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` + // Page size (1-100). + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Period filter (YYYYMM). Empty = all periods. + Period string `protobuf:"bytes,3,opt,name=period,proto3" json:"period,omitempty"` + // RM code filter (empty = all). + RmCode string `protobuf:"bytes,4,opt,name=rm_code,json=rmCode,proto3" json:"rm_code,omitempty"` + // Optional group scope. + GroupHeadId *string `protobuf:"bytes,5,opt,name=group_head_id,json=groupHeadId,proto3,oneof" json:"group_head_id,omitempty"` + // Optional job scope. + JobId *string `protobuf:"bytes,6,opt,name=job_id,json=jobId,proto3,oneof" json:"job_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRMCostHistoryRequest) Reset() { + *x = ListRMCostHistoryRequest{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRMCostHistoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRMCostHistoryRequest) ProtoMessage() {} + +func (x *ListRMCostHistoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRMCostHistoryRequest.ProtoReflect.Descriptor instead. +func (*ListRMCostHistoryRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{11} +} + +func (x *ListRMCostHistoryRequest) GetPage() int32 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ListRMCostHistoryRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListRMCostHistoryRequest) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *ListRMCostHistoryRequest) GetRmCode() string { + if x != nil { + return x.RmCode + } + return "" +} + +func (x *ListRMCostHistoryRequest) GetGroupHeadId() string { + if x != nil && x.GroupHeadId != nil { + return *x.GroupHeadId + } + return "" +} + +func (x *ListRMCostHistoryRequest) GetJobId() string { + if x != nil && x.JobId != nil { + return *x.JobId + } + return "" +} + +// History response. +type ListRMCostHistoryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // History rows ordered by calculated_at DESC. + Data []*RMCostHistory `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty"` + // Pagination metadata. + Pagination *v1.PaginationResponse `protobuf:"bytes,3,opt,name=pagination,proto3" json:"pagination,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRMCostHistoryResponse) Reset() { + *x = ListRMCostHistoryResponse{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRMCostHistoryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRMCostHistoryResponse) ProtoMessage() {} + +func (x *ListRMCostHistoryResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRMCostHistoryResponse.ProtoReflect.Descriptor instead. +func (*ListRMCostHistoryResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{12} +} + +func (x *ListRMCostHistoryResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ListRMCostHistoryResponse) GetData() []*RMCostHistory { + if x != nil { + return x.Data + } + return nil +} + +func (x *ListRMCostHistoryResponse) GetPagination() *v1.PaginationResponse { + if x != nil { + return x.Pagination + } + return nil +} + +// ListRMCostPeriods has no filter arguments. +type ListRMCostPeriodsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRMCostPeriodsRequest) Reset() { + *x = ListRMCostPeriodsRequest{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRMCostPeriodsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRMCostPeriodsRequest) ProtoMessage() {} + +func (x *ListRMCostPeriodsRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRMCostPeriodsRequest.ProtoReflect.Descriptor instead. +func (*ListRMCostPeriodsRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{13} +} + +// ExportRMCostsRequest filters the cost rows to export. No pagination. +type ExportRMCostsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Period filter (YYYYMM). Empty = all periods. + Period string `protobuf:"bytes,1,opt,name=period,proto3" json:"period,omitempty"` + // Filter by RM type. UNSPECIFIED = all. + RmType RMCostType `protobuf:"varint,2,opt,name=rm_type,json=rmType,proto3,enum=finance.v1.RMCostType" json:"rm_type,omitempty"` + // Optional group scope. + GroupHeadId *string `protobuf:"bytes,3,opt,name=group_head_id,json=groupHeadId,proto3,oneof" json:"group_head_id,omitempty"` + // Free-text search on rm_code + rm_name. + Search string `protobuf:"bytes,4,opt,name=search,proto3" json:"search,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExportRMCostsRequest) Reset() { + *x = ExportRMCostsRequest{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExportRMCostsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportRMCostsRequest) ProtoMessage() {} + +func (x *ExportRMCostsRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportRMCostsRequest.ProtoReflect.Descriptor instead. +func (*ExportRMCostsRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{14} +} + +func (x *ExportRMCostsRequest) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *ExportRMCostsRequest) GetRmType() RMCostType { + if x != nil { + return x.RmType + } + return RMCostType_RM_COST_TYPE_UNSPECIFIED +} + +func (x *ExportRMCostsRequest) GetGroupHeadId() string { + if x != nil && x.GroupHeadId != nil { + return *x.GroupHeadId + } + return "" +} + +func (x *ExportRMCostsRequest) GetSearch() string { + if x != nil { + return x.Search + } + return "" +} + +// ExportRMCostsResponse carries the Excel bytes + filename. +type ExportRMCostsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Excel file content (.xlsx). + FileContent []byte `protobuf:"bytes,2,opt,name=file_content,json=fileContent,proto3" json:"file_content,omitempty"` + // Suggested filename. + FileName string `protobuf:"bytes,3,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExportRMCostsResponse) Reset() { + *x = ExportRMCostsResponse{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExportRMCostsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportRMCostsResponse) ProtoMessage() {} + +func (x *ExportRMCostsResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportRMCostsResponse.ProtoReflect.Descriptor instead. +func (*ExportRMCostsResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{15} +} + +func (x *ExportRMCostsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ExportRMCostsResponse) GetFileContent() []byte { + if x != nil { + return x.FileContent + } + return nil +} + +func (x *ExportRMCostsResponse) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +// Response carries the distinct set of calculated periods. +type ListRMCostPeriodsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Distinct periods ordered DESC (newest first), YYYYMM strings. + Periods []string `protobuf:"bytes,2,rep,name=periods,proto3" json:"periods,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRMCostPeriodsResponse) Reset() { + *x = ListRMCostPeriodsResponse{} + mi := &file_finance_v1_rm_cost_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRMCostPeriodsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRMCostPeriodsResponse) ProtoMessage() {} + +func (x *ListRMCostPeriodsResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_cost_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRMCostPeriodsResponse.ProtoReflect.Descriptor instead. +func (*ListRMCostPeriodsResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_cost_proto_rawDescGZIP(), []int{16} +} + +func (x *ListRMCostPeriodsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ListRMCostPeriodsResponse) GetPeriods() []string { + if x != nil { + return x.Periods + } + return nil +} + +var File_finance_v1_rm_cost_proto protoreflect.FileDescriptor + +const file_finance_v1_rm_cost_proto_rawDesc = "" + + "\n" + + "\x18finance/v1/rm_cost.proto\x12\n" + + "finance.v1\x1a\x1bbuf/validate/validate.proto\x1a\x16common/v1/common.proto\x1a\x19finance/v1/rm_group.proto\x1a\x1cgoogle/api/annotations.proto\"\x86\x01\n" + + "\vRMCostRates\x12\x12\n" + + "\x04cons\x18\x01 \x01(\x01R\x04cons\x12\x16\n" + + "\x06stores\x18\x02 \x01(\x01R\x06stores\x12\x12\n" + + "\x04dept\x18\x03 \x01(\x01R\x04dept\x12\x11\n" + + "\x04po_1\x18\x04 \x01(\x01R\x03po1\x12\x11\n" + + "\x04po_2\x18\x05 \x01(\x01R\x03po2\x12\x11\n" + + "\x04po_3\x18\x06 \x01(\x01R\x03po3\"\xab\b\n" + + "\x06RMCost\x12\x1c\n" + + "\n" + + "rm_cost_id\x18\x01 \x01(\tR\brmCostId\x12\x16\n" + + "\x06period\x18\x02 \x01(\tR\x06period\x12\x17\n" + + "\arm_code\x18\x03 \x01(\tR\x06rmCode\x12/\n" + + "\arm_type\x18\x04 \x01(\x0e2\x16.finance.v1.RMCostTypeR\x06rmType\x12'\n" + + "\rgroup_head_id\x18\x05 \x01(\tH\x00R\vgroupHeadId\x88\x01\x01\x12 \n" + + "\titem_code\x18\x06 \x01(\tH\x01R\bitemCode\x88\x01\x01\x12\x17\n" + + "\arm_name\x18\a \x01(\tR\x06rmName\x12\x19\n" + + "\buom_code\x18\b \x01(\tR\auomCode\x12-\n" + + "\x05rates\x18\t \x01(\v2\x17.finance.v1.RMCostRatesR\x05rates\x12*\n" + + "\x0ecost_valuation\x18\n" + + " \x01(\x01H\x02R\rcostValuation\x88\x01\x01\x12*\n" + + "\x0ecost_marketing\x18\v \x01(\x01H\x03R\rcostMarketing\x88\x01\x01\x12,\n" + + "\x0fcost_simulation\x18\f \x01(\x01H\x04R\x0ecostSimulation\x88\x01\x01\x12>\n" + + "\x0eflag_valuation\x18\r \x01(\x0e2\x17.finance.v1.RMGroupFlagR\rflagValuation\x12>\n" + + "\x0eflag_marketing\x18\x0e \x01(\x0e2\x17.finance.v1.RMGroupFlagR\rflagMarketing\x12@\n" + + "\x0fflag_simulation\x18\x0f \x01(\x0e2\x17.finance.v1.RMGroupFlagR\x0eflagSimulation\x12G\n" + + "\x13flag_valuation_used\x18\x11 \x01(\x0e2\x17.finance.v1.RMGroupFlagR\x11flagValuationUsed\x12G\n" + + "\x13flag_marketing_used\x18\x12 \x01(\x0e2\x17.finance.v1.RMGroupFlagR\x11flagMarketingUsed\x12I\n" + + "\x14flag_simulation_used\x18\x13 \x01(\x0e2\x17.finance.v1.RMGroupFlagR\x12flagSimulationUsed\x12#\n" + + "\rcalculated_at\x18\x14 \x01(\tR\fcalculatedAt\x12#\n" + + "\rcalculated_by\x18\x15 \x01(\tR\fcalculatedBy\x12*\n" + + "\x05audit\x18\x10 \x01(\v2\x14.common.v1.AuditInfoR\x05auditB\x10\n" + + "\x0e_group_head_idB\f\n" + + "\n" + + "_item_codeB\x11\n" + + "\x0f_cost_valuationB\x11\n" + + "\x0f_cost_marketingB\x12\n" + + "\x10_cost_simulation\"\x9a\v\n" + + "\rRMCostHistory\x12\x1d\n" + + "\n" + + "history_id\x18\x01 \x01(\tR\thistoryId\x12!\n" + + "\n" + + "rm_cost_id\x18\x02 \x01(\tH\x00R\brmCostId\x88\x01\x01\x12\x1a\n" + + "\x06job_id\x18\x03 \x01(\tH\x01R\x05jobId\x88\x01\x01\x12\x16\n" + + "\x06period\x18\x04 \x01(\tR\x06period\x12\x17\n" + + "\arm_code\x18\x05 \x01(\tR\x06rmCode\x12/\n" + + "\arm_type\x18\x06 \x01(\x0e2\x16.finance.v1.RMCostTypeR\x06rmType\x12'\n" + + "\rgroup_head_id\x18\a \x01(\tH\x02R\vgroupHeadId\x88\x01\x01\x12-\n" + + "\x05rates\x18\b \x01(\v2\x17.finance.v1.RMCostRatesR\x05rates\x12'\n" + + "\x0fcost_percentage\x18\t \x01(\x01R\x0ecostPercentage\x12\x1e\n" + + "\vcost_per_kg\x18\n" + + " \x01(\x01R\tcostPerKg\x12>\n" + + "\x0eflag_valuation\x18\v \x01(\x0e2\x17.finance.v1.RMGroupFlagR\rflagValuation\x12>\n" + + "\x0eflag_marketing\x18\f \x01(\x0e2\x17.finance.v1.RMGroupFlagR\rflagMarketing\x12@\n" + + "\x0fflag_simulation\x18\r \x01(\x0e2\x17.finance.v1.RMGroupFlagR\x0eflagSimulation\x121\n" + + "\x12init_val_valuation\x18\x0e \x01(\x01H\x03R\x10initValValuation\x88\x01\x01\x121\n" + + "\x12init_val_marketing\x18\x0f \x01(\x01H\x04R\x10initValMarketing\x88\x01\x01\x123\n" + + "\x13init_val_simulation\x18\x10 \x01(\x01H\x05R\x11initValSimulation\x88\x01\x01\x12*\n" + + "\x0ecost_valuation\x18\x11 \x01(\x01H\x06R\rcostValuation\x88\x01\x01\x12*\n" + + "\x0ecost_marketing\x18\x12 \x01(\x01H\aR\rcostMarketing\x88\x01\x01\x12,\n" + + "\x0fcost_simulation\x18\x13 \x01(\x01H\bR\x0ecostSimulation\x88\x01\x01\x12G\n" + + "\x13flag_valuation_used\x18\x14 \x01(\x0e2\x17.finance.v1.RMGroupFlagR\x11flagValuationUsed\x12G\n" + + "\x13flag_marketing_used\x18\x15 \x01(\x0e2\x17.finance.v1.RMGroupFlagR\x11flagMarketingUsed\x12I\n" + + "\x14flag_simulation_used\x18\x16 \x01(\x0e2\x17.finance.v1.RMGroupFlagR\x12flagSimulationUsed\x12*\n" + + "\x11source_item_count\x18\x17 \x01(\x05R\x0fsourceItemCount\x12F\n" + + "\x0etrigger_reason\x18\x18 \x01(\x0e2\x1f.finance.v1.RMCostTriggerReasonR\rtriggerReason\x12#\n" + + "\rcalculated_at\x18\x19 \x01(\tR\fcalculatedAt\x12#\n" + + "\rcalculated_by\x18\x1a \x01(\tR\fcalculatedByB\r\n" + + "\v_rm_cost_idB\t\n" + + "\a_job_idB\x10\n" + + "\x0e_group_head_idB\x15\n" + + "\x13_init_val_valuationB\x15\n" + + "\x13_init_val_marketingB\x16\n" + + "\x14_init_val_simulationB\x11\n" + + "\x0f_cost_valuationB\x11\n" + + "\x0f_cost_marketingB\x12\n" + + "\x10_cost_simulation\"\xe3\x01\n" + + "\x1fTriggerRMCostCalculationRequest\x12)\n" + + "\x06period\x18\x01 \x01(\tB\x11\xbaH\x0er\f2\a^\\d{6}$\x98\x01\x06R\x06period\x121\n" + + "\rgroup_head_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01H\x00R\vgroupHeadId\x88\x01\x01\x12P\n" + + "\x0etrigger_reason\x18\x03 \x01(\x0e2\x1f.finance.v1.RMCostTriggerReasonB\b\xbaH\x05\x82\x01\x02 \x00R\rtriggerReasonB\x10\n" + + "\x0e_group_head_id\"f\n" + + " TriggerRMCostCalculationResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12\x15\n" + + "\x06job_id\x18\x02 \x01(\tR\x05jobId\"\xda\x01\n" + + "\x16CalculateRMCostRequest\x12)\n" + + "\x06period\x18\x01 \x01(\tB\x11\xbaH\x0er\f2\a^\\d{6}$\x98\x01\x06R\x06period\x121\n" + + "\rgroup_head_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01H\x00R\vgroupHeadId\x88\x01\x01\x12P\n" + + "\x0etrigger_reason\x18\x03 \x01(\x0e2\x1f.finance.v1.RMCostTriggerReasonB\b\xbaH\x05\x82\x01\x02 \x00R\rtriggerReasonB\x10\n" + + "\x0e_group_head_id\"\x96\x01\n" + + "\x17CalculateRMCostResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12\x1c\n" + + "\tprocessed\x18\x02 \x01(\x05R\tprocessed\x12\x18\n" + + "\askipped\x18\x03 \x01(\x05R\askipped\x12\x16\n" + + "\x06period\x18\x04 \x01(\tR\x06period\"a\n" + + "\x10GetRMCostRequest\x12)\n" + + "\x06period\x18\x01 \x01(\tB\x11\xbaH\x0er\f2\a^\\d{6}$\x98\x01\x06R\x06period\x12\"\n" + + "\arm_code\x18\x02 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x182R\x06rmCode\"h\n" + + "\x11GetRMCostResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12&\n" + + "\x04data\x18\x02 \x01(\v2\x12.finance.v1.RMCostR\x04data\"\x9b\x03\n" + + "\x12ListRMCostsRequest\x12\x1b\n" + + "\x04page\x18\x01 \x01(\x05B\a\xbaH\x04\x1a\x02(\x01R\x04page\x12&\n" + + "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d(\x01R\bpageSize\x12+\n" + + "\x06period\x18\x03 \x01(\tB\x13\xbaH\x10r\x0e\x18\x062\n" + + "^$|^\\d{6}$R\x06period\x12/\n" + + "\arm_type\x18\x04 \x01(\x0e2\x16.finance.v1.RMCostTypeR\x06rmType\x121\n" + + "\rgroup_head_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01H\x00R\vgroupHeadId\x88\x01\x01\x12\x1f\n" + + "\x06search\x18\x06 \x01(\tB\a\xbaH\x04r\x02\x18dR\x06search\x12I\n" + + "\asort_by\x18\a \x01(\tB0\xbaH-r+R\x00R\x06periodR\arm_codeR\arm_nameR\rcalculated_atR\x06sortBy\x121\n" + + "\n" + + "sort_order\x18\b \x01(\tB\x12\xbaH\x0fr\rR\x00R\x03ascR\x04descR\tsortOrderB\x10\n" + + "\x0e_group_head_id\"\xa9\x01\n" + + "\x13ListRMCostsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12&\n" + + "\x04data\x18\x02 \x03(\v2\x12.finance.v1.RMCostR\x04data\x12=\n" + + "\n" + + "pagination\x18\x03 \x01(\v2\x1d.common.v1.PaginationResponseR\n" + + "pagination\"\xa4\x02\n" + + "\x18ListRMCostHistoryRequest\x12\x1b\n" + + "\x04page\x18\x01 \x01(\x05B\a\xbaH\x04\x1a\x02(\x01R\x04page\x12&\n" + + "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d(\x01R\bpageSize\x12+\n" + + "\x06period\x18\x03 \x01(\tB\x13\xbaH\x10r\x0e\x18\x062\n" + + "^$|^\\d{6}$R\x06period\x12 \n" + + "\arm_code\x18\x04 \x01(\tB\a\xbaH\x04r\x02\x182R\x06rmCode\x121\n" + + "\rgroup_head_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01H\x00R\vgroupHeadId\x88\x01\x01\x12$\n" + + "\x06job_id\x18\x06 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01H\x01R\x05jobId\x88\x01\x01B\x10\n" + + "\x0e_group_head_idB\t\n" + + "\a_job_id\"\xb6\x01\n" + + "\x19ListRMCostHistoryResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12-\n" + + "\x04data\x18\x02 \x03(\v2\x19.finance.v1.RMCostHistoryR\x04data\x12=\n" + + "\n" + + "pagination\x18\x03 \x01(\v2\x1d.common.v1.PaginationResponseR\n" + + "pagination\"\x1a\n" + + "\x18ListRMCostPeriodsRequest\"\xda\x01\n" + + "\x14ExportRMCostsRequest\x12+\n" + + "\x06period\x18\x01 \x01(\tB\x13\xbaH\x10r\x0e\x18\x062\n" + + "^$|^\\d{6}$R\x06period\x12/\n" + + "\arm_type\x18\x02 \x01(\x0e2\x16.finance.v1.RMCostTypeR\x06rmType\x121\n" + + "\rgroup_head_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01H\x00R\vgroupHeadId\x88\x01\x01\x12\x1f\n" + + "\x06search\x18\x04 \x01(\tB\a\xbaH\x04r\x02\x18dR\x06searchB\x10\n" + + "\x0e_group_head_id\"\x84\x01\n" + + "\x15ExportRMCostsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12!\n" + + "\ffile_content\x18\x02 \x01(\fR\vfileContent\x12\x1b\n" + + "\tfile_name\x18\x03 \x01(\tR\bfileName\"b\n" + + "\x19ListRMCostPeriodsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12\x18\n" + + "\aperiods\x18\x02 \x03(\tR\aperiods*Y\n" + + "\n" + + "RMCostType\x12\x1c\n" + + "\x18RM_COST_TYPE_UNSPECIFIED\x10\x00\x12\x16\n" + + "\x12RM_COST_TYPE_GROUP\x10\x01\x12\x15\n" + + "\x11RM_COST_TYPE_ITEM\x10\x02*\xe4\x01\n" + + "\x13RMCostTriggerReason\x12&\n" + + "\"RM_COST_TRIGGER_REASON_UNSPECIFIED\x10\x00\x12,\n" + + "(RM_COST_TRIGGER_REASON_ORACLE_SYNC_CHAIN\x10\x01\x12'\n" + + "#RM_COST_TRIGGER_REASON_GROUP_UPDATE\x10\x02\x12(\n" + + "$RM_COST_TRIGGER_REASON_DETAIL_CHANGE\x10\x03\x12$\n" + + " RM_COST_TRIGGER_REASON_MANUAL_UI\x10\x042\xca\a\n" + + "\rRMCostService\x12\xa2\x01\n" + + "\x18TriggerRMCostCalculation\x12+.finance.v1.TriggerRMCostCalculationRequest\x1a,.finance.v1.TriggerRMCostCalculationResponse\"+\x82\xd3\xe4\x93\x02%:\x01*\" /api/v1/finance/rm-costs/trigger\x12\x89\x01\n" + + "\x0fCalculateRMCost\x12\".finance.v1.CalculateRMCostRequest\x1a#.finance.v1.CalculateRMCostResponse\"-\x82\xd3\xe4\x93\x02':\x01*\"\"/api/v1/finance/rm-costs/calculate\x12}\n" + + "\tGetRMCost\x12\x1c.finance.v1.GetRMCostRequest\x1a\x1d.finance.v1.GetRMCostResponse\"3\x82\xd3\xe4\x93\x02-\x12+/api/v1/finance/rm-costs/{period}/{rm_code}\x12p\n" + + "\vListRMCosts\x12\x1e.finance.v1.ListRMCostsRequest\x1a\x1f.finance.v1.ListRMCostsResponse\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/api/v1/finance/rm-costs\x12\x8a\x01\n" + + "\x11ListRMCostHistory\x12$.finance.v1.ListRMCostHistoryRequest\x1a%.finance.v1.ListRMCostHistoryResponse\"(\x82\xd3\xe4\x93\x02\"\x12 /api/v1/finance/rm-costs/history\x12\x8a\x01\n" + + "\x11ListRMCostPeriods\x12$.finance.v1.ListRMCostPeriodsRequest\x1a%.finance.v1.ListRMCostPeriodsResponse\"(\x82\xd3\xe4\x93\x02\"\x12 /api/v1/finance/rm-costs/periods\x12}\n" + + "\rExportRMCosts\x12 .finance.v1.ExportRMCostsRequest\x1a!.finance.v1.ExportRMCostsResponse\"'\x82\xd3\xe4\x93\x02!\x12\x1f/api/v1/finance/rm-costs/exportB\xa5\x01\n" + + "\x0ecom.finance.v1B\vRmCostProtoP\x01Z=github.com/mutugading/goapps-backend/gen/finance/v1;financev1\xa2\x02\x03FXX\xaa\x02\n" + + "Finance.V1\xca\x02\n" + + "Finance\\V1\xe2\x02\x16Finance\\V1\\GPBMetadata\xea\x02\vFinance::V1b\x06proto3" + +var ( + file_finance_v1_rm_cost_proto_rawDescOnce sync.Once + file_finance_v1_rm_cost_proto_rawDescData []byte +) + +func file_finance_v1_rm_cost_proto_rawDescGZIP() []byte { + file_finance_v1_rm_cost_proto_rawDescOnce.Do(func() { + file_finance_v1_rm_cost_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_finance_v1_rm_cost_proto_rawDesc), len(file_finance_v1_rm_cost_proto_rawDesc))) + }) + return file_finance_v1_rm_cost_proto_rawDescData +} + +var file_finance_v1_rm_cost_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_finance_v1_rm_cost_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_finance_v1_rm_cost_proto_goTypes = []any{ + (RMCostType)(0), // 0: finance.v1.RMCostType + (RMCostTriggerReason)(0), // 1: finance.v1.RMCostTriggerReason + (*RMCostRates)(nil), // 2: finance.v1.RMCostRates + (*RMCost)(nil), // 3: finance.v1.RMCost + (*RMCostHistory)(nil), // 4: finance.v1.RMCostHistory + (*TriggerRMCostCalculationRequest)(nil), // 5: finance.v1.TriggerRMCostCalculationRequest + (*TriggerRMCostCalculationResponse)(nil), // 6: finance.v1.TriggerRMCostCalculationResponse + (*CalculateRMCostRequest)(nil), // 7: finance.v1.CalculateRMCostRequest + (*CalculateRMCostResponse)(nil), // 8: finance.v1.CalculateRMCostResponse + (*GetRMCostRequest)(nil), // 9: finance.v1.GetRMCostRequest + (*GetRMCostResponse)(nil), // 10: finance.v1.GetRMCostResponse + (*ListRMCostsRequest)(nil), // 11: finance.v1.ListRMCostsRequest + (*ListRMCostsResponse)(nil), // 12: finance.v1.ListRMCostsResponse + (*ListRMCostHistoryRequest)(nil), // 13: finance.v1.ListRMCostHistoryRequest + (*ListRMCostHistoryResponse)(nil), // 14: finance.v1.ListRMCostHistoryResponse + (*ListRMCostPeriodsRequest)(nil), // 15: finance.v1.ListRMCostPeriodsRequest + (*ExportRMCostsRequest)(nil), // 16: finance.v1.ExportRMCostsRequest + (*ExportRMCostsResponse)(nil), // 17: finance.v1.ExportRMCostsResponse + (*ListRMCostPeriodsResponse)(nil), // 18: finance.v1.ListRMCostPeriodsResponse + (RMGroupFlag)(0), // 19: finance.v1.RMGroupFlag + (*v1.AuditInfo)(nil), // 20: common.v1.AuditInfo + (*v1.BaseResponse)(nil), // 21: common.v1.BaseResponse + (*v1.PaginationResponse)(nil), // 22: common.v1.PaginationResponse +} +var file_finance_v1_rm_cost_proto_depIdxs = []int32{ + 0, // 0: finance.v1.RMCost.rm_type:type_name -> finance.v1.RMCostType + 2, // 1: finance.v1.RMCost.rates:type_name -> finance.v1.RMCostRates + 19, // 2: finance.v1.RMCost.flag_valuation:type_name -> finance.v1.RMGroupFlag + 19, // 3: finance.v1.RMCost.flag_marketing:type_name -> finance.v1.RMGroupFlag + 19, // 4: finance.v1.RMCost.flag_simulation:type_name -> finance.v1.RMGroupFlag + 19, // 5: finance.v1.RMCost.flag_valuation_used:type_name -> finance.v1.RMGroupFlag + 19, // 6: finance.v1.RMCost.flag_marketing_used:type_name -> finance.v1.RMGroupFlag + 19, // 7: finance.v1.RMCost.flag_simulation_used:type_name -> finance.v1.RMGroupFlag + 20, // 8: finance.v1.RMCost.audit:type_name -> common.v1.AuditInfo + 0, // 9: finance.v1.RMCostHistory.rm_type:type_name -> finance.v1.RMCostType + 2, // 10: finance.v1.RMCostHistory.rates:type_name -> finance.v1.RMCostRates + 19, // 11: finance.v1.RMCostHistory.flag_valuation:type_name -> finance.v1.RMGroupFlag + 19, // 12: finance.v1.RMCostHistory.flag_marketing:type_name -> finance.v1.RMGroupFlag + 19, // 13: finance.v1.RMCostHistory.flag_simulation:type_name -> finance.v1.RMGroupFlag + 19, // 14: finance.v1.RMCostHistory.flag_valuation_used:type_name -> finance.v1.RMGroupFlag + 19, // 15: finance.v1.RMCostHistory.flag_marketing_used:type_name -> finance.v1.RMGroupFlag + 19, // 16: finance.v1.RMCostHistory.flag_simulation_used:type_name -> finance.v1.RMGroupFlag + 1, // 17: finance.v1.RMCostHistory.trigger_reason:type_name -> finance.v1.RMCostTriggerReason + 1, // 18: finance.v1.TriggerRMCostCalculationRequest.trigger_reason:type_name -> finance.v1.RMCostTriggerReason + 21, // 19: finance.v1.TriggerRMCostCalculationResponse.base:type_name -> common.v1.BaseResponse + 1, // 20: finance.v1.CalculateRMCostRequest.trigger_reason:type_name -> finance.v1.RMCostTriggerReason + 21, // 21: finance.v1.CalculateRMCostResponse.base:type_name -> common.v1.BaseResponse + 21, // 22: finance.v1.GetRMCostResponse.base:type_name -> common.v1.BaseResponse + 3, // 23: finance.v1.GetRMCostResponse.data:type_name -> finance.v1.RMCost + 0, // 24: finance.v1.ListRMCostsRequest.rm_type:type_name -> finance.v1.RMCostType + 21, // 25: finance.v1.ListRMCostsResponse.base:type_name -> common.v1.BaseResponse + 3, // 26: finance.v1.ListRMCostsResponse.data:type_name -> finance.v1.RMCost + 22, // 27: finance.v1.ListRMCostsResponse.pagination:type_name -> common.v1.PaginationResponse + 21, // 28: finance.v1.ListRMCostHistoryResponse.base:type_name -> common.v1.BaseResponse + 4, // 29: finance.v1.ListRMCostHistoryResponse.data:type_name -> finance.v1.RMCostHistory + 22, // 30: finance.v1.ListRMCostHistoryResponse.pagination:type_name -> common.v1.PaginationResponse + 0, // 31: finance.v1.ExportRMCostsRequest.rm_type:type_name -> finance.v1.RMCostType + 21, // 32: finance.v1.ExportRMCostsResponse.base:type_name -> common.v1.BaseResponse + 21, // 33: finance.v1.ListRMCostPeriodsResponse.base:type_name -> common.v1.BaseResponse + 5, // 34: finance.v1.RMCostService.TriggerRMCostCalculation:input_type -> finance.v1.TriggerRMCostCalculationRequest + 7, // 35: finance.v1.RMCostService.CalculateRMCost:input_type -> finance.v1.CalculateRMCostRequest + 9, // 36: finance.v1.RMCostService.GetRMCost:input_type -> finance.v1.GetRMCostRequest + 11, // 37: finance.v1.RMCostService.ListRMCosts:input_type -> finance.v1.ListRMCostsRequest + 13, // 38: finance.v1.RMCostService.ListRMCostHistory:input_type -> finance.v1.ListRMCostHistoryRequest + 15, // 39: finance.v1.RMCostService.ListRMCostPeriods:input_type -> finance.v1.ListRMCostPeriodsRequest + 16, // 40: finance.v1.RMCostService.ExportRMCosts:input_type -> finance.v1.ExportRMCostsRequest + 6, // 41: finance.v1.RMCostService.TriggerRMCostCalculation:output_type -> finance.v1.TriggerRMCostCalculationResponse + 8, // 42: finance.v1.RMCostService.CalculateRMCost:output_type -> finance.v1.CalculateRMCostResponse + 10, // 43: finance.v1.RMCostService.GetRMCost:output_type -> finance.v1.GetRMCostResponse + 12, // 44: finance.v1.RMCostService.ListRMCosts:output_type -> finance.v1.ListRMCostsResponse + 14, // 45: finance.v1.RMCostService.ListRMCostHistory:output_type -> finance.v1.ListRMCostHistoryResponse + 18, // 46: finance.v1.RMCostService.ListRMCostPeriods:output_type -> finance.v1.ListRMCostPeriodsResponse + 17, // 47: finance.v1.RMCostService.ExportRMCosts:output_type -> finance.v1.ExportRMCostsResponse + 41, // [41:48] is the sub-list for method output_type + 34, // [34:41] is the sub-list for method input_type + 34, // [34:34] is the sub-list for extension type_name + 34, // [34:34] is the sub-list for extension extendee + 0, // [0:34] is the sub-list for field type_name +} + +func init() { file_finance_v1_rm_cost_proto_init() } +func file_finance_v1_rm_cost_proto_init() { + if File_finance_v1_rm_cost_proto != nil { + return + } + file_finance_v1_rm_group_proto_init() + file_finance_v1_rm_cost_proto_msgTypes[1].OneofWrappers = []any{} + file_finance_v1_rm_cost_proto_msgTypes[2].OneofWrappers = []any{} + file_finance_v1_rm_cost_proto_msgTypes[3].OneofWrappers = []any{} + file_finance_v1_rm_cost_proto_msgTypes[5].OneofWrappers = []any{} + file_finance_v1_rm_cost_proto_msgTypes[9].OneofWrappers = []any{} + file_finance_v1_rm_cost_proto_msgTypes[11].OneofWrappers = []any{} + file_finance_v1_rm_cost_proto_msgTypes[14].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_finance_v1_rm_cost_proto_rawDesc), len(file_finance_v1_rm_cost_proto_rawDesc)), + NumEnums: 2, + NumMessages: 17, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_finance_v1_rm_cost_proto_goTypes, + DependencyIndexes: file_finance_v1_rm_cost_proto_depIdxs, + EnumInfos: file_finance_v1_rm_cost_proto_enumTypes, + MessageInfos: file_finance_v1_rm_cost_proto_msgTypes, + }.Build() + File_finance_v1_rm_cost_proto = out.File + file_finance_v1_rm_cost_proto_goTypes = nil + file_finance_v1_rm_cost_proto_depIdxs = nil +} diff --git a/gen/finance/v1/rm_cost.pb.gw.go b/gen/finance/v1/rm_cost.pb.gw.go new file mode 100644 index 0000000..48326fb --- /dev/null +++ b/gen/finance/v1/rm_cost.pb.gw.go @@ -0,0 +1,599 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: finance/v1/rm_cost.proto + +/* +Package financev1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package financev1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_RMCostService_TriggerRMCostCalculation_0(ctx context.Context, marshaler runtime.Marshaler, client RMCostServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq TriggerRMCostCalculationRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.TriggerRMCostCalculation(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMCostService_TriggerRMCostCalculation_0(ctx context.Context, marshaler runtime.Marshaler, server RMCostServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq TriggerRMCostCalculationRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.TriggerRMCostCalculation(ctx, &protoReq) + return msg, metadata, err +} + +func request_RMCostService_CalculateRMCost_0(ctx context.Context, marshaler runtime.Marshaler, client RMCostServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CalculateRMCostRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.CalculateRMCost(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMCostService_CalculateRMCost_0(ctx context.Context, marshaler runtime.Marshaler, server RMCostServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CalculateRMCostRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CalculateRMCost(ctx, &protoReq) + return msg, metadata, err +} + +func request_RMCostService_GetRMCost_0(ctx context.Context, marshaler runtime.Marshaler, client RMCostServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetRMCostRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["period"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "period") + } + protoReq.Period, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "period", err) + } + val, ok = pathParams["rm_code"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "rm_code") + } + protoReq.RmCode, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "rm_code", err) + } + msg, err := client.GetRMCost(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMCostService_GetRMCost_0(ctx context.Context, marshaler runtime.Marshaler, server RMCostServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetRMCostRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["period"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "period") + } + protoReq.Period, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "period", err) + } + val, ok = pathParams["rm_code"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "rm_code") + } + protoReq.RmCode, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "rm_code", err) + } + msg, err := server.GetRMCost(ctx, &protoReq) + return msg, metadata, err +} + +var filter_RMCostService_ListRMCosts_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_RMCostService_ListRMCosts_0(ctx context.Context, marshaler runtime.Marshaler, client RMCostServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListRMCostsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMCostService_ListRMCosts_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListRMCosts(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMCostService_ListRMCosts_0(ctx context.Context, marshaler runtime.Marshaler, server RMCostServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListRMCostsRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMCostService_ListRMCosts_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListRMCosts(ctx, &protoReq) + return msg, metadata, err +} + +var filter_RMCostService_ListRMCostHistory_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_RMCostService_ListRMCostHistory_0(ctx context.Context, marshaler runtime.Marshaler, client RMCostServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListRMCostHistoryRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMCostService_ListRMCostHistory_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListRMCostHistory(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMCostService_ListRMCostHistory_0(ctx context.Context, marshaler runtime.Marshaler, server RMCostServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListRMCostHistoryRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMCostService_ListRMCostHistory_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListRMCostHistory(ctx, &protoReq) + return msg, metadata, err +} + +func request_RMCostService_ListRMCostPeriods_0(ctx context.Context, marshaler runtime.Marshaler, client RMCostServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListRMCostPeriodsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ListRMCostPeriods(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMCostService_ListRMCostPeriods_0(ctx context.Context, marshaler runtime.Marshaler, server RMCostServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListRMCostPeriodsRequest + metadata runtime.ServerMetadata + ) + msg, err := server.ListRMCostPeriods(ctx, &protoReq) + return msg, metadata, err +} + +var filter_RMCostService_ExportRMCosts_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_RMCostService_ExportRMCosts_0(ctx context.Context, marshaler runtime.Marshaler, client RMCostServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ExportRMCostsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMCostService_ExportRMCosts_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ExportRMCosts(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMCostService_ExportRMCosts_0(ctx context.Context, marshaler runtime.Marshaler, server RMCostServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ExportRMCostsRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMCostService_ExportRMCosts_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ExportRMCosts(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterRMCostServiceHandlerServer registers the http handlers for service RMCostService to "mux". +// UnaryRPC :call RMCostServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterRMCostServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterRMCostServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server RMCostServiceServer) error { + mux.Handle(http.MethodPost, pattern_RMCostService_TriggerRMCostCalculation_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMCostService/TriggerRMCostCalculation", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs/trigger")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMCostService_TriggerRMCostCalculation_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_TriggerRMCostCalculation_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_RMCostService_CalculateRMCost_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMCostService/CalculateRMCost", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs/calculate")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMCostService_CalculateRMCost_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_CalculateRMCost_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMCostService_GetRMCost_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMCostService/GetRMCost", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs/{period}/{rm_code}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMCostService_GetRMCost_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_GetRMCost_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMCostService_ListRMCosts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMCostService/ListRMCosts", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMCostService_ListRMCosts_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_ListRMCosts_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMCostService_ListRMCostHistory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMCostService/ListRMCostHistory", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs/history")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMCostService_ListRMCostHistory_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_ListRMCostHistory_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMCostService_ListRMCostPeriods_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMCostService/ListRMCostPeriods", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs/periods")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMCostService_ListRMCostPeriods_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_ListRMCostPeriods_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMCostService_ExportRMCosts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMCostService/ExportRMCosts", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs/export")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMCostService_ExportRMCosts_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_ExportRMCosts_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterRMCostServiceHandlerFromEndpoint is same as RegisterRMCostServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterRMCostServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterRMCostServiceHandler(ctx, mux, conn) +} + +// RegisterRMCostServiceHandler registers the http handlers for service RMCostService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterRMCostServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterRMCostServiceHandlerClient(ctx, mux, NewRMCostServiceClient(conn)) +} + +// RegisterRMCostServiceHandlerClient registers the http handlers for service RMCostService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "RMCostServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "RMCostServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "RMCostServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterRMCostServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client RMCostServiceClient) error { + mux.Handle(http.MethodPost, pattern_RMCostService_TriggerRMCostCalculation_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMCostService/TriggerRMCostCalculation", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs/trigger")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMCostService_TriggerRMCostCalculation_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_TriggerRMCostCalculation_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_RMCostService_CalculateRMCost_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMCostService/CalculateRMCost", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs/calculate")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMCostService_CalculateRMCost_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_CalculateRMCost_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMCostService_GetRMCost_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMCostService/GetRMCost", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs/{period}/{rm_code}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMCostService_GetRMCost_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_GetRMCost_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMCostService_ListRMCosts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMCostService/ListRMCosts", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMCostService_ListRMCosts_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_ListRMCosts_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMCostService_ListRMCostHistory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMCostService/ListRMCostHistory", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs/history")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMCostService_ListRMCostHistory_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_ListRMCostHistory_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMCostService_ListRMCostPeriods_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMCostService/ListRMCostPeriods", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs/periods")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMCostService_ListRMCostPeriods_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_ListRMCostPeriods_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMCostService_ExportRMCosts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMCostService/ExportRMCosts", runtime.WithHTTPPathPattern("/api/v1/finance/rm-costs/export")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMCostService_ExportRMCosts_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMCostService_ExportRMCosts_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_RMCostService_TriggerRMCostCalculation_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "finance", "rm-costs", "trigger"}, "")) + pattern_RMCostService_CalculateRMCost_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "finance", "rm-costs", "calculate"}, "")) + pattern_RMCostService_GetRMCost_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 1, 0, 4, 1, 5, 5}, []string{"api", "v1", "finance", "rm-costs", "period", "rm_code"}, "")) + pattern_RMCostService_ListRMCosts_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "finance", "rm-costs"}, "")) + pattern_RMCostService_ListRMCostHistory_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "finance", "rm-costs", "history"}, "")) + pattern_RMCostService_ListRMCostPeriods_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "finance", "rm-costs", "periods"}, "")) + pattern_RMCostService_ExportRMCosts_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "finance", "rm-costs", "export"}, "")) +) + +var ( + forward_RMCostService_TriggerRMCostCalculation_0 = runtime.ForwardResponseMessage + forward_RMCostService_CalculateRMCost_0 = runtime.ForwardResponseMessage + forward_RMCostService_GetRMCost_0 = runtime.ForwardResponseMessage + forward_RMCostService_ListRMCosts_0 = runtime.ForwardResponseMessage + forward_RMCostService_ListRMCostHistory_0 = runtime.ForwardResponseMessage + forward_RMCostService_ListRMCostPeriods_0 = runtime.ForwardResponseMessage + forward_RMCostService_ExportRMCosts_0 = runtime.ForwardResponseMessage +) diff --git a/gen/finance/v1/rm_cost_grpc.pb.go b/gen/finance/v1/rm_cost_grpc.pb.go new file mode 100644 index 0000000..c8b620f --- /dev/null +++ b/gen/finance/v1/rm_cost_grpc.pb.go @@ -0,0 +1,369 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: finance/v1/rm_cost.proto + +package financev1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + RMCostService_TriggerRMCostCalculation_FullMethodName = "/finance.v1.RMCostService/TriggerRMCostCalculation" + RMCostService_CalculateRMCost_FullMethodName = "/finance.v1.RMCostService/CalculateRMCost" + RMCostService_GetRMCost_FullMethodName = "/finance.v1.RMCostService/GetRMCost" + RMCostService_ListRMCosts_FullMethodName = "/finance.v1.RMCostService/ListRMCosts" + RMCostService_ListRMCostHistory_FullMethodName = "/finance.v1.RMCostService/ListRMCostHistory" + RMCostService_ListRMCostPeriods_FullMethodName = "/finance.v1.RMCostService/ListRMCostPeriods" + RMCostService_ExportRMCosts_FullMethodName = "/finance.v1.RMCostService/ExportRMCosts" +) + +// RMCostServiceClient is the client API for RMCostService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// RMCostService exposes the landed-cost calculation pipeline: async trigger, +// synchronous admin recalc, single-row fetch, list, and history. +type RMCostServiceClient interface { + // TriggerRMCostCalculation enqueues an async recalculation job. + TriggerRMCostCalculation(ctx context.Context, in *TriggerRMCostCalculationRequest, opts ...grpc.CallOption) (*TriggerRMCostCalculationResponse, error) + // CalculateRMCost runs a recalculation synchronously (admin-only). + CalculateRMCost(ctx context.Context, in *CalculateRMCostRequest, opts ...grpc.CallOption) (*CalculateRMCostResponse, error) + // GetRMCost fetches a single cost row by (period, rm_code). + GetRMCost(ctx context.Context, in *GetRMCostRequest, opts ...grpc.CallOption) (*GetRMCostResponse, error) + // ListRMCosts lists cost rows with filter + pagination. + ListRMCosts(ctx context.Context, in *ListRMCostsRequest, opts ...grpc.CallOption) (*ListRMCostsResponse, error) + // ListRMCostHistory lists audit-history rows with filter + pagination. + ListRMCostHistory(ctx context.Context, in *ListRMCostHistoryRequest, opts ...grpc.CallOption) (*ListRMCostHistoryResponse, error) + // ListRMCostPeriods returns distinct periods from cost rows (newest first). + ListRMCostPeriods(ctx context.Context, in *ListRMCostPeriodsRequest, opts ...grpc.CallOption) (*ListRMCostPeriodsResponse, error) + // ExportRMCosts exports cost rows matching the filter to a single-sheet Excel. + ExportRMCosts(ctx context.Context, in *ExportRMCostsRequest, opts ...grpc.CallOption) (*ExportRMCostsResponse, error) +} + +type rMCostServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewRMCostServiceClient(cc grpc.ClientConnInterface) RMCostServiceClient { + return &rMCostServiceClient{cc} +} + +func (c *rMCostServiceClient) TriggerRMCostCalculation(ctx context.Context, in *TriggerRMCostCalculationRequest, opts ...grpc.CallOption) (*TriggerRMCostCalculationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TriggerRMCostCalculationResponse) + err := c.cc.Invoke(ctx, RMCostService_TriggerRMCostCalculation_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMCostServiceClient) CalculateRMCost(ctx context.Context, in *CalculateRMCostRequest, opts ...grpc.CallOption) (*CalculateRMCostResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CalculateRMCostResponse) + err := c.cc.Invoke(ctx, RMCostService_CalculateRMCost_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMCostServiceClient) GetRMCost(ctx context.Context, in *GetRMCostRequest, opts ...grpc.CallOption) (*GetRMCostResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetRMCostResponse) + err := c.cc.Invoke(ctx, RMCostService_GetRMCost_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMCostServiceClient) ListRMCosts(ctx context.Context, in *ListRMCostsRequest, opts ...grpc.CallOption) (*ListRMCostsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRMCostsResponse) + err := c.cc.Invoke(ctx, RMCostService_ListRMCosts_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMCostServiceClient) ListRMCostHistory(ctx context.Context, in *ListRMCostHistoryRequest, opts ...grpc.CallOption) (*ListRMCostHistoryResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRMCostHistoryResponse) + err := c.cc.Invoke(ctx, RMCostService_ListRMCostHistory_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMCostServiceClient) ListRMCostPeriods(ctx context.Context, in *ListRMCostPeriodsRequest, opts ...grpc.CallOption) (*ListRMCostPeriodsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRMCostPeriodsResponse) + err := c.cc.Invoke(ctx, RMCostService_ListRMCostPeriods_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMCostServiceClient) ExportRMCosts(ctx context.Context, in *ExportRMCostsRequest, opts ...grpc.CallOption) (*ExportRMCostsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExportRMCostsResponse) + err := c.cc.Invoke(ctx, RMCostService_ExportRMCosts_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// RMCostServiceServer is the server API for RMCostService service. +// All implementations must embed UnimplementedRMCostServiceServer +// for forward compatibility. +// +// RMCostService exposes the landed-cost calculation pipeline: async trigger, +// synchronous admin recalc, single-row fetch, list, and history. +type RMCostServiceServer interface { + // TriggerRMCostCalculation enqueues an async recalculation job. + TriggerRMCostCalculation(context.Context, *TriggerRMCostCalculationRequest) (*TriggerRMCostCalculationResponse, error) + // CalculateRMCost runs a recalculation synchronously (admin-only). + CalculateRMCost(context.Context, *CalculateRMCostRequest) (*CalculateRMCostResponse, error) + // GetRMCost fetches a single cost row by (period, rm_code). + GetRMCost(context.Context, *GetRMCostRequest) (*GetRMCostResponse, error) + // ListRMCosts lists cost rows with filter + pagination. + ListRMCosts(context.Context, *ListRMCostsRequest) (*ListRMCostsResponse, error) + // ListRMCostHistory lists audit-history rows with filter + pagination. + ListRMCostHistory(context.Context, *ListRMCostHistoryRequest) (*ListRMCostHistoryResponse, error) + // ListRMCostPeriods returns distinct periods from cost rows (newest first). + ListRMCostPeriods(context.Context, *ListRMCostPeriodsRequest) (*ListRMCostPeriodsResponse, error) + // ExportRMCosts exports cost rows matching the filter to a single-sheet Excel. + ExportRMCosts(context.Context, *ExportRMCostsRequest) (*ExportRMCostsResponse, error) + mustEmbedUnimplementedRMCostServiceServer() +} + +// UnimplementedRMCostServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedRMCostServiceServer struct{} + +func (UnimplementedRMCostServiceServer) TriggerRMCostCalculation(context.Context, *TriggerRMCostCalculationRequest) (*TriggerRMCostCalculationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method TriggerRMCostCalculation not implemented") +} +func (UnimplementedRMCostServiceServer) CalculateRMCost(context.Context, *CalculateRMCostRequest) (*CalculateRMCostResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CalculateRMCost not implemented") +} +func (UnimplementedRMCostServiceServer) GetRMCost(context.Context, *GetRMCostRequest) (*GetRMCostResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetRMCost not implemented") +} +func (UnimplementedRMCostServiceServer) ListRMCosts(context.Context, *ListRMCostsRequest) (*ListRMCostsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRMCosts not implemented") +} +func (UnimplementedRMCostServiceServer) ListRMCostHistory(context.Context, *ListRMCostHistoryRequest) (*ListRMCostHistoryResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRMCostHistory not implemented") +} +func (UnimplementedRMCostServiceServer) ListRMCostPeriods(context.Context, *ListRMCostPeriodsRequest) (*ListRMCostPeriodsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRMCostPeriods not implemented") +} +func (UnimplementedRMCostServiceServer) ExportRMCosts(context.Context, *ExportRMCostsRequest) (*ExportRMCostsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ExportRMCosts not implemented") +} +func (UnimplementedRMCostServiceServer) mustEmbedUnimplementedRMCostServiceServer() {} +func (UnimplementedRMCostServiceServer) testEmbeddedByValue() {} + +// UnsafeRMCostServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to RMCostServiceServer will +// result in compilation errors. +type UnsafeRMCostServiceServer interface { + mustEmbedUnimplementedRMCostServiceServer() +} + +func RegisterRMCostServiceServer(s grpc.ServiceRegistrar, srv RMCostServiceServer) { + // If the following call panics, it indicates UnimplementedRMCostServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&RMCostService_ServiceDesc, srv) +} + +func _RMCostService_TriggerRMCostCalculation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TriggerRMCostCalculationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMCostServiceServer).TriggerRMCostCalculation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMCostService_TriggerRMCostCalculation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMCostServiceServer).TriggerRMCostCalculation(ctx, req.(*TriggerRMCostCalculationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMCostService_CalculateRMCost_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CalculateRMCostRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMCostServiceServer).CalculateRMCost(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMCostService_CalculateRMCost_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMCostServiceServer).CalculateRMCost(ctx, req.(*CalculateRMCostRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMCostService_GetRMCost_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRMCostRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMCostServiceServer).GetRMCost(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMCostService_GetRMCost_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMCostServiceServer).GetRMCost(ctx, req.(*GetRMCostRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMCostService_ListRMCosts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRMCostsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMCostServiceServer).ListRMCosts(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMCostService_ListRMCosts_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMCostServiceServer).ListRMCosts(ctx, req.(*ListRMCostsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMCostService_ListRMCostHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRMCostHistoryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMCostServiceServer).ListRMCostHistory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMCostService_ListRMCostHistory_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMCostServiceServer).ListRMCostHistory(ctx, req.(*ListRMCostHistoryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMCostService_ListRMCostPeriods_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRMCostPeriodsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMCostServiceServer).ListRMCostPeriods(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMCostService_ListRMCostPeriods_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMCostServiceServer).ListRMCostPeriods(ctx, req.(*ListRMCostPeriodsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMCostService_ExportRMCosts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExportRMCostsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMCostServiceServer).ExportRMCosts(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMCostService_ExportRMCosts_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMCostServiceServer).ExportRMCosts(ctx, req.(*ExportRMCostsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// RMCostService_ServiceDesc is the grpc.ServiceDesc for RMCostService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var RMCostService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "finance.v1.RMCostService", + HandlerType: (*RMCostServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "TriggerRMCostCalculation", + Handler: _RMCostService_TriggerRMCostCalculation_Handler, + }, + { + MethodName: "CalculateRMCost", + Handler: _RMCostService_CalculateRMCost_Handler, + }, + { + MethodName: "GetRMCost", + Handler: _RMCostService_GetRMCost_Handler, + }, + { + MethodName: "ListRMCosts", + Handler: _RMCostService_ListRMCosts_Handler, + }, + { + MethodName: "ListRMCostHistory", + Handler: _RMCostService_ListRMCostHistory_Handler, + }, + { + MethodName: "ListRMCostPeriods", + Handler: _RMCostService_ListRMCostPeriods_Handler, + }, + { + MethodName: "ExportRMCosts", + Handler: _RMCostService_ExportRMCosts_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "finance/v1/rm_cost.proto", +} diff --git a/gen/finance/v1/rm_group.pb.go b/gen/finance/v1/rm_group.pb.go new file mode 100644 index 0000000..6275365 --- /dev/null +++ b/gen/finance/v1/rm_group.pb.go @@ -0,0 +1,3731 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: finance/v1/rm_group.proto + +package financev1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + v1 "github.com/mutugading/goapps-backend/gen/common/v1" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// RMGroupFlag identifies which aggregated stage rate feeds a cost purpose +// (valuation / marketing / simulation), or whether the group uses its +// configured init override value. Mirrors the `cst_rm_group_head.flag_*` +// domain constraint. +type RMGroupFlag int32 + +const ( + // Default zero value. Rejected on create/update requests via not_in: [0]. + RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED RMGroupFlag = 0 + // Use the init_val_* override on the head; skips cascade. + RMGroupFlag_RM_GROUP_FLAG_INIT RMGroupFlag = 1 + // Use the CONS stage rate. + RMGroupFlag_RM_GROUP_FLAG_CONS RMGroupFlag = 2 + // Use the STORES stage rate. + RMGroupFlag_RM_GROUP_FLAG_STORES RMGroupFlag = 3 + // Use the DEPT stage rate. + RMGroupFlag_RM_GROUP_FLAG_DEPT RMGroupFlag = 4 + // Use the first PO stage rate. + RMGroupFlag_RM_GROUP_FLAG_PO_1 RMGroupFlag = 5 + // Use the second PO stage rate. + RMGroupFlag_RM_GROUP_FLAG_PO_2 RMGroupFlag = 6 + // Use the third PO stage rate. + RMGroupFlag_RM_GROUP_FLAG_PO_3 RMGroupFlag = 7 +) + +// Enum value maps for RMGroupFlag. +var ( + RMGroupFlag_name = map[int32]string{ + 0: "RM_GROUP_FLAG_UNSPECIFIED", + 1: "RM_GROUP_FLAG_INIT", + 2: "RM_GROUP_FLAG_CONS", + 3: "RM_GROUP_FLAG_STORES", + 4: "RM_GROUP_FLAG_DEPT", + 5: "RM_GROUP_FLAG_PO_1", + 6: "RM_GROUP_FLAG_PO_2", + 7: "RM_GROUP_FLAG_PO_3", + } + RMGroupFlag_value = map[string]int32{ + "RM_GROUP_FLAG_UNSPECIFIED": 0, + "RM_GROUP_FLAG_INIT": 1, + "RM_GROUP_FLAG_CONS": 2, + "RM_GROUP_FLAG_STORES": 3, + "RM_GROUP_FLAG_DEPT": 4, + "RM_GROUP_FLAG_PO_1": 5, + "RM_GROUP_FLAG_PO_2": 6, + "RM_GROUP_FLAG_PO_3": 7, + } +) + +func (x RMGroupFlag) Enum() *RMGroupFlag { + p := new(RMGroupFlag) + *p = x + return p +} + +func (x RMGroupFlag) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RMGroupFlag) Descriptor() protoreflect.EnumDescriptor { + return file_finance_v1_rm_group_proto_enumTypes[0].Descriptor() +} + +func (RMGroupFlag) Type() protoreflect.EnumType { + return &file_finance_v1_rm_group_proto_enumTypes[0] +} + +func (x RMGroupFlag) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RMGroupFlag.Descriptor instead. +func (RMGroupFlag) EnumDescriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{0} +} + +// RemoveItemsMode controls how RemoveItems disposes of detail rows. +type RemoveItemsMode int32 + +const ( + // Default zero value. Rejected via not_in: [0]. + RemoveItemsMode_REMOVE_ITEMS_MODE_UNSPECIFIED RemoveItemsMode = 0 + // Mark the details inactive but keep the rows for audit history. + RemoveItemsMode_REMOVE_ITEMS_MODE_DEACTIVATE RemoveItemsMode = 1 + // Soft-delete the details (sets deleted_at/deleted_by). + RemoveItemsMode_REMOVE_ITEMS_MODE_SOFT_DELETE RemoveItemsMode = 2 +) + +// Enum value maps for RemoveItemsMode. +var ( + RemoveItemsMode_name = map[int32]string{ + 0: "REMOVE_ITEMS_MODE_UNSPECIFIED", + 1: "REMOVE_ITEMS_MODE_DEACTIVATE", + 2: "REMOVE_ITEMS_MODE_SOFT_DELETE", + } + RemoveItemsMode_value = map[string]int32{ + "REMOVE_ITEMS_MODE_UNSPECIFIED": 0, + "REMOVE_ITEMS_MODE_DEACTIVATE": 1, + "REMOVE_ITEMS_MODE_SOFT_DELETE": 2, + } +) + +func (x RemoveItemsMode) Enum() *RemoveItemsMode { + p := new(RemoveItemsMode) + *p = x + return p +} + +func (x RemoveItemsMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RemoveItemsMode) Descriptor() protoreflect.EnumDescriptor { + return file_finance_v1_rm_group_proto_enumTypes[1].Descriptor() +} + +func (RemoveItemsMode) Type() protoreflect.EnumType { + return &file_finance_v1_rm_group_proto_enumTypes[1] +} + +func (x RemoveItemsMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RemoveItemsMode.Descriptor instead. +func (RemoveItemsMode) EnumDescriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{1} +} + +// RMGroupHead is the aggregate root representing an RM group's cost configuration. +type RMGroupHead struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Group head UUID. + GroupHeadId string `protobuf:"bytes,1,opt,name=group_head_id,json=groupHeadId,proto3" json:"group_head_id,omitempty"` + // Unique group code (uppercase; allows spaces + hyphens; 1-30 chars). + GroupCode string `protobuf:"bytes,2,opt,name=group_code,json=groupCode,proto3" json:"group_code,omitempty"` + // Display name. + GroupName string `protobuf:"bytes,3,opt,name=group_name,json=groupName,proto3" json:"group_name,omitempty"` + // Optional description. + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + // Optional colourant tag. + Colourant string `protobuf:"bytes,5,opt,name=colourant,proto3" json:"colourant,omitempty"` + // Optional CI name tag. + CiName string `protobuf:"bytes,6,opt,name=ci_name,json=ciName,proto3" json:"ci_name,omitempty"` + // Cost percentage multiplier (raw, UI formats for display). + CostPercentage float64 `protobuf:"fixed64,7,opt,name=cost_percentage,json=costPercentage,proto3" json:"cost_percentage,omitempty"` + // Per-kg overhead added to landed cost. + CostPerKg float64 `protobuf:"fixed64,8,opt,name=cost_per_kg,json=costPerKg,proto3" json:"cost_per_kg,omitempty"` + // Stage flag used for valuation cost. + FlagValuation RMGroupFlag `protobuf:"varint,9,opt,name=flag_valuation,json=flagValuation,proto3,enum=finance.v1.RMGroupFlag" json:"flag_valuation,omitempty"` + // Stage flag used for marketing cost. + FlagMarketing RMGroupFlag `protobuf:"varint,10,opt,name=flag_marketing,json=flagMarketing,proto3,enum=finance.v1.RMGroupFlag" json:"flag_marketing,omitempty"` + // Stage flag used for simulation cost. + FlagSimulation RMGroupFlag `protobuf:"varint,11,opt,name=flag_simulation,json=flagSimulation,proto3,enum=finance.v1.RMGroupFlag" json:"flag_simulation,omitempty"` + // Init-value override for valuation (set when flag_valuation = INIT). + InitValValuation *float64 `protobuf:"fixed64,12,opt,name=init_val_valuation,json=initValValuation,proto3,oneof" json:"init_val_valuation,omitempty"` + // Init-value override for marketing (set when flag_marketing = INIT). + InitValMarketing *float64 `protobuf:"fixed64,13,opt,name=init_val_marketing,json=initValMarketing,proto3,oneof" json:"init_val_marketing,omitempty"` + // Init-value override for simulation (set when flag_simulation = INIT). + InitValSimulation *float64 `protobuf:"fixed64,14,opt,name=init_val_simulation,json=initValSimulation,proto3,oneof" json:"init_val_simulation,omitempty"` + // Whether the group is active. + IsActive bool `protobuf:"varint,15,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"` + // Audit metadata. + Audit *v1.AuditInfo `protobuf:"bytes,16,opt,name=audit,proto3" json:"audit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RMGroupHead) Reset() { + *x = RMGroupHead{} + mi := &file_finance_v1_rm_group_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RMGroupHead) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RMGroupHead) ProtoMessage() {} + +func (x *RMGroupHead) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RMGroupHead.ProtoReflect.Descriptor instead. +func (*RMGroupHead) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{0} +} + +func (x *RMGroupHead) GetGroupHeadId() string { + if x != nil { + return x.GroupHeadId + } + return "" +} + +func (x *RMGroupHead) GetGroupCode() string { + if x != nil { + return x.GroupCode + } + return "" +} + +func (x *RMGroupHead) GetGroupName() string { + if x != nil { + return x.GroupName + } + return "" +} + +func (x *RMGroupHead) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *RMGroupHead) GetColourant() string { + if x != nil { + return x.Colourant + } + return "" +} + +func (x *RMGroupHead) GetCiName() string { + if x != nil { + return x.CiName + } + return "" +} + +func (x *RMGroupHead) GetCostPercentage() float64 { + if x != nil { + return x.CostPercentage + } + return 0 +} + +func (x *RMGroupHead) GetCostPerKg() float64 { + if x != nil { + return x.CostPerKg + } + return 0 +} + +func (x *RMGroupHead) GetFlagValuation() RMGroupFlag { + if x != nil { + return x.FlagValuation + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMGroupHead) GetFlagMarketing() RMGroupFlag { + if x != nil { + return x.FlagMarketing + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMGroupHead) GetFlagSimulation() RMGroupFlag { + if x != nil { + return x.FlagSimulation + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *RMGroupHead) GetInitValValuation() float64 { + if x != nil && x.InitValValuation != nil { + return *x.InitValValuation + } + return 0 +} + +func (x *RMGroupHead) GetInitValMarketing() float64 { + if x != nil && x.InitValMarketing != nil { + return *x.InitValMarketing + } + return 0 +} + +func (x *RMGroupHead) GetInitValSimulation() float64 { + if x != nil && x.InitValSimulation != nil { + return *x.InitValSimulation + } + return 0 +} + +func (x *RMGroupHead) GetIsActive() bool { + if x != nil { + return x.IsActive + } + return false +} + +func (x *RMGroupHead) GetAudit() *v1.AuditInfo { + if x != nil { + return x.Audit + } + return nil +} + +// RMGroupDetail is one item's membership in an RM group. +type RMGroupDetail struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Detail UUID. + GroupDetailId string `protobuf:"bytes,1,opt,name=group_detail_id,json=groupDetailId,proto3" json:"group_detail_id,omitempty"` + // Owning group head UUID. + GroupHeadId string `protobuf:"bytes,2,opt,name=group_head_id,json=groupHeadId,proto3" json:"group_head_id,omitempty"` + // Item code. + ItemCode string `protobuf:"bytes,3,opt,name=item_code,json=itemCode,proto3" json:"item_code,omitempty"` + // Item name (snapshot). + ItemName string `protobuf:"bytes,4,opt,name=item_name,json=itemName,proto3" json:"item_name,omitempty"` + // Item type code. + ItemTypeCode string `protobuf:"bytes,5,opt,name=item_type_code,json=itemTypeCode,proto3" json:"item_type_code,omitempty"` + // Grade code. + GradeCode string `protobuf:"bytes,6,opt,name=grade_code,json=gradeCode,proto3" json:"grade_code,omitempty"` + // Item grade. + ItemGrade string `protobuf:"bytes,7,opt,name=item_grade,json=itemGrade,proto3" json:"item_grade,omitempty"` + // UOM code. + UomCode string `protobuf:"bytes,8,opt,name=uom_code,json=uomCode,proto3" json:"uom_code,omitempty"` + // Per-item marketing percentage (nil when unset). + MarketPercentage *float64 `protobuf:"fixed64,9,opt,name=market_percentage,json=marketPercentage,proto3,oneof" json:"market_percentage,omitempty"` + // Per-item marketing value in rupiah (nil when unset). + MarketValueRp *float64 `protobuf:"fixed64,10,opt,name=market_value_rp,json=marketValueRp,proto3,oneof" json:"market_value_rp,omitempty"` + // Display order within the group. + SortOrder int32 `protobuf:"varint,11,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + // Contributes to rate aggregation when true. + IsActive bool `protobuf:"varint,12,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"` + // Placeholder row (excluded from aggregation regardless of is_active). + IsDummy bool `protobuf:"varint,13,opt,name=is_dummy,json=isDummy,proto3" json:"is_dummy,omitempty"` + // Audit metadata. + Audit *v1.AuditInfo `protobuf:"bytes,16,opt,name=audit,proto3" json:"audit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RMGroupDetail) Reset() { + *x = RMGroupDetail{} + mi := &file_finance_v1_rm_group_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RMGroupDetail) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RMGroupDetail) ProtoMessage() {} + +func (x *RMGroupDetail) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RMGroupDetail.ProtoReflect.Descriptor instead. +func (*RMGroupDetail) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{1} +} + +func (x *RMGroupDetail) GetGroupDetailId() string { + if x != nil { + return x.GroupDetailId + } + return "" +} + +func (x *RMGroupDetail) GetGroupHeadId() string { + if x != nil { + return x.GroupHeadId + } + return "" +} + +func (x *RMGroupDetail) GetItemCode() string { + if x != nil { + return x.ItemCode + } + return "" +} + +func (x *RMGroupDetail) GetItemName() string { + if x != nil { + return x.ItemName + } + return "" +} + +func (x *RMGroupDetail) GetItemTypeCode() string { + if x != nil { + return x.ItemTypeCode + } + return "" +} + +func (x *RMGroupDetail) GetGradeCode() string { + if x != nil { + return x.GradeCode + } + return "" +} + +func (x *RMGroupDetail) GetItemGrade() string { + if x != nil { + return x.ItemGrade + } + return "" +} + +func (x *RMGroupDetail) GetUomCode() string { + if x != nil { + return x.UomCode + } + return "" +} + +func (x *RMGroupDetail) GetMarketPercentage() float64 { + if x != nil && x.MarketPercentage != nil { + return *x.MarketPercentage + } + return 0 +} + +func (x *RMGroupDetail) GetMarketValueRp() float64 { + if x != nil && x.MarketValueRp != nil { + return *x.MarketValueRp + } + return 0 +} + +func (x *RMGroupDetail) GetSortOrder() int32 { + if x != nil { + return x.SortOrder + } + return 0 +} + +func (x *RMGroupDetail) GetIsActive() bool { + if x != nil { + return x.IsActive + } + return false +} + +func (x *RMGroupDetail) GetIsDummy() bool { + if x != nil { + return x.IsDummy + } + return false +} + +func (x *RMGroupDetail) GetAudit() *v1.AuditInfo { + if x != nil { + return x.Audit + } + return nil +} + +// RMGroupHeadWithDetails bundles a head and its details for the Get response. +type RMGroupHeadWithDetails struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Head data. + Head *RMGroupHead `protobuf:"bytes,1,opt,name=head,proto3" json:"head,omitempty"` + // Details owned by the head. + Details []*RMGroupDetail `protobuf:"bytes,2,rep,name=details,proto3" json:"details,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RMGroupHeadWithDetails) Reset() { + *x = RMGroupHeadWithDetails{} + mi := &file_finance_v1_rm_group_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RMGroupHeadWithDetails) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RMGroupHeadWithDetails) ProtoMessage() {} + +func (x *RMGroupHeadWithDetails) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RMGroupHeadWithDetails.ProtoReflect.Descriptor instead. +func (*RMGroupHeadWithDetails) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{2} +} + +func (x *RMGroupHeadWithDetails) GetHead() *RMGroupHead { + if x != nil { + return x.Head + } + return nil +} + +func (x *RMGroupHeadWithDetails) GetDetails() []*RMGroupDetail { + if x != nil { + return x.Details + } + return nil +} + +// UngroupedItem is a raw material present in the Oracle sync feed that has no +// active RM group assignment. +type UngroupedItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Period (YYYYMM). + Period string `protobuf:"bytes,1,opt,name=period,proto3" json:"period,omitempty"` + // Item code. + ItemCode string `protobuf:"bytes,2,opt,name=item_code,json=itemCode,proto3" json:"item_code,omitempty"` + // Item name. + ItemName string `protobuf:"bytes,3,opt,name=item_name,json=itemName,proto3" json:"item_name,omitempty"` + // Item type code. + ItemTypeCode string `protobuf:"bytes,4,opt,name=item_type_code,json=itemTypeCode,proto3" json:"item_type_code,omitempty"` + // Grade code. + GradeCode string `protobuf:"bytes,5,opt,name=grade_code,json=gradeCode,proto3" json:"grade_code,omitempty"` + // Item grade. + ItemGrade string `protobuf:"bytes,6,opt,name=item_grade,json=itemGrade,proto3" json:"item_grade,omitempty"` + // UOM code. + UomCode string `protobuf:"bytes,7,opt,name=uom_code,json=uomCode,proto3" json:"uom_code,omitempty"` + // CONS stage value. + ConsVal float64 `protobuf:"fixed64,8,opt,name=cons_val,json=consVal,proto3" json:"cons_val,omitempty"` + // STORES stage value. + StoresVal float64 `protobuf:"fixed64,9,opt,name=stores_val,json=storesVal,proto3" json:"stores_val,omitempty"` + // CONS stage quantity. + ConsQty float64 `protobuf:"fixed64,10,opt,name=cons_qty,json=consQty,proto3" json:"cons_qty,omitempty"` + // CONS stage rate. + ConsRate float64 `protobuf:"fixed64,11,opt,name=cons_rate,json=consRate,proto3" json:"cons_rate,omitempty"` + // STORES stage quantity. + StoresQty float64 `protobuf:"fixed64,12,opt,name=stores_qty,json=storesQty,proto3" json:"stores_qty,omitempty"` + // STORES stage rate. + StoresRate float64 `protobuf:"fixed64,13,opt,name=stores_rate,json=storesRate,proto3" json:"stores_rate,omitempty"` + // DEPT stage quantity. + DeptQty float64 `protobuf:"fixed64,14,opt,name=dept_qty,json=deptQty,proto3" json:"dept_qty,omitempty"` + // DEPT stage value. + DeptVal float64 `protobuf:"fixed64,15,opt,name=dept_val,json=deptVal,proto3" json:"dept_val,omitempty"` + // DEPT stage rate. + DeptRate float64 `protobuf:"fixed64,16,opt,name=dept_rate,json=deptRate,proto3" json:"dept_rate,omitempty"` + // PO_1 stage quantity. + LastPoQty1 float64 `protobuf:"fixed64,17,opt,name=last_po_qty1,json=lastPoQty1,proto3" json:"last_po_qty1,omitempty"` + // PO_1 stage value. + LastPoVal1 float64 `protobuf:"fixed64,18,opt,name=last_po_val1,json=lastPoVal1,proto3" json:"last_po_val1,omitempty"` + // PO_1 stage rate. + LastPoRate1 float64 `protobuf:"fixed64,19,opt,name=last_po_rate1,json=lastPoRate1,proto3" json:"last_po_rate1,omitempty"` + // PO_2 stage quantity. + LastPoQty2 float64 `protobuf:"fixed64,20,opt,name=last_po_qty2,json=lastPoQty2,proto3" json:"last_po_qty2,omitempty"` + // PO_2 stage value. + LastPoVal2 float64 `protobuf:"fixed64,21,opt,name=last_po_val2,json=lastPoVal2,proto3" json:"last_po_val2,omitempty"` + // PO_2 stage rate. + LastPoRate2 float64 `protobuf:"fixed64,22,opt,name=last_po_rate2,json=lastPoRate2,proto3" json:"last_po_rate2,omitempty"` + // PO_3 stage quantity. + LastPoQty3 float64 `protobuf:"fixed64,23,opt,name=last_po_qty3,json=lastPoQty3,proto3" json:"last_po_qty3,omitempty"` + // PO_3 stage value. + LastPoVal3 float64 `protobuf:"fixed64,24,opt,name=last_po_val3,json=lastPoVal3,proto3" json:"last_po_val3,omitempty"` + // PO_3 stage rate. + LastPoRate3 float64 `protobuf:"fixed64,25,opt,name=last_po_rate3,json=lastPoRate3,proto3" json:"last_po_rate3,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UngroupedItem) Reset() { + *x = UngroupedItem{} + mi := &file_finance_v1_rm_group_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UngroupedItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UngroupedItem) ProtoMessage() {} + +func (x *UngroupedItem) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UngroupedItem.ProtoReflect.Descriptor instead. +func (*UngroupedItem) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{3} +} + +func (x *UngroupedItem) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *UngroupedItem) GetItemCode() string { + if x != nil { + return x.ItemCode + } + return "" +} + +func (x *UngroupedItem) GetItemName() string { + if x != nil { + return x.ItemName + } + return "" +} + +func (x *UngroupedItem) GetItemTypeCode() string { + if x != nil { + return x.ItemTypeCode + } + return "" +} + +func (x *UngroupedItem) GetGradeCode() string { + if x != nil { + return x.GradeCode + } + return "" +} + +func (x *UngroupedItem) GetItemGrade() string { + if x != nil { + return x.ItemGrade + } + return "" +} + +func (x *UngroupedItem) GetUomCode() string { + if x != nil { + return x.UomCode + } + return "" +} + +func (x *UngroupedItem) GetConsVal() float64 { + if x != nil { + return x.ConsVal + } + return 0 +} + +func (x *UngroupedItem) GetStoresVal() float64 { + if x != nil { + return x.StoresVal + } + return 0 +} + +func (x *UngroupedItem) GetConsQty() float64 { + if x != nil { + return x.ConsQty + } + return 0 +} + +func (x *UngroupedItem) GetConsRate() float64 { + if x != nil { + return x.ConsRate + } + return 0 +} + +func (x *UngroupedItem) GetStoresQty() float64 { + if x != nil { + return x.StoresQty + } + return 0 +} + +func (x *UngroupedItem) GetStoresRate() float64 { + if x != nil { + return x.StoresRate + } + return 0 +} + +func (x *UngroupedItem) GetDeptQty() float64 { + if x != nil { + return x.DeptQty + } + return 0 +} + +func (x *UngroupedItem) GetDeptVal() float64 { + if x != nil { + return x.DeptVal + } + return 0 +} + +func (x *UngroupedItem) GetDeptRate() float64 { + if x != nil { + return x.DeptRate + } + return 0 +} + +func (x *UngroupedItem) GetLastPoQty1() float64 { + if x != nil { + return x.LastPoQty1 + } + return 0 +} + +func (x *UngroupedItem) GetLastPoVal1() float64 { + if x != nil { + return x.LastPoVal1 + } + return 0 +} + +func (x *UngroupedItem) GetLastPoRate1() float64 { + if x != nil { + return x.LastPoRate1 + } + return 0 +} + +func (x *UngroupedItem) GetLastPoQty2() float64 { + if x != nil { + return x.LastPoQty2 + } + return 0 +} + +func (x *UngroupedItem) GetLastPoVal2() float64 { + if x != nil { + return x.LastPoVal2 + } + return 0 +} + +func (x *UngroupedItem) GetLastPoRate2() float64 { + if x != nil { + return x.LastPoRate2 + } + return 0 +} + +func (x *UngroupedItem) GetLastPoQty3() float64 { + if x != nil { + return x.LastPoQty3 + } + return 0 +} + +func (x *UngroupedItem) GetLastPoVal3() float64 { + if x != nil { + return x.LastPoVal3 + } + return 0 +} + +func (x *UngroupedItem) GetLastPoRate3() float64 { + if x != nil { + return x.LastPoRate3 + } + return 0 +} + +// SkippedItem captures items rejected by AddItems because they already belong +// to another active group. +type SkippedItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Item code that was skipped. + ItemCode string `protobuf:"bytes,1,opt,name=item_code,json=itemCode,proto3" json:"item_code,omitempty"` + // Owning group head UUID. + OwningGroupHeadId string `protobuf:"bytes,2,opt,name=owning_group_head_id,json=owningGroupHeadId,proto3" json:"owning_group_head_id,omitempty"` + // Owning detail UUID. + OwningGroupDetailId string `protobuf:"bytes,3,opt,name=owning_group_detail_id,json=owningGroupDetailId,proto3" json:"owning_group_detail_id,omitempty"` + // Owning group code (for UI display). + OwningGroupCode string `protobuf:"bytes,4,opt,name=owning_group_code,json=owningGroupCode,proto3" json:"owning_group_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SkippedItem) Reset() { + *x = SkippedItem{} + mi := &file_finance_v1_rm_group_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SkippedItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SkippedItem) ProtoMessage() {} + +func (x *SkippedItem) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SkippedItem.ProtoReflect.Descriptor instead. +func (*SkippedItem) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{4} +} + +func (x *SkippedItem) GetItemCode() string { + if x != nil { + return x.ItemCode + } + return "" +} + +func (x *SkippedItem) GetOwningGroupHeadId() string { + if x != nil { + return x.OwningGroupHeadId + } + return "" +} + +func (x *SkippedItem) GetOwningGroupDetailId() string { + if x != nil { + return x.OwningGroupDetailId + } + return "" +} + +func (x *SkippedItem) GetOwningGroupCode() string { + if x != nil { + return x.OwningGroupCode + } + return "" +} + +// Create a new RM group head. +type CreateRMGroupRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Group code (uppercase; spaces + hyphens allowed; 1-30 chars). + GroupCode string `protobuf:"bytes,1,opt,name=group_code,json=groupCode,proto3" json:"group_code,omitempty"` + // Display name (1-200 chars). + GroupName string `protobuf:"bytes,2,opt,name=group_name,json=groupName,proto3" json:"group_name,omitempty"` + // Optional description (max 1000 chars). + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // Optional colourant tag. + Colourant string `protobuf:"bytes,4,opt,name=colourant,proto3" json:"colourant,omitempty"` + // Optional CI name tag. + CiName string `protobuf:"bytes,5,opt,name=ci_name,json=ciName,proto3" json:"ci_name,omitempty"` + // Cost percentage multiplier (>= 0). + CostPercentage float64 `protobuf:"fixed64,6,opt,name=cost_percentage,json=costPercentage,proto3" json:"cost_percentage,omitempty"` + // Per-kg overhead (>= 0). + CostPerKg float64 `protobuf:"fixed64,7,opt,name=cost_per_kg,json=costPerKg,proto3" json:"cost_per_kg,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateRMGroupRequest) Reset() { + *x = CreateRMGroupRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateRMGroupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRMGroupRequest) ProtoMessage() {} + +func (x *CreateRMGroupRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRMGroupRequest.ProtoReflect.Descriptor instead. +func (*CreateRMGroupRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateRMGroupRequest) GetGroupCode() string { + if x != nil { + return x.GroupCode + } + return "" +} + +func (x *CreateRMGroupRequest) GetGroupName() string { + if x != nil { + return x.GroupName + } + return "" +} + +func (x *CreateRMGroupRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *CreateRMGroupRequest) GetColourant() string { + if x != nil { + return x.Colourant + } + return "" +} + +func (x *CreateRMGroupRequest) GetCiName() string { + if x != nil { + return x.CiName + } + return "" +} + +func (x *CreateRMGroupRequest) GetCostPercentage() float64 { + if x != nil { + return x.CostPercentage + } + return 0 +} + +func (x *CreateRMGroupRequest) GetCostPerKg() float64 { + if x != nil { + return x.CostPerKg + } + return 0 +} + +// Create response. +type CreateRMGroupResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Created head. + Data *RMGroupHead `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateRMGroupResponse) Reset() { + *x = CreateRMGroupResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateRMGroupResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRMGroupResponse) ProtoMessage() {} + +func (x *CreateRMGroupResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRMGroupResponse.ProtoReflect.Descriptor instead. +func (*CreateRMGroupResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{6} +} + +func (x *CreateRMGroupResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *CreateRMGroupResponse) GetData() *RMGroupHead { + if x != nil { + return x.Data + } + return nil +} + +// Get an RM group head + its details. +type GetRMGroupRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Head UUID. + GroupHeadId string `protobuf:"bytes,1,opt,name=group_head_id,json=groupHeadId,proto3" json:"group_head_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRMGroupRequest) Reset() { + *x = GetRMGroupRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRMGroupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRMGroupRequest) ProtoMessage() {} + +func (x *GetRMGroupRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRMGroupRequest.ProtoReflect.Descriptor instead. +func (*GetRMGroupRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{7} +} + +func (x *GetRMGroupRequest) GetGroupHeadId() string { + if x != nil { + return x.GroupHeadId + } + return "" +} + +// Get response. +type GetRMGroupResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Head + details. + Data *RMGroupHeadWithDetails `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRMGroupResponse) Reset() { + *x = GetRMGroupResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRMGroupResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRMGroupResponse) ProtoMessage() {} + +func (x *GetRMGroupResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRMGroupResponse.ProtoReflect.Descriptor instead. +func (*GetRMGroupResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{8} +} + +func (x *GetRMGroupResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *GetRMGroupResponse) GetData() *RMGroupHeadWithDetails { + if x != nil { + return x.Data + } + return nil +} + +// Partial update of an RM group head. Absent fields leave state unchanged. +// Clear_* booleans explicitly set nullable fields to NULL. +type UpdateRMGroupRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Head UUID to update. + GroupHeadId string `protobuf:"bytes,1,opt,name=group_head_id,json=groupHeadId,proto3" json:"group_head_id,omitempty"` + // New display name. + GroupName *string `protobuf:"bytes,2,opt,name=group_name,json=groupName,proto3,oneof" json:"group_name,omitempty"` + // New description. + Description *string `protobuf:"bytes,3,opt,name=description,proto3,oneof" json:"description,omitempty"` + // New colourant tag. + Colourant *string `protobuf:"bytes,4,opt,name=colourant,proto3,oneof" json:"colourant,omitempty"` + // New CI name tag. + CiName *string `protobuf:"bytes,5,opt,name=ci_name,json=ciName,proto3,oneof" json:"ci_name,omitempty"` + // New cost percentage (>= 0). + CostPercentage *float64 `protobuf:"fixed64,6,opt,name=cost_percentage,json=costPercentage,proto3,oneof" json:"cost_percentage,omitempty"` + // New per-kg overhead (>= 0). + CostPerKg *float64 `protobuf:"fixed64,7,opt,name=cost_per_kg,json=costPerKg,proto3,oneof" json:"cost_per_kg,omitempty"` + // New valuation flag. + FlagValuation *RMGroupFlag `protobuf:"varint,8,opt,name=flag_valuation,json=flagValuation,proto3,enum=finance.v1.RMGroupFlag,oneof" json:"flag_valuation,omitempty"` + // New marketing flag. + FlagMarketing *RMGroupFlag `protobuf:"varint,9,opt,name=flag_marketing,json=flagMarketing,proto3,enum=finance.v1.RMGroupFlag,oneof" json:"flag_marketing,omitempty"` + // New simulation flag. + FlagSimulation *RMGroupFlag `protobuf:"varint,10,opt,name=flag_simulation,json=flagSimulation,proto3,enum=finance.v1.RMGroupFlag,oneof" json:"flag_simulation,omitempty"` + // New init-val override for valuation (>= 0). + InitValValuation *float64 `protobuf:"fixed64,11,opt,name=init_val_valuation,json=initValValuation,proto3,oneof" json:"init_val_valuation,omitempty"` + // New init-val override for marketing (>= 0). + InitValMarketing *float64 `protobuf:"fixed64,12,opt,name=init_val_marketing,json=initValMarketing,proto3,oneof" json:"init_val_marketing,omitempty"` + // New init-val override for simulation (>= 0). + InitValSimulation *float64 `protobuf:"fixed64,13,opt,name=init_val_simulation,json=initValSimulation,proto3,oneof" json:"init_val_simulation,omitempty"` + // New active status. + IsActive *bool `protobuf:"varint,14,opt,name=is_active,json=isActive,proto3,oneof" json:"is_active,omitempty"` + // When true, force init_val_valuation to NULL (overrides init_val_valuation field). + ClearInitValValuation bool `protobuf:"varint,15,opt,name=clear_init_val_valuation,json=clearInitValValuation,proto3" json:"clear_init_val_valuation,omitempty"` + // When true, force init_val_marketing to NULL. + ClearInitValMarketing bool `protobuf:"varint,16,opt,name=clear_init_val_marketing,json=clearInitValMarketing,proto3" json:"clear_init_val_marketing,omitempty"` + // When true, force init_val_simulation to NULL. + ClearInitValSimulation bool `protobuf:"varint,17,opt,name=clear_init_val_simulation,json=clearInitValSimulation,proto3" json:"clear_init_val_simulation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateRMGroupRequest) Reset() { + *x = UpdateRMGroupRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateRMGroupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateRMGroupRequest) ProtoMessage() {} + +func (x *UpdateRMGroupRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateRMGroupRequest.ProtoReflect.Descriptor instead. +func (*UpdateRMGroupRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{9} +} + +func (x *UpdateRMGroupRequest) GetGroupHeadId() string { + if x != nil { + return x.GroupHeadId + } + return "" +} + +func (x *UpdateRMGroupRequest) GetGroupName() string { + if x != nil && x.GroupName != nil { + return *x.GroupName + } + return "" +} + +func (x *UpdateRMGroupRequest) GetDescription() string { + if x != nil && x.Description != nil { + return *x.Description + } + return "" +} + +func (x *UpdateRMGroupRequest) GetColourant() string { + if x != nil && x.Colourant != nil { + return *x.Colourant + } + return "" +} + +func (x *UpdateRMGroupRequest) GetCiName() string { + if x != nil && x.CiName != nil { + return *x.CiName + } + return "" +} + +func (x *UpdateRMGroupRequest) GetCostPercentage() float64 { + if x != nil && x.CostPercentage != nil { + return *x.CostPercentage + } + return 0 +} + +func (x *UpdateRMGroupRequest) GetCostPerKg() float64 { + if x != nil && x.CostPerKg != nil { + return *x.CostPerKg + } + return 0 +} + +func (x *UpdateRMGroupRequest) GetFlagValuation() RMGroupFlag { + if x != nil && x.FlagValuation != nil { + return *x.FlagValuation + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *UpdateRMGroupRequest) GetFlagMarketing() RMGroupFlag { + if x != nil && x.FlagMarketing != nil { + return *x.FlagMarketing + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *UpdateRMGroupRequest) GetFlagSimulation() RMGroupFlag { + if x != nil && x.FlagSimulation != nil { + return *x.FlagSimulation + } + return RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED +} + +func (x *UpdateRMGroupRequest) GetInitValValuation() float64 { + if x != nil && x.InitValValuation != nil { + return *x.InitValValuation + } + return 0 +} + +func (x *UpdateRMGroupRequest) GetInitValMarketing() float64 { + if x != nil && x.InitValMarketing != nil { + return *x.InitValMarketing + } + return 0 +} + +func (x *UpdateRMGroupRequest) GetInitValSimulation() float64 { + if x != nil && x.InitValSimulation != nil { + return *x.InitValSimulation + } + return 0 +} + +func (x *UpdateRMGroupRequest) GetIsActive() bool { + if x != nil && x.IsActive != nil { + return *x.IsActive + } + return false +} + +func (x *UpdateRMGroupRequest) GetClearInitValValuation() bool { + if x != nil { + return x.ClearInitValValuation + } + return false +} + +func (x *UpdateRMGroupRequest) GetClearInitValMarketing() bool { + if x != nil { + return x.ClearInitValMarketing + } + return false +} + +func (x *UpdateRMGroupRequest) GetClearInitValSimulation() bool { + if x != nil { + return x.ClearInitValSimulation + } + return false +} + +// Update response. +type UpdateRMGroupResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Updated head. + Data *RMGroupHead `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateRMGroupResponse) Reset() { + *x = UpdateRMGroupResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateRMGroupResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateRMGroupResponse) ProtoMessage() {} + +func (x *UpdateRMGroupResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateRMGroupResponse.ProtoReflect.Descriptor instead. +func (*UpdateRMGroupResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{10} +} + +func (x *UpdateRMGroupResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *UpdateRMGroupResponse) GetData() *RMGroupHead { + if x != nil { + return x.Data + } + return nil +} + +// Soft-delete an RM group head and cascade to its details. +type DeleteRMGroupRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Head UUID. + GroupHeadId string `protobuf:"bytes,1,opt,name=group_head_id,json=groupHeadId,proto3" json:"group_head_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRMGroupRequest) Reset() { + *x = DeleteRMGroupRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRMGroupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRMGroupRequest) ProtoMessage() {} + +func (x *DeleteRMGroupRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRMGroupRequest.ProtoReflect.Descriptor instead. +func (*DeleteRMGroupRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{11} +} + +func (x *DeleteRMGroupRequest) GetGroupHeadId() string { + if x != nil { + return x.GroupHeadId + } + return "" +} + +// Delete response. +type DeleteRMGroupResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRMGroupResponse) Reset() { + *x = DeleteRMGroupResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRMGroupResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRMGroupResponse) ProtoMessage() {} + +func (x *DeleteRMGroupResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRMGroupResponse.ProtoReflect.Descriptor instead. +func (*DeleteRMGroupResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{12} +} + +func (x *DeleteRMGroupResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +// List RM group heads with search + filter + pagination. +type ListRMGroupsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Page number (1-indexed). + Page int32 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` + // Page size (1-100). + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Free-text search on code, name, description, colourant, ci_name. + Search string `protobuf:"bytes,3,opt,name=search,proto3" json:"search,omitempty"` + // Filter by active status. + ActiveFilter ActiveFilter `protobuf:"varint,4,opt,name=active_filter,json=activeFilter,proto3,enum=finance.v1.ActiveFilter" json:"active_filter,omitempty"` + // Sort field. + SortBy string `protobuf:"bytes,5,opt,name=sort_by,json=sortBy,proto3" json:"sort_by,omitempty"` + // Sort order. + SortOrder string `protobuf:"bytes,6,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRMGroupsRequest) Reset() { + *x = ListRMGroupsRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRMGroupsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRMGroupsRequest) ProtoMessage() {} + +func (x *ListRMGroupsRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRMGroupsRequest.ProtoReflect.Descriptor instead. +func (*ListRMGroupsRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{13} +} + +func (x *ListRMGroupsRequest) GetPage() int32 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ListRMGroupsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListRMGroupsRequest) GetSearch() string { + if x != nil { + return x.Search + } + return "" +} + +func (x *ListRMGroupsRequest) GetActiveFilter() ActiveFilter { + if x != nil { + return x.ActiveFilter + } + return ActiveFilter_ACTIVE_FILTER_UNSPECIFIED +} + +func (x *ListRMGroupsRequest) GetSortBy() string { + if x != nil { + return x.SortBy + } + return "" +} + +func (x *ListRMGroupsRequest) GetSortOrder() string { + if x != nil { + return x.SortOrder + } + return "" +} + +// List response. +type ListRMGroupsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Group heads. + Data []*RMGroupHead `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty"` + // Pagination metadata. + Pagination *v1.PaginationResponse `protobuf:"bytes,3,opt,name=pagination,proto3" json:"pagination,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRMGroupsResponse) Reset() { + *x = ListRMGroupsResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRMGroupsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRMGroupsResponse) ProtoMessage() {} + +func (x *ListRMGroupsResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRMGroupsResponse.ProtoReflect.Descriptor instead. +func (*ListRMGroupsResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{14} +} + +func (x *ListRMGroupsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ListRMGroupsResponse) GetData() []*RMGroupHead { + if x != nil { + return x.Data + } + return nil +} + +func (x *ListRMGroupsResponse) GetPagination() *v1.PaginationResponse { + if x != nil { + return x.Pagination + } + return nil +} + +// AddItemSelection identifies one RM variant to assign to a group. The +// (item_code, grade_code) pair is the natural key: an item_code with +// multiple grade_code variants in the sync feed represents distinct +// rows that can be grouped independently. +type AddItemSelection struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Item code (required). + ItemCode string `protobuf:"bytes,1,opt,name=item_code,json=itemCode,proto3" json:"item_code,omitempty"` + // Grade code (optional). Empty matches the variant with NULL / empty + // grade_code in the sync feed. Use "" when the row has a single variant. + GradeCode string `protobuf:"bytes,2,opt,name=grade_code,json=gradeCode,proto3" json:"grade_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddItemSelection) Reset() { + *x = AddItemSelection{} + mi := &file_finance_v1_rm_group_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddItemSelection) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddItemSelection) ProtoMessage() {} + +func (x *AddItemSelection) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddItemSelection.ProtoReflect.Descriptor instead. +func (*AddItemSelection) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{15} +} + +func (x *AddItemSelection) GetItemCode() string { + if x != nil { + return x.ItemCode + } + return "" +} + +func (x *AddItemSelection) GetGradeCode() string { + if x != nil { + return x.GradeCode + } + return "" +} + +// Add one or more items to a group. Items already in another active group are +// returned in `skipped` rather than erroring the whole batch. +// +// Prefer `selections` — it carries grade_code which is required to correctly +// disambiguate multi-variant items. `item_codes` is retained for backwards +// compatibility; when both are supplied, `selections` wins. +type AddItemsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Target group head UUID. + GroupHeadId string `protobuf:"bytes,1,opt,name=group_head_id,json=groupHeadId,proto3" json:"group_head_id,omitempty"` + // Item codes to add (legacy: grade_code is resolved server-side). + ItemCodes []string `protobuf:"bytes,2,rep,name=item_codes,json=itemCodes,proto3" json:"item_codes,omitempty"` + // Structured selections carrying grade_code. Preferred over item_codes. + Selections []*AddItemSelection `protobuf:"bytes,3,rep,name=selections,proto3" json:"selections,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddItemsRequest) Reset() { + *x = AddItemsRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddItemsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddItemsRequest) ProtoMessage() {} + +func (x *AddItemsRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddItemsRequest.ProtoReflect.Descriptor instead. +func (*AddItemsRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{16} +} + +func (x *AddItemsRequest) GetGroupHeadId() string { + if x != nil { + return x.GroupHeadId + } + return "" +} + +func (x *AddItemsRequest) GetItemCodes() []string { + if x != nil { + return x.ItemCodes + } + return nil +} + +func (x *AddItemsRequest) GetSelections() []*AddItemSelection { + if x != nil { + return x.Selections + } + return nil +} + +// Add items response. +type AddItemsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Details created by this call. + Added []*RMGroupDetail `protobuf:"bytes,2,rep,name=added,proto3" json:"added,omitempty"` + // Items that were skipped because they already belong to another active group. + Skipped []*SkippedItem `protobuf:"bytes,3,rep,name=skipped,proto3" json:"skipped,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddItemsResponse) Reset() { + *x = AddItemsResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddItemsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddItemsResponse) ProtoMessage() {} + +func (x *AddItemsResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddItemsResponse.ProtoReflect.Descriptor instead. +func (*AddItemsResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{17} +} + +func (x *AddItemsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *AddItemsResponse) GetAdded() []*RMGroupDetail { + if x != nil { + return x.Added + } + return nil +} + +func (x *AddItemsResponse) GetSkipped() []*SkippedItem { + if x != nil { + return x.Skipped + } + return nil +} + +// Remove items from a group by detail IDs. The mode controls whether the rows +// are deactivated (preserved for history) or soft-deleted. +type RemoveItemsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Owning group head UUID (validated against each detail). + GroupHeadId string `protobuf:"bytes,1,opt,name=group_head_id,json=groupHeadId,proto3" json:"group_head_id,omitempty"` + // Detail UUIDs to remove. + GroupDetailIds []string `protobuf:"bytes,2,rep,name=group_detail_ids,json=groupDetailIds,proto3" json:"group_detail_ids,omitempty"` + // Disposition mode. + Mode RemoveItemsMode `protobuf:"varint,3,opt,name=mode,proto3,enum=finance.v1.RemoveItemsMode" json:"mode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveItemsRequest) Reset() { + *x = RemoveItemsRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveItemsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveItemsRequest) ProtoMessage() {} + +func (x *RemoveItemsRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveItemsRequest.ProtoReflect.Descriptor instead. +func (*RemoveItemsRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{18} +} + +func (x *RemoveItemsRequest) GetGroupHeadId() string { + if x != nil { + return x.GroupHeadId + } + return "" +} + +func (x *RemoveItemsRequest) GetGroupDetailIds() []string { + if x != nil { + return x.GroupDetailIds + } + return nil +} + +func (x *RemoveItemsRequest) GetMode() RemoveItemsMode { + if x != nil { + return x.Mode + } + return RemoveItemsMode_REMOVE_ITEMS_MODE_UNSPECIFIED +} + +// Remove items response. +type RemoveItemsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Number of details affected. + RemovedCount int32 `protobuf:"varint,2,opt,name=removed_count,json=removedCount,proto3" json:"removed_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveItemsResponse) Reset() { + *x = RemoveItemsResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveItemsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveItemsResponse) ProtoMessage() {} + +func (x *RemoveItemsResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveItemsResponse.ProtoReflect.Descriptor instead. +func (*RemoveItemsResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{19} +} + +func (x *RemoveItemsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *RemoveItemsResponse) GetRemovedCount() int32 { + if x != nil { + return x.RemovedCount + } + return 0 +} + +// List items from the Oracle sync feed that have no active RM group assignment. +type ListUngroupedItemsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Page number (1-indexed). + Page int32 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` + // Page size (1-100). + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Period filter (YYYYMM). Empty = all periods. + Period string `protobuf:"bytes,3,opt,name=period,proto3" json:"period,omitempty"` + // Free-text search on item_code, item_name, item_type_code, grade_code. + Search string `protobuf:"bytes,4,opt,name=search,proto3" json:"search,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUngroupedItemsRequest) Reset() { + *x = ListUngroupedItemsRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUngroupedItemsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUngroupedItemsRequest) ProtoMessage() {} + +func (x *ListUngroupedItemsRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUngroupedItemsRequest.ProtoReflect.Descriptor instead. +func (*ListUngroupedItemsRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{20} +} + +func (x *ListUngroupedItemsRequest) GetPage() int32 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ListUngroupedItemsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListUngroupedItemsRequest) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *ListUngroupedItemsRequest) GetSearch() string { + if x != nil { + return x.Search + } + return "" +} + +// ImportGroupItemsRequest bulk-adds items to a specific existing group. +type ImportGroupItemsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Target group head ID (UUID). + GroupHeadId string `protobuf:"bytes,1,opt,name=group_head_id,json=groupHeadId,proto3" json:"group_head_id,omitempty"` + // Excel file content (.xlsx / .xls). Expected "Items" sheet with columns + // item_code (required), grade_code (optional), sort_order (optional). + FileContent []byte `protobuf:"bytes,2,opt,name=file_content,json=fileContent,proto3" json:"file_content,omitempty"` + // Original filename (format detected by extension). + FileName string `protobuf:"bytes,3,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImportGroupItemsRequest) Reset() { + *x = ImportGroupItemsRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImportGroupItemsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImportGroupItemsRequest) ProtoMessage() {} + +func (x *ImportGroupItemsRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImportGroupItemsRequest.ProtoReflect.Descriptor instead. +func (*ImportGroupItemsRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{21} +} + +func (x *ImportGroupItemsRequest) GetGroupHeadId() string { + if x != nil { + return x.GroupHeadId + } + return "" +} + +func (x *ImportGroupItemsRequest) GetFileContent() []byte { + if x != nil { + return x.FileContent + } + return nil +} + +func (x *ImportGroupItemsRequest) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +// ImportGroupItemsResponse summarizes the outcome. +type ImportGroupItemsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Number of items successfully added. + ItemsAdded int32 `protobuf:"varint,2,opt,name=items_added,json=itemsAdded,proto3" json:"items_added,omitempty"` + // Number of items skipped (already in this or another group, or invalid). + ItemsSkipped int32 `protobuf:"varint,3,opt,name=items_skipped,json=itemsSkipped,proto3" json:"items_skipped,omitempty"` + // Number of rows that failed to parse at the Excel layer. + FailedCount int32 `protobuf:"varint,4,opt,name=failed_count,json=failedCount,proto3" json:"failed_count,omitempty"` + // Parse-level errors (bad item_code format, etc). + Errors []*ImportError `protobuf:"bytes,5,rep,name=errors,proto3" json:"errors,omitempty"` + // Skip details propagated from AddItems (ownership collisions, etc). + Skipped []*SkippedItem `protobuf:"bytes,6,rep,name=skipped,proto3" json:"skipped,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImportGroupItemsResponse) Reset() { + *x = ImportGroupItemsResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImportGroupItemsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImportGroupItemsResponse) ProtoMessage() {} + +func (x *ImportGroupItemsResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImportGroupItemsResponse.ProtoReflect.Descriptor instead. +func (*ImportGroupItemsResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{22} +} + +func (x *ImportGroupItemsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ImportGroupItemsResponse) GetItemsAdded() int32 { + if x != nil { + return x.ItemsAdded + } + return 0 +} + +func (x *ImportGroupItemsResponse) GetItemsSkipped() int32 { + if x != nil { + return x.ItemsSkipped + } + return 0 +} + +func (x *ImportGroupItemsResponse) GetFailedCount() int32 { + if x != nil { + return x.FailedCount + } + return 0 +} + +func (x *ImportGroupItemsResponse) GetErrors() []*ImportError { + if x != nil { + return x.Errors + } + return nil +} + +func (x *ImportGroupItemsResponse) GetSkipped() []*SkippedItem { + if x != nil { + return x.Skipped + } + return nil +} + +// DownloadGroupItemsTemplateRequest has no input fields. +type DownloadGroupItemsTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DownloadGroupItemsTemplateRequest) Reset() { + *x = DownloadGroupItemsTemplateRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DownloadGroupItemsTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DownloadGroupItemsTemplateRequest) ProtoMessage() {} + +func (x *DownloadGroupItemsTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DownloadGroupItemsTemplateRequest.ProtoReflect.Descriptor instead. +func (*DownloadGroupItemsTemplateRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{23} +} + +// DownloadGroupItemsTemplateResponse carries the blank template bytes. +type DownloadGroupItemsTemplateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Excel file content (.xlsx). + FileContent []byte `protobuf:"bytes,2,opt,name=file_content,json=fileContent,proto3" json:"file_content,omitempty"` + // Suggested filename. + FileName string `protobuf:"bytes,3,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DownloadGroupItemsTemplateResponse) Reset() { + *x = DownloadGroupItemsTemplateResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DownloadGroupItemsTemplateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DownloadGroupItemsTemplateResponse) ProtoMessage() {} + +func (x *DownloadGroupItemsTemplateResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DownloadGroupItemsTemplateResponse.ProtoReflect.Descriptor instead. +func (*DownloadGroupItemsTemplateResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{24} +} + +func (x *DownloadGroupItemsTemplateResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *DownloadGroupItemsTemplateResponse) GetFileContent() []byte { + if x != nil { + return x.FileContent + } + return nil +} + +func (x *DownloadGroupItemsTemplateResponse) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +// ExportUngroupedItemsRequest filters the ungrouped items export. No pagination. +type ExportUngroupedItemsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Period filter (YYYYMM). Empty = all periods. + Period string `protobuf:"bytes,1,opt,name=period,proto3" json:"period,omitempty"` + // Free-text search on item_code, item_name, item_type_code, grade_code. + Search string `protobuf:"bytes,2,opt,name=search,proto3" json:"search,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExportUngroupedItemsRequest) Reset() { + *x = ExportUngroupedItemsRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExportUngroupedItemsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportUngroupedItemsRequest) ProtoMessage() {} + +func (x *ExportUngroupedItemsRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportUngroupedItemsRequest.ProtoReflect.Descriptor instead. +func (*ExportUngroupedItemsRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{25} +} + +func (x *ExportUngroupedItemsRequest) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *ExportUngroupedItemsRequest) GetSearch() string { + if x != nil { + return x.Search + } + return "" +} + +// ExportUngroupedItemsResponse carries the Excel bytes + filename. +type ExportUngroupedItemsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Excel file content (.xlsx). + FileContent []byte `protobuf:"bytes,2,opt,name=file_content,json=fileContent,proto3" json:"file_content,omitempty"` + // Suggested filename. + FileName string `protobuf:"bytes,3,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExportUngroupedItemsResponse) Reset() { + *x = ExportUngroupedItemsResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExportUngroupedItemsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportUngroupedItemsResponse) ProtoMessage() {} + +func (x *ExportUngroupedItemsResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportUngroupedItemsResponse.ProtoReflect.Descriptor instead. +func (*ExportUngroupedItemsResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{26} +} + +func (x *ExportUngroupedItemsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ExportUngroupedItemsResponse) GetFileContent() []byte { + if x != nil { + return x.FileContent + } + return nil +} + +func (x *ExportUngroupedItemsResponse) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +// Ungrouped items response. +type ListUngroupedItemsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Ungrouped items for the requested filters. + Data []*UngroupedItem `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty"` + // Pagination metadata. + Pagination *v1.PaginationResponse `protobuf:"bytes,3,opt,name=pagination,proto3" json:"pagination,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUngroupedItemsResponse) Reset() { + *x = ListUngroupedItemsResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUngroupedItemsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUngroupedItemsResponse) ProtoMessage() {} + +func (x *ListUngroupedItemsResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUngroupedItemsResponse.ProtoReflect.Descriptor instead. +func (*ListUngroupedItemsResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{27} +} + +func (x *ListUngroupedItemsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ListUngroupedItemsResponse) GetData() []*UngroupedItem { + if x != nil { + return x.Data + } + return nil +} + +func (x *ListUngroupedItemsResponse) GetPagination() *v1.PaginationResponse { + if x != nil { + return x.Pagination + } + return nil +} + +// RMGroupItemRates is one row per item currently in a group with its per-stage +// Oracle sync rates for a given period. Items with no sync row for the period +// still appear (all rates 0). +type RMGroupItemRates struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Item code. + ItemCode string `protobuf:"bytes,1,opt,name=item_code,json=itemCode,proto3" json:"item_code,omitempty"` + // Item name (snapshot from detail row). + ItemName string `protobuf:"bytes,2,opt,name=item_name,json=itemName,proto3" json:"item_name,omitempty"` + // Grade code. + GradeCode string `protobuf:"bytes,3,opt,name=grade_code,json=gradeCode,proto3" json:"grade_code,omitempty"` + // Item grade. + ItemGrade string `protobuf:"bytes,4,opt,name=item_grade,json=itemGrade,proto3" json:"item_grade,omitempty"` + // UOM code. + UomCode string `protobuf:"bytes,5,opt,name=uom_code,json=uomCode,proto3" json:"uom_code,omitempty"` + // Whether the detail is active. + IsActive bool `protobuf:"varint,6,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"` + // Whether the detail is a dummy row. + IsDummy bool `protobuf:"varint,7,opt,name=is_dummy,json=isDummy,proto3" json:"is_dummy,omitempty"` + // Period the rates apply to (YYYYMM), empty when no sync row was found. + Period string `protobuf:"bytes,8,opt,name=period,proto3" json:"period,omitempty"` + // CONS qty / value / rate. + ConsQty float64 `protobuf:"fixed64,9,opt,name=cons_qty,json=consQty,proto3" json:"cons_qty,omitempty"` + ConsVal float64 `protobuf:"fixed64,10,opt,name=cons_val,json=consVal,proto3" json:"cons_val,omitempty"` + ConsRate float64 `protobuf:"fixed64,11,opt,name=cons_rate,json=consRate,proto3" json:"cons_rate,omitempty"` + // STORES qty / value / rate. + StoresQty float64 `protobuf:"fixed64,12,opt,name=stores_qty,json=storesQty,proto3" json:"stores_qty,omitempty"` + StoresVal float64 `protobuf:"fixed64,13,opt,name=stores_val,json=storesVal,proto3" json:"stores_val,omitempty"` + StoresRate float64 `protobuf:"fixed64,14,opt,name=stores_rate,json=storesRate,proto3" json:"stores_rate,omitempty"` + // DEPT qty / value / rate. + DeptQty float64 `protobuf:"fixed64,15,opt,name=dept_qty,json=deptQty,proto3" json:"dept_qty,omitempty"` + DeptVal float64 `protobuf:"fixed64,16,opt,name=dept_val,json=deptVal,proto3" json:"dept_val,omitempty"` + DeptRate float64 `protobuf:"fixed64,17,opt,name=dept_rate,json=deptRate,proto3" json:"dept_rate,omitempty"` + // PO_1 qty / value / rate. + LastPoQty1 float64 `protobuf:"fixed64,18,opt,name=last_po_qty1,json=lastPoQty1,proto3" json:"last_po_qty1,omitempty"` + LastPoVal1 float64 `protobuf:"fixed64,19,opt,name=last_po_val1,json=lastPoVal1,proto3" json:"last_po_val1,omitempty"` + LastPoRate1 float64 `protobuf:"fixed64,20,opt,name=last_po_rate1,json=lastPoRate1,proto3" json:"last_po_rate1,omitempty"` + // PO_2 qty / value / rate. + LastPoQty2 float64 `protobuf:"fixed64,21,opt,name=last_po_qty2,json=lastPoQty2,proto3" json:"last_po_qty2,omitempty"` + LastPoVal2 float64 `protobuf:"fixed64,22,opt,name=last_po_val2,json=lastPoVal2,proto3" json:"last_po_val2,omitempty"` + LastPoRate2 float64 `protobuf:"fixed64,23,opt,name=last_po_rate2,json=lastPoRate2,proto3" json:"last_po_rate2,omitempty"` + // PO_3 qty / value / rate. + LastPoQty3 float64 `protobuf:"fixed64,24,opt,name=last_po_qty3,json=lastPoQty3,proto3" json:"last_po_qty3,omitempty"` + LastPoVal3 float64 `protobuf:"fixed64,25,opt,name=last_po_val3,json=lastPoVal3,proto3" json:"last_po_val3,omitempty"` + LastPoRate3 float64 `protobuf:"fixed64,26,opt,name=last_po_rate3,json=lastPoRate3,proto3" json:"last_po_rate3,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RMGroupItemRates) Reset() { + *x = RMGroupItemRates{} + mi := &file_finance_v1_rm_group_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RMGroupItemRates) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RMGroupItemRates) ProtoMessage() {} + +func (x *RMGroupItemRates) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RMGroupItemRates.ProtoReflect.Descriptor instead. +func (*RMGroupItemRates) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{28} +} + +func (x *RMGroupItemRates) GetItemCode() string { + if x != nil { + return x.ItemCode + } + return "" +} + +func (x *RMGroupItemRates) GetItemName() string { + if x != nil { + return x.ItemName + } + return "" +} + +func (x *RMGroupItemRates) GetGradeCode() string { + if x != nil { + return x.GradeCode + } + return "" +} + +func (x *RMGroupItemRates) GetItemGrade() string { + if x != nil { + return x.ItemGrade + } + return "" +} + +func (x *RMGroupItemRates) GetUomCode() string { + if x != nil { + return x.UomCode + } + return "" +} + +func (x *RMGroupItemRates) GetIsActive() bool { + if x != nil { + return x.IsActive + } + return false +} + +func (x *RMGroupItemRates) GetIsDummy() bool { + if x != nil { + return x.IsDummy + } + return false +} + +func (x *RMGroupItemRates) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *RMGroupItemRates) GetConsQty() float64 { + if x != nil { + return x.ConsQty + } + return 0 +} + +func (x *RMGroupItemRates) GetConsVal() float64 { + if x != nil { + return x.ConsVal + } + return 0 +} + +func (x *RMGroupItemRates) GetConsRate() float64 { + if x != nil { + return x.ConsRate + } + return 0 +} + +func (x *RMGroupItemRates) GetStoresQty() float64 { + if x != nil { + return x.StoresQty + } + return 0 +} + +func (x *RMGroupItemRates) GetStoresVal() float64 { + if x != nil { + return x.StoresVal + } + return 0 +} + +func (x *RMGroupItemRates) GetStoresRate() float64 { + if x != nil { + return x.StoresRate + } + return 0 +} + +func (x *RMGroupItemRates) GetDeptQty() float64 { + if x != nil { + return x.DeptQty + } + return 0 +} + +func (x *RMGroupItemRates) GetDeptVal() float64 { + if x != nil { + return x.DeptVal + } + return 0 +} + +func (x *RMGroupItemRates) GetDeptRate() float64 { + if x != nil { + return x.DeptRate + } + return 0 +} + +func (x *RMGroupItemRates) GetLastPoQty1() float64 { + if x != nil { + return x.LastPoQty1 + } + return 0 +} + +func (x *RMGroupItemRates) GetLastPoVal1() float64 { + if x != nil { + return x.LastPoVal1 + } + return 0 +} + +func (x *RMGroupItemRates) GetLastPoRate1() float64 { + if x != nil { + return x.LastPoRate1 + } + return 0 +} + +func (x *RMGroupItemRates) GetLastPoQty2() float64 { + if x != nil { + return x.LastPoQty2 + } + return 0 +} + +func (x *RMGroupItemRates) GetLastPoVal2() float64 { + if x != nil { + return x.LastPoVal2 + } + return 0 +} + +func (x *RMGroupItemRates) GetLastPoRate2() float64 { + if x != nil { + return x.LastPoRate2 + } + return 0 +} + +func (x *RMGroupItemRates) GetLastPoQty3() float64 { + if x != nil { + return x.LastPoQty3 + } + return 0 +} + +func (x *RMGroupItemRates) GetLastPoVal3() float64 { + if x != nil { + return x.LastPoVal3 + } + return 0 +} + +func (x *RMGroupItemRates) GetLastPoRate3() float64 { + if x != nil { + return x.LastPoRate3 + } + return 0 +} + +// Fetch per-item rates for a group in a given period. +type GetRMGroupItemRatesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Group head UUID. + GroupHeadId string `protobuf:"bytes,1,opt,name=group_head_id,json=groupHeadId,proto3" json:"group_head_id,omitempty"` + // Period (YYYYMM). + Period string `protobuf:"bytes,2,opt,name=period,proto3" json:"period,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRMGroupItemRatesRequest) Reset() { + *x = GetRMGroupItemRatesRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRMGroupItemRatesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRMGroupItemRatesRequest) ProtoMessage() {} + +func (x *GetRMGroupItemRatesRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRMGroupItemRatesRequest.ProtoReflect.Descriptor instead. +func (*GetRMGroupItemRatesRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{29} +} + +func (x *GetRMGroupItemRatesRequest) GetGroupHeadId() string { + if x != nil { + return x.GroupHeadId + } + return "" +} + +func (x *GetRMGroupItemRatesRequest) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +// Group item rates response. +type GetRMGroupItemRatesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Per-item rates, one row per active detail. + Data []*RMGroupItemRates `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRMGroupItemRatesResponse) Reset() { + *x = GetRMGroupItemRatesResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRMGroupItemRatesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRMGroupItemRatesResponse) ProtoMessage() {} + +func (x *GetRMGroupItemRatesResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRMGroupItemRatesResponse.ProtoReflect.Descriptor instead. +func (*GetRMGroupItemRatesResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{30} +} + +func (x *GetRMGroupItemRatesResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *GetRMGroupItemRatesResponse) GetData() []*RMGroupItemRates { + if x != nil { + return x.Data + } + return nil +} + +// ExportRMGroupsRequest filters the export. Active filter only (no pagination). +type ExportRMGroupsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Filter by active/inactive (UNSPECIFIED = all). + ActiveFilter ActiveFilter `protobuf:"varint,1,opt,name=active_filter,json=activeFilter,proto3,enum=finance.v1.ActiveFilter" json:"active_filter,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExportRMGroupsRequest) Reset() { + *x = ExportRMGroupsRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExportRMGroupsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportRMGroupsRequest) ProtoMessage() {} + +func (x *ExportRMGroupsRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportRMGroupsRequest.ProtoReflect.Descriptor instead. +func (*ExportRMGroupsRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{31} +} + +func (x *ExportRMGroupsRequest) GetActiveFilter() ActiveFilter { + if x != nil { + return x.ActiveFilter + } + return ActiveFilter_ACTIVE_FILTER_UNSPECIFIED +} + +// ExportRMGroupsResponse returns a multi-sheet Excel (Groups + Items). +type ExportRMGroupsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Excel file content (.xlsx). + FileContent []byte `protobuf:"bytes,2,opt,name=file_content,json=fileContent,proto3" json:"file_content,omitempty"` + // Suggested filename. + FileName string `protobuf:"bytes,3,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExportRMGroupsResponse) Reset() { + *x = ExportRMGroupsResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExportRMGroupsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportRMGroupsResponse) ProtoMessage() {} + +func (x *ExportRMGroupsResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportRMGroupsResponse.ProtoReflect.Descriptor instead. +func (*ExportRMGroupsResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{32} +} + +func (x *ExportRMGroupsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ExportRMGroupsResponse) GetFileContent() []byte { + if x != nil { + return x.FileContent + } + return nil +} + +func (x *ExportRMGroupsResponse) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +// ImportRMGroupsRequest accepts a 2-sheet Excel. User may include only the +// Groups sheet (header-only import), only the Items sheet (detail-only for +// existing groups), or both. +type ImportRMGroupsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Excel file content (.xlsx / .xls). + FileContent []byte `protobuf:"bytes,1,opt,name=file_content,json=fileContent,proto3" json:"file_content,omitempty"` + // Original filename (format detected by extension). + FileName string `protobuf:"bytes,2,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + // How to handle existing groups by `group_code`: "skip" (default) or "update". + // Detail rows always additive: items already active in ANOTHER group are + // reported as skipped errors; items already in the target group are ignored. + DuplicateAction string `protobuf:"bytes,3,opt,name=duplicate_action,json=duplicateAction,proto3" json:"duplicate_action,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImportRMGroupsRequest) Reset() { + *x = ImportRMGroupsRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImportRMGroupsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImportRMGroupsRequest) ProtoMessage() {} + +func (x *ImportRMGroupsRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImportRMGroupsRequest.ProtoReflect.Descriptor instead. +func (*ImportRMGroupsRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{33} +} + +func (x *ImportRMGroupsRequest) GetFileContent() []byte { + if x != nil { + return x.FileContent + } + return nil +} + +func (x *ImportRMGroupsRequest) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *ImportRMGroupsRequest) GetDuplicateAction() string { + if x != nil { + return x.DuplicateAction + } + return "" +} + +// ImportRMGroupsResponse summarizes the outcome. +type ImportRMGroupsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Number of group heads created. + GroupsCreated int32 `protobuf:"varint,2,opt,name=groups_created,json=groupsCreated,proto3" json:"groups_created,omitempty"` + // Number of group heads updated (duplicate_action=update). + GroupsUpdated int32 `protobuf:"varint,3,opt,name=groups_updated,json=groupsUpdated,proto3" json:"groups_updated,omitempty"` + // Number of group heads skipped (duplicate_action=skip + existed). + GroupsSkipped int32 `protobuf:"varint,4,opt,name=groups_skipped,json=groupsSkipped,proto3" json:"groups_skipped,omitempty"` + // Number of detail items successfully added. + ItemsAdded int32 `protobuf:"varint,5,opt,name=items_added,json=itemsAdded,proto3" json:"items_added,omitempty"` + // Number of detail items skipped (already in target group). + ItemsSkipped int32 `protobuf:"varint,6,opt,name=items_skipped,json=itemsSkipped,proto3" json:"items_skipped,omitempty"` + // Number of failed rows (across both sheets). + FailedCount int32 `protobuf:"varint,7,opt,name=failed_count,json=failedCount,proto3" json:"failed_count,omitempty"` + // Per-row errors (reuses finance.v1.ImportError). + Errors []*ImportError `protobuf:"bytes,8,rep,name=errors,proto3" json:"errors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImportRMGroupsResponse) Reset() { + *x = ImportRMGroupsResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImportRMGroupsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImportRMGroupsResponse) ProtoMessage() {} + +func (x *ImportRMGroupsResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImportRMGroupsResponse.ProtoReflect.Descriptor instead. +func (*ImportRMGroupsResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{34} +} + +func (x *ImportRMGroupsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ImportRMGroupsResponse) GetGroupsCreated() int32 { + if x != nil { + return x.GroupsCreated + } + return 0 +} + +func (x *ImportRMGroupsResponse) GetGroupsUpdated() int32 { + if x != nil { + return x.GroupsUpdated + } + return 0 +} + +func (x *ImportRMGroupsResponse) GetGroupsSkipped() int32 { + if x != nil { + return x.GroupsSkipped + } + return 0 +} + +func (x *ImportRMGroupsResponse) GetItemsAdded() int32 { + if x != nil { + return x.ItemsAdded + } + return 0 +} + +func (x *ImportRMGroupsResponse) GetItemsSkipped() int32 { + if x != nil { + return x.ItemsSkipped + } + return 0 +} + +func (x *ImportRMGroupsResponse) GetFailedCount() int32 { + if x != nil { + return x.FailedCount + } + return 0 +} + +func (x *ImportRMGroupsResponse) GetErrors() []*ImportError { + if x != nil { + return x.Errors + } + return nil +} + +// DownloadRMGroupTemplateRequest has no filters. +type DownloadRMGroupTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DownloadRMGroupTemplateRequest) Reset() { + *x = DownloadRMGroupTemplateRequest{} + mi := &file_finance_v1_rm_group_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DownloadRMGroupTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DownloadRMGroupTemplateRequest) ProtoMessage() {} + +func (x *DownloadRMGroupTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DownloadRMGroupTemplateRequest.ProtoReflect.Descriptor instead. +func (*DownloadRMGroupTemplateRequest) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{35} +} + +// DownloadRMGroupTemplateResponse returns the blank 2-sheet template. +type DownloadRMGroupTemplateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response envelope. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Excel template file content (.xlsx). + FileContent []byte `protobuf:"bytes,2,opt,name=file_content,json=fileContent,proto3" json:"file_content,omitempty"` + // Suggested filename. + FileName string `protobuf:"bytes,3,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DownloadRMGroupTemplateResponse) Reset() { + *x = DownloadRMGroupTemplateResponse{} + mi := &file_finance_v1_rm_group_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DownloadRMGroupTemplateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DownloadRMGroupTemplateResponse) ProtoMessage() {} + +func (x *DownloadRMGroupTemplateResponse) ProtoReflect() protoreflect.Message { + mi := &file_finance_v1_rm_group_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DownloadRMGroupTemplateResponse.ProtoReflect.Descriptor instead. +func (*DownloadRMGroupTemplateResponse) Descriptor() ([]byte, []int) { + return file_finance_v1_rm_group_proto_rawDescGZIP(), []int{36} +} + +func (x *DownloadRMGroupTemplateResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *DownloadRMGroupTemplateResponse) GetFileContent() []byte { + if x != nil { + return x.FileContent + } + return nil +} + +func (x *DownloadRMGroupTemplateResponse) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +var File_finance_v1_rm_group_proto protoreflect.FileDescriptor + +const file_finance_v1_rm_group_proto_rawDesc = "" + + "\n" + + "\x19finance/v1/rm_group.proto\x12\n" + + "finance.v1\x1a\x1bbuf/validate/validate.proto\x1a\x16common/v1/common.proto\x1a\x14finance/v1/uom.proto\x1a\x1cgoogle/api/annotations.proto\"\xfd\x05\n" + + "\vRMGroupHead\x12\"\n" + + "\rgroup_head_id\x18\x01 \x01(\tR\vgroupHeadId\x12\x1d\n" + + "\n" + + "group_code\x18\x02 \x01(\tR\tgroupCode\x12\x1d\n" + + "\n" + + "group_name\x18\x03 \x01(\tR\tgroupName\x12 \n" + + "\vdescription\x18\x04 \x01(\tR\vdescription\x12\x1c\n" + + "\tcolourant\x18\x05 \x01(\tR\tcolourant\x12\x17\n" + + "\aci_name\x18\x06 \x01(\tR\x06ciName\x12'\n" + + "\x0fcost_percentage\x18\a \x01(\x01R\x0ecostPercentage\x12\x1e\n" + + "\vcost_per_kg\x18\b \x01(\x01R\tcostPerKg\x12>\n" + + "\x0eflag_valuation\x18\t \x01(\x0e2\x17.finance.v1.RMGroupFlagR\rflagValuation\x12>\n" + + "\x0eflag_marketing\x18\n" + + " \x01(\x0e2\x17.finance.v1.RMGroupFlagR\rflagMarketing\x12@\n" + + "\x0fflag_simulation\x18\v \x01(\x0e2\x17.finance.v1.RMGroupFlagR\x0eflagSimulation\x121\n" + + "\x12init_val_valuation\x18\f \x01(\x01H\x00R\x10initValValuation\x88\x01\x01\x121\n" + + "\x12init_val_marketing\x18\r \x01(\x01H\x01R\x10initValMarketing\x88\x01\x01\x123\n" + + "\x13init_val_simulation\x18\x0e \x01(\x01H\x02R\x11initValSimulation\x88\x01\x01\x12\x1b\n" + + "\tis_active\x18\x0f \x01(\bR\bisActive\x12*\n" + + "\x05audit\x18\x10 \x01(\v2\x14.common.v1.AuditInfoR\x05auditB\x15\n" + + "\x13_init_val_valuationB\x15\n" + + "\x13_init_val_marketingB\x16\n" + + "\x14_init_val_simulation\"\xa0\x04\n" + + "\rRMGroupDetail\x12&\n" + + "\x0fgroup_detail_id\x18\x01 \x01(\tR\rgroupDetailId\x12\"\n" + + "\rgroup_head_id\x18\x02 \x01(\tR\vgroupHeadId\x12\x1b\n" + + "\titem_code\x18\x03 \x01(\tR\bitemCode\x12\x1b\n" + + "\titem_name\x18\x04 \x01(\tR\bitemName\x12$\n" + + "\x0eitem_type_code\x18\x05 \x01(\tR\fitemTypeCode\x12\x1d\n" + + "\n" + + "grade_code\x18\x06 \x01(\tR\tgradeCode\x12\x1d\n" + + "\n" + + "item_grade\x18\a \x01(\tR\titemGrade\x12\x19\n" + + "\buom_code\x18\b \x01(\tR\auomCode\x120\n" + + "\x11market_percentage\x18\t \x01(\x01H\x00R\x10marketPercentage\x88\x01\x01\x12+\n" + + "\x0fmarket_value_rp\x18\n" + + " \x01(\x01H\x01R\rmarketValueRp\x88\x01\x01\x12\x1d\n" + + "\n" + + "sort_order\x18\v \x01(\x05R\tsortOrder\x12\x1b\n" + + "\tis_active\x18\f \x01(\bR\bisActive\x12\x19\n" + + "\bis_dummy\x18\r \x01(\bR\aisDummy\x12*\n" + + "\x05audit\x18\x10 \x01(\v2\x14.common.v1.AuditInfoR\x05auditB\x14\n" + + "\x12_market_percentageB\x12\n" + + "\x10_market_value_rp\"z\n" + + "\x16RMGroupHeadWithDetails\x12+\n" + + "\x04head\x18\x01 \x01(\v2\x17.finance.v1.RMGroupHeadR\x04head\x123\n" + + "\adetails\x18\x02 \x03(\v2\x19.finance.v1.RMGroupDetailR\adetails\"\x9d\x06\n" + + "\rUngroupedItem\x12\x16\n" + + "\x06period\x18\x01 \x01(\tR\x06period\x12\x1b\n" + + "\titem_code\x18\x02 \x01(\tR\bitemCode\x12\x1b\n" + + "\titem_name\x18\x03 \x01(\tR\bitemName\x12$\n" + + "\x0eitem_type_code\x18\x04 \x01(\tR\fitemTypeCode\x12\x1d\n" + + "\n" + + "grade_code\x18\x05 \x01(\tR\tgradeCode\x12\x1d\n" + + "\n" + + "item_grade\x18\x06 \x01(\tR\titemGrade\x12\x19\n" + + "\buom_code\x18\a \x01(\tR\auomCode\x12\x19\n" + + "\bcons_val\x18\b \x01(\x01R\aconsVal\x12\x1d\n" + + "\n" + + "stores_val\x18\t \x01(\x01R\tstoresVal\x12\x19\n" + + "\bcons_qty\x18\n" + + " \x01(\x01R\aconsQty\x12\x1b\n" + + "\tcons_rate\x18\v \x01(\x01R\bconsRate\x12\x1d\n" + + "\n" + + "stores_qty\x18\f \x01(\x01R\tstoresQty\x12\x1f\n" + + "\vstores_rate\x18\r \x01(\x01R\n" + + "storesRate\x12\x19\n" + + "\bdept_qty\x18\x0e \x01(\x01R\adeptQty\x12\x19\n" + + "\bdept_val\x18\x0f \x01(\x01R\adeptVal\x12\x1b\n" + + "\tdept_rate\x18\x10 \x01(\x01R\bdeptRate\x12 \n" + + "\flast_po_qty1\x18\x11 \x01(\x01R\n" + + "lastPoQty1\x12 \n" + + "\flast_po_val1\x18\x12 \x01(\x01R\n" + + "lastPoVal1\x12\"\n" + + "\rlast_po_rate1\x18\x13 \x01(\x01R\vlastPoRate1\x12 \n" + + "\flast_po_qty2\x18\x14 \x01(\x01R\n" + + "lastPoQty2\x12 \n" + + "\flast_po_val2\x18\x15 \x01(\x01R\n" + + "lastPoVal2\x12\"\n" + + "\rlast_po_rate2\x18\x16 \x01(\x01R\vlastPoRate2\x12 \n" + + "\flast_po_qty3\x18\x17 \x01(\x01R\n" + + "lastPoQty3\x12 \n" + + "\flast_po_val3\x18\x18 \x01(\x01R\n" + + "lastPoVal3\x12\"\n" + + "\rlast_po_rate3\x18\x19 \x01(\x01R\vlastPoRate3\"\xbc\x01\n" + + "\vSkippedItem\x12\x1b\n" + + "\titem_code\x18\x01 \x01(\tR\bitemCode\x12/\n" + + "\x14owning_group_head_id\x18\x02 \x01(\tR\x11owningGroupHeadId\x123\n" + + "\x16owning_group_detail_id\x18\x03 \x01(\tR\x13owningGroupDetailId\x12*\n" + + "\x11owning_group_code\x18\x04 \x01(\tR\x0fowningGroupCode\"\xe6\x02\n" + + "\x14CreateRMGroupRequest\x12E\n" + + "\n" + + "group_code\x18\x01 \x01(\tB&\xbaH#r!\x10\x01\x18\x1e2\x1b^[A-Z0-9][A-Z0-9 \\-]{0,29}$R\tgroupCode\x12)\n" + + "\n" + + "group_name\x18\x02 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\xc8\x01R\tgroupName\x12*\n" + + "\vdescription\x18\x03 \x01(\tB\b\xbaH\x05r\x03\x18\xe8\aR\vdescription\x12%\n" + + "\tcolourant\x18\x04 \x01(\tB\a\xbaH\x04r\x02\x18dR\tcolourant\x12 \n" + + "\aci_name\x18\x05 \x01(\tB\a\xbaH\x04r\x02\x18dR\x06ciName\x127\n" + + "\x0fcost_percentage\x18\x06 \x01(\x01B\x0e\xbaH\v\x12\t)\x00\x00\x00\x00\x00\x00\x00\x00R\x0ecostPercentage\x12.\n" + + "\vcost_per_kg\x18\a \x01(\x01B\x0e\xbaH\v\x12\t)\x00\x00\x00\x00\x00\x00\x00\x00R\tcostPerKg\"q\n" + + "\x15CreateRMGroupResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12+\n" + + "\x04data\x18\x02 \x01(\v2\x17.finance.v1.RMGroupHeadR\x04data\"A\n" + + "\x11GetRMGroupRequest\x12,\n" + + "\rgroup_head_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\vgroupHeadId\"y\n" + + "\x12GetRMGroupResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x126\n" + + "\x04data\x18\x02 \x01(\v2\".finance.v1.RMGroupHeadWithDetailsR\x04data\"\xdd\t\n" + + "\x14UpdateRMGroupRequest\x12,\n" + + "\rgroup_head_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\vgroupHeadId\x12,\n" + + "\n" + + "group_name\x18\x02 \x01(\tB\b\xbaH\x05r\x03\x18\xc8\x01H\x00R\tgroupName\x88\x01\x01\x12/\n" + + "\vdescription\x18\x03 \x01(\tB\b\xbaH\x05r\x03\x18\xe8\aH\x01R\vdescription\x88\x01\x01\x12*\n" + + "\tcolourant\x18\x04 \x01(\tB\a\xbaH\x04r\x02\x18dH\x02R\tcolourant\x88\x01\x01\x12%\n" + + "\aci_name\x18\x05 \x01(\tB\a\xbaH\x04r\x02\x18dH\x03R\x06ciName\x88\x01\x01\x12<\n" + + "\x0fcost_percentage\x18\x06 \x01(\x01B\x0e\xbaH\v\x12\t)\x00\x00\x00\x00\x00\x00\x00\x00H\x04R\x0ecostPercentage\x88\x01\x01\x123\n" + + "\vcost_per_kg\x18\a \x01(\x01B\x0e\xbaH\v\x12\t)\x00\x00\x00\x00\x00\x00\x00\x00H\x05R\tcostPerKg\x88\x01\x01\x12M\n" + + "\x0eflag_valuation\x18\b \x01(\x0e2\x17.finance.v1.RMGroupFlagB\b\xbaH\x05\x82\x01\x02 \x00H\x06R\rflagValuation\x88\x01\x01\x12M\n" + + "\x0eflag_marketing\x18\t \x01(\x0e2\x17.finance.v1.RMGroupFlagB\b\xbaH\x05\x82\x01\x02 \x00H\aR\rflagMarketing\x88\x01\x01\x12O\n" + + "\x0fflag_simulation\x18\n" + + " \x01(\x0e2\x17.finance.v1.RMGroupFlagB\b\xbaH\x05\x82\x01\x02 \x00H\bR\x0eflagSimulation\x88\x01\x01\x12A\n" + + "\x12init_val_valuation\x18\v \x01(\x01B\x0e\xbaH\v\x12\t)\x00\x00\x00\x00\x00\x00\x00\x00H\tR\x10initValValuation\x88\x01\x01\x12A\n" + + "\x12init_val_marketing\x18\f \x01(\x01B\x0e\xbaH\v\x12\t)\x00\x00\x00\x00\x00\x00\x00\x00H\n" + + "R\x10initValMarketing\x88\x01\x01\x12C\n" + + "\x13init_val_simulation\x18\r \x01(\x01B\x0e\xbaH\v\x12\t)\x00\x00\x00\x00\x00\x00\x00\x00H\vR\x11initValSimulation\x88\x01\x01\x12 \n" + + "\tis_active\x18\x0e \x01(\bH\fR\bisActive\x88\x01\x01\x127\n" + + "\x18clear_init_val_valuation\x18\x0f \x01(\bR\x15clearInitValValuation\x127\n" + + "\x18clear_init_val_marketing\x18\x10 \x01(\bR\x15clearInitValMarketing\x129\n" + + "\x19clear_init_val_simulation\x18\x11 \x01(\bR\x16clearInitValSimulationB\r\n" + + "\v_group_nameB\x0e\n" + + "\f_descriptionB\f\n" + + "\n" + + "_colourantB\n" + + "\n" + + "\b_ci_nameB\x12\n" + + "\x10_cost_percentageB\x0e\n" + + "\f_cost_per_kgB\x11\n" + + "\x0f_flag_valuationB\x11\n" + + "\x0f_flag_marketingB\x12\n" + + "\x10_flag_simulationB\x15\n" + + "\x13_init_val_valuationB\x15\n" + + "\x13_init_val_marketingB\x16\n" + + "\x14_init_val_simulationB\f\n" + + "\n" + + "_is_active\"q\n" + + "\x15UpdateRMGroupResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12+\n" + + "\x04data\x18\x02 \x01(\v2\x17.finance.v1.RMGroupHeadR\x04data\"D\n" + + "\x14DeleteRMGroupRequest\x12,\n" + + "\rgroup_head_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\vgroupHeadId\"D\n" + + "\x15DeleteRMGroupResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\"\xb9\x02\n" + + "\x13ListRMGroupsRequest\x12\x1b\n" + + "\x04page\x18\x01 \x01(\x05B\a\xbaH\x04\x1a\x02(\x01R\x04page\x12&\n" + + "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d(\x01R\bpageSize\x12\x1f\n" + + "\x06search\x18\x03 \x01(\tB\a\xbaH\x04r\x02\x18dR\x06search\x12=\n" + + "\ractive_filter\x18\x04 \x01(\x0e2\x18.finance.v1.ActiveFilterR\factiveFilter\x12J\n" + + "\asort_by\x18\x05 \x01(\tB1\xbaH.r,R\x00R\x04codeR\n" + + "group_nameR\n" + + "sort_orderR\n" + + "created_atR\x06sortBy\x121\n" + + "\n" + + "sort_order\x18\x06 \x01(\tB\x12\xbaH\x0fr\rR\x00R\x03ascR\x04descR\tsortOrder\"\xaf\x01\n" + + "\x14ListRMGroupsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12+\n" + + "\x04data\x18\x02 \x03(\v2\x17.finance.v1.RMGroupHeadR\x04data\x12=\n" + + "\n" + + "pagination\x18\x03 \x01(\v2\x1d.common.v1.PaginationResponseR\n" + + "pagination\"b\n" + + "\x10AddItemSelection\x12&\n" + + "\titem_code\x18\x01 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x182R\bitemCode\x12&\n" + + "\n" + + "grade_code\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x18(R\tgradeCode\"\xba\x01\n" + + "\x0fAddItemsRequest\x12,\n" + + "\rgroup_head_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\vgroupHeadId\x120\n" + + "\n" + + "item_codes\x18\x02 \x03(\tB\x11\xbaH\x0e\x92\x01\v\x10\xf4\x03\"\x06r\x04\x10\x01\x182R\titemCodes\x12G\n" + + "\n" + + "selections\x18\x03 \x03(\v2\x1c.finance.v1.AddItemSelectionB\t\xbaH\x06\x92\x01\x03\x10\xf4\x03R\n" + + "selections\"\xa3\x01\n" + + "\x10AddItemsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12/\n" + + "\x05added\x18\x02 \x03(\v2\x19.finance.v1.RMGroupDetailR\x05added\x121\n" + + "\askipped\x18\x03 \x03(\v2\x17.finance.v1.SkippedItemR\askipped\"\xbb\x01\n" + + "\x12RemoveItemsRequest\x12,\n" + + "\rgroup_head_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\vgroupHeadId\x12<\n" + + "\x10group_detail_ids\x18\x02 \x03(\tB\x12\xbaH\x0f\x92\x01\f\b\x01\x10\xf4\x03\"\x05r\x03\xb0\x01\x01R\x0egroupDetailIds\x129\n" + + "\x04mode\x18\x03 \x01(\x0e2\x1b.finance.v1.RemoveItemsModeB\b\xbaH\x05\x82\x01\x02 \x00R\x04mode\"g\n" + + "\x13RemoveItemsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12#\n" + + "\rremoved_count\x18\x02 \x01(\x05R\fremovedCount\"\xae\x01\n" + + "\x19ListUngroupedItemsRequest\x12\x1b\n" + + "\x04page\x18\x01 \x01(\x05B\a\xbaH\x04\x1a\x02(\x01R\x04page\x12&\n" + + "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d(\x01R\bpageSize\x12+\n" + + "\x06period\x18\x03 \x01(\tB\x13\xbaH\x10r\x0e\x18\x062\n" + + "^$|^\\d{6}$R\x06period\x12\x1f\n" + + "\x06search\x18\x04 \x01(\tB\a\xbaH\x04r\x02\x18dR\x06search\"\xb8\x01\n" + + "\x17ImportGroupItemsRequest\x12,\n" + + "\rgroup_head_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\vgroupHeadId\x12/\n" + + "\ffile_content\x18\x02 \x01(\fB\f\xbaH\tz\a\x10\x01\x18\x80\x80\x80\x05R\vfileContent\x12>\n" + + "\tfile_name\x18\x03 \x01(\tB!\xbaH\x1er\x1c\x10\x01\x18\xff\x012\x15^[^/\\\\]+\\.(xlsx|xls)$R\bfileName\"\x94\x02\n" + + "\x18ImportGroupItemsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12\x1f\n" + + "\vitems_added\x18\x02 \x01(\x05R\n" + + "itemsAdded\x12#\n" + + "\ritems_skipped\x18\x03 \x01(\x05R\fitemsSkipped\x12!\n" + + "\ffailed_count\x18\x04 \x01(\x05R\vfailedCount\x12/\n" + + "\x06errors\x18\x05 \x03(\v2\x17.finance.v1.ImportErrorR\x06errors\x121\n" + + "\askipped\x18\x06 \x03(\v2\x17.finance.v1.SkippedItemR\askipped\"#\n" + + "!DownloadGroupItemsTemplateRequest\"\x91\x01\n" + + "\"DownloadGroupItemsTemplateResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12!\n" + + "\ffile_content\x18\x02 \x01(\fR\vfileContent\x12\x1b\n" + + "\tfile_name\x18\x03 \x01(\tR\bfileName\"k\n" + + "\x1bExportUngroupedItemsRequest\x12+\n" + + "\x06period\x18\x01 \x01(\tB\x13\xbaH\x10r\x0e\x18\x062\n" + + "^$|^\\d{6}$R\x06period\x12\x1f\n" + + "\x06search\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x18dR\x06search\"\x8b\x01\n" + + "\x1cExportUngroupedItemsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12!\n" + + "\ffile_content\x18\x02 \x01(\fR\vfileContent\x12\x1b\n" + + "\tfile_name\x18\x03 \x01(\tR\bfileName\"\xb7\x01\n" + + "\x1aListUngroupedItemsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12-\n" + + "\x04data\x18\x02 \x03(\v2\x19.finance.v1.UngroupedItemR\x04data\x12=\n" + + "\n" + + "pagination\x18\x03 \x01(\v2\x1d.common.v1.PaginationResponseR\n" + + "pagination\"\xb2\x06\n" + + "\x10RMGroupItemRates\x12\x1b\n" + + "\titem_code\x18\x01 \x01(\tR\bitemCode\x12\x1b\n" + + "\titem_name\x18\x02 \x01(\tR\bitemName\x12\x1d\n" + + "\n" + + "grade_code\x18\x03 \x01(\tR\tgradeCode\x12\x1d\n" + + "\n" + + "item_grade\x18\x04 \x01(\tR\titemGrade\x12\x19\n" + + "\buom_code\x18\x05 \x01(\tR\auomCode\x12\x1b\n" + + "\tis_active\x18\x06 \x01(\bR\bisActive\x12\x19\n" + + "\bis_dummy\x18\a \x01(\bR\aisDummy\x12\x16\n" + + "\x06period\x18\b \x01(\tR\x06period\x12\x19\n" + + "\bcons_qty\x18\t \x01(\x01R\aconsQty\x12\x19\n" + + "\bcons_val\x18\n" + + " \x01(\x01R\aconsVal\x12\x1b\n" + + "\tcons_rate\x18\v \x01(\x01R\bconsRate\x12\x1d\n" + + "\n" + + "stores_qty\x18\f \x01(\x01R\tstoresQty\x12\x1d\n" + + "\n" + + "stores_val\x18\r \x01(\x01R\tstoresVal\x12\x1f\n" + + "\vstores_rate\x18\x0e \x01(\x01R\n" + + "storesRate\x12\x19\n" + + "\bdept_qty\x18\x0f \x01(\x01R\adeptQty\x12\x19\n" + + "\bdept_val\x18\x10 \x01(\x01R\adeptVal\x12\x1b\n" + + "\tdept_rate\x18\x11 \x01(\x01R\bdeptRate\x12 \n" + + "\flast_po_qty1\x18\x12 \x01(\x01R\n" + + "lastPoQty1\x12 \n" + + "\flast_po_val1\x18\x13 \x01(\x01R\n" + + "lastPoVal1\x12\"\n" + + "\rlast_po_rate1\x18\x14 \x01(\x01R\vlastPoRate1\x12 \n" + + "\flast_po_qty2\x18\x15 \x01(\x01R\n" + + "lastPoQty2\x12 \n" + + "\flast_po_val2\x18\x16 \x01(\x01R\n" + + "lastPoVal2\x12\"\n" + + "\rlast_po_rate2\x18\x17 \x01(\x01R\vlastPoRate2\x12 \n" + + "\flast_po_qty3\x18\x18 \x01(\x01R\n" + + "lastPoQty3\x12 \n" + + "\flast_po_val3\x18\x19 \x01(\x01R\n" + + "lastPoVal3\x12\"\n" + + "\rlast_po_rate3\x18\x1a \x01(\x01R\vlastPoRate3\"u\n" + + "\x1aGetRMGroupItemRatesRequest\x12,\n" + + "\rgroup_head_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\vgroupHeadId\x12)\n" + + "\x06period\x18\x02 \x01(\tB\x11\xbaH\x0er\f2\a^\\d{6}$\x98\x01\x06R\x06period\"|\n" + + "\x1bGetRMGroupItemRatesResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x120\n" + + "\x04data\x18\x02 \x03(\v2\x1c.finance.v1.RMGroupItemRatesR\x04data\"V\n" + + "\x15ExportRMGroupsRequest\x12=\n" + + "\ractive_filter\x18\x01 \x01(\x0e2\x18.finance.v1.ActiveFilterR\factiveFilter\"\x85\x01\n" + + "\x16ExportRMGroupsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12!\n" + + "\ffile_content\x18\x02 \x01(\fR\vfileContent\x12\x1b\n" + + "\tfile_name\x18\x03 \x01(\tR\bfileName\"\xca\x01\n" + + "\x15ImportRMGroupsRequest\x12/\n" + + "\ffile_content\x18\x01 \x01(\fB\f\xbaH\tz\a\x10\x01\x18\x80\x80\x80\x05R\vfileContent\x12>\n" + + "\tfile_name\x18\x02 \x01(\tB!\xbaH\x1er\x1c\x10\x01\x18\xff\x012\x15^[^/\\\\]+\\.(xlsx|xls)$R\bfileName\x12@\n" + + "\x10duplicate_action\x18\x03 \x01(\tB\x15\xbaH\x12r\x10\x10\x01R\x04skipR\x06updateR\x0fduplicateAction\"\xd4\x02\n" + + "\x16ImportRMGroupsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12%\n" + + "\x0egroups_created\x18\x02 \x01(\x05R\rgroupsCreated\x12%\n" + + "\x0egroups_updated\x18\x03 \x01(\x05R\rgroupsUpdated\x12%\n" + + "\x0egroups_skipped\x18\x04 \x01(\x05R\rgroupsSkipped\x12\x1f\n" + + "\vitems_added\x18\x05 \x01(\x05R\n" + + "itemsAdded\x12#\n" + + "\ritems_skipped\x18\x06 \x01(\x05R\fitemsSkipped\x12!\n" + + "\ffailed_count\x18\a \x01(\x05R\vfailedCount\x12/\n" + + "\x06errors\x18\b \x03(\v2\x17.finance.v1.ImportErrorR\x06errors\" \n" + + "\x1eDownloadRMGroupTemplateRequest\"\x8e\x01\n" + + "\x1fDownloadRMGroupTemplateResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12!\n" + + "\ffile_content\x18\x02 \x01(\fR\vfileContent\x12\x1b\n" + + "\tfile_name\x18\x03 \x01(\tR\bfileName*\xd6\x01\n" + + "\vRMGroupFlag\x12\x1d\n" + + "\x19RM_GROUP_FLAG_UNSPECIFIED\x10\x00\x12\x16\n" + + "\x12RM_GROUP_FLAG_INIT\x10\x01\x12\x16\n" + + "\x12RM_GROUP_FLAG_CONS\x10\x02\x12\x18\n" + + "\x14RM_GROUP_FLAG_STORES\x10\x03\x12\x16\n" + + "\x12RM_GROUP_FLAG_DEPT\x10\x04\x12\x16\n" + + "\x12RM_GROUP_FLAG_PO_1\x10\x05\x12\x16\n" + + "\x12RM_GROUP_FLAG_PO_2\x10\x06\x12\x16\n" + + "\x12RM_GROUP_FLAG_PO_3\x10\a*y\n" + + "\x0fRemoveItemsMode\x12!\n" + + "\x1dREMOVE_ITEMS_MODE_UNSPECIFIED\x10\x00\x12 \n" + + "\x1cREMOVE_ITEMS_MODE_DEACTIVATE\x10\x01\x12!\n" + + "\x1dREMOVE_ITEMS_MODE_SOFT_DELETE\x10\x022\x8a\x11\n" + + "\x0eRMGroupService\x12z\n" + + "\rCreateRMGroup\x12 .finance.v1.CreateRMGroupRequest\x1a!.finance.v1.CreateRMGroupResponse\"$\x82\xd3\xe4\x93\x02\x1e:\x01*\"\x19/api/v1/finance/rm-groups\x12~\n" + + "\n" + + "GetRMGroup\x12\x1d.finance.v1.GetRMGroupRequest\x1a\x1e.finance.v1.GetRMGroupResponse\"1\x82\xd3\xe4\x93\x02+\x12)/api/v1/finance/rm-groups/{group_head_id}\x12\x8a\x01\n" + + "\rUpdateRMGroup\x12 .finance.v1.UpdateRMGroupRequest\x1a!.finance.v1.UpdateRMGroupResponse\"4\x82\xd3\xe4\x93\x02.:\x01*\x1a)/api/v1/finance/rm-groups/{group_head_id}\x12\x87\x01\n" + + "\rDeleteRMGroup\x12 .finance.v1.DeleteRMGroupRequest\x1a!.finance.v1.DeleteRMGroupResponse\"1\x82\xd3\xe4\x93\x02+*)/api/v1/finance/rm-groups/{group_head_id}\x12t\n" + + "\fListRMGroups\x12\x1f.finance.v1.ListRMGroupsRequest\x1a .finance.v1.ListRMGroupsResponse\"!\x82\xd3\xe4\x93\x02\x1b\x12\x19/api/v1/finance/rm-groups\x12\x81\x01\n" + + "\bAddItems\x12\x1b.finance.v1.AddItemsRequest\x1a\x1c.finance.v1.AddItemsResponse\":\x82\xd3\xe4\x93\x024:\x01*\"//api/v1/finance/rm-groups/{group_head_id}/items\x12\x91\x01\n" + + "\vRemoveItems\x12\x1e.finance.v1.RemoveItemsRequest\x1a\x1f.finance.v1.RemoveItemsResponse\"A\x82\xd3\xe4\x93\x02;:\x01*\"6/api/v1/finance/rm-groups/{group_head_id}/items/remove\x12\x90\x01\n" + + "\x12ListUngroupedItems\x12%.finance.v1.ListUngroupedItemsRequest\x1a&.finance.v1.ListUngroupedItemsResponse\"+\x82\xd3\xe4\x93\x02%\x12#/api/v1/finance/rm-groups/ungrouped\x12\xa4\x01\n" + + "\x13GetRMGroupItemRates\x12&.finance.v1.GetRMGroupItemRatesRequest\x1a'.finance.v1.GetRMGroupItemRatesResponse\"<\x82\xd3\xe4\x93\x026\x124/api/v1/finance/rm-groups/{group_head_id}/item-rates\x12\x81\x01\n" + + "\x0eExportRMGroups\x12!.finance.v1.ExportRMGroupsRequest\x1a\".finance.v1.ExportRMGroupsResponse\"(\x82\xd3\xe4\x93\x02\"\x12 /api/v1/finance/rm-groups/export\x12\x84\x01\n" + + "\x0eImportRMGroups\x12!.finance.v1.ImportRMGroupsRequest\x1a\".finance.v1.ImportRMGroupsResponse\"+\x82\xd3\xe4\x93\x02%:\x01*\" /api/v1/finance/rm-groups/import\x12\x9e\x01\n" + + "\x17DownloadRMGroupTemplate\x12*.finance.v1.DownloadRMGroupTemplateRequest\x1a+.finance.v1.DownloadRMGroupTemplateResponse\"*\x82\xd3\xe4\x93\x02$\x12\"/api/v1/finance/rm-groups/template\x12\x9d\x01\n" + + "\x14ExportUngroupedItems\x12'.finance.v1.ExportUngroupedItemsRequest\x1a(.finance.v1.ExportUngroupedItemsResponse\"2\x82\xd3\xe4\x93\x02,\x12*/api/v1/finance/rm-groups/ungrouped/export\x12\xa0\x01\n" + + "\x10ImportGroupItems\x12#.finance.v1.ImportGroupItemsRequest\x1a$.finance.v1.ImportGroupItemsResponse\"A\x82\xd3\xe4\x93\x02;:\x01*\"6/api/v1/finance/rm-groups/{group_head_id}/items/import\x12\xad\x01\n" + + "\x1aDownloadGroupItemsTemplate\x12-.finance.v1.DownloadGroupItemsTemplateRequest\x1a..finance.v1.DownloadGroupItemsTemplateResponse\"0\x82\xd3\xe4\x93\x02*\x12(/api/v1/finance/rm-groups/items/templateB\xa6\x01\n" + + "\x0ecom.finance.v1B\fRmGroupProtoP\x01Z=github.com/mutugading/goapps-backend/gen/finance/v1;financev1\xa2\x02\x03FXX\xaa\x02\n" + + "Finance.V1\xca\x02\n" + + "Finance\\V1\xe2\x02\x16Finance\\V1\\GPBMetadata\xea\x02\vFinance::V1b\x06proto3" + +var ( + file_finance_v1_rm_group_proto_rawDescOnce sync.Once + file_finance_v1_rm_group_proto_rawDescData []byte +) + +func file_finance_v1_rm_group_proto_rawDescGZIP() []byte { + file_finance_v1_rm_group_proto_rawDescOnce.Do(func() { + file_finance_v1_rm_group_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_finance_v1_rm_group_proto_rawDesc), len(file_finance_v1_rm_group_proto_rawDesc))) + }) + return file_finance_v1_rm_group_proto_rawDescData +} + +var file_finance_v1_rm_group_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_finance_v1_rm_group_proto_msgTypes = make([]protoimpl.MessageInfo, 37) +var file_finance_v1_rm_group_proto_goTypes = []any{ + (RMGroupFlag)(0), // 0: finance.v1.RMGroupFlag + (RemoveItemsMode)(0), // 1: finance.v1.RemoveItemsMode + (*RMGroupHead)(nil), // 2: finance.v1.RMGroupHead + (*RMGroupDetail)(nil), // 3: finance.v1.RMGroupDetail + (*RMGroupHeadWithDetails)(nil), // 4: finance.v1.RMGroupHeadWithDetails + (*UngroupedItem)(nil), // 5: finance.v1.UngroupedItem + (*SkippedItem)(nil), // 6: finance.v1.SkippedItem + (*CreateRMGroupRequest)(nil), // 7: finance.v1.CreateRMGroupRequest + (*CreateRMGroupResponse)(nil), // 8: finance.v1.CreateRMGroupResponse + (*GetRMGroupRequest)(nil), // 9: finance.v1.GetRMGroupRequest + (*GetRMGroupResponse)(nil), // 10: finance.v1.GetRMGroupResponse + (*UpdateRMGroupRequest)(nil), // 11: finance.v1.UpdateRMGroupRequest + (*UpdateRMGroupResponse)(nil), // 12: finance.v1.UpdateRMGroupResponse + (*DeleteRMGroupRequest)(nil), // 13: finance.v1.DeleteRMGroupRequest + (*DeleteRMGroupResponse)(nil), // 14: finance.v1.DeleteRMGroupResponse + (*ListRMGroupsRequest)(nil), // 15: finance.v1.ListRMGroupsRequest + (*ListRMGroupsResponse)(nil), // 16: finance.v1.ListRMGroupsResponse + (*AddItemSelection)(nil), // 17: finance.v1.AddItemSelection + (*AddItemsRequest)(nil), // 18: finance.v1.AddItemsRequest + (*AddItemsResponse)(nil), // 19: finance.v1.AddItemsResponse + (*RemoveItemsRequest)(nil), // 20: finance.v1.RemoveItemsRequest + (*RemoveItemsResponse)(nil), // 21: finance.v1.RemoveItemsResponse + (*ListUngroupedItemsRequest)(nil), // 22: finance.v1.ListUngroupedItemsRequest + (*ImportGroupItemsRequest)(nil), // 23: finance.v1.ImportGroupItemsRequest + (*ImportGroupItemsResponse)(nil), // 24: finance.v1.ImportGroupItemsResponse + (*DownloadGroupItemsTemplateRequest)(nil), // 25: finance.v1.DownloadGroupItemsTemplateRequest + (*DownloadGroupItemsTemplateResponse)(nil), // 26: finance.v1.DownloadGroupItemsTemplateResponse + (*ExportUngroupedItemsRequest)(nil), // 27: finance.v1.ExportUngroupedItemsRequest + (*ExportUngroupedItemsResponse)(nil), // 28: finance.v1.ExportUngroupedItemsResponse + (*ListUngroupedItemsResponse)(nil), // 29: finance.v1.ListUngroupedItemsResponse + (*RMGroupItemRates)(nil), // 30: finance.v1.RMGroupItemRates + (*GetRMGroupItemRatesRequest)(nil), // 31: finance.v1.GetRMGroupItemRatesRequest + (*GetRMGroupItemRatesResponse)(nil), // 32: finance.v1.GetRMGroupItemRatesResponse + (*ExportRMGroupsRequest)(nil), // 33: finance.v1.ExportRMGroupsRequest + (*ExportRMGroupsResponse)(nil), // 34: finance.v1.ExportRMGroupsResponse + (*ImportRMGroupsRequest)(nil), // 35: finance.v1.ImportRMGroupsRequest + (*ImportRMGroupsResponse)(nil), // 36: finance.v1.ImportRMGroupsResponse + (*DownloadRMGroupTemplateRequest)(nil), // 37: finance.v1.DownloadRMGroupTemplateRequest + (*DownloadRMGroupTemplateResponse)(nil), // 38: finance.v1.DownloadRMGroupTemplateResponse + (*v1.AuditInfo)(nil), // 39: common.v1.AuditInfo + (*v1.BaseResponse)(nil), // 40: common.v1.BaseResponse + (ActiveFilter)(0), // 41: finance.v1.ActiveFilter + (*v1.PaginationResponse)(nil), // 42: common.v1.PaginationResponse + (*ImportError)(nil), // 43: finance.v1.ImportError +} +var file_finance_v1_rm_group_proto_depIdxs = []int32{ + 0, // 0: finance.v1.RMGroupHead.flag_valuation:type_name -> finance.v1.RMGroupFlag + 0, // 1: finance.v1.RMGroupHead.flag_marketing:type_name -> finance.v1.RMGroupFlag + 0, // 2: finance.v1.RMGroupHead.flag_simulation:type_name -> finance.v1.RMGroupFlag + 39, // 3: finance.v1.RMGroupHead.audit:type_name -> common.v1.AuditInfo + 39, // 4: finance.v1.RMGroupDetail.audit:type_name -> common.v1.AuditInfo + 2, // 5: finance.v1.RMGroupHeadWithDetails.head:type_name -> finance.v1.RMGroupHead + 3, // 6: finance.v1.RMGroupHeadWithDetails.details:type_name -> finance.v1.RMGroupDetail + 40, // 7: finance.v1.CreateRMGroupResponse.base:type_name -> common.v1.BaseResponse + 2, // 8: finance.v1.CreateRMGroupResponse.data:type_name -> finance.v1.RMGroupHead + 40, // 9: finance.v1.GetRMGroupResponse.base:type_name -> common.v1.BaseResponse + 4, // 10: finance.v1.GetRMGroupResponse.data:type_name -> finance.v1.RMGroupHeadWithDetails + 0, // 11: finance.v1.UpdateRMGroupRequest.flag_valuation:type_name -> finance.v1.RMGroupFlag + 0, // 12: finance.v1.UpdateRMGroupRequest.flag_marketing:type_name -> finance.v1.RMGroupFlag + 0, // 13: finance.v1.UpdateRMGroupRequest.flag_simulation:type_name -> finance.v1.RMGroupFlag + 40, // 14: finance.v1.UpdateRMGroupResponse.base:type_name -> common.v1.BaseResponse + 2, // 15: finance.v1.UpdateRMGroupResponse.data:type_name -> finance.v1.RMGroupHead + 40, // 16: finance.v1.DeleteRMGroupResponse.base:type_name -> common.v1.BaseResponse + 41, // 17: finance.v1.ListRMGroupsRequest.active_filter:type_name -> finance.v1.ActiveFilter + 40, // 18: finance.v1.ListRMGroupsResponse.base:type_name -> common.v1.BaseResponse + 2, // 19: finance.v1.ListRMGroupsResponse.data:type_name -> finance.v1.RMGroupHead + 42, // 20: finance.v1.ListRMGroupsResponse.pagination:type_name -> common.v1.PaginationResponse + 17, // 21: finance.v1.AddItemsRequest.selections:type_name -> finance.v1.AddItemSelection + 40, // 22: finance.v1.AddItemsResponse.base:type_name -> common.v1.BaseResponse + 3, // 23: finance.v1.AddItemsResponse.added:type_name -> finance.v1.RMGroupDetail + 6, // 24: finance.v1.AddItemsResponse.skipped:type_name -> finance.v1.SkippedItem + 1, // 25: finance.v1.RemoveItemsRequest.mode:type_name -> finance.v1.RemoveItemsMode + 40, // 26: finance.v1.RemoveItemsResponse.base:type_name -> common.v1.BaseResponse + 40, // 27: finance.v1.ImportGroupItemsResponse.base:type_name -> common.v1.BaseResponse + 43, // 28: finance.v1.ImportGroupItemsResponse.errors:type_name -> finance.v1.ImportError + 6, // 29: finance.v1.ImportGroupItemsResponse.skipped:type_name -> finance.v1.SkippedItem + 40, // 30: finance.v1.DownloadGroupItemsTemplateResponse.base:type_name -> common.v1.BaseResponse + 40, // 31: finance.v1.ExportUngroupedItemsResponse.base:type_name -> common.v1.BaseResponse + 40, // 32: finance.v1.ListUngroupedItemsResponse.base:type_name -> common.v1.BaseResponse + 5, // 33: finance.v1.ListUngroupedItemsResponse.data:type_name -> finance.v1.UngroupedItem + 42, // 34: finance.v1.ListUngroupedItemsResponse.pagination:type_name -> common.v1.PaginationResponse + 40, // 35: finance.v1.GetRMGroupItemRatesResponse.base:type_name -> common.v1.BaseResponse + 30, // 36: finance.v1.GetRMGroupItemRatesResponse.data:type_name -> finance.v1.RMGroupItemRates + 41, // 37: finance.v1.ExportRMGroupsRequest.active_filter:type_name -> finance.v1.ActiveFilter + 40, // 38: finance.v1.ExportRMGroupsResponse.base:type_name -> common.v1.BaseResponse + 40, // 39: finance.v1.ImportRMGroupsResponse.base:type_name -> common.v1.BaseResponse + 43, // 40: finance.v1.ImportRMGroupsResponse.errors:type_name -> finance.v1.ImportError + 40, // 41: finance.v1.DownloadRMGroupTemplateResponse.base:type_name -> common.v1.BaseResponse + 7, // 42: finance.v1.RMGroupService.CreateRMGroup:input_type -> finance.v1.CreateRMGroupRequest + 9, // 43: finance.v1.RMGroupService.GetRMGroup:input_type -> finance.v1.GetRMGroupRequest + 11, // 44: finance.v1.RMGroupService.UpdateRMGroup:input_type -> finance.v1.UpdateRMGroupRequest + 13, // 45: finance.v1.RMGroupService.DeleteRMGroup:input_type -> finance.v1.DeleteRMGroupRequest + 15, // 46: finance.v1.RMGroupService.ListRMGroups:input_type -> finance.v1.ListRMGroupsRequest + 18, // 47: finance.v1.RMGroupService.AddItems:input_type -> finance.v1.AddItemsRequest + 20, // 48: finance.v1.RMGroupService.RemoveItems:input_type -> finance.v1.RemoveItemsRequest + 22, // 49: finance.v1.RMGroupService.ListUngroupedItems:input_type -> finance.v1.ListUngroupedItemsRequest + 31, // 50: finance.v1.RMGroupService.GetRMGroupItemRates:input_type -> finance.v1.GetRMGroupItemRatesRequest + 33, // 51: finance.v1.RMGroupService.ExportRMGroups:input_type -> finance.v1.ExportRMGroupsRequest + 35, // 52: finance.v1.RMGroupService.ImportRMGroups:input_type -> finance.v1.ImportRMGroupsRequest + 37, // 53: finance.v1.RMGroupService.DownloadRMGroupTemplate:input_type -> finance.v1.DownloadRMGroupTemplateRequest + 27, // 54: finance.v1.RMGroupService.ExportUngroupedItems:input_type -> finance.v1.ExportUngroupedItemsRequest + 23, // 55: finance.v1.RMGroupService.ImportGroupItems:input_type -> finance.v1.ImportGroupItemsRequest + 25, // 56: finance.v1.RMGroupService.DownloadGroupItemsTemplate:input_type -> finance.v1.DownloadGroupItemsTemplateRequest + 8, // 57: finance.v1.RMGroupService.CreateRMGroup:output_type -> finance.v1.CreateRMGroupResponse + 10, // 58: finance.v1.RMGroupService.GetRMGroup:output_type -> finance.v1.GetRMGroupResponse + 12, // 59: finance.v1.RMGroupService.UpdateRMGroup:output_type -> finance.v1.UpdateRMGroupResponse + 14, // 60: finance.v1.RMGroupService.DeleteRMGroup:output_type -> finance.v1.DeleteRMGroupResponse + 16, // 61: finance.v1.RMGroupService.ListRMGroups:output_type -> finance.v1.ListRMGroupsResponse + 19, // 62: finance.v1.RMGroupService.AddItems:output_type -> finance.v1.AddItemsResponse + 21, // 63: finance.v1.RMGroupService.RemoveItems:output_type -> finance.v1.RemoveItemsResponse + 29, // 64: finance.v1.RMGroupService.ListUngroupedItems:output_type -> finance.v1.ListUngroupedItemsResponse + 32, // 65: finance.v1.RMGroupService.GetRMGroupItemRates:output_type -> finance.v1.GetRMGroupItemRatesResponse + 34, // 66: finance.v1.RMGroupService.ExportRMGroups:output_type -> finance.v1.ExportRMGroupsResponse + 36, // 67: finance.v1.RMGroupService.ImportRMGroups:output_type -> finance.v1.ImportRMGroupsResponse + 38, // 68: finance.v1.RMGroupService.DownloadRMGroupTemplate:output_type -> finance.v1.DownloadRMGroupTemplateResponse + 28, // 69: finance.v1.RMGroupService.ExportUngroupedItems:output_type -> finance.v1.ExportUngroupedItemsResponse + 24, // 70: finance.v1.RMGroupService.ImportGroupItems:output_type -> finance.v1.ImportGroupItemsResponse + 26, // 71: finance.v1.RMGroupService.DownloadGroupItemsTemplate:output_type -> finance.v1.DownloadGroupItemsTemplateResponse + 57, // [57:72] is the sub-list for method output_type + 42, // [42:57] is the sub-list for method input_type + 42, // [42:42] is the sub-list for extension type_name + 42, // [42:42] is the sub-list for extension extendee + 0, // [0:42] is the sub-list for field type_name +} + +func init() { file_finance_v1_rm_group_proto_init() } +func file_finance_v1_rm_group_proto_init() { + if File_finance_v1_rm_group_proto != nil { + return + } + file_finance_v1_uom_proto_init() + file_finance_v1_rm_group_proto_msgTypes[0].OneofWrappers = []any{} + file_finance_v1_rm_group_proto_msgTypes[1].OneofWrappers = []any{} + file_finance_v1_rm_group_proto_msgTypes[9].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_finance_v1_rm_group_proto_rawDesc), len(file_finance_v1_rm_group_proto_rawDesc)), + NumEnums: 2, + NumMessages: 37, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_finance_v1_rm_group_proto_goTypes, + DependencyIndexes: file_finance_v1_rm_group_proto_depIdxs, + EnumInfos: file_finance_v1_rm_group_proto_enumTypes, + MessageInfos: file_finance_v1_rm_group_proto_msgTypes, + }.Build() + File_finance_v1_rm_group_proto = out.File + file_finance_v1_rm_group_proto_goTypes = nil + file_finance_v1_rm_group_proto_depIdxs = nil +} diff --git a/gen/finance/v1/rm_group.pb.gw.go b/gen/finance/v1/rm_group.pb.gw.go new file mode 100644 index 0000000..bc23783 --- /dev/null +++ b/gen/finance/v1/rm_group.pb.gw.go @@ -0,0 +1,1223 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: finance/v1/rm_group.proto + +/* +Package financev1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package financev1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_RMGroupService_CreateRMGroup_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateRMGroupRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.CreateRMGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_CreateRMGroup_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateRMGroupRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateRMGroup(ctx, &protoReq) + return msg, metadata, err +} + +func request_RMGroupService_GetRMGroup_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetRMGroupRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + msg, err := client.GetRMGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_GetRMGroup_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetRMGroupRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + msg, err := server.GetRMGroup(ctx, &protoReq) + return msg, metadata, err +} + +func request_RMGroupService_UpdateRMGroup_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateRMGroupRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + msg, err := client.UpdateRMGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_UpdateRMGroup_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateRMGroupRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + msg, err := server.UpdateRMGroup(ctx, &protoReq) + return msg, metadata, err +} + +func request_RMGroupService_DeleteRMGroup_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteRMGroupRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + msg, err := client.DeleteRMGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_DeleteRMGroup_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteRMGroupRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + msg, err := server.DeleteRMGroup(ctx, &protoReq) + return msg, metadata, err +} + +var filter_RMGroupService_ListRMGroups_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_RMGroupService_ListRMGroups_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListRMGroupsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMGroupService_ListRMGroups_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListRMGroups(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_ListRMGroups_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListRMGroupsRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMGroupService_ListRMGroups_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListRMGroups(ctx, &protoReq) + return msg, metadata, err +} + +func request_RMGroupService_AddItems_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq AddItemsRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + msg, err := client.AddItems(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_AddItems_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq AddItemsRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + msg, err := server.AddItems(ctx, &protoReq) + return msg, metadata, err +} + +func request_RMGroupService_RemoveItems_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RemoveItemsRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + msg, err := client.RemoveItems(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_RemoveItems_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RemoveItemsRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + msg, err := server.RemoveItems(ctx, &protoReq) + return msg, metadata, err +} + +var filter_RMGroupService_ListUngroupedItems_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_RMGroupService_ListUngroupedItems_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListUngroupedItemsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMGroupService_ListUngroupedItems_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListUngroupedItems(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_ListUngroupedItems_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListUngroupedItemsRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMGroupService_ListUngroupedItems_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListUngroupedItems(ctx, &protoReq) + return msg, metadata, err +} + +var filter_RMGroupService_GetRMGroupItemRates_0 = &utilities.DoubleArray{Encoding: map[string]int{"group_head_id": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_RMGroupService_GetRMGroupItemRates_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetRMGroupItemRatesRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMGroupService_GetRMGroupItemRates_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.GetRMGroupItemRates(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_GetRMGroupItemRates_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetRMGroupItemRatesRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMGroupService_GetRMGroupItemRates_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.GetRMGroupItemRates(ctx, &protoReq) + return msg, metadata, err +} + +var filter_RMGroupService_ExportRMGroups_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_RMGroupService_ExportRMGroups_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ExportRMGroupsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMGroupService_ExportRMGroups_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ExportRMGroups(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_ExportRMGroups_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ExportRMGroupsRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMGroupService_ExportRMGroups_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ExportRMGroups(ctx, &protoReq) + return msg, metadata, err +} + +func request_RMGroupService_ImportRMGroups_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ImportRMGroupsRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ImportRMGroups(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_ImportRMGroups_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ImportRMGroupsRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ImportRMGroups(ctx, &protoReq) + return msg, metadata, err +} + +func request_RMGroupService_DownloadRMGroupTemplate_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DownloadRMGroupTemplateRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.DownloadRMGroupTemplate(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_DownloadRMGroupTemplate_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DownloadRMGroupTemplateRequest + metadata runtime.ServerMetadata + ) + msg, err := server.DownloadRMGroupTemplate(ctx, &protoReq) + return msg, metadata, err +} + +var filter_RMGroupService_ExportUngroupedItems_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_RMGroupService_ExportUngroupedItems_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ExportUngroupedItemsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMGroupService_ExportUngroupedItems_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ExportUngroupedItems(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_ExportUngroupedItems_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ExportUngroupedItemsRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RMGroupService_ExportUngroupedItems_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ExportUngroupedItems(ctx, &protoReq) + return msg, metadata, err +} + +func request_RMGroupService_ImportGroupItems_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ImportGroupItemsRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + msg, err := client.ImportGroupItems(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_ImportGroupItems_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ImportGroupItemsRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["group_head_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group_head_id") + } + protoReq.GroupHeadId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group_head_id", err) + } + msg, err := server.ImportGroupItems(ctx, &protoReq) + return msg, metadata, err +} + +func request_RMGroupService_DownloadGroupItemsTemplate_0(ctx context.Context, marshaler runtime.Marshaler, client RMGroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DownloadGroupItemsTemplateRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.DownloadGroupItemsTemplate(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_RMGroupService_DownloadGroupItemsTemplate_0(ctx context.Context, marshaler runtime.Marshaler, server RMGroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DownloadGroupItemsTemplateRequest + metadata runtime.ServerMetadata + ) + msg, err := server.DownloadGroupItemsTemplate(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterRMGroupServiceHandlerServer registers the http handlers for service RMGroupService to "mux". +// UnaryRPC :call RMGroupServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterRMGroupServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterRMGroupServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server RMGroupServiceServer) error { + mux.Handle(http.MethodPost, pattern_RMGroupService_CreateRMGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/CreateRMGroup", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_CreateRMGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_CreateRMGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_GetRMGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/GetRMGroup", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_GetRMGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_GetRMGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPut, pattern_RMGroupService_UpdateRMGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/UpdateRMGroup", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_UpdateRMGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_UpdateRMGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_RMGroupService_DeleteRMGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/DeleteRMGroup", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_DeleteRMGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_DeleteRMGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_ListRMGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/ListRMGroups", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_ListRMGroups_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_ListRMGroups_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_RMGroupService_AddItems_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/AddItems", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}/items")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_AddItems_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_AddItems_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_RMGroupService_RemoveItems_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/RemoveItems", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}/items/remove")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_RemoveItems_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_RemoveItems_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_ListUngroupedItems_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/ListUngroupedItems", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/ungrouped")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_ListUngroupedItems_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_ListUngroupedItems_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_GetRMGroupItemRates_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/GetRMGroupItemRates", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}/item-rates")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_GetRMGroupItemRates_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_GetRMGroupItemRates_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_ExportRMGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/ExportRMGroups", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/export")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_ExportRMGroups_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_ExportRMGroups_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_RMGroupService_ImportRMGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/ImportRMGroups", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/import")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_ImportRMGroups_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_ImportRMGroups_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_DownloadRMGroupTemplate_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/DownloadRMGroupTemplate", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/template")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_DownloadRMGroupTemplate_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_DownloadRMGroupTemplate_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_ExportUngroupedItems_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/ExportUngroupedItems", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/ungrouped/export")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_ExportUngroupedItems_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_ExportUngroupedItems_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_RMGroupService_ImportGroupItems_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/ImportGroupItems", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}/items/import")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_ImportGroupItems_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_ImportGroupItems_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_DownloadGroupItemsTemplate_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/finance.v1.RMGroupService/DownloadGroupItemsTemplate", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/items/template")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RMGroupService_DownloadGroupItemsTemplate_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_DownloadGroupItemsTemplate_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterRMGroupServiceHandlerFromEndpoint is same as RegisterRMGroupServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterRMGroupServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterRMGroupServiceHandler(ctx, mux, conn) +} + +// RegisterRMGroupServiceHandler registers the http handlers for service RMGroupService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterRMGroupServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterRMGroupServiceHandlerClient(ctx, mux, NewRMGroupServiceClient(conn)) +} + +// RegisterRMGroupServiceHandlerClient registers the http handlers for service RMGroupService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "RMGroupServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "RMGroupServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "RMGroupServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterRMGroupServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client RMGroupServiceClient) error { + mux.Handle(http.MethodPost, pattern_RMGroupService_CreateRMGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/CreateRMGroup", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_CreateRMGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_CreateRMGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_GetRMGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/GetRMGroup", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_GetRMGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_GetRMGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPut, pattern_RMGroupService_UpdateRMGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/UpdateRMGroup", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_UpdateRMGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_UpdateRMGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_RMGroupService_DeleteRMGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/DeleteRMGroup", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_DeleteRMGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_DeleteRMGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_ListRMGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/ListRMGroups", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_ListRMGroups_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_ListRMGroups_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_RMGroupService_AddItems_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/AddItems", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}/items")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_AddItems_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_AddItems_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_RMGroupService_RemoveItems_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/RemoveItems", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}/items/remove")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_RemoveItems_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_RemoveItems_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_ListUngroupedItems_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/ListUngroupedItems", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/ungrouped")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_ListUngroupedItems_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_ListUngroupedItems_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_GetRMGroupItemRates_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/GetRMGroupItemRates", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}/item-rates")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_GetRMGroupItemRates_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_GetRMGroupItemRates_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_ExportRMGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/ExportRMGroups", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/export")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_ExportRMGroups_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_ExportRMGroups_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_RMGroupService_ImportRMGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/ImportRMGroups", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/import")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_ImportRMGroups_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_ImportRMGroups_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_DownloadRMGroupTemplate_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/DownloadRMGroupTemplate", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/template")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_DownloadRMGroupTemplate_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_DownloadRMGroupTemplate_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_ExportUngroupedItems_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/ExportUngroupedItems", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/ungrouped/export")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_ExportUngroupedItems_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_ExportUngroupedItems_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_RMGroupService_ImportGroupItems_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/ImportGroupItems", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/{group_head_id}/items/import")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_ImportGroupItems_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_ImportGroupItems_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_RMGroupService_DownloadGroupItemsTemplate_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/finance.v1.RMGroupService/DownloadGroupItemsTemplate", runtime.WithHTTPPathPattern("/api/v1/finance/rm-groups/items/template")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RMGroupService_DownloadGroupItemsTemplate_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_RMGroupService_DownloadGroupItemsTemplate_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_RMGroupService_CreateRMGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "finance", "rm-groups"}, "")) + pattern_RMGroupService_GetRMGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"api", "v1", "finance", "rm-groups", "group_head_id"}, "")) + pattern_RMGroupService_UpdateRMGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"api", "v1", "finance", "rm-groups", "group_head_id"}, "")) + pattern_RMGroupService_DeleteRMGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"api", "v1", "finance", "rm-groups", "group_head_id"}, "")) + pattern_RMGroupService_ListRMGroups_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "finance", "rm-groups"}, "")) + pattern_RMGroupService_AddItems_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 2, 5}, []string{"api", "v1", "finance", "rm-groups", "group_head_id", "items"}, "")) + pattern_RMGroupService_RemoveItems_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 2, 5, 2, 6}, []string{"api", "v1", "finance", "rm-groups", "group_head_id", "items", "remove"}, "")) + pattern_RMGroupService_ListUngroupedItems_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "finance", "rm-groups", "ungrouped"}, "")) + pattern_RMGroupService_GetRMGroupItemRates_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 2, 5}, []string{"api", "v1", "finance", "rm-groups", "group_head_id", "item-rates"}, "")) + pattern_RMGroupService_ExportRMGroups_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "finance", "rm-groups", "export"}, "")) + pattern_RMGroupService_ImportRMGroups_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "finance", "rm-groups", "import"}, "")) + pattern_RMGroupService_DownloadRMGroupTemplate_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "finance", "rm-groups", "template"}, "")) + pattern_RMGroupService_ExportUngroupedItems_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5}, []string{"api", "v1", "finance", "rm-groups", "ungrouped", "export"}, "")) + pattern_RMGroupService_ImportGroupItems_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 2, 5, 2, 6}, []string{"api", "v1", "finance", "rm-groups", "group_head_id", "items", "import"}, "")) + pattern_RMGroupService_DownloadGroupItemsTemplate_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5}, []string{"api", "v1", "finance", "rm-groups", "items", "template"}, "")) +) + +var ( + forward_RMGroupService_CreateRMGroup_0 = runtime.ForwardResponseMessage + forward_RMGroupService_GetRMGroup_0 = runtime.ForwardResponseMessage + forward_RMGroupService_UpdateRMGroup_0 = runtime.ForwardResponseMessage + forward_RMGroupService_DeleteRMGroup_0 = runtime.ForwardResponseMessage + forward_RMGroupService_ListRMGroups_0 = runtime.ForwardResponseMessage + forward_RMGroupService_AddItems_0 = runtime.ForwardResponseMessage + forward_RMGroupService_RemoveItems_0 = runtime.ForwardResponseMessage + forward_RMGroupService_ListUngroupedItems_0 = runtime.ForwardResponseMessage + forward_RMGroupService_GetRMGroupItemRates_0 = runtime.ForwardResponseMessage + forward_RMGroupService_ExportRMGroups_0 = runtime.ForwardResponseMessage + forward_RMGroupService_ImportRMGroups_0 = runtime.ForwardResponseMessage + forward_RMGroupService_DownloadRMGroupTemplate_0 = runtime.ForwardResponseMessage + forward_RMGroupService_ExportUngroupedItems_0 = runtime.ForwardResponseMessage + forward_RMGroupService_ImportGroupItems_0 = runtime.ForwardResponseMessage + forward_RMGroupService_DownloadGroupItemsTemplate_0 = runtime.ForwardResponseMessage +) diff --git a/gen/finance/v1/rm_group_grpc.pb.go b/gen/finance/v1/rm_group_grpc.pb.go new file mode 100644 index 0000000..63f67f2 --- /dev/null +++ b/gen/finance/v1/rm_group_grpc.pb.go @@ -0,0 +1,703 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: finance/v1/rm_group.proto + +package financev1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + RMGroupService_CreateRMGroup_FullMethodName = "/finance.v1.RMGroupService/CreateRMGroup" + RMGroupService_GetRMGroup_FullMethodName = "/finance.v1.RMGroupService/GetRMGroup" + RMGroupService_UpdateRMGroup_FullMethodName = "/finance.v1.RMGroupService/UpdateRMGroup" + RMGroupService_DeleteRMGroup_FullMethodName = "/finance.v1.RMGroupService/DeleteRMGroup" + RMGroupService_ListRMGroups_FullMethodName = "/finance.v1.RMGroupService/ListRMGroups" + RMGroupService_AddItems_FullMethodName = "/finance.v1.RMGroupService/AddItems" + RMGroupService_RemoveItems_FullMethodName = "/finance.v1.RMGroupService/RemoveItems" + RMGroupService_ListUngroupedItems_FullMethodName = "/finance.v1.RMGroupService/ListUngroupedItems" + RMGroupService_GetRMGroupItemRates_FullMethodName = "/finance.v1.RMGroupService/GetRMGroupItemRates" + RMGroupService_ExportRMGroups_FullMethodName = "/finance.v1.RMGroupService/ExportRMGroups" + RMGroupService_ImportRMGroups_FullMethodName = "/finance.v1.RMGroupService/ImportRMGroups" + RMGroupService_DownloadRMGroupTemplate_FullMethodName = "/finance.v1.RMGroupService/DownloadRMGroupTemplate" + RMGroupService_ExportUngroupedItems_FullMethodName = "/finance.v1.RMGroupService/ExportUngroupedItems" + RMGroupService_ImportGroupItems_FullMethodName = "/finance.v1.RMGroupService/ImportGroupItems" + RMGroupService_DownloadGroupItemsTemplate_FullMethodName = "/finance.v1.RMGroupService/DownloadGroupItemsTemplate" +) + +// RMGroupServiceClient is the client API for RMGroupService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// RMGroupService manages RM group heads + their item memberships and exposes +// the "ungrouped items" report that drives the grouping workflow. +type RMGroupServiceClient interface { + // CreateRMGroup creates a new RM group head. + CreateRMGroup(ctx context.Context, in *CreateRMGroupRequest, opts ...grpc.CallOption) (*CreateRMGroupResponse, error) + // GetRMGroup retrieves a group head + its details. + GetRMGroup(ctx context.Context, in *GetRMGroupRequest, opts ...grpc.CallOption) (*GetRMGroupResponse, error) + // UpdateRMGroup applies a partial update to a group head. + UpdateRMGroup(ctx context.Context, in *UpdateRMGroupRequest, opts ...grpc.CallOption) (*UpdateRMGroupResponse, error) + // DeleteRMGroup soft-deletes a group head (cascade to its details). + DeleteRMGroup(ctx context.Context, in *DeleteRMGroupRequest, opts ...grpc.CallOption) (*DeleteRMGroupResponse, error) + // ListRMGroups lists group heads with search + filter + pagination. + ListRMGroups(ctx context.Context, in *ListRMGroupsRequest, opts ...grpc.CallOption) (*ListRMGroupsResponse, error) + // AddItems assigns items to a group. Items already in another active group + // are returned in `skipped` instead of failing the batch. + AddItems(ctx context.Context, in *AddItemsRequest, opts ...grpc.CallOption) (*AddItemsResponse, error) + // RemoveItems removes details from a group (deactivate or soft-delete). + RemoveItems(ctx context.Context, in *RemoveItemsRequest, opts ...grpc.CallOption) (*RemoveItemsResponse, error) + // ListUngroupedItems reports items from the sync feed that have no active + // group assignment. + ListUngroupedItems(ctx context.Context, in *ListUngroupedItemsRequest, opts ...grpc.CallOption) (*ListUngroupedItemsResponse, error) + // GetRMGroupItemRates returns per-item per-stage rates for every active + // detail of a group in a given period. + GetRMGroupItemRates(ctx context.Context, in *GetRMGroupItemRatesRequest, opts ...grpc.CallOption) (*GetRMGroupItemRatesResponse, error) + // ExportRMGroups exports all groups + their items to a 2-sheet Excel. + ExportRMGroups(ctx context.Context, in *ExportRMGroupsRequest, opts ...grpc.CallOption) (*ExportRMGroupsResponse, error) + // ImportRMGroups imports groups and/or items from a 2-sheet Excel. Users can + // include only the Groups sheet, only the Items sheet, or both. + ImportRMGroups(ctx context.Context, in *ImportRMGroupsRequest, opts ...grpc.CallOption) (*ImportRMGroupsResponse, error) + // DownloadRMGroupTemplate returns a blank 2-sheet Excel with header rows. + DownloadRMGroupTemplate(ctx context.Context, in *DownloadRMGroupTemplateRequest, opts ...grpc.CallOption) (*DownloadRMGroupTemplateResponse, error) + // ExportUngroupedItems exports ungrouped items matching the filter to Excel. + ExportUngroupedItems(ctx context.Context, in *ExportUngroupedItemsRequest, opts ...grpc.CallOption) (*ExportUngroupedItemsResponse, error) + // ImportGroupItems bulk-assigns items to ONE existing group from a + // one-sheet Excel. Uses the same validation as the interactive AddItems + // flow (one item / one active group, sync-feed lookup, idempotent re-add). + ImportGroupItems(ctx context.Context, in *ImportGroupItemsRequest, opts ...grpc.CallOption) (*ImportGroupItemsResponse, error) + // DownloadGroupItemsTemplate returns a blank one-sheet Excel with just + // the columns ImportGroupItems expects. Paired with ImportGroupItems. + DownloadGroupItemsTemplate(ctx context.Context, in *DownloadGroupItemsTemplateRequest, opts ...grpc.CallOption) (*DownloadGroupItemsTemplateResponse, error) +} + +type rMGroupServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewRMGroupServiceClient(cc grpc.ClientConnInterface) RMGroupServiceClient { + return &rMGroupServiceClient{cc} +} + +func (c *rMGroupServiceClient) CreateRMGroup(ctx context.Context, in *CreateRMGroupRequest, opts ...grpc.CallOption) (*CreateRMGroupResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateRMGroupResponse) + err := c.cc.Invoke(ctx, RMGroupService_CreateRMGroup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) GetRMGroup(ctx context.Context, in *GetRMGroupRequest, opts ...grpc.CallOption) (*GetRMGroupResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetRMGroupResponse) + err := c.cc.Invoke(ctx, RMGroupService_GetRMGroup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) UpdateRMGroup(ctx context.Context, in *UpdateRMGroupRequest, opts ...grpc.CallOption) (*UpdateRMGroupResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateRMGroupResponse) + err := c.cc.Invoke(ctx, RMGroupService_UpdateRMGroup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) DeleteRMGroup(ctx context.Context, in *DeleteRMGroupRequest, opts ...grpc.CallOption) (*DeleteRMGroupResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteRMGroupResponse) + err := c.cc.Invoke(ctx, RMGroupService_DeleteRMGroup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) ListRMGroups(ctx context.Context, in *ListRMGroupsRequest, opts ...grpc.CallOption) (*ListRMGroupsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRMGroupsResponse) + err := c.cc.Invoke(ctx, RMGroupService_ListRMGroups_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) AddItems(ctx context.Context, in *AddItemsRequest, opts ...grpc.CallOption) (*AddItemsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AddItemsResponse) + err := c.cc.Invoke(ctx, RMGroupService_AddItems_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) RemoveItems(ctx context.Context, in *RemoveItemsRequest, opts ...grpc.CallOption) (*RemoveItemsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RemoveItemsResponse) + err := c.cc.Invoke(ctx, RMGroupService_RemoveItems_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) ListUngroupedItems(ctx context.Context, in *ListUngroupedItemsRequest, opts ...grpc.CallOption) (*ListUngroupedItemsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListUngroupedItemsResponse) + err := c.cc.Invoke(ctx, RMGroupService_ListUngroupedItems_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) GetRMGroupItemRates(ctx context.Context, in *GetRMGroupItemRatesRequest, opts ...grpc.CallOption) (*GetRMGroupItemRatesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetRMGroupItemRatesResponse) + err := c.cc.Invoke(ctx, RMGroupService_GetRMGroupItemRates_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) ExportRMGroups(ctx context.Context, in *ExportRMGroupsRequest, opts ...grpc.CallOption) (*ExportRMGroupsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExportRMGroupsResponse) + err := c.cc.Invoke(ctx, RMGroupService_ExportRMGroups_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) ImportRMGroups(ctx context.Context, in *ImportRMGroupsRequest, opts ...grpc.CallOption) (*ImportRMGroupsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ImportRMGroupsResponse) + err := c.cc.Invoke(ctx, RMGroupService_ImportRMGroups_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) DownloadRMGroupTemplate(ctx context.Context, in *DownloadRMGroupTemplateRequest, opts ...grpc.CallOption) (*DownloadRMGroupTemplateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DownloadRMGroupTemplateResponse) + err := c.cc.Invoke(ctx, RMGroupService_DownloadRMGroupTemplate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) ExportUngroupedItems(ctx context.Context, in *ExportUngroupedItemsRequest, opts ...grpc.CallOption) (*ExportUngroupedItemsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExportUngroupedItemsResponse) + err := c.cc.Invoke(ctx, RMGroupService_ExportUngroupedItems_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) ImportGroupItems(ctx context.Context, in *ImportGroupItemsRequest, opts ...grpc.CallOption) (*ImportGroupItemsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ImportGroupItemsResponse) + err := c.cc.Invoke(ctx, RMGroupService_ImportGroupItems_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rMGroupServiceClient) DownloadGroupItemsTemplate(ctx context.Context, in *DownloadGroupItemsTemplateRequest, opts ...grpc.CallOption) (*DownloadGroupItemsTemplateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DownloadGroupItemsTemplateResponse) + err := c.cc.Invoke(ctx, RMGroupService_DownloadGroupItemsTemplate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// RMGroupServiceServer is the server API for RMGroupService service. +// All implementations must embed UnimplementedRMGroupServiceServer +// for forward compatibility. +// +// RMGroupService manages RM group heads + their item memberships and exposes +// the "ungrouped items" report that drives the grouping workflow. +type RMGroupServiceServer interface { + // CreateRMGroup creates a new RM group head. + CreateRMGroup(context.Context, *CreateRMGroupRequest) (*CreateRMGroupResponse, error) + // GetRMGroup retrieves a group head + its details. + GetRMGroup(context.Context, *GetRMGroupRequest) (*GetRMGroupResponse, error) + // UpdateRMGroup applies a partial update to a group head. + UpdateRMGroup(context.Context, *UpdateRMGroupRequest) (*UpdateRMGroupResponse, error) + // DeleteRMGroup soft-deletes a group head (cascade to its details). + DeleteRMGroup(context.Context, *DeleteRMGroupRequest) (*DeleteRMGroupResponse, error) + // ListRMGroups lists group heads with search + filter + pagination. + ListRMGroups(context.Context, *ListRMGroupsRequest) (*ListRMGroupsResponse, error) + // AddItems assigns items to a group. Items already in another active group + // are returned in `skipped` instead of failing the batch. + AddItems(context.Context, *AddItemsRequest) (*AddItemsResponse, error) + // RemoveItems removes details from a group (deactivate or soft-delete). + RemoveItems(context.Context, *RemoveItemsRequest) (*RemoveItemsResponse, error) + // ListUngroupedItems reports items from the sync feed that have no active + // group assignment. + ListUngroupedItems(context.Context, *ListUngroupedItemsRequest) (*ListUngroupedItemsResponse, error) + // GetRMGroupItemRates returns per-item per-stage rates for every active + // detail of a group in a given period. + GetRMGroupItemRates(context.Context, *GetRMGroupItemRatesRequest) (*GetRMGroupItemRatesResponse, error) + // ExportRMGroups exports all groups + their items to a 2-sheet Excel. + ExportRMGroups(context.Context, *ExportRMGroupsRequest) (*ExportRMGroupsResponse, error) + // ImportRMGroups imports groups and/or items from a 2-sheet Excel. Users can + // include only the Groups sheet, only the Items sheet, or both. + ImportRMGroups(context.Context, *ImportRMGroupsRequest) (*ImportRMGroupsResponse, error) + // DownloadRMGroupTemplate returns a blank 2-sheet Excel with header rows. + DownloadRMGroupTemplate(context.Context, *DownloadRMGroupTemplateRequest) (*DownloadRMGroupTemplateResponse, error) + // ExportUngroupedItems exports ungrouped items matching the filter to Excel. + ExportUngroupedItems(context.Context, *ExportUngroupedItemsRequest) (*ExportUngroupedItemsResponse, error) + // ImportGroupItems bulk-assigns items to ONE existing group from a + // one-sheet Excel. Uses the same validation as the interactive AddItems + // flow (one item / one active group, sync-feed lookup, idempotent re-add). + ImportGroupItems(context.Context, *ImportGroupItemsRequest) (*ImportGroupItemsResponse, error) + // DownloadGroupItemsTemplate returns a blank one-sheet Excel with just + // the columns ImportGroupItems expects. Paired with ImportGroupItems. + DownloadGroupItemsTemplate(context.Context, *DownloadGroupItemsTemplateRequest) (*DownloadGroupItemsTemplateResponse, error) + mustEmbedUnimplementedRMGroupServiceServer() +} + +// UnimplementedRMGroupServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedRMGroupServiceServer struct{} + +func (UnimplementedRMGroupServiceServer) CreateRMGroup(context.Context, *CreateRMGroupRequest) (*CreateRMGroupResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateRMGroup not implemented") +} +func (UnimplementedRMGroupServiceServer) GetRMGroup(context.Context, *GetRMGroupRequest) (*GetRMGroupResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetRMGroup not implemented") +} +func (UnimplementedRMGroupServiceServer) UpdateRMGroup(context.Context, *UpdateRMGroupRequest) (*UpdateRMGroupResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateRMGroup not implemented") +} +func (UnimplementedRMGroupServiceServer) DeleteRMGroup(context.Context, *DeleteRMGroupRequest) (*DeleteRMGroupResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteRMGroup not implemented") +} +func (UnimplementedRMGroupServiceServer) ListRMGroups(context.Context, *ListRMGroupsRequest) (*ListRMGroupsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRMGroups not implemented") +} +func (UnimplementedRMGroupServiceServer) AddItems(context.Context, *AddItemsRequest) (*AddItemsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AddItems not implemented") +} +func (UnimplementedRMGroupServiceServer) RemoveItems(context.Context, *RemoveItemsRequest) (*RemoveItemsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveItems not implemented") +} +func (UnimplementedRMGroupServiceServer) ListUngroupedItems(context.Context, *ListUngroupedItemsRequest) (*ListUngroupedItemsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListUngroupedItems not implemented") +} +func (UnimplementedRMGroupServiceServer) GetRMGroupItemRates(context.Context, *GetRMGroupItemRatesRequest) (*GetRMGroupItemRatesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetRMGroupItemRates not implemented") +} +func (UnimplementedRMGroupServiceServer) ExportRMGroups(context.Context, *ExportRMGroupsRequest) (*ExportRMGroupsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ExportRMGroups not implemented") +} +func (UnimplementedRMGroupServiceServer) ImportRMGroups(context.Context, *ImportRMGroupsRequest) (*ImportRMGroupsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ImportRMGroups not implemented") +} +func (UnimplementedRMGroupServiceServer) DownloadRMGroupTemplate(context.Context, *DownloadRMGroupTemplateRequest) (*DownloadRMGroupTemplateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DownloadRMGroupTemplate not implemented") +} +func (UnimplementedRMGroupServiceServer) ExportUngroupedItems(context.Context, *ExportUngroupedItemsRequest) (*ExportUngroupedItemsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ExportUngroupedItems not implemented") +} +func (UnimplementedRMGroupServiceServer) ImportGroupItems(context.Context, *ImportGroupItemsRequest) (*ImportGroupItemsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ImportGroupItems not implemented") +} +func (UnimplementedRMGroupServiceServer) DownloadGroupItemsTemplate(context.Context, *DownloadGroupItemsTemplateRequest) (*DownloadGroupItemsTemplateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DownloadGroupItemsTemplate not implemented") +} +func (UnimplementedRMGroupServiceServer) mustEmbedUnimplementedRMGroupServiceServer() {} +func (UnimplementedRMGroupServiceServer) testEmbeddedByValue() {} + +// UnsafeRMGroupServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to RMGroupServiceServer will +// result in compilation errors. +type UnsafeRMGroupServiceServer interface { + mustEmbedUnimplementedRMGroupServiceServer() +} + +func RegisterRMGroupServiceServer(s grpc.ServiceRegistrar, srv RMGroupServiceServer) { + // If the following call panics, it indicates UnimplementedRMGroupServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&RMGroupService_ServiceDesc, srv) +} + +func _RMGroupService_CreateRMGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateRMGroupRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).CreateRMGroup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_CreateRMGroup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).CreateRMGroup(ctx, req.(*CreateRMGroupRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_GetRMGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRMGroupRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).GetRMGroup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_GetRMGroup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).GetRMGroup(ctx, req.(*GetRMGroupRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_UpdateRMGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateRMGroupRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).UpdateRMGroup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_UpdateRMGroup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).UpdateRMGroup(ctx, req.(*UpdateRMGroupRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_DeleteRMGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteRMGroupRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).DeleteRMGroup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_DeleteRMGroup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).DeleteRMGroup(ctx, req.(*DeleteRMGroupRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_ListRMGroups_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRMGroupsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).ListRMGroups(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_ListRMGroups_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).ListRMGroups(ctx, req.(*ListRMGroupsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_AddItems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddItemsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).AddItems(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_AddItems_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).AddItems(ctx, req.(*AddItemsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_RemoveItems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveItemsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).RemoveItems(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_RemoveItems_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).RemoveItems(ctx, req.(*RemoveItemsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_ListUngroupedItems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListUngroupedItemsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).ListUngroupedItems(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_ListUngroupedItems_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).ListUngroupedItems(ctx, req.(*ListUngroupedItemsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_GetRMGroupItemRates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRMGroupItemRatesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).GetRMGroupItemRates(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_GetRMGroupItemRates_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).GetRMGroupItemRates(ctx, req.(*GetRMGroupItemRatesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_ExportRMGroups_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExportRMGroupsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).ExportRMGroups(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_ExportRMGroups_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).ExportRMGroups(ctx, req.(*ExportRMGroupsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_ImportRMGroups_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ImportRMGroupsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).ImportRMGroups(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_ImportRMGroups_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).ImportRMGroups(ctx, req.(*ImportRMGroupsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_DownloadRMGroupTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DownloadRMGroupTemplateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).DownloadRMGroupTemplate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_DownloadRMGroupTemplate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).DownloadRMGroupTemplate(ctx, req.(*DownloadRMGroupTemplateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_ExportUngroupedItems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExportUngroupedItemsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).ExportUngroupedItems(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_ExportUngroupedItems_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).ExportUngroupedItems(ctx, req.(*ExportUngroupedItemsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_ImportGroupItems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ImportGroupItemsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).ImportGroupItems(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_ImportGroupItems_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).ImportGroupItems(ctx, req.(*ImportGroupItemsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RMGroupService_DownloadGroupItemsTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DownloadGroupItemsTemplateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RMGroupServiceServer).DownloadGroupItemsTemplate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RMGroupService_DownloadGroupItemsTemplate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RMGroupServiceServer).DownloadGroupItemsTemplate(ctx, req.(*DownloadGroupItemsTemplateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// RMGroupService_ServiceDesc is the grpc.ServiceDesc for RMGroupService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var RMGroupService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "finance.v1.RMGroupService", + HandlerType: (*RMGroupServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateRMGroup", + Handler: _RMGroupService_CreateRMGroup_Handler, + }, + { + MethodName: "GetRMGroup", + Handler: _RMGroupService_GetRMGroup_Handler, + }, + { + MethodName: "UpdateRMGroup", + Handler: _RMGroupService_UpdateRMGroup_Handler, + }, + { + MethodName: "DeleteRMGroup", + Handler: _RMGroupService_DeleteRMGroup_Handler, + }, + { + MethodName: "ListRMGroups", + Handler: _RMGroupService_ListRMGroups_Handler, + }, + { + MethodName: "AddItems", + Handler: _RMGroupService_AddItems_Handler, + }, + { + MethodName: "RemoveItems", + Handler: _RMGroupService_RemoveItems_Handler, + }, + { + MethodName: "ListUngroupedItems", + Handler: _RMGroupService_ListUngroupedItems_Handler, + }, + { + MethodName: "GetRMGroupItemRates", + Handler: _RMGroupService_GetRMGroupItemRates_Handler, + }, + { + MethodName: "ExportRMGroups", + Handler: _RMGroupService_ExportRMGroups_Handler, + }, + { + MethodName: "ImportRMGroups", + Handler: _RMGroupService_ImportRMGroups_Handler, + }, + { + MethodName: "DownloadRMGroupTemplate", + Handler: _RMGroupService_DownloadRMGroupTemplate_Handler, + }, + { + MethodName: "ExportUngroupedItems", + Handler: _RMGroupService_ExportUngroupedItems_Handler, + }, + { + MethodName: "ImportGroupItems", + Handler: _RMGroupService_ImportGroupItems_Handler, + }, + { + MethodName: "DownloadGroupItemsTemplate", + Handler: _RMGroupService_DownloadGroupItemsTemplate_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "finance/v1/rm_group.proto", +} diff --git a/gen/openapi/finance/v1/rm_cost.swagger.json b/gen/openapi/finance/v1/rm_cost.swagger.json new file mode 100644 index 0000000..6c1ca1e --- /dev/null +++ b/gen/openapi/finance/v1/rm_cost.swagger.json @@ -0,0 +1,938 @@ +{ + "swagger": "2.0", + "info": { + "title": "finance/v1/rm_cost.proto", + "version": "version not set" + }, + "tags": [ + { + "name": "RMCostService" + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/api/v1/finance/rm-costs": { + "get": { + "summary": "ListRMCosts lists cost rows with filter + pagination.", + "operationId": "RMCostService_ListRMCosts", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListRMCostsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "page", + "description": "Page number (1-indexed).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageSize", + "description": "Page size (1-100).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "period", + "description": "Period filter (YYYYMM). Empty = all periods.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "rmType", + "description": "Filter by RM type. UNSPECIFIED = all.\n\n - RM_COST_TYPE_UNSPECIFIED: Default zero value. Means \"no filter\" in list requests.\n - RM_COST_TYPE_GROUP: Cost row aggregates a whole RM group.\n - RM_COST_TYPE_ITEM: Cost row is computed for a single item (future phase).", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "RM_COST_TYPE_UNSPECIFIED", + "RM_COST_TYPE_GROUP", + "RM_COST_TYPE_ITEM" + ], + "default": "RM_COST_TYPE_UNSPECIFIED" + }, + { + "name": "groupHeadId", + "description": "Optional group scope.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "search", + "description": "Free-text search on rm_code + rm_name.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "sortBy", + "description": "Sort field.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "sortOrder", + "description": "Sort order.", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "RMCostService" + ] + } + }, + "/api/v1/finance/rm-costs/calculate": { + "post": { + "summary": "CalculateRMCost runs a recalculation synchronously (admin-only).", + "operationId": "RMCostService_CalculateRMCost", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1CalculateRMCostResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "CalculateRMCost runs a calculation synchronously and returns the produced rows.\nIntended for admin/troubleshooting use — production traffic should go through\nTriggerRMCostCalculation.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1CalculateRMCostRequest" + } + } + ], + "tags": [ + "RMCostService" + ] + } + }, + "/api/v1/finance/rm-costs/export": { + "get": { + "summary": "ExportRMCosts exports cost rows matching the filter to a single-sheet Excel.", + "operationId": "RMCostService_ExportRMCosts", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ExportRMCostsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "period", + "description": "Period filter (YYYYMM). Empty = all periods.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "rmType", + "description": "Filter by RM type. UNSPECIFIED = all.\n\n - RM_COST_TYPE_UNSPECIFIED: Default zero value. Means \"no filter\" in list requests.\n - RM_COST_TYPE_GROUP: Cost row aggregates a whole RM group.\n - RM_COST_TYPE_ITEM: Cost row is computed for a single item (future phase).", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "RM_COST_TYPE_UNSPECIFIED", + "RM_COST_TYPE_GROUP", + "RM_COST_TYPE_ITEM" + ], + "default": "RM_COST_TYPE_UNSPECIFIED" + }, + { + "name": "groupHeadId", + "description": "Optional group scope.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "search", + "description": "Free-text search on rm_code + rm_name.", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "RMCostService" + ] + } + }, + "/api/v1/finance/rm-costs/history": { + "get": { + "summary": "ListRMCostHistory lists audit-history rows with filter + pagination.", + "operationId": "RMCostService_ListRMCostHistory", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListRMCostHistoryResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "page", + "description": "Page number (1-indexed).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageSize", + "description": "Page size (1-100).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "period", + "description": "Period filter (YYYYMM). Empty = all periods.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "rmCode", + "description": "RM code filter (empty = all).", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "groupHeadId", + "description": "Optional group scope.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "jobId", + "description": "Optional job scope.", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "RMCostService" + ] + } + }, + "/api/v1/finance/rm-costs/periods": { + "get": { + "summary": "ListRMCostPeriods returns distinct periods from cost rows (newest first).", + "operationId": "RMCostService_ListRMCostPeriods", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListRMCostPeriodsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "RMCostService" + ] + } + }, + "/api/v1/finance/rm-costs/trigger": { + "post": { + "summary": "TriggerRMCostCalculation enqueues an async recalculation job.", + "operationId": "RMCostService_TriggerRMCostCalculation", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1TriggerRMCostCalculationResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "TriggerRMCostCalculation enqueues a recalculation job. Returns immediately\nwith a job ID the caller can poll via the job-execution endpoint.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1TriggerRMCostCalculationRequest" + } + } + ], + "tags": [ + "RMCostService" + ] + } + }, + "/api/v1/finance/rm-costs/{period}/{rmCode}": { + "get": { + "summary": "GetRMCost fetches a single cost row by (period, rm_code).", + "operationId": "RMCostService_GetRMCost", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetRMCostResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "period", + "description": "Period (YYYYMM).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "rmCode", + "description": "RM code (group or item).", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "RMCostService" + ] + } + } + }, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "v1AuditInfo": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "description": "Timestamp when the record was created (ISO 8601)." + }, + "createdBy": { + "type": "string", + "description": "User who created the record." + }, + "updatedAt": { + "type": "string", + "description": "Timestamp when the record was last updated (ISO 8601)." + }, + "updatedBy": { + "type": "string", + "description": "User who last updated the record." + } + }, + "description": "AuditInfo contains audit trail information." + }, + "v1BaseResponse": { + "type": "object", + "properties": { + "validationErrors": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ValidationError" + }, + "description": "List of validation errors if any." + }, + "statusCode": { + "type": "string", + "description": "HTTP-like status code (e.g., \"200\", \"400\", \"404\", \"500\")." + }, + "isSuccess": { + "type": "boolean", + "description": "Indicates if the operation was successful." + }, + "message": { + "type": "string", + "description": "Human-readable message describing the result." + } + }, + "description": "BaseResponse is the standard response wrapper for all API responses." + }, + "v1CalculateRMCostRequest": { + "type": "object", + "properties": { + "period": { + "type": "string", + "description": "Period (YYYYMM)." + }, + "groupHeadId": { + "type": "string", + "description": "Optional single-group scope." + }, + "triggerReason": { + "$ref": "#/definitions/v1RMCostTriggerReason", + "description": "Why this calc was run." + } + }, + "description": "CalculateRMCost runs a calculation synchronously and returns the produced rows.\nIntended for admin/troubleshooting use — production traffic should go through\nTriggerRMCostCalculation." + }, + "v1CalculateRMCostResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "processed": { + "type": "integer", + "format": "int32", + "description": "Number of group heads processed." + }, + "skipped": { + "type": "integer", + "format": "int32", + "description": "Number of group heads skipped (no active details / no source rows)." + }, + "period": { + "type": "string", + "description": "Period that was calculated (echoes request)." + } + }, + "description": "Calculate response." + }, + "v1ExportRMCostsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "fileContent": { + "type": "string", + "format": "byte", + "description": "Excel file content (.xlsx)." + }, + "fileName": { + "type": "string", + "description": "Suggested filename." + } + }, + "description": "ExportRMCostsResponse carries the Excel bytes + filename." + }, + "v1GetRMCostResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "$ref": "#/definitions/v1RMCost", + "description": "Cost data." + } + }, + "description": "Get response." + }, + "v1ListRMCostHistoryResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1RMCostHistory" + }, + "description": "History rows ordered by calculated_at DESC." + }, + "pagination": { + "$ref": "#/definitions/v1PaginationResponse", + "description": "Pagination metadata." + } + }, + "description": "History response." + }, + "v1ListRMCostPeriodsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "periods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Distinct periods ordered DESC (newest first), YYYYMM strings." + } + }, + "description": "Response carries the distinct set of calculated periods." + }, + "v1ListRMCostsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1RMCost" + }, + "description": "Cost rows." + }, + "pagination": { + "$ref": "#/definitions/v1PaginationResponse", + "description": "Pagination metadata." + } + }, + "description": "List response." + }, + "v1PaginationResponse": { + "type": "object", + "properties": { + "currentPage": { + "type": "integer", + "format": "int32", + "description": "Current page number." + }, + "pageSize": { + "type": "integer", + "format": "int32", + "description": "Number of items per page." + }, + "totalItems": { + "type": "string", + "format": "int64", + "description": "Total number of items across all pages." + }, + "totalPages": { + "type": "integer", + "format": "int32", + "description": "Total number of pages." + } + }, + "description": "PaginationResponse contains pagination metadata for list responses." + }, + "v1RMCost": { + "type": "object", + "properties": { + "rmCostId": { + "type": "string", + "description": "Cost row UUID." + }, + "period": { + "type": "string", + "description": "Period (YYYYMM)." + }, + "rmCode": { + "type": "string", + "description": "Group code (rm_type=GROUP) or item code (rm_type=ITEM)." + }, + "rmType": { + "$ref": "#/definitions/v1RMCostType", + "description": "Discriminator." + }, + "groupHeadId": { + "type": "string", + "description": "Owning group head UUID (set when rm_type=GROUP)." + }, + "itemCode": { + "type": "string", + "description": "Item code (set when rm_type=ITEM)." + }, + "rmName": { + "type": "string", + "description": "Display name of the RM (group or item)." + }, + "uomCode": { + "type": "string", + "description": "UOM code." + }, + "rates": { + "$ref": "#/definitions/v1RMCostRates", + "description": "Per-stage rate snapshot." + }, + "costValuation": { + "type": "number", + "format": "double", + "description": "Computed valuation landed cost (nil when never calculated)." + }, + "costMarketing": { + "type": "number", + "format": "double", + "description": "Computed marketing landed cost." + }, + "costSimulation": { + "type": "number", + "format": "double", + "description": "Computed simulation landed cost." + }, + "flagValuation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Flags configured on the group header at calc time." + }, + "flagMarketing": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Flag configured on the group header at calc time." + }, + "flagSimulation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Flag configured on the group header at calc time." + }, + "flagValuationUsed": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stage actually used after cascade / INIT resolution." + }, + "flagMarketingUsed": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stage actually used after cascade / INIT resolution." + }, + "flagSimulationUsed": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stage actually used after cascade / INIT resolution." + }, + "calculatedAt": { + "type": "string", + "description": "Last calculation timestamp (RFC3339, empty when never calculated)." + }, + "calculatedBy": { + "type": "string", + "description": "Last calculator (empty when never calculated)." + }, + "audit": { + "$ref": "#/definitions/v1AuditInfo", + "description": "Audit metadata." + } + }, + "description": "RMCost is the landed cost computed for a single (period, rm_code) pair." + }, + "v1RMCostHistory": { + "type": "object", + "properties": { + "historyId": { + "type": "string", + "description": "History row UUID." + }, + "rmCostId": { + "type": "string", + "description": "Cost row this history refers to (nil if the cost row was later deleted)." + }, + "jobId": { + "type": "string", + "description": "Job that produced this history row (nil when no job context)." + }, + "period": { + "type": "string", + "description": "Period." + }, + "rmCode": { + "type": "string", + "description": "RM code." + }, + "rmType": { + "$ref": "#/definitions/v1RMCostType", + "description": "Discriminator." + }, + "groupHeadId": { + "type": "string", + "description": "Owning group head UUID (when rm_type=GROUP)." + }, + "rates": { + "$ref": "#/definitions/v1RMCostRates", + "description": "Rates captured for this calc pass." + }, + "costPercentage": { + "type": "number", + "format": "double", + "description": "Snapshot of head.cost_percentage at calc time." + }, + "costPerKg": { + "type": "number", + "format": "double", + "description": "Snapshot of head.cost_per_kg at calc time." + }, + "flagValuation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Configured flags at calc time." + }, + "flagMarketing": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Configured flags at calc time." + }, + "flagSimulation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Configured flags at calc time." + }, + "initValValuation": { + "type": "number", + "format": "double", + "description": "Init-value overrides at calc time." + }, + "initValMarketing": { + "type": "number", + "format": "double", + "description": "Init-value overrides at calc time." + }, + "initValSimulation": { + "type": "number", + "format": "double", + "description": "Init-value overrides at calc time." + }, + "costValuation": { + "type": "number", + "format": "double", + "description": "Computed costs at calc time." + }, + "costMarketing": { + "type": "number", + "format": "double", + "description": "Computed costs at calc time." + }, + "costSimulation": { + "type": "number", + "format": "double", + "description": "Computed costs at calc time." + }, + "flagValuationUsed": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stages resolved at calc time." + }, + "flagMarketingUsed": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stages resolved at calc time." + }, + "flagSimulationUsed": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stages resolved at calc time." + }, + "sourceItemCount": { + "type": "integer", + "format": "int32", + "description": "Count of source items aggregated." + }, + "triggerReason": { + "$ref": "#/definitions/v1RMCostTriggerReason", + "description": "Why this calc was triggered." + }, + "calculatedAt": { + "type": "string", + "description": "When the calc ran (RFC3339)." + }, + "calculatedBy": { + "type": "string", + "description": "Who ran the calc." + } + }, + "description": "RMCostHistory is one row of the append-only audit trail written alongside every\ncalculation pass." + }, + "v1RMCostRates": { + "type": "object", + "properties": { + "cons": { + "type": "number", + "format": "double", + "description": "CONS stage rate." + }, + "stores": { + "type": "number", + "format": "double", + "description": "STORES stage rate." + }, + "dept": { + "type": "number", + "format": "double", + "description": "DEPT stage rate." + }, + "po1": { + "type": "number", + "format": "double", + "description": "First PO stage rate." + }, + "po2": { + "type": "number", + "format": "double", + "description": "Second PO stage rate." + }, + "po3": { + "type": "number", + "format": "double", + "description": "Third PO stage rate." + } + }, + "description": "RMCostRates is the per-stage snapshot of aggregated rates captured at calc time." + }, + "v1RMCostTriggerReason": { + "type": "string", + "enum": [ + "RM_COST_TRIGGER_REASON_UNSPECIFIED", + "RM_COST_TRIGGER_REASON_ORACLE_SYNC_CHAIN", + "RM_COST_TRIGGER_REASON_GROUP_UPDATE", + "RM_COST_TRIGGER_REASON_DETAIL_CHANGE", + "RM_COST_TRIGGER_REASON_MANUAL_UI" + ], + "default": "RM_COST_TRIGGER_REASON_UNSPECIFIED", + "description": "RMCostTriggerReason enumerates the reasons a calculation was run. Mirrors\n`rmcost.HistoryTriggerReason`.\n\n - RM_COST_TRIGGER_REASON_UNSPECIFIED: Default zero value. Rejected on trigger requests via not_in: [0].\n - RM_COST_TRIGGER_REASON_ORACLE_SYNC_CHAIN: Auto-chained after a successful Oracle sync for the synced period.\n - RM_COST_TRIGGER_REASON_GROUP_UPDATE: Group header changed.\n - RM_COST_TRIGGER_REASON_DETAIL_CHANGE: An item was added/removed/toggled in the group.\n - RM_COST_TRIGGER_REASON_MANUAL_UI: Explicit user request from the UI." + }, + "v1RMCostType": { + "type": "string", + "enum": [ + "RM_COST_TYPE_UNSPECIFIED", + "RM_COST_TYPE_GROUP", + "RM_COST_TYPE_ITEM" + ], + "default": "RM_COST_TYPE_UNSPECIFIED", + "description": "RMCostType discriminates a group-level cost row from an item-level cost row.\n\n - RM_COST_TYPE_UNSPECIFIED: Default zero value. Means \"no filter\" in list requests.\n - RM_COST_TYPE_GROUP: Cost row aggregates a whole RM group.\n - RM_COST_TYPE_ITEM: Cost row is computed for a single item (future phase)." + }, + "v1RMGroupFlag": { + "type": "string", + "enum": [ + "RM_GROUP_FLAG_UNSPECIFIED", + "RM_GROUP_FLAG_INIT", + "RM_GROUP_FLAG_CONS", + "RM_GROUP_FLAG_STORES", + "RM_GROUP_FLAG_DEPT", + "RM_GROUP_FLAG_PO_1", + "RM_GROUP_FLAG_PO_2", + "RM_GROUP_FLAG_PO_3" + ], + "default": "RM_GROUP_FLAG_UNSPECIFIED", + "description": "RMGroupFlag identifies which aggregated stage rate feeds a cost purpose\n(valuation / marketing / simulation), or whether the group uses its\nconfigured init override value. Mirrors the `cst_rm_group_head.flag_*`\ndomain constraint.\n\n - RM_GROUP_FLAG_UNSPECIFIED: Default zero value. Rejected on create/update requests via not_in: [0].\n - RM_GROUP_FLAG_INIT: Use the init_val_* override on the head; skips cascade.\n - RM_GROUP_FLAG_CONS: Use the CONS stage rate.\n - RM_GROUP_FLAG_STORES: Use the STORES stage rate.\n - RM_GROUP_FLAG_DEPT: Use the DEPT stage rate.\n - RM_GROUP_FLAG_PO_1: Use the first PO stage rate.\n - RM_GROUP_FLAG_PO_2: Use the second PO stage rate.\n - RM_GROUP_FLAG_PO_3: Use the third PO stage rate." + }, + "v1TriggerRMCostCalculationRequest": { + "type": "object", + "properties": { + "period": { + "type": "string", + "description": "Period to recalculate (YYYYMM)." + }, + "groupHeadId": { + "type": "string", + "description": "When set, scope the calc to a single group. Empty = all active groups." + }, + "triggerReason": { + "$ref": "#/definitions/v1RMCostTriggerReason", + "description": "Why the recalc was requested." + } + }, + "description": "TriggerRMCostCalculation enqueues a recalculation job. Returns immediately\nwith a job ID the caller can poll via the job-execution endpoint." + }, + "v1TriggerRMCostCalculationResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "jobId": { + "type": "string", + "description": "Enqueued job UUID." + } + }, + "description": "Trigger response." + }, + "v1ValidationError": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "The field name that failed validation." + }, + "message": { + "type": "string", + "description": "The validation error message." + } + }, + "description": "ValidationError represents a single field validation error." + } + } +} diff --git a/gen/openapi/finance/v1/rm_group.swagger.json b/gen/openapi/finance/v1/rm_group.swagger.json new file mode 100644 index 0000000..b75810a --- /dev/null +++ b/gen/openapi/finance/v1/rm_group.swagger.json @@ -0,0 +1,1707 @@ +{ + "swagger": "2.0", + "info": { + "title": "finance/v1/rm_group.proto", + "version": "version not set" + }, + "tags": [ + { + "name": "RMGroupService" + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/api/v1/finance/rm-groups": { + "get": { + "summary": "ListRMGroups lists group heads with search + filter + pagination.", + "operationId": "RMGroupService_ListRMGroups", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListRMGroupsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "page", + "description": "Page number (1-indexed).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageSize", + "description": "Page size (1-100).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "search", + "description": "Free-text search on code, name, description, colourant, ci_name.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "activeFilter", + "description": "Filter by active status.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records.", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "ACTIVE_FILTER_UNSPECIFIED", + "ACTIVE_FILTER_ACTIVE", + "ACTIVE_FILTER_INACTIVE" + ], + "default": "ACTIVE_FILTER_UNSPECIFIED" + }, + { + "name": "sortBy", + "description": "Sort field.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "sortOrder", + "description": "Sort order.", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "RMGroupService" + ] + }, + "post": { + "summary": "CreateRMGroup creates a new RM group head.", + "operationId": "RMGroupService_CreateRMGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1CreateRMGroupResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Create a new RM group head.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1CreateRMGroupRequest" + } + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/export": { + "get": { + "summary": "ExportRMGroups exports all groups + their items to a 2-sheet Excel.", + "operationId": "RMGroupService_ExportRMGroups", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ExportRMGroupsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "activeFilter", + "description": "Filter by active/inactive (UNSPECIFIED = all).\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records.", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "ACTIVE_FILTER_UNSPECIFIED", + "ACTIVE_FILTER_ACTIVE", + "ACTIVE_FILTER_INACTIVE" + ], + "default": "ACTIVE_FILTER_UNSPECIFIED" + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/import": { + "post": { + "summary": "ImportRMGroups imports groups and/or items from a 2-sheet Excel. Users can\ninclude only the Groups sheet, only the Items sheet, or both.", + "operationId": "RMGroupService_ImportRMGroups", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ImportRMGroupsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "ImportRMGroupsRequest accepts a 2-sheet Excel. User may include only the\nGroups sheet (header-only import), only the Items sheet (detail-only for\nexisting groups), or both.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1ImportRMGroupsRequest" + } + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/items/template": { + "get": { + "summary": "DownloadGroupItemsTemplate returns a blank one-sheet Excel with just\nthe columns ImportGroupItems expects. Paired with ImportGroupItems.", + "operationId": "RMGroupService_DownloadGroupItemsTemplate", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1DownloadGroupItemsTemplateResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/template": { + "get": { + "summary": "DownloadRMGroupTemplate returns a blank 2-sheet Excel with header rows.", + "operationId": "RMGroupService_DownloadRMGroupTemplate", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1DownloadRMGroupTemplateResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/ungrouped": { + "get": { + "summary": "ListUngroupedItems reports items from the sync feed that have no active\ngroup assignment.", + "operationId": "RMGroupService_ListUngroupedItems", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListUngroupedItemsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "page", + "description": "Page number (1-indexed).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageSize", + "description": "Page size (1-100).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "period", + "description": "Period filter (YYYYMM). Empty = all periods.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "search", + "description": "Free-text search on item_code, item_name, item_type_code, grade_code.", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/ungrouped/export": { + "get": { + "summary": "ExportUngroupedItems exports ungrouped items matching the filter to Excel.", + "operationId": "RMGroupService_ExportUngroupedItems", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ExportUngroupedItemsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "period", + "description": "Period filter (YYYYMM). Empty = all periods.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "search", + "description": "Free-text search on item_code, item_name, item_type_code, grade_code.", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/{groupHeadId}": { + "get": { + "summary": "GetRMGroup retrieves a group head + its details.", + "operationId": "RMGroupService_GetRMGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetRMGroupResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "groupHeadId", + "description": "Head UUID.", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "RMGroupService" + ] + }, + "delete": { + "summary": "DeleteRMGroup soft-deletes a group head (cascade to its details).", + "operationId": "RMGroupService_DeleteRMGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1DeleteRMGroupResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "groupHeadId", + "description": "Head UUID.", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "RMGroupService" + ] + }, + "put": { + "summary": "UpdateRMGroup applies a partial update to a group head.", + "operationId": "RMGroupService_UpdateRMGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1UpdateRMGroupResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "groupHeadId", + "description": "Head UUID to update.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RMGroupServiceUpdateRMGroupBody" + } + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/{groupHeadId}/item-rates": { + "get": { + "summary": "GetRMGroupItemRates returns per-item per-stage rates for every active\ndetail of a group in a given period.", + "operationId": "RMGroupService_GetRMGroupItemRates", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetRMGroupItemRatesResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "groupHeadId", + "description": "Group head UUID.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "period", + "description": "Period (YYYYMM).", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/{groupHeadId}/items": { + "post": { + "summary": "AddItems assigns items to a group. Items already in another active group\nare returned in `skipped` instead of failing the batch.", + "operationId": "RMGroupService_AddItems", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1AddItemsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "groupHeadId", + "description": "Target group head UUID.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RMGroupServiceAddItemsBody" + } + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/{groupHeadId}/items/import": { + "post": { + "summary": "ImportGroupItems bulk-assigns items to ONE existing group from a\none-sheet Excel. Uses the same validation as the interactive AddItems\nflow (one item / one active group, sync-feed lookup, idempotent re-add).", + "operationId": "RMGroupService_ImportGroupItems", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ImportGroupItemsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "groupHeadId", + "description": "Target group head ID (UUID).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RMGroupServiceImportGroupItemsBody" + } + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/{groupHeadId}/items/remove": { + "post": { + "summary": "RemoveItems removes details from a group (deactivate or soft-delete).", + "operationId": "RMGroupService_RemoveItems", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1RemoveItemsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "groupHeadId", + "description": "Owning group head UUID (validated against each detail).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RMGroupServiceRemoveItemsBody" + } + } + ], + "tags": [ + "RMGroupService" + ] + } + } + }, + "definitions": { + "RMGroupServiceAddItemsBody": { + "type": "object", + "properties": { + "itemCodes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Item codes to add (legacy: grade_code is resolved server-side)." + }, + "selections": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1AddItemSelection" + }, + "description": "Structured selections carrying grade_code. Preferred over item_codes." + } + }, + "description": "Add one or more items to a group. Items already in another active group are\nreturned in `skipped` rather than erroring the whole batch.\n\nPrefer `selections` — it carries grade_code which is required to correctly\ndisambiguate multi-variant items. `item_codes` is retained for backwards\ncompatibility; when both are supplied, `selections` wins." + }, + "RMGroupServiceImportGroupItemsBody": { + "type": "object", + "properties": { + "fileContent": { + "type": "string", + "format": "byte", + "description": "Excel file content (.xlsx / .xls). Expected \"Items\" sheet with columns\nitem_code (required), grade_code (optional), sort_order (optional)." + }, + "fileName": { + "type": "string", + "description": "Original filename (format detected by extension)." + } + }, + "description": "ImportGroupItemsRequest bulk-adds items to a specific existing group." + }, + "RMGroupServiceRemoveItemsBody": { + "type": "object", + "properties": { + "groupDetailIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Detail UUIDs to remove." + }, + "mode": { + "$ref": "#/definitions/v1RemoveItemsMode", + "description": "Disposition mode." + } + }, + "description": "Remove items from a group by detail IDs. The mode controls whether the rows\nare deactivated (preserved for history) or soft-deleted." + }, + "RMGroupServiceUpdateRMGroupBody": { + "type": "object", + "properties": { + "groupName": { + "type": "string", + "description": "New display name." + }, + "description": { + "type": "string", + "description": "New description." + }, + "colourant": { + "type": "string", + "description": "New colourant tag." + }, + "ciName": { + "type": "string", + "description": "New CI name tag." + }, + "costPercentage": { + "type": "number", + "format": "double", + "description": "New cost percentage (\u003e= 0)." + }, + "costPerKg": { + "type": "number", + "format": "double", + "description": "New per-kg overhead (\u003e= 0)." + }, + "flagValuation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "New valuation flag." + }, + "flagMarketing": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "New marketing flag." + }, + "flagSimulation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "New simulation flag." + }, + "initValValuation": { + "type": "number", + "format": "double", + "description": "New init-val override for valuation (\u003e= 0)." + }, + "initValMarketing": { + "type": "number", + "format": "double", + "description": "New init-val override for marketing (\u003e= 0)." + }, + "initValSimulation": { + "type": "number", + "format": "double", + "description": "New init-val override for simulation (\u003e= 0)." + }, + "isActive": { + "type": "boolean", + "description": "New active status." + }, + "clearInitValValuation": { + "type": "boolean", + "description": "When true, force init_val_valuation to NULL (overrides init_val_valuation field)." + }, + "clearInitValMarketing": { + "type": "boolean", + "description": "When true, force init_val_marketing to NULL." + }, + "clearInitValSimulation": { + "type": "boolean", + "description": "When true, force init_val_simulation to NULL." + } + }, + "description": "Partial update of an RM group head. Absent fields leave state unchanged.\nClear_* booleans explicitly set nullable fields to NULL." + }, + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "v1ActiveFilter": { + "type": "string", + "enum": [ + "ACTIVE_FILTER_UNSPECIFIED", + "ACTIVE_FILTER_ACTIVE", + "ACTIVE_FILTER_INACTIVE" + ], + "default": "ACTIVE_FILTER_UNSPECIFIED", + "description": "ActiveFilter represents filter options for is_active field.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records." + }, + "v1AddItemSelection": { + "type": "object", + "properties": { + "itemCode": { + "type": "string", + "description": "Item code (required)." + }, + "gradeCode": { + "type": "string", + "description": "Grade code (optional). Empty matches the variant with NULL / empty\ngrade_code in the sync feed. Use \"\" when the row has a single variant." + } + }, + "description": "AddItemSelection identifies one RM variant to assign to a group. The\n(item_code, grade_code) pair is the natural key: an item_code with\nmultiple grade_code variants in the sync feed represents distinct\nrows that can be grouped independently." + }, + "v1AddItemsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "added": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1RMGroupDetail" + }, + "description": "Details created by this call." + }, + "skipped": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1SkippedItem" + }, + "description": "Items that were skipped because they already belong to another active group." + } + }, + "description": "Add items response." + }, + "v1AuditInfo": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "description": "Timestamp when the record was created (ISO 8601)." + }, + "createdBy": { + "type": "string", + "description": "User who created the record." + }, + "updatedAt": { + "type": "string", + "description": "Timestamp when the record was last updated (ISO 8601)." + }, + "updatedBy": { + "type": "string", + "description": "User who last updated the record." + } + }, + "description": "AuditInfo contains audit trail information." + }, + "v1BaseResponse": { + "type": "object", + "properties": { + "validationErrors": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ValidationError" + }, + "description": "List of validation errors if any." + }, + "statusCode": { + "type": "string", + "description": "HTTP-like status code (e.g., \"200\", \"400\", \"404\", \"500\")." + }, + "isSuccess": { + "type": "boolean", + "description": "Indicates if the operation was successful." + }, + "message": { + "type": "string", + "description": "Human-readable message describing the result." + } + }, + "description": "BaseResponse is the standard response wrapper for all API responses." + }, + "v1CreateRMGroupRequest": { + "type": "object", + "properties": { + "groupCode": { + "type": "string", + "description": "Group code (uppercase; spaces + hyphens allowed; 1-30 chars)." + }, + "groupName": { + "type": "string", + "description": "Display name (1-200 chars)." + }, + "description": { + "type": "string", + "description": "Optional description (max 1000 chars)." + }, + "colourant": { + "type": "string", + "description": "Optional colourant tag." + }, + "ciName": { + "type": "string", + "description": "Optional CI name tag." + }, + "costPercentage": { + "type": "number", + "format": "double", + "description": "Cost percentage multiplier (\u003e= 0)." + }, + "costPerKg": { + "type": "number", + "format": "double", + "description": "Per-kg overhead (\u003e= 0)." + } + }, + "description": "Create a new RM group head." + }, + "v1CreateRMGroupResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "$ref": "#/definitions/v1RMGroupHead", + "description": "Created head." + } + }, + "description": "Create response." + }, + "v1DeleteRMGroupResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + } + }, + "description": "Delete response." + }, + "v1DownloadGroupItemsTemplateResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "fileContent": { + "type": "string", + "format": "byte", + "description": "Excel file content (.xlsx)." + }, + "fileName": { + "type": "string", + "description": "Suggested filename." + } + }, + "description": "DownloadGroupItemsTemplateResponse carries the blank template bytes." + }, + "v1DownloadRMGroupTemplateResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "fileContent": { + "type": "string", + "format": "byte", + "description": "Excel template file content (.xlsx)." + }, + "fileName": { + "type": "string", + "description": "Suggested filename." + } + }, + "description": "DownloadRMGroupTemplateResponse returns the blank 2-sheet template." + }, + "v1ExportRMGroupsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "fileContent": { + "type": "string", + "format": "byte", + "description": "Excel file content (.xlsx)." + }, + "fileName": { + "type": "string", + "description": "Suggested filename." + } + }, + "description": "ExportRMGroupsResponse returns a multi-sheet Excel (Groups + Items)." + }, + "v1ExportUngroupedItemsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "fileContent": { + "type": "string", + "format": "byte", + "description": "Excel file content (.xlsx)." + }, + "fileName": { + "type": "string", + "description": "Suggested filename." + } + }, + "description": "ExportUngroupedItemsResponse carries the Excel bytes + filename." + }, + "v1GetRMGroupItemRatesResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1RMGroupItemRates" + }, + "description": "Per-item rates, one row per active detail." + } + }, + "description": "Group item rates response." + }, + "v1GetRMGroupResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "$ref": "#/definitions/v1RMGroupHeadWithDetails", + "description": "Head + details." + } + }, + "description": "Get response." + }, + "v1ImportError": { + "type": "object", + "properties": { + "rowNumber": { + "type": "integer", + "format": "int32", + "description": "Row number in the Excel file." + }, + "field": { + "type": "string", + "description": "Field that caused the error." + }, + "message": { + "type": "string", + "description": "Error message." + } + }, + "description": "ImportError represents a single import error." + }, + "v1ImportGroupItemsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "itemsAdded": { + "type": "integer", + "format": "int32", + "description": "Number of items successfully added." + }, + "itemsSkipped": { + "type": "integer", + "format": "int32", + "description": "Number of items skipped (already in this or another group, or invalid)." + }, + "failedCount": { + "type": "integer", + "format": "int32", + "description": "Number of rows that failed to parse at the Excel layer." + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ImportError" + }, + "description": "Parse-level errors (bad item_code format, etc)." + }, + "skipped": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1SkippedItem" + }, + "description": "Skip details propagated from AddItems (ownership collisions, etc)." + } + }, + "description": "ImportGroupItemsResponse summarizes the outcome." + }, + "v1ImportRMGroupsRequest": { + "type": "object", + "properties": { + "fileContent": { + "type": "string", + "format": "byte", + "description": "Excel file content (.xlsx / .xls)." + }, + "fileName": { + "type": "string", + "description": "Original filename (format detected by extension)." + }, + "duplicateAction": { + "type": "string", + "description": "How to handle existing groups by `group_code`: \"skip\" (default) or \"update\".\nDetail rows always additive: items already active in ANOTHER group are\nreported as skipped errors; items already in the target group are ignored." + } + }, + "description": "ImportRMGroupsRequest accepts a 2-sheet Excel. User may include only the\nGroups sheet (header-only import), only the Items sheet (detail-only for\nexisting groups), or both." + }, + "v1ImportRMGroupsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "groupsCreated": { + "type": "integer", + "format": "int32", + "description": "Number of group heads created." + }, + "groupsUpdated": { + "type": "integer", + "format": "int32", + "description": "Number of group heads updated (duplicate_action=update)." + }, + "groupsSkipped": { + "type": "integer", + "format": "int32", + "description": "Number of group heads skipped (duplicate_action=skip + existed)." + }, + "itemsAdded": { + "type": "integer", + "format": "int32", + "description": "Number of detail items successfully added." + }, + "itemsSkipped": { + "type": "integer", + "format": "int32", + "description": "Number of detail items skipped (already in target group)." + }, + "failedCount": { + "type": "integer", + "format": "int32", + "description": "Number of failed rows (across both sheets)." + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ImportError" + }, + "description": "Per-row errors (reuses finance.v1.ImportError)." + } + }, + "description": "ImportRMGroupsResponse summarizes the outcome." + }, + "v1ListRMGroupsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1RMGroupHead" + }, + "description": "Group heads." + }, + "pagination": { + "$ref": "#/definitions/v1PaginationResponse", + "description": "Pagination metadata." + } + }, + "description": "List response." + }, + "v1ListUngroupedItemsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1UngroupedItem" + }, + "description": "Ungrouped items for the requested filters." + }, + "pagination": { + "$ref": "#/definitions/v1PaginationResponse", + "description": "Pagination metadata." + } + }, + "description": "Ungrouped items response." + }, + "v1PaginationResponse": { + "type": "object", + "properties": { + "currentPage": { + "type": "integer", + "format": "int32", + "description": "Current page number." + }, + "pageSize": { + "type": "integer", + "format": "int32", + "description": "Number of items per page." + }, + "totalItems": { + "type": "string", + "format": "int64", + "description": "Total number of items across all pages." + }, + "totalPages": { + "type": "integer", + "format": "int32", + "description": "Total number of pages." + } + }, + "description": "PaginationResponse contains pagination metadata for list responses." + }, + "v1RMGroupDetail": { + "type": "object", + "properties": { + "groupDetailId": { + "type": "string", + "description": "Detail UUID." + }, + "groupHeadId": { + "type": "string", + "description": "Owning group head UUID." + }, + "itemCode": { + "type": "string", + "description": "Item code." + }, + "itemName": { + "type": "string", + "description": "Item name (snapshot)." + }, + "itemTypeCode": { + "type": "string", + "description": "Item type code." + }, + "gradeCode": { + "type": "string", + "description": "Grade code." + }, + "itemGrade": { + "type": "string", + "description": "Item grade." + }, + "uomCode": { + "type": "string", + "description": "UOM code." + }, + "marketPercentage": { + "type": "number", + "format": "double", + "description": "Per-item marketing percentage (nil when unset)." + }, + "marketValueRp": { + "type": "number", + "format": "double", + "description": "Per-item marketing value in rupiah (nil when unset)." + }, + "sortOrder": { + "type": "integer", + "format": "int32", + "description": "Display order within the group." + }, + "isActive": { + "type": "boolean", + "description": "Contributes to rate aggregation when true." + }, + "isDummy": { + "type": "boolean", + "description": "Placeholder row (excluded from aggregation regardless of is_active)." + }, + "audit": { + "$ref": "#/definitions/v1AuditInfo", + "description": "Audit metadata." + } + }, + "description": "RMGroupDetail is one item's membership in an RM group." + }, + "v1RMGroupFlag": { + "type": "string", + "enum": [ + "RM_GROUP_FLAG_UNSPECIFIED", + "RM_GROUP_FLAG_INIT", + "RM_GROUP_FLAG_CONS", + "RM_GROUP_FLAG_STORES", + "RM_GROUP_FLAG_DEPT", + "RM_GROUP_FLAG_PO_1", + "RM_GROUP_FLAG_PO_2", + "RM_GROUP_FLAG_PO_3" + ], + "default": "RM_GROUP_FLAG_UNSPECIFIED", + "description": "RMGroupFlag identifies which aggregated stage rate feeds a cost purpose\n(valuation / marketing / simulation), or whether the group uses its\nconfigured init override value. Mirrors the `cst_rm_group_head.flag_*`\ndomain constraint.\n\n - RM_GROUP_FLAG_UNSPECIFIED: Default zero value. Rejected on create/update requests via not_in: [0].\n - RM_GROUP_FLAG_INIT: Use the init_val_* override on the head; skips cascade.\n - RM_GROUP_FLAG_CONS: Use the CONS stage rate.\n - RM_GROUP_FLAG_STORES: Use the STORES stage rate.\n - RM_GROUP_FLAG_DEPT: Use the DEPT stage rate.\n - RM_GROUP_FLAG_PO_1: Use the first PO stage rate.\n - RM_GROUP_FLAG_PO_2: Use the second PO stage rate.\n - RM_GROUP_FLAG_PO_3: Use the third PO stage rate." + }, + "v1RMGroupHead": { + "type": "object", + "properties": { + "groupHeadId": { + "type": "string", + "description": "Group head UUID." + }, + "groupCode": { + "type": "string", + "description": "Unique group code (uppercase; allows spaces + hyphens; 1-30 chars)." + }, + "groupName": { + "type": "string", + "description": "Display name." + }, + "description": { + "type": "string", + "description": "Optional description." + }, + "colourant": { + "type": "string", + "description": "Optional colourant tag." + }, + "ciName": { + "type": "string", + "description": "Optional CI name tag." + }, + "costPercentage": { + "type": "number", + "format": "double", + "description": "Cost percentage multiplier (raw, UI formats for display)." + }, + "costPerKg": { + "type": "number", + "format": "double", + "description": "Per-kg overhead added to landed cost." + }, + "flagValuation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stage flag used for valuation cost." + }, + "flagMarketing": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stage flag used for marketing cost." + }, + "flagSimulation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stage flag used for simulation cost." + }, + "initValValuation": { + "type": "number", + "format": "double", + "description": "Init-value override for valuation (set when flag_valuation = INIT)." + }, + "initValMarketing": { + "type": "number", + "format": "double", + "description": "Init-value override for marketing (set when flag_marketing = INIT)." + }, + "initValSimulation": { + "type": "number", + "format": "double", + "description": "Init-value override for simulation (set when flag_simulation = INIT)." + }, + "isActive": { + "type": "boolean", + "description": "Whether the group is active." + }, + "audit": { + "$ref": "#/definitions/v1AuditInfo", + "description": "Audit metadata." + } + }, + "description": "RMGroupHead is the aggregate root representing an RM group's cost configuration." + }, + "v1RMGroupHeadWithDetails": { + "type": "object", + "properties": { + "head": { + "$ref": "#/definitions/v1RMGroupHead", + "description": "Head data." + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1RMGroupDetail" + }, + "description": "Details owned by the head." + } + }, + "description": "RMGroupHeadWithDetails bundles a head and its details for the Get response." + }, + "v1RMGroupItemRates": { + "type": "object", + "properties": { + "itemCode": { + "type": "string", + "description": "Item code." + }, + "itemName": { + "type": "string", + "description": "Item name (snapshot from detail row)." + }, + "gradeCode": { + "type": "string", + "description": "Grade code." + }, + "itemGrade": { + "type": "string", + "description": "Item grade." + }, + "uomCode": { + "type": "string", + "description": "UOM code." + }, + "isActive": { + "type": "boolean", + "description": "Whether the detail is active." + }, + "isDummy": { + "type": "boolean", + "description": "Whether the detail is a dummy row." + }, + "period": { + "type": "string", + "description": "Period the rates apply to (YYYYMM), empty when no sync row was found." + }, + "consQty": { + "type": "number", + "format": "double", + "description": "CONS qty / value / rate." + }, + "consVal": { + "type": "number", + "format": "double" + }, + "consRate": { + "type": "number", + "format": "double" + }, + "storesQty": { + "type": "number", + "format": "double", + "description": "STORES qty / value / rate." + }, + "storesVal": { + "type": "number", + "format": "double" + }, + "storesRate": { + "type": "number", + "format": "double" + }, + "deptQty": { + "type": "number", + "format": "double", + "description": "DEPT qty / value / rate." + }, + "deptVal": { + "type": "number", + "format": "double" + }, + "deptRate": { + "type": "number", + "format": "double" + }, + "lastPoQty1": { + "type": "number", + "format": "double", + "description": "PO_1 qty / value / rate." + }, + "lastPoVal1": { + "type": "number", + "format": "double" + }, + "lastPoRate1": { + "type": "number", + "format": "double" + }, + "lastPoQty2": { + "type": "number", + "format": "double", + "description": "PO_2 qty / value / rate." + }, + "lastPoVal2": { + "type": "number", + "format": "double" + }, + "lastPoRate2": { + "type": "number", + "format": "double" + }, + "lastPoQty3": { + "type": "number", + "format": "double", + "description": "PO_3 qty / value / rate." + }, + "lastPoVal3": { + "type": "number", + "format": "double" + }, + "lastPoRate3": { + "type": "number", + "format": "double" + } + }, + "description": "RMGroupItemRates is one row per item currently in a group with its per-stage\nOracle sync rates for a given period. Items with no sync row for the period\nstill appear (all rates 0)." + }, + "v1RemoveItemsMode": { + "type": "string", + "enum": [ + "REMOVE_ITEMS_MODE_UNSPECIFIED", + "REMOVE_ITEMS_MODE_DEACTIVATE", + "REMOVE_ITEMS_MODE_SOFT_DELETE" + ], + "default": "REMOVE_ITEMS_MODE_UNSPECIFIED", + "description": "RemoveItemsMode controls how RemoveItems disposes of detail rows.\n\n - REMOVE_ITEMS_MODE_UNSPECIFIED: Default zero value. Rejected via not_in: [0].\n - REMOVE_ITEMS_MODE_DEACTIVATE: Mark the details inactive but keep the rows for audit history.\n - REMOVE_ITEMS_MODE_SOFT_DELETE: Soft-delete the details (sets deleted_at/deleted_by)." + }, + "v1RemoveItemsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "removedCount": { + "type": "integer", + "format": "int32", + "description": "Number of details affected." + } + }, + "description": "Remove items response." + }, + "v1SkippedItem": { + "type": "object", + "properties": { + "itemCode": { + "type": "string", + "description": "Item code that was skipped." + }, + "owningGroupHeadId": { + "type": "string", + "description": "Owning group head UUID." + }, + "owningGroupDetailId": { + "type": "string", + "description": "Owning detail UUID." + }, + "owningGroupCode": { + "type": "string", + "description": "Owning group code (for UI display)." + } + }, + "description": "SkippedItem captures items rejected by AddItems because they already belong\nto another active group." + }, + "v1UngroupedItem": { + "type": "object", + "properties": { + "period": { + "type": "string", + "description": "Period (YYYYMM)." + }, + "itemCode": { + "type": "string", + "description": "Item code." + }, + "itemName": { + "type": "string", + "description": "Item name." + }, + "itemTypeCode": { + "type": "string", + "description": "Item type code." + }, + "gradeCode": { + "type": "string", + "description": "Grade code." + }, + "itemGrade": { + "type": "string", + "description": "Item grade." + }, + "uomCode": { + "type": "string", + "description": "UOM code." + }, + "consVal": { + "type": "number", + "format": "double", + "description": "CONS stage value." + }, + "storesVal": { + "type": "number", + "format": "double", + "description": "STORES stage value." + }, + "consQty": { + "type": "number", + "format": "double", + "description": "CONS stage quantity." + }, + "consRate": { + "type": "number", + "format": "double", + "description": "CONS stage rate." + }, + "storesQty": { + "type": "number", + "format": "double", + "description": "STORES stage quantity." + }, + "storesRate": { + "type": "number", + "format": "double", + "description": "STORES stage rate." + }, + "deptQty": { + "type": "number", + "format": "double", + "description": "DEPT stage quantity." + }, + "deptVal": { + "type": "number", + "format": "double", + "description": "DEPT stage value." + }, + "deptRate": { + "type": "number", + "format": "double", + "description": "DEPT stage rate." + }, + "lastPoQty1": { + "type": "number", + "format": "double", + "description": "PO_1 stage quantity." + }, + "lastPoVal1": { + "type": "number", + "format": "double", + "description": "PO_1 stage value." + }, + "lastPoRate1": { + "type": "number", + "format": "double", + "description": "PO_1 stage rate." + }, + "lastPoQty2": { + "type": "number", + "format": "double", + "description": "PO_2 stage quantity." + }, + "lastPoVal2": { + "type": "number", + "format": "double", + "description": "PO_2 stage value." + }, + "lastPoRate2": { + "type": "number", + "format": "double", + "description": "PO_2 stage rate." + }, + "lastPoQty3": { + "type": "number", + "format": "double", + "description": "PO_3 stage quantity." + }, + "lastPoVal3": { + "type": "number", + "format": "double", + "description": "PO_3 stage value." + }, + "lastPoRate3": { + "type": "number", + "format": "double", + "description": "PO_3 stage rate." + } + }, + "description": "UngroupedItem is a raw material present in the Oracle sync feed that has no\nactive RM group assignment." + }, + "v1UpdateRMGroupResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "$ref": "#/definitions/v1RMGroupHead", + "description": "Updated head." + } + }, + "description": "Update response." + }, + "v1ValidationError": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "The field name that failed validation." + }, + "message": { + "type": "string", + "description": "The validation error message." + } + }, + "description": "ValidationError represents a single field validation error." + } + } +} diff --git a/services/finance/Makefile b/services/finance/Makefile index 9190bad..b1a4929 100644 --- a/services/finance/Makefile +++ b/services/finance/Makefile @@ -45,11 +45,19 @@ help: run: go run cmd/server/main.go +run-worker: + go run cmd/worker/main.go + build: @mkdir -p bin CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=$(VERSION)" -o $(BINARY) cmd/server/main.go @echo "Built $(BINARY)" +build-worker: + @mkdir -p bin + CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=$(VERSION)" -o bin/finance-worker cmd/worker/main.go + @echo "Built bin/finance-worker" + dev: air diff --git a/services/finance/cmd/server/main.go b/services/finance/cmd/server/main.go index 2027cfe..da8229a 100644 --- a/services/finance/cmd/server/main.go +++ b/services/finance/cmd/server/main.go @@ -13,6 +13,7 @@ import ( financev1 "github.com/mutugading/goapps-backend/gen/finance/v1" "github.com/mutugading/goapps-backend/services/finance/internal/application/oraclesync" + apprmcost "github.com/mutugading/goapps-backend/services/finance/internal/application/rmcost" grpcdelivery "github.com/mutugading/goapps-backend/services/finance/internal/delivery/grpc" httpdelivery "github.com/mutugading/goapps-backend/services/finance/internal/delivery/httpdelivery" "github.com/mutugading/goapps-backend/services/finance/internal/infrastructure/config" @@ -71,9 +72,18 @@ func run() error { } // Setup RabbitMQ (optional - graceful degradation for publisher) - rmqPublisher, closeRabbitMQ := setupRabbitMQ(cfg) + rmqAdapter, closeRabbitMQ := setupRabbitMQ(cfg) defer closeRabbitMQ() + // Wrap into explicit interface values so that when RabbitMQ is unavailable + // the handlers receive a true nil interface (not a typed-nil pointer). + var oracleSyncPublisher oraclesync.JobPublisher + var rmCostPublisher apprmcost.JobPublisher + if rmqAdapter != nil { + oracleSyncPublisher = rmqAdapter + rmCostPublisher = rmqAdapter + } + // Setup repositories uomRepo := postgres.NewUOMRepository(db) rmCategoryRepo := postgres.NewRMCategoryRepository(db) @@ -82,9 +92,11 @@ func run() error { uomCategoryRepo := postgres.NewUOMCategoryRepository(db) jobRepo := postgres.NewJobRepository(db) syncDataRepo := postgres.NewSyncDataRepository(db) + rmGroupRepo := postgres.NewRMGroupRepository(db) + rmCostRepo := postgres.NewRMCostRepository(db) // Setup oracle sync handlers - triggerHandler := oraclesync.NewTriggerHandler(jobRepo, rmqPublisher) + triggerHandler := oraclesync.NewTriggerHandler(jobRepo, oracleSyncPublisher) getJobHandler := oraclesync.NewGetJobHandler(jobRepo) listJobsHandler := oraclesync.NewListJobsHandler(jobRepo) cancelJobHandler := oraclesync.NewCancelJobHandler(jobRepo) @@ -125,8 +137,34 @@ func run() error { return err } + recalcChain := grpcdelivery.NewRecalcChain( + jobRepo, + rmCostPublisher, + rmCostRepo.ListDistinctPeriods, + syncDataRepo.GetDistinctPeriods, + ) + rmGroupHandler, err := grpcdelivery.NewRMGroupHandler(rmGroupRepo, syncDataRepo, syncDataRepo, syncDataRepo, rmCostRepo, syncDataRepo, recalcChain) + if err != nil { + return err + } + + rmCostTrigger := apprmcost.NewTriggerHandler(jobRepo, rmCostPublisher) + rmCostCalculate := apprmcost.NewCalculateHandler(rmGroupRepo, rmCostRepo, syncDataRepo) + rmCostGet := apprmcost.NewGetHandler(rmCostRepo) + rmCostList := apprmcost.NewListHandler(rmCostRepo) + rmCostHistory := apprmcost.NewHistoryHandler(rmCostRepo) + rmCostPeriods := apprmcost.NewPeriodsHandler(rmCostRepo) + rmCostExport := apprmcost.NewExportHandler(rmCostRepo) + + rmCostHandler, err := grpcdelivery.NewRMCostHandler( + rmCostTrigger, rmCostCalculate, rmCostGet, rmCostList, rmCostHistory, rmCostPeriods, rmCostExport, + ) + if err != nil { + return err + } + // Setup and start servers - return startServers(ctx, cfg, uomHandler, rmCategoryHandler, parameterHandler, formulaHandler, uomCategoryHandler, oracleSyncHandler, tokenBlacklist) + return startServers(ctx, cfg, uomHandler, rmCategoryHandler, parameterHandler, formulaHandler, uomCategoryHandler, oracleSyncHandler, rmGroupHandler, rmCostHandler, tokenBlacklist) } // setupLogger configures the application logger. @@ -223,7 +261,7 @@ func closeAuthRedis(bl *redisinfra.TokenBlacklist) { } // startServers starts the gRPC and HTTP servers and handles graceful shutdown. -func startServers(ctx context.Context, cfg *config.Config, uomHandler *grpcdelivery.UOMHandler, rmCategoryHandler *grpcdelivery.RMCategoryHandler, parameterHandler *grpcdelivery.ParameterHandler, formulaHandler *grpcdelivery.FormulaHandler, uomCategoryHandler *grpcdelivery.UOMCategoryHandler, oracleSyncHandler *grpcdelivery.OracleSyncHandler, tokenBlacklist *redisinfra.TokenBlacklist) error { +func startServers(ctx context.Context, cfg *config.Config, uomHandler *grpcdelivery.UOMHandler, rmCategoryHandler *grpcdelivery.RMCategoryHandler, parameterHandler *grpcdelivery.ParameterHandler, formulaHandler *grpcdelivery.FormulaHandler, uomCategoryHandler *grpcdelivery.UOMCategoryHandler, oracleSyncHandler *grpcdelivery.OracleSyncHandler, rmGroupHandler *grpcdelivery.RMGroupHandler, rmCostHandler *grpcdelivery.RMCostHandler, tokenBlacklist *redisinfra.TokenBlacklist) error { // Setup gRPC server with JWT auth and token blacklist grpcServer, err := grpcdelivery.NewServer(&cfg.Server, nil, &cfg.JWT, tokenBlacklist) if err != nil { @@ -237,6 +275,8 @@ func startServers(ctx context.Context, cfg *config.Config, uomHandler *grpcdeliv financev1.RegisterFormulaServiceServer(grpcServer.GRPCServer(), formulaHandler) financev1.RegisterUOMCategoryServiceServer(grpcServer.GRPCServer(), uomCategoryHandler) financev1.RegisterOracleSyncServiceServer(grpcServer.GRPCServer(), oracleSyncHandler) + financev1.RegisterRMGroupServiceServer(grpcServer.GRPCServer(), rmGroupHandler) + financev1.RegisterRMCostServiceServer(grpcServer.GRPCServer(), rmCostHandler) // Start gRPC server go func() { diff --git a/services/finance/cmd/worker/main.go b/services/finance/cmd/worker/main.go index 83c539f..10d2058 100644 --- a/services/finance/cmd/worker/main.go +++ b/services/finance/cmd/worker/main.go @@ -13,6 +13,8 @@ import ( "github.com/rs/zerolog/log" "github.com/mutugading/goapps-backend/services/finance/internal/application/oraclesync" + apprmcost "github.com/mutugading/goapps-backend/services/finance/internal/application/rmcost" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" "github.com/mutugading/goapps-backend/services/finance/internal/infrastructure/config" "github.com/mutugading/goapps-backend/services/finance/internal/infrastructure/oracle" "github.com/mutugading/goapps-backend/services/finance/internal/infrastructure/postgres" @@ -25,7 +27,7 @@ func main() { } } -func run() error { +func run() error { //nolint:gocognit // linear setup function setupLogger() cfg, err := config.Load() @@ -54,17 +56,18 @@ func run() error { Int("port", cfg.Database.Port). Msg("Database connected") - // Setup Oracle. + // Setup Oracle (optional - graceful degradation; RM cost jobs don't need it). oracleClient, err := oracle.NewClient(cfg.Oracle, log.Logger) if err != nil { - return err + log.Warn().Err(err).Msg("Oracle unavailable; oracle_sync jobs will be skipped") + oracleClient = nil + } else { + defer closeResource("oracle", oracleClient) + log.Info(). + Str("host", cfg.Oracle.Host). + Int("port", cfg.Oracle.Port). + Msg("Oracle connected") } - defer closeResource("oracle", oracleClient) - - log.Info(). - Str("host", cfg.Oracle.Host). - Int("port", cfg.Oracle.Port). - Msg("Oracle connected") // Setup RabbitMQ. rmqConn, err := rabbitmq.NewConnection(cfg.RabbitMQ, log.Logger) @@ -75,14 +78,35 @@ func run() error { // Create repositories. jobRepo := postgres.NewJobRepository(db) - oracleRepo := oracle.NewItemConsStockPORepository(oracleClient) + var oracleRepo *oracle.ItemConsStockPORepository + if oracleClient != nil { + oracleRepo = oracle.NewItemConsStockPORepository(oracleClient) + } syncDataRepo := postgres.NewSyncDataRepository(db) + rmGroupRepo := postgres.NewRMGroupRepository(db) + rmCostRepo := postgres.NewRMCostRepository(db) + + // RabbitMQ publisher (also used by sync handler to chain-trigger rm cost). + rmqPublisher := rabbitmq.NewPublisher(rmqConn, log.Logger) + rmqJobPub := rabbitmq.NewJobPublisherAdapter(rmqPublisher, log.Logger) + + // Create sync handler with chain publisher (only when Oracle is available). + var syncHandler *oraclesync.SyncHandler + if oracleRepo != nil { + syncHandler = oraclesync.NewSyncHandler(jobRepo, oracleRepo, syncDataRepo, log.Logger). + WithChainPublisher(rmqJobPub) + } - // Create sync handler. - syncHandler := oraclesync.NewSyncHandler(jobRepo, oracleRepo, syncDataRepo, log.Logger) + // Create rm cost calculation handler. + rmCostCalc := apprmcost.NewCalculateHandler(rmGroupRepo, rmCostRepo, syncDataRepo) + rmCostExec := apprmcost.NewExecuteHandler(jobRepo, rmCostCalc, log.Logger) - // Create message handler. - handler := func(ctx context.Context, msg rabbitmq.JobMessage) error { + // Oracle sync message handler. + syncMsgHandler := func(ctx context.Context, msg rabbitmq.JobMessage) error { + if syncHandler == nil { + log.Warn().Str("job_id", msg.JobID).Msg("Oracle sync job received but Oracle unavailable; skipping") + return nil + } jobID, parseErr := uuid.Parse(msg.JobID) if parseErr != nil { log.Error().Err(parseErr).Str("job_id", msg.JobID).Msg("Invalid job ID in message") @@ -91,16 +115,44 @@ func run() error { return syncHandler.Execute(ctx, jobID) } - // Start consumer. - consumer := rabbitmq.NewConsumer(rmqConn, rabbitmq.QueueOracleSync, handler, log.Logger) + // RM cost calculation message handler. + rmCostMsgHandler := func(ctx context.Context, msg rabbitmq.JobMessage) error { + jobID, parseErr := uuid.Parse(msg.JobID) + if parseErr != nil { + log.Error().Err(parseErr).Str("job_id", msg.JobID).Msg("Invalid rm cost job ID") + return parseErr + } + cmd := apprmcost.ExecuteCommand{ + JobID: jobID, + Period: msg.Period, + CalculatedBy: msg.CreatedBy, + TriggerReason: rmcost.HistoryTriggerReason(msg.Reason), + } + if msg.GroupHeadID != "" { + gid, parseErr := uuid.Parse(msg.GroupHeadID) + if parseErr != nil { + log.Error().Err(parseErr).Str("group_head_id", msg.GroupHeadID).Msg("Invalid group head id in rm cost message") + return parseErr + } + cmd.GroupHeadID = &gid + } + return rmCostExec.Execute(ctx, cmd) + } + + // Start consumers. + syncConsumer := rabbitmq.NewConsumer(rmqConn, rabbitmq.QueueOracleSync, syncMsgHandler, log.Logger) + rmCostConsumer := rabbitmq.NewConsumer(rmqConn, rabbitmq.QueueRMCostCalc, rmCostMsgHandler, log.Logger) // Log connection close events. go watchConnection(ctx, rmqConn) - // Start consuming in a goroutine. - errCh := make(chan error, 1) + // Start consuming in goroutines. + errCh := make(chan error, 2) + go func() { + errCh <- syncConsumer.Start(ctx) + }() go func() { - errCh <- consumer.Start(ctx) + errCh <- rmCostConsumer.Start(ctx) }() // Wait for shutdown signal or consumer error. diff --git a/services/finance/config.yaml b/services/finance/config.yaml index 09042eb..cfedbf1 100644 --- a/services/finance/config.yaml +++ b/services/finance/config.yaml @@ -53,7 +53,7 @@ oracle: conn_max_lifetime: 10m rabbitmq: - url: "" # Override via RABBITMQ_URL env var (e.g. amqp://user:pass@host:5672/) + url: "amqp://guest:guest@localhost:5672/" # Override via RABBITMQ_URL env var prefetch_count: 1 reconnect_delay: 5s diff --git a/services/finance/internal/application/oraclesync/sync_handler.go b/services/finance/internal/application/oraclesync/sync_handler.go index 0aa9494..ff70a5e 100644 --- a/services/finance/internal/application/oraclesync/sync_handler.go +++ b/services/finance/internal/application/oraclesync/sync_handler.go @@ -25,11 +25,19 @@ const ( stepUpsert = "upsert_data" ) +// ChainPublisher publishes a follow-up RM cost calculation job after a +// successful Oracle sync. Matches the rabbitmq.JobPublisherAdapter method set. +// Nil is allowed — chaining is then skipped (useful for tests / dry runs). +type ChainPublisher interface { + PublishRMCostCalculation(ctx context.Context, jobID, period string, groupHeadID *uuid.UUID, reason, createdBy string) error +} + // SyncHandler orchestrates the Oracle sync process. type SyncHandler struct { jobRepo job.Repository oracleRepo syncdata.OracleSourceRepository pgRepo syncdata.PostgresTargetRepository + chainPub ChainPublisher logger zerolog.Logger } @@ -48,6 +56,13 @@ func NewSyncHandler( } } +// WithChainPublisher installs the follow-up publisher so that a successful sync +// automatically enqueues an RM cost recalculation for the synced period. +func (h *SyncHandler) WithChainPublisher(pub ChainPublisher) *SyncHandler { + h.chainPub = pub + return h +} + // Execute runs the full sync workflow for a given job. func (h *SyncHandler) Execute(ctx context.Context, jobID uuid.UUID) error { // Fetch the job execution. @@ -120,7 +135,47 @@ func (h *SyncHandler) runSync(ctx context.Context, exec *job.Execution) error { } // Complete the job with result summary. - return h.completeJob(ctx, exec, result) + if err := h.completeJob(ctx, exec, result); err != nil { + return err + } + + // Chain-trigger RM cost recalculation for the synced period. Failure here + // does not fail the sync job — operators can re-run cost calc manually. + h.publishCostChain(ctx, exec.Period(), exec.CreatedBy()) + return nil +} + +func (h *SyncHandler) publishCostChain(ctx context.Context, period, createdBy string) { + if h.chainPub == nil { + return + } + + chainExec, err := job.NewExecution(job.TypeRMCostCalculation, "landed_cost", period, createdBy, 5, nil) + if err != nil { + h.logger.Warn().Err(err).Str("period", period).Msg("Failed to build chained rm cost job") + return + } + if err := h.jobRepo.Create(ctx, chainExec); err != nil { + h.logger.Warn().Err(err).Str("period", period).Msg("Failed to persist chained rm cost job") + return + } + + if err := h.chainPub.PublishRMCostCalculation(ctx, chainExec.ID().String(), period, nil, "oracle-sync-chain", createdBy); err != nil { + // Compensate: mark the created job as failed so it doesn't linger as queued. + if failErr := chainExec.Fail(err.Error()); failErr == nil { + if updErr := h.jobRepo.UpdateStatus(ctx, chainExec); updErr != nil { + h.logger.Warn().Err(updErr).Msg("Failed to mark chained job as failed") + } + } + h.logger.Warn().Err(err). + Str("period", period). + Msg("Failed to chain-trigger RM cost calculation (operator can run manually)") + return + } + h.logger.Info(). + Str("chain_job_id", chainExec.ID().String()). + Str("period", period). + Msg("Chained RM cost calculation job enqueued") } func (h *SyncHandler) executeProcedure(ctx context.Context, jobID uuid.UUID, period string) error { diff --git a/services/finance/internal/application/rmcost/calculate_handler.go b/services/finance/internal/application/rmcost/calculate_handler.go new file mode 100644 index 0000000..713588f --- /dev/null +++ b/services/finance/internal/application/rmcost/calculate_handler.go @@ -0,0 +1,315 @@ +// Package rmcost provides application layer handlers for RM landed-cost calculation jobs. +package rmcost + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// SourceDataReader loads the per-stage consumption/stock/PO records that feed the +// landed-cost engine. Implementations read from `cst_item_cons_stk_po` filtered +// to (period, item_codes) and return the per-stage qty/val pointers needed by +// rmcost.AggregateRates. +type SourceDataReader interface { + FetchRateInputs(ctx context.Context, period string, itemCodes []string) ([]rmcost.RateInputs, int, error) + // FetchItemUOMs returns a map of item_code -> uom for the given period, for + // items whose uom column is non-empty in the sync feed. Used as a fallback + // when the rm_group detail rows were created without a UOM. + FetchItemUOMs(ctx context.Context, period string, itemCodes []string) (map[string]string, error) +} + +// CalculateCommand requests calculation for one group (GroupHeadID non-nil) or +// all active groups (GroupHeadID nil) in the given period. +type CalculateCommand struct { + Period string + GroupHeadID *uuid.UUID + JobID *uuid.UUID + TriggerReason rmcost.HistoryTriggerReason + CalculatedBy string +} + +// CalculateResult summarizes the outcome of a calculation pass. +type CalculateResult struct { + Period string + Processed int + Skipped int + Costs []*rmcost.Cost +} + +// CalculateHandler runs the full pipeline per group and persists the result. +type CalculateHandler struct { + groupRepo rmgroup.Repository + costRepo rmcost.Repository + source SourceDataReader +} + +// NewCalculateHandler builds a CalculateHandler. +func NewCalculateHandler( + groupRepo rmgroup.Repository, + costRepo rmcost.Repository, + source SourceDataReader, +) *CalculateHandler { + return &CalculateHandler{groupRepo: groupRepo, costRepo: costRepo, source: source} +} + +// Handle validates inputs, expands the target group list, and processes each one +// independently. A group whose active-detail list is empty produces a row with +// all-zero rates — this matches the plan's §6 "all-zero edge case" behavior. +func (h *CalculateHandler) Handle(ctx context.Context, cmd CalculateCommand) (*CalculateResult, error) { + if cmd.CalculatedBy == "" { + return nil, rmcost.ErrEmptyCalculatedBy + } + if err := rmcost.ValidatePeriod(cmd.Period); err != nil { + return nil, err + } + reason := cmd.TriggerReason + if reason == "" { + reason = rmcost.TriggerManualUI + } + if !reason.IsValid() { + return nil, fmt.Errorf("invalid trigger reason %q", cmd.TriggerReason) + } + + heads, err := h.resolveTargets(ctx, cmd.GroupHeadID) + if err != nil { + return nil, err + } + + result := &CalculateResult{Period: cmd.Period} + for _, head := range heads { + if !head.IsActive() || head.IsDeleted() { + result.Skipped++ + continue + } + cost, err := h.processHead(ctx, head, cmd.Period, cmd.JobID, reason, cmd.CalculatedBy) + if err != nil { + return nil, fmt.Errorf("process head %s: %w", head.Code(), err) + } + result.Costs = append(result.Costs, cost) + result.Processed++ + } + return result, nil +} + +// resolveTargets returns the list of heads to calculate. When groupHeadID is +// non-nil only that head is loaded; otherwise every active head is fetched via +// the repository list API in batches. +func (h *CalculateHandler) resolveTargets(ctx context.Context, groupHeadID *uuid.UUID) ([]*rmgroup.Head, error) { + if groupHeadID != nil { + head, err := h.groupRepo.GetHeadByID(ctx, *groupHeadID) + if err != nil { + return nil, fmt.Errorf("load head %s: %w", *groupHeadID, err) + } + return []*rmgroup.Head{head}, nil + } + + active := true + page := 1 + const pageSize = 100 + var all []*rmgroup.Head + for { + filter := rmgroup.ListFilter{IsActive: &active, Page: page, PageSize: pageSize, SortBy: "code", SortOrder: "asc"} + filter.Validate() + heads, total, err := h.groupRepo.ListHeads(ctx, filter) + if err != nil { + return nil, fmt.Errorf("list heads page %d: %w", page, err) + } + all = append(all, heads...) + if int64(len(all)) >= total || len(heads) == 0 { + break + } + page++ + } + return all, nil +} + +// processHead runs one calculation pass for a single group head: load active +// details, fetch source rows, calculate, upsert cost + history in one tx. +func (h *CalculateHandler) processHead( + ctx context.Context, + head *rmgroup.Head, + period string, + jobID *uuid.UUID, + reason rmcost.HistoryTriggerReason, + calculatedBy string, +) (*rmcost.Cost, error) { + details, err := h.groupRepo.ListActiveDetailsByHeadID(ctx, head.ID()) + if err != nil { + return nil, fmt.Errorf("list active details: %w", err) + } + + itemCodes := make([]string, 0, len(details)) + for _, d := range details { + if d.IsDummy() { + continue + } + itemCodes = append(itemCodes, d.ItemCode().String()) + } + + inputs, sourceCount, err := h.fetchInputs(ctx, period, itemCodes) + if err != nil { + return nil, err + } + + header := toHeaderInputs(head) + computed := rmcost.CalculateCost(inputs, header) + + // Pick a representative UOM from the first non-dummy detail with a non-empty + // UOM code. Groups usually hold items of the same unit; this gives operators + // a useful display value instead of "—". When all details were added without + // a UOM, fall back to looking up the UOM from the sync feed by item_code. + uomCode := pickGroupUOM(details) + if uomCode == "" && len(itemCodes) > 0 { + uomCode, err = h.lookupUOMFromSource(ctx, period, itemCodes) + if err != nil { + return nil, err + } + } + + cost, err := h.buildOrUpdateCost(ctx, head, period, uomCode, computed, calculatedBy) + if err != nil { + return nil, err + } + + hist := buildHistory(cost, head, computed, sourceCount, jobID, reason, calculatedBy) + if err := h.costRepo.Upsert(ctx, cost, hist); err != nil { + return nil, fmt.Errorf("upsert cost + history: %w", err) + } + return cost, nil +} + +func (h *CalculateHandler) fetchInputs(ctx context.Context, period string, itemCodes []string) ([]rmcost.RateInputs, int, error) { + if len(itemCodes) == 0 { + return nil, 0, nil + } + inputs, n, err := h.source.FetchRateInputs(ctx, period, itemCodes) + if err != nil { + return nil, 0, fmt.Errorf("fetch rate inputs: %w", err) + } + return inputs, n, nil +} + +// buildOrUpdateCost loads the existing (period, rm_code) row if any and applies +// the fresh Computed values; otherwise constructs a brand-new Cost aggregate. +func (h *CalculateHandler) buildOrUpdateCost( + ctx context.Context, + head *rmgroup.Head, + period, uomCode string, + computed rmcost.Computed, + calculatedBy string, +) (*rmcost.Cost, error) { + existing, err := h.costRepo.GetByPeriodAndCode(ctx, period, head.Code().String()) + if err != nil && !errors.Is(err, rmcost.ErrNotFound) { + return nil, fmt.Errorf("lookup existing cost: %w", err) + } + if existing != nil { + if err := existing.ApplyComputed(computed, calculatedBy); err != nil { + return nil, fmt.Errorf("apply computed: %w", err) + } + if uomCode != "" { + existing.SetUOMCode(uomCode) + } + return existing, nil + } + cost, err := rmcost.NewGroupCost(period, head.Code().String(), head.ID(), head.Name(), uomCode, computed, calculatedBy) + if err != nil { + return nil, fmt.Errorf("new cost: %w", err) + } + return cost, nil +} + +// lookupUOMFromSource returns the first non-empty UOM from cst_item_cons_stk_po +// for the given period+item_codes. Used to backfill the display UOM when none +// of the rm_group details carry one. Returns "" when the feed has no UOM for +// any of the items (not an error). +func (h *CalculateHandler) lookupUOMFromSource(ctx context.Context, period string, itemCodes []string) (string, error) { + uoms, err := h.source.FetchItemUOMs(ctx, period, itemCodes) + if err != nil { + return "", fmt.Errorf("fetch item uoms: %w", err) + } + // Prefer order of the detail list for determinism. + for _, code := range itemCodes { + if u := uoms[code]; u != "" { + return u, nil + } + } + return "", nil +} + +// pickGroupUOM returns the UOM code from the first non-dummy detail whose +// UOMCode is non-empty. Returns "" when no candidate exists. +func pickGroupUOM(details []*rmgroup.Detail) string { + for _, d := range details { + if d.IsDummy() { + continue + } + if u := d.UOMCode(); u != "" { + return u + } + } + return "" +} + +// toHeaderInputs maps the rmgroup.Head fields required by the calc engine. The +// rmcost package never imports rmgroup — this adapter lives in the application +// layer so the domain boundary stays clean. +func toHeaderInputs(head *rmgroup.Head) rmcost.HeaderInputs { + return rmcost.HeaderInputs{ + CostPercentage: head.CostPercentage(), + CostPerKg: head.CostPerKg(), + FlagValuation: rmcost.Stage(head.FlagValuation()), + FlagMarketing: rmcost.Stage(head.FlagMarketing()), + FlagSimulation: rmcost.Stage(head.FlagSimulation()), + InitValValuation: head.InitValValuation(), + InitValMarketing: head.InitValMarketing(), + InitValSimulation: head.InitValSimulation(), + } +} + +func buildHistory( + cost *rmcost.Cost, + head *rmgroup.Head, + computed rmcost.Computed, + sourceCount int, + jobID *uuid.UUID, + reason rmcost.HistoryTriggerReason, + calculatedBy string, +) rmcost.History { + costID := cost.ID() + headID := head.ID() + return rmcost.History{ + ID: uuid.New(), + RMCostID: &costID, + JobID: jobID, + Period: cost.Period(), + RMCode: cost.RMCode(), + RMType: cost.RMType(), + GroupHeadID: &headID, + Rates: computed.Rates, + CostPercentage: head.CostPercentage(), + CostPerKg: head.CostPerKg(), + FlagValuation: computed.FlagValuation, + FlagMarketing: computed.FlagMarketing, + FlagSimulation: computed.FlagSimulation, + InitValValuation: head.InitValValuation(), + InitValMarketing: head.InitValMarketing(), + InitValSimulation: head.InitValSimulation(), + CostValuation: cost.CostValuation(), + CostMarketing: cost.CostMarketing(), + CostSimulation: cost.CostSimulation(), + FlagValuationUsed: computed.FlagValuationUsed, + FlagMarketingUsed: computed.FlagMarketingUsed, + FlagSimulationUsed: computed.FlagSimulationUsed, + SourceItemCount: sourceCount, + TriggerReason: reason, + CalculatedAt: time.Now(), + CalculatedBy: calculatedBy, + } +} diff --git a/services/finance/internal/application/rmcost/execute_handler.go b/services/finance/internal/application/rmcost/execute_handler.go new file mode 100644 index 0000000..72a2387 --- /dev/null +++ b/services/finance/internal/application/rmcost/execute_handler.go @@ -0,0 +1,122 @@ +package rmcost + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/google/uuid" + "github.com/rs/zerolog" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/job" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" +) + +// ExecuteCommand is the worker-side input parsed from a queue message. It mirrors +// CalculateCommand but sources its JobID/Period/GroupHeadID from the message +// payload rather than an HTTP caller. +type ExecuteCommand struct { + JobID uuid.UUID + Period string + GroupHeadID *uuid.UUID + TriggerReason rmcost.HistoryTriggerReason + CalculatedBy string +} + +// ExecuteHandler drives the job lifecycle around CalculateHandler: it marks the +// job as processing, invokes the calculation, then persists the terminal status. +// Lives in the application layer so the worker entrypoint stays thin. +type ExecuteHandler struct { + jobRepo job.Repository + calculate *CalculateHandler + logger zerolog.Logger +} + +// NewExecuteHandler builds an ExecuteHandler. +func NewExecuteHandler(jobRepo job.Repository, calculate *CalculateHandler, logger zerolog.Logger) *ExecuteHandler { + return &ExecuteHandler{jobRepo: jobRepo, calculate: calculate, logger: logger} +} + +// Execute runs a full rm-cost calculation job by ID, updating job status along +// the way. Returns nil if the job completed (or was already terminal), else the +// underlying error so the consumer can nack to DLQ. +func (h *ExecuteHandler) Execute(ctx context.Context, cmd ExecuteCommand) error { + exec, err := h.jobRepo.GetByID(ctx, cmd.JobID) + if err != nil { + return fmt.Errorf("get job %s: %w", cmd.JobID, err) + } + + if exec.Status().IsTerminal() { + h.logger.Info(). + Str("job_id", cmd.JobID.String()). + Str("status", exec.Status().String()). + Msg("Skipping rm cost job — already terminal") + return nil + } + + if err := exec.Start(); err != nil { + return fmt.Errorf("start job %s: %w", cmd.JobID, err) + } + if err := h.jobRepo.UpdateStatus(ctx, exec); err != nil { + return fmt.Errorf("update status to processing: %w", err) + } + + h.logger.Info(). + Str("job_id", cmd.JobID.String()). + Str("period", cmd.Period). + Msg("Starting RM cost calculation job") + + calcID := cmd.JobID + calcCmd := CalculateCommand{ + Period: cmd.Period, + GroupHeadID: cmd.GroupHeadID, + JobID: &calcID, + TriggerReason: cmd.TriggerReason, + CalculatedBy: cmd.CalculatedBy, + } + + result, calcErr := h.calculate.Handle(ctx, calcCmd) + if calcErr != nil { + return h.failJob(ctx, exec, calcErr) + } + + return h.completeJob(ctx, exec, result) +} + +func (h *ExecuteHandler) completeJob(ctx context.Context, exec *job.Execution, result *CalculateResult) error { + summary, err := json.Marshal(map[string]any{ + "period": result.Period, + "processed": result.Processed, + "skipped": result.Skipped, + }) + if err != nil { + return fmt.Errorf("marshal rm cost summary: %w", err) + } + if err := exec.Complete(summary); err != nil { + return fmt.Errorf("complete job: %w", err) + } + if err := h.jobRepo.UpdateStatus(ctx, exec); err != nil { + return fmt.Errorf("update status to success: %w", err) + } + h.logger.Info(). + Str("job_id", exec.ID().String()). + Int("processed", result.Processed). + Int("skipped", result.Skipped). + Msg("RM cost calculation job completed") + return nil +} + +func (h *ExecuteHandler) failJob(ctx context.Context, exec *job.Execution, calcErr error) error { + if failErr := exec.Fail(calcErr.Error()); failErr != nil { + h.logger.Error().Err(failErr).Msg("Failed to transition rm cost job to failed state") + return fmt.Errorf("fail job: %w (original: %w)", failErr, calcErr) + } + if err := h.jobRepo.UpdateStatus(ctx, exec); err != nil { + h.logger.Error().Err(err).Msg("Failed to persist rm cost job failure status") + return fmt.Errorf("update status to failed: %w (original: %w)", err, calcErr) + } + h.logger.Error().Err(calcErr). + Str("job_id", exec.ID().String()). + Msg("RM cost calculation job failed") + return calcErr +} diff --git a/services/finance/internal/application/rmcost/export_handler.go b/services/finance/internal/application/rmcost/export_handler.go new file mode 100644 index 0000000..96f402c --- /dev/null +++ b/services/finance/internal/application/rmcost/export_handler.go @@ -0,0 +1,177 @@ +package rmcost + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + "github.com/xuri/excelize/v2" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" +) + +// ExportQuery is the query for exporting RM cost rows. +type ExportQuery struct { + Period string + RMType rmcost.RMType + GroupHeadID *uuid.UUID + Search string +} + +// ExportResult is the export bytes + filename. +type ExportResult struct { + FileContent []byte + FileName string +} + +// ExportHandler produces a single-sheet Excel of RM cost rows. +type ExportHandler struct { + repo rmcost.Repository +} + +// NewExportHandler builds an ExportHandler. +func NewExportHandler(repo rmcost.Repository) *ExportHandler { + return &ExportHandler{repo: repo} +} + +const costSheetName = "RMCosts" + +var costExportHeaders = []string{ + "period", "rm_code", "rm_name", "rm_type", "uom_code", + "cons_rate", "stores_rate", "dept_rate", "po_rate_1", "po_rate_2", "po_rate_3", + "cost_valuation", "cost_marketing", "cost_simulation", + "flag_valuation", "flag_marketing", "flag_simulation", + "flag_valuation_used", "flag_marketing_used", "flag_simulation_used", + "calculated_at", "calculated_by", +} + +// Handle executes the export. +func (h *ExportHandler) Handle(ctx context.Context, q ExportQuery) (result *ExportResult, err error) { + costs, err := h.repo.ListAll(ctx, rmcost.ExportFilter{ + Period: q.Period, + RMType: q.RMType, + GroupHeadID: q.GroupHeadID, + Search: q.Search, + }) + if err != nil { + return nil, fmt.Errorf("list all costs: %w", err) + } + + f := excelize.NewFile() + defer func() { + if cerr := f.Close(); cerr != nil { + log.Warn().Err(cerr).Msg("close excel") + if err == nil { + err = fmt.Errorf("close file: %w", cerr) + } + } + }() + + if _, err := f.NewSheet(costSheetName); err != nil { + return nil, fmt.Errorf("new sheet: %w", err) + } + if err := writeHeaderRow(f, costSheetName, costExportHeaders); err != nil { + return nil, err + } + + var errs []error + for i, c := range costs { + row := i + 2 + rates := c.Rates() + vals := []any{ + c.Period(), + c.RMCode(), + c.RMName(), + c.RMType().String(), + c.UOMCode(), + rates.Cons, rates.Stores, rates.Dept, rates.PO1, rates.PO2, rates.PO3, + nilableFloatCost(c.CostValuation()), + nilableFloatCost(c.CostMarketing()), + nilableFloatCost(c.CostSimulation()), + c.FlagValuation().String(), c.FlagMarketing().String(), c.FlagSimulation().String(), + c.FlagValuationUsed().String(), c.FlagMarketingUsed().String(), c.FlagSimulationUsed().String(), + formatTimePtr(c.CalculatedAt()), + derefStringCost(c.CalculatedBy()), + } + if werr := writeRow(f, costSheetName, row, vals); werr != nil { + errs = append(errs, werr) + } + } + if len(errs) > 0 { + log.Warn().Err(errors.Join(errs...)).Msg("rm cost export partial row errors") + } + + if delErr := f.DeleteSheet("Sheet1"); delErr != nil { + log.Debug().Err(delErr).Msg("delete default sheet") + } + if idx, idxErr := f.GetSheetIndex(costSheetName); idxErr == nil { + f.SetActiveSheet(idx) + } + + buf, err := f.WriteToBuffer() + if err != nil { + return nil, fmt.Errorf("write buffer: %w", err) + } + return &ExportResult{FileContent: buf.Bytes(), FileName: "rm_costs_export.xlsx"}, nil +} + +// writeHeaderRow writes a styled header row to the given sheet. +func writeHeaderRow(f *excelize.File, sheet string, headers []string) error { + for col, h := range headers { + cell, err := excelize.CoordinatesToCellName(col+1, 1) + if err != nil { + return fmt.Errorf("cell name: %w", err) + } + if err := f.SetCellValue(sheet, cell, h); err != nil { + return fmt.Errorf("set header: %w", err) + } + } + style, err := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "FFFFFF"}, + Fill: excelize.Fill{Type: "pattern", Color: []string{"4472C4"}, Pattern: 1}, + }) + if err == nil { + lastCol, colErr := excelize.CoordinatesToCellName(len(headers), 1) + if colErr == nil { + _ = f.SetCellStyle(sheet, "A1", lastCol, style) //nolint:errcheck // best-effort styling + } + } + return nil +} + +func writeRow(f *excelize.File, sheet string, row int, values []any) error { + for col, v := range values { + cell, err := excelize.CoordinatesToCellName(col+1, row) + if err != nil { + return err + } + if err := f.SetCellValue(sheet, cell, v); err != nil { + return err + } + } + return nil +} + +func nilableFloatCost(v *float64) any { + if v == nil { + return "" + } + return *v +} + +func derefStringCost(s *string) string { + if s == nil { + return "" + } + return *s +} + +func formatTimePtr(t *time.Time) string { + if t == nil { + return "" + } + return t.Format(time.RFC3339) +} diff --git a/services/finance/internal/application/rmcost/get_handler.go b/services/finance/internal/application/rmcost/get_handler.go new file mode 100644 index 0000000..87ea399 --- /dev/null +++ b/services/finance/internal/application/rmcost/get_handler.go @@ -0,0 +1,39 @@ +// Package rmcost provides application layer handlers for RM landed-cost calculation jobs. +package rmcost + +import ( + "context" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" +) + +// GetQuery retrieves a single cost row by ID, or by (period, rm_code). +type GetQuery struct { + CostID string + Period string + RMCode string +} + +// GetHandler handles GetCost queries. +type GetHandler struct { + repo rmcost.Repository +} + +// NewGetHandler builds a GetHandler. +func NewGetHandler(repo rmcost.Repository) *GetHandler { + return &GetHandler{repo: repo} +} + +// Handle prefers CostID when non-empty, otherwise falls back to (period, rm_code). +func (h *GetHandler) Handle(ctx context.Context, query GetQuery) (*rmcost.Cost, error) { + if query.CostID != "" { + id, err := uuid.Parse(query.CostID) + if err != nil { + return nil, rmcost.ErrNotFound + } + return h.repo.GetByID(ctx, id) + } + return h.repo.GetByPeriodAndCode(ctx, query.Period, query.RMCode) +} diff --git a/services/finance/internal/application/rmcost/handlers_test.go b/services/finance/internal/application/rmcost/handlers_test.go new file mode 100644 index 0000000..0b93e35 --- /dev/null +++ b/services/finance/internal/application/rmcost/handlers_test.go @@ -0,0 +1,222 @@ +package rmcost_test + +import ( + "context" + "errors" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + appcost "github.com/mutugading/goapps-backend/services/finance/internal/application/rmcost" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/job" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +func newGroupHead(t *testing.T) *rmgroup.Head { + t.Helper() + code, err := rmgroup.NewCode("GRP-TEST") + require.NoError(t, err) + head, err := rmgroup.NewHead(code, "Test Group", "", 1.0, 10.0, "user:test") + require.NoError(t, err) + return head +} + +// --- TriggerHandler --- + +func TestTriggerHandler_Success(t *testing.T) { + ctx := context.Background() + jobRepo := new(mockJobRepo) + pub := new(mockPublisher) + + jobRepo.On("HasActiveJob", ctx, job.TypeRMCostCalculation, "202604").Return(false, nil) + jobRepo.On("Create", ctx, mock.AnythingOfType("*job.Execution")).Return(nil) + pub.On("PublishRMCostCalculation", ctx, + mock.AnythingOfType("string"), "202604", + (*uuid.UUID)(nil), "manual-ui", "user:x").Return(nil) + + h := appcost.NewTriggerHandler(jobRepo, pub) + res, err := h.Handle(ctx, appcost.TriggerCommand{ + Period: "202604", CreatedBy: "user:x", + }) + require.NoError(t, err) + assert.NotNil(t, res.Execution) +} + +func TestTriggerHandler_NilPublisher(t *testing.T) { + jobRepo := new(mockJobRepo) + h := appcost.NewTriggerHandler(jobRepo, nil) + _, err := h.Handle(context.Background(), appcost.TriggerCommand{Period: "202604", CreatedBy: "u"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "RabbitMQ not connected") +} + +func TestTriggerHandler_DuplicateActive(t *testing.T) { + ctx := context.Background() + jobRepo := new(mockJobRepo) + pub := new(mockPublisher) + jobRepo.On("HasActiveJob", ctx, job.TypeRMCostCalculation, "202604").Return(true, nil) + + h := appcost.NewTriggerHandler(jobRepo, pub) + _, err := h.Handle(ctx, appcost.TriggerCommand{Period: "202604", CreatedBy: "u"}) + assert.ErrorIs(t, err, job.ErrDuplicateActiveJob) +} + +func TestTriggerHandler_PublishFailureMarksFailed(t *testing.T) { + ctx := context.Background() + jobRepo := new(mockJobRepo) + pub := new(mockPublisher) + jobRepo.On("HasActiveJob", ctx, job.TypeRMCostCalculation, "202604").Return(false, nil) + jobRepo.On("Create", ctx, mock.AnythingOfType("*job.Execution")).Return(nil) + pub.On("PublishRMCostCalculation", ctx, + mock.AnythingOfType("string"), "202604", + (*uuid.UUID)(nil), "manual-ui", "u").Return(errors.New("amqp closed")) + jobRepo.On("UpdateStatus", ctx, mock.AnythingOfType("*job.Execution")).Return(nil) + + h := appcost.NewTriggerHandler(jobRepo, pub) + _, err := h.Handle(ctx, appcost.TriggerCommand{Period: "202604", CreatedBy: "u"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "publish job") + jobRepo.AssertCalled(t, "UpdateStatus", ctx, mock.AnythingOfType("*job.Execution")) +} + +func TestTriggerHandler_EmptyCreatedBy(t *testing.T) { + pub := new(mockPublisher) + h := appcost.NewTriggerHandler(new(mockJobRepo), pub) + _, err := h.Handle(context.Background(), appcost.TriggerCommand{Period: "202604"}) + assert.ErrorIs(t, err, rmcost.ErrEmptyCreatedBy) +} + +// --- CalculateHandler --- + +func TestCalculateHandler_SingleHead_EmptyDetails(t *testing.T) { + ctx := context.Background() + head := newGroupHead(t) + id := head.ID() + + groupRepo := new(mockGroupRepo) + costRepo := new(mockCostRepo) + src := new(mockSourceReader) + + groupRepo.On("GetHeadByID", ctx, id).Return(head, nil) + groupRepo.On("ListActiveDetailsByHeadID", ctx, id).Return([]*rmgroup.Detail{}, nil) + costRepo.On("GetByPeriodAndCode", ctx, "202604", head.Code().String()). + Return(nil, rmcost.ErrNotFound) + costRepo.On("Upsert", ctx, mock.AnythingOfType("*rmcost.Cost"), mock.AnythingOfType("rmcost.History")). + Return(nil) + + h := appcost.NewCalculateHandler(groupRepo, costRepo, src) + res, err := h.Handle(ctx, appcost.CalculateCommand{ + Period: "202604", GroupHeadID: &id, CalculatedBy: "user:calc", + }) + require.NoError(t, err) + assert.Equal(t, 1, res.Processed) + assert.Len(t, res.Costs, 1) + // Empty details => no source fetch, all-zero rates => cost == costPerKg (10). + assert.Equal(t, 10.0, *res.Costs[0].CostValuation()) + src.AssertNotCalled(t, "FetchRateInputs") +} + +func TestCalculateHandler_InvalidPeriod(t *testing.T) { + h := appcost.NewCalculateHandler(new(mockGroupRepo), new(mockCostRepo), new(mockSourceReader)) + _, err := h.Handle(context.Background(), appcost.CalculateCommand{ + Period: "bad", CalculatedBy: "u", + }) + assert.ErrorIs(t, err, rmcost.ErrInvalidPeriod) +} + +func TestCalculateHandler_EmptyCalculatedBy(t *testing.T) { + h := appcost.NewCalculateHandler(new(mockGroupRepo), new(mockCostRepo), new(mockSourceReader)) + _, err := h.Handle(context.Background(), appcost.CalculateCommand{Period: "202604"}) + assert.ErrorIs(t, err, rmcost.ErrEmptyCalculatedBy) +} + +// --- GetHandler --- + +func TestGetHandler_ByID(t *testing.T) { + ctx := context.Background() + id := uuid.New() + repo := new(mockCostRepo) + repo.On("GetByID", ctx, id).Return((*rmcost.Cost)(nil), rmcost.ErrNotFound) + + h := appcost.NewGetHandler(repo) + _, err := h.Handle(ctx, appcost.GetQuery{CostID: id.String()}) + assert.ErrorIs(t, err, rmcost.ErrNotFound) +} + +func TestGetHandler_InvalidIDReturnsNotFound(t *testing.T) { + h := appcost.NewGetHandler(new(mockCostRepo)) + _, err := h.Handle(context.Background(), appcost.GetQuery{CostID: "not-a-uuid"}) + assert.ErrorIs(t, err, rmcost.ErrNotFound) +} + +func TestGetHandler_ByPeriodAndCode(t *testing.T) { + ctx := context.Background() + repo := new(mockCostRepo) + repo.On("GetByPeriodAndCode", ctx, "202604", "GRP-1").Return((*rmcost.Cost)(nil), rmcost.ErrNotFound) + + h := appcost.NewGetHandler(repo) + _, err := h.Handle(ctx, appcost.GetQuery{Period: "202604", RMCode: "GRP-1"}) + assert.ErrorIs(t, err, rmcost.ErrNotFound) +} + +// --- ListHandler --- + +func TestListHandler_Pagination(t *testing.T) { + ctx := context.Background() + repo := new(mockCostRepo) + repo.On("List", ctx, mock.AnythingOfType("rmcost.ListFilter")). + Return([]*rmcost.Cost{}, int64(23), nil) + + h := appcost.NewListHandler(repo) + res, err := h.Handle(ctx, appcost.ListQuery{Page: 1, PageSize: 10, Period: "202604"}) + require.NoError(t, err) + assert.Equal(t, int64(23), res.TotalItems) + assert.Equal(t, int32(3), res.TotalPages) +} + +func TestListHandler_InvalidRMType(t *testing.T) { + h := appcost.NewListHandler(new(mockCostRepo)) + _, err := h.Handle(context.Background(), appcost.ListQuery{RMType: "BOGUS"}) + assert.ErrorIs(t, err, rmcost.ErrInvalidRMType) +} + +func TestListHandler_InvalidGroupHeadID(t *testing.T) { + h := appcost.NewListHandler(new(mockCostRepo)) + _, err := h.Handle(context.Background(), appcost.ListQuery{GroupHeadID: "bogus"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid group head id") +} + +// --- HistoryHandler --- + +func TestHistoryHandler_Pagination(t *testing.T) { + ctx := context.Background() + repo := new(mockCostRepo) + repo.On("ListHistory", ctx, mock.AnythingOfType("rmcost.HistoryFilter")). + Return([]rmcost.History{{}}, int64(45), nil) + + h := appcost.NewHistoryHandler(repo) + res, err := h.Handle(ctx, appcost.HistoryQuery{Page: 2, PageSize: 20}) + require.NoError(t, err) + assert.Equal(t, int64(45), res.TotalItems) + assert.Equal(t, int32(3), res.TotalPages) + assert.Equal(t, int32(2), res.CurrentPage) +} + +func TestHistoryHandler_InvalidGroupHeadID(t *testing.T) { + h := appcost.NewHistoryHandler(new(mockCostRepo)) + _, err := h.Handle(context.Background(), appcost.HistoryQuery{GroupHeadID: "bad"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid group head id") +} + +func TestHistoryHandler_InvalidJobID(t *testing.T) { + h := appcost.NewHistoryHandler(new(mockCostRepo)) + _, err := h.Handle(context.Background(), appcost.HistoryQuery{JobID: "bad"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid job id") +} diff --git a/services/finance/internal/application/rmcost/history_handler.go b/services/finance/internal/application/rmcost/history_handler.go new file mode 100644 index 0000000..056e741 --- /dev/null +++ b/services/finance/internal/application/rmcost/history_handler.go @@ -0,0 +1,86 @@ +// Package rmcost provides application layer handlers for RM landed-cost calculation jobs. +package rmcost + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" + "github.com/mutugading/goapps-backend/services/finance/pkg/safeconv" +) + +// HistoryQuery pages through the append-only aud_rm_cost_history trail. +type HistoryQuery struct { + Page int + PageSize int + Period string + RMCode string + GroupHeadID string + JobID string +} + +// HistoryResult is the paginated history result. +type HistoryResult struct { + Rows []rmcost.History + TotalItems int64 + TotalPages int32 + CurrentPage int32 + PageSize int32 +} + +// HistoryHandler returns rows from aud_rm_cost_history. +type HistoryHandler struct { + repo rmcost.Repository +} + +// NewHistoryHandler builds a HistoryHandler. +func NewHistoryHandler(repo rmcost.Repository) *HistoryHandler { + return &HistoryHandler{repo: repo} +} + +// Handle parses optional UUIDs, applies defaults, and returns the matching rows +// newest-first. +func (h *HistoryHandler) Handle(ctx context.Context, query HistoryQuery) (*HistoryResult, error) { + filter := rmcost.HistoryFilter{ + Period: query.Period, + RMCode: query.RMCode, + Page: query.Page, + PageSize: query.PageSize, + } + if query.GroupHeadID != "" { + id, err := uuid.Parse(query.GroupHeadID) + if err != nil { + return nil, fmt.Errorf("invalid group head id: %w", err) + } + filter.GroupHeadID = &id + } + if query.JobID != "" { + id, err := uuid.Parse(query.JobID) + if err != nil { + return nil, fmt.Errorf("invalid job id: %w", err) + } + filter.JobID = &id + } + filter.Validate() + + rows, total, err := h.repo.ListHistory(ctx, filter) + if err != nil { + return nil, fmt.Errorf("list history: %w", err) + } + + var totalPages int32 + if filter.PageSize > 0 && total > 0 { + computed := (total + int64(filter.PageSize) - 1) / int64(filter.PageSize) + totalPages = safeconv.Int64ToInt32(computed) + } + + return &HistoryResult{ + Rows: rows, + TotalItems: total, + TotalPages: totalPages, + CurrentPage: safeconv.IntToInt32(filter.Page), + PageSize: safeconv.IntToInt32(filter.PageSize), + }, nil +} diff --git a/services/finance/internal/application/rmcost/list_handler.go b/services/finance/internal/application/rmcost/list_handler.go new file mode 100644 index 0000000..0cb085a --- /dev/null +++ b/services/finance/internal/application/rmcost/list_handler.go @@ -0,0 +1,93 @@ +// Package rmcost provides application layer handlers for RM landed-cost calculation jobs. +package rmcost + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" + "github.com/mutugading/goapps-backend/services/finance/pkg/safeconv" +) + +// ListQuery is the paginated list query for cost rows. +type ListQuery struct { + Page int + PageSize int + Period string + RMType string + GroupHeadID string + Search string + SortBy string + SortOrder string +} + +// ListResult is the paginated list result. +type ListResult struct { + Costs []*rmcost.Cost + TotalItems int64 + TotalPages int32 + CurrentPage int32 + PageSize int32 +} + +// ListHandler handles ListCosts queries. +type ListHandler struct { + repo rmcost.Repository +} + +// NewListHandler builds a ListHandler. +func NewListHandler(repo rmcost.Repository) *ListHandler { + return &ListHandler{repo: repo} +} + +// Handle applies filters, runs the repository query, and wraps the result with +// pagination metadata suitable for the standard BaseResponse envelope. +func (h *ListHandler) Handle(ctx context.Context, query ListQuery) (*ListResult, error) { + filter := rmcost.ListFilter{ + Period: query.Period, + Search: query.Search, + Page: query.Page, + PageSize: query.PageSize, + SortBy: query.SortBy, + SortOrder: query.SortOrder, + } + + if query.RMType != "" { + rmType := rmcost.RMType(query.RMType) + if !rmType.IsValid() { + return nil, rmcost.ErrInvalidRMType + } + filter.RMType = rmType + } + + if query.GroupHeadID != "" { + id, err := uuid.Parse(query.GroupHeadID) + if err != nil { + return nil, fmt.Errorf("invalid group head id: %w", err) + } + filter.GroupHeadID = &id + } + + filter.Validate() + + costs, total, err := h.repo.List(ctx, filter) + if err != nil { + return nil, fmt.Errorf("list costs: %w", err) + } + + var totalPages int32 + if filter.PageSize > 0 && total > 0 { + computed := (total + int64(filter.PageSize) - 1) / int64(filter.PageSize) + totalPages = safeconv.Int64ToInt32(computed) + } + + return &ListResult{ + Costs: costs, + TotalItems: total, + TotalPages: totalPages, + CurrentPage: safeconv.IntToInt32(filter.Page), + PageSize: safeconv.IntToInt32(filter.PageSize), + }, nil +} diff --git a/services/finance/internal/application/rmcost/mocks_test.go b/services/finance/internal/application/rmcost/mocks_test.go new file mode 100644 index 0000000..e7faf7d --- /dev/null +++ b/services/finance/internal/application/rmcost/mocks_test.go @@ -0,0 +1,262 @@ +package rmcost_test + +import ( + "context" + + "github.com/google/uuid" + "github.com/stretchr/testify/mock" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/job" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// mockCostRepo implements rmcost.Repository. +type mockCostRepo struct{ mock.Mock } + +func (m *mockCostRepo) Upsert(ctx context.Context, cost *rmcost.Cost, hist rmcost.History) error { + return m.Called(ctx, cost, hist).Error(0) +} + +func (m *mockCostRepo) GetByID(ctx context.Context, id uuid.UUID) (*rmcost.Cost, error) { + args := m.Called(ctx, id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*rmcost.Cost), args.Error(1) +} + +func (m *mockCostRepo) GetByPeriodAndCode(ctx context.Context, period, rmCode string) (*rmcost.Cost, error) { + args := m.Called(ctx, period, rmCode) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*rmcost.Cost), args.Error(1) +} + +func (m *mockCostRepo) List(ctx context.Context, filter rmcost.ListFilter) ([]*rmcost.Cost, int64, error) { + args := m.Called(ctx, filter) + var out []*rmcost.Cost + if v := args.Get(0); v != nil { + out = v.([]*rmcost.Cost) + } + return out, args.Get(1).(int64), args.Error(2) +} + +func (m *mockCostRepo) ListAll(ctx context.Context, filter rmcost.ExportFilter) ([]*rmcost.Cost, error) { + args := m.Called(ctx, filter) + var out []*rmcost.Cost + if v := args.Get(0); v != nil { + out = v.([]*rmcost.Cost) + } + return out, args.Error(1) +} + +func (m *mockCostRepo) ExistsForGroupHead(ctx context.Context, groupHeadID uuid.UUID) (bool, error) { + args := m.Called(ctx, groupHeadID) + return args.Bool(0), args.Error(1) +} + +func (m *mockCostRepo) ListDistinctPeriods(ctx context.Context) ([]string, error) { + args := m.Called(ctx) + var out []string + if v := args.Get(0); v != nil { + out = v.([]string) + } + return out, args.Error(1) +} + +func (m *mockCostRepo) ListHistory(ctx context.Context, filter rmcost.HistoryFilter) ([]rmcost.History, int64, error) { + args := m.Called(ctx, filter) + var out []rmcost.History + if v := args.Get(0); v != nil { + out = v.([]rmcost.History) + } + return out, args.Get(1).(int64), args.Error(2) +} + +// mockJobRepo implements job.Repository. +type mockJobRepo struct{ mock.Mock } + +func (m *mockJobRepo) Create(ctx context.Context, exec *job.Execution) error { + return m.Called(ctx, exec).Error(0) +} + +func (m *mockJobRepo) GetByID(ctx context.Context, id uuid.UUID) (*job.Execution, error) { + args := m.Called(ctx, id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*job.Execution), args.Error(1) +} + +func (m *mockJobRepo) GetByCode(ctx context.Context, code string) (*job.Execution, error) { + args := m.Called(ctx, code) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*job.Execution), args.Error(1) +} + +func (m *mockJobRepo) List(ctx context.Context, filter job.ListFilter) ([]*job.Execution, int64, error) { + args := m.Called(ctx, filter) + var out []*job.Execution + if v := args.Get(0); v != nil { + out = v.([]*job.Execution) + } + return out, args.Get(1).(int64), args.Error(2) +} + +func (m *mockJobRepo) UpdateStatus(ctx context.Context, exec *job.Execution) error { + return m.Called(ctx, exec).Error(0) +} + +func (m *mockJobRepo) UpdateProgress(ctx context.Context, id uuid.UUID, progress int) error { + return m.Called(ctx, id, progress).Error(0) +} + +func (m *mockJobRepo) AddLog(ctx context.Context, log *job.ExecutionLog) error { + return m.Called(ctx, log).Error(0) +} + +func (m *mockJobRepo) UpdateLog(ctx context.Context, log *job.ExecutionLog) error { + return m.Called(ctx, log).Error(0) +} + +func (m *mockJobRepo) HasActiveJob(ctx context.Context, jobType job.Type, period string) (bool, error) { + args := m.Called(ctx, jobType, period) + return args.Bool(0), args.Error(1) +} + +func (m *mockJobRepo) GetNextSequence(ctx context.Context, jobType job.Type, period string) (int, error) { + args := m.Called(ctx, jobType, period) + return args.Int(0), args.Error(1) +} + +// mockPublisher implements appcost.JobPublisher. +type mockPublisher struct{ mock.Mock } + +func (m *mockPublisher) PublishRMCostCalculation(ctx context.Context, jobID, period string, groupHeadID *uuid.UUID, reason, createdBy string) error { + return m.Called(ctx, jobID, period, groupHeadID, reason, createdBy).Error(0) +} + +// mockGroupRepo implements rmgroup.Repository (subset used by CalculateHandler). +type mockGroupRepo struct{ mock.Mock } + +func (m *mockGroupRepo) CreateHead(ctx context.Context, head *rmgroup.Head) error { + return m.Called(ctx, head).Error(0) +} + +func (m *mockGroupRepo) GetHeadByID(ctx context.Context, id uuid.UUID) (*rmgroup.Head, error) { + args := m.Called(ctx, id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*rmgroup.Head), args.Error(1) +} + +func (m *mockGroupRepo) GetHeadByCode(ctx context.Context, code rmgroup.Code) (*rmgroup.Head, error) { + args := m.Called(ctx, code) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*rmgroup.Head), args.Error(1) +} + +func (m *mockGroupRepo) ListHeads(ctx context.Context, filter rmgroup.ListFilter) ([]*rmgroup.Head, int64, error) { + args := m.Called(ctx, filter) + var out []*rmgroup.Head + if v := args.Get(0); v != nil { + out = v.([]*rmgroup.Head) + } + return out, args.Get(1).(int64), args.Error(2) +} + +func (m *mockGroupRepo) UpdateHead(ctx context.Context, head *rmgroup.Head) error { + return m.Called(ctx, head).Error(0) +} + +func (m *mockGroupRepo) ListAllHeads(_ context.Context, _ *bool) ([]*rmgroup.Head, error) { + return nil, nil +} + +func (m *mockGroupRepo) SoftDeleteHead(ctx context.Context, id uuid.UUID, deletedBy string) error { + return m.Called(ctx, id, deletedBy).Error(0) +} + +func (m *mockGroupRepo) ExistsHeadByCode(ctx context.Context, code rmgroup.Code) (bool, error) { + args := m.Called(ctx, code) + return args.Bool(0), args.Error(1) +} + +func (m *mockGroupRepo) ExistsHeadByID(ctx context.Context, id uuid.UUID) (bool, error) { + args := m.Called(ctx, id) + return args.Bool(0), args.Error(1) +} + +func (m *mockGroupRepo) AddDetail(ctx context.Context, detail *rmgroup.Detail) error { + return m.Called(ctx, detail).Error(0) +} + +func (m *mockGroupRepo) UpdateDetail(ctx context.Context, detail *rmgroup.Detail) error { + return m.Called(ctx, detail).Error(0) +} + +func (m *mockGroupRepo) GetDetailByID(ctx context.Context, id uuid.UUID) (*rmgroup.Detail, error) { + args := m.Called(ctx, id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*rmgroup.Detail), args.Error(1) +} + +func (m *mockGroupRepo) GetActiveDetailByItemCodeGrade(ctx context.Context, itemCode rmgroup.ItemCode, gradeCode string) (*rmgroup.Detail, error) { + args := m.Called(ctx, itemCode, gradeCode) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*rmgroup.Detail), args.Error(1) +} + +func (m *mockGroupRepo) ListDetailsByHeadID(ctx context.Context, headID uuid.UUID) ([]*rmgroup.Detail, error) { + args := m.Called(ctx, headID) + var out []*rmgroup.Detail + if v := args.Get(0); v != nil { + out = v.([]*rmgroup.Detail) + } + return out, args.Error(1) +} + +func (m *mockGroupRepo) ListActiveDetailsByHeadID(ctx context.Context, headID uuid.UUID) ([]*rmgroup.Detail, error) { + args := m.Called(ctx, headID) + var out []*rmgroup.Detail + if v := args.Get(0); v != nil { + out = v.([]*rmgroup.Detail) + } + return out, args.Error(1) +} + +func (m *mockGroupRepo) SoftDeleteDetail(ctx context.Context, id uuid.UUID, deletedBy string) error { + return m.Called(ctx, id, deletedBy).Error(0) +} + +// mockSourceReader implements appcost.SourceDataReader. +type mockSourceReader struct{ mock.Mock } + +func (m *mockSourceReader) FetchRateInputs(ctx context.Context, period string, itemCodes []string) ([]rmcost.RateInputs, int, error) { + args := m.Called(ctx, period, itemCodes) + var out []rmcost.RateInputs + if v := args.Get(0); v != nil { + out = v.([]rmcost.RateInputs) + } + return out, args.Int(1), args.Error(2) +} + +func (m *mockSourceReader) FetchItemUOMs(ctx context.Context, period string, itemCodes []string) (map[string]string, error) { + args := m.Called(ctx, period, itemCodes) + var out map[string]string + if v := args.Get(0); v != nil { + out = v.(map[string]string) + } + return out, args.Error(1) +} diff --git a/services/finance/internal/application/rmcost/periods_handler.go b/services/finance/internal/application/rmcost/periods_handler.go new file mode 100644 index 0000000..068cab6 --- /dev/null +++ b/services/finance/internal/application/rmcost/periods_handler.go @@ -0,0 +1,27 @@ +package rmcost + +import ( + "context" + "fmt" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" +) + +// PeriodsHandler returns the set of distinct periods with cost rows. +type PeriodsHandler struct { + repo rmcost.Repository +} + +// NewPeriodsHandler builds a PeriodsHandler. +func NewPeriodsHandler(repo rmcost.Repository) *PeriodsHandler { + return &PeriodsHandler{repo: repo} +} + +// Handle returns distinct periods newest-first. +func (h *PeriodsHandler) Handle(ctx context.Context) ([]string, error) { + periods, err := h.repo.ListDistinctPeriods(ctx) + if err != nil { + return nil, fmt.Errorf("list distinct periods: %w", err) + } + return periods, nil +} diff --git a/services/finance/internal/application/rmcost/trigger_handler.go b/services/finance/internal/application/rmcost/trigger_handler.go new file mode 100644 index 0000000..83961e3 --- /dev/null +++ b/services/finance/internal/application/rmcost/trigger_handler.go @@ -0,0 +1,125 @@ +// Package rmcost provides application layer handlers for RM landed-cost calculation jobs. +package rmcost + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/application/oraclesync" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/job" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" +) + +// TriggerReason identifies why the calculation was requested. Maps to +// rmcost.HistoryTriggerReason on the worker side via the job params JSON. +type TriggerReason string + +// Trigger reasons — mirror rmcost.HistoryTriggerReason values. +const ( + TriggerOracleSyncChain TriggerReason = "oracle-sync-chain" + TriggerGroupUpdate TriggerReason = "group-update" + TriggerDetailChange TriggerReason = "detail-change" + TriggerManualUI TriggerReason = "manual-ui" +) + +// IsValid reports whether the trigger reason is one of the recognized values. +func (r TriggerReason) IsValid() bool { + switch r { + case TriggerOracleSyncChain, TriggerGroupUpdate, TriggerDetailChange, TriggerManualUI: + return true + default: + return false + } +} + +// TriggerCommand requests a landed-cost calculation job. When GroupHeadID is nil, +// the worker iterates every active group for the period. +type TriggerCommand struct { + Period string + GroupHeadID *uuid.UUID + Reason TriggerReason + CreatedBy string +} + +// TriggerResult holds the queued job handle. +type TriggerResult struct { + Execution *job.Execution +} + +// JobPublisher publishes RM cost calculation job messages to the queue. +type JobPublisher interface { + PublishRMCostCalculation(ctx context.Context, jobID, period string, groupHeadID *uuid.UUID, reason, createdBy string) error +} + +// TriggerHandler creates a job_execution row and publishes the job to RabbitMQ. +type TriggerHandler struct { + jobRepo job.Repository + publisher JobPublisher +} + +// NewTriggerHandler builds a TriggerHandler. +func NewTriggerHandler(jobRepo job.Repository, publisher JobPublisher) *TriggerHandler { + return &TriggerHandler{jobRepo: jobRepo, publisher: publisher} +} + +// Handle validates input, enforces period duplicate protection, persists the job +// record, and publishes to the queue. On publish failure the job is marked FAILED +// so operators see the error instead of a permanently-QUEUED row. +func (h *TriggerHandler) Handle(ctx context.Context, cmd TriggerCommand) (*TriggerResult, error) { + if h.publisher == nil { + return nil, fmt.Errorf("message queue unavailable: RabbitMQ not connected") + } + if cmd.CreatedBy == "" { + return nil, rmcost.ErrEmptyCreatedBy + } + reason := cmd.Reason + if reason == "" { + reason = TriggerManualUI + } + if !reason.IsValid() { + return nil, fmt.Errorf("invalid trigger reason %q", cmd.Reason) + } + + period := cmd.Period + if period == "" { + period = oraclesync.ResolvePeriod(time.Now()) + } + if err := rmcost.ValidatePeriod(period); err != nil { + return nil, err + } + + hasActive, err := h.jobRepo.HasActiveJob(ctx, job.TypeRMCostCalculation, period) + if err != nil { + return nil, fmt.Errorf("check active job: %w", err) + } + if hasActive { + return nil, job.ErrDuplicateActiveJob + } + + subtype := "all" + if cmd.GroupHeadID != nil { + subtype = cmd.GroupHeadID.String() + } + exec, err := job.NewExecution(job.TypeRMCostCalculation, subtype, period, cmd.CreatedBy, 5, nil) + if err != nil { + return nil, fmt.Errorf("create execution: %w", err) + } + if err := h.jobRepo.Create(ctx, exec); err != nil { + return nil, fmt.Errorf("persist job: %w", err) + } + + if err := h.publisher.PublishRMCostCalculation(ctx, exec.ID().String(), period, cmd.GroupHeadID, string(reason), cmd.CreatedBy); err != nil { + if failErr := exec.Fail("failed to publish to queue: " + err.Error()); failErr != nil { + return nil, fmt.Errorf("fail job after publish error: %w (publish: %w)", failErr, err) + } + if updateErr := h.jobRepo.UpdateStatus(ctx, exec); updateErr != nil { + return nil, fmt.Errorf("update failed status: %w (publish: %w)", updateErr, err) + } + return nil, fmt.Errorf("publish job: %w", err) + } + + return &TriggerResult{Execution: exec}, nil +} diff --git a/services/finance/internal/application/rmgroup/add_items_handler.go b/services/finance/internal/application/rmgroup/add_items_handler.go new file mode 100644 index 0000000..45f1753 --- /dev/null +++ b/services/finance/internal/application/rmgroup/add_items_handler.go @@ -0,0 +1,200 @@ +// Package rmgroup provides application layer handlers for RM group head and detail operations. +package rmgroup + +import ( + "context" + "errors" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// AddItemsCommand assigns a batch of raw-material item codes to an existing head. +// Per-item optional fields (name, uom, grade, market rows) can be supplied; when +// omitted, the detail is created with defaults and can be edited later. +type AddItemsCommand struct { + HeadID string + CreatedBy string + Items []AddItemInput +} + +// AddItemInput describes a single item to assign. +type AddItemInput struct { + ItemCode string + ItemName string + ItemTypeCode string + GradeCode string + ItemGrade string + UOMCode string + MarketPercentage *float64 + MarketValueRp *float64 + SortOrder int32 +} + +// AddItemsResult summarizes the outcome of an add-items call. +type AddItemsResult struct { + HeadID uuid.UUID + Added []*rmgroup.Detail + Skipped []SkippedItem +} + +// SkippedItem describes an item that could not be added, with the reason. +type SkippedItem struct { + ItemCode string + Reason string + OwningGroupID *uuid.UUID + OwningDetailID *uuid.UUID +} + +// AddItemsHandler handles AddItems commands. +type AddItemsHandler struct { + repo rmgroup.Repository +} + +// NewAddItemsHandler builds an AddItemsHandler. +func NewAddItemsHandler(repo rmgroup.Repository) *AddItemsHandler { + return &AddItemsHandler{repo: repo} +} + +// Handle enforces the "one item, one active group" invariant: any item already +// assigned to another active detail is reported in Skipped and not inserted. +// Items that already belong to THIS head are also skipped (idempotent re-add). +func (h *AddItemsHandler) Handle(ctx context.Context, cmd AddItemsCommand) (*AddItemsResult, error) { + if cmd.CreatedBy == "" { + return nil, rmgroup.ErrEmptyCreatedBy + } + headID, err := uuid.Parse(cmd.HeadID) + if err != nil { + return nil, rmgroup.ErrNotFound + } + + head, err := h.repo.GetHeadByID(ctx, headID) + if err != nil { + return nil, err + } + if head.IsDeleted() { + return nil, rmgroup.ErrAlreadyDeleted + } + + result := &AddItemsResult{HeadID: headID} + for i := range cmd.Items { + detail, skip, err := h.processItem(ctx, headID, cmd.CreatedBy, cmd.Items[i]) + if err != nil { + return nil, err + } + if skip != nil { + result.Skipped = append(result.Skipped, *skip) + continue + } + result.Added = append(result.Added, detail) + } + return result, nil +} + +// processItem validates a single item, checks cross-group ownership, and creates +// the detail. Returns (detail, nil, nil) on insert, (nil, skipped, nil) when the +// item is skipped, and (nil, nil, err) on fatal errors. +func (h *AddItemsHandler) processItem( //nolint:gocognit // sequential validation + ctx context.Context, + headID uuid.UUID, + createdBy string, + in AddItemInput, +) (*rmgroup.Detail, *SkippedItem, error) { + itemCode, err := rmgroup.NewItemCode(in.ItemCode) + if err != nil { + return nil, &SkippedItem{ItemCode: in.ItemCode, Reason: err.Error()}, nil //nolint:nilerr // skipped item IS the error report + } + + existing, err := h.repo.GetActiveDetailByItemCodeGrade(ctx, itemCode, in.GradeCode) + if err != nil && !errors.Is(err, rmgroup.ErrDetailNotFound) { + return nil, nil, fmt.Errorf("lookup active detail for %q: %w", in.ItemCode, err) + } + if existing != nil { //nolint:nestif // ownership check + owningGroup := existing.HeadID() + owningDetail := existing.ID() + reason := rmgroup.ErrItemAlreadyInOtherGroup.Error() + if owningGroup == headID { + reason = "item already assigned to this group" + // Backfill snapshot metadata if the existing detail has empty fields + // but the request now carries enriched data from the sync feed. + if needsBackfill(existing) && hasMetadata(in) { + if err := applyItemMetadata(existing, in, createdBy); err == nil { + if saveErr := h.repo.UpdateDetail(ctx, existing); saveErr != nil { + return nil, nil, fmt.Errorf("backfill detail for %q: %w", in.ItemCode, saveErr) + } + } + } + } + return nil, &SkippedItem{ + ItemCode: in.ItemCode, + Reason: reason, + OwningGroupID: &owningGroup, + OwningDetailID: &owningDetail, + }, nil + } + + detail, err := rmgroup.NewDetail(headID, itemCode, createdBy) + if err != nil { + return nil, nil, err + } + + if err := applyItemMetadata(detail, in, createdBy); err != nil { + return nil, nil, err + } + + if err := h.repo.AddDetail(ctx, detail); err != nil { + return nil, nil, fmt.Errorf("persist detail for %q: %w", in.ItemCode, err) + } + return detail, nil, nil +} + +// needsBackfill reports whether an existing detail has empty snapshot columns +// that could be filled in from the sync feed. +func needsBackfill(d *rmgroup.Detail) bool { + return d.ItemName() == "" || d.GradeCode() == "" || d.UOMCode() == "" +} + +// hasMetadata reports whether the incoming request carries any snapshot metadata +// (name/grade/uom) that would be worth applying to an existing detail. +func hasMetadata(in AddItemInput) bool { + return in.ItemName != "" || in.GradeCode != "" || in.ItemGrade != "" || in.UOMCode != "" +} + +func applyItemMetadata(detail *rmgroup.Detail, in AddItemInput, createdBy string) error { + upd := rmgroup.DetailUpdateInput{ + MarketPercentage: in.MarketPercentage, + MarketValueRp: in.MarketValueRp, + } + if in.ItemName != "" { + v := in.ItemName + upd.ItemName = &v + } + if in.ItemTypeCode != "" { + v := in.ItemTypeCode + upd.ItemTypeCode = &v + } + if in.GradeCode != "" { + v := in.GradeCode + upd.GradeCode = &v + } + if in.ItemGrade != "" { + v := in.ItemGrade + upd.ItemGrade = &v + } + if in.UOMCode != "" { + v := in.UOMCode + upd.UOMCode = &v + } + if in.SortOrder > 0 { + v := in.SortOrder + upd.SortOrder = &v + } + if upd.ItemName == nil && upd.ItemTypeCode == nil && upd.GradeCode == nil && + upd.ItemGrade == nil && upd.UOMCode == nil && upd.SortOrder == nil && + upd.MarketPercentage == nil && upd.MarketValueRp == nil { + return nil + } + return detail.Update(upd, createdBy) +} diff --git a/services/finance/internal/application/rmgroup/create_handler.go b/services/finance/internal/application/rmgroup/create_handler.go new file mode 100644 index 0000000..cd801bf --- /dev/null +++ b/services/finance/internal/application/rmgroup/create_handler.go @@ -0,0 +1,80 @@ +// Package rmgroup provides application layer handlers for RM group head and detail operations. +package rmgroup + +import ( + "context" + "errors" + "fmt" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// CreateCommand carries the inputs for creating a new RM group head. +// Flag selectors default to CONS at the domain layer and can be updated afterwards. +type CreateCommand struct { + Code string + Name string + Description string + Colorant string + CIName string + CostPercentage float64 + CostPerKg float64 + CreatedBy string +} + +// CreateHandler handles CreateHead commands. +type CreateHandler struct { + repo rmgroup.Repository +} + +// NewCreateHandler builds a CreateHandler. +func NewCreateHandler(repo rmgroup.Repository) *CreateHandler { + return &CreateHandler{repo: repo} +} + +// Handle validates the command, ensures the code is unique, and persists a new head. +func (h *CreateHandler) Handle(ctx context.Context, cmd CreateCommand) (*rmgroup.Head, error) { + code, err := rmgroup.NewCode(cmd.Code) + if err != nil { + return nil, err + } + + exists, err := h.repo.ExistsHeadByCode(ctx, code) + if err != nil { + return nil, fmt.Errorf("check head code uniqueness: %w", err) + } + if exists { + return nil, rmgroup.ErrCodeAlreadyExists + } + + head, err := rmgroup.NewHead(code, cmd.Name, cmd.Description, cmd.CostPercentage, cmd.CostPerKg, cmd.CreatedBy) + if err != nil { + return nil, err + } + + // Carry optional text fields and leading colorant/ciName via Update so the + // single code path enforces validation and audit stamping. + if cmd.Colorant != "" || cmd.CIName != "" { + in := rmgroup.UpdateInput{} + if cmd.Colorant != "" { + v := cmd.Colorant + in.Colorant = &v + } + if cmd.CIName != "" { + v := cmd.CIName + in.CIName = &v + } + if err := head.Update(in, cmd.CreatedBy); err != nil { + return nil, err + } + } + + if err := h.repo.CreateHead(ctx, head); err != nil { + if errors.Is(err, rmgroup.ErrCodeAlreadyExists) { + return nil, rmgroup.ErrCodeAlreadyExists + } + return nil, fmt.Errorf("persist head: %w", err) + } + + return head, nil +} diff --git a/services/finance/internal/application/rmgroup/delete_handler.go b/services/finance/internal/application/rmgroup/delete_handler.go new file mode 100644 index 0000000..47be86a --- /dev/null +++ b/services/finance/internal/application/rmgroup/delete_handler.go @@ -0,0 +1,71 @@ +// Package rmgroup provides application layer handlers for RM group head and detail operations. +package rmgroup + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// DeleteCommand is the soft-delete command for a head. The repository cascades the +// delete to every active detail belonging to the head. +type DeleteCommand struct { + HeadID string + DeletedBy string +} + +// CostChecker reports whether a group head has produced any cost rows. Kept as +// a narrow interface so the rmgroup application package does not import the +// rmcost domain directly. +type CostChecker interface { + ExistsForGroupHead(ctx context.Context, groupHeadID uuid.UUID) (bool, error) +} + +// DeleteHandler handles DeleteHead commands. +type DeleteHandler struct { + repo rmgroup.Repository + costChecker CostChecker +} + +// NewDeleteHandler builds a DeleteHandler. costChecker may be nil; when nil the +// delete-guard is skipped (used by tests that do not exercise cost state). +func NewDeleteHandler(repo rmgroup.Repository, costChecker CostChecker) *DeleteHandler { + return &DeleteHandler{repo: repo, costChecker: costChecker} +} + +// Handle soft-deletes the head and its active details. +func (h *DeleteHandler) Handle(ctx context.Context, cmd DeleteCommand) error { + if cmd.DeletedBy == "" { + return rmgroup.ErrEmptyUpdatedBy + } + id, err := uuid.Parse(cmd.HeadID) + if err != nil { + return rmgroup.ErrNotFound + } + + exists, err := h.repo.ExistsHeadByID(ctx, id) + if err != nil { + return fmt.Errorf("check head existence: %w", err) + } + if !exists { + return rmgroup.ErrNotFound + } + + if h.costChecker != nil { + hasCost, err := h.costChecker.ExistsForGroupHead(ctx, id) + if err != nil { + return fmt.Errorf("check cost data for group head: %w", err) + } + if hasCost { + return rmgroup.ErrGroupHasCostData + } + } + + if err := h.repo.SoftDeleteHead(ctx, id, cmd.DeletedBy); err != nil { + return fmt.Errorf("soft delete head: %w", err) + } + return nil +} diff --git a/services/finance/internal/application/rmgroup/export_handler.go b/services/finance/internal/application/rmgroup/export_handler.go new file mode 100644 index 0000000..c7520ae --- /dev/null +++ b/services/finance/internal/application/rmgroup/export_handler.go @@ -0,0 +1,213 @@ +package rmgroup + +import ( + "context" + "errors" + "fmt" + + "github.com/rs/zerolog/log" + "github.com/xuri/excelize/v2" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// ExportQuery is the query for exporting RM groups. +type ExportQuery struct { + // IsActive filters by active flag when non-nil. + IsActive *bool +} + +// ExportResult is the export bytes + filename. +type ExportResult struct { + FileContent []byte + FileName string +} + +// ExportHandler produces a 2-sheet Excel (Groups + Items) for all RM groups. +type ExportHandler struct { + repo rmgroup.Repository +} + +// NewExportHandler builds an ExportHandler. +func NewExportHandler(repo rmgroup.Repository) *ExportHandler { + return &ExportHandler{repo: repo} +} + +const ( + sheetGroups = "Groups" + sheetItems = "Items" +) + +var groupsHeaders = []string{ + "group_code", "group_name", "description", "colourant", "ci_name", + "cost_percentage", "cost_per_kg", + "flag_valuation", "flag_marketing", "flag_simulation", + "init_val_valuation", "init_val_marketing", "init_val_simulation", + "is_active", +} + +var itemsHeaders = []string{ + "group_code", "item_code", "grade_code", "sort_order", "is_active", "is_dummy", +} + +// Handle executes the export. +func (h *ExportHandler) Handle(ctx context.Context, q ExportQuery) (result *ExportResult, err error) { + heads, err := h.repo.ListAllHeads(ctx, q.IsActive) + if err != nil { + return nil, fmt.Errorf("list heads: %w", err) + } + + f := excelize.NewFile() + defer func() { + if cerr := f.Close(); cerr != nil { + log.Warn().Err(cerr).Msg("close excel") + if err == nil { + err = fmt.Errorf("close file: %w", cerr) + } + } + }() + + if err := buildGroupsSheet(f, heads); err != nil { + return nil, err + } + if err := buildItemsSheet(ctx, f, h.repo, heads); err != nil { + return nil, err + } + + // Remove default sheet and set active to first real sheet. + if delErr := f.DeleteSheet("Sheet1"); delErr != nil { + log.Debug().Err(delErr).Msg("delete default sheet") + } + if idx, idxErr := f.GetSheetIndex(sheetGroups); idxErr == nil { + f.SetActiveSheet(idx) + } + + buf, err := f.WriteToBuffer() + if err != nil { + return nil, fmt.Errorf("write buffer: %w", err) + } + + return &ExportResult{FileContent: buf.Bytes(), FileName: "rm_groups_export.xlsx"}, nil +} + +func buildGroupsSheet(f *excelize.File, heads []*rmgroup.Head) error { + if _, err := f.NewSheet(sheetGroups); err != nil { + return fmt.Errorf("new sheet %s: %w", sheetGroups, err) + } + if err := writeHeaderRow(f, sheetGroups, groupsHeaders); err != nil { + return err + } + var errs []error + for i, head := range heads { + row := i + 2 + vals := []any{ + head.Code().String(), + head.Name(), + head.Description(), + head.Colorant(), + head.CIName(), + head.CostPercentage(), + head.CostPerKg(), + head.FlagValuation().String(), + head.FlagMarketing().String(), + head.FlagSimulation().String(), + nilableFloat(head.InitValValuation()), + nilableFloat(head.InitValMarketing()), + nilableFloat(head.InitValSimulation()), + head.IsActive(), + } + if err := writeRow(f, sheetGroups, row, vals); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + log.Warn().Err(errors.Join(errs...)).Msg("groups sheet partial errors") + } + return nil +} + +func buildItemsSheet( + ctx context.Context, + f *excelize.File, + repo rmgroup.Repository, + heads []*rmgroup.Head, +) error { + if _, err := f.NewSheet(sheetItems); err != nil { + return fmt.Errorf("new sheet %s: %w", sheetItems, err) + } + if err := writeHeaderRow(f, sheetItems, itemsHeaders); err != nil { + return err + } + rowIdx := 2 + var errs []error + for _, head := range heads { + details, err := repo.ListDetailsByHeadID(ctx, head.ID()) + if err != nil { + return fmt.Errorf("list details for %s: %w", head.Code().String(), err) + } + for _, d := range details { + if d.IsDeleted() { + continue + } + vals := []any{ + head.Code().String(), + d.ItemCode().String(), + d.GradeCode(), + d.SortOrder(), + d.IsActive(), + d.IsDummy(), + } + if werr := writeRow(f, sheetItems, rowIdx, vals); werr != nil { + errs = append(errs, werr) + } + rowIdx++ + } + } + if len(errs) > 0 { + log.Warn().Err(errors.Join(errs...)).Msg("items sheet partial errors") + } + return nil +} + +func writeHeaderRow(f *excelize.File, sheet string, headers []string) error { + for col, h := range headers { + cell, err := excelize.CoordinatesToCellName(col+1, 1) + if err != nil { + return fmt.Errorf("cell name: %w", err) + } + if err := f.SetCellValue(sheet, cell, h); err != nil { + return fmt.Errorf("set header: %w", err) + } + } + style, err := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "FFFFFF"}, + Fill: excelize.Fill{Type: "pattern", Color: []string{"4472C4"}, Pattern: 1}, + }) + if err == nil { + lastCol, colErr := excelize.CoordinatesToCellName(len(headers), 1) + if colErr == nil { + _ = f.SetCellStyle(sheet, "A1", lastCol, style) //nolint:errcheck // best-effort styling + } + } + return nil +} + +func writeRow(f *excelize.File, sheet string, row int, values []any) error { + for col, v := range values { + cell, err := excelize.CoordinatesToCellName(col+1, row) + if err != nil { + return err + } + if err := f.SetCellValue(sheet, cell, v); err != nil { + return err + } + } + return nil +} + +func nilableFloat(v *float64) any { + if v == nil { + return "" + } + return *v +} diff --git a/services/finance/internal/application/rmgroup/get_handler.go b/services/finance/internal/application/rmgroup/get_handler.go new file mode 100644 index 0000000..35d285f --- /dev/null +++ b/services/finance/internal/application/rmgroup/get_handler.go @@ -0,0 +1,68 @@ +// Package rmgroup provides application layer handlers for RM group head and detail operations. +package rmgroup + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// GetQuery retrieves a head (and optionally its details) by ID. +type GetQuery struct { + HeadID string + WithDetails bool + ActiveOnly bool +} + +// GetResult bundles the head with optional detail rows. +type GetResult struct { + Head *rmgroup.Head + Details []*rmgroup.Detail +} + +// GetHandler handles GetHead queries. +type GetHandler struct { + repo rmgroup.Repository +} + +// NewGetHandler builds a GetHandler. +func NewGetHandler(repo rmgroup.Repository) *GetHandler { + return &GetHandler{repo: repo} +} + +// Handle returns the head and, when requested, its detail rows. Soft-deleted rows +// are omitted from the detail list regardless of ActiveOnly; ActiveOnly further +// restricts the result to rows where is_active = true. +func (h *GetHandler) Handle(ctx context.Context, query GetQuery) (*GetResult, error) { + id, err := uuid.Parse(query.HeadID) + if err != nil { + return nil, rmgroup.ErrNotFound + } + + head, err := h.repo.GetHeadByID(ctx, id) + if err != nil { + return nil, err + } + + result := &GetResult{Head: head} + if !query.WithDetails { + return result, nil + } + + details, err := h.loadDetails(ctx, id, query.ActiveOnly) + if err != nil { + return nil, fmt.Errorf("load details: %w", err) + } + result.Details = details + return result, nil +} + +func (h *GetHandler) loadDetails(ctx context.Context, headID uuid.UUID, activeOnly bool) ([]*rmgroup.Detail, error) { + if activeOnly { + return h.repo.ListActiveDetailsByHeadID(ctx, headID) + } + return h.repo.ListDetailsByHeadID(ctx, headID) +} diff --git a/services/finance/internal/application/rmgroup/handlers_test.go b/services/finance/internal/application/rmgroup/handlers_test.go new file mode 100644 index 0000000..5621c36 --- /dev/null +++ b/services/finance/internal/application/rmgroup/handlers_test.go @@ -0,0 +1,258 @@ +package rmgroup_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + appgroup "github.com/mutugading/goapps-backend/services/finance/internal/application/rmgroup" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/syncdata" +) + +func newHead(t *testing.T) *rmgroup.Head { + t.Helper() + code, err := rmgroup.NewCode("GRP-TEST") + require.NoError(t, err) + head, err := rmgroup.NewHead(code, "Test Group", "", 1.0, 10.0, "user:test") + require.NoError(t, err) + return head +} + +func TestCreateHandler_Success(t *testing.T) { + ctx := context.Background() + repo := new(mockRepo) + repo.On("ExistsHeadByCode", ctx, mock.AnythingOfType("rmgroup.Code")).Return(false, nil) + repo.On("CreateHead", ctx, mock.AnythingOfType("*rmgroup.Head")).Return(nil) + + h := appgroup.NewCreateHandler(repo) + out, err := h.Handle(ctx, appgroup.CreateCommand{ + Code: "PIG-001", + Name: "Pigment One", + CostPercentage: 1.1, + CostPerKg: 5.0, + CreatedBy: "user:test", + }) + require.NoError(t, err) + assert.Equal(t, "PIG-001", out.Code().String()) + repo.AssertExpectations(t) +} + +func TestCreateHandler_DuplicateCode(t *testing.T) { + ctx := context.Background() + repo := new(mockRepo) + repo.On("ExistsHeadByCode", ctx, mock.AnythingOfType("rmgroup.Code")).Return(true, nil) + + h := appgroup.NewCreateHandler(repo) + _, err := h.Handle(ctx, appgroup.CreateCommand{ + Code: "PIG-001", Name: "X", CostPercentage: 1, CostPerKg: 1, CreatedBy: "u", + }) + assert.ErrorIs(t, err, rmgroup.ErrCodeAlreadyExists) +} + +func TestCreateHandler_InvalidCode(t *testing.T) { + ctx := context.Background() + repo := new(mockRepo) + + h := appgroup.NewCreateHandler(repo) + _, err := h.Handle(ctx, appgroup.CreateCommand{Code: "", Name: "X", CreatedBy: "u"}) + assert.ErrorIs(t, err, rmgroup.ErrEmptyCode) + repo.AssertNotCalled(t, "ExistsHeadByCode") +} + +func TestGetHandler_InvalidID(t *testing.T) { + h := appgroup.NewGetHandler(new(mockRepo)) + _, err := h.Handle(context.Background(), appgroup.GetQuery{HeadID: "not-a-uuid"}) + assert.ErrorIs(t, err, rmgroup.ErrNotFound) +} + +func TestGetHandler_WithDetails(t *testing.T) { + ctx := context.Background() + head := newHead(t) + repo := new(mockRepo) + repo.On("GetHeadByID", ctx, head.ID()).Return(head, nil) + repo.On("ListActiveDetailsByHeadID", ctx, head.ID()).Return([]*rmgroup.Detail{}, nil) + + h := appgroup.NewGetHandler(repo) + res, err := h.Handle(ctx, appgroup.GetQuery{HeadID: head.ID().String(), WithDetails: true, ActiveOnly: true}) + require.NoError(t, err) + assert.Equal(t, head, res.Head) + repo.AssertExpectations(t) +} + +func TestListHandler_Pagination(t *testing.T) { + ctx := context.Background() + repo := new(mockRepo) + repo.On("ListHeads", ctx, mock.AnythingOfType("rmgroup.ListFilter")). + Return([]*rmgroup.Head{newHead(t)}, int64(25), nil) + + h := appgroup.NewListHandler(repo) + res, err := h.Handle(ctx, appgroup.ListQuery{Page: 2, PageSize: 10}) + require.NoError(t, err) + assert.Equal(t, int64(25), res.TotalItems) + assert.Equal(t, int32(3), res.TotalPages) + assert.Equal(t, int32(2), res.CurrentPage) +} + +func TestListHandler_InvalidFlag(t *testing.T) { + repo := new(mockRepo) + h := appgroup.NewListHandler(repo) + _, err := h.Handle(context.Background(), appgroup.ListQuery{Flag: "BOGUS"}) + assert.ErrorIs(t, err, rmgroup.ErrInvalidFlag) +} + +func TestUpdateHandler_Success(t *testing.T) { + ctx := context.Background() + head := newHead(t) + repo := new(mockRepo) + repo.On("GetHeadByID", ctx, head.ID()).Return(head, nil) + repo.On("UpdateHead", ctx, head).Return(nil) + + newName := "Updated Name" + h := appgroup.NewUpdateHandler(repo) + out, err := h.Handle(ctx, appgroup.UpdateCommand{ + HeadID: head.ID().String(), + Name: &newName, + UpdatedBy: "user:edit", + }) + require.NoError(t, err) + assert.Equal(t, "Updated Name", out.Name()) +} + +func TestUpdateHandler_InvalidFlag(t *testing.T) { + head := newHead(t) + repo := new(mockRepo) + repo.On("GetHeadByID", mock.Anything, head.ID()).Return(head, nil) + + bad := "BOGUS" + h := appgroup.NewUpdateHandler(repo) + _, err := h.Handle(context.Background(), appgroup.UpdateCommand{ + HeadID: head.ID().String(), + FlagValuation: &bad, + UpdatedBy: "user:edit", + }) + assert.ErrorIs(t, err, rmgroup.ErrInvalidFlag) +} + +func TestDeleteHandler_Success(t *testing.T) { + ctx := context.Background() + id := uuid.New() + repo := new(mockRepo) + repo.On("ExistsHeadByID", ctx, id).Return(true, nil) + repo.On("SoftDeleteHead", ctx, id, "user:del").Return(nil) + + h := appgroup.NewDeleteHandler(repo, nil) + err := h.Handle(ctx, appgroup.DeleteCommand{HeadID: id.String(), DeletedBy: "user:del"}) + require.NoError(t, err) + repo.AssertExpectations(t) +} + +func TestDeleteHandler_NotFound(t *testing.T) { + ctx := context.Background() + id := uuid.New() + repo := new(mockRepo) + repo.On("ExistsHeadByID", ctx, id).Return(false, nil) + + h := appgroup.NewDeleteHandler(repo, nil) + err := h.Handle(ctx, appgroup.DeleteCommand{HeadID: id.String(), DeletedBy: "u"}) + assert.ErrorIs(t, err, rmgroup.ErrNotFound) +} + +func TestAddItemsHandler_SkipsItemInAnotherGroup(t *testing.T) { + ctx := context.Background() + head := newHead(t) + repo := new(mockRepo) + repo.On("GetHeadByID", ctx, head.ID()).Return(head, nil) + + // Simulate item ABC-123 already held by a DIFFERENT group. + otherCode, _ := rmgroup.NewItemCode("ABC-123") + otherDetail, err := rmgroup.NewDetail(uuid.New(), otherCode, "user:old") + require.NoError(t, err) + repo.On("GetActiveDetailByItemCodeGrade", ctx, mock.AnythingOfType("rmgroup.ItemCode"), mock.AnythingOfType("string")). + Return(otherDetail, nil).Once() + + // A second item is free; should be created. + freeCode, _ := rmgroup.NewItemCode("FREE-9") + _ = freeCode + repo.On("GetActiveDetailByItemCodeGrade", ctx, mock.AnythingOfType("rmgroup.ItemCode"), mock.AnythingOfType("string")). + Return(nil, rmgroup.ErrDetailNotFound).Once() + repo.On("AddDetail", ctx, mock.AnythingOfType("*rmgroup.Detail")).Return(nil).Once() + + h := appgroup.NewAddItemsHandler(repo) + res, err := h.Handle(ctx, appgroup.AddItemsCommand{ + HeadID: head.ID().String(), + CreatedBy: "user:new", + Items: []appgroup.AddItemInput{ + {ItemCode: "ABC-123"}, + {ItemCode: "FREE-9"}, + }, + }) + require.NoError(t, err) + assert.Len(t, res.Added, 1) + assert.Len(t, res.Skipped, 1) + assert.Equal(t, "ABC-123", res.Skipped[0].ItemCode) +} + +func TestRemoveItemsHandler_SoftDelete(t *testing.T) { + ctx := context.Background() + head := newHead(t) + itemCode, _ := rmgroup.NewItemCode("X-1") + detail, err := rmgroup.NewDetail(head.ID(), itemCode, "user:test") + require.NoError(t, err) + + repo := new(mockRepo) + repo.On("GetHeadByID", ctx, head.ID()).Return(head, nil) + repo.On("GetDetailByID", ctx, detail.ID()).Return(detail, nil) + repo.On("SoftDeleteDetail", ctx, detail.ID(), "user:del").Return(nil) + + h := appgroup.NewRemoveItemsHandler(repo) + res, err := h.Handle(ctx, appgroup.RemoveItemsCommand{ + HeadID: head.ID().String(), + DetailIDs: []string{detail.ID().String()}, + Mode: appgroup.RemoveModeSoftDelete, + RemovedBy: "user:del", + }) + require.NoError(t, err) + assert.Len(t, res.Removed, 1) +} + +func TestRemoveItemsHandler_RejectsMismatchedHead(t *testing.T) { + ctx := context.Background() + head := newHead(t) + otherHeadID := uuid.New() + itemCode, _ := rmgroup.NewItemCode("Y-1") + // Detail belongs to otherHeadID, not head. + detail, err := rmgroup.NewDetail(otherHeadID, itemCode, "user:x") + require.NoError(t, err) + + repo := new(mockRepo) + repo.On("GetHeadByID", ctx, head.ID()).Return(head, nil) + repo.On("GetDetailByID", ctx, detail.ID()).Return(detail, nil) + + h := appgroup.NewRemoveItemsHandler(repo) + _, err = h.Handle(ctx, appgroup.RemoveItemsCommand{ + HeadID: head.ID().String(), + DetailIDs: []string{detail.ID().String()}, + RemovedBy: "user:del", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "does not belong") +} + +func TestUngroupedHandler_Pagination(t *testing.T) { + ctx := context.Background() + reader := new(mockUngroupedReader) + reader.On("ListUngroupedItems", ctx, mock.AnythingOfType("rmgroup.UngroupedItemsFilter")). + Return([]*syncdata.ItemConsStockPO{{ItemCode: "A"}}, int64(7), nil) + + h := appgroup.NewUngroupedHandler(reader) + res, err := h.Handle(ctx, appgroup.UngroupedQuery{Page: 1, PageSize: 5, Period: "202604"}) + require.NoError(t, err) + assert.Equal(t, int64(7), res.TotalItems) + assert.Equal(t, int32(2), res.TotalPages) + assert.Len(t, res.Items, 1) +} diff --git a/services/finance/internal/application/rmgroup/import_group_items_handler.go b/services/finance/internal/application/rmgroup/import_group_items_handler.go new file mode 100644 index 0000000..644ce7c --- /dev/null +++ b/services/finance/internal/application/rmgroup/import_group_items_handler.go @@ -0,0 +1,209 @@ +package rmgroup + +import ( + "bytes" + "context" + "errors" + "fmt" + + "github.com/google/uuid" + "github.com/xuri/excelize/v2" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// ImportGroupItemsCommand bulk-assigns items to a SINGLE existing group from a +// one-sheet Excel. Distinct from ImportHandler which owns the full list-page +// 2-sheet import. +type ImportGroupItemsCommand struct { + HeadID string + FileContent []byte + FileName string + CreatedBy string +} + +// ImportGroupItemsResult summarizes the outcome. +type ImportGroupItemsResult struct { + ItemsAdded int32 + ItemsSkipped int32 + FailedCount int32 + Errors []ImportError + Added []*rmgroup.Detail + Skipped []SkippedItem +} + +// ImportGroupItemsHandler parses a one-sheet Excel and delegates to +// AddItemsHandler so validation (one item / one active group, metadata +// backfill, sync-feed lookup) stays identical to the interactive add flow. +type ImportGroupItemsHandler struct { + addItems *AddItemsHandler + lookup ImportItemLookup +} + +// NewImportGroupItemsHandler builds an ImportGroupItemsHandler. +func NewImportGroupItemsHandler(addItems *AddItemsHandler, lookup ImportItemLookup) *ImportGroupItemsHandler { + return &ImportGroupItemsHandler{addItems: addItems, lookup: lookup} +} + +// Handle parses the "Items" sheet, enriches rows from the sync feed, and +// invokes AddItemsHandler. Rows with an unparseable item_code are recorded as +// errors; the AddItems handler's own Skipped output is propagated. +func (h *ImportGroupItemsHandler) Handle(ctx context.Context, cmd ImportGroupItemsCommand) (*ImportGroupItemsResult, error) { + if cmd.CreatedBy == "" { + return nil, rmgroup.ErrEmptyCreatedBy + } + if _, err := uuid.Parse(cmd.HeadID); err != nil { + return nil, rmgroup.ErrNotFound + } + + f, err := excelize.OpenReader(bytes.NewReader(cmd.FileContent)) + if err != nil { + return nil, fmt.Errorf("open excel: %w", err) + } + defer func() { + if closeErr := f.Close(); closeErr != nil { + _ = closeErr // best-effort + } + }() + + sheet, err := resolveItemsSheet(f) + if err != nil { + return nil, err + } + rows, err := f.GetRows(sheet) + if err != nil { + return nil, fmt.Errorf("read %s: %w", sheet, err) + } + + result := &ImportGroupItemsResult{} + inputs := h.parseRows(ctx, rows, result) + + if len(inputs) == 0 { + return result, nil + } + + addRes, err := h.addItems.Handle(ctx, AddItemsCommand{ + HeadID: cmd.HeadID, + CreatedBy: cmd.CreatedBy, + Items: inputs, + }) + if err != nil { + return nil, fmt.Errorf("add items: %w", err) + } + + result.Added = addRes.Added + result.Skipped = addRes.Skipped + result.ItemsAdded = safeLen32(len(addRes.Added)) + result.ItemsSkipped = safeLen32(len(addRes.Skipped)) + return result, nil +} + +// parseRows walks the items sheet (1 header row + data rows) and returns the +// AddItemInput list. Rows that fail to resolve item_code are recorded in +// result.Errors with FailedCount++. +func (h *ImportGroupItemsHandler) parseRows(ctx context.Context, rows [][]string, result *ImportGroupItemsResult) []AddItemInput { + if len(rows) < 2 { + return nil + } + inputs := make([]AddItemInput, 0, len(rows)-1) + for i, row := range rows[1:] { + rowNum := int32(i + 2) //nolint:gosec // row index bounded by Excel size + if isBlankRow(row) { + continue + } + in, err := h.buildInput(ctx, row) + if err != nil { + result.FailedCount++ + result.Errors = append(result.Errors, ImportError{ + RowNumber: rowNum, + Field: "item_code", + Message: err.Error(), + }) + continue + } + inputs = append(inputs, in) + } + return inputs +} + +func (h *ImportGroupItemsHandler) buildInput(ctx context.Context, row []string) (AddItemInput, error) { + itemCodeStr := colStr(row, 0) + if itemCodeStr == "" { + return AddItemInput{}, errors.New("item_code is required") + } + in := AddItemInput{ItemCode: itemCodeStr} + + if v := colStr(row, 1); v != "" { + in.GradeCode = v + } + if s := colStr(row, 2); s != "" { + // Re-use import_handler's column parsing via colOptBool for is_dummy, + // but sort_order parsing lives inline. + if n, ok := parseInt32(s); ok { + in.SortOrder = n + } + } + + // Enrich metadata from sync feed when available. Missing rows are still + // handed to AddItemsHandler so it can produce the correct "not in sync + // feed" skip reason — we do not reject here. + if h.lookup != nil { //nolint:nestif // enrichment block + if syncItem, err := h.lookup.GetItemByCode(ctx, itemCodeStr); err == nil && syncItem != nil { + if in.ItemName == "" { + in.ItemName = syncItem.ItemName + } + if in.GradeCode == "" { + in.GradeCode = syncItem.GradeCode + } + in.ItemGrade = syncItem.GradeName + in.UOMCode = syncItem.UOM + } + } + return in, nil +} + +func resolveItemsSheet(f *excelize.File) (string, error) { + sheets := f.GetSheetList() + if containsSheet(sheets, sheetItems) { + return sheetItems, nil + } + if len(sheets) == 0 { + return "", errors.New("workbook has no sheets") + } + // Fallback to the first sheet so operators don't need the exact tab name. + return sheets[0], nil +} + +func parseInt32(s string) (int32, bool) { + var n int32 + var neg bool + i := 0 + if len(s) > 0 && (s[0] == '-' || s[0] == '+') { + neg = s[0] == '-' + i = 1 + } + if i == len(s) { + return 0, false + } + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + return 0, false + } + n = n*10 + int32(c-'0') + } + if neg { + n = -n + } + return n, true +} + +func safeLen32(n int) int32 { + if n < 0 { + return 0 + } + if n > 2_147_483_647 { + return 2_147_483_647 + } + return int32(n) //nolint:gosec // bounds checked above +} diff --git a/services/finance/internal/application/rmgroup/import_handler.go b/services/finance/internal/application/rmgroup/import_handler.go new file mode 100644 index 0000000..99a019f --- /dev/null +++ b/services/finance/internal/application/rmgroup/import_handler.go @@ -0,0 +1,516 @@ +package rmgroup + +import ( + "bytes" + "context" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/xuri/excelize/v2" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/syncdata" +) + +// ImportCommand is the command for importing RM groups from Excel. +type ImportCommand struct { + FileContent []byte + FileName string + DuplicateAction string // "skip" or "update" + CreatedBy string +} + +// ImportError is a per-row error report. +type ImportError struct { + RowNumber int32 + Field string + Message string +} + +// ImportResult summarizes the outcome. +type ImportResult struct { + GroupsCreated int32 + GroupsUpdated int32 + GroupsSkipped int32 + ItemsAdded int32 + ItemsSkipped int32 + FailedCount int32 + Errors []ImportError +} + +// ImportItemLookup is the minimum contract needed to resolve item metadata +// during import. Implementations are provided by the sync repository. +type ImportItemLookup interface { + GetItemByCode(ctx context.Context, itemCode string) (*syncdata.ItemConsStockPO, error) +} + +// ImportHandler parses a 2-sheet Excel and upserts heads + details. +type ImportHandler struct { + repo rmgroup.Repository + itemLookup ImportItemLookup +} + +// NewImportHandler builds an ImportHandler. +func NewImportHandler(repo rmgroup.Repository, lookup ImportItemLookup) *ImportHandler { + return &ImportHandler{repo: repo, itemLookup: lookup} +} + +const ( + dupSkip = "skip" + dupUpdate = "update" +) + +// Handle executes the import. +func (h *ImportHandler) Handle(ctx context.Context, cmd ImportCommand) (*ImportResult, error) { + f, err := excelize.OpenReader(bytes.NewReader(cmd.FileContent)) + if err != nil { + return nil, fmt.Errorf("open excel: %w", err) + } + defer func() { + if closeErr := f.Close(); closeErr != nil { + _ = closeErr // best-effort + } + }() + + result := &ImportResult{} + + sheets := f.GetSheetList() + hasGroups := containsSheet(sheets, sheetGroups) + hasItems := containsSheet(sheets, sheetItems) + if !hasGroups && !hasItems { + return nil, fmt.Errorf("expected at least one of sheets %q or %q", sheetGroups, sheetItems) + } + + if hasGroups { + if err := h.importGroupsSheet(ctx, f, cmd, result); err != nil { + return nil, err + } + } + if hasItems { + if err := h.importItemsSheet(ctx, f, cmd, result); err != nil { + return nil, err + } + } + + return result, nil +} + +func (h *ImportHandler) importGroupsSheet( + ctx context.Context, + f *excelize.File, + cmd ImportCommand, + result *ImportResult, +) error { + rows, err := f.GetRows(sheetGroups) + if err != nil { + return fmt.Errorf("read %s: %w", sheetGroups, err) + } + if len(rows) < 2 { + return nil + } + + for i, row := range rows[1:] { + rowNum := int32(i + 2) //nolint:gosec // i is bounded by Excel row count + if isBlankRow(row) { + continue + } + if err := h.importGroupRow(ctx, row, rowNum, cmd, result); err != nil { + result.FailedCount++ + result.Errors = append(result.Errors, ImportError{ + RowNumber: rowNum, + Field: "", + Message: err.Error(), + }) + } + } + return nil +} + +func (h *ImportHandler) importGroupRow( + ctx context.Context, + row []string, + rowNum int32, + cmd ImportCommand, + result *ImportResult, +) error { + groupCode := colStr(row, 0) + if groupCode == "" { + return errors.New("group_code is required") + } + + code, err := rmgroup.NewCode(groupCode) + if err != nil { + return fmt.Errorf("group_code: %w", err) + } + + existing, err := h.repo.GetHeadByCode(ctx, code) + if err != nil && !errors.Is(err, rmgroup.ErrNotFound) { + return fmt.Errorf("lookup head: %w", err) + } + + if existing != nil { + return h.updateOrSkipHead(ctx, existing, row, cmd, result) + } + + return h.createHead(ctx, code, row, cmd.CreatedBy, rowNum, result) +} + +func (h *ImportHandler) updateOrSkipHead( + ctx context.Context, + existing *rmgroup.Head, + row []string, + cmd ImportCommand, + result *ImportResult, +) error { + if cmd.DuplicateAction == dupSkip { + result.GroupsSkipped++ + return nil + } + in, perr := parseGroupUpdateInput(row) + if perr != nil { + return perr + } + if err := existing.Update(in, cmd.CreatedBy); err != nil { + return fmt.Errorf("update head: %w", err) + } + if err := h.repo.UpdateHead(ctx, existing); err != nil { + return fmt.Errorf("persist update: %w", err) + } + result.GroupsUpdated++ + return nil +} + +func (h *ImportHandler) createHead( + ctx context.Context, + code rmgroup.Code, + row []string, + createdBy string, + _ int32, + result *ImportResult, +) error { + name := colStr(row, 1) + description := colStr(row, 2) + costPct, err := colFloat(row, 5) + if err != nil { + return fmt.Errorf("cost_percentage: %w", err) + } + costPerKg, err := colFloat(row, 6) + if err != nil { + return fmt.Errorf("cost_per_kg: %w", err) + } + + head, err := rmgroup.NewHead(code, name, description, costPct, costPerKg, createdBy) + if err != nil { + return fmt.Errorf("new head: %w", err) + } + + // Apply optional fields via Update (colorant, flags, inits, is_active). + in, perr := parseGroupUpdateInput(row) + if perr != nil { + return perr + } + // Name already set, unset to avoid re-validation. + in.Name = nil + in.CostPercentage = nil + in.CostPerKg = nil + if err := head.Update(in, createdBy); err != nil { + return fmt.Errorf("apply optional fields: %w", err) + } + + if err := h.repo.CreateHead(ctx, head); err != nil { + return fmt.Errorf("create head: %w", err) + } + result.GroupsCreated++ + return nil +} + +func parseGroupUpdateInput(row []string) (rmgroup.UpdateInput, error) { + in := rmgroup.UpdateInput{} + + if v := colStr(row, 1); v != "" { + in.Name = &v + } + if v := colStr(row, 2); v != "" { + in.Description = &v + } + if v := colStr(row, 3); v != "" { + in.Colorant = &v + } + if v := colStr(row, 4); v != "" { + in.CIName = &v + } + if v, ok, err := colOptFloat(row, 5); err != nil { + return in, fmt.Errorf("cost_percentage: %w", err) + } else if ok { + in.CostPercentage = &v + } + if v, ok, err := colOptFloat(row, 6); err != nil { + return in, fmt.Errorf("cost_per_kg: %w", err) + } else if ok { + in.CostPerKg = &v + } + + if err := parseFlagFields(row, &in); err != nil { + return in, err + } + if err := parseInitFields(row, &in); err != nil { + return in, err + } + + if v, ok := colOptBool(row, 13); ok { + in.IsActive = &v + } + return in, nil +} + +func parseFlagFields(row []string, in *rmgroup.UpdateInput) error { + if v := colStr(row, 7); v != "" { + f, err := rmgroup.ParseFlag(v) + if err != nil { + return fmt.Errorf("flag_valuation: %w", err) + } + in.FlagValuation = &f + } + if v := colStr(row, 8); v != "" { + f, err := rmgroup.ParseFlag(v) + if err != nil { + return fmt.Errorf("flag_marketing: %w", err) + } + in.FlagMarketing = &f + } + if v := colStr(row, 9); v != "" { + f, err := rmgroup.ParseFlag(v) + if err != nil { + return fmt.Errorf("flag_simulation: %w", err) + } + in.FlagSimulation = &f + } + return nil +} + +func parseInitFields(row []string, in *rmgroup.UpdateInput) error { + if v, ok, err := colOptFloat(row, 10); err != nil { + return fmt.Errorf("init_val_valuation: %w", err) + } else if ok { + in.InitValValuation = &v + } + if v, ok, err := colOptFloat(row, 11); err != nil { + return fmt.Errorf("init_val_marketing: %w", err) + } else if ok { + in.InitValMarketing = &v + } + if v, ok, err := colOptFloat(row, 12); err != nil { + return fmt.Errorf("init_val_simulation: %w", err) + } else if ok { + in.InitValSimulation = &v + } + return nil +} + +// ============================================================================= +// Items sheet +// ============================================================================= + +func (h *ImportHandler) importItemsSheet( + ctx context.Context, + f *excelize.File, + cmd ImportCommand, + result *ImportResult, +) error { + rows, err := f.GetRows(sheetItems) + if err != nil { + return fmt.Errorf("read %s: %w", sheetItems, err) + } + if len(rows) < 2 { + return nil + } + + for i, row := range rows[1:] { + rowNum := int32(i + 2) //nolint:gosec // i is bounded by Excel row count + if isBlankRow(row) { + continue + } + if err := h.importItemRow(ctx, row, cmd, result); err != nil { + result.FailedCount++ + result.Errors = append(result.Errors, ImportError{ + RowNumber: rowNum, + Field: "", + Message: err.Error(), + }) + } + } + return nil +} + +func (h *ImportHandler) importItemRow( //nolint:gocyclo,gocognit // sequential validation steps + ctx context.Context, + row []string, + cmd ImportCommand, + result *ImportResult, +) error { + groupCode := colStr(row, 0) + itemCodeStr := colStr(row, 1) + if groupCode == "" || itemCodeStr == "" { + return errors.New("group_code and item_code are required") + } + + code, err := rmgroup.NewCode(groupCode) + if err != nil { + return fmt.Errorf("group_code: %w", err) + } + head, err := h.repo.GetHeadByCode(ctx, code) + if err != nil { + if errors.Is(err, rmgroup.ErrNotFound) { + return fmt.Errorf("group_code %q not found", groupCode) + } + return fmt.Errorf("lookup group: %w", err) + } + + itemCode, err := rmgroup.NewItemCode(itemCodeStr) + if err != nil { + return fmt.Errorf("item_code: %w", err) + } + + // Skip if already active in the same group for the same (item, grade) + // variant. Use the grade from the row if provided, otherwise the synced + // grade. Items with different grade_codes are treated as independent + // variants per migration 000018. + gradeKey := colStr(row, 2) + if gradeKey == "" { + if synced, lookupErr := h.itemLookup.GetItemByCode(ctx, itemCodeStr); lookupErr == nil && synced != nil { + gradeKey = synced.GradeCode + } + } + existingDetail, err := h.repo.GetActiveDetailByItemCodeGrade(ctx, itemCode, gradeKey) + if err != nil && !errors.Is(err, rmgroup.ErrDetailNotFound) { + return fmt.Errorf("lookup active detail: %w", err) + } + if existingDetail != nil { + if existingDetail.HeadID() == head.ID() { + result.ItemsSkipped++ + return nil + } + return fmt.Errorf("item_code %q is already active in another group", itemCodeStr) + } + + // Look up metadata from sync feed. Missing items are rejected. + synced, err := h.itemLookup.GetItemByCode(ctx, itemCodeStr) + if err != nil { + return fmt.Errorf("lookup sync item: %w", err) + } + if synced == nil { + return fmt.Errorf("item_code %q not present in sync feed", itemCodeStr) + } + + detail, err := rmgroup.NewDetail(head.ID(), itemCode, cmd.CreatedBy) + if err != nil { + return fmt.Errorf("new detail: %w", err) + } + + // Apply sync metadata (name/grade/uom) via Update. + detailIn := rmgroup.DetailUpdateInput{ + ItemName: stringPtrIfNotEmpty(synced.ItemName), + GradeCode: stringPtrIfNotEmpty(synced.GradeCode), + ItemGrade: stringPtrIfNotEmpty(synced.GradeName), + UOMCode: stringPtrIfNotEmpty(synced.UOM), + } + applyItemRowOverrides(row, &detailIn) + + if err := detail.Update(detailIn, cmd.CreatedBy); err != nil { + return fmt.Errorf("apply detail fields: %w", err) + } + + if err := h.repo.AddDetail(ctx, detail); err != nil { + return fmt.Errorf("persist detail: %w", err) + } + result.ItemsAdded++ + return nil +} + +func applyItemRowOverrides(row []string, in *rmgroup.DetailUpdateInput) { + if v := colStr(row, 2); v != "" { + in.GradeCode = &v + } + if s := colStr(row, 3); s != "" { + if n, perr := strconv.ParseInt(s, 10, 32); perr == nil { + n32 := int32(n) //nolint:gosec // ParseInt bitsize 32 + in.SortOrder = &n32 + } + } + if v, ok := colOptBool(row, 4); ok { + in.IsActive = &v + } + if v, ok := colOptBool(row, 5); ok { + in.IsDummy = &v + } +} + +// ============================================================================= +// Helpers +// ============================================================================= + +func containsSheet(sheets []string, name string) bool { + for _, s := range sheets { + if s == name { + return true + } + } + return false +} + +func isBlankRow(row []string) bool { + for _, c := range row { + if strings.TrimSpace(c) != "" { + return false + } + } + return true +} + +func colStr(row []string, idx int) string { + if idx >= len(row) { + return "" + } + return strings.TrimSpace(row[idx]) +} + +func colFloat(row []string, idx int) (float64, error) { + s := colStr(row, idx) + if s == "" { + return 0, nil + } + return strconv.ParseFloat(s, 64) +} + +func colOptFloat(row []string, idx int) (float64, bool, error) { + s := colStr(row, idx) + if s == "" { + return 0, false, nil + } + v, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, false, err + } + return v, true, nil +} + +func colOptBool(row []string, idx int) (bool, bool) { + s := strings.ToLower(colStr(row, idx)) + switch s { + case "true", "1", "yes", "y": + return true, true + case "false", "0", "no", "n": + return false, true + default: + return false, false + } +} + +func stringPtrIfNotEmpty(s string) *string { + if s == "" { + return nil + } + return &s +} diff --git a/services/finance/internal/application/rmgroup/item_rates_handler.go b/services/finance/internal/application/rmgroup/item_rates_handler.go new file mode 100644 index 0000000..1cd293e --- /dev/null +++ b/services/finance/internal/application/rmgroup/item_rates_handler.go @@ -0,0 +1,78 @@ +// Package rmgroup provides application layer handlers for RM group head and detail operations. +package rmgroup + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// GroupItemRates is one row per active detail of a group, joined with the +// period's `cst_item_cons_stk_po` row (LEFT JOIN — missing sync rows produce +// zero rates with an empty Period). +type GroupItemRates struct { + ItemCode string + ItemName string + GradeCode string + ItemGrade string + UOMCode string + IsActive bool + IsDummy bool + Period string + ConsQty float64 + ConsVal float64 + ConsRate float64 + StoresQty float64 + StoresVal float64 + StoresRate float64 + DeptQty float64 + DeptVal float64 + DeptRate float64 + LastPOQty1 float64 + LastPOVal1 float64 + LastPORate1 float64 + LastPOQty2 float64 + LastPOVal2 float64 + LastPORate2 float64 + LastPOQty3 float64 + LastPOVal3 float64 + LastPORate3 float64 +} + +// GroupItemRatesReader exposes the join used by the per-item rates view on the +// group detail page. +type GroupItemRatesReader interface { + ListGroupItemRates(ctx context.Context, headID uuid.UUID, period string) ([]*GroupItemRates, error) +} + +// GroupItemRatesQuery is the input for the item-rates query. +type GroupItemRatesQuery struct { + HeadID string + Period string +} + +// GroupItemRatesHandler returns per-item stage rates for a group + period. +type GroupItemRatesHandler struct { + reader GroupItemRatesReader +} + +// NewGroupItemRatesHandler builds a GroupItemRatesHandler. +func NewGroupItemRatesHandler(reader GroupItemRatesReader) *GroupItemRatesHandler { + return &GroupItemRatesHandler{reader: reader} +} + +// Handle executes the query. +func (h *GroupItemRatesHandler) Handle(ctx context.Context, q GroupItemRatesQuery) ([]*GroupItemRates, error) { + id, err := uuid.Parse(q.HeadID) + if err != nil { + return nil, rmgroup.ErrNotFound + } + rows, err := h.reader.ListGroupItemRates(ctx, id, q.Period) + if err != nil { + return nil, fmt.Errorf("list group item rates: %w", err) + } + return rows, nil +} diff --git a/services/finance/internal/application/rmgroup/list_handler.go b/services/finance/internal/application/rmgroup/list_handler.go new file mode 100644 index 0000000..1b72325 --- /dev/null +++ b/services/finance/internal/application/rmgroup/list_handler.go @@ -0,0 +1,82 @@ +// Package rmgroup provides application layer handlers for RM group head and detail operations. +package rmgroup + +import ( + "context" + "fmt" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" + "github.com/mutugading/goapps-backend/services/finance/pkg/safeconv" +) + +// ListQuery is the paginated list query for heads. +type ListQuery struct { + Page int + PageSize int + Search string + IsActive *bool + Flag string + SortBy string + SortOrder string +} + +// ListResult is the paginated list result. +type ListResult struct { + Heads []*rmgroup.Head + TotalItems int64 + TotalPages int32 + CurrentPage int32 + PageSize int32 +} + +// ListHandler handles ListHeads queries. +type ListHandler struct { + repo rmgroup.Repository +} + +// NewListHandler builds a ListHandler. +func NewListHandler(repo rmgroup.Repository) *ListHandler { + return &ListHandler{repo: repo} +} + +// Handle executes the list query with pagination and filtering. An empty Flag is +// treated as "no flag filter"; otherwise the value must parse to a valid Flag. +func (h *ListHandler) Handle(ctx context.Context, query ListQuery) (*ListResult, error) { + filter := rmgroup.ListFilter{ + Search: query.Search, + IsActive: query.IsActive, + Page: query.Page, + PageSize: query.PageSize, + SortBy: query.SortBy, + SortOrder: query.SortOrder, + } + + if query.Flag != "" { + flag, err := rmgroup.ParseFlag(query.Flag) + if err != nil { + return nil, err + } + filter.Flag = flag + } + + filter.Validate() + + heads, total, err := h.repo.ListHeads(ctx, filter) + if err != nil { + return nil, fmt.Errorf("list heads: %w", err) + } + + var totalPages int32 + if filter.PageSize > 0 && total > 0 { + computed := (total + int64(filter.PageSize) - 1) / int64(filter.PageSize) + totalPages = safeconv.Int64ToInt32(computed) + } + + return &ListResult{ + Heads: heads, + TotalItems: total, + TotalPages: totalPages, + CurrentPage: safeconv.IntToInt32(filter.Page), + PageSize: safeconv.IntToInt32(filter.PageSize), + }, nil +} diff --git a/services/finance/internal/application/rmgroup/mocks_test.go b/services/finance/internal/application/rmgroup/mocks_test.go new file mode 100644 index 0000000..09174af --- /dev/null +++ b/services/finance/internal/application/rmgroup/mocks_test.go @@ -0,0 +1,133 @@ +package rmgroup_test + +import ( + "context" + + "github.com/google/uuid" + "github.com/stretchr/testify/mock" + + appgroup "github.com/mutugading/goapps-backend/services/finance/internal/application/rmgroup" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/syncdata" +) + +// mockRepo is a testify/mock implementation of rmgroup.Repository. +type mockRepo struct { + mock.Mock +} + +func (m *mockRepo) CreateHead(ctx context.Context, head *rmgroup.Head) error { + return m.Called(ctx, head).Error(0) +} + +func (m *mockRepo) GetHeadByID(ctx context.Context, id uuid.UUID) (*rmgroup.Head, error) { + args := m.Called(ctx, id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*rmgroup.Head), args.Error(1) +} + +func (m *mockRepo) GetHeadByCode(ctx context.Context, code rmgroup.Code) (*rmgroup.Head, error) { + args := m.Called(ctx, code) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*rmgroup.Head), args.Error(1) +} + +func (m *mockRepo) ListHeads(ctx context.Context, filter rmgroup.ListFilter) ([]*rmgroup.Head, int64, error) { + args := m.Called(ctx, filter) + var heads []*rmgroup.Head + if v := args.Get(0); v != nil { + heads = v.([]*rmgroup.Head) + } + return heads, args.Get(1).(int64), args.Error(2) +} + +func (m *mockRepo) UpdateHead(ctx context.Context, head *rmgroup.Head) error { + return m.Called(ctx, head).Error(0) +} + +func (m *mockRepo) ListAllHeads(ctx context.Context, activeFilter *bool) ([]*rmgroup.Head, error) { + args := m.Called(ctx, activeFilter) + var heads []*rmgroup.Head + if v := args.Get(0); v != nil { + heads = v.([]*rmgroup.Head) + } + return heads, args.Error(1) +} + +func (m *mockRepo) SoftDeleteHead(ctx context.Context, id uuid.UUID, deletedBy string) error { + return m.Called(ctx, id, deletedBy).Error(0) +} + +func (m *mockRepo) ExistsHeadByCode(ctx context.Context, code rmgroup.Code) (bool, error) { + args := m.Called(ctx, code) + return args.Bool(0), args.Error(1) +} + +func (m *mockRepo) ExistsHeadByID(ctx context.Context, id uuid.UUID) (bool, error) { + args := m.Called(ctx, id) + return args.Bool(0), args.Error(1) +} + +func (m *mockRepo) AddDetail(ctx context.Context, detail *rmgroup.Detail) error { + return m.Called(ctx, detail).Error(0) +} + +func (m *mockRepo) UpdateDetail(ctx context.Context, detail *rmgroup.Detail) error { + return m.Called(ctx, detail).Error(0) +} + +func (m *mockRepo) GetDetailByID(ctx context.Context, id uuid.UUID) (*rmgroup.Detail, error) { + args := m.Called(ctx, id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*rmgroup.Detail), args.Error(1) +} + +func (m *mockRepo) GetActiveDetailByItemCodeGrade(ctx context.Context, itemCode rmgroup.ItemCode, gradeCode string) (*rmgroup.Detail, error) { + args := m.Called(ctx, itemCode, gradeCode) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*rmgroup.Detail), args.Error(1) +} + +func (m *mockRepo) ListDetailsByHeadID(ctx context.Context, headID uuid.UUID) ([]*rmgroup.Detail, error) { + args := m.Called(ctx, headID) + var out []*rmgroup.Detail + if v := args.Get(0); v != nil { + out = v.([]*rmgroup.Detail) + } + return out, args.Error(1) +} + +func (m *mockRepo) ListActiveDetailsByHeadID(ctx context.Context, headID uuid.UUID) ([]*rmgroup.Detail, error) { + args := m.Called(ctx, headID) + var out []*rmgroup.Detail + if v := args.Get(0); v != nil { + out = v.([]*rmgroup.Detail) + } + return out, args.Error(1) +} + +func (m *mockRepo) SoftDeleteDetail(ctx context.Context, id uuid.UUID, deletedBy string) error { + return m.Called(ctx, id, deletedBy).Error(0) +} + +// mockUngroupedReader mocks appgroup.UngroupedItemsReader. +type mockUngroupedReader struct { + mock.Mock +} + +func (m *mockUngroupedReader) ListUngroupedItems(ctx context.Context, filter appgroup.UngroupedItemsFilter) ([]*syncdata.ItemConsStockPO, int64, error) { + args := m.Called(ctx, filter) + var items []*syncdata.ItemConsStockPO + if v := args.Get(0); v != nil { + items = v.([]*syncdata.ItemConsStockPO) + } + return items, args.Get(1).(int64), args.Error(2) +} diff --git a/services/finance/internal/application/rmgroup/remove_items_handler.go b/services/finance/internal/application/rmgroup/remove_items_handler.go new file mode 100644 index 0000000..6b865e8 --- /dev/null +++ b/services/finance/internal/application/rmgroup/remove_items_handler.go @@ -0,0 +1,134 @@ +// Package rmgroup provides application layer handlers for RM group head and detail operations. +package rmgroup + +import ( + "context" + "errors" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// RemoveMode controls how items are removed from a group. +type RemoveMode string + +// Remove modes. +const ( + // RemoveModeDeactivate sets is_active = false, keeping the detail row for audit. + RemoveModeDeactivate RemoveMode = "deactivate" + // RemoveModeSoftDelete sets deleted_at/deleted_by, hiding the row from most reads. + RemoveModeSoftDelete RemoveMode = "soft_delete" +) + +// RemoveItemsCommand removes a set of detail rows from a head. Detail IDs must +// belong to the same head — a mismatch fails the whole call before any row is +// mutated, so partial effects are impossible. +type RemoveItemsCommand struct { + HeadID string + DetailIDs []string + Mode RemoveMode + RemovedBy string +} + +// RemoveItemsResult reports the detail rows that were removed. +type RemoveItemsResult struct { + HeadID uuid.UUID + Removed []uuid.UUID + NotFound []uuid.UUID +} + +// RemoveItemsHandler handles RemoveItems commands. +type RemoveItemsHandler struct { + repo rmgroup.Repository +} + +// NewRemoveItemsHandler builds a RemoveItemsHandler. +func NewRemoveItemsHandler(repo rmgroup.Repository) *RemoveItemsHandler { + return &RemoveItemsHandler{repo: repo} +} + +// Handle validates input, loads each detail, verifies ownership, then removes +// per the configured mode. +func (h *RemoveItemsHandler) Handle(ctx context.Context, cmd RemoveItemsCommand) (*RemoveItemsResult, error) { + if cmd.RemovedBy == "" { + return nil, rmgroup.ErrEmptyUpdatedBy + } + mode := cmd.Mode + if mode == "" { + mode = RemoveModeSoftDelete + } + if mode != RemoveModeDeactivate && mode != RemoveModeSoftDelete { + return nil, fmt.Errorf("invalid remove mode %q", cmd.Mode) + } + + headID, err := uuid.Parse(cmd.HeadID) + if err != nil { + return nil, rmgroup.ErrNotFound + } + if _, err := h.repo.GetHeadByID(ctx, headID); err != nil { + return nil, err + } + + detailIDs, err := parseDetailIDs(cmd.DetailIDs) + if err != nil { + return nil, err + } + + result := &RemoveItemsResult{HeadID: headID} + for _, id := range detailIDs { + if err := h.removeOne(ctx, headID, id, mode, cmd.RemovedBy, result); err != nil { + return nil, err + } + } + return result, nil +} + +func parseDetailIDs(raw []string) ([]uuid.UUID, error) { + ids := make([]uuid.UUID, 0, len(raw)) + for _, s := range raw { + id, err := uuid.Parse(s) + if err != nil { + return nil, fmt.Errorf("invalid detail id %q: %w", s, err) + } + ids = append(ids, id) + } + return ids, nil +} + +func (h *RemoveItemsHandler) removeOne( + ctx context.Context, + headID, detailID uuid.UUID, + mode RemoveMode, + removedBy string, + result *RemoveItemsResult, +) error { + detail, err := h.repo.GetDetailByID(ctx, detailID) + if err != nil { + if errors.Is(err, rmgroup.ErrDetailNotFound) { + result.NotFound = append(result.NotFound, detailID) + return nil + } + return fmt.Errorf("get detail %s: %w", detailID, err) + } + if detail.HeadID() != headID { + return fmt.Errorf("detail %s does not belong to head %s", detailID, headID) + } + + switch mode { + case RemoveModeDeactivate: + if err := detail.Deactivate(removedBy); err != nil { + return err + } + if err := h.repo.UpdateDetail(ctx, detail); err != nil { + return fmt.Errorf("deactivate detail %s: %w", detailID, err) + } + case RemoveModeSoftDelete: + if err := h.repo.SoftDeleteDetail(ctx, detailID, removedBy); err != nil { + return fmt.Errorf("soft delete detail %s: %w", detailID, err) + } + } + result.Removed = append(result.Removed, detailID) + return nil +} diff --git a/services/finance/internal/application/rmgroup/template_handler.go b/services/finance/internal/application/rmgroup/template_handler.go new file mode 100644 index 0000000..3d8f8a5 --- /dev/null +++ b/services/finance/internal/application/rmgroup/template_handler.go @@ -0,0 +1,116 @@ +package rmgroup + +import ( + "fmt" + + "github.com/rs/zerolog/log" + "github.com/xuri/excelize/v2" +) + +// TemplateResult is the template bytes + filename. +type TemplateResult struct { + FileContent []byte + FileName string +} + +// TemplateHandler produces the blank 2-sheet import template. +type TemplateHandler struct{} + +// NewTemplateHandler builds a TemplateHandler. +func NewTemplateHandler() *TemplateHandler { + return &TemplateHandler{} +} + +// Handle returns a blank Excel template. +func (h *TemplateHandler) Handle() (result *TemplateResult, err error) { + f := excelize.NewFile() + defer func() { + if cerr := f.Close(); cerr != nil { + log.Warn().Err(cerr).Msg("close excel template") + if err == nil { + err = fmt.Errorf("close file: %w", cerr) + } + } + }() + + if _, serr := f.NewSheet(sheetGroups); serr != nil { + return nil, fmt.Errorf("new %s: %w", sheetGroups, serr) + } + if werr := writeHeaderRow(f, sheetGroups, groupsHeaders); werr != nil { + return nil, werr + } + + if _, serr := f.NewSheet(sheetItems); serr != nil { + return nil, fmt.Errorf("new %s: %w", sheetItems, serr) + } + if werr := writeHeaderRow(f, sheetItems, itemsHeaders); werr != nil { + return nil, werr + } + + if delErr := f.DeleteSheet("Sheet1"); delErr != nil { + log.Debug().Err(delErr).Msg("delete default sheet") + } + if idx, ierr := f.GetSheetIndex(sheetGroups); ierr == nil { + f.SetActiveSheet(idx) + } + + buf, err := f.WriteToBuffer() + if err != nil { + return nil, fmt.Errorf("write buffer: %w", err) + } + + return &TemplateResult{FileContent: buf.Bytes(), FileName: "rm_groups_template.xlsx"}, nil +} + +// Column order and header labels for the per-group items template. Matches +// what ImportGroupItemsHandler reads (item_code required, rest optional). +var groupItemsTemplateHeaders = []string{"item_code", "grade_code", "sort_order"} + +// GroupItemsTemplateResult mirrors TemplateResult — kept as its own type for +// clarity even though it has the same shape. +type GroupItemsTemplateResult = TemplateResult + +// GroupItemsTemplateHandler produces a blank one-sheet import template +// dedicated to the per-group items upload. +type GroupItemsTemplateHandler struct{} + +// NewGroupItemsTemplateHandler builds a GroupItemsTemplateHandler. +func NewGroupItemsTemplateHandler() *GroupItemsTemplateHandler { + return &GroupItemsTemplateHandler{} +} + +// Handle returns a blank template for per-group item import. +func (h *GroupItemsTemplateHandler) Handle() (result *GroupItemsTemplateResult, err error) { + f := excelize.NewFile() + defer func() { + if cerr := f.Close(); cerr != nil { + log.Warn().Err(cerr).Msg("close group items template") + if err == nil { + err = fmt.Errorf("close file: %w", cerr) + } + } + }() + + if _, serr := f.NewSheet(sheetItems); serr != nil { + return nil, fmt.Errorf("new %s: %w", sheetItems, serr) + } + if werr := writeHeaderRow(f, sheetItems, groupItemsTemplateHeaders); werr != nil { + return nil, werr + } + + if delErr := f.DeleteSheet("Sheet1"); delErr != nil { + log.Debug().Err(delErr).Msg("delete default sheet") + } + if idx, ierr := f.GetSheetIndex(sheetItems); ierr == nil { + f.SetActiveSheet(idx) + } + + buf, err := f.WriteToBuffer() + if err != nil { + return nil, fmt.Errorf("write buffer: %w", err) + } + return &GroupItemsTemplateResult{ + FileContent: buf.Bytes(), + FileName: "rm_group_items_template.xlsx", + }, nil +} diff --git a/services/finance/internal/application/rmgroup/ungrouped_export_handler.go b/services/finance/internal/application/rmgroup/ungrouped_export_handler.go new file mode 100644 index 0000000..3d14963 --- /dev/null +++ b/services/finance/internal/application/rmgroup/ungrouped_export_handler.go @@ -0,0 +1,135 @@ +package rmgroup + +import ( + "context" + "errors" + "fmt" + + "github.com/rs/zerolog/log" + "github.com/xuri/excelize/v2" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/syncdata" +) + +// UngroupedExportQuery filters the ungrouped-items export. +type UngroupedExportQuery struct { + Period string + Search string +} + +// UngroupedExportResult is the export bytes + filename. +type UngroupedExportResult struct { + FileContent []byte + FileName string +} + +// UngroupedExportHandler produces a single-sheet Excel of ungrouped items. It +// pages through the existing paginated reader to avoid introducing a new +// "list all" repo method. +type UngroupedExportHandler struct { + reader UngroupedItemsReader +} + +// NewUngroupedExportHandler builds an UngroupedExportHandler. +func NewUngroupedExportHandler(reader UngroupedItemsReader) *UngroupedExportHandler { + return &UngroupedExportHandler{reader: reader} +} + +const ( + ungroupedSheetName = "UngroupedItems" + ungroupedExportPageMax = 100 +) + +var ungroupedExportHeaders = []string{ + "period", "item_code", "grade_code", "grade_name", "item_name", "uom", + "cons_qty", "cons_val", "cons_rate", + "stores_qty", "stores_val", "stores_rate", + "dept_qty", "dept_val", "dept_rate", + "last_po_qty1", "last_po_val1", "last_po_rate1", + "last_po_qty2", "last_po_val2", "last_po_rate2", + "last_po_qty3", "last_po_val3", "last_po_rate3", +} + +// Handle executes the ungrouped-items export. +func (h *UngroupedExportHandler) Handle(ctx context.Context, q UngroupedExportQuery) (result *UngroupedExportResult, err error) { + items, err := h.collectAll(ctx, q) + if err != nil { + return nil, err + } + + f := excelize.NewFile() + defer func() { + if cerr := f.Close(); cerr != nil { + log.Warn().Err(cerr).Msg("close excel") + if err == nil { + err = fmt.Errorf("close file: %w", cerr) + } + } + }() + + if _, err := f.NewSheet(ungroupedSheetName); err != nil { + return nil, fmt.Errorf("new sheet: %w", err) + } + if err := writeHeaderRow(f, ungroupedSheetName, ungroupedExportHeaders); err != nil { + return nil, err + } + + var errs []error + for i, it := range items { + row := i + 2 + vals := []any{ + it.Period, it.ItemCode, it.GradeCode, it.GradeName, it.ItemName, it.UOM, + it.ConsQty, it.ConsVal, it.ConsRate, + it.StoresQty, it.StoresVal, it.StoresRate, + it.DeptQty, it.DeptVal, it.DeptRate, + it.LastPOQty1, it.LastPOVal1, it.LastPORate1, + it.LastPOQty2, it.LastPOVal2, it.LastPORate2, + it.LastPOQty3, it.LastPOVal3, it.LastPORate3, + } + if werr := writeRow(f, ungroupedSheetName, row, vals); werr != nil { + errs = append(errs, werr) + } + } + if len(errs) > 0 { + log.Warn().Err(errors.Join(errs...)).Msg("ungrouped export partial row errors") + } + + if delErr := f.DeleteSheet("Sheet1"); delErr != nil { + log.Debug().Err(delErr).Msg("delete default sheet") + } + if idx, idxErr := f.GetSheetIndex(ungroupedSheetName); idxErr == nil { + f.SetActiveSheet(idx) + } + + buf, err := f.WriteToBuffer() + if err != nil { + return nil, fmt.Errorf("write buffer: %w", err) + } + return &UngroupedExportResult{FileContent: buf.Bytes(), FileName: "ungrouped_items_export.xlsx"}, nil +} + +// collectAll walks the paginated reader until no more rows are returned. +// ListUngroupedItems caps page_size at 100, so this is how "list all" is +// expressed without adding a new repo method. +func (h *UngroupedExportHandler) collectAll(ctx context.Context, q UngroupedExportQuery) ([]*syncdata.ItemConsStockPO, error) { + var all []*syncdata.ItemConsStockPO + page := 1 + for { + filter := UngroupedItemsFilter{ + Period: q.Period, + Search: q.Search, + Page: page, + PageSize: ungroupedExportPageMax, + } + items, total, err := h.reader.ListUngroupedItems(ctx, filter) + if err != nil { + return nil, fmt.Errorf("list ungrouped page %d: %w", page, err) + } + all = append(all, items...) + if len(items) == 0 || int64(len(all)) >= total { + break + } + page++ + } + return all, nil +} diff --git a/services/finance/internal/application/rmgroup/ungrouped_handler.go b/services/finance/internal/application/rmgroup/ungrouped_handler.go new file mode 100644 index 0000000..1153b4e --- /dev/null +++ b/services/finance/internal/application/rmgroup/ungrouped_handler.go @@ -0,0 +1,96 @@ +// Package rmgroup provides application layer handlers for RM group head and detail operations. +package rmgroup + +import ( + "context" + "fmt" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/syncdata" + "github.com/mutugading/goapps-backend/services/finance/pkg/safeconv" +) + +// UngroupedItemsReader exposes the LEFT JOIN lookup used by the Ungrouped Items +// report: `cst_item_cons_stk_po LEFT JOIN cst_rm_group_detail` filtered to rows +// where no active (non-deleted) detail claims the item_code. The interface is +// defined here rather than on syncdata.PostgresTargetRepository because the join +// references a table from a different bounded context (rm grouping). +type UngroupedItemsReader interface { + ListUngroupedItems(ctx context.Context, filter UngroupedItemsFilter) ([]*syncdata.ItemConsStockPO, int64, error) +} + +// UngroupedItemsFilter scopes the ungrouped report. +type UngroupedItemsFilter struct { + // Period is optional; empty matches all periods. + Period string + // Search matches against item_code / item_name / grade_name. + Search string + Page int + PageSize int +} + +// Validate normalizes the filter with pagination defaults. +func (f *UngroupedItemsFilter) Validate() { + if f.Page < 1 { + f.Page = 1 + } + if f.PageSize < 1 { + f.PageSize = 20 + } + if f.PageSize > 100 { + f.PageSize = 100 + } +} + +// UngroupedQuery is the input for the ungrouped-items report. +type UngroupedQuery struct { + Period string + Search string + Page int + PageSize int +} + +// UngroupedResult is the paginated result. +type UngroupedResult struct { + Items []*syncdata.ItemConsStockPO + TotalItems int64 + TotalPages int32 + CurrentPage int32 + PageSize int32 +} + +// UngroupedHandler reports raw-material items that have been synced from Oracle +// but are not yet assigned to any active RM group — the seed list for operators +// deciding what to group next. +type UngroupedHandler struct { + reader UngroupedItemsReader +} + +// NewUngroupedHandler builds an UngroupedHandler. +func NewUngroupedHandler(reader UngroupedItemsReader) *UngroupedHandler { + return &UngroupedHandler{reader: reader} +} + +// Handle executes the ungrouped-items query. +func (h *UngroupedHandler) Handle(ctx context.Context, query UngroupedQuery) (*UngroupedResult, error) { + filter := UngroupedItemsFilter(query) + filter.Validate() + + items, total, err := h.reader.ListUngroupedItems(ctx, filter) + if err != nil { + return nil, fmt.Errorf("list ungrouped items: %w", err) + } + + var totalPages int32 + if filter.PageSize > 0 && total > 0 { + computed := (total + int64(filter.PageSize) - 1) / int64(filter.PageSize) + totalPages = safeconv.Int64ToInt32(computed) + } + + return &UngroupedResult{ + Items: items, + TotalItems: total, + TotalPages: totalPages, + CurrentPage: safeconv.IntToInt32(filter.Page), + PageSize: safeconv.IntToInt32(filter.PageSize), + }, nil +} diff --git a/services/finance/internal/application/rmgroup/update_handler.go b/services/finance/internal/application/rmgroup/update_handler.go new file mode 100644 index 0000000..4665e30 --- /dev/null +++ b/services/finance/internal/application/rmgroup/update_handler.go @@ -0,0 +1,120 @@ +// Package rmgroup provides application layer handlers for RM group head and detail operations. +package rmgroup + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// UpdateCommand is the partial-update command for a head. Pointer fields stay nil +// when the caller wants to leave them unchanged. The three ClearInitVal flags +// force the corresponding init_val columns to NULL. +type UpdateCommand struct { + HeadID string + + Name *string + Description *string + Colorant *string + CIName *string + CostPercentage *float64 + CostPerKg *float64 + + FlagValuation *string + FlagMarketing *string + FlagSimulation *string + + InitValValuation *float64 + InitValMarketing *float64 + InitValSimulation *float64 + + ClearInitValValuation bool + ClearInitValMarketing bool + ClearInitValSimulation bool + + IsActive *bool + + UpdatedBy string +} + +// UpdateHandler handles UpdateHead commands. +type UpdateHandler struct { + repo rmgroup.Repository +} + +// NewUpdateHandler builds an UpdateHandler. +func NewUpdateHandler(repo rmgroup.Repository) *UpdateHandler { + return &UpdateHandler{repo: repo} +} + +// Handle parses the ID, loads the head, applies the partial update, and persists. +func (h *UpdateHandler) Handle(ctx context.Context, cmd UpdateCommand) (*rmgroup.Head, error) { + id, err := uuid.Parse(cmd.HeadID) + if err != nil { + return nil, rmgroup.ErrNotFound + } + + head, err := h.repo.GetHeadByID(ctx, id) + if err != nil { + return nil, err + } + + in, err := buildHeadUpdateInput(cmd) + if err != nil { + return nil, err + } + if err := head.Update(in, cmd.UpdatedBy); err != nil { + return nil, err + } + + if err := h.repo.UpdateHead(ctx, head); err != nil { + return nil, fmt.Errorf("persist head update: %w", err) + } + return head, nil +} + +// buildHeadUpdateInput maps command pointers to the domain UpdateInput, parsing +// the three optional flag strings into typed Flag values. +func buildHeadUpdateInput(cmd UpdateCommand) (rmgroup.UpdateInput, error) { + in := rmgroup.UpdateInput{ + Name: cmd.Name, + Description: cmd.Description, + Colorant: cmd.Colorant, + CIName: cmd.CIName, + CostPercentage: cmd.CostPercentage, + CostPerKg: cmd.CostPerKg, + InitValValuation: cmd.InitValValuation, + InitValMarketing: cmd.InitValMarketing, + InitValSimulation: cmd.InitValSimulation, + ClearInitValValuation: cmd.ClearInitValValuation, + ClearInitValMarketing: cmd.ClearInitValMarketing, + ClearInitValSimulation: cmd.ClearInitValSimulation, + IsActive: cmd.IsActive, + } + + if err := assignFlag(&in.FlagValuation, cmd.FlagValuation); err != nil { + return in, err + } + if err := assignFlag(&in.FlagMarketing, cmd.FlagMarketing); err != nil { + return in, err + } + if err := assignFlag(&in.FlagSimulation, cmd.FlagSimulation); err != nil { + return in, err + } + return in, nil +} + +func assignFlag(target **rmgroup.Flag, raw *string) error { + if raw == nil { + return nil + } + flag, err := rmgroup.ParseFlag(*raw) + if err != nil { + return err + } + *target = &flag + return nil +} diff --git a/services/finance/internal/delivery/grpc/metrics.go b/services/finance/internal/delivery/grpc/metrics.go index 90d7a4e..d084d54 100644 --- a/services/finance/internal/delivery/grpc/metrics.go +++ b/services/finance/internal/delivery/grpc/metrics.go @@ -78,6 +78,22 @@ var ( []string{"operation", "status"}, ) + rmGroupOperationsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "rm_group_operations_total", + Help: "Total number of RM Group operations", + }, + []string{"operation", "status"}, + ) + + rmCostOperationsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "rm_cost_operations_total", + Help: "Total number of RM Cost operations", + }, + []string{"operation", "status"}, + ) + cacheHitsTotal = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "cache_hits_total", diff --git a/services/finance/internal/delivery/grpc/rm_cost_handler.go b/services/finance/internal/delivery/grpc/rm_cost_handler.go new file mode 100644 index 0000000..049d348 --- /dev/null +++ b/services/finance/internal/delivery/grpc/rm_cost_handler.go @@ -0,0 +1,457 @@ +package grpc + +import ( + "context" + "time" + + "github.com/google/uuid" + + commonv1 "github.com/mutugading/goapps-backend/gen/common/v1" + financev1 "github.com/mutugading/goapps-backend/gen/finance/v1" + apprmcost "github.com/mutugading/goapps-backend/services/finance/internal/application/rmcost" + rmcostdomain "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" + rmgroupdomain "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// RMCostHandler implements the RMCostServiceServer interface. +type RMCostHandler struct { + financev1.UnimplementedRMCostServiceServer + triggerHandler *apprmcost.TriggerHandler + calculateHandler *apprmcost.CalculateHandler + getHandler *apprmcost.GetHandler + listHandler *apprmcost.ListHandler + historyHandler *apprmcost.HistoryHandler + periodsHandler *apprmcost.PeriodsHandler + exportHandler *apprmcost.ExportHandler + validationHelper *ValidationHelper +} + +// NewRMCostHandler builds an RMCostHandler. +func NewRMCostHandler( + trigger *apprmcost.TriggerHandler, + calculate *apprmcost.CalculateHandler, + get *apprmcost.GetHandler, + list *apprmcost.ListHandler, + history *apprmcost.HistoryHandler, + periods *apprmcost.PeriodsHandler, + export *apprmcost.ExportHandler, +) (*RMCostHandler, error) { + v, err := NewValidationHelper() + if err != nil { + return nil, err + } + return &RMCostHandler{ + triggerHandler: trigger, + calculateHandler: calculate, + getHandler: get, + listHandler: list, + historyHandler: history, + periodsHandler: periods, + exportHandler: export, + validationHelper: v, + }, nil +} + +// TriggerRMCostCalculation enqueues an async calculation job. +func (h *RMCostHandler) TriggerRMCostCalculation(ctx context.Context, req *financev1.TriggerRMCostCalculationRequest) (*financev1.TriggerRMCostCalculationResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMCostOperation(opTrigger, false) + return &financev1.TriggerRMCostCalculationResponse{Base: baseResp}, nil + } + + cmd := apprmcost.TriggerCommand{ + Period: req.Period, + Reason: apprmcost.TriggerReason(triggerReasonToString(req.TriggerReason)), + CreatedBy: getUserFromContext(ctx), + } + if gid, badResp := parseOptionalGroupHeadID(req.GroupHeadId); badResp != nil { + RecordRMCostOperation(opTrigger, false) + return &financev1.TriggerRMCostCalculationResponse{Base: badResp}, nil + } else if gid != nil { + cmd.GroupHeadID = gid + } + + result, err := h.triggerHandler.Handle(ctx, cmd) + if err != nil { + RecordRMCostOperation(opTrigger, false) + return &financev1.TriggerRMCostCalculationResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + RecordRMCostOperation(opTrigger, true) + return &financev1.TriggerRMCostCalculationResponse{ + Base: successResponse("RM cost calculation enqueued"), + JobId: result.Execution.ID().String(), + }, nil +} + +// CalculateRMCost runs the calculation synchronously. +func (h *RMCostHandler) CalculateRMCost(ctx context.Context, req *financev1.CalculateRMCostRequest) (*financev1.CalculateRMCostResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMCostOperation(opCalculate, false) + return &financev1.CalculateRMCostResponse{Base: baseResp}, nil + } + + cmd := apprmcost.CalculateCommand{ + Period: req.Period, + TriggerReason: rmcostdomain.HistoryTriggerReason(triggerReasonToString(req.TriggerReason)), + CalculatedBy: getUserFromContext(ctx), + } + if gid, badResp := parseOptionalGroupHeadID(req.GroupHeadId); badResp != nil { + RecordRMCostOperation(opCalculate, false) + return &financev1.CalculateRMCostResponse{Base: badResp}, nil + } else if gid != nil { + cmd.GroupHeadID = gid + } + + result, err := h.calculateHandler.Handle(ctx, cmd) + if err != nil { + RecordRMCostOperation(opCalculate, false) + return &financev1.CalculateRMCostResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + RecordRMCostOperation(opCalculate, true) + return &financev1.CalculateRMCostResponse{ + Base: successResponse("RM cost calculated"), + Processed: safeIntToInt32(result.Processed), + Skipped: safeIntToInt32(result.Skipped), + Period: result.Period, + }, nil +} + +// GetRMCost fetches a single cost row by (period, rm_code). +func (h *RMCostHandler) GetRMCost(ctx context.Context, req *financev1.GetRMCostRequest) (*financev1.GetRMCostResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMCostOperation(opGet, false) + return &financev1.GetRMCostResponse{Base: baseResp}, nil + } + + cost, err := h.getHandler.Handle(ctx, apprmcost.GetQuery{ + Period: req.Period, + RMCode: req.RmCode, + }) + if err != nil { + RecordRMCostOperation(opGet, false) + return &financev1.GetRMCostResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + RecordRMCostOperation(opGet, true) + return &financev1.GetRMCostResponse{ + Base: successResponse(msgRMCostRetrieved), + Data: rmCostToProto(cost), + }, nil +} + +// ListRMCosts returns a paginated list of cost rows. +func (h *RMCostHandler) ListRMCosts(ctx context.Context, req *financev1.ListRMCostsRequest) (*financev1.ListRMCostsResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMCostOperation(opList, false) + return &financev1.ListRMCostsResponse{Base: baseResp}, nil + } + + query := apprmcost.ListQuery{ + Page: int(req.Page), + PageSize: int(req.PageSize), + Period: req.Period, + RMType: rmTypeToString(req.RmType), + Search: req.Search, + SortBy: req.SortBy, + SortOrder: req.SortOrder, + } + if req.GroupHeadId != nil { + query.GroupHeadID = *req.GroupHeadId + } + + result, err := h.listHandler.Handle(ctx, query) + if err != nil { + RecordRMCostOperation(opList, false) + return &financev1.ListRMCostsResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + items := make([]*financev1.RMCost, len(result.Costs)) + for i, c := range result.Costs { + items[i] = rmCostToProto(c) + } + + RecordRMCostOperation(opList, true) + return &financev1.ListRMCostsResponse{ + Base: successResponse("RM costs retrieved successfully"), + Data: items, + Pagination: &commonv1.PaginationResponse{ + CurrentPage: result.CurrentPage, + PageSize: result.PageSize, + TotalItems: result.TotalItems, + TotalPages: result.TotalPages, + }, + }, nil +} + +// ListRMCostHistory returns a paginated list of history rows. +func (h *RMCostHandler) ListRMCostHistory(ctx context.Context, req *financev1.ListRMCostHistoryRequest) (*financev1.ListRMCostHistoryResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMCostOperation(opListHistory, false) + return &financev1.ListRMCostHistoryResponse{Base: baseResp}, nil + } + + query := apprmcost.HistoryQuery{ + Page: int(req.Page), + PageSize: int(req.PageSize), + Period: req.Period, + RMCode: req.RmCode, + } + if req.GroupHeadId != nil { + query.GroupHeadID = *req.GroupHeadId + } + if req.JobId != nil { + query.JobID = *req.JobId + } + + result, err := h.historyHandler.Handle(ctx, query) + if err != nil { + RecordRMCostOperation(opListHistory, false) + return &financev1.ListRMCostHistoryResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + items := make([]*financev1.RMCostHistory, len(result.Rows)) + for i := range result.Rows { + items[i] = rmCostHistoryToProto(&result.Rows[i]) + } + + RecordRMCostOperation(opListHistory, true) + return &financev1.ListRMCostHistoryResponse{ + Base: successResponse("RM cost history retrieved successfully"), + Data: items, + Pagination: &commonv1.PaginationResponse{ + CurrentPage: result.CurrentPage, + PageSize: result.PageSize, + TotalItems: result.TotalItems, + TotalPages: result.TotalPages, + }, + }, nil +} + +// ListRMCostPeriods returns distinct periods from cost rows. +func (h *RMCostHandler) ListRMCostPeriods(ctx context.Context, _ *financev1.ListRMCostPeriodsRequest) (*financev1.ListRMCostPeriodsResponse, error) { + periods, err := h.periodsHandler.Handle(ctx) + if err != nil { + return &financev1.ListRMCostPeriodsResponse{Base: domainErrorToBaseResponse(err)}, nil + } + return &financev1.ListRMCostPeriodsResponse{ + Base: successResponse("RM cost periods retrieved successfully"), + Periods: periods, + }, nil +} + +// ExportRMCosts exports cost rows matching the filter to a single-sheet Excel. +func (h *RMCostHandler) ExportRMCosts(ctx context.Context, req *financev1.ExportRMCostsRequest) (*financev1.ExportRMCostsResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMCostOperation(opExport, false) + return &financev1.ExportRMCostsResponse{Base: baseResp}, nil + } + + query := apprmcost.ExportQuery{ + Period: req.Period, + RMType: rmcostdomain.RMType(rmTypeToString(req.RmType)), + Search: req.Search, + } + if gid, badResp := parseOptionalGroupHeadID(req.GroupHeadId); badResp != nil { + RecordRMCostOperation(opExport, false) + return &financev1.ExportRMCostsResponse{Base: badResp}, nil + } else if gid != nil { + query.GroupHeadID = gid + } + + result, err := h.exportHandler.Handle(ctx, query) + if err != nil { + RecordRMCostOperation(opExport, false) + return &financev1.ExportRMCostsResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + RecordRMCostOperation(opExport, true) + return &financev1.ExportRMCostsResponse{ + Base: successResponse("RM costs exported successfully"), + FileContent: result.FileContent, + FileName: result.FileName, + }, nil +} + +// ============================================================================= +// Entity → proto mappers +// ============================================================================= + +func rmCostToProto(c *rmcostdomain.Cost) *financev1.RMCost { + out := &financev1.RMCost{ + RmCostId: c.ID().String(), + Period: c.Period(), + RmCode: c.RMCode(), + RmType: rmTypeToProto(c.RMType()), + RmName: c.RMName(), + UomCode: c.UOMCode(), + Rates: rmCostRatesToProto(c.Rates()), + CostValuation: c.CostValuation(), + CostMarketing: c.CostMarketing(), + CostSimulation: c.CostSimulation(), + FlagValuation: stageToProto(c.FlagValuation()), + FlagMarketing: stageToProto(c.FlagMarketing()), + FlagSimulation: stageToProto(c.FlagSimulation()), + FlagValuationUsed: stageToProto(c.FlagValuationUsed()), + FlagMarketingUsed: stageToProto(c.FlagMarketingUsed()), + FlagSimulationUsed: stageToProto(c.FlagSimulationUsed()), + Audit: &commonv1.AuditInfo{ + CreatedAt: c.CreatedAt().Format(time.RFC3339), + CreatedBy: c.CreatedBy(), + }, + } + if id := c.GroupHeadID(); id != nil { + s := id.String() + out.GroupHeadId = &s + } + if code := c.ItemCode(); code != nil { + s := *code + out.ItemCode = &s + } + if t := c.CalculatedAt(); t != nil { + out.CalculatedAt = t.Format(time.RFC3339) + } + if by := c.CalculatedBy(); by != nil { + out.CalculatedBy = *by + } + if t := c.UpdatedAt(); t != nil { + out.Audit.UpdatedAt = t.Format(time.RFC3339) + } + if by := c.UpdatedBy(); by != nil { + out.Audit.UpdatedBy = *by + } + return out +} + +func rmCostHistoryToProto(h *rmcostdomain.History) *financev1.RMCostHistory { + out := &financev1.RMCostHistory{ + HistoryId: h.ID.String(), + Period: h.Period, + RmCode: h.RMCode, + RmType: rmTypeToProto(h.RMType), + Rates: rmCostRatesToProto(h.Rates), + CostPercentage: h.CostPercentage, + CostPerKg: h.CostPerKg, + FlagValuation: stageToProto(h.FlagValuation), + FlagMarketing: stageToProto(h.FlagMarketing), + FlagSimulation: stageToProto(h.FlagSimulation), + InitValValuation: h.InitValValuation, + InitValMarketing: h.InitValMarketing, + InitValSimulation: h.InitValSimulation, + CostValuation: h.CostValuation, + CostMarketing: h.CostMarketing, + CostSimulation: h.CostSimulation, + FlagValuationUsed: stageToProto(h.FlagValuationUsed), + FlagMarketingUsed: stageToProto(h.FlagMarketingUsed), + FlagSimulationUsed: stageToProto(h.FlagSimulationUsed), + SourceItemCount: safeIntToInt32(h.SourceItemCount), + TriggerReason: triggerReasonToProto(h.TriggerReason), + CalculatedAt: h.CalculatedAt.Format(time.RFC3339), + CalculatedBy: h.CalculatedBy, + } + if h.RMCostID != nil { + s := h.RMCostID.String() + out.RmCostId = &s + } + if h.JobID != nil { + s := h.JobID.String() + out.JobId = &s + } + if h.GroupHeadID != nil { + s := h.GroupHeadID.String() + out.GroupHeadId = &s + } + return out +} + +func rmCostRatesToProto(r rmcostdomain.StageRates) *financev1.RMCostRates { + return &financev1.RMCostRates{ + Cons: r.Cons, + Stores: r.Stores, + Dept: r.Dept, + Po_1: r.PO1, + Po_2: r.PO2, + Po_3: r.PO3, + } +} + +// stageToProto reuses flagToProto by casting Stage (same underlying string domain). +func stageToProto(s rmcostdomain.Stage) financev1.RMGroupFlag { + return flagToProto(rmgroupdomain.Flag(s)) +} + +func rmTypeToProto(t rmcostdomain.RMType) financev1.RMCostType { + switch t { + case rmcostdomain.RMTypeGroup: + return financev1.RMCostType_RM_COST_TYPE_GROUP + case rmcostdomain.RMTypeItem: + return financev1.RMCostType_RM_COST_TYPE_ITEM + default: + return financev1.RMCostType_RM_COST_TYPE_UNSPECIFIED + } +} + +func rmTypeToString(t financev1.RMCostType) string { + switch t { + case financev1.RMCostType_RM_COST_TYPE_GROUP: + return string(rmcostdomain.RMTypeGroup) + case financev1.RMCostType_RM_COST_TYPE_ITEM: + return string(rmcostdomain.RMTypeItem) + case financev1.RMCostType_RM_COST_TYPE_UNSPECIFIED: + return "" + default: + return "" + } +} + +func triggerReasonToString(r financev1.RMCostTriggerReason) string { + switch r { + case financev1.RMCostTriggerReason_RM_COST_TRIGGER_REASON_ORACLE_SYNC_CHAIN: + return string(rmcostdomain.TriggerOracleSyncChain) + case financev1.RMCostTriggerReason_RM_COST_TRIGGER_REASON_GROUP_UPDATE: + return string(rmcostdomain.TriggerGroupUpdate) + case financev1.RMCostTriggerReason_RM_COST_TRIGGER_REASON_DETAIL_CHANGE: + return string(rmcostdomain.TriggerDetailChange) + case financev1.RMCostTriggerReason_RM_COST_TRIGGER_REASON_MANUAL_UI: + return string(rmcostdomain.TriggerManualUI) + case financev1.RMCostTriggerReason_RM_COST_TRIGGER_REASON_UNSPECIFIED: + return "" + default: + return "" + } +} + +func triggerReasonToProto(r rmcostdomain.HistoryTriggerReason) financev1.RMCostTriggerReason { + switch r { + case rmcostdomain.TriggerOracleSyncChain: + return financev1.RMCostTriggerReason_RM_COST_TRIGGER_REASON_ORACLE_SYNC_CHAIN + case rmcostdomain.TriggerGroupUpdate: + return financev1.RMCostTriggerReason_RM_COST_TRIGGER_REASON_GROUP_UPDATE + case rmcostdomain.TriggerDetailChange: + return financev1.RMCostTriggerReason_RM_COST_TRIGGER_REASON_DETAIL_CHANGE + case rmcostdomain.TriggerManualUI: + return financev1.RMCostTriggerReason_RM_COST_TRIGGER_REASON_MANUAL_UI + default: + return financev1.RMCostTriggerReason_RM_COST_TRIGGER_REASON_UNSPECIFIED + } +} + +// RecordRMCostOperation records an RM Cost operation metric. +func RecordRMCostOperation(operation string, success bool) { + rmCostOperationsTotal.WithLabelValues(operation, metricStatus(success)).Inc() +} + +// parseOptionalGroupHeadID parses an optional *string group_head_id into a +// *uuid.UUID. Returns (nil, nil) when the input is nil or empty. Returns +// (nil, baseResponse) when the input is non-empty but invalid. +func parseOptionalGroupHeadID(raw *string) (*uuid.UUID, *commonv1.BaseResponse) { + if raw == nil || *raw == "" { + return nil, nil + } + id, err := uuid.Parse(*raw) + if err != nil { + return nil, ErrorResponse("400", "invalid group_head_id: "+err.Error()) + } + return &id, nil +} diff --git a/services/finance/internal/delivery/grpc/rm_group_handler.go b/services/finance/internal/delivery/grpc/rm_group_handler.go new file mode 100644 index 0000000..956b174 --- /dev/null +++ b/services/finance/internal/delivery/grpc/rm_group_handler.go @@ -0,0 +1,958 @@ +// Package grpc provides gRPC server implementation for finance service. +package grpc + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + + commonv1 "github.com/mutugading/goapps-backend/gen/common/v1" + financev1 "github.com/mutugading/goapps-backend/gen/finance/v1" + apprmcost "github.com/mutugading/goapps-backend/services/finance/internal/application/rmcost" + appgroup "github.com/mutugading/goapps-backend/services/finance/internal/application/rmgroup" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/job" + rmgroupdomain "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/syncdata" +) + +// Metric operation constants (shared across rm_group_handler + rm_cost_handler). +const ( + opCreate = "create" + opGet = "get" + opUpdate = "update" + opDelete = "delete" + opList = "list" + opAddItems = "add_items" + opRemoveItems = "remove_items" + opListUngrouped = "list_ungrouped" + opGetItemRates = "get_item_rates" + opTrigger = "trigger" + opCalculate = "calculate" + opListHistory = "list_history" + opExport = "export" + opImport = "import" + opTemplate = "template" + msgRMGroupSuccess = "RM Group retrieved successfully" + msgRMCostRetrieved = "RM Cost retrieved successfully" +) + +// ItemMetadataLookup provides item metadata from sync data for enriching +// newly created group details. Without this, detail rows would have empty +// name/grade/UOM columns. GetItemByCodeGrade is preferred — it can pick the +// exact (item_code, grade_code) variant the user selected instead of an +// arbitrary one; GetItemByCode retained for callers that don't have a grade. +type ItemMetadataLookup interface { + GetItemByCode(ctx context.Context, itemCode string) (*syncdata.ItemConsStockPO, error) + GetItemByCodeGrade(ctx context.Context, itemCode, gradeCode string) (*syncdata.ItemConsStockPO, error) +} + +// buildAddItemInputs translates an AddItemsRequest into the application-layer +// AddItemInput slice, preferring the structured `selections` field over the +// legacy `item_codes`. For each item it tries to enrich with sync-feed +// metadata keyed on (item_code, grade_code) so multi-variant items pick the +// exact row the operator saw in the picker. +func (h *RMGroupHandler) buildAddItemInputs(ctx context.Context, req *financev1.AddItemsRequest) []appgroup.AddItemInput { + if len(req.Selections) > 0 { + out := make([]appgroup.AddItemInput, len(req.Selections)) + for i, sel := range req.Selections { + out[i] = h.enrichItemInput(ctx, sel.ItemCode, sel.GradeCode) + } + return out + } + out := make([]appgroup.AddItemInput, len(req.ItemCodes)) + for i, code := range req.ItemCodes { + out[i] = h.enrichItemInput(ctx, code, "") + } + return out +} + +func (h *RMGroupHandler) enrichItemInput(ctx context.Context, itemCode, gradeCode string) appgroup.AddItemInput { + in := appgroup.AddItemInput{ItemCode: itemCode, GradeCode: gradeCode} + if h.itemLookup == nil { + return in + } + syncItem, err := h.itemLookup.GetItemByCodeGrade(ctx, itemCode, gradeCode) + if err != nil || syncItem == nil { + return in + } + in.ItemName = syncItem.ItemName + if in.GradeCode == "" { + in.GradeCode = syncItem.GradeCode + } + in.ItemGrade = syncItem.GradeName + in.UOMCode = syncItem.UOM + return in +} + +// RMGroupHandler implements the RMGroupServiceServer interface. +type RMGroupHandler struct { + financev1.UnimplementedRMGroupServiceServer + createHandler *appgroup.CreateHandler + getHandler *appgroup.GetHandler + updateHandler *appgroup.UpdateHandler + deleteHandler *appgroup.DeleteHandler + listHandler *appgroup.ListHandler + addItemsHandler *appgroup.AddItemsHandler + removeItemsHandler *appgroup.RemoveItemsHandler + ungroupedHandler *appgroup.UngroupedHandler + ungroupedExport *appgroup.UngroupedExportHandler + itemRatesHandler *appgroup.GroupItemRatesHandler + exportHandler *appgroup.ExportHandler + importHandler *appgroup.ImportHandler + importItemsHandler *appgroup.ImportGroupItemsHandler + templateHandler *appgroup.TemplateHandler + itemsTemplate *appgroup.GroupItemsTemplateHandler + itemLookup ItemMetadataLookup + recalc *RecalcChain + validationHelper *ValidationHelper +} + +// PeriodLister returns the set of known periods, newest first. Used by +// RecalcChain to pick the period to recalculate when the caller did not +// specify one. +type PeriodLister func(ctx context.Context) ([]string, error) + +// RecalcChain encapsulates the fire-and-forget enqueue of an RM cost job +// triggered from group CRUD operations (create/update head, add/remove items). +// Failures are logged and swallowed so the CRUD response never fails on them. +type RecalcChain struct { + jobRepo job.Repository + publisher apprmcost.JobPublisher + costPeriods PeriodLister + syncPeriods PeriodLister +} + +// NewRecalcChain builds a RecalcChain. Any of the deps may be nil (all-nil +// makes Publish a no-op — useful when RabbitMQ is unavailable). +func NewRecalcChain(jobRepo job.Repository, publisher apprmcost.JobPublisher, costPeriods, syncPeriods PeriodLister) *RecalcChain { + return &RecalcChain{jobRepo: jobRepo, publisher: publisher, costPeriods: costPeriods, syncPeriods: syncPeriods} +} + +// Publish enqueues a single-group recalculation for groupHeadID with the given +// reason. Returns nil on success or if chain is disabled; logs and returns nil +// on any step failure to keep CRUD responses unaffected. +func (c *RecalcChain) Publish(ctx context.Context, groupHeadID uuid.UUID, reason, createdBy string) { + if c == nil || c.publisher == nil || c.jobRepo == nil { + return + } + period, err := c.resolvePeriod(ctx) + if err != nil { + log.Warn().Err(err).Msg("recalc chain: resolve period failed") + return + } + if period == "" { + // Nothing to recalculate against yet — no cost rows and no synced data. + return + } + + // Skip if another active job for (type, period) already exists. + active, err := c.jobRepo.HasActiveJob(ctx, job.TypeRMCostCalculation, period) + if err != nil { + log.Warn().Err(err).Msg("recalc chain: check active job failed") + return + } + if active { + log.Debug().Str("period", period).Msg("recalc chain: active job already queued, skipping") + return + } + + exec, err := job.NewExecution(job.TypeRMCostCalculation, groupHeadID.String(), period, createdBy, 5, nil) + if err != nil { + log.Warn().Err(err).Msg("recalc chain: new execution failed") + return + } + if err := c.jobRepo.Create(ctx, exec); err != nil { + log.Warn().Err(err).Msg("recalc chain: persist execution failed") + return + } + gid := groupHeadID + if err := c.publisher.PublishRMCostCalculation(ctx, exec.ID().String(), period, &gid, reason, createdBy); err != nil { + if failErr := exec.Fail("publish failed: " + err.Error()); failErr == nil { + if updErr := c.jobRepo.UpdateStatus(ctx, exec); updErr != nil { + log.Warn().Err(updErr).Msg("recalc chain: mark failed status failed") + } + } + log.Warn().Err(err).Msg("recalc chain: publish failed (operator can recalc manually)") + return + } + log.Info().Str("job_id", exec.ID().String()).Str("period", period). + Str("group_head_id", groupHeadID.String()).Str("reason", reason). + Msg("recalc chain: job enqueued") +} + +func (c *RecalcChain) resolvePeriod(ctx context.Context) (string, error) { + if c.costPeriods != nil { + periods, err := c.costPeriods(ctx) + if err != nil { + return "", err + } + if len(periods) > 0 { + return periods[0], nil + } + } + if c.syncPeriods != nil { + periods, err := c.syncPeriods(ctx) + if err != nil { + return "", err + } + if len(periods) > 0 { + return periods[0], nil + } + } + return "", nil +} + +// NewRMGroupHandler builds an RMGroupHandler. +func NewRMGroupHandler( + repo rmgroupdomain.Repository, + ungroupedReader appgroup.UngroupedItemsReader, + itemRatesReader appgroup.GroupItemRatesReader, + itemLookup ItemMetadataLookup, + costChecker appgroup.CostChecker, + importLookup appgroup.ImportItemLookup, + recalc *RecalcChain, +) (*RMGroupHandler, error) { + v, err := NewValidationHelper() + if err != nil { + return nil, err + } + return &RMGroupHandler{ + createHandler: appgroup.NewCreateHandler(repo), + getHandler: appgroup.NewGetHandler(repo), + updateHandler: appgroup.NewUpdateHandler(repo), + deleteHandler: appgroup.NewDeleteHandler(repo, costChecker), + listHandler: appgroup.NewListHandler(repo), + addItemsHandler: appgroup.NewAddItemsHandler(repo), + removeItemsHandler: appgroup.NewRemoveItemsHandler(repo), + ungroupedHandler: appgroup.NewUngroupedHandler(ungroupedReader), + ungroupedExport: appgroup.NewUngroupedExportHandler(ungroupedReader), + itemRatesHandler: appgroup.NewGroupItemRatesHandler(itemRatesReader), + exportHandler: appgroup.NewExportHandler(repo), + importHandler: appgroup.NewImportHandler(repo, importLookup), + importItemsHandler: appgroup.NewImportGroupItemsHandler(appgroup.NewAddItemsHandler(repo), importLookup), + templateHandler: appgroup.NewTemplateHandler(), + itemsTemplate: appgroup.NewGroupItemsTemplateHandler(), + itemLookup: itemLookup, + recalc: recalc, + validationHelper: v, + }, nil +} + +// CreateRMGroup creates a new RM group head. +func (h *RMGroupHandler) CreateRMGroup(ctx context.Context, req *financev1.CreateRMGroupRequest) (*financev1.CreateRMGroupResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opCreate, false) + return &financev1.CreateRMGroupResponse{Base: baseResp}, nil + } + + head, err := h.createHandler.Handle(ctx, appgroup.CreateCommand{ + Code: req.GroupCode, + Name: req.GroupName, + Description: req.Description, + Colorant: req.Colourant, + CIName: req.CiName, + CostPercentage: req.CostPercentage, + CostPerKg: req.CostPerKg, + CreatedBy: getUserFromContext(ctx), + }) + if err != nil { + RecordRMGroupOperation(opCreate, false) + return &financev1.CreateRMGroupResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + RecordRMGroupOperation(opCreate, true) + h.recalc.Publish(ctx, head.ID(), string(apprmcost.TriggerGroupUpdate), getUserFromContext(ctx)) + return &financev1.CreateRMGroupResponse{ + Base: successResponse("RM Group created successfully"), + Data: rmGroupHeadToProto(head), + }, nil +} + +// GetRMGroup retrieves a group head with its details. +func (h *RMGroupHandler) GetRMGroup(ctx context.Context, req *financev1.GetRMGroupRequest) (*financev1.GetRMGroupResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opGet, false) + return &financev1.GetRMGroupResponse{Base: baseResp}, nil + } + + result, err := h.getHandler.Handle(ctx, appgroup.GetQuery{ + HeadID: req.GroupHeadId, + WithDetails: true, + ActiveOnly: false, + }) + if err != nil { + RecordRMGroupOperation(opGet, false) + return &financev1.GetRMGroupResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + details := make([]*financev1.RMGroupDetail, len(result.Details)) + for i, d := range result.Details { + details[i] = rmGroupDetailToProto(d) + } + + RecordRMGroupOperation(opGet, true) + return &financev1.GetRMGroupResponse{ + Base: successResponse(msgRMGroupSuccess), + Data: &financev1.RMGroupHeadWithDetails{ + Head: rmGroupHeadToProto(result.Head), + Details: details, + }, + }, nil +} + +// UpdateRMGroup applies a partial update to a head. +func (h *RMGroupHandler) UpdateRMGroup(ctx context.Context, req *financev1.UpdateRMGroupRequest) (*financev1.UpdateRMGroupResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opUpdate, false) + return &financev1.UpdateRMGroupResponse{Base: baseResp}, nil + } + + cmd := appgroup.UpdateCommand{ + HeadID: req.GroupHeadId, + Name: req.GroupName, + Description: req.Description, + Colorant: req.Colourant, + CIName: req.CiName, + CostPercentage: req.CostPercentage, + CostPerKg: req.CostPerKg, + InitValValuation: req.InitValValuation, + InitValMarketing: req.InitValMarketing, + InitValSimulation: req.InitValSimulation, + ClearInitValValuation: req.ClearInitValValuation, + ClearInitValMarketing: req.ClearInitValMarketing, + ClearInitValSimulation: req.ClearInitValSimulation, + IsActive: req.IsActive, + UpdatedBy: getUserFromContext(ctx), + } + if req.FlagValuation != nil { + s := protoFlagToString(*req.FlagValuation) + cmd.FlagValuation = &s + } + if req.FlagMarketing != nil { + s := protoFlagToString(*req.FlagMarketing) + cmd.FlagMarketing = &s + } + if req.FlagSimulation != nil { + s := protoFlagToString(*req.FlagSimulation) + cmd.FlagSimulation = &s + } + + head, err := h.updateHandler.Handle(ctx, cmd) + if err != nil { + RecordRMGroupOperation(opUpdate, false) + return &financev1.UpdateRMGroupResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + RecordRMGroupOperation(opUpdate, true) + h.recalc.Publish(ctx, head.ID(), string(apprmcost.TriggerGroupUpdate), getUserFromContext(ctx)) + return &financev1.UpdateRMGroupResponse{ + Base: successResponse("RM Group updated successfully"), + Data: rmGroupHeadToProto(head), + }, nil +} + +// DeleteRMGroup soft-deletes a group head (cascade to details). +func (h *RMGroupHandler) DeleteRMGroup(ctx context.Context, req *financev1.DeleteRMGroupRequest) (*financev1.DeleteRMGroupResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opDelete, false) + return &financev1.DeleteRMGroupResponse{Base: baseResp}, nil + } + + if err := h.deleteHandler.Handle(ctx, appgroup.DeleteCommand{ + HeadID: req.GroupHeadId, + DeletedBy: getUserFromContext(ctx), + }); err != nil { + RecordRMGroupOperation(opDelete, false) + return &financev1.DeleteRMGroupResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + RecordRMGroupOperation(opDelete, true) + return &financev1.DeleteRMGroupResponse{ + Base: successResponse("RM Group deleted successfully"), + }, nil +} + +// ListRMGroups returns a paginated list of group heads. +func (h *RMGroupHandler) ListRMGroups(ctx context.Context, req *financev1.ListRMGroupsRequest) (*financev1.ListRMGroupsResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opList, false) + return &financev1.ListRMGroupsResponse{Base: baseResp}, nil + } + + query := appgroup.ListQuery{ + Page: int(req.Page), + PageSize: int(req.PageSize), + Search: req.Search, + SortBy: req.SortBy, + SortOrder: req.SortOrder, + } + switch req.ActiveFilter { + case financev1.ActiveFilter_ACTIVE_FILTER_ACTIVE: + v := true + query.IsActive = &v + case financev1.ActiveFilter_ACTIVE_FILTER_INACTIVE: + v := false + query.IsActive = &v + case financev1.ActiveFilter_ACTIVE_FILTER_UNSPECIFIED: + } + + result, err := h.listHandler.Handle(ctx, query) + if err != nil { + RecordRMGroupOperation(opList, false) + return &financev1.ListRMGroupsResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + items := make([]*financev1.RMGroupHead, len(result.Heads)) + for i, head := range result.Heads { + items[i] = rmGroupHeadToProto(head) + } + + RecordRMGroupOperation(opList, true) + return &financev1.ListRMGroupsResponse{ + Base: successResponse("RM Groups retrieved successfully"), + Data: items, + Pagination: &commonv1.PaginationResponse{ + CurrentPage: result.CurrentPage, + PageSize: result.PageSize, + TotalItems: result.TotalItems, + TotalPages: result.TotalPages, + }, + }, nil +} + +// AddItems assigns items to a group head. +// It enriches each item with metadata (name, grade, UOM) from sync data +// so the detail rows are human-readable without a separate JOIN. +func (h *RMGroupHandler) AddItems(ctx context.Context, req *financev1.AddItemsRequest) (*financev1.AddItemsResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opAddItems, false) + return &financev1.AddItemsResponse{Base: baseResp}, nil + } + + // Prefer the structured `selections` field (carries grade_code) over the + // legacy `item_codes`. When both are supplied, selections wins so new + // frontends can migrate without breaking older callers. + items := h.buildAddItemInputs(ctx, req) + + result, err := h.addItemsHandler.Handle(ctx, appgroup.AddItemsCommand{ + HeadID: req.GroupHeadId, + CreatedBy: getUserFromContext(ctx), + Items: items, + }) + if err != nil { + RecordRMGroupOperation(opAddItems, false) + return &financev1.AddItemsResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + added := make([]*financev1.RMGroupDetail, len(result.Added)) + for i, d := range result.Added { + added[i] = rmGroupDetailToProto(d) + } + skipped := make([]*financev1.SkippedItem, len(result.Skipped)) + for i, s := range result.Skipped { + skipped[i] = skippedItemToProto(s) + } + + RecordRMGroupOperation(opAddItems, true) + if len(result.Added) > 0 { + if headID, parseErr := uuid.Parse(req.GroupHeadId); parseErr == nil { + h.recalc.Publish(ctx, headID, string(apprmcost.TriggerDetailChange), getUserFromContext(ctx)) + } + } + return &financev1.AddItemsResponse{ + Base: successResponse("Items processed"), + Added: added, + Skipped: skipped, + }, nil +} + +// RemoveItems removes details from a group head. +func (h *RMGroupHandler) RemoveItems(ctx context.Context, req *financev1.RemoveItemsRequest) (*financev1.RemoveItemsResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opRemoveItems, false) + return &financev1.RemoveItemsResponse{Base: baseResp}, nil + } + + result, err := h.removeItemsHandler.Handle(ctx, appgroup.RemoveItemsCommand{ + HeadID: req.GroupHeadId, + DetailIDs: req.GroupDetailIds, + Mode: removeModeFromProto(req.Mode), + RemovedBy: getUserFromContext(ctx), + }) + if err != nil { + RecordRMGroupOperation(opRemoveItems, false) + return &financev1.RemoveItemsResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + RecordRMGroupOperation(opRemoveItems, true) + if len(result.Removed) > 0 { + if headID, parseErr := uuid.Parse(req.GroupHeadId); parseErr == nil { + h.recalc.Publish(ctx, headID, string(apprmcost.TriggerDetailChange), getUserFromContext(ctx)) + } + } + return &financev1.RemoveItemsResponse{ + Base: successResponse("Items removed"), + RemovedCount: int32(len(result.Removed)), //nolint:gosec // slice length capped by request validation + }, nil +} + +// ListUngroupedItems returns items from the sync feed with no active group. +func (h *RMGroupHandler) ListUngroupedItems(ctx context.Context, req *financev1.ListUngroupedItemsRequest) (*financev1.ListUngroupedItemsResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opListUngrouped, false) + return &financev1.ListUngroupedItemsResponse{Base: baseResp}, nil + } + + result, err := h.ungroupedHandler.Handle(ctx, appgroup.UngroupedQuery{ + Page: int(req.Page), + PageSize: int(req.PageSize), + Period: req.Period, + Search: req.Search, + }) + if err != nil { + RecordRMGroupOperation(opListUngrouped, false) + return &financev1.ListUngroupedItemsResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + items := make([]*financev1.UngroupedItem, len(result.Items)) + for i, it := range result.Items { + items[i] = ungroupedItemToProto(it) + } + + RecordRMGroupOperation(opListUngrouped, true) + return &financev1.ListUngroupedItemsResponse{ + Base: successResponse("Ungrouped items retrieved successfully"), + Data: items, + Pagination: &commonv1.PaginationResponse{ + CurrentPage: result.CurrentPage, + PageSize: result.PageSize, + TotalItems: result.TotalItems, + TotalPages: result.TotalPages, + }, + }, nil +} + +// GetRMGroupItemRates returns per-item per-stage rates for a group + period. +func (h *RMGroupHandler) GetRMGroupItemRates(ctx context.Context, req *financev1.GetRMGroupItemRatesRequest) (*financev1.GetRMGroupItemRatesResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opGetItemRates, false) + return &financev1.GetRMGroupItemRatesResponse{Base: baseResp}, nil + } + + rows, err := h.itemRatesHandler.Handle(ctx, appgroup.GroupItemRatesQuery{ + HeadID: req.GroupHeadId, + Period: req.Period, + }) + if err != nil { + RecordRMGroupOperation(opGetItemRates, false) + return &financev1.GetRMGroupItemRatesResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + out := make([]*financev1.RMGroupItemRates, len(rows)) + for i, r := range rows { + out[i] = groupItemRatesToProto(r) + } + + RecordRMGroupOperation(opGetItemRates, true) + return &financev1.GetRMGroupItemRatesResponse{ + Base: successResponse("Group item rates retrieved successfully"), + Data: out, + }, nil +} + +func groupItemRatesToProto(r *appgroup.GroupItemRates) *financev1.RMGroupItemRates { + return &financev1.RMGroupItemRates{ + ItemCode: r.ItemCode, + ItemName: r.ItemName, + GradeCode: r.GradeCode, + ItemGrade: r.ItemGrade, + UomCode: r.UOMCode, + IsActive: r.IsActive, + IsDummy: r.IsDummy, + Period: r.Period, + ConsQty: r.ConsQty, + ConsVal: r.ConsVal, + ConsRate: r.ConsRate, + StoresQty: r.StoresQty, + StoresVal: r.StoresVal, + StoresRate: r.StoresRate, + DeptQty: r.DeptQty, + DeptVal: r.DeptVal, + DeptRate: r.DeptRate, + LastPoQty1: r.LastPOQty1, + LastPoVal1: r.LastPOVal1, + LastPoRate1: r.LastPORate1, + LastPoQty2: r.LastPOQty2, + LastPoVal2: r.LastPOVal2, + LastPoRate2: r.LastPORate2, + LastPoQty3: r.LastPOQty3, + LastPoVal3: r.LastPOVal3, + LastPoRate3: r.LastPORate3, + } +} + +// ============================================================================= +// Entity → proto mappers +// ============================================================================= + +func rmGroupHeadToProto(h *rmgroupdomain.Head) *financev1.RMGroupHead { + out := &financev1.RMGroupHead{ + GroupHeadId: h.ID().String(), + GroupCode: h.Code().String(), + GroupName: h.Name(), + Description: h.Description(), + Colourant: h.Colorant(), + CiName: h.CIName(), + CostPercentage: h.CostPercentage(), + CostPerKg: h.CostPerKg(), + FlagValuation: flagToProto(h.FlagValuation()), + FlagMarketing: flagToProto(h.FlagMarketing()), + FlagSimulation: flagToProto(h.FlagSimulation()), + InitValValuation: h.InitValValuation(), + InitValMarketing: h.InitValMarketing(), + InitValSimulation: h.InitValSimulation(), + IsActive: h.IsActive(), + Audit: &commonv1.AuditInfo{ + CreatedAt: h.CreatedAt().Format(time.RFC3339), + CreatedBy: h.CreatedBy(), + }, + } + if h.UpdatedAt() != nil { + out.Audit.UpdatedAt = h.UpdatedAt().Format(time.RFC3339) + } + if h.UpdatedBy() != nil { + out.Audit.UpdatedBy = *h.UpdatedBy() + } + return out +} + +func rmGroupDetailToProto(d *rmgroupdomain.Detail) *financev1.RMGroupDetail { + out := &financev1.RMGroupDetail{ + GroupDetailId: d.ID().String(), + GroupHeadId: d.HeadID().String(), + ItemCode: d.ItemCode().String(), + ItemName: d.ItemName(), + ItemTypeCode: d.ItemTypeCode(), + GradeCode: d.GradeCode(), + ItemGrade: d.ItemGrade(), + UomCode: d.UOMCode(), + MarketPercentage: d.MarketPercentage(), + MarketValueRp: d.MarketValueRp(), + SortOrder: d.SortOrder(), + IsActive: d.IsActive(), + IsDummy: d.IsDummy(), + Audit: &commonv1.AuditInfo{ + CreatedAt: d.CreatedAt().Format(time.RFC3339), + CreatedBy: d.CreatedBy(), + }, + } + if d.UpdatedAt() != nil { + out.Audit.UpdatedAt = d.UpdatedAt().Format(time.RFC3339) + } + if d.UpdatedBy() != nil { + out.Audit.UpdatedBy = *d.UpdatedBy() + } + return out +} + +func ungroupedItemToProto(it *syncdata.ItemConsStockPO) *financev1.UngroupedItem { + out := &financev1.UngroupedItem{ + Period: it.Period, + ItemCode: it.ItemCode, + ItemName: it.ItemName, + GradeCode: it.GradeCode, + ItemGrade: it.GradeName, + UomCode: it.UOM, + } + assignF64(&out.ConsQty, it.ConsQty) + assignF64(&out.ConsVal, it.ConsVal) + assignF64(&out.ConsRate, it.ConsRate) + assignF64(&out.StoresQty, it.StoresQty) + assignF64(&out.StoresVal, it.StoresVal) + assignF64(&out.StoresRate, it.StoresRate) + assignF64(&out.DeptQty, it.DeptQty) + assignF64(&out.DeptVal, it.DeptVal) + assignF64(&out.DeptRate, it.DeptRate) + assignF64(&out.LastPoQty1, it.LastPOQty1) + assignF64(&out.LastPoVal1, it.LastPOVal1) + assignF64(&out.LastPoRate1, it.LastPORate1) + assignF64(&out.LastPoQty2, it.LastPOQty2) + assignF64(&out.LastPoVal2, it.LastPOVal2) + assignF64(&out.LastPoRate2, it.LastPORate2) + assignF64(&out.LastPoQty3, it.LastPOQty3) + assignF64(&out.LastPoVal3, it.LastPOVal3) + assignF64(&out.LastPoRate3, it.LastPORate3) + return out +} + +func assignF64(dst *float64, src *float64) { + if src != nil { + *dst = *src + } +} + +func skippedItemToProto(s appgroup.SkippedItem) *financev1.SkippedItem { + out := &financev1.SkippedItem{ + ItemCode: s.ItemCode, + } + if s.OwningGroupID != nil { + out.OwningGroupHeadId = s.OwningGroupID.String() + } + if s.OwningDetailID != nil { + out.OwningGroupDetailId = s.OwningDetailID.String() + } + return out +} + +// flagToProto maps a domain Flag to its proto counterpart. +func flagToProto(f rmgroupdomain.Flag) financev1.RMGroupFlag { + switch f { + case rmgroupdomain.FlagInit: + return financev1.RMGroupFlag_RM_GROUP_FLAG_INIT + case rmgroupdomain.FlagCons: + return financev1.RMGroupFlag_RM_GROUP_FLAG_CONS + case rmgroupdomain.FlagStores: + return financev1.RMGroupFlag_RM_GROUP_FLAG_STORES + case rmgroupdomain.FlagDept: + return financev1.RMGroupFlag_RM_GROUP_FLAG_DEPT + case rmgroupdomain.FlagPO1: + return financev1.RMGroupFlag_RM_GROUP_FLAG_PO_1 + case rmgroupdomain.FlagPO2: + return financev1.RMGroupFlag_RM_GROUP_FLAG_PO_2 + case rmgroupdomain.FlagPO3: + return financev1.RMGroupFlag_RM_GROUP_FLAG_PO_3 + default: + return financev1.RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED + } +} + +// protoFlagToString maps proto flag to its canonical string form (domain Flag underlying value). +func protoFlagToString(f financev1.RMGroupFlag) string { + switch f { + case financev1.RMGroupFlag_RM_GROUP_FLAG_INIT: + return string(rmgroupdomain.FlagInit) + case financev1.RMGroupFlag_RM_GROUP_FLAG_CONS: + return string(rmgroupdomain.FlagCons) + case financev1.RMGroupFlag_RM_GROUP_FLAG_STORES: + return string(rmgroupdomain.FlagStores) + case financev1.RMGroupFlag_RM_GROUP_FLAG_DEPT: + return string(rmgroupdomain.FlagDept) + case financev1.RMGroupFlag_RM_GROUP_FLAG_PO_1: + return string(rmgroupdomain.FlagPO1) + case financev1.RMGroupFlag_RM_GROUP_FLAG_PO_2: + return string(rmgroupdomain.FlagPO2) + case financev1.RMGroupFlag_RM_GROUP_FLAG_PO_3: + return string(rmgroupdomain.FlagPO3) + case financev1.RMGroupFlag_RM_GROUP_FLAG_UNSPECIFIED: + return "" + default: + return "" + } +} + +func removeModeFromProto(m financev1.RemoveItemsMode) appgroup.RemoveMode { + switch m { + case financev1.RemoveItemsMode_REMOVE_ITEMS_MODE_DEACTIVATE: + return appgroup.RemoveModeDeactivate + case financev1.RemoveItemsMode_REMOVE_ITEMS_MODE_SOFT_DELETE: + return appgroup.RemoveModeSoftDelete + case financev1.RemoveItemsMode_REMOVE_ITEMS_MODE_UNSPECIFIED: + return appgroup.RemoveModeSoftDelete + default: + return appgroup.RemoveModeSoftDelete + } +} + +// ExportRMGroups generates a 2-sheet Excel (Groups + Items) of all RM groups. +func (h *RMGroupHandler) ExportRMGroups(ctx context.Context, req *financev1.ExportRMGroupsRequest) (*financev1.ExportRMGroupsResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opExport, false) + return &financev1.ExportRMGroupsResponse{Base: baseResp}, nil + } + + query := appgroup.ExportQuery{} + switch req.ActiveFilter { + case financev1.ActiveFilter_ACTIVE_FILTER_ACTIVE: + v := true + query.IsActive = &v + case financev1.ActiveFilter_ACTIVE_FILTER_INACTIVE: + v := false + query.IsActive = &v + case financev1.ActiveFilter_ACTIVE_FILTER_UNSPECIFIED: + } + + result, err := h.exportHandler.Handle(ctx, query) + if err != nil { + RecordRMGroupOperation(opExport, false) + return &financev1.ExportRMGroupsResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + RecordRMGroupOperation(opExport, true) + return &financev1.ExportRMGroupsResponse{ + Base: successResponse("RM Groups exported successfully"), + FileContent: result.FileContent, + FileName: result.FileName, + }, nil +} + +// ImportRMGroups parses a 2-sheet Excel and upserts heads + details. +func (h *RMGroupHandler) ImportRMGroups(ctx context.Context, req *financev1.ImportRMGroupsRequest) (*financev1.ImportRMGroupsResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opImport, false) + return &financev1.ImportRMGroupsResponse{Base: baseResp}, nil + } + + result, err := h.importHandler.Handle(ctx, appgroup.ImportCommand{ + FileContent: req.FileContent, + FileName: req.FileName, + DuplicateAction: req.DuplicateAction, + CreatedBy: getUserFromContext(ctx), + }) + if err != nil { + RecordRMGroupOperation(opImport, false) + return &financev1.ImportRMGroupsResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + errs := make([]*financev1.ImportError, len(result.Errors)) + for i, e := range result.Errors { + errs[i] = &financev1.ImportError{ + RowNumber: e.RowNumber, + Field: e.Field, + Message: e.Message, + } + } + + RecordRMGroupOperation(opImport, true) + return &financev1.ImportRMGroupsResponse{ + Base: successResponse("RM Groups imported"), + GroupsCreated: result.GroupsCreated, + GroupsUpdated: result.GroupsUpdated, + GroupsSkipped: result.GroupsSkipped, + ItemsAdded: result.ItemsAdded, + ItemsSkipped: result.ItemsSkipped, + FailedCount: result.FailedCount, + Errors: errs, + }, nil +} + +// DownloadRMGroupTemplate returns the blank 2-sheet import template. +func (h *RMGroupHandler) DownloadRMGroupTemplate(_ context.Context, req *financev1.DownloadRMGroupTemplateRequest) (*financev1.DownloadRMGroupTemplateResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opTemplate, false) + return &financev1.DownloadRMGroupTemplateResponse{Base: baseResp}, nil + } + + result, err := h.templateHandler.Handle() + if err != nil { + RecordRMGroupOperation(opTemplate, false) + return &financev1.DownloadRMGroupTemplateResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + RecordRMGroupOperation(opTemplate, true) + return &financev1.DownloadRMGroupTemplateResponse{ + Base: successResponse("RM Group template generated"), + FileContent: result.FileContent, + FileName: result.FileName, + }, nil +} + +// ExportUngroupedItems exports ungrouped items matching the filter to Excel. +func (h *RMGroupHandler) ExportUngroupedItems(ctx context.Context, req *financev1.ExportUngroupedItemsRequest) (*financev1.ExportUngroupedItemsResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opExport, false) + return &financev1.ExportUngroupedItemsResponse{Base: baseResp}, nil + } + + result, err := h.ungroupedExport.Handle(ctx, appgroup.UngroupedExportQuery{ + Period: req.Period, + Search: req.Search, + }) + if err != nil { + RecordRMGroupOperation(opExport, false) + return &financev1.ExportUngroupedItemsResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + RecordRMGroupOperation(opExport, true) + return &financev1.ExportUngroupedItemsResponse{ + Base: successResponse("Ungrouped items exported successfully"), + FileContent: result.FileContent, + FileName: result.FileName, + }, nil +} + +// ImportGroupItems bulk-adds items to a specific existing group from Excel. +func (h *RMGroupHandler) ImportGroupItems(ctx context.Context, req *financev1.ImportGroupItemsRequest) (*financev1.ImportGroupItemsResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opImport, false) + return &financev1.ImportGroupItemsResponse{Base: baseResp}, nil + } + + result, err := h.importItemsHandler.Handle(ctx, appgroup.ImportGroupItemsCommand{ + HeadID: req.GroupHeadId, + FileContent: req.FileContent, + FileName: req.FileName, + CreatedBy: getUserFromContext(ctx), + }) + if err != nil { + RecordRMGroupOperation(opImport, false) + return &financev1.ImportGroupItemsResponse{Base: domainErrorToBaseResponse(err)}, nil + } + + errs := make([]*financev1.ImportError, len(result.Errors)) + for i, e := range result.Errors { + errs[i] = &financev1.ImportError{ + RowNumber: e.RowNumber, + Field: e.Field, + Message: e.Message, + } + } + skipped := make([]*financev1.SkippedItem, len(result.Skipped)) + for i, s := range result.Skipped { + skipped[i] = skippedItemToProto(s) + } + + RecordRMGroupOperation(opImport, true) + + // Trigger a recalc if any items were actually added. + if result.ItemsAdded > 0 { + if headID, parseErr := uuid.Parse(req.GroupHeadId); parseErr == nil { + h.recalc.Publish(ctx, headID, string(apprmcost.TriggerDetailChange), getUserFromContext(ctx)) + } + } + + return &financev1.ImportGroupItemsResponse{ + Base: successResponse("Items imported"), + ItemsAdded: result.ItemsAdded, + ItemsSkipped: result.ItemsSkipped, + FailedCount: result.FailedCount, + Errors: errs, + Skipped: skipped, + }, nil +} + +// DownloadGroupItemsTemplate returns a one-sheet Excel template matching +// the columns ImportGroupItems expects. +func (h *RMGroupHandler) DownloadGroupItemsTemplate(_ context.Context, req *financev1.DownloadGroupItemsTemplateRequest) (*financev1.DownloadGroupItemsTemplateResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + RecordRMGroupOperation(opTemplate, false) + return &financev1.DownloadGroupItemsTemplateResponse{Base: baseResp}, nil + } + result, err := h.itemsTemplate.Handle() + if err != nil { + RecordRMGroupOperation(opTemplate, false) + return &financev1.DownloadGroupItemsTemplateResponse{Base: domainErrorToBaseResponse(err)}, nil + } + RecordRMGroupOperation(opTemplate, true) + return &financev1.DownloadGroupItemsTemplateResponse{ + Base: successResponse("RM Group items template generated"), + FileContent: result.FileContent, + FileName: result.FileName, + }, nil +} + +// RecordRMGroupOperation records an RM Group operation metric. +func RecordRMGroupOperation(operation string, success bool) { + rmGroupOperationsTotal.WithLabelValues(operation, metricStatus(success)).Inc() +} diff --git a/services/finance/internal/delivery/grpc/uom_handler.go b/services/finance/internal/delivery/grpc/uom_handler.go index 1baf8ca..3e27c27 100644 --- a/services/finance/internal/delivery/grpc/uom_handler.go +++ b/services/finance/internal/delivery/grpc/uom_handler.go @@ -372,7 +372,9 @@ func domainErrorToBaseResponse(err error) *commonv1.BaseResponse { switch { case strings.Contains(errMsg, "not found"): return NotFoundResponse(errMsg) - case strings.Contains(errMsg, "already exists"): + case strings.Contains(errMsg, "already exists"), + strings.Contains(errMsg, "cannot be deleted"), + strings.Contains(errMsg, "already assigned"): return ConflictResponse(errMsg) case strings.Contains(errMsg, "invalid"): return ErrorResponse("400", errMsg) @@ -408,8 +410,16 @@ func entityToProto(entity *uomdomain.UOM) *financev1.UOM { } func getUserFromContext(ctx context.Context) string { + // Prefer human-readable username from JWT claims (set by AuthInterceptor). + if username, ok := ctx.Value(AuthUsernameKey).(string); ok && username != "" { + return username + } + if userID, ok := ctx.Value(AuthUserIDKey).(string); ok && userID != "" { + return userID + } + // Legacy key retained in case any manual population still uses it. if userID, ok := ctx.Value(UserIDKey).(string); ok && userID != "" { return userID } - return "system" // Default for now, will be from JWT in IAM service + return "system" } diff --git a/services/finance/internal/delivery/httpdelivery/gateway.go b/services/finance/internal/delivery/httpdelivery/gateway.go index 25ce167..c9cbba3 100644 --- a/services/finance/internal/delivery/httpdelivery/gateway.go +++ b/services/finance/internal/delivery/httpdelivery/gateway.go @@ -95,6 +95,14 @@ func (s *Server) Start(ctx context.Context) error { return fmt.Errorf("failed to register OracleSync gateway: %w", err) } + if err := financev1.RegisterRMGroupServiceHandlerFromEndpoint(ctx, gwMux, s.grpcTarget, opts); err != nil { + return fmt.Errorf("failed to register RMGroup gateway: %w", err) + } + + if err := financev1.RegisterRMCostServiceHandlerFromEndpoint(ctx, gwMux, s.grpcTarget, opts); err != nil { + return fmt.Errorf("failed to register RMCost gateway: %w", err) + } + // Create main mux mux := http.NewServeMux() diff --git a/services/finance/internal/delivery/httpdelivery/swagger.json b/services/finance/internal/delivery/httpdelivery/swagger.json index ce2d6f8..92fa408 100644 --- a/services/finance/internal/delivery/httpdelivery/swagger.json +++ b/services/finance/internal/delivery/httpdelivery/swagger.json @@ -361,6 +361,275 @@ ] } }, + "/api/v1/finance/item-cons-stock-po": { + "get": { + "summary": "ListItemConsStockPO retrieves synced item consumption, stock, and PO data.", + "operationId": "OracleSyncService_ListItemConsStockPO", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListItemConsStockPOResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "page", + "description": "Page number (1-indexed).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageSize", + "description": "Number of items per page.", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "period", + "description": "Filter by period (YYYYMM).", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "itemCode", + "description": "Filter by item code.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "search", + "description": "Full-text search on item name, item code, grade name.", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "OracleSyncService" + ] + } + }, + "/api/v1/finance/oracle-sync/jobs": { + "get": { + "summary": "ListSyncJobs retrieves a paginated list of sync job executions.", + "operationId": "OracleSyncService_ListSyncJobs", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListSyncJobsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "page", + "description": "Page number (1-indexed).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageSize", + "description": "Number of items per page.", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "jobType", + "description": "Filter by job type.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "status", + "description": "Filter by status.\n\n - JOB_STATUS_UNSPECIFIED: Default unspecified value.\n - JOB_STATUS_QUEUED: Job is queued and waiting for processing.\n - JOB_STATUS_PROCESSING: Job is currently being processed.\n - JOB_STATUS_SUCCESS: Job completed successfully.\n - JOB_STATUS_FAILED: Job failed.\n - JOB_STATUS_CANCELLED: Job was cancelled.", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "JOB_STATUS_UNSPECIFIED", + "JOB_STATUS_QUEUED", + "JOB_STATUS_PROCESSING", + "JOB_STATUS_SUCCESS", + "JOB_STATUS_FAILED", + "JOB_STATUS_CANCELLED" + ], + "default": "JOB_STATUS_UNSPECIFIED" + }, + { + "name": "period", + "description": "Filter by period (YYYYMM).", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "search", + "description": "Search in job code or error message.", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "OracleSyncService" + ] + } + }, + "/api/v1/finance/oracle-sync/jobs/{jobId}": { + "get": { + "summary": "GetSyncJob retrieves a specific sync job by ID with execution logs.", + "operationId": "OracleSyncService_GetSyncJob", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetSyncJobResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "jobId", + "description": "Job identifier (UUID).", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "OracleSyncService" + ] + } + }, + "/api/v1/finance/oracle-sync/jobs/{jobId}/cancel": { + "post": { + "summary": "CancelSyncJob cancels a queued or in-progress sync job.", + "operationId": "OracleSyncService_CancelSyncJob", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1CancelSyncJobResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "jobId", + "description": "Job identifier (UUID).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/OracleSyncServiceCancelSyncJobBody" + } + } + ], + "tags": [ + "OracleSyncService" + ] + } + }, + "/api/v1/finance/oracle-sync/periods": { + "get": { + "summary": "ListSyncPeriods retrieves all available sync periods.", + "operationId": "OracleSyncService_ListSyncPeriods", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListSyncPeriodsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "OracleSyncService" + ] + } + }, + "/api/v1/finance/oracle-sync/trigger": { + "post": { + "summary": "TriggerSync initiates a manual Oracle sync job.", + "operationId": "OracleSyncService_TriggerSync", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1TriggerSyncResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "TriggerSyncRequest initiates an Oracle-to-PostgreSQL sync job.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1TriggerSyncRequest" + } + } + ], + "tags": [ + "OracleSyncService" + ] + } + }, "/api/v1/finance/parameters": { "get": { "summary": "ListParameters lists parameters with search, filter, and pagination.", @@ -1021,15 +1290,15 @@ ] } }, - "/api/v1/finance/uoms": { + "/api/v1/finance/rm-costs": { "get": { - "summary": "ListUOMs lists UOMs with search, filter, and pagination.", - "operationId": "UOMService_ListUOMs", + "summary": "ListRMCosts lists cost rows with filter + pagination.", + "operationId": "RMCostService_ListRMCosts", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1ListUOMsResponse" + "$ref": "#/definitions/v1ListRMCostsResponse" } }, "default": { @@ -1042,7 +1311,7 @@ "parameters": [ { "name": "page", - "description": "Page number (1-indexed, default 1, min 1).", + "description": "Page number (1-indexed).", "in": "query", "required": false, "type": "integer", @@ -1050,66 +1319,75 @@ }, { "name": "pageSize", - "description": "Items per page (1-100, default 10).", + "description": "Page size (1-100).", "in": "query", "required": false, "type": "integer", "format": "int32" }, { - "name": "search", - "description": "Search query (searches in code, name, description).", + "name": "period", + "description": "Period filter (YYYYMM). Empty = all periods.", "in": "query", "required": false, "type": "string" }, { - "name": "activeFilter", - "description": "Filter by active status.\nACTIVE_FILTER_UNSPECIFIED (0) = show all, ACTIVE_FILTER_ACTIVE (1) = only active,\nACTIVE_FILTER_INACTIVE (2) = only inactive.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records.", + "name": "rmType", + "description": "Filter by RM type. UNSPECIFIED = all.\n\n - RM_COST_TYPE_UNSPECIFIED: Default zero value. Means \"no filter\" in list requests.\n - RM_COST_TYPE_GROUP: Cost row aggregates a whole RM group.\n - RM_COST_TYPE_ITEM: Cost row is computed for a single item (future phase).", "in": "query", "required": false, "type": "string", "enum": [ - "ACTIVE_FILTER_UNSPECIFIED", - "ACTIVE_FILTER_ACTIVE", - "ACTIVE_FILTER_INACTIVE" + "RM_COST_TYPE_UNSPECIFIED", + "RM_COST_TYPE_GROUP", + "RM_COST_TYPE_ITEM" ], - "default": "ACTIVE_FILTER_UNSPECIFIED" + "default": "RM_COST_TYPE_UNSPECIFIED" }, { - "name": "sortBy", - "description": "Sort field: \"code\", \"name\", \"category\", \"created_at\" (default: \"code\").", + "name": "groupHeadId", + "description": "Optional group scope.", "in": "query", "required": false, "type": "string" }, { - "name": "sortOrder", - "description": "Sort order: \"asc\", \"desc\" (default: \"asc\").", + "name": "search", + "description": "Free-text search on rm_code + rm_name.", "in": "query", "required": false, "type": "string" }, { - "name": "uomCategoryId", - "description": "Filter by category ID (empty = no filter).", + "name": "sortBy", + "description": "Sort field.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "sortOrder", + "description": "Sort order.", "in": "query", "required": false, "type": "string" } ], "tags": [ - "UOMService" + "RMCostService" ] - }, + } + }, + "/api/v1/finance/rm-costs/calculate": { "post": { - "summary": "CreateUOM creates a new UOM.", - "operationId": "UOMService_CreateUOM", + "summary": "CalculateRMCost runs a recalculation synchronously (admin-only).", + "operationId": "RMCostService_CalculateRMCost", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1CreateUOMResponse" + "$ref": "#/definitions/v1CalculateRMCostResponse" } }, "default": { @@ -1122,28 +1400,28 @@ "parameters": [ { "name": "body", - "description": "CreateUOMRequest is the request for creating a new UOM.", + "description": "CalculateRMCost runs a calculation synchronously and returns the produced rows.\nIntended for admin/troubleshooting use \u2014 production traffic should go through\nTriggerRMCostCalculation.", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1CreateUOMRequest" + "$ref": "#/definitions/v1CalculateRMCostRequest" } } ], "tags": [ - "UOMService" + "RMCostService" ] } }, - "/api/v1/finance/uoms/export": { + "/api/v1/finance/rm-costs/history": { "get": { - "summary": "ExportUOMs exports UOMs to Excel file.", - "operationId": "UOMService_ExportUOMs", + "summary": "ListRMCostHistory lists audit-history rows with filter + pagination.", + "operationId": "RMCostService_ListRMCostHistory", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1ExportUOMsResponse" + "$ref": "#/definitions/v1ListRMCostHistoryResponse" } }, "default": { @@ -1155,74 +1433,64 @@ }, "parameters": [ { - "name": "activeFilter", - "description": "Filter by active status.\nACTIVE_FILTER_UNSPECIFIED (0) = export all, ACTIVE_FILTER_ACTIVE (1) = only active,\nACTIVE_FILTER_INACTIVE (2) = only inactive.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records.", + "name": "page", + "description": "Page number (1-indexed).", "in": "query", "required": false, - "type": "string", - "enum": [ - "ACTIVE_FILTER_UNSPECIFIED", - "ACTIVE_FILTER_ACTIVE", - "ACTIVE_FILTER_INACTIVE" - ], - "default": "ACTIVE_FILTER_UNSPECIFIED" + "type": "integer", + "format": "int32" }, { - "name": "uomCategoryId", - "description": "Filter by category ID (empty = export all categories).", + "name": "pageSize", + "description": "Page size (1-100).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "period", + "description": "Period filter (YYYYMM). Empty = all periods.", "in": "query", "required": false, "type": "string" - } - ], - "tags": [ - "UOMService" - ] - } - }, - "/api/v1/finance/uoms/import": { - "post": { - "summary": "ImportUOMs imports UOMs from Excel file.", - "operationId": "UOMService_ImportUOMs", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/v1ImportUOMsResponse" - } }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } - } - }, - "parameters": [ { - "name": "body", - "description": "ImportUOMsRequest is the request for importing UOMs from Excel.", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1ImportUOMsRequest" - } + "name": "rmCode", + "description": "RM code filter (empty = all).", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "groupHeadId", + "description": "Optional group scope.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "jobId", + "description": "Optional job scope.", + "in": "query", + "required": false, + "type": "string" } ], "tags": [ - "UOMService" + "RMCostService" ] } }, - "/api/v1/finance/uoms/template": { + "/api/v1/finance/rm-costs/periods": { "get": { - "summary": "DownloadTemplate downloads the Excel import template.", - "operationId": "UOMService_DownloadTemplate", + "summary": "ListRMCostPeriods returns distinct periods from cost rows (newest first).", + "operationId": "RMCostService_ListRMCostPeriods", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1DownloadTemplateResponse" + "$ref": "#/definitions/v1ListRMCostPeriodsResponse" } }, "default": { @@ -1233,19 +1501,19 @@ } }, "tags": [ - "UOMService" + "RMCostService" ] } }, - "/api/v1/finance/uoms/{uomId}": { - "get": { - "summary": "GetUOM retrieves a UOM by ID.", - "operationId": "UOMService_GetUOM", + "/api/v1/finance/rm-costs/trigger": { + "post": { + "summary": "TriggerRMCostCalculation enqueues an async recalculation job.", + "operationId": "RMCostService_TriggerRMCostCalculation", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1GetUOMResponse" + "$ref": "#/definitions/v1TriggerRMCostCalculationResponse" } }, "default": { @@ -1257,55 +1525,29 @@ }, "parameters": [ { - "name": "uomId", - "description": "UOM ID (UUID format).", - "in": "path", + "name": "body", + "description": "TriggerRMCostCalculation enqueues a recalculation job. Returns immediately\nwith a job ID the caller can poll via the job-execution endpoint.", + "in": "body", "required": true, - "type": "string" - } - ], - "tags": [ - "UOMService" - ] - }, - "delete": { - "summary": "DeleteUOM soft deletes a UOM.", - "operationId": "UOMService_DeleteUOM", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/v1DeleteUOMResponse" - } - }, - "default": { - "description": "An unexpected error response.", "schema": { - "$ref": "#/definitions/rpcStatus" + "$ref": "#/definitions/v1TriggerRMCostCalculationRequest" } } - }, - "parameters": [ - { - "name": "uomId", - "description": "UOM ID to delete (UUID format).", - "in": "path", - "required": true, - "type": "string" - } ], "tags": [ - "UOMService" + "RMCostService" ] - }, - "put": { - "summary": "UpdateUOM updates an existing UOM.\nNote: uom_code is immutable and cannot be changed.", - "operationId": "UOMService_UpdateUOM", + } + }, + "/api/v1/finance/rm-costs/{period}/{rmCode}": { + "get": { + "summary": "GetRMCost fetches a single cost row by (period, rm_code).", + "operationId": "RMCostService_GetRMCost", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1UpdateUOMResponse" + "$ref": "#/definitions/v1GetRMCostResponse" } }, "default": { @@ -1317,35 +1559,34 @@ }, "parameters": [ { - "name": "uomId", - "description": "UOM ID to update (UUID format).", + "name": "period", + "description": "Period (YYYYMM).", "in": "path", "required": true, "type": "string" }, { - "name": "body", - "in": "body", + "name": "rmCode", + "description": "RM code (group or item).", + "in": "path", "required": true, - "schema": { - "$ref": "#/definitions/UOMServiceUpdateUOMBody" - } + "type": "string" } ], "tags": [ - "UOMService" + "RMCostService" ] } }, - "/api/v1/finance/uom-categories": { + "/api/v1/finance/rm-groups": { "get": { - "summary": "ListUOMCategories lists UOM categories with search, filter, and pagination.", - "operationId": "UOMCategoryService_ListUOMCategories", + "summary": "ListRMGroups lists group heads with search + filter + pagination.", + "operationId": "RMGroupService_ListRMGroups", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1ListUOMCategoriesResponse" + "$ref": "#/definitions/v1ListRMGroupsResponse" } }, "default": { @@ -1358,7 +1599,7 @@ "parameters": [ { "name": "page", - "description": "Page number (1-indexed, default 1, min 1).", + "description": "Page number (1-indexed).", "in": "query", "required": false, "type": "integer", @@ -1366,7 +1607,7 @@ }, { "name": "pageSize", - "description": "Items per page (1-100, default 10).", + "description": "Page size (1-100).", "in": "query", "required": false, "type": "integer", @@ -1374,14 +1615,14 @@ }, { "name": "search", - "description": "Search query (searches in code, name, description).", + "description": "Free-text search on code, name, description, colourant, ci_name.", "in": "query", "required": false, "type": "string" }, { "name": "activeFilter", - "description": "Filter by active status.\nACTIVE_FILTER_UNSPECIFIED (0) = show all, ACTIVE_FILTER_ACTIVE (1) = only active,\nACTIVE_FILTER_INACTIVE (2) = only inactive.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records.", + "description": "Filter by active status.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records.", "in": "query", "required": false, "type": "string", @@ -1394,31 +1635,31 @@ }, { "name": "sortBy", - "description": "Sort field: \"code\", \"name\", \"created_at\" (default: \"code\").", + "description": "Sort field.", "in": "query", "required": false, "type": "string" }, { "name": "sortOrder", - "description": "Sort order: \"asc\", \"desc\" (default: \"asc\").", + "description": "Sort order.", "in": "query", "required": false, "type": "string" } ], "tags": [ - "UOMCategoryService" + "RMGroupService" ] }, "post": { - "summary": "CreateUOMCategory creates a new UOM category.", - "operationId": "UOMCategoryService_CreateUOMCategory", + "summary": "CreateRMGroup creates a new RM group head.", + "operationId": "RMGroupService_CreateRMGroup", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1CreateUOMCategoryResponse" + "$ref": "#/definitions/v1CreateRMGroupResponse" } }, "default": { @@ -1431,28 +1672,28 @@ "parameters": [ { "name": "body", - "description": "CreateUOMCategoryRequest is the request for creating a new UOM category.", + "description": "Create a new RM group head.", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1CreateUOMCategoryRequest" + "$ref": "#/definitions/v1CreateRMGroupRequest" } } ], "tags": [ - "UOMCategoryService" + "RMGroupService" ] } }, - "/api/v1/finance/uom-categories/export": { + "/api/v1/finance/rm-groups/export": { "get": { - "summary": "ExportUOMCategories exports UOM categories to Excel file.", - "operationId": "UOMCategoryService_ExportUOMCategories", + "summary": "ExportRMGroups exports all groups + their items to a 2-sheet Excel.", + "operationId": "RMGroupService_ExportRMGroups", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1ExportUOMCategoriesResponse" + "$ref": "#/definitions/v1ExportRMGroupsResponse" } }, "default": { @@ -1465,7 +1706,7 @@ "parameters": [ { "name": "activeFilter", - "description": "Filter by active status.\nACTIVE_FILTER_UNSPECIFIED (0) = export all, ACTIVE_FILTER_ACTIVE (1) = only active,\nACTIVE_FILTER_INACTIVE (2) = only inactive.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records.", + "description": "Filter by active/inactive (UNSPECIFIED = all).\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records.", "in": "query", "required": false, "type": "string", @@ -1478,19 +1719,19 @@ } ], "tags": [ - "UOMCategoryService" + "RMGroupService" ] } }, - "/api/v1/finance/uom-categories/import": { + "/api/v1/finance/rm-groups/import": { "post": { - "summary": "ImportUOMCategories imports UOM categories from Excel file.", - "operationId": "UOMCategoryService_ImportUOMCategories", + "summary": "ImportRMGroups imports groups and/or items from a 2-sheet Excel. Users can\ninclude only the Groups sheet, only the Items sheet, or both.", + "operationId": "RMGroupService_ImportRMGroups", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1ImportUOMCategoriesResponse" + "$ref": "#/definitions/v1ImportRMGroupsResponse" } }, "default": { @@ -1503,28 +1744,28 @@ "parameters": [ { "name": "body", - "description": "ImportUOMCategoriesRequest is the request for importing UOM categories from Excel.", + "description": "ImportRMGroupsRequest accepts a 2-sheet Excel. User may include only the\nGroups sheet (header-only import), only the Items sheet (detail-only for\nexisting groups), or both.", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1ImportUOMCategoriesRequest" + "$ref": "#/definitions/v1ImportRMGroupsRequest" } } ], "tags": [ - "UOMCategoryService" + "RMGroupService" ] } }, - "/api/v1/finance/uom-categories/template": { + "/api/v1/finance/rm-groups/template": { "get": { - "summary": "DownloadUOMCategoryTemplate downloads the Excel import template.", - "operationId": "UOMCategoryService_DownloadUOMCategoryTemplate", + "summary": "DownloadRMGroupTemplate returns a blank 2-sheet Excel with header rows.", + "operationId": "RMGroupService_DownloadRMGroupTemplate", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1DownloadUOMCategoryTemplateResponse" + "$ref": "#/definitions/v1DownloadRMGroupTemplateResponse" } }, "default": { @@ -1535,19 +1776,19 @@ } }, "tags": [ - "UOMCategoryService" + "RMGroupService" ] } }, - "/api/v1/finance/uom-categories/{uomCategoryId}": { + "/api/v1/finance/rm-groups/ungrouped": { "get": { - "summary": "GetUOMCategory retrieves a UOM category by ID.", - "operationId": "UOMCategoryService_GetUOMCategory", + "summary": "ListUngroupedItems reports items from the sync feed that have no active\ngroup assignment.", + "operationId": "RMGroupService_ListUngroupedItems", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1GetUOMCategoryResponse" + "$ref": "#/definitions/v1ListUngroupedItemsResponse" } }, "default": { @@ -1559,25 +1800,80 @@ }, "parameters": [ { - "name": "uomCategoryId", - "description": "UOM category ID (UUID format).", + "name": "page", + "description": "Page number (1-indexed).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageSize", + "description": "Page size (1-100).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "period", + "description": "Period filter (YYYYMM). Empty = all periods.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "search", + "description": "Free-text search on item_code, item_name, item_type_code, grade_code.", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/{groupHeadId}": { + "get": { + "summary": "GetRMGroup retrieves a group head + its details.", + "operationId": "RMGroupService_GetRMGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetRMGroupResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "groupHeadId", + "description": "Head UUID.", "in": "path", "required": true, "type": "string" } ], "tags": [ - "UOMCategoryService" + "RMGroupService" ] }, "delete": { - "summary": "DeleteUOMCategory soft deletes a UOM category.", - "operationId": "UOMCategoryService_DeleteUOMCategory", + "summary": "DeleteRMGroup soft-deletes a group head (cascade to its details).", + "operationId": "RMGroupService_DeleteRMGroup", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1DeleteUOMCategoryResponse" + "$ref": "#/definitions/v1DeleteRMGroupResponse" } }, "default": { @@ -1589,25 +1885,25 @@ }, "parameters": [ { - "name": "uomCategoryId", - "description": "UOM category ID to delete (UUID format).", + "name": "groupHeadId", + "description": "Head UUID.", "in": "path", "required": true, "type": "string" } ], "tags": [ - "UOMCategoryService" + "RMGroupService" ] }, "put": { - "summary": "UpdateUOMCategory updates an existing UOM category.\nNote: category_code is immutable and cannot be changed.", - "operationId": "UOMCategoryService_UpdateUOMCategory", + "summary": "UpdateRMGroup applies a partial update to a group head.", + "operationId": "RMGroupService_UpdateRMGroup", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1UpdateUOMCategoryResponse" + "$ref": "#/definitions/v1UpdateRMGroupResponse" } }, "default": { @@ -1619,8 +1915,8 @@ }, "parameters": [ { - "name": "uomCategoryId", - "description": "UOM category ID to update (UUID format).", + "name": "groupHeadId", + "description": "Head UUID to update.", "in": "path", "required": true, "type": "string" @@ -1630,178 +1926,1743 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/UOMCategoryServiceUpdateUOMCategoryBody" + "$ref": "#/definitions/RMGroupServiceUpdateRMGroupBody" } } ], "tags": [ - "UOMCategoryService" + "RMGroupService" ] } - } - }, - "definitions": { - "FormulaServiceUpdateFormulaBody": { - "type": "object", - "properties": { - "formulaName": { - "type": "string", - "description": "New display name (optional, 1-200 chars if provided)." - }, - "formulaType": { - "$ref": "#/definitions/v1FormulaType", - "description": "New formula type (optional)." + }, + "/api/v1/finance/rm-groups/{groupHeadId}/item-rates": { + "get": { + "summary": "GetRMGroupItemRates returns per-item per-stage rates for every active\ndetail of a group in a given period.", + "operationId": "RMGroupService_GetRMGroupItemRates", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetRMGroupItemRatesResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } }, - "expression": { - "type": "string", + "parameters": [ + { + "name": "groupHeadId", + "description": "Group head UUID.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "period", + "description": "Period (YYYYMM).", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/{groupHeadId}/items": { + "post": { + "summary": "AddItems assigns items to a group. Items already in another active group\nare returned in `skipped` instead of failing the batch.", + "operationId": "RMGroupService_AddItems", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1AddItemsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "groupHeadId", + "description": "Target group head UUID.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RMGroupServiceAddItemsBody" + } + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/rm-groups/{groupHeadId}/items/remove": { + "post": { + "summary": "RemoveItems removes details from a group (deactivate or soft-delete).", + "operationId": "RMGroupService_RemoveItems", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1RemoveItemsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "groupHeadId", + "description": "Owning group head UUID (validated against each detail).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RMGroupServiceRemoveItemsBody" + } + } + ], + "tags": [ + "RMGroupService" + ] + } + }, + "/api/v1/finance/uoms": { + "get": { + "summary": "ListUOMs lists UOMs with search, filter, and pagination.", + "operationId": "UOMService_ListUOMs", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListUOMsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "page", + "description": "Page number (1-indexed, default 1, min 1).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageSize", + "description": "Items per page (1-100, default 10).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "search", + "description": "Search query (searches in code, name, description).", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "activeFilter", + "description": "Filter by active status.\nACTIVE_FILTER_UNSPECIFIED (0) = show all, ACTIVE_FILTER_ACTIVE (1) = only active,\nACTIVE_FILTER_INACTIVE (2) = only inactive.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records.", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "ACTIVE_FILTER_UNSPECIFIED", + "ACTIVE_FILTER_ACTIVE", + "ACTIVE_FILTER_INACTIVE" + ], + "default": "ACTIVE_FILTER_UNSPECIFIED" + }, + { + "name": "sortBy", + "description": "Sort field: \"code\", \"name\", \"category\", \"created_at\" (default: \"code\").", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "sortOrder", + "description": "Sort order: \"asc\", \"desc\" (default: \"asc\").", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "uomCategoryId", + "description": "Filter by category ID (empty = no filter).", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "UOMService" + ] + }, + "post": { + "summary": "CreateUOM creates a new UOM.", + "operationId": "UOMService_CreateUOM", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1CreateUOMResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "CreateUOMRequest is the request for creating a new UOM.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1CreateUOMRequest" + } + } + ], + "tags": [ + "UOMService" + ] + } + }, + "/api/v1/finance/uoms/export": { + "get": { + "summary": "ExportUOMs exports UOMs to Excel file.", + "operationId": "UOMService_ExportUOMs", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ExportUOMsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "activeFilter", + "description": "Filter by active status.\nACTIVE_FILTER_UNSPECIFIED (0) = export all, ACTIVE_FILTER_ACTIVE (1) = only active,\nACTIVE_FILTER_INACTIVE (2) = only inactive.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records.", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "ACTIVE_FILTER_UNSPECIFIED", + "ACTIVE_FILTER_ACTIVE", + "ACTIVE_FILTER_INACTIVE" + ], + "default": "ACTIVE_FILTER_UNSPECIFIED" + }, + { + "name": "uomCategoryId", + "description": "Filter by category ID (empty = export all categories).", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "UOMService" + ] + } + }, + "/api/v1/finance/uoms/import": { + "post": { + "summary": "ImportUOMs imports UOMs from Excel file.", + "operationId": "UOMService_ImportUOMs", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ImportUOMsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "ImportUOMsRequest is the request for importing UOMs from Excel.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1ImportUOMsRequest" + } + } + ], + "tags": [ + "UOMService" + ] + } + }, + "/api/v1/finance/uoms/template": { + "get": { + "summary": "DownloadTemplate downloads the Excel import template.", + "operationId": "UOMService_DownloadTemplate", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1DownloadTemplateResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "UOMService" + ] + } + }, + "/api/v1/finance/uoms/{uomId}": { + "get": { + "summary": "GetUOM retrieves a UOM by ID.", + "operationId": "UOMService_GetUOM", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetUOMResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "uomId", + "description": "UOM ID (UUID format).", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "UOMService" + ] + }, + "delete": { + "summary": "DeleteUOM soft deletes a UOM.", + "operationId": "UOMService_DeleteUOM", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1DeleteUOMResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "uomId", + "description": "UOM ID to delete (UUID format).", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "UOMService" + ] + }, + "put": { + "summary": "UpdateUOM updates an existing UOM.\nNote: uom_code is immutable and cannot be changed.", + "operationId": "UOMService_UpdateUOM", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1UpdateUOMResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "uomId", + "description": "UOM ID to update (UUID format).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UOMServiceUpdateUOMBody" + } + } + ], + "tags": [ + "UOMService" + ] + } + }, + "/api/v1/finance/uom-categories": { + "get": { + "summary": "ListUOMCategories lists UOM categories with search, filter, and pagination.", + "operationId": "UOMCategoryService_ListUOMCategories", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListUOMCategoriesResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "page", + "description": "Page number (1-indexed, default 1, min 1).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageSize", + "description": "Items per page (1-100, default 10).", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "search", + "description": "Search query (searches in code, name, description).", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "activeFilter", + "description": "Filter by active status.\nACTIVE_FILTER_UNSPECIFIED (0) = show all, ACTIVE_FILTER_ACTIVE (1) = only active,\nACTIVE_FILTER_INACTIVE (2) = only inactive.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records.", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "ACTIVE_FILTER_UNSPECIFIED", + "ACTIVE_FILTER_ACTIVE", + "ACTIVE_FILTER_INACTIVE" + ], + "default": "ACTIVE_FILTER_UNSPECIFIED" + }, + { + "name": "sortBy", + "description": "Sort field: \"code\", \"name\", \"created_at\" (default: \"code\").", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "sortOrder", + "description": "Sort order: \"asc\", \"desc\" (default: \"asc\").", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "UOMCategoryService" + ] + }, + "post": { + "summary": "CreateUOMCategory creates a new UOM category.", + "operationId": "UOMCategoryService_CreateUOMCategory", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1CreateUOMCategoryResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "CreateUOMCategoryRequest is the request for creating a new UOM category.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1CreateUOMCategoryRequest" + } + } + ], + "tags": [ + "UOMCategoryService" + ] + } + }, + "/api/v1/finance/uom-categories/export": { + "get": { + "summary": "ExportUOMCategories exports UOM categories to Excel file.", + "operationId": "UOMCategoryService_ExportUOMCategories", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ExportUOMCategoriesResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "activeFilter", + "description": "Filter by active status.\nACTIVE_FILTER_UNSPECIFIED (0) = export all, ACTIVE_FILTER_ACTIVE (1) = only active,\nACTIVE_FILTER_INACTIVE (2) = only inactive.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records.", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "ACTIVE_FILTER_UNSPECIFIED", + "ACTIVE_FILTER_ACTIVE", + "ACTIVE_FILTER_INACTIVE" + ], + "default": "ACTIVE_FILTER_UNSPECIFIED" + } + ], + "tags": [ + "UOMCategoryService" + ] + } + }, + "/api/v1/finance/uom-categories/import": { + "post": { + "summary": "ImportUOMCategories imports UOM categories from Excel file.", + "operationId": "UOMCategoryService_ImportUOMCategories", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ImportUOMCategoriesResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "ImportUOMCategoriesRequest is the request for importing UOM categories from Excel.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1ImportUOMCategoriesRequest" + } + } + ], + "tags": [ + "UOMCategoryService" + ] + } + }, + "/api/v1/finance/uom-categories/template": { + "get": { + "summary": "DownloadUOMCategoryTemplate downloads the Excel import template.", + "operationId": "UOMCategoryService_DownloadUOMCategoryTemplate", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1DownloadUOMCategoryTemplateResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "UOMCategoryService" + ] + } + }, + "/api/v1/finance/uom-categories/{uomCategoryId}": { + "get": { + "summary": "GetUOMCategory retrieves a UOM category by ID.", + "operationId": "UOMCategoryService_GetUOMCategory", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetUOMCategoryResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "uomCategoryId", + "description": "UOM category ID (UUID format).", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "UOMCategoryService" + ] + }, + "delete": { + "summary": "DeleteUOMCategory soft deletes a UOM category.", + "operationId": "UOMCategoryService_DeleteUOMCategory", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1DeleteUOMCategoryResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "uomCategoryId", + "description": "UOM category ID to delete (UUID format).", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "UOMCategoryService" + ] + }, + "put": { + "summary": "UpdateUOMCategory updates an existing UOM category.\nNote: category_code is immutable and cannot be changed.", + "operationId": "UOMCategoryService_UpdateUOMCategory", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1UpdateUOMCategoryResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "uomCategoryId", + "description": "UOM category ID to update (UUID format).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UOMCategoryServiceUpdateUOMCategoryBody" + } + } + ], + "tags": [ + "UOMCategoryService" + ] + } + } + }, + "definitions": { + "FormulaServiceUpdateFormulaBody": { + "type": "object", + "properties": { + "formulaName": { + "type": "string", + "description": "New display name (optional, 1-200 chars if provided)." + }, + "formulaType": { + "$ref": "#/definitions/v1FormulaType", + "description": "New formula type (optional)." + }, + "expression": { + "type": "string", "description": "New expression (optional, 1-5000 chars if provided)." }, - "resultParamId": { + "resultParamId": { + "type": "string", + "description": "New result parameter ID (optional, UUID)." + }, + "inputParamIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "New input parameter IDs (optional, replaces all existing)." + }, + "description": { + "type": "string", + "description": "New description (optional, max 1000 chars)." + }, + "isActive": { + "type": "boolean", + "description": "New active status (optional)." + } + }, + "description": "UpdateFormulaRequest is the request for updating a formula.\nNote: formula_code is immutable and cannot be changed after creation." + }, + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "v1ActiveFilter": { + "type": "string", + "enum": [ + "ACTIVE_FILTER_UNSPECIFIED", + "ACTIVE_FILTER_ACTIVE", + "ACTIVE_FILTER_INACTIVE" + ], + "default": "ACTIVE_FILTER_UNSPECIFIED", + "description": "ActiveFilter represents filter options for is_active field.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records." + }, + "v1AuditInfo": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "description": "Timestamp when the record was created (ISO 8601)." + }, + "createdBy": { + "type": "string", + "description": "User who created the record." + }, + "updatedAt": { + "type": "string", + "description": "Timestamp when the record was last updated (ISO 8601)." + }, + "updatedBy": { + "type": "string", + "description": "User who last updated the record." + } + }, + "description": "AuditInfo contains audit trail information." + }, + "v1BaseResponse": { + "type": "object", + "properties": { + "validationErrors": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ValidationError" + }, + "description": "List of validation errors if any." + }, + "statusCode": { + "type": "string", + "description": "HTTP-like status code (e.g., \"200\", \"400\", \"404\", \"500\")." + }, + "isSuccess": { + "type": "boolean", + "description": "Indicates if the operation was successful." + }, + "message": { + "type": "string", + "description": "Human-readable message describing the result." + } + }, + "description": "BaseResponse is the standard response wrapper for all API responses." + }, + "v1CreateFormulaRequest": { + "type": "object", + "properties": { + "formulaCode": { + "type": "string", + "description": "Unique code (uppercase, alphanumeric with underscore, 1-50 chars)." + }, + "formulaName": { + "type": "string", + "description": "Display name (1-200 chars)." + }, + "formulaType": { + "$ref": "#/definitions/v1FormulaType", + "description": "Formula type (required, cannot be UNSPECIFIED)." + }, + "expression": { + "type": "string", + "description": "Expression (required, 1-5000 chars)." + }, + "resultParamId": { + "type": "string", + "description": "Result parameter ID (UUID, required)." + }, + "inputParamIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Input parameter IDs (at least 1 for CALCULATION type)." + }, + "description": { + "type": "string", + "description": "Optional description (max 1000 chars)." + } + }, + "description": "CreateFormulaRequest is the request for creating a new formula." + }, + "v1CreateFormulaResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "data": { + "$ref": "#/definitions/v1Formula", + "description": "Created formula data." + } + }, + "description": "CreateFormulaResponse is the response for creating a formula." + }, + "v1DeleteFormulaResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + } + }, + "description": "DeleteFormulaResponse is the response for deleting a formula." + }, + "v1DownloadFormulaTemplateResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "fileContent": { + "type": "string", + "format": "byte", + "description": "Excel template file content as bytes (.xlsx format)." + }, + "fileName": { + "type": "string", + "description": "Suggested filename." + } + }, + "description": "DownloadFormulaTemplateResponse is the response containing the template file." + }, + "v1ExportFormulasResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "fileContent": { + "type": "string", + "format": "byte", + "description": "Excel file content as bytes (.xlsx format)." + }, + "fileName": { + "type": "string", + "description": "Suggested filename." + } + }, + "description": "ExportFormulasResponse is the response containing the Excel file." + }, + "v1Formula": { + "type": "object", + "properties": { + "formulaId": { + "type": "string", + "description": "Unique identifier (UUID)." + }, + "formulaCode": { + "type": "string", + "description": "Unique code (e.g., \"COST_ELEC_STD\"). Immutable after creation." + }, + "formulaName": { + "type": "string", + "description": "Display name (e.g., \"Electricity Cost Standard\")." + }, + "formulaType": { + "$ref": "#/definitions/v1FormulaType", + "description": "Type of formula expression." + }, + "expression": { + "type": "string", + "description": "Expression text (e.g., \"ELEC_CONSUMPTION * ELEC_RATE\" or SQL query)." + }, + "resultParamId": { + "type": "string", + "description": "Result/output parameter ID (UUID FK to mst_parameter)." + }, + "resultParamCode": { + "type": "string", + "description": "Resolved result parameter code (read-only, from join)." + }, + "resultParamName": { + "type": "string", + "description": "Resolved result parameter name (read-only, from join)." + }, + "description": { + "type": "string", + "description": "Optional description." + }, + "version": { + "type": "integer", + "format": "int32", + "description": "Formula version number." + }, + "isActive": { + "type": "boolean", + "description": "Whether the formula is active." + }, + "audit": { + "$ref": "#/definitions/v1AuditInfo", + "description": "Audit information." + }, + "inputParams": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1FormulaParam" + }, + "description": "Input parameters (from junction table)." + } + }, + "description": "Formula represents a master formula entity for costing calculations." + }, + "v1FormulaParam": { + "type": "object", + "properties": { + "formulaParamId": { + "type": "string", + "description": "Unique identifier (UUID)." + }, + "paramId": { + "type": "string", + "description": "Parameter ID reference (UUID)." + }, + "paramCode": { + "type": "string", + "description": "Resolved parameter code (read-only, from join)." + }, + "paramName": { + "type": "string", + "description": "Resolved parameter name (read-only, from join)." + }, + "sortOrder": { + "type": "integer", + "format": "int32", + "description": "Display order of the parameter in the formula." + } + }, + "description": "FormulaParam represents an input parameter reference within a formula." + }, + "v1FormulaType": { + "type": "string", + "enum": [ + "FORMULA_TYPE_UNSPECIFIED", + "FORMULA_TYPE_CALCULATION", + "FORMULA_TYPE_SQL_QUERY", + "FORMULA_TYPE_CONSTANT" + ], + "default": "FORMULA_TYPE_UNSPECIFIED", + "description": "FormulaType represents the type of a formula expression.\n\n - FORMULA_TYPE_UNSPECIFIED: Default unspecified value - used as \"no filter\" in list/export requests.\n - FORMULA_TYPE_CALCULATION: Mathematical calculation using parameter codes (e.g., \"ELEC_CONSUMPTION * ELEC_RATE\").\n - FORMULA_TYPE_SQL_QUERY: SQL query to fetch data from other tables.\n - FORMULA_TYPE_CONSTANT: Constant value (no calculation)." + }, + "v1GetFormulaResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "data": { + "$ref": "#/definitions/v1Formula", + "description": "Formula data." + } + }, + "description": "GetFormulaResponse is the response for getting a formula." + }, + "v1ImportError": { + "type": "object", + "properties": { + "rowNumber": { + "type": "integer", + "format": "int32", + "description": "Row number in the Excel file." + }, + "field": { + "type": "string", + "description": "Field that caused the error." + }, + "message": { + "type": "string", + "description": "Error message." + } + }, + "description": "ImportError represents a single import error." + }, + "v1ImportFormulasRequest": { + "type": "object", + "properties": { + "fileContent": { + "type": "string", + "format": "byte", + "description": "Excel file content as bytes (.xlsx or .xls format)." + }, + "fileName": { + "type": "string", + "description": "Original filename (for format detection)." + }, + "duplicateAction": { + "type": "string", + "description": "Required. How to handle duplicates: \"skip\", \"update\", \"error\"." + } + }, + "description": "ImportFormulasRequest is the request for importing formulas from Excel." + }, + "v1ImportFormulasResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "successCount": { + "type": "integer", + "format": "int32", + "description": "Number of successfully imported records." + }, + "skippedCount": { + "type": "integer", + "format": "int32", + "description": "Number of skipped records (duplicates with skip action)." + }, + "updatedCount": { + "type": "integer", + "format": "int32", + "description": "Number of updated records (duplicates with update action)." + }, + "failedCount": { + "type": "integer", + "format": "int32", + "description": "Number of failed records." + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ImportError" + }, + "description": "Details of failed records." + } + }, + "description": "ImportFormulasResponse is the response for importing formulas." + }, + "v1ListFormulasResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1Formula" + }, + "description": "List of formulas." + }, + "pagination": { + "$ref": "#/definitions/v1PaginationResponse", + "description": "Pagination metadata." + } + }, + "description": "ListFormulasResponse is the response for listing formulas." + }, + "v1PaginationResponse": { + "type": "object", + "properties": { + "currentPage": { + "type": "integer", + "format": "int32", + "description": "Current page number." + }, + "pageSize": { + "type": "integer", + "format": "int32", + "description": "Number of items per page." + }, + "totalItems": { + "type": "string", + "format": "int64", + "description": "Total number of items across all pages." + }, + "totalPages": { + "type": "integer", + "format": "int32", + "description": "Total number of pages." + } + }, + "description": "PaginationResponse contains pagination metadata for list responses." + }, + "v1UpdateFormulaResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "data": { + "$ref": "#/definitions/v1Formula", + "description": "Updated formula data." + } + }, + "description": "UpdateFormulaResponse is the response for updating a formula." + }, + "v1ValidationError": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "The field name that failed validation." + }, + "message": { + "type": "string", + "description": "The validation error message." + } + }, + "description": "ValidationError represents a single field validation error." + }, + "OracleSyncServiceCancelSyncJobBody": { + "type": "object", + "description": "CancelSyncJobRequest cancels a queued or processing sync job." + }, + "v1CancelSyncJobResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "$ref": "#/definitions/v1SyncJob", + "description": "The cancelled sync job." + } + }, + "description": "CancelSyncJobResponse returns the cancelled job." + }, + "v1GetSyncJobResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "$ref": "#/definitions/v1SyncJob", + "description": "The sync job with logs." + } + }, + "description": "GetSyncJobResponse returns a single sync job with logs." + }, + "v1ItemConsStockPO": { + "type": "object", + "properties": { + "period": { "type": "string", - "description": "New result parameter ID (optional, UUID)." + "description": "Period (YYYYMM format)." }, - "inputParamIds": { + "itemCode": { + "type": "string", + "description": "Item code." + }, + "gradeCode": { + "type": "string", + "description": "Grade code." + }, + "gradeName": { + "type": "string", + "description": "Grade name." + }, + "itemName": { + "type": "string", + "description": "Item name." + }, + "uom": { + "type": "string", + "description": "Unit of measure." + }, + "consQty": { + "type": "number", + "format": "double", + "description": "Consumption quantity." + }, + "consVal": { + "type": "number", + "format": "double", + "description": "Consumption value." + }, + "consRate": { + "type": "number", + "format": "double", + "description": "Consumption rate." + }, + "storesQty": { + "type": "number", + "format": "double", + "description": "Stores quantity." + }, + "storesVal": { + "type": "number", + "format": "double", + "description": "Stores value." + }, + "storesRate": { + "type": "number", + "format": "double", + "description": "Stores rate." + }, + "deptQty": { + "type": "number", + "format": "double", + "description": "Department quantity." + }, + "deptVal": { + "type": "number", + "format": "double", + "description": "Department value." + }, + "deptRate": { + "type": "number", + "format": "double", + "description": "Department rate." + }, + "lastPoQty1": { + "type": "number", + "format": "double", + "description": "Last PO 1 quantity." + }, + "lastPoVal1": { + "type": "number", + "format": "double", + "description": "Last PO 1 value." + }, + "lastPoRate1": { + "type": "number", + "format": "double", + "description": "Last PO 1 rate." + }, + "lastPoDt1": { + "type": "string", + "description": "Last PO 1 date (ISO 8601)." + }, + "lastPoQty2": { + "type": "number", + "format": "double", + "description": "Last PO 2 quantity." + }, + "lastPoVal2": { + "type": "number", + "format": "double", + "description": "Last PO 2 value." + }, + "lastPoRate2": { + "type": "number", + "format": "double", + "description": "Last PO 2 rate." + }, + "lastPoDt2": { + "type": "string", + "description": "Last PO 2 date (ISO 8601)." + }, + "lastPoQty3": { + "type": "number", + "format": "double", + "description": "Last PO 3 quantity." + }, + "lastPoVal3": { + "type": "number", + "format": "double", + "description": "Last PO 3 value." + }, + "lastPoRate3": { + "type": "number", + "format": "double", + "description": "Last PO 3 rate." + }, + "lastPoDt3": { + "type": "string", + "description": "Last PO 3 date (ISO 8601)." + }, + "syncedAt": { + "type": "string", + "description": "Timestamp when the record was synced (ISO 8601)." + }, + "syncedByJob": { + "type": "string", + "description": "Job ID that synced this record (UUID)." + } + }, + "description": "ItemConsStockPO represents a synced item consumption, stock, and PO record." + }, + "v1JobLogStatus": { + "type": "string", + "enum": [ + "JOB_LOG_STATUS_UNSPECIFIED", + "JOB_LOG_STATUS_STARTED", + "JOB_LOG_STATUS_SUCCESS", + "JOB_LOG_STATUS_FAILED", + "JOB_LOG_STATUS_SKIPPED" + ], + "default": "JOB_LOG_STATUS_UNSPECIFIED", + "description": "JobLogStatus represents the status of a job execution log step.\n\n - JOB_LOG_STATUS_UNSPECIFIED: Default unspecified value.\n - JOB_LOG_STATUS_STARTED: Step has started.\n - JOB_LOG_STATUS_SUCCESS: Step completed successfully.\n - JOB_LOG_STATUS_FAILED: Step failed.\n - JOB_LOG_STATUS_SKIPPED: Step was skipped." + }, + "v1JobStatus": { + "type": "string", + "enum": [ + "JOB_STATUS_UNSPECIFIED", + "JOB_STATUS_QUEUED", + "JOB_STATUS_PROCESSING", + "JOB_STATUS_SUCCESS", + "JOB_STATUS_FAILED", + "JOB_STATUS_CANCELLED" + ], + "default": "JOB_STATUS_UNSPECIFIED", + "description": "JobStatus represents the current state of a job execution.\n\n - JOB_STATUS_UNSPECIFIED: Default unspecified value.\n - JOB_STATUS_QUEUED: Job is queued and waiting for processing.\n - JOB_STATUS_PROCESSING: Job is currently being processed.\n - JOB_STATUS_SUCCESS: Job completed successfully.\n - JOB_STATUS_FAILED: Job failed.\n - JOB_STATUS_CANCELLED: Job was cancelled." + }, + "v1ListItemConsStockPOResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { "type": "array", "items": { - "type": "string" + "type": "object", + "$ref": "#/definitions/v1ItemConsStockPO" }, - "description": "New input parameter IDs (optional, replaces all existing)." + "description": "List of synced records." }, - "description": { - "type": "string", - "description": "New description (optional, max 1000 chars)." + "pagination": { + "$ref": "#/definitions/v1PaginationResponse", + "description": "Pagination metadata." + } + }, + "description": "ListItemConsStockPOResponse returns synced item consumption data." + }, + "v1ListSyncJobsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." }, - "isActive": { - "type": "boolean", - "description": "New active status (optional)." + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1SyncJob" + }, + "description": "List of sync jobs." + }, + "pagination": { + "$ref": "#/definitions/v1PaginationResponse", + "description": "Pagination metadata." } }, - "description": "UpdateFormulaRequest is the request for updating a formula.\nNote: formula_code is immutable and cannot be changed after creation." + "description": "ListSyncJobsResponse returns a paginated list of sync jobs." }, - "protobufAny": { + "v1ListSyncPeriodsResponse": { "type": "object", "properties": { - "@type": { - "type": "string" + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "periods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of periods in YYYYMM format, ordered descending." } }, - "additionalProperties": {} + "description": "ListSyncPeriodsResponse returns the list of distinct periods." }, - "rpcStatus": { + "v1SyncJob": { "type": "object", "properties": { - "code": { + "jobId": { + "type": "string", + "description": "Unique job identifier (UUID)." + }, + "jobCode": { + "type": "string", + "description": "Human-readable job code (e.g., \"ORACLE_SYNC-202601-001\")." + }, + "jobType": { + "type": "string", + "description": "Job type (e.g., \"oracle_sync\")." + }, + "jobSubtype": { + "type": "string", + "description": "Job subtype for categorization." + }, + "period": { + "type": "string", + "description": "Period being synced (YYYYMM format)." + }, + "status": { + "$ref": "#/definitions/v1JobStatus", + "description": "Current job status." + }, + "priority": { "type": "integer", - "format": "int32" + "format": "int32", + "description": "Priority (1-10, lower is higher priority)." }, - "message": { - "type": "string" + "progress": { + "type": "integer", + "format": "int32", + "description": "Progress percentage (0-100)." }, - "details": { + "errorMessage": { + "type": "string", + "description": "Error message if the job failed." + }, + "resultSummary": { + "type": "string", + "description": "Result summary as JSON string." + }, + "retryCount": { + "type": "integer", + "format": "int32", + "description": "Number of retry attempts." + }, + "maxRetries": { + "type": "integer", + "format": "int32", + "description": "Maximum allowed retries." + }, + "queuedAt": { + "type": "string", + "description": "Timestamp when the job was queued (ISO 8601)." + }, + "startedAt": { + "type": "string", + "description": "Timestamp when processing started (ISO 8601)." + }, + "completedAt": { + "type": "string", + "description": "Timestamp when the job completed (ISO 8601)." + }, + "createdBy": { + "type": "string", + "description": "User who created the job." + }, + "cancelledBy": { + "type": "string", + "description": "User who cancelled the job." + }, + "cancelledAt": { + "type": "string", + "description": "Timestamp when the job was cancelled (ISO 8601)." + }, + "logs": { "type": "array", "items": { "type": "object", - "$ref": "#/definitions/protobufAny" - } + "$ref": "#/definitions/v1SyncJobLog" + }, + "description": "Execution logs for this job." } - } - }, - "v1ActiveFilter": { - "type": "string", - "enum": [ - "ACTIVE_FILTER_UNSPECIFIED", - "ACTIVE_FILTER_ACTIVE", - "ACTIVE_FILTER_INACTIVE" - ], - "default": "ACTIVE_FILTER_UNSPECIFIED", - "description": "ActiveFilter represents filter options for is_active field.\n\n - ACTIVE_FILTER_UNSPECIFIED: Show all records regardless of active status (default).\n - ACTIVE_FILTER_ACTIVE: Show only active records.\n - ACTIVE_FILTER_INACTIVE: Show only inactive records." + }, + "description": "SyncJob represents a job execution for Oracle-to-PostgreSQL sync." }, - "v1AuditInfo": { + "v1SyncJobLog": { "type": "object", "properties": { - "createdAt": { + "logId": { "type": "string", - "description": "Timestamp when the record was created (ISO 8601)." + "description": "Unique log identifier (UUID)." }, - "createdBy": { + "jobId": { "type": "string", - "description": "User who created the record." + "description": "Parent job identifier (UUID)." }, - "updatedAt": { + "step": { "type": "string", - "description": "Timestamp when the record was last updated (ISO 8601)." + "description": "Step name (e.g., \"execute_procedure\", \"fetch_data\", \"upsert_data\")." }, - "updatedBy": { + "status": { + "$ref": "#/definitions/v1JobLogStatus", + "description": "Step status." + }, + "message": { "type": "string", - "description": "User who last updated the record." + "description": "Log message." + }, + "metadata": { + "type": "string", + "description": "Additional metadata as JSON string." + }, + "startedAt": { + "type": "string", + "description": "Timestamp when the step started (ISO 8601)." + }, + "completedAt": { + "type": "string", + "description": "Timestamp when the step completed (ISO 8601)." + }, + "durationMs": { + "type": "integer", + "format": "int32", + "description": "Duration of the step in milliseconds." } }, - "description": "AuditInfo contains audit trail information." + "description": "SyncJobLog represents a single step log entry within a job execution." }, - "v1BaseResponse": { + "v1TriggerSyncRequest": { "type": "object", "properties": { - "validationErrors": { - "type": "array", - "items": { - "type": "object", - "$ref": "#/definitions/v1ValidationError" - }, - "description": "List of validation errors if any." + "period": { + "type": "string", + "description": "Period to sync (YYYYMM format). If empty, auto-resolved based on current date." + } + }, + "description": "TriggerSyncRequest initiates an Oracle-to-PostgreSQL sync job." + }, + "v1TriggerSyncResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." }, - "statusCode": { + "data": { + "$ref": "#/definitions/v1SyncJob", + "description": "The created sync job." + } + }, + "description": "TriggerSyncResponse returns the created job." + }, + "ParameterServiceUpdateParameterBody": { + "type": "object", + "properties": { + "paramName": { "type": "string", - "description": "HTTP-like status code (e.g., \"200\", \"400\", \"404\", \"500\")." + "description": "New display name (optional, 1-200 chars if provided)." }, - "isSuccess": { - "type": "boolean", - "description": "Indicates if the operation was successful." + "paramShortName": { + "type": "string", + "description": "New short name (optional, max 50 chars)." }, - "message": { + "dataType": { + "$ref": "#/definitions/v1DataType", + "description": "New data type (optional, cannot be UNSPECIFIED if provided)." + }, + "paramCategory": { + "$ref": "#/definitions/v1ParamCategory", + "description": "New category (optional, cannot be UNSPECIFIED if provided)." + }, + "uomId": { "type": "string", - "description": "Human-readable message describing the result." + "description": "New UOM reference (optional, UUID format, empty string to clear)." + }, + "defaultValue": { + "type": "string", + "description": "New default value (optional)." + }, + "minValue": { + "type": "string", + "description": "New minimum value (optional)." + }, + "maxValue": { + "type": "string", + "description": "New maximum value (optional)." + }, + "isActive": { + "type": "boolean", + "description": "New active status (optional)." } }, - "description": "BaseResponse is the standard response wrapper for all API responses." + "description": "UpdateParameterRequest is the request for updating a parameter.\nNote: param_code is immutable and cannot be changed after creation." }, - "v1CreateFormulaRequest": { + "v1CreateParameterRequest": { "type": "object", "properties": { - "formulaCode": { + "paramCode": { "type": "string", - "description": "Unique code (uppercase, alphanumeric with underscore, 1-50 chars)." + "description": "Unique code (uppercase, alphanumeric with underscore, 1-20 chars).\nMust start with an uppercase letter. Immutable after creation." }, - "formulaName": { + "paramName": { "type": "string", "description": "Display name (1-200 chars)." }, - "formulaType": { - "$ref": "#/definitions/v1FormulaType", - "description": "Formula type (required, cannot be UNSPECIFIED)." + "paramShortName": { + "type": "string", + "description": "Short name (optional, max 50 chars)." }, - "expression": { + "dataType": { + "$ref": "#/definitions/v1DataType", + "description": "Data type (required, cannot be UNSPECIFIED)." + }, + "paramCategory": { + "$ref": "#/definitions/v1ParamCategory", + "description": "Category (required, cannot be UNSPECIFIED)." + }, + "uomId": { "type": "string", - "description": "Expression (required, 1-5000 chars)." + "description": "Optional UOM reference (UUID format, empty string means no UOM)." }, - "resultParamId": { + "defaultValue": { "type": "string", - "description": "Result parameter ID (UUID, required)." + "description": "Default value (optional, decimal as string)." }, - "inputParamIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Input parameter IDs (at least 1 for CALCULATION type)." + "minValue": { + "type": "string", + "description": "Minimum value (optional, decimal as string)." }, - "description": { + "maxValue": { "type": "string", - "description": "Optional description (max 1000 chars)." + "description": "Maximum value (optional, decimal as string)." } }, - "description": "CreateFormulaRequest is the request for creating a new formula." + "description": "CreateParameterRequest is the request for creating a new parameter." }, - "v1CreateFormulaResponse": { + "v1CreateParameterResponse": { "type": "object", "properties": { "base": { @@ -1809,13 +3670,24 @@ "description": "Standard response metadata." }, "data": { - "$ref": "#/definitions/v1Formula", - "description": "Created formula data." + "$ref": "#/definitions/v1Parameter", + "description": "Created parameter data." } }, - "description": "CreateFormulaResponse is the response for creating a formula." + "description": "CreateParameterResponse is the response for creating a parameter." }, - "v1DeleteFormulaResponse": { + "v1DataType": { + "type": "string", + "enum": [ + "DATA_TYPE_UNSPECIFIED", + "DATA_TYPE_NUMBER", + "DATA_TYPE_TEXT", + "DATA_TYPE_BOOLEAN" + ], + "default": "DATA_TYPE_UNSPECIFIED", + "description": "DataType represents the data type of a parameter value.\n\n - DATA_TYPE_UNSPECIFIED: Default unspecified value - used as \"no filter\" in list/export requests.\n - DATA_TYPE_NUMBER: Numeric values (integer or decimal).\n - DATA_TYPE_TEXT: Text/string values.\n - DATA_TYPE_BOOLEAN: Boolean (true/false) values." + }, + "v1DeleteParameterResponse": { "type": "object", "properties": { "base": { @@ -1823,9 +3695,9 @@ "description": "Standard response metadata." } }, - "description": "DeleteFormulaResponse is the response for deleting a formula." + "description": "DeleteParameterResponse is the response for deleting a parameter." }, - "v1DownloadFormulaTemplateResponse": { + "v1DownloadParameterTemplateResponse": { "type": "object", "properties": { "base": { @@ -1842,9 +3714,9 @@ "description": "Suggested filename." } }, - "description": "DownloadFormulaTemplateResponse is the response containing the template file." + "description": "DownloadParameterTemplateResponse is the response containing the template file." }, - "v1ExportFormulasResponse": { + "v1ExportParametersResponse": { "type": "object", "properties": { "base": { @@ -1861,143 +3733,301 @@ "description": "Suggested filename." } }, - "description": "ExportFormulasResponse is the response containing the Excel file." + "description": "ExportParametersResponse is the response containing the Excel file." }, - "v1Formula": { + "v1GetParameterResponse": { "type": "object", "properties": { - "formulaId": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "data": { + "$ref": "#/definitions/v1Parameter", + "description": "Parameter data." + } + }, + "description": "GetParameterResponse is the response for getting a parameter." + }, + "v1ImportParametersRequest": { + "type": "object", + "properties": { + "fileContent": { "type": "string", - "description": "Unique identifier (UUID)." + "format": "byte", + "description": "Excel file content as bytes (.xlsx or .xls format)." }, - "formulaCode": { + "fileName": { "type": "string", - "description": "Unique code (e.g., \"COST_ELEC_STD\"). Immutable after creation." + "description": "Original filename (for format detection)." }, - "formulaName": { + "duplicateAction": { "type": "string", - "description": "Display name (e.g., \"Electricity Cost Standard\")." + "description": "Required. How to handle duplicates: \"skip\", \"update\", \"error\"." + } + }, + "description": "ImportParametersRequest is the request for importing parameters from Excel." + }, + "v1ImportParametersResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." }, - "formulaType": { - "$ref": "#/definitions/v1FormulaType", - "description": "Type of formula expression." + "successCount": { + "type": "integer", + "format": "int32", + "description": "Number of successfully imported records." }, - "expression": { + "skippedCount": { + "type": "integer", + "format": "int32", + "description": "Number of skipped records (duplicates with skip action)." + }, + "updatedCount": { + "type": "integer", + "format": "int32", + "description": "Number of updated records (duplicates with update action)." + }, + "failedCount": { + "type": "integer", + "format": "int32", + "description": "Number of failed records." + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ImportError" + }, + "description": "Details of failed records." + } + }, + "description": "ImportParametersResponse is the response for importing parameters." + }, + "v1ListParametersResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1Parameter" + }, + "description": "List of parameters." + }, + "pagination": { + "$ref": "#/definitions/v1PaginationResponse", + "description": "Pagination metadata." + } + }, + "description": "ListParametersResponse is the response for listing parameters." + }, + "v1ParamCategory": { + "type": "string", + "enum": [ + "PARAM_CATEGORY_UNSPECIFIED", + "PARAM_CATEGORY_INPUT", + "PARAM_CATEGORY_RATE", + "PARAM_CATEGORY_CALCULATED" + ], + "default": "PARAM_CATEGORY_UNSPECIFIED", + "description": "ParamCategory represents the category/purpose of a parameter.\n\n - PARAM_CATEGORY_UNSPECIFIED: Default unspecified value - used as \"no filter\" in list/export requests.\n - PARAM_CATEGORY_INPUT: Input parameter - manually entered values.\n - PARAM_CATEGORY_RATE: Rate parameter - rate/ratio values.\n - PARAM_CATEGORY_CALCULATED: Calculated parameter - computed from other parameters." + }, + "v1Parameter": { + "type": "object", + "properties": { + "paramId": { "type": "string", - "description": "Expression text (e.g., \"ELEC_CONSUMPTION * ELEC_RATE\" or SQL query)." + "description": "Unique identifier (UUID)." }, - "resultParamId": { + "paramCode": { "type": "string", - "description": "Result/output parameter ID (UUID FK to mst_parameter)." + "description": "Unique code (e.g., \"SPEED\", \"DENIER\", \"ELEC_RATE\"). Immutable after creation." }, - "resultParamCode": { + "paramName": { "type": "string", - "description": "Resolved result parameter code (read-only, from join)." + "description": "Display name (e.g., \"Speed\", \"Denier\")." }, - "resultParamName": { + "paramShortName": { "type": "string", - "description": "Resolved result parameter name (read-only, from join)." + "description": "Short name for compact display." }, - "description": { + "dataType": { + "$ref": "#/definitions/v1DataType", + "description": "Data type of the parameter value." + }, + "paramCategory": { + "$ref": "#/definitions/v1ParamCategory", + "description": "Category/purpose of the parameter." + }, + "defaultValue": { "type": "string", - "description": "Optional description." + "description": "Default value (decimal, as string for precision)." }, - "version": { - "type": "integer", - "format": "int32", - "description": "Formula version number." + "minValue": { + "type": "string", + "description": "Minimum allowed value (decimal, as string for precision)." + }, + "maxValue": { + "type": "string", + "description": "Maximum allowed value (decimal, as string for precision)." }, "isActive": { "type": "boolean", - "description": "Whether the formula is active." + "description": "Whether the parameter is active." }, "audit": { "$ref": "#/definitions/v1AuditInfo", "description": "Audit information." }, - "inputParams": { - "type": "array", - "items": { - "type": "object", - "$ref": "#/definitions/v1FormulaParam" - }, - "description": "Input parameters (from junction table)." + "uomId": { + "type": "string", + "description": "Optional UOM reference ID (UUID, nullable)." + }, + "uomCode": { + "type": "string", + "description": "Resolved UOM code (read-only, populated from mst_uom join)." + }, + "uomName": { + "type": "string", + "description": "Resolved UOM name (read-only, populated from mst_uom join)." } }, - "description": "Formula represents a master formula entity for costing calculations." + "description": "Parameter represents a master parameter entity for costing calculations." }, - "v1FormulaParam": { + "v1UpdateParameterResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "data": { + "$ref": "#/definitions/v1Parameter", + "description": "Updated parameter data." + } + }, + "description": "UpdateParameterResponse is the response for updating a parameter." + }, + "RMCategoryServiceUpdateRMCategoryBody": { + "type": "object", + "properties": { + "categoryName": { + "type": "string", + "description": "New display name (optional, 1-100 chars if provided)." + }, + "description": { + "type": "string", + "description": "New description (optional, max 500 chars)." + }, + "isActive": { + "type": "boolean", + "description": "New active status (optional)." + } + }, + "description": "UpdateRMCategoryRequest is the request for updating a raw material category.\nNote: category_code is immutable and cannot be changed after creation." + }, + "v1CreateRMCategoryRequest": { "type": "object", "properties": { - "formulaParamId": { - "type": "string", - "description": "Unique identifier (UUID)." - }, - "paramId": { + "categoryCode": { "type": "string", - "description": "Parameter ID reference (UUID)." + "description": "Unique code (uppercase, alphanumeric with underscore, 1-20 chars).\nMust start with an uppercase letter. Immutable after creation." }, - "paramCode": { + "categoryName": { "type": "string", - "description": "Resolved parameter code (read-only, from join)." + "description": "Display name (1-100 chars)." }, - "paramName": { + "description": { "type": "string", - "description": "Resolved parameter name (read-only, from join)." + "description": "Optional description (max 500 chars)." + } + }, + "description": "CreateRMCategoryRequest is the request for creating a new raw material category." + }, + "v1CreateRMCategoryResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." }, - "sortOrder": { - "type": "integer", - "format": "int32", - "description": "Display order of the parameter in the formula." + "data": { + "$ref": "#/definitions/v1RMCategory", + "description": "Created raw material category data." } }, - "description": "FormulaParam represents an input parameter reference within a formula." + "description": "CreateRMCategoryResponse is the response for creating a raw material category." }, - "v1FormulaType": { - "type": "string", - "enum": [ - "FORMULA_TYPE_UNSPECIFIED", - "FORMULA_TYPE_CALCULATION", - "FORMULA_TYPE_SQL_QUERY", - "FORMULA_TYPE_CONSTANT" - ], - "default": "FORMULA_TYPE_UNSPECIFIED", - "description": "FormulaType represents the type of a formula expression.\n\n - FORMULA_TYPE_UNSPECIFIED: Default unspecified value - used as \"no filter\" in list/export requests.\n - FORMULA_TYPE_CALCULATION: Mathematical calculation using parameter codes (e.g., \"ELEC_CONSUMPTION * ELEC_RATE\").\n - FORMULA_TYPE_SQL_QUERY: SQL query to fetch data from other tables.\n - FORMULA_TYPE_CONSTANT: Constant value (no calculation)." + "v1DeleteRMCategoryResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + } + }, + "description": "DeleteRMCategoryResponse is the response for deleting a raw material category." }, - "v1GetFormulaResponse": { + "v1DownloadRMCategoryTemplateResponse": { "type": "object", "properties": { "base": { "$ref": "#/definitions/v1BaseResponse", "description": "Standard response metadata." }, - "data": { - "$ref": "#/definitions/v1Formula", - "description": "Formula data." + "fileContent": { + "type": "string", + "format": "byte", + "description": "Excel template file content as bytes (.xlsx format)." + }, + "fileName": { + "type": "string", + "description": "Suggested filename." } }, - "description": "GetFormulaResponse is the response for getting a formula." + "description": "DownloadRMCategoryTemplateResponse is the response containing the template file." }, - "v1ImportError": { + "v1ExportRMCategoriesResponse": { "type": "object", "properties": { - "rowNumber": { - "type": "integer", - "format": "int32", - "description": "Row number in the Excel file." + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." }, - "field": { + "fileContent": { "type": "string", - "description": "Field that caused the error." + "format": "byte", + "description": "Excel file content as bytes (.xlsx format)." }, - "message": { + "fileName": { "type": "string", - "description": "Error message." + "description": "Suggested filename." } }, - "description": "ImportError represents a single import error." + "description": "ExportRMCategoriesResponse is the response containing the Excel file." }, - "v1ImportFormulasRequest": { + "v1GetRMCategoryResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "data": { + "$ref": "#/definitions/v1RMCategory", + "description": "Raw material category data." + } + }, + "description": "GetRMCategoryResponse is the response for getting a raw material category." + }, + "v1ImportRMCategoriesRequest": { "type": "object", "properties": { "fileContent": { @@ -2007,16 +4037,16 @@ }, "fileName": { "type": "string", - "description": "Original filename (for format detection)." + "description": "Original filename (for format detection).\nMust be a simple filename without path separators." }, "duplicateAction": { "type": "string", "description": "Required. How to handle duplicates: \"skip\", \"update\", \"error\"." } }, - "description": "ImportFormulasRequest is the request for importing formulas from Excel." + "description": "ImportRMCategoriesRequest is the request for importing raw material categories from Excel." }, - "v1ImportFormulasResponse": { + "v1ImportRMCategoriesResponse": { "type": "object", "properties": { "base": { @@ -2052,9 +4082,9 @@ "description": "Details of failed records." } }, - "description": "ImportFormulasResponse is the response for importing formulas." + "description": "ImportRMCategoriesResponse is the response for importing raw material categories." }, - "v1ListFormulasResponse": { + "v1ListRMCategoriesResponse": { "type": "object", "properties": { "base": { @@ -2065,565 +4095,811 @@ "type": "array", "items": { "type": "object", - "$ref": "#/definitions/v1Formula" + "$ref": "#/definitions/v1RMCategory" }, - "description": "List of formulas." + "description": "List of raw material categories." }, "pagination": { "$ref": "#/definitions/v1PaginationResponse", "description": "Pagination metadata." } }, - "description": "ListFormulasResponse is the response for listing formulas." + "description": "ListRMCategoriesResponse is the response for listing raw material categories." }, - "v1PaginationResponse": { + "v1RMCategory": { "type": "object", "properties": { - "currentPage": { + "rmCategoryId": { + "type": "string", + "description": "Unique identifier (UUID)." + }, + "categoryCode": { + "type": "string", + "description": "Unique code (e.g., \"CHIP\", \"OIL\", \"DYES\"). Immutable after creation." + }, + "categoryName": { + "type": "string", + "description": "Display name (e.g., \"Chips\", \"Oil\", \"Dyes\")." + }, + "description": { + "type": "string", + "description": "Optional description." + }, + "isActive": { + "type": "boolean", + "description": "Whether the category is active." + }, + "audit": { + "$ref": "#/definitions/v1AuditInfo", + "description": "Audit information." + } + }, + "description": "RMCategory represents a Raw Material Category entity.\nUsed to classify raw materials (e.g., Chips, Oil, Dyes, Chemicals)." + }, + "v1UpdateRMCategoryResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "data": { + "$ref": "#/definitions/v1RMCategory", + "description": "Updated raw material category data." + } + }, + "description": "UpdateRMCategoryResponse is the response for updating a raw material category." + }, + "v1CalculateRMCostRequest": { + "type": "object", + "properties": { + "period": { + "type": "string", + "description": "Period (YYYYMM)." + }, + "groupHeadId": { + "type": "string", + "description": "Optional single-group scope." + }, + "triggerReason": { + "$ref": "#/definitions/v1RMCostTriggerReason", + "description": "Why this calc was run." + } + }, + "description": "CalculateRMCost runs a calculation synchronously and returns the produced rows.\nIntended for admin/troubleshooting use \u2014 production traffic should go through\nTriggerRMCostCalculation." + }, + "v1CalculateRMCostResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "processed": { "type": "integer", "format": "int32", - "description": "Current page number." + "description": "Number of group heads processed." }, - "pageSize": { + "skipped": { "type": "integer", "format": "int32", - "description": "Number of items per page." + "description": "Number of group heads skipped (no active details / no source rows)." }, - "totalItems": { + "period": { "type": "string", - "format": "int64", - "description": "Total number of items across all pages." + "description": "Period that was calculated (echoes request)." + } + }, + "description": "Calculate response." + }, + "v1GetRMCostResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." }, - "totalPages": { - "type": "integer", - "format": "int32", - "description": "Total number of pages." + "data": { + "$ref": "#/definitions/v1RMCost", + "description": "Cost data." } }, - "description": "PaginationResponse contains pagination metadata for list responses." + "description": "Get response." }, - "v1UpdateFormulaResponse": { + "v1ListRMCostHistoryResponse": { "type": "object", "properties": { "base": { "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." + "description": "Standard response envelope." }, "data": { - "$ref": "#/definitions/v1Formula", - "description": "Updated formula data." + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1RMCostHistory" + }, + "description": "History rows ordered by calculated_at DESC." + }, + "pagination": { + "$ref": "#/definitions/v1PaginationResponse", + "description": "Pagination metadata." } }, - "description": "UpdateFormulaResponse is the response for updating a formula." + "description": "History response." }, - "v1ValidationError": { + "v1ListRMCostPeriodsResponse": { "type": "object", "properties": { - "field": { - "type": "string", - "description": "The field name that failed validation." + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." }, - "message": { - "type": "string", - "description": "The validation error message." + "periods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Distinct periods ordered DESC (newest first), YYYYMM strings." } }, - "description": "ValidationError represents a single field validation error." + "description": "Response carries the distinct set of calculated periods." }, - "ParameterServiceUpdateParameterBody": { + "v1ListRMCostsResponse": { "type": "object", "properties": { - "paramName": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1RMCost" + }, + "description": "Cost rows." + }, + "pagination": { + "$ref": "#/definitions/v1PaginationResponse", + "description": "Pagination metadata." + } + }, + "description": "List response." + }, + "v1RMCost": { + "type": "object", + "properties": { + "rmCostId": { "type": "string", - "description": "New display name (optional, 1-200 chars if provided)." + "description": "Cost row UUID." + }, + "period": { + "type": "string", + "description": "Period (YYYYMM)." + }, + "rmCode": { + "type": "string", + "description": "Group code (rm_type=GROUP) or item code (rm_type=ITEM)." + }, + "rmType": { + "$ref": "#/definitions/v1RMCostType", + "description": "Discriminator." + }, + "groupHeadId": { + "type": "string", + "description": "Owning group head UUID (set when rm_type=GROUP)." + }, + "itemCode": { + "type": "string", + "description": "Item code (set when rm_type=ITEM)." + }, + "rmName": { + "type": "string", + "description": "Display name of the RM (group or item)." }, - "paramShortName": { + "uomCode": { "type": "string", - "description": "New short name (optional, max 50 chars)." + "description": "UOM code." }, - "dataType": { - "$ref": "#/definitions/v1DataType", - "description": "New data type (optional, cannot be UNSPECIFIED if provided)." + "rates": { + "$ref": "#/definitions/v1RMCostRates", + "description": "Per-stage rate snapshot." }, - "paramCategory": { - "$ref": "#/definitions/v1ParamCategory", - "description": "New category (optional, cannot be UNSPECIFIED if provided)." + "costValuation": { + "type": "number", + "format": "double", + "description": "Computed valuation landed cost (nil when never calculated)." }, - "uomId": { - "type": "string", - "description": "New UOM reference (optional, UUID format, empty string to clear)." + "costMarketing": { + "type": "number", + "format": "double", + "description": "Computed marketing landed cost." }, - "defaultValue": { - "type": "string", - "description": "New default value (optional)." + "costSimulation": { + "type": "number", + "format": "double", + "description": "Computed simulation landed cost." }, - "minValue": { + "flagValuation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Flags configured on the group header at calc time." + }, + "flagMarketing": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Flag configured on the group header at calc time." + }, + "flagSimulation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Flag configured on the group header at calc time." + }, + "flagValuationUsed": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stage actually used after cascade / INIT resolution." + }, + "flagMarketingUsed": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stage actually used after cascade / INIT resolution." + }, + "flagSimulationUsed": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stage actually used after cascade / INIT resolution." + }, + "calculatedAt": { "type": "string", - "description": "New minimum value (optional)." + "description": "Last calculation timestamp (RFC3339, empty when never calculated)." }, - "maxValue": { + "calculatedBy": { "type": "string", - "description": "New maximum value (optional)." + "description": "Last calculator (empty when never calculated)." }, - "isActive": { - "type": "boolean", - "description": "New active status (optional)." + "audit": { + "$ref": "#/definitions/v1AuditInfo", + "description": "Audit metadata." } }, - "description": "UpdateParameterRequest is the request for updating a parameter.\nNote: param_code is immutable and cannot be changed after creation." + "description": "RMCost is the landed cost computed for a single (period, rm_code) pair." }, - "v1CreateParameterRequest": { + "v1RMCostHistory": { "type": "object", "properties": { - "paramCode": { + "historyId": { "type": "string", - "description": "Unique code (uppercase, alphanumeric with underscore, 1-20 chars).\nMust start with an uppercase letter. Immutable after creation." + "description": "History row UUID." }, - "paramName": { + "rmCostId": { "type": "string", - "description": "Display name (1-200 chars)." + "description": "Cost row this history refers to (nil if the cost row was later deleted)." }, - "paramShortName": { + "jobId": { "type": "string", - "description": "Short name (optional, max 50 chars)." - }, - "dataType": { - "$ref": "#/definitions/v1DataType", - "description": "Data type (required, cannot be UNSPECIFIED)." + "description": "Job that produced this history row (nil when no job context)." }, - "paramCategory": { - "$ref": "#/definitions/v1ParamCategory", - "description": "Category (required, cannot be UNSPECIFIED)." + "period": { + "type": "string", + "description": "Period." }, - "uomId": { + "rmCode": { "type": "string", - "description": "Optional UOM reference (UUID format, empty string means no UOM)." + "description": "RM code." }, - "defaultValue": { + "rmType": { + "$ref": "#/definitions/v1RMCostType", + "description": "Discriminator." + }, + "groupHeadId": { "type": "string", - "description": "Default value (optional, decimal as string)." + "description": "Owning group head UUID (when rm_type=GROUP)." + }, + "rates": { + "$ref": "#/definitions/v1RMCostRates", + "description": "Rates captured for this calc pass." + }, + "costPercentage": { + "type": "number", + "format": "double", + "description": "Snapshot of head.cost_percentage at calc time." + }, + "costPerKg": { + "type": "number", + "format": "double", + "description": "Snapshot of head.cost_per_kg at calc time." + }, + "flagValuation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Configured flags at calc time." + }, + "flagMarketing": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Configured flags at calc time." + }, + "flagSimulation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Configured flags at calc time." + }, + "initValValuation": { + "type": "number", + "format": "double", + "description": "Init-value overrides at calc time." + }, + "initValMarketing": { + "type": "number", + "format": "double", + "description": "Init-value overrides at calc time." + }, + "initValSimulation": { + "type": "number", + "format": "double", + "description": "Init-value overrides at calc time." + }, + "costValuation": { + "type": "number", + "format": "double", + "description": "Computed costs at calc time." + }, + "costMarketing": { + "type": "number", + "format": "double", + "description": "Computed costs at calc time." + }, + "costSimulation": { + "type": "number", + "format": "double", + "description": "Computed costs at calc time." + }, + "flagValuationUsed": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stages resolved at calc time." + }, + "flagMarketingUsed": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stages resolved at calc time." + }, + "flagSimulationUsed": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stages resolved at calc time." + }, + "sourceItemCount": { + "type": "integer", + "format": "int32", + "description": "Count of source items aggregated." }, - "minValue": { + "triggerReason": { + "$ref": "#/definitions/v1RMCostTriggerReason", + "description": "Why this calc was triggered." + }, + "calculatedAt": { "type": "string", - "description": "Minimum value (optional, decimal as string)." + "description": "When the calc ran (RFC3339)." }, - "maxValue": { + "calculatedBy": { "type": "string", - "description": "Maximum value (optional, decimal as string)." + "description": "Who ran the calc." } }, - "description": "CreateParameterRequest is the request for creating a new parameter." + "description": "RMCostHistory is one row of the append-only audit trail written alongside every\ncalculation pass." }, - "v1CreateParameterResponse": { + "v1RMCostRates": { "type": "object", "properties": { - "base": { - "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." - }, - "data": { - "$ref": "#/definitions/v1Parameter", - "description": "Created parameter data." + "cons": { + "type": "number", + "format": "double", + "description": "CONS stage rate." + }, + "stores": { + "type": "number", + "format": "double", + "description": "STORES stage rate." + }, + "dept": { + "type": "number", + "format": "double", + "description": "DEPT stage rate." + }, + "po1": { + "type": "number", + "format": "double", + "description": "First PO stage rate." + }, + "po2": { + "type": "number", + "format": "double", + "description": "Second PO stage rate." + }, + "po3": { + "type": "number", + "format": "double", + "description": "Third PO stage rate." } }, - "description": "CreateParameterResponse is the response for creating a parameter." + "description": "RMCostRates is the per-stage snapshot of aggregated rates captured at calc time." }, - "v1DataType": { + "v1RMCostTriggerReason": { "type": "string", "enum": [ - "DATA_TYPE_UNSPECIFIED", - "DATA_TYPE_NUMBER", - "DATA_TYPE_TEXT", - "DATA_TYPE_BOOLEAN" + "RM_COST_TRIGGER_REASON_UNSPECIFIED", + "RM_COST_TRIGGER_REASON_ORACLE_SYNC_CHAIN", + "RM_COST_TRIGGER_REASON_GROUP_UPDATE", + "RM_COST_TRIGGER_REASON_DETAIL_CHANGE", + "RM_COST_TRIGGER_REASON_MANUAL_UI" ], - "default": "DATA_TYPE_UNSPECIFIED", - "description": "DataType represents the data type of a parameter value.\n\n - DATA_TYPE_UNSPECIFIED: Default unspecified value - used as \"no filter\" in list/export requests.\n - DATA_TYPE_NUMBER: Numeric values (integer or decimal).\n - DATA_TYPE_TEXT: Text/string values.\n - DATA_TYPE_BOOLEAN: Boolean (true/false) values." + "default": "RM_COST_TRIGGER_REASON_UNSPECIFIED", + "description": "RMCostTriggerReason enumerates the reasons a calculation was run. Mirrors\n`rmcost.HistoryTriggerReason`.\n\n - RM_COST_TRIGGER_REASON_UNSPECIFIED: Default zero value. Rejected on trigger requests via not_in: [0].\n - RM_COST_TRIGGER_REASON_ORACLE_SYNC_CHAIN: Auto-chained after a successful Oracle sync for the synced period.\n - RM_COST_TRIGGER_REASON_GROUP_UPDATE: Group header changed.\n - RM_COST_TRIGGER_REASON_DETAIL_CHANGE: An item was added/removed/toggled in the group.\n - RM_COST_TRIGGER_REASON_MANUAL_UI: Explicit user request from the UI." }, - "v1DeleteParameterResponse": { - "type": "object", - "properties": { - "base": { - "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." - } - }, - "description": "DeleteParameterResponse is the response for deleting a parameter." + "v1RMCostType": { + "type": "string", + "enum": [ + "RM_COST_TYPE_UNSPECIFIED", + "RM_COST_TYPE_GROUP", + "RM_COST_TYPE_ITEM" + ], + "default": "RM_COST_TYPE_UNSPECIFIED", + "description": "RMCostType discriminates a group-level cost row from an item-level cost row.\n\n - RM_COST_TYPE_UNSPECIFIED: Default zero value. Means \"no filter\" in list requests.\n - RM_COST_TYPE_GROUP: Cost row aggregates a whole RM group.\n - RM_COST_TYPE_ITEM: Cost row is computed for a single item (future phase)." }, - "v1DownloadParameterTemplateResponse": { - "type": "object", - "properties": { - "base": { - "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." - }, - "fileContent": { - "type": "string", - "format": "byte", - "description": "Excel template file content as bytes (.xlsx format)." - }, - "fileName": { - "type": "string", - "description": "Suggested filename." - } - }, - "description": "DownloadParameterTemplateResponse is the response containing the template file." + "v1RMGroupFlag": { + "type": "string", + "enum": [ + "RM_GROUP_FLAG_UNSPECIFIED", + "RM_GROUP_FLAG_INIT", + "RM_GROUP_FLAG_CONS", + "RM_GROUP_FLAG_STORES", + "RM_GROUP_FLAG_DEPT", + "RM_GROUP_FLAG_PO_1", + "RM_GROUP_FLAG_PO_2", + "RM_GROUP_FLAG_PO_3" + ], + "default": "RM_GROUP_FLAG_UNSPECIFIED", + "description": "RMGroupFlag identifies which aggregated stage rate feeds a cost purpose\n(valuation / marketing / simulation), or whether the group uses its\nconfigured init override value. Mirrors the `cst_rm_group_head.flag_*`\ndomain constraint.\n\n - RM_GROUP_FLAG_UNSPECIFIED: Default zero value. Rejected on create/update requests via not_in: [0].\n - RM_GROUP_FLAG_INIT: Use the init_val_* override on the head; skips cascade.\n - RM_GROUP_FLAG_CONS: Use the CONS stage rate.\n - RM_GROUP_FLAG_STORES: Use the STORES stage rate.\n - RM_GROUP_FLAG_DEPT: Use the DEPT stage rate.\n - RM_GROUP_FLAG_PO_1: Use the first PO stage rate.\n - RM_GROUP_FLAG_PO_2: Use the second PO stage rate.\n - RM_GROUP_FLAG_PO_3: Use the third PO stage rate." }, - "v1ExportParametersResponse": { + "v1TriggerRMCostCalculationRequest": { "type": "object", "properties": { - "base": { - "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." - }, - "fileContent": { + "period": { "type": "string", - "format": "byte", - "description": "Excel file content as bytes (.xlsx format)." + "description": "Period to recalculate (YYYYMM)." }, - "fileName": { + "groupHeadId": { "type": "string", - "description": "Suggested filename." + "description": "When set, scope the calc to a single group. Empty = all active groups." + }, + "triggerReason": { + "$ref": "#/definitions/v1RMCostTriggerReason", + "description": "Why the recalc was requested." } }, - "description": "ExportParametersResponse is the response containing the Excel file." + "description": "TriggerRMCostCalculation enqueues a recalculation job. Returns immediately\nwith a job ID the caller can poll via the job-execution endpoint." }, - "v1GetParameterResponse": { + "v1TriggerRMCostCalculationResponse": { "type": "object", "properties": { "base": { "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." - }, - "data": { - "$ref": "#/definitions/v1Parameter", - "description": "Parameter data." - } - }, - "description": "GetParameterResponse is the response for getting a parameter." - }, - "v1ImportParametersRequest": { - "type": "object", - "properties": { - "fileContent": { - "type": "string", - "format": "byte", - "description": "Excel file content as bytes (.xlsx or .xls format)." - }, - "fileName": { - "type": "string", - "description": "Original filename (for format detection)." + "description": "Standard response envelope." }, - "duplicateAction": { + "jobId": { "type": "string", - "description": "Required. How to handle duplicates: \"skip\", \"update\", \"error\"." + "description": "Enqueued job UUID." } }, - "description": "ImportParametersRequest is the request for importing parameters from Excel." + "description": "Trigger response." }, - "v1ImportParametersResponse": { + "RMGroupServiceAddItemsBody": { "type": "object", "properties": { - "base": { - "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." - }, - "successCount": { - "type": "integer", - "format": "int32", - "description": "Number of successfully imported records." - }, - "skippedCount": { - "type": "integer", - "format": "int32", - "description": "Number of skipped records (duplicates with skip action)." - }, - "updatedCount": { - "type": "integer", - "format": "int32", - "description": "Number of updated records (duplicates with update action)." - }, - "failedCount": { - "type": "integer", - "format": "int32", - "description": "Number of failed records." - }, - "errors": { + "itemCodes": { "type": "array", "items": { - "type": "object", - "$ref": "#/definitions/v1ImportError" + "type": "string" }, - "description": "Details of failed records." + "description": "Item codes to add (at least 1)." } }, - "description": "ImportParametersResponse is the response for importing parameters." + "description": "Add one or more items to a group. Items already in another active group are\nreturned in `skipped` rather than erroring the whole batch." }, - "v1ListParametersResponse": { + "RMGroupServiceRemoveItemsBody": { "type": "object", "properties": { - "base": { - "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." - }, - "data": { + "groupDetailIds": { "type": "array", "items": { - "type": "object", - "$ref": "#/definitions/v1Parameter" + "type": "string" }, - "description": "List of parameters." + "description": "Detail UUIDs to remove." }, - "pagination": { - "$ref": "#/definitions/v1PaginationResponse", - "description": "Pagination metadata." + "mode": { + "$ref": "#/definitions/v1RemoveItemsMode", + "description": "Disposition mode." } }, - "description": "ListParametersResponse is the response for listing parameters." - }, - "v1ParamCategory": { - "type": "string", - "enum": [ - "PARAM_CATEGORY_UNSPECIFIED", - "PARAM_CATEGORY_INPUT", - "PARAM_CATEGORY_RATE", - "PARAM_CATEGORY_CALCULATED" - ], - "default": "PARAM_CATEGORY_UNSPECIFIED", - "description": "ParamCategory represents the category/purpose of a parameter.\n\n - PARAM_CATEGORY_UNSPECIFIED: Default unspecified value - used as \"no filter\" in list/export requests.\n - PARAM_CATEGORY_INPUT: Input parameter - manually entered values.\n - PARAM_CATEGORY_RATE: Rate parameter - rate/ratio values.\n - PARAM_CATEGORY_CALCULATED: Calculated parameter - computed from other parameters." + "description": "Remove items from a group by detail IDs. The mode controls whether the rows\nare deactivated (preserved for history) or soft-deleted." }, - "v1Parameter": { + "RMGroupServiceUpdateRMGroupBody": { "type": "object", "properties": { - "paramId": { + "groupName": { "type": "string", - "description": "Unique identifier (UUID)." + "description": "New display name." }, - "paramCode": { + "description": { "type": "string", - "description": "Unique code (e.g., \"SPEED\", \"DENIER\", \"ELEC_RATE\"). Immutable after creation." + "description": "New description." }, - "paramName": { + "colourant": { "type": "string", - "description": "Display name (e.g., \"Speed\", \"Denier\")." + "description": "New colourant tag." }, - "paramShortName": { + "ciName": { "type": "string", - "description": "Short name for compact display." + "description": "New CI name tag." }, - "dataType": { - "$ref": "#/definitions/v1DataType", - "description": "Data type of the parameter value." + "costPercentage": { + "type": "number", + "format": "double", + "description": "New cost percentage (>= 0)." }, - "paramCategory": { - "$ref": "#/definitions/v1ParamCategory", - "description": "Category/purpose of the parameter." + "costPerKg": { + "type": "number", + "format": "double", + "description": "New per-kg overhead (>= 0)." }, - "defaultValue": { - "type": "string", - "description": "Default value (decimal, as string for precision)." + "flagValuation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "New valuation flag." }, - "minValue": { - "type": "string", - "description": "Minimum allowed value (decimal, as string for precision)." + "flagMarketing": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "New marketing flag." }, - "maxValue": { - "type": "string", - "description": "Maximum allowed value (decimal, as string for precision)." + "flagSimulation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "New simulation flag." + }, + "initValValuation": { + "type": "number", + "format": "double", + "description": "New init-val override for valuation (>= 0)." + }, + "initValMarketing": { + "type": "number", + "format": "double", + "description": "New init-val override for marketing (>= 0)." + }, + "initValSimulation": { + "type": "number", + "format": "double", + "description": "New init-val override for simulation (>= 0)." }, "isActive": { "type": "boolean", - "description": "Whether the parameter is active." - }, - "audit": { - "$ref": "#/definitions/v1AuditInfo", - "description": "Audit information." + "description": "New active status." }, - "uomId": { - "type": "string", - "description": "Optional UOM reference ID (UUID, nullable)." + "clearInitValValuation": { + "type": "boolean", + "description": "When true, force init_val_valuation to NULL (overrides init_val_valuation field)." }, - "uomCode": { - "type": "string", - "description": "Resolved UOM code (read-only, populated from mst_uom join)." + "clearInitValMarketing": { + "type": "boolean", + "description": "When true, force init_val_marketing to NULL." }, - "uomName": { - "type": "string", - "description": "Resolved UOM name (read-only, populated from mst_uom join)." + "clearInitValSimulation": { + "type": "boolean", + "description": "When true, force init_val_simulation to NULL." } }, - "description": "Parameter represents a master parameter entity for costing calculations." + "description": "Partial update of an RM group head. Absent fields leave state unchanged.\nClear_* booleans explicitly set nullable fields to NULL." }, - "v1UpdateParameterResponse": { + "v1AddItemsResponse": { "type": "object", "properties": { "base": { "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." + "description": "Standard response envelope." }, - "data": { - "$ref": "#/definitions/v1Parameter", - "description": "Updated parameter data." + "added": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1RMGroupDetail" + }, + "description": "Details created by this call." + }, + "skipped": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1SkippedItem" + }, + "description": "Items that were skipped because they already belong to another active group." } }, - "description": "UpdateParameterResponse is the response for updating a parameter." + "description": "Add items response." }, - "RMCategoryServiceUpdateRMCategoryBody": { + "v1CreateRMGroupRequest": { "type": "object", "properties": { - "categoryName": { + "groupCode": { "type": "string", - "description": "New display name (optional, 1-100 chars if provided)." + "description": "Group code (uppercase; spaces + hyphens allowed; 1-30 chars)." }, - "description": { + "groupName": { "type": "string", - "description": "New description (optional, max 500 chars)." + "description": "Display name (1-200 chars)." }, - "isActive": { - "type": "boolean", - "description": "New active status (optional)." - } - }, - "description": "UpdateRMCategoryRequest is the request for updating a raw material category.\nNote: category_code is immutable and cannot be changed after creation." - }, - "v1CreateRMCategoryRequest": { - "type": "object", - "properties": { - "categoryCode": { + "description": { "type": "string", - "description": "Unique code (uppercase, alphanumeric with underscore, 1-20 chars).\nMust start with an uppercase letter. Immutable after creation." + "description": "Optional description (max 1000 chars)." }, - "categoryName": { + "colourant": { "type": "string", - "description": "Display name (1-100 chars)." + "description": "Optional colourant tag." }, - "description": { + "ciName": { "type": "string", - "description": "Optional description (max 500 chars)." + "description": "Optional CI name tag." + }, + "costPercentage": { + "type": "number", + "format": "double", + "description": "Cost percentage multiplier (>= 0)." + }, + "costPerKg": { + "type": "number", + "format": "double", + "description": "Per-kg overhead (>= 0)." } }, - "description": "CreateRMCategoryRequest is the request for creating a new raw material category." + "description": "Create a new RM group head." }, - "v1CreateRMCategoryResponse": { + "v1CreateRMGroupResponse": { "type": "object", "properties": { "base": { "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." + "description": "Standard response envelope." }, "data": { - "$ref": "#/definitions/v1RMCategory", - "description": "Created raw material category data." + "$ref": "#/definitions/v1RMGroupHead", + "description": "Created head." } }, - "description": "CreateRMCategoryResponse is the response for creating a raw material category." + "description": "Create response." }, - "v1DeleteRMCategoryResponse": { + "v1DeleteRMGroupResponse": { "type": "object", "properties": { "base": { "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." + "description": "Standard response envelope." } }, - "description": "DeleteRMCategoryResponse is the response for deleting a raw material category." + "description": "Delete response." }, - "v1DownloadRMCategoryTemplateResponse": { + "v1DownloadRMGroupTemplateResponse": { "type": "object", "properties": { "base": { "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." + "description": "Standard response envelope." }, "fileContent": { "type": "string", "format": "byte", - "description": "Excel template file content as bytes (.xlsx format)." + "description": "Excel template file content (.xlsx)." }, "fileName": { "type": "string", "description": "Suggested filename." } }, - "description": "DownloadRMCategoryTemplateResponse is the response containing the template file." + "description": "DownloadRMGroupTemplateResponse returns the blank 2-sheet template." }, - "v1ExportRMCategoriesResponse": { + "v1ExportRMGroupsResponse": { "type": "object", "properties": { "base": { "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." + "description": "Standard response envelope." }, "fileContent": { "type": "string", "format": "byte", - "description": "Excel file content as bytes (.xlsx format)." + "description": "Excel file content (.xlsx)." }, "fileName": { "type": "string", "description": "Suggested filename." } }, - "description": "ExportRMCategoriesResponse is the response containing the Excel file." + "description": "ExportRMGroupsResponse returns a multi-sheet Excel (Groups + Items)." }, - "v1GetRMCategoryResponse": { + "v1GetRMGroupItemRatesResponse": { "type": "object", "properties": { "base": { "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." + "description": "Standard response envelope." }, "data": { - "$ref": "#/definitions/v1RMCategory", - "description": "Raw material category data." + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1RMGroupItemRates" + }, + "description": "Per-item rates, one row per active detail." } }, - "description": "GetRMCategoryResponse is the response for getting a raw material category." + "description": "Group item rates response." }, - "v1ImportRMCategoriesRequest": { + "v1GetRMGroupResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "$ref": "#/definitions/v1RMGroupHeadWithDetails", + "description": "Head + details." + } + }, + "description": "Get response." + }, + "v1ImportRMGroupsRequest": { "type": "object", "properties": { "fileContent": { "type": "string", "format": "byte", - "description": "Excel file content as bytes (.xlsx or .xls format)." + "description": "Excel file content (.xlsx / .xls)." }, "fileName": { "type": "string", - "description": "Original filename (for format detection).\nMust be a simple filename without path separators." + "description": "Original filename (format detected by extension)." }, "duplicateAction": { "type": "string", - "description": "Required. How to handle duplicates: \"skip\", \"update\", \"error\"." + "description": "How to handle existing groups by `group_code`: \"skip\" (default) or \"update\".\nDetail rows always additive: items already active in ANOTHER group are\nreported as skipped errors; items already in the target group are ignored." } }, - "description": "ImportRMCategoriesRequest is the request for importing raw material categories from Excel." + "description": "ImportRMGroupsRequest accepts a 2-sheet Excel. User may include only the\nGroups sheet (header-only import), only the Items sheet (detail-only for\nexisting groups), or both." }, - "v1ImportRMCategoriesResponse": { + "v1ImportRMGroupsResponse": { "type": "object", "properties": { "base": { "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." + "description": "Standard response envelope." }, - "successCount": { + "groupsCreated": { "type": "integer", "format": "int32", - "description": "Number of successfully imported records." + "description": "Number of group heads created." }, - "skippedCount": { + "groupsUpdated": { "type": "integer", "format": "int32", - "description": "Number of skipped records (duplicates with skip action)." + "description": "Number of group heads updated (duplicate_action=update)." }, - "updatedCount": { + "groupsSkipped": { "type": "integer", "format": "int32", - "description": "Number of updated records (duplicates with update action)." + "description": "Number of group heads skipped (duplicate_action=skip + existed)." + }, + "itemsAdded": { + "type": "integer", + "format": "int32", + "description": "Number of detail items successfully added." + }, + "itemsSkipped": { + "type": "integer", + "format": "int32", + "description": "Number of detail items skipped (already in target group)." }, "failedCount": { "type": "integer", "format": "int32", - "description": "Number of failed records." + "description": "Number of failed rows (across both sheets)." }, "errors": { "type": "array", @@ -2631,76 +4907,513 @@ "type": "object", "$ref": "#/definitions/v1ImportError" }, - "description": "Details of failed records." + "description": "Per-row errors (reuses finance.v1.ImportError)." } }, - "description": "ImportRMCategoriesResponse is the response for importing raw material categories." + "description": "ImportRMGroupsResponse summarizes the outcome." }, - "v1ListRMCategoriesResponse": { + "v1ListRMGroupsResponse": { "type": "object", "properties": { "base": { "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." + "description": "Standard response envelope." }, "data": { "type": "array", "items": { "type": "object", - "$ref": "#/definitions/v1RMCategory" + "$ref": "#/definitions/v1RMGroupHead" }, - "description": "List of raw material categories." + "description": "Group heads." }, "pagination": { "$ref": "#/definitions/v1PaginationResponse", "description": "Pagination metadata." } }, - "description": "ListRMCategoriesResponse is the response for listing raw material categories." + "description": "List response." }, - "v1RMCategory": { + "v1ListUngroupedItemsResponse": { "type": "object", "properties": { - "rmCategoryId": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." + }, + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1UngroupedItem" + }, + "description": "Ungrouped items for the requested filters." + }, + "pagination": { + "$ref": "#/definitions/v1PaginationResponse", + "description": "Pagination metadata." + } + }, + "description": "Ungrouped items response." + }, + "v1RMGroupDetail": { + "type": "object", + "properties": { + "groupDetailId": { "type": "string", - "description": "Unique identifier (UUID)." + "description": "Detail UUID." }, - "categoryCode": { + "groupHeadId": { "type": "string", - "description": "Unique code (e.g., \"CHIP\", \"OIL\", \"DYES\"). Immutable after creation." + "description": "Owning group head UUID." }, - "categoryName": { + "itemCode": { "type": "string", - "description": "Display name (e.g., \"Chips\", \"Oil\", \"Dyes\")." + "description": "Item code." + }, + "itemName": { + "type": "string", + "description": "Item name (snapshot)." + }, + "itemTypeCode": { + "type": "string", + "description": "Item type code." + }, + "gradeCode": { + "type": "string", + "description": "Grade code." + }, + "itemGrade": { + "type": "string", + "description": "Item grade." + }, + "uomCode": { + "type": "string", + "description": "UOM code." + }, + "marketPercentage": { + "type": "number", + "format": "double", + "description": "Per-item marketing percentage (nil when unset)." + }, + "marketValueRp": { + "type": "number", + "format": "double", + "description": "Per-item marketing value in rupiah (nil when unset)." + }, + "sortOrder": { + "type": "integer", + "format": "int32", + "description": "Display order within the group." + }, + "isActive": { + "type": "boolean", + "description": "Contributes to rate aggregation when true." + }, + "isDummy": { + "type": "boolean", + "description": "Placeholder row (excluded from aggregation regardless of is_active)." + }, + "audit": { + "$ref": "#/definitions/v1AuditInfo", + "description": "Audit metadata." + } + }, + "description": "RMGroupDetail is one item's membership in an RM group." + }, + "v1RMGroupHead": { + "type": "object", + "properties": { + "groupHeadId": { + "type": "string", + "description": "Group head UUID." + }, + "groupCode": { + "type": "string", + "description": "Unique group code (uppercase; allows spaces + hyphens; 1-30 chars)." + }, + "groupName": { + "type": "string", + "description": "Display name." }, "description": { "type": "string", "description": "Optional description." }, + "colourant": { + "type": "string", + "description": "Optional colourant tag." + }, + "ciName": { + "type": "string", + "description": "Optional CI name tag." + }, + "costPercentage": { + "type": "number", + "format": "double", + "description": "Cost percentage multiplier (raw, UI formats for display)." + }, + "costPerKg": { + "type": "number", + "format": "double", + "description": "Per-kg overhead added to landed cost." + }, + "flagValuation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stage flag used for valuation cost." + }, + "flagMarketing": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stage flag used for marketing cost." + }, + "flagSimulation": { + "$ref": "#/definitions/v1RMGroupFlag", + "description": "Stage flag used for simulation cost." + }, + "initValValuation": { + "type": "number", + "format": "double", + "description": "Init-value override for valuation (set when flag_valuation = INIT)." + }, + "initValMarketing": { + "type": "number", + "format": "double", + "description": "Init-value override for marketing (set when flag_marketing = INIT)." + }, + "initValSimulation": { + "type": "number", + "format": "double", + "description": "Init-value override for simulation (set when flag_simulation = INIT)." + }, "isActive": { "type": "boolean", - "description": "Whether the category is active." + "description": "Whether the group is active." }, "audit": { "$ref": "#/definitions/v1AuditInfo", - "description": "Audit information." + "description": "Audit metadata." } }, - "description": "RMCategory represents a Raw Material Category entity.\nUsed to classify raw materials (e.g., Chips, Oil, Dyes, Chemicals)." + "description": "RMGroupHead is the aggregate root representing an RM group's cost configuration." }, - "v1UpdateRMCategoryResponse": { + "v1RMGroupHeadWithDetails": { + "type": "object", + "properties": { + "head": { + "$ref": "#/definitions/v1RMGroupHead", + "description": "Head data." + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1RMGroupDetail" + }, + "description": "Details owned by the head." + } + }, + "description": "RMGroupHeadWithDetails bundles a head and its details for the Get response." + }, + "v1RMGroupItemRates": { + "type": "object", + "properties": { + "itemCode": { + "type": "string", + "description": "Item code." + }, + "itemName": { + "type": "string", + "description": "Item name (snapshot from detail row)." + }, + "gradeCode": { + "type": "string", + "description": "Grade code." + }, + "itemGrade": { + "type": "string", + "description": "Item grade." + }, + "uomCode": { + "type": "string", + "description": "UOM code." + }, + "isActive": { + "type": "boolean", + "description": "Whether the detail is active." + }, + "isDummy": { + "type": "boolean", + "description": "Whether the detail is a dummy row." + }, + "period": { + "type": "string", + "description": "Period the rates apply to (YYYYMM), empty when no sync row was found." + }, + "consQty": { + "type": "number", + "format": "double", + "description": "CONS qty / value / rate." + }, + "consVal": { + "type": "number", + "format": "double" + }, + "consRate": { + "type": "number", + "format": "double" + }, + "storesQty": { + "type": "number", + "format": "double", + "description": "STORES qty / value / rate." + }, + "storesVal": { + "type": "number", + "format": "double" + }, + "storesRate": { + "type": "number", + "format": "double" + }, + "deptQty": { + "type": "number", + "format": "double", + "description": "DEPT qty / value / rate." + }, + "deptVal": { + "type": "number", + "format": "double" + }, + "deptRate": { + "type": "number", + "format": "double" + }, + "lastPoQty1": { + "type": "number", + "format": "double", + "description": "PO_1 qty / value / rate." + }, + "lastPoVal1": { + "type": "number", + "format": "double" + }, + "lastPoRate1": { + "type": "number", + "format": "double" + }, + "lastPoQty2": { + "type": "number", + "format": "double", + "description": "PO_2 qty / value / rate." + }, + "lastPoVal2": { + "type": "number", + "format": "double" + }, + "lastPoRate2": { + "type": "number", + "format": "double" + }, + "lastPoQty3": { + "type": "number", + "format": "double", + "description": "PO_3 qty / value / rate." + }, + "lastPoVal3": { + "type": "number", + "format": "double" + }, + "lastPoRate3": { + "type": "number", + "format": "double" + } + }, + "description": "RMGroupItemRates is one row per item currently in a group with its per-stage\nOracle sync rates for a given period. Items with no sync row for the period\nstill appear (all rates 0)." + }, + "v1RemoveItemsMode": { + "type": "string", + "enum": [ + "REMOVE_ITEMS_MODE_UNSPECIFIED", + "REMOVE_ITEMS_MODE_DEACTIVATE", + "REMOVE_ITEMS_MODE_SOFT_DELETE" + ], + "default": "REMOVE_ITEMS_MODE_UNSPECIFIED", + "description": "RemoveItemsMode controls how RemoveItems disposes of detail rows.\n\n - REMOVE_ITEMS_MODE_UNSPECIFIED: Default zero value. Rejected via not_in: [0].\n - REMOVE_ITEMS_MODE_DEACTIVATE: Mark the details inactive but keep the rows for audit history.\n - REMOVE_ITEMS_MODE_SOFT_DELETE: Soft-delete the details (sets deleted_at/deleted_by)." + }, + "v1RemoveItemsResponse": { "type": "object", "properties": { "base": { "$ref": "#/definitions/v1BaseResponse", - "description": "Standard response metadata." + "description": "Standard response envelope." + }, + "removedCount": { + "type": "integer", + "format": "int32", + "description": "Number of details affected." + } + }, + "description": "Remove items response." + }, + "v1SkippedItem": { + "type": "object", + "properties": { + "itemCode": { + "type": "string", + "description": "Item code that was skipped." + }, + "owningGroupHeadId": { + "type": "string", + "description": "Owning group head UUID." + }, + "owningGroupDetailId": { + "type": "string", + "description": "Owning detail UUID." + }, + "owningGroupCode": { + "type": "string", + "description": "Owning group code (for UI display)." + } + }, + "description": "SkippedItem captures items rejected by AddItems because they already belong\nto another active group." + }, + "v1UngroupedItem": { + "type": "object", + "properties": { + "period": { + "type": "string", + "description": "Period (YYYYMM)." + }, + "itemCode": { + "type": "string", + "description": "Item code." + }, + "itemName": { + "type": "string", + "description": "Item name." + }, + "itemTypeCode": { + "type": "string", + "description": "Item type code." + }, + "gradeCode": { + "type": "string", + "description": "Grade code." + }, + "itemGrade": { + "type": "string", + "description": "Item grade." + }, + "uomCode": { + "type": "string", + "description": "UOM code." + }, + "consVal": { + "type": "number", + "format": "double", + "description": "CONS stage value." + }, + "storesVal": { + "type": "number", + "format": "double", + "description": "STORES stage value." + }, + "consQty": { + "type": "number", + "format": "double", + "description": "CONS stage quantity." + }, + "consRate": { + "type": "number", + "format": "double", + "description": "CONS stage rate." + }, + "storesQty": { + "type": "number", + "format": "double", + "description": "STORES stage quantity." + }, + "storesRate": { + "type": "number", + "format": "double", + "description": "STORES stage rate." + }, + "deptQty": { + "type": "number", + "format": "double", + "description": "DEPT stage quantity." + }, + "deptVal": { + "type": "number", + "format": "double", + "description": "DEPT stage value." + }, + "deptRate": { + "type": "number", + "format": "double", + "description": "DEPT stage rate." + }, + "lastPoQty1": { + "type": "number", + "format": "double", + "description": "PO_1 stage quantity." + }, + "lastPoVal1": { + "type": "number", + "format": "double", + "description": "PO_1 stage value." + }, + "lastPoRate1": { + "type": "number", + "format": "double", + "description": "PO_1 stage rate." + }, + "lastPoQty2": { + "type": "number", + "format": "double", + "description": "PO_2 stage quantity." + }, + "lastPoVal2": { + "type": "number", + "format": "double", + "description": "PO_2 stage value." + }, + "lastPoRate2": { + "type": "number", + "format": "double", + "description": "PO_2 stage rate." + }, + "lastPoQty3": { + "type": "number", + "format": "double", + "description": "PO_3 stage quantity." + }, + "lastPoVal3": { + "type": "number", + "format": "double", + "description": "PO_3 stage value." + }, + "lastPoRate3": { + "type": "number", + "format": "double", + "description": "PO_3 stage rate." + } + }, + "description": "UngroupedItem is a raw material present in the Oracle sync feed that has no\nactive RM group assignment." + }, + "v1UpdateRMGroupResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response envelope." }, "data": { - "$ref": "#/definitions/v1RMCategory", - "description": "Updated raw material category data." + "$ref": "#/definitions/v1RMGroupHead", + "description": "Updated head." } }, - "description": "UpdateRMCategoryResponse is the response for updating a raw material category." + "description": "Update response." }, "UOMServiceUpdateUOMBody": { "type": "object", @@ -3197,12 +5910,21 @@ { "name": "FormulaService" }, + { + "name": "OracleSyncService" + }, { "name": "ParameterService" }, { "name": "RMCategoryService" }, + { + "name": "RMCostService" + }, + { + "name": "RMGroupService" + }, { "name": "UOMService" }, diff --git a/services/finance/internal/domain/job/value_objects.go b/services/finance/internal/domain/job/value_objects.go index 9891d55..83c12f0 100644 --- a/services/finance/internal/domain/job/value_objects.go +++ b/services/finance/internal/domain/job/value_objects.go @@ -53,9 +53,10 @@ type Type string // Type constants. const ( - TypeOracleSync Type = "oracle_sync" - TypeCalculation Type = "calculation" - TypeExport Type = "export" + TypeOracleSync Type = "oracle_sync" + TypeRMCostCalculation Type = "rm_cost_calculation" + TypeCalculation Type = "calculation" + TypeExport Type = "export" ) // String returns the string representation. diff --git a/services/finance/internal/domain/rmcost/calculation.go b/services/finance/internal/domain/rmcost/calculation.go new file mode 100644 index 0000000..cd606d3 --- /dev/null +++ b/services/finance/internal/domain/rmcost/calculation.go @@ -0,0 +1,183 @@ +// Package rmcost provides the landed-cost calculation engine and persistence contract +// for the RM cost aggregates produced from grouped raw-material consumption data. +package rmcost + +// Stage selects which per-stage rate feeds the landed-cost formula. +// Mirrors the `rmgroup.Flag` string values exactly so the two types can +// be converted with a single cast at the application-layer boundary. +type Stage string + +// Stage constants — MUST match rmgroup.Flag values and the DB CHECK constraints. +const ( + // StageCons selects the consumption-aggregated rate. + StageCons Stage = "CONS" + // StageStores selects the stores-aggregated rate. + StageStores Stage = "STORES" + // StageDept selects the department-aggregated rate. + StageDept Stage = "DEPT" + // StagePO1 selects the purchase-order slot 1 rate. + StagePO1 Stage = "PO_1" + // StagePO2 selects the purchase-order slot 2 rate. + StagePO2 Stage = "PO_2" + // StagePO3 selects the purchase-order slot 3 rate. + StagePO3 Stage = "PO_3" + // StageInit signals that the init_val override should be used instead of any aggregated rate. + StageInit Stage = "INIT" +) + +// IsValid reports whether the stage is one of the recognized values. +func (s Stage) IsValid() bool { + switch s { + case StageCons, StageStores, StageDept, StagePO1, StagePO2, StagePO3, StageInit: + return true + default: + return false + } +} + +// IsInit reports whether the stage is the INIT override. +func (s Stage) IsInit() bool { return s == StageInit } + +// String returns the canonical string form. +func (s Stage) String() string { return string(s) } + +// cascadeOrder is the fixed fallback chain used when the requested stage's rate is zero. +// INIT is intentionally excluded — an INIT request never cascades. +var cascadeOrder = []Stage{StageCons, StageStores, StageDept, StagePO1, StagePO2, StagePO3} + +// StageRates holds the aggregated (SUM(val) / SUM(qty)) rate per stage for a group +// in a given period. A zero value means either no data or a denominator of zero. +type StageRates struct { + Cons float64 + Stores float64 + Dept float64 + PO1 float64 + PO2 float64 + PO3 float64 +} + +// Get returns the rate for the given stage, or 0 when the stage is not a per-stage +// slot (e.g. StageInit, or an unknown value). +func (r StageRates) Get(s Stage) float64 { + switch s { + case StageCons: + return r.Cons + case StageStores: + return r.Stores + case StageDept: + return r.Dept + case StagePO1: + return r.PO1 + case StagePO2: + return r.PO2 + case StagePO3: + return r.PO3 + case StageInit: + return 0 + default: + return 0 + } +} + +// RateInputs are the per-stage numerator/denominator pairs the engine aggregates. +// One RateInputs row corresponds to one `cst_item_cons_stk_po` record for the group's +// period; nil pointers are treated as zero contribution. +type RateInputs struct { + ConsQty *float64 + ConsVal *float64 + StoresQty *float64 + StoresVal *float64 + DeptQty *float64 + DeptVal *float64 + PO1Qty *float64 + PO1Val *float64 + PO2Qty *float64 + PO2Val *float64 + PO3Qty *float64 + PO3Val *float64 +} + +// AggregateRates computes SUM(val) / SUM(qty) per stage across the supplied items. +// When SUM(qty) is zero for a stage, that stage's rate is returned as zero rather +// than producing NaN — the cascade logic in SelectRate treats zero as "no data". +func AggregateRates(items []RateInputs) StageRates { + var ( + qtyC, valC float64 + qtyS, valS float64 + qtyD, valD float64 + qty1, val1 float64 + qty2, val2 float64 + qty3, val3 float64 + ) + for i := range items { + it := items[i] + qtyC += deref(it.ConsQty) + valC += deref(it.ConsVal) + qtyS += deref(it.StoresQty) + valS += deref(it.StoresVal) + qtyD += deref(it.DeptQty) + valD += deref(it.DeptVal) + qty1 += deref(it.PO1Qty) + val1 += deref(it.PO1Val) + qty2 += deref(it.PO2Qty) + val2 += deref(it.PO2Val) + qty3 += deref(it.PO3Qty) + val3 += deref(it.PO3Val) + } + return StageRates{ + Cons: safeDiv(valC, qtyC), + Stores: safeDiv(valS, qtyS), + Dept: safeDiv(valD, qtyD), + PO1: safeDiv(val1, qty1), + PO2: safeDiv(val2, qty2), + PO3: safeDiv(val3, qty3), + } +} + +// SelectRate applies the flag + cascade rule. +// +// Returns (selectedRate, stageUsed): +// - If flag == StageInit: returns (*initVal, StageInit). When initVal is nil, returns (0, StageInit). +// - Otherwise returns the rate for `flag` if it is > 0. +// - If the requested rate is zero, cascades in fixed order CONS → STORES → DEPT → PO_1 → PO_2 → PO_3 +// and returns the first non-zero rate with that stage. +// - If every stage in the cascade is zero, returns (0, flag) — the original flag is preserved +// in `stageUsed` so callers can record "cascaded but nothing found". +func SelectRate(rates StageRates, flag Stage, initVal *float64) (float64, Stage) { + if flag == StageInit { + if initVal != nil { + return *initVal, StageInit + } + return 0, StageInit + } + if r := rates.Get(flag); r > 0 { + return r, flag + } + for _, s := range cascadeOrder { + if r := rates.Get(s); r > 0 { + return r, s + } + } + return 0, flag +} + +// LandedCost = (cost_percentage × selected_rate) + cost_per_kg. +func LandedCost(costPercentage, selectedRate, costPerKg float64) float64 { + return (costPercentage * selectedRate) + costPerKg +} + +// safeDiv returns num/denom, or 0 when denom is zero. +func safeDiv(num, denom float64) float64 { + if denom == 0 { + return 0 + } + return num / denom +} + +// deref returns *v or 0 when v is nil. +func deref(v *float64) float64 { + if v == nil { + return 0 + } + return *v +} diff --git a/services/finance/internal/domain/rmcost/calculation_test.go b/services/finance/internal/domain/rmcost/calculation_test.go new file mode 100644 index 0000000..31bf189 --- /dev/null +++ b/services/finance/internal/domain/rmcost/calculation_test.go @@ -0,0 +1,642 @@ +package rmcost_test + +import ( + "math" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" +) + +func mustUUID() uuid.UUID { return uuid.New() } +func nowish() time.Time { return time.Now() } + +// floatEq compares floats with a small epsilon — calculation results involve division +// and multiplication that would otherwise suffer from IEEE-754 rounding mismatches. +const epsilon = 1e-4 + +func floatEq(t *testing.T, want, got float64, msgAndArgs ...any) { + t.Helper() + if math.Abs(want-got) > epsilon { + assert.InDelta(t, want, got, epsilon, msgAndArgs...) + } +} + +func ptr(v float64) *float64 { return &v } + +// ---------------------------------------------------------------------------- +// Stage predicate tests +// ---------------------------------------------------------------------------- + +func TestStage_IsValid(t *testing.T) { + t.Parallel() + cases := []struct { + stage rmcost.Stage + want bool + }{ + {rmcost.StageCons, true}, + {rmcost.StageStores, true}, + {rmcost.StageDept, true}, + {rmcost.StagePO1, true}, + {rmcost.StagePO2, true}, + {rmcost.StagePO3, true}, + {rmcost.StageInit, true}, + {rmcost.Stage("BOGUS"), false}, + {rmcost.Stage(""), false}, + } + for _, tc := range cases { + assert.Equal(t, tc.want, tc.stage.IsValid(), "stage=%q", tc.stage) + } +} + +func TestStage_IsInit(t *testing.T) { + t.Parallel() + assert.True(t, rmcost.StageInit.IsInit()) + assert.False(t, rmcost.StageCons.IsInit()) +} + +func TestStageRates_Get(t *testing.T) { + t.Parallel() + r := rmcost.StageRates{Cons: 1, Stores: 2, Dept: 3, PO1: 4, PO2: 5, PO3: 6} + assert.Equal(t, 1.0, r.Get(rmcost.StageCons)) + assert.Equal(t, 2.0, r.Get(rmcost.StageStores)) + assert.Equal(t, 3.0, r.Get(rmcost.StageDept)) + assert.Equal(t, 4.0, r.Get(rmcost.StagePO1)) + assert.Equal(t, 5.0, r.Get(rmcost.StagePO2)) + assert.Equal(t, 6.0, r.Get(rmcost.StagePO3)) + assert.Equal(t, 0.0, r.Get(rmcost.StageInit)) + assert.Equal(t, 0.0, r.Get(rmcost.Stage("BOGUS"))) +} + +// ---------------------------------------------------------------------------- +// AggregateRates tests +// ---------------------------------------------------------------------------- + +func TestAggregateRates_Empty(t *testing.T) { + t.Parallel() + r := rmcost.AggregateRates(nil) + assert.Equal(t, rmcost.StageRates{}, r) + + r = rmcost.AggregateRates([]rmcost.RateInputs{}) + assert.Equal(t, rmcost.StageRates{}, r) +} + +func TestAggregateRates_WorkedExampleFromPlan(t *testing.T) { + t.Parallel() + // Item 1: cons(1000, 12,500,000), stores(500, 6,300,000), po1(0, 0) + // Item 2: cons(800, 9,920,000), stores(400, 5,000,000), po1(200, 2,500,000) + items := []rmcost.RateInputs{ + { + ConsQty: ptr(1000), + ConsVal: ptr(12_500_000), + StoresQty: ptr(500), + StoresVal: ptr(6_300_000), + PO1Qty: ptr(0), + PO1Val: ptr(0), + }, + { + ConsQty: ptr(800), + ConsVal: ptr(9_920_000), + StoresQty: ptr(400), + StoresVal: ptr(5_000_000), + PO1Qty: ptr(200), + PO1Val: ptr(2_500_000), + }, + } + r := rmcost.AggregateRates(items) + + // 22,420,000 / 1800 = 12,455.555... + floatEq(t, 12_455.555556, r.Cons, "cons_rate") + // 11,300,000 / 900 = 12,555.555... + floatEq(t, 12_555.555556, r.Stores, "stores_rate") + // 2,500,000 / 200 = 12,500 + floatEq(t, 12_500.0, r.PO1, "po1_rate") + // everything else has 0 qty → 0 rate + assert.Equal(t, 0.0, r.Dept) + assert.Equal(t, 0.0, r.PO2) + assert.Equal(t, 0.0, r.PO3) +} + +func TestAggregateRates_ZeroQtyReturnsZeroRate(t *testing.T) { + t.Parallel() + // value but no qty — denominator is zero, rate is zero (not NaN). + items := []rmcost.RateInputs{ + {ConsQty: ptr(0), ConsVal: ptr(1000)}, + } + r := rmcost.AggregateRates(items) + assert.Equal(t, 0.0, r.Cons) + assert.False(t, math.IsNaN(r.Cons)) + assert.False(t, math.IsInf(r.Cons, 0)) +} + +func TestAggregateRates_NilPointersTreatedAsZero(t *testing.T) { + t.Parallel() + items := []rmcost.RateInputs{ + {ConsQty: ptr(100), ConsVal: ptr(1000)}, + { /* every pointer nil */ }, + {ConsQty: ptr(100), ConsVal: ptr(1500)}, + } + r := rmcost.AggregateRates(items) + // (1000 + 0 + 1500) / (100 + 0 + 100) = 2500/200 = 12.5 + floatEq(t, 12.5, r.Cons) +} + +func TestAggregateRates_AllStages(t *testing.T) { + t.Parallel() + items := []rmcost.RateInputs{ + { + ConsQty: ptr(10), ConsVal: ptr(100), + StoresQty: ptr(5), StoresVal: ptr(25), + DeptQty: ptr(2), DeptVal: ptr(20), + PO1Qty: ptr(1), PO1Val: ptr(8), + PO2Qty: ptr(4), PO2Val: ptr(40), + PO3Qty: ptr(8), PO3Val: ptr(64), + }, + } + r := rmcost.AggregateRates(items) + floatEq(t, 10.0, r.Cons) + floatEq(t, 5.0, r.Stores) + floatEq(t, 10.0, r.Dept) + floatEq(t, 8.0, r.PO1) + floatEq(t, 10.0, r.PO2) + floatEq(t, 8.0, r.PO3) +} + +// ---------------------------------------------------------------------------- +// SelectRate tests +// ---------------------------------------------------------------------------- + +func TestSelectRate_FlagResolvesDirectly(t *testing.T) { + t.Parallel() + rates := rmcost.StageRates{Cons: 100, Stores: 200, Dept: 300, PO1: 400, PO2: 500, PO3: 600} + + cases := []struct { + flag rmcost.Stage + wantRate float64 + wantUsed rmcost.Stage + }{ + {rmcost.StageCons, 100, rmcost.StageCons}, + {rmcost.StageStores, 200, rmcost.StageStores}, + {rmcost.StageDept, 300, rmcost.StageDept}, + {rmcost.StagePO1, 400, rmcost.StagePO1}, + {rmcost.StagePO2, 500, rmcost.StagePO2}, + {rmcost.StagePO3, 600, rmcost.StagePO3}, + } + for _, tc := range cases { + rate, used := rmcost.SelectRate(rates, tc.flag, nil) + assert.Equal(t, tc.wantRate, rate, "flag=%q", tc.flag) + assert.Equal(t, tc.wantUsed, used, "flag=%q", tc.flag) + } +} + +func TestSelectRate_CascadeFromDept(t *testing.T) { + t.Parallel() + // Requested DEPT but it's zero → cascade starts from CONS. + rates := rmcost.StageRates{Cons: 12_455.56, Stores: 12_555.56, Dept: 0} + rate, used := rmcost.SelectRate(rates, rmcost.StageDept, nil) + floatEq(t, 12_455.56, rate) + assert.Equal(t, rmcost.StageCons, used) +} + +func TestSelectRate_CascadeSkipsZerosUntilMatch(t *testing.T) { + t.Parallel() + // Cons=0, Stores=0, Dept=0, PO1=0, PO2=0, PO3=999 → cascade finds PO3. + rates := rmcost.StageRates{PO3: 999} + rate, used := rmcost.SelectRate(rates, rmcost.StageCons, nil) + assert.Equal(t, 999.0, rate) + assert.Equal(t, rmcost.StagePO3, used) +} + +func TestSelectRate_AllZerosPreservesRequestedFlag(t *testing.T) { + t.Parallel() + rate, used := rmcost.SelectRate(rmcost.StageRates{}, rmcost.StagePO2, nil) + assert.Equal(t, 0.0, rate) + assert.Equal(t, rmcost.StagePO2, used, "original flag preserved when cascade finds nothing") +} + +func TestSelectRate_InitOverride(t *testing.T) { + t.Parallel() + // INIT flag + init_val present → returns init_val, no cascade even if other stages > 0. + rates := rmcost.StageRates{Cons: 999_999} + initVal := 13_000.0 + rate, used := rmcost.SelectRate(rates, rmcost.StageInit, &initVal) + assert.Equal(t, 13_000.0, rate) + assert.Equal(t, rmcost.StageInit, used) +} + +func TestSelectRate_InitWithoutInitValReturnsZero(t *testing.T) { + t.Parallel() + rates := rmcost.StageRates{Cons: 100} + rate, used := rmcost.SelectRate(rates, rmcost.StageInit, nil) + assert.Equal(t, 0.0, rate) + assert.Equal(t, rmcost.StageInit, used) +} + +func TestSelectRate_CascadeOrderIsStable(t *testing.T) { + t.Parallel() + // Multiple non-zero stages → cascade picks CONS first even if requested flag was PO_3. + rates := rmcost.StageRates{Cons: 1, Stores: 2, PO3: 3} + rate, used := rmcost.SelectRate(rates, rmcost.StagePO2, nil) + assert.Equal(t, 1.0, rate) + assert.Equal(t, rmcost.StageCons, used) +} + +// ---------------------------------------------------------------------------- +// LandedCost tests +// ---------------------------------------------------------------------------- + +func TestLandedCost(t *testing.T) { + t.Parallel() + cases := []struct { + name string + pct float64 + rate float64 + perKg float64 + want float64 + }{ + {"zero all", 0, 0, 0, 0}, + {"only per-kg", 0, 0, 50, 50}, + {"only pct*rate", 0.2, 100, 0, 20}, + {"plan worked example valuation", 0.20, 12_455.5556, 0.0125, 2_491.1236}, + {"plan INIT example", 0.20, 13_000, 0.0125, 2_600.0125}, + {"all zero edge case", 0.20, 0, 0.0125, 0.0125}, + } + for _, tc := range cases { + got := rmcost.LandedCost(tc.pct, tc.rate, tc.perKg) + floatEq(t, tc.want, got, tc.name) + } +} + +// ---------------------------------------------------------------------------- +// CalculateCost (full pipeline) tests +// ---------------------------------------------------------------------------- + +func TestCalculateCost_WorkedExample(t *testing.T) { + t.Parallel() + // Same inputs as TestAggregateRates_WorkedExampleFromPlan with flags per plan §6. + items := []rmcost.RateInputs{ + { + ConsQty: ptr(1000), ConsVal: ptr(12_500_000), + StoresQty: ptr(500), StoresVal: ptr(6_300_000), + PO1Qty: ptr(0), PO1Val: ptr(0), + }, + { + ConsQty: ptr(800), ConsVal: ptr(9_920_000), + StoresQty: ptr(400), StoresVal: ptr(5_000_000), + PO1Qty: ptr(200), PO1Val: ptr(2_500_000), + }, + } + h := rmcost.HeaderInputs{ + CostPercentage: 0.20, + CostPerKg: 0.0125, + FlagValuation: rmcost.StageCons, + FlagMarketing: rmcost.StageStores, + FlagSimulation: rmcost.StagePO1, + } + comp := rmcost.CalculateCost(items, h) + + floatEq(t, 2_491.1236, comp.CostValuation, "cost_val") + floatEq(t, 2_511.1236, comp.CostMarketing, "cost_mark") + floatEq(t, 2_500.0125, comp.CostSimulation, "cost_sim") + assert.Equal(t, rmcost.StageCons, comp.FlagValuationUsed) + assert.Equal(t, rmcost.StageStores, comp.FlagMarketingUsed) + assert.Equal(t, rmcost.StagePO1, comp.FlagSimulationUsed) +} + +func TestCalculateCost_CascadeExample(t *testing.T) { + t.Parallel() + // Valuation requests DEPT but dept=0 → cascades to CONS. + items := []rmcost.RateInputs{ + {ConsQty: ptr(1800), ConsVal: ptr(22_420_000)}, + } + h := rmcost.HeaderInputs{ + CostPercentage: 0.20, + CostPerKg: 0.0125, + FlagValuation: rmcost.StageDept, + FlagMarketing: rmcost.StageCons, + FlagSimulation: rmcost.StageCons, + } + comp := rmcost.CalculateCost(items, h) + + floatEq(t, 22_420_000.0/1800.0, comp.Rates.Cons) + assert.Equal(t, rmcost.StageCons, comp.FlagValuationUsed, "cascaded") + assert.Equal(t, rmcost.StageDept, comp.FlagValuation, "original flag preserved") +} + +func TestCalculateCost_AllZeroEdgeCase(t *testing.T) { + t.Parallel() + items := []rmcost.RateInputs{ + {ConsQty: ptr(0), ConsVal: ptr(0)}, + } + h := rmcost.HeaderInputs{ + CostPercentage: 0.20, + CostPerKg: 0.0125, + FlagValuation: rmcost.StageCons, + FlagMarketing: rmcost.StageStores, + FlagSimulation: rmcost.StagePO1, + } + comp := rmcost.CalculateCost(items, h) + + // Cost = (0.20 × 0) + 0.0125 = 0.0125 per purpose. + floatEq(t, 0.0125, comp.CostValuation) + floatEq(t, 0.0125, comp.CostMarketing) + floatEq(t, 0.0125, comp.CostSimulation) + // Original flags are preserved in FlagUsed when cascade found nothing. + assert.Equal(t, rmcost.StageCons, comp.FlagValuationUsed) + assert.Equal(t, rmcost.StageStores, comp.FlagMarketingUsed) + assert.Equal(t, rmcost.StagePO1, comp.FlagSimulationUsed) +} + +func TestCalculateCost_InitOverride(t *testing.T) { + t.Parallel() + items := []rmcost.RateInputs{ + {ConsQty: ptr(100), ConsVal: ptr(999_999)}, + } + initVal := 13_000.0 + h := rmcost.HeaderInputs{ + CostPercentage: 0.20, + CostPerKg: 0.0125, + FlagValuation: rmcost.StageInit, + FlagMarketing: rmcost.StageCons, + FlagSimulation: rmcost.StageCons, + InitValValuation: &initVal, + } + comp := rmcost.CalculateCost(items, h) + + // Valuation must use the override (13,000), not the aggregated cons rate. + floatEq(t, 2_600.0125, comp.CostValuation) + assert.Equal(t, rmcost.StageInit, comp.FlagValuationUsed) + // Marketing and simulation still use CONS as configured. + assert.Equal(t, rmcost.StageCons, comp.FlagMarketingUsed) +} + +func TestCalculateCost_PartialZeroCascadeMixed(t *testing.T) { + t.Parallel() + // Cons=0, Stores=50, Dept=0 → both "Cons" and "Dept" requests should cascade to Stores. + items := []rmcost.RateInputs{ + {StoresQty: ptr(10), StoresVal: ptr(500)}, + } + h := rmcost.HeaderInputs{ + CostPercentage: 1.0, + CostPerKg: 0, + FlagValuation: rmcost.StageCons, + FlagMarketing: rmcost.StageDept, + FlagSimulation: rmcost.StageStores, + } + comp := rmcost.CalculateCost(items, h) + + floatEq(t, 50.0, comp.CostValuation) + floatEq(t, 50.0, comp.CostMarketing) + floatEq(t, 50.0, comp.CostSimulation) + assert.Equal(t, rmcost.StageStores, comp.FlagValuationUsed) + assert.Equal(t, rmcost.StageStores, comp.FlagMarketingUsed) + assert.Equal(t, rmcost.StageStores, comp.FlagSimulationUsed) +} + +// ---------------------------------------------------------------------------- +// Validation + cost entity tests +// ---------------------------------------------------------------------------- + +func TestValidatePeriod(t *testing.T) { + t.Parallel() + cases := []struct { + in string + wantErr bool + }{ + {"202604", false}, + {"202001", false}, + {"202012", false}, + {"20260", true}, // 5 digits + {"2026041", true}, // 7 digits + {"202613", true}, // month 13 + {"202600", true}, // month 00 + {"20260A", true}, // non-digit + {"", true}, + } + for _, tc := range cases { + err := rmcost.ValidatePeriod(tc.in) + if tc.wantErr { + require.ErrorIs(t, err, rmcost.ErrInvalidPeriod, "in=%q", tc.in) + } else { + require.NoError(t, err, "in=%q", tc.in) + } + } +} + +func TestRMType_IsValid(t *testing.T) { + t.Parallel() + assert.True(t, rmcost.RMTypeGroup.IsValid()) + assert.True(t, rmcost.RMTypeItem.IsValid()) + assert.False(t, rmcost.RMType("OTHER").IsValid()) +} + +func TestHistoryTriggerReason_IsValid(t *testing.T) { + t.Parallel() + assert.True(t, rmcost.TriggerOracleSyncChain.IsValid()) + assert.True(t, rmcost.TriggerGroupUpdate.IsValid()) + assert.True(t, rmcost.TriggerDetailChange.IsValid()) + assert.True(t, rmcost.TriggerManualUI.IsValid()) + assert.False(t, rmcost.HistoryTriggerReason("other").IsValid()) +} + +func TestNewGroupCost_Validations(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + period string + rmCode string + calculatedBy string + wantErr error + }{ + {"valid", "202604", "GRP-CHIPS", "system", nil}, + {"bad period", "XXX", "GRP", "system", rmcost.ErrInvalidPeriod}, + {"empty rm code", "202604", "", "system", rmcost.ErrEmptyRMCode}, + {"empty calculatedBy", "202604", "GRP", "", rmcost.ErrEmptyCalculatedBy}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + comp := rmcost.Computed{FlagValuation: rmcost.StageCons} + headID := mustUUID() + _, err := rmcost.NewGroupCost(tc.period, tc.rmCode, headID, "name", "KG", comp, tc.calculatedBy) + if tc.wantErr != nil { + require.ErrorIs(t, err, tc.wantErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestCost_ApplyComputed(t *testing.T) { + t.Parallel() + comp := rmcost.Computed{ + CostValuation: 100, + CostMarketing: 200, + CostSimulation: 300, + FlagValuation: rmcost.StageCons, + FlagMarketing: rmcost.StageStores, + FlagSimulation: rmcost.StagePO1, + FlagValuationUsed: rmcost.StageCons, + FlagMarketingUsed: rmcost.StageStores, + FlagSimulationUsed: rmcost.StagePO1, + } + cost, err := rmcost.NewGroupCost("202604", "GRP", mustUUID(), "Group Name", "KG", comp, "system") + require.NoError(t, err) + + // Recalculate with new values. + newComp := rmcost.Computed{ + CostValuation: 999, + FlagValuation: rmcost.StageInit, + FlagValuationUsed: rmcost.StageInit, + FlagMarketing: rmcost.StageCons, + FlagMarketingUsed: rmcost.StageCons, + FlagSimulation: rmcost.StageCons, + FlagSimulationUsed: rmcost.StageCons, + } + require.NoError(t, cost.ApplyComputed(newComp, "editor")) + + require.NotNil(t, cost.CostValuation()) + assert.Equal(t, 999.0, *cost.CostValuation()) + assert.Equal(t, rmcost.StageInit, cost.FlagValuation()) + require.NotNil(t, cost.UpdatedBy()) + assert.Equal(t, "editor", *cost.UpdatedBy()) +} + +func TestCost_ApplyComputed_EmptyBy(t *testing.T) { + t.Parallel() + comp := rmcost.Computed{FlagValuation: rmcost.StageCons} + cost, err := rmcost.NewGroupCost("202604", "GRP", mustUUID(), "Group", "KG", comp, "system") + require.NoError(t, err) + + err = cost.ApplyComputed(rmcost.Computed{}, "") + require.ErrorIs(t, err, rmcost.ErrEmptyCalculatedBy) +} + +// ---------------------------------------------------------------------------- +// ListFilter tests +// ---------------------------------------------------------------------------- + +func TestListFilter_Validate(t *testing.T) { + t.Parallel() + + f := rmcost.ListFilter{Page: 0, PageSize: 0} + f.Validate() + assert.Equal(t, 1, f.Page) + assert.Equal(t, 10, f.PageSize) + assert.Equal(t, "period", f.SortBy) + assert.Equal(t, "desc", f.SortOrder) + + f = rmcost.ListFilter{PageSize: 500} + f.Validate() + assert.Equal(t, 100, f.PageSize) + + f = rmcost.ListFilter{Page: 3, PageSize: 20} + f.Validate() + assert.Equal(t, 40, f.Offset()) +} + +func TestReconstructCost_Getters(t *testing.T) { + t.Parallel() + + id := uuid.New() + headID := uuid.New() + itemCode := "ITEM" + rates := rmcost.StageRates{Cons: 1, Stores: 2} + costVal := 100.0 + costMkt := 200.0 + costSim := 300.0 + calcAt := nowish() + calcBy := "worker" + createdAt := nowish() + updAt := nowish() + updBy := "editor" + + c := rmcost.ReconstructCost( + id, + "202604", + "GRP", + rmcost.RMTypeGroup, + &headID, + &itemCode, + "Group Name", + "KG", + rates, + &costVal, &costMkt, &costSim, + rmcost.StageCons, rmcost.StageStores, rmcost.StagePO1, + rmcost.StageCons, rmcost.StageStores, rmcost.StagePO1, + &calcAt, + &calcBy, + createdAt, + "system", + &updAt, + &updBy, + ) + + assert.Equal(t, id, c.ID()) + assert.Equal(t, "202604", c.Period()) + assert.Equal(t, "GRP", c.RMCode()) + assert.Equal(t, rmcost.RMTypeGroup, c.RMType()) + assert.Equal(t, &headID, c.GroupHeadID()) + assert.Equal(t, &itemCode, c.ItemCode()) + assert.Equal(t, "Group Name", c.RMName()) + assert.Equal(t, "KG", c.UOMCode()) + assert.Equal(t, rates, c.Rates()) + assert.Equal(t, &costVal, c.CostValuation()) + assert.Equal(t, &costMkt, c.CostMarketing()) + assert.Equal(t, &costSim, c.CostSimulation()) + assert.Equal(t, rmcost.StageCons, c.FlagValuation()) + assert.Equal(t, rmcost.StageStores, c.FlagMarketing()) + assert.Equal(t, rmcost.StagePO1, c.FlagSimulation()) + assert.Equal(t, rmcost.StageCons, c.FlagValuationUsed()) + assert.Equal(t, rmcost.StageStores, c.FlagMarketingUsed()) + assert.Equal(t, rmcost.StagePO1, c.FlagSimulationUsed()) + assert.Equal(t, &calcAt, c.CalculatedAt()) + assert.Equal(t, &calcBy, c.CalculatedBy()) + assert.Equal(t, createdAt, c.CreatedAt()) + assert.Equal(t, "system", c.CreatedBy()) + assert.Equal(t, &updAt, c.UpdatedAt()) + assert.Equal(t, &updBy, c.UpdatedBy()) +} + +func TestStringMethods(t *testing.T) { + t.Parallel() + assert.Equal(t, "CONS", rmcost.StageCons.String()) + assert.Equal(t, "GROUP", rmcost.RMTypeGroup.String()) +} + +func TestNewListAndHistoryFilter(t *testing.T) { + t.Parallel() + + lf := rmcost.NewListFilter() + assert.Equal(t, 1, lf.Page) + assert.Equal(t, 10, lf.PageSize) + assert.Equal(t, "period", lf.SortBy) + assert.Equal(t, "desc", lf.SortOrder) + + hf := rmcost.NewHistoryFilter() + assert.Equal(t, 1, hf.Page) + assert.Equal(t, 20, hf.PageSize) +} + +func TestHistoryFilter_Validate(t *testing.T) { + t.Parallel() + + f := rmcost.HistoryFilter{} + f.Validate() + assert.Equal(t, 1, f.Page) + assert.Equal(t, 20, f.PageSize) + + f = rmcost.HistoryFilter{PageSize: 500} + f.Validate() + assert.Equal(t, 100, f.PageSize) + + f = rmcost.HistoryFilter{Page: 2, PageSize: 10} + f.Validate() + assert.Equal(t, 10, f.Offset()) +} diff --git a/services/finance/internal/domain/rmcost/entity.go b/services/finance/internal/domain/rmcost/entity.go new file mode 100644 index 0000000..4374d4b --- /dev/null +++ b/services/finance/internal/domain/rmcost/entity.go @@ -0,0 +1,412 @@ +// Package rmcost provides the landed-cost calculation engine and persistence contract +// for the RM cost aggregates produced from grouped raw-material consumption data. +package rmcost + +import ( + "regexp" + "time" + + "github.com/google/uuid" +) + +// RMType distinguishes a group-level cost row from an item-level cost row. +// Mirrors the chk_rm_type CHECK constraint on cst_rm_cost. +type RMType string + +const ( + // RMTypeGroup indicates the cost row aggregates a whole RM group. + RMTypeGroup RMType = "GROUP" + // RMTypeItem indicates the cost row is computed for a single item (future phase). + RMTypeItem RMType = "ITEM" +) + +// IsValid reports whether the RMType is one of the recognized values. +func (t RMType) IsValid() bool { + switch t { + case RMTypeGroup, RMTypeItem: + return true + default: + return false + } +} + +// String returns the canonical string form. +func (t RMType) String() string { return string(t) } + +// periodPattern validates a 6-character YYYYMM period (e.g. "202604"). +var periodPattern = regexp.MustCompile(`^\d{6}$`) + +// ValidatePeriod returns ErrInvalidPeriod when the supplied string is not a 6-digit YYYYMM value. +func ValidatePeriod(period string) error { + if !periodPattern.MatchString(period) { + return ErrInvalidPeriod + } + // Month digits must be 01-12. + month := period[4:] + if month < "01" || month > "12" { + return ErrInvalidPeriod + } + return nil +} + +// Cost is the aggregate root representing the computed landed cost for a single +// (period, rm_code) pair. It is populated by the worker and then upserted to +// `cst_rm_cost`; callers retrieve it via the Repository to display in the UI. +// +//nolint:revive // Wide struct mirrors the persistence row one-for-one. +type Cost struct { + id uuid.UUID + period string + rmCode string + rmType RMType + groupHeadID *uuid.UUID + itemCode *string + rmName string + uomCode string + + // Per-stage snapshot of the aggregated rates (before selection). + rates StageRates + + // Landed cost per purpose (raw — UI formats for display). + costValuation *float64 + costMarketing *float64 + costSimulation *float64 + + // Configured flags at calc time. + flagValuation Stage + flagMarketing Stage + flagSimulation Stage + + // Stages actually used after cascade/INIT resolution. + flagValuationUsed Stage + flagMarketingUsed Stage + flagSimulationUsed Stage + + calculatedAt *time.Time + calculatedBy *string + + createdAt time.Time + createdBy string + updatedAt *time.Time + updatedBy *string +} + +// Computed carries the output of one calculation pass. Construct via CalculateCost, +// then pass to NewCost / ApplyComputed to persist. +type Computed struct { + Rates StageRates + CostValuation float64 + CostMarketing float64 + CostSimulation float64 + FlagValuation Stage + FlagMarketing Stage + FlagSimulation Stage + FlagValuationUsed Stage + FlagMarketingUsed Stage + FlagSimulationUsed Stage +} + +// HeaderInputs captures the RM group header fields the engine needs. Passed by +// the application layer; rmcost does not import rmgroup (keeps domain boundaries clean). +type HeaderInputs struct { + CostPercentage float64 + CostPerKg float64 + FlagValuation Stage + FlagMarketing Stage + FlagSimulation Stage + InitValValuation *float64 + InitValMarketing *float64 + InitValSimulation *float64 +} + +// CalculateCost runs the full pipeline (AggregateRates → SelectRate × 3 → LandedCost × 3) +// for one group in one period. Pure function — no I/O, no state. +func CalculateCost(items []RateInputs, h HeaderInputs) Computed { + rates := AggregateRates(items) + valRate, valUsed := SelectRate(rates, h.FlagValuation, h.InitValValuation) + mktRate, mktUsed := SelectRate(rates, h.FlagMarketing, h.InitValMarketing) + simRate, simUsed := SelectRate(rates, h.FlagSimulation, h.InitValSimulation) + return Computed{ + Rates: rates, + CostValuation: LandedCost(h.CostPercentage, valRate, h.CostPerKg), + CostMarketing: LandedCost(h.CostPercentage, mktRate, h.CostPerKg), + CostSimulation: LandedCost(h.CostPercentage, simRate, h.CostPerKg), + FlagValuation: h.FlagValuation, + FlagMarketing: h.FlagMarketing, + FlagSimulation: h.FlagSimulation, + FlagValuationUsed: valUsed, + FlagMarketingUsed: mktUsed, + FlagSimulationUsed: simUsed, + } +} + +// NewGroupCost creates a new Cost row for rm_type=GROUP. The worker calls this +// after calculation and then persists via Repository.Upsert. +func NewGroupCost( + period, rmCode string, + groupHeadID uuid.UUID, + rmName, uomCode string, + comp Computed, + calculatedBy string, +) (*Cost, error) { + if err := ValidatePeriod(period); err != nil { + return nil, err + } + if rmCode == "" { + return nil, ErrEmptyRMCode + } + if calculatedBy == "" { + return nil, ErrEmptyCalculatedBy + } + now := time.Now() + headID := groupHeadID + by := calculatedBy + costVal := comp.CostValuation + costMkt := comp.CostMarketing + costSim := comp.CostSimulation + return &Cost{ + id: uuid.New(), + period: period, + rmCode: rmCode, + rmType: RMTypeGroup, + groupHeadID: &headID, + rmName: rmName, + uomCode: uomCode, + rates: comp.Rates, + costValuation: &costVal, + costMarketing: &costMkt, + costSimulation: &costSim, + flagValuation: comp.FlagValuation, + flagMarketing: comp.FlagMarketing, + flagSimulation: comp.FlagSimulation, + flagValuationUsed: comp.FlagValuationUsed, + flagMarketingUsed: comp.FlagMarketingUsed, + flagSimulationUsed: comp.FlagSimulationUsed, + calculatedAt: &now, + calculatedBy: &by, + createdAt: now, + createdBy: calculatedBy, + }, nil +} + +// ReconstructCost rebuilds a Cost from persistence. Used by repositories only. +// +//nolint:revive // Persistence reconstitution takes many fields by design. +func ReconstructCost( + id uuid.UUID, + period, rmCode string, + rmType RMType, + groupHeadID *uuid.UUID, + itemCode *string, + rmName, uomCode string, + rates StageRates, + costValuation, costMarketing, costSimulation *float64, + flagValuation, flagMarketing, flagSimulation Stage, + flagValuationUsed, flagMarketingUsed, flagSimulationUsed Stage, + calculatedAt *time.Time, + calculatedBy *string, + createdAt time.Time, + createdBy string, + updatedAt *time.Time, + updatedBy *string, +) *Cost { + return &Cost{ + id: id, + period: period, + rmCode: rmCode, + rmType: rmType, + groupHeadID: groupHeadID, + itemCode: itemCode, + rmName: rmName, + uomCode: uomCode, + rates: rates, + costValuation: costValuation, + costMarketing: costMarketing, + costSimulation: costSimulation, + flagValuation: flagValuation, + flagMarketing: flagMarketing, + flagSimulation: flagSimulation, + flagValuationUsed: flagValuationUsed, + flagMarketingUsed: flagMarketingUsed, + flagSimulationUsed: flagSimulationUsed, + calculatedAt: calculatedAt, + calculatedBy: calculatedBy, + createdAt: createdAt, + createdBy: createdBy, + updatedAt: updatedAt, + updatedBy: updatedBy, + } +} + +// ApplyComputed overwrites the per-stage rates, costs, and flag-used fields with +// the output of a fresh CalculateCost pass. The caller passes `recalculatedBy`, +// and the Cost records it on the calculated_* and updated_* audit columns. +func (c *Cost) ApplyComputed(comp Computed, recalculatedBy string) error { + if recalculatedBy == "" { + return ErrEmptyCalculatedBy + } + now := time.Now() + by := recalculatedBy + costVal := comp.CostValuation + costMkt := comp.CostMarketing + costSim := comp.CostSimulation + c.rates = comp.Rates + c.costValuation = &costVal + c.costMarketing = &costMkt + c.costSimulation = &costSim + c.flagValuation = comp.FlagValuation + c.flagMarketing = comp.FlagMarketing + c.flagSimulation = comp.FlagSimulation + c.flagValuationUsed = comp.FlagValuationUsed + c.flagMarketingUsed = comp.FlagMarketingUsed + c.flagSimulationUsed = comp.FlagSimulationUsed + c.calculatedAt = &now + c.calculatedBy = &by + c.updatedAt = &now + c.updatedBy = &by + return nil +} + +// SetUOMCode updates the unit-of-measure code. Used by recalc to refresh a +// stale UOM on an existing Cost row when group details change. +func (c *Cost) SetUOMCode(code string) { c.uomCode = code } + +// Cost getters. + +// ID returns the cost row UUID. +func (c *Cost) ID() uuid.UUID { return c.id } + +// Period returns the YYYYMM period string. +func (c *Cost) Period() string { return c.period } + +// RMCode returns the rm_code (group code for rm_type=GROUP, item code for ITEM). +func (c *Cost) RMCode() string { return c.rmCode } + +// RMType returns the RM type discriminator. +func (c *Cost) RMType() RMType { return c.rmType } + +// GroupHeadID returns the owning group head ID (nil when rm_type=ITEM). +func (c *Cost) GroupHeadID() *uuid.UUID { return c.groupHeadID } + +// ItemCode returns the item code (nil when rm_type=GROUP). +func (c *Cost) ItemCode() *string { return c.itemCode } + +// RMName returns the display name of the RM (group or item name). +func (c *Cost) RMName() string { return c.rmName } + +// UOMCode returns the unit-of-measure code for the RM. +func (c *Cost) UOMCode() string { return c.uomCode } + +// Rates returns the per-stage aggregated rate snapshot. +func (c *Cost) Rates() StageRates { return c.rates } + +// CostValuation returns the computed valuation landed cost (nil when never calculated). +func (c *Cost) CostValuation() *float64 { return c.costValuation } + +// CostMarketing returns the computed marketing landed cost (nil when never calculated). +func (c *Cost) CostMarketing() *float64 { return c.costMarketing } + +// CostSimulation returns the computed simulation landed cost (nil when never calculated). +func (c *Cost) CostSimulation() *float64 { return c.costSimulation } + +// FlagValuation returns the flag configured on the group header at calc time. +func (c *Cost) FlagValuation() Stage { return c.flagValuation } + +// FlagMarketing returns the flag configured on the group header at calc time. +func (c *Cost) FlagMarketing() Stage { return c.flagMarketing } + +// FlagSimulation returns the flag configured on the group header at calc time. +func (c *Cost) FlagSimulation() Stage { return c.flagSimulation } + +// FlagValuationUsed returns the stage actually used after cascade/INIT resolution. +func (c *Cost) FlagValuationUsed() Stage { return c.flagValuationUsed } + +// FlagMarketingUsed returns the stage actually used after cascade/INIT resolution. +func (c *Cost) FlagMarketingUsed() Stage { return c.flagMarketingUsed } + +// FlagSimulationUsed returns the stage actually used after cascade/INIT resolution. +func (c *Cost) FlagSimulationUsed() Stage { return c.flagSimulationUsed } + +// CalculatedAt returns the last calculation timestamp (nil when never calculated). +func (c *Cost) CalculatedAt() *time.Time { return c.calculatedAt } + +// CalculatedBy returns who last ran the calculation (nil when never calculated). +func (c *Cost) CalculatedBy() *string { return c.calculatedBy } + +// CreatedAt returns the creation timestamp. +func (c *Cost) CreatedAt() time.Time { return c.createdAt } + +// CreatedBy returns the creator. +func (c *Cost) CreatedBy() string { return c.createdBy } + +// UpdatedAt returns the last update timestamp. +func (c *Cost) UpdatedAt() *time.Time { return c.updatedAt } + +// UpdatedBy returns the last updater. +func (c *Cost) UpdatedBy() *string { return c.updatedBy } + +// ============================================================================= +// History — append-only audit trail (aud_rm_cost_history). +// ============================================================================= + +// HistoryTriggerReason enumerates the reasons a calculation was run. The DB +// stores the raw string; callers must pass one of these canonical values. +type HistoryTriggerReason string + +// HistoryTriggerReason canonical values. +const ( + // TriggerOracleSyncChain = auto-chained after a successful Oracle sync for the synced period. + TriggerOracleSyncChain HistoryTriggerReason = "oracle-sync-chain" + // TriggerGroupUpdate = recalculated because the group header changed. + TriggerGroupUpdate HistoryTriggerReason = "group-update" + // TriggerDetailChange = recalculated because an item was added/removed/toggled in the group. + TriggerDetailChange HistoryTriggerReason = "detail-change" + // TriggerManualUI = recalculated on explicit user request from the UI. + TriggerManualUI HistoryTriggerReason = "manual-ui" +) + +// IsValid reports whether the trigger reason is one of the recognized values. +func (r HistoryTriggerReason) IsValid() bool { + switch r { + case TriggerOracleSyncChain, TriggerGroupUpdate, TriggerDetailChange, TriggerManualUI: + return true + default: + return false + } +} + +// History is a single row of the append-only audit trail. Stores a full snapshot +// of the inputs AND outputs of one calculation pass so operators can diff and +// replay. +// +//nolint:revive // Wide struct mirrors the persistence row one-for-one. +type History struct { + ID uuid.UUID + RMCostID *uuid.UUID + JobID *uuid.UUID + Period string + RMCode string + RMType RMType + GroupHeadID *uuid.UUID + Rates StageRates + CostPercentage float64 + CostPerKg float64 + + FlagValuation Stage + FlagMarketing Stage + FlagSimulation Stage + InitValValuation *float64 + InitValMarketing *float64 + InitValSimulation *float64 + CostValuation *float64 + CostMarketing *float64 + CostSimulation *float64 + FlagValuationUsed Stage + FlagMarketingUsed Stage + FlagSimulationUsed Stage + + SourceItemCount int + TriggerReason HistoryTriggerReason + CalculatedAt time.Time + CalculatedBy string +} diff --git a/services/finance/internal/domain/rmcost/errors.go b/services/finance/internal/domain/rmcost/errors.go new file mode 100644 index 0000000..fe4b535 --- /dev/null +++ b/services/finance/internal/domain/rmcost/errors.go @@ -0,0 +1,32 @@ +// Package rmcost provides the landed-cost calculation engine and persistence contract +// for the RM cost aggregates produced from grouped raw-material consumption data. +package rmcost + +import "errors" + +// Domain errors for RM cost operations. +var ( + // ErrNotFound is returned when an RM cost row is not found. + ErrNotFound = errors.New("rm cost not found") + + // ErrInvalidPeriod is returned when the period string is not a valid YYYYMM value. + ErrInvalidPeriod = errors.New("period must be a 6-character YYYYMM string") + + // ErrEmptyRMCode is returned when rm_code is empty. + ErrEmptyRMCode = errors.New("rm_code cannot be empty") + + // ErrInvalidRMType is returned when rm_type is not one of the recognized values. + ErrInvalidRMType = errors.New("rm_type must be GROUP or ITEM") + + // ErrInvalidStage is returned when a stage value is not one of the recognized values. + ErrInvalidStage = errors.New("invalid stage — must be one of CONS, STORES, DEPT, PO_1, PO_2, PO_3, INIT") + + // ErrEmptyCreatedBy is returned when created_by is empty. + ErrEmptyCreatedBy = errors.New("created_by cannot be empty") + + // ErrEmptyCalculatedBy is returned when calculated_by is empty. + ErrEmptyCalculatedBy = errors.New("calculated_by cannot be empty") + + // ErrNegativeCost is returned when a computed or supplied cost is negative. + ErrNegativeCost = errors.New("cost must be non-negative") +) diff --git a/services/finance/internal/domain/rmcost/repository.go b/services/finance/internal/domain/rmcost/repository.go new file mode 100644 index 0000000..47a88aa --- /dev/null +++ b/services/finance/internal/domain/rmcost/repository.go @@ -0,0 +1,157 @@ +// Package rmcost provides the landed-cost calculation engine and persistence contract +// for the RM cost aggregates produced from grouped raw-material consumption data. +package rmcost + +import ( + "context" + + "github.com/google/uuid" +) + +// Repository defines the persistence contract for `cst_rm_cost` and the +// append-only history table `aud_rm_cost_history`. +type Repository interface { + // Upsert writes the Cost row keyed on (period, rm_code). When a row already + // exists it is overwritten with the supplied fields; otherwise a new row is + // inserted. Implementations must perform the write inside a transaction and + // append one History row within the same transaction. + Upsert(ctx context.Context, cost *Cost, hist History) error + + // GetByID retrieves a Cost by its primary key. Returns ErrNotFound when absent. + GetByID(ctx context.Context, id uuid.UUID) (*Cost, error) + + // GetByPeriodAndCode retrieves a Cost by (period, rm_code). Returns ErrNotFound when absent. + GetByPeriodAndCode(ctx context.Context, period, rmCode string) (*Cost, error) + + // List returns a page of Cost rows plus the total count of matching rows. + List(ctx context.Context, filter ListFilter) ([]*Cost, int64, error) + + // ListAll returns every cost row matching the filter with no pagination, + // ordered by period DESC then rm_code ASC. Used by export. + ListAll(ctx context.Context, filter ExportFilter) ([]*Cost, error) + + // ListHistory returns the history rows matching the supplied filter, newest first. + ListHistory(ctx context.Context, filter HistoryFilter) ([]History, int64, error) + + // ExistsForGroupHead reports whether any Cost row currently references the + // given group head. Used as a delete-guard so a group head cannot be removed + // after cost data has been generated for it. + ExistsForGroupHead(ctx context.Context, groupHeadID uuid.UUID) (bool, error) + + // ListDistinctPeriods returns the set of periods that have at least one + // cost row, ordered DESC (newest first). + ListDistinctPeriods(ctx context.Context) ([]string, error) +} + +// ListFilter describes pagination, search, and sort options for ListCosts. +type ListFilter struct { + // Period matches the 6-character YYYYMM period exactly when non-empty. + Period string + + // RMType filters by rm_type when non-empty. + RMType RMType + + // GroupHeadID filters to costs belonging to the given group head when non-nil. + GroupHeadID *uuid.UUID + + // Search matches against rm_code and rm_name. + Search string + + Page int + PageSize int + + // SortBy accepts "period", "rm_code", "rm_name", "calculated_at". + SortBy string + // SortOrder accepts "asc" or "desc". + SortOrder string +} + +// NewListFilter returns a ListFilter with sensible defaults. +func NewListFilter() ListFilter { + return ListFilter{ + Page: 1, + PageSize: 10, + SortBy: "period", + SortOrder: "desc", + } +} + +// Validate normalizes the filter so callers can pass partially-populated structs. +func (f *ListFilter) Validate() { + if f.Page < 1 { + f.Page = 1 + } + if f.PageSize < 1 { + f.PageSize = 10 + } + if f.PageSize > 100 { + f.PageSize = 100 + } + if f.SortBy == "" { + f.SortBy = "period" + } + if f.SortOrder == "" { + f.SortOrder = "desc" + } +} + +// Offset returns the SQL OFFSET corresponding to Page/PageSize. +func (f *ListFilter) Offset() int { + return (f.Page - 1) * f.PageSize +} + +// ExportFilter scopes the unpaginated ListAll used by the Excel export path. +type ExportFilter struct { + // Period matches the 6-character YYYYMM period exactly when non-empty. + Period string + // RMType filters by rm_type when non-empty. + RMType RMType + // GroupHeadID filters to costs belonging to the given group head when non-nil. + GroupHeadID *uuid.UUID + // Search matches against rm_code and rm_name. + Search string +} + +// HistoryFilter scopes a ListHistory query. +type HistoryFilter struct { + // Period matches the 6-character YYYYMM period exactly when non-empty. + Period string + + // RMCode matches a specific rm_code when non-empty. + RMCode string + + // GroupHeadID filters to history rows for the given group head when non-nil. + GroupHeadID *uuid.UUID + + // JobID filters to history rows produced by a given job run when non-nil. + JobID *uuid.UUID + + Page int + PageSize int +} + +// NewHistoryFilter returns a HistoryFilter with sensible defaults. +func NewHistoryFilter() HistoryFilter { + return HistoryFilter{ + Page: 1, + PageSize: 20, + } +} + +// Validate normalizes the filter so callers can pass partially-populated structs. +func (f *HistoryFilter) Validate() { + if f.Page < 1 { + f.Page = 1 + } + if f.PageSize < 1 { + f.PageSize = 20 + } + if f.PageSize > 100 { + f.PageSize = 100 + } +} + +// Offset returns the SQL OFFSET corresponding to Page/PageSize. +func (f *HistoryFilter) Offset() int { + return (f.Page - 1) * f.PageSize +} diff --git a/services/finance/internal/domain/rmgroup/entity.go b/services/finance/internal/domain/rmgroup/entity.go new file mode 100644 index 0000000..cd6eac1 --- /dev/null +++ b/services/finance/internal/domain/rmgroup/entity.go @@ -0,0 +1,664 @@ +// Package rmgroup provides domain logic for raw-material grouping and landed-cost configuration. +package rmgroup + +import ( + "time" + + "github.com/google/uuid" +) + +// ============================================================================= +// Head — aggregate root for an RM group's cost configuration. +// ============================================================================= + +// Head is the aggregate root. It carries cost-formula inputs and the flags that +// select which stage rate feeds each purpose (valuation, marketing, simulation). +type Head struct { + id uuid.UUID + code Code + name string + description string + colorant string + ciName string + costPercentage float64 + costPerKg float64 + flagValuation Flag + flagMarketing Flag + flagSimulation Flag + initValValuation *float64 + initValMarketing *float64 + initValSimulation *float64 + isActive bool + createdAt time.Time + createdBy string + updatedAt *time.Time + updatedBy *string + deletedAt *time.Time + deletedBy *string +} + +// NewHead creates a new Head with validation. Defaults: flags = CONS, isActive = true. +func NewHead( + code Code, + name string, + description string, + costPercentage float64, + costPerKg float64, + createdBy string, +) (*Head, error) { + if code.IsEmpty() { + return nil, ErrEmptyCode + } + if name == "" { + return nil, ErrEmptyName + } + if len(name) > 200 { + return nil, ErrNameTooLong + } + if createdBy == "" { + return nil, ErrEmptyCreatedBy + } + if costPercentage < 0 { + return nil, ErrNegativeCostPercentage + } + if costPerKg < 0 { + return nil, ErrNegativeCostPerKg + } + + return &Head{ + id: uuid.New(), + code: code, + name: name, + description: description, + costPercentage: costPercentage, + costPerKg: costPerKg, + flagValuation: FlagCons, + flagMarketing: FlagCons, + flagSimulation: FlagCons, + isActive: true, + createdAt: time.Now(), + createdBy: createdBy, + }, nil +} + +// ReconstructHead rebuilds a Head from persistence. Used by repositories only. +func ReconstructHead( + id uuid.UUID, + code Code, + name, description, colorant, ciName string, + costPercentage, costPerKg float64, + flagValuation, flagMarketing, flagSimulation Flag, + initValValuation, initValMarketing, initValSimulation *float64, + isActive bool, + createdAt time.Time, + createdBy string, + updatedAt *time.Time, + updatedBy *string, + deletedAt *time.Time, + deletedBy *string, +) *Head { + return &Head{ + id: id, + code: code, + name: name, + description: description, + colorant: colorant, + ciName: ciName, + costPercentage: costPercentage, + costPerKg: costPerKg, + flagValuation: flagValuation, + flagMarketing: flagMarketing, + flagSimulation: flagSimulation, + initValValuation: initValValuation, + initValMarketing: initValMarketing, + initValSimulation: initValSimulation, + isActive: isActive, + createdAt: createdAt, + createdBy: createdBy, + updatedAt: updatedAt, + updatedBy: updatedBy, + deletedAt: deletedAt, + deletedBy: deletedBy, + } +} + +// Getters (read-only exposure of internal state). + +// ID returns the head UUID. +func (h *Head) ID() uuid.UUID { return h.id } + +// Code returns the group code. +func (h *Head) Code() Code { return h.code } + +// Name returns the group display name. +func (h *Head) Name() string { return h.name } + +// Description returns the free-text description. +func (h *Head) Description() string { return h.description } + +// Colorant returns the optional colorant tag. +func (h *Head) Colorant() string { return h.colorant } + +// CIName returns the optional CI name tag. +func (h *Head) CIName() string { return h.ciName } + +// CostPercentage returns the cost percentage multiplier used in the landed-cost formula. +func (h *Head) CostPercentage() float64 { return h.costPercentage } + +// CostPerKg returns the per-kg overhead added to the landed cost. +func (h *Head) CostPerKg() float64 { return h.costPerKg } + +// FlagValuation returns the stage flag used for valuation cost. +func (h *Head) FlagValuation() Flag { return h.flagValuation } + +// FlagMarketing returns the stage flag used for marketing cost. +func (h *Head) FlagMarketing() Flag { return h.flagMarketing } + +// FlagSimulation returns the stage flag used for simulation cost. +func (h *Head) FlagSimulation() Flag { return h.flagSimulation } + +// InitValValuation returns the init_val override for valuation (nil when unset). +func (h *Head) InitValValuation() *float64 { return h.initValValuation } + +// InitValMarketing returns the init_val override for marketing (nil when unset). +func (h *Head) InitValMarketing() *float64 { return h.initValMarketing } + +// InitValSimulation returns the init_val override for simulation (nil when unset). +func (h *Head) InitValSimulation() *float64 { return h.initValSimulation } + +// IsActive returns whether the group is active. +func (h *Head) IsActive() bool { return h.isActive } + +// CreatedAt returns the creation timestamp. +func (h *Head) CreatedAt() time.Time { return h.createdAt } + +// CreatedBy returns the creator. +func (h *Head) CreatedBy() string { return h.createdBy } + +// UpdatedAt returns the last update timestamp. +func (h *Head) UpdatedAt() *time.Time { return h.updatedAt } + +// UpdatedBy returns the last updater. +func (h *Head) UpdatedBy() *string { return h.updatedBy } + +// DeletedAt returns the soft-delete timestamp. +func (h *Head) DeletedAt() *time.Time { return h.deletedAt } + +// DeletedBy returns who soft-deleted the group. +func (h *Head) DeletedBy() *string { return h.deletedBy } + +// IsDeleted reports whether the group is soft-deleted. +func (h *Head) IsDeleted() bool { return h.deletedAt != nil } + +// ============================================================================= +// Head — behavior methods +// ============================================================================= + +// UpdateInput carries all optional mutations for Head.Update. Callers set only the +// fields they intend to change (pointers stay nil otherwise). This keeps the Update +// signature stable as fields grow. +type UpdateInput struct { + Name *string + Description *string + Colorant *string + CIName *string + CostPercentage *float64 + CostPerKg *float64 + FlagValuation *Flag + FlagMarketing *Flag + FlagSimulation *Flag + InitValValuation *float64 + InitValMarketing *float64 + InitValSimulation *float64 + IsActive *bool + + // ClearInitValValuation forces the init_val_valuation to NULL. + // Use when the caller wants to unset a previously-set init value. + ClearInitValValuation bool + ClearInitValMarketing bool + ClearInitValSimulation bool +} + +// Update applies a partial update to the head and records audit fields. +// Each field has a dedicated apply helper to keep cognitive complexity low. +func (h *Head) Update(in UpdateInput, updatedBy string) error { + if h.IsDeleted() { + return ErrAlreadyDeleted + } + if updatedBy == "" { + return ErrEmptyUpdatedBy + } + + if err := h.applyNameField(in.Name); err != nil { + return err + } + h.applyTextFields(in.Description, in.Colorant, in.CIName) + if err := h.applyCostFields(in.CostPercentage, in.CostPerKg); err != nil { + return err + } + if err := h.applyInitValues(in); err != nil { + return err + } + if err := h.applyFlagFields(in.FlagValuation, in.FlagMarketing, in.FlagSimulation); err != nil { + return err + } + if err := h.assertFlagInitConsistency(); err != nil { + return err + } + if in.IsActive != nil { + h.isActive = *in.IsActive + } + + now := time.Now() + h.updatedAt = &now + h.updatedBy = &updatedBy + return nil +} + +func (h *Head) applyNameField(name *string) error { + if name == nil { + return nil + } + if *name == "" { + return ErrEmptyName + } + if len(*name) > 200 { + return ErrNameTooLong + } + h.name = *name + return nil +} + +func (h *Head) applyTextFields(description, colorant, ciName *string) { + if description != nil { + h.description = *description + } + if colorant != nil { + h.colorant = *colorant + } + if ciName != nil { + h.ciName = *ciName + } +} + +func (h *Head) applyCostFields(costPercentage, costPerKg *float64) error { + if costPercentage != nil { + if *costPercentage < 0 { + return ErrNegativeCostPercentage + } + h.costPercentage = *costPercentage + } + if costPerKg != nil { + if *costPerKg < 0 { + return ErrNegativeCostPerKg + } + h.costPerKg = *costPerKg + } + return nil +} + +func (h *Head) applyInitValues(in UpdateInput) error { + if err := assignInitVal(&h.initValValuation, in.InitValValuation, in.ClearInitValValuation); err != nil { + return err + } + if err := assignInitVal(&h.initValMarketing, in.InitValMarketing, in.ClearInitValMarketing); err != nil { + return err + } + return assignInitVal(&h.initValSimulation, in.InitValSimulation, in.ClearInitValSimulation) +} + +func assignInitVal(target **float64, incoming *float64, reset bool) error { + if reset { + *target = nil + return nil + } + if incoming == nil { + return nil + } + if *incoming < 0 { + return ErrNegativeInitValue + } + v := *incoming + *target = &v + return nil +} + +func (h *Head) applyFlagFields(valuation, marketing, simulation *Flag) error { + if valuation != nil { + if !valuation.IsValid() { + return ErrInvalidFlag + } + h.flagValuation = *valuation + } + if marketing != nil { + if !marketing.IsValid() { + return ErrInvalidFlag + } + h.flagMarketing = *marketing + } + if simulation != nil { + if !simulation.IsValid() { + return ErrInvalidFlag + } + h.flagSimulation = *simulation + } + return nil +} + +// assertFlagInitConsistency enforces that a flag set to INIT has a non-nil init_val. +// Mirrors the chk_rm_group_init_val_* CHECK constraints on cst_rm_group_head. +func (h *Head) assertFlagInitConsistency() error { + if h.flagValuation == FlagInit && h.initValValuation == nil { + return ErrInitValueRequired + } + if h.flagMarketing == FlagInit && h.initValMarketing == nil { + return ErrInitValueRequired + } + if h.flagSimulation == FlagInit && h.initValSimulation == nil { + return ErrInitValueRequired + } + return nil +} + +// SoftDelete marks the head as deleted. +func (h *Head) SoftDelete(deletedBy string) error { + if h.IsDeleted() { + return ErrAlreadyDeleted + } + if deletedBy == "" { + return ErrEmptyUpdatedBy + } + now := time.Now() + h.deletedAt = &now + h.deletedBy = &deletedBy + h.isActive = false + return nil +} + +// ============================================================================= +// Detail — items (RMs) assigned to a Head. +// ============================================================================= + +// Detail represents one item's membership in an RM group. +type Detail struct { + id uuid.UUID + headID uuid.UUID + itemCode ItemCode + itemName string + itemTypeCode string + gradeCode string + itemGrade string + uomCode string + marketPercentage *float64 + marketValueRp *float64 + sortOrder int32 + isActive bool + isDummy bool + createdAt time.Time + createdBy string + updatedAt *time.Time + updatedBy *string + deletedAt *time.Time + deletedBy *string +} + +// NewDetail creates a new Detail with validation. Defaults: isActive = true, isDummy = false. +func NewDetail(headID uuid.UUID, itemCode ItemCode, createdBy string) (*Detail, error) { + if itemCode.IsEmpty() { + return nil, ErrEmptyItemCode + } + if createdBy == "" { + return nil, ErrEmptyCreatedBy + } + return &Detail{ + id: uuid.New(), + headID: headID, + itemCode: itemCode, + isActive: true, + createdAt: time.Now(), + createdBy: createdBy, + }, nil +} + +// ReconstructDetail rebuilds a Detail from persistence. Used by repositories only. +// +//nolint:revive // Many fields required for persistence reconstitution. +func ReconstructDetail( + id, headID uuid.UUID, + itemCode ItemCode, + itemName, itemTypeCode, gradeCode, itemGrade, uomCode string, + marketPercentage, marketValueRp *float64, + sortOrder int32, + isActive, isDummy bool, + createdAt time.Time, + createdBy string, + updatedAt *time.Time, + updatedBy *string, + deletedAt *time.Time, + deletedBy *string, +) *Detail { + return &Detail{ + id: id, + headID: headID, + itemCode: itemCode, + itemName: itemName, + itemTypeCode: itemTypeCode, + gradeCode: gradeCode, + itemGrade: itemGrade, + uomCode: uomCode, + marketPercentage: marketPercentage, + marketValueRp: marketValueRp, + sortOrder: sortOrder, + isActive: isActive, + isDummy: isDummy, + createdAt: createdAt, + createdBy: createdBy, + updatedAt: updatedAt, + updatedBy: updatedBy, + deletedAt: deletedAt, + deletedBy: deletedBy, + } +} + +// Detail getters. + +// ID returns the detail UUID. +func (d *Detail) ID() uuid.UUID { return d.id } + +// HeadID returns the owning head UUID. +func (d *Detail) HeadID() uuid.UUID { return d.headID } + +// ItemCode returns the item code. +func (d *Detail) ItemCode() ItemCode { return d.itemCode } + +// ItemName returns the item name. +func (d *Detail) ItemName() string { return d.itemName } + +// ItemTypeCode returns the item type code. +func (d *Detail) ItemTypeCode() string { return d.itemTypeCode } + +// GradeCode returns the grade code. +func (d *Detail) GradeCode() string { return d.gradeCode } + +// ItemGrade returns the item grade. +func (d *Detail) ItemGrade() string { return d.itemGrade } + +// UOMCode returns the unit-of-measure code. +func (d *Detail) UOMCode() string { return d.uomCode } + +// MarketPercentage returns the per-item marketing percentage (nil when unset). +func (d *Detail) MarketPercentage() *float64 { return d.marketPercentage } + +// MarketValueRp returns the per-item marketing value in rupiah (nil when unset). +func (d *Detail) MarketValueRp() *float64 { return d.marketValueRp } + +// SortOrder returns the display order within the group. +func (d *Detail) SortOrder() int32 { return d.sortOrder } + +// IsActive reports whether the detail contributes to rate aggregation. +func (d *Detail) IsActive() bool { return d.isActive } + +// IsDummy reports whether the detail is a placeholder (excluded from aggregation regardless of IsActive). +func (d *Detail) IsDummy() bool { return d.isDummy } + +// CreatedAt returns the creation timestamp. +func (d *Detail) CreatedAt() time.Time { return d.createdAt } + +// CreatedBy returns the creator. +func (d *Detail) CreatedBy() string { return d.createdBy } + +// UpdatedAt returns the last update timestamp. +func (d *Detail) UpdatedAt() *time.Time { return d.updatedAt } + +// UpdatedBy returns the last updater. +func (d *Detail) UpdatedBy() *string { return d.updatedBy } + +// DeletedAt returns the soft-delete timestamp. +func (d *Detail) DeletedAt() *time.Time { return d.deletedAt } + +// DeletedBy returns who soft-deleted the detail. +func (d *Detail) DeletedBy() *string { return d.deletedBy } + +// IsDeleted reports whether the detail is soft-deleted. +func (d *Detail) IsDeleted() bool { return d.deletedAt != nil } + +// ============================================================================= +// Detail — behavior methods +// ============================================================================= + +// DetailUpdateInput carries optional mutations for Detail.Update. +type DetailUpdateInput struct { + ItemName *string + ItemTypeCode *string + GradeCode *string + ItemGrade *string + UOMCode *string + MarketPercentage *float64 + MarketValueRp *float64 + SortOrder *int32 + IsActive *bool + IsDummy *bool + + // ClearMarketPercentage forces market_percentage to NULL. + ClearMarketPercentage bool + ClearMarketValueRp bool +} + +// Update applies a partial update to the detail. +func (d *Detail) Update(in DetailUpdateInput, updatedBy string) error { + if d.IsDeleted() { + return ErrAlreadyDeleted + } + if updatedBy == "" { + return ErrEmptyUpdatedBy + } + + d.applyDetailTextFields(in) + if err := d.applyDetailMarketFields(in); err != nil { + return err + } + d.applyDetailFlagFields(in) + + now := time.Now() + d.updatedAt = &now + d.updatedBy = &updatedBy + return nil +} + +func (d *Detail) applyDetailTextFields(in DetailUpdateInput) { + if in.ItemName != nil { + d.itemName = *in.ItemName + } + if in.ItemTypeCode != nil { + d.itemTypeCode = *in.ItemTypeCode + } + if in.GradeCode != nil { + d.gradeCode = *in.GradeCode + } + if in.ItemGrade != nil { + d.itemGrade = *in.ItemGrade + } + if in.UOMCode != nil { + d.uomCode = *in.UOMCode + } +} + +func (d *Detail) applyDetailMarketFields(in DetailUpdateInput) error { + if in.ClearMarketPercentage { + d.marketPercentage = nil + } else if in.MarketPercentage != nil { + if *in.MarketPercentage < 0 { + return ErrNegativeMarketPercentage + } + v := *in.MarketPercentage + d.marketPercentage = &v + } + if in.ClearMarketValueRp { + d.marketValueRp = nil + } else if in.MarketValueRp != nil { + if *in.MarketValueRp < 0 { + return ErrNegativeMarketValue + } + v := *in.MarketValueRp + d.marketValueRp = &v + } + return nil +} + +func (d *Detail) applyDetailFlagFields(in DetailUpdateInput) { + if in.SortOrder != nil { + d.sortOrder = *in.SortOrder + } + if in.IsActive != nil { + d.isActive = *in.IsActive + } + if in.IsDummy != nil { + d.isDummy = *in.IsDummy + } +} + +// SoftDelete marks the detail as deleted. +func (d *Detail) SoftDelete(deletedBy string) error { + if d.IsDeleted() { + return ErrAlreadyDeleted + } + if deletedBy == "" { + return ErrEmptyUpdatedBy + } + now := time.Now() + d.deletedAt = &now + d.deletedBy = &deletedBy + d.isActive = false + return nil +} + +// Activate sets the detail active. Callers must guarantee no other active detail +// holds the same item_code (enforced at DB via partial unique index). +func (d *Detail) Activate(updatedBy string) error { + if d.IsDeleted() { + return ErrAlreadyDeleted + } + if updatedBy == "" { + return ErrEmptyUpdatedBy + } + d.isActive = true + now := time.Now() + d.updatedAt = &now + d.updatedBy = &updatedBy + return nil +} + +// Deactivate excludes the detail from rate aggregation while keeping audit history. +func (d *Detail) Deactivate(updatedBy string) error { + if d.IsDeleted() { + return ErrAlreadyDeleted + } + if updatedBy == "" { + return ErrEmptyUpdatedBy + } + d.isActive = false + now := time.Now() + d.updatedAt = &now + d.updatedBy = &updatedBy + return nil +} diff --git a/services/finance/internal/domain/rmgroup/entity_test.go b/services/finance/internal/domain/rmgroup/entity_test.go new file mode 100644 index 0000000..9825178 --- /dev/null +++ b/services/finance/internal/domain/rmgroup/entity_test.go @@ -0,0 +1,543 @@ +package rmgroup_test + +import ( + "errors" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// ---------------------------------------------------------------------------- +// Value object tests +// ---------------------------------------------------------------------------- + +func TestNewCode(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want string + wantErr error + }{ + {"valid alpha", "CHM0000118", "CHM0000118", nil}, + {"valid with space", "BLUE MGTS-5109", "BLUE MGTS-5109", nil}, + {"valid with hyphen", "PIG0000005-COM", "PIG0000005-COM", nil}, + {"lowercase normalized", "pig0000005-com", "PIG0000005-COM", nil}, + {"trimmed", " CHM0000118 ", "CHM0000118", nil}, + {"empty", "", "", rmgroup.ErrEmptyCode}, + {"whitespace only", " ", "", rmgroup.ErrEmptyCode}, + {"too long", strings.Repeat("A", 31), "", rmgroup.ErrCodeTooLong}, + {"starts with hyphen", "-ABC", "", rmgroup.ErrInvalidCodeFormat}, + {"invalid char underscore", "ABC_123", "", rmgroup.ErrInvalidCodeFormat}, + {"invalid char dot", "ABC.123", "", rmgroup.ErrInvalidCodeFormat}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := rmgroup.NewCode(tt.input) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got.String()) + }) + } +} + +func TestCodeEqualAndIsEmpty(t *testing.T) { + t.Parallel() + a, _ := rmgroup.NewCode("CHM0000118") + b, _ := rmgroup.NewCode("chm0000118") + c, _ := rmgroup.NewCode("OTHER") + + assert.True(t, a.Equal(b)) + assert.False(t, a.Equal(c)) + assert.False(t, a.IsEmpty()) + assert.True(t, (rmgroup.Code{}).IsEmpty()) +} + +func TestNewItemCode(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want string + wantErr error + }{ + {"valid", "ITEM001", "ITEM001", nil}, + {"trimmed", " ITEM001 ", "ITEM001", nil}, + {"empty", "", "", rmgroup.ErrEmptyItemCode}, + {"too long", strings.Repeat("A", 21), "", rmgroup.ErrItemCodeTooLong}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := rmgroup.NewItemCode(tt.input) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got.String()) + }) + } +} + +func TestParseFlag(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want rmgroup.Flag + wantErr error + }{ + {"CONS", "CONS", rmgroup.FlagCons, nil}, + {"stores lowercase", "stores", rmgroup.FlagStores, nil}, + {"DEPT", "DEPT", rmgroup.FlagDept, nil}, + {"PO_1", "PO_1", rmgroup.FlagPO1, nil}, + {"PO_2", "PO_2", rmgroup.FlagPO2, nil}, + {"PO_3", "PO_3", rmgroup.FlagPO3, nil}, + {"INIT trimmed", " INIT ", rmgroup.FlagInit, nil}, + {"unknown", "XYZ", "", rmgroup.ErrInvalidFlag}, + {"empty", "", "", rmgroup.ErrInvalidFlag}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := rmgroup.ParseFlag(tt.input) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestFlagPredicates(t *testing.T) { + t.Parallel() + assert.True(t, rmgroup.FlagCons.IsValid()) + assert.True(t, rmgroup.FlagInit.IsValid()) + assert.False(t, rmgroup.Flag("BOGUS").IsValid()) + assert.True(t, rmgroup.FlagInit.IsInit()) + assert.False(t, rmgroup.FlagCons.IsInit()) + assert.Equal(t, "CONS", rmgroup.FlagCons.String()) +} + +// ---------------------------------------------------------------------------- +// Head — constructor tests +// ---------------------------------------------------------------------------- + +func mustCode(t *testing.T, raw string) rmgroup.Code { + t.Helper() + c, err := rmgroup.NewCode(raw) + require.NoError(t, err) + return c +} + +func TestNewHead(t *testing.T) { + t.Parallel() + + validCode := mustCode(t, "CHM0000118") + + tests := []struct { + name string + code rmgroup.Code + groupName string + costPercentage float64 + costPerKg float64 + createdBy string + wantErr error + }{ + {"valid", validCode, "Pigment Group A", 10, 100, "admin", nil}, + {"empty code", rmgroup.Code{}, "Name", 0, 0, "admin", rmgroup.ErrEmptyCode}, + {"empty name", validCode, "", 0, 0, "admin", rmgroup.ErrEmptyName}, + {"name too long", validCode, strings.Repeat("n", 201), 0, 0, "admin", rmgroup.ErrNameTooLong}, + {"empty createdBy", validCode, "Name", 0, 0, "", rmgroup.ErrEmptyCreatedBy}, + {"negative cost percentage", validCode, "Name", -1, 0, "admin", rmgroup.ErrNegativeCostPercentage}, + {"negative cost per kg", validCode, "Name", 0, -1, "admin", rmgroup.ErrNegativeCostPerKg}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := rmgroup.NewHead(tt.code, tt.groupName, "", tt.costPercentage, tt.costPerKg, tt.createdBy) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, tt.code.String(), got.Code().String()) + assert.Equal(t, tt.groupName, got.Name()) + assert.Equal(t, tt.costPercentage, got.CostPercentage()) + assert.Equal(t, tt.costPerKg, got.CostPerKg()) + assert.Equal(t, rmgroup.FlagCons, got.FlagValuation()) + assert.Equal(t, rmgroup.FlagCons, got.FlagMarketing()) + assert.Equal(t, rmgroup.FlagCons, got.FlagSimulation()) + assert.True(t, got.IsActive()) + assert.False(t, got.IsDeleted()) + assert.NotEqual(t, uuid.Nil, got.ID()) + }) + } +} + +// ---------------------------------------------------------------------------- +// Head — Update tests +// ---------------------------------------------------------------------------- + +func newTestHead(t *testing.T) *rmgroup.Head { + t.Helper() + h, err := rmgroup.NewHead(mustCode(t, "CHM0000118"), "Name", "", 10, 100, "admin") + require.NoError(t, err) + return h +} + +func TestHead_Update_FieldChanges(t *testing.T) { + t.Parallel() + + newName := "Updated" + newDescription := "desc" + newColorant := "red" + newCI := "ci" + newCostPct := 20.0 + newCostPerKg := 200.0 + inactive := false + + h := newTestHead(t) + err := h.Update(rmgroup.UpdateInput{ + Name: &newName, + Description: &newDescription, + Colorant: &newColorant, + CIName: &newCI, + CostPercentage: &newCostPct, + CostPerKg: &newCostPerKg, + IsActive: &inactive, + }, "editor") + require.NoError(t, err) + + assert.Equal(t, newName, h.Name()) + assert.Equal(t, newDescription, h.Description()) + assert.Equal(t, newColorant, h.Colorant()) + assert.Equal(t, newCI, h.CIName()) + assert.Equal(t, newCostPct, h.CostPercentage()) + assert.Equal(t, newCostPerKg, h.CostPerKg()) + assert.False(t, h.IsActive()) + require.NotNil(t, h.UpdatedBy()) + assert.Equal(t, "editor", *h.UpdatedBy()) + require.NotNil(t, h.UpdatedAt()) +} + +func TestHead_Update_Validations(t *testing.T) { + t.Parallel() + + empty := "" + tooLong := strings.Repeat("x", 201) + negativePct := -1.0 + negativePerKg := -1.0 + + tests := []struct { + name string + input rmgroup.UpdateInput + wantErr error + }{ + {"empty name", rmgroup.UpdateInput{Name: &empty}, rmgroup.ErrEmptyName}, + {"name too long", rmgroup.UpdateInput{Name: &tooLong}, rmgroup.ErrNameTooLong}, + {"negative cost percentage", rmgroup.UpdateInput{CostPercentage: &negativePct}, rmgroup.ErrNegativeCostPercentage}, + {"negative cost per kg", rmgroup.UpdateInput{CostPerKg: &negativePerKg}, rmgroup.ErrNegativeCostPerKg}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + h := newTestHead(t) + err := h.Update(tt.input, "editor") + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestHead_Update_EmptyUpdatedBy(t *testing.T) { + t.Parallel() + h := newTestHead(t) + err := h.Update(rmgroup.UpdateInput{}, "") + require.ErrorIs(t, err, rmgroup.ErrEmptyUpdatedBy) +} + +func TestHead_Update_AfterDelete(t *testing.T) { + t.Parallel() + h := newTestHead(t) + require.NoError(t, h.SoftDelete("admin")) + err := h.Update(rmgroup.UpdateInput{}, "admin") + require.ErrorIs(t, err, rmgroup.ErrAlreadyDeleted) +} + +func TestHead_Update_FlagInitRequiresInitVal(t *testing.T) { + t.Parallel() + + initFlag := rmgroup.FlagInit + h := newTestHead(t) + + // INIT without a init_val → error. + err := h.Update(rmgroup.UpdateInput{FlagValuation: &initFlag}, "editor") + require.ErrorIs(t, err, rmgroup.ErrInitValueRequired) + + // INIT with init_val → ok. + val := 42.5 + err = h.Update(rmgroup.UpdateInput{ + FlagValuation: &initFlag, + InitValValuation: &val, + }, "editor") + require.NoError(t, err) + require.NotNil(t, h.InitValValuation()) + assert.Equal(t, val, *h.InitValValuation()) + + // Clearing init_val while flag still INIT → error. + err = h.Update(rmgroup.UpdateInput{ClearInitValValuation: true}, "editor") + require.ErrorIs(t, err, rmgroup.ErrInitValueRequired) +} + +func TestHead_Update_InvalidFlag(t *testing.T) { + t.Parallel() + h := newTestHead(t) + bogus := rmgroup.Flag("BOGUS") + err := h.Update(rmgroup.UpdateInput{FlagValuation: &bogus}, "editor") + require.ErrorIs(t, err, rmgroup.ErrInvalidFlag) +} + +func TestHead_Update_NegativeInitValue(t *testing.T) { + t.Parallel() + h := newTestHead(t) + neg := -0.01 + err := h.Update(rmgroup.UpdateInput{InitValMarketing: &neg}, "editor") + require.ErrorIs(t, err, rmgroup.ErrNegativeInitValue) +} + +func TestHead_Update_ClearInitVal(t *testing.T) { + t.Parallel() + h := newTestHead(t) + val := 10.0 + require.NoError(t, h.Update(rmgroup.UpdateInput{InitValMarketing: &val}, "editor")) + require.NotNil(t, h.InitValMarketing()) + + require.NoError(t, h.Update(rmgroup.UpdateInput{ClearInitValMarketing: true}, "editor")) + assert.Nil(t, h.InitValMarketing()) +} + +func TestHead_SoftDelete(t *testing.T) { + t.Parallel() + + h := newTestHead(t) + require.NoError(t, h.SoftDelete("admin")) + assert.True(t, h.IsDeleted()) + assert.False(t, h.IsActive()) + require.NotNil(t, h.DeletedBy()) + assert.Equal(t, "admin", *h.DeletedBy()) + + // Idempotent — second call errors. + err := h.SoftDelete("admin") + require.ErrorIs(t, err, rmgroup.ErrAlreadyDeleted) +} + +func TestHead_SoftDelete_EmptyBy(t *testing.T) { + t.Parallel() + h := newTestHead(t) + err := h.SoftDelete("") + require.ErrorIs(t, err, rmgroup.ErrEmptyUpdatedBy) +} + +// ---------------------------------------------------------------------------- +// Detail tests +// ---------------------------------------------------------------------------- + +func mustItemCode(t *testing.T, raw string) rmgroup.ItemCode { + t.Helper() + ic, err := rmgroup.NewItemCode(raw) + require.NoError(t, err) + return ic +} + +func TestNewDetail(t *testing.T) { + t.Parallel() + + headID := uuid.New() + ic := mustItemCode(t, "ITEM001") + + d, err := rmgroup.NewDetail(headID, ic, "admin") + require.NoError(t, err) + assert.Equal(t, headID, d.HeadID()) + assert.Equal(t, "ITEM001", d.ItemCode().String()) + assert.True(t, d.IsActive()) + assert.False(t, d.IsDummy()) + assert.False(t, d.IsDeleted()) + assert.NotEqual(t, uuid.Nil, d.ID()) +} + +func TestNewDetail_Validations(t *testing.T) { + t.Parallel() + + headID := uuid.New() + ic := mustItemCode(t, "ITEM001") + + _, err := rmgroup.NewDetail(headID, rmgroup.ItemCode{}, "admin") + require.ErrorIs(t, err, rmgroup.ErrEmptyItemCode) + + _, err = rmgroup.NewDetail(headID, ic, "") + require.ErrorIs(t, err, rmgroup.ErrEmptyCreatedBy) +} + +func newTestDetail(t *testing.T) *rmgroup.Detail { + t.Helper() + d, err := rmgroup.NewDetail(uuid.New(), mustItemCode(t, "ITEM001"), "admin") + require.NoError(t, err) + return d +} + +func TestDetail_Update_Fields(t *testing.T) { + t.Parallel() + + itemName := "Item Name" + itemType := "TYPE" + grade := "G1" + itemGrade := "Grade 1" + uom := "KG" + mktPct := 12.5 + mktVal := 100.0 + sortOrder := int32(5) + active := false + dummy := true + + d := newTestDetail(t) + err := d.Update(rmgroup.DetailUpdateInput{ + ItemName: &itemName, + ItemTypeCode: &itemType, + GradeCode: &grade, + ItemGrade: &itemGrade, + UOMCode: &uom, + MarketPercentage: &mktPct, + MarketValueRp: &mktVal, + SortOrder: &sortOrder, + IsActive: &active, + IsDummy: &dummy, + }, "editor") + require.NoError(t, err) + + assert.Equal(t, itemName, d.ItemName()) + assert.Equal(t, itemType, d.ItemTypeCode()) + assert.Equal(t, grade, d.GradeCode()) + assert.Equal(t, itemGrade, d.ItemGrade()) + assert.Equal(t, uom, d.UOMCode()) + require.NotNil(t, d.MarketPercentage()) + assert.Equal(t, mktPct, *d.MarketPercentage()) + require.NotNil(t, d.MarketValueRp()) + assert.Equal(t, mktVal, *d.MarketValueRp()) + assert.Equal(t, sortOrder, d.SortOrder()) + assert.False(t, d.IsActive()) + assert.True(t, d.IsDummy()) +} + +func TestDetail_Update_NegativeMarketValues(t *testing.T) { + t.Parallel() + + neg := -1.0 + + d := newTestDetail(t) + err := d.Update(rmgroup.DetailUpdateInput{MarketPercentage: &neg}, "editor") + require.ErrorIs(t, err, rmgroup.ErrNegativeMarketPercentage) + + err = d.Update(rmgroup.DetailUpdateInput{MarketValueRp: &neg}, "editor") + require.ErrorIs(t, err, rmgroup.ErrNegativeMarketValue) +} + +func TestDetail_Update_ClearMarketFields(t *testing.T) { + t.Parallel() + + pct := 10.0 + val := 100.0 + + d := newTestDetail(t) + require.NoError(t, d.Update(rmgroup.DetailUpdateInput{ + MarketPercentage: &pct, + MarketValueRp: &val, + }, "editor")) + require.NotNil(t, d.MarketPercentage()) + require.NotNil(t, d.MarketValueRp()) + + require.NoError(t, d.Update(rmgroup.DetailUpdateInput{ + ClearMarketPercentage: true, + ClearMarketValueRp: true, + }, "editor")) + assert.Nil(t, d.MarketPercentage()) + assert.Nil(t, d.MarketValueRp()) +} + +func TestDetail_Update_AfterDelete(t *testing.T) { + t.Parallel() + d := newTestDetail(t) + require.NoError(t, d.SoftDelete("admin")) + err := d.Update(rmgroup.DetailUpdateInput{}, "admin") + require.ErrorIs(t, err, rmgroup.ErrAlreadyDeleted) +} + +func TestDetail_SoftDelete(t *testing.T) { + t.Parallel() + + d := newTestDetail(t) + require.NoError(t, d.SoftDelete("admin")) + assert.True(t, d.IsDeleted()) + assert.False(t, d.IsActive()) + + err := d.SoftDelete("admin") + require.ErrorIs(t, err, rmgroup.ErrAlreadyDeleted) +} + +func TestDetail_ActivateDeactivate(t *testing.T) { + t.Parallel() + + d := newTestDetail(t) + require.NoError(t, d.Deactivate("editor")) + assert.False(t, d.IsActive()) + + require.NoError(t, d.Activate("editor")) + assert.True(t, d.IsActive()) + + require.NoError(t, d.SoftDelete("admin")) + err := d.Activate("editor") + require.True(t, errors.Is(err, rmgroup.ErrAlreadyDeleted)) + err = d.Deactivate("editor") + require.True(t, errors.Is(err, rmgroup.ErrAlreadyDeleted)) +} + +// ---------------------------------------------------------------------------- +// ListFilter tests +// ---------------------------------------------------------------------------- + +func TestListFilter_Validate(t *testing.T) { + t.Parallel() + + f := rmgroup.ListFilter{Page: 0, PageSize: 0} + f.Validate() + assert.Equal(t, 1, f.Page) + assert.Equal(t, 10, f.PageSize) + assert.Equal(t, "code", f.SortBy) + assert.Equal(t, "asc", f.SortOrder) + + f = rmgroup.ListFilter{PageSize: 500} + f.Validate() + assert.Equal(t, 100, f.PageSize) + + f = rmgroup.ListFilter{Page: 3, PageSize: 20} + f.Validate() + assert.Equal(t, 40, f.Offset()) +} diff --git a/services/finance/internal/domain/rmgroup/errors.go b/services/finance/internal/domain/rmgroup/errors.go new file mode 100644 index 0000000..c0429c9 --- /dev/null +++ b/services/finance/internal/domain/rmgroup/errors.go @@ -0,0 +1,76 @@ +// Package rmgroup provides domain logic for raw-material grouping and landed-cost configuration. +package rmgroup + +import "errors" + +// Domain errors for RM group operations. +var ( + // ErrNotFound is returned when an RM group head is not found. + ErrNotFound = errors.New("rm group not found") + + // ErrCodeAlreadyExists is returned when a group is created with a code that is already in use. + ErrCodeAlreadyExists = errors.New("rm group code already exists") + + // ErrAlreadyDeleted is returned when attempting to modify a soft-deleted group. + ErrAlreadyDeleted = errors.New("rm group is already deleted") + + // ErrEmptyCode is returned when the group code is empty. + ErrEmptyCode = errors.New("rm group code cannot be empty") + + // ErrInvalidCodeFormat is returned when the group code does not match the allowed pattern. + ErrInvalidCodeFormat = errors.New("rm group code must start with an uppercase letter or digit and may only contain uppercase letters, digits, spaces, and hyphens") + + // ErrCodeTooLong is returned when the group code exceeds the max length. + ErrCodeTooLong = errors.New("rm group code must be at most 30 characters") + + // ErrEmptyName is returned when the group name is empty. + ErrEmptyName = errors.New("rm group name cannot be empty") + + // ErrNameTooLong is returned when the group name exceeds the max length. + ErrNameTooLong = errors.New("rm group name must be at most 200 characters") + + // ErrEmptyCreatedBy is returned when created_by is empty. + ErrEmptyCreatedBy = errors.New("created_by cannot be empty") + + // ErrEmptyUpdatedBy is returned when updated_by is empty. + ErrEmptyUpdatedBy = errors.New("updated_by cannot be empty") + + // ErrInvalidFlag is returned when a stage flag is not one of the allowed values. + ErrInvalidFlag = errors.New("invalid stage flag — must be one of CONS, STORES, DEPT, PO_1, PO_2, PO_3, INIT") + + // ErrInitValueRequired is returned when a flag is set to INIT without a corresponding init_val. + ErrInitValueRequired = errors.New("init value must be provided when the flag is INIT") + + // ErrNegativeCostPercentage is returned when cost_percentage is negative. + ErrNegativeCostPercentage = errors.New("cost percentage must be non-negative") + + // ErrNegativeCostPerKg is returned when cost_per_kg is negative. + ErrNegativeCostPerKg = errors.New("cost per kg must be non-negative") + + // ErrNegativeInitValue is returned when an init value is negative. + ErrNegativeInitValue = errors.New("init value must be non-negative") + + // ErrEmptyItemCode is returned when an item code is empty. + ErrEmptyItemCode = errors.New("item code cannot be empty") + + // ErrItemCodeTooLong is returned when an item code exceeds the max length. + ErrItemCodeTooLong = errors.New("item code must be at most 20 characters") + + // ErrItemAlreadyInOtherGroup is returned when adding an item that is already assigned + // to another active group. Callers should consult the repository to identify the owning group. + ErrItemAlreadyInOtherGroup = errors.New("item is already assigned to another active group") + + // ErrDetailNotFound is returned when a group detail (item membership) is not found. + ErrDetailNotFound = errors.New("rm group detail not found") + + // ErrNegativeMarketPercentage is returned when market_percentage is negative. + ErrNegativeMarketPercentage = errors.New("market percentage must be non-negative") + + // ErrNegativeMarketValue is returned when market_value_rp is negative. + ErrNegativeMarketValue = errors.New("market value must be non-negative") + + // ErrGroupHasCostData is returned when attempting to delete a group head + // that has already produced cost calculation rows. Deleting would orphan + // the historical cost audit trail, so the operation is blocked. + ErrGroupHasCostData = errors.New("rm group cannot be deleted: cost data has already been generated for this group") +) diff --git a/services/finance/internal/domain/rmgroup/repository.go b/services/finance/internal/domain/rmgroup/repository.go new file mode 100644 index 0000000..2981083 --- /dev/null +++ b/services/finance/internal/domain/rmgroup/repository.go @@ -0,0 +1,129 @@ +// Package rmgroup provides domain logic for raw-material grouping and landed-cost configuration. +package rmgroup + +import ( + "context" + + "github.com/google/uuid" +) + +// Repository defines the persistence contract for RM group heads and details. +// The interface lives in the domain layer; the implementation sits in infrastructure. +type Repository interface { + // ---------- Head operations ---------- + + // CreateHead persists a new Head row. + CreateHead(ctx context.Context, head *Head) error + + // GetHeadByID retrieves a head by ID. Returns ErrNotFound when absent. + GetHeadByID(ctx context.Context, id uuid.UUID) (*Head, error) + + // GetHeadByCode retrieves a head by its unique code. Returns ErrNotFound when absent. + GetHeadByCode(ctx context.Context, code Code) (*Head, error) + + // ListHeads returns a page of heads plus the total count of matching rows. + ListHeads(ctx context.Context, filter ListFilter) ([]*Head, int64, error) + + // ListAllHeads returns every non-deleted head matching the active filter. + // Used by export — no pagination. activeFilter nil = all. + ListAllHeads(ctx context.Context, activeFilter *bool) ([]*Head, error) + + // UpdateHead persists changes to an existing head. + UpdateHead(ctx context.Context, head *Head) error + + // SoftDeleteHead marks the head and all of its active details as deleted. + SoftDeleteHead(ctx context.Context, id uuid.UUID, deletedBy string) error + + // ExistsHeadByCode reports whether a non-deleted head with this code exists. + ExistsHeadByCode(ctx context.Context, code Code) (bool, error) + + // ExistsHeadByID reports whether a non-deleted head with this ID exists. + ExistsHeadByID(ctx context.Context, id uuid.UUID) (bool, error) + + // ---------- Detail operations ---------- + + // AddDetail persists a new Detail row. + AddDetail(ctx context.Context, detail *Detail) error + + // UpdateDetail persists changes to an existing detail. + UpdateDetail(ctx context.Context, detail *Detail) error + + // GetDetailByID retrieves a detail by ID. + GetDetailByID(ctx context.Context, id uuid.UUID) (*Detail, error) + + // GetActiveDetailByItemCodeGrade looks up the active, non-deleted detail + // owning the given (item_code, grade_code) pair across ALL groups. Used to + // enforce the "one (item, grade) variant, one active group" invariant. + // The Oracle sync feed keys items on (item_code, grade_code), so treating + // the grade as part of the natural key keeps multi-variant items (same + // item_code with different grades) independently groupable. + // gradeCode may be empty — matches rows with NULL / empty grade_code. + // Returns ErrDetailNotFound when the variant is not currently assigned. + GetActiveDetailByItemCodeGrade(ctx context.Context, itemCode ItemCode, gradeCode string) (*Detail, error) + + // ListDetailsByHeadID returns every detail that belongs to the given head, + // including soft-deleted rows (callers filter as needed). Ordered by sort_order. + ListDetailsByHeadID(ctx context.Context, headID uuid.UUID) ([]*Detail, error) + + // ListActiveDetailsByHeadID returns only the active, non-deleted details used by + // the landed-cost engine for rate aggregation. + ListActiveDetailsByHeadID(ctx context.Context, headID uuid.UUID) ([]*Detail, error) + + // SoftDeleteDetail marks a single detail row as deleted. + SoftDeleteDetail(ctx context.Context, id uuid.UUID, deletedBy string) error +} + +// ListFilter describes pagination, search, and sort options for ListHeads. +type ListFilter struct { + // Search matches against code, name, description, colorant, ci_name. + Search string + + // IsActive filters by the is_active flag when non-nil. + IsActive *bool + + // Flag filter — when non-empty, matches heads where ANY of the three flag_* + // columns equals this value. Empty disables the filter. + Flag Flag + + Page int + PageSize int + + // SortBy accepts "code", "name", "created_at", "updated_at". + SortBy string + // SortOrder accepts "asc" or "desc". + SortOrder string +} + +// NewListFilter returns a ListFilter with sensible defaults. +func NewListFilter() ListFilter { + return ListFilter{ + Page: 1, + PageSize: 10, + SortBy: "code", + SortOrder: "asc", + } +} + +// Validate normalizes the filter so callers can pass partially-populated structs. +func (f *ListFilter) Validate() { + if f.Page < 1 { + f.Page = 1 + } + if f.PageSize < 1 { + f.PageSize = 10 + } + if f.PageSize > 100 { + f.PageSize = 100 + } + if f.SortBy == "" { + f.SortBy = "code" + } + if f.SortOrder == "" { + f.SortOrder = "asc" + } +} + +// Offset returns the SQL OFFSET corresponding to Page/PageSize. +func (f *ListFilter) Offset() int { + return (f.Page - 1) * f.PageSize +} diff --git a/services/finance/internal/domain/rmgroup/value_objects.go b/services/finance/internal/domain/rmgroup/value_objects.go new file mode 100644 index 0000000..cea18e0 --- /dev/null +++ b/services/finance/internal/domain/rmgroup/value_objects.go @@ -0,0 +1,126 @@ +// Package rmgroup provides domain logic for raw-material grouping and landed-cost configuration. +package rmgroup + +import ( + "regexp" + "strings" +) + +// codePattern validates the group code format — uppercase alphanumeric with optional +// spaces and hyphens, starting with alphanumeric. Max 30 characters. +// Mirrors the chk_rm_group_code_format CHECK constraint on cst_rm_group_head. +var codePattern = regexp.MustCompile(`^[A-Z0-9][A-Z0-9 \-]{0,29}$`) + +// Code represents a validated RM group code (e.g., "BLUE MGTS-5109", "PIG0000005-COM"). +type Code struct { + value string +} + +// NewCode creates a Code value object with validation. +func NewCode(raw string) (Code, error) { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return Code{}, ErrEmptyCode + } + if len(trimmed) > 30 { + return Code{}, ErrCodeTooLong + } + // Normalize letters to uppercase; preserve spaces and hyphens. + normalized := strings.ToUpper(trimmed) + if !codePattern.MatchString(normalized) { + return Code{}, ErrInvalidCodeFormat + } + return Code{value: normalized}, nil +} + +// String returns the canonical string form. +func (c Code) String() string { return c.value } + +// IsEmpty returns true if the code has no value. +func (c Code) IsEmpty() bool { return c.value == "" } + +// Equal reports whether two codes are equal. +func (c Code) Equal(other Code) bool { return c.value == other.value } + +// ItemCode represents a validated raw-material item code (mirrors cst_item_cons_stk_po.item_code). +type ItemCode struct { + value string +} + +// NewItemCode creates an ItemCode value object with validation. +func NewItemCode(raw string) (ItemCode, error) { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return ItemCode{}, ErrEmptyItemCode + } + if len(trimmed) > 20 { + return ItemCode{}, ErrItemCodeTooLong + } + return ItemCode{value: trimmed}, nil +} + +// String returns the item code. +func (i ItemCode) String() string { return i.value } + +// IsEmpty returns true if the item code is empty. +func (i ItemCode) IsEmpty() bool { return i.value == "" } + +// Flag represents the stage selector used to pick which rate feeds the landed cost formula. +// Mirrors the CHECK-constrained values on cst_rm_group_head.flag_*. +type Flag string + +// Flag constants — MUST match DB CHECK constraint values exactly. +const ( + // FlagCons selects the consumption-aggregated rate. + FlagCons Flag = "CONS" + // FlagStores selects the stores-aggregated rate. + FlagStores Flag = "STORES" + // FlagDept selects the department-aggregated rate. + FlagDept Flag = "DEPT" + // FlagPO1 selects purchase-order slot 1 rate. + FlagPO1 Flag = "PO_1" + // FlagPO2 selects purchase-order slot 2 rate. + FlagPO2 Flag = "PO_2" + // FlagPO3 selects purchase-order slot 3 rate. + FlagPO3 Flag = "PO_3" + // FlagInit signals that the init_val override should be used instead of any aggregated rate. + FlagInit Flag = "INIT" +) + +// ParseFlag validates and returns a Flag, or ErrInvalidFlag if the value is unknown. +func ParseFlag(raw string) (Flag, error) { + switch Flag(strings.ToUpper(strings.TrimSpace(raw))) { + case FlagCons: + return FlagCons, nil + case FlagStores: + return FlagStores, nil + case FlagDept: + return FlagDept, nil + case FlagPO1: + return FlagPO1, nil + case FlagPO2: + return FlagPO2, nil + case FlagPO3: + return FlagPO3, nil + case FlagInit: + return FlagInit, nil + default: + return "", ErrInvalidFlag + } +} + +// IsValid reports whether the flag is one of the recognized values. +func (f Flag) IsValid() bool { + switch f { + case FlagCons, FlagStores, FlagDept, FlagPO1, FlagPO2, FlagPO3, FlagInit: + return true + default: + return false + } +} + +// IsInit reports whether the flag requests the init_val override. +func (f Flag) IsInit() bool { return f == FlagInit } + +// String returns the canonical string form. +func (f Flag) String() string { return string(f) } diff --git a/services/finance/internal/infrastructure/postgres/close_rows.go b/services/finance/internal/infrastructure/postgres/close_rows.go new file mode 100644 index 0000000..b4435a7 --- /dev/null +++ b/services/finance/internal/infrastructure/postgres/close_rows.go @@ -0,0 +1,15 @@ +package postgres + +import ( + "database/sql" + + "github.com/rs/zerolog/log" +) + +// closeRows is a deferred helper that closes sql.Rows and logs any error +// instead of silently discarding it (satisfies errcheck linter). +func closeRows(rows *sql.Rows) { + if err := rows.Close(); err != nil { + log.Warn().Err(err).Msg("failed to close rows") + } +} diff --git a/services/finance/internal/infrastructure/postgres/rmcost_repository.go b/services/finance/internal/infrastructure/postgres/rmcost_repository.go new file mode 100644 index 0000000..9e00648 --- /dev/null +++ b/services/finance/internal/infrastructure/postgres/rmcost_repository.go @@ -0,0 +1,550 @@ +// Package postgres provides PostgreSQL implementations for domain repositories. +package postgres + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" +) + +// RMCostRepository implements rmcost.Repository using PostgreSQL. +type RMCostRepository struct { + db *DB +} + +// NewRMCostRepository creates a new RMCostRepository instance. +func NewRMCostRepository(db *DB) *RMCostRepository { + return &RMCostRepository{db: db} +} + +// Verify interface implementation at compile time. +var _ rmcost.Repository = (*RMCostRepository)(nil) + +// Upsert writes the Cost row keyed on (period, rm_code) and appends one History +// row within the same transaction. On INSERT the row is created with its +// calculated_* and created_* fields; on UPDATE the mutable snapshot columns are +// overwritten and updated_* is set. +func (r *RMCostRepository) Upsert(ctx context.Context, cost *rmcost.Cost, hist rmcost.History) error { + return r.db.Transaction(ctx, func(tx *sql.Tx) error { + if err := upsertCost(ctx, tx, cost); err != nil { + return err + } + return insertHistory(ctx, tx, hist) + }) +} + +func upsertCost(ctx context.Context, tx *sql.Tx, c *rmcost.Cost) error { + rates := c.Rates() + query := ` + INSERT INTO cst_rm_cost ( + rm_cost_id, period, rm_code, rm_type, group_head_id, item_code, rm_name, uom_code, + cons_rate, stores_rate, dept_rate, po_rate_1, po_rate_2, po_rate_3, + cost_val, cost_mark, cost_sim, + flag_valuation, flag_marketing, flag_simulation, + flag_valuation_used, flag_marketing_used, flag_simulation_used, + calculated_at, calculated_by, created_at, created_by + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27) + ON CONFLICT (period, rm_code) DO UPDATE SET + group_head_id = EXCLUDED.group_head_id, + item_code = EXCLUDED.item_code, + rm_name = EXCLUDED.rm_name, + uom_code = EXCLUDED.uom_code, + cons_rate = EXCLUDED.cons_rate, + stores_rate = EXCLUDED.stores_rate, + dept_rate = EXCLUDED.dept_rate, + po_rate_1 = EXCLUDED.po_rate_1, + po_rate_2 = EXCLUDED.po_rate_2, + po_rate_3 = EXCLUDED.po_rate_3, + cost_val = EXCLUDED.cost_val, + cost_mark = EXCLUDED.cost_mark, + cost_sim = EXCLUDED.cost_sim, + flag_valuation = EXCLUDED.flag_valuation, + flag_marketing = EXCLUDED.flag_marketing, + flag_simulation = EXCLUDED.flag_simulation, + flag_valuation_used = EXCLUDED.flag_valuation_used, + flag_marketing_used = EXCLUDED.flag_marketing_used, + flag_simulation_used = EXCLUDED.flag_simulation_used, + calculated_at = EXCLUDED.calculated_at, + calculated_by = EXCLUDED.calculated_by, + updated_at = EXCLUDED.calculated_at, + updated_by = EXCLUDED.calculated_by + ` + _, err := tx.ExecContext(ctx, query, + c.ID(), c.Period(), c.RMCode(), c.RMType().String(), + c.GroupHeadID(), c.ItemCode(), nullableString(c.RMName()), nullableString(c.UOMCode()), + rates.Cons, rates.Stores, rates.Dept, rates.PO1, rates.PO2, rates.PO3, + c.CostValuation(), c.CostMarketing(), c.CostSimulation(), + c.FlagValuation().String(), c.FlagMarketing().String(), c.FlagSimulation().String(), + c.FlagValuationUsed().String(), c.FlagMarketingUsed().String(), c.FlagSimulationUsed().String(), + c.CalculatedAt(), c.CalculatedBy(), c.CreatedAt(), c.CreatedBy(), + ) + if err != nil { + return fmt.Errorf("upsert rm_cost: %w", err) + } + return nil +} + +func insertHistory(ctx context.Context, tx *sql.Tx, h rmcost.History) error { + query := ` + INSERT INTO aud_rm_cost_history ( + history_id, rm_cost_id, job_id, period, rm_code, rm_type, group_head_id, + cons_rate, stores_rate, dept_rate, po_rate_1, po_rate_2, po_rate_3, + cost_percentage, cost_per_kg, + flag_valuation, flag_marketing, flag_simulation, + init_val_valuation, init_val_marketing, init_val_simulation, + cost_val, cost_mark, cost_sim, + flag_valuation_used, flag_marketing_used, flag_simulation_used, + source_item_count, trigger_reason, calculated_at, calculated_by + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31) + ` + _, err := tx.ExecContext(ctx, query, + h.ID, h.RMCostID, h.JobID, h.Period, h.RMCode, h.RMType.String(), h.GroupHeadID, + h.Rates.Cons, h.Rates.Stores, h.Rates.Dept, h.Rates.PO1, h.Rates.PO2, h.Rates.PO3, + h.CostPercentage, h.CostPerKg, + h.FlagValuation.String(), h.FlagMarketing.String(), h.FlagSimulation.String(), + h.InitValValuation, h.InitValMarketing, h.InitValSimulation, + h.CostValuation, h.CostMarketing, h.CostSimulation, + h.FlagValuationUsed.String(), h.FlagMarketingUsed.String(), h.FlagSimulationUsed.String(), + h.SourceItemCount, string(h.TriggerReason), h.CalculatedAt, h.CalculatedBy, + ) + if err != nil { + return fmt.Errorf("insert rm_cost history: %w", err) + } + return nil +} + +// GetByID retrieves a Cost by its primary key. +func (r *RMCostRepository) GetByID(ctx context.Context, id uuid.UUID) (*rmcost.Cost, error) { + return r.scanCost(r.db.QueryRowContext(ctx, costSelectSQL+` WHERE rm_cost_id = $1`, id)) +} + +// GetByPeriodAndCode retrieves a Cost by (period, rm_code). +func (r *RMCostRepository) GetByPeriodAndCode(ctx context.Context, period, rmCode string) (*rmcost.Cost, error) { + return r.scanCost(r.db.QueryRowContext(ctx, costSelectSQL+` WHERE period = $1 AND rm_code = $2`, period, rmCode)) +} + +// List returns a page of Cost rows plus total count. +func (r *RMCostRepository) List(ctx context.Context, filter rmcost.ListFilter) ([]*rmcost.Cost, int64, error) { + filter.Validate() + base, args := buildCostListWhere(filter) + + var total int64 + if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) `+base, args...).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("count rm_costs: %w", err) + } + + orderCol := map[string]string{ + "period": "period", + "rm_code": "rm_code", + "rm_name": "rm_name", + "calculated_at": "calculated_at", + }[filter.SortBy] + if orderCol == "" { + orderCol = "period" + } + orderDir := sortDESC + if filter.SortOrder == "asc" || filter.SortOrder == "ASC" { + orderDir = sortASC + } + + argIdx := len(args) + 1 + query := costSelectColumnsSQL + " " + base + + fmt.Sprintf(` ORDER BY %s %s, rm_code ASC LIMIT $%d OFFSET $%d`, orderCol, orderDir, argIdx, argIdx+1) + args = append(args, filter.PageSize, filter.Offset()) + + rows, err := r.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, 0, fmt.Errorf("list rm_costs: %w", err) + } + defer closeRows(rows) + + var out []*rmcost.Cost + for rows.Next() { + cost, err := r.scanCostRow(rows) + if err != nil { + return nil, 0, err + } + out = append(out, cost) + } + if err := rows.Err(); err != nil { + return nil, 0, fmt.Errorf("iterate rm_costs: %w", err) + } + return out, total, nil +} + +// ListAll returns every cost row matching the filter with no pagination, +// ordered by period DESC then rm_code ASC. Used by the Excel export path. +func (r *RMCostRepository) ListAll(ctx context.Context, filter rmcost.ExportFilter) ([]*rmcost.Cost, error) { + base, args := buildCostExportWhere(filter) + query := costSelectColumnsSQL + " " + base + ` ORDER BY period DESC, rm_code ASC` + rows, err := r.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("list all rm_costs: %w", err) + } + defer closeRows(rows) + + var out []*rmcost.Cost + for rows.Next() { + cost, err := r.scanCostRow(rows) + if err != nil { + return nil, err + } + out = append(out, cost) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate all rm_costs: %w", err) + } + return out, nil +} + +func buildCostExportWhere(f rmcost.ExportFilter) (string, []any) { + base := `FROM cst_rm_cost WHERE 1=1` + args := []any{} + idx := 1 + if f.Period != "" { + base += fmt.Sprintf(` AND period = $%d`, idx) + args = append(args, f.Period) + idx++ + } + if f.RMType != "" { + base += fmt.Sprintf(` AND rm_type = $%d`, idx) + args = append(args, f.RMType.String()) + idx++ + } + if f.GroupHeadID != nil { + base += fmt.Sprintf(` AND group_head_id = $%d`, idx) + args = append(args, *f.GroupHeadID) + idx++ + } + if f.Search != "" { + base += fmt.Sprintf(` AND (rm_code ILIKE $%d OR rm_name ILIKE $%d)`, idx, idx) + args = append(args, "%"+f.Search+"%") + } + return base, args +} + +func buildCostListWhere(f rmcost.ListFilter) (string, []any) { + base := `FROM cst_rm_cost WHERE 1=1` + args := []any{} + idx := 1 + if f.Period != "" { + base += fmt.Sprintf(` AND period = $%d`, idx) + args = append(args, f.Period) + idx++ + } + if f.RMType != "" { + base += fmt.Sprintf(` AND rm_type = $%d`, idx) + args = append(args, f.RMType.String()) + idx++ + } + if f.GroupHeadID != nil { + base += fmt.Sprintf(` AND group_head_id = $%d`, idx) + args = append(args, *f.GroupHeadID) + idx++ + } + if f.Search != "" { + base += fmt.Sprintf(` AND (rm_code ILIKE $%d OR rm_name ILIKE $%d)`, idx, idx) + args = append(args, "%"+f.Search+"%") + } + return base, args +} + +// ExistsForGroupHead reports whether any cst_rm_cost row currently references +// the given group head. Used to block deletion of group heads with cost data. +func (r *RMCostRepository) ExistsForGroupHead(ctx context.Context, groupHeadID uuid.UUID) (bool, error) { + var exists bool + err := r.db.QueryRowContext(ctx, + `SELECT EXISTS(SELECT 1 FROM cst_rm_cost WHERE group_head_id = $1)`, + groupHeadID, + ).Scan(&exists) + if err != nil { + return false, fmt.Errorf("check rm_cost exists for group head: %w", err) + } + return exists, nil +} + +// ListDistinctPeriods returns the distinct periods (YYYYMM) that have cost rows, +// ordered newest first. +func (r *RMCostRepository) ListDistinctPeriods(ctx context.Context) ([]string, error) { + rows, err := r.db.QueryContext(ctx, + `SELECT DISTINCT period FROM cst_rm_cost ORDER BY period DESC`, + ) + if err != nil { + return nil, fmt.Errorf("list distinct cost periods: %w", err) + } + defer closeRows(rows) + var out []string + for rows.Next() { + var p string + if scanErr := rows.Scan(&p); scanErr != nil { + return nil, fmt.Errorf("scan period: %w", scanErr) + } + out = append(out, p) + } + if rowsErr := rows.Err(); rowsErr != nil { + return nil, fmt.Errorf("iterate periods: %w", rowsErr) + } + return out, nil +} + +// ListHistory returns history rows matching the filter, newest first. +func (r *RMCostRepository) ListHistory(ctx context.Context, filter rmcost.HistoryFilter) ([]rmcost.History, int64, error) { + filter.Validate() + base := `FROM aud_rm_cost_history WHERE 1=1` + args := []any{} + idx := 1 + if filter.Period != "" { + base += fmt.Sprintf(` AND period = $%d`, idx) + args = append(args, filter.Period) + idx++ + } + if filter.RMCode != "" { + base += fmt.Sprintf(` AND rm_code = $%d`, idx) + args = append(args, filter.RMCode) + idx++ + } + if filter.GroupHeadID != nil { + base += fmt.Sprintf(` AND group_head_id = $%d`, idx) + args = append(args, *filter.GroupHeadID) + idx++ + } + if filter.JobID != nil { + base += fmt.Sprintf(` AND job_id = $%d`, idx) + args = append(args, *filter.JobID) + idx++ + } + + var total int64 + if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) `+base, args...).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("count rm_cost history: %w", err) + } + + query := historySelectColumnsSQL + " " + base + + fmt.Sprintf(` ORDER BY calculated_at DESC LIMIT $%d OFFSET $%d`, idx, idx+1) + args = append(args, filter.PageSize, filter.Offset()) + + rows, err := r.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, 0, fmt.Errorf("list rm_cost history: %w", err) + } + defer closeRows(rows) + + var out []rmcost.History + for rows.Next() { + h, err := scanHistoryRow(rows) + if err != nil { + return nil, 0, err + } + out = append(out, h) + } + if err := rows.Err(); err != nil { + return nil, 0, fmt.Errorf("iterate rm_cost history: %w", err) + } + return out, total, nil +} + +// ============================================================================= +// Cost scanning +// ============================================================================= + +const costSelectColumnsSQL = ` + SELECT rm_cost_id, period, rm_code, rm_type, group_head_id, item_code, rm_name, uom_code, + cons_rate, stores_rate, dept_rate, po_rate_1, po_rate_2, po_rate_3, + cost_val, cost_mark, cost_sim, + flag_valuation, flag_marketing, flag_simulation, + flag_valuation_used, flag_marketing_used, flag_simulation_used, + calculated_at, calculated_by, created_at, created_by, updated_at, updated_by` + +const costSelectSQL = costSelectColumnsSQL + ` FROM cst_rm_cost` + +type costDTO struct { + ID uuid.UUID + Period string + RMCode string + RMType string + GroupHeadID uuid.NullUUID + ItemCode sql.NullString + RMName sql.NullString + UOMCode sql.NullString + ConsRate sql.NullFloat64 + StoresRate sql.NullFloat64 + DeptRate sql.NullFloat64 + PO1Rate sql.NullFloat64 + PO2Rate sql.NullFloat64 + PO3Rate sql.NullFloat64 + CostVal sql.NullFloat64 + CostMark sql.NullFloat64 + CostSim sql.NullFloat64 + FlagValuation string + FlagMarketing string + FlagSimulation string + FlagValuationUsed string + FlagMarketingUsed string + FlagSimulationUsed string + CalculatedAt sql.NullTime + CalculatedBy sql.NullString + CreatedAt time.Time + CreatedBy string + UpdatedAt sql.NullTime + UpdatedBy sql.NullString +} + +func (d *costDTO) toEntity() *rmcost.Cost { + rates := rmcost.StageRates{ + Cons: nullFloatOrZero(d.ConsRate), + Stores: nullFloatOrZero(d.StoresRate), + Dept: nullFloatOrZero(d.DeptRate), + PO1: nullFloatOrZero(d.PO1Rate), + PO2: nullFloatOrZero(d.PO2Rate), + PO3: nullFloatOrZero(d.PO3Rate), + } + var groupID *uuid.UUID + if d.GroupHeadID.Valid { + id := d.GroupHeadID.UUID + groupID = &id + } + return rmcost.ReconstructCost( + d.ID, d.Period, d.RMCode, rmcost.RMType(d.RMType), groupID, + nullStringPtr(d.ItemCode), nullStringVal(d.RMName), nullStringVal(d.UOMCode), + rates, + nullFloatPtr(d.CostVal), nullFloatPtr(d.CostMark), nullFloatPtr(d.CostSim), + rmcost.Stage(d.FlagValuation), rmcost.Stage(d.FlagMarketing), rmcost.Stage(d.FlagSimulation), + rmcost.Stage(d.FlagValuationUsed), rmcost.Stage(d.FlagMarketingUsed), rmcost.Stage(d.FlagSimulationUsed), + nullTimePtr(d.CalculatedAt), nullStringPtr(d.CalculatedBy), + d.CreatedAt, d.CreatedBy, + nullTimePtr(d.UpdatedAt), nullStringPtr(d.UpdatedBy), + ) +} + +func (r *RMCostRepository) scanCost(row *sql.Row) (*rmcost.Cost, error) { + var d costDTO + err := scanCostInto(row.Scan, &d) + if errors.Is(err, sql.ErrNoRows) { + return nil, rmcost.ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("scan rm_cost: %w", err) + } + return d.toEntity(), nil +} + +func (r *RMCostRepository) scanCostRow(rows *sql.Rows) (*rmcost.Cost, error) { + var d costDTO + if err := scanCostInto(rows.Scan, &d); err != nil { + return nil, fmt.Errorf("scan rm_cost row: %w", err) + } + return d.toEntity(), nil +} + +type scanFn func(...any) error + +func scanCostInto(scan scanFn, d *costDTO) error { + return scan( + &d.ID, &d.Period, &d.RMCode, &d.RMType, &d.GroupHeadID, &d.ItemCode, &d.RMName, &d.UOMCode, + &d.ConsRate, &d.StoresRate, &d.DeptRate, &d.PO1Rate, &d.PO2Rate, &d.PO3Rate, + &d.CostVal, &d.CostMark, &d.CostSim, + &d.FlagValuation, &d.FlagMarketing, &d.FlagSimulation, + &d.FlagValuationUsed, &d.FlagMarketingUsed, &d.FlagSimulationUsed, + &d.CalculatedAt, &d.CalculatedBy, &d.CreatedAt, &d.CreatedBy, &d.UpdatedAt, &d.UpdatedBy, + ) +} + +// ============================================================================= +// History scanning +// ============================================================================= + +const historySelectColumnsSQL = ` + SELECT history_id, rm_cost_id, job_id, period, rm_code, rm_type, group_head_id, + cons_rate, stores_rate, dept_rate, po_rate_1, po_rate_2, po_rate_3, + cost_percentage, cost_per_kg, + flag_valuation, flag_marketing, flag_simulation, + init_val_valuation, init_val_marketing, init_val_simulation, + cost_val, cost_mark, cost_sim, + flag_valuation_used, flag_marketing_used, flag_simulation_used, + source_item_count, trigger_reason, calculated_at, calculated_by` + +func scanHistoryRow(rows *sql.Rows) (rmcost.History, error) { + var ( + h rmcost.History + rmCostID uuid.NullUUID + jobID uuid.NullUUID + groupHeadID uuid.NullUUID + cons sql.NullFloat64 + stores sql.NullFloat64 + dept sql.NullFloat64 + po1 sql.NullFloat64 + po2 sql.NullFloat64 + po3 sql.NullFloat64 + initVal sql.NullFloat64 + initMkt sql.NullFloat64 + initSim sql.NullFloat64 + costVal sql.NullFloat64 + costMkt sql.NullFloat64 + costSim sql.NullFloat64 + rmType string + flagVal string + flagMkt string + flagSim string + flagValUsed string + flagMktUsed string + flagSimUsed string + reason string + ) + if err := rows.Scan( + &h.ID, &rmCostID, &jobID, &h.Period, &h.RMCode, &rmType, &groupHeadID, + &cons, &stores, &dept, &po1, &po2, &po3, + &h.CostPercentage, &h.CostPerKg, + &flagVal, &flagMkt, &flagSim, + &initVal, &initMkt, &initSim, + &costVal, &costMkt, &costSim, + &flagValUsed, &flagMktUsed, &flagSimUsed, + &h.SourceItemCount, &reason, &h.CalculatedAt, &h.CalculatedBy, + ); err != nil { + return rmcost.History{}, err + } + if rmCostID.Valid { + id := rmCostID.UUID + h.RMCostID = &id + } + if jobID.Valid { + id := jobID.UUID + h.JobID = &id + } + if groupHeadID.Valid { + id := groupHeadID.UUID + h.GroupHeadID = &id + } + h.RMType = rmcost.RMType(rmType) + h.Rates = rmcost.StageRates{ + Cons: nullFloatOrZero(cons), Stores: nullFloatOrZero(stores), Dept: nullFloatOrZero(dept), + PO1: nullFloatOrZero(po1), PO2: nullFloatOrZero(po2), PO3: nullFloatOrZero(po3), + } + h.FlagValuation = rmcost.Stage(flagVal) + h.FlagMarketing = rmcost.Stage(flagMkt) + h.FlagSimulation = rmcost.Stage(flagSim) + h.FlagValuationUsed = rmcost.Stage(flagValUsed) + h.FlagMarketingUsed = rmcost.Stage(flagMktUsed) + h.FlagSimulationUsed = rmcost.Stage(flagSimUsed) + h.InitValValuation = nullFloatPtr(initVal) + h.InitValMarketing = nullFloatPtr(initMkt) + h.InitValSimulation = nullFloatPtr(initSim) + h.CostValuation = nullFloatPtr(costVal) + h.CostMarketing = nullFloatPtr(costMkt) + h.CostSimulation = nullFloatPtr(costSim) + h.TriggerReason = rmcost.HistoryTriggerReason(reason) + return h, nil +} + +// nullFloatOrZero returns the float64 value or 0 when the NullFloat64 is invalid. +func nullFloatOrZero(v sql.NullFloat64) float64 { + if v.Valid { + return v.Float64 + } + return 0 +} diff --git a/services/finance/internal/infrastructure/postgres/rmgroup_audit.go b/services/finance/internal/infrastructure/postgres/rmgroup_audit.go new file mode 100644 index 0000000..d1a4c79 --- /dev/null +++ b/services/finance/internal/infrastructure/postgres/rmgroup_audit.go @@ -0,0 +1,85 @@ +// Package postgres provides PostgreSQL implementations for domain repositories. +package postgres + +import ( + "context" + "database/sql" + "fmt" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// Audit action constants for aud_rm_group / aud_rm_group_detail. +const ( + auditActionCreate = "CREATE" + auditActionUpdate = "UPDATE" + auditActionDelete = "DELETE" +) + +// sqlExecutor is the narrow contract shared by *sql.DB and *sql.Tx. Audit +// helpers accept this so they can run inside a transaction when the caller +// supplies one, or directly on the pool when no transaction is in scope. +type sqlExecutor interface { + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) +} + +// insertHeadAudit appends one row to aud_rm_group capturing the post-operation +// snapshot of the head. Callers pass the action type (CREATE/UPDATE/DELETE) +// and the actor who performed the operation. +func insertHeadAudit(ctx context.Context, exec sqlExecutor, head *rmgroup.Head, action, changedBy string) error { + query := ` + INSERT INTO aud_rm_group ( + group_head_id, action, + group_code, group_name, description, colourant, ci_name, + cost_percentage, cost_per_kg, + flag_valuation, flag_marketing, flag_simulation, + init_val_valuation, init_val_marketing, init_val_simulation, + is_active, changed_by + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) + ` + _, err := exec.ExecContext(ctx, query, + head.ID(), action, + head.Code().String(), head.Name(), nullableString(head.Description()), + nullableString(head.Colorant()), nullableString(head.CIName()), + head.CostPercentage(), head.CostPerKg(), + head.FlagValuation().String(), head.FlagMarketing().String(), head.FlagSimulation().String(), + head.InitValValuation(), head.InitValMarketing(), head.InitValSimulation(), + head.IsActive(), changedBy, + ) + if err != nil { + return fmt.Errorf("insert aud_rm_group: %w", err) + } + return nil +} + +// insertDetailAudit appends one row to aud_rm_group_detail. +func insertDetailAudit(ctx context.Context, exec sqlExecutor, detail *rmgroup.Detail, action, changedBy string) error { + query := ` + INSERT INTO aud_rm_group_detail ( + group_detail_id, group_head_id, action, + item_code, item_name, item_type_code, grade_code, item_grade, uom_code, + market_percentage, market_value_rp, + sort_order, is_active, is_dummy, changed_by + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) + ` + _, err := exec.ExecContext(ctx, query, + detail.ID(), detail.HeadID(), action, + detail.ItemCode().String(), + nullableString(detail.ItemName()), nullableString(detail.ItemTypeCode()), + nullableString(detail.GradeCode()), nullableString(detail.ItemGrade()), nullableString(detail.UOMCode()), + detail.MarketPercentage(), detail.MarketValueRp(), + detail.SortOrder(), detail.IsActive(), detail.IsDummy(), + changedBy, + ) + if err != nil { + return fmt.Errorf("insert aud_rm_group_detail: %w", err) + } + return nil +} + +// insertDetailAuditDelete writes a DELETE audit row using a partial snapshot +// (only the ids + actor). Used on soft-delete when the full entity may not be +// readily available — the existing row stays addressable via group_detail_id. +func insertDetailAuditDelete(ctx context.Context, exec sqlExecutor, detail *rmgroup.Detail, changedBy string) error { + return insertDetailAudit(ctx, exec, detail, auditActionDelete, changedBy) +} diff --git a/services/finance/internal/infrastructure/postgres/rmgroup_detail_repository.go b/services/finance/internal/infrastructure/postgres/rmgroup_detail_repository.go new file mode 100644 index 0000000..c26c10c --- /dev/null +++ b/services/finance/internal/infrastructure/postgres/rmgroup_detail_repository.go @@ -0,0 +1,247 @@ +// Package postgres provides PostgreSQL implementations for domain repositories. +package postgres + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// Verify full interface implementation at compile time (both head + detail methods). +var _ rmgroup.Repository = (*RMGroupRepository)(nil) + +// ============================================================================= +// Detail operations +// ============================================================================= + +// AddDetail persists a new Detail row and writes a CREATE audit row in the same tx. +func (r *RMGroupRepository) AddDetail(ctx context.Context, detail *rmgroup.Detail) error { + return r.db.Transaction(ctx, func(tx *sql.Tx) error { + query := ` + INSERT INTO cst_rm_group_detail ( + group_detail_id, group_head_id, item_code, item_name, item_type_code, + grade_code, item_grade, uom_code, + market_percentage, market_value_rp, + sort_order, is_active, is_dummy, created_at, created_by + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) + ` + _, err := tx.ExecContext(ctx, query, + detail.ID(), detail.HeadID(), detail.ItemCode().String(), + nullableString(detail.ItemName()), nullableString(detail.ItemTypeCode()), + nullableString(detail.GradeCode()), nullableString(detail.ItemGrade()), nullableString(detail.UOMCode()), + detail.MarketPercentage(), detail.MarketValueRp(), + detail.SortOrder(), detail.IsActive(), detail.IsDummy(), + detail.CreatedAt(), detail.CreatedBy(), + ) + if err != nil { + if isUniqueViolation(err) { + return rmgroup.ErrItemAlreadyInOtherGroup + } + return fmt.Errorf("add rm group detail: %w", err) + } + return insertDetailAudit(ctx, tx, detail, auditActionCreate, detail.CreatedBy()) + }) +} + +// UpdateDetail persists changes to an existing detail and writes an UPDATE audit row in the same tx. +func (r *RMGroupRepository) UpdateDetail(ctx context.Context, detail *rmgroup.Detail) error { + return r.db.Transaction(ctx, func(tx *sql.Tx) error { + query := ` + UPDATE cst_rm_group_detail SET + item_name=$2, item_type_code=$3, grade_code=$4, item_grade=$5, uom_code=$6, + market_percentage=$7, market_value_rp=$8, + sort_order=$9, is_active=$10, is_dummy=$11, + updated_at=$12, updated_by=$13 + WHERE group_detail_id=$1 AND deleted_at IS NULL + ` + res, err := tx.ExecContext(ctx, query, + detail.ID(), + nullableString(detail.ItemName()), nullableString(detail.ItemTypeCode()), + nullableString(detail.GradeCode()), nullableString(detail.ItemGrade()), nullableString(detail.UOMCode()), + detail.MarketPercentage(), detail.MarketValueRp(), + detail.SortOrder(), detail.IsActive(), detail.IsDummy(), + detail.UpdatedAt(), detail.UpdatedBy(), + ) + if err != nil { + if isUniqueViolation(err) { + return rmgroup.ErrItemAlreadyInOtherGroup + } + return fmt.Errorf("update rm group detail: %w", err) + } + n, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("rows affected: %w", err) + } + if n == 0 { + return rmgroup.ErrDetailNotFound + } + return insertDetailAudit(ctx, tx, detail, auditActionUpdate, derefString(detail.UpdatedBy())) + }) +} + +// GetDetailByID retrieves a detail by ID. +func (r *RMGroupRepository) GetDetailByID(ctx context.Context, id uuid.UUID) (*rmgroup.Detail, error) { + return r.scanDetail(r.db.QueryRowContext(ctx, detailSelectSQL+` WHERE group_detail_id=$1 AND deleted_at IS NULL`, id)) +} + +// GetActiveDetailByItemCodeGrade looks up the single active detail holding the +// given (item_code, grade_code) pair. The natural key for a RM "variant" +// matches the Oracle sync feed's (item_code, grade_code) key, so variants +// with the same item_code but different grade_code are independent. +// gradeCode "" matches rows with NULL or empty grade_code (mirrors the +// migration 000018 unique index that COALESCEs NULL to ”). +func (r *RMGroupRepository) GetActiveDetailByItemCodeGrade(ctx context.Context, itemCode rmgroup.ItemCode, gradeCode string) (*rmgroup.Detail, error) { + return r.scanDetail(r.db.QueryRowContext(ctx, + detailSelectSQL+` WHERE item_code=$1 AND COALESCE(grade_code,'')=$2 AND is_active=true AND deleted_at IS NULL LIMIT 1`, + itemCode.String(), gradeCode)) +} + +// ListDetailsByHeadID returns every non-deleted detail for the given head, ordered by sort_order. +func (r *RMGroupRepository) ListDetailsByHeadID(ctx context.Context, headID uuid.UUID) ([]*rmgroup.Detail, error) { + return r.listDetails(ctx, + detailSelectSQL+` WHERE group_head_id=$1 AND deleted_at IS NULL ORDER BY sort_order ASC, item_code ASC`, + headID) +} + +// ListActiveDetailsByHeadID returns only active, non-deleted details (used by the calc engine). +func (r *RMGroupRepository) ListActiveDetailsByHeadID(ctx context.Context, headID uuid.UUID) ([]*rmgroup.Detail, error) { + return r.listDetails(ctx, + detailSelectSQL+` WHERE group_head_id=$1 AND is_active=true AND deleted_at IS NULL ORDER BY sort_order ASC, item_code ASC`, + headID) +} + +// SoftDeleteDetail marks a single detail row as deleted and writes a DELETE audit row in the same tx. +func (r *RMGroupRepository) SoftDeleteDetail(ctx context.Context, id uuid.UUID, deletedBy string) error { + return r.db.Transaction(ctx, func(tx *sql.Tx) error { + detail, err := r.scanDetail(tx.QueryRowContext(ctx, + detailSelectSQL+` WHERE group_detail_id=$1 AND deleted_at IS NULL`, id)) + if err != nil { + return err + } + res, err := tx.ExecContext(ctx, + `UPDATE cst_rm_group_detail SET deleted_at=$2, deleted_by=$3, is_active=false + WHERE group_detail_id=$1 AND deleted_at IS NULL`, + id, time.Now(), deletedBy) + if err != nil { + return fmt.Errorf("soft-delete detail: %w", err) + } + n, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("rows affected: %w", err) + } + if n == 0 { + return rmgroup.ErrDetailNotFound + } + return insertDetailAuditDelete(ctx, tx, detail, deletedBy) + }) +} + +// ============================================================================= +// Detail scanning helpers +// ============================================================================= + +const detailSelectSQL = ` + SELECT group_detail_id, group_head_id, item_code, item_name, item_type_code, + grade_code, item_grade, uom_code, + market_percentage, market_value_rp, + sort_order, is_active, is_dummy, created_at, created_by, + updated_at, updated_by, deleted_at, deleted_by + FROM cst_rm_group_detail` + +type detailDTO struct { + ID uuid.UUID + HeadID uuid.UUID + ItemCode string + ItemName sql.NullString + ItemTypeCode sql.NullString + GradeCode sql.NullString + ItemGrade sql.NullString + UOMCode sql.NullString + MarketPercentage sql.NullFloat64 + MarketValueRp sql.NullFloat64 + SortOrder int32 + IsActive bool + IsDummy bool + CreatedAt time.Time + CreatedBy string + UpdatedAt sql.NullTime + UpdatedBy sql.NullString + DeletedAt sql.NullTime + DeletedBy sql.NullString +} + +func (d *detailDTO) toEntity() (*rmgroup.Detail, error) { + itemCode, err := rmgroup.NewItemCode(d.ItemCode) + if err != nil { + return nil, fmt.Errorf("invalid item_code from db: %w", err) + } + return rmgroup.ReconstructDetail( + d.ID, d.HeadID, itemCode, + nullStringVal(d.ItemName), nullStringVal(d.ItemTypeCode), + nullStringVal(d.GradeCode), nullStringVal(d.ItemGrade), nullStringVal(d.UOMCode), + nullFloatPtr(d.MarketPercentage), nullFloatPtr(d.MarketValueRp), + d.SortOrder, d.IsActive, d.IsDummy, + d.CreatedAt, d.CreatedBy, + nullTimePtr(d.UpdatedAt), nullStringPtr(d.UpdatedBy), + nullTimePtr(d.DeletedAt), nullStringPtr(d.DeletedBy), + ), nil +} + +func (r *RMGroupRepository) scanDetail(row *sql.Row) (*rmgroup.Detail, error) { + var d detailDTO + err := row.Scan( + &d.ID, &d.HeadID, &d.ItemCode, &d.ItemName, &d.ItemTypeCode, + &d.GradeCode, &d.ItemGrade, &d.UOMCode, + &d.MarketPercentage, &d.MarketValueRp, + &d.SortOrder, &d.IsActive, &d.IsDummy, &d.CreatedAt, &d.CreatedBy, + &d.UpdatedAt, &d.UpdatedBy, &d.DeletedAt, &d.DeletedBy, + ) + if errors.Is(err, sql.ErrNoRows) { + return nil, rmgroup.ErrDetailNotFound + } + if err != nil { + return nil, fmt.Errorf("scan rm group detail: %w", err) + } + return d.toEntity() +} + +func (r *RMGroupRepository) listDetails(ctx context.Context, query string, args ...any) ([]*rmgroup.Detail, error) { + rows, err := r.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("list rm group details: %w", err) + } + defer closeRows(rows) + + var out []*rmgroup.Detail + for rows.Next() { + var d detailDTO + if err := rows.Scan( + &d.ID, &d.HeadID, &d.ItemCode, &d.ItemName, &d.ItemTypeCode, + &d.GradeCode, &d.ItemGrade, &d.UOMCode, + &d.MarketPercentage, &d.MarketValueRp, + &d.SortOrder, &d.IsActive, &d.IsDummy, &d.CreatedAt, &d.CreatedBy, + &d.UpdatedAt, &d.UpdatedBy, &d.DeletedAt, &d.DeletedBy, + ); err != nil { + return nil, fmt.Errorf("scan detail row: %w", err) + } + entity, err := d.toEntity() + if err != nil { + return nil, err + } + out = append(out, entity) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate details: %w", err) + } + return out, nil +} + +// nullTimePtr converts a sql.NullTime to *time.Time. Defined here to avoid +// clashing with the same-named helper in job_repository.go by reusing that one. +// (No redeclaration — this comment documents intent; actual helper lives in job_repository.go.) diff --git a/services/finance/internal/infrastructure/postgres/rmgroup_head_repository.go b/services/finance/internal/infrastructure/postgres/rmgroup_head_repository.go new file mode 100644 index 0000000..abfd630 --- /dev/null +++ b/services/finance/internal/infrastructure/postgres/rmgroup_head_repository.go @@ -0,0 +1,416 @@ +// Package postgres provides PostgreSQL implementations for domain repositories. +package postgres + +import ( + "context" + "database/sql" + "errors" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmgroup" +) + +// RMGroupRepository implements rmgroup.Repository using PostgreSQL. +type RMGroupRepository struct { + db *DB +} + +// NewRMGroupRepository creates a new RMGroupRepository instance. +func NewRMGroupRepository(db *DB) *RMGroupRepository { + return &RMGroupRepository{db: db} +} + +// ============================================================================= +// Head operations +// ============================================================================= + +// CreateHead persists a new Head row and writes a CREATE audit row in the same tx. +func (r *RMGroupRepository) CreateHead(ctx context.Context, head *rmgroup.Head) error { + return r.db.Transaction(ctx, func(tx *sql.Tx) error { + query := ` + INSERT INTO cst_rm_group_head ( + group_head_id, group_code, group_name, description, colourant, ci_name, + cost_percentage, cost_per_kg, + flag_valuation, flag_marketing, flag_simulation, + init_val_valuation, init_val_marketing, init_val_simulation, + is_active, created_at, created_by + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) + ` + _, err := tx.ExecContext(ctx, query, + head.ID(), head.Code().String(), head.Name(), head.Description(), + nullableString(head.Colorant()), nullableString(head.CIName()), + head.CostPercentage(), head.CostPerKg(), + head.FlagValuation().String(), head.FlagMarketing().String(), head.FlagSimulation().String(), + head.InitValValuation(), head.InitValMarketing(), head.InitValSimulation(), + head.IsActive(), head.CreatedAt(), head.CreatedBy(), + ) + if err != nil { + if isUniqueViolation(err) { + return rmgroup.ErrCodeAlreadyExists + } + return fmt.Errorf("create rm group head: %w", err) + } + return insertHeadAudit(ctx, tx, head, auditActionCreate, head.CreatedBy()) + }) +} + +// GetHeadByID retrieves a head by ID. +func (r *RMGroupRepository) GetHeadByID(ctx context.Context, id uuid.UUID) (*rmgroup.Head, error) { + return r.scanHead(r.db.QueryRowContext(ctx, headSelectSQL+` WHERE group_head_id = $1 AND deleted_at IS NULL`, id)) +} + +// GetHeadByCode retrieves a head by its unique code. +func (r *RMGroupRepository) GetHeadByCode(ctx context.Context, code rmgroup.Code) (*rmgroup.Head, error) { + return r.scanHead(r.db.QueryRowContext(ctx, headSelectSQL+` WHERE group_code = $1 AND deleted_at IS NULL`, code.String())) +} + +// ListHeads returns a page of heads plus the total count. +func (r *RMGroupRepository) ListHeads(ctx context.Context, filter rmgroup.ListFilter) ([]*rmgroup.Head, int64, error) { + filter.Validate() + + base := ` WHERE deleted_at IS NULL` + args := []any{} + argIdx := 1 + + if filter.Search != "" { + base += fmt.Sprintf(` AND (group_code ILIKE $%d OR group_name ILIKE $%d OR description ILIKE $%d OR colourant ILIKE $%d OR ci_name ILIKE $%d)`, + argIdx, argIdx, argIdx, argIdx, argIdx) + args = append(args, "%"+filter.Search+"%") + argIdx++ + } + if filter.IsActive != nil { + base += fmt.Sprintf(` AND is_active = $%d`, argIdx) + args = append(args, *filter.IsActive) + argIdx++ + } + if filter.Flag != "" { + base += fmt.Sprintf(` AND (flag_valuation = $%d OR flag_marketing = $%d OR flag_simulation = $%d)`, argIdx, argIdx, argIdx) + args = append(args, filter.Flag.String()) + argIdx++ + } + + var total int64 + if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM cst_rm_group_head`+base, args...).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("count rm group heads: %w", err) + } + + orderCol := map[string]string{ + "code": "group_code", + "name": "group_name", + "created_at": "created_at", + "updated_at": "updated_at", + }[filter.SortBy] + if orderCol == "" { + orderCol = "group_code" + } + orderDir := sortASC + if strings.ToUpper(filter.SortOrder) == sortDESC { + orderDir = sortDESC + } + + selectQuery := headSelectSQL + " " + base + + fmt.Sprintf(` ORDER BY %s %s LIMIT $%d OFFSET $%d`, orderCol, orderDir, argIdx, argIdx+1) + args = append(args, filter.PageSize, filter.Offset()) + + rows, err := r.db.QueryContext(ctx, selectQuery, args...) + if err != nil { + return nil, 0, fmt.Errorf("list rm group heads: %w", err) + } + defer closeRows(rows) + + var heads []*rmgroup.Head + for rows.Next() { + head, err := r.scanHeadRow(rows) + if err != nil { + return nil, 0, err + } + heads = append(heads, head) + } + if err := rows.Err(); err != nil { + return nil, 0, fmt.Errorf("iterate rm group heads: %w", err) + } + return heads, total, nil +} + +// ListAllHeads returns every non-deleted head matching the active filter, +// ordered by group_code. No pagination — intended for export. +func (r *RMGroupRepository) ListAllHeads(ctx context.Context, activeFilter *bool) ([]*rmgroup.Head, error) { + base := ` WHERE deleted_at IS NULL` + args := []any{} + if activeFilter != nil { + base += ` AND is_active = $1` + args = append(args, *activeFilter) + } + q := headSelectSQL + " " + base + ` ORDER BY group_code ASC` + rows, err := r.db.QueryContext(ctx, q, args...) + if err != nil { + return nil, fmt.Errorf("list all rm group heads: %w", err) + } + defer closeRows(rows) + + var out []*rmgroup.Head + for rows.Next() { + h, err := r.scanHeadRow(rows) + if err != nil { + return nil, err + } + out = append(out, h) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate all rm group heads: %w", err) + } + return out, nil +} + +// UpdateHead persists changes to an existing head and writes an UPDATE audit row in the same tx. +func (r *RMGroupRepository) UpdateHead(ctx context.Context, head *rmgroup.Head) error { + return r.db.Transaction(ctx, func(tx *sql.Tx) error { + query := ` + UPDATE cst_rm_group_head SET + group_name = $2, description = $3, colourant = $4, ci_name = $5, + cost_percentage = $6, cost_per_kg = $7, + flag_valuation = $8, flag_marketing = $9, flag_simulation = $10, + init_val_valuation = $11, init_val_marketing = $12, init_val_simulation = $13, + is_active = $14, updated_at = $15, updated_by = $16 + WHERE group_head_id = $1 AND deleted_at IS NULL + ` + res, err := tx.ExecContext(ctx, query, + head.ID(), head.Name(), head.Description(), + nullableString(head.Colorant()), nullableString(head.CIName()), + head.CostPercentage(), head.CostPerKg(), + head.FlagValuation().String(), head.FlagMarketing().String(), head.FlagSimulation().String(), + head.InitValValuation(), head.InitValMarketing(), head.InitValSimulation(), + head.IsActive(), head.UpdatedAt(), head.UpdatedBy(), + ) + if err != nil { + return fmt.Errorf("update rm group head: %w", err) + } + n, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("rows affected: %w", err) + } + if n == 0 { + return rmgroup.ErrNotFound + } + return insertHeadAudit(ctx, tx, head, auditActionUpdate, derefString(head.UpdatedBy())) + }) +} + +// SoftDeleteHead marks the head and all of its active details as deleted in a single tx, +// and writes a DELETE audit row for the head and for each affected detail. +func (r *RMGroupRepository) SoftDeleteHead(ctx context.Context, id uuid.UUID, deletedBy string) error { + return r.db.Transaction(ctx, func(tx *sql.Tx) error { + head, err := r.scanHead(tx.QueryRowContext(ctx, headSelectSQL+` WHERE group_head_id = $1 AND deleted_at IS NULL`, id)) + if err != nil { + return err + } + details, err := r.listDetails(ctx, + detailSelectSQL+` WHERE group_head_id=$1 AND deleted_at IS NULL`, id) + if err != nil { + return err + } + + now := time.Now() + res, err := tx.ExecContext(ctx, ` + UPDATE cst_rm_group_head SET deleted_at=$2, deleted_by=$3, is_active=false + WHERE group_head_id=$1 AND deleted_at IS NULL + `, id, now, deletedBy) + if err != nil { + return fmt.Errorf("soft-delete head: %w", err) + } + n, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("rows affected: %w", err) + } + if n == 0 { + return rmgroup.ErrNotFound + } + if _, err := tx.ExecContext(ctx, ` + UPDATE cst_rm_group_detail SET deleted_at=$2, deleted_by=$3, is_active=false + WHERE group_head_id=$1 AND deleted_at IS NULL + `, id, now, deletedBy); err != nil { + return fmt.Errorf("soft-delete details: %w", err) + } + + if err := insertHeadAudit(ctx, tx, head, auditActionDelete, deletedBy); err != nil { + return err + } + for _, d := range details { + if err := insertDetailAuditDelete(ctx, tx, d, deletedBy); err != nil { + return err + } + } + return nil + }) +} + +// derefString returns the value pointed to by s, or "" if s is nil. Used for +// audit changedBy values where the updater/deleter field is an optional *string. +func derefString(s *string) string { + if s == nil { + return "" + } + return *s +} + +// ExistsHeadByCode reports whether a non-deleted head with this code exists. +func (r *RMGroupRepository) ExistsHeadByCode(ctx context.Context, code rmgroup.Code) (bool, error) { + var exists bool + err := r.db.QueryRowContext(ctx, + `SELECT EXISTS(SELECT 1 FROM cst_rm_group_head WHERE group_code=$1 AND deleted_at IS NULL)`, + code.String()).Scan(&exists) + if err != nil { + return false, fmt.Errorf("exists head by code: %w", err) + } + return exists, nil +} + +// ExistsHeadByID reports whether a non-deleted head with this ID exists. +func (r *RMGroupRepository) ExistsHeadByID(ctx context.Context, id uuid.UUID) (bool, error) { + var exists bool + err := r.db.QueryRowContext(ctx, + `SELECT EXISTS(SELECT 1 FROM cst_rm_group_head WHERE group_head_id=$1 AND deleted_at IS NULL)`, + id).Scan(&exists) + if err != nil { + return false, fmt.Errorf("exists head by id: %w", err) + } + return exists, nil +} + +// ============================================================================= +// Head scanning helpers +// ============================================================================= + +const headSelectSQL = ` + SELECT group_head_id, group_code, group_name, description, colourant, ci_name, + cost_percentage, cost_per_kg, + flag_valuation, flag_marketing, flag_simulation, + init_val_valuation, init_val_marketing, init_val_simulation, + is_active, created_at, created_by, updated_at, updated_by, deleted_at, deleted_by + FROM cst_rm_group_head` + +type headDTO struct { + ID uuid.UUID + Code string + Name string + Description sql.NullString + Colorant sql.NullString + CIName sql.NullString + CostPercentage float64 + CostPerKg float64 + FlagValuation string + FlagMarketing string + FlagSimulation string + InitValValuation sql.NullFloat64 + InitValMarketing sql.NullFloat64 + InitValSimulation sql.NullFloat64 + IsActive bool + CreatedAt time.Time + CreatedBy string + UpdatedAt sql.NullTime + UpdatedBy sql.NullString + DeletedAt sql.NullTime + DeletedBy sql.NullString +} + +func (d *headDTO) toEntity() (*rmgroup.Head, error) { + code, err := rmgroup.NewCode(d.Code) + if err != nil { + return nil, fmt.Errorf("invalid code from db: %w", err) + } + flagV, err := rmgroup.ParseFlag(d.FlagValuation) + if err != nil { + return nil, fmt.Errorf("invalid flag_valuation from db: %w", err) + } + flagM, err := rmgroup.ParseFlag(d.FlagMarketing) + if err != nil { + return nil, fmt.Errorf("invalid flag_marketing from db: %w", err) + } + flagS, err := rmgroup.ParseFlag(d.FlagSimulation) + if err != nil { + return nil, fmt.Errorf("invalid flag_simulation from db: %w", err) + } + return rmgroup.ReconstructHead( + d.ID, code, d.Name, + nullStringVal(d.Description), nullStringVal(d.Colorant), nullStringVal(d.CIName), + d.CostPercentage, d.CostPerKg, + flagV, flagM, flagS, + nullFloatPtr(d.InitValValuation), nullFloatPtr(d.InitValMarketing), nullFloatPtr(d.InitValSimulation), + d.IsActive, d.CreatedAt, d.CreatedBy, + nullTimePtr(d.UpdatedAt), nullStringPtr(d.UpdatedBy), + nullTimePtr(d.DeletedAt), nullStringPtr(d.DeletedBy), + ), nil +} + +func (r *RMGroupRepository) scanHead(row *sql.Row) (*rmgroup.Head, error) { + var d headDTO + err := row.Scan( + &d.ID, &d.Code, &d.Name, &d.Description, &d.Colorant, &d.CIName, + &d.CostPercentage, &d.CostPerKg, + &d.FlagValuation, &d.FlagMarketing, &d.FlagSimulation, + &d.InitValValuation, &d.InitValMarketing, &d.InitValSimulation, + &d.IsActive, &d.CreatedAt, &d.CreatedBy, + &d.UpdatedAt, &d.UpdatedBy, &d.DeletedAt, &d.DeletedBy, + ) + if errors.Is(err, sql.ErrNoRows) { + return nil, rmgroup.ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("scan rm group head: %w", err) + } + return d.toEntity() +} + +func (r *RMGroupRepository) scanHeadRow(rows *sql.Rows) (*rmgroup.Head, error) { + var d headDTO + err := rows.Scan( + &d.ID, &d.Code, &d.Name, &d.Description, &d.Colorant, &d.CIName, + &d.CostPercentage, &d.CostPerKg, + &d.FlagValuation, &d.FlagMarketing, &d.FlagSimulation, + &d.InitValValuation, &d.InitValMarketing, &d.InitValSimulation, + &d.IsActive, &d.CreatedAt, &d.CreatedBy, + &d.UpdatedAt, &d.UpdatedBy, &d.DeletedAt, &d.DeletedBy, + ) + if err != nil { + return nil, fmt.Errorf("scan rm group head row: %w", err) + } + return d.toEntity() +} + +// ============================================================================= +// Shared helpers (used by head + detail repo + rmcost repo). +// ============================================================================= + +func nullableString(s string) sql.NullString { + if s == "" { + return sql.NullString{} + } + return sql.NullString{String: s, Valid: true} +} + +func nullStringVal(v sql.NullString) string { + if v.Valid { + return v.String + } + return "" +} + +func nullStringPtr(v sql.NullString) *string { + if v.Valid { + s := v.String + return &s + } + return nil +} + +func nullFloatPtr(v sql.NullFloat64) *float64 { + if v.Valid { + f := v.Float64 + return &f + } + return nil +} diff --git a/services/finance/internal/infrastructure/postgres/rmgroup_item_rates.go b/services/finance/internal/infrastructure/postgres/rmgroup_item_rates.go new file mode 100644 index 0000000..47da5ea --- /dev/null +++ b/services/finance/internal/infrastructure/postgres/rmgroup_item_rates.go @@ -0,0 +1,87 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + + "github.com/google/uuid" + + appgroup "github.com/mutugading/goapps-backend/services/finance/internal/application/rmgroup" +) + +// Verify interface compliance at compile time. +var _ appgroup.GroupItemRatesReader = (*SyncDataRepository)(nil) + +// ListGroupItemRates joins active (non-deleted) details of a group with the +// period's Oracle sync rates. Details without a sync row produce zero rates. +func (r *SyncDataRepository) ListGroupItemRates( + ctx context.Context, + headID uuid.UUID, + period string, +) ([]*appgroup.GroupItemRates, error) { + const q = ` + SELECT + d.item_code, + COALESCE(d.item_name, ''), + COALESCE(d.grade_code, ''), + COALESCE(d.item_grade, ''), + COALESCE(d.uom_code, ''), + d.is_active, + d.is_dummy, + COALESCE(s.period, ''), + COALESCE(s.cons_qty, 0), COALESCE(s.cons_val, 0), COALESCE(s.cons_rate, 0), + COALESCE(s.stores_qty, 0), COALESCE(s.stores_val, 0), COALESCE(s.stores_rate, 0), + COALESCE(s.dept_qty, 0), COALESCE(s.dept_val, 0), COALESCE(s.dept_rate, 0), + COALESCE(s.last_po_qty1, 0), COALESCE(s.last_po_val1, 0), COALESCE(s.last_po_rate1, 0), + COALESCE(s.last_po_qty2, 0), COALESCE(s.last_po_val2, 0), COALESCE(s.last_po_rate2, 0), + COALESCE(s.last_po_qty3, 0), COALESCE(s.last_po_val3, 0), COALESCE(s.last_po_rate3, 0) + FROM cst_rm_group_detail d + LEFT JOIN cst_item_cons_stk_po s + ON s.item_code = d.item_code + AND COALESCE(s.grade_code, '') = COALESCE(d.grade_code, '') + AND s.period = $2 + WHERE d.group_head_id = $1 + AND d.deleted_at IS NULL + ORDER BY d.sort_order, d.item_code, d.grade_code + ` + rows, err := r.db.QueryContext(ctx, q, headID, period) + if err != nil { + return nil, fmt.Errorf("query group item rates: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + _ = closeErr + } + }() + + var out []*appgroup.GroupItemRates + for rows.Next() { + row, scanErr := scanGroupItemRates(rows) + if scanErr != nil { + return nil, fmt.Errorf("scan group item rates: %w", scanErr) + } + out = append(out, row) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate group item rates: %w", err) + } + return out, nil +} + +func scanGroupItemRates(rows *sql.Rows) (*appgroup.GroupItemRates, error) { + var r appgroup.GroupItemRates + if err := rows.Scan( + &r.ItemCode, &r.ItemName, &r.GradeCode, &r.ItemGrade, &r.UOMCode, + &r.IsActive, &r.IsDummy, &r.Period, + &r.ConsQty, &r.ConsVal, &r.ConsRate, + &r.StoresQty, &r.StoresVal, &r.StoresRate, + &r.DeptQty, &r.DeptVal, &r.DeptRate, + &r.LastPOQty1, &r.LastPOVal1, &r.LastPORate1, + &r.LastPOQty2, &r.LastPOVal2, &r.LastPORate2, + &r.LastPOQty3, &r.LastPOVal3, &r.LastPORate3, + ); err != nil { + return nil, err + } + return &r, nil +} diff --git a/services/finance/internal/infrastructure/postgres/syncdata_item_lookup.go b/services/finance/internal/infrastructure/postgres/syncdata_item_lookup.go new file mode 100644 index 0000000..5eebe5b --- /dev/null +++ b/services/finance/internal/infrastructure/postgres/syncdata_item_lookup.go @@ -0,0 +1,68 @@ +package postgres + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/syncdata" +) + +// GetItemByCode returns the most recent sync record for the given item_code, +// prefers a row whose non-null qty columns are populated (so we don't latch +// onto a low-value variant when an enriched variant exists). Retained as a +// compatibility shim that delegates to GetItemByCodeGrade with "". +func (r *SyncDataRepository) GetItemByCode(ctx context.Context, itemCode string) (*syncdata.ItemConsStockPO, error) { + return r.GetItemByCodeGrade(ctx, itemCode, "") +} + +// GetItemByCodeGrade returns the sync record matching (item_code, grade_code). +// When gradeCode is empty, falls back to the most recent variant with the +// largest cons_qty + stores_qty (so metadata backfill picks the enriched row +// instead of an arbitrary one). +func (r *SyncDataRepository) GetItemByCodeGrade(ctx context.Context, itemCode, gradeCode string) (*syncdata.ItemConsStockPO, error) { + var row *sql.Row + if gradeCode != "" { + row = r.db.QueryRowContext(ctx, ` + SELECT period, item_code, grade_code, grade_name, item_name, uom + FROM cst_item_cons_stk_po + WHERE item_code = $1 AND COALESCE(grade_code,'') = $2 + ORDER BY period DESC + LIMIT 1 + `, itemCode, gradeCode) + } else { + row = r.db.QueryRowContext(ctx, ` + SELECT period, item_code, grade_code, grade_name, item_name, uom + FROM cst_item_cons_stk_po + WHERE item_code = $1 + ORDER BY period DESC, + (COALESCE(cons_qty,0) + COALESCE(stores_qty,0)) DESC + LIMIT 1 + `, itemCode) + } + + var ( + period string + code string + grade sql.NullString + gradeName sql.NullString + itemName sql.NullString + uom sql.NullString + ) + if err := row.Scan(&period, &code, &grade, &gradeName, &itemName, &uom); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil //nolint:nilnil // not found is not an error in this API + } + return nil, fmt.Errorf("get item by code: %w", err) + } + + return &syncdata.ItemConsStockPO{ + Period: period, + ItemCode: code, + GradeCode: grade.String, + GradeName: gradeName.String, + ItemName: itemName.String, + UOM: uom.String, + }, nil +} diff --git a/services/finance/internal/infrastructure/postgres/syncdata_rateinputs.go b/services/finance/internal/infrastructure/postgres/syncdata_rateinputs.go new file mode 100644 index 0000000..e3b4675 --- /dev/null +++ b/services/finance/internal/infrastructure/postgres/syncdata_rateinputs.go @@ -0,0 +1,130 @@ +package postgres + +import ( + "context" + "fmt" + "strings" + + "github.com/mutugading/goapps-backend/services/finance/internal/domain/rmcost" +) + +// FetchRateInputs returns per-stage numerator/denominator pointers from +// cst_item_cons_stk_po scoped to (period, item_codes). The returned slice is the +// input to rmcost.AggregateRates; int is the number of source rows found. +// +// Implements appcost.SourceDataReader. Lives on SyncDataRepository because the +// data source is the Oracle-synced table owned by the syncdata bounded context. +func (r *SyncDataRepository) FetchRateInputs( + ctx context.Context, + period string, + itemCodes []string, +) ([]rmcost.RateInputs, int, error) { + if len(itemCodes) == 0 { + return nil, 0, nil + } + + placeholders := make([]string, len(itemCodes)) + args := make([]any, 0, len(itemCodes)+1) + args = append(args, period) + for i, code := range itemCodes { + placeholders[i] = fmt.Sprintf("$%d", i+2) + args = append(args, code) + } + + query := fmt.Sprintf(` + SELECT cons_qty, cons_val, + stores_qty, stores_val, + dept_qty, dept_val, + last_po_qty1, last_po_val1, + last_po_qty2, last_po_val2, + last_po_qty3, last_po_val3 + FROM cst_item_cons_stk_po + WHERE period = $1 AND item_code IN (%s) + `, strings.Join(placeholders, ",")) + + rows, err := r.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, 0, fmt.Errorf("fetch rate inputs: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + _ = closeErr + } + }() + + var out []rmcost.RateInputs + for rows.Next() { + var in rmcost.RateInputs + if err := rows.Scan( + &in.ConsQty, &in.ConsVal, + &in.StoresQty, &in.StoresVal, + &in.DeptQty, &in.DeptVal, + &in.PO1Qty, &in.PO1Val, + &in.PO2Qty, &in.PO2Val, + &in.PO3Qty, &in.PO3Val, + ); err != nil { + return nil, 0, fmt.Errorf("scan rate inputs: %w", err) + } + out = append(out, in) + } + if err := rows.Err(); err != nil { + return nil, 0, fmt.Errorf("iterate rate inputs: %w", err) + } + + return out, len(out), nil +} + +// FetchItemUOMs returns a map of item_code -> uom pulled from +// cst_item_cons_stk_po for the given period. Items with a NULL or empty uom +// are omitted from the map so the caller can treat "missing" uniformly. +// +// Implements appcost.SourceDataReader.FetchItemUOMs. +func (r *SyncDataRepository) FetchItemUOMs( + ctx context.Context, + period string, + itemCodes []string, +) (map[string]string, error) { + if len(itemCodes) == 0 { + return map[string]string{}, nil + } + + placeholders := make([]string, len(itemCodes)) + args := make([]any, 0, len(itemCodes)+1) + args = append(args, period) + for i, code := range itemCodes { + placeholders[i] = fmt.Sprintf("$%d", i+2) + args = append(args, code) + } + + query := fmt.Sprintf(` + SELECT item_code, uom + FROM cst_item_cons_stk_po + WHERE period = $1 AND item_code IN (%s) AND uom IS NOT NULL AND uom <> '' + `, strings.Join(placeholders, ",")) + + rows, err := r.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("fetch item uoms: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + _ = closeErr + } + }() + + out := make(map[string]string, len(itemCodes)) + for rows.Next() { + var code, uom string + if err := rows.Scan(&code, &uom); err != nil { + return nil, fmt.Errorf("scan item uom: %w", err) + } + // First non-empty wins per item_code. + if _, seen := out[code]; !seen { + out[code] = uom + } + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate item uoms: %w", err) + } + return out, nil +} diff --git a/services/finance/internal/infrastructure/postgres/syncdata_repository.go b/services/finance/internal/infrastructure/postgres/syncdata_repository.go index 1f4eb72..dae92b9 100644 --- a/services/finance/internal/infrastructure/postgres/syncdata_repository.go +++ b/services/finance/internal/infrastructure/postgres/syncdata_repository.go @@ -2,6 +2,7 @@ package postgres import ( "context" + "database/sql" "fmt" "strings" "time" @@ -174,9 +175,10 @@ func (r *SyncDataRepository) ListItemConsStockPO( } if filter.Search != "" { conditions = append(conditions, fmt.Sprintf( - "to_tsvector('english', coalesce(item_code,'') || ' ' || coalesce(item_name,'') || ' ' || coalesce(grade_name,'')) @@ plainto_tsquery('english', $%d)", argIdx, + "(item_code ILIKE $%d OR item_name ILIKE $%d OR grade_name ILIKE $%d OR grade_code ILIKE $%d)", + argIdx, argIdx, argIdx, argIdx, )) - args = append(args, filter.Search) + args = append(args, "%"+filter.Search+"%") argIdx++ } @@ -269,22 +271,29 @@ func (r *SyncDataRepository) GetDistinctPeriods(ctx context.Context) ([]string, func (r *SyncDataRepository) scanSyncedRow(rows interface{ Scan(dest ...any) error }) (*syncdata.ItemConsStockPO, error) { var item syncdata.ItemConsStockPO var ( - gradeName *string - itemName *string - uom *string - syncedAt time.Time - syncedByJob *uuid.UUID + gradeName *string + itemName *string + uom *string + consQty, consVal, consRate sql.NullFloat64 + storesQty, storesVal, storesRate sql.NullFloat64 + deptQty, deptVal, deptRate sql.NullFloat64 + lastPOQty1, lastPOVal1, lastPORate1 sql.NullFloat64 + lastPOQty2, lastPOVal2, lastPORate2 sql.NullFloat64 + lastPOQty3, lastPOVal3, lastPORate3 sql.NullFloat64 + lastPODt1, lastPODt2, lastPODt3 sql.NullTime + syncedAt time.Time + syncedByJob *uuid.UUID ) err := rows.Scan( &item.Period, &item.ItemCode, &item.GradeCode, &gradeName, &itemName, &uom, - &item.ConsQty, &item.ConsVal, &item.ConsRate, - &item.StoresQty, &item.StoresVal, &item.StoresRate, - &item.DeptQty, &item.DeptVal, &item.DeptRate, - &item.LastPOQty1, &item.LastPOVal1, &item.LastPORate1, &item.LastPODt1, - &item.LastPOQty2, &item.LastPOVal2, &item.LastPORate2, &item.LastPODt2, - &item.LastPOQty3, &item.LastPOVal3, &item.LastPORate3, &item.LastPODt3, + &consQty, &consVal, &consRate, + &storesQty, &storesVal, &storesRate, + &deptQty, &deptVal, &deptRate, + &lastPOQty1, &lastPOVal1, &lastPORate1, &lastPODt1, + &lastPOQty2, &lastPOVal2, &lastPORate2, &lastPODt2, + &lastPOQty3, &lastPOVal3, &lastPORate3, &lastPODt3, &syncedAt, &syncedByJob, ) if err != nil { @@ -300,12 +309,49 @@ func (r *SyncDataRepository) scanSyncedRow(rows interface{ Scan(dest ...any) err if uom != nil { item.UOM = *uom } + item.ConsQty = nullFloat(consQty) + item.ConsVal = nullFloat(consVal) + item.ConsRate = nullFloat(consRate) + item.StoresQty = nullFloat(storesQty) + item.StoresVal = nullFloat(storesVal) + item.StoresRate = nullFloat(storesRate) + item.DeptQty = nullFloat(deptQty) + item.DeptVal = nullFloat(deptVal) + item.DeptRate = nullFloat(deptRate) + item.LastPOQty1 = nullFloat(lastPOQty1) + item.LastPOVal1 = nullFloat(lastPOVal1) + item.LastPORate1 = nullFloat(lastPORate1) + item.LastPOQty2 = nullFloat(lastPOQty2) + item.LastPOVal2 = nullFloat(lastPOVal2) + item.LastPORate2 = nullFloat(lastPORate2) + item.LastPOQty3 = nullFloat(lastPOQty3) + item.LastPOVal3 = nullFloat(lastPOVal3) + item.LastPORate3 = nullFloat(lastPORate3) + item.LastPODt1 = nullTime(lastPODt1) + item.LastPODt2 = nullTime(lastPODt2) + item.LastPODt3 = nullTime(lastPODt3) item.SyncedAt = &syncedAt item.SyncedByJob = syncedByJob return &item, nil } +func nullFloat(v sql.NullFloat64) *float64 { + if !v.Valid { + return nil + } + f := v.Float64 + return &f +} + +func nullTime(v sql.NullTime) *time.Time { + if !v.Valid { + return nil + } + t := v.Time + return &t +} + func nullStr(s string) *string { if s == "" { return nil diff --git a/services/finance/internal/infrastructure/postgres/syncdata_ungrouped.go b/services/finance/internal/infrastructure/postgres/syncdata_ungrouped.go new file mode 100644 index 0000000..bfd4675 --- /dev/null +++ b/services/finance/internal/infrastructure/postgres/syncdata_ungrouped.go @@ -0,0 +1,104 @@ +package postgres + +import ( + "context" + "fmt" + "strings" + + appgroup "github.com/mutugading/goapps-backend/services/finance/internal/application/rmgroup" + "github.com/mutugading/goapps-backend/services/finance/internal/domain/syncdata" +) + +// Verify interface compliance at compile time. +var _ appgroup.UngroupedItemsReader = (*SyncDataRepository)(nil) + +// ListUngroupedItems returns synced raw-material rows from cst_item_cons_stk_po +// that have no active (non-deleted) entry in cst_rm_group_detail. Used by the +// Ungrouped Items report to seed operators' grouping decisions. +func (r *SyncDataRepository) ListUngroupedItems( + ctx context.Context, + filter appgroup.UngroupedItemsFilter, +) ([]*syncdata.ItemConsStockPO, int64, error) { + filter.Validate() + + var conds []string + var args []any + idx := 1 + + if filter.Period != "" { + conds = append(conds, fmt.Sprintf("s.period = $%d", idx)) + args = append(args, filter.Period) + idx++ + } + if filter.Search != "" { + conds = append(conds, fmt.Sprintf( + "(s.item_code ILIKE $%d OR s.item_name ILIKE $%d OR s.grade_code ILIKE $%d OR s.grade_name ILIKE $%d)", + idx, idx, idx, idx, + )) + args = append(args, "%"+filter.Search+"%") + idx++ + } + + where := "WHERE d.group_detail_id IS NULL" + if len(conds) > 0 { + where += " AND " + strings.Join(conds, " AND ") + } + + // Left-join on (item_code, grade_code) as the natural key — this matches + // the unique index from migration 000018. Variants of the same item_code + // with different grade_codes stay independently visible, so once variant + // A is grouped, variant B still appears here waiting to be assigned. + // COALESCE pins NULL grade_code to '' symmetrically on both sides. + const joinClause = ` + FROM cst_item_cons_stk_po s + LEFT JOIN cst_rm_group_detail d + ON d.item_code = s.item_code + AND COALESCE(d.grade_code, '') = COALESCE(s.grade_code, '') + AND d.is_active = true + AND d.deleted_at IS NULL + ` + + var total int64 + countSQL := "SELECT COUNT(*) " + joinClause + " " + where + if err := r.db.QueryRowContext(ctx, countSQL, args...).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("count ungrouped items: %w", err) + } + + offset := (filter.Page - 1) * filter.PageSize + listSQL := ` + SELECT s.period, s.item_code, s.grade_code, s.grade_name, s.item_name, s.uom, + s.cons_qty, s.cons_val, s.cons_rate, + s.stores_qty, s.stores_val, s.stores_rate, + s.dept_qty, s.dept_val, s.dept_rate, + s.last_po_qty1, s.last_po_val1, s.last_po_rate1, s.last_po_dt1, + s.last_po_qty2, s.last_po_val2, s.last_po_rate2, s.last_po_dt2, + s.last_po_qty3, s.last_po_val3, s.last_po_rate3, s.last_po_dt3, + s.synced_at, s.synced_by_job + ` + joinClause + " " + where + + fmt.Sprintf(" ORDER BY s.period DESC, s.item_code, s.grade_code LIMIT $%d OFFSET $%d", idx, idx+1) + args = append(args, filter.PageSize, offset) + + rows, err := r.db.QueryContext(ctx, listSQL, args...) + if err != nil { + return nil, 0, fmt.Errorf("list ungrouped items: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + _ = closeErr + } + }() + + var items []*syncdata.ItemConsStockPO + for rows.Next() { + item, scanErr := r.scanSyncedRow(rows) + if scanErr != nil { + return nil, 0, fmt.Errorf("scan ungrouped row: %w", scanErr) + } + items = append(items, item) + } + if err := rows.Err(); err != nil { + return nil, 0, fmt.Errorf("iterate ungrouped rows: %w", err) + } + + return items, total, nil +} diff --git a/services/finance/internal/infrastructure/rabbitmq/connection.go b/services/finance/internal/infrastructure/rabbitmq/connection.go index 279728a..fcfd971 100644 --- a/services/finance/internal/infrastructure/rabbitmq/connection.go +++ b/services/finance/internal/infrastructure/rabbitmq/connection.go @@ -19,6 +19,10 @@ const ( QueueOracleSync = "finance.jobs.oracle_sync" // RoutingKeyOracleSync is the routing key for oracle sync messages. RoutingKeyOracleSync = "oracle_sync" + // QueueRMCostCalc is the queue for RM landed-cost calculation jobs. + QueueRMCostCalc = "finance.jobs.rm_cost_calc" + // RoutingKeyRMCostCalc is the routing key for RM cost calculation messages. + RoutingKeyRMCostCalc = "rm_cost_calculation" // DeadLetterExchange is the dead letter exchange for failed messages. DeadLetterExchange = "finance.jobs.dlx" // DeadLetterQueue is the dead letter queue. @@ -144,6 +148,14 @@ func (c *Connection) declareTopology() error { return fmt.Errorf("bind oracle sync queue: %w", err) } + // RM cost calculation queue with dead-letter routing. + if _, err := c.channel.QueueDeclare(QueueRMCostCalc, true, false, false, false, args); err != nil { + return fmt.Errorf("declare rm cost calc queue: %w", err) + } + if err := c.channel.QueueBind(QueueRMCostCalc, RoutingKeyRMCostCalc, ExchangeName, false, nil); err != nil { + return fmt.Errorf("bind rm cost calc queue: %w", err) + } + return nil } diff --git a/services/finance/internal/infrastructure/rabbitmq/job_publisher.go b/services/finance/internal/infrastructure/rabbitmq/job_publisher.go index 2a8712e..5e81c67 100644 --- a/services/finance/internal/infrastructure/rabbitmq/job_publisher.go +++ b/services/finance/internal/infrastructure/rabbitmq/job_publisher.go @@ -3,6 +3,7 @@ package rabbitmq import ( "context" + "github.com/google/uuid" "github.com/rs/zerolog" ) @@ -31,3 +32,27 @@ func (a *JobPublisherAdapter) PublishOracleSync(ctx context.Context, jobID strin } return a.publisher.PublishJob(ctx, RoutingKeyOracleSync, msg) } + +// PublishRMCostCalculation publishes an RM landed-cost calculation job message. +// groupHeadID is optional (nil means recalculate every active group for the period). +func (a *JobPublisherAdapter) PublishRMCostCalculation( + ctx context.Context, + jobID string, + period string, + groupHeadID *uuid.UUID, + reason string, + createdBy string, +) error { + msg := JobMessage{ + JobID: jobID, + JobType: "rm_cost_calculation", + Subtype: "landed_cost", + Period: period, + CreatedBy: createdBy, + Reason: reason, + } + if groupHeadID != nil { + msg.GroupHeadID = groupHeadID.String() + } + return a.publisher.PublishJob(ctx, RoutingKeyRMCostCalc, msg) +} diff --git a/services/finance/internal/infrastructure/rabbitmq/publisher.go b/services/finance/internal/infrastructure/rabbitmq/publisher.go index d3f8d7b..d517ba6 100644 --- a/services/finance/internal/infrastructure/rabbitmq/publisher.go +++ b/services/finance/internal/infrastructure/rabbitmq/publisher.go @@ -18,6 +18,10 @@ type JobMessage struct { Subtype string `json:"subtype"` Period string `json:"period"` CreatedBy string `json:"created_by"` + // GroupHeadID scopes rm_cost_calculation jobs to a single group. Empty = all active groups. + GroupHeadID string `json:"group_head_id,omitempty"` + // Reason is the HistoryTriggerReason for rm_cost_calculation jobs. + Reason string `json:"reason,omitempty"` } // Publisher publishes messages to RabbitMQ exchanges. diff --git a/services/finance/migrations/postgres/000010_create_cst_rm_group_head.down.sql b/services/finance/migrations/postgres/000010_create_cst_rm_group_head.down.sql new file mode 100644 index 0000000..84cdc2e --- /dev/null +++ b/services/finance/migrations/postgres/000010_create_cst_rm_group_head.down.sql @@ -0,0 +1,7 @@ +-- Rollback: drop cst_rm_group_head and all its indexes/constraints. + +DROP INDEX IF EXISTS idx_rm_group_head_search; +DROP INDEX IF EXISTS idx_rm_group_head_is_active; +DROP INDEX IF EXISTS uk_rm_group_head_code_active; + +DROP TABLE IF EXISTS cst_rm_group_head; diff --git a/services/finance/migrations/postgres/000010_create_cst_rm_group_head.up.sql b/services/finance/migrations/postgres/000010_create_cst_rm_group_head.up.sql new file mode 100644 index 0000000..fea003c --- /dev/null +++ b/services/finance/migrations/postgres/000010_create_cst_rm_group_head.up.sql @@ -0,0 +1,73 @@ +-- Migration: Create cst_rm_group_head — user-defined groups of raw materials (RMs) +-- that share a landed-cost configuration (percentage, per-kg overhead, flags per purpose). + +CREATE TABLE IF NOT EXISTS cst_rm_group_head ( + group_head_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Identification. + group_code VARCHAR(30) NOT NULL, + group_name VARCHAR(200) NOT NULL, + description TEXT, + colourant VARCHAR(30), + ci_name VARCHAR(30), + + -- Cost formula inputs. + cost_percentage DECIMAL(20,6) NOT NULL DEFAULT 0, + cost_per_kg DECIMAL(20,6) NOT NULL DEFAULT 0, + + -- Flag selects which stage rate is used for each purpose. + flag_valuation VARCHAR(20) NOT NULL DEFAULT 'CONS', + flag_marketing VARCHAR(20) NOT NULL DEFAULT 'CONS', + flag_simulation VARCHAR(20) NOT NULL DEFAULT 'CONS', + + -- INIT override values — used when the corresponding flag is 'INIT'. + init_val_valuation DECIMAL(20,6), + init_val_marketing DECIMAL(20,6), + init_val_simulation DECIMAL(20,6), + + -- Lifecycle. + is_active BOOLEAN NOT NULL DEFAULT true, + + -- Audit. + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by VARCHAR(100) NOT NULL, + updated_at TIMESTAMPTZ, + updated_by VARCHAR(100), + deleted_at TIMESTAMPTZ, + deleted_by VARCHAR(100), + + -- Flag value domain. + CONSTRAINT chk_rm_group_flag_valuation CHECK (flag_valuation IN ('CONS','STORES','DEPT','PO_1','PO_2','PO_3','INIT')), + CONSTRAINT chk_rm_group_flag_marketing CHECK (flag_marketing IN ('CONS','STORES','DEPT','PO_1','PO_2','PO_3','INIT')), + CONSTRAINT chk_rm_group_flag_simulation CHECK (flag_simulation IN ('CONS','STORES','DEPT','PO_1','PO_2','PO_3','INIT')), + + -- Non-negative cost inputs. + CONSTRAINT chk_rm_group_cost_percentage_nonneg CHECK (cost_percentage >= 0), + CONSTRAINT chk_rm_group_cost_per_kg_nonneg CHECK (cost_per_kg >= 0), + + -- When a flag is INIT, the corresponding init value MUST be set. + CONSTRAINT chk_rm_group_init_val_valuation CHECK (flag_valuation <> 'INIT' OR init_val_valuation IS NOT NULL), + CONSTRAINT chk_rm_group_init_val_marketing CHECK (flag_marketing <> 'INIT' OR init_val_marketing IS NOT NULL), + CONSTRAINT chk_rm_group_init_val_simulation CHECK (flag_simulation <> 'INIT' OR init_val_simulation IS NOT NULL), + + -- Group code format: uppercase alphanumeric with optional spaces and hyphens, + -- must start with alphanumeric, max 30 chars. Real examples: 'BLUE MGTS-5109', + -- 'PIG0000005-COM', 'CHM0000118'. + CONSTRAINT chk_rm_group_code_format CHECK ( + group_code ~ '^[A-Z0-9][A-Z0-9 \-]{0,29}$' + ) +); + +COMMENT ON TABLE cst_rm_group_head IS 'User-defined groups of raw materials sharing a landed-cost configuration.'; + +-- Active group_code must be unique (soft-deleted rows may reuse the code). +CREATE UNIQUE INDEX IF NOT EXISTS uk_rm_group_head_code_active + ON cst_rm_group_head (group_code) WHERE deleted_at IS NULL; + +-- Filter active groups quickly. +CREATE INDEX IF NOT EXISTS idx_rm_group_head_is_active + ON cst_rm_group_head (is_active) WHERE deleted_at IS NULL; + +-- Full-text search on code + name for UI pickers. +CREATE INDEX IF NOT EXISTS idx_rm_group_head_search + ON cst_rm_group_head USING gin (to_tsvector('simple', group_code || ' ' || group_name)); diff --git a/services/finance/migrations/postgres/000011_create_cst_rm_group_detail.down.sql b/services/finance/migrations/postgres/000011_create_cst_rm_group_detail.down.sql new file mode 100644 index 0000000..39351f9 --- /dev/null +++ b/services/finance/migrations/postgres/000011_create_cst_rm_group_detail.down.sql @@ -0,0 +1,7 @@ +-- Rollback: drop cst_rm_group_detail and all its indexes. + +DROP INDEX IF EXISTS idx_rm_group_detail_item; +DROP INDEX IF EXISTS idx_rm_group_detail_head; +DROP INDEX IF EXISTS uk_rm_group_detail_item_active; + +DROP TABLE IF EXISTS cst_rm_group_detail; diff --git a/services/finance/migrations/postgres/000011_create_cst_rm_group_detail.up.sql b/services/finance/migrations/postgres/000011_create_cst_rm_group_detail.up.sql new file mode 100644 index 0000000..7bfee39 --- /dev/null +++ b/services/finance/migrations/postgres/000011_create_cst_rm_group_detail.up.sql @@ -0,0 +1,50 @@ +-- Migration: Create cst_rm_group_detail — items assigned to an RM group. +-- Rule: one item_code may belong to AT MOST ONE active group (enforced via partial unique index). + +CREATE TABLE IF NOT EXISTS cst_rm_group_detail ( + group_detail_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + group_head_id UUID NOT NULL REFERENCES cst_rm_group_head(group_head_id) ON DELETE RESTRICT, + + -- Item identification (mirrors cst_item_cons_stk_po). + item_code VARCHAR(20) NOT NULL, + item_name VARCHAR(200), + item_type_code VARCHAR(30), + grade_code VARCHAR(40), + item_grade VARCHAR(30), + uom_code VARCHAR(12), + + -- Marketing breakdown (optional per-item contribution within group). + market_percentage DECIMAL(20,6), + market_value_rp DECIMAL(20,6), + + -- Ordering + flags. + sort_order INT NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT true, + is_dummy BOOLEAN NOT NULL DEFAULT false, + + -- Audit. + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by VARCHAR(100) NOT NULL, + updated_at TIMESTAMPTZ, + updated_by VARCHAR(100), + deleted_at TIMESTAMPTZ, + deleted_by VARCHAR(100), + + CONSTRAINT chk_rm_group_detail_market_percentage_nonneg CHECK (market_percentage IS NULL OR market_percentage >= 0), + CONSTRAINT chk_rm_group_detail_market_value_nonneg CHECK (market_value_rp IS NULL OR market_value_rp >= 0) +); + +COMMENT ON TABLE cst_rm_group_detail IS 'Items (RMs) assigned to an RM group. One item_code may belong to at most one active group.'; + +-- One item per active group — enforce "1 item, 1 group" business rule at DB layer. +CREATE UNIQUE INDEX IF NOT EXISTS uk_rm_group_detail_item_active + ON cst_rm_group_detail (item_code) + WHERE deleted_at IS NULL AND is_active = true; + +-- Fast lookups by group. +CREATE INDEX IF NOT EXISTS idx_rm_group_detail_head + ON cst_rm_group_detail (group_head_id) WHERE deleted_at IS NULL; + +-- Fast lookups by item (e.g., "which group does item X belong to?"). +CREATE INDEX IF NOT EXISTS idx_rm_group_detail_item + ON cst_rm_group_detail (item_code) WHERE deleted_at IS NULL; diff --git a/services/finance/migrations/postgres/000012_create_cst_rm_cost.down.sql b/services/finance/migrations/postgres/000012_create_cst_rm_cost.down.sql new file mode 100644 index 0000000..fffa791 --- /dev/null +++ b/services/finance/migrations/postgres/000012_create_cst_rm_cost.down.sql @@ -0,0 +1,8 @@ +-- Rollback: drop cst_rm_cost and all its indexes. + +DROP INDEX IF EXISTS idx_rm_cost_calculated_at; +DROP INDEX IF EXISTS idx_rm_cost_group_head; +DROP INDEX IF EXISTS idx_rm_cost_period; +DROP INDEX IF EXISTS uk_rm_cost_period_rm; + +DROP TABLE IF EXISTS cst_rm_cost; diff --git a/services/finance/migrations/postgres/000012_create_cst_rm_cost.up.sql b/services/finance/migrations/postgres/000012_create_cst_rm_cost.up.sql new file mode 100644 index 0000000..55a3eee --- /dev/null +++ b/services/finance/migrations/postgres/000012_create_cst_rm_cost.up.sql @@ -0,0 +1,75 @@ +-- Migration: Create cst_rm_cost — the result table for per-period RM landed cost. +-- UPSERTed (period, rm_code) by the calculation worker. + +CREATE TABLE IF NOT EXISTS cst_rm_cost ( + rm_cost_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Identity of the cost row. + period VARCHAR(6) NOT NULL, -- YYYYMM + rm_code VARCHAR(30) NOT NULL, -- group_code (phase 1) or item_code (phase 2) + rm_type VARCHAR(20) NOT NULL, -- 'GROUP' or 'ITEM' + group_head_id UUID REFERENCES cst_rm_group_head(group_head_id) ON DELETE SET NULL, + item_code VARCHAR(20), -- NULL when rm_type='GROUP' + rm_name VARCHAR(200), + uom_code VARCHAR(12), + + -- Aggregated per-stage rates (SUM(val)/SUM(qty) across the group's active items). + cons_rate DECIMAL(20,6), + stores_rate DECIMAL(20,6), + dept_rate DECIMAL(20,6), + po_rate_1 DECIMAL(20,6), + po_rate_2 DECIMAL(20,6), + po_rate_3 DECIMAL(20,6), + + -- Landed costs per purpose (raw value; UI layer formats for display). + cost_val DECIMAL(20,6), + cost_mark DECIMAL(20,6), + cost_sim DECIMAL(20,6), + + -- Snapshot of flags as configured on the header at calculation time. + flag_valuation VARCHAR(20) NOT NULL, + flag_marketing VARCHAR(20) NOT NULL, + flag_simulation VARCHAR(20) NOT NULL, + + -- Flag actually used after cascade resolution (differs from requested when cascade triggered). + flag_valuation_used VARCHAR(20) NOT NULL, + flag_marketing_used VARCHAR(20) NOT NULL, + flag_simulation_used VARCHAR(20) NOT NULL, + + -- Traceability. + calculated_at TIMESTAMPTZ, + calculated_by VARCHAR(100), + + -- Audit. + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by VARCHAR(100) NOT NULL, + updated_at TIMESTAMPTZ, + updated_by VARCHAR(100), + + CONSTRAINT chk_rm_cost_rm_type CHECK (rm_type IN ('GROUP','ITEM')), + CONSTRAINT chk_rm_cost_period_format CHECK (period ~ '^[0-9]{6}$'), + CONSTRAINT chk_rm_cost_flag_valuation CHECK (flag_valuation IN ('CONS','STORES','DEPT','PO_1','PO_2','PO_3','INIT')), + CONSTRAINT chk_rm_cost_flag_marketing CHECK (flag_marketing IN ('CONS','STORES','DEPT','PO_1','PO_2','PO_3','INIT')), + CONSTRAINT chk_rm_cost_flag_simulation CHECK (flag_simulation IN ('CONS','STORES','DEPT','PO_1','PO_2','PO_3','INIT')), + CONSTRAINT chk_rm_cost_flag_valuation_used CHECK (flag_valuation_used IN ('CONS','STORES','DEPT','PO_1','PO_2','PO_3','INIT')), + CONSTRAINT chk_rm_cost_flag_marketing_used CHECK (flag_marketing_used IN ('CONS','STORES','DEPT','PO_1','PO_2','PO_3','INIT')), + CONSTRAINT chk_rm_cost_flag_simulation_used CHECK (flag_simulation_used IN ('CONS','STORES','DEPT','PO_1','PO_2','PO_3','INIT')) +); + +COMMENT ON TABLE cst_rm_cost IS 'Per-period landed cost per RM (group in phase 1). UPSERTed by calculation worker on (period, rm_code).'; + +-- UPSERT target. +CREATE UNIQUE INDEX IF NOT EXISTS uk_rm_cost_period_rm + ON cst_rm_cost (period, rm_code); + +-- Period filtering (most listing queries filter by period). +CREATE INDEX IF NOT EXISTS idx_rm_cost_period + ON cst_rm_cost (period); + +-- Lookup cost history by group. +CREATE INDEX IF NOT EXISTS idx_rm_cost_group_head + ON cst_rm_cost (group_head_id) WHERE group_head_id IS NOT NULL; + +-- Most-recently-calculated listing. +CREATE INDEX IF NOT EXISTS idx_rm_cost_calculated_at + ON cst_rm_cost (calculated_at DESC); diff --git a/services/finance/migrations/postgres/000013_extend_job_execution_for_rmcost.down.sql b/services/finance/migrations/postgres/000013_extend_job_execution_for_rmcost.down.sql new file mode 100644 index 0000000..7994702 --- /dev/null +++ b/services/finance/migrations/postgres/000013_extend_job_execution_for_rmcost.down.sql @@ -0,0 +1,6 @@ +-- Rollback: drop chk_job_type and the type/status index. + +DROP INDEX IF EXISTS idx_job_execution_type_status; + +ALTER TABLE job_execution + DROP CONSTRAINT IF EXISTS chk_job_type; diff --git a/services/finance/migrations/postgres/000013_extend_job_execution_for_rmcost.up.sql b/services/finance/migrations/postgres/000013_extend_job_execution_for_rmcost.up.sql new file mode 100644 index 0000000..cac4f77 --- /dev/null +++ b/services/finance/migrations/postgres/000013_extend_job_execution_for_rmcost.up.sql @@ -0,0 +1,21 @@ +-- Migration: Add chk_job_type to job_execution and include new rm_cost_calculation type. +-- Context: 000008 created job_execution without a job_type whitelist; the Oracle sync +-- feature stores the lowercase token 'oracle_sync' (see job.TypeOracleSync in Go). We +-- now formalize the allowed set and add 'rm_cost_calculation' for the raw-material +-- landed-cost calculation job. Values are lowercase to match the domain constants. + +-- DROP IF EXISTS keeps this migration idempotent in case chk_job_type was added ad-hoc. +ALTER TABLE job_execution + DROP CONSTRAINT IF EXISTS chk_job_type; + +-- Normalize any legacy uppercase values written before the lowercase constants +-- were introduced (early Oracle-sync runs stored 'ORACLE_SYNC'). +UPDATE job_execution SET job_type = LOWER(job_type) WHERE job_type <> LOWER(job_type); + +ALTER TABLE job_execution + ADD CONSTRAINT chk_job_type + CHECK (job_type IN ('oracle_sync', 'rm_cost_calculation')); + +-- Composite index speeds up filtering the job history page by type + status. +CREATE INDEX IF NOT EXISTS idx_job_execution_type_status + ON job_execution (job_type, status); diff --git a/services/finance/migrations/postgres/000014_create_aud_rm_cost_history.down.sql b/services/finance/migrations/postgres/000014_create_aud_rm_cost_history.down.sql new file mode 100644 index 0000000..25c438d --- /dev/null +++ b/services/finance/migrations/postgres/000014_create_aud_rm_cost_history.down.sql @@ -0,0 +1,7 @@ +-- Rollback: drop aud_rm_cost_history and its indexes. + +DROP INDEX IF EXISTS idx_aud_rm_cost_group; +DROP INDEX IF EXISTS idx_aud_rm_cost_job; +DROP INDEX IF EXISTS idx_aud_rm_cost_period_rm; + +DROP TABLE IF EXISTS aud_rm_cost_history; diff --git a/services/finance/migrations/postgres/000014_create_aud_rm_cost_history.up.sql b/services/finance/migrations/postgres/000014_create_aud_rm_cost_history.up.sql new file mode 100644 index 0000000..f7f6377 --- /dev/null +++ b/services/finance/migrations/postgres/000014_create_aud_rm_cost_history.up.sql @@ -0,0 +1,67 @@ +-- Migration: Create aud_rm_cost_history — append-only audit trail for every RM cost calculation. +-- Written in the SAME transaction as the cst_rm_cost UPSERT so runs are traceable and diffable. + +CREATE TABLE IF NOT EXISTS aud_rm_cost_history ( + history_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Links to produced cost row + job (both nullable: cost row may be NULL if UPSERT fails + -- after snapshot was captured, and job_id may be NULL for direct-call calculations). + rm_cost_id UUID, + job_id UUID REFERENCES job_execution(job_id) ON DELETE SET NULL, + + -- Identity (copied for standalone queryability). + period VARCHAR(6) NOT NULL, + rm_code VARCHAR(30) NOT NULL, + rm_type VARCHAR(20) NOT NULL, + group_head_id UUID, + + -- Snapshot: pre-cascade per-stage rates aggregated from source data. + cons_rate DECIMAL(20,6), + stores_rate DECIMAL(20,6), + dept_rate DECIMAL(20,6), + po_rate_1 DECIMAL(20,6), + po_rate_2 DECIMAL(20,6), + po_rate_3 DECIMAL(20,6), + + -- Snapshot: header configuration used for this calculation. + cost_percentage DECIMAL(20,6) NOT NULL, + cost_per_kg DECIMAL(20,6) NOT NULL, + flag_valuation VARCHAR(20) NOT NULL, + flag_marketing VARCHAR(20) NOT NULL, + flag_simulation VARCHAR(20) NOT NULL, + init_val_valuation DECIMAL(20,6), + init_val_marketing DECIMAL(20,6), + init_val_simulation DECIMAL(20,6), + + -- Snapshot: computed outputs. + cost_val DECIMAL(20,6), + cost_mark DECIMAL(20,6), + cost_sim DECIMAL(20,6), + flag_valuation_used VARCHAR(20) NOT NULL, + flag_marketing_used VARCHAR(20) NOT NULL, + flag_simulation_used VARCHAR(20) NOT NULL, + + -- Context for reproducibility / debugging. + source_item_count INT NOT NULL DEFAULT 0, + trigger_reason VARCHAR(50) NOT NULL, -- 'oracle-sync-chain' | 'group-update' | 'detail-change' | 'manual-ui' | 'cron' + calculated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + calculated_by VARCHAR(100) NOT NULL, + + CONSTRAINT chk_aud_rm_cost_rm_type CHECK (rm_type IN ('GROUP','ITEM')), + CONSTRAINT chk_aud_rm_cost_period_format CHECK (period ~ '^[0-9]{6}$'), + CONSTRAINT chk_aud_rm_cost_source_item_count_nonneg CHECK (source_item_count >= 0) +); + +COMMENT ON TABLE aud_rm_cost_history IS 'Append-only audit trail: every RM cost calculation writes one row here (same transaction as cst_rm_cost UPSERT).'; + +-- Lookup "show me all runs for this cost row" (audit drawer). +CREATE INDEX IF NOT EXISTS idx_aud_rm_cost_period_rm + ON aud_rm_cost_history (period, rm_code, calculated_at DESC); + +-- Lookup by job_id for "what did this job produce?". +CREATE INDEX IF NOT EXISTS idx_aud_rm_cost_job + ON aud_rm_cost_history (job_id) WHERE job_id IS NOT NULL; + +-- Lookup by group for per-group recalc timeline. +CREATE INDEX IF NOT EXISTS idx_aud_rm_cost_group + ON aud_rm_cost_history (group_head_id, calculated_at DESC) WHERE group_head_id IS NOT NULL; diff --git a/services/finance/migrations/postgres/000015_widen_rm_group_detail_text_cols.down.sql b/services/finance/migrations/postgres/000015_widen_rm_group_detail_text_cols.down.sql new file mode 100644 index 0000000..7918604 --- /dev/null +++ b/services/finance/migrations/postgres/000015_widen_rm_group_detail_text_cols.down.sql @@ -0,0 +1,7 @@ +-- Rollback: restore the original widths. This may fail if rows already contain +-- values longer than the old limits; in that case truncate or clean up first. + +ALTER TABLE cst_rm_group_detail + ALTER COLUMN item_name TYPE VARCHAR(200), + ALTER COLUMN item_type_code TYPE VARCHAR(30), + ALTER COLUMN item_grade TYPE VARCHAR(30); diff --git a/services/finance/migrations/postgres/000015_widen_rm_group_detail_text_cols.up.sql b/services/finance/migrations/postgres/000015_widen_rm_group_detail_text_cols.up.sql new file mode 100644 index 0000000..c8749e4 --- /dev/null +++ b/services/finance/migrations/postgres/000015_widen_rm_group_detail_text_cols.up.sql @@ -0,0 +1,9 @@ +-- Migration: widen text columns on cst_rm_group_detail to match the source +-- cst_item_cons_stk_po (whose grade_name / item_name are VARCHAR(240)). The +-- previous VARCHAR(30) on item_grade triggered 22001 overflow when operators +-- imported items whose Oracle-provided grade description exceeded 30 chars. + +ALTER TABLE cst_rm_group_detail + ALTER COLUMN item_name TYPE VARCHAR(240), + ALTER COLUMN item_type_code TYPE VARCHAR(60), + ALTER COLUMN item_grade TYPE VARCHAR(240); diff --git a/services/finance/migrations/postgres/000016_create_aud_rm_group.down.sql b/services/finance/migrations/postgres/000016_create_aud_rm_group.down.sql new file mode 100644 index 0000000..2cc159c --- /dev/null +++ b/services/finance/migrations/postgres/000016_create_aud_rm_group.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS aud_rm_group_detail; +DROP TABLE IF EXISTS aud_rm_group; diff --git a/services/finance/migrations/postgres/000016_create_aud_rm_group.up.sql b/services/finance/migrations/postgres/000016_create_aud_rm_group.up.sql new file mode 100644 index 0000000..3aac59c --- /dev/null +++ b/services/finance/migrations/postgres/000016_create_aud_rm_group.up.sql @@ -0,0 +1,77 @@ +-- Migration: Create aud_rm_group + aud_rm_group_detail — append-only audit trail +-- for RM group head and detail mutations. Every create/update/delete records a +-- full snapshot of the row as it appeared after the operation, together with +-- the actor and the action type. + +CREATE TABLE IF NOT EXISTS aud_rm_group ( + history_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + group_head_id UUID NOT NULL, + action VARCHAR(20) NOT NULL, + + -- Snapshot of head fields after the operation. + group_code VARCHAR(30) NOT NULL, + group_name VARCHAR(200) NOT NULL, + description TEXT, + colourant VARCHAR(30), + ci_name VARCHAR(30), + cost_percentage DECIMAL(20,6) NOT NULL DEFAULT 0, + cost_per_kg DECIMAL(20,6) NOT NULL DEFAULT 0, + flag_valuation VARCHAR(20) NOT NULL, + flag_marketing VARCHAR(20) NOT NULL, + flag_simulation VARCHAR(20) NOT NULL, + init_val_valuation DECIMAL(20,6), + init_val_marketing DECIMAL(20,6), + init_val_simulation DECIMAL(20,6), + is_active BOOLEAN NOT NULL, + + -- Actor + timestamp. + changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + changed_by VARCHAR(100) NOT NULL, + + CONSTRAINT chk_aud_rm_group_action CHECK (action IN ('CREATE','UPDATE','DELETE')) +); + +COMMENT ON TABLE aud_rm_group IS 'Append-only audit log of cst_rm_group_head mutations. Each row captures a snapshot of head fields after the operation.'; + +CREATE INDEX IF NOT EXISTS idx_aud_rm_group_head + ON aud_rm_group (group_head_id, changed_at DESC); + +CREATE INDEX IF NOT EXISTS idx_aud_rm_group_changed_at + ON aud_rm_group (changed_at DESC); + +CREATE TABLE IF NOT EXISTS aud_rm_group_detail ( + history_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + group_detail_id UUID NOT NULL, + group_head_id UUID NOT NULL, + action VARCHAR(20) NOT NULL, + + -- Snapshot of detail fields after the operation. + item_code VARCHAR(20) NOT NULL, + item_name VARCHAR(240), + item_type_code VARCHAR(60), + grade_code VARCHAR(40), + item_grade VARCHAR(240), + uom_code VARCHAR(12), + market_percentage DECIMAL(20,6), + market_value_rp DECIMAL(20,6), + sort_order INT NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL, + is_dummy BOOLEAN NOT NULL, + + -- Actor + timestamp. + changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + changed_by VARCHAR(100) NOT NULL, + + CONSTRAINT chk_aud_rm_group_detail_action CHECK (action IN ('CREATE','UPDATE','DELETE')) +); + +COMMENT ON TABLE aud_rm_group_detail IS 'Append-only audit log of cst_rm_group_detail mutations. Each row captures a snapshot of detail fields after the operation.'; + +CREATE INDEX IF NOT EXISTS idx_aud_rm_group_detail_detail + ON aud_rm_group_detail (group_detail_id, changed_at DESC); + +CREATE INDEX IF NOT EXISTS idx_aud_rm_group_detail_head + ON aud_rm_group_detail (group_head_id, changed_at DESC); + +CREATE INDEX IF NOT EXISTS idx_aud_rm_group_detail_changed_at + ON aud_rm_group_detail (changed_at DESC); diff --git a/services/finance/migrations/postgres/000017_widen_rm_group_text_cols_defensive.down.sql b/services/finance/migrations/postgres/000017_widen_rm_group_text_cols_defensive.down.sql new file mode 100644 index 0000000..f38240c --- /dev/null +++ b/services/finance/migrations/postgres/000017_widen_rm_group_text_cols_defensive.down.sql @@ -0,0 +1,16 @@ +-- Revert defensive widenings applied in 000017. Narrowing a VARCHAR can fail +-- if existing rows exceed the target width; callers must clean up before +-- rolling back. + +ALTER TABLE aud_rm_group_detail + ALTER COLUMN grade_code TYPE VARCHAR(40), + ALTER COLUMN changed_by TYPE VARCHAR(100); + +ALTER TABLE cst_rm_group_detail + ALTER COLUMN item_name TYPE VARCHAR(200), + ALTER COLUMN item_type_code TYPE VARCHAR(30), + ALTER COLUMN item_grade TYPE VARCHAR(30), + ALTER COLUMN grade_code TYPE VARCHAR(40), + ALTER COLUMN created_by TYPE VARCHAR(100), + ALTER COLUMN updated_by TYPE VARCHAR(100), + ALTER COLUMN deleted_by TYPE VARCHAR(100); diff --git a/services/finance/migrations/postgres/000017_widen_rm_group_text_cols_defensive.up.sql b/services/finance/migrations/postgres/000017_widen_rm_group_text_cols_defensive.up.sql new file mode 100644 index 0000000..8fb68e9 --- /dev/null +++ b/services/finance/migrations/postgres/000017_widen_rm_group_text_cols_defensive.up.sql @@ -0,0 +1,27 @@ +-- Migration: defensively re-assert width on cst_rm_group_detail text columns. +-- +-- Context: operators hit a "value too long for type character varying(30)" +-- (SQLSTATE 22001) error when adding items to a freshly re-created group. +-- Migration 000015 already widened item_name, item_type_code and item_grade, +-- but this re-asserts the widening idempotently so the error cannot reappear +-- on databases where 000015 got stuck as dirty or was never applied. +-- ALTER TABLE ... TYPE to a wider VARCHAR is a no-op when the column is +-- already at least that wide, so this migration is safe to run on any DB. +-- +-- Also widens grade_code to 60 (source is 40) to give margin for future data, +-- and widens *_by audit columns to 150 so longer emails fit. + +ALTER TABLE cst_rm_group_detail + ALTER COLUMN item_name TYPE VARCHAR(240), + ALTER COLUMN item_type_code TYPE VARCHAR(60), + ALTER COLUMN item_grade TYPE VARCHAR(240), + ALTER COLUMN grade_code TYPE VARCHAR(60), + ALTER COLUMN created_by TYPE VARCHAR(150), + ALTER COLUMN updated_by TYPE VARCHAR(150), + ALTER COLUMN deleted_by TYPE VARCHAR(150); + +-- aud_rm_group_detail: match detail widenings so audit rows do not overflow +-- when the detail row fits. +ALTER TABLE aud_rm_group_detail + ALTER COLUMN grade_code TYPE VARCHAR(60), + ALTER COLUMN changed_by TYPE VARCHAR(150); diff --git a/services/finance/migrations/postgres/000018_rm_group_detail_grade_unique.down.sql b/services/finance/migrations/postgres/000018_rm_group_detail_grade_unique.down.sql new file mode 100644 index 0000000..5fd5909 --- /dev/null +++ b/services/finance/migrations/postgres/000018_rm_group_detail_grade_unique.down.sql @@ -0,0 +1,12 @@ +-- Revert the composite (item_code, grade_code) uniqueness back to item_code +-- only. Dropping rows that would conflict is the caller's responsibility. + +DROP INDEX IF EXISTS uk_rm_group_detail_item_grade_active; +DROP INDEX IF EXISTS idx_rm_group_detail_item_grade; + +CREATE UNIQUE INDEX IF NOT EXISTS uk_rm_group_detail_item_active + ON cst_rm_group_detail (item_code) + WHERE deleted_at IS NULL AND is_active = true; + +CREATE INDEX IF NOT EXISTS idx_rm_group_detail_item + ON cst_rm_group_detail (item_code) WHERE deleted_at IS NULL; diff --git a/services/finance/migrations/postgres/000018_rm_group_detail_grade_unique.up.sql b/services/finance/migrations/postgres/000018_rm_group_detail_grade_unique.up.sql new file mode 100644 index 0000000..68a6213 --- /dev/null +++ b/services/finance/migrations/postgres/000018_rm_group_detail_grade_unique.up.sql @@ -0,0 +1,29 @@ +-- Migration: evolve the "one item per active group" rule to include grade_code. +-- +-- Context: the Oracle sync feed (cst_item_cons_stk_po) keys items on +-- (item_code, grade_code) — the SAME item_code can have multiple grade +-- variants with different qty/val/rate snapshots. The original grouping rule +-- collapsed all variants into one row, which caused: +-- 1) Picker displayed four rows, user picked the "enriched" variant, save +-- used the wrong variant's metadata (arbitrary first-row). +-- 2) After grouping one variant, the other variants disappeared from the +-- "Ungrouped Items" report. +-- +-- Fix: treat (item_code, grade_code) as the business natural key for +-- grouping. A NULL/empty grade_code behaves as its own variant. The partial +-- unique index is rebuilt accordingly. + +DROP INDEX IF EXISTS uk_rm_group_detail_item_active; + +-- COALESCE pins a NULL grade_code to the empty string so a single +-- no-grade variant still has exactly one active row. Expression indexes +-- require IMMUTABLE functions, and COALESCE with a literal qualifies. +CREATE UNIQUE INDEX IF NOT EXISTS uk_rm_group_detail_item_grade_active + ON cst_rm_group_detail (item_code, COALESCE(grade_code, '')) + WHERE deleted_at IS NULL AND is_active = true; + +-- Replace the item-only lookup index with a composite (item_code, grade_code) +-- index to serve the new natural-key queries efficiently. +DROP INDEX IF EXISTS idx_rm_group_detail_item; +CREATE INDEX IF NOT EXISTS idx_rm_group_detail_item_grade + ON cst_rm_group_detail (item_code, grade_code) WHERE deleted_at IS NULL; diff --git a/services/finance/worker b/services/finance/worker new file mode 100755 index 0000000..2fe61c7 Binary files /dev/null and b/services/finance/worker differ diff --git a/services/iam/migrations/postgres/000026_seed_rm_grouping_menus.down.sql b/services/iam/migrations/postgres/000026_seed_rm_grouping_menus.down.sql new file mode 100644 index 0000000..4ebbfca --- /dev/null +++ b/services/iam/migrations/postgres/000026_seed_rm_grouping_menus.down.sql @@ -0,0 +1,53 @@ +-- Rollback: remove RM Pricing menus, permissions, and role assignments. +-- Also restore chk_permission_action to its pre-migration state (removes 'recalculate'). + +-- ============================================================================= +-- REMOVE ROLE ASSIGNMENTS +-- ============================================================================= + +DELETE FROM role_permissions +WHERE permission_id IN ( + SELECT permission_id FROM mst_permission + WHERE permission_code LIKE 'finance.rmpricing.%' +); + +-- ============================================================================= +-- REMOVE MENU PERMISSIONS +-- ============================================================================= + +DELETE FROM menu_permissions +WHERE menu_id IN ( + '00000000-0000-0000-0003-000000000012', + '00000000-0000-0000-0003-000000000013', + '00000000-0000-0000-0003-000000000014' +); + +-- ============================================================================= +-- REMOVE MENUS +-- ============================================================================= + +DELETE FROM mst_menu +WHERE menu_id IN ( + '00000000-0000-0000-0003-000000000012', + '00000000-0000-0000-0003-000000000013', + '00000000-0000-0000-0003-000000000014', + '00000000-0000-0000-0002-000000000014' +); + +-- ============================================================================= +-- REMOVE PERMISSIONS +-- ============================================================================= + +DELETE FROM mst_permission +WHERE permission_code LIKE 'finance.rmpricing.%'; + +-- ============================================================================= +-- RESTORE chk_permission_action — remove 'recalculate' +-- ============================================================================= + +ALTER TABLE mst_permission DROP CONSTRAINT IF EXISTS chk_permission_action; +ALTER TABLE mst_permission ADD CONSTRAINT chk_permission_action + CHECK (action_type IN ( + 'view', 'create', 'update', 'delete', 'export', 'import', + 'submit', 'approve', 'release', 'bypass' + )); diff --git a/services/iam/migrations/postgres/000026_seed_rm_grouping_menus.up.sql b/services/iam/migrations/postgres/000026_seed_rm_grouping_menus.up.sql new file mode 100644 index 0000000..8af2e36 --- /dev/null +++ b/services/iam/migrations/postgres/000026_seed_rm_grouping_menus.up.sql @@ -0,0 +1,119 @@ +-- IAM Service Database Migrations +-- 000026: Seed RM Grouping (RM Pricing) menu entries and permissions. +-- +-- Adds "RM Pricing" parent menu under Finance, with three children: +-- - RM Groups → /finance/rm-pricing/groups +-- - RM Costs → /finance/rm-pricing/costs +-- - Ungrouped Items → /finance/rm-pricing/ungrouped +-- +-- Seeds 11 granular permissions (grouphead/groupdetail/cost/ungrouped split). +-- Extends chk_permission_action to include 'recalculate' for the manual recalc permission. +-- All inserts use ON CONFLICT DO NOTHING for idempotency. + +-- ============================================================================= +-- EXTEND chk_permission_action — allow 'recalculate' action +-- ============================================================================= + +ALTER TABLE mst_permission DROP CONSTRAINT IF EXISTS chk_permission_action; +ALTER TABLE mst_permission ADD CONSTRAINT chk_permission_action + CHECK (action_type IN ( + 'view', 'create', 'update', 'delete', 'export', 'import', + 'submit', 'approve', 'release', 'bypass', + 'recalculate' + )); + +-- ============================================================================= +-- PERMISSIONS — finance.rmpricing.{grouphead|groupdetail|cost|ungrouped}.* +-- Permission code format constraint: ^[a-z][a-z0-9]*\.[a-z][a-z0-9]*\.[a-z][a-z0-9]*\.[a-z]+$ +-- (no underscores or hyphens in segments; multi-word entities concatenated: grouphead, groupdetail, rmpricing) +-- ============================================================================= + +INSERT INTO mst_permission (permission_code, permission_name, description, service_name, module_name, action_type, is_active, created_by) +VALUES + -- Group header CRUD + ('finance.rmpricing.grouphead.view', 'View RM Group Headers', 'List and view RM group headers', 'finance', 'rmpricing', 'view', true, 'seed'), + ('finance.rmpricing.grouphead.create', 'Create RM Group Header', 'Create new RM group headers', 'finance', 'rmpricing', 'create', true, 'seed'), + ('finance.rmpricing.grouphead.update', 'Update RM Group Header', 'Update RM group header fields (flags, costs)', 'finance', 'rmpricing', 'update', true, 'seed'), + ('finance.rmpricing.grouphead.delete', 'Delete RM Group Header', 'Soft-delete RM group headers', 'finance', 'rmpricing', 'delete', true, 'seed'), + + -- Group detail (items in group) CRUD + ('finance.rmpricing.groupdetail.view', 'View RM Group Items', 'View items assigned to RM groups', 'finance', 'rmpricing', 'view', true, 'seed'), + ('finance.rmpricing.groupdetail.create', 'Add Items to RM Group', 'Add items to RM groups', 'finance', 'rmpricing', 'create', true, 'seed'), + ('finance.rmpricing.groupdetail.update', 'Update RM Group Items', 'Update RM group item fields (activate, etc.)', 'finance', 'rmpricing', 'update', true, 'seed'), + ('finance.rmpricing.groupdetail.delete', 'Remove Items from Group', 'Remove items from RM groups', 'finance', 'rmpricing', 'delete', true, 'seed'), + + -- Cost view + manual recalc + ('finance.rmpricing.cost.view', 'View RM Costs', 'View calculated RM landed costs per period', 'finance', 'rmpricing', 'view', true, 'seed'), + ('finance.rmpricing.cost.recalculate', 'Recalculate RM Costs', 'Manually trigger RM cost recalculation', 'finance', 'rmpricing', 'recalculate', true, 'seed'), + + -- Ungrouped items report + ('finance.rmpricing.ungrouped.view', 'View Ungrouped Items', 'View RMs not yet assigned to any group', 'finance', 'rmpricing', 'view', true, 'seed') +ON CONFLICT (permission_code) DO NOTHING; + +-- ============================================================================= +-- MENUS — Finance > RM Pricing > (Groups | Costs | Ungrouped) +-- +-- Parent: 00000000-0000-0000-0002-000000000014 FINANCE_RM_PRICING (LEVEL 2, next available) +-- Children: 00000000-0000-0000-0003-000000000012 FINANCE_RM_GROUPS +-- 00000000-0000-0000-0003-000000000013 FINANCE_RM_COSTS +-- 00000000-0000-0000-0003-000000000014 FINANCE_UNGROUPED_RM +-- ============================================================================= + +INSERT INTO mst_menu (menu_id, parent_id, menu_code, menu_title, menu_url, icon_name, service_name, menu_level, sort_order, is_visible, is_active, created_by) +VALUES + -- Parent (section header under Finance) + ('00000000-0000-0000-0002-000000000014', '00000000-0000-0000-0001-000000000002', 'FINANCE_RM_PRICING', + 'RM Pricing', NULL, 'Layers', 'finance', 2, 40, true, true, 'seed'), + + -- Children + ('00000000-0000-0000-0003-000000000012', '00000000-0000-0000-0002-000000000014', 'FINANCE_RM_GROUPS', + 'RM Groups', '/finance/rm-pricing/groups', 'FolderTree', 'finance', 3, 10, true, true, 'seed'), + + ('00000000-0000-0000-0003-000000000013', '00000000-0000-0000-0002-000000000014', 'FINANCE_RM_COSTS', + 'RM Costs', '/finance/rm-pricing/costs', 'Calculator', 'finance', 3, 20, true, true, 'seed'), + + ('00000000-0000-0000-0003-000000000014', '00000000-0000-0000-0002-000000000014', 'FINANCE_UNGROUPED_RM', + 'Ungrouped Items', '/finance/rm-pricing/ungrouped', 'AlertCircle', 'finance', 3, 30, true, true, 'seed') +ON CONFLICT (menu_code) DO NOTHING; + +-- ============================================================================= +-- MENU PERMISSIONS — Link menus to their view permissions +-- ============================================================================= + +-- RM Groups menu — visible to users with either grouphead.view OR groupdetail.view +INSERT INTO menu_permissions (menu_id, permission_id, assigned_by) +SELECT '00000000-0000-0000-0003-000000000012', permission_id, 'seed' +FROM mst_permission +WHERE permission_code IN ('finance.rmpricing.grouphead.view', 'finance.rmpricing.groupdetail.view') + AND is_active = true +ON CONFLICT (menu_id, permission_id) DO NOTHING; + +-- RM Costs menu — visible to users with cost.view +INSERT INTO menu_permissions (menu_id, permission_id, assigned_by) +SELECT '00000000-0000-0000-0003-000000000013', permission_id, 'seed' +FROM mst_permission +WHERE permission_code = 'finance.rmpricing.cost.view' + AND is_active = true +ON CONFLICT (menu_id, permission_id) DO NOTHING; + +-- Ungrouped Items menu — visible to users with ungrouped.view +INSERT INTO menu_permissions (menu_id, permission_id, assigned_by) +SELECT '00000000-0000-0000-0003-000000000014', permission_id, 'seed' +FROM mst_permission +WHERE permission_code = 'finance.rmpricing.ungrouped.view' + AND is_active = true +ON CONFLICT (menu_id, permission_id) DO NOTHING; + +-- ============================================================================= +-- ASSIGN ALL RM PRICING PERMISSIONS TO SUPER_ADMIN ROLE +-- ============================================================================= + +INSERT INTO role_permissions (role_id, permission_id, assigned_by) +SELECT r.role_id, p.permission_id, 'seed' +FROM mst_role r +CROSS JOIN mst_permission p +WHERE r.role_code = 'SUPER_ADMIN' + AND p.permission_code LIKE 'finance.rmpricing.%' + AND r.is_active = true + AND p.is_active = true +ON CONFLICT (role_id, permission_id) DO NOTHING;