package processor import ( "testing" "github.com/stretchr/testify/assert" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/entities" "apskel-pos-be/internal/models" ) // 1 EnakPoint per Rp 100 and 1 EnakCoin per Rp 25.000, as in the F1 example. func prdEarningSettings() models.OutletLoyaltySettings { return models.OutletLoyaltySettings{ Point: models.LoyaltyEarnSettings{Enabled: true, EarnPerAmount: 100, EarnValue: 1}, Coin: models.LoyaltyEarnSettings{Enabled: true, EarnPerAmount: 25000, EarnValue: 1}, } } func TestCalculateEarning_PRDExample(t *testing.T) { // Subtotal after discount Rp 87.500, paid in full in cash. order := &entities.Order{Subtotal: 97500, DiscountAmount: 10000, TaxAmount: 9625, TotalAmount: 97125} got := CalculateEarning(order, prdEarningSettings()) assert.Equal(t, 87500.0, got.Basis) assert.Equal(t, int64(875), got.Point.Amount) assert.Equal(t, int64(3), got.Coin.Amount) } func TestCalculateEarning_TaxIsNotPartOfTheBasis(t *testing.T) { withoutTax := &entities.Order{Subtotal: 50000} withTax := &entities.Order{Subtotal: 50000, TaxAmount: 5500, TotalAmount: 55500} assert.Equal(t, CalculateEarning(withoutTax, prdEarningSettings()), CalculateEarning(withTax, prdEarningSettings())) assert.Equal(t, int64(500), CalculateEarning(withTax, prdEarningSettings()).Point.Amount) } func TestCalculateEarning_BelowMinimum(t *testing.T) { s := prdEarningSettings() s.Point.MinOrderAmount = 50000 assert.Equal(t, int64(0), CalculateEarning(&entities.Order{Subtotal: 49999}, s).Point.Amount) assert.Equal(t, int64(500), CalculateEarning(&entities.Order{Subtotal: 50000}, s).Point.Amount, "the minimum itself earns") // The minimum applies to the basis, after discount. assert.Equal(t, int64(0), CalculateEarning(&entities.Order{Subtotal: 60000, DiscountAmount: 15000}, s).Point.Amount) // Coin has its own minimum. assert.Equal(t, int64(1), CalculateEarning(&entities.Order{Subtotal: 49999}, s).Coin.Amount) } func TestCalculateEarning_MaxPerOrder(t *testing.T) { s := prdEarningSettings() max := int64(300) s.Point.MaxPerOrder = &max got := CalculateEarning(&entities.Order{Subtotal: 87500}, s) assert.Equal(t, int64(300), got.Point.Amount) assert.True(t, got.Point.Capped) assert.Equal(t, int64(300), got.Metadata(got.Point)["max_per_order"]) got = CalculateEarning(&entities.Order{Subtotal: 20000}, s) assert.Equal(t, int64(200), got.Point.Amount) assert.False(t, got.Point.Capped) zero := int64(0) s.Point.MaxPerOrder = &zero assert.Equal(t, int64(0), CalculateEarning(&entities.Order{Subtotal: 87500}, s).Point.Amount) } func TestCalculateEarning_DisabledEarnsNothing(t *testing.T) { s := prdEarningSettings() s.Point.Enabled = false got := CalculateEarning(&entities.Order{Subtotal: 87500}, s) assert.Equal(t, int64(0), got.Point.Amount) assert.Equal(t, int64(3), got.Coin.Amount, "each currency is switched on its own") s.Coin.Enabled = false got = CalculateEarning(&entities.Order{Subtotal: 87500}, s) assert.Equal(t, int64(0), got.Coin.Amount) assert.Equal(t, 87500.0, got.Basis, "the basis is still reported") // The defaults of an outlet that never set anything earn nothing. var defaults models.OutletLoyaltySettings loadLoyaltyFields(outletLoyaltyFields(&defaults), nil, "test") got = CalculateEarning(&entities.Order{Subtotal: 87500}, defaults) assert.Equal(t, int64(0), got.Point.Amount) assert.Equal(t, int64(0), got.Coin.Amount) } func TestCalculateEarning_EdgeCases(t *testing.T) { s := prdEarningSettings() // floor, not round. assert.Equal(t, int64(875), CalculateEarning(&entities.Order{Subtotal: 87599.99}, s).Point.Amount) // Values float64 cannot hold exactly do not lose a point: computed in float64 this // basis divides to 4956.999…, which a naive floor turns into 4956. per250 := prdEarningSettings() per250.Point.EarnPerAmount = 250 assert.Equal(t, int64(4957), CalculateEarning(&entities.Order{Subtotal: 1240155.48, DiscountAmount: 905.48}, per250).Point.Amount) // A discount larger than the subtotal leaves nothing, never a negative amount. got := CalculateEarning(&entities.Order{Subtotal: 10000, DiscountAmount: 15000}, s) assert.Equal(t, 0.0, got.Basis) assert.Equal(t, int64(0), got.Point.Amount) // earn_value multiplies. s.Point.EarnValue = 5 assert.Equal(t, int64(4375), CalculateEarning(&entities.Order{Subtotal: 87500}, s).Point.Amount) // A zero earn_value earns nothing even when enabled. s.Point.EarnValue = 0 assert.Equal(t, int64(0), CalculateEarning(&entities.Order{Subtotal: 87500}, s).Point.Amount) } func TestCalculateEarning_Percentage(t *testing.T) { s := prdEarningSettings() s.Point.EarnMode = constants.LoyaltyEarnModePercentage s.Point.EarnPercent = 1 // 1% of the basis; the per-amount settings are ignored, and coin keeps its own mode. got := CalculateEarning(&entities.Order{Subtotal: 97500, DiscountAmount: 10000}, s) assert.Equal(t, int64(875), got.Point.Amount) assert.Equal(t, int64(3), got.Coin.Amount) // Decimals, and floor, not round: 2.5% of 87.500 is 2187.5. s.Point.EarnPercent = 2.5 assert.Equal(t, int64(2187), CalculateEarning(&entities.Order{Subtotal: 87500}, s).Point.Amount) s.Point.EarnPercent = 0.01 assert.Equal(t, int64(8), CalculateEarning(&entities.Order{Subtotal: 87599.99}, s).Point.Amount) // The minimum and the cap apply as in the other mode. s.Point.EarnPercent = 10 s.Point.MinOrderAmount = 50000 max := int64(6000) s.Point.MaxPerOrder = &max assert.Equal(t, int64(0), CalculateEarning(&entities.Order{Subtotal: 49999}, s).Point.Amount) assert.Equal(t, int64(5000), CalculateEarning(&entities.Order{Subtotal: 50000}, s).Point.Amount) got = CalculateEarning(&entities.Order{Subtotal: 87500}, s) assert.Equal(t, int64(6000), got.Point.Amount) assert.True(t, got.Point.Capped) // The mode and percent are frozen on the ledger row. assert.Equal(t, constants.LoyaltyEarnModePercentage, got.Metadata(got.Point)["earn_mode"]) assert.Equal(t, 10.0, got.Metadata(got.Point)["earn_percent"]) // A zero percent earns nothing even when enabled. s.Point.EarnPercent = 0 assert.Equal(t, int64(0), CalculateEarning(&entities.Order{Subtotal: 87500}, s).Point.Amount) } func TestCalculateEarning_MetadataSnapshot(t *testing.T) { got := CalculateEarning(&entities.Order{Subtotal: 87500}, prdEarningSettings()) assert.Equal(t, entities.Metadata{ "basis": 87500.0, "earn_per_amount": int64(100), "earn_value": int64(1), "min_order_amount": int64(0), "capped": false, }, got.Metadata(got.Point)) }