payment_edge_test.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. package vehicle
  2. import (
  3. "fmt"
  4. "testing"
  5. "time"
  6. "github.com/glebarez/sqlite"
  7. "github.com/stretchr/testify/require"
  8. "gorm.io/gorm"
  9. "wails-app/internal/dao"
  10. "wails-app/internal/global"
  11. "wails-app/internal/model/vehicle/request"
  12. ticketService "wails-app/internal/modules/digital-ticket/service"
  13. paymentService "wails-app/internal/modules/payment/service"
  14. )
  15. func setupVehiclePaymentTest(t *testing.T) *VehicleService {
  16. t.Helper()
  17. dsn := fmt.Sprintf("file:vehicle-payment-%d?mode=memory&cache=shared", time.Now().UnixNano())
  18. db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
  19. require.NoError(t, err)
  20. sqlDB, err := db.DB()
  21. require.NoError(t, err)
  22. sqlDB.SetMaxOpenConns(8)
  23. t.Cleanup(func() {
  24. _ = sqlDB.Close()
  25. if global.GVA_DB == db {
  26. global.GVA_DB = nil
  27. }
  28. })
  29. global.GVA_DB = db
  30. require.NoError(t, db.AutoMigrate(
  31. &dao.Owner{}, &dao.Vehicle{}, &dao.VehicleType{}, &dao.FeeConfig{}, &dao.VehicleRecord{}, &dao.DigitalTicket{},
  32. &dao.PaymentMethodConfig{}, &dao.PaymentEntryConfig{}, &dao.PaymentEntryMethod{}, &dao.PaymentRecord{}, &dao.Shortlist{},
  33. &dao.Channel{}, &dao.UHFReader{},
  34. ))
  35. require.NoError(t, db.Create(&dao.PaymentMethodConfig{
  36. Code: paymentService.PaymentCash, Name: "cash", InputMode: paymentService.InputModeManualAmount,
  37. Scenes: []string{paymentService.EntryCounter}, Enabled: true, IsSystem: true,
  38. }).Error)
  39. require.NoError(t, db.Create(&dao.PaymentEntryConfig{
  40. Code: paymentService.EntryCounter, Name: "counter", Enabled: true, IsSystem: true,
  41. }).Error)
  42. require.NoError(t, db.Create(&dao.PaymentEntryMethod{
  43. EntryCode: paymentService.EntryCounter, MethodCode: paymentService.PaymentCash,
  44. }).Error)
  45. return &VehicleService{}
  46. }
  47. func createVehiclePaymentFixture(t *testing.T, withTicket bool) (*dao.VehicleRecord, *dao.Vehicle) {
  48. t.Helper()
  49. vehicleType := &dao.VehicleType{Name: "temporary", IsSystem: true}
  50. require.NoError(t, global.GVA_DB.Create(vehicleType).Error)
  51. require.NoError(t, global.GVA_DB.Create(&dao.FeeConfig{
  52. VehicleTypeID: vehicleType.ID, StartTime: 0, StartFee: 0, UnitTime: 60, UnitFee: 1, DailyMaxFee: 100,
  53. }).Error)
  54. vehicle := &dao.Vehicle{PlateNumber: "PAY-EDGE", VehicleTypeID: vehicleType.ID}
  55. require.NoError(t, global.GVA_DB.Create(vehicle).Error)
  56. record := &dao.VehicleRecord{
  57. PlateNumber: vehicle.PlateNumber, EntryTime: time.Now().Add(-2 * time.Hour), PaymentStatus: "unpaid",
  58. }
  59. require.NoError(t, global.GVA_DB.Create(record).Error)
  60. if withTicket {
  61. _, err := ticketService.NewTicketService().CreateTx(global.GVA_DB, vehicle.PlateNumber, "test", record.ID)
  62. require.NoError(t, err)
  63. }
  64. return record, vehicle
  65. }
  66. func TestExitConfirmRollsBackPaymentWhenTicketUpdateFails(t *testing.T) {
  67. svc := setupVehiclePaymentTest(t)
  68. record, vehicle := createVehiclePaymentFixture(t, false)
  69. _, err := svc.ExitConfirm(request.ExitConfirmRequest{
  70. PlateNumber: vehicle.PlateNumber, PaymentEntry: paymentService.EntryCounter,
  71. PaymentMethod: paymentService.PaymentCash, PaidAmount: 10,
  72. }, 1)
  73. require.Error(t, err)
  74. var persistedRecord dao.VehicleRecord
  75. require.NoError(t, global.GVA_DB.First(&persistedRecord, record.ID).Error)
  76. require.Nil(t, persistedRecord.ExitTime)
  77. require.Equal(t, "unpaid", persistedRecord.PaymentStatus)
  78. var paymentCount int64
  79. require.NoError(t, global.GVA_DB.Model(&dao.PaymentRecord{}).Where("record_id = ?", record.ID).Count(&paymentCount).Error)
  80. require.Zero(t, paymentCount)
  81. var persistedVehicle dao.Vehicle
  82. require.NoError(t, global.GVA_DB.First(&persistedVehicle, vehicle.ID).Error)
  83. require.Zero(t, persistedVehicle.TotalFee)
  84. require.Zero(t, persistedVehicle.TotalStayTime)
  85. }
  86. func TestExitConfirmRejectsDuplicatePayment(t *testing.T) {
  87. svc := setupVehiclePaymentTest(t)
  88. record, vehicle := createVehiclePaymentFixture(t, true)
  89. req := request.ExitConfirmRequest{
  90. PlateNumber: vehicle.PlateNumber, PaymentEntry: paymentService.EntryCounter,
  91. PaymentMethod: paymentService.PaymentCash, PaidAmount: 10,
  92. }
  93. _, err := svc.ExitConfirm(req, 1)
  94. require.NoError(t, err)
  95. _, err = svc.ExitConfirm(req, 1)
  96. require.Error(t, err)
  97. var paymentCount int64
  98. require.NoError(t, global.GVA_DB.Model(&dao.PaymentRecord{}).Where("record_id = ?", record.ID).Count(&paymentCount).Error)
  99. require.EqualValues(t, 1, paymentCount)
  100. var ticket dao.DigitalTicket
  101. require.NoError(t, global.GVA_DB.Where("vehicle_record_id = ?", record.ID).First(&ticket).Error)
  102. require.Equal(t, ticketService.StateExited, ticket.State)
  103. }
  104. func TestPayDigitalTicketUpdatesPaymentAndVehicleRecordTogether(t *testing.T) {
  105. svc := setupVehiclePaymentTest(t)
  106. record, _ := createVehiclePaymentFixture(t, true)
  107. var ticket dao.DigitalTicket
  108. require.NoError(t, global.GVA_DB.Where("vehicle_record_id = ?", record.ID).First(&ticket).Error)
  109. require.NoError(t, svc.PayDigitalTicket(
  110. ticket.TicketNo, paymentService.EntryCounter, paymentService.PaymentCash, "ORDER-UNIFIED-1", 10, 7,
  111. ))
  112. var persistedRecord dao.VehicleRecord
  113. require.NoError(t, global.GVA_DB.First(&persistedRecord, record.ID).Error)
  114. require.Equal(t, "paid", persistedRecord.PaymentStatus)
  115. require.Equal(t, paymentService.EntryCounter, persistedRecord.PaymentEntry)
  116. require.Equal(t, paymentService.PaymentCash, persistedRecord.PaymentMethod)
  117. require.Positive(t, persistedRecord.Fee)
  118. require.Nil(t, persistedRecord.ExitTime)
  119. var payment dao.PaymentRecord
  120. require.NoError(t, global.GVA_DB.Where("record_id = ?", record.ID).First(&payment).Error)
  121. require.EqualValues(t, 7, payment.OperatorID)
  122. require.InDelta(t, persistedRecord.Fee, payment.Amount, 0.001)
  123. require.NoError(t, global.GVA_DB.First(&ticket, ticket.ID).Error)
  124. require.Equal(t, ticketService.StatePaid, ticket.State)
  125. require.Equal(t, "ORDER-UNIFIED-1", ticket.PaymentOrderNo)
  126. require.InDelta(t, payment.Amount, ticket.Fee, 0.001)
  127. }
  128. func TestExitConfirmConsumesPrepaidTicketWithoutSecondPayment(t *testing.T) {
  129. svc := setupVehiclePaymentTest(t)
  130. record, vehicle := createVehiclePaymentFixture(t, true)
  131. var ticket dao.DigitalTicket
  132. require.NoError(t, global.GVA_DB.Where("vehicle_record_id = ?", record.ID).First(&ticket).Error)
  133. require.NoError(t, svc.PayDigitalTicket(
  134. ticket.TicketNo, paymentService.EntryCounter, paymentService.PaymentCash, "ORDER-UNIFIED-2", 10, 7,
  135. ))
  136. result, err := svc.ExitConfirm(request.ExitConfirmRequest{
  137. PlateNumber: vehicle.PlateNumber,
  138. // A device may still send its normal free-release request. A prepaid
  139. // ticket must consume the recorded payment instead of charging again.
  140. PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentFree,
  141. }, 9)
  142. require.NoError(t, err)
  143. require.Equal(t, paymentService.PaymentCash, result.PaymentMethod)
  144. require.Equal(t, paymentService.EntryCounter, result.PaymentEntry)
  145. var paymentCount int64
  146. require.NoError(t, global.GVA_DB.Model(&dao.PaymentRecord{}).Where("record_id = ?", record.ID).Count(&paymentCount).Error)
  147. require.EqualValues(t, 1, paymentCount)
  148. var persistedRecord dao.VehicleRecord
  149. require.NoError(t, global.GVA_DB.First(&persistedRecord, record.ID).Error)
  150. require.NotNil(t, persistedRecord.ExitTime)
  151. require.Equal(t, "paid", persistedRecord.PaymentStatus)
  152. require.Equal(t, paymentService.PaymentCash, persistedRecord.PaymentMethod)
  153. require.NoError(t, global.GVA_DB.First(&ticket, ticket.ID).Error)
  154. require.Equal(t, ticketService.StateExited, ticket.State)
  155. }