feat: implement component-item management with CRUD operations and status updates

This commit is contained in:
Tran Anh Tuan
2026-05-11 17:49:18 +07:00
parent 9ea72b4eea
commit 0ff65a18c0
23 changed files with 3870 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ const (
API_GROUP_COMPONENT_TYPE = "/component-types"
API_GROUP_COMPONENT = "/components"
API_GROUP_COMPONENT_CODE = "/component-codes"
API_GROUP_COMPONENT_ITEM = "/component-items"
)
const (

View File

@@ -0,0 +1,75 @@
-- name: GetComponentItemByID :one
SELECT * FROM component_items
WHERE id = sqlc.arg(id);
-- name: ListComponentItems :many
SELECT * FROM component_items
ORDER BY created_at DESC;
-- name: CreateComponentItem :one
INSERT INTO component_items (component_id,container_id,quantity, status, metadata, created_at)
VALUES (
sqlc.arg(component_id),
sqlc.arg(container_id),
sqlc.arg(quantity),
sqlc.arg(status),
sqlc.arg(metadata),
sqlc.arg(created_at)
)
RETURNING *;
-- name: UpdateComponentItem :one
UPDATE component_items
SET component_id = CASE WHEN sqlc.arg(component_id) = '' THEN component_id ELSE sqlc.arg(component_id) END,
container_id = CASE WHEN sqlc.arg(container_id) = '' THEN container_id ELSE sqlc.arg(container_id) END,
metadata = coalesce(sqlc.arg(metadata), metadata),
updated_at = sqlc.arg(updated_at)
WHERE id = sqlc.arg(id)
RETURNING *;
-- name: DeleteComponentItem :execrows
DELETE FROM component_items
WHERE id = $1;
-- name: UpdateComponentItemStatus :one
UPDATE component_items
SET status = sqlc.arg(status),
updated_at = sqlc.arg(updated_at)
WHERE id = sqlc.arg(id)
RETURNING *;
-- name: UpdateComponentItemQuantity :one
UPDATE component_items
SET quantity = sqlc.arg(quantity),
updated_at = sqlc.arg(updated_at)
WHERE id = sqlc.arg(id)
RETURNING *;
-- name: GetComponentItemByComponentContainerStatus :one
SELECT * FROM component_items
WHERE component_id = sqlc.arg(component_id)
AND container_id = sqlc.arg(container_id)
AND status = sqlc.arg(status);
-- name: FindComponentItem :many
SELECT
c.name AS component_name,
ct.name AS type_name,
ci.quantity,
ci.status,
cn.name AS container_name,
cn.container_type,
s.name AS shelf_name,
cb.name AS cabinet_name,
r.name AS room_name,
w.name AS warehouse_name
FROM component_items ci
JOIN components c ON ci.component_id = c.id
JOIN component_types ct ON c.component_type_id = ct.id
JOIN containers cn ON ci.container_id = cn.id
JOIN shelves s ON cn.shelf_id = s.id
JOIN cabinets cb ON s.cabinet_id = cb.id
JOIN rooms r ON cb.room_id = r.id
JOIN warehouses w ON r.warehouse_id = w.id
WHERE ci.component_id = sqlc.arg(componentId) AND ci.quantity > 0;

View File

@@ -0,0 +1,15 @@
-- name: CreateComponentStatusHistory :one
INSERT INTO component_status_history (
component_item_id, old_status, new_status,
changed_quantity, note, changed_by, changed_at
)
VALUES (
sqlc.arg(component_item_id),
sqlc.arg(old_status),
sqlc.arg(new_status),
sqlc.arg(changed_quantity),
sqlc.arg(note),
sqlc.arg(changed_by),
sqlc.arg(changed_at)
)
RETURNING *;

View File

@@ -0,0 +1,517 @@
# Change Status Component - Implementation Guide (Go Service Layer)
## Overview
Khi người dùng cập nhật `status` của 1 bản ghi trong bảng `component_items`, hệ thống phải:
1. **Tách/mở rộng record** trong bảng `component_items` (quản lý quantity chính xác theo status)
2. **Tự động ghi log** vào bảng `component_status_history`
3. Tất cả trong **cùng 1 transaction**
### Nguyên tắc: Mỗi record = 1 nhóm linh kiện cùng tình trạng
Mỗi record `component_items` đại diện cho **một nhóm linh kiện có cùng component, cùng container, cùng status**. Khi đổi status một phần số lượng → phải tách record.
## Tables liên quan
- `component_items` — bản ghi linh kiện tại 1 vị trí (container), mỗi record = 1 nhóm cùng status
- `component_status_history` — lịch sử thay đổi tình trạng (audit log)
## Status Enum Values
```
normal | damaged | long_unused | expired | pending_inspection
```
---
## Business Logic - 3 Trường Hợp
### Request Body
```json
{
"status": "damaged",
"changed_quantity": 5,
"note": "Bị cháy khi test mạch",
"changed_by": "nguyenvana"
}
```
### Trường hợp 1: `changed_quantity` = NULL hoặc = `quantity` (đổi toàn bộ)
Đổi status của **toàn bộ** linh kiện trong record.
```
Trước: component_items (id=1): quantity=20, status="normal"
Request: { status: "damaged", changed_quantity: null } hoặc changed_quantity: 20
Sau: component_items (id=1): quantity=20, status="damaged"
```
→ Chỉ UPDATE status, không tách record.
### Trường hợp 2: `changed_quantity` < `quantity` (đổi một phần, CHƯA có record cùng status mới)
Tách thành 2 record: phần còn tốt + phần đổi status.
```
Trước: component_items (id=1): quantity=20, status="normal"
Request: { status: "damaged", changed_quantity: 5 }
Kiểm tra: KHÔNG có record nào có (component_id=1.component_id, container_id=1.container_id, status="damaged")
Sau:
component_items (id=1): quantity=15, status="normal" ← giảm 5
component_items (id=NEW): quantity=5, status="damaged" ← record mới
```
### Trường hợp 3: `changed_quantity` < `quantity` (đổi một phần, ĐÃ có record cùng status mới)
Giảm quantity record cũ, **cộng dồn** vào record đã có cùng status.
```
Trước:
component_items (id=1): quantity=20, status="normal"
component_items (id=2): quantity=3, status="damaged" ← đã có sẵn
Request: { status: "damaged", changed_quantity: 5 }
Kiểm tra: ĐÃ CÓ record (id=2) cùng (component_id, container_id, status="damaged")
Sau:
component_items (id=1): quantity=15, status="normal" ← giảm 5
component_items (id=2): quantity=8, status="damaged" ← 3 + 5 = 8
```
---
## Go Service Layer Flow
### 1. API Endpoint
```
PUT /api/v1/component-items/:id/status
```
### 2. Request Model
```go
type UpdateComponentItemStatusRequest struct {
Status string `json:"status" binding:"required,oneof=normal damaged long_unused expired pending_inspection"`
ChangedQuantity *int `json:"changed_quantity"`
Note string `json:"note"`
ChangedBy string `json:"changed_by" binding:"required"`
}
```
### 3. Chi tiết Transaction
```
BEGIN TRANSACTION
├── Bước 1: SELECT component_items WHERE id = :id
│ → Lưu lại old_status, old_quantity, component_id, container_id
├── Bước 2: Kiểm tra changed_quantity
│ │
│ ├── Nếu changed_quantity IS NULL hoặc changed_quantity == old_quantity:
│ │ │
│ │ └── Bước 2a: UPDATE component_items
│ │ SET status = :new_status, updated_at = NOW()
│ │ WHERE id = :id
│ │
│ └── Nếu changed_quantity < old_quantity (đổi một phần):
│ │
│ ├── Bước 2b-i: UPDATE component_items (record cũ)
│ │ SET quantity = old_quantity - changed_quantity, updated_at = NOW()
│ │ WHERE id = :id
│ │
│ ├── Bước 2b-ii: SELECT component_items
│ │ WHERE component_id = :component_id
│ │ AND container_id = :container_id
│ │ AND status = :new_status
│ │ → Kiểm tra đã có record cùng status chưa
│ │
│ ├── Nếu CHƯA có:
│ │ └── Bước 2b-iii: INSERT INTO component_items
│ │ (component_id, container_id, quantity, status, metadata, created_at)
│ │ VALUES (:component_id, :container_id, :changed_quantity, :new_status, :metadata, NOW())
│ │
│ └── Nếu ĐÃ có (existing_id):
│ └── Bước 2b-iv: UPDATE component_items
│ SET quantity = existing_quantity + changed_quantity, updated_at = NOW()
│ WHERE id = existing_id
├── Bước 3: INSERT INTO component_status_history
│ (component_item_id, old_status, new_status, changed_quantity, note, changed_by, changed_at)
│ VALUES (:id, :old_status, :new_status, :changed_quantity, :note, :changed_by, NOW())
└── COMMIT (hoặc ROLLBACK nếu có lỗi)
```
### 4. Service Logic (Pseudocode)
```go
func UpdateComponentItemStatus(ctx context.Context, dbPool *pgxpool.Pool, queries *db.Queries, id int64, req UpdateComponentItemStatusRequest) error {
// 1. Lấy bản ghi hiện tại
existing, err := queries.GetComponentItemByID(ctx, id)
if err != nil {
return ErrNotFound
}
// 2. Kiểm tra status không đổi
if existing.Status == db.ComponentItemStatusEnum(req.Status) {
return ErrStatusUnchanged
}
// 3. Bắt đầu transaction
tx, err := dbPool.Begin(ctx)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback(ctx)
}
}()
txQueries := queries.WithTx(tx)
newStatus := db.ComponentItemStatusEnum(req.Status)
// 4. Xác định changed_qty (default = toàn bộ)
changedQty := int32(existing.Quantity)
if req.ChangedQuantity != nil {
changedQty = int32(*req.ChangedQuantity)
}
// 5. Phân nhánh theo trường hợp
if changedQty >= existing.Quantity {
// Trường hợp 1: Đổi toàn bộ
_, err = txQueries.UpdateComponentItemStatus(ctx, db.UpdateComponentItemStatusParams{
ID: id,
Status: newStatus,
UpdatedAt: time.Now(),
})
} else {
// Trường hợp 2 & 3: Đổi một phần
// 5a. Giảm quantity record cũ
_, err = txQueries.UpdateComponentItemQuantity(ctx, db.UpdateComponentItemQuantityParams{
ID: id,
Quantity: existing.Quantity - changedQty,
UpdatedAt: time.Now(),
})
if err != nil {
return err
}
// 5b. Tìm record cùng (component_id, container_id, new_status)
existingNewStatus, findErr := txQueries.GetComponentItemByComponentContainerStatus(ctx,
db.GetComponentItemByComponentContainerStatusParams{
ComponentID: existing.ComponentID,
ContainerID: existing.ContainerID,
Status: newStatus,
},
)
if findErr == sql.ErrNoRows {
// Trường hợp 2: Chưa có → tạo record mới
_, err = txQueries.CreateComponentItem(ctx, db.CreateComponentItemParams{
ComponentID: existing.ComponentID,
ContainerID: existing.ContainerID,
Quantity: changedQty,
Status: newStatus,
Metadata: existing.Metadata,
CreatedAt: time.Now(),
})
} else if findErr == nil {
// Trường hợp 3: Đã có → cộng dồn quantity
_, err = txQueries.UpdateComponentItemQuantity(ctx, db.UpdateComponentItemQuantityParams{
ID: existingNewStatus.ID,
Quantity: existingNewStatus.Quantity + changedQty,
UpdatedAt: time.Now(),
})
} else {
return findErr
}
}
if err != nil {
return err
}
// 6. Ghi log vào component_status_history
_, err = txQueries.CreateComponentStatusHistory(ctx, db.CreateComponentStatusHistoryParams{
ComponentItemID: id,
OldStatus: db.NullComponentItemStatusEnum{
ComponentItemStatusEnum: existing.Status,
Valid: true,
},
NewStatus: newStatus,
ChangedQuantity: pgtype.Int4{Int32: changedQty, Valid: true},
Note: pgtype.Text{String: req.Note, Valid: req.Note != ""},
ChangedBy: pgtype.Text{String: req.ChangedBy, Valid: true},
ChangedAt: time.Now(),
})
if err != nil {
return err
}
// 7. Commit
return tx.Commit(ctx)
}
```
---
## SQL Queries cần viết (cho sqlc)
### db/queries/component_item.sql
```sql
-- name: GetComponentItemByID :one
SELECT * FROM component_items WHERE id = sqlc.arg(id);
-- name: UpdateComponentItemStatus :one
UPDATE component_items
SET status = sqlc.arg(status),
updated_at = sqlc.arg(updated_at)
WHERE id = sqlc.arg(id)
RETURNING *;
-- name: UpdateComponentItemQuantity :one
UPDATE component_items
SET quantity = sqlc.arg(quantity),
updated_at = sqlc.arg(updated_at)
WHERE id = sqlc.arg(id)
RETURNING *;
-- name: GetComponentItemByComponentContainerStatus :one
SELECT * FROM component_items
WHERE component_id = sqlc.arg(component_id)
AND container_id = sqlc.arg(container_id)
AND status = sqlc.arg(status);
-- name: CreateComponentItem :one
INSERT INTO component_items (component_id, container_id, quantity, status, metadata, created_at)
VALUES (
sqlc.arg(component_id),
sqlc.arg(container_id),
sqlc.arg(quantity),
sqlc.arg(status),
sqlc.arg(metadata),
sqlc.arg(created_at)
)
RETURNING *;
```
### db/queries/component_status_history.sql
```sql
-- name: CreateComponentStatusHistory :one
INSERT INTO component_status_history (
component_item_id, old_status, new_status,
changed_quantity, note, changed_by, changed_at
)
VALUES (
sqlc.arg(component_item_id),
sqlc.arg(old_status),
sqlc.arg(new_status),
sqlc.arg(changed_quantity),
sqlc.arg(note),
sqlc.arg(changed_by),
sqlc.arg(changed_at)
)
RETURNING *;
```
---
## Files cần tạo/sửa
| File | Action | Mô tả |
|------|--------|-------|
| `internal/models/requests/component_item_request.go` | Sửa | Thêm `UpdateComponentItemStatusRequest` |
| `internal/models/responses/component_item_response.go` | Sửa | Thêm response struct cho status change |
| `internal/repositories/component_item_repository.go` | Sửa | Hàm `UpdateComponentItemStatus` (chứa transaction logic) |
| `internal/services/component_item_service.go` | Sửa | Handler `ComponentItemUpdateStatus` |
| `internal/routers/router.go` | Sửa | Thêm route `PUT /api/v1/component-items/:id/status` |
| `db/queries/component_item.sql` | Sửa | Thêm `UpdateComponentItemQuantity`, `GetComponentItemByComponentContainerStatus` |
| `db/queries/component_status_history.sql` | Đã có | `CreateComponentStatusHistory` |
| `db/migrations/` | (đã có) | Bảng đã tạo ở migration 000001 |
---
## Edge Cases cần xử lý
| Trường hợp | Xử lý |
|---|---|
| `old_status == new_status` | Không update, không ghi history, trả về `"Status unchanged"` |
| `component_item` không tồn tại | Return 404 |
| `changed_quantity > quantity` | Return 400 — không thể đổi status nhiều hơn số lượng hiện có |
| `changed_quantity == 0` | Return 400 — không hợp lý |
| `changed_quantity < 0` | Return 400 — không cho số âm |
| `changed_quantity` NULL | Đổi status toàn bộ, tương đương `changed_quantity = quantity` |
| Transaction failed | Rollback, return 500 |
| `changed_by` | Lấy từ JWT token context hoặc request body, ưu tiên JWT |
| Record cũ sau khi trừ quantity = 0 | Xóa record đó (`DELETE WHERE quantity = 0`) hoặc để số 0 (tùy chọn) |
### Lưu ý: Xử lý khi quantity record cũ = 0 sau khi tách
```
Trước: (id=1): quantity=5, status="normal"
Request: changed_quantity=5, status="damaged"
Sau khi UPDATE: (id=1): quantity=0, status="normal"
```
Nên **xóa** record có quantity = 0 sau khi UPDATE, để tránh record rác:
```sql
-- name: DeleteComponentItemZeroQuantity :execrows
DELETE FROM component_items WHERE id = sqlc.arg(id) AND quantity = 0;
```
Hoặc thêm logic sau bước UPDATE quantity:
```go
if existing.Quantity - changedQty == 0 {
txQueries.DeleteComponentItem(ctx, id)
}
```
---
## Ví dụ Response
### Thành công - Đổi toàn bộ (Trường hợp 1):
```json
{
"code": 200,
"message": "Status updated successfully",
"data": {
"id": 1,
"old_status": "normal",
"new_status": "damaged",
"changed_quantity": 20,
"history_id": 42
}
}
```
### Thành công - Đổi một phần, tạo record mới (Trường hợp 2):
```json
{
"code": 200,
"message": "Status updated successfully",
"data": {
"id": 1,
"old_status": "normal",
"new_status": "damaged",
"changed_quantity": 5,
"new_component_item_id": 15,
"history_id": 42
}
}
```
### Thành công - Đổi một phần, cộng dồn record cũ (Trường hợp 3):
```json
{
"code": 200,
"message": "Status updated successfully",
"data": {
"id": 1,
"old_status": "normal",
"new_status": "damaged",
"changed_quantity": 5,
"merged_component_item_id": 2,
"history_id": 42
}
}
```
### Status không đổi:
```json
{
"code": 200,
"message": "Status unchanged",
"data": null
}
```
### Validation lỗi - changed_quantity > quantity:
```json
{
"code": 400,
"message": "changed_quantity (25) exceeds current quantity (20)",
"data": null
}
```
---
## Ví dụ Vòng Đời Hoàn Chỉnh
```
Ngày 01/03: Nhập kho 20 tụ điện 100uF vào container A
→ Invoice type=import, quantity=20
→ component_items (id=1): quantity=20, status="normal"
─────────────────────────────────────────────────
Ngày 02/04: Kiểm kho phát hiện 5 cái bị ẩm
→ Request: { status: "damaged", changed_quantity: 5, note: "Bị ẩm mốc" }
Transaction:
UPDATE (id=1): quantity = 20 - 5 = 15
INSERT (id=2): quantity = 5, status = "damaged"
INSERT history: old=normal, new=damaged, qty=5
Sau:
component_items (id=1): quantity=15, status="normal"
component_items (id=2): quantity=5, status="damaged"
─────────────────────────────────────────────────
Ngày 05/04: Phát hiện thêm 3 cái bị ẩm nữa
→ Request: { status: "damaged", changed_quantity: 3, note: "Tiếp tục bị ẩm" }
Transaction:
UPDATE (id=1): quantity = 15 - 3 = 12
UPDATE (id=2): quantity = 5 + 3 = 8 ← cộng dồn vào record damaged đã có
INSERT history: old=normal, new=damaged, qty=3
Sau:
component_items (id=1): quantity=12, status="normal"
component_items (id=2): quantity=8, status="damaged"
─────────────────────────────────────────────────
Ngày 10/04: Kỹ thuật xác nhận 5 cái damaged không sửa được
→ Request trên record id=2: { status: "expired", changed_quantity: 5 }
Transaction:
UPDATE (id=2): quantity = 8 - 5 = 3
INSERT (id=3): quantity = 5, status = "expired"
INSERT history: old=damaged, new=expired, qty=5
Sau:
component_items (id=1): quantity=12, status="normal"
component_items (id=2): quantity=3, status="damaged"
component_items (id=3): quantity=5, status="expired"
─────────────────────────────────────────────────
Ngày 15/04: Xuất bỏ 5 cái expired ra khỏi kho
→ Invoice type=export, note="Loại bỏ linh kiện expired"
→ stock_transactions ghi nhận xuất
→ component_items (id=3): bị xóa hoặc quantity = 0
```

4
docs/db/Note.md Normal file
View File

@@ -0,0 +1,4 @@
- Ở bảng component_items số lượng chỉ được thêm khi ở lúc tạo
* Sau này cập nhật sẽ được tự động từ invoice
* Trạng thái muốn cập nhật sẽ phải transaction cả bảng component_status_history

View File

@@ -288,6 +288,409 @@ const docTemplate = `{
}
}
},
"/api/v1/component-items": {
"get": {
"description": "Retrieve a list of all component items ordered by creation date",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "List all component items",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.ComponentItem"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"post": {
"description": "Create a new component item with the provided details",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "Create a new component item",
"parameters": [
{
"description": "Component item request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.CreateComponentItemRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.CreateComponentItemResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/api/v1/component-items/find/{componentId}": {
"get": {
"description": "Retrieve component items with full location details (container, shelf, cabinet, room, warehouse) for a given component ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "Find component items by component ID",
"parameters": [
{
"type": "integer",
"description": "Component ID",
"name": "componentId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.FindComponentItemResult"
}
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/api/v1/component-items/{id}": {
"get": {
"description": "Retrieve a single component item using its unique identifier",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "Get component item by ID",
"parameters": [
{
"type": "integer",
"description": "Component item ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.ComponentItem"
}
}
}
]
}
},
"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 component item by its ID. Only non-empty fields will be updated.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "Update component item",
"parameters": [
{
"type": "integer",
"description": "Component item ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Component item request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.UpdateComponentItemRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.UpdateComponentItemResponse"
}
}
}
]
}
},
"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 component item by its unique identifier",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "Delete component item",
"parameters": [
{
"type": "integer",
"description": "Component item 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"
}
}
}
}
},
"/api/v1/component-items/{id}/status": {
"put": {
"description": "Change the status of a component item. Supports partial quantity change with automatic split/merge logic. A status history record is created automatically.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "Change component item status",
"parameters": [
{
"type": "integer",
"description": "Component item ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Status change request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.UpdateComponentItemStatusRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.UpdateComponentItemStatusResponse"
}
}
}
]
}
},
"400": {
"description": "Validation error (e.g., changed_quantity \u003e quantity, status unchanged)",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"404": {
"description": "Component item not found",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/api/v1/component-types": {
"get": {
"description": "Retrieve a list of all component types ordered by creation date",
@@ -2375,6 +2778,38 @@ const docTemplate = `{
}
}
},
"models.ComponentItem": {
"type": "object",
"properties": {
"componentId": {
"type": "integer"
},
"containerId": {
"type": "integer"
},
"createdAt": {
"type": "string"
},
"id": {
"type": "integer"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
},
"quantity": {
"type": "integer"
},
"status": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"models.ComponentType": {
"type": "object",
"properties": {
@@ -2436,6 +2871,41 @@ const docTemplate = `{
}
}
},
"models.FindComponentItemResult": {
"type": "object",
"properties": {
"cabinetName": {
"type": "string"
},
"componentName": {
"type": "string"
},
"containerName": {
"type": "string"
},
"containerType": {
"type": "string"
},
"quantity": {
"type": "integer"
},
"roomName": {
"type": "string"
},
"shelfName": {
"type": "string"
},
"status": {
"type": "string"
},
"typeName": {
"type": "string"
},
"warehouseName": {
"type": "string"
}
}
},
"models.Room": {
"type": "object",
"properties": {
@@ -2576,6 +3046,35 @@ const docTemplate = `{
}
}
},
"requests.CreateComponentItemRequest": {
"type": "object",
"required": [
"componentId",
"containerId",
"quantity",
"status"
],
"properties": {
"componentId": {
"type": "integer"
},
"containerId": {
"type": "integer"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
},
"quantity": {
"type": "integer"
},
"status": {
"type": "string"
}
}
},
"requests.CreateComponentRequest": {
"type": "object",
"required": [
@@ -2751,6 +3250,47 @@ const docTemplate = `{
}
}
},
"requests.UpdateComponentItemRequest": {
"type": "object",
"properties": {
"componentId": {
"type": "integer"
},
"containerId": {
"type": "integer"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"requests.UpdateComponentItemStatusRequest": {
"type": "object",
"required": [
"status"
],
"properties": {
"changedQuantity": {
"type": "integer"
},
"note": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"normal",
"damaged",
"long_unused",
"expired",
"pending_inspection"
]
}
}
},
"requests.UpdateComponentRequest": {
"type": "object",
"properties": {
@@ -2913,6 +3453,14 @@ const docTemplate = `{
}
}
},
"responses.CreateComponentItemResponse": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
},
"responses.CreateComponentResponse": {
"type": "object",
"properties": {
@@ -2998,6 +3546,58 @@ const docTemplate = `{
}
}
},
"responses.UpdateComponentItemResponse": {
"type": "object",
"properties": {
"componentId": {
"type": "integer"
},
"containerId": {
"type": "integer"
},
"id": {
"type": "integer"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
},
"quantity": {
"type": "integer"
},
"status": {
"type": "string"
}
}
},
"responses.UpdateComponentItemStatusResponse": {
"type": "object",
"properties": {
"changedQuantity": {
"type": "integer"
},
"historyId": {
"type": "integer"
},
"id": {
"type": "integer"
},
"mergedComponentItemId": {
"type": "integer"
},
"newComponentItemId": {
"type": "integer"
},
"newStatus": {
"type": "string"
},
"oldStatus": {
"type": "string"
}
}
},
"responses.UpdateComponentResponse": {
"type": "object",
"properties": {

View File

@@ -282,6 +282,409 @@
}
}
},
"/api/v1/component-items": {
"get": {
"description": "Retrieve a list of all component items ordered by creation date",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "List all component items",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.ComponentItem"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"post": {
"description": "Create a new component item with the provided details",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "Create a new component item",
"parameters": [
{
"description": "Component item request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.CreateComponentItemRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.CreateComponentItemResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/api/v1/component-items/find/{componentId}": {
"get": {
"description": "Retrieve component items with full location details (container, shelf, cabinet, room, warehouse) for a given component ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "Find component items by component ID",
"parameters": [
{
"type": "integer",
"description": "Component ID",
"name": "componentId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.FindComponentItemResult"
}
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/api/v1/component-items/{id}": {
"get": {
"description": "Retrieve a single component item using its unique identifier",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "Get component item by ID",
"parameters": [
{
"type": "integer",
"description": "Component item ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.ComponentItem"
}
}
}
]
}
},
"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 component item by its ID. Only non-empty fields will be updated.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "Update component item",
"parameters": [
{
"type": "integer",
"description": "Component item ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Component item request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.UpdateComponentItemRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.UpdateComponentItemResponse"
}
}
}
]
}
},
"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 component item by its unique identifier",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "Delete component item",
"parameters": [
{
"type": "integer",
"description": "Component item 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"
}
}
}
}
},
"/api/v1/component-items/{id}/status": {
"put": {
"description": "Change the status of a component item. Supports partial quantity change with automatic split/merge logic. A status history record is created automatically.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component-item"
],
"summary": "Change component item status",
"parameters": [
{
"type": "integer",
"description": "Component item ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Status change request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.UpdateComponentItemStatusRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.UpdateComponentItemStatusResponse"
}
}
}
]
}
},
"400": {
"description": "Validation error (e.g., changed_quantity \u003e quantity, status unchanged)",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"404": {
"description": "Component item not found",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/api/v1/component-types": {
"get": {
"description": "Retrieve a list of all component types ordered by creation date",
@@ -2369,6 +2772,38 @@
}
}
},
"models.ComponentItem": {
"type": "object",
"properties": {
"componentId": {
"type": "integer"
},
"containerId": {
"type": "integer"
},
"createdAt": {
"type": "string"
},
"id": {
"type": "integer"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
},
"quantity": {
"type": "integer"
},
"status": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"models.ComponentType": {
"type": "object",
"properties": {
@@ -2430,6 +2865,41 @@
}
}
},
"models.FindComponentItemResult": {
"type": "object",
"properties": {
"cabinetName": {
"type": "string"
},
"componentName": {
"type": "string"
},
"containerName": {
"type": "string"
},
"containerType": {
"type": "string"
},
"quantity": {
"type": "integer"
},
"roomName": {
"type": "string"
},
"shelfName": {
"type": "string"
},
"status": {
"type": "string"
},
"typeName": {
"type": "string"
},
"warehouseName": {
"type": "string"
}
}
},
"models.Room": {
"type": "object",
"properties": {
@@ -2570,6 +3040,35 @@
}
}
},
"requests.CreateComponentItemRequest": {
"type": "object",
"required": [
"componentId",
"containerId",
"quantity",
"status"
],
"properties": {
"componentId": {
"type": "integer"
},
"containerId": {
"type": "integer"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
},
"quantity": {
"type": "integer"
},
"status": {
"type": "string"
}
}
},
"requests.CreateComponentRequest": {
"type": "object",
"required": [
@@ -2745,6 +3244,47 @@
}
}
},
"requests.UpdateComponentItemRequest": {
"type": "object",
"properties": {
"componentId": {
"type": "integer"
},
"containerId": {
"type": "integer"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"requests.UpdateComponentItemStatusRequest": {
"type": "object",
"required": [
"status"
],
"properties": {
"changedQuantity": {
"type": "integer"
},
"note": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"normal",
"damaged",
"long_unused",
"expired",
"pending_inspection"
]
}
}
},
"requests.UpdateComponentRequest": {
"type": "object",
"properties": {
@@ -2907,6 +3447,14 @@
}
}
},
"responses.CreateComponentItemResponse": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
},
"responses.CreateComponentResponse": {
"type": "object",
"properties": {
@@ -2992,6 +3540,58 @@
}
}
},
"responses.UpdateComponentItemResponse": {
"type": "object",
"properties": {
"componentId": {
"type": "integer"
},
"containerId": {
"type": "integer"
},
"id": {
"type": "integer"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
},
"quantity": {
"type": "integer"
},
"status": {
"type": "string"
}
}
},
"responses.UpdateComponentItemStatusResponse": {
"type": "object",
"properties": {
"changedQuantity": {
"type": "integer"
},
"historyId": {
"type": "integer"
},
"id": {
"type": "integer"
},
"mergedComponentItemId": {
"type": "integer"
},
"newComponentItemId": {
"type": "integer"
},
"newStatus": {
"type": "string"
},
"oldStatus": {
"type": "string"
}
}
},
"responses.UpdateComponentResponse": {
"type": "object",
"properties": {

View File

@@ -59,6 +59,27 @@ definitions:
type: integer
type: array
type: object
models.ComponentItem:
properties:
componentId:
type: integer
containerId:
type: integer
createdAt:
type: string
id:
type: integer
metadata:
items:
type: integer
type: array
quantity:
type: integer
status:
type: string
updatedAt:
type: string
type: object
models.ComponentType:
properties:
createdAt:
@@ -99,6 +120,29 @@ definitions:
updatedAt:
type: string
type: object
models.FindComponentItemResult:
properties:
cabinetName:
type: string
componentName:
type: string
containerName:
type: string
containerType:
type: string
quantity:
type: integer
roomName:
type: string
shelfName:
type: string
status:
type: string
typeName:
type: string
warehouseName:
type: string
type: object
models.Room:
properties:
createdAt:
@@ -192,6 +236,26 @@ definitions:
- code
- componentId
type: object
requests.CreateComponentItemRequest:
properties:
componentId:
type: integer
containerId:
type: integer
metadata:
items:
type: integer
type: array
quantity:
type: integer
status:
type: string
required:
- componentId
- containerId
- quantity
- status
type: object
requests.CreateComponentRequest:
properties:
componentTypeId:
@@ -309,6 +373,34 @@ definitions:
type: integer
type: array
type: object
requests.UpdateComponentItemRequest:
properties:
componentId:
type: integer
containerId:
type: integer
metadata:
items:
type: integer
type: array
type: object
requests.UpdateComponentItemStatusRequest:
properties:
changedQuantity:
type: integer
note:
type: string
status:
enum:
- normal
- damaged
- long_unused
- expired
- pending_inspection
type: string
required:
- status
type: object
requests.UpdateComponentRequest:
properties:
componentTypeId:
@@ -414,6 +506,11 @@ definitions:
id:
type: integer
type: object
responses.CreateComponentItemResponse:
properties:
id:
type: integer
type: object
responses.CreateComponentResponse:
properties:
id:
@@ -468,6 +565,40 @@ definitions:
isPrimary:
type: boolean
type: object
responses.UpdateComponentItemResponse:
properties:
componentId:
type: integer
containerId:
type: integer
id:
type: integer
metadata:
items:
type: integer
type: array
quantity:
type: integer
status:
type: string
type: object
responses.UpdateComponentItemStatusResponse:
properties:
changedQuantity:
type: integer
historyId:
type: integer
id:
type: integer
mergedComponentItemId:
type: integer
newComponentItemId:
type: integer
newStatus:
type: string
oldStatus:
type: string
type: object
responses.UpdateComponentResponse:
properties:
componentTypeId:
@@ -723,6 +854,260 @@ paths:
summary: Update component code
tags:
- component-code
/api/v1/component-items:
get:
consumes:
- application/json
description: Retrieve a list of all component items ordered by creation date
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
items:
$ref: '#/definitions/models.ComponentItem'
type: array
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: List all component items
tags:
- component-item
post:
consumes:
- application/json
description: Create a new component item with the provided details
parameters:
- description: Component item request body
in: body
name: body
required: true
schema:
$ref: '#/definitions/requests.CreateComponentItemRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
$ref: '#/definitions/responses.CreateComponentItemResponse'
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 component item
tags:
- component-item
/api/v1/component-items/{id}:
delete:
consumes:
- application/json
description: Delete a component item by its unique identifier
parameters:
- description: Component item 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 component item
tags:
- component-item
get:
consumes:
- application/json
description: Retrieve a single component item using its unique identifier
parameters:
- description: Component item 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.ComponentItem'
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 component item by ID
tags:
- component-item
put:
consumes:
- application/json
description: Update an existing component item by its ID. Only non-empty fields
will be updated.
parameters:
- description: Component item ID
in: path
name: id
required: true
type: integer
- description: Component item request body
in: body
name: body
required: true
schema:
$ref: '#/definitions/requests.UpdateComponentItemRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
$ref: '#/definitions/responses.UpdateComponentItemResponse'
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 component item
tags:
- component-item
/api/v1/component-items/{id}/status:
put:
consumes:
- application/json
description: Change the status of a component item. Supports partial quantity
change with automatic split/merge logic. A status history record is created
automatically.
parameters:
- description: Component item ID
in: path
name: id
required: true
type: integer
- description: Status change request body
in: body
name: body
required: true
schema:
$ref: '#/definitions/requests.UpdateComponentItemStatusRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
$ref: '#/definitions/responses.UpdateComponentItemStatusResponse'
type: object
"400":
description: Validation error (e.g., changed_quantity > quantity, status
unchanged)
schema:
$ref: '#/definitions/response.ErrorResponse'
"404":
description: Component item not found
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Change component item status
tags:
- component-item
/api/v1/component-items/find/{componentId}:
get:
consumes:
- application/json
description: Retrieve component items with full location details (container,
shelf, cabinet, room, warehouse) for a given component ID
parameters:
- description: Component ID
in: path
name: componentId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
items:
$ref: '#/definitions/models.FindComponentItemResult'
type: array
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Find component items by component ID
tags:
- component-item
/api/v1/component-types:
get:
consumes:

View File

@@ -0,0 +1,56 @@
package mapper
import (
"encoding/json"
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
)
func ToDomainComponentItem(r db.ComponentItem) *models.ComponentItem {
return &models.ComponentItem{
ID: r.ID,
ComponentID: r.ComponentID,
ContainerID: r.ContainerID,
Quantity: r.Quantity,
Status: string(r.Status),
Metadata: json.RawMessage(r.Metadata),
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
}
}
func ToModelComponentItem(r *models.ComponentItem) *db.CreateComponentItemParams {
return &db.CreateComponentItemParams{
ComponentID: r.ComponentID,
ContainerID: r.ContainerID,
Quantity: r.Quantity,
Status: db.ComponentItemStatusEnum(r.Status),
Metadata: []byte(r.Metadata),
CreatedAt: r.CreatedAt,
}
}
func ToUpdateModelComponentItem(r *models.ComponentItem) *db.UpdateComponentItemParams {
return &db.UpdateComponentItemParams{
ComponentID: r.ComponentID,
ContainerID: r.ContainerID,
Metadata: []byte(r.Metadata),
UpdatedAt: r.UpdatedAt,
ID: r.ID,
}
}
func ToDomainFindComponentItem(r db.FindComponentItemRow) *models.FindComponentItemResult {
return &models.FindComponentItemResult{
ComponentName: r.ComponentName,
TypeName: r.TypeName,
Quantity: r.Quantity,
Status: string(r.Status),
ContainerName: r.ContainerName,
ContainerType: string(r.ContainerType),
ShelfName: r.ShelfName,
CabinetName: r.CabinetName,
RoomName: r.RoomName,
WarehouseName: r.WarehouseName,
}
}

View File

@@ -0,0 +1,45 @@
package mapper
import (
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"github.com/jackc/pgx/v5/pgtype"
)
func ToDomainComponentStatusHistory(r db.ComponentStatusHistory) *models.ComponentStatusHistory {
return &models.ComponentStatusHistory{
ID: r.ID,
ComponentItemID: r.ComponentItemID,
OldStatus: string(r.OldStatus.ComponentItemStatusEnum),
NewStatus: string(r.NewStatus),
ChangedQuantity: r.ChangedQuantity.Int32,
Note: r.Note.String,
ChangedBy: r.ChangedBy.String,
ChangedAt: r.ChangedAt,
}
}
func ToModelComponentStatusHistory(r *models.ComponentStatusHistory) *db.CreateComponentStatusHistoryParams {
return &db.CreateComponentStatusHistoryParams{
ComponentItemID: r.ComponentItemID,
OldStatus: db.NullComponentItemStatusEnum{
ComponentItemStatusEnum: db.ComponentItemStatusEnum(r.OldStatus),
Valid: r.OldStatus != "",
},
NewStatus: db.ComponentItemStatusEnum(r.NewStatus),
ChangedQuantity: pgtype.Int4{
Int32: r.ChangedQuantity,
Valid: r.ChangedQuantity != 0,
},
Note: pgtype.Text{
String: r.Note,
Valid: r.Note != "",
},
ChangedBy: pgtype.Text{
String: r.ChangedBy,
Valid: r.ChangedBy != "",
},
ChangedAt: r.ChangedAt,
}
}

View File

@@ -0,0 +1,42 @@
package models
import (
"encoding/json"
"time"
)
type ComponentItem struct {
ID int64 `json:"id"`
ComponentID int64 `json:"componentId"`
ContainerID int64 `json:"containerId"`
Quantity int32 `json:"quantity"`
Status string `json:"status"`
Metadata json.RawMessage `json:"metadata"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// UpdateStatusResult holds the results of a component item status change operation.
// Different fields are populated depending on the case:
// - Case 1 (change all): only ComponentItem and StatusHistory are set
// - Case 2 (split): NewComponentItemID is also set
// - Case 3 (merge): MergedComponentItemID is also set
type UpdateStatusResult struct {
ComponentItem ComponentItem
StatusHistory ComponentStatusHistory
NewComponentItemID *int64
MergedComponentItemID *int64
}
type FindComponentItemResult struct {
ComponentName string `json:"componentName"`
TypeName string `json:"typeName"`
Quantity int32 `json:"quantity"`
Status string `json:"status"`
ContainerName string `json:"containerName"`
ContainerType string `json:"containerType"`
ShelfName string `json:"shelfName"`
CabinetName string `json:"cabinetName"`
RoomName string `json:"roomName"`
WarehouseName string `json:"warehouseName"`
}

View File

@@ -0,0 +1,14 @@
package models
import "time"
type ComponentStatusHistory struct {
ID int64 `json:"id"`
ComponentItemID int64 `json:"componentItemId"`
OldStatus string `json:"oldStatus"`
NewStatus string `json:"newStatus"`
ChangedQuantity int32 `json:"changedQuantity"`
Note string `json:"note"`
ChangedBy string `json:"changedBy"`
ChangedAt time.Time `json:"changedAt"`
}

View File

@@ -0,0 +1,24 @@
package requests
import "encoding/json"
type CreateComponentItemRequest struct {
ComponentID int64 `json:"componentId" binding:"required"`
ContainerID int64 `json:"containerId" binding:"required"`
Quantity int32 `json:"quantity" binding:"required"`
Status string `json:"status" binding:"required"`
Metadata json.RawMessage `json:"metadata"`
}
type UpdateComponentItemRequest struct {
ComponentID *int64 `json:"componentId"`
ContainerID *int64 `json:"containerId"`
Metadata json.RawMessage `json:"metadata"`
}
// UpdateComponentItemStatusRequest represents the request body for changing the status of a component item.
type UpdateComponentItemStatusRequest struct {
Status string `json:"status" binding:"required,oneof=normal damaged long_unused expired pending_inspection"`
ChangedQuantity *int32 `json:"changedQuantity"`
Note string `json:"note"`
}

View File

@@ -0,0 +1,8 @@
package requests
type CreateComponentStatusHistoryRequest struct {
OldStatus string `json:"oldStatus"`
NewStatus string `json:"newStatus" binding:"required"`
ChangedQuantity *int32 `json:"changedQuantity"`
Note string `json:"note"`
}

View File

@@ -0,0 +1,28 @@
package responses
import "encoding/json"
type CreateComponentItemResponse struct {
ID int64 `json:"id"`
}
type UpdateComponentItemResponse struct {
ID int64 `json:"id"`
ComponentID int64 `json:"componentId"`
ContainerID int64 `json:"containerId"`
Quantity int32 `json:"quantity"`
Status string `json:"status"`
Metadata json.RawMessage `json:"metadata"`
}
// UpdateComponentItemStatusResponse represents the response for a status change operation.
// Different fields are populated depending on the case (change all, split, merge).
type UpdateComponentItemStatusResponse struct {
ID int64 `json:"id"`
OldStatus string `json:"oldStatus"`
NewStatus string `json:"newStatus"`
ChangedQuantity int32 `json:"changedQuantity"`
HistoryID int64 `json:"historyId"`
NewComponentItemID *int64 `json:"newComponentItemId,omitempty"`
MergedComponentItemID *int64 `json:"mergedComponentItemId,omitempty"`
}

View File

@@ -0,0 +1,5 @@
package responses
type CreateComponentStatusHistoryResponse struct {
ID int64 `json:"id"`
}

View File

@@ -0,0 +1,238 @@
package repositories
import (
"context"
"errors"
"fmt"
"time"
"wm-backend/global"
"wm-backend/internal/mapper"
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/rs/zerolog/log"
)
func CreateComponentItem(ctx context.Context, queries *db.Queries, body models.ComponentItem) (models.ComponentItem, error) {
result, err := queries.CreateComponentItem(ctx, *mapper.ToModelComponentItem(&body))
if err != nil {
log.Error().Err(err).Msg("Failed to create component item")
return models.ComponentItem{}, err
}
return *mapper.ToDomainComponentItem(result), nil
}
func GetComponentItemByID(ctx context.Context, queries *db.Queries, id int64) (models.ComponentItem, error) {
result, err := queries.GetComponentItemByID(ctx, id)
if err != nil {
return models.ComponentItem{}, err
}
return *mapper.ToDomainComponentItem(result), nil
}
func ListComponentItems(ctx context.Context, queries *db.Queries) ([]models.ComponentItem, error) {
results, err := queries.ListComponentItems(ctx)
if err != nil {
return nil, err
}
var items []models.ComponentItem
for _, r := range results {
items = append(items, *mapper.ToDomainComponentItem(r))
}
return items, nil
}
func UpdateComponentItem(ctx context.Context, queries *db.Queries, body models.ComponentItem) (models.ComponentItem, error) {
result, err := queries.UpdateComponentItem(ctx, *mapper.ToUpdateModelComponentItem(&body))
if err != nil {
return models.ComponentItem{}, err
}
return *mapper.ToDomainComponentItem(result), nil
}
func DeleteComponentItem(ctx context.Context, queries *db.Queries, id int64) (int64, error) {
rowsAffected, err := queries.DeleteComponentItem(ctx, id)
if err != nil {
return rowsAffected, err
}
return rowsAffected, nil
}
func FindComponentItems(ctx context.Context, queries *db.Queries, componentID int64) ([]models.FindComponentItemResult, error) {
results, err := queries.FindComponentItem(ctx, componentID)
if err != nil {
return nil, err
}
var items []models.FindComponentItemResult
for _, r := range results {
items = append(items, *mapper.ToDomainFindComponentItem(r))
}
return items, nil
}
// UpdateComponentItemStatus changes the status of a component item within a transaction.
// It handles three cases:
// - Case 1: changedQuantity is nil or >= quantity → change status of entire record
// - Case 2: changedQuantity < quantity, no existing record with target status → split into 2 records
// - Case 3: changedQuantity < quantity, existing record with target status → merge quantities
//
// Returns UpdateStatusResult with case-specific fields populated.
func UpdateComponentItemStatus(ctx context.Context, dbPool *pgxpool.Pool, id int64, newStatus db.ComponentItemStatusEnum,
changedQuantity *int32,
note string,
changedBy string) (result models.UpdateStatusResult, err error) {
// 1. Begin transaction
tx, err := dbPool.Begin(ctx)
if err != nil {
return models.UpdateStatusResult{}, fmt.Errorf("begin tx: %w", err)
}
defer func() {
if err != nil {
tx.Rollback(ctx)
}
}()
txQueries := global.Queries.WithTx(tx)
// 2. Get existing item
existingItem, err := txQueries.GetComponentItemByID(ctx, id)
if err != nil {
return models.UpdateStatusResult{}, fmt.Errorf("item not found: %w", err)
}
// 3. Check status unchanged
if existingItem.Status == newStatus {
tx.Rollback(ctx)
return models.UpdateStatusResult{}, fmt.Errorf("status unchanged")
}
// 4. Determine changed quantity (nil = change all)
var changedQty int32
if changedQuantity == nil {
changedQty = existingItem.Quantity
} else {
changedQty = *changedQuantity
}
// 5. Validate changed quantity
if changedQty <= 0 {
return models.UpdateStatusResult{}, fmt.Errorf("changed_quantity must be positive, got %d", changedQty)
}
if changedQty > existingItem.Quantity {
return models.UpdateStatusResult{}, fmt.Errorf("changed_quantity (%d) exceeds current quantity (%d)", changedQty, existingItem.Quantity)
}
// 6. Branch by case
if changedQty >= existingItem.Quantity {
// ── Case 1: Change status of entire record ──
updatedItem, err := txQueries.UpdateComponentItemStatus(ctx, db.UpdateComponentItemStatusParams{
ID: id,
Status: newStatus,
UpdatedAt: time.Now(),
})
if err != nil {
return models.UpdateStatusResult{}, fmt.Errorf("update status: %w", err)
}
result.ComponentItem = *mapper.ToDomainComponentItem(updatedItem)
} else {
// ── Case 2 & 3: Partial change ──
// 6a. Reduce quantity on original record (or delete if qty becomes 0)
newQty := existingItem.Quantity - changedQty
if newQty == 0 {
_, err = txQueries.DeleteComponentItem(ctx, id)
if err != nil {
return models.UpdateStatusResult{}, fmt.Errorf("delete zero-qty record: %w", err)
}
} else {
_, err = txQueries.UpdateComponentItemQuantity(ctx, db.UpdateComponentItemQuantityParams{
ID: id,
Quantity: newQty,
UpdatedAt: time.Now(),
})
if err != nil {
return models.UpdateStatusResult{}, fmt.Errorf("update quantity: %w", err)
}
}
// 6b. Find existing record with same (component_id, container_id, new_status)
existingNewStatus, findErr := txQueries.GetComponentItemByComponentContainerStatus(ctx,
db.GetComponentItemByComponentContainerStatusParams{
ComponentID: existingItem.ComponentID,
ContainerID: existingItem.ContainerID,
Status: newStatus,
})
if findErr != nil {
if errors.Is(findErr, pgx.ErrNoRows) {
// ── Case 2: No existing record → create new one ──
newItem, createErr := txQueries.CreateComponentItem(ctx, db.CreateComponentItemParams{
ComponentID: existingItem.ComponentID,
ContainerID: existingItem.ContainerID,
Quantity: changedQty,
Status: newStatus,
Metadata: existingItem.Metadata,
CreatedAt: time.Now(),
})
if createErr != nil {
return models.UpdateStatusResult{}, fmt.Errorf("create new item: %w", createErr)
}
newID := newItem.ID
result.NewComponentItemID = &newID
} else {
return models.UpdateStatusResult{}, fmt.Errorf("find existing new status: %w", findErr)
}
} else {
// ── Case 3: Existing record → merge quantities ──
mergedQty := existingNewStatus.Quantity + changedQty
_, err = txQueries.UpdateComponentItemQuantity(ctx, db.UpdateComponentItemQuantityParams{
ID: existingNewStatus.ID,
Quantity: mergedQty,
UpdatedAt: time.Now(),
})
if err != nil {
return models.UpdateStatusResult{}, fmt.Errorf("merge quantity: %w", err)
}
mergedID := existingNewStatus.ID
result.MergedComponentItemID = &mergedID
}
}
// 7. Insert status history record
history, err := txQueries.CreateComponentStatusHistory(ctx, db.CreateComponentStatusHistoryParams{
ComponentItemID: id,
OldStatus: db.NullComponentItemStatusEnum{
ComponentItemStatusEnum: existingItem.Status,
Valid: true,
},
NewStatus: newStatus,
ChangedQuantity: pgtype.Int4{
Int32: changedQty,
Valid: true,
},
Note: pgtype.Text{
String: note,
Valid: note != "",
},
ChangedBy: pgtype.Text{
String: changedBy,
Valid: true,
},
ChangedAt: time.Now(),
})
if err != nil {
return models.UpdateStatusResult{}, fmt.Errorf("insert history: %w", err)
}
result.StatusHistory = *mapper.ToDomainComponentStatusHistory(history)
// 8. Commit transaction
if err = tx.Commit(ctx); err != nil {
return models.UpdateStatusResult{}, fmt.Errorf("commit: %w", err)
}
return result, nil
}

View File

@@ -0,0 +1,518 @@
package repositories
import (
"context"
"fmt"
"os"
"testing"
"time"
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"github.com/jackc/pgx/v5/pgxpool"
)
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
// testDB returns a connection pool for tests. Set WM_TEST_DB_URL to override
// the default connection string. If the DB is not available the test is skipped.
func testDB(t *testing.T) *pgxpool.Pool {
t.Helper()
dsn := os.Getenv("WM_TEST_DB_URL")
if dsn == "" {
dsn = "postgres://root:Smatec2026@localhost:5432/warehouse_management?sslmode=disable"
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Skipf("skipping integration test: cannot connect to DB: %v", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
t.Skipf("skipping integration test: cannot ping DB: %v", err)
}
return pool
}
// seedComponentItem inserts all required parent records and returns
// the created component_item ID, plus its component_id and container_id.
func seedComponentItem(t *testing.T, pool *pgxpool.Pool, q *db.Queries, quantity int32, status db.ComponentItemStatusEnum) (itemID, componentID, containerID int64) {
t.Helper()
ctx := context.Background()
// warehouse
w, err := q.CreateWarehouse(ctx, db.CreateWarehouseParams{
Name: fmt.Sprintf("test-wh-%d", time.Now().UnixNano()),
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed warehouse: %v", err)
}
// room
r, err := q.CreateRoom(ctx, db.CreateRoomParams{
WarehouseID: w.ID,
Name: fmt.Sprintf("test-room-%d", time.Now().UnixNano()),
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed room: %v", err)
}
// cabinet
cb, err := q.CreateCabinet(ctx, db.CreateCabinetParams{
RoomID: r.ID,
Name: fmt.Sprintf("test-cab-%d", time.Now().UnixNano()),
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed cabinet: %v", err)
}
// shelf
s, err := q.CreateShelve(ctx, db.CreateShelveParams{
CabinetID: cb.ID,
Name: fmt.Sprintf("test-shelf-%d", time.Now().UnixNano()),
LevelIndex: 1,
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed shelf: %v", err)
}
// container
cn, err := q.CreateContainer(ctx, db.CreateContainerParams{
ShelfID: s.ID,
Name: fmt.Sprintf("test-cont-%d", time.Now().UnixNano()),
ContainerType: db.ContainerTypeEnumOther,
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed container: %v", err)
}
containerID = cn.ID
// component_type
ct, err := q.CreateComponentType(ctx, db.CreateComponentTypeParams{
Name: fmt.Sprintf("test-ctype-%d", time.Now().UnixNano()),
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed component_type: %v", err)
}
// component
c, err := q.CreateComponent(ctx, db.CreateComponentParams{
ComponentTypeID: ct.ID,
Name: fmt.Sprintf("test-comp-%d", time.Now().UnixNano()),
Unit: "cái",
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed component: %v", err)
}
componentID = c.ID
// component_item
ci, err := q.CreateComponentItem(ctx, db.CreateComponentItemParams{
ComponentID: componentID,
ContainerID: containerID,
Quantity: quantity,
Status: status,
Metadata: []byte("{}"),
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed component_item: %v", err)
}
return ci.ID, componentID, containerID
}
// cleanupSeeded removes test data in reverse dependency order.
func cleanupSeeded(t *testing.T, pool *pgxpool.Pool, itemID, componentID, containerID int64) {
t.Helper()
ctx := context.Background()
q := db.New(pool)
// Delete history rows referencing this item
_, _ = pool.Exec(ctx, "DELETE FROM component_status_history WHERE component_item_id = $1", itemID)
// Delete component_items (including those possibly created by the test)
_, _ = pool.Exec(ctx, "DELETE FROM component_items WHERE component_id = $1", componentID)
_, _ = q.DeleteComponent(ctx, componentID)
// Delete component_type we need to find it from the component
_, _ = pool.Exec(ctx, "DELETE FROM component_types WHERE id = (SELECT component_type_id FROM components WHERE id = $1)", componentID)
_, _ = q.DeleteContainer(ctx, containerID)
// Delete container's hierarchy
_, _ = pool.Exec(ctx, "DELETE FROM shelves WHERE id = (SELECT shelf_id FROM containers WHERE id = $1)", containerID)
_, _ = pool.Exec(ctx, "DELETE FROM cabinets WHERE id IN (SELECT cabinet_id FROM shelves WHERE id = (SELECT shelf_id FROM containers WHERE id = $1))", containerID)
_, _ = pool.Exec(ctx, "DELETE FROM rooms WHERE id IN (SELECT room_id FROM cabinets WHERE id IN (SELECT cabinet_id FROM shelves WHERE id = (SELECT shelf_id FROM containers WHERE id = $1)))", containerID)
_, _ = pool.Exec(ctx, "DELETE FROM warehouses WHERE id IN (SELECT warehouse_id FROM rooms WHERE id IN (SELECT room_id FROM cabinets WHERE id IN (SELECT cabinet_id FROM shelves WHERE id = (SELECT shelf_id FROM containers WHERE id = $1))))", containerID)
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
func TestUpdateComponentItemStatus_Case1_ChangeAll_NilQuantity(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 20, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
result, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, nil, "test note", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify result
if result.StatusHistory.OldStatus != "normal" {
t.Errorf("expected old status 'normal', got '%s'", result.StatusHistory.OldStatus)
}
if result.StatusHistory.NewStatus != "damaged" {
t.Errorf("expected new status 'damaged', got '%s'", result.StatusHistory.NewStatus)
}
if result.StatusHistory.ChangedQuantity != 20 {
t.Errorf("expected changed quantity 20, got %d", result.StatusHistory.ChangedQuantity)
}
if result.StatusHistory.ChangedBy != "system" {
t.Errorf("expected changed_by 'system', got '%s'", result.StatusHistory.ChangedBy)
}
if result.NewComponentItemID != nil {
t.Error("expected NewComponentItemID to be nil for case 1")
}
if result.MergedComponentItemID != nil {
t.Error("expected MergedComponentItemID to be nil for case 1")
}
// Verify DB state
updated, err := q.GetComponentItemByID(context.Background(), itemID)
if err != nil {
t.Fatalf("get updated item: %v", err)
}
if updated.Status != db.ComponentItemStatusEnumDamaged {
t.Errorf("expected status 'damaged', got '%s'", updated.Status)
}
if updated.Quantity != 20 {
t.Errorf("expected quantity 20, got %d", updated.Quantity)
}
}
func TestUpdateComponentItemStatus_Case1_ChangeAll_ExplicitQuantity(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 15, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
changedQty := int32(15)
result, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.StatusHistory.ChangedQuantity != 15 {
t.Errorf("expected changed quantity 15, got %d", result.StatusHistory.ChangedQuantity)
}
}
func TestUpdateComponentItemStatus_Case2_Split_NoExistingTarget(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 20, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
changedQty := int32(5)
result, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "split test", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify result
if result.NewComponentItemID == nil {
t.Fatal("expected NewComponentItemID to be set for case 2")
}
if result.MergedComponentItemID != nil {
t.Error("expected MergedComponentItemID to be nil for case 2")
}
// Original record should have reduced quantity
orig, err := q.GetComponentItemByID(context.Background(), itemID)
if err != nil {
t.Fatalf("get original item: %v", err)
}
if orig.Quantity != 15 {
t.Errorf("expected original quantity 15, got %d", orig.Quantity)
}
if orig.Status != db.ComponentItemStatusEnumNormal {
t.Errorf("expected original status 'normal', got '%s'", orig.Status)
}
// New record should have the split quantity with target status
newItem, err := q.GetComponentItemByID(context.Background(), *result.NewComponentItemID)
if err != nil {
t.Fatalf("get new item: %v", err)
}
if newItem.Quantity != 5 {
t.Errorf("expected new item quantity 5, got %d", newItem.Quantity)
}
if newItem.Status != db.ComponentItemStatusEnumDamaged {
t.Errorf("expected new item status 'damaged', got '%s'", newItem.Status)
}
if newItem.ComponentID != orig.ComponentID {
t.Error("new item should have same component_id")
}
if newItem.ContainerID != orig.ContainerID {
t.Error("new item should have same container_id")
}
}
func TestUpdateComponentItemStatus_Case3_Merge_ExistingTarget(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 20, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
// Pre-create a damaged record to merge into
ctx := context.Background()
existingDamaged, err := q.CreateComponentItem(ctx, db.CreateComponentItemParams{
ComponentID: compID,
ContainerID: contID,
Quantity: 3,
Status: db.ComponentItemStatusEnumDamaged,
Metadata: []byte("{}"),
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("pre-create damaged record: %v", err)
}
changedQty := int32(5)
result, err := UpdateComponentItemStatus(ctx, pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "merge test", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify result
if result.NewComponentItemID != nil {
t.Error("expected NewComponentItemID to be nil for case 3")
}
if result.MergedComponentItemID == nil {
t.Fatal("expected MergedComponentItemID to be set for case 3")
}
if *result.MergedComponentItemID != existingDamaged.ID {
t.Errorf("expected merged ID %d, got %d", existingDamaged.ID, *result.MergedComponentItemID)
}
// Original record should have reduced quantity
orig, err := q.GetComponentItemByID(ctx, itemID)
if err != nil {
t.Fatalf("get original item: %v", err)
}
if orig.Quantity != 15 {
t.Errorf("expected original quantity 15, got %d", orig.Quantity)
}
// Merged record should have increased quantity
merged, err := q.GetComponentItemByID(ctx, existingDamaged.ID)
if err != nil {
t.Fatalf("get merged item: %v", err)
}
if merged.Quantity != 8 { // 3 + 5
t.Errorf("expected merged quantity 8, got %d", merged.Quantity)
}
if merged.Status != db.ComponentItemStatusEnumDamaged {
t.Errorf("expected merged status 'damaged', got '%s'", merged.Status)
}
}
func TestUpdateComponentItemStatus_Edge_StatusUnchanged(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 10, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
_, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumNormal, nil, "", "system")
if err == nil {
t.Fatal("expected error for status unchanged")
}
if err.Error() != "status unchanged" {
t.Errorf("expected 'status unchanged', got '%s'", err.Error())
}
}
func TestUpdateComponentItemStatus_Edge_ChangedQuantityZero(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 10, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
changedQty := int32(0)
_, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "", "system")
if err == nil {
t.Fatal("expected error for zero changed_quantity")
}
}
func TestUpdateComponentItemStatus_Edge_ChangedQuantityNegative(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 10, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
changedQty := int32(-5)
_, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "", "system")
if err == nil {
t.Fatal("expected error for negative changed_quantity")
}
}
func TestUpdateComponentItemStatus_Edge_ChangedQuantityExceedsQuantity(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 10, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
changedQty := int32(25)
_, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "", "system")
if err == nil {
t.Fatal("expected error for changed_quantity > quantity")
}
}
func TestUpdateComponentItemStatus_Edge_ItemNotFound(t *testing.T) {
pool := testDB(t)
defer pool.Close()
_, err := UpdateComponentItemStatus(context.Background(), pool, 999999,
db.ComponentItemStatusEnumDamaged, nil, "", "system")
if err == nil {
t.Fatal("expected error for non-existent item")
}
}
func TestUpdateComponentItemStatus_Edge_QuantityBecomesZero(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 5, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
// Change all 5 to damaged → original record quantity becomes 0 → should be deleted
changedQty := int32(5)
_, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "zero qty test", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// The original record should be deleted (quantity became 0)
_, err = q.GetComponentItemByID(context.Background(), itemID)
if err == nil {
t.Error("expected original item to be deleted when quantity becomes 0")
}
}
func TestUpdateComponentItemStatus_ChangeToPendingInspection(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 30, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
result, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumPendingInspection, nil, "pending inspection check", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.StatusHistory.NewStatus != "pending_inspection" {
t.Errorf("expected new status 'pending_inspection', got '%s'", result.StatusHistory.NewStatus)
}
updated, _ := q.GetComponentItemByID(context.Background(), itemID)
if updated.Status != db.ComponentItemStatusEnumPendingInspection {
t.Errorf("expected status 'pending_inspection', got '%s'", updated.Status)
}
}
func TestUpdateComponentItemStatus_HistoryRecordIntegrity(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 10, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
result, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumLongUnused, nil, "history integrity test note", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
history := result.StatusHistory
if history.ComponentItemID != itemID {
t.Errorf("expected component_item_id %d, got %d", itemID, history.ComponentItemID)
}
if history.OldStatus != "normal" {
t.Errorf("expected old_status 'normal', got '%s'", history.OldStatus)
}
if history.NewStatus != "long_unused" {
t.Errorf("expected new_status 'long_unused', got '%s'", history.NewStatus)
}
if history.Note != "history integrity test note" {
t.Errorf("expected note 'history integrity test note', got '%s'", history.Note)
}
if history.ChangedBy != "system" {
t.Errorf("expected changed_by 'system', got '%s'", history.ChangedBy)
}
if history.ChangedQuantity != 10 {
t.Errorf("expected changed_quantity 10, got %d", history.ChangedQuantity)
}
if history.ID == 0 {
t.Error("expected history ID to be non-zero")
}
}
// TestUpdateComponentItemStatus_ResultType ensures the UpdateStatusResult domain
// model is populated correctly.
func TestUpdateComponentItemStatus_ResultType(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 10, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
result, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, nil, "", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify UpdateStatusResult fields
var _ models.UpdateStatusResult = result // compile-time check
if result.ComponentItem.ID == 0 && result.NewComponentItemID == nil && result.MergedComponentItemID == nil {
// Case 1 doesn't set ComponentItem either in current impl — that's fine
// ComponentItem is only set in Case 1 (change all). Let's verify.
// Actually looking at the repo code, ComponentItem is only set in Case 1.
// For Case 2/3 it's not set because the original item is only partially changed.
// This is intentional.
// For Case 1, ComponentItem should be set
if result.ComponentItem.ID == 0 {
t.Error("expected ComponentItem.ID to be set for Case 1 (change all)")
}
}
if result.StatusHistory.ID == 0 {
t.Error("expected StatusHistory.ID to be non-zero")
}
}

View File

@@ -100,6 +100,17 @@ func NewRouter() *gin.Engine {
componentCode.PUT("/:id", utils.AsyncHandler(services.ComponentCodeUpdate))
componentCode.DELETE("/:id", utils.AsyncHandler(services.ComponentCodeDelete))
}
componentItem := v1.Group(constants.API_GROUP_COMPONENT_ITEM)
{
componentItem.GET("", utils.AsyncHandler(services.ComponentItemList))
componentItem.GET("/find/:componentId", utils.AsyncHandler(services.ComponentItemFind))
componentItem.GET("/:id", utils.AsyncHandler(services.ComponentItemGetByID))
componentItem.POST("", utils.AsyncHandler(services.ComponentItemCreate))
componentItem.PUT("/:id/status", utils.AsyncHandler(services.ComponentItemUpdateStatus))
componentItem.PUT("/:id", utils.AsyncHandler(services.ComponentItemUpdate))
componentItem.DELETE("/:id", utils.AsyncHandler(services.ComponentItemDelete))
}
}
r.GET(constants.API_PATH_PING, services.PingHandler)

View File

@@ -0,0 +1,290 @@
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"
db "wm-backend/sqlc_gen"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)
// ComponentItemCreate creates a new component item.
// It validates the request body and creates the component item in the database.
//
// @Summary Create a new component item
// @Description Create a new component item with the provided details
// @Tags component-item
// @Accept json
// @Produce json
// @Param body body requests.CreateComponentItemRequest true "Component item request body"
// @Success 201 {object} response.SuccessResponse{data=responses.CreateComponentItemResponse}
// @Failure 400 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /api/v1/component-items [post]
func ComponentItemCreate(c *gin.Context) error {
requestBody := requests.CreateComponentItemRequest{}
if helper.IsShouldBindJSON(c, &requestBody) {
return nil
}
componentItemModel := &models.ComponentItem{
ComponentID: requestBody.ComponentID,
ContainerID: requestBody.ContainerID,
Quantity: requestBody.Quantity,
Status: requestBody.Status,
Metadata: requestBody.Metadata,
CreatedAt: time.Now(),
}
componentItem, err := repositories.CreateComponentItem(c.Request.Context(), global.Queries, *componentItemModel)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError, "Failed to create component item")
return nil
}
response.Created(c, "Component item created successfully", &responses.CreateComponentItemResponse{
ID: componentItem.ID,
})
return nil
}
// ComponentItemGetByID retrieves a single component item by its ID.
//
// @Summary Get component item by ID
// @Description Retrieve a single component item using its unique identifier
// @Tags component-item
// @Accept json
// @Produce json
// @Param id path int true "Component item ID"
// @Success 200 {object} response.SuccessResponse{data=models.ComponentItem}
// @Failure 400 {object} response.ErrorResponse
// @Failure 404 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /api/v1/component-items/{id} [get]
func ComponentItemGetByID(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
}
componentItem, err := repositories.GetComponentItemByID(c.Request.Context(), global.Queries, id)
if err != nil {
response.NotFoundError(c, http.StatusNotFound, "Component item not found")
return nil
}
response.Ok(c, "Success", componentItem)
return nil
}
// ComponentItemList retrieves all component items.
//
// @Summary List all component items
// @Description Retrieve a list of all component items ordered by creation date
// @Tags component-item
// @Accept json
// @Produce json
// @Success 200 {object} response.SuccessResponse{data=[]models.ComponentItem}
// @Failure 500 {object} response.ErrorResponse
// @Router /api/v1/component-items [get]
func ComponentItemList(c *gin.Context) error {
componentItems, err := repositories.ListComponentItems(c.Request.Context(), global.Queries)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError, "Failed to list component items")
return nil
}
response.Ok(c, "Success", componentItems)
return nil
}
// ComponentItemUpdate updates an existing component item by its ID.
// It validates the request body, fetches the existing record,
// merges non-empty fields from the request, and updates the component item in the database.
//
// @Summary Update component item
// @Description Update an existing component item by its ID. Only non-empty fields will be updated.
// @Tags component-item
// @Accept json
// @Produce json
// @Param id path int true "Component item ID"
// @Param body body requests.UpdateComponentItemRequest true "Component item request body"
// @Success 200 {object} response.SuccessResponse{data=responses.UpdateComponentItemResponse}
// @Failure 400 {object} response.ErrorResponse
// @Failure 404 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /api/v1/component-items/{id} [put]
func ComponentItemUpdate(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.UpdateComponentItemRequest{}
if helper.IsShouldBindJSON(c, &requestBody) {
return nil
}
existing, err := repositories.GetComponentItemByID(c.Request.Context(), global.Queries, id)
if err != nil {
response.NotFoundError(c, http.StatusNotFound, "Component item not found")
return nil
}
if requestBody.ComponentID != nil {
existing.ComponentID = *requestBody.ComponentID
}
if requestBody.ContainerID != nil {
existing.ContainerID = *requestBody.ContainerID
}
if len(requestBody.Metadata) > 0 {
existing.Metadata = requestBody.Metadata
}
existing.UpdatedAt = time.Now()
componentItem, err := repositories.UpdateComponentItem(c.Request.Context(), global.Queries, existing)
if err != nil {
log.Error().Err(err).Msgf("Failed to update component item with ID: %d", id)
response.InternalServerError(c, http.StatusInternalServerError, "Failed to update component item")
return nil
}
response.Ok(c, "Component item updated successfully", &responses.UpdateComponentItemResponse{
ID: componentItem.ID,
ComponentID: componentItem.ComponentID,
ContainerID: componentItem.ContainerID,
Quantity: componentItem.Quantity,
Status: componentItem.Status,
Metadata: componentItem.Metadata,
})
return nil
}
// ComponentItemDelete deletes a component item by its ID.
//
// @Summary Delete component item
// @Description Delete a component item by its unique identifier
// @Tags component-item
// @Accept json
// @Produce json
// @Param id path int true "Component item ID"
// @Success 200 {object} response.SuccessResponse
// @Failure 400 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /api/v1/component-items/{id} [delete]
func ComponentItemDelete(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
}
rowsAffected, err := repositories.DeleteComponentItem(c.Request.Context(), global.Queries, id)
if err != nil {
log.Error().Err(err).Msgf("Failed to delete component item with ID: %d", id)
response.InternalServerError(c, http.StatusInternalServerError, "Failed to delete component item")
return nil
}
if rowsAffected == 0 {
response.NotFoundError(c, http.StatusNotFound, "Component item not found")
return nil
}
response.Ok(c, "Delete Success", nil)
return nil
}
// ComponentItemFind retrieves component items by component ID with full location info.
//
// @Summary Find component items by component ID
// @Description Retrieve component items with full location details (container, shelf, cabinet, room, warehouse) for a given component ID
// @Tags component-item
// @Accept json
// @Produce json
// @Param componentId path int true "Component ID"
// @Success 200 {object} response.SuccessResponse{data=[]models.FindComponentItemResult}
// @Failure 400 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /api/v1/component-items/find/{componentId} [get]
func ComponentItemFind(c *gin.Context) error {
componentID, err := strconv.ParseInt(c.Param("componentId"), 10, 64)
if err != nil {
response.BadRequestError(c, http.StatusBadRequest, "Invalid component ID")
return nil
}
items, err := repositories.FindComponentItems(c.Request.Context(), global.Queries, componentID)
if err != nil {
log.Error().Err(err).Msgf("Failed to find component items for component ID: %d", componentID)
response.InternalServerError(c, http.StatusInternalServerError, "Failed to find component items")
return nil
}
response.Ok(c, "Success", items)
return nil
}
// ComponentItemUpdateStatus changes the status of a component item.
// It handles three cases within a single transaction:
// - Case 1 (change all): changedQuantity is nil or equals quantity → updates status of entire record
// - Case 2 (split): changedQuantity < quantity, no existing record with target status → reduces old record, creates new record
// - Case 3 (merge): changedQuantity < quantity, existing record with target status → reduces old record, merges into existing
//
// A history record is always created. If quantity becomes 0 after the split, the old record is deleted.
//
// @Summary Change component item status
// @Description Change the status of a component item. Supports partial quantity change with automatic split/merge logic. A status history record is created automatically.
// @Tags component-item
// @Accept json
// @Produce json
// @Param id path int true "Component item ID"
// @Param body body requests.UpdateComponentItemStatusRequest true "Status change request body"
// @Success 200 {object} response.SuccessResponse{data=responses.UpdateComponentItemStatusResponse}
// @Failure 400 {object} response.ErrorResponse "Validation error (e.g., changed_quantity > quantity, status unchanged)"
// @Failure 404 {object} response.ErrorResponse "Component item not found"
// @Failure 500 {object} response.ErrorResponse "Internal server error"
// @Router /api/v1/component-items/{id}/status [put]
func ComponentItemUpdateStatus(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
}
var req requests.UpdateComponentItemStatusRequest
if helper.IsShouldBindJSON(c, &req) {
return nil
}
result, err := repositories.UpdateComponentItemStatus(
c.Request.Context(),
global.DB,
id,
db.ComponentItemStatusEnum(req.Status),
req.ChangedQuantity,
req.Note,
"system",
)
if err != nil {
if err.Error() == "status unchanged" {
response.Ok(c, "Status unchanged", nil)
return nil
}
log.Error().Err(err).Msgf("Failed to update component item status for ID: %d", id)
response.BadRequestError(c, http.StatusBadRequest, err.Error())
return nil
}
// Build response based on case
resp := responses.UpdateComponentItemStatusResponse{
ID: id,
OldStatus: result.StatusHistory.OldStatus,
NewStatus: result.StatusHistory.NewStatus,
ChangedQuantity: result.StatusHistory.ChangedQuantity,
HistoryID: result.StatusHistory.ID,
}
if result.NewComponentItemID != nil {
resp.NewComponentItemID = result.NewComponentItemID
}
if result.MergedComponentItemID != nil {
resp.MergedComponentItemID = result.MergedComponentItemID
}
response.Ok(c, "Status updated successfully", resp)
return nil
}

View File

@@ -0,0 +1,320 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: component_item.sql
package db
import (
"context"
"time"
)
const createComponentItem = `-- name: CreateComponentItem :one
INSERT INTO component_items (component_id,container_id,quantity, status, metadata, created_at)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6
)
RETURNING id, component_id, container_id, quantity, status, metadata, created_at, updated_at
`
type CreateComponentItemParams struct {
ComponentID int64 `db:"component_id" json:"componentId"`
ContainerID int64 `db:"container_id" json:"containerId"`
Quantity int32 `db:"quantity" json:"quantity"`
Status ComponentItemStatusEnum `db:"status" json:"status"`
Metadata []byte `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
}
func (q *Queries) CreateComponentItem(ctx context.Context, arg CreateComponentItemParams) (ComponentItem, error) {
row := q.db.QueryRow(ctx, createComponentItem,
arg.ComponentID,
arg.ContainerID,
arg.Quantity,
arg.Status,
arg.Metadata,
arg.CreatedAt,
)
var i ComponentItem
err := row.Scan(
&i.ID,
&i.ComponentID,
&i.ContainerID,
&i.Quantity,
&i.Status,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteComponentItem = `-- name: DeleteComponentItem :execrows
DELETE FROM component_items
WHERE id = $1
`
func (q *Queries) DeleteComponentItem(ctx context.Context, id int64) (int64, error) {
result, err := q.db.Exec(ctx, deleteComponentItem, id)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const findComponentItem = `-- name: FindComponentItem :many
SELECT
c.name AS component_name,
ct.name AS type_name,
ci.quantity,
ci.status,
cn.name AS container_name,
cn.container_type,
s.name AS shelf_name,
cb.name AS cabinet_name,
r.name AS room_name,
w.name AS warehouse_name
FROM component_items ci
JOIN components c ON ci.component_id = c.id
JOIN component_types ct ON c.component_type_id = ct.id
JOIN containers cn ON ci.container_id = cn.id
JOIN shelves s ON cn.shelf_id = s.id
JOIN cabinets cb ON s.cabinet_id = cb.id
JOIN rooms r ON cb.room_id = r.id
JOIN warehouses w ON r.warehouse_id = w.id
WHERE ci.component_id = $1 AND ci.quantity > 0
`
type FindComponentItemRow struct {
ComponentName string `db:"component_name" json:"componentName"`
TypeName string `db:"type_name" json:"typeName"`
Quantity int32 `db:"quantity" json:"quantity"`
Status ComponentItemStatusEnum `db:"status" json:"status"`
ContainerName string `db:"container_name" json:"containerName"`
ContainerType ContainerTypeEnum `db:"container_type" json:"containerType"`
ShelfName string `db:"shelf_name" json:"shelfName"`
CabinetName string `db:"cabinet_name" json:"cabinetName"`
RoomName string `db:"room_name" json:"roomName"`
WarehouseName string `db:"warehouse_name" json:"warehouseName"`
}
func (q *Queries) FindComponentItem(ctx context.Context, componentid int64) ([]FindComponentItemRow, error) {
rows, err := q.db.Query(ctx, findComponentItem, componentid)
if err != nil {
return nil, err
}
defer rows.Close()
var items []FindComponentItemRow
for rows.Next() {
var i FindComponentItemRow
if err := rows.Scan(
&i.ComponentName,
&i.TypeName,
&i.Quantity,
&i.Status,
&i.ContainerName,
&i.ContainerType,
&i.ShelfName,
&i.CabinetName,
&i.RoomName,
&i.WarehouseName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getComponentItemByComponentContainerStatus = `-- name: GetComponentItemByComponentContainerStatus :one
SELECT id, component_id, container_id, quantity, status, metadata, created_at, updated_at FROM component_items
WHERE component_id = $1
AND container_id = $2
AND status = $3
`
type GetComponentItemByComponentContainerStatusParams struct {
ComponentID int64 `db:"component_id" json:"componentId"`
ContainerID int64 `db:"container_id" json:"containerId"`
Status ComponentItemStatusEnum `db:"status" json:"status"`
}
func (q *Queries) GetComponentItemByComponentContainerStatus(ctx context.Context, arg GetComponentItemByComponentContainerStatusParams) (ComponentItem, error) {
row := q.db.QueryRow(ctx, getComponentItemByComponentContainerStatus, arg.ComponentID, arg.ContainerID, arg.Status)
var i ComponentItem
err := row.Scan(
&i.ID,
&i.ComponentID,
&i.ContainerID,
&i.Quantity,
&i.Status,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getComponentItemByID = `-- name: GetComponentItemByID :one
SELECT id, component_id, container_id, quantity, status, metadata, created_at, updated_at FROM component_items
WHERE id = $1
`
func (q *Queries) GetComponentItemByID(ctx context.Context, id int64) (ComponentItem, error) {
row := q.db.QueryRow(ctx, getComponentItemByID, id)
var i ComponentItem
err := row.Scan(
&i.ID,
&i.ComponentID,
&i.ContainerID,
&i.Quantity,
&i.Status,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listComponentItems = `-- name: ListComponentItems :many
SELECT id, component_id, container_id, quantity, status, metadata, created_at, updated_at FROM component_items
ORDER BY created_at DESC
`
func (q *Queries) ListComponentItems(ctx context.Context) ([]ComponentItem, error) {
rows, err := q.db.Query(ctx, listComponentItems)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ComponentItem
for rows.Next() {
var i ComponentItem
if err := rows.Scan(
&i.ID,
&i.ComponentID,
&i.ContainerID,
&i.Quantity,
&i.Status,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateComponentItem = `-- name: UpdateComponentItem :one
UPDATE component_items
SET component_id = CASE WHEN $1 = '' THEN component_id ELSE $1 END,
container_id = CASE WHEN $2 = '' THEN container_id ELSE $2 END,
metadata = coalesce($3, metadata),
updated_at = $4
WHERE id = $5
RETURNING id, component_id, container_id, quantity, status, metadata, created_at, updated_at
`
type UpdateComponentItemParams struct {
ComponentID interface{} `db:"component_id" json:"componentId"`
ContainerID interface{} `db:"container_id" json:"containerId"`
Metadata []byte `db:"metadata" json:"metadata"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
ID int64 `db:"id" json:"id"`
}
func (q *Queries) UpdateComponentItem(ctx context.Context, arg UpdateComponentItemParams) (ComponentItem, error) {
row := q.db.QueryRow(ctx, updateComponentItem,
arg.ComponentID,
arg.ContainerID,
arg.Metadata,
arg.UpdatedAt,
arg.ID,
)
var i ComponentItem
err := row.Scan(
&i.ID,
&i.ComponentID,
&i.ContainerID,
&i.Quantity,
&i.Status,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const updateComponentItemQuantity = `-- name: UpdateComponentItemQuantity :one
UPDATE component_items
SET quantity = $1,
updated_at = $2
WHERE id = $3
RETURNING id, component_id, container_id, quantity, status, metadata, created_at, updated_at
`
type UpdateComponentItemQuantityParams struct {
Quantity int32 `db:"quantity" json:"quantity"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
ID int64 `db:"id" json:"id"`
}
func (q *Queries) UpdateComponentItemQuantity(ctx context.Context, arg UpdateComponentItemQuantityParams) (ComponentItem, error) {
row := q.db.QueryRow(ctx, updateComponentItemQuantity, arg.Quantity, arg.UpdatedAt, arg.ID)
var i ComponentItem
err := row.Scan(
&i.ID,
&i.ComponentID,
&i.ContainerID,
&i.Quantity,
&i.Status,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const updateComponentItemStatus = `-- name: UpdateComponentItemStatus :one
UPDATE component_items
SET status = $1,
updated_at = $2
WHERE id = $3
RETURNING id, component_id, container_id, quantity, status, metadata, created_at, updated_at
`
type UpdateComponentItemStatusParams struct {
Status ComponentItemStatusEnum `db:"status" json:"status"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
ID int64 `db:"id" json:"id"`
}
func (q *Queries) UpdateComponentItemStatus(ctx context.Context, arg UpdateComponentItemStatusParams) (ComponentItem, error) {
row := q.db.QueryRow(ctx, updateComponentItemStatus, arg.Status, arg.UpdatedAt, arg.ID)
var i ComponentItem
err := row.Scan(
&i.ID,
&i.ComponentID,
&i.ContainerID,
&i.Quantity,
&i.Status,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

View File

@@ -0,0 +1,64 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: component_status_history.sql
package db
import (
"context"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
const createComponentStatusHistory = `-- name: CreateComponentStatusHistory :one
INSERT INTO component_status_history (
component_item_id, old_status, new_status,
changed_quantity, note, changed_by, changed_at
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7
)
RETURNING id, component_item_id, old_status, new_status, changed_quantity, note, changed_by, changed_at
`
type CreateComponentStatusHistoryParams struct {
ComponentItemID int64 `db:"component_item_id" json:"componentItemId"`
OldStatus NullComponentItemStatusEnum `db:"old_status" json:"oldStatus"`
NewStatus ComponentItemStatusEnum `db:"new_status" json:"newStatus"`
ChangedQuantity pgtype.Int4 `db:"changed_quantity" json:"changedQuantity"`
Note pgtype.Text `db:"note" json:"note"`
ChangedBy pgtype.Text `db:"changed_by" json:"changedBy"`
ChangedAt time.Time `db:"changed_at" json:"changedAt"`
}
func (q *Queries) CreateComponentStatusHistory(ctx context.Context, arg CreateComponentStatusHistoryParams) (ComponentStatusHistory, error) {
row := q.db.QueryRow(ctx, createComponentStatusHistory,
arg.ComponentItemID,
arg.OldStatus,
arg.NewStatus,
arg.ChangedQuantity,
arg.Note,
arg.ChangedBy,
arg.ChangedAt,
)
var i ComponentStatusHistory
err := row.Scan(
&i.ID,
&i.ComponentItemID,
&i.OldStatus,
&i.NewStatus,
&i.ChangedQuantity,
&i.Note,
&i.ChangedBy,
&i.ChangedAt,
)
return i, err
}

View File

@@ -16,6 +16,8 @@ type Querier interface {
CreateCabinet(ctx context.Context, arg CreateCabinetParams) (Cabinet, error)
CreateComponent(ctx context.Context, arg CreateComponentParams) (Component, error)
CreateComponentCode(ctx context.Context, arg CreateComponentCodeParams) (ComponentCode, error)
CreateComponentItem(ctx context.Context, arg CreateComponentItemParams) (ComponentItem, error)
CreateComponentStatusHistory(ctx context.Context, arg CreateComponentStatusHistoryParams) (ComponentStatusHistory, error)
CreateComponentType(ctx context.Context, arg CreateComponentTypeParams) (ComponentType, error)
CreateContainer(ctx context.Context, arg CreateContainerParams) (Container, error)
CreateRole(ctx context.Context, arg CreateRoleParams) (Role, error)
@@ -26,15 +28,19 @@ type Querier interface {
DeleteCabinet(ctx context.Context, id int64) (int64, error)
DeleteComponent(ctx context.Context, id int64) (int64, error)
DeleteComponentCode(ctx context.Context, id int64) (int64, error)
DeleteComponentItem(ctx context.Context, id int64) (int64, error)
DeleteComponentType(ctx context.Context, id int64) (int64, error)
DeleteContainer(ctx context.Context, id int64) (int64, error)
DeleteRole(ctx context.Context, id uuid.UUID) (int64, error)
DeleteRoom(ctx context.Context, id int64) (int64, error)
DeleteShelve(ctx context.Context, id int64) (int64, error)
DeleteWarehouse(ctx context.Context, id int64) (int64, error)
FindComponentItem(ctx context.Context, componentid int64) ([]FindComponentItemRow, error)
GetCabinetByID(ctx context.Context, id int64) (Cabinet, error)
GetComponentByID(ctx context.Context, id int64) (Component, error)
GetComponentCodeByID(ctx context.Context, id int64) (ComponentCode, error)
GetComponentItemByComponentContainerStatus(ctx context.Context, arg GetComponentItemByComponentContainerStatusParams) (ComponentItem, error)
GetComponentItemByID(ctx context.Context, id int64) (ComponentItem, error)
GetComponentTypeByID(ctx context.Context, id int64) (ComponentType, error)
GetContainerByID(ctx context.Context, id int64) (Container, error)
GetRoleByID(ctx context.Context, id uuid.UUID) (Role, error)
@@ -49,6 +55,7 @@ type Querier interface {
GetWarehouseByID(ctx context.Context, id int64) (Warehouse, error)
ListCabinets(ctx context.Context) ([]Cabinet, error)
ListComponentCodes(ctx context.Context) ([]ComponentCode, error)
ListComponentItems(ctx context.Context) ([]ComponentItem, error)
ListComponentTypes(ctx context.Context) ([]ComponentType, error)
ListComponents(ctx context.Context) ([]Component, error)
ListContainers(ctx context.Context) ([]Container, error)
@@ -61,6 +68,9 @@ type Querier interface {
UpdateCabinet(ctx context.Context, arg UpdateCabinetParams) (Cabinet, error)
UpdateComponent(ctx context.Context, arg UpdateComponentParams) (Component, error)
UpdateComponentCode(ctx context.Context, arg UpdateComponentCodeParams) (ComponentCode, error)
UpdateComponentItem(ctx context.Context, arg UpdateComponentItemParams) (ComponentItem, error)
UpdateComponentItemQuantity(ctx context.Context, arg UpdateComponentItemQuantityParams) (ComponentItem, error)
UpdateComponentItemStatus(ctx context.Context, arg UpdateComponentItemStatusParams) (ComponentItem, error)
UpdateComponentType(ctx context.Context, arg UpdateComponentTypeParams) (ComponentType, error)
UpdateContainer(ctx context.Context, arg UpdateContainerParams) (Container, error)
UpdateRole(ctx context.Context, arg UpdateRoleParams) (Role, error)