Simplify routing to award assignment, seed all CSV entries, fix category mapping
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m3s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m3s
- Remove RoutingRule model and routing engine (replaced by direct award assignment) - Simplify RoutingMode enum: PARALLEL/POST_MAIN → SHARED, keep EXCLUSIVE - Remove routing router, routing-rules-editor, and related tests - Update pipeline, award, and notification code to remove routing references - Seed: include all CSV entries (no filtering/dedup), AI screening handles duplicates - Seed: fix non-breaking space (U+00A0) bug in category/issue mapping - Stage filtering: add duplicate detection that flags projects for admin review Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
118
prisma/seed.ts
118
prisma/seed.ts
@@ -50,9 +50,14 @@ const issueMap: Record<string, OceanIssue> = {
|
||||
'Other': OceanIssue.OTHER,
|
||||
}
|
||||
|
||||
function normalizeSpaces(s: string): string {
|
||||
// Replace non-breaking spaces (U+00A0) and other whitespace variants with regular spaces
|
||||
return s.replace(/\u00A0/g, ' ')
|
||||
}
|
||||
|
||||
function mapCategory(raw: string | undefined): CompetitionCategory | null {
|
||||
if (!raw) return null
|
||||
const trimmed = raw.trim()
|
||||
const trimmed = normalizeSpaces(raw.trim())
|
||||
for (const [prefix, value] of Object.entries(categoryMap)) {
|
||||
if (trimmed.startsWith(prefix)) return value
|
||||
}
|
||||
@@ -61,7 +66,7 @@ function mapCategory(raw: string | undefined): CompetitionCategory | null {
|
||||
|
||||
function mapIssue(raw: string | undefined): OceanIssue | null {
|
||||
if (!raw) return null
|
||||
const trimmed = raw.trim()
|
||||
const trimmed = normalizeSpaces(raw.trim())
|
||||
for (const [prefix, value] of Object.entries(issueMap)) {
|
||||
if (trimmed.startsWith(prefix)) return value
|
||||
}
|
||||
@@ -76,17 +81,11 @@ function parseFoundedDate(raw: string | undefined): Date | null {
|
||||
return isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
function isValidEntry(row: Record<string, string>): boolean {
|
||||
const status = (row['Application status'] || '').trim().toLowerCase()
|
||||
if (status === 'ignore' || status === 'doublon') return false
|
||||
|
||||
function isEmptyRow(row: Record<string, string>): boolean {
|
||||
const name = (row['Full name'] || '').trim()
|
||||
if (name.length <= 2) return false // skip test entries
|
||||
|
||||
const email = (row['E-mail'] || '').trim()
|
||||
if (!email || !email.includes('@')) return false
|
||||
|
||||
return true
|
||||
const project = (row["Project's name"] || '').trim()
|
||||
return !name && !email && !project
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -476,7 +475,7 @@ async function main() {
|
||||
name: 'Ocean Innovation Award',
|
||||
slug: 'innovation-award',
|
||||
kind: TrackKind.AWARD,
|
||||
routingMode: RoutingMode.PARALLEL,
|
||||
routingMode: RoutingMode.SHARED,
|
||||
decisionMode: DecisionMode.JURY_VOTE,
|
||||
sortOrder: 1,
|
||||
settingsJson: { description: 'Award for most innovative ocean technology' },
|
||||
@@ -506,16 +505,16 @@ async function main() {
|
||||
name: "People's Choice",
|
||||
slug: 'peoples-choice',
|
||||
kind: TrackKind.SHOWCASE,
|
||||
routingMode: RoutingMode.POST_MAIN,
|
||||
routingMode: RoutingMode.SHARED,
|
||||
sortOrder: 3,
|
||||
settingsJson: { description: 'Public audience voting for fan favorite' },
|
||||
},
|
||||
})
|
||||
|
||||
console.log(` ✓ Main Competition (MAIN)`)
|
||||
console.log(` ✓ Ocean Innovation Award (AWARD, PARALLEL)`)
|
||||
console.log(` ✓ Ocean Innovation Award (AWARD, SHARED)`)
|
||||
console.log(` ✓ Ocean Impact Award (AWARD, EXCLUSIVE)`)
|
||||
console.log(` ✓ People's Choice (SHOWCASE, POST_MAIN)`)
|
||||
console.log(` ✓ People's Choice (SHOWCASE, SHARED)`)
|
||||
|
||||
// ==========================================================================
|
||||
// 9. Stages
|
||||
@@ -814,21 +813,9 @@ async function main() {
|
||||
|
||||
console.log(` Raw CSV rows: ${records.length}`)
|
||||
|
||||
// Filter and deduplicate
|
||||
const seenEmails = new Set<string>()
|
||||
const validRecords: Record<string, string>[] = []
|
||||
|
||||
for (const row of records) {
|
||||
if (!isValidEntry(row)) continue
|
||||
|
||||
const email = (row['E-mail'] || '').trim().toLowerCase()
|
||||
if (seenEmails.has(email)) continue
|
||||
|
||||
seenEmails.add(email)
|
||||
validRecords.push(row)
|
||||
}
|
||||
|
||||
console.log(` Valid entries after filtering: ${validRecords.length}`)
|
||||
// Skip only completely empty rows (no name, no email, no project)
|
||||
const validRecords = records.filter((row: Record<string, string>) => !isEmptyRow(row))
|
||||
console.log(` Entries to seed: ${validRecords.length}`)
|
||||
|
||||
// Create applicant users and projects
|
||||
console.log('\n🚀 Creating applicant users and projects...')
|
||||
@@ -836,7 +823,9 @@ async function main() {
|
||||
const intakeStage = mainStages[0] // INTAKE - CLOSED
|
||||
const filterStage = mainStages[1] // FILTER - ACTIVE
|
||||
|
||||
for (const row of validRecords) {
|
||||
let skippedNoEmail = 0
|
||||
for (let rowIdx = 0; rowIdx < validRecords.length; rowIdx++) {
|
||||
const row = validRecords[rowIdx]
|
||||
const email = (row['E-mail'] || '').trim().toLowerCase()
|
||||
const name = (row['Full name'] || '').trim()
|
||||
const phone = (row['Téléphone'] || '').trim() || null
|
||||
@@ -855,7 +844,14 @@ async function main() {
|
||||
const phase2Url = (row['PHASE 2 - Submission'] || '').trim() || null
|
||||
const foundedAt = parseFoundedDate(row['Date of creation'])
|
||||
|
||||
// Create or get applicant user
|
||||
// Skip rows with no usable email (can't create user without one)
|
||||
if (!email || !email.includes('@')) {
|
||||
skippedNoEmail++
|
||||
console.log(` ⚠ Row ${rowIdx + 2}: skipped (no valid email)`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Create or get applicant user (upsert handles duplicate emails)
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email },
|
||||
update: {
|
||||
@@ -864,7 +860,7 @@ async function main() {
|
||||
},
|
||||
create: {
|
||||
email,
|
||||
name,
|
||||
name: name || `Applicant ${rowIdx + 1}`,
|
||||
role: UserRole.APPLICANT,
|
||||
status: UserStatus.NONE,
|
||||
phoneNumber: phone,
|
||||
@@ -930,6 +926,9 @@ async function main() {
|
||||
}
|
||||
|
||||
console.log(` ✓ Created ${projectCount} projects with stage states`)
|
||||
if (skippedNoEmail > 0) {
|
||||
console.log(` ⚠ Skipped ${skippedNoEmail} rows with no valid email`)
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
@@ -998,58 +997,7 @@ async function main() {
|
||||
console.log(' ✓ Ocean Impact Award → impact-award track')
|
||||
|
||||
// ==========================================================================
|
||||
// 14. Routing Rules
|
||||
// ==========================================================================
|
||||
console.log('\n🔀 Creating routing rules...')
|
||||
|
||||
const existingTechRule = await prisma.routingRule.findFirst({
|
||||
where: { pipelineId: pipeline.id, name: 'Route Tech Innovation to Innovation Award' },
|
||||
})
|
||||
if (!existingTechRule) {
|
||||
await prisma.routingRule.create({
|
||||
data: {
|
||||
pipelineId: pipeline.id,
|
||||
name: 'Route Tech Innovation to Innovation Award',
|
||||
scope: 'global',
|
||||
destinationTrackId: innovationTrack.id,
|
||||
predicateJson: {
|
||||
field: 'oceanIssue',
|
||||
operator: 'eq',
|
||||
value: 'TECHNOLOGY_INNOVATION',
|
||||
},
|
||||
priority: 10,
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const existingImpactRule = await prisma.routingRule.findFirst({
|
||||
where: { pipelineId: pipeline.id, name: 'Route Community Impact to Impact Award' },
|
||||
})
|
||||
if (!existingImpactRule) {
|
||||
await prisma.routingRule.create({
|
||||
data: {
|
||||
pipelineId: pipeline.id,
|
||||
name: 'Route Community Impact to Impact Award',
|
||||
scope: 'global',
|
||||
destinationTrackId: impactTrack.id,
|
||||
predicateJson: {
|
||||
or: [
|
||||
{ field: 'oceanIssue', operator: 'eq', value: 'COMMUNITY_CAPACITY' },
|
||||
{ field: 'oceanIssue', operator: 'eq', value: 'HABITAT_RESTORATION' },
|
||||
],
|
||||
},
|
||||
priority: 5,
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
console.log(' ✓ Tech Innovation → Innovation Award (PARALLEL)')
|
||||
console.log(' ✓ Community Impact → Impact Award (EXCLUSIVE)')
|
||||
|
||||
// ==========================================================================
|
||||
// 15. Notification Email Settings
|
||||
// 14. Notification Email Settings
|
||||
// ==========================================================================
|
||||
console.log('\n🔔 Creating notification email settings...')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user