feat: implement component-item management with CRUD operations and status updates
This commit is contained in:
@@ -19,6 +19,7 @@ const (
|
|||||||
API_GROUP_COMPONENT_TYPE = "/component-types"
|
API_GROUP_COMPONENT_TYPE = "/component-types"
|
||||||
API_GROUP_COMPONENT = "/components"
|
API_GROUP_COMPONENT = "/components"
|
||||||
API_GROUP_COMPONENT_CODE = "/component-codes"
|
API_GROUP_COMPONENT_CODE = "/component-codes"
|
||||||
|
API_GROUP_COMPONENT_ITEM = "/component-items"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
75
db/queries/component_item.sql
Normal file
75
db/queries/component_item.sql
Normal 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;
|
||||||
|
|
||||||
15
db/queries/component_status_history.sql
Normal file
15
db/queries/component_status_history.sql
Normal 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 *;
|
||||||
517
docs/db/ChangeStatusComponent.md
Normal file
517
docs/db/ChangeStatusComponent.md
Normal 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
4
docs/db/Note.md
Normal 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
|
||||||
@@ -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": {
|
"/api/v1/component-types": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Retrieve a list of all component types ordered by creation date",
|
"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": {
|
"models.ComponentType": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"models.Room": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"requests.CreateComponentRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"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": {
|
"requests.UpdateComponentRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2913,6 +3453,14 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"responses.CreateComponentItemResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses.CreateComponentResponse": {
|
"responses.CreateComponentResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"responses.UpdateComponentResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -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": {
|
"/api/v1/component-types": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Retrieve a list of all component types ordered by creation date",
|
"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": {
|
"models.ComponentType": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"models.Room": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"requests.CreateComponentRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"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": {
|
"requests.UpdateComponentRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2907,6 +3447,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"responses.CreateComponentItemResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses.CreateComponentResponse": {
|
"responses.CreateComponentResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"responses.UpdateComponentResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -59,6 +59,27 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
type: array
|
type: array
|
||||||
type: object
|
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:
|
models.ComponentType:
|
||||||
properties:
|
properties:
|
||||||
createdAt:
|
createdAt:
|
||||||
@@ -99,6 +120,29 @@ definitions:
|
|||||||
updatedAt:
|
updatedAt:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
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:
|
models.Room:
|
||||||
properties:
|
properties:
|
||||||
createdAt:
|
createdAt:
|
||||||
@@ -192,6 +236,26 @@ definitions:
|
|||||||
- code
|
- code
|
||||||
- componentId
|
- componentId
|
||||||
type: object
|
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:
|
requests.CreateComponentRequest:
|
||||||
properties:
|
properties:
|
||||||
componentTypeId:
|
componentTypeId:
|
||||||
@@ -309,6 +373,34 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
type: array
|
type: array
|
||||||
type: object
|
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:
|
requests.UpdateComponentRequest:
|
||||||
properties:
|
properties:
|
||||||
componentTypeId:
|
componentTypeId:
|
||||||
@@ -414,6 +506,11 @@ definitions:
|
|||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
|
responses.CreateComponentItemResponse:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
responses.CreateComponentResponse:
|
responses.CreateComponentResponse:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
@@ -468,6 +565,40 @@ definitions:
|
|||||||
isPrimary:
|
isPrimary:
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
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:
|
responses.UpdateComponentResponse:
|
||||||
properties:
|
properties:
|
||||||
componentTypeId:
|
componentTypeId:
|
||||||
@@ -723,6 +854,260 @@ paths:
|
|||||||
summary: Update component code
|
summary: Update component code
|
||||||
tags:
|
tags:
|
||||||
- component-code
|
- 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:
|
/api/v1/component-types:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
56
internal/mapper/component_item_mapper.go
Normal file
56
internal/mapper/component_item_mapper.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
45
internal/mapper/component_status_history_mapper.go
Normal file
45
internal/mapper/component_status_history_mapper.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
42
internal/models/component_item_model.go
Normal file
42
internal/models/component_item_model.go
Normal 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"`
|
||||||
|
}
|
||||||
14
internal/models/component_status_history_model.go
Normal file
14
internal/models/component_status_history_model.go
Normal 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"`
|
||||||
|
}
|
||||||
24
internal/models/requests/component_item_request.go
Normal file
24
internal/models/requests/component_item_request.go
Normal 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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
28
internal/models/responses/component_item_response.go
Normal file
28
internal/models/responses/component_item_response.go
Normal 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"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package responses
|
||||||
|
|
||||||
|
type CreateComponentStatusHistoryResponse struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
238
internal/repositories/component_item_repository.go
Normal file
238
internal/repositories/component_item_repository.go
Normal 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
|
||||||
|
}
|
||||||
518
internal/repositories/component_item_repository_test.go
Normal file
518
internal/repositories/component_item_repository_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,6 +100,17 @@ func NewRouter() *gin.Engine {
|
|||||||
componentCode.PUT("/:id", utils.AsyncHandler(services.ComponentCodeUpdate))
|
componentCode.PUT("/:id", utils.AsyncHandler(services.ComponentCodeUpdate))
|
||||||
componentCode.DELETE("/:id", utils.AsyncHandler(services.ComponentCodeDelete))
|
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)
|
r.GET(constants.API_PATH_PING, services.PingHandler)
|
||||||
|
|||||||
290
internal/services/component_item_service.go
Normal file
290
internal/services/component_item_service.go
Normal 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
|
||||||
|
}
|
||||||
320
sqlc_gen/component_item.sql.go
Normal file
320
sqlc_gen/component_item.sql.go
Normal 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
|
||||||
|
}
|
||||||
64
sqlc_gen/component_status_history.sql.go
Normal file
64
sqlc_gen/component_status_history.sql.go
Normal 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
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ type Querier interface {
|
|||||||
CreateCabinet(ctx context.Context, arg CreateCabinetParams) (Cabinet, error)
|
CreateCabinet(ctx context.Context, arg CreateCabinetParams) (Cabinet, error)
|
||||||
CreateComponent(ctx context.Context, arg CreateComponentParams) (Component, error)
|
CreateComponent(ctx context.Context, arg CreateComponentParams) (Component, error)
|
||||||
CreateComponentCode(ctx context.Context, arg CreateComponentCodeParams) (ComponentCode, 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)
|
CreateComponentType(ctx context.Context, arg CreateComponentTypeParams) (ComponentType, error)
|
||||||
CreateContainer(ctx context.Context, arg CreateContainerParams) (Container, error)
|
CreateContainer(ctx context.Context, arg CreateContainerParams) (Container, error)
|
||||||
CreateRole(ctx context.Context, arg CreateRoleParams) (Role, error)
|
CreateRole(ctx context.Context, arg CreateRoleParams) (Role, error)
|
||||||
@@ -26,15 +28,19 @@ type Querier interface {
|
|||||||
DeleteCabinet(ctx context.Context, id int64) (int64, error)
|
DeleteCabinet(ctx context.Context, id int64) (int64, error)
|
||||||
DeleteComponent(ctx context.Context, id int64) (int64, error)
|
DeleteComponent(ctx context.Context, id int64) (int64, error)
|
||||||
DeleteComponentCode(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)
|
DeleteComponentType(ctx context.Context, id int64) (int64, error)
|
||||||
DeleteContainer(ctx context.Context, id int64) (int64, error)
|
DeleteContainer(ctx context.Context, id int64) (int64, error)
|
||||||
DeleteRole(ctx context.Context, id uuid.UUID) (int64, error)
|
DeleteRole(ctx context.Context, id uuid.UUID) (int64, error)
|
||||||
DeleteRoom(ctx context.Context, id int64) (int64, error)
|
DeleteRoom(ctx context.Context, id int64) (int64, error)
|
||||||
DeleteShelve(ctx context.Context, id int64) (int64, error)
|
DeleteShelve(ctx context.Context, id int64) (int64, error)
|
||||||
DeleteWarehouse(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)
|
GetCabinetByID(ctx context.Context, id int64) (Cabinet, error)
|
||||||
GetComponentByID(ctx context.Context, id int64) (Component, error)
|
GetComponentByID(ctx context.Context, id int64) (Component, error)
|
||||||
GetComponentCodeByID(ctx context.Context, id int64) (ComponentCode, 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)
|
GetComponentTypeByID(ctx context.Context, id int64) (ComponentType, error)
|
||||||
GetContainerByID(ctx context.Context, id int64) (Container, error)
|
GetContainerByID(ctx context.Context, id int64) (Container, error)
|
||||||
GetRoleByID(ctx context.Context, id uuid.UUID) (Role, 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)
|
GetWarehouseByID(ctx context.Context, id int64) (Warehouse, error)
|
||||||
ListCabinets(ctx context.Context) ([]Cabinet, error)
|
ListCabinets(ctx context.Context) ([]Cabinet, error)
|
||||||
ListComponentCodes(ctx context.Context) ([]ComponentCode, error)
|
ListComponentCodes(ctx context.Context) ([]ComponentCode, error)
|
||||||
|
ListComponentItems(ctx context.Context) ([]ComponentItem, error)
|
||||||
ListComponentTypes(ctx context.Context) ([]ComponentType, error)
|
ListComponentTypes(ctx context.Context) ([]ComponentType, error)
|
||||||
ListComponents(ctx context.Context) ([]Component, error)
|
ListComponents(ctx context.Context) ([]Component, error)
|
||||||
ListContainers(ctx context.Context) ([]Container, error)
|
ListContainers(ctx context.Context) ([]Container, error)
|
||||||
@@ -61,6 +68,9 @@ type Querier interface {
|
|||||||
UpdateCabinet(ctx context.Context, arg UpdateCabinetParams) (Cabinet, error)
|
UpdateCabinet(ctx context.Context, arg UpdateCabinetParams) (Cabinet, error)
|
||||||
UpdateComponent(ctx context.Context, arg UpdateComponentParams) (Component, error)
|
UpdateComponent(ctx context.Context, arg UpdateComponentParams) (Component, error)
|
||||||
UpdateComponentCode(ctx context.Context, arg UpdateComponentCodeParams) (ComponentCode, 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)
|
UpdateComponentType(ctx context.Context, arg UpdateComponentTypeParams) (ComponentType, error)
|
||||||
UpdateContainer(ctx context.Context, arg UpdateContainerParams) (Container, error)
|
UpdateContainer(ctx context.Context, arg UpdateContainerParams) (Container, error)
|
||||||
UpdateRole(ctx context.Context, arg UpdateRoleParams) (Role, error)
|
UpdateRole(ctx context.Context, arg UpdateRoleParams) (Role, error)
|
||||||
|
|||||||
Reference in New Issue
Block a user