375 lines
13 KiB
Go
375 lines
13 KiB
Go
package httpserver
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
fulfillmentdomain "github.com/dujiao-next/internal/modules/fulfillment/domain"
|
|
orderdomain "github.com/dujiao-next/internal/modules/order/domain"
|
|
|
|
walletdomain "github.com/dujiao-next/internal/modules/wallet/domain"
|
|
|
|
affiliateapp "github.com/dujiao-next/internal/modules/affiliate/application"
|
|
affiliatedomain "github.com/dujiao-next/internal/modules/affiliate/domain"
|
|
affiliategormstore "github.com/dujiao-next/internal/modules/affiliate/infrastructure/gormstore"
|
|
orderrefund "github.com/dujiao-next/internal/modules/order/application/refund"
|
|
ordergormstore "github.com/dujiao-next/internal/modules/order/infrastructure/gormstore"
|
|
|
|
userstore "github.com/dujiao-next/internal/modules/identity/user/infrastructure/gormstore"
|
|
|
|
userdomain "github.com/dujiao-next/internal/modules/identity/user/domain"
|
|
|
|
"github.com/dujiao-next/internal/app/container"
|
|
orderwiring "github.com/dujiao-next/internal/bootstrap/order"
|
|
"github.com/dujiao-next/internal/constants"
|
|
externalidentitydomain "github.com/dujiao-next/internal/modules/identity/externalidentity/domain"
|
|
ordertransport "github.com/dujiao-next/internal/modules/order/transport/http"
|
|
"github.com/dujiao-next/internal/shared/jsonmap"
|
|
"github.com/dujiao-next/internal/shared/money"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"github.com/shopspring/decimal"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type adminOrderRefundFixture struct {
|
|
MemberUserID uint
|
|
MemberOrderID uint
|
|
GuestOrderID uint
|
|
ManualRefundID uint
|
|
WalletRefundID uint
|
|
}
|
|
|
|
func setupAdminOrderRefundHandlerTest(t *testing.T) (*ordertransport.AdminRefundHandler, *gorm.DB) {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
dsn := fmt.Sprintf("file:admin_order_refund_handler_test_%d?mode=memory&cache=shared", time.Now().UnixNano())
|
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite failed: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(
|
|
&userdomain.User{},
|
|
&externalidentitydomain.Identity{},
|
|
&orderdomain.Order{},
|
|
&orderdomain.OrderItem{},
|
|
&fulfillmentdomain.Fulfillment{},
|
|
&orderdomain.OrderRefundRecord{},
|
|
&affiliatedomain.Profile{},
|
|
&affiliatedomain.Commission{},
|
|
&affiliatedomain.WithdrawRequest{},
|
|
&walletdomain.Account{},
|
|
&walletdomain.Transaction{},
|
|
); err != nil {
|
|
t.Fatalf("auto migrate failed: %v", err)
|
|
}
|
|
|
|
orderRepo := ordergormstore.New(db, "test-guest-credential-secret-with-32-bytes")
|
|
userRepo := userstore.New(db)
|
|
affiliateSvc := affiliateapp.NewService(affiliategormstore.New(db), nil, nil, nil, nil)
|
|
orderRefundService := orderrefund.New(orderRepo, userRepo, affiliateSvc, nil, nil)
|
|
|
|
return orderwiring.NewAdminRefundHandler(&container.Container{
|
|
OrderStore: orderRepo,
|
|
OrderRefundService: orderRefundService,
|
|
}), db
|
|
}
|
|
|
|
func seedAdminOrderRefundData(t *testing.T, db *gorm.DB) adminOrderRefundFixture {
|
|
t.Helper()
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
|
|
member := &userdomain.User{
|
|
Email: "refund-member@example.com",
|
|
DisplayName: "Refund Member",
|
|
PasswordHash: "hash",
|
|
Status: constants.UserStatusActive,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if err := db.Create(member).Error; err != nil {
|
|
t.Fatalf("create member user failed: %v", err)
|
|
}
|
|
if err := db.Create(&externalidentitydomain.Identity{
|
|
UserID: member.ID,
|
|
Provider: "telegram",
|
|
ProviderUserID: "700001",
|
|
Username: "refund_member_tg",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}).Error; err != nil {
|
|
t.Fatalf("create oauth identity failed: %v", err)
|
|
}
|
|
|
|
memberOrder := &orderdomain.Order{
|
|
OrderNo: "DJ-ADMIN-REFUND-ORDER-1",
|
|
UserID: member.ID,
|
|
Status: constants.OrderStatusPartiallyRefunded,
|
|
Currency: "CNY",
|
|
OriginalAmount: money.FromDecimal(decimal.NewFromInt(100)),
|
|
DiscountAmount: money.FromDecimal(decimal.Zero),
|
|
TotalAmount: money.FromDecimal(decimal.NewFromInt(100)),
|
|
WalletPaidAmount: money.FromDecimal(decimal.Zero),
|
|
OnlinePaidAmount: money.FromDecimal(decimal.NewFromInt(100)),
|
|
RefundedAmount: money.FromDecimal(decimal.NewFromInt(10)),
|
|
CreatedAt: now.Add(-2 * time.Hour),
|
|
UpdatedAt: now.Add(-2 * time.Hour),
|
|
}
|
|
guestOrder := &orderdomain.Order{
|
|
OrderNo: "DJ-ADMIN-REFUND-ORDER-2",
|
|
UserID: 0,
|
|
GuestEmail: "refund-guest@example.com",
|
|
GuestLocale: constants.LocaleZhCN,
|
|
Status: constants.OrderStatusPartiallyRefunded,
|
|
Currency: "CNY",
|
|
OriginalAmount: money.FromDecimal(decimal.NewFromInt(80)),
|
|
DiscountAmount: money.FromDecimal(decimal.Zero),
|
|
TotalAmount: money.FromDecimal(decimal.NewFromInt(80)),
|
|
WalletPaidAmount: money.FromDecimal(decimal.Zero),
|
|
OnlinePaidAmount: money.FromDecimal(decimal.NewFromInt(80)),
|
|
RefundedAmount: money.FromDecimal(decimal.NewFromInt(20)),
|
|
CreatedAt: now.Add(-90 * time.Minute),
|
|
UpdatedAt: now.Add(-90 * time.Minute),
|
|
}
|
|
for _, order := range []*orderdomain.Order{memberOrder, guestOrder} {
|
|
if err := db.Create(order).Error; err != nil {
|
|
t.Fatalf("create order failed: %v", err)
|
|
}
|
|
}
|
|
|
|
items := []orderdomain.OrderItem{
|
|
{
|
|
OrderID: memberOrder.ID,
|
|
ProductID: 1,
|
|
TitleJSON: jsonmap.JSON{"zh-CN": "会员退款测试商品"},
|
|
UnitPrice: money.FromDecimal(decimal.NewFromInt(100)),
|
|
CostPrice: money.FromDecimal(decimal.NewFromInt(50)),
|
|
Quantity: 1,
|
|
TotalPrice: money.FromDecimal(decimal.NewFromInt(100)),
|
|
CouponDiscount: money.FromDecimal(decimal.Zero),
|
|
FulfillmentType: constants.FulfillmentTypeManual,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
{
|
|
OrderID: guestOrder.ID,
|
|
ProductID: 2,
|
|
TitleJSON: jsonmap.JSON{"zh-CN": "游客退款测试商品"},
|
|
UnitPrice: money.FromDecimal(decimal.NewFromInt(80)),
|
|
CostPrice: money.FromDecimal(decimal.NewFromInt(40)),
|
|
Quantity: 1,
|
|
TotalPrice: money.FromDecimal(decimal.NewFromInt(80)),
|
|
CouponDiscount: money.FromDecimal(decimal.Zero),
|
|
FulfillmentType: constants.FulfillmentTypeManual,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
}
|
|
for idx := range items {
|
|
if err := db.Create(&items[idx]).Error; err != nil {
|
|
t.Fatalf("create order item failed: %v", err)
|
|
}
|
|
}
|
|
|
|
manualRefund := &orderdomain.OrderRefundRecord{
|
|
UserID: member.ID,
|
|
OrderID: memberOrder.ID,
|
|
Type: constants.OrderRefundTypeManual,
|
|
Amount: money.FromDecimal(decimal.NewFromInt(10)),
|
|
Currency: "CNY",
|
|
Remark: "manual refund reason",
|
|
CreatedAt: now.Add(-40 * time.Minute),
|
|
UpdatedAt: now.Add(-40 * time.Minute),
|
|
GuestEmail: "",
|
|
}
|
|
walletRefund := &orderdomain.OrderRefundRecord{
|
|
UserID: 0,
|
|
GuestEmail: "refund-guest@example.com",
|
|
OrderID: guestOrder.ID,
|
|
Type: constants.OrderRefundTypeWallet,
|
|
Amount: money.FromDecimal(decimal.NewFromInt(20)),
|
|
Currency: "CNY",
|
|
Remark: "wallet refund reason",
|
|
CreatedAt: now.Add(-20 * time.Minute),
|
|
UpdatedAt: now.Add(-20 * time.Minute),
|
|
}
|
|
for _, record := range []*orderdomain.OrderRefundRecord{manualRefund, walletRefund} {
|
|
if err := db.Create(record).Error; err != nil {
|
|
t.Fatalf("create refund record failed: %v", err)
|
|
}
|
|
}
|
|
|
|
return adminOrderRefundFixture{
|
|
MemberUserID: member.ID,
|
|
MemberOrderID: memberOrder.ID,
|
|
GuestOrderID: guestOrder.ID,
|
|
ManualRefundID: manualRefund.ID,
|
|
WalletRefundID: walletRefund.ID,
|
|
}
|
|
}
|
|
|
|
func TestGetAdminOrderRefundsFiltersByUserKeywordAndProduct(t *testing.T) {
|
|
h, db := setupAdminOrderRefundHandlerTest(t)
|
|
fixture := seedAdminOrderRefundData(t, db)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(
|
|
http.MethodGet,
|
|
"/admin/order-refunds?page=1&page_size=20&user_keyword=refund_member_tg&product_keyword=会员退款测试商品",
|
|
nil,
|
|
)
|
|
|
|
h.GetAdminOrderRefunds(c)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status want 200 got %d", w.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
StatusCode int `json:"status_code"`
|
|
Pagination struct {
|
|
Total int64 `json:"total"`
|
|
} `json:"pagination"`
|
|
Data []map[string]interface{} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal response failed: %v", err)
|
|
}
|
|
if resp.StatusCode != 0 {
|
|
t.Fatalf("status_code want 0 got %d", resp.StatusCode)
|
|
}
|
|
if resp.Pagination.Total != 1 || len(resp.Data) != 1 {
|
|
t.Fatalf("unexpected list result: total=%d len=%d", resp.Pagination.Total, len(resp.Data))
|
|
}
|
|
|
|
row := resp.Data[0]
|
|
if uint(row["id"].(float64)) != fixture.ManualRefundID {
|
|
t.Fatalf("unexpected refund id: %+v", row["id"])
|
|
}
|
|
if row["refund_type_label"] != "manual" {
|
|
t.Fatalf("unexpected refund_type_label: %+v", row["refund_type_label"])
|
|
}
|
|
if row["order_no"] != "DJ-ADMIN-REFUND-ORDER-1" {
|
|
t.Fatalf("unexpected order_no: %+v", row["order_no"])
|
|
}
|
|
if row["user_email"] != "refund-member@example.com" {
|
|
t.Fatalf("unexpected user_email: %+v", row["user_email"])
|
|
}
|
|
items, ok := row["items"].([]interface{})
|
|
if !ok || len(items) == 0 {
|
|
t.Fatalf("items should not be empty: %+v", row["items"])
|
|
}
|
|
}
|
|
|
|
func TestGetAdminOrderRefundsGuestRowUsesGuestEmail(t *testing.T) {
|
|
h, db := setupAdminOrderRefundHandlerTest(t)
|
|
fixture := seedAdminOrderRefundData(t, db)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(
|
|
http.MethodGet,
|
|
"/admin/order-refunds?page=1&page_size=20&guest_email=refund-guest@example.com",
|
|
nil,
|
|
)
|
|
|
|
h.GetAdminOrderRefunds(c)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status want 200 got %d", w.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
StatusCode int `json:"status_code"`
|
|
Data []map[string]interface{} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal response failed: %v", err)
|
|
}
|
|
if resp.StatusCode != 0 {
|
|
t.Fatalf("status_code want 0 got %d", resp.StatusCode)
|
|
}
|
|
if len(resp.Data) != 1 {
|
|
t.Fatalf("unexpected data size: %d", len(resp.Data))
|
|
}
|
|
|
|
row := resp.Data[0]
|
|
if uint(row["id"].(float64)) != fixture.WalletRefundID {
|
|
t.Fatalf("unexpected refund id: %+v", row["id"])
|
|
}
|
|
if row["guest_email"] != "refund-guest@example.com" {
|
|
t.Fatalf("unexpected guest_email: %+v", row["guest_email"])
|
|
}
|
|
if row["guest_locale"] != constants.LocaleZhCN {
|
|
t.Fatalf("unexpected guest_locale: %+v", row["guest_locale"])
|
|
}
|
|
if row["user_email"] != nil {
|
|
t.Fatalf("guest refund should not include user_email: %+v", row["user_email"])
|
|
}
|
|
if row["user_display_name"] != nil {
|
|
t.Fatalf("guest refund should not include user_display_name: %+v", row["user_display_name"])
|
|
}
|
|
}
|
|
|
|
func TestGetAdminOrderRefundDetailIncludesOrderIDAndGuestEmail(t *testing.T) {
|
|
h, db := setupAdminOrderRefundHandlerTest(t)
|
|
fixture := seedAdminOrderRefundData(t, db)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", fixture.WalletRefundID)}}
|
|
c.Request = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/admin/order-refunds/%d", fixture.WalletRefundID), nil)
|
|
|
|
h.GetAdminOrderRefund(c)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status want 200 got %d", w.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
StatusCode int `json:"status_code"`
|
|
Data map[string]interface{} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal response failed: %v", err)
|
|
}
|
|
if resp.StatusCode != 0 {
|
|
t.Fatalf("status_code want 0 got %d", resp.StatusCode)
|
|
}
|
|
if uint(resp.Data["id"].(float64)) != fixture.WalletRefundID {
|
|
t.Fatalf("unexpected refund id: %+v", resp.Data["id"])
|
|
}
|
|
if resp.Data["refund_type_label"] != "wallet" {
|
|
t.Fatalf("unexpected refund_type_label: %+v", resp.Data["refund_type_label"])
|
|
}
|
|
if uint(resp.Data["order_id"].(float64)) != fixture.GuestOrderID {
|
|
t.Fatalf("unexpected order_id: %+v", resp.Data["order_id"])
|
|
}
|
|
if resp.Data["guest_email"] != "refund-guest@example.com" {
|
|
t.Fatalf("unexpected guest_email: %+v", resp.Data["guest_email"])
|
|
}
|
|
if resp.Data["guest_locale"] != constants.LocaleZhCN {
|
|
t.Fatalf("unexpected guest_locale: %+v", resp.Data["guest_locale"])
|
|
}
|
|
if resp.Data["user_email"] != nil {
|
|
t.Fatalf("guest refund should not include user_email: %+v", resp.Data["user_email"])
|
|
}
|
|
if resp.Data["user_display_name"] != nil {
|
|
t.Fatalf("guest refund should not include user_display_name: %+v", resp.Data["user_display_name"])
|
|
}
|
|
if resp.Data["remark"] != "wallet refund reason" {
|
|
t.Fatalf("unexpected remark: %+v", resp.Data["remark"])
|
|
}
|
|
detailItems, ok := resp.Data["items"].([]interface{})
|
|
if !ok || len(detailItems) == 0 {
|
|
t.Fatalf("items should not be empty: %+v", resp.Data["items"])
|
|
}
|
|
}
|