Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
# ================================================================
# services/procurement/app/core/config.py
# ================================================================
"""
Procurement Service Configuration
"""
import os
from decimal import Decimal
from pydantic import Field
from shared.config.base import BaseServiceSettings
class ProcurementSettings(BaseServiceSettings):
"""Procurement service specific settings"""
# Service Identity
APP_NAME: str = "Procurement Service"
SERVICE_NAME: str = "procurement-service"
VERSION: str = "1.0.0"
DESCRIPTION: str = "Procurement planning, purchase order management, and supplier integration"
# Database configuration (secure approach - build from components)
@property
def DATABASE_URL(self) -> str:
"""Build database URL from secure components"""
# Try complete URL first (for backward compatibility)
complete_url = os.getenv("PROCUREMENT_DATABASE_URL")
if complete_url:
return complete_url
# Build from components (secure approach)
user = os.getenv("PROCUREMENT_DB_USER", "procurement_user")
password = os.getenv("PROCUREMENT_DB_PASSWORD", "procurement_pass123")
host = os.getenv("PROCUREMENT_DB_HOST", "localhost")
port = os.getenv("PROCUREMENT_DB_PORT", "5432")
name = os.getenv("PROCUREMENT_DB_NAME", "procurement_db")
return f"postgresql+asyncpg://{user}:{password}@{host}:{port}/{name}"
# Procurement Planning
PROCUREMENT_PLANNING_ENABLED: bool = os.getenv("PROCUREMENT_PLANNING_ENABLED", "true").lower() == "true"
PROCUREMENT_LEAD_TIME_DAYS: int = int(os.getenv("PROCUREMENT_LEAD_TIME_DAYS", "3"))
DEMAND_FORECAST_DAYS: int = int(os.getenv("DEMAND_FORECAST_DAYS", "14"))
SAFETY_STOCK_PERCENTAGE: float = float(os.getenv("SAFETY_STOCK_PERCENTAGE", "20.0"))
# Purchase Order Settings
AUTO_APPROVE_POS: bool = os.getenv("AUTO_APPROVE_POS", "false").lower() == "true"
AUTO_APPROVAL_MAX_AMOUNT: float = float(os.getenv("AUTO_APPROVAL_MAX_AMOUNT", "1000.0"))
MAX_PO_ITEMS: int = int(os.getenv("MAX_PO_ITEMS", "100"))
PO_EXPIRY_DAYS: int = int(os.getenv("PO_EXPIRY_DAYS", "30"))
# Local Production Settings
SUPPORT_LOCAL_PRODUCTION: bool = os.getenv("SUPPORT_LOCAL_PRODUCTION", "true").lower() == "true"
MAX_BOM_EXPLOSION_DEPTH: int = int(os.getenv("MAX_BOM_EXPLOSION_DEPTH", "5"))
RECIPE_CACHE_TTL_SECONDS: int = int(os.getenv("RECIPE_CACHE_TTL_SECONDS", "3600"))
# Supplier Integration
SUPPLIER_VALIDATION_ENABLED: bool = os.getenv("SUPPLIER_VALIDATION_ENABLED", "true").lower() == "true"
MIN_SUPPLIER_RATING: float = float(os.getenv("MIN_SUPPLIER_RATING", "3.0"))
MULTI_SUPPLIER_ENABLED: bool = os.getenv("MULTI_SUPPLIER_ENABLED", "true").lower() == "true"
# Plan Management
STALE_PLAN_DAYS: int = int(os.getenv("STALE_PLAN_DAYS", "7"))
ARCHIVE_PLAN_DAYS: int = int(os.getenv("ARCHIVE_PLAN_DAYS", "90"))
MAX_CONCURRENT_PLANS: int = int(os.getenv("MAX_CONCURRENT_PLANS", "10"))
# Integration Settings
INVENTORY_SERVICE_URL: str = os.getenv("INVENTORY_SERVICE_URL", "http://inventory-service:8000")
SUPPLIERS_SERVICE_URL: str = os.getenv("SUPPLIERS_SERVICE_URL", "http://suppliers-service:8000")
# ================================================================
# REPLENISHMENT PLANNING SETTINGS
# ================================================================
# Projection Settings
REPLENISHMENT_PROJECTION_HORIZON_DAYS: int = Field(
default=7,
description="Days to project ahead for inventory planning"
)
REPLENISHMENT_SERVICE_LEVEL: float = Field(
default=0.95,
description="Target service level for safety stock (0-1)"
)
REPLENISHMENT_BUFFER_DAYS: int = Field(
default=1,
description="Buffer days to add to lead time"
)
# Safety Stock Settings
SAFETY_STOCK_SERVICE_LEVEL: float = Field(
default=0.95,
description="Default service level for safety stock calculation"
)
SAFETY_STOCK_METHOD: str = Field(
default="statistical",
description="Method for safety stock: 'statistical' or 'fixed_percentage'"
)
# MOQ Aggregation Settings
MOQ_CONSOLIDATION_WINDOW_DAYS: int = Field(
default=7,
description="Days within which to consolidate orders for MOQ"
)
MOQ_ALLOW_EARLY_ORDERING: bool = Field(
default=True,
description="Allow ordering early to meet MOQ"
)
# Supplier Selection Settings
SUPPLIER_PRICE_WEIGHT: float = Field(
default=0.40,
description="Weight for price in supplier selection (0-1)"
)
SUPPLIER_LEAD_TIME_WEIGHT: float = Field(
default=0.20,
description="Weight for lead time in supplier selection (0-1)"
)
SUPPLIER_QUALITY_WEIGHT: float = Field(
default=0.20,
description="Weight for quality in supplier selection (0-1)"
)
SUPPLIER_RELIABILITY_WEIGHT: float = Field(
default=0.20,
description="Weight for reliability in supplier selection (0-1)"
)
SUPPLIER_DIVERSIFICATION_THRESHOLD: Decimal = Field(
default=Decimal('1000'),
description="Quantity threshold for supplier diversification"
)
SUPPLIER_MAX_SINGLE_PERCENTAGE: float = Field(
default=0.70,
description="Maximum % of order to single supplier (0-1)"
)
FORECASTING_SERVICE_URL: str = os.getenv("FORECASTING_SERVICE_URL", "http://forecasting-service:8000")
RECIPES_SERVICE_URL: str = os.getenv("RECIPES_SERVICE_URL", "http://recipes-service:8000")
NOTIFICATION_SERVICE_URL: str = os.getenv("NOTIFICATION_SERVICE_URL", "http://notification-service:8000")
TENANT_SERVICE_URL: str = os.getenv("TENANT_SERVICE_URL", "http://tenant-service:8000")
# Global settings instance
settings = ProcurementSettings()

View File

@@ -0,0 +1,47 @@
# ================================================================
# services/procurement/app/core/database.py
# ================================================================
"""
Database connection and session management for Procurement Service
"""
from shared.database.base import DatabaseManager
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from .config import settings
# Initialize database manager
database_manager = DatabaseManager(
database_url=settings.DATABASE_URL,
echo=settings.DEBUG
)
# Create async session factory
AsyncSessionLocal = async_sessionmaker(
database_manager.async_engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False,
)
async def get_db() -> AsyncSession:
"""
Dependency to get database session.
Used in FastAPI endpoints via Depends(get_db).
"""
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()
async def init_db():
"""Initialize database (create tables if needed)"""
await database_manager.create_all()
async def close_db():
"""Close database connections"""
await database_manager.close()

View File

@@ -0,0 +1,47 @@
"""
FastAPI Dependencies for Procurement Service
Uses shared authentication infrastructure with UUID validation
"""
from fastapi import Depends, HTTPException, status
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from .database import get_db
from shared.auth.decorators import get_current_tenant_id_dep
async def get_current_tenant_id(
tenant_id: Optional[str] = Depends(get_current_tenant_id_dep)
) -> UUID:
"""
Extract and validate tenant ID from request using shared infrastructure.
Adds UUID validation to ensure tenant ID format is correct.
Args:
tenant_id: Tenant ID from shared dependency
Returns:
UUID: Validated tenant ID
Raises:
HTTPException: If tenant ID is missing or invalid UUID format
"""
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="x-tenant-id header is required"
)
try:
return UUID(tenant_id)
except (ValueError, AttributeError):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid tenant ID format: {tenant_id}"
)
# Re-export get_db for convenience
__all__ = ["get_db", "get_current_tenant_id"]