package service import ( "testing" "time" "github.com/glebarez/sqlite" "github.com/stretchr/testify/require" "gorm.io/gorm" "wails-app/internal/dao" "wails-app/internal/global" ) func setupMonthlyServiceTest(t *testing.T) *MonthlyCardService { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&dao.Vehicle{}, &dao.MonthlyCard{}, &dao.Shortlist{}, &dao.PaymentRecord{})) global.GVA_DB = db t.Cleanup(func() { global.GVA_DB = nil }) return NewMonthlyCardService() } func createVehicle(t *testing.T, plate string) *dao.Vehicle { t.Helper() vehicle := dao.Vehicle{PlateNumber: plate, VehicleTypeID: 1} require.NoError(t, global.GVA_DB.Create(&vehicle).Error) return &vehicle } func countTable(t *testing.T, model interface{}) int64 { t.Helper() var count int64 require.NoError(t, global.GVA_DB.Model(model).Count(&count).Error) return count } func TestCreateWritesCardWhitelistAndPayment(t *testing.T) { svc := setupMonthlyServiceTest(t) vehicle := createVehicle(t, "MC-001") card, err := svc.Create(vehicle.ID, "month", 300, 7, "测试办理") require.NoError(t, err) require.NotZero(t, card.ID) require.Equal(t, "paid", card.PaymentStatus) require.True(t, card.EndDate.After(time.Now().Add(29*24*time.Hour))) // 三张表全部写入 require.Equal(t, int64(1), countTable(t, &dao.MonthlyCard{})) require.Equal(t, int64(1), countTable(t, &dao.Shortlist{})) require.Equal(t, int64(1), countTable(t, &dao.PaymentRecord{})) // 白名单到期日与月卡一致 var shortlist dao.Shortlist require.NoError(t, global.GVA_DB.First(&shortlist).Error) require.Equal(t, "白名单", shortlist.ListType) require.NotNil(t, shortlist.ExpirationTime) require.True(t, shortlist.ExpirationTime.Equal(card.EndDate)) // 支付流水 var pay dao.PaymentRecord require.NoError(t, global.GVA_DB.First(&pay).Error) require.Equal(t, 300.0, pay.Amount) require.Equal(t, uint(7), pay.OperatorID) } func TestCreateRollsBackWhenWhitelistWriteFails(t *testing.T) { svc := setupMonthlyServiceTest(t) vehicle := createVehicle(t, "MC-002") // 模拟白名单写入失败:删除 shortlist 表 require.NoError(t, global.GVA_DB.Exec("DROP TABLE shortlist").Error) _, err := svc.Create(vehicle.ID, "month", 300, 7, "") require.Error(t, err) require.Contains(t, err.Error(), "白名单") // 月卡与流水均回滚 require.Equal(t, int64(0), countTable(t, &dao.MonthlyCard{})) require.Equal(t, int64(0), countTable(t, &dao.PaymentRecord{})) } func TestCreateRollsBackWhenPaymentWriteFails(t *testing.T) { svc := setupMonthlyServiceTest(t) vehicle := createVehicle(t, "MC-003") // 模拟支付流水写入失败:删除 payment_record 表 require.NoError(t, global.GVA_DB.Exec("DROP TABLE payment_record").Error) _, err := svc.Create(vehicle.ID, "month", 300, 7, "") require.Error(t, err) require.Contains(t, err.Error(), "支付流水") // 月卡与白名单均回滚 require.Equal(t, int64(0), countTable(t, &dao.MonthlyCard{})) require.Equal(t, int64(0), countTable(t, &dao.Shortlist{})) } func TestCreateRejectsDuplicateActiveCard(t *testing.T) { svc := setupMonthlyServiceTest(t) vehicle := createVehicle(t, "MC-004") _, err := svc.Create(vehicle.ID, "month", 300, 7, "") require.NoError(t, err) _, err = svc.Create(vehicle.ID, "month", 300, 7, "") require.Error(t, err) require.Contains(t, err.Error(), "已有有效月卡") require.Equal(t, int64(1), countTable(t, &dao.MonthlyCard{})) } func TestCreateRejectsUnknownVehicle(t *testing.T) { svc := setupMonthlyServiceTest(t) _, err := svc.Create(99999, "month", 300, 7, "") require.Error(t, err) require.Contains(t, err.Error(), "车辆不存在") } func TestRenewExtendsFromEndDateWhenActive(t *testing.T) { svc := setupMonthlyServiceTest(t) vehicle := createVehicle(t, "MC-005") now := time.Now() card := &dao.MonthlyCard{ VehicleID: vehicle.ID, CardType: "month", StartDate: now.AddDate(0, -1, 0), EndDate: now.AddDate(0, 0, 10), Fee: 300, PaymentStatus: "paid", } require.NoError(t, global.GVA_DB.Create(card).Error) originalEnd := card.EndDate require.NoError(t, global.GVA_DB.Create(&dao.Shortlist{ VehicleId: int(vehicle.ID), ListType: "白名单", ExpirationTime: &originalEnd, }).Error) err := svc.Renew(card.ID, "month", 300, 7) require.NoError(t, err) var renewed dao.MonthlyCard require.NoError(t, global.GVA_DB.First(&renewed, card.ID).Error) // 未过期:从原到期日顺延 30 天 require.True(t, renewed.EndDate.After(now.AddDate(0, 0, 39)), "到期日应约为原到期日+30天") require.True(t, renewed.EndDate.Before(now.AddDate(0, 0, 41))) require.Equal(t, 600.0, renewed.Fee) // 白名单到期日同步 var shortlist dao.Shortlist require.NoError(t, global.GVA_DB.Where("vehicle_id = ?", vehicle.ID).First(&shortlist).Error) require.True(t, shortlist.ExpirationTime.Equal(renewed.EndDate)) } func TestRenewStartsFromNowWhenCardExpired(t *testing.T) { svc := setupMonthlyServiceTest(t) vehicle := createVehicle(t, "MC-006") now := time.Now() card := &dao.MonthlyCard{ VehicleID: vehicle.ID, CardType: "month", StartDate: now.AddDate(0, -3, 0), EndDate: now.AddDate(0, -2, 0), // 已过期 2 个月 Fee: 300, PaymentStatus: "paid", } require.NoError(t, global.GVA_DB.Create(card).Error) expiredEnd := card.EndDate require.NoError(t, global.GVA_DB.Create(&dao.Shortlist{ VehicleId: int(vehicle.ID), ListType: "白名单", ExpirationTime: &expiredEnd, }).Error) err := svc.Renew(card.ID, "month", 300, 7) require.NoError(t, err) var renewed dao.MonthlyCard require.NoError(t, global.GVA_DB.First(&renewed, card.ID).Error) // 已过期:从今天起算约 30 天,而不是从两个月前的到期日续期 require.True(t, renewed.EndDate.After(now.AddDate(0, 0, 29)), "过期卡应从今天起算") require.True(t, renewed.EndDate.Before(now.AddDate(0, 0, 31))) // 白名单到期日同步为新到期日(未过期卡同样适用) var shortlist dao.Shortlist require.NoError(t, global.GVA_DB.Where("vehicle_id = ?", vehicle.ID).First(&shortlist).Error) require.True(t, shortlist.ExpirationTime.Equal(renewed.EndDate)) } func TestRenewRollsBackWhenPaymentWriteFails(t *testing.T) { svc := setupMonthlyServiceTest(t) vehicle := createVehicle(t, "MC-007") now := time.Now() card := &dao.MonthlyCard{ VehicleID: vehicle.ID, CardType: "month", StartDate: now, EndDate: now.AddDate(0, 0, 10), Fee: 300, PaymentStatus: "paid", } require.NoError(t, global.GVA_DB.Create(card).Error) originalEnd := card.EndDate require.NoError(t, global.GVA_DB.Exec("DROP TABLE payment_record").Error) err := svc.Renew(card.ID, "month", 300, 7) require.Error(t, err) // 到期日与费用均未变(回滚) var after dao.MonthlyCard require.NoError(t, global.GVA_DB.First(&after, card.ID).Error) require.True(t, after.EndDate.Equal(originalEnd)) require.Equal(t, 300.0, after.Fee) } func TestRefundMarksRefundedAndRemovesWhitelist(t *testing.T) { svc := setupMonthlyServiceTest(t) vehicle := createVehicle(t, "MC-008") card, err := svc.Create(vehicle.ID, "month", 300, 7, "") require.NoError(t, err) require.Equal(t, int64(1), countTable(t, &dao.Shortlist{})) err = svc.Refund(card.ID) require.NoError(t, err) var refunded dao.MonthlyCard require.NoError(t, global.GVA_DB.First(&refunded, card.ID).Error) require.Equal(t, "refunded", refunded.PaymentStatus) var whitelistCount int64 require.NoError(t, global.GVA_DB.Model(&dao.Shortlist{}). Where("vehicle_id = ? AND list_type = ?", vehicle.ID, "白名单").Count(&whitelistCount).Error) require.Equal(t, int64(0), whitelistCount) } func TestRefundRejectsUnknownCard(t *testing.T) { svc := setupMonthlyServiceTest(t) err := svc.Refund(99999) require.Error(t, err) require.Contains(t, err.Error(), "月卡记录不存在") }