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 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,

View file

@ -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,
},
)