Bluewoo HRMS
Micro-Step Build PlanBuilding Blocks

Phase 10.5: Pre-Launch Critical Features

Email notifications, onboarding/offboarding workflows, alternate approvers

Phase 10.5: Pre-Launch Critical Features

Goal: Address critical workflow gaps identified before production launch.

AttributeValue
Steps01-10
Estimated Time17-24 hours
DependenciesPhase 10 complete (Platform Admin)
Completion GateEmail notifications working, onboarding checklist functional, alternate approvers configured

Phase Context (READ FIRST)

What This Phase Accomplishes

  • Email Notifications: Automated emails for time-off requests and approvals
  • Welcome Emails: Email sent to new employees when account is created
  • Onboarding Checklists: HR can create templates, assign to new hires, track progress
  • Offboarding Workflow: Checklist for terminating employees (access revocation, equipment return, exit interview)
  • Alternate Approvers: Configure backup approvers when managers are unavailable

What This Phase Does NOT Include

  • Push notifications (future Phase 11+)
  • In-app notification center (future)
  • Automated policy assignment (TO-09 remains in Phase 12)
  • Complex escalation chains
  • Email verification for registration domains

Target User Stories

IDStoryPriority
NTF-01Manager receives email when time-off request submittedCRITICAL
NTF-02Employee receives email when time-off approved/rejectedCRITICAL
NTF-03New employee receives welcome emailHIGH
ONB-01HR creates onboarding checklist templatesHIGH
ONB-02New employee sees their onboarding checklistHIGH
ONB-03HR tracks onboarding completion progressHIGH
OFF-01HR initiates offboarding workflowHIGH
OFF-02HR tracks offboarding checklist completionHIGH
TO-07HR configures alternate approversHIGH

Why This Phase Exists

Gap analysis identified these as critical missing workflows compared to industry HRMS standards (BambooHR, Workday, Personio):

  1. No notification system: Users have no way to know when requests are submitted/approved
  2. No onboarding structure: New hires have no guided experience
  3. No offboarding checklist: High risk of missed steps during termination
  4. Approval bottlenecks: Requests block when managers are on vacation

Database Schema Additions

Onboarding/Offboarding Models

model OnboardingChecklist {
  id          String   @id @default(cuid())
  tenantId    String
  name        String
  description String?
  type        String   @default("ONBOARDING") // "ONBOARDING" | "OFFBOARDING"
  isDefault   Boolean  @default(false)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  tenant      Tenant   @relation(fields: [tenantId], references: [id])
  items       ChecklistItem[]
  assignments EmployeeChecklist[]

  @@index([tenantId])
  @@map("onboarding_checklists")
}

model ChecklistItem {
  id          String   @id @default(cuid())
  checklistId String
  title       String
  description String?
  dueInDays   Int?     // Days from start date (onboarding) or end date (offboarding)
  assignedTo  String?  // "EMPLOYEE" | "MANAGER" | "HR"
  order       Int

  checklist   OnboardingChecklist @relation(fields: [checklistId], references: [id], onDelete: Cascade)

  @@index([checklistId])
  @@map("checklist_items")
}

model EmployeeChecklist {
  id          String   @id @default(cuid())
  employeeId  String
  checklistId String
  startedAt   DateTime @default(now())
  completedAt DateTime?

  employee    Employee @relation(fields: [employeeId], references: [id])
  checklist   OnboardingChecklist @relation(fields: [checklistId], references: [id])
  progress    ChecklistProgress[]

  @@index([employeeId])
  @@index([checklistId])
  @@map("employee_checklists")
}

model ChecklistProgress {
  id                  String   @id @default(cuid())
  employeeChecklistId String
  itemId              String
  completedAt         DateTime?
  completedBy         String?  // User ID who marked complete
  notes               String?

  employeeChecklist   EmployeeChecklist @relation(fields: [employeeChecklistId], references: [id], onDelete: Cascade)

  @@index([employeeChecklistId])
  @@map("checklist_progress")
}

Alternate Approver Model

model AlternateApprover {
  id            String    @id @default(cuid())
  tenantId      String
  managerId     String    // Primary manager who delegates
  alternateId   String    // Backup approver
  startDate     DateTime?
  endDate       DateTime?
  isActive      Boolean   @default(true)
  createdAt     DateTime  @default(now())

  tenant        Tenant    @relation(fields: [tenantId], references: [id])
  manager       Employee  @relation("PrimaryManager", fields: [managerId], references: [id])
  alternate     Employee  @relation("AlternateApprover", fields: [alternateId], references: [id])

  @@index([tenantId])
  @@index([managerId])
  @@map("alternate_approvers")
}

Step 01: Setup Email Service (Resend)

Input

  • Phase 10 complete

Constraints

  • Use Resend (not SendGrid) - simpler API, better pricing for startups
  • Configure via environment variables
  • No hard-coded email addresses

Task

Install Resend and create email service:

// packages/emails/src/resend.ts
import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

export interface SendEmailOptions {
  to: string | string[];
  subject: string;
  html: string;
  from?: string;
}

export async function sendEmail(options: SendEmailOptions) {
  const from = options.from || process.env.EMAIL_FROM || 'noreply@hrms.app';

  return resend.emails.send({
    from,
    to: options.to,
    subject: options.subject,
    html: options.html,
  });
}

Environment variables:

RESEND_API_KEY=re_xxxxx
EMAIL_FROM=noreply@yourdomain.com

Completion Gate

  • Resend installed: npm install resend
  • Email service module exists
  • Test email sends successfully

Step 02: Create Email Templates

Input

  • Step 01 complete

Constraints

  • Use React Email for templates (type-safe, maintainable)
  • Keep templates simple and clean
  • Include company logo placeholder

Task

Create email templates:

// packages/emails/src/templates/time-off-request.tsx
import { Html, Head, Body, Container, Text, Button, Hr } from '@react-email/components';

interface TimeOffRequestEmailProps {
  employeeName: string;
  leaveType: string;
  startDate: string;
  endDate: string;
  approveUrl: string;
  rejectUrl: string;
}

export function TimeOffRequestEmail({
  employeeName,
  leaveType,
  startDate,
  endDate,
  approveUrl,
  rejectUrl,
}: TimeOffRequestEmailProps) {
  return (
    <Html>
      <Head />
      <Body style={main}>
        <Container style={container}>
          <Text style={heading}>New Time-Off Request</Text>
          <Text style={paragraph}>
            <strong>{employeeName}</strong> has requested time off:
          </Text>
          <Text style={details}>
            Type: {leaveType}<br />
            From: {startDate}<br />
            To: {endDate}
          </Text>
          <Hr style={hr} />
          <Button style={buttonApprove} href={approveUrl}>
            Approve
          </Button>
          <Button style={buttonReject} href={rejectUrl}>
            Reject
          </Button>
        </Container>
      </Body>
    </Html>
  );
}
// packages/emails/src/templates/time-off-decision.tsx
export function TimeOffDecisionEmail({
  decision,  // 'APPROVED' | 'REJECTED'
  leaveType,
  startDate,
  endDate,
  managerName,
  comment,
}: TimeOffDecisionEmailProps) {
  return (
    <Html>
      <Head />
      <Body style={main}>
        <Container style={container}>
          <Text style={heading}>
            Time-Off Request {decision === 'APPROVED' ? 'Approved' : 'Rejected'}
          </Text>
          <Text style={paragraph}>
            Your {leaveType} request for {startDate} to {endDate} has been
            <strong>{decision === 'APPROVED' ? ' approved' : ' rejected'}</strong>
            {managerName && ` by ${managerName}`}.
          </Text>
          {comment && (
            <Text style={details}>
              Comment: {comment}
            </Text>
          )}
        </Container>
      </Body>
    </Html>
  );
}
// packages/emails/src/templates/welcome.tsx
export function WelcomeEmail({
  employeeName,
  companyName,
  loginUrl,
}: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Body style={main}>
        <Container style={container}>
          <Text style={heading}>Welcome to {companyName}!</Text>
          <Text style={paragraph}>
            Hi {employeeName},
          </Text>
          <Text style={paragraph}>
            Your account has been created. You can now access the HRMS system
            to view your profile, request time off, and more.
          </Text>
          <Button style={button} href={loginUrl}>
            Log In
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

Completion Gate

  • 3 email templates created (time-off-request, time-off-decision, welcome)
  • Templates use consistent styling
  • Templates have all required props typed

Step 03: Time-Off Notification Triggers

Input

  • Step 02 complete

Constraints

  • Trigger emails asynchronously (don't block request/response)
  • Handle email failures gracefully (log, don't throw)
  • Respect user preferences if notification settings exist

Task

Add email triggers to time-off service:

// apps/api/src/modules/timeoff/timeoff.service.ts

async createRequest(dto: CreateTimeOffRequestDto, employeeId: string) {
  const request = await this.prisma.timeOffRequest.create({
    data: { ...dto, employeeId },
    include: { employee: true },
  });

  // Send notification to manager (async, don't await)
  this.notifyManager(request).catch((err) => {
    console.error('Failed to send manager notification:', err);
  });

  return request;
}

private async notifyManager(request: TimeOffRequest & { employee: Employee }) {
  const manager = await this.findApprover(request.employee.id);

  if (!manager?.email) return;

  const html = render(TimeOffRequestEmail({
    employeeName: request.employee.name,
    leaveType: request.leaveType,
    startDate: format(request.startDate, 'PPP'),
    endDate: format(request.endDate, 'PPP'),
    approveUrl: `${process.env.APP_URL}/timeoff/approve/${request.id}`,
    rejectUrl: `${process.env.APP_URL}/timeoff/reject/${request.id}`,
  }));

  await sendEmail({
    to: manager.email,
    subject: `Time-Off Request from ${request.employee.name}`,
    html,
  });
}

async approveRequest(id: string, approverId: string, comment?: string) {
  const request = await this.prisma.timeOffRequest.update({
    where: { id },
    data: {
      status: 'APPROVED',
      approvedBy: approverId,
      approverComment: comment,
    },
    include: { employee: { include: { user: true } } },
  });

  // Notify employee
  this.notifyEmployee(request, 'APPROVED', comment).catch((err) => {
    console.error('Failed to send employee notification:', err);
  });

  return request;
}

async rejectRequest(id: string, approverId: string, comment?: string) {
  const request = await this.prisma.timeOffRequest.update({
    where: { id },
    data: {
      status: 'REJECTED',
      approvedBy: approverId,
      approverComment: comment,
    },
    include: { employee: { include: { user: true } } },
  });

  // Notify employee
  this.notifyEmployee(request, 'REJECTED', comment).catch((err) => {
    console.error('Failed to send employee notification:', err);
  });

  return request;
}

Completion Gate

  • Email sent when request created (to manager)
  • Email sent when request approved (to employee)
  • Email sent when request rejected (to employee)
  • Failures logged but don't break the workflow

Step 04: Onboarding Checklist Model Migration

Input

  • Step 03 complete

Constraints

  • Follow existing Prisma migration patterns
  • Add indexes for query performance

Task

Run Prisma migration:

npx prisma migrate dev --name add_onboarding_checklists

Verify with seed data:

// packages/database/prisma/seed.ts

// Add default onboarding checklist
const defaultChecklist = await prisma.onboardingChecklist.create({
  data: {
    tenantId: tenant.id,
    name: 'Standard Onboarding',
    description: 'Default checklist for new employees',
    type: 'ONBOARDING',
    isDefault: true,
    items: {
      create: [
        { title: 'Complete personal information', assignedTo: 'EMPLOYEE', order: 1 },
        { title: 'Review company handbook', assignedTo: 'EMPLOYEE', order: 2 },
        { title: 'Set up workstation', assignedTo: 'HR', order: 3, dueInDays: 1 },
        { title: 'Meet with manager', assignedTo: 'MANAGER', order: 4, dueInDays: 3 },
        { title: 'Complete IT security training', assignedTo: 'EMPLOYEE', order: 5, dueInDays: 7 },
      ],
    },
  },
});

Completion Gate

  • Migration runs successfully
  • All 4 new models exist (OnboardingChecklist, ChecklistItem, EmployeeChecklist, ChecklistProgress)
  • Seed data creates default checklist

Step 05: Onboarding Checklist API

Input

  • Step 04 complete

Constraints

  • Tenant-scoped (all endpoints require X-Tenant-ID)
  • HR_ADMIN role required for template CRUD
  • Employees can view their own checklists

Task

Create checklist controller:

// apps/api/src/modules/onboarding/onboarding.controller.ts
@Controller('api/v1/checklists')
@UseGuards(TenantGuard)
export class OnboardingController {
  constructor(private readonly service: OnboardingService) {}

  // Template management (HR only)
  @Post()
  @Roles('HR_ADMIN')
  async createTemplate(@Body() dto: CreateChecklistDto, @TenantId() tenantId: string) {
    return this.service.createTemplate(tenantId, dto);
  }

  @Get()
  async listTemplates(@TenantId() tenantId: string, @Query() query: ListChecklistsDto) {
    return this.service.listTemplates(tenantId, query);
  }

  @Get(':id')
  async getTemplate(@Param('id') id: string, @TenantId() tenantId: string) {
    return this.service.getTemplate(id, tenantId);
  }

  @Patch(':id')
  @Roles('HR_ADMIN')
  async updateTemplate(
    @Param('id') id: string,
    @Body() dto: UpdateChecklistDto,
    @TenantId() tenantId: string
  ) {
    return this.service.updateTemplate(id, tenantId, dto);
  }

  @Delete(':id')
  @Roles('HR_ADMIN')
  async deleteTemplate(@Param('id') id: string, @TenantId() tenantId: string) {
    return this.service.deleteTemplate(id, tenantId);
  }
}
// Employee-specific checklist endpoints
@Controller('api/v1/employees/:employeeId/onboarding')
@UseGuards(TenantGuard)
export class EmployeeOnboardingController {
  // Assign checklist to employee (HR only)
  @Post()
  @Roles('HR_ADMIN')
  async assignChecklist(
    @Param('employeeId') employeeId: string,
    @Body() dto: AssignChecklistDto
  ) {
    return this.service.assignChecklist(employeeId, dto.checklistId);
  }

  // Get employee's checklist progress (self or HR)
  @Get()
  async getProgress(@Param('employeeId') employeeId: string, @CurrentUser() user: User) {
    // Verify access (self or HR/Manager)
    return this.service.getEmployeeProgress(employeeId);
  }

  // Mark item complete
  @Patch(':itemId')
  async markComplete(
    @Param('employeeId') employeeId: string,
    @Param('itemId') itemId: string,
    @Body() dto: CompleteItemDto,
    @CurrentUser() user: User
  ) {
    return this.service.markItemComplete(employeeId, itemId, user.id, dto.notes);
  }
}

API Endpoints:

MethodPathRoleDescription
POST/api/v1/checklistsHR_ADMINCreate checklist template
GET/api/v1/checklistsAnyList templates
GET/api/v1/checklists/:idAnyGet template details
PATCH/api/v1/checklists/:idHR_ADMINUpdate template
DELETE/api/v1/checklists/:idHR_ADMINDelete template
POST/api/v1/employees/:id/onboardingHR_ADMINAssign checklist
GET/api/v1/employees/:id/onboardingSelf/HRGet progress
PATCH/api/v1/employees/:id/onboarding/:itemIdAssigneeMark item complete

Completion Gate

  • All 8 endpoints work
  • Role-based access enforced
  • Tenant isolation verified

Step 06: Onboarding UI Components

Input

  • Step 05 complete

Constraints

  • Follow design system (gradient cards, rounded-2xl, etc.)
  • Use React Query for data fetching
  • Mobile-responsive

Task

1. Checklist Template Manager (HR)

// apps/web/src/app/(dashboard)/settings/onboarding/page.tsx
export default function OnboardingSettingsPage() {
  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-2xl font-bold">Onboarding Checklists</h1>
          <p className="text-muted-foreground">
            Create templates for new employee onboarding
          </p>
        </div>
        <CreateChecklistDialog />
      </div>

      <ChecklistTemplateList />
    </div>
  );
}

2. Employee Onboarding View

// apps/web/src/components/onboarding/EmployeeChecklist.tsx
export function EmployeeChecklist({ employeeId }: { employeeId: string }) {
  const { data } = useQuery({
    queryKey: ['employee', employeeId, 'onboarding'],
    queryFn: () => getEmployeeOnboarding(employeeId),
  });

  if (!data) return <EmptyState title="No checklist assigned" />;

  const completedCount = data.progress.filter(p => p.completedAt).length;
  const totalCount = data.checklist.items.length;
  const percentComplete = Math.round((completedCount / totalCount) * 100);

  return (
    <ContentCard>
      <div className="flex items-center justify-between mb-6">
        <div>
          <h3 className="text-lg font-semibold">{data.checklist.name}</h3>
          <p className="text-sm text-muted-foreground">
            {completedCount} of {totalCount} tasks complete
          </p>
        </div>
        <div className="text-2xl font-bold text-primary">
          {percentComplete}%
        </div>
      </div>

      <Progress value={percentComplete} className="h-2 mb-6" />

      <div className="space-y-3">
        {data.checklist.items.map((item) => {
          const progress = data.progress.find(p => p.itemId === item.id);
          return (
            <ChecklistItemRow
              key={item.id}
              item={item}
              progress={progress}
              onComplete={() => handleComplete(item.id)}
            />
          );
        })}
      </div>
    </ContentCard>
  );
}

3. HR Onboarding Dashboard

// apps/web/src/app/(dashboard)/hr/onboarding/page.tsx
export default function HROnboardingDashboard() {
  const { data: inProgress } = useQuery({
    queryKey: ['onboarding', 'in-progress'],
    queryFn: getOnboardingInProgress,
  });

  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-bold">Onboarding Progress</h1>

      <div className="grid grid-cols-3 gap-4">
        <StatsCard
          title="Active Onboardings"
          value={inProgress?.active || 0}
          variant="blue"
        />
        <StatsCard
          title="Completed This Month"
          value={inProgress?.completedThisMonth || 0}
          variant="emerald"
        />
        <StatsCard
          title="Overdue Tasks"
          value={inProgress?.overdue || 0}
          variant="amber"
        />
      </div>

      <ContentCard>
        <h2 className="text-lg font-semibold mb-4">In Progress</h2>
        <OnboardingTable data={inProgress?.employees || []} />
      </ContentCard>
    </div>
  );
}

Completion Gate

  • Checklist template management works
  • Employee can view and complete their checklist
  • HR can track all onboarding progress
  • Progress percentage calculated correctly
  • Design system followed

Step 07: Offboarding Workflow

Input

  • Step 06 complete

Constraints

  • Reuse checklist infrastructure (type: "OFFBOARDING")
  • Trigger when employee status changes to TERMINATED
  • Include standard items: access revocation, equipment return, exit interview

Task

Create default offboarding template:

// Seed data
const offboardingChecklist = await prisma.onboardingChecklist.create({
  data: {
    tenantId: tenant.id,
    name: 'Standard Offboarding',
    description: 'Checklist for departing employees',
    type: 'OFFBOARDING',
    isDefault: true,
    items: {
      create: [
        { title: 'Revoke system access', assignedTo: 'HR', order: 1, dueInDays: 0 },
        { title: 'Collect company equipment', assignedTo: 'HR', order: 2, dueInDays: 1 },
        { title: 'Transfer knowledge/documents', assignedTo: 'EMPLOYEE', order: 3, dueInDays: -3 },
        { title: 'Conduct exit interview', assignedTo: 'HR', order: 4, dueInDays: -1 },
        { title: 'Process final paycheck', assignedTo: 'HR', order: 5, dueInDays: 7 },
        { title: 'Update org chart', assignedTo: 'HR', order: 6, dueInDays: 1 },
      ],
    },
  },
});

Auto-assign offboarding when employee terminated:

// apps/api/src/modules/employee/employee.service.ts
async updateEmployee(id: string, dto: UpdateEmployeeDto, tenantId: string) {
  const employee = await this.prisma.employee.findUnique({ where: { id } });

  // Check if status is changing to TERMINATED
  if (dto.status === 'TERMINATED' && employee.status !== 'TERMINATED') {
    // Auto-assign default offboarding checklist
    await this.onboardingService.assignDefaultOffboarding(id, tenantId);
  }

  return this.prisma.employee.update({
    where: { id },
    data: dto,
  });
}

Completion Gate

  • Default offboarding template exists
  • Offboarding auto-assigned when employee terminated
  • HR can view offboarding progress
  • Offboarding items have negative dueInDays (before end date)

Step 08: Alternate Approvers Migration

Input

  • Step 07 complete

Task

Run migration for AlternateApprover model:

npx prisma migrate dev --name add_alternate_approvers

Completion Gate

  • AlternateApprover table exists
  • Indexes created for performance

Step 09: Alternate Approvers Implementation

Input

  • Step 08 complete

Constraints

  • HR_ADMIN can configure alternate approvers
  • Time-based delegation (startDate/endDate optional)
  • Approval logic checks for alternates when manager unavailable

Task

1. API Endpoints:

@Controller('api/v1/alternate-approvers')
@UseGuards(TenantGuard)
@Roles('HR_ADMIN')
export class AlternateApproverController {
  @Post()
  async create(@Body() dto: CreateAlternateDto, @TenantId() tenantId: string) {
    return this.service.create(tenantId, dto);
  }

  @Get()
  async list(@TenantId() tenantId: string) {
    return this.service.list(tenantId);
  }

  @Delete(':id')
  async remove(@Param('id') id: string, @TenantId() tenantId: string) {
    return this.service.remove(id, tenantId);
  }
}

2. Update Time-Off Approval Logic:

// apps/api/src/modules/timeoff/timeoff.service.ts

async findApprover(employeeId: string): Promise<Employee | null> {
  const employee = await this.prisma.employee.findUnique({
    where: { id: employeeId },
    include: { primaryManager: true },
  });

  if (!employee?.primaryManager) return null;

  const manager = employee.primaryManager;

  // Check if manager is on leave
  const managerOnLeave = await this.isEmployeeOnLeave(manager.id);

  if (managerOnLeave) {
    // Find alternate approver
    const alternate = await this.prisma.alternateApprover.findFirst({
      where: {
        managerId: manager.id,
        isActive: true,
        OR: [
          { startDate: null, endDate: null },
          {
            startDate: { lte: new Date() },
            endDate: { gte: new Date() },
          },
        ],
      },
      include: { alternate: true },
    });

    if (alternate) {
      return alternate.alternate;
    }
  }

  return manager;
}

private async isEmployeeOnLeave(employeeId: string): Promise<boolean> {
  const today = new Date();

  const activeLeave = await this.prisma.timeOffRequest.findFirst({
    where: {
      employeeId,
      status: 'APPROVED',
      startDate: { lte: today },
      endDate: { gte: today },
    },
  });

  return !!activeLeave;
}

3. UI for Managing Alternates:

// apps/web/src/app/(dashboard)/settings/approvers/page.tsx
export default function AlternateApproversPage() {
  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-2xl font-bold">Alternate Approvers</h1>
          <p className="text-muted-foreground">
            Configure backup approvers for when managers are unavailable
          </p>
        </div>
        <CreateAlternateDialog />
      </div>

      <AlternateApproversList />
    </div>
  );
}

Completion Gate

  • HR can create/delete alternate approver assignments
  • Time-off requests route to alternate when manager on leave
  • Date-based delegation works (startDate/endDate)
  • UI shows current alternate approver assignments

Step 10: Integration Testing

Input

  • All previous steps complete

Task

Create integration tests for new features:

// Test: Email notifications
describe('Time-Off Email Notifications', () => {
  it('should send email to manager on request creation', async () => {
    // Mock Resend
    const sendSpy = jest.spyOn(resend.emails, 'send');

    await createTimeOffRequest(employeeId, requestDto);

    expect(sendSpy).toHaveBeenCalledWith(
      expect.objectContaining({
        to: manager.email,
        subject: expect.stringContaining('Time-Off Request'),
      })
    );
  });

  it('should send email to employee on approval', async () => {
    const sendSpy = jest.spyOn(resend.emails, 'send');

    await approveTimeOffRequest(requestId, managerId);

    expect(sendSpy).toHaveBeenCalledWith(
      expect.objectContaining({
        to: employee.email,
        subject: expect.stringContaining('Approved'),
      })
    );
  });
});

// Test: Alternate approvers
describe('Alternate Approvers', () => {
  it('should route to alternate when manager is on leave', async () => {
    // Setup: Manager on approved leave today
    await createApprovedLeave(managerId, today, today);

    // Setup: Alternate approver configured
    await createAlternateApprover(managerId, alternateId);

    // Create time-off request
    await createTimeOffRequest(employeeId, requestDto);

    // Verify notification sent to alternate
    expect(sendSpy).toHaveBeenCalledWith(
      expect.objectContaining({
        to: alternate.email,
      })
    );
  });
});

// Test: Onboarding checklist
describe('Onboarding Checklist', () => {
  it('should track completion progress', async () => {
    // Assign checklist
    await assignChecklist(employeeId, checklistId);

    // Mark first item complete
    await markItemComplete(employeeId, item1Id);

    // Check progress
    const progress = await getEmployeeProgress(employeeId);
    expect(progress.completedCount).toBe(1);
    expect(progress.percentComplete).toBe(20); // 1/5 items
  });
});

Completion Gate

  • Email notification tests pass
  • Onboarding checklist tests pass
  • Alternate approver tests pass
  • Integration test suite runs successfully

Quick Reference

New API Endpoints

MethodPathGuardDescription
POST/api/v1/checklistsTenantGuard, HR_ADMINCreate checklist template
GET/api/v1/checklistsTenantGuardList templates
GET/api/v1/checklists/:idTenantGuardGet template
PATCH/api/v1/checklists/:idTenantGuard, HR_ADMINUpdate template
DELETE/api/v1/checklists/:idTenantGuard, HR_ADMINDelete template
POST/api/v1/employees/:id/onboardingTenantGuard, HR_ADMINAssign checklist
GET/api/v1/employees/:id/onboardingTenantGuardGet progress
PATCH/api/v1/employees/:id/onboarding/:itemIdTenantGuardMark complete
POST/api/v1/alternate-approversTenantGuard, HR_ADMINCreate alternate
GET/api/v1/alternate-approversTenantGuard, HR_ADMINList alternates
DELETE/api/v1/alternate-approvers/:idTenantGuard, HR_ADMINRemove alternate

New Pages

PathDescription
/settings/onboardingChecklist template management
/hr/onboardingHR onboarding dashboard
/settings/approversAlternate approver configuration
/my/onboardingEmployee's own checklist

File Structure

apps/
├── api/src/modules/
│   ├── onboarding/
│   │   ├── onboarding.module.ts
│   │   ├── onboarding.controller.ts
│   │   └── onboarding.service.ts
│   └── alternate-approvers/
│       ├── alternate-approvers.module.ts
│       ├── alternate-approvers.controller.ts
│       └── alternate-approvers.service.ts
├── web/src/
│   ├── app/(dashboard)/
│   │   ├── settings/onboarding/
│   │   ├── settings/approvers/
│   │   └── hr/onboarding/
│   └── components/onboarding/
│       ├── ChecklistTemplateList.tsx
│       ├── EmployeeChecklist.tsx
│       └── CreateChecklistDialog.tsx
└── packages/emails/
    ├── src/resend.ts
    └── src/templates/
        ├── time-off-request.tsx
        ├── time-off-decision.tsx
        └── welcome.tsx

Success Criteria

After Phase 10.5 completion:

FeatureVerification
Email: Time-off requestManager receives email when employee submits request
Email: Time-off decisionEmployee receives email when request approved/rejected
Email: WelcomeNew employee receives welcome email on account creation
Onboarding: TemplatesHR can create/edit onboarding checklist templates
Onboarding: AssignmentHR can assign checklist to new employee
Onboarding: ProgressEmployee can view and complete checklist items
Onboarding: TrackingHR can see all employees' onboarding progress
Offboarding: Auto-assignOffboarding checklist auto-assigned when employee terminated
Offboarding: TrackingHR can track offboarding completion
Alternate: ConfigurationHR can configure backup approvers
Alternate: RoutingRequests route to alternate when manager on leave

Next Phase

After Phase 10.5, proceed to:


Phase Completion Checklist (MANDATORY)

BEFORE LAUNCHING

Complete ALL items before proceeding to production. Do NOT skip any step.

1. Gate Verification

  • All step gates passed
  • Email notifications working
  • Onboarding checklists functional
  • Alternate approvers working
  • All core features tested

2. Update PROJECT_STATE.md

- Mark Phase 10.5 as COMPLETED with timestamp
- Update "Current Phase" to PRODUCTION
- Add session log entry
- Document final locked files

3. Update WHAT_EXISTS.md

## Complete Inventory
- All database models documented
- All API endpoints documented
- All frontend routes documented
- All patterns documented

4. Git Tag & Commit

git add PROJECT_STATE.md WHAT_EXISTS.md
git commit -m "chore: complete Phase 10.5 - Pre-Launch"
git tag phase-10.5-pre-launch
git tag v1.0.0-mvp

MVP Code Complete

All code phases completed. System is ready for production deployment.

Next Step: Phase 11: Production Deployment

See also: Deployment Guide for detailed infrastructure documentation.


Last Updated: 2025-11-30