@pisell/pisellos 2.3.73 → 2.3.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.js +111 -71
- package/dist/model/strategy/adapter/promotion/index.js +9 -0
- package/dist/model/strategy/adapter/walletPass/example.js +3 -26
- package/dist/model/strategy/strategy-example.js +5 -19
- package/dist/modules/Cart/index.js +0 -3
- package/dist/modules/Cart/utils/cartProduct.js +0 -1
- package/dist/modules/Date/index.js +1 -5
- package/dist/modules/Discount/index.js +2 -4
- package/dist/modules/OpenData/index.js +0 -2
- package/dist/modules/Order/index.d.ts +1 -1
- package/dist/modules/Order/index.js +36 -64
- package/dist/modules/Payment/index.js +97 -162
- package/dist/modules/Quotation/index.d.ts +6 -0
- package/dist/modules/Quotation/index.js +75 -19
- package/dist/modules/Rules/index.js +1 -4
- package/dist/modules/Schedule/index.js +0 -1
- package/dist/modules/Schedule/utils.js +0 -1
- package/dist/modules/Step/index.js +0 -1
- package/dist/plugins/request.d.ts +1 -0
- package/dist/plugins/request.js +1 -4
- package/dist/plugins/window.js +2 -6
- package/dist/server/index.d.ts +8 -2
- package/dist/server/index.js +303 -269
- package/dist/server/modules/floor-plan/index.d.ts +4 -0
- package/dist/server/modules/floor-plan/index.js +240 -98
- package/dist/server/modules/menu/index.js +75 -59
- package/dist/server/modules/order/index.d.ts +17 -7
- package/dist/server/modules/order/index.js +421 -211
- package/dist/server/modules/products/index.d.ts +8 -2
- package/dist/server/modules/products/index.js +191 -117
- package/dist/server/modules/quotation/index.js +29 -38
- package/dist/server/modules/resource/index.js +24 -17
- package/dist/server/modules/schedule/index.js +27 -35
- package/dist/server/utils/product.js +0 -1
- package/dist/solution/BaseSales/index.js +38 -41
- package/dist/solution/BookingByStep/index.d.ts +1 -1
- package/dist/solution/BookingByStep/index.js +7 -59
- package/dist/solution/BookingByStep/utils/capacity.js +1 -11
- package/dist/solution/BookingByStep/utils/resources.js +1 -3
- package/dist/solution/BookingByStep/utils/timeslots.js +2 -43
- package/dist/solution/BookingTicket/index.js +0 -2
- package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +6 -7
- package/dist/solution/BookingTicket/utils/scan/handleScan.js +25 -31
- package/dist/solution/BookingTicket/utils/scan/index.js +6 -10
- package/dist/solution/BuyTickets/index.js +9 -12
- package/dist/solution/Checkout/index.js +177 -252
- package/dist/solution/Checkout/utils/index.js +4 -16
- package/dist/solution/RegisterAndLogin/index.js +30 -76
- package/dist/solution/ScanOrder/index.js +50 -60
- package/dist/solution/ShopDiscount/index.js +2 -7
- package/dist/solution/VenueBooking/index.js +45 -55
- package/dist/utils/task.js +15 -18
- package/dist/utils/watch.js +0 -1
- package/lib/apis/picoding.js +2 -0
- package/lib/core/index.d.ts +7 -0
- package/lib/core/index.js +149 -131
- package/lib/effects/index.js +46 -57
- package/lib/index.js +82 -49
- package/lib/model/index.js +14 -21
- package/lib/model/strategy/adapter/dataVariant/adapter.js +38 -50
- package/lib/model/strategy/adapter/dataVariant/evaluator.js +162 -236
- package/lib/model/strategy/adapter/dataVariant/examples.js +229 -237
- package/lib/model/strategy/adapter/dataVariant/index.js +39 -34
- package/lib/model/strategy/adapter/dataVariant/type.js +57 -28
- package/lib/model/strategy/adapter/index.js +100 -64
- package/lib/model/strategy/adapter/itemRule/adapter.js +156 -144
- package/lib/model/strategy/adapter/itemRule/evaluator.js +36 -48
- package/lib/model/strategy/adapter/itemRule/examples.js +295 -225
- package/lib/model/strategy/adapter/itemRule/index.js +83 -57
- package/lib/model/strategy/adapter/itemRule/type.js +97 -36
- package/lib/model/strategy/adapter/promotion/adapter.js +91 -69
- package/lib/model/strategy/adapter/promotion/evaluator.js +444 -310
- package/lib/model/strategy/adapter/promotion/examples.js +222 -234
- package/lib/model/strategy/adapter/promotion/index.js +1 -0
- package/lib/model/strategy/adapter/promotion/type.js +243 -36
- package/lib/model/strategy/adapter/type.js +4 -16
- package/lib/model/strategy/adapter/walletPass/evaluator.js +180 -150
- package/lib/model/strategy/adapter/walletPass/example.js +217 -190
- package/lib/model/strategy/adapter/walletPass/index.js +75 -51
- package/lib/model/strategy/adapter/walletPass/locales.js +36 -58
- package/lib/model/strategy/adapter/walletPass/type.js +4 -16
- package/lib/model/strategy/adapter/walletPass/utils.js +588 -378
- package/lib/model/strategy/engine.js +270 -168
- package/lib/model/strategy/index.js +49 -34
- package/lib/model/strategy/strategy-example.js +243 -239
- package/lib/model/strategy/type.js +100 -40
- package/lib/modules/Account/index.js +39 -53
- package/lib/modules/Account/types.js +20 -33
- package/lib/modules/AccountList/index.js +116 -154
- package/lib/modules/AccountList/types.js +30 -31
- package/lib/modules/AccountList/utils.js +18 -30
- package/lib/modules/BaseModule.js +23 -42
- package/lib/modules/BookingContext/index.js +158 -136
- package/lib/modules/BookingContext/types.js +46 -32
- package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +138 -146
- package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +78 -83
- package/lib/modules/BookingContext/utils/cacheItemToBookingInput.js +170 -150
- package/lib/modules/BookingContext/utils/flexible.js +38 -55
- package/lib/modules/BookingContext/utils/formatResourceList.js +19 -34
- package/lib/modules/BookingContext/utils/index.js +124 -41
- package/lib/modules/BookingContext/utils/noResource.js +58 -70
- package/lib/modules/BookingContext/utils/orderLineDisplay.js +14 -41
- package/lib/modules/BookingContext/utils/productExtend.js +202 -155
- package/lib/modules/BookingContext/utils/resourceErrors.js +53 -53
- package/lib/modules/BookingContext/utils/resourceSelection.js +19 -41
- package/lib/modules/BookingContext/utils/resources.js +209 -167
- package/lib/modules/BookingContext/utils/serviceTimes.js +115 -116
- package/lib/modules/BookingContext/utils/timeSlices.js +76 -86
- package/lib/modules/Cart/index.js +170 -124
- package/lib/modules/Cart/types.js +46 -51
- package/lib/modules/Cart/utils/cartAccount.js +40 -39
- package/lib/modules/Cart/utils/cartDate.js +56 -61
- package/lib/modules/Cart/utils/cartDiscount.js +28 -33
- package/lib/modules/Cart/utils/cartNote.js +27 -32
- package/lib/modules/Cart/utils/cartProduct.js +321 -251
- package/lib/modules/Cart/utils/cartRelationForms.js +29 -35
- package/lib/modules/Cart/utils/cartResource.js +70 -78
- package/lib/modules/Cart/utils/changePrice.js +22 -50
- package/lib/modules/Cart/utils/index.js +106 -48
- package/lib/modules/Customer/constants.js +13 -34
- package/lib/modules/Customer/index.js +235 -172
- package/lib/modules/Customer/types.js +38 -35
- package/lib/modules/Date/index.js +116 -115
- package/lib/modules/Date/types.js +16 -28
- package/lib/modules/Date/utils.js +174 -123
- package/lib/modules/Discount/index.js +122 -127
- package/lib/modules/Discount/types.js +9 -31
- package/lib/modules/Guests/index.js +30 -56
- package/lib/modules/Guests/types.js +23 -33
- package/lib/modules/Holder/index.js +209 -189
- package/lib/modules/Holder/types.js +4 -16
- package/lib/modules/Holder/utils.js +20 -49
- package/lib/modules/OpenData/index.js +70 -76
- package/lib/modules/OpenData/types.js +4 -16
- package/lib/modules/OpenData/utils.js +44 -78
- package/lib/modules/Order/index.d.ts +1 -1
- package/lib/modules/Order/index.js +1713 -2118
- package/lib/modules/Order/payment-utils.js +77 -120
- package/lib/modules/Order/types.js +88 -35
- package/lib/modules/Order/utils/bundleDiscountHydration.js +41 -74
- package/lib/modules/Order/utils/discountProductLineIdentity.js +142 -100
- package/lib/modules/Order/utils/manualProductDiscount.js +124 -123
- package/lib/modules/Order/utils/orderCollectionIdentity.js +184 -244
- package/lib/modules/Order/utils.js +814 -647
- package/lib/modules/Payment/cash.js +20 -33
- package/lib/modules/Payment/cashRecommendationAlgorithm.js +145 -76
- package/lib/modules/Payment/eftpos.js +16 -30
- package/lib/modules/Payment/index.js +532 -399
- package/lib/modules/Payment/mx51.js +1 -0
- package/lib/modules/Payment/types.js +230 -108
- package/lib/modules/Payment/utils.js +52 -51
- package/lib/modules/Payment/walletpass.js +485 -660
- package/lib/modules/Product/index.js +51 -46
- package/lib/modules/Product/types.js +4 -16
- package/lib/modules/Product/utils.js +32 -33
- package/lib/modules/ProductList/index.js +113 -123
- package/lib/modules/ProductList/types.js +9 -31
- package/lib/modules/Quotation/index.d.ts +6 -0
- package/lib/modules/Quotation/index.js +110 -103
- package/lib/modules/Quotation/types.js +4 -16
- package/lib/modules/Resource/index.js +27 -59
- package/lib/modules/Resource/types.js +22 -32
- package/lib/modules/Resource/utils.js +30 -38
- package/lib/modules/ResourcePlanner/index.js +25 -23
- package/lib/modules/ResourcePlanner/localClock.js +54 -119
- package/lib/modules/ResourcePlanner/planner.js +589 -770
- package/lib/modules/ResourcePlanner/types.js +10 -29
- package/lib/modules/Rules/index.js +1218 -959
- package/lib/modules/Rules/types.js +17 -40
- package/lib/modules/SalesSummary/index.js +85 -90
- package/lib/modules/SalesSummary/types.js +4 -16
- package/lib/modules/SalesSummary/utils.js +355 -430
- package/lib/modules/ScanOrderLogger/index.js +59 -79
- package/lib/modules/ScanOrderLogger/providers/feishu.js +54 -78
- package/lib/modules/ScanOrderLogger/providers/grafana.js +13 -36
- package/lib/modules/ScanOrderLogger/types.js +4 -16
- package/lib/modules/Schedule/getDateIsInSchedule.js +334 -163
- package/lib/modules/Schedule/index.js +100 -114
- package/lib/modules/Schedule/type.js +4 -16
- package/lib/modules/Schedule/types.js +4 -16
- package/lib/modules/Schedule/utils.js +433 -291
- package/lib/modules/Step/index.js +40 -52
- package/lib/modules/Step/tyeps.js +4 -16
- package/lib/modules/Summary/index.js +66 -71
- package/lib/modules/Summary/types.js +4 -16
- package/lib/modules/Summary/utils.js +773 -418
- package/lib/modules/SurchargeList/index.js +46 -60
- package/lib/modules/SurchargeList/types.js +4 -16
- package/lib/modules/index.js +245 -63
- package/lib/plugins/app.js +4 -16
- package/lib/plugins/index.js +36 -25
- package/lib/plugins/request.d.ts +1 -0
- package/lib/plugins/request.js +137 -126
- package/lib/plugins/shopStore.js +4 -16
- package/lib/plugins/user.js +4 -16
- package/lib/plugins/window.js +171 -179
- package/lib/server/index.d.ts +8 -2
- package/lib/server/index.js +3083 -2663
- package/lib/server/modules/floor-plan/index.d.ts +4 -0
- package/lib/server/modules/floor-plan/index.js +159 -127
- package/lib/server/modules/floor-plan/types.js +15 -30
- package/lib/server/modules/index.js +106 -68
- package/lib/server/modules/menu/index.js +170 -124
- package/lib/server/modules/menu/types.js +18 -31
- package/lib/server/modules/order/index.d.ts +17 -7
- package/lib/server/modules/order/index.js +916 -977
- package/lib/server/modules/order/types.js +122 -33
- package/lib/server/modules/order/utils/filterBookings.js +219 -194
- package/lib/server/modules/order/utils/filterOrders.js +118 -92
- package/lib/server/modules/payment/index.js +59 -83
- package/lib/server/modules/payment/types.js +4 -16
- package/lib/server/modules/products/index.d.ts +8 -2
- package/lib/server/modules/products/index.js +656 -471
- package/lib/server/modules/products/types.js +44 -34
- package/lib/server/modules/quotation/index.js +137 -172
- package/lib/server/modules/quotation/types.js +21 -31
- package/lib/server/modules/resource/index.js +225 -186
- package/lib/server/modules/resource/types.js +42 -33
- package/lib/server/modules/schedule/index.js +135 -123
- package/lib/server/modules/schedule/types.js +14 -21
- package/lib/server/modules/schedule/utils.js +334 -163
- package/lib/server/types.js +4 -16
- package/lib/server/utils/index.js +25 -23
- package/lib/server/utils/product.js +196 -139
- package/lib/server/utils/schedule.js +34 -41
- package/lib/server/utils/small-ticket.js +479 -456
- package/lib/server/utils/time.js +40 -49
- package/lib/solution/BaseSales/index.js +1186 -1410
- package/lib/solution/BaseSales/types.js +42 -38
- package/lib/solution/BaseSales/utils/cartPromotion.js +642 -498
- package/lib/solution/BaseSales/utils/parseSalesResponse.js +85 -105
- package/lib/solution/BaseSales/utils/quotationPrice.js +56 -114
- package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +108 -145
- package/lib/solution/BaseSales/utils.js +158 -143
- package/lib/solution/BookingByStep/index.d.ts +1 -1
- package/lib/solution/BookingByStep/index.js +1527 -1340
- package/lib/solution/BookingByStep/types.js +40 -99
- package/lib/solution/BookingByStep/utils/capacity.js +192 -160
- package/lib/solution/BookingByStep/utils/products.js +42 -52
- package/lib/solution/BookingByStep/utils/resources.js +527 -330
- package/lib/solution/BookingByStep/utils/stock.js +65 -44
- package/lib/solution/BookingByStep/utils/timeslots.js +129 -131
- package/lib/solution/BookingTicket/index.js +607 -522
- package/lib/solution/BookingTicket/types.js +99 -77
- package/lib/solution/BookingTicket/utils/addProductDecision.js +226 -189
- package/lib/solution/BookingTicket/utils/addTimeAvailability.js +58 -89
- package/lib/solution/BookingTicket/utils/bookingStatus.js +107 -78
- package/lib/solution/BookingTicket/utils/cartView.js +114 -99
- package/lib/solution/BookingTicket/utils/exampleData.js +185 -0
- package/lib/solution/BookingTicket/utils/orderCustomer.js +18 -41
- package/lib/solution/BookingTicket/utils/resolveBestAddTimePlan.js +219 -86
- package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +168 -116
- package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +79 -91
- package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +54 -41
- package/lib/solution/BookingTicket/utils/scan/handleScan.js +84 -81
- package/lib/solution/BookingTicket/utils/scan/index.js +98 -104
- package/lib/solution/BookingTicket/utils/scan/scanCache.js +90 -43
- package/lib/solution/BookingTicket/utils/scan/scanProductValues.js +52 -64
- package/lib/solution/BookingTicket/utils/sessionCatalogStale.js +56 -58
- package/lib/solution/BookingTicket/utils/weighingProductSku.js +34 -48
- package/lib/solution/BuyTickets/index.js +67 -70
- package/lib/solution/BuyTickets/types.js +22 -38
- package/lib/solution/Checkout/index.js +1451 -1158
- package/lib/solution/Checkout/types.js +91 -61
- package/lib/solution/Checkout/utils/index.js +228 -156
- package/lib/solution/PlaceOrder/index.js +55 -0
- package/lib/solution/RegisterAndLogin/config.js +493 -460
- package/lib/solution/RegisterAndLogin/index.js +633 -630
- package/lib/solution/RegisterAndLogin/types.js +189 -76
- package/lib/solution/RegisterAndLogin/utils.js +183 -125
- package/lib/solution/Sales/index.js +328 -324
- package/lib/solution/Sales/types.js +27 -33
- package/lib/solution/ScanOrder/index.js +832 -864
- package/lib/solution/ScanOrder/types.js +33 -34
- package/lib/solution/ScanOrder/utils.js +409 -374
- package/lib/solution/ShopDiscount/index.js +226 -232
- package/lib/solution/ShopDiscount/types.js +15 -37
- package/lib/solution/ShopDiscount/utils.js +300 -172
- package/lib/solution/UnifiedBookingSales/index.js +1035 -1201
- package/lib/solution/UnifiedBookingSales/remoteOccupancyCache.js +99 -159
- package/lib/solution/UnifiedBookingSales/types.js +10 -32
- package/lib/solution/VenueBooking/index.js +811 -879
- package/lib/solution/VenueBooking/types.js +19 -39
- package/lib/solution/VenueBooking/utils/dateSummary.js +27 -56
- package/lib/solution/VenueBooking/utils/resource.js +31 -54
- package/lib/solution/VenueBooking/utils/slotMerge.js +94 -107
- package/lib/solution/VenueBooking/utils/timeSlot.js +132 -132
- package/lib/solution/VenueBooking/utils.js +130 -67
- package/lib/solution/index.js +124 -41
- package/lib/store/createStore.js +32 -29
- package/lib/types/index.js +4 -16
- package/lib/utils/payment-number.js +26 -46
- package/lib/utils/task.js +36 -36
- package/lib/utils/watch.js +81 -47
- package/package.json +2 -1
|
@@ -1,69 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
-
var __export = (target, all) => {
|
|
6
|
-
for (var name in all)
|
|
7
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
-
};
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
-
for (let key of __getOwnPropNames(from))
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
-
}
|
|
15
|
-
return to;
|
|
16
|
-
};
|
|
17
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
1
|
+
"use strict";
|
|
18
2
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
__export(planner_exports, {
|
|
22
|
-
createAvailabilityProjection: () => createAvailabilityProjection,
|
|
23
|
-
getProductTimeRanges: () => getProductTimeRanges,
|
|
24
|
-
queryAvailabilityProjection: () => queryAvailabilityProjection,
|
|
25
|
-
validateAvailabilitySelection: () => validateAvailabilitySelection
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
26
5
|
});
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
6
|
+
exports.createAvailabilityProjection = createAvailabilityProjection;
|
|
7
|
+
exports.getProductTimeRanges = getProductTimeRanges;
|
|
8
|
+
exports.queryAvailabilityProjection = queryAvailabilityProjection;
|
|
9
|
+
exports.validateAvailabilitySelection = validateAvailabilitySelection;
|
|
10
|
+
var _types = require("./types");
|
|
11
|
+
var _localClock = require("./localClock");
|
|
12
|
+
const DEFAULT_SLOT_STEP_MINUTES = 30;
|
|
13
|
+
const DEFAULT_BUSINESS_HOURS = {
|
|
14
|
+
startTime: '09:00',
|
|
15
|
+
endTime: '18:00'
|
|
34
16
|
};
|
|
35
17
|
function availabilityError(code, message, details) {
|
|
36
|
-
return new
|
|
18
|
+
return new _types.AvailabilityError(code, message, details);
|
|
37
19
|
}
|
|
38
20
|
function sameId(left, right) {
|
|
39
|
-
if (left ===
|
|
40
|
-
return false;
|
|
21
|
+
if (left === undefined || left === null || right === undefined || right === null) return false;
|
|
41
22
|
return String(left) === String(right);
|
|
42
23
|
}
|
|
43
24
|
function uniqById(values = []) {
|
|
44
|
-
const map =
|
|
45
|
-
values.forEach(
|
|
25
|
+
const map = new Map();
|
|
26
|
+
values.forEach(value => {
|
|
46
27
|
map.set(String(value), value);
|
|
47
28
|
});
|
|
48
29
|
return Array.from(map.values());
|
|
49
30
|
}
|
|
50
31
|
function normalizePartySize(partySize) {
|
|
51
|
-
if (partySize ===
|
|
52
|
-
return void 0;
|
|
32
|
+
if (partySize === undefined || partySize === null) return undefined;
|
|
53
33
|
const normalized = Number(partySize);
|
|
54
34
|
if (!Number.isInteger(normalized) || normalized <= 0) {
|
|
55
|
-
throw availabilityError(
|
|
35
|
+
throw availabilityError('INVALID_TARGET_FILTER', 'partySize 必须是正整数', {
|
|
56
36
|
partySize
|
|
57
37
|
});
|
|
58
38
|
}
|
|
59
39
|
return normalized;
|
|
60
40
|
}
|
|
61
41
|
function normalizeBookingCount(bookingCount) {
|
|
62
|
-
if (bookingCount ===
|
|
63
|
-
return 1;
|
|
42
|
+
if (bookingCount === undefined || bookingCount === null) return 1;
|
|
64
43
|
const normalized = Number(bookingCount);
|
|
65
44
|
if (!Number.isInteger(normalized) || normalized <= 0) {
|
|
66
|
-
throw availabilityError(
|
|
45
|
+
throw availabilityError('INVALID_TARGET_FILTER', 'bookingCount 必须是正整数', {
|
|
67
46
|
bookingCount
|
|
68
47
|
});
|
|
69
48
|
}
|
|
@@ -71,49 +50,51 @@ function normalizeBookingCount(bookingCount) {
|
|
|
71
50
|
}
|
|
72
51
|
function toDateTime(value, _timezoneName) {
|
|
73
52
|
try {
|
|
74
|
-
return (0,
|
|
53
|
+
return (0, _localClock.localDateTimeToScalar)((0, _localClock.parseLocalDateTime)(value));
|
|
75
54
|
} catch (_error) {
|
|
76
55
|
return Number.NaN;
|
|
77
56
|
}
|
|
78
57
|
}
|
|
79
58
|
function toDateStart(value, _timezoneName) {
|
|
80
59
|
try {
|
|
81
|
-
return (0,
|
|
60
|
+
return (0, _localClock.localDateToScalarStart)(value);
|
|
82
61
|
} catch (_error) {
|
|
83
62
|
return Number.NaN;
|
|
84
63
|
}
|
|
85
64
|
}
|
|
86
65
|
function toDateEnd(value, _timezoneName) {
|
|
87
66
|
const start = toDateStart(value);
|
|
88
|
-
return Number.isFinite(start) ? (0,
|
|
67
|
+
return Number.isFinite(start) ? (0, _localClock.addLocalCalendarDays)(start, 1) - 1 : Number.NaN;
|
|
89
68
|
}
|
|
90
69
|
function toDateTimeInDate(date, time, _timezoneName) {
|
|
91
70
|
return toDateTime(`${date}T${time.length === 5 ? `${time}:00` : time}`);
|
|
92
71
|
}
|
|
93
72
|
function addMinutesZoned(value, minutes, _timezoneName) {
|
|
94
|
-
return (0,
|
|
73
|
+
return (0, _localClock.addLocalMinutes)(value, minutes);
|
|
95
74
|
}
|
|
96
75
|
function ceilToTenMinutes(value, _timezoneName) {
|
|
97
|
-
return (0,
|
|
76
|
+
return (0, _localClock.ceilLocalScalarToTenMinutes)(value);
|
|
98
77
|
}
|
|
99
78
|
function nextDate(value, _timezoneName) {
|
|
100
|
-
return (0,
|
|
79
|
+
return (0, _localClock.nextDate)(value);
|
|
101
80
|
}
|
|
102
81
|
function formatDateTime(value, _timezoneName, fixedOffsetMinutes) {
|
|
103
|
-
return (0,
|
|
82
|
+
return (0, _localClock.formatLocalDateTime)(value, fixedOffsetMinutes ?? (0, _localClock.captureRuntimeOffsetMinutes)());
|
|
104
83
|
}
|
|
105
84
|
function formatDate(value, _timezoneName) {
|
|
106
|
-
return (0,
|
|
85
|
+
return (0, _localClock.formatLocalDate)(value);
|
|
107
86
|
}
|
|
108
87
|
function formatTime(value, _timezoneName) {
|
|
109
|
-
return (0,
|
|
88
|
+
return (0, _localClock.formatLocalTime)(value, false);
|
|
110
89
|
}
|
|
111
90
|
function normalizeAbsoluteRange(range, _timezoneName) {
|
|
112
91
|
const start = toDateTime(range.startAt);
|
|
113
92
|
const end = toDateTime(range.endAt);
|
|
114
|
-
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start)
|
|
115
|
-
|
|
116
|
-
|
|
93
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return null;
|
|
94
|
+
return {
|
|
95
|
+
start,
|
|
96
|
+
end
|
|
97
|
+
};
|
|
117
98
|
}
|
|
118
99
|
function getCandidateBookingRange(candidate) {
|
|
119
100
|
return {
|
|
@@ -128,13 +109,7 @@ function getCandidateRangeCacheKey(candidate) {
|
|
|
128
109
|
}
|
|
129
110
|
function getProductTimeRangeIdentity(range) {
|
|
130
111
|
const bookingRange = getCandidateBookingRange(range);
|
|
131
|
-
return [
|
|
132
|
-
String(range.productId),
|
|
133
|
-
range.startAt,
|
|
134
|
-
range.endAt,
|
|
135
|
-
bookingRange.startAt,
|
|
136
|
-
bookingRange.endAt
|
|
137
|
-
].join(":");
|
|
112
|
+
return [String(range.productId), range.startAt, range.endAt, bookingRange.startAt, bookingRange.endAt].join(':');
|
|
138
113
|
}
|
|
139
114
|
function toIndexedRange(range) {
|
|
140
115
|
return {
|
|
@@ -145,107 +120,97 @@ function toIndexedRange(range) {
|
|
|
145
120
|
}
|
|
146
121
|
function normalizeNonNegativeNumber(value) {
|
|
147
122
|
const normalized = Number(value);
|
|
148
|
-
if (!Number.isFinite(normalized) || normalized < 0)
|
|
149
|
-
return null;
|
|
123
|
+
if (!Number.isFinite(normalized) || normalized < 0) return null;
|
|
150
124
|
return normalized;
|
|
151
125
|
}
|
|
152
126
|
function resolveCutOffPolicyIssue(policy) {
|
|
153
|
-
if (policy.type ===
|
|
154
|
-
return policy.reason ||
|
|
127
|
+
if (policy.type === 'invalid') {
|
|
128
|
+
return policy.reason || 'invalid_cutoff_policy';
|
|
155
129
|
}
|
|
156
|
-
if (
|
|
157
|
-
return
|
|
130
|
+
if ('futureDay' in policy && policy.futureDay !== undefined && normalizeNonNegativeNumber(policy.futureDay) === null) {
|
|
131
|
+
return 'invalid_future_day';
|
|
158
132
|
}
|
|
159
|
-
if (policy.type ===
|
|
160
|
-
return
|
|
133
|
+
if (policy.type === 'ongoing' && policy.mode === 'after_start' && normalizeNonNegativeNumber(policy.graceMinutes) === null) {
|
|
134
|
+
return 'invalid_grace_minutes';
|
|
161
135
|
}
|
|
162
|
-
if (policy.type ===
|
|
163
|
-
if (normalizeNonNegativeNumber(policy.unit) === null)
|
|
164
|
-
|
|
165
|
-
if (!["days", "hours", "minutes"].includes(policy.unitType))
|
|
166
|
-
return "invalid_advance_unit_type";
|
|
136
|
+
if (policy.type === 'advance') {
|
|
137
|
+
if (normalizeNonNegativeNumber(policy.unit) === null) return 'invalid_advance_unit';
|
|
138
|
+
if (!['days', 'hours', 'minutes'].includes(policy.unitType)) return 'invalid_advance_unit_type';
|
|
167
139
|
}
|
|
168
140
|
return null;
|
|
169
141
|
}
|
|
170
142
|
function addLeadWindow(base, unit, unitType, _timezoneName) {
|
|
171
|
-
if (unitType ===
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return (0, import_localClock.addLocalHours)(base, unit);
|
|
175
|
-
return (0, import_localClock.addLocalMinutes)(base, unit);
|
|
143
|
+
if (unitType === 'days') return (0, _localClock.addLocalCalendarDays)(base, unit);
|
|
144
|
+
if (unitType === 'hours') return (0, _localClock.addLocalHours)(base, unit);
|
|
145
|
+
return (0, _localClock.addLocalMinutes)(base, unit);
|
|
176
146
|
}
|
|
177
147
|
function evaluateCandidateTimeGate(params) {
|
|
178
|
-
const {
|
|
148
|
+
const {
|
|
149
|
+
product,
|
|
150
|
+
candidateRange,
|
|
151
|
+
evaluationContext,
|
|
152
|
+
timezoneName
|
|
153
|
+
} = params;
|
|
179
154
|
const now = evaluationContext.evaluatedAt;
|
|
180
155
|
if (candidateRange.endMs <= evaluationContext.evaluatedAtMs) {
|
|
181
156
|
return {
|
|
182
|
-
status:
|
|
183
|
-
reasonCodes: [
|
|
184
|
-
conflictCode:
|
|
157
|
+
status: 'past',
|
|
158
|
+
reasonCodes: ['past'],
|
|
159
|
+
conflictCode: 'past'
|
|
185
160
|
};
|
|
186
161
|
}
|
|
187
162
|
const policy = product.cutOffPolicy;
|
|
188
|
-
if (!policy)
|
|
189
|
-
return null;
|
|
163
|
+
if (!policy) return null;
|
|
190
164
|
if (resolveCutOffPolicyIssue(policy)) {
|
|
191
165
|
return {
|
|
192
|
-
status:
|
|
193
|
-
reasonCodes: [
|
|
194
|
-
conflictCode:
|
|
166
|
+
status: 'unavailable',
|
|
167
|
+
reasonCodes: ['booking_cutoff_invalid'],
|
|
168
|
+
conflictCode: 'booking_cutoff'
|
|
195
169
|
};
|
|
196
170
|
}
|
|
197
|
-
if (
|
|
171
|
+
if ('futureDay' in policy && policy.futureDay !== undefined) {
|
|
198
172
|
const futureDay = normalizeNonNegativeNumber(policy.futureDay);
|
|
199
|
-
const lastAllowedDateOrdinal = (0,
|
|
200
|
-
if ((0,
|
|
173
|
+
const lastAllowedDateOrdinal = (0, _localClock.dateOrdinalFromLocalDate)(formatDate(now)) + futureDay;
|
|
174
|
+
if ((0, _localClock.dateOrdinalFromLocalDate)(formatDate(candidateRange.start)) > lastAllowedDateOrdinal) {
|
|
201
175
|
return {
|
|
202
|
-
status:
|
|
203
|
-
reasonCodes: [
|
|
204
|
-
conflictCode:
|
|
176
|
+
status: 'unavailable',
|
|
177
|
+
reasonCodes: ['booking_cutoff_future_day'],
|
|
178
|
+
conflictCode: 'booking_cutoff'
|
|
205
179
|
};
|
|
206
180
|
}
|
|
207
181
|
}
|
|
208
|
-
if (policy.type ===
|
|
209
|
-
if (policy.mode ===
|
|
210
|
-
return null;
|
|
182
|
+
if (policy.type === 'ongoing') {
|
|
183
|
+
if (policy.mode === 'before_end') return null;
|
|
211
184
|
const graceMinutes = normalizeNonNegativeNumber(policy.graceMinutes);
|
|
212
185
|
const graceDeadline = addMinutesZoned(candidateRange.start, graceMinutes, timezoneName);
|
|
213
|
-
if (now < graceDeadline)
|
|
214
|
-
return null;
|
|
186
|
+
if (now < graceDeadline) return null;
|
|
215
187
|
return {
|
|
216
|
-
status:
|
|
217
|
-
reasonCodes: [
|
|
218
|
-
conflictCode:
|
|
188
|
+
status: 'unavailable',
|
|
189
|
+
reasonCodes: ['booking_cutoff_after_start'],
|
|
190
|
+
conflictCode: 'booking_cutoff'
|
|
219
191
|
};
|
|
220
192
|
}
|
|
221
|
-
if (policy.type ===
|
|
222
|
-
if (now < candidateRange.start)
|
|
223
|
-
return null;
|
|
193
|
+
if (policy.type === 'before_start') {
|
|
194
|
+
if (now < candidateRange.start) return null;
|
|
224
195
|
return {
|
|
225
|
-
status:
|
|
226
|
-
reasonCodes: [
|
|
227
|
-
conflictCode:
|
|
196
|
+
status: 'unavailable',
|
|
197
|
+
reasonCodes: ['booking_cutoff_before_start'],
|
|
198
|
+
conflictCode: 'booking_cutoff'
|
|
228
199
|
};
|
|
229
200
|
}
|
|
230
|
-
if (policy.type ===
|
|
231
|
-
const leadWindow = addLeadWindow(
|
|
232
|
-
|
|
233
|
-
normalizeNonNegativeNumber(policy.unit),
|
|
234
|
-
policy.unitType,
|
|
235
|
-
timezoneName
|
|
236
|
-
);
|
|
237
|
-
if (leadWindow <= candidateRange.start)
|
|
238
|
-
return null;
|
|
201
|
+
if (policy.type === 'advance') {
|
|
202
|
+
const leadWindow = addLeadWindow(now, normalizeNonNegativeNumber(policy.unit), policy.unitType, timezoneName);
|
|
203
|
+
if (leadWindow <= candidateRange.start) return null;
|
|
239
204
|
return {
|
|
240
|
-
status:
|
|
241
|
-
reasonCodes: [
|
|
242
|
-
conflictCode:
|
|
205
|
+
status: 'unavailable',
|
|
206
|
+
reasonCodes: ['booking_cutoff_advance'],
|
|
207
|
+
conflictCode: 'booking_cutoff'
|
|
243
208
|
};
|
|
244
209
|
}
|
|
245
210
|
return {
|
|
246
|
-
status:
|
|
247
|
-
reasonCodes: [
|
|
248
|
-
conflictCode:
|
|
211
|
+
status: 'unavailable',
|
|
212
|
+
reasonCodes: ['booking_cutoff_invalid'],
|
|
213
|
+
conflictCode: 'booking_cutoff'
|
|
249
214
|
};
|
|
250
215
|
}
|
|
251
216
|
function overlaps(left, right) {
|
|
@@ -257,9 +222,11 @@ function contains(outer, inner) {
|
|
|
257
222
|
function clipRange(base, clip) {
|
|
258
223
|
const start = Math.max(base.start, clip.start);
|
|
259
224
|
const end = Math.min(base.end, clip.end);
|
|
260
|
-
if (end <= start)
|
|
261
|
-
|
|
262
|
-
|
|
225
|
+
if (end <= start) return null;
|
|
226
|
+
return {
|
|
227
|
+
start,
|
|
228
|
+
end
|
|
229
|
+
};
|
|
263
230
|
}
|
|
264
231
|
function sortRanges(ranges) {
|
|
265
232
|
return [...ranges].sort((left, right) => left.start - right.start);
|
|
@@ -267,7 +234,7 @@ function sortRanges(ranges) {
|
|
|
267
234
|
function mergeRanges(ranges) {
|
|
268
235
|
const sorted = sortRanges(ranges);
|
|
269
236
|
const merged = [];
|
|
270
|
-
sorted.forEach(
|
|
237
|
+
sorted.forEach(range => {
|
|
271
238
|
const last = merged[merged.length - 1];
|
|
272
239
|
if (!last) {
|
|
273
240
|
merged.push(range);
|
|
@@ -283,11 +250,10 @@ function mergeRanges(ranges) {
|
|
|
283
250
|
}
|
|
284
251
|
function intersectRangeLists(left, right) {
|
|
285
252
|
const results = [];
|
|
286
|
-
left.forEach(
|
|
287
|
-
right.forEach(
|
|
253
|
+
left.forEach(leftRange => {
|
|
254
|
+
right.forEach(rightRange => {
|
|
288
255
|
const clipped = clipRange(leftRange, rightRange);
|
|
289
|
-
if (clipped)
|
|
290
|
-
results.push(clipped);
|
|
256
|
+
if (clipped) results.push(clipped);
|
|
291
257
|
});
|
|
292
258
|
});
|
|
293
259
|
return mergeRanges(results);
|
|
@@ -304,25 +270,28 @@ function enumerateDates(startDate, endDate) {
|
|
|
304
270
|
function normalizeCoverage(input) {
|
|
305
271
|
const startDate = input.coverage.startDate;
|
|
306
272
|
const endDate = input.coverage.endDate;
|
|
307
|
-
const isCanonicalDate =
|
|
273
|
+
const isCanonicalDate = value => {
|
|
308
274
|
try {
|
|
309
|
-
return (0,
|
|
275
|
+
return (0, _localClock.formatLocalDate)((0, _localClock.parseLocalDate)(value)) === value;
|
|
310
276
|
} catch (_error) {
|
|
311
277
|
return false;
|
|
312
278
|
}
|
|
313
279
|
};
|
|
314
280
|
if (!startDate || !endDate || !isCanonicalDate(startDate) || !isCanonicalDate(endDate)) {
|
|
315
|
-
throw availabilityError(
|
|
281
|
+
throw availabilityError('INVALID_DATE_RANGE', 'coverage.startDate / coverage.endDate 非法', {
|
|
316
282
|
coverage: input.coverage
|
|
317
283
|
});
|
|
318
284
|
}
|
|
319
285
|
if (endDate < startDate) {
|
|
320
|
-
throw availabilityError(
|
|
286
|
+
throw availabilityError('INVALID_DATE_RANGE', 'coverage.endDate 不能早于 startDate', {
|
|
321
287
|
coverage: input.coverage
|
|
322
288
|
});
|
|
323
289
|
}
|
|
324
290
|
return {
|
|
325
|
-
dateRange: {
|
|
291
|
+
dateRange: {
|
|
292
|
+
startDate,
|
|
293
|
+
endDate
|
|
294
|
+
},
|
|
326
295
|
absolute: {
|
|
327
296
|
start: toDateStart(startDate, input.timezone),
|
|
328
297
|
end: toDateEnd(nextDate(endDate, input.timezone), input.timezone)
|
|
@@ -330,17 +299,13 @@ function normalizeCoverage(input) {
|
|
|
330
299
|
};
|
|
331
300
|
}
|
|
332
301
|
function normalizeResourceWindow(resource, coverage, timezoneName) {
|
|
333
|
-
return mergeRanges(
|
|
334
|
-
(resource.windows || []).map((window) => normalizeAbsoluteRange(window, timezoneName)).filter((window) => Boolean(window)).map((window) => clipRange(window, coverage)).filter((window) => Boolean(window))
|
|
335
|
-
);
|
|
302
|
+
return mergeRanges((resource.windows || []).map(window => normalizeAbsoluteRange(window, timezoneName)).filter(window => Boolean(window)).map(window => clipRange(window, coverage)).filter(window => Boolean(window)));
|
|
336
303
|
}
|
|
337
304
|
function normalizeOccupancy(occupancy, coverage, timezoneName, fixedOffsetMinutes) {
|
|
338
305
|
const normalized = normalizeAbsoluteRange(occupancy, timezoneName);
|
|
339
|
-
if (!normalized)
|
|
340
|
-
return null;
|
|
306
|
+
if (!normalized) return null;
|
|
341
307
|
const clipped = clipRange(normalized, coverage);
|
|
342
|
-
if (!clipped)
|
|
343
|
-
return null;
|
|
308
|
+
if (!clipped) return null;
|
|
344
309
|
return {
|
|
345
310
|
...occupancy,
|
|
346
311
|
startAt: formatDateTime(clipped.start, timezoneName, fixedOffsetMinutes),
|
|
@@ -349,7 +314,7 @@ function normalizeOccupancy(occupancy, coverage, timezoneName, fixedOffsetMinute
|
|
|
349
314
|
};
|
|
350
315
|
}
|
|
351
316
|
function normalizeRequirementGroup(product, requirementGroup) {
|
|
352
|
-
const resourceType =
|
|
317
|
+
const resourceType = 'single';
|
|
353
318
|
const required = requirementGroup.required !== false;
|
|
354
319
|
const min = required ? 1 : 0;
|
|
355
320
|
return {
|
|
@@ -360,107 +325,97 @@ function normalizeRequirementGroup(product, requirementGroup) {
|
|
|
360
325
|
min,
|
|
361
326
|
max: 1,
|
|
362
327
|
resourceIds: uniqById(requirementGroup.resourceIds || []),
|
|
363
|
-
capacityRequired: Math.max(
|
|
364
|
-
1,
|
|
365
|
-
Number(requirementGroup.capacityRequired || product.capacityRequired || 1) || 1
|
|
366
|
-
)
|
|
328
|
+
capacityRequired: Math.max(1, Number(requirementGroup.capacityRequired || product.capacityRequired || 1) || 1)
|
|
367
329
|
};
|
|
368
330
|
}
|
|
369
331
|
function normalizeProducts(products) {
|
|
370
|
-
return products.map(
|
|
332
|
+
return products.map(product => ({
|
|
371
333
|
...product,
|
|
372
334
|
quantity: Math.max(1, Number(product.quantity || 1) || 1),
|
|
373
335
|
capacityRequired: Math.max(1, Number(product.capacityRequired || 1) || 1),
|
|
374
336
|
durationMinutes: Math.max(1, Number(product.durationMinutes || 30) || 30),
|
|
375
|
-
cutOffPolicy: product.cutOffPolicy ? {
|
|
376
|
-
|
|
377
|
-
|
|
337
|
+
cutOffPolicy: product.cutOffPolicy ? {
|
|
338
|
+
...product.cutOffPolicy
|
|
339
|
+
} : undefined,
|
|
340
|
+
requirementGroups: (product.requirementGroups || []).map(group => normalizeRequirementGroup(product, group)),
|
|
341
|
+
sessionRules: (product.sessionRules || []).map(rule => ({
|
|
378
342
|
...rule,
|
|
379
|
-
weekdays: rule.weekdays ? [...rule.weekdays] :
|
|
380
|
-
dates: rule.dates ? [...rule.dates] :
|
|
381
|
-
includeDates: rule.includeDates ? [...rule.includeDates] :
|
|
382
|
-
excludeDates: rule.excludeDates ? [...rule.excludeDates] :
|
|
383
|
-
scheduleRef: rule.scheduleRef ? {
|
|
343
|
+
weekdays: rule.weekdays ? [...rule.weekdays] : undefined,
|
|
344
|
+
dates: rule.dates ? [...rule.dates] : undefined,
|
|
345
|
+
includeDates: rule.includeDates ? [...rule.includeDates] : undefined,
|
|
346
|
+
excludeDates: rule.excludeDates ? [...rule.excludeDates] : undefined,
|
|
347
|
+
scheduleRef: rule.scheduleRef ? {
|
|
348
|
+
...rule.scheduleRef
|
|
349
|
+
} : undefined
|
|
384
350
|
})),
|
|
385
|
-
sessionRanges: (product.sessionRanges || []).map(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
...
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
})
|
|
351
|
+
sessionRanges: (product.sessionRanges || []).map(range => ({
|
|
352
|
+
...range,
|
|
353
|
+
scheduleRefs: range.scheduleRefs?.map(scheduleRef => ({
|
|
354
|
+
...scheduleRef
|
|
355
|
+
}))
|
|
356
|
+
}))
|
|
392
357
|
}));
|
|
393
358
|
}
|
|
394
359
|
function normalizeResources(resources) {
|
|
395
|
-
return resources.map(
|
|
360
|
+
return resources.map(resource => ({
|
|
396
361
|
...resource,
|
|
397
|
-
resourceType:
|
|
362
|
+
resourceType: 'single',
|
|
398
363
|
capacity: Math.max(1, Number(resource.capacity || 1) || 1),
|
|
399
|
-
windows: (resource.windows || []).map(
|
|
364
|
+
windows: (resource.windows || []).map(window => ({
|
|
365
|
+
...window
|
|
366
|
+
}))
|
|
400
367
|
}));
|
|
401
368
|
}
|
|
402
369
|
function normalizeOperatingHours(params) {
|
|
403
|
-
|
|
404
|
-
const fallbackWindows = enumerateDates(params.dateRange.startDate, params.dateRange.endDate).map((date) => {
|
|
370
|
+
const fallbackWindows = enumerateDates(params.dateRange.startDate, params.dateRange.endDate).map(date => {
|
|
405
371
|
const start = toDateTimeInDate(date, params.businessHours.startTime, params.timezoneName);
|
|
406
372
|
let end = toDateTimeInDate(date, params.businessHours.endTime, params.timezoneName);
|
|
407
373
|
if (end <= start) {
|
|
408
374
|
end = toDateTimeInDate(nextDate(date), params.businessHours.endTime, params.timezoneName);
|
|
409
375
|
}
|
|
410
|
-
return {
|
|
376
|
+
return {
|
|
377
|
+
start,
|
|
378
|
+
end
|
|
379
|
+
};
|
|
411
380
|
});
|
|
412
|
-
const sourceWindows = params.input ? params.input.windows : fallbackWindows.map(
|
|
381
|
+
const sourceWindows = params.input ? params.input.windows : fallbackWindows.map(range => ({
|
|
413
382
|
startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
|
|
414
383
|
endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
|
|
415
384
|
}));
|
|
416
|
-
const windows = mergeRanges(
|
|
417
|
-
sourceWindows.map((window) => normalizeAbsoluteRange(window, params.timezoneName)).filter((window) => Boolean(window)).map((window) => clipRange(window, params.coverage)).filter((window) => Boolean(window))
|
|
418
|
-
).map((range) => ({
|
|
385
|
+
const windows = mergeRanges(sourceWindows.map(window => normalizeAbsoluteRange(window, params.timezoneName)).filter(window => Boolean(window)).map(window => clipRange(window, params.coverage)).filter(window => Boolean(window))).map(range => ({
|
|
419
386
|
startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
|
|
420
387
|
endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
|
|
421
388
|
}));
|
|
422
389
|
return {
|
|
423
|
-
source:
|
|
424
|
-
scheduleIds: uniqById(
|
|
425
|
-
missingScheduleIds: uniqById(
|
|
390
|
+
source: params.input?.source || params.fallbackSource,
|
|
391
|
+
scheduleIds: uniqById(params.input?.scheduleIds || []),
|
|
392
|
+
missingScheduleIds: uniqById(params.input?.missingScheduleIds || []),
|
|
426
393
|
windows
|
|
427
394
|
};
|
|
428
395
|
}
|
|
429
396
|
function matchesRequirementGroup(requirementGroup, resource, product, partySize = 1) {
|
|
430
|
-
|
|
431
|
-
if (requirementGroup.formId !== void 0 && resource.formId !== void 0 && !sameId(requirementGroup.formId, resource.formId)) {
|
|
397
|
+
if (requirementGroup.formId !== undefined && resource.formId !== undefined && !sameId(requirementGroup.formId, resource.formId)) {
|
|
432
398
|
return false;
|
|
433
399
|
}
|
|
434
|
-
if (
|
|
400
|
+
if (requirementGroup.resourceIds?.length && !requirementGroup.resourceIds.some(resourceId => sameId(resourceId, resource.id))) {
|
|
435
401
|
return false;
|
|
436
402
|
}
|
|
437
403
|
return resource.capacity >= resolveRequiredCapacity(requirementGroup, product, partySize);
|
|
438
404
|
}
|
|
439
405
|
function resolveRequiredCapacity(requirementGroup, product, partySize) {
|
|
440
|
-
return Math.max(
|
|
441
|
-
1,
|
|
442
|
-
Number(
|
|
443
|
-
partySize ?? requirementGroup.capacityRequired ?? product.capacityRequired ?? 1
|
|
444
|
-
) || 1
|
|
445
|
-
);
|
|
406
|
+
return Math.max(1, Number(partySize ?? requirementGroup.capacityRequired ?? product.capacityRequired ?? 1) || 1);
|
|
446
407
|
}
|
|
447
408
|
function buildCellKey(params) {
|
|
448
|
-
return [
|
|
449
|
-
String(params.productId),
|
|
450
|
-
params.groupId,
|
|
451
|
-
String(params.resourceId),
|
|
452
|
-
params.start,
|
|
453
|
-
params.end
|
|
454
|
-
].join(":");
|
|
409
|
+
return [String(params.productId), params.groupId, String(params.resourceId), params.start, params.end].join(':');
|
|
455
410
|
}
|
|
456
411
|
function dedupeOccupancyRefKey(occupancy) {
|
|
457
|
-
if (occupancy.scheduleEventId !==
|
|
412
|
+
if (occupancy.scheduleEventId !== undefined && occupancy.scheduleEventId !== null) {
|
|
458
413
|
return `scheduleEvent:${String(occupancy.resourceId)}:${String(occupancy.scheduleEventId)}`;
|
|
459
414
|
}
|
|
460
|
-
if (occupancy.bookingUid !==
|
|
415
|
+
if (occupancy.bookingUid !== undefined && occupancy.bookingUid !== null) {
|
|
461
416
|
return `bookingUid:${String(occupancy.resourceId)}:${String(occupancy.bookingUid)}`;
|
|
462
417
|
}
|
|
463
|
-
if (occupancy.productUid !==
|
|
418
|
+
if (occupancy.productUid !== undefined && occupancy.productUid !== null) {
|
|
464
419
|
return `productUid:${String(occupancy.resourceId)}:${String(occupancy.productUid)}`;
|
|
465
420
|
}
|
|
466
421
|
return `range:${String(occupancy.resourceId)}:${occupancy.startAt}:${occupancy.endAt}:${String(occupancy.pax || 1)}`;
|
|
@@ -472,22 +427,27 @@ function buildCells(projection) {
|
|
|
472
427
|
};
|
|
473
428
|
const cells = [];
|
|
474
429
|
const timezoneName = projection.meta.timezone;
|
|
475
|
-
const fixedOffsetMinutes = (0,
|
|
430
|
+
const fixedOffsetMinutes = (0, _localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, _localClock.captureRuntimeOffsetMinutes)();
|
|
476
431
|
const projectionNow = toDateTime(projection.meta.now);
|
|
477
|
-
projection.products.forEach(
|
|
478
|
-
(product.requirementGroups || []).forEach(
|
|
479
|
-
projection.resources.filter(
|
|
432
|
+
projection.products.forEach(product => {
|
|
433
|
+
(product.requirementGroups || []).forEach(requirementGroup => {
|
|
434
|
+
projection.resources.filter(resource => matchesRequirementGroup(requirementGroup, resource, product)).forEach(resource => {
|
|
480
435
|
const windows = normalizeResourceWindow(resource, coverage, timezoneName);
|
|
481
|
-
const occupancies = projection.resourceOccupancies.filter(
|
|
436
|
+
const occupancies = projection.resourceOccupancies.filter(occupancy => sameId(occupancy.resourceId, resource.id)).map(occupancy => ({
|
|
482
437
|
occupancy,
|
|
483
438
|
range: normalizeAbsoluteRange(occupancy, timezoneName)
|
|
484
|
-
})).filter(
|
|
485
|
-
|
|
486
|
-
|
|
439
|
+
})).filter(item => Boolean(item.range)).map(item => ({
|
|
440
|
+
...item,
|
|
441
|
+
range: clipRange(item.range, coverage)
|
|
442
|
+
})).filter(item => Boolean(item.range));
|
|
443
|
+
const boundaries = new Set([coverage.start, coverage.end]);
|
|
444
|
+
windows.forEach(window => {
|
|
487
445
|
boundaries.add(window.start);
|
|
488
446
|
boundaries.add(window.end);
|
|
489
447
|
});
|
|
490
|
-
occupancies.forEach(({
|
|
448
|
+
occupancies.forEach(({
|
|
449
|
+
range
|
|
450
|
+
}) => {
|
|
491
451
|
boundaries.add(range.start);
|
|
492
452
|
boundaries.add(range.end);
|
|
493
453
|
});
|
|
@@ -495,39 +455,38 @@ function buildCells(projection) {
|
|
|
495
455
|
for (let index = 0; index < sortedBoundaries.length - 1; index += 1) {
|
|
496
456
|
const start = sortedBoundaries[index];
|
|
497
457
|
const end = sortedBoundaries[index + 1];
|
|
498
|
-
if (end <= start)
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
)
|
|
458
|
+
if (end <= start) continue;
|
|
459
|
+
const segment = {
|
|
460
|
+
start,
|
|
461
|
+
end
|
|
462
|
+
};
|
|
463
|
+
const isInsideWorkingWindow = windows.some(window => contains(window, segment));
|
|
464
|
+
const segmentOccupancies = isInsideWorkingWindow ? occupancies.filter(({
|
|
465
|
+
range
|
|
466
|
+
}) => overlaps(range, segment)).map(({
|
|
467
|
+
occupancy
|
|
468
|
+
}) => occupancy) : [];
|
|
469
|
+
const usedCapacity = segmentOccupancies.reduce((sum, occupancy) => sum + Math.max(1, Number(occupancy.pax || 1) || 1), 0);
|
|
507
470
|
const remainingCapacity = Math.max(0, resource.capacity - usedCapacity);
|
|
508
471
|
const requiredCapacity = resolveRequiredCapacity(requirementGroup, product);
|
|
509
|
-
const reasonCodes = Array.from(new Set(
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
return ["resource_occupied"];
|
|
517
|
-
})
|
|
518
|
-
));
|
|
519
|
-
let status = "available";
|
|
472
|
+
const reasonCodes = Array.from(new Set(segmentOccupancies.flatMap(occupancy => {
|
|
473
|
+
const source = occupancy.source || 'remote';
|
|
474
|
+
if (source === 'selection') return ['selection_occupied'];
|
|
475
|
+
if (source === 'cart') return ['cart_occupied'];
|
|
476
|
+
return ['resource_occupied'];
|
|
477
|
+
})));
|
|
478
|
+
let status = 'available';
|
|
520
479
|
if (end <= projectionNow) {
|
|
521
|
-
status =
|
|
522
|
-
reasonCodes.push(
|
|
480
|
+
status = 'past';
|
|
481
|
+
reasonCodes.push('past');
|
|
523
482
|
} else if (!isInsideWorkingWindow) {
|
|
524
|
-
status =
|
|
525
|
-
reasonCodes.push(
|
|
483
|
+
status = 'unavailable';
|
|
484
|
+
reasonCodes.push('outside_resource_window');
|
|
526
485
|
} else if (remainingCapacity < requiredCapacity) {
|
|
527
|
-
status =
|
|
528
|
-
reasonCodes.push(
|
|
486
|
+
status = 'full';
|
|
487
|
+
reasonCodes.push('capacity_full');
|
|
529
488
|
} else if (remainingCapacity < resource.capacity) {
|
|
530
|
-
status =
|
|
489
|
+
status = 'limited';
|
|
531
490
|
}
|
|
532
491
|
cells.push({
|
|
533
492
|
key: buildCellKey({
|
|
@@ -554,7 +513,7 @@ function buildCells(projection) {
|
|
|
554
513
|
remainingCapacity,
|
|
555
514
|
maxCapacity: resource.capacity,
|
|
556
515
|
reasonCodes: Array.from(new Set(reasonCodes)),
|
|
557
|
-
occupancyRefs: segmentOccupancies.map(
|
|
516
|
+
occupancyRefs: segmentOccupancies.map(occupancy => ({
|
|
558
517
|
id: occupancy.id,
|
|
559
518
|
resourceId: occupancy.resourceId,
|
|
560
519
|
source: occupancy.source,
|
|
@@ -572,18 +531,8 @@ function buildCells(projection) {
|
|
|
572
531
|
}
|
|
573
532
|
function compressCells(cells) {
|
|
574
533
|
const sorted = [...cells].sort((left, right) => {
|
|
575
|
-
const keyLeft = [
|
|
576
|
-
|
|
577
|
-
String(left.requirementGroupId),
|
|
578
|
-
String(left.resourceId),
|
|
579
|
-
left.startAt
|
|
580
|
-
].join(":");
|
|
581
|
-
const keyRight = [
|
|
582
|
-
String(right.productId),
|
|
583
|
-
String(right.requirementGroupId),
|
|
584
|
-
String(right.resourceId),
|
|
585
|
-
right.startAt
|
|
586
|
-
].join(":");
|
|
534
|
+
const keyLeft = [String(left.productId), String(left.requirementGroupId), String(left.resourceId), left.startAt].join(':');
|
|
535
|
+
const keyRight = [String(right.productId), String(right.requirementGroupId), String(right.resourceId), right.startAt].join(':');
|
|
587
536
|
return keyLeft.localeCompare(keyRight);
|
|
588
537
|
});
|
|
589
538
|
return sorted.reduce((result, cell) => {
|
|
@@ -593,7 +542,9 @@ function compressCells(cells) {
|
|
|
593
542
|
last.endTime = cell.endTime;
|
|
594
543
|
return result;
|
|
595
544
|
}
|
|
596
|
-
result.push({
|
|
545
|
+
result.push({
|
|
546
|
+
...cell
|
|
547
|
+
});
|
|
597
548
|
return result;
|
|
598
549
|
}, []);
|
|
599
550
|
}
|
|
@@ -606,68 +557,45 @@ function makeCoverageAbsolute(dateRange, timezoneName, fixedOffsetMinutes) {
|
|
|
606
557
|
}
|
|
607
558
|
function normalizeProjectionInstant(value, fallbackScalar, timezoneName, fixedOffsetMinutes) {
|
|
608
559
|
const scalar = value ? toDateTime(value) : fallbackScalar;
|
|
609
|
-
return formatDateTime(
|
|
610
|
-
Number.isFinite(scalar) ? scalar : fallbackScalar,
|
|
611
|
-
timezoneName,
|
|
612
|
-
fixedOffsetMinutes
|
|
613
|
-
);
|
|
560
|
+
return formatDateTime(Number.isFinite(scalar) ? scalar : fallbackScalar, timezoneName, fixedOffsetMinutes);
|
|
614
561
|
}
|
|
615
562
|
function cloneSnapshotMetadataValue(value) {
|
|
616
563
|
if (Array.isArray(value)) {
|
|
617
|
-
return value.map(
|
|
564
|
+
return value.map(item => cloneSnapshotMetadataValue(item));
|
|
618
565
|
}
|
|
619
|
-
if (!value || typeof value !==
|
|
620
|
-
return value;
|
|
566
|
+
if (!value || typeof value !== 'object') return value;
|
|
621
567
|
return Object.keys(value).reduce((result, key) => {
|
|
622
568
|
result[key] = cloneSnapshotMetadataValue(value[key]);
|
|
623
569
|
return result;
|
|
624
570
|
}, {});
|
|
625
571
|
}
|
|
626
572
|
function deepFreeze(value) {
|
|
627
|
-
if (!value || typeof value !==
|
|
628
|
-
return value;
|
|
573
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
629
574
|
Object.freeze(value);
|
|
630
|
-
Object.getOwnPropertyNames(value).forEach(
|
|
631
|
-
|
|
632
|
-
|
|
575
|
+
Object.getOwnPropertyNames(value).forEach(key => {
|
|
576
|
+
// `raw` is an explicit escape hatch to the backend/catalog record. It is not owned by the
|
|
577
|
+
// projection and must not freeze mutable ProductList / Order data shared elsewhere.
|
|
578
|
+
if (key === 'raw') return;
|
|
633
579
|
const child = value[key];
|
|
634
|
-
if (child && (typeof child ===
|
|
580
|
+
if (child && (typeof child === 'object' || typeof child === 'function')) {
|
|
635
581
|
deepFreeze(child);
|
|
636
582
|
}
|
|
637
583
|
});
|
|
638
584
|
return value;
|
|
639
585
|
}
|
|
640
586
|
function createAvailabilityProjection(input) {
|
|
641
|
-
var _a, _b;
|
|
642
587
|
const coverage = normalizeCoverage(input);
|
|
643
|
-
const fixedOffsetMinutes = (0,
|
|
644
|
-
const runtimeNow = (0,
|
|
588
|
+
const fixedOffsetMinutes = (0, _localClock.captureRuntimeOffsetMinutes)();
|
|
589
|
+
const runtimeNow = (0, _localClock.localDateToScalar)(new Date());
|
|
645
590
|
const products = normalizeProducts(input.products || []);
|
|
646
591
|
const resources = normalizeResources(input.resources || []);
|
|
647
|
-
const resourceOccupancies = uniqByOccupancy(
|
|
648
|
-
|
|
649
|
-
occupancy,
|
|
650
|
-
coverage.absolute,
|
|
651
|
-
input.timezone,
|
|
652
|
-
fixedOffsetMinutes
|
|
653
|
-
)).filter((occupancy) => Boolean(occupancy))
|
|
654
|
-
);
|
|
655
|
-
const preparedAt = normalizeProjectionInstant(
|
|
656
|
-
input.preparedAt,
|
|
657
|
-
runtimeNow,
|
|
658
|
-
input.timezone,
|
|
659
|
-
fixedOffsetMinutes
|
|
660
|
-
);
|
|
592
|
+
const resourceOccupancies = uniqByOccupancy((input.resourceOccupancies || []).map(occupancy => normalizeOccupancy(occupancy, coverage.absolute, input.timezone, fixedOffsetMinutes)).filter(occupancy => Boolean(occupancy)));
|
|
593
|
+
const preparedAt = normalizeProjectionInstant(input.preparedAt, runtimeNow, input.timezone, fixedOffsetMinutes);
|
|
661
594
|
const now = normalizeProjectionInstant(input.now, runtimeNow, input.timezone, fixedOffsetMinutes);
|
|
662
|
-
const durationCandidateAnchorAt = normalizeProjectionInstant(
|
|
663
|
-
input.durationCandidateAnchorAt || now,
|
|
664
|
-
runtimeNow,
|
|
665
|
-
input.timezone,
|
|
666
|
-
fixedOffsetMinutes
|
|
667
|
-
);
|
|
595
|
+
const durationCandidateAnchorAt = normalizeProjectionInstant(input.durationCandidateAnchorAt || now, runtimeNow, input.timezone, fixedOffsetMinutes);
|
|
668
596
|
const businessHours = {
|
|
669
|
-
startTime:
|
|
670
|
-
endTime:
|
|
597
|
+
startTime: input.businessHours?.startTime || DEFAULT_BUSINESS_HOURS.startTime,
|
|
598
|
+
endTime: input.businessHours?.endTime || DEFAULT_BUSINESS_HOURS.endTime
|
|
671
599
|
};
|
|
672
600
|
const operatingHours = normalizeOperatingHours({
|
|
673
601
|
input: input.operatingHours,
|
|
@@ -676,14 +604,14 @@ function createAvailabilityProjection(input) {
|
|
|
676
604
|
dateRange: coverage.dateRange,
|
|
677
605
|
timezoneName: input.timezone,
|
|
678
606
|
fixedOffsetMinutes,
|
|
679
|
-
fallbackSource: input.businessHours ?
|
|
607
|
+
fallbackSource: input.businessHours ? 'prepare_business_hours' : 'default_business_hours'
|
|
680
608
|
});
|
|
681
609
|
const meta = {
|
|
682
610
|
snapshotId: input.snapshotId || `availability-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
683
611
|
preparedAt,
|
|
684
612
|
timezone: input.timezone,
|
|
685
613
|
orderRevision: input.orderRevision,
|
|
686
|
-
mode: input.mode ||
|
|
614
|
+
mode: input.mode || 'appointment',
|
|
687
615
|
coverage: {
|
|
688
616
|
...coverage.dateRange,
|
|
689
617
|
...makeCoverageAbsolute(coverage.dateRange, input.timezone, fixedOffsetMinutes)
|
|
@@ -697,8 +625,8 @@ function createAvailabilityProjection(input) {
|
|
|
697
625
|
},
|
|
698
626
|
now,
|
|
699
627
|
durationCandidateAnchorAt,
|
|
700
|
-
anchors: input.anchors ? cloneSnapshotMetadataValue(input.anchors) :
|
|
701
|
-
prepareParams: input.prepareParams ? cloneSnapshotMetadataValue(input.prepareParams) :
|
|
628
|
+
anchors: input.anchors ? cloneSnapshotMetadataValue(input.anchors) : undefined,
|
|
629
|
+
prepareParams: input.prepareParams ? cloneSnapshotMetadataValue(input.prepareParams) : undefined
|
|
702
630
|
};
|
|
703
631
|
const projectionBase = {
|
|
704
632
|
meta,
|
|
@@ -712,79 +640,77 @@ function createAvailabilityProjection(input) {
|
|
|
712
640
|
});
|
|
713
641
|
}
|
|
714
642
|
function uniqByOccupancy(occupancies) {
|
|
715
|
-
const map =
|
|
716
|
-
occupancies.forEach(
|
|
643
|
+
const map = new Map();
|
|
644
|
+
occupancies.forEach(occupancy => {
|
|
717
645
|
map.set(dedupeOccupancyRefKey(occupancy), occupancy);
|
|
718
646
|
});
|
|
719
647
|
return Array.from(map.values());
|
|
720
648
|
}
|
|
721
649
|
function resolveProjectionDateRange(projection, requestDateRange) {
|
|
722
|
-
const startDate =
|
|
723
|
-
const endDate =
|
|
724
|
-
const isCanonicalDate =
|
|
650
|
+
const startDate = requestDateRange?.startDate || projection.meta.coverage.startDate;
|
|
651
|
+
const endDate = requestDateRange?.endDate || projection.meta.coverage.endDate;
|
|
652
|
+
const isCanonicalDate = value => {
|
|
725
653
|
try {
|
|
726
|
-
return (0,
|
|
654
|
+
return (0, _localClock.formatLocalDate)((0, _localClock.parseLocalDate)(value)) === value;
|
|
727
655
|
} catch (_error) {
|
|
728
656
|
return false;
|
|
729
657
|
}
|
|
730
658
|
};
|
|
731
659
|
if (!isCanonicalDate(startDate) || !isCanonicalDate(endDate) || endDate < startDate) {
|
|
732
|
-
throw availabilityError(
|
|
660
|
+
throw availabilityError('INVALID_DATE_RANGE', '查询日期范围必须是有效的 YYYY-MM-DD,且结束日期不能早于开始日期', {
|
|
733
661
|
requested: requestDateRange
|
|
734
662
|
});
|
|
735
663
|
}
|
|
736
664
|
if (startDate < projection.meta.coverage.startDate || endDate > projection.meta.coverage.endDate) {
|
|
737
|
-
throw availabilityError(
|
|
665
|
+
throw availabilityError('OUT_OF_PROJECTION_COVERAGE', '查询范围超出 projection.coverage', {
|
|
738
666
|
requested: requestDateRange,
|
|
739
667
|
coverage: projection.meta.coverage
|
|
740
668
|
});
|
|
741
669
|
}
|
|
742
|
-
return {
|
|
670
|
+
return {
|
|
671
|
+
startDate,
|
|
672
|
+
endDate
|
|
673
|
+
};
|
|
743
674
|
}
|
|
744
675
|
function buildOperatingRanges(projection, dateRange) {
|
|
745
676
|
const coverage = {
|
|
746
677
|
start: toDateStart(dateRange.startDate, projection.meta.timezone),
|
|
747
678
|
end: toDateEnd(nextDate(dateRange.endDate), projection.meta.timezone)
|
|
748
679
|
};
|
|
749
|
-
return mergeRanges(
|
|
750
|
-
projection.meta.defaults.operatingHours.windows.map((window) => normalizeAbsoluteRange(window, projection.meta.timezone)).filter((window) => Boolean(window)).map((window) => clipRange(window, coverage)).filter((window) => Boolean(window))
|
|
751
|
-
);
|
|
680
|
+
return mergeRanges(projection.meta.defaults.operatingHours.windows.map(window => normalizeAbsoluteRange(window, projection.meta.timezone)).filter(window => Boolean(window)).map(window => clipRange(window, coverage)).filter(window => Boolean(window)));
|
|
752
681
|
}
|
|
753
682
|
function buildResourceWindowIndex(projection) {
|
|
754
683
|
const coverage = {
|
|
755
684
|
start: toDateTime(projection.meta.coverage.startAt, projection.meta.timezone),
|
|
756
685
|
end: toDateTime(projection.meta.coverage.endAt, projection.meta.timezone)
|
|
757
686
|
};
|
|
758
|
-
return new Map(
|
|
759
|
-
projection.resources.map((resource) => [
|
|
760
|
-
String(resource.id),
|
|
761
|
-
normalizeResourceWindow(resource, coverage, projection.meta.timezone)
|
|
762
|
-
])
|
|
763
|
-
);
|
|
687
|
+
return new Map(projection.resources.map(resource => [String(resource.id), normalizeResourceWindow(resource, coverage, projection.meta.timezone)]));
|
|
764
688
|
}
|
|
765
689
|
function buildGroupWorkingRanges(params) {
|
|
766
|
-
const {
|
|
690
|
+
const {
|
|
691
|
+
projection,
|
|
692
|
+
requirementGroup,
|
|
693
|
+
dateRange,
|
|
694
|
+
resourceIds,
|
|
695
|
+
partySize
|
|
696
|
+
} = params;
|
|
767
697
|
const guardEndDate = nextDate(dateRange.endDate, projection.meta.timezone);
|
|
768
698
|
const coverage = {
|
|
769
699
|
start: toDateStart(dateRange.startDate, projection.meta.timezone),
|
|
770
700
|
end: toDateEnd(guardEndDate, projection.meta.timezone)
|
|
771
701
|
};
|
|
772
|
-
const filteredResourceIdSet =
|
|
773
|
-
const resourceWindows = projection.resources.filter(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
return windows.map((window) => clipRange(window, coverage)).filter((window) => Boolean(window));
|
|
702
|
+
const filteredResourceIdSet = resourceIds?.length ? new Set(resourceIds.map(String)) : null;
|
|
703
|
+
const resourceWindows = projection.resources.filter(resource => matchesRequirementGroup(requirementGroup, resource, params.product, partySize)).filter(resource => !filteredResourceIdSet || filteredResourceIdSet.has(String(resource.id))).map(resource => {
|
|
704
|
+
const windows = params.resourceWindowIndex?.get(String(resource.id)) || normalizeResourceWindow(resource, coverage, projection.meta.timezone);
|
|
705
|
+
return windows.map(window => clipRange(window, coverage)).filter(window => Boolean(window));
|
|
777
706
|
});
|
|
778
|
-
const requiredConcurrentResources = Math.max(
|
|
779
|
-
requirementGroup.required === false ? 0 : 1,
|
|
780
|
-
Number(requirementGroup.min ?? (requirementGroup.required === false ? 0 : 1)) || 0
|
|
781
|
-
);
|
|
707
|
+
const requiredConcurrentResources = Math.max(requirementGroup.required === false ? 0 : 1, Number(requirementGroup.min ?? (requirementGroup.required === false ? 0 : 1)) || 0);
|
|
782
708
|
if (requiredConcurrentResources <= 1) {
|
|
783
|
-
return mergeRanges(resourceWindows.flatMap(
|
|
709
|
+
return mergeRanges(resourceWindows.flatMap(windows => windows));
|
|
784
710
|
}
|
|
785
|
-
const boundaries =
|
|
786
|
-
resourceWindows.forEach(
|
|
787
|
-
windows.forEach(
|
|
711
|
+
const boundaries = new Set();
|
|
712
|
+
resourceWindows.forEach(windows => {
|
|
713
|
+
windows.forEach(window => {
|
|
788
714
|
boundaries.add(window.start);
|
|
789
715
|
boundaries.add(window.end);
|
|
790
716
|
});
|
|
@@ -794,10 +720,12 @@ function buildGroupWorkingRanges(params) {
|
|
|
794
720
|
for (let index = 0; index < sortedBoundaries.length - 1; index += 1) {
|
|
795
721
|
const start = sortedBoundaries[index];
|
|
796
722
|
const end = sortedBoundaries[index + 1];
|
|
797
|
-
if (end <= start)
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
723
|
+
if (end <= start) continue;
|
|
724
|
+
const segment = {
|
|
725
|
+
start,
|
|
726
|
+
end
|
|
727
|
+
};
|
|
728
|
+
const activeResources = resourceWindows.reduce((count, windows) => windows.some(window => contains(window, segment)) ? count + 1 : count, 0);
|
|
801
729
|
if (activeResources >= requiredConcurrentResources) {
|
|
802
730
|
satisfiedRanges.push(segment);
|
|
803
731
|
}
|
|
@@ -806,21 +734,19 @@ function buildGroupWorkingRanges(params) {
|
|
|
806
734
|
}
|
|
807
735
|
function materializeScheduleRules(rules, dateRange, timezoneName, fixedOffsetMinutes) {
|
|
808
736
|
const dates = enumerateDates(dateRange.startDate, dateRange.endDate);
|
|
809
|
-
const seen =
|
|
810
|
-
rules.forEach(
|
|
811
|
-
dates.forEach(
|
|
812
|
-
if (!matchesScheduleRuleDate(rule, date, timezoneName))
|
|
813
|
-
return;
|
|
737
|
+
const seen = new Map();
|
|
738
|
+
rules.forEach(rule => {
|
|
739
|
+
dates.forEach(date => {
|
|
740
|
+
if (!matchesScheduleRuleDate(rule, date, timezoneName)) return;
|
|
814
741
|
const start = toDateTimeInDate(date, rule.startTime, timezoneName);
|
|
815
742
|
let end = toDateTimeInDate(date, rule.endTime, timezoneName);
|
|
816
|
-
if (end <= start)
|
|
817
|
-
end = toDateTimeInDate(nextDate(date, timezoneName), rule.endTime, timezoneName);
|
|
743
|
+
if (end <= start) end = toDateTimeInDate(nextDate(date, timezoneName), rule.endTime, timezoneName);
|
|
818
744
|
const key = `${date}:${start}:${end}`;
|
|
819
745
|
if (!seen.has(key)) {
|
|
820
746
|
seen.set(key, {
|
|
821
747
|
key,
|
|
822
|
-
productId:
|
|
823
|
-
source:
|
|
748
|
+
productId: '',
|
|
749
|
+
source: 'session_schedule',
|
|
824
750
|
startAt: formatDateTime(start, timezoneName, fixedOffsetMinutes),
|
|
825
751
|
endAt: formatDateTime(end, timezoneName, fixedOffsetMinutes),
|
|
826
752
|
bookingStartAt: formatDateTime(start, timezoneName, fixedOffsetMinutes),
|
|
@@ -834,85 +760,66 @@ function materializeScheduleRules(rules, dateRange, timezoneName, fixedOffsetMin
|
|
|
834
760
|
}
|
|
835
761
|
const current = seen.get(key);
|
|
836
762
|
const scheduleRef = {
|
|
837
|
-
...rule.scheduleRef || {},
|
|
763
|
+
...(rule.scheduleRef || {}),
|
|
838
764
|
ruleId: rule.id
|
|
839
765
|
};
|
|
840
766
|
current.scheduleRefs = mergeScheduleRefs(current.scheduleRefs, [scheduleRef]);
|
|
841
|
-
if (!current.primaryScheduleRef)
|
|
842
|
-
current.primaryScheduleRef = scheduleRef;
|
|
767
|
+
if (!current.primaryScheduleRef) current.primaryScheduleRef = scheduleRef;
|
|
843
768
|
});
|
|
844
769
|
});
|
|
845
770
|
return Array.from(seen.values());
|
|
846
771
|
}
|
|
847
772
|
function matchesScheduleRuleDate(rule, date, _timezoneName) {
|
|
848
|
-
|
|
849
|
-
if (
|
|
850
|
-
|
|
851
|
-
if (
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
return false;
|
|
855
|
-
if (rule.endDate && date > rule.endDate)
|
|
856
|
-
return false;
|
|
857
|
-
if (rule.mode === "designation") {
|
|
858
|
-
if ((_c = rule.dates) == null ? void 0 : _c.includes(date))
|
|
859
|
-
return true;
|
|
773
|
+
if (rule.includeDates?.includes(date)) return true;
|
|
774
|
+
if (rule.excludeDates?.includes(date)) return false;
|
|
775
|
+
if (rule.startDate && date < rule.startDate) return false;
|
|
776
|
+
if (rule.endDate && date > rule.endDate) return false;
|
|
777
|
+
if (rule.mode === 'designation') {
|
|
778
|
+
if (rule.dates?.includes(date)) return true;
|
|
860
779
|
return false;
|
|
861
780
|
}
|
|
862
|
-
if (rule.mode ===
|
|
863
|
-
if (!
|
|
864
|
-
return true;
|
|
781
|
+
if (rule.mode === 'weekly') {
|
|
782
|
+
if (!rule.weekdays?.length) return true;
|
|
865
783
|
const freq = Math.max(1, Number(rule.frequency || 1) || 1);
|
|
866
|
-
if (!rule.weekdays.includes((0,
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const targetWeekStart = (0, import_localClock.dateOrdinalFromLocalDate)(date) - (0, import_localClock.weekdayFromLocalDate)(date);
|
|
871
|
-
const ruleWeekStart = (0, import_localClock.dateOrdinalFromLocalDate)(rule.startDate) - (0, import_localClock.weekdayFromLocalDate)(rule.startDate);
|
|
784
|
+
if (!rule.weekdays.includes((0, _localClock.weekdayFromLocalDate)(date))) return false;
|
|
785
|
+
if (!rule.startDate) return true;
|
|
786
|
+
const targetWeekStart = (0, _localClock.dateOrdinalFromLocalDate)(date) - (0, _localClock.weekdayFromLocalDate)(date);
|
|
787
|
+
const ruleWeekStart = (0, _localClock.dateOrdinalFromLocalDate)(rule.startDate) - (0, _localClock.weekdayFromLocalDate)(rule.startDate);
|
|
872
788
|
return Math.floor((targetWeekStart - ruleWeekStart) / 7) % freq === 0;
|
|
873
789
|
}
|
|
874
|
-
if (rule.mode ===
|
|
790
|
+
if (rule.mode === 'daily' && rule.startDate) {
|
|
875
791
|
const freq = Math.max(1, Number(rule.frequency || 1) || 1);
|
|
876
|
-
return ((0,
|
|
792
|
+
return ((0, _localClock.dateOrdinalFromLocalDate)(date) - (0, _localClock.dateOrdinalFromLocalDate)(rule.startDate)) % freq === 0;
|
|
877
793
|
}
|
|
878
794
|
return true;
|
|
879
795
|
}
|
|
880
796
|
function mergeScheduleRefs(base, next) {
|
|
881
|
-
const map =
|
|
882
|
-
[...base, ...next].forEach(
|
|
883
|
-
const key = [
|
|
884
|
-
ref.scheduleId ?? "",
|
|
885
|
-
ref.timeSlotId ?? "",
|
|
886
|
-
ref.ruleId ?? "",
|
|
887
|
-
ref.source ?? "",
|
|
888
|
-
ref.label ?? ""
|
|
889
|
-
].join(":");
|
|
797
|
+
const map = new Map();
|
|
798
|
+
[...base, ...next].forEach(ref => {
|
|
799
|
+
const key = [ref.scheduleId ?? '', ref.timeSlotId ?? '', ref.ruleId ?? '', ref.source ?? '', ref.label ?? ''].join(':');
|
|
890
800
|
map.set(key, ref);
|
|
891
801
|
});
|
|
892
802
|
return Array.from(map.values());
|
|
893
803
|
}
|
|
894
804
|
function buildProductIndex(projection) {
|
|
895
|
-
return new Map(projection.products.map(
|
|
805
|
+
return new Map(projection.products.map(product => [String(product.id), product]));
|
|
896
806
|
}
|
|
897
807
|
function buildGroupResourceIndex(params) {
|
|
898
|
-
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
index.set(key, params.projection.resources.filter((resource) => matchesRequirementGroup(requirementGroup, resource, product, params.partySize)).filter((resource) => !resourceIdSet || resourceIdSet.has(String(resource.id))));
|
|
808
|
+
const resourceIdSet = params.resourceIds?.length ? new Set(params.resourceIds.map(String)) : null;
|
|
809
|
+
const index = new Map();
|
|
810
|
+
params.projection.products.forEach(product => {
|
|
811
|
+
(product.requirementGroups || []).forEach(requirementGroup => {
|
|
812
|
+
const key = [String(product.id), requirementGroup.id].join(':');
|
|
813
|
+
index.set(key, params.projection.resources.filter(resource => matchesRequirementGroup(requirementGroup, resource, product, params.partySize)).filter(resource => !resourceIdSet || resourceIdSet.has(String(resource.id))));
|
|
905
814
|
});
|
|
906
815
|
});
|
|
907
816
|
return index;
|
|
908
817
|
}
|
|
909
818
|
function getSessionRangesForProduct(projection, product, dateRange) {
|
|
910
|
-
const fixedOffsetMinutes = (0,
|
|
911
|
-
const ranges = (product.sessionRanges || []).map(
|
|
912
|
-
var _a;
|
|
819
|
+
const fixedOffsetMinutes = (0, _localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, _localClock.captureRuntimeOffsetMinutes)();
|
|
820
|
+
const ranges = (product.sessionRanges || []).map(range => {
|
|
913
821
|
const normalized = normalizeAbsoluteRange(range, projection.meta.timezone);
|
|
914
|
-
if (!normalized)
|
|
915
|
-
return null;
|
|
822
|
+
if (!normalized) return null;
|
|
916
823
|
const startDate = formatDate(normalized.start, projection.meta.timezone);
|
|
917
824
|
if (startDate < dateRange.startDate || startDate > dateRange.endDate) {
|
|
918
825
|
return null;
|
|
@@ -920,7 +827,7 @@ function getSessionRangesForProduct(projection, product, dateRange) {
|
|
|
920
827
|
return {
|
|
921
828
|
key: `${String(product.id)}:${normalized.start}:${normalized.end}`,
|
|
922
829
|
productId: product.id,
|
|
923
|
-
source:
|
|
830
|
+
source: 'session_schedule',
|
|
924
831
|
startAt: formatDateTime(normalized.start, projection.meta.timezone, fixedOffsetMinutes),
|
|
925
832
|
endAt: formatDateTime(normalized.end, projection.meta.timezone, fixedOffsetMinutes),
|
|
926
833
|
bookingStartAt: formatDateTime(normalized.start, projection.meta.timezone, fixedOffsetMinutes),
|
|
@@ -930,16 +837,11 @@ function getSessionRangesForProduct(projection, product, dateRange) {
|
|
|
930
837
|
endTime: formatTime(normalized.end, projection.meta.timezone),
|
|
931
838
|
timezone: projection.meta.timezone,
|
|
932
839
|
scheduleRefs: range.scheduleRefs || [],
|
|
933
|
-
primaryScheduleRef:
|
|
840
|
+
primaryScheduleRef: range.scheduleRefs?.[0]
|
|
934
841
|
};
|
|
935
842
|
}).filter(Boolean);
|
|
936
|
-
const ruleRanges = materializeScheduleRules(
|
|
937
|
-
|
|
938
|
-
dateRange,
|
|
939
|
-
projection.meta.timezone,
|
|
940
|
-
fixedOffsetMinutes
|
|
941
|
-
);
|
|
942
|
-
return dedupeProductRanges([...ranges, ...ruleRanges.map((range) => ({
|
|
843
|
+
const ruleRanges = materializeScheduleRules(product.sessionRules || [], dateRange, projection.meta.timezone, fixedOffsetMinutes);
|
|
844
|
+
return dedupeProductRanges([...ranges, ...ruleRanges.map(range => ({
|
|
943
845
|
...range,
|
|
944
846
|
key: `${String(product.id)}:${range.startAt}:${range.endAt}`,
|
|
945
847
|
productId: product.id,
|
|
@@ -947,29 +849,30 @@ function getSessionRangesForProduct(projection, product, dateRange) {
|
|
|
947
849
|
}))]);
|
|
948
850
|
}
|
|
949
851
|
function dedupeProductRanges(ranges) {
|
|
950
|
-
const map =
|
|
951
|
-
ranges.forEach(
|
|
852
|
+
const map = new Map();
|
|
853
|
+
ranges.forEach(range => {
|
|
952
854
|
const key = getProductTimeRangeIdentity(range);
|
|
953
855
|
if (!map.has(key)) {
|
|
954
|
-
map.set(key, {
|
|
856
|
+
map.set(key, {
|
|
857
|
+
...range,
|
|
858
|
+
key
|
|
859
|
+
});
|
|
955
860
|
return;
|
|
956
861
|
}
|
|
957
862
|
const existing = map.get(key);
|
|
958
863
|
existing.scheduleRefs = mergeScheduleRefs(existing.scheduleRefs, range.scheduleRefs || []);
|
|
959
|
-
if (!existing.primaryScheduleRef)
|
|
960
|
-
existing.primaryScheduleRef = range.primaryScheduleRef;
|
|
864
|
+
if (!existing.primaryScheduleRef) existing.primaryScheduleRef = range.primaryScheduleRef;
|
|
961
865
|
});
|
|
962
866
|
return Array.from(map.values()).sort((left, right) => left.startAt.localeCompare(right.startAt));
|
|
963
867
|
}
|
|
964
868
|
function getProductTimeRanges(projection, request = {}) {
|
|
965
|
-
var _a;
|
|
966
869
|
const normalizedPartySize = normalizePartySize(request.partySize);
|
|
967
870
|
const dateRange = resolveProjectionDateRange(projection, request.dateRange);
|
|
968
|
-
const fixedOffsetMinutes = (0,
|
|
871
|
+
const fixedOffsetMinutes = (0, _localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, _localClock.captureRuntimeOffsetMinutes)();
|
|
969
872
|
const resourceWindowIndex = buildResourceWindowIndex(projection);
|
|
970
|
-
const filteredProductIdSet =
|
|
971
|
-
return projection.products.filter(
|
|
972
|
-
if (product.kind ===
|
|
873
|
+
const filteredProductIdSet = request.productIds?.length ? new Set(request.productIds.map(String)) : null;
|
|
874
|
+
return projection.products.filter(product => !filteredProductIdSet || filteredProductIdSet.has(String(product.id))).map(product => {
|
|
875
|
+
if (product.kind === 'normal') {
|
|
973
876
|
return {
|
|
974
877
|
productId: product.id,
|
|
975
878
|
title: product.title,
|
|
@@ -977,7 +880,7 @@ function getProductTimeRanges(projection, request = {}) {
|
|
|
977
880
|
ranges: []
|
|
978
881
|
};
|
|
979
882
|
}
|
|
980
|
-
if (product.kind ===
|
|
883
|
+
if (product.kind === 'session' || product.kind === 'venue_slot') {
|
|
981
884
|
return {
|
|
982
885
|
productId: product.id,
|
|
983
886
|
title: product.title,
|
|
@@ -985,9 +888,9 @@ function getProductTimeRanges(projection, request = {}) {
|
|
|
985
888
|
ranges: getSessionRangesForProduct(projection, product, dateRange)
|
|
986
889
|
};
|
|
987
890
|
}
|
|
988
|
-
const requiredGroups = (product.requirementGroups || []).filter(
|
|
891
|
+
const requiredGroups = (product.requirementGroups || []).filter(group => group.required !== false);
|
|
989
892
|
const operatingRanges = buildOperatingRanges(projection, dateRange);
|
|
990
|
-
const resourceRanges = requiredGroups.length ? requiredGroups.map(
|
|
893
|
+
const resourceRanges = requiredGroups.length ? requiredGroups.map(group => buildGroupWorkingRanges({
|
|
991
894
|
projection,
|
|
992
895
|
product,
|
|
993
896
|
requirementGroup: group,
|
|
@@ -995,32 +898,27 @@ function getProductTimeRanges(projection, request = {}) {
|
|
|
995
898
|
resourceIds: request.resourceIds,
|
|
996
899
|
partySize: normalizedPartySize,
|
|
997
900
|
resourceWindowIndex
|
|
998
|
-
})).reduce((current,
|
|
901
|
+
})).reduce((current, ranges) => current === null ? ranges : intersectRangeLists(current, ranges), null) || [] : null;
|
|
999
902
|
const baseRanges = resourceRanges === null ? operatingRanges : intersectRangeLists(resourceRanges, operatingRanges);
|
|
1000
903
|
const durationMinutes = Math.max(1, Number(product.durationMinutes || 30) || 30);
|
|
1001
904
|
const timeSliceMinutes = projection.meta.defaults.slotStepMinutes;
|
|
1002
|
-
const durationAnchor = ceilToTenMinutes(
|
|
1003
|
-
toDateTime(projection.meta.durationCandidateAnchorAt, projection.meta.timezone),
|
|
1004
|
-
projection.meta.timezone
|
|
1005
|
-
);
|
|
905
|
+
const durationAnchor = ceilToTenMinutes(toDateTime(projection.meta.durationCandidateAnchorAt, projection.meta.timezone), projection.meta.timezone);
|
|
1006
906
|
const ranges = [];
|
|
1007
|
-
baseRanges.forEach(
|
|
1008
|
-
if (range.end <= durationAnchor)
|
|
1009
|
-
return;
|
|
907
|
+
baseRanges.forEach(range => {
|
|
908
|
+
if (range.end <= durationAnchor) return;
|
|
1010
909
|
let cursor = range.start;
|
|
1011
910
|
if (cursor < durationAnchor) {
|
|
1012
911
|
cursor = durationAnchor;
|
|
1013
912
|
}
|
|
1014
913
|
while (true) {
|
|
1015
914
|
const bookingEnd = addMinutesZoned(cursor, durationMinutes, projection.meta.timezone);
|
|
1016
|
-
if (bookingEnd > range.end)
|
|
1017
|
-
break;
|
|
915
|
+
if (bookingEnd > range.end) break;
|
|
1018
916
|
const bookingDate = formatDate(cursor);
|
|
1019
917
|
if (bookingDate >= dateRange.startDate && bookingDate <= dateRange.endDate) {
|
|
1020
918
|
ranges.push({
|
|
1021
919
|
key: `${String(product.id)}:${cursor}:${bookingEnd}`,
|
|
1022
920
|
productId: product.id,
|
|
1023
|
-
source:
|
|
921
|
+
source: 'duration_window',
|
|
1024
922
|
startAt: formatDateTime(cursor, projection.meta.timezone, fixedOffsetMinutes),
|
|
1025
923
|
endAt: formatDateTime(bookingEnd, projection.meta.timezone, fixedOffsetMinutes),
|
|
1026
924
|
bookingStartAt: formatDateTime(cursor, projection.meta.timezone, fixedOffsetMinutes),
|
|
@@ -1058,61 +956,42 @@ function aggregateMatchedCells(productId, requirementGroupId, resourceId, candid
|
|
|
1058
956
|
}
|
|
1059
957
|
const overlapped = [];
|
|
1060
958
|
for (let index = Math.max(0, left - 1); index < indexedCells.length; index += 1) {
|
|
1061
|
-
const {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
959
|
+
const {
|
|
960
|
+
cell,
|
|
961
|
+
range
|
|
962
|
+
} = indexedCells[index];
|
|
963
|
+
if (range.startMs >= candidateEndMs) break;
|
|
964
|
+
if (range.endMs <= candidateStartMs) continue;
|
|
1066
965
|
const clipped = clipRange(range, candidateRange);
|
|
1067
|
-
if (clipped)
|
|
1068
|
-
|
|
966
|
+
if (clipped) overlapped.push({
|
|
967
|
+
cell,
|
|
968
|
+
range: clipped
|
|
969
|
+
});
|
|
1069
970
|
}
|
|
1070
|
-
if (!overlapped.length)
|
|
1071
|
-
return void 0;
|
|
971
|
+
if (!overlapped.length) return undefined;
|
|
1072
972
|
let cursor = candidateRange.start;
|
|
1073
973
|
for (const item of overlapped) {
|
|
1074
|
-
if (item.range.start > cursor)
|
|
1075
|
-
return void 0;
|
|
974
|
+
if (item.range.start > cursor) return undefined;
|
|
1076
975
|
if (item.range.end > cursor) {
|
|
1077
976
|
cursor = item.range.end;
|
|
1078
977
|
}
|
|
1079
|
-
if (cursor >= candidateRange.end)
|
|
1080
|
-
break;
|
|
978
|
+
if (cursor >= candidateRange.end) break;
|
|
1081
979
|
}
|
|
1082
|
-
if (cursor < candidateRange.end)
|
|
1083
|
-
|
|
1084
|
-
const
|
|
1085
|
-
const
|
|
1086
|
-
const nonPastStatuses = statuses.filter((status) => status !== "past");
|
|
980
|
+
if (cursor < candidateRange.end) return undefined;
|
|
981
|
+
const cells = overlapped.map(item => item.cell);
|
|
982
|
+
const statuses = cells.map(cell => cell.status);
|
|
983
|
+
const nonPastStatuses = statuses.filter(status => status !== 'past');
|
|
1087
984
|
const aggregatedStatus = (() => {
|
|
1088
|
-
if (nonPastStatuses.some(
|
|
1089
|
-
|
|
1090
|
-
if (nonPastStatuses.some(
|
|
1091
|
-
|
|
1092
|
-
if (
|
|
1093
|
-
|
|
1094
|
-
if (nonPastStatuses.every((status) => status === "available"))
|
|
1095
|
-
return "available";
|
|
1096
|
-
if (statuses.every((status) => status === "past"))
|
|
1097
|
-
return "past";
|
|
1098
|
-
return "available";
|
|
985
|
+
if (nonPastStatuses.some(status => status === 'unavailable')) return 'unavailable';
|
|
986
|
+
if (nonPastStatuses.some(status => status === 'full')) return 'full';
|
|
987
|
+
if (nonPastStatuses.some(status => status === 'limited')) return 'limited';
|
|
988
|
+
if (nonPastStatuses.every(status => status === 'available')) return 'available';
|
|
989
|
+
if (statuses.every(status => status === 'past')) return 'past';
|
|
990
|
+
return 'available';
|
|
1099
991
|
})();
|
|
1100
|
-
const capacityCells = cells.filter(
|
|
1101
|
-
const reasonCodes = Array.from(new Set(cells.flatMap(
|
|
1102
|
-
const occupancyRefs = Array.from(new Map(
|
|
1103
|
-
cells.flatMap((cell) => cell.occupancyRefs || []).map((ref) => [
|
|
1104
|
-
[
|
|
1105
|
-
ref.id ?? "",
|
|
1106
|
-
ref.resourceId ?? "",
|
|
1107
|
-
ref.serverEventId ?? "",
|
|
1108
|
-
ref.bookingUid ?? "",
|
|
1109
|
-
ref.productUid ?? "",
|
|
1110
|
-
ref.scheduleEventId ?? "",
|
|
1111
|
-
ref.source ?? ""
|
|
1112
|
-
].join(":"),
|
|
1113
|
-
ref
|
|
1114
|
-
])
|
|
1115
|
-
).values());
|
|
992
|
+
const capacityCells = cells.filter(cell => cell.remainingCapacity !== undefined || cell.maxCapacity !== undefined);
|
|
993
|
+
const reasonCodes = Array.from(new Set(cells.flatMap(cell => cell.reasonCodes)));
|
|
994
|
+
const occupancyRefs = Array.from(new Map(cells.flatMap(cell => cell.occupancyRefs || []).map(ref => [[ref.id ?? '', ref.resourceId ?? '', ref.serverEventId ?? '', ref.bookingUid ?? '', ref.productUid ?? '', ref.scheduleEventId ?? '', ref.source ?? ''].join(':'), ref])).values());
|
|
1116
995
|
const firstCell = cells[0];
|
|
1117
996
|
return {
|
|
1118
997
|
...firstCell,
|
|
@@ -1123,22 +1002,14 @@ function aggregateMatchedCells(productId, requirementGroupId, resourceId, candid
|
|
|
1123
1002
|
start: candidateRange.start,
|
|
1124
1003
|
end: candidateRange.end
|
|
1125
1004
|
}),
|
|
1126
|
-
startAt: formatDateTime(
|
|
1127
|
-
|
|
1128
|
-
firstCell.timezone,
|
|
1129
|
-
(0, import_localClock.extractFixedOffsetMinutes)(firstCell.startAt) ?? (0, import_localClock.captureRuntimeOffsetMinutes)()
|
|
1130
|
-
),
|
|
1131
|
-
endAt: formatDateTime(
|
|
1132
|
-
candidateRange.end,
|
|
1133
|
-
firstCell.timezone,
|
|
1134
|
-
(0, import_localClock.extractFixedOffsetMinutes)(firstCell.startAt) ?? (0, import_localClock.captureRuntimeOffsetMinutes)()
|
|
1135
|
-
),
|
|
1005
|
+
startAt: formatDateTime(candidateRange.start, firstCell.timezone, (0, _localClock.extractFixedOffsetMinutes)(firstCell.startAt) ?? (0, _localClock.captureRuntimeOffsetMinutes)()),
|
|
1006
|
+
endAt: formatDateTime(candidateRange.end, firstCell.timezone, (0, _localClock.extractFixedOffsetMinutes)(firstCell.startAt) ?? (0, _localClock.captureRuntimeOffsetMinutes)()),
|
|
1136
1007
|
date: formatDate(candidateRange.start, firstCell.timezone),
|
|
1137
1008
|
startTime: formatTime(candidateRange.start, firstCell.timezone),
|
|
1138
1009
|
endTime: formatTime(candidateRange.end, firstCell.timezone),
|
|
1139
1010
|
status: aggregatedStatus,
|
|
1140
|
-
remainingCapacity: capacityCells.length ? Math.min(...capacityCells.map(
|
|
1141
|
-
maxCapacity: capacityCells.length ? Math.min(...capacityCells.map(
|
|
1011
|
+
remainingCapacity: capacityCells.length ? Math.min(...capacityCells.map(cell => cell.remainingCapacity ?? Number.MAX_SAFE_INTEGER)) : undefined,
|
|
1012
|
+
maxCapacity: capacityCells.length ? Math.min(...capacityCells.map(cell => cell.maxCapacity ?? Number.MAX_SAFE_INTEGER)) : undefined,
|
|
1142
1013
|
reasonCodes,
|
|
1143
1014
|
occupancyRefs
|
|
1144
1015
|
};
|
|
@@ -1159,54 +1030,44 @@ function aggregateMatchedCellSummary(candidateRange, indexedCells) {
|
|
|
1159
1030
|
const cells = [];
|
|
1160
1031
|
let cursorMs = candidateStartMs;
|
|
1161
1032
|
for (let index = Math.max(0, left - 1); index < indexedCells.length; index += 1) {
|
|
1162
|
-
const {
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1033
|
+
const {
|
|
1034
|
+
cell,
|
|
1035
|
+
range
|
|
1036
|
+
} = indexedCells[index];
|
|
1037
|
+
if (range.startMs >= candidateEndMs) break;
|
|
1038
|
+
if (range.endMs <= candidateStartMs) continue;
|
|
1167
1039
|
const clippedStartMs = Math.max(range.startMs, candidateStartMs);
|
|
1168
1040
|
const clippedEndMs = Math.min(range.endMs, candidateEndMs);
|
|
1169
|
-
if (clippedEndMs <= clippedStartMs)
|
|
1170
|
-
|
|
1171
|
-
if (clippedStartMs > cursorMs)
|
|
1172
|
-
return void 0;
|
|
1041
|
+
if (clippedEndMs <= clippedStartMs) continue;
|
|
1042
|
+
if (clippedStartMs > cursorMs) return undefined;
|
|
1173
1043
|
cells.push(cell);
|
|
1174
|
-
if (clippedEndMs > cursorMs)
|
|
1175
|
-
|
|
1176
|
-
if (cursorMs >= candidateEndMs)
|
|
1177
|
-
break;
|
|
1044
|
+
if (clippedEndMs > cursorMs) cursorMs = clippedEndMs;
|
|
1045
|
+
if (cursorMs >= candidateEndMs) break;
|
|
1178
1046
|
}
|
|
1179
|
-
if (!cells.length || cursorMs < candidateEndMs)
|
|
1180
|
-
|
|
1181
|
-
const
|
|
1182
|
-
const nonPastStatuses = statuses.filter((status2) => status2 !== "past");
|
|
1047
|
+
if (!cells.length || cursorMs < candidateEndMs) return undefined;
|
|
1048
|
+
const statuses = cells.map(cell => cell.status);
|
|
1049
|
+
const nonPastStatuses = statuses.filter(status => status !== 'past');
|
|
1183
1050
|
const status = (() => {
|
|
1184
|
-
if (nonPastStatuses.some(
|
|
1185
|
-
|
|
1186
|
-
if (nonPastStatuses.some(
|
|
1187
|
-
|
|
1188
|
-
if (
|
|
1189
|
-
|
|
1190
|
-
if (nonPastStatuses.every((cellStatus) => cellStatus === "available"))
|
|
1191
|
-
return "available";
|
|
1192
|
-
if (statuses.every((cellStatus) => cellStatus === "past"))
|
|
1193
|
-
return "past";
|
|
1194
|
-
return "available";
|
|
1051
|
+
if (nonPastStatuses.some(cellStatus => cellStatus === 'unavailable')) return 'unavailable';
|
|
1052
|
+
if (nonPastStatuses.some(cellStatus => cellStatus === 'full')) return 'full';
|
|
1053
|
+
if (nonPastStatuses.some(cellStatus => cellStatus === 'limited')) return 'limited';
|
|
1054
|
+
if (nonPastStatuses.every(cellStatus => cellStatus === 'available')) return 'available';
|
|
1055
|
+
if (statuses.every(cellStatus => cellStatus === 'past')) return 'past';
|
|
1056
|
+
return 'available';
|
|
1195
1057
|
})();
|
|
1196
|
-
const capacityCells = cells.filter(
|
|
1058
|
+
const capacityCells = cells.filter(cell => cell.remainingCapacity !== undefined || cell.maxCapacity !== undefined);
|
|
1197
1059
|
return {
|
|
1198
1060
|
status,
|
|
1199
|
-
remainingCapacity: capacityCells.length ? Math.min(...capacityCells.map(
|
|
1200
|
-
maxCapacity: capacityCells.length ? Math.min(...capacityCells.map(
|
|
1201
|
-
reasonCodes: Array.from(new Set(cells.flatMap(
|
|
1061
|
+
remainingCapacity: capacityCells.length ? Math.min(...capacityCells.map(cell => cell.remainingCapacity ?? Number.MAX_SAFE_INTEGER)) : undefined,
|
|
1062
|
+
maxCapacity: capacityCells.length ? Math.min(...capacityCells.map(cell => cell.maxCapacity ?? Number.MAX_SAFE_INTEGER)) : undefined,
|
|
1063
|
+
reasonCodes: Array.from(new Set(cells.flatMap(cell => cell.reasonCodes)))
|
|
1202
1064
|
};
|
|
1203
1065
|
}
|
|
1204
1066
|
function buildAvailabilityCellIndex(projection) {
|
|
1205
|
-
const index =
|
|
1206
|
-
projection.cells.forEach(
|
|
1207
|
-
if (cell.requirementGroupId == null || cell.resourceId == null)
|
|
1208
|
-
|
|
1209
|
-
const key = [String(cell.productId), cell.requirementGroupId, String(cell.resourceId)].join(":");
|
|
1067
|
+
const index = new Map();
|
|
1068
|
+
projection.cells.forEach(cell => {
|
|
1069
|
+
if (cell.requirementGroupId == null || cell.resourceId == null) return;
|
|
1070
|
+
const key = [String(cell.productId), cell.requirementGroupId, String(cell.resourceId)].join(':');
|
|
1210
1071
|
const entries = index.get(key) || [];
|
|
1211
1072
|
entries.push({
|
|
1212
1073
|
cell,
|
|
@@ -1223,35 +1084,27 @@ function buildAvailabilityCellIndex(projection) {
|
|
|
1223
1084
|
return index;
|
|
1224
1085
|
}
|
|
1225
1086
|
function buildEvaluationContext(params) {
|
|
1226
|
-
var _a;
|
|
1227
1087
|
const evaluatedAt = toDateTime(params.evaluatedAt || params.projection.meta.now, params.projection.meta.timezone);
|
|
1228
|
-
const selectionsByGroupKey =
|
|
1229
|
-
(params.requirementSelections || []).forEach(
|
|
1230
|
-
const key = selection.requirementGroupId || String(selection.formId ??
|
|
1088
|
+
const selectionsByGroupKey = new Map();
|
|
1089
|
+
(params.requirementSelections || []).forEach(selection => {
|
|
1090
|
+
const key = selection.requirementGroupId || String(selection.formId ?? '');
|
|
1231
1091
|
selectionsByGroupKey.set(key, selection);
|
|
1232
1092
|
});
|
|
1233
1093
|
return {
|
|
1234
1094
|
evaluatedAt,
|
|
1235
1095
|
evaluatedAtMs: evaluatedAt,
|
|
1236
|
-
resourceIdSet:
|
|
1096
|
+
resourceIdSet: params.resourceIds?.length ? new Set(params.resourceIds.map(String)) : null,
|
|
1237
1097
|
selectionsByGroupKey
|
|
1238
1098
|
};
|
|
1239
1099
|
}
|
|
1240
1100
|
function summarizeStatuses(statuses) {
|
|
1241
|
-
if (!statuses.length)
|
|
1242
|
-
|
|
1243
|
-
if (statuses.
|
|
1244
|
-
|
|
1245
|
-
if (statuses.some(
|
|
1246
|
-
|
|
1247
|
-
if (statuses.some(
|
|
1248
|
-
return "full";
|
|
1249
|
-
if (statuses.some((status) => status === "limited"))
|
|
1250
|
-
return "limited";
|
|
1251
|
-
if (statuses.every((status) => status === "available"))
|
|
1252
|
-
return "available";
|
|
1253
|
-
if (statuses.some((status) => status === "past"))
|
|
1254
|
-
return "past";
|
|
1101
|
+
if (!statuses.length) return 'unavailable';
|
|
1102
|
+
if (statuses.every(status => status === 'past')) return 'past';
|
|
1103
|
+
if (statuses.some(status => status === 'unavailable')) return 'unavailable';
|
|
1104
|
+
if (statuses.some(status => status === 'full')) return 'full';
|
|
1105
|
+
if (statuses.some(status => status === 'limited')) return 'limited';
|
|
1106
|
+
if (statuses.every(status => status === 'available')) return 'available';
|
|
1107
|
+
if (statuses.some(status => status === 'past')) return 'past';
|
|
1255
1108
|
return statuses[0];
|
|
1256
1109
|
}
|
|
1257
1110
|
function createStatusAggregate() {
|
|
@@ -1265,83 +1118,72 @@ function createStatusAggregate() {
|
|
|
1265
1118
|
}
|
|
1266
1119
|
function pushStatusAggregate(aggregate, status) {
|
|
1267
1120
|
aggregate.count += 1;
|
|
1268
|
-
if (status !==
|
|
1269
|
-
|
|
1270
|
-
if (status ===
|
|
1271
|
-
|
|
1272
|
-
if (status === "limited")
|
|
1273
|
-
aggregate.hasLimited = true;
|
|
1274
|
-
if (status === "full")
|
|
1275
|
-
aggregate.hasFull = true;
|
|
1121
|
+
if (status !== 'past') aggregate.allPast = false;
|
|
1122
|
+
if (status === 'available') aggregate.hasAvailable = true;
|
|
1123
|
+
if (status === 'limited') aggregate.hasLimited = true;
|
|
1124
|
+
if (status === 'full') aggregate.hasFull = true;
|
|
1276
1125
|
}
|
|
1277
1126
|
function summarizeStatusAggregate(aggregate) {
|
|
1278
|
-
if (aggregate.count === 0)
|
|
1279
|
-
|
|
1280
|
-
if (aggregate.
|
|
1281
|
-
|
|
1282
|
-
if (aggregate.
|
|
1283
|
-
|
|
1284
|
-
if (aggregate.hasFull)
|
|
1285
|
-
return "full";
|
|
1286
|
-
if (aggregate.allPast)
|
|
1287
|
-
return "past";
|
|
1288
|
-
return "unavailable";
|
|
1127
|
+
if (aggregate.count === 0) return 'unavailable';
|
|
1128
|
+
if (aggregate.hasAvailable) return 'available';
|
|
1129
|
+
if (aggregate.hasLimited) return 'limited';
|
|
1130
|
+
if (aggregate.hasFull) return 'full';
|
|
1131
|
+
if (aggregate.allPast) return 'past';
|
|
1132
|
+
return 'unavailable';
|
|
1289
1133
|
}
|
|
1290
1134
|
function evaluateMatchedResourceSummary(params) {
|
|
1291
1135
|
const isPast = params.candidateRange.endMs <= params.evaluationContext.evaluatedAtMs;
|
|
1292
1136
|
if (!params.matchedCell) {
|
|
1293
1137
|
return {
|
|
1294
|
-
status: isPast ?
|
|
1138
|
+
status: isPast ? 'past' : 'unavailable',
|
|
1295
1139
|
maxPartySize: 0,
|
|
1296
1140
|
remainingCapacity: 0,
|
|
1297
1141
|
maxCapacity: 0,
|
|
1298
|
-
reasonCodes: isPast ? [
|
|
1142
|
+
reasonCodes: isPast ? ['past'] : ['outside_resource_window']
|
|
1299
1143
|
};
|
|
1300
1144
|
}
|
|
1301
1145
|
const remainingCapacity = Math.max(0, params.matchedCell.remainingCapacity || 0);
|
|
1302
1146
|
const maxCapacity = Math.max(0, params.matchedCell.maxCapacity ?? params.resourceCapacity ?? 0);
|
|
1303
1147
|
const maxPartySize = isPast ? 0 : remainingCapacity;
|
|
1304
1148
|
const status = (() => {
|
|
1305
|
-
if (isPast)
|
|
1306
|
-
|
|
1307
|
-
if (params.
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
return "full";
|
|
1311
|
-
if (remainingCapacity < maxCapacity)
|
|
1312
|
-
return "limited";
|
|
1313
|
-
return "available";
|
|
1149
|
+
if (isPast) return 'past';
|
|
1150
|
+
if (params.matchedCell.status === 'unavailable') return 'unavailable';
|
|
1151
|
+
if (remainingCapacity < params.requiredCapacity) return 'full';
|
|
1152
|
+
if (remainingCapacity < maxCapacity) return 'limited';
|
|
1153
|
+
return 'available';
|
|
1314
1154
|
})();
|
|
1315
1155
|
return {
|
|
1316
1156
|
status,
|
|
1317
1157
|
remainingCapacity,
|
|
1318
1158
|
maxCapacity,
|
|
1319
1159
|
maxPartySize,
|
|
1320
|
-
reasonCodes: isPast ? Array.from(
|
|
1160
|
+
reasonCodes: isPast ? Array.from(new Set([...(params.matchedCell.reasonCodes || []), 'past'])) : params.matchedCell.reasonCodes
|
|
1321
1161
|
};
|
|
1322
1162
|
}
|
|
1323
1163
|
function evaluateRequirementGroupSummary(params) {
|
|
1324
|
-
|
|
1325
|
-
|
|
1164
|
+
const {
|
|
1165
|
+
projection,
|
|
1166
|
+
product,
|
|
1167
|
+
requirementGroup,
|
|
1168
|
+
candidateRange,
|
|
1169
|
+
requestedPartySize,
|
|
1170
|
+
evaluationContext
|
|
1171
|
+
} = params;
|
|
1326
1172
|
const required = requirementGroup.required !== false;
|
|
1327
1173
|
const minimum = Math.max(required ? 1 : 0, Number(requirementGroup.min ?? (required ? 1 : 0)) || 0);
|
|
1328
1174
|
const maximum = Math.max(minimum, Number(requirementGroup.max ?? Math.max(minimum, 1)) || Math.max(minimum, 1));
|
|
1329
|
-
const matchedResources =
|
|
1175
|
+
const matchedResources = params.groupResourceIndex?.get([String(product.id), requirementGroup.id].join(':')) || projection.resources.filter(resource => matchesRequirementGroup(requirementGroup, resource, product, requestedPartySize ?? 1)).filter(resource => !evaluationContext.resourceIdSet || evaluationContext.resourceIdSet.has(String(resource.id)));
|
|
1330
1176
|
const requiredCapacity = resolveRequiredCapacity(requirementGroup, product, requestedPartySize);
|
|
1331
1177
|
const optionStatusAggregate = createStatusAggregate();
|
|
1332
|
-
const reasonCodeSet =
|
|
1178
|
+
const reasonCodeSet = new Set();
|
|
1333
1179
|
let totalResourceCount = 0;
|
|
1334
1180
|
let availableResourceCount = 0;
|
|
1335
1181
|
let groupMaxPartySize = 0;
|
|
1336
1182
|
let groupRemainingCapacity = 0;
|
|
1337
1183
|
let groupMaxCapacity = 0;
|
|
1338
|
-
matchedResources.forEach(
|
|
1339
|
-
var _a2;
|
|
1184
|
+
matchedResources.forEach(resource => {
|
|
1340
1185
|
totalResourceCount += 1;
|
|
1341
|
-
const matchedCell = aggregateMatchedCellSummary(
|
|
1342
|
-
candidateRange,
|
|
1343
|
-
((_a2 = params.cellIndex) == null ? void 0 : _a2.get([String(product.id), requirementGroup.id, String(resource.id)].join(":"))) || []
|
|
1344
|
-
);
|
|
1186
|
+
const matchedCell = aggregateMatchedCellSummary(candidateRange, params.cellIndex?.get([String(product.id), requirementGroup.id, String(resource.id)].join(':')) || []);
|
|
1345
1187
|
const option = evaluateMatchedResourceSummary({
|
|
1346
1188
|
matchedCell,
|
|
1347
1189
|
resourceCapacity: resource.capacity,
|
|
@@ -1350,23 +1192,23 @@ function evaluateRequirementGroupSummary(params) {
|
|
|
1350
1192
|
evaluationContext
|
|
1351
1193
|
});
|
|
1352
1194
|
pushStatusAggregate(optionStatusAggregate, option.status);
|
|
1353
|
-
option.reasonCodes.forEach(
|
|
1195
|
+
option.reasonCodes.forEach(reasonCode => reasonCodeSet.add(reasonCode));
|
|
1354
1196
|
groupRemainingCapacity = Math.max(groupRemainingCapacity, option.remainingCapacity);
|
|
1355
1197
|
groupMaxCapacity = Math.max(groupMaxCapacity, option.maxCapacity);
|
|
1356
|
-
if (option.status ===
|
|
1198
|
+
if (option.status === 'available' || option.status === 'limited') {
|
|
1357
1199
|
availableResourceCount += 1;
|
|
1358
1200
|
groupMaxPartySize = Math.max(groupMaxPartySize, option.maxPartySize);
|
|
1359
1201
|
}
|
|
1360
1202
|
});
|
|
1361
|
-
let status =
|
|
1203
|
+
let status = 'available';
|
|
1362
1204
|
if (candidateRange.endMs <= evaluationContext.evaluatedAtMs) {
|
|
1363
|
-
status =
|
|
1205
|
+
status = 'past';
|
|
1364
1206
|
} else if (availableResourceCount < minimum) {
|
|
1365
|
-
status = optionStatusAggregate.hasAvailable || optionStatusAggregate.hasLimited || optionStatusAggregate.hasFull ?
|
|
1207
|
+
status = optionStatusAggregate.hasAvailable || optionStatusAggregate.hasLimited || optionStatusAggregate.hasFull ? 'full' : 'unavailable';
|
|
1366
1208
|
} else if (availableResourceCount === totalResourceCount && !optionStatusAggregate.hasLimited && !optionStatusAggregate.hasFull && !optionStatusAggregate.allPast) {
|
|
1367
|
-
status =
|
|
1209
|
+
status = 'available';
|
|
1368
1210
|
} else {
|
|
1369
|
-
status =
|
|
1211
|
+
status = 'limited';
|
|
1370
1212
|
}
|
|
1371
1213
|
return {
|
|
1372
1214
|
required,
|
|
@@ -1380,20 +1222,19 @@ function evaluateRequirementGroupSummary(params) {
|
|
|
1380
1222
|
};
|
|
1381
1223
|
}
|
|
1382
1224
|
function evaluateCandidateSummary(projection, candidate, request) {
|
|
1383
|
-
|
|
1384
|
-
const product = ((_a = request.productIndex) == null ? void 0 : _a.get(String(candidate.productId))) || projection.products.find((item) => sameId(item.id, candidate.productId));
|
|
1225
|
+
const product = request.productIndex?.get(String(candidate.productId)) || projection.products.find(item => sameId(item.id, candidate.productId));
|
|
1385
1226
|
if (!product) {
|
|
1386
|
-
throw availabilityError(
|
|
1227
|
+
throw availabilityError('INVALID_TARGET_FILTER', 'candidate.productId 不存在于 projection.products', {
|
|
1387
1228
|
candidate
|
|
1388
1229
|
});
|
|
1389
1230
|
}
|
|
1390
1231
|
const candidateRangeCacheKey = getCandidateRangeCacheKey(candidate);
|
|
1391
|
-
const candidateRange =
|
|
1232
|
+
const candidateRange = request.candidateRangeCache?.get(candidateRangeCacheKey) || (() => {
|
|
1392
1233
|
const normalized = normalizeAbsoluteRange(getCandidateBookingRange(candidate), projection.meta.timezone);
|
|
1393
1234
|
return normalized ? toIndexedRange(normalized) : null;
|
|
1394
1235
|
})();
|
|
1395
1236
|
if (!candidateRange) {
|
|
1396
|
-
throw availabilityError(
|
|
1237
|
+
throw availabilityError('INVALID_TARGET_FILTER', 'candidate 时间范围非法', {
|
|
1397
1238
|
candidate
|
|
1398
1239
|
});
|
|
1399
1240
|
}
|
|
@@ -1406,7 +1247,7 @@ function evaluateCandidateSummary(projection, candidate, request) {
|
|
|
1406
1247
|
resourceIds: request.resourceIds
|
|
1407
1248
|
});
|
|
1408
1249
|
const requestedPartySize = normalizePartySize(request.partySize);
|
|
1409
|
-
const requiredGroups = (product.requirementGroups || []).filter(
|
|
1250
|
+
const requiredGroups = (product.requirementGroups || []).filter(group => group.required !== false).map(requirementGroup => evaluateRequirementGroupSummary({
|
|
1410
1251
|
projection,
|
|
1411
1252
|
product,
|
|
1412
1253
|
requirementGroup,
|
|
@@ -1422,34 +1263,33 @@ function evaluateCandidateSummary(projection, candidate, request) {
|
|
|
1422
1263
|
evaluationContext,
|
|
1423
1264
|
timezoneName: projection.meta.timezone
|
|
1424
1265
|
});
|
|
1425
|
-
const candidateStatus = requiredGroups.length ? summarizeStatuses(requiredGroups.map(
|
|
1266
|
+
const candidateStatus = requiredGroups.length ? summarizeStatuses(requiredGroups.map(group => group.status)) : candidateRange.endMs <= evaluationContext.evaluatedAtMs ? 'past' : 'available';
|
|
1426
1267
|
const maxPartySize = requiredGroups.length ? requiredGroups.reduce((min, group) => Math.min(min, group.maxPartySize), Number.MAX_SAFE_INTEGER) : 0;
|
|
1427
1268
|
const remainingCapacity = requiredGroups.length ? requiredGroups.reduce((min, group) => Math.min(min, group.remainingCapacity), Number.MAX_SAFE_INTEGER) : 0;
|
|
1428
1269
|
const maxCapacity = requiredGroups.length ? requiredGroups.reduce((min, group) => Math.min(min, group.maxCapacity), Number.MAX_SAFE_INTEGER) : 0;
|
|
1429
|
-
const status =
|
|
1270
|
+
const status = gate?.status || candidateStatus;
|
|
1430
1271
|
return {
|
|
1431
1272
|
status,
|
|
1432
1273
|
maxPartySize: gate ? 0 : Number.isFinite(maxPartySize) ? maxPartySize : 0,
|
|
1433
|
-
remainingCapacity: status ===
|
|
1274
|
+
remainingCapacity: status === 'past' || status === 'unavailable' ? 0 : Number.isFinite(remainingCapacity) ? remainingCapacity : 0,
|
|
1434
1275
|
maxCapacity: Number.isFinite(maxCapacity) ? maxCapacity : 0,
|
|
1435
|
-
reasonCodes:
|
|
1276
|
+
reasonCodes: gate?.reasonCodes || Array.from(new Set(requiredGroups.flatMap(group => group.reasonCodes)))
|
|
1436
1277
|
};
|
|
1437
1278
|
}
|
|
1438
1279
|
function evaluateCandidate(projection, candidate, request) {
|
|
1439
|
-
|
|
1440
|
-
const product = ((_a = request.productIndex) == null ? void 0 : _a.get(String(candidate.productId))) || projection.products.find((item) => sameId(item.id, candidate.productId));
|
|
1280
|
+
const product = request.productIndex?.get(String(candidate.productId)) || projection.products.find(item => sameId(item.id, candidate.productId));
|
|
1441
1281
|
if (!product) {
|
|
1442
|
-
throw availabilityError(
|
|
1282
|
+
throw availabilityError('INVALID_TARGET_FILTER', 'candidate.productId 不存在于 projection.products', {
|
|
1443
1283
|
candidate
|
|
1444
1284
|
});
|
|
1445
1285
|
}
|
|
1446
1286
|
const candidateRangeCacheKey = getCandidateRangeCacheKey(candidate);
|
|
1447
|
-
const candidateRange =
|
|
1287
|
+
const candidateRange = request.candidateRangeCache?.get(candidateRangeCacheKey) || (() => {
|
|
1448
1288
|
const normalized = normalizeAbsoluteRange(getCandidateBookingRange(candidate), projection.meta.timezone);
|
|
1449
1289
|
return normalized ? toIndexedRange(normalized) : null;
|
|
1450
1290
|
})();
|
|
1451
1291
|
if (!candidateRange) {
|
|
1452
|
-
throw availabilityError(
|
|
1292
|
+
throw availabilityError('INVALID_TARGET_FILTER', 'candidate 时间范围非法', {
|
|
1453
1293
|
candidate
|
|
1454
1294
|
});
|
|
1455
1295
|
}
|
|
@@ -1465,79 +1305,64 @@ function evaluateCandidate(projection, candidate, request) {
|
|
|
1465
1305
|
const requestedPartySize = normalizePartySize(request.partySize);
|
|
1466
1306
|
const partySize = requestedPartySize ?? 1;
|
|
1467
1307
|
const selectionsByGroupKey = evaluationContext.selectionsByGroupKey;
|
|
1468
|
-
const requirementGroups = (product.requirementGroups || []).map(
|
|
1469
|
-
var _a2;
|
|
1308
|
+
const requirementGroups = (product.requirementGroups || []).map(requirementGroup => {
|
|
1470
1309
|
const required = requirementGroup.required !== false;
|
|
1471
1310
|
const minimum = Math.max(required ? 1 : 0, Number(requirementGroup.min ?? (required ? 1 : 0)) || 0);
|
|
1472
1311
|
const maximum = Math.max(minimum, Number(requirementGroup.max ?? Math.max(minimum, 1)) || Math.max(minimum, 1));
|
|
1473
|
-
const selection = selectionsByGroupKey.get(requirementGroup.id) || selectionsByGroupKey.get(String(requirementGroup.formId ??
|
|
1474
|
-
const matchedResources = (
|
|
1475
|
-
const options = matchedResources.map(
|
|
1476
|
-
|
|
1477
|
-
const matchedCell = aggregateMatchedCells(
|
|
1478
|
-
product.id,
|
|
1479
|
-
requirementGroup.id,
|
|
1480
|
-
resource.id,
|
|
1481
|
-
candidateRange,
|
|
1482
|
-
((_a3 = request.cellIndex) == null ? void 0 : _a3.get([String(product.id), requirementGroup.id, String(resource.id)].join(":"))) || []
|
|
1483
|
-
);
|
|
1312
|
+
const selection = selectionsByGroupKey.get(requirementGroup.id) || selectionsByGroupKey.get(String(requirementGroup.formId ?? ''));
|
|
1313
|
+
const matchedResources = (request.groupResourceIndex?.get([String(product.id), requirementGroup.id].join(':')) || projection.resources.filter(resource => matchesRequirementGroup(requirementGroup, resource, product, partySize)).filter(resource => !evaluationContext.resourceIdSet || evaluationContext.resourceIdSet.has(String(resource.id)))).filter(resource => !selection || selection.resourceIds.some(resourceId => sameId(resourceId, resource.id)));
|
|
1314
|
+
const options = matchedResources.map(resource => {
|
|
1315
|
+
const matchedCell = aggregateMatchedCells(product.id, requirementGroup.id, resource.id, candidateRange, request.cellIndex?.get([String(product.id), requirementGroup.id, String(resource.id)].join(':')) || []);
|
|
1484
1316
|
if (!matchedCell) {
|
|
1485
|
-
const
|
|
1317
|
+
const isPast = candidateRange.endMs <= evaluationContext.evaluatedAtMs;
|
|
1486
1318
|
return {
|
|
1487
1319
|
resourceId: resource.id,
|
|
1488
1320
|
resourceName: resource.name,
|
|
1489
1321
|
resourceFormId: resource.formId,
|
|
1490
1322
|
resourceType: resource.resourceType,
|
|
1491
|
-
status:
|
|
1323
|
+
status: isPast ? 'past' : 'unavailable',
|
|
1492
1324
|
maxPartySize: 0,
|
|
1493
|
-
reasonCodes:
|
|
1325
|
+
reasonCodes: isPast ? ['past'] : ['outside_resource_window']
|
|
1494
1326
|
};
|
|
1495
1327
|
}
|
|
1496
1328
|
const remainingCapacity = matchedCell.remainingCapacity;
|
|
1497
1329
|
const maxCapacity = matchedCell.maxCapacity;
|
|
1498
1330
|
const isPast = candidateRange.endMs <= evaluationContext.evaluatedAtMs;
|
|
1499
|
-
const
|
|
1331
|
+
const maxPartySize = isPast ? 0 : Math.max(0, remainingCapacity || 0);
|
|
1500
1332
|
const requiredCapacity = resolveRequiredCapacity(requirementGroup, product, requestedPartySize);
|
|
1501
|
-
const
|
|
1502
|
-
if (isPast)
|
|
1503
|
-
|
|
1504
|
-
if (
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
return "full";
|
|
1508
|
-
if ((remainingCapacity || 0) < (maxCapacity || resource.capacity))
|
|
1509
|
-
return "limited";
|
|
1510
|
-
return "available";
|
|
1333
|
+
const status = (() => {
|
|
1334
|
+
if (isPast) return 'past';
|
|
1335
|
+
if (matchedCell.status === 'unavailable') return 'unavailable';
|
|
1336
|
+
if ((remainingCapacity || 0) < requiredCapacity) return 'full';
|
|
1337
|
+
if ((remainingCapacity || 0) < (maxCapacity || resource.capacity)) return 'limited';
|
|
1338
|
+
return 'available';
|
|
1511
1339
|
})();
|
|
1512
1340
|
return {
|
|
1513
1341
|
resourceId: resource.id,
|
|
1514
1342
|
resourceName: resource.name,
|
|
1515
1343
|
resourceFormId: resource.formId,
|
|
1516
1344
|
resourceType: resource.resourceType,
|
|
1517
|
-
status
|
|
1345
|
+
status,
|
|
1518
1346
|
remainingCapacity,
|
|
1519
1347
|
maxCapacity,
|
|
1520
|
-
maxPartySize
|
|
1521
|
-
reasonCodes: isPast ? Array.from(
|
|
1348
|
+
maxPartySize,
|
|
1349
|
+
reasonCodes: isPast ? Array.from(new Set([...matchedCell.reasonCodes, 'past'])) : matchedCell.reasonCodes,
|
|
1522
1350
|
matchedCell
|
|
1523
1351
|
};
|
|
1524
1352
|
});
|
|
1525
|
-
const availableOptions = options.filter(
|
|
1353
|
+
const availableOptions = options.filter(option => option.status === 'available' || option.status === 'limited');
|
|
1526
1354
|
const totalResourceCount = options.length;
|
|
1527
1355
|
const availableResourceCount = availableOptions.length;
|
|
1528
|
-
const groupMaxPartySize = availableOptions.reduce(
|
|
1529
|
-
|
|
1530
|
-
0
|
|
1531
|
-
);
|
|
1532
|
-
let status = "available";
|
|
1356
|
+
const groupMaxPartySize = availableOptions.reduce((max, option) => Math.max(max, option.maxPartySize), 0);
|
|
1357
|
+
let status = 'available';
|
|
1533
1358
|
if (candidateRange.endMs <= evaluationContext.evaluatedAtMs) {
|
|
1534
|
-
status =
|
|
1359
|
+
status = 'past';
|
|
1535
1360
|
} else if (availableResourceCount < minimum) {
|
|
1536
|
-
status = options.some(
|
|
1537
|
-
} else if (options.every(
|
|
1538
|
-
status =
|
|
1361
|
+
status = options.some(option => option.status === 'full' || option.status === 'limited' || option.status === 'available') ? 'full' : 'unavailable';
|
|
1362
|
+
} else if (options.every(option => option.status === 'available')) {
|
|
1363
|
+
status = 'available';
|
|
1539
1364
|
} else {
|
|
1540
|
-
status =
|
|
1365
|
+
status = 'limited';
|
|
1541
1366
|
}
|
|
1542
1367
|
return {
|
|
1543
1368
|
groupId: requirementGroup.id,
|
|
@@ -1552,70 +1377,66 @@ function evaluateCandidate(projection, candidate, request) {
|
|
|
1552
1377
|
availableResourceCount,
|
|
1553
1378
|
totalResourceCount,
|
|
1554
1379
|
maxPartySize: groupMaxPartySize,
|
|
1555
|
-
remainingCapacity: Math.max(...availableOptions.map(
|
|
1556
|
-
maxCapacity: Math.max(...availableOptions.map(
|
|
1557
|
-
reasonCodes: Array.from(new Set(options.flatMap(
|
|
1380
|
+
remainingCapacity: Math.max(...availableOptions.map(option => option.remainingCapacity || 0), 0),
|
|
1381
|
+
maxCapacity: Math.max(...availableOptions.map(option => option.maxCapacity || 0), 0),
|
|
1382
|
+
reasonCodes: Array.from(new Set(options.flatMap(option => option.reasonCodes))),
|
|
1558
1383
|
options
|
|
1559
1384
|
};
|
|
1560
1385
|
});
|
|
1561
|
-
const requiredGroups = requirementGroups.filter(
|
|
1562
|
-
const requiredStatuses = requiredGroups.map(
|
|
1386
|
+
const requiredGroups = requirementGroups.filter(group => group.required);
|
|
1387
|
+
const requiredStatuses = requiredGroups.map(group => group.status);
|
|
1563
1388
|
const gate = evaluateCandidateTimeGate({
|
|
1564
1389
|
product,
|
|
1565
1390
|
candidateRange,
|
|
1566
1391
|
evaluationContext,
|
|
1567
1392
|
timezoneName: projection.meta.timezone
|
|
1568
1393
|
});
|
|
1569
|
-
const candidateStatus = requiredGroups.length ? summarizeStatuses(requiredStatuses) : candidateRange.endMs <= evaluationContext.evaluatedAtMs ?
|
|
1394
|
+
const candidateStatus = requiredGroups.length ? summarizeStatuses(requiredStatuses) : candidateRange.endMs <= evaluationContext.evaluatedAtMs ? 'past' : 'available';
|
|
1570
1395
|
const maxPartySize = requiredGroups.length ? requiredGroups.reduce((min, group) => Math.min(min, group.maxPartySize), Number.MAX_SAFE_INTEGER) : 0;
|
|
1571
|
-
const selectedAssignments = requirementGroups.flatMap(
|
|
1572
|
-
const selection = selectionsByGroupKey.get(group.groupId) || selectionsByGroupKey.get(String(group.formId ??
|
|
1573
|
-
if (!selection && !(request.allowImplicitSelection && group.required))
|
|
1574
|
-
|
|
1575
|
-
const availableOptions = group.options.filter((option) => option.status === "available" || option.status === "limited");
|
|
1396
|
+
const selectedAssignments = requirementGroups.flatMap(group => {
|
|
1397
|
+
const selection = selectionsByGroupKey.get(group.groupId) || selectionsByGroupKey.get(String(group.formId ?? ''));
|
|
1398
|
+
if (!selection && !(request.allowImplicitSelection && group.required)) return [];
|
|
1399
|
+
const availableOptions = group.options.filter(option => option.status === 'available' || option.status === 'limited');
|
|
1576
1400
|
const selectedOptions = selection ? availableOptions.slice(0, group.max) : availableOptions.slice(0, Math.max(group.min, 1));
|
|
1577
|
-
const originalGroup = (product.requirementGroups || []).find(
|
|
1401
|
+
const originalGroup = (product.requirementGroups || []).find(item => item.id === group.groupId) || {
|
|
1578
1402
|
id: group.groupId,
|
|
1579
1403
|
resourceType: group.resourceType,
|
|
1580
1404
|
min: group.min,
|
|
1581
1405
|
max: group.max
|
|
1582
1406
|
};
|
|
1583
|
-
return selectedOptions.map(
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
timeSlotId: (_b2 = candidate.primaryScheduleRef) == null ? void 0 : _b2.timeSlotId
|
|
1594
|
-
};
|
|
1595
|
-
});
|
|
1407
|
+
return selectedOptions.map(selectedOption => ({
|
|
1408
|
+
productId: product.id,
|
|
1409
|
+
resourceId: selectedOption.resourceId,
|
|
1410
|
+
formId: group.formId,
|
|
1411
|
+
startAt: candidate.bookingStartAt || candidate.startAt,
|
|
1412
|
+
endAt: candidate.bookingEndAt || candidate.endAt,
|
|
1413
|
+
capacityRequired: resolveRequiredCapacity(originalGroup, product, requestedPartySize),
|
|
1414
|
+
scheduleId: candidate.primaryScheduleRef?.scheduleId,
|
|
1415
|
+
timeSlotId: candidate.primaryScheduleRef?.timeSlotId
|
|
1416
|
+
}));
|
|
1596
1417
|
});
|
|
1597
1418
|
return {
|
|
1598
1419
|
item: {
|
|
1599
1420
|
...candidate,
|
|
1600
|
-
status:
|
|
1421
|
+
status: gate?.status || candidateStatus,
|
|
1601
1422
|
maxPartySize: gate ? 0 : Number.isFinite(maxPartySize) ? maxPartySize : 0,
|
|
1602
|
-
reasonCodes:
|
|
1423
|
+
reasonCodes: gate?.reasonCodes || Array.from(new Set(requiredGroups.flatMap(group => group.reasonCodes))),
|
|
1603
1424
|
requirementGroups
|
|
1604
1425
|
},
|
|
1605
1426
|
assignments: gate ? [] : selectedAssignments,
|
|
1606
|
-
gate: gate ||
|
|
1427
|
+
gate: gate || undefined
|
|
1607
1428
|
};
|
|
1608
1429
|
}
|
|
1609
1430
|
function assertTimeCandidates(request) {
|
|
1610
|
-
if (request.target ===
|
|
1611
|
-
throw availabilityError(
|
|
1431
|
+
if (request.target === 'time' && (!Array.isArray(request.candidates) || request.candidates.length === 0)) {
|
|
1432
|
+
throw availabilityError('MISSING_TIME_CANDIDATES', 'target=time 必须传非空 candidates', {
|
|
1612
1433
|
request
|
|
1613
1434
|
});
|
|
1614
1435
|
}
|
|
1615
1436
|
}
|
|
1616
1437
|
function assertSelectedRanges(request) {
|
|
1617
|
-
if (request.target ===
|
|
1618
|
-
throw availabilityError(
|
|
1438
|
+
if (request.target === 'resource' && (!Array.isArray(request.selectedRanges) || request.selectedRanges.length === 0)) {
|
|
1439
|
+
throw availabilityError('MISSING_SELECTED_RANGE', 'target=resource 必须传非空 selectedRanges', {
|
|
1619
1440
|
request
|
|
1620
1441
|
});
|
|
1621
1442
|
}
|
|
@@ -1625,14 +1446,16 @@ function assertRangesWithinProjection(projection, ranges) {
|
|
|
1625
1446
|
start: toDateTime(projection.meta.coverage.startAt, projection.meta.timezone),
|
|
1626
1447
|
end: toDateTime(projection.meta.coverage.endAt, projection.meta.timezone)
|
|
1627
1448
|
};
|
|
1628
|
-
ranges.forEach(
|
|
1449
|
+
ranges.forEach(range => {
|
|
1629
1450
|
const displayRange = normalizeAbsoluteRange(range, projection.meta.timezone);
|
|
1630
1451
|
const bookingRange = normalizeAbsoluteRange(getCandidateBookingRange(range), projection.meta.timezone);
|
|
1631
1452
|
if (!displayRange || !bookingRange) {
|
|
1632
|
-
throw availabilityError(
|
|
1453
|
+
throw availabilityError('INVALID_TARGET_FILTER', '候选时间范围非法', {
|
|
1454
|
+
range
|
|
1455
|
+
});
|
|
1633
1456
|
}
|
|
1634
1457
|
if (!contains(coverage, displayRange) || !contains(coverage, bookingRange)) {
|
|
1635
|
-
throw availabilityError(
|
|
1458
|
+
throw availabilityError('OUT_OF_PROJECTION_COVERAGE', '候选时间范围超出 projection.coverage', {
|
|
1636
1459
|
range,
|
|
1637
1460
|
coverage: projection.meta.coverage
|
|
1638
1461
|
});
|
|
@@ -1640,22 +1463,17 @@ function assertRangesWithinProjection(projection, ranges) {
|
|
|
1640
1463
|
});
|
|
1641
1464
|
}
|
|
1642
1465
|
function queryAvailabilityProjection(projection, request) {
|
|
1643
|
-
var _a;
|
|
1644
1466
|
const normalizedPartySize = normalizePartySize(request.partySize);
|
|
1645
|
-
const evaluatedAt = formatDateTime(
|
|
1646
|
-
|
|
1647
|
-
projection.meta.timezone,
|
|
1648
|
-
(0, import_localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, import_localClock.captureRuntimeOffsetMinutes)()
|
|
1649
|
-
);
|
|
1650
|
-
if (request.target === "time") {
|
|
1467
|
+
const evaluatedAt = formatDateTime((0, _localClock.localDateToScalar)(new Date()), projection.meta.timezone, (0, _localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, _localClock.captureRuntimeOffsetMinutes)());
|
|
1468
|
+
if (request.target === 'time') {
|
|
1651
1469
|
assertTimeCandidates(request);
|
|
1652
1470
|
assertRangesWithinProjection(projection, request.candidates);
|
|
1653
1471
|
}
|
|
1654
|
-
if (request.target ===
|
|
1472
|
+
if (request.target === 'resource') {
|
|
1655
1473
|
assertSelectedRanges(request);
|
|
1656
1474
|
assertRangesWithinProjection(projection, request.selectedRanges);
|
|
1657
1475
|
}
|
|
1658
|
-
const productIdSet =
|
|
1476
|
+
const productIdSet = request.productIds?.length ? new Set(request.productIds.map(String)) : null;
|
|
1659
1477
|
const productIndex = buildProductIndex(projection);
|
|
1660
1478
|
const cellIndex = buildAvailabilityCellIndex(projection);
|
|
1661
1479
|
const groupResourceIndex = buildGroupResourceIndex({
|
|
@@ -1663,23 +1481,23 @@ function queryAvailabilityProjection(projection, request) {
|
|
|
1663
1481
|
resourceIds: request.resourceIds,
|
|
1664
1482
|
partySize: normalizedPartySize
|
|
1665
1483
|
});
|
|
1666
|
-
const candidateRangeCache =
|
|
1484
|
+
const candidateRangeCache = new Map();
|
|
1667
1485
|
const evaluationContext = buildEvaluationContext({
|
|
1668
1486
|
projection,
|
|
1669
1487
|
evaluatedAt,
|
|
1670
1488
|
resourceIds: request.resourceIds
|
|
1671
1489
|
});
|
|
1672
|
-
if (request.target ===
|
|
1490
|
+
if (request.target === 'time') {
|
|
1673
1491
|
const candidates = dedupeProductRanges(request.candidates);
|
|
1674
|
-
const candidatesByProduct =
|
|
1675
|
-
candidates.forEach(
|
|
1492
|
+
const candidatesByProduct = new Map();
|
|
1493
|
+
candidates.forEach(candidate => {
|
|
1676
1494
|
const key = String(candidate.productId);
|
|
1677
|
-
candidatesByProduct.set(key, [...candidatesByProduct.get(key) || [], candidate]);
|
|
1495
|
+
candidatesByProduct.set(key, [...(candidatesByProduct.get(key) || []), candidate]);
|
|
1678
1496
|
});
|
|
1679
|
-
const
|
|
1497
|
+
const data = projection.products.filter(product => !productIdSet || productIdSet.has(String(product.id))).map(product => ({
|
|
1680
1498
|
productId: product.id,
|
|
1681
1499
|
title: product.title,
|
|
1682
|
-
times: (candidatesByProduct.get(String(product.id)) || []).map(
|
|
1500
|
+
times: (candidatesByProduct.get(String(product.id)) || []).map(candidate => evaluateCandidate(projection, candidate, {
|
|
1683
1501
|
partySize: normalizedPartySize,
|
|
1684
1502
|
resourceIds: request.resourceIds,
|
|
1685
1503
|
evaluatedAt,
|
|
@@ -1689,25 +1507,25 @@ function queryAvailabilityProjection(projection, request) {
|
|
|
1689
1507
|
candidateRangeCache,
|
|
1690
1508
|
evaluationContext
|
|
1691
1509
|
}).item)
|
|
1692
|
-
})).filter(
|
|
1510
|
+
})).filter(group => group.times.length > 0);
|
|
1693
1511
|
return {
|
|
1694
|
-
target:
|
|
1512
|
+
target: 'time',
|
|
1695
1513
|
evaluatedAt,
|
|
1696
1514
|
snapshotId: projection.meta.snapshotId,
|
|
1697
|
-
data
|
|
1515
|
+
data
|
|
1698
1516
|
};
|
|
1699
1517
|
}
|
|
1700
|
-
if (request.target ===
|
|
1518
|
+
if (request.target === 'resource') {
|
|
1701
1519
|
const selectedRanges = dedupeProductRanges(request.selectedRanges);
|
|
1702
|
-
const rangesByProduct =
|
|
1703
|
-
selectedRanges.forEach(
|
|
1520
|
+
const rangesByProduct = new Map();
|
|
1521
|
+
selectedRanges.forEach(candidate => {
|
|
1704
1522
|
const key = String(candidate.productId);
|
|
1705
|
-
rangesByProduct.set(key, [...rangesByProduct.get(key) || [], candidate]);
|
|
1523
|
+
rangesByProduct.set(key, [...(rangesByProduct.get(key) || []), candidate]);
|
|
1706
1524
|
});
|
|
1707
|
-
const
|
|
1525
|
+
const data = projection.products.filter(product => !productIdSet || productIdSet.has(String(product.id))).map(product => ({
|
|
1708
1526
|
productId: product.id,
|
|
1709
1527
|
title: product.title,
|
|
1710
|
-
ranges: (rangesByProduct.get(String(product.id)) || []).map(
|
|
1528
|
+
ranges: (rangesByProduct.get(String(product.id)) || []).map(candidate => {
|
|
1711
1529
|
const evaluated = evaluateCandidate(projection, candidate, {
|
|
1712
1530
|
partySize: normalizedPartySize,
|
|
1713
1531
|
resourceIds: request.resourceIds,
|
|
@@ -1726,12 +1544,12 @@ function queryAvailabilityProjection(projection, request) {
|
|
|
1726
1544
|
requirementGroups: evaluated.requirementGroups
|
|
1727
1545
|
};
|
|
1728
1546
|
})
|
|
1729
|
-
})).filter(
|
|
1547
|
+
})).filter(group => group.ranges.length > 0);
|
|
1730
1548
|
return {
|
|
1731
|
-
target:
|
|
1549
|
+
target: 'resource',
|
|
1732
1550
|
evaluatedAt,
|
|
1733
1551
|
snapshotId: projection.meta.snapshotId,
|
|
1734
|
-
data
|
|
1552
|
+
data
|
|
1735
1553
|
};
|
|
1736
1554
|
}
|
|
1737
1555
|
const dateRange = resolveProjectionDateRange(projection, request.dateRange);
|
|
@@ -1741,17 +1559,17 @@ function queryAvailabilityProjection(projection, request) {
|
|
|
1741
1559
|
resourceIds: request.resourceIds,
|
|
1742
1560
|
partySize: normalizedPartySize
|
|
1743
1561
|
});
|
|
1744
|
-
const candidateGroupMap = new Map(candidateGroups.map(
|
|
1745
|
-
if (request.target ===
|
|
1746
|
-
const
|
|
1562
|
+
const candidateGroupMap = new Map(candidateGroups.map(group => [String(group.productId), group]));
|
|
1563
|
+
if (request.target === 'date') {
|
|
1564
|
+
const data = projection.products.filter(product => !productIdSet || productIdSet.has(String(product.id))).map(product => {
|
|
1747
1565
|
const group = candidateGroupMap.get(String(product.id)) || {
|
|
1748
1566
|
productId: product.id,
|
|
1749
1567
|
title: product.title,
|
|
1750
1568
|
kind: product.kind,
|
|
1751
1569
|
ranges: []
|
|
1752
1570
|
};
|
|
1753
|
-
const byDate =
|
|
1754
|
-
group.ranges.forEach(
|
|
1571
|
+
const byDate = new Map();
|
|
1572
|
+
group.ranges.forEach(candidate => {
|
|
1755
1573
|
const evaluated = evaluateCandidateSummary(projection, candidate, {
|
|
1756
1574
|
partySize: normalizedPartySize,
|
|
1757
1575
|
resourceIds: request.resourceIds,
|
|
@@ -1768,30 +1586,30 @@ function queryAvailabilityProjection(projection, request) {
|
|
|
1768
1586
|
maxPartySize: 0,
|
|
1769
1587
|
remainingCapacity: 0,
|
|
1770
1588
|
maxCapacity: 0,
|
|
1771
|
-
reasonCodeSet:
|
|
1589
|
+
reasonCodeSet: new Set(),
|
|
1772
1590
|
statusAggregate: createStatusAggregate()
|
|
1773
1591
|
};
|
|
1774
1592
|
aggregate.totalCount += 1;
|
|
1775
|
-
if (evaluated.status ===
|
|
1593
|
+
if (evaluated.status === 'available' || evaluated.status === 'limited') {
|
|
1776
1594
|
aggregate.availableCount += 1;
|
|
1777
1595
|
}
|
|
1778
1596
|
aggregate.maxPartySize = Math.max(aggregate.maxPartySize, evaluated.maxPartySize);
|
|
1779
1597
|
aggregate.remainingCapacity += evaluated.remainingCapacity;
|
|
1780
1598
|
aggregate.maxCapacity += evaluated.maxCapacity;
|
|
1781
1599
|
pushStatusAggregate(aggregate.statusAggregate, evaluated.status);
|
|
1782
|
-
evaluated.reasonCodes.forEach(
|
|
1600
|
+
evaluated.reasonCodes.forEach(reasonCode => aggregate.reasonCodeSet.add(reasonCode));
|
|
1783
1601
|
byDate.set(candidate.date, aggregate);
|
|
1784
1602
|
});
|
|
1785
|
-
const dates = enumerateDates(dateRange.startDate, dateRange.endDate).map(
|
|
1603
|
+
const dates = enumerateDates(dateRange.startDate, dateRange.endDate).map(date => {
|
|
1786
1604
|
const aggregate = byDate.get(date);
|
|
1787
1605
|
return {
|
|
1788
1606
|
date,
|
|
1789
|
-
status: aggregate ? summarizeStatusAggregate(aggregate.statusAggregate) :
|
|
1790
|
-
availableCount:
|
|
1791
|
-
totalCount:
|
|
1792
|
-
maxPartySize:
|
|
1793
|
-
remainingCapacity:
|
|
1794
|
-
maxCapacity:
|
|
1607
|
+
status: aggregate ? summarizeStatusAggregate(aggregate.statusAggregate) : 'unavailable',
|
|
1608
|
+
availableCount: aggregate?.availableCount || 0,
|
|
1609
|
+
totalCount: aggregate?.totalCount || 0,
|
|
1610
|
+
maxPartySize: aggregate?.maxPartySize || 0,
|
|
1611
|
+
remainingCapacity: aggregate?.remainingCapacity || 0,
|
|
1612
|
+
maxCapacity: aggregate?.maxCapacity || 0,
|
|
1795
1613
|
reasonCodes: aggregate ? Array.from(aggregate.reasonCodeSet) : []
|
|
1796
1614
|
};
|
|
1797
1615
|
});
|
|
@@ -1802,13 +1620,13 @@ function queryAvailabilityProjection(projection, request) {
|
|
|
1802
1620
|
};
|
|
1803
1621
|
});
|
|
1804
1622
|
return {
|
|
1805
|
-
target:
|
|
1623
|
+
target: 'date',
|
|
1806
1624
|
evaluatedAt,
|
|
1807
1625
|
snapshotId: projection.meta.snapshotId,
|
|
1808
|
-
data
|
|
1626
|
+
data
|
|
1809
1627
|
};
|
|
1810
1628
|
}
|
|
1811
|
-
const data = projection.products.filter(
|
|
1629
|
+
const data = projection.products.filter(product => !productIdSet || productIdSet.has(String(product.id))).map(product => {
|
|
1812
1630
|
const group = candidateGroupMap.get(String(product.id)) || {
|
|
1813
1631
|
productId: product.id,
|
|
1814
1632
|
title: product.title,
|
|
@@ -1816,12 +1634,12 @@ function queryAvailabilityProjection(projection, request) {
|
|
|
1816
1634
|
ranges: []
|
|
1817
1635
|
};
|
|
1818
1636
|
const productStatusAggregate = createStatusAggregate();
|
|
1819
|
-
const productReasonCodeSet =
|
|
1820
|
-
const dateStatusMap =
|
|
1637
|
+
const productReasonCodeSet = new Set();
|
|
1638
|
+
const dateStatusMap = new Map();
|
|
1821
1639
|
let availableTimeCount = 0;
|
|
1822
1640
|
let totalTimeCount = 0;
|
|
1823
1641
|
let maxPartySize = 0;
|
|
1824
|
-
group.ranges.forEach(
|
|
1642
|
+
group.ranges.forEach(candidate => {
|
|
1825
1643
|
const evaluated = evaluateCandidateSummary(projection, candidate, {
|
|
1826
1644
|
partySize: normalizedPartySize,
|
|
1827
1645
|
resourceIds: request.resourceIds,
|
|
@@ -1833,19 +1651,19 @@ function queryAvailabilityProjection(projection, request) {
|
|
|
1833
1651
|
evaluationContext
|
|
1834
1652
|
});
|
|
1835
1653
|
totalTimeCount += 1;
|
|
1836
|
-
if (evaluated.status ===
|
|
1654
|
+
if (evaluated.status === 'available' || evaluated.status === 'limited') {
|
|
1837
1655
|
availableTimeCount += 1;
|
|
1838
1656
|
}
|
|
1839
1657
|
maxPartySize = Math.max(maxPartySize, evaluated.maxPartySize);
|
|
1840
1658
|
pushStatusAggregate(productStatusAggregate, evaluated.status);
|
|
1841
|
-
evaluated.reasonCodes.forEach(
|
|
1659
|
+
evaluated.reasonCodes.forEach(reasonCode => productReasonCodeSet.add(reasonCode));
|
|
1842
1660
|
const dateAggregate = dateStatusMap.get(candidate.date) || createStatusAggregate();
|
|
1843
1661
|
pushStatusAggregate(dateAggregate, evaluated.status);
|
|
1844
1662
|
dateStatusMap.set(candidate.date, dateAggregate);
|
|
1845
1663
|
});
|
|
1846
|
-
const availableDateCount = Array.from(dateStatusMap.values()).filter(
|
|
1847
|
-
const
|
|
1848
|
-
return
|
|
1664
|
+
const availableDateCount = Array.from(dateStatusMap.values()).filter(aggregate => {
|
|
1665
|
+
const status = summarizeStatusAggregate(aggregate);
|
|
1666
|
+
return status === 'available' || status === 'limited';
|
|
1849
1667
|
}).length;
|
|
1850
1668
|
const totalDateCount = dateStatusMap.size;
|
|
1851
1669
|
const status = summarizeStatusAggregate(productStatusAggregate);
|
|
@@ -1867,22 +1685,34 @@ function queryAvailabilityProjection(projection, request) {
|
|
|
1867
1685
|
};
|
|
1868
1686
|
});
|
|
1869
1687
|
return {
|
|
1870
|
-
target:
|
|
1688
|
+
target: 'product',
|
|
1871
1689
|
evaluatedAt,
|
|
1872
1690
|
snapshotId: projection.meta.snapshotId,
|
|
1873
1691
|
data
|
|
1874
1692
|
};
|
|
1875
1693
|
}
|
|
1694
|
+
function summarizeDateStatus(statuses) {
|
|
1695
|
+
if (!statuses.length) return 'unavailable';
|
|
1696
|
+
if (statuses.some(status => status === 'available')) return 'available';
|
|
1697
|
+
if (statuses.some(status => status === 'limited')) return 'limited';
|
|
1698
|
+
if (statuses.some(status => status === 'full')) return 'full';
|
|
1699
|
+
if (statuses.every(status => status === 'past')) return 'past';
|
|
1700
|
+
return 'unavailable';
|
|
1701
|
+
}
|
|
1702
|
+
function summarizeProductStatus(statuses) {
|
|
1703
|
+
if (!statuses.length) return 'unavailable';
|
|
1704
|
+
if (statuses.some(status => status === 'available')) return 'available';
|
|
1705
|
+
if (statuses.some(status => status === 'limited')) return 'limited';
|
|
1706
|
+
if (statuses.some(status => status === 'full')) return 'full';
|
|
1707
|
+
if (statuses.every(status => status === 'past')) return 'past';
|
|
1708
|
+
return 'unavailable';
|
|
1709
|
+
}
|
|
1876
1710
|
function validateAvailabilitySelection(projection, params) {
|
|
1877
1711
|
const normalizedPartySize = normalizePartySize(params.partySize);
|
|
1878
1712
|
const normalizedBookingCount = normalizeBookingCount(params.bookingCount);
|
|
1879
1713
|
assertRangesWithinProjection(projection, [params.candidate]);
|
|
1880
1714
|
const productIndex = buildProductIndex(projection);
|
|
1881
|
-
const evaluatedAt = formatDateTime(
|
|
1882
|
-
(0, import_localClock.localDateToScalar)(/* @__PURE__ */ new Date()),
|
|
1883
|
-
projection.meta.timezone,
|
|
1884
|
-
(0, import_localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, import_localClock.captureRuntimeOffsetMinutes)()
|
|
1885
|
-
);
|
|
1715
|
+
const evaluatedAt = formatDateTime((0, _localClock.localDateToScalar)(new Date()), projection.meta.timezone, (0, _localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, _localClock.captureRuntimeOffsetMinutes)());
|
|
1886
1716
|
const evaluated = evaluateCandidate(projection, params.candidate, {
|
|
1887
1717
|
partySize: normalizedPartySize,
|
|
1888
1718
|
evaluatedAt,
|
|
@@ -1894,7 +1724,7 @@ function validateAvailabilitySelection(projection, params) {
|
|
|
1894
1724
|
projection,
|
|
1895
1725
|
partySize: normalizedPartySize
|
|
1896
1726
|
}),
|
|
1897
|
-
candidateRangeCache:
|
|
1727
|
+
candidateRangeCache: new Map(),
|
|
1898
1728
|
evaluationContext: buildEvaluationContext({
|
|
1899
1729
|
projection,
|
|
1900
1730
|
evaluatedAt,
|
|
@@ -1905,27 +1735,27 @@ function validateAvailabilitySelection(projection, params) {
|
|
|
1905
1735
|
if (evaluated.gate) {
|
|
1906
1736
|
conflicts.push({
|
|
1907
1737
|
code: evaluated.gate.conflictCode,
|
|
1908
|
-
message: evaluated.gate.status ===
|
|
1738
|
+
message: evaluated.gate.status === 'past' ? '候选时间已过期' : '候选时间不满足预订时间门禁',
|
|
1909
1739
|
reasonCodes: evaluated.gate.reasonCodes,
|
|
1910
1740
|
productId: params.candidate.productId,
|
|
1911
1741
|
candidateKey: params.candidate.key
|
|
1912
1742
|
});
|
|
1913
1743
|
return {
|
|
1914
1744
|
ok: false,
|
|
1915
|
-
status: evaluated.gate.status ===
|
|
1745
|
+
status: evaluated.gate.status === 'past' ? 'past' : 'conflict',
|
|
1916
1746
|
assignments: [],
|
|
1917
1747
|
conflicts,
|
|
1918
1748
|
availability: evaluated.item
|
|
1919
1749
|
};
|
|
1920
1750
|
}
|
|
1921
|
-
const requiredGroups = evaluated.item.requirementGroups.filter(
|
|
1922
|
-
const findSelectionForGroup =
|
|
1923
|
-
requiredGroups.forEach(
|
|
1751
|
+
const requiredGroups = evaluated.item.requirementGroups.filter(group => group.required);
|
|
1752
|
+
const findSelectionForGroup = group => params.requirementSelections.find(selection => selection.requirementGroupId && selection.requirementGroupId === group.groupId || selection.formId !== undefined && sameId(selection.formId, group.formId));
|
|
1753
|
+
requiredGroups.forEach(group => {
|
|
1924
1754
|
const selection = findSelectionForGroup(group);
|
|
1925
|
-
const selectedCount = new Set((
|
|
1755
|
+
const selectedCount = new Set((selection?.resourceIds || []).map(String)).size;
|
|
1926
1756
|
if (selectedCount < group.min) {
|
|
1927
1757
|
conflicts.push({
|
|
1928
|
-
code:
|
|
1758
|
+
code: 'missing_resource',
|
|
1929
1759
|
message: `资源组 ${group.groupId} 缺少已选资源`,
|
|
1930
1760
|
productId: params.candidate.productId,
|
|
1931
1761
|
requirementGroupId: group.groupId,
|
|
@@ -1936,7 +1766,7 @@ function validateAvailabilitySelection(projection, params) {
|
|
|
1936
1766
|
}
|
|
1937
1767
|
if (selectedCount > group.max) {
|
|
1938
1768
|
conflicts.push({
|
|
1939
|
-
code:
|
|
1769
|
+
code: 'invalid_selection_count',
|
|
1940
1770
|
message: `资源组 ${group.groupId} 已选资源数量超出上限`,
|
|
1941
1771
|
productId: params.candidate.productId,
|
|
1942
1772
|
requirementGroupId: group.groupId,
|
|
@@ -1945,10 +1775,9 @@ function validateAvailabilitySelection(projection, params) {
|
|
|
1945
1775
|
});
|
|
1946
1776
|
return;
|
|
1947
1777
|
}
|
|
1948
|
-
if (group.status ===
|
|
1949
|
-
return;
|
|
1778
|
+
if (group.status === 'available' || group.status === 'limited') return;
|
|
1950
1779
|
conflicts.push({
|
|
1951
|
-
code: group.status ===
|
|
1780
|
+
code: group.status === 'past' ? 'past' : group.status === 'full' ? 'resource_full' : 'missing_resource',
|
|
1952
1781
|
message: `资源组 ${group.groupId} 当前不可提交`,
|
|
1953
1782
|
productId: params.candidate.productId,
|
|
1954
1783
|
requirementGroupId: group.groupId,
|
|
@@ -1956,12 +1785,12 @@ function validateAvailabilitySelection(projection, params) {
|
|
|
1956
1785
|
candidateKey: params.candidate.key
|
|
1957
1786
|
});
|
|
1958
1787
|
});
|
|
1959
|
-
params.requirementSelections.forEach(
|
|
1960
|
-
const matchedGroup = evaluated.item.requirementGroups.find(
|
|
1788
|
+
params.requirementSelections.forEach(selection => {
|
|
1789
|
+
const matchedGroup = evaluated.item.requirementGroups.find(group => selection.requirementGroupId ? group.groupId === selection.requirementGroupId : sameId(group.formId, selection.formId));
|
|
1961
1790
|
if (!matchedGroup) {
|
|
1962
1791
|
conflicts.push({
|
|
1963
|
-
code:
|
|
1964
|
-
message:
|
|
1792
|
+
code: 'invalid_requirement_group',
|
|
1793
|
+
message: '选择的资源组不属于当前商品',
|
|
1965
1794
|
productId: params.candidate.productId,
|
|
1966
1795
|
requirementGroupId: selection.requirementGroupId,
|
|
1967
1796
|
formId: selection.formId,
|
|
@@ -1972,7 +1801,7 @@ function validateAvailabilitySelection(projection, params) {
|
|
|
1972
1801
|
const selectedCount = new Set(selection.resourceIds.map(String)).size;
|
|
1973
1802
|
if (!matchedGroup.required && (selectedCount < matchedGroup.min || selectedCount > matchedGroup.max)) {
|
|
1974
1803
|
conflicts.push({
|
|
1975
|
-
code:
|
|
1804
|
+
code: 'invalid_selection_count',
|
|
1976
1805
|
message: `资源组 ${matchedGroup.groupId} 已选资源数量不满足 ${matchedGroup.min}-${matchedGroup.max}`,
|
|
1977
1806
|
productId: params.candidate.productId,
|
|
1978
1807
|
requirementGroupId: matchedGroup.groupId,
|
|
@@ -1980,23 +1809,20 @@ function validateAvailabilitySelection(projection, params) {
|
|
|
1980
1809
|
candidateKey: params.candidate.key
|
|
1981
1810
|
});
|
|
1982
1811
|
}
|
|
1983
|
-
selection.resourceIds.forEach(
|
|
1984
|
-
const option = matchedGroup
|
|
1985
|
-
const matchedCell = option
|
|
1986
|
-
const remainingCapacity = matchedCell
|
|
1987
|
-
if (option && (option.status ===
|
|
1812
|
+
selection.resourceIds.forEach(resourceId => {
|
|
1813
|
+
const option = matchedGroup?.options.find(item => sameId(item.resourceId, resourceId));
|
|
1814
|
+
const matchedCell = option?.matchedCell;
|
|
1815
|
+
const remainingCapacity = matchedCell?.remainingCapacity;
|
|
1816
|
+
if (option && (option.status === 'available' || option.status === 'limited') && remainingCapacity !== undefined) {
|
|
1988
1817
|
const capacityPerBooking = normalizedPartySize ?? 1;
|
|
1989
|
-
const availableQuantity = Math.max(
|
|
1990
|
-
0,
|
|
1991
|
-
Math.floor(remainingCapacity / capacityPerBooking)
|
|
1992
|
-
);
|
|
1818
|
+
const availableQuantity = Math.max(0, Math.floor(remainingCapacity / capacityPerBooking));
|
|
1993
1819
|
if (normalizedBookingCount > availableQuantity) {
|
|
1994
1820
|
conflicts.push({
|
|
1995
|
-
code:
|
|
1821
|
+
code: 'resource_capacity_exceeded',
|
|
1996
1822
|
message: `所选资源「${option.resourceName || String(resourceId)}」当前最多可预订 ${availableQuantity} 个`,
|
|
1997
1823
|
productId: params.candidate.productId,
|
|
1998
|
-
requirementGroupId: matchedGroup
|
|
1999
|
-
formId: matchedGroup
|
|
1824
|
+
requirementGroupId: matchedGroup?.groupId,
|
|
1825
|
+
formId: matchedGroup?.formId,
|
|
2000
1826
|
resourceId,
|
|
2001
1827
|
resourceName: option.resourceName,
|
|
2002
1828
|
candidateKey: params.candidate.key,
|
|
@@ -2008,13 +1834,13 @@ function validateAvailabilitySelection(projection, params) {
|
|
|
2008
1834
|
return;
|
|
2009
1835
|
}
|
|
2010
1836
|
}
|
|
2011
|
-
if (!option || option.status !==
|
|
1837
|
+
if (!option || option.status !== 'available' && option.status !== 'limited') {
|
|
2012
1838
|
conflicts.push({
|
|
2013
|
-
code:
|
|
1839
|
+
code: 'resource_unavailable',
|
|
2014
1840
|
message: `资源 ${String(resourceId)} 当前不可提交`,
|
|
2015
1841
|
productId: params.candidate.productId,
|
|
2016
|
-
requirementGroupId: matchedGroup
|
|
2017
|
-
formId: matchedGroup
|
|
1842
|
+
requirementGroupId: matchedGroup?.groupId,
|
|
1843
|
+
formId: matchedGroup?.formId,
|
|
2018
1844
|
resourceId,
|
|
2019
1845
|
candidateKey: params.candidate.key
|
|
2020
1846
|
});
|
|
@@ -2022,17 +1848,10 @@ function validateAvailabilitySelection(projection, params) {
|
|
|
2022
1848
|
});
|
|
2023
1849
|
});
|
|
2024
1850
|
return {
|
|
2025
|
-
ok: conflicts.length === 0 && (evaluated.item.status ===
|
|
2026
|
-
status: conflicts.length === 0 ? evaluated.item.status :
|
|
1851
|
+
ok: conflicts.length === 0 && (evaluated.item.status === 'available' || evaluated.item.status === 'limited'),
|
|
1852
|
+
status: conflicts.length === 0 ? evaluated.item.status : 'conflict',
|
|
2027
1853
|
assignments: evaluated.assignments,
|
|
2028
1854
|
conflicts,
|
|
2029
1855
|
availability: evaluated.item
|
|
2030
1856
|
};
|
|
2031
|
-
}
|
|
2032
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
2033
|
-
0 && (module.exports = {
|
|
2034
|
-
createAvailabilityProjection,
|
|
2035
|
-
getProductTimeRanges,
|
|
2036
|
-
queryAvailabilityProjection,
|
|
2037
|
-
validateAvailabilitySelection
|
|
2038
|
-
});
|
|
1857
|
+
}
|