| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- package service
- import (
- "errors"
- "sync"
- "testing"
- "github.com/stretchr/testify/require"
- "gorm.io/gorm"
- "wails-app/internal/dao"
- "wails-app/internal/global"
- )
- func createPendingTicketForEdgeTest(t *testing.T, svc *TicketService, plate string) *dao.DigitalTicket {
- t.Helper()
- record := &dao.VehicleRecord{PlateNumber: plate, PaymentStatus: "unpaid"}
- require.NoError(t, global.GVA_DB.Create(record).Error)
- ticket, err := svc.Create(plate, "test", record.ID)
- require.NoError(t, err)
- return ticket
- }
- func TestDigitalTicketRejectsNegativePaymentAmounts(t *testing.T) {
- svc := setupTicketServiceTest(t)
- ticket := createPendingTicketForEdgeTest(t, svc, "NEGATIVE-TICKET")
- err := svc.TransitionTx(global.GVA_DB, ticket.ID, StatePaid, map[string]interface{}{
- "fee": -1.0, "paid_amount": 0.0,
- })
- require.ErrorIs(t, err, ErrInvalidTicketPayment)
- var current dao.DigitalTicket
- require.NoError(t, global.GVA_DB.First(¤t, ticket.ID).Error)
- require.Equal(t, StatePendingPayment, current.State)
- err = svc.MarkPaidAndExitedTx(global.GVA_DB, ticket.VehicleRecordID, "counter", "cash", "", -1, 0)
- require.ErrorIs(t, err, ErrInvalidTicketPayment)
- }
- func TestDigitalTicketAllowsZeroFeePayment(t *testing.T) {
- svc := setupTicketServiceTest(t)
- ticket := createPendingTicketForEdgeTest(t, svc, "ZERO-TICKET")
- require.NoError(t, svc.TransitionTx(global.GVA_DB, ticket.ID, StatePaid, map[string]interface{}{
- "fee": 0.0, "paid_amount": 0.0, "payment_entry": "counter", "payment_method": "cash",
- }))
- var current dao.DigitalTicket
- require.NoError(t, global.GVA_DB.First(¤t, ticket.ID).Error)
- require.Equal(t, StatePaid, current.State)
- require.Zero(t, current.Fee)
- require.Zero(t, current.PaidAmount)
- }
- func TestDigitalTicketRejectsDuplicatePayment(t *testing.T) {
- svc := setupTicketServiceTest(t)
- ticket := createPendingTicketForEdgeTest(t, svc, "DUPLICATE-TICKET")
- extra := map[string]interface{}{"fee": 10.0, "paid_amount": 10.0, "payment_order_no": "ORDER-1"}
- require.NoError(t, svc.TransitionTx(global.GVA_DB, ticket.ID, StatePaid, extra))
- require.Error(t, svc.TransitionTx(global.GVA_DB, ticket.ID, StatePaid, extra))
- var current dao.DigitalTicket
- require.NoError(t, global.GVA_DB.First(¤t, ticket.ID).Error)
- require.Equal(t, StatePaid, current.State)
- require.Equal(t, "ORDER-1", current.PaymentOrderNo)
- }
- func TestDigitalTicketConcurrentPaymentHasSingleWinner(t *testing.T) {
- svc := setupTicketServiceTest(t)
- ticket := createPendingTicketForEdgeTest(t, svc, "CONCURRENT-TICKET")
- const attempts = 8
- start := make(chan struct{})
- results := make(chan error, attempts)
- var wg sync.WaitGroup
- for i := 0; i < attempts; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- <-start
- results <- svc.TransitionTx(global.GVA_DB, ticket.ID, StatePaid, map[string]interface{}{
- "fee": 10.0, "paid_amount": 10.0,
- })
- }()
- }
- close(start)
- wg.Wait()
- close(results)
- successes := 0
- for err := range results {
- if err == nil {
- successes++
- }
- }
- require.Equal(t, 1, successes)
- var current dao.DigitalTicket
- require.NoError(t, global.GVA_DB.First(¤t, ticket.ID).Error)
- require.Equal(t, StatePaid, current.State)
- }
- func TestDigitalTicketTransitionRollbackRestoresState(t *testing.T) {
- svc := setupTicketServiceTest(t)
- ticket := createPendingTicketForEdgeTest(t, svc, "ROLLBACK-TICKET")
- rollbackErr := errors.New("force rollback")
- err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
- if err := svc.TransitionTx(tx, ticket.ID, StatePaid, map[string]interface{}{
- "fee": 10.0, "paid_amount": 10.0,
- }); err != nil {
- return err
- }
- return rollbackErr
- })
- require.ErrorIs(t, err, rollbackErr)
- var current dao.DigitalTicket
- require.NoError(t, global.GVA_DB.First(¤t, ticket.ID).Error)
- require.Equal(t, StatePendingPayment, current.State)
- require.Empty(t, current.PaymentOrderNo)
- }
|