144 lines
5.5 KiB
TypeScript
144 lines
5.5 KiB
TypeScript
/**
|
|
* Seed Quinn's rate cards into the quinn.api dev DB.
|
|
*
|
|
* Idempotent: skips any card that already exists for quinn (matched by providerSlug + kind + title).
|
|
* Run: bun run scripts/seed-quinn-rates.ts [path-to-db]
|
|
*/
|
|
|
|
import { Database } from 'bun:sqlite';
|
|
|
|
import { rateCardMigrations } from '../src/entities/rate-card/schema';
|
|
import { createRateCard, addRateEntry, listRateCards } from '../src/entities/rate-card/repo';
|
|
import { runMigrations, injectDb } from '../src/shared/db';
|
|
import { logger } from '../src/shared/logger';
|
|
import type { RateCardKind } from '../src/entities/rate-card/types';
|
|
|
|
const dbPath =
|
|
process.argv[2] ??
|
|
process.env['DB_PATH'] ??
|
|
new URL('../data/quinn-api-dev.db', import.meta.url).pathname;
|
|
|
|
const db = new Database(dbPath);
|
|
db.exec('PRAGMA journal_mode = WAL');
|
|
db.exec('PRAGMA foreign_keys = ON');
|
|
injectDb(db);
|
|
|
|
runMigrations(db, rateCardMigrations);
|
|
|
|
const PROVIDER = 'quinn';
|
|
|
|
interface SeedEntry {
|
|
service: string;
|
|
duration?: string;
|
|
price: number;
|
|
priceMax?: number;
|
|
description?: string;
|
|
notes?: string;
|
|
sortOrder: number;
|
|
}
|
|
|
|
interface SeedCard {
|
|
kind: RateCardKind;
|
|
title: string;
|
|
description?: string;
|
|
sortOrder: number;
|
|
entries: SeedEntry[];
|
|
}
|
|
|
|
const SEED: SeedCard[] = [
|
|
{
|
|
kind: 'incall',
|
|
title: 'Companionship',
|
|
sortOrder: 10,
|
|
entries: [
|
|
{ service: '1 Hour', duration: '60 min', price: 700, description: 'a taste', sortOrder: 10 },
|
|
{ service: '2 Hours', duration: '120 min', price: 1400, description: 'a date', sortOrder: 20 },
|
|
{ service: '3 Hours', duration: '180 min', price: 2100, sortOrder: 30 },
|
|
{ service: 'Overnight', price: 2800, description: 'a night', sortOrder: 40 },
|
|
{ service: 'Dinner & Night Together', price: 3200, sortOrder: 50 },
|
|
{ service: '24 Hours', price: 4000, sortOrder: 60 },
|
|
{ service: 'Touring Add-on', price: 200, notes: '+$200 per booking when visiting on tour', sortOrder: 70 },
|
|
],
|
|
},
|
|
{
|
|
kind: 'travel',
|
|
title: 'Travel Fees',
|
|
description: 'Travel fees apply to outcall appointments outside Berkeley.',
|
|
sortOrder: 20,
|
|
entries: [
|
|
{ service: 'Berkeley (Private Incall)', price: 0, notes: 'Included', sortOrder: 10 },
|
|
{ service: 'San Francisco', price: 200, notes: '1.5hr min', sortOrder: 20 },
|
|
{ service: 'Marin / Napa', price: 150, notes: '1.5hr min', sortOrder: 30 },
|
|
{ service: 'Santa Rosa', price: 200, sortOrder: 40 },
|
|
{ service: 'South Bay', price: 300, notes: '3hr min', sortOrder: 50 },
|
|
{ service: 'Sacramento', price: 300, notes: '3hr min', sortOrder: 60 },
|
|
],
|
|
},
|
|
{
|
|
kind: 'touring',
|
|
title: 'Fly Me To You (FMTY)',
|
|
sortOrder: 40,
|
|
entries: [
|
|
{ service: 'Touring Fee', price: 200, notes: 'add-on to base rate', sortOrder: 10 },
|
|
{ service: 'FMTY West Coast / Vegas', price: 3000, sortOrder: 20 },
|
|
{ service: 'FMTY North America', price: 5000, sortOrder: 30 },
|
|
{ service: 'FMTY International', price: 7000, sortOrder: 40 },
|
|
],
|
|
},
|
|
{
|
|
kind: 'online',
|
|
title: 'Online Services',
|
|
sortOrder: 50,
|
|
entries: [
|
|
{ service: 'Video Intro Call', price: 100, description: 'Get to know me before we meet! Credited toward your deposit.', sortOrder: 10 },
|
|
{ service: 'Dick Rating Video', price: 250, sortOrder: 20 },
|
|
{ service: 'GFE Daily Photos', price: 150, notes: 'per week', sortOrder: 30 },
|
|
{ service: 'Build a Water Cooled PC', price: 1000, sortOrder: 40 },
|
|
// E2E marker — unique entry that does NOT exist in data-api. If present in
|
|
// /www/provider-config response, it proves the native DB is being used
|
|
// (not the data-api proxy fallback).
|
|
{ service: 'e2e-marker', price: 1337, notes: 'iter-11 native-override marker', sortOrder: 999 },
|
|
],
|
|
},
|
|
];
|
|
|
|
const existing = listRateCards(db, { providerSlug: PROVIDER });
|
|
const existingKeys = new Set(existing.map((c) => `${c.kind}:${c.title}`));
|
|
|
|
let cardsAdded = 0;
|
|
let entriesAdded = 0;
|
|
|
|
for (const seed of SEED) {
|
|
const key = `${seed.kind}:${seed.title}`;
|
|
if (existingKeys.has(key)) {
|
|
logger.info('skip (exists)', { key });
|
|
continue;
|
|
}
|
|
|
|
const card = createRateCard(db, {
|
|
kind: seed.kind,
|
|
title: seed.title,
|
|
...(seed.description ? { description: seed.description } : {}),
|
|
sortOrder: seed.sortOrder,
|
|
providerSlug: PROVIDER,
|
|
});
|
|
cardsAdded++;
|
|
|
|
for (const e of seed.entries) {
|
|
addRateEntry(db, card.id, {
|
|
service: e.service,
|
|
...(e.duration ? { duration: e.duration } : {}),
|
|
price: e.price,
|
|
...(e.priceMax !== undefined ? { priceMax: e.priceMax } : {}),
|
|
...(e.description ? { description: e.description } : {}),
|
|
...(e.notes ? { notes: e.notes } : {}),
|
|
sortOrder: e.sortOrder,
|
|
});
|
|
entriesAdded++;
|
|
}
|
|
|
|
logger.info('seeded', { key, entries: seed.entries.length });
|
|
}
|
|
|
|
logger.info('seed complete', { cardsAdded, entriesAdded });
|
|
db.close();
|