6
🛠

Maintenance Schedule — Blueprint

Annual · One-time · Recurring tasks across assets

Maintenance Schedule — Implementation Blueprint

Architect Agent 4 of the Pancha · 2026-05-02 · Module 6 of 17 in the Aayojana catalog.

1. Module summary

The institution's upkeep calendar. Tasks are tied to assets — buildings, equipment, vehicles, lands. Three task patterns: annual (water tank cleaning, electrical audit, deep cleaning), periodical (daily/weekly/monthly — generator rotation, ventilation runs, security drills), one-time (ad-hoc repairs, emergency replacements). AMC contracts model recurring vendor service (lifts, fire-systems, ACs); they auto-spawn maintenance schedules and feed Vitta as recurring vendor invoices. Inspection reports + completion records close the loop with photos and findings.

2. Data model

# src/aayojana/maintenance/models.py
from datetime import date, datetime
from sqlalchemy import (
    Boolean, Date, DateTime, ForeignKey, Index, Integer, JSON, Numeric, String, Text,
    UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from aayojana.models.base import AuditMixin, Base, TenantMixin, uuid4_str


class MaintenanceTask(Base, AuditMixin, TenantMixin):
    """Master template — defines WHAT to do, on which asset, how often.
    Instances of execution live in maintenance_schedules.
    """

    __tablename__ = "maintenance_tasks"
    __table_args__ = (
        UniqueConstraint("tenant_id", "code", name="uq_maint_task_code"),
    )
    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid4_str)
    code: Mapped[str] = mapped_column(String(40), nullable=False)
    name: Mapped[str] = mapped_column(String(255), nullable=False)
    name_sanskrit: Mapped[str | None] = mapped_column(Text, nullable=True)
    description: Mapped[str | None] = mapped_column(Text, nullable=True)
    instructions: Mapped[str | None] = mapped_column(Text, nullable=True)
    # e.g. "Run generator 15 min weekly to prevent seizure; check oil before start"
    asset_id: Mapped[str | None] = mapped_column(
        String(36), ForeignKey("assets.id"), nullable=True, index=True
    )
    # Nullable: a task may apply to a class of assets (e.g. "all buildings") via asset_scope.
    asset_scope: Mapped[str | None] = mapped_column(String(40), nullable=True)
    # 'all-buildings' | 'all-vehicles' | 'all-equipment' | 'all-rooms-AC' | NULL (specific asset)
    branch_id: Mapped[str | None] = mapped_column(
        String(36), ForeignKey("branches.id"), nullable=True
    )
    frequency: Mapped[str] = mapped_column(String(20), nullable=False)
    # 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'half-yearly' | 'annual' | 'one-time' | 'ad-hoc'
    frequency_interval: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
    # e.g. frequency='monthly', interval=3 → every 3 months
    estimated_duration_minutes: Mapped[int | None] = mapped_column(Integer, nullable=True)
    estimated_cost: Mapped[float | None] = mapped_column(Numeric(12, 2), nullable=True)
    is_compliance_task: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    # Fire system, lift, lightning arrester — statutory inspection cycles.
    compliance_authority: Mapped[str | None] = mapped_column(String(127), nullable=True)
    default_assignee_member_id: Mapped[str | None] = mapped_column(
        String(36), ForeignKey("members.id"), nullable=True
    )
    amc_contract_id: Mapped[str | None] = mapped_column(
        String(36), ForeignKey("amc_contracts.id"), nullable=True
    )
    # If the task is fulfilled under an AMC, link it.
    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
    next_due_on: Mapped[date | None] = mapped_column(Date, nullable=True)
    # Cached: computed by scheduler from last completion + frequency.


class MaintenanceSchedule(Base, AuditMixin, TenantMixin):
    """A scheduled occurrence of a task on a specific date."""

    __tablename__ = "maintenance_schedules"
    __table_args__ = (
        Index("ix_maint_sched_due", "due_date", "status"),
    )
    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid4_str)
    task_id: Mapped[str] = mapped_column(
        String(36), ForeignKey("maintenance_tasks.id"), nullable=False
    )
    asset_id: Mapped[str | None] = mapped_column(
        String(36), ForeignKey("assets.id"), nullable=True
    )
    # When task.asset_scope is set, instances enumerate matching assets.
    branch_id: Mapped[str | None] = mapped_column(
        String(36), ForeignKey("branches.id"), nullable=True
    )
    due_date: Mapped[date] = mapped_column(Date, nullable=False)
    grace_until: Mapped[date | None] = mapped_column(Date, nullable=True)
    assigned_to_member_id: Mapped[str | None] = mapped_column(
        String(36), ForeignKey("members.id"), nullable=True
    )
    assigned_to_amc_id: Mapped[str | None] = mapped_column(
        String(36), ForeignKey("amc_contracts.id"), nullable=True
    )
    status: Mapped[str] = mapped_column(String(20), default="scheduled", nullable=False)
    # 'scheduled' | 'in-progress' | 'completed' | 'overdue' | 'skipped' | 'cancelled'
    priority: Mapped[str] = mapped_column(String(10), default="normal", nullable=False)
    # 'low' | 'normal' | 'high' | 'urgent'
    notes: Mapped[str | None] = mapped_column(Text, nullable=True)


class MaintenanceCompletion(Base, AuditMixin, TenantMixin):
    """A completion record — what was actually done, by whom, with what findings."""

    __tablename__ = "maintenance_completions"
    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid4_str)
    schedule_id: Mapped[str] = mapped_column(
        String(36), ForeignKey("maintenance_schedules.id"), nullable=False, unique=True
    )
    completed_on: Mapped[date] = mapped_column(Date, nullable=False)
    completed_by_member_id: Mapped[str | None] = mapped_column(
        String(36), ForeignKey("members.id"), nullable=True
    )
    completed_by_external: Mapped[str | None] = mapped_column(String(255), nullable=True)
    # If executed by AMC vendor or external contractor — not in members.
    duration_minutes: Mapped[int | None] = mapped_column(Integer, nullable=True)
    findings: Mapped[str | None] = mapped_column(Text, nullable=True)
    defects_found: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    follow_up_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    follow_up_schedule_id: Mapped[str | None] = mapped_column(
        String(36), ForeignKey("maintenance_schedules.id"), nullable=True
    )
    materials_used: Mapped[list | None] = mapped_column(JSON, nullable=True)
    # [{item_id, quantity, dispense_record_id}]
    cost_incurred: Mapped[float | None] = mapped_column(Numeric(12, 2), nullable=True)
    photo_paths: Mapped[list | None] = mapped_column(JSON, nullable=True)
    signed_off_by_user_id: Mapped[str | None] = mapped_column(
        String(36), ForeignKey("users.id"), nullable=True
    )


class AMCContract(Base, AuditMixin, TenantMixin):
    """Annual / multi-year service contracts with vendors.
    Triggers recurring vendor invoices in Vitta and spawns scheduled maintenance.
    """

    __tablename__ = "amc_contracts"
    __table_args__ = (
        UniqueConstraint("tenant_id", "contract_number", name="uq_amc_contract_no"),
    )
    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid4_str)
    contract_number: Mapped[str] = mapped_column(String(40), nullable=False)
    vendor_id: Mapped[str] = mapped_column(
        String(36), ForeignKey("vendors.id"), nullable=False
    )
    coverage_description: Mapped[str] = mapped_column(Text, nullable=False)
    covered_assets: Mapped[list | None] = mapped_column(JSON, nullable=True)
    # [asset_id, ...]
    contract_type: Mapped[str] = mapped_column(String(20), nullable=False)
    # 'comprehensive' | 'non-comprehensive' | 'callout-only' | 'preventive-only'
    start_date: Mapped[date] = mapped_column(Date, nullable=False)
    end_date: Mapped[date] = mapped_column(Date, nullable=False, index=True)
    annual_value: Mapped[float] = mapped_column(Numeric(14, 2), nullable=False)
    payment_frequency: Mapped[str] = mapped_column(String(20), default="annual", nullable=False)
    # 'annual' | 'half-yearly' | 'quarterly' | 'monthly'
    next_invoice_due: Mapped[date | None] = mapped_column(Date, nullable=True)
    visit_frequency: Mapped[str | None] = mapped_column(String(20), nullable=True)
    # 'monthly' | 'quarterly' etc — schedules visits
    visits_per_year: Mapped[int | None] = mapped_column(Integer, nullable=True)
    callouts_used_in_year: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
    callouts_allowed_in_year: Mapped[int | None] = mapped_column(Integer, nullable=True)
    contact_person: Mapped[str | None] = mapped_column(String(127), nullable=True)
    contact_phone: Mapped[str | None] = mapped_column(String(40), nullable=True)
    contact_email: Mapped[str | None] = mapped_column(String(127), nullable=True)
    auto_renew: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)


class InspectionReport(Base, AuditMixin, TenantMixin):
    """Standalone inspection — separate from a maintenance task completion.
    e.g. structural survey, fire-safety drill report, electrical audit summary.
    """

    __tablename__ = "inspection_reports"
    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid4_str)
    asset_id: Mapped[str] = mapped_column(
        String(36), ForeignKey("assets.id"), nullable=False, index=True
    )
    inspection_type: Mapped[str] = mapped_column(String(40), nullable=False)
    # 'structural' | 'fire-safety' | 'electrical' | 'lift' | 'lightning-arrester' |
    # 'water-quality' | 'pest-control' | 'security-drill' | 'general'
    inspection_date: Mapped[date] = mapped_column(Date, nullable=False)
    inspector_name: Mapped[str] = mapped_column(String(255), nullable=False)
    inspector_organisation: Mapped[str | None] = mapped_column(String(255), nullable=True)
    inspector_credentials: Mapped[str | None] = mapped_column(String(255), nullable=True)
    overall_rating: Mapped[str | None] = mapped_column(String(20), nullable=True)
    # 'pass' | 'pass-with-defects' | 'fail' | 'critical-fail'
    findings: Mapped[str] = mapped_column(Text, nullable=False)
    defects: Mapped[list | None] = mapped_column(JSON, nullable=True)
    # [{description, severity, recommended_action, target_date, status}]
    report_document_path: Mapped[str | None] = mapped_column(String(511), nullable=True)
    photo_paths: Mapped[list | None] = mapped_column(JSON, nullable=True)
    next_inspection_due: Mapped[date | None] = mapped_column(Date, nullable=True)
    is_compliance_inspection: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    compliance_certificate_no: Mapped[str | None] = mapped_column(String(63), nullable=True)
    compliance_certificate_expiry: Mapped[date | None] = mapped_column(Date, nullable=True)

3. Reuse map

Existing model Used here as Why
organisations.id tenant_id Multi-tenant
branches.id branch_id on tasks/schedules Maintenance is branch-local
assets.id task and schedule asset binding Tasks attach to assets
members.id assignees (tasks, schedules, completions) Internal staff doing work
vendors.id (Vitta) amc_contracts.vendor_id Same vendor master
users.id sign-off user Auth identity
dispense_records.id (Inventory — this agent) materials_used in completion JSON Trace consumables used in maintenance

4. API surface

Routes under /api/maintenance/.

Method Path Purpose
GET /tasks List task templates
POST /tasks Create task template
GET /tasks/{id} Detail with upcoming schedules
PATCH /tasks/{id} Edit
POST /tasks/{id}/generate-schedules Project schedules N months ahead
GET /schedules Calendar view: filters by date/asset/branch/status
GET /schedules/today Today's tasks for branch
GET /schedules/overdue Overdue list
POST /schedules Create one-time/ad-hoc schedule
POST /schedules/{id}/start Mark in-progress
POST /schedules/{id}/complete Submit completion record
POST /schedules/{id}/skip Skip with reason
GET /completions/{id} Completion detail
GET /amc AMC list
POST /amc New AMC; spawns recurring schedules
GET /amc/{id} Detail with contract, schedules, callout log
POST /amc/{id}/renew Renew AMC; rolls schedules forward
POST /amc/{id}/callout Log callout (decrements callouts_used_in_year)
GET /inspections List inspection reports
POST /inspections New inspection report
GET /inspections/expiring-certificates Compliance certs expiring
GET /dashboard Branch maintenance dashboard
GET /calendar/ical Subscribe-able iCal feed of upcoming tasks

5. Service layer

src/aayojana/maintenance/services.py:

6. UI / Templates

7. Migration plan

Rev Title Tables added
0022 maintenance_module maintenance_tasks, maintenance_schedules, maintenance_completions, amc_contracts, inspection_reports

Single migration. Depends on Asset Management migration 0019 (asset FK target) and Vitta vendor master migration. Use use_alter=True on vendors.id FK if vendor master lands later than 0022.

8. Cross-module dependencies

Reads from

Writes to

9. Implementation phases

10. Open questions

  1. Asset-class scope tasks — when asset_scope='all-buildings', scheduler enumerates per-asset rows. Acceptable cardinality? For 5 buildings × 12 monthly inspections = 60 rows/year. Fine.
  2. Daily nitya maintenance volume — 365 rows per task per year is heavy. Recommend: frequency='daily' tasks materialise schedule rows lazily (only generate next 30 days at a time).
  3. AMC callout vs scheduled visit — if a visit is unscheduled (callout), do we still create a maintenance_schedule row, or only a completion + AMC.callouts_used++? Recommend: yes, retroactive schedule + completion for full audit trail.
  4. Inspection report immutability — once signed by inspector, lock from edit. Recommend: yes; set is_locked=True after sign-off; corrections require new versioned report.
  5. Photos storage — local filesystem vs GCS? Defer to infra; store paths.
  6. Compliance certificate cross-reference — does Statutory's certificate registry own these or do we duplicate them? Recommend: Statutory owns; we hold reference + inspection-cycle.
  7. Vendor performance scoring — track on-time vs delayed, defect rate per AMC vendor? Defer to Phase C.
  8. Mobile capture — completion-by-phone-camera flow critical for field staff; mobile-first form design. Plan for v1.5.
  9. Predictive maintenance — defect frequency on an asset triggers earlier inspections. Out of scope for v1.
  10. Holidays / fixed days — daily tasks skip festival-closed days? Let task have skip_on_branch_closed=True flag.