diff --git a/frontend/app/generated/api.ts b/frontend/app/generated/api.ts index 2f52326..f96d190 100644 --- a/frontend/app/generated/api.ts +++ b/frontend/app/generated/api.ts @@ -192,6 +192,30 @@ export interface paths { patch?: never; trace?: never; }; + "/api/admin/pricing": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Pricing Config + * @description Get current pricing configuration. + */ + get: operations["get_pricing_config_api_admin_pricing_get"]; + /** + * Update Pricing Config + * @description Update pricing configuration. + */ + put: operations["update_pricing_config_api_admin_pricing_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/meta/constants": { parameters: { query?: never; @@ -353,9 +377,8 @@ export interface paths { * * The response includes: * - market_price: The raw price from the exchange - * - premium_percentage: The premium to apply to trades * - is_stale: Whether the price is older than 5 minutes - * - config: Trading configuration (min/max EUR, increment) + * - config: Trading configuration (min/max EUR per direction, premiums, increment) */ get: operations["get_exchange_price_api_exchange_price_get"]; put?: never; @@ -760,14 +783,24 @@ export interface components { * @description Exchange configuration for the frontend. */ ExchangeConfigResponse: { - /** Eur Min */ - eur_min: number; - /** Eur Max */ - eur_max: number; + /** Eur Min Buy */ + eur_min_buy: number; + /** Eur Max Buy */ + eur_max_buy: number; + /** Eur Min Sell */ + eur_min_sell: number; + /** Eur Max Sell */ + eur_max_sell: number; /** Eur Increment */ eur_increment: number; - /** Premium Percentage */ - premium_percentage: number; + /** Premium Buy */ + premium_buy: number; + /** Premium Sell */ + premium_sell: number; + /** Small Trade Threshold Eur */ + small_trade_threshold_eur: number; + /** Small Trade Extra Premium */ + small_trade_extra_premium: number; }; /** * ExchangePriceResponse @@ -939,7 +972,7 @@ export interface components { * @description All available permissions in the system. * @enum {string} */ - Permission: "view_audit" | "fetch_price" | "manage_own_profile" | "manage_invites" | "view_own_invites" | "create_exchange" | "view_own_exchanges" | "cancel_own_exchange" | "manage_availability" | "view_all_exchanges" | "cancel_any_exchange" | "complete_exchange"; + Permission: "view_audit" | "fetch_price" | "manage_own_profile" | "manage_invites" | "view_own_invites" | "create_exchange" | "view_own_exchanges" | "cancel_own_exchange" | "manage_availability" | "manage_pricing" | "view_all_exchanges" | "cancel_any_exchange" | "complete_exchange"; /** * PriceHistoryResponse * @description Response model for a price history record. @@ -969,13 +1002,13 @@ export interface components { * @description Current BTC/EUR price for trading. * * Note: The actual agreed price depends on trade direction (buy/sell) - * and is calculated by the frontend using market_price and premium_percentage. + * and is calculated by the frontend using market_price and premium values. + * Premium calculation: base premium for direction + extra premium if + * trade <= threshold. */ PriceResponse: { /** Market Price */ market_price: number; - /** Premium Percentage */ - premium_percentage: number; /** * Timestamp * Format: date-time @@ -984,6 +1017,50 @@ export interface components { /** Is Stale */ is_stale: boolean; }; + /** + * PricingConfigResponse + * @description Response model for pricing configuration. + */ + PricingConfigResponse: { + /** Premium Buy */ + premium_buy: number; + /** Premium Sell */ + premium_sell: number; + /** Small Trade Threshold Eur */ + small_trade_threshold_eur: number; + /** Small Trade Extra Premium */ + small_trade_extra_premium: number; + /** Eur Min Buy */ + eur_min_buy: number; + /** Eur Max Buy */ + eur_max_buy: number; + /** Eur Min Sell */ + eur_min_sell: number; + /** Eur Max Sell */ + eur_max_sell: number; + }; + /** + * PricingConfigUpdate + * @description Request model for updating pricing configuration. + */ + PricingConfigUpdate: { + /** Premium Buy */ + premium_buy: number; + /** Premium Sell */ + premium_sell: number; + /** Small Trade Threshold Eur */ + small_trade_threshold_eur: number; + /** Small Trade Extra Premium */ + small_trade_extra_premium: number; + /** Eur Min Buy */ + eur_min_buy: number; + /** Eur Max Buy */ + eur_max_buy: number; + /** Eur Min Sell */ + eur_min_sell: number; + /** Eur Max Sell */ + eur_max_sell: number; + }; /** * ProfileResponse * @description Response model for profile data. @@ -1435,6 +1512,59 @@ export interface operations { }; }; }; + get_pricing_config_api_admin_pricing_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PricingConfigResponse"]; + }; + }; + }; + }; + update_pricing_config_api_admin_pricing_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PricingConfigUpdate"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PricingConfigResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_constants_api_meta_constants_get: { parameters: { query?: never; diff --git a/frontend/e2e/admin-pricing.spec.ts b/frontend/e2e/admin-pricing.spec.ts index 275b30d..908b5ff 100644 --- a/frontend/e2e/admin-pricing.spec.ts +++ b/frontend/e2e/admin-pricing.spec.ts @@ -29,15 +29,16 @@ test.describe("Admin Pricing Page - Admin Access", () => { await expect(page.getByRole("heading", { name: "Pricing Configuration" })).toBeVisible(); await expect(page.getByText("Configure premium pricing and trade amount limits")).toBeVisible(); - // Check all form fields are present - await expect(page.getByLabel(/Premium for BUY/i)).toBeVisible(); - await expect(page.getByLabel(/Premium for SELL/i)).toBeVisible(); - await expect(page.getByLabel(/Small Trade Threshold/i)).toBeVisible(); - await expect(page.getByLabel(/Extra Premium for Small Trades/i)).toBeVisible(); - await expect(page.getByLabel(/Minimum Amount.*BUY/i)).toBeVisible(); - await expect(page.getByLabel(/Maximum Amount.*BUY/i)).toBeVisible(); - await expect(page.getByLabel(/Minimum Amount.*SELL/i)).toBeVisible(); - await expect(page.getByLabel(/Maximum Amount.*SELL/i)).toBeVisible(); + // Check all form fields are present (using text + input selector since labels aren't associated) + await expect(page.getByText(/Premium for BUY/i)).toBeVisible(); + await expect(page.locator('input[type="number"]').first()).toBeVisible(); + await expect(page.getByText(/Premium for SELL/i)).toBeVisible(); + await expect(page.getByText(/Small Trade Threshold/i)).toBeVisible(); + await expect(page.getByText(/Extra Premium for Small Trades/i)).toBeVisible(); + await expect(page.getByText(/Trade Amount Limits.*BUY/i)).toBeVisible(); + await expect(page.getByText(/Minimum Amount/i).first()).toBeVisible(); + await expect(page.getByText(/Maximum Amount/i).first()).toBeVisible(); + await expect(page.getByText(/Trade Amount Limits.*SELL/i)).toBeVisible(); // Check save button is present await expect(page.getByRole("button", { name: /Save Changes/i })).toBeVisible(); @@ -54,11 +55,10 @@ test.describe("Admin Pricing Page - Admin Access", () => { const count = await inputs.count(); expect(count).toBeGreaterThan(0); - // Check that premium fields have values - const premiumBuyInput = page.getByLabel(/Premium for BUY/i); - const premiumSellInput = page.getByLabel(/Premium for SELL/i); - const buyValue = await premiumBuyInput.inputValue(); - const sellValue = await premiumSellInput.inputValue(); + // Check that premium fields have values (inputs are after labels) + const inputs = page.locator('input[type="number"]'); + const buyValue = await inputs.nth(0).inputValue(); // First input is premium_buy + const sellValue = await inputs.nth(1).inputValue(); // Second input is premium_sell expect(buyValue).not.toBe(""); expect(sellValue).not.toBe(""); @@ -68,8 +68,9 @@ test.describe("Admin Pricing Page - Admin Access", () => { await page.goto("/admin/pricing"); await page.waitForLoadState("networkidle"); - // Get current values - const premiumBuyInput = page.getByLabel(/Premium for BUY/i); + // Get current values (first input is premium_buy) + const inputs = page.locator('input[type="number"]'); + const premiumBuyInput = inputs.nth(0); const currentBuyValue = await premiumBuyInput.inputValue(); const newBuyValue = currentBuyValue === "5" ? "6" : "5"; @@ -110,7 +111,8 @@ test.describe("Admin Pricing Page - Admin Access", () => { await page.waitForLoadState("networkidle"); // Test premium range validation (should be -100 to 100) - const premiumBuyInput = page.getByLabel(/Premium for BUY/i); + const inputs = page.locator('input[type="number"]'); + const premiumBuyInput = inputs.nth(0); // First input is premium_buy await premiumBuyInput.clear(); await premiumBuyInput.fill("150"); // Invalid: > 100 @@ -122,9 +124,10 @@ test.describe("Admin Pricing Page - Admin Access", () => { // Should show validation error await expect(page.getByText(/must be between.*-100.*100/i)).toBeVisible({ timeout: 2000 }); - // Test min < max validation - const minBuyInput = page.getByLabel(/Minimum Amount.*BUY/i); - const maxBuyInput = page.getByLabel(/Maximum Amount.*BUY/i); + // Test min < max validation (inputs 4 and 5 are min/max buy) + const inputs = page.locator('input[type="number"]'); + const minBuyInput = inputs.nth(4); // Min buy is 5th input (after 4 premium/threshold inputs) + const maxBuyInput = inputs.nth(5); // Max buy is 6th input const currentMin = await minBuyInput.inputValue(); const currentMax = await maxBuyInput.inputValue(); @@ -150,7 +153,9 @@ test.describe("Admin Pricing Page - Admin Access", () => { await page.goto("/admin/pricing"); await page.waitForLoadState("networkidle"); - const smallTradeThresholdInput = page.getByLabel(/Small Trade Threshold/i); + // Small trade threshold is the 3rd input (after premium_buy and premium_sell) + const inputs = page.locator('input[type="number"]'); + const smallTradeThresholdInput = inputs.nth(2); const currentThreshold = await smallTradeThresholdInput.inputValue(); // Update threshold @@ -278,7 +283,7 @@ test.describe("Admin Pricing API", () => { eur_max_sell: 300000, }, }); - expect(invalidResponse.status()).toBe(422); + expect(invalidResponse.status()).toBe(400); // BadRequestError returns 400 // Restore original values await request.put(`${getBackendUrl()}/api/admin/pricing`, { diff --git a/frontend/e2e/exchange.spec.ts b/frontend/e2e/exchange.spec.ts index 8d25750..26cafda 100644 --- a/frontend/e2e/exchange.spec.ts +++ b/frontend/e2e/exchange.spec.ts @@ -339,8 +339,10 @@ test.describe("Exchange API", () => { expect(priceResponse.status()).toBe(200); const priceData = await priceResponse.json(); expect(priceData.config).toBeDefined(); - expect(priceData.config.eur_min).toBeDefined(); - expect(priceData.config.eur_max).toBeDefined(); + expect(priceData.config.eur_min_buy).toBeDefined(); + expect(priceData.config.eur_max_buy).toBeDefined(); + expect(priceData.config.eur_min_sell).toBeDefined(); + expect(priceData.config.eur_max_sell).toBeDefined(); // Test regular user can get trades const tradesResponse = await request.get(`${getBackendUrl()}/api/trades`, {