package vehicle import ( "testing" "time" "github.com/stretchr/testify/require" "wails-app/internal/dao" "wails-app/internal/global" "wails-app/internal/model/vehicle/request" "wails-app/internal/modules/digital-ticket/service" paymentService "wails-app/internal/modules/payment/service" ) // createPaidTicketFixture 构造"已支付票 + 已支付流水 + 会话已支付"的出场场景。 // paidAgoMinutes 控制支付时间距今多久(用于超时判定)。 func createPaidTicketFixture(t *testing.T, paidAgoMinutes int) (*dao.VehicleRecord, *dao.Vehicle, *dao.DigitalTicket) { t.Helper() record, vehicle := createVehiclePaymentFixture(t, true) ticketSvc := service.NewTicketService() ticket, err := ticketSvc.GetByVehicleRecordIDTx(global.GVA_DB, record.ID) require.NoError(t, err) paidAt := time.Now().Add(-time.Duration(paidAgoMinutes) * time.Minute) require.NoError(t, global.GVA_DB.Model(ticket).Updates(map[string]interface{}{ "state": service.StatePaid, "fee": 10.0, "paid_amount": 10.0, "paid_at": paidAt, "payment_entry": paymentService.EntryCounter, "payment_method": paymentService.PaymentCash, }).Error) require.NoError(t, global.GVA_DB.Create(&dao.PaymentRecord{ RecordID: record.ID, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentCash, Amount: 10, PaidAmount: 10, ChangeAmount: 0, PaidAt: paidAt, OperatorID: 1, }).Error) require.NoError(t, global.GVA_DB.Model(record).Update("payment_status", "paid").Error) ticket, err = ticketSvc.GetByVehicleRecordIDTx(global.GVA_DB, record.ID) require.NoError(t, err) return record, vehicle, ticket } // setFreeExitMinutes 设置该车辆类型的缴费后免费离场时限。 func setFreeExitMinutes(t *testing.T, vehicleTypeID uint, minutes int) { t.Helper() require.NoError(t, global.GVA_DB.Model(&dao.FeeConfig{}). Where("vehicle_type_id = ?", vehicleTypeID).Update("free_exit_minutes", minutes).Error) } func TestPaidTicketOvertimeNotExceeded(t *testing.T) { svc := setupVehiclePaymentTest(t) _, vehicle, ticket := createPaidTicketFixture(t, 3) // 3 分钟前支付 setFreeExitMinutes(t, vehicle.VehicleTypeID, 15) overtime, newFee, diff, err := svc.paidTicketOvertime(vehicle, ticket, 120) require.NoError(t, err) require.False(t, overtime) require.Zero(t, newFee) require.Zero(t, diff) } func TestPaidTicketOvertimeExceeded(t *testing.T) { svc := setupVehiclePaymentTest(t) _, vehicle, ticket := createPaidTicketFixture(t, 20) // 20 分钟前支付 setFreeExitMinutes(t, vehicle.VehicleTypeID, 15) // 停留 1200 分钟:fixture 配置 UnitTime=60/UnitFee=1 → 新费用 20 元,已付 10 → 补缴 10 overtime, newFee, diff, err := svc.paidTicketOvertime(vehicle, ticket, 1200) require.NoError(t, err) require.True(t, overtime) require.Equal(t, 20.0, newFee) require.Equal(t, 10.0, diff) } func TestPaidTicketOvertimeDisabledAndNilPaidAt(t *testing.T) { svc := setupVehiclePaymentTest(t) _, vehicle, ticket := createPaidTicketFixture(t, 20) setFreeExitMinutes(t, vehicle.VehicleTypeID, 0) // 0=不限 overtime, _, _, err := svc.paidTicketOvertime(vehicle, ticket, 1200) require.NoError(t, err) require.False(t, overtime) // PaidAt 为空(历史票)视为未超时 require.NoError(t, global.GVA_DB.Model(ticket).Update("paid_at", nil).Error) ticket.PaidAt = nil overtime, _, _, err = svc.paidTicketOvertime(vehicle, ticket, 1200) require.NoError(t, err) require.False(t, overtime) } func TestExitConfirmOvertimeRequiresTopUp(t *testing.T) { svc := setupVehiclePaymentTest(t) record, vehicle, _ := createPaidTicketFixture(t, 20) setFreeExitMinutes(t, vehicle.VehicleTypeID, 15) // 停留 20 小时 → 费用 20 元,补缴 10 元 require.NoError(t, global.GVA_DB.Model(record).Update("entry_time", time.Now().Add(-20*time.Hour)).Error) // 未携带补缴实收 → 拒绝 _, err := svc.ExitConfirm(request.ExitConfirmRequest{ SessionID: record.ID, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentCash, }, 1) require.Error(t, err) require.Contains(t, err.Error(), "需补缴差额") // 会话仍未关闭 var after dao.VehicleRecord require.NoError(t, global.GVA_DB.First(&after, record.ID).Error) require.Nil(t, after.ExitTime) } func TestExitConfirmOvertimeFreeMethodRejected(t *testing.T) { svc := setupVehiclePaymentTest(t) record, vehicle, _ := createPaidTicketFixture(t, 20) setFreeExitMinutes(t, vehicle.VehicleTypeID, 15) require.NoError(t, global.GVA_DB.Model(record).Update("entry_time", time.Now().Add(-20*time.Hour)).Error) _, err := svc.ExitConfirm(request.ExitConfirmRequest{ SessionID: record.ID, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentFree, PaidAmount: 10, }, 1) require.Error(t, err) require.Contains(t, err.Error(), "免密放行") } func TestExitConfirmOvertimeTopUpSucceeds(t *testing.T) { svc := setupVehiclePaymentTest(t) record, vehicle, ticket := createPaidTicketFixture(t, 20) setFreeExitMinutes(t, vehicle.VehicleTypeID, 15) require.NoError(t, global.GVA_DB.Model(record).Update("entry_time", time.Now().Add(-20*time.Hour)).Error) result, err := svc.ExitConfirm(request.ExitConfirmRequest{ SessionID: record.ID, PlateNumber: vehicle.PlateNumber, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentCash, PaidAmount: 10, }, 2) require.NoError(t, err) require.Equal(t, 20.0, result.Fee) // 总费用 // 会话已关闭且费用为总费用 var after dao.VehicleRecord require.NoError(t, global.GVA_DB.First(&after, record.ID).Error) require.NotNil(t, after.ExitTime) require.Equal(t, 20.0, after.Fee) // 两条流水:原 10 元 + 补缴 10 元 var payments []dao.PaymentRecord require.NoError(t, global.GVA_DB.Where("record_id = ?", record.ID).Find(&payments).Error) require.Len(t, payments, 2) require.Equal(t, 20.0, payments[0].Amount+payments[1].Amount) // 票面累计 + PaidAt 刷新 var updatedTicket dao.DigitalTicket require.NoError(t, global.GVA_DB.First(&updatedTicket, ticket.ID).Error) require.Equal(t, 20.0, updatedTicket.Fee) require.Equal(t, 20.0, updatedTicket.PaidAmount) require.NotNil(t, updatedTicket.PaidAt) require.True(t, updatedTicket.PaidAt.After(time.Now().Add(-time.Minute)), "PaidAt 应刷新为补缴时间") } func TestExitConfirmOvertimeNoDiffPasses(t *testing.T) { svc := setupVehiclePaymentTest(t) record, vehicle, _ := createPaidTicketFixture(t, 20) setFreeExitMinutes(t, vehicle.VehicleTypeID, 15) // 停留仅 2 小时:新费用 2 元 < 已付 10 元 → 差额 0,无需补缴直接放行 require.NoError(t, global.GVA_DB.Model(record).Update("entry_time", time.Now().Add(-2*time.Hour)).Error) result, err := svc.ExitConfirm(request.ExitConfirmRequest{ SessionID: record.ID, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentCash, }, 1) require.NoError(t, err) require.Equal(t, 2.0, result.Fee) var after dao.VehicleRecord require.NoError(t, global.GVA_DB.First(&after, record.ID).Error) require.NotNil(t, after.ExitTime) } func TestExitConfirmPaidNotOvertimeRegresses(t *testing.T) { svc := setupVehiclePaymentTest(t) record, vehicle, _ := createPaidTicketFixture(t, 3) setFreeExitMinutes(t, vehicle.VehicleTypeID, 15) require.NoError(t, global.GVA_DB.Model(record).Update("entry_time", time.Now().Add(-2*time.Hour)).Error) // 未超时:消费旧流水(10 元)放行,不重算不补缴 result, err := svc.ExitConfirm(request.ExitConfirmRequest{ SessionID: record.ID, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentCash, }, 1) require.NoError(t, err) require.Equal(t, 10.0, result.Fee) var payments []dao.PaymentRecord require.NoError(t, global.GVA_DB.Where("record_id = ?", record.ID).Find(&payments).Error) require.Len(t, payments, 1) }