diff --git a/.gitignore b/.gitignore index bd6445c..0cebedb 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,7 @@ Thumbs.db postgres_data/ # Client data -backend/data/ +backend/hidden_data/ # Testing .pytest_cache/ @@ -40,8 +40,6 @@ htmlcov/ # uv .uv/ -# Generated data -backend/data/ # Ruff .ruff_cache/ diff --git a/backend/.dockerignore b/backend/.dockerignore index f87ea77..a9d5b7b 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -22,6 +22,9 @@ htmlcov/ .env.* !.env.example +# Real client exports (gitignored); never bake them into the image +hidden_data/ + # Docs and Docker plumbing don't belong inside the image *.md Dockerfile diff --git a/backend/data/Actual & Forecast.csv b/backend/data/Actual & Forecast.csv new file mode 100644 index 0000000..79b128b --- /dev/null +++ b/backend/data/Actual & Forecast.csv @@ -0,0 +1,15 @@ +"Date","DayOfWeek","Avail","Total","Indv","Multi","Blocks","Occ","Ad","Ch","Inf","Accomm","FB","Other","NonRevenue","TotalRevenue","GrandTotal","CumulativeAccomm","CumulativeFB","CumulativeOther","CumulativeTotalRevenue","CumulativeNonRevenue","CumulativeGrandTotal","ARR","APR","Yield","Blked","ROOS","DFS","Waitlist","InStock","RatePlanRevenue" +"01/09/2025 00:00:00","Monday","0.00","9","5","4","0","90.00","18","0","0","1384.62","0.00","0.00","0.00","1384.62","1384.62","1384.62","0.00","0.00","1384.62","0.00","1384.62","153.85","76.92","138.46","0","1","0","0","10","1571.54" +"02/09/2025 00:00:00","Tuesday","2.00","8","6","2","0","80.00","16","0","0","1244.96","0.00","0.00","0.00","1244.96","1244.96","2629.58","0.00","0.00","2629.58","0.00","2629.58","155.62","77.81","124.50","0","0","0","0","10","1413.03" +"03/09/2025 00:00:00","Wednesday","0.00","10","6","4","0","100.00","20","0","0","1634.13","0.00","0.00","0.00","1634.13","1634.13","4263.71","0.00","0.00","4263.71","0.00","4263.71","163.41","81.71","163.41","0","0","0","0","10","1854.74" +"04/09/2025 00:00:00","Thursday","2.00","8","4","4","0","80.00","16","0","0","1212.69","0.00","0.00","0.00","1212.69","1212.69","5476.40","0.00","0.00","5476.40","0.00","5476.40","151.59","75.79","121.27","0","0","0","0","10","1376.40" +"05/09/2025 00:00:00","Friday","1.00","8","3","5","0","80.00","16","0","0","1130.68","0.00","0.00","0.00","1130.68","1130.68","6607.08","0.00","0.00","6607.08","0.00","6607.08","141.34","70.67","113.07","0","1","0","0","10","1283.32" +"06/09/2025 00:00:00","Saturday","0.00","10","8","2","0","100.00","20","0","0","1405.27","0.00","0.00","0.00","1405.27","1405.27","8012.35","0.00","0.00","8012.35","0.00","8012.35","140.53","70.26","140.53","0","0","0","0","10","1594.98" +"07/09/2025 00:00:00","Sunday","0.00","9","9","0","0","90.00","18","0","0","1119.64","0.00","0.00","0.00","1119.64","1119.64","9131.99","0.00","0.00","9131.99","0.00","9131.99","124.40","62.20","111.96","0","1","0","0","10","1270.79" +"08/09/2025 00:00:00","Monday","2.00","8","8","0","0","80.00","16","0","0","1378.96","0.00","0.00","0.00","1378.96","1378.96","10510.95","0.00","0.00","10510.95","0.00","10510.95","172.37","86.19","137.90","0","0","0","0","10","1565.12" +"09/09/2025 00:00:00","Tuesday","0.00","10","7","3","0","100.00","20","0","0","1402.79","0.00","0.00","0.00","1402.79","1402.79","11913.74","0.00","0.00","11913.74","0.00","11913.74","140.28","70.14","140.28","0","0","0","0","10","1592.17" +"10/09/2025 00:00:00","Wednesday","4.00","5","5","0","0","50.00","10","0","0","794.03","0.00","0.00","0.00","794.03","794.03","12707.77","0.00","0.00","12707.77","0.00","12707.77","158.81","79.40","79.40","0","1","0","0","10","901.22" +"11/09/2025 00:00:00","Thursday","0.00","10","6","4","0","100.00","20","0","0","1438.49","0.00","0.00","0.00","1438.49","1438.49","14146.26","0.00","0.00","14146.26","0.00","14146.26","143.85","71.92","143.85","0","0","0","0","10","1632.69" +"12/09/2025 00:00:00","Friday","0.00","10","6","4","0","100.00","20","0","0","1418.36","0.00","0.00","0.00","1418.36","1418.36","15564.62","0.00","0.00","15564.62","0.00","15564.62","141.84","70.92","141.84","0","0","0","0","10","1609.84" +"13/09/2025 00:00:00","Saturday","0.00","10","8","2","0","100.00","20","0","0","1441.80","0.00","0.00","0.00","1441.80","1441.80","17006.42","0.00","0.00","17006.42","0.00","17006.42","144.18","72.09","144.18","0","0","0","0","10","1636.44" +"14/09/2025 00:00:00","Sunday","0.00","10","5","5","0","100.00","20","0","0","1557.88","0.00","0.00","0.00","1557.88","1557.88","18564.30","0.00","0.00","18564.30","0.00","18564.30","155.79","77.89","155.79","0","0","0","0","10","1768.19" diff --git a/backend/data/Cancellation.csv b/backend/data/Cancellation.csv new file mode 100644 index 0000000..4315aec --- /dev/null +++ b/backend/data/Cancellation.csv @@ -0,0 +1,39 @@ +"CancelRef","CancelDate","CancelReasonCode","Surname","Company","Agent","Ref","ArrivalDate","DepartureDate","ProfileRef","Operator","EstimatedRevenue","CancelReason","BookingCreatedDate" +"CN50081/1","28/06/2025 10:53:20","GDE","Guest81","","","BK100081/1","06/Sep/2025","07/Sep/2025","PF000081","SYSTEM","86.18","","20/06/2025 17:25:00" +"CN50039/2","16/07/2025 12:15:42","GDE","Guest39","","","BK100039/2","04/Sep/2025","07/Sep/2025","PF000039","SYSTEM","410.49","","19/06/2025 07:21:00" +"CN50030/1","19/07/2025 05:11:26","GDE","Guest30","","","BK100030/1","02/Sep/2025","05/Sep/2025","PF000030","SYSTEM","601.77","","06/07/2025 20:03:00" +"CN50039/1","27/07/2025 15:19:07","GDE","Guest39","","","BK100039/1","04/Sep/2025","07/Sep/2025","PF000039","SYSTEM","302.34","","19/06/2025 07:21:00" +"CN50007/1","30/07/2025 22:49:51","GDE","Guest7","","","BK100007/1","04/Sep/2025","05/Sep/2025","PF000007","SYSTEM","219.72","","25/07/2025 06:40:00" +"CN50055/1","03/08/2025 10:23:50","GDE","Guest55","","","BK100055/1","10/Sep/2025","11/Sep/2025","PF000055","SYSTEM","138.34","","02/08/2025 05:13:00" +"CN50041/2","06/08/2025 12:30:28","GDE","Guest41","","","BK100041/2","13/Sep/2025","14/Sep/2025","PF000041","SYSTEM","142.80","","05/08/2025 23:29:00" +"CN50064/1","06/08/2025 21:19:20","GDE","Guest64","","","BK100064/1","04/Sep/2025","05/Sep/2025","PF000064","SYSTEM","104.27","","14/06/2025 00:53:00" +"CN50030/2","11/08/2025 04:07:32","GDE","Guest30","","","BK100030/2","02/Sep/2025","05/Sep/2025","PF000030","SYSTEM","296.04","","06/07/2025 20:03:00" +"CN50080/1","11/08/2025 12:32:07","GDE","Guest80","","","BK100080/1","11/Sep/2025","12/Sep/2025","PF000080","SYSTEM","113.42","","14/07/2025 03:09:00" +"CN50036/2","15/08/2025 20:01:11","GDE","Guest36","","","BK100036/2","13/Sep/2025","14/Sep/2025","PF000036","SYSTEM","140.37","","10/08/2025 04:02:00" +"CN50067/1","16/08/2025 02:39:37","GDE","Guest67","","","BK100067/1","04/Sep/2025","05/Sep/2025","PF000067","SYSTEM","110.53","","12/07/2025 15:31:00" +"CN50041/3","19/08/2025 09:24:31","GDE","Guest41","","","BK100041/3","13/Sep/2025","14/Sep/2025","PF000041","SYSTEM","200.67","","05/08/2025 23:29:00" +"CN50010/3","22/08/2025 14:48:52","GDE","Guest10","","","BK100010/3","03/Sep/2025","04/Sep/2025","PF000010","SYSTEM","205.74","","17/08/2025 18:54:00" +"CN50010/2","24/08/2025 07:02:04","GDE","Guest10","","","BK100010/2","03/Sep/2025","04/Sep/2025","PF000010","SYSTEM","197.94","","17/08/2025 18:54:00" +"CN50097/1","26/08/2025 03:38:36","GDE","Guest97","","","BK100097/1","10/Sep/2025","11/Sep/2025","PF000097","SYSTEM","151.78","","05/08/2025 17:08:00" +"CN50059/1","27/08/2025 07:27:42","GDE","Guest59","","","BK100059/1","02/Sep/2025","04/Sep/2025","PF000059","SYSTEM","358.80","","26/06/2025 04:09:00" +"CN50010/1","27/08/2025 13:33:45","GDE","Guest10","","","BK100010/1","03/Sep/2025","04/Sep/2025","PF000010","SYSTEM","209.27","","17/08/2025 18:54:00" +"CN50078/1","27/08/2025 17:04:48","GDE","Guest78","","","BK100078/1","10/Sep/2025","12/Sep/2025","PF000078","SYSTEM","311.38","","26/08/2025 01:05:00" +"CN50070/1","27/08/2025 21:28:19","GDE","Guest70","","","BK100070/1","11/Sep/2025","12/Sep/2025","PF000070","SYSTEM","125.73","","13/08/2025 01:41:00" +"CN50092/1","30/08/2025 05:33:34","GDE","Guest92","","","BK100092/1","08/Sep/2025","09/Sep/2025","PF000092","SYSTEM","204.74","","29/07/2025 10:33:00" +"CN50002/3","01/09/2025 07:05:51","GDE","Guest2","","","BK100002/3","08/Sep/2025","10/Sep/2025","PF000002","SYSTEM","309.12","","26/08/2025 02:54:00" +"CN50041/1","01/09/2025 13:46:50","GDE","Guest41","","","BK100041/1","13/Sep/2025","14/Sep/2025","PF000041","SYSTEM","186.25","","05/08/2025 23:29:00" +"CN50098/1","03/09/2025 04:26:47","GDE","Guest98","","","BK100098/1","05/Sep/2025","06/Sep/2025","PF000098","SYSTEM","198.39","","03/09/2025 02:13:00" +"CN50095/1","03/09/2025 07:00:59","GDE","Guest95","","","BK100095/1","08/Sep/2025","09/Sep/2025","PF000095","SYSTEM","199.55","","12/08/2025 21:01:00" +"CN50035/1","03/09/2025 08:33:50","GDE","Guest35","","","BK100035/1","13/Sep/2025","14/Sep/2025","PF000035","SYSTEM","133.87","","15/08/2025 21:55:00" +"CN50065/1","03/09/2025 12:43:35","GDE","Guest65","","","BK100065/1","04/Sep/2025","06/Sep/2025","PF000065","SYSTEM","161.08","","14/08/2025 12:04:00" +"CN50002/2","03/09/2025 23:38:53","GDE","Guest2","","","BK100002/2","08/Sep/2025","10/Sep/2025","PF000002","SYSTEM","436.44","","26/08/2025 02:54:00" +"CN50090/1","04/09/2025 15:06:28","GDE","Guest90","","","BK100090/1","05/Sep/2025","06/Sep/2025","PF000090","SYSTEM","99.15","","28/08/2025 01:17:00" +"CN50098/2","04/09/2025 20:19:10","GDE","Guest98","","","BK100098/2","05/Sep/2025","06/Sep/2025","PF000098","SYSTEM","90.66","","03/09/2025 02:13:00" +"CN50055/2","07/09/2025 07:37:11","GDE","Guest55","","","BK100055/2","10/Sep/2025","11/Sep/2025","PF000055","SYSTEM","209.70","","02/08/2025 05:13:00" +"CN50002/1","07/09/2025 12:46:30","GDE","Guest2","","","BK100002/1","08/Sep/2025","10/Sep/2025","PF000002","SYSTEM","383.84","","26/08/2025 02:54:00" +"CN50031/1","08/09/2025 07:15:22","GDE","Guest31","","","BK100031/1","09/Sep/2025","11/Sep/2025","PF000031","SYSTEM","171.38","","06/09/2025 22:34:00" +"CN50088/1","08/09/2025 10:00:00","NO_SHOW","Guest88","","","BK100088/1","07/Sep/2025","08/Sep/2025","PF000088","SYSTEM","124.57","","22/07/2025 07:36:00" +"CN50096/1","08/09/2025 10:00:00","NO_SHOW","Guest96","","","BK100096/1","07/Sep/2025","08/Sep/2025","PF000096","SYSTEM","84.45","","13/07/2025 15:33:00" +"CN50099/1","08/09/2025 10:00:00","NO_SHOW","Guest99","","","BK100099/1","07/Sep/2025","09/Sep/2025","PF000099","SYSTEM","168.58","","07/07/2025 10:55:00" +"CN50029/1","10/09/2025 22:41:32","GDE","Guest29","","","BK100029/1","11/Sep/2025","14/Sep/2025","PF000029","SYSTEM","461.91","","10/09/2025 00:57:00" +"CN50036/1","14/09/2025 10:00:00","NO_SHOW","Guest36","","","BK100036/1","13/Sep/2025","14/Sep/2025","PF000036","SYSTEM","188.79","","10/08/2025 04:02:00" diff --git a/backend/data/DailyNewReservations.csv b/backend/data/DailyNewReservations.csv new file mode 100644 index 0000000..4ff3aab --- /dev/null +++ b/backend/data/DailyNewReservations.csv @@ -0,0 +1,89 @@ +"Site","Company","CompanyName","Agent","AgentName","BookRef","GuestName","Arrival","Departure","OperatorCode","BookingDate","Nights","ADRSoldNett","ADRSoldGross","ReservationValueNett","ReservationValueGross","CancelledTimeStamp","CancelledNights","CancelledRevenueNett","CancelledRevenueGross","RatePlan","MarketSegment","RoomType" +"Synthetic Hotel","","","","","BK100014/1","Synthetic Guest 14","03/Sep/2025","05/Sep/2025","SYSTEM","05/06/2025 16:32:00","2","86.33","97.98","172.66","195.97","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100014/2","Synthetic Guest 14","03/Sep/2025","05/Sep/2025","SYSTEM","05/06/2025 16:32:00","2","160.34","181.99","320.68","363.97","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100025/1","Synthetic Guest 25","04/Sep/2025","07/Sep/2025","SYSTEM","06/06/2025 04:38:00","3","138.55","157.25","415.65","471.76","","0","0.00","0.00","BAR_RO_SGL","LEISURE","SINGLE" +"Synthetic Hotel","","","","","BK100025/2","Synthetic Guest 25","04/Sep/2025","07/Sep/2025","SYSTEM","06/06/2025 04:38:00","3","172.71","196.03","518.13","588.08","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100046/1","Synthetic Guest 46","01/Sep/2025","02/Sep/2025","SYSTEM","09/06/2025 12:11:00","1","107.30","121.79","107.30","121.79","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100063/1","Synthetic Guest 63","07/Sep/2025","08/Sep/2025","SYSTEM","11/06/2025 09:09:00","1","100.72","114.32","100.72","114.32","","0","0.00","0.00","BAR_RO_SGL","LEISURE","SINGLE" +"Synthetic Hotel","","","","","BK100054/1","Synthetic Guest 54","04/Sep/2025","05/Sep/2025","SYSTEM","11/06/2025 22:26:00","1","200.54","227.61","200.54","227.61","","0","0.00","0.00","BAR_RO_DB","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100024/1","Synthetic Guest 24","12/Sep/2025","15/Sep/2025","SYSTEM","14/06/2025 12:36:00","3","102.41","116.24","307.23","348.71","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100083/1","Synthetic Guest 83","03/Sep/2025","04/Sep/2025","SYSTEM","15/06/2025 12:48:00","1","212.38","241.05","212.38","241.05","","0","0.00","0.00","BAR_RO_DB","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100066/1","Synthetic Guest 66","11/Sep/2025","12/Sep/2025","SYSTEM","15/06/2025 16:26:00","1","215.89","245.04","215.89","245.04","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100003/1","Synthetic Guest 3","12/Sep/2025","13/Sep/2025","SYSTEM","16/06/2025 12:44:00","1","146.10","165.82","146.10","165.82","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100033/1","Synthetic Guest 33","06/Sep/2025","08/Sep/2025","SYSTEM","18/06/2025 22:47:00","2","85.77","97.35","171.54","194.70","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100005/1","Synthetic Guest 5","13/Sep/2025","14/Sep/2025","SYSTEM","19/06/2025 02:40:00","1","80.16","90.98","80.16","90.98","","0","0.00","0.00","BAR_RO_SGL","LEISURE","SINGLE" +"Synthetic Hotel","","","","","BK100048/1","Synthetic Guest 48","09/Sep/2025","10/Sep/2025","SYSTEM","20/06/2025 06:52:00","1","177.79","201.79","177.79","201.79","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100017/1","Synthetic Guest 17","08/Sep/2025","09/Sep/2025","SYSTEM","20/06/2025 20:33:00","1","197.03","223.63","197.03","223.63","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100087/1","Synthetic Guest 87","11/Sep/2025","12/Sep/2025","SYSTEM","21/06/2025 06:21:00","1","94.95","107.77","94.95","107.77","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100015/1","Synthetic Guest 15","09/Sep/2025","10/Sep/2025","SYSTEM","23/06/2025 04:21:00","1","96.76","109.82","96.76","109.82","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100045/1","Synthetic Guest 45","10/Sep/2025","11/Sep/2025","SYSTEM","23/06/2025 08:06:00","1","134.14","152.25","134.14","152.25","","0","0.00","0.00","BAR_RO_DB","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100093/1","Synthetic Guest 93","12/Sep/2025","13/Sep/2025","SYSTEM","24/06/2025 01:31:00","1","99.03","112.40","99.03","112.40","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100021/1","Synthetic Guest 21","08/Sep/2025","09/Sep/2025","SYSTEM","27/06/2025 06:39:00","1","208.45","236.59","208.45","236.59","","0","0.00","0.00","BAR_RO_SGL","LEISURE","SINGLE" +"Synthetic Hotel","","","","","BK100069/1","Synthetic Guest 69","06/Sep/2025","07/Sep/2025","SYSTEM","28/06/2025 22:01:00","1","182.52","207.16","182.52","207.16","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100008/1","Synthetic Guest 8","08/Sep/2025","09/Sep/2025","SYSTEM","29/06/2025 12:39:00","1","194.03","220.22","194.03","220.22","","0","0.00","0.00","BAR_RO_DB","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100011/1","Synthetic Guest 11","14/Sep/2025","15/Sep/2025","SYSTEM","30/06/2025 09:42:00","1","161.16","182.92","161.16","182.92","","0","0.00","0.00","BAR_RO","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100011/2","Synthetic Guest 11","14/Sep/2025","15/Sep/2025","SYSTEM","30/06/2025 09:42:00","1","143.08","162.40","143.08","162.40","","0","0.00","0.00","BAR_RO","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100011/3","Synthetic Guest 11","14/Sep/2025","15/Sep/2025","SYSTEM","30/06/2025 09:42:00","1","172.43","195.71","172.43","195.71","","0","0.00","0.00","BAR_RO_DB","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100023/1","Synthetic Guest 23","01/Sep/2025","03/Sep/2025","SYSTEM","01/07/2025 11:59:00","2","165.42","187.75","330.84","375.50","","0","0.00","0.00","BAR_RO_DB","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100019/1","Synthetic Guest 19","01/Sep/2025","02/Sep/2025","SYSTEM","02/07/2025 04:34:00","1","103.72","117.72","103.72","117.72","","0","0.00","0.00","BAR_RO_DB","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100019/2","Synthetic Guest 19","01/Sep/2025","02/Sep/2025","SYSTEM","02/07/2025 04:34:00","1","174.17","197.68","174.17","197.68","","0","0.00","0.00","BAR_RO_DB","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100004/1","Synthetic Guest 4","02/Sep/2025","04/Sep/2025","SYSTEM","02/07/2025 08:54:00","2","201.14","228.29","402.28","456.59","","0","0.00","0.00","BAR_RO_DB","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100004/2","Synthetic Guest 4","02/Sep/2025","04/Sep/2025","SYSTEM","02/07/2025 08:54:00","2","116.47","132.19","232.94","264.39","","0","0.00","0.00","BAR_RO_DB","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100085/1","Synthetic Guest 85","04/Sep/2025","05/Sep/2025","SYSTEM","02/07/2025 11:49:00","1","205.98","233.79","205.98","233.79","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100026/1","Synthetic Guest 26","01/Sep/2025","02/Sep/2025","SYSTEM","05/07/2025 06:00:00","1","203.89","231.42","203.89","231.42","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100026/2","Synthetic Guest 26","01/Sep/2025","02/Sep/2025","SYSTEM","05/07/2025 06:00:00","1","207.13","235.09","207.13","235.09","","0","0.00","0.00","BAR_RO","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100082/1","Synthetic Guest 82","01/Sep/2025","02/Sep/2025","SYSTEM","06/07/2025 11:54:00","1","164.58","186.80","164.58","186.80","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100058/1","Synthetic Guest 58","05/Sep/2025","06/Sep/2025","SYSTEM","06/07/2025 20:51:00","1","174.95","198.57","174.95","198.57","","0","0.00","0.00","BAR_RO_DB","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100094/1","Synthetic Guest 94","06/Sep/2025","07/Sep/2025","SYSTEM","08/07/2025 10:41:00","1","174.89","198.50","174.89","198.50","","0","0.00","0.00","BAR_RO","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100073/1","Synthetic Guest 73","11/Sep/2025","12/Sep/2025","SYSTEM","09/07/2025 22:16:00","1","82.90","94.09","82.90","94.09","","0","0.00","0.00","BAR_RO_SGL","LEISURE","SINGLE" +"Synthetic Hotel","","","","","BK100073/2","Synthetic Guest 73","11/Sep/2025","12/Sep/2025","SYSTEM","09/07/2025 22:16:00","1","183.59","208.37","183.59","208.37","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100037/1","Synthetic Guest 37","06/Sep/2025","07/Sep/2025","SYSTEM","10/07/2025 00:54:00","1","108.24","122.85","108.24","122.85","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100012/1","Synthetic Guest 12","12/Sep/2025","14/Sep/2025","SYSTEM","11/07/2025 03:58:00","2","168.22","190.93","336.44","381.86","","0","0.00","0.00","BAR_RO","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100027/1","Synthetic Guest 27","10/Sep/2025","11/Sep/2025","SYSTEM","12/07/2025 22:18:00","1","193.27","219.36","193.27","219.36","","0","0.00","0.00","BAR_RO_SGL","LEISURE","SINGLE" +"Synthetic Hotel","","","","","BK100001/1","Synthetic Guest 1","14/Sep/2025","15/Sep/2025","SYSTEM","13/07/2025 10:11:00","1","208.55","236.70","208.55","236.70","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100001/2","Synthetic Guest 1","14/Sep/2025","15/Sep/2025","SYSTEM","13/07/2025 10:11:00","1","196.18","222.66","196.18","222.66","","0","0.00","0.00","BAR_RO_DB","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100071/1","Synthetic Guest 71","06/Sep/2025","08/Sep/2025","SYSTEM","15/07/2025 10:57:00","2","81.35","92.33","162.70","184.66","","0","0.00","0.00","BAR_RO_DB","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100075/1","Synthetic Guest 75","06/Sep/2025","08/Sep/2025","SYSTEM","16/07/2025 03:30:00","2","140.34","159.29","280.68","318.57","","0","0.00","0.00","BAR_RO_DB","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100057/1","Synthetic Guest 57","12/Sep/2025","13/Sep/2025","SYSTEM","18/07/2025 18:03:00","1","114.71","130.20","114.71","130.20","","0","0.00","0.00","BAR_RO_SGL","LEISURE","SINGLE" +"Synthetic Hotel","","","","","BK100057/2","Synthetic Guest 57","12/Sep/2025","13/Sep/2025","SYSTEM","18/07/2025 18:03:00","1","143.48","162.85","143.48","162.85","","0","0.00","0.00","BAR_RO_DB","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100077/1","Synthetic Guest 77","10/Sep/2025","11/Sep/2025","SYSTEM","18/07/2025 21:21:00","1","160.24","181.87","160.24","181.87","","0","0.00","0.00","BAR_RO","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100061/1","Synthetic Guest 61","06/Sep/2025","07/Sep/2025","SYSTEM","19/07/2025 10:19:00","1","162.75","184.72","162.75","184.72","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100086/1","Synthetic Guest 86","10/Sep/2025","12/Sep/2025","SYSTEM","19/07/2025 15:04:00","2","205.89","233.69","411.78","467.37","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100009/1","Synthetic Guest 9","09/Sep/2025","10/Sep/2025","SYSTEM","21/07/2025 13:11:00","1","160.59","182.27","160.59","182.27","","0","0.00","0.00","BAR_RO_DB","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100009/2","Synthetic Guest 9","09/Sep/2025","10/Sep/2025","SYSTEM","21/07/2025 13:11:00","1","120.65","136.94","120.65","136.94","","0","0.00","0.00","BAR_RO_SGL","LEISURE","SINGLE" +"Synthetic Hotel","","","","","BK100009/3","Synthetic Guest 9","09/Sep/2025","10/Sep/2025","SYSTEM","21/07/2025 13:11:00","1","106.51","120.89","106.51","120.89","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100076/1","Synthetic Guest 76","03/Sep/2025","04/Sep/2025","SYSTEM","21/07/2025 23:41:00","1","212.81","241.54","212.81","241.54","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100074/1","Synthetic Guest 74","07/Sep/2025","09/Sep/2025","SYSTEM","26/07/2025 15:44:00","2","162.06","183.94","324.12","367.88","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100060/1","Synthetic Guest 60","05/Sep/2025","06/Sep/2025","SYSTEM","27/07/2025 05:48:00","1","127.58","144.80","127.58","144.80","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100060/2","Synthetic Guest 60","05/Sep/2025","06/Sep/2025","SYSTEM","27/07/2025 05:48:00","1","154.23","175.05","154.23","175.05","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100060/3","Synthetic Guest 60","05/Sep/2025","06/Sep/2025","SYSTEM","27/07/2025 05:48:00","1","114.87","130.38","114.87","130.38","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100068/1","Synthetic Guest 68","11/Sep/2025","14/Sep/2025","SYSTEM","27/07/2025 07:19:00","3","143.78","163.19","431.34","489.57","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100052/1","Synthetic Guest 52","03/Sep/2025","04/Sep/2025","SYSTEM","27/07/2025 13:54:00","1","211.20","239.71","211.20","239.71","","0","0.00","0.00","BAR_RO","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100062/1","Synthetic Guest 62","01/Sep/2025","03/Sep/2025","SYSTEM","28/07/2025 02:09:00","2","177.96","201.98","355.92","403.97","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100056/1","Synthetic Guest 56","05/Sep/2025","08/Sep/2025","SYSTEM","31/07/2025 20:11:00","3","158.15","179.50","474.45","538.50","","0","0.00","0.00","BAR_RO","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100091/1","Synthetic Guest 91","11/Sep/2025","12/Sep/2025","SYSTEM","02/08/2025 14:38:00","1","111.45","126.50","111.45","126.50","","0","0.00","0.00","BAR_RO","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100016/1","Synthetic Guest 16","02/Sep/2025","04/Sep/2025","SYSTEM","08/08/2025 17:42:00","2","180.54","204.91","361.08","409.83","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100042/1","Synthetic Guest 42","11/Sep/2025","12/Sep/2025","SYSTEM","09/08/2025 12:39:00","1","89.19","101.23","89.19","101.23","","0","0.00","0.00","BAR_RO","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100018/1","Synthetic Guest 18","02/Sep/2025","03/Sep/2025","SYSTEM","09/08/2025 21:55:00","1","150.51","170.83","150.51","170.83","","0","0.00","0.00","BAR_RO_DB","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100034/1","Synthetic Guest 34","09/Sep/2025","10/Sep/2025","SYSTEM","09/08/2025 22:22:00","1","123.10","139.72","123.10","139.72","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100040/1","Synthetic Guest 40","12/Sep/2025","14/Sep/2025","SYSTEM","10/08/2025 03:26:00","2","189.78","215.40","379.56","430.80","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100038/1","Synthetic Guest 38","08/Sep/2025","11/Sep/2025","SYSTEM","11/08/2025 02:44:00","3","100.49","114.06","301.47","342.17","","0","0.00","0.00","BAR_RO","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100020/1","Synthetic Guest 20","13/Sep/2025","14/Sep/2025","SYSTEM","11/08/2025 20:00:00","1","163.55","185.63","163.55","185.63","","0","0.00","0.00","BAR_RO","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100072/1","Synthetic Guest 72","04/Sep/2025","05/Sep/2025","SYSTEM","13/08/2025 00:40:00","1","158.60","180.01","158.60","180.01","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100051/1","Synthetic Guest 51","01/Sep/2025","02/Sep/2025","SYSTEM","13/08/2025 16:55:00","1","80.45","91.31","80.45","91.31","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100084/1","Synthetic Guest 84","07/Sep/2025","08/Sep/2025","SYSTEM","15/08/2025 18:56:00","1","82.73","93.90","82.73","93.90","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100089/1","Synthetic Guest 89","04/Sep/2025","06/Sep/2025","SYSTEM","16/08/2025 08:25:00","2","89.64","101.74","179.28","203.48","","0","0.00","0.00","BAR_RO","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100053/1","Synthetic Guest 53","02/Sep/2025","04/Sep/2025","SYSTEM","16/08/2025 16:24:00","2","124.22","140.99","248.44","281.98","","0","0.00","0.00","BAR_RO_SGL","LEISURE","SINGLE" +"Synthetic Hotel","","","","","BK100079/1","Synthetic Guest 79","13/Sep/2025","14/Sep/2025","SYSTEM","18/08/2025 15:06:00","1","125.91","142.91","125.91","142.91","","0","0.00","0.00","BAR_RO","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100047/1","Synthetic Guest 47","13/Sep/2025","14/Sep/2025","SYSTEM","19/08/2025 22:42:00","1","189.50","215.08","189.50","215.08","","0","0.00","0.00","BAR_RO","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100047/2","Synthetic Guest 47","13/Sep/2025","14/Sep/2025","SYSTEM","19/08/2025 22:42:00","1","142.24","161.44","142.24","161.44","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100043/1","Synthetic Guest 43","07/Sep/2025","10/Sep/2025","SYSTEM","21/08/2025 15:51:00","3","120.19","136.42","360.57","409.25","","0","0.00","0.00","BAR_RO_DB","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100049/1","Synthetic Guest 49","07/Sep/2025","10/Sep/2025","SYSTEM","25/08/2025 09:50:00","3","188.33","213.75","564.99","641.26","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100044/1","Synthetic Guest 44","14/Sep/2025","15/Sep/2025","SYSTEM","26/08/2025 07:13:00","1","204.59","232.21","204.59","232.21","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100028/1","Synthetic Guest 28","13/Sep/2025","15/Sep/2025","SYSTEM","27/08/2025 23:30:00","2","136.25","154.64","272.50","309.29","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100050/1","Synthetic Guest 50","02/Sep/2025","04/Sep/2025","SYSTEM","29/08/2025 12:13:00","2","128.70","146.07","257.40","292.15","","0","0.00","0.00","BAR_RO","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100013/1","Synthetic Guest 13","14/Sep/2025","15/Sep/2025","SYSTEM","30/08/2025 16:29:00","1","132.09","149.92","132.09","149.92","","0","0.00","0.00","BAR_RO_DB","LEISURE","DOUBLE" +"Synthetic Hotel","","","","","BK100006/1","Synthetic Guest 6","08/Sep/2025","10/Sep/2025","SYSTEM","30/08/2025 17:29:00","2","208.38","236.51","416.76","473.02","","0","0.00","0.00","BAR_RO_DB","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100022/1","Synthetic Guest 22","11/Sep/2025","13/Sep/2025","SYSTEM","09/09/2025 08:23:00","2","183.82","208.64","367.64","417.27","","0","0.00","0.00","BAR_RO","LEISURE","TWIN" +"Synthetic Hotel","","","","","BK100022/2","Synthetic Guest 22","11/Sep/2025","13/Sep/2025","SYSTEM","09/09/2025 08:23:00","2","127.03","144.18","254.06","288.36","","0","0.00","0.00","BAR_RO_DB","LEISURE","DBZ" +"Synthetic Hotel","","","","","BK100032/1","Synthetic Guest 32","14/Sep/2025","15/Sep/2025","SYSTEM","12/09/2025 19:17:00","1","101.14","114.79","101.14","114.79","","0","0.00","0.00","BAR_RO_SGL","LEISURE","SINGLE" diff --git a/backend/data/generate.py b/backend/data/generate.py new file mode 100644 index 0000000..9c5ee29 --- /dev/null +++ b/backend/data/generate.py @@ -0,0 +1,175 @@ +"""Write synthetic Guestline exports in the same format as the real Rezlynx CSVs. + +Every value is made up. Guest columns are kept, filled with placeholders, so +parsers can be tested on dropping them. + +usage: python3 data/generate.py +""" + +import csv +import random +from collections import Counter +from datetime import date, datetime, time, timedelta +from pathlib import Path + +OUT = Path(__file__).parent +ROOMS = {"DOUBLE": 5, "DBZ": 2, "TWIN": 2, "SINGLE": 1} +IN_STOCK = sum(ROOMS.values()) +NIGHTS = [date(2025, 9, 1) + timedelta(n) for n in range(14)] +VAT = 1.135 +STAMP = "%d/%m/%Y %H:%M:%S" +DAY = "%d/%b/%Y" + +DNR_HEADER = [ + "Site", "Company", "CompanyName", "Agent", "AgentName", "BookRef", + "GuestName", "Arrival", "Departure", "OperatorCode", "BookingDate", + "Nights", "ADRSoldNett", "ADRSoldGross", "ReservationValueNett", + "ReservationValueGross", "CancelledTimeStamp", "CancelledNights", + "CancelledRevenueNett", "CancelledRevenueGross", "RatePlan", + "MarketSegment", "RoomType", +] # fmt: skip +CN_HEADER = [ + "CancelRef", "CancelDate", "CancelReasonCode", "Surname", "Company", + "Agent", "Ref", "ArrivalDate", "DepartureDate", "ProfileRef", "Operator", + "EstimatedRevenue", "CancelReason", "BookingCreatedDate", +] # fmt: skip +AF_HEADER = [ + "Date", "DayOfWeek", "Avail", "Total", "Indv", "Multi", "Blocks", "Occ", + "Ad", "Ch", "Inf", "Accomm", "FB", "Other", "NonRevenue", "TotalRevenue", + "GrandTotal", "CumulativeAccomm", "CumulativeFB", "CumulativeOther", + "CumulativeTotalRevenue", "CumulativeNonRevenue", "CumulativeGrandTotal", + "ARR", "APR", "Yield", "Blked", "ROOS", "DFS", "Waitlist", "InStock", + "RatePlanRevenue", +] # fmt: skip + + +def money(x): + return f"{x:.2f}" + + +def stay(arrival, departure): + return [arrival + timedelta(n) for n in range((departure - arrival).days)] + + +def make_bookings(rng): + """Bookings that fit the rooms; about 30% are later cancelled.""" + live = Counter() + bookings = [] + for _ in range(400): + arrival = rng.choice(NIGHTS) + nights = rng.choice([1, 1, 2, 3]) + departure = min(arrival + timedelta(nights), NIGHTS[-1] + timedelta(1)) + rooms = [rng.choice(list(ROOMS)) for _ in range(rng.choice([1, 1, 1, 2, 3]))] + need = Counter((t, n) for t in rooms for n in stay(arrival, departure)) + if any(live[k] + c > ROOMS[k[0]] for k, c in need.items()): + continue + cancelled = rng.random() < 0.3 + if not cancelled: + live.update(need) + lead = timedelta(days=rng.randint(0, 90), minutes=rng.randint(0, 1439)) + bookings.append( + { + "arrival": arrival, + "departure": departure, + "booked_at": datetime.combine(arrival, time()) - lead, + "rooms": [(t, round(rng.uniform(80, 220), 2)) for t in rooms], + "cancelled": cancelled, + } + ) + return bookings + + +def cancel_time(rng, booking): + arrival = datetime.combine(booking["arrival"], time()) + if rng.random() < 0.1: + return "NO_SHOW", arrival + timedelta(days=1, hours=10) + return "GDE", booking["booked_at"] + (arrival - booking["booked_at"]) * rng.random() + + +def write(name, header, rows): + with open(OUT / name, "w", newline="", encoding="utf-8-sig") as f: + w = csv.writer(f, quoting=csv.QUOTE_ALL) + w.writerow(header) + w.writerows(rows) + + +def read(name): + with open(OUT / name, encoding="utf-8-sig") as f: + return list(csv.DictReader(f)) + + +def main(): + rng = random.Random(0) + bookings = make_bookings(rng) + dnr, cn = [], [] + total, multi, nett = Counter(), Counter(), Counter() + for i, b in enumerate(bookings, start=1): + nights = stay(b["arrival"], b["departure"]) + arrival, departure = b["arrival"].strftime(DAY), b["departure"].strftime(DAY) + booked_at = b["booked_at"].strftime(STAMP) + for n, (room_type, rate) in enumerate(b["rooms"], start=1): + ref = f"BK{100000 + i}/{n}" + value = rate * len(nights) + if b["cancelled"]: + code, at = cancel_time(rng, b) + cn.append([ + f"CN{50000 + i}/{n}", at.strftime(STAMP), code, f"Guest{i}", + "", "", ref, arrival, departure, f"PF{i:06d}", "SYSTEM", + money(value), "", booked_at, + ]) # fmt: skip + continue + single = room_type == "SINGLE" + plan = "BAR_RO_SGL" if single else rng.choice(["BAR_RO", "BAR_RO_DB"]) + dnr.append([ + "Synthetic Hotel", "", "", "", "", ref, f"Synthetic Guest {i}", + arrival, departure, "SYSTEM", booked_at, str(len(nights)), + money(rate), money(rate * VAT), money(value), money(value * VAT), + "", "0", "0.00", "0.00", plan, "LEISURE", room_type, + ]) # fmt: skip + for night in nights: + total[night] += 1 + multi[night] += len(b["rooms"]) > 1 + nett[night] += rate + dnr.sort(key=lambda r: datetime.strptime(r[10], STAMP)) + cn.sort(key=lambda r: datetime.strptime(r[1], STAMP)) + + af, cum = [], 0.0 + for night in NIGHTS: + sold, accomm = total[night], round(nett[night], 2) + roos = rng.choice([0, 0, 0, 1]) if sold < IN_STOCK else 0 + adults = sold * 2 + cum += accomm + af.append([ + datetime.combine(night, time()).strftime(STAMP), night.strftime("%A"), + money(IN_STOCK - sold - roos), str(sold), str(sold - multi[night]), + str(multi[night]), "0", money(100 * sold / IN_STOCK), str(adults), + "0", "0", money(accomm), "0.00", "0.00", "0.00", money(accomm), + money(accomm), money(cum), "0.00", "0.00", money(cum), "0.00", + money(cum), money(accomm / sold if sold else 0), + money(accomm / adults if adults else 0), money(accomm / IN_STOCK), + "0", str(roos), "0", "0", str(IN_STOCK), money(accomm * VAT), + ]) # fmt: skip + + write("DailyNewReservations.csv", DNR_HEADER, dnr) + write("Cancellation.csv", CN_HEADER, cn) + write("Actual & Forecast.csv", AF_HEADER, af) + + rebuilt = Counter() + for r in read("DailyNewReservations.csv"): + a, d = (datetime.strptime(r[k], DAY).date() for k in ("Arrival", "Departure")) + rebuilt.update(stay(a, d)) + for r in read("Actual & Forecast.csv"): + night = datetime.strptime(r["Date"], STAMP).date() + assert rebuilt[night] == int(r["Total"]), f"{night}: rebuild != Total" + assert float(r["Avail"]) >= 0, f"{night}: overbooked" + for r in read("Cancellation.csv"): + created, cancelled = ( + datetime.strptime(r[k], STAMP) for k in ("BookingCreatedDate", "CancelDate") + ) + assert created <= cancelled, f"{r['CancelRef']}: cancelled before booked" + occupancy = sum(total.values()) / (IN_STOCK * len(NIGHTS)) + print(f"{len(dnr)} live rooms, {len(cn)} cancelled, {occupancy:.0%} occupancy") + + +if __name__ == "__main__": + main() diff --git a/docs/decisions/0031-pms-reports-for-training.md b/docs/decisions/0031-pms-reports-for-training.md index 9d1704c..7b68122 100644 --- a/docs/decisions/0031-pms-reports-for-training.md +++ b/docs/decisions/0031-pms-reports-for-training.md @@ -1,6 +1,6 @@ # 0031. Guestline reports used for training -**Status:** Accepted · **Date:** 2026-10-20 +**Status:** Superseded by [0037](0037-three-guestline-reports.md) · **Date:** 2026-10-20 ## Context We have full access to the client's PMS, Guestline Rezlynx (Beresford Hotel, diff --git a/docs/decisions/0037-three-guestline-reports.md b/docs/decisions/0037-three-guestline-reports.md new file mode 100644 index 0000000..6f9df44 --- /dev/null +++ b/docs/decisions/0037-three-guestline-reports.md @@ -0,0 +1,109 @@ +# 0037. Three Guestline reports, uploaded daily + +**Status:** Accepted · **Date:** 2026-10-25 + +Supersedes [0031](0031-pms-reports-for-training.md). + +## Context +[0031](0031-pms-reports-for-training.md) picked five Rezlynx reports to tell, +for every past night, whether its price was too low, about right or too high. +The client runs at 93–99% occupancy, so final occupancy alone can't tell an +underpriced night (sold out weeks ahead) from a well-priced one (sold out at +the last minute). **When** a night filled up tells them apart. + +The first exports have been checked +([guestline-data.md](../guestline-data.md)). The team decided Daily Rate +Analysis and Rate Override aren't needed for the MVP. The one Rate Override +export (200 rows, cut off by a row limit) was mostly automatic OTA discounts, +not prices staff set by hand. + +## Decision +Use three reports, all under **Menu → Reports** in Rezlynx. + +| Report | What it gives | Why we need it | +|---|---|---| +| **Daily New Reservations** | The date each live booking was made | Shows when each night filled up. Sold out weeks ahead means **too low**; sold out in the last days means **about right**. For future nights, "how full now vs similar nights at the same point" is the best early sign of extra demand. | +| **Cancellations** | The date each cancelled booking was made and cancelled | About a third of booked nights are cancelled. Daily New Reservations leaves cancelled bookings out, so this report is the only record of them. | +| **Actual & Forecast** | Final rooms sold and revenue per night (already imported), plus **Blocks**, **ROOS** and **Wait List** | Empty rooms on the night mean **too high**. Blocks (group rooms at negotiated prices) are excluded. ROOS (rooms out of service) gives the true number of rooms that could be sold. A waitlist means **too low**. Also checks that the booking rebuild adds up. | + +How full a night was on an earlier date D is **rebuilt**: live bookings made +on or before D, plus cancelled bookings made on or before D and cancelled +after D. Each room is its own booking: `BK…/N` numbers the rooms within one +reservation. + +### Key columns + +| Report | One row per | Key columns | +|---|---|---| +| Daily New Reservations | room booked | BookRef, BookingDate, Arrival, Departure, Nights, Company, Agent, ADRSold and ReservationValue (Nett/Gross), RatePlan, MarketSegment, RoomType | +| Cancellations | room cancelled | CancelRef, CancelDate, CancelReasonCode, Ref, ArrivalDate, DepartureDate, EstimatedRevenue, BookingCreatedDate | +| Actual & Forecast | night | Date, Avail, Total, Indv, Multi, Blocks, Occ, Accomm, ARR, APR, Yield, Blked, ROOS, DFS, Waitlist, InStock | + +### Exports and uploads + +All three reports are uploaded daily. The first upload of each is a one-off +backfill of the history. + +| Report | Filter by | Backfill (once) | Daily upload | +|---|---|---|---| +| Daily New Reservations | booking date | 2 years back to today | last 7 days | +| Cancellations | cancellation date | 2 years back to today | last 7 days | +| Actual & Forecast | stay date; Blocks, Wait List and ROOS on; one row per day | 2 years back to today | last 7 nights | + +- Each daily upload overlaps the last six, so a missed day is filled by the + next upload. Loading the same rows twice changes nothing. +- Actual & Forecast stops at today. A future night's row only shows rooms + booked so far, which the booking rebuild already gives, and it would look + like a finished night with low occupancy. + +- Delete guest columns before sharing (GuestName, Surname, ProfileRef, + Operator, OperatorCode, and the free-text CompanyName, Company and + CancelReason) but keep BookRef, Ref and CancelRef. Share through a private + drive, never the repo. + +### Not used for now +- **Daily Rate Analysis** and **Rate Override**: dropped on 2026-10-25. +- **Occupancy** (rooms sold per room type per night): covered by the booking + reports; revisit when pricing more than one room type. +- **New Reservations by Date**: daily totals not tied to stay nights; only a + check on import totals. +- **Pickup & Wash**: group blocks only, from Jan 2025, no export. + +Room types and room counts are entered at onboarding, not imported (today: +DOUBLE 53, DS 19, DBZ 18, TWIN 13, SINGLE 4). + +## Consequences +- Adds the "sold out too early" factor that [0003](0003-sample-weighting.md) + left unimplemented, plus a waitlist factor. Each needs its own record when + built. +- Enables "% sold so far" and "7-day pickup" as features for future nights, + computed the same way for training and serving + ([0007](0007-shared-feature-builder.md)). +- Needs a booking-level table, parsers and a migration; not designed yet. +- The rebuild matches Actual & Forecast `Total` on every night from Jan 2025. + Nights in 2024 are short until about Oct 2024 because their earliest + bookings predate the export. +- Occupancy is computed from sellable rooms (rooms − ROOS). Occ% counts + out-of-service rooms as unsold, so a fully sold night shows as 99%. +- The only price per night is the nightly average (`ARR`), not the price + each booking paid. Overridden or discounted bookings are not marked. +- Nett vs gross must be consistent: gross = nett + 13.5% VAT. `Accomm` in + Actual & Forecast is 8.5% above the bookings' nett total and 4.3% below + gross, for reasons not yet known. +- "% sold so far" for future nights uses total rooms, not rooms − ROOS, + because future nights' ROOS isn't imported. +- Parsers drop guest columns; real exports aren't committed. +- Daily New Reservations filtered by booking date never shows a booking + again after it is made. If amendments turn out to be common, add a + periodic export of future arrivals by arrival date. +- The rebuild misses date changes on amended bookings and may miss group + blocks. + +Still to confirm: +- Whether amended bookings keep their original dates (Help → Logs → + Reservation Logs). +- What DFS means, and whether Blocks, Wait List and DFS are always 0 or were + switched off in the export. +- Why `Accomm` matches neither nett nor gross. +- Whether Actual & Forecast's "Compare with → Custom" shows how full a past + night was at an earlier date (a ready-made check on the rebuild). diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 16376cb..fe5f731 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -51,9 +51,10 @@ Number files sequentially (`0027-short-title.md`) and add them to the index. | [0028](0028-train-and-serve-on-ingested-data.md) | Training and recommendations use only ingested data | ML | | [0029](0029-every-training-run-is-a-new-version.md) | Every training run is a new, described version | ML | | [0030](0030-schema-descriptions-and-examples.md) | Every field is described; examples are built from the fields | API | -| [0031](0031-pms-reports-for-training.md) | Guestline reports used for training | data | +| [0031](0031-pms-reports-for-training.md) | ~~Guestline reports used for training~~ (superseded by 0037) | data | | [0032](0032-events-on-a-night-follow-the-model.md) | The events shown for a night follow the model's rule | API | | [0033](0033-data-freshness-rules.md) | Freshness comes from jobs; the service decides what's stale | API | | [0034](0034-snapshot-only-when-changed.md) | A snapshot is saved only when the model, rate or range changed | data model | | [0035](0035-users-keyed-on-google-id.md) | Users are identified by their Google account id | data model | | [0036](0036-operator-commands-in-cli.md) | Operator commands live in `cli.py` and commit once | architecture | +| [0037](0037-three-guestline-reports.md) | Three Guestline reports, uploaded daily | data | diff --git a/docs/guestline-data.md b/docs/guestline-data.md new file mode 100644 index 0000000..c381ec6 --- /dev/null +++ b/docs/guestline-data.md @@ -0,0 +1,79 @@ +# Guestline export data + +Beresford Hotel exports from Rezlynx, checked on 25 Oct 2026. [0037](decisions/0037-three-guestline-reports.md) lists the reports and why each one matters. This page records what the exports actually contain. + +The real exports are not in the repo. `backend/hidden_data/` is gitignored: + +- `backend/hidden_data/*.csv` are the raw exports, with guest names. +- `backend/hidden_data/clean/` holds the same four files with guest columns removed. + +`backend/data/` is committed and holds synthetic files for tests. They have the real headers, date formats and quoting, for a made-up 10-room hotel over 1 to 14 Sep 2025. Guest columns hold placeholders. Cancelled rooms appear only in `Cancellation.csv`, a few bookings have more than one room, and the live bookings add up to `Total` on every night. Regenerate them with `python3 data/generate.py` from `backend/`. + +## Columns removed in `clean/` + +| Report | Removed | +|---|---| +| Daily New Reservations | `GuestName`, `OperatorCode`, `CompanyName` | +| Cancellations | `Surname`, `ProfileRef`, `Operator`, `Company`, `CancelReason` | +| Rate Override | `Surname`, `OperatorCode` | +| Actual & Forecast | none | + +`CompanyName`, `Company` and `CancelReason` are not on the ticket's list. They are free text that can hold a person's name. The code columns (`Company`, `CancelReasonCode`) and the references (`BookRef`, `Ref`, `CancelRef`) stay. + +No field in `clean/` equals a guest name or part of one. The only matches were values such as `DOUBLE` and `Friday`, which also appear in some guest names. + +## Coverage + +| Report | Rows | Range | Filter (inferred from the data) | Status | +|---|---|---|---|---| +| Daily New Reservations | 55,377 | booked 1 Jan 2024 to 21 Oct 2026 | booking date | Complete for the range | +| Cancellations | 44,026 | arrivals 1 Jan 2024 to 22 Oct 2026 | arrival date, not cancellation date | Usable for past nights | +| Rate Override | 200 | applied 1 to 2 Jan 2024 | applied date | Out of scope. Cut off at 200 rows. | +| Actual & Forecast | 1,025 | 1 Jan 2024 to 21 Oct 2026 | stay date | Complete for the range | +| Daily Rate Analysis | none | | | Out of scope. Not exported. | + +Rate Override and Daily Rate Analysis are out of scope. On 25 Oct 2026 the team decided neither is useful. The Rate Override rows already exported stay in `clean/`, but nothing depends on them. + +Cancellations lines 32388 and 32869 have the wrong number of fields because a stray quote in a guest name breaks the CSV. Both lines are dropped from `clean/`. + +## Answers to the open questions in 0031 + +**Does each report export as CSV and cover two years?** Daily New Reservations, Cancellations and Actual & Forecast export as CSV and cover 1 Jan 2024 to the export date. + +**Does Daily New Reservations include bookings that were later cancelled?** No. None of the 43,834 references in Cancellations appears in Daily New Reservations, and `CancelledTimeStamp` is empty on every row. Cancellations has `BookingCreatedDate`, so the two reports together give every booking. The number of rooms on the books for a night as of date D is the live bookings made by D, plus the cancelled bookings made by D and cancelled after D. + +**Is `CancelDate` the cancellation date?** Yes. It is never earlier than `BookingCreatedDate`. 75 rows fall one day after arrival, and 36 of those have reason code `NO_SHOW`. + +**What does `/1` in BookRef mean?** It numbers the rooms within one booking. 3,769 bookings have more than one room, and the largest has 45. Within one booking, 1,560 bookings mix room types, 678 mix rate plans, and 2,700 had rooms added at different times, almost all on the same day. Treat each `BK.../N` as its own row. + +**Do amended bookings keep their original dates?** Not answered. Each BookRef appears once, so amendments leave no trace in these exports. Check **Help → Logs → Reservation Logs** in Rezlynx. + +**What does DFS mean?** Not answered. `DFS`, `Waitlist`, `Blked` and `Blocks` are 0 on every night. Either the export had those options switched off, or the hotel does not use them. `ROOS` varies, and `Avail + Total + ROOS = InStock` (107) on 1,001 of 1,025 nights. + +## Nett and gross + +- Daily New Reservations has both. `ADRSoldGross / ADRSoldNett` is 1.135 on 96% of rows, which matches 13.5% VAT. +- Actual & Forecast `Accomm` matches neither. From Jan 2025 to Oct 2026, `Accomm` totals €10.33m. Over the same nights, the bookings total €9.52m nett and €10.80m gross. `Accomm` is 8.5% above nett and 4.3% below gross. `ARR` is `Accomm / Total`. `RatePlanRevenue` is 1.5% below the bookings' gross. The cause is unknown. + +## The booking rebuild matches Actual & Forecast + +Counting live bookings per night reproduces Actual & Forecast `Total` exactly on every night from 1 Jan 2025 to 20 Oct 2026. + +In 2024 the count runs short. It is 29 rooms a night short in January and reaches zero by December, because those stays include bookings made before 1 Jan 2024. Treat 2024 nights as complete only from about Oct 2024, or export bookings from mid-2023. + +## Pickup for future nights needs cancellations by cancellation date + +The current Cancellations export is filtered by arrival date and has only 44 rows for arrivals after the export date. Future-night features ("% sold so far", "7-day pickup") need cancellations of future stays. + +## All three reports are uploaded daily + +[0037](decisions/0037-three-guestline-reports.md) records the decision. The first upload of each report is a 2-year backfill to today. After that, the manager uploads the last 7 days of each report every day. + +## Still open + +- Check whether Blocks, Wait List and DFS in Actual & Forecast are always 0 or were switched off in the export. +- Re-export Cancellations by cancellation date. +- Optional: export bookings from mid-2023. +- Find out why `Accomm` sits between nett and gross. +- Check Reservation Logs for amendments and System Logs for rate-table changes. +- Check whether **Compare with → Custom** in Actual & Forecast shows how full a past night was at an earlier date. diff --git a/docs/mvp.md b/docs/mvp.md index b01acc3..8b31161 100644 --- a/docs/mvp.md +++ b/docs/mvp.md @@ -26,7 +26,7 @@ end. | Events from PredictHQ | AskNews / news-extracted events | | Weather | Flights, competitor rates (built but not connected) | | Scheduled refresh and retraining | Pushing rates to the PMS or channel manager (no API exists) | -| Manual CSV upload of Guestline reports ([0031](decisions/0031-pms-reports-for-training.md)) | Guestline API integration (full PMS access, but API needs partner onboarding) | +| Manual CSV upload of Guestline reports ([0037](decisions/0037-three-guestline-reports.md)) | Guestline API integration (full PMS access, but API needs partner onboarding) | | One room type ("Standard") | Multiple room types (soon after) | ## User stories @@ -99,8 +99,8 @@ The current `frontend/` is a static prototype with no API calls. | # | Item | Stories | |---|---|---| -| **D1** | **Collect the five Guestline exports listed in [0031](decisions/0031-pms-reports-for-training.md) (2 years, CSV, guest columns removed) and answer its "still to confirm" list.** | S11 | -| D2 | Booking-level table, parsers and migration for Daily New Reservations, Cancellations, Daily Rate Analysis and Rate Override; map Blocks, ROOS and Wait List from Actual & Forecast. Check the rebuild of how full each night was against Actual & Forecast. New decision record. | S11, S2 | +| **D1** | **Collect the three Guestline exports in [guestline-data.md](guestline-data.md) (2 years, CSV, guest columns removed) and answer the "still to confirm" list in [0037](decisions/0037-three-guestline-reports.md).** | S11 | +| D2 | Booking-level table, parsers and migration for Daily New Reservations and Cancellations; map Blocks, ROOS and Wait List from Actual & Forecast. Check the rebuild of how full each night was against Actual & Forecast. New decision record. | S11, S2 | | D3 | Weighting factors for "sold out too early" and waitlist, and "% sold so far" / "7-day pickup" features (Q2). New decision records. | S2, S10 | ## Open questions @@ -108,13 +108,13 @@ The current `frontend/` is a static prototype with no API calls. | # | Question | Blocks | |---|---|---| | Q1 | How is the manager told about an update: a badge on the calendar card, a notification, or nothing beyond the change shown on the card? | F2, S6 | -| Q2 | **The design shows data the MVP doesn't have.** "% sold" and "Pickup, 7 days" can come from the Guestline booking reports ([0031](decisions/0031-pms-reports-for-training.md), D2/D3). "Comp set median" and "Comp set gap" need competitor rates (out of scope): drop them or mock them? | D2, F2, F3 | +| Q2 | **The design shows data the MVP doesn't have.** "% sold" and "Pickup, 7 days" can come from the Guestline booking reports ([0037](decisions/0037-three-guestline-reports.md), D2/D3). "Comp set median" and "Comp set gap" need competitor rates (out of scope): drop them or mock them? | D2, F2, F3 | | Q3 | What goes on each calendar card and in the "Color by" options (Demand, Occupancy, Price change, Comp set gap, Events, Weather)? This is a design question. | F2 | | Q4 | How does feedback train the model? Replace the label with the ideal rate, add a feature, or weight the night more? How are verdicts with no number used? Does "who was right" help the model? | M1 | | Q5 | Retrain on every piece of feedback, or on a schedule? Depends on M2. | M1, O1 | | Q6 | Where does the scheduler run: in the API process, external cron calling the API, or a Neon cron trigger? The API currently runs a single worker and jobs die on restart. | O1 | | Q7 | How often should each refresh run (events, weather, retrain)? "Within hours" for events is the goal; PredictHQ credit limits apply. | O1, S6 | -| Q8 | ~~Which CSVs will the manager upload, and what decides whether a past rate was optimal?~~ Answered in [0031](decisions/0031-pms-reports-for-training.md). Still open: which of the five the manager uploads daily vs. once as a backfill. | D1, D2 | +| Q8 | ~~Which CSVs will the manager upload, and what decides whether a past rate was optimal?~~ Answered in [0037](decisions/0037-three-guestline-reports.md): all three daily, each covering the last 7 days, after a one-off 2-year backfill. | D1, D2 | | Q9 | Auth: invite-only, or self-signup and Google login as in the design? The sign-up page says "free for 30 days" and "within the limits you set". Are trials and rate limits (min/max rates) in the MVP? | B1, F1 | | Q10 | The design says "Rates shown are Double, lowest available". Until room types are added, is "Standard" the rate we show? | F2 | | Q11 | Is past performance (e.g. the same night last year) ever shown? It's not in the MVP for now. | — | diff --git a/docs/system-design.md b/docs/system-design.md index bb99c7e..72faf37 100644 --- a/docs/system-design.md +++ b/docs/system-design.md @@ -541,7 +541,7 @@ requested. recommendation snapshots) only run when called. 5. **Tests.** The suite predates this design and hasn't been updated. Nothing has been run against real PredictHQ, AskNews, AeroDataBox or OpenSky keys. -6. **Booking-level PMS data.** [0031](decisions/0031-pms-reports-for-training.md) +6. **Booking-level PMS data.** [0037](decisions/0037-three-guestline-reports.md) picks the Guestline reports to import, but the table, parsers and the rebuild of how full each night was before arrival aren't designed yet. Until then, the performance import (6.2) is the only hotel data.