From 459ff6b384ebb50a989d101e097a4e35d7d69c14 Mon Sep 17 00:00:00 2001 From: Tran Anh Tuan Date: Fri, 8 May 2026 14:58:59 +0700 Subject: [PATCH] feat: add room management functionality --- configs/constants/constants.go | 1 + db/queries/room.sql | 2 +- db/queries/warehouse.sql | 2 +- docs/swagger/docs.go | 370 ++++++++++++++++++++- docs/swagger/swagger.json | 370 ++++++++++++++++++++- docs/swagger/swagger.yaml | 235 ++++++++++++- internal/mapper/room_mapper.go | 43 +++ internal/models/requests/room_request.go | 12 + internal/models/responses/room_response.go | 12 + internal/models/room_model.go | 12 + internal/repositories/room_repository.go | 48 +++ internal/routers/router.go | 9 + internal/services/room_service.go | 181 ++++++++++ sqlc_gen/room.sql.go | 4 +- sqlc_gen/warehouse.sql.go | 2 +- 15 files changed, 1289 insertions(+), 14 deletions(-) create mode 100644 internal/mapper/room_mapper.go create mode 100644 internal/models/requests/room_request.go create mode 100644 internal/models/responses/room_response.go create mode 100644 internal/models/room_model.go create mode 100644 internal/repositories/room_repository.go create mode 100644 internal/services/room_service.go diff --git a/configs/constants/constants.go b/configs/constants/constants.go index 86367cd..f648bf1 100644 --- a/configs/constants/constants.go +++ b/configs/constants/constants.go @@ -12,6 +12,7 @@ const ( const ( API_GROUP_AUTH = "/auth" API_GROUP_WAREHOUSE = "/warehouses" + API_GROUP_ROOM = "/rooms" ) const ( diff --git a/db/queries/room.sql b/db/queries/room.sql index f9d4de2..9081e1d 100644 --- a/db/queries/room.sql +++ b/db/queries/room.sql @@ -18,7 +18,7 @@ RETURNING *; -- name: UpdateRoom :one UPDATE rooms -SET name = coalesce(sqlc.arg(name), name), +SET name = CASE WHEN sqlc.arg(name) = '' THEN name ELSE sqlc.arg(name) END, description = coalesce(sqlc.arg(description), description), updated_at = sqlc.arg(updated_at) WHERE id = sqlc.arg(id) diff --git a/db/queries/warehouse.sql b/db/queries/warehouse.sql index 946a993..896a749 100644 --- a/db/queries/warehouse.sql +++ b/db/queries/warehouse.sql @@ -18,7 +18,7 @@ RETURNING *; -- name: UpdateWarehouse :one UPDATE warehouses -SET name = CASE WHEN sqlc.arg(name) = '' THEN name ELSE sqlc.arg(name) END, +SET name = CASE WHEN sqlc.arg(name) = '' THEN name ELSE sqlc.arg(name) END, description = coalesce(sqlc.arg(description), description), address = coalesce(sqlc.arg(address), address), updated_at = sqlc.arg(updated_at) diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 68fde21..7b118f6 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -105,6 +105,279 @@ const docTemplate = `{ } } }, + "/v1/rooms": { + "get": { + "description": "Retrieve a list of all rooms ordered by creation date", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "room" + ], + "summary": "List all rooms", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Room" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + }, + "post": { + "description": "Create a new room with the provided details", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "room" + ], + "summary": "Create a new room", + "parameters": [ + { + "description": "Room request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.CreateRoomRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/responses.CreateRoomResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, + "/v1/rooms/{id}": { + "get": { + "description": "Retrieve a single room using its unique identifier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "room" + ], + "summary": "Get room by ID", + "parameters": [ + { + "type": "integer", + "description": "Room ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.Room" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + }, + "put": { + "description": "Update an existing room by its ID. Only non-empty fields will be updated.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "room" + ], + "summary": "Update room", + "parameters": [ + { + "type": "integer", + "description": "Room ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Room request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.UpdateRoomRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/responses.UpdateRoomResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Delete a room by its unique identifier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "room" + ], + "summary": "Delete room", + "parameters": [ + { + "type": "integer", + "description": "Room ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.SuccessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, "/v1/warehouses": { "get": { "description": "Retrieve a list of all warehouses ordered by creation date", @@ -167,7 +440,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/requests.CUWarehouseRequest" + "$ref": "#/definitions/requests.CreateWarehouseRequest" } } ], @@ -292,7 +565,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/requests.CUWarehouseRequest" + "$ref": "#/definitions/requests.UpdateWarehouseRequest" } } ], @@ -374,6 +647,29 @@ const docTemplate = `{ } }, "definitions": { + "models.Room": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "warehouseId": { + "type": "integer" + } + } + }, "models.Warehouse": { "type": "object", "properties": { @@ -420,7 +716,25 @@ const docTemplate = `{ } } }, - "requests.CUWarehouseRequest": { + "requests.CreateRoomRequest": { + "type": "object", + "required": [ + "name", + "warehouseId" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "warehouseId": { + "type": "integer" + } + } + }, + "requests.CreateWarehouseRequest": { "type": "object", "required": [ "address", @@ -438,6 +752,31 @@ const docTemplate = `{ } } }, + "requests.UpdateRoomRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "requests.UpdateWarehouseRequest": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "response.ErrorResponse": { "type": "object", "properties": { @@ -479,6 +818,14 @@ const docTemplate = `{ } } }, + "responses.CreateRoomResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, "responses.CreateWarehouseResponse": { "type": "object", "properties": { @@ -487,6 +834,23 @@ const docTemplate = `{ } } }, + "responses.UpdateRoomResponse": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "warehouseId": { + "type": "integer" + } + } + }, "responses.UpdateWarehouseResponse": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 5a3f255..c1d4dda 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -99,6 +99,279 @@ } } }, + "/v1/rooms": { + "get": { + "description": "Retrieve a list of all rooms ordered by creation date", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "room" + ], + "summary": "List all rooms", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Room" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + }, + "post": { + "description": "Create a new room with the provided details", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "room" + ], + "summary": "Create a new room", + "parameters": [ + { + "description": "Room request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.CreateRoomRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/responses.CreateRoomResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, + "/v1/rooms/{id}": { + "get": { + "description": "Retrieve a single room using its unique identifier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "room" + ], + "summary": "Get room by ID", + "parameters": [ + { + "type": "integer", + "description": "Room ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.Room" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + }, + "put": { + "description": "Update an existing room by its ID. Only non-empty fields will be updated.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "room" + ], + "summary": "Update room", + "parameters": [ + { + "type": "integer", + "description": "Room ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Room request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.UpdateRoomRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/responses.UpdateRoomResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Delete a room by its unique identifier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "room" + ], + "summary": "Delete room", + "parameters": [ + { + "type": "integer", + "description": "Room ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.SuccessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, "/v1/warehouses": { "get": { "description": "Retrieve a list of all warehouses ordered by creation date", @@ -161,7 +434,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/requests.CUWarehouseRequest" + "$ref": "#/definitions/requests.CreateWarehouseRequest" } } ], @@ -286,7 +559,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/requests.CUWarehouseRequest" + "$ref": "#/definitions/requests.UpdateWarehouseRequest" } } ], @@ -368,6 +641,29 @@ } }, "definitions": { + "models.Room": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "warehouseId": { + "type": "integer" + } + } + }, "models.Warehouse": { "type": "object", "properties": { @@ -414,7 +710,25 @@ } } }, - "requests.CUWarehouseRequest": { + "requests.CreateRoomRequest": { + "type": "object", + "required": [ + "name", + "warehouseId" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "warehouseId": { + "type": "integer" + } + } + }, + "requests.CreateWarehouseRequest": { "type": "object", "required": [ "address", @@ -432,6 +746,31 @@ } } }, + "requests.UpdateRoomRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "requests.UpdateWarehouseRequest": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "response.ErrorResponse": { "type": "object", "properties": { @@ -473,6 +812,14 @@ } } }, + "responses.CreateRoomResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, "responses.CreateWarehouseResponse": { "type": "object", "properties": { @@ -481,6 +828,23 @@ } } }, + "responses.UpdateRoomResponse": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "warehouseId": { + "type": "integer" + } + } + }, "responses.UpdateWarehouseResponse": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 12b760a..70df188 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -1,5 +1,20 @@ basePath: /api/v1 definitions: + models.Room: + properties: + createdAt: + type: string + description: + type: string + id: + type: integer + name: + type: string + updatedAt: + type: string + warehouseId: + type: integer + type: object models.Warehouse: properties: address: @@ -31,7 +46,19 @@ definitions: - password - username type: object - requests.CUWarehouseRequest: + requests.CreateRoomRequest: + properties: + description: + type: string + name: + type: string + warehouseId: + type: integer + required: + - name + - warehouseId + type: object + requests.CreateWarehouseRequest: properties: address: type: string @@ -43,6 +70,22 @@ definitions: - address - name type: object + requests.UpdateRoomRequest: + properties: + description: + type: string + name: + type: string + type: object + requests.UpdateWarehouseRequest: + properties: + address: + type: string + description: + type: string + name: + type: string + type: object response.ErrorResponse: properties: code: @@ -70,11 +113,27 @@ definitions: id: type: string type: object + responses.CreateRoomResponse: + properties: + id: + type: integer + type: object responses.CreateWarehouseResponse: properties: id: type: integer type: object + responses.UpdateRoomResponse: + properties: + description: + type: string + id: + type: integer + name: + type: string + warehouseId: + type: integer + type: object responses.UpdateWarehouseResponse: properties: address: @@ -149,6 +208,176 @@ paths: summary: Health check tags: - health + /v1/rooms: + get: + consumes: + - application/json + description: Retrieve a list of all rooms ordered by creation date + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + items: + $ref: '#/definitions/models.Room' + type: array + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.ErrorResponse' + summary: List all rooms + tags: + - room + post: + consumes: + - application/json + description: Create a new room with the provided details + parameters: + - description: Room request body + in: body + name: body + required: true + schema: + $ref: '#/definitions/requests.CreateRoomRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + $ref: '#/definitions/responses.CreateRoomResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.ErrorResponse' + summary: Create a new room + tags: + - room + /v1/rooms/{id}: + delete: + consumes: + - application/json + description: Delete a room by its unique identifier + parameters: + - description: Room ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.SuccessResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.ErrorResponse' + summary: Delete room + tags: + - room + get: + consumes: + - application/json + description: Retrieve a single room using its unique identifier + parameters: + - description: Room ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + $ref: '#/definitions/models.Room' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.ErrorResponse' + summary: Get room by ID + tags: + - room + put: + consumes: + - application/json + description: Update an existing room by its ID. Only non-empty fields will be + updated. + parameters: + - description: Room ID + in: path + name: id + required: true + type: integer + - description: Room request body + in: body + name: body + required: true + schema: + $ref: '#/definitions/requests.UpdateRoomRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + $ref: '#/definitions/responses.UpdateRoomResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.ErrorResponse' + summary: Update room + tags: + - room /v1/warehouses: get: consumes: @@ -185,7 +414,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/requests.CUWarehouseRequest' + $ref: '#/definitions/requests.CreateWarehouseRequest' produces: - application/json responses: @@ -290,7 +519,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/requests.CUWarehouseRequest' + $ref: '#/definitions/requests.UpdateWarehouseRequest' produces: - application/json responses: diff --git a/internal/mapper/room_mapper.go b/internal/mapper/room_mapper.go new file mode 100644 index 0000000..bb1879a --- /dev/null +++ b/internal/mapper/room_mapper.go @@ -0,0 +1,43 @@ +package mapper + +import ( + "wm-backend/internal/models" + db "wm-backend/sqlc_gen" + + "github.com/jackc/pgx/v5/pgtype" +) + +func ToDomainRoom(r db.Room) *models.Room { + return &models.Room{ + ID: r.ID, + WarehouseID: r.WarehouseID, + Name: r.Name, + Description: r.Description.String, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + } +} + +func ToModelRoom(r *models.Room) *db.CreateRoomParams { + return &db.CreateRoomParams{ + WarehouseID: r.WarehouseID, + Name: r.Name, + Description: pgtype.Text{ + String: r.Description, + Valid: r.Description != "", + }, + CreatedAt: r.CreatedAt, + } +} + +func ToUpdateModelRoom(r *models.Room) *db.UpdateRoomParams { + return &db.UpdateRoomParams{ + Name: r.Name, + Description: pgtype.Text{ + String: r.Description, + Valid: r.Description != "", + }, + UpdatedAt: r.UpdatedAt, + ID: r.ID, + } +} diff --git a/internal/models/requests/room_request.go b/internal/models/requests/room_request.go new file mode 100644 index 0000000..facb8d3 --- /dev/null +++ b/internal/models/requests/room_request.go @@ -0,0 +1,12 @@ +package requests + +type CreateRoomRequest struct { + WarehouseID int64 `json:"warehouseId" binding:"required"` + Name string `json:"name" binding:"required"` + Description string `json:"description"` +} + +type UpdateRoomRequest struct { + Name string `json:"name"` + Description string `json:"description"` +} diff --git a/internal/models/responses/room_response.go b/internal/models/responses/room_response.go new file mode 100644 index 0000000..52c78a9 --- /dev/null +++ b/internal/models/responses/room_response.go @@ -0,0 +1,12 @@ +package responses + +type CreateRoomResponse struct { + ID int64 `json:"id"` +} + +type UpdateRoomResponse struct { + ID int64 `json:"id"` + WarehouseID int64 `json:"warehouseId"` + Name string `json:"name"` + Description string `json:"description"` +} diff --git a/internal/models/room_model.go b/internal/models/room_model.go new file mode 100644 index 0000000..ec47c38 --- /dev/null +++ b/internal/models/room_model.go @@ -0,0 +1,12 @@ +package models + +import "time" + +type Room struct { + ID int64 `json:"id"` + WarehouseID int64 `json:"warehouseId"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/internal/repositories/room_repository.go b/internal/repositories/room_repository.go new file mode 100644 index 0000000..23b0390 --- /dev/null +++ b/internal/repositories/room_repository.go @@ -0,0 +1,48 @@ +package repositories + +import ( + "context" + "wm-backend/internal/mapper" + "wm-backend/internal/models" + db "wm-backend/sqlc_gen" +) + +func CreateRoom(ctx context.Context, queries *db.Queries, body models.Room) (models.Room, error) { + result, err := queries.CreateRoom(ctx, *mapper.ToModelRoom(&body)) + if err != nil { + return models.Room{}, err + } + return *mapper.ToDomainRoom(result), nil +} + +func GetRoomByID(ctx context.Context, queries *db.Queries, id int64) (models.Room, error) { + result, err := queries.GetRoomByID(ctx, id) + if err != nil { + return models.Room{}, err + } + return *mapper.ToDomainRoom(result), nil +} + +func ListRooms(ctx context.Context, queries *db.Queries) ([]models.Room, error) { + results, err := queries.ListRooms(ctx) + if err != nil { + return nil, err + } + var items []models.Room + for _, r := range results { + items = append(items, *mapper.ToDomainRoom(r)) + } + return items, nil +} + +func UpdateRoom(ctx context.Context, queries *db.Queries, body models.Room) (models.Room, error) { + result, err := queries.UpdateRoom(ctx, *mapper.ToUpdateModelRoom(&body)) + if err != nil { + return models.Room{}, err + } + return *mapper.ToDomainRoom(result), nil +} + +func DeleteRoom(ctx context.Context, queries *db.Queries, id int64) error { + return queries.DeleteRoom(ctx, id) +} diff --git a/internal/routers/router.go b/internal/routers/router.go index 9d9e30a..0d5679c 100644 --- a/internal/routers/router.go +++ b/internal/routers/router.go @@ -37,6 +37,15 @@ func NewRouter() *gin.Engine { warehouse.PUT("/:id", utils.AsyncHandler(services.WareHouseUpdate)) warehouse.DELETE("/:id", utils.AsyncHandler(services.WareHouseDelete)) } + + room := v1.Group(constants.API_GROUP_ROOM) + { + room.GET("", utils.AsyncHandler(services.RoomList)) + room.GET("/:id", utils.AsyncHandler(services.RoomGetByID)) + room.POST("", utils.AsyncHandler(services.RoomCreate)) + room.PUT("/:id", utils.AsyncHandler(services.RoomUpdate)) + room.DELETE("/:id", utils.AsyncHandler(services.RoomDelete)) + } } r.GET(constants.API_PATH_PING, services.PingHandler) diff --git a/internal/services/room_service.go b/internal/services/room_service.go new file mode 100644 index 0000000..ff353c2 --- /dev/null +++ b/internal/services/room_service.go @@ -0,0 +1,181 @@ +package services + +import ( + "net/http" + "strconv" + "time" + "wm-backend/global" + "wm-backend/internal/models" + "wm-backend/internal/models/requests" + "wm-backend/internal/models/responses" + "wm-backend/internal/repositories" + "wm-backend/pkg/helper" + "wm-backend/response" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" +) + +// RoomCreate creates a new room. +// It validates the request body and creates the room in the database. +// +// @Summary Create a new room +// @Description Create a new room with the provided details +// @Tags room +// @Accept json +// @Produce json +// @Param body body requests.CreateRoomRequest true "Room request body" +// @Success 201 {object} response.SuccessResponse{data=responses.CreateRoomResponse} +// @Failure 400 {object} response.ErrorResponse +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/rooms [post] +func RoomCreate(c *gin.Context) error { + requestBody := requests.CreateRoomRequest{} + if helper.IsShouldBindJSON(c, &requestBody) { + return nil + } + roomModel := &models.Room{ + WarehouseID: requestBody.WarehouseID, + Name: requestBody.Name, + Description: requestBody.Description, + CreatedAt: time.Now(), + } + room, err := repositories.CreateRoom(c.Request.Context(), global.Queries, *roomModel) + if err != nil { + log.Error().Err(err).Msg("Failed to create room") + response.InternalServerError(c, http.StatusInternalServerError, "Failed to create room") + return nil + } + response.Created(c, "Room created successfully", &responses.CreateRoomResponse{ + ID: room.ID, + }) + return nil +} + +// RoomGetByID retrieves a single room by its ID. +// +// @Summary Get room by ID +// @Description Retrieve a single room using its unique identifier +// @Tags room +// @Accept json +// @Produce json +// @Param id path int true "Room ID" +// @Success 200 {object} response.SuccessResponse{data=models.Room} +// @Failure 400 {object} response.ErrorResponse +// @Failure 404 {object} response.ErrorResponse +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/rooms/{id} [get] +func RoomGetByID(c *gin.Context) error { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequestError(c, http.StatusBadRequest, "Invalid ID") + return nil + } + room, err := repositories.GetRoomByID(c.Request.Context(), global.Queries, id) + if err != nil { + log.Error().Err(err).Msgf("Failed to get room by ID: %d", id) + response.NotFoundError(c, http.StatusNotFound, "Room not found") + return nil + } + response.Ok(c, "Success", room) + return nil +} + +// RoomList retrieves all rooms. +// +// @Summary List all rooms +// @Description Retrieve a list of all rooms ordered by creation date +// @Tags room +// @Accept json +// @Produce json +// @Success 200 {object} response.SuccessResponse{data=[]models.Room} +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/rooms [get] +func RoomList(c *gin.Context) error { + rooms, err := repositories.ListRooms(c.Request.Context(), global.Queries) + if err != nil { + response.InternalServerError(c, http.StatusInternalServerError, "Failed to list rooms") + return nil + } + response.Ok(c, "Success", rooms) + return nil +} + +// RoomUpdate updates an existing room by its ID. +// It validates the request body, fetches the existing record, +// merges non-empty fields from the request, and updates the room in the database. +// +// @Summary Update room +// @Description Update an existing room by its ID. Only non-empty fields will be updated. +// @Tags room +// @Accept json +// @Produce json +// @Param id path int true "Room ID" +// @Param body body requests.UpdateRoomRequest true "Room request body" +// @Success 200 {object} response.SuccessResponse{data=responses.UpdateRoomResponse} +// @Failure 400 {object} response.ErrorResponse +// @Failure 404 {object} response.ErrorResponse +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/rooms/{id} [put] +func RoomUpdate(c *gin.Context) error { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequestError(c, http.StatusBadRequest, "Invalid ID") + return nil + } + requestBody := requests.UpdateRoomRequest{} + if helper.IsShouldBindJSON(c, &requestBody) { + return nil + } + existing, err := repositories.GetRoomByID(c.Request.Context(), global.Queries, id) + if err != nil { + response.NotFoundError(c, http.StatusNotFound, "Room not found") + return nil + } + if requestBody.Name != "" { + existing.Name = requestBody.Name + } + if requestBody.Description != "" { + existing.Description = requestBody.Description + } + existing.UpdatedAt = time.Now() + room, err := repositories.UpdateRoom(c.Request.Context(), global.Queries, existing) + if err != nil { + response.InternalServerError(c, http.StatusInternalServerError, "Failed to update room") + return nil + } + response.Ok(c, "Room updated successfully", &responses.UpdateRoomResponse{ + ID: room.ID, + WarehouseID: room.WarehouseID, + Name: room.Name, + Description: room.Description, + }) + return nil +} + +// RoomDelete deletes a room by its ID. +// +// @Summary Delete room +// @Description Delete a room by its unique identifier +// @Tags room +// @Accept json +// @Produce json +// @Param id path int true "Room ID" +// @Success 200 {object} response.SuccessResponse +// @Failure 400 {object} response.ErrorResponse +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/rooms/{id} [delete] +func RoomDelete(c *gin.Context) error { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequestError(c, http.StatusBadRequest, "Invalid ID") + return nil + } + err = repositories.DeleteRoom(c.Request.Context(), global.Queries, id) + if err != nil { + response.InternalServerError(c, http.StatusInternalServerError, "Failed to delete room") + return nil + } + response.Ok(c, "Đã xóa thành công", nil) + return nil +} diff --git a/sqlc_gen/room.sql.go b/sqlc_gen/room.sql.go index 68beb62..a411453 100644 --- a/sqlc_gen/room.sql.go +++ b/sqlc_gen/room.sql.go @@ -112,7 +112,7 @@ func (q *Queries) ListRooms(ctx context.Context) ([]Room, error) { const updateRoom = `-- name: UpdateRoom :one UPDATE rooms -SET name = coalesce($1, name), +SET name = CASE WHEN $1 = '' THEN name ELSE $1 END, description = coalesce($2, description), updated_at = $3 WHERE id = $4 @@ -120,7 +120,7 @@ RETURNING id, warehouse_id, name, description, created_at, updated_at ` type UpdateRoomParams struct { - Name string `db:"name" json:"name"` + Name interface{} `db:"name" json:"name"` Description pgtype.Text `db:"description" json:"description"` UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` ID int64 `db:"id" json:"id"` diff --git a/sqlc_gen/warehouse.sql.go b/sqlc_gen/warehouse.sql.go index 0b5f937..b11a528 100644 --- a/sqlc_gen/warehouse.sql.go +++ b/sqlc_gen/warehouse.sql.go @@ -112,7 +112,7 @@ func (q *Queries) ListWarehouses(ctx context.Context) ([]Warehouse, error) { const updateWarehouse = `-- name: UpdateWarehouse :one UPDATE warehouses -SET name = CASE WHEN $1 = '' THEN name ELSE $1 END, +SET name = CASE WHEN $1 = '' THEN name ELSE $1 END, description = coalesce($2, description), address = coalesce($3, address), updated_at = $4