Fix: Update permission names, models and constants
Permission renames: - BOOK_APPOINTMENT -> CREATE_EXCHANGE - VIEW_OWN_APPOINTMENTS -> VIEW_OWN_EXCHANGES - CANCEL_OWN_APPOINTMENT -> CANCEL_OWN_EXCHANGE - VIEW_ALL_APPOINTMENTS -> VIEW_ALL_EXCHANGES - CANCEL_ANY_APPOINTMENT -> CANCEL_ANY_EXCHANGE - Add COMPLETE_EXCHANGE permission Model changes: - Delete AppointmentStatus enum - Delete Appointment model Schema changes: - Delete BookingRequest (was for old booking) - Delete AppointmentResponse, PaginatedAppointments - Delete BookableSlot, AvailableSlotsResponse (unused) Constants changes: - Remove appointmentStatuses from shared/constants.json - Merge booking constants into exchange section - Update shared_constants.py and validate_constants.py
This commit is contained in:
parent
743129b11d
commit
fa07490b7b
6 changed files with 32 additions and 147 deletions
|
|
@ -41,15 +41,16 @@ class Permission(str, PyEnum):
|
||||||
MANAGE_INVITES = "manage_invites"
|
MANAGE_INVITES = "manage_invites"
|
||||||
VIEW_OWN_INVITES = "view_own_invites"
|
VIEW_OWN_INVITES = "view_own_invites"
|
||||||
|
|
||||||
# Booking permissions (regular users)
|
# Exchange permissions (regular users)
|
||||||
BOOK_APPOINTMENT = "book_appointment"
|
CREATE_EXCHANGE = "create_exchange"
|
||||||
VIEW_OWN_APPOINTMENTS = "view_own_appointments"
|
VIEW_OWN_EXCHANGES = "view_own_exchanges"
|
||||||
CANCEL_OWN_APPOINTMENT = "cancel_own_appointment"
|
CANCEL_OWN_EXCHANGE = "cancel_own_exchange"
|
||||||
|
|
||||||
# Availability/Appointments permissions (admin)
|
# Availability/Exchange permissions (admin)
|
||||||
MANAGE_AVAILABILITY = "manage_availability"
|
MANAGE_AVAILABILITY = "manage_availability"
|
||||||
VIEW_ALL_APPOINTMENTS = "view_all_appointments"
|
VIEW_ALL_EXCHANGES = "view_all_exchanges"
|
||||||
CANCEL_ANY_APPOINTMENT = "cancel_any_appointment"
|
CANCEL_ANY_EXCHANGE = "cancel_any_exchange"
|
||||||
|
COMPLETE_EXCHANGE = "complete_exchange"
|
||||||
|
|
||||||
|
|
||||||
class InviteStatus(str, PyEnum):
|
class InviteStatus(str, PyEnum):
|
||||||
|
|
@ -60,14 +61,6 @@ class InviteStatus(str, PyEnum):
|
||||||
REVOKED = "revoked"
|
REVOKED = "revoked"
|
||||||
|
|
||||||
|
|
||||||
class AppointmentStatus(str, PyEnum):
|
|
||||||
"""Status of an appointment."""
|
|
||||||
|
|
||||||
BOOKED = "booked"
|
|
||||||
CANCELLED_BY_USER = "cancelled_by_user"
|
|
||||||
CANCELLED_BY_ADMIN = "cancelled_by_admin"
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeStatus(str, PyEnum):
|
class ExchangeStatus(str, PyEnum):
|
||||||
"""Status of an exchange trade."""
|
"""Status of an exchange trade."""
|
||||||
|
|
||||||
|
|
@ -92,24 +85,25 @@ ROLE_REGULAR = "regular"
|
||||||
# Role definitions with their permissions
|
# Role definitions with their permissions
|
||||||
ROLE_DEFINITIONS: dict[str, RoleConfig] = {
|
ROLE_DEFINITIONS: dict[str, RoleConfig] = {
|
||||||
ROLE_ADMIN: {
|
ROLE_ADMIN: {
|
||||||
"description": "Administrator with audit/invite/appointment access",
|
"description": "Administrator with audit/invite/exchange access",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
Permission.VIEW_AUDIT,
|
Permission.VIEW_AUDIT,
|
||||||
Permission.FETCH_PRICE,
|
Permission.FETCH_PRICE,
|
||||||
Permission.MANAGE_INVITES,
|
Permission.MANAGE_INVITES,
|
||||||
Permission.MANAGE_AVAILABILITY,
|
Permission.MANAGE_AVAILABILITY,
|
||||||
Permission.VIEW_ALL_APPOINTMENTS,
|
Permission.VIEW_ALL_EXCHANGES,
|
||||||
Permission.CANCEL_ANY_APPOINTMENT,
|
Permission.CANCEL_ANY_EXCHANGE,
|
||||||
|
Permission.COMPLETE_EXCHANGE,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ROLE_REGULAR: {
|
ROLE_REGULAR: {
|
||||||
"description": "Regular user with profile, invite, and booking access",
|
"description": "Regular user with profile, invite, and exchange access",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
Permission.MANAGE_OWN_PROFILE,
|
Permission.MANAGE_OWN_PROFILE,
|
||||||
Permission.VIEW_OWN_INVITES,
|
Permission.VIEW_OWN_INVITES,
|
||||||
Permission.BOOK_APPOINTMENT,
|
Permission.CREATE_EXCHANGE,
|
||||||
Permission.VIEW_OWN_APPOINTMENTS,
|
Permission.VIEW_OWN_EXCHANGES,
|
||||||
Permission.CANCEL_OWN_APPOINTMENT,
|
Permission.CANCEL_OWN_EXCHANGE,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -303,33 +297,6 @@ class Availability(Base):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Appointment(Base):
|
|
||||||
"""User appointment bookings."""
|
|
||||||
|
|
||||||
__tablename__ = "appointments"
|
|
||||||
__table_args__ = (UniqueConstraint("slot_start", name="uq_appointment_slot_start"),)
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
user_id: Mapped[int] = mapped_column(
|
|
||||||
Integer, ForeignKey("users.id"), nullable=False, index=True
|
|
||||||
)
|
|
||||||
user: Mapped[User] = relationship("User", foreign_keys=[user_id], lazy="joined")
|
|
||||||
slot_start: Mapped[datetime] = mapped_column(
|
|
||||||
DateTime(timezone=True), nullable=False, index=True
|
|
||||||
)
|
|
||||||
slot_end: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
||||||
note: Mapped[str | None] = mapped_column(String(144), nullable=True)
|
|
||||||
status: Mapped[AppointmentStatus] = mapped_column(
|
|
||||||
Enum(AppointmentStatus), nullable=False, default=AppointmentStatus.BOOKED
|
|
||||||
)
|
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
|
||||||
DateTime(timezone=True), default=lambda: datetime.now(UTC)
|
|
||||||
)
|
|
||||||
cancelled_at: Mapped[datetime | None] = mapped_column(
|
|
||||||
DateTime(timezone=True), nullable=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PriceHistory(Base):
|
class PriceHistory(Base):
|
||||||
"""Price history records from external exchanges."""
|
"""Price history records from external exchanges."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@ def _to_exchange_response(
|
||||||
@router.get("/price", response_model=ExchangePriceResponse)
|
@router.get("/price", response_model=ExchangePriceResponse)
|
||||||
async def get_exchange_price(
|
async def get_exchange_price(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_current_user: User = Depends(require_permission(Permission.BOOK_APPOINTMENT)),
|
_current_user: User = Depends(require_permission(Permission.CREATE_EXCHANGE)),
|
||||||
) -> ExchangePriceResponse:
|
) -> ExchangePriceResponse:
|
||||||
"""
|
"""
|
||||||
Get the current BTC/EUR price for trading.
|
Get the current BTC/EUR price for trading.
|
||||||
|
|
@ -275,7 +275,7 @@ async def get_exchange_price(
|
||||||
async def create_exchange(
|
async def create_exchange(
|
||||||
request: ExchangeRequest,
|
request: ExchangeRequest,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission(Permission.BOOK_APPOINTMENT)),
|
current_user: User = Depends(require_permission(Permission.CREATE_EXCHANGE)),
|
||||||
) -> ExchangeResponse:
|
) -> ExchangeResponse:
|
||||||
"""
|
"""
|
||||||
Create a new exchange trade booking.
|
Create a new exchange trade booking.
|
||||||
|
|
@ -415,7 +415,7 @@ trades_router = APIRouter(prefix="/api/trades", tags=["trades"])
|
||||||
@trades_router.get("", response_model=list[ExchangeResponse])
|
@trades_router.get("", response_model=list[ExchangeResponse])
|
||||||
async def get_my_trades(
|
async def get_my_trades(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission(Permission.VIEW_OWN_APPOINTMENTS)),
|
current_user: User = Depends(require_permission(Permission.VIEW_OWN_EXCHANGES)),
|
||||||
) -> list[ExchangeResponse]:
|
) -> list[ExchangeResponse]:
|
||||||
"""Get the current user's exchanges, sorted by date (newest first)."""
|
"""Get the current user's exchanges, sorted by date (newest first)."""
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
|
|
@ -432,7 +432,7 @@ async def get_my_trades(
|
||||||
async def cancel_my_trade(
|
async def cancel_my_trade(
|
||||||
exchange_id: int,
|
exchange_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission(Permission.CANCEL_OWN_APPOINTMENT)),
|
current_user: User = Depends(require_permission(Permission.CANCEL_OWN_EXCHANGE)),
|
||||||
) -> ExchangeResponse:
|
) -> ExchangeResponse:
|
||||||
"""Cancel one of the current user's exchanges."""
|
"""Cancel one of the current user's exchanges."""
|
||||||
# Get the exchange with eager loading of user relationship
|
# Get the exchange with eager loading of user relationship
|
||||||
|
|
@ -512,7 +512,7 @@ def _to_admin_exchange_response(exchange: Exchange) -> AdminExchangeResponse:
|
||||||
@admin_trades_router.get("/upcoming", response_model=list[AdminExchangeResponse])
|
@admin_trades_router.get("/upcoming", response_model=list[AdminExchangeResponse])
|
||||||
async def get_upcoming_trades(
|
async def get_upcoming_trades(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_current_user: User = Depends(require_permission(Permission.VIEW_ALL_APPOINTMENTS)),
|
_current_user: User = Depends(require_permission(Permission.VIEW_ALL_EXCHANGES)),
|
||||||
) -> list[AdminExchangeResponse]:
|
) -> list[AdminExchangeResponse]:
|
||||||
"""Get all upcoming booked trades, sorted by slot time ascending."""
|
"""Get all upcoming booked trades, sorted by slot time ascending."""
|
||||||
now = datetime.now(UTC)
|
now = datetime.now(UTC)
|
||||||
|
|
@ -539,7 +539,7 @@ async def get_past_trades(
|
||||||
end_date: date | None = None,
|
end_date: date | None = None,
|
||||||
user_search: str | None = None,
|
user_search: str | None = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_current_user: User = Depends(require_permission(Permission.VIEW_ALL_APPOINTMENTS)),
|
_current_user: User = Depends(require_permission(Permission.VIEW_ALL_EXCHANGES)),
|
||||||
) -> list[AdminExchangeResponse]:
|
) -> list[AdminExchangeResponse]:
|
||||||
"""
|
"""
|
||||||
Get past trades with optional filters.
|
Get past trades with optional filters.
|
||||||
|
|
@ -599,9 +599,7 @@ async def get_past_trades(
|
||||||
async def complete_trade(
|
async def complete_trade(
|
||||||
exchange_id: int,
|
exchange_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_current_user: User = Depends(
|
_current_user: User = Depends(require_permission(Permission.CANCEL_ANY_EXCHANGE)),
|
||||||
require_permission(Permission.CANCEL_ANY_APPOINTMENT)
|
|
||||||
),
|
|
||||||
) -> AdminExchangeResponse:
|
) -> AdminExchangeResponse:
|
||||||
"""Mark a trade as completed. Only possible after slot time has passed."""
|
"""Mark a trade as completed. Only possible after slot time has passed."""
|
||||||
|
|
||||||
|
|
@ -647,9 +645,7 @@ async def complete_trade(
|
||||||
async def mark_no_show(
|
async def mark_no_show(
|
||||||
exchange_id: int,
|
exchange_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_current_user: User = Depends(
|
_current_user: User = Depends(require_permission(Permission.CANCEL_ANY_EXCHANGE)),
|
||||||
require_permission(Permission.CANCEL_ANY_APPOINTMENT)
|
|
||||||
),
|
|
||||||
) -> AdminExchangeResponse:
|
) -> AdminExchangeResponse:
|
||||||
"""Mark a trade as no-show. Only possible after slot time has passed."""
|
"""Mark a trade as no-show. Only possible after slot time has passed."""
|
||||||
|
|
||||||
|
|
@ -693,9 +689,7 @@ async def mark_no_show(
|
||||||
async def admin_cancel_trade(
|
async def admin_cancel_trade(
|
||||||
exchange_id: int,
|
exchange_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_current_user: User = Depends(
|
_current_user: User = Depends(require_permission(Permission.CANCEL_ANY_EXCHANGE)),
|
||||||
require_permission(Permission.CANCEL_ANY_APPOINTMENT)
|
|
||||||
),
|
|
||||||
) -> AdminExchangeResponse:
|
) -> AdminExchangeResponse:
|
||||||
"""Cancel any trade (admin only)."""
|
"""Cancel any trade (admin only)."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ from typing import Generic, TypeVar
|
||||||
from pydantic import BaseModel, EmailStr, field_validator
|
from pydantic import BaseModel, EmailStr, field_validator
|
||||||
|
|
||||||
from models import InviteStatus, Permission
|
from models import InviteStatus, Permission
|
||||||
from shared_constants import NOTE_MAX_LENGTH
|
|
||||||
|
|
||||||
|
|
||||||
class UserCredentials(BaseModel):
|
class UserCredentials(BaseModel):
|
||||||
|
|
@ -173,51 +172,6 @@ class CopyAvailabilityRequest(BaseModel):
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
class BookableSlot(BaseModel):
|
|
||||||
"""A bookable 15-minute slot."""
|
|
||||||
|
|
||||||
start_time: datetime
|
|
||||||
end_time: datetime
|
|
||||||
|
|
||||||
|
|
||||||
class AvailableSlotsResponse(BaseModel):
|
|
||||||
"""Response for available slots on a given date."""
|
|
||||||
|
|
||||||
date: date
|
|
||||||
slots: list[BookableSlot]
|
|
||||||
|
|
||||||
|
|
||||||
class BookingRequest(BaseModel):
|
|
||||||
"""Request to book an appointment."""
|
|
||||||
|
|
||||||
slot_start: datetime
|
|
||||||
note: str | None = None
|
|
||||||
|
|
||||||
@field_validator("note")
|
|
||||||
@classmethod
|
|
||||||
def validate_note_length(cls, v: str | None) -> str | None:
|
|
||||||
if v is not None and len(v) > NOTE_MAX_LENGTH:
|
|
||||||
raise ValueError(f"Note must be at most {NOTE_MAX_LENGTH} characters")
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class AppointmentResponse(BaseModel):
|
|
||||||
"""Response model for an appointment."""
|
|
||||||
|
|
||||||
id: int
|
|
||||||
user_id: int
|
|
||||||
user_email: str
|
|
||||||
slot_start: datetime
|
|
||||||
slot_end: datetime
|
|
||||||
note: str | None
|
|
||||||
status: str
|
|
||||||
created_at: datetime
|
|
||||||
cancelled_at: datetime | None
|
|
||||||
|
|
||||||
|
|
||||||
PaginatedAppointments = PaginatedResponse[AppointmentResponse]
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Price History Schemas
|
# Price History Schemas
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,10 @@ from pathlib import Path
|
||||||
_constants_path = Path(__file__).parent.parent / "shared" / "constants.json"
|
_constants_path = Path(__file__).parent.parent / "shared" / "constants.json"
|
||||||
_constants = json.loads(_constants_path.read_text())
|
_constants = json.loads(_constants_path.read_text())
|
||||||
|
|
||||||
# Booking constants
|
|
||||||
SLOT_DURATION_MINUTES: int = _constants["booking"]["slotDurationMinutes"]
|
|
||||||
MIN_ADVANCE_DAYS: int = _constants["booking"]["minAdvanceDays"]
|
|
||||||
MAX_ADVANCE_DAYS: int = _constants["booking"]["maxAdvanceDays"]
|
|
||||||
NOTE_MAX_LENGTH: int = _constants["booking"]["noteMaxLength"]
|
|
||||||
|
|
||||||
# Exchange constants
|
# Exchange constants
|
||||||
|
SLOT_DURATION_MINUTES: int = _constants["exchange"]["slotDurationMinutes"]
|
||||||
|
MIN_ADVANCE_DAYS: int = _constants["exchange"]["minAdvanceDays"]
|
||||||
|
MAX_ADVANCE_DAYS: int = _constants["exchange"]["maxAdvanceDays"]
|
||||||
EUR_TRADE_MIN: int = _constants["exchange"]["eurTradeMin"]
|
EUR_TRADE_MIN: int = _constants["exchange"]["eurTradeMin"]
|
||||||
EUR_TRADE_MAX: int = _constants["exchange"]["eurTradeMax"]
|
EUR_TRADE_MAX: int = _constants["exchange"]["eurTradeMax"]
|
||||||
EUR_TRADE_INCREMENT: int = _constants["exchange"]["eurTradeIncrement"]
|
EUR_TRADE_INCREMENT: int = _constants["exchange"]["eurTradeIncrement"]
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ from pathlib import Path
|
||||||
from models import (
|
from models import (
|
||||||
ROLE_ADMIN,
|
ROLE_ADMIN,
|
||||||
ROLE_REGULAR,
|
ROLE_REGULAR,
|
||||||
AppointmentStatus,
|
|
||||||
ExchangeStatus,
|
ExchangeStatus,
|
||||||
InviteStatus,
|
InviteStatus,
|
||||||
TradeDirection,
|
TradeDirection,
|
||||||
|
|
@ -42,15 +41,6 @@ def validate_shared_constants() -> None:
|
||||||
f"Invite status mismatch. Expected: {expected_invite_statuses}, Got: {got}"
|
f"Invite status mismatch. Expected: {expected_invite_statuses}, Got: {got}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate appointment statuses
|
|
||||||
expected_appointment_statuses = {s.name: s.value for s in AppointmentStatus}
|
|
||||||
if constants.get("appointmentStatuses") != expected_appointment_statuses:
|
|
||||||
got = constants.get("appointmentStatuses")
|
|
||||||
raise ValueError(
|
|
||||||
f"Appointment status mismatch. "
|
|
||||||
f"Expected: {expected_appointment_statuses}, Got: {got}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate exchange statuses
|
# Validate exchange statuses
|
||||||
expected_exchange_statuses = {s.name: s.value for s in ExchangeStatus}
|
expected_exchange_statuses = {s.name: s.value for s in ExchangeStatus}
|
||||||
if constants.get("exchangeStatuses") != expected_exchange_statuses:
|
if constants.get("exchangeStatuses") != expected_exchange_statuses:
|
||||||
|
|
@ -69,21 +59,12 @@ def validate_shared_constants() -> None:
|
||||||
f"Expected: {expected_trade_directions}, Got: {got}"
|
f"Expected: {expected_trade_directions}, Got: {got}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate booking constants exist with required fields
|
|
||||||
booking = constants.get("booking", {})
|
|
||||||
required_booking_fields = [
|
|
||||||
"slotDurationMinutes",
|
|
||||||
"maxAdvanceDays",
|
|
||||||
"minAdvanceDays",
|
|
||||||
"noteMaxLength",
|
|
||||||
]
|
|
||||||
for field in required_booking_fields:
|
|
||||||
if field not in booking:
|
|
||||||
raise ValueError(f"Missing booking constant '{field}' in constants.json")
|
|
||||||
|
|
||||||
# Validate exchange constants exist with required fields
|
# Validate exchange constants exist with required fields
|
||||||
exchange = constants.get("exchange", {})
|
exchange = constants.get("exchange", {})
|
||||||
required_exchange_fields = [
|
required_exchange_fields = [
|
||||||
|
"slotDurationMinutes",
|
||||||
|
"maxAdvanceDays",
|
||||||
|
"minAdvanceDays",
|
||||||
"eurTradeMin",
|
"eurTradeMin",
|
||||||
"eurTradeMax",
|
"eurTradeMax",
|
||||||
"eurTradeIncrement",
|
"eurTradeIncrement",
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,6 @@
|
||||||
"SPENT": "spent",
|
"SPENT": "spent",
|
||||||
"REVOKED": "revoked"
|
"REVOKED": "revoked"
|
||||||
},
|
},
|
||||||
"appointmentStatuses": {
|
|
||||||
"BOOKED": "booked",
|
|
||||||
"CANCELLED_BY_USER": "cancelled_by_user",
|
|
||||||
"CANCELLED_BY_ADMIN": "cancelled_by_admin"
|
|
||||||
},
|
|
||||||
"exchangeStatuses": {
|
"exchangeStatuses": {
|
||||||
"BOOKED": "booked",
|
"BOOKED": "booked",
|
||||||
"COMPLETED": "completed",
|
"COMPLETED": "completed",
|
||||||
|
|
@ -24,13 +19,10 @@
|
||||||
"BUY": "buy",
|
"BUY": "buy",
|
||||||
"SELL": "sell"
|
"SELL": "sell"
|
||||||
},
|
},
|
||||||
"booking": {
|
"exchange": {
|
||||||
"slotDurationMinutes": 15,
|
"slotDurationMinutes": 15,
|
||||||
"maxAdvanceDays": 30,
|
"maxAdvanceDays": 30,
|
||||||
"minAdvanceDays": 1,
|
"minAdvanceDays": 1,
|
||||||
"noteMaxLength": 144
|
|
||||||
},
|
|
||||||
"exchange": {
|
|
||||||
"eurTradeMin": 100,
|
"eurTradeMin": 100,
|
||||||
"eurTradeMax": 3000,
|
"eurTradeMax": 3000,
|
||||||
"eurTradeIncrement": 20,
|
"eurTradeIncrement": 20,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue