@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.js
CHANGED
|
@@ -316,6 +316,20 @@ var SEARCH_BY_ACTIVITY_DATE = "activityDate";
|
|
|
316
316
|
function normalizeBookingId(bookingId) {
|
|
317
317
|
return bookingId.toLowerCase().replace(/-/g, "_");
|
|
318
318
|
}
|
|
319
|
+
function buildSalesQuery(nodeBody) {
|
|
320
|
+
return `
|
|
321
|
+
query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
|
|
322
|
+
sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
|
|
323
|
+
pageInfo { endCursor hasNextPage }
|
|
324
|
+
edges {
|
|
325
|
+
node {
|
|
326
|
+
${nodeBody}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
`;
|
|
332
|
+
}
|
|
319
333
|
var GUEST_IDENTITY_PII_FIELDS = "name country dateOfBirth email isGdpr";
|
|
320
334
|
var GUEST_CONTACT_PII_FIELDS = `phone
|
|
321
335
|
postalCode
|
|
@@ -409,6 +423,7 @@ function buildBookingQueryFields(fullCustomerAccess) {
|
|
|
409
423
|
availabilityTimeId
|
|
410
424
|
${fullCustomerAccess ? "bookingPortalUrl" : ""}
|
|
411
425
|
operatorNotes
|
|
426
|
+
operatorStatus
|
|
412
427
|
value {
|
|
413
428
|
total {
|
|
414
429
|
formatted
|
|
@@ -491,67 +506,40 @@ function buildBookingsListingQuery(includeGuests, includePriceBreakdown, fullCus
|
|
|
491
506
|
const guestsSection = includeGuests ? buildBookingGuestsFields(fullCustomerAccess) : "";
|
|
492
507
|
const baseFields = buildBookingQueryFields(fullCustomerAccess);
|
|
493
508
|
const fields = includePriceBreakdown ? baseFields.replace("value {", `value { ${PRICE_BREAKDOWN_FIELDS}`).replace("ticketQuantities {", `ticketQuantities { ${TICKET_VALUE_FIELDS}`) : baseFields;
|
|
494
|
-
return `
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
edges {
|
|
499
|
-
node {
|
|
500
|
-
... on Booking {
|
|
501
|
-
${fields}
|
|
502
|
-
${guestsSection}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
509
|
+
return buildSalesQuery(`
|
|
510
|
+
... on Booking {
|
|
511
|
+
${fields}
|
|
512
|
+
${guestsSection}
|
|
507
513
|
}
|
|
508
|
-
|
|
514
|
+
`);
|
|
509
515
|
}
|
|
510
516
|
function buildBookingGuestsQuery(fullCustomerAccess) {
|
|
511
|
-
return `
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
node {
|
|
517
|
-
... on Booking {
|
|
518
|
-
displayId
|
|
519
|
-
id
|
|
520
|
-
${buildBookingGuestsFields(fullCustomerAccess)}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
517
|
+
return buildSalesQuery(`
|
|
518
|
+
... on Booking {
|
|
519
|
+
displayId
|
|
520
|
+
id
|
|
521
|
+
${buildBookingGuestsFields(fullCustomerAccess)}
|
|
524
522
|
}
|
|
525
|
-
|
|
526
|
-
`;
|
|
523
|
+
`);
|
|
527
524
|
}
|
|
528
|
-
var BOOKING_PAYMENTS_ON_FILE_QUERY = `
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
id
|
|
537
|
-
paymentSource { id }
|
|
538
|
-
appliedAt
|
|
539
|
-
currentAmount { amount currency }
|
|
540
|
-
refundableAmount { amount currency }
|
|
541
|
-
}
|
|
542
|
-
id
|
|
543
|
-
displayId
|
|
544
|
-
paymentSources { description type id }
|
|
545
|
-
}
|
|
546
|
-
... on Booking {
|
|
547
|
-
displayId
|
|
548
|
-
id
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
525
|
+
var BOOKING_PAYMENTS_ON_FILE_QUERY = buildSalesQuery(`
|
|
526
|
+
order {
|
|
527
|
+
payments {
|
|
528
|
+
id
|
|
529
|
+
paymentSource { id }
|
|
530
|
+
appliedAt
|
|
531
|
+
currentAmount { amount currency }
|
|
532
|
+
refundableAmount { amount currency }
|
|
552
533
|
}
|
|
534
|
+
id
|
|
535
|
+
displayId
|
|
536
|
+
paymentSources { description type id }
|
|
553
537
|
}
|
|
554
|
-
|
|
538
|
+
... on Booking {
|
|
539
|
+
displayId
|
|
540
|
+
id
|
|
541
|
+
}
|
|
542
|
+
`);
|
|
555
543
|
var UPDATE_OPERATOR_NOTES_MUTATION = `
|
|
556
544
|
mutation Account($input: UpdateOperatorNotesForBookingInput!) {
|
|
557
545
|
updateOperatorNotesForBooking(input: $input) {
|
|
@@ -559,6 +547,13 @@ var UPDATE_OPERATOR_NOTES_MUTATION = `
|
|
|
559
547
|
}
|
|
560
548
|
}
|
|
561
549
|
`;
|
|
550
|
+
var UPDATE_OPERATOR_STATUS_MUTATION = `
|
|
551
|
+
mutation Account($input: UpdateOperatorStatusForBookingInput!) {
|
|
552
|
+
updateOperatorStatusForBooking(input: $input) {
|
|
553
|
+
booking { operatorStatus }
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
`;
|
|
562
557
|
var UPDATE_BOOKING_CHECKIN_MUTATION = `
|
|
563
558
|
mutation Account($input: UpdateBookingCheckInInput!) {
|
|
564
559
|
updateBookingCheckIn(input: $input) {
|
|
@@ -781,6 +776,7 @@ function fromBookingNode(node, includeGuests = false, includePriceBreakdown = fa
|
|
|
781
776
|
availabilityTimeId: data.availabilityTimeId || null,
|
|
782
777
|
portalUrl: data.bookingPortalUrl || null,
|
|
783
778
|
notes: data.operatorNotes || "",
|
|
779
|
+
customStatus: data.operatorStatus || null,
|
|
784
780
|
valueDisplay: data.value?.total?.formatted || "",
|
|
785
781
|
valueAmount: data.value?.total?.amount || "",
|
|
786
782
|
outstandingBalanceAmount: data.balance?.total?.amount || "",
|
|
@@ -935,6 +931,251 @@ function fromBookingGuestsResponse(response) {
|
|
|
935
931
|
return guests;
|
|
936
932
|
}
|
|
937
933
|
|
|
934
|
+
// src/internal/peek/bookings/metadata-converter.ts
|
|
935
|
+
var UPSERT_SUCCESS_TYPENAME = "UpsertBookingFieldResponsesSuccess";
|
|
936
|
+
function fromBookingMetaDataNode(node) {
|
|
937
|
+
return (node?.fieldResponses ?? []).map(fromFieldResponseNode);
|
|
938
|
+
}
|
|
939
|
+
function fromFieldResponseNode(response) {
|
|
940
|
+
const field = response.fieldLocation?.field;
|
|
941
|
+
const prompt = response.fieldLocation?.prompt;
|
|
942
|
+
return {
|
|
943
|
+
id: field?.id || "",
|
|
944
|
+
name: field?.name || "",
|
|
945
|
+
slug: field?.slug || "",
|
|
946
|
+
type: field?.type || "",
|
|
947
|
+
prompt: prompt?.label ?? null,
|
|
948
|
+
promptHint: prompt?.hint ?? null,
|
|
949
|
+
isRequired: Boolean(prompt?.isRequired),
|
|
950
|
+
refid: response.refid || "",
|
|
951
|
+
value: fromValueNode(response.value)
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
function fromValueNode(value) {
|
|
955
|
+
if (!value || !value.__typename) return null;
|
|
956
|
+
switch (value.__typename) {
|
|
957
|
+
case "AgeFieldResponseValue": {
|
|
958
|
+
const age = value.age ?? 0;
|
|
959
|
+
return { kind: "age", age, displayValue: String(age) };
|
|
960
|
+
}
|
|
961
|
+
case "AttachmentFieldResponseValue": {
|
|
962
|
+
const attachmentUrl = value.attachmentUrl ?? "";
|
|
963
|
+
const name = value.name ?? null;
|
|
964
|
+
return {
|
|
965
|
+
kind: "attachment",
|
|
966
|
+
attachmentUrl,
|
|
967
|
+
mime: value.mime ?? null,
|
|
968
|
+
name,
|
|
969
|
+
size: value.size ?? null,
|
|
970
|
+
displayValue: name || attachmentUrl
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
case "BarcodeFieldResponseValue": {
|
|
974
|
+
const barcodeValue = value.barcodeValue ?? "";
|
|
975
|
+
return {
|
|
976
|
+
kind: "barcode",
|
|
977
|
+
barcodeType: value.barcodeType ?? null,
|
|
978
|
+
barcodeValue,
|
|
979
|
+
displayValue: barcodeValue
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
case "BooleanFieldResponseValue": {
|
|
983
|
+
const bool = Boolean(value.boolean);
|
|
984
|
+
return { kind: "boolean", boolean: bool, displayValue: yesNo(bool) };
|
|
985
|
+
}
|
|
986
|
+
case "CheckboxFieldResponseValue": {
|
|
987
|
+
const isChecked = Boolean(value.isChecked);
|
|
988
|
+
return { kind: "checkbox", isChecked, displayValue: yesNo(isChecked) };
|
|
989
|
+
}
|
|
990
|
+
case "CurrencyFieldResponseValue": {
|
|
991
|
+
const currency = value.currency ?? "";
|
|
992
|
+
return { kind: "currency", currency, displayValue: currency };
|
|
993
|
+
}
|
|
994
|
+
case "DateFieldResponseValue": {
|
|
995
|
+
const date = value.date ?? "";
|
|
996
|
+
return { kind: "date", date, displayValue: date };
|
|
997
|
+
}
|
|
998
|
+
case "DecimalFieldResponseValue": {
|
|
999
|
+
const decimal = value.decimal ?? null;
|
|
1000
|
+
return { kind: "decimal", decimal, displayValue: decimal ?? "" };
|
|
1001
|
+
}
|
|
1002
|
+
case "DurationFieldResponseValue": {
|
|
1003
|
+
const amount = value.duration?.amount ?? 0;
|
|
1004
|
+
const unit = value.duration?.unit ?? null;
|
|
1005
|
+
return { kind: "duration", amount, unit, displayValue: amountUnit(amount, unit) };
|
|
1006
|
+
}
|
|
1007
|
+
case "EmailFieldResponseValue": {
|
|
1008
|
+
const email = value.email ?? null;
|
|
1009
|
+
return { kind: "email", email, displayValue: email ?? "" };
|
|
1010
|
+
}
|
|
1011
|
+
case "GuestFieldResponseValue": {
|
|
1012
|
+
const name = value.name ?? null;
|
|
1013
|
+
return {
|
|
1014
|
+
kind: "guest",
|
|
1015
|
+
name,
|
|
1016
|
+
email: value.email ?? null,
|
|
1017
|
+
dateOfBirth: value.dateOfBirth ?? null,
|
|
1018
|
+
notes: value.notes ?? null,
|
|
1019
|
+
waiverSigned: value.waiverSigned ?? null,
|
|
1020
|
+
displayValue: name ?? ""
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
case "HtmlFieldResponseValue": {
|
|
1024
|
+
const html = value.html ?? "";
|
|
1025
|
+
return { kind: "html", html, displayValue: escapeHtml(html) };
|
|
1026
|
+
}
|
|
1027
|
+
case "ImageUrlFieldResponseValue": {
|
|
1028
|
+
const imageUrl = value.imageUrl ?? "";
|
|
1029
|
+
return { kind: "imageUrl", imageUrl, displayValue: imageUrl };
|
|
1030
|
+
}
|
|
1031
|
+
case "IntegerFieldResponseValue": {
|
|
1032
|
+
const integer = value.integer ?? 0;
|
|
1033
|
+
return { kind: "integer", integer, displayValue: String(integer) };
|
|
1034
|
+
}
|
|
1035
|
+
case "LocationFieldResponseValue": {
|
|
1036
|
+
const streetAddress = value.streetAddress ?? null;
|
|
1037
|
+
const locality = value.locality ?? null;
|
|
1038
|
+
const region = value.region ?? null;
|
|
1039
|
+
const postalCode = value.postalCode ?? null;
|
|
1040
|
+
const country = value.country ?? null;
|
|
1041
|
+
return {
|
|
1042
|
+
kind: "location",
|
|
1043
|
+
country,
|
|
1044
|
+
county: value.county ?? null,
|
|
1045
|
+
locality,
|
|
1046
|
+
postalCode,
|
|
1047
|
+
region,
|
|
1048
|
+
streetAddress,
|
|
1049
|
+
displayValue: [streetAddress, locality, region, postalCode, country].filter((part) => part).join(", ")
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
case "LongTextFieldResponseValue": {
|
|
1053
|
+
const longText = value.longText ?? "";
|
|
1054
|
+
return { kind: "longText", longText, displayValue: longText };
|
|
1055
|
+
}
|
|
1056
|
+
case "MetaFieldResponseValue": {
|
|
1057
|
+
const meta = value.meta ?? null;
|
|
1058
|
+
return { kind: "meta", meta, displayValue: meta == null ? "" : JSON.stringify(meta) };
|
|
1059
|
+
}
|
|
1060
|
+
case "PercentFieldResponseValue": {
|
|
1061
|
+
const percent = value.percent ?? null;
|
|
1062
|
+
return { kind: "percent", percent, displayValue: percent == null ? "" : `${percent}%` };
|
|
1063
|
+
}
|
|
1064
|
+
case "PhoneFieldResponseValue": {
|
|
1065
|
+
const phone = value.phone ?? "";
|
|
1066
|
+
return { kind: "phone", phone, displayValue: phone };
|
|
1067
|
+
}
|
|
1068
|
+
case "ShortTextFieldResponseValue": {
|
|
1069
|
+
const shortText = value.shortText ?? "";
|
|
1070
|
+
return { kind: "shortText", shortText, displayValue: shortText };
|
|
1071
|
+
}
|
|
1072
|
+
case "TimeFieldResponseValue": {
|
|
1073
|
+
const time = value.time ?? "";
|
|
1074
|
+
return { kind: "time", time, displayValue: time };
|
|
1075
|
+
}
|
|
1076
|
+
case "UrlFieldResponseValue": {
|
|
1077
|
+
const url = value.url ?? null;
|
|
1078
|
+
return { kind: "url", url, displayValue: url ?? "" };
|
|
1079
|
+
}
|
|
1080
|
+
case "VolumeFieldResponseValue": {
|
|
1081
|
+
const amount = value.volume?.amount ?? "0";
|
|
1082
|
+
const unit = value.volume?.unit ?? null;
|
|
1083
|
+
return { kind: "volume", amount, unit, displayValue: amountUnit(amount, unit) };
|
|
1084
|
+
}
|
|
1085
|
+
case "WeightFieldResponseValue": {
|
|
1086
|
+
const amount = value.weight?.amount ?? "0";
|
|
1087
|
+
const unit = value.weight?.unit ?? null;
|
|
1088
|
+
return { kind: "weight", amount, unit, displayValue: amountUnit(amount, unit) };
|
|
1089
|
+
}
|
|
1090
|
+
default:
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
function yesNo(value) {
|
|
1095
|
+
return value ? "Yes" : "No";
|
|
1096
|
+
}
|
|
1097
|
+
function amountUnit(amount, unit) {
|
|
1098
|
+
return unit ? `${amount} ${unit.toLowerCase()}` : String(amount);
|
|
1099
|
+
}
|
|
1100
|
+
function escapeHtml(value) {
|
|
1101
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1102
|
+
}
|
|
1103
|
+
function toSetMetaDataResult(node, bookingId) {
|
|
1104
|
+
return {
|
|
1105
|
+
success: node?.__typename === UPSERT_SUCCESS_TYPENAME,
|
|
1106
|
+
bookingId: node?.bookingId || bookingId,
|
|
1107
|
+
message: node?.message || ""
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function filterMetaDataByIntegrator(metaData, integrator) {
|
|
1111
|
+
const prefix = `integrator:${integrator}:`;
|
|
1112
|
+
return metaData.filter((entry) => entry.slug.startsWith(prefix)).map((entry) => ({ ...entry, slug: entry.slug.slice(prefix.length) }));
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// src/internal/peek/bookings/metadata-queries.ts
|
|
1116
|
+
var GUEST_VALUE_PII_FIELDS = "dateOfBirth\n email\n name\n ";
|
|
1117
|
+
function buildGuestValueFields(fullCustomerAccess) {
|
|
1118
|
+
return `... on GuestFieldResponseValue {
|
|
1119
|
+
${fullCustomerAccess ? GUEST_VALUE_PII_FIELDS : ""}notes
|
|
1120
|
+
waiverSigned
|
|
1121
|
+
}`;
|
|
1122
|
+
}
|
|
1123
|
+
function buildMetaDataValueSelection(fullCustomerAccess) {
|
|
1124
|
+
return `value {
|
|
1125
|
+
__typename
|
|
1126
|
+
... on AgeFieldResponseValue { age }
|
|
1127
|
+
... on AttachmentFieldResponseValue { attachmentUrl mime name size }
|
|
1128
|
+
... on BarcodeFieldResponseValue { barcodeType barcodeValue }
|
|
1129
|
+
... on BooleanFieldResponseValue { boolean }
|
|
1130
|
+
... on CheckboxFieldResponseValue { isChecked }
|
|
1131
|
+
... on CurrencyFieldResponseValue { currency }
|
|
1132
|
+
... on DateFieldResponseValue { date }
|
|
1133
|
+
... on DecimalFieldResponseValue { decimal }
|
|
1134
|
+
... on DurationFieldResponseValue { duration { amount unit } }
|
|
1135
|
+
... on EmailFieldResponseValue { email }
|
|
1136
|
+
${buildGuestValueFields(fullCustomerAccess)}
|
|
1137
|
+
... on HtmlFieldResponseValue { html }
|
|
1138
|
+
... on ImageUrlFieldResponseValue { imageUrl }
|
|
1139
|
+
... on IntegerFieldResponseValue { integer }
|
|
1140
|
+
... on LocationFieldResponseValue { country county locality postalCode region streetAddress }
|
|
1141
|
+
... on LongTextFieldResponseValue { longText }
|
|
1142
|
+
... on MetaFieldResponseValue { meta }
|
|
1143
|
+
... on PercentFieldResponseValue { percent }
|
|
1144
|
+
... on PhoneFieldResponseValue { phone }
|
|
1145
|
+
... on ShortTextFieldResponseValue { shortText }
|
|
1146
|
+
... on TimeFieldResponseValue { time }
|
|
1147
|
+
... on UrlFieldResponseValue { url }
|
|
1148
|
+
... on VolumeFieldResponseValue { volume { amount unit } }
|
|
1149
|
+
... on WeightFieldResponseValue { weight { amount unit } }
|
|
1150
|
+
}`;
|
|
1151
|
+
}
|
|
1152
|
+
function buildBookingMetaDataQuery(fullCustomerAccess) {
|
|
1153
|
+
return buildSalesQuery(`
|
|
1154
|
+
... on Booking {
|
|
1155
|
+
displayId
|
|
1156
|
+
id
|
|
1157
|
+
fieldResponses {
|
|
1158
|
+
fieldLocation {
|
|
1159
|
+
field { id name slug type }
|
|
1160
|
+
prompt { label hint isRequired }
|
|
1161
|
+
}
|
|
1162
|
+
refid
|
|
1163
|
+
${buildMetaDataValueSelection(fullCustomerAccess)}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
`);
|
|
1167
|
+
}
|
|
1168
|
+
var UPSERT_BOOKING_FIELD_RESPONSES_MUTATION = `
|
|
1169
|
+
mutation UpsertBookingFieldResponses($input: UpsertBookingFieldResponsesInput!) {
|
|
1170
|
+
upsertBookingFieldResponses(input: $input) {
|
|
1171
|
+
__typename
|
|
1172
|
+
... on UpsertBookingFieldResponsesSuccess { bookingId message }
|
|
1173
|
+
... on BookingNotFoundError { message bookingId }
|
|
1174
|
+
... on GenericError { message }
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
`;
|
|
1178
|
+
|
|
938
1179
|
// src/internal/peek/bookings/payments-on-file-converter.ts
|
|
939
1180
|
function fromPaymentsOnFileResponse(response, bookingId) {
|
|
940
1181
|
const firstEdge = (response?.sales?.edges ?? [])[0];
|
|
@@ -1194,10 +1435,41 @@ function assertRequiredAnswered(resolved, questions) {
|
|
|
1194
1435
|
}
|
|
1195
1436
|
}
|
|
1196
1437
|
|
|
1438
|
+
// src/internal/peek/bookings/list-price.ts
|
|
1439
|
+
var ERROR_LIST_PRICE_INVALID = "listPrice must be a positive number string, e.g. '100', '1,000', or '10.00'";
|
|
1440
|
+
function parseListPriceToCents(listPrice) {
|
|
1441
|
+
if (typeof listPrice !== "string") {
|
|
1442
|
+
throw new Error(ERROR_LIST_PRICE_INVALID);
|
|
1443
|
+
}
|
|
1444
|
+
const cleaned = listPrice.replace(/,/g, "").trim();
|
|
1445
|
+
if (!/^\d+(\.\d+)?$/.test(cleaned)) {
|
|
1446
|
+
throw new Error(ERROR_LIST_PRICE_INVALID);
|
|
1447
|
+
}
|
|
1448
|
+
const cents = Math.round(Number(cleaned) * 100);
|
|
1449
|
+
if (!Number.isFinite(cents) || cents <= 0) {
|
|
1450
|
+
throw new Error(ERROR_LIST_PRICE_INVALID);
|
|
1451
|
+
}
|
|
1452
|
+
return cents;
|
|
1453
|
+
}
|
|
1454
|
+
function centsToAmount(cents) {
|
|
1455
|
+
return (cents / 100).toFixed(2);
|
|
1456
|
+
}
|
|
1457
|
+
function distributeListPrice(listPrice, ticketCount) {
|
|
1458
|
+
const totalCents = parseListPriceToCents(listPrice);
|
|
1459
|
+
const perTicketCents = Math.floor(totalCents / ticketCount);
|
|
1460
|
+
const amounts = [];
|
|
1461
|
+
for (let i = 0; i < ticketCount - 1; i += 1) {
|
|
1462
|
+
amounts.push(centsToAmount(perTicketCents));
|
|
1463
|
+
}
|
|
1464
|
+
amounts.push(centsToAmount(totalCents - perTicketCents * (ticketCount - 1)));
|
|
1465
|
+
return amounts;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1197
1468
|
// src/internal/peek/bookings/booking-service.ts
|
|
1198
1469
|
var DEFAULT_PAGE_SIZE2 = 50;
|
|
1199
1470
|
var DEFAULT_CANCEL_NOTE = "Canceled";
|
|
1200
1471
|
var DEFAULT_CUSTOMER_MESSAGE = "Charge initiated via API";
|
|
1472
|
+
var DEFAULT_CURRENCY = "USD";
|
|
1201
1473
|
var BOOKING_DB_ID_REGEX = /^b_[a-z0-9]+$/;
|
|
1202
1474
|
var BOOKING_DISPLAY_ID_REGEX = /^B-[A-Z0-9]+$/;
|
|
1203
1475
|
var ORDER_DB_ID_REGEX = /^o_[a-z0-9]+$/;
|
|
@@ -1210,6 +1482,17 @@ var ERROR_ADDON_OPTION_ID_REQUIRED = "addonOptionId is required";
|
|
|
1210
1482
|
var ERROR_QUANTITY_INVALID = "quantity must be a positive integer string";
|
|
1211
1483
|
var ERROR_INVALID_BOOKING_ID = "bookingId is required and must be a valid booking id, e.g. 'b_abc123' or 'B-ABC123'";
|
|
1212
1484
|
var ERROR_INVALID_ORDER_ID = "orderId is required and must be a valid order id, e.g. 'o_abc123' or 'O-ABC123'";
|
|
1485
|
+
var ERROR_CUSTOM_STATUS_REQUIRED = "customStatus is required and must be a non-empty string";
|
|
1486
|
+
var ERROR_GUESTS_REQUIRED = "at least one guest is required";
|
|
1487
|
+
var ERROR_ATTACHMENTS_REQUIRED = "at least one attachment is required";
|
|
1488
|
+
var META_FIELD_GUEST = "guest";
|
|
1489
|
+
var META_FIELD_ATTACHMENT = "attachment";
|
|
1490
|
+
var META_FIELD_MANIFEST_URL = "manifest_url";
|
|
1491
|
+
var META_FIELD_RESERVATION_ID = "reservation_id";
|
|
1492
|
+
var META_FIELD_BOOKING_STATUS = "booking_status";
|
|
1493
|
+
var META_FIELD_ASSIGNED_PRODUCT = "assigned_product";
|
|
1494
|
+
var META_FIELD_ASSIGNED_EMPLOYEE = "assigned_employee";
|
|
1495
|
+
var META_FIELD_INSURANCE_PURCHASED = "insurance_purchased";
|
|
1213
1496
|
var ERROR_BOOKING_NOT_FOUND = "Booking not found";
|
|
1214
1497
|
var ERROR_MULTIPLE_BOOKINGS_FOUND = "Expected exactly one booking for the provided bookingId";
|
|
1215
1498
|
var ERROR_NO_ADDON_OPTION_TO_REMOVE = "No confirmed add-on option matching addonOptionId was found on the booking";
|
|
@@ -1222,12 +1505,15 @@ var BookingService = class {
|
|
|
1222
1505
|
this.deps = deps;
|
|
1223
1506
|
this.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE2;
|
|
1224
1507
|
this.fullCustomerAccess = resolveAccessOptions(options.accessOptions).fullCustomerAccess;
|
|
1508
|
+
this.integrator = options.integrator ?? "";
|
|
1225
1509
|
}
|
|
1226
1510
|
client;
|
|
1227
1511
|
deps;
|
|
1228
1512
|
pageSize;
|
|
1229
1513
|
/** Whether customer PII is requested and payment operations are allowed. */
|
|
1230
1514
|
fullCustomerAccess;
|
|
1515
|
+
/** Integrator id scoping `getMetaData` results. */
|
|
1516
|
+
integrator;
|
|
1231
1517
|
/**
|
|
1232
1518
|
* Throws a {@link PiiAccessDisabledError} for `operation` when this service was
|
|
1233
1519
|
* created without `fullCustomerAccess` — used to gate payment and booking-modification
|
|
@@ -1315,6 +1601,131 @@ var BookingService = class {
|
|
|
1315
1601
|
);
|
|
1316
1602
|
return fromBookingGuestsResponse(body.data);
|
|
1317
1603
|
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Returns a booking's custom-field metadata (its `fieldResponses`), scoped to
|
|
1606
|
+
* this service's integrator. Only fields whose slug starts with
|
|
1607
|
+
* `integrator:<integrator>:` are returned, and that prefix is stripped from
|
|
1608
|
+
* each returned `slug`. Returns `[]` when the booking is not found or has no
|
|
1609
|
+
* matching fields.
|
|
1610
|
+
*
|
|
1611
|
+
* @example
|
|
1612
|
+
* ```ts
|
|
1613
|
+
* const meta = await peek.getBookingService().getMetaData("b_abc123");
|
|
1614
|
+
* for (const field of meta) {
|
|
1615
|
+
* if (field.value?.kind === "url") console.log(field.slug, field.value.url);
|
|
1616
|
+
* }
|
|
1617
|
+
* ```
|
|
1618
|
+
*/
|
|
1619
|
+
async getMetaData(bookingId) {
|
|
1620
|
+
assertBookingId(bookingId);
|
|
1621
|
+
const body = await this.client.request(
|
|
1622
|
+
SALES_ENDPOINT,
|
|
1623
|
+
buildBookingMetaDataQuery(this.fullCustomerAccess),
|
|
1624
|
+
buildBookingsVariables({
|
|
1625
|
+
pageSize: this.pageSize,
|
|
1626
|
+
after: null,
|
|
1627
|
+
bookingId: normalizeBookingId(bookingId)
|
|
1628
|
+
})
|
|
1629
|
+
);
|
|
1630
|
+
const firstEdge = (body.data?.sales?.edges ?? [])[0];
|
|
1631
|
+
if (!firstEdge) {
|
|
1632
|
+
return [];
|
|
1633
|
+
}
|
|
1634
|
+
return filterMetaDataByIntegrator(fromBookingMetaDataNode(firstEdge.node), this.integrator);
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Sets the booking's `guest` metadata field. Each guest requires a `name` and
|
|
1638
|
+
* `email`; `dateOfBirth`/`waiverSigned`/`notes` are optional.
|
|
1639
|
+
*/
|
|
1640
|
+
async setMetaDataGuest(bookingId, guests) {
|
|
1641
|
+
if (!Array.isArray(guests) || guests.length === 0) {
|
|
1642
|
+
throw new Error(ERROR_GUESTS_REQUIRED);
|
|
1643
|
+
}
|
|
1644
|
+
const built = guests.map((guest) => {
|
|
1645
|
+
assertNonEmptyString(guest?.name, "guest name");
|
|
1646
|
+
assertNonEmptyString(guest?.email, "guest email");
|
|
1647
|
+
const out = { name: guest.name, email: guest.email };
|
|
1648
|
+
if (guest.dateOfBirth !== void 0) out.dateOfBirth = guest.dateOfBirth;
|
|
1649
|
+
if (guest.waiverSigned !== void 0) out.waiverSigned = guest.waiverSigned;
|
|
1650
|
+
if (guest.notes !== void 0) out.notes = guest.notes;
|
|
1651
|
+
return out;
|
|
1652
|
+
});
|
|
1653
|
+
return this.upsertFieldResponse(bookingId, { fieldName: META_FIELD_GUEST, guests: built });
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Sets the booking's `attachment` metadata field. Each attachment requires a
|
|
1657
|
+
* `name` and `attachmentUrl`.
|
|
1658
|
+
*/
|
|
1659
|
+
async setMetaDataAttachment(bookingId, attachments) {
|
|
1660
|
+
if (!Array.isArray(attachments) || attachments.length === 0) {
|
|
1661
|
+
throw new Error(ERROR_ATTACHMENTS_REQUIRED);
|
|
1662
|
+
}
|
|
1663
|
+
const built = attachments.map((attachment) => {
|
|
1664
|
+
assertNonEmptyString(attachment?.name, "attachment name");
|
|
1665
|
+
assertNonEmptyString(attachment?.attachmentUrl, "attachment attachmentUrl");
|
|
1666
|
+
return { name: attachment.name, attachmentUrl: attachment.attachmentUrl };
|
|
1667
|
+
});
|
|
1668
|
+
return this.upsertFieldResponse(bookingId, {
|
|
1669
|
+
fieldName: META_FIELD_ATTACHMENT,
|
|
1670
|
+
attachments: built
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
/** Sets the booking's `manifest_url` metadata field. */
|
|
1674
|
+
async setMetaDataManifestUrl(bookingId, url) {
|
|
1675
|
+
assertNonEmptyString(url, "url");
|
|
1676
|
+
return this.upsertFieldResponse(bookingId, { fieldName: META_FIELD_MANIFEST_URL, url });
|
|
1677
|
+
}
|
|
1678
|
+
/** Sets the booking's `reservation_id` metadata field. */
|
|
1679
|
+
async setMetaDataReservationId(bookingId, reservationId) {
|
|
1680
|
+
return this.setShortTextField(bookingId, META_FIELD_RESERVATION_ID, reservationId, "reservationId");
|
|
1681
|
+
}
|
|
1682
|
+
/** Sets the booking's `booking_status` metadata field. */
|
|
1683
|
+
async setMetaDataBookingStatus(bookingId, status) {
|
|
1684
|
+
return this.setShortTextField(bookingId, META_FIELD_BOOKING_STATUS, status, "status");
|
|
1685
|
+
}
|
|
1686
|
+
/** Sets the booking's `assigned_product` metadata field. */
|
|
1687
|
+
async setMetaDataAssignedProduct(bookingId, productName) {
|
|
1688
|
+
return this.setShortTextField(bookingId, META_FIELD_ASSIGNED_PRODUCT, productName, "productName");
|
|
1689
|
+
}
|
|
1690
|
+
/** Sets the booking's `assigned_employee` metadata field. */
|
|
1691
|
+
async setMetaDataAssignedEmployee(bookingId, employeeName) {
|
|
1692
|
+
return this.setShortTextField(bookingId, META_FIELD_ASSIGNED_EMPLOYEE, employeeName, "employeeName");
|
|
1693
|
+
}
|
|
1694
|
+
/** Sets the booking's `insurance_purchased` metadata field. */
|
|
1695
|
+
async setMetaDataInsurancePurchased(bookingId, insurancePurchased) {
|
|
1696
|
+
if (typeof insurancePurchased !== "boolean") {
|
|
1697
|
+
throw new Error("insurancePurchased is required and must be a boolean");
|
|
1698
|
+
}
|
|
1699
|
+
return this.upsertFieldResponse(bookingId, {
|
|
1700
|
+
fieldName: META_FIELD_INSURANCE_PURCHASED,
|
|
1701
|
+
boolean: insurancePurchased
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
/** Validates a non-empty `shortText` value and upserts it under `fieldName`. */
|
|
1705
|
+
setShortTextField(bookingId, fieldName, value, argName) {
|
|
1706
|
+
assertNonEmptyString(value, argName);
|
|
1707
|
+
return this.upsertFieldResponse(bookingId, { fieldName, shortText: value });
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Validates the booking id and upserts a single field response (scoped to this
|
|
1711
|
+
* service's integrator), mapping the result union into a {@link SetMetaDataResult}.
|
|
1712
|
+
*/
|
|
1713
|
+
async upsertFieldResponse(bookingId, fieldResponse) {
|
|
1714
|
+
assertBookingId(bookingId);
|
|
1715
|
+
const normalized = normalizeBookingId(bookingId);
|
|
1716
|
+
const body = await this.client.request(
|
|
1717
|
+
SALES_ENDPOINT,
|
|
1718
|
+
UPSERT_BOOKING_FIELD_RESPONSES_MUTATION,
|
|
1719
|
+
{
|
|
1720
|
+
input: {
|
|
1721
|
+
bookingId: normalized,
|
|
1722
|
+
integrator: this.integrator,
|
|
1723
|
+
fieldResponses: [fieldResponse]
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
);
|
|
1727
|
+
return toSetMetaDataResult(body.data?.upsertBookingFieldResponses, normalized);
|
|
1728
|
+
}
|
|
1318
1729
|
/** Returns the payments on file for a booking, or null when not found. */
|
|
1319
1730
|
async getPaymentsOnFile(bookingId) {
|
|
1320
1731
|
this.assertPiiEnabled("getPaymentsOnFile");
|
|
@@ -1359,6 +1770,30 @@ ${note}`;
|
|
|
1359
1770
|
});
|
|
1360
1771
|
return this.getById(normalized);
|
|
1361
1772
|
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Sets a booking's operator-facing custom status. Returns `true` when the
|
|
1775
|
+
* gateway confirms the stored `operatorStatus` equals the requested
|
|
1776
|
+
* `customStatus`, `false` otherwise.
|
|
1777
|
+
*
|
|
1778
|
+
* @example
|
|
1779
|
+
* ```ts
|
|
1780
|
+
* const ok = await peek.getBookingService().updateCustomStatus("b_abc123", "Awaiting deposit");
|
|
1781
|
+
* ```
|
|
1782
|
+
*
|
|
1783
|
+
* @throws {Error} when `bookingId` is not a valid booking id (`b_…`/`B-…`) or
|
|
1784
|
+
* when `customStatus` is not a non-empty string.
|
|
1785
|
+
*/
|
|
1786
|
+
async updateCustomStatus(bookingId, customStatus) {
|
|
1787
|
+
assertBookingId(bookingId);
|
|
1788
|
+
assertCustomStatus(customStatus);
|
|
1789
|
+
const normalized = normalizeBookingId(bookingId);
|
|
1790
|
+
const body = await this.client.request(
|
|
1791
|
+
SALES_ENDPOINT,
|
|
1792
|
+
UPDATE_OPERATOR_STATUS_MUTATION,
|
|
1793
|
+
{ input: { id: normalized, operatorStatus: customStatus } }
|
|
1794
|
+
);
|
|
1795
|
+
return body.data?.updateOperatorStatusForBooking?.booking?.operatorStatus === customStatus;
|
|
1796
|
+
}
|
|
1362
1797
|
/** Cancels a booking and returns its id/displayId/status. */
|
|
1363
1798
|
async cancel(bookingId, notes = DEFAULT_CANCEL_NOTE) {
|
|
1364
1799
|
assertBookingId(bookingId);
|
|
@@ -1714,13 +2149,26 @@ ${note}`;
|
|
|
1714
2149
|
*/
|
|
1715
2150
|
async create(input) {
|
|
1716
2151
|
validateCreateInput(input);
|
|
1717
|
-
const
|
|
2152
|
+
const ticketCount = input.tickets.reduce((count, ticket) => count + ticket.quantity, 0);
|
|
2153
|
+
const listPriceAmounts = input.listPrice === void 0 ? null : distributeListPrice(input.listPrice, ticketCount);
|
|
2154
|
+
const [questionAnswers, currency] = await Promise.all([
|
|
2155
|
+
this.buildQuestionAnswers(input),
|
|
2156
|
+
listPriceAmounts ? this.resolveActivityCurrency(input.activityId) : Promise.resolve("")
|
|
2157
|
+
]);
|
|
2158
|
+
let seatIndex = 0;
|
|
1718
2159
|
const tickets = input.tickets.flatMap(
|
|
1719
|
-
(ticket) => Array.from({ length: ticket.quantity }, () =>
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
2160
|
+
(ticket) => Array.from({ length: ticket.quantity }, () => {
|
|
2161
|
+
const seat = {
|
|
2162
|
+
resourceOptionId: ticket.resourceOptionId,
|
|
2163
|
+
reservationStatus: "CONFIRMED",
|
|
2164
|
+
refid: randomUUID()
|
|
2165
|
+
};
|
|
2166
|
+
if (listPriceAmounts) {
|
|
2167
|
+
seat.price = { amount: listPriceAmounts[seatIndex], currency };
|
|
2168
|
+
}
|
|
2169
|
+
seatIndex += 1;
|
|
2170
|
+
return seat;
|
|
2171
|
+
})
|
|
1724
2172
|
);
|
|
1725
2173
|
const bookingQuote = {
|
|
1726
2174
|
tickets,
|
|
@@ -1835,6 +2283,15 @@ ${note}`;
|
|
|
1835
2283
|
}
|
|
1836
2284
|
return result.transactionId;
|
|
1837
2285
|
}
|
|
2286
|
+
/**
|
|
2287
|
+
* Resolves an activity's ISO currency via the product service, falling back to
|
|
2288
|
+
* {@link DEFAULT_CURRENCY} when the activity (or its currency) can't be found.
|
|
2289
|
+
*/
|
|
2290
|
+
async resolveActivityCurrency(activityId) {
|
|
2291
|
+
const products = await this.deps.productService.getAllProducts();
|
|
2292
|
+
const match = products.find((product) => product.productId === activityId);
|
|
2293
|
+
return match?.currency || DEFAULT_CURRENCY;
|
|
2294
|
+
}
|
|
1838
2295
|
/** Finds the parent item id of an add-on by matching its option id. */
|
|
1839
2296
|
async resolveParentItemId(addonOptionId) {
|
|
1840
2297
|
const products = await this.deps.productService.getAllProducts();
|
|
@@ -1909,6 +2366,16 @@ function assertBookingId(bookingId) {
|
|
|
1909
2366
|
throw new Error(ERROR_INVALID_BOOKING_ID);
|
|
1910
2367
|
}
|
|
1911
2368
|
}
|
|
2369
|
+
function assertCustomStatus(customStatus) {
|
|
2370
|
+
if (typeof customStatus !== "string" || customStatus.trim().length === 0) {
|
|
2371
|
+
throw new Error(ERROR_CUSTOM_STATUS_REQUIRED);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
function assertNonEmptyString(value, field) {
|
|
2375
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
2376
|
+
throw new Error(`${field} is required and must be a non-empty string`);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
1912
2379
|
function assertOrderId(orderId) {
|
|
1913
2380
|
if (!(ORDER_DB_ID_REGEX.test(orderId) || ORDER_DISPLAY_ID_REGEX.test(orderId))) {
|
|
1914
2381
|
throw new Error(ERROR_INVALID_ORDER_ID);
|
|
@@ -2175,7 +2642,7 @@ async function requestWithRetry(http, url, init, label, onResponse) {
|
|
|
2175
2642
|
// src/internal/sdk-headers.ts
|
|
2176
2643
|
var SDK_HEADER_NAME = "x-peek-sdk";
|
|
2177
2644
|
var SDK_PLATFORM = "js";
|
|
2178
|
-
var SDK_VERSION = "0.9.
|
|
2645
|
+
var SDK_VERSION = "0.9.3";
|
|
2179
2646
|
var SDK_HEADER_VALUE = `${SDK_PLATFORM}-${SDK_VERSION}`;
|
|
2180
2647
|
|
|
2181
2648
|
// src/internal/peek/graphql-client.ts
|
|
@@ -3461,7 +3928,7 @@ var CREATE_PROMO_CODE_MUTATION = `
|
|
|
3461
3928
|
|
|
3462
3929
|
// src/internal/peek/promo-codes/promo-code-service.ts
|
|
3463
3930
|
var DEFAULT_PAGE_SIZE3 = 50;
|
|
3464
|
-
var
|
|
3931
|
+
var DEFAULT_CURRENCY2 = "USD";
|
|
3465
3932
|
var CURRENCY_PATTERN = /^[A-Z]{3}$/;
|
|
3466
3933
|
var PromoCodeService = class {
|
|
3467
3934
|
constructor(client, options = {}) {
|
|
@@ -3507,7 +3974,7 @@ var PromoCodeService = class {
|
|
|
3507
3974
|
if (Number.isNaN(parseFloat(input.amount))) {
|
|
3508
3975
|
throw new Error("amount must be a valid number");
|
|
3509
3976
|
}
|
|
3510
|
-
const currency = input.currency ||
|
|
3977
|
+
const currency = input.currency || DEFAULT_CURRENCY2;
|
|
3511
3978
|
if (!CURRENCY_PATTERN.test(currency)) {
|
|
3512
3979
|
throw new Error("currency must be 3 uppercase letters");
|
|
3513
3980
|
}
|
|
@@ -3881,6 +4348,7 @@ var PeekAccessService = class {
|
|
|
3881
4348
|
productServiceOptions;
|
|
3882
4349
|
accessOptions;
|
|
3883
4350
|
jwtSecret;
|
|
4351
|
+
issuer;
|
|
3884
4352
|
productService;
|
|
3885
4353
|
accountUserService;
|
|
3886
4354
|
resourcePoolService;
|
|
@@ -3891,7 +4359,7 @@ var PeekAccessService = class {
|
|
|
3891
4359
|
dailyNoteService;
|
|
3892
4360
|
availabilityService;
|
|
3893
4361
|
membershipService;
|
|
3894
|
-
|
|
4362
|
+
bookingServices = /* @__PURE__ */ new Map();
|
|
3895
4363
|
reviewService;
|
|
3896
4364
|
constructor(config) {
|
|
3897
4365
|
const hasApiUrl = !!config.apiUrl;
|
|
@@ -3902,6 +4370,7 @@ var PeekAccessService = class {
|
|
|
3902
4370
|
if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "PeekAccessService");
|
|
3903
4371
|
if (!hasApiUrl && !isV2) requireNonEmpty(config.gatewayKey ?? "", "gatewayKey", "PeekAccessService");
|
|
3904
4372
|
this.jwtSecret = config.jwtSecret;
|
|
4373
|
+
this.issuer = config.issuer;
|
|
3905
4374
|
const logger = config.logger ?? noopLogger;
|
|
3906
4375
|
const tokens = createTokenManager(config);
|
|
3907
4376
|
const useRegistrySlug = hasApiUrl || isV2;
|
|
@@ -4062,17 +4531,25 @@ var PeekAccessService = class {
|
|
|
4062
4531
|
}
|
|
4063
4532
|
/**
|
|
4064
4533
|
* Returns the {@link BookingService} for this install, bound to the shared
|
|
4065
|
-
* authenticated transport.
|
|
4534
|
+
* authenticated transport. Instances are created lazily and reused per
|
|
4535
|
+
* `integrator`.
|
|
4536
|
+
*
|
|
4537
|
+
* `integrator` scopes `getMetaData` results to a custom integrator id. **Do
|
|
4538
|
+
* not set it** unless Peek engineering created a custom integrator id for this
|
|
4539
|
+
* integration — omit it and the correct default is used.
|
|
4066
4540
|
*/
|
|
4067
|
-
getBookingService() {
|
|
4068
|
-
|
|
4069
|
-
|
|
4541
|
+
getBookingService(integrator) {
|
|
4542
|
+
const resolved = integrator ?? this.issuer;
|
|
4543
|
+
let service = this.bookingServices.get(resolved);
|
|
4544
|
+
if (!service) {
|
|
4545
|
+
service = new BookingService(
|
|
4070
4546
|
this.client,
|
|
4071
4547
|
{ productService: this.getProductService() },
|
|
4072
|
-
{ accessOptions: this.accessOptions }
|
|
4548
|
+
{ accessOptions: this.accessOptions, integrator: resolved }
|
|
4073
4549
|
);
|
|
4550
|
+
this.bookingServices.set(resolved, service);
|
|
4074
4551
|
}
|
|
4075
|
-
return
|
|
4552
|
+
return service;
|
|
4076
4553
|
}
|
|
4077
4554
|
/**
|
|
4078
4555
|
* Returns the {@link ReviewService} for this install, bound to the shared
|