import { Prisma, PrismaClient } from "../src/generated/prisma/client.js"; import { PrismaPg } from "@prisma/adapter-pg"; import pg from "pg"; const ECHO_LIFE_ORG_ID = "cmo110dib000004kwa5nlxvwz"; const url = new URL(process.env.DATABASE_URL!); url.searchParams.delete("sslmode"); const pool = new pg.Pool({ connectionString: url.toString(), max: 5, ssl: { rejectUnauthorized: false }, }); const prisma = new PrismaClient({ adapter: new PrismaPg(pool) }); async function main() { console.log("=== Echo Life Church Data Migration ===\n"); // 1. Associate all cards with Echo Life Church console.log("1. Associating cards with Echo Life Church..."); const cardUpdate = await prisma.responseCard.updateMany({ where: { organizationId: null }, data: { organizationId: ECHO_LIFE_ORG_ID }, }); console.log(` Updated ${cardUpdate.count} cards\n`); // 2. Split name into firstName + lastName console.log("2. Splitting name into firstName/lastName..."); const cardsWithName = await prisma.responseCard.findMany({ where: { name: { not: null }, firstName: null }, select: { id: true, name: true }, }); let splitCount = 0; for (const card of cardsWithName) { if (!card.name) continue; const parts = card.name.trim().split(/\s+/); const firstName = parts[0] || ""; const lastName = parts.slice(1).join(" ") || ""; await prisma.responseCard.update({ where: { id: card.id }, data: { firstName, lastName }, }); splitCount++; } console.log(` Split ${splitCount} names\n`); // 3. Seed default form template console.log("3. Seeding default Connect Card template..."); const existingTemplate = await prisma.formTemplate.findFirst({ where: { organizationId: ECHO_LIFE_ORG_ID, isDefault: true }, }); let templateId: string; if (existingTemplate) { templateId = existingTemplate.id; console.log(` Template already exists: ${existingTemplate.id}\n`); } else { const DEFAULT_FIELDS = [ { key: "firstName", label: "First Name", type: "text", section: "personal", required: true, removable: false, isCore: true, sortOrder: 0 }, { key: "lastName", label: "Last Name", type: "text", section: "personal", required: true, removable: false, isCore: true, sortOrder: 1 }, { key: "email", label: "Email", type: "email", section: "contact", sortOrder: 2 }, { key: "cellPhone", label: "Cell Phone", type: "phone", section: "contact", sortOrder: 3 }, { key: "homePhone", label: "Home Phone", type: "phone", section: "contact", sortOrder: 4 }, { key: "gender", label: "Gender", type: "select", section: "personal", options: ["Male", "Female"], sortOrder: 5 }, { key: "dateOfBirth", label: "Date of Birth", type: "date", section: "personal", sortOrder: 6 }, { key: "maritalStatus", label: "Marital Status", type: "select", section: "personal", options: ["Married", "Single", "Other"], sortOrder: 7 }, { key: "visitType", label: "Visit Type", type: "select", section: "survey", options: ["First/Second Time Guest", "Update My Information"], sortOrder: 8 }, { key: "address", label: "Address", type: "text", section: "address", sortOrder: 9 }, { key: "aptNumber", label: "Apt #", type: "text", section: "address", sortOrder: 10 }, { key: "city", label: "City", type: "text", section: "address", sortOrder: 11 }, { key: "state", label: "State", type: "text", section: "address", sortOrder: 12 }, { key: "zip", label: "Zip", type: "text", section: "address", sortOrder: 13 }, { key: "prayerRequests", label: "Prayer Requests", type: "textarea", section: "survey", sortOrder: 14 }, { key: "prayerForTeam", label: "For Prayer Team", type: "checkbox", section: "survey", sortOrder: 15 }, { key: "prayerConfidential", label: "Confidential", type: "checkbox", section: "survey", sortOrder: 16 }, { key: "messageTopics", label: "Message Topics", type: "multiselect", section: "survey", options: ["Stress/Anxiety", "Marriage", "Hearing God's Voice", "Dealing With Doubt", "Parenting", "Grief & Loss", "Forgiveness", "Finances", "Purpose/Calling", "Prayer", "Healthy Boundaries", "Understanding The Bible", "Emotional Health", "Sharing My Faith", "Decision Making", "Spiritual Disciplines", "Spiritual Gifts"], sortOrder: 17 }, { key: "nextStep", label: "Next Steps", type: "multiselect", section: "survey", options: ["Baptism", "Next Steps"], sortOrder: 18 }, { key: "attendanceDuration", label: "Attendance Duration", type: "radio", section: "survey", options: ["Less than 6 months", "6 Months - 1 Year", "1-3 Years", "4-6 Years", "7+ Years"], sortOrder: 19 }, { key: "campusPreference", label: "Campus Preference", type: "multiselect", section: "survey", options: ["Beulah", "Pace/Milton", "Gulf Breeze", "Warrington"], sortOrder: 20 }, { key: "howHeard", label: "How Did You Hear About Us?", type: "multiselect", section: "survey", options: ["This is my church home", "Regular Attender", "Drove by", "Social Media", "Google", "Personal Invite"], sortOrder: 21 }, { key: "serviceAttended", label: "Service Attended", type: "select", section: "survey", options: ["8:00 AM", "9:30 AM", "11:00 AM"], sortOrder: 22 }, { key: "followUp", label: "Follow-Up", type: "text", section: "followup", sortOrder: 23 }, { key: "notes", label: "Notes", type: "textarea", section: "followup", sortOrder: 24 }, ]; const template = await prisma.formTemplate.create({ data: { organizationId: ECHO_LIFE_ORG_ID, name: "Connect Card", slug: "connect-card", description: "Default connect card for Echo Life Church", isDefault: true, isActive: true, fields: { create: DEFAULT_FIELDS.map((f) => ({ key: f.key, label: f.label, type: f.type, section: f.section, required: f.required ?? false, removable: f.removable ?? true, isCore: f.isCore ?? false, sortOrder: f.sortOrder, options: f.options ?? undefined, })), }, }, }); templateId = template.id; console.log(` Created template: ${template.id}\n`); } // 4. Associate all cards with the default template console.log("4. Linking cards to Connect Card template..."); const templateLink = await prisma.responseCard.updateMany({ where: { organizationId: ECHO_LIFE_ORG_ID, formTemplateId: null }, data: { formTemplateId: templateId }, }); console.log(` Linked ${templateLink.count} cards\n`); // 5. Migrate legacy column data into fieldData JSON console.log("5. Migrating legacy fields into fieldData..."); const allCards = await prisma.responseCard.findMany({ where: { organizationId: ECHO_LIFE_ORG_ID, fieldData: { equals: Prisma.DbNull } }, select: { id: true, gender: true, dateOfBirth: true, maritalStatus: true, maritalStatusOther: true, visitType: true, homePhone: true, address: true, aptNumber: true, city: true, state: true, zip: true, prayerRequests: true, prayerForTeam: true, prayerConfidential: true, messageTopics: true, messageTopicsOther: true, nextStep: true, attendanceDuration: true, campusPreference: true, campusPreferenceOther: true, howHeard: true, howHeardOther: true, serviceAttended: true, followUp: true, notes: true, }, }); let fieldDataCount = 0; for (const card of allCards) { const fd: Record = {}; if (card.gender) fd.gender = card.gender; if (card.dateOfBirth) fd.dateOfBirth = card.dateOfBirth; if (card.maritalStatus) fd.maritalStatus = card.maritalStatus; if (card.visitType) fd.visitType = card.visitType; if (card.homePhone) fd.homePhone = card.homePhone; if (card.address) fd.address = card.address; if (card.aptNumber) fd.aptNumber = card.aptNumber; if (card.city) fd.city = card.city; if (card.state) fd.state = card.state; if (card.zip) fd.zip = card.zip; if (card.prayerRequests) fd.prayerRequests = card.prayerRequests; if (card.prayerForTeam) fd.prayerForTeam = card.prayerForTeam; if (card.prayerConfidential) fd.prayerConfidential = card.prayerConfidential; if (card.messageTopics) fd.messageTopics = card.messageTopics; if (card.nextStep) fd.nextStep = card.nextStep; if (card.attendanceDuration) fd.attendanceDuration = card.attendanceDuration; if (card.campusPreference) fd.campusPreference = card.campusPreference; if (card.howHeard) fd.howHeard = card.howHeard; if (card.serviceAttended) fd.serviceAttended = card.serviceAttended; if (card.followUp) fd.followUp = card.followUp; if (card.notes) fd.notes = card.notes; if (Object.keys(fd).length > 0) { await prisma.responseCard.update({ where: { id: card.id }, data: { fieldData: fd as Record }, }); fieldDataCount++; } } console.log(` Migrated fieldData for ${fieldDataCount} cards\n`); // 6. Create location + 3 collection days (services) console.log("6. Creating location and services..."); const existingLocation = await prisma.location.findFirst({ where: { organizationId: ECHO_LIFE_ORG_ID }, }); let locationId: string; if (existingLocation) { locationId = existingLocation.id; console.log(` Location already exists: ${existingLocation.name}\n`); } else { const location = await prisma.location.create({ data: { organizationId: ECHO_LIFE_ORG_ID, name: "Echo Life Church", address: "Pensacola, FL", timezone: "America/Chicago", }, }); locationId = location.id; console.log(` Created location: ${location.name}\n`); } const existingDays = await prisma.collectionDay.count({ where: { locationId }, }); if (existingDays > 0) { console.log(` ${existingDays} collection days already exist, skipping\n`); } else { const services = [ { name: "8:00 AM Service", dayOfWeek: 0, timeStart: "08:00", timeEnd: "09:15" }, { name: "9:30 AM Service", dayOfWeek: 0, timeStart: "09:30", timeEnd: "10:45" }, { name: "11:00 AM Service", dayOfWeek: 0, timeStart: "11:00", timeEnd: "12:15" }, ]; for (const svc of services) { await prisma.collectionDay.create({ data: { locationId, name: svc.name, description: `Sunday ${svc.name}`, dayOfWeek: svc.dayOfWeek, timeStart: svc.timeStart, timeEnd: svc.timeEnd, isRecurring: true, isActive: true, }, }); } console.log(` Created 3 Sunday services\n`); } // 7. Add all users as members of Echo Life Church console.log("7. Adding users to Echo Life Church..."); const allUsers = await prisma.user.findMany({ select: { id: true, email: true } }); const existingMembers = await prisma.orgMember.findMany({ where: { organizationId: ECHO_LIFE_ORG_ID }, select: { userId: true }, }); const existingMemberIds = new Set(existingMembers.map((m) => m.userId)); let added = 0; for (const user of allUsers) { if (existingMemberIds.has(user.id)) continue; const role = user.email === "randall.stillwell@gmail.com" ? "admin" : "viewer"; await prisma.orgMember.create({ data: { userId: user.id, organizationId: ECHO_LIFE_ORG_ID, role }, }); added++; } console.log(` Added ${added} new members (${existingMembers.length} already existed)\n`); // 8. Set activeOrgId for users without one console.log("8. Setting active org for users..."); const usersNoOrg = await prisma.user.findMany({ where: { activeOrgId: null }, select: { id: true }, }); for (const u of usersNoOrg) { await prisma.user.update({ where: { id: u.id }, data: { activeOrgId: ECHO_LIFE_ORG_ID }, }); } console.log(` Set activeOrgId for ${usersNoOrg.length} users\n`); // 9. Populate People from card data console.log("9. Creating People from card data..."); const completeCards = await prisma.responseCard.findMany({ where: { organizationId: ECHO_LIFE_ORG_ID, ocrStatus: "complete", personId: null, }, select: { id: true, firstName: true, lastName: true, name: true, email: true, cellPhone: true, fieldData: true, }, orderBy: { createdAt: "asc" }, }); let peopleCreated = 0; let peopleLinked = 0; for (const card of completeCards) { const fName = card.firstName || card.name?.split(" ")[0] || ""; const lName = card.lastName || card.name?.split(" ").slice(1).join(" ") || ""; if (!fName && !lName) continue; const email = card.email?.toLowerCase() || null; // Try match by email first let person = null; if (email) { person = await prisma.person.findFirst({ where: { organizationId: ECHO_LIFE_ORG_ID, email, mergedIntoId: null }, }); } // Try match by name if (!person && fName && lName) { person = await prisma.person.findFirst({ where: { organizationId: ECHO_LIFE_ORG_ID, firstName: { equals: fName, mode: "insensitive" }, lastName: { equals: lName, mode: "insensitive" }, mergedIntoId: null, }, }); } if (person) { await prisma.responseCard.update({ where: { id: card.id }, data: { personId: person.id }, }); peopleLinked++; } else { const newPerson = await prisma.person.create({ data: { organizationId: ECHO_LIFE_ORG_ID, firstName: fName || "Unknown", lastName: lName || "", email, cellPhone: card.cellPhone || null, fieldData: (card.fieldData as object) || null, }, }); await prisma.responseCard.update({ where: { id: card.id }, data: { personId: newPerson.id }, }); peopleCreated++; } } console.log(` Created ${peopleCreated} people, linked ${peopleLinked} cards to existing people\n`); // 10. Summary console.log("=== Migration Complete ==="); const finalCards = await prisma.responseCard.count({ where: { organizationId: ECHO_LIFE_ORG_ID } }); const finalPeople = await prisma.person.count({ where: { organizationId: ECHO_LIFE_ORG_ID } }); const finalMembers = await prisma.orgMember.count({ where: { organizationId: ECHO_LIFE_ORG_ID } }); const finalDays = await prisma.collectionDay.count({ where: { locationId } }); console.log(`Cards: ${finalCards}`); console.log(`People: ${finalPeople}`); console.log(`Members: ${finalMembers}`); console.log(`Services: ${finalDays}`); console.log(`Template: ${templateId}`); await prisma.$disconnect(); pool.end(); } main().catch((e) => { console.error(e); process.exit(1); });