MOPC-App/docs/claude-architecture-redesign/04-round-intake.md

1540 lines
75 KiB
Markdown
Raw Normal View History

# Round Type: INTAKE — Application Window
## 1. Overview
The **INTAKE round** is the first phase of any MOPC competition. It represents the application submission window where teams apply to participate by submitting their ocean conservation projects along with required documentation.
### Purpose
- Collect project applications from teams worldwide
- Capture essential project information (title, description, team, ocean issue addressed)
- Receive required documentation (executive summaries, business plans, videos)
- Support draft/save-and-continue workflow for incomplete applications
- Enforce submission deadlines with configurable late submission policies
- Enable both authenticated (user login) and public (anonymous) application flows
### Position in Competition Flow
```
Competition 2026
└─ Round 1: "Application Window" ────── [INTAKE]
└─ Round 2: "AI Screening" ───────── [FILTERING]
└─ Round 3: "Jury Evaluation 1" ── [EVALUATION]
└─ Round 4: "Semi-finalist Docs" ── [SUBMISSION]
└─ ...
```
The intake round creates the initial pool of projects that flow through subsequent rounds. All projects begin here.
---
## 2. Current System (Pipeline/Track/Stage)
### How Intake Works Today
In the current architecture, intake is implemented as a **Stage** with `StageType: INTAKE` inside the MAIN track.
```typescript
// Current structure
Pipeline: "MOPC 2026"
└─ Track: "Main Competition" (kind: MAIN)
└─ Stage: "Intake" (stageType: INTAKE)
├─ windowOpenAt: 2026-01-15T00:00:00Z
├─ windowCloseAt: 2026-03-01T23:59:59Z
├─ configJson: {
│ submissionWindowEnabled: true,
│ lateSubmissionPolicy: "flag",
│ lateGraceHours: 24,
│ fileRequirements: [...]
│ }
└─ FileRequirement records (linked to stageId)
```
### Current Config Fields (src/lib/pipeline-defaults.ts)
```typescript
type IntakeConfig = {
submissionWindowEnabled: boolean // Whether submission is open
lateSubmissionPolicy: 'reject' | 'flag' | 'accept'
lateGraceHours: number // Hours after close for late submissions
fileRequirements: FileRequirementConfig[]
}
type FileRequirementConfig = {
name: string // "Executive Summary"
description?: string // Help text
acceptedMimeTypes: string[] // ["application/pdf"]
maxSizeMB?: number // 50
isRequired: boolean // true
}
```
### Current Applicant Flow
1. **Access**: Applicant visits `/applicant/pipeline/{slug}` or receives invite link
2. **Auth**: User logs in (email magic link or password) or proceeds as guest
3. **Form**: Fills out project form (title, description, category, ocean issue, team info)
4. **Files**: Uploads required files (exec summary, business plan, video)
5. **Draft**: Can save as draft and return later
6. **Submit**: Final submission creates Project record with status "SUBMITTED"
7. **Email**: Confirmation email sent if configured
### Current Admin Experience
Admins configure the intake stage via the pipeline wizard:
- Set open/close dates
- Define file requirements (name, mime types, size limits, required/optional)
- Choose late submission policy (reject, flag, accept)
- Set grace period for late submissions
### Current Database Schema
```prisma
model Stage {
id String @id
trackId String
stageType StageType // INTAKE
name String
slug String
status StageStatus
configJson Json? // IntakeConfig stored here
windowOpenAt DateTime?
windowCloseAt DateTime?
...
}
model FileRequirement {
id String @id
stageId String // Links to intake stage
name String
acceptedMimeTypes String[]
maxSizeMB Int?
isRequired Boolean
sortOrder Int
...
}
model ProjectFile {
id String @id
projectId String
roundId String? // Legacy field
requirementId String? // FK to FileRequirement
fileType FileType
fileName String
mimeType String
size Int
bucket String
objectKey String
isLate Boolean @default(false)
version Int @default(1)
...
}
model Project {
id String @id
programId String
roundId String? // Legacy — which round project was submitted for
status ProjectStatus @default(SUBMITTED)
title String
teamName String?
description String?
competitionCategory CompetitionCategory? // STARTUP | BUSINESS_CONCEPT
oceanIssue OceanIssue?
country String?
wantsMentorship Boolean @default(false)
isDraft Boolean @default(false)
draftDataJson Json? // Form data for drafts
draftExpiresAt DateTime?
submissionSource SubmissionSource @default(MANUAL)
submittedByEmail String?
submittedAt DateTime?
submittedByUserId String?
...
}
```
### Current Limitations
| Issue | Impact |
|-------|--------|
| **Single submission window** | Can't require new docs from semi-finalists |
| **No form builder** | All fields hardcoded in application code |
| **No category quotas at intake** | Can't limit "first 50 startups, first 50 concepts" |
| **Generic configJson** | Unclear what fields exist for intake stages |
| **File requirements per stage** | Awkward: "intake stage" is the only stage with file requirements |
| **No dynamic forms** | Can't add custom questions per competition year |
| **No public form branding** | External applicants see generic MOPC form |
---
## 3. Redesigned Intake Round
### New Round Structure
```typescript
// Redesigned
Competition: "MOPC 2026"
└─ Round 1: "Application Window" (roundType: INTAKE)
├─ competitionId: competition-2026
├─ name: "Application Window"
├─ slug: "application-window"
├─ roundType: INTAKE
├─ status: ROUND_ACTIVE
├─ sortOrder: 0
├─ windowOpenAt: 2026-01-15T00:00:00Z
├─ windowCloseAt: 2026-03-01T23:59:59Z
├─ submissionWindowId: "sw-1" // NEW: Links to SubmissionWindow
├─ configJson: IntakeConfig { // NEW: Typed, validated config
│ applicationFormId: "form-2026",
│ deadlinePolicy: "GRACE",
│ gracePeriodMinutes: 180,
│ allowDraftSubmissions: true,
│ requireTeamProfile: true,
│ maxTeamSize: 5,
│ minTeamSize: 1,
│ autoConfirmReceipt: true,
│ publicFormEnabled: true,
│ categoryQuotas: { STARTUP: 100, BUSINESS_CONCEPT: 100 }
│ }
└─ SubmissionWindow: "Round 1 Docs"
├─ id: "sw-1"
├─ competitionId: competition-2026
├─ name: "Round 1 Application Docs"
├─ slug: "round-1-docs"
├─ roundNumber: 1
├─ windowOpenAt: 2026-01-15T00:00:00Z
├─ windowCloseAt: 2026-03-01T23:59:59Z
├─ deadlinePolicy: GRACE
├─ graceHours: 3
└─ FileRequirements: [
├─ "Executive Summary" (PDF, required)
├─ "Business Plan" (PDF, required)
└─ "Video Pitch" (video/*, optional)
]
```
### IntakeConfig Type (Zod-validated)
```typescript
type IntakeConfig = {
// Application Form
applicationFormId: string // Links to ApplicationForm template (future)
// Submission Window (linked via Round.submissionWindowId)
submissionWindowId: string // Which SubmissionWindow to use
// Deadline Behavior
deadlinePolicy: DeadlinePolicy // HARD | FLAG | GRACE
gracePeriodMinutes: number // For GRACE policy (e.g., 180 = 3 hours)
// Draft System
allowDraftSubmissions: boolean // Save-and-continue enabled
draftExpiryDays: number // Auto-delete abandoned drafts after N days
// Team Profile
requireTeamProfile: boolean // Require team member info
maxTeamSize: number // Max team members (including lead)
minTeamSize: number // Min team members (default: 1)
// Notifications
autoConfirmReceipt: boolean // Email confirmation on submission
reminderEmailSchedule: number[] // Days before deadline: [7, 3, 1]
// Public Access
publicFormEnabled: boolean // Allow external application link
publicFormSlug?: string // Custom slug for public URL
// Category Quotas (STARTUP vs BUSINESS_CONCEPT)
categoryQuotasEnabled: boolean
categoryQuotas?: {
STARTUP: number // Max startups accepted
BUSINESS_CONCEPT: number // Max concepts accepted
}
quotaOverflowPolicy?: 'reject' | 'waitlist' // What happens when quota full
// Custom Fields (future: dynamic form builder)
customFields?: CustomFieldDef[] // Additional form fields
}
enum DeadlinePolicy {
HARD // Submissions blocked after deadline, no exceptions
FLAG // Submissions accepted but flagged as late
GRACE // Accept for N minutes after deadline with warning
}
type CustomFieldDef = {
id: string
label: string
type: 'text' | 'textarea' | 'select' | 'multiselect' | 'date' | 'number'
required: boolean
options?: string[] // For select/multiselect
validation?: {
min?: number
max?: number
regex?: string
}
}
```
### Zod Schema for Validation
```typescript
import { z } from 'zod'
export const intakeConfigSchema = z.object({
applicationFormId: z.string().cuid(),
submissionWindowId: z.string().cuid(),
deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']),
gracePeriodMinutes: z.number().int().min(0).max(1440), // Max 24 hours
allowDraftSubmissions: z.boolean().default(true),
draftExpiryDays: z.number().int().min(1).default(30),
requireTeamProfile: z.boolean().default(true),
maxTeamSize: z.number().int().min(1).max(20).default(5),
minTeamSize: z.number().int().min(1).default(1),
autoConfirmReceipt: z.boolean().default(true),
reminderEmailSchedule: z.array(z.number().int()).default([7, 3, 1]),
publicFormEnabled: z.boolean().default(false),
publicFormSlug: z.string().optional(),
categoryQuotasEnabled: z.boolean().default(false),
categoryQuotas: z.object({
STARTUP: z.number().int().min(0),
BUSINESS_CONCEPT: z.number().int().min(0),
}).optional(),
quotaOverflowPolicy: z.enum(['reject', 'waitlist']).optional(),
customFields: z.array(z.object({
id: z.string(),
label: z.string().min(1).max(200),
type: z.enum(['text', 'textarea', 'select', 'multiselect', 'date', 'number']),
required: z.boolean(),
options: z.array(z.string()).optional(),
validation: z.object({
min: z.number().optional(),
max: z.number().optional(),
regex: z.string().optional(),
}).optional(),
})).optional(),
})
export type IntakeConfig = z.infer<typeof intakeConfigSchema>
```
---
## 4. Application Form System
### ApplicationForm Model (Future Enhancement)
For now, the application form is hardcoded. In the future, a dynamic form builder will replace this.
```prisma
model ApplicationForm {
id String @id @default(cuid())
competitionId String
name String // "MOPC 2026 Application"
description String?
fieldsJson Json @db.JsonB // Array of field definitions
version Int @default(1)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
@@index([competitionId])
@@index([isActive])
}
```
### Standard Form Fields (Hardcoded for MVP)
```typescript
type ApplicationFormData = {
// Project Info
title: string // Required, 1-500 chars
teamName?: string // Optional
description: string // Required, max 5000 chars
competitionCategory: 'STARTUP' | 'BUSINESS_CONCEPT' // Required
oceanIssue: OceanIssue // Required enum
// Location
country: string // Required
geographicZone?: string // "Europe, France"
institution?: string // Required for BUSINESS_CONCEPT
// Founding
foundedAt?: Date // When project/company started
// Mentorship
wantsMentorship: boolean // Default: false
// Referral
referralSource?: string // "LinkedIn", "Email", etc.
// Team Members (if requireTeamProfile: true)
teamMembers?: TeamMemberInput[]
// Custom Fields (future)
customFieldValues?: Record<string, unknown>
}
type TeamMemberInput = {
name: string
email: string
role: 'LEAD' | 'MEMBER' | 'ADVISOR'
title?: string // "CEO", "CTO"
}
```
### Field Validation Rules
| Field | Validation | Error Message |
|-------|-----------|---------------|
| `title` | Required, 1-500 chars | "Project title is required" |
| `description` | Required, max 5000 chars | "Description must be under 5000 characters" |
| `competitionCategory` | Required enum | "Please select Startup or Business Concept" |
| `oceanIssue` | Required enum | "Please select an ocean issue" |
| `country` | Required string | "Country is required" |
| `institution` | Required if category = BUSINESS_CONCEPT | "Institution is required for student projects" |
| `teamMembers[].email` | Valid email format | "Invalid email address" |
| `teamMembers.length` | >= minTeamSize, <= maxTeamSize | "Team must have 1-5 members" |
### Conditional Logic
- **Institution field**: Only shown/required when `competitionCategory = BUSINESS_CONCEPT`
- **Team members section**: Only shown if `requireTeamProfile = true` in config
- **Mentorship checkbox**: Always shown, default unchecked
---
## 5. Submission Window Integration
### SubmissionWindow Model (from 03-data-model.md)
```prisma
model SubmissionWindow {
id String @id @default(cuid())
competitionId String
name String // "Round 1 Application Docs"
slug String // "round-1-docs"
roundNumber Int // 1 (first window), 2 (second window), etc.
sortOrder Int @default(0)
windowOpenAt DateTime?
windowCloseAt DateTime?
deadlinePolicy DeadlinePolicy @default(FLAG)
graceHours Int? // For GRACE policy
lockOnClose Boolean @default(true) // Prevent edits after close
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
fileRequirements SubmissionFileRequirement[]
projectFiles ProjectFile[]
rounds Round[] // INTAKE rounds using this window
visibility RoundSubmissionVisibility[]
@@unique([competitionId, slug])
@@unique([competitionId, roundNumber])
@@index([competitionId])
}
model SubmissionFileRequirement {
id String @id @default(cuid())
submissionWindowId String
name String // "Executive Summary"
description String? @db.Text
acceptedMimeTypes String[] // ["application/pdf"]
maxSizeMB Int?
isRequired Boolean @default(true)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
submissionWindow SubmissionWindow @relation(fields: [submissionWindowId], references: [id], onDelete: Cascade)
files ProjectFile[]
@@index([submissionWindowId])
}
```
### How Intake Round Links to SubmissionWindow
```typescript
// Round creation
const round = await prisma.round.create({
data: {
competitionId: competition.id,
name: "Application Window",
slug: "application-window",
roundType: "INTAKE",
sortOrder: 0,
windowOpenAt: new Date("2026-01-15T00:00:00Z"),
windowCloseAt: new Date("2026-03-01T23:59:59Z"),
submissionWindowId: submissionWindow.id, // Link to window
configJson: {
applicationFormId: "form-2026",
submissionWindowId: submissionWindow.id, // Redundant but explicit
deadlinePolicy: "GRACE",
gracePeriodMinutes: 180,
// ... rest of IntakeConfig
}
}
})
```
### File Requirements for Intake
Admin configures file requirements at the **SubmissionWindow** level, not the round level.
```typescript
// Example: Create file requirements for Round 1 docs
const requirements = await prisma.submissionFileRequirement.createMany({
data: [
{
submissionWindowId: "sw-1",
name: "Executive Summary",
description: "A PDF executive summary of your project (max 2 pages)",
acceptedMimeTypes: ["application/pdf"],
maxSizeMB: 10,
isRequired: true,
sortOrder: 0,
},
{
submissionWindowId: "sw-1",
name: "Business Plan",
description: "Full business plan or project proposal (PDF)",
acceptedMimeTypes: ["application/pdf"],
maxSizeMB: 50,
isRequired: true,
sortOrder: 1,
},
{
submissionWindowId: "sw-1",
name: "Video Pitch",
description: "Optional video pitch (max 5 minutes, MP4 or MOV)",
acceptedMimeTypes: ["video/mp4", "video/quicktime"],
maxSizeMB: 500,
isRequired: false,
sortOrder: 2,
},
]
})
```
### Deadline Enforcement
When an applicant tries to upload a file or submit the form:
```typescript
async function canSubmitToWindow(submissionWindow: SubmissionWindow): Promise<{
canSubmit: boolean
reason?: string
isLate?: boolean
}> {
const now = new Date()
// Not yet open
if (submissionWindow.windowOpenAt && now < submissionWindow.windowOpenAt) {
return {
canSubmit: false,
reason: `Window opens on ${submissionWindow.windowOpenAt.toLocaleDateString()}`
}
}
// Window closed
if (submissionWindow.windowCloseAt && now > submissionWindow.windowCloseAt) {
const { deadlinePolicy, graceHours } = submissionWindow
if (deadlinePolicy === 'HARD') {
return {
canSubmit: false,
reason: "Deadline has passed. Submissions are no longer accepted."
}
}
if (deadlinePolicy === 'GRACE' && graceHours) {
const graceDeadline = new Date(submissionWindow.windowCloseAt.getTime() + graceHours * 60 * 60 * 1000)
if (now > graceDeadline) {
return {
canSubmit: false,
reason: `Grace period ended on ${graceDeadline.toLocaleString()}`
}
}
return {
canSubmit: true,
isLate: true
}
}
if (deadlinePolicy === 'FLAG') {
return {
canSubmit: true,
isLate: true
}
}
}
return { canSubmit: true }
}
```
---
## 6. Draft System
### Auto-Save Behavior
When `allowDraftSubmissions: true`, the form auto-saves every 30 seconds (or on field blur).
```typescript
// Draft auto-save
const saveDraft = async (formData: Partial<ApplicationFormData>) => {
const project = await trpc.applicant.saveDraft.mutate({
programId: programId,
projectId: existingProjectId, // null for new draft
draftData: formData,
})
return project
}
// Draft expiry
const draftExpiresAt = new Date()
draftExpiresAt.setDate(draftExpiresAt.getDate() + config.draftExpiryDays)
await prisma.project.upsert({
where: { id: projectId },
update: {
isDraft: true,
draftDataJson: formData,
draftExpiresAt: draftExpiresAt,
updatedAt: new Date(),
},
create: {
programId,
isDraft: true,
draftDataJson: formData,
draftExpiresAt: draftExpiresAt,
status: 'SUBMITTED', // Will change to DRAFT status in redesign
submittedByUserId: userId,
submissionSource: 'PUBLIC_FORM',
}
})
```
### Resume Draft Flow
1. User returns to application page
2. System checks for existing draft:
```typescript
const draft = await prisma.project.findFirst({
where: {
programId,
submittedByUserId: userId,
isDraft: true,
draftExpiresAt: { gt: new Date() } // Not expired
}
})
```
3. If found, pre-populate form with `draft.draftDataJson`
4. User can continue editing or discard draft
### Validation States
```typescript
type FormValidationState = {
isValid: boolean
canSaveDraft: boolean // Always true
canSubmit: boolean // All required fields filled + files uploaded
errors: Record<string, string>
warnings: Record<string, string>
}
// Example states:
// 1. Empty form → isValid: false, canSaveDraft: true, canSubmit: false
// 2. Partial form → isValid: false, canSaveDraft: true, canSubmit: false
// 3. Complete form → isValid: true, canSaveDraft: true, canSubmit: true
```
---
## 7. Applicant Experience
### Landing Page Flow
```
┌─────────────────────────────────────────────────────────────┐
│ MOPC 2026 Application │
│ ══════════════════════════════════════════════════════ │
│ │
│ Application Window: Jan 15 - Mar 1, 2026 │
│ ⏱️ 23 days remaining │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 🌊 Ocean Conservation Innovation Challenge │ │
│ │ │ │
│ │ We're looking for breakthrough ocean projects │ │
│ │ from startups and student teams worldwide. │ │
│ │ │ │
│ │ 📋 Requirements: │ │
│ │ • Executive Summary (PDF) │ │
│ │ • Business Plan (PDF) │ │
│ │ • Video Pitch (optional) │ │
│ │ │ │
│ │ ⏱️ Application takes ~30 minutes │ │
│ │ │ │
│ │ [ Login to Start ] [ Apply as Guest ] │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Multi-Step Application Form
```
┌─────────────────────────────────────────────────────────────┐
│ Step 1 of 4: Project Information │
│ ●───────○───────○───────○ [Save Draft] [Continue →] │
│ │
│ Project Title * │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Ocean Plastic Recycling Platform │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Team Name │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ PlastiClean Solutions │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Competition Category * │
│ ○ Startup (existing company) │
│ ● Business Concept (student/graduate project) │
│ │
│ Institution * (required for Business Concept) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Stanford University │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Ocean Issue Addressed * │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 🔍 Pollution Reduction [Dropdown ▼] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Description * (max 5000 characters) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ PlastiClean is an AI-powered sorting system... │ │
│ │ │ │
│ │ 247 / 5000 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Country * │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 🌍 United States [Dropdown ▼] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Founding Date │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 2024-06-01 [Date Picker] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ☑ I'm interested in mentorship from industry experts │
│ │
│ [ ← Back ] [ Save Draft ] [ Continue → ] │
└─────────────────────────────────────────────────────────────┘
```
```
┌─────────────────────────────────────────────────────────────┐
│ Step 2 of 4: Team Members │
│ ○───────●───────○───────○ [Save Draft] [Continue →] │
│ │
│ Team Members (1-5 members) * │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 👤 Sarah Johnson (You) │ │
│ │ sarah.johnson@stanford.edu │ │
│ │ Role: Lead | Title: CEO │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 👤 Mark Chen [Remove ✕] │ │
│ │ Name: ┌──────────────────────┐ │ │
│ │ │ Mark Chen │ │ │
│ │ └──────────────────────┘ │ │
│ │ Email: ┌──────────────────────┐ │ │
│ │ │ mark@stanford.edu │ │ │
│ │ └──────────────────────┘ │ │
│ │ Role: ● Member ○ Advisor │ │
│ │ Title: ┌──────────────────────┐ │ │
│ │ │ CTO │ │ │
│ │ └──────────────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ [ + Add Team Member ] │
│ │
Team members will receive an email invite to view │
│ your project status. │
│ │
│ [ ← Back ] [ Save Draft ] [ Continue → ] │
└─────────────────────────────────────────────────────────────┘
```
```
┌─────────────────────────────────────────────────────────────┐
│ Step 3 of 4: Document Upload │
│ ○───────○───────●───────○ [Save Draft] [Continue →] │
│ │
│ Required Documents │
│ │
│ 📄 Executive Summary (PDF, max 10 MB) * │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ✓ PlastiClean_Executive_Summary.pdf │ │
│ │ Uploaded 2 hours ago | 2.3 MB │ │
│ │ [ View ] [ Replace ] [ Delete ] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 📄 Business Plan (PDF, max 50 MB) * │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 📎 Drag & drop or click to upload │ │
│ │ │ │
│ │ Accepted formats: PDF │ │
│ │ Max size: 50 MB │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ Optional Documents │
│ │
│ 🎥 Video Pitch (MP4/MOV, max 500 MB) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ⚡ PlastiClean_Pitch.mp4 │ │
│ │ Uploading... 67% complete │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░ │ │
│ │ [ Cancel ] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ Please upload all required documents before submitting. │
│ │
│ [ ← Back ] [ Save Draft ] [ Continue → ] │
└─────────────────────────────────────────────────────────────┘
```
```
┌─────────────────────────────────────────────────────────────┐
│ Step 4 of 4: Review & Submit │
│ ○───────○───────○───────● [Save Draft] [Submit] │
│ │
│ Review Your Application │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Project Information [Edit Step 1] │ │
│ │ ───────────────────────────────────────────────── │ │
│ │ Title: Ocean Plastic Recycling Platform │ │
│ │ Team: PlastiClean Solutions │ │
│ │ Category: Business Concept │ │
│ │ Institution: Stanford University │ │
│ │ Ocean Issue: Pollution Reduction │ │
│ │ Country: United States │ │
│ │ Founded: June 2024 │ │
│ │ Mentorship: Yes │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Team Members [Edit Step 2] │ │
│ │ ───────────────────────────────────────────────── │ │
│ │ • Sarah Johnson (Lead) - sarah.johnson@stanford.edu│ │
│ │ • Mark Chen (Member) - mark@stanford.edu │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Documents [Edit Step 3] │ │
│ │ ───────────────────────────────────────────────── │ │
│ │ ✓ Executive Summary (2.3 MB) │ │
│ │ ✓ Business Plan (8.7 MB) │ │
│ │ ✓ Video Pitch (124 MB) │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ☑ I confirm that all information is accurate and that I │
│ have the authority to submit this application. │
│ │
│ ☑ I agree to the MOPC Terms & Conditions │
│ │
│ ⏱️ Deadline: March 1, 2026 at 11:59 PM UTC │
│ │
│ [ ← Back ] [ Save Draft ] [ Submit ✓ ] │
└─────────────────────────────────────────────────────────────┘
```
### Post-Submission Confirmation
```
┌─────────────────────────────────────────────────────────────┐
│ ✓ Application Submitted Successfully │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 🎉 Thank You! │ │
│ │ │ │
│ │ Your application has been submitted successfully. │ │
│ │ │ │
│ │ Confirmation #: MOPC-2026-00123 │ │
│ │ Submitted: Feb 15, 2026 at 3:42 PM UTC │ │
│ │ │ │
│ │ ✉️ A confirmation email has been sent to: │ │
│ │ sarah.johnson@stanford.edu │ │
│ │ │ │
│ │ What's Next? │ │
│ │ ─────────────────────────────────────────── │ │
│ │ 1. AI Screening (March 2-5) │ │
│ │ 2. Jury Evaluation (March 10-31) │ │
│ │ 3. Semi-finalist Notification (April 5) │ │
│ │ │ │
│ │ You'll receive email updates at each stage. │ │
│ │ │ │
│ │ [ View My Dashboard ] [ Download Receipt ] │ │
│ │ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Applicant Dashboard (After Submission)
```
┌─────────────────────────────────────────────────────────────┐
│ My Application sarah@stanford.edu│
│ ══════════════════════════════════════════════════════ │
│ │
│ PlastiClean Solutions │
│ Ocean Plastic Recycling Platform │
│ Status: Under Review [🟡 In Progress] │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Application Progress │ │
│ │ │ │
│ │ ✓ Submitted Feb 15, 2026 │ │
│ │ ─────────────────────────────────────────── │ │
│ │ ⏳ AI Screening Expected: Mar 2-5 │ │
│ │ ─────────────────────────────────────────── │ │
│ │ ○ Jury Review Expected: Mar 10-31 │ │
│ │ ─────────────────────────────────────────── │ │
│ │ ○ Decision Expected: Apr 5 │ │
│ │ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Deadline Countdown │ │
│ │ 13 days remaining until March 1, 2026 │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ Quick Actions │
│ [ 📄 View Application ] [ 📎 Documents ] [ 👥 Team ] │
│ │
│ Recent Activity │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ✓ Application submitted Feb 15, 3:42 PM │ │
│ │ ✓ Video pitch uploaded Feb 15, 3:38 PM │ │
│ │ ✓ Business plan uploaded Feb 15, 2:15 PM │ │
│ │ ✓ Executive summary uploaded Feb 15, 1:47 PM │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 8. Admin Experience
### Intake Round Configuration Wizard
```
┌─────────────────────────────────────────────────────────────┐
│ Configure Round: Application Window │
│ ══════════════════════════════════════════════════════ │
│ │
│ Basic Settings │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Round Name * │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Application Window │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Submission Window * │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 🔍 Round 1 Docs [Select Window ▼] │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ [ + Create New Window ] │ │
│ │ │ │
│ │ Open Date * │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 2026-01-15 00:00 UTC [Date Picker] │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Close Date * │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 2026-03-01 23:59 UTC [Date Picker] │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ Deadline Policy │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ○ Hard Deadline │ │
│ │ Block submissions after close time │ │
│ │ │ │
│ │ ○ Flag Late Submissions │ │
│ │ Accept but mark as late │ │
│ │ │ │
│ │ ● Grace Period │ │
│ │ Accept for: ┌────┐ minutes after deadline │ │
│ │ │ 180│ │ │
│ │ └────┘ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ Draft Settings │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ☑ Allow draft submissions (save & continue) │ │
│ │ │ │
│ │ Auto-delete drafts after: ┌────┐ days │ │
│ │ │ 30 │ │ │
│ │ └────┘ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ Team Profile │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ☑ Require team member information │ │
│ │ │ │
│ │ Min team size: ┌───┐ Max team size: ┌───┐ │ │
│ │ │ 1 │ │ 5 │ │ │
│ │ └───┘ └───┘ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ Notifications │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ☑ Send confirmation email on submission │ │
│ │ │ │
│ │ Send deadline reminders: │ │
│ │ ☑ 7 days before ☑ 3 days before ☑ 1 day before│ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ Category Quotas (Optional) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ☐ Enable category quotas │ │
│ │ │ │
│ │ Max Startups: ┌─────┐ │ │
│ │ │ 100 │ │ │
│ │ └─────┘ │ │
│ │ │ │
│ │ Max Business Concepts: ┌─────┐ │ │
│ │ │ 100 │ │ │
│ │ └─────┘ │ │
│ │ │ │
│ │ When quota full: ○ Reject ● Add to waitlist │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ [ Cancel ] [ Save Round ] │
└─────────────────────────────────────────────────────────────┘
```
### Submissions Dashboard
```
┌─────────────────────────────────────────────────────────────┐
│ Round: Application Window Admin Dashboard │
│ ══════════════════════════════════════════════════════ │
│ │
│ Overview │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 123 Total Submissions │ │
│ │ ┌──────────┬──────────┬──────────┬──────────┐ │ │
│ │ │ 68 │ 55 │ 12 │ 11 │ │ │
│ │ │ Startups │ Concepts │ Drafts │ Late │ │ │
│ │ └──────────┴──────────┴──────────┴──────────┘ │ │
│ │ │ │
│ │ Deadline: March 1, 2026 (13 days remaining) │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ Filters: [ All ] [ Startups ] [ Concepts ] [ Late ] [ Drafts ]│
│ Search: ┌─────────────────────────────────────┐ [Export ↓] │
│ │ 🔍 Search by project or team... │ │
│ └─────────────────────────────────────┘ │
│ │
│ Recent Submissions │
│ ┌────────────────────────────────────────────────────┐ │
│ │ PlastiClean Solutions │ │
│ │ Ocean Plastic Recycling Platform │ │
│ │ Business Concept | United States | Feb 15, 3:42 PM │ │
│ │ [ View ] [ Edit ] [ Override Deadline ] │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ AquaTech Innovations │ │
│ │ Sustainable Aquaculture Monitoring │ │
│ │ Startup | Norway | Feb 15, 2:18 PM │ │
│ │ [ View ] [ Edit ] [ Override Deadline ] │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ OceanSense Labs 🔴 LATE │ │
│ │ AI-Powered Ocean Pollution Detection │ │
│ │ Startup | Singapore | Mar 2, 1:15 AM (+3 hours) │ │
│ │ [ View ] [ Edit ] [ Override Deadline ] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ [ Previous ] Page 1 of 7 [ Next ] │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Override Deadline Modal
```
┌─────────────────────────────────────────────────────────────┐
│ Override Deadline: PlastiClean Solutions │
│ ══════════════════════════════════════════════════════ │
│ │
│ Current Status: Submitted on time │
│ Original Deadline: March 1, 2026 11:59 PM UTC │
│ │
│ Extend submission window for this applicant: │
│ │
│ New Deadline │
│ ┌──────────────────────────────────────────────┐ │
│ │ 2026-03-08 23:59 UTC [Date Picker] │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ Reason for Override * │
│ ┌──────────────────────────────────────────────┐ │
│ │ Technical issue during original submission │ │
│ │ │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ⚠️ This will create a GracePeriod record and allow the │
│ applicant to edit their submission until the new deadline│
│ │
│ [ Cancel ] [ Grant Extension ] │
└─────────────────────────────────────────────────────────────┘
```
---
## 9. Deadline Behavior
### Deadline Policy Comparison
| Policy | Before Deadline | After Deadline | Grace Period | Flagged |
|--------|----------------|----------------|--------------|---------|
| **HARD** | ✅ Accept | ❌ Block | N/A | N/A |
| **FLAG** | ✅ Accept | ✅ Accept | N/A | ✅ Yes |
| **GRACE** | ✅ Accept | ✅ Accept (for N min) | ✅ Yes | ✅ Yes (after grace) |
### HARD Policy Behavior
**Configuration:**
```typescript
{
deadlinePolicy: "HARD",
gracePeriodMinutes: null // Ignored
}
```
**User Experience:**
- **Before deadline**: Form is fully functional, all uploads allowed
- **At deadline**: Form locks immediately at `windowCloseAt`
- **After deadline**: Form displays:
```
❌ Deadline Passed
The application deadline was March 1, 2026 at 11:59 PM UTC.
Submissions are no longer accepted.
Contact admin@monaco-opc.com for assistance.
```
**Admin Override:**
- Admin can create a `GracePeriod` record for specific applicant
- This extends their personal deadline (doesn't affect global deadline)
### FLAG Policy Behavior
**Configuration:**
```typescript
{
deadlinePolicy: "FLAG",
gracePeriodMinutes: null // Ignored
}
```
**User Experience:**
- **Before deadline**: Normal submission
- **After deadline**: Warning banner shown:
```
⚠️ Late Submission
The deadline was March 1, 2026. Your submission will be marked as late.
You can still submit, but late submissions may be deprioritized.
```
- Submission still works, but `ProjectFile.isLate` set to `true`
**Database Effect:**
```typescript
await prisma.projectFile.create({
data: {
projectId,
submissionWindowId,
requirementId,
fileName,
mimeType,
size,
bucket,
objectKey,
isLate: true, // Flagged
// ...
}
})
```
### GRACE Policy Behavior
**Configuration:**
```typescript
{
deadlinePolicy: "GRACE",
gracePeriodMinutes: 180 // 3 hours
}
```
**User Experience:**
- **Before deadline**: Normal submission
- **0-3 hours after deadline**: Warning banner:
```
⏱️ Grace Period Active
Deadline: March 1, 2026 11:59 PM UTC (passed)
Grace period ends: March 2, 2026 2:59 AM UTC (1 hour 23 minutes remaining)
Your submission will be marked as late. Please submit as soon as possible.
```
- **After grace period**: Hard block (same as HARD policy)
**Grace Period Calculation:**
```typescript
const graceDeadline = new Date(windowCloseAt.getTime() + gracePeriodMinutes * 60 * 1000)
if (now > windowCloseAt && now <= graceDeadline) {
// In grace period
return {
canSubmit: true,
isLate: true,
graceEndsAt: graceDeadline,
remainingMinutes: Math.floor((graceDeadline - now) / 60000)
}
}
```
---
## 10. Category Quotas
### How Category Quotas Work
When `categoryQuotasEnabled: true`, the system tracks submissions per category and enforces limits.
**Configuration:**
```typescript
{
categoryQuotasEnabled: true,
categoryQuotas: {
STARTUP: 100,
BUSINESS_CONCEPT: 100
},
quotaOverflowPolicy: "reject" // or "waitlist"
}
```
### Quota Enforcement Flow
```typescript
async function canSubmitInCategory(
competitionId: string,
category: 'STARTUP' | 'BUSINESS_CONCEPT',
config: IntakeConfig
): Promise<{ canSubmit: boolean; reason?: string }> {
if (!config.categoryQuotasEnabled || !config.categoryQuotas) {
return { canSubmit: true }
}
const quota = config.categoryQuotas[category]
if (!quota) {
return { canSubmit: true }
}
const submittedCount = await prisma.project.count({
where: {
competitionId,
competitionCategory: category,
isDraft: false, // Don't count drafts
}
})
if (submittedCount >= quota) {
if (config.quotaOverflowPolicy === 'waitlist') {
return {
canSubmit: true,
reason: `Quota full (${submittedCount}/${quota}). You're on the waitlist.`
}
} else {
return {
canSubmit: false,
reason: `Quota full (${submittedCount}/${quota}). No more ${category} applications accepted.`
}
}
}
return { canSubmit: true }
}
```
### Quota Dashboard for Admins
```
┌─────────────────────────────────────────────────────────────┐
│ Category Quota Status │
│ ══════════════════════════════════════════════════════ │
│ │
│ Startups │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 68 / 100 submissions │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░ 68% │ │
│ │ │ │
│ │ 32 slots remaining │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ Business Concepts │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 55 / 100 submissions │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░ 55% │ │
│ │ │ │
│ │ 45 slots remaining │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ Waitlist (if quota full) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Startups: 0 on waitlist │ │
│ │ Concepts: 0 on waitlist │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Overflow Handling
**Reject Policy:**
- Form shows error: "Category quota reached. No more startups/concepts accepted."
- User cannot submit
- Draft is saved but cannot be finalized
**Waitlist Policy:**
- Submission accepted, but `Project.status = WAITLISTED` (new status)
- User sees message: "You're on the waitlist (position #12). We'll notify you if a slot opens."
- If someone withdraws, next waitlist entry promoted to SUBMITTED
---
## 11. Email Notifications
### Receipt Confirmation Email
**Trigger:** `autoConfirmReceipt: true` + project submitted
**Template:**
```
Subject: Application Received — MOPC 2026
Dear Sarah Johnson,
Thank you for submitting your application to the Monaco Ocean Protection Challenge 2026.
Application Details:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Project: Ocean Plastic Recycling Platform
Team: PlastiClean Solutions
Category: Business Concept
Confirmation #: MOPC-2026-00123
Submitted: February 15, 2026 at 3:42 PM UTC
What's Next?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. AI Screening: March 2-5, 2026
Your application will be automatically screened for eligibility.
2. Jury Evaluation: March 10-31, 2026
Expert judges will review eligible projects.
3. Semi-finalist Notification: April 5, 2026
Selected teams will be invited to the next round.
Track Your Progress:
View your application status anytime at:
https://monaco-opc.com/applicant/dashboard
Questions?
Contact us at admin@monaco-opc.com
Best regards,
MOPC Team
```
### Deadline Reminder Emails
**Trigger:** Configured days before deadline (e.g., [7, 3, 1])
**7-Day Reminder:**
```
Subject: MOPC 2026 Application Deadline — 7 Days Remaining
Dear Applicant,
This is a friendly reminder that the MOPC 2026 application deadline is approaching.
Deadline: March 1, 2026 at 11:59 PM UTC
Time Remaining: 7 days
Have you started your application?
☐ Draft saved
☐ Documents uploaded
☐ Final submission
Complete your application:
https://monaco-opc.com/apply/mopc-2026
Need help? Contact admin@monaco-opc.com
Best regards,
MOPC Team
```
**1-Day Reminder:**
```
Subject: ⏰ MOPC 2026 Application Deadline — Tomorrow!
Dear Applicant,
The MOPC 2026 application deadline is tomorrow!
Deadline: March 1, 2026 at 11:59 PM UTC
Time Remaining: 23 hours 17 minutes
Don't miss out! Complete your application now:
https://monaco-opc.com/apply/mopc-2026
Best regards,
MOPC Team
```
---
## 12. API Changes (tRPC Procedures)
### New/Modified Procedures
All procedures are in `src/server/routers/` with these key changes:
**applicant.getSubmissionBySlug** — Get intake round info by slug (for public access)
**applicant.getMySubmission** (enhanced) — Get current user's application (draft or submitted)
**applicant.saveDraft** (new) — Auto-save form data as draft
**applicant.submitApplication** (new) — Finalize draft and mark as submitted
**file.getUploadUrl** (enhanced) — Get pre-signed URL for file upload
**file.confirmUpload** (new) — Mark file upload as complete after successful S3 upload
**admin.getIntakeSubmissions** — Admin dashboard for intake round
**admin.extendDeadline** (new) — Create grace period for specific applicant
---
## 13. Service Functions
Key service functions in `src/server/services/intake-round.ts`:
- `canSubmitToIntakeRound()` — Check if submission window is accepting
- `checkCategoryQuota()` — Validate category quota
- `validateApplicationData()` — Form validation
- `validateFileUpload()` — File requirement validation
- `checkRequiredFiles()` — Verify all required files uploaded
---
## 14. Edge Cases
| Edge Case | Behavior | Solution |
|-----------|----------|----------|
| **User starts draft, deadline passes** | Draft is preserved but cannot submit | Show banner: "Deadline passed. Contact admin if you need extension." Admin can grant GracePeriod. |
| **User submits at exact deadline second** | Accept if server time <= windowCloseAt | Use database server time for consistency |
| **Category quota reached mid-submission** | Check quota again on final submit | Race condition: if quota hit between form start and submit, show error "Quota just filled" |
| **File upload fails mid-submission** | ProjectFile record exists but no S3 object | Cleanup orphaned records via cron job; allow re-upload |
| **User replaces file after deadline** | Check deadline on upload, not just submit | Each file upload checks `canSubmitToWindow()` |
| **Team member email already registered** | Invite sent, user can claim | Email contains link: "Join team or login to existing account" |
| **Applicant deletes draft** | Hard delete or soft delete? | Soft delete: set `deletedAt` field, hide from UI but keep for audit |
| **Admin extends deadline globally** | Update Round.windowCloseAt | All applicants benefit; no GracePeriod records needed |
| **Duplicate submissions (same email)** | One email = one project per competition | Upsert logic: update existing project instead of creating new |
| **File version conflict** | User uploads same requirement twice | Create new ProjectFile, link to old via `replacedById` |
| **Draft expires while user editing** | Auto-save fails with "Draft expired" | Extend expiry on each auto-save (rolling window) |
---
## 15. Integration Points
### Connects to Filtering Round (Next Round)
When intake round closes:
1. Create ProjectRoundState records
2. Trigger filtering job
### File System (MinIO)
- All uploads go to MinIO bucket: `mopc-submissions`
- Object keys: `projects/{projectId}/submissions/{submissionWindowId}/{filename}_{timestamp}.{ext}`
- Pre-signed URLs expire after 1 hour (uploads) or 24 hours (downloads)
### Notification System
**Events emitted:**
- `INTAKE_SUBMISSION_RECEIVED` — confirmation email + in-app notification
- `INTAKE_DEADLINE_APPROACHING` — reminder emails (7d, 3d, 1d before)
- `INTAKE_LATE_SUBMISSION` — flag for admin review
- `INTAKE_QUOTA_REACHED` — notify admins
### Audit Logging
All actions logged in `DecisionAuditLog`:
- `intake.draft_saved` — auto-save triggered
- `intake.submission_finalized` — final submit
- `intake.file_uploaded` — file added
- `intake.file_replaced` — file updated
- `intake.deadline_extended` — admin override
- `intake.quota_reached` — category quota hit
---
## Document Complete
This specification defines the **INTAKE round type** for the redesigned MOPC architecture. Key takeaways:
1. **Typed Config**: IntakeConfig replaces generic JSON with validated, documented fields
2. **SubmissionWindow**: Decouples file requirements from round, enables multi-round submissions
3. **Deadline Policies**: HARD, FLAG, GRACE with clear behavior differences
4. **Draft System**: Auto-save + expiry for incomplete applications
5. **Category Quotas**: Limit startups/concepts with overflow handling
6. **Email Automation**: Confirmation + reminders built-in
7. **Admin Controls**: Dashboard, deadline extensions, quota monitoring
**Next documents:**
- 05-round-filtering.md — AI screening and eligibility
- 06-round-evaluation.md — Jury review with multi-jury support
- 07-round-submission.md — Additional docs from advancing teams