@peektravel/app-utilities 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/index.cjs +547 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +296 -4
- package/dist/index.d.ts +296 -4
- package/dist/index.js +547 -70
- package/dist/index.js.map +1 -1
- package/docs/webhooks.md +1 -1
- package/llms.txt +26 -4
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -338,6 +338,20 @@ var SEARCH_BY_ACTIVITY_DATE = "activityDate";
|
|
|
338
338
|
function normalizeBookingId(bookingId) {
|
|
339
339
|
return bookingId.toLowerCase().replace(/-/g, "_");
|
|
340
340
|
}
|
|
341
|
+
function buildSalesQuery(nodeBody) {
|
|
342
|
+
return `
|
|
343
|
+
query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
|
|
344
|
+
sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
|
|
345
|
+
pageInfo { endCursor hasNextPage }
|
|
346
|
+
edges {
|
|
347
|
+
node {
|
|
348
|
+
${nodeBody}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
`;
|
|
354
|
+
}
|
|
341
355
|
var GUEST_IDENTITY_PII_FIELDS = "name country dateOfBirth email isGdpr";
|
|
342
356
|
var GUEST_CONTACT_PII_FIELDS = `phone
|
|
343
357
|
postalCode
|
|
@@ -431,6 +445,7 @@ function buildBookingQueryFields(fullCustomerAccess) {
|
|
|
431
445
|
availabilityTimeId
|
|
432
446
|
${fullCustomerAccess ? "bookingPortalUrl" : ""}
|
|
433
447
|
operatorNotes
|
|
448
|
+
operatorStatus
|
|
434
449
|
value {
|
|
435
450
|
total {
|
|
436
451
|
formatted
|
|
@@ -513,67 +528,40 @@ function buildBookingsListingQuery(includeGuests, includePriceBreakdown, fullCus
|
|
|
513
528
|
const guestsSection = includeGuests ? buildBookingGuestsFields(fullCustomerAccess) : "";
|
|
514
529
|
const baseFields = buildBookingQueryFields(fullCustomerAccess);
|
|
515
530
|
const fields = includePriceBreakdown ? baseFields.replace("value {", `value { ${PRICE_BREAKDOWN_FIELDS}`).replace("ticketQuantities {", `ticketQuantities { ${TICKET_VALUE_FIELDS}`) : baseFields;
|
|
516
|
-
return `
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
edges {
|
|
521
|
-
node {
|
|
522
|
-
... on Booking {
|
|
523
|
-
${fields}
|
|
524
|
-
${guestsSection}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
531
|
+
return buildSalesQuery(`
|
|
532
|
+
... on Booking {
|
|
533
|
+
${fields}
|
|
534
|
+
${guestsSection}
|
|
529
535
|
}
|
|
530
|
-
|
|
536
|
+
`);
|
|
531
537
|
}
|
|
532
538
|
function buildBookingGuestsQuery(fullCustomerAccess) {
|
|
533
|
-
return `
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
node {
|
|
539
|
-
... on Booking {
|
|
540
|
-
displayId
|
|
541
|
-
id
|
|
542
|
-
${buildBookingGuestsFields(fullCustomerAccess)}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
539
|
+
return buildSalesQuery(`
|
|
540
|
+
... on Booking {
|
|
541
|
+
displayId
|
|
542
|
+
id
|
|
543
|
+
${buildBookingGuestsFields(fullCustomerAccess)}
|
|
546
544
|
}
|
|
547
|
-
|
|
548
|
-
`;
|
|
545
|
+
`);
|
|
549
546
|
}
|
|
550
|
-
var BOOKING_PAYMENTS_ON_FILE_QUERY = `
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
id
|
|
559
|
-
paymentSource { id }
|
|
560
|
-
appliedAt
|
|
561
|
-
currentAmount { amount currency }
|
|
562
|
-
refundableAmount { amount currency }
|
|
563
|
-
}
|
|
564
|
-
id
|
|
565
|
-
displayId
|
|
566
|
-
paymentSources { description type id }
|
|
567
|
-
}
|
|
568
|
-
... on Booking {
|
|
569
|
-
displayId
|
|
570
|
-
id
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
547
|
+
var BOOKING_PAYMENTS_ON_FILE_QUERY = buildSalesQuery(`
|
|
548
|
+
order {
|
|
549
|
+
payments {
|
|
550
|
+
id
|
|
551
|
+
paymentSource { id }
|
|
552
|
+
appliedAt
|
|
553
|
+
currentAmount { amount currency }
|
|
554
|
+
refundableAmount { amount currency }
|
|
574
555
|
}
|
|
556
|
+
id
|
|
557
|
+
displayId
|
|
558
|
+
paymentSources { description type id }
|
|
575
559
|
}
|
|
576
|
-
|
|
560
|
+
... on Booking {
|
|
561
|
+
displayId
|
|
562
|
+
id
|
|
563
|
+
}
|
|
564
|
+
`);
|
|
577
565
|
var UPDATE_OPERATOR_NOTES_MUTATION = `
|
|
578
566
|
mutation Account($input: UpdateOperatorNotesForBookingInput!) {
|
|
579
567
|
updateOperatorNotesForBooking(input: $input) {
|
|
@@ -581,6 +569,13 @@ var UPDATE_OPERATOR_NOTES_MUTATION = `
|
|
|
581
569
|
}
|
|
582
570
|
}
|
|
583
571
|
`;
|
|
572
|
+
var UPDATE_OPERATOR_STATUS_MUTATION = `
|
|
573
|
+
mutation Account($input: UpdateOperatorStatusForBookingInput!) {
|
|
574
|
+
updateOperatorStatusForBooking(input: $input) {
|
|
575
|
+
booking { operatorStatus }
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
`;
|
|
584
579
|
var UPDATE_BOOKING_CHECKIN_MUTATION = `
|
|
585
580
|
mutation Account($input: UpdateBookingCheckInInput!) {
|
|
586
581
|
updateBookingCheckIn(input: $input) {
|
|
@@ -803,6 +798,7 @@ function fromBookingNode(node, includeGuests = false, includePriceBreakdown = fa
|
|
|
803
798
|
availabilityTimeId: data.availabilityTimeId || null,
|
|
804
799
|
portalUrl: data.bookingPortalUrl || null,
|
|
805
800
|
notes: data.operatorNotes || "",
|
|
801
|
+
customStatus: data.operatorStatus || null,
|
|
806
802
|
valueDisplay: data.value?.total?.formatted || "",
|
|
807
803
|
valueAmount: data.value?.total?.amount || "",
|
|
808
804
|
outstandingBalanceAmount: data.balance?.total?.amount || "",
|
|
@@ -957,6 +953,251 @@ function fromBookingGuestsResponse(response) {
|
|
|
957
953
|
return guests;
|
|
958
954
|
}
|
|
959
955
|
|
|
956
|
+
// src/internal/peek/bookings/metadata-converter.ts
|
|
957
|
+
var UPSERT_SUCCESS_TYPENAME = "UpsertBookingFieldResponsesSuccess";
|
|
958
|
+
function fromBookingMetaDataNode(node) {
|
|
959
|
+
return (node?.fieldResponses ?? []).map(fromFieldResponseNode);
|
|
960
|
+
}
|
|
961
|
+
function fromFieldResponseNode(response) {
|
|
962
|
+
const field = response.fieldLocation?.field;
|
|
963
|
+
const prompt = response.fieldLocation?.prompt;
|
|
964
|
+
return {
|
|
965
|
+
id: field?.id || "",
|
|
966
|
+
name: field?.name || "",
|
|
967
|
+
slug: field?.slug || "",
|
|
968
|
+
type: field?.type || "",
|
|
969
|
+
prompt: prompt?.label ?? null,
|
|
970
|
+
promptHint: prompt?.hint ?? null,
|
|
971
|
+
isRequired: Boolean(prompt?.isRequired),
|
|
972
|
+
refid: response.refid || "",
|
|
973
|
+
value: fromValueNode(response.value)
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function fromValueNode(value) {
|
|
977
|
+
if (!value || !value.__typename) return null;
|
|
978
|
+
switch (value.__typename) {
|
|
979
|
+
case "AgeFieldResponseValue": {
|
|
980
|
+
const age = value.age ?? 0;
|
|
981
|
+
return { kind: "age", age, displayValue: String(age) };
|
|
982
|
+
}
|
|
983
|
+
case "AttachmentFieldResponseValue": {
|
|
984
|
+
const attachmentUrl = value.attachmentUrl ?? "";
|
|
985
|
+
const name = value.name ?? null;
|
|
986
|
+
return {
|
|
987
|
+
kind: "attachment",
|
|
988
|
+
attachmentUrl,
|
|
989
|
+
mime: value.mime ?? null,
|
|
990
|
+
name,
|
|
991
|
+
size: value.size ?? null,
|
|
992
|
+
displayValue: name || attachmentUrl
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
case "BarcodeFieldResponseValue": {
|
|
996
|
+
const barcodeValue = value.barcodeValue ?? "";
|
|
997
|
+
return {
|
|
998
|
+
kind: "barcode",
|
|
999
|
+
barcodeType: value.barcodeType ?? null,
|
|
1000
|
+
barcodeValue,
|
|
1001
|
+
displayValue: barcodeValue
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
case "BooleanFieldResponseValue": {
|
|
1005
|
+
const bool = Boolean(value.boolean);
|
|
1006
|
+
return { kind: "boolean", boolean: bool, displayValue: yesNo(bool) };
|
|
1007
|
+
}
|
|
1008
|
+
case "CheckboxFieldResponseValue": {
|
|
1009
|
+
const isChecked = Boolean(value.isChecked);
|
|
1010
|
+
return { kind: "checkbox", isChecked, displayValue: yesNo(isChecked) };
|
|
1011
|
+
}
|
|
1012
|
+
case "CurrencyFieldResponseValue": {
|
|
1013
|
+
const currency = value.currency ?? "";
|
|
1014
|
+
return { kind: "currency", currency, displayValue: currency };
|
|
1015
|
+
}
|
|
1016
|
+
case "DateFieldResponseValue": {
|
|
1017
|
+
const date = value.date ?? "";
|
|
1018
|
+
return { kind: "date", date, displayValue: date };
|
|
1019
|
+
}
|
|
1020
|
+
case "DecimalFieldResponseValue": {
|
|
1021
|
+
const decimal = value.decimal ?? null;
|
|
1022
|
+
return { kind: "decimal", decimal, displayValue: decimal ?? "" };
|
|
1023
|
+
}
|
|
1024
|
+
case "DurationFieldResponseValue": {
|
|
1025
|
+
const amount = value.duration?.amount ?? 0;
|
|
1026
|
+
const unit = value.duration?.unit ?? null;
|
|
1027
|
+
return { kind: "duration", amount, unit, displayValue: amountUnit(amount, unit) };
|
|
1028
|
+
}
|
|
1029
|
+
case "EmailFieldResponseValue": {
|
|
1030
|
+
const email = value.email ?? null;
|
|
1031
|
+
return { kind: "email", email, displayValue: email ?? "" };
|
|
1032
|
+
}
|
|
1033
|
+
case "GuestFieldResponseValue": {
|
|
1034
|
+
const name = value.name ?? null;
|
|
1035
|
+
return {
|
|
1036
|
+
kind: "guest",
|
|
1037
|
+
name,
|
|
1038
|
+
email: value.email ?? null,
|
|
1039
|
+
dateOfBirth: value.dateOfBirth ?? null,
|
|
1040
|
+
notes: value.notes ?? null,
|
|
1041
|
+
waiverSigned: value.waiverSigned ?? null,
|
|
1042
|
+
displayValue: name ?? ""
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
case "HtmlFieldResponseValue": {
|
|
1046
|
+
const html = value.html ?? "";
|
|
1047
|
+
return { kind: "html", html, displayValue: escapeHtml(html) };
|
|
1048
|
+
}
|
|
1049
|
+
case "ImageUrlFieldResponseValue": {
|
|
1050
|
+
const imageUrl = value.imageUrl ?? "";
|
|
1051
|
+
return { kind: "imageUrl", imageUrl, displayValue: imageUrl };
|
|
1052
|
+
}
|
|
1053
|
+
case "IntegerFieldResponseValue": {
|
|
1054
|
+
const integer = value.integer ?? 0;
|
|
1055
|
+
return { kind: "integer", integer, displayValue: String(integer) };
|
|
1056
|
+
}
|
|
1057
|
+
case "LocationFieldResponseValue": {
|
|
1058
|
+
const streetAddress = value.streetAddress ?? null;
|
|
1059
|
+
const locality = value.locality ?? null;
|
|
1060
|
+
const region = value.region ?? null;
|
|
1061
|
+
const postalCode = value.postalCode ?? null;
|
|
1062
|
+
const country = value.country ?? null;
|
|
1063
|
+
return {
|
|
1064
|
+
kind: "location",
|
|
1065
|
+
country,
|
|
1066
|
+
county: value.county ?? null,
|
|
1067
|
+
locality,
|
|
1068
|
+
postalCode,
|
|
1069
|
+
region,
|
|
1070
|
+
streetAddress,
|
|
1071
|
+
displayValue: [streetAddress, locality, region, postalCode, country].filter((part) => part).join(", ")
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
case "LongTextFieldResponseValue": {
|
|
1075
|
+
const longText = value.longText ?? "";
|
|
1076
|
+
return { kind: "longText", longText, displayValue: longText };
|
|
1077
|
+
}
|
|
1078
|
+
case "MetaFieldResponseValue": {
|
|
1079
|
+
const meta = value.meta ?? null;
|
|
1080
|
+
return { kind: "meta", meta, displayValue: meta == null ? "" : JSON.stringify(meta) };
|
|
1081
|
+
}
|
|
1082
|
+
case "PercentFieldResponseValue": {
|
|
1083
|
+
const percent = value.percent ?? null;
|
|
1084
|
+
return { kind: "percent", percent, displayValue: percent == null ? "" : `${percent}%` };
|
|
1085
|
+
}
|
|
1086
|
+
case "PhoneFieldResponseValue": {
|
|
1087
|
+
const phone = value.phone ?? "";
|
|
1088
|
+
return { kind: "phone", phone, displayValue: phone };
|
|
1089
|
+
}
|
|
1090
|
+
case "ShortTextFieldResponseValue": {
|
|
1091
|
+
const shortText = value.shortText ?? "";
|
|
1092
|
+
return { kind: "shortText", shortText, displayValue: shortText };
|
|
1093
|
+
}
|
|
1094
|
+
case "TimeFieldResponseValue": {
|
|
1095
|
+
const time = value.time ?? "";
|
|
1096
|
+
return { kind: "time", time, displayValue: time };
|
|
1097
|
+
}
|
|
1098
|
+
case "UrlFieldResponseValue": {
|
|
1099
|
+
const url = value.url ?? null;
|
|
1100
|
+
return { kind: "url", url, displayValue: url ?? "" };
|
|
1101
|
+
}
|
|
1102
|
+
case "VolumeFieldResponseValue": {
|
|
1103
|
+
const amount = value.volume?.amount ?? "0";
|
|
1104
|
+
const unit = value.volume?.unit ?? null;
|
|
1105
|
+
return { kind: "volume", amount, unit, displayValue: amountUnit(amount, unit) };
|
|
1106
|
+
}
|
|
1107
|
+
case "WeightFieldResponseValue": {
|
|
1108
|
+
const amount = value.weight?.amount ?? "0";
|
|
1109
|
+
const unit = value.weight?.unit ?? null;
|
|
1110
|
+
return { kind: "weight", amount, unit, displayValue: amountUnit(amount, unit) };
|
|
1111
|
+
}
|
|
1112
|
+
default:
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function yesNo(value) {
|
|
1117
|
+
return value ? "Yes" : "No";
|
|
1118
|
+
}
|
|
1119
|
+
function amountUnit(amount, unit) {
|
|
1120
|
+
return unit ? `${amount} ${unit.toLowerCase()}` : String(amount);
|
|
1121
|
+
}
|
|
1122
|
+
function escapeHtml(value) {
|
|
1123
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1124
|
+
}
|
|
1125
|
+
function toSetMetaDataResult(node, bookingId) {
|
|
1126
|
+
return {
|
|
1127
|
+
success: node?.__typename === UPSERT_SUCCESS_TYPENAME,
|
|
1128
|
+
bookingId: node?.bookingId || bookingId,
|
|
1129
|
+
message: node?.message || ""
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function filterMetaDataByIntegrator(metaData, integrator) {
|
|
1133
|
+
const prefix = `integrator:${integrator}:`;
|
|
1134
|
+
return metaData.filter((entry) => entry.slug.startsWith(prefix)).map((entry) => ({ ...entry, slug: entry.slug.slice(prefix.length) }));
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// src/internal/peek/bookings/metadata-queries.ts
|
|
1138
|
+
var GUEST_VALUE_PII_FIELDS = "dateOfBirth\n email\n name\n ";
|
|
1139
|
+
function buildGuestValueFields(fullCustomerAccess) {
|
|
1140
|
+
return `... on GuestFieldResponseValue {
|
|
1141
|
+
${fullCustomerAccess ? GUEST_VALUE_PII_FIELDS : ""}notes
|
|
1142
|
+
waiverSigned
|
|
1143
|
+
}`;
|
|
1144
|
+
}
|
|
1145
|
+
function buildMetaDataValueSelection(fullCustomerAccess) {
|
|
1146
|
+
return `value {
|
|
1147
|
+
__typename
|
|
1148
|
+
... on AgeFieldResponseValue { age }
|
|
1149
|
+
... on AttachmentFieldResponseValue { attachmentUrl mime name size }
|
|
1150
|
+
... on BarcodeFieldResponseValue { barcodeType barcodeValue }
|
|
1151
|
+
... on BooleanFieldResponseValue { boolean }
|
|
1152
|
+
... on CheckboxFieldResponseValue { isChecked }
|
|
1153
|
+
... on CurrencyFieldResponseValue { currency }
|
|
1154
|
+
... on DateFieldResponseValue { date }
|
|
1155
|
+
... on DecimalFieldResponseValue { decimal }
|
|
1156
|
+
... on DurationFieldResponseValue { duration { amount unit } }
|
|
1157
|
+
... on EmailFieldResponseValue { email }
|
|
1158
|
+
${buildGuestValueFields(fullCustomerAccess)}
|
|
1159
|
+
... on HtmlFieldResponseValue { html }
|
|
1160
|
+
... on ImageUrlFieldResponseValue { imageUrl }
|
|
1161
|
+
... on IntegerFieldResponseValue { integer }
|
|
1162
|
+
... on LocationFieldResponseValue { country county locality postalCode region streetAddress }
|
|
1163
|
+
... on LongTextFieldResponseValue { longText }
|
|
1164
|
+
... on MetaFieldResponseValue { meta }
|
|
1165
|
+
... on PercentFieldResponseValue { percent }
|
|
1166
|
+
... on PhoneFieldResponseValue { phone }
|
|
1167
|
+
... on ShortTextFieldResponseValue { shortText }
|
|
1168
|
+
... on TimeFieldResponseValue { time }
|
|
1169
|
+
... on UrlFieldResponseValue { url }
|
|
1170
|
+
... on VolumeFieldResponseValue { volume { amount unit } }
|
|
1171
|
+
... on WeightFieldResponseValue { weight { amount unit } }
|
|
1172
|
+
}`;
|
|
1173
|
+
}
|
|
1174
|
+
function buildBookingMetaDataQuery(fullCustomerAccess) {
|
|
1175
|
+
return buildSalesQuery(`
|
|
1176
|
+
... on Booking {
|
|
1177
|
+
displayId
|
|
1178
|
+
id
|
|
1179
|
+
fieldResponses {
|
|
1180
|
+
fieldLocation {
|
|
1181
|
+
field { id name slug type }
|
|
1182
|
+
prompt { label hint isRequired }
|
|
1183
|
+
}
|
|
1184
|
+
refid
|
|
1185
|
+
${buildMetaDataValueSelection(fullCustomerAccess)}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
`);
|
|
1189
|
+
}
|
|
1190
|
+
var UPSERT_BOOKING_FIELD_RESPONSES_MUTATION = `
|
|
1191
|
+
mutation UpsertBookingFieldResponses($input: UpsertBookingFieldResponsesInput!) {
|
|
1192
|
+
upsertBookingFieldResponses(input: $input) {
|
|
1193
|
+
__typename
|
|
1194
|
+
... on UpsertBookingFieldResponsesSuccess { bookingId message }
|
|
1195
|
+
... on BookingNotFoundError { message bookingId }
|
|
1196
|
+
... on GenericError { message }
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
`;
|
|
1200
|
+
|
|
960
1201
|
// src/internal/peek/bookings/payments-on-file-converter.ts
|
|
961
1202
|
function fromPaymentsOnFileResponse(response, bookingId) {
|
|
962
1203
|
const firstEdge = (response?.sales?.edges ?? [])[0];
|
|
@@ -1216,10 +1457,41 @@ function assertRequiredAnswered(resolved, questions) {
|
|
|
1216
1457
|
}
|
|
1217
1458
|
}
|
|
1218
1459
|
|
|
1460
|
+
// src/internal/peek/bookings/list-price.ts
|
|
1461
|
+
var ERROR_LIST_PRICE_INVALID = "listPrice must be a positive number string, e.g. '100', '1,000', or '10.00'";
|
|
1462
|
+
function parseListPriceToCents(listPrice) {
|
|
1463
|
+
if (typeof listPrice !== "string") {
|
|
1464
|
+
throw new Error(ERROR_LIST_PRICE_INVALID);
|
|
1465
|
+
}
|
|
1466
|
+
const cleaned = listPrice.replace(/,/g, "").trim();
|
|
1467
|
+
if (!/^\d+(\.\d+)?$/.test(cleaned)) {
|
|
1468
|
+
throw new Error(ERROR_LIST_PRICE_INVALID);
|
|
1469
|
+
}
|
|
1470
|
+
const cents = Math.round(Number(cleaned) * 100);
|
|
1471
|
+
if (!Number.isFinite(cents) || cents <= 0) {
|
|
1472
|
+
throw new Error(ERROR_LIST_PRICE_INVALID);
|
|
1473
|
+
}
|
|
1474
|
+
return cents;
|
|
1475
|
+
}
|
|
1476
|
+
function centsToAmount(cents) {
|
|
1477
|
+
return (cents / 100).toFixed(2);
|
|
1478
|
+
}
|
|
1479
|
+
function distributeListPrice(listPrice, ticketCount) {
|
|
1480
|
+
const totalCents = parseListPriceToCents(listPrice);
|
|
1481
|
+
const perTicketCents = Math.floor(totalCents / ticketCount);
|
|
1482
|
+
const amounts = [];
|
|
1483
|
+
for (let i = 0; i < ticketCount - 1; i += 1) {
|
|
1484
|
+
amounts.push(centsToAmount(perTicketCents));
|
|
1485
|
+
}
|
|
1486
|
+
amounts.push(centsToAmount(totalCents - perTicketCents * (ticketCount - 1)));
|
|
1487
|
+
return amounts;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1219
1490
|
// src/internal/peek/bookings/booking-service.ts
|
|
1220
1491
|
var DEFAULT_PAGE_SIZE2 = 50;
|
|
1221
1492
|
var DEFAULT_CANCEL_NOTE = "Canceled";
|
|
1222
1493
|
var DEFAULT_CUSTOMER_MESSAGE = "Charge initiated via API";
|
|
1494
|
+
var DEFAULT_CURRENCY = "USD";
|
|
1223
1495
|
var BOOKING_DB_ID_REGEX = /^b_[a-z0-9]+$/;
|
|
1224
1496
|
var BOOKING_DISPLAY_ID_REGEX = /^B-[A-Z0-9]+$/;
|
|
1225
1497
|
var ORDER_DB_ID_REGEX = /^o_[a-z0-9]+$/;
|
|
@@ -1232,6 +1504,17 @@ var ERROR_ADDON_OPTION_ID_REQUIRED = "addonOptionId is required";
|
|
|
1232
1504
|
var ERROR_QUANTITY_INVALID = "quantity must be a positive integer string";
|
|
1233
1505
|
var ERROR_INVALID_BOOKING_ID = "bookingId is required and must be a valid booking id, e.g. 'b_abc123' or 'B-ABC123'";
|
|
1234
1506
|
var ERROR_INVALID_ORDER_ID = "orderId is required and must be a valid order id, e.g. 'o_abc123' or 'O-ABC123'";
|
|
1507
|
+
var ERROR_CUSTOM_STATUS_REQUIRED = "customStatus is required and must be a non-empty string";
|
|
1508
|
+
var ERROR_GUESTS_REQUIRED = "at least one guest is required";
|
|
1509
|
+
var ERROR_ATTACHMENTS_REQUIRED = "at least one attachment is required";
|
|
1510
|
+
var META_FIELD_GUEST = "guest";
|
|
1511
|
+
var META_FIELD_ATTACHMENT = "attachment";
|
|
1512
|
+
var META_FIELD_MANIFEST_URL = "manifest_url";
|
|
1513
|
+
var META_FIELD_RESERVATION_ID = "reservation_id";
|
|
1514
|
+
var META_FIELD_BOOKING_STATUS = "booking_status";
|
|
1515
|
+
var META_FIELD_ASSIGNED_PRODUCT = "assigned_product";
|
|
1516
|
+
var META_FIELD_ASSIGNED_EMPLOYEE = "assigned_employee";
|
|
1517
|
+
var META_FIELD_INSURANCE_PURCHASED = "insurance_purchased";
|
|
1235
1518
|
var ERROR_BOOKING_NOT_FOUND = "Booking not found";
|
|
1236
1519
|
var ERROR_MULTIPLE_BOOKINGS_FOUND = "Expected exactly one booking for the provided bookingId";
|
|
1237
1520
|
var ERROR_NO_ADDON_OPTION_TO_REMOVE = "No confirmed add-on option matching addonOptionId was found on the booking";
|
|
@@ -1244,12 +1527,15 @@ var BookingService = class {
|
|
|
1244
1527
|
this.deps = deps;
|
|
1245
1528
|
this.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE2;
|
|
1246
1529
|
this.fullCustomerAccess = resolveAccessOptions(options.accessOptions).fullCustomerAccess;
|
|
1530
|
+
this.integrator = options.integrator ?? "";
|
|
1247
1531
|
}
|
|
1248
1532
|
client;
|
|
1249
1533
|
deps;
|
|
1250
1534
|
pageSize;
|
|
1251
1535
|
/** Whether customer PII is requested and payment operations are allowed. */
|
|
1252
1536
|
fullCustomerAccess;
|
|
1537
|
+
/** Integrator id scoping `getMetaData` results. */
|
|
1538
|
+
integrator;
|
|
1253
1539
|
/**
|
|
1254
1540
|
* Throws a {@link PiiAccessDisabledError} for `operation` when this service was
|
|
1255
1541
|
* created without `fullCustomerAccess` — used to gate payment and booking-modification
|
|
@@ -1337,6 +1623,131 @@ var BookingService = class {
|
|
|
1337
1623
|
);
|
|
1338
1624
|
return fromBookingGuestsResponse(body.data);
|
|
1339
1625
|
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Returns a booking's custom-field metadata (its `fieldResponses`), scoped to
|
|
1628
|
+
* this service's integrator. Only fields whose slug starts with
|
|
1629
|
+
* `integrator:<integrator>:` are returned, and that prefix is stripped from
|
|
1630
|
+
* each returned `slug`. Returns `[]` when the booking is not found or has no
|
|
1631
|
+
* matching fields.
|
|
1632
|
+
*
|
|
1633
|
+
* @example
|
|
1634
|
+
* ```ts
|
|
1635
|
+
* const meta = await peek.getBookingService().getMetaData("b_abc123");
|
|
1636
|
+
* for (const field of meta) {
|
|
1637
|
+
* if (field.value?.kind === "url") console.log(field.slug, field.value.url);
|
|
1638
|
+
* }
|
|
1639
|
+
* ```
|
|
1640
|
+
*/
|
|
1641
|
+
async getMetaData(bookingId) {
|
|
1642
|
+
assertBookingId(bookingId);
|
|
1643
|
+
const body = await this.client.request(
|
|
1644
|
+
SALES_ENDPOINT,
|
|
1645
|
+
buildBookingMetaDataQuery(this.fullCustomerAccess),
|
|
1646
|
+
buildBookingsVariables({
|
|
1647
|
+
pageSize: this.pageSize,
|
|
1648
|
+
after: null,
|
|
1649
|
+
bookingId: normalizeBookingId(bookingId)
|
|
1650
|
+
})
|
|
1651
|
+
);
|
|
1652
|
+
const firstEdge = (body.data?.sales?.edges ?? [])[0];
|
|
1653
|
+
if (!firstEdge) {
|
|
1654
|
+
return [];
|
|
1655
|
+
}
|
|
1656
|
+
return filterMetaDataByIntegrator(fromBookingMetaDataNode(firstEdge.node), this.integrator);
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Sets the booking's `guest` metadata field. Each guest requires a `name` and
|
|
1660
|
+
* `email`; `dateOfBirth`/`waiverSigned`/`notes` are optional.
|
|
1661
|
+
*/
|
|
1662
|
+
async setMetaDataGuest(bookingId, guests) {
|
|
1663
|
+
if (!Array.isArray(guests) || guests.length === 0) {
|
|
1664
|
+
throw new Error(ERROR_GUESTS_REQUIRED);
|
|
1665
|
+
}
|
|
1666
|
+
const built = guests.map((guest) => {
|
|
1667
|
+
assertNonEmptyString(guest?.name, "guest name");
|
|
1668
|
+
assertNonEmptyString(guest?.email, "guest email");
|
|
1669
|
+
const out = { name: guest.name, email: guest.email };
|
|
1670
|
+
if (guest.dateOfBirth !== void 0) out.dateOfBirth = guest.dateOfBirth;
|
|
1671
|
+
if (guest.waiverSigned !== void 0) out.waiverSigned = guest.waiverSigned;
|
|
1672
|
+
if (guest.notes !== void 0) out.notes = guest.notes;
|
|
1673
|
+
return out;
|
|
1674
|
+
});
|
|
1675
|
+
return this.upsertFieldResponse(bookingId, { fieldName: META_FIELD_GUEST, guests: built });
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Sets the booking's `attachment` metadata field. Each attachment requires a
|
|
1679
|
+
* `name` and `attachmentUrl`.
|
|
1680
|
+
*/
|
|
1681
|
+
async setMetaDataAttachment(bookingId, attachments) {
|
|
1682
|
+
if (!Array.isArray(attachments) || attachments.length === 0) {
|
|
1683
|
+
throw new Error(ERROR_ATTACHMENTS_REQUIRED);
|
|
1684
|
+
}
|
|
1685
|
+
const built = attachments.map((attachment) => {
|
|
1686
|
+
assertNonEmptyString(attachment?.name, "attachment name");
|
|
1687
|
+
assertNonEmptyString(attachment?.attachmentUrl, "attachment attachmentUrl");
|
|
1688
|
+
return { name: attachment.name, attachmentUrl: attachment.attachmentUrl };
|
|
1689
|
+
});
|
|
1690
|
+
return this.upsertFieldResponse(bookingId, {
|
|
1691
|
+
fieldName: META_FIELD_ATTACHMENT,
|
|
1692
|
+
attachments: built
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
/** Sets the booking's `manifest_url` metadata field. */
|
|
1696
|
+
async setMetaDataManifestUrl(bookingId, url) {
|
|
1697
|
+
assertNonEmptyString(url, "url");
|
|
1698
|
+
return this.upsertFieldResponse(bookingId, { fieldName: META_FIELD_MANIFEST_URL, url });
|
|
1699
|
+
}
|
|
1700
|
+
/** Sets the booking's `reservation_id` metadata field. */
|
|
1701
|
+
async setMetaDataReservationId(bookingId, reservationId) {
|
|
1702
|
+
return this.setShortTextField(bookingId, META_FIELD_RESERVATION_ID, reservationId, "reservationId");
|
|
1703
|
+
}
|
|
1704
|
+
/** Sets the booking's `booking_status` metadata field. */
|
|
1705
|
+
async setMetaDataBookingStatus(bookingId, status) {
|
|
1706
|
+
return this.setShortTextField(bookingId, META_FIELD_BOOKING_STATUS, status, "status");
|
|
1707
|
+
}
|
|
1708
|
+
/** Sets the booking's `assigned_product` metadata field. */
|
|
1709
|
+
async setMetaDataAssignedProduct(bookingId, productName) {
|
|
1710
|
+
return this.setShortTextField(bookingId, META_FIELD_ASSIGNED_PRODUCT, productName, "productName");
|
|
1711
|
+
}
|
|
1712
|
+
/** Sets the booking's `assigned_employee` metadata field. */
|
|
1713
|
+
async setMetaDataAssignedEmployee(bookingId, employeeName) {
|
|
1714
|
+
return this.setShortTextField(bookingId, META_FIELD_ASSIGNED_EMPLOYEE, employeeName, "employeeName");
|
|
1715
|
+
}
|
|
1716
|
+
/** Sets the booking's `insurance_purchased` metadata field. */
|
|
1717
|
+
async setMetaDataInsurancePurchased(bookingId, insurancePurchased) {
|
|
1718
|
+
if (typeof insurancePurchased !== "boolean") {
|
|
1719
|
+
throw new Error("insurancePurchased is required and must be a boolean");
|
|
1720
|
+
}
|
|
1721
|
+
return this.upsertFieldResponse(bookingId, {
|
|
1722
|
+
fieldName: META_FIELD_INSURANCE_PURCHASED,
|
|
1723
|
+
boolean: insurancePurchased
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
/** Validates a non-empty `shortText` value and upserts it under `fieldName`. */
|
|
1727
|
+
setShortTextField(bookingId, fieldName, value, argName) {
|
|
1728
|
+
assertNonEmptyString(value, argName);
|
|
1729
|
+
return this.upsertFieldResponse(bookingId, { fieldName, shortText: value });
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Validates the booking id and upserts a single field response (scoped to this
|
|
1733
|
+
* service's integrator), mapping the result union into a {@link SetMetaDataResult}.
|
|
1734
|
+
*/
|
|
1735
|
+
async upsertFieldResponse(bookingId, fieldResponse) {
|
|
1736
|
+
assertBookingId(bookingId);
|
|
1737
|
+
const normalized = normalizeBookingId(bookingId);
|
|
1738
|
+
const body = await this.client.request(
|
|
1739
|
+
SALES_ENDPOINT,
|
|
1740
|
+
UPSERT_BOOKING_FIELD_RESPONSES_MUTATION,
|
|
1741
|
+
{
|
|
1742
|
+
input: {
|
|
1743
|
+
bookingId: normalized,
|
|
1744
|
+
integrator: this.integrator,
|
|
1745
|
+
fieldResponses: [fieldResponse]
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
);
|
|
1749
|
+
return toSetMetaDataResult(body.data?.upsertBookingFieldResponses, normalized);
|
|
1750
|
+
}
|
|
1340
1751
|
/** Returns the payments on file for a booking, or null when not found. */
|
|
1341
1752
|
async getPaymentsOnFile(bookingId) {
|
|
1342
1753
|
this.assertPiiEnabled("getPaymentsOnFile");
|
|
@@ -1381,6 +1792,30 @@ ${note}`;
|
|
|
1381
1792
|
});
|
|
1382
1793
|
return this.getById(normalized);
|
|
1383
1794
|
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Sets a booking's operator-facing custom status. Returns `true` when the
|
|
1797
|
+
* gateway confirms the stored `operatorStatus` equals the requested
|
|
1798
|
+
* `customStatus`, `false` otherwise.
|
|
1799
|
+
*
|
|
1800
|
+
* @example
|
|
1801
|
+
* ```ts
|
|
1802
|
+
* const ok = await peek.getBookingService().updateCustomStatus("b_abc123", "Awaiting deposit");
|
|
1803
|
+
* ```
|
|
1804
|
+
*
|
|
1805
|
+
* @throws {Error} when `bookingId` is not a valid booking id (`b_…`/`B-…`) or
|
|
1806
|
+
* when `customStatus` is not a non-empty string.
|
|
1807
|
+
*/
|
|
1808
|
+
async updateCustomStatus(bookingId, customStatus) {
|
|
1809
|
+
assertBookingId(bookingId);
|
|
1810
|
+
assertCustomStatus(customStatus);
|
|
1811
|
+
const normalized = normalizeBookingId(bookingId);
|
|
1812
|
+
const body = await this.client.request(
|
|
1813
|
+
SALES_ENDPOINT,
|
|
1814
|
+
UPDATE_OPERATOR_STATUS_MUTATION,
|
|
1815
|
+
{ input: { id: normalized, operatorStatus: customStatus } }
|
|
1816
|
+
);
|
|
1817
|
+
return body.data?.updateOperatorStatusForBooking?.booking?.operatorStatus === customStatus;
|
|
1818
|
+
}
|
|
1384
1819
|
/** Cancels a booking and returns its id/displayId/status. */
|
|
1385
1820
|
async cancel(bookingId, notes = DEFAULT_CANCEL_NOTE) {
|
|
1386
1821
|
assertBookingId(bookingId);
|
|
@@ -1736,13 +2171,26 @@ ${note}`;
|
|
|
1736
2171
|
*/
|
|
1737
2172
|
async create(input) {
|
|
1738
2173
|
validateCreateInput(input);
|
|
1739
|
-
const
|
|
2174
|
+
const ticketCount = input.tickets.reduce((count, ticket) => count + ticket.quantity, 0);
|
|
2175
|
+
const listPriceAmounts = input.listPrice === void 0 ? null : distributeListPrice(input.listPrice, ticketCount);
|
|
2176
|
+
const [questionAnswers, currency] = await Promise.all([
|
|
2177
|
+
this.buildQuestionAnswers(input),
|
|
2178
|
+
listPriceAmounts ? this.resolveActivityCurrency(input.activityId) : Promise.resolve("")
|
|
2179
|
+
]);
|
|
2180
|
+
let seatIndex = 0;
|
|
1740
2181
|
const tickets = input.tickets.flatMap(
|
|
1741
|
-
(ticket) => Array.from({ length: ticket.quantity }, () =>
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
2182
|
+
(ticket) => Array.from({ length: ticket.quantity }, () => {
|
|
2183
|
+
const seat = {
|
|
2184
|
+
resourceOptionId: ticket.resourceOptionId,
|
|
2185
|
+
reservationStatus: "CONFIRMED",
|
|
2186
|
+
refid: crypto.randomUUID()
|
|
2187
|
+
};
|
|
2188
|
+
if (listPriceAmounts) {
|
|
2189
|
+
seat.price = { amount: listPriceAmounts[seatIndex], currency };
|
|
2190
|
+
}
|
|
2191
|
+
seatIndex += 1;
|
|
2192
|
+
return seat;
|
|
2193
|
+
})
|
|
1746
2194
|
);
|
|
1747
2195
|
const bookingQuote = {
|
|
1748
2196
|
tickets,
|
|
@@ -1857,6 +2305,15 @@ ${note}`;
|
|
|
1857
2305
|
}
|
|
1858
2306
|
return result.transactionId;
|
|
1859
2307
|
}
|
|
2308
|
+
/**
|
|
2309
|
+
* Resolves an activity's ISO currency via the product service, falling back to
|
|
2310
|
+
* {@link DEFAULT_CURRENCY} when the activity (or its currency) can't be found.
|
|
2311
|
+
*/
|
|
2312
|
+
async resolveActivityCurrency(activityId) {
|
|
2313
|
+
const products = await this.deps.productService.getAllProducts();
|
|
2314
|
+
const match = products.find((product) => product.productId === activityId);
|
|
2315
|
+
return match?.currency || DEFAULT_CURRENCY;
|
|
2316
|
+
}
|
|
1860
2317
|
/** Finds the parent item id of an add-on by matching its option id. */
|
|
1861
2318
|
async resolveParentItemId(addonOptionId) {
|
|
1862
2319
|
const products = await this.deps.productService.getAllProducts();
|
|
@@ -1931,6 +2388,16 @@ function assertBookingId(bookingId) {
|
|
|
1931
2388
|
throw new Error(ERROR_INVALID_BOOKING_ID);
|
|
1932
2389
|
}
|
|
1933
2390
|
}
|
|
2391
|
+
function assertCustomStatus(customStatus) {
|
|
2392
|
+
if (typeof customStatus !== "string" || customStatus.trim().length === 0) {
|
|
2393
|
+
throw new Error(ERROR_CUSTOM_STATUS_REQUIRED);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
function assertNonEmptyString(value, field) {
|
|
2397
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
2398
|
+
throw new Error(`${field} is required and must be a non-empty string`);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
1934
2401
|
function assertOrderId(orderId) {
|
|
1935
2402
|
if (!(ORDER_DB_ID_REGEX.test(orderId) || ORDER_DISPLAY_ID_REGEX.test(orderId))) {
|
|
1936
2403
|
throw new Error(ERROR_INVALID_ORDER_ID);
|
|
@@ -2197,7 +2664,7 @@ async function requestWithRetry(http, url, init, label, onResponse) {
|
|
|
2197
2664
|
// src/internal/sdk-headers.ts
|
|
2198
2665
|
var SDK_HEADER_NAME = "x-peek-sdk";
|
|
2199
2666
|
var SDK_PLATFORM = "js";
|
|
2200
|
-
var SDK_VERSION = "0.9.
|
|
2667
|
+
var SDK_VERSION = "0.9.3";
|
|
2201
2668
|
var SDK_HEADER_VALUE = `${SDK_PLATFORM}-${SDK_VERSION}`;
|
|
2202
2669
|
|
|
2203
2670
|
// src/internal/peek/graphql-client.ts
|
|
@@ -3483,7 +3950,7 @@ var CREATE_PROMO_CODE_MUTATION = `
|
|
|
3483
3950
|
|
|
3484
3951
|
// src/internal/peek/promo-codes/promo-code-service.ts
|
|
3485
3952
|
var DEFAULT_PAGE_SIZE3 = 50;
|
|
3486
|
-
var
|
|
3953
|
+
var DEFAULT_CURRENCY2 = "USD";
|
|
3487
3954
|
var CURRENCY_PATTERN = /^[A-Z]{3}$/;
|
|
3488
3955
|
var PromoCodeService = class {
|
|
3489
3956
|
constructor(client, options = {}) {
|
|
@@ -3529,7 +3996,7 @@ var PromoCodeService = class {
|
|
|
3529
3996
|
if (Number.isNaN(parseFloat(input.amount))) {
|
|
3530
3997
|
throw new Error("amount must be a valid number");
|
|
3531
3998
|
}
|
|
3532
|
-
const currency = input.currency ||
|
|
3999
|
+
const currency = input.currency || DEFAULT_CURRENCY2;
|
|
3533
4000
|
if (!CURRENCY_PATTERN.test(currency)) {
|
|
3534
4001
|
throw new Error("currency must be 3 uppercase letters");
|
|
3535
4002
|
}
|
|
@@ -3903,6 +4370,7 @@ var PeekAccessService = class {
|
|
|
3903
4370
|
productServiceOptions;
|
|
3904
4371
|
accessOptions;
|
|
3905
4372
|
jwtSecret;
|
|
4373
|
+
issuer;
|
|
3906
4374
|
productService;
|
|
3907
4375
|
accountUserService;
|
|
3908
4376
|
resourcePoolService;
|
|
@@ -3913,7 +4381,7 @@ var PeekAccessService = class {
|
|
|
3913
4381
|
dailyNoteService;
|
|
3914
4382
|
availabilityService;
|
|
3915
4383
|
membershipService;
|
|
3916
|
-
|
|
4384
|
+
bookingServices = /* @__PURE__ */ new Map();
|
|
3917
4385
|
reviewService;
|
|
3918
4386
|
constructor(config) {
|
|
3919
4387
|
const hasApiUrl = !!config.apiUrl;
|
|
@@ -3924,6 +4392,7 @@ var PeekAccessService = class {
|
|
|
3924
4392
|
if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "PeekAccessService");
|
|
3925
4393
|
if (!hasApiUrl && !isV2) requireNonEmpty(config.gatewayKey ?? "", "gatewayKey", "PeekAccessService");
|
|
3926
4394
|
this.jwtSecret = config.jwtSecret;
|
|
4395
|
+
this.issuer = config.issuer;
|
|
3927
4396
|
const logger = config.logger ?? noopLogger;
|
|
3928
4397
|
const tokens = createTokenManager(config);
|
|
3929
4398
|
const useRegistrySlug = hasApiUrl || isV2;
|
|
@@ -4084,17 +4553,25 @@ var PeekAccessService = class {
|
|
|
4084
4553
|
}
|
|
4085
4554
|
/**
|
|
4086
4555
|
* Returns the {@link BookingService} for this install, bound to the shared
|
|
4087
|
-
* authenticated transport.
|
|
4556
|
+
* authenticated transport. Instances are created lazily and reused per
|
|
4557
|
+
* `integrator`.
|
|
4558
|
+
*
|
|
4559
|
+
* `integrator` scopes `getMetaData` results to a custom integrator id. **Do
|
|
4560
|
+
* not set it** unless Peek engineering created a custom integrator id for this
|
|
4561
|
+
* integration — omit it and the correct default is used.
|
|
4088
4562
|
*/
|
|
4089
|
-
getBookingService() {
|
|
4090
|
-
|
|
4091
|
-
|
|
4563
|
+
getBookingService(integrator) {
|
|
4564
|
+
const resolved = integrator ?? this.issuer;
|
|
4565
|
+
let service = this.bookingServices.get(resolved);
|
|
4566
|
+
if (!service) {
|
|
4567
|
+
service = new BookingService(
|
|
4092
4568
|
this.client,
|
|
4093
4569
|
{ productService: this.getProductService() },
|
|
4094
|
-
{ accessOptions: this.accessOptions }
|
|
4570
|
+
{ accessOptions: this.accessOptions, integrator: resolved }
|
|
4095
4571
|
);
|
|
4572
|
+
this.bookingServices.set(resolved, service);
|
|
4096
4573
|
}
|
|
4097
|
-
return
|
|
4574
|
+
return service;
|
|
4098
4575
|
}
|
|
4099
4576
|
/**
|
|
4100
4577
|
* Returns the {@link ReviewService} for this install, bound to the shared
|