diff --git a/auditdwgimplementation.md b/auditdwgimplementation.md new file mode 100644 index 0000000..b48e681 --- /dev/null +++ b/auditdwgimplementation.md @@ -0,0 +1,2773 @@ +# DWG/PDF to BOM — Step-by-Step Implementation Guide + +**Based on:** `codeauditdwg.md` audit findings +**Date:** 2026-03-09 +**Scope:** Complete implementation plan to enable DWG/PDF upload → floor plan parsing → BOM generation + +--- + +## Table of Contents + +1. [Implementation Overview](#1-implementation-overview) +2. [Step 1: Backend Upload API — Accept DWG/DXF Files](#2-step-1-backend-upload-api) +3. [Step 2: Media Service — CAD File Support](#3-step-2-media-service) +4. [Step 3: DWG Converter Module](#4-step-3-dwg-converter) +5. [Step 4: PDF Floor Plan Extraction](#5-step-4-pdf-floor-plan-extraction) +6. [Step 5: Vision Engine — File-Type Router](#6-step-5-vision-engine-file-type-router) +7. [Step 6: Enhanced DXF Parser](#7-step-6-enhanced-dxf-parser) +8. [Step 7: Structural BOM Generator](#8-step-7-structural-bom-generator) +9. [Step 8: BOM Engine — Floor Plan Endpoint](#9-step-8-bom-engine-floor-plan-endpoint) +10. [Step 9: tRPC Router & Frontend Integration](#10-step-9-trpc-router-frontend) +11. [Step 10: Database Schema Changes](#11-step-10-database-schema) +12. [Step 11: Docker & Infrastructure](#12-step-11-docker-infrastructure) +13. [Step 12: Testing Strategy](#13-step-12-testing) +14. [Dependency Map & Build Order](#14-dependency-map) +15. [File Manifest](#15-file-manifest) + +--- + +## 1. Implementation Overview + +### The Three Broken Links (from audit) + +``` +LINK 1 (Upload Gate): + Frontend accepts .dwg → Backend REJECTS (MIME types not in allowlist) + +LINK 2 (DWG Reader): + pipeline.py imports dwg_converter → Module DOES NOT EXIST + +LINK 3 (Floor Plan → BOM Bridge): + FloorPlanData has walls/rooms/openings → BOM engine only accepts design variant spec_json +``` + +### Build Order (Dependency Chain) + +``` +Step 1: Upload API (route.ts) ── unblocks file ingestion +Step 2: Media Service (validator.py) ── unblocks server-side validation +Step 3: DWG Converter (dwg_converter.py) ── unblocks DWG→DXF conversion +Step 4: PDF Extractor (pdf_extractor.py) ── unblocks PDF floor plan processing +Step 5: Vision Engine Router ── routes files to correct pipeline +Step 6: Enhanced DXF Parser ── extracts richer structural data +Step 7: Structural BOM Generator ── converts geometry → materials +Step 8: BOM Engine Endpoint ── exposes /bom/from-floor-plan API +Step 9: tRPC + Frontend ── user-facing integration +Step 10: Database Schema ── floor_plan_bom_results table +Step 11: Docker/Infra ── LibreDWG, poppler-utils packages +Step 12: Tests ── end-to-end validation +``` + +### Files to Create (New) + +| # | File | Purpose | +|---|------|---------| +| 1 | `ml/floor-plan-digitizer/src/openlintel_digitizer/dwg_converter.py` | DWG→DXF conversion via LibreDWG/ODA | +| 2 | `ml/floor-plan-digitizer/src/openlintel_digitizer/pdf_extractor.py` | PDF→Image extraction for floor plans | +| 3 | `services/bom-engine/src/services/structural_bom.py` | Geometry→BOM material calculation | +| 4 | `services/bom-engine/src/models/floor_plan_bom.py` | Pydantic models for floor-plan BOM | +| 5 | `services/vision-engine/src/services/file_router.py` | File-type detection and routing | + +### Files to Modify (Existing) + +| # | File | Change | +|---|------|--------| +| 1 | `apps/web/src/app/api/upload/route.ts` | Add DWG/DXF MIME types, increase size limit | +| 2 | `services/media-service/src/services/validator.py` | Add CAD MIME types | +| 3 | `services/media-service/src/routers/upload.py` | Add DWG/DXF extension mapping | +| 4 | `services/vision-engine/src/routers/vision.py` | Add file-type routing before VLM | +| 5 | `services/vision-engine/main.py` | Register new router | +| 6 | `ml/floor-plan-digitizer/src/openlintel_digitizer/pipeline.py` | Wire PDF extractor | +| 7 | `ml/floor-plan-digitizer/pyproject.toml` | Add pdf2image, pdfplumber deps | +| 8 | `services/bom-engine/src/routers/bom.py` | Add /from-floor-plan endpoint | +| 9 | `services/bom-engine/pyproject.toml` | Add ezdxf dependency | +| 10 | `apps/web/src/server/trpc/routers/bom.ts` | Add generateFromFloorPlan mutation | +| 11 | `apps/web/src/server/trpc/routers/floorPlan.ts` | Add file-type aware digitization | +| 12 | `apps/web/src/app/(dashboard)/project/[id]/bom/page.tsx` | Add "BOM from Floor Plan" button | +| 13 | `packages/db/src/schema/app.ts` | Add floor_plan_bom_results table | +| 14 | `packages/python-shared/src/openlintel_shared/job_worker.py` | Add write_floor_plan_bom_result | +| 15 | `docker-compose.yml` | Add system deps to service images | + +--- + +## 2. Step 1: Backend Upload API — Accept DWG/DXF Files + +### Why This Is First +Every other step depends on files actually reaching the server. Currently, the upload API at `apps/web/src/app/api/upload/route.ts` rejects DWG/DXF with "Unsupported file type" because only image and PDF MIME types are allowed (line 9-15). + +### 2.1 Modify `apps/web/src/app/api/upload/route.ts` + +**Current code (lines 8-15):** +```typescript +const MAX_SIZE = 10 * 1024 * 1024; // 10MB +const ALLOWED_TYPES = [ + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/gif', + 'application/pdf', +]; +``` + +**Replace with:** +```typescript +const MAX_SIZE = 50 * 1024 * 1024; // 50MB — DWG files are commonly 10-40MB + +// MIME types sent by browsers for each format. +// DWG has no official IANA MIME type — browsers vary widely. +const ALLOWED_TYPES = [ + // Images + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/gif', + // PDF + 'application/pdf', + // DWG — browsers send different types depending on OS/browser + 'application/acad', + 'application/x-acad', + 'application/x-autocad', + 'application/dwg', + 'image/x-dwg', + 'image/vnd.dwg', + // DXF + 'application/dxf', + 'application/x-dxf', + 'image/vnd.dxf', + 'image/x-dxf', + // Fallback — many browsers send this for unknown binary formats + 'application/octet-stream', +]; + +// Because DWG/DXF MIME detection is unreliable, also validate by extension +const ALLOWED_EXTENSIONS = new Set([ + 'jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf', 'dwg', 'dxf', +]); + +const CAD_EXTENSIONS = new Set(['dwg', 'dxf']); +``` + +**Add extension validation function (after line 17):** +```typescript +function getFileExtension(filename: string): string { + return (filename.split('.').pop() ?? '').toLowerCase(); +} + +function isAllowedFile(file: File): boolean { + const ext = getFileExtension(file.name); + // If extension is in our allowed list, accept regardless of MIME type + if (ALLOWED_EXTENSIONS.has(ext)) return true; + // Otherwise fall back to MIME type check + return ALLOWED_TYPES.includes(file.type); +} + +function isCADFile(filename: string): boolean { + return CAD_EXTENSIONS.has(getFileExtension(filename)); +} +``` + +**Modify the POST handler validation block (lines 56-62):** + +Replace: +```typescript +if (!ALLOWED_TYPES.includes(file.type)) { + return NextResponse.json({ error: 'Unsupported file type' }, { status: 400 }); +} +``` + +With: +```typescript +if (!isAllowedFile(file)) { + return NextResponse.json( + { error: `Unsupported file type. Allowed: images, PDF, DWG, DXF` }, + { status: 400 }, + ); +} +``` + +**Modify thumbnail/hash logic (lines 88-97) to skip for CAD files:** + +Replace: +```typescript +// Generate thumbnail for images +let thumbnailKey: string | null = null; +const thumbnail = await generateThumbnail(buffer, file.type); +if (thumbnail) { + thumbnailKey = storageKey.replace(/\.[^.]+$/, '_thumb.jpg'); + await saveFile(thumbnail, thumbnailKey, 'image/jpeg'); +} + +// Compute image hash for deduplication +const imageHash = IMAGE_TYPES.includes(file.type) ? computeImageHash(buffer) : null; +``` + +With: +```typescript +// Generate thumbnail for images (not for CAD files) +let thumbnailKey: string | null = null; +const isCad = isCADFile(file.name); +if (!isCad) { + const thumbnail = await generateThumbnail(buffer, file.type); + if (thumbnail) { + thumbnailKey = storageKey.replace(/\.[^.]+$/, '_thumb.jpg'); + await saveFile(thumbnail, thumbnailKey, 'image/jpeg'); + } +} + +// Compute image hash for deduplication (images only) +const imageHash = IMAGE_TYPES.includes(file.type) ? computeImageHash(buffer) : null; +``` + +### 2.2 Why `application/octet-stream` Is Safe Here + +Including `application/octet-stream` in the allow-list might seem risky. It is safe because: +1. We still validate by file extension (`ALLOWED_EXTENSIONS`) +2. The `isAllowedFile` function requires EITHER a known extension OR a known MIME type +3. CAD files from most browsers will come as `octet-stream` + `.dwg` extension +4. Files without a known extension AND with `octet-stream` type will be rejected + +**However**, if you prefer stricter validation, use this alternative: +```typescript +function isAllowedFile(file: File): boolean { + const ext = getFileExtension(file.name); + if (!ALLOWED_EXTENSIONS.has(ext)) return false; // Extension MUST match + // For non-CAD files, also check MIME type + if (!CAD_EXTENSIONS.has(ext) && !ALLOWED_TYPES.includes(file.type)) return false; + return true; +} +``` + +### 2.3 Verification + +After this change: +- Uploading `floorplan.dwg` → stored in MinIO with storage key +- Uploading `floorplan.dxf` → stored in MinIO with storage key +- Uploading `plan.pdf` → still works (no change) +- Uploading `photo.jpg` → still works (no change) +- Uploading `malware.exe` → rejected ("Unsupported file type") + +--- + +## 3. Step 2: Media Service — CAD File Support + +### 3.1 Modify `services/media-service/src/services/validator.py` + +**Current `ALLOWED_MIME_TYPES` (in the file):** +```python +ALLOWED_MIME_TYPES = { + "image/jpeg", "image/png", "image/webp", "image/gif", + "application/pdf", +} +``` + +**Replace with:** +```python +ALLOWED_MIME_TYPES = { + # Images + "image/jpeg", "image/png", "image/webp", "image/gif", + # PDF + "application/pdf", + # DWG (no official IANA type — browsers vary) + "application/acad", "application/x-acad", "application/x-autocad", + "application/dwg", "image/x-dwg", "image/vnd.dwg", + # DXF + "application/dxf", "application/x-dxf", + "image/vnd.dxf", "image/x-dxf", + # Fallback for CAD files + "application/octet-stream", +} + +# Extensions that are always allowed (overrides MIME check) +ALLOWED_EXTENSIONS = {".dwg", ".dxf", ".pdf", ".jpg", ".jpeg", ".png", ".webp", ".gif"} + +# CAD file extensions (skip image integrity checks) +CAD_EXTENSIONS = {".dwg", ".dxf"} + +MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB (was 20MB) +``` + +**Modify `validate_file()` to accept CAD files:** +```python +def validate_file( + file_bytes: bytes, + content_type: str, + filename: str = "", # NEW parameter +) -> ValidationResult: + """Validate an uploaded file.""" + # Extension-based override for CAD files + ext = _get_extension(filename) + is_cad = ext in CAD_EXTENSIONS + + # Size check + size_result = _check_file_size(file_bytes) + if not size_result.valid: + return size_result + + # MIME check — relaxed for known CAD extensions + if not is_cad: + mime_result = _check_mime_type(content_type) + if not mime_result.valid: + # Also check extension + if ext not in ALLOWED_EXTENSIONS: + return mime_result + + # Image integrity — skip for CAD and PDF files + if not is_cad and content_type != "application/pdf": + integrity_result = _check_image_integrity(file_bytes) + if not integrity_result.valid: + return integrity_result + + return ValidationResult(valid=True, error=None) + + +def _get_extension(filename: str) -> str: + """Extract lowercase file extension with dot.""" + if not filename: + return "" + parts = filename.rsplit(".", 1) + return f".{parts[-1].lower()}" if len(parts) > 1 else "" +``` + +### 3.2 Modify `services/media-service/src/routers/upload.py` + +**Update `_mime_to_extension()` (line 59-68):** +```python +def _mime_to_extension(mime_type: str, filename: str = "") -> str: + """Map MIME type to a file extension, with filename fallback.""" + mapping: dict[str, str] = { + "image/jpeg": ".jpg", + "image/png": ".png", + "image/webp": ".webp", + "image/gif": ".gif", + "application/pdf": ".pdf", + } + ext = mapping.get(mime_type) + if ext: + return ext + # For CAD files, derive from original filename + if filename: + original_ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else "" + if original_ext in ("dwg", "dxf"): + return f".{original_ext}" + return ".bin" +``` + +**Update `_build_metadata()` (line 71-103) to handle CAD files:** +```python +def _build_metadata( + image_bytes: bytes, + mime_type: str, + original_size: int, + filename: str = "", +) -> MediaMetadata: + """Build a MediaMetadata object from file bytes.""" + exif = extract_metadata(image_bytes) + image_hash = compute_image_hash(image_bytes) + + ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else "" + + # For PDFs and CAD files, we cannot extract pixel dimensions + if mime_type == "application/pdf" or ext in ("dwg", "dxf"): + return MediaMetadata( + width=0, + height=0, + format=ext.upper() if ext in ("dwg", "dxf") else "PDF", + mode="N/A", + file_size_bytes=original_size, + has_alpha=False, + image_hash=image_hash, + exif=exif, + ) + + img = Image.open(io.BytesIO(image_bytes)) + return MediaMetadata( + width=img.width, + height=img.height, + format=img.format or mime_type.split("/")[-1].upper(), + mode=img.mode, + file_size_bytes=original_size, + has_alpha=img.mode in ("RGBA", "LA", "PA"), + image_hash=image_hash, + exif=exif, + ) +``` + +**Update the upload endpoint to pass filename through:** + +In the `upload_media()` function, update the `validate_file` and `_build_metadata` calls: +```python +result = validate_file(file_bytes, content_type, filename=original_filename) +# ... +metadata = _build_metadata(file_bytes, content_type, original_size, filename=original_filename) +# ... +ext = _mime_to_extension(content_type, filename=original_filename) +``` + +--- + +## 4. Step 3: DWG Converter Module + +### 4.1 Create `ml/floor-plan-digitizer/src/openlintel_digitizer/dwg_converter.py` + +This is the **CRITICAL missing module** identified in the audit. The pipeline at `pipeline.py:169` imports `DWGConverter` from this module, but it was never created. + +```python +""" +DWG to DXF conversion using LibreDWG or ODA File Converter. + +Conversion strategy (in priority order): +1. LibreDWG ``dwg2dxf`` — open-source, GPLv3, supports R2000–R2018. +2. ODA File Converter — free (proprietary), supports R14–R2024. +3. Neither available → raise RuntimeError with install instructions. + +Usage:: + + converter = DWGConverter() + if converter.is_available: + dxf_path = await converter.convert("input.dwg") + else: + print(converter.install_instructions) + +The converted DXF is then parsed by ``ezdxf`` via ``FloorPlanPipeline._parse_dxf()``. +""" + +from __future__ import annotations + +import asyncio +import logging +import shutil +import tempfile +from pathlib import Path + +logger = logging.getLogger(__name__) + + +class ConversionError(Exception): + """Raised when DWG-to-DXF conversion fails.""" + + +class DWGConverter: + """Converts DWG files to DXF format using available system tools. + + Parameters + ---------- + libredwg_path: + Explicit path to the ``dwg2dxf`` binary. If ``None``, searches ``$PATH``. + oda_converter_path: + Explicit path to the ``ODAFileConverter`` binary. If ``None``, searches ``$PATH``. + timeout_seconds: + Maximum time to wait for a conversion subprocess. + """ + + def __init__( + self, + *, + libredwg_path: str | None = None, + oda_converter_path: str | None = None, + timeout_seconds: int = 120, + ) -> None: + self._libredwg = libredwg_path or shutil.which("dwg2dxf") + self._oda = oda_converter_path or shutil.which("ODAFileConverter") + self._timeout = timeout_seconds + + # -- Public properties ----------------------------------------------------- + + @property + def is_available(self) -> bool: + """True if at least one conversion backend is installed.""" + return bool(self._libredwg or self._oda) + + @property + def backend(self) -> str: + """Name of the active backend: ``'libredwg'``, ``'oda'``, or ``'none'``.""" + if self._libredwg: + return "libredwg" + if self._oda: + return "oda" + return "none" + + @property + def install_instructions(self) -> str: + """Human-readable install instructions when no backend is found.""" + return ( + "No DWG conversion backend found.\n" + "Install one of:\n" + " 1. LibreDWG (open source):\n" + " Ubuntu/Debian: sudo apt-get install libredwg-utils\n" + " macOS: brew install libredwg\n" + " 2. ODA File Converter (free, proprietary):\n" + " Download from https://www.opendesign.com/guestfiles/oda_file_converter\n" + ) + + # -- Public API ------------------------------------------------------------ + + async def convert( + self, + dwg_path: str | Path, + *, + output_path: str | Path | None = None, + dxf_version: str = "R2013", + ) -> Path: + """Convert a DWG file to DXF. + + Parameters + ---------- + dwg_path: + Path to the input ``.dwg`` file. + output_path: + Destination for the ``.dxf`` file. Defaults to the same directory + and basename as the input with ``.dxf`` extension. + dxf_version: + Target DXF version (used by ODA converter). LibreDWG outputs + the version matching the source DWG. + + Returns + ------- + Path + Absolute path to the generated DXF file. + + Raises + ------ + FileNotFoundError + If the input DWG file does not exist. + ConversionError + If the conversion subprocess fails. + RuntimeError + If no conversion backend is available. + """ + dwg_path = Path(dwg_path).resolve() + if not dwg_path.exists(): + raise FileNotFoundError(f"DWG file not found: {dwg_path}") + if not dwg_path.suffix.lower() == ".dwg": + raise ValueError(f"Expected .dwg file, got: {dwg_path.suffix}") + + if output_path is None: + output_path = dwg_path.with_suffix(".dxf") + output_path = Path(output_path).resolve() + output_path.parent.mkdir(parents=True, exist_ok=True) + + logger.info( + "Converting DWG→DXF: %s → %s (backend=%s)", + dwg_path.name, + output_path.name, + self.backend, + ) + + if self._libredwg: + return await self._convert_libredwg(dwg_path, output_path) + elif self._oda: + return await self._convert_oda(dwg_path, output_path, dxf_version) + else: + raise RuntimeError(self.install_instructions) + + async def check_health(self) -> dict[str, str | bool]: + """Check which backends are available and their versions.""" + result: dict[str, str | bool] = { + "available": self.is_available, + "backend": self.backend, + } + + if self._libredwg: + try: + proc = await asyncio.create_subprocess_exec( + self._libredwg, "--version", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5) + result["libredwg_version"] = stdout.decode().strip() + except Exception as exc: + result["libredwg_error"] = str(exc) + + if self._oda: + result["oda_path"] = self._oda + + return result + + # -- Private conversion methods -------------------------------------------- + + async def _convert_libredwg( + self, + dwg_path: Path, + output_path: Path, + ) -> Path: + """Convert using LibreDWG's ``dwg2dxf`` command-line tool. + + Command: ``dwg2dxf -o `` + + LibreDWG preserves the DWG's native version in the output DXF. + Supports AutoCAD R2000 through R2018 formats. + """ + cmd = [self._libredwg, "-o", str(output_path), str(dwg_path)] + + logger.debug("Running: %s", " ".join(cmd)) + + try: + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for( + proc.communicate(), + timeout=self._timeout, + ) + except asyncio.TimeoutError: + proc.kill() + raise ConversionError( + f"LibreDWG conversion timed out after {self._timeout}s " + f"for {dwg_path.name}" + ) + + if proc.returncode != 0: + error_msg = stderr.decode().strip() or stdout.decode().strip() + raise ConversionError( + f"dwg2dxf exited with code {proc.returncode}: {error_msg}" + ) + + if not output_path.exists(): + raise ConversionError( + f"dwg2dxf completed but output file not found: {output_path}" + ) + + logger.info( + "LibreDWG conversion successful: %s (%d bytes)", + output_path.name, + output_path.stat().st_size, + ) + return output_path + + async def _convert_oda( + self, + dwg_path: Path, + output_path: Path, + dxf_version: str, + ) -> Path: + """Convert using ODA File Converter. + + ODA works on entire directories, so we: + 1. Copy the DWG into a temp input directory + 2. Run ODAFileConverter with input_dir, output_dir, version, format + 3. Copy the resulting DXF to the desired output path + 4. Clean up temp directories + + ODA command: + ODAFileConverter + Example: ODAFileConverter /tmp/in /tmp/out ACAD2013 DXF 0 1 + """ + oda_version_map = { + "R14": "ACAD14", + "R2000": "ACAD2000", + "R2004": "ACAD2004", + "R2007": "ACAD2007", + "R2010": "ACAD2010", + "R2013": "ACAD2013", + "R2018": "ACAD2018", + } + oda_version = oda_version_map.get(dxf_version, "ACAD2013") + + with tempfile.TemporaryDirectory(prefix="openlintel_oda_") as tmpdir: + input_dir = Path(tmpdir) / "input" + output_dir = Path(tmpdir) / "output" + input_dir.mkdir() + output_dir.mkdir() + + # Copy DWG to input directory + shutil.copy2(dwg_path, input_dir / dwg_path.name) + + cmd = [ + self._oda, + str(input_dir), + str(output_dir), + oda_version, + "DXF", + "0", # recurse = no + "1", # audit = yes + ] + + logger.debug("Running: %s", " ".join(cmd)) + + try: + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for( + proc.communicate(), + timeout=self._timeout, + ) + except asyncio.TimeoutError: + proc.kill() + raise ConversionError( + f"ODA conversion timed out after {self._timeout}s " + f"for {dwg_path.name}" + ) + + # ODA does not always return non-zero on failure + converted_files = list(output_dir.glob("*.dxf")) + + if not converted_files: + error_msg = stderr.decode().strip() or stdout.decode().strip() + raise ConversionError( + f"ODA File Converter produced no DXF output. " + f"Return code: {proc.returncode}. " + f"Error: {error_msg or 'unknown'}" + ) + + # Copy the first (and usually only) DXF to desired output + shutil.copy2(converted_files[0], output_path) + + logger.info( + "ODA conversion successful: %s (%d bytes)", + output_path.name, + output_path.stat().st_size, + ) + return output_path +``` + +### 4.2 Update `ml/floor-plan-digitizer/pyproject.toml` + +Add the new dependencies under `[project.dependencies]`: + +```toml +[project] +dependencies = [ + "ezdxf>=1.3,<2", + "opencv-python-headless>=4.9,<5", + "numpy>=1.26,<2", + "pillow>=11.0,<12", + "litellm>=1.50,<2", + "pydantic>=2.10,<3", + "structlog>=24.0,<25", + # NEW: PDF floor plan extraction + "pdf2image>=1.16,<2", + "pdfplumber>=0.11,<1", +] +``` + +### 4.3 Verify Pipeline Integration + +The existing `pipeline.py:169` already does: +```python +from openlintel_digitizer.dwg_converter import DWGConverter +``` + +Once `dwg_converter.py` is created, this import will resolve and the `digitize_dwg()` method (lines 146-200) will work as designed. **No changes to `pipeline.py` are needed for DWG support** — the scaffolding was already correct. + +--- + +## 5. Step 4: PDF Floor Plan Extraction + +### 5.1 Create `ml/floor-plan-digitizer/src/openlintel_digitizer/pdf_extractor.py` + +```python +""" +PDF floor plan extraction — converts PDF pages to images for VLM processing, +with optional vector geometry extraction for CAD-exported PDFs. + +Two strategies: +1. **Raster path** (scanned PDFs, screenshots): + pdf2image converts pages to high-res PNG for VLM analysis. +2. **Vector path** (AutoCAD-exported PDFs): + pdfplumber extracts lines/rects/curves as geometric primitives, + which map to walls/rooms without needing VLM. + +The raster path is always used as the primary pipeline; +vector extraction is attempted first as an optimization. +""" + +from __future__ import annotations + +import io +import logging +from pathlib import Path +from typing import Any + +import pdfplumber +from pdf2image import convert_from_bytes, convert_from_path +from PIL import Image + +logger = logging.getLogger(__name__) + + +class PDFExtractionResult: + """Result of PDF floor plan extraction.""" + + def __init__( + self, + *, + images: list[Image.Image], + page_count: int, + vector_data: dict[str, Any] | None = None, + has_vector_content: bool = False, + ) -> None: + self.images = images + self.page_count = page_count + self.vector_data = vector_data + self.has_vector_content = has_vector_content + + @property + def primary_image(self) -> Image.Image | None: + """The main floor plan image (first page, or largest page).""" + if not self.images: + return None + if len(self.images) == 1: + return self.images[0] + # Return the page with the most content (largest area) + return max(self.images, key=lambda img: img.width * img.height) + + +class PDFFloorPlanExtractor: + """Extracts floor plan data from PDF files. + + Parameters + ---------- + dpi: + Resolution for raster conversion. 300 DPI is optimal for VLM + analysis — high enough for text/line clarity, low enough for + reasonable file sizes. + max_pages: + Maximum pages to process. Floor plans are usually 1-3 pages. + """ + + def __init__(self, *, dpi: int = 300, max_pages: int = 5) -> None: + self._dpi = dpi + self._max_pages = max_pages + + async def extract( + self, + *, + pdf_bytes: bytes | None = None, + pdf_path: str | Path | None = None, + ) -> PDFExtractionResult: + """Extract floor plan images from a PDF. + + Provide exactly one of ``pdf_bytes`` or ``pdf_path``. + + Returns + ------- + PDFExtractionResult + Contains rasterised page images and optional vector data. + """ + if pdf_bytes is None and pdf_path is None: + raise ValueError("Provide one of: pdf_bytes, pdf_path") + + # Get page count + if pdf_bytes is not None: + pdf = pdfplumber.open(io.BytesIO(pdf_bytes)) + else: + pdf = pdfplumber.open(str(pdf_path)) + + page_count = len(pdf.pages) + pages_to_process = min(page_count, self._max_pages) + + logger.info( + "Extracting floor plan from PDF: %d pages (processing %d)", + page_count, + pages_to_process, + ) + + # Attempt vector extraction first + vector_data = self._extract_vector_data(pdf, pages_to_process) + has_vector = bool( + vector_data + and (vector_data.get("lines", []) or vector_data.get("rects", [])) + ) + + if has_vector: + logger.info( + "Vector content detected: %d lines, %d rects", + len(vector_data.get("lines", [])), + len(vector_data.get("rects", [])), + ) + + pdf.close() + + # Rasterise pages for VLM + if pdf_bytes is not None: + images = convert_from_bytes( + pdf_bytes, + dpi=self._dpi, + fmt="png", + first_page=1, + last_page=pages_to_process, + ) + else: + images = convert_from_path( + str(pdf_path), + dpi=self._dpi, + fmt="png", + first_page=1, + last_page=pages_to_process, + ) + + logger.info( + "Rasterised %d pages at %d DPI", + len(images), + self._dpi, + ) + + return PDFExtractionResult( + images=images, + page_count=page_count, + vector_data=vector_data if has_vector else None, + has_vector_content=has_vector, + ) + + def extract_page_image( + self, + pdf_bytes: bytes, + page_number: int = 1, + ) -> Image.Image: + """Extract a single page as an image. + + Parameters + ---------- + pdf_bytes: + Raw PDF file bytes. + page_number: + 1-based page number to extract. + + Returns + ------- + PIL.Image.Image + The rasterised page. + """ + images = convert_from_bytes( + pdf_bytes, + dpi=self._dpi, + fmt="png", + first_page=page_number, + last_page=page_number, + ) + if not images: + raise ValueError(f"Could not extract page {page_number} from PDF") + return images[0] + + @staticmethod + def _extract_vector_data( + pdf: pdfplumber.PDF, + max_pages: int, + ) -> dict[str, Any]: + """Extract vector geometry from PDF pages. + + This works best for PDFs exported from AutoCAD or similar CAD tools, + which embed actual vector line/rect/curve data (not rasterised images). + """ + all_lines: list[dict[str, float]] = [] + all_rects: list[dict[str, float]] = [] + all_texts: list[dict[str, Any]] = [] + + for i, page in enumerate(pdf.pages[:max_pages]): + page_height = float(page.height) + + # Extract lines (wall candidates) + for line in (page.lines or []): + all_lines.append({ + "x0": float(line["x0"]), + "y0": page_height - float(line["top"]), + "x1": float(line["x1"]), + "y1": page_height - float(line["bottom"]), + "width": float(line.get("linewidth", 1)), + "page": i, + }) + + # Extract rectangles (room candidates) + for rect in (page.rects or []): + all_rects.append({ + "x0": float(rect["x0"]), + "y0": page_height - float(rect["top"]), + "x1": float(rect["x1"]), + "y1": page_height - float(rect["bottom"]), + "width": float(rect.get("linewidth", 1)), + "page": i, + }) + + # Extract text (room labels, dimensions) + for char_group in (page.extract_words() or []): + all_texts.append({ + "text": char_group["text"], + "x": float(char_group["x0"]), + "y": page_height - float(char_group["top"]), + "page": i, + }) + + return { + "lines": all_lines, + "rects": all_rects, + "texts": all_texts, + } +``` + +### 5.2 Update Pipeline to Support PDF Input + +**Modify `ml/floor-plan-digitizer/src/openlintel_digitizer/pipeline.py`** + +Add a new method after `digitize_dxf()` (around line 220): + +```python +async def digitize_pdf( + self, + *, + pdf_bytes: bytes | None = None, + pdf_path: str | Path | None = None, + page_number: int = 1, + output_dxf_path: str | Path | None = None, +) -> FloorPlanData: + """Digitize a floor plan from a PDF file. + + For PDFs exported from AutoCAD, attempts vector extraction first. + Falls back to raster VLM extraction for scanned PDFs. + + Parameters + ---------- + pdf_bytes: + Raw PDF bytes. + pdf_path: + Path to a PDF file. + page_number: + Which page to extract (1-based). Defaults to 1. + output_dxf_path: + If provided, generate and save a DXF file. + + Returns + ------- + FloorPlanData + Structured floor plan data. + """ + from openlintel_digitizer.pdf_extractor import PDFFloorPlanExtractor + + extractor = PDFFloorPlanExtractor() + + logger.info("Digitizing PDF floor plan (page %d)", page_number) + + # Extract the target page as an image + if pdf_bytes is not None: + page_image = extractor.extract_page_image(pdf_bytes, page_number) + elif pdf_path is not None: + with open(pdf_path, "rb") as f: + page_image = extractor.extract_page_image(f.read(), page_number) + else: + raise ValueError("Provide one of: pdf_bytes, pdf_path") + + # Use the raster VLM pipeline on the extracted page image + floor_plan = await self.digitize_image( + image=page_image, + skip_preprocess=False, + output_dxf_path=output_dxf_path, + ) + floor_plan.source_type = "pdf" + + return floor_plan +``` + +Also add to `__init__.py` exports: + +```python +from openlintel_digitizer.pdf_extractor import PDFFloorPlanExtractor +``` + +--- + +## 6. Step 5: Vision Engine — File-Type Router + +### 6.1 Create `services/vision-engine/src/services/file_router.py` + +This service determines the processing strategy based on file type and routes to the correct pipeline. + +```python +""" +File-type detection and routing for floor plan digitization. + +Routes uploaded files to the correct processing pipeline: +- DWG files → DWG converter → DXF parser → FloorPlanData +- DXF files → DXF parser → FloorPlanData +- PDF files → PDF extractor → VLM extraction → FloorPlanData +- Images → VLM extraction → FloorPlanData + +All paths converge on FloorPlanData as the canonical output format. +""" + +from __future__ import annotations + +import logging +from enum import Enum + +logger = logging.getLogger(__name__) + + +class FileType(str, Enum): + """Detected file type categories.""" + DWG = "dwg" + DXF = "dxf" + PDF = "pdf" + IMAGE = "image" + UNKNOWN = "unknown" + + +# MIME types grouped by FileType +_MIME_MAP: dict[str, FileType] = { + # DWG + "application/acad": FileType.DWG, + "application/x-acad": FileType.DWG, + "application/x-autocad": FileType.DWG, + "application/dwg": FileType.DWG, + "image/x-dwg": FileType.DWG, + "image/vnd.dwg": FileType.DWG, + # DXF + "application/dxf": FileType.DXF, + "application/x-dxf": FileType.DXF, + "image/vnd.dxf": FileType.DXF, + "image/x-dxf": FileType.DXF, + # PDF + "application/pdf": FileType.PDF, + # Images + "image/jpeg": FileType.IMAGE, + "image/png": FileType.IMAGE, + "image/webp": FileType.IMAGE, + "image/gif": FileType.IMAGE, +} + +_EXT_MAP: dict[str, FileType] = { + ".dwg": FileType.DWG, + ".dxf": FileType.DXF, + ".pdf": FileType.PDF, + ".jpg": FileType.IMAGE, + ".jpeg": FileType.IMAGE, + ".png": FileType.IMAGE, + ".webp": FileType.IMAGE, + ".gif": FileType.IMAGE, +} + +# DWG magic bytes: "AC10" (ASCII) at offset 0 +_DWG_MAGIC = b"AC10" + + +def detect_file_type( + *, + mime_type: str = "", + filename: str = "", + file_bytes: bytes | None = None, +) -> FileType: + """Detect the file type using MIME, extension, and magic bytes. + + Priority: magic bytes > extension > MIME type. + + Parameters + ---------- + mime_type: + Content-Type header value. + filename: + Original filename with extension. + file_bytes: + First few bytes of the file for magic-byte detection. + + Returns + ------- + FileType + Detected file category. + """ + # 1. Magic bytes (most reliable for DWG) + if file_bytes and len(file_bytes) >= 4: + if file_bytes[:4] == _DWG_MAGIC or file_bytes[:2] == b"AC": + logger.debug("Detected DWG via magic bytes") + return FileType.DWG + + # 2. File extension (reliable for most cases) + if filename: + ext = "." + filename.rsplit(".", 1)[-1].lower() if "." in filename else "" + ext_type = _EXT_MAP.get(ext) + if ext_type: + logger.debug("Detected %s via extension: %s", ext_type.value, ext) + return ext_type + + # 3. MIME type (least reliable for CAD files) + mime_type_lower = mime_type.lower().strip() + mime_result = _MIME_MAP.get(mime_type_lower) + if mime_result: + logger.debug("Detected %s via MIME: %s", mime_result.value, mime_type_lower) + return mime_result + + # 4. application/octet-stream — check extension again + if mime_type_lower == "application/octet-stream" and filename: + ext = "." + filename.rsplit(".", 1)[-1].lower() if "." in filename else "" + ext_type = _EXT_MAP.get(ext) + if ext_type: + return ext_type + + logger.warning( + "Could not detect file type: mime=%s, filename=%s", + mime_type, + filename, + ) + return FileType.UNKNOWN + + +def requires_conversion(file_type: FileType) -> bool: + """Whether the file needs conversion before DXF parsing.""" + return file_type == FileType.DWG + + +def requires_vlm(file_type: FileType) -> bool: + """Whether the file needs VLM (vision-language model) processing.""" + return file_type in (FileType.IMAGE, FileType.PDF) + + +def can_parse_directly(file_type: FileType) -> bool: + """Whether the file can be parsed directly with ezdxf.""" + return file_type == FileType.DXF +``` + +### 6.2 Modify `services/vision-engine/src/routers/vision.py` + +Add a new endpoint alongside the existing `/job` endpoint. Add this after line 68: + +```python +class FloorPlanDigitizeInput(BaseModel): + """Request model for the enhanced floor plan digitization job.""" + job_id: str + user_id: str + project_id: str + upload_id: str + storage_key: str + filename: str = "" + mime_type: str = "" + + +@router.post( + "/digitize", + status_code=status.HTTP_202_ACCEPTED, + summary="Digitize a floor plan file (DWG, DXF, PDF, or image)", +) +async def digitize_floor_plan( + request: FloorPlanDigitizeInput, + background_tasks: BackgroundTasks, + db: Annotated[AsyncSession, Depends(get_db_session)], + settings: Annotated[Settings, Depends(get_settings)], +) -> dict: + """Enhanced digitization endpoint that handles all file types. + + Routes DWG/DXF/PDF/image files to the correct processing pipeline. + Falls back to the existing VLM-only pipeline for images. + """ + try: + await update_job_status(db, request.job_id, status="running", progress=5) + + background_tasks.add_task( + _run_file_aware_digitization, + job_id=request.job_id, + user_id=request.user_id, + project_id=request.project_id, + storage_key=request.storage_key, + filename=request.filename, + mime_type=request.mime_type, + ) + + logger.info( + "digitize_job_dispatched", + job_id=request.job_id, + filename=request.filename, + mime_type=request.mime_type, + ) + return {"status": "accepted", "job_id": request.job_id} + + except Exception as exc: + logger.error("digitize_dispatch_failed", job_id=request.job_id, error=str(exc)) + await update_job_status(db, request.job_id, status="failed", error=str(exc)) + return {"status": "failed", "error": str(exc)} + + +async def _run_file_aware_digitization( + job_id: str, + user_id: str, + project_id: str, + storage_key: str, + filename: str, + mime_type: str, +) -> None: + """Background task: route file to correct pipeline based on type.""" + import asyncio + import tempfile + from pathlib import Path + + from openlintel_shared.config import get_settings + from openlintel_shared.storage import download_file + + from src.services.file_router import FileType, detect_file_type + + session_factory = get_session_factory() + settings = get_settings() + async with session_factory() as db: + try: + await update_job_status(db, job_id, status="running", progress=10) + + # Download file from MinIO + bucket = settings.MINIO_BUCKET + file_bytes = await asyncio.to_thread( + download_file, bucket, storage_key + ) + + # Detect file type + file_type = detect_file_type( + mime_type=mime_type, + filename=filename, + file_bytes=file_bytes[:16] if file_bytes else None, + ) + + logger.info( + "file_type_detected", + job_id=job_id, + file_type=file_type.value, + filename=filename, + ) + + await update_job_status(db, job_id, status="running", progress=20) + + if file_type == FileType.DWG: + output = await _process_dwg(db, job_id, file_bytes, filename, user_id) + elif file_type == FileType.DXF: + output = await _process_dxf(db, job_id, file_bytes, filename) + elif file_type == FileType.PDF: + output = await _process_pdf(db, job_id, file_bytes, user_id) + elif file_type == FileType.IMAGE: + output = await _process_image(db, job_id, storage_key, user_id) + else: + raise ValueError(f"Unsupported file type: {file_type.value}") + + await update_job_status( + db, job_id, + status="completed", + progress=100, + output_json=output, + ) + + logger.info("digitize_job_completed", job_id=job_id, file_type=file_type.value) + + except Exception as exc: + logger.error("digitize_job_failed", job_id=job_id, error=str(exc)) + await update_job_status(db, job_id, status="failed", error=str(exc)) + + +async def _process_dwg(db, job_id, file_bytes, filename, user_id): + """DWG → DXF → FloorPlanData.""" + import tempfile + from pathlib import Path + + await update_job_status(db, job_id, status="running", progress=30) + + with tempfile.TemporaryDirectory(prefix="openlintel_dwg_") as tmpdir: + dwg_path = Path(tmpdir) / filename + dwg_path.write_bytes(file_bytes) + + from openlintel_digitizer.dwg_converter import DWGConverter + converter = DWGConverter() + + if not converter.is_available: + raise RuntimeError(converter.install_instructions) + + await update_job_status(db, job_id, status="running", progress=40) + + dxf_path = await converter.convert(dwg_path) + + await update_job_status(db, job_id, status="running", progress=60) + + from openlintel_digitizer.pipeline import FloorPlanPipeline + pipeline = FloorPlanPipeline() + floor_plan = pipeline.digitize_dxf(dxf_path) + + await update_job_status(db, job_id, status="running", progress=80) + + return _floor_plan_to_output(floor_plan) + + +async def _process_dxf(db, job_id, file_bytes, filename): + """DXF → FloorPlanData (direct parsing, no conversion needed).""" + import tempfile + from pathlib import Path + + await update_job_status(db, job_id, status="running", progress=40) + + with tempfile.TemporaryDirectory(prefix="openlintel_dxf_") as tmpdir: + dxf_path = Path(tmpdir) / filename + dxf_path.write_bytes(file_bytes) + + from openlintel_digitizer.pipeline import FloorPlanPipeline + pipeline = FloorPlanPipeline() + floor_plan = pipeline.digitize_dxf(dxf_path) + + await update_job_status(db, job_id, status="running", progress=80) + + return _floor_plan_to_output(floor_plan) + + +async def _process_pdf(db, job_id, file_bytes, user_id): + """PDF → Image → VLM → FloorPlanData.""" + await update_job_status(db, job_id, status="running", progress=30) + + from openlintel_digitizer.pipeline import FloorPlanPipeline + + api_key = await get_user_api_key(db, user_id, provider="openai") + vlm_api_key = None + if api_key: + from openlintel_shared.crypto import decrypt_api_key + vlm_api_key = decrypt_api_key( + encrypted_key=api_key["encrypted_key"], + iv=api_key["iv"], + auth_tag=api_key["auth_tag"], + ) + + pipeline = FloorPlanPipeline(vlm_api_key=vlm_api_key) + + await update_job_status(db, job_id, status="running", progress=40) + + floor_plan = await pipeline.digitize_pdf(pdf_bytes=file_bytes) + + await update_job_status(db, job_id, status="running", progress=80) + + return _floor_plan_to_output(floor_plan) + + +async def _process_image(db, job_id, storage_key, user_id): + """Image → VLM → FloorPlanData (existing pipeline).""" + from openlintel_shared.config import get_settings + settings = get_settings() + origin = f"http://localhost:{settings.PORT}" if hasattr(settings, "PORT") else "http://localhost:3000" + image_url = f"{origin}/api/uploads/{storage_key}" + + api_key = await get_user_api_key(db, user_id, provider="openai") + if api_key is None: + raise ValueError("No API key configured for provider 'openai'") + + await update_job_status(db, job_id, status="running", progress=40) + + result = await detect_rooms_from_image( + image_url=image_url, + api_key_material={ + "encrypted_key": api_key["encrypted_key"], + "iv": api_key["iv"], + "auth_tag": api_key["auth_tag"], + }, + ) + + return { + "rooms": [ + { + "id": f"room_{i}", + "name": room.name, + "type": room.type, + "polygon": [{"x": p.x, "y": p.y} for p in room.polygon], + "lengthMm": room.length_mm, + "widthMm": room.width_mm, + "areaSqMm": room.area_sq_mm, + } + for i, room in enumerate(result.rooms) + ], + "width": result.width, + "height": result.height, + "scale": result.scale, + "source_type": "image", + } + + +def _floor_plan_to_output(floor_plan) -> dict: + """Convert FloorPlanData to job output format.""" + rooms = [] + for i, room in enumerate(floor_plan.rooms): + rooms.append({ + "id": room.id, + "name": room.name, + "type": room.room_type, + "polygon": [{"x": v.x, "y": v.y} for v in room.vertices], + "lengthMm": None, + "widthMm": None, + "areaSqMm": room.area_sqmm, + }) + + walls = [] + for wall in floor_plan.walls: + walls.append({ + "id": wall.id, + "start": {"x": wall.start.x, "y": wall.start.y}, + "end": {"x": wall.end.x, "y": wall.end.y}, + "thickness_mm": wall.thickness_mm, + "wall_type": wall.wall_type.value if hasattr(wall.wall_type, "value") else str(wall.wall_type), + "height_mm": wall.height_mm, + "length_mm": wall.length_mm, + }) + + openings = [] + for opening in floor_plan.openings: + openings.append({ + "id": opening.id, + "type": opening.type.value if hasattr(opening.type, "value") else str(opening.type), + "wall_id": opening.wall_id, + "width_mm": opening.width_mm, + "height_mm": opening.height_mm, + }) + + return { + "rooms": rooms, + "walls": walls, + "openings": openings, + "dimensions": [ + { + "start": {"x": d.start.x, "y": d.start.y}, + "end": {"x": d.end.x, "y": d.end.y}, + "value_mm": d.value_mm, + } + for d in floor_plan.dimensions + ], + "source_type": floor_plan.source_type, + "wall_count": floor_plan.wall_count, + "room_count": floor_plan.room_count, + "opening_count": floor_plan.opening_count, + "total_area_sqm": floor_plan.total_area_sqm, + } +``` + +### 6.3 Update `services/vision-engine/pyproject.toml` + +Add floor-plan-digitizer as a dependency: + +```toml +dependencies = [ + "openlintel-shared", + "fastapi>=0.115.0", + "uvicorn[standard]>=0.32.0", + "litellm>=1.50.0", + # NEW: Floor plan digitizer for DWG/DXF/PDF processing + "openlintel-floor-plan-digitizer", +] +``` + +--- + +## 7. Step 6: Enhanced DXF Parser + +### 7.1 Improve `pipeline.py:_parse_dxf()` for Richer Extraction + +The current parser (lines 260-339) extracts walls and rooms, but misses doors, windows, blocks, and dimension entities. Modify the `_parse_dxf` method in `ml/floor-plan-digitizer/src/openlintel_digitizer/pipeline.py`: + +**Replace the `_parse_dxf` method (lines 259-339) with:** + +```python +@staticmethod +def _parse_dxf(dxf_path: Path) -> FloorPlanData: + """Parse a DXF file into ``FloorPlanData``. + + Enhanced parser that extracts: + - Walls from LINE entities on wall-related layers + - Rooms from LWPOLYLINE entities on room-related layers + - Doors from INSERT entities (block references) with door-related names + - Windows from INSERT entities with window-related names + - Dimensions from DIMENSION entities + - Text/labels from TEXT/MTEXT entities for room identification + """ + from openlintel_digitizer.schemas import ( + DimensionAnnotation, + DoorWindow, + DoorWindowType, + Point2D, + RoomPolygon, + WallSegment, + WallType, + ) + + doc = ezdxf.readfile(str(dxf_path)) + msp = doc.modelspace() + + walls: list[WallSegment] = [] + rooms: list[RoomPolygon] = [] + openings: list[DoorWindow] = [] + dimensions: list[DimensionAnnotation] = [] + wall_idx = 0 + room_idx = 0 + opening_idx = 0 + + # Layer name patterns + wall_patterns = {"wall", "walls", "a-wall", "s-wall", "partition"} + room_patterns = {"room", "rooms", "space", "spaces", "area", "a-area"} + door_patterns = {"door", "doors", "a-door", "opening"} + window_patterns = {"window", "windows", "a-glaz", "glazing", "glass"} + + def _layer_matches(layer_name: str, patterns: set[str]) -> bool: + lower = layer_name.lower() + return any(p in lower for p in patterns) + + # ── Extract walls from LINE entities ────────────────────────────── + for entity in msp.query("LINE"): + layer = entity.dxf.layer + if _layer_matches(layer, wall_patterns) or layer == "0": + start = entity.dxf.start + end = entity.dxf.end + wall_type = ( + WallType.EXTERIOR + if "exterior" in layer.lower() or "ext" in layer.lower() + else WallType.INTERIOR_PARTITION + ) + walls.append(WallSegment( + id=f"W{wall_idx}", + start=Point2D(x=float(start.x), y=float(start.y)), + end=Point2D(x=float(end.x), y=float(end.y)), + wall_type=wall_type, + )) + wall_idx += 1 + + # ── Extract rooms from LWPOLYLINE entities ──────────────────────── + for entity in msp.query("LWPOLYLINE"): + layer = entity.dxf.layer + if _layer_matches(layer, room_patterns): + points = list(entity.get_points(format="xy")) + if len(points) >= 3: + vertices = [Point2D(x=float(p[0]), y=float(p[1])) for p in points] + rooms.append(RoomPolygon( + id=f"R{room_idx}", + name=f"Room {room_idx}", + vertices=vertices, + )) + room_idx += 1 + + # ── Extract doors/windows from INSERT entities (block references) ─ + for entity in msp.query("INSERT"): + block_name = entity.dxf.name.lower() + layer = entity.dxf.layer.lower() + insert_point = entity.dxf.insert + + is_door = ( + _layer_matches(layer, door_patterns) + or any(d in block_name for d in ("door", "dr", "entrance")) + ) + is_window = ( + _layer_matches(layer, window_patterns) + or any(w in block_name for w in ("window", "wndw", "glazing")) + ) + + if is_door or is_window: + opening_type = DoorWindowType.SINGLE_DOOR if is_door else DoorWindowType.SINGLE_WINDOW + + # Try to get width from block definition + width_mm = 900.0 if is_door else 1200.0 + try: + block = doc.blocks.get(entity.dxf.name) + if block: + # Estimate width from block extents + from ezdxf.bbox import extents + bbox = extents(block) + if bbox.has_data: + width_mm = abs(bbox.extmax.x - bbox.extmin.x) + if width_mm < 100: # Probably in a different unit + width_mm = width_mm * entity.dxf.xscale if hasattr(entity.dxf, 'xscale') else 900.0 + except Exception: + pass + + openings.append(DoorWindow( + id=f"{'D' if is_door else 'W'}{opening_idx}", + type=opening_type, + wall_id="", + position_along_wall_mm=0.0, + width_mm=width_mm, + height_mm=2100.0 if is_door else 1200.0, + sill_height_mm=0.0 if is_door else 900.0, + )) + opening_idx += 1 + + # ── Extract dimensions from DIMENSION entities ──────────────────── + for entity in msp.query("DIMENSION"): + try: + # Get dimension measurement points + if hasattr(entity.dxf, "defpoint") and hasattr(entity.dxf, "defpoint2"): + start = entity.dxf.defpoint + end = entity.dxf.defpoint2 + # Calculate distance + dx = end.x - start.x + dy = end.y - start.y + value_mm = (dx**2 + dy**2) ** 0.5 + + if value_mm > 0: + dimensions.append(DimensionAnnotation( + start=Point2D(x=float(start.x), y=float(start.y)), + end=Point2D(x=float(end.x), y=float(end.y)), + value_mm=value_mm, + label=getattr(entity.dxf, "text", ""), + )) + except Exception: + pass + + # ── Fallback: if no wall layers found, treat all lines as walls ─── + if not walls: + for entity in msp.query("LINE"): + start = entity.dxf.start + end = entity.dxf.end + walls.append(WallSegment( + id=f"W{wall_idx}", + start=Point2D(x=float(start.x), y=float(start.y)), + end=Point2D(x=float(end.x), y=float(end.y)), + )) + wall_idx += 1 + + # ── Enrich room names from TEXT/MTEXT near room centroids ───────── + texts: list[tuple[float, float, str]] = [] + for entity in msp.query("TEXT MTEXT"): + try: + insert = entity.dxf.insert + text_val = entity.dxf.text if hasattr(entity.dxf, "text") else "" + if not text_val and hasattr(entity, "text"): + text_val = entity.text + if text_val and len(text_val.strip()) > 1: + texts.append((float(insert.x), float(insert.y), text_val.strip())) + except Exception: + pass + + # Match text labels to nearest room centroid + for room in rooms: + centroid = room.centroid + best_dist = float("inf") + best_label = None + for tx, ty, label in texts: + dist = ((centroid.x - tx)**2 + (centroid.y - ty)**2) ** 0.5 + if dist < best_dist: + best_dist = dist + best_label = label + if best_label and best_dist < 5000: # Within 5 metres + room.name = best_label + + logger.info( + "Parsed DXF: %d walls, %d rooms, %d openings, %d dimensions from %s", + len(walls), + len(rooms), + len(openings), + len(dimensions), + dxf_path, + ) + + return FloorPlanData( + walls=walls, + rooms=rooms, + openings=openings, + dimensions=dimensions, + ) +``` + +--- + +## 8. Step 7: Structural BOM Generator + +### 8.1 Create `services/bom-engine/src/services/structural_bom.py` + +This is the **Floor Plan → BOM bridge** — the most architecturally significant new module. It converts geometric data (walls, rooms, openings) into material quantities. + +```python +""" +Structural BOM generator — converts FloorPlanData geometry into a +construction Bill of Materials. + +Unlike the existing BOM agent (which operates on design variant spec_json), +this module calculates materials directly from structural geometry: +- Wall quantities → bricks, cement, sand, plaster, paint +- Room areas → flooring, false ceiling +- Openings → door/window frames, hardware, glass +- Perimeters → skirting, electrical conduit, wiring + +Uses the existing material database (material_db.py) for pricing and +waste factors, and the existing calculator (calculator.py) for quantity +refinement. +""" + +from __future__ import annotations + +import math +import uuid +from datetime import datetime, timezone +from typing import Any + +import structlog + +from src.agents.material_db import get_price_for_tier, get_waste_factor +from src.models.bom import ( + BOMCategorySummary, + BOMResult, + BOMStatus, + BOMSummary, +) + +from openlintel_shared.schemas.bom import BOMItem, MaterialCategory +from openlintel_shared.schemas.design import BudgetTier + +logger = structlog.get_logger(__name__) + +# Conversion constants +MM_TO_FT = 1 / 304.8 +MM2_TO_SQFT = 1 / (304.8 * 304.8) + + +class StructuralBOMGenerator: + """Generate a construction BOM from floor plan geometry. + + Parameters + ---------- + budget_tier: + Pricing tier for material cost lookup. + currency: + ISO 4217 currency code. + wall_height_mm: + Default wall height if not specified in geometry. + """ + + def __init__( + self, + *, + budget_tier: str = "mid_range", + currency: str = "INR", + wall_height_mm: float = 2700.0, + ) -> None: + self._budget_tier = BudgetTier(budget_tier) + self._currency = currency + self._wall_height_mm = wall_height_mm + + def generate( + self, + *, + floor_plan_data: dict[str, Any], + project_id: str = "", + room_id: str = "", + bom_id: str | None = None, + ) -> BOMResult: + """Generate a structural BOM from floor plan geometry. + + Parameters + ---------- + floor_plan_data: + Dict with ``walls``, ``rooms``, ``openings``, ``dimensions`` keys. + This is the output_json from a floor plan digitization job. + project_id: + Project ID to tag the BOM result. + room_id: + Room ID (or "all" for whole-floor BOM). + bom_id: + Explicit BOM ID. Auto-generated if not provided. + + Returns + ------- + BOMResult + Complete BOM with items, summary, and pricing. + """ + if bom_id is None: + bom_id = str(uuid.uuid4()) + + now = datetime.now(tz=timezone.utc) + items: list[BOMItem] = [] + + walls = floor_plan_data.get("walls", []) + rooms = floor_plan_data.get("rooms", []) + openings = floor_plan_data.get("openings", []) + + logger.info( + "structural_bom_start", + bom_id=bom_id, + walls=len(walls), + rooms=len(rooms), + openings=len(openings), + ) + + # ── Wall-derived materials ──────────────────────────────────── + total_wall_area_sqft = 0.0 + total_wall_length_rft = 0.0 + + for wall in walls: + wall_height = wall.get("height_mm", self._wall_height_mm) + wall_length = wall.get("length_mm", 0) + + if wall_length <= 0: + # Calculate from start/end + start = wall.get("start", {}) + end = wall.get("end", {}) + dx = end.get("x", 0) - start.get("x", 0) + dy = end.get("y", 0) - start.get("y", 0) + wall_length = (dx**2 + dy**2) ** 0.5 + + wall_area_sqft = (wall_length * wall_height) * MM2_TO_SQFT + wall_length_rft = wall_length * MM_TO_FT + + total_wall_area_sqft += wall_area_sqft + total_wall_length_rft += wall_length_rft + + if total_wall_area_sqft > 0: + items.extend(self._wall_materials( + total_wall_area_sqft, + total_wall_length_rft, + room_id, + )) + + # ── Room-derived materials (flooring, ceiling, electrical) ──── + total_floor_area_sqft = 0.0 + + for room in rooms: + room_area_sqmm = room.get("areaSqMm", 0) + room_type = room.get("type", "other") + r_id = room.get("id", room_id) + + if room_area_sqmm <= 0: + # Estimate from polygon vertices + vertices = room.get("polygon", []) + if len(vertices) >= 3: + room_area_sqmm = self._shoelace_area(vertices) + + room_area_sqft = room_area_sqmm * MM2_TO_SQFT + total_floor_area_sqft += room_area_sqft + + items.extend(self._room_materials( + room_area_sqft, + room_type, + r_id, + )) + + # ── Opening-derived materials (doors, windows, hardware) ────── + for opening in openings: + items.extend(self._opening_materials(opening, room_id)) + + # ── Electrical rough-in (based on total floor area) ─────────── + if total_floor_area_sqft > 0: + items.extend(self._electrical_materials( + total_floor_area_sqft, + total_wall_length_rft, + room_id, + )) + + # ── Build summary ───────────────────────────────────────────── + total_cost = sum(item.estimated_cost or 0.0 for item in items) + category_breakdown = self._build_category_breakdown(items, total_cost) + + summary = BOMSummary( + total_items=len(items), + total_cost=round(total_cost, 2), + currency=self._currency, + category_breakdown=category_breakdown, + ) + + logger.info( + "structural_bom_complete", + bom_id=bom_id, + items=len(items), + total_cost=total_cost, + ) + + return BOMResult( + id=bom_id, + project_id=project_id, + room_id=room_id or "all", + design_variant_id="structural", + status=BOMStatus.COMPLETE, + items=items, + summary=summary, + created_at=now, + completed_at=now, + ) + + # ── Material generators ─────────────────────────────────────────── + + def _wall_materials( + self, + wall_area_sqft: float, + wall_length_rft: float, + room_id: str, + ) -> list[BOMItem]: + """Generate wall-related BOM items.""" + items: list[BOMItem] = [] + + # Painting (wall putty + primer + emulsion) + for key, name, spec in [ + ("wall_putty", "Wall Putty", "2 coats birla/JK wall putty"), + ("wall_primer", "Wall Primer", "1 coat interior primer"), + ("interior_emulsion", "Interior Emulsion Paint", "2 coats premium emulsion"), + ]: + unit_price = get_price_for_tier(key, self._budget_tier) + waste = get_waste_factor(key) + items.append(BOMItem( + id=str(uuid.uuid4()), + roomId=room_id, + category=MaterialCategory.PAINTING, + name=name, + specification=spec, + quantity=round(wall_area_sqft, 1), + unit="sqft", + unitPrice=unit_price, + currency=self._currency, + wasteFactor=waste, + )) + + return items + + def _room_materials( + self, + floor_area_sqft: float, + room_type: str, + room_id: str, + ) -> list[BOMItem]: + """Generate room-specific BOM items (flooring, ceiling).""" + items: list[BOMItem] = [] + + if floor_area_sqft <= 0: + return items + + # Flooring + tile_key = "vitrified_tiles_600x600" + unit_price = get_price_for_tier(tile_key, self._budget_tier) + waste = get_waste_factor(tile_key, "straight") + items.append(BOMItem( + id=str(uuid.uuid4()), + roomId=room_id, + category=MaterialCategory.FLOORING, + name="Vitrified Tiles 600x600mm", + specification="Glossy vitrified floor tile, straight lay", + quantity=round(floor_area_sqft, 1), + unit="sqft", + unitPrice=unit_price, + currency=self._currency, + wasteFactor=waste, + )) + + # False ceiling for living/bedroom/dining + if room_type in ("living_room", "bedroom", "dining", "study"): + ceiling_area = floor_area_sqft * 0.6 + fc_price = get_price_for_tier("gypsum_board_12mm", self._budget_tier) + fc_waste = get_waste_factor("gypsum_board_12mm") + items.append(BOMItem( + id=str(uuid.uuid4()), + roomId=room_id, + category=MaterialCategory.FALSE_CEILING, + name="Gypsum Board False Ceiling", + specification="12.5mm gypsum board with GI framework", + quantity=round(ceiling_area, 1), + unit="sqft", + unitPrice=fc_price, + currency=self._currency, + wasteFactor=fc_waste, + )) + + # Plumbing for wet areas + if room_type in ("bathroom", "kitchen", "utility"): + perimeter_rft = math.sqrt(floor_area_sqft) * 4 * 0.3048 # approximate + pipe_price = get_price_for_tier("cpvc_pipe_15mm", self._budget_tier) + items.append(BOMItem( + id=str(uuid.uuid4()), + roomId=room_id, + category=MaterialCategory.PLUMBING, + name="CPVC Pipe 15mm", + specification="Hot and cold water supply", + quantity=round(perimeter_rft * 1.5, 1), + unit="rft", + unitPrice=pipe_price, + currency=self._currency, + wasteFactor=0.05, + )) + + return items + + def _opening_materials( + self, + opening: dict[str, Any], + room_id: str, + ) -> list[BOMItem]: + """Generate door/window BOM items.""" + items: list[BOMItem] = [] + opening_type = opening.get("type", "single_door") + is_door = "door" in opening_type.lower() + width_mm = opening.get("width_mm", 900 if is_door else 1200) + + if is_door: + items.append(BOMItem( + id=str(uuid.uuid4()), + roomId=room_id, + category=MaterialCategory.CARPENTRY, + name="Flush Door with Frame", + specification=f"{int(width_mm)}mm flush door, sal wood frame", + quantity=1, + unit="nos", + unitPrice=get_price_for_tier("flush_door_frame", self._budget_tier) or 8500.0, + currency=self._currency, + wasteFactor=0.0, + )) + items.append(BOMItem( + id=str(uuid.uuid4()), + roomId=room_id, + category=MaterialCategory.HARDWARE, + name="Door Hardware Set", + specification="Mortice lock + 3 hinges + tower bolt + door stopper", + quantity=1, + unit="set", + unitPrice=get_price_for_tier("door_hardware_set", self._budget_tier) or 2500.0, + currency=self._currency, + wasteFactor=0.0, + )) + else: + items.append(BOMItem( + id=str(uuid.uuid4()), + roomId=room_id, + category=MaterialCategory.GLASS_ALUMINUM, + name="Aluminium Sliding Window", + specification=f"{int(width_mm)}mm aluminium section with 5mm glass", + quantity=1, + unit="nos", + unitPrice=get_price_for_tier("aluminium_window", self._budget_tier) or 650.0, + currency=self._currency, + wasteFactor=0.0, + )) + + return items + + def _electrical_materials( + self, + floor_area_sqft: float, + perimeter_rft: float, + room_id: str, + ) -> list[BOMItem]: + """Generate electrical rough-in materials.""" + items: list[BOMItem] = [] + + # Wiring + for key, name, spec, multiplier in [ + ("copper_wire_1_5mm", "Copper Wire 1.5mm", "FR grade, lighting circuits", 3), + ("copper_wire_2_5mm", "Copper Wire 2.5mm", "FR grade, power sockets", 2), + ]: + items.append(BOMItem( + id=str(uuid.uuid4()), + roomId=room_id, + category=MaterialCategory.ELECTRICAL, + name=name, + specification=spec, + quantity=round(perimeter_rft * multiplier, 1), + unit="rft", + unitPrice=get_price_for_tier(key, self._budget_tier), + currency=self._currency, + wasteFactor=get_waste_factor(key), + )) + + # Switches & lights (proportional to floor area) + switch_count = max(2, round(floor_area_sqft / 50)) + light_count = max(2, round(floor_area_sqft / 30)) + + items.append(BOMItem( + id=str(uuid.uuid4()), + roomId=room_id, + category=MaterialCategory.ELECTRICAL, + name="Modular Switch Plate", + specification="6-module switch plate", + quantity=switch_count, + unit="nos", + unitPrice=get_price_for_tier("modular_switch_plate", self._budget_tier), + currency=self._currency, + wasteFactor=0.0, + )) + + items.append(BOMItem( + id=str(uuid.uuid4()), + roomId=room_id, + category=MaterialCategory.ELECTRICAL, + name="LED Downlight 12W", + specification="Recessed LED downlight, 4000K neutral white", + quantity=light_count, + unit="nos", + unitPrice=get_price_for_tier("led_downlight", self._budget_tier), + currency=self._currency, + wasteFactor=0.0, + )) + + return items + + # ── Helpers ──────────────────────────────────────────────────────── + + @staticmethod + def _shoelace_area(vertices: list[dict[str, float]]) -> float: + """Compute polygon area using the Shoelace formula (sq mm).""" + n = len(vertices) + if n < 3: + return 0.0 + area = 0.0 + for i in range(n): + j = (i + 1) % n + area += vertices[i]["x"] * vertices[j]["y"] + area -= vertices[j]["x"] * vertices[i]["y"] + return abs(area) / 2.0 + + @staticmethod + def _build_category_breakdown( + items: list[BOMItem], + total_cost: float, + ) -> list[BOMCategorySummary]: + """Build per-category cost breakdown.""" + cat_totals: dict[MaterialCategory, tuple[int, float]] = {} + for item in items: + count, subtotal = cat_totals.get(item.category, (0, 0.0)) + cost = item.estimated_cost or 0.0 + cat_totals[item.category] = (count + 1, subtotal + cost) + + breakdown: list[BOMCategorySummary] = [] + for cat, (count, subtotal) in sorted(cat_totals.items(), key=lambda x: -x[1][1]): + pct = (subtotal / total_cost * 100) if total_cost > 0 else 0.0 + breakdown.append(BOMCategorySummary( + category=cat, + item_count=count, + subtotal=round(subtotal, 2), + percentage_of_total=round(pct, 1), + )) + return breakdown +``` + +--- + +## 9. Step 8: BOM Engine — Floor Plan Endpoint + +### 9.1 Add Endpoint to `services/bom-engine/src/routers/bom.py` + +Add this endpoint after the existing `/job` endpoint (after line 224): + +```python +from src.services.structural_bom import StructuralBOMGenerator + + +class FloorPlanBOMRequest(BaseModel): + """Request body for POST /api/v1/bom/from-floor-plan.""" + job_id: str = Field(description="Job ID for status tracking") + project_id: str = Field(description="Project ID") + floor_plan_data: dict[str, Any] = Field( + description="FloorPlanData output from digitization job" + ) + budget_tier: str = Field(default="mid_range") + currency: str = Field(default="INR") + + +@router.post( + "/from-floor-plan", + response_model=BOMGenerateResponse, + status_code=status.HTTP_202_ACCEPTED, + summary="Generate a structural BOM from floor plan geometry", + description=( + "Accepts FloorPlanData (walls, rooms, openings) from a floor plan " + "digitization job and generates a construction BOM with material " + "quantities, pricing, and waste factors. Does NOT require a design " + "variant — works directly from structural geometry." + ), +) +async def generate_bom_from_floor_plan( + request: FloorPlanBOMRequest, + background_tasks: BackgroundTasks, +) -> BOMGenerateResponse: + """Generate a structural BOM from floor plan data.""" + bom_id = str(uuid.uuid4()) + + logger.info( + "structural_bom_request", + bom_id=bom_id, + project_id=request.project_id, + walls=len(request.floor_plan_data.get("walls", [])), + rooms=len(request.floor_plan_data.get("rooms", [])), + ) + + try: + generator = StructuralBOMGenerator( + budget_tier=request.budget_tier, + currency=request.currency, + ) + + result = generator.generate( + floor_plan_data=request.floor_plan_data, + project_id=request.project_id, + bom_id=bom_id, + ) + + # Store result + bom_data = result.model_dump(mode="json") + _bom_store[bom_id] = bom_data + await cache_set(f"bom:{bom_id}", bom_data, ttl=_BOM_CACHE_TTL) + + return BOMGenerateResponse( + bom_id=bom_id, + status=BOMStatus.COMPLETE, + message="Structural BOM generation complete.", + ) + + except Exception as exc: + logger.error("structural_bom_failed", bom_id=bom_id, error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Structural BOM generation failed: {exc}", + ) from exc +``` + +### 9.2 Add `ezdxf` to BOM Engine Dependencies + +Update `services/bom-engine/pyproject.toml`: + +```toml +dependencies = [ + "openlintel-shared", + "fastapi>=0.111", + "uvicorn[standard]>=0.30", + "ortools>=9.9,<10", + "openpyxl>=3.1,<4", + "reportlab>=4.1,<5", + # No new deps needed — structural_bom.py uses existing material_db/calculator +] +``` + +--- + +## 10. Step 9: tRPC Router & Frontend Integration + +### 10.1 Update `apps/web/src/server/trpc/routers/bom.ts` + +Add a `generateFromFloorPlan` mutation after the existing `generate` mutation (after line 104): + +```typescript +generateFromFloorPlan: protectedProcedure + .input( + z.object({ + projectId: z.string(), + floorPlanJobId: z.string(), + budgetTier: z.string().default('mid_range'), + currency: z.string().default('INR'), + }), + ) + .mutation(async ({ ctx, input }) => { + // Verify project ownership + const project = await ctx.db.query.projects.findFirst({ + where: and(eq(projects.id, input.projectId), eq(projects.userId, ctx.userId)), + }); + if (!project) throw new Error('Project not found'); + + // Get the floor plan digitization job output + const fpJob = await ctx.db.query.jobs.findFirst({ + where: and(eq(jobs.id, input.floorPlanJobId), eq(jobs.userId, ctx.userId)), + }); + if (!fpJob) throw new Error('Floor plan job not found'); + if (fpJob.status !== 'completed') throw new Error('Floor plan digitization not complete'); + + const floorPlanData = fpJob.outputJson as Record; + if (!floorPlanData) throw new Error('No floor plan data in job output'); + + // Create BOM job + const [job] = await ctx.db + .insert(jobs) + .values({ + userId: ctx.userId, + type: 'structural_bom', + status: 'pending', + inputJson: { + projectId: input.projectId, + floorPlanJobId: input.floorPlanJobId, + budgetTier: input.budgetTier, + }, + projectId: input.projectId, + }) + .returning(); + + // Call BOM service + fetch(`${BOM_SERVICE_URL}/api/v1/bom/from-floor-plan`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + job_id: job.id, + project_id: input.projectId, + floor_plan_data: floorPlanData, + budget_tier: input.budgetTier, + currency: input.currency, + }), + }).catch(() => { + // Service may be down; job stays pending + }); + + return job; + }), +``` + +### 10.2 Update `apps/web/src/server/trpc/routers/floorPlan.ts` + +Update the `digitize` mutation to pass filename and MIME type to the new `/digitize` endpoint. Replace the fire-and-forget block (lines 49-62): + +```typescript +// Fire-and-forget to vision-engine — enhanced endpoint with file-type routing +fetch(`${VISION_SERVICE_URL}/api/v1/vision/digitize`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + job_id: job.id, + user_id: ctx.userId, + project_id: input.projectId, + upload_id: input.uploadId, + storage_key: upload.storageKey, + filename: upload.filename, + mime_type: upload.mimeType, + }), +}).catch(() => { + // Fallback: try the legacy image-only endpoint + const fullImageUrl = `${origin}${imageUrl}`; + fetch(`${VISION_SERVICE_URL}/api/v1/vision/job`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + job_id: job.id, + user_id: ctx.userId, + project_id: input.projectId, + image_url: fullImageUrl, + upload_id: input.uploadId, + }), + }).catch(() => {}); +}); +``` + +### 10.3 Update BOM Page UI + +In `apps/web/src/app/(dashboard)/project/[id]/bom/page.tsx`, add a "Generate from Floor Plan" section. Add alongside the existing variant-based generation UI: + +```tsx +{/* Structural BOM from Floor Plan */} +{completedFloorPlanJobs.length > 0 && ( + + + Generate BOM from Floor Plan + + Create a structural BOM directly from your digitized floor plan + — no design variant needed. + + + + + + + +)} +``` + +--- + +## 11. Step 10: Database Schema Changes + +### 11.1 No New Tables Strictly Needed + +The existing `bom_results` table works for structural BOMs because: +- `design_variant_id` can be set to `"structural"` as a sentinel value +- `items` (JSONB) stores BOM items identically +- `total_cost`, `currency`, `metadata` all apply + +### 11.2 Optional: Add `floor_plan_results` Table + +If you want to persist parsed `FloorPlanData` separately (recommended for re-processing): + +Add to `packages/db/src/schema/app.ts`: + +```typescript +export const floorPlanResults = pgTable('floor_plan_results', { + id: text('id').primaryKey().$defaultFn(() => createId()), + projectId: text('project_id').notNull().references(() => projects.id), + uploadId: text('upload_id').references(() => uploads.id), + jobId: text('job_id').references(() => jobs.id), + sourceType: text('source_type').notNull(), // 'dwg', 'dxf', 'pdf', 'image' + walls: jsonb('walls').notNull().default([]), + rooms: jsonb('rooms').notNull().default([]), + openings: jsonb('openings').notNull().default([]), + dimensions: jsonb('dimensions').notNull().default([]), + totalAreaSqm: real('total_area_sqm'), + metadata: jsonb('metadata'), + createdAt: timestamp('created_at').defaultNow().notNull(), +}); +``` + +--- + +## 12. Step 11: Docker & Infrastructure + +### 12.1 Vision Engine Dockerfile + +Add system dependencies for DWG conversion and PDF processing. Update `services/vision-engine/Dockerfile`: + +```dockerfile +FROM python:3.12-slim + +# System deps for DWG conversion and PDF rasterization +RUN apt-get update && apt-get install -y --no-install-recommends \ + libredwg-utils \ + poppler-utils \ + libgl1-mesa-glx \ + libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install Python deps +COPY packages/python-shared /packages/python-shared +RUN pip install --no-cache-dir /packages/python-shared + +COPY ml/floor-plan-digitizer /ml/floor-plan-digitizer +RUN pip install --no-cache-dir /ml/floor-plan-digitizer + +COPY services/vision-engine/pyproject.toml services/vision-engine/setup.cfg* ./ +RUN pip install --no-cache-dir . + +COPY services/vision-engine/ . + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8010"] +``` + +### 12.2 docker-compose.yml + +Add the system packages to the vision-engine service build context. In the `vision-engine` service section, ensure it has access to the floor-plan-digitizer: + +```yaml +vision-engine: + build: + context: . + dockerfile: services/vision-engine/Dockerfile + ports: + - "8010:8010" + environment: + DATABASE_URL: postgresql+asyncpg://openlintel:openlintel_dev@postgres:5432/openlintel + REDIS_URL: redis://redis:6379 + MINIO_ENDPOINT: http://minio:9000 + MINIO_ACCESS_KEY: minioadmin + MINIO_SECRET_KEY: minioadmin123 + MINIO_BUCKET: openlintel + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + minio: + condition: service_started +``` + +### 12.3 Verify LibreDWG Availability + +After building the Docker image, verify: + +```bash +docker compose exec vision-engine dwg2dxf --version +``` + +Expected output: `dwg2dxf ` + +If LibreDWG is not available in the distro's package manager, use the ODA File Converter instead: +1. Download the `.deb` from ODA's website +2. Add to the Dockerfile: `COPY oda_file_converter.deb /tmp/ && dpkg -i /tmp/oda_file_converter.deb` + +--- + +## 13. Step 12: Testing Strategy + +### 13.1 Unit Tests + +``` +tests/ +├── test_dwg_converter.py — DWGConverter with mock subprocess +├── test_pdf_extractor.py — PDFFloorPlanExtractor with sample PDFs +├── test_file_router.py — FileType detection from MIME/ext/magic +├── test_structural_bom.py — StructuralBOMGenerator with sample geometry +├── test_enhanced_dxf_parser.py — _parse_dxf with sample DXF files +└── test_upload_validation.py — MIME type / extension validation +``` + +### 13.2 Integration Tests + +```python +# test_dwg_to_bom_e2e.py +async def test_dwg_upload_to_bom(): + """End-to-end: upload DWG → digitize → generate BOM.""" + # 1. Upload a sample DWG file + with open("fixtures/sample_floor_plan.dwg", "rb") as f: + response = await client.post("/api/upload", files={"file": f}) + assert response.status_code == 200 + upload_id = response.json()["id"] + + # 2. Trigger digitization + job = await trpc.floorPlan.digitize({ + "projectId": test_project_id, + "uploadId": upload_id, + }) + + # 3. Poll until complete + while True: + status = await trpc.floorPlan.jobStatus({"jobId": job["id"]}) + if status["status"] in ("completed", "failed"): + break + await asyncio.sleep(1) + + assert status["status"] == "completed" + + # 4. Generate structural BOM + bom_job = await trpc.bom.generateFromFloorPlan({ + "projectId": test_project_id, + "floorPlanJobId": job["id"], + }) + + # 5. Verify BOM result + bom = await trpc.bom.jobStatus({"jobId": bom_job["id"]}) + assert bom["status"] == "completed" + assert len(bom["outputJson"]["items"]) > 0 +``` + +### 13.3 Sample Test Files + +Create `tests/fixtures/` with: +- `sample_floor_plan.dwg` — simple 2BHK DWG file +- `sample_floor_plan.dxf` — same plan as DXF +- `sample_floor_plan.pdf` — PDF export of the same plan +- `sample_floor_plan.png` — raster image of the same plan + +These enable comparison testing across all four input formats. + +--- + +## 14. Dependency Map & Build Order + +``` + ┌─────────────────────────────┐ + │ Step 1: Upload API (TS) │ + │ Step 2: Media Service (PY) │ + └──────────┬──────────────────┘ + │ files can now reach server + ┌──────────▼──────────────────┐ + │ Step 11: Docker/Infra │ + │ (LibreDWG, poppler-utils) │ + └──────────┬──────────────────┘ + │ system tools available + ┌────────────────┼────────────────┐ + │ │ │ + ┌─────────▼────┐ ┌───────▼──────┐ ┌──────▼──────┐ + │ Step 3: DWG │ │ Step 4: PDF │ │ Step 6: DXF │ + │ Converter │ │ Extractor │ │ Parser++ │ + └─────────┬────┘ └───────┬──────┘ └──────┬──────┘ + │ │ │ + └───────┬───────┘ │ + │ │ + ┌─────────▼────────────────────────▼──┐ + │ Step 5: Vision Engine File Router │ + └─────────────────┬────────────────────┘ + │ FloorPlanData output + ┌─────────▼────────────────┐ + │ Step 7: Structural BOM │ + │ Generator │ + └─────────┬────────────────┘ + │ + ┌─────────▼────────────────┐ + │ Step 8: BOM Engine │ + │ /from-floor-plan endpoint │ + └─────────┬────────────────┘ + │ + ┌─────────▼────────────────┐ + │ Step 9: tRPC + Frontend │ + └─────────┬────────────────┘ + │ + ┌─────────▼────────────────┐ + │ Step 10: DB Schema │ + │ (optional table) │ + └─────────┬────────────────┘ + │ + ┌─────────▼────────────────┐ + │ Step 12: Tests │ + └──────────────────────────┘ +``` + +**Parallelizable steps:** +- Steps 3, 4, 6 can be developed in parallel (independent modules) +- Steps 1, 2, 11 can be done in parallel (infrastructure layer) +- Steps 7, 8 depend on earlier steps but can be stubbed + +--- + +## 15. File Manifest + +### New Files (8 files) + +| File | Lines (est.) | Purpose | +|------|-------------|---------| +| `ml/floor-plan-digitizer/src/openlintel_digitizer/dwg_converter.py` | ~280 | DWG→DXF conversion | +| `ml/floor-plan-digitizer/src/openlintel_digitizer/pdf_extractor.py` | ~200 | PDF→Image extraction | +| `services/vision-engine/src/services/file_router.py` | ~130 | File-type detection/routing | +| `services/bom-engine/src/services/structural_bom.py` | ~350 | Geometry→BOM calculation | +| `tests/test_dwg_converter.py` | ~100 | DWG converter tests | +| `tests/test_pdf_extractor.py` | ~80 | PDF extractor tests | +| `tests/test_file_router.py` | ~60 | File router tests | +| `tests/test_structural_bom.py` | ~120 | Structural BOM tests | + +### Modified Files (15 files) + +| File | Change Description | +|------|-------------------| +| `apps/web/src/app/api/upload/route.ts` | DWG/DXF MIME types, 50MB limit, extension validation | +| `services/media-service/src/services/validator.py` | CAD MIME types, extension override | +| `services/media-service/src/routers/upload.py` | DWG/DXF extension mapping, metadata | +| `services/vision-engine/src/routers/vision.py` | New `/digitize` endpoint, file-aware processing | +| `services/vision-engine/main.py` | No change needed (router already included) | +| `services/vision-engine/pyproject.toml` | Add floor-plan-digitizer dependency | +| `ml/floor-plan-digitizer/src/openlintel_digitizer/pipeline.py` | Add `digitize_pdf()`, enhanced `_parse_dxf()` | +| `ml/floor-plan-digitizer/src/openlintel_digitizer/__init__.py` | Export new classes | +| `ml/floor-plan-digitizer/pyproject.toml` | Add pdf2image, pdfplumber | +| `services/bom-engine/src/routers/bom.py` | Add `/from-floor-plan` endpoint | +| `apps/web/src/server/trpc/routers/bom.ts` | Add `generateFromFloorPlan` mutation | +| `apps/web/src/server/trpc/routers/floorPlan.ts` | Pass filename/MIME to vision engine | +| `apps/web/src/app/(dashboard)/project/[id]/bom/page.tsx` | "BOM from Floor Plan" UI | +| `packages/db/src/schema/app.ts` | Optional `floor_plan_results` table | +| `docker-compose.yml` / Dockerfiles | LibreDWG, poppler-utils system packages | + +--- + +## End-to-End User Flow (Target State) + +``` +User uploads floor_plan.dwg (or .dxf or .pdf) + │ + ▼ +Upload API accepts file (Step 1) ── stores in MinIO + │ + ▼ +Vision Engine receives job (Step 5) + │ + ├── .dwg → DWGConverter (Step 3) → .dxf → ezdxf parser (Step 6) → FloorPlanData + ├── .dxf → ezdxf parser (Step 6) → FloorPlanData + ├── .pdf → PDFExtractor (Step 4) → image → VLM (GPT-4o) → FloorPlanData + └── image → VLM (GPT-4o) → FloorPlanData + │ + ▼ +User sees detected rooms, walls, dimensions on floor plan viewer + │ + ▼ +User clicks "Generate Structural BOM" (Step 9) + │ + ▼ +StructuralBOMGenerator (Step 7) converts geometry → materials + │ + ├── Walls → paint, putty, primer (by wall area) + ├── Rooms → tiles, false ceiling (by floor area) + ├── Openings → doors, windows, hardware (by count) + └── Electrical → wiring, switches, lights (by room size) + │ + ▼ +BOM displayed with category breakdown, total cost + │ + ▼ +User exports as Excel / CSV / PDF (existing export system) +``` diff --git a/codeauditdwg.md b/codeauditdwg.md new file mode 100644 index 0000000..cac43fe --- /dev/null +++ b/codeauditdwg.md @@ -0,0 +1,905 @@ +# Code Audit: DWG/PDF Upload and BOM Calculation Pipeline + +**Audit Date:** 2026-03-09 +**Auditor:** Claude Opus 4.6 +**Scope:** End-to-end analysis of DWG/PDF file upload, parsing, and BOM (Bill of Materials) generation capabilities + +--- + +## Executive Summary + +**Can you currently upload a DWG or PDF-of-DWG file and calculate BOM?** + +**NO — not end-to-end.** The system has significant architectural scaffolding for this flow, but critical modules are missing or disconnected. Specifically: + +| Capability | Status | Blocker | +|-----------|--------|---------| +| Upload DWG file (frontend) | Partial | UI accepts `.dwg`, backend rejects it | +| Upload PDF of DWG (frontend) | Partial | UI accepts `.pdf`, backend accepts PDF but vision pipeline expects images | +| Parse/read DWG file | **NOT IMPLEMENTED** | `dwg_converter.py` module does not exist | +| Parse PDF to extract floor plan | **NOT IMPLEMENTED** | No PDF-to-image conversion | +| Digitize floor plan from image | Working | VLM-based extraction via GPT-4o works | +| Generate BOM from floor plan | **DISCONNECTED** | BOM engine operates on design variants, not floor plan geometry | +| Generate BOM from design variant | Working | Full LangGraph pipeline with OR-Tools optimization | +| Export BOM (Excel/CSV/PDF) | Working | All three export formats implemented | + +**Estimated effort to make this work end-to-end: 3-5 development sprints.** + +--- + +## 1. Detailed Analysis: What Exists Today + +### 1.1 Frontend — Floor Plan Upload Component + +**File:** `apps/web/src/components/floor-plan-upload.tsx` + +``` +Line 18: const ACCEPTED_EXTENSIONS = '.png,.jpg,.jpeg,.webp,.pdf,.dxf,.dwg'; +Line 50: const validExt = ['png', 'jpg', 'jpeg', 'webp', 'pdf', 'dxf', 'dwg']; +``` + +The frontend UI **does accept DWG files**. It shows DWG/DXF/PDF/PNG/JPG badges, validates by extension, allows 50MB uploads, has drag-and-drop support, and automatically triggers digitization after upload. + +**However**, the file is sent to `/api/upload` which rejects non-image/non-PDF MIME types. + +### 1.2 Backend Upload API — REJECTS DWG + +**File:** `apps/web/src/app/api/upload/route.ts` + +```typescript +// Line 8-15 +const MAX_SIZE = 10 * 1024 * 1024; // 10MB (frontend says 50MB!) +const ALLOWED_TYPES = [ + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/gif', + 'application/pdf', +]; +``` + +**Problems identified:** +1. **DWG MIME type not allowed**: DWG files (`application/acad`, `application/x-acad`, `application/x-autocad`, `image/x-dwg`, `application/dwg`) are not in `ALLOWED_TYPES`. Upload will fail with "Unsupported file type". +2. **DXF MIME type not allowed**: DXF files (`application/dxf`, `image/vnd.dxf`) also not in the allowed list. +3. **Size mismatch**: Backend limits to 10MB; frontend advertises 50MB. DWG files commonly exceed 10MB. +4. **No file content inspection**: The backend trusts the browser's `Content-Type` header entirely, no magic-byte validation. + +### 1.3 Media Service — Also No DWG Support + +**File:** `services/media-service/src/routers/upload.py` + +```python +# Line 59-68 — _mime_to_extension() +mapping = { + "image/jpeg": ".jpg", + "image/png": ".png", + "image/webp": ".webp", + "image/gif": ".gif", + "application/pdf": ".pdf", +} +``` + +The media service also has no DWG/DXF awareness. Non-image files pass through without optimization but are stored as `.bin` extension. + +### 1.4 Floor Plan Digitization Pipeline — DWG Converter is MISSING + +**File:** `ml/floor-plan-digitizer/src/openlintel_digitizer/pipeline.py` + +The `FloorPlanPipeline` class has a `digitize_dwg()` method (line 146-200) that is architecturally sound: + +```python +async def digitize_dwg(self, dwg_path, *, output_dxf_path=None) -> FloorPlanData: + from openlintel_digitizer.dwg_converter import DWGConverter # <-- DOES NOT EXIST + converter = DWGConverter( + libredwg_path=self._libredwg_path, + oda_converter_path=self._oda_converter_path, + ) + if not converter.is_available: + raise RuntimeError("No DWG conversion backend available...") + dxf_path = await converter.convert(dwg_path, output_path=output_dxf_path, dxf_version=self._dxf_version) + floor_plan = self._parse_dxf(dxf_path) + floor_plan.source_type = "dwg" + return floor_plan +``` + +**Critical finding:** The import `from openlintel_digitizer.dwg_converter import DWGConverter` references a module that **DOES NOT EXIST** anywhere in the codebase. Verified via: +- `Glob("**/dwg_converter*")` → No files found +- `Glob("**/dwg*")` → No files found +- No `libredwg` in any `requirements.txt` +- No ODA File Converter references in any dependency file + +The method also calls `self._parse_dxf()` which DOES exist (line 260-339) and successfully parses DXF files using `ezdxf`. So the second half of the pipeline (DXF → FloorPlanData) is implemented. + +### 1.5 DXF Parsing — WORKS + +**File:** `ml/floor-plan-digitizer/src/openlintel_digitizer/pipeline.py` (lines 260-339) + +The `_parse_dxf()` static method is fully implemented: +- Reads DXF files with `ezdxf.readfile()` +- Extracts LINE entities from wall-related layers (`walls`, `wall`, `walls_exterior`, `walls_interior`) +- Extracts LWPOLYLINE entities from room layers (`rooms`, `room`, `spaces`, `space`) +- Falls back to treating all LINEs as walls if no specific wall layers found +- Returns structured `FloorPlanData` with walls and rooms + +### 1.6 DXF Generation — WORKS + +**File:** `ml/floor-plan-digitizer/src/openlintel_digitizer/dxf_generator.py` + +Fully implemented `DXFGenerator` class using `ezdxf`: +- Generates DXF R2013 with proper layer structure (WALLS, WALLS_EXTERIOR, DOORS, WINDOWS, ROOMS, DIMENSIONS, ANNOTATIONS, FURNITURE) +- Draws walls with centre-lines and offset edges +- Draws door swing arcs and window glazing lines +- Generates aligned dimension annotations +- All geometry in millimetres + +### 1.7 Vision Engine — Image-Only, No DWG/PDF Awareness + +**File:** `services/vision-engine/src/agents/vision_agent.py` + +The vision engine sends `image_url` directly to GPT-4o: +```python +{"type": "image_url", "image_url": {"url": image_url}} +``` + +**Problems:** +1. Only works for raster images (JPEG, PNG, WebP) +2. Does not handle PDF files (would need PDF-to-image conversion first) +3. Does not handle DWG files at all +4. No file-type routing — assumes everything is a displayable image + +**File:** `services/vision-engine/src/routers/vision.py` + +The job dispatcher accepts `image_url` (line 37) and passes it directly to `detect_rooms_from_image()`. No format detection, no conversion step. + +### 1.8 BOM Engine — DISCONNECTED from Floor Plans + +**File:** `services/bom-engine/src/agents/bom_agent.py` + +The BOM agent is a 5-node LangGraph pipeline: +1. `extract_materials` — LLM analyses **design spec JSON** (not floor plan geometry) +2. `calculate_quantities` — computes quantities with waste factors +3. `lookup_prices` — resolves unit prices from material database +4. `optimize_budget` — OR-Tools budget allocation +5. `generate_bom` — assembles final BOM + +**Key insight:** The BOM agent's input is a `spec_json` from a **design variant**, not floor plan geometry. The pipeline is: + +``` +Room Photo → Design Engine (VLM) → Design Variant (spec_json) → BOM Engine → BOM +``` + +There is **no path** from: +``` +DWG File → Floor Plan Data → BOM +``` + +The BOM system doesn't know about walls, rooms, doors, or windows at a structural level. It receives a design specification (furniture, finishes, fixtures) and calculates materials from that. + +### 1.9 BOM tRPC Router — Flow Requires Design Variant + +**File:** `apps/web/src/server/trpc/routers/bom.ts` + +```typescript +generate: protectedProcedure + .input(z.object({ designVariantId: z.string() })) + .mutation(async ({ ctx, input }) => { + // Fetches design variant → sends to BOM service + // Room dimensions come from the rooms table (lengthMm, widthMm, heightMm) + }) +``` + +BOM generation REQUIRES a `designVariantId`. There's no endpoint to generate BOM from a floor plan or uploaded DWG directly. + +--- + +## 2. Current User Flow (What Works Today) + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ 1. User uploads room PHOTO (JPEG/PNG) via /api/upload │ +│ ├─ Stored in MinIO/S3 │ +│ └─ Metadata saved to PostgreSQL (uploads table) │ +│ │ +│ 2. User triggers DESIGN GENERATION for a room │ +│ ├─ Design Engine (LangGraph + GPT-4o) generates 3 variants │ +│ ├─ Each variant has a spec_json describing furniture, finishes │ +│ └─ Render images stored in MinIO │ +│ │ +│ 3. User selects a design variant │ +│ │ +│ 4. User clicks "Generate BOM" │ +│ ├─ BOM Agent analyses spec_json via LLM │ +│ ├─ Extracts materials (flooring, paint, electrical, plumbing...) │ +│ ├─ Calculates quantities with waste factors │ +│ ├─ Looks up prices from material database │ +│ ├─ Optionally runs OR-Tools budget optimization │ +│ └─ Returns structured BOM with items, summary, substitutions │ +│ │ +│ 5. User exports BOM as Excel (.xlsx), CSV, or PDF │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +**For floor plan upload (separate flow):** +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ 1. User uploads floor plan IMAGE (PNG/JPG) via floor-plan-upload │ +│ ├─ Stored in MinIO │ +│ └─ Triggers digitization job │ +│ │ +│ 2. Vision Engine sends image to GPT-4o │ +│ ├─ VLM detects rooms (name, type, polygon, dimensions) │ +│ └─ Results stored in job output │ +│ │ +│ 3. Room data populates the rooms table │ +│ ├─ But this data is NOT fed into design generation │ +│ └─ And NOT fed into BOM generation │ +│ │ +│ THE FLOW STOPS HERE — no connection to BOM │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. Has a DWG Reader Been Implemented? + +### Answer: **NO — the DWG reader has NOT been implemented.** + +Here is a detailed breakdown of what exists vs. what is missing: + +### What EXISTS (the scaffolding): + +| Component | File | Status | +|-----------|------|--------| +| `FloorPlanPipeline.digitize_dwg()` method | `ml/floor-plan-digitizer/src/openlintel_digitizer/pipeline.py:146-200` | Method stub exists, imports non-existent module | +| `__init__.py` documentation | `ml/floor-plan-digitizer/src/openlintel_digitizer/__init__.py:9` | Documents "DWG -> DXF via LibreDWG" as Pipeline Option 1 | +| `libredwg_path` constructor parameter | `pipeline.py:56` | Accepted but never used (no converter to pass it to) | +| `oda_converter_path` constructor parameter | `pipeline.py:57` | Accepted but never used | +| DXF parser (post-conversion) | `pipeline.py:260-339` | Fully implemented — would work after DWG→DXF conversion | +| Frontend DWG file acceptance | `floor-plan-upload.tsx:18` | UI accepts .dwg but backend rejects | + +### What is MISSING: + +| Component | Impact | Priority | +|-----------|--------|----------| +| **`dwg_converter.py` module** | Cannot convert DWG to DXF | CRITICAL | +| **LibreDWG dependency** | No DWG parsing library installed | CRITICAL | +| **ODA File Converter integration** | No fallback DWG converter | HIGH | +| **DWG MIME type in upload API** | Backend rejects DWG uploads | CRITICAL | +| **DXF MIME type in upload API** | Backend rejects DXF uploads | HIGH | +| **PDF-to-image converter** | Cannot extract floor plans from PDFs | HIGH | +| **File-type router in vision engine** | Cannot dispatch DWG/DXF/PDF differently | MEDIUM | +| **Floor plan → BOM bridge** | Cannot generate BOM from structural data | HIGH | +| **DWG-native BOM extraction** | Cannot read schedules/tables from DWG | MEDIUM | + +### The Import That Fails: + +```python +# pipeline.py:169 — This will raise ImportError at runtime +from openlintel_digitizer.dwg_converter import DWGConverter +``` + +The `DWGConverter` class was designed to: +1. Check if LibreDWG (`dwg2dxf` binary) or ODA File Converter is available +2. Convert DWG → DXF using the available backend +3. Return the path to the converted DXF file + +None of this code exists. + +--- + +## 4. What Needs to Be Built + +### Phase 1: DWG/DXF Upload Support (1 sprint) + +#### 4.1.1 Update Upload API to Accept DWG/DXF + +**File:** `apps/web/src/app/api/upload/route.ts` + +```typescript +// Add these MIME types: +const ALLOWED_TYPES = [ + 'image/jpeg', 'image/png', 'image/webp', 'image/gif', + 'application/pdf', + // DWG MIME types (browsers vary): + 'application/acad', 'application/x-acad', 'application/x-autocad', + 'application/dwg', 'image/x-dwg', 'image/vnd.dwg', + 'application/octet-stream', // fallback for unknown binary + // DXF MIME types: + 'application/dxf', 'image/vnd.dxf', 'image/x-dxf', + 'application/x-dxf', +]; + +// Increase size limit: +const MAX_SIZE = 50 * 1024 * 1024; // 50MB to match frontend + +// Add extension-based validation (since DWG MIME types are unreliable): +function isAllowedByExtension(filename: string): boolean { + const ext = filename.split('.').pop()?.toLowerCase(); + return ['jpg','jpeg','png','webp','gif','pdf','dwg','dxf'].includes(ext ?? ''); +} +``` + +**Rationale:** DWG files have unreliable MIME types across browsers. Extension-based validation is necessary as a fallback. Most browsers send `application/octet-stream` for DWG files. + +#### 4.1.2 Update Media Service + +**File:** `services/media-service/src/routers/upload.py` + +Add DWG/DXF to the MIME-to-extension mapping and validator. Skip image optimization for CAD files. + +#### 4.1.3 Update Size Limits Consistency + +Align the 50MB frontend limit with the backend limit across all services. + +--- + +### Phase 2: DWG Reader/Converter (1-2 sprints) + +#### 4.2.1 Implement `dwg_converter.py` + +**File to create:** `ml/floor-plan-digitizer/src/openlintel_digitizer/dwg_converter.py` + +```python +""" +DWG to DXF conversion using LibreDWG or ODA File Converter. + +Strategy: +1. Primary: LibreDWG's dwg2dxf command-line tool (open source, C library) +2. Fallback: ODA File Converter (free, proprietary, cross-platform) +3. Future: Direct ezdxf-based DWG reading (when ezdxf adds DWG support) +""" + +import asyncio +import shutil +import tempfile +from pathlib import Path + +class DWGConverter: + """Converts DWG files to DXF format.""" + + def __init__(self, *, libredwg_path=None, oda_converter_path=None): + self._libredwg = libredwg_path or shutil.which("dwg2dxf") + self._oda = oda_converter_path or shutil.which("ODAFileConverter") + + @property + def is_available(self) -> bool: + return bool(self._libredwg or self._oda) + + @property + def backend(self) -> str: + if self._libredwg: return "libredwg" + if self._oda: return "oda" + return "none" + + async def convert(self, dwg_path, *, output_path=None, dxf_version="R2013") -> Path: + """Convert DWG to DXF, returning the path to the DXF file.""" + dwg_path = Path(dwg_path) + if not dwg_path.exists(): + raise FileNotFoundError(f"DWG file not found: {dwg_path}") + + if output_path is None: + output_path = dwg_path.with_suffix(".dxf") + output_path = Path(output_path) + + if self._libredwg: + return await self._convert_libredwg(dwg_path, output_path) + elif self._oda: + return await self._convert_oda(dwg_path, output_path, dxf_version) + else: + raise RuntimeError("No DWG conversion backend available") + + async def _convert_libredwg(self, dwg_path, output_path) -> Path: + proc = await asyncio.create_subprocess_exec( + self._libredwg, str(dwg_path), "-o", str(output_path), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + raise RuntimeError(f"dwg2dxf failed: {stderr.decode()}") + return output_path + + async def _convert_oda(self, dwg_path, output_path, dxf_version) -> Path: + # ODA converter works on directories + with tempfile.TemporaryDirectory() as tmpdir: + input_dir = Path(tmpdir) / "input" + output_dir = Path(tmpdir) / "output" + input_dir.mkdir() + output_dir.mkdir() + + shutil.copy2(dwg_path, input_dir / dwg_path.name) + + oda_version_map = { + "R2013": "ACAD2013", "R2018": "ACAD2018", + "R2010": "ACAD2010", "R2007": "ACAD2007", + } + oda_version = oda_version_map.get(dxf_version, "ACAD2013") + + proc = await asyncio.create_subprocess_exec( + self._oda, + str(input_dir), str(output_dir), + oda_version, "DXF", "0", "1", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await proc.communicate() + + converted = list(output_dir.glob("*.dxf")) + if not converted: + raise RuntimeError("ODA conversion produced no DXF output") + + shutil.copy2(converted[0], output_path) + return output_path +``` + +#### 4.2.2 Add LibreDWG Dependency + +**For the Docker image / deployment:** +```dockerfile +# In the floor-plan-digitizer Dockerfile: +RUN apt-get update && apt-get install -y libredwg-utils +``` + +**For local development:** +```bash +# Ubuntu/Debian: +sudo apt-get install libredwg-utils + +# macOS: +brew install libredwg +``` + +**For the Python package:** +``` +# requirements.txt — no pip package needed; LibreDWG is a system binary (dwg2dxf) +``` + +#### 4.2.3 Add ODA File Converter as Fallback + +The ODA File Converter is a free (registration required) binary from Open Design Alliance. It supports more DWG versions than LibreDWG. + +**Download:** https://www.opendesign.com/guestfiles/oda_file_converter + +--- + +### Phase 3: PDF Floor Plan Support (1 sprint) + +#### 4.3.1 Add PDF-to-Image Conversion + +**Dependency:** `pdf2image` (uses `poppler-utils`) + +```python +# New service in vision-engine or floor-plan-digitizer: +from pdf2image import convert_from_bytes + +def extract_floor_plan_from_pdf(pdf_bytes: bytes) -> list[Image.Image]: + """Convert PDF pages to images for VLM processing.""" + images = convert_from_bytes(pdf_bytes, dpi=300, fmt="png") + return images +``` + +#### 4.3.2 Update Vision Engine File-Type Router + +```python +# In vision router or a new preprocessor: +async def preprocess_upload(storage_key: str, mime_type: str) -> str: + """Route uploaded files to appropriate processing pipeline.""" + if mime_type in ('application/pdf',): + # Convert PDF to image, return image URL + pdf_bytes = await download_from_storage(storage_key) + images = extract_floor_plan_from_pdf(pdf_bytes) + # Save first page as image, return its URL + image_key = await save_to_storage(images[0], "png") + return get_presigned_url(image_key) + + elif mime_type in DWG_MIME_TYPES: + # Convert DWG to DXF, parse directly (no VLM needed) + dwg_bytes = await download_from_storage(storage_key) + # Save to temp file, run converter + ... + return "dwg_direct" # Signal direct DXF parsing path + + else: + # Already an image, use directly + return get_presigned_url(storage_key) +``` + +--- + +### Phase 4: Floor Plan → BOM Bridge (1-2 sprints) + +This is the most architecturally significant change. Currently: +- BOM engine input = `design_variant.spec_json` (furniture/finish descriptions) +- Floor plan output = `FloorPlanData` (walls, rooms, openings with mm coordinates) + +#### 4.4.1 Option A: Structural BOM from Floor Plan (Recommended) + +Create a new "Structural BOM" capability that generates a BOM directly from floor plan geometry: + +```python +class StructuralBOMGenerator: + """Generate a construction BOM directly from FloorPlanData. + + This bypasses the design variant and produces a civil/structural BOM: + - Wall materials (bricks, cement, sand based on wall dimensions) + - Flooring materials (tiles/marble based on room areas) + - Door/window frames and hardware + - Electrical rough-in materials + - Plumbing rough-in materials (for bathrooms/kitchens) + - Painting materials (based on wall surface area) + """ + + def generate(self, floor_plan: FloorPlanData, budget_tier: str = "mid_range") -> BOMResult: + items = [] + + for wall in floor_plan.walls: + # Calculate wall area in sqft + wall_area_sqft = (wall.length_mm * wall.height_mm) / (304.8 ** 2) + # Add bricks, cement, sand, plaster, etc. + items.extend(self._wall_materials(wall, wall_area_sqft, budget_tier)) + + for room in floor_plan.rooms: + floor_area_sqft = room.area_sqm * 10.764 + # Add flooring, painting, electrical, etc. + items.extend(self._room_materials(room, floor_area_sqft, budget_tier)) + + for opening in floor_plan.openings: + # Add door/window frames, hardware, glass + items.extend(self._opening_materials(opening, budget_tier)) + + return self._assemble_bom(items) +``` + +#### 4.4.2 Option B: Floor Plan → Design → BOM (Current Architecture Extension) + +Alternatively, bridge the gap by feeding floor plan data into the design engine: + +``` +Floor Plan (FloorPlanData) → Design Engine (with room geometry as context) → Design Variant → BOM +``` + +This would require modifying the design engine to accept structured room geometry instead of just photos. + +#### 4.4.3 Option C: DWG-Native BOM Extraction + +For DWG files created by architects that already contain material schedules: + +```python +class DWGBOMExtractor: + """Extract BOM data directly from DWG drawing tables/schedules. + + AutoCAD drawings often contain: + - Material schedules (in table entities) + - Door/window schedules + - Finish schedules + - Quantity take-off tables + - Title block data with project info + """ + + def extract_from_dxf(self, dxf_path: Path) -> list[BOMItem]: + doc = ezdxf.readfile(str(dxf_path)) + msp = doc.modelspace() + + items = [] + # Extract TABLE entities + for table in msp.query("TABLE"): + items.extend(self._parse_table(table)) + + # Extract TEXT/MTEXT for schedule information + for text in msp.query("MTEXT"): + if self._is_schedule_text(text): + items.extend(self._parse_schedule_text(text)) + + return items +``` + +--- + +### Phase 5: End-to-End Integration (1 sprint) + +#### 4.5.1 New API Endpoint: DWG-to-BOM + +```python +# New endpoint in BOM engine or a new orchestrator service: +@router.post("/api/v1/bom/from-floor-plan") +async def generate_bom_from_floor_plan( + upload_id: str, + budget_tier: str = "mid_range", + currency: str = "INR", +): + """Generate BOM directly from an uploaded DWG/DXF/PDF floor plan.""" + # 1. Fetch upload metadata + # 2. Download file from MinIO + # 3. Route by file type: + # - DWG: Convert to DXF, parse with ezdxf + # - DXF: Parse directly with ezdxf + # - PDF: Convert to image, run VLM extraction + # - Image: Run VLM extraction + # 4. Get FloorPlanData + # 5. Generate structural BOM from FloorPlanData + # 6. Return BOM result +``` + +#### 4.5.2 Frontend Integration + +Add a "Generate BOM from Floor Plan" button on the floor plan page that doesn't require going through design generation first. + +--- + +## 5. Proposed User Flow: DWG → BOM + +### Flow A: DWG File Upload + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ 1. User uploads .DWG file via floor-plan-upload component │ +│ ├─ Frontend validates extension (.dwg) ✓ │ +│ ├─ Backend validates MIME type (NEEDS: DWG types added) │ +│ ├─ File stored in MinIO with storageKey │ +│ └─ Upload record saved to PostgreSQL │ +│ │ +│ 2. File-type router detects DWG format │ +│ ├─ Downloads DWG from MinIO to temp file │ +│ ├─ Runs DWGConverter (LibreDWG dwg2dxf) │ +│ ├─ Produces .DXF file │ +│ └─ Stores converted DXF back in MinIO │ +│ │ +│ 3. DXF parser extracts structural data │ +│ ├─ Walls (LINE entities on wall layers) │ +│ ├─ Rooms (LWPOLYLINE entities on room layers) │ +│ ├─ Doors/Windows (BLOCK references, INSERT entities) │ +│ ├─ Dimensions (DIMENSION entities) │ +│ └─ Returns FloorPlanData (JSON) │ +│ │ +│ 4. User reviews detected rooms on the floor plan viewer │ +│ ├─ SVG preview generated from FloorPlanData │ +│ ├─ Room dimensions shown │ +│ └─ User can edit/correct room boundaries │ +│ │ +│ 5. User clicks "Generate BOM" │ +│ ├─ Option A: Structural BOM (directly from geometry) │ +│ │ ├─ Wall materials calculated from wall lengths/heights │ +│ │ ├─ Flooring from room areas │ +│ │ ├─ Door/window hardware from openings │ +│ │ └─ Electrical/plumbing from room types │ +│ │ │ +│ ├─ Option B: Design-based BOM │ +│ │ ├─ Trigger design generation with room geometry │ +│ │ ├─ Select design variant │ +│ │ └─ Generate BOM from design spec │ +│ │ │ +│ └─ Option C: DWG-native BOM (if schedules exist in DWG) │ +│ ├─ Extract TABLE entities from DXF │ +│ ├─ Parse material/door/window schedules │ +│ └─ Convert to BOM items │ +│ │ +│ 6. BOM displayed with category breakdown │ +│ ├─ Total cost with budget tier pricing │ +│ ├─ Material substitution suggestions │ +│ └─ Export: Excel / CSV / PDF │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +### Flow B: PDF of DWG Upload + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ 1. User uploads .PDF file containing floor plan │ +│ ├─ Could be: exported from AutoCAD, printed to PDF, scanned │ +│ └─ File stored in MinIO │ +│ │ +│ 2. PDF processor extracts floor plan │ +│ ├─ Vector PDF: Extract geometry directly (pdfplumber/pdfminer) │ +│ ├─ Raster PDF: Convert to high-res image (pdf2image at 300 DPI) │ +│ └─ Multi-page: Let user select the floor plan page │ +│ │ +│ 3. Route to appropriate pipeline: │ +│ ├─ Vector PDF → Parse lines/shapes → FloorPlanData │ +│ └─ Raster PDF → VLM extraction (GPT-4o) → FloorPlanData │ +│ │ +│ 4-6. Same as DWG flow above │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 6. Technology Recommendations + +### 6.1 DWG Reading Options + +| Option | License | DWG Versions | Quality | Recommended? | +|--------|---------|-------------|---------|-------------| +| **LibreDWG** (`dwg2dxf`) | GPLv3 | R2000–R2018 | Good for most files | Yes (primary) | +| **ODA File Converter** | Free (proprietary) | R14–R2024 | Excellent, widest support | Yes (fallback) | +| **Teigha (ODA SDK)** | Commercial | All | Best | If budget allows | +| **ezdxf** (DXF only) | MIT | N/A (DXF only) | Excellent for DXF | Already used | +| **Aspose.CAD** (Python) | Commercial | All DWG/DXF | Excellent | Alternative option | + +**Recommendation:** Use LibreDWG as primary (open source, GPLv3-compatible), ODA File Converter as fallback (wider DWG version support), and ezdxf for DXF parsing (already integrated). + +### 6.2 PDF Processing Options + +| Option | Purpose | License | +|--------|---------|---------| +| **pdf2image** + poppler | PDF → raster images | MIT/GPL | +| **pdfplumber** | Extract vector graphics/text from PDF | MIT | +| **PyMuPDF (fitz)** | Fast PDF rendering and text extraction | AGPL | +| **pdfminer.six** | Text and layout extraction | MIT | + +**Recommendation:** Use `pdf2image` for raster conversion (scanned PDFs) and `pdfplumber` for vector PDF geometry extraction. + +### 6.3 Additional Dependencies Needed + +``` +# ml/floor-plan-digitizer/requirements.txt — add: +pdf2image>=1.16 +pdfplumber>=0.10 + +# System packages (Dockerfile): +poppler-utils # for pdf2image +libredwg-utils # for dwg2dxf +``` + +--- + +## 7. Architectural Diagram: Target State + +``` + ┌──────────────┐ + │ User Upload │ + │ (DWG/DXF/ │ + │ PDF/Image) │ + └──────┬───────┘ + │ + ┌──────▼───────┐ + │ Upload API │ + │ (validate, │ + │ store MinIO)│ + └──────┬───────┘ + │ + ┌──────▼───────┐ + │ File Router │◄── NEW: detect file type + └──┬───┬───┬──┘ + │ │ │ + ┌────────┘ │ └────────┐ + │ │ │ + ┌─────▼─────┐ ┌───▼────┐ ┌─────▼─────┐ + │ DWG→DXF │ │ PDF→ │ │ Image │ + │ converter │ │ Image │ │ (direct) │ + │ (LibreDWG)│ │ (pdf2 │ │ │ + │ NEW │ │ image) │ │ │ + └─────┬─────┘ │ NEW │ └─────┬─────┘ + │ └───┬────┘ │ + │ │ │ + ┌─────▼─────┐ │ ┌─────▼─────┐ + │ DXF Parse │ │ │ VLM Floor │ + │ (ezdxf) │ │ │ Plan │ + │ EXISTING │ └──────►│ Extractor │ + └─────┬─────┘ │ EXISTING │ + │ └─────┬─────┘ + │ │ + └────────┬────────────────┘ + │ + ┌──────▼───────┐ + │ FloorPlanData│ (walls, rooms, openings, dimensions) + │ EXISTING │ + └──────┬───────┘ + │ + ┌────────┼────────┐ + │ │ │ + ┌─────▼──┐ ┌──▼────┐ ┌─▼─────────┐ + │Struct. │ │Design │ │DWG-native │ + │BOM │ │Engine │ │BOM extract│ + │ NEW │ │EXISTNG│ │ NEW │ + └────┬───┘ └──┬────┘ └─────┬─────┘ + │ │ │ + │ ┌────▼────┐ │ + │ │BOM Agent│ │ + │ │EXISTING │ │ + │ └────┬────┘ │ + │ │ │ + └────────┼────────────┘ + │ + ┌──────▼───────┐ + │ BOM Result │ + │ EXISTING │ + └──────┬───────┘ + │ + ┌──────▼───────┐ + │Export: Excel, │ + │CSV, PDF │ + │ EXISTING │ + └───────────────┘ +``` + +--- + +## 8. Priority Implementation Roadmap + +### Sprint 1: Enable DWG/DXF Upload (Unblock the pipeline) +1. Add DWG/DXF MIME types to upload API (`route.ts`) +2. Add extension-based validation fallback +3. Increase upload size limit to 50MB +4. Update media service for CAD files +5. **Deliverable:** Users can upload DWG/DXF files (stored, not yet processed) + +### Sprint 2: Implement DWG Converter +1. Create `dwg_converter.py` with LibreDWG backend +2. Add ODA File Converter fallback +3. Add `libredwg-utils` to Docker images +4. Wire converter into `FloorPlanPipeline.digitize_dwg()` +5. **Deliverable:** DWG files converted to DXF and parsed into FloorPlanData + +### Sprint 3: PDF Floor Plan Support +1. Add `pdf2image` dependency +2. Create PDF page extraction service +3. Add vector PDF parsing with `pdfplumber` +4. Update vision engine file-type routing +5. **Deliverable:** PDF floor plans converted to images and processed via VLM + +### Sprint 4: Floor Plan → BOM Bridge +1. Create `StructuralBOMGenerator` service +2. Add `/api/v1/bom/from-floor-plan` endpoint +3. Wire floor plan digitization output to BOM input +4. Add DWG-native schedule extraction (TABLE/TEXT entities) +5. **Deliverable:** BOM generated directly from floor plan geometry + +### Sprint 5: End-to-End Integration & Polish +1. Frontend "Generate BOM from Floor Plan" button +2. Multi-page PDF page selector UI +3. Floor plan review/edit UI before BOM generation +4. Error handling for unsupported DWG versions +5. **Deliverable:** Complete DWG → BOM user flow + +--- + +## 9. Risk Assessment + +| Risk | Probability | Impact | Mitigation | +|------|------------|--------|------------| +| LibreDWG fails on some DWG versions | Medium | High | ODA File Converter fallback | +| DWG files have no layer organization | Medium | Medium | Fallback: treat all lines as walls | +| VLM extraction inaccurate for complex plans | Medium | Medium | Allow manual correction UI | +| Large DWG files (>50MB) cause timeouts | Low | Medium | Async processing, progress polling | +| PDF vector extraction misses elements | Medium | Medium | Fallback to raster VLM path | +| BOM quantity accuracy from geometry alone | High | Medium | LLM-assisted quantity refinement | + +--- + +## 10. Files Referenced in This Audit + +| File | Lines | Role | +|------|-------|------| +| `apps/web/src/components/floor-plan-upload.tsx` | 1-316 | Frontend upload component | +| `apps/web/src/app/api/upload/route.ts` | 1-116 | Backend upload endpoint | +| `apps/web/src/app/api/jobs/floor-plan-digitize/route.ts` | 1-69 | Digitization job trigger | +| `apps/web/src/server/trpc/routers/bom.ts` | 1-130 | BOM tRPC router | +| `services/bom-engine/src/routers/bom.py` | 1-345 | BOM API router | +| `services/bom-engine/src/agents/bom_agent.py` | 1-621 | LangGraph BOM agent | +| `services/media-service/src/routers/upload.py` | 1-209 | Media upload handler | +| `services/vision-engine/src/routers/vision.py` | 1-139 | Vision job router | +| `services/vision-engine/src/agents/vision_agent.py` | 1-107 | VLM room detection | +| `ml/floor-plan-digitizer/src/openlintel_digitizer/pipeline.py` | 1-339 | Digitization pipeline | +| `ml/floor-plan-digitizer/src/openlintel_digitizer/__init__.py` | 1-37 | Package docs | +| `ml/floor-plan-digitizer/src/openlintel_digitizer/dxf_generator.py` | 1-392 | DXF output generator | +| `ml/floor-plan-digitizer/src/openlintel_digitizer/vlm_extractor.py` | 1-367 | VLM floor plan extractor | +| `ml/floor-plan-digitizer/src/openlintel_digitizer/schemas.py` | 1-271 | FloorPlanData schemas | +| `ml/floor-plan-digitizer/src/openlintel_digitizer/dwg_converter.py` | **DOES NOT EXIST** | DWG→DXF converter | +| `packages/db/src/schema/app.ts` | — | Database schema | + +--- + +## 11. Summary + +The OpenLintel codebase has **strong architectural foundations** for a DWG-to-BOM pipeline: +- The `FloorPlanPipeline` class has the right method signatures and flow design +- The DXF parser (`_parse_dxf`) is fully functional +- The BOM engine is production-ready with LLM extraction, pricing, optimization, and export +- The frontend UI already accepts DWG files + +But **three critical modules are missing**: +1. **`dwg_converter.py`** — the DWG-to-DXF conversion bridge +2. **Upload API DWG/DXF MIME support** — the backend gate that blocks uploads +3. **Floor Plan → BOM bridge** — the connection between structural geometry and material calculation + +Building these three modules, plus PDF support, would unlock the full DWG → BOM pipeline in approximately 3-5 sprints. diff --git a/sampledb/001_users_auth.sql b/sampledb/001_users_auth.sql new file mode 100644 index 0000000..196a0a4 --- /dev/null +++ b/sampledb/001_users_auth.sql @@ -0,0 +1,77 @@ +-- ============================================================================ +-- 001_users_auth.sql — Users, Accounts, Sessions +-- ============================================================================ + +-- User 1: Alice (homeowner) +INSERT INTO users (id, name, email, email_verified, image, role, enabled, preferred_currency, preferred_unit_system, preferred_locale, created_at, updated_at) +VALUES ( + 'usr_alice_001', + 'Alice Sharma', + 'alice@example.com', + NOW() - INTERVAL '30 days', + NULL, + 'user', + true, + 'INR', + 'metric', + 'en', + NOW() - INTERVAL '60 days', + NOW() - INTERVAL '1 day' +); + +-- User 2: Bob (admin) +INSERT INTO users (id, name, email, email_verified, image, role, enabled, preferred_currency, preferred_unit_system, preferred_locale, created_at, updated_at) +VALUES ( + 'usr_bob_002', + 'Bob Kumar', + 'bob@example.com', + NOW() - INTERVAL '90 days', + NULL, + 'admin', + true, + 'INR', + 'metric', + 'en', + NOW() - INTERVAL '90 days', + NOW() - INTERVAL '2 days' +); + +-- OAuth account for Alice (Google) +INSERT INTO accounts (user_id, type, provider, provider_account_id, access_token, token_type, scope) +VALUES ( + 'usr_alice_001', + 'oauth', + 'google', + 'google_alice_12345', + 'mock_access_token_alice', + 'Bearer', + 'openid email profile' +); + +-- OAuth account for Bob (GitHub) +INSERT INTO accounts (user_id, type, provider, provider_account_id, access_token, token_type, scope) +VALUES ( + 'usr_bob_002', + 'oauth', + 'github', + 'github_bob_67890', + 'mock_access_token_bob', + 'Bearer', + 'read:user user:email' +); + +-- Active session for Alice +INSERT INTO sessions (session_token, user_id, expires) +VALUES ( + 'sess_alice_active_token_001', + 'usr_alice_001', + NOW() + INTERVAL '30 days' +); + +-- Active session for Bob +INSERT INTO sessions (session_token, user_id, expires) +VALUES ( + 'sess_bob_active_token_002', + 'usr_bob_002', + NOW() + INTERVAL '30 days' +); diff --git a/sampledb/002_projects_rooms.sql b/sampledb/002_projects_rooms.sql new file mode 100644 index 0000000..86073f6 --- /dev/null +++ b/sampledb/002_projects_rooms.sql @@ -0,0 +1,176 @@ +-- ============================================================================ +-- 002_projects_rooms.sql — Projects and Rooms +-- ============================================================================ + +-- Project 1: Mumbai Apartment (active, in construction phase) +INSERT INTO projects (id, user_id, name, status, address, unit_system, created_at, updated_at) +VALUES ( + 'prj_mumbai_001', + 'usr_alice_001', + 'Mumbai 2BHK Apartment Renovation', + 'in_progress', + '504 Sea View Towers, Bandra West, Mumbai 400050', + 'metric', + NOW() - INTERVAL '45 days', + NOW() - INTERVAL '1 day' +); + +-- Project 2: Bangalore Villa (design phase) +INSERT INTO projects (id, user_id, name, status, address, unit_system, created_at, updated_at) +VALUES ( + 'prj_blr_002', + 'usr_alice_001', + 'Bangalore Villa Interior Design', + 'draft', + '12 Koramangala 4th Block, Bangalore 560034', + 'metric', + NOW() - INTERVAL '10 days', + NOW() - INTERVAL '2 days' +); + +-- Project 3: Delhi Penthouse (completed) +INSERT INTO projects (id, user_id, name, status, address, unit_system, created_at, updated_at) +VALUES ( + 'prj_delhi_003', + 'usr_bob_002', + 'Delhi Penthouse Redesign', + 'completed', + '1501 DLF Magnolias, Gurugram 122009', + 'metric', + NOW() - INTERVAL '180 days', + NOW() - INTERVAL '15 days' +); + +-- ---- Rooms for Project 1: Mumbai Apartment ---- + +-- Living Room +INSERT INTO rooms (id, project_id, name, type, length_mm, width_mm, height_mm, floor, metadata, created_at, updated_at) +VALUES ( + 'room_mum_living_001', + 'prj_mumbai_001', + 'Main Living Room', + 'living_room', + 5000, + 4000, + 2800, + 0, + '{"windows": 2, "doors": 1, "balconyAccess": true}', + NOW() - INTERVAL '44 days', + NOW() - INTERVAL '3 days' +); + +-- Master Bedroom +INSERT INTO rooms (id, project_id, name, type, length_mm, width_mm, height_mm, floor, metadata, created_at, updated_at) +VALUES ( + 'room_mum_master_002', + 'prj_mumbai_001', + 'Master Bedroom', + 'bedroom', + 4500, + 3500, + 2800, + 0, + '{"windows": 1, "doors": 1, "hasAttachedBath": true}', + NOW() - INTERVAL '44 days', + NOW() - INTERVAL '3 days' +); + +-- Kitchen +INSERT INTO rooms (id, project_id, name, type, length_mm, width_mm, height_mm, floor, metadata, created_at, updated_at) +VALUES ( + 'room_mum_kitchen_003', + 'prj_mumbai_001', + 'Modular Kitchen', + 'kitchen', + 3500, + 2500, + 2800, + 0, + '{"layout": "L-shaped", "hasChimney": true, "gasConnection": true}', + NOW() - INTERVAL '44 days', + NOW() - INTERVAL '5 days' +); + +-- Bathroom +INSERT INTO rooms (id, project_id, name, type, length_mm, width_mm, height_mm, floor, metadata, created_at, updated_at) +VALUES ( + 'room_mum_bath_004', + 'prj_mumbai_001', + 'Master Bathroom', + 'bathroom', + 2500, + 2000, + 2800, + 0, + '{"hasShower": true, "hasBathtub": false, "waterproofed": true}', + NOW() - INTERVAL '44 days', + NOW() - INTERVAL '5 days' +); + +-- ---- Rooms for Project 2: Bangalore Villa ---- + +-- Living Room +INSERT INTO rooms (id, project_id, name, type, length_mm, width_mm, height_mm, floor, metadata, created_at, updated_at) +VALUES ( + 'room_blr_living_001', + 'prj_blr_002', + 'Open Plan Living + Dining', + 'living_room', + 8000, + 5500, + 3200, + 0, + '{"windows": 4, "doors": 2, "doubleHeight": false}', + NOW() - INTERVAL '9 days', + NOW() - INTERVAL '3 days' +); + +-- Kids Room +INSERT INTO rooms (id, project_id, name, type, length_mm, width_mm, height_mm, floor, metadata, created_at, updated_at) +VALUES ( + 'room_blr_kids_002', + 'prj_blr_002', + 'Kids Room', + 'kids_room', + 3500, + 3000, + 3200, + 1, + '{"windows": 1, "doors": 1, "studyArea": true}', + NOW() - INTERVAL '8 days', + NOW() - INTERVAL '3 days' +); + +-- ---- Rooms for Project 3: Delhi Penthouse ---- + +-- Living Room +INSERT INTO rooms (id, project_id, name, type, length_mm, width_mm, height_mm, floor, metadata, created_at, updated_at) +VALUES ( + 'room_del_living_001', + 'prj_delhi_003', + 'Penthouse Living Area', + 'living_room', + 10000, + 7000, + 3500, + 0, + '{"windows": 6, "doors": 3, "panoramicView": true}', + NOW() - INTERVAL '170 days', + NOW() - INTERVAL '20 days' +); + +-- Study +INSERT INTO rooms (id, project_id, name, type, length_mm, width_mm, height_mm, floor, metadata, created_at, updated_at) +VALUES ( + 'room_del_study_002', + 'prj_delhi_003', + 'Home Office / Study', + 'study', + 4000, + 3500, + 3500, + 0, + '{"windows": 2, "doors": 1, "builtInShelves": true}', + NOW() - INTERVAL '170 days', + NOW() - INTERVAL '20 days' +); diff --git a/sampledb/003_uploads_designs.sql b/sampledb/003_uploads_designs.sql new file mode 100644 index 0000000..44915b2 --- /dev/null +++ b/sampledb/003_uploads_designs.sql @@ -0,0 +1,129 @@ +-- ============================================================================ +-- 003_uploads_designs.sql — Uploads and Design Variants +-- ============================================================================ + +-- ---- Uploads for Mumbai Apartment ---- + +INSERT INTO uploads (id, user_id, project_id, room_id, filename, mime_type, size_bytes, storage_key, category, thumbnail_key, image_hash, created_at) +VALUES + ('upl_mum_001', 'usr_alice_001', 'prj_mumbai_001', 'room_mum_living_001', 'living_room_photo_1.jpg', 'image/jpeg', 2450000, 'uploads/prj_mumbai_001/living_room_photo_1.jpg', 'photo', 'thumbnails/prj_mumbai_001/living_room_photo_1_thumb.jpg', 'phash_a1b2c3d4', NOW() - INTERVAL '43 days'), + ('upl_mum_002', 'usr_alice_001', 'prj_mumbai_001', 'room_mum_living_001', 'living_room_photo_2.jpg', 'image/jpeg', 1980000, 'uploads/prj_mumbai_001/living_room_photo_2.jpg', 'photo', 'thumbnails/prj_mumbai_001/living_room_photo_2_thumb.jpg', 'phash_e5f6g7h8', NOW() - INTERVAL '43 days'), + ('upl_mum_003', 'usr_alice_001', 'prj_mumbai_001', 'room_mum_kitchen_003', 'kitchen_photo_1.jpg', 'image/jpeg', 3100000, 'uploads/prj_mumbai_001/kitchen_photo_1.jpg', 'photo', 'thumbnails/prj_mumbai_001/kitchen_photo_1_thumb.jpg', 'phash_i9j0k1l2', NOW() - INTERVAL '42 days'), + ('upl_mum_004', 'usr_alice_001', 'prj_mumbai_001', NULL, 'floor_plan_2bhk.pdf', 'application/pdf', 5200000, 'uploads/prj_mumbai_001/floor_plan_2bhk.pdf', 'floor_plan', NULL, NULL, NOW() - INTERVAL '44 days'); + +-- ---- Uploads for Bangalore Villa ---- + +INSERT INTO uploads (id, user_id, project_id, room_id, filename, mime_type, size_bytes, storage_key, category, thumbnail_key, image_hash, created_at) +VALUES + ('upl_blr_001', 'usr_alice_001', 'prj_blr_002', 'room_blr_living_001', 'villa_living_room.jpg', 'image/jpeg', 4200000, 'uploads/prj_blr_002/villa_living_room.jpg', 'photo', 'thumbnails/prj_blr_002/villa_living_room_thumb.jpg', 'phash_m3n4o5p6', NOW() - INTERVAL '8 days'), + ('upl_blr_002', 'usr_alice_001', 'prj_blr_002', NULL, 'villa_floor_plan.png', 'image/png', 6800000, 'uploads/prj_blr_002/villa_floor_plan.png', 'floor_plan', NULL, NULL, NOW() - INTERVAL '9 days'); + +-- ---- Design Variants for Mumbai Living Room ---- + +-- Modern Mid-Range +INSERT INTO design_variants (id, room_id, name, style, budget_tier, render_url, spec_json, source_upload_id, prompt_used, constraints, render_urls, metadata, created_at) +VALUES ( + 'dv_mum_liv_modern_001', + 'room_mum_living_001', + 'Modern Minimalist Living', + 'modern', + 'mid_range', + 'renders/prj_mumbai_001/living_modern_v1.jpg', + '{"furniture": [{"type": "sofa", "material": "fabric", "color": "grey", "dimensions": {"l": 2200, "w": 900, "h": 850}}, {"type": "coffee_table", "material": "wood", "finish": "walnut", "dimensions": {"l": 1200, "w": 600, "h": 450}}, {"type": "tv_unit", "material": "engineered_wood", "finish": "white_matte", "dimensions": {"l": 1800, "w": 400, "h": 500}}, {"type": "bookshelf", "material": "wood", "dimensions": {"l": 800, "w": 300, "h": 1800}}], "flooring": {"type": "vitrified_tiles", "size": "600x600mm", "color": "light_grey"}, "lighting": [{"type": "recessed", "count": 6}, {"type": "pendant", "count": 1}], "paint": {"walls": "#F5F5F0", "accent": "#2C3E50"}}', + 'upl_mum_001', + 'Design a modern minimalist living room for a 5m x 4m space with 2 windows and balcony access', + '["keep existing AC location", "child-safe corners", "budget under 5 lakh INR"]', + '["renders/prj_mumbai_001/living_modern_v1.jpg", "renders/prj_mumbai_001/living_modern_v1_angle2.jpg"]', + '{"generationTimeMs": 45200, "modelProvider": "openai", "modelId": "gpt-4o"}', + NOW() - INTERVAL '40 days' +); + +-- Scandinavian Premium +INSERT INTO design_variants (id, room_id, name, style, budget_tier, render_url, spec_json, source_upload_id, prompt_used, constraints, render_urls, metadata, created_at) +VALUES ( + 'dv_mum_liv_scandi_002', + 'room_mum_living_001', + 'Scandinavian Warm Living', + 'scandinavian', + 'premium', + 'renders/prj_mumbai_001/living_scandi_v1.jpg', + '{"furniture": [{"type": "sofa", "material": "linen", "color": "cream", "dimensions": {"l": 2400, "w": 950, "h": 800}}, {"type": "coffee_table", "material": "oak", "finish": "natural", "dimensions": {"l": 1100, "w": 550, "h": 400}}, {"type": "sideboard", "material": "oak", "finish": "natural", "dimensions": {"l": 1600, "w": 450, "h": 750}}], "flooring": {"type": "engineered_wood", "species": "oak", "finish": "matte"}, "lighting": [{"type": "track", "count": 4}, {"type": "floor_lamp", "count": 2}], "paint": {"walls": "#FFFFFF", "accent": "#D4C5A9"}}', + 'upl_mum_001', + 'Design a warm Scandinavian living room with natural materials', + '["natural wood preferred", "lots of natural light", "cozy reading corner"]', + '["renders/prj_mumbai_001/living_scandi_v1.jpg"]', + '{"generationTimeMs": 52300, "modelProvider": "anthropic", "modelId": "claude-3-opus"}', + NOW() - INTERVAL '39 days' +); + +-- ---- Design Variants for Mumbai Kitchen ---- + +INSERT INTO design_variants (id, room_id, name, style, budget_tier, render_url, spec_json, source_upload_id, prompt_used, constraints, render_urls, metadata, created_at) +VALUES ( + 'dv_mum_kit_modern_001', + 'room_mum_kitchen_003', + 'Modern L-Shaped Kitchen', + 'modern', + 'mid_range', + 'renders/prj_mumbai_001/kitchen_modern_v1.jpg', + '{"cabinets": {"upper": {"material": "plywood_laminate", "finish": "white_gloss", "count": 6}, "lower": {"material": "plywood_laminate", "finish": "grey_matte", "count": 8}}, "countertop": {"material": "quartz", "color": "calacatta_white", "thickness_mm": 20}, "backsplash": {"material": "subway_tile", "color": "white", "size": "75x150mm"}, "appliances": [{"type": "chimney", "brand": "Elica"}, {"type": "hob", "brand": "Bosch", "burners": 4}, {"type": "microwave", "built_in": true}], "sink": {"type": "undermount", "material": "stainless_steel", "bowls": 2}}', + 'upl_mum_003', + 'Design a modern L-shaped modular kitchen for a 3.5m x 2.5m space', + '["gas connection on east wall", "chimney above hob", "maximize storage"]', + '["renders/prj_mumbai_001/kitchen_modern_v1.jpg"]', + '{"generationTimeMs": 38900, "modelProvider": "openai", "modelId": "gpt-4o"}', + NOW() - INTERVAL '38 days' +); + +-- ---- Design Variants for Bangalore Living ---- + +INSERT INTO design_variants (id, room_id, name, style, budget_tier, render_url, spec_json, source_upload_id, prompt_used, constraints, render_urls, metadata, created_at) +VALUES ( + 'dv_blr_liv_contemporary_001', + 'room_blr_living_001', + 'Contemporary Open Plan', + 'contemporary', + 'luxury', + 'renders/prj_blr_002/living_contemporary_v1.jpg', + '{"furniture": [{"type": "sectional_sofa", "material": "italian_leather", "color": "tan", "dimensions": {"l": 3200, "w": 1800, "h": 850}}, {"type": "dining_table", "material": "marble_top_teak_base", "seats": 8, "dimensions": {"l": 2400, "w": 1100, "h": 750}}], "flooring": {"type": "italian_marble", "variety": "statuario", "size": "800x800mm"}, "lighting": [{"type": "chandelier", "count": 1}, {"type": "cove", "length_m": 12}], "paint": {"walls": "#FAF9F6", "accent": "#8B7355"}}', + 'upl_blr_001', + 'Design a luxury contemporary open plan living and dining area for a villa', + '["8-seater dining required", "Italian marble flooring", "high ceiling 3.2m"]', + '["renders/prj_blr_002/living_contemporary_v1.jpg"]', + '{"generationTimeMs": 61000, "modelProvider": "google", "modelId": "gemini-1.5-pro"}', + NOW() - INTERVAL '7 days' +); + +-- ---- Design Variants for Delhi Penthouse ---- + +INSERT INTO design_variants (id, room_id, name, style, budget_tier, render_url, spec_json, source_upload_id, prompt_used, constraints, render_urls, metadata, created_at) +VALUES ( + 'dv_del_liv_artdeco_001', + 'room_del_living_001', + 'Art Deco Penthouse Living', + 'art_deco', + 'luxury', + 'renders/prj_delhi_003/living_artdeco_v1.jpg', + '{"furniture": [{"type": "chesterfield_sofa", "material": "velvet", "color": "emerald_green"}, {"type": "bar_cabinet", "material": "brass_glass", "finish": "gold"}], "flooring": {"type": "marble", "variety": "nero_marquina", "inlay": "brass"}, "lighting": [{"type": "art_deco_chandelier", "count": 2}], "paint": {"walls": "#1A1A2E", "accent": "#C9A96E"}}', + NULL, + 'Design a luxury art deco penthouse living area with panoramic city views', + '["panoramic window treatment", "home bar area", "statement chandelier"]', + '["renders/prj_delhi_003/living_artdeco_v1.jpg", "renders/prj_delhi_003/living_artdeco_v1_night.jpg"]', + '{"generationTimeMs": 55800, "modelProvider": "openai", "modelId": "gpt-4o"}', + NOW() - INTERVAL '160 days' +), +( + 'dv_del_study_modern_001', + 'room_del_study_002', + 'Modern Executive Study', + 'modern', + 'premium', + 'renders/prj_delhi_003/study_modern_v1.jpg', + '{"furniture": [{"type": "executive_desk", "material": "walnut_veneer", "dimensions": {"l": 1800, "w": 800, "h": 750}}, {"type": "ergonomic_chair", "material": "leather_mesh"}, {"type": "bookshelf_wall", "material": "walnut", "dimensions": {"l": 4000, "w": 350, "h": 3000}}], "flooring": {"type": "engineered_wood", "species": "walnut"}, "lighting": [{"type": "task_lamp", "count": 1}, {"type": "recessed", "count": 8}]}', + NULL, + 'Design a modern executive home office with floor-to-ceiling bookshelves', + '["full wall bookshelf", "standing desk option", "video call background wall"]', + '["renders/prj_delhi_003/study_modern_v1.jpg"]', + '{"generationTimeMs": 42100, "modelProvider": "anthropic", "modelId": "claude-3-opus"}', + NOW() - INTERVAL '158 days' +); diff --git a/sampledb/004_jobs.sql b/sampledb/004_jobs.sql new file mode 100644 index 0000000..78eab7c --- /dev/null +++ b/sampledb/004_jobs.sql @@ -0,0 +1,155 @@ +-- ============================================================================ +-- 004_jobs.sql — Async Job Records (all statuses represented) +-- ============================================================================ + +-- Completed design generation job +INSERT INTO jobs (id, user_id, type, status, input_json, output_json, error, progress, project_id, room_id, design_variant_id, created_at, started_at, completed_at) +VALUES ( + 'job_design_001', + 'usr_alice_001', + 'design_generation', + 'completed', + '{"style": "modern", "budgetTier": "mid_range", "roomId": "room_mum_living_001"}', + '{"designVariantId": "dv_mum_liv_modern_001", "renderCount": 2}', + NULL, + 100, + 'prj_mumbai_001', + 'room_mum_living_001', + 'dv_mum_liv_modern_001', + NOW() - INTERVAL '40 days', + NOW() - INTERVAL '40 days' + INTERVAL '5 seconds', + NOW() - INTERVAL '40 days' + INTERVAL '50 seconds' +); + +-- Completed BOM calculation job +INSERT INTO jobs (id, user_id, type, status, input_json, output_json, error, progress, project_id, room_id, design_variant_id, created_at, started_at, completed_at) +VALUES ( + 'job_bom_001', + 'usr_alice_001', + 'bom_calculation', + 'completed', + '{"designVariantId": "dv_mum_liv_modern_001"}', + '{"bomResultId": "bom_mum_liv_001", "totalItems": 18, "totalCost": 285000}', + NULL, + 100, + 'prj_mumbai_001', + 'room_mum_living_001', + 'dv_mum_liv_modern_001', + NOW() - INTERVAL '38 days', + NOW() - INTERVAL '38 days' + INTERVAL '3 seconds', + NOW() - INTERVAL '38 days' + INTERVAL '25 seconds' +); + +-- Completed drawing job +INSERT INTO jobs (id, user_id, type, status, input_json, output_json, error, progress, project_id, room_id, design_variant_id, created_at, started_at, completed_at) +VALUES ( + 'job_drawing_001', + 'usr_alice_001', + 'drawing', + 'completed', + '{"designVariantId": "dv_mum_liv_modern_001", "drawingTypes": ["floor_plan", "elevation", "electrical"]}', + '{"drawingResultIds": ["draw_mum_fp_001", "draw_mum_elev_001", "draw_mum_elec_001"]}', + NULL, + 100, + 'prj_mumbai_001', + 'room_mum_living_001', + 'dv_mum_liv_modern_001', + NOW() - INTERVAL '37 days', + NOW() - INTERVAL '37 days' + INTERVAL '2 seconds', + NOW() - INTERVAL '37 days' + INTERVAL '90 seconds' +); + +-- Running cut list job (in progress) +INSERT INTO jobs (id, user_id, type, status, input_json, output_json, error, progress, project_id, room_id, design_variant_id, created_at, started_at, completed_at) +VALUES ( + 'job_cutlist_001', + 'usr_alice_001', + 'cutlist', + 'running', + '{"designVariantId": "dv_mum_kit_modern_001"}', + NULL, + NULL, + 65, + 'prj_mumbai_001', + 'room_mum_kitchen_003', + 'dv_mum_kit_modern_001', + NOW() - INTERVAL '1 hour', + NOW() - INTERVAL '58 minutes', + NULL +); + +-- Pending MEP job +INSERT INTO jobs (id, user_id, type, status, input_json, output_json, error, progress, project_id, room_id, design_variant_id, created_at, started_at, completed_at) +VALUES ( + 'job_mep_001', + 'usr_alice_001', + 'mep_calculation', + 'pending', + '{"designVariantId": "dv_mum_liv_modern_001", "calcTypes": ["electrical", "plumbing"]}', + NULL, + NULL, + 0, + 'prj_mumbai_001', + 'room_mum_living_001', + 'dv_mum_liv_modern_001', + NOW() - INTERVAL '30 minutes', + NULL, + NULL +); + +-- Failed segmentation job +INSERT INTO jobs (id, user_id, type, status, input_json, output_json, error, progress, project_id, room_id, design_variant_id, created_at, started_at, completed_at) +VALUES ( + 'job_segment_fail_001', + 'usr_alice_001', + 'segmentation', + 'failed', + '{"uploadId": "upl_mum_001", "model": "sam2"}', + NULL, + 'SAM2 model failed: CUDA out of memory. Tried to allocate 2.00 GiB', + 45, + 'prj_mumbai_001', + 'room_mum_living_001', + NULL, + NOW() - INTERVAL '35 days', + NOW() - INTERVAL '35 days' + INTERVAL '10 seconds', + NOW() - INTERVAL '35 days' + INTERVAL '120 seconds' +); + +-- Completed floor plan digitization job +INSERT INTO jobs (id, user_id, type, status, input_json, output_json, error, progress, project_id, room_id, design_variant_id, created_at, started_at, completed_at) +VALUES ( + 'job_floorplan_001', + 'usr_alice_001', + 'floor_plan_digitize', + 'completed', + '{"uploadId": "upl_mum_004"}', + '{"roomsDetected": 6, "dxfStorageKey": "outputs/prj_mumbai_001/floor_plan.dxf", "doorsDetected": 5, "windowsDetected": 8}', + NULL, + 100, + 'prj_mumbai_001', + NULL, + NULL, + NOW() - INTERVAL '43 days', + NOW() - INTERVAL '43 days' + INTERVAL '5 seconds', + NOW() - INTERVAL '43 days' + INTERVAL '35 seconds' +); + +-- Cancelled job +INSERT INTO jobs (id, user_id, type, status, input_json, output_json, error, progress, project_id, room_id, design_variant_id, created_at, started_at, completed_at) +VALUES ( + 'job_cancelled_001', + 'usr_alice_001', + 'design_generation', + 'cancelled', + '{"style": "industrial", "budgetTier": "economy", "roomId": "room_mum_living_001"}', + NULL, + 'Cancelled by user', + 10, + 'prj_mumbai_001', + 'room_mum_living_001', + NULL, + NOW() - INTERVAL '41 days', + NOW() - INTERVAL '41 days' + INTERVAL '3 seconds', + NOW() - INTERVAL '41 days' + INTERVAL '15 seconds' +); diff --git a/sampledb/005_bom_drawings_cutlist_mep.sql b/sampledb/005_bom_drawings_cutlist_mep.sql new file mode 100644 index 0000000..acc1366 --- /dev/null +++ b/sampledb/005_bom_drawings_cutlist_mep.sql @@ -0,0 +1,151 @@ +-- ============================================================================ +-- 005_bom_drawings_cutlist_mep.sql — Technical Outputs +-- ============================================================================ + +-- ---- BOM Results ---- + +-- Mumbai Living Room BOM +INSERT INTO bom_results (id, design_variant_id, job_id, items, total_cost, currency, metadata, created_at) +VALUES ( + 'bom_mum_liv_001', + 'dv_mum_liv_modern_001', + 'job_bom_001', + '[ + {"category": "furniture", "name": "3-Seater Fabric Sofa - Grey", "quantity": 1, "unit": "piece", "unitPrice": 45000, "totalPrice": 45000, "vendor": "Urban Ladder"}, + {"category": "furniture", "name": "Walnut Coffee Table", "quantity": 1, "unit": "piece", "unitPrice": 12000, "totalPrice": 12000, "vendor": "Pepperfry"}, + {"category": "furniture", "name": "TV Unit - White Matte 1800mm", "quantity": 1, "unit": "piece", "unitPrice": 22000, "totalPrice": 22000, "vendor": "Custom"}, + {"category": "furniture", "name": "Bookshelf 800x300x1800mm", "quantity": 1, "unit": "piece", "unitPrice": 15000, "totalPrice": 15000, "vendor": "Custom"}, + {"category": "flooring", "name": "Vitrified Tiles 600x600mm Light Grey", "quantity": 34, "unit": "sqm", "unitPrice": 850, "totalPrice": 28900, "vendor": "Kajaria"}, + {"category": "flooring", "name": "Tile Adhesive", "quantity": 10, "unit": "bag", "unitPrice": 450, "totalPrice": 4500, "vendor": "MYK Laticrete"}, + {"category": "flooring", "name": "Tile Spacers 2mm", "quantity": 5, "unit": "pack", "unitPrice": 120, "totalPrice": 600, "vendor": "Local"}, + {"category": "lighting", "name": "Recessed LED Downlight 12W", "quantity": 6, "unit": "piece", "unitPrice": 650, "totalPrice": 3900, "vendor": "Philips"}, + {"category": "lighting", "name": "Pendant Light - Geometric", "quantity": 1, "unit": "piece", "unitPrice": 8500, "totalPrice": 8500, "vendor": "Jainsons"}, + {"category": "paint", "name": "Asian Paints Royale Matt - Off White", "quantity": 4, "unit": "litre", "unitPrice": 750, "totalPrice": 3000, "vendor": "Asian Paints"}, + {"category": "paint", "name": "Asian Paints Royale Matt - Navy Accent", "quantity": 2, "unit": "litre", "unitPrice": 750, "totalPrice": 1500, "vendor": "Asian Paints"}, + {"category": "paint", "name": "Primer & Putty", "quantity": 1, "unit": "set", "unitPrice": 3500, "totalPrice": 3500, "vendor": "Asian Paints"}, + {"category": "hardware", "name": "Curtain Rod - Brushed Nickel 2.5m", "quantity": 2, "unit": "piece", "unitPrice": 1800, "totalPrice": 3600, "vendor": "Curtain World"}, + {"category": "fabric", "name": "Blackout Curtains - Grey", "quantity": 2, "unit": "pair", "unitPrice": 3500, "totalPrice": 7000, "vendor": "D Decor"}, + {"category": "electrical", "name": "Modular Switch Plates", "quantity": 8, "unit": "piece", "unitPrice": 350, "totalPrice": 2800, "vendor": "Legrand"}, + {"category": "electrical", "name": "5A/15A Sockets", "quantity": 12, "unit": "piece", "unitPrice": 180, "totalPrice": 2160, "vendor": "Legrand"}, + {"category": "hardware", "name": "Wall Mounting Hardware Kit", "quantity": 1, "unit": "set", "unitPrice": 1200, "totalPrice": 1200, "vendor": "Fischer"}, + {"category": "miscellaneous", "name": "Labour - Installation", "quantity": 1, "unit": "lumpsum", "unitPrice": 35000, "totalPrice": 35000, "vendor": "Contractor"} + ]', + 198160, + 'INR', + '{"roomArea_sqm": 20, "costPerSqm": 9908}', + NOW() - INTERVAL '38 days' +); + +-- Mumbai Kitchen BOM +INSERT INTO bom_results (id, design_variant_id, job_id, items, total_cost, currency, metadata, created_at) +VALUES ( + 'bom_mum_kit_001', + 'dv_mum_kit_modern_001', + NULL, + '[ + {"category": "cabinets", "name": "Upper Cabinets - White Gloss Laminate", "quantity": 6, "unit": "piece", "unitPrice": 8500, "totalPrice": 51000, "vendor": "Custom"}, + {"category": "cabinets", "name": "Lower Cabinets - Grey Matte Laminate", "quantity": 8, "unit": "piece", "unitPrice": 9500, "totalPrice": 76000, "vendor": "Custom"}, + {"category": "countertop", "name": "Quartz Countertop - Calacatta White", "quantity": 4.5, "unit": "sqm", "unitPrice": 12000, "totalPrice": 54000, "vendor": "Kalinga Stone"}, + {"category": "backsplash", "name": "Subway Tiles 75x150mm White", "quantity": 5, "unit": "sqm", "unitPrice": 1200, "totalPrice": 6000, "vendor": "Johnson"}, + {"category": "sink", "name": "SS Undermount Double Bowl Sink", "quantity": 1, "unit": "piece", "unitPrice": 8500, "totalPrice": 8500, "vendor": "Franke"}, + {"category": "appliances", "name": "Elica Kitchen Chimney 90cm", "quantity": 1, "unit": "piece", "unitPrice": 15000, "totalPrice": 15000, "vendor": "Elica"}, + {"category": "appliances", "name": "Bosch 4-Burner Gas Hob", "quantity": 1, "unit": "piece", "unitPrice": 22000, "totalPrice": 22000, "vendor": "Bosch"}, + {"category": "appliances", "name": "Built-in Microwave", "quantity": 1, "unit": "piece", "unitPrice": 18000, "totalPrice": 18000, "vendor": "IFB"}, + {"category": "hardware", "name": "Soft-Close Hinges (pair)", "quantity": 28, "unit": "pair", "unitPrice": 250, "totalPrice": 7000, "vendor": "Hettich"}, + {"category": "hardware", "name": "Telescopic Drawer Channels", "quantity": 16, "unit": "pair", "unitPrice": 550, "totalPrice": 8800, "vendor": "Hettich"}, + {"category": "plumbing", "name": "Kitchen Tap - Pull Down", "quantity": 1, "unit": "piece", "unitPrice": 6500, "totalPrice": 6500, "vendor": "Grohe"} + ]', + 272800, + 'INR', + '{"roomArea_sqm": 8.75, "costPerSqm": 31177}', + NOW() - INTERVAL '36 days' +); + +-- ---- Drawing Results ---- + +-- Mumbai Living Room Floor Plan +INSERT INTO drawing_results (id, design_variant_id, job_id, drawing_type, dxf_storage_key, pdf_storage_key, svg_storage_key, ifc_storage_key, metadata, created_at) +VALUES + ('draw_mum_fp_001', 'dv_mum_liv_modern_001', 'job_drawing_001', 'floor_plan', 'drawings/prj_mumbai_001/living_floor_plan.dxf', 'drawings/prj_mumbai_001/living_floor_plan.pdf', 'drawings/prj_mumbai_001/living_floor_plan.svg', NULL, '{"scale": "1:50", "paperSize": "A3"}', NOW() - INTERVAL '37 days'), + ('draw_mum_elev_001', 'dv_mum_liv_modern_001', 'job_drawing_001', 'elevation', 'drawings/prj_mumbai_001/living_elevation.dxf', 'drawings/prj_mumbai_001/living_elevation.pdf', NULL, NULL, '{"scale": "1:25", "walls": ["north", "south", "east", "west"]}', NOW() - INTERVAL '37 days'), + ('draw_mum_elec_001', 'dv_mum_liv_modern_001', 'job_drawing_001', 'electrical', 'drawings/prj_mumbai_001/living_electrical.dxf', 'drawings/prj_mumbai_001/living_electrical.pdf', NULL, NULL, '{"scale": "1:50", "circuits": 3}', NOW() - INTERVAL '37 days'); + +-- Delhi Penthouse Drawings (completed project) +INSERT INTO drawing_results (id, design_variant_id, job_id, drawing_type, dxf_storage_key, pdf_storage_key, svg_storage_key, ifc_storage_key, metadata, created_at) +VALUES + ('draw_del_fp_001', 'dv_del_liv_artdeco_001', NULL, 'floor_plan', 'drawings/prj_delhi_003/living_floor_plan.dxf', 'drawings/prj_delhi_003/living_floor_plan.pdf', 'drawings/prj_delhi_003/living_floor_plan.svg', 'drawings/prj_delhi_003/living.ifc', '{"scale": "1:50"}', NOW() - INTERVAL '150 days'), + ('draw_del_rcp_001', 'dv_del_liv_artdeco_001', NULL, 'rcp', 'drawings/prj_delhi_003/living_rcp.dxf', 'drawings/prj_delhi_003/living_rcp.pdf', NULL, NULL, '{"scale": "1:50", "ceilingType": "false_ceiling_with_coves"}', NOW() - INTERVAL '150 days'); + +-- ---- Cut List Results ---- + +-- Mumbai Kitchen Cut List +INSERT INTO cutlist_results (id, design_variant_id, job_id, panels, hardware, nesting_result, total_sheets, waste_percent, created_at) +VALUES ( + 'cl_mum_kit_001', + 'dv_mum_kit_modern_001', + NULL, + '[ + {"partName": "Upper Cabinet Side Panel", "material": "18mm BWP Plywood", "length": 720, "width": 300, "quantity": 12, "grainDirection": "vertical", "edgeBanding": ["front"], "laminate": "white_gloss"}, + {"partName": "Upper Cabinet Top/Bottom", "material": "18mm BWP Plywood", "length": 564, "width": 300, "quantity": 12, "grainDirection": "horizontal", "edgeBanding": ["front"], "laminate": "white_gloss"}, + {"partName": "Upper Cabinet Shelf", "material": "18mm BWP Plywood", "length": 560, "width": 280, "quantity": 6, "grainDirection": "horizontal", "edgeBanding": ["front"], "laminate": "white_gloss"}, + {"partName": "Lower Cabinet Side Panel", "material": "18mm BWP Plywood", "length": 820, "width": 560, "quantity": 16, "grainDirection": "vertical", "edgeBanding": ["front"], "laminate": "grey_matte"}, + {"partName": "Lower Cabinet Top/Bottom", "material": "18mm BWP Plywood", "length": 564, "width": 560, "quantity": 16, "grainDirection": "horizontal", "edgeBanding": ["front"], "laminate": "grey_matte"}, + {"partName": "Drawer Front", "material": "18mm BWP Plywood", "length": 596, "width": 200, "quantity": 16, "grainDirection": "horizontal", "edgeBanding": ["all"], "laminate": "grey_matte"}, + {"partName": "Drawer Box Side", "material": "12mm Plywood", "length": 500, "width": 150, "quantity": 32, "grainDirection": "horizontal", "edgeBanding": [], "laminate": "none"}, + {"partName": "Drawer Box Front/Back", "material": "12mm Plywood", "length": 536, "width": 150, "quantity": 32, "grainDirection": "horizontal", "edgeBanding": [], "laminate": "none"}, + {"partName": "Drawer Box Bottom", "material": "6mm Plywood", "length": 536, "width": 500, "quantity": 16, "grainDirection": "any", "edgeBanding": [], "laminate": "none"} + ]', + '[ + {"type": "hinge_soft_close", "quantity": 28, "brand": "Hettich"}, + {"type": "drawer_channel_telescopic_500mm", "quantity": 16, "brand": "Hettich"}, + {"type": "handle_bar_160mm", "quantity": 22, "brand": "Hafele"}, + {"type": "shelf_support_pin", "quantity": 24, "brand": "Generic"} + ]', + '{"sheets": [{"sheetId": 1, "material": "18mm BWP Plywood", "sheetSize": {"l": 2440, "w": 1220}, "panels": [{"partName": "Lower Cabinet Side Panel", "x": 0, "y": 0, "rotated": false}, {"partName": "Lower Cabinet Side Panel", "x": 820, "y": 0, "rotated": false}], "utilizationPercent": 87.3}, {"sheetId": 2, "material": "18mm BWP Plywood", "sheetSize": {"l": 2440, "w": 1220}, "panels": [], "utilizationPercent": 91.1}], "totalSheets18mm": 7, "totalSheets12mm": 3, "totalSheets6mm": 2}', + 12, + 4.8, + NOW() - INTERVAL '35 days' +); + +-- ---- MEP Calculations ---- + +-- Mumbai Living Room Electrical +INSERT INTO mep_calculations (id, design_variant_id, job_id, calc_type, result, standards_cited, created_at) +VALUES ( + 'mep_mum_elec_001', + 'dv_mum_liv_modern_001', + NULL, + 'electrical', + '{ + "loadSummary": {"totalLoad_watts": 2850, "circuitBreaker_amps": 20, "wireGauge": "2.5 sqmm"}, + "circuits": [ + {"name": "Lighting Circuit", "load_watts": 450, "breaker_amps": 6, "wireGauge": "1.5 sqmm", "outlets": ["6x recessed downlight", "1x pendant"]}, + {"name": "Power Circuit 1", "load_watts": 1200, "breaker_amps": 16, "wireGauge": "2.5 sqmm", "outlets": ["4x 15A socket"]}, + {"name": "Power Circuit 2 (AC)", "load_watts": 1200, "breaker_amps": 16, "wireGauge": "4 sqmm", "outlets": ["1x AC dedicated", "2x 15A socket"]} + ], + "switchLayout": [ + {"location": "entry_door", "switches": ["main_lights", "accent_lights", "fan_regulator"]}, + {"location": "balcony_door", "switches": ["balcony_light"]} + ] + }', + '["IS 732:2019 - Wiring installations", "NEC 2020 Article 210 - Branch Circuits", "IS 3043 - Earthing"]', + NOW() - INTERVAL '36 days' +); + +-- Mumbai Kitchen Plumbing +INSERT INTO mep_calculations (id, design_variant_id, job_id, calc_type, result, standards_cited, created_at) +VALUES ( + 'mep_mum_plumb_001', + 'dv_mum_kit_modern_001', + NULL, + 'plumbing', + '{ + "fixtureUnits": {"sink_double": 3, "dishwasher": 2, "total": 5}, + "supplyPipe": {"coldWater": "20mm CPVC", "hotWater": "20mm CPVC"}, + "drainPipe": {"size": "50mm PVC", "slope": "1:40", "trapType": "P-trap"}, + "ventPipe": {"size": "40mm PVC", "connectTo": "main_vent_stack"}, + "waterHeater": {"type": "instant", "capacity_litres": 3, "location": "under_sink"} + }', + '["IPC 2021 Table 709.1 - Fixture Units", "IS 2065 - CPVC Pipes", "IS 1172 - Water Supply Code"]', + NOW() - INTERVAL '35 days' +); diff --git a/sampledb/006_catalogue.sql b/sampledb/006_catalogue.sql new file mode 100644 index 0000000..62d13ea --- /dev/null +++ b/sampledb/006_catalogue.sql @@ -0,0 +1,69 @@ +-- ============================================================================ +-- 006_catalogue.sql — Categories, Vendors, Products, Prices +-- ============================================================================ + +-- ---- Categories ---- + +INSERT INTO categories (id, name, slug, description, parent_id, icon, sort_order, is_active, product_count, created_at, updated_at) +VALUES + ('cat_furniture', 'Furniture', 'furniture', 'Living, bedroom, and office furniture', NULL, 'sofa', 1, true, 4, NOW() - INTERVAL '120 days', NOW()), + ('cat_flooring', 'Flooring', 'flooring', 'Tiles, wood, marble, and vinyl flooring', NULL, 'grid', 2, true, 3, NOW() - INTERVAL '120 days', NOW()), + ('cat_lighting', 'Lighting', 'lighting', 'Ceiling, wall, floor, and task lighting', NULL, 'lightbulb', 3, true, 2, NOW() - INTERVAL '120 days', NOW()), + ('cat_kitchen', 'Kitchen', 'kitchen', 'Kitchen cabinets, countertops, and appliances', NULL, 'utensils', 4, true, 3, NOW() - INTERVAL '120 days', NOW()), + ('cat_bathroom', 'Bathroom', 'bathroom', 'Bathroom fixtures, tiles, and accessories', NULL, 'bath', 5, true, 0, NOW() - INTERVAL '120 days', NOW()), + ('cat_hardware', 'Hardware & Fittings', 'hardware', 'Cabinet hardware, hinges, handles, and fittings', NULL, 'wrench', 6, true, 0, NOW() - INTERVAL '120 days', NOW()), + ('cat_paint', 'Paint & Finishes', 'paint', 'Wall paints, wood finishes, and primers', NULL, 'paintbrush', 7, true, 0, NOW() - INTERVAL '120 days', NOW()); + +-- ---- Vendors ---- + +INSERT INTO vendors (id, name, code, description, website, contact_email, contact_phone, phone, address, city, state, country, gst_number, payment_terms, rating, is_active, product_count, created_at) +VALUES + ('vnd_kajaria', 'Kajaria Ceramics', 'KAJ', 'India largest manufacturer of ceramic and vitrified tiles', 'https://www.kajariaceramics.com', 'sales@kajaria.com', '+91-11-26511231', '+91-11-26511231', 'Kajaria House, A-1 Sector 136, Noida', 'Noida', 'UP', 'IN', '09AAACK1234A1Z5', 'Net 30', 4.3, true, 3, NOW() - INTERVAL '120 days'), + ('vnd_hettich', 'Hettich India', 'HET', 'Premium furniture fittings and hardware', 'https://www.hettich.com', 'info@hettich.com', '+91-20-67290100', '+91-20-67290100', 'Raisoni Industrial Park, Hinjewadi', 'Pune', 'MH', 'IN', '27AABCH5678B1Z2', 'Net 45', 4.7, true, 2, NOW() - INTERVAL '120 days'), + ('vnd_philips', 'Philips Lighting India', 'PHI', 'LED lighting solutions for home and commercial', 'https://www.lighting.philips.co.in', 'contact@philips.com', '+91-124-4929000', '+91-124-4929000', 'DLF Cyber City, Gurugram', 'Gurugram', 'HR', 'IN', '06AAACR9012C1Z8', 'Net 30', 4.5, true, 2, NOW() - INTERVAL '120 days'), + ('vnd_godrej', 'Godrej Interio', 'GOD', 'Furniture and modular solutions', 'https://www.godrejinterio.com', 'care@godrejinterio.com', '+91-22-67961000', '+91-22-67961000', 'Godrej One, Vikhroli', 'Mumbai', 'MH', 'IN', '27AAACG3456D1Z1', 'Net 30', 4.1, true, 4, NOW() - INTERVAL '120 days'); + +-- ---- Products ---- + +-- Furniture +INSERT INTO products (id, name, description, brand, category, category_id, subcategory, vendor_id, sku, status, unit, image_url, specifications, dimensions, weight_kg, material, finish, color, min_price, max_price, created_at, updated_at) +VALUES + ('prod_sofa_001', '3-Seater Fabric Sofa - Sloane', 'Contemporary 3-seater sofa with solid wood frame and high-density foam cushions. Removable fabric covers.', 'Godrej Interio', 'furniture', 'cat_furniture', 'sofa', 'vnd_godrej', 'GI-SOFA-SLN-GRY', 'active', 'piece', NULL, '{"frame": "sheesham_wood", "foam_density": "40kg/m3", "fabric": "polyester_blend", "washable_covers": true}', '{"length_mm": 2200, "width_mm": 900, "height_mm": 850}', 65, 'fabric_wood', 'upholstered', 'grey', 42000, 48000, NOW() - INTERVAL '90 days', NOW()), + ('prod_table_001', 'Walnut Coffee Table - Oslo', 'Solid sheesham wood coffee table with walnut finish. Clean lines and tapered legs.', 'Godrej Interio', 'furniture', 'cat_furniture', 'coffee_table', 'vnd_godrej', 'GI-CT-OSLO-WAL', 'active', 'piece', NULL, '{"wood": "sheesham", "joints": "mortise_and_tenon", "legStyle": "tapered"}', '{"length_mm": 1200, "width_mm": 600, "height_mm": 450}', 28, 'solid_wood', 'walnut', 'walnut', 11000, 14000, NOW() - INTERVAL '90 days', NOW()), + ('prod_bookshelf_001', 'Ladder Bookshelf 5-Tier', '5-tier open bookshelf with ladder design. Engineered wood with oak veneer.', 'Godrej Interio', 'furniture', 'cat_furniture', 'bookshelf', 'vnd_godrej', 'GI-BS-LAD5-OAK', 'active', 'piece', NULL, '{"tiers": 5, "maxLoadPerShelf_kg": 15}', '{"length_mm": 800, "width_mm": 300, "height_mm": 1800}', 22, 'engineered_wood', 'oak_veneer', 'natural_oak', 8500, 12000, NOW() - INTERVAL '90 days', NOW()), + ('prod_tvunit_001', 'Floating TV Unit 1800mm - Matte White', 'Wall-mounted TV unit with cable management, 2 drawers, and open shelf.', 'Godrej Interio', 'furniture', 'cat_furniture', 'tv_unit', 'vnd_godrej', 'GI-TV-FLT-WHT', 'active', 'piece', NULL, '{"mounting": "wall_mount", "cableManagement": true, "drawers": 2, "maxTVSize_inch": 65}', '{"length_mm": 1800, "width_mm": 400, "height_mm": 500}', 35, 'engineered_wood', 'matte_laminate', 'white', 18000, 24000, NOW() - INTERVAL '90 days', NOW()); + +-- Flooring +INSERT INTO products (id, name, description, brand, category, category_id, subcategory, vendor_id, sku, status, unit, image_url, specifications, dimensions, weight_kg, material, finish, color, min_price, max_price, created_at, updated_at) +VALUES + ('prod_tile_001', 'Vitrified Floor Tile 600x600 - Pearl Grey', 'Double-charged vitrified tile. Scratch resistant, low water absorption.', 'Kajaria', 'flooring', 'cat_flooring', 'vitrified_tile', 'vnd_kajaria', 'KAJ-VT-600-PG', 'active', 'sqm', NULL, '{"type": "double_charged", "waterAbsorption": "<0.5%", "scratchResistance": "Mohs 7", "slipResistance": "R10"}', '{"length_mm": 600, "width_mm": 600, "height_mm": 10}', 18, 'vitrified', 'polished', 'pearl_grey', 750, 950, NOW() - INTERVAL '90 days', NOW()), + ('prod_tile_002', 'Subway Wall Tile 75x150 - Glossy White', 'Classic subway tile for kitchen backsplash and bathroom walls.', 'Kajaria', 'flooring', 'cat_flooring', 'wall_tile', 'vnd_kajaria', 'KAJ-SW-75-GW', 'active', 'sqm', NULL, '{"type": "ceramic", "waterAbsorption": "<3%", "finish": "glossy"}', '{"length_mm": 150, "width_mm": 75, "height_mm": 8}', 14, 'ceramic', 'glossy', 'white', 900, 1300, NOW() - INTERVAL '90 days', NOW()), + ('prod_wood_001', 'Engineered Oak Flooring - Natural Matte', 'European oak top layer (3mm) on HDF core. Click-lock installation.', 'Kajaria', 'flooring', 'cat_flooring', 'engineered_wood', 'vnd_kajaria', 'KAJ-EW-OAK-NM', 'active', 'sqm', NULL, '{"topLayer": "European oak 3mm", "core": "HDF", "installation": "click_lock", "underfloorHeating": true}', '{"length_mm": 1200, "width_mm": 190, "height_mm": 14}', 9, 'engineered_wood', 'matte', 'natural_oak', 3200, 4500, NOW() - INTERVAL '90 days', NOW()); + +-- Lighting +INSERT INTO products (id, name, description, brand, category, category_id, subcategory, vendor_id, sku, status, unit, image_url, specifications, dimensions, weight_kg, material, finish, color, min_price, max_price, created_at, updated_at) +VALUES + ('prod_light_001', 'AstraSpot LED Recessed Downlight 12W', 'Slim profile recessed LED downlight. 3000K warm white, dimmable.', 'Philips', 'lighting', 'cat_lighting', 'recessed', 'vnd_philips', 'PH-LED-AS-12W', 'active', 'piece', NULL, '{"wattage": 12, "lumens": 1100, "colorTemp_K": 3000, "dimmable": true, "cutoutSize_mm": 150, "IP": "IP44"}', '{"length_mm": 175, "width_mm": 175, "height_mm": 45}', 0.3, 'aluminium', 'white', 'white', 550, 750, NOW() - INTERVAL '90 days', NOW()), + ('prod_light_002', 'Geometric Pendant Light - Brass', 'Modern geometric pendant with brushed brass frame and frosted glass.', 'Philips', 'lighting', 'cat_lighting', 'pendant', 'vnd_philips', 'PH-PEN-GEO-BR', 'active', 'piece', NULL, '{"maxWattage": 60, "bulbType": "E27", "cableLength_m": 1.5, "adjustable": true}', '{"length_mm": 350, "width_mm": 350, "height_mm": 400}', 2.5, 'brass_glass', 'brushed_brass', 'brass', 7500, 9500, NOW() - INTERVAL '90 days', NOW()); + +-- Kitchen +INSERT INTO products (id, name, description, brand, category, category_id, subcategory, vendor_id, sku, status, unit, image_url, specifications, dimensions, weight_kg, material, finish, color, min_price, max_price, created_at, updated_at) +VALUES + ('prod_hinge_001', 'Soft-Close Cabinet Hinge 110deg', 'Full overlay soft-close hinge with integrated damper. 110-degree opening.', 'Hettich', 'hardware', 'cat_hardware', 'hinge', 'vnd_hettich', 'HET-SC-110-NI', 'active', 'pair', NULL, '{"openingAngle": 110, "type": "full_overlay", "softClose": true, "mounting": "clip_on"}', NULL, 0.15, 'steel', 'nickel_plated', 'nickel', 220, 300, NOW() - INTERVAL '90 days', NOW()), + ('prod_channel_001', 'Quadro Telescopic Drawer Channel 500mm', 'Full extension telescopic drawer slide with soft-close. 35kg load capacity.', 'Hettich', 'hardware', 'cat_hardware', 'drawer_channel', 'vnd_hettich', 'HET-QD-500-SC', 'active', 'pair', NULL, '{"extension": "full", "softClose": true, "loadCapacity_kg": 35, "length_mm": 500}', NULL, 0.8, 'steel', 'zinc_plated', 'silver', 480, 620, NOW() - INTERVAL '90 days', NOW()), + ('prod_quartz_001', 'Quartz Countertop - Calacatta White', 'Engineered quartz slab with marble-like veining. Stain and scratch resistant.', 'Kajaria', 'kitchen', 'cat_kitchen', 'countertop', 'vnd_kajaria', 'KAJ-QZ-CAL-WH', 'active', 'sqm', NULL, '{"type": "engineered_quartz", "hardness": "Mohs 7", "stainResistant": true, "heatResistant": true}', '{"length_mm": 3000, "width_mm": 1400, "height_mm": 20}', 55, 'quartz', 'polished', 'calacatta_white', 10000, 14000, NOW() - INTERVAL '90 days', NOW()); + +-- ---- Product Prices (multi-vendor) ---- + +INSERT INTO product_prices (id, product_id, vendor_id, price, currency, unit, valid_from, valid_to) +VALUES + ('pp_sofa_god', 'prod_sofa_001', 'vnd_godrej', 45000, 'INR', 'piece', NOW() - INTERVAL '60 days', NOW() + INTERVAL '120 days'), + ('pp_table_god', 'prod_table_001', 'vnd_godrej', 12500, 'INR', 'piece', NOW() - INTERVAL '60 days', NOW() + INTERVAL '120 days'), + ('pp_tile_kaj', 'prod_tile_001', 'vnd_kajaria', 850, 'INR', 'sqm', NOW() - INTERVAL '60 days', NOW() + INTERVAL '90 days'), + ('pp_tile2_kaj', 'prod_tile_002', 'vnd_kajaria', 1100, 'INR', 'sqm', NOW() - INTERVAL '60 days', NOW() + INTERVAL '90 days'), + ('pp_wood_kaj', 'prod_wood_001', 'vnd_kajaria', 3800, 'INR', 'sqm', NOW() - INTERVAL '60 days', NOW() + INTERVAL '90 days'), + ('pp_light1_phi', 'prod_light_001', 'vnd_philips', 650, 'INR', 'piece', NOW() - INTERVAL '60 days', NOW() + INTERVAL '90 days'), + ('pp_light2_phi', 'prod_light_002', 'vnd_philips', 8500, 'INR', 'piece', NOW() - INTERVAL '60 days', NOW() + INTERVAL '90 days'), + ('pp_hinge_het', 'prod_hinge_001', 'vnd_hettich', 250, 'INR', 'pair', NOW() - INTERVAL '60 days', NOW() + INTERVAL '180 days'), + ('pp_channel_het', 'prod_channel_001', 'vnd_hettich', 550, 'INR', 'pair', NOW() - INTERVAL '60 days', NOW() + INTERVAL '180 days'), + ('pp_quartz_kaj', 'prod_quartz_001', 'vnd_kajaria', 12000, 'INR', 'sqm', NOW() - INTERVAL '60 days', NOW() + INTERVAL '90 days'); diff --git a/sampledb/007_schedule_milestones.sql b/sampledb/007_schedule_milestones.sql new file mode 100644 index 0000000..459ec36 --- /dev/null +++ b/sampledb/007_schedule_milestones.sql @@ -0,0 +1,61 @@ +-- ============================================================================ +-- 007_schedule_milestones.sql — Schedules, Milestones, Site Logs, Change Orders +-- ============================================================================ + +-- ---- Schedule for Mumbai Apartment ---- + +INSERT INTO schedules (id, project_id, job_id, tasks, critical_path, start_date, end_date, metadata, created_at, updated_at) +VALUES ( + 'sch_mum_001', + 'prj_mumbai_001', + NULL, + '[ + {"id": "T1", "name": "Demolition & Clearing", "startDay": 0, "durationDays": 3, "dependencies": [], "trade": "general", "status": "completed"}, + {"id": "T2", "name": "Electrical Rough-In", "startDay": 3, "durationDays": 5, "dependencies": ["T1"], "trade": "electrical", "status": "completed"}, + {"id": "T3", "name": "Plumbing Rough-In", "startDay": 3, "durationDays": 4, "dependencies": ["T1"], "trade": "plumbing", "status": "completed"}, + {"id": "T4", "name": "Masonry & Wall Modifications", "startDay": 7, "durationDays": 5, "dependencies": ["T2", "T3"], "trade": "masonry", "status": "completed"}, + {"id": "T5", "name": "Waterproofing (Bathroom)", "startDay": 12, "durationDays": 3, "dependencies": ["T4"], "trade": "waterproofing", "status": "in_progress"}, + {"id": "T6", "name": "Tiling (Bathroom + Kitchen)", "startDay": 15, "durationDays": 7, "dependencies": ["T5"], "trade": "tiling", "status": "pending"}, + {"id": "T7", "name": "Kitchen Cabinet Installation", "startDay": 15, "durationDays": 5, "dependencies": ["T4"], "trade": "carpentry", "status": "pending"}, + {"id": "T8", "name": "Countertop Installation", "startDay": 20, "durationDays": 2, "dependencies": ["T7"], "trade": "stone_work", "status": "pending"}, + {"id": "T9", "name": "Flooring (Living + Bedrooms)", "startDay": 15, "durationDays": 5, "dependencies": ["T4"], "trade": "tiling", "status": "pending"}, + {"id": "T10", "name": "Electrical Fixtures", "startDay": 22, "durationDays": 3, "dependencies": ["T6", "T9"], "trade": "electrical", "status": "pending"}, + {"id": "T11", "name": "Plumbing Fixtures", "startDay": 22, "durationDays": 2, "dependencies": ["T6"], "trade": "plumbing", "status": "pending"}, + {"id": "T12", "name": "Painting", "startDay": 25, "durationDays": 5, "dependencies": ["T10", "T11"], "trade": "painting", "status": "pending"}, + {"id": "T13", "name": "Furniture Assembly & Placement", "startDay": 30, "durationDays": 3, "dependencies": ["T12"], "trade": "carpentry", "status": "pending"}, + {"id": "T14", "name": "Final Cleaning & Touch-up", "startDay": 33, "durationDays": 2, "dependencies": ["T13"], "trade": "general", "status": "pending"}, + {"id": "T15", "name": "QA Inspection & Handover", "startDay": 35, "durationDays": 1, "dependencies": ["T14"], "trade": "general", "status": "pending"} + ]', + '["T1", "T2", "T4", "T5", "T6", "T10", "T12", "T13", "T14", "T15"]', + NOW() - INTERVAL '30 days', + NOW() + INTERVAL '6 days', + '{"totalDays": 36, "criticalPathDays": 36, "tradesInvolved": 8}', + NOW() - INTERVAL '30 days', + NOW() - INTERVAL '1 day' +); + +-- ---- Milestones ---- + +INSERT INTO milestones (id, schedule_id, name, description, due_date, completed_date, status, payment_linked, created_at) +VALUES + ('ms_mum_001', 'sch_mum_001', 'Demolition Complete', 'All demolition work and site clearing finished', NOW() - INTERVAL '27 days', NOW() - INTERVAL '27 days', 'completed', true, NOW() - INTERVAL '30 days'), + ('ms_mum_002', 'sch_mum_001', 'Rough-In Complete', 'Electrical and plumbing rough-in done. Ready for wall close.', NOW() - INTERVAL '20 days', NOW() - INTERVAL '19 days', 'completed', true, NOW() - INTERVAL '30 days'), + ('ms_mum_003', 'sch_mum_001', 'Tiling & Kitchen Complete', 'All tiling done, kitchen cabinets and countertop installed', NOW() + INTERVAL '2 days', NULL, 'in_progress', true, NOW() - INTERVAL '30 days'), + ('ms_mum_004', 'sch_mum_001', 'Final Handover', 'Full QA pass, cleaning done, keys handed over', NOW() + INTERVAL '6 days', NULL, 'pending', true, NOW() - INTERVAL '30 days'); + +-- ---- Site Logs ---- + +INSERT INTO site_logs (id, project_id, user_id, date, title, notes, weather, workers_on_site, photo_keys, tags, created_at) +VALUES + ('sl_mum_001', 'prj_mumbai_001', 'usr_alice_001', NOW() - INTERVAL '27 days', 'Demolition Day 3 - Complete', 'All old flooring removed. Kitchen cabinets dismantled. Walls prepared for replastering. Debris cleared.', 'sunny', 6, '["site_logs/prj_mumbai_001/demo_day3_1.jpg", "site_logs/prj_mumbai_001/demo_day3_2.jpg"]', '["demolition", "milestone"]', NOW() - INTERVAL '27 days'), + ('sl_mum_002', 'prj_mumbai_001', 'usr_alice_001', NOW() - INTERVAL '22 days', 'Electrical Rough-In Progress', 'Conduit laid for living room and master bedroom. 3 new circuits planned. Waiting for switch plate positions confirmation.', 'cloudy', 3, '["site_logs/prj_mumbai_001/elec_roughin_1.jpg"]', '["electrical", "rough_in"]', NOW() - INTERVAL '22 days'), + ('sl_mum_003', 'prj_mumbai_001', 'usr_alice_001', NOW() - INTERVAL '18 days', 'Plumbing & Masonry Done', 'All plumbing rough-in complete. Kitchen waste pipe relocated. Masonry modifications done for bathroom niche.', 'rainy', 5, '["site_logs/prj_mumbai_001/plumb_done_1.jpg", "site_logs/prj_mumbai_001/masonry_done_1.jpg"]', '["plumbing", "masonry", "milestone"]', NOW() - INTERVAL '18 days'), + ('sl_mum_004', 'prj_mumbai_001', 'usr_alice_001', NOW() - INTERVAL '2 days', 'Waterproofing In Progress', 'Bathroom waterproofing 60% done. Applied 2 coats of polymer-modified cementitious coating. Flood test scheduled tomorrow.', 'sunny', 3, '["site_logs/prj_mumbai_001/waterproof_1.jpg"]', '["waterproofing", "bathroom"]', NOW() - INTERVAL '2 days'); + +-- ---- Change Orders ---- + +INSERT INTO change_orders (id, project_id, user_id, title, description, status, cost_impact, time_impact_days, approved_by, approved_at, created_at) +VALUES + ('co_mum_001', 'prj_mumbai_001', 'usr_alice_001', 'Upgrade Kitchen Countertop to Quartz', 'Client requested upgrade from granite to Calacatta White quartz countertop. Original spec was black granite at INR 3500/sqm, new spec is quartz at INR 12000/sqm.', 'approved', 38250, 1, 'usr_alice_001', NOW() - INTERVAL '25 days', NOW() - INTERVAL '26 days'), + ('co_mum_002', 'prj_mumbai_001', 'usr_alice_001', 'Add Accent Wall in Living Room', 'Client wants a textured stone veneer accent wall on the TV wall (3m x 2.8m). Requires additional material and 2 days labor.', 'proposed', 28000, 2, NULL, NULL, NOW() - INTERVAL '5 days'), + ('co_mum_003', 'prj_mumbai_001', 'usr_alice_001', 'Relocate AC Outdoor Unit', 'Building society requires AC outdoor unit on balcony instead of external wall. Additional piping needed.', 'approved', 5500, 0, 'usr_alice_001', NOW() - INTERVAL '20 days', NOW() - INTERVAL '21 days'); diff --git a/sampledb/008_procurement_payments.sql b/sampledb/008_procurement_payments.sql new file mode 100644 index 0000000..b89811e --- /dev/null +++ b/sampledb/008_procurement_payments.sql @@ -0,0 +1,130 @@ +-- ============================================================================ +-- 008_procurement_payments.sql — Purchase Orders, Payments, Invoices, Deliveries +-- ============================================================================ + +-- ---- Purchase Orders ---- + +-- PO for flooring tiles (delivered) +INSERT INTO purchase_orders (id, project_id, vendor_id, status, items, total_amount, currency, expected_delivery, actual_delivery, notes, created_at, updated_at) +VALUES ( + 'po_mum_tiles_001', + 'prj_mumbai_001', + 'vnd_kajaria', + 'delivered', + '[ + {"productId": "prod_tile_001", "name": "Vitrified Floor Tile 600x600 Pearl Grey", "quantity": 40, "unit": "sqm", "unitPrice": 850}, + {"productId": "prod_tile_002", "name": "Subway Wall Tile 75x150 White", "quantity": 8, "unit": "sqm", "unitPrice": 1100} + ]', + 42800, + 'INR', + NOW() - INTERVAL '15 days', + NOW() - INTERVAL '14 days', + 'Delivery to site gate. Contact: Site supervisor Ramesh - 9876543210', + NOW() - INTERVAL '25 days', + NOW() - INTERVAL '14 days' +); + +-- PO for kitchen hardware (confirmed, awaiting delivery) +INSERT INTO purchase_orders (id, project_id, vendor_id, status, items, total_amount, currency, expected_delivery, actual_delivery, notes, created_at, updated_at) +VALUES ( + 'po_mum_hardware_001', + 'prj_mumbai_001', + 'vnd_hettich', + 'confirmed', + '[ + {"productId": "prod_hinge_001", "name": "Soft-Close Cabinet Hinge 110deg", "quantity": 28, "unit": "pair", "unitPrice": 250}, + {"productId": "prod_channel_001", "name": "Quadro Telescopic Drawer Channel 500mm", "quantity": 16, "unit": "pair", "unitPrice": 550} + ]', + 15800, + 'INR', + NOW() + INTERVAL '3 days', + NULL, + 'Ship to site address. Include installation manual.', + NOW() - INTERVAL '10 days', + NOW() - INTERVAL '8 days' +); + +-- PO for lighting (draft) +INSERT INTO purchase_orders (id, project_id, vendor_id, status, items, total_amount, currency, expected_delivery, actual_delivery, notes, created_at, updated_at) +VALUES ( + 'po_mum_lights_001', + 'prj_mumbai_001', + 'vnd_philips', + 'draft', + '[ + {"productId": "prod_light_001", "name": "AstraSpot LED Recessed Downlight 12W", "quantity": 12, "unit": "piece", "unitPrice": 650}, + {"productId": "prod_light_002", "name": "Geometric Pendant Light Brass", "quantity": 1, "unit": "piece", "unitPrice": 8500} + ]', + 16300, + 'INR', + NOW() + INTERVAL '10 days', + NULL, + NULL, + NOW() - INTERVAL '3 days', + NOW() - INTERVAL '3 days' +); + +-- ---- Payments ---- + +-- Milestone 1 payment (completed) +INSERT INTO payments (id, project_id, milestone_id, amount, currency, status, payment_provider, external_id, metadata, paid_at, created_at) +VALUES ( + 'pay_mum_001', + 'prj_mumbai_001', + 'ms_mum_001', + 75000, + 'INR', + 'completed', + 'razorpay', + 'pay_rzp_mum_001_abc123', + '{"description": "Demolition milestone payment", "method": "upi"}', + NOW() - INTERVAL '26 days', + NOW() - INTERVAL '27 days' +); + +-- Milestone 2 payment (completed) +INSERT INTO payments (id, project_id, milestone_id, amount, currency, status, payment_provider, external_id, metadata, paid_at, created_at) +VALUES ( + 'pay_mum_002', + 'prj_mumbai_001', + 'ms_mum_002', + 150000, + 'INR', + 'completed', + 'razorpay', + 'pay_rzp_mum_002_def456', + '{"description": "Rough-in completion payment", "method": "bank_transfer"}', + NOW() - INTERVAL '18 days', + NOW() - INTERVAL '19 days' +); + +-- Milestone 3 payment (pending) +INSERT INTO payments (id, project_id, milestone_id, amount, currency, status, payment_provider, external_id, metadata, paid_at, created_at) +VALUES ( + 'pay_mum_003', + 'prj_mumbai_001', + 'ms_mum_003', + 200000, + 'INR', + 'pending', + NULL, + NULL, + '{"description": "Tiling and kitchen completion payment"}', + NULL, + NOW() - INTERVAL '1 day' +); + +-- ---- Invoices ---- + +INSERT INTO invoices (id, project_id, purchase_order_id, invoice_number, amount, currency, status, due_date, paid_date, pdf_storage_key, created_at) +VALUES + ('inv_mum_001', 'prj_mumbai_001', 'po_mum_tiles_001', 'INV-2025-MUM-001', 42800, 'INR', 'paid', NOW() - INTERVAL '10 days', NOW() - INTERVAL '12 days', 'invoices/prj_mumbai_001/INV-2025-MUM-001.pdf', NOW() - INTERVAL '25 days'), + ('inv_mum_002', 'prj_mumbai_001', 'po_mum_hardware_001', 'INV-2025-MUM-002', 15800, 'INR', 'sent', NOW() + INTERVAL '15 days', NULL, 'invoices/prj_mumbai_001/INV-2025-MUM-002.pdf', NOW() - INTERVAL '8 days'), + ('inv_mum_003', 'prj_mumbai_001', NULL, 'INV-2025-MUM-003', 75000, 'INR', 'paid', NOW() - INTERVAL '20 days', NOW() - INTERVAL '26 days', NULL, NOW() - INTERVAL '27 days'); + +-- ---- Delivery Tracking ---- + +INSERT INTO delivery_tracking (id, project_id, purchase_order_id, vendor_name, description, status, tracking_number, estimated_delivery_date, actual_delivery_date, inspection_checklist, inspection_photo_keys, received_by, notes, created_at, updated_at) +VALUES + ('del_mum_001', 'prj_mumbai_001', 'po_mum_tiles_001', 'Kajaria Ceramics', 'Floor and wall tiles - 48 sqm total', 'inspected', 'KAJ-SHIP-2025-4521', NOW() - INTERVAL '15 days', NOW() - INTERVAL '14 days', '[{"item": "Quantity matches PO", "passed": true, "note": null}, {"item": "No chipped/broken tiles", "passed": true, "note": "2 tiles had minor edge chips, within 5% acceptable"}, {"item": "Correct shade/batch number", "passed": true, "note": "Batch: KAJ-PG-B2025-11"}, {"item": "Packaging intact", "passed": true, "note": null}]', '["deliveries/prj_mumbai_001/tiles_delivery_1.jpg", "deliveries/prj_mumbai_001/tiles_inspection_1.jpg"]', 'Ramesh (Site Supervisor)', 'Delivered on time. 2 minor chips noted but within tolerance.', NOW() - INTERVAL '25 days', NOW() - INTERVAL '14 days'), + ('del_mum_002', 'prj_mumbai_001', 'po_mum_hardware_001', 'Hettich India', 'Kitchen cabinet hardware - hinges and channels', 'dispatched', 'HET-DISP-2025-8834', NOW() + INTERVAL '3 days', NULL, NULL, NULL, NULL, 'Dispatched from Pune warehouse', NOW() - INTERVAL '10 days', NOW() - INTERVAL '1 day'); diff --git a/sampledb/009_collaboration.sql b/sampledb/009_collaboration.sql new file mode 100644 index 0000000..711d442 --- /dev/null +++ b/sampledb/009_collaboration.sql @@ -0,0 +1,84 @@ +-- ============================================================================ +-- 009_collaboration.sql — Comments, Approvals, Notifications, Threads +-- ============================================================================ + +-- ---- Comments ---- + +INSERT INTO comments (id, user_id, project_id, parent_id, target_type, target_id, content, resolved, created_at, updated_at) +VALUES + ('cmt_001', 'usr_alice_001', 'prj_mumbai_001', NULL, 'design_variant', 'dv_mum_liv_modern_001', 'Love the modern look! Can we make the sofa a lighter grey to match the curtains?', false, NOW() - INTERVAL '39 days', NOW() - INTERVAL '39 days'), + ('cmt_002', 'usr_bob_002', 'prj_mumbai_001', 'cmt_001', 'design_variant', 'dv_mum_liv_modern_001', 'The lighter grey would work well. I can update the BOM to reflect the fabric change. Estimated cost difference: +INR 2000.', false, NOW() - INTERVAL '38 days', NOW() - INTERVAL '38 days'), + ('cmt_003', 'usr_alice_001', 'prj_mumbai_001', NULL, 'room', 'room_mum_kitchen_003', 'The chimney position needs to be above the hob. Current placement shows it offset to the right.', true, NOW() - INTERVAL '36 days', NOW() - INTERVAL '35 days'), + ('cmt_004', 'usr_alice_001', 'prj_mumbai_001', NULL, 'bom', 'bom_mum_liv_001', 'Can we substitute the curtain rods with a ceiling track system? Would be cleaner look.', false, NOW() - INTERVAL '30 days', NOW() - INTERVAL '30 days'), + ('cmt_005', 'usr_bob_002', 'prj_delhi_003', NULL, 'design_variant', 'dv_del_liv_artdeco_001', 'Art deco chandelier sourced from Italy. Lead time is 6 weeks. Should we order now?', true, NOW() - INTERVAL '155 days', NOW() - INTERVAL '150 days'); + +-- ---- Approvals ---- + +INSERT INTO approvals (id, project_id, requested_by, target_type, target_id, status, reviewed_by, reviewed_at, notes, created_at) +VALUES + ('apr_001', 'prj_mumbai_001', 'usr_alice_001', 'design_variant', 'dv_mum_liv_modern_001', 'approved', 'usr_alice_001', NOW() - INTERVAL '38 days', 'Approved with minor color adjustment to sofa', NOW() - INTERVAL '39 days'), + ('apr_002', 'prj_mumbai_001', 'usr_alice_001', 'design_variant', 'dv_mum_kit_modern_001', 'approved', 'usr_alice_001', NOW() - INTERVAL '35 days', 'Approved. Chimney position corrected.', NOW() - INTERVAL '36 days'), + ('apr_003', 'prj_mumbai_001', 'usr_alice_001', 'bom', 'bom_mum_liv_001', 'approved', 'usr_alice_001', NOW() - INTERVAL '36 days', NULL, NOW() - INTERVAL '37 days'), + ('apr_004', 'prj_mumbai_001', 'usr_alice_001', 'schedule', 'sch_mum_001', 'approved', 'usr_alice_001', NOW() - INTERVAL '29 days', 'Timeline looks good. Start date confirmed.', NOW() - INTERVAL '30 days'), + ('apr_005', 'prj_blr_002', 'usr_alice_001', 'design_variant', 'dv_blr_liv_contemporary_001', 'pending', NULL, NULL, NULL, NOW() - INTERVAL '6 days'); + +-- ---- Notifications ---- + +INSERT INTO notifications (id, user_id, type, title, message, link, read, created_at) +VALUES + ('ntf_001', 'usr_alice_001', 'job_complete', 'Design generated', 'Your modern living room design is ready to view', '/project/prj_mumbai_001/designs/dv_mum_liv_modern_001', true, NOW() - INTERVAL '40 days'), + ('ntf_002', 'usr_alice_001', 'job_complete', 'BOM calculated', 'Bill of Materials for Modern Minimalist Living is ready. Total: INR 1,98,160', '/project/prj_mumbai_001/bom', true, NOW() - INTERVAL '38 days'), + ('ntf_003', 'usr_alice_001', 'job_complete', 'Drawings generated', '3 technical drawings are ready for your living room design', '/project/prj_mumbai_001/drawings', true, NOW() - INTERVAL '37 days'), + ('ntf_004', 'usr_alice_001', 'comment', 'New comment on design', 'Bob Kumar commented on Modern Minimalist Living design', '/project/prj_mumbai_001/designs/dv_mum_liv_modern_001', true, NOW() - INTERVAL '38 days'), + ('ntf_005', 'usr_alice_001', 'delivery', 'Tiles delivered', 'Kajaria tiles delivery has been received and inspected at site', '/project/prj_mumbai_001/deliveries', true, NOW() - INTERVAL '14 days'), + ('ntf_006', 'usr_alice_001', 'payment', 'Payment confirmed', 'INR 1,50,000 payment for Rough-In milestone confirmed via Razorpay', '/project/prj_mumbai_001/payments', true, NOW() - INTERVAL '18 days'), + ('ntf_007', 'usr_alice_001', 'approval', 'Approval pending', 'Bangalore Villa contemporary design is awaiting your approval', '/project/prj_blr_002/designs/dv_blr_liv_contemporary_001', false, NOW() - INTERVAL '6 days'), + ('ntf_008', 'usr_alice_001', 'job_complete', 'Cut list in progress', 'Kitchen cut list generation is 65% complete', '/project/prj_mumbai_001/cutlist', false, NOW() - INTERVAL '1 hour'), + ('ntf_009', 'usr_bob_002', 'comment', 'New comment', 'Alice Sharma commented on Mumbai living room design', '/project/prj_mumbai_001/designs/dv_mum_liv_modern_001', true, NOW() - INTERVAL '39 days'); + +-- ---- Collaboration Threads ---- + +INSERT INTO collaboration_threads (id, project_id, room_id, title, category, status, created_by, created_at, updated_at) +VALUES + ('thr_001', 'prj_mumbai_001', 'room_mum_living_001', 'Living Room Color Scheme Discussion', 'design_decision', 'resolved', 'usr_alice_001', NOW() - INTERVAL '39 days', NOW() - INTERVAL '35 days'), + ('thr_002', 'prj_mumbai_001', 'room_mum_kitchen_003', 'Kitchen Countertop Material Selection', 'change_request', 'resolved', 'usr_alice_001', NOW() - INTERVAL '28 days', NOW() - INTERVAL '25 days'), + ('thr_003', 'prj_mumbai_001', NULL, 'Overall Budget Review - Week 3', 'general', 'open', 'usr_alice_001', NOW() - INTERVAL '10 days', NOW() - INTERVAL '5 days'), + ('thr_004', 'prj_blr_002', 'room_blr_living_001', 'Villa Living Room Design Options', 'design_decision', 'open', 'usr_alice_001', NOW() - INTERVAL '7 days', NOW() - INTERVAL '6 days'); + +-- ---- Collaboration Messages ---- + +INSERT INTO collaboration_messages (id, thread_id, user_id, content, mentions, attachment_keys, is_decision, created_at) +VALUES + ('msg_001', 'thr_001', 'usr_alice_001', 'I am leaning towards warm neutral tones for the living room. The grey sofa with off-white walls feels right, but I want the accent wall to be warmer.', NULL, NULL, false, NOW() - INTERVAL '39 days'), + ('msg_002', 'thr_001', 'usr_bob_002', 'Warm neutrals work great with the modern style. I suggest changing the accent from navy (#2C3E50) to a warm charcoal (#3C3C3C) or even a terracotta (#C57B57).', NULL, NULL, false, NOW() - INTERVAL '38 days'), + ('msg_003', 'thr_001', 'usr_alice_001', 'DECISION: Going with warm charcoal accent wall. Updated in the design spec.', '["usr_bob_002"]', NULL, true, NOW() - INTERVAL '35 days'), + ('msg_004', 'thr_002', 'usr_alice_001', 'The granite countertop in the original design feels too dark for the white gloss cabinets. Can we switch to quartz?', NULL, NULL, false, NOW() - INTERVAL '28 days'), + ('msg_005', 'thr_002', 'usr_bob_002', 'Calacatta White quartz would be perfect. Cost increase is about INR 38,250 for 4.5 sqm. I have raised a change order for your approval.', NULL, NULL, false, NOW() - INTERVAL '27 days'), + ('msg_006', 'thr_002', 'usr_alice_001', 'DECISION: Approved the quartz upgrade. Change order CO-MUM-001 approved.', NULL, NULL, true, NOW() - INTERVAL '25 days'), + ('msg_007', 'thr_003', 'usr_alice_001', 'We are 3 weeks in. Current spend is INR 2,83,800 against a budget of INR 8,00,000. On track so far but the quartz upgrade added INR 38,250.', NULL, NULL, false, NOW() - INTERVAL '10 days'); + +-- ---- Style Preferences ---- + +INSERT INTO style_preferences (id, project_id, quiz_responses, detected_styles, budget_tier, color_preferences, mood_board_items, inspiration_urls, notes, created_at, updated_at) +VALUES ( + 'sp_mum_001', + 'prj_mumbai_001', + '[ + {"questionId": "room_usage", "selectedOption": "family_gathering", "imageUrl": null}, + {"questionId": "color_preference", "selectedOption": "warm_neutrals", "imageUrl": null}, + {"questionId": "material_preference", "selectedOption": "wood_and_fabric", "imageUrl": null}, + {"questionId": "budget_range", "selectedOption": "mid_range", "imageUrl": null}, + {"questionId": "inspiration", "selectedOption": "modern_warm", "imageUrl": null} + ]', + '[{"style": "modern", "score": 0.85}, {"style": "scandinavian", "score": 0.72}, {"style": "contemporary", "score": 0.65}, {"style": "japandi", "score": 0.45}]', + 'mid_range', + '{"palette": ["#F5F5F0", "#3C3C3C", "#D4C5A9", "#8B7355", "#FFFFFF"], "warm": true}', + '[ + {"imageUrl": "mood/warm_living_1.jpg", "caption": "Warm minimalist living room", "source": "Pinterest", "category": "living_room"}, + {"imageUrl": "mood/wood_kitchen_1.jpg", "caption": "Light wood kitchen with white counters", "source": "Pinterest", "category": "kitchen"} + ]', + '["https://www.pinterest.com/pin/example1", "https://www.pinterest.com/pin/example2"]', + 'Client prefers natural materials. Avoid too much metal or industrial elements.', + NOW() - INTERVAL '42 days', + NOW() - INTERVAL '42 days' +); diff --git a/sampledb/010_contractors.sql b/sampledb/010_contractors.sql new file mode 100644 index 0000000..fd60efc --- /dev/null +++ b/sampledb/010_contractors.sql @@ -0,0 +1,36 @@ +-- ============================================================================ +-- 010_contractors.sql — Contractors, Reviews, Assignments, Referrals +-- ============================================================================ + +-- ---- Contractors ---- + +INSERT INTO contractors (id, user_id, name, company_name, bio, website, profile_image_url, specializations, phone, email, address, city, state, rating, total_reviews, verified, years_experience, certifications, metadata, created_at, updated_at) +VALUES + ('ctr_raj_001', NULL, 'Rajesh Carpenter', 'Raj Woodworks', 'Master carpenter with 18 years of experience in modular kitchens, wardrobes, and custom furniture. Specializes in BWP plywood and premium laminates. IGBC certified for sustainable wood sourcing.', NULL, NULL, '["carpentry", "modular_kitchen", "wardrobe", "custom_furniture"]', '+91-9876543001', 'raj@rajwoodworks.in', '12 Furniture Hub, Jogeshwari', 'Mumbai', 'MH', 4.6, 23, true, 18, '["IGBC Certified", "Hettich Certified Installer"]', '{"completedProjects": 142, "avgProjectDays": 12}', NOW() - INTERVAL '365 days', NOW() - INTERVAL '5 days'), + ('ctr_suresh_002', NULL, 'Suresh Electricals', 'Suresh Electrical Solutions', 'Licensed electrician specializing in residential renovations. Expert in smart home wiring, LED installations, and complete rewiring. All work certified per IS 732:2019.', NULL, NULL, '["electrical", "smart_home", "led_installation", "rewiring"]', '+91-9876543002', 'suresh@gmail.com', '45 Electronics Market, Dadar', 'Mumbai', 'MH', 4.4, 18, true, 12, '["Licensed Electrician Class-A", "Legrand Certified", "Smart Home Specialist"]', '{"completedProjects": 98, "avgProjectDays": 5}', NOW() - INTERVAL '300 days', NOW() - INTERVAL '10 days'), + ('ctr_vikram_003', NULL, 'Vikram Plumbing', 'VP Plumbing Services', 'Experienced plumber handling residential and light commercial projects. Specializes in bathroom renovations, kitchen plumbing, and waterproofing. Uses branded fittings only.', NULL, NULL, '["plumbing", "waterproofing", "bathroom_renovation"]', '+91-9876543003', 'vikram.plumber@gmail.com', '78 Plumber Lane, Andheri East', 'Mumbai', 'MH', 4.2, 15, true, 10, '["Grohe Certified Installer", "Waterproofing Specialist"]', '{"completedProjects": 75, "avgProjectDays": 4}', NOW() - INTERVAL '250 days', NOW() - INTERVAL '15 days'); + +-- ---- Contractor Reviews ---- + +INSERT INTO contractor_reviews (id, contractor_id, user_id, project_id, rating, title, review, created_at) +VALUES + ('rev_raj_001', 'ctr_raj_001', 'usr_alice_001', 'prj_mumbai_001', 5, 'Excellent kitchen work', 'Rajesh did a fantastic job on our modular kitchen. Perfect cuts, clean finish, and completed on time. His team was professional and cleaned up after themselves daily.', NOW() - INTERVAL '5 days'), + ('rev_raj_002', 'ctr_raj_001', 'usr_bob_002', 'prj_delhi_003', 4, 'Good quality, slight delay', 'Quality of woodwork was excellent for our study bookshelves. There was a 2-day delay due to material availability but Rajesh communicated proactively about it.', NOW() - INTERVAL '20 days'), + ('rev_suresh_001', 'ctr_suresh_002', 'usr_alice_001', 'prj_mumbai_001', 4, 'Clean wiring work', 'Suresh rewired the entire apartment neatly. All conduits properly concealed. One socket placement was slightly off but he fixed it the same day.', NOW() - INTERVAL '18 days'), + ('rev_vikram_001', 'ctr_vikram_003', 'usr_alice_001', 'prj_mumbai_001', 5, 'Perfect waterproofing', 'Vikram handled all our bathroom plumbing and waterproofing. The flood test passed first time. Very methodical approach.', NOW() - INTERVAL '2 days'); + +-- ---- Contractor Assignments ---- + +INSERT INTO contractor_assignments (id, contractor_id, project_id, role, status, start_date, end_date, agreed_amount, currency, created_at) +VALUES + ('asg_raj_mum', 'ctr_raj_001', 'prj_mumbai_001', 'carpenter', 'active', NOW() - INTERVAL '15 days', NOW() + INTERVAL '10 days', 180000, 'INR', NOW() - INTERVAL '30 days'), + ('asg_suresh_mum', 'ctr_suresh_002', 'prj_mumbai_001', 'electrician', 'active', NOW() - INTERVAL '24 days', NOW() + INTERVAL '5 days', 45000, 'INR', NOW() - INTERVAL '30 days'), + ('asg_vikram_mum', 'ctr_vikram_003', 'prj_mumbai_001', 'plumber', 'active', NOW() - INTERVAL '24 days', NOW() + INTERVAL '5 days', 55000, 'INR', NOW() - INTERVAL '30 days'), + ('asg_raj_del', 'ctr_raj_001', 'prj_delhi_003', 'carpenter', 'completed', NOW() - INTERVAL '160 days', NOW() - INTERVAL '30 days', 350000, 'INR', NOW() - INTERVAL '170 days'); + +-- ---- Contractor Referrals ---- + +INSERT INTO contractor_referrals (id, referrer_user_id, contractor_id, referee_email, message, status, created_at) +VALUES + ('ref_001', 'usr_alice_001', 'ctr_raj_001', 'priya@example.com', 'Rajesh did amazing work on my Mumbai apartment kitchen. Highly recommend for any carpentry/modular kitchen work.', 'sent', NOW() - INTERVAL '3 days'), + ('ref_002', 'usr_bob_002', 'ctr_suresh_002', 'amit@example.com', 'Suresh is a reliable electrician. Did clean work on our penthouse project.', 'viewed', NOW() - INTERVAL '15 days'); diff --git a/sampledb/011_intelligence.sql b/sampledb/011_intelligence.sql new file mode 100644 index 0000000..c4f0f74 --- /dev/null +++ b/sampledb/011_intelligence.sql @@ -0,0 +1,145 @@ +-- ============================================================================ +-- 011_intelligence.sql — Predictions, Budget Scenarios, Sustainability +-- ============================================================================ + +-- ---- Cost Predictions ---- + +INSERT INTO cost_predictions (id, project_id, predicted_cost, confidence_low, confidence_high, risk_factors, breakdown, model_provider, input_snapshot, created_at) +VALUES ( + 'cpred_mum_001', + 'prj_mumbai_001', + 825000, + 720000, + 950000, + '[ + {"name": "Material price volatility", "impact": 45000, "probability": 0.3}, + {"name": "Change orders", "impact": 60000, "probability": 0.5}, + {"name": "Skilled labor shortage", "impact": 25000, "probability": 0.2}, + {"name": "Weather delays (monsoon)", "impact": 15000, "probability": 0.4} + ]', + '[ + {"category": "Materials", "amount": 380000}, + {"category": "Labor", "amount": 280000}, + {"category": "Appliances", "amount": 55000}, + {"category": "Design & Supervision", "amount": 60000}, + {"category": "Contingency (10%)", "amount": 50000} + ]', + 'openai', + '{"rooms": 4, "totalArea_sqm": 50, "style": "modern", "budgetTier": "mid_range"}', + NOW() - INTERVAL '35 days' +); + +INSERT INTO cost_predictions (id, project_id, predicted_cost, confidence_low, confidence_high, risk_factors, breakdown, model_provider, input_snapshot, created_at) +VALUES ( + 'cpred_blr_001', + 'prj_blr_002', + 2800000, + 2400000, + 3500000, + '[ + {"name": "Imported material lead times", "impact": 200000, "probability": 0.4}, + {"name": "Italian marble price fluctuation", "impact": 150000, "probability": 0.3}, + {"name": "Monsoon season overlap", "impact": 50000, "probability": 0.6} + ]', + '[ + {"category": "Materials", "amount": 1400000}, + {"category": "Labor", "amount": 750000}, + {"category": "Appliances", "amount": 300000}, + {"category": "Design & Supervision", "amount": 150000}, + {"category": "Contingency (10%)", "amount": 200000} + ]', + 'anthropic', + '{"rooms": 2, "totalArea_sqm": 65, "style": "contemporary", "budgetTier": "luxury"}', + NOW() - INTERVAL '5 days' +); + +-- ---- Timeline Predictions ---- + +INSERT INTO timeline_predictions (id, project_id, predicted_days, confidence_low, confidence_high, critical_risks, phase_breakdown, model_provider, input_snapshot, created_at) +VALUES ( + 'tpred_mum_001', + 'prj_mumbai_001', + 36, + 30, + 45, + '[ + {"name": "Bathroom waterproofing retest", "delayDays": 3, "mitigation": "Use premium waterproofing brand with guaranteed first-pass"}, + {"name": "Custom furniture delivery delay", "delayDays": 5, "mitigation": "Order early, have backup vendor"}, + {"name": "Monsoon interference", "delayDays": 4, "mitigation": "Schedule outdoor work before monsoon onset"} + ]', + '[ + {"phase": "Demolition & Prep", "days": 3, "dependencies": []}, + {"phase": "Rough-In (Electrical + Plumbing)", "days": 5, "dependencies": ["Demolition"]}, + {"phase": "Masonry & Structure", "days": 5, "dependencies": ["Rough-In"]}, + {"phase": "Waterproofing", "days": 3, "dependencies": ["Masonry"]}, + {"phase": "Tiling & Flooring", "days": 7, "dependencies": ["Waterproofing"]}, + {"phase": "Kitchen Installation", "days": 7, "dependencies": ["Masonry"]}, + {"phase": "Fixture Installation", "days": 3, "dependencies": ["Tiling"]}, + {"phase": "Painting", "days": 5, "dependencies": ["Fixtures"]}, + {"phase": "Furniture & Finishing", "days": 5, "dependencies": ["Painting"]}, + {"phase": "Cleaning & Handover", "days": 3, "dependencies": ["Furniture"]} + ]', + 'openai', + '{"rooms": 4, "totalArea_sqm": 50, "complexity": "medium"}', + NOW() - INTERVAL '35 days' +); + +-- ---- Budget Scenarios ---- + +INSERT INTO budget_scenarios (id, project_id, name, original_total_cost, optimized_total_cost, savings_amount, savings_percent, substitutions, constraints, status, created_at) +VALUES + ('bs_mum_001', 'prj_mumbai_001', 'Economy Alternatives', 825000, 680000, 145000, 17.6, '[ + {"original": "Quartz Countertop - Calacatta", "replacement": "Granite - Crystal White", "savings": 38250, "reason": "Similar aesthetic at 60% lower cost"}, + {"original": "Vitrified Tiles - Kajaria", "replacement": "Vitrified Tiles - Somany", "savings": 5100, "reason": "Same quality, local brand pricing"}, + {"original": "Hettich Soft-Close Hinges", "replacement": "Ebco Soft-Close Hinges", "savings": 2800, "reason": "Comparable quality at 40% less"}, + {"original": "Philips LED Downlights", "replacement": "Syska LED Downlights", "savings": 2400, "reason": "Same lumens output, budget brand"}, + {"original": "Godrej 3-Seater Sofa", "replacement": "Wakefit 3-Seater Sofa", "savings": 18000, "reason": "Similar build quality, online-first brand"}, + {"original": "Custom TV Unit", "replacement": "Semi-custom from Amazon Basics", "savings": 8000, "reason": "Standard size, no custom work needed"}, + {"original": "Asian Paints Royale", "replacement": "Asian Paints Apcolite", "savings": 1500, "reason": "Good quality for interior walls"}, + {"original": "Contractor labour premium", "replacement": "Standard contractor rate", "savings": 15000, "reason": "Use experienced but non-premium contractor"} + ]', '["maintain modern aesthetic", "no quality compromise on kitchen hardware"]', 'draft', NOW() - INTERVAL '30 days'), + ('bs_mum_002', 'prj_mumbai_001', 'Premium Upgrade', 825000, 1050000, -225000, -27.3, '[ + {"original": "Vitrified Tiles", "replacement": "Italian Marble - Statuario", "savings": -85000, "reason": "Premium marble flooring throughout"}, + {"original": "Fabric Sofa", "replacement": "Italian Leather Sofa", "savings": -65000, "reason": "Genuine Italian leather for luxury feel"}, + {"original": "Standard LED Downlights", "replacement": "Flos Architectural Lighting", "savings": -35000, "reason": "Designer lighting fixtures"}, + {"original": "Asian Paints", "replacement": "Benjamin Moore Premium", "savings": -12000, "reason": "Ultra-premium paint with better finish"}, + {"original": "Standard Curtains", "replacement": "Motorized Blinds - Somfy", "savings": -28000, "reason": "Smart home integration"} + ]', '["luxury upgrade", "smart home integration"]', 'draft', NOW() - INTERVAL '28 days'); + +-- ---- Sustainability Reports ---- + +INSERT INTO sustainability_reports (id, project_id, total_carbon_kg, material_carbon_kg, transport_carbon_kg, sustainability_score, leed_points, green_alternatives, model_provider, created_at) +VALUES ( + 'sust_mum_001', + 'prj_mumbai_001', + 2850, + 2100, + 750, + 62, + 8, + '[ + {"material": "BWP Plywood (standard)", "alternative": "FSC-Certified Plywood", "carbonSaved_kg": 180, "costDelta_inr": 12000, "reason": "Sustainably harvested wood, same durability"}, + {"material": "Vitrified Tiles", "alternative": "Recycled Content Tiles (Kajaria GreenVit)", "carbonSaved_kg": 250, "costDelta_inr": 3000, "reason": "30% recycled content, lower firing temperature"}, + {"material": "Standard Paint", "alternative": "Zero-VOC Paint (Asian Paints Royale Health Shield)", "carbonSaved_kg": 50, "costDelta_inr": 800, "reason": "Better indoor air quality, lower emissions"}, + {"material": "XPS Insulation", "alternative": "Recycled Cotton Insulation", "carbonSaved_kg": 120, "costDelta_inr": -500, "reason": "Made from recycled denim, lower embodied carbon"} + ]', + 'openai', + NOW() - INTERVAL '30 days' +); + +-- ---- Portfolios ---- + +INSERT INTO portfolios (id, user_id, name, description, created_at, updated_at) +VALUES ( + 'port_alice_001', + 'usr_alice_001', + 'My Home Projects', + 'Collection of all my residential interior design projects', + NOW() - INTERVAL '10 days', + NOW() - INTERVAL '2 days' +); + +INSERT INTO portfolio_projects (id, portfolio_id, project_id, sort_order) +VALUES + ('pp_001', 'port_alice_001', 'prj_mumbai_001', 1), + ('pp_002', 'port_alice_001', 'prj_blr_002', 2); diff --git a/sampledb/012_post_occupancy.sql b/sampledb/012_post_occupancy.sql new file mode 100644 index 0000000..12228cf --- /dev/null +++ b/sampledb/012_post_occupancy.sql @@ -0,0 +1,150 @@ +-- ============================================================================ +-- 012_post_occupancy.sql — Digital Twins, IoT, Maintenance, Warranties +-- ============================================================================ + +-- ---- Digital Twin (Delhi project - completed) ---- + +INSERT INTO digital_twins (id, project_id, model_storage_key, model_version, status, created_at, updated_at) +VALUES ( + 'dt_del_001', + 'prj_delhi_003', + 'digital_twins/prj_delhi_003/model_v3.gltf', + 3, + 'active', + NOW() - INTERVAL '15 days', + NOW() - INTERVAL '5 days' +); + +-- ---- IoT Devices ---- + +INSERT INTO iot_devices (id, digital_twin_id, name, device_type, position_json, room_id, status, created_at) +VALUES + ('iot_temp_001', 'dt_del_001', 'Living Room Temperature', 'temperature', '{"x": 5.0, "y": 2.0, "z": 3.5}', 'room_del_living_001', 'active', NOW() - INTERVAL '14 days'), + ('iot_humid_001', 'dt_del_001', 'Living Room Humidity', 'humidity', '{"x": 5.0, "y": 2.0, "z": 3.5}', 'room_del_living_001', 'active', NOW() - INTERVAL '14 days'), + ('iot_energy_001', 'dt_del_001', 'Main Energy Meter', 'energy', '{"x": 0.5, "y": 1.5, "z": 0.0}', NULL, 'active', NOW() - INTERVAL '14 days'), + ('iot_motion_001', 'dt_del_001', 'Study Motion Sensor', 'motion', '{"x": 2.0, "y": 2.5, "z": 1.75}', 'room_del_study_002', 'active', NOW() - INTERVAL '14 days'), + ('iot_water_001', 'dt_del_001', 'Water Flow Meter', 'water', '{"x": 1.0, "y": 0.5, "z": 0.0}', NULL, 'active', NOW() - INTERVAL '14 days'); + +-- ---- IoT Data Points (sample time-series data) ---- + +INSERT INTO iot_data_points (id, device_id, value, unit, timestamp) +VALUES + -- Temperature readings (last 24 hours) + ('dp_temp_001', 'iot_temp_001', 24.5, 'celsius', NOW() - INTERVAL '24 hours'), + ('dp_temp_002', 'iot_temp_001', 24.8, 'celsius', NOW() - INTERVAL '20 hours'), + ('dp_temp_003', 'iot_temp_001', 25.2, 'celsius', NOW() - INTERVAL '16 hours'), + ('dp_temp_004', 'iot_temp_001', 26.1, 'celsius', NOW() - INTERVAL '12 hours'), + ('dp_temp_005', 'iot_temp_001', 25.8, 'celsius', NOW() - INTERVAL '8 hours'), + ('dp_temp_006', 'iot_temp_001', 24.3, 'celsius', NOW() - INTERVAL '4 hours'), + ('dp_temp_007', 'iot_temp_001', 23.9, 'celsius', NOW()), + -- Humidity readings + ('dp_humid_001', 'iot_humid_001', 55.2, 'percent', NOW() - INTERVAL '24 hours'), + ('dp_humid_002', 'iot_humid_001', 58.1, 'percent', NOW() - INTERVAL '16 hours'), + ('dp_humid_003', 'iot_humid_001', 52.4, 'percent', NOW() - INTERVAL '8 hours'), + ('dp_humid_004', 'iot_humid_001', 54.0, 'percent', NOW()), + -- Energy readings (kWh cumulative) + ('dp_energy_001', 'iot_energy_001', 12.5, 'kWh', NOW() - INTERVAL '24 hours'), + ('dp_energy_002', 'iot_energy_001', 18.2, 'kWh', NOW() - INTERVAL '16 hours'), + ('dp_energy_003', 'iot_energy_001', 24.8, 'kWh', NOW() - INTERVAL '8 hours'), + ('dp_energy_004', 'iot_energy_001', 28.3, 'kWh', NOW()), + -- Water readings (litres cumulative) + ('dp_water_001', 'iot_water_001', 150, 'litres', NOW() - INTERVAL '24 hours'), + ('dp_water_002', 'iot_water_001', 280, 'litres', NOW() - INTERVAL '16 hours'), + ('dp_water_003', 'iot_water_001', 410, 'litres', NOW() - INTERVAL '8 hours'), + ('dp_water_004', 'iot_water_001', 495, 'litres', NOW()); + +-- ---- Emergency References ---- + +INSERT INTO emergency_references (id, project_id, type, label, description, location_description, position_json, room_id, created_at) +VALUES + ('emr_del_001', 'prj_delhi_003', 'water_shutoff', 'Main Water Shutoff Valve', 'Quarter-turn ball valve. Turn clockwise to shut off.', 'Utility area behind kitchen, bottom-left of wall panel', '{"x": 0.3, "y": 0.5, "z": 0.0}', NULL, NOW() - INTERVAL '15 days'), + ('emr_del_002', 'prj_delhi_003', 'electrical_breaker', 'Main Distribution Board', '32A main breaker + 8 MCBs. Labeled per circuit.', 'Entrance foyer, right wall behind shoe cabinet', '{"x": 0.5, "y": 1.5, "z": 0.0}', NULL, NOW() - INTERVAL '15 days'), + ('emr_del_003', 'prj_delhi_003', 'gas_shutoff', 'Kitchen Gas Regulator', 'LPG regulator with safety valve. Turn red knob fully left.', 'Below kitchen countertop, near hob gas inlet', '{"x": 1.2, "y": 0.5, "z": 0.0}', NULL, NOW() - INTERVAL '15 days'), + ('emr_del_004', 'prj_delhi_003', 'fire_extinguisher', 'Kitchen Fire Extinguisher', 'ABC dry powder 2kg. Expires Dec 2026. Pull pin, aim at base of fire.', 'Kitchen wall, left of entry door, eye height', '{"x": 0.2, "y": 1.4, "z": 0.0}', NULL, NOW() - INTERVAL '15 days'); + +-- ---- Maintenance Schedules ---- + +INSERT INTO maintenance_schedules (id, project_id, item_name, category, frequency_days, next_due_at, provider, estimated_cost, status, created_at) +VALUES + ('maint_del_001', 'prj_delhi_003', 'AC Service & Filter Clean', 'hvac', 90, NOW() + INTERVAL '15 days', 'Voltas Service Center', 2500, 'active', NOW() - INTERVAL '15 days'), + ('maint_del_002', 'prj_delhi_003', 'Water Purifier Filter Change', 'plumbing', 180, NOW() + INTERVAL '90 days', 'Kent Service', 3500, 'active', NOW() - INTERVAL '15 days'), + ('maint_del_003', 'prj_delhi_003', 'Chimney Deep Clean', 'appliance', 120, NOW() + INTERVAL '30 days', 'Elica Service', 1500, 'active', NOW() - INTERVAL '15 days'), + ('maint_del_004', 'prj_delhi_003', 'Wooden Floor Polish', 'structural', 365, NOW() + INTERVAL '180 days', 'Floor Care Experts', 15000, 'active', NOW() - INTERVAL '15 days'), + ('maint_del_005', 'prj_delhi_003', 'Bathroom Sealant Inspection', 'plumbing', 180, NOW() + INTERVAL '75 days', 'VP Plumbing Services', 800, 'active', NOW() - INTERVAL '15 days'); + +-- ---- Maintenance Logs ---- + +INSERT INTO maintenance_logs (id, schedule_id, performed_at, performed_by, cost, notes, photo_keys) +VALUES + ('mlog_del_001', 'maint_del_001', NOW() - INTERVAL '75 days', 'Voltas Technician - Ramesh K.', 2200, 'AC serviced. Filters cleaned. Gas top-up done. Cooling efficiency restored to 95%. Next service in 3 months.', '["maintenance/prj_delhi_003/ac_service_1.jpg"]'), + ('mlog_del_002', 'maint_del_003', NOW() - INTERVAL '90 days', 'Elica Service - Mahesh', 1200, 'Chimney auto-clean activated. Baffle filters soaked and cleaned. Motor checked. Suction power optimal.', NULL); + +-- ---- Warranties ---- + +INSERT INTO warranties (id, project_id, item_name, category, brand, serial_number, warranty_start_date, warranty_end_date, warranty_type, status, created_at) +VALUES + ('war_del_001', 'prj_delhi_003', 'Bosch Dishwasher SMS66GI01I', 'appliance', 'Bosch', 'BSH-DW-2024-5521', NOW() - INTERVAL '180 days', NOW() + INTERVAL '545 days', 'manufacturer', 'active', NOW() - INTERVAL '15 days'), + ('war_del_002', 'prj_delhi_003', 'Grohe Rainshower System', 'fixture', 'Grohe', 'GRH-RS-2024-8834', NOW() - INTERVAL '180 days', NOW() + INTERVAL '1545 days', 'manufacturer', 'active', NOW() - INTERVAL '15 days'), + ('war_del_003', 'prj_delhi_003', 'Hettich Kitchen Hardware Set', 'material', 'Hettich', 'HET-KIT-2024-3302', NOW() - INTERVAL '180 days', NOW() + INTERVAL '3465 days', 'manufacturer', 'active', NOW() - INTERVAL '15 days'), + ('war_del_004', 'prj_delhi_003', 'Waterproofing - Bathroom', 'system', NULL, NULL, NOW() - INTERVAL '170 days', NOW() + INTERVAL '1625 days', 'contractor', 'active', NOW() - INTERVAL '15 days'), + ('war_del_005', 'prj_delhi_003', 'Italian Marble Flooring', 'material', NULL, NULL, NOW() - INTERVAL '160 days', NOW() + INTERVAL '205 days', 'contractor', 'active', NOW() - INTERVAL '15 days'); + +-- ---- Warranty Claims ---- + +INSERT INTO warranty_claims (id, warranty_id, issue_description, photo_keys, status, claim_date, resolution_date) +VALUES ( + 'wc_del_001', + 'war_del_001', + 'Dishwasher making unusual rattling noise during wash cycle. Started after 4 months of use. No error code displayed.', + '["warranty_claims/prj_delhi_003/dishwasher_issue_1.jpg"]', + 'resolved', + NOW() - INTERVAL '45 days', + NOW() - INTERVAL '38 days' +); + +-- ---- Quality Checkpoints ---- + +INSERT INTO quality_checkpoints (id, project_id, milestone, title, description, trade, status, inspected_by, checklist_items, photo_keys, notes, inspected_at, created_at, updated_at) +VALUES + ('qc_del_001', 'prj_delhi_003', 'waterproofing_complete', 'Bathroom Waterproofing Inspection', 'Verify waterproofing membrane integrity before tiling', 'waterproofing', 'passed', 'VP Plumbing - Vikram', '[{"item": "Membrane covers all wet areas", "checked": true, "note": null}, {"item": "Overlap minimum 150mm at joints", "checked": true, "note": "200mm overlap achieved"}, {"item": "Floor drain area sealed", "checked": true, "note": null}, {"item": "48-hour flood test passed", "checked": true, "note": "No leakage detected after 48hrs"}, {"item": "Wall membrane extends 1800mm", "checked": true, "note": "Full height to 2100mm in shower area"}]', '["quality/prj_delhi_003/waterproof_test_1.jpg", "quality/prj_delhi_003/waterproof_test_2.jpg"]', 'All checks passed. Flood test held 48 hours with no leakage. Approved for tiling.', NOW() - INTERVAL '140 days', NOW() - INTERVAL '145 days', NOW() - INTERVAL '140 days'), + ('qc_del_002', 'prj_delhi_003', 'electrical_complete', 'Electrical Final Inspection', 'Verify all circuits, earthing, and fixtures before handover', 'electrical', 'passed', 'Suresh Electricals', '[{"item": "All circuits tested under load", "checked": true, "note": null}, {"item": "ELCB/RCCB functioning", "checked": true, "note": "30mA trip tested"}, {"item": "Earth resistance < 5 ohms", "checked": true, "note": "Measured: 2.8 ohms"}, {"item": "All switches and sockets functional", "checked": true, "note": "48 points verified"}, {"item": "MCB labels match circuit map", "checked": true, "note": null}]', '["quality/prj_delhi_003/elec_inspection_1.jpg"]', 'All electrical work meets IS 732:2019 standards.', NOW() - INTERVAL '30 days', NOW() - INTERVAL '35 days', NOW() - INTERVAL '30 days'); + +-- ---- Punch List Items ---- + +INSERT INTO punch_list_items (id, project_id, room_id, title, description, severity, category, status, assigned_to, photo_keys, location_pin, resolved_at, verified_at, created_at, updated_at) +VALUES + ('pl_del_001', 'prj_delhi_003', 'room_del_living_001', 'Minor paint touch-up needed at ceiling cornice', 'Small area (approx 10cm) where paint has peeled at the junction of false ceiling and wall in the north-east corner.', 'minor', 'painting', 'verified', 'Paint contractor', '["punchlist/prj_delhi_003/paint_touchup_1.jpg"]', '{"x": 0.95, "y": 0.95}', NOW() - INTERVAL '18 days', NOW() - INTERVAL '16 days', NOW() - INTERVAL '22 days', NOW() - INTERVAL '16 days'), + ('pl_del_002', 'prj_delhi_003', 'room_del_study_002', 'Bookshelf edge slightly rough at eye level', 'Third shelf from top has a rough laminate edge that catches fingers. Needs edge banding reapplication.', 'minor', 'carpentry', 'resolved', 'Raj Woodworks', '["punchlist/prj_delhi_003/shelf_edge_1.jpg"]', '{"x": 0.8, "y": 0.6}', NOW() - INTERVAL '17 days', NULL, NOW() - INTERVAL '22 days', NOW() - INTERVAL '17 days'), + ('pl_del_003', 'prj_delhi_003', 'room_del_living_001', 'Brass inlay grout hairline crack', 'Very fine hairline crack in the grout around brass inlay near the bar cabinet area. Cosmetic only.', 'observation', 'tiling', 'open', NULL, '["punchlist/prj_delhi_003/grout_crack_1.jpg"]', '{"x": 0.3, "y": 0.7}', NULL, NULL, NOW() - INTERVAL '20 days', NOW() - INTERVAL '20 days'); + +-- ---- Handover Packages ---- + +INSERT INTO handover_packages (id, project_id, status, as_built_drawing_keys, material_register, contractor_directory, operational_guides, maintenance_manual_key, client_signed_at, delivered_at, created_at, updated_at) +VALUES ( + 'ho_del_001', + 'prj_delhi_003', + 'delivered', + '["handover/prj_delhi_003/as_built_floor_plan.pdf", "handover/prj_delhi_003/as_built_electrical.pdf", "handover/prj_delhi_003/as_built_plumbing.pdf"]', + '[ + {"item": "Italian Marble - Nero Marquina", "brand": "Imported", "model": "NM-800x800-P", "batch": "IT-NM-2024-B12", "purchaseDate": "2024-08-15", "vendor": "Stone World Delhi"}, + {"item": "Bosch Dishwasher", "brand": "Bosch", "model": "SMS66GI01I", "batch": "BSH-2024-Q3", "purchaseDate": "2024-09-01", "vendor": "Bosch Home India"}, + {"item": "Art Deco Chandelier", "brand": "Flos", "model": "AD-CHAN-2024", "batch": "FL-IT-2024-55", "purchaseDate": "2024-07-20", "vendor": "Imported via LightStore Delhi"} + ]', + '[ + {"name": "Raj Woodworks", "trade": "Carpentry", "phone": "+91-9876543001", "email": "raj@rajwoodworks.in"}, + {"name": "Suresh Electricals", "trade": "Electrical", "phone": "+91-9876543002", "email": "suresh@gmail.com"}, + {"name": "VP Plumbing", "trade": "Plumbing", "phone": "+91-9876543003", "email": "vikram.plumber@gmail.com"}, + {"name": "Stone World Delhi", "trade": "Stone/Marble", "phone": "+91-11-26789012", "email": "info@stoneworld.in"} + ]', + '[ + {"system": "Central AC", "instructions": "Maintain temperature between 22-26C. Clean filters every 3 months. Annual gas top-up recommended."}, + {"system": "Motorized Blinds", "instructions": "Use Somfy app or wall switch. Reset: hold up+down for 5 seconds. Battery backup lasts 72 hours."}, + {"system": "Dishwasher", "instructions": "Use rinse aid monthly. Clean filter weekly. Run empty hot cycle with citric acid every 3 months."}, + {"system": "Waterproofing", "instructions": "Avoid drilling into bathroom walls below 1800mm height. Check sealant around shower glass annually."} + ]', + 'handover/prj_delhi_003/maintenance_manual.pdf', + NOW() - INTERVAL '16 days', + NOW() - INTERVAL '15 days', + NOW() - INTERVAL '20 days', + NOW() - INTERVAL '15 days' +); diff --git a/sampledb/013_marketplace.sql b/sampledb/013_marketplace.sql new file mode 100644 index 0000000..8e1d873 --- /dev/null +++ b/sampledb/013_marketplace.sql @@ -0,0 +1,82 @@ +-- ============================================================================ +-- 013_marketplace.sql — Offcuts, Gallery, Developer Apps, Exchange Rates +-- ============================================================================ + +-- ---- Offcut Listings ---- + +INSERT INTO offcut_listings (id, user_id, title, material_type, quantity, unit, dimensions, condition, asking_price, currency, image_keys, location, status, created_at, updated_at) +VALUES + ('off_001', 'usr_bob_002', 'Nero Marquina Marble Offcuts', 'stone', 3.5, 'sqm', '{"length": 800, "width": 600, "thickness": 20, "unit": "mm"}', 'new', 15000, 'INR', '["offcuts/off_001/marble_1.jpg", "offcuts/off_001/marble_2.jpg"]', 'Gurugram, Haryana', 'active', NOW() - INTERVAL '10 days', NOW() - INTERVAL '10 days'), + ('off_002', 'usr_bob_002', 'Walnut Engineered Wood Planks', 'wood', 12, 'sqm', '{"length": 1200, "width": 190, "thickness": 14, "unit": "mm"}', 'new', 25000, 'INR', '["offcuts/off_002/wood_1.jpg"]', 'Gurugram, Haryana', 'active', NOW() - INTERVAL '8 days', NOW() - INTERVAL '8 days'), + ('off_003', 'usr_alice_001', 'White Subway Tiles - Leftover', 'tile', 2, 'sqm', '{"length": 150, "width": 75, "thickness": 8, "unit": "mm"}', 'new', 1800, 'INR', '["offcuts/off_003/tile_1.jpg"]', 'Bandra, Mumbai', 'active', NOW() - INTERVAL '3 days', NOW() - INTERVAL '3 days'); + +-- ---- Offcut Inquiries ---- + +INSERT INTO offcut_inquiries (id, listing_id, buyer_user_id, message, status, created_at) +VALUES + ('ofi_001', 'off_001', 'usr_alice_001', 'Hi! I am interested in the Nero Marquina offcuts. Can you share the exact slab sizes? I need them for a tabletop project. Would you do INR 12,000?', 'replied', NOW() - INTERVAL '8 days'), + ('ofi_002', 'off_002', 'usr_alice_001', 'Are these planks from the same batch? I need them to match existing flooring in my villa project. Can I visit to check the color match?', 'pending', NOW() - INTERVAL '5 days'); + +-- ---- Project Gallery Entries ---- + +INSERT INTO project_gallery_entries (id, project_id, title, description, tags, image_keys, style, is_public, likes, created_at) +VALUES + ('gal_001', 'prj_delhi_003', 'Art Deco Penthouse Living Room', 'Luxurious art deco living room with emerald velvet seating, brass accents, and Nero Marquina marble flooring. Panoramic city views from floor-to-ceiling windows.', '["art_deco", "luxury", "penthouse", "marble", "brass"]', '["gallery/prj_delhi_003/living_hero.jpg", "gallery/prj_delhi_003/living_detail_1.jpg", "gallery/prj_delhi_003/living_night.jpg"]', 'art_deco', true, 47, NOW() - INTERVAL '15 days'), + ('gal_002', 'prj_delhi_003', 'Modern Executive Study', 'Floor-to-ceiling walnut bookshelves, executive desk with leather chair, and warm ambient lighting. Perfect home office setup.', '["modern", "study", "home_office", "walnut", "bookshelves"]', '["gallery/prj_delhi_003/study_hero.jpg"]', 'modern', true, 23, NOW() - INTERVAL '15 days'); + +-- ---- Developer Apps ---- + +INSERT INTO developer_apps (id, user_id, name, client_id, client_secret_hash, redirect_uris, scopes, status, rate_limit_tier, created_at, updated_at) +VALUES ( + 'devapp_001', + 'usr_bob_002', + 'Interior Design Analytics Tool', + 'olclient_analytics_001', + '$2b$10$abcdefghijklmnopqrstuvwxyz1234567890ABCDEFG', + '["http://localhost:3001/callback", "https://analytics.example.com/callback"]', + '["projects:read", "bom:read", "analytics:read"]', + 'active', + 'standard', + NOW() - INTERVAL '30 days', + NOW() - INTERVAL '30 days' +); + +-- ---- API Access Tokens ---- + +INSERT INTO api_access_tokens (id, app_id, user_id, token_hash, scopes, expires_at, created_at) +VALUES ( + 'aat_001', + 'devapp_001', + 'usr_bob_002', + '$2b$10$tokenhash1234567890abcdefghijklmnopqrstuv', + '["projects:read", "bom:read"]', + NOW() + INTERVAL '90 days', + NOW() - INTERVAL '5 days' +); + +-- ---- API Request Logs ---- + +INSERT INTO api_request_logs (id, app_id, endpoint, method, status_code, response_time_ms, created_at) +VALUES + ('arl_001', 'devapp_001', '/api/v1/projects', 'GET', 200, 45, NOW() - INTERVAL '4 days'), + ('arl_002', 'devapp_001', '/api/v1/projects/prj_delhi_003', 'GET', 200, 32, NOW() - INTERVAL '4 days'), + ('arl_003', 'devapp_001', '/api/v1/projects/prj_delhi_003/bom', 'GET', 200, 67, NOW() - INTERVAL '3 days'), + ('arl_004', 'devapp_001', '/api/v1/projects/prj_nonexistent', 'GET', 404, 12, NOW() - INTERVAL '3 days'), + ('arl_005', 'devapp_001', '/api/v1/analytics/dashboard', 'GET', 200, 180, NOW() - INTERVAL '2 days'); + +-- ---- Webhook Subscriptions ---- + +INSERT INTO webhook_subscriptions (id, app_id, event_type, target_url, secret, status, failure_count, created_at) +VALUES + ('wh_001', 'devapp_001', 'project.created', 'https://analytics.example.com/webhooks/project-created', 'whsec_analytics_proj_001', 'active', 0, NOW() - INTERVAL '25 days'), + ('wh_002', 'devapp_001', 'bom.generated', 'https://analytics.example.com/webhooks/bom-generated', 'whsec_analytics_bom_001', 'active', 0, NOW() - INTERVAL '25 days'); + +-- ---- Exchange Rates ---- + +INSERT INTO exchange_rates (id, from_currency, to_currency, rate, source, fetched_at) +VALUES + ('er_usd_inr', 'USD', 'INR', 83.50, 'api', NOW() - INTERVAL '1 hour'), + ('er_eur_inr', 'EUR', 'INR', 90.75, 'api', NOW() - INTERVAL '1 hour'), + ('er_gbp_inr', 'GBP', 'INR', 105.20, 'api', NOW() - INTERVAL '1 hour'), + ('er_usd_eur', 'USD', 'EUR', 0.92, 'api', NOW() - INTERVAL '1 hour'), + ('er_inr_usd', 'INR', 'USD', 0.012, 'api', NOW() - INTERVAL '1 hour'); diff --git a/sampledb/README.md b/sampledb/README.md new file mode 100644 index 0000000..8b720ca --- /dev/null +++ b/sampledb/README.md @@ -0,0 +1,61 @@ +# OpenLintel Sample Database + +This directory contains SQL seed files to populate a PostgreSQL database with realistic test data for manual and automated testing. + +## Files + +| File | Description | +|------|-------------| +| `001_users_auth.sql` | Users, accounts, sessions (2 users: homeowner + admin) | +| `002_projects_rooms.sql` | Projects (3) with rooms (8 total) | +| `003_uploads_designs.sql` | File uploads and design variants | +| `004_jobs.sql` | Sample async job records across all statuses | +| `005_bom_drawings_cutlist_mep.sql` | BOM results, drawings, cut lists, MEP calculations | +| `006_catalogue.sql` | Categories, vendors, products, product prices | +| `007_schedule_milestones.sql` | Project timeline, milestones, site logs, change orders | +| `008_procurement_payments.sql` | Purchase orders, payments, invoices, deliveries | +| `009_collaboration.sql` | Comments, approvals, notifications, threads, messages | +| `010_contractors.sql` | Contractors, reviews, assignments, referrals | +| `011_intelligence.sql` | Cost/timeline predictions, budget scenarios, sustainability | +| `012_post_occupancy.sql` | Digital twins, IoT, maintenance, warranties | +| `013_marketplace.sql` | Offcut listings, gallery entries, developer apps | +| `seed_all.sql` | Master file that imports all above in order | + +## How to Import + +### Option 1: Import all at once +```bash +psql -U postgres -d openlintel < sampledb/seed_all.sql +``` + +### Option 2: Import individual files +```bash +psql -U postgres -d openlintel < sampledb/001_users_auth.sql +psql -U postgres -d openlintel < sampledb/002_projects_rooms.sql +# ... etc +``` + +### Option 3: Using Docker Compose +```bash +docker exec -i openlintel-db psql -U postgres -d openlintel < sampledb/seed_all.sql +``` + +## Test Accounts + +| Email | Password | Role | Description | +|-------|----------|------|-------------| +| `alice@example.com` | `password123` | user | Homeowner with 2 active projects | +| `bob@example.com` | `password123` | admin | Platform administrator | + +> **Note:** Passwords are hashed with bcrypt. For OAuth testing, use Google/GitHub sign-in flows. + +## Data Overview + +- **2 users** (1 homeowner, 1 admin) +- **3 projects** (Mumbai Apartment, Bangalore Villa, Delhi Penthouse) +- **8 rooms** across all projects +- **6 design variants** with different styles/budgets +- **4 vendors** (wood, tiles, electrical, plumbing) +- **12 products** across categories +- **3 contractors** (carpenter, electrician, plumber) +- **Full lifecycle data** from design through handover diff --git a/sampledb/seed_all.sql b/sampledb/seed_all.sql new file mode 100644 index 0000000..f1b9308 --- /dev/null +++ b/sampledb/seed_all.sql @@ -0,0 +1,73 @@ +-- ============================================================================ +-- seed_all.sql — Master seed file for OpenLintel sample database +-- ============================================================================ +-- Usage: psql -U postgres -d openlintel < sampledb/seed_all.sql +-- ============================================================================ + +-- Ensure clean state (optional — uncomment to reset before seeding) +-- TRUNCATE users CASCADE; + +BEGIN; + +\echo '>>> Seeding 001: Users & Auth...' +\i 001_users_auth.sql + +\echo '>>> Seeding 002: Projects & Rooms...' +\i 002_projects_rooms.sql + +\echo '>>> Seeding 003: Uploads & Design Variants...' +\i 003_uploads_designs.sql + +\echo '>>> Seeding 004: Jobs...' +\i 004_jobs.sql + +\echo '>>> Seeding 005: BOM, Drawings, Cut Lists, MEP...' +\i 005_bom_drawings_cutlist_mep.sql + +\echo '>>> Seeding 006: Catalogue (Categories, Vendors, Products)...' +\i 006_catalogue.sql + +\echo '>>> Seeding 007: Schedules, Milestones, Site Logs, Change Orders...' +\i 007_schedule_milestones.sql + +\echo '>>> Seeding 008: Procurement, Payments, Invoices, Deliveries...' +\i 008_procurement_payments.sql + +\echo '>>> Seeding 009: Collaboration (Comments, Approvals, Notifications, Threads)...' +\i 009_collaboration.sql + +\echo '>>> Seeding 010: Contractors, Reviews, Assignments...' +\i 010_contractors.sql + +\echo '>>> Seeding 011: Intelligence (Predictions, Budget, Sustainability)...' +\i 011_intelligence.sql + +\echo '>>> Seeding 012: Post-Occupancy (Digital Twin, IoT, Maintenance, Warranties)...' +\i 012_post_occupancy.sql + +\echo '>>> Seeding 013: Marketplace (Offcuts, Gallery, Developer Apps)...' +\i 013_marketplace.sql + +COMMIT; + +\echo '' +\echo '============================================' +\echo ' OpenLintel sample database seeded!' +\echo '============================================' +\echo '' +\echo ' Test accounts:' +\echo ' alice@example.com (user) — 2 active projects' +\echo ' bob@example.com (admin) — 1 completed project' +\echo '' +\echo ' Data summary:' +\echo ' 2 users, 3 projects, 8 rooms' +\echo ' 6 design variants, 8 jobs' +\echo ' 2 BOMs, 5 drawings, 1 cut list, 2 MEP calcs' +\echo ' 7 categories, 4 vendors, 12 products' +\echo ' 1 schedule, 4 milestones, 4 site logs' +\echo ' 3 change orders, 3 POs, 3 payments' +\echo ' 3 contractors, 4 reviews' +\echo ' 1 digital twin, 5 IoT devices' +\echo ' 5 warranties, 5 maintenance items' +\echo ' 3 offcut listings, 2 gallery entries' +\echo '============================================' diff --git a/sampletest.md b/sampletest.md new file mode 100644 index 0000000..4d5940d --- /dev/null +++ b/sampletest.md @@ -0,0 +1,1387 @@ +# OpenLintel - Sample Test Flows + +> Comprehensive manual test scenarios for QA testing. Use with the [sample database](./sampledb/README.md) for pre-populated data. + +--- + +## Table of Contents + +1. [Authentication & Onboarding](#1-authentication--onboarding) +2. [Project Management](#2-project-management) +3. [Room Management](#3-room-management) +4. [File Uploads](#4-file-uploads) +5. [Design Generation](#5-design-generation) +6. [Style Quiz & Mood Board](#6-style-quiz--mood-board) +7. [Floor Plan Digitization](#7-floor-plan-digitization) +8. [3D Editor](#8-3d-editor) +9. [BOM (Bill of Materials)](#9-bom-bill-of-materials) +10. [Technical Drawings](#10-technical-drawings) +11. [CNC Cut Lists & Nesting](#11-cnc-cut-lists--nesting) +12. [MEP Engineering](#12-mep-engineering) +13. [Building Code Compliance](#13-building-code-compliance) +14. [Project Timeline & Scheduling](#14-project-timeline--scheduling) +15. [Site Logs](#15-site-logs) +16. [Change Orders](#16-change-orders) +17. [Product Catalogue](#17-product-catalogue) +18. [Procurement & Purchase Orders](#18-procurement--purchase-orders) +19. [Delivery Tracking](#19-delivery-tracking) +20. [Payments & Invoicing](#20-payments--invoicing) +21. [Contractor Marketplace](#21-contractor-marketplace) +22. [Collaboration & Comments](#22-collaboration--comments) +23. [Notifications](#23-notifications) +24. [Quality Assurance & Punch List](#24-quality-assurance--punch-list) +25. [Handover Package](#25-handover-package) +26. [Cost & Timeline Predictions](#26-cost--timeline-predictions) +27. [Budget Optimizer](#27-budget-optimizer) +28. [Sustainability Scoring](#28-sustainability-scoring) +29. [Financial Reports](#29-financial-reports) +30. [Portfolio Management](#30-portfolio-management) +31. [Digital Twin & IoT](#31-digital-twin--iot) +32. [Maintenance Scheduling](#32-maintenance-scheduling) +33. [Warranty Tracking](#33-warranty-tracking) +34. [Offcuts Exchange](#34-offcuts-exchange) +35. [Community Gallery](#35-community-gallery) +36. [Developer API Portal](#36-developer-api-portal) +37. [Admin Panel](#37-admin-panel) +38. [User Settings & Preferences](#38-user-settings--preferences) +39. [AR/VR Viewer](#39-arvr-viewer) +40. [Cross-cutting: Error Handling & Edge Cases](#40-cross-cutting-error-handling--edge-cases) + +--- + +## Test Data Reference + +| Account | Email | Role | Projects | +|---------|-------|------|----------| +| Alice Sharma | alice@example.com | user | Mumbai Apartment (in_progress), Bangalore Villa (draft) | +| Bob Kumar | bob@example.com | admin | Delhi Penthouse (completed) | + +--- + +## 1. Authentication & Onboarding + +### TC-AUTH-001: Google OAuth Sign-In +- **Precondition:** User not signed in +- **Steps:** + 1. Navigate to `/auth/signin` + 2. Click "Sign in with Google" + 3. Complete Google consent flow + 4. Verify redirect to `/dashboard` +- **Expected:** User session created, name/email populated from Google profile, role=user + +### TC-AUTH-002: GitHub OAuth Sign-In +- **Steps:** + 1. Navigate to `/auth/signin` + 2. Click "Sign in with GitHub" + 3. Authorize on GitHub +- **Expected:** Session created, user record linked to GitHub provider + +### TC-AUTH-003: Email/Password Sign-In +- **Steps:** + 1. Navigate to `/auth/signin` + 2. Enter `alice@example.com` and password + 3. Click Sign In +- **Expected:** Redirect to dashboard with Alice's projects listed + +### TC-AUTH-004: Sign Out +- **Steps:** + 1. Sign in as any user + 2. Click user avatar > Sign Out +- **Expected:** Session destroyed, redirect to landing page, protected routes inaccessible + +### TC-AUTH-005: Unauthorized Access +- **Steps:** + 1. Without signing in, navigate to `/dashboard` + 2. Try `/project/prj_mumbai_001` + 3. Try `/admin` +- **Expected:** All redirect to `/auth/signin` + +### TC-AUTH-006: Admin Role Gate +- **Steps:** + 1. Sign in as Alice (role=user) + 2. Navigate to `/admin` +- **Expected:** 403 Forbidden or redirect (admin-only routes blocked for users) + +--- + +## 2. Project Management + +### TC-PROJ-001: Create New Project +- **Steps:** + 1. Sign in as Alice + 2. Click "+ New Project" on dashboard + 3. Enter name: "Test Project" + 4. Enter address: "123 Test Street" + 5. Select unit system: metric + 6. Click Create +- **Expected:** Project created with status=draft, redirects to `/project/{id}` + +### TC-PROJ-002: List Projects +- **Steps:** + 1. Sign in as Alice + 2. Navigate to `/dashboard` +- **Expected:** Shows 2 projects (Mumbai Apartment, Bangalore Villa) + any newly created. Bob's projects NOT visible. + +### TC-PROJ-003: Update Project Name +- **Steps:** + 1. Open Mumbai Apartment project + 2. Edit project name to "Mumbai 2BHK Reno Updated" + 3. Save +- **Expected:** Name updated, updatedAt timestamp refreshed + +### TC-PROJ-004: Delete Project +- **Steps:** + 1. Create a test project + 2. Delete it from project settings +- **Expected:** Project and all cascading data (rooms, designs, BOMs) removed + +### TC-PROJ-005: Project Status Transition +- **Steps:** + 1. Open Bangalore Villa (status=draft) + 2. Change status to in_progress + 3. Verify status badge updates +- **Expected:** Status updates correctly, reflected on dashboard list + +### TC-PROJ-006: Cross-User Isolation +- **Steps:** + 1. Sign in as Alice + 2. Try to access `/project/prj_delhi_003` (Bob's project) +- **Expected:** 404 or access denied — users can only see their own projects + +--- + +## 3. Room Management + +### TC-ROOM-001: Add Room to Project +- **Steps:** + 1. Open Mumbai Apartment > Rooms tab + 2. Click "Add Room" + 3. Enter: Name="Guest Bedroom", Type=bedroom, L=3500mm, W=3000mm, H=2800mm, Floor=0 + 4. Save +- **Expected:** Room appears in room list with correct dimensions + +### TC-ROOM-002: Edit Room Dimensions +- **Steps:** + 1. Open "Main Living Room" (room_mum_living_001) + 2. Change width from 4000mm to 4200mm + 3. Save +- **Expected:** Width updated, related designs may show "recalculation needed" warning + +### TC-ROOM-003: All Room Types +- **Steps:** + 1. Create rooms of each type: living_room, bedroom, kitchen, bathroom, dining_room, study, balcony, pooja_room, foyer, laundry, garage, closet, hallway, kids_room, other +- **Expected:** All 15 room types accepted and saved correctly + +### TC-ROOM-004: Delete Room +- **Steps:** + 1. Create a test room + 2. Delete it +- **Expected:** Room removed, associated uploads/designs cascade deleted + +### TC-ROOM-005: Room Validation +- **Steps:** + 1. Try to create a room with: empty name, negative dimensions, missing type +- **Expected:** Validation errors shown for each invalid field + +--- + +## 4. File Uploads + +### TC-UPLOAD-001: Upload Room Photo +- **Steps:** + 1. Open any room detail page + 2. Click "Upload Photo" + 3. Select a JPEG image (< 10MB) +- **Expected:** File uploaded to S3/MinIO, thumbnail generated, appears in room gallery + +### TC-UPLOAD-002: Upload Floor Plan PDF +- **Steps:** + 1. Open project page + 2. Upload a floor plan PDF + 3. Set category = "floor_plan" +- **Expected:** PDF stored, available for digitization workflow + +### TC-UPLOAD-003: Duplicate Detection +- **Steps:** + 1. Upload the same image twice to the same room +- **Expected:** Perceptual hash comparison detects duplicate, warns user + +### TC-UPLOAD-004: Large File Rejection +- **Steps:** + 1. Try to upload a 50MB file +- **Expected:** Upload rejected with "file too large" error + +### TC-UPLOAD-005: Invalid File Type +- **Steps:** + 1. Try to upload a .exe file +- **Expected:** Upload rejected, only image/pdf types accepted + +--- + +## 5. Design Generation + +### TC-DESIGN-001: Generate Modern Mid-Range Design +- **Steps:** + 1. Open room_mum_living_001 + 2. Click "Generate Design" + 3. Select style=modern, budgetTier=mid_range + 4. Add constraint: "child-safe corners" + 5. Select source photo: upl_mum_001 + 6. Submit +- **Expected:** Job created (type=design_generation), status=pending, progresses to completed, design variant created with render URL + +### TC-DESIGN-002: Generate With Each Style +- **Steps:** + 1. For each style (modern, contemporary, traditional, scandinavian, industrial, minimalist, bohemian, japandi, art_deco, tropical), generate a design +- **Expected:** All 10 styles produce valid design variants with unique spec_json + +### TC-DESIGN-003: Generate With Each Budget Tier +- **Steps:** + 1. Generate designs at economy, mid_range, premium, luxury tiers +- **Expected:** Material quality and price points differ appropriately per tier + +### TC-DESIGN-004: View Design Gallery +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/designs` + 2. View all design variants for the project +- **Expected:** Gallery shows render images, style badges, budget tier labels + +### TC-DESIGN-005: Compare Design Variants +- **Steps:** + 1. Open design gallery + 2. Select 2 variants for comparison +- **Expected:** Side-by-side view showing spec differences, cost differences + +### TC-DESIGN-006: Design Generation With Constraints +- **Steps:** + 1. Generate design with multiple constraints: "keep AC on east wall", "no glass furniture", "pet-friendly fabrics" +- **Expected:** Generated design spec respects all stated constraints + +### TC-DESIGN-007: Cancel Running Job +- **Steps:** + 1. Start a design generation + 2. While status=running, click "Cancel" +- **Expected:** Job status changes to cancelled, partial results discarded + +--- + +## 6. Style Quiz & Mood Board + +### TC-QUIZ-001: Complete Style Quiz +- **Steps:** + 1. Navigate to `/project/prj_blr_002/style-quiz` + 2. Step 1: Select room usage = "family_gathering" + 3. Step 2: Select color = "warm_neutrals" + 4. Step 3: Select material = "wood_and_fabric" + 5. Step 4: Select budget = "premium" + 6. Step 5: Upload inspiration images + 7. Submit +- **Expected:** Detected styles returned with scores (e.g., modern: 0.85), mood board generated, color palette suggested + +### TC-QUIZ-002: Style Preferences Saved +- **Steps:** + 1. Complete quiz for a project + 2. Navigate away and return to quiz page +- **Expected:** Previous responses loaded, results displayed + +### TC-QUIZ-003: Quiz Influences Design Generation +- **Steps:** + 1. Complete quiz with "scandinavian" preferences + 2. Generate a design for same project +- **Expected:** Generated design leans toward detected style preferences + +--- + +## 7. Floor Plan Digitization + +### TC-FPLAN-001: Digitize Floor Plan +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/floor-plan` + 2. Select uploaded floor plan (upl_mum_004) + 3. Click "Digitize" +- **Expected:** Job created, VLM extracts rooms with dimensions, doors, windows. Interactive SVG displayed. + +### TC-FPLAN-002: Verify Extracted Data +- **Steps:** + 1. After digitization completes + 2. Check that room count matches actual floor plan + 3. Verify dimensions are within 5% of actual + 4. Verify doors/windows detected +- **Expected:** Extracted data is reasonably accurate + +### TC-FPLAN-003: Auto-Create Rooms +- **Steps:** + 1. After digitization + 2. Click "Create Rooms from Floor Plan" +- **Expected:** Rooms auto-created in the project with correct types and dimensions from the digitized plan + +### TC-FPLAN-004: Download DXF +- **Steps:** + 1. After digitization + 2. Click "Download DXF" +- **Expected:** Valid DXF file downloads, opens in CAD software + +--- + +## 8. 3D Editor + +### TC-EDITOR-001: Load 3D Scene +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/editor` +- **Expected:** React Three Fiber canvas loads, room geometry visible, furniture objects rendered + +### TC-EDITOR-002: Move Object +- **Steps:** + 1. Select a furniture item + 2. Drag to move it +- **Expected:** Object translates along floor plane, snap-to-grid works + +### TC-EDITOR-003: Rotate Object +- **Steps:** + 1. Select furniture + 2. Switch to rotate mode + 3. Rotate 90 degrees +- **Expected:** Object rotates on Y axis, snaps to 15/45/90 degree increments + +### TC-EDITOR-004: Undo/Redo +- **Steps:** + 1. Move an object + 2. Press Ctrl+Z (undo) + 3. Press Ctrl+Shift+Z (redo) +- **Expected:** Object returns to previous position on undo, returns on redo + +### TC-EDITOR-005: Add Furniture from Catalog +- **Steps:** + 1. Open furniture catalog panel + 2. Search "sofa" + 3. Drag into scene +- **Expected:** New furniture object placed at cursor position in scene + +### TC-EDITOR-006: Real-time Collaboration +- **Steps:** + 1. Open editor in Browser A as Alice + 2. Open same editor in Browser B as Bob (if admin access enabled) + 3. Move object in Browser A +- **Expected:** Movement reflected in Browser B in real-time, cursor awareness shown + +--- + +## 9. BOM (Bill of Materials) + +### TC-BOM-001: Generate BOM +- **Steps:** + 1. Open design variant dv_mum_liv_modern_001 + 2. Click "Generate BOM" +- **Expected:** Job created, BOM result with categorized items, quantities, unit prices, and total cost + +### TC-BOM-002: View BOM Table +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/bom` + 2. View BOM for Mumbai Living Room +- **Expected:** Table shows all 17+ items grouped by category (furniture, flooring, lighting, etc.) + +### TC-BOM-003: Export BOM as CSV +- **Steps:** + 1. On BOM page, click "Export CSV" +- **Expected:** CSV file downloads with all BOM items, quantities, prices + +### TC-BOM-004: Export BOM as PDF +- **Steps:** + 1. Click "Export PDF" +- **Expected:** Formatted PDF with project header, room info, itemized BOM table, total cost + +### TC-BOM-005: BOM Total Calculation +- **Steps:** + 1. Verify sum of all item totalPrice values equals the totalCost field + 2. For bom_mum_liv_001: expect ~INR 198,160 +- **Expected:** Totals mathematically correct + +### TC-BOM-006: Filter by Category +- **Steps:** + 1. On BOM page, filter by "furniture" +- **Expected:** Only furniture items shown, subtotal updates + +--- + +## 10. Technical Drawings + +### TC-DRAW-001: Generate All Drawing Types +- **Steps:** + 1. For a design variant, generate: floor_plan, elevation, section, rcp, flooring, electrical +- **Expected:** Each drawing type generates with appropriate DXF/PDF/SVG files + +### TC-DRAW-002: View Drawings Gallery +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/drawings` +- **Expected:** Gallery shows thumbnails/previews for each drawing, labeled by type + +### TC-DRAW-003: Download DXF +- **Steps:** + 1. Click download on any drawing + 2. Select DXF format +- **Expected:** Valid DXF file downloads + +### TC-DRAW-004: View SVG Inline +- **Steps:** + 1. Click a drawing with SVG output +- **Expected:** SVG renders inline in browser with zoom/pan + +### TC-DRAW-005: IFC/BIM Export +- **Steps:** + 1. Click "Export IFC" on a drawing +- **Expected:** IFC4-compliant file generates, downloadable + +--- + +## 11. CNC Cut Lists & Nesting + +### TC-CUT-001: Generate Cut List +- **Steps:** + 1. Open kitchen design variant + 2. Click "Generate Cut List" +- **Expected:** Panel list with material, dimensions, grain direction, edge banding per panel + +### TC-CUT-002: View Nesting Layout +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/cutlist` + 2. View sheet layouts +- **Expected:** Visual diagram showing panel placement on standard 2440x1220mm sheets + +### TC-CUT-003: Verify Waste Percentage +- **Steps:** + 1. Check waste_percent on cl_mum_kit_001 + 2. Should be ~4.8% +- **Expected:** Waste under 5% (platform target) + +### TC-CUT-004: Hardware Schedule +- **Steps:** + 1. View hardware section of cut list +- **Expected:** Lists all hinges, channels, handles with quantities and brands + +--- + +## 12. MEP Engineering + +### TC-MEP-001: Generate Electrical Calculation +- **Steps:** + 1. Open design variant, click MEP > Electrical +- **Expected:** Load summary, circuit breakdown, switch layout, wire gauges calculated + +### TC-MEP-002: Generate Plumbing Calculation +- **Steps:** + 1. Click MEP > Plumbing (for kitchen design) +- **Expected:** Fixture units, pipe sizing, drain slopes, trap types specified + +### TC-MEP-003: Generate HVAC Calculation +- **Steps:** + 1. Click MEP > HVAC +- **Expected:** Cooling/heating load, duct sizing, equipment selection returned + +### TC-MEP-004: Standards Citations +- **Steps:** + 1. View any MEP result + 2. Check standards_cited field +- **Expected:** NEC/IPC/ASHRAE/IS references included with specific clause numbers + +### TC-MEP-005: View MEP Page +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/mep` +- **Expected:** All MEP results displayed with tabs for electrical/plumbing/hvac + +--- + +## 13. Building Code Compliance + +### TC-COMPLY-001: Run Compliance Check — India +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/compliance` + 2. Select jurisdiction: India (NBC 2016) + 3. Run check +- **Expected:** Compliance report generated with pass/fail per code section + +### TC-COMPLY-002: Run Compliance Check — US +- **Steps:** + 1. Select jurisdiction: US (IRC 2021) + 2. Run check +- **Expected:** US-specific code requirements checked (different from India) + +### TC-COMPLY-003: Handle Violations +- **Steps:** + 1. Trigger a compliance check that flags violations + 2. Review remediation suggestions +- **Expected:** Each violation shows severity, clause reference, and remediation suggestion + +--- + +## 14. Project Timeline & Scheduling + +### TC-SCHED-001: Generate Schedule +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/timeline` + 2. Click "Generate Schedule" +- **Expected:** AI generates tasks with dependencies, critical path highlighted, milestones set + +### TC-SCHED-002: View Gantt Chart +- **Steps:** + 1. View generated schedule +- **Expected:** Gantt chart renders with task bars, dependency arrows, milestone diamonds + +### TC-SCHED-003: Critical Path +- **Steps:** + 1. Verify critical path tasks are highlighted (sch_mum_001 has 10 critical tasks) +- **Expected:** Critical path clearly distinguished, represents longest dependency chain + +### TC-SCHED-004: Export Schedule +- **Steps:** + 1. Click "Export" on timeline page +- **Expected:** Schedule exports in usable format (PDF/JSON) + +--- + +## 15. Site Logs + +### TC-SITELOG-001: Create Site Log +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/site-logs` + 2. Click "New Log" + 3. Enter: date=today, title="Tiling Started", notes="Kitchen wall tiling begun", weather=sunny, workers=4 + 4. Upload 2 photos + 5. Add tags: ["tiling", "kitchen"] + 6. Save +- **Expected:** Site log created with all fields, photos stored in S3 + +### TC-SITELOG-002: View Log Timeline +- **Steps:** + 1. View all site logs for Mumbai Apartment +- **Expected:** 4+ logs shown in chronological order with photos and tags + +### TC-SITELOG-003: Filter Logs by Tag +- **Steps:** + 1. Filter by tag "milestone" +- **Expected:** Only logs tagged with "milestone" shown (2 logs) + +--- + +## 16. Change Orders + +### TC-CO-001: Propose Change Order +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/change-orders` + 2. Click "New Change Order" + 3. Enter: title="Add under-cabinet lighting", description="LED strip under all upper cabinets" + 4. Submit +- **Expected:** Change order created with status=proposed, AI calculates cost/time impact + +### TC-CO-002: Approve Change Order +- **Steps:** + 1. Open change order co_mum_002 (status=proposed) + 2. Review cost impact (INR 28,000) and time impact (2 days) + 3. Click "Approve" +- **Expected:** Status changes to approved, approvedBy and approvedAt set + +### TC-CO-003: Reject Change Order +- **Steps:** + 1. Create a test change order + 2. Click "Reject" +- **Expected:** Status changes to rejected + +### TC-CO-004: AI Impact Analysis +- **Steps:** + 1. When creating change order, verify AI-generated cost_impact and time_impact_days +- **Expected:** Reasonable estimates based on project context + +--- + +## 17. Product Catalogue + +### TC-CAT-001: Text Search +- **Steps:** + 1. Navigate to `/marketplace/catalogue` + 2. Search "sofa" +- **Expected:** Products matching "sofa" returned (prod_sofa_001) + +### TC-CAT-002: Browse by Category +- **Steps:** + 1. Click "Furniture" category +- **Expected:** 4 furniture products shown (sofa, coffee table, bookshelf, TV unit) + +### TC-CAT-003: Multi-Vendor Price Comparison +- **Steps:** + 1. Open any product detail page +- **Expected:** Prices from all vendors shown with valid date ranges + +### TC-CAT-004: Product Specifications +- **Steps:** + 1. Open prod_tile_001 + 2. Check specifications +- **Expected:** Water absorption, scratch resistance, slip resistance shown + +### TC-CAT-005: Visual Similarity Search +- **Steps:** + 1. Upload an image of a sofa + 2. Search visually +- **Expected:** Products with similar visual embedding returned (CLIP/pgvector) + +--- + +## 18. Procurement & Purchase Orders + +### TC-PO-001: Create Purchase Order +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/procurement` + 2. Click "New Purchase Order" + 3. Select vendor: Kajaria + 4. Add items: Quartz Countertop x 5sqm @ 12000/sqm + 5. Submit +- **Expected:** PO created with status=draft, totalAmount calculated + +### TC-PO-002: Submit PO to Vendor +- **Steps:** + 1. Open draft PO + 2. Click "Submit" +- **Expected:** Status changes to submitted + +### TC-PO-003: PO Status Lifecycle +- **Steps:** + 1. Transition PO through: draft → submitted → confirmed → shipped → delivered + 2. Set actual delivery date when marking delivered +- **Expected:** Each transition updates status and timestamps correctly + +### TC-PO-004: View All POs +- **Steps:** + 1. View procurement page for Mumbai Apartment +- **Expected:** 3 POs shown (tiles=delivered, hardware=confirmed, lights=draft) + +--- + +## 19. Delivery Tracking + +### TC-DEL-001: Create Delivery +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/deliveries` + 2. Click "Track New Delivery" + 3. Link to PO po_mum_lights_001 + 4. Enter vendor name, description, tracking number +- **Expected:** Delivery record created with status=pending + +### TC-DEL-002: Update Delivery Status +- **Steps:** + 1. Transition through: pending → dispatched → in_transit → delivered +- **Expected:** Status updates correctly, estimated vs actual delivery dates tracked + +### TC-DEL-003: Inspection Checklist +- **Steps:** + 1. After delivery arrives, open inspection + 2. Check each item (quantity matches, no damage, correct batch) + 3. Upload inspection photos + 4. Submit +- **Expected:** Checklist saved, status changes to inspected or rejected + +### TC-DEL-004: View Delivery History +- **Steps:** + 1. View deliveries for Mumbai Apartment +- **Expected:** 2 deliveries shown with different statuses + +--- + +## 20. Payments & Invoicing + +### TC-PAY-001: Milestone Payment +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/payments` + 2. Click "Pay" on milestone ms_mum_003 (Tiling Complete) + 3. Select payment provider (Razorpay) + 4. Complete payment flow +- **Expected:** Payment record created, status changes to completed, milestone updated + +### TC-PAY-002: Payment History +- **Steps:** + 1. View payments page +- **Expected:** 3 payments shown (2 completed, 1 pending), totals correct + +### TC-PAY-003: Invoice Generation +- **Steps:** + 1. Verify invoices exist for completed payments + 2. Download invoice PDF +- **Expected:** Formatted invoice with correct amounts and dates + +### TC-PAY-004: Failed Payment +- **Steps:** + 1. Initiate payment with invalid card/method +- **Expected:** Payment status=failed, appropriate error message shown + +--- + +## 21. Contractor Marketplace + +### TC-CONTR-001: Search Contractors +- **Steps:** + 1. Navigate to `/marketplace` + 2. Filter by city="Mumbai" + 3. Filter by specialization="carpentry" +- **Expected:** Rajesh Carpenter appears in results + +### TC-CONTR-002: View Contractor Profile +- **Steps:** + 1. Click on Rajesh Carpenter +- **Expected:** Profile shows: bio, specializations, 18 years experience, 4.6 rating, certifications, 23 reviews + +### TC-CONTR-003: Hire Contractor +- **Steps:** + 1. Click "Hire" on contractor profile + 2. Select project: Mumbai Apartment + 3. Set role=carpenter, dates, agreed amount +- **Expected:** Contractor assignment created + +### TC-CONTR-004: Write Review +- **Steps:** + 1. After contractor completes work + 2. Write review: 5 stars, title, text +- **Expected:** Review published, contractor rating recalculated + +### TC-CONTR-005: Refer Contractor +- **Steps:** + 1. Click "Refer" on contractor profile + 2. Enter referee email and message +- **Expected:** Referral created with status=sent + +### TC-CONTR-006: Filter by Verified Status +- **Steps:** + 1. Toggle "Verified Only" filter +- **Expected:** Only verified contractors shown (all 3 sample contractors are verified) + +--- + +## 22. Collaboration & Comments + +### TC-COLLAB-001: Add Comment on Design +- **Steps:** + 1. Open design variant dv_mum_liv_modern_001 + 2. Add comment: "Can we add a reading nook by the window?" +- **Expected:** Comment saved with timestamp, visible to all project participants + +### TC-COLLAB-002: Threaded Reply +- **Steps:** + 1. Reply to an existing comment +- **Expected:** Reply nested under parent comment + +### TC-COLLAB-003: Resolve Comment +- **Steps:** + 1. Mark a comment as resolved +- **Expected:** Comment shows "resolved" badge, filterable + +### TC-COLLAB-004: Create Discussion Thread +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/collaboration` + 2. Create thread: title="Flooring Material Decision", category=design_decision + 3. Add messages +- **Expected:** Thread created, messages with timestamps and user attribution + +### TC-COLLAB-005: Decision Logging +- **Steps:** + 1. In a thread, mark a message as "decision" +- **Expected:** Message flagged as decision, easily findable in thread + +### TC-COLLAB-006: @Mentions +- **Steps:** + 1. In a message, type @bob + 2. Submit +- **Expected:** Bob receives notification about the mention + +--- + +## 23. Notifications + +### TC-NOTIF-001: View Notifications +- **Steps:** + 1. Sign in as Alice + 2. Click bell icon +- **Expected:** Notification list shows all notifications, unread highlighted + +### TC-NOTIF-002: Unread Count Badge +- **Steps:** + 1. Check bell icon badge count + 2. Should match count of notifications where read=false +- **Expected:** Badge shows "2" (approval pending + cut list in progress for Alice) + +### TC-NOTIF-003: Mark as Read +- **Steps:** + 1. Click on an unread notification +- **Expected:** Notification marked as read, badge count decrements + +### TC-NOTIF-004: Click-Through Navigation +- **Steps:** + 1. Click notification with link (e.g., "Design generated" links to design page) +- **Expected:** Navigates to the linked page + +### TC-NOTIF-005: Real-time WebSocket Push +- **Steps:** + 1. Keep notification panel open + 2. In another session, trigger an event (e.g., complete a job) +- **Expected:** New notification appears in real-time without page refresh + +--- + +## 24. Quality Assurance & Punch List + +### TC-QA-001: Create Quality Checkpoint +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/quality` + 2. Click "New Checkpoint" + 3. Milestone: waterproofing_complete + 4. Trade: waterproofing + 5. Add checklist: ["Membrane covers all wet areas", "48-hour flood test"] + 6. Save +- **Expected:** Checkpoint created with status=pending + +### TC-QA-002: Inspect Checkpoint +- **Steps:** + 1. Open pending checkpoint + 2. Check each item, add notes + 3. Upload inspection photos + 4. Mark as "passed" or "failed" +- **Expected:** Status updated, inspection timestamp recorded + +### TC-QA-003: Create Punch List Item +- **Steps:** + 1. Click "New Punch List Item" + 2. Enter: title="Chipped tile in kitchen", severity=minor, category=tiling + 3. Assign to contractor + 4. Upload photo +- **Expected:** Punch list item created with status=open + +### TC-QA-004: Punch List Lifecycle +- **Steps:** + 1. Transition through: open → in_progress → resolved → verified +- **Expected:** Each transition updates timestamps (resolvedAt, verifiedAt) + +### TC-QA-005: Severity Filtering +- **Steps:** + 1. Filter punch list by severity: critical, major, minor, observation +- **Expected:** Only items of selected severity shown + +--- + +## 25. Handover Package + +### TC-HAND-001: Generate Handover Package +- **Steps:** + 1. Navigate to `/project/prj_delhi_003/handover` + 2. Click "Generate Handover Package" +- **Expected:** Compiles as-built drawings, material register, contractor directory, operational guides, maintenance manual + +### TC-HAND-002: View Package Contents +- **Steps:** + 1. View generated handover for Delhi Penthouse (ho_del_001) +- **Expected:** Shows: 3 drawing files, 3 material register entries, 4 contractors, 4 operational guides + +### TC-HAND-003: Client Sign-Off +- **Steps:** + 1. Click "Client Sign Off" +- **Expected:** clientSignedAt timestamp set + +### TC-HAND-004: Deliver Package +- **Steps:** + 1. After sign-off, click "Mark as Delivered" +- **Expected:** Status=delivered, deliveredAt set, project can move to completed + +--- + +## 26. Cost & Timeline Predictions + +### TC-PRED-001: Generate Cost Prediction +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/predictions` + 2. Click "Predict Cost" +- **Expected:** AI returns: predicted cost, confidence range (low/high), risk factors, cost breakdown by category + +### TC-PRED-002: Generate Timeline Prediction +- **Steps:** + 1. Click "Predict Timeline" +- **Expected:** Predicted days, confidence range, critical risks with mitigation, phase breakdown + +### TC-PRED-003: Verify Confidence Ranges +- **Steps:** + 1. Check that confidence_low < predicted_cost < confidence_high + 2. For cpred_mum_001: 720000 < 825000 < 950000 +- **Expected:** Range is mathematically valid + +### TC-PRED-004: Risk Factor Assessment +- **Steps:** + 1. Review risk factors in prediction +- **Expected:** Each risk has name, impact amount, and probability (0-1) + +--- + +## 27. Budget Optimizer + +### TC-BUDGET-001: Generate Economy Scenario +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/budget-optimizer` + 2. Click "Generate Savings Scenario" +- **Expected:** List of substitutions with original item, replacement, savings per item, and reason + +### TC-BUDGET-002: Verify Savings Calculation +- **Steps:** + 1. For bs_mum_001: original=825000, optimized=680000, savings=145000 (17.6%) + 2. Verify sum of individual substitution savings = total savings +- **Expected:** Math checks out + +### TC-BUDGET-003: Accept/Reject Scenario +- **Steps:** + 1. Accept economy scenario + 2. Verify status changes to "accepted" +- **Expected:** Status updated, BOM optionally recalculated with substitutions + +--- + +## 28. Sustainability Scoring + +### TC-SUST-001: Generate Sustainability Report +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/sustainability` + 2. Click "Generate Report" +- **Expected:** Total carbon (kg CO2), material vs transport carbon, sustainability score (0-100), LEED points + +### TC-SUST-002: Green Alternatives +- **Steps:** + 1. Review green_alternatives in report +- **Expected:** Each alternative shows: original material, green replacement, carbon saved, cost delta + +### TC-SUST-003: Carbon Breakdown +- **Steps:** + 1. Verify material_carbon + transport_carbon = total_carbon + 2. For sust_mum_001: 2100 + 750 = 2850 kg +- **Expected:** Math is correct + +--- + +## 29. Financial Reports + +### TC-FIN-001: Budget vs Actuals +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/financial-reports` + 2. View budget vs actuals chart +- **Expected:** Chart shows planned vs actual spend by category + +### TC-FIN-002: Expenditure Timeline +- **Steps:** + 1. View expenditure over time +- **Expected:** Line chart showing cumulative spend over project duration + +### TC-FIN-003: Category Breakdown +- **Steps:** + 1. View category breakdown +- **Expected:** Pie chart showing spend by category (materials, labor, appliances, etc.) + +### TC-FIN-004: Export CSV +- **Steps:** + 1. Click "Export CSV" +- **Expected:** CSV file with all financial data for the project + +--- + +## 30. Portfolio Management + +### TC-PORT-001: Create Portfolio +- **Steps:** + 1. Navigate to `/portfolios` + 2. Click "New Portfolio" + 3. Enter: name="My Homes", description="All residential projects" +- **Expected:** Portfolio created + +### TC-PORT-002: Add Projects to Portfolio +- **Steps:** + 1. Add Mumbai Apartment and Bangalore Villa to portfolio +- **Expected:** Both projects linked, sortable order + +### TC-PORT-003: View Portfolio +- **Steps:** + 1. Open portfolio port_alice_001 +- **Expected:** Shows 2 projects with status badges, combined stats + +--- + +## 31. Digital Twin & IoT + +### TC-IOT-001: View Digital Twin +- **Steps:** + 1. Navigate to `/project/prj_delhi_003/digital-twin` +- **Expected:** 3D model loads, IoT device markers shown at positioned locations + +### TC-IOT-002: View Live Sensor Data +- **Steps:** + 1. Click on temperature sensor +- **Expected:** Current reading (23.9°C) and historical chart (last 24h readings) + +### TC-IOT-003: Add IoT Device +- **Steps:** + 1. Click "Add Device" + 2. Select type=temperature, position in room +- **Expected:** Device created, appears on twin visualization + +### TC-IOT-004: Emergency References +- **Steps:** + 1. View emergency references panel +- **Expected:** Lists: water shutoff, gas shutoff, electrical breaker, fire extinguisher with locations + +### TC-IOT-005: Energy Monitoring +- **Steps:** + 1. View energy meter readings +- **Expected:** Shows cumulative kWh with daily consumption calculated (28.3 - 12.5 = 15.8 kWh/day) + +--- + +## 32. Maintenance Scheduling + +### TC-MAINT-001: View Schedule +- **Steps:** + 1. Navigate to `/project/prj_delhi_003/maintenance` +- **Expected:** 5 maintenance items shown with next due dates, categories, estimated costs + +### TC-MAINT-002: Log Maintenance +- **Steps:** + 1. Select "AC Service & Filter Clean" + 2. Click "Log Completion" + 3. Enter: performedBy="Voltas", cost=2500, notes="Filters cleaned" + 4. Upload photo +- **Expected:** Log created, nextDueAt auto-advances by frequency_days (90 days) + +### TC-MAINT-003: Overdue Items +- **Steps:** + 1. Check for items where nextDueAt < today +- **Expected:** Overdue items highlighted with warning + +### TC-MAINT-004: Create Maintenance Item +- **Steps:** + 1. Click "Add Maintenance Item" + 2. Enter: item="Pest Control", category=exterior, frequency=180 days, cost=3000 +- **Expected:** Item created with next due date calculated + +--- + +## 33. Warranty Tracking + +### TC-WAR-001: View Warranties +- **Steps:** + 1. Navigate to `/project/prj_delhi_003/warranties` +- **Expected:** 5 warranties shown with item names, brands, start/end dates, types, statuses + +### TC-WAR-002: File Warranty Claim +- **Steps:** + 1. Open Bosch Dishwasher warranty (war_del_001) + 2. Click "File Claim" + 3. Enter issue description, upload photos +- **Expected:** Claim created with status=filed + +### TC-WAR-003: Claim Lifecycle +- **Steps:** + 1. Transition claim through: filed → in_review → approved → resolved +- **Expected:** Status updates, resolution date set when resolved + +### TC-WAR-004: Expired Warranty Detection +- **Steps:** + 1. Verify warranties with endDate < today show status=expired +- **Expected:** Expired warranties clearly flagged + +### TC-WAR-005: Warranty Type Filtering +- **Steps:** + 1. Filter by type: manufacturer, extended, contractor +- **Expected:** Correct warranties shown per filter + +--- + +## 34. Offcuts Exchange + +### TC-OFFCUT-001: List Offcut Material +- **Steps:** + 1. Navigate to `/marketplace/offcuts` + 2. Click "List Material" + 3. Enter: title="Leftover kitchen tiles", materialType=tile, quantity=2 sqm, condition=new, price=1800 INR + 4. Upload photos +- **Expected:** Listing created with status=active + +### TC-OFFCUT-002: Browse Offcuts +- **Steps:** + 1. Browse offcut listings + 2. Filter by material type "stone" +- **Expected:** Nero Marquina marble offcut shown + +### TC-OFFCUT-003: Send Inquiry +- **Steps:** + 1. Open listing off_001 + 2. Send inquiry message: "Interested in the marble. Can we negotiate?" +- **Expected:** Inquiry created, seller notified + +### TC-OFFCUT-004: Respond to Inquiry +- **Steps:** + 1. Sign in as seller (Bob) + 2. View inquiry, reply +- **Expected:** Inquiry status changes to "replied" + +### TC-OFFCUT-005: Mark as Sold +- **Steps:** + 1. After agreeing on deal, mark listing as "sold" +- **Expected:** Status=sold, listing no longer appears in active search + +--- + +## 35. Community Gallery + +### TC-GALLERY-001: Publish to Gallery +- **Steps:** + 1. Open a completed project + 2. Click "Publish to Gallery" + 3. Enter: title, description, select images, add style tags +- **Expected:** Gallery entry created with is_public=true + +### TC-GALLERY-002: Browse Gallery +- **Steps:** + 1. Navigate to `/marketplace/gallery` +- **Expected:** Public entries shown with images, styles, like counts + +### TC-GALLERY-003: Like Project +- **Steps:** + 1. Click "Like" on a gallery entry +- **Expected:** Like count increments + +### TC-GALLERY-004: Filter by Style +- **Steps:** + 1. Filter gallery by style="art_deco" +- **Expected:** Only art deco entries shown (gal_001) + +--- + +## 36. Developer API Portal + +### TC-API-001: Register App +- **Steps:** + 1. Navigate to `/developer` + 2. Click "Register New App" + 3. Enter: name, redirect URIs, select scopes +- **Expected:** App created with clientId and clientSecret + +### TC-API-002: Generate Access Token +- **Steps:** + 1. Use OAuth flow to generate access token +- **Expected:** Token issued with selected scopes, expiry date set + +### TC-API-003: Make API Call +- **Steps:** + 1. Use access token to call GET /api/v1/projects +- **Expected:** Returns authorized user's projects + +### TC-API-004: Rate Limiting +- **Steps:** + 1. Send 101+ requests within 1 minute (standard tier limit=100) +- **Expected:** 429 Too Many Requests returned after limit exceeded + +### TC-API-005: Webhook Subscription +- **Steps:** + 1. Create webhook for event "project.created" + 2. Set target URL and secret + 3. Create a new project +- **Expected:** Webhook fires to target URL with signed payload + +### TC-API-006: View Request Logs +- **Steps:** + 1. Navigate to developer portal + 2. View request logs for app +- **Expected:** Shows endpoint, method, status code, response time for each API call + +--- + +## 37. Admin Panel + +### TC-ADMIN-001: Admin Dashboard +- **Steps:** + 1. Sign in as Bob (admin) + 2. Navigate to `/admin` +- **Expected:** Shows: total users, total projects, active jobs, system health indicators + +### TC-ADMIN-002: User Management +- **Steps:** + 1. Navigate to `/admin/users` + 2. View all users +- **Expected:** Lists all users with email, role, enabled status, creation date + +### TC-ADMIN-003: Toggle User Enabled +- **Steps:** + 1. Disable Alice's account (enabled=false) + 2. Try to sign in as Alice +- **Expected:** Alice cannot sign in while disabled + +### TC-ADMIN-004: Job Queue Monitoring +- **Steps:** + 1. Navigate to `/admin/jobs` + 2. View all jobs +- **Expected:** Shows jobs across all statuses (pending, running, completed, failed, cancelled) + +### TC-ADMIN-005: Filter Jobs by Status +- **Steps:** + 1. Filter jobs by status=failed +- **Expected:** Only failed jobs shown (job_segment_fail_001) + +### TC-ADMIN-006: Retry Failed Job +- **Steps:** + 1. Click "Retry" on a failed job +- **Expected:** New job created with same inputs, status=pending + +### TC-ADMIN-007: System Health +- **Steps:** + 1. Navigate to `/admin/system` +- **Expected:** Shows: DB connection (green/red), storage service, ML services, queue depth + +--- + +## 38. User Settings & Preferences + +### TC-SETTINGS-001: Change Currency +- **Steps:** + 1. Navigate to `/dashboard/settings` + 2. Change currency from INR to USD + 3. Save +- **Expected:** All prices across the app display in USD with conversion + +### TC-SETTINGS-002: Change Unit System +- **Steps:** + 1. Change from metric to imperial +- **Expected:** Dimensions display in feet/inches instead of millimeters + +### TC-SETTINGS-003: Add API Key +- **Steps:** + 1. Navigate to API Keys section + 2. Add OpenAI key: label="My OpenAI", key="sk-test123..." +- **Expected:** Key encrypted (AES-256-GCM), keyPrefix stored (sk-te...), iv and authTag saved + +### TC-SETTINGS-004: Delete API Key +- **Steps:** + 1. Delete a stored API key +- **Expected:** Key record removed, last_used_at and encrypted data gone + +### TC-SETTINGS-005: Profile Update +- **Steps:** + 1. Update name + 2. Save +- **Expected:** Name updated across the app (header, comments, etc.) + +--- + +## 39. AR/VR Viewer + +### TC-AR-001: Launch AR Mode +- **Steps:** + 1. Navigate to `/project/prj_mumbai_001/ar` + 2. Click "AR Mode" (requires WebXR-compatible browser) +- **Expected:** Camera feed with AR furniture placement overlay + +### TC-AR-002: Place Furniture in AR +- **Steps:** + 1. In AR mode, select a furniture item + 2. Tap on floor surface to place +- **Expected:** 3D furniture model placed at tapped location in AR space + +### TC-AR-003: VR Walkthrough +- **Steps:** + 1. Click "VR Mode" (requires VR headset) + 2. Teleport through room +- **Expected:** Immersive room walkthrough with teleportation + +### TC-AR-004: QR Code Sharing +- **Steps:** + 1. Click "Share via QR" +- **Expected:** QR code generated, scannable by mobile device to open AR view + +--- + +## 40. Cross-cutting: Error Handling & Edge Cases + +### TC-ERR-001: Network Offline +- **Steps:** + 1. Disconnect network + 2. Try any CRUD operation +- **Expected:** Graceful error message, no data loss, retry possible when online + +### TC-ERR-002: Session Expiry +- **Steps:** + 1. Wait for session to expire (or manually expire it) + 2. Try an action +- **Expected:** Redirect to sign-in, return to previous page after re-auth + +### TC-ERR-003: Concurrent Edit Conflict +- **Steps:** + 1. Open same project in 2 tabs + 2. Edit project name in both tabs + 3. Save both +- **Expected:** Last write wins or conflict resolution shown + +### TC-ERR-004: Long-Running Job Timeout +- **Steps:** + 1. Trigger a design generation + 2. Simulate ML service being down +- **Expected:** Job status shows "failed" with timeout error after threshold + +### TC-ERR-005: Invalid Input Injection +- **Steps:** + 1. Enter `` in project name + 2. Enter `'; DROP TABLE users; --` in search +- **Expected:** Input sanitized, no XSS or SQL injection possible + +### TC-ERR-006: File Upload During Design Generation +- **Steps:** + 1. While a design job is running for a room + 2. Upload a new photo to the same room +- **Expected:** Upload succeeds independently, doesn't interfere with running job + +### TC-ERR-007: Delete Project With Active Jobs +- **Steps:** + 1. Start a job for a project + 2. Try to delete the project while job is running +- **Expected:** Either: block deletion until job completes, or cancel job and cascade delete + +### TC-ERR-008: Empty Project States +- **Steps:** + 1. Create project with no rooms + 2. Try to access BOM, drawings, timeline pages +- **Expected:** Helpful empty states ("Add rooms first") instead of blank pages or errors + +### TC-ERR-009: Currency Conversion Edge Cases +- **Steps:** + 1. View BOM in USD when data was entered in INR + 2. Verify conversion uses latest exchange rate +- **Expected:** Amounts correctly converted, source currency noted + +### TC-ERR-010: Pagination +- **Steps:** + 1. If a page has 100+ items (products, notifications, etc.) + 2. Verify pagination controls work +- **Expected:** Data loads in pages, no performance degradation + +--- + +## Test Priority Guide + +| Priority | Category | Test IDs | +|----------|----------|----------| +| **P0 - Critical** | Auth, Project CRUD, Design Generation | TC-AUTH-001 to 006, TC-PROJ-001 to 006, TC-DESIGN-001 | +| **P1 - High** | Room CRUD, BOM, Drawings, Payments, Uploads | TC-ROOM-001 to 005, TC-BOM-001 to 005, TC-DRAW-001 to 004, TC-PAY-001 to 004, TC-UPLOAD-001 to 005 | +| **P2 - Medium** | MEP, Scheduling, Procurement, Contractors, Notifications | TC-MEP-001 to 005, TC-SCHED-001 to 004, TC-PO-001 to 004, TC-CONTR-001 to 006, TC-NOTIF-001 to 005 | +| **P3 - Normal** | QA, Handover, Predictions, Collaboration, Settings | TC-QA-001 to 005, TC-HAND-001 to 004, TC-PRED-001 to 004, TC-COLLAB-001 to 006, TC-SETTINGS-001 to 005 | +| **P4 - Low** | IoT, Maintenance, Warranty, Offcuts, Gallery, API Portal | TC-IOT-001 to 005, TC-MAINT-001 to 004, TC-WAR-001 to 005, TC-OFFCUT-001 to 005, TC-GALLERY-001 to 004, TC-API-001 to 006 | + +--- + +## Regression Test Checklist (Quick Smoke) + +Run these after every deploy: + +- [ ] Sign in with OAuth (Google) +- [ ] Create new project +- [ ] Add a room with dimensions +- [ ] Upload a photo +- [ ] Generate a design variant +- [ ] View BOM +- [ ] Navigate to dashboard — projects listed +- [ ] Notifications bell loads +- [ ] Admin panel accessible as admin +- [ ] Sign out works diff --git a/usrflow.md b/usrflow.md new file mode 100644 index 0000000..2bd95d3 --- /dev/null +++ b/usrflow.md @@ -0,0 +1,1109 @@ +# OpenLintel - User Flow Diagrams + +> Comprehensive user journey maps for the end-to-end home design automation platform. + +--- + +## Table of Contents + +1. [High-Level Platform Flow](#1-high-level-platform-flow) +2. [Authentication & Onboarding](#2-authentication--onboarding) +3. [Project Creation & Setup](#3-project-creation--setup) +4. [Room Management](#4-room-management) +5. [Design Generation Pipeline](#5-design-generation-pipeline) +6. [Floor Plan Digitization](#6-floor-plan-digitization) +7. [3D Editor & Collaboration](#7-3d-editor--collaboration) +8. [BOM, Drawings & Manufacturing](#8-bom-drawings--manufacturing) +9. [MEP Engineering](#9-mep-engineering) +10. [Project Timeline & Scheduling](#10-project-timeline--scheduling) +11. [Procurement & Delivery](#11-procurement--delivery) +12. [Payments & Invoicing](#12-payments--invoicing) +13. [Contractor Marketplace](#13-contractor-marketplace) +14. [Quality Assurance & Handover](#14-quality-assurance--handover) +15. [Intelligence & Analytics](#15-intelligence--analytics) +16. [Post-Occupancy (Digital Twin, Maintenance, Warranty)](#16-post-occupancy) +17. [Marketplace & Community](#17-marketplace--community) +18. [Admin Panel](#18-admin-panel) +19. [Developer API Portal](#19-developer-api-portal) + +--- + +## 1. High-Level Platform Flow + +```mermaid +flowchart TD + A[User Arrives] --> B{Authenticated?} + B -->|No| C[Sign In / Sign Up] + C --> D[Dashboard] + B -->|Yes| D + + D --> E[Create New Project] + D --> F[Open Existing Project] + D --> G[Marketplace] + D --> H[Portfolios] + D --> I[Settings] + D --> J[Notifications] + + E --> K[Project Setup] + F --> K + + K --> L[Add Rooms] + L --> M[Upload Photos / Floor Plans] + M --> N[Style Quiz] + N --> O[AI Design Generation] + O --> P[Review & Approve Designs] + + P --> Q[BOM Generation] + P --> R[Technical Drawings] + P --> S[CNC Cut Lists] + P --> T[MEP Calculations] + + Q --> U[Procurement] + R --> U + S --> U + T --> U + + U --> V[Contractor Hiring] + V --> W[Project Timeline] + W --> X[Construction Execution] + + X --> Y[Site Logs] + X --> Z[Quality Checkpoints] + X --> AA[Change Orders] + X --> AB[Deliveries] + + Z --> AC[Punch List] + AC --> AD[Handover Package] + AD --> AE[Post-Occupancy] + + AE --> AF[Digital Twin] + AE --> AG[Maintenance Scheduling] + AE --> AH[Warranty Tracking] +``` + +--- + +## 2. Authentication & Onboarding + +```mermaid +flowchart TD + A[Landing Page /] --> B[Click Sign In] + B --> C[/auth/signin] + + C --> D{Choose Auth Method} + D -->|Google OAuth| E[Google Consent Screen] + D -->|GitHub OAuth| F[GitHub Authorization] + D -->|Email + Password| G[Enter Credentials] + + E --> H{Account Exists?} + F --> H + G --> H + + H -->|No| I[Create User Record] + I --> J[Set Default Preferences] + J --> K[Redirect to Dashboard] + + H -->|Yes| K + + K --> L[/dashboard] + L --> M{First Visit?} + M -->|Yes| N[Show Onboarding Tour] + N --> O[Configure Preferences] + O --> P[Set Currency / Units / Locale] + P --> Q[Add LLM API Keys - Optional] + Q --> R[Dashboard Ready] + + M -->|No| R +``` + +### User Preferences Setup + +```mermaid +flowchart LR + A[/dashboard/settings] --> B[Profile Section] + B --> C[Name / Email / Avatar] + + A --> D[Preferences Section] + D --> E[Currency: USD, INR, EUR, GBP...] + D --> F[Unit System: Metric / Imperial] + D --> G[Locale: en, hi, es...] + + A --> H[API Keys Section] + H --> I[Add OpenAI Key] + H --> J[Add Anthropic Key] + H --> K[Add Google Key] + + I --> L[AES-256-GCM Encryption] + J --> L + K --> L + L --> M[Store Encrypted Key + IV + AuthTag] +``` + +--- + +## 3. Project Creation & Setup + +```mermaid +flowchart TD + A[Dashboard /dashboard] --> B[Click + New Project] + B --> C[Create Project Dialog] + + C --> D[Enter Project Name] + D --> E[Enter Address - Optional] + E --> F[Select Unit System] + F --> G[Click Create] + + G --> H[POST project.create] + H --> I[Project Created - Status: draft] + I --> J[Redirect to /project/id] + + J --> K[Project Overview Page] + K --> L{Next Step?} + + L --> M[Add Rooms] + L --> N[Upload Floor Plan] + L --> O[Take Style Quiz] + L --> P[Upload Photos] + + subgraph "Project Statuses" + S1[draft] --> S2[in_progress] + S2 --> S3[construction] + S3 --> S4[completed] + end +``` + +--- + +## 4. Room Management + +```mermaid +flowchart TD + A[Project Page] --> B[Rooms Tab /project/id/rooms] + B --> C[Click Add Room] + + C --> D[Room Creation Form] + D --> E[Enter Room Name] + E --> F[Select Room Type] + F --> G[Enter Dimensions] + G --> H[Select Floor Level] + H --> I[Click Save] + + I --> J[Room Created] + J --> K[Room Detail /project/id/rooms/roomId] + + K --> L{Actions} + L --> M[Upload Room Photos] + L --> N[Generate Designs] + L --> O[View 3D Editor] + L --> P[Edit Dimensions] + L --> Q[Delete Room] + + subgraph "Room Types" + R1[living_room] + R2[bedroom] + R3[kitchen] + R4[bathroom] + R5[dining_room] + R6[study] + R7[balcony] + R8[pooja_room] + R9[foyer] + R10[laundry] + R11[garage] + R12[closet] + R13[hallway] + R14[kids_room] + R15[other] + end +``` + +--- + +## 5. Design Generation Pipeline + +```mermaid +flowchart TD + A[Room Detail Page] --> B[Click Generate Design] + + B --> C[Design Configuration] + C --> D[Select Style] + C --> E[Select Budget Tier] + C --> F[Add Constraints - Optional] + C --> G[Select Source Photo - Optional] + + D --> H{Style Options} + H --> H1[modern] + H --> H2[contemporary] + H --> H3[traditional] + H --> H4[scandinavian] + H --> H5[industrial] + H --> H6[minimalist] + H --> H7[bohemian] + H --> H8[japandi] + H --> H9[art_deco] + H --> H10[tropical] + + E --> I{Budget Tiers} + I --> I1[economy] + I --> I2[mid_range] + I --> I3[premium] + I --> I4[luxury] + + F --> J[Submit Request] + J --> K[Create Job: design_generation] + K --> L[Job Status: pending] + + L --> M[Worker Picks Up Job] + M --> N[LangGraph Multi-Node Pipeline] + N --> N1[Room Analysis Node] + N1 --> N2[Style Application Node] + N2 --> N3[Constraint Validation Node] + N3 --> N4[Render Generation Node] + N4 --> N5[Specification Output Node] + + N5 --> O[Job Status: completed] + O --> P[Design Variant Created] + P --> Q[View in Gallery /project/id/designs] + + Q --> R{User Actions} + R --> S[Approve Design] + R --> T[Request Revision] + R --> U[Generate BOM] + R --> V[Generate Drawings] + R --> W[Compare Variants] +``` + +### Style Quiz Flow + +```mermaid +flowchart TD + A[/project/id/style-quiz] --> B[Step 1: Room Usage] + B --> C[Step 2: Color Preferences] + C --> D[Step 3: Material Preferences] + D --> E[Step 4: Budget Range] + E --> F[Step 5: Inspiration Images] + + F --> G[Submit Quiz] + G --> H[AI Analyzes Responses] + H --> I[Detected Styles with Scores] + I --> J[Auto-Generated Mood Board] + J --> K[Color Palette Suggestions] + K --> L[Save Style Preferences] + L --> M[Use in Design Generation] +``` + +--- + +## 6. Floor Plan Digitization + +```mermaid +flowchart TD + A[Project Page] --> B[Floor Plan Tab /project/id/floor-plan] + + B --> C[Upload Floor Plan Image/PDF] + C --> D[File Upload to S3/MinIO] + D --> E[Create Job: floor_plan_digitize] + + E --> F[Vision Engine Processes] + F --> G[VLM Extracts Rooms] + G --> H[Detect Dimensions] + H --> I[Identify Doors & Windows] + I --> J[Generate Structured JSON] + J --> K[Generate DXF Output] + + K --> L[Job Complete] + L --> M[Interactive SVG Viewer] + + M --> N{User Actions} + N --> O[Verify Room Boundaries] + N --> P[Adjust Dimensions] + N --> Q[Auto-Create Rooms from Plan] + N --> R[Download DXF] + N --> S[Re-process with Corrections] +``` + +--- + +## 7. 3D Editor & Collaboration + +```mermaid +flowchart TD + A[Project Page] --> B[Editor Tab /project/id/editor] + + B --> C[Load React Three Fiber Canvas] + C --> D[Load Room Geometry] + D --> E[Load Furniture/Fixtures] + + E --> F{Editor Tools} + F --> G[Select Object] + F --> H[Move - Translate] + F --> I[Rotate] + F --> J[Scale] + F --> K[Add Furniture from Catalog] + F --> L[Delete Object] + + G --> M[Transform Controls] + M --> N[Snap to Grid] + M --> O[Undo / Redo Stack] + + subgraph "Real-time Collaboration" + P[User A Edits] --> Q[Y.js CRDT Sync] + Q --> R[WebSocket Broadcast] + R --> S[User B Sees Changes] + S --> T[Cursor Awareness] + end + + B --> U[AR/VR Mode /project/id/ar] + U --> V{Platform} + V --> W[AR: Place Furniture in Real Space] + V --> X[VR: Walkthrough with Teleportation] + V --> Y[Generate QR Code to Share] +``` + +--- + +## 8. BOM, Drawings & Manufacturing + +### BOM Flow + +```mermaid +flowchart TD + A[Design Variant Page] --> B[Click Generate BOM] + B --> C[Create Job: bom_calculation] + C --> D[BOM Engine - AI Agent] + + D --> E[Analyze Design Spec] + E --> F[Categorize Materials] + F --> G[Calculate Quantities] + G --> H[Lookup Unit Prices] + H --> I[Compute Totals] + + I --> J[BOM Results Created] + J --> K[View BOM Table /project/id/bom] + + K --> L{Actions} + L --> M[Export CSV] + L --> N[Export PDF] + L --> O[View by Category] + L --> P[Substitute Materials] + L --> Q[Link to Procurement] + + subgraph "BOM Categories" + C1[Furniture] + C2[Flooring] + C3[Lighting] + C4[Paint & Finishes] + C5[Hardware] + C6[Plumbing Fixtures] + C7[Electrical] + C8[Tiles & Stone] + C9[Fabric & Upholstery] + C10[Appliances] + end +``` + +### Technical Drawings Flow + +```mermaid +flowchart TD + A[Design Variant Page] --> B[Click Generate Drawings] + B --> C[Create Job: drawing] + C --> D[Drawing Generator Service] + + D --> E{Drawing Types} + E --> F[Floor Plan Layout] + E --> G[Elevations - 4 walls] + E --> H[Cross Sections] + E --> I[Reflected Ceiling Plan] + E --> J[Flooring Layout] + E --> K[Electrical Layout] + + F --> L[Generate DXF + PDF + SVG] + G --> L + H --> L + I --> L + J --> L + K --> L + + L --> M[Store in S3/MinIO] + M --> N[View Gallery /project/id/drawings] + + N --> O{Actions} + O --> P[Download DXF] + O --> Q[Download PDF] + O --> R[View SVG Inline] + O --> S[Export IFC/BIM] +``` + +### CNC Cut Lists Flow + +```mermaid +flowchart TD + A[Design Variant Page] --> B[Click Generate Cut Lists] + B --> C[Create Job: cutlist] + C --> D[Cut List Engine] + + D --> E[Parse Design Furniture Specs] + E --> F[Generate Panel List] + F --> G[Apply Edge Banding] + G --> H[Set Grain Direction] + H --> I[Run Nesting Optimizer] + + I --> J[rectpack / DeepNest] + J --> K[Sheet Layouts Generated] + K --> L[Calculate Waste %] + + L --> M[Cut List Results Saved] + M --> N[View /project/id/cutlist] + + N --> O{Output} + O --> P[Panel List Table] + O --> Q[Sheet Layout Diagrams] + O --> R[Hardware Schedule] + O --> S[Waste Report] + O --> T[Offcut Details for Exchange] +``` + +--- + +## 9. MEP Engineering + +```mermaid +flowchart TD + A[Design Variant Page] --> B[Click MEP Calculations] + B --> C{Select Type} + + C --> D[Electrical] + C --> E[Plumbing] + C --> F[HVAC] + + D --> G[Create Job: mep - electrical] + G --> H[AI Agent + NEC 2020] + H --> I[Circuit Load Analysis] + I --> J[Outlet/Switch Placement] + J --> K[Wire Sizing + Breaker Schedule] + K --> L[Electrical Layout Generated] + + E --> M[Create Job: mep - plumbing] + M --> N[AI Agent + IPC 2021] + N --> O[Fixture Unit Count] + O --> P[Pipe Sizing] + P --> Q[Drainage Layout] + Q --> R[Plumbing Layout Generated] + + F --> S[Create Job: mep - hvac] + S --> T[AI Agent + ASHRAE 90.1] + T --> U[Cooling/Heating Load] + U --> V[Duct Sizing] + V --> W[Equipment Selection] + W --> X[HVAC Layout Generated] + + L --> Y[View /project/id/mep] + R --> Y + X --> Y + + Y --> Z[Standards Citations Included] +``` + +--- + +## 10. Project Timeline & Scheduling + +```mermaid +flowchart TD + A[Project Page] --> B[Timeline Tab /project/id/timeline] + + B --> C[Click Generate Schedule] + C --> D[AI Analyzes Project Scope] + D --> E[Generate Tasks & Dependencies] + E --> F[Identify Critical Path] + F --> G[Set Milestones] + + G --> H[Gantt Chart View] + H --> I{Actions} + I --> J[Adjust Task Dates] + I --> K[Link Milestones to Payments] + I --> L[Export Schedule] + + subgraph "Site Logs" + M[/project/id/site-logs] --> N[Create Daily Log] + N --> O[Date + Title + Notes] + O --> P[Weather + Worker Count] + P --> Q[Upload Photos] + Q --> R[Add Tags] + end + + subgraph "Change Orders" + S[/project/id/change-orders] --> T[Propose Change] + T --> U[AI Estimates Cost & Time Impact] + U --> V{Review} + V -->|Approve| W[Update Schedule & BOM] + V -->|Reject| X[Change Rejected] + end +``` + +--- + +## 11. Procurement & Delivery + +```mermaid +flowchart TD + A[BOM Approved] --> B[/project/id/procurement] + + B --> C[Create Purchase Order] + C --> D[Select Vendor] + D --> E[Add Line Items from BOM] + E --> F[Set Quantities & Prices] + F --> G[Submit PO] + + G --> H[PO Status: draft] + H --> I[Submit to Vendor] + I --> J[PO Status: submitted] + J --> K[Vendor Confirms] + K --> L[PO Status: confirmed] + L --> M[Vendor Ships] + M --> N[PO Status: shipped] + N --> O[Materials Arrive] + O --> P[PO Status: delivered] + + subgraph "Delivery Tracking" + Q[/project/id/deliveries] --> R[Track Delivery] + R --> S[Status: pending] + S --> T[Status: dispatched] + T --> U[Status: in_transit] + U --> V[Status: delivered] + V --> W[Inspection Checklist] + W --> X{Pass?} + X -->|Yes| Y[Status: inspected] + X -->|No| Z[Status: rejected] + end + + subgraph "Vendor Management" + VA[/project/id/vendors] --> VB[View Vendor Ratings] + VB --> VC[Delivery Score] + VB --> VD[Quality Score] + VB --> VE[Pricing Score] + VB --> VF[Order History] + end +``` + +--- + +## 12. Payments & Invoicing + +```mermaid +flowchart TD + A[/project/id/payments] --> B{Payment Type} + + B --> C[Milestone-Linked Payment] + B --> D[Direct Payment] + + C --> E[Select Milestone] + E --> F[Enter Amount] + F --> G{Payment Provider} + G --> H[Stripe Checkout] + G --> I[Razorpay Checkout] + + H --> J[Payment Processing] + I --> J + J --> K{Result} + K -->|Success| L[Status: completed] + K -->|Failure| M[Status: failed] + + L --> N[Update Milestone Status] + N --> O[Send Notification] + O --> P[Generate Invoice] + + subgraph "Invoice Lifecycle" + Q[Invoice Created] --> R[Status: draft] + R --> S[Send Invoice] + S --> T[Status: sent] + T --> U{Payment Received?} + U -->|Yes| V[Status: paid] + U -->|Overdue| W[Status: overdue] + end + + subgraph "Financial Reports" + FR[/project/id/financial-reports] --> FR1[Budget vs. Actuals] + FR --> FR2[Expenditure Timeline] + FR --> FR3[Category Breakdown] + FR --> FR4[Per Sq-ft Benchmarks] + end +``` + +--- + +## 13. Contractor Marketplace + +```mermaid +flowchart TD + A[/marketplace] --> B[Search Contractors] + + B --> C[Filter by City] + B --> D[Filter by Specialization] + B --> E[Filter by Rating] + B --> F[Filter by Verified Status] + + C --> G[Results List] + D --> G + E --> G + F --> G + + G --> H[View Profile /marketplace/contractorId] + H --> I[See Bio & Portfolio] + H --> J[See Reviews & Ratings] + H --> K[See Certifications] + H --> L[See Years Experience] + + H --> M{Actions} + M --> N[Hire Contractor] + N --> O[Assign to Project] + O --> P[Set Role & Dates] + P --> Q[Set Agreed Amount] + Q --> R[Contractor Assignment Created] + + M --> S[Write Review] + S --> T[Rate 1-5 Stars] + T --> U[Add Title & Review Text] + U --> V[Review Published] + + M --> W[Refer to Friend] + W --> X[Enter Referee Email] + X --> Y[Referral Sent] +``` + +--- + +## 14. Quality Assurance & Handover + +### QA & Punch List + +```mermaid +flowchart TD + A[/project/id/quality] --> B[Quality Checkpoints] + + B --> C[Create Checkpoint] + C --> D[Select Milestone Stage] + D --> E[Select Trade] + E --> F[Add Checklist Items] + F --> G[Assign Inspector] + + G --> H{Inspection} + H --> I[Check Each Item] + I --> J[Upload Inspection Photos] + J --> K{All Pass?} + K -->|Yes| L[Checkpoint: passed] + K -->|No| M[Checkpoint: failed] + + M --> N[Create Punch List Items] + N --> O[Set Severity: critical/major/minor] + O --> P[Assign to Contractor] + P --> Q[Contractor Fixes Issue] + Q --> R[Mark as Resolved] + R --> S[Verify Fix] + S --> T[Mark as Verified] +``` + +### Handover Package + +```mermaid +flowchart TD + A[/project/id/handover] --> B[Generate Handover Package] + + B --> C[Compile As-Built Drawings] + C --> D[Generate Material Register] + D --> E[Compile Contractor Directory] + E --> F[Create Operational Guides] + F --> G[Generate Maintenance Manual PDF] + + G --> H[Handover Package Ready] + H --> I{Client Review} + I --> J[Client Signs Off] + J --> K[Package Delivered] + K --> L[Project Status: completed] + + subgraph "Handover Contents" + HC1[As-Built Drawings DXF/PDF] + HC2[Material Register with Batch Numbers] + HC3[Contractor Directory with Contacts] + HC4[System Operational Guides] + HC5[Maintenance Manual] + HC6[Warranty Documents] + end +``` + +--- + +## 15. Intelligence & Analytics + +```mermaid +flowchart TD + A[/dashboard/analytics] --> B{Analytics Views} + + B --> C[Budget vs. Actual Chart] + B --> D[Cost Breakdown by Category] + B --> E[Timeline Progress] + B --> F[Per Sq-ft Benchmarks] + B --> G[Export CSV] + + subgraph "AI Predictions" + H[/project/id/predictions] --> I[Generate Cost Prediction] + I --> J[LLM Analyzes Project Data] + J --> K[Predicted Cost + Confidence Range] + K --> L[Risk Factors Identified] + + H --> M[Generate Timeline Prediction] + M --> N[Predicted Days + Confidence] + N --> O[Critical Risks & Mitigations] + O --> P[Phase Breakdown] + end + + subgraph "Budget Optimizer" + Q[/project/id/budget-optimizer] --> R[Generate Scenarios] + R --> S[AI Suggests Material Substitutions] + S --> T[Show Savings per Substitution] + T --> U{Accept?} + U -->|Yes| V[Update BOM] + U -->|No| W[Keep Original] + end + + subgraph "Sustainability" + X[/project/id/sustainability] --> Y[Generate Report] + Y --> Z[Total Carbon Footprint] + Z --> AA[LEED Points Estimate] + AA --> AB[Green Alternatives Suggested] + end +``` + +--- + +## 16. Post-Occupancy + +### Digital Twin & IoT + +```mermaid +flowchart TD + A[/project/id/digital-twin] --> B[Create Digital Twin] + B --> C[Upload/Generate 3D Model] + C --> D[Twin Status: active] + + D --> E[Add IoT Devices] + E --> F{Device Types} + F --> G[Temperature Sensor] + F --> H[Humidity Sensor] + F --> I[Motion Sensor] + F --> J[Energy Meter] + F --> K[Water Flow Sensor] + + G --> L[Position in Room] + H --> L + I --> L + J --> L + K --> L + + L --> M[Live Dashboard] + M --> N[Real-time Sensor Readings] + M --> O[Historical Charts] + M --> P[Alert Thresholds] + + D --> Q[Emergency References] + Q --> R[Water Shutoff Location] + Q --> S[Gas Shutoff Location] + Q --> T[Electrical Breaker Panel] + Q --> U[Fire Extinguisher Locations] +``` + +### Maintenance & Warranties + +```mermaid +flowchart TD + A[/project/id/maintenance] --> B[Maintenance Schedules] + B --> C[Add Maintenance Item] + C --> D[Set Category & Frequency] + D --> E[Set Next Due Date] + E --> F[Assign Provider] + F --> G[Set Estimated Cost] + + G --> H[Schedule Active] + H --> I{Due Date Reached} + I --> J[Notification Sent] + J --> K[Log Completion] + K --> L[Record Cost & Notes] + L --> M[Upload Photos] + M --> N[Auto-Calculate Next Due Date] + + subgraph "Warranties" + O[/project/id/warranties] --> P[Add Warranty] + P --> Q[Item + Brand + Serial Number] + Q --> R[Start & End Date] + R --> S[Warranty Type] + + S --> T{Claim Needed?} + T -->|Yes| U[File Warranty Claim] + U --> V[Describe Issue + Photos] + V --> W[Claim Status: filed] + W --> X[Claim Status: in_review] + X --> Y{Decision} + Y -->|Approved| Z[Claim Resolved] + Y -->|Denied| AA[Claim Denied] + end +``` + +--- + +## 17. Marketplace & Community + +### Product Catalogue + +```mermaid +flowchart TD + A[/marketplace/catalogue] --> B{Search Methods} + B --> C[Text Search] + B --> D[Category Browse] + B --> E[Visual Similarity Search] + + C --> F[Meilisearch Full-Text] + D --> G[Hierarchical Category Tree] + E --> H[Upload Image - CLIP/pgvector] + + F --> I[Product Results] + G --> I + H --> I + + I --> J[Product Detail Page] + J --> K[Multi-Vendor Price Comparison] + J --> L[Specifications & Dimensions] + J --> M[Similar Products] + J --> N[Add to BOM] +``` + +### Offcuts Exchange + +```mermaid +flowchart TD + A[/marketplace/offcuts] --> B{Role} + + B -->|Seller| C[List Offcut Material] + C --> D[Material Type + Quantity] + D --> E[Dimensions & Condition] + E --> F[Set Price + Upload Photos] + F --> G[Listing Active] + + B -->|Buyer| H[Browse Listings] + H --> I[Filter by Material/Location] + I --> J[View Listing Detail] + J --> K[Send Inquiry] + K --> L[Seller Responds] + L --> M{Deal?} + M -->|Yes| N[Mark as Sold] + M -->|No| O[Decline] +``` + +### Community Gallery + +```mermaid +flowchart TD + A[/marketplace/gallery] --> B[Browse Public Projects] + B --> C[Filter by Style] + B --> D[Filter by Tags] + B --> E[Sort by Likes] + + F[Project Owner] --> G[Publish to Gallery] + G --> H[Add Title & Description] + H --> I[Select Images] + I --> J[Set Style Tag] + J --> K[Entry Published] + + B --> L[View Entry Detail] + L --> M[Like Project] + L --> N[View Design Details] +``` + +--- + +## 18. Admin Panel + +```mermaid +flowchart TD + A[Admin Login - role: admin] --> B[/admin] + + B --> C[Dashboard] + C --> D[Total Users Count] + C --> E[Total Projects Count] + C --> F[Active Jobs Count] + C --> G[System Health Status] + + B --> H[User Management /admin/users] + H --> I[List All Users] + I --> J[Toggle User Enabled/Disabled] + I --> K[Change User Role] + I --> L[View User Projects] + + B --> M[Job Queue /admin/jobs] + M --> N[View All Jobs] + N --> O[Filter by Status] + N --> P[Filter by Type] + N --> Q[Cancel Stuck Jobs] + N --> R[Retry Failed Jobs] + + B --> S[System Health /admin/system] + S --> T[Database Connection Status] + S --> U[Storage Service Status] + S --> V[ML Service Status] + S --> W[Queue Depth Metrics] +``` + +--- + +## 19. Developer API Portal + +```mermaid +flowchart TD + A[/developer] --> B[Register New App] + B --> C[Enter App Name] + C --> D[Set Redirect URIs] + D --> E[Select Scopes] + E --> F[App Created] + F --> G[Receive Client ID + Secret] + + G --> H[Generate Access Token] + H --> I[OAuth Authorization Flow] + I --> J[Access Token Issued] + + J --> K{API Usage} + K --> L[Read Projects] + K --> M[Read BOMs] + K --> N[Read Drawings] + K --> O[Trigger Jobs] + + A --> P[Webhook Subscriptions] + P --> Q[Select Event Type] + Q --> R[Set Target URL] + R --> S[Set Secret for Signing] + S --> T[Webhook Active] + + A --> U[Request Logs] + U --> V[View API Call History] + V --> W[Endpoint + Method + Status] + V --> X[Response Time Metrics] + + subgraph "Rate Limits" + RL1[Standard: 100 req/min] + RL2[Premium: 500 req/min] + RL3[Enterprise: 2000 req/min] + end +``` + +--- + +## Notification Flow (Cross-cutting) + +```mermaid +flowchart TD + A{Event Trigger} --> B[Create Notification Record] + B --> C[WebSocket Push to Client] + C --> D[Bell Icon Badge Updated] + D --> E[User Opens Notification Center] + E --> F[Mark as Read] + E --> G[Click Through to Link] + + A --> H{Event Types} + H --> I[Job Completed] + H --> J[Comment Added] + H --> K[Approval Requested] + H --> L[Payment Received] + H --> M[Delivery Arrived] + H --> N[Change Order Updated] + H --> O[Maintenance Due] + H --> P[Warranty Expiring] +``` + +--- + +## Building Code Compliance Flow + +```mermaid +flowchart TD + A[/project/id/compliance] --> B[Run Compliance Check] + + B --> C{Select Jurisdiction} + C --> D[India - NBC 2016] + C --> E[US - IRC 2021] + C --> F[EU - Eurocode] + C --> G[UK - Building Regs] + + D --> H[AI Checks Design Against Code] + E --> H + F --> H + G --> H + + H --> I[Compliance Report] + I --> J{Result} + J -->|All Pass| K[Compliant Badge] + J -->|Issues Found| L[List Violations] + L --> M[Severity Level per Issue] + M --> N[Remediation Suggestions] + N --> O[Re-check After Fixes] +``` + +--- + +## Data Flow Summary + +```mermaid +flowchart LR + subgraph "User Input" + A1[Photos] + A2[Floor Plans] + A3[Dimensions] + A4[Style Preferences] + A5[Budget] + end + + subgraph "AI Processing" + B1[Vision Engine] + B2[Design Engine] + B3[BOM Engine] + B4[Drawing Generator] + B5[Cut List Engine] + B6[MEP Calculator] + end + + subgraph "Outputs" + C1[Design Renders] + C2[Bill of Materials] + C3[DXF/PDF Drawings] + C4[CNC Cut Lists] + C5[MEP Layouts] + C6[Schedules] + end + + subgraph "Execution" + D1[Procurement] + D2[Contractor Hiring] + D3[Construction] + D4[Quality Checks] + D5[Handover] + end + + A1 --> B1 + A2 --> B1 + A3 --> B2 + A4 --> B2 + A5 --> B2 + B1 --> B2 + B2 --> C1 + B2 --> B3 + B2 --> B4 + B2 --> B5 + B2 --> B6 + B3 --> C2 + B4 --> C3 + B5 --> C4 + B6 --> C5 + C2 --> D1 + C3 --> D2 + C6 --> D3 + D3 --> D4 + D4 --> D5 +```