Skip to content

getAvailableSlots added#4

Open
chakrihacker wants to merge 1 commit into
mainfrom
feat/get-slots
Open

getAvailableSlots added#4
chakrihacker wants to merge 1 commit into
mainfrom
feat/get-slots

Conversation

@chakrihacker

@chakrihacker chakrihacker commented Nov 12, 2025

Copy link
Copy Markdown
Contributor

User description

Summary by Sourcery

Implement customer booking availability retrieval, enhance professional scheduling by migrating to dayIndex and handling location updates, introduce a slot categorization utility, expand documentation, and apply a database migration for availability exceptions.

New Features:

  • Add customer.getAvailableSlots endpoint to fetch and categorize available booking slots.
  • Create ProfessionalAvailabilityException table to support per-date availability overrides.

Enhancements:

  • Refactor professional availability procedures to use numeric dayIndex and prevent duplicate entries.
  • Persist professional baseLocation when editing profile with provided coordinates.
  • Improve professional.getProfile to preserve and rethrow oRPC errors.
  • Introduce categorizeTimeSlots utility to group time slots into morning, afternoon, and evening.

Documentation:

  • Add comprehensive API reference, testing guide, architecture overview, and README documentation.

CodeAnt-AI Description

Customer can fetch available booking slots for a service near a location

What Changed

  • Added customer.getAvailableSlots: returns available 30‑minute slots for today + next 3 days for a given service and location, grouped into morning/afternoon/evening and also provided per-duration slot options (1h, 1.5h, 2h, etc.).
  • Slot results exclude professionals who are not approved, not available, or outside their service radius and remove times that overlap confirmed/in-progress bookings so returned slots are actually bookable.
  • Professional profile edits now persist geographic base location when latitude/longitude are supplied, improving proximity-based matching; availability APIs now use a numeric weekday index (dayIndex) and reject duplicate availability entries.
  • Database and schema now support per-date availability exceptions and full-day-off/breaks for professionals; API docs, testing guide, and architecture docs added/expanded.

Impact

✅ More accurate local matches
✅ Fewer scheduling conflicts
✅ Fewer duplicate availability entries

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

@sourcery-ai

sourcery-ai Bot commented Nov 12, 2025

Copy link
Copy Markdown

Reviewer's Guide

Adds a new getAvailableSlots RPC in the customer router to compute and categorize available time slots over upcoming days based on location and existing bookings; enhances the professional router with baseLocation updates, refined availability inputs using dayIndex and duplicate checks; introduces a time-slot categorization util with dayjs, updates dependencies; includes a Prisma migration for availability improvements and delivers comprehensive project documentation.

Sequence diagram for getAvailableSlots RPC in customer router

sequenceDiagram
actor Customer
participant API_Server
participant Prisma_DB
participant "categorizeTimeSlots()"
Customer->>API_Server: Call getAvailableSlots(serviceId, location)
API_Server->>Prisma_DB: Query professionals by serviceId, location, approvalStatus
API_Server->>Prisma_DB: For each of next 4 days, get professional availabilities
API_Server->>Prisma_DB: Get existing bookings for those professionals on each day
API_Server->>API_Server: Filter out slots overlapping with bookings
API_Server->>"categorizeTimeSlots()": Categorize available slots by time of day
API_Server-->>Customer: Return grouped available slots by date and duration
Loading

ER diagram for ProfessionalAvailability and ProfessionalAvailabilityException

erDiagram
PROFESSIONAL ||--o{ PROFESSIONAL_AVAILABILITY : has
PROFESSIONAL ||--o{ PROFESSIONAL_AVAILABILITY_EXCEPTION : has
PROFESSIONAL_AVAILABILITY {
  professionalId text
  dayIndex int
  startTime text
  endTime text
  isAvailable boolean
  isFullDayOff boolean
  breakSlots jsonb
}
PROFESSIONAL_AVAILABILITY_EXCEPTION {
  _id text
  professionalId text
  date timestamp
  isAvailable boolean
  reason text
  startTime text
  endTime text
  createdAt timestamp
  updatedAt timestamp
}
Loading

Class diagram for ProfessionalAvailability and ProfessionalAvailabilityException changes

classDiagram
class ProfessionalAvailability {
  +String professionalId
  +Integer dayIndex
  +String startTime
  +String endTime
  +Boolean isAvailable
  +Boolean isFullDayOff
  +Json breakSlots
}
class ProfessionalAvailabilityException {
  +String _id
  +String professionalId
  +Date date
  +Boolean isAvailable
  +String reason
  +String startTime
  +String endTime
  +Date createdAt
  +Date updatedAt
}
ProfessionalAvailabilityException --> ProfessionalAvailability: professionalId
Loading

File-Level Changes

Change Details Files
Add getAvailableSlots endpoint to customer router
  • Defined protectedProcedure with zod input for serviceId and location
  • Queried approved professionals within service radius using ST_DWithin raw SQL
  • Looped through next 4 days to fetch availabilities and generate 30-min slots
  • Filtered out slots overlapping existing bookings and grouped by date
  • Computed duration-based slot sets and applied categorizeTimeSlots util
packages/api/src/routers/customer.ts
Enhance professional router with baseLocation updates, error handling, and availability refactor
  • Executed raw SQL to update professional baseLocation when GPS inputs are provided
  • Preserved ORPCError propagation instead of swallowing non-error instances
  • Changed availability creation and update to use dayIndex with regex time validation
  • Added a lookup to prevent duplicate availability entries
  • Adjusted updateAvailability handler to accept dayIndex instead of dayOfWeek
packages/api/src/routers/professional.ts
Add time-slot categorization util and update dependencies
  • Introduced categorizeTimeSlots util grouping slots into morning/afternoon/evening
  • Installed and imported dayjs for date arithmetic
  • Integrated categorizeTimeSlots into getAvailableSlots logic
packages/api/src/utils/booking-util.ts
packages/api/package.json
Introduce comprehensive project documentation
  • Added TESTING.md with detailed API test cases
  • Created API.md for full RPC and REST endpoint reference
  • Compiled ARCHITECTURE.md outlining system design and data flow
  • Provided a README.md with project overview and setup instructions
docs/TESTING.md
docs/API.md
docs/ARCHITECTURE.md
docs/README.md
Add Prisma migration for professional availability enhancements
  • Dropped obsolete dayOfWeek column and added dayIndex, breakSlots, isFullDayOff fields
  • Created ProfessionalAvailabilityException table with appropriate indexes and foreign key constraints
packages/db/prisma/migrations/20251112134256_professional_availability_exception/migration.sql

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Nov 12, 2025

Copy link
Copy Markdown

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Summary by CodeRabbit

  • Documentation

    • Added comprehensive API, architecture, and testing documentation
    • Added project overview and getting started guide
  • New Features

    • Customers can now view available booking slots based on service location and professional availability
    • Professionals can manage daily availability with more granular control, including marking specific dates as unavailable
  • Bug Fixes

    • Improved error handling in professional profile retrieval

Walkthrough

This PR introduces comprehensive API documentation, adds a getAvailableSlots endpoint for booking availability queries, refactors professional availability management from dayOfWeek to dayIndex, and implements a new ProfessionalAvailabilityException model for per-date exceptions in the database schema.

Changes

Cohort / File(s) Summary
Documentation
docs/API.md, docs/ARCHITECTURE.md, docs/README.md, docs/TESTING.md
Comprehensive documentation suite covering API surface, system architecture, project overview, and API testing guide with endpoint specifications, schemas, error handling, and test cases.
Package Configuration
packages/api/package.json
Dependency reorganization: moved @house-help/auth and @house-help/db to workspace declarations, reintroduced elysia and zod as catalog dependencies, added @orpc/server to catalog, and introduced dayjs (^1.11.19) as new direct dependency.
Customer Router
packages/api/src/routers/customer.ts
Added new getAvailableSlots endpoint computing available booking slots by querying professionals within service area, gathering their availabilities, generating 30-minute slots, filtering by existing bookings, and returning duration-categorized results.
Professional Router
packages/api/src/routers/professional.ts
Refactored availability management: replaced dayOfWeek enum with dayIndex number field; added time validation via HH:MM regex; added duplicate availability checks; added geospatial baseLocation update; improved error propagation in getProfile.
Booking Utilities
packages/api/src/utils/booking-util.ts
Added categorizeTimeSlots function grouping Date slots into morning, afternoon, and evening periods using dayjs hour extraction and formatted 12-hour labels.
Database Schema & Migration
packages/db/prisma/schema/professional.prisma, packages/db/prisma/migrations/20251112134256_professional_availability_exception/migration.sql
Schema refactor: replaced dayOfWeek with dayIndex in ProfessionalAvailability; added breakSlots, isFullDayOff fields; created new ProfessionalAvailabilityException model for date-specific exceptions; updated indexes; added relations and cascade constraints.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant CustomerRouter as Customer Router
    participant DB as Database
    participant Utils as Booking Utils

    Client->>CustomerRouter: getAvailableSlots(serviceId, location)
    activate CustomerRouter
    
    CustomerRouter->>DB: Query professionals by service<br/>and geospatial location
    activate DB
    DB-->>CustomerRouter: Professional[] within area
    deactivate DB
    
    CustomerRouter->>DB: Get availabilities for next 4 days
    activate DB
    DB-->>CustomerRouter: Availability[] + Exceptions
    deactivate DB
    
    CustomerRouter->>CustomerRouter: Generate 30-min slots<br/>per professional per day
    
    CustomerRouter->>DB: Query confirmed/in-progress<br/>bookings for time ranges
    activate DB
    DB-->>CustomerRouter: Booking[] with conflicts
    deactivate DB
    
    CustomerRouter->>CustomerRouter: Filter out conflicting slots
    
    CustomerRouter->>Utils: categorizeTimeSlots(remaining slots)
    activate Utils
    Utils-->>CustomerRouter: {morning, afternoon, evening}
    deactivate Utils
    
    CustomerRouter-->>Client: {date, slots by duration}[]
    deactivate CustomerRouter
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Areas requiring extra attention:

  • Professional availability refactor: Signature changes from dayOfWeek enum to dayIndex number across createAvailability and updateAvailability endpoints; validate impact on existing data transformations and client compatibility.
  • Database migration complexity: Schema changes include dropping/altering columns (dayOfWeek → dayIndex), adding new fields (breakSlots, isFullDayOff), and introducing entirely new ProfessionalAvailabilityException model; verify migration reversibility and data integrity.
  • getAvailableSlots business logic: Complex spatial-temporal query combining geospatial filtering, multi-day availability aggregation, booking conflict detection, and slot categorization; verify correctness of 30-minute slot generation, duration-wise grouping, and edge cases (e.g., no professionals found, no availability).
  • Booking utility categorization: Verify dayjs hour-based time categorization (morning < 12, afternoon < 17, evening ≥ 17) aligns with business requirements and timezone handling.
  • Documentation scope: Four new documentation files spanning API, architecture, testing, and README; verify accuracy and consistency across examples and specifications.

Possibly related PRs

  • PR #3: Modifies the same customer/professional routers, API context, and availability-related utilities; directly related code areas.
  • PR #2: Introduces the Professional availability models and logic that this PR refactors from dayOfWeek to dayIndex and extends with exceptions.

Suggested labels

size:XXL, Review effort 5/5

Poem

🐰 Slots bloom like carrots in the garden bright,
Professional schedules now indexed just right,
Exceptions cascade, exceptions take flight,
Available moments from morning till night,
Documentation gardens, a beautiful sight! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'getAvailableSlots added' accurately reflects the primary feature introduced in this pull request—a new customer endpoint for retrieving available booking slots.
Description check ✅ Passed The PR description accurately summarizes the changeset, covering new features (getAvailableSlots endpoint, ProfessionalAvailabilityException table), enhancements (dayIndex refactoring, location persistence, error handling), and documentation additions.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/get-slots

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai

codeant-ai Bot commented Nov 12, 2025

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@qodo-code-review

Copy link
Copy Markdown

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
SQL injection

Description: Raw SQL via prisma.$queryRaw interpolates latitude/longitude directly into a PostGIS query
which can allow SQL injection if any part becomes user-controlled; prefer using
parameterized prisma.$queryRaw... ${param} ... (ensuring tagged template params are bound)
or Prisma client, and validate numeric ranges for coordinates.
customer.ts [425-437]

Referred Code
const professionals: Professional[] = await prisma.$queryRaw`
	SELECT p._id, p."isAvailable", p."approvalStatus", p."serviceRadiusKm"  
	FROM professional p
	JOIN professional_service ps ON ps."professionalId" = p._id
	WHERE ps."serviceId" = ${serviceId}
	AND p."approvalStatus" = 'APPROVED'
	AND p."isAvailable" = true
	AND ST_DWithin(
		p."baseLocation", 
		ST_SetSRID(ST_MakePoint(${longitude}, ${latitude}), 4326)::geography, 
		p."serviceRadiusKm" * 1000 
	);
`;
SQL injection

Description: Raw SQL update using prisma.$executeRaw with interpolated longitude/latitude risks SQL
injection if values are not strictly validated; use tagged-template parameter binding and
enforce numeric range validation before execution.
professional.ts [199-204]

Referred Code
	await prisma.$executeRaw`
		UPDATE professional 
		SET "baseLocation" = ST_SetSRID(ST_MakePoint(${input.baseLongitude}, ${input.baseLatitude}), 4326)::geography
		WHERE _id = ${professionalId}
	`;
}
Timezone handling risk

Description: Time slot generation uses dayjs parsing of start/end strings without timezone
normalization, which can cause logic errors and potential booking overlaps or bypasses
around DST/timezone boundaries; normalize to UTC or a fixed TZ and validate slot
boundaries.
customer.ts [446-472]

Referred Code
const date = dayjs().add(dayOffset, "day");
const dayIndex = date.day(); // 0–6

// 3️⃣ Get all professional availabilities for this weekday
const availability = await prisma.professionalAvailability.findMany({
	where: {
		professionalId: { in: professionals.map((p) => p._id) },
		dayIndex,
		isAvailable: true,
	},
});
if (availability.length === 0) {
	continue;
}
// Generate all possible 30-min slots between start–end ranges
const allSlots = [];
for (const a of availability) {
	let cursor = dayjs(`${date.format("YYYY-MM-DD")}T${a.startTime}`);
	const end = dayjs(`${date.format("YYYY-MM-DD")}T${a.endTime}`);
	while (
		cursor.add(30, "minute").isBefore(end) ||


 ... (clipped 6 lines)
Timezone handling risk

Description: Categorizing slots by local hour without explicit timezone assumptions may misclassify
times and expose users to incorrect availability, leading to booking confusion; ensure
consistent timezone handling before formatting.
booking-util.ts [7-11]

Referred Code
const hour = dayjs(slot).hour();
const label = dayjs(slot).format("hh:mm a");
if (hour < 12) grouped.morning.push(label);
else if (hour < 17) grouped.afternoon.push(label);
else grouped.evening.push(label);
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logs: Newly added booking availability lookup performs critical queries without emitting
structured audit logs (no user ID, timestamp, action, or outcome recorded).

Referred Code
getAvailableSlots: protectedProcedure
	.input(
		z.object({
			serviceId: z.string(),
			location: z.object({
				latitude: z.number(),
				longitude: z.number(),
			}),
		}),
	)
	.handler(async ({ input, context }) => {
		// Find Professionals available for the service in the location
		// Check if the professional is available on the date
		// Response should be based on the date and should contain the slots
		// No need to get professionals, just the slots
		// If date is not provided, get the slots for the next 4 days
		// If date is provided, get the slots for the date
		// Group Response by date and slot
		try {
			const { serviceId, location } = input;
			const DURATIONS = [


 ... (clipped 122 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Edge cases missing: getAvailableSlots lacks validation for latitude/longitude ranges, time window coherence,
and handling of availability exceptions/breaks, which may cause incorrect slot results.

Referred Code
getAvailableSlots: protectedProcedure
	.input(
		z.object({
			serviceId: z.string(),
			location: z.object({
				latitude: z.number(),
				longitude: z.number(),
			}),
		}),
	)
	.handler(async ({ input, context }) => {
		// Find Professionals available for the service in the location
		// Check if the professional is available on the date
		// Response should be based on the date and should contain the slots
		// No need to get professionals, just the slots
		// If date is not provided, get the slots for the next 4 days
		// If date is provided, get the slots for the date
		// Group Response by date and slot
		try {
			const { serviceId, location } = input;
			const DURATIONS = [


 ... (clipped 122 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Unstructured logs: The new handlers use console.error without structured logging, making logs harder to audit
and potentially inconsistent across services.

Referred Code
	console.error(error);
	throw new ORPCError("INTERNAL_SERVER_ERROR", {
		message: "Failed to get service availability",
	});
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Raw SQL usage: The use of prisma.$queryRaw with template literals for geospatial filtering warrants
review to ensure parameters remain safely bound and not susceptible to injection.

Referred Code
const professionals: Professional[] = await prisma.$queryRaw`
	SELECT p._id, p."isAvailable", p."approvalStatus", p."serviceRadiusKm"  
	FROM professional p
	JOIN professional_service ps ON ps."professionalId" = p._id
	WHERE ps."serviceId" = ${serviceId}
	AND p."approvalStatus" = 'APPROVED'
	AND p."isAvailable" = true
	AND ST_DWithin(
		p."baseLocation", 
		ST_SetSRID(ST_MakePoint(${longitude}, ${latitude}), 4326)::geography, 
		p."serviceRadiusKm" * 1000 
	);
`;

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `packages/api/src/routers/customer.ts:389` </location>
<code_context>
 			}
 		}),
+	// Bookings
+	getAvailableSlots: protectedProcedure
+		.input(
+			z.object({
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the handler by extracting major steps into pure functions and batching database queries to simplify logic and improve testability.

Consider pulling each major step into its own function and batching your database calls so you only loop over in-memory data in your handler. For example:

1. Batch-fetch all availabilities and bookings for the next 4 days.  
2. Group them by dayIndex/date.  
3. Extract three pure functions:
   - `generateSlots(avails: Availability[], date: Dayjs): Date[]`  
   - `filterBookedSlots(slots: Date[], bookings: Booking[]): Date[]`  
   - `computeDurationSlots(slots: Date[], durations: Duration[]): {label: string; hours: number; slots: Date[]}[]`  

Then your handler reduces to:

```ts
const dates = Array.from({length: 4}, (_, i) => dayjs().add(i, 'day'));
const dayIndices = dates.map(d => d.day());

// 1) Batch fetch:
// professionalIds already computed above
const [allAvails, allBookings] = await Promise.all([
  prisma.professionalAvailability.findMany({
    where: { professionalId: { in: professionalIds }, dayIndex: { in: dayIndices }, isAvailable: true },
  }),
  prisma.booking.findMany({
    where: {
      professionalId: { in: professionalIds },
      status: { in: ["CONFIRMED", "IN_PROGRESS"] },
      scheduledStartTime: {
        gte: dates[0].startOf("day").toDate(),
        lt: dates[dates.length - 1].endOf("day").toDate(),
      },
    },
    select: { scheduledStartTime: true, completedAt: true },
  }),
]);

// 2) Group by dayIndex/date:
const availsByDay = groupBy(allAvails, (a) => a.dayIndex);
const bookingsByDate = groupBy(allBookings, (b) => dayjs(b.scheduledStartTime).format("YYYY-MM-DD"));

// 3) Build results:
const results = dates.map((date) => {
  const key = date.format("YYYY-MM-DD");
  const slots = generateSlots(availsByDay[date.day()] || [], date);
  const free = filterBookedSlots(slots, bookingsByDate[key] || []);
  return {
    date: key,
    slots: categorizeTimeSlots(free),
    durationWiseSlots: computeDurationSlots(free, DURATIONS),
  };
});

return { data: results };
```

And in `utils/slots.ts`:

```ts
export function generateSlots(avails, date) {
  const slots: Date[] = [];
  for (const a of avails) {
    let cursor = date.hour(a.startTime.split(":")[0]).minute(a.startTime.split(":")[1]);
    const end = date.hour(a.endTime.split(":")[0]).minute(a.endTime.split(":")[1]);
    while (cursor.add(30, "minute").isSameOrBefore(end)) {
      slots.push(cursor.toDate());
      cursor = cursor.add(30, "minute");
    }
  }
  return slots;
}

export function filterBookedSlots(slots, bookings) {
  return slots.filter((slot) => {
    const start = dayjs(slot), end = start.add(30, "minute");
    return !bookings.some((b) => {
      const bStart = dayjs(b.scheduledStartTime);
      const bEnd = b.completedAt ? dayjs(b.completedAt) : bStart.add(2, "hour");
      return start.isBefore(bEnd) && end.isAfter(bStart);
    });
  });
}

export function computeDurationSlots(slots, durations) {
  return durations.map((d) => ({
    ...d,
    slots: categorizeTimeSlots(
      slots.filter((s) =>
        slots.some((t) =>
          dayjs(t).isSame(dayjs(s).add(d.hours * 60 - 30, "minute"))
        )
      )
    ),
  }));
}
```

This removes deep nesting, cuts down on per‐slot database calls, and makes each step easier to test.
</issue_to_address>

### Comment 2
<location> `packages/api/src/routers/professional.ts:335-341` </location>
<code_context>
				const professionalAvailability =
					await prisma.professionalAvailability.create({
						data: {
							dayIndex: input.dayIndex,
							startTime: input.startTime,
							endTime: input.endTime,
							professionalId: professionalId,
						},
					});
				return professionalAvailability;

</code_context>

<issue_to_address>
**suggestion (code-quality):** Inline variable that is immediately returned ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/inline-immediately-returned-variable))

```suggestion
				return await prisma.professionalAvailability.create({
    						data: {
    							dayIndex: input.dayIndex,
    							startTime: input.startTime,
    							endTime: input.endTime,
    							professionalId: professionalId,
    						},
    					});

```

<br/><details><summary>Explanation</summary>Something that we often see in people's code is assigning to a result variable
and then immediately returning it.

Returning the result directly shortens the code and removes an unnecessary
variable, reducing the mental load of reading the function.

Where intermediate variables can be useful is if they then get used as a
parameter or a condition, and the name can act like a comment on what the
variable represents. In the case where you're returning it from a function, the
function name is there to tell you what the result is, so the variable name
is unnecessary.
</details>
</issue_to_address>

### Comment 3
<location> `packages/api/src/routers/professional.ts:436-442` </location>
<code_context>
				const professionalAvailability =
					await prisma.professionalAvailability.update({
						where: {
							id: input.id,
							professionalId: professionalId,
						},
						data: {
							dayIndex: input.dayIndex,
							startTime: input.startTime,
							endTime: input.endTime,
						},
					});
				return professionalAvailability;

</code_context>

<issue_to_address>
**suggestion (code-quality):** Inline variable that is immediately returned ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/inline-immediately-returned-variable))

```suggestion
				return await prisma.professionalAvailability.update({
    						where: {
    							id: input.id,
    							professionalId: professionalId,
    						},
    						data: {
    							dayIndex: input.dayIndex,
    							startTime: input.startTime,
    							endTime: input.endTime,
    						},
    					});

```

<br/><details><summary>Explanation</summary>Something that we often see in people's code is assigning to a result variable
and then immediately returning it.

Returning the result directly shortens the code and removes an unnecessary
variable, reducing the mental load of reading the function.

Where intermediate variables can be useful is if they then get used as a
parameter or a condition, and the name can act like a comment on what the
variable represents. In the case where you're returning it from a function, the
function name is there to tell you what the result is, so the variable name
is unnecessary.
</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 335 to 341
const professionalAvailability =
await prisma.professionalAvailability.create({
data: {
dayOfWeek: input.dayOfWeek,
dayIndex: input.dayIndex,
startTime: input.startTime,
endTime: input.endTime,
professionalId: professionalId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Inline variable that is immediately returned (inline-immediately-returned-variable)

Suggested change
const professionalAvailability =
await prisma.professionalAvailability.create({
data: {
dayOfWeek: input.dayOfWeek,
dayIndex: input.dayIndex,
startTime: input.startTime,
endTime: input.endTime,
professionalId: professionalId,
return await prisma.professionalAvailability.create({
data: {
dayIndex: input.dayIndex,
startTime: input.startTime,
endTime: input.endTime,
professionalId: professionalId,
},
});


ExplanationSomething that we often see in people's code is assigning to a result variable
and then immediately returning it.

Returning the result directly shortens the code and removes an unnecessary
variable, reducing the mental load of reading the function.

Where intermediate variables can be useful is if they then get used as a
parameter or a condition, and the name can act like a comment on what the
variable represents. In the case where you're returning it from a function, the
function name is there to tell you what the result is, so the variable name
is unnecessary.

Comment on lines 436 to 442
professionalId: professionalId,
},
data: {
dayOfWeek: input.dayOfWeek,
dayIndex: input.dayIndex,
startTime: input.startTime,
endTime: input.endTime,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Inline variable that is immediately returned (inline-immediately-returned-variable)

Suggested change
professionalId: professionalId,
},
data: {
dayOfWeek: input.dayOfWeek,
dayIndex: input.dayIndex,
startTime: input.startTime,
endTime: input.endTime,
},
return await prisma.professionalAvailability.update({
where: {
id: input.id,
professionalId: professionalId,
},
data: {
dayIndex: input.dayIndex,
startTime: input.startTime,
endTime: input.endTime,
},
});


ExplanationSomething that we often see in people's code is assigning to a result variable
and then immediately returning it.

Returning the result directly shortens the code and removes an unnecessary
variable, reducing the mental load of reading the function.

Where intermediate variables can be useful is if they then get used as a
parameter or a condition, and the name can act like a comment on what the
variable represents. In the case where you're returning it from a function, the
function name is there to tell you what the result is, so the variable name
is unnecessary.

@codeant-ai codeant-ai Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files label Nov 12, 2025
@qodo-code-review

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
The slot availability logic is complex and potentially flawed

The getAvailableSlots endpoint's scheduling logic is complex and contains
critical flaws, such as a hardcoded booking duration for conflict checks and a
buggy algorithm for finding continuous time slots. It is recommended to use a
more robust, database-centric approach or a dedicated library to avoid
performance issues and bugs.

Examples:

packages/api/src/routers/customer.ts [492-494]
							const bEnd = b.completedAt
								? dayjs(b.completedAt)
								: bStart.add(2, "hour");
packages/api/src/routers/customer.ts [503-509]
						const validSlots = availableSlots.filter((slot) => {
							const slotEnd = dayjs(slot).add(minMinutes, "minute");
							// Ensure the entire duration fits within availability
							return availableSlots.some((s) =>
								dayjs(s).isSame(slotEnd.subtract(30, "minute")),
							);
						});

Solution Walkthrough:

Before:

// in getAvailableSlots handler
// ... find professionals
for (dayOffset in 0..3) {
  // ... get availability for the day
  allSlots = generate_30_min_slots_from_availability();

  // ... get bookings for the day
  availableSlots = allSlots.filter(slot => {
    // FLAW: uses hardcoded 2-hour duration for ongoing bookings
    const bookingEnd = booking.completedAt ?? booking.start.add(2, "hour");
    return !overlaps(slot, bookingStart, bookingEnd);
  });

  for (duration in DURATIONS) {
    validStartSlots = availableSlots.filter(slot => {
      // FLAW: only checks if the last 30-min chunk of the duration is available
      const slotEnd = dayjs(slot).add(duration, "minutes");
      return availableSlots.includes(slotEnd.subtract(30, "minute"));
    });
    // ...
  }
}

After:

// in getAvailableSlots handler
// Use a database-centric approach with PostgreSQL functions
// or a dedicated scheduling library.

// Example with a hypothetical DB function:
const availableSlotsByDuration = await prisma.$queryRaw`
  SELECT 
    duration,
    find_available_slots(
      professionals_ids,
      start_date,
      end_date,
      duration
    ) as slots
  FROM (VALUES (60), (90), (120), ...) AS t(duration)
`;

// The DB function `find_available_slots` would internally handle:
// 1. Generating time series based on professional availability.
// 2. Excluding time ranges covered by existing bookings (using actual booking durations).
// 3. Finding contiguous blocks of free time that match the requested duration.
// This is more robust, performant, and less prone to bugs.
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies two critical flaws in the core business logic of the new getAvailableSlots endpoint, including a hardcoded duration and a buggy algorithm for finding contiguous slots, which would lead to incorrect availability and a poor user experience.

High
Possible issue
Fix incorrect continuous slot validation

Refactor the slot validation logic to ensure all 30-minute intervals within a
requested duration are continuously available, instead of only checking the
final interval.

packages/api/src/routers/customer.ts [500-514]

 					const durationSlots = [];
+					const availableSlotTimes = new Set(availableSlots.map(s => dayjs(s).valueOf()));
+
 					for (const d of DURATIONS) {
 						const minMinutes = d.hours * 60;
+						const requiredSlots = minMinutes / 30;
+
 						const validSlots = availableSlots.filter((slot) => {
-							const slotEnd = dayjs(slot).add(minMinutes, "minute");
-							// Ensure the entire duration fits within availability
-							return availableSlots.some((s) =>
-								dayjs(s).isSame(slotEnd.subtract(30, "minute")),
-							);
+							const start = dayjs(slot);
+							let isContinuous = true;
+							for (let i = 1; i < requiredSlots; i++) {
+								const nextSlotTime = start.add(i * 30, "minute").valueOf();
+								if (!availableSlotTimes.has(nextSlotTime)) {
+									isContinuous = false;
+									break;
+								}
+							}
+							return isContinuous;
 						});
+
 						durationSlots.push({
 							...d,
 							slots: categorizeTimeSlots(validSlots),
 						});
 					}

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical logic flaw in validating continuous time slots, which would lead to offering incorrect availability and allowing double bookings.

High
Correct slot generation loop logic

Correct the while loop condition for time slot generation to
cursor.isBefore(end) to ensure all valid 30-minute slots within an availability
window are generated.

packages/api/src/routers/customer.ts [463-472]

 						let cursor = dayjs(`${date.format("YYYY-MM-DD")}T${a.startTime}`);
 						const end = dayjs(`${date.format("YYYY-MM-DD")}T${a.endTime}`);
-						while (
-							cursor.add(30, "minute").isBefore(end) ||
-							cursor.isSame(end)
-						) {
+						while (cursor.isBefore(end)) {
 							allSlots.push(cursor.toDate());
 							cursor = cursor.add(30, "minute");
 						}
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a significant bug in the time slot generation logic that causes valid slots to be missed, directly impacting the core functionality of the new endpoint.

High
Ensure atomic update with transaction

Wrap the professional profile update and the raw SQL query for baseLocation in a
prisma.$transaction to ensure the operations are atomic and prevent data
inconsistency.

packages/api/src/routers/professional.ts [188-206]

-				const professional = await prisma.professional.update({
-					where: { id: professionalId },
-					data: {
-						...
-						...(input.baseLongitude !== undefined && {
-							baseLongitude: input.baseLongitude,
-						}),
-					},
+				const professional = await prisma.$transaction(async (tx) => {
+					const updatedProfessional = await tx.professional.update({
+						where: { id: professionalId },
+						data: {
+							...(input.profileImage && { profileImage: input.profileImage }),
+							...(input.bio && { bio: input.bio }),
+							...(input.experienceYears !== undefined && {
+								experienceYears: input.experienceYears,
+							}),
+							...(input.languages && { languages: input.languages }),
+							...(input.serviceRadiusKm !== undefined && {
+								serviceRadiusKm: input.serviceRadiusKm,
+							}),
+							...(input.baseLatitude !== undefined && {
+								baseLatitude: input.baseLatitude,
+							}),
+							...(input.baseLongitude !== undefined && {
+								baseLongitude: input.baseLongitude,
+							}),
+						},
+					});
+
+					if (
+						input.baseLatitude !== undefined &&
+						input.baseLongitude !== undefined
+					) {
+						await tx.$executeRaw`
+							UPDATE professional 
+							SET "baseLocation" = ST_SetSRID(ST_MakePoint(${input.baseLongitude}, ${input.baseLatitude}), 4326)::geography
+							WHERE _id = ${professionalId}
+						`;
+					}
+					return updatedProfessional;
 				});
-				// Update baseLocation if baseLatitude and baseLongitude are provided
-				if (
-					input.baseLatitude !== undefined &&
-					input.baseLongitude !== undefined
-				) {
-					await prisma.$executeRaw`
-						UPDATE professional 
-						SET "baseLocation" = ST_SetSRID(ST_MakePoint(${input.baseLongitude}, ${input.baseLatitude}), 4326)::geography
-						WHERE _id = ${professionalId}
-					`;
-				}
 				return professional;

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out a data integrity issue where a partial update can occur, and proposes using a transaction to ensure the location update is atomic.

Medium
  • More

FROM professional p
JOIN professional_service ps ON ps."professionalId" = p._id
WHERE ps."serviceId" = ${serviceId}
AND p."approvalStatus" = 'APPROVED'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Use the imported enum value for approval status in the raw SQL query instead of a hard-coded string literal. [best practice]

Severity Level: Minor ⚠️

Suggested change
AND p."approvalStatus" = 'APPROVED'
AND p."approvalStatus" = ${ApprovalStatus.APPROVED}
Why it matters? ⭐

Using ApprovalStatus.APPROVED (already imported at the top of the file) keeps the value DRY and type-safe,
avoids a hard-coded magic string and prevents accidental typos if the enum value ever changes.
In a prisma.$queryRaw template the ${...} interpolation is parameterized, so replacing the literal
with ${ApprovalStatus.APPROVED} is both safe and clearer.
The ApprovalStatus import is present in the file, so this is a straightforward improvement.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/api/src/routers/customer.ts
**Line:** 430:430
**Comment:**
	*Best Practice: Use the imported enum value for approval status in the raw SQL query instead of a hard-coded string literal.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment on lines +465 to +468
while (
cursor.add(30, "minute").isBefore(end) ||
cursor.isSame(end)
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Replace the loop condition that generates 30-minute slots so it includes the last valid start time within the availability range (use cursor.isBefore(end) instead of the existing compound check). [possible bug]

Severity Level: Critical 🚨

Suggested change
while (
cursor.add(30, "minute").isBefore(end) ||
cursor.isSame(end)
) {
while (cursor.isBefore(end)) {
Why it matters? ⭐

The current condition misses valid 30-minute start times that end exactly at the availability end.
Example: start=09:00, end=10:00 — the loop pushes 09:00 but stops before 09:30 because
cursor.add(30).isBefore(end) is false when it equals end, and cursor.isSame(end) is also false.
Replacing the check with cursor.isBefore(end) will correctly produce starts 09:00 and 09:30.
This is a real logic bug in slot generation and the suggested change fixes it with minimal surface area.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/api/src/routers/customer.ts
**Line:** 465:468
**Comment:**
	*Possible Bug: Replace the loop condition that generates 30-minute slots so it includes the last valid start time within the availability range (use cursor.isBefore(end) instead of the existing compound check).

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment on lines 420 to 421
startTime: z.string(),
endTime: z.string(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Apply the same time format validation to updateAvailability inputs by adding the regex to startTime and endTime [enhancement]

Severity Level: Minor ⚠️

Suggested change
startTime: z.string(),
endTime: z.string(),
startTime: z.string().regex(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/),
endTime: z.string().regex(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/),
Why it matters? ⭐

createAvailability already validates startTime/endTime with the regex /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/ (see createAvailability in the same file). updateAvailability currently accepts any string for those fields, so callers could submit invalid formats and cause inconsistent data or downstream parsing bugs. Applying the same regex keeps validation consistent and prevents bad data.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/api/src/routers/professional.ts
**Line:** 420:421
**Comment:**
	*Enhancement: Apply the same time format validation to `updateAvailability` inputs by adding the regex to `startTime` and `endTime`

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.


*/
-- DropIndex
DROP INDEX "public"."professional_availability_professionalId_dayOfWeek_idx";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Use IF EXISTS when dropping the index to avoid migration failure if the index is already absent. [possible issue]

Severity Level: Minor ⚠️

Suggested change
DROP INDEX "public"."professional_availability_professionalId_dayOfWeek_idx";
DROP INDEX IF EXISTS "public"."professional_availability_professionalId_dayOfWeek_idx";
Why it matters? ⭐

The migration currently unconditionally drops that index (the file contains the exact DROP INDEX line). Using IF EXISTS prevents the migration from failing when the index is already absent and is a safe, non-invasive hardening of this single migration statement.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/db/prisma/migrations/20251112134256_professional_availability_exception/migration.sql
**Line:** 9:9
**Comment:**
	*Possible Issue: Use IF EXISTS when dropping the index to avoid migration failure if the index is already absent.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

@codeant-ai

codeant-ai Bot commented Nov 12, 2025

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
docs/README.md (1)

1-164: Excellent documentation! Minor markdown improvements suggested.

The documentation provides a comprehensive overview of the project structure, features, and setup instructions.

Consider adding language specifiers to fenced code blocks for better syntax highlighting:

Line 53 - Add language specifier:

-```
+```text
 house-help/

Line 79 - Already has bash specified ✓

Based on learnings

packages/api/src/utils/booking-util.ts (1)

1-14: Consider timezone handling for accurate time categorization.

The function correctly categorizes time slots, but dayjs will use the timezone of the input Date objects. If your application deals with users across multiple timezones, consider:

  1. Accepting a timezone parameter and explicitly setting it
  2. Documenting the expected timezone behavior
  3. Using dayjs plugins for timezone support if needed

Example enhancement:

-export function categorizeTimeSlots(slots: Date[]) {
+export function categorizeTimeSlots(slots: Date[], timezone: string = "Asia/Kolkata") {
+	const dayjs = require('dayjs');
+	const utc = require('dayjs/plugin/utc');
+	const tz = require('dayjs/plugin/timezone');
+	dayjs.extend(utc);
+	dayjs.extend(tz);
+
 	const grouped: { morning: string[]; afternoon: string[]; evening: string[] } =
 		{ morning: [], afternoon: [], evening: [] };
 	for (const slot of slots) {
-		const hour = dayjs(slot).hour();
-		const label = dayjs(slot).format("hh:mm a");
+		const hour = dayjs(slot).tz(timezone).hour();
+		const label = dayjs(slot).tz(timezone).format("hh:mm a");
 		if (hour < 12) grouped.morning.push(label);
 		else if (hour < 17) grouped.afternoon.push(label);
 		else grouped.evening.push(label);
 	}
 	return grouped;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9715c52 and 9876312.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • docs/API.md (1 hunks)
  • docs/ARCHITECTURE.md (1 hunks)
  • docs/README.md (1 hunks)
  • docs/TESTING.md (1 hunks)
  • packages/api/package.json (1 hunks)
  • packages/api/src/routers/customer.ts (2 hunks)
  • packages/api/src/routers/professional.ts (6 hunks)
  • packages/api/src/utils/booking-util.ts (1 hunks)
  • packages/db/prisma/migrations/20251112134256_professional_availability_exception/migration.sql (1 hunks)
  • packages/db/prisma/schema/professional.prisma (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/api/src/routers/customer.ts (2)
packages/api/src/index.ts (1)
  • protectedProcedure (19-19)
packages/api/src/utils/booking-util.ts (1)
  • categorizeTimeSlots (3-14)
packages/api/src/routers/professional.ts (1)
packages/api/src/index.ts (1)
  • protectedProcedure (19-19)
🪛 LanguageTool
docs/TESTING.md

[style] ~563-~563: The double modal “required Expected” is nonstandard (only accepted in certain dialects). Consider “to be Expected”.
Context: ...Auth*: No authentication required - Expected: 200 OK - Verify: Public endpoint...

(NEEDS_FIXED)


[uncategorized] ~682-~682: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...: CORS middleware working ### SEC-006: Rate Limiting - Test: Send multiple rapid request...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[grammar] ~742-~742: Ensure spelling is correct
Context: ...mes for all endpoints - Expected: < 500ms for most endpoints - Verify: Accept...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

docs/API.md

[grammar] ~671-~671: Ensure spelling is correct
Context: ...` --- ## Error Handling The API uses oRPC error handling. Errors are returned in ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[style] ~716-~716: This phrase is redundant (‘I’ stands for ‘interface’). Use simply “UI”.
Context: ...api-reference` This provides a Swagger UI interface for exploring and testing all API endpo...

(ACRONYM_TAUTOLOGY)

🪛 markdownlint-cli2 (0.18.1)
docs/README.md

13-13: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


51-51: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (8)
packages/db/prisma/schema/professional.prisma (3)

41-51: LGTM! Well-structured relations.

The new relations for professionalAvailabilityExceptions and bookingNotifications are properly defined and align with the expanded data model.


120-134: LGTM! Availability model evolution looks good.

The transition from dayOfWeek enum to dayIndex integer provides more flexibility. The addition of breakSlots, isFullDayOff, and standardized timestamp fields enhances the availability management capabilities. The updated index on [professionalId, dayIndex] maintains query performance.


137-152: LGTM! Exception model is well-designed.

The ProfessionalAvailabilityException model properly supports per-date availability overrides with appropriate constraints:

  • Unique constraint on [professionalId, date] prevents duplicates
  • Index on date for efficient temporal queries
  • Cascade deletion maintains referential integrity
  • Optional startTime/endTime allows both full-day and partial-day exceptions
packages/api/package.json (1)

23-34: LGTM! Dependency management is appropriate.

The addition of dayjs@^1.11.19 supports the new time-slot categorization feature. The version is current and the dependency organization is clean.

docs/ARCHITECTURE.md (1)

1-354: LGTM! Comprehensive architecture documentation.

The documentation provides excellent coverage of the system design, including monorepo structure, technology stack, API architecture, database design with PostGIS, security considerations, and deployment guidance. This will be valuable for onboarding and maintenance.

docs/TESTING.md (1)

1-812: LGTM! Thorough testing guide.

The testing guide provides comprehensive coverage of test scenarios across all API endpoints, including edge cases, error handling, security tests, and integration flows. This will be invaluable for ensuring API quality and correctness.

packages/db/prisma/migrations/20251112134256_professional_availability_exception/migration.sql (2)

17-42: LGTM! Exception table structure is solid.

The ProfessionalAvailabilityException table creation with appropriate constraints, indexes, and foreign key relationships follows best practices:

  • Unique constraint on (professionalId, date) prevents duplicate exceptions
  • Date index for efficient temporal queries
  • Cascade deletion maintains referential integrity

1-16: Migration is safe to proceed as-is.

The professional_availability table is empty. The initial migration (Nov 7) created it with no data, the seed file only populates services, and no intervening migrations add records. Adding a required dayIndex column without a default succeeds on empty tables.

The original review comment's critical concern—"migration will fail on non-empty tables"—doesn't apply here, as this is a fresh development database with no existing professional_availability records.

Likely an incorrect or invalid review comment.

Comment thread docs/API.md
Comment on lines +407 to +430
### Availability Management

#### Create Availability

**Endpoint**: `professional.createAvailability`

**Method**: `POST`

**Auth**: Protected (Professional)

**Request Body**:
```json
{
"dayOfWeek": "MONDAY",
"startTime": "09:00",
"endTime": "17:00"
}
```

**Day of Week**: `MONDAY`, `TUESDAY`, `WEDNESDAY`, `THURSDAY`, `FRIDAY`, `SATURDAY`, `SUNDAY`

**Time Format**: `HH:MM` (24-hour format)

**Response**: Created availability object

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Documentation inconsistent with schema changes.

The API documentation still references dayOfWeek with enum values like "MONDAY", but the schema has changed to use dayIndex as an integer (0-6). This inconsistency will cause integration failures.

Update the documentation to reflect the schema changes:

 **Request Body**:
 ```json
 {
-  "dayOfWeek": "MONDAY",
+  "dayIndex": 0,
   "startTime": "09:00",
   "endTime": "17:00"
 }

-Day of Week: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
+Day Index: Integer 0-6 (0 = Sunday, 1 = Monday, ..., 6 = Saturday)

Time Format: HH:MM (24-hour format)


<details>
<summary>🤖 Prompt for AI Agents</summary>

In docs/API.md around lines 407 to 430, the Create Availability example and
description still reference the old "dayOfWeek" enum values; update the request
body and descriptive text to use the new "dayIndex" integer field (0-6) instead
of "dayOfWeek", replace the example JSON so it shows "dayIndex": 0 with
startTime/endTime unchanged, and change the explanatory line to "Day Index:
Integer 0-6 (0 = Sunday, 1 = Monday, ..., 6 = Saturday)"; keep the Time Format
line as-is.


</details>

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread docs/API.md
Comment on lines +442 to +461
#### Update Availability

**Endpoint**: `professional.updateAvailability`

**Method**: `POST`

**Auth**: Protected (Professional)

**Request Body**:
```json
{
"id": "availability_id",
"dayOfWeek": "MONDAY",
"startTime": "10:00",
"endTime": "18:00"
}
```

**Response**: Updated availability object

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Update availability documentation to use dayIndex.

Same issue as createAvailability - the documentation shows the old dayOfWeek field instead of the new dayIndex.

Apply this fix:

 **Request Body**:
 ```json
 {
   "id": "availability_id",
-  "dayOfWeek": "MONDAY",
+  "dayIndex": 1,
   "startTime": "10:00",
   "endTime": "18:00"
 }

<details>
<summary>🤖 Prompt for AI Agents</summary>

In docs/API.md around lines 442 to 461 the Update Availability example still
shows the old "dayOfWeek" field; replace it with "dayIndex" (number) in the
request body example and update the JSON snippet accordingly (e.g., "dayIndex":

  1. so the docs match the API change; ensure the surrounding text and any other
    example occurrences in this block reference dayIndex instead of dayOfWeek.

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines +463 to +472
let cursor = dayjs(`${date.format("YYYY-MM-DD")}T${a.startTime}`);
const end = dayjs(`${date.format("YYYY-MM-DD")}T${a.endTime}`);
while (
cursor.add(30, "minute").isBefore(end) ||
cursor.isSame(end)
) {
allSlots.push(cursor.toDate());
cursor = cursor.add(30, "minute");
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Stop emitting slots beyond the availability window

A slot starting exactly at endTime still survives this loop, so a 09:00–12:00 availability produces a 12:00 slot that would run until 12:30. That exposes customers to sessions the professional cannot fulfill.

-					for (const a of availability) {
-						let cursor = dayjs(`${date.format("YYYY-MM-DD")}T${a.startTime}`);
-						const end = dayjs(`${date.format("YYYY-MM-DD")}T${a.endTime}`);
-						while (
-							cursor.add(30, "minute").isBefore(end) ||
-							cursor.isSame(end)
-						) {
-							allSlots.push(cursor.toDate());
-							cursor = cursor.add(30, "minute");
-						}
-					}
+					for (const a of availability) {
+						let cursor = dayjs(`${date.format("YYYY-MM-DD")}T${a.startTime}`);
+						const end = dayjs(`${date.format("YYYY-MM-DD")}T${a.endTime}`);
+						while (true) {
+							const next = cursor.add(30, "minute");
+							if (next.valueOf() > end.valueOf()) {
+								break;
+							}
+							allSlots.push(cursor.toDate());
+							cursor = next;
+						}
+					}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let cursor = dayjs(`${date.format("YYYY-MM-DD")}T${a.startTime}`);
const end = dayjs(`${date.format("YYYY-MM-DD")}T${a.endTime}`);
while (
cursor.add(30, "minute").isBefore(end) ||
cursor.isSame(end)
) {
allSlots.push(cursor.toDate());
cursor = cursor.add(30, "minute");
}
}
for (const a of availability) {
let cursor = dayjs(`${date.format("YYYY-MM-DD")}T${a.startTime}`);
const end = dayjs(`${date.format("YYYY-MM-DD")}T${a.endTime}`);
while (true) {
const next = cursor.add(30, "minute");
if (next.valueOf() > end.valueOf()) {
break;
}
allSlots.push(cursor.toDate());
cursor = next;
}
}
🤖 Prompt for AI Agents
In packages/api/src/routers/customer.ts around lines 463–472, the loop can emit
a slot that starts exactly at the availability endTime (e.g., 12:00 for a
09:00–12:00 window). Change the loop so it only emits slots with start < end: do
not call cursor.add(...) inside the while condition (add mutates); instead, use
while(cursor.isBefore(end)) { allSlots.push(cursor.toDate()); cursor =
cursor.add(30, "minute"); } (or clone before adding) so a slot equal to endTime
is never produced.

Comment on lines +502 to +509
const minMinutes = d.hours * 60;
const validSlots = availableSlots.filter((slot) => {
const slotEnd = dayjs(slot).add(minMinutes, "minute");
// Ensure the entire duration fits within availability
return availableSlots.some((s) =>
dayjs(s).isSame(slotEnd.subtract(30, "minute")),
);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Require continuous 30-minute blocks for multi-hour slots

This check only looks for the final 30‑minute chunk, so with availability 09:00–11:00 and a confirmed booking at 09:30–10:30, the system still advertises a 2‑hour slot starting 09:00 even though the middle hour is gone. We need to verify that every intermediate 30‑minute block survives the booking filter.

-					const availableSlots = allSlots.filter((slot) => {
+					const availableSlots = allSlots.filter((slot) => {
 						const slotStart = dayjs(slot);
 						const slotEnd = slotStart.add(30, "minute");
 						return !bookedSlots.some((b) => {
 							const bStart = dayjs(b.scheduledStartTime);
 							const bEnd = b.completedAt
 								? dayjs(b.completedAt)
 								: bStart.add(2, "hour");
 							return slotStart.isBefore(bEnd) && slotEnd.isAfter(bStart);
 						});
 					});
 
-					// 6️⃣ For each duration, compute valid slots (enough continuous time)
+					const availableSlotTimes = new Set(
+						availableSlots.map((slot) => dayjs(slot).valueOf()),
+					);
+
+					// 6️⃣ For each duration, compute valid slots (enough continuous time)
 					const durationSlots = [];
 					for (const d of DURATIONS) {
 						const minMinutes = d.hours * 60;
-						const validSlots = availableSlots.filter((slot) => {
-							const slotEnd = dayjs(slot).add(minMinutes, "minute");
-							// Ensure the entire duration fits within availability
-							return availableSlots.some((s) =>
-								dayjs(s).isSame(slotEnd.subtract(30, "minute")),
-							);
-						});
+						const blocks = minMinutes / 30;
+						if (!Number.isInteger(blocks)) {
+							continue;
+						}
+						const validSlots = availableSlots.filter((slot) => {
+							const slotStart = dayjs(slot);
+							for (let step = 1; step < blocks; step++) {
+								const checkpoint = slotStart
+									.add(step * 30, "minute")
+									.valueOf();
+								if (!availableSlotTimes.has(checkpoint)) {
+									return false;
+								}
+							}
+							return true;
+						});

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/api/src/routers/customer.ts around lines 502–509, the current
validSlots filter only checks that the final 30‑minute chunk exists, so
multi‑hour slots can be advertised even if middle blocks are taken; change the
predicate to require that every 30‑minute block across the requested duration
exists in availableSlots. Compute blockCount = (d.hours * 60) / 30 and for each
candidate slot verify for i from 0 to blockCount-1 that dayjs(slot).add(i * 30,
"minute") (or its ISO/string form) is present in availableSlots (use
Array.prototype.every or an explicit loop) before including the slot. Ensure
comparisons use the same time granularity/format as availableSlots (e.g., isSame
on minute).

Comment on lines +308 to 311
dayIndex: z.number(),
startTime: z.string().regex(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/),
endTime: z.string().regex(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore validation on dayIndex

Switching from the enum to a bare number drops the guardrails we previously had. Any value (e.g. 7, 99, or 2.5) now gets persisted, and the scheduler will never match those rows. Please cap the field back to integers 0–6.

-				dayIndex: z.number(),
+				dayIndex: z.number().int().min(0).max(6),
@@
-				dayIndex: z.number(),
+				dayIndex: z.number().int().min(0).max(6),

Also applies to: 418-421

🤖 Prompt for AI Agents
In packages/api/src/routers/professional.ts around lines 308 to 311 (and
likewise lines 418 to 421) the schema switched dayIndex from an enum to a plain
number, allowing out-of-range and non-integer values; restore validation by
constraining dayIndex to an integer within 0–6 (e.g., use the Zod number
validators to require an int with min 0 and max 6) so only valid weekdays can be
persisted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review effort 3/5 Review effort 5/5 size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant