diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..2cf622e --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,383 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" +} + +model Church { + id String @id @default(uuid()) + name String + address String? + city String? + state String? + zip String? + country String @default("US") + latitude Float + longitude Float + phone String? + website String? + massScheduleUrl String? @map("mass_schedule_url") + email String? + pastorName String? @map("pastor_name") + diocese String? + directions String? + wheelchairAccess Boolean @default(false) @map("wheelchair_access") + masstimesId String? @unique @map("masstimes_id") + source String @default("masstimes") // "osm", "masstimes", "manual" + osmId String? @unique @map("osm_id") // OpenStreetMap node/way/relation ID (e.g. "node/12345") + baiduId String? @unique @map("baidu_id") // Baidu Maps POI uid + hasWebsite Boolean @default(false) @map("has_website") + osmLastSyncedAt DateTime? @map("osm_last_synced_at") // Last time this church was synced from OSM + baiduLastSyncedAt DateTime? @map("baidu_last_synced_at") // Last time this church was synced from Baidu Maps + googlePlaceId String? @unique @map("google_place_id") + orarimesseId String? @unique @map("orarimesse_id") + orarimesseLastSyncedAt DateTime? @map("orarimesse_last_synced_at") + massSchedulesPhId String? @unique @map("mass_schedules_ph_id") + philmassId String? @unique @map("philmass_id") + horariosMisasId String? @unique @map("horarios_misas_id") + mszeInfoId String? @unique @map("msze_info_id") + weekdayMassesId String? @unique @map("weekday_masses_id") + messesInfoId String? @unique @map("messes_info_id") + bohosluzbyId String? @unique @map("bohosluzby_id") + miserendId String? @unique @map("miserend_id") + kerknetId String? @unique @map("kerknet_id") + gottesdienstzeitenId String? @unique @map("gottesdienstzeiten_id") + discovermassId String? @unique @map("discovermass_id") + claimed Boolean @default(false) + claimedAt DateTime? @map("claimed_at") + lastScrapedAt DateTime? @map("last_scraped_at") + scrapeStrategy String @default("generic") @map("scrape_strategy") + cityNormalized String? @map("city_normalized") + lastTransferredAt DateTime? @map("last_transferred_at") // Last time this church was transferred to Neon production + freeSearchedAt DateTime? @map("free_searched_at") // Last time FreeSearch was used to find website + websiteLanguage String? @map("website_language") // ISO 639-1 language code detected from website + websiteType String? @map("website_type") // URL classification: parish, social_media, wiki, directory, umbrella, government, heritage, review, video, maps + reverseGeocodedAt DateTime? @map("reverse_geocoded_at") // When reverse geocoding was attempted + googleSearchedAt DateTime? @map("google_searched_at") // When Google Places enrichment was attempted + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + dioceseId String? @map("diocese_id") + + claimedByUserId String? @map("claimed_by_user_id") + + massSchedules MassSchedule[] + confessionSchedules ConfessionSchedule[] + adorationSchedules AdorationSchedule[] + specialServices SpecialService[] + scraperConfig ChurchScraperConfig? + dioceseObj Diocese? @relation(fields: [dioceseId], references: [id]) + claimedByUser User? @relation(fields: [claimedByUserId], references: [id]) + + @@index([osmId]) + @@index([baiduId]) + @@index([source]) + @@index([hasWebsite]) + @@index([state]) + @@index([city]) + @@index([state, cityNormalized]) + @@index([country]) + @@index([country, cityNormalized]) + @@index([lastTransferredAt]) + @@index([freeSearchedAt]) + @@index([websiteLanguage]) + @@index([websiteType]) + @@index([orarimesseId]) + @@index([massSchedulesPhId]) + @@index([philmassId]) + @@index([horariosMisasId]) + @@index([mszeInfoId]) + @@index([weekdayMassesId]) + @@index([messesInfoId]) + @@index([bohosluzbyId]) + @@index([miserendId]) + @@index([kerknetId]) + @@index([gottesdienstzeitenId]) + @@index([discovermassId]) + @@index([dioceseId]) + @@index([claimedByUserId]) + @@map("churches") +} + +model MassSchedule { + id String @id @default(uuid()) + churchId String @map("church_id") + dayOfWeek Int @map("day_of_week") + time String // Store as HH:MM string + massType String? @map("mass_type") + language String @default("English") + notes String? + isActive Boolean @default(true) @map("is_active") + createdByUserId String? @map("created_by_user_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + church Church @relation(fields: [churchId], references: [id], onDelete: Cascade) + createdBy User? @relation("MassScheduleCreator", fields: [createdByUserId], references: [id]) + + @@index([churchId, dayOfWeek, isActive]) + @@index([churchId, isActive]) + @@map("mass_schedules") +} + +model ConfessionSchedule { + id String @id @default(uuid()) + churchId String @map("church_id") + dayOfWeek Int @map("day_of_week") + startTime String @map("start_time") + endTime String @map("end_time") + notes String? + isActive Boolean @default(true) @map("is_active") + createdByUserId String? @map("created_by_user_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + church Church @relation(fields: [churchId], references: [id], onDelete: Cascade) + createdBy User? @relation("ConfessionScheduleCreator", fields: [createdByUserId], references: [id]) + + @@index([churchId, dayOfWeek, isActive]) + @@index([churchId, isActive]) + @@map("confession_schedules") +} + +model AdorationSchedule { + id String @id @default(uuid()) + churchId String @map("church_id") + dayOfWeek Int @map("day_of_week") + startTime String @map("start_time") + endTime String @map("end_time") + isPerpetual Boolean @default(false) @map("is_perpetual") + notes String? + isActive Boolean @default(true) @map("is_active") + createdByUserId String? @map("created_by_user_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + church Church @relation(fields: [churchId], references: [id], onDelete: Cascade) + createdBy User? @relation("AdorationScheduleCreator", fields: [createdByUserId], references: [id]) + + @@index([churchId, dayOfWeek, isActive]) + @@index([churchId, isActive]) + @@map("adoration_schedules") +} + +model LiturgicalDay { + id String @id @default(uuid()) + date DateTime @unique @db.Date + season String // "Ordinary Time", "Advent", "Lent", etc. + seasonWeek String? @map("season_week") // "23rd Week" etc. + liturgicalColor String? @map("liturgical_color") // "green", "violet", "white", "red" + feastName String? @map("feast_name") // Solemnity/feast name if applicable + feastRank String? @map("feast_rank") // "Solemnity", "Feast", "Memorial", "Optional Memorial" + firstReading String? @map("first_reading") // Citation: "Wisdom 9:13-18b" + psalm String? // "Psalm 90:3-4, 5-6, 12-13, 14 and 17" + secondReading String? @map("second_reading") // Citation (Sundays/Solemnities only) + gospel String? // Citation: "Luke 14:25-33" + firstReadingText String? @map("first_reading_text") // Full verse text from Bible API + psalmText String? @map("psalm_text") // Full verse text from Bible API + secondReadingText String? @map("second_reading_text") // Full verse text from Bible API + gospelText String? @map("gospel_text") // Full verse text from Bible API + usccbUrl String? @map("usccb_url") // Link to full text on USCCB + versesSource String? @map("verses_source") // 'bible_api' | 'youversion' | 'none' + versesCachedAt DateTime? @map("verses_cached_at") // When verses were last fetched + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([date]) + @@index([season]) + @@index([versesCachedAt]) + @@map("liturgical_days") +} + +model ChurchScraperConfig { + id String @id @default(uuid()) + churchId String @unique @map("church_id") + strategyName String @map("strategy_name") + lastSuccessAt DateTime? @map("last_success_at") + lastFailureAt DateTime? @map("last_failure_at") + failureCount Int @default(0) @map("failure_count") + rawHtml String? @map("raw_html") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + church Church @relation(fields: [churchId], references: [id], onDelete: Cascade) + + @@map("church_scraper_config") +} + +model User { + id String @id @default(uuid()) + name String + email String @unique + emailVerified DateTime? @map("email_verified") + hashedPassword String? @map("hashed_password") + image String? + sortPreference String @default("time") @map("sort_preference") + languagePreference String? @map("language_preference") + lastLatitude Float? @map("last_latitude") + lastLongitude Float? @map("last_longitude") + lastLocationName String? @map("last_location_name") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + suspended Boolean @default(false) + suspendedAt DateTime? @map("suspended_at") + + accounts Account[] + sessions Session[] + claimedChurches Church[] + massSchedules MassSchedule[] @relation("MassScheduleCreator") + confessionSchedules ConfessionSchedule[] @relation("ConfessionScheduleCreator") + adorationSchedules AdorationSchedule[] @relation("AdorationScheduleCreator") + specialServices SpecialService[] @relation("SpecialServiceCreator") + + @@map("users") +} + +model Account { + id String @id @default(uuid()) + userId String @map("user_id") + type String + provider String + providerAccountId String @map("provider_account_id") + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@map("accounts") +} + +model Session { + id String @id @default(uuid()) + sessionToken String @unique @map("session_token") + userId String @map("user_id") + expires DateTime + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("sessions") +} + +model ContactMessage { + id String @id @default(uuid()) + name String? + email String? + message String? + wantsToDonate Boolean @default(false) @map("wants_to_donate") + showInDonorLog Boolean @default(false) @map("show_in_donor_log") + read Boolean @default(false) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([read]) + @@index([createdAt]) + @@map("contact_messages") +} + +model Donation { + id String @id @default(uuid()) + displayName String? @map("display_name") + message String? + approved Boolean @default(false) + approvedAt DateTime? @map("approved_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([approved]) + @@index([createdAt]) + @@map("donations") +} + +model Diocese { + id String @id @default(uuid()) + name String + country String + language String + website String? + directoryUrl String? @map("directory_url") + scrapeConfig Json? @map("scrape_config") + churchCount Int @default(0) @map("church_count") + lastScrapedAt DateTime? @map("last_scraped_at") + lastSuccessAt DateTime? @map("last_success_at") + lastFailureAt DateTime? @map("last_failure_at") + failureCount Int @default(0) @map("failure_count") + active Boolean @default(true) + notes String? + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + churches Church[] + + @@unique([name, country]) + @@index([country]) + @@index([active]) + @@index([lastScrapedAt]) + @@map("dioceses") +} + +model BackgroundJob { + id String @id @default(uuid()) + type String // "scraper", "google-enrichment", "freesearch-enrichment" + language String? // "english", "french", "generic" (null for enrichment jobs) + status String @default("pending") // "pending", "running", "completed", "failed", "stopping" + totalItems Int @default(0) @map("total_items") + processed Int @default(0) + succeeded Int @default(0) + failed Int @default(0) + itemsFound Int @default(0) @map("items_found") + error String? + config Json? + startedAt DateTime? @map("started_at") + completedAt DateTime? @map("completed_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([type]) + @@index([status]) + @@map("background_jobs") +} + +model EmailVerification { + id String @id @default(uuid()) + email String + code String // 6-digit code + expiresAt DateTime @map("expires_at") + attempts Int @default(0) // rate limit verification attempts + createdAt DateTime @default(now()) @map("created_at") + + @@index([email]) + @@index([expiresAt]) + @@map("email_verifications") +} + +model SpecialService { + id String @id @default(uuid()) + churchId String @map("church_id") + serviceType String @map("service_type") // "stations_of_the_cross", "midnight_mass", "easter_vigil", etc. + customName String? @map("custom_name") // only when serviceType="other" + dayOfWeek Int? @map("day_of_week") // 0-6, null for date-specific + date DateTime? @db.Date // specific date for non-recurring + time String // HH:MM + endTime String? @map("end_time") // HH:MM, optional + recurrence String @default("weekly") // "weekly", "biweekly", "monthly", "annually", "once" + notes String? + isActive Boolean @default(true) @map("is_active") + createdByUserId String? @map("created_by_user_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + church Church @relation(fields: [churchId], references: [id], onDelete: Cascade) + createdBy User? @relation("SpecialServiceCreator", fields: [createdByUserId], references: [id]) + + @@index([churchId, isActive]) + @@index([churchId, serviceType]) + @@map("special_services") +}