实现多商户履约平台基础
This commit is contained in:
@@ -0,0 +1,530 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type FulfillmentService struct {
|
||||
db *gorm.DB
|
||||
callbacks *CallbackService
|
||||
}
|
||||
|
||||
func NewFulfillmentService(db *gorm.DB, callbacks *CallbackService) *FulfillmentService {
|
||||
return &FulfillmentService{db: db, callbacks: callbacks}
|
||||
}
|
||||
|
||||
type CreateFulfillmentOrderInput struct {
|
||||
MerchantID uint
|
||||
APIClientID uint
|
||||
ClientOrderNo string
|
||||
SKU string
|
||||
Quantity int64
|
||||
BuyerReference string
|
||||
RequestData interface{}
|
||||
}
|
||||
|
||||
type CreateFulfillmentOrderResult struct {
|
||||
Order *model.FulfillmentOrder `json:"order"`
|
||||
Idempotent bool `json:"idempotent"`
|
||||
}
|
||||
|
||||
func (s *FulfillmentService) CreateOrder(in CreateFulfillmentOrderInput) (*CreateFulfillmentOrderResult, error) {
|
||||
in.ClientOrderNo = strings.TrimSpace(in.ClientOrderNo)
|
||||
in.SKU = strings.TrimSpace(in.SKU)
|
||||
if in.MerchantID == 0 || in.APIClientID == 0 {
|
||||
return nil, errors.New("无效的商户或 API 客户端")
|
||||
}
|
||||
if in.ClientOrderNo == "" || len(in.ClientOrderNo) > 96 {
|
||||
return nil, errors.New("client_order_no 不能为空且最长 96 位")
|
||||
}
|
||||
if in.SKU == "" {
|
||||
return nil, errors.New("sku 不能为空")
|
||||
}
|
||||
if in.Quantity == 0 {
|
||||
in.Quantity = 1
|
||||
}
|
||||
if in.Quantity < 1 {
|
||||
return nil, errors.New("quantity 必须大于零")
|
||||
}
|
||||
requestData := ""
|
||||
if in.RequestData != nil {
|
||||
raw, err := json.Marshal(in.RequestData)
|
||||
if err != nil {
|
||||
return nil, errors.New("订单请求数据无法序列化")
|
||||
}
|
||||
requestData = string(raw)
|
||||
}
|
||||
|
||||
result := &CreateFulfillmentOrderResult{}
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var existing model.FulfillmentOrder
|
||||
err := tx.Where("merchant_id = ? AND client_order_no = ?", in.MerchantID, in.ClientOrderNo).First(&existing).Error
|
||||
if err == nil {
|
||||
result.Order = &existing
|
||||
result.Idempotent = true
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
var product model.MerchantProduct
|
||||
if err := tx.Preload("Product").Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("merchant_id = ? AND sku = ? AND status = ?", in.MerchantID, in.SKU, model.ProductStatusActive).
|
||||
First(&product).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("商品不存在或已下架")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if product.Product == nil || product.Product.Status != model.ProductStatusActive {
|
||||
return errors.New("商品目录已下架")
|
||||
}
|
||||
if product.Stock >= 0 && product.Stock < in.Quantity {
|
||||
return errors.New("商品库存不足")
|
||||
}
|
||||
if product.PriceAmount > 0 && in.Quantity > math.MaxInt64/product.PriceAmount {
|
||||
return errors.New("订单金额超出范围")
|
||||
}
|
||||
totalAmount := product.PriceAmount * in.Quantity
|
||||
|
||||
var wallet model.WalletAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("merchant_id = ?", in.MerchantID).First(&wallet).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if wallet.AvailableBalance < totalAmount {
|
||||
return errors.New("商户钱包余额不足")
|
||||
}
|
||||
newBalance := wallet.AvailableBalance - totalAmount
|
||||
if err := tx.Model(&wallet).Update("available_balance", newBalance).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
order := &model.FulfillmentOrder{
|
||||
MerchantID: in.MerchantID,
|
||||
OrderNo: newFulfillmentOrderNo(),
|
||||
ClientOrderNo: in.ClientOrderNo,
|
||||
MerchantProductID: product.ID,
|
||||
ProductSKU: product.SKU,
|
||||
ProductName: fallbackName(product.DisplayName, product.Product.Name),
|
||||
Quantity: in.Quantity,
|
||||
Amount: totalAmount,
|
||||
Currency: product.Currency,
|
||||
PaymentStatus: model.PaymentStatusPaid,
|
||||
FulfillmentStatus: model.FulfillmentStatusPending,
|
||||
BuyerReference: in.BuyerReference,
|
||||
RequestData: requestData,
|
||||
}
|
||||
if err := tx.Create(order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
idempotencyKey := in.ClientOrderNo
|
||||
if err := tx.Create(&model.WalletLedgerEntry{
|
||||
MerchantID: in.MerchantID,
|
||||
WalletAccountID: wallet.ID,
|
||||
EntryNo: "WL" + uuid.NewString(),
|
||||
Type: model.WalletLedgerDebit,
|
||||
Amount: -totalAmount,
|
||||
BalanceAfter: newBalance,
|
||||
ReferenceType: "fulfillment_order",
|
||||
ReferenceNo: order.OrderNo,
|
||||
IdempotencyKey: &idempotencyKey,
|
||||
Note: "开放接口下单扣款",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if product.Stock >= 0 {
|
||||
if err := tx.Model(&model.MerchantProduct{}).Where("id = ? AND stock >= ?", product.ID, in.Quantity).
|
||||
Update("stock", gorm.Expr("stock - ?", in.Quantity)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Create(&model.FulfillmentJob{
|
||||
MerchantID: in.MerchantID,
|
||||
OrderID: order.ID,
|
||||
Status: model.FulfillmentJobStatusPending,
|
||||
NextRunAt: time.Now(),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeAudit(tx, &in.MerchantID, nil, &in.APIClientID, "open_order.create", "fulfillment_order", order.OrderNo, map[string]interface{}{"client_order_no": in.ClientOrderNo, "sku": in.SKU}); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.callbacks != nil {
|
||||
if err := s.callbacks.Enqueue(tx, in.MerchantID, "order.created", orderCallbackData(order)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
result.Order = order
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
// 并发请求恰好同时通过首次查询时,唯一约束冲突后返回既有订单。
|
||||
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "UNIQUE") {
|
||||
var existing model.FulfillmentOrder
|
||||
if queryErr := s.db.Where("merchant_id = ? AND client_order_no = ?", in.MerchantID, in.ClientOrderNo).First(&existing).Error; queryErr == nil {
|
||||
return &CreateFulfillmentOrderResult{Order: &existing, Idempotent: true}, nil
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *FulfillmentService) GetOrder(merchantID uint, orderNo string) (*model.FulfillmentOrder, error) {
|
||||
var order model.FulfillmentOrder
|
||||
err := s.db.Preload("MerchantProduct.Product").
|
||||
Where("merchant_id = ? AND order_no = ?", merchantID, orderNo).
|
||||
First(&order).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("订单不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (s *FulfillmentService) ListOrders(merchantID uint, page, size int, fulfillmentStatus string) ([]model.FulfillmentOrder, int64, error) {
|
||||
page, size = normalizePage(page, size)
|
||||
tx := s.db.Model(&model.FulfillmentOrder{}).Where("merchant_id = ?", merchantID)
|
||||
if fulfillmentStatus != "" {
|
||||
tx = tx.Where("fulfillment_status = ?", fulfillmentStatus)
|
||||
}
|
||||
var total int64
|
||||
if err := tx.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var orders []model.FulfillmentOrder
|
||||
err := tx.Preload("MerchantProduct.Product").Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&orders).Error
|
||||
return orders, total, err
|
||||
}
|
||||
|
||||
type FulfillmentUpdateInput struct {
|
||||
MerchantID uint
|
||||
APIClientID uint
|
||||
OrderNo string
|
||||
Status string
|
||||
ProviderOrderNo string
|
||||
FailureReason string
|
||||
ResultData interface{}
|
||||
}
|
||||
|
||||
func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*model.FulfillmentOrder, error) {
|
||||
switch in.Status {
|
||||
case model.FulfillmentStatusProcessing, model.FulfillmentStatusSucceeded, model.FulfillmentStatusFailed:
|
||||
default:
|
||||
return nil, errors.New("无效的履约状态")
|
||||
}
|
||||
resultData := ""
|
||||
if in.ResultData != nil {
|
||||
raw, err := json.Marshal(in.ResultData)
|
||||
if err != nil {
|
||||
return nil, errors.New("履约结果无法序列化")
|
||||
}
|
||||
resultData = string(raw)
|
||||
}
|
||||
var out model.FulfillmentOrder
|
||||
err := s.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 = ?", in.MerchantID, in.OrderNo).
|
||||
First(&order).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("订单不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if order.FulfillmentStatus == model.FulfillmentStatusCancelled {
|
||||
return errors.New("订单已取消,不能更新履约状态")
|
||||
}
|
||||
if order.PaymentStatus != model.PaymentStatusPaid {
|
||||
return errors.New("订单未支付,不能履约")
|
||||
}
|
||||
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded && in.Status == model.FulfillmentStatusSucceeded {
|
||||
out = order
|
||||
return nil
|
||||
}
|
||||
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded {
|
||||
return errors.New("订单已履约成功,不能回退状态")
|
||||
}
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
"fulfillment_status": in.Status,
|
||||
"result_data": resultData,
|
||||
}
|
||||
if in.ProviderOrderNo != "" {
|
||||
updates["provider_order_no"] = in.ProviderOrderNo
|
||||
}
|
||||
switch in.Status {
|
||||
case model.FulfillmentStatusSucceeded:
|
||||
updates["delivered_at"] = now
|
||||
updates["failure_reason"] = ""
|
||||
case model.FulfillmentStatusFailed:
|
||||
updates["failure_reason"] = in.FailureReason
|
||||
}
|
||||
if err := tx.Model(&order).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
jobStatus := model.FulfillmentJobStatusProcessing
|
||||
if in.Status == model.FulfillmentStatusSucceeded {
|
||||
jobStatus = model.FulfillmentJobStatusSucceeded
|
||||
} else if in.Status == model.FulfillmentStatusFailed {
|
||||
jobStatus = model.FulfillmentJobStatusFailed
|
||||
}
|
||||
if err := tx.Model(&model.FulfillmentJob{}).Where("order_id = ?", order.ID).Updates(map[string]interface{}{
|
||||
"status": jobStatus,
|
||||
"provider_order_no": in.ProviderOrderNo,
|
||||
"result_payload": resultData,
|
||||
"last_error": in.FailureReason,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.First(&out, order.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeAudit(tx, &in.MerchantID, nil, &in.APIClientID, "fulfillment.update", "fulfillment_order", order.OrderNo, map[string]string{"status": in.Status}); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.callbacks != nil {
|
||||
if err := s.callbacks.Enqueue(tx, in.MerchantID, "order.fulfillment.updated", orderCallbackData(&out)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo, reason string) (*model.FulfillmentOrder, error) {
|
||||
var out model.FulfillmentOrder
|
||||
err := s.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.FulfillmentStatusCancelled {
|
||||
out = order
|
||||
return nil
|
||||
}
|
||||
if order.FulfillmentStatus == model.FulfillmentStatusProcessing || order.FulfillmentStatus == model.FulfillmentStatusSucceeded {
|
||||
return errors.New("订单已进入履约流程,不能取消")
|
||||
}
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
"payment_status": model.PaymentStatusRefunded,
|
||||
"fulfillment_status": model.FulfillmentStatusCancelled,
|
||||
"failure_reason": reason,
|
||||
"cancelled_at": now,
|
||||
}
|
||||
if err := tx.Model(&order).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var wallet model.WalletAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("merchant_id = ?", merchantID).First(&wallet).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
newBalance := wallet.AvailableBalance + order.Amount
|
||||
if err := tx.Model(&wallet).Update("available_balance", newBalance).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
idempotencyKey := "cancel:" + order.OrderNo
|
||||
if err := tx.Create(&model.WalletLedgerEntry{
|
||||
MerchantID: merchantID,
|
||||
WalletAccountID: wallet.ID,
|
||||
EntryNo: "WL" + uuid.NewString(),
|
||||
Type: model.WalletLedgerRefund,
|
||||
Amount: order.Amount,
|
||||
BalanceAfter: newBalance,
|
||||
ReferenceType: "fulfillment_order",
|
||||
ReferenceNo: order.OrderNo,
|
||||
IdempotencyKey: &idempotencyKey,
|
||||
Note: "订单取消退款",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var product model.MerchantProduct
|
||||
if err := tx.Where("id = ?", order.MerchantProductID).First(&product).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if product.Stock >= 0 {
|
||||
if err := tx.Model(&product).Update("stock", gorm.Expr("stock + ?", order.Quantity)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Model(&model.FulfillmentJob{}).Where("order_id = ?", order.ID).Updates(map[string]interface{}{
|
||||
"status": model.FulfillmentJobStatusFailed,
|
||||
"last_error": "订单已取消",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.First(&out, order.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeAudit(tx, &merchantID, nil, &apiClientID, "open_order.cancel", "fulfillment_order", order.OrderNo, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.callbacks != nil {
|
||||
if err := s.callbacks.Enqueue(tx, merchantID, "order.cancelled", orderCallbackData(&out)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
type WalletAdjustInput struct {
|
||||
MerchantID uint
|
||||
ActorUserID uint
|
||||
Amount int64
|
||||
IdempotencyKey string
|
||||
Note string
|
||||
}
|
||||
|
||||
func (s *FulfillmentService) AdjustWallet(in WalletAdjustInput) (*model.WalletAccount, error) {
|
||||
if in.Amount == 0 {
|
||||
return nil, errors.New("调整金额不能为零")
|
||||
}
|
||||
if in.IdempotencyKey == "" {
|
||||
return nil, errors.New("账务调整必须提供幂等键")
|
||||
}
|
||||
var out model.WalletAccount
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var existing model.WalletLedgerEntry
|
||||
if err := tx.Where("merchant_id = ? AND idempotency_key = ?", in.MerchantID, in.IdempotencyKey).First(&existing).Error; err == nil {
|
||||
if err := tx.First(&out, existing.WalletAccountID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
var wallet model.WalletAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("merchant_id = ?", in.MerchantID).First(&wallet).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
newBalance := wallet.AvailableBalance + in.Amount
|
||||
if newBalance < 0 {
|
||||
return errors.New("调整后余额不能小于零")
|
||||
}
|
||||
if err := tx.Model(&wallet).Update("available_balance", newBalance).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
entryType := model.WalletLedgerAdjust
|
||||
if in.Amount > 0 {
|
||||
entryType = model.WalletLedgerCredit
|
||||
} else {
|
||||
entryType = model.WalletLedgerDebit
|
||||
}
|
||||
idempotencyKey := in.IdempotencyKey
|
||||
if err := tx.Create(&model.WalletLedgerEntry{
|
||||
MerchantID: in.MerchantID,
|
||||
WalletAccountID: wallet.ID,
|
||||
EntryNo: "WL" + uuid.NewString(),
|
||||
Type: entryType,
|
||||
Amount: in.Amount,
|
||||
BalanceAfter: newBalance,
|
||||
ReferenceType: "manual_adjustment",
|
||||
ReferenceNo: in.IdempotencyKey,
|
||||
IdempotencyKey: &idempotencyKey,
|
||||
Note: in.Note,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
out = wallet
|
||||
out.AvailableBalance = newBalance
|
||||
return writeAudit(tx, &in.MerchantID, &in.ActorUserID, nil, "wallet.adjust", "wallet_account", fmt.Sprint(wallet.ID), map[string]int64{"amount": in.Amount})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (s *FulfillmentService) GetWallet(merchantID uint) (*model.WalletAccount, error) {
|
||||
var wallet model.WalletAccount
|
||||
if err := s.db.Where("merchant_id = ?", merchantID).First(&wallet).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("商户钱包不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &wallet, nil
|
||||
}
|
||||
|
||||
func (s *FulfillmentService) ListWalletLedger(merchantID uint, page, size int) ([]model.WalletLedgerEntry, int64, error) {
|
||||
page, size = normalizePage(page, size)
|
||||
tx := s.db.Model(&model.WalletLedgerEntry{}).Where("merchant_id = ?", merchantID)
|
||||
var total int64
|
||||
if err := tx.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var entries []model.WalletLedgerEntry
|
||||
err := tx.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&entries).Error
|
||||
return entries, total, err
|
||||
}
|
||||
|
||||
func newFulfillmentOrderNo() string {
|
||||
return "FO" + time.Now().UTC().Format("20060102150405") + strings.ReplaceAll(uuid.NewString()[:12], "-", "")
|
||||
}
|
||||
|
||||
func CanFulfill(order *model.FulfillmentOrder) (bool, string) {
|
||||
if order.PaymentStatus != model.PaymentStatusPaid {
|
||||
return false, "订单未支付或已退款"
|
||||
}
|
||||
switch order.FulfillmentStatus {
|
||||
case model.FulfillmentStatusPending, model.FulfillmentStatusFailed:
|
||||
return true, ""
|
||||
case model.FulfillmentStatusProcessing:
|
||||
return false, "订单履约中"
|
||||
case model.FulfillmentStatusSucceeded:
|
||||
return false, "订单已履约成功"
|
||||
case model.FulfillmentStatusCancelled:
|
||||
return false, "订单已取消"
|
||||
default:
|
||||
return false, "订单状态不可履约"
|
||||
}
|
||||
}
|
||||
|
||||
func orderCallbackData(order *model.FulfillmentOrder) map[string]interface{} {
|
||||
canFulfill, cannotFulfillReason := CanFulfill(order)
|
||||
return map[string]interface{}{
|
||||
"order_no": order.OrderNo,
|
||||
"client_order_no": order.ClientOrderNo,
|
||||
"product_sku": order.ProductSKU,
|
||||
"quantity": order.Quantity,
|
||||
"amount": order.Amount,
|
||||
"currency": order.Currency,
|
||||
"payment_status": order.PaymentStatus,
|
||||
"fulfillment_status": order.FulfillmentStatus,
|
||||
"can_fulfill": canFulfill,
|
||||
"cannot_fulfill_reason": cannotFulfillReason,
|
||||
"provider_order_no": order.ProviderOrderNo,
|
||||
"failure_reason": order.FailureReason,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user