feat: add initial database models and changelog for financial inputs and projects
This commit is contained in:
5
changelog.md
Normal file
5
changelog.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-11-09
|
||||||
|
|
||||||
|
- Captured current implementation status, requirements coverage, missing features, and prioritized roadmap in `calminer-docs/implementation_status.md` to guide future development.
|
||||||
37
main.py
37
main.py
@@ -1,25 +1,17 @@
|
|||||||
from routes.distributions import router as distributions_router
|
|
||||||
from routes.ui import router as ui_router
|
|
||||||
from routes.parameters import router as parameters_router
|
|
||||||
from typing import Awaitable, Callable
|
from typing import Awaitable, Callable
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Response
|
from fastapi import FastAPI, Request, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from middleware.validation import validate_json
|
from middleware.validation import validate_json
|
||||||
from config.database import Base, engine
|
from config.database import Base, engine
|
||||||
from routes.scenarios import router as scenarios_router
|
from models import (
|
||||||
from routes.costs import router as costs_router
|
FinancialInput,
|
||||||
from routes.consumption import router as consumption_router
|
Project,
|
||||||
from routes.production import router as production_router
|
Scenario,
|
||||||
from routes.equipment import router as equipment_router
|
SimulationParameter,
|
||||||
from routes.reporting import router as reporting_router
|
)
|
||||||
from routes.currencies import router as currencies_router
|
|
||||||
from routes.simulations import router as simulations_router
|
|
||||||
from routes.maintenance import router as maintenance_router
|
|
||||||
from routes.settings import router as settings_router
|
|
||||||
from routes.users import router as users_router
|
|
||||||
|
|
||||||
# Initialize database schema
|
# Initialize database schema (imports above ensure models are registered)
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -39,18 +31,3 @@ async def health() -> dict[str, str]:
|
|||||||
|
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
# Include API routers
|
|
||||||
app.include_router(scenarios_router)
|
|
||||||
app.include_router(parameters_router)
|
|
||||||
app.include_router(distributions_router)
|
|
||||||
app.include_router(costs_router)
|
|
||||||
app.include_router(consumption_router)
|
|
||||||
app.include_router(simulations_router)
|
|
||||||
app.include_router(production_router)
|
|
||||||
app.include_router(equipment_router)
|
|
||||||
app.include_router(maintenance_router)
|
|
||||||
app.include_router(reporting_router)
|
|
||||||
app.include_router(currencies_router)
|
|
||||||
app.include_router(settings_router)
|
|
||||||
app.include_router(ui_router)
|
|
||||||
app.include_router(users_router)
|
|
||||||
|
|||||||
17
models/__init__.py
Normal file
17
models/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
"""Database models for the CalMiner domain."""
|
||||||
|
|
||||||
|
from .financial_input import FinancialCategory, FinancialInput
|
||||||
|
from .project import MiningOperationType, Project
|
||||||
|
from .scenario import Scenario, ScenarioStatus
|
||||||
|
from .simulation_parameter import DistributionType, SimulationParameter
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"FinancialCategory",
|
||||||
|
"FinancialInput",
|
||||||
|
"MiningOperationType",
|
||||||
|
"Project",
|
||||||
|
"Scenario",
|
||||||
|
"ScenarioStatus",
|
||||||
|
"DistributionType",
|
||||||
|
"SimulationParameter",
|
||||||
|
]
|
||||||
63
models/financial_input.py
Normal file
63
models/financial_input.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
Date,
|
||||||
|
DateTime,
|
||||||
|
Enum as SQLEnum,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
Numeric,
|
||||||
|
String,
|
||||||
|
Text,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
from config.database import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from .scenario import Scenario
|
||||||
|
|
||||||
|
|
||||||
|
class FinancialCategory(str, Enum):
|
||||||
|
"""Enumeration of cost and revenue classifications."""
|
||||||
|
|
||||||
|
CAPITAL_EXPENDITURE = "capex"
|
||||||
|
OPERATING_EXPENDITURE = "opex"
|
||||||
|
REVENUE = "revenue"
|
||||||
|
CONTINGENCY = "contingency"
|
||||||
|
OTHER = "other"
|
||||||
|
|
||||||
|
|
||||||
|
class FinancialInput(Base):
|
||||||
|
"""Line-item financial assumption attached to a scenario."""
|
||||||
|
|
||||||
|
__tablename__ = "financial_inputs"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
scenario_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("scenarios.id", ondelete="CASCADE"), nullable=False, index=True
|
||||||
|
)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
category: Mapped[FinancialCategory] = mapped_column(
|
||||||
|
SQLEnum(FinancialCategory), nullable=False
|
||||||
|
)
|
||||||
|
amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
|
||||||
|
currency: Mapped[str | None] = mapped_column(String(3), nullable=True)
|
||||||
|
effective_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||||
|
)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
scenario: Mapped["Scenario"] = relationship("Scenario", back_populates="financial_inputs")
|
||||||
|
|
||||||
|
def __repr__(self) -> str: # pragma: no cover
|
||||||
|
return f"FinancialInput(id={self.id!r}, scenario_id={self.scenario_id!r}, name={self.name!r})"
|
||||||
54
models/project.py
Normal file
54
models/project.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, Enum as SQLEnum, Integer, String, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
from config.database import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from .scenario import Scenario
|
||||||
|
|
||||||
|
|
||||||
|
class MiningOperationType(str, Enum):
|
||||||
|
"""Supported mining operation categories."""
|
||||||
|
|
||||||
|
OPEN_PIT = "open_pit"
|
||||||
|
UNDERGROUND = "underground"
|
||||||
|
PLACER = "placer"
|
||||||
|
QUARRY = "quarry"
|
||||||
|
OTHER = "other"
|
||||||
|
|
||||||
|
|
||||||
|
class Project(Base):
|
||||||
|
"""Top-level mining project grouping multiple scenarios."""
|
||||||
|
|
||||||
|
__tablename__ = "projects"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
||||||
|
location: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||||
|
operation_type: Mapped[MiningOperationType] = mapped_column(
|
||||||
|
SQLEnum(MiningOperationType), nullable=False, default=MiningOperationType.OTHER
|
||||||
|
)
|
||||||
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||||
|
)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
scenarios: Mapped[List["Scenario"]] = relationship(
|
||||||
|
"Scenario",
|
||||||
|
back_populates="project",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
passive_deletes=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str: # pragma: no cover - helpful for debugging
|
||||||
|
return f"Project(id={self.id!r}, name={self.name!r})"
|
||||||
67
models/scenario.py
Normal file
67
models/scenario.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
|
from sqlalchemy import Date, DateTime, Enum as SQLEnum, ForeignKey, Integer, Numeric, String, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
from config.database import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from .financial_input import FinancialInput
|
||||||
|
from .project import Project
|
||||||
|
from .simulation_parameter import SimulationParameter
|
||||||
|
|
||||||
|
|
||||||
|
class ScenarioStatus(str, Enum):
|
||||||
|
"""Lifecycle states for project scenarios."""
|
||||||
|
|
||||||
|
DRAFT = "draft"
|
||||||
|
ACTIVE = "active"
|
||||||
|
ARCHIVED = "archived"
|
||||||
|
|
||||||
|
|
||||||
|
class Scenario(Base):
|
||||||
|
"""A specific configuration of assumptions for a project."""
|
||||||
|
|
||||||
|
__tablename__ = "scenarios"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
|
project_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||||
|
)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
status: Mapped[ScenarioStatus] = mapped_column(
|
||||||
|
SQLEnum(ScenarioStatus), nullable=False, default=ScenarioStatus.DRAFT
|
||||||
|
)
|
||||||
|
start_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
end_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
discount_rate: Mapped[float | None] = mapped_column(Numeric(5, 2), nullable=True)
|
||||||
|
currency: Mapped[str | None] = mapped_column(String(3), nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||||
|
)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
project: Mapped["Project"] = relationship("Project", back_populates="scenarios")
|
||||||
|
financial_inputs: Mapped[List["FinancialInput"]] = relationship(
|
||||||
|
"FinancialInput",
|
||||||
|
back_populates="scenario",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
passive_deletes=True,
|
||||||
|
)
|
||||||
|
simulation_parameters: Mapped[List["SimulationParameter"]] = relationship(
|
||||||
|
"SimulationParameter",
|
||||||
|
back_populates="scenario",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
passive_deletes=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str: # pragma: no cover
|
||||||
|
return f"Scenario(id={self.id!r}, name={self.name!r}, project_id={self.project_id!r})"
|
||||||
69
models/simulation_parameter.py
Normal file
69
models/simulation_parameter.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
JSON,
|
||||||
|
DateTime,
|
||||||
|
Enum as SQLEnum,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
Numeric,
|
||||||
|
String,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
from config.database import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from .scenario import Scenario
|
||||||
|
|
||||||
|
|
||||||
|
class DistributionType(str, Enum):
|
||||||
|
"""Supported stochastic distribution families for simulations."""
|
||||||
|
|
||||||
|
NORMAL = "normal"
|
||||||
|
TRIANGULAR = "triangular"
|
||||||
|
UNIFORM = "uniform"
|
||||||
|
LOGNORMAL = "lognormal"
|
||||||
|
CUSTOM = "custom"
|
||||||
|
|
||||||
|
|
||||||
|
class SimulationParameter(Base):
|
||||||
|
"""Probability distribution settings for scenario simulations."""
|
||||||
|
|
||||||
|
__tablename__ = "simulation_parameters"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
scenario_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("scenarios.id", ondelete="CASCADE"), nullable=False, index=True
|
||||||
|
)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
distribution: Mapped[DistributionType] = mapped_column(
|
||||||
|
SQLEnum(DistributionType), nullable=False
|
||||||
|
)
|
||||||
|
mean_value: Mapped[float | None] = mapped_column(Numeric(18, 4), nullable=True)
|
||||||
|
standard_deviation: Mapped[float | None] = mapped_column(Numeric(18, 4), nullable=True)
|
||||||
|
minimum_value: Mapped[float | None] = mapped_column(Numeric(18, 4), nullable=True)
|
||||||
|
maximum_value: Mapped[float | None] = mapped_column(Numeric(18, 4), nullable=True)
|
||||||
|
unit: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||||
|
metadata: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||||
|
)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
scenario: Mapped["Scenario"] = relationship(
|
||||||
|
"Scenario", back_populates="simulation_parameters"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str: # pragma: no cover
|
||||||
|
return (
|
||||||
|
f"SimulationParameter(id={self.id!r}, scenario_id={self.scenario_id!r}, "
|
||||||
|
f"name={self.name!r})"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user