@voyant-travel/availability 0.0.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-core.d.ts","sourceRoot":"","sources":["../src/schema-core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,188 @@
1
+ import { typeId, typeIdRef } from "@voyant-travel/db/lib/typeid-column";
2
+ import { sql } from "drizzle-orm";
3
+ import { boolean, date, index, integer, jsonb, pgEnum, pgTable, text, timestamp, uniqueIndex, } from "drizzle-orm/pg-core";
4
+ export const availabilitySlotStatusEnum = pgEnum("availability_slot_status", [
5
+ "open",
6
+ "closed",
7
+ "sold_out",
8
+ "cancelled",
9
+ ]);
10
+ export const meetingModeEnum = pgEnum("meeting_mode", [
11
+ "meeting_only",
12
+ "pickup_only",
13
+ "meet_or_pickup",
14
+ ]);
15
+ export const pickupGroupKindEnum = pgEnum("pickup_group_kind", ["pickup", "dropoff", "meeting"]);
16
+ export const pickupTimingModeEnum = pgEnum("pickup_timing_mode", [
17
+ "fixed_time",
18
+ "offset_from_start",
19
+ ]);
20
+ export const availabilityRules = pgTable("availability_rules", {
21
+ id: typeId("availability_rules"),
22
+ productId: text("product_id").notNull(),
23
+ optionId: text("option_id"),
24
+ facilityId: text("facility_id"),
25
+ timezone: text("timezone").notNull(),
26
+ recurrenceRule: text("recurrence_rule").notNull(),
27
+ maxCapacity: integer("max_capacity").notNull(),
28
+ maxPickupCapacity: integer("max_pickup_capacity"),
29
+ minTotalPax: integer("min_total_pax"),
30
+ cutoffMinutes: integer("cutoff_minutes"),
31
+ earlyBookingLimitMinutes: integer("early_booking_limit_minutes"),
32
+ active: boolean("active").notNull().default(true),
33
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
34
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
35
+ }, (table) => [
36
+ index("idx_availability_rules_updated").on(table.updatedAt),
37
+ index("idx_availability_rules_product_updated").on(table.productId, table.updatedAt),
38
+ index("idx_availability_rules_option_updated").on(table.optionId, table.updatedAt),
39
+ index("idx_availability_rules_facility_updated").on(table.facilityId, table.updatedAt),
40
+ index("idx_availability_rules_active_updated").on(table.active, table.updatedAt),
41
+ ]);
42
+ export const availabilityStartTimes = pgTable("availability_start_times", {
43
+ id: typeId("availability_start_times"),
44
+ productId: text("product_id").notNull(),
45
+ optionId: text("option_id"),
46
+ facilityId: text("facility_id"),
47
+ label: text("label"),
48
+ startTimeLocal: text("start_time_local").notNull(),
49
+ durationMinutes: integer("duration_minutes"),
50
+ sortOrder: integer("sort_order").notNull().default(0),
51
+ active: boolean("active").notNull().default(true),
52
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
53
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
54
+ }, (table) => [
55
+ index("idx_availability_start_times_product_sort_created").on(table.productId, table.sortOrder, table.createdAt),
56
+ index("idx_availability_start_times_option_sort_created").on(table.optionId, table.sortOrder, table.createdAt),
57
+ index("idx_availability_start_times_facility_sort_created").on(table.facilityId, table.sortOrder, table.createdAt),
58
+ index("idx_availability_start_times_active_sort_created").on(table.active, table.sortOrder, table.createdAt),
59
+ ]);
60
+ export const availabilitySlots = pgTable("availability_slots", {
61
+ id: typeId("availability_slots"),
62
+ productId: text("product_id").notNull(),
63
+ itineraryId: text("itinerary_id"),
64
+ optionId: text("option_id"),
65
+ facilityId: text("facility_id"),
66
+ availabilityRuleId: typeIdRef("availability_rule_id").references(() => availabilityRules.id, {
67
+ onDelete: "set null",
68
+ }),
69
+ startTimeId: typeIdRef("start_time_id").references(() => availabilityStartTimes.id, {
70
+ onDelete: "set null",
71
+ }),
72
+ dateLocal: date("date_local").notNull(),
73
+ startsAt: timestamp("starts_at", { withTimezone: true }).notNull(),
74
+ endsAt: timestamp("ends_at", { withTimezone: true }),
75
+ timezone: text("timezone").notNull(),
76
+ status: availabilitySlotStatusEnum("status").notNull().default("open"),
77
+ unlimited: boolean("unlimited").notNull().default(false),
78
+ initialPax: integer("initial_pax"),
79
+ remainingPax: integer("remaining_pax"),
80
+ initialPickups: integer("initial_pickups"),
81
+ remainingPickups: integer("remaining_pickups"),
82
+ remainingResources: integer("remaining_resources"),
83
+ pastCutoff: boolean("past_cutoff").notNull().default(false),
84
+ tooEarly: boolean("too_early").notNull().default(false),
85
+ nights: integer("nights"),
86
+ days: integer("days"),
87
+ notes: text("notes"),
88
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
89
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
90
+ }, (table) => [
91
+ index("idx_availability_slots_product_starts_at").on(table.productId, table.startsAt),
92
+ index("idx_availability_slots_itinerary_starts_at").on(table.itineraryId, table.startsAt),
93
+ index("idx_availability_slots_option_starts_at").on(table.optionId, table.startsAt),
94
+ index("idx_availability_slots_facility_starts_at").on(table.facilityId, table.startsAt),
95
+ index("idx_availability_slots_rule_starts_at").on(table.availabilityRuleId, table.startsAt),
96
+ index("idx_availability_slots_start_time_starts_at").on(table.startTimeId, table.startsAt),
97
+ index("idx_availability_slots_date_starts_at").on(table.dateLocal, table.startsAt),
98
+ index("idx_availability_slots_status_starts_at").on(table.status, table.startsAt),
99
+ // Bare starts_at index for date-range scans that don't lead with a
100
+ // product/status column (dashboard aggregates' from..to window).
101
+ index("idx_availability_slots_starts_at").on(table.startsAt),
102
+ ]);
103
+ export const availabilityCloseouts = pgTable("availability_closeouts", {
104
+ id: typeId("availability_closeouts"),
105
+ productId: text("product_id").notNull(),
106
+ slotId: typeIdRef("slot_id").references(() => availabilitySlots.id, { onDelete: "set null" }),
107
+ dateLocal: date("date_local").notNull(),
108
+ reason: text("reason"),
109
+ createdBy: text("created_by"),
110
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
111
+ }, (table) => [
112
+ index("idx_availability_closeouts_product_created").on(table.productId, table.createdAt),
113
+ index("idx_availability_closeouts_slot_created").on(table.slotId, table.createdAt),
114
+ index("idx_availability_closeouts_date_created").on(table.dateLocal, table.createdAt),
115
+ ]);
116
+ export const allocationResources = pgTable("allocation_resources", {
117
+ id: typeId("allocation_resources"),
118
+ slotId: typeIdRef("slot_id")
119
+ .notNull()
120
+ .references(() => availabilitySlots.id, { onDelete: "cascade" }),
121
+ kind: text("kind").notNull(),
122
+ refType: text("ref_type"),
123
+ refId: text("ref_id"),
124
+ label: text("label"),
125
+ capacity: integer("capacity").notNull(),
126
+ flags: jsonb("flags").$type().notNull().default({}),
127
+ parentId: text("parent_id"),
128
+ sortOrder: integer("sort_order").notNull().default(0),
129
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
130
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
131
+ }, (table) => [
132
+ index("idx_allocation_resources_slot_kind").on(table.slotId, table.kind),
133
+ index("idx_allocation_resources_parent").on(table.parentId),
134
+ index("idx_allocation_resources_kind_sort").on(table.kind, table.sortOrder, table.createdAt),
135
+ ]);
136
+ export const sharingGroupLabels = pgTable("sharing_group_labels", {
137
+ groupId: text("group_id").primaryKey(),
138
+ label: text("label").notNull(),
139
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
140
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
141
+ });
142
+ export const allocationAuditLog = pgTable("allocation_audit_log", {
143
+ id: typeId("allocation_audit_log"),
144
+ slotId: typeIdRef("slot_id")
145
+ .notNull()
146
+ .references(() => availabilitySlots.id, { onDelete: "cascade" }),
147
+ action: text("action").notNull(),
148
+ actorId: text("actor_id"),
149
+ travelerId: text("traveler_id"),
150
+ resourceId: text("resource_id"),
151
+ before: jsonb("before").$type(),
152
+ after: jsonb("after").$type(),
153
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
154
+ }, (table) => [
155
+ index("idx_allocation_audit_slot_created").on(table.slotId, table.createdAt),
156
+ index("idx_allocation_audit_traveler").on(table.travelerId),
157
+ ]);
158
+ export const productOptionResourceTemplates = pgTable("product_option_resource_templates", {
159
+ id: typeId("product_option_resource_templates"),
160
+ productOptionId: text("product_option_id").notNull(),
161
+ kind: text("kind").notNull(),
162
+ refType: text("ref_type"),
163
+ refId: text("ref_id"),
164
+ capacity: integer("capacity").notNull(),
165
+ namePattern: text("name_pattern").notNull(),
166
+ layout: text("layout"),
167
+ /**
168
+ * How many resources to instantiate per slot when auto-materialising
169
+ * from this template (e.g. "5 SGL, 20 DBL"). Null skips the
170
+ * template during slot-publish auto-seed — admins must seed those
171
+ * resources manually or via `autoMaterializeAllocationResources`
172
+ * once bookings exist.
173
+ */
174
+ defaultCount: integer("default_count"),
175
+ flags: jsonb("flags").$type().notNull().default({}),
176
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
177
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
178
+ }, (table) => [
179
+ // Unique per (option, kind, ref) — `COALESCE(ref_id,'')` keeps "one
180
+ // non-ref template per kind" while allowing multiple unit-keyed room
181
+ // templates under one option (Single/Double/Triple all kind="room",
182
+ // distinguished by their option_unit ref). The allocator's option_unit
183
+ // matching depends on this.
184
+ uniqueIndex("idx_product_option_resource_templates_option_kind").on(table.productOptionId, table.kind,
185
+ // agent-quality: raw-sql reviewed -- owner: availability; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
186
+ sql `coalesce(${table.refId}, '')`),
187
+ index("idx_product_option_resource_templates_kind").on(table.kind, table.createdAt),
188
+ ]);
@@ -0,0 +1,191 @@
1
+ /**
2
+ * `availability_holds` — soft holds against an availability slot,
3
+ * tied to a `booking_drafts` row. Decrements
4
+ * `availability_slots.remainingPax` while live; the reaper releases
5
+ * stale holds and increments capacity back.
6
+ *
7
+ * Per booking-journey-architecture §5.7 + §6 — the doc proposes
8
+ * `bookingAllocations` for owned holds, but allocations require a
9
+ * `booking_id` FK and journey holds are pre-booking. A dedicated
10
+ * table avoids the chicken-and-egg.
11
+ */
12
+ export declare const availabilityHolds: import("drizzle-orm/pg-core").PgTableWithColumns<{
13
+ name: "availability_holds";
14
+ schema: undefined;
15
+ columns: {
16
+ id: import("drizzle-orm/pg-core").PgColumn<{
17
+ name: string;
18
+ tableName: "availability_holds";
19
+ dataType: "string";
20
+ columnType: "PgText";
21
+ data: string;
22
+ driverParam: string;
23
+ notNull: true;
24
+ hasDefault: true;
25
+ isPrimaryKey: true;
26
+ isAutoincrement: false;
27
+ hasRuntimeDefault: true;
28
+ enumValues: [string, ...string[]];
29
+ baseColumn: never;
30
+ identity: undefined;
31
+ generated: undefined;
32
+ }, {}, {}>;
33
+ draftId: import("drizzle-orm/pg-core").PgColumn<{
34
+ name: "draft_id";
35
+ tableName: "availability_holds";
36
+ dataType: "string";
37
+ columnType: "PgText";
38
+ data: string;
39
+ driverParam: string;
40
+ notNull: true;
41
+ hasDefault: false;
42
+ isPrimaryKey: false;
43
+ isAutoincrement: false;
44
+ hasRuntimeDefault: false;
45
+ enumValues: [string, ...string[]];
46
+ baseColumn: never;
47
+ identity: undefined;
48
+ generated: undefined;
49
+ }, {}, {}>;
50
+ holdToken: import("drizzle-orm/pg-core").PgColumn<{
51
+ name: "hold_token";
52
+ tableName: "availability_holds";
53
+ dataType: "string";
54
+ columnType: "PgText";
55
+ data: string;
56
+ driverParam: string;
57
+ notNull: true;
58
+ hasDefault: false;
59
+ isPrimaryKey: false;
60
+ isAutoincrement: false;
61
+ hasRuntimeDefault: false;
62
+ enumValues: [string, ...string[]];
63
+ baseColumn: never;
64
+ identity: undefined;
65
+ generated: undefined;
66
+ }, {}, {}>;
67
+ productId: import("drizzle-orm/pg-core").PgColumn<{
68
+ name: "product_id";
69
+ tableName: "availability_holds";
70
+ dataType: "string";
71
+ columnType: "PgText";
72
+ data: string;
73
+ driverParam: string;
74
+ notNull: true;
75
+ hasDefault: false;
76
+ isPrimaryKey: false;
77
+ isAutoincrement: false;
78
+ hasRuntimeDefault: false;
79
+ enumValues: [string, ...string[]];
80
+ baseColumn: never;
81
+ identity: undefined;
82
+ generated: undefined;
83
+ }, {}, {}>;
84
+ slotId: import("drizzle-orm/pg-core").PgColumn<{
85
+ name: string;
86
+ tableName: "availability_holds";
87
+ dataType: "string";
88
+ columnType: "PgText";
89
+ data: string;
90
+ driverParam: string;
91
+ notNull: true;
92
+ hasDefault: false;
93
+ isPrimaryKey: false;
94
+ isAutoincrement: false;
95
+ hasRuntimeDefault: false;
96
+ enumValues: [string, ...string[]];
97
+ baseColumn: never;
98
+ identity: undefined;
99
+ generated: undefined;
100
+ }, {}, {}>;
101
+ paxCount: import("drizzle-orm/pg-core").PgColumn<{
102
+ name: "pax_count";
103
+ tableName: "availability_holds";
104
+ dataType: "number";
105
+ columnType: "PgInteger";
106
+ data: number;
107
+ driverParam: string | number;
108
+ notNull: true;
109
+ hasDefault: false;
110
+ isPrimaryKey: false;
111
+ isAutoincrement: false;
112
+ hasRuntimeDefault: false;
113
+ enumValues: undefined;
114
+ baseColumn: never;
115
+ identity: undefined;
116
+ generated: undefined;
117
+ }, {}, {}>;
118
+ expiresAt: import("drizzle-orm/pg-core").PgColumn<{
119
+ name: "expires_at";
120
+ tableName: "availability_holds";
121
+ dataType: "date";
122
+ columnType: "PgTimestamp";
123
+ data: Date;
124
+ driverParam: string;
125
+ notNull: true;
126
+ hasDefault: false;
127
+ isPrimaryKey: false;
128
+ isAutoincrement: false;
129
+ hasRuntimeDefault: false;
130
+ enumValues: undefined;
131
+ baseColumn: never;
132
+ identity: undefined;
133
+ generated: undefined;
134
+ }, {}, {}>;
135
+ releasedAt: import("drizzle-orm/pg-core").PgColumn<{
136
+ name: "released_at";
137
+ tableName: "availability_holds";
138
+ dataType: "date";
139
+ columnType: "PgTimestamp";
140
+ data: Date;
141
+ driverParam: string;
142
+ notNull: false;
143
+ hasDefault: false;
144
+ isPrimaryKey: false;
145
+ isAutoincrement: false;
146
+ hasRuntimeDefault: false;
147
+ enumValues: undefined;
148
+ baseColumn: never;
149
+ identity: undefined;
150
+ generated: undefined;
151
+ }, {}, {}>;
152
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
153
+ name: "created_at";
154
+ tableName: "availability_holds";
155
+ dataType: "date";
156
+ columnType: "PgTimestamp";
157
+ data: Date;
158
+ driverParam: string;
159
+ notNull: true;
160
+ hasDefault: true;
161
+ isPrimaryKey: false;
162
+ isAutoincrement: false;
163
+ hasRuntimeDefault: false;
164
+ enumValues: undefined;
165
+ baseColumn: never;
166
+ identity: undefined;
167
+ generated: undefined;
168
+ }, {}, {}>;
169
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
170
+ name: "updated_at";
171
+ tableName: "availability_holds";
172
+ dataType: "date";
173
+ columnType: "PgTimestamp";
174
+ data: Date;
175
+ driverParam: string;
176
+ notNull: true;
177
+ hasDefault: true;
178
+ isPrimaryKey: false;
179
+ isAutoincrement: false;
180
+ hasRuntimeDefault: false;
181
+ enumValues: undefined;
182
+ baseColumn: never;
183
+ identity: undefined;
184
+ generated: undefined;
185
+ }, {}, {}>;
186
+ };
187
+ dialect: "pg";
188
+ }>;
189
+ export type AvailabilityHold = typeof availabilityHolds.$inferSelect;
190
+ export type NewAvailabilityHold = typeof availabilityHolds.$inferInsert;
191
+ //# sourceMappingURL=schema-holds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-holds.d.ts","sourceRoot":"","sources":["../src/schema-holds.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyB7B,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG,OAAO,iBAAiB,CAAC,YAAY,CAAA;AACpE,MAAM,MAAM,mBAAmB,GAAG,OAAO,iBAAiB,CAAC,YAAY,CAAA"}
@@ -0,0 +1,36 @@
1
+ import { typeId, typeIdRef } from "@voyant-travel/db/lib/typeid-column";
2
+ import { index, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
3
+ import { availabilitySlots } from "./schema-core.js";
4
+ /**
5
+ * `availability_holds` — soft holds against an availability slot,
6
+ * tied to a `booking_drafts` row. Decrements
7
+ * `availability_slots.remainingPax` while live; the reaper releases
8
+ * stale holds and increments capacity back.
9
+ *
10
+ * Per booking-journey-architecture §5.7 + §6 — the doc proposes
11
+ * `bookingAllocations` for owned holds, but allocations require a
12
+ * `booking_id` FK and journey holds are pre-booking. A dedicated
13
+ * table avoids the chicken-and-egg.
14
+ */
15
+ export const availabilityHolds = pgTable("availability_holds", {
16
+ id: typeId("availability_holds"),
17
+ /** Plain text — booking_drafts lives in @voyant-travel/catalog. */
18
+ draftId: text("draft_id").notNull(),
19
+ /** Token returned to callers; uniquely identifies the hold for
20
+ * extend/release. */
21
+ holdToken: text("hold_token").notNull(),
22
+ productId: text("product_id").notNull(),
23
+ slotId: typeIdRef("slot_id")
24
+ .notNull()
25
+ .references(() => availabilitySlots.id, { onDelete: "cascade" }),
26
+ paxCount: integer("pax_count").notNull(),
27
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
28
+ releasedAt: timestamp("released_at", { withTimezone: true }),
29
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
30
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
31
+ }, (table) => [
32
+ index("idx_availability_holds_slot").on(table.slotId),
33
+ index("idx_availability_holds_draft").on(table.draftId),
34
+ index("idx_availability_holds_token").on(table.holdToken),
35
+ index("idx_availability_holds_expires").on(table.expiresAt),
36
+ ]);