Initial kitchen scaffold — Phase 1 kitchen port (build-verified 2026-07-11)
FastAPI backend (Python 3.11, MSSQL ODBC for SambaPOS, Azure DI OCR),
kitchen_db on central PG. React/TS/Vite frontend with navy sidebar layout.
Backend: auth.py (APP_SLUG=kitchen, SimpleNamespace — archive routes use
.kitchen_id/.is_admin without modification), main.py (51 migrations, scheduler,
internal router for KDS bookings feed), api/internal.py, full archive API
(31 routers: invoices, recipes, menus, sambapos, resos, newbook, disputes,
purchase_orders, etc.), models, migrations, OCR pipeline.
kitchen_id pinned to 1 (B1 — single hotel).
Frontend: AuthGate (app=kitchen, token shim for archive compat — B5b pending),
Layout (navy sidebar, 6 sections, Lucide icons, teal --app-primary),
App.tsx (Outlet pattern, UploadApp outside Layout), index.css (full :root block).
strict: false — archive components have type issues; build clean.
Note: 45 archive components call fetch('/api/...') without /kitchen/ prefix
(B5b). Runtime 404s; deferred until after initial testing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
8d688b459d
10003 changed files with 1928395 additions and 0 deletions
390
docs/archive/PLAN_1_newbook_room_level_sync.md
Normal file
390
docs/archive/PLAN_1_newbook_room_level_sync.md
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
# Plan 1: Newbook Room-Level Occupancy Sync
|
||||
|
||||
## Current State
|
||||
|
||||
The ResidentsTableChart feature has:
|
||||
- ✅ Database schema with room-level fields (room_number, booking_id, guest_name, is_dbb, is_package)
|
||||
- ✅ API endpoint to serve room-level data
|
||||
- ✅ Frontend chart to display room occupancy
|
||||
- ❌ Sync service only fetches aggregated daily totals, not individual room details
|
||||
|
||||
**Current database state:**
|
||||
```sql
|
||||
room_number | booking_id | guest_name | is_dbb
|
||||
------------|------------|------------|-------
|
||||
NULL | NULL | NULL | false
|
||||
```
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The Newbook API sync (`backend/services/newbook.py`) currently only fetches:
|
||||
- Daily aggregated occupancy (total_rooms, occupied_rooms, occupancy_percentage)
|
||||
- Meal allocations (breakfast/dinner counts and revenue)
|
||||
- Arrival tracking (booking IDs and details for arrivals)
|
||||
|
||||
It does NOT fetch:
|
||||
- Individual room numbers
|
||||
- Which rooms are occupied on which dates
|
||||
- Guest names per room
|
||||
- Booking IDs per occupied room
|
||||
- Meal plan details per booking (DBB, package deals)
|
||||
|
||||
## Goal
|
||||
|
||||
Update the Newbook sync service to fetch and store room-level occupancy data so the ResidentsTableChart displays actual rooms instead of "Unknown Room".
|
||||
|
||||
## Investigation Required
|
||||
|
||||
Before implementation, we need to verify what data is available from the Newbook API:
|
||||
|
||||
### 1. Review Newbook API Documentation
|
||||
- What endpoint provides room-level occupancy?
|
||||
- Does it return individual room statuses?
|
||||
- Is guest name available (privacy concerns)?
|
||||
- How are meal plans (DBB, packages) represented?
|
||||
|
||||
### 2. Examine Existing Sync Code
|
||||
**File:** `backend/services/newbook.py`
|
||||
|
||||
Current occupancy sync likely uses:
|
||||
- `/api/occupancy` or similar endpoint
|
||||
- Returns aggregated daily statistics
|
||||
- May need different endpoint for room-level details
|
||||
|
||||
### 3. Check Existing Arrival Tracking
|
||||
The code already populates `arrival_booking_details` (JSONB) which might contain:
|
||||
- Booking reference numbers
|
||||
- Room assignments
|
||||
- Guest names
|
||||
- Meal plan flags
|
||||
|
||||
**SQL to check existing arrival data:**
|
||||
```sql
|
||||
SELECT date, arrival_booking_details
|
||||
FROM newbook_daily_occupancy
|
||||
WHERE arrival_booking_details IS NOT NULL
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
## Potential Approaches
|
||||
|
||||
### Option A: Expand Arrival Tracking to All Stays
|
||||
If `arrival_booking_details` contains room information, extend this to track:
|
||||
- Not just arrivals, but all bookings staying on each date
|
||||
- Store as `active_bookings_details` JSONB field
|
||||
- Parse and populate room_number, booking_id, guest_name from this JSON
|
||||
|
||||
**Pros:**
|
||||
- Minimal API changes if data already available
|
||||
- Leverages existing JSON structure
|
||||
|
||||
**Cons:**
|
||||
- JSONB storage plus denormalized columns (redundancy)
|
||||
- May not scale well with many rooms
|
||||
|
||||
### Option B: New Endpoint for Room Status
|
||||
Use a Newbook API endpoint that returns room-by-room status:
|
||||
|
||||
**Expected API response:**
|
||||
```json
|
||||
{
|
||||
"date": "2026-01-22",
|
||||
"rooms": [
|
||||
{
|
||||
"room_number": "101",
|
||||
"status": "occupied",
|
||||
"booking_id": "NB-12345",
|
||||
"guest_name": "John Smith",
|
||||
"check_in": "2026-01-22",
|
||||
"check_out": "2026-01-25",
|
||||
"meal_plan": "DBB",
|
||||
"is_package": false
|
||||
},
|
||||
{
|
||||
"room_number": "102",
|
||||
"status": "vacant"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Clean, structured data
|
||||
- All room details in one call
|
||||
- Easier to maintain
|
||||
|
||||
**Cons:**
|
||||
- May require different API endpoint
|
||||
- More data to fetch/store
|
||||
|
||||
### Option C: Hybrid Approach (RECOMMENDED)
|
||||
1. Use existing occupancy endpoint for aggregated stats
|
||||
2. Fetch booking details separately (may already be happening for arrivals)
|
||||
3. Match bookings to dates they span (check_in to check_out)
|
||||
4. Create separate row per room per night
|
||||
|
||||
**Storage strategy:**
|
||||
```
|
||||
Current: 1 row per date (aggregated)
|
||||
New: 1 row per room per night (denormalized)
|
||||
|
||||
Example for date 2026-01-22:
|
||||
OLD:
|
||||
- date: 2026-01-22, occupied_rooms: 6
|
||||
|
||||
NEW:
|
||||
- date: 2026-01-22, room: 101, booking_id: NB-12345, guest: Smith
|
||||
- date: 2026-01-22, room: 102, booking_id: NB-12346, guest: Jones
|
||||
- date: 2026-01-22, room: 103, booking_id: NB-12347, guest: Brown
|
||||
...
|
||||
```
|
||||
|
||||
**Trade-off:** More database rows, but enables room-level reporting.
|
||||
|
||||
## Schema Considerations
|
||||
|
||||
### Current Schema Issue
|
||||
`newbook_daily_occupancy` has unique constraint:
|
||||
```sql
|
||||
CONSTRAINT uq_newbook_occupancy_per_day
|
||||
UNIQUE(kitchen_id, date)
|
||||
```
|
||||
|
||||
This **prevents** multiple rows per date!
|
||||
|
||||
### Solution 1: Change Unique Constraint
|
||||
```sql
|
||||
-- Drop old constraint
|
||||
ALTER TABLE newbook_daily_occupancy
|
||||
DROP CONSTRAINT uq_newbook_occupancy_per_day;
|
||||
|
||||
-- Add new constraint for room-level uniqueness
|
||||
ALTER TABLE newbook_daily_occupancy
|
||||
ADD CONSTRAINT uq_newbook_occupancy_per_room_per_day
|
||||
UNIQUE(kitchen_id, date, room_number);
|
||||
```
|
||||
|
||||
**Impact:**
|
||||
- Breaking change to schema
|
||||
- Need migration to handle existing aggregated rows
|
||||
- Backward compatibility: API endpoint must handle both aggregated and room-level rows
|
||||
|
||||
### Solution 2: Separate Table (Alternative)
|
||||
Create `newbook_room_occupancy` table:
|
||||
```sql
|
||||
CREATE TABLE newbook_room_occupancy (
|
||||
id SERIAL PRIMARY KEY,
|
||||
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
|
||||
date DATE NOT NULL,
|
||||
room_number VARCHAR(50) NOT NULL,
|
||||
booking_id VARCHAR(100),
|
||||
guest_name VARCHAR(255),
|
||||
check_in DATE,
|
||||
check_out DATE,
|
||||
is_dbb BOOLEAN DEFAULT FALSE,
|
||||
is_package BOOLEAN DEFAULT FALSE,
|
||||
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_room_occupancy_per_day
|
||||
UNIQUE(kitchen_id, date, room_number)
|
||||
);
|
||||
```
|
||||
|
||||
Keep `newbook_daily_occupancy` for aggregated stats.
|
||||
|
||||
**Pros:**
|
||||
- No breaking changes
|
||||
- Clean separation of concerns
|
||||
- Can keep both aggregated and room-level data
|
||||
|
||||
**Cons:**
|
||||
- Two tables to maintain
|
||||
- API endpoint needs to join data or use new table
|
||||
|
||||
## Recommended Implementation Plan
|
||||
|
||||
### Phase 1: Investigate Newbook API
|
||||
1. Review Newbook API documentation for room-level endpoints
|
||||
2. Test API calls to see what data is available
|
||||
3. Check if current `arrival_booking_details` contains room info
|
||||
4. Determine if guest names are available (privacy/GDPR)
|
||||
|
||||
### Phase 2: Database Migration
|
||||
**Option A (if room data is sparse/optional):**
|
||||
- Keep current schema
|
||||
- Allow room_number, booking_id to be NULL
|
||||
- Populate when available
|
||||
- Current unique constraint stays
|
||||
|
||||
**Option B (if room data is always available):**
|
||||
- Change unique constraint to (kitchen_id, date, room_number)
|
||||
- Migrate existing aggregated rows (need strategy)
|
||||
- Update sync to create multiple rows per date
|
||||
|
||||
**Option C (cleanest):**
|
||||
- Create new `newbook_room_occupancy` table
|
||||
- Keep existing table for aggregated stats
|
||||
- Update API endpoint to use new table
|
||||
|
||||
### Phase 3: Update Sync Service
|
||||
**File:** `backend/services/newbook.py`
|
||||
|
||||
**Current sync logic:**
|
||||
```python
|
||||
async def sync_occupancy(kitchen_id, date_from, date_to):
|
||||
# Fetch aggregated occupancy
|
||||
data = await newbook_api.get_occupancy(date_from, date_to)
|
||||
|
||||
# Store one row per date
|
||||
for day in data:
|
||||
occupancy = NewbookDailyOccupancy(
|
||||
kitchen_id=kitchen_id,
|
||||
date=day['date'],
|
||||
total_rooms=day['total_rooms'],
|
||||
occupied_rooms=day['occupied_rooms'],
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
**New sync logic (Option B - room-level):**
|
||||
```python
|
||||
async def sync_occupancy(kitchen_id, date_from, date_to):
|
||||
# Fetch room-level occupancy
|
||||
data = await newbook_api.get_room_status(date_from, date_to)
|
||||
|
||||
# Delete existing rows for this date range (full refresh)
|
||||
await db.execute(
|
||||
delete(NewbookDailyOccupancy).where(
|
||||
and_(
|
||||
NewbookDailyOccupancy.kitchen_id == kitchen_id,
|
||||
NewbookDailyOccupancy.date >= date_from,
|
||||
NewbookDailyOccupancy.date <= date_to
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Store one row per room per date
|
||||
for day in data:
|
||||
for room in day['rooms']:
|
||||
if room['status'] == 'occupied':
|
||||
occupancy = NewbookDailyOccupancy(
|
||||
kitchen_id=kitchen_id,
|
||||
date=day['date'],
|
||||
room_number=room['room_number'],
|
||||
booking_id=room['booking_id'],
|
||||
guest_name=room.get('guest_name'), # May be NULL for privacy
|
||||
is_dbb=room.get('meal_plan') == 'DBB',
|
||||
is_package=room.get('is_package', False),
|
||||
# Aggregated stats on each row (redundant but simple)
|
||||
total_rooms=day['total_rooms'],
|
||||
occupied_rooms=day['occupied_rooms'],
|
||||
# ...
|
||||
)
|
||||
db.add(occupancy)
|
||||
```
|
||||
|
||||
### Phase 4: Update API Endpoint
|
||||
**File:** `backend/api/residents_table_chart.py`
|
||||
|
||||
**Current logic:**
|
||||
- Groups by (room_number, booking_id)
|
||||
- Expects rows already grouped
|
||||
|
||||
**Updated logic (if using room-level rows):**
|
||||
- No changes needed! Already groups correctly
|
||||
- Just needs room_number and booking_id to be populated
|
||||
|
||||
**Alternative (if keeping aggregated + separate table):**
|
||||
- Query `newbook_room_occupancy` instead
|
||||
- Join with `newbook_daily_occupancy` for aggregated stats
|
||||
|
||||
### Phase 5: Testing
|
||||
1. Run sync for a test date range
|
||||
2. Verify database populated:
|
||||
```sql
|
||||
SELECT date, room_number, booking_id, guest_name
|
||||
FROM newbook_daily_occupancy
|
||||
WHERE date = '2026-01-22'
|
||||
ORDER BY room_number;
|
||||
```
|
||||
3. Test ResidentsTableChart page
|
||||
4. Verify rooms display with actual names
|
||||
5. Check restaurant booking linkage (hotel_booking_number)
|
||||
|
||||
## Privacy & GDPR Considerations
|
||||
|
||||
**Guest Names:**
|
||||
- May need to be masked/hashed for privacy
|
||||
- Consider: "Guest in Room 101" instead of actual name
|
||||
- Add setting: "Show guest names" (admin only)
|
||||
- Log access to guest PII
|
||||
|
||||
**Data Retention:**
|
||||
- How long to keep room-level data?
|
||||
- May need purge policy for old bookings
|
||||
- Aggregate historical data, delete room details
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
**Database Size:**
|
||||
- Current: ~365 rows per year (1 per day)
|
||||
- New: ~365 × 25 rooms = 9,125 rows per year
|
||||
- 10 years: ~91,000 rows (still manageable)
|
||||
|
||||
**Indexing:**
|
||||
- Existing: (kitchen_id, date)
|
||||
- New: (kitchen_id, date, room_number) - already created
|
||||
- Consider: (kitchen_id, booking_id) for linking to Resos
|
||||
|
||||
**Query Performance:**
|
||||
- ResidentsTableChart queries 7-day window
|
||||
- Fetching 7 × 25 = 175 rows max
|
||||
- Indexes should handle this easily
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
**For Existing Aggregated Rows:**
|
||||
|
||||
**Option 1:** Delete and re-sync
|
||||
```sql
|
||||
DELETE FROM newbook_daily_occupancy;
|
||||
-- Then run sync to repopulate with room-level data
|
||||
```
|
||||
|
||||
**Option 2:** Keep aggregated rows, add room-level
|
||||
- Set room_number = NULL for aggregated rows
|
||||
- Add new room-level rows alongside
|
||||
- API filters WHERE room_number IS NOT NULL
|
||||
|
||||
**Option 3:** Backfill from booking history (if available)
|
||||
- Query Newbook API for historical bookings
|
||||
- Reconstruct room occupancy for past dates
|
||||
- May be slow/rate-limited
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Immediate:** Check Newbook API docs for room-level endpoint
|
||||
2. **Investigate:** Review current `arrival_booking_details` structure
|
||||
3. **Decide:** Choose schema approach (modify existing vs new table)
|
||||
4. **Prototype:** Test API calls to fetch room data
|
||||
5. **Implement:** Update sync service based on findings
|
||||
6. **Test:** Verify ResidentsTableChart shows real rooms
|
||||
|
||||
## Unknown/Questions
|
||||
|
||||
- [ ] Does Newbook API provide room-level occupancy?
|
||||
- [ ] What endpoint? (e.g., `/api/room_status`, `/api/bookings`)
|
||||
- [ ] Are guest names available?
|
||||
- [ ] How are meal plans represented in API?
|
||||
- [ ] Is booking ID always available?
|
||||
- [ ] Rate limits for room-level sync?
|
||||
- [ ] Historical data availability?
|
||||
|
||||
## Success Criteria
|
||||
|
||||
✅ ResidentsTableChart shows actual room numbers (e.g., "101", "102") instead of "Unknown Room"
|
||||
✅ Guest names populated (if available and permitted)
|
||||
✅ Booking IDs linked correctly
|
||||
✅ DBB/package flags set correctly
|
||||
✅ Restaurant bookings link via hotel_booking_number
|
||||
✅ Data refreshes daily (or more frequently)
|
||||
✅ Performance remains acceptable (<500ms API response)
|
||||
Loading…
Add table
Add a link
Reference in a new issue