This commit is contained in:
Trey t
2026-02-18 10:54:18 -06:00
parent a5245955af
commit 215e7c895d
11 changed files with 638 additions and 79 deletions

View File

@@ -1,6 +1,12 @@
{ {
"permissions": {
"allow": [
"WebSearch",
"WebFetch(domain:github.com)"
]
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": [ "enabledMcpjsonServers": [
"ios-simulator" "ios-simulator"
], ]
"enableAllProjectMcpServers": true
} }

View File

@@ -0,0 +1,511 @@
'use client';
import { Clock, Mail, Bell, Calendar, Info, Timer, Repeat } from 'lucide-react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
export default function AutomationReferencePage() {
return (
<div className="p-6 space-y-6">
<div>
<h1 className="text-2xl font-bold">Automation Reference</h1>
<p className="text-muted-foreground">
Quick reference for all automated emails, notifications, and scheduled jobs
</p>
</div>
{/* Scheduled Jobs */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
Scheduled Jobs (Cron)
</CardTitle>
<CardDescription>
Background jobs that run on a schedule. All times are in UTC.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Job</TableHead>
<TableHead>Schedule</TableHead>
<TableHead>Default Hour</TableHead>
<TableHead>Description</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">Task Reminders (Due Soon)</TableCell>
<TableCell>
<Badge variant="outline" className="font-mono">0 20 * * *</Badge>
<span className="text-xs text-muted-foreground ml-2">Daily 8 PM UTC</span>
</TableCell>
<TableCell>
<span className="font-mono">20:00</span> (8 PM)
</TableCell>
<TableCell className="max-w-md">
<p className="text-sm">
Sends frequency-aware &quot;due soon&quot; task reminders. Reminder schedule varies by task frequency
(weekly: day-of only, monthly: 7 days + day-of, annual: 30/14/7 days + day-of).
</p>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Overdue Reminders</TableCell>
<TableCell>
<Badge variant="outline" className="font-mono">0 9 * * *</Badge>
<span className="text-xs text-muted-foreground ml-2">Daily 9 AM UTC</span>
</TableCell>
<TableCell>
<span className="font-mono">09:00</span> (9 AM)
</TableCell>
<TableCell className="max-w-md">
<p className="text-sm">
Sends overdue task reminders with tapering: daily for days 1-3, every 3 days for days 4-14,
then stops to avoid notification fatigue.
</p>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Daily Digest</TableCell>
<TableCell>
<Badge variant="outline" className="font-mono">0 11 * * *</Badge>
<span className="text-xs text-muted-foreground ml-2">Daily 11 AM UTC</span>
</TableCell>
<TableCell>
<span className="font-mono">11:00</span> (11 AM)
</TableCell>
<TableCell className="max-w-md">
<p className="text-sm">
Summary push notification with overdue count and tasks due this week.
Uses user&apos;s stored timezone for accurate overdue calculation.
</p>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Onboarding Emails</TableCell>
<TableCell>
<Badge variant="outline" className="font-mono text-yellow-600">Not scheduled</Badge>
<span className="text-xs text-muted-foreground ml-2">Manual only</span>
</TableCell>
<TableCell>
<span className="text-muted-foreground">-</span>
</TableCell>
<TableCell className="max-w-md">
<p className="text-sm">
Handler exists but not registered in scheduler. Can be triggered manually from admin.
Sends emails to users who haven&apos;t created residences (2+ days) or tasks (5+ days).
</p>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
{/* Push Notification Types */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="h-5 w-5" />
Push Notification Types
</CardTitle>
<CardDescription>
Types of push notifications and when they are sent
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Type</TableHead>
<TableHead>Trigger</TableHead>
<TableHead>User Preference</TableHead>
<TableHead>Custom Time Field</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">
<Badge>task_due_soon</Badge>
</TableCell>
<TableCell>Smart Reminder job (tasks due within 2 days)</TableCell>
<TableCell><code>task_due_soon</code></TableCell>
<TableCell><code>task_due_soon_hour</code></TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">
<Badge variant="destructive">task_overdue</Badge>
</TableCell>
<TableCell>Smart Reminder job (past due date)</TableCell>
<TableCell><code>task_overdue</code></TableCell>
<TableCell><code>task_overdue_hour</code></TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">
<Badge variant="secondary">task_completed</Badge>
</TableCell>
<TableCell>When another user completes a task</TableCell>
<TableCell><code>task_completed</code></TableCell>
<TableCell>-</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">
<Badge variant="secondary">task_assigned</Badge>
</TableCell>
<TableCell>When assigned to a task</TableCell>
<TableCell><code>task_assigned</code></TableCell>
<TableCell>-</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">
<Badge variant="secondary">residence_shared</Badge>
</TableCell>
<TableCell>When a residence is shared with user</TableCell>
<TableCell><code>residence_shared</code></TableCell>
<TableCell>-</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">
<Badge variant="outline">warranty_expiring</Badge>
</TableCell>
<TableCell>Document warranty expiring (not yet implemented)</TableCell>
<TableCell><code>warranty_expiring</code></TableCell>
<TableCell><code>warranty_expiring_hour</code></TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">
<Badge className="bg-blue-500">daily_digest</Badge>
</TableCell>
<TableCell>Daily Digest job (summary notification)</TableCell>
<TableCell><code>daily_digest</code></TableCell>
<TableCell><code>daily_digest_hour</code></TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
{/* Email Templates */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Mail className="h-5 w-5" />
Email Templates
</CardTitle>
<CardDescription>
All email templates and when they are sent
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="transactional" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="transactional" className="flex items-center gap-2">
<Timer className="h-4 w-4" />
Transactional Emails
</TabsTrigger>
<TabsTrigger value="onboarding" className="flex items-center gap-2">
<Repeat className="h-4 w-4" />
Onboarding Emails
</TabsTrigger>
</TabsList>
<TabsContent value="transactional" className="mt-4">
<Table>
<TableHeader>
<TableRow>
<TableHead>Email</TableHead>
<TableHead>Subject</TableHead>
<TableHead>Trigger</TableHead>
<TableHead>Expiry</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">Welcome Email</TableCell>
<TableCell>Welcome to Casera - Verify Your Email</TableCell>
<TableCell>User registration (email/password)</TableCell>
<TableCell>24 hours (verification code)</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Apple Welcome</TableCell>
<TableCell>Welcome to Casera!</TableCell>
<TableCell>Apple Sign In registration</TableCell>
<TableCell>-</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Google Welcome</TableCell>
<TableCell>Welcome to Casera!</TableCell>
<TableCell>Google Sign In registration</TableCell>
<TableCell>-</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Post-Verification</TableCell>
<TableCell>You&apos;re All Set! Getting Started with Casera</TableCell>
<TableCell>After email verification</TableCell>
<TableCell>-</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Verification Email</TableCell>
<TableCell>Casera - Verify Your Email</TableCell>
<TableCell>Resend verification code</TableCell>
<TableCell>24 hours</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Password Reset</TableCell>
<TableCell>Casera - Password Reset Request</TableCell>
<TableCell>Password reset request</TableCell>
<TableCell>15 minutes</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Password Changed</TableCell>
<TableCell>Casera - Your Password Has Been Changed</TableCell>
<TableCell>After password change</TableCell>
<TableCell>-</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Task Completed</TableCell>
<TableCell>Casera - Task Completed: [Task Title]</TableCell>
<TableCell>When task is completed (if email_task_completed = true)</TableCell>
<TableCell>-</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Tasks Report</TableCell>
<TableCell>Casera - Tasks Report for [Residence]</TableCell>
<TableCell>User requests PDF export</TableCell>
<TableCell>-</TableCell>
</TableRow>
</TableBody>
</Table>
</TabsContent>
<TabsContent value="onboarding" className="mt-4">
<p className="text-sm text-muted-foreground mb-4">
Cron-triggered daily at 10:00 AM UTC. Each email type sent only once per user.
</p>
<Table>
<TableHeader>
<TableRow>
<TableHead>Email Type</TableHead>
<TableHead>Subject</TableHead>
<TableHead>Criteria</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">
<Badge variant="outline">no_residence</Badge>
</TableCell>
<TableCell>Get started with Casera - Add your first property</TableCell>
<TableCell>2+ days since registration, no residence created</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">
<Badge variant="outline">no_tasks</Badge>
</TableCell>
<TableCell>Stay on top of home maintenance with Casera</TableCell>
<TableCell>5+ days since first residence, no tasks created</TableCell>
</TableRow>
</TableBody>
</Table>
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* Default Configuration */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="h-5 w-5" />
Scheduled Job Times
</CardTitle>
<CardDescription>
Hardcoded cron schedules in internal/worker/scheduler.go (all times UTC)
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Cron Expression</TableHead>
<TableHead>Hour (UTC)</TableHead>
<TableHead>Description</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-mono">Cron: 0 20 * * *</TableCell>
<TableCell>
<Badge variant="secondary">20</Badge>
<span className="text-xs text-muted-foreground ml-2">(8:00 PM UTC)</span>
</TableCell>
<TableCell>Task reminders (due soon) - hardcoded in scheduler.go</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono">Cron: 0 9 * * *</TableCell>
<TableCell>
<Badge variant="secondary">9</Badge>
<span className="text-xs text-muted-foreground ml-2">(9:00 AM UTC)</span>
</TableCell>
<TableCell>Overdue reminders - hardcoded in scheduler.go</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono">Cron: 0 11 * * *</TableCell>
<TableCell>
<Badge variant="secondary">11</Badge>
<span className="text-xs text-muted-foreground ml-2">(11:00 AM UTC)</span>
</TableCell>
<TableCell>Daily digest - hardcoded in scheduler.go</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
{/* Smart Reminder Logic */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Info className="h-5 w-5" />
Smart Reminder Logic
</CardTitle>
<CardDescription>
How the frequency-aware reminder system works
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-lg border p-4 space-y-3">
<h4 className="font-medium">Reminder Stages by Frequency</h4>
<p className="text-xs text-muted-foreground mb-2">
Source: internal/notifications/reminder_config.go
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
<div>
<p className="font-medium text-muted-foreground">Once / Daily / Weekly:</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>Day-of only</li>
</ul>
</div>
<div>
<p className="font-medium text-muted-foreground">Bi-Weekly (14d):</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>1 day before</li>
<li>Day-of</li>
</ul>
</div>
<div>
<p className="font-medium text-muted-foreground">Monthly (30d):</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>3 days before</li>
<li>1 day before</li>
<li>Day-of</li>
</ul>
</div>
<div>
<p className="font-medium text-muted-foreground">Quarterly (90d):</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>7 days before</li>
<li>3 days before</li>
<li>1 day before</li>
<li>Day-of</li>
</ul>
</div>
<div>
<p className="font-medium text-muted-foreground">Semi-Annually (180d):</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>14 days before</li>
<li>7 days before</li>
<li>3 days before</li>
<li>1 day before</li>
<li>Day-of</li>
</ul>
</div>
<div>
<p className="font-medium text-muted-foreground">Annually (365d):</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>30 days before</li>
<li>14 days before</li>
<li>7 days before</li>
<li>3 days before</li>
<li>1 day before</li>
<li>Day-of</li>
</ul>
</div>
</div>
</div>
<div className="rounded-lg border p-4 space-y-3">
<h4 className="font-medium">Overdue Reminder Tapering</h4>
<p className="text-sm text-muted-foreground">
Overdue reminders taper off to avoid notification fatigue (configured in reminder_config.go):
</p>
<ul className="list-disc list-inside text-sm space-y-1">
<li><strong>Days 1-3:</strong> Daily reminders (DailyReminderDays: 3)</li>
<li><strong>Days 4, 7, 10, 13:</strong> Every 3 days (TaperIntervalDays: 3)</li>
<li><strong>After 14 days:</strong> No more reminders (MaxOverdueDays: 14)</li>
</ul>
</div>
<div className="rounded-lg border p-4 space-y-3">
<h4 className="font-medium">Duplicate Prevention</h4>
<p className="text-sm text-muted-foreground">
Each reminder is tracked in the <code className="bg-muted px-1 rounded">task_reminder_logs</code> table
with task ID, user ID, due date, and stage. This prevents sending the same reminder twice.
Logs older than 90 days are automatically cleaned up.
</p>
</div>
</CardContent>
</Card>
{/* Timezone Handling */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
Timezone Handling
</CardTitle>
<CardDescription>
How user timezones are captured and used
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-lg border p-4 space-y-3">
<h4 className="font-medium">Auto-Capture</h4>
<p className="text-sm text-muted-foreground">
The mobile app sends an <code className="bg-muted px-1 rounded">X-Timezone</code> header
on every API request (IANA format, e.g., &quot;America/Los_Angeles&quot;).
The server captures this when the user fetches their tasks (happens on every app launch)
and stores it in the <code className="bg-muted px-1 rounded">timezone</code> column of
<code className="bg-muted px-1 rounded">notifications_notificationpreference</code>.
</p>
</div>
<div className="rounded-lg border p-4 space-y-3">
<h4 className="font-medium">Usage in Daily Digest</h4>
<p className="text-sm text-muted-foreground">
The Daily Digest job uses the stored timezone to calculate &quot;start of day&quot; in the user&apos;s
local time. This ensures the overdue task count matches what the user sees in the app&apos;s Kanban view.
If no timezone is stored, UTC is used as fallback.
</p>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -30,6 +30,7 @@ import {
Apple, Apple,
Image, Image,
ImagePlus, ImagePlus,
Cog,
} from 'lucide-react'; } from 'lucide-react';
import { useRouter, usePathname } from 'next/navigation'; import { useRouter, usePathname } from 'next/navigation';
import { useAuthStore } from '@/store/auth'; import { useAuthStore } from '@/store/auth';
@@ -77,6 +78,7 @@ const limitationsItems = [
const settingsItems = [ const settingsItems = [
{ title: 'Monitoring', url: '/admin/monitoring', icon: Activity }, { title: 'Monitoring', url: '/admin/monitoring', icon: Activity },
{ title: 'Automation Reference', url: '/admin/automation-reference', icon: Cog },
{ title: 'Lookup Tables', url: '/admin/lookups', icon: BookOpen }, { title: 'Lookup Tables', url: '/admin/lookups', icon: BookOpen },
{ title: 'Task Templates', url: '/admin/task-templates', icon: LayoutTemplate }, { title: 'Task Templates', url: '/admin/task-templates', icon: LayoutTemplate },
{ title: 'Admin Users', url: '/admin/admin-users', icon: UserCog }, { title: 'Admin Users', url: '/admin/admin-users', icon: UserCog },

View File

@@ -2,6 +2,9 @@
This document explains how tasks are categorized into kanban columns in the Casera application. This document explains how tasks are categorized into kanban columns in the Casera application.
> Note: The categorization chain still computes `cancelled_tasks`, but the kanban board response
> intentionally hides cancelled/archived tasks and returns only 5 visible columns.
## Overview ## Overview
The task categorization system uses the **Chain of Responsibility** design pattern to determine which kanban column a task belongs to. Each handler in the chain evaluates the task against specific criteria, and if matched, returns the appropriate column. If not matched, the task is passed to the next handler. The task categorization system uses the **Chain of Responsibility** design pattern to determine which kanban column a task belongs to. Each handler in the chain evaluates the task against specific criteria, and if matched, returns the appropriate column. If not matched, the task is passed to the next handler.

View File

@@ -2,9 +2,12 @@
This document describes how tasks are categorized into kanban columns for display in the Casera mobile app. This document describes how tasks are categorized into kanban columns for display in the Casera mobile app.
> Important: The board intentionally returns **5 visible columns**. Cancelled and archived tasks are
> hidden from board responses (though task-level `kanban_column` may still be `cancelled_tasks`).
## Overview ## Overview
Tasks are organized into 6 kanban columns based on their state and due date. The categorization logic is implemented in `internal/repositories/task_repo.go` in the `GetKanbanData` and `GetKanbanDataForMultipleResidences` functions. Tasks are organized into 5 visible kanban columns based on their state and due date. The categorization logic is implemented in `internal/repositories/task_repo.go` in the `GetKanbanData` and `GetKanbanDataForMultipleResidences` functions.
## Columns Summary ## Columns Summary
@@ -15,7 +18,6 @@ Tasks are organized into 6 kanban columns based on their state and due date. The
| 3 | **Upcoming** | `#007AFF` (Blue) | edit, complete, cancel, mark_in_progress | Tasks due beyond the threshold or with no due date | | 3 | **Upcoming** | `#007AFF` (Blue) | edit, complete, cancel, mark_in_progress | Tasks due beyond the threshold or with no due date |
| 4 | **In Progress** | `#5856D6` (Purple) | edit, complete, cancel | Tasks with status "In Progress" | | 4 | **In Progress** | `#5856D6` (Purple) | edit, complete, cancel | Tasks with status "In Progress" |
| 5 | **Completed** | `#34C759` (Green) | view | Tasks with at least one completion record | | 5 | **Completed** | `#34C759` (Green) | view | Tasks with at least one completion record |
| 6 | **Cancelled** | `#8E8E93` (Gray) | uncancel, delete | Tasks marked as cancelled |
## Categorization Flow ## Categorization Flow

View File

@@ -227,7 +227,7 @@ func TestTaskHandler_GetTasksByResidence(t *testing.T) {
assert.Contains(t, response, "residence_id") assert.Contains(t, response, "residence_id")
columns := response["columns"].([]interface{}) columns := response["columns"].([]interface{})
assert.Len(t, columns, 6) // 6 kanban columns assert.Len(t, columns, 5) // 5 visible kanban columns (cancelled/archived hidden)
}) })
t.Run("kanban column structure", func(t *testing.T) { t.Run("kanban column structure", func(t *testing.T) {

View File

@@ -7,6 +7,7 @@
"error.email_already_taken": "Email already taken", "error.email_already_taken": "Email already taken",
"error.registration_failed": "Registration failed", "error.registration_failed": "Registration failed",
"error.not_authenticated": "Not authenticated", "error.not_authenticated": "Not authenticated",
"error.invalid_token": "Invalid token",
"error.failed_to_get_user": "Failed to get user", "error.failed_to_get_user": "Failed to get user",
"error.failed_to_update_profile": "Failed to update profile", "error.failed_to_update_profile": "Failed to update profile",
"error.invalid_verification_code": "Invalid verification code", "error.invalid_verification_code": "Invalid verification code",

View File

@@ -943,7 +943,7 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
{"Cancelled Task 1 - Build shed", 3, 15, "cancelled"}, {"Cancelled Task 1 - Build shed", 3, 15, "cancelled"},
{"Cancelled Task 2 - Install pool", 4, 60, "cancelled"}, {"Cancelled Task 2 - Install pool", 4, 60, "cancelled"},
// Archived tasks (should appear in cancelled column) // Archived tasks (hidden from kanban board)
{"Archived Task 1 - Old project", 0, -30, "archived"}, {"Archived Task 1 - Old project", 0, -30, "archived"},
{"Archived Task 2 - Deprecated work", 1, -20, "archived"}, {"Archived Task 2 - Deprecated work", 1, -20, "archived"},
@@ -1059,7 +1059,7 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
var taskListResp map[string]interface{} var taskListResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &taskListResp) json.Unmarshal(w.Body.Bytes(), &taskListResp)
// Count total tasks across all columns // Count total visible tasks across all columns
totalTasks := 0 totalTasks := 0
if columns, ok := taskListResp["columns"].([]interface{}); ok { if columns, ok := taskListResp["columns"].([]interface{}); ok {
for _, col := range columns { for _, col := range columns {
@@ -1069,7 +1069,13 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
} }
} }
} }
assert.Equal(t, 20, totalTasks, "Should have 20 total tasks") expectedVisibleTasks := 0
for _, task := range createdTasks {
if task.ExpectedColumn != "" {
expectedVisibleTasks++
}
}
assert.Equal(t, expectedVisibleTasks, totalTasks, "Should have %d visible tasks", expectedVisibleTasks)
// Verify individual task retrieval // Verify individual task retrieval
for _, task := range createdTasks { for _, task := range createdTasks {
@@ -1131,7 +1137,6 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
"due_soon_tasks": false, "due_soon_tasks": false,
"upcoming_tasks": false, "upcoming_tasks": false,
"completed_tasks": false, "completed_tasks": false,
"cancelled_tasks": false,
} }
for _, col := range columns { for _, col := range columns {
@@ -1191,10 +1196,29 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
} }
} }
// Verify EACH task by ID is in its expected column // Verify each task is in expected column (or hidden for cancelled/archived)
// This catches swaps where counts match but tasks are in wrong columns
t.Log(" Verifying each task's column membership by ID:") t.Log(" Verifying each task's column membership by ID:")
for _, task := range createdTasks { for _, task := range createdTasks {
if task.ExpectedColumn == "" {
found := false
for colName, ids := range columnTaskIDs {
for _, id := range ids {
if id == task.ID {
found = true
assert.Fail(t, "Hidden task unexpectedly visible",
"Task ID %d ('%s') should be hidden from kanban but appeared in '%s'",
task.ID, task.Title, colName)
break
}
}
}
assert.False(t, found, "Task ID %d ('%s') should be hidden from kanban", task.ID, task.Title)
if !found {
t.Logf(" ✓ Task %d ('%s') correctly hidden from board", task.ID, task.Title)
}
continue
}
actualIDs := columnTaskIDs[task.ExpectedColumn] actualIDs := columnTaskIDs[task.ExpectedColumn]
found := false found := false
for _, id := range actualIDs { for _, id := range actualIDs {
@@ -1210,14 +1234,14 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
} }
} }
// Verify total equals 20 (sanity check) // Verify total equals expected visible tasks (sanity check)
total := 0 total := 0
for _, ids := range columnTaskIDs { for _, ids := range columnTaskIDs {
total += len(ids) total += len(ids)
} }
assert.Equal(t, 20, total, "Total tasks across all columns should be 20") assert.Equal(t, expectedVisibleTasks, total, "Total tasks across all columns should be %d", expectedVisibleTasks)
t.Log("✓ All 20 tasks verified in correct columns by ID") t.Logf("✓ All %d visible tasks verified in correct columns by ID", expectedVisibleTasks)
// ============ Phase 9: Create User B ============ // ============ Phase 9: Create User B ============
t.Log("Phase 9: Creating User B and verifying login") t.Log("Phase 9: Creating User B and verifying login")
@@ -1335,7 +1359,7 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
// Count expected tasks for shared residence (residenceIndex=0 in our config) // Count expected tasks for shared residence (residenceIndex=0 in our config)
expectedTasksForResidence := 0 expectedTasksForResidence := 0
for _, task := range createdTasks { for _, task := range createdTasks {
if task.ResidenceID == sharedResidenceID { if task.ResidenceID == sharedResidenceID && task.ExpectedColumn != "" {
expectedTasksForResidence++ expectedTasksForResidence++
} }
} }
@@ -1409,7 +1433,7 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
expectedColumnNames := []string{ expectedColumnNames := []string{
"overdue_tasks", "in_progress_tasks", "due_soon_tasks", "overdue_tasks", "in_progress_tasks", "due_soon_tasks",
"upcoming_tasks", "completed_tasks", "cancelled_tasks", "upcoming_tasks", "completed_tasks",
} }
for _, colName := range expectedColumnNames { for _, colName := range expectedColumnNames {
assert.True(t, foundColumns[colName], "User B should have column: %s", colName) assert.True(t, foundColumns[colName], "User B should have column: %s", colName)
@@ -1530,16 +1554,16 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
// based on its due date offset, status, and threshold // based on its due date offset, status, and threshold
func determineExpectedColumn(daysFromNow int, status string, threshold int) string { func determineExpectedColumn(daysFromNow int, status string, threshold int) string {
// This must match the categorization chain priority order: // This must match the categorization chain priority order:
// 1. Cancelled (priority 1) // Cancelled and archived tasks are intentionally hidden from kanban board view.
// 2. Archived (priority 2) // Remaining visible columns follow:
// 3. Completed (priority 3) // 1. Completed
// 4. InProgress (priority 4) - takes precedence over date-based columns! // 2. InProgress (takes precedence over date-based columns)
// 5. Overdue (priority 5) // 3. Overdue
// 6. DueSoon (priority 6) // 4. DueSoon
// 7. Upcoming (priority 7) // 5. Upcoming
switch status { switch status {
case "cancelled", "archived": case "cancelled", "archived":
return "cancelled_tasks" return "" // Hidden from board
case "completed": case "completed":
return "completed_tasks" return "completed_tasks"
case "in_progress": case "in_progress":

View File

@@ -346,7 +346,8 @@ func (r *TaskRepository) Unarchive(id uint) error {
// buildKanbanColumns builds the kanban column array from categorized task slices. // buildKanbanColumns builds the kanban column array from categorized task slices.
// This is a helper function to reduce duplication between GetKanbanData and GetKanbanDataForMultipleResidences. // This is a helper function to reduce duplication between GetKanbanData and GetKanbanDataForMultipleResidences.
// TEMPORARILY DISABLED: cancelled parameter removed - cancel column hidden from kanban // Note: cancelled/archived tasks are intentionally hidden from the kanban board.
// They still retain "cancelled_tasks" as task-level categorization for detail views/actions.
func buildKanbanColumns( func buildKanbanColumns(
overdue, inProgress, dueSoon, upcoming, completed []models.Task, overdue, inProgress, dueSoon, upcoming, completed []models.Task,
) []models.KanbanColumn { ) []models.KanbanColumn {
@@ -396,7 +397,8 @@ func buildKanbanColumns(
Tasks: completed, Tasks: completed,
Count: len(completed), Count: len(completed),
}, },
// TEMPORARILY DISABLED - Cancel column hidden from kanban // Intentionally hidden from board:
// cancelled/archived tasks are not returned as a kanban column.
// { // {
// Name: string(categorization.ColumnCancelled), // Name: string(categorization.ColumnCancelled),
// DisplayName: "Cancelled", // DisplayName: "Cancelled",
@@ -451,7 +453,8 @@ func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int, now
return nil, fmt.Errorf("get completed tasks: %w", err) return nil, fmt.Errorf("get completed tasks: %w", err)
} }
// TEMPORARILY DISABLED - Cancel column hidden from kanban // Intentionally hidden from board:
// cancelled/archived tasks are not returned as a kanban column.
// cancelled, err := r.GetCancelledTasks(opts) // cancelled, err := r.GetCancelledTasks(opts)
// if err != nil { // if err != nil {
// return nil, fmt.Errorf("get cancelled tasks: %w", err) // return nil, fmt.Errorf("get cancelled tasks: %w", err)
@@ -509,7 +512,8 @@ func (r *TaskRepository) GetKanbanDataForMultipleResidences(residenceIDs []uint,
return nil, fmt.Errorf("get completed tasks: %w", err) return nil, fmt.Errorf("get completed tasks: %w", err)
} }
// TEMPORARILY DISABLED - Cancel column hidden from kanban // Intentionally hidden from board:
// cancelled/archived tasks are not returned as a kanban column.
// cancelled, err := r.GetCancelledTasks(opts) // cancelled, err := r.GetCancelledTasks(opts)
// if err != nil { // if err != nil {
// return nil, fmt.Errorf("get cancelled tasks: %w", err) // return nil, fmt.Errorf("get cancelled tasks: %w", err)

View File

@@ -306,7 +306,7 @@ func TestTaskRepository_CountByResidence(t *testing.T) {
// === Kanban Board Categorization Tests === // === Kanban Board Categorization Tests ===
func TestKanbanBoard_CancelledTasksGoToCancelledColumn(t *testing.T) { func TestKanbanBoard_CancelledTasksHiddenFromKanbanBoard(t *testing.T) {
db := testutil.SetupTestDB(t) db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db) repo := NewTaskRepository(db)
testutil.SeedLookupData(t, db) testutil.SeedLookupData(t, db)
@@ -321,22 +321,16 @@ func TestKanbanBoard_CancelledTasksGoToCancelledColumn(t *testing.T) {
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC()) board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
require.NoError(t, err) require.NoError(t, err)
// Find cancelled column assert.Len(t, board.Columns, 5, "board should have 5 visible columns")
var cancelledColumn *models.KanbanColumn for _, col := range board.Columns {
for i := range board.Columns { assert.NotEqual(t, "cancelled_tasks", col.Name, "cancelled column must be hidden")
if board.Columns[i].Name == "cancelled_tasks" {
cancelledColumn = &board.Columns[i]
break
}
} }
require.NotNil(t, cancelledColumn, "cancelled_tasks column should exist") totalTasks := 0
assert.Equal(t, 1, cancelledColumn.Count) for _, col := range board.Columns {
assert.Len(t, cancelledColumn.Tasks, 1) totalTasks += col.Count
assert.Equal(t, "Cancelled Task", cancelledColumn.Tasks[0].Title) }
assert.Equal(t, 0, totalTasks, "cancelled task should be hidden from board")
// Verify button types for cancelled column
assert.ElementsMatch(t, []string{"uncancel", "delete"}, cancelledColumn.ButtonTypes)
} }
func TestKanbanBoard_CompletedTasksGoToCompletedColumn(t *testing.T) { func TestKanbanBoard_CompletedTasksGoToCompletedColumn(t *testing.T) {
@@ -566,7 +560,7 @@ func TestKanbanBoard_TasksWithNoDueDateGoToUpcomingColumn(t *testing.T) {
assert.Equal(t, task.ID, upcomingColumn.Tasks[0].ID) assert.Equal(t, task.ID, upcomingColumn.Tasks[0].ID)
} }
func TestKanbanBoard_ArchivedTasksGoToCancelledColumn(t *testing.T) { func TestKanbanBoard_ArchivedTasksHiddenFromKanbanBoard(t *testing.T) {
db := testutil.SetupTestDB(t) db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db) repo := NewTaskRepository(db)
testutil.SeedLookupData(t, db) testutil.SeedLookupData(t, db)
@@ -582,38 +576,36 @@ func TestKanbanBoard_ArchivedTasksGoToCancelledColumn(t *testing.T) {
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC()) board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
require.NoError(t, err) require.NoError(t, err)
// Find the cancelled column and verify archived task is there // Find the upcoming column and verify archived task is hidden
var cancelledColumn *models.KanbanColumn
var upcomingColumn *models.KanbanColumn var upcomingColumn *models.KanbanColumn
foundCancelledColumn := false
for i := range board.Columns { for i := range board.Columns {
if board.Columns[i].Name == "cancelled_tasks" { if board.Columns[i].Name == "cancelled_tasks" {
cancelledColumn = &board.Columns[i] foundCancelledColumn = true
} }
if board.Columns[i].Name == "upcoming_tasks" { if board.Columns[i].Name == "upcoming_tasks" {
upcomingColumn = &board.Columns[i] upcomingColumn = &board.Columns[i]
} }
} }
require.NotNil(t, cancelledColumn, "cancelled_tasks column should exist")
require.NotNil(t, upcomingColumn, "upcoming_tasks column should exist") require.NotNil(t, upcomingColumn, "upcoming_tasks column should exist")
assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
// Archived task should be in the cancelled column // Archived task should be hidden from board
assert.Equal(t, 1, cancelledColumn.Count, "archived task should be in cancelled column")
assert.Equal(t, "Archived Task", cancelledColumn.Tasks[0].Title)
// Regular task should be in upcoming (no due date) // Regular task should be in upcoming (no due date)
assert.Equal(t, 1, upcomingColumn.Count, "regular task should be in upcoming column") assert.Equal(t, 1, upcomingColumn.Count, "regular task should be in upcoming column")
assert.Equal(t, "Regular Task", upcomingColumn.Tasks[0].Title) assert.Equal(t, "Regular Task", upcomingColumn.Tasks[0].Title)
// Total tasks should be 2 (both appear in the board) // Total tasks should be 1 (archived task is hidden)
totalTasks := 0 totalTasks := 0
for _, col := range board.Columns { for _, col := range board.Columns {
totalTasks += col.Count totalTasks += col.Count
} }
assert.Equal(t, 2, totalTasks, "both tasks should appear in the board") assert.Equal(t, 1, totalTasks, "archived task should be hidden from board")
} }
func TestKanbanBoard_ArchivedOverdueTask_GoesToCancelledNotOverdue(t *testing.T) { func TestKanbanBoard_ArchivedOverdueTask_HiddenFromKanbanBoard(t *testing.T) {
db := testutil.SetupTestDB(t) db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db) repo := NewTaskRepository(db)
testutil.SeedLookupData(t, db) testutil.SeedLookupData(t, db)
@@ -638,26 +630,30 @@ func TestKanbanBoard_ArchivedOverdueTask_GoesToCancelledNotOverdue(t *testing.T)
require.NoError(t, err) require.NoError(t, err)
// Find columns // Find columns
var cancelledColumn, overdueColumn *models.KanbanColumn var overdueColumn *models.KanbanColumn
foundCancelledColumn := false
for i := range board.Columns { for i := range board.Columns {
if board.Columns[i].Name == "cancelled_tasks" { if board.Columns[i].Name == "cancelled_tasks" {
cancelledColumn = &board.Columns[i] foundCancelledColumn = true
} }
if board.Columns[i].Name == "overdue_tasks" { if board.Columns[i].Name == "overdue_tasks" {
overdueColumn = &board.Columns[i] overdueColumn = &board.Columns[i]
} }
} }
require.NotNil(t, cancelledColumn)
require.NotNil(t, overdueColumn) require.NotNil(t, overdueColumn)
assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
// Archived task should be in cancelled, NOT overdue // Archived task should be hidden and NOT overdue
assert.Equal(t, 1, cancelledColumn.Count, "archived task should be in cancelled column")
assert.Equal(t, 0, overdueColumn.Count, "archived task should NOT be in overdue column") assert.Equal(t, 0, overdueColumn.Count, "archived task should NOT be in overdue column")
assert.Equal(t, "Archived Overdue Task", cancelledColumn.Tasks[0].Title) totalTasks := 0
for _, col := range board.Columns {
totalTasks += col.Count
}
assert.Equal(t, 0, totalTasks, "archived task should be hidden from board")
} }
func TestKanbanBoard_CategoryPriority_CancelledTakesPrecedence(t *testing.T) { func TestKanbanBoard_CategoryPriority_CancelledTasksAreHidden(t *testing.T) {
db := testutil.SetupTestDB(t) db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db) repo := NewTaskRepository(db)
testutil.SeedLookupData(t, db) testutil.SeedLookupData(t, db)
@@ -681,21 +677,26 @@ func TestKanbanBoard_CategoryPriority_CancelledTakesPrecedence(t *testing.T) {
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC()) board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
require.NoError(t, err) require.NoError(t, err)
// Find cancelled column
var cancelledColumn *models.KanbanColumn
var overdueColumn *models.KanbanColumn var overdueColumn *models.KanbanColumn
foundCancelledColumn := false
for i := range board.Columns { for i := range board.Columns {
if board.Columns[i].Name == "cancelled_tasks" { if board.Columns[i].Name == "cancelled_tasks" {
cancelledColumn = &board.Columns[i] foundCancelledColumn = true
} }
if board.Columns[i].Name == "overdue_tasks" { if board.Columns[i].Name == "overdue_tasks" {
overdueColumn = &board.Columns[i] overdueColumn = &board.Columns[i]
} }
} }
// Task should be in cancelled, not overdue // Cancelled task should be hidden and not appear as overdue
assert.Equal(t, 1, cancelledColumn.Count, "Task should be in cancelled column") assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
require.NotNil(t, overdueColumn, "overdue column should exist")
assert.Equal(t, 0, overdueColumn.Count, "Task should NOT be in overdue column") assert.Equal(t, 0, overdueColumn.Count, "Task should NOT be in overdue column")
totalTasks := 0
for _, col := range board.Columns {
totalTasks += col.Count
}
assert.Equal(t, 0, totalTasks, "cancelled task should be hidden from board")
} }
func TestKanbanBoard_CategoryPriority_CompletedTakesPrecedenceOverInProgress(t *testing.T) { func TestKanbanBoard_CategoryPriority_CompletedTakesPrecedenceOverInProgress(t *testing.T) {
@@ -808,7 +809,7 @@ func TestKanbanBoard_ColumnMetadata(t *testing.T) {
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC()) board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
require.NoError(t, err) require.NoError(t, err)
// Verify all 6 columns exist with correct metadata // Verify all 5 visible columns exist with correct metadata
expectedColumns := []struct { expectedColumns := []struct {
name string name string
displayName string displayName string
@@ -822,10 +823,9 @@ func TestKanbanBoard_ColumnMetadata(t *testing.T) {
{"due_soon_tasks", "Due Soon", "#FF9500", []string{"edit", "complete", "cancel", "mark_in_progress"}, "clock", "Schedule"}, {"due_soon_tasks", "Due Soon", "#FF9500", []string{"edit", "complete", "cancel", "mark_in_progress"}, "clock", "Schedule"},
{"upcoming_tasks", "Upcoming", "#007AFF", []string{"edit", "complete", "cancel", "mark_in_progress"}, "calendar", "Event"}, {"upcoming_tasks", "Upcoming", "#007AFF", []string{"edit", "complete", "cancel", "mark_in_progress"}, "calendar", "Event"},
{"completed_tasks", "Completed", "#34C759", []string{}, "checkmark.circle", "CheckCircle"}, // Completed tasks are read-only (no buttons) {"completed_tasks", "Completed", "#34C759", []string{}, "checkmark.circle", "CheckCircle"}, // Completed tasks are read-only (no buttons)
{"cancelled_tasks", "Cancelled", "#8E8E93", []string{"uncancel", "delete"}, "xmark.circle", "Cancel"},
} }
assert.Len(t, board.Columns, 6, "Board should have 6 columns") assert.Len(t, board.Columns, 5, "Board should have 5 visible columns")
for i, expected := range expectedColumns { for i, expected := range expectedColumns {
col := board.Columns[i] col := board.Columns[i]
@@ -861,26 +861,28 @@ func TestKanbanBoard_MultipleResidences(t *testing.T) {
board, err := repo.GetKanbanDataForMultipleResidences([]uint{residence1.ID, residence2.ID}, 30, time.Now().UTC()) board, err := repo.GetKanbanDataForMultipleResidences([]uint{residence1.ID, residence2.ID}, 30, time.Now().UTC())
require.NoError(t, err) require.NoError(t, err)
// Count total tasks // Count total tasks (cancelled task is intentionally hidden from board)
totalTasks := 0 totalTasks := 0
for _, col := range board.Columns { for _, col := range board.Columns {
totalTasks += col.Count totalTasks += col.Count
} }
assert.Equal(t, 3, totalTasks, "Should have 3 tasks total across both residences") assert.Equal(t, 2, totalTasks, "Should have 2 visible tasks total across both residences")
// Find upcoming and cancelled columns // Find upcoming column and ensure cancelled column is hidden
var upcomingColumn, cancelledColumn *models.KanbanColumn var upcomingColumn *models.KanbanColumn
foundCancelledColumn := false
for i := range board.Columns { for i := range board.Columns {
if board.Columns[i].Name == "upcoming_tasks" { if board.Columns[i].Name == "upcoming_tasks" {
upcomingColumn = &board.Columns[i] upcomingColumn = &board.Columns[i]
} }
if board.Columns[i].Name == "cancelled_tasks" { if board.Columns[i].Name == "cancelled_tasks" {
cancelledColumn = &board.Columns[i] foundCancelledColumn = true
} }
} }
assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
require.NotNil(t, upcomingColumn, "upcoming column should exist")
assert.Equal(t, 2, upcomingColumn.Count, "Should have 2 upcoming tasks") assert.Equal(t, 2, upcomingColumn.Count, "Should have 2 upcoming tasks")
assert.Equal(t, 1, cancelledColumn.Count, "Should have 1 cancelled task")
} }
// === Single-Purpose Function Tests === // === Single-Purpose Function Tests ===
@@ -1562,7 +1564,8 @@ func TestKanbanBoardMatchesSinglePurposeFunctions(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Compare counts // Compare counts
var boardOverdue, boardDueSoon, boardInProgress, boardUpcoming, boardCompleted, boardCancelled int var boardOverdue, boardDueSoon, boardInProgress, boardUpcoming, boardCompleted int
foundCancelledColumn := false
for _, col := range board.Columns { for _, col := range board.Columns {
switch col.Name { switch col.Name {
case "overdue_tasks": case "overdue_tasks":
@@ -1576,7 +1579,7 @@ func TestKanbanBoardMatchesSinglePurposeFunctions(t *testing.T) {
case "completed_tasks": case "completed_tasks":
boardCompleted = col.Count boardCompleted = col.Count
case "cancelled_tasks": case "cancelled_tasks":
boardCancelled = col.Count foundCancelledColumn = true
} }
} }
@@ -1585,7 +1588,8 @@ func TestKanbanBoardMatchesSinglePurposeFunctions(t *testing.T) {
assert.Equal(t, len(inProgress), boardInProgress, "In Progress count mismatch") assert.Equal(t, len(inProgress), boardInProgress, "In Progress count mismatch")
assert.Equal(t, len(upcoming), boardUpcoming, "Upcoming count mismatch") assert.Equal(t, len(upcoming), boardUpcoming, "Upcoming count mismatch")
assert.Equal(t, len(completed), boardCompleted, "Completed count mismatch") assert.Equal(t, len(completed), boardCompleted, "Completed count mismatch")
assert.Equal(t, len(cancelled), boardCancelled, "Cancelled count mismatch") assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
assert.NotEmpty(t, cancelled, "single-purpose cancelled query should still return cancelled tasks")
} }
// === Additional Timezone Tests === // === Additional Timezone Tests ===
@@ -2093,4 +2097,3 @@ func TestConsistency_OverduePredicateVsScopeVsRepo(t *testing.T) {
} }
assert.Equal(t, expectedCount, len(repoTasks), "Overdue task count mismatch") assert.Equal(t, expectedCount, len(repoTasks), "Overdue task count mismatch")
} }

View File

@@ -134,10 +134,12 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
// Initialize Apple auth service // Initialize Apple auth service
appleAuthService := services.NewAppleAuthService(deps.Cache, cfg) appleAuthService := services.NewAppleAuthService(deps.Cache, cfg)
googleAuthService := services.NewGoogleAuthService(deps.Cache, cfg)
// Initialize handlers // Initialize handlers
authHandler := handlers.NewAuthHandler(authService, deps.EmailService, deps.Cache) authHandler := handlers.NewAuthHandler(authService, deps.EmailService, deps.Cache)
authHandler.SetAppleAuthService(appleAuthService) authHandler.SetAppleAuthService(appleAuthService)
authHandler.SetGoogleAuthService(googleAuthService)
userHandler := handlers.NewUserHandler(userService) userHandler := handlers.NewUserHandler(userService)
residenceHandler := handlers.NewResidenceHandler(residenceService, deps.PDFService, deps.EmailService) residenceHandler := handlers.NewResidenceHandler(residenceService, deps.PDFService, deps.EmailService)
taskHandler := handlers.NewTaskHandler(taskService, deps.StorageService) taskHandler := handlers.NewTaskHandler(taskService, deps.StorageService)
@@ -243,6 +245,7 @@ func setupPublicAuthRoutes(api *echo.Group, authHandler *handlers.AuthHandler) {
auth.POST("/verify-reset-code/", authHandler.VerifyResetCode) auth.POST("/verify-reset-code/", authHandler.VerifyResetCode)
auth.POST("/reset-password/", authHandler.ResetPassword) auth.POST("/reset-password/", authHandler.ResetPassword)
auth.POST("/apple-sign-in/", authHandler.AppleSignIn) auth.POST("/apple-sign-in/", authHandler.AppleSignIn)
auth.POST("/google-sign-in/", authHandler.GoogleSignIn)
} }
} }