120 lines
6.6 KiB
Go
120 lines
6.6 KiB
Go
package service
|
||
|
||
import (
|
||
"errors"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/DATA-DOG/go-sqlmock"
|
||
"github.com/wmms/wmms/internal/model"
|
||
)
|
||
|
||
func applicationHeadColumns() []string {
|
||
return []string{"id", "organization_id", "user_id", "purpose", "status", "rejection_reason", "approved_by", "created_at", "updated_at", "username"}
|
||
}
|
||
|
||
func applicationItemColumns() []string {
|
||
return []string{"id", "application_id", "material_id", "quantity", "material_code", "name"}
|
||
}
|
||
|
||
// 普通审批(不扣库存)沿用已有规则:只要求公司授权、状态待审和相关物料合法,公司没有
|
||
// 启用仓库也必须能审批,不能因为无法解析库存维度而返回 ErrInventoryScopeUnavailable。
|
||
// 这里只登记审批必需的查询;若代码解析仓库 scope,sqlmock 会因未预期语句失败。
|
||
func TestApproveWithoutAutoOutboundDoesNotRequireInventoryScope(t *testing.T) {
|
||
mock := outboundMock(t)
|
||
now := time.Now()
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery("SELECT a.id, a.organization_id, a.user_id").WithArgs(int64(1), int64(1)).WillReturnRows(
|
||
sqlmock.NewRows(applicationHeadColumns()).
|
||
AddRow(1, 1, 2, "领用", model.ApplicationStatusPending, "", 0, now, now, "alice"))
|
||
mock.ExpectQuery("SELECT ai.id, ai.application_id").WithArgs(int64(1), int64(1)).WillReturnRows(
|
||
sqlmock.NewRows(applicationItemColumns()).AddRow(11, 1, 7, 2, "M-1", "Material"))
|
||
mock.ExpectExec("UPDATE material_applications SET status").WithArgs(model.ApplicationStatusApproved, int64(9), int64(1), int64(1), model.ApplicationStatusPending).WillReturnResult(sqlmock.NewResult(0, 1))
|
||
mock.ExpectCommit()
|
||
|
||
if err := NewApplicationService().Approve(1, 1, 9, false); err != nil {
|
||
t.Fatalf("普通审批不应要求仓库配置,got %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
// 自动出库审批在同一事务内解析本公司 scope、条件扣减余额、创建带三维度的出库单和流水,
|
||
// 并带原状态条件改申请状态;所有 SQL 都必须带对组织与维度参数。
|
||
func TestApproveAutoOutboundWritesScopeAndDeductsStock(t *testing.T) {
|
||
mock := outboundMock(t)
|
||
now := time.Now()
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery("SELECT a.id, a.organization_id, a.user_id").WithArgs(int64(1), int64(1)).WillReturnRows(
|
||
sqlmock.NewRows(applicationHeadColumns()).
|
||
AddRow(1, 1, 2, "领用", model.ApplicationStatusPending, "", 0, now, now, "alice"))
|
||
mock.ExpectQuery("SELECT ai.id, ai.application_id").WithArgs(int64(1), int64(1)).WillReturnRows(
|
||
sqlmock.NewRows(applicationItemColumns()).AddRow(11, 1, 7, 2, "M-1", "Material"))
|
||
// ResolveInventoryScope:公司 1 的启用仓库 10、库位 20。
|
||
mock.ExpectQuery("SELECT w.id, w.organization_id").WithArgs(int64(1)).WillReturnRows(
|
||
sqlmock.NewRows([]string{"id", "organization_id", "code", "name", "status", "location_id", "location_code", "location_name", "location_status"}).
|
||
AddRow(10, 1, "W", "仓库", "active", 20, "L", "库位", "active"))
|
||
mock.ExpectQuery("SELECT username FROM users").WithArgs(int64(9)).WillReturnRows(sqlmock.NewRows([]string{"username"}).AddRow("approver"))
|
||
// 条件扣减带 scope 的三维度,余额不足会返回 0 行。
|
||
mock.ExpectExec("UPDATE inventory_balances SET on_hand = on_hand").WithArgs("2", int64(1), int64(10), int64(20), int64(7), "2").WillReturnResult(sqlmock.NewResult(0, 1))
|
||
// 出库单头表显式写入公司、仓库、库位。
|
||
mock.ExpectExec("INSERT INTO outbound_orders").WithArgs(int64(1), int64(10), int64(20), sqlmock.AnyArg(), "approver", "approver", "alice", "").WillReturnResult(sqlmock.NewResult(5, 1))
|
||
mock.ExpectExec("UPDATE outbound_orders SET outbound_no").WithArgs("OUT-00000005", int64(5)).WillReturnResult(sqlmock.NewResult(0, 1))
|
||
mock.ExpectExec("INSERT INTO outbound_items").WithArgs(int64(5), int64(7), 2).WillReturnResult(sqlmock.NewResult(1, 1))
|
||
// 库存流水显式写入三维度。
|
||
mock.ExpectExec("INSERT INTO stock_logs").WithArgs(int64(1), int64(10), int64(20), int64(7), "out", 2, "application", int64(1)).WillReturnResult(sqlmock.NewResult(1, 1))
|
||
mock.ExpectExec("UPDATE material_applications SET status").WithArgs(model.ApplicationStatusApproved, int64(9), int64(1), int64(1), model.ApplicationStatusPending).WillReturnResult(sqlmock.NewResult(0, 1))
|
||
mock.ExpectCommit()
|
||
|
||
if err := NewApplicationService().Approve(1, 1, 9, true); err != nil {
|
||
t.Fatalf("自动出库审批失败: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
// 自动出库遇到余额不足时,事务必须整体回滚:状态不落库、不出单、不生成流水。
|
||
func TestApproveAutoOutboundRollsBackWhenStockInsufficient(t *testing.T) {
|
||
mock := outboundMock(t)
|
||
now := time.Now()
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery("SELECT a.id, a.organization_id, a.user_id").WithArgs(int64(1), int64(1)).WillReturnRows(
|
||
sqlmock.NewRows(applicationHeadColumns()).
|
||
AddRow(1, 1, 2, "领用", model.ApplicationStatusPending, "", 0, now, now, "alice"))
|
||
mock.ExpectQuery("SELECT ai.id, ai.application_id").WithArgs(int64(1), int64(1)).WillReturnRows(
|
||
sqlmock.NewRows(applicationItemColumns()).AddRow(11, 1, 7, 2, "M-1", "Material"))
|
||
mock.ExpectQuery("SELECT w.id, w.organization_id").WithArgs(int64(1)).WillReturnRows(
|
||
sqlmock.NewRows([]string{"id", "organization_id", "code", "name", "status", "location_id", "location_code", "location_name", "location_status"}).
|
||
AddRow(10, 1, "W", "仓库", "active", 20, "L", "库位", "active"))
|
||
mock.ExpectQuery("SELECT username FROM users").WithArgs(int64(9)).WillReturnRows(sqlmock.NewRows([]string{"username"}).AddRow("approver"))
|
||
// 条件扣减影响 0 行:余额不存在或不足,必须整体回滚。
|
||
mock.ExpectExec("UPDATE inventory_balances SET on_hand = on_hand").WithArgs("2", int64(1), int64(10), int64(20), int64(7), "2").WillReturnResult(sqlmock.NewResult(0, 0))
|
||
mock.ExpectRollback()
|
||
|
||
err := NewApplicationService().Approve(1, 1, 9, true)
|
||
if !errors.Is(err, ErrInsufficientStock) {
|
||
t.Fatalf("got %v, want ErrInsufficientStock", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
// 别家公司的申请在锁定阶段命中空行,回 ErrApplicationNotFound,不解析 scope、不落任何单据。
|
||
func TestApproveCrossOrganizationReturnsNotFound(t *testing.T) {
|
||
mock := outboundMock(t)
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery("SELECT a.id, a.organization_id, a.user_id").WithArgs(int64(9), int64(1)).WillReturnRows(sqlmock.NewRows(applicationHeadColumns()))
|
||
mock.ExpectRollback()
|
||
|
||
err := NewApplicationService().Approve(1, 9, 3, true)
|
||
if !errors.Is(err, ErrApplicationNotFound) {
|
||
t.Fatalf("got %v, want ErrApplicationNotFound", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|