material_manage_system/internal/service/application_test.go

120 lines
6.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}