247 lines
9.9 KiB
Go
247 lines
9.9 KiB
Go
package procurement_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
fulfillmentdomain "github.com/dujiao-next/internal/modules/fulfillment/domain"
|
|
orderdomain "github.com/dujiao-next/internal/modules/order/domain"
|
|
|
|
"github.com/dujiao-next/internal/constants"
|
|
procurementcontract "github.com/dujiao-next/internal/modules/procurement/contract"
|
|
procurementgormstore "github.com/dujiao-next/internal/modules/procurement/infrastructure/gormstore"
|
|
)
|
|
|
|
type procurementCallbackStatusFixture struct {
|
|
orderNo string
|
|
initialOrderStatus string
|
|
initialProcurementStatus string
|
|
callbackStatus string
|
|
expectedProcurementStatus string
|
|
expectedOrderStatus string
|
|
}
|
|
|
|
func assertProcurementCallbackStatus(t *testing.T, fixture procurementCallbackStatusFixture) {
|
|
t.Helper()
|
|
db := setupProcurementTestDB(t)
|
|
|
|
order := createProcTestOrder(t, db, fixture.orderNo, fixture.initialOrderStatus, constants.FulfillmentTypeUpstream)
|
|
proc := createTestProcurementOrder(t, db, 1, order.ID, order.OrderNo, fixture.initialProcurementStatus)
|
|
|
|
connSvc := newTestSiteConnectionService(db, "test-key", t.TempDir())
|
|
svc := newTestProcurementService(db, connSvc)
|
|
|
|
if err := svc.HandleUpstreamCallback(proc.ID, fixture.callbackStatus, nil); err != nil {
|
|
t.Fatalf("HandleUpstreamCallback: %v", err)
|
|
}
|
|
|
|
var updatedProc ProcurementOrder
|
|
if err := db.First(&updatedProc, proc.ID).Error; err != nil {
|
|
t.Fatalf("load procurement: %v", err)
|
|
}
|
|
if updatedProc.Status != fixture.expectedProcurementStatus {
|
|
t.Errorf("expected procurement status %q, got %q", fixture.expectedProcurementStatus, updatedProc.Status)
|
|
}
|
|
|
|
var updatedOrder orderdomain.Order
|
|
if err := db.First(&updatedOrder, order.ID).Error; err != nil {
|
|
t.Fatalf("load order: %v", err)
|
|
}
|
|
if updatedOrder.Status != fixture.expectedOrderStatus {
|
|
t.Errorf("expected order status %q, got %q", fixture.expectedOrderStatus, updatedOrder.Status)
|
|
}
|
|
}
|
|
|
|
// ── Phase 1 tests: order rollback on procurement failure ──
|
|
|
|
func TestRejectProcurement_RollsBackOrderStatus(t *testing.T) {
|
|
db := setupProcurementTestDB(t)
|
|
|
|
order := createProcTestOrder(t, db, "PROC-REJECT-001", constants.OrderStatusFulfilling, constants.FulfillmentTypeUpstream)
|
|
proc := createTestProcurementOrder(t, db, 1, order.ID, order.OrderNo, "pending")
|
|
|
|
connSvc := newTestSiteConnectionService(db, "test-key", t.TempDir())
|
|
svc := newTestProcurementService(db, connSvc)
|
|
|
|
if err := svc.SubmitToUpstream(proc.ID); err != nil {
|
|
t.Fatalf("SubmitToUpstream with missing connection: %v", err)
|
|
}
|
|
|
|
// 验证采购单状态 = rejected
|
|
var updatedProc ProcurementOrder
|
|
if err := db.First(&updatedProc, proc.ID).Error; err != nil {
|
|
t.Fatalf("load procurement: %v", err)
|
|
}
|
|
if updatedProc.Status != "rejected" {
|
|
t.Errorf("expected procurement status 'rejected', got %q", updatedProc.Status)
|
|
}
|
|
|
|
// 验证本地订单状态从 fulfilling 回退到 paid
|
|
var updatedOrder orderdomain.Order
|
|
if err := db.First(&updatedOrder, order.ID).Error; err != nil {
|
|
t.Fatalf("load order: %v", err)
|
|
}
|
|
if updatedOrder.Status != constants.OrderStatusPaid {
|
|
t.Errorf("expected order status %q, got %q", constants.OrderStatusPaid, updatedOrder.Status)
|
|
}
|
|
}
|
|
|
|
func TestHandleUpstreamCallback_Canceled_RollsBackOrder(t *testing.T) {
|
|
db := setupProcurementTestDB(t)
|
|
|
|
order := createProcTestOrder(t, db, "PROC-CANCEL-001", constants.OrderStatusFulfilling, constants.FulfillmentTypeUpstream)
|
|
proc := createTestProcurementOrder(t, db, 1, order.ID, order.OrderNo, "accepted")
|
|
|
|
connSvc := newTestSiteConnectionService(db, "test-key", t.TempDir())
|
|
svc := newTestProcurementService(db, connSvc)
|
|
|
|
if err := svc.HandleUpstreamCallback(proc.ID, "canceled", nil); err != nil {
|
|
t.Fatalf("HandleUpstreamCallback: %v", err)
|
|
}
|
|
|
|
// 验证采购单状态 = canceled
|
|
var updatedProc ProcurementOrder
|
|
if err := db.First(&updatedProc, proc.ID).Error; err != nil {
|
|
t.Fatalf("load procurement: %v", err)
|
|
}
|
|
if updatedProc.Status != "canceled" {
|
|
t.Errorf("expected procurement status 'canceled', got %q", updatedProc.Status)
|
|
}
|
|
|
|
// 验证本地订单状态从 fulfilling 回退到 paid
|
|
var updatedOrder orderdomain.Order
|
|
if err := db.First(&updatedOrder, order.ID).Error; err != nil {
|
|
t.Fatalf("load order: %v", err)
|
|
}
|
|
if updatedOrder.Status != constants.OrderStatusPaid {
|
|
t.Errorf("expected order status %q, got %q", constants.OrderStatusPaid, updatedOrder.Status)
|
|
}
|
|
}
|
|
|
|
func TestHandleUpstreamCallback_Delivered_CreatesFulfillment(t *testing.T) {
|
|
db := setupProcurementTestDB(t)
|
|
|
|
order := createProcTestOrder(t, db, "PROC-DELIVER-001", constants.OrderStatusFulfilling, constants.FulfillmentTypeUpstream)
|
|
proc := createTestProcurementOrder(t, db, 1, order.ID, order.OrderNo, "accepted")
|
|
|
|
connSvc := newTestSiteConnectionService(db, "test-key", t.TempDir())
|
|
svc := newTestProcurementService(db, connSvc)
|
|
|
|
now := time.Now()
|
|
fulfillment := &procurementcontract.Fulfillment{
|
|
Type: constants.FulfillmentTypeUpstream,
|
|
Status: constants.FulfillmentStatusDelivered,
|
|
Payload: "CDK-001\nCDK-002",
|
|
DeliveredAt: &now,
|
|
}
|
|
|
|
if err := svc.HandleUpstreamCallback(proc.ID, "delivered", fulfillment); err != nil {
|
|
t.Fatalf("HandleUpstreamCallback: %v", err)
|
|
}
|
|
|
|
// 验证采购单状态 = fulfilled
|
|
var updatedProc ProcurementOrder
|
|
if err := db.First(&updatedProc, proc.ID).Error; err != nil {
|
|
t.Fatalf("load procurement: %v", err)
|
|
}
|
|
if updatedProc.Status != "fulfilled" {
|
|
t.Errorf("expected procurement status 'fulfilled', got %q", updatedProc.Status)
|
|
}
|
|
|
|
// 验证本地订单状态 = delivered
|
|
var updatedOrder orderdomain.Order
|
|
if err := db.First(&updatedOrder, order.ID).Error; err != nil {
|
|
t.Fatalf("load order: %v", err)
|
|
}
|
|
if updatedOrder.Status != constants.OrderStatusDelivered {
|
|
t.Errorf("expected order status %q, got %q", constants.OrderStatusDelivered, updatedOrder.Status)
|
|
}
|
|
|
|
// 验证 Fulfillment 记录已创建
|
|
var ff fulfillmentdomain.Fulfillment
|
|
if err := db.Where("order_id = ?", order.ID).First(&ff).Error; err != nil {
|
|
t.Fatalf("expected fulfillment record to exist: %v", err)
|
|
}
|
|
if ff.Payload != "CDK-001\nCDK-002" {
|
|
t.Errorf("unexpected fulfillment payload: %q", ff.Payload)
|
|
}
|
|
if ff.Type != constants.FulfillmentTypeUpstream {
|
|
t.Errorf("expected fulfillment type %q, got %q", constants.FulfillmentTypeUpstream, ff.Type)
|
|
}
|
|
}
|
|
|
|
func TestHandleUpstreamCallback_Delivered_SynchronizesParentStatus(t *testing.T) {
|
|
db := setupProcurementTestDB(t)
|
|
parent := createProcTestOrder(t, db, "PROC-PARENT-DELIVERED", constants.OrderStatusFulfilling, constants.FulfillmentTypeUpstream)
|
|
child := createProcTestOrder(t, db, "PROC-CHILD-DELIVERED", constants.OrderStatusFulfilling, constants.FulfillmentTypeUpstream)
|
|
if err := db.Model(&child).Update("parent_id", parent.ID).Error; err != nil {
|
|
t.Fatalf("set child parent: %v", err)
|
|
}
|
|
proc := createTestProcurementOrder(t, db, 1, child.ID, child.OrderNo, constants.ProcurementStatusAccepted)
|
|
|
|
svc := newTestProcurementService(db, newTestSiteConnectionService(db, "test-key", t.TempDir()))
|
|
if err := svc.HandleUpstreamCallback(proc.ID, "delivered", nil); err != nil {
|
|
t.Fatalf("HandleUpstreamCallback: %v", err)
|
|
}
|
|
|
|
var updatedParent orderdomain.Order
|
|
if err := db.First(&updatedParent, parent.ID).Error; err != nil {
|
|
t.Fatalf("load parent order: %v", err)
|
|
}
|
|
if updatedParent.Status != constants.OrderStatusDelivered {
|
|
t.Fatalf("parent status = %q, want %q", updatedParent.Status, constants.OrderStatusDelivered)
|
|
}
|
|
}
|
|
|
|
func TestHandleUpstreamCallback_PartiallyRefunded_AfterFulfilledUpdatesProcurementStatus(t *testing.T) {
|
|
assertProcurementCallbackStatus(t, procurementCallbackStatusFixture{
|
|
orderNo: "PROC-REFUND-KEEP-001",
|
|
initialOrderStatus: constants.OrderStatusDelivered,
|
|
initialProcurementStatus: constants.ProcurementStatusFulfilled,
|
|
callbackStatus: "partially_refunded",
|
|
expectedProcurementStatus: constants.ProcurementStatusPartiallyRefunded,
|
|
expectedOrderStatus: constants.OrderStatusDelivered,
|
|
})
|
|
}
|
|
|
|
func TestHandleUpstreamCallback_PartiallyRefunded_WhileFulfillingKeepsOrderStatus(t *testing.T) {
|
|
assertProcurementCallbackStatus(t, procurementCallbackStatusFixture{
|
|
orderNo: "PROC-REFUND-FULFILLING-001",
|
|
initialOrderStatus: constants.OrderStatusFulfilling,
|
|
initialProcurementStatus: constants.ProcurementStatusAccepted,
|
|
callbackStatus: "partially_refunded",
|
|
expectedProcurementStatus: constants.ProcurementStatusPartiallyRefunded,
|
|
expectedOrderStatus: constants.OrderStatusFulfilling,
|
|
})
|
|
}
|
|
|
|
func TestHandleUpstreamCallback_Refunded_AfterCompletedKeepsOrderStatus(t *testing.T) {
|
|
assertProcurementCallbackStatus(t, procurementCallbackStatusFixture{
|
|
orderNo: "PROC-REFUND-COMPLETED-001",
|
|
initialOrderStatus: constants.OrderStatusCompleted,
|
|
initialProcurementStatus: constants.ProcurementStatusFulfilled,
|
|
callbackStatus: "refunded",
|
|
expectedProcurementStatus: constants.ProcurementStatusRefunded,
|
|
expectedOrderStatus: constants.OrderStatusCompleted,
|
|
})
|
|
}
|
|
|
|
// 秒发货的回调可能先于下单响应把采购单推进到 fulfilled;随后的提交 / 轮询写入
|
|
// 必须基于读到的旧状态做比较后再写,不能把状态回退为 accepted。
|
|
func TestProcurementStoreUpdateStatusIsCompareAndSet(t *testing.T) {
|
|
db := setupProcurementTestDB(t)
|
|
order := createProcTestOrder(t, db, "PROC-CAS-001", constants.OrderStatusDelivered, constants.FulfillmentTypeUpstream)
|
|
proc := createTestProcurementOrder(t, db, 1, order.ID, order.OrderNo, constants.ProcurementStatusFulfilled)
|
|
|
|
store := procurementgormstore.New(db)
|
|
written, err := store.UpdateStatus(proc.ID, constants.ProcurementStatusPending, constants.ProcurementStatusAccepted, nil)
|
|
if err != nil || written {
|
|
t.Fatalf("stale transition must not be written: written=%v err=%v", written, err)
|
|
}
|
|
var reloaded ProcurementOrder
|
|
db.First(&reloaded, proc.ID)
|
|
if reloaded.Status != constants.ProcurementStatusFulfilled {
|
|
t.Fatalf("expected status to stay fulfilled, got %q", reloaded.Status)
|
|
}
|
|
}
|