Commit graph

141 commits

Author SHA1 Message Date
361dc8764d
Phase 3.1: Add Exchange page UI
New /exchange page for Bitcoin trading:
- Buy/Sell direction toggle with visual feedback
- Amount slider (€100-€3000 in €20 increments)
- Live price display with auto-refresh every 60s
- Direction-specific pricing (buy +5%, sell -5%)
- Sats amount displayed in BTC format (0.XX XXX XXX)
- Date selection with availability indicators
- Time slot selection grid
- Confirmation panel with trade summary

Regenerate frontend types from updated OpenAPI schema.
2025-12-22 19:59:42 +01:00
811fdf2663
Phase 2.5: Add exchange endpoint tests
Comprehensive test coverage for exchange endpoints:
- Price endpoint: permission checks, price retrieval, staleness, config
- Create exchange: buy/sell, double booking, validation, stale price
- User trades: list trades, cancel own trade, cancel restrictions
- Admin trades: view upcoming/past, complete, no-show, cancel

Tests mock the Bitfinex price fetcher to ensure deterministic results.
2025-12-22 18:48:23 +01:00
d39ada1bef
Phase 2.4: Add admin exchange endpoints
Admin trade management:
- GET /api/admin/trades/upcoming: Upcoming booked trades (sorted by time)
- GET /api/admin/trades/past: Past trades with filters
  - status, start_date, end_date, user_search
- POST /api/admin/trades/{id}/complete: Mark as completed (after slot time)
- POST /api/admin/trades/{id}/no-show: Mark as no-show (after slot time)
- POST /api/admin/trades/{id}/cancel: Admin cancel trade

AdminExchangeResponse includes user contact info for admin view.
2025-12-22 18:34:56 +01:00
ce9159c5b0
Phase 2.1-2.3: Add exchange endpoints
Add exchange trading endpoints:
- POST /api/exchange: Create exchange trade
  - Validates slot, price staleness, EUR amount limits
  - Calculates sats from EUR and agreed price
  - Direction-specific premium (buy=+5%, sell=-5%)

- GET /api/trades: List user's exchanges
- POST /api/trades/{id}/cancel: Cancel user's exchange

Add schemas:
- ExchangeRequest, ExchangeResponse
- ExchangeUserContact, AdminExchangeResponse (for Phase 2.4)
- PaginatedExchanges, PaginatedAdminExchanges
2025-12-22 18:28:56 +01:00
d88d69cf9f
Phase 1.4: Update frontend types
Regenerate API types from OpenAPI schema with new:
- ExchangeStatus enum
- TradeDirection enum
- ExchangeConfigResponse schema
- PriceResponse schema
- ExchangePriceResponse schema
- GET /api/exchange/price endpoint
2025-12-22 18:23:52 +01:00
2702b66fd2
Phase 1.3: Create price endpoint for users
Add GET /api/exchange/price endpoint:
- Available to regular users (BOOK_APPOINTMENT permission)
- Returns current BTC/EUR price with admin premium applied
- Uses cached price from PriceHistory if not stale
- Fetches fresh price from Bitfinex if needed
- Returns is_stale flag when price is older than 5 minutes
- Includes exchange configuration (min/max EUR, increment)
- Handles fetch failures gracefully (returns stale price with error)
2025-12-22 18:22:46 +01:00
61e95e56d5
Phase 1.2: Create Exchange model
Add Exchange model with:
- user_id / user relationship
- slot_start / slot_end (datetime)
- direction (TradeDirection enum: buy/sell)
- eur_amount (int, cents), sats_amount (int)
- market_price_eur, agreed_price_eur (float)
- premium_percentage (int, snapshot)
- status (ExchangeStatus enum)
- created_at, cancelled_at, completed_at timestamps
- unique constraint on slot_start
2025-12-22 18:19:26 +01:00
30e5d0828e
Phase 1.1: Add exchange configuration
- Add exchange constants to shared/constants.json:
  - eurTradeMin: 100, eurTradeMax: 3000, eurTradeIncrement: 20
  - premiumPercentage: 5
  - priceRefreshSeconds: 60, priceStalenessSeconds: 300
- Add exchangeStatuses and tradeDirections to shared constants
- Add ExchangeStatus and TradeDirection enums to models.py
- Update shared_constants.py to export new exchange constants
- Update validate_constants.py to validate new enums and fields
2025-12-22 18:16:35 +01:00
c89e0312fa
Phase 0.3: Update E2E tests for cleanup
- Delete counter.spec.ts and random-jobs.spec.ts
- Rewrite permissions.spec.ts for new permission structure
- Update scripts/e2e.sh: remove worker.py execution
- Update generated api.ts types
2025-12-22 18:13:24 +01:00
a5c1eccb4b
Phase 0.2: Remove frontend deprecated code
- Delete pages: sum, audit, admin/random-jobs
- Delete old homepage (counter) and create redirect page
- Update Header.tsx: remove Counter, Sum, Audit, Random Jobs nav items
- Update auth-context.tsx: remove VIEW_COUNTER, INCREMENT_COUNTER, USE_SUM permissions
- Update profile/page.test.tsx: fix nav link assertions
2025-12-22 18:09:09 +01:00
5bad1e7e17
Phase 0.1: Remove backend deprecated code
- Delete routes: counter.py, sum.py
- Delete jobs.py and worker.py
- Delete tests: test_counter.py, test_jobs.py
- Update audit.py: keep only price-history endpoints
- Update models.py: remove VIEW_COUNTER, INCREMENT_COUNTER, USE_SUM permissions
- Update models.py: remove Counter, SumRecord, CounterRecord, RandomNumberOutcome models
- Update schemas.py: remove sum/counter related schemas
- Update main.py: remove deleted router imports
- Update test_permissions.py: remove tests for deprecated features
- Update test_price_history.py: remove worker-related tests
- Update conftest.py: remove mock_enqueue_job fixture
- Update auth.py: fix example in docstring
2025-12-22 18:07:14 +01:00
ea85198171
updated generated 2025-12-22 17:29:44 +01:00
1af0854d80
fix: improve worker task failure handling
Use asyncio.wait with FIRST_EXCEPTION to:
- Properly name tasks for better error logging
- Cancel remaining tasks when one fails
- Log which specific manager failed before propagating the exception
2025-12-22 16:24:40 +01:00
3806361fac
feat: add FETCH_PRICE permission for manual price fetch endpoint
The POST /api/audit/price-history/fetch endpoint now requires
FETCH_PRICE permission instead of VIEW_AUDIT, which is more
semantically correct since it's a write operation.
2025-12-22 16:22:54 +01:00
54709888e1
refactor: extract httpx mock helpers in price history tests
- create_mock_httpx_client() for mocking AsyncClient with various configs
- create_bitfinex_ticker_response() for creating ticker response arrays

Reduces test boilerplate significantly.
2025-12-22 16:21:18 +01:00
de12300593
fix: use Python datetime for created_at in worker
Consistent with model default which uses datetime.now(UTC) rather than
SQL NOW() for created_at field.
2025-12-22 16:18:51 +01:00
b0d5d51a21
refactor: extract _to_price_history_response helper function
Consistent with other response conversion functions in the codebase
(_to_counter_record_response, _to_invite_response, etc.)
2025-12-22 16:17:43 +01:00
3abc7b18c6
test: add test for network errors (httpx.ConnectError)
The docstring of fetch_btc_eur_price mentions it raises httpx.RequestError
on network errors, but this wasn't tested. Add test for ConnectError.
2025-12-22 16:11:00 +01:00
13d2e4adeb
refactor: move JOB_FETCH_BITCOIN_PRICE constant to worker.py
Unlike JOB_RANDOM_NUMBER which is used for external job enqueueing,
JOB_FETCH_BITCOIN_PRICE is only used internally by the scheduler.
Move it to worker.py to clarify it's not part of the public job API.
2025-12-22 16:10:13 +01:00
a5488fd20b
fix: handle unique constraint violation in manual fetch endpoint
When a duplicate timestamp occurs (rare but possible), return the
existing record instead of failing with a 500 error. This matches
the worker's ON CONFLICT DO NOTHING behavior.

Added test for duplicate timestamp handling.
2025-12-22 16:09:05 +01:00
1c9761e559
feat: add explicit 30 second timeout for Bitfinex API requests
Previously used httpx's implicit default (5 seconds). Explicit timeout
makes behavior clear and gives more time for slow responses.
2025-12-22 16:07:33 +01:00
ec835a2935
refactor: extract 'bitfinex' and 'BTC/EUR' magic strings to constants
Add SOURCE_BITFINEX and PAIR_BTC_EUR constants in price_fetcher.py and
use them consistently in routes/audit.py, worker.py, and tests.
2025-12-22 16:06:56 +01:00
dd7bec6091
test: add E2E tests for price history feature 2025-12-22 15:56:12 +01:00
9db43c474e
test: add unit tests for scheduled Bitcoin price job handler 2025-12-22 15:53:05 +01:00
cd2285395d
feat: add scheduled Bitcoin price fetch job running every minute 2025-12-22 15:51:57 +01:00
4e96b9ee28
feat: add JOB_FETCH_BITCOIN_PRICE constant 2025-12-22 15:49:57 +01:00
c2dd7b5b91
feat: add price history admin page with fetch button 2025-12-22 15:49:41 +01:00
94497f9200
test: add unit tests for price history feature 2025-12-22 15:47:20 +01:00
e3b047f782
feat: add price history GET and POST endpoints 2025-12-22 15:43:46 +01:00
b077c93351
feat: add Bitfinex price fetcher for BTC/EUR 2025-12-22 15:42:59 +01:00
3f3822f7c0
feat: add PriceHistoryResponse schema 2025-12-22 15:42:31 +01:00
7339858a02
feat: add PriceHistory model for storing exchange price data 2025-12-22 15:42:11 +01:00
0957d88785
chore: promote httpx from dev to regular dependency 2025-12-22 15:41:40 +01:00
20f7af7ffd
generated ts 2025-12-22 09:31:19 +01:00
976038c806
no parallel running to avoid flaky tests 2025-12-22 09:31:00 +01:00
e7e3c97102
refactor(backend): standardize model-to-response conversion naming
Issue #8: Inconsistent naming for model-to-response conversion functions.

Changes:
- Rename build_invite_response to _to_invite_response (invites.py)
- Rename _map_counter_record to _to_counter_record_response (audit.py)
- Rename _map_sum_record to _to_sum_record_response (audit.py)

All conversion functions now follow the _to_X_response pattern,
using underscore prefix for module-private functions.
2025-12-22 09:16:05 +01:00
fdab4a5dac
refactor(frontend): extract validation utilities to shared module
Issue #7: Profile validation logic was embedded in page component.

Changes:
- Create utils/validation.ts with shared validation functions:
  - validateEmail: email format validation
  - validateTelegram: handle format with @ prefix
  - validateSignal: username length validation
  - validateNostrNpub: bech32 format validation
  - validateProfileFields: combined validation
- Update profile/page.tsx to use shared validation
- Both frontend and backend now read validation rules from
  shared/constants.json for consistency
2025-12-22 09:13:03 +01:00
53aa54d6c9
refactor(backend): clean up router registration pattern
Issue #6: Multiple routers per file made main.py verbose.

Changes:
- Add 'routers' list export to booking.py and invites.py
- Update main.py to iterate over router lists for multi-router modules
- Keep explicit registration for single-router modules
- Cleaner separation between simple and complex route modules
2025-12-22 09:10:26 +01:00
db7a0dbe28
refactor(backend): extract date range validation utilities
Issue #5: Date validation logic was duplicated across availability
and booking routes.

Changes:
- Add date_validation.py with shared utilities:
  - get_bookable_date_range: returns (min_date, max_date) tuple
  - validate_date_in_range: validates date with contextual errors
- Update routes/availability.py to use shared utilities
- Update routes/booking.py to use shared utilities
- Remove redundant _get_date_range_bounds and _get_bookable_date_range
- Error messages now include context (book, set availability, etc.)
2025-12-22 00:02:41 +01:00
0dd84e90a5
refactor(backend): extract pagination utilities
Issue #4: Pagination logic was repeated across multiple routes.

Changes:
- Add pagination.py with reusable utilities:
  - calculate_total_pages: computes page count from total/per_page
  - calculate_offset: computes offset for given page
  - create_paginated_response: builds PaginatedResponse with metadata
- Update routes/audit.py to use pagination utilities
- Update routes/booking.py to use pagination utilities
- Update routes/invites.py to use pagination utilities

The utilities handle the common pagination math while routes
still manage their own query logic (filters, joins, ordering).
2025-12-22 00:00:24 +01:00
09560296aa
refactor: derive Permission type from generated OpenAPI schema
Issue #3: The frontend Permission enum was manually duplicated from
the backend. While full generation isn't practical, this change
ties the frontend constants to the generated OpenAPI types for
compile-time validation.

Changes:
- Update ConstantsResponse schema to use actual Permission/InviteStatus
  enums (enables OpenAPI to include enum values)
- Import enums in schemas.py (no circular dependency issue)
- Update auth-context.tsx to derive PermissionType from generated schema
- Update meta route to return enum instances instead of string values
- Permission values are now type-checked against the OpenAPI schema

If a permission is added to the backend but not to the frontend's
Permission object, TypeScript will fail to compile. This provides
a safety net without requiring a complex build-time generation step.
2025-12-21 23:55:47 +01:00
21698203fe
refactor(auth): unify authorization patterns with MANAGE_OWN_PROFILE permission
Issue #2: The profile route used a custom role-based check instead
of the permission-based pattern used everywhere else.

Changes:
- Add MANAGE_OWN_PROFILE permission to backend Permission enum
- Add permission to ROLE_REGULAR role definition
- Update profile routes to use require_permission(MANAGE_OWN_PROFILE)
- Remove custom require_regular_user dependency
- Update frontend Permission constant and profile page
- Update invites page to use permission instead of role check
- Update profile tests with proper permission mocking

This ensures consistent authorization patterns across all routes.
2025-12-21 23:50:06 +01:00
81cd34b0e7
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
  - layoutStyles: main, loader, content variants
  - cardStyles: card, tableCard, cardHeader
  - tableStyles: complete table styling
  - paginationStyles: pagination controls
  - formStyles: inputs, labels, errors
  - buttonStyles: primary, secondary, accent, danger variants
  - badgeStyles: status badges with color variants
  - bannerStyles: error/success banners
  - modalStyles: modal overlay and content
  - toastStyles: toast notifications
  - utilityStyles: divider, emptyState, etc.

- Refactor all page components to use shared styles:
  - page.tsx (counter)
  - audit/page.tsx
  - booking/page.tsx
  - appointments/page.tsx
  - profile/page.tsx
  - invites/page.tsx
  - admin/invites/page.tsx
  - admin/availability/page.tsx

- Reduce code duplication significantly (each page now has only
  truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
4d9edd7fd4
refactor: make mock_enqueue_job fixture opt-in instead of autouse
Tests that call POST /api/counter/increment now explicitly request
the mock_enqueue_job fixture. This prevents the mock from masking
issues in other tests that don't need it.
2025-12-21 23:29:48 +01:00
6f3e729b25
refactor: move process_random_number_job import to module level in tests 2025-12-21 23:25:31 +01:00
89fbdb37bd
fix: handle malformed JSON payloads in worker with error logging 2025-12-21 23:24:28 +01:00
ca97ff1aa8
refactor: use JOB_RANDOM_NUMBER constant instead of magic string in worker 2025-12-21 23:23:46 +01:00
027a6fb64c
fix: guard pool initialization with asyncio.Lock to prevent race condition 2025-12-21 23:23:21 +01:00
b33e5e425a
Add trap for cleanup in e2e.sh script
- Added cleanup function to kill background processes
- Trap on EXIT ensures cleanup happens on normal exit, errors, or Ctrl+C
- Prevents orphaned backend/worker processes if script is interrupted
2025-12-21 23:17:17 +01:00
a8ad6e6384
Extract duplicated DATABASE_URL parsing to database.py
- Added ASYNCPG_DATABASE_URL constant in database.py
- Updated jobs.py to import from database module
- Updated worker.py to import from database module
- Removed duplicate URL parsing logic from both files
2025-12-21 23:16:29 +01:00