diff --git a/backend/routes/exchange.py b/backend/routes/exchange.py index 00e113b..92d3203 100644 --- a/backend/routes/exchange.py +++ b/backend/routes/exchange.py @@ -33,6 +33,7 @@ from shared_constants import ( EUR_TRADE_INCREMENT, EUR_TRADE_MAX, EUR_TRADE_MIN, + LIGHTNING_MAX_EUR, PREMIUM_PERCENTAGE, PRICE_STALENESS_SECONDS, SLOT_DURATION_MINUTES, @@ -415,6 +416,20 @@ async def create_exchange( detail=f"EUR amount must be a multiple of €{EUR_TRADE_INCREMENT}", ) + # Validate Lightning threshold + if ( + bitcoin_transfer_method == BitcoinTransferMethod.LIGHTNING + and request.eur_amount > LIGHTNING_MAX_EUR * 100 + ): + raise HTTPException( + status_code=400, + detail=( + f"Lightning payments are only allowed for amounts up to " + f"€{LIGHTNING_MAX_EUR}. For amounts above €{LIGHTNING_MAX_EUR}, " + "please use onchain transactions." + ), + ) + # Validate slot timing - compute valid boundaries from slot duration valid_minutes = tuple(range(0, 60, SLOT_DURATION_MINUTES)) if request.slot_start.minute not in valid_minutes: diff --git a/backend/shared_constants.py b/backend/shared_constants.py index 5c5e2ed..2950fff 100644 --- a/backend/shared_constants.py +++ b/backend/shared_constants.py @@ -16,3 +16,4 @@ EUR_TRADE_INCREMENT: int = _constants["exchange"]["eurTradeIncrement"] PREMIUM_PERCENTAGE: int = _constants["exchange"]["premiumPercentage"] PRICE_REFRESH_SECONDS: int = _constants["exchange"]["priceRefreshSeconds"] PRICE_STALENESS_SECONDS: int = _constants["exchange"]["priceStalenessSeconds"] +LIGHTNING_MAX_EUR: int = _constants["exchange"]["lightningMaxEur"] diff --git a/backend/tests/test_exchange.py b/backend/tests/test_exchange.py index 52e4313..a4e6cb5 100644 --- a/backend/tests/test_exchange.py +++ b/backend/tests/test_exchange.py @@ -324,6 +324,77 @@ class TestCreateExchange: assert response.status_code == 400 assert "Invalid bitcoin_transfer_method" in response.json()["detail"] + @pytest.mark.asyncio + async def test_lightning_above_threshold_rejected( + self, client_factory, regular_user, admin_user + ): + """Lightning payment above threshold is rejected.""" + target_date = await setup_availability_and_price(client_factory, admin_user) + + with mock_price_fetcher(20000.0): + async with client_factory.create(cookies=regular_user["cookies"]) as client: + # Try Lightning with amount above threshold (€1000 = 100000 cents) + response = await client.post( + "/api/exchange", + json={ + "slot_start": f"{target_date}T09:00:00Z", + "direction": "buy", + "bitcoin_transfer_method": "lightning", + "eur_amount": 110000, # €1100, above €1000 threshold + }, + ) + + assert response.status_code == 400 + assert "Lightning payments are only allowed" in response.json()["detail"] + + @pytest.mark.asyncio + async def test_lightning_below_threshold_accepted( + self, client_factory, regular_user, admin_user + ): + """Lightning payment below threshold is accepted.""" + target_date = await setup_availability_and_price(client_factory, admin_user) + + with mock_price_fetcher(20000.0): + async with client_factory.create(cookies=regular_user["cookies"]) as client: + # Lightning with amount at threshold + response = await client.post( + "/api/exchange", + json={ + "slot_start": f"{target_date}T09:00:00Z", + "direction": "buy", + "bitcoin_transfer_method": "lightning", + "eur_amount": 100000, # €1000, exactly at threshold + }, + ) + + assert response.status_code == 200 + data = response.json() + assert data["bitcoin_transfer_method"] == "lightning" + + @pytest.mark.asyncio + async def test_onchain_above_threshold_accepted( + self, client_factory, regular_user, admin_user + ): + """Onchain payment above threshold is accepted.""" + target_date = await setup_availability_and_price(client_factory, admin_user) + + with mock_price_fetcher(20000.0): + async with client_factory.create(cookies=regular_user["cookies"]) as client: + # Onchain with amount above Lightning threshold + response = await client.post( + "/api/exchange", + json={ + "slot_start": f"{target_date}T09:00:00Z", + "direction": "buy", + "bitcoin_transfer_method": "onchain", + "eur_amount": 110000, # €1100, above €1000 threshold + }, + ) + + assert response.status_code == 200 + data = response.json() + assert data["bitcoin_transfer_method"] == "onchain" + @pytest.mark.asyncio async def test_amount_below_minimum_rejected( self, client_factory, regular_user, admin_user