diff --git a/gateway/app/main.py b/gateway/app/main.py index ce8e4e7b..0dffc29a 100644 --- a/gateway/app/main.py +++ b/gateway/app/main.py @@ -67,10 +67,21 @@ async def startup_event(): logger.info("Starting API Gateway") # Start metrics server - metrics_collector.start_metrics_server(8080) + metrics_collector.register_counter( + "gateway_auth_requests_total", + "Total authentication requests through gateway" + ) + metrics_collector.register_counter( + "gateway_auth_responses_total", + "Total authentication responses through gateway" + ) + metrics_collector.register_histogram( + "gateway_request_duration_seconds", + "Gateway request duration" + ) - # Initialize service discovery - # await service_discovery.initialize() + + metrics_collector.start_metrics_server(8080) logger.info("API Gateway started successfully") diff --git a/gateway/app/routes/tenant.py b/gateway/app/routes/tenant.py index 426a8084..defd279b 100644 --- a/gateway/app/routes/tenant.py +++ b/gateway/app/routes/tenant.py @@ -42,10 +42,20 @@ async def get_tenant_members(request: Request, tenant_id: str = Path(...)): # TENANT-SCOPED DATA SERVICE ENDPOINTS # ================================================================ -@router.api_route("/{tenant_id}/sales/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) -async def proxy_tenant_sales(request: Request, tenant_id: str = Path(...), path: str = ""): - """Proxy tenant sales requests to data service""" - target_path = f"/api/v1/tenants/{tenant_id}/sales/{path}".rstrip("/") +@router.api_route("/{tenant_id}/sales{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) +async def proxy_all_tenant_sales_alternative(request: Request, tenant_id: str = Path(...), path: str = ""): + """Proxy all tenant sales requests - handles both base and sub-paths""" + base_path = f"/api/v1/tenants/{tenant_id}/sales" + + # If path is empty or just "/", use base path + if not path or path == "/" or path == "": + target_path = base_path + else: + # Ensure path starts with "/" + if not path.startswith("/"): + path = "/" + path + target_path = base_path + path + return await _proxy_to_data_service(request, target_path) @router.api_route("/{tenant_id}/weather/{path:path}", methods=["GET", "POST", "OPTIONS"]) diff --git a/services/data/app/api/sales.py b/services/data/app/api/sales.py index 8fac8fff..ae188428 100644 --- a/services/data/app/api/sales.py +++ b/services/data/app/api/sales.py @@ -173,7 +173,7 @@ async def import_sales_data( current_user: Dict[str, Any] = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): - """Import sales data from file for tenant""" + """Import sales data from file for tenant - FIXED VERSION""" try: logger.info("Importing sales data", tenant_id=tenant_id, @@ -185,7 +185,7 @@ async def import_sales_data( content = await file.read() file_content = content.decode('utf-8') - # Process import + # ✅ FIX: tenant_id comes from URL path, not file upload result = await DataImportService.process_upload( tenant_id, file_content, @@ -198,7 +198,7 @@ async def import_sales_data( # Publish event try: await publish_data_imported({ - "tenant_id": tenant_id, + "tenant_id": str(tenant_id), # Ensure string conversion "type": "file_import", "format": file_format, "filename": file.filename, diff --git a/services/data/app/models/sales.py b/services/data/app/models/sales.py index d0072a2c..edd9d936 100644 --- a/services/data/app/models/sales.py +++ b/services/data/app/models/sales.py @@ -6,7 +6,7 @@ from sqlalchemy import Column, String, DateTime, Float, Integer, Text, Index from sqlalchemy.dialects.postgresql import UUID import uuid -from datetime import datetime +from datetime import datetime, timezone from app.core.database import Base @@ -15,15 +15,17 @@ class SalesData(Base): id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True) - date = Column(DateTime, nullable=False, index=True) + date = Column(DateTime(timezone=True), nullable=False, index=True) product_name = Column(String(255), nullable=False, index=True) quantity_sold = Column(Integer, nullable=False) revenue = Column(Float, nullable=False) location_id = Column(String(100), nullable=True, index=True) source = Column(String(50), nullable=False, default="manual") notes = Column(Text, nullable=True) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) + updated_at = Column(DateTime(timezone=True), + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc)) __table_args__ = ( Index('idx_sales_tenant_date', 'tenant_id', 'date'), diff --git a/services/data/app/schemas/sales.py b/services/data/app/schemas/sales.py index 60cf8fa0..3c212cc2 100644 --- a/services/data/app/schemas/sales.py +++ b/services/data/app/schemas/sales.py @@ -9,8 +9,9 @@ from typing import Optional, List, Dict, Any from uuid import UUID class SalesDataCreate(BaseModel): - """Schema for creating sales data""" - tenant_id: UUID + """Schema for creating sales data - FIXED to work with gateway""" + # ✅ FIX: Make tenant_id optional since it comes from URL path + tenant_id: Optional[UUID] = Field(None, description="Tenant ID (auto-injected from URL path)") date: datetime product_name: str = Field(..., min_length=1, max_length=255) quantity_sold: int = Field(..., gt=0) @@ -25,6 +26,16 @@ class SalesDataCreate(BaseModel): class Config: from_attributes = True + json_schema_extra = { + "example": { + "date": "2024-01-15T10:00:00Z", + "product_name": "Pan Integral", + "quantity_sold": 25, + "revenue": 37.50, + "source": "manual" + # Note: tenant_id is automatically injected from URL path by gateway + } + } class SalesDataResponse(BaseModel): """Schema for sales data response""" @@ -62,15 +73,23 @@ class SalesDataQuery(BaseModel): from_attributes = True class SalesDataImport(BaseModel): - """Schema for importing sales data""" - tenant_id: UUID - data: str # JSON string or CSV content + """Schema for importing sales data - FIXED to work with gateway""" + # ✅ FIX: Make tenant_id optional since it comes from URL path + tenant_id: Optional[UUID] = Field(None, description="Tenant ID (auto-injected from URL path)") + data: str = Field(..., description="JSON string or CSV content") data_format: str = Field(..., pattern="^(csv|json|excel)$") source: str = Field(default="import", max_length=50) validate_only: bool = Field(default=False) class Config: from_attributes = True + json_schema_extra = { + "example": { + "data": "date,product,quantity,revenue\n2024-01-01,bread,10,25.50", + "data_format": "csv", + # Note: tenant_id is automatically injected from URL path by gateway + } + } class SalesDataBulkCreate(BaseModel): """Schema for bulk creating sales data""" diff --git a/test_new.sh b/test_new.sh index 9a8e0adb..0a9f40c3 100755 --- a/test_new.sh +++ b/test_new.sh @@ -181,6 +181,36 @@ if [ -n "$TENANT_ID" ]; then echo "Validation Response: $VALIDATION_RESPONSE" check_response "$VALIDATION_RESPONSE" "Import Validation" + + # Step 6.5: Import Sample Sales Data + echo -e "\n6.5. Importing Sample Sales Data..." + IMPORT_RESPONSE=$(curl -s -X POST "$API_BASE/api/v1/tenants/$TENANT_ID/sales" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -d '{ + "product_name": "Pan Integral", + "quantity_sold": 25, + "revenue": 37.50, + "date": "2024-01-15T10:00:00Z" + }') + + echo "Import Response: $IMPORT_RESPONSE" + check_response "$IMPORT_RESPONSE" "Sales Data Import" + + # Now test sales endpoint again - should have data! + echo -e "\n6.6. Testing Sales Endpoint Again (Should Have Data)..." + SALES_RESPONSE_WITH_DATA=$(curl -s -X GET "$API_BASE/api/v1/tenants/$TENANT_ID/sales" \ + -H "Authorization: Bearer $ACCESS_TOKEN") + + echo "Sales Response with Data: $SALES_RESPONSE_WITH_DATA" + check_response "$SALES_RESPONSE_WITH_DATA" "Tenant Sales Endpoint with Data" + + # Check if we actually got data + if echo "$SALES_RESPONSE_WITH_DATA" | grep -q "Pan Integral"; then + echo -e "${GREEN}✅ Successfully retrieved sales data!${NC}" + else + echo -e "${YELLOW}⚠️ No sales data returned (might need different import endpoint)${NC}" + fi fi # Step 7: Additional Debug Information