Update create_exchange endpoint to accept and validate bitcoin_transfer_method

This commit is contained in:
counterweight 2025-12-23 14:40:42 +01:00
parent d82829ab40
commit 28e8ba218f
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
2 changed files with 80 additions and 0 deletions

View file

@ -14,6 +14,7 @@ from database import get_db
from date_validation import validate_date_in_range from date_validation import validate_date_in_range
from models import ( from models import (
Availability, Availability,
BitcoinTransferMethod,
Exchange, Exchange,
ExchangeStatus, ExchangeStatus,
Permission, Permission,
@ -385,6 +386,18 @@ async def create_exchange(
detail=f"Invalid direction: {request.direction}. Must be 'buy' or 'sell'.", detail=f"Invalid direction: {request.direction}. Must be 'buy' or 'sell'.",
) from None ) 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 # Validate EUR amount
if request.eur_amount < EUR_TRADE_MIN * 100: if request.eur_amount < EUR_TRADE_MIN * 100:
raise HTTPException( raise HTTPException(
@ -468,6 +481,7 @@ async def create_exchange(
slot_start=request.slot_start, slot_start=request.slot_start,
slot_end=slot_end_dt, slot_end=slot_end_dt,
direction=direction, direction=direction,
bitcoin_transfer_method=bitcoin_transfer_method,
eur_amount=request.eur_amount, eur_amount=request.eur_amount,
sats_amount=sats_amount, sats_amount=sats_amount,
market_price_eur=market_price, market_price_eur=market_price,

View file

@ -180,6 +180,7 @@ class TestCreateExchange:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, # €100 in cents "eur_amount": 10000, # €100 in cents
}, },
) )
@ -187,6 +188,7 @@ class TestCreateExchange:
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["direction"] == "buy" assert data["direction"] == "buy"
assert data["bitcoin_transfer_method"] == "onchain"
assert data["eur_amount"] == 10000 assert data["eur_amount"] == 10000
assert data["status"] == "booked" assert data["status"] == "booked"
assert data["sats_amount"] > 0 assert data["sats_amount"] > 0
@ -207,6 +209,7 @@ class TestCreateExchange:
json={ json={
"slot_start": f"{target_date}T10:00:00Z", "slot_start": f"{target_date}T10:00:00Z",
"direction": "sell", "direction": "sell",
"bitcoin_transfer_method": "lightning",
"eur_amount": 20000, # €200 in cents "eur_amount": 20000, # €200 in cents
}, },
) )
@ -214,6 +217,7 @@ class TestCreateExchange:
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["direction"] == "sell" assert data["direction"] == "sell"
assert data["bitcoin_transfer_method"] == "lightning"
assert data["eur_amount"] == 20000 assert data["eur_amount"] == 20000
# For sell, agreed price is market * 0.95 # For sell, agreed price is market * 0.95
assert data["agreed_price_eur"] == pytest.approx(19000.0, rel=0.001) assert data["agreed_price_eur"] == pytest.approx(19000.0, rel=0.001)
@ -233,6 +237,7 @@ class TestCreateExchange:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -247,6 +252,7 @@ class TestCreateExchange:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -268,6 +274,7 @@ class TestCreateExchange:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "invalid", "direction": "invalid",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -275,6 +282,48 @@ class TestCreateExchange:
assert response.status_code == 400 assert response.status_code == 400
assert "Invalid direction" in response.json()["detail"] 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 @pytest.mark.asyncio
async def test_amount_below_minimum_rejected( async def test_amount_below_minimum_rejected(
self, client_factory, regular_user, admin_user self, client_factory, regular_user, admin_user
@ -289,6 +338,7 @@ class TestCreateExchange:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 5000, # €50, below min of €100 "eur_amount": 5000, # €50, below min of €100
}, },
) )
@ -310,6 +360,7 @@ class TestCreateExchange:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 400000, # €4000, above max of €3000 "eur_amount": 400000, # €4000, above max of €3000
}, },
) )
@ -331,6 +382,7 @@ class TestCreateExchange:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 11500, # €115, not multiple of €20 "eur_amount": 11500, # €115, not multiple of €20
}, },
) )
@ -352,6 +404,7 @@ class TestCreateExchange:
json={ json={
"slot_start": f"{target_date}T09:07:00Z", # 07 is not on 15-min boundary "slot_start": f"{target_date}T09:07:00Z", # 07 is not on 15-min boundary
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -384,6 +437,7 @@ class TestCreateExchange:
json={ json={
"slot_start": f"{tomorrow()}T09:00:00Z", "slot_start": f"{tomorrow()}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -405,6 +459,7 @@ class TestCreateExchange:
json={ json={
"slot_start": f"{target_date}T20:00:00Z", # Outside 09-17 "slot_start": f"{target_date}T20:00:00Z", # Outside 09-17
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -421,6 +476,7 @@ class TestCreateExchange:
json={ json={
"slot_start": f"{tomorrow()}T09:00:00Z", "slot_start": f"{tomorrow()}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -460,6 +516,7 @@ class TestUserTrades:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -468,6 +525,7 @@ class TestUserTrades:
json={ json={
"slot_start": f"{target_date}T10:00:00Z", "slot_start": f"{target_date}T10:00:00Z",
"direction": "sell", "direction": "sell",
"bitcoin_transfer_method": "lightning",
"eur_amount": 20000, "eur_amount": 20000,
}, },
) )
@ -496,6 +554,7 @@ class TestCancelTrade:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -524,6 +583,7 @@ class TestCancelTrade:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -567,6 +627,7 @@ class TestCancelTrade:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -601,6 +662,7 @@ class TestCancelTrade:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -671,6 +733,7 @@ class TestAdminUpcomingTrades:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -787,6 +850,7 @@ class TestAdminCompleteTrade:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -920,6 +984,7 @@ class TestAdminCancelTrade:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )
@ -948,6 +1013,7 @@ class TestAdminCancelTrade:
json={ json={
"slot_start": f"{target_date}T09:00:00Z", "slot_start": f"{target_date}T09:00:00Z",
"direction": "buy", "direction": "buy",
"bitcoin_transfer_method": "onchain",
"eur_amount": 10000, "eur_amount": 10000,
}, },
) )