perbaikan generete tool
This commit is contained in:
526
docs/docs.go
526
docs/docs.go
@@ -24,6 +24,56 @@ const docTemplate = `{
|
|||||||
"host": "{{.Host}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "{{.BasePath}}",
|
"basePath": "{{.BasePath}}",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/api/v1/Order/{id}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns a single order by ID",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Order"
|
||||||
|
],
|
||||||
|
"summary": "Get Order by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Order ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success response",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderGetByIDResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid ID format",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "order not found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/auth/login": {
|
"/api/v1/auth/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Authenticate user with username and password to receive JWT token",
|
"description": "Authenticate user with username and password to receive JWT token",
|
||||||
@@ -214,6 +264,264 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/order/{id}": {
|
||||||
|
"put": {
|
||||||
|
"description": "Updates an existing order record",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"order"
|
||||||
|
],
|
||||||
|
"summary": "Update order",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Order ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Order update request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderUpdateRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Order updated successfully",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderUpdateResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request or validation error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Order not found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"description": "Soft deletes a order by setting status to 'deleted'",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"order"
|
||||||
|
],
|
||||||
|
"summary": "Delete order",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Order ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Order deleted successfully",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderDeleteResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid ID format",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Order not found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/orders": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns a paginated list of orders with optional summary statistics",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"order"
|
||||||
|
],
|
||||||
|
"summary": "Get order with pagination and optional aggregation",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"default": 10,
|
||||||
|
"description": "Limit (max 100)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0,
|
||||||
|
"description": "Offset",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Include aggregation summary",
|
||||||
|
"name": "include_summary",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by status",
|
||||||
|
"name": "status",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search in multiple fields",
|
||||||
|
"name": "search",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success response",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderGetResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "Creates a new order record",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"order"
|
||||||
|
],
|
||||||
|
"summary": "Create order",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Order creation request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderCreateRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Order created successfully",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderCreateResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request or validation error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/orders/stats": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns comprehensive statistics about order data",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"order"
|
||||||
|
],
|
||||||
|
"summary": "Get order statistics",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter statistics by status",
|
||||||
|
"name": "status",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Statistics data",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.AggregateData"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/retribusi/{id}": {
|
"/api/v1/retribusi/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a single retribusi by ID",
|
"description": "Returns a single retribusi by ID",
|
||||||
@@ -678,6 +986,224 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"api-service_internal_models_order.AggregateData": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"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_order.ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.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_order.NullableInt32": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"int32": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.Order": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"date_created": {
|
||||||
|
"$ref": "#/definitions/sql.NullTime"
|
||||||
|
},
|
||||||
|
"date_updated": {
|
||||||
|
"$ref": "#/definitions/sql.NullTime"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"$ref": "#/definitions/sql.NullString"
|
||||||
|
},
|
||||||
|
"sort": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.NullableInt32"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_created": {
|
||||||
|
"$ref": "#/definitions/sql.NullString"
|
||||||
|
},
|
||||||
|
"user_updated": {
|
||||||
|
"$ref": "#/definitions/sql.NullString"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderCreateRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"draft",
|
||||||
|
"active",
|
||||||
|
"inactive"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderCreateResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderDeleteResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderGetByIDResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderGetResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.MetaResponse"
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.AggregateData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderUpdateRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"draft",
|
||||||
|
"active",
|
||||||
|
"inactive"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderUpdateResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"api-service_internal_models_retribusi.AggregateData": {
|
"api-service_internal_models_retribusi.AggregateData": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -22,6 +22,56 @@
|
|||||||
"host": "localhost:8080",
|
"host": "localhost:8080",
|
||||||
"basePath": "/api/v1",
|
"basePath": "/api/v1",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/api/v1/Order/{id}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns a single order by ID",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Order"
|
||||||
|
],
|
||||||
|
"summary": "Get Order by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Order ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success response",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderGetByIDResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid ID format",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "order not found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/auth/login": {
|
"/api/v1/auth/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Authenticate user with username and password to receive JWT token",
|
"description": "Authenticate user with username and password to receive JWT token",
|
||||||
@@ -212,6 +262,264 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/order/{id}": {
|
||||||
|
"put": {
|
||||||
|
"description": "Updates an existing order record",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"order"
|
||||||
|
],
|
||||||
|
"summary": "Update order",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Order ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Order update request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderUpdateRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Order updated successfully",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderUpdateResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request or validation error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Order not found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"description": "Soft deletes a order by setting status to 'deleted'",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"order"
|
||||||
|
],
|
||||||
|
"summary": "Delete order",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Order ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Order deleted successfully",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderDeleteResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid ID format",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Order not found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/orders": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns a paginated list of orders with optional summary statistics",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"order"
|
||||||
|
],
|
||||||
|
"summary": "Get order with pagination and optional aggregation",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"default": 10,
|
||||||
|
"description": "Limit (max 100)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0,
|
||||||
|
"description": "Offset",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Include aggregation summary",
|
||||||
|
"name": "include_summary",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by status",
|
||||||
|
"name": "status",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search in multiple fields",
|
||||||
|
"name": "search",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success response",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderGetResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "Creates a new order record",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"order"
|
||||||
|
],
|
||||||
|
"summary": "Create order",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Order creation request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderCreateRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Order created successfully",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.OrderCreateResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request or validation error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/orders/stats": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns comprehensive statistics about order data",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"order"
|
||||||
|
],
|
||||||
|
"summary": "Get order statistics",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter statistics by status",
|
||||||
|
"name": "status",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Statistics data",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.AggregateData"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/retribusi/{id}": {
|
"/api/v1/retribusi/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a single retribusi by ID",
|
"description": "Returns a single retribusi by ID",
|
||||||
@@ -676,6 +984,224 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"api-service_internal_models_order.AggregateData": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"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_order.ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.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_order.NullableInt32": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"int32": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.Order": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"date_created": {
|
||||||
|
"$ref": "#/definitions/sql.NullTime"
|
||||||
|
},
|
||||||
|
"date_updated": {
|
||||||
|
"$ref": "#/definitions/sql.NullTime"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"$ref": "#/definitions/sql.NullString"
|
||||||
|
},
|
||||||
|
"sort": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.NullableInt32"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_created": {
|
||||||
|
"$ref": "#/definitions/sql.NullString"
|
||||||
|
},
|
||||||
|
"user_updated": {
|
||||||
|
"$ref": "#/definitions/sql.NullString"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderCreateRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"draft",
|
||||||
|
"active",
|
||||||
|
"inactive"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderCreateResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderDeleteResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderGetByIDResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderGetResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.MetaResponse"
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.AggregateData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderUpdateRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"draft",
|
||||||
|
"active",
|
||||||
|
"inactive"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-service_internal_models_order.OrderUpdateResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"api-service_internal_models_retribusi.AggregateData": {
|
"api-service_internal_models_retribusi.AggregateData": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -30,6 +30,150 @@ definitions:
|
|||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
api-service_internal_models_order.AggregateData:
|
||||||
|
properties:
|
||||||
|
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_order.ErrorResponse:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: integer
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
timestamp:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
api-service_internal_models_order.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_order.NullableInt32:
|
||||||
|
properties:
|
||||||
|
int32:
|
||||||
|
type: integer
|
||||||
|
valid:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
api-service_internal_models_order.Order:
|
||||||
|
properties:
|
||||||
|
date_created:
|
||||||
|
$ref: '#/definitions/sql.NullTime'
|
||||||
|
date_updated:
|
||||||
|
$ref: '#/definitions/sql.NullTime'
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
$ref: '#/definitions/sql.NullString'
|
||||||
|
sort:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.NullableInt32'
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
user_created:
|
||||||
|
$ref: '#/definitions/sql.NullString'
|
||||||
|
user_updated:
|
||||||
|
$ref: '#/definitions/sql.NullString'
|
||||||
|
type: object
|
||||||
|
api-service_internal_models_order.OrderCreateRequest:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
enum:
|
||||||
|
- draft
|
||||||
|
- active
|
||||||
|
- inactive
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
type: object
|
||||||
|
api-service_internal_models_order.OrderCreateResponse:
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.Order'
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
api-service_internal_models_order.OrderDeleteResponse:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
api-service_internal_models_order.OrderGetByIDResponse:
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.Order'
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
api-service_internal_models_order.OrderGetResponse:
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.Order'
|
||||||
|
type: array
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
meta:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.MetaResponse'
|
||||||
|
summary:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.AggregateData'
|
||||||
|
type: object
|
||||||
|
api-service_internal_models_order.OrderUpdateRequest:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
enum:
|
||||||
|
- draft
|
||||||
|
- active
|
||||||
|
- inactive
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
type: object
|
||||||
|
api-service_internal_models_order.OrderUpdateResponse:
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.Order'
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
api-service_internal_models_retribusi.AggregateData:
|
api-service_internal_models_retribusi.AggregateData:
|
||||||
properties:
|
properties:
|
||||||
by_dinas:
|
by_dinas:
|
||||||
@@ -322,6 +466,39 @@ info:
|
|||||||
title: API Service
|
title: API Service
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
paths:
|
paths:
|
||||||
|
/api/v1/Order/{id}:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns a single order by ID
|
||||||
|
parameters:
|
||||||
|
- description: Order ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Success response
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.OrderGetByIDResponse'
|
||||||
|
"400":
|
||||||
|
description: Invalid ID format
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
"404":
|
||||||
|
description: order not found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
summary: Get Order by ID
|
||||||
|
tags:
|
||||||
|
- Order
|
||||||
/api/v1/auth/login:
|
/api/v1/auth/login:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -445,6 +622,177 @@ paths:
|
|||||||
summary: Register new user
|
summary: Register new user
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
|
/api/v1/order/{id}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Soft deletes a order by setting status to 'deleted'
|
||||||
|
parameters:
|
||||||
|
- description: Order ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Order deleted successfully
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.OrderDeleteResponse'
|
||||||
|
"400":
|
||||||
|
description: Invalid ID format
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
"404":
|
||||||
|
description: Order not found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
summary: Delete order
|
||||||
|
tags:
|
||||||
|
- order
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Updates an existing order record
|
||||||
|
parameters:
|
||||||
|
- description: Order ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Order update request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.OrderUpdateRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Order updated successfully
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.OrderUpdateResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad request or validation error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
"404":
|
||||||
|
description: Order not found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
summary: Update order
|
||||||
|
tags:
|
||||||
|
- order
|
||||||
|
/api/v1/orders:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns a paginated list of orders with optional summary statistics
|
||||||
|
parameters:
|
||||||
|
- default: 10
|
||||||
|
description: Limit (max 100)
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
- default: 0
|
||||||
|
description: Offset
|
||||||
|
in: query
|
||||||
|
name: offset
|
||||||
|
type: integer
|
||||||
|
- default: false
|
||||||
|
description: Include aggregation summary
|
||||||
|
in: query
|
||||||
|
name: include_summary
|
||||||
|
type: boolean
|
||||||
|
- description: Filter by status
|
||||||
|
in: query
|
||||||
|
name: status
|
||||||
|
type: string
|
||||||
|
- description: Search in multiple fields
|
||||||
|
in: query
|
||||||
|
name: search
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Success response
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.OrderGetResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
summary: Get order with pagination and optional aggregation
|
||||||
|
tags:
|
||||||
|
- order
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Creates a new order record
|
||||||
|
parameters:
|
||||||
|
- description: Order creation request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.OrderCreateRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Order created successfully
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.OrderCreateResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad request or validation error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
summary: Create order
|
||||||
|
tags:
|
||||||
|
- order
|
||||||
|
/api/v1/orders/stats:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns comprehensive statistics about order data
|
||||||
|
parameters:
|
||||||
|
- description: Filter statistics by status
|
||||||
|
in: query
|
||||||
|
name: status
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Statistics data
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.AggregateData'
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||||
|
summary: Get order statistics
|
||||||
|
tags:
|
||||||
|
- order
|
||||||
/api/v1/retribusi/{id}:
|
/api/v1/retribusi/{id}:
|
||||||
delete:
|
delete:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
683
internal/handlers/order/order.go
Normal file
683
internal/handlers/order/order.go
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"api-service/internal/config"
|
||||||
|
"api-service/internal/database"
|
||||||
|
models "api-service/internal/models/order"
|
||||||
|
"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("order_status", validateOrderStatus)
|
||||||
|
if db == nil {
|
||||||
|
log.Fatal("Failed to initialize database connection")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom validation for order status
|
||||||
|
func validateOrderStatus(fl validator.FieldLevel) bool {
|
||||||
|
return models.IsValidStatus(fl.Field().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrderHandler handles order services
|
||||||
|
type OrderHandler struct {
|
||||||
|
db database.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOrderHandler creates a new OrderHandler
|
||||||
|
func NewOrderHandler() *OrderHandler {
|
||||||
|
return &OrderHandler{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrder godoc
|
||||||
|
// @Summary Get order with pagination and optional aggregation
|
||||||
|
// @Description Returns a paginated list of orders with optional summary statistics
|
||||||
|
// @Tags order
|
||||||
|
// @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.OrderGetResponse "Success response"
|
||||||
|
// @Failure 400 {object} models.ErrorResponse "Bad request"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/orders [get]
|
||||||
|
func (h *OrderHandler) GetOrder(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("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.Order
|
||||||
|
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 - FIXED: Proper method name
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
result, err := h.fetchOrders(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.OrderGetResponse{
|
||||||
|
Message: "Data order berhasil diambil",
|
||||||
|
Data: items,
|
||||||
|
Meta: meta,
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeAggregation && aggregateData != nil {
|
||||||
|
response.Summary = aggregateData
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrderByID godoc
|
||||||
|
// @Summary Get Order by ID
|
||||||
|
// @Description Returns a single order by ID
|
||||||
|
// @Tags order
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Order ID (UUID)"
|
||||||
|
// @Success 200 {object} models.OrderGetByIDResponse "Success response"
|
||||||
|
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
|
||||||
|
// @Failure 404 {object} models.ErrorResponse "Order not found"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/order/{id} [get]
|
||||||
|
func (h *OrderHandler) GetOrderByID(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("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.getOrderByID(ctx, dbConn, id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
h.respondError(c, "Order not found", err, http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
h.logAndRespondError(c, "Failed to get order", err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := models.OrderGetByIDResponse{
|
||||||
|
Message: "Order details retrieved successfully",
|
||||||
|
Data: item,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrder godoc
|
||||||
|
// @Summary Create order
|
||||||
|
// @Description Creates a new order record
|
||||||
|
// @Tags order
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body models.OrderCreateRequest true "Order creation request"
|
||||||
|
// @Success 201 {object} models.OrderCreateResponse "Order created successfully"
|
||||||
|
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/orders [post]
|
||||||
|
func (h *OrderHandler) CreateOrder(c *gin.Context) {
|
||||||
|
var req models.OrderCreateRequest
|
||||||
|
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("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.createOrder(ctx, dbConn, &req)
|
||||||
|
if err != nil {
|
||||||
|
h.logAndRespondError(c, "Failed to create order", err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := models.OrderCreateResponse{
|
||||||
|
Message: "Order berhasil dibuat",
|
||||||
|
Data: item,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateOrder godoc
|
||||||
|
// @Summary Update order
|
||||||
|
// @Description Updates an existing order record
|
||||||
|
// @Tags order
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Order ID (UUID)"
|
||||||
|
// @Param request body models.OrderUpdateRequest true "Order update request"
|
||||||
|
// @Success 200 {object} models.OrderUpdateResponse "Order updated successfully"
|
||||||
|
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
|
||||||
|
// @Failure 404 {object} models.ErrorResponse "Order not found"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/order/{id} [put]
|
||||||
|
func (h *OrderHandler) UpdateOrder(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.OrderUpdateRequest
|
||||||
|
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("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.updateOrder(ctx, dbConn, &req)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
h.respondError(c, "Order not found", err, http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
h.logAndRespondError(c, "Failed to update order", err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := models.OrderUpdateResponse{
|
||||||
|
Message: "Order berhasil diperbarui",
|
||||||
|
Data: item,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOrder godoc
|
||||||
|
// @Summary Delete order
|
||||||
|
// @Description Soft deletes a order by setting status to 'deleted'
|
||||||
|
// @Tags order
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Order ID (UUID)"
|
||||||
|
// @Success 200 {object} models.OrderDeleteResponse "Order deleted successfully"
|
||||||
|
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
|
||||||
|
// @Failure 404 {object} models.ErrorResponse "Order not found"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/order/{id} [delete]
|
||||||
|
func (h *OrderHandler) DeleteOrder(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("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.deleteOrder(ctx, dbConn, id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
h.respondError(c, "Order not found", err, http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
h.logAndRespondError(c, "Failed to delete order", err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := models.OrderDeleteResponse{
|
||||||
|
Message: "Order berhasil dihapus",
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrderStats godoc
|
||||||
|
// @Summary Get order statistics
|
||||||
|
// @Description Returns comprehensive statistics about order data
|
||||||
|
// @Tags order
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param status query string false "Filter statistics by status"
|
||||||
|
// @Success 200 {object} models.AggregateData "Statistics data"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/orders/stats [get]
|
||||||
|
func (h *OrderHandler) GetOrderStats(c *gin.Context) {
|
||||||
|
dbConn, err := h.db.GetDB("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 order berhasil diambil",
|
||||||
|
"data": aggregateData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database operations
|
||||||
|
func (h *OrderHandler) getOrderByID(ctx context.Context, dbConn *sql.DB, id string) (*models.Order, error) {
|
||||||
|
query := "SELECT id, status, date_created, date_updated, name FROM data_order WHERE id = $1 AND status != 'deleted'"
|
||||||
|
row := dbConn.QueryRowContext(ctx, query, id)
|
||||||
|
|
||||||
|
var item models.Order
|
||||||
|
err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) createOrder(ctx context.Context, dbConn *sql.DB, req *models.OrderCreateRequest) (*models.Order, error) {
|
||||||
|
id := uuid.New().String()
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
query := "INSERT INTO data_order (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, date_created, date_updated, name"
|
||||||
|
row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name)
|
||||||
|
|
||||||
|
var item models.Order
|
||||||
|
err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create order: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) updateOrder(ctx context.Context, dbConn *sql.DB, req *models.OrderUpdateRequest) (*models.Order, error) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
query := "UPDATE data_order SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, date_created, date_updated, name"
|
||||||
|
row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name)
|
||||||
|
|
||||||
|
var item models.Order
|
||||||
|
err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update order: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) deleteOrder(ctx context.Context, dbConn *sql.DB, id string) error {
|
||||||
|
now := time.Now()
|
||||||
|
query := "UPDATE data_order 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 order: %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 *OrderHandler) fetchOrders(ctx context.Context, dbConn *sql.DB, filter models.OrderFilter, limit, offset int) ([]models.Order, error) {
|
||||||
|
whereClause, args := h.buildWhereClause(filter)
|
||||||
|
query := fmt.Sprintf("SELECT id, status, date_created, date_updated, name FROM data_order 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 orders query failed: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
items := make([]models.Order, 0, limit)
|
||||||
|
for rows.Next() {
|
||||||
|
var item models.Order
|
||||||
|
err := rows.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scan Order failed: %w", err)
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter models.OrderFilter, total *int) error {
|
||||||
|
whereClause, args := h.buildWhereClause(filter)
|
||||||
|
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM data_order 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter models.OrderFilter) (*models.AggregateData, error) {
|
||||||
|
aggregate := &models.AggregateData{
|
||||||
|
ByStatus: make(map[string]int),
|
||||||
|
}
|
||||||
|
|
||||||
|
whereClause, args := h.buildWhereClause(filter)
|
||||||
|
statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM data_order WHERE %s GROUP BY status ORDER BY status", whereClause)
|
||||||
|
|
||||||
|
rows, err := dbConn.QueryContext(ctx, statusQuery, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("status query failed: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var status string
|
||||||
|
var count int
|
||||||
|
if err := rows.Scan(&status, &count); err != nil {
|
||||||
|
return nil, fmt.Errorf("status scan failed: %w", err)
|
||||||
|
}
|
||||||
|
aggregate.ByStatus[status] = count
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case "active":
|
||||||
|
aggregate.TotalActive = count
|
||||||
|
case "draft":
|
||||||
|
aggregate.TotalDraft = count
|
||||||
|
case "inactive":
|
||||||
|
aggregate.TotalInactive = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
func (h *OrderHandler) 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 *OrderHandler) 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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
return limit, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) parseFilterParams(c *gin.Context) models.OrderFilter {
|
||||||
|
filter := models.OrderFilter{}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) buildWhereClause(filter models.OrderFilter) (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 *OrderHandler) 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
683
internal/handlers/product/product.go
Normal file
683
internal/handlers/product/product.go
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"api-service/internal/config"
|
||||||
|
"api-service/internal/database"
|
||||||
|
models "api-service/internal/models/product"
|
||||||
|
"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("product_status", validateProductStatus)
|
||||||
|
if db == nil {
|
||||||
|
log.Fatal("Failed to initialize database connection")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom validation for product status
|
||||||
|
func validateProductStatus(fl validator.FieldLevel) bool {
|
||||||
|
return models.IsValidStatus(fl.Field().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProductHandler handles product services
|
||||||
|
type ProductHandler struct {
|
||||||
|
db database.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProductHandler creates a new ProductHandler
|
||||||
|
func NewProductHandler() *ProductHandler {
|
||||||
|
return &ProductHandler{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProduct godoc
|
||||||
|
// @Summary Get product with pagination and optional aggregation
|
||||||
|
// @Description Returns a paginated list of products with optional summary statistics
|
||||||
|
// @Tags product
|
||||||
|
// @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.ProductGetResponse "Success response"
|
||||||
|
// @Failure 400 {object} models.ErrorResponse "Bad request"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/products [get]
|
||||||
|
func (h *ProductHandler) GetProduct(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("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.Product
|
||||||
|
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 - FIXED: Proper method name
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
result, err := h.fetchProducts(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.ProductGetResponse{
|
||||||
|
Message: "Data product berhasil diambil",
|
||||||
|
Data: items,
|
||||||
|
Meta: meta,
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeAggregation && aggregateData != nil {
|
||||||
|
response.Summary = aggregateData
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProductByID godoc
|
||||||
|
// @Summary Get Product by ID
|
||||||
|
// @Description Returns a single product by ID
|
||||||
|
// @Tags product
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Product ID (UUID)"
|
||||||
|
// @Success 200 {object} models.ProductGetByIDResponse "Success response"
|
||||||
|
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
|
||||||
|
// @Failure 404 {object} models.ErrorResponse "Product not found"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/product/{id} [get]
|
||||||
|
func (h *ProductHandler) GetProductByID(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("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.getProductByID(ctx, dbConn, id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
h.respondError(c, "Product not found", err, http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
h.logAndRespondError(c, "Failed to get product", err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := models.ProductGetByIDResponse{
|
||||||
|
Message: "Product details retrieved successfully",
|
||||||
|
Data: item,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProduct godoc
|
||||||
|
// @Summary Create product
|
||||||
|
// @Description Creates a new product record
|
||||||
|
// @Tags product
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body models.ProductCreateRequest true "Product creation request"
|
||||||
|
// @Success 201 {object} models.ProductCreateResponse "Product created successfully"
|
||||||
|
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/products [post]
|
||||||
|
func (h *ProductHandler) CreateProduct(c *gin.Context) {
|
||||||
|
var req models.ProductCreateRequest
|
||||||
|
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("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.createProduct(ctx, dbConn, &req)
|
||||||
|
if err != nil {
|
||||||
|
h.logAndRespondError(c, "Failed to create product", err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := models.ProductCreateResponse{
|
||||||
|
Message: "Product berhasil dibuat",
|
||||||
|
Data: item,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProduct godoc
|
||||||
|
// @Summary Update product
|
||||||
|
// @Description Updates an existing product record
|
||||||
|
// @Tags product
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Product ID (UUID)"
|
||||||
|
// @Param request body models.ProductUpdateRequest true "Product update request"
|
||||||
|
// @Success 200 {object} models.ProductUpdateResponse "Product updated successfully"
|
||||||
|
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
|
||||||
|
// @Failure 404 {object} models.ErrorResponse "Product not found"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/product/{id} [put]
|
||||||
|
func (h *ProductHandler) UpdateProduct(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.ProductUpdateRequest
|
||||||
|
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("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.updateProduct(ctx, dbConn, &req)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
h.respondError(c, "Product not found", err, http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
h.logAndRespondError(c, "Failed to update product", err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := models.ProductUpdateResponse{
|
||||||
|
Message: "Product berhasil diperbarui",
|
||||||
|
Data: item,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProduct godoc
|
||||||
|
// @Summary Delete product
|
||||||
|
// @Description Soft deletes a product by setting status to 'deleted'
|
||||||
|
// @Tags product
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Product ID (UUID)"
|
||||||
|
// @Success 200 {object} models.ProductDeleteResponse "Product deleted successfully"
|
||||||
|
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
|
||||||
|
// @Failure 404 {object} models.ErrorResponse "Product not found"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/product/{id} [delete]
|
||||||
|
func (h *ProductHandler) DeleteProduct(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("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.deleteProduct(ctx, dbConn, id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
h.respondError(c, "Product not found", err, http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
h.logAndRespondError(c, "Failed to delete product", err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := models.ProductDeleteResponse{
|
||||||
|
Message: "Product berhasil dihapus",
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProductStats godoc
|
||||||
|
// @Summary Get product statistics
|
||||||
|
// @Description Returns comprehensive statistics about product data
|
||||||
|
// @Tags product
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param status query string false "Filter statistics by status"
|
||||||
|
// @Success 200 {object} models.AggregateData "Statistics data"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/products/stats [get]
|
||||||
|
func (h *ProductHandler) GetProductStats(c *gin.Context) {
|
||||||
|
dbConn, err := h.db.GetDB("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 product berhasil diambil",
|
||||||
|
"data": aggregateData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database operations
|
||||||
|
func (h *ProductHandler) getProductByID(ctx context.Context, dbConn *sql.DB, id string) (*models.Product, error) {
|
||||||
|
query := "SELECT id, status, date_created, date_updated, name FROM data_product WHERE id = $1 AND status != 'deleted'"
|
||||||
|
row := dbConn.QueryRowContext(ctx, query, id)
|
||||||
|
|
||||||
|
var item models.Product
|
||||||
|
err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) createProduct(ctx context.Context, dbConn *sql.DB, req *models.ProductCreateRequest) (*models.Product, error) {
|
||||||
|
id := uuid.New().String()
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
query := "INSERT INTO data_product (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, date_created, date_updated, name"
|
||||||
|
row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name)
|
||||||
|
|
||||||
|
var item models.Product
|
||||||
|
err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create product: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) updateProduct(ctx context.Context, dbConn *sql.DB, req *models.ProductUpdateRequest) (*models.Product, error) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
query := "UPDATE data_product SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, date_created, date_updated, name"
|
||||||
|
row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name)
|
||||||
|
|
||||||
|
var item models.Product
|
||||||
|
err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update product: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) deleteProduct(ctx context.Context, dbConn *sql.DB, id string) error {
|
||||||
|
now := time.Now()
|
||||||
|
query := "UPDATE data_product 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 product: %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 *ProductHandler) fetchProducts(ctx context.Context, dbConn *sql.DB, filter models.ProductFilter, limit, offset int) ([]models.Product, error) {
|
||||||
|
whereClause, args := h.buildWhereClause(filter)
|
||||||
|
query := fmt.Sprintf("SELECT id, status, date_created, date_updated, name FROM data_product 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 products query failed: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
items := make([]models.Product, 0, limit)
|
||||||
|
for rows.Next() {
|
||||||
|
var item models.Product
|
||||||
|
err := rows.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scan Product failed: %w", err)
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter models.ProductFilter, total *int) error {
|
||||||
|
whereClause, args := h.buildWhereClause(filter)
|
||||||
|
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM data_product 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter models.ProductFilter) (*models.AggregateData, error) {
|
||||||
|
aggregate := &models.AggregateData{
|
||||||
|
ByStatus: make(map[string]int),
|
||||||
|
}
|
||||||
|
|
||||||
|
whereClause, args := h.buildWhereClause(filter)
|
||||||
|
statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM data_product WHERE %s GROUP BY status ORDER BY status", whereClause)
|
||||||
|
|
||||||
|
rows, err := dbConn.QueryContext(ctx, statusQuery, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("status query failed: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var status string
|
||||||
|
var count int
|
||||||
|
if err := rows.Scan(&status, &count); err != nil {
|
||||||
|
return nil, fmt.Errorf("status scan failed: %w", err)
|
||||||
|
}
|
||||||
|
aggregate.ByStatus[status] = count
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case "active":
|
||||||
|
aggregate.TotalActive = count
|
||||||
|
case "draft":
|
||||||
|
aggregate.TotalDraft = count
|
||||||
|
case "inactive":
|
||||||
|
aggregate.TotalInactive = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
func (h *ProductHandler) 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 *ProductHandler) 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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
return limit, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) parseFilterParams(c *gin.Context) models.ProductFilter {
|
||||||
|
filter := models.ProductFilter{}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) buildWhereClause(filter models.ProductFilter) (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 *ProductHandler) 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
195
internal/models/order/order.go
Normal file
195
internal/models/order/order.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility
|
||||||
|
type NullableInt32 struct {
|
||||||
|
Int32 int32 `json:"int32,omitempty"`
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the sql.Scanner interface for NullableInt32
|
||||||
|
func (n *NullableInt32) Scan(value interface{}) error {
|
||||||
|
var ni sql.NullInt32
|
||||||
|
if err := ni.Scan(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n.Int32 = ni.Int32
|
||||||
|
n.Valid = ni.Valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver.Valuer interface for NullableInt32
|
||||||
|
func (n NullableInt32) Value() (driver.Value, error) {
|
||||||
|
if !n.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return n.Int32, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order represents the data structure for the order table
|
||||||
|
type Order struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
Status string `json:"status" db:"status"`
|
||||||
|
Sort 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 for Order
|
||||||
|
func (r Order) MarshalJSON() ([]byte, error) {
|
||||||
|
type Alias Order
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
func (r *Order) GetName() string {
|
||||||
|
if r.Name.Valid {
|
||||||
|
return r.Name.String
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response struct for GET by ID
|
||||||
|
type OrderGetByIDResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data *Order `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced GET response with pagination and aggregation
|
||||||
|
type OrderGetResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data []Order `json:"data"`
|
||||||
|
Meta MetaResponse `json:"meta"`
|
||||||
|
Summary *AggregateData `json:"summary,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request struct for create
|
||||||
|
type OrderCreateRequest struct {
|
||||||
|
Status string `json:"status" validate:"required,oneof=draft active inactive"`
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response struct for create
|
||||||
|
type OrderCreateResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data *Order `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update request
|
||||||
|
type OrderUpdateRequest 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 for update
|
||||||
|
type OrderUpdateResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data *Order `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response struct for delete
|
||||||
|
type OrderDeleteResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata for pagination
|
||||||
|
type MetaResponse struct {
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
CurrentPage int `json:"current_page"`
|
||||||
|
HasNext bool `json:"has_next"`
|
||||||
|
HasPrev bool `json:"has_prev"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate data for summary
|
||||||
|
type AggregateData struct {
|
||||||
|
TotalActive int `json:"total_active"`
|
||||||
|
TotalDraft int `json:"total_draft"`
|
||||||
|
TotalInactive int `json:"total_inactive"`
|
||||||
|
ByStatus map[string]int `json:"by_status"`
|
||||||
|
LastUpdated *time.Time `json:"last_updated,omitempty"`
|
||||||
|
CreatedToday int `json:"created_today"`
|
||||||
|
UpdatedToday int `json:"updated_today"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error response
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter struct for query parameters
|
||||||
|
type OrderFilter 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation constants
|
||||||
|
const (
|
||||||
|
StatusDraft = "draft"
|
||||||
|
StatusActive = "active"
|
||||||
|
StatusInactive = "inactive"
|
||||||
|
StatusDeleted = "deleted"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidStatuses for validation
|
||||||
|
var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive}
|
||||||
|
|
||||||
|
// IsValidStatus helper function
|
||||||
|
func IsValidStatus(status string) bool {
|
||||||
|
for _, validStatus := range ValidStatuses {
|
||||||
|
if status == validStatus {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
195
internal/models/product/product.go
Normal file
195
internal/models/product/product.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility
|
||||||
|
type NullableInt32 struct {
|
||||||
|
Int32 int32 `json:"int32,omitempty"`
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the sql.Scanner interface for NullableInt32
|
||||||
|
func (n *NullableInt32) Scan(value interface{}) error {
|
||||||
|
var ni sql.NullInt32
|
||||||
|
if err := ni.Scan(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n.Int32 = ni.Int32
|
||||||
|
n.Valid = ni.Valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver.Valuer interface for NullableInt32
|
||||||
|
func (n NullableInt32) Value() (driver.Value, error) {
|
||||||
|
if !n.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return n.Int32, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product represents the data structure for the product table
|
||||||
|
type Product struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
Status string `json:"status" db:"status"`
|
||||||
|
Sort 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 for Product
|
||||||
|
func (r Product) MarshalJSON() ([]byte, error) {
|
||||||
|
type Alias Product
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
func (r *Product) GetName() string {
|
||||||
|
if r.Name.Valid {
|
||||||
|
return r.Name.String
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response struct for GET by ID
|
||||||
|
type ProductGetByIDResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data *Product `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced GET response with pagination and aggregation
|
||||||
|
type ProductGetResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data []Product `json:"data"`
|
||||||
|
Meta MetaResponse `json:"meta"`
|
||||||
|
Summary *AggregateData `json:"summary,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request struct for create
|
||||||
|
type ProductCreateRequest struct {
|
||||||
|
Status string `json:"status" validate:"required,oneof=draft active inactive"`
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response struct for create
|
||||||
|
type ProductCreateResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data *Product `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update request
|
||||||
|
type ProductUpdateRequest 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 for update
|
||||||
|
type ProductUpdateResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data *Product `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response struct for delete
|
||||||
|
type ProductDeleteResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata for pagination
|
||||||
|
type MetaResponse struct {
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
CurrentPage int `json:"current_page"`
|
||||||
|
HasNext bool `json:"has_next"`
|
||||||
|
HasPrev bool `json:"has_prev"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate data for summary
|
||||||
|
type AggregateData struct {
|
||||||
|
TotalActive int `json:"total_active"`
|
||||||
|
TotalDraft int `json:"total_draft"`
|
||||||
|
TotalInactive int `json:"total_inactive"`
|
||||||
|
ByStatus map[string]int `json:"by_status"`
|
||||||
|
LastUpdated *time.Time `json:"last_updated,omitempty"`
|
||||||
|
CreatedToday int `json:"created_today"`
|
||||||
|
UpdatedToday int `json:"updated_today"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error response
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter struct for query parameters
|
||||||
|
type ProductFilter 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation constants
|
||||||
|
const (
|
||||||
|
StatusDraft = "draft"
|
||||||
|
StatusActive = "active"
|
||||||
|
StatusInactive = "inactive"
|
||||||
|
StatusDeleted = "deleted"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidStatuses for validation
|
||||||
|
var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive}
|
||||||
|
|
||||||
|
// IsValidStatus helper function
|
||||||
|
func IsValidStatus(status string) bool {
|
||||||
|
for _, validStatus := range ValidStatuses {
|
||||||
|
if status == validStatus {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
orderHandlers "api-service/internal/handlers/order"
|
||||||
retribusiHandlers "api-service/internal/handlers/retribusi"
|
retribusiHandlers "api-service/internal/handlers/retribusi"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -59,6 +60,15 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
|||||||
v1.DELETE("/retribusi/:id", retribusiHandler.DeleteRetribusi)
|
v1.DELETE("/retribusi/:id", retribusiHandler.DeleteRetribusi)
|
||||||
|
|
||||||
// Protected routes (require authentication)
|
// Protected routes (require authentication)
|
||||||
|
|
||||||
|
// Order endpoints
|
||||||
|
orderHandler := orderHandlers.NewOrderHandler()
|
||||||
|
v1.GET("/orders", orderHandler.GetOrder)
|
||||||
|
v1.GET("/order/:id", orderHandler.GetOrderByID)
|
||||||
|
v1.POST("/orders", orderHandler.CreateOrder)
|
||||||
|
v1.PUT("/order/:id", orderHandler.UpdateOrder)
|
||||||
|
v1.DELETE("/order/:id", orderHandler.DeleteOrder)
|
||||||
|
|
||||||
protected := v1.Group("/")
|
protected := v1.Group("/")
|
||||||
protected.Use(middleware.JWTAuthMiddleware(authService))
|
protected.Use(middleware.JWTAuthMiddleware(authService))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
package product
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
model "api-service/internal/models/product"
|
|
||||||
"api-service/internal/repository/product"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service defines the interface for product business logic
|
|
||||||
type Service interface {
|
|
||||||
CreateProduct(ctx context.Context, req *model.ProductCreateRequest) (*model.ProductResponse, error)
|
|
||||||
GetProduct(ctx context.Context, id string) (*model.ProductResponse, error)
|
|
||||||
GetAllProducts(ctx context.Context) (*model.ProductsResponse, error)
|
|
||||||
UpdateProduct(ctx context.Context, id string, req *model.ProductUpdateRequest) (*model.ProductResponse, error)
|
|
||||||
DeleteProduct(ctx context.Context, id string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// service implements the Service interface
|
|
||||||
type service struct {
|
|
||||||
repo product.Repository
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewService creates a new product service
|
|
||||||
func NewService(repo product.Repository) Service {
|
|
||||||
return &service{repo: repo}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateProduct creates a new product
|
|
||||||
func (s *service) CreateProduct(ctx context.Context, req *model.ProductCreateRequest) (*model.ProductResponse, error) {
|
|
||||||
if req.Name == "" {
|
|
||||||
return nil, errors.New("product name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Price <= 0 {
|
|
||||||
return nil, errors.New("product price must be greater than 0")
|
|
||||||
}
|
|
||||||
|
|
||||||
product := &model.Product{
|
|
||||||
ID: generateID(),
|
|
||||||
Name: req.Name,
|
|
||||||
Description: req.Description,
|
|
||||||
Price: req.Price,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.repo.Create(ctx, product)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.ProductResponse{
|
|
||||||
ID: product.ID,
|
|
||||||
Name: product.Name,
|
|
||||||
Description: product.Description,
|
|
||||||
Price: product.Price,
|
|
||||||
CreatedAt: product.CreatedAt,
|
|
||||||
UpdatedAt: product.UpdatedAt,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetProduct retrieves a product by ID
|
|
||||||
func (s *service) GetProduct(ctx context.Context, id string) (*model.ProductResponse, error) {
|
|
||||||
if id == "" {
|
|
||||||
return nil, errors.New("product ID is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
product, err := s.repo.GetByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.ProductResponse{
|
|
||||||
ID: product.ID,
|
|
||||||
Name: product.Name,
|
|
||||||
Description: product.Description,
|
|
||||||
Price: product.Price,
|
|
||||||
CreatedAt: product.CreatedAt,
|
|
||||||
UpdatedAt: product.UpdatedAt,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllProducts retrieves all products
|
|
||||||
func (s *service) GetAllProducts(ctx context.Context) (*model.ProductsResponse, error) {
|
|
||||||
products, err := s.repo.GetAll(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.ProductsResponse{
|
|
||||||
Data: products,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateProduct updates an existing product
|
|
||||||
func (s *service) UpdateProduct(ctx context.Context, id string, req *model.ProductUpdateRequest) (*model.ProductResponse, error) {
|
|
||||||
if id == "" {
|
|
||||||
return nil, errors.New("product ID is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
existingProduct, err := s.repo.GetByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
existingProduct.Name = req.Name
|
|
||||||
existingProduct.Description = req.Description
|
|
||||||
existingProduct.Price = req.Price
|
|
||||||
existingProduct.UpdatedAt = time.Now()
|
|
||||||
|
|
||||||
err = s.repo.Update(ctx, existingProduct)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.ProductResponse{
|
|
||||||
ID: existingProduct.ID,
|
|
||||||
Name: existingProduct.Name,
|
|
||||||
Description: existingProduct.Description,
|
|
||||||
Price: existingProduct.Price,
|
|
||||||
CreatedAt: existingProduct.CreatedAt,
|
|
||||||
UpdatedAt: existingProduct.UpdatedAt,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteProduct deletes a product
|
|
||||||
func (s *service) DeleteProduct(ctx context.Context, id string) error {
|
|
||||||
if id == "" {
|
|
||||||
return errors.New("product ID is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.repo.Delete(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
func generateID() string {
|
|
||||||
return "prod_" + time.Now().Format("20060102150405")
|
|
||||||
}
|
|
||||||
@@ -32,6 +32,7 @@ go run tools/generate-handler.go <nama-handler> [methods]
|
|||||||
# Contoh:
|
# Contoh:
|
||||||
go run tools/generate-handler.go user get post put delete
|
go run tools/generate-handler.go user get post put delete
|
||||||
go run tools/generate-handler.go product get post
|
go run tools/generate-handler.go product get post
|
||||||
|
go run tools/ generate-handler.go order get post put delete stats
|
||||||
```
|
```
|
||||||
|
|
||||||
## Method yang Tersedia
|
## Method yang Tersedia
|
||||||
@@ -69,7 +70,7 @@ go run tools/generate-handler.go product get post
|
|||||||
|
|
||||||
1. Jalankan swagger generator:
|
1. Jalankan swagger generator:
|
||||||
```bash
|
```bash
|
||||||
swag init -g cmd/api/main.go
|
swag init -g cmd/api/main.go --parseDependency --parseInternal
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Jalankan aplikasi:
|
2. Jalankan aplikasi:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func maindiagnostic() {
|
||||||
fmt.Println("=== Database Connection Diagnostic Tool ===")
|
fmt.Println("=== Database Connection Diagnostic Tool ===")
|
||||||
|
|
||||||
// Load environment variables from .env file
|
// Load environment variables from .env file
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user