# 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 ``` --- ## 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 } 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) => { 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 warnings: Record } // 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