# Hybrid Tender System Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Enable hybrid tendering with category-based (bulk) and project-specific tendering, including dynamic item creation with auto-generated codes.

**Architecture:** Add ProjectSpecificItem table to store project-specific items not in master list. Extend TenderBundleItem to support both item sources. Auto-generate codes using pattern PROJ-{projectCode}-{sequence}.

**Tech Stack:** Next.js, tRPC, Prisma, PostgreSQL

---

## File Structure

```
prisma/schema.prisma                    - Add new models
src/lib/types/item.ts                   - Add type for ProjectSpecificItem
src/server/services/project-specific-item.service.ts - New service for item creation
src/server/routers/tender.router.ts     - Update for new item types
src/server/routers/boq.router.ts        - Update to handle both item sources
```

---

## Task 1: Prisma Schema Changes

**Files:**

- Modify: `prisma/schema.prisma:188-206` (Item model area)
- Modify: `prisma/schema.prisma:266-294` (TenderBundleItem area)
- Modify: `prisma/schema.prisma:404-428` (TenderNotice area)

- [ ] **Step 1: Add TenderItemSource enum to schema**

```prisma
enum TenderItemSource {
    MASTER_ITEM
    PROJECT_SPECIFIC_ITEM
}
```

Add after line 69 (after TenderType enum).

- [ ] **Step 2: Add ProjectSpecificItem model**

Add after Item model (after line 206):

```prisma
model ProjectSpecificItem {
    id          String   @id @default(cuid())
    code        String   @unique
    name        String
    description String?
    unit        String
    projectId   String
    createdAt   DateTime @default(now())
    updatedAt   DateTime @updatedAt

    project            Project             @relation(fields: [projectId], references: [id])
    tenderBundleItems  TenderBundleItem[]
    bidLineItems       BidLineItem[]
}
```

- [ ] **Step 3: Add projectSpecificItemId to TenderBundleItem**

In TenderBundleItem model (line 282-294), add field:

```prisma
projectSpecificItemId String?
projectSpecificItem   ProjectSpecificItem? @relation(fields: [projectSpecificItemId], references: [id])
```

- [ ] **Step 4: Add projectId to TenderNotice**

In TenderNotice model (line 404-428), add field after tenderType:

```prisma
projectId  String?
project    Project?  @relation(fields: [projectId], references: [id])
```

- [ ] **Step 5: Add projectSpecificItemId to BidLineItem**

In BidLineItem model (line 467-479), add:

```prisma
projectSpecificItemId String?
projectSpecificItem   ProjectSpecificItem? @relation(fields: [projectSpecificItemId], references: [id])
```

- [ ] **Step 6: Update Project model relation**

In Project model (line 225-248), add:

```prisma
projectSpecificItems ProjectSpecificItem[]
```

- [ ] **Step 7: Generate Prisma client**

Run: `npx prisma generate`
Expected: Success with new types

- [ ] **Step 8: Run migration**

Run: `npx prisma migrate dev --name add_project_specific_items`
Expected: Migration applied

- [ ] **Step 9: Commit**

```bash
git add prisma/schema.prisma
git commit -m "feat: add ProjectSpecificItem model and relations"
npx prisma generate && npx prisma migrate dev --name add_project_specific_items
```

---

## Task 2: TypeScript Types

**Files:**

- Modify: `src/lib/types/item.ts`

- [ ] **Step 1: Read existing types**

```typescript
// src/lib/types/item.ts
```

- [ ] **Step 2: Add ProjectSpecificItem type**

```typescript
export interface ProjectSpecificItem {
  id: string;
  code: string;
  name: string;
  description: string | null;
  unit: string;
  projectId: string;
  createdAt: Date;
  updatedAt: Date;
}

export type TenderItemSource = "MASTER_ITEM" | "PROJECT_SPECIFIC_ITEM";

export interface TenderItem {
  id: string;
  name: string;
  unit: string;
  code: string | null;
  quantity: number;
  estimatedRate: number | null;
  source: TenderItemSource;
  itemId?: string;
  projectSpecificItemId?: string;
}
```

- [ ] **Step 3: Commit**

```bash
git add src/lib/types/item.ts
git commit -m "types: add ProjectSpecificItem and TenderItem types"
```

---

## Task 3: Project-Specific Item Service

**Files:**

- Create: `src/server/services/project-specific-item.service.ts`

- [ ] **Step 1: Create service file**

```typescript
import { db } from "@/lib/db";
import { Prisma } from "@prisma/generated/client";

export async function generateProjectItemCode(
  projectId: string,
): Promise<string> {
  const project = await db.project.findUnique({
    where: { id: projectId },
    select: { code: true },
  });

  if (!project) {
    throw new Error("Project not found");
  }

  const prefix = `PROJ-${project.code}`;

  const lastItem = await db.projectSpecificItem.findFirst({
    where: { code: { startsWith: prefix } },
    orderBy: { code: "desc" },
    select: { code: true },
  });

  let sequence = 1;
  if (lastItem) {
    const parts = lastItem.code.split("-");
    const lastPart = parts[parts.length - 1];
    if (lastPart) {
      const num = parseInt(lastPart, 10);
      if (!isNaN(num)) {
        sequence = num + 1;
      }
    }
  }

  return `${prefix}-${sequence.toString().padStart(4, "0")}`;
}

export interface CreateProjectSpecificItemParams {
  projectId: string;
  name: string;
  unit: string;
  description?: string;
}

export async function createProjectSpecificItem(
  params: CreateProjectSpecificItemParams,
): Promise<{ id: string; code: string; name: string; unit: string }> {
  const { projectId, name, unit, description } = params;

  const code = await generateProjectItemCode(projectId);

  const item = await db.projectSpecificItem.create({
    data: {
      code,
      name,
      unit,
      description,
      projectId,
    },
    select: { id: true, code: true, name: true, unit: true },
  });

  return item;
}

export async function getProjectSpecificItems(
  projectId: string,
): Promise<
  Array<{
    id: string;
    code: string;
    name: string;
    unit: string;
    description: string | null;
  }>
> {
  return db.projectSpecificItem.findMany({
    where: { projectId },
    select: {
      id: true,
      code: true,
      name: true,
      unit: true,
      description: true,
    },
    orderBy: { createdAt: "desc" },
  });
}

export async function getProjectSpecificItemById(
  id: string,
): Promise<{
  id: string;
  code: string;
  name: string;
  unit: string;
  projectId: string;
} | null> {
  return db.projectSpecificItem.findUnique({
    where: { id },
    select: { id: true, code: true, name: true, unit: true, projectId: true },
  });
}
```

- [ ] **Step 2: Commit**

```bash
git add src/server/services/project-specific-item.service.ts
git commit -m "feat: add project-specific item service with auto-code generation"
```

---

## Task 4: Update tender.router.ts

**Files:**

- Modify: `src/server/routers/tender.router.ts`

- [ ] **Step 1: Read current create mutation (lines 73-105)**

- [ ] **Step 2: Update create input to support new fields**

Replace create input (lines 73-82):

```typescript
.create: adminProcedure
  .input(
    z.object({
      bundleId: z.string().optional(),
      categoryBundleId: z.string().optional(),
      tenderType: z.enum(["PROJECT", "CATEGORY"]).default("PROJECT"),
      projectId: z.string().optional(),  // For ad-hoc PROJECT tenders
      title: z.string().min(1),
      description: z.string().optional(),
      eligibility: z.string().optional(),
      submissionDeadline: z.date(),
      fiscalYearId: z.string(),
    }),
  )
```

- [ ] **Step 3: Validate tender type consistency in create mutation**

Add validation inside create mutation after line 84:

```typescript
// Validate tender type consistency
if (input.tenderType === "CATEGORY" && !input.categoryBundleId) {
  throw new TRPCError({
    code: "BAD_REQUEST",
    message: "Category tender requires categoryBundleId",
  });
}

if (input.tenderType === "PROJECT" && !input.bundleId && !input.projectId) {
  throw new TRPCError({
    code: "BAD_REQUEST",
    message: "Project tender requires either bundleId or projectId",
  });
}
```

- [ ] **Step 4: Commit**

```bash
git add src/server/routers/tender.router.ts
git commit -m "feat: update tender router to support tender type and ad-hoc tenders"
```

---

## Task 5: Update boq.router.ts

**Files:**

- Modify: `src/server/routers/boq.router.ts`

- [ ] **Step 1: Read current create mutation (lines 42-96)**

- [ ] **Step 2: Update items input to support both item types**

Replace items array in create input (lines 49-55):

```typescript
items: z.array(
  z.object({
    itemId: z.string().optional(),
    projectSpecificItemId: z.string().optional(),
    quantity: z.number(),
    estimatedRate: z.number().optional(),
  }),
),
```

- [ ] **Step 3: Add validation in create mutation**

After line 59 (after const assignments), add:

```typescript
// Validate mutual exclusivity
for (const item of items) {
  if (item.itemId && item.projectSpecificItemId) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: "Cannot specify both itemId and projectSpecificItemId",
    });
  }
  if (!item.itemId && !item.projectSpecificItemId) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: "Must specify either itemId or projectSpecificItemId",
    });
  }
}
```

- [ ] **Step 4: Update create data to handle both types**

Replace lines 89-95 (the create call):

```typescript
const tenderBundleItems = items.map((item) => ({
  itemId: item.itemId ?? undefined,
  projectSpecificItemId: item.projectSpecificItemId ?? undefined,
  quantity: item.quantity,
  estimatedRate: item.estimatedRate,
}));

const estimatedTotal = items.reduce(
  (sum, item) => sum + item.quantity * (item.estimatedRate ?? 0),
  0,
);

return ctx.db.tenderBundle.create({
  data: {
    ...bundleData,
    estimatedTotal,
    tenderBundleItems: { create: tenderBundleItems },
  },
});
```

- [ ] **Step 5: Commit**

```bash
git add src/server/routers/boq.router.ts
git commit -m "feat: update BOQ router to support both item types"
```

---

## Task 6: Add Project-Specific Item Router

**Files:**

- Create: `src/server/routers/project-specific-item.router.ts`

- [ ] **Step 1: Create new router**

```typescript
import { z } from "zod";
import {
  createTRPCRouter,
  adminProcedure,
  protectedProcedure,
} from "@/server/trpc";
import {
  createProjectSpecificItem,
  getProjectSpecificItems,
} from "@/server/services/project-specific-item.service";

export const projectSpecificItemRouter = createTRPCRouter({
  list: protectedProcedure
    .input(z.object({ projectId: z.string() }))
    .query(async ({ ctx, input }) => {
      return getProjectSpecificItems(input.projectId);
    }),

  create: adminProcedure
    .input(
      z.object({
        projectId: z.string(),
        name: z.string().min(1).max(255),
        unit: z.string().min(1).max(50),
        description: z.string().optional(),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      return createProjectSpecificItem(input);
    }),
});
```

- [ ] **Step 2: Register router in main router**

Find where routers are combined (likely in a root router file), add:

```typescript
import { projectSpecificItemRouter } from "./project-specific-item.router";

// Add to root router:
projectSpecificItem: projectSpecificItemRouter,
```

- [ ] **Step 3: Commit**

```bash
git add src/server/routers/project-specific-item.router.ts
git commit -m "feat: add project-specific-item router"
```

---

## Task 7: TypeScript Build Check

**Files:**

- Run: TypeScript compilation

- [ ] **Step 1: Run build**

Run: `npm run build` or `npx tsc --noEmit`
Expected: No errors

- [ ] **Step 2: Fix any type errors**

If errors, fix them

- [ ] **Step 3: Commit any fixes**

---

## Open Questions for Implementation

1. Should UI components be created now? (Not included in this plan - can be added later)
2. Should we add API for fetching combined tender items? (Service function already covers core logic)
3. Need to handle bid submission with project-specific items? (Already covered in schema - BidLineItem has projectSpecificItemId)

---

## Plan Complete

This plan adds:

- New ProjectSpecificItem table with auto-generated codes
- Schema changes to TenderBundleItem, TenderNotice, BidLineItem
- Service functions for item management
- Router updates to support both item types

**Next steps:**

1. Create worktree and start implementation
2. Run type checks and tests after each task
3. Verify with the user after core implementation
