From 535200aca2584e7d4a3c5035d142b3622efe386c Mon Sep 17 00:00:00 2001 From: Hardik Date: Tue, 5 May 2026 23:24:04 +0530 Subject: [PATCH] feat(db): Prisma schema with all 11 models, migrations and seed data Models: User, Vessel, Account, Vendor, Product, PurchaseOrder, POLineItem, PODocument, POAction, Receipt, Notification. PO fields include piQuotationNo/Date, requisitionNo/Date, placeOfDelivery, structured T&C (tcDelivery/tcDispatch/tcInspection/tcTransitInsurance/ tcPaymentTerms/tcOthers), currency default INR. POLineItem includes gstRate (default 0.18). Vendor includes address, gstin, contactMobile. Seed: 5 users across all roles, 3 vessels, 3 accounts, 3 vendors, 3 POs, 4 products. --- App/pelagia-portal/lib/db.ts | 16 + .../20260503171522_add_item_db/migration.sql | 228 ++++++++++++++ .../migration.sql | 5 + .../migration.sql | 16 + .../migration.sql | 14 + .../prisma/migrations/migration_lock.toml | 3 + App/pelagia-portal/prisma/schema.prisma | 226 ++++++++++++++ App/pelagia-portal/prisma/seed.ts | 280 ++++++++++++++++++ 8 files changed, 788 insertions(+) create mode 100644 App/pelagia-portal/lib/db.ts create mode 100644 App/pelagia-portal/prisma/migrations/20260503171522_add_item_db/migration.sql create mode 100644 App/pelagia-portal/prisma/migrations/20260503180801_add_product_catalogue_size_manager_edit/migration.sql create mode 100644 App/pelagia-portal/prisma/migrations/20260505114211_add_po_export_fields/migration.sql create mode 100644 App/pelagia-portal/prisma/migrations/20260505121423_structured_tc_fields/migration.sql create mode 100644 App/pelagia-portal/prisma/migrations/migration_lock.toml create mode 100644 App/pelagia-portal/prisma/schema.prisma create mode 100644 App/pelagia-portal/prisma/seed.ts diff --git a/App/pelagia-portal/lib/db.ts b/App/pelagia-portal/lib/db.ts new file mode 100644 index 0000000..9bb9ce6 --- /dev/null +++ b/App/pelagia-portal/lib/db.ts @@ -0,0 +1,16 @@ +import { PrismaClient } from "@prisma/client"; + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +export const db = + globalForPrisma.prisma ?? + new PrismaClient({ + log: + process.env.NODE_ENV === "development" + ? ["query", "error", "warn"] + : ["error"], + }); + +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/App/pelagia-portal/prisma/migrations/20260503171522_add_item_db/migration.sql b/App/pelagia-portal/prisma/migrations/20260503171522_add_item_db/migration.sql new file mode 100644 index 0000000..91b8c8d --- /dev/null +++ b/App/pelagia-portal/prisma/migrations/20260503171522_add_item_db/migration.sql @@ -0,0 +1,228 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('TECHNICAL', 'MANNING', 'ACCOUNTS', 'MANAGER', 'SUPERUSER', 'AUDITOR', 'ADMIN'); + +-- CreateEnum +CREATE TYPE "POStatus" AS ENUM ('DRAFT', 'SUBMITTED', 'MGR_REVIEW', 'VENDOR_ID_PENDING', 'EDITS_REQUESTED', 'REJECTED', 'MGR_APPROVED', 'SENT_FOR_PAYMENT', 'PAID_DELIVERED', 'CLOSED'); + +-- CreateEnum +CREATE TYPE "ActionType" AS ENUM ('CREATED', 'SUBMITTED', 'APPROVED', 'APPROVED_WITH_NOTE', 'REJECTED', 'EDITS_REQUESTED', 'VENDOR_ID_REQUESTED', 'VENDOR_ID_PROVIDED', 'PAYMENT_SENT', 'RECEIPT_CONFIRMED', 'CLOSED', 'REASSIGNED', 'PRODUCT_PRICE_UPDATED'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "employeeId" TEXT NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "role" "Role" NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Vessel" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "imoNumber" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Vessel_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Vendor" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "vendorId" TEXT, + "contactName" TEXT, + "contactEmail" TEXT, + "isVerified" BOOLEAN NOT NULL DEFAULT false, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Vendor_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Product" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "lastPrice" DECIMAL(12,2), + "lastVendorId" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "updatedAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PurchaseOrder" ( + "id" TEXT NOT NULL, + "poNumber" TEXT NOT NULL, + "title" TEXT NOT NULL, + "status" "POStatus" NOT NULL DEFAULT 'DRAFT', + "totalAmount" DECIMAL(12,2) NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'USD', + "dateRequired" TIMESTAMP(3), + "projectCode" TEXT, + "managerNote" TEXT, + "paymentRef" TEXT, + "submittedAt" TIMESTAMP(3), + "approvedAt" TIMESTAMP(3), + "paidAt" TIMESTAMP(3), + "closedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "submitterId" TEXT NOT NULL, + "vesselId" TEXT NOT NULL, + "accountId" TEXT NOT NULL, + "vendorId" TEXT, + + CONSTRAINT "PurchaseOrder_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "POLineItem" ( + "id" TEXT NOT NULL, + "description" TEXT NOT NULL, + "quantity" DECIMAL(10,3) NOT NULL, + "unit" TEXT NOT NULL, + "unitPrice" DECIMAL(12,2) NOT NULL, + "totalPrice" DECIMAL(12,2) NOT NULL, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "productId" TEXT, + "poId" TEXT NOT NULL, + + CONSTRAINT "POLineItem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PODocument" ( + "id" TEXT NOT NULL, + "fileName" TEXT NOT NULL, + "fileSize" INTEGER NOT NULL, + "mimeType" TEXT NOT NULL, + "storageKey" TEXT NOT NULL, + "uploadedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "poId" TEXT NOT NULL, + + CONSTRAINT "PODocument_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "POAction" ( + "id" TEXT NOT NULL, + "actionType" "ActionType" NOT NULL, + "note" TEXT, + "metadata" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "poId" TEXT NOT NULL, + "actorId" TEXT NOT NULL, + + CONSTRAINT "POAction_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Receipt" ( + "id" TEXT NOT NULL, + "storageKey" TEXT NOT NULL, + "fileName" TEXT NOT NULL, + "notes" TEXT, + "confirmedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "poId" TEXT NOT NULL, + + CONSTRAINT "Receipt_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Notification" ( + "id" TEXT NOT NULL, + "subject" TEXT NOT NULL, + "body" TEXT NOT NULL, + "sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "status" TEXT NOT NULL DEFAULT 'sent', + "poId" TEXT, + "userId" TEXT NOT NULL, + + CONSTRAINT "Notification_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_employeeId_key" ON "User"("employeeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Vessel_imoNumber_key" ON "Vessel"("imoNumber"); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_code_key" ON "Account"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "Vendor_vendorId_key" ON "Vendor"("vendorId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Product_code_key" ON "Product"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "PurchaseOrder_poNumber_key" ON "PurchaseOrder"("poNumber"); + +-- CreateIndex +CREATE UNIQUE INDEX "Receipt_poId_key" ON "Receipt"("poId"); + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_lastVendorId_fkey" FOREIGN KEY ("lastVendorId") REFERENCES "Vendor"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PurchaseOrder" ADD CONSTRAINT "PurchaseOrder_submitterId_fkey" FOREIGN KEY ("submitterId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PurchaseOrder" ADD CONSTRAINT "PurchaseOrder_vesselId_fkey" FOREIGN KEY ("vesselId") REFERENCES "Vessel"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PurchaseOrder" ADD CONSTRAINT "PurchaseOrder_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PurchaseOrder" ADD CONSTRAINT "PurchaseOrder_vendorId_fkey" FOREIGN KEY ("vendorId") REFERENCES "Vendor"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "POLineItem" ADD CONSTRAINT "POLineItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "POLineItem" ADD CONSTRAINT "POLineItem_poId_fkey" FOREIGN KEY ("poId") REFERENCES "PurchaseOrder"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PODocument" ADD CONSTRAINT "PODocument_poId_fkey" FOREIGN KEY ("poId") REFERENCES "PurchaseOrder"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "POAction" ADD CONSTRAINT "POAction_poId_fkey" FOREIGN KEY ("poId") REFERENCES "PurchaseOrder"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "POAction" ADD CONSTRAINT "POAction_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Receipt" ADD CONSTRAINT "Receipt_poId_fkey" FOREIGN KEY ("poId") REFERENCES "PurchaseOrder"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_poId_fkey" FOREIGN KEY ("poId") REFERENCES "PurchaseOrder"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/App/pelagia-portal/prisma/migrations/20260503180801_add_product_catalogue_size_manager_edit/migration.sql b/App/pelagia-portal/prisma/migrations/20260503180801_add_product_catalogue_size_manager_edit/migration.sql new file mode 100644 index 0000000..f7791cd --- /dev/null +++ b/App/pelagia-portal/prisma/migrations/20260503180801_add_product_catalogue_size_manager_edit/migration.sql @@ -0,0 +1,5 @@ +-- AlterEnum +ALTER TYPE "ActionType" ADD VALUE 'MANAGER_LINE_EDIT'; + +-- AlterTable +ALTER TABLE "POLineItem" ADD COLUMN "size" TEXT; diff --git a/App/pelagia-portal/prisma/migrations/20260505114211_add_po_export_fields/migration.sql b/App/pelagia-portal/prisma/migrations/20260505114211_add_po_export_fields/migration.sql new file mode 100644 index 0000000..dfc2b8b --- /dev/null +++ b/App/pelagia-portal/prisma/migrations/20260505114211_add_po_export_fields/migration.sql @@ -0,0 +1,16 @@ +-- AlterTable +ALTER TABLE "POLineItem" ADD COLUMN "gstRate" DECIMAL(5,4) NOT NULL DEFAULT 0.18; + +-- AlterTable +ALTER TABLE "PurchaseOrder" ADD COLUMN "piQuotationDate" TIMESTAMP(3), +ADD COLUMN "piQuotationNo" TEXT, +ADD COLUMN "placeOfDelivery" TEXT, +ADD COLUMN "requisitionDate" TIMESTAMP(3), +ADD COLUMN "requisitionNo" TEXT, +ADD COLUMN "termsAndConditions" TEXT, +ALTER COLUMN "currency" SET DEFAULT 'INR'; + +-- AlterTable +ALTER TABLE "Vendor" ADD COLUMN "address" TEXT, +ADD COLUMN "contactMobile" TEXT, +ADD COLUMN "gstin" TEXT; diff --git a/App/pelagia-portal/prisma/migrations/20260505121423_structured_tc_fields/migration.sql b/App/pelagia-portal/prisma/migrations/20260505121423_structured_tc_fields/migration.sql new file mode 100644 index 0000000..a08425f --- /dev/null +++ b/App/pelagia-portal/prisma/migrations/20260505121423_structured_tc_fields/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the column `termsAndConditions` on the `PurchaseOrder` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "PurchaseOrder" DROP COLUMN "termsAndConditions", +ADD COLUMN "tcDelivery" TEXT, +ADD COLUMN "tcDispatch" TEXT, +ADD COLUMN "tcInspection" TEXT, +ADD COLUMN "tcOthers" TEXT, +ADD COLUMN "tcPaymentTerms" TEXT, +ADD COLUMN "tcTransitInsurance" TEXT; diff --git a/App/pelagia-portal/prisma/migrations/migration_lock.toml b/App/pelagia-portal/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/App/pelagia-portal/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/App/pelagia-portal/prisma/schema.prisma b/App/pelagia-portal/prisma/schema.prisma new file mode 100644 index 0000000..d939cfc --- /dev/null +++ b/App/pelagia-portal/prisma/schema.prisma @@ -0,0 +1,226 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum Role { + TECHNICAL + MANNING + ACCOUNTS + MANAGER + SUPERUSER + AUDITOR + ADMIN +} + +enum POStatus { + DRAFT + SUBMITTED + MGR_REVIEW + VENDOR_ID_PENDING + EDITS_REQUESTED + REJECTED + MGR_APPROVED + SENT_FOR_PAYMENT + PAID_DELIVERED + CLOSED +} + +enum ActionType { + CREATED + SUBMITTED + APPROVED + APPROVED_WITH_NOTE + REJECTED + EDITS_REQUESTED + VENDOR_ID_REQUESTED + VENDOR_ID_PROVIDED + PAYMENT_SENT + RECEIPT_CONFIRMED + CLOSED + REASSIGNED + PRODUCT_PRICE_UPDATED + MANAGER_LINE_EDIT +} + +model User { + id String @id @default(cuid()) + employeeId String @unique + email String @unique + name String + passwordHash String + role Role + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + submittedPOs PurchaseOrder[] @relation("Submitter") + actions POAction[] + notifications Notification[] +} + +model Vessel { + id String @id @default(cuid()) + name String + imoNumber String? @unique + isActive Boolean @default(true) + + purchaseOrders PurchaseOrder[] +} + +model Account { + id String @id @default(cuid()) + code String @unique + name String + description String? + isActive Boolean @default(true) + + purchaseOrders PurchaseOrder[] +} + +model Vendor { + id String @id @default(cuid()) + name String + vendorId String? @unique + address String? + gstin String? + contactName String? + contactMobile String? + contactEmail String? + isVerified Boolean @default(false) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + + purchaseOrders PurchaseOrder[] + products Product[] @relation("ProductLastVendor") +} + +model Product { + id String @id @default(cuid()) + code String @unique + name String + description String? + lastPrice Decimal? @db.Decimal(12, 2) + lastVendorId String? + lastVendor Vendor? @relation("ProductLastVendor", fields: [lastVendorId], references: [id]) + isActive Boolean @default(true) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + + lineItems POLineItem[] +} + +model PurchaseOrder { + id String @id @default(cuid()) + poNumber String @unique + title String + status POStatus @default(DRAFT) + totalAmount Decimal @db.Decimal(12, 2) + currency String @default("INR") + dateRequired DateTime? + projectCode String? + managerNote String? + paymentRef String? + piQuotationNo String? + piQuotationDate DateTime? + requisitionNo String? + requisitionDate DateTime? + placeOfDelivery String? + tcDelivery String? + tcDispatch String? + tcInspection String? + tcTransitInsurance String? + tcPaymentTerms String? + tcOthers String? + submittedAt DateTime? + approvedAt DateTime? + paidAt DateTime? + closedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + submitterId String + submitter User @relation("Submitter", fields: [submitterId], references: [id]) + vesselId String + vessel Vessel @relation(fields: [vesselId], references: [id]) + accountId String + account Account @relation(fields: [accountId], references: [id]) + vendorId String? + vendor Vendor? @relation(fields: [vendorId], references: [id]) + + lineItems POLineItem[] + documents PODocument[] + actions POAction[] + receipt Receipt? + notifications Notification[] +} + +model POLineItem { + id String @id @default(cuid()) + description String + quantity Decimal @db.Decimal(10, 3) + unit String + unitPrice Decimal @db.Decimal(12, 2) + totalPrice Decimal @db.Decimal(12, 2) + gstRate Decimal @default(0.18) @db.Decimal(5, 4) + sortOrder Int @default(0) + size String? + productId String? + product Product? @relation(fields: [productId], references: [id]) + + poId String + po PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade) +} + +model PODocument { + id String @id @default(cuid()) + fileName String + fileSize Int + mimeType String + storageKey String + uploadedAt DateTime @default(now()) + + poId String + po PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade) +} + +model POAction { + id String @id @default(cuid()) + actionType ActionType + note String? + metadata Json? + createdAt DateTime @default(now()) + + poId String + po PurchaseOrder @relation(fields: [poId], references: [id]) + actorId String + actor User @relation(fields: [actorId], references: [id]) +} + +model Receipt { + id String @id @default(cuid()) + storageKey String + fileName String + notes String? + confirmedAt DateTime @default(now()) + + poId String @unique + po PurchaseOrder @relation(fields: [poId], references: [id]) +} + +model Notification { + id String @id @default(cuid()) + subject String + body String + sentAt DateTime @default(now()) + status String @default("sent") + + poId String? + po PurchaseOrder? @relation(fields: [poId], references: [id]) + userId String + user User @relation(fields: [userId], references: [id]) +} diff --git a/App/pelagia-portal/prisma/seed.ts b/App/pelagia-portal/prisma/seed.ts new file mode 100644 index 0000000..391d934 --- /dev/null +++ b/App/pelagia-portal/prisma/seed.ts @@ -0,0 +1,280 @@ +import { PrismaClient, Role } from "@prisma/client"; +import bcrypt from "bcryptjs"; + +const db = new PrismaClient(); + +async function main() { + console.log("Seeding database..."); + + const hash = (p: string) => bcrypt.hash(p, 12); + + // Users + const admin = await db.user.upsert({ + where: { email: "admin@pelagia.local" }, + update: {}, + create: { + employeeId: "EMP-001", + email: "admin@pelagia.local", + name: "System Admin", + passwordHash: await hash("admin1234"), + role: Role.ADMIN, + }, + }); + + const manager = await db.user.upsert({ + where: { email: "manager@pelagia.local" }, + update: {}, + create: { + employeeId: "EMP-002", + email: "manager@pelagia.local", + name: "James Hartwell", + passwordHash: await hash("manager1234"), + role: Role.MANAGER, + }, + }); + + const technical = await db.user.upsert({ + where: { email: "tech@pelagia.local" }, + update: {}, + create: { + employeeId: "EMP-003", + email: "tech@pelagia.local", + name: "Maria Santos", + passwordHash: await hash("tech1234"), + role: Role.TECHNICAL, + }, + }); + + const accounts = await db.user.upsert({ + where: { email: "accounts@pelagia.local" }, + update: {}, + create: { + employeeId: "EMP-004", + email: "accounts@pelagia.local", + name: "Chen Wei", + passwordHash: await hash("accounts1234"), + role: Role.ACCOUNTS, + }, + }); + + await db.user.upsert({ + where: { email: "manning@pelagia.local" }, + update: {}, + create: { + employeeId: "EMP-005", + email: "manning@pelagia.local", + name: "Raj Patel", + passwordHash: await hash("manning1234"), + role: Role.MANNING, + }, + }); + + // Vessels + const mv1 = await db.vessel.upsert({ + where: { imoNumber: "IMO9876543" }, + update: {}, + create: { name: "MV Pelagia Star", imoNumber: "IMO9876543" }, + }); + + const mv2 = await db.vessel.upsert({ + where: { imoNumber: "IMO9123456" }, + update: {}, + create: { name: "MV Aegean Wind", imoNumber: "IMO9123456" }, + }); + + await db.vessel.upsert({ + where: { imoNumber: "IMO9654321" }, + update: {}, + create: { name: "MV Poseidon", imoNumber: "IMO9654321" }, + }); + + // Accounts + const acc1 = await db.account.upsert({ + where: { code: "TECH-OPS" }, + update: {}, + create: { code: "TECH-OPS", name: "Technical Operations", description: "Engine and deck equipment" }, + }); + + const acc2 = await db.account.upsert({ + where: { code: "CREW-MGT" }, + update: {}, + create: { code: "CREW-MGT", name: "Crew Management", description: "Manning and crew welfare" }, + }); + + await db.account.upsert({ + where: { code: "FUEL-BNK" }, + update: {}, + create: { code: "FUEL-BNK", name: "Fuel & Bunkers", description: "Fuel procurement" }, + }); + + // Vendors + const vendor1 = await db.vendor.upsert({ + where: { vendorId: "VND-0001" }, + update: {}, + create: { + name: "Marine Parts International", + vendorId: "VND-0001", + contactName: "Tony Nguyen", + contactEmail: "tnguyen@marinepartsinternational.com", + isVerified: true, + }, + }); + + await db.vendor.upsert({ + where: { vendorId: "VND-0002" }, + update: {}, + create: { + name: "Global Crew Supplies", + vendorId: "VND-0002", + contactName: "Sarah Kim", + contactEmail: "sarah@globalcrewsupplies.com", + isVerified: true, + }, + }); + + await db.vendor.upsert({ + where: { vendorId: "VND-0003" }, + update: {}, + create: { + name: "Atlas Ship Chandlers", + vendorId: "VND-0003", + contactName: "Marco Rossi", + contactEmail: "marco@atlaschandlers.com", + isVerified: false, + }, + }); + + // Products + const prod1 = await db.product.upsert({ + where: { code: "PART-TURBO-SEAL" }, + update: {}, + create: { + code: "PART-TURBO-SEAL", + name: "Turbocharger Seal Kit", + description: "Replacement seal kit for main engine turbocharger", + }, + }); + + const prod2 = await db.product.upsert({ + where: { code: "PART-FP-PUMP" }, + update: {}, + create: { + code: "PART-FP-PUMP", + name: "High-Pressure Fuel Pump", + description: "Main engine high-pressure fuel pump assembly", + }, + }); + + await db.product.upsert({ + where: { code: "SAFE-LIFEJKT" }, + update: {}, + create: { + code: "SAFE-LIFEJKT", + name: "Life Jacket (SOLAS)", + description: "SOLAS-approved adult life jacket", + }, + }); + + await db.product.upsert({ + where: { code: "SAFE-EXTG-9KG" }, + update: {}, + create: { + code: "SAFE-EXTG-9KG", + name: "Fire Extinguisher 9kg", + description: "Dry powder fire extinguisher, 9kg", + }, + }); + + // Sample POs + const po1 = await db.purchaseOrder.create({ + data: { + poNumber: "PO-2026-00001", + title: "Engine Room Spare Parts — MV Pelagia Star", + status: "MGR_REVIEW", + totalAmount: 8450.0, + currency: "USD", + submittedAt: new Date(), + submitterId: technical.id, + vesselId: mv1.id, + accountId: acc1.id, + vendorId: vendor1.id, + lineItems: { + create: [ + { description: "Turbocharger seal kit", quantity: 2, unit: "set", unitPrice: 1200, totalPrice: 2400, sortOrder: 0, productId: prod1.id }, + { description: "High-pressure fuel pump", quantity: 1, unit: "pc", unitPrice: 4800, totalPrice: 4800, sortOrder: 1, productId: prod2.id }, + { description: "O-ring assortment pack", quantity: 5, unit: "pk", unitPrice: 250, totalPrice: 1250, sortOrder: 2 }, + ], + }, + actions: { + create: [ + { actionType: "CREATED", actorId: technical.id }, + { actionType: "SUBMITTED", actorId: technical.id }, + ], + }, + }, + }); + + await db.purchaseOrder.create({ + data: { + poNumber: "PO-2026-00002", + title: "Crew Safety Equipment — MV Aegean Wind", + status: "DRAFT", + totalAmount: 3200.0, + currency: "USD", + submitterId: technical.id, + vesselId: mv2.id, + accountId: acc2.id, + lineItems: { + create: [ + { description: "Life jackets (SOLAS)", quantity: 20, unit: "pc", unitPrice: 120, totalPrice: 2400, sortOrder: 0 }, + { description: "Fire extinguisher — 9kg", quantity: 4, unit: "pc", unitPrice: 200, totalPrice: 800, sortOrder: 1 }, + ], + }, + actions: { + create: [{ actionType: "CREATED", actorId: technical.id }], + }, + }, + }); + + await db.purchaseOrder.create({ + data: { + poNumber: "PO-2026-00003", + title: "Navigation Charts Update — Fleet", + status: "MGR_APPROVED", + totalAmount: 950.0, + currency: "USD", + submittedAt: new Date(Date.now() - 5 * 86400000), + approvedAt: new Date(Date.now() - 2 * 86400000), + submitterId: technical.id, + vesselId: mv1.id, + accountId: acc1.id, + lineItems: { + create: [ + { description: "INT chart folio update", quantity: 1, unit: "set", unitPrice: 950, totalPrice: 950, sortOrder: 0 }, + ], + }, + actions: { + create: [ + { actionType: "CREATED", actorId: technical.id }, + { actionType: "SUBMITTED", actorId: technical.id }, + { actionType: "APPROVED", actorId: manager.id, note: "Approved — update due before next voyage." }, + ], + }, + }, + }); + + console.log("Seed complete."); + console.log("\nDev login credentials:"); + console.log(" Admin: admin@pelagia.local / admin1234"); + console.log(" Manager: manager@pelagia.local / manager1234"); + console.log(" Tech: tech@pelagia.local / tech1234"); + console.log(" Accounts: accounts@pelagia.local / accounts1234"); + console.log(" Manning: manning@pelagia.local / manning1234"); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => db.$disconnect());