Files
dujiao-next/internal/app/httpserver/order_refund_handler_test.go
T

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"])
}
}