From 339c48a39dc912b2bd0654d372b6e56e6a2345da Mon Sep 17 00:00:00 2001 From: ascpixi <44982772+ascpixi@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:29:33 -0400 Subject: [PATCH] feat: lock shipping after game end, allowing re-ships for newly rejected projects --- app/controllers/projects_controller.rb | 20 +++++------ .../components/projects/ProjectForm.tsx | 34 +++++++++---------- app/frontend/pages/projects/index.tsx | 20 +++++------ app/frontend/utils/shippingLock.ts | 24 +++++++++++++ app/frontend/utils/shippingPause.ts | 31 ----------------- 5 files changed, 60 insertions(+), 69 deletions(-) create mode 100644 app/frontend/utils/shippingLock.ts delete mode 100644 app/frontend/utils/shippingPause.ts diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index fcdb9746..a24376cb 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -121,20 +121,21 @@ def destroy end end - SHIPPING_PAUSE_START = Time.parse("2026-05-10 22:00:00 UTC").freeze - SHIPPING_RESUME = Time.parse("2026-05-11 22:00:00 UTC").freeze - REJECTION_EXCEPTION_START = (SHIPPING_PAUSE_START - 2.days).freeze + # Shipping locked for good when the game ended on July 6th, 2026. Projects + # rejected after the lock may still be re-shipped, so their authors can + # address the rejection. + SHIPPING_LOCKED_AT = Time.parse("2026-07-06 00:00:00 UTC").freeze def ship authorize @project - if Time.current >= SHIPPING_PAUSE_START && Time.current < SHIPPING_RESUME - unless recently_rejected_reship? || current_user.admin? + if Time.current >= SHIPPING_LOCKED_AT + unless rejected_after_lock? || current_user.admin? track_event("project_ship_failed", { project_id: @project.id, - reason: "shipping_paused" + reason: "shipping_locked" }) - redirect_to manage_project_path(@project), flash: { alert: "Shipping is temporarily paused as reviewers work to review projects of people who are qualifying for the HCTG event. It will be unpaused at 6:00pm ET on May 11th, 2026." } + redirect_to manage_project_path(@project), flash: { alert: "Shipping is locked now that the game has ended. Projects rejected after shipping locked can still be re-shipped." } return end end @@ -190,11 +191,10 @@ def check_repo private - def recently_rejected_reship? + def rejected_after_lock? @project.aasm_state == "rejected" && @project.rejected_at.present? && - @project.rejected_at >= REJECTION_EXCEPTION_START && - @project.rejected_at < SHIPPING_PAUSE_START + @project.rejected_at >= SHIPPING_LOCKED_AT end def project_params diff --git a/app/frontend/components/projects/ProjectForm.tsx b/app/frontend/components/projects/ProjectForm.tsx index c4a9e285..3129d7ad 100644 --- a/app/frontend/components/projects/ProjectForm.tsx +++ b/app/frontend/components/projects/ProjectForm.tsx @@ -5,10 +5,10 @@ import type { ProjectTag } from "@/interfaces/project_tag"; import type { SharedProps } from "@/types"; import formatTime from "@/utils/formatTime"; import { - isShippingPaused, - isShippingPausedForProject, - SHIPPING_PAUSE_MESSAGE, -} from "@/utils/shippingPause"; + isShippingLocked, + isShippingLockedForProject, + SHIPPING_LOCK_MESSAGE, +} from "@/utils/shippingLock"; import { useMemo, useState, useEffect, useRef, useCallback } from "react"; import arrowIcon from "@/assets/icons/arrow.svg"; import clsx from "clsx"; @@ -274,13 +274,13 @@ export default function ProjectForm({ const disabled = project?.aasm_state === "submitted"; const idvVerified = props.user.verification_status === "verified"; const isAdmin = props.user.is_admin; - const shippingPaused = isAdmin + const shippingLocked = isAdmin ? false : project - ? isShippingPausedForProject(project) - : isShippingPaused(); + ? isShippingLockedForProject(project) + : isShippingLocked(); const hasRejectionException = - !isAdmin && isShippingPaused() && !shippingPaused; + !isAdmin && isShippingLocked() && !shippingLocked; const [showShipChecklist, setShowShipChecklist] = useState(false); const [repoChecking, setRepoChecking] = useState(false); const [repoAccessible, setRepoAccessible] = useState(null); @@ -663,16 +663,16 @@ export default function ProjectForm({ - {shippingPaused && + {shippingLocked && project.reported_seconds > project.approved_seconds && (

- {SHIPPING_PAUSE_MESSAGE} + {SHIPPING_LOCK_MESSAGE}

)} {hasRejectionException && project.reported_seconds > project.approved_seconds && (

- Shipping is temporarily paused, but you can re-ship this - project because it was rejected in the two days before the - pause began. Note: if your project is rejected again while - shipping is paused, you will not be able to re-ship it - until the pause ends at 5:00pm ET on May 11th, 2026. + Shipping is locked now that the game has ended, but you + can re-ship this project because it was rejected after + shipping locked.

)} diff --git a/app/frontend/pages/projects/index.tsx b/app/frontend/pages/projects/index.tsx index 0155dee7..2131b94e 100644 --- a/app/frontend/pages/projects/index.tsx +++ b/app/frontend/pages/projects/index.tsx @@ -3,10 +3,10 @@ import IdvVerificationAlert from "@/components/IdvVerificationAlert"; import PageHeading from "@/components/layout/PageHeading"; import ProjectList from "@/components/projects/ProjectList"; import { - isShippingPaused, - isShippingPausedForProject, - SHIPPING_PAUSE_MESSAGE, -} from "@/utils/shippingPause"; + isShippingLocked, + isShippingLockedForProject, + SHIPPING_LOCK_MESSAGE, +} from "@/utils/shippingLock"; import { usePage } from "@inertiajs/react"; import type { SharedProps } from "@/types"; @@ -15,10 +15,10 @@ import type { Project } from "@/interfaces/project"; export default function Index({ projects }: { projects: Project[] }) { const { props } = usePage(); const isAdmin = props.user.is_admin; - const allPaused = + const allLocked = !isAdmin && - isShippingPaused() && - projects.every((p) => isShippingPausedForProject(p)); + isShippingLocked() && + projects.every((p) => isShippingLockedForProject(p)); return ( @@ -29,10 +29,10 @@ export default function Index({ projects }: { projects: Project[] }) {
- {allPaused && ( + {allLocked && (
- Shipping paused -

{SHIPPING_PAUSE_MESSAGE}

+ Shipping locked +

{SHIPPING_LOCK_MESSAGE}

)} diff --git a/app/frontend/utils/shippingLock.ts b/app/frontend/utils/shippingLock.ts new file mode 100644 index 00000000..20706933 --- /dev/null +++ b/app/frontend/utils/shippingLock.ts @@ -0,0 +1,24 @@ +// Shipping locked for good when the game ended on July 6th, 2026. Projects +// rejected after the lock may still be re-shipped, so their authors can +// address the rejection. Must match SHIPPING_LOCKED_AT in projects_controller.rb. +const SHIPPING_LOCKED_AT = new Date("2026-07-06T00:00:00Z"); + +export const SHIPPING_LOCK_MESSAGE = + "Shipping is locked now that the game has ended. Projects rejected after shipping locked can still be re-shipped."; + +export function isShippingLocked(): boolean { + return new Date() >= SHIPPING_LOCKED_AT; +} + +export function isShippingLockedForProject(project: { + aasm_state: string; + rejected_at: string | null; +}): boolean { + if (!isShippingLocked()) return false; + if (project.aasm_state === "rejected" && project.rejected_at !== null) { + if (new Date(project.rejected_at) >= SHIPPING_LOCKED_AT) { + return false; + } + } + return true; +} diff --git a/app/frontend/utils/shippingPause.ts b/app/frontend/utils/shippingPause.ts deleted file mode 100644 index 1303f935..00000000 --- a/app/frontend/utils/shippingPause.ts +++ /dev/null @@ -1,31 +0,0 @@ -// EDT is UTC-4 -const SHIPPING_PAUSE_START = new Date("2026-05-10T22:00:00Z"); -const SHIPPING_RESUME = new Date("2026-05-11T22:00:00Z"); -const REJECTION_EXCEPTION_START = new Date( - SHIPPING_PAUSE_START.getTime() - 2 * 24 * 60 * 60 * 1000, -); - -export const SHIPPING_PAUSE_MESSAGE = - "Shipping is temporarily paused as reviewers work to review projects of people who are qualifying for the HCTG event. It will be unpaused at 6:00pm ET on May 11th, 2026."; - -export function isShippingPaused(): boolean { - const now = new Date(); - return now >= SHIPPING_PAUSE_START && now < SHIPPING_RESUME; -} - -export function isShippingPausedForProject(project: { - aasm_state: string; - rejected_at: string | null; -}): boolean { - if (!isShippingPaused()) return false; - if (project.aasm_state === "rejected" && project.rejected_at !== null) { - const rejectedAt = new Date(project.rejected_at); - if ( - rejectedAt >= REJECTION_EXCEPTION_START && - rejectedAt < SHIPPING_PAUSE_START - ) { - return false; - } - } - return true; -}