@happyvertical/smrt-bookings 0.60.1 → 0.60.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,19 @@ Intervals include their start and exclude their end. A supervision window
45
45
  starts and ends on one local date, or ends at midnight opening the next date;
46
46
  it need not cover the entire day and may span a daylight-saving transition.
47
47
 
48
+ Use `listBookings` to find the same booking views returned by `get`. An
49
+ optional project UUID narrows the results within the caller's existing scope.
50
+ Operators see all bookings in their tenant. Other callers see only bookings
51
+ for the contact the host maps to their actor. `listOfferings` exposes the
52
+ offering ID, kind and enabled flag. Contacts only see enabled offerings.
53
+ Operators can use `listAvailability` to inspect existing provider windows
54
+ before replacing a declaration. All three methods use the `read` authority.
55
+
56
+ Lists return up to 50 items by default and accept a limit from 1 to 100.
57
+ Results are ordered by ascending UUID. Pass the returned `nextCursor` as
58
+ `afterId` with the same filters to fetch another page. A null cursor means
59
+ the page reached the end of the results.
60
+
48
61
  Use an authenticated tenant/actor context from the host, never from a request
49
62
  body. The host's `BookingAuthority` supplies current operator permission and
50
63
  the actor's named contact mapping, and must verify that external references
package/dist/index.js CHANGED
@@ -223,6 +223,41 @@ function id(value) {
223
223
  if (typeof value !== "string" || !UUID.test(value)) throw new BookingError("invalid", "Expected a UUID reference");
224
224
  return value.toLowerCase();
225
225
  }
226
+ function pageBounds({ afterId, limit = 50 }) {
227
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) throw new BookingError("invalid", "Page limit must be between 1 and 100");
228
+ return {
229
+ after: afterId === void 0 ? null : id(afterId),
230
+ limit
231
+ };
232
+ }
233
+ function page(rows, limit, cursor) {
234
+ return {
235
+ items: rows.slice(0, limit),
236
+ nextCursor: rows.length > limit ? cursor(rows[limit - 1]) : null
237
+ };
238
+ }
239
+ function bookingView(row, proposal) {
240
+ return {
241
+ id: row.id,
242
+ offeringId: row.offering_id,
243
+ contactId: row.contact_id,
244
+ projectId: row.project_id,
245
+ kind: row.kind,
246
+ status: row.status,
247
+ proposal,
248
+ assentedProposalId: row.assented_proposal_id,
249
+ confirmedProposalId: row.confirmed_proposal_id
250
+ };
251
+ }
252
+ function proposalView(proposalId, row) {
253
+ return {
254
+ id: proposalId,
255
+ providerId: row.provider_id,
256
+ window: JSON.parse(row.coverage),
257
+ terms: JSON.parse(row.terms),
258
+ approvedPlan: row.approved_plan === null ? null : JSON.parse(row.approved_plan)
259
+ };
260
+ }
226
261
  function required(value, max = 200) {
227
262
  if (typeof value !== "string" || value.trim().length === 0 || value.length > max || value.includes("\0")) throw new BookingError("invalid", "Expected a nonempty text value within the length limit");
228
263
  return value;
@@ -431,16 +466,56 @@ var BookingService = class {
431
466
  const access = await this.#access(ctx, "read");
432
467
  const row = await this.#booking(this.#db, ctx, id(bookingId), false);
433
468
  visible(access, row);
469
+ return bookingView(row, row.proposal_id ? await this.#proposal(this.#db, ctx, row, row.proposal_id) : null);
470
+ }
471
+ /** Lists the tenant's bookings, restricted to the current contact for nonoperators. */
472
+ async listBookings(context, options = {}) {
473
+ const ctx = identity(context);
474
+ const access = await this.#discover(ctx);
475
+ const { after, limit } = pageBounds(options);
476
+ const projectId = options.projectId === void 0 ? null : id(options.projectId);
477
+ const rows = (await this.#db.query(`SELECT b.id, b.offering_id, b.contact_id, b.project_id, b.kind, b.status,
478
+ b.proposal_id, b.assented_proposal_id, b.confirmed_proposal_id,
479
+ p.provider_id, p.coverage::text, p.terms::text, p.approved_plan::text
480
+ FROM smrt_bookings b
481
+ LEFT JOIN smrt_booking_proposals p
482
+ ON p.tenant_id = b.tenant_id AND p.booking_id = b.id AND p.id = b.proposal_id
483
+ WHERE b.tenant_id = ? AND (?::boolean OR b.contact_id = ?)
484
+ AND (?::uuid IS NULL OR b.project_id = ?)
485
+ AND (?::uuid IS NULL OR b.id > ?)
486
+ ORDER BY b.id LIMIT ?`, ctx.tenantId, access.operator, access.contactId ?? null, projectId, projectId, after, after, limit + 1)).rows;
487
+ const selected = page(rows, limit, (row) => row.id);
434
488
  return {
435
- id: row.id,
436
- offeringId: row.offering_id,
437
- contactId: row.contact_id,
438
- projectId: row.project_id,
439
- kind: row.kind,
440
- status: row.status,
441
- proposal: row.proposal_id ? await this.#proposal(this.#db, ctx, row, row.proposal_id) : null,
442
- assentedProposalId: row.assented_proposal_id,
443
- confirmedProposalId: row.confirmed_proposal_id
489
+ items: selected.items.map((row) => bookingView(row, row.proposal_id ? proposalView(row.proposal_id, row) : null)),
490
+ nextCursor: selected.nextCursor
491
+ };
492
+ }
493
+ /** Lists configured offerings. Contacts only see enabled offerings. */
494
+ async listOfferings(context, options = {}) {
495
+ const ctx = identity(context);
496
+ const access = await this.#discover(ctx);
497
+ const { after, limit } = pageBounds(options);
498
+ const { rows } = await this.#db.query(`SELECT id, kind, enabled FROM smrt_booking_offerings
499
+ WHERE tenant_id = ? AND (?::boolean OR enabled)
500
+ AND (?::uuid IS NULL OR id > ?)
501
+ ORDER BY id LIMIT ?`, ctx.tenantId, access.operator, after, after, limit + 1);
502
+ return page(rows, limit, (row) => row.id);
503
+ }
504
+ /** Lists provider schedules so operators can inspect or replace existing windows. */
505
+ async listAvailability(context, options = {}) {
506
+ const ctx = identity(context);
507
+ operator(await this.#access(ctx, "read"));
508
+ const { after, limit } = pageBounds(options);
509
+ const { rows } = await this.#db.query(`SELECT id, windows::text FROM smrt_booking_resources
510
+ WHERE tenant_id = ? AND (?::uuid IS NULL OR id > ?)
511
+ ORDER BY id LIMIT ?`, ctx.tenantId, after, after, limit + 1);
512
+ const selected = page(rows, limit, (row) => row.id);
513
+ return {
514
+ items: selected.items.map((row) => ({
515
+ providerId: row.id,
516
+ windows: JSON.parse(row.windows)
517
+ })),
518
+ nextCursor: selected.nextCursor
444
519
  };
445
520
  }
446
521
  async getProposal(context, bookingId, proposalId) {
@@ -497,6 +572,11 @@ var BookingService = class {
497
572
  contactId: access.contactId === void 0 ? void 0 : id(access.contactId)
498
573
  };
499
574
  }
575
+ async #discover(ctx) {
576
+ const access = await this.#access(ctx, "read");
577
+ if (!access.operator && !access.contactId) throw new BookingError("forbidden", "A named customer contact is required");
578
+ return access;
579
+ }
500
580
  async #verify(ctx, refs) {
501
581
  if (await this.#authority.verifyReferences(ctx, structuredClone(refs)) !== true) throw new BookingError("forbidden", "Booking references or approval could not be verified");
502
582
  }
@@ -519,13 +599,7 @@ var BookingService = class {
519
599
  WHERE tenant_id = ? AND booking_id = ? AND id = ?`, ctx.tenantId, booking.id, proposalId);
520
600
  const row = rows[0];
521
601
  if (!row) throw new BookingError("not-found", "Proposal does not exist");
522
- return {
523
- id: proposalId,
524
- providerId: row.provider_id,
525
- window: JSON.parse(row.coverage),
526
- terms: JSON.parse(row.terms),
527
- approvedPlan: row.approved_plan === null ? null : JSON.parse(row.approved_plan)
528
- };
602
+ return proposalView(proposalId, row);
529
603
  }
530
604
  async #resource(tx, ctx, providerId, lock) {
531
605
  const { rows } = await tx.query(`SELECT windows::text FROM smrt_booking_resources WHERE tenant_id = ? AND id = ?${lock ? " FOR UPDATE" : ""}`, ctx.tenantId, providerId);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["value"],"sources":["../src/errors.ts","../src/migration.ts","../src/windows.ts","../src/service.ts"],"sourcesContent":["/** A service rejection suitable for mapping to a host response. */\nexport class BookingError extends Error {\n constructor(\n readonly code:\n | 'invalid'\n | 'forbidden'\n | 'not-found'\n | 'conflict'\n | 'unavailable'\n | 'duplicate-project-window',\n message: string,\n ) {\n super(message);\n this.name = 'BookingError';\n }\n}\n","import type { DatabaseInterface } from '@happyvertical/sql';\n\nconst statements = [\n `CREATE TABLE smrt_booking_offerings (\n tenant_id uuid NOT NULL,\n id uuid NOT NULL,\n kind text NOT NULL CHECK (kind IN ('developer-hour', 'supervision-day')),\n enabled boolean NOT NULL,\n PRIMARY KEY (tenant_id, id)\n )`,\n `CREATE TABLE smrt_booking_resources (\n tenant_id uuid NOT NULL,\n id uuid NOT NULL,\n windows jsonb NOT NULL,\n PRIMARY KEY (tenant_id, id)\n )`,\n `CREATE TABLE smrt_bookings (\n tenant_id uuid NOT NULL,\n id uuid NOT NULL,\n offering_id uuid NOT NULL,\n contact_id uuid NOT NULL,\n project_id uuid,\n kind text NOT NULL CHECK (kind IN ('developer-hour', 'supervision-day')),\n status text NOT NULL CHECK (status IN ('requested', 'proposed', 'confirmed', 'declined', 'cancelled')),\n proposal_id uuid,\n assented_proposal_id uuid,\n confirmed_proposal_id uuid,\n PRIMARY KEY (tenant_id, id),\n FOREIGN KEY (tenant_id, offering_id) REFERENCES smrt_booking_offerings (tenant_id, id),\n CHECK (kind <> 'supervision-day' OR project_id IS NOT NULL),\n CHECK (status <> 'confirmed' OR confirmed_proposal_id IS NOT NULL),\n CHECK (assented_proposal_id IS NULL OR assented_proposal_id IS NOT DISTINCT FROM proposal_id),\n CHECK (confirmed_proposal_id IS NULL OR confirmed_proposal_id IS NOT DISTINCT FROM assented_proposal_id)\n )`,\n `CREATE TABLE smrt_booking_proposals (\n tenant_id uuid NOT NULL,\n booking_id uuid NOT NULL,\n id uuid NOT NULL,\n provider_id uuid NOT NULL,\n coverage jsonb NOT NULL,\n terms jsonb NOT NULL,\n approved_plan jsonb,\n PRIMARY KEY (tenant_id, booking_id, id),\n FOREIGN KEY (tenant_id, booking_id) REFERENCES smrt_bookings (tenant_id, id),\n FOREIGN KEY (tenant_id, provider_id) REFERENCES smrt_booking_resources (tenant_id, id)\n )`,\n `ALTER TABLE smrt_bookings\n ADD FOREIGN KEY (tenant_id, id, proposal_id) REFERENCES smrt_booking_proposals (tenant_id, booking_id, id)`,\n `CREATE TABLE smrt_booking_allocations (\n tenant_id uuid NOT NULL,\n booking_id uuid NOT NULL,\n proposal_id uuid NOT NULL,\n provider_id uuid NOT NULL,\n project_id uuid,\n exclusive boolean NOT NULL,\n start_at timestamptz NOT NULL,\n end_at timestamptz NOT NULL CHECK (end_at > start_at),\n released_at timestamptz,\n PRIMARY KEY (tenant_id, booking_id),\n FOREIGN KEY (tenant_id, booking_id, proposal_id)\n REFERENCES smrt_booking_proposals (tenant_id, booking_id, id),\n FOREIGN KEY (tenant_id, provider_id) REFERENCES smrt_booking_resources (tenant_id, id),\n CHECK (exclusive = (project_id IS NULL))\n )`,\n `CREATE INDEX smrt_booking_active_provider ON smrt_booking_allocations (tenant_id, provider_id, start_at)\n WHERE released_at IS NULL`,\n `CREATE UNIQUE INDEX smrt_booking_active_project_window\n ON smrt_booking_allocations (tenant_id, project_id, start_at, end_at)\n WHERE released_at IS NULL AND project_id IS NOT NULL`,\n `CREATE TABLE smrt_booking_commands (\n sequence bigint GENERATED ALWAYS AS IDENTITY UNIQUE,\n tenant_id uuid NOT NULL,\n operation text NOT NULL,\n key_hash text NOT NULL,\n actor_id text NOT NULL,\n payload_hash text NOT NULL,\n booking_id uuid,\n result jsonb,\n reason text,\n at timestamptz NOT NULL DEFAULT clock_timestamp(),\n PRIMARY KEY (tenant_id, operation, key_hash),\n FOREIGN KEY (tenant_id, booking_id) REFERENCES smrt_bookings (tenant_id, id)\n )`,\n `CREATE INDEX smrt_booking_history ON smrt_booking_commands (tenant_id, booking_id, sequence)`,\n];\n\n/** Initial PostgreSQL schema. Register once in the host deployment's migration ledger. */\nexport async function migrateBookingsV1(db: DatabaseInterface): Promise<void> {\n if (!db.transaction)\n throw new TypeError('Bookings requires a PostgreSQL transaction');\n await db.transaction(async (tx) => {\n for (const statement of statements) await tx.query(statement);\n });\n}\n","import { BookingError } from './errors.js';\nimport type {\n AvailabilityWindow,\n BookingWindow,\n ResolvedBookingWindow,\n} from './types.js';\n\nconst WALL = /^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2})([+-]\\d{2}:\\d{2})$/;\nconst MINUTE_MS = 60_000;\n\nfunction formatter(zone: string): Intl.DateTimeFormat {\n // Intl also accepts offsets as time zone names. These are not IANA zones.\n if (typeof zone !== 'string' || !/^[A-Za-z][A-Za-z0-9_+/-]*$/.test(zone)) {\n throw new BookingError('invalid', 'An IANA time zone is required');\n }\n try {\n return new Intl.DateTimeFormat('en-GB', {\n timeZone: zone,\n calendar: 'iso8601',\n numberingSystem: 'latn',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hourCycle: 'h23',\n });\n } catch {\n throw new BookingError('invalid', 'Unknown IANA time zone');\n }\n}\n\nfunction instant(value: string, format: Intl.DateTimeFormat): string {\n const match = typeof value === 'string' ? WALL.exec(value) : null;\n if (!match || match[2] === '-00:00') {\n throw new BookingError(\n 'invalid',\n 'Expected local minute with explicit UTC offset',\n );\n }\n const time = Date.parse(value);\n if (!Number.isFinite(time)) {\n throw new BookingError('invalid', 'Invalid local timestamp');\n }\n const parts = Object.fromEntries(\n format.formatToParts(time).map(({ type, value }) => [type, value]),\n );\n const local = `${parts.year.padStart(4, '0')}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}`;\n if (local !== match[1] || parts.second !== '00') {\n throw new BookingError(\n 'invalid',\n 'Local timestamp and offset do not exist in this zone',\n );\n }\n return new Date(time).toISOString();\n}\n\n/** Resolve half-open wall time; a DST fold must name the intended offset. */\nexport function resolveBookingWindow(\n window: BookingWindow,\n): ResolvedBookingWindow {\n const format = formatter(window.zone);\n const startUtc = instant(window.start, format);\n const endUtc = instant(window.end, format);\n if (startUtc >= endUtc) {\n throw new BookingError(\n 'invalid',\n 'A booking window must have positive duration',\n );\n }\n return {\n zone: format.resolvedOptions().timeZone,\n start: window.start,\n end: window.end,\n startUtc,\n endUtc,\n };\n}\n\nexport function elapsedHour(window: ResolvedBookingWindow): boolean {\n return (\n Date.parse(window.endUtc) - Date.parse(window.startUtc) === 60 * MINUTE_MS\n );\n}\n\nfunction oneLocalDay(window: ResolvedBookingWindow): boolean {\n const startDate = window.start.slice(0, 10);\n const endDate = window.end.slice(0, 10);\n if (startDate === endDate) return true;\n if (!window.end.slice(10).startsWith('T00:00')) return false;\n const tomorrow = new Date(`${startDate}T00:00:00Z`);\n tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);\n return tomorrow.toISOString().slice(0, 10) === endDate;\n}\n\nexport interface DeclaredWindow extends ResolvedBookingWindow {\n capacity: number;\n supervision: boolean;\n}\n\nexport function declareWindows(\n zone: string,\n windows: readonly AvailabilityWindow[],\n): DeclaredWindow[] {\n formatter(zone);\n if (!Array.isArray(windows))\n throw new BookingError('invalid', 'Expected availability windows');\n const days = new Set<string>();\n const normalized = windows\n .map(({ start, end, capacity, supervision }) => {\n if (\n !Number.isSafeInteger(capacity) ||\n capacity < 1 ||\n typeof supervision !== 'boolean'\n ) {\n throw new BookingError(\n 'invalid',\n 'Availability requires positive integer capacity and a supervision flag',\n );\n }\n const window = resolveBookingWindow({ start, end, zone });\n const day = start.slice(0, 10);\n if (supervision) {\n if (!oneLocalDay(window) || days.has(day)) {\n throw new BookingError(\n 'invalid',\n 'Declare at most one supervision window per local day',\n );\n }\n days.add(day);\n }\n return { ...window, capacity, supervision };\n })\n .sort((a, b) => a.startUtc.localeCompare(b.startUtc));\n if (\n normalized.some(\n (window, i) => i > 0 && normalized[i - 1].endUtc > window.startUtc,\n )\n ) {\n throw new BookingError('invalid', 'Availability windows must not overlap');\n }\n return normalized;\n}\n\nexport interface Reservation {\n startUtc: string;\n endUtc: string;\n exclusive: boolean;\n}\n\nexport function supervisionDeclared(\n candidate: ResolvedBookingWindow,\n windows: readonly DeclaredWindow[],\n): boolean {\n return windows.some(\n (window) =>\n window.supervision &&\n candidate.start === window.start &&\n candidate.end === window.end &&\n candidate.zone === window.zone,\n );\n}\n\n/** Check every segment; endpoints belonging to ending windows are excluded. */\nexport function fitsCapacity(\n candidate: Reservation,\n declared: readonly DeclaredWindow[],\n occupied: readonly Reservation[],\n): boolean {\n type Delta = { capacity: number; shared: number; exclusive: number };\n const boundaries = new Map<string, Delta>();\n const add = (at: string, key: keyof Delta, delta: number) => {\n const event = boundaries.get(at) ?? {\n capacity: 0,\n shared: 0,\n exclusive: 0,\n };\n event[key] += delta;\n boundaries.set(at, event);\n };\n const sweep = (\n window: Reservation | DeclaredWindow,\n key: keyof Delta,\n units: number,\n ) => {\n if (\n window.startUtc >= candidate.endUtc ||\n window.endUtc <= candidate.startUtc\n )\n return;\n add(\n window.startUtc < candidate.startUtc\n ? candidate.startUtc\n : window.startUtc,\n key,\n units,\n );\n if (window.endUtc < candidate.endUtc) add(window.endUtc, key, -units);\n };\n\n // Seed the first segment even when there is no declared coverage there.\n add(candidate.startUtc, 'capacity', 0);\n for (const window of declared) sweep(window, 'capacity', window.capacity);\n for (const reservation of occupied)\n sweep(reservation, reservation.exclusive ? 'exclusive' : 'shared', 1);\n\n let capacity = 0;\n let shared = 0;\n let exclusive = 0;\n for (const at of [...boundaries.keys()].sort()) {\n const delta = boundaries.get(at);\n if (!delta) continue;\n // Apply ends and starts together before checking the segment beginning here.\n capacity += delta.capacity;\n shared += delta.shared;\n exclusive += delta.exclusive;\n if (\n capacity < 1 ||\n exclusive > 0 ||\n (candidate.exclusive ? shared > 0 : shared >= capacity)\n )\n return false;\n }\n return true;\n}\n","import { createHash, randomUUID } from 'node:crypto';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { BookingError } from './errors.js';\nimport type {\n AvailabilityInput,\n BookingAccess,\n BookingAuthority,\n BookingContext,\n BookingHistoryEvent,\n BookingKind,\n BookingOperation,\n BookingProposal,\n BookingReceipt,\n BookingReferences,\n BookingStatus,\n BookingView,\n ProposeBookingInput,\n RequestBookingInput,\n ResolvedBookingWindow,\n} from './types.js';\nimport {\n type DeclaredWindow,\n declareWindows,\n elapsedHour,\n fitsCapacity,\n type Reservation,\n resolveBookingWindow,\n supervisionDeclared,\n} from './windows.js';\n\ntype WriteOperation = Exclude<BookingOperation, 'read' | 'history'>;\n\ninterface BookingRow {\n id: string;\n offering_id: string;\n contact_id: string;\n project_id: string | null;\n kind: BookingKind;\n status: BookingStatus;\n proposal_id: string | null;\n assented_proposal_id: string | null;\n confirmed_proposal_id: string | null;\n}\n\ninterface Allocation extends Reservation {\n window: ResolvedBookingWindow;\n}\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nfunction id(value: string): string {\n if (typeof value !== 'string' || !UUID.test(value)) {\n throw new BookingError('invalid', 'Expected a UUID reference');\n }\n return value.toLowerCase();\n}\n\nfunction required(value: string, max = 200): string {\n if (\n typeof value !== 'string' ||\n value.trim().length === 0 ||\n value.length > max ||\n value.includes('\\0')\n ) {\n throw new BookingError(\n 'invalid',\n 'Expected a nonempty text value within the length limit',\n );\n }\n return value;\n}\n\nfunction identity(input: BookingContext): Readonly<BookingContext> {\n return Object.freeze({\n tenantId: id(input.tenantId),\n actorId: required(input.actorId),\n });\n}\n\nfunction digest(value: string): string {\n return createHash('sha256').update(value).digest('hex');\n}\n\nfunction operator(access: BookingAccess): void {\n if (!access.operator)\n throw new BookingError('forbidden', 'Operator access required');\n}\n\nfunction contact(access: BookingAccess, contactId: string): void {\n if (access.contactId !== contactId) {\n throw new BookingError(\n 'forbidden',\n 'Only the named customer contact may act for this booking',\n );\n }\n}\n\nfunction visible(access: BookingAccess, row: BookingRow): void {\n if (!access.operator && access.contactId !== row.contact_id) {\n throw new BookingError('not-found', 'Booking does not exist');\n }\n}\n\nfunction pending(row: BookingRow): void {\n if (row.status !== 'requested' && row.status !== 'proposed') {\n throw new BookingError('conflict', 'Booking is no longer pending');\n }\n}\n\nfunction declared(\n kind: BookingKind,\n window: ResolvedBookingWindow,\n availability: readonly DeclaredWindow[],\n): void {\n if (\n (kind === 'supervision-day' &&\n !supervisionDeclared(window, availability)) ||\n (kind === 'developer-hour' &&\n !availability.some((entry) => entry.zone === window.zone)) ||\n !fitsCapacity(\n { ...window, exclusive: kind === 'developer-hour' },\n availability,\n [],\n )\n ) {\n throw new BookingError(\n 'unavailable',\n 'The proposed window is outside declared availability',\n );\n }\n}\n\n/** Transactional PostgreSQL service. The application supplies trusted identity and authorization. */\nexport class BookingService {\n readonly #db: DatabaseInterface;\n readonly #authority: BookingAuthority;\n\n constructor(db: DatabaseInterface, authority: BookingAuthority) {\n this.#db = db;\n this.#authority = authority;\n }\n\n async configureOffering(\n context: BookingContext,\n key: string,\n input: { offeringId: string; kind: BookingKind; enabled: boolean },\n ): Promise<BookingReceipt> {\n const offeringId = id(input.offeringId);\n const { kind, enabled } = input;\n if (\n (kind !== 'developer-hour' && kind !== 'supervision-day') ||\n typeof enabled !== 'boolean'\n ) {\n throw new BookingError(\n 'invalid',\n 'Invalid offering kind or enabled flag',\n );\n }\n return this.#write(\n context,\n 'configure-offering',\n key,\n { offeringId, kind, enabled },\n operator,\n async (tx, ctx) => {\n const saved = await tx.query(\n `INSERT INTO smrt_booking_offerings (tenant_id, id, kind, enabled) VALUES (?, ?, ?, ?)\n ON CONFLICT (tenant_id, id) DO UPDATE SET enabled = EXCLUDED.enabled\n WHERE smrt_booking_offerings.kind = EXCLUDED.kind RETURNING id`,\n ctx.tenantId,\n offeringId,\n kind,\n enabled,\n );\n if (!saved.rows.length)\n throw new BookingError('conflict', 'An offering kind cannot change');\n return { id: offeringId };\n },\n );\n }\n\n async declareAvailability(\n context: BookingContext,\n key: string,\n input: AvailabilityInput,\n ): Promise<BookingReceipt> {\n const providerId = id(input.providerId);\n const windows = declareWindows(input.zone, input.windows);\n return this.#write(\n context,\n 'declare-availability',\n key,\n { providerId, windows },\n operator,\n async (tx, ctx) => {\n await tx.query(\n `INSERT INTO smrt_booking_resources (tenant_id, id, windows) VALUES (?, ?, '[]')\n ON CONFLICT (tenant_id, id) DO NOTHING`,\n ctx.tenantId,\n providerId,\n );\n await this.#resource(tx, ctx, providerId, true);\n operator(await this.#access(ctx, 'declare-availability'));\n await this.#verify(ctx, { providerId });\n const allocated = await this.#allocations(tx, ctx, providerId);\n if (\n allocated.some(\n (item, i) =>\n (!item.exclusive && !supervisionDeclared(item.window, windows)) ||\n !fitsCapacity(\n item,\n windows,\n allocated.filter((_, j) => j !== i),\n ),\n )\n ) {\n throw new BookingError(\n 'unavailable',\n 'Availability change would undercut confirmed reservations',\n );\n }\n await tx.query(\n 'UPDATE smrt_booking_resources SET windows = ? WHERE tenant_id = ? AND id = ?',\n JSON.stringify(windows),\n ctx.tenantId,\n providerId,\n );\n return { id: providerId };\n },\n );\n }\n\n async request(\n context: BookingContext,\n key: string,\n input: RequestBookingInput,\n ): Promise<BookingReceipt> {\n const offeringId = id(input.offeringId);\n const contactId = id(input.contactId);\n const projectId =\n input.projectId === undefined ? null : id(input.projectId);\n return this.#write(\n context,\n 'request',\n key,\n { offeringId, contactId, projectId },\n (access) => {\n if (!access.operator) contact(access, contactId);\n },\n async (tx, ctx) => {\n const { rows } = await tx.query(\n 'SELECT kind, enabled FROM smrt_booking_offerings WHERE tenant_id = ? AND id = ? FOR SHARE',\n ctx.tenantId,\n offeringId,\n );\n if (!rows[0]?.enabled)\n throw new BookingError('not-found', 'Offering is not available');\n const kind = rows[0].kind as BookingKind;\n if (kind === 'supervision-day' && !projectId) {\n throw new BookingError(\n 'invalid',\n 'A supervision request requires a project',\n );\n }\n await this.#verify(ctx, {\n contactId,\n projectId: projectId ?? undefined,\n });\n const bookingId = randomUUID();\n await tx.query(\n `INSERT INTO smrt_bookings (tenant_id, id, offering_id, contact_id, project_id, kind, status)\n VALUES (?, ?, ?, ?, ?, ?, 'requested')`,\n ctx.tenantId,\n bookingId,\n offeringId,\n contactId,\n projectId,\n kind,\n );\n return { id: bookingId };\n },\n { createsBooking: true },\n );\n }\n\n async propose(\n context: BookingContext,\n key: string,\n input: ProposeBookingInput,\n ): Promise<BookingReceipt> {\n const bookingId = id(input.bookingId);\n const providerId = id(input.providerId);\n const window = resolveBookingWindow(input.window);\n const terms = {\n revision: required(input.terms.revision),\n text: required(input.terms.text, 65_536),\n };\n const plan = input.approvedPlan;\n const approvedPlan =\n plan === undefined\n ? null\n : { id: id(plan.id), version: required(plan.version) };\n return this.#write(\n context,\n 'propose',\n key,\n { bookingId, providerId, window, terms, approvedPlan },\n operator,\n async (tx, ctx) => {\n const row = await this.#booking(tx, ctx, bookingId, true);\n pending(row);\n if (\n row.kind === 'supervision-day'\n ? !approvedPlan\n : approvedPlan || !elapsedHour(window)\n ) {\n throw new BookingError(\n 'invalid',\n 'Supervision requires a plan version; dedicated time requires one elapsed hour',\n );\n }\n await this.#verify(ctx, {\n ...this.#refs(row),\n providerId,\n approvedPlan: approvedPlan ?? undefined,\n });\n declared(\n row.kind,\n window,\n await this.#resource(tx, ctx, providerId, false),\n );\n const proposalId = randomUUID();\n await tx.query(\n `INSERT INTO smrt_booking_proposals (tenant_id, booking_id, id, provider_id, coverage, terms, approved_plan)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n ctx.tenantId,\n bookingId,\n proposalId,\n providerId,\n JSON.stringify(window),\n JSON.stringify(terms),\n approvedPlan && JSON.stringify(approvedPlan),\n );\n await tx.query(\n `UPDATE smrt_bookings SET status = 'proposed', proposal_id = ?, assented_proposal_id = NULL\n WHERE tenant_id = ? AND id = ?`,\n proposalId,\n ctx.tenantId,\n bookingId,\n );\n return { id: bookingId, proposalId };\n },\n { bookingId },\n );\n }\n\n async assent(\n context: BookingContext,\n key: string,\n input: { bookingId: string; proposalId: string },\n ): Promise<BookingReceipt> {\n const bookingId = id(input.bookingId);\n const proposalId = id(input.proposalId);\n return this.#write(\n context,\n 'assent',\n key,\n { bookingId, proposalId },\n async (access, tx, ctx) => {\n // Check contact ownership before exposing the existence of a replay.\n const row = await this.#booking(tx, ctx, bookingId, false);\n visible(access, row);\n contact(access, row.contact_id);\n },\n async (tx, ctx) => {\n const row = await this.#booking(tx, ctx, bookingId, true);\n contact(await this.#access(ctx, 'assent'), row.contact_id);\n this.#current(row, proposalId);\n if (row.assented_proposal_id)\n throw new BookingError(\n 'conflict',\n 'Customer assent is already recorded',\n );\n await this.#verify(ctx, this.#refs(row));\n await tx.query(\n 'UPDATE smrt_bookings SET assented_proposal_id = ? WHERE tenant_id = ? AND id = ?',\n proposalId,\n ctx.tenantId,\n bookingId,\n );\n return { id: bookingId, proposalId };\n },\n { bookingId },\n );\n }\n\n async confirm(\n context: BookingContext,\n key: string,\n input: { bookingId: string; proposalId: string },\n ): Promise<BookingReceipt> {\n const bookingId = id(input.bookingId);\n const proposalId = id(input.proposalId);\n return this.#write(\n context,\n 'confirm',\n key,\n { bookingId, proposalId },\n operator,\n async (tx, ctx) => {\n const row = await this.#booking(tx, ctx, bookingId, true);\n this.#current(row, proposalId);\n if (row.assented_proposal_id !== proposalId) {\n throw new BookingError(\n 'conflict',\n 'Exact proposal does not have customer assent',\n );\n }\n const proposal = await this.#proposal(tx, ctx, row, proposalId);\n const availability = await this.#resource(\n tx,\n ctx,\n proposal.providerId,\n true,\n );\n // Refresh after waiting for contested capacity; this does not lock external authorities.\n operator(await this.#access(ctx, 'confirm'));\n await this.#verify(ctx, {\n ...this.#refs(row),\n providerId: proposal.providerId,\n approvedPlan: proposal.approvedPlan ?? undefined,\n });\n const exclusive = row.kind === 'developer-hour';\n const window = proposal.window;\n declared(row.kind, window, availability);\n if (\n !fitsCapacity(\n { ...window, exclusive },\n availability,\n await this.#allocations(tx, ctx, proposal.providerId, window),\n )\n ) {\n throw new BookingError(\n 'unavailable',\n 'The proposed window has insufficient capacity',\n );\n }\n const inserted = await tx.query(\n `INSERT INTO smrt_booking_allocations\n (tenant_id, booking_id, proposal_id, provider_id, project_id, exclusive, start_at, end_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (tenant_id, project_id, start_at, end_at)\n WHERE released_at IS NULL AND project_id IS NOT NULL DO NOTHING RETURNING booking_id`,\n ctx.tenantId,\n bookingId,\n proposalId,\n proposal.providerId,\n exclusive ? null : row.project_id,\n exclusive,\n window.startUtc,\n window.endUtc,\n );\n if (!inserted.rows.length) {\n throw new BookingError(\n 'duplicate-project-window',\n 'Project already has a reservation for this window',\n );\n }\n await tx.query(\n `UPDATE smrt_bookings SET status = 'confirmed', confirmed_proposal_id = ?\n WHERE tenant_id = ? AND id = ?`,\n proposalId,\n ctx.tenantId,\n bookingId,\n );\n return { id: bookingId, proposalId };\n },\n { bookingId },\n );\n }\n\n decline(\n context: BookingContext,\n key: string,\n input: { bookingId: string; reason: string },\n ): Promise<BookingReceipt> {\n return this.#close(context, 'decline', key, input);\n }\n\n cancel(\n context: BookingContext,\n key: string,\n input: { bookingId: string; reason: string },\n ): Promise<BookingReceipt> {\n return this.#close(context, 'cancel', key, input);\n }\n\n async get(context: BookingContext, bookingId: string): Promise<BookingView> {\n const ctx = identity(context);\n const access = await this.#access(ctx, 'read');\n const row = await this.#booking(this.#db, ctx, id(bookingId), false);\n visible(access, row);\n return {\n id: row.id,\n offeringId: row.offering_id,\n contactId: row.contact_id,\n projectId: row.project_id,\n kind: row.kind,\n status: row.status,\n proposal: row.proposal_id\n ? await this.#proposal(this.#db, ctx, row, row.proposal_id)\n : null,\n assentedProposalId: row.assented_proposal_id,\n confirmedProposalId: row.confirmed_proposal_id,\n };\n }\n\n async getProposal(\n context: BookingContext,\n bookingId: string,\n proposalId: string,\n ): Promise<BookingProposal> {\n const ctx = identity(context);\n const access = await this.#access(ctx, 'read');\n const row = await this.#booking(this.#db, ctx, id(bookingId), false);\n visible(access, row);\n return this.#proposal(this.#db, ctx, row, id(proposalId));\n }\n\n async history(\n context: BookingContext,\n bookingId: string,\n ): Promise<BookingHistoryEvent[]> {\n const ctx = identity(context);\n operator(await this.#access(ctx, 'history'));\n await this.#booking(this.#db, ctx, id(bookingId), false);\n const { rows } = await this.#db.query(\n `SELECT operation, actor_id, result->>'proposalId' AS proposal_id, reason, at\n FROM smrt_booking_commands WHERE tenant_id = ? AND booking_id = ? ORDER BY at, sequence`,\n ctx.tenantId,\n id(bookingId),\n );\n return rows.map((row) => ({\n operation: row.operation as BookingOperation,\n actorId: row.actor_id,\n proposalId: row.proposal_id,\n reason: row.reason,\n at: new Date(row.at).toISOString(),\n }));\n }\n\n async #close(\n context: BookingContext,\n operation: 'decline' | 'cancel',\n key: string,\n input: { bookingId: string; reason: string },\n ): Promise<BookingReceipt> {\n const bookingId = id(input.bookingId);\n const reason = required(input.reason, 2_048);\n return this.#write(\n context,\n operation,\n key,\n { bookingId, reason },\n operator,\n async (tx, ctx) => {\n const row = await this.#booking(tx, ctx, bookingId, true);\n if (\n operation === 'cancel' &&\n row.confirmed_proposal_id &&\n row.status === 'confirmed'\n ) {\n const proposal = await this.#proposal(\n tx,\n ctx,\n row,\n row.confirmed_proposal_id,\n );\n await this.#resource(tx, ctx, proposal.providerId, true);\n operator(await this.#access(ctx, 'cancel'));\n const released = await tx.query(\n `UPDATE smrt_booking_allocations SET released_at = clock_timestamp()\n WHERE tenant_id = ? AND booking_id = ? AND released_at IS NULL RETURNING booking_id`,\n ctx.tenantId,\n bookingId,\n );\n if (!released.rows.length)\n throw new BookingError(\n 'conflict',\n 'Confirmed booking has no active allocation',\n );\n } else {\n pending(row);\n }\n await tx.query(\n 'UPDATE smrt_bookings SET status = ? WHERE tenant_id = ? AND id = ?',\n operation === 'cancel' ? 'cancelled' : 'declined',\n ctx.tenantId,\n bookingId,\n );\n return {\n id: bookingId,\n ...(row.proposal_id ? { proposalId: row.proposal_id } : {}),\n };\n },\n { bookingId, reason },\n );\n }\n\n async #access(\n context: Readonly<BookingContext>,\n operation: BookingOperation,\n ): Promise<BookingAccess> {\n const access = await this.#authority.access(context, operation);\n if (!access) throw new BookingError('forbidden', 'Booking access denied');\n return {\n operator: access.operator === true,\n contactId:\n access.contactId === undefined ? undefined : id(access.contactId),\n };\n }\n\n async #verify(\n ctx: Readonly<BookingContext>,\n refs: BookingReferences,\n ): Promise<void> {\n // Use a detached copy so a host callback cannot change the version being verified and saved.\n if (\n (await this.#authority.verifyReferences(ctx, structuredClone(refs))) !==\n true\n ) {\n throw new BookingError(\n 'forbidden',\n 'Booking references or approval could not be verified',\n );\n }\n }\n\n #refs(row: BookingRow): BookingReferences {\n return {\n contactId: row.contact_id,\n projectId: row.project_id ?? undefined,\n };\n }\n\n async #booking(\n tx: DatabaseInterface,\n ctx: Readonly<BookingContext>,\n bookingId: string,\n lock: boolean,\n ): Promise<BookingRow> {\n const { rows } = await tx.query(\n `SELECT * FROM smrt_bookings WHERE tenant_id = ? AND id = ?${lock ? ' FOR UPDATE' : ''}`,\n ctx.tenantId,\n bookingId,\n );\n if (!rows[0]) throw new BookingError('not-found', 'Booking does not exist');\n return rows[0] as BookingRow;\n }\n\n #current(row: BookingRow, proposalId: string): void {\n if (row.status !== 'proposed' || row.proposal_id !== proposalId) {\n throw new BookingError(\n 'conflict',\n 'Booking does not have this current proposal',\n );\n }\n }\n\n async #proposal(\n tx: DatabaseInterface,\n ctx: Readonly<BookingContext>,\n booking: BookingRow,\n proposalId: string,\n ): Promise<BookingProposal> {\n const { rows } = await tx.query(\n `SELECT provider_id, coverage::text, terms::text, approved_plan::text FROM smrt_booking_proposals\n WHERE tenant_id = ? AND booking_id = ? AND id = ?`,\n ctx.tenantId,\n booking.id,\n proposalId,\n );\n const row = rows[0];\n if (!row) throw new BookingError('not-found', 'Proposal does not exist');\n return {\n id: proposalId,\n providerId: row.provider_id,\n window: JSON.parse(row.coverage),\n terms: JSON.parse(row.terms),\n approvedPlan:\n row.approved_plan === null ? null : JSON.parse(row.approved_plan),\n };\n }\n\n async #resource(\n tx: DatabaseInterface,\n ctx: Readonly<BookingContext>,\n providerId: string,\n lock: boolean,\n ): Promise<DeclaredWindow[]> {\n const { rows } = await tx.query(\n `SELECT windows::text FROM smrt_booking_resources WHERE tenant_id = ? AND id = ?${lock ? ' FOR UPDATE' : ''}`,\n ctx.tenantId,\n providerId,\n );\n if (!rows[0])\n throw new BookingError(\n 'not-found',\n 'Provider availability is not configured',\n );\n return JSON.parse(rows[0].windows);\n }\n\n async #allocations(\n tx: DatabaseInterface,\n ctx: Readonly<BookingContext>,\n providerId: string,\n window?: ResolvedBookingWindow,\n ): Promise<Allocation[]> {\n const { rows } = await tx.query(\n `SELECT a.exclusive, p.coverage::text FROM smrt_booking_allocations a\n JOIN smrt_booking_proposals p ON p.tenant_id = a.tenant_id AND p.booking_id = a.booking_id AND p.id = a.proposal_id\n WHERE a.tenant_id = ? AND a.provider_id = ? AND a.released_at IS NULL${window ? ' AND a.start_at < ? AND a.end_at > ?' : ''}`,\n ctx.tenantId,\n providerId,\n ...(window ? [window.endUtc, window.startUtc] : []),\n );\n return rows.map((row) => {\n const snapshot = JSON.parse(row.coverage) as ResolvedBookingWindow;\n return { ...snapshot, window: snapshot, exclusive: row.exclusive };\n });\n }\n\n async #write(\n context: BookingContext,\n operation: WriteOperation,\n key: string,\n canonical: object,\n permit: (\n access: BookingAccess,\n tx: DatabaseInterface,\n ctx: Readonly<BookingContext>,\n ) => void | Promise<void>,\n work: (\n tx: DatabaseInterface,\n ctx: Readonly<BookingContext>,\n ) => Promise<BookingReceipt>,\n options: {\n bookingId?: string;\n reason?: string;\n createsBooking?: boolean;\n } = {},\n ): Promise<BookingReceipt> {\n const ctx = identity(context);\n const keyHash = digest(required(key));\n const payloadHash = digest(JSON.stringify(canonical));\n if (!this.#db.transaction)\n throw new TypeError('Bookings requires a PostgreSQL transaction');\n return this.#db.transaction(async (tx) => {\n await tx.query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED');\n await permit(await this.#access(ctx, operation), tx, ctx);\n const inserted = await tx.query(\n `INSERT INTO smrt_booking_commands (tenant_id, operation, key_hash, actor_id, payload_hash)\n VALUES (?, ?, ?, ?, ?) ON CONFLICT (tenant_id, operation, key_hash) DO NOTHING RETURNING key_hash`,\n ctx.tenantId,\n operation,\n keyHash,\n ctx.actorId,\n payloadHash,\n );\n if (!inserted.rows.length) {\n await permit(await this.#access(ctx, operation), tx, ctx);\n const { rows } = await tx.query(\n `SELECT actor_id, payload_hash, result::text FROM smrt_booking_commands\n WHERE tenant_id = ? AND operation = ? AND key_hash = ?`,\n ctx.tenantId,\n operation,\n keyHash,\n );\n const prior = rows[0];\n if (\n !prior ||\n prior.actor_id !== ctx.actorId ||\n prior.payload_hash !== payloadHash ||\n !prior.result\n ) {\n throw new BookingError(\n 'conflict',\n 'Idempotency key is bound to a different command',\n );\n }\n return JSON.parse(prior.result) as BookingReceipt;\n }\n const result = await work(tx, ctx);\n await tx.query(\n `UPDATE smrt_booking_commands SET booking_id = ?, result = ?, reason = ?, at = clock_timestamp()\n WHERE tenant_id = ? AND operation = ? AND key_hash = ?`,\n options.bookingId ?? (options.createsBooking ? result.id : null),\n JSON.stringify(result),\n options.reason ?? null,\n ctx.tenantId,\n operation,\n keyHash,\n );\n return result;\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["value"],"sources":["../src/errors.ts","../src/migration.ts","../src/windows.ts","../src/service.ts"],"sourcesContent":["/** A service rejection suitable for mapping to a host response. */\nexport class BookingError extends Error {\n constructor(\n readonly code:\n | 'invalid'\n | 'forbidden'\n | 'not-found'\n | 'conflict'\n | 'unavailable'\n | 'duplicate-project-window',\n message: string,\n ) {\n super(message);\n this.name = 'BookingError';\n }\n}\n","import type { DatabaseInterface } from '@happyvertical/sql';\n\nconst statements = [\n `CREATE TABLE smrt_booking_offerings (\n tenant_id uuid NOT NULL,\n id uuid NOT NULL,\n kind text NOT NULL CHECK (kind IN ('developer-hour', 'supervision-day')),\n enabled boolean NOT NULL,\n PRIMARY KEY (tenant_id, id)\n )`,\n `CREATE TABLE smrt_booking_resources (\n tenant_id uuid NOT NULL,\n id uuid NOT NULL,\n windows jsonb NOT NULL,\n PRIMARY KEY (tenant_id, id)\n )`,\n `CREATE TABLE smrt_bookings (\n tenant_id uuid NOT NULL,\n id uuid NOT NULL,\n offering_id uuid NOT NULL,\n contact_id uuid NOT NULL,\n project_id uuid,\n kind text NOT NULL CHECK (kind IN ('developer-hour', 'supervision-day')),\n status text NOT NULL CHECK (status IN ('requested', 'proposed', 'confirmed', 'declined', 'cancelled')),\n proposal_id uuid,\n assented_proposal_id uuid,\n confirmed_proposal_id uuid,\n PRIMARY KEY (tenant_id, id),\n FOREIGN KEY (tenant_id, offering_id) REFERENCES smrt_booking_offerings (tenant_id, id),\n CHECK (kind <> 'supervision-day' OR project_id IS NOT NULL),\n CHECK (status <> 'confirmed' OR confirmed_proposal_id IS NOT NULL),\n CHECK (assented_proposal_id IS NULL OR assented_proposal_id IS NOT DISTINCT FROM proposal_id),\n CHECK (confirmed_proposal_id IS NULL OR confirmed_proposal_id IS NOT DISTINCT FROM assented_proposal_id)\n )`,\n `CREATE TABLE smrt_booking_proposals (\n tenant_id uuid NOT NULL,\n booking_id uuid NOT NULL,\n id uuid NOT NULL,\n provider_id uuid NOT NULL,\n coverage jsonb NOT NULL,\n terms jsonb NOT NULL,\n approved_plan jsonb,\n PRIMARY KEY (tenant_id, booking_id, id),\n FOREIGN KEY (tenant_id, booking_id) REFERENCES smrt_bookings (tenant_id, id),\n FOREIGN KEY (tenant_id, provider_id) REFERENCES smrt_booking_resources (tenant_id, id)\n )`,\n `ALTER TABLE smrt_bookings\n ADD FOREIGN KEY (tenant_id, id, proposal_id) REFERENCES smrt_booking_proposals (tenant_id, booking_id, id)`,\n `CREATE TABLE smrt_booking_allocations (\n tenant_id uuid NOT NULL,\n booking_id uuid NOT NULL,\n proposal_id uuid NOT NULL,\n provider_id uuid NOT NULL,\n project_id uuid,\n exclusive boolean NOT NULL,\n start_at timestamptz NOT NULL,\n end_at timestamptz NOT NULL CHECK (end_at > start_at),\n released_at timestamptz,\n PRIMARY KEY (tenant_id, booking_id),\n FOREIGN KEY (tenant_id, booking_id, proposal_id)\n REFERENCES smrt_booking_proposals (tenant_id, booking_id, id),\n FOREIGN KEY (tenant_id, provider_id) REFERENCES smrt_booking_resources (tenant_id, id),\n CHECK (exclusive = (project_id IS NULL))\n )`,\n `CREATE INDEX smrt_booking_active_provider ON smrt_booking_allocations (tenant_id, provider_id, start_at)\n WHERE released_at IS NULL`,\n `CREATE UNIQUE INDEX smrt_booking_active_project_window\n ON smrt_booking_allocations (tenant_id, project_id, start_at, end_at)\n WHERE released_at IS NULL AND project_id IS NOT NULL`,\n `CREATE TABLE smrt_booking_commands (\n sequence bigint GENERATED ALWAYS AS IDENTITY UNIQUE,\n tenant_id uuid NOT NULL,\n operation text NOT NULL,\n key_hash text NOT NULL,\n actor_id text NOT NULL,\n payload_hash text NOT NULL,\n booking_id uuid,\n result jsonb,\n reason text,\n at timestamptz NOT NULL DEFAULT clock_timestamp(),\n PRIMARY KEY (tenant_id, operation, key_hash),\n FOREIGN KEY (tenant_id, booking_id) REFERENCES smrt_bookings (tenant_id, id)\n )`,\n `CREATE INDEX smrt_booking_history ON smrt_booking_commands (tenant_id, booking_id, sequence)`,\n];\n\n/** Initial PostgreSQL schema. Register once in the host deployment's migration ledger. */\nexport async function migrateBookingsV1(db: DatabaseInterface): Promise<void> {\n if (!db.transaction)\n throw new TypeError('Bookings requires a PostgreSQL transaction');\n await db.transaction(async (tx) => {\n for (const statement of statements) await tx.query(statement);\n });\n}\n","import { BookingError } from './errors.js';\nimport type {\n AvailabilityWindow,\n BookingWindow,\n DeclaredAvailabilityWindow,\n ResolvedBookingWindow,\n} from './types.js';\n\nconst WALL = /^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2})([+-]\\d{2}:\\d{2})$/;\nconst MINUTE_MS = 60_000;\n\nfunction formatter(zone: string): Intl.DateTimeFormat {\n // Intl also accepts offsets as time zone names. These are not IANA zones.\n if (typeof zone !== 'string' || !/^[A-Za-z][A-Za-z0-9_+/-]*$/.test(zone)) {\n throw new BookingError('invalid', 'An IANA time zone is required');\n }\n try {\n return new Intl.DateTimeFormat('en-GB', {\n timeZone: zone,\n calendar: 'iso8601',\n numberingSystem: 'latn',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hourCycle: 'h23',\n });\n } catch {\n throw new BookingError('invalid', 'Unknown IANA time zone');\n }\n}\n\nfunction instant(value: string, format: Intl.DateTimeFormat): string {\n const match = typeof value === 'string' ? WALL.exec(value) : null;\n if (!match || match[2] === '-00:00') {\n throw new BookingError(\n 'invalid',\n 'Expected local minute with explicit UTC offset',\n );\n }\n const time = Date.parse(value);\n if (!Number.isFinite(time)) {\n throw new BookingError('invalid', 'Invalid local timestamp');\n }\n const parts = Object.fromEntries(\n format.formatToParts(time).map(({ type, value }) => [type, value]),\n );\n const local = `${parts.year.padStart(4, '0')}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}`;\n if (local !== match[1] || parts.second !== '00') {\n throw new BookingError(\n 'invalid',\n 'Local timestamp and offset do not exist in this zone',\n );\n }\n return new Date(time).toISOString();\n}\n\n/** Resolve half-open wall time; a DST fold must name the intended offset. */\nexport function resolveBookingWindow(\n window: BookingWindow,\n): ResolvedBookingWindow {\n const format = formatter(window.zone);\n const startUtc = instant(window.start, format);\n const endUtc = instant(window.end, format);\n if (startUtc >= endUtc) {\n throw new BookingError(\n 'invalid',\n 'A booking window must have positive duration',\n );\n }\n return {\n zone: format.resolvedOptions().timeZone,\n start: window.start,\n end: window.end,\n startUtc,\n endUtc,\n };\n}\n\nexport function elapsedHour(window: ResolvedBookingWindow): boolean {\n return (\n Date.parse(window.endUtc) - Date.parse(window.startUtc) === 60 * MINUTE_MS\n );\n}\n\nfunction oneLocalDay(window: ResolvedBookingWindow): boolean {\n const startDate = window.start.slice(0, 10);\n const endDate = window.end.slice(0, 10);\n if (startDate === endDate) return true;\n if (!window.end.slice(10).startsWith('T00:00')) return false;\n const tomorrow = new Date(`${startDate}T00:00:00Z`);\n tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);\n return tomorrow.toISOString().slice(0, 10) === endDate;\n}\n\nexport function declareWindows(\n zone: string,\n windows: readonly AvailabilityWindow[],\n): DeclaredAvailabilityWindow[] {\n formatter(zone);\n if (!Array.isArray(windows))\n throw new BookingError('invalid', 'Expected availability windows');\n const days = new Set<string>();\n const normalized = windows\n .map(({ start, end, capacity, supervision }) => {\n if (\n !Number.isSafeInteger(capacity) ||\n capacity < 1 ||\n typeof supervision !== 'boolean'\n ) {\n throw new BookingError(\n 'invalid',\n 'Availability requires positive integer capacity and a supervision flag',\n );\n }\n const window = resolveBookingWindow({ start, end, zone });\n const day = start.slice(0, 10);\n if (supervision) {\n if (!oneLocalDay(window) || days.has(day)) {\n throw new BookingError(\n 'invalid',\n 'Declare at most one supervision window per local day',\n );\n }\n days.add(day);\n }\n return { ...window, capacity, supervision };\n })\n .sort((a, b) => a.startUtc.localeCompare(b.startUtc));\n if (\n normalized.some(\n (window, i) => i > 0 && normalized[i - 1].endUtc > window.startUtc,\n )\n ) {\n throw new BookingError('invalid', 'Availability windows must not overlap');\n }\n return normalized;\n}\n\nexport interface Reservation {\n startUtc: string;\n endUtc: string;\n exclusive: boolean;\n}\n\nexport function supervisionDeclared(\n candidate: ResolvedBookingWindow,\n windows: readonly DeclaredAvailabilityWindow[],\n): boolean {\n return windows.some(\n (window) =>\n window.supervision &&\n candidate.start === window.start &&\n candidate.end === window.end &&\n candidate.zone === window.zone,\n );\n}\n\n/** Check every segment; endpoints belonging to ending windows are excluded. */\nexport function fitsCapacity(\n candidate: Reservation,\n declared: readonly DeclaredAvailabilityWindow[],\n occupied: readonly Reservation[],\n): boolean {\n type Delta = { capacity: number; shared: number; exclusive: number };\n const boundaries = new Map<string, Delta>();\n const add = (at: string, key: keyof Delta, delta: number) => {\n const event = boundaries.get(at) ?? {\n capacity: 0,\n shared: 0,\n exclusive: 0,\n };\n event[key] += delta;\n boundaries.set(at, event);\n };\n const sweep = (\n window: Reservation | DeclaredAvailabilityWindow,\n key: keyof Delta,\n units: number,\n ) => {\n if (\n window.startUtc >= candidate.endUtc ||\n window.endUtc <= candidate.startUtc\n )\n return;\n add(\n window.startUtc < candidate.startUtc\n ? candidate.startUtc\n : window.startUtc,\n key,\n units,\n );\n if (window.endUtc < candidate.endUtc) add(window.endUtc, key, -units);\n };\n\n // Seed the first segment even when there is no declared coverage there.\n add(candidate.startUtc, 'capacity', 0);\n for (const window of declared) sweep(window, 'capacity', window.capacity);\n for (const reservation of occupied)\n sweep(reservation, reservation.exclusive ? 'exclusive' : 'shared', 1);\n\n let capacity = 0;\n let shared = 0;\n let exclusive = 0;\n for (const at of [...boundaries.keys()].sort()) {\n const delta = boundaries.get(at);\n if (!delta) continue;\n // Apply ends and starts together before checking the segment beginning here.\n capacity += delta.capacity;\n shared += delta.shared;\n exclusive += delta.exclusive;\n if (\n capacity < 1 ||\n exclusive > 0 ||\n (candidate.exclusive ? shared > 0 : shared >= capacity)\n )\n return false;\n }\n return true;\n}\n","import { createHash, randomUUID } from 'node:crypto';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { BookingError } from './errors.js';\nimport type {\n AvailabilityInput,\n BookingAccess,\n BookingAuthority,\n BookingContext,\n BookingHistoryEvent,\n BookingKind,\n BookingOffering,\n BookingOperation,\n BookingPage,\n BookingPageOptions,\n BookingProposal,\n BookingReceipt,\n BookingReferences,\n BookingStatus,\n BookingView,\n DeclaredAvailabilityWindow,\n ListBookingsOptions,\n ProposeBookingInput,\n ProviderAvailability,\n RequestBookingInput,\n ResolvedBookingWindow,\n} from './types.js';\nimport {\n declareWindows,\n elapsedHour,\n fitsCapacity,\n type Reservation,\n resolveBookingWindow,\n supervisionDeclared,\n} from './windows.js';\n\ntype WriteOperation = Exclude<BookingOperation, 'read' | 'history'>;\n\ninterface BookingRow {\n id: string;\n offering_id: string;\n contact_id: string;\n project_id: string | null;\n kind: BookingKind;\n status: BookingStatus;\n proposal_id: string | null;\n assented_proposal_id: string | null;\n confirmed_proposal_id: string | null;\n}\n\ninterface ProposalRow {\n provider_id: string;\n coverage: string;\n terms: string;\n approved_plan: string | null;\n}\n\ninterface Allocation extends Reservation {\n window: ResolvedBookingWindow;\n}\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nfunction id(value: string): string {\n if (typeof value !== 'string' || !UUID.test(value)) {\n throw new BookingError('invalid', 'Expected a UUID reference');\n }\n return value.toLowerCase();\n}\n\nfunction pageBounds({ afterId, limit = 50 }: BookingPageOptions) {\n if (!Number.isInteger(limit) || limit < 1 || limit > 100) {\n throw new BookingError('invalid', 'Page limit must be between 1 and 100');\n }\n return { after: afterId === undefined ? null : id(afterId), limit };\n}\n\nfunction page<T>(\n rows: T[],\n limit: number,\n cursor: (item: T) => string,\n): BookingPage<T> {\n const items = rows.slice(0, limit);\n return {\n items,\n nextCursor: rows.length > limit ? cursor(rows[limit - 1]) : null,\n };\n}\n\nfunction bookingView(\n row: BookingRow,\n proposal: BookingProposal | null,\n): BookingView {\n return {\n id: row.id,\n offeringId: row.offering_id,\n contactId: row.contact_id,\n projectId: row.project_id,\n kind: row.kind,\n status: row.status,\n proposal,\n assentedProposalId: row.assented_proposal_id,\n confirmedProposalId: row.confirmed_proposal_id,\n };\n}\n\nfunction proposalView(proposalId: string, row: ProposalRow): BookingProposal {\n return {\n id: proposalId,\n providerId: row.provider_id,\n window: JSON.parse(row.coverage),\n terms: JSON.parse(row.terms),\n approvedPlan:\n row.approved_plan === null ? null : JSON.parse(row.approved_plan),\n };\n}\n\nfunction required(value: string, max = 200): string {\n if (\n typeof value !== 'string' ||\n value.trim().length === 0 ||\n value.length > max ||\n value.includes('\\0')\n ) {\n throw new BookingError(\n 'invalid',\n 'Expected a nonempty text value within the length limit',\n );\n }\n return value;\n}\n\nfunction identity(input: BookingContext): Readonly<BookingContext> {\n return Object.freeze({\n tenantId: id(input.tenantId),\n actorId: required(input.actorId),\n });\n}\n\nfunction digest(value: string): string {\n return createHash('sha256').update(value).digest('hex');\n}\n\nfunction operator(access: BookingAccess): void {\n if (!access.operator)\n throw new BookingError('forbidden', 'Operator access required');\n}\n\nfunction contact(access: BookingAccess, contactId: string): void {\n if (access.contactId !== contactId) {\n throw new BookingError(\n 'forbidden',\n 'Only the named customer contact may act for this booking',\n );\n }\n}\n\nfunction visible(access: BookingAccess, row: BookingRow): void {\n if (!access.operator && access.contactId !== row.contact_id) {\n throw new BookingError('not-found', 'Booking does not exist');\n }\n}\n\nfunction pending(row: BookingRow): void {\n if (row.status !== 'requested' && row.status !== 'proposed') {\n throw new BookingError('conflict', 'Booking is no longer pending');\n }\n}\n\nfunction declared(\n kind: BookingKind,\n window: ResolvedBookingWindow,\n availability: readonly DeclaredAvailabilityWindow[],\n): void {\n if (\n (kind === 'supervision-day' &&\n !supervisionDeclared(window, availability)) ||\n (kind === 'developer-hour' &&\n !availability.some((entry) => entry.zone === window.zone)) ||\n !fitsCapacity(\n { ...window, exclusive: kind === 'developer-hour' },\n availability,\n [],\n )\n ) {\n throw new BookingError(\n 'unavailable',\n 'The proposed window is outside declared availability',\n );\n }\n}\n\n/** Transactional PostgreSQL service. The application supplies trusted identity and authorization. */\nexport class BookingService {\n readonly #db: DatabaseInterface;\n readonly #authority: BookingAuthority;\n\n constructor(db: DatabaseInterface, authority: BookingAuthority) {\n this.#db = db;\n this.#authority = authority;\n }\n\n async configureOffering(\n context: BookingContext,\n key: string,\n input: { offeringId: string; kind: BookingKind; enabled: boolean },\n ): Promise<BookingReceipt> {\n const offeringId = id(input.offeringId);\n const { kind, enabled } = input;\n if (\n (kind !== 'developer-hour' && kind !== 'supervision-day') ||\n typeof enabled !== 'boolean'\n ) {\n throw new BookingError(\n 'invalid',\n 'Invalid offering kind or enabled flag',\n );\n }\n return this.#write(\n context,\n 'configure-offering',\n key,\n { offeringId, kind, enabled },\n operator,\n async (tx, ctx) => {\n const saved = await tx.query(\n `INSERT INTO smrt_booking_offerings (tenant_id, id, kind, enabled) VALUES (?, ?, ?, ?)\n ON CONFLICT (tenant_id, id) DO UPDATE SET enabled = EXCLUDED.enabled\n WHERE smrt_booking_offerings.kind = EXCLUDED.kind RETURNING id`,\n ctx.tenantId,\n offeringId,\n kind,\n enabled,\n );\n if (!saved.rows.length)\n throw new BookingError('conflict', 'An offering kind cannot change');\n return { id: offeringId };\n },\n );\n }\n\n async declareAvailability(\n context: BookingContext,\n key: string,\n input: AvailabilityInput,\n ): Promise<BookingReceipt> {\n const providerId = id(input.providerId);\n const windows = declareWindows(input.zone, input.windows);\n return this.#write(\n context,\n 'declare-availability',\n key,\n { providerId, windows },\n operator,\n async (tx, ctx) => {\n await tx.query(\n `INSERT INTO smrt_booking_resources (tenant_id, id, windows) VALUES (?, ?, '[]')\n ON CONFLICT (tenant_id, id) DO NOTHING`,\n ctx.tenantId,\n providerId,\n );\n await this.#resource(tx, ctx, providerId, true);\n operator(await this.#access(ctx, 'declare-availability'));\n await this.#verify(ctx, { providerId });\n const allocated = await this.#allocations(tx, ctx, providerId);\n if (\n allocated.some(\n (item, i) =>\n (!item.exclusive && !supervisionDeclared(item.window, windows)) ||\n !fitsCapacity(\n item,\n windows,\n allocated.filter((_, j) => j !== i),\n ),\n )\n ) {\n throw new BookingError(\n 'unavailable',\n 'Availability change would undercut confirmed reservations',\n );\n }\n await tx.query(\n 'UPDATE smrt_booking_resources SET windows = ? WHERE tenant_id = ? AND id = ?',\n JSON.stringify(windows),\n ctx.tenantId,\n providerId,\n );\n return { id: providerId };\n },\n );\n }\n\n async request(\n context: BookingContext,\n key: string,\n input: RequestBookingInput,\n ): Promise<BookingReceipt> {\n const offeringId = id(input.offeringId);\n const contactId = id(input.contactId);\n const projectId =\n input.projectId === undefined ? null : id(input.projectId);\n return this.#write(\n context,\n 'request',\n key,\n { offeringId, contactId, projectId },\n (access) => {\n if (!access.operator) contact(access, contactId);\n },\n async (tx, ctx) => {\n const { rows } = await tx.query(\n 'SELECT kind, enabled FROM smrt_booking_offerings WHERE tenant_id = ? AND id = ? FOR SHARE',\n ctx.tenantId,\n offeringId,\n );\n if (!rows[0]?.enabled)\n throw new BookingError('not-found', 'Offering is not available');\n const kind = rows[0].kind as BookingKind;\n if (kind === 'supervision-day' && !projectId) {\n throw new BookingError(\n 'invalid',\n 'A supervision request requires a project',\n );\n }\n await this.#verify(ctx, {\n contactId,\n projectId: projectId ?? undefined,\n });\n const bookingId = randomUUID();\n await tx.query(\n `INSERT INTO smrt_bookings (tenant_id, id, offering_id, contact_id, project_id, kind, status)\n VALUES (?, ?, ?, ?, ?, ?, 'requested')`,\n ctx.tenantId,\n bookingId,\n offeringId,\n contactId,\n projectId,\n kind,\n );\n return { id: bookingId };\n },\n { createsBooking: true },\n );\n }\n\n async propose(\n context: BookingContext,\n key: string,\n input: ProposeBookingInput,\n ): Promise<BookingReceipt> {\n const bookingId = id(input.bookingId);\n const providerId = id(input.providerId);\n const window = resolveBookingWindow(input.window);\n const terms = {\n revision: required(input.terms.revision),\n text: required(input.terms.text, 65_536),\n };\n const plan = input.approvedPlan;\n const approvedPlan =\n plan === undefined\n ? null\n : { id: id(plan.id), version: required(plan.version) };\n return this.#write(\n context,\n 'propose',\n key,\n { bookingId, providerId, window, terms, approvedPlan },\n operator,\n async (tx, ctx) => {\n const row = await this.#booking(tx, ctx, bookingId, true);\n pending(row);\n if (\n row.kind === 'supervision-day'\n ? !approvedPlan\n : approvedPlan || !elapsedHour(window)\n ) {\n throw new BookingError(\n 'invalid',\n 'Supervision requires a plan version; dedicated time requires one elapsed hour',\n );\n }\n await this.#verify(ctx, {\n ...this.#refs(row),\n providerId,\n approvedPlan: approvedPlan ?? undefined,\n });\n declared(\n row.kind,\n window,\n await this.#resource(tx, ctx, providerId, false),\n );\n const proposalId = randomUUID();\n await tx.query(\n `INSERT INTO smrt_booking_proposals (tenant_id, booking_id, id, provider_id, coverage, terms, approved_plan)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n ctx.tenantId,\n bookingId,\n proposalId,\n providerId,\n JSON.stringify(window),\n JSON.stringify(terms),\n approvedPlan && JSON.stringify(approvedPlan),\n );\n await tx.query(\n `UPDATE smrt_bookings SET status = 'proposed', proposal_id = ?, assented_proposal_id = NULL\n WHERE tenant_id = ? AND id = ?`,\n proposalId,\n ctx.tenantId,\n bookingId,\n );\n return { id: bookingId, proposalId };\n },\n { bookingId },\n );\n }\n\n async assent(\n context: BookingContext,\n key: string,\n input: { bookingId: string; proposalId: string },\n ): Promise<BookingReceipt> {\n const bookingId = id(input.bookingId);\n const proposalId = id(input.proposalId);\n return this.#write(\n context,\n 'assent',\n key,\n { bookingId, proposalId },\n async (access, tx, ctx) => {\n // Check contact ownership before exposing the existence of a replay.\n const row = await this.#booking(tx, ctx, bookingId, false);\n visible(access, row);\n contact(access, row.contact_id);\n },\n async (tx, ctx) => {\n const row = await this.#booking(tx, ctx, bookingId, true);\n contact(await this.#access(ctx, 'assent'), row.contact_id);\n this.#current(row, proposalId);\n if (row.assented_proposal_id)\n throw new BookingError(\n 'conflict',\n 'Customer assent is already recorded',\n );\n await this.#verify(ctx, this.#refs(row));\n await tx.query(\n 'UPDATE smrt_bookings SET assented_proposal_id = ? WHERE tenant_id = ? AND id = ?',\n proposalId,\n ctx.tenantId,\n bookingId,\n );\n return { id: bookingId, proposalId };\n },\n { bookingId },\n );\n }\n\n async confirm(\n context: BookingContext,\n key: string,\n input: { bookingId: string; proposalId: string },\n ): Promise<BookingReceipt> {\n const bookingId = id(input.bookingId);\n const proposalId = id(input.proposalId);\n return this.#write(\n context,\n 'confirm',\n key,\n { bookingId, proposalId },\n operator,\n async (tx, ctx) => {\n const row = await this.#booking(tx, ctx, bookingId, true);\n this.#current(row, proposalId);\n if (row.assented_proposal_id !== proposalId) {\n throw new BookingError(\n 'conflict',\n 'Exact proposal does not have customer assent',\n );\n }\n const proposal = await this.#proposal(tx, ctx, row, proposalId);\n const availability = await this.#resource(\n tx,\n ctx,\n proposal.providerId,\n true,\n );\n // Refresh after waiting for contested capacity; this does not lock external authorities.\n operator(await this.#access(ctx, 'confirm'));\n await this.#verify(ctx, {\n ...this.#refs(row),\n providerId: proposal.providerId,\n approvedPlan: proposal.approvedPlan ?? undefined,\n });\n const exclusive = row.kind === 'developer-hour';\n const window = proposal.window;\n declared(row.kind, window, availability);\n if (\n !fitsCapacity(\n { ...window, exclusive },\n availability,\n await this.#allocations(tx, ctx, proposal.providerId, window),\n )\n ) {\n throw new BookingError(\n 'unavailable',\n 'The proposed window has insufficient capacity',\n );\n }\n const inserted = await tx.query(\n `INSERT INTO smrt_booking_allocations\n (tenant_id, booking_id, proposal_id, provider_id, project_id, exclusive, start_at, end_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (tenant_id, project_id, start_at, end_at)\n WHERE released_at IS NULL AND project_id IS NOT NULL DO NOTHING RETURNING booking_id`,\n ctx.tenantId,\n bookingId,\n proposalId,\n proposal.providerId,\n exclusive ? null : row.project_id,\n exclusive,\n window.startUtc,\n window.endUtc,\n );\n if (!inserted.rows.length) {\n throw new BookingError(\n 'duplicate-project-window',\n 'Project already has a reservation for this window',\n );\n }\n await tx.query(\n `UPDATE smrt_bookings SET status = 'confirmed', confirmed_proposal_id = ?\n WHERE tenant_id = ? AND id = ?`,\n proposalId,\n ctx.tenantId,\n bookingId,\n );\n return { id: bookingId, proposalId };\n },\n { bookingId },\n );\n }\n\n decline(\n context: BookingContext,\n key: string,\n input: { bookingId: string; reason: string },\n ): Promise<BookingReceipt> {\n return this.#close(context, 'decline', key, input);\n }\n\n cancel(\n context: BookingContext,\n key: string,\n input: { bookingId: string; reason: string },\n ): Promise<BookingReceipt> {\n return this.#close(context, 'cancel', key, input);\n }\n\n async get(context: BookingContext, bookingId: string): Promise<BookingView> {\n const ctx = identity(context);\n const access = await this.#access(ctx, 'read');\n const row = await this.#booking(this.#db, ctx, id(bookingId), false);\n visible(access, row);\n return bookingView(\n row,\n row.proposal_id\n ? await this.#proposal(this.#db, ctx, row, row.proposal_id)\n : null,\n );\n }\n\n /** Lists the tenant's bookings, restricted to the current contact for nonoperators. */\n async listBookings(\n context: BookingContext,\n options: ListBookingsOptions = {},\n ): Promise<BookingPage<BookingView>> {\n const ctx = identity(context);\n const access = await this.#discover(ctx);\n const { after, limit } = pageBounds(options);\n const projectId =\n options.projectId === undefined ? null : id(options.projectId);\n const result = await this.#db.query(\n `SELECT b.id, b.offering_id, b.contact_id, b.project_id, b.kind, b.status,\n b.proposal_id, b.assented_proposal_id, b.confirmed_proposal_id,\n p.provider_id, p.coverage::text, p.terms::text, p.approved_plan::text\n FROM smrt_bookings b\n LEFT JOIN smrt_booking_proposals p\n ON p.tenant_id = b.tenant_id AND p.booking_id = b.id AND p.id = b.proposal_id\n WHERE b.tenant_id = ? AND (?::boolean OR b.contact_id = ?)\n AND (?::uuid IS NULL OR b.project_id = ?)\n AND (?::uuid IS NULL OR b.id > ?)\n ORDER BY b.id LIMIT ?`,\n ctx.tenantId,\n access.operator,\n access.contactId ?? null,\n projectId,\n projectId,\n after,\n after,\n limit + 1,\n );\n const rows = result.rows as Array<BookingRow & ProposalRow>;\n const selected = page(rows, limit, (row) => row.id);\n return {\n items: selected.items.map((row) =>\n bookingView(\n row,\n row.proposal_id ? proposalView(row.proposal_id, row) : null,\n ),\n ),\n nextCursor: selected.nextCursor,\n };\n }\n\n /** Lists configured offerings. Contacts only see enabled offerings. */\n async listOfferings(\n context: BookingContext,\n options: BookingPageOptions = {},\n ): Promise<BookingPage<BookingOffering>> {\n const ctx = identity(context);\n const access = await this.#discover(ctx);\n const { after, limit } = pageBounds(options);\n const { rows } = await this.#db.query(\n `SELECT id, kind, enabled FROM smrt_booking_offerings\n WHERE tenant_id = ? AND (?::boolean OR enabled)\n AND (?::uuid IS NULL OR id > ?)\n ORDER BY id LIMIT ?`,\n ctx.tenantId,\n access.operator,\n after,\n after,\n limit + 1,\n );\n return page(rows as BookingOffering[], limit, (row) => row.id);\n }\n\n /** Lists provider schedules so operators can inspect or replace existing windows. */\n async listAvailability(\n context: BookingContext,\n options: BookingPageOptions = {},\n ): Promise<BookingPage<ProviderAvailability>> {\n const ctx = identity(context);\n operator(await this.#access(ctx, 'read'));\n const { after, limit } = pageBounds(options);\n const { rows } = await this.#db.query(\n `SELECT id, windows::text FROM smrt_booking_resources\n WHERE tenant_id = ? AND (?::uuid IS NULL OR id > ?)\n ORDER BY id LIMIT ?`,\n ctx.tenantId,\n after,\n after,\n limit + 1,\n );\n const selected = page(\n rows as Array<{ id: string; windows: string }>,\n limit,\n (row) => row.id,\n );\n return {\n items: selected.items.map((row) => ({\n providerId: row.id,\n windows: JSON.parse(row.windows),\n })),\n nextCursor: selected.nextCursor,\n };\n }\n\n async getProposal(\n context: BookingContext,\n bookingId: string,\n proposalId: string,\n ): Promise<BookingProposal> {\n const ctx = identity(context);\n const access = await this.#access(ctx, 'read');\n const row = await this.#booking(this.#db, ctx, id(bookingId), false);\n visible(access, row);\n return this.#proposal(this.#db, ctx, row, id(proposalId));\n }\n\n async history(\n context: BookingContext,\n bookingId: string,\n ): Promise<BookingHistoryEvent[]> {\n const ctx = identity(context);\n operator(await this.#access(ctx, 'history'));\n await this.#booking(this.#db, ctx, id(bookingId), false);\n const { rows } = await this.#db.query(\n `SELECT operation, actor_id, result->>'proposalId' AS proposal_id, reason, at\n FROM smrt_booking_commands WHERE tenant_id = ? AND booking_id = ? ORDER BY at, sequence`,\n ctx.tenantId,\n id(bookingId),\n );\n return rows.map((row) => ({\n operation: row.operation as BookingOperation,\n actorId: row.actor_id,\n proposalId: row.proposal_id,\n reason: row.reason,\n at: new Date(row.at).toISOString(),\n }));\n }\n\n async #close(\n context: BookingContext,\n operation: 'decline' | 'cancel',\n key: string,\n input: { bookingId: string; reason: string },\n ): Promise<BookingReceipt> {\n const bookingId = id(input.bookingId);\n const reason = required(input.reason, 2_048);\n return this.#write(\n context,\n operation,\n key,\n { bookingId, reason },\n operator,\n async (tx, ctx) => {\n const row = await this.#booking(tx, ctx, bookingId, true);\n if (\n operation === 'cancel' &&\n row.confirmed_proposal_id &&\n row.status === 'confirmed'\n ) {\n const proposal = await this.#proposal(\n tx,\n ctx,\n row,\n row.confirmed_proposal_id,\n );\n await this.#resource(tx, ctx, proposal.providerId, true);\n operator(await this.#access(ctx, 'cancel'));\n const released = await tx.query(\n `UPDATE smrt_booking_allocations SET released_at = clock_timestamp()\n WHERE tenant_id = ? AND booking_id = ? AND released_at IS NULL RETURNING booking_id`,\n ctx.tenantId,\n bookingId,\n );\n if (!released.rows.length)\n throw new BookingError(\n 'conflict',\n 'Confirmed booking has no active allocation',\n );\n } else {\n pending(row);\n }\n await tx.query(\n 'UPDATE smrt_bookings SET status = ? WHERE tenant_id = ? AND id = ?',\n operation === 'cancel' ? 'cancelled' : 'declined',\n ctx.tenantId,\n bookingId,\n );\n return {\n id: bookingId,\n ...(row.proposal_id ? { proposalId: row.proposal_id } : {}),\n };\n },\n { bookingId, reason },\n );\n }\n\n async #access(\n context: Readonly<BookingContext>,\n operation: BookingOperation,\n ): Promise<BookingAccess> {\n const access = await this.#authority.access(context, operation);\n if (!access) throw new BookingError('forbidden', 'Booking access denied');\n return {\n operator: access.operator === true,\n contactId:\n access.contactId === undefined ? undefined : id(access.contactId),\n };\n }\n\n async #discover(ctx: Readonly<BookingContext>): Promise<BookingAccess> {\n const access = await this.#access(ctx, 'read');\n if (!access.operator && !access.contactId) {\n throw new BookingError(\n 'forbidden',\n 'A named customer contact is required',\n );\n }\n return access;\n }\n\n async #verify(\n ctx: Readonly<BookingContext>,\n refs: BookingReferences,\n ): Promise<void> {\n // Use a detached copy so a host callback cannot change the version being verified and saved.\n if (\n (await this.#authority.verifyReferences(ctx, structuredClone(refs))) !==\n true\n ) {\n throw new BookingError(\n 'forbidden',\n 'Booking references or approval could not be verified',\n );\n }\n }\n\n #refs(row: BookingRow): BookingReferences {\n return {\n contactId: row.contact_id,\n projectId: row.project_id ?? undefined,\n };\n }\n\n async #booking(\n tx: DatabaseInterface,\n ctx: Readonly<BookingContext>,\n bookingId: string,\n lock: boolean,\n ): Promise<BookingRow> {\n const { rows } = await tx.query(\n `SELECT * FROM smrt_bookings WHERE tenant_id = ? AND id = ?${lock ? ' FOR UPDATE' : ''}`,\n ctx.tenantId,\n bookingId,\n );\n if (!rows[0]) throw new BookingError('not-found', 'Booking does not exist');\n return rows[0] as BookingRow;\n }\n\n #current(row: BookingRow, proposalId: string): void {\n if (row.status !== 'proposed' || row.proposal_id !== proposalId) {\n throw new BookingError(\n 'conflict',\n 'Booking does not have this current proposal',\n );\n }\n }\n\n async #proposal(\n tx: DatabaseInterface,\n ctx: Readonly<BookingContext>,\n booking: BookingRow,\n proposalId: string,\n ): Promise<BookingProposal> {\n const { rows } = await tx.query(\n `SELECT provider_id, coverage::text, terms::text, approved_plan::text FROM smrt_booking_proposals\n WHERE tenant_id = ? AND booking_id = ? AND id = ?`,\n ctx.tenantId,\n booking.id,\n proposalId,\n );\n const row = rows[0];\n if (!row) throw new BookingError('not-found', 'Proposal does not exist');\n return proposalView(proposalId, row as ProposalRow);\n }\n\n async #resource(\n tx: DatabaseInterface,\n ctx: Readonly<BookingContext>,\n providerId: string,\n lock: boolean,\n ): Promise<DeclaredAvailabilityWindow[]> {\n const { rows } = await tx.query(\n `SELECT windows::text FROM smrt_booking_resources WHERE tenant_id = ? AND id = ?${lock ? ' FOR UPDATE' : ''}`,\n ctx.tenantId,\n providerId,\n );\n if (!rows[0])\n throw new BookingError(\n 'not-found',\n 'Provider availability is not configured',\n );\n return JSON.parse(rows[0].windows);\n }\n\n async #allocations(\n tx: DatabaseInterface,\n ctx: Readonly<BookingContext>,\n providerId: string,\n window?: ResolvedBookingWindow,\n ): Promise<Allocation[]> {\n const { rows } = await tx.query(\n `SELECT a.exclusive, p.coverage::text FROM smrt_booking_allocations a\n JOIN smrt_booking_proposals p ON p.tenant_id = a.tenant_id AND p.booking_id = a.booking_id AND p.id = a.proposal_id\n WHERE a.tenant_id = ? AND a.provider_id = ? AND a.released_at IS NULL${window ? ' AND a.start_at < ? AND a.end_at > ?' : ''}`,\n ctx.tenantId,\n providerId,\n ...(window ? [window.endUtc, window.startUtc] : []),\n );\n return rows.map((row) => {\n const snapshot = JSON.parse(row.coverage) as ResolvedBookingWindow;\n return { ...snapshot, window: snapshot, exclusive: row.exclusive };\n });\n }\n\n async #write(\n context: BookingContext,\n operation: WriteOperation,\n key: string,\n canonical: object,\n permit: (\n access: BookingAccess,\n tx: DatabaseInterface,\n ctx: Readonly<BookingContext>,\n ) => void | Promise<void>,\n work: (\n tx: DatabaseInterface,\n ctx: Readonly<BookingContext>,\n ) => Promise<BookingReceipt>,\n options: {\n bookingId?: string;\n reason?: string;\n createsBooking?: boolean;\n } = {},\n ): Promise<BookingReceipt> {\n const ctx = identity(context);\n const keyHash = digest(required(key));\n const payloadHash = digest(JSON.stringify(canonical));\n if (!this.#db.transaction)\n throw new TypeError('Bookings requires a PostgreSQL transaction');\n return this.#db.transaction(async (tx) => {\n await tx.query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED');\n await permit(await this.#access(ctx, operation), tx, ctx);\n const inserted = await tx.query(\n `INSERT INTO smrt_booking_commands (tenant_id, operation, key_hash, actor_id, payload_hash)\n VALUES (?, ?, ?, ?, ?) ON CONFLICT (tenant_id, operation, key_hash) DO NOTHING RETURNING key_hash`,\n ctx.tenantId,\n operation,\n keyHash,\n ctx.actorId,\n payloadHash,\n );\n if (!inserted.rows.length) {\n await permit(await this.#access(ctx, operation), tx, ctx);\n const { rows } = await tx.query(\n `SELECT actor_id, payload_hash, result::text FROM smrt_booking_commands\n WHERE tenant_id = ? AND operation = ? AND key_hash = ?`,\n ctx.tenantId,\n operation,\n keyHash,\n );\n const prior = rows[0];\n if (\n !prior ||\n prior.actor_id !== ctx.actorId ||\n prior.payload_hash !== payloadHash ||\n !prior.result\n ) {\n throw new BookingError(\n 'conflict',\n 'Idempotency key is bound to a different command',\n );\n }\n return JSON.parse(prior.result) as BookingReceipt;\n }\n const result = await work(tx, ctx);\n await tx.query(\n `UPDATE smrt_booking_commands SET booking_id = ?, result = ?, reason = ?, at = clock_timestamp()\n WHERE tenant_id = ? AND operation = ? AND key_hash = ?`,\n options.bookingId ?? (options.createsBooking ? result.id : null),\n JSON.stringify(result),\n options.reason ?? null,\n ctx.tenantId,\n operation,\n keyHash,\n );\n return result;\n });\n }\n}\n"],"mappings":";;AACO,IAAM,eAAN,cAA2B,MAAM;CACtC,YACW,MAOT,SACA;EACA,MAAM,OAAO;EATJ,KAAA,OAAA;EAUT,KAAK,OAAO;CACd;CAXW;AAYb;;;ACbA,IAAM,aAAa;CACjB;;;;;;;CAOA;;;;;;CAMA;;;;;;;;;;;;;;;;;;CAkBA;;;;;;;;;;;;CAYA;;CAEA;;;;;;;;;;;;;;;;CAgBA;;CAEA;;;CAGA;;;;;;;;;;;;;;CAcA;AACF;AAGA,eAAsB,kBAAkB,IAAsC;CAC5E,IAAI,CAAC,GAAG,aACN,MAAM,IAAI,UAAU,4CAA4C;CAClE,MAAM,GAAG,YAAY,OAAO,OAAO;EACjC,KAAA,MAAW,aAAa,YAAY,MAAM,GAAG,MAAM,SAAS;CAC9D,CAAC;AACH;;;ACrFA,IAAM,OAAO;AACb,IAAM,YAAY;AAElB,SAAS,UAAU,MAAmC;CAEpD,IAAI,OAAO,SAAS,YAAY,CAAC,6BAA6B,KAAK,IAAI,GACrE,MAAM,IAAI,aAAa,WAAW,+BAA+B;CAEnE,IAAI;EACF,OAAO,IAAI,KAAK,eAAe,SAAS;GACtC,UAAU;GACV,UAAU;GACV,iBAAiB;GACjB,MAAM;GACN,OAAO;GACP,KAAK;GACL,MAAM;GACN,QAAQ;GACR,QAAQ;GACR,WAAW;EACb,CAAC;CACH,QAAQ;EACN,MAAM,IAAI,aAAa,WAAW,wBAAwB;CAC5D;AACF;AAEA,SAAS,QAAQ,OAAe,QAAqC;CACnE,MAAM,QAAQ,OAAO,UAAU,WAAW,KAAK,KAAK,KAAK,IAAI;CAC7D,IAAI,CAAC,SAAS,MAAM,OAAO,UACzB,MAAM,IAAI,aACR,WACA,gDACF;CAEF,MAAM,OAAO,KAAK,MAAM,KAAK;CAC7B,IAAI,CAAC,OAAO,SAAS,IAAI,GACvB,MAAM,IAAI,aAAa,WAAW,yBAAyB;CAE7D,MAAM,QAAQ,OAAO,YACnB,OAAO,cAAc,IAAI,CAAA,CAAE,KAAK,EAAE,MAAM,OAAAA,aAAY,CAAC,MAAMA,MAAK,CAAC,CACnE;CAEA,IAAI,GADa,MAAM,KAAK,SAAS,GAAG,GAAG,EAAC,GAAI,MAAM,MAAK,GAAI,MAAM,IAAG,GAAI,MAAM,KAAI,GAAI,MAAM,aAClF,MAAM,MAAM,MAAM,WAAW,MACzC,MAAM,IAAI,aACR,WACA,sDACF;CAEF,OAAO,IAAI,KAAK,IAAI,CAAA,CAAE,YAAY;AACpC;AAGO,SAAS,qBACd,QACuB;CACvB,MAAM,SAAS,UAAU,OAAO,IAAI;CACpC,MAAM,WAAW,QAAQ,OAAO,OAAO,MAAM;CAC7C,MAAM,SAAS,QAAQ,OAAO,KAAK,MAAM;CACzC,IAAI,YAAY,QACd,MAAM,IAAI,aACR,WACA,8CACF;CAEF,OAAO;EACL,MAAM,OAAO,gBAAgB,CAAA,CAAE;EAC/B,OAAO,OAAO;EACd,KAAK,OAAO;EACZ;EACA;CACF;AACF;AAEO,SAAS,YAAY,QAAwC;CAClE,OACE,KAAK,MAAM,OAAO,MAAM,IAAI,KAAK,MAAM,OAAO,QAAQ,MAAM,KAAK;AAErE;AAEA,SAAS,YAAY,QAAwC;CAC3D,MAAM,YAAY,OAAO,MAAM,MAAM,GAAG,EAAE;CAC1C,MAAM,UAAU,OAAO,IAAI,MAAM,GAAG,EAAE;CACtC,IAAI,cAAc,SAAS,OAAO;CAClC,IAAI,CAAC,OAAO,IAAI,MAAM,EAAE,CAAA,CAAE,WAAW,QAAQ,GAAG,OAAO;CACvD,MAAM,2BAAW,IAAI,KAAK,GAAG,UAAS,WAAY;CAClD,SAAS,WAAW,SAAS,WAAW,IAAI,CAAC;CAC7C,OAAO,SAAS,YAAY,CAAA,CAAE,MAAM,GAAG,EAAE,MAAM;AACjD;AAEO,SAAS,eACd,MACA,SAC8B;CAC9B,UAAU,IAAI;CACd,IAAI,CAAC,MAAM,QAAQ,OAAO,GACxB,MAAM,IAAI,aAAa,WAAW,+BAA+B;CACnE,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,aAAa,QAChB,KAAK,EAAE,OAAO,KAAK,UAAU,kBAAkB;EAC9C,IACE,CAAC,OAAO,cAAc,QAAQ,KAC9B,WAAW,KACX,OAAO,gBAAgB,WAEvB,MAAM,IAAI,aACR,WACA,wEACF;EAEF,MAAM,SAAS,qBAAqB;GAAE;GAAO;GAAK;EAAK,CAAC;EACxD,MAAM,MAAM,MAAM,MAAM,GAAG,EAAE;EAC7B,IAAI,aAAa;GACf,IAAI,CAAC,YAAY,MAAM,KAAK,KAAK,IAAI,GAAG,GACtC,MAAM,IAAI,aACR,WACA,sDACF;GAEF,KAAK,IAAI,GAAG;EACd;EACA,OAAO;GAAE,GAAG;GAAQ;GAAU;EAAY;CAC5C,CAAC,CAAA,CACA,MAAM,GAAG,MAAM,EAAE,SAAS,cAAc,EAAE,QAAQ,CAAC;CACtD,IACE,WAAW,MACR,QAAQ,MAAM,IAAI,KAAK,WAAW,IAAI,EAAC,CAAE,SAAS,OAAO,QAC5D,GAEA,MAAM,IAAI,aAAa,WAAW,uCAAuC;CAE3E,OAAO;AACT;AAQO,SAAS,oBACd,WACA,SACS;CACT,OAAO,QAAQ,MACZ,WACC,OAAO,eACP,UAAU,UAAU,OAAO,SAC3B,UAAU,QAAQ,OAAO,OACzB,UAAU,SAAS,OAAO,IAC9B;AACF;AAGO,SAAS,aACd,WACA,UACA,UACS;CAET,MAAM,6BAAa,IAAI,IAAmB;CAC1C,MAAM,OAAO,IAAY,KAAkB,UAAkB;EAC3D,MAAM,QAAQ,WAAW,IAAI,EAAE,KAAK;GAClC,UAAU;GACV,QAAQ;GACR,WAAW;EACb;EACA,MAAM,QAAQ;EACd,WAAW,IAAI,IAAI,KAAK;CAC1B;CACA,MAAM,SACJ,QACA,KACA,UACG;EACH,IACE,OAAO,YAAY,UAAU,UAC7B,OAAO,UAAU,UAAU,UAE3B;EACF,IACE,OAAO,WAAW,UAAU,WACxB,UAAU,WACV,OAAO,UACX,KACA,KACF;EACA,IAAI,OAAO,SAAS,UAAU,QAAQ,IAAI,OAAO,QAAQ,KAAK,CAAC,KAAK;CACtE;CAGA,IAAI,UAAU,UAAU,YAAY,CAAC;CACrC,KAAA,MAAW,UAAU,UAAU,MAAM,QAAQ,YAAY,OAAO,QAAQ;CACxE,KAAA,MAAW,eAAe,UACxB,MAAM,aAAa,YAAY,YAAY,cAAc,UAAU,CAAC;CAEtE,IAAI,WAAW;CACf,IAAI,SAAS;CACb,IAAI,YAAY;CAChB,KAAA,MAAW,MAAM,CAAC,GAAG,WAAW,KAAK,CAAC,CAAA,CAAE,KAAK,GAAG;EAC9C,MAAM,QAAQ,WAAW,IAAI,EAAE;EAC/B,IAAI,CAAC,OAAO;EAEZ,YAAY,MAAM;EAClB,UAAU,MAAM;EAChB,aAAa,MAAM;EACnB,IACE,WAAW,KACX,YAAY,MACX,UAAU,YAAY,SAAS,IAAI,UAAU,WAE9C,OAAO;CACX;CACA,OAAO;AACT;;;ACjKA,IAAM,OAAO;AAEb,SAAS,GAAG,OAAuB;CACjC,IAAI,OAAO,UAAU,YAAY,CAAC,KAAK,KAAK,KAAK,GAC/C,MAAM,IAAI,aAAa,WAAW,2BAA2B;CAE/D,OAAO,MAAM,YAAY;AAC3B;AAEA,SAAS,WAAW,EAAE,SAAS,QAAQ,MAA0B;CAC/D,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,KACnD,MAAM,IAAI,aAAa,WAAW,sCAAsC;CAE1E,OAAO;EAAE,OAAO,YAAY,KAAA,IAAY,OAAO,GAAG,OAAO;EAAG;CAAM;AACpE;AAEA,SAAS,KACP,MACA,OACA,QACgB;CAEhB,OAAO;EACL,OAFY,KAAK,MAAM,GAAG,KAE1B;EACA,YAAY,KAAK,SAAS,QAAQ,OAAO,KAAK,QAAQ,EAAE,IAAI;CAC9D;AACF;AAEA,SAAS,YACP,KACA,UACa;CACb,OAAO;EACL,IAAI,IAAI;EACR,YAAY,IAAI;EAChB,WAAW,IAAI;EACf,WAAW,IAAI;EACf,MAAM,IAAI;EACV,QAAQ,IAAI;EACZ;EACA,oBAAoB,IAAI;EACxB,qBAAqB,IAAI;CAC3B;AACF;AAEA,SAAS,aAAa,YAAoB,KAAmC;CAC3E,OAAO;EACL,IAAI;EACJ,YAAY,IAAI;EAChB,QAAQ,KAAK,MAAM,IAAI,QAAQ;EAC/B,OAAO,KAAK,MAAM,IAAI,KAAK;EAC3B,cACE,IAAI,kBAAkB,OAAO,OAAO,KAAK,MAAM,IAAI,aAAa;CACpE;AACF;AAEA,SAAS,SAAS,OAAe,MAAM,KAAa;CAClD,IACE,OAAO,UAAU,YACjB,MAAM,KAAK,CAAA,CAAE,WAAW,KACxB,MAAM,SAAS,OACf,MAAM,SAAS,IAAI,GAEnB,MAAM,IAAI,aACR,WACA,wDACF;CAEF,OAAO;AACT;AAEA,SAAS,SAAS,OAAiD;CACjE,OAAO,OAAO,OAAO;EACnB,UAAU,GAAG,MAAM,QAAQ;EAC3B,SAAS,SAAS,MAAM,OAAO;CACjC,CAAC;AACH;AAEA,SAAS,OAAO,OAAuB;CACrC,OAAO,WAAW,QAAQ,CAAA,CAAE,OAAO,KAAK,CAAA,CAAE,OAAO,KAAK;AACxD;AAEA,SAAS,SAAS,QAA6B;CAC7C,IAAI,CAAC,OAAO,UACV,MAAM,IAAI,aAAa,aAAa,0BAA0B;AAClE;AAEA,SAAS,QAAQ,QAAuB,WAAyB;CAC/D,IAAI,OAAO,cAAc,WACvB,MAAM,IAAI,aACR,aACA,0DACF;AAEJ;AAEA,SAAS,QAAQ,QAAuB,KAAuB;CAC7D,IAAI,CAAC,OAAO,YAAY,OAAO,cAAc,IAAI,YAC/C,MAAM,IAAI,aAAa,aAAa,wBAAwB;AAEhE;AAEA,SAAS,QAAQ,KAAuB;CACtC,IAAI,IAAI,WAAW,eAAe,IAAI,WAAW,YAC/C,MAAM,IAAI,aAAa,YAAY,8BAA8B;AAErE;AAEA,SAAS,SACP,MACA,QACA,cACM;CACN,IACG,SAAS,qBACR,CAAC,oBAAoB,QAAQ,YAAY,KAC1C,SAAS,oBACR,CAAC,aAAa,MAAM,UAAU,MAAM,SAAS,OAAO,IAAI,KAC1D,CAAC,aACC;EAAE,GAAG;EAAQ,WAAW,SAAS;CAAiB,GAClD,cACA,CAAC,CACH,GAEA,MAAM,IAAI,aACR,eACA,sDACF;AAEJ;AAGO,IAAM,iBAAN,MAAqB;CACjB;CACA;CAET,YAAY,IAAuB,WAA6B;EAC9D,KAAK,MAAM;EACX,KAAK,aAAa;CACpB;CAEA,MAAM,kBACJ,SACA,KACA,OACyB;EACzB,MAAM,aAAa,GAAG,MAAM,UAAU;EACtC,MAAM,EAAE,MAAM,YAAY;EAC1B,IACG,SAAS,oBAAoB,SAAS,qBACvC,OAAO,YAAY,WAEnB,MAAM,IAAI,aACR,WACA,uCACF;EAEF,OAAO,KAAK,OACV,SACA,sBACA,KACA;GAAE;GAAY;GAAM;EAAQ,GAC5B,UACA,OAAO,IAAI,QAAQ;GAUjB,IAAI,EAAC,MATe,GAAG,MACrB;;0EAGA,IAAI,UACJ,YACA,MACA,OACF,EAAA,CACW,KAAK,QACd,MAAM,IAAI,aAAa,YAAY,gCAAgC;GACrE,OAAO,EAAE,IAAI,WAAW;EAC1B,CACF;CACF;CAEA,MAAM,oBACJ,SACA,KACA,OACyB;EACzB,MAAM,aAAa,GAAG,MAAM,UAAU;EACtC,MAAM,UAAU,eAAe,MAAM,MAAM,MAAM,OAAO;EACxD,OAAO,KAAK,OACV,SACA,wBACA,KACA;GAAE;GAAY;EAAQ,GACtB,UACA,OAAO,IAAI,QAAQ;GACjB,MAAM,GAAG,MACP;kDAEA,IAAI,UACJ,UACF;GACA,MAAM,KAAK,UAAU,IAAI,KAAK,YAAY,IAAI;GAC9C,SAAS,MAAM,KAAK,QAAQ,KAAK,sBAAsB,CAAC;GACxD,MAAM,KAAK,QAAQ,KAAK,EAAE,WAAW,CAAC;GACtC,MAAM,YAAY,MAAM,KAAK,aAAa,IAAI,KAAK,UAAU;GAC7D,IACE,UAAU,MACP,MAAM,MACJ,CAAC,KAAK,aAAa,CAAC,oBAAoB,KAAK,QAAQ,OAAO,KAC7D,CAAC,aACC,MACA,SACA,UAAU,QAAQ,GAAG,MAAM,MAAM,CAAC,CACpC,CACJ,GAEA,MAAM,IAAI,aACR,eACA,2DACF;GAEF,MAAM,GAAG,MACP,gFACA,KAAK,UAAU,OAAO,GACtB,IAAI,UACJ,UACF;GACA,OAAO,EAAE,IAAI,WAAW;EAC1B,CACF;CACF;CAEA,MAAM,QACJ,SACA,KACA,OACyB;EACzB,MAAM,aAAa,GAAG,MAAM,UAAU;EACtC,MAAM,YAAY,GAAG,MAAM,SAAS;EACpC,MAAM,YACJ,MAAM,cAAc,KAAA,IAAY,OAAO,GAAG,MAAM,SAAS;EAC3D,OAAO,KAAK,OACV,SACA,WACA,KACA;GAAE;GAAY;GAAW;EAAU,IAClC,WAAW;GACV,IAAI,CAAC,OAAO,UAAU,QAAQ,QAAQ,SAAS;EACjD,GACA,OAAO,IAAI,QAAQ;GACjB,MAAM,EAAE,SAAS,MAAM,GAAG,MACxB,6FACA,IAAI,UACJ,UACF;GACA,IAAI,CAAC,KAAK,EAAC,EAAG,SACZ,MAAM,IAAI,aAAa,aAAa,2BAA2B;GACjE,MAAM,OAAO,KAAK,EAAC,CAAE;GACrB,IAAI,SAAS,qBAAqB,CAAC,WACjC,MAAM,IAAI,aACR,WACA,0CACF;GAEF,MAAM,KAAK,QAAQ,KAAK;IACtB;IACA,WAAW,aAAa,KAAA;GAC1B,CAAC;GACD,MAAM,YAAY,WAAW;GAC7B,MAAM,GAAG,MACP;kDAEA,IAAI,UACJ,WACA,YACA,WACA,WACA,IACF;GACA,OAAO,EAAE,IAAI,UAAU;EACzB,GACA,EAAE,gBAAgB,KAAK,CACzB;CACF;CAEA,MAAM,QACJ,SACA,KACA,OACyB;EACzB,MAAM,YAAY,GAAG,MAAM,SAAS;EACpC,MAAM,aAAa,GAAG,MAAM,UAAU;EACtC,MAAM,SAAS,qBAAqB,MAAM,MAAM;EAChD,MAAM,QAAQ;GACZ,UAAU,SAAS,MAAM,MAAM,QAAQ;GACvC,MAAM,SAAS,MAAM,MAAM,MAAM,KAAM;EACzC;EACA,MAAM,OAAO,MAAM;EACnB,MAAM,eACJ,SAAS,KAAA,IACL,OACA;GAAE,IAAI,GAAG,KAAK,EAAE;GAAG,SAAS,SAAS,KAAK,OAAO;EAAE;EACzD,OAAO,KAAK,OACV,SACA,WACA,KACA;GAAE;GAAW;GAAY;GAAQ;GAAO;EAAa,GACrD,UACA,OAAO,IAAI,QAAQ;GACjB,MAAM,MAAM,MAAM,KAAK,SAAS,IAAI,KAAK,WAAW,IAAI;GACxD,QAAQ,GAAG;GACX,IACE,IAAI,SAAS,oBACT,CAAC,eACD,gBAAgB,CAAC,YAAY,MAAM,GAEvC,MAAM,IAAI,aACR,WACA,+EACF;GAEF,MAAM,KAAK,QAAQ,KAAK;IACtB,GAAG,KAAK,MAAM,GAAG;IACjB;IACA,cAAc,gBAAgB,KAAA;GAChC,CAAC;GACD,SACE,IAAI,MACJ,QACA,MAAM,KAAK,UAAU,IAAI,KAAK,YAAY,KAAK,CACjD;GACA,MAAM,aAAa,WAAW;GAC9B,MAAM,GAAG,MACP;wCAEA,IAAI,UACJ,WACA,YACA,YACA,KAAK,UAAU,MAAM,GACrB,KAAK,UAAU,KAAK,GACpB,gBAAgB,KAAK,UAAU,YAAY,CAC7C;GACA,MAAM,GAAG,MACP;0CAEA,YACA,IAAI,UACJ,SACF;GACA,OAAO;IAAE,IAAI;IAAW;GAAW;EACrC,GACA,EAAE,UAAU,CACd;CACF;CAEA,MAAM,OACJ,SACA,KACA,OACyB;EACzB,MAAM,YAAY,GAAG,MAAM,SAAS;EACpC,MAAM,aAAa,GAAG,MAAM,UAAU;EACtC,OAAO,KAAK,OACV,SACA,UACA,KACA;GAAE;GAAW;EAAW,GACxB,OAAO,QAAQ,IAAI,QAAQ;GAEzB,MAAM,MAAM,MAAM,KAAK,SAAS,IAAI,KAAK,WAAW,KAAK;GACzD,QAAQ,QAAQ,GAAG;GACnB,QAAQ,QAAQ,IAAI,UAAU;EAChC,GACA,OAAO,IAAI,QAAQ;GACjB,MAAM,MAAM,MAAM,KAAK,SAAS,IAAI,KAAK,WAAW,IAAI;GACxD,QAAQ,MAAM,KAAK,QAAQ,KAAK,QAAQ,GAAG,IAAI,UAAU;GACzD,KAAK,SAAS,KAAK,UAAU;GAC7B,IAAI,IAAI,sBACN,MAAM,IAAI,aACR,YACA,qCACF;GACF,MAAM,KAAK,QAAQ,KAAK,KAAK,MAAM,GAAG,CAAC;GACvC,MAAM,GAAG,MACP,oFACA,YACA,IAAI,UACJ,SACF;GACA,OAAO;IAAE,IAAI;IAAW;GAAW;EACrC,GACA,EAAE,UAAU,CACd;CACF;CAEA,MAAM,QACJ,SACA,KACA,OACyB;EACzB,MAAM,YAAY,GAAG,MAAM,SAAS;EACpC,MAAM,aAAa,GAAG,MAAM,UAAU;EACtC,OAAO,KAAK,OACV,SACA,WACA,KACA;GAAE;GAAW;EAAW,GACxB,UACA,OAAO,IAAI,QAAQ;GACjB,MAAM,MAAM,MAAM,KAAK,SAAS,IAAI,KAAK,WAAW,IAAI;GACxD,KAAK,SAAS,KAAK,UAAU;GAC7B,IAAI,IAAI,yBAAyB,YAC/B,MAAM,IAAI,aACR,YACA,8CACF;GAEF,MAAM,WAAW,MAAM,KAAK,UAAU,IAAI,KAAK,KAAK,UAAU;GAC9D,MAAM,eAAe,MAAM,KAAK,UAC9B,IACA,KACA,SAAS,YACT,IACF;GAEA,SAAS,MAAM,KAAK,QAAQ,KAAK,SAAS,CAAC;GAC3C,MAAM,KAAK,QAAQ,KAAK;IACtB,GAAG,KAAK,MAAM,GAAG;IACjB,YAAY,SAAS;IACrB,cAAc,SAAS,gBAAgB,KAAA;GACzC,CAAC;GACD,MAAM,YAAY,IAAI,SAAS;GAC/B,MAAM,SAAS,SAAS;GACxB,SAAS,IAAI,MAAM,QAAQ,YAAY;GACvC,IACE,CAAC,aACC;IAAE,GAAG;IAAQ;GAAU,GACvB,cACA,MAAM,KAAK,aAAa,IAAI,KAAK,SAAS,YAAY,MAAM,CAC9D,GAEA,MAAM,IAAI,aACR,eACA,+CACF;GAiBF,IAAI,EAAC,MAfkB,GAAG,MACxB;;;;kGAKA,IAAI,UACJ,WACA,YACA,SAAS,YACT,YAAY,OAAO,IAAI,YACvB,WACA,OAAO,UACP,OAAO,MACT,EAAA,CACc,KAAK,QACjB,MAAM,IAAI,aACR,4BACA,mDACF;GAEF,MAAM,GAAG,MACP;0CAEA,YACA,IAAI,UACJ,SACF;GACA,OAAO;IAAE,IAAI;IAAW;GAAW;EACrC,GACA,EAAE,UAAU,CACd;CACF;CAEA,QACE,SACA,KACA,OACyB;EACzB,OAAO,KAAK,OAAO,SAAS,WAAW,KAAK,KAAK;CACnD;CAEA,OACE,SACA,KACA,OACyB;EACzB,OAAO,KAAK,OAAO,SAAS,UAAU,KAAK,KAAK;CAClD;CAEA,MAAM,IAAI,SAAyB,WAAyC;EAC1E,MAAM,MAAM,SAAS,OAAO;EAC5B,MAAM,SAAS,MAAM,KAAK,QAAQ,KAAK,MAAM;EAC7C,MAAM,MAAM,MAAM,KAAK,SAAS,KAAK,KAAK,KAAK,GAAG,SAAS,GAAG,KAAK;EACnE,QAAQ,QAAQ,GAAG;EACnB,OAAO,YACL,KACA,IAAI,cACA,MAAM,KAAK,UAAU,KAAK,KAAK,KAAK,KAAK,IAAI,WAAW,IACxD,IACN;CACF;;CAGA,MAAM,aACJ,SACA,UAA+B,CAAC,GACG;EACnC,MAAM,MAAM,SAAS,OAAO;EAC5B,MAAM,SAAS,MAAM,KAAK,UAAU,GAAG;EACvC,MAAM,EAAE,OAAO,UAAU,WAAW,OAAO;EAC3C,MAAM,YACJ,QAAQ,cAAc,KAAA,IAAY,OAAO,GAAG,QAAQ,SAAS;EAqB/D,MAAM,QAAO,MApBQ,KAAK,IAAI,MAC5B;;;;;;;;;+BAUA,IAAI,UACJ,OAAO,UACP,OAAO,aAAa,MACpB,WACA,WACA,OACA,OACA,QAAQ,CACV,EAAA,CACoB;EACpB,MAAM,WAAW,KAAK,MAAM,QAAQ,QAAQ,IAAI,EAAE;EAClD,OAAO;GACL,OAAO,SAAS,MAAM,KAAK,QACzB,YACE,KACA,IAAI,cAAc,aAAa,IAAI,aAAa,GAAG,IAAI,IACzD,CACF;GACA,YAAY,SAAS;EACvB;CACF;;CAGA,MAAM,cACJ,SACA,UAA8B,CAAC,GACQ;EACvC,MAAM,MAAM,SAAS,OAAO;EAC5B,MAAM,SAAS,MAAM,KAAK,UAAU,GAAG;EACvC,MAAM,EAAE,OAAO,UAAU,WAAW,OAAO;EAC3C,MAAM,EAAE,SAAS,MAAM,KAAK,IAAI,MAC9B;;;6BAIA,IAAI,UACJ,OAAO,UACP,OACA,OACA,QAAQ,CACV;EACA,OAAO,KAAK,MAA2B,QAAQ,QAAQ,IAAI,EAAE;CAC/D;;CAGA,MAAM,iBACJ,SACA,UAA8B,CAAC,GACa;EAC5C,MAAM,MAAM,SAAS,OAAO;EAC5B,SAAS,MAAM,KAAK,QAAQ,KAAK,MAAM,CAAC;EACxC,MAAM,EAAE,OAAO,UAAU,WAAW,OAAO;EAC3C,MAAM,EAAE,SAAS,MAAM,KAAK,IAAI,MAC9B;;6BAGA,IAAI,UACJ,OACA,OACA,QAAQ,CACV;EACA,MAAM,WAAW,KACf,MACA,QACC,QAAQ,IAAI,EACf;EACA,OAAO;GACL,OAAO,SAAS,MAAM,KAAK,SAAS;IAClC,YAAY,IAAI;IAChB,SAAS,KAAK,MAAM,IAAI,OAAO;GACjC,EAAE;GACF,YAAY,SAAS;EACvB;CACF;CAEA,MAAM,YACJ,SACA,WACA,YAC0B;EAC1B,MAAM,MAAM,SAAS,OAAO;EAC5B,MAAM,SAAS,MAAM,KAAK,QAAQ,KAAK,MAAM;EAC7C,MAAM,MAAM,MAAM,KAAK,SAAS,KAAK,KAAK,KAAK,GAAG,SAAS,GAAG,KAAK;EACnE,QAAQ,QAAQ,GAAG;EACnB,OAAO,KAAK,UAAU,KAAK,KAAK,KAAK,KAAK,GAAG,UAAU,CAAC;CAC1D;CAEA,MAAM,QACJ,SACA,WACgC;EAChC,MAAM,MAAM,SAAS,OAAO;EAC5B,SAAS,MAAM,KAAK,QAAQ,KAAK,SAAS,CAAC;EAC3C,MAAM,KAAK,SAAS,KAAK,KAAK,KAAK,GAAG,SAAS,GAAG,KAAK;EACvD,MAAM,EAAE,SAAS,MAAM,KAAK,IAAI,MAC9B;iGAEA,IAAI,UACJ,GAAG,SAAS,CACd;EACA,OAAO,KAAK,KAAK,SAAS;GACxB,WAAW,IAAI;GACf,SAAS,IAAI;GACb,YAAY,IAAI;GAChB,QAAQ,IAAI;GACZ,IAAI,IAAI,KAAK,IAAI,EAAE,CAAA,CAAE,YAAY;EACnC,EAAE;CACJ;CAEA,MAAM,OACJ,SACA,WACA,KACA,OACyB;EACzB,MAAM,YAAY,GAAG,MAAM,SAAS;EACpC,MAAM,SAAS,SAAS,MAAM,QAAQ,IAAK;EAC3C,OAAO,KAAK,OACV,SACA,WACA,KACA;GAAE;GAAW;EAAO,GACpB,UACA,OAAO,IAAI,QAAQ;GACjB,MAAM,MAAM,MAAM,KAAK,SAAS,IAAI,KAAK,WAAW,IAAI;GACxD,IACE,cAAc,YACd,IAAI,yBACJ,IAAI,WAAW,aACf;IACA,MAAM,WAAW,MAAM,KAAK,UAC1B,IACA,KACA,KACA,IAAI,qBACN;IACA,MAAM,KAAK,UAAU,IAAI,KAAK,SAAS,YAAY,IAAI;IACvD,SAAS,MAAM,KAAK,QAAQ,KAAK,QAAQ,CAAC;IAO1C,IAAI,EAAC,MANkB,GAAG,MACxB;iGAEA,IAAI,UACJ,SACF,EAAA,CACc,KAAK,QACjB,MAAM,IAAI,aACR,YACA,4CACF;GACJ,OACE,QAAQ,GAAG;GAEb,MAAM,GAAG,MACP,sEACA,cAAc,WAAW,cAAc,YACvC,IAAI,UACJ,SACF;GACA,OAAO;IACL,IAAI;IACJ,GAAI,IAAI,cAAc,EAAE,YAAY,IAAI,YAAY,IAAI,CAAC;GAC3D;EACF,GACA;GAAE;GAAW;EAAO,CACtB;CACF;CAEA,MAAM,QACJ,SACA,WACwB;EACxB,MAAM,SAAS,MAAM,KAAK,WAAW,OAAO,SAAS,SAAS;EAC9D,IAAI,CAAC,QAAQ,MAAM,IAAI,aAAa,aAAa,uBAAuB;EACxE,OAAO;GACL,UAAU,OAAO,aAAa;GAC9B,WACE,OAAO,cAAc,KAAA,IAAY,KAAA,IAAY,GAAG,OAAO,SAAS;EACpE;CACF;CAEA,MAAM,UAAU,KAAuD;EACrE,MAAM,SAAS,MAAM,KAAK,QAAQ,KAAK,MAAM;EAC7C,IAAI,CAAC,OAAO,YAAY,CAAC,OAAO,WAC9B,MAAM,IAAI,aACR,aACA,sCACF;EAEF,OAAO;CACT;CAEA,MAAM,QACJ,KACA,MACe;EAEf,IACG,MAAM,KAAK,WAAW,iBAAiB,KAAK,gBAAgB,IAAI,CAAC,MAClE,MAEA,MAAM,IAAI,aACR,aACA,sDACF;CAEJ;CAEA,MAAM,KAAoC;EACxC,OAAO;GACL,WAAW,IAAI;GACf,WAAW,IAAI,cAAc,KAAA;EAC/B;CACF;CAEA,MAAM,SACJ,IACA,KACA,WACA,MACqB;EACrB,MAAM,EAAE,SAAS,MAAM,GAAG,MACxB,6DAA6D,OAAO,gBAAgB,MACpF,IAAI,UACJ,SACF;EACA,IAAI,CAAC,KAAK,IAAI,MAAM,IAAI,aAAa,aAAa,wBAAwB;EAC1E,OAAO,KAAK;CACd;CAEA,SAAS,KAAiB,YAA0B;EAClD,IAAI,IAAI,WAAW,cAAc,IAAI,gBAAgB,YACnD,MAAM,IAAI,aACR,YACA,6CACF;CAEJ;CAEA,MAAM,UACJ,IACA,KACA,SACA,YAC0B;EAC1B,MAAM,EAAE,SAAS,MAAM,GAAG,MACxB;2DAEA,IAAI,UACJ,QAAQ,IACR,UACF;EACA,MAAM,MAAM,KAAK;EACjB,IAAI,CAAC,KAAK,MAAM,IAAI,aAAa,aAAa,yBAAyB;EACvE,OAAO,aAAa,YAAY,GAAkB;CACpD;CAEA,MAAM,UACJ,IACA,KACA,YACA,MACuC;EACvC,MAAM,EAAE,SAAS,MAAM,GAAG,MACxB,kFAAkF,OAAO,gBAAgB,MACzG,IAAI,UACJ,UACF;EACA,IAAI,CAAC,KAAK,IACR,MAAM,IAAI,aACR,aACA,yCACF;EACF,OAAO,KAAK,MAAM,KAAK,EAAC,CAAE,OAAO;CACnC;CAEA,MAAM,aACJ,IACA,KACA,YACA,QACuB;EACvB,MAAM,EAAE,SAAS,MAAM,GAAG,MACxB;;8EAEwE,SAAS,yCAAyC,MAC1H,IAAI,UACJ,YACA,GAAI,SAAS,CAAC,OAAO,QAAQ,OAAO,QAAQ,IAAI,CAAC,CACnD;EACA,OAAO,KAAK,KAAK,QAAQ;GACvB,MAAM,WAAW,KAAK,MAAM,IAAI,QAAQ;GACxC,OAAO;IAAE,GAAG;IAAU,QAAQ;IAAU,WAAW,IAAI;GAAU;EACnE,CAAC;CACH;CAEA,MAAM,OACJ,SACA,WACA,KACA,WACA,QAKA,MAIA,UAII,CAAC,GACoB;EACzB,MAAM,MAAM,SAAS,OAAO;EAC5B,MAAM,UAAU,OAAO,SAAS,GAAG,CAAC;EACpC,MAAM,cAAc,OAAO,KAAK,UAAU,SAAS,CAAC;EACpD,IAAI,CAAC,KAAK,IAAI,aACZ,MAAM,IAAI,UAAU,4CAA4C;EAClE,OAAO,KAAK,IAAI,YAAY,OAAO,OAAO;GACxC,MAAM,GAAG,MAAM,gDAAgD;GAC/D,MAAM,OAAO,MAAM,KAAK,QAAQ,KAAK,SAAS,GAAG,IAAI,GAAG;GAUxD,IAAI,EAAC,MATkB,GAAG,MACxB;6GAEA,IAAI,UACJ,WACA,SACA,IAAI,SACJ,WACF,EAAA,CACc,KAAK,QAAQ;IACzB,MAAM,OAAO,MAAM,KAAK,QAAQ,KAAK,SAAS,GAAG,IAAI,GAAG;IACxD,MAAM,EAAE,SAAS,MAAM,GAAG,MACxB;oEAEA,IAAI,UACJ,WACA,OACF;IACA,MAAM,QAAQ,KAAK;IACnB,IACE,CAAC,SACD,MAAM,aAAa,IAAI,WACvB,MAAM,iBAAiB,eACvB,CAAC,MAAM,QAEP,MAAM,IAAI,aACR,YACA,iDACF;IAEF,OAAO,KAAK,MAAM,MAAM,MAAM;GAChC;GACA,MAAM,SAAS,MAAM,KAAK,IAAI,GAAG;GACjC,MAAM,GAAG,MACP;kEAEA,QAAQ,cAAc,QAAQ,iBAAiB,OAAO,KAAK,OAC3D,KAAK,UAAU,MAAM,GACrB,QAAQ,UAAU,MAClB,IAAI,UACJ,WACA,OACF;GACA,OAAO;EACT,CAAC;CACH;AACF"}
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-bookings",
5
- "packageVersion": "0.60.1",
5
+ "packageVersion": "0.60.2",
6
6
  "objects": {},
7
7
  "moduleType": "smrt",
8
8
  "stampsConstructors": true
package/dist/service.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { DatabaseInterface } from '@happyvertical/sql';
2
- import type { AvailabilityInput, BookingAuthority, BookingContext, BookingHistoryEvent, BookingKind, BookingProposal, BookingReceipt, BookingView, ProposeBookingInput, RequestBookingInput } from './types.js';
2
+ import type { AvailabilityInput, BookingAuthority, BookingContext, BookingHistoryEvent, BookingKind, BookingOffering, BookingPage, BookingPageOptions, BookingProposal, BookingReceipt, BookingView, ListBookingsOptions, ProposeBookingInput, ProviderAvailability, RequestBookingInput } from './types.js';
3
3
  /** Transactional PostgreSQL service. The application supplies trusted identity and authorization. */
4
4
  export declare class BookingService {
5
5
  #private;
@@ -29,6 +29,12 @@ export declare class BookingService {
29
29
  reason: string;
30
30
  }): Promise<BookingReceipt>;
31
31
  get(context: BookingContext, bookingId: string): Promise<BookingView>;
32
+ /** Lists the tenant's bookings, restricted to the current contact for nonoperators. */
33
+ listBookings(context: BookingContext, options?: ListBookingsOptions): Promise<BookingPage<BookingView>>;
34
+ /** Lists configured offerings. Contacts only see enabled offerings. */
35
+ listOfferings(context: BookingContext, options?: BookingPageOptions): Promise<BookingPage<BookingOffering>>;
36
+ /** Lists provider schedules so operators can inspect or replace existing windows. */
37
+ listAvailability(context: BookingContext, options?: BookingPageOptions): Promise<BookingPage<ProviderAvailability>>;
32
38
  getProposal(context: BookingContext, bookingId: string, proposalId: string): Promise<BookingProposal>;
33
39
  history(context: BookingContext, bookingId: string): Promise<BookingHistoryEvent[]>;
34
40
  }
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,KAAK,EACV,iBAAiB,EAEjB,gBAAgB,EAChB,cAAc,EACd,mBAAmB,EACnB,WAAW,EAEX,eAAe,EACf,cAAc,EAGd,WAAW,EACX,mBAAmB,EACnB,mBAAmB,EAEpB,MAAM,YAAY,CAAC;AAiHpB,qGAAqG;AACrG,qBAAa,cAAc;;gBAIb,EAAE,EAAE,iBAAiB,EAAE,SAAS,EAAE,gBAAgB;IAKxD,iBAAiB,CACrB,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,WAAW,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GACjE,OAAO,CAAC,cAAc,CAAC;IAmCpB,mBAAmB,CACvB,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,cAAc,CAAC;IA+CpB,OAAO,CACX,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,cAAc,CAAC;IAiDpB,OAAO,CACX,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,cAAc,CAAC;IAmEpB,MAAM,CACV,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAC/C,OAAO,CAAC,cAAc,CAAC;IAoCpB,OAAO,CACX,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAC/C,OAAO,CAAC,cAAc,CAAC;IAiF1B,OAAO,CACL,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAC3C,OAAO,CAAC,cAAc,CAAC;IAI1B,MAAM,CACJ,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAC3C,OAAO,CAAC,cAAc,CAAC;IAIpB,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAoBrE,WAAW,CACf,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,eAAe,CAAC;IAQrB,OAAO,CACX,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,mBAAmB,EAAE,CAAC;CAmRlC"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
@@ -3,14 +3,14 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-bookings",
6
- "packageVersion": "0.60.1",
6
+ "packageVersion": "0.60.2",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "68f1efc4c9a35f4b6e4dfb89cdc4fa4af956e7c6a2ab345188459a27b32fb204",
11
- "packageJson": "21494a041a05bbe4722ef92d5d833cbfa3ba213ac43dccfb8073007d24342fa5",
10
+ "manifest": "904dba4fbe9d0dcde5db8f5f072182cdde3f86677553226cd1a11d7413053b02",
11
+ "packageJson": "fa1532b61c33ced4b00af3150e913a503b8eb8be444c6ced5b081c49c85eb37a",
12
12
  "agents": "01210ae016391e945e8ceac798120ebd5a8deeb807fe7c5cac6f5fc824e0ce02",
13
- "moduleDoc:README.md": "a4b1fef604b682fdb60c1297427fb8a4cd56cba8a76e3e8eb113ddc5689cfff6"
13
+ "moduleDoc:README.md": "b054330ea8f35f34ebfea29bfc8295c82616d798c00b8d97273da349bc431931"
14
14
  },
15
15
  "exports": [
16
16
  "."
@@ -51,7 +51,7 @@
51
51
  {
52
52
  "path": "README.md",
53
53
  "module": "README",
54
- "content": "# s-m-r-t Bookings\n\n`@happyvertical/smrt-bookings` reserves one elapsed developer hour or one\noperator-declared local supervision window. It does not start agents, collect\npayment, send notifications, or write Commerce records.\n\nApply `migrateBookingsV1` once through the host's deployment migration ledger\nusing a PostgreSQL database handle. Use an ordinary PostgreSQL connection or\npool for `BookingService`; the service owns its transaction and explicitly uses\nREAD COMMITTED. It does not create schema on demand. No ORM collection, CRUD\nroute, or generated CLI or MCP write is registered.\n\n1. An operator enables an offering and declares availability for a canonical\n provider UUID. Use the same provider identity for all services that reserve\n the same person. A declaration replaces that provider's configured windows.\n Windows must not overlap and must have positive integer capacity. Mark at\n most one exact supervision window per provider per local date. Changes that\n invalidate any active reservation are refused.\n2. The named customer contact, or an operator acting for them, requests an\n enabled offering. A supervision request requires a project. Disabling an\n offering stops new requests; operators may finish pending requests.\n3. An operator proposes a provider, window, and terms revision with the exact\n agreed wording. Supervision also needs an approved project plan ID and\n version. Each proposal is immutable. The contact assents to its exact ID.\n Replacing the proposal clears current assent. Requests, proposals, and\n assent reserve no capacity.\n4. An operator confirms the exact assented proposal. The service locks the\n booking and then the stable provider row, checks authority and current\n declared availability, and checks every half-open segment. An exclusive\n hour blocks all overlaps; supervision consumes one unit. Two active\n supervision reservations for the same project and exact UTC interval are\n refused even on different providers. An unsuccessful confirmation leaves\n the proposal and assent available for a retry or revision.\n5. An operator can decline a pending request or cancel a pending or confirmed\n booking with a reason. Cancelling a confirmed booking releases capacity\n in the same transaction; it preserves the confirmed terms and history.\n Booking and historical proposal reads are scoped to the named contact or\n an operator. Booking audit history is operator-only.\n\nWindows use an IANA zone and local timestamps of the form\n`2027-11-07T01:30-04:00`. For example, use that offset with America/New_York. The offset must be valid for that instant in the zone.\nDuring a DST fold the offsets identify different instants; a nonexistent wall\ntime is rejected. Stored proposals retain the local input and UTC instants.\nIntervals include their start and exclude their end. A supervision window\nstarts and ends on one local date, or ends at midnight opening the next date;\nit need not cover the entire day and may span a daylight-saving transition.\n\nUse an authenticated tenant/actor context from the host, never from a request\nbody. The host's `BookingAuthority` supplies current operator permission and\nthe actor's named contact mapping, and must verify that external references\nexist in that tenant and relate to each other. For a supervision proposal and\nagain at confirmation, it must verify that the exact plan version is approved\nfor the project. The confirmation check occurs after the provider lock; it is\nevidence of approval at that point, not a distributed lock on a plan system.\nAuthority callbacks should be read-only and must not call a booking command.\n\nEach write takes a retry key scoped to tenant and operation. A committed key\nis bound to its actor and normalized command payload. An identical authorized\nretry returns the original receipt; a different payload or actor is rejected.\nA failed command rolls back both its changes and the key claim. Command receipts\nand audit history commit with the booking, so a failure in caller-owned\ncommunication does not undo the reservation. Clients can fetch it afterward\nand coordinate manual invoicing separately.\n\nOnly real PostgreSQL is supported for writes. To run the package unit tests,\nuse `pnpm --filter @happyvertical/smrt-bookings test`. To run the separate\nPostgreSQL suite, point `CI_POSTGRES_BASE_URL` at an isolated server with\ndatabase creation privileges and run `pnpm --filter @happyvertical/smrt-bookings\ntest:postgres`. The test wrapper provisions and removes a disposable database.\n"
54
+ "content": "# s-m-r-t Bookings\n\n`@happyvertical/smrt-bookings` reserves one elapsed developer hour or one\noperator-declared local supervision window. It does not start agents, collect\npayment, send notifications, or write Commerce records.\n\nApply `migrateBookingsV1` once through the host's deployment migration ledger\nusing a PostgreSQL database handle. Use an ordinary PostgreSQL connection or\npool for `BookingService`; the service owns its transaction and explicitly uses\nREAD COMMITTED. It does not create schema on demand. No ORM collection, CRUD\nroute, or generated CLI or MCP write is registered.\n\n1. An operator enables an offering and declares availability for a canonical\n provider UUID. Use the same provider identity for all services that reserve\n the same person. A declaration replaces that provider's configured windows.\n Windows must not overlap and must have positive integer capacity. Mark at\n most one exact supervision window per provider per local date. Changes that\n invalidate any active reservation are refused.\n2. The named customer contact, or an operator acting for them, requests an\n enabled offering. A supervision request requires a project. Disabling an\n offering stops new requests; operators may finish pending requests.\n3. An operator proposes a provider, window, and terms revision with the exact\n agreed wording. Supervision also needs an approved project plan ID and\n version. Each proposal is immutable. The contact assents to its exact ID.\n Replacing the proposal clears current assent. Requests, proposals, and\n assent reserve no capacity.\n4. An operator confirms the exact assented proposal. The service locks the\n booking and then the stable provider row, checks authority and current\n declared availability, and checks every half-open segment. An exclusive\n hour blocks all overlaps; supervision consumes one unit. Two active\n supervision reservations for the same project and exact UTC interval are\n refused even on different providers. An unsuccessful confirmation leaves\n the proposal and assent available for a retry or revision.\n5. An operator can decline a pending request or cancel a pending or confirmed\n booking with a reason. Cancelling a confirmed booking releases capacity\n in the same transaction; it preserves the confirmed terms and history.\n Booking and historical proposal reads are scoped to the named contact or\n an operator. Booking audit history is operator-only.\n\nWindows use an IANA zone and local timestamps of the form\n`2027-11-07T01:30-04:00`. For example, use that offset with America/New_York. The offset must be valid for that instant in the zone.\nDuring a DST fold the offsets identify different instants; a nonexistent wall\ntime is rejected. Stored proposals retain the local input and UTC instants.\nIntervals include their start and exclude their end. A supervision window\nstarts and ends on one local date, or ends at midnight opening the next date;\nit need not cover the entire day and may span a daylight-saving transition.\n\nUse `listBookings` to find the same booking views returned by `get`. An\noptional project UUID narrows the results within the caller's existing scope.\nOperators see all bookings in their tenant. Other callers see only bookings\nfor the contact the host maps to their actor. `listOfferings` exposes the\noffering ID, kind and enabled flag. Contacts only see enabled offerings.\nOperators can use `listAvailability` to inspect existing provider windows\nbefore replacing a declaration. All three methods use the `read` authority.\n\nLists return up to 50 items by default and accept a limit from 1 to 100.\nResults are ordered by ascending UUID. Pass the returned `nextCursor` as\n`afterId` with the same filters to fetch another page. A null cursor means\nthe page reached the end of the results.\n\nUse an authenticated tenant/actor context from the host, never from a request\nbody. The host's `BookingAuthority` supplies current operator permission and\nthe actor's named contact mapping, and must verify that external references\nexist in that tenant and relate to each other. For a supervision proposal and\nagain at confirmation, it must verify that the exact plan version is approved\nfor the project. The confirmation check occurs after the provider lock; it is\nevidence of approval at that point, not a distributed lock on a plan system.\nAuthority callbacks should be read-only and must not call a booking command.\n\nEach write takes a retry key scoped to tenant and operation. A committed key\nis bound to its actor and normalized command payload. An identical authorized\nretry returns the original receipt; a different payload or actor is rejected.\nA failed command rolls back both its changes and the key claim. Command receipts\nand audit history commit with the booking, so a failure in caller-owned\ncommunication does not undo the reservation. Clients can fetch it afterward\nand coordinate manual invoicing separately.\n\nOnly real PostgreSQL is supported for writes. To run the package unit tests,\nuse `pnpm --filter @happyvertical/smrt-bookings test`. To run the separate\nPostgreSQL suite, point `CI_POSTGRES_BASE_URL` at an isolated server with\ndatabase creation privileges and run `pnpm --filter @happyvertical/smrt-bookings\ntest:postgres`. The test wrapper provisions and removes a disposable database.\n"
55
55
  }
56
56
  ]
57
57
  }
package/dist/types.d.ts CHANGED
@@ -27,6 +27,24 @@ export interface BookingAuthority {
27
27
  verifyReferences(context: Readonly<BookingContext>, references: Readonly<BookingReferences>): Promise<boolean>;
28
28
  }
29
29
  export type BookingKind = 'developer-hour' | 'supervision-day';
30
+ export interface BookingOffering {
31
+ id: string;
32
+ kind: BookingKind;
33
+ enabled: boolean;
34
+ }
35
+ export interface BookingPageOptions {
36
+ /** The nextCursor returned by the previous page, with the same filters. */
37
+ afterId?: string;
38
+ /** Defaults to 50. Accepts 1 through 100. */
39
+ limit?: number;
40
+ }
41
+ export interface ListBookingsOptions extends BookingPageOptions {
42
+ projectId?: string;
43
+ }
44
+ export interface BookingPage<T> {
45
+ items: T[];
46
+ nextCursor: string | null;
47
+ }
30
48
  /** A minute-precision local timestamp with explicit UTC offset, e.g. YYYY-MM-DDTHH:mm-06:00. */
31
49
  export interface WallWindow {
32
50
  start: string;
@@ -44,6 +62,12 @@ export interface AvailabilityWindow extends WallWindow {
44
62
  /** May this exact local coverage window be proposed as a supervision day? */
45
63
  supervision: boolean;
46
64
  }
65
+ export interface DeclaredAvailabilityWindow extends ResolvedBookingWindow, AvailabilityWindow {
66
+ }
67
+ export interface ProviderAvailability {
68
+ providerId: string;
69
+ windows: DeclaredAvailabilityWindow[];
70
+ }
47
71
  export interface AvailabilityInput {
48
72
  providerId: string;
49
73
  zone: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,gBAAgB,GACxB,oBAAoB,GACpB,sBAAsB,GACtB,SAAS,GACT,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,QAAQ,GACR,MAAM,GACN,SAAS,CAAC;AAEd,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,eAAe,CAAC;CAChC;AAED,mFAAmF;AACnF,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CACJ,OAAO,EAAE,QAAQ,CAAC,cAAc,CAAC,EACjC,SAAS,EAAE,gBAAgB,GAC1B,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IACjC,gBAAgB,CACd,OAAO,EAAE,QAAQ,CAAC,cAAc,CAAC,EACjC,UAAU,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GACtC,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAED,MAAM,MAAM,WAAW,GAAG,gBAAgB,GAAG,iBAAiB,CAAC;AAE/D,gGAAgG;AAChG,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACxC;AAED,+FAA+F;AAC/F,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,YAAY,EAAE,eAAe,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,aAAa,CAAC;IACrB,YAAY,CAAC,EAAE,eAAe,CAAC;CAChC;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,UAAU,GACV,WAAW,GACX,UAAU,GACV,WAAW,CAAC;AAEhB,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,EAAE,EAAE,MAAM,CAAC;CACZ"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/windows.d.ts CHANGED
@@ -1,18 +1,14 @@
1
- import type { AvailabilityWindow, BookingWindow, ResolvedBookingWindow } from './types.js';
1
+ import type { AvailabilityWindow, BookingWindow, DeclaredAvailabilityWindow, ResolvedBookingWindow } from './types.js';
2
2
  /** Resolve half-open wall time; a DST fold must name the intended offset. */
3
3
  export declare function resolveBookingWindow(window: BookingWindow): ResolvedBookingWindow;
4
4
  export declare function elapsedHour(window: ResolvedBookingWindow): boolean;
5
- export interface DeclaredWindow extends ResolvedBookingWindow {
6
- capacity: number;
7
- supervision: boolean;
8
- }
9
- export declare function declareWindows(zone: string, windows: readonly AvailabilityWindow[]): DeclaredWindow[];
5
+ export declare function declareWindows(zone: string, windows: readonly AvailabilityWindow[]): DeclaredAvailabilityWindow[];
10
6
  export interface Reservation {
11
7
  startUtc: string;
12
8
  endUtc: string;
13
9
  exclusive: boolean;
14
10
  }
15
- export declare function supervisionDeclared(candidate: ResolvedBookingWindow, windows: readonly DeclaredWindow[]): boolean;
11
+ export declare function supervisionDeclared(candidate: ResolvedBookingWindow, windows: readonly DeclaredAvailabilityWindow[]): boolean;
16
12
  /** Check every segment; endpoints belonging to ending windows are excluded. */
17
- export declare function fitsCapacity(candidate: Reservation, declared: readonly DeclaredWindow[], occupied: readonly Reservation[]): boolean;
13
+ export declare function fitsCapacity(candidate: Reservation, declared: readonly DeclaredAvailabilityWindow[], occupied: readonly Reservation[]): boolean;
18
14
  //# sourceMappingURL=windows.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"windows.d.ts","sourceRoot":"","sources":["../src/windows.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,kBAAkB,EAClB,aAAa,EACb,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAqDpB,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,aAAa,GACpB,qBAAqB,CAiBvB;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAIlE;AAYD,MAAM,WAAW,cAAe,SAAQ,qBAAqB;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,kBAAkB,EAAE,GACrC,cAAc,EAAE,CAuClB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,qBAAqB,EAChC,OAAO,EAAE,SAAS,cAAc,EAAE,GACjC,OAAO,CAQT;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAC1B,SAAS,EAAE,WAAW,EACtB,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAC/B,OAAO,CAwDT"}
1
+ {"version":3,"file":"windows.d.ts","sourceRoot":"","sources":["../src/windows.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,kBAAkB,EAClB,aAAa,EACb,0BAA0B,EAC1B,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAqDpB,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,aAAa,GACpB,qBAAqB,CAiBvB;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAIlE;AAYD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,kBAAkB,EAAE,GACrC,0BAA0B,EAAE,CAuC9B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,qBAAqB,EAChC,OAAO,EAAE,SAAS,0BAA0B,EAAE,GAC7C,OAAO,CAQT;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAC1B,SAAS,EAAE,WAAW,EACtB,QAAQ,EAAE,SAAS,0BAA0B,EAAE,EAC/C,QAAQ,EAAE,SAAS,WAAW,EAAE,GAC/B,OAAO,CAwDT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-bookings",
3
- "version": "0.60.1",
3
+ "version": "0.60.2",
4
4
  "description": "Operator-managed service reservations for SMRT",
5
5
  "type": "module",
6
6
  "author": "HappyVertical",
@@ -22,7 +22,7 @@
22
22
  "@happyvertical/sql": "^0.111.3"
23
23
  },
24
24
  "devDependencies": {
25
- "@happyvertical/smrt-vitest": "0.60.1",
25
+ "@happyvertical/smrt-vitest": "0.60.2",
26
26
  "@types/node": "24.13.6",
27
27
  "typescript": "6.0.3",
28
28
  "vite": "8.1.6",