From 28e8ba218fb4f87c3dd41f1a08d182512be85c20 Mon Sep 17 00:00:00 2001 From: counterweight Date: Tue, 23 Dec 2025 14:40:42 +0100 Subject: [PATCH] Update create_exchange endpoint to accept and validate bitcoin_transfer_method --- backend/routes/exchange.py | 14 ++++++++ backend/tests/test_exchange.py | 66 ++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/backend/routes/exchange.py b/backend/routes/exchange.py index ff58619..00e113b 100644 --- a/backend/routes/exchange.py +++ b/backend/routes/exchange.py @@ -14,6 +14,7 @@ from database import get_db from date_validation import validate_date_in_range from models import ( Availability, + BitcoinTransferMethod, Exchange, ExchangeStatus, Permission, @@ -385,6 +386,18 @@ async def create_exchange( detail=f"Invalid direction: {request.direction}. Must be 'buy' or 'sell'.", ) from None + # Validate bitcoin transfer method + try: + bitcoin_transfer_method = BitcoinTransferMethod(request.bitcoin_transfer_method) + except ValueError: + raise HTTPException( + status_code=400, + detail=( + f"Invalid bitcoin_transfer_method: {request.bitcoin_transfer_method}. " + "Must be 'onchain' or 'lightning'." + ), + ) from None + # Validate EUR amount if request.eur_amount < EUR_TRADE_MIN * 100: raise HTTPException( @@ -468,6 +481,7 @@ async def create_exchange( slot_start=request.slot_start, slot_end=slot_end_dt, direction=direction, + bitcoin_transfer_method=bitcoin_transfer_method, eur_amount=request.eur_amount, sats_amount=sats_amount, market_price_eur=market_price, diff --git a/backend/tests/test_exchange.py b/backend/tests/test_exchange.py index c7ec778..52e4313 100644 --- a/backend/tests/test_exchange.py +++ b/backend/tests/test_exchange.py @@ -180,6 +180,7 @@ class TestCreateExchange: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, # €100 in cents }, ) @@ -187,6 +188,7 @@ class TestCreateExchange: assert response.status_code == 200 data = response.json() assert data["direction"] == "buy" + assert data["bitcoin_transfer_method"] == "onchain" assert data["eur_amount"] == 10000 assert data["status"] == "booked" assert data["sats_amount"] > 0 @@ -207,6 +209,7 @@ class TestCreateExchange: json={ "slot_start": f"{target_date}T10:00:00Z", "direction": "sell", + "bitcoin_transfer_method": "lightning", "eur_amount": 20000, # €200 in cents }, ) @@ -214,6 +217,7 @@ class TestCreateExchange: assert response.status_code == 200 data = response.json() assert data["direction"] == "sell" + assert data["bitcoin_transfer_method"] == "lightning" assert data["eur_amount"] == 20000 # For sell, agreed price is market * 0.95 assert data["agreed_price_eur"] == pytest.approx(19000.0, rel=0.001) @@ -233,6 +237,7 @@ class TestCreateExchange: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -247,6 +252,7 @@ class TestCreateExchange: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -268,6 +274,7 @@ class TestCreateExchange: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "invalid", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -275,6 +282,48 @@ class TestCreateExchange: assert response.status_code == 400 assert "Invalid direction" in response.json()["detail"] + @pytest.mark.asyncio + async def test_missing_bitcoin_transfer_method_rejected( + self, client_factory, regular_user, admin_user + ): + """Missing bitcoin_transfer_method 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: + response = await client.post( + "/api/exchange", + json={ + "slot_start": f"{target_date}T09:00:00Z", + "direction": "buy", + "eur_amount": 10000, + }, + ) + + assert response.status_code == 422 + + @pytest.mark.asyncio + async def test_invalid_bitcoin_transfer_method_rejected( + self, client_factory, regular_user, admin_user + ): + """Invalid bitcoin_transfer_method 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: + response = await client.post( + "/api/exchange", + json={ + "slot_start": f"{target_date}T09:00:00Z", + "direction": "buy", + "bitcoin_transfer_method": "invalid", + "eur_amount": 10000, + }, + ) + + assert response.status_code == 400 + assert "Invalid bitcoin_transfer_method" in response.json()["detail"] + @pytest.mark.asyncio async def test_amount_below_minimum_rejected( self, client_factory, regular_user, admin_user @@ -289,6 +338,7 @@ class TestCreateExchange: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 5000, # €50, below min of €100 }, ) @@ -310,6 +360,7 @@ class TestCreateExchange: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 400000, # €4000, above max of €3000 }, ) @@ -331,6 +382,7 @@ class TestCreateExchange: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 11500, # €115, not multiple of €20 }, ) @@ -352,6 +404,7 @@ class TestCreateExchange: json={ "slot_start": f"{target_date}T09:07:00Z", # 07 is not on 15-min boundary "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -384,6 +437,7 @@ class TestCreateExchange: json={ "slot_start": f"{tomorrow()}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -405,6 +459,7 @@ class TestCreateExchange: json={ "slot_start": f"{target_date}T20:00:00Z", # Outside 09-17 "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -421,6 +476,7 @@ class TestCreateExchange: json={ "slot_start": f"{tomorrow()}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -460,6 +516,7 @@ class TestUserTrades: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -468,6 +525,7 @@ class TestUserTrades: json={ "slot_start": f"{target_date}T10:00:00Z", "direction": "sell", + "bitcoin_transfer_method": "lightning", "eur_amount": 20000, }, ) @@ -496,6 +554,7 @@ class TestCancelTrade: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -524,6 +583,7 @@ class TestCancelTrade: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -567,6 +627,7 @@ class TestCancelTrade: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -601,6 +662,7 @@ class TestCancelTrade: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -671,6 +733,7 @@ class TestAdminUpcomingTrades: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -787,6 +850,7 @@ class TestAdminCompleteTrade: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -920,6 +984,7 @@ class TestAdminCancelTrade: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, ) @@ -948,6 +1013,7 @@ class TestAdminCancelTrade: json={ "slot_start": f"{target_date}T09:00:00Z", "direction": "buy", + "bitcoin_transfer_method": "onchain", "eur_amount": 10000, }, )