getAvailableSlots added#4
Conversation
Reviewer's GuideAdds 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 routersequenceDiagram
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
ER diagram for ProfessionalAvailability and ProfessionalAvailabilityExceptionerDiagram
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
}
Class diagram for ProfessionalAvailability and ProfessionalAvailabilityException changesclassDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Note Other AI code review bot(s) detectedCodeRabbit 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. 📝 WalkthroughSummary by CodeRabbit
WalkthroughThis 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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~90+ minutes Areas requiring extra attention:
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
|
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 · |
PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
|||||||||||||||||||||||||||
There was a problem hiding this comment.
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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const professionalAvailability = | ||
| await prisma.professionalAvailability.create({ | ||
| data: { | ||
| dayOfWeek: input.dayOfWeek, | ||
| dayIndex: input.dayIndex, | ||
| startTime: input.startTime, | ||
| endTime: input.endTime, | ||
| professionalId: professionalId, |
There was a problem hiding this comment.
suggestion (code-quality): Inline variable that is immediately returned (inline-immediately-returned-variable)
| 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, | |
| }, | |
| }); | |
Explanation
Something that we often see in people's code is assigning to a result variableand 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.
| professionalId: professionalId, | ||
| }, | ||
| data: { | ||
| dayOfWeek: input.dayOfWeek, | ||
| dayIndex: input.dayIndex, | ||
| startTime: input.startTime, | ||
| endTime: input.endTime, | ||
| }, |
There was a problem hiding this comment.
suggestion (code-quality): Inline variable that is immediately returned (inline-immediately-returned-variable)
| 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, | |
| }, | |
| }); | |
Explanation
Something that we often see in people's code is assigning to a result variableand 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.
PR Code Suggestions ✨Explore these optional code suggestions:
|
||||||||||||||||
| FROM professional p | ||
| JOIN professional_service ps ON ps."professionalId" = p._id | ||
| WHERE ps."serviceId" = ${serviceId} | ||
| AND p."approvalStatus" = 'APPROVED' |
There was a problem hiding this comment.
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
| 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.| while ( | ||
| cursor.add(30, "minute").isBefore(end) || | ||
| cursor.isSame(end) | ||
| ) { |
There was a problem hiding this comment.
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 🚨
| 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.| startTime: z.string(), | ||
| endTime: z.string(), |
There was a problem hiding this comment.
Suggestion: Apply the same time format validation to updateAvailability inputs by adding the regex to startTime and endTime [enhancement]
Severity Level: Minor
| 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"; |
There was a problem hiding this comment.
Suggestion: Use IF EXISTS when dropping the index to avoid migration failure if the index is already absent. [possible issue]
Severity Level: Minor
| 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 finished reviewing your PR. |
There was a problem hiding this comment.
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
bashspecified ✓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:
- Accepting a timezone parameter and explicitly setting it
- Documenting the expected timezone behavior
- 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
⛔ Files ignored due to path filters (1)
bun.lockis 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
professionalAvailabilityExceptionsandbookingNotificationsare properly defined and align with the expanded data model.
120-134: LGTM! Availability model evolution looks good.The transition from
dayOfWeekenum todayIndexinteger provides more flexibility. The addition ofbreakSlots,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
ProfessionalAvailabilityExceptionmodel properly supports per-date availability overrides with appropriate constraints:
- Unique constraint on
[professionalId, date]prevents duplicates- Index on
datefor efficient temporal queries- Cascade deletion maintains referential integrity
- Optional
startTime/endTimeallows both full-day and partial-day exceptionspackages/api/package.json (1)
23-34: LGTM! Dependency management is appropriate.The addition of
dayjs@^1.11.19supports 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
ProfessionalAvailabilityExceptiontable 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_availabilitytable 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 requireddayIndexcolumn 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_availabilityrecords.Likely an incorrect or invalid review comment.
| ### 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 |
There was a problem hiding this comment.
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 -->
| #### 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 | ||
|
|
There was a problem hiding this comment.
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":
- 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 -->
| 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"); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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")), | ||
| ); | ||
| }); |
There was a problem hiding this comment.
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).
| 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]$/), | ||
| }), |
There was a problem hiding this comment.
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.
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:
Enhancements:
Documentation:
CodeAnt-AI Description
Customer can fetch available booking slots for a service near a location
What Changed
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:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
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:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
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.