Initial commit: STR Optimization Manager MVP

Full-stack short-term rental management platform with:
- React/Vite frontend with dark theme dashboard, performance, pricing,
  reservations, experiments, and settings pages
- Fastify API server with auth, platform management, performance tracking,
  pricing, reservations, experiments, and weekly report endpoints
- Playwright-based scraper service with Airbnb adapter (login with MFA,
  performance metrics, reservations, calendar pricing, price changes)
- VRBO adapter scaffold and mock adapter for development
- PostgreSQL with Drizzle ORM, migrations, and seed scripts
- Job queue with worker for async scraping tasks
- AES-256-GCM credential encryption for platform credentials
- Session cookie persistence for scraper browser sessions
- Docker Compose for PostgreSQL database

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 15:03:21 -04:00
parent 4735c73b3a
commit d4c714fadc
76 changed files with 18465 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'drizzle-kit';
import dotenv from 'dotenv';
dotenv.config({ path: '../../.env' });
export default defineConfig({
schema: './src/db/schema.ts',
out: './src/db/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});

41
apps/api/package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "@str/api",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:seed": "tsx src/db/seed.ts",
"db:seed:demo": "tsx src/db/seed-demo.ts",
"db:seed:clean": "tsx src/db/seed-clean.ts",
"db:restore:demo": "psql $DATABASE_URL < src/db/demo-seed.sql"
},
"dependencies": {
"@str/shared-types": "*",
"fastify": "^5.2.0",
"@fastify/cors": "^10.0.0",
"@fastify/cookie": "^11.0.0",
"@fastify/jwt": "^9.0.0",
"drizzle-orm": "^0.38.0",
"postgres": "^3.4.0",
"bcrypt": "^5.1.1",
"node-cron": "^3.0.3",
"nodemailer": "^6.9.0",
"zod": "^3.24.0",
"dotenv": "^16.4.0",
"fastify-plugin": "^5.0.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/node": "^22.0.0",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.17",
"drizzle-kit": "^0.30.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
}

27
apps/api/src/config.ts Normal file
View File

@@ -0,0 +1,27 @@
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { z } from 'zod';
const __dirname = dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: resolve(__dirname, '../../../.env') });
const envSchema = z.object({
DATABASE_URL: z.string().min(1),
JWT_SECRET: z.string().min(1),
ENCRYPTION_KEY: z.string().min(1),
APP_USERNAME: z.string().min(1),
APP_PASSWORD_HASH: z.string().min(1),
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().optional().default(587),
SMTP_USER: z.string().optional(),
SMTP_PASS: z.string().optional(),
REPORT_EMAIL_TO: z.string().optional(),
SCRAPER_URL: z.string().default('http://localhost:3001'),
});
export type Config = z.infer<typeof envSchema>;
export const config: Config = envSchema.parse(process.env);

View File

@@ -0,0 +1,545 @@
--
-- PostgreSQL database dump
--
\restrict MeN1ws58DquMlfn6S1mGywDt5fuo6rhsE6g75LgmXhiU6DV4nSIsGAX8gVJ0f2n
-- Dumped from database version 16.13
-- Dumped by pg_dump version 16.13
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Data for Name: __drizzle_migrations; Type: TABLE DATA; Schema: drizzle; Owner: str_manager
--
INSERT INTO drizzle.__drizzle_migrations VALUES (1, 'ad15e3288889b1c1a4e2a827a3805267b06787a9dafee630dfde2b265bf2e90c', 1774223467634);
--
-- Data for Name: platforms; Type: TABLE DATA; Schema: public; Owner: str_manager
--
INSERT INTO public.platforms VALUES ('airbnb', 'Airbnb', NULL, NULL, NULL, true);
INSERT INTO public.platforms VALUES ('vrbo', 'VRBO', NULL, NULL, NULL, true);
INSERT INTO public.platforms VALUES ('mock', 'Mock (Dev)', NULL, NULL, NULL, true);
--
-- Data for Name: daily_prices; Type: TABLE DATA; Schema: public; Owner: str_manager
--
INSERT INTO public.daily_prices VALUES ('2c9dea07-ec0d-48f5-b8a8-d7110c39bbb1', 'airbnb', '2025-12-23', 237.95, true, 1, '2025-12-23 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('dd1f5e59-4c64-4f68-81e5-29a6598b22cc', 'airbnb', '2025-12-24', 194.13, true, 1, '2025-12-24 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('a1852c5a-c712-443c-8b9d-6c8c2eab081b', 'airbnb', '2025-12-25', 235.94, true, 1, '2025-12-25 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('d9debe06-4f06-4749-8186-0636450764c3', 'airbnb', '2025-12-26', 195.10, true, 2, '2025-12-26 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('11bcd32e-ef92-4c61-9f04-d2fd55b5761f', 'airbnb', '2025-12-27', 191.84, true, 2, '2025-12-27 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('b7c303f0-4570-4bec-ae35-7e4506571831', 'airbnb', '2025-12-28', 249.27, true, 2, '2025-12-28 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('9255fbb6-c744-4bf2-a7f8-fb16f8248a17', 'airbnb', '2025-12-29', 174.15, true, 1, '2025-12-29 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('4cef5cdd-0354-48ab-b35a-87d2ba36b71c', 'airbnb', '2025-12-30', 233.95, true, 1, '2025-12-30 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('2f664dbf-8ccc-4f30-b488-27f1650fda9f', 'airbnb', '2025-12-31', 163.79, true, 1, '2025-12-31 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('d68f65ea-f2e1-4c8a-ab7e-7fe078f4a8dd', 'airbnb', '2026-01-01', 196.62, true, 1, '2026-01-01 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('9929a38c-03c8-4bbf-9e17-dc1f0d459dfa', 'airbnb', '2026-01-02', 290.31, true, 2, '2026-01-02 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('6a55a1b7-8283-4a5f-b2fb-f7de046cb143', 'airbnb', '2026-01-03', 247.44, true, 2, '2026-01-03 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('02024d55-bae0-499b-86fc-6856d1576924', 'airbnb', '2026-01-04', 298.87, true, 2, '2026-01-04 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('621cc0bd-d499-4e36-ad3b-239374980d43', 'airbnb', '2026-01-05', 225.26, false, 1, '2026-01-05 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('0efe73d0-ca93-4629-8192-63662bcfe764', 'airbnb', '2026-01-06', 164.11, true, 1, '2026-01-06 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('4e653a32-cfcd-42d0-a6f8-fcd9c7cc0803', 'airbnb', '2026-01-07', 186.78, true, 1, '2026-01-07 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('312e8284-ccdb-4ddd-a5ee-f8ef6c02a472', 'airbnb', '2026-01-08', 242.66, false, 1, '2026-01-08 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('8d33139a-d235-4e44-a02d-873a087460a4', 'airbnb', '2026-01-09', 208.09, false, 2, '2026-01-09 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('41ec240d-15de-409f-8f55-882f16f33dbb', 'airbnb', '2026-01-10', 216.00, true, 2, '2026-01-10 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('9a6ae96b-03f0-40e4-a984-5479a9503421', 'airbnb', '2026-01-11', 254.83, true, 2, '2026-01-11 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('4cd31c31-1911-4845-864f-ffc0fc0bd9f6', 'airbnb', '2026-01-12', 180.48, true, 1, '2026-01-12 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('24710d96-c5e3-467b-bdd7-71774a3916da', 'airbnb', '2026-01-13', 201.25, false, 1, '2026-01-13 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('07d4c62f-585d-4b34-b28e-d0b57be028b6', 'airbnb', '2026-01-14', 234.48, true, 1, '2026-01-14 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('f85aa922-16bd-4c3f-b9c9-f9980f48b69d', 'airbnb', '2026-01-15', 240.79, true, 1, '2026-01-15 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('9ce4ffc0-2285-45e5-9b5b-aa3a95108c19', 'airbnb', '2026-01-16', 252.73, false, 2, '2026-01-16 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('fab441e7-8bdb-4b09-a417-0c6b349b0959', 'airbnb', '2026-01-17', 325.09, true, 2, '2026-01-17 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('38148a0f-742c-438e-92b9-70f82565a231', 'airbnb', '2026-01-18', 246.46, true, 2, '2026-01-18 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('2cdbebcd-3158-43f9-8467-85188410f113', 'airbnb', '2026-01-19', 246.85, true, 1, '2026-01-19 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('1926caaf-9c20-48b9-8979-a2267d07b192', 'airbnb', '2026-01-20', 221.77, true, 1, '2026-01-20 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('c2e2691a-0c9c-44a3-a7d1-46939fee3df0', 'airbnb', '2026-01-21', 244.93, true, 1, '2026-01-21 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('41e57335-31af-40e7-b0c6-7294dab2131c', 'airbnb', '2026-01-22', 184.38, false, 1, '2026-01-22 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('f04b3baf-8071-44e3-9edc-b481687486b2', 'airbnb', '2026-01-23', 300.03, true, 2, '2026-01-23 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('e0352ebf-9642-4d6b-a8e4-185e60bae543', 'airbnb', '2026-01-24', 287.10, true, 2, '2026-01-24 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('a5cf1b79-7baf-4cf2-8a11-6efa368f8b19', 'airbnb', '2026-01-25', 261.57, true, 2, '2026-01-25 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('7e965a25-bdde-441b-94e9-06395c448f71', 'airbnb', '2026-01-26', 182.52, true, 1, '2026-01-26 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('8996b768-71d3-45d1-8f6e-a0a6da83af42', 'airbnb', '2026-01-27', 237.59, false, 1, '2026-01-27 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('84e5dd9d-b708-4df3-8a3b-dcf876a26c62', 'airbnb', '2026-01-28', 164.46, true, 1, '2026-01-28 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('cea3a47f-1784-4419-b234-cbf13bca7c47', 'airbnb', '2026-01-29', 166.64, false, 1, '2026-01-29 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('66a6b2f8-59e9-43d3-8067-6b826d3d9e57', 'airbnb', '2026-01-30', 250.10, true, 2, '2026-01-30 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('4b887be4-1507-4433-bcc0-9dae5c20b6ff', 'airbnb', '2026-01-31', 303.08, true, 2, '2026-01-31 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('2f355b7e-7492-488b-a37c-fa92cd293571', 'airbnb', '2026-02-01', 229.66, true, 2, '2026-02-01 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('600623bd-d604-47ca-ab53-6485977e239b', 'airbnb', '2026-02-02', 244.30, true, 1, '2026-02-02 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('fe31882c-9b0b-47d4-8e19-f6b94510a872', 'airbnb', '2026-02-03', 177.29, true, 1, '2026-02-03 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('0662fddb-e7d7-423d-8e33-5f144a5a1432', 'airbnb', '2026-02-04', 225.51, true, 1, '2026-02-04 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('5f12731a-adf5-4bd3-981c-99dd50cc421f', 'airbnb', '2026-02-05', 199.96, true, 1, '2026-02-05 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('9c440537-e4ae-4c56-8cd5-4be5dc3d582b', 'airbnb', '2026-02-06', 282.86, true, 2, '2026-02-06 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('ae8003e0-358f-4b69-a92a-8ee3a0cd3c01', 'airbnb', '2026-02-07', 309.42, true, 2, '2026-02-07 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('4ff3ab6f-9f0c-4f5d-b994-cf7f7b2da380', 'airbnb', '2026-02-08', 224.26, false, 2, '2026-02-08 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('6dd9ead4-e6c0-4b72-aed2-42a3516fe717', 'airbnb', '2026-02-09', 227.35, true, 1, '2026-02-09 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('6e725f12-0d6a-4a70-a5a1-e628f448a544', 'airbnb', '2026-02-10', 162.89, true, 1, '2026-02-10 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('883fb377-6532-41ae-b533-0fe1388cf768', 'airbnb', '2026-02-11', 218.63, true, 1, '2026-02-11 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('974b56a4-97de-4da7-b116-044a29c0fed2', 'airbnb', '2026-02-12', 171.73, true, 1, '2026-02-12 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('a9135752-c32d-475c-b652-e1716ba1ba57', 'airbnb', '2026-02-13', 189.10, true, 2, '2026-02-13 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('dfa95f66-0b3f-4d30-8aaf-916ac1a0cb49', 'airbnb', '2026-02-14', 341.28, true, 2, '2026-02-14 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('b091cdca-6d4a-4c37-9452-89e8f0d2049c', 'airbnb', '2026-02-15', 268.24, true, 2, '2026-02-15 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('db55b412-5bfc-4d69-b527-7a065f408edb', 'airbnb', '2026-02-16', 159.41, true, 1, '2026-02-16 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('05184acc-7c48-43b6-a7b0-3e93e4a722b4', 'airbnb', '2026-02-17', 157.34, true, 1, '2026-02-17 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('7b0c965f-7e3f-4e5e-816e-3ce3ddf953b7', 'airbnb', '2026-02-18', 222.94, true, 1, '2026-02-18 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('59a3a5e3-fdd0-4142-9cfa-639d1e70984c', 'airbnb', '2026-02-19', 232.85, true, 1, '2026-02-19 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('56446833-d44c-4d61-a3e4-c0f3749080f1', 'airbnb', '2026-02-20', 188.73, true, 2, '2026-02-20 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('9c0e4c8c-2e60-489c-bf58-b19dc9352187', 'airbnb', '2026-02-21', 221.09, true, 2, '2026-02-21 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('d6b83823-b2b3-49b8-ad70-3e2357403ab9', 'airbnb', '2026-02-22', 299.01, true, 2, '2026-02-22 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('02739fe6-9480-4f8b-8038-9eba4eb51451', 'airbnb', '2026-02-23', 211.04, true, 1, '2026-02-23 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('421546f8-b1ea-4f51-b426-fc2f5ed34cb6', 'airbnb', '2026-02-24', 180.85, true, 1, '2026-02-24 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('889f9040-e3ec-478e-9ed0-85275f950368', 'airbnb', '2026-02-25', 185.16, true, 1, '2026-02-25 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('8ddaf51a-7c09-4a1a-9303-9b8b0865227b', 'airbnb', '2026-02-26', 254.54, true, 1, '2026-02-26 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('4c8cf0f1-3d70-4dd7-bb14-48d43bc0f128', 'airbnb', '2026-02-27', 282.83, false, 2, '2026-02-27 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('4043b859-8096-43f6-a4c9-0520176aa1d4', 'airbnb', '2026-02-28', 296.40, true, 2, '2026-02-28 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('8907ddc3-b485-474e-ab7d-29ab4d685c2a', 'airbnb', '2026-03-01', 304.85, true, 2, '2026-03-01 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('5bbc17c8-8afe-44f1-972c-f259b6783afc', 'airbnb', '2026-03-02', 174.80, true, 1, '2026-03-02 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('667f8722-1752-4c07-8f4f-4d0342c4dd98', 'airbnb', '2026-03-03', 231.72, true, 1, '2026-03-03 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('6619cc03-4c21-43d7-a565-e976e5d75229', 'airbnb', '2026-03-04', 229.93, true, 1, '2026-03-04 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('dbeb6d1b-f0d1-4788-807f-f77d051e758c', 'airbnb', '2026-03-05', 212.14, true, 1, '2026-03-05 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('c0ad47f4-b1a0-42e7-bdde-cb85fdf70a60', 'airbnb', '2026-03-06', 295.08, false, 2, '2026-03-06 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('4814fdfe-fbcc-491a-a19b-9c253846ba64', 'airbnb', '2026-03-07', 332.95, true, 2, '2026-03-07 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('eaf35023-a102-4d1d-a7f1-7ccccc95ddf6', 'airbnb', '2026-03-08', 205.24, true, 2, '2026-03-08 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('b4b00968-9fde-403c-acfc-8ef478e286fe', 'airbnb', '2026-03-09', 220.21, true, 1, '2026-03-09 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('ab62b569-7de8-42de-87c4-df3fe95a9a92', 'airbnb', '2026-03-10', 191.39, true, 1, '2026-03-10 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('3b1b7cc6-62f8-45a2-9e7d-ad35df9e0b37', 'airbnb', '2026-03-11', 243.49, true, 1, '2026-03-11 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('7e2111bb-bb0e-429c-aede-564dd6762a76', 'airbnb', '2026-03-12', 229.69, false, 1, '2026-03-12 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('3ace8e2f-bd5d-4879-a941-b5918778cd45', 'airbnb', '2026-03-13', 288.21, true, 2, '2026-03-13 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('7db61230-cc4e-4f60-9836-5bd16487410b', 'airbnb', '2026-03-14', 287.18, true, 2, '2026-03-14 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('09b8cea7-1e43-4224-b5b9-8111cf0d394a', 'airbnb', '2026-03-15', 264.88, true, 2, '2026-03-15 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('c6a0a46c-e2a0-4309-8801-3dce2eb6f3e5', 'airbnb', '2026-03-16', 180.82, false, 1, '2026-03-16 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('58c6866d-551b-403b-88fd-9f12001c6e43', 'airbnb', '2026-03-17', 206.31, true, 1, '2026-03-17 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('b4e30d53-e615-4cc8-a554-9f5965625c81', 'airbnb', '2026-03-18', 246.44, false, 1, '2026-03-18 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('43d1d722-6dcd-4d6e-930b-47bc60d9cfcd', 'airbnb', '2026-03-19', 228.78, true, 1, '2026-03-19 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('730f18ae-68e1-4fea-a194-2c5c037ace08', 'airbnb', '2026-03-20', 295.96, true, 2, '2026-03-20 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('cb37b090-71a1-4f47-b33e-8bdb8cb88205', 'airbnb', '2026-03-21', 323.74, false, 2, '2026-03-21 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('afabbb45-3061-412d-852f-8b415126a5a5', 'airbnb', '2026-03-22', 298.92, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('66e28822-a497-460f-bfe4-42aaaaae160e', 'airbnb', '2026-03-23', 245.29, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('18286a2a-e233-495e-b21c-6349d352c5e7', 'airbnb', '2026-03-24', 191.22, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('a4ac710b-74cf-44c4-8833-99103f053eb4', 'airbnb', '2026-03-25', 182.74, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('6a09fd4b-4d3d-4f23-83f3-6f44a5b9dc1b', 'airbnb', '2026-03-26', 245.85, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('055cbe5f-23ff-4208-a2b4-656455a56437', 'airbnb', '2026-03-27', 254.91, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('fba2dc5a-c6e5-4525-ac5d-6f6c621b882f', 'airbnb', '2026-03-28', 296.14, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('bdcc65dd-9e59-4301-9038-a880afa0665b', 'airbnb', '2026-03-29', 260.76, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('301cea30-76a9-4953-a335-bb548e91f3c3', 'airbnb', '2026-03-30', 196.18, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('b40b779b-13a9-4757-a3d5-f4a132bf5638', 'airbnb', '2026-03-31', 187.47, false, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('ba3be540-5191-4bb5-bc28-d1a5971bbea6', 'airbnb', '2026-04-01', 245.51, false, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('ac25034a-1f27-403f-90df-1b46c9d9ba5d', 'airbnb', '2026-04-02', 164.85, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('6f932708-7adf-4ebf-932d-a184e45514ec', 'airbnb', '2026-04-03', 315.69, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('4df356f5-2917-4eb2-9428-2e7e71998b1f', 'airbnb', '2026-04-04', 265.43, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('19f011a9-a5e8-4220-9d28-9394b5ea92e3', 'airbnb', '2026-04-05', 295.06, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('a27361ae-6ffe-4843-8a31-c772c3b1d489', 'airbnb', '2026-04-06', 172.34, false, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('9ad04c21-c9d3-4e7b-94f6-6595d7961d01', 'airbnb', '2026-04-07', 234.57, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('f55506ac-fe17-4e6e-bf14-1b6b50f8514f', 'airbnb', '2026-04-08', 173.22, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('2cc7bc05-8a06-4168-bdc9-9f9d017b627e', 'airbnb', '2026-04-09', 203.89, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('5f735c14-0a30-4300-b7bc-52646d6777a1', 'airbnb', '2026-04-10', 270.28, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('84cdbc92-eb99-4c09-a039-ba29e6446eef', 'airbnb', '2026-04-11', 233.24, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('d67afeed-cfc7-453c-b418-80c7f7b19a16', 'airbnb', '2026-04-12', 209.87, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('ce9aacd6-1caa-47a1-b7e6-2c79285b0292', 'airbnb', '2026-04-13', 262.22, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('04fdddcd-a030-4405-998d-e764c64afb62', 'airbnb', '2026-04-14', 170.57, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('86d6983f-c21e-498a-84b3-d2c956d6ae67', 'airbnb', '2026-04-15', 163.64, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('1a555fbf-394d-48a8-8a24-ff60da28c349', 'airbnb', '2026-04-16', 235.11, false, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('74afffbb-4915-450d-8c31-11a5a308571f', 'airbnb', '2026-04-17', 314.95, false, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('dac5964d-8337-4a12-9f91-37a5526fb2e0', 'airbnb', '2026-04-18', 263.67, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('d5edf515-5e42-47f0-97a4-448ff6e4be67', 'airbnb', '2026-04-19', 220.53, false, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('c64d0d9f-0c53-4470-bd4e-fffc42300f65', 'airbnb', '2026-04-20', 197.42, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('81a16975-fc48-4e73-abe9-0d1e01622058', 'airbnb', '2026-04-21', 254.52, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('59d58410-4942-422b-8c6c-f98489d2c7e0', 'vrbo', '2025-12-23', 228.36, true, 1, '2025-12-23 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('26434ea6-9727-4cf3-90ab-f3f57a78da93', 'vrbo', '2025-12-24', 152.61, true, 1, '2025-12-24 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('5d880bcb-1c16-486c-ac91-c8eceee4827b', 'vrbo', '2025-12-25', 230.97, true, 1, '2025-12-25 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('21b6cf5f-dcef-40f9-9583-e7f756b4d616', 'vrbo', '2025-12-26', 211.16, true, 2, '2025-12-26 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('029ae0a4-a90b-4d6d-8775-d0878c2a7acc', 'vrbo', '2025-12-27', 200.67, true, 2, '2025-12-27 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('53310caf-b6bc-40f6-8860-6fad010df42d', 'vrbo', '2025-12-28', 315.46, true, 2, '2025-12-28 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('882f60dc-145e-4b4a-8125-6ab10dc90c3f', 'vrbo', '2025-12-29', 249.07, true, 1, '2025-12-29 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('f59a7811-0394-4e70-903c-17a75346338f', 'vrbo', '2025-12-30', 187.75, true, 1, '2025-12-30 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('b6cc13f9-0770-4145-baa2-87b1a4602679', 'vrbo', '2025-12-31', 214.94, true, 1, '2025-12-31 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('42ffac8d-7f43-4071-8870-c3269c9e00ea', 'vrbo', '2026-01-01', 228.78, true, 1, '2026-01-01 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('d030c063-0a82-4208-8b7c-c40d0a04d5ad', 'vrbo', '2026-01-02', 261.39, true, 2, '2026-01-02 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('b78b9727-5668-431c-bfa1-685eded88735', 'vrbo', '2026-01-03', 256.04, true, 2, '2026-01-03 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('3c72e68f-0267-4f52-a2d6-642059802d38', 'vrbo', '2026-01-04', 225.05, true, 2, '2026-01-04 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('56d6eb63-efb3-42af-bde0-79c4e79bafc7', 'vrbo', '2026-01-05', 176.31, true, 1, '2026-01-05 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('21c09c9d-4cbc-43f9-8109-f4aa29225189', 'vrbo', '2026-01-06', 202.40, true, 1, '2026-01-06 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('4af10ce2-c8e6-46b5-b8a6-301566f33ac3', 'vrbo', '2026-01-07', 174.60, true, 1, '2026-01-07 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('dcb7c44d-a88a-4d1d-a952-73207eb8f371', 'vrbo', '2026-01-08', 211.56, true, 1, '2026-01-08 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('b39d66c6-3795-4543-ba96-04426798b8df', 'vrbo', '2026-01-09', 202.87, true, 2, '2026-01-09 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('23dcd0c0-4cce-4788-a761-6f20749f00df', 'vrbo', '2026-01-10', 202.58, true, 2, '2026-01-10 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('42c324ab-1306-4521-8add-884ad53a85db', 'vrbo', '2026-01-11', 269.75, true, 2, '2026-01-11 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('2acc849f-9f41-4362-9092-d3ab38110118', 'vrbo', '2026-01-12', 170.09, true, 1, '2026-01-12 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('c99561e9-de23-433b-b20d-de0b826b22c9', 'vrbo', '2026-01-13', 216.18, false, 1, '2026-01-13 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('7253b5d5-6b46-4798-b8a2-e8473de22ef9', 'vrbo', '2026-01-14', 220.32, true, 1, '2026-01-14 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('5b1a3913-b826-4b67-8734-6c631adc4848', 'vrbo', '2026-01-15', 183.16, true, 1, '2026-01-15 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('2e0cae4e-6d5b-43e2-af4b-9189c531be51', 'vrbo', '2026-01-16', 277.42, true, 2, '2026-01-16 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('7be55507-0230-4924-9fe1-769213c01ca7', 'vrbo', '2026-01-17', 230.84, true, 2, '2026-01-17 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('ffc2ba5d-e559-431a-aa0d-55333a908ed6', 'vrbo', '2026-01-18', 238.68, true, 2, '2026-01-18 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('5dd9e151-6c22-4dbc-8899-030cf60a3369', 'vrbo', '2026-01-19', 225.99, true, 1, '2026-01-19 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('b5bac0e4-3cb9-4534-8bed-53e3b69261d9', 'vrbo', '2026-01-20', 219.21, true, 1, '2026-01-20 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('a8983c73-13eb-466d-8496-eb4637a5eaa1', 'vrbo', '2026-01-21', 172.70, true, 1, '2026-01-21 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('9958021f-52e5-484b-9272-650c35d5fd2d', 'vrbo', '2026-01-22', 190.05, false, 1, '2026-01-22 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('3cec201d-3af1-4e02-aa81-fbb523d9f5f7', 'vrbo', '2026-01-23', 265.87, false, 2, '2026-01-23 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('8a3aaae5-b21d-4c24-a02c-00d5e02de680', 'vrbo', '2026-01-24', 273.18, true, 2, '2026-01-24 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('439ba613-10a0-449b-8ad7-265af119c36c', 'vrbo', '2026-01-25', 234.99, true, 2, '2026-01-25 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('38e4b52d-d78f-4a48-a853-c4728c56db5d', 'vrbo', '2026-01-26', 188.70, true, 1, '2026-01-26 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('301e47cf-e6e0-4505-b676-aa2bbea7ad89', 'vrbo', '2026-01-27', 237.70, true, 1, '2026-01-27 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('553385b8-bad8-4e82-9c8c-42fe032d616a', 'vrbo', '2026-01-28', 157.71, true, 1, '2026-01-28 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('31bc7167-4eb8-424e-843c-9a1e75691a82', 'vrbo', '2026-01-29', 254.73, true, 1, '2026-01-29 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('e9b2979d-7bbd-4b29-9506-387f16d74f78', 'vrbo', '2026-01-30', 223.78, true, 2, '2026-01-30 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('28602304-b061-4d18-b4a6-b0029e0a2d81', 'vrbo', '2026-01-31', 223.12, true, 2, '2026-01-31 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('058e8f00-856f-4c05-9091-2b4806d3aec5', 'vrbo', '2026-02-01', 258.79, true, 2, '2026-02-01 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('bc9aadae-7289-4e9d-ba36-1ab3796ec3bd', 'vrbo', '2026-02-02', 233.09, true, 1, '2026-02-02 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('b0571daa-0de9-4743-b448-5d5918a3dc25', 'vrbo', '2026-02-03', 167.80, true, 1, '2026-02-03 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('a75d15cd-2fde-4bb1-9b7b-4ef6b676375b', 'vrbo', '2026-02-04', 198.99, false, 1, '2026-02-04 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('6c5f920d-2336-47b0-980e-7c9157863f96', 'vrbo', '2026-02-05', 191.26, true, 1, '2026-02-05 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('b3924b90-22c5-446b-a0ef-965606c4da0f', 'vrbo', '2026-02-06', 247.77, true, 2, '2026-02-06 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('bb398835-3b26-4b13-b6a2-33eecb287361', 'vrbo', '2026-02-07', 205.75, true, 2, '2026-02-07 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('1af1e5d2-4211-49b0-97d6-56fa07c15db8', 'vrbo', '2026-02-08', 240.70, false, 2, '2026-02-08 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('59214f4d-9669-447a-9ffb-d0116a7ad316', 'vrbo', '2026-02-09', 235.25, true, 1, '2026-02-09 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('7ce7434b-a583-4e61-af2e-31b8245e072e', 'vrbo', '2026-02-10', 223.08, true, 1, '2026-02-10 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('fb14010d-0dfd-40a3-8b16-75a6eed5d3f1', 'vrbo', '2026-02-11', 176.65, true, 1, '2026-02-11 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('7d15debf-bbfd-4a7f-8669-fa9fc8a40f47', 'vrbo', '2026-02-12', 222.24, true, 1, '2026-02-12 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('a18440f6-4829-499a-84ad-b9484a0c3944', 'vrbo', '2026-02-13', 263.91, true, 2, '2026-02-13 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('57b56e55-b59b-46d2-ad0f-f89acc34a471', 'vrbo', '2026-02-14', 259.97, true, 2, '2026-02-14 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('530614fd-39b5-412e-aa9d-a2c238777be7', 'vrbo', '2026-02-15', 193.89, true, 2, '2026-02-15 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('b1cfd580-a89b-4a65-b757-5f0b13e9ab33', 'vrbo', '2026-02-16', 209.09, true, 1, '2026-02-16 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('7c038c7b-7a19-4867-8b71-060c8ef64ce1', 'vrbo', '2026-02-17', 221.17, true, 1, '2026-02-17 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('8ba39ab5-1092-4f7a-a1fd-a53ed173d33c', 'vrbo', '2026-02-18', 227.03, true, 1, '2026-02-18 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('dcaedf18-31ec-4e96-b9e3-7c986fa50830', 'vrbo', '2026-02-19', 240.63, true, 1, '2026-02-19 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('fb6e116e-f0ee-46a4-8d02-6d3fd3614cc4', 'vrbo', '2026-02-20', 264.04, true, 2, '2026-02-20 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('bb71c3e6-4e9e-4a4c-a914-73f2ecddac30', 'vrbo', '2026-02-21', 261.36, true, 2, '2026-02-21 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('3cb03c40-56c5-4311-bcfe-1ccce1aa25d3', 'vrbo', '2026-02-22', 250.35, false, 2, '2026-02-22 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('0f1fe27e-36f4-4863-9ec4-ac057709dd2a', 'vrbo', '2026-02-23', 194.31, true, 1, '2026-02-23 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('7d2a24a1-1a61-4c41-8dd0-583d96ef0ca3', 'vrbo', '2026-02-24', 216.36, true, 1, '2026-02-24 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('b0ee14d6-f037-4640-b738-162c3524d42e', 'vrbo', '2026-02-25', 248.33, false, 1, '2026-02-25 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('471ed7cf-2a1f-46b5-8a08-ba5d5e18500d', 'vrbo', '2026-02-26', 241.62, false, 1, '2026-02-26 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('c91cc721-1b32-4a08-9ce1-4a32be45aff2', 'vrbo', '2026-02-27', 277.87, true, 2, '2026-02-27 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('2cddb799-b926-4222-aa65-e0c4a2d61931', 'vrbo', '2026-02-28', 229.45, false, 2, '2026-02-28 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('5fb76c31-0cb3-4cfd-9de4-bafd61b0b543', 'vrbo', '2026-03-01', 240.38, true, 2, '2026-03-01 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('62d3d7f8-3fb7-45fb-93d0-79054b924c4f', 'vrbo', '2026-03-02', 208.47, true, 1, '2026-03-02 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('8c8e1eb3-d5a7-4a6e-8f28-807fdd913359', 'vrbo', '2026-03-03', 203.70, true, 1, '2026-03-03 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('e34a0d7e-f39d-4fcc-9c48-a0b7d33c1faf', 'vrbo', '2026-03-04', 191.99, true, 1, '2026-03-04 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('f4067600-2701-4679-9267-c1527170c404', 'vrbo', '2026-03-05', 175.46, false, 1, '2026-03-05 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('3f14ee80-b3ac-4cd5-988e-9e53c5f2c121', 'vrbo', '2026-03-06', 247.94, true, 2, '2026-03-06 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('68cc38c2-aeae-4b0c-bd9a-60b4bb74636b', 'vrbo', '2026-03-07', 202.52, true, 2, '2026-03-07 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('89730edc-2bda-4b83-b725-ea45af253796', 'vrbo', '2026-03-08', 249.99, true, 2, '2026-03-08 05:00:00+00');
INSERT INTO public.daily_prices VALUES ('7e2138d5-7fd3-42ac-b4c3-9b55bd4b5fd5', 'vrbo', '2026-03-09', 183.16, true, 1, '2026-03-09 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('7d0fa092-9d46-44dc-a2b8-27821f5af1f8', 'vrbo', '2026-03-10', 208.75, false, 1, '2026-03-10 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('4253f00f-5e01-49aa-adab-a433bbea762d', 'vrbo', '2026-03-11', 195.87, true, 1, '2026-03-11 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('adc088ab-a6d2-4d75-befa-5b8030302c1d', 'vrbo', '2026-03-12', 217.33, true, 1, '2026-03-12 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('7be41ebf-c53a-4436-91dd-a6055a46f18f', 'vrbo', '2026-03-13', 326.82, true, 2, '2026-03-13 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('88bfd1b6-ea0f-4da5-9c47-d77694a2520c', 'vrbo', '2026-03-14', 209.27, true, 2, '2026-03-14 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('2da98a40-bc28-4389-8a53-27921d487136', 'vrbo', '2026-03-15', 345.83, false, 2, '2026-03-15 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('17b79dbf-1de4-424a-af0f-7d8b9734f0b7', 'vrbo', '2026-03-16', 255.31, false, 1, '2026-03-16 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('55346d5a-9cf8-45fc-828b-8326dc723382', 'vrbo', '2026-03-17', 215.49, true, 1, '2026-03-17 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('5d6b34a6-a72f-4a12-9c10-98809f0afc7d', 'vrbo', '2026-03-18', 215.74, true, 1, '2026-03-18 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('aaf63647-2cf8-4c27-b504-d83d1630d9aa', 'vrbo', '2026-03-19', 220.55, true, 1, '2026-03-19 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('155323e3-cb9f-476a-b1c7-e00055e9df36', 'vrbo', '2026-03-20', 280.97, true, 2, '2026-03-20 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('3dd75efd-68e5-47ef-b4aa-61d264f0ae50', 'vrbo', '2026-03-21', 264.34, true, 2, '2026-03-21 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('d2f9b63d-a335-42c6-8819-fe81fdb1f33a', 'vrbo', '2026-03-22', 323.49, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('384a80b1-0670-453d-8a76-fb1efef72bc3', 'vrbo', '2026-03-23', 221.25, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('c17633a0-7229-4812-8b55-2eae11a7f331', 'vrbo', '2026-03-24', 231.85, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('ee198138-ed30-441c-8b1a-76bc13a7ed7d', 'vrbo', '2026-03-25', 254.89, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('f5f14ea6-9441-4f31-a34b-00a79fe109a4', 'vrbo', '2026-03-26', 232.54, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('207f3bdd-cbe1-4003-a5cf-d7f0562a2267', 'vrbo', '2026-03-27', 279.08, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('e1cfa57f-cc81-448e-94ba-bf778e1d505a', 'vrbo', '2026-03-28', 347.93, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('2920f98f-c062-4b92-a649-dffc79423b9d', 'vrbo', '2026-03-29', 247.31, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('93d4e29a-8ba8-45d4-b3cf-bce8bd34ae14', 'vrbo', '2026-03-30', 239.21, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('742caaca-3ded-4ac5-94a5-49968841106c', 'vrbo', '2026-03-31', 185.94, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('c65408e6-14a9-4878-83ee-562870df2a8c', 'vrbo', '2026-04-01', 198.91, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('5a516832-fbbb-4ae2-8ce6-48b228045573', 'vrbo', '2026-04-02', 238.87, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('acf3bb0f-ad57-40e1-b9a7-951726639db0', 'vrbo', '2026-04-03', 240.52, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('be39fab2-dcc8-46bd-950d-5f718a1df1ba', 'vrbo', '2026-04-04', 240.56, false, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('c36fea22-bc12-471b-bc19-631ac5a4a53f', 'vrbo', '2026-04-05', 344.03, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('e83e9fb8-0eb7-4974-8410-1662577e033a', 'vrbo', '2026-04-06', 259.61, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('437d1fee-b933-4615-b95a-9ccbb460f939', 'vrbo', '2026-04-07', 212.35, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('f415db90-3679-4502-a05a-fde530215e27', 'vrbo', '2026-04-08', 185.00, false, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('b851aef4-1fce-4826-91f4-70314219de66', 'vrbo', '2026-04-09', 195.79, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('b2b4930c-ee19-4611-9d09-88d902bd16ae', 'vrbo', '2026-04-10', 215.78, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('ac7ee906-15ad-474c-98df-b41d4bd58f91', 'vrbo', '2026-04-11', 267.56, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('d76ef069-8d69-49dc-a648-890cbf8034b2', 'vrbo', '2026-04-12', 225.30, false, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('259a4b27-75fc-42f4-92c8-16b2ffe7da6d', 'vrbo', '2026-04-13', 160.55, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('4199fd01-cfb0-479f-a2e9-b524a9f2d00e', 'vrbo', '2026-04-14', 246.16, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('843d9fbb-d63c-409d-98ee-0807ed99587b', 'vrbo', '2026-04-15', 229.60, false, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('c1a93d3f-6353-4223-9fdf-d6c37445e143', 'vrbo', '2026-04-16', 213.72, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('d7cbc98c-d60b-40f8-8f9c-d33fb55c1e3b', 'vrbo', '2026-04-17', 227.68, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('0d0a467f-ecbd-440c-a21f-f1df0b71b3e3', 'vrbo', '2026-04-18', 320.55, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('25ec3766-1c97-4714-add3-8bb86dca116f', 'vrbo', '2026-04-19', 259.75, true, 2, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('502cebf6-7a75-451a-a1b5-d98dfaa27e6a', 'vrbo', '2026-04-20', 177.45, true, 1, '2026-03-22 04:00:00+00');
INSERT INTO public.daily_prices VALUES ('817c4d2e-3799-465d-9301-b696c1642cb8', 'vrbo', '2026-04-21', 214.50, true, 1, '2026-03-22 04:00:00+00');
--
-- Data for Name: experiments; Type: TABLE DATA; Schema: public; Owner: str_manager
--
INSERT INTO public.experiments VALUES ('93286d29-29ac-494a-945e-38a6da3511a9', 'Weekend Premium Pricing', 'Increasing weekend rates by 15% will maintain occupancy while boosting revenue.', '2026-01-21', '2026-02-20', 'completed', '2026-03-22 23:53:19.584947+00', 'Weekend premium of 15% maintained 92% of baseline bookings while increasing weekend revenue by 11%.');
INSERT INTO public.experiments VALUES ('495d07b2-2d8b-4a8e-b0f4-0c686ff54723', 'Minimum Stay Reduction', 'Reducing minimum stay from 3 nights to 2 nights on weekdays will fill gap nights and increase occupancy.', '2026-02-20', '2026-03-15', 'completed', '2026-03-22 23:53:19.584947+00', 'Occupancy rose 8% during the test period. Gap nights filled at an acceptable rate. Recommend keeping 2-night min on weekdays.');
INSERT INTO public.experiments VALUES ('84410aa4-4b71-43f2-8fe3-5df561d8cb88', 'Dynamic Last-Minute Discount', 'Offering a 10% discount for bookings within 3 days of check-in will reduce vacancy on otherwise-empty nights.', '2026-03-08', NULL, 'active', '2026-03-22 23:53:19.584947+00', NULL);
--
-- Data for Name: performance_snapshots; Type: TABLE DATA; Schema: public; Owner: str_manager
--
INSERT INTO public.performance_snapshots VALUES ('e37a3501-cb67-4180-909e-0195d5975349', 'airbnb', '2025-12-23 05:00:00+00', '2025-12-23', 1518, 246, 2.44, 6, 79.5, 166.61, 3974.32, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('1a8ae8cc-db15-4576-8b33-7e9c5c61c996', 'airbnb', '2025-12-24 05:00:00+00', '2025-12-24', 1898, 352, 0.57, 2, 59.7, 236.57, 4234.21, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('a81e12ff-4865-4472-829b-15d04fc0eeb6', 'airbnb', '2025-12-25 05:00:00+00', '2025-12-25', 917, 335, 1.79, 6, 62.8, 209.20, 3942.35, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('b0e8c48c-f428-40b9-9fd4-730843a39eb5', 'airbnb', '2025-12-26 05:00:00+00', '2025-12-26', 1199, 426, 1.41, 6, 79.3, 243.08, 5780.30, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('4d6df332-e34c-454c-b1b6-ad988cec2f57', 'airbnb', '2025-12-27 05:00:00+00', '2025-12-27', 1246, 121, 1.65, 2, 58.7, 223.01, 3925.95, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('c32976de-f31d-4ce7-8501-4afa7e75412e', 'airbnb', '2025-12-28 05:00:00+00', '2025-12-28', 1306, 299, 1.67, 5, 63.7, 150.93, 2885.49, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('6672972f-cd76-4b58-b6ae-6718d8d7492b', 'airbnb', '2025-12-29 05:00:00+00', '2025-12-29', 1595, 259, 1.16, 3, 68.0, 237.44, 4846.82, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('66e4d1d6-d914-40a6-bc04-0e7d82b7e4f8', 'airbnb', '2025-12-30 05:00:00+00', '2025-12-30', 1617, 114, 7.02, 8, 75.2, 236.43, 5333.25, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('ee8f88ea-cb0f-44be-92bd-f030fab19d57', 'airbnb', '2025-12-31 05:00:00+00', '2025-12-31', 937, 368, 2.72, 10, 84.5, 229.28, 5809.68, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('4ef0c205-cef1-43fd-b41d-afd95ff5ccdb', 'airbnb', '2026-01-01 05:00:00+00', '2026-01-01', 1876, 483, 0.62, 3, 80.7, 239.06, 5785.00, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('b6243d4d-74c1-49e7-a129-6b46d007f814', 'airbnb', '2026-01-02 05:00:00+00', '2026-01-02', 1050, 113, 7.08, 8, 83.5, 231.92, 5809.88, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('3bdbd4d1-d7c8-48a5-ae32-133bdd369cbe', 'airbnb', '2026-01-03 05:00:00+00', '2026-01-03', 742, 192, 1.56, 3, 60.8, 180.60, 3291.74, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('c4b9b5f3-2480-41fb-9adf-d75aeb020fdc', 'airbnb', '2026-01-04 05:00:00+00', '2026-01-04', 1988, 485, 0.62, 3, 77.7, 242.74, 5657.54, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('d6d72802-6a71-4a09-861d-0fe545a993dd', 'airbnb', '2026-01-05 05:00:00+00', '2026-01-05', 516, 257, 1.95, 5, 62.5, 203.90, 3824.34, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('05b72597-d958-4361-aabd-58347df90261', 'airbnb', '2026-01-06 05:00:00+00', '2026-01-06', 1206, 460, 0.87, 4, 57.9, 155.47, 2698.69, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('790fcd04-df70-40da-909b-d16bf103fa12', 'airbnb', '2026-01-07 05:00:00+00', '2026-01-07', 1484, 350, 1.71, 6, 58.6, 172.37, 3028.03, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('bfa1eec0-670d-499c-b772-56e5fce9162c', 'airbnb', '2026-01-08 05:00:00+00', '2026-01-08', 1431, 230, 0.87, 2, 72.7, 205.23, 4474.93, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('1d55ceb1-2a00-419e-afc6-16f470b6147b', 'airbnb', '2026-01-09 05:00:00+00', '2026-01-09', 1660, 415, 0.72, 3, 69.9, 154.28, 3234.88, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('eae7310f-0909-465c-9ed5-761c34dc3f1f', 'airbnb', '2026-01-10 05:00:00+00', '2026-01-10', 1805, 320, 0.94, 3, 58.4, 156.97, 2751.81, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('5c4df048-d216-4b64-9828-86dd2ee63ed2', 'airbnb', '2026-01-11 05:00:00+00', '2026-01-11', 771, 486, 1.03, 5, 81.7, 161.68, 3964.72, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('7f33a359-60ba-435d-b255-7aca1190aa7e', 'airbnb', '2026-01-12 05:00:00+00', '2026-01-12', 1091, 378, 1.32, 5, 85.9, 154.50, 3982.13, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('1d59967a-1bf2-4cf4-a50e-822f1e461dda', 'airbnb', '2026-01-13 05:00:00+00', '2026-01-13', 1615, 389, 0.77, 3, 76.1, 209.15, 4776.70, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('6ad3c51a-343d-4a5a-b084-345f7e1ea475', 'airbnb', '2026-01-14 05:00:00+00', '2026-01-14', 1057, 443, 0.45, 2, 70.3, 159.54, 3364.39, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('99b20238-4bc0-407d-abd7-745685b87e88', 'airbnb', '2026-01-15 05:00:00+00', '2026-01-15', 930, 147, 2.04, 3, 60.2, 181.54, 3279.85, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('42fbc79d-6d2b-40d5-8592-a34c04dd4e24', 'airbnb', '2026-01-16 05:00:00+00', '2026-01-16', 710, 395, 0.51, 2, 69.7, 234.48, 4904.01, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('30e91aef-b46c-4e2b-ae94-091911a8c834', 'airbnb', '2026-01-17 05:00:00+00', '2026-01-17', 1670, 313, 0.64, 2, 56.8, 200.58, 3420.65, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('17b7e10d-9a2d-48bf-a12c-48197a57927e', 'airbnb', '2026-01-18 05:00:00+00', '2026-01-18', 1349, 432, 0.93, 4, 67.1, 252.18, 5078.73, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('0341510a-c4df-412f-b5fc-b383a80cc6f7', 'airbnb', '2026-01-19 05:00:00+00', '2026-01-19', 1400, 332, 1.81, 6, 77.6, 216.25, 5032.96, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('d1c1c004-1aa6-4327-852c-3cfc41924500', 'airbnb', '2026-01-20 05:00:00+00', '2026-01-20', 638, 263, 2.28, 6, 64.4, 165.09, 3188.16, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('12524681-aa26-4996-9f32-efeed7ad400b', 'airbnb', '2026-01-21 05:00:00+00', '2026-01-21', 1571, 193, 2.59, 5, 59.7, 236.53, 4233.71, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('47e5c0cc-7576-4dde-af6e-bc91fda5234d', 'airbnb', '2026-01-22 05:00:00+00', '2026-01-22', 760, 461, 1.30, 6, 81.5, 228.83, 5594.14, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('ba70e2da-2830-4042-ab6e-b7c6c03207f4', 'airbnb', '2026-01-23 05:00:00+00', '2026-01-23', 734, 227, 3.08, 7, 68.6, 162.80, 3349.86, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('d84f8d4f-4bb1-4a9b-a505-0dc6da53e89c', 'airbnb', '2026-01-24 05:00:00+00', '2026-01-24', 1265, 304, 1.97, 6, 75.3, 210.28, 4751.82, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('215fd154-ae77-4462-a2f4-ba118528850c', 'airbnb', '2026-01-25 05:00:00+00', '2026-01-25', 1810, 428, 1.64, 7, 86.4, 173.80, 4504.13, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('fe318719-35d1-438c-a208-24acc46b4f3f', 'airbnb', '2026-01-26 05:00:00+00', '2026-01-26', 693, 139, 4.32, 6, 68.4, 161.19, 3307.75, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('c30c1d8a-d735-424c-b36f-42279781f5ed', 'airbnb', '2026-01-27 05:00:00+00', '2026-01-27', 1623, 389, 2.06, 8, 75.9, 252.18, 5744.26, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('8ad7670e-8127-4f8a-9fcb-aca382d038a7', 'airbnb', '2026-01-28 05:00:00+00', '2026-01-28', 1449, 400, 1.75, 7, 80.8, 172.83, 4187.71, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('3be9857f-6cce-4d2c-bd1e-164b25daf76f', 'airbnb', '2026-01-29 05:00:00+00', '2026-01-29', 682, 116, 2.59, 3, 87.8, 231.51, 6099.02, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('7ee3083c-e09d-4612-8065-ff130173febe', 'airbnb', '2026-01-30 05:00:00+00', '2026-01-30', 937, 485, 1.86, 9, 78.6, 213.06, 5022.19, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('0a863ff8-8ceb-4c95-be56-539d6a44f12a', 'airbnb', '2026-01-31 05:00:00+00', '2026-01-31', 878, 444, 0.45, 2, 58.5, 168.27, 2951.45, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('bc5aa091-53dc-4110-a4bc-a30455092df8', 'airbnb', '2026-02-01 05:00:00+00', '2026-02-01', 1093, 175, 3.43, 6, 84.4, 163.89, 4148.65, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('6e01338a-c154-4229-af93-73192418d20a', 'airbnb', '2026-02-02 05:00:00+00', '2026-02-02', 1949, 490, 0.61, 3, 57.5, 253.74, 4374.97, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('c1a7d7d1-c1cf-496e-b99e-9d568a8559af', 'airbnb', '2026-02-03 05:00:00+00', '2026-02-03', 1522, 211, 3.79, 8, 80.6, 166.11, 4014.05, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('9cdbea6c-4f94-460e-ab42-b4c88374c28f', 'airbnb', '2026-02-04 05:00:00+00', '2026-02-04', 1883, 366, 1.09, 4, 66.8, 220.12, 4410.23, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('0310a54a-33d8-4cf6-af1b-808b896fd6ca', 'airbnb', '2026-02-05 05:00:00+00', '2026-02-05', 1573, 178, 2.81, 5, 74.6, 222.36, 4975.96, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('101f620c-8120-4fb6-b3df-d9f4f9378ef9', 'airbnb', '2026-02-06 05:00:00+00', '2026-02-06', 1829, 449, 1.34, 6, 58.3, 188.73, 3298.89, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('5945576d-550c-498b-9768-ef6d6b9aae22', 'airbnb', '2026-02-07 05:00:00+00', '2026-02-07', 1870, 513, 0.78, 4, 66.8, 261.11, 5232.02, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('4c8a7d4c-c006-48ce-8d3c-1a349e1991a5', 'airbnb', '2026-02-08 05:00:00+00', '2026-02-08', 763, 145, 3.45, 5, 84.1, 245.51, 6192.04, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('176bf91f-1c8a-445a-93c9-ad392fffdacd', 'airbnb', '2026-02-09 05:00:00+00', '2026-02-09', 1158, 111, 1.80, 2, 74.2, 249.27, 5551.45, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('a7ad5c31-bf6e-47e4-b6e0-61fda94da673', 'airbnb', '2026-02-10 05:00:00+00', '2026-02-10', 756, 470, 0.85, 4, 64.4, 253.13, 4890.70, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e49963aa-02d4-4e2d-9175-5d083348b9d3', 'airbnb', '2026-02-11 05:00:00+00', '2026-02-11', 1118, 238, 2.10, 5, 63.1, 168.42, 3187.54, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('af15067c-ac52-4619-b18a-a1fc91a51dc9', 'airbnb', '2026-02-12 05:00:00+00', '2026-02-12', 734, 402, 1.99, 8, 76.6, 242.69, 5574.70, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('9a31af88-02ca-4fcd-a204-5aa486fc5e8a', 'airbnb', '2026-02-13 05:00:00+00', '2026-02-13', 1318, 497, 1.41, 7, 63.6, 177.57, 3387.33, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('bc77afe8-099c-48bf-8322-d2ab4da82f7b', 'airbnb', '2026-02-14 05:00:00+00', '2026-02-14', 1611, 276, 1.45, 4, 61.9, 162.40, 3016.93, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('be706c74-716d-4489-8a3d-2a82d1026ca6', 'airbnb', '2026-02-15 05:00:00+00', '2026-02-15', 1904, 473, 0.85, 4, 59.0, 259.26, 4587.10, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('92fbc6dd-d12a-4cc7-8d30-c1e6202d6bf9', 'airbnb', '2026-02-16 05:00:00+00', '2026-02-16', 1571, 183, 4.37, 8, 75.2, 221.82, 5005.35, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('9ce56cce-2725-4088-a821-032c92456f6a', 'airbnb', '2026-02-17 05:00:00+00', '2026-02-17', 1401, 448, 0.45, 2, 80.3, 158.97, 3829.20, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('06bb86d8-8e66-4cf8-bd93-652fe29009ad', 'airbnb', '2026-02-18 05:00:00+00', '2026-02-18', 700, 366, 2.19, 8, 87.6, 209.21, 5500.26, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('8114ad85-faaa-40da-982c-c4dfea296954', 'airbnb', '2026-02-19 05:00:00+00', '2026-02-19', 1670, 251, 2.39, 6, 81.4, 237.75, 5809.11, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('9b3b4190-2a1b-4f00-8688-ab22620268ae', 'airbnb', '2026-02-20 05:00:00+00', '2026-02-20', 1465, 390, 1.03, 4, 69.0, 205.86, 4262.47, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('66a32049-4e7d-4118-a801-a4aede4e7153', 'airbnb', '2026-02-21 05:00:00+00', '2026-02-21', 1695, 349, 1.72, 6, 64.9, 217.55, 4233.06, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('74ad1be3-da0b-4968-9220-4eb2b02fed24', 'airbnb', '2026-02-22 05:00:00+00', '2026-02-22', 1305, 287, 1.39, 4, 64.5, 215.95, 4181.63, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('4fd9f864-ef00-4129-a696-25b12295c815', 'airbnb', '2026-02-23 05:00:00+00', '2026-02-23', 1934, 117, 3.42, 4, 88.3, 233.47, 6183.64, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('a4b4f8f4-1e76-4967-a523-24a8d301bb47', 'airbnb', '2026-02-24 05:00:00+00', '2026-02-24', 748, 132, 3.79, 5, 59.7, 250.58, 4491.14, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('5d60b4ec-3913-4a6a-a5f7-a38dffd65898', 'airbnb', '2026-02-25 05:00:00+00', '2026-02-25', 1474, 164, 3.66, 6, 77.4, 163.65, 3802.19, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('0eda95f6-4af1-4099-bd3a-d97608719f57', 'airbnb', '2026-02-26 05:00:00+00', '2026-02-26', 1655, 449, 1.11, 5, 85.4, 163.38, 4186.97, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('800b7226-1620-4e45-b7bc-aba34c61544d', 'airbnb', '2026-02-27 05:00:00+00', '2026-02-27', 2089, 407, 1.97, 8, 75.8, 183.47, 4171.59, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('b1bacff8-db8c-4d5b-8b01-7193758cb9e4', 'airbnb', '2026-02-28 05:00:00+00', '2026-02-28', 1642, 485, 0.41, 2, 71.5, 231.69, 4968.84, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('956ad085-a4d5-4624-9010-0ff6cd6f1229', 'airbnb', '2026-03-01 05:00:00+00', '2026-03-01', 1171, 359, 1.11, 4, 88.4, 171.65, 4551.37, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('1a58bacd-7373-4e7f-9272-83b0a7725485', 'airbnb', '2026-03-02 05:00:00+00', '2026-03-02', 1003, 282, 1.77, 5, 62.3, 243.94, 4556.49, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('9258e227-5c7f-4d52-ad84-734bd7301645', 'airbnb', '2026-03-03 05:00:00+00', '2026-03-03', 1012, 506, 0.40, 2, 69.0, 198.90, 4118.42, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('8e68e39d-df5a-40d9-8c63-1ced9e3e53e0', 'airbnb', '2026-03-04 05:00:00+00', '2026-03-04', 1433, 362, 0.83, 3, 73.5, 223.02, 4916.08, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('4ad0ffb6-d603-49df-af55-a23a8c9fa789', 'airbnb', '2026-03-05 05:00:00+00', '2026-03-05', 1262, 310, 2.90, 9, 80.7, 191.86, 4643.63, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('c35e411c-e541-4c32-b4ef-2ae0afec1af6', 'airbnb', '2026-03-06 05:00:00+00', '2026-03-06', 991, 498, 1.00, 5, 81.4, 264.12, 6448.55, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('f0ed7608-46c1-4528-978a-b44890babc6a', 'airbnb', '2026-03-07 05:00:00+00', '2026-03-07', 936, 531, 1.69, 9, 89.2, 201.97, 5405.07, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e9f2024c-cd28-43ea-b104-08042946f3b1', 'airbnb', '2026-03-08 05:00:00+00', '2026-03-08', 1861, 148, 4.73, 7, 76.5, 259.05, 5947.76, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e9a02122-12ac-4df3-b797-60ccf4dd4dc7', 'airbnb', '2026-03-09 04:00:00+00', '2026-03-09', 600, 123, 5.69, 7, 71.2, 236.90, 5062.69, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('7943e7fc-23f8-460c-a5e6-c27ce023ef7a', 'airbnb', '2026-03-10 04:00:00+00', '2026-03-10', 1242, 166, 2.41, 4, 87.0, 261.11, 6818.25, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('58057a6e-b486-49fc-8697-ebe8e7a179c9', 'airbnb', '2026-03-11 04:00:00+00', '2026-03-11', 1911, 192, 4.69, 9, 91.4, 199.54, 5470.29, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('a8f3d24e-13ce-4a98-8b78-adeab0c46ba9', 'airbnb', '2026-03-12 04:00:00+00', '2026-03-12', 1031, 123, 4.07, 5, 85.3, 236.12, 6043.44, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('88602e18-9a8a-4537-a421-390e61ea9115', 'airbnb', '2026-03-13 04:00:00+00', '2026-03-13', 1212, 441, 0.91, 4, 83.8, 239.54, 6023.31, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('3bee59c3-c736-41e0-87ce-741741dd0a38', 'airbnb', '2026-03-14 04:00:00+00', '2026-03-14', 1425, 187, 4.81, 9, 81.4, 198.21, 4840.71, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('777c12b7-26d7-4a3a-8929-6e39fb70df95', 'airbnb', '2026-03-15 04:00:00+00', '2026-03-15', 819, 510, 0.98, 5, 67.5, 185.16, 3750.76, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e2e16394-4f61-45e1-8f9a-171b7f7506d2', 'airbnb', '2026-03-16 04:00:00+00', '2026-03-16', 1009, 505, 1.19, 6, 75.5, 230.12, 5210.89, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('27b242b3-453c-4d13-b407-fec2ce3153a2', 'airbnb', '2026-03-17 04:00:00+00', '2026-03-17', 970, 388, 2.58, 10, 85.2, 269.45, 6884.76, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('8fc650c7-4c44-48de-a9b5-315a32bab231', 'airbnb', '2026-03-18 04:00:00+00', '2026-03-18', 1069, 185, 3.24, 6, 83.7, 224.41, 5637.86, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('0fc9b766-5d62-4766-b515-215828efb8c7', 'airbnb', '2026-03-19 04:00:00+00', '2026-03-19', 778, 408, 1.23, 5, 61.1, 200.74, 3677.79, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('76a03869-33ee-436c-8d1c-cf066a50dafe', 'airbnb', '2026-03-20 04:00:00+00', '2026-03-20', 1467, 214, 3.27, 7, 84.0, 183.83, 4634.17, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('cc9a412a-11fd-42cb-8001-f6cc0fd86791', 'airbnb', '2026-03-21 04:00:00+00', '2026-03-21', 1108, 213, 0.94, 2, 77.9, 167.28, 3909.96, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('879d10be-a291-498f-843b-41537596c758', 'airbnb', '2026-03-22 04:00:00+00', '2026-03-22', 1891, 428, 0.93, 4, 92.1, 227.23, 6280.57, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('2eab0ccb-ac2c-42ed-9b9c-ab1b5808332a', 'vrbo', '2025-12-23 05:00:00+00', '2025-12-23', 899, 211, 0.95, 2, 69.6, 245.19, 5117.95, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('418d080b-cb71-4d3b-adae-e2b82effbc12', 'vrbo', '2025-12-24 05:00:00+00', '2025-12-24', 960, 185, 1.08, 2, 68.9, 165.88, 3429.06, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e490cee1-221e-4d76-a6ea-f432d02cc493', 'vrbo', '2025-12-25 05:00:00+00', '2025-12-25', 578, 131, 1.53, 2, 64.9, 178.34, 3473.27, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('b82c334e-b0cc-49ab-bbbe-ae21200ea1db', 'vrbo', '2025-12-26 05:00:00+00', '2025-12-26', 395, 162, 2.47, 4, 65.1, 207.39, 4047.70, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('6b0282de-1dca-4602-9981-6876c8585630', 'vrbo', '2025-12-27 05:00:00+00', '2025-12-27', 406, 260, 0.77, 2, 71.0, 164.17, 3495.30, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('8be231fc-b4da-4db0-8088-4f5ea27872a8', 'vrbo', '2025-12-28 05:00:00+00', '2025-12-28', 321, 83, 3.61, 3, 69.8, 243.31, 5096.73, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('0d3bf356-e0ea-48a5-9f21-9db258d2cf5f', 'vrbo', '2025-12-29 05:00:00+00', '2025-12-29', 901, 274, 1.09, 3, 74.7, 247.07, 5535.57, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('71268916-c463-4ee1-862b-683615e7d003', 'vrbo', '2025-12-30 05:00:00+00', '2025-12-30', 645, 65, 7.69, 5, 81.1, 244.48, 5946.70, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('0b015265-8699-481d-b7f1-a752468e1611', 'vrbo', '2025-12-31 05:00:00+00', '2025-12-31', 987, 137, 2.92, 4, 74.8, 201.67, 4523.69, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('fbfaa3de-e41f-435e-b9a4-1c1c9ac954bc', 'vrbo', '2026-01-01 05:00:00+00', '2026-01-01', 928, 299, 0.67, 2, 67.1, 183.77, 3698.46, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('b9241ec5-4fc6-42ad-863d-8d513b167161', 'vrbo', '2026-01-02 05:00:00+00', '2026-01-02', 491, 126, 4.76, 6, 84.0, 189.88, 4787.77, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('148db6fd-69d7-4481-9b82-459b13b5fd74', 'vrbo', '2026-01-03 05:00:00+00', '2026-01-03', 360, 179, 1.68, 3, 77.8, 152.05, 3549.25, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('a3f91809-12a8-402c-8ead-869bd74078c8', 'vrbo', '2026-01-04 05:00:00+00', '2026-01-04', 646, 70, 4.29, 3, 57.3, 224.43, 3859.14, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('6483a1b0-8026-457f-9c82-cd08ac672a15', 'vrbo', '2026-01-05 05:00:00+00', '2026-01-05', 723, 193, 2.07, 4, 61.5, 153.40, 2829.96, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('058000ba-1b2f-4f85-a2c5-b7174668d2e7', 'vrbo', '2026-01-06 05:00:00+00', '2026-01-06', 336, 230, 0.87, 2, 64.2, 244.82, 4713.70, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('a55c7e9b-c1c3-4b6f-abb1-a1f9ee04d559', 'vrbo', '2026-01-07 05:00:00+00', '2026-01-07', 1212, 212, 1.89, 4, 83.2, 247.17, 6171.46, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('f37d14f7-00bb-4192-9502-2076daed13ff', 'vrbo', '2026-01-08 05:00:00+00', '2026-01-08', 690, 87, 2.30, 2, 83.1, 171.72, 4282.99, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('0ea80884-d5ef-4f9c-8296-5109cbf38302', 'vrbo', '2026-01-09 05:00:00+00', '2026-01-09', 1100, 231, 0.43, 1, 58.8, 206.58, 3645.18, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('7c97b3bc-a834-42ac-ab19-6aad1207f99d', 'vrbo', '2026-01-10 05:00:00+00', '2026-01-10', 575, 99, 5.05, 5, 77.9, 216.59, 5063.39, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('ad4f392e-d672-4734-b465-6650836e78a2', 'vrbo', '2026-01-11 05:00:00+00', '2026-01-11', 854, 128, 3.91, 5, 79.5, 159.12, 3797.14, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('4cad4d6f-ddf9-4638-9296-57191e244d3d', 'vrbo', '2026-01-12 05:00:00+00', '2026-01-12', 919, 93, 3.23, 3, 81.1, 246.59, 5999.58, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('887b8c44-7656-4148-882e-a0e18235c7e9', 'vrbo', '2026-01-13 05:00:00+00', '2026-01-13', 521, 143, 1.40, 2, 64.0, 245.64, 4719.29, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('66734730-e21b-4a39-8c48-fc11f5733c2d', 'vrbo', '2026-01-14 05:00:00+00', '2026-01-14', 308, 231, 0.87, 2, 57.3, 235.59, 4049.28, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('5d020b76-bba2-4417-a062-d66927bba029', 'vrbo', '2026-01-15 05:00:00+00', '2026-01-15', 438, 227, 0.88, 2, 62.4, 154.57, 2894.43, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('a43531b6-930c-4fcd-959c-339cda8ba422', 'vrbo', '2026-01-16 05:00:00+00', '2026-01-16', 617, 240, 0.83, 2, 86.8, 199.36, 5190.21, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('d7fbfd22-17a0-46e6-b5d4-bcad304c52ab', 'vrbo', '2026-01-17 05:00:00+00', '2026-01-17', 462, 94, 6.38, 6, 80.5, 211.98, 5119.87, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('4380c274-efe6-457f-b8e8-1d62fa4e9f57', 'vrbo', '2026-01-18 05:00:00+00', '2026-01-18', 783, 70, 7.14, 5, 80.9, 173.30, 4205.35, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e04f9111-e83a-4a72-b271-bfbfe764f6cc', 'vrbo', '2026-01-19 05:00:00+00', '2026-01-19', 1147, 268, 1.49, 4, 71.9, 196.09, 4227.00, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('4bdc56cb-b316-412c-9700-944fcaef0a78', 'vrbo', '2026-01-20 05:00:00+00', '2026-01-20', 879, 295, 1.69, 5, 72.8, 223.12, 4875.12, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e9ae8b49-0f99-4142-af73-57ca502b3e77', 'vrbo', '2026-01-21 05:00:00+00', '2026-01-21', 1018, 81, 6.17, 5, 77.7, 234.58, 5467.93, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('d80310ea-f2d7-4698-acb2-323b69187828', 'vrbo', '2026-01-22 05:00:00+00', '2026-01-22', 708, 229, 1.75, 4, 71.6, 248.08, 5330.34, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('aea4a8fe-60f3-40e9-b1c5-9b235347f5b9', 'vrbo', '2026-01-23 05:00:00+00', '2026-01-23', 707, 310, 0.97, 3, 74.3, 169.60, 3780.74, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('a76d7247-71a4-4098-a2b3-9c1aa02610d1', 'vrbo', '2026-01-24 05:00:00+00', '2026-01-24', 443, 211, 1.90, 4, 58.3, 245.87, 4302.05, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('ccacce19-08e7-4ea0-aaca-ed464bb6f063', 'vrbo', '2026-01-25 05:00:00+00', '2026-01-25', 538, 300, 2.00, 6, 83.6, 155.78, 3909.06, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e59c7398-ccce-468b-99d4-1d546376d0da', 'vrbo', '2026-01-26 05:00:00+00', '2026-01-26', 655, 213, 1.41, 3, 72.1, 238.34, 5156.29, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('084e9d7d-f045-4607-ad31-a7641680153c', 'vrbo', '2026-01-27 05:00:00+00', '2026-01-27', 1216, 182, 1.10, 2, 61.4, 216.20, 3983.30, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('d7ec6692-675e-44fa-8bbc-fa3f276a90eb', 'vrbo', '2026-01-28 05:00:00+00', '2026-01-28', 809, 115, 2.61, 3, 74.4, 162.82, 3636.29, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('b4c5aa02-85f6-4a81-8a1f-9b0a43459c16', 'vrbo', '2026-01-29 05:00:00+00', '2026-01-29', 1165, 113, 1.77, 2, 58.4, 213.90, 3750.05, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('a88ec25e-3970-435c-9296-6733b12743e9', 'vrbo', '2026-01-30 05:00:00+00', '2026-01-30', 844, 270, 1.48, 4, 74.2, 163.31, 3633.34, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('201c6997-a977-42d6-b0ba-ad221a09e847', 'vrbo', '2026-01-31 05:00:00+00', '2026-01-31', 1022, 220, 0.45, 1, 69.1, 221.98, 4602.27, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('78cdccd2-a90f-42b4-a58a-087304e530dc', 'vrbo', '2026-02-01 05:00:00+00', '2026-02-01', 827, 154, 2.60, 4, 83.5, 224.45, 5621.61, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e648f1ca-6090-4fb6-aaea-36083077e2f9', 'vrbo', '2026-02-02 05:00:00+00', '2026-02-02', 1058, 64, 3.13, 2, 62.9, 234.01, 4418.34, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('91d0a3da-7f61-4e48-942b-463d11332262', 'vrbo', '2026-02-03 05:00:00+00', '2026-02-03', 321, 257, 1.17, 3, 65.2, 196.46, 3842.13, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('f99749b9-b30d-47f7-b565-b32dfba158ac', 'vrbo', '2026-02-04 05:00:00+00', '2026-02-04', 983, 301, 1.33, 4, 79.0, 226.00, 5356.24, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('ec5ba9af-d695-4a1b-9e90-a8cf0a359fc2', 'vrbo', '2026-02-05 05:00:00+00', '2026-02-05', 1108, 260, 0.38, 1, 61.1, 255.71, 4687.30, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('df6bf2d3-93dc-4898-8fb1-edab0677880b', 'vrbo', '2026-02-06 05:00:00+00', '2026-02-06', 570, 84, 4.76, 4, 80.3, 160.99, 3876.81, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('86df6648-c970-41b8-b1d4-abf4ebacd12b', 'vrbo', '2026-02-07 05:00:00+00', '2026-02-07', 1164, 153, 3.27, 5, 71.2, 210.76, 4500.38, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e678d779-8c0c-48fd-acb2-ebf76adf5daa', 'vrbo', '2026-02-08 05:00:00+00', '2026-02-08', 932, 148, 2.03, 3, 62.2, 247.10, 4610.42, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('b53bd705-ae32-48ca-9b2a-071cb5ec8b8e', 'vrbo', '2026-02-09 05:00:00+00', '2026-02-09', 393, 315, 0.95, 3, 61.9, 161.14, 2994.31, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('bedfe513-cd8f-4051-8469-8bd75cfa19aa', 'vrbo', '2026-02-10 05:00:00+00', '2026-02-10', 965, 260, 1.92, 5, 69.9, 172.03, 3606.85, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e6f8abd0-04f0-4e72-aea6-4d7c436f5f36', 'vrbo', '2026-02-11 05:00:00+00', '2026-02-11', 820, 104, 0.96, 1, 86.1, 173.43, 4481.87, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('282219de-5d1c-4ae5-bfa7-fdd81d02f196', 'vrbo', '2026-02-12 05:00:00+00', '2026-02-12', 383, 259, 2.32, 6, 89.0, 245.72, 6557.09, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('ed4c5c1c-c6d0-4f35-873e-8b0ab33db601', 'vrbo', '2026-02-13 05:00:00+00', '2026-02-13', 1231, 238, 0.42, 1, 72.0, 184.08, 3977.99, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('9860ab15-8f66-4ea6-b1dd-f70bdc4c646c', 'vrbo', '2026-02-14 05:00:00+00', '2026-02-14', 1067, 96, 2.08, 2, 62.9, 237.15, 4473.27, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('ece78d07-cf61-4e41-a05c-bfe9e1eae89c', 'vrbo', '2026-02-15 05:00:00+00', '2026-02-15', 763, 302, 0.66, 2, 76.1, 259.01, 5914.95, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('4b3e0a8e-a0dd-4edf-8c6e-c0ae6237d719', 'vrbo', '2026-02-16 05:00:00+00', '2026-02-16', 507, 264, 1.14, 3, 80.7, 184.56, 4469.54, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('309174ef-ab34-47af-8225-703cef4a7f53', 'vrbo', '2026-02-17 05:00:00+00', '2026-02-17', 1172, 188, 0.53, 1, 82.0, 202.97, 4993.90, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('06fe611d-1bbd-45ba-9369-777cf716629b', 'vrbo', '2026-02-18 05:00:00+00', '2026-02-18', 575, 65, 7.69, 5, 65.8, 208.11, 4105.87, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('f84df611-a5f3-4154-aa37-2b3c79bbc8b3', 'vrbo', '2026-02-19 05:00:00+00', '2026-02-19', 1111, 308, 0.65, 2, 78.0, 226.01, 5286.30, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('b10c891a-3c92-4169-9608-099fbeb74742', 'vrbo', '2026-02-20 05:00:00+00', '2026-02-20', 419, 135, 2.96, 4, 85.2, 241.66, 6174.63, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('a73a6843-cf8a-46e2-86d1-2f83cf7f7ccb', 'vrbo', '2026-02-21 05:00:00+00', '2026-02-21', 883, 266, 1.50, 4, 71.2, 160.81, 3435.47, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('f3bab766-faa4-4c50-b376-a009c668206c', 'vrbo', '2026-02-22 05:00:00+00', '2026-02-22', 386, 299, 0.33, 1, 66.1, 191.95, 3808.04, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('f6196c15-76dc-4801-9fb9-b962f0d42be8', 'vrbo', '2026-02-23 05:00:00+00', '2026-02-23', 361, 182, 1.10, 2, 88.8, 261.32, 6959.42, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('4b2fdd7e-bba2-4856-979a-928226775bae', 'vrbo', '2026-02-24 05:00:00+00', '2026-02-24', 328, 102, 0.98, 1, 77.4, 188.06, 4367.89, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('963412a6-1052-4eda-97b6-6cec819ab142', 'vrbo', '2026-02-25 05:00:00+00', '2026-02-25', 551, 231, 1.30, 3, 62.3, 245.65, 4592.96, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('5a519622-4084-4a67-be16-9496668dcac0', 'vrbo', '2026-02-26 05:00:00+00', '2026-02-26', 1261, 258, 1.55, 4, 65.1, 196.24, 3831.81, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('6fa16059-e48c-42ff-8352-3b72568b25af', 'vrbo', '2026-02-27 05:00:00+00', '2026-02-27', 826, 90, 3.33, 3, 60.7, 193.82, 3529.29, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e91a320b-468d-42c5-84cb-35cd3c04c029', 'vrbo', '2026-02-28 05:00:00+00', '2026-02-28', 465, 130, 2.31, 3, 78.0, 166.79, 3904.57, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('215e8ba5-a042-4644-9c61-a1acc070058d', 'vrbo', '2026-03-01 05:00:00+00', '2026-03-01', 546, 298, 1.01, 3, 85.6, 231.53, 5948.95, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('22616899-85b2-4859-b6fc-f3fc3b121fd1', 'vrbo', '2026-03-02 05:00:00+00', '2026-03-02', 690, 302, 0.33, 1, 63.6, 241.05, 4600.45, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('c7646ea6-b754-4df8-8c45-e93e509e1129', 'vrbo', '2026-03-03 05:00:00+00', '2026-03-03', 914, 200, 3.00, 6, 84.7, 260.06, 6609.56, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('86eb199d-017a-4d66-8804-85068cf7826d', 'vrbo', '2026-03-04 05:00:00+00', '2026-03-04', 379, 146, 2.74, 4, 83.2, 161.84, 4038.42, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('686b3e33-3e20-492e-b9f3-513a7cde023a', 'vrbo', '2026-03-05 05:00:00+00', '2026-03-05', 963, 79, 7.59, 6, 87.6, 260.98, 6857.73, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('69e6be4e-044e-4eb4-be3d-7f1cc551e392', 'vrbo', '2026-03-06 05:00:00+00', '2026-03-06', 1194, 131, 3.05, 4, 68.2, 166.62, 3408.27, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('5022357c-43dc-456d-a43b-8f5a86dc4729', 'vrbo', '2026-03-07 05:00:00+00', '2026-03-07', 360, 156, 3.21, 5, 74.9, 205.87, 4623.13, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('36106b04-2567-4a3b-a144-11e34712a0f0', 'vrbo', '2026-03-08 05:00:00+00', '2026-03-08', 1093, 81, 4.94, 4, 82.8, 202.59, 5033.81, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('9be17d0d-b3d1-4d7e-9c59-86ef9ac3b1ae', 'vrbo', '2026-03-09 04:00:00+00', '2026-03-09', 330, 170, 2.35, 4, 89.4, 233.68, 6269.48, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('6816aa8a-1541-408c-a8f7-75a811ea711d', 'vrbo', '2026-03-10 04:00:00+00', '2026-03-10', 1167, 281, 1.78, 5, 87.8, 206.57, 5438.26, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('9ccc5192-3c16-4666-8935-96e47c0c3308', 'vrbo', '2026-03-11 04:00:00+00', '2026-03-11', 1143, 294, 1.36, 4, 84.5, 167.39, 4241.21, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('1dd655f7-8b11-40bf-bb77-099e020f83f1', 'vrbo', '2026-03-12 04:00:00+00', '2026-03-12', 452, 231, 2.60, 6, 85.4, 177.01, 4534.07, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('4185234b-68c8-4764-8669-f6ab55a7ad6f', 'vrbo', '2026-03-13 04:00:00+00', '2026-03-13', 1274, 248, 0.81, 2, 63.8, 182.72, 3499.50, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('b9631b12-291c-4be5-b7c3-ca12c0781b2b', 'vrbo', '2026-03-14 04:00:00+00', '2026-03-14', 1263, 253, 1.58, 4, 70.0, 234.80, 4931.46, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('fefb99b9-aa03-485b-90cf-2b63d9de5d1c', 'vrbo', '2026-03-15 04:00:00+00', '2026-03-15', 552, 169, 2.37, 4, 86.4, 218.05, 5654.37, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('868c761c-f604-47dd-820d-6a69efb1bf41', 'vrbo', '2026-03-16 04:00:00+00', '2026-03-16', 595, 142, 1.41, 2, 62.1, 210.00, 3915.28, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('e27e93d6-764e-4e75-8358-62cae817ab03', 'vrbo', '2026-03-17 04:00:00+00', '2026-03-17', 1258, 262, 1.53, 4, 82.7, 262.06, 6502.75, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('96f05ce2-1948-4e34-b110-406cc3756de2', 'vrbo', '2026-03-18 04:00:00+00', '2026-03-18', 1139, 71, 2.82, 2, 72.3, 259.55, 5631.93, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('017227c3-99dc-4f62-a64b-f0f9e6a04f44', 'vrbo', '2026-03-19 04:00:00+00', '2026-03-19', 537, 87, 4.60, 4, 60.5, 230.69, 4187.59, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('fccbd533-f7d9-48e1-9b4b-aff8622a74a9', 'vrbo', '2026-03-20 04:00:00+00', '2026-03-20', 416, 240, 0.83, 2, 69.0, 248.46, 5139.61, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('f7e1c486-52c6-422e-b73c-82bfb0f84188', 'vrbo', '2026-03-21 04:00:00+00', '2026-03-21', 1300, 81, 3.70, 3, 76.4, 241.36, 5530.78, '{"source": "seed", "generated": true}');
INSERT INTO public.performance_snapshots VALUES ('22822337-fba4-4e5b-a670-e8c3035e5d02', 'vrbo', '2026-03-22 04:00:00+00', '2026-03-22', 388, 234, 1.71, 4, 74.6, 246.57, 5517.31, '{"source": "seed", "generated": true}');
--
-- Data for Name: price_changes; Type: TABLE DATA; Schema: public; Owner: str_manager
--
INSERT INTO public.price_changes VALUES ('9ea58dc0-9c2a-4a7b-b880-aeb408f2502e', 'airbnb', '2026-02-06', 195.69, 225.05, '2026-03-22 23:53:19.624483+00', 'experiment', 'Weekend premium pricing experiment', '93286d29-29ac-494a-945e-38a6da3511a9');
INSERT INTO public.price_changes VALUES ('f0ccc342-2bdd-48ee-a93c-6c1303604021', 'vrbo', '2026-01-28', 212.45, 244.32, '2026-03-22 23:53:19.624483+00', 'experiment', 'Weekend premium pricing experiment', '93286d29-29ac-494a-945e-38a6da3511a9');
INSERT INTO public.price_changes VALUES ('267a9989-eda2-4c6b-84ea-6c5091e11d37', 'airbnb', '2026-02-03', 199.81, 229.78, '2026-03-22 23:53:19.624483+00', 'experiment', 'Weekend premium pricing experiment', '93286d29-29ac-494a-945e-38a6da3511a9');
INSERT INTO public.price_changes VALUES ('728c0a86-f214-423e-bc1f-c386cc549578', 'vrbo', '2026-02-10', 183.61, 211.15, '2026-03-22 23:53:19.624483+00', 'experiment', 'Weekend premium pricing experiment', '93286d29-29ac-494a-945e-38a6da3511a9');
INSERT INTO public.price_changes VALUES ('01f0d793-2dd3-48f9-9ef9-7b34fec2584a', 'airbnb', '2026-02-19', 207.13, 238.20, '2026-03-22 23:53:19.624483+00', 'experiment', 'Weekend premium pricing experiment', '93286d29-29ac-494a-945e-38a6da3511a9');
INSERT INTO public.price_changes VALUES ('2f78b2cb-234c-4ada-926e-d9ff4864c9f0', 'vrbo', '2026-01-22', 192.77, 221.68, '2026-03-22 23:53:19.624483+00', 'experiment', 'Weekend premium pricing experiment', '93286d29-29ac-494a-945e-38a6da3511a9');
INSERT INTO public.price_changes VALUES ('34aa0f2f-2dac-4b0c-a74d-b84d77c60316', 'airbnb', '2026-01-22', 211.19, 242.87, '2026-03-22 23:53:19.624483+00', 'experiment', 'Weekend premium pricing experiment', '93286d29-29ac-494a-945e-38a6da3511a9');
INSERT INTO public.price_changes VALUES ('0908dac9-9bc9-4fd5-a398-908b1c53b5d6', 'vrbo', '2026-02-03', 192.50, 221.38, '2026-03-22 23:53:19.624483+00', 'experiment', 'Weekend premium pricing experiment', '93286d29-29ac-494a-945e-38a6da3511a9');
INSERT INTO public.price_changes VALUES ('1849553b-ec27-4e8a-87ea-7a83112ecf33', 'airbnb', '2026-03-10', 233.93, 252.46, '2026-03-22 23:53:19.624483+00', 'manual', 'Manual price adjustment', NULL);
INSERT INTO public.price_changes VALUES ('0ae102c4-2372-4646-a51d-92692d48c6c7', 'airbnb', '2026-03-17', 202.66, 186.09, '2026-03-22 23:53:19.624483+00', 'manual', 'Manual price adjustment', NULL);
INSERT INTO public.price_changes VALUES ('ecfe30ec-a7b0-4fcb-b019-a7e594ab0f05', 'airbnb', '2026-03-10', 219.61, 216.50, '2026-03-22 23:53:19.624483+00', 'manual', 'Manual price adjustment', NULL);
INSERT INTO public.price_changes VALUES ('5fb17220-1d5e-4d9a-aa2c-da1c62452efb', 'airbnb', '2026-02-28', 198.23, 182.94, '2026-03-22 23:53:19.624483+00', 'manual', 'Manual price adjustment', NULL);
--
-- Data for Name: reservations; Type: TABLE DATA; Schema: public; Owner: str_manager
--
INSERT INTO public.reservations VALUES ('51619f59-a28e-4200-bd71-53f1abfcd8bb', 'vrbo', 'VRBO-1001', 'Sarah Johnson', '2026-03-05', '2026-03-09', 4, 1, 182.12, 109, 24.56, 812.91, 'completed', '2026-02-15 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('04416b43-c1fd-4023-a98a-69320627d205', 'airbnb', 'AIRBNB-1002', 'Michael Chen', '2026-03-02', '2026-03-07', 5, 6, 256.74, 83, 61.11, 1305.59, 'completed', '2026-02-04 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('99fbfe16-fa3e-42cf-8dd3-4e5bf669a59d', 'airbnb', 'AIRBNB-1003', 'Emily Rodriguez', '2026-03-04', '2026-03-06', 2, 2, 191.40, 94, 13.50, 463.29, 'completed', '2026-02-24 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('1c35d6e5-ae54-4082-a0ef-65dd9f475a7f', 'vrbo', 'VRBO-1004', 'James Wilson', '2026-03-05', '2026-03-11', 6, 3, 255.52, 149, 50.52, 1631.57, 'completed', '2026-02-18 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('d8be4d57-62de-463b-a13e-88b737f95424', 'airbnb', 'AIRBNB-1005', 'Amanda Foster', '2026-01-07', '2026-01-10', 3, 4, 187.53, 91, 25.68, 627.90, 'completed', '2025-12-14 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('ffd9cabd-47df-48b2-ba6f-37ba94b59a98', 'airbnb', 'AIRBNB-1006', 'David Kim', '2026-01-05', '2026-01-08', 3, 3, 164.14, 114, 17.35, 589.06, 'completed', '2026-01-01 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('af9f48de-4568-4338-81f3-3671c6fd437e', 'vrbo', 'VRBO-1007', 'Jessica Taylor', '2026-03-13', '2026-03-20', 7, 5, 182.06, 110, 38.37, 1346.05, 'completed', '2026-02-16 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('36529cc5-1867-4ec7-b333-9b37b75eb8cc', 'airbnb', 'AIRBNB-1008', 'Robert Martinez', '2026-03-07', '2026-03-11', 4, 3, 193.30, 76, 33.25, 815.94, 'completed', '2026-02-25 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('ec1838fb-3503-4abe-af7f-0c3cf603dd0a', 'airbnb', 'AIRBNB-1009', 'Lisa Anderson', '2025-12-30', '2026-01-02', 3, 6, 279.86, 90, 31.28, 898.29, 'completed', '2025-12-09 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('d0555972-1045-4930-8857-0f12cdb52371', 'vrbo', 'VRBO-1010', 'Chris Thompson', '2026-01-04', '2026-01-11', 7, 3, 210.60, 128, 73.50, 1528.72, 'completed', '2025-12-20 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('a33e70f5-59c7-4288-9d65-e55b7b28b9eb', 'airbnb', 'AIRBNB-1011', 'Rachel Lee', '2026-03-01', '2026-03-03', 2, 6, 174.00, 89, 17.00, 419.99, 'completed', '2026-02-13 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('c92ea7c2-efc5-4cde-b076-0ac7aebf7568', 'airbnb', 'AIRBNB-1012', 'Daniel Brown', '2026-01-25', '2026-01-31', 6, 4, 237.65, 108, 53.90, 1479.98, 'completed', '2026-01-01 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('68a41a37-9932-48c4-a3ba-2b26f81f6b58', 'vrbo', 'VRBO-1013', 'Maria Garcia', '2026-03-12', '2026-03-19', 7, 1, 253.22, 84, 68.53, 1788.00, 'completed', '2026-02-26 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('2a9b6665-4a85-4460-bdcd-d05f13d5cce9', 'airbnb', 'AIRBNB-1014', 'Kevin Wright', '2026-01-02', '2026-01-07', 5, 4, 255.56, 84, 41.63, 1320.16, 'completed', '2025-12-24 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
INSERT INTO public.reservations VALUES ('b6e7d75f-9d2d-4700-994d-ff0943043c65', 'airbnb', 'AIRBNB-1015', 'Stephanie Davis', '2026-02-17', '2026-02-20', 3, 6, 254.88, 142, 36.75, 869.88, 'completed', '2026-01-26 05:00:00+00', '2026-03-22 23:53:19.622069+00', '{"source": "seed", "generated": true}');
--
-- Data for Name: scrape_jobs; Type: TABLE DATA; Schema: public; Owner: str_manager
--
INSERT INTO public.scrape_jobs VALUES ('5e87a505-7e50-4d2a-bda9-16f30f76ba65', 'airbnb', 'performance', 'manual', 'completed', '2026-03-16 04:00:00+00', '2026-03-16 04:01:14+00', NULL, 50);
INSERT INTO public.scrape_jobs VALUES ('08e3157b-5d6d-4009-9485-23dccbd7bc6a', 'airbnb', 'calendar', 'scheduled', 'completed', '2026-03-08 05:00:00+00', '2026-03-08 05:01:07+00', NULL, 194);
INSERT INTO public.scrape_jobs VALUES ('19977120-01b9-4ed4-8a30-b513ceba46d0', 'airbnb', 'performance', 'scheduled', 'completed', '2026-03-18 04:00:00+00', '2026-03-18 04:01:41+00', NULL, 135);
INSERT INTO public.scrape_jobs VALUES ('874266e0-d821-4953-848c-ad8ef1ab4532', 'airbnb', 'calendar', 'scheduled', 'completed', '2026-03-15 04:00:00+00', '2026-03-15 04:00:06+00', NULL, 105);
INSERT INTO public.scrape_jobs VALUES ('113ae8d3-364c-4f4c-a4d3-2f55912ed8c8', 'airbnb', 'performance', 'scheduled', 'completed', '2026-03-03 05:00:00+00', '2026-03-03 05:01:28+00', NULL, 169);
INSERT INTO public.scrape_jobs VALUES ('4981633e-a8e3-437b-b8ee-7c51d4227746', 'vrbo', 'performance', 'manual', 'completed', '2026-02-27 05:00:00+00', '2026-02-27 05:01:49+00', NULL, 155);
INSERT INTO public.scrape_jobs VALUES ('792564e6-7196-4408-892d-1e0e8e621acd', 'vrbo', 'calendar', 'scheduled', 'completed', '2026-02-28 05:00:00+00', '2026-02-28 05:01:26+00', NULL, 69);
INSERT INTO public.scrape_jobs VALUES ('453e6d24-4a92-48d9-ac40-ef5cfac1ef66', 'vrbo', 'performance', 'scheduled', 'completed', '2026-03-01 05:00:00+00', '2026-03-01 05:00:17+00', NULL, 13);
INSERT INTO public.scrape_jobs VALUES ('ed8f5c32-eae1-4ca0-ac08-68092185a055', 'vrbo', 'calendar', 'scheduled', 'completed', '2026-02-24 05:00:00+00', '2026-02-24 05:01:12+00', NULL, 200);
INSERT INTO public.scrape_jobs VALUES ('cae7b256-fad8-441b-b76c-2b0fae4a38c1', 'vrbo', 'performance', 'scheduled', 'completed', '2026-02-20 05:00:00+00', '2026-02-20 05:01:58+00', NULL, 37);
--
-- Name: __drizzle_migrations_id_seq; Type: SEQUENCE SET; Schema: drizzle; Owner: str_manager
--
SELECT pg_catalog.setval('drizzle.__drizzle_migrations_id_seq', 1, true);
--
-- PostgreSQL database dump complete
--
\unrestrict MeN1ws58DquMlfn6S1mGywDt5fuo6rhsE6g75LgmXhiU6DV4nSIsGAX8gVJ0f2n

14
apps/api/src/db/index.ts Normal file
View File

@@ -0,0 +1,14 @@
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: resolve(__dirname, '../../../../.env') });
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema.js';
const connectionString = process.env.DATABASE_URL!;
const sql = postgres(connectionString);
export const db = drizzle(sql, { schema });
export { sql };

View File

@@ -0,0 +1,96 @@
CREATE TABLE "daily_prices" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"platform_id" varchar NOT NULL,
"date" date NOT NULL,
"price" numeric NOT NULL,
"is_available" boolean DEFAULT true NOT NULL,
"min_stay_nights" integer DEFAULT 1 NOT NULL,
"synced_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "daily_prices_platform_date" UNIQUE("platform_id","date")
);
--> statement-breakpoint
CREATE TABLE "experiments" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar NOT NULL,
"hypothesis" text,
"start_date" date NOT NULL,
"end_date" date,
"status" varchar DEFAULT 'active' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"conclusion" text
);
--> statement-breakpoint
CREATE TABLE "performance_snapshots" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"platform_id" varchar NOT NULL,
"captured_at" timestamp with time zone DEFAULT now() NOT NULL,
"period_label" varchar NOT NULL,
"views_search" integer,
"views_listing" integer,
"conversion_rate" numeric,
"bookings_count" integer,
"occupancy_rate" numeric,
"avg_daily_rate" numeric,
"revenue_total" numeric,
"raw_json" jsonb
);
--> statement-breakpoint
CREATE TABLE "platforms" (
"id" varchar PRIMARY KEY NOT NULL,
"display_name" varchar NOT NULL,
"credentials_encrypted" text,
"session_data_encrypted" text,
"last_scrape_at" timestamp with time zone,
"is_active" boolean DEFAULT true NOT NULL
);
--> statement-breakpoint
CREATE TABLE "price_changes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"platform_id" varchar NOT NULL,
"date" date NOT NULL,
"price_before" numeric NOT NULL,
"price_after" numeric NOT NULL,
"changed_at" timestamp with time zone DEFAULT now() NOT NULL,
"changed_by" varchar,
"note" text,
"experiment_id" uuid
);
--> statement-breakpoint
CREATE TABLE "reservations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"platform_id" varchar NOT NULL,
"platform_reservation_id" varchar NOT NULL,
"guest_name" varchar NOT NULL,
"check_in" date NOT NULL,
"check_out" date NOT NULL,
"nights" integer NOT NULL,
"guests_count" integer,
"nightly_rate" numeric NOT NULL,
"cleaning_fee" numeric,
"platform_fee" numeric,
"total_payout" numeric NOT NULL,
"status" varchar DEFAULT 'confirmed' NOT NULL,
"booked_at" timestamp with time zone,
"synced_at" timestamp with time zone DEFAULT now() NOT NULL,
"raw_json" jsonb,
CONSTRAINT "reservations_platform_res_id" UNIQUE("platform_id","platform_reservation_id")
);
--> statement-breakpoint
CREATE TABLE "scrape_jobs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"platform_id" varchar NOT NULL,
"job_type" varchar NOT NULL,
"triggered_by" varchar NOT NULL,
"status" varchar DEFAULT 'pending' NOT NULL,
"started_at" timestamp with time zone,
"completed_at" timestamp with time zone,
"error_message" text,
"rows_collected" integer
);
--> statement-breakpoint
ALTER TABLE "daily_prices" ADD CONSTRAINT "daily_prices_platform_id_platforms_id_fk" FOREIGN KEY ("platform_id") REFERENCES "public"."platforms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "performance_snapshots" ADD CONSTRAINT "performance_snapshots_platform_id_platforms_id_fk" FOREIGN KEY ("platform_id") REFERENCES "public"."platforms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "price_changes" ADD CONSTRAINT "price_changes_platform_id_platforms_id_fk" FOREIGN KEY ("platform_id") REFERENCES "public"."platforms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "price_changes" ADD CONSTRAINT "price_changes_experiment_id_experiments_id_fk" FOREIGN KEY ("experiment_id") REFERENCES "public"."experiments"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "reservations" ADD CONSTRAINT "reservations_platform_id_platforms_id_fk" FOREIGN KEY ("platform_id") REFERENCES "public"."platforms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "scrape_jobs" ADD CONSTRAINT "scrape_jobs_platform_id_platforms_id_fk" FOREIGN KEY ("platform_id") REFERENCES "public"."platforms"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -0,0 +1,630 @@
{
"id": "1ae55d0b-2157-4fab-ac85-289ea14275fb",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.daily_prices": {
"name": "daily_prices",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"platform_id": {
"name": "platform_id",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"date": {
"name": "date",
"type": "date",
"primaryKey": false,
"notNull": true
},
"price": {
"name": "price",
"type": "numeric",
"primaryKey": false,
"notNull": true
},
"is_available": {
"name": "is_available",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"min_stay_nights": {
"name": "min_stay_nights",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"synced_at": {
"name": "synced_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"daily_prices_platform_id_platforms_id_fk": {
"name": "daily_prices_platform_id_platforms_id_fk",
"tableFrom": "daily_prices",
"tableTo": "platforms",
"columnsFrom": [
"platform_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"daily_prices_platform_date": {
"name": "daily_prices_platform_date",
"nullsNotDistinct": false,
"columns": [
"platform_id",
"date"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.experiments": {
"name": "experiments",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"hypothesis": {
"name": "hypothesis",
"type": "text",
"primaryKey": false,
"notNull": false
},
"start_date": {
"name": "start_date",
"type": "date",
"primaryKey": false,
"notNull": true
},
"end_date": {
"name": "end_date",
"type": "date",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'active'"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"conclusion": {
"name": "conclusion",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.performance_snapshots": {
"name": "performance_snapshots",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"platform_id": {
"name": "platform_id",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"captured_at": {
"name": "captured_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"period_label": {
"name": "period_label",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"views_search": {
"name": "views_search",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"views_listing": {
"name": "views_listing",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"conversion_rate": {
"name": "conversion_rate",
"type": "numeric",
"primaryKey": false,
"notNull": false
},
"bookings_count": {
"name": "bookings_count",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"occupancy_rate": {
"name": "occupancy_rate",
"type": "numeric",
"primaryKey": false,
"notNull": false
},
"avg_daily_rate": {
"name": "avg_daily_rate",
"type": "numeric",
"primaryKey": false,
"notNull": false
},
"revenue_total": {
"name": "revenue_total",
"type": "numeric",
"primaryKey": false,
"notNull": false
},
"raw_json": {
"name": "raw_json",
"type": "jsonb",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"performance_snapshots_platform_id_platforms_id_fk": {
"name": "performance_snapshots_platform_id_platforms_id_fk",
"tableFrom": "performance_snapshots",
"tableTo": "platforms",
"columnsFrom": [
"platform_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.platforms": {
"name": "platforms",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar",
"primaryKey": true,
"notNull": true
},
"display_name": {
"name": "display_name",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"credentials_encrypted": {
"name": "credentials_encrypted",
"type": "text",
"primaryKey": false,
"notNull": false
},
"session_data_encrypted": {
"name": "session_data_encrypted",
"type": "text",
"primaryKey": false,
"notNull": false
},
"last_scrape_at": {
"name": "last_scrape_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"is_active": {
"name": "is_active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.price_changes": {
"name": "price_changes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"platform_id": {
"name": "platform_id",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"date": {
"name": "date",
"type": "date",
"primaryKey": false,
"notNull": true
},
"price_before": {
"name": "price_before",
"type": "numeric",
"primaryKey": false,
"notNull": true
},
"price_after": {
"name": "price_after",
"type": "numeric",
"primaryKey": false,
"notNull": true
},
"changed_at": {
"name": "changed_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"changed_by": {
"name": "changed_by",
"type": "varchar",
"primaryKey": false,
"notNull": false
},
"note": {
"name": "note",
"type": "text",
"primaryKey": false,
"notNull": false
},
"experiment_id": {
"name": "experiment_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"price_changes_platform_id_platforms_id_fk": {
"name": "price_changes_platform_id_platforms_id_fk",
"tableFrom": "price_changes",
"tableTo": "platforms",
"columnsFrom": [
"platform_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"price_changes_experiment_id_experiments_id_fk": {
"name": "price_changes_experiment_id_experiments_id_fk",
"tableFrom": "price_changes",
"tableTo": "experiments",
"columnsFrom": [
"experiment_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.reservations": {
"name": "reservations",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"platform_id": {
"name": "platform_id",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"platform_reservation_id": {
"name": "platform_reservation_id",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"guest_name": {
"name": "guest_name",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"check_in": {
"name": "check_in",
"type": "date",
"primaryKey": false,
"notNull": true
},
"check_out": {
"name": "check_out",
"type": "date",
"primaryKey": false,
"notNull": true
},
"nights": {
"name": "nights",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"guests_count": {
"name": "guests_count",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"nightly_rate": {
"name": "nightly_rate",
"type": "numeric",
"primaryKey": false,
"notNull": true
},
"cleaning_fee": {
"name": "cleaning_fee",
"type": "numeric",
"primaryKey": false,
"notNull": false
},
"platform_fee": {
"name": "platform_fee",
"type": "numeric",
"primaryKey": false,
"notNull": false
},
"total_payout": {
"name": "total_payout",
"type": "numeric",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'confirmed'"
},
"booked_at": {
"name": "booked_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"synced_at": {
"name": "synced_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"raw_json": {
"name": "raw_json",
"type": "jsonb",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"reservations_platform_id_platforms_id_fk": {
"name": "reservations_platform_id_platforms_id_fk",
"tableFrom": "reservations",
"tableTo": "platforms",
"columnsFrom": [
"platform_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"reservations_platform_res_id": {
"name": "reservations_platform_res_id",
"nullsNotDistinct": false,
"columns": [
"platform_id",
"platform_reservation_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.scrape_jobs": {
"name": "scrape_jobs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"platform_id": {
"name": "platform_id",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"job_type": {
"name": "job_type",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"triggered_by": {
"name": "triggered_by",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"started_at": {
"name": "started_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"completed_at": {
"name": "completed_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"error_message": {
"name": "error_message",
"type": "text",
"primaryKey": false,
"notNull": false
},
"rows_collected": {
"name": "rows_collected",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"scrape_jobs_platform_id_platforms_id_fk": {
"name": "scrape_jobs_platform_id_platforms_id_fk",
"tableFrom": "scrape_jobs",
"tableTo": "platforms",
"columnsFrom": [
"platform_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1774223467634,
"tag": "0000_modern_screwball",
"breakpoints": true
}
]
}

195
apps/api/src/db/schema.ts Normal file
View File

@@ -0,0 +1,195 @@
import { relations } from 'drizzle-orm';
import {
boolean,
date,
integer,
jsonb,
numeric,
pgTable,
text,
timestamp,
unique,
uuid,
varchar,
} from 'drizzle-orm/pg-core';
const timestamptz = (name: string) => timestamp(name, { withTimezone: true, mode: 'string' });
// ── Platforms ────────────────────────────────────────────────────────────────
export const platforms = pgTable('platforms', {
id: varchar('id').primaryKey(),
displayName: varchar('display_name').notNull(),
credentialsEncrypted: text('credentials_encrypted'),
sessionDataEncrypted: text('session_data_encrypted'),
lastScrapeAt: timestamptz('last_scrape_at'),
isActive: boolean('is_active').default(true).notNull(),
});
export const platformsRelations = relations(platforms, ({ many }) => ({
performanceSnapshots: many(performanceSnapshots),
dailyPrices: many(dailyPrices),
priceChanges: many(priceChanges),
reservations: many(reservations),
scrapeJobs: many(scrapeJobs),
}));
// ── Performance Snapshots ───────────────────────────────────────────────────
export const performanceSnapshots = pgTable('performance_snapshots', {
id: uuid('id').defaultRandom().primaryKey(),
platformId: varchar('platform_id')
.notNull()
.references(() => platforms.id),
capturedAt: timestamptz('captured_at').defaultNow().notNull(),
periodLabel: varchar('period_label').notNull(),
viewsSearch: integer('views_search'),
viewsListing: integer('views_listing'),
conversionRate: numeric('conversion_rate'),
bookingsCount: integer('bookings_count'),
occupancyRate: numeric('occupancy_rate'),
avgDailyRate: numeric('avg_daily_rate'),
revenueTotal: numeric('revenue_total'),
rawJson: jsonb('raw_json'),
});
export const performanceSnapshotsRelations = relations(
performanceSnapshots,
({ one }) => ({
platform: one(platforms, {
fields: [performanceSnapshots.platformId],
references: [platforms.id],
}),
}),
);
// ── Daily Prices ────────────────────────────────────────────────────────────
export const dailyPrices = pgTable(
'daily_prices',
{
id: uuid('id').defaultRandom().primaryKey(),
platformId: varchar('platform_id')
.notNull()
.references(() => platforms.id),
date: date('date').notNull(),
price: numeric('price').notNull(),
isAvailable: boolean('is_available').default(true).notNull(),
minStayNights: integer('min_stay_nights').default(1).notNull(),
syncedAt: timestamptz('synced_at').defaultNow().notNull(),
},
(t) => [unique('daily_prices_platform_date').on(t.platformId, t.date)],
);
export const dailyPricesRelations = relations(dailyPrices, ({ one }) => ({
platform: one(platforms, {
fields: [dailyPrices.platformId],
references: [platforms.id],
}),
}));
// ── Experiments ─────────────────────────────────────────────────────────────
export const experiments = pgTable('experiments', {
id: uuid('id').defaultRandom().primaryKey(),
name: varchar('name').notNull(),
hypothesis: text('hypothesis'),
startDate: date('start_date').notNull(),
endDate: date('end_date'),
status: varchar('status').default('active').notNull(),
createdAt: timestamptz('created_at').defaultNow().notNull(),
conclusion: text('conclusion'),
});
export const experimentsRelations = relations(experiments, ({ many }) => ({
priceChanges: many(priceChanges),
}));
// ── Price Changes ───────────────────────────────────────────────────────────
export const priceChanges = pgTable('price_changes', {
id: uuid('id').defaultRandom().primaryKey(),
platformId: varchar('platform_id')
.notNull()
.references(() => platforms.id),
date: date('date').notNull(),
priceBefore: numeric('price_before').notNull(),
priceAfter: numeric('price_after').notNull(),
changedAt: timestamptz('changed_at').defaultNow().notNull(),
changedBy: varchar('changed_by'),
note: text('note'),
experimentId: uuid('experiment_id').references(() => experiments.id),
});
export const priceChangesRelations = relations(priceChanges, ({ one }) => ({
platform: one(platforms, {
fields: [priceChanges.platformId],
references: [platforms.id],
}),
experiment: one(experiments, {
fields: [priceChanges.experimentId],
references: [experiments.id],
}),
}));
// ── Reservations ────────────────────────────────────────────────────────────
export const reservations = pgTable(
'reservations',
{
id: uuid('id').defaultRandom().primaryKey(),
platformId: varchar('platform_id')
.notNull()
.references(() => platforms.id),
platformReservationId: varchar('platform_reservation_id').notNull(),
guestName: varchar('guest_name').notNull(),
checkIn: date('check_in').notNull(),
checkOut: date('check_out').notNull(),
nights: integer('nights').notNull(),
guestsCount: integer('guests_count'),
nightlyRate: numeric('nightly_rate').notNull(),
cleaningFee: numeric('cleaning_fee'),
platformFee: numeric('platform_fee'),
totalPayout: numeric('total_payout').notNull(),
status: varchar('status').default('confirmed').notNull(),
bookedAt: timestamptz('booked_at'),
syncedAt: timestamptz('synced_at').defaultNow().notNull(),
rawJson: jsonb('raw_json'),
},
(t) => [
unique('reservations_platform_res_id').on(
t.platformId,
t.platformReservationId,
),
],
);
export const reservationsRelations = relations(reservations, ({ one }) => ({
platform: one(platforms, {
fields: [reservations.platformId],
references: [platforms.id],
}),
}));
// ── Scrape Jobs ─────────────────────────────────────────────────────────────
export const scrapeJobs = pgTable('scrape_jobs', {
id: uuid('id').defaultRandom().primaryKey(),
platformId: varchar('platform_id')
.notNull()
.references(() => platforms.id),
jobType: varchar('job_type').notNull(),
triggeredBy: varchar('triggered_by').notNull(),
status: varchar('status').default('pending').notNull(),
startedAt: timestamptz('started_at'),
completedAt: timestamptz('completed_at'),
errorMessage: text('error_message'),
rowsCollected: integer('rows_collected'),
});
export const scrapeJobsRelations = relations(scrapeJobs, ({ one }) => ({
platform: one(platforms, {
fields: [scrapeJobs.platformId],
references: [platforms.id],
}),
}));

View File

@@ -0,0 +1,44 @@
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: resolve(__dirname, '../../../../.env') });
import { db, sql } from './index.js';
import {
platforms,
performanceSnapshots,
dailyPrices,
priceChanges,
experiments,
reservations,
scrapeJobs,
} from './schema.js';
async function seedClean() {
console.log('Clearing all existing data...');
await db.delete(priceChanges);
await db.delete(scrapeJobs);
await db.delete(reservations);
await db.delete(dailyPrices);
await db.delete(performanceSnapshots);
await db.delete(experiments);
await db.delete(platforms);
console.log('Inserting platform entries...');
await db.insert(platforms).values([
{ id: 'airbnb', displayName: 'Airbnb' },
{ id: 'vrbo', displayName: 'VRBO' },
]);
console.log('Clean seed complete. Database ready for real data.');
}
seedClean()
.then(() => process.exit(0))
.catch((err) => {
console.error('Clean seed failed:', err);
process.exit(1);
});

View File

@@ -0,0 +1,340 @@
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: resolve(__dirname, '../../../../.env') });
import { db, sql } from './index.js';
import {
platforms,
performanceSnapshots,
dailyPrices,
priceChanges,
experiments,
reservations,
scrapeJobs,
} from './schema.js';
// ── Helpers ─────────────────────────────────────────────────────────────────
function rand(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
function randInt(min: number, max: number): number {
return Math.floor(rand(min, max + 1));
}
function roundTo(n: number, decimals: number): string {
return n.toFixed(decimals);
}
function daysAgo(n: number): Date {
const d = new Date();
d.setDate(d.getDate() - n);
d.setHours(0, 0, 0, 0);
return d;
}
function formatDate(d: Date): string {
return d.toISOString().split('T')[0];
}
function isWeekend(d: Date): boolean {
const day = d.getDay();
return day === 0 || day === 5 || day === 6; // Fri, Sat, Sun
}
const GUEST_NAMES = [
'Sarah Johnson',
'Michael Chen',
'Emily Rodriguez',
'James Wilson',
'Amanda Foster',
'David Kim',
'Jessica Taylor',
'Robert Martinez',
'Lisa Anderson',
'Chris Thompson',
'Rachel Lee',
'Daniel Brown',
'Maria Garcia',
'Kevin Wright',
'Stephanie Davis',
'Andrew Moore',
'Nicole Clark',
'Brandon Hall',
];
// ── Seed Logic ──────────────────────────────────────────────────────────────
async function seed() {
console.log('Clearing existing data...');
// Delete in FK-safe order
await db.delete(priceChanges);
await db.delete(scrapeJobs);
await db.delete(reservations);
await db.delete(dailyPrices);
await db.delete(performanceSnapshots);
await db.delete(experiments);
await db.delete(platforms);
console.log('Inserting platforms...');
await db.insert(platforms).values([
{ id: 'airbnb', displayName: 'Airbnb' },
{ id: 'vrbo', displayName: 'VRBO' },
{ id: 'mock', displayName: 'Mock (Dev)' },
]);
// ── Experiments ───────────────────────────────────────────────────────────
console.log('Inserting experiments...');
const experimentRows = [
{
name: 'Weekend Premium Pricing',
hypothesis:
'Increasing weekend rates by 15% will maintain occupancy while boosting revenue.',
startDate: formatDate(daysAgo(60)),
endDate: formatDate(daysAgo(30)),
status: 'completed',
conclusion:
'Weekend premium of 15% maintained 92% of baseline bookings while increasing weekend revenue by 11%.',
},
{
name: 'Minimum Stay Reduction',
hypothesis:
'Reducing minimum stay from 3 nights to 2 nights on weekdays will fill gap nights and increase occupancy.',
startDate: formatDate(daysAgo(30)),
endDate: formatDate(daysAgo(7)),
status: 'completed',
conclusion:
'Occupancy rose 8% during the test period. Gap nights filled at an acceptable rate. Recommend keeping 2-night min on weekdays.',
},
{
name: 'Dynamic Last-Minute Discount',
hypothesis:
'Offering a 10% discount for bookings within 3 days of check-in will reduce vacancy on otherwise-empty nights.',
startDate: formatDate(daysAgo(14)),
status: 'active',
},
];
const insertedExperiments = await db
.insert(experiments)
.values(experimentRows)
.returning({ id: experiments.id });
// ── Performance Snapshots ─────────────────────────────────────────────────
console.log('Inserting performance snapshots...');
const snapshotRows: (typeof performanceSnapshots.$inferInsert)[] = [];
for (const platformId of ['airbnb', 'vrbo']) {
const platformMultiplier = platformId === 'airbnb' ? 1.0 : 0.6;
for (let daysBack = 89; daysBack >= 0; daysBack--) {
const d = daysAgo(daysBack);
// Slight upward trend: later days get a small boost
const trendFactor = 1 + (90 - daysBack) * 0.001;
const viewsSearch = Math.round(
randInt(500, 2000) * platformMultiplier * trendFactor,
);
const viewsListing = Math.round(
randInt(100, 500) * platformMultiplier * trendFactor,
);
const occupancy = Math.min(
rand(55, 85) * trendFactor,
98,
);
const adr = rand(150, 250) * trendFactor;
const bookingsCount = Math.round(
randInt(2, 8) * platformMultiplier * (occupancy / 70),
);
const conversionRate = (bookingsCount / viewsListing) * 100;
const revenue = adr * occupancy * 0.3; // rough approximation
snapshotRows.push({
platformId,
capturedAt: d.toISOString(),
periodLabel: formatDate(d),
viewsSearch,
viewsListing,
conversionRate: roundTo(conversionRate, 2),
bookingsCount,
occupancyRate: roundTo(occupancy, 1),
avgDailyRate: roundTo(adr, 2),
revenueTotal: roundTo(revenue, 2),
rawJson: { source: 'seed', generated: true },
});
}
}
// Insert in batches
for (let i = 0; i < snapshotRows.length; i += 50) {
await db
.insert(performanceSnapshots)
.values(snapshotRows.slice(i, i + 50));
}
// ── Daily Prices ──────────────────────────────────────────────────────────
console.log('Inserting daily prices...');
const priceRows: (typeof dailyPrices.$inferInsert)[] = [];
for (const platformId of ['airbnb', 'vrbo']) {
for (let daysBack = 89; daysBack >= -30; daysBack--) {
// Include 30 days into the future
const d = daysAgo(daysBack);
const trendFactor = 1 + (90 - daysBack) * 0.0005;
let basePrice = rand(150, 250) * trendFactor;
if (isWeekend(d)) {
basePrice *= rand(1.15, 1.35); // Weekend premium
}
priceRows.push({
platformId,
date: formatDate(d),
price: roundTo(basePrice, 2),
isAvailable: Math.random() > 0.15, // ~85% availability
minStayNights: isWeekend(d) ? 2 : 1,
syncedAt: daysAgo(Math.max(daysBack, 0)).toISOString(),
});
}
}
for (let i = 0; i < priceRows.length; i += 50) {
await db.insert(dailyPrices).values(priceRows.slice(i, i + 50));
}
// ── Reservations ──────────────────────────────────────────────────────────
console.log('Inserting reservations...');
const reservationRows: (typeof reservations.$inferInsert)[] = [];
let reservationCounter = 1000;
for (let i = 0; i < 15; i++) {
const platformId = i % 3 === 0 ? 'vrbo' : 'airbnb';
const checkInDaysAgo = randInt(5, 85);
const nights = randInt(2, 7);
const checkIn = daysAgo(checkInDaysAgo);
const checkOut = daysAgo(checkInDaysAgo - nights);
const nightlyRate = rand(160, 280);
const cleaningFee = randInt(75, 150);
const platformFee = nightlyRate * nights * rand(0.03, 0.05);
const totalPayout =
nightlyRate * nights + cleaningFee - platformFee;
const bookedDaysBeforeCheckIn = randInt(3, 30);
const bookedAt = daysAgo(checkInDaysAgo + bookedDaysBeforeCheckIn);
reservationCounter++;
const isPast = checkInDaysAgo - nights > 0;
reservationRows.push({
platformId,
platformReservationId: `${platformId.toUpperCase()}-${reservationCounter}`,
guestName: GUEST_NAMES[i % GUEST_NAMES.length],
checkIn: formatDate(checkIn),
checkOut: formatDate(checkOut),
nights,
guestsCount: randInt(1, 6),
nightlyRate: roundTo(nightlyRate, 2),
cleaningFee: cleaningFee.toString(),
platformFee: roundTo(platformFee, 2),
totalPayout: roundTo(totalPayout, 2),
status: isPast ? 'completed' : 'confirmed',
bookedAt: bookedAt.toISOString(),
rawJson: { source: 'seed', generated: true },
});
}
await db.insert(reservations).values(reservationRows);
// ── Price Changes ─────────────────────────────────────────────────────────
console.log('Inserting price changes...');
const priceChangeRows: (typeof priceChanges.$inferInsert)[] = [];
// Link some price changes to the first experiment (weekend premium)
for (let i = 0; i < 8; i++) {
const d = daysAgo(randInt(30, 60));
const before = rand(180, 230);
const after = before * 1.15;
priceChangeRows.push({
platformId: i % 2 === 0 ? 'airbnb' : 'vrbo',
date: formatDate(d),
priceBefore: roundTo(before, 2),
priceAfter: roundTo(after, 2),
changedBy: 'experiment',
note: 'Weekend premium pricing experiment',
experimentId: insertedExperiments[0].id,
});
}
// A few manual changes
for (let i = 0; i < 4; i++) {
const d = daysAgo(randInt(5, 25));
const before = rand(170, 250);
const after = before * rand(0.9, 1.1);
priceChangeRows.push({
platformId: 'airbnb',
date: formatDate(d),
priceBefore: roundTo(before, 2),
priceAfter: roundTo(after, 2),
changedBy: 'manual',
note: 'Manual price adjustment',
});
}
await db.insert(priceChanges).values(priceChangeRows);
// ── Scrape Jobs ───────────────────────────────────────────────────────────
console.log('Inserting scrape jobs...');
const scrapeRows: (typeof scrapeJobs.$inferInsert)[] = [];
for (const platformId of ['airbnb', 'vrbo']) {
for (let i = 0; i < 5; i++) {
const startedDaysAgo = randInt(0, 30);
const started = daysAgo(startedDaysAgo);
const completed = new Date(
started.getTime() + randInt(5, 120) * 1000,
);
scrapeRows.push({
platformId,
jobType: i % 2 === 0 ? 'performance' : 'calendar',
triggeredBy: i === 0 ? 'manual' : 'scheduled',
status: 'completed',
startedAt: started.toISOString(),
completedAt: completed.toISOString(),
rowsCollected: randInt(10, 200),
});
}
}
await db.insert(scrapeJobs).values(scrapeRows);
console.log('Seed complete.');
}
seed()
.then(() => {
process.exit(0);
})
.catch((err) => {
console.error('Seed failed:', err);
process.exit(1);
});

340
apps/api/src/db/seed.ts Normal file
View File

@@ -0,0 +1,340 @@
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: resolve(__dirname, '../../../../.env') });
import { db, sql } from './index.js';
import {
platforms,
performanceSnapshots,
dailyPrices,
priceChanges,
experiments,
reservations,
scrapeJobs,
} from './schema.js';
// ── Helpers ─────────────────────────────────────────────────────────────────
function rand(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
function randInt(min: number, max: number): number {
return Math.floor(rand(min, max + 1));
}
function roundTo(n: number, decimals: number): string {
return n.toFixed(decimals);
}
function daysAgo(n: number): Date {
const d = new Date();
d.setDate(d.getDate() - n);
d.setHours(0, 0, 0, 0);
return d;
}
function formatDate(d: Date): string {
return d.toISOString().split('T')[0];
}
function isWeekend(d: Date): boolean {
const day = d.getDay();
return day === 0 || day === 5 || day === 6; // Fri, Sat, Sun
}
const GUEST_NAMES = [
'Sarah Johnson',
'Michael Chen',
'Emily Rodriguez',
'James Wilson',
'Amanda Foster',
'David Kim',
'Jessica Taylor',
'Robert Martinez',
'Lisa Anderson',
'Chris Thompson',
'Rachel Lee',
'Daniel Brown',
'Maria Garcia',
'Kevin Wright',
'Stephanie Davis',
'Andrew Moore',
'Nicole Clark',
'Brandon Hall',
];
// ── Seed Logic ──────────────────────────────────────────────────────────────
async function seed() {
console.log('Clearing existing data...');
// Delete in FK-safe order
await db.delete(priceChanges);
await db.delete(scrapeJobs);
await db.delete(reservations);
await db.delete(dailyPrices);
await db.delete(performanceSnapshots);
await db.delete(experiments);
await db.delete(platforms);
console.log('Inserting platforms...');
await db.insert(platforms).values([
{ id: 'airbnb', displayName: 'Airbnb' },
{ id: 'vrbo', displayName: 'VRBO' },
{ id: 'mock', displayName: 'Mock (Dev)' },
]);
// ── Experiments ───────────────────────────────────────────────────────────
console.log('Inserting experiments...');
const experimentRows = [
{
name: 'Weekend Premium Pricing',
hypothesis:
'Increasing weekend rates by 15% will maintain occupancy while boosting revenue.',
startDate: formatDate(daysAgo(60)),
endDate: formatDate(daysAgo(30)),
status: 'completed',
conclusion:
'Weekend premium of 15% maintained 92% of baseline bookings while increasing weekend revenue by 11%.',
},
{
name: 'Minimum Stay Reduction',
hypothesis:
'Reducing minimum stay from 3 nights to 2 nights on weekdays will fill gap nights and increase occupancy.',
startDate: formatDate(daysAgo(30)),
endDate: formatDate(daysAgo(7)),
status: 'completed',
conclusion:
'Occupancy rose 8% during the test period. Gap nights filled at an acceptable rate. Recommend keeping 2-night min on weekdays.',
},
{
name: 'Dynamic Last-Minute Discount',
hypothesis:
'Offering a 10% discount for bookings within 3 days of check-in will reduce vacancy on otherwise-empty nights.',
startDate: formatDate(daysAgo(14)),
status: 'active',
},
];
const insertedExperiments = await db
.insert(experiments)
.values(experimentRows)
.returning({ id: experiments.id });
// ── Performance Snapshots ─────────────────────────────────────────────────
console.log('Inserting performance snapshots...');
const snapshotRows: (typeof performanceSnapshots.$inferInsert)[] = [];
for (const platformId of ['airbnb', 'vrbo']) {
const platformMultiplier = platformId === 'airbnb' ? 1.0 : 0.6;
for (let daysBack = 89; daysBack >= 0; daysBack--) {
const d = daysAgo(daysBack);
// Slight upward trend: later days get a small boost
const trendFactor = 1 + (90 - daysBack) * 0.001;
const viewsSearch = Math.round(
randInt(500, 2000) * platformMultiplier * trendFactor,
);
const viewsListing = Math.round(
randInt(100, 500) * platformMultiplier * trendFactor,
);
const occupancy = Math.min(
rand(55, 85) * trendFactor,
98,
);
const adr = rand(150, 250) * trendFactor;
const bookingsCount = Math.round(
randInt(2, 8) * platformMultiplier * (occupancy / 70),
);
const conversionRate = (bookingsCount / viewsListing) * 100;
const revenue = adr * occupancy * 0.3; // rough approximation
snapshotRows.push({
platformId,
capturedAt: d.toISOString(),
periodLabel: formatDate(d),
viewsSearch,
viewsListing,
conversionRate: roundTo(conversionRate, 2),
bookingsCount,
occupancyRate: roundTo(occupancy, 1),
avgDailyRate: roundTo(adr, 2),
revenueTotal: roundTo(revenue, 2),
rawJson: { source: 'seed', generated: true },
});
}
}
// Insert in batches
for (let i = 0; i < snapshotRows.length; i += 50) {
await db
.insert(performanceSnapshots)
.values(snapshotRows.slice(i, i + 50));
}
// ── Daily Prices ──────────────────────────────────────────────────────────
console.log('Inserting daily prices...');
const priceRows: (typeof dailyPrices.$inferInsert)[] = [];
for (const platformId of ['airbnb', 'vrbo']) {
for (let daysBack = 89; daysBack >= -30; daysBack--) {
// Include 30 days into the future
const d = daysAgo(daysBack);
const trendFactor = 1 + (90 - daysBack) * 0.0005;
let basePrice = rand(150, 250) * trendFactor;
if (isWeekend(d)) {
basePrice *= rand(1.15, 1.35); // Weekend premium
}
priceRows.push({
platformId,
date: formatDate(d),
price: roundTo(basePrice, 2),
isAvailable: Math.random() > 0.15, // ~85% availability
minStayNights: isWeekend(d) ? 2 : 1,
syncedAt: daysAgo(Math.max(daysBack, 0)).toISOString(),
});
}
}
for (let i = 0; i < priceRows.length; i += 50) {
await db.insert(dailyPrices).values(priceRows.slice(i, i + 50));
}
// ── Reservations ──────────────────────────────────────────────────────────
console.log('Inserting reservations...');
const reservationRows: (typeof reservations.$inferInsert)[] = [];
let reservationCounter = 1000;
for (let i = 0; i < 15; i++) {
const platformId = i % 3 === 0 ? 'vrbo' : 'airbnb';
const checkInDaysAgo = randInt(5, 85);
const nights = randInt(2, 7);
const checkIn = daysAgo(checkInDaysAgo);
const checkOut = daysAgo(checkInDaysAgo - nights);
const nightlyRate = rand(160, 280);
const cleaningFee = randInt(75, 150);
const platformFee = nightlyRate * nights * rand(0.03, 0.05);
const totalPayout =
nightlyRate * nights + cleaningFee - platformFee;
const bookedDaysBeforeCheckIn = randInt(3, 30);
const bookedAt = daysAgo(checkInDaysAgo + bookedDaysBeforeCheckIn);
reservationCounter++;
const isPast = checkInDaysAgo - nights > 0;
reservationRows.push({
platformId,
platformReservationId: `${platformId.toUpperCase()}-${reservationCounter}`,
guestName: GUEST_NAMES[i % GUEST_NAMES.length],
checkIn: formatDate(checkIn),
checkOut: formatDate(checkOut),
nights,
guestsCount: randInt(1, 6),
nightlyRate: roundTo(nightlyRate, 2),
cleaningFee: cleaningFee.toString(),
platformFee: roundTo(platformFee, 2),
totalPayout: roundTo(totalPayout, 2),
status: isPast ? 'completed' : 'confirmed',
bookedAt: bookedAt.toISOString(),
rawJson: { source: 'seed', generated: true },
});
}
await db.insert(reservations).values(reservationRows);
// ── Price Changes ─────────────────────────────────────────────────────────
console.log('Inserting price changes...');
const priceChangeRows: (typeof priceChanges.$inferInsert)[] = [];
// Link some price changes to the first experiment (weekend premium)
for (let i = 0; i < 8; i++) {
const d = daysAgo(randInt(30, 60));
const before = rand(180, 230);
const after = before * 1.15;
priceChangeRows.push({
platformId: i % 2 === 0 ? 'airbnb' : 'vrbo',
date: formatDate(d),
priceBefore: roundTo(before, 2),
priceAfter: roundTo(after, 2),
changedBy: 'experiment',
note: 'Weekend premium pricing experiment',
experimentId: insertedExperiments[0].id,
});
}
// A few manual changes
for (let i = 0; i < 4; i++) {
const d = daysAgo(randInt(5, 25));
const before = rand(170, 250);
const after = before * rand(0.9, 1.1);
priceChangeRows.push({
platformId: 'airbnb',
date: formatDate(d),
priceBefore: roundTo(before, 2),
priceAfter: roundTo(after, 2),
changedBy: 'manual',
note: 'Manual price adjustment',
});
}
await db.insert(priceChanges).values(priceChangeRows);
// ── Scrape Jobs ───────────────────────────────────────────────────────────
console.log('Inserting scrape jobs...');
const scrapeRows: (typeof scrapeJobs.$inferInsert)[] = [];
for (const platformId of ['airbnb', 'vrbo']) {
for (let i = 0; i < 5; i++) {
const startedDaysAgo = randInt(0, 30);
const started = daysAgo(startedDaysAgo);
const completed = new Date(
started.getTime() + randInt(5, 120) * 1000,
);
scrapeRows.push({
platformId,
jobType: i % 2 === 0 ? 'performance' : 'calendar',
triggeredBy: i === 0 ? 'manual' : 'scheduled',
status: 'completed',
startedAt: started.toISOString(),
completedAt: completed.toISOString(),
rowsCollected: randInt(10, 200),
});
}
}
await db.insert(scrapeJobs).values(scrapeRows);
console.log('Seed complete.');
}
seed()
.then(() => {
process.exit(0);
})
.catch((err) => {
console.error('Seed failed:', err);
process.exit(1);
});

View File

@@ -0,0 +1,194 @@
import nodemailer from 'nodemailer';
import { sql } from 'drizzle-orm';
import { db } from '../db/index.js';
import { performanceSnapshots, reservations, platforms } from '../db/schema.js';
import { config } from '../config.js';
export interface WeeklyReportData {
generatedAt: string;
thisWeek: WeekMetrics;
lastWeek: WeekMetrics;
platformBreakdown: PlatformMetrics[];
}
interface WeekMetrics {
from: string;
to: string;
totalRevenue: string;
reservationCount: number;
avgDailyRate: string;
avgOccupancy: string;
}
interface PlatformMetrics {
platformId: string;
displayName: string;
revenue: string;
reservations: number;
avgDailyRate: string;
}
export async function buildWeeklyReport(): Promise<WeeklyReportData> {
const now = new Date();
const thisWeekEnd = new Date(now);
thisWeekEnd.setHours(23, 59, 59, 999);
const thisWeekStart = new Date(now);
thisWeekStart.setDate(thisWeekStart.getDate() - 7);
thisWeekStart.setHours(0, 0, 0, 0);
const lastWeekEnd = new Date(thisWeekStart);
lastWeekEnd.setMilliseconds(-1);
const lastWeekStart = new Date(lastWeekEnd);
lastWeekStart.setDate(lastWeekStart.getDate() - 7);
lastWeekStart.setHours(0, 0, 0, 0);
const [thisWeekMetrics] = await db
.select({
totalRevenue: sql<string>`coalesce(sum(${reservations.totalPayout}::numeric), 0)`.as('total_revenue'),
reservationCount: sql<number>`count(*)::int`.as('reservation_count'),
avgDailyRate: sql<string>`coalesce(avg(${reservations.nightlyRate}::numeric), 0)`.as('avg_daily_rate'),
})
.from(reservations)
.where(
sql`${reservations.checkIn}::date >= ${thisWeekStart.toISOString().split('T')[0]}
AND ${reservations.checkIn}::date <= ${thisWeekEnd.toISOString().split('T')[0]}
AND ${reservations.status} = 'confirmed'`,
);
const [lastWeekMetrics] = await db
.select({
totalRevenue: sql<string>`coalesce(sum(${reservations.totalPayout}::numeric), 0)`.as('total_revenue'),
reservationCount: sql<number>`count(*)::int`.as('reservation_count'),
avgDailyRate: sql<string>`coalesce(avg(${reservations.nightlyRate}::numeric), 0)`.as('avg_daily_rate'),
})
.from(reservations)
.where(
sql`${reservations.checkIn}::date >= ${lastWeekStart.toISOString().split('T')[0]}
AND ${reservations.checkIn}::date <= ${lastWeekEnd.toISOString().split('T')[0]}
AND ${reservations.status} = 'confirmed'`,
);
// Get avg occupancy from performance snapshots
const [thisWeekOccupancy] = await db
.select({
avgOccupancy: sql<string>`coalesce(avg(${performanceSnapshots.occupancyRate}::numeric), 0)`.as('avg_occupancy'),
})
.from(performanceSnapshots)
.where(
sql`${performanceSnapshots.capturedAt} >= ${thisWeekStart.toISOString()}
AND ${performanceSnapshots.capturedAt} <= ${thisWeekEnd.toISOString()}`,
);
const [lastWeekOccupancy] = await db
.select({
avgOccupancy: sql<string>`coalesce(avg(${performanceSnapshots.occupancyRate}::numeric), 0)`.as('avg_occupancy'),
})
.from(performanceSnapshots)
.where(
sql`${performanceSnapshots.capturedAt} >= ${lastWeekStart.toISOString()}
AND ${performanceSnapshots.capturedAt} <= ${lastWeekEnd.toISOString()}`,
);
// Platform breakdown for this week
const platformBreakdown = await db
.select({
platformId: reservations.platformId,
displayName: platforms.displayName,
revenue: sql<string>`coalesce(sum(${reservations.totalPayout}::numeric), 0)`.as('revenue'),
reservationCount: sql<number>`count(*)::int`.as('reservations'),
avgDailyRate: sql<string>`coalesce(avg(${reservations.nightlyRate}::numeric), 0)`.as('avg_daily_rate'),
})
.from(reservations)
.innerJoin(platforms, sql`${reservations.platformId} = ${platforms.id}`)
.where(
sql`${reservations.checkIn}::date >= ${thisWeekStart.toISOString().split('T')[0]}
AND ${reservations.checkIn}::date <= ${thisWeekEnd.toISOString().split('T')[0]}
AND ${reservations.status} = 'confirmed'`,
)
.groupBy(reservations.platformId, platforms.displayName);
const fmt = (d: Date) => d.toISOString().split('T')[0]!;
return {
generatedAt: now.toISOString(),
thisWeek: {
from: fmt(thisWeekStart),
to: fmt(thisWeekEnd),
totalRevenue: thisWeekMetrics?.totalRevenue ?? '0',
reservationCount: thisWeekMetrics?.reservationCount ?? 0,
avgDailyRate: thisWeekMetrics?.avgDailyRate ?? '0',
avgOccupancy: thisWeekOccupancy?.avgOccupancy ?? '0',
},
lastWeek: {
from: fmt(lastWeekStart),
to: fmt(lastWeekEnd),
totalRevenue: lastWeekMetrics?.totalRevenue ?? '0',
reservationCount: lastWeekMetrics?.reservationCount ?? 0,
avgDailyRate: lastWeekMetrics?.avgDailyRate ?? '0',
avgOccupancy: lastWeekOccupancy?.avgOccupancy ?? '0',
},
platformBreakdown: platformBreakdown.map((p) => ({
platformId: p.platformId,
displayName: p.displayName,
revenue: p.revenue,
reservations: p.reservationCount,
avgDailyRate: p.avgDailyRate,
})),
};
}
export async function sendWeeklyReport(report: WeeklyReportData): Promise<void> {
if (!config.SMTP_HOST || !config.REPORT_EMAIL_TO) {
console.log('[email] SMTP not configured, skipping send. Report data:', JSON.stringify(report, null, 2));
return;
}
const transporter = nodemailer.createTransport({
host: config.SMTP_HOST,
port: config.SMTP_PORT,
secure: config.SMTP_PORT === 465,
auth:
config.SMTP_USER && config.SMTP_PASS
? { user: config.SMTP_USER, pass: config.SMTP_PASS }
: undefined,
});
const revenueChange =
parseFloat(report.lastWeek.totalRevenue) > 0
? (
((parseFloat(report.thisWeek.totalRevenue) - parseFloat(report.lastWeek.totalRevenue)) /
parseFloat(report.lastWeek.totalRevenue)) *
100
).toFixed(1)
: 'N/A';
const subject = `STR Weekly Report: $${parseFloat(report.thisWeek.totalRevenue).toFixed(0)} revenue (${revenueChange}% WoW)`;
const body = `
STR Optimization Manager - Weekly Report
Generated: ${report.generatedAt}
=== This Week (${report.thisWeek.from} to ${report.thisWeek.to}) ===
Revenue: $${parseFloat(report.thisWeek.totalRevenue).toFixed(2)}
Reservations: ${report.thisWeek.reservationCount}
Avg Daily Rate: $${parseFloat(report.thisWeek.avgDailyRate).toFixed(2)}
Avg Occupancy: ${(parseFloat(report.thisWeek.avgOccupancy) * 100).toFixed(1)}%
=== Last Week (${report.lastWeek.from} to ${report.lastWeek.to}) ===
Revenue: $${parseFloat(report.lastWeek.totalRevenue).toFixed(2)}
Reservations: ${report.lastWeek.reservationCount}
Avg Daily Rate: $${parseFloat(report.lastWeek.avgDailyRate).toFixed(2)}
Avg Occupancy: ${(parseFloat(report.lastWeek.avgOccupancy) * 100).toFixed(1)}%
=== Platform Breakdown (This Week) ===
${report.platformBreakdown.map((p) => `${p.displayName}: $${parseFloat(p.revenue).toFixed(2)} (${p.reservations} reservations)`).join('\n')}
`.trim();
await transporter.sendMail({
from: config.SMTP_USER ?? 'noreply@str-manager.local',
to: config.REPORT_EMAIL_TO,
subject,
text: body,
});
}

54
apps/api/src/index.ts Normal file
View File

@@ -0,0 +1,54 @@
import './config.js'; // loads dotenv first
import Fastify from 'fastify';
import cors from '@fastify/cors';
import cookie from '@fastify/cookie';
import authPlugin from './plugins/auth.js';
import healthRoutes from './routes/health.js';
import authRoutes from './routes/auth.js';
import platformRoutes from './routes/platforms.js';
import performanceRoutes from './routes/performance.js';
import pricingRoutes from './routes/pricing.js';
import experimentRoutes from './routes/experiments.js';
import reservationRoutes from './routes/reservations.js';
import reportRoutes from './routes/reports.js';
import { initCronJobs } from './scheduler/cron.js';
declare module 'fastify' {
interface FastifyInstance {
authenticate: (request: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply) => Promise<void>;
}
}
const fastify = Fastify({ logger: true });
async function start() {
// Plugins
await fastify.register(cors, {
origin: 'http://localhost:5173',
credentials: true,
});
await fastify.register(cookie);
await fastify.register(authPlugin);
// Routes - health at root, everything else under /api/v1
await fastify.register(healthRoutes);
await fastify.register(authRoutes, { prefix: '/api/v1' });
await fastify.register(platformRoutes, { prefix: '/api/v1' });
await fastify.register(performanceRoutes, { prefix: '/api/v1' });
await fastify.register(pricingRoutes, { prefix: '/api/v1' });
await fastify.register(experimentRoutes, { prefix: '/api/v1' });
await fastify.register(reservationRoutes, { prefix: '/api/v1' });
await fastify.register(reportRoutes, { prefix: '/api/v1' });
// Cron
initCronJobs();
// Start
await fastify.listen({ port: 3000, host: '0.0.0.0' });
console.log('API server running on http://localhost:3000');
}
start().catch((err) => {
fastify.log.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,41 @@
import fp from 'fastify-plugin';
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { config } from '../config.js';
declare module 'fastify' {
interface FastifyRequest {
user: { username: string };
}
}
declare module '@fastify/jwt' {
interface FastifyJWT {
payload: { username: string };
user: { username: string };
}
}
export default fp(async function authPlugin(fastify: FastifyInstance) {
fastify.register(import('@fastify/jwt'), {
secret: config.JWT_SECRET,
cookie: {
cookieName: 'token',
signed: false,
},
});
fastify.decorate(
'authenticate',
async function (request: FastifyRequest, reply: FastifyReply) {
try {
await request.jwtVerify();
} catch {
reply.status(401).send({ error: 'Unauthorized' });
}
},
);
});
export function signToken(fastify: FastifyInstance, payload: { username: string }): string {
return fastify.jwt.sign(payload, { expiresIn: '24h' });
}

View File

@@ -0,0 +1,47 @@
import { FastifyInstance } from 'fastify';
import bcrypt from 'bcrypt';
import { config } from '../config.js';
import { signToken } from '../plugins/auth.js';
export default async function (fastify: FastifyInstance) {
fastify.post<{
Body: { username: string; password: string };
}>('/auth/login', async (request, reply) => {
const { username, password } = request.body;
if (username !== config.APP_USERNAME) {
return reply.status(401).send({ error: 'Invalid credentials' });
}
const valid = await bcrypt.compare(password, config.APP_PASSWORD_HASH);
if (!valid) {
return reply.status(401).send({ error: 'Invalid credentials' });
}
const token = signToken(fastify, { username });
reply
.setCookie('token', token, {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24, // 24 hours
})
.send({ ok: true, username });
});
fastify.post('/auth/logout', async (_request, reply) => {
reply
.clearCookie('token', { path: '/' })
.send({ ok: true });
});
fastify.get(
'/auth/me',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
return reply.send({ username: request.user.username });
},
);
}

View File

@@ -0,0 +1,145 @@
import { FastifyInstance } from 'fastify';
import { eq, and, gte, lte, desc } from 'drizzle-orm';
import { db } from '../db/index.js';
import { experiments, priceChanges, performanceSnapshots, platforms } from '../db/schema.js';
export default async function (fastify: FastifyInstance) {
// List all experiments
fastify.get(
'/experiments',
{ preHandler: [fastify.authenticate] },
async (_request, reply) => {
const rows = await db
.select()
.from(experiments)
.orderBy(desc(experiments.createdAt));
return reply.send(rows);
},
);
// Create new experiment
fastify.post<{
Body: {
name: string;
hypothesis?: string;
startDate: string;
endDate?: string;
};
}>(
'/experiments',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { name, hypothesis, startDate, endDate } = request.body;
const [created] = await db
.insert(experiments)
.values({
name,
hypothesis: hypothesis ?? null,
startDate,
endDate: endDate ?? null,
status: 'active',
})
.returning();
return reply.status(201).send(created);
},
);
// Update experiment
fastify.put<{
Params: { id: string };
Body: {
status?: string;
conclusion?: string;
endDate?: string;
name?: string;
hypothesis?: string;
};
}>(
'/experiments/:id',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { id } = request.params;
const updates: Record<string, unknown> = {};
if (request.body.status !== undefined) updates.status = request.body.status;
if (request.body.conclusion !== undefined) updates.conclusion = request.body.conclusion;
if (request.body.endDate !== undefined) updates.endDate = request.body.endDate;
if (request.body.name !== undefined) updates.name = request.body.name;
if (request.body.hypothesis !== undefined) updates.hypothesis = request.body.hypothesis;
if (Object.keys(updates).length === 0) {
return reply.status(400).send({ error: 'No fields to update' });
}
const [updated] = await db
.update(experiments)
.set(updates)
.where(eq(experiments.id, id))
.returning();
if (!updated) {
return reply.status(404).send({ error: 'Experiment not found' });
}
return reply.send(updated);
},
);
// Analysis: linked price changes + performance data for the experiment date range
fastify.get<{ Params: { id: string } }>(
'/experiments/:id/analysis',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { id } = request.params;
const [experiment] = await db
.select()
.from(experiments)
.where(eq(experiments.id, id))
.limit(1);
if (!experiment) {
return reply.status(404).send({ error: 'Experiment not found' });
}
// Get price changes linked to this experiment
const linkedChanges = await db
.select({
id: priceChanges.id,
platformId: priceChanges.platformId,
date: priceChanges.date,
priceBefore: priceChanges.priceBefore,
priceAfter: priceChanges.priceAfter,
changedAt: priceChanges.changedAt,
platformDisplayName: platforms.displayName,
})
.from(priceChanges)
.innerJoin(platforms, eq(priceChanges.platformId, platforms.id))
.where(eq(priceChanges.experimentId, id))
.orderBy(priceChanges.date);
// Get performance snapshots within the experiment date range
const perfConditions = [
gte(performanceSnapshots.capturedAt, new Date(experiment.startDate)),
];
if (experiment.endDate) {
perfConditions.push(lte(performanceSnapshots.capturedAt, new Date(experiment.endDate)));
}
const snapshots = await db
.select()
.from(performanceSnapshots)
.where(and(...perfConditions))
.orderBy(performanceSnapshots.capturedAt);
return reply.send({
experiment,
priceChanges: linkedChanges,
performanceSnapshots: snapshots,
});
},
);
}

View File

@@ -0,0 +1,20 @@
import { FastifyInstance } from 'fastify';
import { sql } from '../db/index.js';
export default async function (fastify: FastifyInstance) {
fastify.get('/health', async (_request, reply) => {
let dbOk = false;
try {
await sql`SELECT 1`;
dbOk = true;
} catch {
// db unreachable
}
return reply.send({
status: 'ok',
db: dbOk,
timestamp: new Date().toISOString(),
});
});
}

View File

@@ -0,0 +1,142 @@
import { FastifyInstance } from 'fastify';
import { eq, and, gte, lte, desc, sql } from 'drizzle-orm';
import { db } from '../db/index.js';
import { performanceSnapshots, platforms } from '../db/schema.js';
export default async function (fastify: FastifyInstance) {
// Query snapshots with filters
fastify.get<{
Querystring: { platform?: string; from?: string; to?: string };
}>(
'/performance/snapshots',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { platform, from, to } = request.query;
const conditions = [];
if (platform) conditions.push(eq(performanceSnapshots.platformId, platform));
if (from) conditions.push(gte(performanceSnapshots.capturedAt, from));
if (to) conditions.push(lte(performanceSnapshots.capturedAt, to));
const rows = await db
.select()
.from(performanceSnapshots)
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(performanceSnapshots.capturedAt))
.limit(200);
return reply.send(rows);
},
);
// Aggregated summary for the dashboard
fastify.get(
'/performance/summary',
{ preHandler: [fastify.authenticate] },
async (_request, reply) => {
// Get latest snapshot per platform
const latestPerPlatform = await db
.selectDistinctOn([performanceSnapshots.platformId], {
platformId: performanceSnapshots.platformId,
viewsSearch: performanceSnapshots.viewsSearch,
viewsListing: performanceSnapshots.viewsListing,
conversionRate: performanceSnapshots.conversionRate,
bookingsCount: performanceSnapshots.bookingsCount,
occupancyRate: performanceSnapshots.occupancyRate,
avgDailyRate: performanceSnapshots.avgDailyRate,
revenueTotal: performanceSnapshots.revenueTotal,
})
.from(performanceSnapshots)
.orderBy(performanceSnapshots.platformId, desc(performanceSnapshots.capturedAt));
// Aggregate across platforms
let totalOccupancy = 0;
let totalAdr = 0;
let totalRevenue = 0;
let totalViews = 0;
const count = latestPerPlatform.length || 1;
for (const snap of latestPerPlatform) {
totalOccupancy += Number(snap.occupancyRate || 0);
totalAdr += Number(snap.avgDailyRate || 0);
totalRevenue += Number(snap.revenueTotal || 0);
totalViews += Number(snap.viewsSearch || 0);
}
// Get data from ~30 days ago for trend calculation
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const oldSnapshots = await db
.selectDistinctOn([performanceSnapshots.platformId], {
platformId: performanceSnapshots.platformId,
occupancyRate: performanceSnapshots.occupancyRate,
avgDailyRate: performanceSnapshots.avgDailyRate,
revenueTotal: performanceSnapshots.revenueTotal,
viewsSearch: performanceSnapshots.viewsSearch,
})
.from(performanceSnapshots)
.where(lte(performanceSnapshots.capturedAt, thirtyDaysAgo.toISOString()))
.orderBy(performanceSnapshots.platformId, desc(performanceSnapshots.capturedAt));
let oldOccupancy = 0, oldAdr = 0, oldRevenue = 0, oldViews = 0;
for (const snap of oldSnapshots) {
oldOccupancy += Number(snap.occupancyRate || 0);
oldAdr += Number(snap.avgDailyRate || 0);
oldRevenue += Number(snap.revenueTotal || 0);
oldViews += Number(snap.viewsSearch || 0);
}
const oldCount = oldSnapshots.length || 1;
const calcTrend = (current: number, old: number) =>
old > 0 ? (current - old) / old : 0;
const summary = {
occupancyRate: (totalOccupancy / count) / 100,
occupancyTrend: calcTrend(totalOccupancy / count, oldOccupancy / oldCount),
avgDailyRate: totalAdr / count,
adrTrend: calcTrend(totalAdr / count, oldAdr / oldCount),
revenueMtd: totalRevenue,
revenueTrend: calcTrend(totalRevenue, oldRevenue),
searchViews30d: totalViews,
viewsTrend: calcTrend(totalViews, oldViews),
platforms: latestPerPlatform,
};
return reply.send(summary);
},
);
// Time-series data grouped by date for charts
fastify.get<{
Querystring: { platform?: string; from?: string; to?: string };
}>(
'/performance/trends',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { platform, from, to } = request.query;
const conditions = [];
if (platform) conditions.push(eq(performanceSnapshots.platformId, platform));
if (from) conditions.push(gte(performanceSnapshots.capturedAt, from));
if (to) conditions.push(lte(performanceSnapshots.capturedAt, to));
const rows = await db
.select({
date: performanceSnapshots.periodLabel,
platformId: performanceSnapshots.platformId,
viewsSearch: performanceSnapshots.viewsSearch,
viewsListing: performanceSnapshots.viewsListing,
conversionRate: performanceSnapshots.conversionRate,
bookingsCount: performanceSnapshots.bookingsCount,
occupancyRate: performanceSnapshots.occupancyRate,
avgDailyRate: performanceSnapshots.avgDailyRate,
revenueTotal: performanceSnapshots.revenueTotal,
})
.from(performanceSnapshots)
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(performanceSnapshots.capturedAt);
return reply.send(rows);
},
);
}

View File

@@ -0,0 +1,209 @@
import { FastifyInstance } from 'fastify';
import { eq, desc } from 'drizzle-orm';
import { db } from '../db/index.js';
import { platforms, scrapeJobs, performanceSnapshots } from '../db/schema.js';
import { encrypt, decrypt } from '../services/encryption.js';
import { config } from '../config.js';
export default async function (fastify: FastifyInstance) {
// List all platforms with latest metrics
fastify.get(
'/platforms',
{ preHandler: [fastify.authenticate] },
async (_request, reply) => {
const rows = await db
.select({
id: platforms.id,
displayName: platforms.displayName,
hasCredentials: platforms.credentialsEncrypted,
lastScrapeAt: platforms.lastScrapeAt,
isActive: platforms.isActive,
})
.from(platforms);
// Get latest snapshot and scrape job per platform
const result = await Promise.all(
rows.map(async (r) => {
const [latestSnapshot] = await db
.select()
.from(performanceSnapshots)
.where(eq(performanceSnapshots.platformId, r.id))
.orderBy(desc(performanceSnapshots.capturedAt))
.limit(1);
const [latestJob] = await db
.select()
.from(scrapeJobs)
.where(eq(scrapeJobs.platformId, r.id))
.orderBy(desc(scrapeJobs.startedAt))
.limit(1);
return {
id: r.id,
name: r.id,
displayName: r.displayName,
hasCredentials: r.hasCredentials !== null,
lastScrapeAt: latestJob?.completedAt || r.lastScrapeAt,
lastScrapeStatus: latestJob?.status === 'completed' ? 'success' : (latestJob?.status || 'unknown'),
isActive: r.isActive,
occupancyRate: latestSnapshot ? Number(latestSnapshot.occupancyRate) / 100 : 0,
avgDailyRate: latestSnapshot ? Number(latestSnapshot.avgDailyRate) : 0,
revenueMtd: latestSnapshot ? Number(latestSnapshot.revenueTotal) : 0,
searchViews30d: latestSnapshot?.viewsSearch || 0,
};
}),
);
return reply.send(result);
},
);
// Update encrypted credentials
fastify.put<{
Params: { id: string };
Body: Record<string, string>;
}>(
'/platforms/:id/credentials',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { id } = request.params;
const credentials = request.body;
const encrypted = encrypt(JSON.stringify(credentials));
const updated = await db
.update(platforms)
.set({ credentialsEncrypted: encrypted })
.where(eq(platforms.id, id))
.returning();
if (updated.length === 0) {
return reply.status(404).send({ error: 'Platform not found' });
}
return reply.send({ ok: true });
},
);
// Test platform credentials via scraper
fastify.post<{ Params: { id: string } }>(
'/platforms/:id/test',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { id } = request.params;
try {
const res = await fetch(`${config.SCRAPER_URL}/platforms/${id}/test`, {
method: 'POST',
});
const data = await res.json();
return reply.status(res.status).send(data);
} catch {
return reply.status(502).send({ error: 'Scraper service unavailable' });
}
},
);
// Trigger scrape job
fastify.post<{ Params: { id: string } }>(
'/platforms/:id/scrape',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { id } = request.params;
try {
const res = await fetch(`${config.SCRAPER_URL}/jobs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ platformId: id, jobType: 'full', triggeredBy: 'manual' }),
});
const data = await res.json();
return reply.status(res.status).send(data);
} catch {
return reply.status(502).send({ error: 'Scraper service unavailable' });
}
},
);
// Login to platform via scraper (launches browser for MFA)
fastify.post<{
Params: { id: string };
Body: { email?: string; password?: string };
}>(
'/platforms/:id/login',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { id } = request.params;
let { email, password } = request.body || {};
// If no credentials in body, pull from DB
if (!email || !password) {
const [platform] = await db
.select({ credentialsEncrypted: platforms.credentialsEncrypted })
.from(platforms)
.where(eq(platforms.id, id));
if (platform?.credentialsEncrypted) {
try {
const saved = JSON.parse(decrypt(platform.credentialsEncrypted));
email = email || saved.email;
password = password || saved.password;
} catch (err) {
return reply.status(500).send({ error: 'Failed to decrypt stored credentials' });
}
}
}
if (!email || !password) {
return reply.status(400).send({ error: 'No credentials provided and none saved for this platform. Save credentials first.' });
}
try {
const res = await fetch(`${config.SCRAPER_URL}/platforms/${id}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
return reply.status(res.status).send(data);
} catch {
return reply.status(502).send({ error: 'Scraper service unavailable' });
}
},
);
// Check platform session status
fastify.get<{ Params: { id: string } }>(
'/platforms/:id/session',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { id } = request.params;
try {
const res = await fetch(`${config.SCRAPER_URL}/platforms/${id}/session`);
const data = await res.json();
return reply.status(res.status).send(data);
} catch {
return reply.status(502).send({ error: 'Scraper service unavailable' });
}
},
);
// List recent scrape jobs for a platform
fastify.get<{ Params: { id: string } }>(
'/platforms/:id/scrape-jobs',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { id } = request.params;
const jobs = await db
.select()
.from(scrapeJobs)
.where(eq(scrapeJobs.platformId, id))
.orderBy(desc(scrapeJobs.startedAt))
.limit(20);
return reply.send(jobs);
},
);
}

View File

@@ -0,0 +1,185 @@
import { FastifyInstance } from 'fastify';
import { eq, and, gte, lte, desc } from 'drizzle-orm';
import { db } from '../db/index.js';
import { dailyPrices, priceChanges, platforms } from '../db/schema.js';
import { config } from '../config.js';
interface PriceChangeItem {
date: string;
price: number;
platformIds: string[];
}
export default async function (fastify: FastifyInstance) {
// Get daily prices calendar
fastify.get<{
Querystring: { platform?: string; from?: string; to?: string };
}>(
'/pricing/calendar',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { platform, from, to } = request.query;
const conditions = [];
if (platform) conditions.push(eq(dailyPrices.platformId, platform));
if (from) conditions.push(gte(dailyPrices.date, from));
if (to) conditions.push(lte(dailyPrices.date, to));
const rows = await db
.select()
.from(dailyPrices)
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(dailyPrices.date);
return reply.send(rows);
},
);
// Preview price changes - returns diff with previewToken
fastify.post<{
Body: { changes: PriceChangeItem[] };
}>(
'/pricing/preview',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { changes } = request.body;
// Build the diff: fetch current prices for the affected dates/platforms
const diff = [];
for (const change of changes) {
for (const platformId of change.platformIds) {
const current = await db
.select()
.from(dailyPrices)
.where(
and(
eq(dailyPrices.platformId, platformId),
eq(dailyPrices.date, change.date),
),
)
.limit(1);
diff.push({
platformId,
date: change.date,
currentPrice: current[0]?.price ?? null,
newPrice: change.price.toString(),
});
}
}
// Issue a short-lived preview token
const previewToken = fastify.jwt.sign(
{ changes, diff, type: 'price-preview' },
{ expiresIn: '5m' },
);
return reply.send({ diff, previewToken });
},
);
// Apply previewed price changes
fastify.post<{
Body: { previewToken: string; note?: string; experimentId?: string };
}>(
'/pricing/apply',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { previewToken, note, experimentId } = request.body;
// Verify the preview token
let payload: { changes: PriceChangeItem[]; diff: Array<{ platformId: string; date: string; currentPrice: string | null; newPrice: string }> };
try {
payload = fastify.jwt.verify<typeof payload>(previewToken);
} catch {
return reply.status(400).send({ error: 'Invalid or expired preview token' });
}
// Forward price updates to scraper service
try {
const res = await fetch(`${config.SCRAPER_URL}/api/pricing/apply`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ changes: payload.changes }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
return reply.status(res.status).send({ error: 'Scraper failed to apply prices', detail: err });
}
} catch {
return reply.status(502).send({ error: 'Scraper service unavailable' });
}
// Record price changes in DB
const changeRecords = payload.diff.map((d) => ({
platformId: d.platformId,
date: d.date,
priceBefore: d.currentPrice ?? '0',
priceAfter: d.newPrice,
changedBy: request.user.username,
note: note ?? null,
experimentId: experimentId ?? null,
}));
if (changeRecords.length > 0) {
await db.insert(priceChanges).values(changeRecords);
}
// Update daily_prices table
for (const d of payload.diff) {
await db
.insert(dailyPrices)
.values({
platformId: d.platformId,
date: d.date,
price: d.newPrice,
syncedAt: new Date(),
})
.onConflictDoUpdate({
target: [dailyPrices.platformId, dailyPrices.date],
set: { price: d.newPrice, syncedAt: new Date() },
});
}
return reply.send({ ok: true, applied: changeRecords.length });
},
);
// Price change log
fastify.get<{
Querystring: { platform?: string; from?: string; to?: string };
}>(
'/pricing/changes',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { platform, from, to } = request.query;
const conditions = [];
if (platform) conditions.push(eq(priceChanges.platformId, platform));
if (from) conditions.push(gte(priceChanges.date, from));
if (to) conditions.push(lte(priceChanges.date, to));
const rows = await db
.select({
id: priceChanges.id,
platformId: priceChanges.platformId,
date: priceChanges.date,
priceBefore: priceChanges.priceBefore,
priceAfter: priceChanges.priceAfter,
changedAt: priceChanges.changedAt,
changedBy: priceChanges.changedBy,
note: priceChanges.note,
experimentId: priceChanges.experimentId,
platformDisplayName: platforms.displayName,
})
.from(priceChanges)
.innerJoin(platforms, eq(priceChanges.platformId, platforms.id))
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(priceChanges.changedAt))
.limit(200);
return reply.send(rows);
},
);
}

View File

@@ -0,0 +1,30 @@
import { FastifyInstance } from 'fastify';
import { buildWeeklyReport, sendWeeklyReport } from '../email/weeklyReport.js';
export default async function (fastify: FastifyInstance) {
// Send weekly report email
fastify.post(
'/reports/weekly/send',
{ preHandler: [fastify.authenticate] },
async (_request, reply) => {
try {
const report = await buildWeeklyReport();
await sendWeeklyReport(report);
return reply.send({ ok: true, message: 'Weekly report sent' });
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return reply.status(500).send({ error: 'Failed to send report', detail: message });
}
},
);
// Preview weekly report as JSON
fastify.get(
'/reports/weekly/preview',
{ preHandler: [fastify.authenticate] },
async (_request, reply) => {
const report = await buildWeeklyReport();
return reply.send(report);
},
);
}

View File

@@ -0,0 +1,75 @@
import { FastifyInstance } from 'fastify';
import { eq, and, gte, lte, desc, sql } from 'drizzle-orm';
import { db } from '../db/index.js';
import { reservations, platforms } from '../db/schema.js';
export default async function (fastify: FastifyInstance) {
// List reservations with filters
fastify.get<{
Querystring: { platform?: string; status?: string; from?: string; to?: string };
}>(
'/reservations',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const { platform, status, from, to } = request.query;
const conditions = [];
if (platform) conditions.push(eq(reservations.platformId, platform));
if (status) conditions.push(eq(reservations.status, status));
if (from) conditions.push(gte(reservations.checkIn, from));
if (to) conditions.push(lte(reservations.checkIn, to));
const rows = await db
.select({
id: reservations.id,
platformId: reservations.platformId,
platformReservationId: reservations.platformReservationId,
guestName: reservations.guestName,
checkIn: reservations.checkIn,
checkOut: reservations.checkOut,
nights: reservations.nights,
guestsCount: reservations.guestsCount,
nightlyRate: reservations.nightlyRate,
cleaningFee: reservations.cleaningFee,
platformFee: reservations.platformFee,
totalPayout: reservations.totalPayout,
status: reservations.status,
bookedAt: reservations.bookedAt,
platformDisplayName: platforms.displayName,
})
.from(reservations)
.innerJoin(platforms, eq(reservations.platformId, platforms.id))
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(reservations.checkIn))
.limit(200);
return reply.send(rows);
},
);
// Monthly revenue and occupancy aggregation
fastify.get(
'/reservations/summary',
{ preHandler: [fastify.authenticate] },
async (_request, reply) => {
const rows = await db
.select({
month: sql<string>`to_char(${reservations.checkIn}::date, 'YYYY-MM')`.as('month'),
platformId: reservations.platformId,
totalRevenue: sql<string>`sum(${reservations.totalPayout}::numeric)`.as('total_revenue'),
reservationCount: sql<number>`count(*)::int`.as('reservation_count'),
totalNights: sql<number>`sum(${reservations.nights})::int`.as('total_nights'),
avgNightlyRate: sql<string>`avg(${reservations.nightlyRate}::numeric)`.as('avg_nightly_rate'),
})
.from(reservations)
.where(eq(reservations.status, 'confirmed'))
.groupBy(
sql`to_char(${reservations.checkIn}::date, 'YYYY-MM')`,
reservations.platformId,
)
.orderBy(sql`to_char(${reservations.checkIn}::date, 'YYYY-MM')`);
return reply.send(rows);
},
);
}

View File

@@ -0,0 +1,58 @@
import cron from 'node-cron';
import { eq } from 'drizzle-orm';
import { db } from '../db/index.js';
import { platforms } from '../db/schema.js';
import { config } from '../config.js';
import { buildWeeklyReport, sendWeeklyReport } from '../email/weeklyReport.js';
async function triggerDailyScrape() {
console.log('[cron] Starting daily scrape for all active platforms');
const activePlatforms = await db
.select({ id: platforms.id, displayName: platforms.displayName })
.from(platforms)
.where(eq(platforms.isActive, true));
for (const platform of activePlatforms) {
try {
const res = await fetch(`${config.SCRAPER_URL}/api/platforms/${platform.id}/scrape`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ triggeredBy: 'cron' }),
});
if (res.ok) {
console.log(`[cron] Scrape triggered for ${platform.displayName}`);
} else {
console.error(`[cron] Scrape failed for ${platform.displayName}: ${res.status}`);
}
} catch (err) {
console.error(`[cron] Scrape error for ${platform.displayName}:`, err);
}
}
}
async function triggerWeeklyReport() {
console.log('[cron] Building and sending weekly report');
try {
const report = await buildWeeklyReport();
await sendWeeklyReport(report);
console.log('[cron] Weekly report sent successfully');
} catch (err) {
console.error('[cron] Weekly report failed:', err);
}
}
export function initCronJobs() {
// Daily scrape at 3:00 AM
cron.schedule('0 3 * * *', () => {
triggerDailyScrape().catch(console.error);
});
// Weekly report Monday at 8:00 AM
cron.schedule('0 8 * * 1', () => {
triggerWeeklyReport().catch(console.error);
});
console.log('[cron] Scheduled jobs initialized: daily scrape (3am), weekly report (Mon 8am)');
}

View File

@@ -0,0 +1,45 @@
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
import { config } from '../config.js';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;
function getKey(): Buffer {
const key = Buffer.from(config.ENCRYPTION_KEY, 'hex');
if (key.length !== 32) {
throw new Error('ENCRYPTION_KEY must be 32 bytes (64 hex characters)');
}
return key;
}
export function encrypt(plaintext: string): string {
const key = getKey();
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Format: iv:authTag:ciphertext (all hex)
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
export function decrypt(encryptedPayload: string): string {
const key = getKey();
const [ivHex, authTagHex, ciphertext] = encryptedPayload.split(':');
if (!ivHex || !authTagHex || !ciphertext) {
throw new Error('Invalid encrypted payload format');
}
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}

8
apps/api/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}