M0: implement operational core
Backend: SQLAlchemy models, Alembic migrations, requirements lockfile. Frontend: pinned deps, package-lock, vite-env types fix. Verified compose build/health, pytest, ruff clean.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
from app.core.db import Base
|
||||
from app.models.audit import AuditEvent
|
||||
from app.models.booking import Booking
|
||||
from app.models.customer import Customer
|
||||
from app.models.data_quality import DataQualityIssue
|
||||
from app.models.inspection import Inspection
|
||||
from app.models.maintenance import MaintenanceRecord
|
||||
from app.models.outbox import OutboxEvent
|
||||
from app.models.user import User
|
||||
from app.models.vehicle import Vehicle
|
||||
|
||||
__all__ = [
|
||||
"Base",
|
||||
"AuditEvent",
|
||||
"Booking",
|
||||
"Customer",
|
||||
"DataQualityIssue",
|
||||
"Inspection",
|
||||
"MaintenanceRecord",
|
||||
"OutboxEvent",
|
||||
"User",
|
||||
"Vehicle",
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, String
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
from app.models.mixins import UUIDPrimaryKeyMixin
|
||||
|
||||
ACTOR_TYPES = ("user", "service", "system")
|
||||
|
||||
|
||||
class AuditEvent(UUIDPrimaryKeyMixin, Base):
|
||||
__tablename__ = "audit_events"
|
||||
|
||||
actor_type: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
actor_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
|
||||
actor_label: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
action: Mapped[str] = mapped_column(String(80), nullable=False)
|
||||
entity_type: Mapped[str] = mapped_column(String(30), nullable=False)
|
||||
entity_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
|
||||
correlation_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False)
|
||||
before_json: Mapped[dict | None] = mapped_column(JSONB)
|
||||
after_json: Mapped[dict | None] = mapped_column(JSONB)
|
||||
metadata_json: Mapped[dict | None] = mapped_column(JSONB)
|
||||
occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
@@ -0,0 +1,29 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
from app.models.mixins import TimestampMixin, UUIDPrimaryKeyMixin
|
||||
|
||||
BOOKING_STATUSES = ("reserved", "active", "returned", "cancelled", "blocked")
|
||||
|
||||
|
||||
class Booking(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
__tablename__ = "bookings"
|
||||
|
||||
public_ref: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
|
||||
customer_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("customers.id"), nullable=False
|
||||
)
|
||||
vehicle_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("vehicles.id"), nullable=False
|
||||
)
|
||||
starts_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
ends_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
start_odometer_km: Mapped[int | None] = mapped_column(Integer)
|
||||
end_odometer_km: Mapped[int | None] = mapped_column(Integer)
|
||||
requirements_complete: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
@@ -0,0 +1,25 @@
|
||||
import uuid
|
||||
from datetime import date
|
||||
|
||||
from sqlalchemy import Date, ForeignKey, String
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
from app.models.mixins import TimestampMixin, UUIDPrimaryKeyMixin
|
||||
|
||||
|
||||
class Customer(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
__tablename__ = "customers"
|
||||
|
||||
public_ref: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
|
||||
first_name: Mapped[str] = mapped_column(String(80), nullable=False)
|
||||
last_name: Mapped[str] = mapped_column(String(80), nullable=False)
|
||||
email: Mapped[str | None] = mapped_column(String(200))
|
||||
phone: Mapped[str | None] = mapped_column(String(40))
|
||||
postal_code: Mapped[str | None] = mapped_column(String(20))
|
||||
city: Mapped[str | None] = mapped_column(String(120))
|
||||
date_of_birth: Mapped[date | None] = mapped_column(Date)
|
||||
merged_into_customer_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("customers.id")
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, String
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
from app.models.mixins import TimestampMixin, UUIDPrimaryKeyMixin
|
||||
|
||||
RULE_TYPES = (
|
||||
"possible_duplicate_customer",
|
||||
"missing_required_field",
|
||||
"odometer_regression",
|
||||
"booking_overlap",
|
||||
"vehicle_status_conflict",
|
||||
)
|
||||
SEVERITIES = ("low", "medium", "high")
|
||||
ISSUE_STATUSES = ("open", "deferred", "resolved", "rejected")
|
||||
|
||||
|
||||
class DataQualityIssue(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
__tablename__ = "data_quality_issues"
|
||||
|
||||
public_ref: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
|
||||
rule_type: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
entity_type: Mapped[str] = mapped_column(String(30), nullable=False)
|
||||
entity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False)
|
||||
severity: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="open")
|
||||
evidence_json: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
|
||||
proposed_action_json: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
|
||||
detected_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
resolved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
resolved_by: Mapped[str | None] = mapped_column(String(120))
|
||||
@@ -0,0 +1,32 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
from app.models.mixins import TimestampMixin, UUIDPrimaryKeyMixin
|
||||
|
||||
INSPECTION_TYPES = ("checkout", "return")
|
||||
|
||||
|
||||
class Inspection(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
__tablename__ = "inspections"
|
||||
|
||||
public_ref: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
|
||||
booking_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("bookings.id"), nullable=False
|
||||
)
|
||||
vehicle_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("vehicles.id"), nullable=False
|
||||
)
|
||||
type: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
fuel_level_percent: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
cleanliness_ok: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||
damage_reported: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
technical_warning: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
notes: Mapped[str | None] = mapped_column(Text)
|
||||
odometer_km: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
completed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
completed_by: Mapped[str | None] = mapped_column(String(120))
|
||||
@@ -0,0 +1,22 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
from app.models.mixins import UUIDPrimaryKeyMixin
|
||||
|
||||
|
||||
class MaintenanceRecord(UUIDPrimaryKeyMixin, Base):
|
||||
__tablename__ = "maintenance_records"
|
||||
|
||||
public_ref: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
|
||||
vehicle_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("vehicles.id"), nullable=False
|
||||
)
|
||||
occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
odometer_km: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
category: Mapped[str] = mapped_column(String(60), nullable=False)
|
||||
summary: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
@@ -0,0 +1,24 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class UUIDPrimaryKeyMixin:
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
|
||||
|
||||
class TimestampMixin:
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Integer, String, Text
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
from app.models.mixins import TimestampMixin
|
||||
|
||||
DELIVERY_STATUSES = ("pending", "delivering", "succeeded", "failed")
|
||||
|
||||
|
||||
class OutboxEvent(TimestampMixin, Base):
|
||||
__tablename__ = "outbox_events"
|
||||
|
||||
event_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
event_type: Mapped[str] = mapped_column(String(60), nullable=False)
|
||||
aggregate_type: Mapped[str] = mapped_column(String(30), nullable=False)
|
||||
aggregate_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False)
|
||||
payload_json: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
|
||||
occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
delivery_status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending")
|
||||
attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
next_attempt_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
last_error: Mapped[str | None] = mapped_column(Text)
|
||||
external_run_id: Mapped[str | None] = mapped_column(String(120))
|
||||
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy import Boolean, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
from app.models.mixins import TimestampMixin, UUIDPrimaryKeyMixin
|
||||
|
||||
ROLES = ("operations_manager", "rental_employee")
|
||||
|
||||
|
||||
class User(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
public_ref: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
|
||||
display_name: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
role: Mapped[str] = mapped_column(String(30), nullable=False)
|
||||
active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
@@ -0,0 +1,23 @@
|
||||
from sqlalchemy import Boolean, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
from app.models.mixins import TimestampMixin, UUIDPrimaryKeyMixin
|
||||
|
||||
OPERATIONAL_STATUSES = ("available", "rented", "cleaning", "maintenance", "blocked")
|
||||
|
||||
|
||||
class Vehicle(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
__tablename__ = "vehicles"
|
||||
|
||||
public_ref: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
|
||||
make: Mapped[str] = mapped_column(String(80), nullable=False)
|
||||
model: Mapped[str] = mapped_column(String(80), nullable=False)
|
||||
model_year: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
registration_number: Mapped[str] = mapped_column(String(40), unique=True, nullable=False)
|
||||
location: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
operational_status: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
odometer_km: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
next_service_km: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
Reference in New Issue
Block a user