补商户发货API
This commit is contained in:
@@ -16,6 +16,9 @@ import (
|
||||
"time"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const defaultDeliveryBFFBaseURL = "https://www.jxya.top/bff-stg"
|
||||
@@ -139,15 +142,50 @@ func (s *DeliveryService) GetOrder(orderNo string, auth DeliveryLinkAuth) (*Deli
|
||||
return info, err
|
||||
}
|
||||
|
||||
func (s *DeliveryService) GetMerchantOrder(merchantID uint, orderNo string) (*DeliveryOrderInfo, error) {
|
||||
openOrder, order, goodID, err := s.loadMerchantOrder(merchantID, orderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, _, _, err := s.buildDeliveryState(openOrder, order, goodID, false)
|
||||
return info, err
|
||||
}
|
||||
|
||||
func (s *DeliveryService) Bind(orderNo, gameAccount string, auth DeliveryLinkAuth) (*DeliveryBindResult, error) {
|
||||
return s.bindWithOrder(orderNo, gameAccount, &auth, 0)
|
||||
}
|
||||
|
||||
func (s *DeliveryService) BindForMerchant(merchantID uint, orderNo, gameAccount string) (*DeliveryBindResult, error) {
|
||||
return s.bindWithOrder(orderNo, gameAccount, nil, merchantID)
|
||||
}
|
||||
|
||||
func (s *DeliveryService) bindWithOrder(orderNo, gameAccount string, auth *DeliveryLinkAuth, merchantID uint) (*DeliveryBindResult, error) {
|
||||
gameAccount = strings.TrimSpace(gameAccount)
|
||||
if gameAccount == "" {
|
||||
return nil, errors.New("请输入玩家编号")
|
||||
}
|
||||
_, _, goodID, err := s.prepareOrder(orderNo, true, &auth)
|
||||
var (
|
||||
openOrder *OpenOrderQuery
|
||||
order *model.FulfillmentOrder
|
||||
goodID string
|
||||
err error
|
||||
)
|
||||
if merchantID > 0 {
|
||||
openOrder, order, goodID, err = s.loadMerchantOrder(merchantID, orderNo)
|
||||
} else {
|
||||
openOrder, order, goodID, err = s.loadLinkedOrder(orderNo, auth)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _, _, err = s.buildDeliveryState(openOrder, order, goodID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.bindGood(gameAccount, goodID)
|
||||
}
|
||||
|
||||
func (s *DeliveryService) bindGood(gameAccount, goodID string) (*DeliveryBindResult, error) {
|
||||
var out struct {
|
||||
BindUUID string `json:"bind_uuid"`
|
||||
URL string `json:"url"`
|
||||
@@ -171,32 +209,85 @@ func (s *DeliveryService) Bind(orderNo, gameAccount string, auth DeliveryLinkAut
|
||||
}
|
||||
|
||||
func (s *DeliveryService) Submit(orderNo, gameAccount, bindUUID string, auth DeliveryLinkAuth) (*DeliverySubmitResult, error) {
|
||||
return s.submit(orderNo, gameAccount, bindUUID, 0, &auth, 0)
|
||||
}
|
||||
|
||||
func (s *DeliveryService) SubmitForMerchant(merchantID, apiClientID uint, orderNo, gameAccount, bindUUID string) (*DeliverySubmitResult, error) {
|
||||
return s.submit(orderNo, gameAccount, bindUUID, apiClientID, nil, merchantID)
|
||||
}
|
||||
|
||||
func (s *DeliveryService) submit(orderNo, gameAccount, bindUUID string, apiClientID uint, auth *DeliveryLinkAuth, merchantID uint) (*DeliverySubmitResult, error) {
|
||||
gameAccount = strings.TrimSpace(gameAccount)
|
||||
bindUUID = strings.TrimSpace(bindUUID)
|
||||
orderNo = strings.TrimSpace(orderNo)
|
||||
if gameAccount == "" || bindUUID == "" {
|
||||
return nil, errors.New("玩家编号和绑定凭证不能为空")
|
||||
}
|
||||
_, order, goodID, err := s.prepareOrder(orderNo, true, &auth)
|
||||
var (
|
||||
openOrder *OpenOrderQuery
|
||||
order *model.FulfillmentOrder
|
||||
goodID string
|
||||
err error
|
||||
)
|
||||
if merchantID > 0 {
|
||||
openOrder, order, goodID, err = s.loadMerchantOrder(merchantID, orderNo)
|
||||
} else {
|
||||
openOrder, order, goodID, err = s.loadLinkedOrder(orderNo, auth)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, _, _, err := s.buildDeliveryState(openOrder, order, goodID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if order.FulfillmentStatus == model.FulfillmentStatusProcessing ||
|
||||
order.FulfillmentStatus == model.FulfillmentStatusSucceeded ||
|
||||
strings.TrimSpace(order.ProviderOrderNo) != "" ||
|
||||
strings.Contains(order.ResultData, "\"queue_order_id\"") {
|
||||
if existing := buildExistingDeliverySubmitResult(order); existing != nil {
|
||||
return existing, nil
|
||||
}
|
||||
}
|
||||
if !info.CanShip {
|
||||
reason := info.CannotShipReason
|
||||
if reason == "" {
|
||||
reason = "订单暂不可发货"
|
||||
}
|
||||
return nil, newDeliveryHTTPError(http.StatusBadRequest, reason)
|
||||
}
|
||||
claimed, claimedNow, err := s.claimDeliverySubmission(order.MerchantID, apiClientID, order.OrderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !claimedNow {
|
||||
if existing := buildExistingDeliverySubmitResult(claimed); existing != nil {
|
||||
return existing, nil
|
||||
}
|
||||
return nil, newDeliveryHTTPError(http.StatusConflict, "订单暂时正在履约中,请稍后查询")
|
||||
}
|
||||
boundAccount, err := s.accountBound(bindUUID, goodID)
|
||||
if err != nil {
|
||||
s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
if boundGameAccount := stringFromMap(boundAccount, "game_account"); boundGameAccount != "" && boundGameAccount != gameAccount {
|
||||
return nil, errors.New("玩家编号与绑定的游戏账号不一致,请重新绑定")
|
||||
err := errors.New("玩家编号与绑定的游戏账号不一致,请重新绑定")
|
||||
s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
queueOrderID, err := s.createOrderQueue(goodID, order.OrderNo)
|
||||
if err != nil {
|
||||
s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
if err := s.patchOrderQueue(queueOrderID, gameAccount, bindUUID); err != nil {
|
||||
s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
upstreamOrder, err := s.createUpstreamOrder(queueOrderID, gameAccount, goodID, order.OrderNo)
|
||||
if err != nil {
|
||||
s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
providerOrderNo := firstNonEmpty(
|
||||
@@ -217,7 +308,7 @@ func (s *DeliveryService) Submit(orderNo, gameAccount, bindUUID string, auth Del
|
||||
}
|
||||
updated, err := s.fulfillment.UpdateFulfillment(FulfillmentUpdateInput{
|
||||
MerchantID: order.MerchantID,
|
||||
APIClientID: 0,
|
||||
APIClientID: apiClientID,
|
||||
OrderNo: order.OrderNo,
|
||||
Status: "processing",
|
||||
ProviderOrderNo: providerOrderNo,
|
||||
@@ -236,6 +327,204 @@ func (s *DeliveryService) Submit(orderNo, gameAccount, bindUUID string, auth Del
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) claimDeliverySubmission(merchantID, apiClientID uint, orderNo string) (*model.FulfillmentOrder, bool, error) {
|
||||
var updated model.FulfillmentOrder
|
||||
claimedNow := false
|
||||
err := s.fulfillment.db.Transaction(func(tx *gorm.DB) error {
|
||||
var order model.FulfillmentOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("merchant_id = ? AND order_no = ?", merchantID, orderNo).
|
||||
First(&order).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("订单不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if order.FulfillmentStatus == model.FulfillmentStatusProcessing || order.FulfillmentStatus == model.FulfillmentStatusSucceeded {
|
||||
updated = order
|
||||
return nil
|
||||
}
|
||||
if order.FulfillmentStatus != model.FulfillmentStatusPending && order.FulfillmentStatus != model.FulfillmentStatusFailed {
|
||||
return newDeliveryHTTPError(http.StatusConflict, "订单暂不可发货")
|
||||
}
|
||||
raw, _ := json.Marshal(map[string]interface{}{
|
||||
"source": "delivery_proxy",
|
||||
"submit_started_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"provider_order_stage": "claimed",
|
||||
})
|
||||
if err := tx.Model(&order).Updates(map[string]interface{}{
|
||||
"fulfillment_status": model.FulfillmentStatusProcessing,
|
||||
"result_data": string(raw),
|
||||
"failure_reason": "",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
claimedNow = true
|
||||
if err := tx.First(&updated, order.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
apiClientIDPtr := optionalUint(apiClientID)
|
||||
if err := writeAudit(tx, &merchantID, nil, apiClientIDPtr, "delivery.submit.claim", "fulfillment_order", order.OrderNo, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.fulfillment.callbacks != nil {
|
||||
if err := s.fulfillment.callbacks.Enqueue(tx, merchantID, "order.fulfillment.updated", orderCallbackData(&updated)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return &updated, claimedNow, nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) markDeliverySubmissionFailed(order *model.FulfillmentOrder, apiClientID uint, reason string) {
|
||||
if order == nil {
|
||||
return
|
||||
}
|
||||
_, _ = s.fulfillment.UpdateFulfillment(FulfillmentUpdateInput{
|
||||
MerchantID: order.MerchantID,
|
||||
APIClientID: apiClientID,
|
||||
OrderNo: order.OrderNo,
|
||||
Status: model.FulfillmentStatusFailed,
|
||||
FailureReason: reason,
|
||||
ResultData: map[string]interface{}{
|
||||
"source": "delivery_proxy",
|
||||
"fail_reason": reason,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DeliveryService) loadLinkedOrder(orderNo string, auth *DeliveryLinkAuth) (*OpenOrderQuery, *model.FulfillmentOrder, string, error) {
|
||||
openOrder, err := s.fulfillment.QueryOpenOrder(orderNo)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
order, err := s.fulfillment.GetByOrderNo(orderNo)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
if auth != nil {
|
||||
if err := s.authorizeDeliveryLink(order, *auth); err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
}
|
||||
goodID := ""
|
||||
if openOrder.Product != nil {
|
||||
goodID = deliveryGoodID(s.channel, openOrder.Product.SKU)
|
||||
}
|
||||
return openOrder, order, goodID, nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) loadMerchantOrder(merchantID uint, orderNo string) (*OpenOrderQuery, *model.FulfillmentOrder, string, error) {
|
||||
if merchantID == 0 {
|
||||
return nil, nil, "", errors.New("无效的商户")
|
||||
}
|
||||
openOrder, err := s.fulfillment.QueryOpenOrder(orderNo)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
order, err := s.fulfillment.GetOrder(merchantID, orderNo)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
goodID := ""
|
||||
if openOrder.Product != nil {
|
||||
goodID = deliveryGoodID(s.channel, openOrder.Product.SKU)
|
||||
}
|
||||
return openOrder, order, goodID, nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) buildDeliveryState(openOrder *OpenOrderQuery, order *model.FulfillmentOrder, goodID string, requireCanShip bool) (*DeliveryOrderInfo, *model.FulfillmentOrder, string, error) {
|
||||
if openOrder == nil || order == nil {
|
||||
return nil, nil, "", errors.New("订单不存在")
|
||||
}
|
||||
canShip, reason := CanFulfill(order)
|
||||
if !openOrder.CanShip {
|
||||
reason = openOrder.CannotShipReason
|
||||
canShip = false
|
||||
}
|
||||
if !canShip && reason == "" {
|
||||
reason = "订单暂不可发货"
|
||||
}
|
||||
if openOrder.Product == nil || openOrder.Product.SKU == "" {
|
||||
canShip = false
|
||||
reason = "订单缺少商品 SKU"
|
||||
} else if goodID == "" {
|
||||
goodID = deliveryGoodID(s.channel, openOrder.Product.SKU)
|
||||
if goodID == "" {
|
||||
canShip = false
|
||||
reason = "未找到对应的上游商品配置"
|
||||
}
|
||||
}
|
||||
if requireCanShip && !canShip {
|
||||
return nil, nil, "", newDeliveryHTTPError(http.StatusBadRequest, reason)
|
||||
}
|
||||
var good map[string]interface{}
|
||||
if goodID != "" {
|
||||
good, _ = s.goodsDetail(goodID)
|
||||
}
|
||||
product := buildDeliveryProduct(openOrder.Product, good)
|
||||
info := &DeliveryOrderInfo{
|
||||
OrderNo: openOrder.OrderNo,
|
||||
Status: openOrder.Status,
|
||||
CanShip: canShip,
|
||||
CannotShipReason: reason,
|
||||
Product: product,
|
||||
BuyerName: openOrder.BuyerName,
|
||||
Amount: openOrder.Amount,
|
||||
CreatedAt: openOrder.CreatedAt,
|
||||
ShippedAt: openOrder.ShippedAt,
|
||||
ShipFailReason: openOrder.ShipFailReason,
|
||||
GameChannel: openOrder.GameChannel,
|
||||
GameUID: openOrder.GameUID,
|
||||
RoleName: openOrder.RoleName,
|
||||
PayScore: openOrder.PayScore,
|
||||
Good: good,
|
||||
}
|
||||
return info, order, goodID, nil
|
||||
}
|
||||
|
||||
func buildExistingDeliverySubmitResult(order *model.FulfillmentOrder) *DeliverySubmitResult {
|
||||
if order == nil {
|
||||
return nil
|
||||
}
|
||||
switch order.FulfillmentStatus {
|
||||
case model.FulfillmentStatusProcessing, model.FulfillmentStatusSucceeded:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
var resultData map[string]interface{}
|
||||
if json.Valid([]byte(order.ResultData)) {
|
||||
_ = json.Unmarshal([]byte(order.ResultData), &resultData)
|
||||
}
|
||||
providerOrderNo := firstNonEmpty(
|
||||
order.ProviderOrderNo,
|
||||
stringFromMap(resultData, "provider_order_no"),
|
||||
)
|
||||
message := "订单暂时正在履约中,请稍后查询"
|
||||
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded {
|
||||
message = "订单已履约成功"
|
||||
}
|
||||
return &DeliverySubmitResult{
|
||||
OrderNo: order.OrderNo,
|
||||
Status: order.FulfillmentStatus,
|
||||
Message: message,
|
||||
ProviderOrderNo: providerOrderNo,
|
||||
UpstreamOrder: resultData,
|
||||
}
|
||||
}
|
||||
|
||||
func optionalUint(value uint) *uint {
|
||||
if value == 0 {
|
||||
return nil
|
||||
}
|
||||
out := value
|
||||
return &out
|
||||
}
|
||||
|
||||
func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo string) (*DeliveryLinkResult, error) {
|
||||
orderNo = strings.TrimSpace(orderNo)
|
||||
if orderNo == "" {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
@@ -71,3 +75,171 @@ func TestDeliveryLinkGenerateAuthorizeAndRevoke(t *testing.T) {
|
||||
t.Fatalf("restored link should be accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliveryMerchantApiBindAndSubmit(t *testing.T) {
|
||||
db := newServiceTestDB(t)
|
||||
merchantID, _ := seedFulfillmentMerchant(t, db, "delivery-api", 5000, 5, 100)
|
||||
product := model.Product{Code: "delivery-api-alt-product", Name: "测试发货商品", Status: model.ProductStatusActive}
|
||||
if err := db.Create(&product).Error; err != nil {
|
||||
t.Fatalf("create product: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.MerchantProduct{
|
||||
MerchantID: merchantID,
|
||||
ProductID: product.ID,
|
||||
SKU: "suit_alan_walker",
|
||||
DisplayName: "测试发货商品",
|
||||
PriceAmount: 100,
|
||||
Currency: "POINT",
|
||||
Stock: 5,
|
||||
Status: model.ProductStatusActive,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create merchant product: %v", err)
|
||||
}
|
||||
|
||||
var bindCalls, boundCalls, queueCreateCalls, queuePatchCalls, upstreamCalls int32
|
||||
var requestLog []string
|
||||
mockBFF := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/sign-proxy" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
var payload struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
requestLog = append(requestLog, payload.Method+" "+payload.Path)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch payload.Path {
|
||||
case "/public/goods/detail":
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"good": map[string]interface{}{
|
||||
"title": "测试发货商品",
|
||||
"image": "https://img.example/good.png",
|
||||
},
|
||||
},
|
||||
})
|
||||
case "/public/games/bind-account":
|
||||
atomic.AddInt32(&bindCalls, 1)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"bind_uuid": "bind-123",
|
||||
"url": "https://bind.example/qrcode",
|
||||
},
|
||||
})
|
||||
case "/public/games/account-bound":
|
||||
atomic.AddInt32(&boundCalls, 1)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"gameAccount": map[string]interface{}{
|
||||
"game_account": "4808146277",
|
||||
"game_account_role_name": "巫师哈丁12",
|
||||
"game_account_area": "安卓",
|
||||
"game_account_plat": "QQ",
|
||||
"game_account_channel": "安卓-QQ",
|
||||
},
|
||||
},
|
||||
})
|
||||
case "/public/users/orders-queue":
|
||||
if payload.Method == http.MethodPost {
|
||||
atomic.AddInt32(&queueCreateCalls, 1)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"orders": []map[string]interface{}{
|
||||
{"_id": "queue-1"},
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if payload.Method == http.MethodPatch {
|
||||
atomic.AddInt32(&queuePatchCalls, 1)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"code": 0, "data": map[string]interface{}{}})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
case "/public/users/orders":
|
||||
atomic.AddInt32(&upstreamCalls, 1)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"order": map[string]interface{}{
|
||||
"order_no": "provider-1",
|
||||
},
|
||||
},
|
||||
})
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer mockBFF.Close()
|
||||
|
||||
fulfillmentSvc := NewFulfillmentService(db, nil)
|
||||
deliverySvc := NewDeliveryService(fulfillmentSvc, mockBFF.URL, "dlc", "https://shop.example", "link-secret", 60)
|
||||
|
||||
created, err := fulfillmentSvc.CreateOrder(CreateFulfillmentOrderInput{
|
||||
MerchantID: merchantID,
|
||||
APIClientID: 1,
|
||||
ClientOrderNo: "delivery-api-001",
|
||||
SKU: "suit_alan_walker",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create order: %v", err)
|
||||
}
|
||||
|
||||
info, err := deliverySvc.GetMerchantOrder(merchantID, created.Order.OrderNo)
|
||||
if err != nil {
|
||||
t.Fatalf("get merchant order: %v", err)
|
||||
}
|
||||
if !info.CanShip || info.Product == nil || info.Product.Image == "" {
|
||||
t.Fatalf("merchant order should include delivery data: %+v", info)
|
||||
}
|
||||
|
||||
bind, err := deliverySvc.BindForMerchant(merchantID, created.Order.OrderNo, "4808146277")
|
||||
if err != nil {
|
||||
t.Fatalf("bind for merchant: %v", err)
|
||||
}
|
||||
if bind.BindUUID != "bind-123" || bind.BindURL == "" || bind.QRURL == "" {
|
||||
t.Fatalf("unexpected bind result: %+v", bind)
|
||||
}
|
||||
|
||||
submitted, err := deliverySvc.SubmitForMerchant(merchantID, 88, created.Order.OrderNo, "4808146277", bind.BindUUID)
|
||||
if err != nil {
|
||||
t.Fatalf("submit for merchant: %v", err)
|
||||
}
|
||||
if submitted.Status != model.FulfillmentStatusProcessing || submitted.ProviderOrderNo != "provider-1" {
|
||||
t.Fatalf("unexpected submit result: %+v", submitted)
|
||||
}
|
||||
var afterSubmit model.FulfillmentOrder
|
||||
if err := db.Where("order_no = ?", created.Order.OrderNo).First(&afterSubmit).Error; err != nil {
|
||||
t.Fatalf("reload after submit: %v", err)
|
||||
}
|
||||
if afterSubmit.FulfillmentStatus != model.FulfillmentStatusProcessing || afterSubmit.ProviderOrderNo != "provider-1" {
|
||||
t.Fatalf("submit should persist processing state: %+v", afterSubmit)
|
||||
}
|
||||
if atomic.LoadInt32(&queueCreateCalls) != 1 || atomic.LoadInt32(&upstreamCalls) != 1 {
|
||||
t.Fatalf("first submit should only create one queue, queue=%d upstream=%d requests=%v", queueCreateCalls, upstreamCalls, requestLog)
|
||||
}
|
||||
second, err := deliverySvc.SubmitForMerchant(merchantID, 88, created.Order.OrderNo, "4808146277", bind.BindUUID)
|
||||
if err != nil {
|
||||
t.Fatalf("repeat submit for merchant: %v", err)
|
||||
}
|
||||
if second.ProviderOrderNo != "provider-1" || second.Status != model.FulfillmentStatusProcessing {
|
||||
t.Fatalf("repeat submit should reuse existing result: %+v", second)
|
||||
}
|
||||
if atomic.LoadInt32(&queueCreateCalls) != 1 || atomic.LoadInt32(&upstreamCalls) != 1 {
|
||||
t.Fatalf("submit should be idempotent, queue=%d upstream=%d requests=%v", queueCreateCalls, upstreamCalls, requestLog)
|
||||
}
|
||||
if atomic.LoadInt32(&bindCalls) != 1 || atomic.LoadInt32(&queuePatchCalls) != 1 || atomic.LoadInt32(&boundCalls) != 1 {
|
||||
t.Fatalf("unexpected mock counts bind=%d bound=%d patch=%d", bindCalls, boundCalls, queuePatchCalls)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user