import { config } from "dotenv"; config(); import { PrismaClient, JobStatus } from "@prisma/client"; import { PrismaPg } from "@prisma/adapter-pg"; import Decimal from "decimal.js"; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); const prisma = new PrismaClient({ adapter } as any); async function main() { console.log("🌱 Seeding database..."); // --- Rate Cards --- const rateCardsData = [ { name: "VFX Supervisor", department: "Leadership", defaultDailyRate: 1200, hoursPerDay: 8 }, { name: "CG Supervisor", department: "Leadership", defaultDailyRate: 1100, hoursPerDay: 8 }, { name: "Senior Compositor", department: "Compositing", defaultDailyRate: 950, hoursPerDay: 8 }, { name: "Mid Compositor", department: "Compositing", defaultDailyRate: 750, hoursPerDay: 8 }, { name: "Junior Compositor", department: "Compositing", defaultDailyRate: 500, hoursPerDay: 8 }, { name: "Senior Animator", department: "Animation", defaultDailyRate: 900, hoursPerDay: 8 }, { name: "Mid Animator", department: "Animation", defaultDailyRate: 700, hoursPerDay: 8 }, { name: "Senior Modeller", department: "3D Art", defaultDailyRate: 850, hoursPerDay: 8 }, { name: "Texture Artist", department: "3D Art", defaultDailyRate: 750, hoursPerDay: 8 }, { name: "Lighting TD", department: "Lighting", defaultDailyRate: 900, hoursPerDay: 8 }, { name: "FX Artist", department: "FX", defaultDailyRate: 900, hoursPerDay: 8 }, { name: "Matchmove Artist", department: "Tracking", defaultDailyRate: 650, hoursPerDay: 8 }, { name: "Roto Artist", department: "Prep", defaultDailyRate: 450, hoursPerDay: 8 }, { name: "Paint Artist", department: "Prep", defaultDailyRate: 500, hoursPerDay: 8 }, { name: "Pipeline TD", department: "Pipeline", defaultDailyRate: 1000, hoursPerDay: 8 }, ]; const rateCards = await Promise.all( rateCardsData.map((rc) => prisma.rateCard.upsert({ where: { id: `seed-rc-${rc.name.toLowerCase().replace(/\s+/g, "-")}` }, create: { id: `seed-rc-${rc.name.toLowerCase().replace(/\s+/g, "-")}`, name: rc.name, department: rc.department, defaultDailyRate: new Decimal(rc.defaultDailyRate), hoursPerDay: new Decimal(rc.hoursPerDay), currency: "ZAR", isActive: true, }, update: {}, }) ) ); console.log(` ✓ ${rateCards.length} rate cards`); // --- Clients --- const client1 = await prisma.client.upsert({ where: { id: "seed-client-01" }, create: { id: "seed-client-01", companyName: "Acme Studios", contactPerson: "Jane Reynolds", email: "jane@acmestudios.com", phone: "+1 310 555 0100", billingAddress: "1234 Sunset Blvd\nLos Angeles, CA 90028", vatNumber: "US-123456789", }, update: {}, }); const client2 = await prisma.client.upsert({ where: { id: "seed-client-02" }, create: { id: "seed-client-02", companyName: "Nebula Films", contactPerson: "Tom Kavanagh", email: "tom@nebulafilms.co.uk", phone: "+44 20 7946 0100", billingAddress: "22 Soho Square\nLondon, W1D 4QE", }, update: {}, }); console.log(" ✓ 2 clients"); // --- Jobs --- const job1 = await prisma.job.upsert({ where: { id: "seed-job-01" }, create: { id: "seed-job-01", projectTitle: "Feature Film VFX Package", projectCode: "ACME-2024-001", clientId: client1.id, status: "DRAFT" as JobStatus, currency: "ZAR", markupPercent: new Decimal(10), taxPercent: new Decimal(15), discountPercent: new Decimal(0), description: "Full VFX package for Acme Studios' upcoming feature film including creature work, environments, and practical enhancement.", termsAndConditions: "Payment terms: 50% on approval, 50% on delivery. Prices valid for 30 days. Rates exclude VAT where applicable.", }, update: {}, }); const shot1 = await prisma.shot.upsert({ where: { id: "seed-shot-01" }, create: { id: "seed-shot-01", jobId: job1.id, shotCode: "SH0010", durationSeconds: 4, complexityMultiplier: new Decimal(1.5), description: "Hero creature reveal — CG creature integration in live action environment", sortOrder: 0, }, update: {}, }); const shot2 = await prisma.shot.upsert({ where: { id: "seed-shot-02" }, create: { id: "seed-shot-02", jobId: job1.id, shotCode: "SH0020", durationSeconds: 2, complexityMultiplier: new Decimal(1.0), description: "Establishing environment — CG extension of practical set", sortOrder: 1, }, update: {}, }); const seniorComp = rateCards.find((r) => r.name === "Senior Compositor")!; const lightingTD = rateCards.find((r) => r.name === "Lighting TD")!; const fxArtist = rateCards.find((r) => r.name === "FX Artist")!; const rotoArtist = rateCards.find((r) => r.name === "Roto Artist")!; // Line items for shot 1 const lineItemsShot1 = [ { taskName: "Roto / Paint", rateCardId: rotoArtist.id, artistRole: rotoArtist.name, dailyRate: 450, hoursPerDay: 8, numArtists: 1, numDays: 3, sortOrder: 0 }, { taskName: "Lighting & Rendering", rateCardId: lightingTD.id, artistRole: lightingTD.name, dailyRate: 900, hoursPerDay: 8, numArtists: 1, numDays: 5, sortOrder: 1 }, { taskName: "FX Simulation", rateCardId: fxArtist.id, artistRole: fxArtist.name, dailyRate: 900, hoursPerDay: 8, numArtists: 1, numDays: 4, sortOrder: 2 }, { taskName: "Compositing", rateCardId: seniorComp.id, artistRole: seniorComp.name, dailyRate: 950, hoursPerDay: 8, numArtists: 1, numDays: 7, sortOrder: 3 }, ]; for (const li of lineItemsShot1) { await prisma.lineItem.upsert({ where: { id: `seed-li-s1-${li.sortOrder}` }, create: { id: `seed-li-s1-${li.sortOrder}`, shotId: shot1.id, taskName: li.taskName, rateCardId: li.rateCardId, artistRole: li.artistRole, dailyRate: new Decimal(li.dailyRate), hoursPerDay: new Decimal(li.hoursPerDay), numArtists: new Decimal(li.numArtists), numDays: new Decimal(li.numDays), quantityHours: new Decimal(0), markupPercent: new Decimal(0), discountPercent: new Decimal(0), taxPercent: new Decimal(0), hourlyRate: new Decimal(li.dailyRate / li.hoursPerDay), subtotal: new Decimal(li.dailyRate * li.numDays * li.numArtists * 1.5), markupAmount: new Decimal(0), discountAmount: new Decimal(0), taxAmount: new Decimal(0), total: new Decimal(li.dailyRate * li.numDays * li.numArtists * 1.5), sortOrder: li.sortOrder, }, update: {}, }); } // Line items for shot 2 const midComp = rateCards.find((r) => r.name === "Mid Compositor")!; const lineItemsShot2 = [ { taskName: "Roto / Paint", rateCardId: rotoArtist.id, artistRole: rotoArtist.name, dailyRate: 450, hoursPerDay: 8, numArtists: 2, numDays: 2, sortOrder: 0 }, { taskName: "Compositing", rateCardId: midComp.id, artistRole: midComp.name, dailyRate: 750, hoursPerDay: 8, numArtists: 1, numDays: 4, sortOrder: 1 }, ]; for (const li of lineItemsShot2) { await prisma.lineItem.upsert({ where: { id: `seed-li-s2-${li.sortOrder}` }, create: { id: `seed-li-s2-${li.sortOrder}`, shotId: shot2.id, taskName: li.taskName, rateCardId: li.rateCardId, artistRole: li.artistRole, dailyRate: new Decimal(li.dailyRate), hoursPerDay: new Decimal(li.hoursPerDay), numArtists: new Decimal(li.numArtists), numDays: new Decimal(li.numDays), quantityHours: new Decimal(0), markupPercent: new Decimal(0), discountPercent: new Decimal(0), taxPercent: new Decimal(0), hourlyRate: new Decimal(li.dailyRate / li.hoursPerDay), subtotal: new Decimal(li.dailyRate * li.numDays * li.numArtists), markupAmount: new Decimal(0), discountAmount: new Decimal(0), taxAmount: new Decimal(0), total: new Decimal(li.dailyRate * li.numDays * li.numArtists), sortOrder: li.sortOrder, }, update: {}, }); } console.log(" ✓ 1 job with 2 shots and line items"); // --- Asset example on job 1 --- const asset1 = await prisma.asset.upsert({ where: { id: "seed-asset-01" }, create: { id: "seed-asset-01", jobId: job1.id, assetName: "Hero Creature", assetType: "Character", complexityMultiplier: new Decimal(2.0), description: "Full CG creature, hero quality, 4K textures", sortOrder: 0, }, update: {}, }); const seniorModeller = rateCards.find((r) => r.name === "Senior Modeller")!; const textureArtist = rateCards.find((r) => r.name === "Texture Artist")!; const assetLineItems = [ { taskName: "High Res Modelling", rateCardId: seniorModeller.id, artistRole: seniorModeller.name, dailyRate: 850, numDays: 8, numArtists: 1, sortOrder: 0 }, { taskName: "Texturing & Look Dev", rateCardId: textureArtist.id, artistRole: textureArtist.name, dailyRate: 750, numDays: 5, numArtists: 1, sortOrder: 1 }, { taskName: "Rigging", rateCardId: seniorModeller.id, artistRole: "Rigging Artist", dailyRate: 850, numDays: 6, numArtists: 1, sortOrder: 2 }, ]; for (const li of assetLineItems) { await prisma.lineItem.upsert({ where: { id: `seed-li-a1-${li.sortOrder}` }, create: { id: `seed-li-a1-${li.sortOrder}`, assetId: asset1.id, taskName: li.taskName, rateCardId: li.rateCardId, artistRole: li.artistRole, dailyRate: new Decimal(li.dailyRate), hoursPerDay: new Decimal(8), numArtists: new Decimal(li.numArtists), numDays: new Decimal(li.numDays), quantityHours: new Decimal(0), markupPercent: new Decimal(0), discountPercent: new Decimal(0), taxPercent: new Decimal(0), hourlyRate: new Decimal(li.dailyRate / 8), subtotal: new Decimal(li.dailyRate * li.numDays * li.numArtists * 2.0), markupAmount: new Decimal(0), discountAmount: new Decimal(0), taxAmount: new Decimal(0), total: new Decimal(li.dailyRate * li.numDays * li.numArtists * 2.0), sortOrder: li.sortOrder, }, update: {}, }); } console.log(" ✓ 1 asset with line items"); // Second job for client 2 (sent) await prisma.job.upsert({ where: { id: "seed-job-02" }, create: { id: "seed-job-02", projectTitle: "TV Commercial Package", projectCode: "NEBULA-2024-001", clientId: client2.id, status: "SENT" as JobStatus, currency: "ZAR", markupPercent: new Decimal(15), taxPercent: new Decimal(15), discountPercent: new Decimal(0), description: "30-second commercial with full CG product visualization and environment.", }, update: {}, }); console.log(" ✓ 1 additional job (sent)"); console.log("✅ Seeding complete!"); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });