package vehicle import ( "fmt" "testing" "time" "github.com/glebarez/sqlite" "github.com/stretchr/testify/require" "gorm.io/gorm" "wails-app/internal/dao" "wails-app/internal/global" "wails-app/internal/model/vehicle/request" ticketService "wails-app/internal/modules/digital-ticket/service" paymentService "wails-app/internal/modules/payment/service" ) func setupVehiclePaymentTest(t *testing.T) *VehicleService { t.Helper() dsn := fmt.Sprintf("file:vehicle-payment-%d?mode=memory&cache=shared", time.Now().UnixNano()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) sqlDB, err := db.DB() require.NoError(t, err) sqlDB.SetMaxOpenConns(8) t.Cleanup(func() { _ = sqlDB.Close() if global.GVA_DB == db { global.GVA_DB = nil } }) global.GVA_DB = db require.NoError(t, db.AutoMigrate( &dao.Owner{}, &dao.Vehicle{}, &dao.VehicleType{}, &dao.FeeConfig{}, &dao.VehicleRecord{}, &dao.DigitalTicket{}, &dao.PaymentMethodConfig{}, &dao.PaymentEntryConfig{}, &dao.PaymentEntryMethod{}, &dao.PaymentRecord{}, &dao.Shortlist{}, &dao.Channel{}, &dao.UHFReader{}, &dao.ParkingLot{}, &dao.MonthlyCard{}, )) require.NoError(t, db.Create(&dao.ParkingLot{ LotCode: "TEST-LOT", LotName: "测试停车场", Capacity: 100, Available: 100, AllowMonthlyWhenFull: true, AllowVIPWhenFull: true, }).Error) require.NoError(t, db.Create(&dao.PaymentMethodConfig{ Code: paymentService.PaymentCash, Name: "cash", InputMode: paymentService.InputModeManualAmount, Scenes: []string{paymentService.EntryCounter}, Enabled: true, IsSystem: true, }).Error) require.NoError(t, db.Create(&dao.PaymentEntryConfig{ Code: paymentService.EntryCounter, Name: "counter", Enabled: true, IsSystem: true, }).Error) require.NoError(t, db.Create(&dao.PaymentEntryMethod{ EntryCode: paymentService.EntryCounter, MethodCode: paymentService.PaymentCash, }).Error) return &VehicleService{} } func createVehiclePaymentFixture(t *testing.T, withTicket bool) (*dao.VehicleRecord, *dao.Vehicle) { t.Helper() vehicleType := &dao.VehicleType{Name: "temporary", IsSystem: true} require.NoError(t, global.GVA_DB.Create(vehicleType).Error) require.NoError(t, global.GVA_DB.Create(&dao.FeeConfig{ VehicleTypeID: vehicleType.ID, StartTime: 0, StartFee: 0, UnitTime: 60, UnitFee: 1, DailyMaxFee: 100, }).Error) vehicle := &dao.Vehicle{PlateNumber: "PAY-EDGE", VehicleTypeID: vehicleType.ID} require.NoError(t, global.GVA_DB.Create(vehicle).Error) record := &dao.VehicleRecord{ PlateNumber: vehicle.PlateNumber, EntryTime: time.Now().Add(-2 * time.Hour), PaymentStatus: "unpaid", } require.NoError(t, global.GVA_DB.Create(record).Error) if withTicket { _, err := ticketService.NewTicketService().CreateTx(global.GVA_DB, vehicle.PlateNumber, "test", record.ID) require.NoError(t, err) } return record, vehicle } func TestExitConfirmRollsBackPaymentWhenTicketUpdateFails(t *testing.T) { svc := setupVehiclePaymentTest(t) record, vehicle := createVehiclePaymentFixture(t, false) _, err := svc.ExitConfirm(request.ExitConfirmRequest{ PlateNumber: vehicle.PlateNumber, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentCash, PaidAmount: 10, }, 1) require.Error(t, err) var persistedRecord dao.VehicleRecord require.NoError(t, global.GVA_DB.First(&persistedRecord, record.ID).Error) require.Nil(t, persistedRecord.ExitTime) require.Equal(t, "unpaid", persistedRecord.PaymentStatus) var paymentCount int64 require.NoError(t, global.GVA_DB.Model(&dao.PaymentRecord{}).Where("record_id = ?", record.ID).Count(&paymentCount).Error) require.Zero(t, paymentCount) var persistedVehicle dao.Vehicle require.NoError(t, global.GVA_DB.First(&persistedVehicle, vehicle.ID).Error) require.Zero(t, persistedVehicle.TotalFee) require.Zero(t, persistedVehicle.TotalStayTime) } func TestExitConfirmRejectsDuplicatePayment(t *testing.T) { svc := setupVehiclePaymentTest(t) record, vehicle := createVehiclePaymentFixture(t, true) req := request.ExitConfirmRequest{ PlateNumber: vehicle.PlateNumber, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentCash, PaidAmount: 10, } _, err := svc.ExitConfirm(req, 1) require.NoError(t, err) _, err = svc.ExitConfirm(req, 1) require.Error(t, err) var paymentCount int64 require.NoError(t, global.GVA_DB.Model(&dao.PaymentRecord{}).Where("record_id = ?", record.ID).Count(&paymentCount).Error) require.EqualValues(t, 1, paymentCount) var ticket dao.DigitalTicket require.NoError(t, global.GVA_DB.Where("vehicle_record_id = ?", record.ID).First(&ticket).Error) require.Equal(t, ticketService.StateExited, ticket.State) } func TestPayDigitalTicketUpdatesPaymentAndVehicleRecordTogether(t *testing.T) { svc := setupVehiclePaymentTest(t) record, _ := createVehiclePaymentFixture(t, true) var ticket dao.DigitalTicket require.NoError(t, global.GVA_DB.Where("vehicle_record_id = ?", record.ID).First(&ticket).Error) require.NoError(t, svc.PayDigitalTicket( ticket.TicketNo, paymentService.EntryCounter, paymentService.PaymentCash, "ORDER-UNIFIED-1", 10, 7, )) var persistedRecord dao.VehicleRecord require.NoError(t, global.GVA_DB.First(&persistedRecord, record.ID).Error) require.Equal(t, "paid", persistedRecord.PaymentStatus) require.Equal(t, paymentService.EntryCounter, persistedRecord.PaymentEntry) require.Equal(t, paymentService.PaymentCash, persistedRecord.PaymentMethod) require.Positive(t, persistedRecord.Fee) require.Nil(t, persistedRecord.ExitTime) var payment dao.PaymentRecord require.NoError(t, global.GVA_DB.Where("record_id = ?", record.ID).First(&payment).Error) require.EqualValues(t, 7, payment.OperatorID) require.InDelta(t, persistedRecord.Fee, payment.Amount, 0.001) require.NoError(t, global.GVA_DB.First(&ticket, ticket.ID).Error) require.Equal(t, ticketService.StatePaid, ticket.State) require.Equal(t, "ORDER-UNIFIED-1", ticket.PaymentOrderNo) require.InDelta(t, payment.Amount, ticket.Fee, 0.001) } func TestExitConfirmConsumesPrepaidTicketWithoutSecondPayment(t *testing.T) { svc := setupVehiclePaymentTest(t) record, vehicle := createVehiclePaymentFixture(t, true) var ticket dao.DigitalTicket require.NoError(t, global.GVA_DB.Where("vehicle_record_id = ?", record.ID).First(&ticket).Error) require.NoError(t, svc.PayDigitalTicket( ticket.TicketNo, paymentService.EntryCounter, paymentService.PaymentCash, "ORDER-UNIFIED-2", 10, 7, )) result, err := svc.ExitConfirm(request.ExitConfirmRequest{ PlateNumber: vehicle.PlateNumber, // A device may still send its normal free-release request. A prepaid // ticket must consume the recorded payment instead of charging again. PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentFree, }, 9) require.NoError(t, err) require.Equal(t, paymentService.PaymentCash, result.PaymentMethod) require.Equal(t, paymentService.EntryCounter, result.PaymentEntry) var paymentCount int64 require.NoError(t, global.GVA_DB.Model(&dao.PaymentRecord{}).Where("record_id = ?", record.ID).Count(&paymentCount).Error) require.EqualValues(t, 1, paymentCount) var persistedRecord dao.VehicleRecord require.NoError(t, global.GVA_DB.First(&persistedRecord, record.ID).Error) require.NotNil(t, persistedRecord.ExitTime) require.Equal(t, "paid", persistedRecord.PaymentStatus) require.Equal(t, paymentService.PaymentCash, persistedRecord.PaymentMethod) require.NoError(t, global.GVA_DB.First(&ticket, ticket.ID).Error) require.Equal(t, ticketService.StateExited, ticket.State) }