package processor import ( "context" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/models" "apskel-pos-be/internal/repository" ) func (e *pointPaymentEnv) sum(query string, args ...any) int64 { e.t.Helper() var n int64 require.NoError(e.t, e.db.Raw(query, args...).Scan(&n).Error) return n } func (e *pointPaymentEnv) returned(order uuid.UUID) int64 { return e.sum(`SELECT COALESCE(SUM(t.amount), 0) FROM wallet_transactions t JOIN payments p ON p.id = t.reference_id WHERE p.order_id = ? AND t.type = 'PAYMENT_REFUND'`, order) } func (e *pointPaymentEnv) assertReconciled(customers ...uuid.UUID) { e.t.Helper() found, err := repository.NewWalletReconciliationRepository(e.db).FindDiscrepancies(context.Background(), 1000) require.NoError(e.t, err) for _, d := range found { for _, c := range customers { assert.NotEqual(e.t, c, d.CustomerID, d.Check) } } } func TestPointRefund_VoidReturnsEverythingToItsExpiry(t *testing.T) { e := newPointPaymentEnv(t) customer := e.customerWith(0) expires := time.Now().Add(30 * 24 * time.Hour).Truncate(time.Second) require.NoError(t, repository.NewTxManager(e.db).WithTransaction(context.Background(), func(ctx context.Context) error { _, err := NewWalletProcessor(repository.NewWalletRepository(e.db)).Credit(ctx, WalletCreditInput{ WalletEntry: WalletEntry{CustomerID: customer, Currency: constants.WalletCurrencyPoint, Type: constants.WalletTxTypeMigration, Amount: 40000, ReferenceType: constants.WalletRefTypeLegacyPoints, ReferenceID: uuid.New(), Description: "Saldo awal"}, Lots: []WalletLotInput{{Amount: 40000, ExpiresAt: &expires}}, }) return err })) // Part of the order paid with EnakPoint, then the order is voided. order := e.order(customer, 50000) _, err := e.payPoints(order, 30000, e.code(customer)) require.NoError(t, err) assert.Equal(t, int64(10000), e.balance(customer)) require.NoError(t, e.orders.VoidOrder(e.ctx, &models.VoidOrderRequest{OrderID: order, Type: "ALL", Reason: "batal"}, e.cashier)) assert.Equal(t, int64(30000), e.returned(order)) assert.Equal(t, int64(40000), e.balance(customer)) var expiry time.Time require.NoError(t, e.db.Raw(`SELECT l.expires_at FROM wallet_lots l JOIN wallet_transactions t ON t.id = l.source_transaction_id WHERE t.customer_id = ? AND t.type = 'PAYMENT_REFUND'`, customer).Scan(&expiry).Error) assert.WithinDuration(t, expires, expiry, time.Second, "returned EnakPoint keep the expiry they had") // Calling it again returns nothing more. e.orders.onOrderRefunded(e.ctx, order) assert.Equal(t, int64(30000), e.returned(order)) e.assertReconciled(customer) } func TestPointRefund_PartialRefundFloors(t *testing.T) { e := newPointPaymentEnv(t) customer := e.customerWith(100000) order := e.order(customer, 50000) payment, err := e.payPoints(order, 50000, e.code(customer)) require.NoError(t, err) // Rp 12.345,67 back at Rp 1 a point: 12.345 EnakPoint; the 67 sen are lost (Q13). require.NoError(t, e.orders.RefundPayment(e.ctx, payment.ID, 12345.67, "sebagian", e.cashier)) assert.Equal(t, int64(12345), e.returned(order)) assert.Equal(t, int64(50000+12345), e.balance(customer)) e.assertReconciled(customer) } func TestPointRefund_UsesTheFrozenValue(t *testing.T) { e := newPointPaymentEnv(t) settings := NewLoyaltySettingsProcessor(repository.NewLoyaltySettingsRepository(e.db), repository.NewTxManager(e.db)) setValue := func(v int64) { s, err := settings.Organization(context.Background(), e.org) require.NoError(t, err) s.PointValue = v _, _, err = settings.UpdateOrganization(context.Background(), e.org, e.cashier, *s) require.NoError(t, err) } setValue(100) customer := e.customerWith(1000) order := e.order(customer, 50000) payment, err := e.payPoints(order, 500, e.code(customer)) require.NoError(t, err) assert.Equal(t, 50000.0, payment.Amount, "500 × Rp 100") // The value changes before the refund; the customer still gets back what they used. setValue(250) require.NoError(t, e.orders.RefundPayment(e.ctx, payment.ID, 50000, "semua", e.cashier)) assert.Equal(t, int64(500), e.returned(order), "50.000 / the frozen Rp 100, not the new Rp 250") assert.Equal(t, int64(1000), e.balance(customer)) } func TestPointRefund_NoCashForTheEnakPointPart(t *testing.T) { e := newPointPaymentEnv(t) customer := e.customerWith(100000) order := e.order(customer, 50000) _, err := e.payPoints(order, 20000, e.code(customer)) require.NoError(t, err) _, err = e.orders.CreatePayment(e.ctx, &models.CreatePaymentRequest{OrderID: order, PaymentMethodID: e.cash, Amount: 30000}) require.NoError(t, err) amount := 40000.0 err = e.orders.RefundOrder(e.ctx, order, &models.RefundOrderRequest{RefundAmount: &amount}, e.cashier) assert.ErrorIs(t, err, ErrPointPaymentRejected, "Rp 40.000 in cash would include EnakPoint") var refunded float64 require.NoError(t, e.db.Raw(`SELECT refund_amount FROM orders WHERE id = ?`, order).Scan(&refunded).Error) assert.Zero(t, refunded, "nothing was written") amount = 30000 require.NoError(t, e.orders.RefundOrder(e.ctx, order, &models.RefundOrderRequest{RefundAmount: &amount}, e.cashier), "the cash part can be refunded") assert.Zero(t, e.returned(order), "and no EnakPoint came back for it") } // Giving back the EnakPoint part does not take earning back: that part never earned. func TestPointRefund_DoesNotReverseEarning(t *testing.T) { e := newPointPaymentEnv(t) customer := e.customerWith(100000) order := e.order(customer, 87500) pointPayment, err := e.payPoints(order, 20000, e.code(customer)) require.NoError(t, err) _, err = e.orders.CreatePayment(e.ctx, &models.CreatePaymentRequest{OrderID: order, PaymentMethodID: e.cash, Amount: 67500}) require.NoError(t, err) earned := e.sum(`SELECT COALESCE(SUM(amount), 0) FROM wallet_transactions WHERE reference_id = ? AND type = 'EARN'`, order) require.Equal(t, int64(675), earned) require.NoError(t, e.orders.RefundPayment(e.ctx, pointPayment.ID, 20000, "kembali", e.cashier)) assert.Equal(t, int64(20000), e.returned(order)) reversed := e.sum(`SELECT COALESCE(SUM(-amount), 0) FROM wallet_transactions WHERE reference_id = ? AND type = 'EARN_REVERSAL'`, order) assert.Zero(t, reversed, "the EnakPoint part never earned, so giving it back takes nothing") e.assertReconciled(customer) }