package processor import ( "context" "os" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/models" "apskel-pos-be/internal/repository" ) // Needs TEST_DATABASE_URL pointing at a migrated database; see // internal/repository/wallet_repository_test.go. func TestPointPaymentMethod_AgainstPostgres(t *testing.T) { dsn := os.Getenv("TEST_DATABASE_URL") if dsn == "" { t.Skip("TEST_DATABASE_URL not set") } db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) require.NoError(t, err) ctx := context.Background() org, user, accepting, refusing := uuid.New(), uuid.New(), uuid.New(), uuid.New() exec := func(q string, args ...any) error { return db.Exec(q, args...).Error } require.NoError(t, exec(`INSERT INTO organizations (id, name, plan_type) VALUES (?, 'pm test', 'basic')`, org)) require.NoError(t, exec(`INSERT INTO outlets (id, organization_id, name) VALUES (?, ?, 'Terima'), (?, ?, 'Tolak')`, accepting, org, refusing, org)) t.Cleanup(func() { db.Exec(`DELETE FROM loyalty_setting_changes WHERE organization_id = ?`, org) db.Exec(`DELETE FROM outlet_settings WHERE outlet_id IN ?`, []uuid.UUID{accepting, refusing}) db.Exec(`DELETE FROM outlets WHERE id IN ?`, []uuid.UUID{accepting, refusing}) db.Exec(`DELETE FROM payment_methods WHERE organization_id = ?`, org) db.Exec(`DELETE FROM organizations WHERE id = ?`, org) }) // A new organization gets exactly one EnakPoint method from the trigger. var methods []struct { ID string Name string Type string } require.NoError(t, db.Raw(`SELECT id::text AS id, name, type FROM payment_methods WHERE organization_id = ?`, org).Scan(&methods).Error) require.Len(t, methods, 1) assert.Equal(t, "EnakPoint", methods[0].Name) assert.Equal(t, "point", methods[0].Type) pointID := uuid.MustParse(methods[0].ID) // The database refuses a second one. assert.Error(t, exec(`INSERT INTO payment_methods (organization_id, name, type) VALUES (?, 'EnakPoint 2', 'point')`, org)) txm := repository.NewTxManager(db) settings := NewLoyaltySettingsProcessor(repository.NewLoyaltySettingsRepository(db), txm) s, err := settings.Outlet(ctx, accepting) require.NoError(t, err) s.PointPayment.AcceptPayment = true _, err = settings.UpdateOutlet(ctx, org, accepting, user, *s) require.NoError(t, err) p := NewPaymentMethodProcessorImpl(repository.NewPaymentMethodRepositoryImpl(db), settings) cash, err := p.CreatePaymentMethod(ctx, &models.CreatePaymentMethodRequest{OrganizationID: org, Name: "Tunai", Type: constants.PaymentMethodTypeCash, IsActive: ptr(true)}) require.NoError(t, err) // The API cannot make, retype or delete an EnakPoint method. _, err = p.CreatePaymentMethod(ctx, &models.CreatePaymentMethodRequest{OrganizationID: org, Name: "Poin Lain", Type: constants.PaymentMethodTypePoint, IsActive: ptr(true)}) assert.ErrorIs(t, err, ErrSystemPaymentMethod) toCash := constants.PaymentMethodTypeCash _, err = p.UpdatePaymentMethod(ctx, pointID, &models.UpdatePaymentMethodRequest{Type: &toCash}) assert.ErrorIs(t, err, ErrSystemPaymentMethod) toPoint := constants.PaymentMethodTypePoint _, err = p.UpdatePaymentMethod(ctx, cash.ID, &models.UpdatePaymentMethodRequest{Type: &toPoint}) assert.ErrorIs(t, err, ErrSystemPaymentMethod) assert.ErrorIs(t, p.DeletePaymentMethod(ctx, pointID), ErrSystemPaymentMethod) // Renaming it is fine; its type stays. name := "Bayar pakai EnakPoint" renamed, err := p.UpdatePaymentMethod(ctx, pointID, &models.UpdatePaymentMethodRequest{Name: &name, Type: &toPoint}) require.NoError(t, err) assert.Equal(t, name, renamed.Name) // At the cashier it shows only where the outlet accepts EnakPoint. types := func(outlet *uuid.UUID) []constants.PaymentMethodType { t.Helper() list, err := p.ListPaymentMethods(ctx, &models.ListPaymentMethodsRequest{OrganizationID: &org, OutletID: outlet, Page: 1, Limit: 50}) require.NoError(t, err) var out []constants.PaymentMethodType for _, m := range list.PaymentMethods { out = append(out, m.Type) } assert.Equal(t, len(out), list.TotalCount, "the count matches what is listed") return out } assert.ElementsMatch(t, []constants.PaymentMethodType{"cash", "point"}, types(&accepting)) assert.ElementsMatch(t, []constants.PaymentMethodType{"cash"}, types(&refusing)) assert.ElementsMatch(t, []constants.PaymentMethodType{"cash", "point"}, types(nil), "the dashboard, without an outlet, sees it") // A payment either records both points_used and point_value, or neither. var orderID uuid.UUID require.NoError(t, exec(`INSERT INTO users (id, organization_id, name, email, password_hash, role) VALUES (?, ?, 'K', ?, 'x', 'cashier')`, user, org, user.String()+"@t")) orderID = uuid.New() require.NoError(t, exec(`INSERT INTO orders (id, organization_id, outlet_id, user_id, order_number, order_type, subtotal, tax_amount, total_amount) VALUES (?, ?, ?, ?, ?, 'dine_in', 1000, 0, 1000)`, orderID, org, accepting, user, "PM-"+orderID.String()[:8])) t.Cleanup(func() { db.Exec(`DELETE FROM payments WHERE order_id = ?`, orderID) db.Exec(`DELETE FROM orders WHERE id = ?`, orderID) db.Exec(`DELETE FROM users WHERE id = ?`, user) }) assert.Error(t, exec(`INSERT INTO payments (order_id, payment_method_id, amount, points_used) VALUES (?, ?, 1000, 1000)`, orderID, pointID)) assert.Error(t, exec(`INSERT INTO payments (order_id, payment_method_id, amount, points_used, point_value) VALUES (?, ?, 1000, 0, 1)`, orderID, pointID)) assert.NoError(t, exec(`INSERT INTO payments (order_id, payment_method_id, amount, points_used, point_value) VALUES (?, ?, 1000, 1000, 1)`, orderID, pointID)) assert.NoError(t, exec(`INSERT INTO payments (order_id, payment_method_id, amount) VALUES (?, ?, 1000)`, orderID, cash.ID)) }