@voyantjs/finance 0.6.9 → 0.8.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.
Files changed (47) hide show
  1. package/dist/index.d.ts +9 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +6 -1
  4. package/dist/routes-bookings-quick-create.d.ts +3 -0
  5. package/dist/routes-bookings-quick-create.d.ts.map +1 -0
  6. package/dist/routes-bookings-quick-create.js +103 -0
  7. package/dist/routes-public.d.ts +22 -22
  8. package/dist/routes.d.ts +279 -18
  9. package/dist/routes.d.ts.map +1 -1
  10. package/dist/routes.js +57 -1
  11. package/dist/schema.d.ts +451 -0
  12. package/dist/schema.d.ts.map +1 -1
  13. package/dist/schema.js +79 -0
  14. package/dist/service-aggregates.d.ts +47 -0
  15. package/dist/service-aggregates.d.ts.map +1 -0
  16. package/dist/service-aggregates.js +106 -0
  17. package/dist/service-bookings-dual-create.d.ts +185 -0
  18. package/dist/service-bookings-dual-create.d.ts.map +1 -0
  19. package/dist/service-bookings-dual-create.js +131 -0
  20. package/dist/service-bookings-quick-create.d.ts +168 -0
  21. package/dist/service-bookings-quick-create.d.ts.map +1 -0
  22. package/dist/service-bookings-quick-create.js +312 -0
  23. package/dist/service-public.d.ts +11 -11
  24. package/dist/service-public.d.ts.map +1 -1
  25. package/dist/service-public.js +79 -36
  26. package/dist/service-vouchers-migration.d.ts +44 -0
  27. package/dist/service-vouchers-migration.d.ts.map +1 -0
  28. package/dist/service-vouchers-migration.js +147 -0
  29. package/dist/service-vouchers.d.ts +157 -0
  30. package/dist/service-vouchers.d.ts.map +1 -0
  31. package/dist/service-vouchers.js +191 -0
  32. package/dist/service.d.ts +180 -17
  33. package/dist/service.d.ts.map +1 -1
  34. package/dist/service.js +4 -0
  35. package/dist/validation-public.d.ts +2 -2
  36. package/dist/validation-public.d.ts.map +1 -1
  37. package/dist/validation-public.js +4 -1
  38. package/dist/validation-shared.d.ts +17 -0
  39. package/dist/validation-shared.d.ts.map +1 -1
  40. package/dist/validation-shared.js +12 -0
  41. package/dist/validation-vouchers.d.ts +62 -0
  42. package/dist/validation-vouchers.d.ts.map +1 -0
  43. package/dist/validation-vouchers.js +46 -0
  44. package/dist/validation.d.ts +1 -0
  45. package/dist/validation.d.ts.map +1 -1
  46. package/dist/validation.js +1 -0
  47. package/package.json +9 -8
@@ -0,0 +1,106 @@
1
+ import { and, inArray, ne, sql } from "drizzle-orm";
2
+ import { invoices } from "./schema.js";
3
+ const ALL_INVOICE_STATUSES = [
4
+ "draft",
5
+ "sent",
6
+ "partially_paid",
7
+ "paid",
8
+ "overdue",
9
+ "void",
10
+ ];
11
+ /** Statuses where balance_due_cents > 0 is meaningful money we're owed. */
12
+ const OUTSTANDING_STATUSES = ["sent", "partially_paid", "overdue"];
13
+ export async function getFinanceAggregates(db, options = {}) {
14
+ const fromDate = options.from ? new Date(options.from) : undefined;
15
+ const toDate = options.to ? new Date(options.to) : undefined;
16
+ const rangeConditions = [];
17
+ if (fromDate)
18
+ rangeConditions.push(sql `${invoices.createdAt} >= ${fromDate}`);
19
+ if (toDate)
20
+ rangeConditions.push(sql `${invoices.createdAt} < ${toDate}`);
21
+ const rangeWhere = rangeConditions.length ? and(...rangeConditions) : undefined;
22
+ const [totalRow] = await db
23
+ .select({ count: sql `count(*)::int` })
24
+ .from(invoices)
25
+ .where(rangeWhere);
26
+ const statusRows = await db
27
+ .select({
28
+ status: invoices.status,
29
+ count: sql `count(*)::int`,
30
+ })
31
+ .from(invoices)
32
+ .where(rangeWhere)
33
+ .groupBy(invoices.status);
34
+ const countsByStatusMap = new Map(statusRows.map((row) => [row.status, row.count]));
35
+ const monthlyInvoiceCountsRows = await db
36
+ .select({
37
+ yearMonth: sql `to_char(${invoices.createdAt} at time zone 'UTC', 'YYYY-MM')`,
38
+ count: sql `count(*)::int`,
39
+ })
40
+ .from(invoices)
41
+ .where(rangeWhere)
42
+ .groupBy(sql `to_char(${invoices.createdAt} at time zone 'UTC', 'YYYY-MM')`)
43
+ .orderBy(sql `to_char(${invoices.createdAt} at time zone 'UTC', 'YYYY-MM')`);
44
+ const monthlyRevenueRows = await db
45
+ .select({
46
+ yearMonth: sql `to_char(${invoices.createdAt} at time zone 'UTC', 'YYYY-MM')`,
47
+ currency: invoices.currency,
48
+ totalCents: sql `coalesce(sum(${invoices.totalCents}), 0)::bigint`,
49
+ })
50
+ .from(invoices)
51
+ .where(and(...(rangeConditions.length ? rangeConditions : []), ne(invoices.status, "void")))
52
+ .groupBy(sql `to_char(${invoices.createdAt} at time zone 'UTC', 'YYYY-MM')`, invoices.currency)
53
+ .orderBy(sql `to_char(${invoices.createdAt} at time zone 'UTC', 'YYYY-MM')`, invoices.currency);
54
+ // Outstanding + overdue always look at the whole book (not the date range),
55
+ // since "what are we owed right now" is a point-in-time question — bounding
56
+ // it by `from..to` would hide old unpaid invoices.
57
+ const outstandingRows = await db
58
+ .select({
59
+ currency: invoices.currency,
60
+ balanceDueCents: sql `coalesce(sum(${invoices.balanceDueCents}), 0)::bigint`,
61
+ count: sql `count(*)::int`,
62
+ })
63
+ .from(invoices)
64
+ .where(and(inArray(invoices.status, [...OUTSTANDING_STATUSES]), sql `${invoices.balanceDueCents} > 0`))
65
+ .groupBy(invoices.currency)
66
+ .orderBy(invoices.currency);
67
+ const todayUtc = new Date();
68
+ todayUtc.setUTCHours(0, 0, 0, 0);
69
+ const todayDateString = todayUtc.toISOString().slice(0, 10);
70
+ const overdueRows = await db
71
+ .select({
72
+ currency: invoices.currency,
73
+ balanceDueCents: sql `coalesce(sum(${invoices.balanceDueCents}), 0)::bigint`,
74
+ count: sql `count(*)::int`,
75
+ })
76
+ .from(invoices)
77
+ .where(and(inArray(invoices.status, [...OUTSTANDING_STATUSES]), sql `${invoices.balanceDueCents} > 0`, sql `${invoices.dueDate} < ${todayDateString}`))
78
+ .groupBy(invoices.currency)
79
+ .orderBy(invoices.currency);
80
+ return {
81
+ total: totalRow?.count ?? 0,
82
+ countsByStatus: ALL_INVOICE_STATUSES.map((status) => ({
83
+ status,
84
+ count: countsByStatusMap.get(status) ?? 0,
85
+ })),
86
+ monthlyRevenue: monthlyRevenueRows.map((row) => ({
87
+ yearMonth: row.yearMonth,
88
+ currency: row.currency,
89
+ totalCents: Number(row.totalCents),
90
+ })),
91
+ monthlyInvoiceCounts: monthlyInvoiceCountsRows.map((row) => ({
92
+ yearMonth: row.yearMonth,
93
+ count: row.count,
94
+ })),
95
+ outstanding: outstandingRows.map((row) => ({
96
+ currency: row.currency,
97
+ balanceDueCents: Number(row.balanceDueCents),
98
+ count: row.count,
99
+ })),
100
+ overdue: overdueRows.map((row) => ({
101
+ currency: row.currency,
102
+ balanceDueCents: Number(row.balanceDueCents),
103
+ count: row.count,
104
+ })),
105
+ };
106
+ }
@@ -0,0 +1,185 @@
1
+ import type { BookingGroup, BookingGroupMember } from "@voyantjs/bookings/schema";
2
+ import type { EventBus } from "@voyantjs/core";
3
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
4
+ import { z } from "zod";
5
+ import { type QuickCreateBookingOutcome, type QuickCreateBookingResult } from "./service-bookings-quick-create.js";
6
+ export declare const dualCreateBookingSchema: z.ZodObject<{
7
+ primary: z.ZodObject<{
8
+ organizationId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
9
+ bookingNumber: z.ZodString;
10
+ personId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
11
+ internalNotes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
12
+ productId: z.ZodString;
13
+ optionId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
14
+ travelers: z.ZodOptional<z.ZodArray<z.ZodObject<{
15
+ firstName: z.ZodString;
16
+ lastName: z.ZodString;
17
+ email: z.ZodNullable<z.ZodOptional<z.ZodString>>;
18
+ phone: z.ZodNullable<z.ZodOptional<z.ZodString>>;
19
+ personId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
20
+ participantType: z.ZodDefault<z.ZodEnum<{
21
+ other: "other";
22
+ traveler: "traveler";
23
+ occupant: "occupant";
24
+ }>>;
25
+ travelerCategory: z.ZodNullable<z.ZodOptional<z.ZodEnum<{
26
+ other: "other";
27
+ adult: "adult";
28
+ child: "child";
29
+ infant: "infant";
30
+ senior: "senior";
31
+ }>>>;
32
+ preferredLanguage: z.ZodNullable<z.ZodOptional<z.ZodString>>;
33
+ accessibilityNeeds: z.ZodNullable<z.ZodOptional<z.ZodString>>;
34
+ specialRequests: z.ZodNullable<z.ZodOptional<z.ZodString>>;
35
+ roomUnitId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
36
+ isPrimary: z.ZodNullable<z.ZodOptional<z.ZodBoolean>>;
37
+ notes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
38
+ }, z.core.$strip>>>;
39
+ slotId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
40
+ paymentSchedules: z.ZodOptional<z.ZodArray<z.ZodObject<{
41
+ scheduleType: z.ZodDefault<z.ZodEnum<{
42
+ other: "other";
43
+ deposit: "deposit";
44
+ installment: "installment";
45
+ balance: "balance";
46
+ hold: "hold";
47
+ }>>;
48
+ status: z.ZodDefault<z.ZodEnum<{
49
+ pending: "pending";
50
+ expired: "expired";
51
+ cancelled: "cancelled";
52
+ paid: "paid";
53
+ due: "due";
54
+ waived: "waived";
55
+ }>>;
56
+ dueDate: z.ZodString;
57
+ currency: z.ZodString;
58
+ amountCents: z.ZodNumber;
59
+ notes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
60
+ }, z.core.$strip>>>;
61
+ voucherRedemption: z.ZodOptional<z.ZodObject<{
62
+ voucherId: z.ZodString;
63
+ amountCents: z.ZodNumber;
64
+ }, z.core.$strip>>;
65
+ }, z.core.$strip>;
66
+ secondary: z.ZodObject<{
67
+ organizationId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
68
+ bookingNumber: z.ZodString;
69
+ personId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
70
+ internalNotes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
71
+ productId: z.ZodString;
72
+ optionId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
73
+ travelers: z.ZodOptional<z.ZodArray<z.ZodObject<{
74
+ firstName: z.ZodString;
75
+ lastName: z.ZodString;
76
+ email: z.ZodNullable<z.ZodOptional<z.ZodString>>;
77
+ phone: z.ZodNullable<z.ZodOptional<z.ZodString>>;
78
+ personId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
79
+ participantType: z.ZodDefault<z.ZodEnum<{
80
+ other: "other";
81
+ traveler: "traveler";
82
+ occupant: "occupant";
83
+ }>>;
84
+ travelerCategory: z.ZodNullable<z.ZodOptional<z.ZodEnum<{
85
+ other: "other";
86
+ adult: "adult";
87
+ child: "child";
88
+ infant: "infant";
89
+ senior: "senior";
90
+ }>>>;
91
+ preferredLanguage: z.ZodNullable<z.ZodOptional<z.ZodString>>;
92
+ accessibilityNeeds: z.ZodNullable<z.ZodOptional<z.ZodString>>;
93
+ specialRequests: z.ZodNullable<z.ZodOptional<z.ZodString>>;
94
+ roomUnitId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
95
+ isPrimary: z.ZodNullable<z.ZodOptional<z.ZodBoolean>>;
96
+ notes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
97
+ }, z.core.$strip>>>;
98
+ slotId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
99
+ paymentSchedules: z.ZodOptional<z.ZodArray<z.ZodObject<{
100
+ scheduleType: z.ZodDefault<z.ZodEnum<{
101
+ other: "other";
102
+ deposit: "deposit";
103
+ installment: "installment";
104
+ balance: "balance";
105
+ hold: "hold";
106
+ }>>;
107
+ status: z.ZodDefault<z.ZodEnum<{
108
+ pending: "pending";
109
+ expired: "expired";
110
+ cancelled: "cancelled";
111
+ paid: "paid";
112
+ due: "due";
113
+ waived: "waived";
114
+ }>>;
115
+ dueDate: z.ZodString;
116
+ currency: z.ZodString;
117
+ amountCents: z.ZodNumber;
118
+ notes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
119
+ }, z.core.$strip>>>;
120
+ voucherRedemption: z.ZodOptional<z.ZodObject<{
121
+ voucherId: z.ZodString;
122
+ amountCents: z.ZodNumber;
123
+ }, z.core.$strip>>;
124
+ }, z.core.$strip>;
125
+ group: z.ZodDefault<z.ZodObject<{
126
+ kind: z.ZodDefault<z.ZodEnum<{
127
+ other: "other";
128
+ shared_room: "shared_room";
129
+ }>>;
130
+ label: z.ZodNullable<z.ZodOptional<z.ZodString>>;
131
+ optionUnitId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
132
+ }, z.core.$strip>>;
133
+ }, z.core.$strip>;
134
+ export type DualCreateBookingInput = z.infer<typeof dualCreateBookingSchema>;
135
+ export interface DualCreateBookingRuntime {
136
+ eventBus?: EventBus;
137
+ }
138
+ export interface BookingDualCreatedEvent {
139
+ groupId: string;
140
+ primaryBookingId: string;
141
+ secondaryBookingId: string;
142
+ productId: string;
143
+ createdByUserId: string | null;
144
+ occurredAt: Date;
145
+ }
146
+ export interface DualCreateBookingResult {
147
+ primary: QuickCreateBookingResult;
148
+ secondary: QuickCreateBookingResult;
149
+ group: BookingGroup;
150
+ primaryMember: BookingGroupMember;
151
+ secondaryMember: BookingGroupMember;
152
+ }
153
+ export type DualCreateBookingOutcome = {
154
+ status: "ok";
155
+ result: DualCreateBookingResult;
156
+ } | {
157
+ status: "primary_failed" | "secondary_failed";
158
+ reason: Exclude<QuickCreateBookingOutcome, {
159
+ status: "ok";
160
+ }>;
161
+ };
162
+ /**
163
+ * Create two bookings linked via a new `booking_group`, atomically. The
164
+ * canonical operator flow: two travelers book a shared room ("partaj"), each
165
+ * gets their own booking, and both are attached to a new shared_room group
166
+ * so subsequent payment / cancellation decisions can fan out across the
167
+ * pair.
168
+ *
169
+ * Transaction shape:
170
+ * - Outer tx opens via `db.transaction`.
171
+ * - Inner: two savepoint-scoped `quickCreateBooking(tx, ...)` calls — the
172
+ * nested transactions drizzle opens use SAVEPOINTs, so partial failures
173
+ * surface up to this layer as non-ok outcomes.
174
+ * - If either fails, the outer tx throws `DualCreateAbort` so the whole
175
+ * thing rolls back (no orphan booking, no orphan group).
176
+ * - Group creation + both memberships run last, inside the same outer tx.
177
+ *
178
+ * Event emission (`booking.dual-created`) is post-commit — subscribers only
179
+ * hear about successful pairs.
180
+ */
181
+ export declare function dualCreateBooking(db: PostgresJsDatabase, rawInput: DualCreateBookingInput, options?: {
182
+ userId?: string;
183
+ runtime?: DualCreateBookingRuntime;
184
+ }): Promise<DualCreateBookingOutcome>;
185
+ //# sourceMappingURL=service-bookings-dual-create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-bookings-dual-create.d.ts","sourceRoot":"","sources":["../src/service-bookings-dual-create.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AACjF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,EACL,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAG9B,MAAM,oCAAoC,CAAA;AAkB3C,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAIlC,CAAA;AAEF,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAA;AAI5E,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAA;IACf,gBAAgB,EAAE,MAAM,CAAA;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,UAAU,EAAE,IAAI,CAAA;CACjB;AAID,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,wBAAwB,CAAA;IACjC,SAAS,EAAE,wBAAwB,CAAA;IACnC,KAAK,EAAE,YAAY,CAAA;IACnB,aAAa,EAAE,kBAAkB,CAAA;IACjC,eAAe,EAAE,kBAAkB,CAAA;CACpC;AAED,MAAM,MAAM,wBAAwB,GAChC;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAA;CAAE,GACjD;IACE,MAAM,EAAE,gBAAgB,GAAG,kBAAkB,CAAA;IAC7C,MAAM,EAAE,OAAO,CAAC,yBAAyB,EAAE;QAAE,MAAM,EAAE,IAAI,CAAA;KAAE,CAAC,CAAA;CAC7D,CAAA;AAqBL;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,kBAAkB,EACtB,QAAQ,EAAE,sBAAsB,EAChC,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,wBAAwB,CAAA;CAC9B,GACL,OAAO,CAAC,wBAAwB,CAAC,CAkFnC"}
@@ -0,0 +1,131 @@
1
+ import { bookingGroupsService } from "@voyantjs/bookings";
2
+ import { z } from "zod";
3
+ import { quickCreateBooking, quickCreateBookingSchema, } from "./service-bookings-quick-create.js";
4
+ // ---------- validation ----------
5
+ /**
6
+ * Sub-booking input. Takes the full quick-create payload minus `group
7
+ * Membership` — dual-create owns the group lifecycle (one new group, both
8
+ * bookings linked as members) so accepting a nested group override would
9
+ * just be an opportunity for the caller to desync.
10
+ */
11
+ const dualSubBookingSchema = quickCreateBookingSchema.omit({ groupMembership: true });
12
+ const dualCreateGroupSchema = z.object({
13
+ kind: z.enum(["shared_room", "other"]).default("shared_room"),
14
+ label: z.string().max(255).optional().nullable(),
15
+ optionUnitId: z.string().optional().nullable(),
16
+ });
17
+ export const dualCreateBookingSchema = z.object({
18
+ primary: dualSubBookingSchema,
19
+ secondary: dualSubBookingSchema,
20
+ group: dualCreateGroupSchema.default({ kind: "shared_room" }),
21
+ });
22
+ /**
23
+ * Thrown inside the outer tx to force drizzle to roll back both bookings +
24
+ * the group when one of the inner quick-create calls returns non-ok.
25
+ * Drizzle doesn't abort on a non-throwing tx callback, so we convert the
26
+ * discriminated outcome into a throw here.
27
+ */
28
+ class DualCreateAbort extends Error {
29
+ outcome;
30
+ constructor(outcome) {
31
+ super(outcome.status === "ok"
32
+ ? "dual-create aborted: ok (unexpected)"
33
+ : `dual-create aborted: ${outcome.status}:${outcome.reason.status}`);
34
+ this.outcome = outcome;
35
+ this.name = "DualCreateAbort";
36
+ }
37
+ }
38
+ // ---------- service ----------
39
+ /**
40
+ * Create two bookings linked via a new `booking_group`, atomically. The
41
+ * canonical operator flow: two travelers book a shared room ("partaj"), each
42
+ * gets their own booking, and both are attached to a new shared_room group
43
+ * so subsequent payment / cancellation decisions can fan out across the
44
+ * pair.
45
+ *
46
+ * Transaction shape:
47
+ * - Outer tx opens via `db.transaction`.
48
+ * - Inner: two savepoint-scoped `quickCreateBooking(tx, ...)` calls — the
49
+ * nested transactions drizzle opens use SAVEPOINTs, so partial failures
50
+ * surface up to this layer as non-ok outcomes.
51
+ * - If either fails, the outer tx throws `DualCreateAbort` so the whole
52
+ * thing rolls back (no orphan booking, no orphan group).
53
+ * - Group creation + both memberships run last, inside the same outer tx.
54
+ *
55
+ * Event emission (`booking.dual-created`) is post-commit — subscribers only
56
+ * hear about successful pairs.
57
+ */
58
+ export async function dualCreateBooking(db, rawInput, options = {}) {
59
+ const { userId, runtime } = options;
60
+ const input = dualCreateBookingSchema.parse(rawInput);
61
+ let result;
62
+ try {
63
+ result = await db.transaction(async (tx) => {
64
+ const primaryOutcome = await quickCreateBooking(tx, input.primary, { userId });
65
+ if (primaryOutcome.status !== "ok") {
66
+ throw new DualCreateAbort({ status: "primary_failed", reason: primaryOutcome });
67
+ }
68
+ const secondaryOutcome = await quickCreateBooking(tx, input.secondary, { userId });
69
+ if (secondaryOutcome.status !== "ok") {
70
+ throw new DualCreateAbort({ status: "secondary_failed", reason: secondaryOutcome });
71
+ }
72
+ const primaryBooking = primaryOutcome.result.booking;
73
+ const secondaryBooking = secondaryOutcome.result.booking;
74
+ const group = await bookingGroupsService.createBookingGroup(tx, {
75
+ kind: input.group.kind,
76
+ label: input.group.label ??
77
+ `Shared — ${primaryBooking.bookingNumber} + ${secondaryBooking.bookingNumber}`,
78
+ productId: input.primary.productId,
79
+ optionUnitId: input.group.optionUnitId ?? null,
80
+ primaryBookingId: primaryBooking.id,
81
+ });
82
+ const primaryMemberResult = await bookingGroupsService.addGroupMember(tx, group.id, {
83
+ bookingId: primaryBooking.id,
84
+ role: "primary",
85
+ });
86
+ if (primaryMemberResult.status !== "ok") {
87
+ // Shouldn't happen — we just created both rows in this tx — but bail
88
+ // through the same abort path to unwind cleanly.
89
+ throw new DualCreateAbort({
90
+ status: "primary_failed",
91
+ reason: { status: "group_not_found" },
92
+ });
93
+ }
94
+ const secondaryMemberResult = await bookingGroupsService.addGroupMember(tx, group.id, {
95
+ bookingId: secondaryBooking.id,
96
+ role: "shared",
97
+ });
98
+ if (secondaryMemberResult.status !== "ok") {
99
+ throw new DualCreateAbort({
100
+ status: "secondary_failed",
101
+ reason: { status: "group_not_found" },
102
+ });
103
+ }
104
+ return {
105
+ primary: primaryOutcome.result,
106
+ secondary: secondaryOutcome.result,
107
+ group,
108
+ primaryMember: primaryMemberResult.member,
109
+ secondaryMember: secondaryMemberResult.member,
110
+ };
111
+ });
112
+ }
113
+ catch (error) {
114
+ if (error instanceof DualCreateAbort) {
115
+ return error.outcome;
116
+ }
117
+ throw error;
118
+ }
119
+ if (runtime?.eventBus) {
120
+ const event = {
121
+ groupId: result.group.id,
122
+ primaryBookingId: result.primary.booking.id,
123
+ secondaryBookingId: result.secondary.booking.id,
124
+ productId: input.primary.productId,
125
+ createdByUserId: userId ?? null,
126
+ occurredAt: new Date(),
127
+ };
128
+ await runtime.eventBus.emit("booking.dual-created", event);
129
+ }
130
+ return { status: "ok", result };
131
+ }
@@ -0,0 +1,168 @@
1
+ import type { Booking, BookingGroupMember, BookingTraveler } from "@voyantjs/bookings/schema";
2
+ import type { EventBus } from "@voyantjs/core";
3
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
4
+ import { z } from "zod";
5
+ import type { BookingPaymentSchedule, Voucher, VoucherRedemption } from "./schema.js";
6
+ declare const travelerInputSchema: z.ZodObject<{
7
+ firstName: z.ZodString;
8
+ lastName: z.ZodString;
9
+ email: z.ZodNullable<z.ZodOptional<z.ZodString>>;
10
+ phone: z.ZodNullable<z.ZodOptional<z.ZodString>>;
11
+ personId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
12
+ participantType: z.ZodDefault<z.ZodEnum<{
13
+ other: "other";
14
+ traveler: "traveler";
15
+ occupant: "occupant";
16
+ }>>;
17
+ travelerCategory: z.ZodNullable<z.ZodOptional<z.ZodEnum<{
18
+ other: "other";
19
+ adult: "adult";
20
+ child: "child";
21
+ infant: "infant";
22
+ senior: "senior";
23
+ }>>>;
24
+ preferredLanguage: z.ZodNullable<z.ZodOptional<z.ZodString>>;
25
+ accessibilityNeeds: z.ZodNullable<z.ZodOptional<z.ZodString>>;
26
+ specialRequests: z.ZodNullable<z.ZodOptional<z.ZodString>>;
27
+ roomUnitId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
28
+ isPrimary: z.ZodNullable<z.ZodOptional<z.ZodBoolean>>;
29
+ notes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
30
+ }, z.core.$strip>;
31
+ export declare const quickCreateBookingSchema: z.ZodObject<{
32
+ productId: z.ZodString;
33
+ optionId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
34
+ slotId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
35
+ bookingNumber: z.ZodString;
36
+ personId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
37
+ organizationId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
38
+ internalNotes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
39
+ travelers: z.ZodOptional<z.ZodArray<z.ZodObject<{
40
+ firstName: z.ZodString;
41
+ lastName: z.ZodString;
42
+ email: z.ZodNullable<z.ZodOptional<z.ZodString>>;
43
+ phone: z.ZodNullable<z.ZodOptional<z.ZodString>>;
44
+ personId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
45
+ participantType: z.ZodDefault<z.ZodEnum<{
46
+ other: "other";
47
+ traveler: "traveler";
48
+ occupant: "occupant";
49
+ }>>;
50
+ travelerCategory: z.ZodNullable<z.ZodOptional<z.ZodEnum<{
51
+ other: "other";
52
+ adult: "adult";
53
+ child: "child";
54
+ infant: "infant";
55
+ senior: "senior";
56
+ }>>>;
57
+ preferredLanguage: z.ZodNullable<z.ZodOptional<z.ZodString>>;
58
+ accessibilityNeeds: z.ZodNullable<z.ZodOptional<z.ZodString>>;
59
+ specialRequests: z.ZodNullable<z.ZodOptional<z.ZodString>>;
60
+ roomUnitId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
61
+ isPrimary: z.ZodNullable<z.ZodOptional<z.ZodBoolean>>;
62
+ notes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
63
+ }, z.core.$strip>>>;
64
+ paymentSchedules: z.ZodOptional<z.ZodArray<z.ZodObject<{
65
+ scheduleType: z.ZodDefault<z.ZodEnum<{
66
+ other: "other";
67
+ deposit: "deposit";
68
+ installment: "installment";
69
+ balance: "balance";
70
+ hold: "hold";
71
+ }>>;
72
+ status: z.ZodDefault<z.ZodEnum<{
73
+ pending: "pending";
74
+ expired: "expired";
75
+ cancelled: "cancelled";
76
+ paid: "paid";
77
+ due: "due";
78
+ waived: "waived";
79
+ }>>;
80
+ dueDate: z.ZodString;
81
+ currency: z.ZodString;
82
+ amountCents: z.ZodNumber;
83
+ notes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
84
+ }, z.core.$strip>>>;
85
+ voucherRedemption: z.ZodOptional<z.ZodObject<{
86
+ voucherId: z.ZodString;
87
+ amountCents: z.ZodNumber;
88
+ }, z.core.$strip>>;
89
+ groupMembership: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
90
+ action: z.ZodLiteral<"join">;
91
+ groupId: z.ZodString;
92
+ role: z.ZodDefault<z.ZodEnum<{
93
+ primary: "primary";
94
+ shared: "shared";
95
+ }>>;
96
+ }, z.core.$strip>, z.ZodObject<{
97
+ action: z.ZodLiteral<"create">;
98
+ kind: z.ZodDefault<z.ZodEnum<{
99
+ other: "other";
100
+ shared_room: "shared_room";
101
+ }>>;
102
+ label: z.ZodNullable<z.ZodOptional<z.ZodString>>;
103
+ optionUnitId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
104
+ makeBookingPrimary: z.ZodDefault<z.ZodBoolean>;
105
+ }, z.core.$strip>], "action">>;
106
+ }, z.core.$strip>;
107
+ export type QuickCreateBookingInput = z.infer<typeof quickCreateBookingSchema>;
108
+ export type QuickCreateTravelerInput = z.infer<typeof travelerInputSchema>;
109
+ /**
110
+ * Fire-and-forget post-commit events. The orchestrator only knows about
111
+ * `booking.quick-created` — downstream confirm/cancel lifecycle events stay
112
+ * with the booking service itself (the booking lands in `draft` status so no
113
+ * `booking.confirmed` should fire here).
114
+ */
115
+ export interface BookingQuickCreateRuntime {
116
+ eventBus?: EventBus;
117
+ }
118
+ export interface BookingQuickCreatedEvent {
119
+ bookingId: string;
120
+ bookingNumber: string;
121
+ productId: string;
122
+ travelerCount: number;
123
+ paymentScheduleCount: number;
124
+ voucherRedeemedCents: number | null;
125
+ groupId: string | null;
126
+ createdByUserId: string | null;
127
+ occurredAt: Date;
128
+ }
129
+ export interface QuickCreateBookingResult {
130
+ booking: Booking;
131
+ travelers: BookingTraveler[];
132
+ paymentSchedules: BookingPaymentSchedule[];
133
+ voucherRedemption: {
134
+ voucher: Voucher;
135
+ redemption: VoucherRedemption;
136
+ } | null;
137
+ groupMembership: {
138
+ groupId: string;
139
+ member: BookingGroupMember;
140
+ } | null;
141
+ }
142
+ export type QuickCreateBookingOutcome = {
143
+ status: "ok";
144
+ result: QuickCreateBookingResult;
145
+ } | {
146
+ status: "product_not_found";
147
+ } | {
148
+ status: "voucher_not_found";
149
+ } | {
150
+ status: "voucher_inactive";
151
+ } | {
152
+ status: "voucher_not_started";
153
+ } | {
154
+ status: "voucher_expired";
155
+ } | {
156
+ status: "voucher_insufficient_balance";
157
+ } | {
158
+ status: "group_not_found";
159
+ } | {
160
+ status: "booking_already_in_group";
161
+ currentGroupId: string;
162
+ };
163
+ export declare function quickCreateBooking(db: PostgresJsDatabase, rawInput: QuickCreateBookingInput, options?: {
164
+ userId?: string;
165
+ runtime?: BookingQuickCreateRuntime;
166
+ }): Promise<QuickCreateBookingOutcome>;
167
+ export {};
168
+ //# sourceMappingURL=service-bookings-quick-create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-bookings-quick-create.d.ts","sourceRoot":"","sources":["../src/service-bookings-quick-create.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAE7F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAE9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,KAAK,EAAE,sBAAsB,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAOrF,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;iBAqBvB,CAAA;AAyCF,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAenC,CAAA;AAEF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAA;AAC9E,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAI1E;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,UAAU,EAAE,IAAI,CAAA;CACjB;AAID,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,eAAe,EAAE,CAAA;IAC5B,gBAAgB,EAAE,sBAAsB,EAAE,CAAA;IAC1C,iBAAiB,EAAE;QACjB,OAAO,EAAE,OAAO,CAAA;QAChB,UAAU,EAAE,iBAAiB,CAAA;KAC9B,GAAG,IAAI,CAAA;IACR,eAAe,EAAE;QACf,OAAO,EAAE,MAAM,CAAA;QACf,MAAM,EAAE,kBAAkB,CAAA;KAC3B,GAAG,IAAI,CAAA;CACT;AAED,MAAM,MAAM,yBAAyB,GACjC;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,wBAAwB,CAAA;CAAE,GAClD;IAAE,MAAM,EAAE,mBAAmB,CAAA;CAAE,GAC/B;IAAE,MAAM,EAAE,mBAAmB,CAAA;CAAE,GAC/B;IAAE,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAC9B;IAAE,MAAM,EAAE,qBAAqB,CAAA;CAAE,GACjC;IAAE,MAAM,EAAE,iBAAiB,CAAA;CAAE,GAC7B;IAAE,MAAM,EAAE,8BAA8B,CAAA;CAAE,GAC1C;IAAE,MAAM,EAAE,iBAAiB,CAAA;CAAE,GAC7B;IAAE,MAAM,EAAE,0BAA0B,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAA;AAiClE,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,kBAAkB,EACtB,QAAQ,EAAE,uBAAuB,EACjC,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,yBAAyB,CAAA;CAC/B,GACL,OAAO,CAAC,yBAAyB,CAAC,CAgNpC"}