From ea62bf2915075ee5576e1cfef0f7a3675479a662 Mon Sep 17 00:00:00 2001 From: ilhom Date: Tue, 26 May 2026 19:29:02 +0700 Subject: [PATCH 1/3] feat(bi): add bi.proto enums and entity messages --- finance/v1/bi.proto | 152 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 finance/v1/bi.proto diff --git a/finance/v1/bi.proto b/finance/v1/bi.proto new file mode 100644 index 0000000..d8e81b9 --- /dev/null +++ b/finance/v1/bi.proto @@ -0,0 +1,152 @@ +syntax = "proto3"; + +package finance.v1; + +// Note: go_package is managed by buf.gen.yaml managed mode +// option go_package = "github.com/mutugading/goapps-backend/gen/finance/v1;financev1"; + +import "common/v1/common.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; + +// =================== Enums =================== +enum PeriodeGrain { + PERIODE_GRAIN_UNSPECIFIED = 0; + PERIODE_GRAIN_DAILY = 1; + PERIODE_GRAIN_MONTHLY = 2; + PERIODE_GRAIN_QUARTERLY = 3; + PERIODE_GRAIN_YEARLY = 4; +} +enum CompareMode { + COMPARE_MODE_UNSPECIFIED = 0; + COMPARE_MODE_NONE = 1; + COMPARE_MODE_MOM = 2; + COMPARE_MODE_QOQ = 3; + COMPARE_MODE_YOY = 4; + COMPARE_MODE_YTD = 5; + COMPARE_MODE_R12 = 6; +} +enum ChartType { + CHART_TYPE_UNSPECIFIED = 0; + CHART_TYPE_BAR = 1; + CHART_TYPE_HORIZONTAL_BAR = 2; + CHART_TYPE_STACKED_BAR = 3; + CHART_TYPE_LINE = 4; + CHART_TYPE_AREA = 5; + CHART_TYPE_WATERFALL = 6; + CHART_TYPE_DONUT = 7; + CHART_TYPE_KPI_CARD = 8; + CHART_TYPE_TREEMAP = 9; + CHART_TYPE_HEATMAP = 10; + CHART_TYPE_SCATTER = 11; + CHART_TYPE_MIXED = 12; + CHART_TYPE_DATA_TABLE = 13; +} +enum DataSourceType { + DATA_SOURCE_TYPE_UNSPECIFIED = 0; + DATA_SOURCE_TYPE_ORACLE = 1; + DATA_SOURCE_TYPE_LARAVEL = 2; + DATA_SOURCE_TYPE_EXCEL = 3; + DATA_SOURCE_TYPE_MANUAL = 4; + DATA_SOURCE_TYPE_API = 5; +} + +// =================== Entity Messages =================== +message DashboardGroup { + string group_id = 1; + string group_code = 2; + string group_name = 3; + string description = 4; + string icon = 5; + int32 display_order = 6; + bool is_active = 7; + common.v1.AuditInfo audit = 8; +} + +message Dashboard { + string dashboard_id = 1; + string dashboard_code = 2; + string dashboard_title = 3; + string description = 4; + string filter_type = 5; + string filter_group_1 = 6; + PeriodeGrain periode_grain = 7; + string default_period = 8; + ChartType chart_type = 9; + google.protobuf.Struct chart_config = 10; + google.protobuf.Struct layout_config = 11; + repeated CompareMode compare_modes = 12; + google.protobuf.Struct kpi_config = 13; + bool drill_enabled = 14; + int32 max_drill_level = 15; + int32 cache_ttl_sec = 16; + int32 refresh_interval_sec = 17; + int32 display_order = 18; + string group_id = 19; + string group_name = 20; + string group_icon = 21; + bool is_active = 22; + repeated string allowed_role_codes = 23; + common.v1.AuditInfo audit = 24; +} + +message DataSource { + string source_id = 1; + string source_code = 2; + string source_name = 3; + DataSourceType source_type = 4; + string description = 5; + bool is_active = 6; + common.v1.AuditInfo audit = 7; +} + +message FactMetricDistinct { + repeated string types = 1; + repeated string group_1s = 2; + repeated string group_2s = 3; + repeated string group_3s = 4; + repeated string dimension_keys = 5; +} + +message ChartDataResponse { + google.protobuf.Struct config = 1; + repeated Series series = 2; + repeated string categories = 3; + repeated KpiResult kpis = 4; + DrillContext drill_context = 5; + Meta meta = 6; +} +message Series { + string name = 1; + string lib_hint = 2; + repeated DataPoint points = 3; +} +message DataPoint { + string category = 1; + double value = 2; + string label = 3; + google.protobuf.Struct meta = 4; +} +message KpiResult { + string label = 1; + double value = 2; + string value_formatted = 3; + double compare_value = 4; + double delta_abs = 5; + double delta_pct = 6; + string compare_period_label = 7; + bool improving = 8; + repeated double sparkline = 9; +} +message DrillContext { + repeated string current_path = 1; + string next_field = 2; + repeated string next_values = 3; + bool can_drill = 4; +} +message Meta { + google.protobuf.Timestamp as_of = 1; + int32 row_count = 2; + bool cache_hit = 3; + string query_hash = 4; +} From b27c81267abc26fc8f5608b12234a6c449f0e80e Mon Sep 17 00:00:00 2001 From: ilhom Date: Tue, 26 May 2026 19:34:12 +0700 Subject: [PATCH 2/3] feat(bi): add 4 BI services with full RPC contracts --- finance/v1/bi.proto | 516 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 516 insertions(+) diff --git a/finance/v1/bi.proto b/finance/v1/bi.proto index d8e81b9..f48f739 100644 --- a/finance/v1/bi.proto +++ b/finance/v1/bi.proto @@ -5,7 +5,9 @@ package finance.v1; // Note: go_package is managed by buf.gen.yaml managed mode // option go_package = "github.com/mutugading/goapps-backend/gen/finance/v1;financev1"; +import "buf/validate/validate.proto"; import "common/v1/common.proto"; +import "google/api/annotations.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; @@ -150,3 +152,517 @@ message Meta { bool cache_hit = 3; string query_hash = 4; } + +// =================== Request/Response Messages — DashboardGroup =================== + +message CreateDashboardGroupRequest { + string group_code = 1 [(buf.validate.field).string = { + min_len: 2 + max_len: 40 + pattern: "^[A-Z][A-Z0-9_]*$" + }]; + string group_name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 120 + }]; + string description = 3 [(buf.validate.field).string.max_len = 500]; + string icon = 4 [(buf.validate.field).string.max_len = 40]; + int32 display_order = 5; + bool is_active = 6; +} +message CreateDashboardGroupResponse { + common.v1.BaseResponse base = 1; + DashboardGroup data = 2; +} + +message ListDashboardGroupsRequest { + bool include_inactive = 1; +} +message ListDashboardGroupsResponse { + common.v1.BaseResponse base = 1; + repeated DashboardGroup data = 2; +} + +message UpdateDashboardGroupRequest { + string group_id = 1 [(buf.validate.field).string.uuid = true]; + optional string group_name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 120 + }]; + optional string description = 3 [(buf.validate.field).string.max_len = 500]; + optional string icon = 4 [(buf.validate.field).string.max_len = 40]; + optional int32 display_order = 5; + optional bool is_active = 6; +} +message UpdateDashboardGroupResponse { + common.v1.BaseResponse base = 1; + DashboardGroup data = 2; +} + +message DeleteDashboardGroupRequest { + string group_id = 1 [(buf.validate.field).string.uuid = true]; +} +message DeleteDashboardGroupResponse { + common.v1.BaseResponse base = 1; +} + +// =================== Request/Response Messages — Dashboard =================== + +message CreateDashboardRequest { + string dashboard_code = 1 [(buf.validate.field).string = { + min_len: 2 + max_len: 60 + pattern: "^[A-Z][A-Z0-9_]*$" + }]; + string dashboard_title = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 200 + }]; + string description = 3 [(buf.validate.field).string.max_len = 1000]; + string filter_type = 4 [(buf.validate.field).string = { + min_len: 1 + max_len: 40 + }]; + string filter_group_1 = 5 [(buf.validate.field).string.max_len = 100]; + PeriodeGrain periode_grain = 6 [(buf.validate.field).enum = { + not_in: [0] + }]; + string default_period = 7 [(buf.validate.field).string = { + in: [ + "L12M", + "L24M", + "THIS_YEAR", + "THIS_QTR", + "THIS_MONTH", + "ALL", + "CUSTOM" + ] + }]; + ChartType chart_type = 8 [(buf.validate.field).enum = { + not_in: [0] + }]; + google.protobuf.Struct chart_config = 9; + google.protobuf.Struct layout_config = 10; + repeated CompareMode compare_modes = 11; + google.protobuf.Struct kpi_config = 12; + bool drill_enabled = 13; + int32 max_drill_level = 14 [(buf.validate.field).int32 = { + gte: 1 + lte: 3 + }]; + int32 cache_ttl_sec = 15 [(buf.validate.field).int32 = { + gte: 0 + lte: 86400 + }]; + int32 refresh_interval_sec = 16 [(buf.validate.field).int32 = { + gte: 0 + lte: 3600 + }]; + int32 display_order = 17; + string group_id = 18 [(buf.validate.field).string.uuid = true]; + repeated string allowed_role_codes = 19; + bool is_active = 20; +} +message CreateDashboardResponse { + common.v1.BaseResponse base = 1; + Dashboard data = 2; +} + +message GetDashboardRequest { + string dashboard_id = 1 [(buf.validate.field).string.uuid = true]; +} +message GetDashboardResponse { + common.v1.BaseResponse base = 1; + Dashboard data = 2; +} + +message GetDashboardByCodeRequest { + string dashboard_code = 1 [(buf.validate.field).string = { + min_len: 2 + max_len: 60 + }]; +} +message GetDashboardByCodeResponse { + common.v1.BaseResponse base = 1; + Dashboard data = 2; +} + +message ListDashboardsRequest { + int32 page = 1 [(buf.validate.field).int32.gte = 1]; + int32 page_size = 2 [(buf.validate.field).int32 = { + gte: 1 + lte: 100 + }]; + string search = 3 [(buf.validate.field).string.max_len = 100]; + string group_id = 4; + string filter_type = 5; + bool include_inactive = 6; + string sort_by = 7 [(buf.validate.field).string = { + in: [ + "", + "code", + "title", + "display_order", + "created_at" + ] + }]; + string sort_order = 8 [(buf.validate.field).string = { + in: [ + "", + "asc", + "desc" + ] + }]; +} +message ListDashboardsResponse { + common.v1.BaseResponse base = 1; + repeated Dashboard data = 2; + common.v1.PaginationResponse pagination = 3; +} + +message UpdateDashboardRequest { + string dashboard_id = 1 [(buf.validate.field).string.uuid = true]; + optional string dashboard_title = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 200 + }]; + optional string description = 3 [(buf.validate.field).string.max_len = 1000]; + optional string filter_type = 4 [(buf.validate.field).string = {max_len: 40}]; + optional string filter_group_1 = 5 [(buf.validate.field).string.max_len = 100]; + optional PeriodeGrain periode_grain = 6; + optional string default_period = 7 [(buf.validate.field).string = { + in: [ + "", + "L12M", + "L24M", + "THIS_YEAR", + "THIS_QTR", + "THIS_MONTH", + "ALL", + "CUSTOM" + ] + }]; + optional ChartType chart_type = 8; + google.protobuf.Struct chart_config = 9; + google.protobuf.Struct layout_config = 10; + repeated CompareMode compare_modes = 11; + google.protobuf.Struct kpi_config = 12; + optional bool drill_enabled = 13; + optional int32 max_drill_level = 14 [(buf.validate.field).int32 = { + gte: 0 + lte: 3 + }]; + optional int32 cache_ttl_sec = 15 [(buf.validate.field).int32 = { + gte: 0 + lte: 86400 + }]; + optional int32 refresh_interval_sec = 16 [(buf.validate.field).int32 = { + gte: 0 + lte: 3600 + }]; + optional int32 display_order = 17; + optional string group_id = 18 [(buf.validate.field).string = {pattern: "^$|^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"}]; + optional bool is_active = 19; +} +message UpdateDashboardResponse { + common.v1.BaseResponse base = 1; + Dashboard data = 2; +} + +message DeleteDashboardRequest { + string dashboard_id = 1 [(buf.validate.field).string.uuid = true]; +} +message DeleteDashboardResponse { + common.v1.BaseResponse base = 1; +} + +message DuplicateDashboardRequest { + string dashboard_id = 1 [(buf.validate.field).string.uuid = true]; + string new_code = 2 [(buf.validate.field).string = { + min_len: 2 + max_len: 60 + pattern: "^[A-Z][A-Z0-9_]*$" + }]; + string new_title = 3 [(buf.validate.field).string = { + min_len: 1 + max_len: 200 + }]; +} +message DuplicateDashboardResponse { + common.v1.BaseResponse base = 1; + Dashboard data = 2; +} + +message SetDashboardRolesRequest { + string dashboard_id = 1 [(buf.validate.field).string.uuid = true]; + repeated string role_codes = 2; +} +message SetDashboardRolesResponse { + common.v1.BaseResponse base = 1; + repeated string role_codes = 2; +} + +message ListAccessibleDashboardsRequest {} +message ListAccessibleDashboardsResponse { + common.v1.BaseResponse base = 1; + repeated Dashboard data = 2; +} + +// =================== ChartDataService =================== + +message ViewerFilters { + string period_preset = 1; + google.protobuf.Timestamp period_from = 2; + google.protobuf.Timestamp period_to = 3; + CompareMode compare = 4; + repeated string drill_path = 5; +} + +message GetDashboardDataRequest { + string dashboard_code = 1 [(buf.validate.field).string = { + min_len: 2 + max_len: 60 + }]; + string period_preset = 2 [(buf.validate.field).string = { + in: [ + "", + "L12M", + "L24M", + "THIS_YEAR", + "THIS_QTR", + "THIS_MONTH", + "ALL", + "CUSTOM" + ] + }]; + google.protobuf.Timestamp period_from = 3; + google.protobuf.Timestamp period_to = 4; + CompareMode compare = 5; + repeated string drill_path = 6; +} +message GetDashboardDataResponse { + common.v1.BaseResponse base = 1; + ChartDataResponse data = 2; +} + +message PreviewDashboardRequest { + string filter_type = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 40 + }]; + string filter_group_1 = 2 [(buf.validate.field).string.max_len = 100]; + PeriodeGrain periode_grain = 3 [(buf.validate.field).enum = { + not_in: [0] + }]; + ChartType chart_type = 4 [(buf.validate.field).enum = { + not_in: [0] + }]; + google.protobuf.Struct chart_config = 5; + google.protobuf.Struct kpi_config = 6; + repeated CompareMode compare_modes = 7; +} +message PreviewDashboardResponse { + common.v1.BaseResponse base = 1; + ChartDataResponse data = 2; +} + +// =================== DataSourceService =================== + +message ListDataSourcesRequest { + bool include_inactive = 1; +} +message ListDataSourcesResponse { + common.v1.BaseResponse base = 1; + repeated DataSource data = 2; +} + +message GetFactDistinctsRequest { + string type = 1 [(buf.validate.field).string.max_len = 40]; +} +message GetFactDistinctsResponse { + common.v1.BaseResponse base = 1; + FactMetricDistinct data = 2; +} + +// =================== BiJobService =================== + +message BiJob { + string job_id = 1; + string job_name = 2; + string source_id = 3; + string source_code = 4; + string target_type = 5; + string schedule_cron = 6; + string oracle_procedure = 7; + google.protobuf.Struct config = 8; + bool is_active = 9; + string last_status = 10; + google.protobuf.Timestamp last_run_at = 11; + int32 last_duration_ms = 12; + common.v1.AuditInfo audit = 13; +} + +message BiJobLog { + int64 log_id = 1; + string job_id = 2; + string job_name = 3; + google.protobuf.Timestamp started_at = 4; + google.protobuf.Timestamp ended_at = 5; + string status = 6; + int32 rows_affected = 7; + string error_message = 8; + string triggered_by = 9; + int32 duration_ms = 10; +} + +message ListJobsRequest { + bool include_inactive = 1; +} +message ListJobsResponse { + common.v1.BaseResponse base = 1; + repeated BiJob data = 2; +} + +message ListJobLogsRequest { + string job_id = 1 [(buf.validate.field).string.uuid = true]; + int32 page = 2 [(buf.validate.field).int32.gte = 1]; + int32 page_size = 3 [(buf.validate.field).int32 = { + gte: 1 + lte: 100 + }]; +} +message ListJobLogsResponse { + common.v1.BaseResponse base = 1; + repeated BiJobLog data = 2; + common.v1.PaginationResponse pagination = 3; +} + +message TriggerJobRequest { + string job_id = 1 [(buf.validate.field).string.uuid = true]; +} +message TriggerJobResponse { + common.v1.BaseResponse base = 1; + BiJobLog data = 2; +} + +// =================== Services =================== + +// DashboardService manages BI dashboard definitions, groups, and per-dashboard role mappings. +service DashboardService { + // CreateDashboard creates a new dashboard definition. + rpc CreateDashboard(CreateDashboardRequest) returns (CreateDashboardResponse) { + option (google.api.http) = { + post: "/api/v1/finance/bi/dashboards" + body: "*" + }; + } + // GetDashboard returns a dashboard by ID. + rpc GetDashboard(GetDashboardRequest) returns (GetDashboardResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/dashboards/{dashboard_id}"}; + } + // GetDashboardByCode returns a dashboard by its business code. + rpc GetDashboardByCode(GetDashboardByCodeRequest) returns (GetDashboardByCodeResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/dashboards/by-code/{dashboard_code}"}; + } + // ListDashboards returns paginated dashboards for admin. + rpc ListDashboards(ListDashboardsRequest) returns (ListDashboardsResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/dashboards"}; + } + // UpdateDashboard mutates an existing dashboard. + rpc UpdateDashboard(UpdateDashboardRequest) returns (UpdateDashboardResponse) { + option (google.api.http) = { + put: "/api/v1/finance/bi/dashboards/{dashboard_id}" + body: "*" + }; + } + // DeleteDashboard soft-deletes a dashboard. + rpc DeleteDashboard(DeleteDashboardRequest) returns (DeleteDashboardResponse) { + option (google.api.http) = {delete: "/api/v1/finance/bi/dashboards/{dashboard_id}"}; + } + // DuplicateDashboard clones a dashboard with a new code/title. + rpc DuplicateDashboard(DuplicateDashboardRequest) returns (DuplicateDashboardResponse) { + option (google.api.http) = { + post: "/api/v1/finance/bi/dashboards/{dashboard_id}/duplicate" + body: "*" + }; + } + // SetDashboardRoles overwrites the dashboard's role whitelist. + rpc SetDashboardRoles(SetDashboardRolesRequest) returns (SetDashboardRolesResponse) { + option (google.api.http) = { + put: "/api/v1/finance/bi/dashboards/{dashboard_id}/roles" + body: "*" + }; + } + // ListAccessibleDashboards returns dashboards visible to the calling user (for viewer sidebar). + rpc ListAccessibleDashboards(ListAccessibleDashboardsRequest) returns (ListAccessibleDashboardsResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/dashboards/accessible"}; + } + // CreateDashboardGroup creates a new dashboard group. + rpc CreateDashboardGroup(CreateDashboardGroupRequest) returns (CreateDashboardGroupResponse) { + option (google.api.http) = { + post: "/api/v1/finance/bi/groups" + body: "*" + }; + } + // ListDashboardGroups returns dashboard groups. + rpc ListDashboardGroups(ListDashboardGroupsRequest) returns (ListDashboardGroupsResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/groups"}; + } + // UpdateDashboardGroup mutates a group. + rpc UpdateDashboardGroup(UpdateDashboardGroupRequest) returns (UpdateDashboardGroupResponse) { + option (google.api.http) = { + put: "/api/v1/finance/bi/groups/{group_id}" + body: "*" + }; + } + // DeleteDashboardGroup soft-deletes a group. + rpc DeleteDashboardGroup(DeleteDashboardGroupRequest) returns (DeleteDashboardGroupResponse) { + option (google.api.http) = {delete: "/api/v1/finance/bi/groups/{group_id}"}; + } +} + +// ChartDataService serves shaped chart data for the viewer and admin preview. +service ChartDataService { + // GetDashboardData resolves dashboard config + filters and returns chart-ready data. + rpc GetDashboardData(GetDashboardDataRequest) returns (GetDashboardDataResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/dashboards/{dashboard_code}/data"}; + } + // PreviewDashboard renders an unsaved dashboard config against real fact data (admin only). + rpc PreviewDashboard(PreviewDashboardRequest) returns (PreviewDashboardResponse) { + option (google.api.http) = { + post: "/api/v1/finance/bi/preview" + body: "*" + }; + } +} + +// DataSourceService exposes the data-source registry and fact-metric distincts for admin form dropdowns. +service DataSourceService { + // ListDataSources returns all data sources. + rpc ListDataSources(ListDataSourcesRequest) returns (ListDataSourcesResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/data-sources"}; + } + // GetFactDistincts returns distinct type/group_1/group_2/group_3 values for an optional type filter. + rpc GetFactDistincts(GetFactDistinctsRequest) returns (GetFactDistinctsResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/fact/distincts"}; + } +} + +// BiJobService manages ETL job registry and logs. +service BiJobService { + // ListJobs returns ETL job registry entries. + rpc ListJobs(ListJobsRequest) returns (ListJobsResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/jobs"}; + } + // ListJobLogs returns logs for a specific job. + rpc ListJobLogs(ListJobLogsRequest) returns (ListJobLogsResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/jobs/{job_id}/logs"}; + } + // TriggerJob manually triggers an ETL job run. + rpc TriggerJob(TriggerJobRequest) returns (TriggerJobResponse) { + option (google.api.http) = { + post: "/api/v1/finance/bi/jobs/{job_id}/trigger" + body: "*" + }; + } +} From ae2b4652f2b0b0921e833d7689bf4c56733962ab Mon Sep 17 00:00:00 2001 From: ilhom Date: Wed, 27 May 2026 19:07:16 +0700 Subject: [PATCH 3/3] feat(bi): add Excel upload + config-audit RPCs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add BiUploadService (DownloadUploadTemplate/ParseUpload/CommitUpload/ CancelUpload/ListUploads) and a ListConfigAudit RPC on DashboardService, plus BiUpload, UploadRowError, and BiAuditEntry messages — backing the dashboard Excel-upload and config-change audit-log features. Co-Authored-By: Claude Opus 4.7 (1M context) --- finance/v1/bi.proto | 151 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/finance/v1/bi.proto b/finance/v1/bi.proto index f48f739..9c6c508 100644 --- a/finance/v1/bi.proto +++ b/finance/v1/bi.proto @@ -545,6 +545,119 @@ message TriggerJobResponse { BiJobLog data = 2; } +// =================== Excel Upload =================== + +message UploadRowError { + int32 row = 1; + string column = 2; + string value = 3; + string issue = 4; + string expected = 5; +} + +message BiUpload { + string upload_id = 1; + string target_type = 2; // FM_TYPE the file targets (e.g. MIS) + string file_name = 3; + int64 file_size = 4; + string status = 5; // PENDING|PREVIEW|COMMITTED|CANCELLED|FAILED + int32 total_rows = 6; + int32 valid_rows = 7; + int32 invalid_rows = 8; + int32 committed_rows = 9; + repeated UploadRowError errors = 10; + string uploaded_by = 11; + google.protobuf.Timestamp uploaded_at = 12; +} + +message DownloadUploadTemplateRequest { + string target_type = 1 [(buf.validate.field).string.max_len = 40]; +} +message DownloadUploadTemplateResponse { + common.v1.BaseResponse base = 1; + bytes file_content = 2; + string file_name = 3; +} + +message ParseUploadRequest { + string target_type = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 40 + }]; + string file_name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 255 + }]; + bytes file_content = 3 [(buf.validate.field).bytes = { + min_len: 1 + max_len: 10485760 + }]; +} +message ParseUploadResponse { + common.v1.BaseResponse base = 1; + BiUpload data = 2; // preview header + per-row errors +} + +message CommitUploadRequest { + string upload_id = 1 [(buf.validate.field).string.uuid = true]; +} +message CommitUploadResponse { + common.v1.BaseResponse base = 1; + BiUpload data = 2; +} + +message CancelUploadRequest { + string upload_id = 1 [(buf.validate.field).string.uuid = true]; +} +message CancelUploadResponse { + common.v1.BaseResponse base = 1; +} + +message ListUploadsRequest { + int32 page = 1 [(buf.validate.field).int32.gte = 1]; + int32 page_size = 2 [(buf.validate.field).int32 = { + gte: 1 + lte: 100 + }]; +} +message ListUploadsResponse { + common.v1.BaseResponse base = 1; + repeated BiUpload data = 2; + common.v1.PaginationResponse pagination = 3; +} + +// =================== Config Audit =================== + +message BiAuditEntry { + int64 audit_id = 1; + string entity_type = 2; // dashboard|group + string entity_code = 3; // dashboard_code or group_code + string entity_title = 4; + string action = 5; // CREATE|UPDATE|DELETE + string changed_by = 6; + google.protobuf.Timestamp changed_at = 7; + string summary = 8; +} +message ListConfigAuditRequest { + int32 page = 1 [(buf.validate.field).int32.gte = 1]; + int32 page_size = 2 [(buf.validate.field).int32 = { + gte: 1 + lte: 100 + }]; + string entity_type = 3 [(buf.validate.field).string = { + in: [ + "", + "dashboard", + "group" + ] + }]; +} +message ListConfigAuditResponse { + common.v1.BaseResponse base = 1; + repeated BiAuditEntry data = 2; + common.v1.PaginationResponse pagination = 3; +} + // =================== Services =================== // DashboardService manages BI dashboard definitions, groups, and per-dashboard role mappings. @@ -619,6 +732,10 @@ service DashboardService { rpc DeleteDashboardGroup(DeleteDashboardGroupRequest) returns (DeleteDashboardGroupResponse) { option (google.api.http) = {delete: "/api/v1/finance/bi/groups/{group_id}"}; } + // ListConfigAudit returns the dashboard/group configuration change history. + rpc ListConfigAudit(ListConfigAuditRequest) returns (ListConfigAuditResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/audit"}; + } } // ChartDataService serves shaped chart data for the viewer and admin preview. @@ -666,3 +783,37 @@ service BiJobService { }; } } + +// BiUploadService handles Excel upload: template download, parse/preview into staging, +// commit (UPSERT to fact_metric), cancel, and upload session history. +service BiUploadService { + // DownloadUploadTemplate returns a blank .xlsx template matching the FACT_METRIC shape. + rpc DownloadUploadTemplate(DownloadUploadTemplateRequest) returns (DownloadUploadTemplateResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/uploads/template"}; + } + // ParseUpload parses an uploaded .xlsx, validates rows, writes to staging, returns a preview. + rpc ParseUpload(ParseUploadRequest) returns (ParseUploadResponse) { + option (google.api.http) = { + post: "/api/v1/finance/bi/uploads/parse" + body: "*" + }; + } + // CommitUpload UPSERTs the staged rows of a previewed session into fact_metric. + rpc CommitUpload(CommitUploadRequest) returns (CommitUploadResponse) { + option (google.api.http) = { + post: "/api/v1/finance/bi/uploads/{upload_id}/commit" + body: "*" + }; + } + // CancelUpload discards a previewed session without committing. + rpc CancelUpload(CancelUploadRequest) returns (CancelUploadResponse) { + option (google.api.http) = { + post: "/api/v1/finance/bi/uploads/{upload_id}/cancel" + body: "*" + }; + } + // ListUploads returns paginated upload session history. + rpc ListUploads(ListUploadsRequest) returns (ListUploadsResponse) { + option (google.api.http) = {get: "/api/v1/finance/bi/uploads"}; + } +}