Initial commit - production deployment
This commit is contained in:
0
services/procurement/app/core/__init__.py
Normal file
0
services/procurement/app/core/__init__.py
Normal file
142
services/procurement/app/core/config.py
Normal file
142
services/procurement/app/core/config.py
Normal 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()
|
||||
47
services/procurement/app/core/database.py
Normal file
47
services/procurement/app/core/database.py
Normal 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()
|
||||
47
services/procurement/app/core/dependencies.py
Normal file
47
services/procurement/app/core/dependencies.py
Normal 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"]
|
||||
Reference in New Issue
Block a user