Overview

This document describes the implementation of the Leave Types API endpoint that allows the frontend to fetch available leave types for display in the leave request form.

Problem Statement

Previously, the leave types were seeded in the database but were not accessible via the API. The frontend’s LeaveRequestForm component was unable to populate the leave type dropdown because:
  1. Missing Backend Endpoint: No dedicated API endpoint existed to fetch leave types
  2. Frontend Calling Wrong Endpoint: The form was attempting to use the leave balances endpoint

Solution

Backend Changes

1. Updated Query Service Interface (internal/modules/leave/application/port/leave_query_ports.go)

Added a new method to the LeaveQueryService interface:
// GetLeaveTypes retrieves all active leave types for an organization
GetLeaveTypes(ctx context.Context, orgID string) ([]*domain.LeaveType, error)

2. Implemented Service Method (internal/modules/leave/application/usecase/leave_query_service.go)

Added leaveTypeRepo field to the service:
type LeaveQueryService struct {
    requestQueryRepo ports.LeaveQueryRepository
    balanceQueryRepo ports.LeaveBalanceQueryRepository
    leaveTypeRepo    ports.LeaveTypeRepository  // NEW
    logger           *logger.Logger
}
Implemented the GetLeaveTypes method:
func (s *LeaveQueryService) GetLeaveTypes(ctx context.Context, orgID string) ([]*domain.LeaveType, error) {
    // Try to get organization-specific leave types first
    leaveTypes, err := s.leaveTypeRepo.FindByOrganization(ctx, orgID)
    if err != nil {
        s.logger.Error("failed to get leave types by organization", zap.Error(err), zap.String("org_id", orgID))
        return nil, err
    }

    // If no organization-specific types found, fall back to all active types
    // This is useful for testing or default organizations
    if len(leaveTypes) == 0 {
        s.logger.Info("no organization-specific leave types found, fetching all active types", zap.String("org_id", orgID))
        leaveTypes, err = s.leaveTypeRepo.FindAllActive(ctx)
        if err != nil {
            s.logger.Error("failed to get all active leave types", zap.Error(err))
            return nil, err
        }
    }

    return leaveTypes, nil
}
Key Features:
  • Attempts to fetch organization-specific leave types first
  • Falls back to all active leave types if none are found for the organization
  • Useful for testing with the default organization (org_default)
  • Proper error handling and logging

3. Updated Module Initialization (internal/modules/leave/module.go)

Modified the NewLeaveQueryService call to include the leaveTypeRepo:
leaveQueryService := services.NewLeaveQueryService(
    leaveRequestQueryRepo,
    leaveBalanceQueryRepo,
    leaveTypeRepo,  // NEW
    c.Logger,
)

4. Updated HTTP Handler (internal/modules/leave/adapter/inbound/http/handler.go)

Modified the GetLeaveTypes handler to use the query service instead of directly accessing the repository:
// GetLeaveTypes handles GET /v1/leave/types
func (h *LeaveHandler) GetLeaveTypes(c echo.Context) error {
    orgID := "org_default" // TODO: Get from context when multi-tenant is implemented

    // Use Query Service for consistent business logic
    leaveTypes, err := h.queryService.GetLeaveTypes(c.Request().Context(), orgID)
    if err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
    }

    return c.JSON(http.StatusOK, leaveTypes)
}
The route is already registered in RegisterRoutes:
leave.GET("/types", h.GetLeaveTypes)

Frontend Integration

The frontend is already correctly configured to use this endpoint: API Client (frontend/apps/platform/src/lib/api.ts):
async getLeaveTypes(token: string) {
  return this.fetch<LeaveType[]>(`/leave/types`, {
    headers: { Authorization: `Bearer ${token}` },
  });
}
Leave Request Form (frontend/apps/platform/src/components/qwik/leave/LeaveRequestForm.tsx):
useVisibleTask$(async () => {
  try {
    const data = await api.getLeaveTypes(token);
    leaveTypes.value = data ?? [];
    isLoading.value = false;
  } catch (err) {
    error.value = "Failed to load leave types";
    isLoading.value = false;
  }
});

API Endpoint

GET /v1/leave/types

Retrieves all active leave types available for the organization. Authentication: Required (Bearer token) Request:
GET /v1/leave/types
Authorization: Bearer <better-auth_token>
Response: 200 OK
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Annual Leave",
    "description": "Paid time off for vacation or personal matters",
    "max_days": 14,
    "requires_approval": true,
    "is_active": true,
    "created_at": "2025-12-23T02:19:04.149Z",
    "updated_at": "2025-12-23T02:19:04.149Z",
    "created_by": "system_seed",
    "updated_by": "system_seed"
  },
  {
    "id": "550e8400-e29b-41d4-a716-446655440001",
    "name": "Sick Leave",
    "description": "Leave for medical reasons or illness",
    "max_days": 14,
    "requires_approval": true,
    "is_active": true,
    "created_at": "2025-12-23T02:19:04.149Z",
    "updated_at": "2025-12-23T02:19:04.149Z",
    "created_by": "system_seed",
    "updated_by": "system_seed"
  }
]
Error Response: 500 Internal Server Error
{
  "message": "error message"
}

Testing

1. Verify Database Has Leave Types

-- Connect to the database and check leave_types table
SELECT id, name, max_days, is_active FROM leave_types;
Expected: Should show several leave types (Annual, Sick, Emergency, Maternity, Paternity, Unpaid Leave)

2. Test Backend Endpoint

Option A: Using cURL (requires valid Better Auth JWT token)
curl -X GET http://localhost:8080/v1/leave/types \
  -H "Authorization: Bearer <your_better-auth_token>"
Option B: Using the Frontend
  1. Start the backend: cd backend && go run cmd/server/main.go
  2. Start the frontend: cd frontend && npm run dev
  3. Navigate to the leave request form
  4. The leave type dropdown should now be populated with the seeded leave types

3. Verify Frontend Integration

  1. Open the browser developer console
  2. Navigate to the leave request form page
  3. Check the Network tab for a request to /v1/leave/types
  4. Verify the response contains the leave types
  5. Confirm the dropdown is populated

Data Model

LeaveType Schema

type LeaveType struct {
    ID               string    `gorm:"primaryKey;type:uuid" json:"id"`
    OrganizationID   string    `gorm:"type:varchar(26)" json:"organization_id"`
    Name             string    `gorm:"type:varchar(255);not null" json:"name"`
    Description      string    `gorm:"type:text" json:"description"`
    Code             string    `gorm:"type:varchar(50)" json:"code"`
    MaxDays          float64   `gorm:"type:decimal(10,2);not null" json:"max_days"`
    RequiresApproval bool      `gorm:"not null;default:true" json:"requires_approval"`
    IsActive         bool      `gorm:"not null;default:true" json:"is_active"`
    CreatedAt        time.Time `gorm:"type:timestamptz" json:"created_at"`
    UpdatedAt        time.Time `gorm:"type:timestamptz" json:"updated_at"`
    CreatedBy        string    `gorm:"type:varchar(26)" json:"created_by"`
    UpdatedBy        string    `gorm:"type:varchar(26)" json:"updated_by"`
}
Note: The domain entity in internal/modules/leave/domain/entity/leave.go defines the core logic, while the GORM model in persistence/postgresql/models.go handles the database mapping.

Frontend TypeScript Interface

export interface LeaveType {
  id: string;
  name: string;
  description?: string;
  max_days: number;
  requires_approval: boolean;
  is_active: boolean;
}

Multi-Tenancy Support

The implementation is designed with multi-tenancy in mind:
  1. Organization Filtering: The service first attempts to fetch organization-specific leave types using FindByOrganization(orgID)
  2. Fallback Mechanism: If no organization-specific types exist, it falls back to FindAllActive() for backward compatibility
  3. Future Enhancement: When multi-tenancy is fully implemented, the orgID will be extracted from the authenticated user’s context instead of using "org_default"

Future Enhancements

  1. Admin Panel: Build an admin interface to manage leave types (create, update, deactivate)
  2. Organization Context: Extract orgID from authenticated user context
  3. Caching: Implement caching for leave types to reduce database queries
  4. Pagination: Add pagination support if the number of leave types grows significantly
  5. Leave Type Categories: Group leave types by category (paid, unpaid, statutory, etc.)

Backend

  • internal/modules/leave/application/port/leave_query_ports.go - Service interface
  • internal/modules/leave/application/usecase/leave_query_service.go - Service implementation
  • internal/modules/leave/module.go - Module initialization
  • internal/modules/leave/adapter/inbound/http/handler.go - HTTP handler
  • internal/modules/leave/adapter/outbound/persistence/postgresql/leave_type_repository.go - Repository
  • internal/modules/leave/domain/leave.go - Domain model

Frontend

  • frontend/apps/platform/src/lib/api.ts - API client
  • frontend/apps/platform/src/lib/types.ts - TypeScript types
  • frontend/apps/platform/src/components/qwik/leave/LeaveRequestForm.tsx - Form component

Troubleshooting

Leave types not appearing in the frontend

  1. Check backend is running: lsof -ti:8080 should return a process ID
  2. Verify database has data: Run the SQL query above
  3. Check authentication: Ensure you’re logged in with a valid Better Auth JWT token
  4. Check browser console: Look for any API errors
  5. Verify API response: Check the Network tab in browser DevTools

Backend errors

  1. Check logs: Backend logs will show any errors in fetching leave types
  2. Verify database connection: Ensure PostgreSQL is running and accessible
  3. Check repository implementation: Verify FindByOrganization and FindAllActive methods work correctly

Summary

The Leave Types API is now fully functional and integrated with the frontend. Users can:
  • View all active leave types in the leave request form dropdown
  • See leave type details (name, max days)
  • Submit leave requests with the selected leave type
The implementation follows CQRS principles, uses the query service layer for consistency, and is designed to support future multi-tenancy requirements.