overtime_test.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. package vehicle
  2. import (
  3. "testing"
  4. "time"
  5. "github.com/stretchr/testify/require"
  6. "wails-app/internal/dao"
  7. "wails-app/internal/global"
  8. "wails-app/internal/model/vehicle/request"
  9. "wails-app/internal/modules/digital-ticket/service"
  10. paymentService "wails-app/internal/modules/payment/service"
  11. )
  12. // createPaidTicketFixture 构造"已支付票 + 已支付流水 + 会话已支付"的出场场景。
  13. // paidAgoMinutes 控制支付时间距今多久(用于超时判定)。
  14. func createPaidTicketFixture(t *testing.T, paidAgoMinutes int) (*dao.VehicleRecord, *dao.Vehicle, *dao.DigitalTicket) {
  15. t.Helper()
  16. record, vehicle := createVehiclePaymentFixture(t, true)
  17. ticketSvc := service.NewTicketService()
  18. ticket, err := ticketSvc.GetByVehicleRecordIDTx(global.GVA_DB, record.ID)
  19. require.NoError(t, err)
  20. paidAt := time.Now().Add(-time.Duration(paidAgoMinutes) * time.Minute)
  21. require.NoError(t, global.GVA_DB.Model(ticket).Updates(map[string]interface{}{
  22. "state": service.StatePaid,
  23. "fee": 10.0,
  24. "paid_amount": 10.0,
  25. "paid_at": paidAt,
  26. "payment_entry": paymentService.EntryCounter,
  27. "payment_method": paymentService.PaymentCash,
  28. }).Error)
  29. require.NoError(t, global.GVA_DB.Create(&dao.PaymentRecord{
  30. RecordID: record.ID, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentCash,
  31. Amount: 10, PaidAmount: 10, ChangeAmount: 0, PaidAt: paidAt, OperatorID: 1,
  32. }).Error)
  33. require.NoError(t, global.GVA_DB.Model(record).Update("payment_status", "paid").Error)
  34. ticket, err = ticketSvc.GetByVehicleRecordIDTx(global.GVA_DB, record.ID)
  35. require.NoError(t, err)
  36. return record, vehicle, ticket
  37. }
  38. // setFreeExitMinutes 设置该车辆类型的缴费后免费离场时限。
  39. func setFreeExitMinutes(t *testing.T, vehicleTypeID uint, minutes int) {
  40. t.Helper()
  41. require.NoError(t, global.GVA_DB.Model(&dao.FeeConfig{}).
  42. Where("vehicle_type_id = ?", vehicleTypeID).Update("free_exit_minutes", minutes).Error)
  43. }
  44. func TestPaidTicketOvertimeNotExceeded(t *testing.T) {
  45. svc := setupVehiclePaymentTest(t)
  46. _, vehicle, ticket := createPaidTicketFixture(t, 3) // 3 分钟前支付
  47. setFreeExitMinutes(t, vehicle.VehicleTypeID, 15)
  48. overtime, newFee, diff, err := svc.paidTicketOvertime(vehicle, ticket, 120)
  49. require.NoError(t, err)
  50. require.False(t, overtime)
  51. require.Zero(t, newFee)
  52. require.Zero(t, diff)
  53. }
  54. func TestPaidTicketOvertimeExceeded(t *testing.T) {
  55. svc := setupVehiclePaymentTest(t)
  56. _, vehicle, ticket := createPaidTicketFixture(t, 20) // 20 分钟前支付
  57. setFreeExitMinutes(t, vehicle.VehicleTypeID, 15)
  58. // 停留 1200 分钟:fixture 配置 UnitTime=60/UnitFee=1 → 新费用 20 元,已付 10 → 补缴 10
  59. overtime, newFee, diff, err := svc.paidTicketOvertime(vehicle, ticket, 1200)
  60. require.NoError(t, err)
  61. require.True(t, overtime)
  62. require.Equal(t, 20.0, newFee)
  63. require.Equal(t, 10.0, diff)
  64. }
  65. func TestPaidTicketOvertimeDisabledAndNilPaidAt(t *testing.T) {
  66. svc := setupVehiclePaymentTest(t)
  67. _, vehicle, ticket := createPaidTicketFixture(t, 20)
  68. setFreeExitMinutes(t, vehicle.VehicleTypeID, 0) // 0=不限
  69. overtime, _, _, err := svc.paidTicketOvertime(vehicle, ticket, 1200)
  70. require.NoError(t, err)
  71. require.False(t, overtime)
  72. // PaidAt 为空(历史票)视为未超时
  73. require.NoError(t, global.GVA_DB.Model(ticket).Update("paid_at", nil).Error)
  74. ticket.PaidAt = nil
  75. overtime, _, _, err = svc.paidTicketOvertime(vehicle, ticket, 1200)
  76. require.NoError(t, err)
  77. require.False(t, overtime)
  78. }
  79. func TestExitConfirmOvertimeRequiresTopUp(t *testing.T) {
  80. svc := setupVehiclePaymentTest(t)
  81. record, vehicle, _ := createPaidTicketFixture(t, 20)
  82. setFreeExitMinutes(t, vehicle.VehicleTypeID, 15)
  83. // 停留 20 小时 → 费用 20 元,补缴 10 元
  84. require.NoError(t, global.GVA_DB.Model(record).Update("entry_time", time.Now().Add(-20*time.Hour)).Error)
  85. // 未携带补缴实收 → 拒绝
  86. _, err := svc.ExitConfirm(request.ExitConfirmRequest{
  87. SessionID: record.ID, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentCash,
  88. }, 1)
  89. require.Error(t, err)
  90. require.Contains(t, err.Error(), "需补缴差额")
  91. // 会话仍未关闭
  92. var after dao.VehicleRecord
  93. require.NoError(t, global.GVA_DB.First(&after, record.ID).Error)
  94. require.Nil(t, after.ExitTime)
  95. }
  96. func TestExitConfirmOvertimeFreeMethodRejected(t *testing.T) {
  97. svc := setupVehiclePaymentTest(t)
  98. record, vehicle, _ := createPaidTicketFixture(t, 20)
  99. setFreeExitMinutes(t, vehicle.VehicleTypeID, 15)
  100. require.NoError(t, global.GVA_DB.Model(record).Update("entry_time", time.Now().Add(-20*time.Hour)).Error)
  101. _, err := svc.ExitConfirm(request.ExitConfirmRequest{
  102. SessionID: record.ID, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentFree,
  103. PaidAmount: 10,
  104. }, 1)
  105. require.Error(t, err)
  106. require.Contains(t, err.Error(), "免密放行")
  107. }
  108. func TestExitConfirmOvertimeTopUpSucceeds(t *testing.T) {
  109. svc := setupVehiclePaymentTest(t)
  110. record, vehicle, ticket := createPaidTicketFixture(t, 20)
  111. setFreeExitMinutes(t, vehicle.VehicleTypeID, 15)
  112. require.NoError(t, global.GVA_DB.Model(record).Update("entry_time", time.Now().Add(-20*time.Hour)).Error)
  113. result, err := svc.ExitConfirm(request.ExitConfirmRequest{
  114. SessionID: record.ID, PlateNumber: vehicle.PlateNumber,
  115. PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentCash,
  116. PaidAmount: 10,
  117. }, 2)
  118. require.NoError(t, err)
  119. require.Equal(t, 20.0, result.Fee) // 总费用
  120. // 会话已关闭且费用为总费用
  121. var after dao.VehicleRecord
  122. require.NoError(t, global.GVA_DB.First(&after, record.ID).Error)
  123. require.NotNil(t, after.ExitTime)
  124. require.Equal(t, 20.0, after.Fee)
  125. // 两条流水:原 10 元 + 补缴 10 元
  126. var payments []dao.PaymentRecord
  127. require.NoError(t, global.GVA_DB.Where("record_id = ?", record.ID).Find(&payments).Error)
  128. require.Len(t, payments, 2)
  129. require.Equal(t, 20.0, payments[0].Amount+payments[1].Amount)
  130. // 票面累计 + PaidAt 刷新
  131. var updatedTicket dao.DigitalTicket
  132. require.NoError(t, global.GVA_DB.First(&updatedTicket, ticket.ID).Error)
  133. require.Equal(t, 20.0, updatedTicket.Fee)
  134. require.Equal(t, 20.0, updatedTicket.PaidAmount)
  135. require.NotNil(t, updatedTicket.PaidAt)
  136. require.True(t, updatedTicket.PaidAt.After(time.Now().Add(-time.Minute)), "PaidAt 应刷新为补缴时间")
  137. }
  138. func TestExitConfirmOvertimeNoDiffPasses(t *testing.T) {
  139. svc := setupVehiclePaymentTest(t)
  140. record, vehicle, _ := createPaidTicketFixture(t, 20)
  141. setFreeExitMinutes(t, vehicle.VehicleTypeID, 15)
  142. // 停留仅 2 小时:新费用 2 元 < 已付 10 元 → 差额 0,无需补缴直接放行
  143. require.NoError(t, global.GVA_DB.Model(record).Update("entry_time", time.Now().Add(-2*time.Hour)).Error)
  144. result, err := svc.ExitConfirm(request.ExitConfirmRequest{
  145. SessionID: record.ID, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentCash,
  146. }, 1)
  147. require.NoError(t, err)
  148. require.Equal(t, 2.0, result.Fee)
  149. var after dao.VehicleRecord
  150. require.NoError(t, global.GVA_DB.First(&after, record.ID).Error)
  151. require.NotNil(t, after.ExitTime)
  152. }
  153. func TestExitConfirmPaidNotOvertimeRegresses(t *testing.T) {
  154. svc := setupVehiclePaymentTest(t)
  155. record, vehicle, _ := createPaidTicketFixture(t, 3)
  156. setFreeExitMinutes(t, vehicle.VehicleTypeID, 15)
  157. require.NoError(t, global.GVA_DB.Model(record).Update("entry_time", time.Now().Add(-2*time.Hour)).Error)
  158. // 未超时:消费旧流水(10 元)放行,不重算不补缴
  159. result, err := svc.ExitConfirm(request.ExitConfirmRequest{
  160. SessionID: record.ID, PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentCash,
  161. }, 1)
  162. require.NoError(t, err)
  163. require.Equal(t, 10.0, result.Fee)
  164. var payments []dao.PaymentRecord
  165. require.NoError(t, global.GVA_DB.Where("record_id = ?", record.ID).Find(&payments).Error)
  166. require.Len(t, payments, 1)
  167. }