diff --git a/frontend/src/components/domain/equipment/EquipmentModal.tsx b/frontend/src/components/domain/equipment/EquipmentModal.tsx index 503094df..ce8fec7c 100644 --- a/frontend/src/components/domain/equipment/EquipmentModal.tsx +++ b/frontend/src/components/domain/equipment/EquipmentModal.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Building2, Settings, Wrench, Thermometer, Activity, CheckCircle, AlertTriangle, Eye, Edit, FileText } from 'lucide-react'; +import { Building2, Settings, Wrench, Thermometer, Activity, CheckCircle, AlertTriangle, Eye, Edit, FileText, Mail } from 'lucide-react'; import { EditViewModal, EditViewModalSection } from '../../ui/EditViewModal/EditViewModal'; import { Equipment } from '../../../api/types/equipment'; @@ -101,7 +101,12 @@ export const EquipmentModal: React.FC = ({ [t('fields.specifications.depth')]: 'depth', [t('fields.current_temperature')]: 'temperature', [t('fields.target_temperature')]: 'targetTemperature', - [t('fields.notes')]: 'notes' + [t('fields.notes')]: 'notes', + [t('fields.support_contact_email')]: 'support_contact_email', + [t('fields.support_contact_phone')]: 'support_contact_phone', + [t('fields.support_contact_company')]: 'support_contact_company', + [t('fields.support_contact_contract_number')]: 'support_contact_contract_number', + [t('fields.support_contact_response_time_sla')]: 'support_contact_response_time_sla' }; const propertyName = fieldLabelKeyMap[field.label]; @@ -120,6 +125,13 @@ export const EquipmentModal: React.FC = ({ ...newEquipment.specifications, [propertyName]: value }; + } else if (propertyName.startsWith('support_contact_')) { + // Handle nested support_contact fields + const contactField = propertyName.replace('support_contact_', ''); + newEquipment.support_contact = { + ...newEquipment.support_contact, + [contactField]: value + }; } else { (newEquipment as any)[propertyName] = value; } @@ -262,6 +274,47 @@ export const EquipmentModal: React.FC = ({ } ] }, + { + title: t('sections.support_contact'), + icon: Mail, + fields: [ + { + label: t('fields.support_contact_email'), + value: equipment.support_contact?.email || '', + type: 'text', + editable: true, + placeholder: t('placeholders.support_contact_email') + }, + { + label: t('fields.support_contact_phone'), + value: equipment.support_contact?.phone || '', + type: 'text', + editable: true, + placeholder: t('placeholders.support_contact_phone') + }, + { + label: t('fields.support_contact_company'), + value: equipment.support_contact?.company || '', + type: 'text', + editable: true, + placeholder: t('placeholders.support_contact_company') + }, + { + label: t('fields.support_contact_contract_number'), + value: equipment.support_contact?.contract_number || '', + type: 'text', + editable: true, + placeholder: t('placeholders.support_contact_contract_number') + }, + { + label: t('fields.support_contact_response_time_sla'), + value: equipment.support_contact?.response_time_sla || 0, + type: 'number', + editable: true, + placeholder: '24' + } + ] + }, { title: t('sections.specifications'), icon: Building2, diff --git a/frontend/src/components/domain/unified-wizard/wizards/EquipmentWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/EquipmentWizard.tsx index 9b8f30e5..459bb61a 100644 --- a/frontend/src/components/domain/unified-wizard/wizards/EquipmentWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/wizards/EquipmentWizard.tsx @@ -73,6 +73,16 @@ const EquipmentDetailsStep: React.FC = ({ dataRef, onDataChange className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]" /> +
+ + handleFieldChange('support_contact', { ...data.support_contact, email: e.target.value })} + placeholder={t('equipment.fields.supportContactEmailPlaceholder')} + className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]" + /> +
); diff --git a/frontend/src/components/subscription/SubscriptionPricingCards.tsx b/frontend/src/components/subscription/SubscriptionPricingCards.tsx index f3521c0d..47bbaf86 100644 --- a/frontend/src/components/subscription/SubscriptionPricingCards.tsx +++ b/frontend/src/components/subscription/SubscriptionPricingCards.tsx @@ -184,9 +184,10 @@ export const SubscriptionPricingCards: React.FC = const topBenefits = getTopBenefits(tierKey, plan); const CardWrapper = mode === 'landing' ? Link : 'div'; + const isCurrentPlan = mode === 'settings' && selectedPlan === tier; const cardProps = mode === 'landing' ? { to: getRegisterUrl(tier) } - : mode === 'selection' || mode === 'settings' + : mode === 'selection' || (mode === 'settings' && !isCurrentPlan) ? { onClick: () => handlePlanAction(tier, plan) } : {}; @@ -198,8 +199,10 @@ export const SubscriptionPricingCards: React.FC = {...cardProps} className={` relative rounded-2xl p-8 transition-all duration-300 block no-underline - ${mode === 'settings' || mode === 'selection' || mode === 'landing' ? 'cursor-pointer' : ''} - ${isSelected + ${(mode === 'settings' && !isCurrentPlan) || mode === 'selection' || mode === 'landing' ? 'cursor-pointer' : ''} + ${isCurrentPlan + ? 'bg-[var(--bg-secondary)] border-2 border-[var(--color-primary)] opacity-70 cursor-not-allowed' + : isSelected ? 'bg-gradient-to-br from-green-600 to-emerald-700 shadow-2xl ring-4 ring-green-400/60 scale-105 z-20 transform animate-[pulse_2s_ease-in-out_infinite]' : isPopular ? 'bg-gradient-to-br from-blue-600 to-blue-800 shadow-xl ring-2 ring-blue-400 lg:scale-105 lg:z-10 hover:ring-4 hover:ring-blue-300 hover:shadow-2xl' @@ -207,8 +210,18 @@ export const SubscriptionPricingCards: React.FC = } `} > + {/* Current Plan Badge */} + {isCurrentPlan && ( +
+
+ + {t('ui.current_plan', 'Plan Actual')} +
+
+ )} + {/* Selected Badge */} - {isSelected && ( + {isSelected && !isCurrentPlan && (
@@ -218,7 +231,7 @@ export const SubscriptionPricingCards: React.FC = )} {/* Popular Badge */} - {isPopular && !isSelected && ( + {isPopular && !isSelected && !isCurrentPlan && (
@@ -229,10 +242,10 @@ export const SubscriptionPricingCards: React.FC = {/* Plan Header */}
-

+

{t(`plans.${tier}.name`, plan.name)}

-

+

{plan.tagline_key ? t(plan.tagline_key) : plan.tagline || ''}

@@ -240,17 +253,17 @@ export const SubscriptionPricingCards: React.FC = {/* Price */}
- + {subscriptionService.formatPrice(price)} - + /{billingCycle === 'monthly' ? t('ui.per_month') : t('ui.per_year')}
{/* Trial Badge - Always Visible */}
{savings ? t('ui.save_amount', { amount: subscriptionService.formatPrice(savings.savingsAmount) }) @@ -264,9 +277,9 @@ export const SubscriptionPricingCards: React.FC = {/* Perfect For */} {plan.recommended_for_key && (
-

+

{t(plan.recommended_for_key)}

@@ -275,12 +288,12 @@ export const SubscriptionPricingCards: React.FC = {/* Feature Inheritance Indicator */} {tier === SUBSCRIPTION_TIERS.PROFESSIONAL && (

✓ {t('ui.feature_inheritance_professional')}

@@ -288,12 +301,12 @@ export const SubscriptionPricingCards: React.FC = )} {tier === SUBSCRIPTION_TIERS.ENTERPRISE && (

✓ {t('ui.feature_inheritance_enterprise')}

@@ -307,34 +320,34 @@ export const SubscriptionPricingCards: React.FC =
- +
- + {formatFeatureName(feature)}
))} {/* Key Limits (Users, Locations, Products) */} -
+
- - + + {formatLimit(plan.limits.users, 'limits.users_unlimited')} {t('limits.users_label', 'usuarios')}
- - + + {formatLimit(plan.limits.locations, 'limits.locations_unlimited')} {t('limits.locations_label', 'ubicaciones')}
- - + + {formatLimit(plan.limits.products, 'limits.products_unlimited')} {t('limits.products_label', 'productos')}
@@ -343,33 +356,45 @@ export const SubscriptionPricingCards: React.FC = {/* CTA Button */} {/* Footer */} -

+

{showPilotBanner ? t('ui.free_trial_footer', { months: pilotTrialMonths }) : t('ui.free_trial_footer', { months: 0 }) diff --git a/frontend/src/contexts/SSEContext.tsx b/frontend/src/contexts/SSEContext.tsx index 894ee40f..b1fdc5df 100644 --- a/frontend/src/contexts/SSEContext.tsx +++ b/frontend/src/contexts/SSEContext.tsx @@ -1,9 +1,10 @@ import React, { createContext, useContext, useEffect, useRef, useState, ReactNode, useCallback } from 'react'; import { useAuthStore } from '../stores/auth.store'; import { useCurrentTenant } from '../stores/tenant.store'; -import { showToast } from '../utils/toast'; -import i18n from '../i18n'; -import { getTenantCurrencySymbol } from '../hooks/useTenantCurrency'; +// Toast notifications disabled - imports commented out +// import { showToast } from '../utils/toast'; +// import i18n from '../i18n'; +// import { getTenantCurrencySymbol } from '../hooks/useTenantCurrency'; interface SSEEvent { type: string; @@ -164,6 +165,9 @@ export const SSEProvider: React.FC = ({ children }) => { setLastEvent(sseEvent); + // Toast notifications disabled for SSE alerts + // Code below kept for potential future re-enablement + /* // Determine toast type based on enriched priority_level and type_class let toastType: 'info' | 'success' | 'warning' | 'error' = 'info'; @@ -217,6 +221,7 @@ export const SSEProvider: React.FC = ({ children }) => { } showToast[toastType](message, { title, duration }); + */ // Trigger listeners with enriched alert data // Wrap in queueMicrotask to prevent setState during render warnings @@ -264,6 +269,9 @@ export const SSEProvider: React.FC = ({ children }) => { setLastEvent(sseEvent); + // Toast notifications disabled for SSE notifications + // Code below kept for potential future re-enablement + /* // Determine toast type based on notification priority or type let toastType: 'info' | 'success' | 'warning' | 'error' = 'info'; @@ -341,6 +349,7 @@ export const SSEProvider: React.FC = ({ children }) => { const duration = data.priority_level === 'critical' ? 0 : 5000; showToast[toastType](message, { title, duration }); + */ // Trigger listeners with notification data // Wrap in queueMicrotask to prevent setState during render warnings @@ -383,6 +392,9 @@ export const SSEProvider: React.FC = ({ children }) => { setLastEvent(sseEvent); + // Toast notifications disabled for SSE recommendations + // Code below kept for potential future re-enablement + /* // Recommendations are typically positive insights let toastType: 'info' | 'success' | 'warning' | 'error' = 'info'; @@ -458,6 +470,7 @@ export const SSEProvider: React.FC = ({ children }) => { const duration = 5000; // Recommendations are typically informational showToast[toastType](message, { title, duration }); + */ // Trigger listeners with recommendation data // Wrap in queueMicrotask to prevent setState during render warnings diff --git a/frontend/src/hooks/useSubscriptionAwareRoutes.ts b/frontend/src/hooks/useSubscriptionAwareRoutes.ts index 1218bbcf..abf4e783 100644 --- a/frontend/src/hooks/useSubscriptionAwareRoutes.ts +++ b/frontend/src/hooks/useSubscriptionAwareRoutes.ts @@ -5,18 +5,35 @@ import { useMemo } from 'react'; import { RouteConfig } from '../router/routes.config'; import { useSubscription } from '../api/hooks/subscription'; +import { useCurrentTenant } from '../stores'; +import { usePremises } from '../api/hooks/usePremises'; export const useSubscriptionAwareRoutes = (routes: RouteConfig[]) => { const { subscriptionInfo, canAccessAnalytics } = useSubscription(); + const currentTenant = useCurrentTenant(); + + // Check if tenant has child tenants (only for enterprise tier) + const isEnterprise = subscriptionInfo.plan === 'enterprise'; + const { data: childTenants = [] } = usePremises( + currentTenant?.id || '', + {}, + { enabled: isEnterprise && !!currentTenant?.id } + ); + const hasChildTenants = childTenants.length > 0; const filteredRoutes = useMemo(() => { const filterRoutesBySubscription = (routeList: RouteConfig[]): RouteConfig[] => { return routeList.reduce((filtered, route) => { // Check if route requires subscription features if (route.requiredSubscriptionFeature) { + // Special case for multi_tenant_management which requires enterprise AND child tenants + if (route.requiredSubscriptionFeature === 'multi_tenant_management') { + if (!isEnterprise || !hasChildTenants) { + return filtered; // Skip this route if not enterprise or no child tenants + } + } // Special case for distribution feature which requires enterprise - if (route.requiredSubscriptionFeature === 'distribution') { - const isEnterprise = subscriptionInfo.plan === 'enterprise'; + else if (route.requiredSubscriptionFeature === 'distribution') { if (!isEnterprise) { return filtered; // Skip this route } @@ -70,7 +87,7 @@ export const useSubscriptionAwareRoutes = (routes: RouteConfig[]) => { } return filterRoutesBySubscription(routes); - }, [routes, subscriptionInfo, canAccessAnalytics]); + }, [routes, subscriptionInfo, canAccessAnalytics, isEnterprise, hasChildTenants]); return { filteredRoutes, diff --git a/frontend/src/locales/en/equipment.json b/frontend/src/locales/en/equipment.json index 510ca580..ce36c91b 100644 --- a/frontend/src/locales/en/equipment.json +++ b/frontend/src/locales/en/equipment.json @@ -53,6 +53,11 @@ "severity": "Severity", "photos": "Photos", "estimated_impact": "Production Impact", + "support_contact_email": "Support Contact Email", + "support_contact_phone": "Support Contact Phone", + "support_contact_company": "Support Company", + "support_contact_contract_number": "Contract Number", + "support_contact_response_time_sla": "Response Time SLA (hours)", "specifications": { "power": "Power", "capacity": "Capacity", @@ -96,6 +101,7 @@ "performance": "Performance", "maintenance": "Maintenance Information", "maintenance_info": "Maintenance Information", + "support_contact": "Support Contact Information", "specifications": "Specifications", "temperature_monitoring": "Temperature Monitoring", "notes": "Notes", @@ -111,7 +117,11 @@ "notes": "Additional notes and observations", "technician": "Assigned technician name", "parts_needed": "List of required parts and materials", - "maintenance_description": "Description of the maintenance work to be performed" + "maintenance_description": "Description of the maintenance work to be performed", + "support_contact_email": "support@company.com", + "support_contact_phone": "+1-800-555-1234", + "support_contact_company": "Support company name", + "support_contact_contract_number": "CONTRACT-2024-001" }, "descriptions": { "equipment_efficiency": "Current equipment efficiency percentage", diff --git a/frontend/src/locales/en/subscription.json b/frontend/src/locales/en/subscription.json index fb324a04..3164340d 100644 --- a/frontend/src/locales/en/subscription.json +++ b/frontend/src/locales/en/subscription.json @@ -142,6 +142,8 @@ "choose_starter": "Choose Starter", "choose_professional": "Choose Professional", "choose_enterprise": "Choose Enterprise", + "change_subscription": "Change Subscription", + "current_plan": "Current Plan", "compare_plans": "Compare Plans", "detailed_feature_comparison": "Detailed feature comparison across all subscription tiers", "payback_period": "Pays for itself in {days} days", diff --git a/frontend/src/locales/en/wizards.json b/frontend/src/locales/en/wizards.json index 57a13ef4..fd633c80 100644 --- a/frontend/src/locales/en/wizards.json +++ b/frontend/src/locales/en/wizards.json @@ -1104,7 +1104,9 @@ "location": "Location", "locationPlaceholder": "E.g., Main kitchen", "status": "Status", - "installDate": "Install Date" + "installDate": "Install Date", + "supportContactEmail": "Support Contact Email", + "supportContactEmailPlaceholder": "support@company.com" }, "types": { "oven": "Oven", diff --git a/frontend/src/locales/es/equipment.json b/frontend/src/locales/es/equipment.json index 45c67832..ace9302b 100644 --- a/frontend/src/locales/es/equipment.json +++ b/frontend/src/locales/es/equipment.json @@ -53,6 +53,11 @@ "severity": "Gravedad", "photos": "Fotos", "estimated_impact": "Impacto en la Producción", + "support_contact_email": "Email de Contacto de Soporte", + "support_contact_phone": "Teléfono de Contacto de Soporte", + "support_contact_company": "Empresa de Soporte", + "support_contact_contract_number": "Número de Contrato", + "support_contact_response_time_sla": "SLA Tiempo de Respuesta (horas)", "specifications": { "power": "Potencia", "capacity": "Capacidad", @@ -96,6 +101,7 @@ "performance": "Rendimiento", "maintenance": "Información de Mantenimiento", "maintenance_info": "Información de Mantenimiento", + "support_contact": "Información de Contacto de Soporte", "specifications": "Especificaciones", "temperature_monitoring": "Monitoreo de Temperatura", "notes": "Notas", @@ -111,7 +117,11 @@ "notes": "Notas y observaciones adicionales", "technician": "Nombre del técnico asignado", "parts_needed": "Lista de repuestos y materiales necesarios", - "maintenance_description": "Descripción del trabajo a realizar" + "maintenance_description": "Descripción del trabajo a realizar", + "support_contact_email": "soporte@empresa.com", + "support_contact_phone": "+34-900-555-1234", + "support_contact_company": "Nombre de la empresa de soporte", + "support_contact_contract_number": "CONTRATO-2024-001" }, "descriptions": { "equipment_efficiency": "Porcentaje de eficiencia actual de los equipos", diff --git a/frontend/src/locales/es/subscription.json b/frontend/src/locales/es/subscription.json index 6206e0fd..deb221d8 100644 --- a/frontend/src/locales/es/subscription.json +++ b/frontend/src/locales/es/subscription.json @@ -142,6 +142,8 @@ "choose_starter": "Elegir Starter", "choose_professional": "Elegir Professional", "choose_enterprise": "Elegir Enterprise", + "change_subscription": "Cambiar Suscripción", + "current_plan": "Plan Actual", "compare_plans": "Comparar Planes", "detailed_feature_comparison": "Comparación detallada de características entre todos los niveles de suscripción", "payback_period": "Se paga solo en {days} días", @@ -149,6 +151,12 @@ "calculate_savings": "Calcular Mis Ahorros", "feature_inheritance_starter": "Incluye todas las características esenciales", "feature_inheritance_professional": "Todas las características de Starter +", - "feature_inheritance_enterprise": "Todas las características de Professional +" + "feature_inheritance_enterprise": "Todas las características de Professional +", + "update_payment_method": "Actualizar Método de Pago", + "payment_method_updated": "Método de pago actualizado correctamente", + "payment_details": "Detalles de Pago", + "payment_info_secure": "Tu información de pago está protegida con encriptación de extremo a extremo", + "updating_payment": "Actualizando...", + "cancel": "Cancelar" } } diff --git a/frontend/src/locales/es/wizards.json b/frontend/src/locales/es/wizards.json index 661f46ce..35938395 100644 --- a/frontend/src/locales/es/wizards.json +++ b/frontend/src/locales/es/wizards.json @@ -1129,7 +1129,9 @@ "locationPlaceholder": "Ej: Cocina principal", "status": "Estado", "purchaseDate": "Fecha de Compra", - "installDate": "Fecha de Instalación" + "installDate": "Fecha de Instalación", + "supportContactEmail": "Email de Contacto de Soporte", + "supportContactEmailPlaceholder": "soporte@empresa.com" }, "types": { "oven": "Horno", diff --git a/frontend/src/locales/eu/equipment.json b/frontend/src/locales/eu/equipment.json index 5b20843d..25302f52 100644 --- a/frontend/src/locales/eu/equipment.json +++ b/frontend/src/locales/eu/equipment.json @@ -53,6 +53,11 @@ "severity": "Larritasuna", "photos": "Argazkiak", "estimated_impact": "Ekoizpenaren gaineko eragina", + "support_contact_email": "Laguntza kontaktuaren emaila", + "support_contact_phone": "Laguntza kontaktuaren telefonoa", + "support_contact_company": "Laguntza enpresa", + "support_contact_contract_number": "Kontratu zenbakia", + "support_contact_response_time_sla": "Erantzun-denboraren SLA (orduak)", "specifications": { "power": "Potentzia", "capacity": "Edukiera", @@ -93,6 +98,7 @@ "performance": "Errendimendua", "maintenance": "Mantentze informazioa", "maintenance_info": "Mantentze informazioa", + "support_contact": "Laguntza kontaktuaren informazioa", "specifications": "Zehaztapenak", "temperature_monitoring": "Tenperatura-jarraipena", "notes": "Oharrak", @@ -108,7 +114,11 @@ "notes": "Ohar eta behaketa gehigarriak", "technician": "Esleitutako teknikariaren izena", "parts_needed": "Beharrezko piezen eta materialen zerrenda", - "maintenance_description": "Egingo den lanaren deskribapena" + "maintenance_description": "Egingo den lanaren deskribapena", + "support_contact_email": "laguntza@enpresa.eus", + "support_contact_phone": "+34-900-555-1234", + "support_contact_company": "Laguntza enpresaren izena", + "support_contact_contract_number": "KONTRATUA-2024-001" }, "descriptions": { "equipment_efficiency": "Uneko makinaren eraginkortasun-ehunekoa", diff --git a/frontend/src/locales/eu/subscription.json b/frontend/src/locales/eu/subscription.json index 90bb7be8..3fc507d3 100644 --- a/frontend/src/locales/eu/subscription.json +++ b/frontend/src/locales/eu/subscription.json @@ -140,6 +140,8 @@ "choose_starter": "Aukeratu Starter", "choose_professional": "Aukeratu Professional", "choose_enterprise": "Aukeratu Enterprise", + "change_subscription": "Aldatu Harpidetza", + "current_plan": "Oraingo Plana", "compare_plans": "Konparatu Planak", "detailed_feature_comparison": "Ezaugarrien konparazio zehatza harpidetza maila guztien artean", "payback_period": "Bere burua ordaintzen du {days} egunetan", diff --git a/frontend/src/locales/eu/wizards.json b/frontend/src/locales/eu/wizards.json index 7370325b..88e336a2 100644 --- a/frontend/src/locales/eu/wizards.json +++ b/frontend/src/locales/eu/wizards.json @@ -563,6 +563,42 @@ } } }, + "equipment": { + "title": "Gehitu Ekipamendua", + "equipmentDetails": "Ekipamenduaren Xehetasunak", + "subtitle": "Okindegiaren Ekipamendua", + "fields": { + "name": "Ekipamenduaren Izena", + "namePlaceholder": "Adib: Ekoizpen Labe Nagusia", + "type": "Ekipamendu Mota", + "model": "Modeloa", + "modelPlaceholder": "Adib: Rational SCC 101", + "location": "Kokapena", + "locationPlaceholder": "Adib: Sukalde nagusia", + "status": "Egoera", + "installDate": "Instalazio Data", + "supportContactEmail": "Laguntza Kontaktuaren Emaila", + "supportContactEmailPlaceholder": "laguntza@enpresa.eus" + }, + "types": { + "oven": "Labea", + "mixer": "Nahasmaila", + "proofer": "Fermentatzailea", + "freezer": "Izozkailua", + "packaging": "Ontziratzailea", + "other": "Bestelakoa" + }, + "steps": { + "equipmentDetails": "Ekipamenduaren Xehetasunak", + "equipmentDetailsDescription": "Mota, modeloa, kokapena" + }, + "messages": { + "errorGettingTenant": "Ezin izan da tenant informazioa lortu", + "noBrand": "Markarik gabe", + "successCreate": "Ekipamendua ondo sortu da", + "errorCreate": "Errorea ekipamendua sortzean" + } + }, "itemTypeSelector": { "title": "Hautatu Mota", "description": "Aukeratu zer gehitu nahi duzun", diff --git a/gateway/app/routes/subscription.py b/gateway/app/routes/subscription.py index ac68d865..42d57191 100644 --- a/gateway/app/routes/subscription.py +++ b/gateway/app/routes/subscription.py @@ -59,6 +59,18 @@ async def proxy_subscription_reactivate(request: Request): target_path = "/api/v1/subscriptions/reactivate" return await _proxy_to_tenant_service(request, target_path) +@router.api_route("/usage-forecast", methods=["GET", "OPTIONS"]) +async def proxy_usage_forecast(request: Request): + """Proxy usage forecast request to tenant service""" + target_path = "/api/v1/usage-forecast" + return await _proxy_to_tenant_service(request, target_path) + +@router.api_route("/usage-forecast/track-usage", methods=["POST", "OPTIONS"]) +async def proxy_track_usage(request: Request): + """Proxy track usage request to tenant service""" + target_path = "/api/v1/usage-forecast/track-usage" + return await _proxy_to_tenant_service(request, target_path) + # ================================================================ # PROXY HELPER FUNCTIONS # ================================================================ diff --git a/services/tenant/app/api/usage_forecast.py b/services/tenant/app/api/usage_forecast.py index 9bc677ea..35116eea 100644 --- a/services/tenant/app/api/usage_forecast.py +++ b/services/tenant/app/api/usage_forecast.py @@ -13,6 +13,7 @@ import redis.asyncio as redis from shared.auth.decorators import get_current_user_dep from app.core.config import settings +from app.core.database import database_manager from app.services.subscription_limit_service import SubscriptionLimitService router = APIRouter(prefix="/usage-forecast", tags=["usage-forecast"]) @@ -180,82 +181,84 @@ async def get_usage_forecast( """ # Initialize services redis_client = await get_redis_client() - limit_service = SubscriptionLimitService() + limit_service = SubscriptionLimitService(database_manager=database_manager) try: - # Get current usage summary + # Get current usage summary (includes limits) usage_summary = await limit_service.get_usage_summary(tenant_id) - subscription = await limit_service.get_active_subscription(tenant_id) - if not subscription: + if not usage_summary or 'error' in usage_summary: raise HTTPException( status_code=404, detail=f"No active subscription found for tenant {tenant_id}" ) + # Extract usage data + usage = usage_summary.get('usage', {}) + # Define metrics to forecast metric_configs = [ { 'key': 'users', 'label': 'Users', - 'current': usage_summary['users'], - 'limit': subscription.max_users, + 'current': usage.get('users', {}).get('current', 0), + 'limit': usage.get('users', {}).get('limit'), 'unit': '' }, { 'key': 'locations', 'label': 'Locations', - 'current': usage_summary['locations'], - 'limit': subscription.max_locations, + 'current': usage.get('locations', {}).get('current', 0), + 'limit': usage.get('locations', {}).get('limit'), 'unit': '' }, { 'key': 'products', 'label': 'Products', - 'current': usage_summary['products'], - 'limit': subscription.max_products, + 'current': usage.get('products', {}).get('current', 0), + 'limit': usage.get('products', {}).get('limit'), 'unit': '' }, { 'key': 'recipes', 'label': 'Recipes', - 'current': usage_summary['recipes'], - 'limit': subscription.max_recipes, + 'current': usage.get('recipes', {}).get('current', 0), + 'limit': usage.get('recipes', {}).get('limit'), 'unit': '' }, { 'key': 'suppliers', 'label': 'Suppliers', - 'current': usage_summary['suppliers'], - 'limit': subscription.max_suppliers, + 'current': usage.get('suppliers', {}).get('current', 0), + 'limit': usage.get('suppliers', {}).get('limit'), 'unit': '' }, { 'key': 'training_jobs', 'label': 'Training Jobs', - 'current': usage_summary.get('training_jobs_today', 0), - 'limit': subscription.max_training_jobs_per_day, + 'current': usage.get('training_jobs_today', {}).get('current', 0), + 'limit': usage.get('training_jobs_today', {}).get('limit'), 'unit': '/day' }, { 'key': 'forecasts', 'label': 'Forecasts', - 'current': usage_summary.get('forecasts_today', 0), - 'limit': subscription.max_forecasts_per_day, + 'current': usage.get('forecasts_today', {}).get('current', 0), + 'limit': usage.get('forecasts_today', {}).get('limit'), 'unit': '/day' }, { 'key': 'api_calls', 'label': 'API Calls', - 'current': usage_summary.get('api_calls_this_hour', 0), - 'limit': subscription.max_api_calls_per_hour, + 'current': usage.get('api_calls_this_hour', {}).get('current', 0), + 'limit': usage.get('api_calls_this_hour', {}).get('limit'), 'unit': '/hour' }, { 'key': 'storage', 'label': 'File Storage', - 'current': int(usage_summary.get('file_storage_used_gb', 0)), - 'limit': subscription.max_storage_gb, + 'current': int(usage.get('file_storage_used_gb', {}).get('current', 0)), + 'limit': usage.get('file_storage_used_gb', {}).get('limit'), 'unit': ' GB' } ] diff --git a/services/tenant/app/main.py b/services/tenant/app/main.py index 1bb85e6a..3fc805ca 100644 --- a/services/tenant/app/main.py +++ b/services/tenant/app/main.py @@ -143,7 +143,7 @@ service.add_router(plans.router, tags=["subscription-plans"]) # Public endpoint service.add_router(internal_demo.router, tags=["internal-demo"]) # Internal demo data cloning service.add_router(subscription.router, tags=["subscription"]) service.add_router(internal_demo.router, tags=["internal-demo"]) # Internal demo data cloning -service.add_router(usage_forecast.router, tags=["usage-forecast"]) # Usage forecasting & predictive analytics +service.add_router(usage_forecast.router, prefix="/api/v1", tags=["usage-forecast"]) # Usage forecasting & predictive analytics service.add_router(internal_demo.router, tags=["internal-demo"]) # Internal demo data cloning # Register settings router BEFORE tenants router to ensure proper route matching service.add_router(tenant_settings.router, prefix="/api/v1/tenants", tags=["tenant-settings"])