package services import ( "context" "database/sql" "errors" "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/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) tu := &models.TenantUser{ TenantID: tenantID, UserID: userID, Role: types.NewArray([]consts.TenantUserRole{consts.TenantUserRoleMember}), Balance: balance, BalanceFrozen: frozen, Status: consts.UserStatusVerified, } 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.TenantUser, 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.TenantUser.Balance, ShouldEqual, 700) So(res.TenantUser.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 tu2 models.TenantUser So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, userID).First(&tu2).Error, ShouldBeNil) So(tu2.Balance, ShouldEqual, 700) So(tu2.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.TenantUser.Balance, ShouldEqual, 1000) So(res.TenantUser.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 tu2 models.TenantUser So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, userID).First(&tu2).Error, ShouldBeNil) So(tu2.Balance, ShouldEqual, 1000) So(tu2.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, 123, 0, "k_debit_invalid_amount", "debit", now) So(err, ShouldNotBeNil) }) Convey("冻结余额不足应返回前置条件失败", func() { _, err := Ledger.DebitPurchaseTx(ctx, _db, tenantID, 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, 123, 300, "k_debit_1", "debit", now) So(err, ShouldBeNil) So(res, ShouldNotBeNil) So(res.Ledger.Type, ShouldEqual, consts.TenantLedgerTypeDebitPurchase) So(res.TenantUser.Balance, ShouldEqual, 700) So(res.TenantUser.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, 123, 300, "k_debit_idem", "debit", now) So(err, ShouldBeNil) _, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, 123, 300, "k_debit_idem", "debit", now.Add(time.Second)) So(err, ShouldBeNil) var tu2 models.TenantUser So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, userID).First(&tu2).Error, ShouldBeNil) So(tu2.Balance, ShouldEqual, 700) So(tu2.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, 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, 123, 300, "k_debit_for_refund", "debit", now) So(err, ShouldBeNil) res, err := Ledger.CreditRefundTx(ctx, _db, tenantID, userID, 123, 300, "k_refund_1", "refund", now) So(err, ShouldBeNil) So(res, ShouldNotBeNil) So(res.Ledger.Type, ShouldEqual, consts.TenantLedgerTypeCreditRefund) So(res.TenantUser.Balance, ShouldEqual, 1000) So(res.TenantUser.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, 123, 300, "k_debit_for_refund_idem", "debit", now) So(err, ShouldBeNil) _, err = Ledger.CreditRefundTx(ctx, _db, tenantID, userID, 123, 300, "k_refund_idem", "refund", now) So(err, ShouldBeNil) _, err = Ledger.CreditRefundTx(ctx, _db, tenantID, userID, 123, 300, "k_refund_idem", "refund", now.Add(time.Second)) So(err, ShouldBeNil) var tu2 models.TenantUser So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, userID).First(&tu2).Error, ShouldBeNil) So(tu2.Balance, ShouldEqual, 1000) }) }) } func (s *LedgerTestSuite) Test_CreditTopupTx() { Convey("Ledger.CreditTopupTx", 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.CreditTopupTx(ctx, _db, tenantID, userID, 456, 0, "k_topup_invalid_amount", "topup", now) So(err, ShouldNotBeNil) }) Convey("成功充值应增加可用余额并写入账本", func() { res, err := Ledger.CreditTopupTx(ctx, _db, tenantID, userID, 456, 200, "k_topup_1", "topup", now) So(err, ShouldBeNil) So(res, ShouldNotBeNil) So(res.Ledger.Type, ShouldEqual, consts.TenantLedgerTypeCreditTopup) So(res.TenantUser.Balance, ShouldEqual, 1200) So(res.TenantUser.BalanceFrozen, ShouldEqual, 0) }) Convey("幂等键重复调用不应重复充值入账", func() { _, err := Ledger.CreditTopupTx(ctx, _db, tenantID, userID, 456, 200, "k_topup_idem", "topup", now) So(err, ShouldBeNil) _, err = Ledger.CreditTopupTx(ctx, _db, tenantID, userID, 456, 200, "k_topup_idem", "topup", now.Add(time.Second)) So(err, ShouldBeNil) var tu2 models.TenantUser So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, userID).First(&tu2).Error, ShouldBeNil) So(tu2.Balance, ShouldEqual, 1200) }) }) } 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.CreditTopupTx(ctx, _db, tenantID, userID, 1, 200, "k_topup_for_page", "topup", now) So(err, ShouldBeNil) _, err = Ledger.Freeze(ctx, tenantID, userID, 2, 100, "k_freeze_for_page", "freeze", 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.TenantLedgerTypeCreditTopup pager, err := Ledger.MyLedgerPage(ctx, tenantID, userID, &dto.MyLedgerListFilter{Type: &typ}) So(err, ShouldBeNil) So(pager.Total, ShouldEqual, 1) }) }) }