@voyantjs/customer-portal 0.5.0 → 0.6.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.
@@ -1 +1 @@
1
- {"version":3,"file":"routes-public.d.ts","sourceRoot":"","sources":["../src/routes-public.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAYjE,KAAK,GAAG,GAAG;IACT,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AA0CD,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+CA8MnC,CAAA;AAEJ,MAAM,MAAM,0BAA0B,GAAG,OAAO,0BAA0B,CAAA"}
1
+ {"version":3,"file":"routes-public.d.ts","sourceRoot":"","sources":["../src/routes-public.ts"],"names":[],"mappings":"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"}
@@ -1,25 +1,9 @@
1
- import { createKmsProviderFromEnv } from "@voyantjs/utils";
1
+ import { ForbiddenApiError, handleApiError, parseJsonBody, requireUserId } from "@voyantjs/hono";
2
2
  import { Hono } from "hono";
3
+ import { buildPublicCustomerPortalRouteRuntime, CUSTOMER_PORTAL_ROUTE_RUNTIME_CONTAINER_KEY, } from "./route-runtime.js";
4
+ import { customerPortalRoutes } from "./routes.js";
3
5
  import { publicCustomerPortalService } from "./service-public.js";
4
6
  import { bootstrapCustomerPortalSchema, createCustomerPortalCompanionSchema, importCustomerPortalBookingParticipantsSchema, updateCustomerPortalCompanionSchema, updateCustomerPortalProfileSchema, } from "./validation-public.js";
5
- function getRuntimeEnv(c) {
6
- const processEnv = globalThis.process?.env ?? {};
7
- return {
8
- ...processEnv,
9
- ...(c.env ?? {}),
10
- };
11
- }
12
- function resolveOptionalKms(c) {
13
- try {
14
- return createKmsProviderFromEnv(getRuntimeEnv(c));
15
- }
16
- catch {
17
- return null;
18
- }
19
- }
20
- function unauthorized(c) {
21
- return c.json({ error: "Unauthorized" }, 401);
22
- }
23
7
  function notFound(c, error) {
24
8
  return c.json({ error }, 404);
25
9
  }
@@ -29,139 +13,114 @@ function hasErrorResult(value) {
29
13
  function hasBootstrapErrorResult(value) {
30
14
  return "error" in value;
31
15
  }
32
- export const publicCustomerPortalRoutes = new Hono()
33
- .get("/me", async (c) => {
34
- const userId = c.get("userId");
35
- if (!userId) {
36
- return unauthorized(c);
37
- }
38
- const profile = await publicCustomerPortalService.getProfileWithOptions(c.get("db"), userId, {
39
- kms: resolveOptionalKms(c),
40
- });
41
- return profile ? c.json({ data: profile }) : notFound(c, "Customer profile not found");
42
- })
43
- .patch("/me", async (c) => {
44
- const userId = c.get("userId");
45
- if (!userId) {
46
- return unauthorized(c);
47
- }
48
- const result = await publicCustomerPortalService.updateProfileWithOptions(c.get("db"), userId, updateCustomerPortalProfileSchema.parse(await c.req.json()), {
49
- kms: resolveOptionalKms(c),
50
- });
51
- if (hasErrorResult(result)) {
52
- if (result.error === "not_found") {
53
- return notFound(c, "Customer profile not found");
16
+ export function createPublicCustomerPortalRoutes(options = {}) {
17
+ const getRuntime = (c) => c.var.container?.resolve(CUSTOMER_PORTAL_ROUTE_RUNTIME_CONTAINER_KEY) ?? buildPublicCustomerPortalRouteRuntime(c.env, options);
18
+ const resolveOptionalKms = (c) => getRuntime(c).getOptionalKmsProvider();
19
+ const resolveDocumentDownloadUrl = (c, storageKey) => getRuntime(c).resolveDocumentDownloadUrl?.(storageKey) ?? null;
20
+ return new Hono()
21
+ .route("/", customerPortalRoutes)
22
+ .get("/me", async (c) => {
23
+ const userId = requireUserId(c);
24
+ const profile = await publicCustomerPortalService.getProfileWithOptions(c.get("db"), userId, {
25
+ kms: resolveOptionalKms(c),
26
+ });
27
+ return profile ? c.json({ data: profile }) : notFound(c, "Customer profile not found");
28
+ })
29
+ .patch("/me", async (c) => {
30
+ const userId = requireUserId(c);
31
+ const result = await publicCustomerPortalService.updateProfileWithOptions(c.get("db"), userId, await parseJsonBody(c, updateCustomerPortalProfileSchema), {
32
+ kms: resolveOptionalKms(c),
33
+ });
34
+ if (hasErrorResult(result)) {
35
+ if (result.error === "not_found") {
36
+ return notFound(c, "Customer profile not found");
37
+ }
38
+ return c.json({ error: "Customer record is not linked to this account" }, 409);
39
+ }
40
+ return c.json({ data: result.profile });
41
+ })
42
+ .post("/bootstrap", async (c) => {
43
+ const userId = requireUserId(c);
44
+ const result = await publicCustomerPortalService.bootstrap(c.get("db"), userId, await parseJsonBody(c, bootstrapCustomerPortalSchema));
45
+ if (hasBootstrapErrorResult(result)) {
46
+ if (result.error === "not_found") {
47
+ return notFound(c, "Customer profile not found");
48
+ }
49
+ if (result.error === "customer_record_not_found") {
50
+ return notFound(c, "Customer record not found");
51
+ }
52
+ return c.json({ error: "Customer record is already linked to another account" }, 409);
53
+ }
54
+ if (result.status === "customer_selection_required") {
55
+ return c.json({ data: result }, 409);
54
56
  }
55
- return c.json({ error: "Customer record is not linked to this account" }, 409);
56
- }
57
- return c.json({ data: result.profile });
58
- })
59
- .post("/bootstrap", async (c) => {
60
- const userId = c.get("userId");
61
- if (!userId) {
62
- return unauthorized(c);
63
- }
64
- const result = await publicCustomerPortalService.bootstrap(c.get("db"), userId, bootstrapCustomerPortalSchema.parse(await c.req.json()));
65
- if (hasBootstrapErrorResult(result)) {
66
- if (result.error === "not_found") {
67
- return notFound(c, "Customer profile not found");
57
+ return c.json({ data: result });
58
+ })
59
+ .get("/companions", async (c) => {
60
+ const userId = requireUserId(c);
61
+ return c.json({ data: await publicCustomerPortalService.listCompanions(c.get("db"), userId) });
62
+ })
63
+ .post("/companions", async (c) => {
64
+ const userId = requireUserId(c);
65
+ const companion = await publicCustomerPortalService.createCompanion(c.get("db"), userId, await parseJsonBody(c, createCustomerPortalCompanionSchema));
66
+ if (!companion) {
67
+ return c.json({ error: "Customer record is not linked to this account" }, 409);
68
68
  }
69
- if (result.error === "customer_record_not_found") {
70
- return notFound(c, "Customer record not found");
69
+ return c.json({ data: companion }, 201);
70
+ })
71
+ .post("/companions/import-booking-participants", async (c) => {
72
+ const userId = requireUserId(c);
73
+ const result = await publicCustomerPortalService.importBookingParticipantsAsCompanions(c.get("db"), userId, await parseJsonBody(c, importCustomerPortalBookingParticipantsSchema));
74
+ if (!result) {
75
+ return c.json({ error: "Customer record is not linked to this account" }, 409);
71
76
  }
72
- return c.json({ error: "Customer record is already linked to another account" }, 409);
73
- }
74
- if (result.status === "customer_selection_required") {
75
- return c.json({ data: result }, 409);
76
- }
77
- return c.json({ data: result });
78
- })
79
- .get("/companions", async (c) => {
80
- const userId = c.get("userId");
81
- if (!userId) {
82
- return unauthorized(c);
83
- }
84
- return c.json({ data: await publicCustomerPortalService.listCompanions(c.get("db"), userId) });
85
- })
86
- .post("/companions", async (c) => {
87
- const userId = c.get("userId");
88
- if (!userId) {
89
- return unauthorized(c);
90
- }
91
- const companion = await publicCustomerPortalService.createCompanion(c.get("db"), userId, createCustomerPortalCompanionSchema.parse(await c.req.json()));
92
- if (!companion) {
93
- return c.json({ error: "Customer record is not linked to this account" }, 409);
94
- }
95
- return c.json({ data: companion }, 201);
96
- })
97
- .post("/companions/import-booking-participants", async (c) => {
98
- const userId = c.get("userId");
99
- if (!userId) {
100
- return unauthorized(c);
101
- }
102
- const result = await publicCustomerPortalService.importBookingParticipantsAsCompanions(c.get("db"), userId, importCustomerPortalBookingParticipantsSchema.parse(await c.req.json()));
103
- if (!result) {
104
- return c.json({ error: "Customer record is not linked to this account" }, 409);
105
- }
106
- return c.json({ data: result });
107
- })
108
- .patch("/companions/:companionId", async (c) => {
109
- const userId = c.get("userId");
110
- if (!userId) {
111
- return unauthorized(c);
112
- }
113
- const companion = await publicCustomerPortalService.updateCompanion(c.get("db"), userId, c.req.param("companionId"), updateCustomerPortalCompanionSchema.parse(await c.req.json()));
114
- if (companion === "forbidden") {
115
- return c.json({ error: "Companion does not belong to this customer" }, 403);
116
- }
117
- if (!companion) {
118
- return notFound(c, "Companion not found");
119
- }
120
- return c.json({ data: companion });
121
- })
122
- .delete("/companions/:companionId", async (c) => {
123
- const userId = c.get("userId");
124
- if (!userId) {
125
- return unauthorized(c);
126
- }
127
- const result = await publicCustomerPortalService.deleteCompanion(c.get("db"), userId, c.req.param("companionId"));
128
- if (result === "forbidden") {
129
- return c.json({ error: "Companion does not belong to this customer" }, 403);
130
- }
131
- if (result === "not_found") {
132
- return notFound(c, "Companion not found");
133
- }
134
- return c.json({ success: true });
135
- })
136
- .get("/bookings", async (c) => {
137
- const userId = c.get("userId");
138
- if (!userId) {
139
- return unauthorized(c);
140
- }
141
- const bookings = await publicCustomerPortalService.listBookings(c.get("db"), userId);
142
- return bookings ? c.json({ data: bookings }) : notFound(c, "Customer profile not found");
143
- })
144
- .get("/bookings/:bookingId", async (c) => {
145
- const userId = c.get("userId");
146
- if (!userId) {
147
- return unauthorized(c);
148
- }
149
- const booking = await publicCustomerPortalService.getBooking(c.get("db"), userId, c.req.param("bookingId"));
150
- return booking ? c.json({ data: booking }) : notFound(c, "Booking not found");
151
- })
152
- .get("/bookings/:bookingId/documents", async (c) => {
153
- const userId = c.get("userId");
154
- if (!userId) {
155
- return unauthorized(c);
156
- }
157
- const documents = await publicCustomerPortalService.listBookingDocuments(c.get("db"), userId, c.req.param("bookingId"));
158
- return documents ? c.json({ data: documents }) : notFound(c, "Booking not found");
159
- })
160
- .get("/bookings/:bookingId/billing-contact", async (c) => {
161
- const userId = c.get("userId");
162
- if (!userId) {
163
- return unauthorized(c);
164
- }
165
- const billingContact = await publicCustomerPortalService.getBookingBillingContact(c.get("db"), userId, c.req.param("bookingId"));
166
- return billingContact ? c.json({ data: billingContact }) : notFound(c, "Booking not found");
167
- });
77
+ return c.json({ data: result });
78
+ })
79
+ .patch("/companions/:companionId", async (c) => {
80
+ const userId = requireUserId(c);
81
+ const companion = await publicCustomerPortalService.updateCompanion(c.get("db"), userId, c.req.param("companionId"), await parseJsonBody(c, updateCustomerPortalCompanionSchema));
82
+ if (companion === "forbidden") {
83
+ return handleApiError(new ForbiddenApiError("Companion does not belong to this customer"), c);
84
+ }
85
+ if (!companion) {
86
+ return notFound(c, "Companion not found");
87
+ }
88
+ return c.json({ data: companion });
89
+ })
90
+ .delete("/companions/:companionId", async (c) => {
91
+ const userId = requireUserId(c);
92
+ const result = await publicCustomerPortalService.deleteCompanion(c.get("db"), userId, c.req.param("companionId"));
93
+ if (result === "forbidden") {
94
+ return handleApiError(new ForbiddenApiError("Companion does not belong to this customer"), c);
95
+ }
96
+ if (result === "not_found") {
97
+ return notFound(c, "Companion not found");
98
+ }
99
+ return c.json({ success: true });
100
+ })
101
+ .get("/bookings", async (c) => {
102
+ const userId = requireUserId(c);
103
+ const bookings = await publicCustomerPortalService.listBookings(c.get("db"), userId);
104
+ return bookings ? c.json({ data: bookings }) : notFound(c, "Customer profile not found");
105
+ })
106
+ .get("/bookings/:bookingId", async (c) => {
107
+ const userId = requireUserId(c);
108
+ const booking = await publicCustomerPortalService.getBooking(c.get("db"), userId, c.req.param("bookingId"), {
109
+ resolveDocumentDownloadUrl: (storageKey) => resolveDocumentDownloadUrl(c, storageKey),
110
+ });
111
+ return booking ? c.json({ data: booking }) : notFound(c, "Booking not found");
112
+ })
113
+ .get("/bookings/:bookingId/documents", async (c) => {
114
+ const userId = requireUserId(c);
115
+ const documents = await publicCustomerPortalService.listBookingDocuments(c.get("db"), userId, c.req.param("bookingId"), {
116
+ resolveDocumentDownloadUrl: (storageKey) => resolveDocumentDownloadUrl(c, storageKey),
117
+ });
118
+ return documents ? c.json({ data: documents }) : notFound(c, "Booking not found");
119
+ })
120
+ .get("/bookings/:bookingId/billing-contact", async (c) => {
121
+ const userId = requireUserId(c);
122
+ const billingContact = await publicCustomerPortalService.getBookingBillingContact(c.get("db"), userId, c.req.param("bookingId"));
123
+ return billingContact ? c.json({ data: billingContact }) : notFound(c, "Booking not found");
124
+ });
125
+ }
126
+ export const publicCustomerPortalRoutes = createPublicCustomerPortalRoutes();
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AASjE,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;KACvB,CAAA;CACF,CAAA;AAED,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAmB7B,CAAA;AAEJ,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAA"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AASjE,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;KACvB,CAAA;CACF,CAAA;AAED,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAe7B,CAAA;AAEJ,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAA"}
package/dist/routes.js CHANGED
@@ -1,15 +1,16 @@
1
+ import { parseQuery } from "@voyantjs/hono";
1
2
  import { Hono } from "hono";
2
3
  import { publicCustomerPortalService } from "./service-public.js";
3
4
  import { customerPortalContactExistsQuerySchema, customerPortalPhoneContactExistsQuerySchema, } from "./validation-public.js";
4
5
  export const customerPortalRoutes = new Hono()
5
6
  .get("/contact-exists", async (c) => {
6
- const query = customerPortalContactExistsQuerySchema.parse(Object.fromEntries(new URL(c.req.url).searchParams));
7
+ const query = parseQuery(c, customerPortalContactExistsQuerySchema);
7
8
  return c.json({
8
9
  data: await publicCustomerPortalService.contactExists(c.get("db"), query.email),
9
10
  });
10
11
  })
11
12
  .get("/contact-exists/phone", async (c) => {
12
- const query = customerPortalPhoneContactExistsQuerySchema.parse(Object.fromEntries(new URL(c.req.url).searchParams));
13
+ const query = parseQuery(c, customerPortalPhoneContactExistsQuerySchema);
13
14
  return c.json({
14
15
  data: await publicCustomerPortalService.phoneContactExists(c.get("db"), query.phone),
15
16
  });
@@ -3,6 +3,7 @@ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
3
  import type { BootstrapCustomerPortalInput, BootstrapCustomerPortalResult, CreateCustomerPortalCompanionInput, CustomerPortalBookingDetail, CustomerPortalBookingSummary, CustomerPortalCompanion, CustomerPortalContactExistsResult, CustomerPortalPhoneContactExistsResult, CustomerPortalProfile, ImportCustomerPortalBookingParticipantsInput, ImportCustomerPortalBookingParticipantsResult, UpdateCustomerPortalCompanionInput, UpdateCustomerPortalProfileInput } from "./validation-public.js";
4
4
  interface CustomerPortalServiceOptions {
5
5
  kms?: KmsProvider | null;
6
+ resolveDocumentDownloadUrl?: (storageKey: string) => Promise<string | null> | string | null;
6
7
  }
7
8
  export declare const publicCustomerPortalService: {
8
9
  contactExists(db: PostgresJsDatabase, email: string): Promise<CustomerPortalContactExistsResult>;
@@ -28,12 +29,12 @@ export declare const publicCustomerPortalService: {
28
29
  updateCompanion(db: PostgresJsDatabase, userId: string, companionId: string, input: UpdateCustomerPortalCompanionInput): Promise<CustomerPortalCompanion | null | "forbidden">;
29
30
  deleteCompanion(db: PostgresJsDatabase, userId: string, companionId: string): Promise<"deleted" | "not_found" | "forbidden">;
30
31
  listBookings(db: PostgresJsDatabase, userId: string): Promise<CustomerPortalBookingSummary[] | null>;
31
- getBooking(db: PostgresJsDatabase, userId: string, bookingId: string): Promise<CustomerPortalBookingDetail | null>;
32
- listBookingDocuments(db: PostgresJsDatabase, userId: string, bookingId: string): Promise<{
32
+ getBooking(db: PostgresJsDatabase, userId: string, bookingId: string, options?: CustomerPortalServiceOptions): Promise<CustomerPortalBookingDetail | null>;
33
+ listBookingDocuments(db: PostgresJsDatabase, userId: string, bookingId: string, options?: CustomerPortalServiceOptions): Promise<{
33
34
  id: string;
34
35
  source: "legal" | "finance" | "booking_document";
35
36
  participantId: string | null;
36
- type: "visa" | "insurance" | "health" | "passport_copy" | "other" | "invoice" | "proforma" | "credit_note" | "contract";
37
+ type: "visa" | "other" | "insurance" | "health" | "passport_copy" | "invoice" | "proforma" | "credit_note" | "contract";
37
38
  fileName: string;
38
39
  fileUrl: string;
39
40
  mimeType: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"service-public.d.ts","sourceRoot":"","sources":["../src/service-public.ts"],"names":[],"mappings":"AAoBA,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,iBAAiB,CAAA;AAExB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,KAAK,EACV,4BAA4B,EAC5B,6BAA6B,EAC7B,kCAAkC,EAGlC,2BAA2B,EAK3B,4BAA4B,EAE5B,uBAAuB,EACvB,iCAAiC,EACjC,sCAAsC,EACtC,qBAAqB,EACrB,4CAA4C,EAC5C,6CAA6C,EAE7C,kCAAkC,EAClC,gCAAgC,EACjC,MAAM,wBAAwB,CAAA;AAQ/B,UAAU,4BAA4B;IACpC,GAAG,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;CACzB;AA8vCD,eAAO,MAAM,2BAA2B;sBAEhC,kBAAkB,SACf,MAAM,GACZ,OAAO,CAAC,iCAAiC,CAAC;2BAuBvC,kBAAkB,SACf,MAAM,GACZ,OAAO,CAAC,sCAAsC,CAAC;mBAa7B,kBAAkB,UAAU,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;8BAKzF,kBAAkB,UACd,MAAM,YACJ,4BAA4B,GACrC,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;sBA+ClC,kBAAkB,UACd,MAAM,SACP,gCAAgC,GACtC,OAAO,CACR;QAAE,OAAO,EAAE,qBAAqB,CAAA;KAAE,GAAG;QAAE,KAAK,EAAE,WAAW,GAAG,0BAA0B,CAAA;KAAE,CACzF;iCAKK,kBAAkB,UACd,MAAM,SACP,gCAAgC,YAC7B,4BAA4B,GACrC,OAAO,CACR;QAAE,OAAO,EAAE,qBAAqB,CAAA;KAAE,GAAG;QAAE,KAAK,EAAE,WAAW,GAAG,0BAA0B,CAAA;KAAE,CACzF;kBAgNK,kBAAkB,UACd,MAAM,SACP,4BAA4B,GAClC,OAAO,CACN,6BAA6B,GAC7B;QAAE,KAAK,EAAE,WAAW,GAAG,2BAA2B,GAAG,yBAAyB,CAAA;KAAE,CACnF;uBA4MwB,kBAAkB,UAAU,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC;8CAiB1F,kBAAkB,UACd,MAAM,SACP,4CAA4C,GAClD,OAAO,CAAC,6CAA6C,GAAG,IAAI,CAAC;wBA4G1D,kBAAkB,UACd,MAAM,SACP,kCAAkC,GACxC,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC;wBA2BpC,kBAAkB,UACd,MAAM,eACD,MAAM,SACZ,kCAAkC,GACxC,OAAO,CAAC,uBAAuB,GAAG,IAAI,GAAG,WAAW,CAAC;wBA2ClD,kBAAkB,UACd,MAAM,eACD,MAAM,GAClB,OAAO,CAAC,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;qBAyB3C,kBAAkB,UACd,MAAM,GACb,OAAO,CAAC,4BAA4B,EAAE,GAAG,IAAI,CAAC;mBAmG3C,kBAAkB,UACd,MAAM,aACH,MAAM,GAChB,OAAO,CAAC,2BAA2B,GAAG,IAAI,CAAC;6BA0Bf,kBAAkB,UAAU,MAAM,aAAa,MAAM;;;;;;;;;;iCAKjD,kBAAkB,UAAU,MAAM,aAAa,MAAM;;;;;;;;;;;CAyBzF,CAAA"}
1
+ {"version":3,"file":"service-public.d.ts","sourceRoot":"","sources":["../src/service-public.ts"],"names":[],"mappings":"AAoBA,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,iBAAiB,CAAA;AAExB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,KAAK,EACV,4BAA4B,EAC5B,6BAA6B,EAC7B,kCAAkC,EAGlC,2BAA2B,EAK3B,4BAA4B,EAE5B,uBAAuB,EACvB,iCAAiC,EACjC,sCAAsC,EACtC,qBAAqB,EACrB,4CAA4C,EAC5C,6CAA6C,EAE7C,kCAAkC,EAClC,gCAAgC,EACjC,MAAM,wBAAwB,CAAA;AAQ/B,UAAU,4BAA4B;IACpC,GAAG,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;IACxB,0BAA0B,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,CAAA;CAC5F;AAmwCD,eAAO,MAAM,2BAA2B;sBAEhC,kBAAkB,SACf,MAAM,GACZ,OAAO,CAAC,iCAAiC,CAAC;2BAuBvC,kBAAkB,SACf,MAAM,GACZ,OAAO,CAAC,sCAAsC,CAAC;mBAa7B,kBAAkB,UAAU,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;8BAKzF,kBAAkB,UACd,MAAM,YACJ,4BAA4B,GACrC,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;sBA+ClC,kBAAkB,UACd,MAAM,SACP,gCAAgC,GACtC,OAAO,CACR;QAAE,OAAO,EAAE,qBAAqB,CAAA;KAAE,GAAG;QAAE,KAAK,EAAE,WAAW,GAAG,0BAA0B,CAAA;KAAE,CACzF;iCAKK,kBAAkB,UACd,MAAM,SACP,gCAAgC,YAC7B,4BAA4B,GACrC,OAAO,CACR;QAAE,OAAO,EAAE,qBAAqB,CAAA;KAAE,GAAG;QAAE,KAAK,EAAE,WAAW,GAAG,0BAA0B,CAAA;KAAE,CACzF;kBAgNK,kBAAkB,UACd,MAAM,SACP,4BAA4B,GAClC,OAAO,CACN,6BAA6B,GAC7B;QAAE,KAAK,EAAE,WAAW,GAAG,2BAA2B,GAAG,yBAAyB,CAAA;KAAE,CACnF;uBA4MwB,kBAAkB,UAAU,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC;8CAiB1F,kBAAkB,UACd,MAAM,SACP,4CAA4C,GAClD,OAAO,CAAC,6CAA6C,GAAG,IAAI,CAAC;wBA4G1D,kBAAkB,UACd,MAAM,SACP,kCAAkC,GACxC,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC;wBA2BpC,kBAAkB,UACd,MAAM,eACD,MAAM,SACZ,kCAAkC,GACxC,OAAO,CAAC,uBAAuB,GAAG,IAAI,GAAG,WAAW,CAAC;wBA2ClD,kBAAkB,UACd,MAAM,eACD,MAAM,GAClB,OAAO,CAAC,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;qBAyB3C,kBAAkB,UACd,MAAM,GACb,OAAO,CAAC,4BAA4B,EAAE,GAAG,IAAI,CAAC;mBAmG3C,kBAAkB,UACd,MAAM,aACH,MAAM,YACR,4BAA4B,GACpC,OAAO,CAAC,2BAA2B,GAAG,IAAI,CAAC;6BA2BxC,kBAAkB,UACd,MAAM,aACH,MAAM,YACR,4BAA4B;;;;;;;;;;iCAMJ,kBAAkB,UAAU,MAAM,aAAa,MAAM;;;;;;;;;;;CAyBzF,CAAA"}
@@ -320,7 +320,7 @@ function resolveFinanceDocumentFileName(invoiceNumber, invoiceType, format) {
320
320
  const extension = format ?? "pdf";
321
321
  return `${invoiceType}-${invoiceNumber}.${extension}`;
322
322
  }
323
- async function listLegalDocumentsForBooking(db, bookingId) {
323
+ async function listLegalDocumentsForBooking(db, bookingId, options = {}) {
324
324
  const contractRows = await db
325
325
  .select({
326
326
  id: contracts.id,
@@ -340,22 +340,20 @@ async function listLegalDocumentsForBooking(db, bookingId) {
340
340
  const bestAttachmentByContractId = new Map();
341
341
  for (const attachment of attachmentRows) {
342
342
  const metadata = getMetadataRecord(attachment.metadata);
343
- const downloadUrl = getMetadataString(metadata, ["url", "downloadUrl"]);
343
+ const downloadUrl = attachment.storageKey && options.resolveDocumentDownloadUrl
344
+ ? await options.resolveDocumentDownloadUrl(attachment.storageKey)
345
+ : getMetadataString(metadata, ["url"]);
344
346
  if (!downloadUrl || bestAttachmentByContractId.has(attachment.contractId)) {
345
347
  continue;
346
348
  }
347
- bestAttachmentByContractId.set(attachment.contractId, attachment);
349
+ bestAttachmentByContractId.set(attachment.contractId, { attachment, downloadUrl });
348
350
  }
349
351
  return contractRows.flatMap((contract) => {
350
- const attachment = bestAttachmentByContractId.get(contract.id);
351
- if (!attachment) {
352
- return [];
353
- }
354
- const metadata = getMetadataRecord(attachment.metadata);
355
- const downloadUrl = getMetadataString(metadata, ["url", "downloadUrl"]);
356
- if (!downloadUrl) {
352
+ const document = bestAttachmentByContractId.get(contract.id);
353
+ if (!document) {
357
354
  return [];
358
355
  }
356
+ const { attachment, downloadUrl } = document;
359
357
  return [
360
358
  {
361
359
  id: attachment.id,
@@ -371,17 +369,7 @@ async function listLegalDocumentsForBooking(db, bookingId) {
371
369
  });
372
370
  }
373
371
  function resolveFinanceDocumentDownloadUrl(metadata) {
374
- return getMetadataString(metadata, [
375
- "downloadUrl",
376
- "download_url",
377
- "signedUrl",
378
- "signed_url",
379
- "publicUrl",
380
- "public_url",
381
- "fileUrl",
382
- "file_url",
383
- "url",
384
- ]);
372
+ return getMetadataString(metadata, ["url"]);
385
373
  }
386
374
  function selectBookingSummaryProductTitle(items) {
387
375
  const preferredItem = items.find((item) => item.itemType === "unit") ??
@@ -409,7 +397,7 @@ function deriveBookingSummaryPaymentStatus(invoicesForBooking, fallbackSellAmoun
409
397
  }
410
398
  return "unpaid";
411
399
  }
412
- async function getFinanceDataForBooking(db, bookingId) {
400
+ async function getFinanceDataForBooking(db, bookingId, options = {}) {
413
401
  const invoiceRows = await db
414
402
  .select()
415
403
  .from(invoices)
@@ -436,11 +424,13 @@ async function getFinanceDataForBooking(db, bookingId) {
436
424
  renditionByInvoiceId.set(rendition.invoiceId, existing);
437
425
  }
438
426
  const invoiceById = new Map(invoiceRows.map((invoice) => [invoice.id, invoice]));
439
- const documents = invoiceRows.map((invoice) => {
427
+ const resolvedDocuments = await Promise.all(invoiceRows.map(async (invoice) => {
440
428
  const renditions = renditionByInvoiceId.get(invoice.id) ?? [];
441
429
  const selectedRendition = renditions.find((rendition) => rendition.status === "ready") ?? renditions[0] ?? null;
442
430
  const metadata = getMetadataRecord(selectedRendition?.metadata ?? null);
443
- const downloadUrl = resolveFinanceDocumentDownloadUrl(metadata);
431
+ const downloadUrl = selectedRendition?.storageKey && options.resolveDocumentDownloadUrl
432
+ ? await options.resolveDocumentDownloadUrl(selectedRendition.storageKey)
433
+ : resolveFinanceDocumentDownloadUrl(metadata);
444
434
  return {
445
435
  invoiceId: invoice.id,
446
436
  invoiceNumber: invoice.invoiceNumber,
@@ -457,7 +447,7 @@ async function getFinanceDataForBooking(db, bookingId) {
457
447
  generatedAt: normalizeDateTime(selectedRendition?.generatedAt ?? null),
458
448
  downloadUrl,
459
449
  };
460
- });
450
+ }));
461
451
  const paymentHistory = paymentRows.flatMap((payment) => {
462
452
  const invoice = invoiceById.get(payment.invoiceId);
463
453
  if (!invoice) {
@@ -479,7 +469,7 @@ async function getFinanceDataForBooking(db, bookingId) {
479
469
  },
480
470
  ];
481
471
  });
482
- const portalDocuments = documents.flatMap((document) => {
472
+ const portalDocuments = resolvedDocuments.flatMap((document) => {
483
473
  if (!document.downloadUrl) {
484
474
  return [];
485
475
  }
@@ -496,7 +486,7 @@ async function getFinanceDataForBooking(db, bookingId) {
496
486
  },
497
487
  ];
498
488
  });
499
- return { documents, payments: paymentHistory, portalDocuments };
489
+ return { documents: resolvedDocuments, payments: paymentHistory, portalDocuments };
500
490
  }
501
491
  function toCustomerCompanion(row) {
502
492
  const metadata = row.metadata ?? null;
@@ -810,7 +800,7 @@ async function getBookingBillingContact(db, bookingId, customerRecord) {
810
800
  const hasValue = Object.values(result).some((value) => typeof value === "string" && value.length > 0);
811
801
  return hasValue ? result : null;
812
802
  }
813
- async function buildBookingDetail(db, bookingId, customerRecord = null) {
803
+ async function buildBookingDetail(db, bookingId, customerRecord = null, options = {}) {
814
804
  const [booking] = await db.select().from(bookings).where(eq(bookings.id, bookingId)).limit(1);
815
805
  if (!booking) {
816
806
  return null;
@@ -848,8 +838,8 @@ async function buildBookingDetail(db, bookingId, customerRecord = null) {
848
838
  .from(bookingFulfillments)
849
839
  .where(eq(bookingFulfillments.bookingId, booking.id))
850
840
  .orderBy(asc(bookingFulfillments.createdAt)),
851
- listLegalDocumentsForBooking(db, booking.id),
852
- getFinanceDataForBooking(db, booking.id),
841
+ listLegalDocumentsForBooking(db, booking.id, options),
842
+ getFinanceDataForBooking(db, booking.id, options),
853
843
  getBookingBillingContact(db, booking.id, customerRecord),
854
844
  ]);
855
845
  const itemLinksByItemId = new Map();
@@ -1601,7 +1591,7 @@ export const publicCustomerPortalService = {
1601
1591
  };
1602
1592
  });
1603
1593
  },
1604
- async getBooking(db, userId, bookingId) {
1594
+ async getBooking(db, userId, bookingId, options = {}) {
1605
1595
  const authProfile = await getAuthProfileRow(db, userId);
1606
1596
  if (!authProfile) {
1607
1597
  return null;
@@ -1621,10 +1611,10 @@ export const publicCustomerPortalService = {
1621
1611
  if (!canAccess) {
1622
1612
  return null;
1623
1613
  }
1624
- return buildBookingDetail(db, bookingId, customerRecord);
1614
+ return buildBookingDetail(db, bookingId, customerRecord, options);
1625
1615
  },
1626
- async listBookingDocuments(db, userId, bookingId) {
1627
- const detail = await this.getBooking(db, userId, bookingId);
1616
+ async listBookingDocuments(db, userId, bookingId, options = {}) {
1617
+ const detail = await this.getBooking(db, userId, bookingId, options);
1628
1618
  return detail?.documents ?? null;
1629
1619
  },
1630
1620
  async getBookingBillingContact(db, userId, bookingId) {