diff --git a/docs/docs.go b/docs/docs.go index e2f54f4a..50c510d2 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -352,19 +352,19 @@ const docTemplate = `{ "400": { "description": "Invalid ID format", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "404": { "description": "Retribusi not found", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } } } @@ -409,19 +409,19 @@ const docTemplate = `{ "400": { "description": "Bad request or validation error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "404": { "description": "Retribusi not found", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } } } @@ -457,19 +457,19 @@ const docTemplate = `{ "400": { "description": "Invalid ID format", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "404": { "description": "Retribusi not found", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } } } @@ -545,13 +545,13 @@ const docTemplate = `{ "400": { "description": "Bad request", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } } } @@ -589,13 +589,82 @@ const docTemplate = `{ "400": { "description": "Bad request or validation error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/retribusis/dynamic": { + "get": { + "description": "Returns retribusis with advanced dynamic filtering like Directus", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "retribusi" + ], + "summary": "Get retribusi with dynamic filtering", + "parameters": [ + { + "type": "string", + "description": "Fields to select (e.g., fields=*.*)", + "name": "fields", + "in": "query" + }, + { + "type": "string", + "description": "Dynamic filters (e.g., filter[Jenis][_eq]=value)", + "name": "filter[column][operator]", + "in": "query" + }, + { + "type": "string", + "description": "Sort fields (e.g., sort=date_created,-Jenis)", + "name": "sort", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_retribusi.RetribusiGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } } } @@ -626,13 +695,13 @@ const docTemplate = `{ "200": { "description": "Statistics data", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.AggregateData" + "$ref": "#/definitions/api-service_internal_models.AggregateData" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } } } @@ -735,9 +804,287 @@ const docTemplate = `{ } } } + }, + "/sep": { + "put": { + "description": "Update an existing Surat Eligibilitas Peserta", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bpjs" + ], + "summary": "Update an existing SEP", + "parameters": [ + { + "description": "SEP update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.SepPutRequest" + } + } + ], + "responses": { + "200": { + "description": "SEP updated successfully", + "schema": { + "$ref": "#/definitions/models.SepResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + }, + "post": { + "description": "Create a new Surat Eligibilitas Peserta", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bpjs" + ], + "summary": "Create a new SEP", + "parameters": [ + { + "description": "SEP creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.SepPostRequest" + } + } + ], + "responses": { + "200": { + "description": "SEP created successfully", + "schema": { + "$ref": "#/definitions/models.SepResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/sep/{noSep}": { + "get": { + "description": "Retrieve a Surat Eligibilitas Peserta by noSep", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bpjs" + ], + "summary": "Get an existing SEP", + "parameters": [ + { + "type": "string", + "description": "No SEP", + "name": "noSep", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Data SEP retrieved successfully", + "schema": { + "$ref": "#/definitions/models.SepResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + }, + "delete": { + "description": "Delete a Surat Eligibilitas Peserta by noSep", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bpjs" + ], + "summary": "Delete an existing SEP", + "parameters": [ + { + "type": "string", + "description": "No SEP", + "name": "noSep", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "User", + "name": "user", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "SEP deleted successfully", + "schema": { + "$ref": "#/definitions/models.SepResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } } }, "definitions": { + "api-service_internal_models.AggregateData": { + "type": "object", + "properties": { + "by_dinas": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "by_jenis": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "by_status": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "created_today": { + "type": "integer" + }, + "last_updated": { + "type": "string" + }, + "total_active": { + "type": "integer" + }, + "total_draft": { + "type": "integer" + }, + "total_inactive": { + "type": "integer" + }, + "updated_today": { + "type": "integer" + } + } + }, + "api-service_internal_models.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "api-service_internal_models.MetaResponse": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "has_next": { + "type": "boolean" + }, + "has_prev": { + "type": "boolean" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "api-service_internal_models.NullableInt32": { + "type": "object", + "properties": { + "int32": { + "type": "integer" + }, + "valid": { + "type": "boolean" + } + } + }, "api-service_internal_models_auth.LoginRequest": { "type": "object", "required": [ @@ -784,101 +1131,6 @@ const docTemplate = `{ } } }, - "api-service_internal_models_retribusi.AggregateData": { - "type": "object", - "properties": { - "by_dinas": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "by_jenis": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "by_status": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "created_today": { - "type": "integer" - }, - "last_updated": { - "type": "string" - }, - "total_active": { - "type": "integer" - }, - "total_draft": { - "type": "integer" - }, - "total_inactive": { - "type": "integer" - }, - "updated_today": { - "type": "integer" - } - } - }, - "api-service_internal_models_retribusi.ErrorResponse": { - "type": "object", - "properties": { - "code": { - "type": "integer" - }, - "error": { - "type": "string" - }, - "message": { - "type": "string" - }, - "timestamp": { - "type": "string" - } - } - }, - "api-service_internal_models_retribusi.MetaResponse": { - "type": "object", - "properties": { - "current_page": { - "type": "integer" - }, - "has_next": { - "type": "boolean" - }, - "has_prev": { - "type": "boolean" - }, - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - }, - "total_pages": { - "type": "integer" - } - } - }, - "api-service_internal_models_retribusi.NullableInt32": { - "type": "object", - "properties": { - "int32": { - "type": "integer" - }, - "valid": { - "type": "boolean" - } - } - }, "api-service_internal_models_retribusi.Retribusi": { "type": "object", "properties": { @@ -919,7 +1171,7 @@ const docTemplate = `{ "$ref": "#/definitions/sql.NullString" }, "sort": { - "$ref": "#/definitions/api-service_internal_models_retribusi.NullableInt32" + "$ref": "#/definitions/api-service_internal_models.NullableInt32" }, "status": { "type": "string" @@ -1069,10 +1321,10 @@ const docTemplate = `{ "type": "string" }, "meta": { - "$ref": "#/definitions/api-service_internal_models_retribusi.MetaResponse" + "$ref": "#/definitions/api-service_internal_models.MetaResponse" }, "summary": { - "$ref": "#/definitions/api-service_internal_models_retribusi.AggregateData" + "$ref": "#/definitions/api-service_internal_models.AggregateData" } } }, @@ -1163,6 +1415,10 @@ const docTemplate = `{ } } }, + "gin.H": { + "type": "object", + "additionalProperties": {} + }, "models.DiagnosaResponse": { "type": "object", "properties": { @@ -1175,6 +1431,329 @@ const docTemplate = `{ } } }, + "models.Flag": { + "type": "object", + "required": [ + "cob" + ], + "properties": { + "cob": { + "type": "string" + } + } + }, + "models.Jaminan": { + "type": "object", + "required": [ + "lakaLantas" + ], + "properties": { + "lakaLantas": { + "type": "string" + }, + "noLP": { + "type": "string" + }, + "penjamin": { + "$ref": "#/definitions/models.Penjamin" + } + } + }, + "models.KlsRawatPost": { + "type": "object", + "required": [ + "klsRawatHak" + ], + "properties": { + "klsRawatHak": { + "type": "string" + }, + "klsRawatNaik": { + "type": "string" + }, + "pembiayaan": { + "type": "string" + }, + "penanggungJawab": { + "type": "string" + } + } + }, + "models.KlsRawatPut": { + "type": "object", + "properties": { + "klsRawatHak": { + "type": "string" + }, + "klsRawatNaik": { + "type": "string" + }, + "pembiayaan": { + "type": "string" + }, + "penanggungJawab": { + "type": "string" + } + } + }, + "models.LokasiLaka": { + "type": "object", + "properties": { + "kdKabupaten": { + "type": "string" + }, + "kdKecamatan": { + "type": "string" + }, + "kdPropinsi": { + "type": "string" + } + } + }, + "models.Penjamin": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "suplesi": { + "$ref": "#/definitions/models.Suplesi" + }, + "tglKejadian": { + "type": "string" + } + } + }, + "models.Poli": { + "type": "object", + "required": [ + "eksekutif" + ], + "properties": { + "eksekutif": { + "type": "string" + }, + "tujuan": { + "type": "string" + } + } + }, + "models.Rujukan": { + "type": "object", + "required": [ + "asalRujukan", + "noRujukan", + "ppkRujukan", + "tglRujukan" + ], + "properties": { + "asalRujukan": { + "type": "string" + }, + "noRujukan": { + "type": "string" + }, + "ppkRujukan": { + "type": "string" + }, + "tglRujukan": { + "type": "string" + } + } + }, + "models.SepPostRequest": { + "type": "object", + "required": [ + "t_sep" + ], + "properties": { + "t_sep": { + "$ref": "#/definitions/models.TSepPost" + } + } + }, + "models.SepPutRequest": { + "type": "object", + "required": [ + "t_sep" + ], + "properties": { + "t_sep": { + "$ref": "#/definitions/models.TSepPut" + } + } + }, + "models.SepResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "message": { + "type": "string" + } + } + }, + "models.Skdp": { + "type": "object", + "required": [ + "kodeDPJP", + "noSurat" + ], + "properties": { + "kodeDPJP": { + "type": "string" + }, + "noSurat": { + "type": "string" + } + } + }, + "models.Suplesi": { + "type": "object", + "properties": { + "lokasiLaka": { + "$ref": "#/definitions/models.LokasiLaka" + }, + "noSepSuplesi": { + "type": "string" + }, + "suplesi": { + "type": "string" + } + } + }, + "models.TSepPost": { + "type": "object", + "required": [ + "cob", + "diagAwal", + "jaminan", + "jnsPelayanan", + "katarak", + "klsRawat", + "noKartu", + "noMR", + "poli", + "ppkPelayanan", + "rujukan", + "skdp", + "tglSep", + "user" + ], + "properties": { + "assesmentPel": { + "type": "string" + }, + "catatan": { + "type": "string" + }, + "cob": { + "$ref": "#/definitions/models.Flag" + }, + "diagAwal": { + "type": "string" + }, + "dpjpLayan": { + "type": "string" + }, + "flagProcedure": { + "type": "string" + }, + "jaminan": { + "$ref": "#/definitions/models.Jaminan" + }, + "jnsPelayanan": { + "type": "string" + }, + "katarak": { + "$ref": "#/definitions/models.Flag" + }, + "kdPenunjang": { + "type": "string" + }, + "klsRawat": { + "$ref": "#/definitions/models.KlsRawatPost" + }, + "noKartu": { + "type": "string" + }, + "noMR": { + "type": "string" + }, + "noTelp": { + "type": "string" + }, + "poli": { + "$ref": "#/definitions/models.Poli" + }, + "ppkPelayanan": { + "type": "string" + }, + "rujukan": { + "$ref": "#/definitions/models.Rujukan" + }, + "skdp": { + "$ref": "#/definitions/models.Skdp" + }, + "tglSep": { + "description": "yyyy-MM-dd", + "type": "string" + }, + "tujuanKunj": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "models.TSepPut": { + "type": "object", + "required": [ + "noSep", + "user" + ], + "properties": { + "catatan": { + "type": "string" + }, + "cob": { + "$ref": "#/definitions/models.Flag" + }, + "diagAwal": { + "type": "string" + }, + "dpjpLayan": { + "type": "string" + }, + "jaminan": { + "$ref": "#/definitions/models.Jaminan" + }, + "katarak": { + "$ref": "#/definitions/models.Flag" + }, + "klsRawat": { + "$ref": "#/definitions/models.KlsRawatPut" + }, + "noMR": { + "type": "string" + }, + "noSep": { + "type": "string" + }, + "noTelp": { + "type": "string" + }, + "poli": { + "$ref": "#/definitions/models.Poli" + }, + "user": { + "type": "string" + } + } + }, "sql.NullString": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 4f277a52..bdc7db54 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -349,19 +349,19 @@ "400": { "description": "Invalid ID format", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "404": { "description": "Retribusi not found", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } } } @@ -406,19 +406,19 @@ "400": { "description": "Bad request or validation error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "404": { "description": "Retribusi not found", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } } } @@ -454,19 +454,19 @@ "400": { "description": "Invalid ID format", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "404": { "description": "Retribusi not found", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } } } @@ -542,13 +542,13 @@ "400": { "description": "Bad request", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } } } @@ -586,13 +586,82 @@ "400": { "description": "Bad request or validation error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/retribusis/dynamic": { + "get": { + "description": "Returns retribusis with advanced dynamic filtering like Directus", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "retribusi" + ], + "summary": "Get retribusi with dynamic filtering", + "parameters": [ + { + "type": "string", + "description": "Fields to select (e.g., fields=*.*)", + "name": "fields", + "in": "query" + }, + { + "type": "string", + "description": "Dynamic filters (e.g., filter[Jenis][_eq]=value)", + "name": "filter[column][operator]", + "in": "query" + }, + { + "type": "string", + "description": "Sort fields (e.g., sort=date_created,-Jenis)", + "name": "sort", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_retribusi.RetribusiGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } } } @@ -623,13 +692,13 @@ "200": { "description": "Statistics data", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.AggregateData" + "$ref": "#/definitions/api-service_internal_models.AggregateData" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_retribusi.ErrorResponse" + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" } } } @@ -732,9 +801,287 @@ } } } + }, + "/sep": { + "put": { + "description": "Update an existing Surat Eligibilitas Peserta", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bpjs" + ], + "summary": "Update an existing SEP", + "parameters": [ + { + "description": "SEP update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.SepPutRequest" + } + } + ], + "responses": { + "200": { + "description": "SEP updated successfully", + "schema": { + "$ref": "#/definitions/models.SepResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + }, + "post": { + "description": "Create a new Surat Eligibilitas Peserta", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bpjs" + ], + "summary": "Create a new SEP", + "parameters": [ + { + "description": "SEP creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.SepPostRequest" + } + } + ], + "responses": { + "200": { + "description": "SEP created successfully", + "schema": { + "$ref": "#/definitions/models.SepResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/sep/{noSep}": { + "get": { + "description": "Retrieve a Surat Eligibilitas Peserta by noSep", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bpjs" + ], + "summary": "Get an existing SEP", + "parameters": [ + { + "type": "string", + "description": "No SEP", + "name": "noSep", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Data SEP retrieved successfully", + "schema": { + "$ref": "#/definitions/models.SepResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + }, + "delete": { + "description": "Delete a Surat Eligibilitas Peserta by noSep", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bpjs" + ], + "summary": "Delete an existing SEP", + "parameters": [ + { + "type": "string", + "description": "No SEP", + "name": "noSep", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "User", + "name": "user", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "SEP deleted successfully", + "schema": { + "$ref": "#/definitions/models.SepResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } } }, "definitions": { + "api-service_internal_models.AggregateData": { + "type": "object", + "properties": { + "by_dinas": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "by_jenis": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "by_status": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "created_today": { + "type": "integer" + }, + "last_updated": { + "type": "string" + }, + "total_active": { + "type": "integer" + }, + "total_draft": { + "type": "integer" + }, + "total_inactive": { + "type": "integer" + }, + "updated_today": { + "type": "integer" + } + } + }, + "api-service_internal_models.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "api-service_internal_models.MetaResponse": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "has_next": { + "type": "boolean" + }, + "has_prev": { + "type": "boolean" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "api-service_internal_models.NullableInt32": { + "type": "object", + "properties": { + "int32": { + "type": "integer" + }, + "valid": { + "type": "boolean" + } + } + }, "api-service_internal_models_auth.LoginRequest": { "type": "object", "required": [ @@ -781,101 +1128,6 @@ } } }, - "api-service_internal_models_retribusi.AggregateData": { - "type": "object", - "properties": { - "by_dinas": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "by_jenis": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "by_status": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "created_today": { - "type": "integer" - }, - "last_updated": { - "type": "string" - }, - "total_active": { - "type": "integer" - }, - "total_draft": { - "type": "integer" - }, - "total_inactive": { - "type": "integer" - }, - "updated_today": { - "type": "integer" - } - } - }, - "api-service_internal_models_retribusi.ErrorResponse": { - "type": "object", - "properties": { - "code": { - "type": "integer" - }, - "error": { - "type": "string" - }, - "message": { - "type": "string" - }, - "timestamp": { - "type": "string" - } - } - }, - "api-service_internal_models_retribusi.MetaResponse": { - "type": "object", - "properties": { - "current_page": { - "type": "integer" - }, - "has_next": { - "type": "boolean" - }, - "has_prev": { - "type": "boolean" - }, - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - }, - "total_pages": { - "type": "integer" - } - } - }, - "api-service_internal_models_retribusi.NullableInt32": { - "type": "object", - "properties": { - "int32": { - "type": "integer" - }, - "valid": { - "type": "boolean" - } - } - }, "api-service_internal_models_retribusi.Retribusi": { "type": "object", "properties": { @@ -916,7 +1168,7 @@ "$ref": "#/definitions/sql.NullString" }, "sort": { - "$ref": "#/definitions/api-service_internal_models_retribusi.NullableInt32" + "$ref": "#/definitions/api-service_internal_models.NullableInt32" }, "status": { "type": "string" @@ -1066,10 +1318,10 @@ "type": "string" }, "meta": { - "$ref": "#/definitions/api-service_internal_models_retribusi.MetaResponse" + "$ref": "#/definitions/api-service_internal_models.MetaResponse" }, "summary": { - "$ref": "#/definitions/api-service_internal_models_retribusi.AggregateData" + "$ref": "#/definitions/api-service_internal_models.AggregateData" } } }, @@ -1160,6 +1412,10 @@ } } }, + "gin.H": { + "type": "object", + "additionalProperties": {} + }, "models.DiagnosaResponse": { "type": "object", "properties": { @@ -1172,6 +1428,329 @@ } } }, + "models.Flag": { + "type": "object", + "required": [ + "cob" + ], + "properties": { + "cob": { + "type": "string" + } + } + }, + "models.Jaminan": { + "type": "object", + "required": [ + "lakaLantas" + ], + "properties": { + "lakaLantas": { + "type": "string" + }, + "noLP": { + "type": "string" + }, + "penjamin": { + "$ref": "#/definitions/models.Penjamin" + } + } + }, + "models.KlsRawatPost": { + "type": "object", + "required": [ + "klsRawatHak" + ], + "properties": { + "klsRawatHak": { + "type": "string" + }, + "klsRawatNaik": { + "type": "string" + }, + "pembiayaan": { + "type": "string" + }, + "penanggungJawab": { + "type": "string" + } + } + }, + "models.KlsRawatPut": { + "type": "object", + "properties": { + "klsRawatHak": { + "type": "string" + }, + "klsRawatNaik": { + "type": "string" + }, + "pembiayaan": { + "type": "string" + }, + "penanggungJawab": { + "type": "string" + } + } + }, + "models.LokasiLaka": { + "type": "object", + "properties": { + "kdKabupaten": { + "type": "string" + }, + "kdKecamatan": { + "type": "string" + }, + "kdPropinsi": { + "type": "string" + } + } + }, + "models.Penjamin": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "suplesi": { + "$ref": "#/definitions/models.Suplesi" + }, + "tglKejadian": { + "type": "string" + } + } + }, + "models.Poli": { + "type": "object", + "required": [ + "eksekutif" + ], + "properties": { + "eksekutif": { + "type": "string" + }, + "tujuan": { + "type": "string" + } + } + }, + "models.Rujukan": { + "type": "object", + "required": [ + "asalRujukan", + "noRujukan", + "ppkRujukan", + "tglRujukan" + ], + "properties": { + "asalRujukan": { + "type": "string" + }, + "noRujukan": { + "type": "string" + }, + "ppkRujukan": { + "type": "string" + }, + "tglRujukan": { + "type": "string" + } + } + }, + "models.SepPostRequest": { + "type": "object", + "required": [ + "t_sep" + ], + "properties": { + "t_sep": { + "$ref": "#/definitions/models.TSepPost" + } + } + }, + "models.SepPutRequest": { + "type": "object", + "required": [ + "t_sep" + ], + "properties": { + "t_sep": { + "$ref": "#/definitions/models.TSepPut" + } + } + }, + "models.SepResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "message": { + "type": "string" + } + } + }, + "models.Skdp": { + "type": "object", + "required": [ + "kodeDPJP", + "noSurat" + ], + "properties": { + "kodeDPJP": { + "type": "string" + }, + "noSurat": { + "type": "string" + } + } + }, + "models.Suplesi": { + "type": "object", + "properties": { + "lokasiLaka": { + "$ref": "#/definitions/models.LokasiLaka" + }, + "noSepSuplesi": { + "type": "string" + }, + "suplesi": { + "type": "string" + } + } + }, + "models.TSepPost": { + "type": "object", + "required": [ + "cob", + "diagAwal", + "jaminan", + "jnsPelayanan", + "katarak", + "klsRawat", + "noKartu", + "noMR", + "poli", + "ppkPelayanan", + "rujukan", + "skdp", + "tglSep", + "user" + ], + "properties": { + "assesmentPel": { + "type": "string" + }, + "catatan": { + "type": "string" + }, + "cob": { + "$ref": "#/definitions/models.Flag" + }, + "diagAwal": { + "type": "string" + }, + "dpjpLayan": { + "type": "string" + }, + "flagProcedure": { + "type": "string" + }, + "jaminan": { + "$ref": "#/definitions/models.Jaminan" + }, + "jnsPelayanan": { + "type": "string" + }, + "katarak": { + "$ref": "#/definitions/models.Flag" + }, + "kdPenunjang": { + "type": "string" + }, + "klsRawat": { + "$ref": "#/definitions/models.KlsRawatPost" + }, + "noKartu": { + "type": "string" + }, + "noMR": { + "type": "string" + }, + "noTelp": { + "type": "string" + }, + "poli": { + "$ref": "#/definitions/models.Poli" + }, + "ppkPelayanan": { + "type": "string" + }, + "rujukan": { + "$ref": "#/definitions/models.Rujukan" + }, + "skdp": { + "$ref": "#/definitions/models.Skdp" + }, + "tglSep": { + "description": "yyyy-MM-dd", + "type": "string" + }, + "tujuanKunj": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "models.TSepPut": { + "type": "object", + "required": [ + "noSep", + "user" + ], + "properties": { + "catatan": { + "type": "string" + }, + "cob": { + "$ref": "#/definitions/models.Flag" + }, + "diagAwal": { + "type": "string" + }, + "dpjpLayan": { + "type": "string" + }, + "jaminan": { + "$ref": "#/definitions/models.Jaminan" + }, + "katarak": { + "$ref": "#/definitions/models.Flag" + }, + "klsRawat": { + "$ref": "#/definitions/models.KlsRawatPut" + }, + "noMR": { + "type": "string" + }, + "noSep": { + "type": "string" + }, + "noTelp": { + "type": "string" + }, + "poli": { + "$ref": "#/definitions/models.Poli" + }, + "user": { + "type": "string" + } + } + }, "sql.NullString": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 8bc9b8df..721069e5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,5 +1,67 @@ basePath: /api/v1 definitions: + api-service_internal_models.AggregateData: + properties: + by_dinas: + additionalProperties: + type: integer + type: object + by_jenis: + additionalProperties: + type: integer + type: object + by_status: + additionalProperties: + type: integer + type: object + created_today: + type: integer + last_updated: + type: string + total_active: + type: integer + total_draft: + type: integer + total_inactive: + type: integer + updated_today: + type: integer + type: object + api-service_internal_models.ErrorResponse: + properties: + code: + type: integer + error: + type: string + message: + type: string + timestamp: + type: string + type: object + api-service_internal_models.MetaResponse: + properties: + current_page: + type: integer + has_next: + type: boolean + has_prev: + type: boolean + limit: + type: integer + offset: + type: integer + total: + type: integer + total_pages: + type: integer + type: object + api-service_internal_models.NullableInt32: + properties: + int32: + type: integer + valid: + type: boolean + type: object api-service_internal_models_auth.LoginRequest: properties: password: @@ -30,68 +92,6 @@ definitions: username: type: string type: object - api-service_internal_models_retribusi.AggregateData: - properties: - by_dinas: - additionalProperties: - type: integer - type: object - by_jenis: - additionalProperties: - type: integer - type: object - by_status: - additionalProperties: - type: integer - type: object - created_today: - type: integer - last_updated: - type: string - total_active: - type: integer - total_draft: - type: integer - total_inactive: - type: integer - updated_today: - type: integer - type: object - api-service_internal_models_retribusi.ErrorResponse: - properties: - code: - type: integer - error: - type: string - message: - type: string - timestamp: - type: string - type: object - api-service_internal_models_retribusi.MetaResponse: - properties: - current_page: - type: integer - has_next: - type: boolean - has_prev: - type: boolean - limit: - type: integer - offset: - type: integer - total: - type: integer - total_pages: - type: integer - type: object - api-service_internal_models_retribusi.NullableInt32: - properties: - int32: - type: integer - valid: - type: boolean - type: object api-service_internal_models_retribusi.Retribusi: properties: date_created: @@ -119,7 +119,7 @@ definitions: satuan_overtime: $ref: '#/definitions/sql.NullString' sort: - $ref: '#/definitions/api-service_internal_models_retribusi.NullableInt32' + $ref: '#/definitions/api-service_internal_models.NullableInt32' status: type: string tarif: @@ -224,9 +224,9 @@ definitions: message: type: string meta: - $ref: '#/definitions/api-service_internal_models_retribusi.MetaResponse' + $ref: '#/definitions/api-service_internal_models.MetaResponse' summary: - $ref: '#/definitions/api-service_internal_models_retribusi.AggregateData' + $ref: '#/definitions/api-service_internal_models.AggregateData' type: object api-service_internal_models_retribusi.RetribusiUpdateRequest: properties: @@ -292,6 +292,9 @@ definitions: message: type: string type: object + gin.H: + additionalProperties: {} + type: object models.DiagnosaResponse: properties: data: @@ -300,6 +303,223 @@ definitions: message: type: string type: object + models.Flag: + properties: + cob: + type: string + required: + - cob + type: object + models.Jaminan: + properties: + lakaLantas: + type: string + noLP: + type: string + penjamin: + $ref: '#/definitions/models.Penjamin' + required: + - lakaLantas + type: object + models.KlsRawatPost: + properties: + klsRawatHak: + type: string + klsRawatNaik: + type: string + pembiayaan: + type: string + penanggungJawab: + type: string + required: + - klsRawatHak + type: object + models.KlsRawatPut: + properties: + klsRawatHak: + type: string + klsRawatNaik: + type: string + pembiayaan: + type: string + penanggungJawab: + type: string + type: object + models.LokasiLaka: + properties: + kdKabupaten: + type: string + kdKecamatan: + type: string + kdPropinsi: + type: string + type: object + models.Penjamin: + properties: + keterangan: + type: string + suplesi: + $ref: '#/definitions/models.Suplesi' + tglKejadian: + type: string + type: object + models.Poli: + properties: + eksekutif: + type: string + tujuan: + type: string + required: + - eksekutif + type: object + models.Rujukan: + properties: + asalRujukan: + type: string + noRujukan: + type: string + ppkRujukan: + type: string + tglRujukan: + type: string + required: + - asalRujukan + - noRujukan + - ppkRujukan + - tglRujukan + type: object + models.SepPostRequest: + properties: + t_sep: + $ref: '#/definitions/models.TSepPost' + required: + - t_sep + type: object + models.SepPutRequest: + properties: + t_sep: + $ref: '#/definitions/models.TSepPut' + required: + - t_sep + type: object + models.SepResponse: + properties: + data: + additionalProperties: true + type: object + message: + type: string + type: object + models.Skdp: + properties: + kodeDPJP: + type: string + noSurat: + type: string + required: + - kodeDPJP + - noSurat + type: object + models.Suplesi: + properties: + lokasiLaka: + $ref: '#/definitions/models.LokasiLaka' + noSepSuplesi: + type: string + suplesi: + type: string + type: object + models.TSepPost: + properties: + assesmentPel: + type: string + catatan: + type: string + cob: + $ref: '#/definitions/models.Flag' + diagAwal: + type: string + dpjpLayan: + type: string + flagProcedure: + type: string + jaminan: + $ref: '#/definitions/models.Jaminan' + jnsPelayanan: + type: string + katarak: + $ref: '#/definitions/models.Flag' + kdPenunjang: + type: string + klsRawat: + $ref: '#/definitions/models.KlsRawatPost' + noKartu: + type: string + noMR: + type: string + noTelp: + type: string + poli: + $ref: '#/definitions/models.Poli' + ppkPelayanan: + type: string + rujukan: + $ref: '#/definitions/models.Rujukan' + skdp: + $ref: '#/definitions/models.Skdp' + tglSep: + description: yyyy-MM-dd + type: string + tujuanKunj: + type: string + user: + type: string + required: + - cob + - diagAwal + - jaminan + - jnsPelayanan + - katarak + - klsRawat + - noKartu + - noMR + - poli + - ppkPelayanan + - rujukan + - skdp + - tglSep + - user + type: object + models.TSepPut: + properties: + catatan: + type: string + cob: + $ref: '#/definitions/models.Flag' + diagAwal: + type: string + dpjpLayan: + type: string + jaminan: + $ref: '#/definitions/models.Jaminan' + katarak: + $ref: '#/definitions/models.Flag' + klsRawat: + $ref: '#/definitions/models.KlsRawatPut' + noMR: + type: string + noSep: + type: string + noTelp: + type: string + poli: + $ref: '#/definitions/models.Poli' + user: + type: string + required: + - noSep + - user + type: object sql.NullString: properties: string: @@ -546,15 +766,15 @@ paths: "400": description: Invalid ID format schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' "404": description: Retribusi not found schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' "500": description: Internal server error schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' summary: Delete retribusi tags: - retribusi @@ -578,15 +798,15 @@ paths: "400": description: Invalid ID format schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' "404": description: Retribusi not found schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' "500": description: Internal server error schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' summary: Get Retribusi by ID tags: - retribusi @@ -616,15 +836,15 @@ paths: "400": description: Bad request or validation error schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' "404": description: Retribusi not found schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' "500": description: Internal server error schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' summary: Update retribusi tags: - retribusi @@ -675,11 +895,11 @@ paths: "400": description: Bad request schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' "500": description: Internal server error schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' summary: Get retribusi with pagination and optional aggregation tags: - retribusi @@ -704,14 +924,60 @@ paths: "400": description: Bad request or validation error schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' "500": description: Internal server error schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' summary: Create retribusi tags: - retribusi + /api/v1/retribusis/dynamic: + get: + consumes: + - application/json + description: Returns retribusis with advanced dynamic filtering like Directus + parameters: + - description: Fields to select (e.g., fields=*.*) + in: query + name: fields + type: string + - description: Dynamic filters (e.g., filter[Jenis][_eq]=value) + in: query + name: filter[column][operator] + type: string + - description: Sort fields (e.g., sort=date_created,-Jenis) + in: query + name: sort + type: string + - default: 10 + description: Limit + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/api-service_internal_models_retribusi.RetribusiGetResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Get retribusi with dynamic filtering + tags: + - retribusi /api/v1/retribusis/stats: get: consumes: @@ -728,11 +994,11 @@ paths: "200": description: Statistics data schema: - $ref: '#/definitions/api-service_internal_models_retribusi.AggregateData' + $ref: '#/definitions/api-service_internal_models.AggregateData' "500": description: Internal server error schema: - $ref: '#/definitions/api-service_internal_models_retribusi.ErrorResponse' + $ref: '#/definitions/api-service_internal_models.ErrorResponse' summary: Get retribusi statistics tags: - retribusi @@ -801,6 +1067,127 @@ paths: summary: Generate token directly tags: - Token + /sep: + post: + consumes: + - application/json + description: Create a new Surat Eligibilitas Peserta + parameters: + - description: SEP creation request + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.SepPostRequest' + produces: + - application/json + responses: + "200": + description: SEP created successfully + schema: + $ref: '#/definitions/models.SepResponse' + "400": + description: Invalid request + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Create a new SEP + tags: + - bpjs + put: + consumes: + - application/json + description: Update an existing Surat Eligibilitas Peserta + parameters: + - description: SEP update request + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.SepPutRequest' + produces: + - application/json + responses: + "200": + description: SEP updated successfully + schema: + $ref: '#/definitions/models.SepResponse' + "400": + description: Invalid request + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Update an existing SEP + tags: + - bpjs + /sep/{noSep}: + delete: + consumes: + - application/json + description: Delete a Surat Eligibilitas Peserta by noSep + parameters: + - description: No SEP + in: path + name: noSep + required: true + type: string + - description: User + in: query + name: user + required: true + type: string + produces: + - application/json + responses: + "200": + description: SEP deleted successfully + schema: + $ref: '#/definitions/models.SepResponse' + "400": + description: Invalid request + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Delete an existing SEP + tags: + - bpjs + get: + consumes: + - application/json + description: Retrieve a Surat Eligibilitas Peserta by noSep + parameters: + - description: No SEP + in: path + name: noSep + required: true + type: string + produces: + - application/json + responses: + "200": + description: Data SEP retrieved successfully + schema: + $ref: '#/definitions/models.SepResponse' + "400": + description: Invalid request + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get an existing SEP + tags: + - bpjs schemes: - http - https diff --git a/example.env b/example.env index 59a9cf0f..537853c2 100644 --- a/example.env +++ b/example.env @@ -68,4 +68,13 @@ KEYCLOAK_ENABLED=true BPJS_BASEURL=https://apijkn.bpjs-kesehatan.go.id/vclaim-rest BPJS_CONSID=5257 BPJS_USERKEY=4cf1cbef8c008440bbe9ef9ba789e482 -BPJS_SECRETKEY=1bV363512D \ No newline at end of file +BPJS_SECRETKEY=1bV363512D + +BRIDGING_SATUSEHAT_ORG_ID=100026555 +BRIDGING_SATUSEHAT_FASYAKES_ID=3573011 +BRIDGING_SATUSEHAT_CLIENT_ID=l1ZgJGW6K5pnrqGUikWM7fgIoquA2AQ5UUG0U8WqHaq2VEyZ +BRIDGING_SATUSEHAT_CLIENT_SECRET=Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lGTEjjcR6zxa06ThPDy +BRIDGING_SATUSEHAT_AUTH_URL=https://api-satusehat.kemkes.go.id/oauth2/v1 +BRIDGING_SATUSEHAT_BASE_URL=https://api-satusehat.kemkes.go.id/fhir-r4/v1 +BRIDGING_SATUSEHAT_CONSENT_URL=https://api-satusehat.dto.kemkes.go.id/consent/v1 +BRIDGING_SATUSEHAT_KFA_URL=https://api-satusehat.kemkes.go.id/kfa-v2 \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index efa806b5..d2a3311f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,6 +19,7 @@ type Config struct { ReadReplicas map[string][]DatabaseConfig // For read replicas Keycloak KeycloakConfig Bpjs BpjsConfig + SatuSehat SatuSehatConfig } type ServerConfig struct { @@ -58,6 +59,18 @@ type BpjsConfig struct { Timeout time.Duration `json:"timeout"` } +type SatuSehatConfig struct { + OrgID string `json:"org_id"` + FasyakesID string `json:"fasyakes_id"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + AuthURL string `json:"auth_url"` + BaseURL string `json:"base_url"` + ConsentURL string `json:"consent_url"` + KFAURL string `json:"kfa_url"` + Timeout time.Duration `json:"timeout"` +} + // SetHeader generates required headers for BPJS VClaim API func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) { timenow := time.Now().UTC() @@ -117,6 +130,17 @@ func LoadConfig() *Config { SecretKey: getEnv("BPJS_SECRETKEY", ""), Timeout: parseDuration(getEnv("BPJS_TIMEOUT", "30s")), }, + SatuSehat: SatuSehatConfig{ + OrgID: getEnv("BRIDGING_SATUSEHAT_ORG_ID", ""), + FasyakesID: getEnv("BRIDGING_SATUSEHAT_FASYAKES_ID", ""), + ClientID: getEnv("BRIDGING_SATUSEHAT_CLIENT_ID", ""), + ClientSecret: getEnv("BRIDGING_SATUSEHAT_CLIENT_SECRET", ""), + AuthURL: getEnv("BRIDGING_SATUSEHAT_AUTH_URL", "https://api-satusehat.kemkes.go.id/oauth2/v1"), + BaseURL: getEnv("BRIDGING_SATUSEHAT_BASE_URL", "https://api-satusehat.kemkes.go.id/fhir-r4/v1"), + ConsentURL: getEnv("BRIDGING_SATUSEHAT_CONSENT_URL", "https://api-satusehat.dto.kemkes.go.id/consent/v1"), + KFAURL: getEnv("BRIDGING_SATUSEHAT_KFA_URL", "https://api-satusehat.kemkes.go.id/kfa-v2"), + Timeout: parseDuration(getEnv("BRIDGING_SATUSEHAT_TIMEOUT", "30s")), + }, } // Load database configurations @@ -622,5 +646,25 @@ func (c *Config) Validate() error { } } + // Validate SatuSehat configuration + if c.SatuSehat.OrgID == "" { + log.Fatal("SatuSehat Organization ID is required") + } + if c.SatuSehat.FasyakesID == "" { + log.Fatal("SatuSehat Fasyankes ID is required") + } + if c.SatuSehat.ClientID == "" { + log.Fatal("SatuSehat Client ID is required") + } + if c.SatuSehat.ClientSecret == "" { + log.Fatal("SatuSehat Client Secret is required") + } + if c.SatuSehat.AuthURL == "" { + log.Fatal("SatuSehat Auth URL is required") + } + if c.SatuSehat.BaseURL == "" { + log.Fatal("SatuSehat Base URL is required") + } + return nil } diff --git a/internal/handlers/retribusi/retribusi.go b/internal/handlers/retribusi/retribusi.go index eb0dca59..9b25975b 100644 --- a/internal/handlers/retribusi/retribusi.go +++ b/internal/handlers/retribusi/retribusi.go @@ -74,8 +74,8 @@ func NewRetribusiHandler() *RetribusiHandler { // @Param dinas query string false "Filter by dinas" // @Param search query string false "Search in multiple fields" // @Success 200 {object} modelsretribusi.RetribusiGetResponse "Success response" -// @Failure 400 {object} modelsretribusi.ErrorResponse "Bad request" -// @Failure 500 {object} modelsretribusi.ErrorResponse "Internal server error" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/retribusis [get] func (h *RetribusiHandler) GetRetribusi(c *gin.Context) { // Parse pagination parameters @@ -186,9 +186,9 @@ func (h *RetribusiHandler) GetRetribusi(c *gin.Context) { // @Produce json // @Param id path string true "Retribusi ID (UUID)" // @Success 200 {object} modelsretribusi.RetribusiGetByIDResponse "Success response" -// @Failure 400 {object} modelsretribusi.ErrorResponse "Invalid ID format" -// @Failure 404 {object} modelsretribusi.ErrorResponse "Retribusi not found" -// @Failure 500 {object} modelsretribusi.ErrorResponse "Internal server error" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "Retribusi not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/retribusi/{id} [get] func (h *RetribusiHandler) GetRetribusiByID(c *gin.Context) { id := c.Param("id") @@ -238,8 +238,8 @@ func (h *RetribusiHandler) GetRetribusiByID(c *gin.Context) { // @Param limit query int false "Limit" default(10) // @Param offset query int false "Offset" default(0) // @Success 200 {object} modelsretribusi.RetribusiGetResponse "Success response" -// @Failure 400 {object} modelsretribusi.ErrorResponse "Bad request" -// @Failure 500 {object} modelsretribusi.ErrorResponse "Internal server error" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/retribusis/dynamic [get] func (h *RetribusiHandler) GetRetribusiDynamic(c *gin.Context) { // Parse query parameters @@ -501,8 +501,8 @@ func (h *RetribusiHandler) SearchRetribusiAdvanced(c *gin.Context) { // @Produce json // @Param request body modelsretribusi.RetribusiCreateRequest true "Retribusi creation request" // @Success 201 {object} modelsretribusi.RetribusiCreateResponse "Retribusi created successfully" -// @Failure 400 {object} modelsretribusi.ErrorResponse "Bad request or validation error" -// @Failure 500 {object} modelsretribusi.ErrorResponse "Internal server error" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/retribusis [post] func (h *RetribusiHandler) CreateRetribusi(c *gin.Context) { var req modelsretribusi.RetribusiCreateRequest @@ -556,9 +556,9 @@ func (h *RetribusiHandler) CreateRetribusi(c *gin.Context) { // @Param id path string true "Retribusi ID (UUID)" // @Param request body modelsretribusi.RetribusiUpdateRequest true "Retribusi update request" // @Success 200 {object} modelsretribusi.RetribusiUpdateResponse "Retribusi updated successfully" -// @Failure 400 {object} modelsretribusi.ErrorResponse "Bad request or validation error" -// @Failure 404 {object} modelsretribusi.ErrorResponse "Retribusi not found" -// @Failure 500 {object} modelsretribusi.ErrorResponse "Internal server error" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 404 {object} models.ErrorResponse "Retribusi not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/retribusi/{id} [put] func (h *RetribusiHandler) UpdateRetribusi(c *gin.Context) { id := c.Param("id") @@ -619,9 +619,9 @@ func (h *RetribusiHandler) UpdateRetribusi(c *gin.Context) { // @Produce json // @Param id path string true "Retribusi ID (UUID)" // @Success 200 {object} modelsretribusi.RetribusiDeleteResponse "Retribusi deleted successfully" -// @Failure 400 {object} modelsretribusi.ErrorResponse "Invalid ID format" -// @Failure 404 {object} modelsretribusi.ErrorResponse "Retribusi not found" -// @Failure 500 {object} modelsretribusi.ErrorResponse "Internal server error" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "Retribusi not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/retribusi/{id} [delete] func (h *RetribusiHandler) DeleteRetribusi(c *gin.Context) { id := c.Param("id") @@ -666,8 +666,8 @@ func (h *RetribusiHandler) DeleteRetribusi(c *gin.Context) { // @Accept json // @Produce json // @Param status query string false "Filter statistics by status" -// @Success 200 {object} modelsretribusi.AggregateData "Statistics data" -// @Failure 500 {object} modelsretribusi.ErrorResponse "Internal server error" +// @Success 200 {object} models.AggregateData "Statistics data" +// @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/retribusis/stats [get] func (h *RetribusiHandler) GetRetribusiStats(c *gin.Context) { dbConn, err := h.db.GetDB("postgres_satudata") diff --git a/internal/handlers/satusehat/patient_handler.go b/internal/handlers/satusehat/patient_handler.go new file mode 100644 index 00000000..7f2f09d0 --- /dev/null +++ b/internal/handlers/satusehat/patient_handler.go @@ -0,0 +1,192 @@ +package satusehat + +import ( + "net/http" + + "api-service/internal/services/satusehat" + + "github.com/gin-gonic/gin" +) + +type PatientHandler struct { + service *satusehat.SatuSehatService +} + +func NewPatientHandler(service *satusehat.SatuSehatService) *PatientHandler { + return &PatientHandler{ + service: service, + } +} + +// SearchPatientByNIK godoc +// @Summary Search patient by NIK +// @Description Search patient data from SatuSehat by National Identity Number (NIK) +// @Tags SatuSehat +// @Accept json +// @Produce json +// @Param nik query string true "National Identity Number (NIK)" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]interface{} +// @Failure 500 {object} map[string]interface{} +// @Router /satusehat/patient/search/nik [get] +func (h *PatientHandler) SearchPatientByNIK(c *gin.Context) { + nik := c.Query("nik") + if nik == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Bad Request", + "message": "NIK parameter is required", + }) + return + } + + patientResp, err := h.service.SearchPatientByNIK(c.Request.Context(), nik) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal Server Error", + "message": err.Error(), + }) + return + } + + patientInfo, err := satusehat.ExtractPatientInfo(patientResp) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "error": "Not Found", + "message": "Patient not found", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": patientInfo, + }) +} + +// SearchPatientByName godoc +// @Summary Search patient by name +// @Description Search patient data from SatuSehat by name +// @Tags SatuSehat +// @Accept json +// @Produce json +// @Param name query string true "Patient name" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]interface{} +// @Failure 500 {object} map[string]interface{} +// @Router /satusehat/patient/search/name [get] +func (h *PatientHandler) SearchPatientByName(c *gin.Context) { + name := c.Query("name") + if name == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Bad Request", + "message": "Name parameter is required", + }) + return + } + + patientResp, err := h.service.SearchPatientByName(c.Request.Context(), name) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal Server Error", + "message": err.Error(), + }) + return + } + + if patientResp == nil || len(patientResp.Entry) == 0 { + c.JSON(http.StatusNotFound, gin.H{ + "error": "Not Found", + "message": "Patient not found", + }) + return + } + + // Return all found patients + var patients []map[string]interface{} + for _, entry := range patientResp.Entry { + patientInfo := map[string]interface{}{ + "id": entry.Resource.ID, + "name": satusehat.ExtractPatientName(entry.Resource.Name), + "nik": satusehat.ExtractNIK(entry.Resource.Identifier), + "gender": entry.Resource.Gender, + "birthDate": entry.Resource.BirthDate, + "address": satusehat.ExtractAddress(entry.Resource.Address), + "phone": satusehat.ExtractPhone(entry.Resource.Telecom), + "lastUpdated": entry.Resource.Meta.LastUpdated, + } + patients = append(patients, patientInfo) + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": patients, + "total": len(patients), + }) +} + +// CreatePatient godoc +// @Summary Create new patient +// @Description Create new patient data in SatuSehat +// @Tags SatuSehat +// @Accept json +// @Produce json +// @Param patient body map[string]interface{} true "Patient data" +// @Success 201 {object} map[string]interface{} +// @Failure 400 {object} map[string]interface{} +// @Failure 500 {object} map[string]interface{} +// @Router /satusehat/patient [post] +func (h *PatientHandler) CreatePatient(c *gin.Context) { + var patientData map[string]interface{} + if err := c.ShouldBindJSON(&patientData); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Bad Request", + "message": "Invalid JSON format", + }) + return + } + + response, err := h.service.CreatePatient(c.Request.Context(), patientData) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal Server Error", + "message": err.Error(), + }) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "success": true, + "data": response, + }) +} + +// GetAccessToken godoc +// @Summary Get access token +// @Description Get SatuSehat access token +// @Tags SatuSehat +// @Accept json +// @Produce json +// @Success 200 {object} map[string]interface{} +// @Failure 500 {object} map[string]interface{} +// @Router /satusehat/token [get] +func (h *PatientHandler) GetAccessToken(c *gin.Context) { + token, err := h.service.GetAccessToken(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal Server Error", + "message": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": map[string]interface{}{ + "access_token": token.AccessToken, + "token_type": token.TokenType, + "expires_in": token.ExpiresIn, + "scope": token.Scope, + "issued_at": token.IssuedAt, + }, + }) +} diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index 405f3b2b..5ea2965e 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -5,8 +5,10 @@ import ( authHandlers "api-service/internal/handlers/auth" bpjsPesertaHandlers "api-service/internal/handlers/bpjs/reference" retribusiHandlers "api-service/internal/handlers/retribusi" + satusehatHandlers "api-service/internal/handlers/satusehat" "api-service/internal/middleware" services "api-service/internal/services/auth" + satusehatServices "api-service/internal/services/satusehat" "api-service/pkg/logger" "github.com/gin-gonic/gin" @@ -32,6 +34,12 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { logger.Fatal("Failed to initialize auth service") } + // Initialize SatuSehat service + satusehatService := satusehatServices.NewSatuSehatService(&cfg.SatuSehat) + if satusehatService == nil { + logger.Fatal("Failed to initialize SatuSehat service") + } + // Swagger UI route router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) @@ -59,6 +67,16 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs) v1.GET("/bpjs/peserta/nik/:nik/tglSEP/:tglSEP", bpjsPesertaHandler.GetPesertaByNIK) + // SatuSehat endpoints + satusehatPatientHandler := satusehatHandlers.NewPatientHandler(satusehatService) + satusehatGroup := v1.Group("/satusehat") + { + satusehatGroup.GET("/patient/search/nik", satusehatPatientHandler.SearchPatientByNIK) + satusehatGroup.GET("/patient/search/name", satusehatPatientHandler.SearchPatientByName) + satusehatGroup.POST("/patient", satusehatPatientHandler.CreatePatient) + satusehatGroup.GET("/token", satusehatPatientHandler.GetAccessToken) + } + // ============= PUBLISHED ROUTES =============================================== // // Retribusi endpoints diff --git a/internal/services/satusehat/satusehat_service.go b/internal/services/satusehat/satusehat_service.go new file mode 100644 index 00000000..988ca3dc --- /dev/null +++ b/internal/services/satusehat/satusehat_service.go @@ -0,0 +1,350 @@ +package satusehat + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "api-service/internal/config" +) + +type SatuSehatService struct { + config *config.SatuSehatConfig + client *http.Client + token *TokenResponse +} + +type TokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` + IssuedAt time.Time +} + +type PatientResponse struct { + ResourceType string `json:"resourceType"` + ID string `json:"id"` + Meta struct { + VersionID string `json:"versionId"` + LastUpdated string `json:"lastUpdated"` + } `json:"meta"` + Type string `json:"type"` + Total int `json:"total"` + Link []Link `json:"link"` + Entry []Entry `json:"entry"` +} + +type Link struct { + Relation string `json:"relation"` + URL string `json:"url"` +} + +type Entry struct { + FullURL string `json:"fullUrl"` + Resource struct { + ResourceType string `json:"resourceType"` + ID string `json:"id"` + Meta struct { + VersionID string `json:"versionId"` + LastUpdated string `json:"lastUpdated"` + Profile []string `json:"profile"` + } `json:"meta"` + Identifier []Identifier `json:"identifier"` + Name []Name `json:"name"` + Telecom []Telecom `json:"telecom"` + Gender string `json:"gender"` + BirthDate string `json:"birthDate"` + Deceased bool `json:"deceasedBoolean"` + Address []Address `json:"address"` + MaritalStatus struct { + Coding []Coding `json:"coding"` + } `json:"maritalStatus"` + MultipleBirth bool `json:"multipleBirthBoolean"` + Contact []Contact `json:"contact"` + Communication []Communication `json:"communication"` + Extension []Extension `json:"extension"` + } `json:"resource"` + Search struct { + Mode string `json:"mode"` + } `json:"search"` +} + +type Identifier struct { + System string `json:"system"` + Value string `json:"value"` + Use string `json:"use,omitempty"` +} + +type Name struct { + Use string `json:"use"` + Text string `json:"text"` + Family string `json:"family"` + Given []string `json:"given"` +} + +type Telecom struct { + System string `json:"system"` + Value string `json:"value"` + Use string `json:"use,omitempty"` +} + +type Address struct { + Use string `json:"use"` + Type string `json:"type"` + Line []string `json:"line"` + City string `json:"city"` + PostalCode string `json:"postalCode"` + Country string `json:"country"` + Extension []Extension `json:"extension"` +} + +type Coding struct { + System string `json:"system"` + Code string `json:"code"` + Display string `json:"display"` +} + +type Contact struct { + Relationship []Coding `json:"relationship"` + Name Name `json:"name"` + Telecom []Telecom `json:"telecom"` + Address Address `json:"address"` + Gender string `json:"gender"` +} + +type Communication struct { + Language Coding `json:"language"` + Preferred bool `json:"preferred"` +} + +type Extension struct { + URL string `json:"url"` + ValueAddress Address `json:"valueAddress,omitempty"` + ValueCode string `json:"valueCode,omitempty"` +} + +func NewSatuSehatService(cfg *config.SatuSehatConfig) *SatuSehatService { + return &SatuSehatService{ + config: cfg, + client: &http.Client{ + Timeout: cfg.Timeout, + }, + } +} + +func (s *SatuSehatService) GetAccessToken(ctx context.Context) (*TokenResponse, error) { + // Check if we have a valid token + if s.token != nil && time.Since(s.token.IssuedAt) < time.Duration(s.token.ExpiresIn-60)*time.Second { + return s.token, nil + } + + url := fmt.Sprintf("%s/accesstoken?grant_type=client_credentials", s.config.AuthURL) + + req, err := http.NewRequestWithContext(ctx, "POST", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + req.SetBasicAuth(s.config.ClientID, s.config.ClientSecret) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := s.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to get access token: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get access token, status: %s", resp.Status) + } + + var tokenResp TokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + return nil, fmt.Errorf("failed to decode token response: %v", err) + } + + tokenResp.IssuedAt = time.Now() + s.token = &tokenResp + + return &tokenResp, nil +} + +func (s *SatuSehatService) SearchPatientByNIK(ctx context.Context, nik string) (*PatientResponse, error) { + token, err := s.GetAccessToken(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get access token: %v", err) + } + + url := fmt.Sprintf("%s/Patient?identifier=https://fhir.kemkes.go.id/id/nik|%s", s.config.BaseURL, nik) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + req.Header.Set("Content-Type", "application/json") + + resp, err := s.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to search patient: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to search patient, status: %s", resp.Status) + } + + var patientResp PatientResponse + if err := json.NewDecoder(resp.Body).Decode(&patientResp); err != nil { + return nil, fmt.Errorf("failed to decode patient response: %v", err) + } + + return &patientResp, nil +} + +func (s *SatuSehatService) SearchPatientByName(ctx context.Context, name string) (*PatientResponse, error) { + token, err := s.GetAccessToken(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get access token: %v", err) + } + + url := fmt.Sprintf("%s/Patient?name=%s", s.config.BaseURL, name) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + req.Header.Set("Content-Type", "application/json") + + resp, err := s.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to search patient: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to search patient, status: %s", resp.Status) + } + + var patientResp PatientResponse + if err := json.NewDecoder(resp.Body).Decode(&patientResp); err != nil { + return nil, fmt.Errorf("failed to decode patient response: %v", err) + } + + return &patientResp, nil +} + +func (s *SatuSehatService) CreatePatient(ctx context.Context, patientData map[string]interface{}) (map[string]interface{}, error) { + token, err := s.GetAccessToken(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get access token: %v", err) + } + + url := fmt.Sprintf("%s/Patient", s.config.BaseURL) + + patientData["resourceType"] = "Patient" + + jsonData, err := json.Marshal(patientData) + if err != nil { + return nil, fmt.Errorf("failed to marshal patient data: %v", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(jsonData))) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + req.Header.Set("Content-Type", "application/json") + + resp, err := s.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to create patient: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("failed to create patient, status: %s", resp.Status) + } + + var response map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, fmt.Errorf("failed to decode response: %v", err) + } + + return response, nil +} + +// Helper function to extract patient information +func ExtractPatientInfo(patientResp *PatientResponse) (map[string]interface{}, error) { + if patientResp == nil || len(patientResp.Entry) == 0 { + return nil, fmt.Errorf("no patient data found") + } + + entry := patientResp.Entry[0] + resource := entry.Resource + + patientInfo := map[string]interface{}{ + "id": resource.ID, + "name": ExtractPatientName(resource.Name), + "nik": ExtractNIK(resource.Identifier), + "gender": resource.Gender, + "birthDate": resource.BirthDate, + "address": ExtractAddress(resource.Address), + "phone": ExtractPhone(resource.Telecom), + "lastUpdated": resource.Meta.LastUpdated, + } + + return patientInfo, nil +} + +func ExtractPatientName(names []Name) string { + for _, name := range names { + if name.Use == "official" || name.Text != "" { + if name.Text != "" { + return name.Text + } + return fmt.Sprintf("%s %s", strings.Join(name.Given, " "), name.Family) + } + } + return "" +} + +func ExtractNIK(identifiers []Identifier) string { + for _, ident := range identifiers { + if ident.System == "https://fhir.kemkes.go.id/id/nik" { + return ident.Value + } + } + return "" +} + +func ExtractAddress(addresses []Address) map[string]interface{} { + if len(addresses) == 0 { + return nil + } + + addr := addresses[0] + return map[string]interface{}{ + "line": strings.Join(addr.Line, ", "), + "city": addr.City, + "postalCode": addr.PostalCode, + "country": addr.Country, + } +} + +func ExtractPhone(telecoms []Telecom) string { + for _, telecom := range telecoms { + if telecom.System == "phone" { + return telecom.Value + } + } + return "" +} diff --git a/tools/generate-bpjs-handler.go b/tools/bpjs/generate-bpjs-handler.go similarity index 98% rename from tools/generate-bpjs-handler.go rename to tools/bpjs/generate-bpjs-handler.go index c3e5ac1d..e6c3c664 100644 --- a/tools/generate-bpjs-handler.go +++ b/tools/bpjs/generate-bpjs-handler.go @@ -182,7 +182,7 @@ func New` + data.Name + `Handler(cfg config.BpjsConfig) *` + data.Name + `Handle handlerContent += generateBpjsGetMethod(data) } - writeFile(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent) + writeFileBpjs(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent) } func generateBpjsCreateMethod(data BpjsHandlerData) string { @@ -490,7 +490,7 @@ func IsValidStatus(status string) bool { return false }` - writeFile(filepath.Join(modelDir, data.NameLower+".go"), modelContent) + writeFileBpjs(filepath.Join(modelDir, data.NameLower+".go"), modelContent) } // ================= ROUTES GENERATION ===================== @@ -634,7 +634,7 @@ func printBpjsRoutesSample(data BpjsHandlerData) { // ================= UTILITY FUNCTIONS ===================== -func writeFile(filename, content string) { +func writeFileBpjs(filename, content string) { if err := os.WriteFile(filename, []byte(content), 0644); err != nil { fmt.Printf("❌ Error creating file %s: %v\n", filename, err) return diff --git a/tools/general/generate-handler.go b/tools/general/generate-handler.go new file mode 100644 index 00000000..205c7bb0 --- /dev/null +++ b/tools/general/generate-handler.go @@ -0,0 +1,1590 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" +) + +// HandlerData contains template data for handler generation +type HandlerData struct { + Name string + NameLower string + NamePlural string + Category string + CategoryPath string + ModuleName string + TableName string + HasGet bool + HasPost bool + HasPut bool + HasDelete bool + HasStats bool + HasDynamic bool + HasSearch bool + HasFilter bool + HasPagination bool + Timestamp string +} + +func main() { + if len(os.Args) < 2 { + fmt.Println("Usage: go run generate-handler.go [category/]entity [methods]") + fmt.Println("Examples:") + fmt.Println(" go run generate-handler.go product get post put delete") + fmt.Println(" go run generate-handler.go retribusi/tarif get post put delete dynamic search") + os.Exit(1) + } + + // Parse entity path (could be "entity" or "category/entity") + entityPath := os.Args[1] + methods := []string{} + if len(os.Args) > 2 { + methods = os.Args[2:] + } else { + // Default methods with advanced features + methods = []string{"get", "post", "put", "delete", "dynamic", "search"} + } + + // Parse category and entity + var category, entityName string + if strings.Contains(entityPath, "/") { + parts := strings.Split(entityPath, "/") + if len(parts) != 2 { + fmt.Println("❌ Error: Invalid path format. Use 'category/entity' or just 'entity'") + os.Exit(1) + } + category = parts[0] + entityName = parts[1] + } else { + category = "" + entityName = entityPath + } + + // Format names + entityName = strings.Title(entityName) // PascalCase entity name + entityLower := strings.ToLower(entityName) + entityPlural := entityLower + "s" + + // Table name: include category if exists + var tableName string + if category != "" { + tableName = "data_" + category + "_" + entityLower + } else { + tableName = "data_" + entityLower + } + + data := HandlerData{ + Name: entityName, + NameLower: entityLower, + NamePlural: entityPlural, + Category: category, + CategoryPath: category, + ModuleName: "api-service", + TableName: tableName, + HasPagination: true, + HasFilter: true, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + } + + // Set methods based on arguments + for _, m := range methods { + switch strings.ToLower(m) { + case "get": + data.HasGet = true + case "post": + data.HasPost = true + case "put": + data.HasPut = true + case "delete": + data.HasDelete = true + case "stats": + data.HasStats = true + case "dynamic": + data.HasDynamic = true + case "search": + data.HasSearch = true + } + } + + // Always add stats if we have get + if data.HasGet { + data.HasStats = true + } + + // Create directories with improved logic + var handlerDir, modelDir string + if category != "" { + handlerDir = filepath.Join("internal", "handlers", category) + modelDir = filepath.Join("internal", "models", category) + } else { + handlerDir = filepath.Join("internal", "handlers") + modelDir = filepath.Join("internal", "models") + } + + // Create directories + for _, d := range []string{handlerDir, modelDir} { + if err := os.MkdirAll(d, 0755); err != nil { + panic(err) + } + } + + // Generate files + generateHandlerFile(data, handlerDir) + generateModelFile(data, modelDir) + updateRoutesFile(data) + + fmt.Printf("✅ Successfully generated handler: %s\n", entityName) + if category != "" { + fmt.Printf("📁 Category: %s\n", category) + } + fmt.Printf("📁 Handler: %s\n", filepath.Join(handlerDir, entityLower+".go")) + fmt.Printf("📁 Model: %s\n", filepath.Join(modelDir, entityLower+".go")) +} + +// ================= HANDLER GENERATION ===================== +func generateHandlerFile(data HandlerData, handlerDir string) { + // Build import path based on category + var modelsImportPath string + if data.Category != "" { + modelsImportPath = data.ModuleName + "/internal/models/" + data.Category + } else { + modelsImportPath = data.ModuleName + "/internal/models" + } + + handlerContent := `package handlers + +import ( + "` + data.ModuleName + `/internal/config" + "` + data.ModuleName + `/internal/database" + models "` + data.ModuleName + `/internal/models" + models` + data.NameLower + ` "` + modelsImportPath + `"` + + // Add conditional imports for dynamic and search functionality + if data.HasDynamic || data.HasSearch { + handlerContent += ` + utils "` + data.ModuleName + `/internal/utils/filters"` + } + + handlerContent += ` + "` + data.ModuleName + `/internal/utils/validation" + "context" + "database/sql" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" +) + +var ( + db database.Service + once sync.Once + validate *validator.Validate +) + +// Initialize the database connection and validator +func init() { + once.Do(func() { + db = database.New(config.LoadConfig()) + validate = validator.New() + // Register custom validations if needed + validate.RegisterValidation("` + data.NameLower + `_status", validate` + data.Name + `Status) + if db == nil { + log.Fatal("Failed to initialize database connection") + } + }) +} + +// Custom validation for ` + data.NameLower + ` status +func validate` + data.Name + `Status(fl validator.FieldLevel) bool { + return models.IsValidStatus(fl.Field().String()) +} + +// ` + data.Name + `Handler handles ` + data.NameLower + ` services +type ` + data.Name + `Handler struct { + db database.Service +} + +// New` + data.Name + `Handler creates a new ` + data.Name + `Handler +func New` + data.Name + `Handler() *` + data.Name + `Handler { + return &` + data.Name + `Handler{ + db: db, + } +}` + + // Add methods + if data.HasGet { + handlerContent += generateGetMethods(data) + } + if data.HasDynamic { + handlerContent += generateDynamicMethod(data) + } + if data.HasSearch { + handlerContent += generateSearchMethod(data) + } + if data.HasPost { + handlerContent += generateCreateMethod(data) + } + if data.HasPut { + handlerContent += generateUpdateMethod(data) + } + if data.HasDelete { + handlerContent += generateDeleteMethod(data) + } + if data.HasStats { + handlerContent += generateStatsMethod(data) + } + + // Add helper methods + handlerContent += generateHelperMethods(data) + + writeFile(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent) + +} + +func generateGetMethods(data HandlerData) string { + return ` + +// Get` + data.Name + ` godoc +// @Summary Get ` + data.NameLower + ` with pagination and optional aggregation +// @Description Returns a paginated list of ` + data.NamePlural + ` with optional summary statistics +// @Tags ` + data.NameLower + ` +// @Accept json +// @Produce json +// @Param limit query int false "Limit (max 100)" default(10) +// @Param offset query int false "Offset" default(0) +// @Param include_summary query bool false "Include aggregation summary" default(false) +// @Param status query string false "Filter by status" +// @Param search query string false "Search in multiple fields" +// @Success 200 {object} models` + data.NameLower + `.` + data.Name + `GetResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NamePlural + ` [get] +func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) { + // Parse pagination parameters + limit, offset, err := h.parsePaginationParams(c) + if err != nil { + h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest) + return + } + + // Parse filter parameters + filter := h.parseFilterParams(c) + includeAggregation := c.Query("include_summary") == "true" + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute concurrent operations + var ( + items []models` + data.NameLower + `.` + data.Name + ` + total int + aggregateData *models.AggregateData + wg sync.WaitGroup + errChan = make(chan error, 3) + mu sync.Mutex + ) + + // Fetch total count + wg.Add(1) + go func() { + defer wg.Done() + if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil { + mu.Lock() + errChan <- fmt.Errorf("failed to get total count: %w", err) + mu.Unlock() + } + }() + + // Fetch main data + wg.Add(1) + go func() { + defer wg.Done() + result, err := h.fetch` + data.Name + `s(ctx, dbConn, filter, limit, offset) + mu.Lock() + if err != nil { + errChan <- fmt.Errorf("failed to fetch data: %w", err) + } else { + items = result + } + mu.Unlock() + }() + + // Fetch aggregation data if requested + if includeAggregation { + wg.Add(1) + go func() { + defer wg.Done() + result, err := h.getAggregateData(ctx, dbConn, filter) + mu.Lock() + if err != nil { + errChan <- fmt.Errorf("failed to get aggregate data: %w", err) + } else { + aggregateData = result + } + mu.Unlock() + }() + } + + // Wait for all goroutines + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError) + return + } + } + + // Build response + meta := h.calculateMeta(limit, offset, total) + response := models` + data.NameLower + `.` + data.Name + `GetResponse{ + Message: "Data ` + data.NameLower + ` berhasil diambil", + Data: items, + Meta: meta, + } + + if includeAggregation && aggregateData != nil { + response.Summary = aggregateData + } + + c.JSON(http.StatusOK, response) +} + +// Get` + data.Name + `ByID godoc +// @Summary Get ` + data.Name + ` by ID +// @Description Returns a single ` + data.NameLower + ` by ID +// @Tags ` + data.NameLower + ` +// @Accept json +// @Produce json +// @Param id path string true "` + data.Name + ` ID (UUID)" +// @Success 200 {object} models` + data.NameLower + `.` + data.Name + `GetByIDResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NameLower + `/{id} [get] +func (h *` + data.Name + `Handler) Get` + data.Name + `ByID(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + item, err := h.get` + data.Name + `ByID(ctx, dbConn, id) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to get ` + data.NameLower + `", err, http.StatusInternalServerError) + } + return + } + + response := models` + data.NameLower + `.` + data.Name + `GetByIDResponse{ + Message: "` + data.Name + ` details retrieved successfully", + Data: item, + } + + c.JSON(http.StatusOK, response) +}` +} + +func generateDynamicMethod(data HandlerData) string { + return ` + +// Get` + data.Name + `Dynamic godoc +// @Summary Get ` + data.NameLower + ` with dynamic filtering +// @Description Returns ` + data.NamePlural + ` with advanced dynamic filtering like Directus +// @Tags ` + data.NameLower + ` +// @Accept json +// @Produce json +// @Param fields query string false "Fields to select (e.g., fields=*.*)" +// @Param filter[column][operator] query string false "Dynamic filters (e.g., filter[name][_eq]=value)" +// @Param sort query string false "Sort fields (e.g., sort=date_created,-name)" +// @Param limit query int false "Limit" default(10) +// @Param offset query int false "Offset" default(0) +// @Success 200 {object} models` + data.NameLower + `.` + data.Name + `GetResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NamePlural + `/dynamic [get] +func (h *` + data.Name + `Handler) Get` + data.Name + `Dynamic(c *gin.Context) { + // Parse query parameters + parser := utils.NewQueryParser().SetLimits(10, 100) + dynamicQuery, err := parser.ParseQuery(c.Request.URL.Query()) + if err != nil { + h.respondError(c, "Invalid query parameters", err, http.StatusBadRequest) + return + } + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute query with dynamic filtering + items, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, dynamicQuery) + if err != nil { + h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) + return + } + + // Build response + meta := h.calculateMeta(dynamicQuery.Limit, dynamicQuery.Offset, total) + response := models` + data.NameLower + `.` + data.Name + `GetResponse{ + Message: "Data ` + data.NameLower + ` berhasil diambil", + Data: items, + Meta: meta, + } + + c.JSON(http.StatusOK, response) +}` +} + +func generateSearchMethod(data HandlerData) string { + return ` + +// Search` + data.Name + `Advanced provides advanced search capabilities +func (h *` + data.Name + `Handler) Search` + data.Name + `Advanced(c *gin.Context) { + // Parse complex search parameters + searchQuery := c.Query("q") + if searchQuery == "" { + h.respondError(c, "Search query is required", fmt.Errorf("empty search query"), http.StatusBadRequest) + return + } + + // Build dynamic query for search + query := utils.DynamicQuery{ + Fields: []string{"*"}, + Filters: []utils.FilterGroup{{ + Filters: []utils.DynamicFilter{ + { + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }, + { + Column: "name", + Operator: utils.OpContains, + Value: searchQuery, + LogicOp: "OR", + }, + }, + LogicOp: "AND", + }}, + Sort: []utils.SortField{{ + Column: "date_created", + Order: "DESC", + }}, + Limit: 20, + Offset: 0, + } + + // Parse pagination if provided + if limit := c.Query("limit"); limit != "" { + if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 { + query.Limit = l + } + } + if offset := c.Query("offset"); offset != "" { + if o, err := strconv.Atoi(offset); err == nil && o >= 0 { + query.Offset = o + } + } + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute search + items, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, query) + if err != nil { + h.logAndRespondError(c, "Search failed", err, http.StatusInternalServerError) + return + } + + // Build response + meta := h.calculateMeta(query.Limit, query.Offset, total) + response := models` + data.NameLower + `.` + data.Name + `GetResponse{ + Message: fmt.Sprintf("Search results for '%s'", searchQuery), + Data: items, + Meta: meta, + } + + c.JSON(http.StatusOK, response) +}` +} + +func generateCreateMethod(data HandlerData) string { + return ` + +// Create` + data.Name + ` godoc +// @Summary Create ` + data.NameLower + ` +// @Description Creates a new ` + data.NameLower + ` record +// @Tags ` + data.NameLower + ` +// @Accept json +// @Produce json +// @Param request body models` + data.NameLower + `.` + data.Name + `CreateRequest true "` + data.Name + ` creation request" +// @Success 201 {object} models` + data.NameLower + `.` + data.Name + `CreateResponse "` + data.Name + ` created successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NamePlural + ` [post] +func (h *` + data.Name + `Handler) Create` + data.Name + `(c *gin.Context) { + var req models` + data.NameLower + `.` + data.Name + `CreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + // Validate request + if err := validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + // Validate duplicate and daily submission + if err := h.validate` + data.Name + `Submission(ctx, dbConn, &req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + item, err := h.create` + data.Name + `(ctx, dbConn, &req) + if err != nil { + h.logAndRespondError(c, "Failed to create ` + data.NameLower + `", err, http.StatusInternalServerError) + return + } + + response := models` + data.NameLower + `.` + data.Name + `CreateResponse{ + Message: "` + data.Name + ` berhasil dibuat", + Data: item, + } + + c.JSON(http.StatusCreated, response) +}` +} + +func generateUpdateMethod(data HandlerData) string { + return ` + +// Update` + data.Name + ` godoc +// @Summary Update ` + data.NameLower + ` +// @Description Updates an existing ` + data.NameLower + ` record +// @Tags ` + data.NameLower + ` +// @Accept json +// @Produce json +// @Param id path string true "` + data.Name + ` ID (UUID)" +// @Param request body models` + data.NameLower + `.` + data.Name + `UpdateRequest true "` + data.Name + ` update request" +// @Success 200 {object} models` + data.NameLower + `.` + data.Name + `UpdateResponse "` + data.Name + ` updated successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NameLower + `/{id} [put] +func (h *` + data.Name + `Handler) Update` + data.Name + `(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + var req models` + data.NameLower + `.` + data.Name + `UpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + // Set ID from path parameter + req.ID = id + + // Validate request + if err := validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + item, err := h.update` + data.Name + `(ctx, dbConn, &req) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to update ` + data.NameLower + `", err, http.StatusInternalServerError) + } + return + } + + response := models` + data.NameLower + `.` + data.Name + `UpdateResponse{ + Message: "` + data.Name + ` berhasil diperbarui", + Data: item, + } + + c.JSON(http.StatusOK, response) +}` +} + +func generateDeleteMethod(data HandlerData) string { + return ` + +// Delete` + data.Name + ` godoc +// @Summary Delete ` + data.NameLower + ` +// @Description Soft deletes a ` + data.NameLower + ` by setting status to 'deleted' +// @Tags ` + data.NameLower + ` +// @Accept json +// @Produce json +// @Param id path string true "` + data.Name + ` ID (UUID)" +// @Success 200 {object} models` + data.NameLower + `.` + data.Name + `DeleteResponse "` + data.Name + ` deleted successfully" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NameLower + `/{id} [delete] +func (h *` + data.Name + `Handler) Delete` + data.Name + `(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + err = h.delete` + data.Name + `(ctx, dbConn, id) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to delete ` + data.NameLower + `", err, http.StatusInternalServerError) + } + return + } + + response := models` + data.NameLower + `.` + data.Name + `DeleteResponse{ + Message: "` + data.Name + ` berhasil dihapus", + ID: id, + } + + c.JSON(http.StatusOK, response) +}` +} + +func generateStatsMethod(data HandlerData) string { + return ` + +// Get` + data.Name + `Stats godoc +// @Summary Get ` + data.NameLower + ` statistics +// @Description Returns comprehensive statistics about ` + data.NameLower + ` data +// @Tags ` + data.NameLower + ` +// @Accept json +// @Produce json +// @Param status query string false "Filter statistics by status" +// @Success 200 {object} models.AggregateData "Statistics data" +// @Failure 500 {object} models` + data.NameLower + `.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NamePlural + `/stats [get] +func (h *` + data.Name + `Handler) Get` + data.Name + `Stats(c *gin.Context) { + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + filter := h.parseFilterParams(c) + aggregateData, err := h.getAggregateData(ctx, dbConn, filter) + if err != nil { + h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Statistik ` + data.NameLower + ` berhasil diambil", + "data": aggregateData, + }) +}` +} + +func generateHelperMethods(data HandlerData) string { + helperMethods := ` + +// Database operations +func (h *` + data.Name + `Handler) get` + data.Name + `ByID(ctx context.Context, dbConn *sql.DB, id string) (*models` + data.NameLower + `.` + data.Name + `, error) { + query := "SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM ` + data.TableName + ` WHERE id = $1 AND status != 'deleted'" + row := dbConn.QueryRowContext(ctx, query, id) + + var item models` + data.NameLower + `.` + data.Name + ` + err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) + if err != nil { + return nil, err + } + + return &item, nil +} + +func (h *` + data.Name + `Handler) create` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *models` + data.NameLower + `.` + data.Name + `CreateRequest) (*models` + data.NameLower + `.` + data.Name + `, error) { + id := uuid.New().String() + now := time.Now() + + query := "INSERT INTO ` + data.TableName + ` (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name" + row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name) + + var item models` + data.NameLower + `.` + data.Name + ` + err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) + if err != nil { + return nil, fmt.Errorf("failed to create ` + data.NameLower + `: %w", err) + } + + return &item, nil +} + +func (h *` + data.Name + `Handler) update` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *models` + data.NameLower + `.` + data.Name + `UpdateRequest) (*models` + data.NameLower + `.` + data.Name + `, error) { + now := time.Now() + + query := "UPDATE ` + data.TableName + ` SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name" + row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name) + + var item models` + data.NameLower + `.` + data.Name + ` + err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) + if err != nil { + return nil, fmt.Errorf("failed to update ` + data.NameLower + `: %w", err) + } + + return &item, nil +} + +func (h *` + data.Name + `Handler) delete` + data.Name + `(ctx context.Context, dbConn *sql.DB, id string) error { + now := time.Now() + query := "UPDATE ` + data.TableName + ` SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'" + + result, err := dbConn.ExecContext(ctx, query, id, now) + if err != nil { + return fmt.Errorf("failed to delete ` + data.NameLower + `: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rowsAffected == 0 { + return sql.ErrNoRows + } + + return nil +} + +func (h *` + data.Name + `Handler) fetch` + data.Name + `s(ctx context.Context, dbConn *sql.DB, filter models` + data.NameLower + `.` + data.Name + `Filter, limit, offset int) ([]models` + data.NameLower + `.` + data.Name + `, error) { + whereClause, args := h.buildWhereClause(filter) + query := fmt.Sprintf("SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM ` + data.TableName + ` WHERE %s ORDER BY date_created DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2) + args = append(args, limit, offset) + + rows, err := dbConn.QueryContext(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("fetch ` + data.NamePlural + ` query failed: %w", err) + } + defer rows.Close() + + items := make([]models` + data.NameLower + `.` + data.Name + `, 0, limit) + for rows.Next() { + item, err := h.scan` + data.Name + `(rows) + if err != nil { + return nil, fmt.Errorf("scan ` + data.Name + ` failed: %w", err) + } + items = append(items, item) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + log.Printf("Successfully fetched %d ` + data.NamePlural + ` with filters applied", len(items)) + return items, nil +}` + + // Add dynamic fetch method if needed + if data.HasDynamic { + helperMethods += ` + +// fetchRetribusisDynamic executes dynamic query +func (h *` + data.Name + `Handler) fetch` + data.Name + `sDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]models` + data.NameLower + `.` + data.Name + `, int, error) { + // Setup query builder + builder := utils.NewQueryBuilder("` + data.TableName + `"). + SetAllowedColumns([]string{ + "id", "status", "sort", "user_created", "date_created", + "user_updated", "date_updated", "name", + }) + + // Add default filter to exclude deleted records + query.Filters = append([]utils.FilterGroup{{ + Filters: []utils.DynamicFilter{{ + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }}, + LogicOp: "AND", + }}, query.Filters...) + + // Execute concurrent queries + var ( + items []models` + data.NameLower + `.` + data.Name + ` + total int + wg sync.WaitGroup + errChan = make(chan error, 2) + mu sync.Mutex + ) + + // Fetch total count + wg.Add(1) + go func() { + defer wg.Done() + countQuery := query + countQuery.Limit = 0 + countQuery.Offset = 0 + countSQL, countArgs, err := builder.BuildCountQuery(countQuery) + if err != nil { + errChan <- fmt.Errorf("failed to build count query: %w", err) + return + } + if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil { + errChan <- fmt.Errorf("failed to get total count: %w", err) + return + } + }() + + // Fetch main data + wg.Add(1) + go func() { + defer wg.Done() + mainSQL, mainArgs, err := builder.BuildQuery(query) + if err != nil { + errChan <- fmt.Errorf("failed to build main query: %w", err) + return + } + + rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...) + if err != nil { + errChan <- fmt.Errorf("failed to execute main query: %w", err) + return + } + defer rows.Close() + + var results []models` + data.NameLower + `.` + data.Name + ` + for rows.Next() { + item, err := h.scan` + data.Name + `(rows) + if err != nil { + errChan <- fmt.Errorf("failed to scan ` + data.NameLower + `: %w", err) + return + } + results = append(results, item) + } + + if err := rows.Err(); err != nil { + errChan <- fmt.Errorf("rows iteration error: %w", err) + return + } + + mu.Lock() + items = results + mu.Unlock() + }() + + // Wait for all goroutines + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + return nil, 0, err + } + } + + return items, total, nil +} + +// Optimized scanning function yang menggunakan sql.Null* types langsung +func (h *` + data.Name + `Handler) scan` + data.Name + `(rows *sql.Rows) (models` + data.NameLower + `.` + data.Name + `, error) { + var item models` + data.NameLower + `.` + data.Name + ` + return item, rows.Scan( + &item.ID, &item.Status, &item.Sort, &item.UserCreated, + &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name, + ) +}` + } + + helperMethods += ` + +func (h *` + data.Name + `Handler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter models` + data.NameLower + `.` + data.Name + `Filter, total *int) error { + whereClause, args := h.buildWhereClause(filter) + countQuery := fmt.Sprintf("SELECT COUNT(*) FROM ` + data.TableName + ` WHERE %s", whereClause) + if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil { + return fmt.Errorf("total count query failed: %w", err) + } + return nil +} + +// Get comprehensive aggregate data dengan filter support +func (h *` + data.Name + `Handler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter models` + data.NameLower + `.` + data.Name + `Filter) (*models.AggregateData, error) { + aggregate := &models.AggregateData{ + ByStatus: make(map[string]int), + } + + // Build where clause untuk filter + whereClause, args := h.buildWhereClause(filter) + + // Use concurrent execution untuk performance + var wg sync.WaitGroup + var mu sync.Mutex + errChan := make(chan error, 4) + + // 1. Count by status + wg.Add(1) + go func() { + defer wg.Done() + statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM ` + data.TableName + ` WHERE %s GROUP BY status ORDER BY status", whereClause) + + rows, err := dbConn.QueryContext(ctx, statusQuery, args...) + if err != nil { + errChan <- fmt.Errorf("status query failed: %w", err) + return + } + defer rows.Close() + + mu.Lock() + for rows.Next() { + var status string + var count int + if err := rows.Scan(&status, &count); err != nil { + mu.Unlock() + errChan <- fmt.Errorf("status scan failed: %w", err) + return + } + aggregate.ByStatus[status] = count + switch status { + case "active": + aggregate.TotalActive = count + case "draft": + aggregate.TotalDraft = count + case "inactive": + aggregate.TotalInactive = count + } + } + mu.Unlock() + + if err := rows.Err(); err != nil { + errChan <- fmt.Errorf("status iteration error: %w", err) + } + }() + + // 2. Get last updated time dan today statistics + wg.Add(1) + go func() { + defer wg.Done() + + // Last updated + lastUpdatedQuery := fmt.Sprintf("SELECT MAX(date_updated) FROM ` + data.TableName + ` WHERE %s AND date_updated IS NOT NULL", whereClause) + var lastUpdated sql.NullTime + if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil { + errChan <- fmt.Errorf("last updated query failed: %w", err) + return + } + + // Today statistics + today := time.Now().Format("2006-01-02") + todayStatsQuery := fmt.Sprintf(` + "`" + ` + SELECT + SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today, + SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today + FROM ` + data.TableName + ` + WHERE %s` + "`" + `, len(args)+1, len(args)+1, len(args)+1, whereClause) + + todayArgs := append(args, today) + var createdToday, updatedToday int + if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil { + errChan <- fmt.Errorf("today stats query failed: %w", err) + return + } + + mu.Lock() + if lastUpdated.Valid { + aggregate.LastUpdated = &lastUpdated.Time + } + aggregate.CreatedToday = createdToday + aggregate.UpdatedToday = updatedToday + mu.Unlock() + }() + + // Wait for all goroutines + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + return nil, err + } + } + + return aggregate, nil +} + +// Enhanced error handling +func (h *` + data.Name + `Handler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { + log.Printf("[ERROR] %s: %v", message, err) + h.respondError(c, message, err, statusCode) +} + +func (h *` + data.Name + `Handler) respondError(c *gin.Context, message string, err error, statusCode int) { + errorMessage := message + if gin.Mode() == gin.ReleaseMode { + errorMessage = "Internal server error" + } + + c.JSON(statusCode, models.ErrorResponse{ + Error: errorMessage, + Code: statusCode, + Message: err.Error(), + Timestamp: time.Now(), + }) +} + +// Parse pagination parameters dengan validation yang lebih ketat +func (h *` + data.Name + `Handler) parsePaginationParams(c *gin.Context) (int, int, error) { + limit := 10 // Default limit + offset := 0 // Default offset + + if limitStr := c.Query("limit"); limitStr != "" { + parsedLimit, err := strconv.Atoi(limitStr) + if err != nil { + return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr) + } + if parsedLimit <= 0 { + return 0, 0, fmt.Errorf("limit must be greater than 0") + } + if parsedLimit > 100 { + return 0, 0, fmt.Errorf("limit cannot exceed 100") + } + limit = parsedLimit + } + + if offsetStr := c.Query("offset"); offsetStr != "" { + parsedOffset, err := strconv.Atoi(offsetStr) + if err != nil { + return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr) + } + if parsedOffset < 0 { + return 0, 0, fmt.Errorf("offset cannot be negative") + } + offset = parsedOffset + } + + log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset) + return limit, offset, nil +} + +func (h *` + data.Name + `Handler) parseFilterParams(c *gin.Context) models` + data.NameLower + `.` + data.Name + `Filter { + filter := models` + data.NameLower + `.` + data.Name + `Filter{} + + if status := c.Query("status"); status != "" { + if models.IsValidStatus(status) { + filter.Status = &status + } + } + + if search := c.Query("search"); search != "" { + filter.Search = &search + } + + // Parse date filters + if dateFromStr := c.Query("date_from"); dateFromStr != "" { + if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil { + filter.DateFrom = &dateFrom + } + } + + if dateToStr := c.Query("date_to"); dateToStr != "" { + if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil { + filter.DateTo = &dateTo + } + } + + return filter +} + +// Build WHERE clause dengan filter parameters +func (h *` + data.Name + `Handler) buildWhereClause(filter models` + data.NameLower + `.` + data.Name + `Filter) (string, []interface{}) { + conditions := []string{"status != 'deleted'"} + args := []interface{}{} + paramCount := 1 + + if filter.Status != nil { + conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount)) + args = append(args, *filter.Status) + paramCount++ + } + + if filter.Search != nil { + searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount) + conditions = append(conditions, searchCondition) + searchTerm := "%" + *filter.Search + "%" + args = append(args, searchTerm) + paramCount++ + } + + if filter.DateFrom != nil { + conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount)) + args = append(args, *filter.DateFrom) + paramCount++ + } + + if filter.DateTo != nil { + conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount)) + args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond)) + paramCount++ + } + + return strings.Join(conditions, " AND "), args +} + +func (h *` + data.Name + `Handler) calculateMeta(limit, offset, total int) models.MetaResponse { + totalPages := 0 + currentPage := 1 + if limit > 0 { + totalPages = (total + limit - 1) / limit // Ceiling division + currentPage = (offset / limit) + 1 + } + + return models.MetaResponse{ + Limit: limit, + Offset: offset, + Total: total, + TotalPages: totalPages, + CurrentPage: currentPage, + HasNext: offset+limit < total, + HasPrev: offset > 0, + } +} + +// validate` + data.Name + `Submission performs validation for duplicate entries and daily submission limits +func (h *` + data.Name + `Handler) validate` + data.Name + `Submission(ctx context.Context, dbConn *sql.DB, req *models` + data.NameLower + `.` + data.Name + `CreateRequest) error { + // Import the validation utility + validator := validation.NewDuplicateValidator(dbConn) + + // Use default configuration + config := validation.ValidationConfig{ + TableName: "` + data.TableName + `", + IDColumn: "id", + StatusColumn: "status", + DateColumn: "date_created", + ActiveStatuses: []string{"active", "draft"}, + } + + // Validate duplicate entries with active status for today + err := validator.ValidateDuplicate(ctx, config, "dummy_id") + if err != nil { + return fmt.Errorf("validation failed: %w", err) + } + + // Validate once per day submission + err = validator.ValidateOncePerDay(ctx, "` + data.TableName + `", "id", "date_created", "daily_limit") + if err != nil { + return fmt.Errorf("daily submission limit exceeded: %w", err) + } + + return nil +} + +// Example usage of the validation utility with custom configuration +func (h *` + data.Name + `Handler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *models` + data.NameLower + `.` + data.Name + `CreateRequest) error { + // Create validator instance + validator := validation.NewDuplicateValidator(dbConn) + + // Use custom configuration + config := validation.ValidationConfig{ + TableName: "` + data.TableName + `", + IDColumn: "id", + StatusColumn: "status", + DateColumn: "date_created", + ActiveStatuses: []string{"active", "draft"}, + AdditionalFields: map[string]interface{}{ + "name": req.Name, + }, + } + + // Validate with custom fields + fields := map[string]interface{}{ + "name": *req.Name, + } + + err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields) + if err != nil { + return fmt.Errorf("custom validation failed: %w", err) + } + + return nil +} + +// GetLastSubmissionTime example +func (h *` + data.Name + `Handler) getLastSubmissionTimeExample(ctx context.Context, dbConn *sql.DB, identifier string) (*time.Time, error) { + validator := validation.NewDuplicateValidator(dbConn) + return validator.GetLastSubmissionTime(ctx, "` + data.TableName + `", "id", "date_created", identifier) +}` + + return helperMethods +} + +// Keep existing functions for model generation and routes... +// (The remaining functions stay the same as in the original file) + +// ================= MODEL GENERATION ===================== +func generateModelFile(data HandlerData, modelDir string) { + modelContent := `package models + +import ( + "` + data.ModuleName + `/internal/models" + "database/sql" + "encoding/json" + "time" +) + +// ` + data.Name + ` represents the data structure for the ` + data.NameLower + ` table +// with proper null handling and optimized JSON marshaling +type ` + data.Name + ` struct { + ID string ` + "`json:\"id\" db:\"id\"`" + ` + Status string ` + "`json:\"status\" db:\"status\"`" + ` + Sort models.NullableInt32 ` + "`json:\"sort,omitempty\" db:\"sort\"`" + ` + UserCreated sql.NullString ` + "`json:\"user_created,omitempty\" db:\"user_created\"`" + ` + DateCreated sql.NullTime ` + "`json:\"date_created,omitempty\" db:\"date_created\"`" + ` + UserUpdated sql.NullString ` + "`json:\"user_updated,omitempty\" db:\"user_updated\"`" + ` + DateUpdated sql.NullTime ` + "`json:\"date_updated,omitempty\" db:\"date_updated\"`" + ` + Name sql.NullString ` + "`json:\"name,omitempty\" db:\"name\"`" + ` +} + +// Custom JSON marshaling untuk ` + data.Name + ` agar NULL values tidak muncul di response +func (r ` + data.Name + `) MarshalJSON() ([]byte, error) { + type Alias ` + data.Name + ` + aux := &struct { + Sort *int ` + "`json:\"sort,omitempty\"`" + ` + UserCreated *string ` + "`json:\"user_created,omitempty\"`" + ` + DateCreated *time.Time ` + "`json:\"date_created,omitempty\"`" + ` + UserUpdated *string ` + "`json:\"user_updated,omitempty\"`" + ` + DateUpdated *time.Time ` + "`json:\"date_updated,omitempty\"`" + ` + Name *string ` + "`json:\"name,omitempty\"`" + ` + *Alias + }{ + Alias: (*Alias)(&r), + } + + // Convert NullableInt32 to pointer + if r.Sort.Valid { + sort := int(r.Sort.Int32) + aux.Sort = &sort + } + + if r.UserCreated.Valid { + aux.UserCreated = &r.UserCreated.String + } + + if r.DateCreated.Valid { + aux.DateCreated = &r.DateCreated.Time + } + + if r.UserUpdated.Valid { + aux.UserUpdated = &r.UserUpdated.String + } + + if r.DateUpdated.Valid { + aux.DateUpdated = &r.DateUpdated.Time + } + + if r.Name.Valid { + aux.Name = &r.Name.String + } + + return json.Marshal(aux) +} + +// Helper methods untuk mendapatkan nilai yang aman +func (r *` + data.Name + `) GetName() string { + if r.Name.Valid { + return r.Name.String + } + return "" +}` + + // Add request/response structs based on enabled methods + if data.HasGet { + modelContent += ` + +// Response struct untuk GET by ID - diperbaiki struktur +type ` + data.Name + `GetByIDResponse struct { + Message string ` + "`json:\"message\"`" + ` + Data *` + data.Name + ` ` + "`json:\"data\"`" + ` +} + +// Enhanced GET response dengan pagination dan aggregation +type ` + data.Name + `GetResponse struct { + Message string ` + "`json:\"message\"`" + ` + Data []` + data.Name + ` ` + "`json:\"data\"`" + ` + Meta models.MetaResponse ` + "`json:\"meta\"`" + ` + Summary *models.AggregateData ` + "`json:\"summary,omitempty\"`" + ` +}` + } + + if data.HasPost { + modelContent += ` + +// Request struct untuk create - dioptimalkan dengan validasi +type ` + data.Name + `CreateRequest struct { + Status string ` + "`json:\"status\" validate:\"required,oneof=draft active inactive\"`" + ` + Name *string ` + "`json:\"name,omitempty\" validate:\"omitempty,min=1,max=255\"`" + ` +} + +// Response struct untuk create +type ` + data.Name + `CreateResponse struct { + Message string ` + "`json:\"message\"`" + ` + Data *` + data.Name + ` ` + "`json:\"data\"`" + ` +}` + } + + if data.HasPut { + modelContent += ` + +// Update request - sama seperti create tapi dengan ID +type ` + data.Name + `UpdateRequest struct { + ID string ` + "`json:\"-\" validate:\"required,uuid4\"`" + ` // ID dari URL path + Status string ` + "`json:\"status\" validate:\"required,oneof=draft active inactive\"`" + ` + Name *string ` + "`json:\"name,omitempty\" validate:\"omitempty,min=1,max=255\"`" + ` +} + +// Response struct untuk update +type ` + data.Name + `UpdateResponse struct { + Message string ` + "`json:\"message\"`" + ` + Data *` + data.Name + ` ` + "`json:\"data\"`" + ` +}` + } + + if data.HasDelete { + modelContent += ` + +// Response struct untuk delete +type ` + data.Name + `DeleteResponse struct { + Message string ` + "`json:\"message\"`" + ` + ID string ` + "`json:\"id\"`" + ` +}` + } + + // Add filter struct + modelContent += ` + +// Filter struct untuk query parameters +type ` + data.Name + `Filter struct { + Status *string ` + "`json:\"status,omitempty\" form:\"status\"`" + ` + Search *string ` + "`json:\"search,omitempty\" form:\"search\"`" + ` + DateFrom *time.Time ` + "`json:\"date_from,omitempty\" form:\"date_from\"`" + ` + DateTo *time.Time ` + "`json:\"date_to,omitempty\" form:\"date_to\"`" + ` +}` + + writeFile(filepath.Join(modelDir, data.NameLower+".go"), modelContent) +} + +// ================= ROUTES GENERATION ===================== +func updateRoutesFile(data HandlerData) { + routesFile := "internal/routes/v1/routes.go" + content, err := os.ReadFile(routesFile) + if err != nil { + fmt.Printf("⚠️ Could not read routes.go: %v\n", err) + fmt.Printf("📝 Please manually add these routes to your routes.go file:\n") + printRoutesSample(data) + return + } + + routesContent := string(content) + + // Build import path + var importPath, importAlias string + if data.Category != "" { + importPath = fmt.Sprintf("%s/internal/handlers", data.ModuleName) + importAlias = data.NameLower + "Handlers" + } else { + importPath = fmt.Sprintf("%s/internal/handlers", data.ModuleName) + importAlias = data.NameLower + "Handlers" + } + + // Add import + importPattern := fmt.Sprintf("%s \"%s\"", importAlias, importPath) + if !strings.Contains(routesContent, importPattern) { + importToAdd := fmt.Sprintf("\t%s \"%s\"", importAlias, importPath) + if strings.Contains(routesContent, "import (") { + routesContent = strings.Replace(routesContent, "import (", + "import (\n"+importToAdd, 1) + } + } + + // Build new routes in protected group format + newRoutes := generateProtectedRouteBlock(data) + + // Insert above protected routes marker + insertMarker := "// ============= PUBLISHED ROUTES ===============================================" + if strings.Contains(routesContent, insertMarker) { + if !strings.Contains(routesContent, fmt.Sprintf("New%sHandler", data.Name)) { + // Insert before the marker + routesContent = strings.Replace(routesContent, insertMarker, + newRoutes+"\n\t"+insertMarker, 1) + } else { + fmt.Printf("✅ Routes for %s already exist, skipping...\n", data.Name) + return + } + } else { + // Fallback: insert at end of setupV1Routes function + setupFuncEnd := "\treturn r" + if strings.Contains(routesContent, setupFuncEnd) { + routesContent = strings.Replace(routesContent, setupFuncEnd, + newRoutes+"\n\n\t"+setupFuncEnd, 1) + } + } + + if err := os.WriteFile(routesFile, []byte(routesContent), 0644); err != nil { + fmt.Printf("Error writing routes.go: %v\n", err) + return + } + + fmt.Printf("✅ Updated routes.go with %s endpoints\n", data.Name) +} + +func generateProtectedRouteBlock(data HandlerData) string { + routes := fmt.Sprintf(` + // %s endpoints + %sHandler := %sHandlers.New%sHandler() + %sGroup := v1.Group("/%s") + { + %sGroup.GET("", %sHandler.Get%s)`, + strings.Title(data.NamePlural), data.NameLower, data.NameLower, data.Name, + data.NameLower, data.NameLower, + data.NameLower, data.NameLower, data.Name) + + if data.HasDynamic { + routes += fmt.Sprintf(` + %sGroup.GET("/dynamic", %sHandler.Get%sDynamic) // Route baru`, + data.NameLower, data.NameLower, data.Name) + } + + if data.HasSearch { + routes += fmt.Sprintf(` + %sGroup.GET("/search", %sHandler.Search%sAdvanced) // Route pencarian`, + data.NameLower, data.NameLower, data.Name) + } + + routes += fmt.Sprintf(` + %sGroup.GET("/:id", %sHandler.Get%sByID)`, + data.NameLower, data.NameLower, data.Name) + + if data.HasPost { + routes += fmt.Sprintf(` + %sGroup.POST("", %sHandler.Create%s)`, + data.NameLower, data.NameLower, data.Name) + } + + if data.HasPut { + routes += fmt.Sprintf(` + %sGroup.PUT("/:id", %sHandler.Update%s)`, + data.NameLower, data.NameLower, data.Name) + } + + if data.HasDelete { + routes += fmt.Sprintf(` + %sGroup.DELETE("/:id", %sHandler.Delete%s)`, + data.NameLower, data.NameLower, data.Name) + } + + if data.HasStats { + routes += fmt.Sprintf(` + %sGroup.GET("/stats", %sHandler.Get%sStats)`, + data.NameLower, data.NameLower, data.Name) + } + + routes += ` + }` + + return routes +} + +func printRoutesSample(data HandlerData) { + fmt.Print(generateProtectedRouteBlock(data)) + fmt.Println() +} + +// ================= UTILITY FUNCTIONS ===================== +func writeFile(filename, content string) { + if err := os.WriteFile(filename, []byte(content), 0644); err != nil { + fmt.Printf("❌ Error creating file %s: %v\n", filename, err) + return + } + fmt.Printf("✅ Generated: %s\n", filename) +} diff --git a/tools/generate-handler.go b/tools/generate-handler.go.backup similarity index 100% rename from tools/generate-handler.go rename to tools/generate-handler.go.backup diff --git a/tools/generate.bat b/tools/generate.bat index eb7d0928..ed988936 100644 --- a/tools/generate.bat +++ b/tools/generate.bat @@ -20,7 +20,7 @@ echo Generating handler: %HANDLER_NAME% with methods: %METHODS% echo. cd /d "%~dp0.." -go run tools/generate-handler.go %HANDLER_NAME% %METHODS% +go run tools/general/generate-handler.go %HANDLER_NAME% %METHODS% echo. echo Handler generated successfully! diff --git a/tools/generate.sh b/tools/generate.sh index c0abfd13..5b926b0b 100644 --- a/tools/generate.sh +++ b/tools/generate.sh @@ -26,7 +26,7 @@ echo cd "$(dirname "$0")/.." # Run the generator -go run tools/generate-handler.go "$HANDLER_NAME" $METHODS +go run tools/general/generate-handler.go "$HANDLER_NAME" $METHODS echo echo "Handler generated successfully!"