387 lines
13 KiB
Go
387 lines
13 KiB
Go
package services
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
"fmt"
|
||
"testing"
|
||
"time"
|
||
|
||
"quyun/v2/app/commands/testx"
|
||
"quyun/v2/app/errorx"
|
||
"quyun/v2/app/http/tenant/dto"
|
||
"quyun/v2/database"
|
||
"quyun/v2/database/models"
|
||
"quyun/v2/pkg/consts"
|
||
|
||
"github.com/samber/lo"
|
||
. "github.com/smartystreets/goconvey/convey"
|
||
"github.com/stretchr/testify/suite"
|
||
|
||
_ "go.ipao.vip/atom"
|
||
"go.ipao.vip/atom/contracts"
|
||
"go.ipao.vip/gen/types"
|
||
"go.uber.org/dig"
|
||
)
|
||
|
||
type LedgerTestSuiteInjectParams struct {
|
||
dig.In
|
||
|
||
DB *sql.DB
|
||
Initials []contracts.Initial `group:"initials"` // nolint:structcheck
|
||
}
|
||
|
||
type LedgerTestSuite struct {
|
||
suite.Suite
|
||
|
||
LedgerTestSuiteInjectParams
|
||
}
|
||
|
||
func Test_Ledger(t *testing.T) {
|
||
providers := testx.Default().With(Provide)
|
||
|
||
testx.Serve(providers, t, func(p LedgerTestSuiteInjectParams) {
|
||
suite.Run(t, &LedgerTestSuite{LedgerTestSuiteInjectParams: p})
|
||
})
|
||
}
|
||
|
||
func (s *LedgerTestSuite) seedTenantUser(ctx context.Context, tenantID, userID, balance, frozen int64) {
|
||
database.Truncate(ctx, s.DB, models.TableNameTenantLedger, models.TableNameTenantUser, models.TableNameUser)
|
||
|
||
now := time.Now().UTC()
|
||
_, err := s.DB.ExecContext(ctx, `
|
||
INSERT INTO users (id, username, password, roles, status, metas, created_at, updated_at, balance, balance_frozen)
|
||
VALUES ($1, $2, 'x', ARRAY['user'], $3, '{}'::jsonb, $4, $4, $5, $6)
|
||
ON CONFLICT (id) DO UPDATE
|
||
SET balance = EXCLUDED.balance, balance_frozen = EXCLUDED.balance_frozen, updated_at = EXCLUDED.updated_at
|
||
`, userID, fmt.Sprintf("u%d", userID), consts.UserStatusVerified, now, balance, frozen)
|
||
So(err, ShouldBeNil)
|
||
|
||
tu := &models.TenantUser{
|
||
TenantID: tenantID,
|
||
UserID: userID,
|
||
Role: types.NewArray([]consts.TenantUserRole{consts.TenantUserRoleMember}),
|
||
Status: consts.UserStatusVerified,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
So(tu.Create(ctx), ShouldBeNil)
|
||
}
|
||
|
||
func (s *LedgerTestSuite) Test_Freeze() {
|
||
Convey("Ledger.Freeze", s.T(), func() {
|
||
ctx := s.T().Context()
|
||
tenantID := int64(1)
|
||
userID := int64(2)
|
||
now := time.Now().UTC()
|
||
|
||
s.seedTenantUser(ctx, tenantID, userID, 1000, 0)
|
||
|
||
Convey("金额非法应返回参数错误", func() {
|
||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 0, "k_freeze_invalid_amount", "freeze", now)
|
||
So(err, ShouldNotBeNil)
|
||
|
||
var appErr *errorx.AppError
|
||
So(errors.As(err, &appErr), ShouldBeTrue)
|
||
So(appErr.Code, ShouldEqual, errorx.CodeInvalidParameter)
|
||
})
|
||
|
||
Convey("成功冻结", func() {
|
||
res, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_1", "freeze", now)
|
||
So(err, ShouldBeNil)
|
||
So(res, ShouldNotBeNil)
|
||
So(res.Ledger, ShouldNotBeNil)
|
||
So(res.User, ShouldNotBeNil)
|
||
So(res.Ledger.Type, ShouldEqual, consts.TenantLedgerTypeFreeze)
|
||
So(res.Ledger.Amount, ShouldEqual, 300)
|
||
So(res.Ledger.BalanceBefore, ShouldEqual, 1000)
|
||
So(res.Ledger.BalanceAfter, ShouldEqual, 700)
|
||
So(res.Ledger.FrozenBefore, ShouldEqual, 0)
|
||
So(res.Ledger.FrozenAfter, ShouldEqual, 300)
|
||
So(res.Ledger.OperatorUserID, ShouldEqual, userID)
|
||
So(res.Ledger.BizRefType, ShouldEqual, "")
|
||
So(res.Ledger.BizRefID, ShouldEqual, int64(0))
|
||
So(res.User.Balance, ShouldEqual, 700)
|
||
So(res.User.BalanceFrozen, ShouldEqual, 300)
|
||
})
|
||
|
||
Convey("幂等键重复调用不应重复扣减", func() {
|
||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_idem", "freeze", now)
|
||
So(err, ShouldBeNil)
|
||
|
||
res2, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_idem", "freeze", now.Add(time.Second))
|
||
So(err, ShouldBeNil)
|
||
So(res2, ShouldNotBeNil)
|
||
So(res2.Ledger, ShouldNotBeNil)
|
||
So(res2.Ledger.IdempotencyKey, ShouldEqual, "k_freeze_idem")
|
||
|
||
var u2 models.User
|
||
So(_db.WithContext(ctx).Where("id = ?", userID).First(&u2).Error, ShouldBeNil)
|
||
So(u2.Balance, ShouldEqual, 700)
|
||
So(u2.BalanceFrozen, ShouldEqual, 300)
|
||
})
|
||
|
||
Convey("余额不足应返回前置条件失败", func() {
|
||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 999999, "k_over", "freeze", now)
|
||
So(err, ShouldNotBeNil)
|
||
|
||
var appErr *errorx.AppError
|
||
So(errors.As(err, &appErr), ShouldBeTrue)
|
||
So(appErr.Code, ShouldEqual, errorx.CodePreconditionFailed)
|
||
})
|
||
})
|
||
}
|
||
|
||
func (s *LedgerTestSuite) Test_Unfreeze() {
|
||
Convey("Ledger.Unfreeze", s.T(), func() {
|
||
ctx := s.T().Context()
|
||
tenantID := int64(1)
|
||
userID := int64(2)
|
||
now := time.Now().UTC()
|
||
|
||
s.seedTenantUser(ctx, tenantID, userID, 1000, 0)
|
||
|
||
Convey("金额非法应返回参数错误", func() {
|
||
_, err := Ledger.Unfreeze(ctx, tenantID, userID, 0, 0, "k_unfreeze_invalid_amount", "unfreeze", now)
|
||
So(err, ShouldNotBeNil)
|
||
})
|
||
|
||
Convey("冻结余额不足应返回前置条件失败", func() {
|
||
_, err := Ledger.Unfreeze(ctx, tenantID, userID, 0, 999999, "k_unfreeze_over", "unfreeze", now)
|
||
So(err, ShouldNotBeNil)
|
||
|
||
var appErr *errorx.AppError
|
||
So(errors.As(err, &appErr), ShouldBeTrue)
|
||
So(appErr.Code, ShouldEqual, errorx.CodePreconditionFailed)
|
||
})
|
||
|
||
Convey("成功解冻", func() {
|
||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_for_unfreeze", "freeze", now)
|
||
So(err, ShouldBeNil)
|
||
|
||
res, err := Ledger.Unfreeze(ctx, tenantID, userID, 0, 300, "k_unfreeze_ok", "unfreeze", now)
|
||
So(err, ShouldBeNil)
|
||
So(res, ShouldNotBeNil)
|
||
So(res.Ledger.Type, ShouldEqual, consts.TenantLedgerTypeUnfreeze)
|
||
So(res.Ledger.OperatorUserID, ShouldEqual, userID)
|
||
So(res.Ledger.BizRefType, ShouldEqual, "")
|
||
So(res.Ledger.BizRefID, ShouldEqual, int64(0))
|
||
So(res.User.Balance, ShouldEqual, 1000)
|
||
So(res.User.BalanceFrozen, ShouldEqual, 0)
|
||
})
|
||
|
||
Convey("幂等键重复调用不应重复入账", func() {
|
||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_for_unfreeze_idem", "freeze", now)
|
||
So(err, ShouldBeNil)
|
||
|
||
_, err = Ledger.Unfreeze(ctx, tenantID, userID, 0, 300, "k_unfreeze_idem", "unfreeze", now)
|
||
So(err, ShouldBeNil)
|
||
|
||
res2, err := Ledger.Unfreeze(ctx, tenantID, userID, 0, 300, "k_unfreeze_idem", "unfreeze", now.Add(time.Second))
|
||
So(err, ShouldBeNil)
|
||
So(res2, ShouldNotBeNil)
|
||
|
||
var u2 models.User
|
||
So(_db.WithContext(ctx).Where("id = ?", userID).First(&u2).Error, ShouldBeNil)
|
||
So(u2.Balance, ShouldEqual, 1000)
|
||
So(u2.BalanceFrozen, ShouldEqual, 0)
|
||
})
|
||
})
|
||
}
|
||
|
||
func (s *LedgerTestSuite) Test_DebitPurchaseTx() {
|
||
Convey("Ledger.DebitPurchaseTx", s.T(), func() {
|
||
ctx := s.T().Context()
|
||
tenantID := int64(1)
|
||
userID := int64(2)
|
||
now := time.Now().UTC()
|
||
|
||
s.seedTenantUser(ctx, tenantID, userID, 1000, 0)
|
||
|
||
Convey("金额非法应返回参数错误", func() {
|
||
_, err := Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 0, "k_debit_invalid_amount", "debit", now)
|
||
So(err, ShouldNotBeNil)
|
||
})
|
||
|
||
Convey("冻结余额不足应返回前置条件失败", func() {
|
||
_, err := Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_debit_no_frozen", "debit", now)
|
||
So(err, ShouldNotBeNil)
|
||
})
|
||
|
||
Convey("成功扣款应减少冻结余额并保持可用余额不变", func() {
|
||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_for_debit", "freeze", now)
|
||
So(err, ShouldBeNil)
|
||
|
||
res, err := Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_debit_1", "debit", now)
|
||
So(err, ShouldBeNil)
|
||
So(res, ShouldNotBeNil)
|
||
So(res.Ledger.Type, ShouldEqual, consts.TenantLedgerTypeDebitPurchase)
|
||
So(res.Ledger.OperatorUserID, ShouldEqual, userID)
|
||
So(res.Ledger.BizRefType, ShouldEqual, "order")
|
||
So(res.Ledger.BizRefID, ShouldEqual, int64(123))
|
||
So(res.User.Balance, ShouldEqual, 700)
|
||
So(res.User.BalanceFrozen, ShouldEqual, 0)
|
||
})
|
||
|
||
Convey("幂等键重复调用不应重复扣减冻结余额", func() {
|
||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_for_debit_idem", "freeze", now)
|
||
So(err, ShouldBeNil)
|
||
|
||
_, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_debit_idem", "debit", now)
|
||
So(err, ShouldBeNil)
|
||
|
||
_, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_debit_idem", "debit", now.Add(time.Second))
|
||
So(err, ShouldBeNil)
|
||
|
||
var u2 models.User
|
||
So(_db.WithContext(ctx).Where("id = ?", userID).First(&u2).Error, ShouldBeNil)
|
||
So(u2.Balance, ShouldEqual, 700)
|
||
So(u2.BalanceFrozen, ShouldEqual, 0)
|
||
})
|
||
})
|
||
}
|
||
|
||
func (s *LedgerTestSuite) Test_CreditRefundTx() {
|
||
Convey("Ledger.CreditRefundTx", s.T(), func() {
|
||
ctx := s.T().Context()
|
||
tenantID := int64(1)
|
||
userID := int64(2)
|
||
now := time.Now().UTC()
|
||
|
||
s.seedTenantUser(ctx, tenantID, userID, 1000, 0)
|
||
|
||
Convey("金额非法应返回参数错误", func() {
|
||
_, err := Ledger.CreditRefundTx(ctx, _db, tenantID, userID, userID, 123, 0, "k_refund_invalid_amount", "refund", now)
|
||
So(err, ShouldNotBeNil)
|
||
})
|
||
|
||
Convey("成功退款应增加可用余额", func() {
|
||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_for_refund", "freeze", now)
|
||
So(err, ShouldBeNil)
|
||
_, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_debit_for_refund", "debit", now)
|
||
So(err, ShouldBeNil)
|
||
|
||
res, err := Ledger.CreditRefundTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_refund_1", "refund", now)
|
||
So(err, ShouldBeNil)
|
||
So(res, ShouldNotBeNil)
|
||
So(res.Ledger.Type, ShouldEqual, consts.TenantLedgerTypeCreditRefund)
|
||
So(res.Ledger.OperatorUserID, ShouldEqual, userID)
|
||
So(res.Ledger.BizRefType, ShouldEqual, "order")
|
||
So(res.Ledger.BizRefID, ShouldEqual, int64(123))
|
||
So(res.User.Balance, ShouldEqual, 1000)
|
||
So(res.User.BalanceFrozen, ShouldEqual, 0)
|
||
})
|
||
|
||
Convey("幂等键重复调用不应重复退款入账", func() {
|
||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_for_refund_idem", "freeze", now)
|
||
So(err, ShouldBeNil)
|
||
_, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_debit_for_refund_idem", "debit", now)
|
||
So(err, ShouldBeNil)
|
||
|
||
_, err = Ledger.CreditRefundTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_refund_idem", "refund", now)
|
||
So(err, ShouldBeNil)
|
||
_, err = Ledger.CreditRefundTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_refund_idem", "refund", now.Add(time.Second))
|
||
So(err, ShouldBeNil)
|
||
|
||
var u2 models.User
|
||
So(_db.WithContext(ctx).Where("id = ?", userID).First(&u2).Error, ShouldBeNil)
|
||
So(u2.Balance, ShouldEqual, 1000)
|
||
})
|
||
})
|
||
}
|
||
|
||
func (s *LedgerTestSuite) Test_MyBalance() {
|
||
Convey("Ledger.MyBalance", s.T(), func() {
|
||
ctx := s.T().Context()
|
||
tenantID := int64(1)
|
||
userID := int64(2)
|
||
|
||
s.seedTenantUser(ctx, tenantID, userID, 1000, 200)
|
||
|
||
Convey("成功返回全局余额", func() {
|
||
m, err := Ledger.MyBalance(ctx, tenantID, userID)
|
||
So(err, ShouldBeNil)
|
||
So(m, ShouldNotBeNil)
|
||
So(m.Balance, ShouldEqual, 1000)
|
||
So(m.BalanceFrozen, ShouldEqual, 200)
|
||
})
|
||
|
||
Convey("参数非法应返回错误", func() {
|
||
_, err := Ledger.MyBalance(ctx, 0, userID)
|
||
So(err, ShouldNotBeNil)
|
||
})
|
||
})
|
||
}
|
||
|
||
func (s *LedgerTestSuite) Test_MyLedgerPage() {
|
||
Convey("Ledger.MyLedgerPage", s.T(), func() {
|
||
ctx := s.T().Context()
|
||
tenantID := int64(1)
|
||
userID := int64(2)
|
||
now := time.Now().UTC()
|
||
|
||
s.seedTenantUser(ctx, tenantID, userID, 1000, 0)
|
||
|
||
_, err := Ledger.Freeze(ctx, tenantID, userID, 1, 200, "k_freeze_for_page_1", "freeze", now)
|
||
So(err, ShouldBeNil)
|
||
_, err = Ledger.Unfreeze(ctx, tenantID, userID, 1, 100, "k_unfreeze_for_page_1", "unfreeze", now.Add(time.Second))
|
||
So(err, ShouldBeNil)
|
||
|
||
Convey("分页返回流水列表", func() {
|
||
pager, err := Ledger.MyLedgerPage(ctx, tenantID, userID, &dto.MyLedgerListFilter{})
|
||
So(err, ShouldBeNil)
|
||
So(pager, ShouldNotBeNil)
|
||
So(pager.Total, ShouldBeGreaterThanOrEqualTo, 2)
|
||
})
|
||
|
||
Convey("按 type 过滤", func() {
|
||
typ := consts.TenantLedgerTypeFreeze
|
||
pager, err := Ledger.MyLedgerPage(ctx, tenantID, userID, &dto.MyLedgerListFilter{Type: &typ})
|
||
So(err, ShouldBeNil)
|
||
So(pager.Total, ShouldEqual, 1)
|
||
})
|
||
})
|
||
}
|
||
|
||
func (s *LedgerTestSuite) Test_AdminLedgerPage() {
|
||
Convey("Ledger.AdminLedgerPage", s.T(), func() {
|
||
ctx := s.T().Context()
|
||
tenantID := int64(1)
|
||
userID := int64(2)
|
||
now := time.Now().UTC()
|
||
|
||
s.seedTenantUser(ctx, tenantID, userID, 1000, 0)
|
||
|
||
// 模拟后台管理员为用户冻结资金:operator_user_id 与 user_id 不同。
|
||
_, err := Ledger.FreezeTx(ctx, _db, tenantID, 999, userID, 777, 200, "k_admin_freeze_for_page", "freeze", now)
|
||
So(err, ShouldBeNil)
|
||
|
||
Convey("按 operator_user_id 过滤", func() {
|
||
pager, err := Ledger.AdminLedgerPage(ctx, tenantID, &dto.AdminLedgerListFilter{
|
||
OperatorUserID: lo.ToPtr(int64(999)),
|
||
})
|
||
So(err, ShouldBeNil)
|
||
So(pager.Total, ShouldEqual, 1)
|
||
})
|
||
|
||
Convey("按 biz_ref_type + biz_ref_id 过滤", func() {
|
||
bizRefType := "order"
|
||
pager, err := Ledger.AdminLedgerPage(ctx, tenantID, &dto.AdminLedgerListFilter{
|
||
BizRefType: &bizRefType,
|
||
BizRefID: lo.ToPtr(int64(777)),
|
||
})
|
||
So(err, ShouldBeNil)
|
||
So(pager.Total, ShouldEqual, 1)
|
||
})
|
||
|
||
Convey("按 order_id 过滤", func() {
|
||
pager, err := Ledger.AdminLedgerPage(ctx, tenantID, &dto.AdminLedgerListFilter{
|
||
OrderID: lo.ToPtr(int64(777)),
|
||
})
|
||
So(err, ShouldBeNil)
|
||
So(pager.Total, ShouldEqual, 1)
|
||
})
|
||
})
|
||
}
|