edge_cases_test.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. package service
  2. import (
  3. "errors"
  4. "sync"
  5. "testing"
  6. "github.com/stretchr/testify/require"
  7. "gorm.io/gorm"
  8. "wails-app/internal/dao"
  9. "wails-app/internal/global"
  10. )
  11. func createPendingTicketForEdgeTest(t *testing.T, svc *TicketService, plate string) *dao.DigitalTicket {
  12. t.Helper()
  13. record := &dao.VehicleRecord{PlateNumber: plate, PaymentStatus: "unpaid"}
  14. require.NoError(t, global.GVA_DB.Create(record).Error)
  15. ticket, err := svc.Create(plate, "test", record.ID)
  16. require.NoError(t, err)
  17. return ticket
  18. }
  19. func TestDigitalTicketRejectsNegativePaymentAmounts(t *testing.T) {
  20. svc := setupTicketServiceTest(t)
  21. ticket := createPendingTicketForEdgeTest(t, svc, "NEGATIVE-TICKET")
  22. err := svc.TransitionTx(global.GVA_DB, ticket.ID, StatePaid, map[string]interface{}{
  23. "fee": -1.0, "paid_amount": 0.0,
  24. })
  25. require.ErrorIs(t, err, ErrInvalidTicketPayment)
  26. var current dao.DigitalTicket
  27. require.NoError(t, global.GVA_DB.First(&current, ticket.ID).Error)
  28. require.Equal(t, StatePendingPayment, current.State)
  29. err = svc.MarkPaidAndExitedTx(global.GVA_DB, ticket.VehicleRecordID, "counter", "cash", "", -1, 0)
  30. require.ErrorIs(t, err, ErrInvalidTicketPayment)
  31. }
  32. func TestDigitalTicketAllowsZeroFeePayment(t *testing.T) {
  33. svc := setupTicketServiceTest(t)
  34. ticket := createPendingTicketForEdgeTest(t, svc, "ZERO-TICKET")
  35. require.NoError(t, svc.TransitionTx(global.GVA_DB, ticket.ID, StatePaid, map[string]interface{}{
  36. "fee": 0.0, "paid_amount": 0.0, "payment_entry": "counter", "payment_method": "cash",
  37. }))
  38. var current dao.DigitalTicket
  39. require.NoError(t, global.GVA_DB.First(&current, ticket.ID).Error)
  40. require.Equal(t, StatePaid, current.State)
  41. require.Zero(t, current.Fee)
  42. require.Zero(t, current.PaidAmount)
  43. }
  44. func TestDigitalTicketRejectsDuplicatePayment(t *testing.T) {
  45. svc := setupTicketServiceTest(t)
  46. ticket := createPendingTicketForEdgeTest(t, svc, "DUPLICATE-TICKET")
  47. extra := map[string]interface{}{"fee": 10.0, "paid_amount": 10.0, "payment_order_no": "ORDER-1"}
  48. require.NoError(t, svc.TransitionTx(global.GVA_DB, ticket.ID, StatePaid, extra))
  49. require.Error(t, svc.TransitionTx(global.GVA_DB, ticket.ID, StatePaid, extra))
  50. var current dao.DigitalTicket
  51. require.NoError(t, global.GVA_DB.First(&current, ticket.ID).Error)
  52. require.Equal(t, StatePaid, current.State)
  53. require.Equal(t, "ORDER-1", current.PaymentOrderNo)
  54. }
  55. func TestDigitalTicketConcurrentPaymentHasSingleWinner(t *testing.T) {
  56. svc := setupTicketServiceTest(t)
  57. ticket := createPendingTicketForEdgeTest(t, svc, "CONCURRENT-TICKET")
  58. const attempts = 8
  59. start := make(chan struct{})
  60. results := make(chan error, attempts)
  61. var wg sync.WaitGroup
  62. for i := 0; i < attempts; i++ {
  63. wg.Add(1)
  64. go func() {
  65. defer wg.Done()
  66. <-start
  67. results <- svc.TransitionTx(global.GVA_DB, ticket.ID, StatePaid, map[string]interface{}{
  68. "fee": 10.0, "paid_amount": 10.0,
  69. })
  70. }()
  71. }
  72. close(start)
  73. wg.Wait()
  74. close(results)
  75. successes := 0
  76. for err := range results {
  77. if err == nil {
  78. successes++
  79. }
  80. }
  81. require.Equal(t, 1, successes)
  82. var current dao.DigitalTicket
  83. require.NoError(t, global.GVA_DB.First(&current, ticket.ID).Error)
  84. require.Equal(t, StatePaid, current.State)
  85. }
  86. func TestDigitalTicketTransitionRollbackRestoresState(t *testing.T) {
  87. svc := setupTicketServiceTest(t)
  88. ticket := createPendingTicketForEdgeTest(t, svc, "ROLLBACK-TICKET")
  89. rollbackErr := errors.New("force rollback")
  90. err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
  91. if err := svc.TransitionTx(tx, ticket.ID, StatePaid, map[string]interface{}{
  92. "fee": 10.0, "paid_amount": 10.0,
  93. }); err != nil {
  94. return err
  95. }
  96. return rollbackErr
  97. })
  98. require.ErrorIs(t, err, rollbackErr)
  99. var current dao.DigitalTicket
  100. require.NoError(t, global.GVA_DB.First(&current, ticket.ID).Error)
  101. require.Equal(t, StatePendingPayment, current.State)
  102. require.Empty(t, current.PaymentOrderNo)
  103. }