Merge dev into flea-flicker/gro-1162-pet-buffer-time
Resolve conflicts in appointments.ts (import style) and stage all fixes: - UAT_PLAYBOOK.md: add §4.4b buffer-aware availability test cases - Migration 0031: add buffer_minutes and pet_size_category columns - Fix DB conflict queries to use effective end (endTime + bufferMinutes) - Remove dead code in book.ts - Fix misleading test name in slots.test.ts Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -67,6 +67,20 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet
|
|||||||
| TC-API-4.9 | Cancel confirmation | POST /api/appointments/{id}/cancel | 200 OK, confirmation cancelled |
|
| TC-API-4.9 | Cancel confirmation | POST /api/appointments/{id}/cancel | 200 OK, confirmation cancelled |
|
||||||
| TC-API-4.10 | Conflict detection | POST /api/appointments with conflicting time | 409 Conflict, error message returned |
|
| TC-API-4.10 | Conflict detection | POST /api/appointments with conflicting time | 409 Conflict, error message returned |
|
||||||
|
|
||||||
|
### 4.4b Buffer-Aware Availability & Booking
|
||||||
|
|
||||||
|
| # | Scenario | Steps | Expected |
|
||||||
|
|---|---|---|---|
|
||||||
|
| TC-API-4b.1 | Buffer blocks subsequent slot | Create a large/long-coat appointment (30-min buffer), then check availability — next slot starts after 09:00 + duration + 30-min buffer | Available slot list correctly excludes times within buffer window |
|
||||||
|
| TC-API-4b.2 | Buffer resolves by pet size | GET /availability with petSizeCategory=large&petCoatType=long → expect larger buffer than small/normal | Slots reflect larger buffer, fewer available times |
|
||||||
|
| TC-API-4b.3 | Buffer resolves by pet size — small/short coat | GET /availability with petSizeCategory=small&petCoatType=short → expect 5-min buffer | Slots reflect smaller buffer, more available times |
|
||||||
|
| TC-API-4b.4 | Buffer defaults when pet info missing | GET /availability without petSizeCategory/petCoatType → defaults to medium/normal (10-min buffer) | Slots use default 10-min buffer |
|
||||||
|
| TC-API-4b.5 | Appointment stores bufferMinutes | POST /appointments with petSizeCategory=large&petCoatType=long → appointment record has bufferMinutes=30 | 201 Created, appointment.bufferMinutes = 30 |
|
||||||
|
| TC-API-4b.6 | Buffer prevents double-booking at buffer boundary | Groomer has 09:00–10:00 appointment with 30-min buffer; POST appointment at 10:15 → should succeed (10:15 > 10:30 effective end) | 201 Created |
|
||||||
|
| TC-API-4b.7 | Buffer prevents overlap booking | Groomer has 09:00–10:00 appointment with 30-min buffer; POST appointment at 10:00 → should be blocked (10:00 ≤ 10:30 effective end) | 409 Conflict |
|
||||||
|
| TC-API-4b.8 | Backward compatibility — no buffer params | GET /availability without petSizeCategory/petCoatType and POST without them | Behaves as before with 0-min buffer or default 10-min |
|
||||||
|
| TC-API-4b.9 | Admin booking also uses buffers | Create appointment via POST /api/appointments (admin) with pet info → bufferMinutes resolved and stored | 201 Created, bufferMinutes set |
|
||||||
|
|
||||||
### 4.5 Services
|
### 4.5 Services
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- Migration: 0031_buffer_and_pet_size
|
||||||
|
-- Adds buffer_minutes to appointments and pet_size_category to pets
|
||||||
|
-- (buffer_minutes was already in schema.ts but no migration created the column)
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS buffer_minutes integer NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE pets ADD COLUMN IF NOT EXISTS pet_size_category text;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,512 @@
|
|||||||
|
{
|
||||||
|
"id": "0031_buffer_and_pet_size",
|
||||||
|
"prevId": "0030_extended_pet_profile",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.appointments": {
|
||||||
|
"name": "appointments",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"client_id": {
|
||||||
|
"name": "client_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"pet_id": {
|
||||||
|
"name": "pet_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"service_id": {
|
||||||
|
"name": "service_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"staff_id": {
|
||||||
|
"name": "staff_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"bather_staff_id": {
|
||||||
|
"name": "bather_staff_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "appointment_status",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'scheduled'"
|
||||||
|
},
|
||||||
|
"start_time": {
|
||||||
|
"name": "start_time",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"end_time": {
|
||||||
|
"name": "end_time",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"name": "notes",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"name": "price_cents",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"series_id": {
|
||||||
|
"name": "series_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"series_index": {
|
||||||
|
"name": "series_index",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"group_id": {
|
||||||
|
"name": "group_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"confirmation_status": {
|
||||||
|
"name": "confirmation_status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'pending'"
|
||||||
|
},
|
||||||
|
"confirmed_at": {
|
||||||
|
"name": "confirmed_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cancelled_at": {
|
||||||
|
"name": "cancelled_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"confirmation_token": {
|
||||||
|
"name": "confirmation_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_notes": {
|
||||||
|
"name": "customer_notes",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"buffer_minutes": {
|
||||||
|
"name": "buffer_minutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"appointments_client_id_clients_id_fk": {
|
||||||
|
"name": "appointments_client_id_clients_id_fk",
|
||||||
|
"tableFrom": "appointments",
|
||||||
|
"tableTo": "clients",
|
||||||
|
"columnsFrom": [
|
||||||
|
"client_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "restrict",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"appointments_pet_id_pets_id_fk": {
|
||||||
|
"name": "appointments_pet_id_pets_id_fk",
|
||||||
|
"tableFrom": "appointments",
|
||||||
|
"tableTo": "pets",
|
||||||
|
"columnsFrom": [
|
||||||
|
"pet_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "restrict",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"appointments_service_id_services_id_fk": {
|
||||||
|
"name": "appointments_service_id_services_id_fk",
|
||||||
|
"tableFrom": "appointments",
|
||||||
|
"tableTo": "services",
|
||||||
|
"columnsFrom": [
|
||||||
|
"service_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "restrict",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"appointments_staff_id_staff_id_fk": {
|
||||||
|
"name": "appointments_staff_id_staff_id_fk",
|
||||||
|
"tableFrom": "appointments",
|
||||||
|
"tableTo": "staff",
|
||||||
|
"columnsFrom": [
|
||||||
|
"staff_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"appointments_bather_staff_id_staff_id_fk": {
|
||||||
|
"name": "appointments_bather_staff_id_staff_id_fk",
|
||||||
|
"tableFrom": "appointments",
|
||||||
|
"tableTo": "staff",
|
||||||
|
"columnsFrom": [
|
||||||
|
"bather_staff_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"appointments_series_id_recurring_series_id_fk": {
|
||||||
|
"name": "appointments_series_id_recurring_series_id_fk",
|
||||||
|
"tableFrom": "appointments",
|
||||||
|
"tableTo": "recurring_series",
|
||||||
|
"columnsFrom": [
|
||||||
|
"series_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"appointments_group_id_appointment_groups_id_fk": {
|
||||||
|
"name": "appointments_group_id_appointment_groups_id_fk",
|
||||||
|
"tableFrom": "appointments",
|
||||||
|
"tableTo": "appointment_groups",
|
||||||
|
"columnsFrom": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"appointments_confirmation_token_unique": {
|
||||||
|
"name": "appointments_confirmation_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"confirmation_token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.pets": {
|
||||||
|
"name": "pets",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"client_id": {
|
||||||
|
"name": "client_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"species": {
|
||||||
|
"name": "species",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"breed": {
|
||||||
|
"name": "breed",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"weight_kg": {
|
||||||
|
"name": "weight_kg",
|
||||||
|
"type": "numeric(5, 2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"date_of_birth": {
|
||||||
|
"name": "date_of_birth",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"health_alerts": {
|
||||||
|
"name": "health_alerts",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"grooming_notes": {
|
||||||
|
"name": "grooming_notes",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cut_style": {
|
||||||
|
"name": "cut_style",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"shampoo_preference": {
|
||||||
|
"name": "shampoo_preference",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"special_care_notes": {
|
||||||
|
"name": "special_care_notes",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"custom_fields": {
|
||||||
|
"name": "custom_fields",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"photo_key": {
|
||||||
|
"name": "photo_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"photo_uploaded_at": {
|
||||||
|
"name": "photo_uploaded_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"pet_size_category": {
|
||||||
|
"name": "pet_size_category",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"coat_type": {
|
||||||
|
"name": "coat_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"temperament_score": {
|
||||||
|
"name": "temperament_score",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"temperament_flags": {
|
||||||
|
"name": "temperament_flags",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'[]'::jsonb"
|
||||||
|
},
|
||||||
|
"medical_alerts": {
|
||||||
|
"name": "medical_alerts",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'[]'::jsonb"
|
||||||
|
},
|
||||||
|
"preferred_cuts": {
|
||||||
|
"name": "preferred_cuts",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'[]'::jsonb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"pets_client_id_clients_id_fk": {
|
||||||
|
"name": "pets_client_id_clients_id_fk",
|
||||||
|
"tableFrom": "pets",
|
||||||
|
"tableTo": "clients",
|
||||||
|
"columnsFrom": [
|
||||||
|
"client_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"public.appointment_status": {
|
||||||
|
"name": "appointment_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"scheduled",
|
||||||
|
"confirmed",
|
||||||
|
"in_progress",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
"no_show"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.client_status": {
|
||||||
|
"name": "client_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"active",
|
||||||
|
"disabled"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.impersonation_session_status": {
|
||||||
|
"name": "impersonation_session_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"active",
|
||||||
|
"ended",
|
||||||
|
"expired"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.invoice_status": {
|
||||||
|
"name": "invoice_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"draft",
|
||||||
|
"pending",
|
||||||
|
"paid",
|
||||||
|
"void"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.payment_method": {
|
||||||
|
"name": "payment_method",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"cash",
|
||||||
|
"card",
|
||||||
|
"check",
|
||||||
|
"other"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.staff_role": {
|
||||||
|
"name": "staff_role",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"groomer",
|
||||||
|
"receptionist",
|
||||||
|
"manager"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.waitlist_status": {
|
||||||
|
"name": "waitlist_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"active",
|
||||||
|
"notified",
|
||||||
|
"expired",
|
||||||
|
"cancelled"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -218,6 +218,13 @@
|
|||||||
"when": 1775914467192,
|
"when": 1775914467192,
|
||||||
"tag": "0030_extended_pet_profile",
|
"tag": "0030_extended_pet_profile",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 31,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1776000867192,
|
||||||
|
"tag": "0031_buffer_and_pet_size",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -214,7 +214,7 @@ describe("generateAvailableSlots", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveBufferMinutes", () => {
|
describe("resolveBufferMinutes", () => {
|
||||||
it("returns 0 buffer for unknown size/coat", () => {
|
it("returns 10-min buffer for unknown/mixed size/coat (medium/normal default)", () => {
|
||||||
expect(resolveBufferMinutes({})).toBe(10);
|
expect(resolveBufferMinutes({})).toBe(10);
|
||||||
expect(resolveBufferMinutes({ petSizeCategory: "medium", petCoatType: "normal" })).toBe(10);
|
expect(resolveBufferMinutes({ petSizeCategory: "medium", petCoatType: "normal" })).toBe(10);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,11 +94,6 @@ function pick<T>(arr: T[]): T {
|
|||||||
return arr[Math.floor(rand() * arr.length)]!;
|
return arr[Math.floor(rand() * arr.length)]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return n distinct random elements from an array. */
|
|
||||||
function pickN<T>(arr: T[], n: number): T[] {
|
|
||||||
const shuffled = [...arr].sort(() => rand() - 0.5);
|
|
||||||
return shuffled.slice(0, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
function randInt(min: number, max: number): number {
|
function randInt(min: number, max: number): number {
|
||||||
return Math.floor(rand() * (max - min + 1)) + min;
|
return Math.floor(rand() * (max - min + 1)) + min;
|
||||||
@@ -1105,7 +1100,7 @@ async function seed() {
|
|||||||
const groomer = pick(groomers);
|
const groomer = pick(groomers);
|
||||||
const bather = bathers.length > 0 && rand() < 0.6 ? pick(bathers) : null;
|
const bather = bathers.length > 0 && rand() < 0.6 ? pick(bathers) : null;
|
||||||
|
|
||||||
let startTime = randDate(appointmentsBackDate, now);
|
const startTime = randDate(appointmentsBackDate, now);
|
||||||
startTime.setHours(randInt(8, 16), pick([0, 15, 30, 45]), 0, 0);
|
startTime.setHours(randInt(8, 16), pick([0, 15, 30, 45]), 0, 0);
|
||||||
const endTime = new Date(startTime.getTime() + svc.dur * 60 * 1000);
|
const endTime = new Date(startTime.getTime() + svc.dur * 60 * 1000);
|
||||||
const effectivePrice = svc.price;
|
const effectivePrice = svc.price;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { searchRouter } from "./routes/search.js";
|
|||||||
import { getObject } from "./lib/s3.js";
|
import { getObject } from "./lib/s3.js";
|
||||||
import { calendarRouter } from "./routes/calendar.js";
|
import { calendarRouter } from "./routes/calendar.js";
|
||||||
import { setupRouter } from "./routes/setup.js";
|
import { setupRouter } from "./routes/setup.js";
|
||||||
import { getDb, businessSettings, eq, staff } from "./db";
|
import { getDb, businessSettings, eq, staff } from "./db/index.js";
|
||||||
import { authMiddleware } from "./middleware/auth.js";
|
import { authMiddleware } from "./middleware/auth.js";
|
||||||
import { resolveStaffMiddleware, requireRole, requireRoleOrSuperUser, requireSuperUser } from "./middleware/rbac.js";
|
import { resolveStaffMiddleware, requireRole, requireRoleOrSuperUser, requireSuperUser } from "./middleware/rbac.js";
|
||||||
import { devRouter } from "./routes/dev.js";
|
import { devRouter } from "./routes/dev.js";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { genericOAuth } from "better-auth/plugins";
|
import { genericOAuth } from "better-auth/plugins";
|
||||||
import { getDb, authProviderConfig, eq } from "./db";
|
import { getDb, authProviderConfig, eq } from "../db/index.js";
|
||||||
import { decryptSecret } from "./db";
|
import { decryptSecret } from "../db/index.js";
|
||||||
import { sendEmail } from "../services/email.js";
|
import { sendEmail } from "../services/email.js";
|
||||||
|
|
||||||
const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET;
|
const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET;
|
||||||
@@ -97,6 +97,9 @@ export async function initAuth(): Promise<void> {
|
|||||||
window: 10,
|
window: 10,
|
||||||
storage: "memory",
|
storage: "memory",
|
||||||
customRules: {
|
customRules: {
|
||||||
|
"/sign-in/social": { max: 10, window: 60 },
|
||||||
|
"/sign-in/email": { max: 10, window: 60 },
|
||||||
|
"/sign-up/email": { max: 5, window: 60 },
|
||||||
"/get-session": false,
|
"/get-session": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -247,6 +250,9 @@ export async function initAuth(): Promise<void> {
|
|||||||
window: 10,
|
window: 10,
|
||||||
storage: "memory",
|
storage: "memory",
|
||||||
customRules: {
|
customRules: {
|
||||||
|
"/sign-in/social": { max: 10, window: 60 },
|
||||||
|
"/sign-in/email": { max: 10, window: 60 },
|
||||||
|
"/sign-up/email": { max: 5, window: 60 },
|
||||||
"/get-session": false,
|
"/get-session": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { MiddlewareHandler } from "hono";
|
import type { MiddlewareHandler } from "hono";
|
||||||
import { getDb, impersonationAuditLogs } from "../db";
|
import { getDb, impersonationAuditLogs } from "../db/index.js";
|
||||||
import type { PortalEnv } from "./portalSession.js";
|
import type { PortalEnv } from "./portalSession.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { MiddlewareHandler } from "hono";
|
import type { MiddlewareHandler } from "hono";
|
||||||
import { and, eq, getDb, impersonationSessions } from "../db";
|
import { and, eq, getDb, impersonationSessions } from "../db/index.js";
|
||||||
|
|
||||||
export interface PortalEnv {
|
export interface PortalEnv {
|
||||||
Variables: {
|
Variables: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { MiddlewareHandler } from "hono";
|
import type { MiddlewareHandler } from "hono";
|
||||||
import { and, eq, getDb, sql, staff } from "../db";
|
import { and, eq, getDb, sql, staff } from "../db/index.js";
|
||||||
|
|
||||||
export type StaffRole = "groomer" | "receptionist" | "manager";
|
export type StaffRole = "groomer" | "receptionist" | "manager";
|
||||||
export type StaffRow = typeof staff.$inferSelect;
|
export type StaffRow = typeof staff.$inferSelect;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { eq, getDb, staff, clients, pets, services } from "./db";
|
import { eq, getDb, staff, clients, pets, services } from "../../db/index.js";
|
||||||
|
|
||||||
export const adminSeedRouter = new Hono();
|
export const adminSeedRouter = new Hono();
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
pets,
|
pets,
|
||||||
services,
|
services,
|
||||||
staff,
|
staff,
|
||||||
} from "../db";
|
} from "../db/index.js";
|
||||||
import type { AppEnv } from "../middleware/rbac.js";
|
import type { AppEnv } from "../middleware/rbac.js";
|
||||||
|
|
||||||
export const appointmentGroupsRouter = new Hono<AppEnv>();
|
export const appointmentGroupsRouter = new Hono<AppEnv>();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
lte,
|
lte,
|
||||||
ne,
|
ne,
|
||||||
or,
|
or,
|
||||||
|
sql,
|
||||||
appointments,
|
appointments,
|
||||||
clients,
|
clients,
|
||||||
pets,
|
pets,
|
||||||
@@ -19,13 +20,8 @@ import {
|
|||||||
services,
|
services,
|
||||||
staff,
|
staff,
|
||||||
} from "../db";
|
} from "../db";
|
||||||
import {
|
import { buildConfirmationEmail, sendEmail } from "../services/email.js";
|
||||||
buildConfirmationEmail,
|
import { resolveBufferMinutes } from "../lib/slots.js";
|
||||||
sendEmail,
|
|
||||||
} from "../services/email.js";
|
|
||||||
import {
|
|
||||||
resolveBufferMinutes,
|
|
||||||
} from "../lib/slots.js";
|
|
||||||
import { notifyWaitlistForAppointment } from "../services/waitlistNotify.js";
|
import { notifyWaitlistForAppointment } from "../services/waitlistNotify.js";
|
||||||
import type { AppEnv } from "../middleware/rbac.js";
|
import type { AppEnv } from "../middleware/rbac.js";
|
||||||
|
|
||||||
@@ -193,7 +189,7 @@ appointmentsRouter.post(
|
|||||||
and(
|
and(
|
||||||
eq(appointments.staffId, apptFields.staffId),
|
eq(appointments.staffId, apptFields.staffId),
|
||||||
lt(appointments.startTime, endWithBuffer),
|
lt(appointments.startTime, endWithBuffer),
|
||||||
gte(appointments.endTime, start),
|
sql`${appointments.endTime} + (${appointments.bufferMinutes} || ' minutes')::interval > ${start}`,
|
||||||
ne(appointments.status, "cancelled"),
|
ne(appointments.status, "cancelled"),
|
||||||
ne(appointments.status, "no_show"),
|
ne(appointments.status, "no_show"),
|
||||||
)
|
)
|
||||||
@@ -215,7 +211,7 @@ appointmentsRouter.post(
|
|||||||
eq(appointments.batherStaffId, apptFields.batherStaffId)
|
eq(appointments.batherStaffId, apptFields.batherStaffId)
|
||||||
),
|
),
|
||||||
lt(appointments.startTime, endWithBuffer),
|
lt(appointments.startTime, endWithBuffer),
|
||||||
gte(appointments.endTime, start),
|
sql`${appointments.endTime} + (${appointments.bufferMinutes} || ' minutes')::interval > ${start}`,
|
||||||
ne(appointments.status, "cancelled"),
|
ne(appointments.status, "cancelled"),
|
||||||
ne(appointments.status, "no_show"),
|
ne(appointments.status, "no_show"),
|
||||||
)
|
)
|
||||||
@@ -267,7 +263,7 @@ appointmentsRouter.post(
|
|||||||
and(
|
and(
|
||||||
eq(appointments.staffId, apptFields.staffId),
|
eq(appointments.staffId, apptFields.staffId),
|
||||||
lt(appointments.startTime, instanceEndWithBuffer),
|
lt(appointments.startTime, instanceEndWithBuffer),
|
||||||
gte(appointments.endTime, instanceStart),
|
sql`${appointments.endTime} + (${appointments.bufferMinutes} || ' minutes')::interval > ${instanceStart}`,
|
||||||
ne(appointments.status, "cancelled"),
|
ne(appointments.status, "cancelled"),
|
||||||
ne(appointments.status, "no_show"),
|
ne(appointments.status, "no_show"),
|
||||||
)
|
)
|
||||||
@@ -289,7 +285,7 @@ appointmentsRouter.post(
|
|||||||
eq(appointments.batherStaffId, apptFields.batherStaffId)
|
eq(appointments.batherStaffId, apptFields.batherStaffId)
|
||||||
),
|
),
|
||||||
lt(appointments.startTime, instanceEndWithBuffer),
|
lt(appointments.startTime, instanceEndWithBuffer),
|
||||||
gte(appointments.endTime, instanceStart),
|
sql`${appointments.endTime} + (${appointments.bufferMinutes} || ' minutes')::interval > ${instanceStart}`,
|
||||||
ne(appointments.status, "cancelled"),
|
ne(appointments.status, "cancelled"),
|
||||||
ne(appointments.status, "no_show"),
|
ne(appointments.status, "no_show"),
|
||||||
)
|
)
|
||||||
@@ -498,7 +494,7 @@ appointmentsRouter.patch(
|
|||||||
and(
|
and(
|
||||||
eq(appointments.staffId, newStaffId),
|
eq(appointments.staffId, newStaffId),
|
||||||
lt(appointments.startTime, conflictEnd),
|
lt(appointments.startTime, conflictEnd),
|
||||||
gte(appointments.endTime, newStart),
|
sql`${appointments.endTime} + (${appointments.bufferMinutes} || ' minutes')::interval > ${newStart}`,
|
||||||
ne(appointments.status, "cancelled"),
|
ne(appointments.status, "cancelled"),
|
||||||
ne(appointments.status, "no_show"),
|
ne(appointments.status, "no_show"),
|
||||||
ne(appointments.id, appt.id),
|
ne(appointments.id, appt.id),
|
||||||
@@ -528,7 +524,7 @@ appointmentsRouter.patch(
|
|||||||
eq(appointments.batherStaffId, newBatherStaffId)
|
eq(appointments.batherStaffId, newBatherStaffId)
|
||||||
),
|
),
|
||||||
lt(appointments.startTime, conflictEnd),
|
lt(appointments.startTime, conflictEnd),
|
||||||
gte(appointments.endTime, newStart),
|
sql`${appointments.endTime} + (${appointments.bufferMinutes} || ' minutes')::interval > ${newStart}`,
|
||||||
ne(appointments.status, "cancelled"),
|
ne(appointments.status, "cancelled"),
|
||||||
ne(appointments.status, "no_show"),
|
ne(appointments.status, "no_show"),
|
||||||
ne(appointments.id, appt.id),
|
ne(appointments.id, appt.id),
|
||||||
@@ -653,7 +649,7 @@ appointmentsRouter.patch(
|
|||||||
and(
|
and(
|
||||||
eq(appointments.staffId, staffId),
|
eq(appointments.staffId, staffId),
|
||||||
lt(appointments.startTime, conflictEnd),
|
lt(appointments.startTime, conflictEnd),
|
||||||
gte(appointments.endTime, start),
|
sql`${appointments.endTime} + (${appointments.bufferMinutes} || ' minutes')::interval > ${start}`,
|
||||||
ne(appointments.status, "cancelled"),
|
ne(appointments.status, "cancelled"),
|
||||||
ne(appointments.status, "no_show"),
|
ne(appointments.status, "no_show"),
|
||||||
ne(appointments.id, id),
|
ne(appointments.id, id),
|
||||||
@@ -679,7 +675,7 @@ appointmentsRouter.patch(
|
|||||||
eq(appointments.batherStaffId, batherStaffId)
|
eq(appointments.batherStaffId, batherStaffId)
|
||||||
),
|
),
|
||||||
lt(appointments.startTime, conflictEnd),
|
lt(appointments.startTime, conflictEnd),
|
||||||
gte(appointments.endTime, start),
|
sql`${appointments.endTime} + (${appointments.bufferMinutes} || ' minutes')::interval > ${start}`,
|
||||||
ne(appointments.status, "cancelled"),
|
ne(appointments.status, "cancelled"),
|
||||||
ne(appointments.status, "no_show"),
|
ne(appointments.status, "no_show"),
|
||||||
ne(appointments.id, id),
|
ne(appointments.id, id),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { z } from "zod/v3";
|
import { z } from "zod/v3";
|
||||||
import { eq, getDb, authProviderConfig, encryptSecret } from "../db";
|
import { eq, getDb, authProviderConfig, encryptSecret } from "../db/index.js";
|
||||||
import { requireSuperUser } from "../middleware/rbac.js";
|
import { requireSuperUser } from "../middleware/rbac.js";
|
||||||
import { reinitAuth } from "../lib/auth.js";
|
import { reinitAuth } from "../lib/auth.js";
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
appointments,
|
appointments,
|
||||||
clients,
|
clients,
|
||||||
pets,
|
pets,
|
||||||
} from "../db";
|
} from "../db/index.js";
|
||||||
import {
|
import {
|
||||||
generateAvailableSlots,
|
generateAvailableSlots,
|
||||||
resolveBufferMinutes,
|
resolveBufferMinutes,
|
||||||
@@ -184,9 +184,6 @@ bookRouter.post(
|
|||||||
if (effectiveEnd > existing) busyGroomers.set(b.staffId ?? "", effectiveEnd);
|
if (effectiveEnd > existing) busyGroomers.set(b.staffId ?? "", effectiveEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// New appointment's effective end (end + its buffer)
|
|
||||||
const newEffectiveEnd = endWithBuffer.getTime();
|
|
||||||
|
|
||||||
const freeGroomer = groomers.find(({ id }) => {
|
const freeGroomer = groomers.find(({ id }) => {
|
||||||
const busyUntil = busyGroomers.get(id) ?? 0;
|
const busyUntil = busyGroomers.get(id) ?? 0;
|
||||||
return busyUntil <= start.getTime();
|
return busyUntil <= start.getTime();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
pets,
|
pets,
|
||||||
services,
|
services,
|
||||||
staff,
|
staff,
|
||||||
} from "../db";
|
} from "../db/index.js";
|
||||||
|
|
||||||
export const calendarRouter = new Hono();
|
export const calendarRouter = new Hono();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { z } from "zod/v3";
|
import { z } from "zod/v3";
|
||||||
import { and, eq, exists, getDb, or, clients, appointments } from "../db";
|
import { and, eq, exists, getDb, or, clients, appointments } from "../db/index.js";
|
||||||
import type { AppEnv } from "../middleware/rbac.js";
|
import type { AppEnv } from "../middleware/rbac.js";
|
||||||
|
|
||||||
export const clientsRouter = new Hono<AppEnv>();
|
export const clientsRouter = new Hono<AppEnv>();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { getDb, staff, clients, eq, sql } from "../db";
|
import { getDb, staff, clients, eq, sql } from "../db/index.js";
|
||||||
|
|
||||||
const devRouter = new Hono();
|
const devRouter = new Hono();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { z } from "zod/v3";
|
import { z } from "zod/v3";
|
||||||
import { and, desc, eq, getDb, groomingVisitLogs, appointments, or } from "../db";
|
import { and, desc, eq, getDb, groomingVisitLogs, appointments, or } from "../db/index.js";
|
||||||
import type { AppEnv } from "../middleware/rbac.js";
|
import type { AppEnv } from "../middleware/rbac.js";
|
||||||
|
|
||||||
export const groomingLogsRouter = new Hono<AppEnv>();
|
export const groomingLogsRouter = new Hono<AppEnv>();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
impersonationAuditLogs,
|
impersonationAuditLogs,
|
||||||
clients,
|
clients,
|
||||||
desc,
|
desc,
|
||||||
} from "../db";
|
} from "../db/index.js";
|
||||||
import type { AppEnv } from "../middleware/rbac.js";
|
import type { AppEnv } from "../middleware/rbac.js";
|
||||||
|
|
||||||
export const impersonationRouter = new Hono<AppEnv>();
|
export const impersonationRouter = new Hono<AppEnv>();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
services,
|
services,
|
||||||
clients,
|
clients,
|
||||||
sql,
|
sql,
|
||||||
} from "../db";
|
} from "../db/index.js";
|
||||||
import type { AppEnv } from "../middleware/rbac.js";
|
import type { AppEnv } from "../middleware/rbac.js";
|
||||||
|
|
||||||
export const invoicesRouter = new Hono<AppEnv>();
|
export const invoicesRouter = new Hono<AppEnv>();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { z } from "zod/v3";
|
import { z } from "zod/v3";
|
||||||
import { and, eq, exists, getDb, or, pets, appointments } from "../db";
|
import { and, eq, exists, getDb, or, pets, appointments } from "../db/index.js";
|
||||||
import type { AppEnv } from "../middleware/rbac.js";
|
import type { AppEnv } from "../middleware/rbac.js";
|
||||||
import {
|
import {
|
||||||
getPresignedUploadUrl,
|
getPresignedUploadUrl,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { z } from "zod/v3";
|
import { z } from "zod/v3";
|
||||||
import { eq, inArray } from "../db";
|
import { eq, inArray } from "../db/index.js";
|
||||||
import { getDb, appointments, impersonationSessions, waitlistEntries, clients, pets, services, staff, invoices, invoiceLineItems } from "../db";
|
import { getDb, appointments, impersonationSessions, waitlistEntries, clients, pets, services, staff, invoices, invoiceLineItems } from "../db/index.js";
|
||||||
import { validatePortalSession } from "../middleware/portalSession.js";
|
import { validatePortalSession } from "../middleware/portalSession.js";
|
||||||
import { portalAudit } from "../middleware/portalAudit.js";
|
import { portalAudit } from "../middleware/portalAudit.js";
|
||||||
import type { PortalEnv } from "../middleware/portalSession.js";
|
import type { PortalEnv } from "../middleware/portalSession.js";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
invoiceTipSplits,
|
invoiceTipSplits,
|
||||||
services,
|
services,
|
||||||
staff,
|
staff,
|
||||||
} from "../db";
|
} from "../db/index.js";
|
||||||
|
|
||||||
export const reportsRouter = new Hono();
|
export const reportsRouter = new Hono();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { and, eq, getDb, clients, ilike, or, pets } from "../db";
|
import { and, eq, getDb, clients, ilike, or, pets } from "../db/index.js";
|
||||||
|
|
||||||
export const searchRouter = new Hono();
|
export const searchRouter = new Hono();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { z } from "zod/v3";
|
import { z } from "zod/v3";
|
||||||
import { eq, getDb, services } from "../db";
|
import { eq, getDb, services } from "../db/index.js";
|
||||||
|
|
||||||
export const servicesRouter = new Hono();
|
export const servicesRouter = new Hono();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { z } from "zod/v3";
|
import { z } from "zod/v3";
|
||||||
import { eq, getDb, businessSettings } from "../db";
|
import { eq, getDb, businessSettings } from "../db/index.js";
|
||||||
import { getPresignedUploadUrl, deleteObject, putObject, getObject } from "../lib/s3.js";
|
import { getPresignedUploadUrl, deleteObject, putObject, getObject } from "../lib/s3.js";
|
||||||
import { requireSuperUser } from "../middleware/rbac.js";
|
import { requireSuperUser } from "../middleware/rbac.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { z } from "zod/v3";
|
import { z } from "zod/v3";
|
||||||
import { and, eq, getDb, sql, staff, businessSettings, authProviderConfig, encryptSecret } from "../db";
|
import { and, eq, getDb, sql, staff, businessSettings, authProviderConfig, encryptSecret } from "../db/index.js";
|
||||||
import type { AppEnv } from "../middleware/rbac.js";
|
import type { AppEnv } from "../middleware/rbac.js";
|
||||||
|
|
||||||
const RATE_LIMIT_WINDOW_MS = 60_000;
|
const RATE_LIMIT_WINDOW_MS = 60_000;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Hono } from "hono";
|
|||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { z } from "zod/v3";
|
import { z } from "zod/v3";
|
||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { and, eq, getDb, ne, staff, appointments } from "../db";
|
import { and, eq, getDb, ne, staff, appointments } from "../db/index.js";
|
||||||
import type { AppEnv } from "../middleware/rbac.js";
|
import type { AppEnv } from "../middleware/rbac.js";
|
||||||
|
|
||||||
export const staffRouter = new Hono<AppEnv>();
|
export const staffRouter = new Hono<AppEnv>();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { z } from "zod/v3";
|
import { z } from "zod/v3";
|
||||||
import { eq, getDb, invoices } from "../db";
|
import { eq, getDb, invoices } from "../db/index.js";
|
||||||
import { getStripeClient } from "../services/payment.js";
|
import { getStripeClient } from "../services/payment.js";
|
||||||
|
|
||||||
export const webhooksRouter = new Hono();
|
export const webhooksRouter = new Hono();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
clients,
|
clients,
|
||||||
pets,
|
pets,
|
||||||
services,
|
services,
|
||||||
} from "../db";
|
} from "../db/index.js";
|
||||||
import type { AppEnv } from "../middleware/rbac.js";
|
import type { AppEnv } from "../middleware/rbac.js";
|
||||||
|
|
||||||
export const waitlistRouter = new Hono<AppEnv>();
|
export const waitlistRouter = new Hono<AppEnv>();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { getDb, clients, eq, inArray, invoices } from "../db";
|
import { getDb, clients, eq, inArray, invoices } from "../db/index.js";
|
||||||
|
|
||||||
let _stripe: Stripe | null | undefined;
|
let _stripe: Stripe | null | undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
staff,
|
staff,
|
||||||
reminderLogs,
|
reminderLogs,
|
||||||
session,
|
session,
|
||||||
} from "../db";
|
} from "../db/index.js";
|
||||||
import {
|
import {
|
||||||
buildReminderEmail,
|
buildReminderEmail,
|
||||||
sendEmail,
|
sendEmail,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, eq, getDb, waitlistEntries, clients, pets, services } from "../db";
|
import { and, eq, getDb, waitlistEntries, clients, pets, services } from "../db/index.js";
|
||||||
import { buildWaitlistNotificationEmail, sendEmail } from "./email.js";
|
import { buildWaitlistNotificationEmail, sendEmail } from "./email.js";
|
||||||
|
|
||||||
export async function notifyWaitlistForAppointment(
|
export async function notifyWaitlistForAppointment(
|
||||||
|
|||||||
Reference in New Issue
Block a user