@voyantjs/bookings 0.8.0 → 0.10.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.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/markets-ref.d.ts +151 -0
- package/dist/markets-ref.d.ts.map +1 -0
- package/dist/markets-ref.js +19 -0
- package/dist/pii-redaction.d.ts +89 -0
- package/dist/pii-redaction.d.ts.map +1 -0
- package/dist/pii-redaction.js +120 -0
- package/dist/pii.d.ts +1 -0
- package/dist/pii.d.ts.map +1 -1
- package/dist/pii.js +20 -1
- package/dist/routes-groups.d.ts +3 -2
- package/dist/routes-groups.d.ts.map +1 -1
- package/dist/routes-public.d.ts +11 -13
- package/dist/routes-public.d.ts.map +1 -1
- package/dist/routes-public.js +3 -3
- package/dist/routes.d.ts +16 -8
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +57 -9
- package/dist/schema/travel-details.d.ts +37 -0
- package/dist/schema/travel-details.d.ts.map +1 -1
- package/dist/schema/travel-details.js +6 -0
- package/dist/schema-core.d.ts +17 -17
- package/dist/schema-core.d.ts.map +1 -1
- package/dist/schema-core.js +8 -2
- package/dist/schema-items.d.ts.map +1 -1
- package/dist/schema-items.js +6 -1
- package/dist/service-public.d.ts +0 -6
- package/dist/service-public.d.ts.map +1 -1
- package/dist/service-public.js +0 -4
- package/dist/service.d.ts +55 -46
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +288 -89
- package/dist/state-machine.d.ts +29 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +39 -0
- package/dist/validation-public.d.ts +0 -6
- package/dist/validation-public.d.ts.map +1 -1
- package/dist/validation-public.js +0 -2
- package/dist/validation.d.ts +0 -4
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +0 -2
- package/dist/workflows/refund-booking.d.ts +87 -0
- package/dist/workflows/refund-booking.d.ts.map +1 -0
- package/dist/workflows/refund-booking.js +210 -0
- package/package.json +7 -6
package/dist/validation.d.ts
CHANGED
|
@@ -366,7 +366,6 @@ export declare const insertTravelerSchema: z.ZodObject<{
|
|
|
366
366
|
email: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
367
367
|
phone: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
368
368
|
preferredLanguage: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
369
|
-
accessibilityNeeds: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
370
369
|
specialRequests: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
371
370
|
travelerCategory: z.ZodNullable<z.ZodOptional<z.ZodEnum<{
|
|
372
371
|
other: "other";
|
|
@@ -384,7 +383,6 @@ export declare const updateTravelerSchema: z.ZodObject<{
|
|
|
384
383
|
email: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
385
384
|
phone: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
386
385
|
preferredLanguage: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
387
|
-
accessibilityNeeds: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
388
386
|
specialRequests: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
389
387
|
travelerCategory: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodEnum<{
|
|
390
388
|
other: "other";
|
|
@@ -415,7 +413,6 @@ export declare const insertTravelerRecordSchema: z.ZodObject<{
|
|
|
415
413
|
email: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
416
414
|
phone: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
417
415
|
preferredLanguage: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
418
|
-
accessibilityNeeds: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
419
416
|
specialRequests: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
420
417
|
isPrimary: z.ZodDefault<z.ZodBoolean>;
|
|
421
418
|
notes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
@@ -439,7 +436,6 @@ export declare const updateTravelerRecordSchema: z.ZodObject<{
|
|
|
439
436
|
email: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
440
437
|
phone: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
441
438
|
preferredLanguage: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
442
|
-
accessibilityNeeds: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
443
439
|
specialRequests: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
444
440
|
isPrimary: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
445
441
|
notes: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
package/dist/validation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AA2DvB,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAoB,CAAA;AACpD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA8B,CAAA;AAE9D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAW5B,CAAA;AAEJ,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;iBASjC,CAAA;AAEF,eAAO,MAAM,4BAA4B;;;iBAGvC,CAAA;AAEF,eAAO,MAAM,oBAAoB;;;;;;;;iBAQ/B,CAAA;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;iBAI/B,CAAA;AAEF,eAAO,MAAM,yBAAyB;;;;;;;;;;;iBAGpC,CAAA;AAEF,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqBnC,CAAA;AAEF,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmB7B,CAAA;AAEJ,eAAO,MAAM,uBAAuB;;;iBAYhC,CAAA;AAEJ,eAAO,MAAM,oBAAoB;;iBAE/B,CAAA;AAEF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AAEF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AAEF,eAAO,MAAM,yBAAyB;;;iBAGpC,CAAA;AAEF,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2B5C,CAAA;
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
|
package/dist/validation.js
CHANGED
|
@@ -194,7 +194,6 @@ const travelerRecordCoreSchema = z.object({
|
|
|
194
194
|
email: z.string().email().optional().nullable(),
|
|
195
195
|
phone: z.string().max(50).optional().nullable(),
|
|
196
196
|
preferredLanguage: z.string().max(35).optional().nullable(),
|
|
197
|
-
accessibilityNeeds: z.string().optional().nullable(),
|
|
198
197
|
specialRequests: z.string().optional().nullable(),
|
|
199
198
|
isPrimary: z.boolean().default(false),
|
|
200
199
|
notes: z.string().optional().nullable(),
|
|
@@ -206,7 +205,6 @@ const travelerCoreSchema = z.object({
|
|
|
206
205
|
email: z.string().email().optional().nullable(),
|
|
207
206
|
phone: z.string().max(50).optional().nullable(),
|
|
208
207
|
preferredLanguage: z.string().max(35).optional().nullable(),
|
|
209
|
-
accessibilityNeeds: z.string().optional().nullable(),
|
|
210
208
|
specialRequests: z.string().optional().nullable(),
|
|
211
209
|
travelerCategory: bookingTravelerCategorySchema.optional().nullable(),
|
|
212
210
|
isPrimary: z.boolean().optional().nullable(),
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type EventBus } from "@voyantjs/core";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
/**
|
|
4
|
+
* Input passed when starting a refund.
|
|
5
|
+
*/
|
|
6
|
+
export interface RefundBookingInput {
|
|
7
|
+
bookingId: string;
|
|
8
|
+
/** Free-form audit reason. Required for ops + customer comms. */
|
|
9
|
+
reason: string;
|
|
10
|
+
/**
|
|
11
|
+
* Refund amount in cents. Pass `null` to refund the booking's full
|
|
12
|
+
* `sellAmountCents`; pass a smaller value for a partial refund.
|
|
13
|
+
*/
|
|
14
|
+
amountCents?: number | null;
|
|
15
|
+
/** User triggering the refund (for audit). */
|
|
16
|
+
userId?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Side-effect dependencies — supplied by the caller. Decouples the saga
|
|
20
|
+
* from finance + transactions + notifications packages so this can ship
|
|
21
|
+
* without those modules being imported.
|
|
22
|
+
*
|
|
23
|
+
* - `createCreditNote` is expected to be transactional internally and to
|
|
24
|
+
* return a credit note id. Pass a no-op when there's no payment to refund.
|
|
25
|
+
* - `voidCreditNote` is the compensation; it should mark the credit note
|
|
26
|
+
* void (or delete it) so a retry of the saga doesn't double-credit.
|
|
27
|
+
* - `reverseSupplierOffer` updates linked supplier offer/order rows. May
|
|
28
|
+
* be a no-op if no transaction link exists.
|
|
29
|
+
* - `notifyCustomer` is fire-and-forget; failures don't trigger
|
|
30
|
+
* compensation (notifications fail closed elsewhere).
|
|
31
|
+
*/
|
|
32
|
+
export interface RefundBookingDeps {
|
|
33
|
+
db: PostgresJsDatabase;
|
|
34
|
+
eventBus?: EventBus;
|
|
35
|
+
createCreditNote: (args: {
|
|
36
|
+
bookingId: string;
|
|
37
|
+
amountCents: number;
|
|
38
|
+
reason: string;
|
|
39
|
+
}) => Promise<{
|
|
40
|
+
creditNoteId: string;
|
|
41
|
+
} | null>;
|
|
42
|
+
voidCreditNote?: (args: {
|
|
43
|
+
creditNoteId: string;
|
|
44
|
+
reason: string;
|
|
45
|
+
}) => Promise<void>;
|
|
46
|
+
reverseSupplierOffer?: (args: {
|
|
47
|
+
bookingId: string;
|
|
48
|
+
reason: string;
|
|
49
|
+
}) => Promise<void>;
|
|
50
|
+
notifyCustomer?: (args: {
|
|
51
|
+
bookingId: string;
|
|
52
|
+
reason: string;
|
|
53
|
+
amountCents: number;
|
|
54
|
+
}) => Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build the refund saga for a booking.
|
|
58
|
+
*
|
|
59
|
+
* **Steps**
|
|
60
|
+
*
|
|
61
|
+
* 1. `validate-state` — load the booking, ensure it's in a refundable
|
|
62
|
+
* status (`confirmed`, `in_progress`, or `on_hold`), compute the refund
|
|
63
|
+
* amount. Compensation: none — this step doesn't mutate.
|
|
64
|
+
* 2. `create-credit-note` — call into finance via the injected dep.
|
|
65
|
+
* Compensation: void the credit note.
|
|
66
|
+
* 3. `release-inventory` — release any held allocations + the slot
|
|
67
|
+
* capacity (only when the booking hasn't started yet). Compensation:
|
|
68
|
+
* re-acquire (best-effort; if inventory has since been re-sold,
|
|
69
|
+
* that's a known limitation logged for ops triage).
|
|
70
|
+
* 4. `reverse-supplier-offer` — best-effort call into transactions.
|
|
71
|
+
* Compensation: none (idempotent re-call expected if needed).
|
|
72
|
+
* 5. `transition-booking` — flip the booking to `cancelled`. Last
|
|
73
|
+
* DB-touching step so a failure here doesn't leave the booking
|
|
74
|
+
* cancelled with no credit-note rollback.
|
|
75
|
+
* 6. `emit` — fire `booking.refunded` after-commit. Best-effort; no
|
|
76
|
+
* compensation.
|
|
77
|
+
* 7. `notify` — async-style notification. Best-effort; no compensation.
|
|
78
|
+
*
|
|
79
|
+
* The saga always runs in a single host process — durability for async
|
|
80
|
+
* notifications is the deployment's responsibility (see `JobRunner`).
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildRefundBookingWorkflow(deps: RefundBookingDeps): import("@voyantjs/core").WorkflowDefinition;
|
|
83
|
+
/**
|
|
84
|
+
* Convenience wrapper: build + run the saga in one call.
|
|
85
|
+
*/
|
|
86
|
+
export declare function refundBooking(input: RefundBookingInput, deps: RefundBookingDeps): Promise<import("@voyantjs/core").WorkflowResult>;
|
|
87
|
+
//# sourceMappingURL=refund-booking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refund-booking.d.ts","sourceRoot":"","sources":["../../src/workflows/refund-booking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,QAAQ,EAAQ,MAAM,gBAAgB,CAAA;AAEpE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAMjE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,kBAAkB,CAAA;IACtB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,gBAAgB,EAAE,CAAC,IAAI,EAAE;QACvB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,MAAM,EAAE,MAAM,CAAA;KACf,KAAK,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAC9C,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAClF,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACrF,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE;QACtB,SAAS,EAAE,MAAM,CAAA;QACjB,MAAM,EAAE,MAAM,CAAA;QACd,WAAW,EAAE,MAAM,CAAA;KACpB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACpB;AAuBD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,iBAAiB,+CAgMjE;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,iBAAiB,oDAGrF"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createWorkflow, step } from "@voyantjs/core";
|
|
2
|
+
import { eq, sql } from "drizzle-orm";
|
|
3
|
+
import { availabilitySlotsRef } from "../availability-ref.js";
|
|
4
|
+
import { bookingActivityLog, bookingAllocations, bookingItems, bookings } from "../schema.js";
|
|
5
|
+
import { transitionBooking } from "../state-machine.js";
|
|
6
|
+
/**
|
|
7
|
+
* Build the refund saga for a booking.
|
|
8
|
+
*
|
|
9
|
+
* **Steps**
|
|
10
|
+
*
|
|
11
|
+
* 1. `validate-state` — load the booking, ensure it's in a refundable
|
|
12
|
+
* status (`confirmed`, `in_progress`, or `on_hold`), compute the refund
|
|
13
|
+
* amount. Compensation: none — this step doesn't mutate.
|
|
14
|
+
* 2. `create-credit-note` — call into finance via the injected dep.
|
|
15
|
+
* Compensation: void the credit note.
|
|
16
|
+
* 3. `release-inventory` — release any held allocations + the slot
|
|
17
|
+
* capacity (only when the booking hasn't started yet). Compensation:
|
|
18
|
+
* re-acquire (best-effort; if inventory has since been re-sold,
|
|
19
|
+
* that's a known limitation logged for ops triage).
|
|
20
|
+
* 4. `reverse-supplier-offer` — best-effort call into transactions.
|
|
21
|
+
* Compensation: none (idempotent re-call expected if needed).
|
|
22
|
+
* 5. `transition-booking` — flip the booking to `cancelled`. Last
|
|
23
|
+
* DB-touching step so a failure here doesn't leave the booking
|
|
24
|
+
* cancelled with no credit-note rollback.
|
|
25
|
+
* 6. `emit` — fire `booking.refunded` after-commit. Best-effort; no
|
|
26
|
+
* compensation.
|
|
27
|
+
* 7. `notify` — async-style notification. Best-effort; no compensation.
|
|
28
|
+
*
|
|
29
|
+
* The saga always runs in a single host process — durability for async
|
|
30
|
+
* notifications is the deployment's responsibility (see `JobRunner`).
|
|
31
|
+
*/
|
|
32
|
+
export function buildRefundBookingWorkflow(deps) {
|
|
33
|
+
return createWorkflow("refund-booking", [
|
|
34
|
+
step("validate-state").run(async (input) => {
|
|
35
|
+
const [row] = await deps.db
|
|
36
|
+
.select({
|
|
37
|
+
id: bookings.id,
|
|
38
|
+
bookingNumber: bookings.bookingNumber,
|
|
39
|
+
status: bookings.status,
|
|
40
|
+
sellAmountCents: bookings.sellAmountCents,
|
|
41
|
+
})
|
|
42
|
+
.from(bookings)
|
|
43
|
+
.where(eq(bookings.id, input.bookingId))
|
|
44
|
+
.limit(1);
|
|
45
|
+
if (!row) {
|
|
46
|
+
throw new Error(`refund-booking: booking ${input.bookingId} not found`);
|
|
47
|
+
}
|
|
48
|
+
if (row.status !== "confirmed" && row.status !== "in_progress" && row.status !== "on_hold") {
|
|
49
|
+
throw new Error(`refund-booking: booking ${input.bookingId} is in ${row.status}, not refundable`);
|
|
50
|
+
}
|
|
51
|
+
const fullRefundAmount = row.sellAmountCents ?? 0;
|
|
52
|
+
const requested = input.amountCents ?? fullRefundAmount;
|
|
53
|
+
if (requested < 0 || requested > fullRefundAmount) {
|
|
54
|
+
throw new Error(`refund-booking: requested amount ${requested} out of range [0, ${fullRefundAmount}]`);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
bookingId: row.id,
|
|
58
|
+
bookingNumber: row.bookingNumber,
|
|
59
|
+
previousStatus: row.status,
|
|
60
|
+
fullRefundAmount,
|
|
61
|
+
refundAmount: requested,
|
|
62
|
+
};
|
|
63
|
+
}),
|
|
64
|
+
step("create-credit-note")
|
|
65
|
+
.run(async (input, ctx) => {
|
|
66
|
+
const validate = ctx.results["validate-state"];
|
|
67
|
+
if (validate.refundAmount === 0) {
|
|
68
|
+
// Draft / unpaid booking — no payment to refund. Short-circuit.
|
|
69
|
+
return { creditNoteId: null, amountCents: 0 };
|
|
70
|
+
}
|
|
71
|
+
const created = await deps.createCreditNote({
|
|
72
|
+
bookingId: input.bookingId,
|
|
73
|
+
amountCents: validate.refundAmount,
|
|
74
|
+
reason: input.reason,
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
creditNoteId: created?.creditNoteId ?? null,
|
|
78
|
+
amountCents: validate.refundAmount,
|
|
79
|
+
};
|
|
80
|
+
})
|
|
81
|
+
.compensate(async (output) => {
|
|
82
|
+
if (output.creditNoteId && deps.voidCreditNote) {
|
|
83
|
+
await deps.voidCreditNote({
|
|
84
|
+
creditNoteId: output.creditNoteId,
|
|
85
|
+
reason: "refund-saga rolled back",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
step("release-inventory")
|
|
90
|
+
.run(async (input) => {
|
|
91
|
+
return await deps.db.transaction(async (tx) => {
|
|
92
|
+
const allocs = await tx
|
|
93
|
+
.select()
|
|
94
|
+
.from(bookingAllocations)
|
|
95
|
+
.where(eq(bookingAllocations.bookingId, input.bookingId));
|
|
96
|
+
const releaseable = allocs.filter((a) => a.status === "held" || a.status === "confirmed");
|
|
97
|
+
for (const allocation of releaseable) {
|
|
98
|
+
if (allocation.availabilitySlotId) {
|
|
99
|
+
// Best-effort capacity release — restore the held quantity.
|
|
100
|
+
await tx
|
|
101
|
+
.update(availabilitySlotsRef)
|
|
102
|
+
.set({
|
|
103
|
+
remainingPax: sql `${availabilitySlotsRef.remainingPax} + ${allocation.quantity}`,
|
|
104
|
+
})
|
|
105
|
+
.where(eq(availabilitySlotsRef.id, allocation.availabilitySlotId));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (releaseable.length > 0) {
|
|
109
|
+
const releaseableIds = releaseable.map((a) => a.id);
|
|
110
|
+
await tx
|
|
111
|
+
.update(bookingAllocations)
|
|
112
|
+
.set({ status: "released", releasedAt: new Date(), updatedAt: new Date() })
|
|
113
|
+
.where(sql `${bookingAllocations.id} IN (${sql.join(releaseableIds.map((id) => sql `${id}`), sql `, `)})`);
|
|
114
|
+
}
|
|
115
|
+
await tx
|
|
116
|
+
.update(bookingItems)
|
|
117
|
+
.set({ status: "cancelled", updatedAt: new Date() })
|
|
118
|
+
.where(eq(bookingItems.bookingId, input.bookingId));
|
|
119
|
+
return {
|
|
120
|
+
releasedAllocationIds: releaseable.map((a) => a.id),
|
|
121
|
+
slotIds: releaseable
|
|
122
|
+
.map((a) => a.availabilitySlotId)
|
|
123
|
+
.filter((id) => Boolean(id)),
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
})
|
|
127
|
+
.compensate(async (output) => {
|
|
128
|
+
// Re-decrement the slots we restored. Note: if the slot has since
|
|
129
|
+
// been re-sold this will fail loudly — that's intentional, an
|
|
130
|
+
// operator must intervene.
|
|
131
|
+
if (output.slotIds.length === 0)
|
|
132
|
+
return;
|
|
133
|
+
await deps.db.transaction(async (tx) => {
|
|
134
|
+
for (const slotId of output.slotIds) {
|
|
135
|
+
await tx
|
|
136
|
+
.update(availabilitySlotsRef)
|
|
137
|
+
.set({ remainingPax: sql `${availabilitySlotsRef.remainingPax} - 1` })
|
|
138
|
+
.where(eq(availabilitySlotsRef.id, slotId));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}),
|
|
142
|
+
step("reverse-supplier-offer").run(async (input) => {
|
|
143
|
+
if (!deps.reverseSupplierOffer)
|
|
144
|
+
return { reversed: false };
|
|
145
|
+
await deps.reverseSupplierOffer({ bookingId: input.bookingId, reason: input.reason });
|
|
146
|
+
return { reversed: true };
|
|
147
|
+
}),
|
|
148
|
+
step("transition-booking").run(async (input, ctx) => {
|
|
149
|
+
const validate = ctx.results["validate-state"];
|
|
150
|
+
const patch = transitionBooking(validate.previousStatus, "cancelled");
|
|
151
|
+
await deps.db.transaction(async (tx) => {
|
|
152
|
+
await tx
|
|
153
|
+
.update(bookings)
|
|
154
|
+
.set({ ...patch, updatedAt: new Date() })
|
|
155
|
+
.where(eq(bookings.id, input.bookingId));
|
|
156
|
+
await tx.insert(bookingActivityLog).values({
|
|
157
|
+
bookingId: input.bookingId,
|
|
158
|
+
actorId: input.userId ?? "system",
|
|
159
|
+
activityType: "status_change",
|
|
160
|
+
description: `Refunded from ${validate.previousStatus}: ${input.reason}`,
|
|
161
|
+
metadata: {
|
|
162
|
+
oldStatus: validate.previousStatus,
|
|
163
|
+
newStatus: "cancelled",
|
|
164
|
+
refundAmountCents: validate.refundAmount,
|
|
165
|
+
reason: input.reason,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
return { status: "cancelled" };
|
|
170
|
+
}),
|
|
171
|
+
step("emit").run(async (input, ctx) => {
|
|
172
|
+
const validate = ctx.results["validate-state"];
|
|
173
|
+
if (!deps.eventBus)
|
|
174
|
+
return { emitted: false };
|
|
175
|
+
await deps.eventBus.emit("booking.refunded", {
|
|
176
|
+
bookingId: input.bookingId,
|
|
177
|
+
bookingNumber: validate.bookingNumber,
|
|
178
|
+
previousStatus: validate.previousStatus,
|
|
179
|
+
refundAmountCents: validate.refundAmount,
|
|
180
|
+
reason: input.reason,
|
|
181
|
+
actorId: input.userId ?? null,
|
|
182
|
+
}, { category: "domain", source: "service" });
|
|
183
|
+
return { emitted: true };
|
|
184
|
+
}),
|
|
185
|
+
step("notify").run(async (input, ctx) => {
|
|
186
|
+
if (!deps.notifyCustomer)
|
|
187
|
+
return { notified: false };
|
|
188
|
+
const validate = ctx.results["validate-state"];
|
|
189
|
+
try {
|
|
190
|
+
await deps.notifyCustomer({
|
|
191
|
+
bookingId: input.bookingId,
|
|
192
|
+
reason: input.reason,
|
|
193
|
+
amountCents: validate.refundAmount,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Notifications are best-effort. Log via the deployment's logger.
|
|
198
|
+
return { notified: false };
|
|
199
|
+
}
|
|
200
|
+
return { notified: true };
|
|
201
|
+
}),
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Convenience wrapper: build + run the saga in one call.
|
|
206
|
+
*/
|
|
207
|
+
export async function refundBooking(input, deps) {
|
|
208
|
+
const wf = buildRefundBookingWorkflow(deps);
|
|
209
|
+
return wf.run({ input });
|
|
210
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyantjs/bookings",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"license": "FSL-1.1-Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -49,14 +49,15 @@
|
|
|
49
49
|
"drizzle-orm": "^0.45.2",
|
|
50
50
|
"hono": "^4.12.10",
|
|
51
51
|
"zod": "^4.3.6",
|
|
52
|
-
"@voyantjs/core": "0.
|
|
53
|
-
"@voyantjs/db": "0.
|
|
54
|
-
"@voyantjs/hono": "0.
|
|
55
|
-
"@voyantjs/utils": "0.
|
|
52
|
+
"@voyantjs/core": "0.10.0",
|
|
53
|
+
"@voyantjs/db": "0.10.0",
|
|
54
|
+
"@voyantjs/hono": "0.10.0",
|
|
55
|
+
"@voyantjs/utils": "0.10.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"typescript": "^6.0.2",
|
|
59
|
-
"@voyantjs/
|
|
59
|
+
"@voyantjs/markets": "0.10.0",
|
|
60
|
+
"@voyantjs/products": "0.10.0",
|
|
60
61
|
"@voyantjs/voyant-typescript-config": "0.1.0"
|
|
61
62
|
},
|
|
62
63
|
"files": [
|