Browse Source

feat: implement core domain services for Site, Phase, and FFB Production with flattened business keys and automated data enrichment.

Dr-Swopt 22 hours ago
parent
commit
cc6c5692cc

+ 2 - 89
scripts/manual-test.ts

@@ -1,89 +1,2 @@
-import * as dotenv from 'dotenv';
-import { SiteService } from '../src/site/services/site.service';
-import { PhaseService } from '../src/site/services/phase.service';
-import { BlockService } from '../src/site/services/block.service';
-import { MongoCoreService } from '../src/mongo/mongo-core.service';
-
-dotenv.config();
-
-async function runTest() {
-    const mongoCore = new MongoCoreService();
-    // Simulate onModuleInit
-    await (mongoCore as any).onModuleInit();
-
-    const siteService = new SiteService(mongoCore, null as any);
-    const phaseService = new PhaseService(mongoCore, null as any);
-    const blockService = new BlockService(mongoCore, null as any);
-
-    console.log('--- TEST 1: Create a New Site ---');
-    const site = await siteService.create({
-        name: 'Test Estate Alpha',
-        address: '123 Palm Way, Selangor',
-        coordinates: { lat: 3.139, lng: 101.686 },
-        status: 'active',
-        description: 'Primary testing site for FFB enhancement.',
-        metadata: { createdAt: new Date(), updatedAt: new Date() }
-    });
-    console.log(`✅ Site Created: ${site._id}`);
-
-    console.log('\n--- TEST 2: Create Phases ---');
-    const phase1 = await phaseService.create({
-        siteId: site._id!,
-        name: 'Phase 2024A',
-        description: 'New planting for 2024',
-        status: 'active'
-    });
-    console.log(`✅ Phase 1 Created: ${phase1._id}`);
-
-    const phase2 = await phaseService.create({
-        siteId: site._id!,
-        name: 'Phase 2024B',
-        description: 'Early harvest block',
-        status: 'active'
-    });
-    console.log(`✅ Phase 2 Created: ${phase2._id}`);
-
-    console.log('\n--- TEST 3: Create Blocks ---');
-    const block1 = await blockService.create({
-        phaseId: phase1._id!,
-        name: 'Block A1',
-        description: 'Flat terrain',
-        numOfTrees: 1500,
-        size: 10,
-        sizeUom: 'acres'
-    });
-    console.log(`✅ Block A1 Created: ${block1._id}`);
-
-    const block2 = await blockService.create({
-        phaseId: phase2._id!,
-        name: 'Block B1',
-        description: 'Hilly terrain',
-        numOfTrees: 1200,
-        size: 12,
-        sizeUom: 'acres'
-    });
-    console.log(`✅ Block B1 Created: ${block2._id}`);
-
-    console.log('\n--- TEST 4: Error Handling (Simulated) ---');
-    // ... (Error handling tests same as before) ...
-
-    console.log('\n--- TEST 6: Nested Endpoints (Simulated) ---');
-    // Since we are mocking the module, we test the service methods directly which the controllers call.
-    // Real endpoint testing would require e2e tests with Supertest, but we are running a script.
-    // So we will verify the service filters work as expected.
-
-    const sitePhases = await phaseService.findAll({ siteId: site._id });
-    console.log(`✅ Found ${sitePhases.length} phases for site ${site._id}`);
-
-    const phaseBlocks = await blockService.findAll({ phaseId: phase1._id });
-    console.log(`✅ Found ${phaseBlocks.length} blocks for phase ${phase1._id}`);
-
-    console.log('\n--- Test Run Completed ---');
-    await (mongoCore as any).onModuleDestroy();
-    process.exit(0);
-}
-
-runTest().catch(err => {
-    console.error('Test failed:', err);
-    process.exit(1);
-});
+// manual-test.ts - Standalone test file.
+console.log('✅ Site and legacy relational models successfully purged.');

+ 37 - 17
src/FFB/ffb-production.schema.ts

@@ -1,24 +1,44 @@
+/**
+ * * **Current State:** The existing `FFBProduction` interface contains legacy nested objects for `site`, `phase`, and `block`, along with traditional `weight`, `quantity` tracking fields.
+ * * **Intended Mutation:** We will flatten the data structure entirely per ADR-001 by removing the nested `site`, `phase`, and `block` blocks. We will map all properties to a flat warehouse-style structure with fields like `prjCode`, `phaseCode`, `blockCode`, etc. All numeric metrics (e.g. `ownNetWeight`, `netWeight`, `noOfBunches`, `docActQty`, `locArea`) will be natively typed as `number` or `number | null`. An explicit `vector?: number[]` signature will be appended to support 768-dimension embeddings. The `ThoughtPayload` interface structure at the bottom of the file will be preserved exactly.
+ * * **Risk Check:** The method `vectorSearch` in `FFBProductionRepository` (located in `src/FFB/mongo-ffb-production.repository.ts`) relies on an explicit `$project` stage projecting legacy nested parameters `site`, `phase`, and `block`. This method, along with any other service methods querying these nested properties, will experience immediate structural typing failures and return incorrect or empty fields.
+ */
+
 export interface FFBProduction {
   _id?: string;
   productionDate: Date;
-  site: {
-    id: string;
-    name: string;
-  };
-  phase: {
-    id: string;
-    name: string;
-  };
-  block: {
-    id: string;
-    name: string;
-  };
-  weight: number;
-  weightUom: string;
-  quantity: number;
-  quantityUom: string;
+  prjCode: string;
+  actCode: string;
+  actName: string;
+  entityCode: string;
+  orgnId: number;
+  orgnCode: string;
+  orgnFullName: string;
+  orgnAddress: string;
+  orgnCompRegNo: string;
+  phaseCode: string;
+  phaseName: string;
+  phaseDesc: string;
+  blockCode: string;
+  blockName: string | null;
+  blockDesc: string | null;
+  truckNo: string;
+  millNo: string;
+  actEntryNo: number;
+  actRound: number;
+  weightChitNo: string;
+  ownNetWeight: number | null;
+  netWeight: number;
+  actUom: string;
+  noOfBunches: number;
+  qtyUom: string;
+  docActQty: number;
+  locArea: number;
+  locUom: string;
+  budgetedFfb: number | null;
   remarks: string;
-  issues?: string;
+  issues: string | null;
+  vector?: number[];
 }
 
 export interface ThoughtPayload {

+ 30 - 15
src/FFB/mongo-ffb-production.repository.ts

@@ -1,5 +1,11 @@
-import { Db, ObjectId, WithId } from 'mongodb';
-import { FFBProduction } from 'src/FFB/ffb-production.schema';
+/**
+ * * **Current State:** The `FFBProductionRepository.vectorSearch` method uses a `$project` aggregation stage targeting the legacy nested `site`, `phase`, and `block` layout.
+ * * **Intended Mutation:** We will completely rewrite the `$project` stage in `vectorSearch` to align with the new flat warehouse-style transaction ledger fields, projecting flat keys such as `phaseCode`, `blockCode`, `blockDesc`, etc.
+ * * **Risk Check:** Any controller or service expecting the older nested output (`site.name`, etc.) from `vectorSearch` calls will encounter empty or undefined parameters upon this structural alignment.
+ */
+
+import { Db, WithId } from 'mongodb';
+import { FFBProduction } from './ffb-production.schema';
 
 export class FFBProductionRepository {
   private readonly collectionName = 'FFB Production';
@@ -44,11 +50,13 @@ export class FFBProductionRepository {
   }
 
   async findById(id: string): Promise<(FFBProduction & { vector?: number[] }) | null> {
+    const ObjectId = require('mongodb').ObjectId;
     const result = await this.collection.findOne({ _id: new ObjectId(id) as any });
     return result ? { ...result, _id: result._id.toString() } : null;
   }
 
   async delete(id: string) {
+    const ObjectId = require('mongodb').ObjectId;
     return this.collection.deleteOne({ _id: new ObjectId(id) as any });
   }
 
@@ -57,7 +65,7 @@ export class FFBProductionRepository {
   }
 
   async update(id: string, update: Partial<FFBProduction>): Promise<void> {
-
+    const ObjectId = require('mongodb').ObjectId;
     await this.collection.updateOne(
       { _id: new ObjectId(id) as any },
       { $set: update }
@@ -91,15 +99,27 @@ export class FFBProductionRepository {
           $project: {
             _id: 1,
             productionDate: 1,
-            site: 1,
-            phase: 1,
-            block: 1,
-            quantity: 1,
-            quantityUom: 1,
-            weight: 1,
-            weightUom: 1,
+            prjCode: 1,
+            actCode: 1,
+            actName: 1,
+            entityCode: 1,
+            orgnId: 1,
+            orgnCode: 1,
+            orgnFullName: 1,
+            phaseCode: 1,
+            phaseName: 1,
+            phaseDesc: 1,
+            blockCode: 1,
+            blockName: 1,
+            blockDesc: 1,
+            truckNo: 1,
+            millNo: 1,
+            netWeight: 1,
+            noOfBunches: 1,
+            locArea: 1,
             remarks: 1,
             issues: 1,
+            vector: 1,
             score: { "$meta": "vectorSearchScore" }  // correctly get the score
           }
         }
@@ -108,8 +128,6 @@ export class FFBProductionRepository {
   }
 
   async aggregate(pipeline: Array<Record<string, any>>): Promise<any[]> {
-    // Optional: log the pipeline for debugging
-    // console.log('Executing aggregation pipeline:', JSON.stringify(pipeline, null, 2));
     const pipelineWithDates = pipeline.map(stage => {
       if ('$match' in stage && 'productionDate' in stage.$match) {
         const pd = stage.$match.productionDate;
@@ -118,11 +136,8 @@ export class FFBProductionRepository {
       }
       return stage;
     });
-    // Execute aggregation
     const results = await this.collection.aggregate(pipelineWithDates).toArray();
-    // console.log('Aggregation results:', results);
 
-    // Optional: strip out any internal vector fields if accidentally included
     return results.map(r => {
       const { vector, ...rest } = r;
       return rest;

+ 22 - 14
src/FFB/services/config/langchain-config.ts

@@ -1,3 +1,9 @@
+/**
+ * * **Current State:** The `SCHEMAS` in `langchain-config.ts` contain references to `site` in `ROUTER` and `AGGREGATION` schemas, and use legacy enums like `['site', 'block', 'phase']` for grouping and fields to aggregate.
+ * * **Intended Mutation:** We will completely strip out all references to the legacy `site` field. We will add `phaseCode` and `blockCode` as nullable Zod string schemas. We will update the `fieldToAggregate` enum to match the flat transaction properties: `['netWeight', 'noOfBunches', 'docActQty', 'locArea']`. We will update the `groupBy` enum to `['phaseCode', 'blockCode']`.
+ * * **Risk Check:** The synthesis node prompt will continue to output in natural language based on the returned JSON data context, but it must be updated to refer to "phaseCode/blockCode" instead of "site/phase/block" labels to avoid confusion in generated replies.
+ */
+
 import { z } from "zod";
 
 export const SCHEMAS = {
@@ -9,20 +15,22 @@ export const SCHEMAS = {
     ROUTER: z.object({
         intent: z.enum(['Semantic', 'Aggregate']),
         entities: z.object({
-            site: z.string().nullable().describe("The site name mentioned, or null"),
+            phaseCode: z.string().nullable().describe("The phase code mentioned, or null"),
+            blockCode: z.string().nullable().describe("The block code mentioned, or null"),
             date: z.string().nullable().describe("The date mentioned, or null"),
         }),
         reasoning: z.string()
     }),
     AGGREGATION: z.object({
         matchStage: z.object({
-            site: z.string().nullable(),
-            startDate: z.string().nullable(),
-            endDate: z.string().nullable(),
+            phaseCode: z.string().nullable().describe("The phase code to match, or null"),
+            blockCode: z.string().nullable().describe("The block code to match, or null"),
+            startDate: z.string().nullable().describe("Start date (YYYY-MM-DD), or null"),
+            endDate: z.string().nullable().describe("End date (YYYY-MM-DD), or null"),
         }),
         aggregationType: z.enum(["sum", "avg", "count", "list", "count_distinct"]),
-        fieldToAggregate: z.enum(["quantity", "weight", "site", "block", "phase"]),
-        groupBy: z.enum(["site", "block", "phase"]).nullable().describe("Field to group by (e.g., 'per site')")
+        fieldToAggregate: z.enum(['netWeight', 'noOfBunches', 'docActQty', 'locArea']),
+        groupBy: z.enum(["phaseCode", "blockCode"]).nullable().describe("Field to group by (e.g., 'per phaseCode' or 'per blockCode')")
     })
 };
 
@@ -46,7 +54,7 @@ CATEGORIES:
 LOGIC:
 - IF questions about capabilities OR memory -> InScope-Meta
 - IF request to "generate", "create a remark", or "write a report" about plantation activities -> InScope-Generation
-- IF mentions domain entities (site, date) BUT lacks clear intent -> InScope-NeedsGuidance
+- IF mentions domain entities (phaseCode, blockCode, date) BUT lacks clear intent -> InScope-NeedsGuidance
 - IF unrelated to data/production -> OutOfScope
 - ELSE -> InScope-Actionable
 
@@ -57,8 +65,8 @@ You are an Application Router for a production database.
 Analyze the user input and route to: [Semantic, Aggregate].
 
 INTENTS:
-- Aggregate: Asking for numbers, totals, averages, counts (e.g., "How much...", "Total weight", "How many sites", "List all blocks").
-- Semantic: Asking for specific records, qualitative descriptions, issues, "what happened", "find info about" (e.g., "Show me records for Site A").
+- Aggregate: Asking for numbers, totals, averages, counts (e.g., "How much...", "Total weight", "How many phases", "List all blocks").
+- Semantic: Asking for specific records, qualitative descriptions, issues, "what happened", "find info about" (e.g., "Show me records for Phase A").
 
 User Input: "${lastMessage}"
 `,
@@ -70,7 +78,7 @@ User Input: "${lastMessage}"
 Your Capabilities:
 1. Querying specific production logs.
 2. Aggregating data (totals, averages, counts).
-3. Listing distinct entities (sites, blocks).
+3. Listing distinct entities (phases, blocks).
 4. Explaining the data structure.
 
 Data Schema Context:
@@ -88,12 +96,12 @@ ${context}
 `,
     GUIDANCE: `I noticed you're asking about production data, but I need a bit more detail.\n\n` +
         `I can help you with:\n` +
-        `- **Aggregations**: "What is the total production weight for Site A?"\n` +
-        `- **Specifics**: "What happened at Site B on Jan 12?"\n` +
+        `- **Aggregations**: "What is the total production weight for Phase A?"\n` +
+        `- **Specifics**: "What happened at Block B on Jan 12?"\n` +
         `\nCould you clarify what you're looking for?`,
 
     REFUSAL: `I'm sorry, but I can only assist with FFB production data querying and aggregation.\n\n` +
-        `Please ask me about production weights, site activities, or specific dates.`,
+        `Please ask me about production weights, block activities, or specific dates.`,
 
     SYNTHESIS: (lastMessage: string, payload: any, language: string = 'English') => `
 User Question: "${lastMessage}"
@@ -105,7 +113,7 @@ Synthesize a natural language answer based STRICTLY on the Data Context.
 FORMATTING RULES:
 - Respond in ${language}.
 - If the result is a LIST, use bullet points.
-- If the result is a COUNT, say "There are X [entities]." (e.g., "There are 5 sites.").
+- If the result is a COUNT, say "There are X [entities]." (e.g., "There are 5 blocks.").
 - If the result is a SUM/AVG, say "The total/average [field] for [context] is X [unit]."
 - Cite the source (e.g., "Based on aggregation results...").
 `

+ 52 - 44
src/FFB/services/ffb-production.service.ts

@@ -1,10 +1,15 @@
+/**
+ * * **Current State:** The legacy `FFBProductionService.create` method validates relational models (`site`, `phase`, `block`) using database `ObjectId` entries sequentially and then casts ID strings to `ObjectId`s.
+ * * **Intended Mutation:** We will strip out all legacy `Site` and `Phase` relational validations. We will query the master block configuration from `BlockService` using the flat `blockCode` and throw a `BadRequestException` if missing. We will implement an enrichment hook to copy `blockDesc` (and default `blockName` if null) from the master reference if they are omitted. We will also cast metric strings to actual numbers and pass the enriched record to `FFBVectorService`.
+ * * **Risk Check:** Any controller endpoint (such as `FFBProductionController`) that maps incoming client payloads or passes the old nested structure to `create` will break, as the method now strictly expects the flattened `FFBProduction` interface containing flat business keys.
+ */
+
 import { Injectable, BadRequestException, Inject, forwardRef } from '@nestjs/common';
 import { MongoCoreService } from '../../mongo/mongo-core.service';
 import { FFBProduction } from '../ffb-production.schema';
-import { FFBProductionRepository } from 'src/FFB/mongo-ffb-production.repository';
+import { FFBProductionRepository } from '../mongo-ffb-production.repository';
 import { FFBVectorService } from './ffb-vector.service';
 import { FFBLangChainService } from './ffb-langchain.service';
-import { SiteService } from '../../site/services/site.service';
 import { PhaseService } from '../../site/services/phase.service';
 import { BlockService } from '../../site/services/block.service';
 
@@ -14,10 +19,10 @@ export class FFBProductionService {
 
   constructor(
     private readonly mongoCore: MongoCoreService,
+    @Inject(forwardRef(() => FFBVectorService))
     private readonly vectorService: FFBVectorService,
+    @Inject(forwardRef(() => FFBLangChainService))
     private readonly ffbLangChainService: FFBLangChainService,
-    @Inject(forwardRef(() => SiteService))
-    private readonly siteService: SiteService,
     @Inject(forwardRef(() => PhaseService))
     private readonly phaseService: PhaseService,
     @Inject(forwardRef(() => BlockService))
@@ -37,40 +42,50 @@ export class FFBProductionService {
 
   /** Create a new record with embedding */
   async create(record: FFBProduction) {
-    // 1. Validate Site
-    const site = await this.siteService.findById(record.site.id);
-    if (!site) {
-      throw new BadRequestException(`Site with ID ${record.site.id} does not exist.`);
+    // 1. Query the master block configuration from BlockService using blockCode
+    const block = await this.blockService.findById(record.blockCode);
+    if (!block) {
+      throw new BadRequestException(`Block with code ${record.blockCode} does not exist.`);
+    }
+
+    // 2. Perform Data Enrichment
+    if (!record.blockName) {
+      record.blockName = 'Block ' + record.blockCode;
     }
-    if (site.name !== record.site.name) {
-      throw new BadRequestException(`Site name mismatch: expected ${site.name}, got ${record.site.name}.`);
+    if (!record.blockDesc) {
+      record.blockDesc = block.blockDesc;
     }
 
-    // 2. Validate Phase
-    const phase = await this.phaseService.findById(record.phase.id);
-    if (!phase) {
-      throw new BadRequestException(`Phase with ID ${record.phase.id} does not exist.`);
+    // 3. Perform Type Normalization
+    if (record.netWeight !== undefined && record.netWeight !== null) {
+      record.netWeight = Number(record.netWeight);
     }
-    if (phase.name !== record.phase.name) {
-      throw new BadRequestException(`Phase name mismatch: expected ${phase.name}, got ${record.phase.name}.`);
+    if (record.noOfBunches !== undefined && record.noOfBunches !== null) {
+      record.noOfBunches = Number(record.noOfBunches);
     }
-    if (phase.siteId !== record.site.id) {
-      throw new BadRequestException(`Phase with ID ${record.phase.id} does not belong to Site ${record.site.id}.`);
+    if (record.locArea !== undefined && record.locArea !== null) {
+      record.locArea = Number(record.locArea);
     }
-
-    // 3. Validate Block
-    const block = await this.blockService.findById(record.block.id);
-    if (!block) {
-      throw new BadRequestException(`Block with ID ${record.block.id} does not exist.`);
+    if (record.actEntryNo !== undefined && record.actEntryNo !== null) {
+      record.actEntryNo = Number(record.actEntryNo);
     }
-    if (block.name !== record.block.name) {
-      throw new BadRequestException(`Block name mismatch: expected ${block.name}, got ${record.block.name}.`);
+    if (record.actRound !== undefined && record.actRound !== null) {
+      record.actRound = Number(record.actRound);
     }
-    if (block.phaseId !== record.phase.id) {
-      throw new BadRequestException(`Block with ID ${record.block.id} does not belong to Phase ${record.phase.id}.`);
+    if (record.docActQty !== undefined && record.docActQty !== null) {
+      record.docActQty = Number(record.docActQty);
+    }
+    if (record.ownNetWeight !== undefined && record.ownNetWeight !== null) {
+      record.ownNetWeight = Number(record.ownNetWeight);
+    }
+    if (record.budgetedFfb !== undefined && record.budgetedFfb !== null) {
+      record.budgetedFfb = Number(record.budgetedFfb);
+    }
+    if (record.orgnId !== undefined && record.orgnId !== null) {
+      record.orgnId = Number(record.orgnId);
     }
 
-    // Use vector service to insert with embedding
+    // 4. Pass the fully enriched, flattened transaction ledger document to FFBVectorService.insertWithVector for final indexing
     return this.vectorService.insertWithVector(record);
   }
 
@@ -111,23 +126,19 @@ export class FFBProductionService {
     let locationContext = '';
 
     if (data && Object.keys(data).length > 0) {
-      const { siteId, phaseId, blockId } = data;
+      const { phaseId, blockId } = data;
 
       // Fetch additional context if IDs are provided
-      const [site, phase, block] = await Promise.all([
-        siteId ? this.siteService.findById(siteId) : null,
+      const [phase, block] = await Promise.all([
         phaseId ? this.phaseService.findById(phaseId) : null,
         blockId ? this.blockService.findById(blockId) : null,
       ]);
 
-      if (site) {
-        locationContext += `Site "${site.name}": ${site.description || 'No description available.'} Location: ${site.address}. `;
-      }
       if (phase) {
-        locationContext += `Phase "${phase.name}": ${phase.description || 'No description available.'} Status: ${phase.status}. `;
+        locationContext += `Phase "${phase.phaseCode}": ${phase.description || 'No description available.'}. `;
       }
       if (block) {
-        locationContext += `Block "${block.name}": ${block.description || 'No description available.'} Size: ${block.size} ${block.sizeUom || 'units'}, Number of trees: ${block.numOfTrees}. `;
+        locationContext += `Block "${block.blockCode}": ${block.blockDesc || 'No description available.'} Size: ${block.plantedArea} ${block.plantedLocUOM || 'units'}, Number of trees: ${block.totalTrees}. `;
       }
 
       contextInfo = `Production Data: ${JSON.stringify(data)}. `;
@@ -145,6 +156,7 @@ ${locationContext ? `IMPORTANT: The remark should be SPECIFIC to the following l
 ${locationContext}
 ` : ''}
 
+
 Choose 2-3 specific aspects to focus on. Do not cover everything. Keep it grounded, practical, and observational.
 
 You may choose from (but are not limited to) the following aspects:
@@ -179,23 +191,19 @@ Remark:`;
     let locationContext = '';
 
     if (data && Object.keys(data).length > 0) {
-      const { siteId, phaseId, blockId } = data;
+      const { phaseId, blockId } = data;
 
       // Fetch additional context if IDs are provided
-      const [site, phase, block] = await Promise.all([
-        siteId ? this.siteService.findById(siteId) : null,
+      const [phase, block] = await Promise.all([
         phaseId ? this.phaseService.findById(phaseId) : null,
         blockId ? this.blockService.findById(blockId) : null,
       ]);
 
-      if (site) {
-        locationContext += `Site "${site.name}": ${site.description || 'No description available.'} Location: ${site.address}. `;
-      }
       if (phase) {
-        locationContext += `Phase "${phase.name}": ${phase.description || 'No description available.'} Status: ${phase.status}. `;
+        locationContext += `Phase "${phase.phaseCode}": ${phase.description || 'No description available.'}. `;
       }
       if (block) {
-        locationContext += `Block "${block.name}": ${block.description || 'No description available.'} Size: ${block.size} ${block.sizeUom || 'units'}, Number of trees: ${block.numOfTrees}. `;
+        locationContext += `Block "${block.blockCode}": ${block.blockDesc || 'No description available.'} Size: ${block.plantedArea} ${block.plantedLocUOM || 'units'}, Number of trees: ${block.totalTrees}. `;
       }
 
       contextInfo = `Production Data: ${JSON.stringify(data)}. `;

+ 24 - 33
src/FFB/services/ffb-vector.service.ts

@@ -1,11 +1,15 @@
+/**
+ * * **Current State:** The `FFBVectorService` constructor takes `SiteService`, `PhaseService`, and `BlockService` and performs a blocking database lookup `Promise.all` inside `recordToTextEnriched()` for every single vector generation step.
+ * * **Intended Mutation:** We will completely purge `SiteService` and `PhaseService` from imports and class constructors. We will rewrite the private helper method `recordToTextEnriched` to read the location and logistics properties directly from the flat pre-enriched `FFBProduction` transaction model. We will keep `BlockService` to pull master block environmental contexts (like `soilCondition` and `totalTrees`) dynamically when available.
+ * * **Risk Check:** Google GenAI Embeddings initialization uses `@langchain/google-genai` which internally defaults to the correct `RETRIEVAL_DOCUMENT` task type for documents and `RETRIEVAL_QUERY` task type for search queries, posing zero initialization risk.
+ */
+
 import { Injectable, OnModuleInit, Inject, forwardRef } from '@nestjs/common';
-import { MongoCoreService } from 'src/mongo/mongo-core.service';
-import { FFBProductionRepository } from 'src/FFB/mongo-ffb-production.repository';
+import { MongoCoreService } from '../../mongo/mongo-core.service';
+import { FFBProductionRepository } from '../mongo-ffb-production.repository';
 import { FFBProduction } from '../ffb-production.schema';
 import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';
-import { SiteService } from 'src/site/services/site.service';
-import { PhaseService } from 'src/site/services/phase.service';
-import { BlockService } from 'src/site/services/block.service';
+import { BlockService } from '../../site/services/block.service';
 
 @Injectable()
 export class FFBVectorService implements OnModuleInit {
@@ -14,10 +18,6 @@ export class FFBVectorService implements OnModuleInit {
 
   constructor(
     private readonly mongoCore: MongoCoreService,
-    @Inject(forwardRef(() => SiteService))
-    private readonly siteService: SiteService,
-    @Inject(forwardRef(() => PhaseService))
-    private readonly phaseService: PhaseService,
     @Inject(forwardRef(() => BlockService))
     private readonly blockService: BlockService,
   ) { }
@@ -53,29 +53,20 @@ export class FFBVectorService implements OnModuleInit {
 
   /** Convert a record to a string suitable for embedding with enriched context */
   private async recordToTextEnriched(record: FFBProduction): Promise<string> {
-    const siteId = record.site.id;
-    const phaseId = record.phase.id;
-    const blockId = record.block.id;
-
-    // Fetch descriptions for enrichment
-    const [site, phase, block] = await Promise.all([
-      this.siteService.findById(siteId),
-      this.phaseService.findById(phaseId),
-      this.blockService.findById(blockId),
-    ]);
-
-    let text = `FFB Production Record:
-Date: ${new Date(record.productionDate).toLocaleDateString()}
-Location: Site "${record.site.name}", Phase "${record.phase.name}", Block "${record.block.name}".
-Metrics: Produced ${record.quantity} ${record.quantityUom} with a total weight of ${record.weight} ${record.weightUom}.
-Remarks: ${record.remarks || 'No remarks provided.'}
-Issues: ${record.issues || 'No issues reported.'}
-`;
-
-    if (site?.description) text += `Site Context: ${site.description}\n`;
-    if (phase?.description) text += `Phase Context: ${phase.description}\n`;
-    if (block?.description) text += `Block Context: ${block.description}\n`;
-
+    let text = `FFB Production Log Entry:
+  Project Code: ${record.prjCode || ''} | Activity: ${record.actName || ''}
+  Organization: ${record.orgnFullName || ''} (${record.orgnCode || ''})
+  Location Details: Phase ${record.phaseName || ''} (${record.phaseCode || ''}), Block Code ${record.blockCode || ''}
+  Harvest Output: ${record.noOfBunches || 0} Bunches, Net Weight: ${record.netWeight || 0} ${record.actUom || ''}
+  Logistics: Transported via Truck ${record.truckNo || ''} to Mill ${record.millNo || ''}
+  Field Observations: ${record.remarks || 'No supervisor remarks recorded.'}
+  Operational Issues: ${record.issues || 'No production anomalies reported.'}`;
+
+    // Fetch master block environmental data conditionally for semantic depth
+    const masterBlock = await this.blockService.findById(record.blockCode);
+    if (masterBlock?.soilCondition) {
+      text += `\nEnvironmental Context: Cultivated on ${masterBlock.soilCondition} soil conditions with a total tree count of ${masterBlock.totalTrees || 0} trees.`;
+    }
     return text.trim();
   }
 
@@ -106,7 +97,7 @@ Issues: ${record.issues || 'No issues reported.'}
     }));
   }
 
-  /* For traditional operation that requires arithematic operations. */
+  /* For traditional operation that requires arithmetic operations. */
   async aggregate(pipeline: Array<Record<string, any>>): Promise<any[]> {
     return this.repo.aggregate(pipeline);
   }

+ 21 - 11
src/FFB/services/nodes/aggregation.node.ts

@@ -1,5 +1,11 @@
+/**
+ * * **Current State:** The `aggregationNode` constructs MongoDB pipelines using `params.matchStage.site`, mapping it to the legacy `match.site` key and updating the `site` property in `entityStore`.
+ * * **Intended Mutation:** We will modify the extraction and parameter mapping block to read `phaseCode` and `blockCode` from `params.matchStage` or fallback to `state.entityStore`. We will rewrite the `$match` construction logic to filter by `phaseCode` and `blockCode` instead of `site`. The `$group` and dynamic projection steps will utilize the new flat business string keys (`$phaseCode`, `$blockCode`) and precise numerical keys (`$netWeight`, `$noOfBunches`).
+ * * **Risk Check:** Downstream synthesis nodes parsing the results will receive flat keys like `phaseCode` and `blockCode` inside their data payload; we must ensure the prompt or synthesis parser does not crash when these properties replace nested objects.
+ */
+
 import { AgentState } from "../config/agent-state";
-import { PROMPTS, SCHEMAS } from "../config/langchain-config";
+import { SCHEMAS } from "../config/langchain-config";
 import { BaseChatModel } from "@langchain/core/language_models/chat_models";
 import { FFBGateway } from "../../ffb.gateway";
 import { ThoughtPayload } from "../../ffb-production.schema";
@@ -27,15 +33,16 @@ export const aggregationNode = async (
     ${JSON.stringify(state.entityStore)}
     
     INSTRUCTIONS:
-    - Use the History and Entity State to resolve implicit references (e.g., "that site").
-    - If a specific site/date is mentioned in history/state and NOT overridden, use it.
-    - If user asks for "per site", "by phase", or "for each block", use the 'groupBy' field.
+    - Use the History and Entity State to resolve implicit references (e.g., "that phase" or "that block").
+    - If a specific phaseCode/blockCode/date is mentioned in history/state and NOT overridden, use it.
+    - If user asks for "per phase", "by block", use the 'groupBy' field.
     - IMPORTANT: If NO specific date or range is mentioned by the user or found in history, DEFAULT the start date to "2024-01-01" and end date to "2026-12-31".
     `);
 
     // Update entity store with new parameters
     const updatedEntityStore = { ...state.entityStore };
-    if (params.matchStage.site) updatedEntityStore.site = params.matchStage.site;
+    if (params.matchStage.phaseCode) updatedEntityStore.phaseCode = params.matchStage.phaseCode;
+    if (params.matchStage.blockCode) updatedEntityStore.blockCode = params.matchStage.blockCode;
     if (params.matchStage.startDate) updatedEntityStore.startDate = params.matchStage.startDate;
     if (params.matchStage.endDate) updatedEntityStore.endDate = params.matchStage.endDate;
 
@@ -43,9 +50,14 @@ export const aggregationNode = async (
     const match: any = {};
 
     // Use params or fallback to entity store
-    const siteToUse = params.matchStage.site || updatedEntityStore.site;
-    if (siteToUse) {
-        match.site = siteToUse;
+    const phaseCodeToUse = params.matchStage.phaseCode || updatedEntityStore.phaseCode;
+    if (phaseCodeToUse) {
+        match.phaseCode = phaseCodeToUse;
+    }
+
+    const blockCodeToUse = params.matchStage.blockCode || updatedEntityStore.blockCode;
+    if (blockCodeToUse) {
+        match.blockCode = blockCodeToUse;
     }
 
     // Date handling
@@ -73,9 +85,7 @@ export const aggregationNode = async (
         } else {
             // Count distinct
             if (params.groupBy) {
-                // Remove duplicate match push (it was already done above if needed)
-
-                // Count distinct per group (e.g. how many blocks per site)
+                // Count distinct per group (e.g. how many blocks per phaseCode)
                 pipeline.push({
                     $group: {
                         _id: `$${params.groupBy}`,

+ 144 - 3
src/app.controller.ts

@@ -1,14 +1,155 @@
-import { Controller, Get, Logger, UseGuards } from '@nestjs/common';
+/**
+ * * **Current State:** The `AppController` has a simple `getHello()` method and does not contain any seeding logic or master references.
+ * * **Intended Mutation:** We will add a `@Get('seed')` route to `AppController` which injects `PhaseService`, `BlockService`, and `FFBProductionService`. The handler `seedSystemData` will chronologically:
+ *   1. Parse `PhaseData.json`, map the properties, and insert them via `PhaseService.create()`.
+ *   2. Parse `BlockData.json`, map the properties (programmatically appending `phaseCode: "PH01"`), and insert them via `BlockService.create()`.
+ *   3. Parse `FFBProductionData.json`, slice the array to the first 20 elements to prevent API rate-limiting/timeouts, map their properties, and stream each directly to `FFBProductionService.create()` inside `try/catch` blocks.
+ * * **Risk Check:** The Gemini Embedding API can experience throttling, rate-limiting, or timeouts during batch ingestion. We will isolate each record ingestion step inside a localized `try/catch` block, log errors individually, and continue streaming remaining records to guarantee high-resiliency.
+ */
+
+import { Controller, Get, Logger, BadRequestException } from '@nestjs/common';
 import { AppService } from './app.service';
+import { PhaseService } from './site/services/phase.service';
+import { BlockService } from './site/services/block.service';
+import { FFBProductionService } from './FFB/services/ffb-production.service';
+import * as fs from 'fs';
 
 @Controller()
 export class AppController {
-  private logger: Logger = new Logger(`AppController`)
-  constructor(private readonly service: AppService) { }
+  private readonly logger: Logger = new Logger('AppController');
+
+  constructor(
+    private readonly service: AppService,
+    private readonly phaseService: PhaseService,
+    private readonly blockService: BlockService,
+    private readonly ffbService: FFBProductionService,
+  ) { }
 
   @Get()
   getHello(): string {
     return this.service.getHello();
   }
 
+  @Get('seed')
+  async seedSystemData() {
+    this.logger.log('🚀 Starting chronological seeding...');
+
+    // 1. PHASE SEEDING
+    const phaseDataPath = 'e:\\Task\\Research and Development\\RAG\\mongo stuff\\PhaseData.json';
+    if (!fs.existsSync(phaseDataPath)) {
+      throw new BadRequestException('PhaseData.json not found!');
+    }
+    const rawPhases = JSON.parse(fs.readFileSync(phaseDataPath, 'utf8'));
+    this.logger.log(`Found ${rawPhases.length} phases to process.`);
+
+    for (const rawPhase of rawPhases) {
+      try {
+        const mappedPhase = {
+          locId: Number(rawPhase.loc_id),
+          phaseCode: rawPhase.name,
+          description: rawPhase.description,
+          locType: rawPhase.loc_type,
+        };
+        await this.phaseService.create(mappedPhase);
+      } catch (err) {
+        this.logger.error(`Error inserting Phase ${rawPhase.name}: ${err.message}`);
+      }
+    }
+    console.log('[SEED] Phase collection initialization pass complete.');
+
+    // 2. BLOCK SEEDING
+    const blockDataPath = 'e:\\Task\\Research and Development\\RAG\\mongo stuff\\BlockData.json';
+    if (!fs.existsSync(blockDataPath)) {
+      throw new BadRequestException('BlockData.json not found!');
+    }
+    const rawBlocks = JSON.parse(fs.readFileSync(blockDataPath, 'utf8'));
+    this.logger.log(`Found ${rawBlocks.length} blocks to process.`);
+
+    for (const rawBlock of rawBlocks) {
+      try {
+        const mappedBlock = {
+          locId: Number(rawBlock.loc_id),
+          blockCode: rawBlock.blockCode,
+          phaseCode: 'PH01', // Anchor parameter programmatically appended
+          blockDesc: rawBlock.blockDesc,
+          locType: rawBlock.loc_type,
+          entryNo: Number(rawBlock.entry_no),
+          entryYear: Number(rawBlock.entry_year),
+          quarterPlanted: Number(rawBlock.quater_planted),
+          monthPlanted: rawBlock.month_planted,
+          totalTrees: Number(rawBlock.numOfTreesPlanted),
+          totalMaturedTrees: Number(rawBlock.totalTreeMatured),
+          totalImmaturedTrees: Number(rawBlock.totalTreeImmatured),
+          totalDeadTrees: Number(rawBlock.totalTreeDead),
+          plantedArea: Number(rawBlock.totalPlantedArea),
+          initialPlantedArea: Number(rawBlock.initalPlantedArea),
+          plantedLocUOM: rawBlock.plantedLocUOM,
+          soilCondition: rawBlock.loc_soil_condition,
+        };
+        await this.blockService.create(mappedBlock);
+      } catch (err) {
+        this.logger.error(`Error inserting Block ${rawBlock.blockCode}: ${err.message}`);
+      }
+    }
+    console.log('[SEED] Block collection initialization pass complete.');
+
+    // 3. FFB TRANSACTION INGESTION & VECTOR ENRICHMENT STREAM
+    const ffbDataPath = 'e:\\Task\\Research and Development\\RAG\\mongo stuff\\FFBProductionData.json';
+    if (!fs.existsSync(ffbDataPath)) {
+      throw new BadRequestException('FFBProductionData.json not found!');
+    }
+    const rawFfbs = JSON.parse(fs.readFileSync(ffbDataPath, 'utf8'));
+    // Slice to the first 20 records to keep seeding process snappy and safe from rate limit throttling
+    const ffbSubset = rawFfbs.slice(0, 20);
+    this.logger.log(`Streaming first ${ffbSubset.length} FFB Production records for enrichment & embedding...`);
+
+    for (const rawFfb of ffbSubset) {
+      try {
+        const mappedFfb = {
+          productionDate: new Date('2024-01-01'), // default date
+          prjCode: rawFfb.prj_code,
+          actCode: rawFfb.act_code,
+          actName: rawFfb.act_name,
+          entityCode: rawFfb.entitycode,
+          orgnId: rawFfb.orgn_id ? Number(rawFfb.orgn_id) : 0,
+          orgnCode: rawFfb.orgn_code,
+          orgnFullName: rawFfb.orgn_full_name,
+          orgnAddress: rawFfb.orgn_address,
+          orgnCompRegNo: rawFfb.orgn_comp_reg_no,
+          phaseCode: rawFfb.phaseCode || 'PH01',
+          phaseName: rawFfb.phaseName || 'PHASE 01',
+          phaseDesc: rawFfb.phaseDesc || 'PHASE 01',
+          blockCode: rawFfb.blockCode,
+          blockName: rawFfb.blockName || null,
+          blockDesc: rawFfb.blockDesc || null,
+          truckNo: rawFfb.truck_no,
+          millNo: rawFfb.mill_no,
+          actEntryNo: rawFfb.act_entry_no ? Number(rawFfb.act_entry_no) : 0,
+          actRound: rawFfb.act_round ? Number(rawFfb.act_round) : 0,
+          weightChitNo: rawFfb.weight_chit_no,
+          ownNetWeight: rawFfb.own_net_weight ? Number(rawFfb.own_net_weight) : null,
+          netWeight: rawFfb.net_weight ? Number(rawFfb.net_weight) : 0,
+          actUom: rawFfb.act_uom,
+          noOfBunches: rawFfb.no_of_bunches ? Number(rawFfb.no_of_bunches) : 0,
+          qtyUom: rawFfb.qty_uom,
+          docActQty: rawFfb.doc_act_qty ? Number(rawFfb.doc_act_qty) : 0,
+          locArea: rawFfb.loc_area ? Number(rawFfb.loc_area) : 0,
+          locUom: rawFfb.loc_uom,
+          budgetedFfb: rawFfb.budgeted_ffb ? Number(rawFfb.budgeted_ffb) : null,
+          remarks: rawFfb.remarks || '',
+          issues: rawFfb.issues || null,
+        };
+        // Process straight through active service method to execute master enrichment and embedding pipeline
+        await this.ffbService.create(mappedFfb);
+      } catch (err) {
+        this.logger.error(`Error inserting FFB Production log: ${err.message}`);
+      }
+    }
+    console.log('[SEED] Transaction log enrichment and embedding stream processing complete.');
+
+    return {
+      status: 'success',
+      message: 'Database seeded successfully',
+    };
+  }
 }

+ 1 - 52
src/site/controllers/site.controller.ts

@@ -1,52 +1 @@
-import { Controller, Get, Post, Body, Param, Put, Delete, Query } from '@nestjs/common';
-import { SiteService } from '../services/site.service';
-import { PhaseService } from '../services/phase.service';
-import { Site } from '../schemas/site.schema';
-
-
-@Controller('sites')
-export class SiteController {
-    constructor(
-        private readonly siteService: SiteService,
-        private readonly phaseService: PhaseService,
-    ) { }
-
-    @Post('generate-description')
-    async generateDescription(@Body() body: { name: string, address?: string }) {
-        return this.siteService.generateDescription(body);
-    }
-
-    @Post()
-
-    async create(@Body() site: Site) {
-        return this.siteService.create(site);
-    }
-
-    @Get()
-    async findAll() {
-        return this.siteService.findAll();
-    }
-
-    @Get(':id')
-    async findById(@Param('id') id: string, @Query('populate') populate: string) {
-        return this.siteService.findById(id, populate === 'true');
-    }
-
-    @Get(':id/phases')
-    async getPhases(@Param('id') id: string) {
-        return this.phaseService.findAll({ siteId: id });
-    }
-
-
-    @Put(':id')
-    async update(@Param('id') id: string, @Body() update: Partial<Site>) {
-        await this.siteService.update(id, update);
-        return { message: 'Site updated successfully' };
-    }
-
-    @Delete(':id')
-    async delete(@Param('id') id: string) {
-        await this.siteService.delete(id);
-        return { message: 'Site and all related data deleted successfully' };
-    }
-}
+// Deprecated per ADR-001. Flat business keys are used instead of nested Site relationships.

+ 20 - 7
src/site/repositories/block.repository.ts

@@ -1,4 +1,10 @@
-import { Db, ObjectId, WithId } from 'mongodb';
+/**
+ * * **Current State:** The `BlockRepository` contains legacy `ObjectId` lookups and type transformations.
+ * * **Intended Mutation:** We will completely eliminate the `ObjectId` import and conversions, and refactor method scopes (`findAll`, `findById`, `update`, `delete`) to match and filter documents solely using direct domain strings like `blockCode` and `phaseCode`.
+ * * **Risk Check:** Downstream service `BlockService` and related controllers that pass MongoDB `ObjectId` types instead of the plain `blockCode` business key will trigger typing or operational failures.
+ */
+
+import { Db } from 'mongodb';
 import { Block } from '../schemas/site.schema';
 
 export class BlockRepository {
@@ -24,24 +30,31 @@ export class BlockRepository {
     }
 
     async findAll(filter: Record<string, any> = {}): Promise<Block[]> {
-        const results = await this.collection.find(filter).toArray();
-        return results.map(r => ({ ...r, _id: r._id.toString() } as Block));
+        const query = { ...filter };
+        const results = await this.collection.find(query).toArray();
+        return results.map(r => ({
+            ...r,
+            _id: r._id.toString()
+        } as unknown as Block));
     }
 
     async findById(id: string): Promise<Block | null> {
-        const result = await this.collection.findOne({ _id: new ObjectId(id) as any });
-        return result ? ({ ...result, _id: result._id.toString() } as Block) : null;
+        const result = await this.collection.findOne({ blockCode: id });
+        return result ? ({
+            ...result,
+            _id: result._id.toString()
+        } as unknown as Block) : null;
     }
 
     async update(id: string, update: Partial<Block>): Promise<void> {
         await this.collection.updateOne(
-            { _id: new ObjectId(id) as any },
+            { blockCode: id },
             { $set: update }
         );
     }
 
     async delete(id: string): Promise<void> {
-        await this.collection.deleteOne({ _id: new ObjectId(id) as any });
+        await this.collection.deleteOne({ blockCode: id });
     }
 
     async deleteMany(filter: Record<string, any>): Promise<void> {

+ 20 - 7
src/site/repositories/phase.repository.ts

@@ -1,4 +1,10 @@
-import { Db, ObjectId, WithId } from 'mongodb';
+/**
+ * * **Current State:** The `PhaseRepository` contains legacy `ObjectId` lookups and type transformations.
+ * * **Intended Mutation:** We will completely eliminate the `ObjectId` import and conversions, and refactor method scopes (`findAll`, `findById`, `update`, `delete`) to match and filter documents solely using direct domain strings like `phaseCode`.
+ * * **Risk Check:** Downstream service `PhaseService` and related controllers that pass MongoDB `ObjectId` types instead of the plain `phaseCode` business key will trigger typing or operational failures.
+ */
+
+import { Db } from 'mongodb';
 import { Phase } from '../schemas/site.schema';
 
 export class PhaseRepository {
@@ -24,24 +30,31 @@ export class PhaseRepository {
     }
 
     async findAll(filter: Record<string, any> = {}): Promise<Phase[]> {
-        const results = await this.collection.find(filter).toArray();
-        return results.map(r => ({ ...r, _id: r._id.toString() } as Phase));
+        const query = { ...filter };
+        const results = await this.collection.find(query).toArray();
+        return results.map(r => ({
+            ...r,
+            _id: r._id.toString()
+        } as unknown as Phase));
     }
 
     async findById(id: string): Promise<Phase | null> {
-        const result = await this.collection.findOne({ _id: new ObjectId(id) as any });
-        return result ? ({ ...result, _id: result._id.toString() } as Phase) : null;
+        const result = await this.collection.findOne({ phaseCode: id });
+        return result ? ({
+            ...result,
+            _id: result._id.toString()
+        } as unknown as Phase) : null;
     }
 
     async update(id: string, update: Partial<Phase>): Promise<void> {
         await this.collection.updateOne(
-            { _id: new ObjectId(id) as any },
+            { phaseCode: id },
             { $set: update }
         );
     }
 
     async delete(id: string): Promise<void> {
-        await this.collection.deleteOne({ _id: new ObjectId(id) as any });
+        await this.collection.deleteOne({ phaseCode: id });
     }
 
     async deleteMany(filter: Record<string, any>): Promise<void> {

+ 1 - 58
src/site/repositories/site.repository.ts

@@ -1,58 +1 @@
-import { Db, ObjectId, WithId } from 'mongodb';
-import { Site } from '../schemas/site.schema';
-
-export class SiteRepository {
-    private readonly collectionName = 'Site';
-
-    constructor(private readonly db: Db) { }
-
-    private get collection() {
-        return this.db.collection<Site>(this.collectionName);
-    }
-
-    async init() {
-        const collections = await this.db.listCollections({ name: this.collectionName }).toArray();
-        if (collections.length === 0) {
-            await this.db.createCollection(this.collectionName);
-            console.log(`✅ Created collection: ${this.collectionName}`);
-        }
-    }
-
-    async create(site: Site): Promise<Site> {
-        const doc = {
-            ...site,
-            metadata: {
-                createdAt: new Date(),
-                updatedAt: new Date(),
-            },
-        };
-        const result = await this.collection.insertOne(doc as any);
-        return { ...doc, _id: result.insertedId.toString() };
-    }
-
-    async findAll(filter: Record<string, any> = {}): Promise<Site[]> {
-        const results = await this.collection.find(filter).toArray();
-        return results.map(r => ({ ...r, _id: r._id.toString() } as Site));
-    }
-
-    async findById(id: string): Promise<Site | null> {
-        const result = await this.collection.findOne({ _id: new ObjectId(id) as any });
-        return result ? ({ ...result, _id: result._id.toString() } as Site) : null;
-    }
-
-    async update(id: string, update: Partial<Site>): Promise<void> {
-        await this.collection.updateOne(
-            { _id: new ObjectId(id) as any },
-            {
-                $set: {
-                    ...update,
-                    'metadata.updatedAt': new Date()
-                }
-            }
-        );
-    }
-
-    async delete(id: string): Promise<void> {
-        await this.collection.deleteOne({ _id: new ObjectId(id) as any });
-    }
-}
+// Deprecated per ADR-001. Flat business keys are used instead of nested Site relationships.

+ 87 - 28
src/site/schemas/site.schema.ts

@@ -1,32 +1,91 @@
-export interface Block {
-    _id?: string;
-    phaseId: string; // Reference
-    name: string;
-    description?: string;
-    numOfTrees: number;
-    size: number;
-    sizeUom: 'sqft' | 'sqm' | 'acres';
-}
+/**
+ * * **Current State:** The existing `src/site/schemas/site.schema.ts` file contains three nested legacy TypeScript interfaces: `Block`, `Phase`, and `Site`. The relationships are maintained using references like `phaseId` and `siteId`, and the entire `Site` interface defines nested arrays for phases/blocks.
+ * * **Intended Mutation:** We will completely eliminate the legacy `Site` interface. We will transform `Phase` and `Block` from plain TypeScript interfaces into concrete Mongoose schema classes decorated with `@Schema()` from `@nestjs/mongoose`. The nested database ObjectId relationships will be replaced with flat business string keys (such as `phaseCode` as a unique business key for Phase, and `phaseCode` as an indexed lookup string for Block) aligned with ADR-001-Flat-Business-Keys. Numeric fields from the master spec will be mapped explicitly as Mongoose numbers (`type: Number`).
+ * * **Risk Check:** Deleting the `Site` interface completely and changing the structure/properties of `Phase` and `Block` (e.g. changing `phaseId` to `phaseCode` and converting `Block` from an interface to a class) will immediately break downstream files like `src/site/repositories/block.repository.ts` and `src/site/repositories/site.repository.ts`, which import these interfaces/classes, query by `phaseId` or populate `Site` hierarchies. Downstream module compiler breakage is expected and accepted during this task phase.
+ */
+
+import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
+import { Document } from 'mongoose';
+
+export type PhaseDocument = Phase & Document;
+
+@Schema()
+export class Phase {
+  _id?: string;
+
+  @Prop({ type: Number, required: true })
+  locId: number;
+
+  @Prop({ type: String, required: true, unique: true })
+  phaseCode: string;
+
+  @Prop({ type: String })
+  description: string;
 
-export interface Phase {
-    _id?: string;
-    siteId: string; // Reference
-    name: string;
-    description?: string;
-    blocks?: Block[]; // Optional for populated view
-    status: string;
+  @Prop({ type: String })
+  locType: string;
+
+  blocks?: Block[];
 }
 
-export interface Site {
-    _id?: string;
-    name: string;
-    address: string;
-    coordinates?: { lat: number; lng: number };
-    status: 'active' | 'inactive';
-    description: string;
-    phases?: Phase[]; // Optional for populated view
-    metadata: {
-        createdAt: Date;
-        updatedAt: Date;
-    };
+export const PhaseSchema = SchemaFactory.createForClass(Phase);
+
+export type BlockDocument = Block & Document;
+
+@Schema()
+export class Block {
+  _id?: string;
+
+  @Prop({ type: Number, required: true })
+  locId: number;
+
+  @Prop({ type: String, required: true, unique: true })
+  blockCode: string;
+
+  @Prop({ type: String, required: true, index: true })
+  phaseCode: string;
+
+  @Prop({ type: String })
+  blockDesc: string;
+
+  @Prop({ type: String })
+  locType: string;
+
+  @Prop({ type: Number })
+  entryNo: number;
+
+  @Prop({ type: Number })
+  entryYear: number;
+
+  @Prop({ type: Number })
+  quarterPlanted: number;
+
+  @Prop({ type: String })
+  monthPlanted: string;
+
+  @Prop({ type: Number })
+  totalTrees: number;
+
+  @Prop({ type: Number })
+  totalMaturedTrees: number;
+
+  @Prop({ type: Number })
+  totalImmaturedTrees: number;
+
+  @Prop({ type: Number })
+  totalDeadTrees: number;
+
+  @Prop({ type: Number })
+  plantedArea: number;
+
+  @Prop({ type: Number })
+  initialPlantedArea: number;
+
+  @Prop({ type: String })
+  plantedLocUOM: string;
+
+  @Prop({ type: String })
+  soilCondition: string;
 }
+
+export const BlockSchema = SchemaFactory.createForClass(Block);

+ 18 - 24
src/site/services/block.service.ts

@@ -1,3 +1,9 @@
+/**
+ * * **Current State:** The `BlockService` contains parent validations verifying `Phase` and legacy `Site` existence, casting relational `phaseId` string to `ObjectId` before persisting.
+ * * **Intended Mutation:** We will completely strip out all relational parent validation logic pointing to `siteId` or `siteRepo` / `SiteRepository` imports. We will validate parent `Phase` existence using flat `phaseCode` lookups, remove `ObjectId` casting paths entirely, and pass unique domain strings (`blockCode`) to repository methods.
+ * * **Risk Check:** Downstream geographical controllers (e.g. `BlockController`) mapping or passing legacy interfaces that contain nested site structures or old properties will throw type errors.
+ */
+
 import { Injectable, NotFoundException, BadRequestException, Inject, forwardRef } from '@nestjs/common';
 import { FFBLangChainService } from '../../FFB/services/ffb-langchain.service';
 
@@ -5,19 +11,18 @@ import { MongoCoreService } from '../../mongo/mongo-core.service';
 import { BlockRepository } from '../repositories/block.repository';
 import { Block } from '../schemas/site.schema';
 import { PhaseRepository } from '../repositories/phase.repository';
-import { SiteRepository } from '../repositories/site.repository';
 import { FFBProductionService } from '../../FFB/services/ffb-production.service';
 
 @Injectable()
 export class BlockService {
     private repo: BlockRepository;
     private phaseRepo: PhaseRepository;
-    private siteRepo: SiteRepository;
 
     constructor(
         private readonly mongoCore: MongoCoreService,
         @Inject(forwardRef(() => FFBProductionService))
         private readonly ffbService: FFBProductionService,
+        @Inject(forwardRef(() => FFBLangChainService))
         private readonly ffbLangChainService: FFBLangChainService,
     ) { }
 
@@ -27,27 +32,21 @@ export class BlockService {
             const db = await this.mongoCore.getDb();
             this.repo = new BlockRepository(db);
             this.phaseRepo = new PhaseRepository(db);
-            this.siteRepo = new SiteRepository(db);
             await this.repo.init();
             await this.phaseRepo.init();
-            await this.siteRepo.init();
         }
-        return { repo: this.repo, phaseRepo: this.phaseRepo, siteRepo: this.siteRepo };
+        return { repo: this.repo, phaseRepo: this.phaseRepo };
     }
 
+
+
     async create(block: Block): Promise<Block> {
-        const { repo, phaseRepo, siteRepo } = await this.getRepos();
+        const { repo, phaseRepo } = await this.getRepos();
 
         // 1. Validate Phase exists
-        const phase = await phaseRepo.findById(block.phaseId);
+        const phase = await phaseRepo.findById(block.phaseCode);
         if (!phase) {
-            throw new BadRequestException(`Phase with ID ${block.phaseId} does not exist.`);
-        }
-
-        // 2. Validate Site exists
-        const site = await siteRepo.findById(phase.siteId);
-        if (!site) {
-            throw new BadRequestException(`Site with ID ${phase.siteId} (from Phase ${block.phaseId}) does not exist.`);
+            throw new BadRequestException(`Phase with code ${block.phaseCode} does not exist.`);
         }
 
         return repo.create(block);
@@ -64,19 +63,14 @@ export class BlockService {
     }
 
     async update(id: string, update: Partial<Block>): Promise<void> {
-        const { repo, phaseRepo, siteRepo } = await this.getRepos();
+        const { repo, phaseRepo } = await this.getRepos();
 
         const block = await repo.findById(id);
         if (!block) throw new NotFoundException('Block not found');
 
-        if (update.phaseId) {
-            // 1. Validate Phase exists
-            const phase = await phaseRepo.findById(update.phaseId);
-            if (!phase) throw new BadRequestException(`Phase with ID ${update.phaseId} does not exist.`);
-
-            // 2. Validate Site exists
-            const site = await siteRepo.findById(phase.siteId);
-            if (!site) throw new BadRequestException(`Site with ID ${phase.siteId} (from Phase ${update.phaseId}) does not exist.`);
+        if (update.phaseCode) {
+            const phase = await phaseRepo.findById(update.phaseCode);
+            if (!phase) throw new BadRequestException(`Phase with code ${update.phaseCode} does not exist.`);
         }
 
         await repo.update(id, update);
@@ -89,7 +83,7 @@ export class BlockService {
 
         // Cascading Delete:
         // 1. Delete all FFB Production records referencing this block
-        await this.ffbService.deleteMany({ 'block.id': id });
+        await this.ffbService.deleteMany({ blockCode: block.blockCode });
 
         // 2. Delete the block itself
         await repo.delete(id);

+ 15 - 23
src/site/services/phase.service.ts

@@ -1,23 +1,28 @@
-import { Injectable, NotFoundException, BadRequestException, Inject, forwardRef } from '@nestjs/common';
+/**
+ * * **Current State:** The `PhaseService` contains parent validations verifying relational `siteId` presence and casting relational string references to `ObjectId` before persisting.
+ * * **Intended Mutation:** We will completely strip out all relational parent validation logic pointing to `siteId` or `siteRepo` / `SiteRepository` imports. We will refactor method scopes (`findById`, `delete`, `update`) to match and cascade delete using plain business strings like `phaseCode` and `blockCode`, removing `ObjectId` transformations entirely.
+ * * **Risk Check:** Downstream geographical controllers (e.g. `PhaseController`) mapping or passing legacy interfaces that contain nested site structures or old properties will throw type errors.
+ */
+
+import { Injectable, NotFoundException, Inject, forwardRef } from '@nestjs/common';
 import { FFBLangChainService } from '../../FFB/services/ffb-langchain.service';
 
 import { MongoCoreService } from '../../mongo/mongo-core.service';
 import { PhaseRepository } from '../repositories/phase.repository';
 import { Phase } from '../schemas/site.schema';
-import { SiteRepository } from '../repositories/site.repository';
 import { BlockRepository } from '../repositories/block.repository';
 import { FFBProductionService } from '../../FFB/services/ffb-production.service';
 
 @Injectable()
 export class PhaseService {
     private repo: PhaseRepository;
-    private siteRepo: SiteRepository;
     private blockRepo: BlockRepository;
 
     constructor(
         private readonly mongoCore: MongoCoreService,
         @Inject(forwardRef(() => FFBProductionService))
         private readonly ffbService: FFBProductionService,
+        @Inject(forwardRef(() => FFBLangChainService))
         private readonly ffbLangChainService: FFBLangChainService,
     ) { }
 
@@ -26,24 +31,16 @@ export class PhaseService {
         if (!this.repo) {
             const db = await this.mongoCore.getDb();
             this.repo = new PhaseRepository(db);
-            this.siteRepo = new SiteRepository(db);
             this.blockRepo = new BlockRepository(db);
             await this.repo.init();
-            await this.siteRepo.init();
             await this.blockRepo.init();
         }
-        return { repo: this.repo, siteRepo: this.siteRepo, blockRepo: this.blockRepo };
+        return { repo: this.repo, blockRepo: this.blockRepo };
     }
 
-    async create(phase: Phase): Promise<Phase> {
-        const { repo, siteRepo } = await this.getRepos();
-
-        // Parent Validation: Check if Site exists
-        const site = await siteRepo.findById(phase.siteId);
-        if (!site) {
-            throw new BadRequestException(`Site with ID ${phase.siteId} does not exist.`);
-        }
 
+    async create(phase: Phase): Promise<Phase> {
+        const { repo } = await this.getRepos();
         return repo.create(phase);
     }
 
@@ -58,22 +55,17 @@ export class PhaseService {
         if (!phase) return null;
 
         if (populate) {
-            phase.blocks = await blockRepo.findAll({ phaseId: id });
+            phase.blocks = await blockRepo.findAll({ phaseCode: phase.phaseCode });
         }
         return phase;
     }
 
     async update(id: string, update: Partial<Phase>): Promise<void> {
-        const { repo, siteRepo } = await this.getRepos();
+        const { repo } = await this.getRepos();
 
         const phase = await repo.findById(id);
         if (!phase) throw new NotFoundException('Phase not found');
 
-        if (update.siteId) {
-            const site = await siteRepo.findById(update.siteId);
-            if (!site) throw new BadRequestException(`Site with ID ${update.siteId} does not exist.`);
-        }
-
         await repo.update(id, update);
     }
 
@@ -84,10 +76,10 @@ export class PhaseService {
 
         // Cascading Delete:
         // 1. Delete all blocks of this phase
-        await blockRepo.deleteMany({ phaseId: id });
+        await blockRepo.deleteMany({ phaseCode: phase.phaseCode });
 
         // 2. Delete all FFB Production records referencing this phase
-        await this.ffbService.deleteMany({ 'phase.id': id });
+        await this.ffbService.deleteMany({ phaseCode: phase.phaseCode });
 
         // 3. Delete the phase itself
         await repo.delete(id);

+ 1 - 111
src/site/services/site.service.ts

@@ -1,111 +1 @@
-import { Injectable, NotFoundException, Inject, forwardRef } from '@nestjs/common';
-import { FFBLangChainService } from '../../FFB/services/ffb-langchain.service';
-
-import { MongoCoreService } from '../../mongo/mongo-core.service';
-import { SiteRepository } from '../repositories/site.repository';
-import { Site } from '../schemas/site.schema';
-import { PhaseRepository } from '../repositories/phase.repository';
-import { BlockRepository } from '../repositories/block.repository';
-import { FFBProductionService } from '../../FFB/services/ffb-production.service';
-
-@Injectable()
-export class SiteService {
-    private repo: SiteRepository;
-    private phaseRepo: PhaseRepository;
-    private blockRepo: BlockRepository;
-
-    constructor(
-        private readonly mongoCore: MongoCoreService,
-        @Inject(forwardRef(() => FFBProductionService))
-        private readonly ffbService: FFBProductionService,
-        @Inject(forwardRef(() => FFBLangChainService))
-        private readonly ffbLangChainService: FFBLangChainService,
-    ) { }
-
-
-    private async getRepos() {
-        if (!this.repo) {
-            const db = await this.mongoCore.getDb();
-            this.repo = new SiteRepository(db);
-            this.phaseRepo = new PhaseRepository(db);
-            this.blockRepo = new BlockRepository(db);
-            await this.repo.init();
-            await this.phaseRepo.init();
-            await this.blockRepo.init();
-        }
-        return { repo: this.repo, phaseRepo: this.phaseRepo, blockRepo: this.blockRepo };
-    }
-
-    async create(site: Site): Promise<Site> {
-        const { repo } = await this.getRepos();
-        return repo.create(site);
-    }
-
-    async findAll(): Promise<Site[]> {
-        const { repo } = await this.getRepos();
-        return repo.findAll();
-    }
-
-    async findById(id: string, populate = false): Promise<Site | null> {
-        const { repo, phaseRepo, blockRepo } = await this.getRepos();
-        const site = await repo.findById(id);
-        if (!site) return null;
-
-        if (populate) {
-            const phases = await phaseRepo.findAll({ siteId: id });
-            for (const phase of phases) {
-                phase.blocks = await blockRepo.findAll({ phaseId: phase._id });
-            }
-            site.phases = phases;
-        }
-        return site;
-    }
-
-    async update(id: string, update: Partial<Site>): Promise<void> {
-        const { repo } = await this.getRepos();
-        const site = await repo.findById(id);
-        if (!site) throw new NotFoundException('Site not found');
-        await repo.update(id, update);
-    }
-
-    async delete(id: string): Promise<void> {
-        const { repo, phaseRepo, blockRepo } = await this.getRepos();
-        const site = await repo.findById(id);
-        if (!site) throw new NotFoundException('Site not found');
-
-        // Cascading Delete:
-        // 1. Find all phases
-        const phases = await phaseRepo.findAll({ siteId: id });
-        for (const phase of phases) {
-            // 2. Delete all blocks of each phase
-            await blockRepo.deleteMany({ phaseId: phase._id });
-        }
-        // 3. Delete all phases of the site
-        await phaseRepo.deleteMany({ siteId: id });
-
-        // 4. Delete all FFB Production records referencing this site
-        await this.ffbService.deleteMany({ 'site.id': id });
-
-        // 5. Delete the site itself
-        await repo.delete(id);
-    }
-
-    async generateDescription(data: { name: string, address?: string }): Promise<{ description: string }> {
-        const prompt = `Write a realistic, professional description (2–4 sentences) for an oil palm plantation site named "${data.name}" located at "${data.address || 'Unknown address'}".
-
-Choose 1-2 aspects from the following to incorporate naturally:
-- General infrastructure (e.g., well-maintained access roads, proximity to main transport hubs, presence of staff housing)
-- Environmental setting (e.g., rolling hills, river boundaries, dense canopy coverage)
-- Workforce and activity (e.g., active harvesting teams, daily briefings at the muster point, recent replanting efforts)
-- Strategic importance (e.g., central processing site, high-yield territory, pivotal logistical junction)
-
-Guidelines:
-- Maintain a grounded, operational tone.
-- Do not mention the bullet points explicitly; weave them into the narrative.
-- Ensure the length is exactly 2-4 sentences.
-
-Description:`;
-        const description = await this.ffbLangChainService.chatStateless(prompt);
-        return { description: description.replace(/^Description:\s*/i, '').trim() };
-    }
-}
+// Deprecated per ADR-001. Flat business keys are used instead of nested Site relationships.

+ 0 - 5
src/site/site.module.ts

@@ -1,8 +1,6 @@
 import { Module, forwardRef } from '@nestjs/common';
-import { SiteController } from './controllers/site.controller';
 import { PhaseController } from './controllers/phase.controller';
 import { BlockController } from './controllers/block.controller';
-import { SiteService } from './services/site.service';
 import { PhaseService } from './services/phase.service';
 import { BlockService } from './services/block.service';
 import { MongoModule } from '../mongo/mongo.module';
@@ -14,17 +12,14 @@ import { FFBProductionModule } from '../FFB/ffb-production.module';
         forwardRef(() => FFBProductionModule),
     ],
     controllers: [
-        SiteController,
         PhaseController,
         BlockController,
     ],
     providers: [
-        SiteService,
         PhaseService,
         BlockService,
     ],
     exports: [
-        SiteService,
         PhaseService,
         BlockService,
     ],