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) }