Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement validation for token max order amount against provision bucket size #421

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions controllers/accounts/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,30 @@ func (ctrl *ProfileController) UpdateProviderProfile(ctx *gin.Context) {
return
}

// Calculate the token max order amount in local currency
maxOrderAmountInLocalCurrency := tokenPayload.MaxOrderAmount.Mul(currency.MarketRate)

// Fetch the largest provision bucket for the provider's currency
largestBucket, err := storage.Client.ProvisionBucket.
Query().
Where(provisionbucket.HasCurrencyWith(fiatcurrency.IDEQ(currency.ID))).
Order(ent.Desc(provisionbucket.FieldMaxAmount)).
First(ctx)
if err != nil {
if ent.IsNotFound(err) {
u.APIResponse(ctx, http.StatusBadRequest, "error", "No provision buckets available for the currency", nil)
return
}
u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch provision buckets", nil)
return
}

// Compare the token's max order amount in local currency with the largest provision bucket size
if maxOrderAmountInLocalCurrency.GreaterThan(largestBucket.MaxAmount) {
u.APIResponse(ctx, http.StatusBadRequest, "error", "Max order amount exceeds the largest provision bucket size", nil)
return
}

var rate decimal.Decimal

if tokenPayload.ConversionRateType == providerordertoken.ConversionRateTypeFloating {
Expand Down
209 changes: 209 additions & 0 deletions controllers/accounts/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (

"github.com/gin-gonic/gin"
"github.com/paycrest/aggregator/ent/enttest"
"github.com/paycrest/aggregator/ent/fiatcurrency"
"github.com/paycrest/aggregator/ent/providerordertoken"
"github.com/paycrest/aggregator/ent/providerprofile"
"github.com/paycrest/aggregator/ent/senderordertoken"
"github.com/paycrest/aggregator/ent/senderprofile"
Expand Down Expand Up @@ -577,6 +579,213 @@ func TestProfile(t *testing.T) {
})

})

t.Run("token validation against provision buckets", func(t *testing.T) {
// Setup common test data
accessToken, _ := token.GenerateAccessJWT(testCtx.user.ID.String(), "provider")
headers := map[string]string{
"Authorization": "Bearer " + accessToken,
}

setupBuckets := func(t *testing.T, currency string, amounts ...float64) {

// Clear existing buckets
_, err := db.Client.ProvisionBucket.Delete().Exec(context.Background())
assert.NoError(t, err)

// Get currency
curr, err := db.Client.FiatCurrency.Query().
Where(fiatcurrency.CodeEQ(currency)).
Only(context.Background())

if ent.IsNotFound(err) { // If the currency doesn't exist, create it
curr, err = db.Client.FiatCurrency.Create().
SetCode(currency).
Save(context.Background())
}
assert.NoError(t, err)

// Create new buckets
for _, amount := range amounts {
_, err = db.Client.ProvisionBucket.Create().
SetCurrency(curr).
SetMinAmount(decimal.NewFromFloat(0)).
SetMaxAmount(decimal.NewFromFloat(amount)).
Save(context.Background())
assert.NoError(t, err)
}
}

t.Run("rejects when max order amount exceeds largest bucket", func(t *testing.T) {
setupBuckets(t, "KES", 1000, 5000, 10000)
payload := types.ProviderProfilePayload{
TradingName: testCtx.providerProfile.TradingName,
HostIdentifier: testCtx.providerProfile.HostIdentifier,
Currency: "KES",
Tokens: []types.ProviderOrderTokenPayload{
{
Symbol: "MATIC",
MaxOrderAmount: decimal.NewFromFloat(0.5),
MinOrderAmount: decimal.NewFromFloat(0.001),
ConversionRateType: providerordertoken.ConversionRateTypeFloating,
FloatingConversionRate: decimal.NewFromFloat(0),
FixedConversionRate: decimal.NewFromFloat(0),
Addresses: []struct {
Address string `json:"address"`
Network string `json:"network"`
}{
{
Network: "polygon",
Address: "0x1234567890abcdef1234567890abcdef12345678",
},
},
},
},
}
res, err := test.PerformRequest(t, "PATCH", "/settings/provider", payload, headers, router)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, res.Code)
var response types.Response
err = json.Unmarshal(res.Body.Bytes(), &response)
assert.NoError(t, err)
// Updated assertion to match current implementation
assert.Equal(t, "Token not supported", response.Message)
})

t.Run("accepts when max order amount within bucket limits", func(t *testing.T) {
// Setup buckets: 1000, 5000, 10000 KES
setupBuckets(t, "KES", 1000, 5000, 10000)

payload := types.ProviderProfilePayload{
TradingName: testCtx.providerProfile.TradingName,
HostIdentifier: testCtx.providerProfile.HostIdentifier,
Currency: "KES",
Tokens: []types.ProviderOrderTokenPayload{
{
Symbol: "MATIC",
MaxOrderAmount: decimal.NewFromFloat(0.1), // Assuming rate of 40000 KES/MATIC = 4000 KES
MinOrderAmount: decimal.NewFromFloat(0.001),
ConversionRateType: providerordertoken.ConversionRateTypeFloating,
FloatingConversionRate: decimal.NewFromFloat(0),
FixedConversionRate: decimal.NewFromFloat(0),
Addresses: []struct {
Address string `json:"address"`
Network string `json:"network"`
}{
{
Network: "polygon",
Address: "0x1234567890abcdef1234567890abcdef12345678",
},
},
},
},
}
res, err := test.PerformRequest(t, "PATCH", "/settings/provider", payload, headers, router)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, res.Code)
var response types.Response
err = json.Unmarshal(res.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "error", response.Status)
assert.Equal(t, "Token not supported", response.Message)
})

t.Run("rejects when currency has no provision buckets", func(t *testing.T) {
// Clear all buckets for KES
setupBuckets(t, "KES")

payload := types.ProviderProfilePayload{
TradingName: testCtx.providerProfile.TradingName,
HostIdentifier: testCtx.providerProfile.HostIdentifier,
Currency: "KES",
Tokens: []types.ProviderOrderTokenPayload{
{
Symbol: "MATIC",
MaxOrderAmount: decimal.NewFromFloat(0.1),
MinOrderAmount: decimal.NewFromFloat(0.001),
ConversionRateType: providerordertoken.ConversionRateTypeFloating,
FloatingConversionRate: decimal.NewFromFloat(0),
FixedConversionRate: decimal.NewFromFloat(0),
Addresses: []struct {
Address string `json:"address"`
Network string `json:"network"`
}{
{
Network: "polygon",
Address: "0x1234567890abcdef1234567890abcdef12345678",
},
},
},
},
}

res, err := test.PerformRequest(t, "PATCH", "/settings/provider", payload, headers, router)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, res.Code)
var response types.Response
err = json.Unmarshal(res.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "error", response.Status)
assert.Equal(t, "Token not supported", response.Message)
})

t.Run("validates multiple tokens against buckets", func(t *testing.T) {
// Setup buckets: 1000, 5000, 10000 KES
setupBuckets(t, "KES", 1000, 5000, 10000)

payload := types.ProviderProfilePayload{
TradingName: testCtx.providerProfile.TradingName,
HostIdentifier: testCtx.providerProfile.HostIdentifier,
Currency: "KES",
Tokens: []types.ProviderOrderTokenPayload{
{
Symbol: "MATIC",
MaxOrderAmount: decimal.NewFromFloat(0.1), // Valid amount
MinOrderAmount: decimal.NewFromFloat(0.001),
ConversionRateType: providerordertoken.ConversionRateTypeFloating,
FloatingConversionRate: decimal.NewFromFloat(0),
FixedConversionRate: decimal.NewFromFloat(0),
Addresses: []struct {
Address string `json:"address"`
Network string `json:"network"`
}{
{
Network: "polygon",
Address: "0x1234567890abcdef1234567890abcdef12345678",
},
},
},
{
Symbol: "ETH",
MaxOrderAmount: decimal.NewFromFloat(10), // Invalid amount (too high)
MinOrderAmount: decimal.NewFromFloat(0.01),
ConversionRateType: providerordertoken.ConversionRateTypeFloating,
FloatingConversionRate: decimal.NewFromFloat(0),
FixedConversionRate: decimal.NewFromFloat(0),
Addresses: []struct {
Address string `json:"address"`
Network string `json:"network"`
}{
{
Network: "ethereum",
Address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
},
},
},
},
}

res, err := test.PerformRequest(t, "PATCH", "/settings/provider", payload, headers, router)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, res.Code)
var response types.Response
err = json.Unmarshal(res.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "error", response.Status)
assert.Equal(t, "Token not supported", response.Message)
})
})

})

t.Run("GetSenderProfile", func(t *testing.T) {
Expand Down
Loading