package service import ( "database/sql" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/shopspring/decimal" "github.com/wmms/wmms/internal/model" ) func createInboundRequest() *model.CreateInboundRequest { req := &model.CreateInboundRequest{Handler: "h", Supplier: "s", Remark: "r"} req.Items = append(req.Items, struct { MaterialID int64 `json:"material_id" binding:"required"` Quantity int `json:"quantity" binding:"required,gt=0"` WarningStock *decimal.Decimal `json:"warning_stock"` WarningEnabled *bool `json:"warning_enabled"` }{MaterialID: 7, Quantity: 2}) return req } // 入库单据的三个维度由服务端按当前公司解析后填写:请求体没有任何归属字段, // 后端把 scope 的公司、仓库、库位写进单据头,落库 SQL 也带上这三列。 func TestInboundCreateFillsOrderDimensionsFromResolvedScope(t *testing.T) { mock := outboundMock(t) // Create 先校验明细物料,再解析库存维度。 expectActiveMaterial(mock, 7) expectStockScope(mock, 1) mock.ExpectBegin() mock.ExpectExec("INSERT INTO inbound_orders"). WithArgs(int64(1), int64(10), int64(20), sqlmock.AnyArg(), "operator", "h", "s", "r"). WillReturnResult(sqlmock.NewResult(5, 1)) mock.ExpectExec("UPDATE inbound_orders").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("INSERT INTO inbound_items").WithArgs(int64(5), int64(7), 2).WillReturnResult(sqlmock.NewResult(6, 1)) // 明细补物料编码/名称时再读一次物料。 mock.ExpectQuery("SELECT id, organization_id, material_code, name, material_image").WithArgs(int64(1), int64(7)).WillReturnRows(activeMaterialRow(7)) mock.ExpectExec("INSERT INTO inventory_balances").WithArgs(int64(1), int64(10), int64(20), int64(7), "2").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("INSERT INTO stock_logs").WithArgs(int64(1), int64(10), int64(20), int64(7), "in", 2, "inbound", int64(5)).WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() order, err := NewInboundService().Create(1, createInboundRequest(), "operator") if err != nil { t.Fatalf("create inbound: %v", err) } if order.OrganizationID != 1 || order.WarehouseID != 10 || order.LocationID != 20 { t.Fatalf("单据维度=%d/%d/%d, want 1/10/20", order.OrganizationID, order.WarehouseID, order.LocationID) } if order.InboundNo != "IN-00000005" { t.Fatalf("单号=%s, want IN-00000005", order.InboundNo) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // 删除别家公司的单据:事务内锁定当前公司原单据时命中 ErrNoRows,整体不冲销任何库存, // 也不会删掉别家公司的单据。 func TestInboundDeleteCrossOrganizationReturnsNoRows(t *testing.T) { mock := outboundMock(t) mock.ExpectBegin() mock.ExpectQuery("SELECT id, COALESCE\\(inbound_no").WithArgs(int64(9), int64(1)). WillReturnRows(sqlmock.NewRows([]string{"id"})) mock.ExpectRollback() err := NewInboundService().Delete(1, 9) if err != sql.ErrNoRows { t.Fatalf("got %v, want sql.ErrNoRows", err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // resolveWarningEnabled 决定入库时写预警规则要不要启用。老客户端只会传 warning_stock、 // 从不传 warning_enabled,那种情况下必须保持原来的"带上阈值即启用"行为。 func TestResolveWarningEnabled(t *testing.T) { enabled := true disabled := false cases := []struct { name string in *bool want bool }{ {"未传则默认启用", nil, true}, {"显式启用", &enabled, true}, {"显式关闭", &disabled, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := resolveWarningEnabled(tc.in); got != tc.want { t.Fatalf("resolveWarningEnabled(%v) = %v, want %v", tc.in, got, tc.want) } }) } }