@final-commerce/common 2.4.1-staging2.1 → 2.5.0-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +379 -19
- package/dist/index.d.ts +379 -19
- package/dist/index.js +827 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +790 -41
- package/dist/index.mjs.map +1 -1
- package/dist/pos-types/index.d.mts +315 -95
- package/dist/pos-types/index.d.ts +315 -95
- package/dist/pos-types/index.js.map +1 -1
- package/dist/pos-types/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -422,6 +422,8 @@ var StripeCurrencyCode = /* @__PURE__ */ ((StripeCurrencyCode2) => {
|
|
|
422
422
|
var ProductType = /* @__PURE__ */ ((ProductType2) => {
|
|
423
423
|
ProductType2["SIMPLE"] = "simple";
|
|
424
424
|
ProductType2["VARIABLE"] = "variable";
|
|
425
|
+
ProductType2["BOOKING"] = "booking";
|
|
426
|
+
ProductType2["COMPOSITE"] = "composite";
|
|
425
427
|
return ProductType2;
|
|
426
428
|
})(ProductType || {});
|
|
427
429
|
var ProductStatus = /* @__PURE__ */ ((ProductStatus2) => {
|
|
@@ -439,6 +441,50 @@ var PaymentProcessor = /* @__PURE__ */ ((PaymentProcessor2) => {
|
|
|
439
441
|
return PaymentProcessor2;
|
|
440
442
|
})(PaymentProcessor || {});
|
|
441
443
|
|
|
444
|
+
// enums/booking.enum.ts
|
|
445
|
+
var BookingType = /* @__PURE__ */ ((BookingType3) => {
|
|
446
|
+
BookingType3["RENTAL"] = "rental";
|
|
447
|
+
BookingType3["APPOINTMENT"] = "appointment";
|
|
448
|
+
BookingType3["OVERNIGHT"] = "overnight";
|
|
449
|
+
BookingType3["GROUP"] = "group";
|
|
450
|
+
return BookingType3;
|
|
451
|
+
})(BookingType || {});
|
|
452
|
+
var BookingRatePeriod = /* @__PURE__ */ ((BookingRatePeriod2) => {
|
|
453
|
+
BookingRatePeriod2["SLOT"] = "slot";
|
|
454
|
+
BookingRatePeriod2["DAY"] = "day";
|
|
455
|
+
BookingRatePeriod2["NIGHT"] = "night";
|
|
456
|
+
BookingRatePeriod2["WEEK"] = "week";
|
|
457
|
+
BookingRatePeriod2["MONTH"] = "month";
|
|
458
|
+
return BookingRatePeriod2;
|
|
459
|
+
})(BookingRatePeriod || {});
|
|
460
|
+
var BookingResourceKind = /* @__PURE__ */ ((BookingResourceKind2) => {
|
|
461
|
+
BookingResourceKind2["ASSET"] = "asset";
|
|
462
|
+
BookingResourceKind2["ROOM"] = "room";
|
|
463
|
+
BookingResourceKind2["PROPERTY"] = "property";
|
|
464
|
+
BookingResourceKind2["SPACE"] = "space";
|
|
465
|
+
BookingResourceKind2["STAFF"] = "staff";
|
|
466
|
+
BookingResourceKind2["TABLE"] = "table";
|
|
467
|
+
return BookingResourceKind2;
|
|
468
|
+
})(BookingResourceKind || {});
|
|
469
|
+
var ReservationStatus = /* @__PURE__ */ ((ReservationStatus2) => {
|
|
470
|
+
ReservationStatus2["HELD"] = "held";
|
|
471
|
+
ReservationStatus2["CONFIRMED"] = "confirmed";
|
|
472
|
+
ReservationStatus2["CANCELLED"] = "cancelled";
|
|
473
|
+
ReservationStatus2["EXPIRED"] = "expired";
|
|
474
|
+
return ReservationStatus2;
|
|
475
|
+
})(ReservationStatus || {});
|
|
476
|
+
var BookingResourceStatus = /* @__PURE__ */ ((BookingResourceStatus2) => {
|
|
477
|
+
BookingResourceStatus2["ACTIVE"] = "active";
|
|
478
|
+
BookingResourceStatus2["INACTIVE"] = "inactive";
|
|
479
|
+
return BookingResourceStatus2;
|
|
480
|
+
})(BookingResourceStatus || {});
|
|
481
|
+
var ResourceAvailabilityState = /* @__PURE__ */ ((ResourceAvailabilityState2) => {
|
|
482
|
+
ResourceAvailabilityState2["AVAILABLE"] = "available";
|
|
483
|
+
ResourceAvailabilityState2["FULLY_BOOKED"] = "fullyBooked";
|
|
484
|
+
ResourceAvailabilityState2["NO_HOURS_IN_RANGE"] = "noHoursInRange";
|
|
485
|
+
return ResourceAvailabilityState2;
|
|
486
|
+
})(ResourceAvailabilityState || {});
|
|
487
|
+
|
|
442
488
|
// utils/currency.util.ts
|
|
443
489
|
var CURRENCY_CONFIG = {
|
|
444
490
|
// Major currencies with 2 decimal places
|
|
@@ -1308,6 +1354,309 @@ function normalizeMongoIdString(value) {
|
|
|
1308
1354
|
return null;
|
|
1309
1355
|
}
|
|
1310
1356
|
|
|
1357
|
+
// utils/composite-price.util.ts
|
|
1358
|
+
var compositeItemCost = (item) => item.extraCharge * item.quantity;
|
|
1359
|
+
function cheapestCompositeItem(items) {
|
|
1360
|
+
let cheapest;
|
|
1361
|
+
items.forEach((item, index) => {
|
|
1362
|
+
if (!item.available) return;
|
|
1363
|
+
if (cheapest === void 0 || compositeItemCost(item) < compositeItemCost(items[cheapest])) cheapest = index;
|
|
1364
|
+
});
|
|
1365
|
+
return cheapest;
|
|
1366
|
+
}
|
|
1367
|
+
function cheapestCompositePrice(parts) {
|
|
1368
|
+
let total = 0;
|
|
1369
|
+
for (const part of parts) {
|
|
1370
|
+
if (part.min === 0) continue;
|
|
1371
|
+
const index = cheapestCompositeItem(part.items) ?? cheapestCompositeItem(part.items.map((item) => ({ ...item, available: true })));
|
|
1372
|
+
if (index === void 0) return void 0;
|
|
1373
|
+
total += part.min * compositeItemCost(part.items[index]);
|
|
1374
|
+
}
|
|
1375
|
+
return total;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// utils/modifier-resolution.util.ts
|
|
1379
|
+
function ancestorChain(categoryId, categoriesById) {
|
|
1380
|
+
const chain = [];
|
|
1381
|
+
const seen = /* @__PURE__ */ new Set([categoryId]);
|
|
1382
|
+
let current = categoriesById.get(categoryId)?.parentId ?? void 0;
|
|
1383
|
+
while (current && !seen.has(current)) {
|
|
1384
|
+
chain.push(current);
|
|
1385
|
+
seen.add(current);
|
|
1386
|
+
current = categoriesById.get(current)?.parentId ?? void 0;
|
|
1387
|
+
}
|
|
1388
|
+
return chain;
|
|
1389
|
+
}
|
|
1390
|
+
function clampRule(rule) {
|
|
1391
|
+
if (rule.min != null && rule.max != null && rule.min > rule.max) {
|
|
1392
|
+
return { ...rule, min: rule.max };
|
|
1393
|
+
}
|
|
1394
|
+
return rule;
|
|
1395
|
+
}
|
|
1396
|
+
function mergeRules(a, b) {
|
|
1397
|
+
const required = a.required || b.required;
|
|
1398
|
+
const min = a.min == null ? b.min : b.min == null ? a.min : Math.max(a.min, b.min);
|
|
1399
|
+
const max = a.max == null ? b.max : b.max == null ? a.max : Math.min(a.max, b.max);
|
|
1400
|
+
return clampRule({ required, min, max, sortOrder: Math.min(a.sortOrder, b.sortOrder) });
|
|
1401
|
+
}
|
|
1402
|
+
function toRule(row) {
|
|
1403
|
+
const required = row.required ?? false;
|
|
1404
|
+
return clampRule({
|
|
1405
|
+
required,
|
|
1406
|
+
// `required` is the gate: a stale min left on a placement that went optional is
|
|
1407
|
+
// ignored. A cleared min/max stays the explicit null it arrived as — nothing
|
|
1408
|
+
// downstream distinguishes null from absent.
|
|
1409
|
+
min: required ? row.min : void 0,
|
|
1410
|
+
max: row.max,
|
|
1411
|
+
sortOrder: row.sortOrder ?? 0
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
function toResolvedChoices(modifier) {
|
|
1415
|
+
return (modifier.choices ?? []).map((choice, index) => ({
|
|
1416
|
+
_id: String(choice._id),
|
|
1417
|
+
name: choice.name,
|
|
1418
|
+
price: choice.price ?? 0,
|
|
1419
|
+
isDefault: choice.isDefault,
|
|
1420
|
+
defaultQuantity: choice.defaultQuantity,
|
|
1421
|
+
unavailableOutletIds: choice.unavailableOutletIds?.map(String),
|
|
1422
|
+
sortOrder: choice.sortOrder ?? index
|
|
1423
|
+
})).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
1424
|
+
}
|
|
1425
|
+
function resolveModifiersForProduct(inputs) {
|
|
1426
|
+
const categoriesById = new Map(
|
|
1427
|
+
inputs.categories.filter((category) => !category.isDeleted).map((category) => [String(category._id), category])
|
|
1428
|
+
);
|
|
1429
|
+
const modifiersById = new Map(
|
|
1430
|
+
inputs.modifiers.filter((modifier) => !modifier.isDeleted).map((modifier) => [String(modifier._id), modifier])
|
|
1431
|
+
);
|
|
1432
|
+
const directCategories = new Set(inputs.categoryIds.map(String));
|
|
1433
|
+
const inheritedCategories = /* @__PURE__ */ new Set();
|
|
1434
|
+
for (const categoryId of directCategories) {
|
|
1435
|
+
for (const ancestor of ancestorChain(categoryId, categoriesById)) {
|
|
1436
|
+
if (!directCategories.has(ancestor)) inheritedCategories.add(ancestor);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const rules = /* @__PURE__ */ new Map();
|
|
1440
|
+
for (const placement of inputs.categoryModifiers) {
|
|
1441
|
+
if (placement.isDeleted) continue;
|
|
1442
|
+
const categoryId = String(placement.categoryId);
|
|
1443
|
+
const precedence = directCategories.has(categoryId) ? 0 : inheritedCategories.has(categoryId) ? 1 : -1;
|
|
1444
|
+
if (precedence === -1) continue;
|
|
1445
|
+
const modifierId = String(placement.modifierId);
|
|
1446
|
+
if (!modifiersById.has(modifierId)) continue;
|
|
1447
|
+
const incoming = toRule(placement);
|
|
1448
|
+
const existing = rules.get(modifierId);
|
|
1449
|
+
if (!existing || precedence < existing.precedence) {
|
|
1450
|
+
rules.set(modifierId, { precedence, rule: incoming });
|
|
1451
|
+
} else if (precedence === existing.precedence) {
|
|
1452
|
+
rules.set(modifierId, { precedence, rule: mergeRules(existing.rule, incoming) });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const resolved = [];
|
|
1456
|
+
for (const [modifierId, { rule }] of rules) {
|
|
1457
|
+
const modifier = modifiersById.get(modifierId);
|
|
1458
|
+
if (!modifier) continue;
|
|
1459
|
+
const required = rule.required;
|
|
1460
|
+
resolved.push({
|
|
1461
|
+
_id: modifierId,
|
|
1462
|
+
name: modifier.name,
|
|
1463
|
+
selectionType: modifier.selectionType,
|
|
1464
|
+
allowQuantity: modifier.allowQuantity,
|
|
1465
|
+
required,
|
|
1466
|
+
// single-selection is implicitly capped at 1 — min/max are never carried.
|
|
1467
|
+
min: modifier.selectionType === "single" ? void 0 : rule.min,
|
|
1468
|
+
max: modifier.selectionType === "single" ? void 0 : rule.max,
|
|
1469
|
+
sortOrder: rule.sortOrder,
|
|
1470
|
+
choices: toResolvedChoices(modifier)
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
return resolved.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name));
|
|
1474
|
+
}
|
|
1475
|
+
function isChoiceAvailableAtOutlet(choice, outletId) {
|
|
1476
|
+
return !(choice.unavailableOutletIds ?? []).includes(outletId);
|
|
1477
|
+
}
|
|
1478
|
+
function hasSellableChoice(modifier, outletId) {
|
|
1479
|
+
return modifier.choices.some((choice) => isChoiceAvailableAtOutlet(choice, outletId));
|
|
1480
|
+
}
|
|
1481
|
+
var modifiersSellable = (modifiers, outletId) => modifiers.every((modifier) => !modifier.required || hasSellableChoice(modifier, outletId));
|
|
1482
|
+
|
|
1483
|
+
// utils/composite-availability.util.ts
|
|
1484
|
+
function poolStockAt(rows, outletId) {
|
|
1485
|
+
if (!rows.some((row) => row.manageStock === true)) return null;
|
|
1486
|
+
return Math.max(0, rows.find((row) => String(row.outletId) === outletId)?.quantity ?? 0);
|
|
1487
|
+
}
|
|
1488
|
+
function compositeVariantInStock(variantId, quantity, outlet) {
|
|
1489
|
+
const shelf = outlet.shelves.get(variantId);
|
|
1490
|
+
if (!shelf || shelf.unresolved) return false;
|
|
1491
|
+
const onHand = outlet.poolStock.get(shelf.pool);
|
|
1492
|
+
if (onHand === void 0) return false;
|
|
1493
|
+
if (onHand === null || shelf.backorder) return true;
|
|
1494
|
+
return onHand >= (shelf.unit ? toBase(quantity, shelf.unit) : quantity);
|
|
1495
|
+
}
|
|
1496
|
+
function compositeItemUnavailable(item, rows) {
|
|
1497
|
+
const { outlet } = rows;
|
|
1498
|
+
const sellableHere = (productId2) => !outlet || !outlet.hiddenProductIds.has(productId2) && !outlet.unsellableProductIds.has(productId2);
|
|
1499
|
+
if (item.categoryId) {
|
|
1500
|
+
const variants = rows.categoryVariants.get(String(item.categoryId));
|
|
1501
|
+
if (!variants) return "deleted";
|
|
1502
|
+
const offered = variants.filter((variantId2) => sellableHere(rows.variantProducts.get(variantId2) ?? ""));
|
|
1503
|
+
if (!offered.length) return "empty";
|
|
1504
|
+
if (!outlet) return null;
|
|
1505
|
+
return offered.some((variantId2) => compositeVariantInStock(variantId2, item.quantity, outlet)) ? null : "out_of_stock";
|
|
1506
|
+
}
|
|
1507
|
+
const variantId = String(item.variantId);
|
|
1508
|
+
const productId = rows.variantProducts.get(variantId);
|
|
1509
|
+
if (!productId || !rows.productStatuses.has(productId)) return "deleted";
|
|
1510
|
+
if (rows.productStatuses.get(productId) !== "active" /* ACTIVE */) return "inactive";
|
|
1511
|
+
if (!outlet) return null;
|
|
1512
|
+
if (outlet.hiddenProductIds.has(productId)) return "hidden";
|
|
1513
|
+
if (outlet.unsellableProductIds.has(productId)) return "empty";
|
|
1514
|
+
if (rows.bookableProductIds.has(productId)) {
|
|
1515
|
+
if (!outlet.bookableSeats) return "by_date";
|
|
1516
|
+
return (outlet.bookableSeats.get(productId) ?? 0) >= item.quantity ? null : "fully_booked";
|
|
1517
|
+
}
|
|
1518
|
+
return compositeVariantInStock(variantId, item.quantity, outlet) ? null : "out_of_stock";
|
|
1519
|
+
}
|
|
1520
|
+
function compositeMakeable(parts, rows) {
|
|
1521
|
+
const { outlet } = rows;
|
|
1522
|
+
if (!outlet) throw new Error("compositeMakeable counts at one outlet: rows.outlet is required");
|
|
1523
|
+
const required = parts.map((part, partIndex) => ({ ...part, partIndex })).filter((part) => part.min > 0);
|
|
1524
|
+
const isBookable = (variantId) => rows.bookableProductIds.has(rows.variantProducts.get(String(variantId)) ?? "");
|
|
1525
|
+
if (required.some((part) => part.items.some((item) => item.variantId && isBookable(item.variantId))))
|
|
1526
|
+
return { byDate: true };
|
|
1527
|
+
const optionsOf = (item) => (item.categoryId ? rows.categoryVariants.get(String(item.categoryId)) ?? [] : [String(item.variantId)]).flatMap(
|
|
1528
|
+
(variantId) => {
|
|
1529
|
+
const reason = compositeItemUnavailable({ variantId, quantity: item.quantity }, rows);
|
|
1530
|
+
const shelf = outlet.shelves.get(variantId);
|
|
1531
|
+
if (reason && reason !== "out_of_stock" || !shelf) return [];
|
|
1532
|
+
const stock = outlet.poolStock.get(shelf.pool);
|
|
1533
|
+
const onHand = shelf.unresolved || stock === void 0 ? 0 : stock === null || shelf.backorder ? null : stock;
|
|
1534
|
+
const perPick = shelf.unit ? toBase(item.quantity, shelf.unit) : item.quantity;
|
|
1535
|
+
return [{ itemId: item.id, pool: shelf.pool, perPick, onHand }];
|
|
1536
|
+
}
|
|
1537
|
+
);
|
|
1538
|
+
const counted = required.map((part) => ({ ...part, options: part.items.flatMap(optionsOf) }));
|
|
1539
|
+
const order = [...counted].sort((a, b) => Number(a.options.length !== 1) - Number(b.options.length !== 1));
|
|
1540
|
+
const picksLeft = (option, reserved) => option.onHand === null ? Infinity : Math.max(0, Math.floor((option.onHand - (reserved.get(option.pool) ?? 0)) / option.perPick));
|
|
1541
|
+
const shortAt = (n) => {
|
|
1542
|
+
const reserved = /* @__PURE__ */ new Map();
|
|
1543
|
+
for (const part of order) {
|
|
1544
|
+
let wanted = part.min * n;
|
|
1545
|
+
const ranked = [...part.options].sort((a, b) => picksLeft(b, reserved) - picksLeft(a, reserved));
|
|
1546
|
+
for (const option of ranked) {
|
|
1547
|
+
if (wanted <= 0) break;
|
|
1548
|
+
const taken = Math.min(wanted, picksLeft(option, reserved));
|
|
1549
|
+
if (option.onHand !== null)
|
|
1550
|
+
reserved.set(option.pool, (reserved.get(option.pool) ?? 0) + taken * option.perPick);
|
|
1551
|
+
wanted -= taken;
|
|
1552
|
+
}
|
|
1553
|
+
if (wanted > 0) return part.partIndex;
|
|
1554
|
+
}
|
|
1555
|
+
return void 0;
|
|
1556
|
+
};
|
|
1557
|
+
const whyRows = [];
|
|
1558
|
+
const rowPools = /* @__PURE__ */ new Map();
|
|
1559
|
+
const makes = (has, uses) => has === null ? null : Math.floor(has / uses);
|
|
1560
|
+
for (const part of counted) {
|
|
1561
|
+
const [option] = part.options;
|
|
1562
|
+
if (part.options.length === 1) {
|
|
1563
|
+
const uses = option.perPick * part.min;
|
|
1564
|
+
const row = whyRows.find((candidate) => rowPools.get(candidate) === option.pool);
|
|
1565
|
+
if (!row) {
|
|
1566
|
+
const created = { itemIds: [option.itemId], uses, has: option.onHand, makes: makes(option.onHand, uses) };
|
|
1567
|
+
rowPools.set(created, option.pool);
|
|
1568
|
+
whyRows.push(created);
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
row.itemIds.push(option.itemId);
|
|
1572
|
+
row.uses += uses;
|
|
1573
|
+
if (option.onHand === null) row.has = null;
|
|
1574
|
+
row.makes = makes(row.has, row.uses);
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
const perPool = /* @__PURE__ */ new Map();
|
|
1578
|
+
for (const candidate of part.options)
|
|
1579
|
+
perPool.set(candidate.pool, Math.max(perPool.get(candidate.pool) ?? 0, picksLeft(candidate, /* @__PURE__ */ new Map())));
|
|
1580
|
+
const total = [...perPool.values()].reduce((sum, picks) => sum + picks, 0);
|
|
1581
|
+
const has = total === Infinity ? null : total;
|
|
1582
|
+
whyRows.push({
|
|
1583
|
+
itemIds: part.items.map((item) => item.id),
|
|
1584
|
+
partIndex: part.partIndex,
|
|
1585
|
+
uses: part.min,
|
|
1586
|
+
has,
|
|
1587
|
+
makes: makes(has, part.min)
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
if (counted.every((part) => part.options.some((option) => option.onHand === null)))
|
|
1591
|
+
return { count: null, rows: whyRows };
|
|
1592
|
+
const ceiling = Math.min(
|
|
1593
|
+
...counted.filter((part) => !part.options.some((option) => option.onHand === null)).map(
|
|
1594
|
+
(part) => Math.floor(part.options.reduce((sum, option) => sum + picksLeft(option, /* @__PURE__ */ new Map()), 0) / part.min)
|
|
1595
|
+
)
|
|
1596
|
+
);
|
|
1597
|
+
let [low, high] = [0, ceiling];
|
|
1598
|
+
while (low < high) {
|
|
1599
|
+
const mid = Math.ceil((low + high) / 2);
|
|
1600
|
+
if (shortAt(mid) === void 0) low = mid;
|
|
1601
|
+
else high = mid - 1;
|
|
1602
|
+
}
|
|
1603
|
+
const short = shortAt(low + 1);
|
|
1604
|
+
const shortPart = counted.find((part) => part.partIndex === short);
|
|
1605
|
+
const limitedBy = whyRows.findIndex(
|
|
1606
|
+
(row) => shortPart?.options.length === 1 ? row.partIndex === void 0 && row.itemIds.includes(shortPart.options[0].itemId) : row.partIndex === short
|
|
1607
|
+
);
|
|
1608
|
+
return { count: low, rows: whyRows, ...limitedBy >= 0 ? { limitedBy } : {} };
|
|
1609
|
+
}
|
|
1610
|
+
var poolOf = (line) => line.stockVariantId || line.variantId;
|
|
1611
|
+
function poolDemand(lines) {
|
|
1612
|
+
const demand = /* @__PURE__ */ new Map();
|
|
1613
|
+
const add = (line, times) => demand.set(
|
|
1614
|
+
poolOf(line),
|
|
1615
|
+
(demand.get(poolOf(line)) ?? 0) + (line.unit ? toBase(line.quantity * times, line.unit) : line.quantity * times)
|
|
1616
|
+
);
|
|
1617
|
+
for (const line of lines) {
|
|
1618
|
+
if (line.components) for (const component of line.components) add(component, line.quantity);
|
|
1619
|
+
else add(line, 1);
|
|
1620
|
+
}
|
|
1621
|
+
return demand;
|
|
1622
|
+
}
|
|
1623
|
+
function poolShortfall(demand, poolStock, backordered = /* @__PURE__ */ new Set()) {
|
|
1624
|
+
for (const [poolId, wanted] of demand) {
|
|
1625
|
+
const available = poolStock.get(poolId);
|
|
1626
|
+
if (available === null || available === void 0 || backordered.has(poolId)) continue;
|
|
1627
|
+
if (wanted > available) return { poolId, available, wanted };
|
|
1628
|
+
}
|
|
1629
|
+
return null;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// utils/composite-reservation.util.ts
|
|
1633
|
+
function compositeModifierMismatches(lineItems) {
|
|
1634
|
+
return lineItems.flatMap(
|
|
1635
|
+
(line) => line.components?.length ? (line.modifiers ?? []).flatMap(
|
|
1636
|
+
(modifier) => modifier.componentIndex !== void 0 && line.components[modifier.componentIndex] !== void 0 ? [] : [
|
|
1637
|
+
`Modifier ${modifier.modifierName ?? "?"} on composite line ${line.internalId ?? "?"} names no component (componentIndex ${modifier.componentIndex}) \u2014 a composite has no modifiers of its own`
|
|
1638
|
+
]
|
|
1639
|
+
) : []
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
function compositeReservationMismatches(order) {
|
|
1643
|
+
return (order.reservations ?? []).flatMap((reservation) => {
|
|
1644
|
+
if (reservation.lineItemInternalId === void 0) return [];
|
|
1645
|
+
const name = `Booking ${String(reservation.bookingId ?? "?")}`;
|
|
1646
|
+
const line = order.lineItems?.find((candidate) => candidate.internalId === reservation.lineItemInternalId);
|
|
1647
|
+
if (!line) return [`${name} names line ${reservation.lineItemInternalId}, which is not on the order`];
|
|
1648
|
+
const component = reservation.componentIndex === void 0 ? void 0 : line.components?.[reservation.componentIndex];
|
|
1649
|
+
if (!component) return [`${name} names no component ${reservation.componentIndex} of line ${line.internalId}`];
|
|
1650
|
+
if (String(component.productId) !== String(reservation.productId))
|
|
1651
|
+
return [
|
|
1652
|
+
`${name} is for ${String(reservation.productId)}, but that component sold ${String(component.productId)}`
|
|
1653
|
+
];
|
|
1654
|
+
if (reservation.price !== 0 || reservation.total !== 0)
|
|
1655
|
+
return [`${name} carries money (${reservation.price}/${reservation.total}); its component already does`];
|
|
1656
|
+
return [];
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1311
1660
|
// units/family-code.ts
|
|
1312
1661
|
var MAX_SLUG_LENGTH = 32;
|
|
1313
1662
|
var FALLBACK_SLUG = "family";
|
|
@@ -1650,6 +1999,15 @@ function sumRefundQuantities(refunds) {
|
|
|
1650
1999
|
}
|
|
1651
2000
|
return t;
|
|
1652
2001
|
}
|
|
2002
|
+
var asCount = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
2003
|
+
function countsOf(row) {
|
|
2004
|
+
const quantity = asCount(row.quantity);
|
|
2005
|
+
return {
|
|
2006
|
+
quantity,
|
|
2007
|
+
fulfilled: Math.min(asCount(row.fulfilledQuantity), quantity),
|
|
2008
|
+
returned: Math.min(asCount(row.returnedQuantity), quantity)
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
1653
2011
|
function buildOrderContext(order, extras) {
|
|
1654
2012
|
const o = order ?? {};
|
|
1655
2013
|
const total = typeof o.total === "number" && Number.isFinite(o.total) ? o.total : 0;
|
|
@@ -1657,6 +2015,7 @@ function buildOrderContext(order, extras) {
|
|
|
1657
2015
|
const paymentMethods = o.paymentMethods ?? [];
|
|
1658
2016
|
const refunds = o.refunds ?? o.refund ?? [];
|
|
1659
2017
|
const lineItems = o.lineItems ?? [];
|
|
2018
|
+
const reservations = o.reservations ?? [];
|
|
1660
2019
|
const totalPaid = sumNumbers(paymentMethods);
|
|
1661
2020
|
const totalRefunded = sumRefundQuantities(refunds);
|
|
1662
2021
|
const isFullyPaid = total > 0 ? balance <= 0 : totalPaid > 0 && balance <= 0;
|
|
@@ -1664,13 +2023,11 @@ function buildOrderContext(order, extras) {
|
|
|
1664
2023
|
let itemCount = 0;
|
|
1665
2024
|
let fulfilledItemCount = 0;
|
|
1666
2025
|
let returnedItemCount = 0;
|
|
1667
|
-
for (const
|
|
1668
|
-
const
|
|
1669
|
-
itemCount +=
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
const rq = typeof li.returnedQuantity === "number" && Number.isFinite(li.returnedQuantity) ? Math.max(0, li.returnedQuantity) : 0;
|
|
1673
|
-
returnedItemCount += Math.min(rq, q);
|
|
2026
|
+
for (const row of [...lineItems, ...reservations]) {
|
|
2027
|
+
const { quantity, fulfilled, returned } = countsOf(row);
|
|
2028
|
+
itemCount += quantity;
|
|
2029
|
+
fulfilledItemCount += fulfilled;
|
|
2030
|
+
returnedItemCount += returned;
|
|
1674
2031
|
}
|
|
1675
2032
|
const isFullyReturned = itemCount > 0 ? returnedItemCount >= itemCount : returnedItemCount > 0;
|
|
1676
2033
|
const hasPartialReturns = returnedItemCount > 0 && !isFullyReturned;
|
|
@@ -1680,7 +2037,8 @@ function buildOrderContext(order, extras) {
|
|
|
1680
2037
|
balance,
|
|
1681
2038
|
paymentMethods,
|
|
1682
2039
|
refunds,
|
|
1683
|
-
lineItems
|
|
2040
|
+
lineItems,
|
|
2041
|
+
reservations
|
|
1684
2042
|
},
|
|
1685
2043
|
computed: {
|
|
1686
2044
|
balance,
|
|
@@ -2760,43 +3118,408 @@ var ORDER_STATE_EVENTS = {
|
|
|
2760
3118
|
configUpdated: "config-updated"
|
|
2761
3119
|
};
|
|
2762
3120
|
|
|
2763
|
-
//
|
|
2764
|
-
var
|
|
2765
|
-
|
|
2766
|
-
|
|
3121
|
+
// booking/types.ts
|
|
3122
|
+
var BookingRulesError = class extends Error {
|
|
3123
|
+
constructor(message) {
|
|
3124
|
+
super(message);
|
|
3125
|
+
this.name = "BookingRulesError";
|
|
3126
|
+
}
|
|
3127
|
+
};
|
|
3128
|
+
|
|
3129
|
+
// booking/zoned-time.ts
|
|
3130
|
+
var MINUTE = 6e4;
|
|
3131
|
+
var formatters = /* @__PURE__ */ new Map();
|
|
3132
|
+
var formatterFor = (timeZone) => {
|
|
3133
|
+
const existing = formatters.get(timeZone);
|
|
3134
|
+
if (existing) return existing;
|
|
3135
|
+
let created;
|
|
3136
|
+
try {
|
|
3137
|
+
created = new Intl.DateTimeFormat("en-US", {
|
|
3138
|
+
timeZone,
|
|
3139
|
+
hour12: false,
|
|
3140
|
+
year: "numeric",
|
|
3141
|
+
month: "2-digit",
|
|
3142
|
+
day: "2-digit",
|
|
3143
|
+
hour: "2-digit",
|
|
3144
|
+
minute: "2-digit",
|
|
3145
|
+
second: "2-digit",
|
|
3146
|
+
weekday: "short"
|
|
3147
|
+
});
|
|
3148
|
+
} catch {
|
|
3149
|
+
throw new BookingRulesError(`Booking rules: "${timeZone}" is not a known time zone`);
|
|
3150
|
+
}
|
|
3151
|
+
formatters.set(timeZone, created);
|
|
3152
|
+
return created;
|
|
3153
|
+
};
|
|
3154
|
+
var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
3155
|
+
function zonedParts(instant, timeZone) {
|
|
3156
|
+
const parts = {};
|
|
3157
|
+
for (const { type, value } of formatterFor(timeZone).formatToParts(instant)) parts[type] = value;
|
|
3158
|
+
const hour = Number(parts.hour) % 24;
|
|
3159
|
+
return {
|
|
3160
|
+
year: Number(parts.year),
|
|
3161
|
+
month: Number(parts.month),
|
|
3162
|
+
day: Number(parts.day),
|
|
3163
|
+
hour,
|
|
3164
|
+
minute: Number(parts.minute),
|
|
3165
|
+
second: Number(parts.second),
|
|
3166
|
+
weekday: WEEKDAYS.indexOf(parts.weekday)
|
|
3167
|
+
};
|
|
3168
|
+
}
|
|
3169
|
+
function offsetAt(instant, timeZone) {
|
|
3170
|
+
const { year, month, day, hour, minute, second } = zonedParts(instant, timeZone);
|
|
3171
|
+
return Date.UTC(year, month - 1, day, hour, minute, second) - Math.floor(instant.getTime() / 1e3) * 1e3;
|
|
3172
|
+
}
|
|
3173
|
+
function instantFromLocal(year, month, day, minutes, timeZone) {
|
|
3174
|
+
const naive = Date.UTC(year, month - 1, day) + minutes * MINUTE;
|
|
3175
|
+
const firstGuess = naive - offsetAt(new Date(naive), timeZone);
|
|
3176
|
+
return new Date(naive - offsetAt(new Date(firstGuess), timeZone));
|
|
3177
|
+
}
|
|
3178
|
+
function startOfZonedDay(instant, timeZone) {
|
|
3179
|
+
const { year, month, day } = zonedParts(instant, timeZone);
|
|
3180
|
+
return instantFromLocal(year, month, day, 0, timeZone);
|
|
3181
|
+
}
|
|
3182
|
+
function nextZonedDay(dayStart, timeZone) {
|
|
3183
|
+
const { year, month, day } = zonedParts(dayStart, timeZone);
|
|
3184
|
+
return instantFromLocal(year, month, day + 1, 0, timeZone);
|
|
2767
3185
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
return
|
|
3186
|
+
function atZonedMinute(dayStart, minutes, timeZone) {
|
|
3187
|
+
const { year, month, day } = zonedParts(dayStart, timeZone);
|
|
3188
|
+
return instantFromLocal(year, month, day, minutes, timeZone);
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// booking/grid.ts
|
|
3192
|
+
var MINUTE2 = 6e4;
|
|
3193
|
+
var MAX_RANGE_DAYS = 366;
|
|
3194
|
+
var asTime = (value, field) => {
|
|
3195
|
+
if (value == null) return void 0;
|
|
3196
|
+
const time = new Date(value).getTime();
|
|
3197
|
+
if (Number.isNaN(time)) throw new BookingRulesError(`Booking rules: ${field} is not a date, got "${String(value)}"`);
|
|
3198
|
+
return time;
|
|
3199
|
+
};
|
|
3200
|
+
function parseHhMm(value, field) {
|
|
3201
|
+
const [hours, minutes] = value.split(":").map(Number);
|
|
3202
|
+
if (!Number.isInteger(hours) || !Number.isInteger(minutes)) {
|
|
3203
|
+
throw new BookingRulesError(`Booking rules: ${field} must be "HH:mm", got "${value}"`);
|
|
3204
|
+
}
|
|
3205
|
+
return hours * 60 + minutes;
|
|
3206
|
+
}
|
|
3207
|
+
function windowEndsAt(rule, now) {
|
|
3208
|
+
const { maxAdvanceDays, bookableTo } = rule.window;
|
|
3209
|
+
const ends = Math.min(
|
|
3210
|
+
maxAdvanceDays ? now.getTime() + maxAdvanceDays * 1440 * MINUTE2 : Number.POSITIVE_INFINITY,
|
|
3211
|
+
asTime(bookableTo, "window.bookableTo") ?? Number.POSITIVE_INFINITY
|
|
3212
|
+
);
|
|
3213
|
+
return Number.isFinite(ends) ? new Date(ends) : null;
|
|
3214
|
+
}
|
|
3215
|
+
function periodSlot(dayStart, timeZone, rule, openMinute, closeMinute) {
|
|
3216
|
+
const { ratePeriod, slot, checkInTime, checkOutTime } = rule;
|
|
3217
|
+
const at = (minutes) => atZonedMinute(dayStart, minutes, timeZone);
|
|
3218
|
+
if (ratePeriod === "night" /* NIGHT */) {
|
|
3219
|
+
if (!checkInTime || !checkOutTime) {
|
|
3220
|
+
throw new BookingRulesError("Overnight booking rules need both checkInTime and checkOutTime");
|
|
3221
|
+
}
|
|
3222
|
+
const checkIn = parseHhMm(checkInTime, "checkInTime");
|
|
3223
|
+
const checkOut = parseHhMm(checkOutTime, "checkOutTime");
|
|
3224
|
+
return {
|
|
3225
|
+
startAt: at(checkIn),
|
|
3226
|
+
endAt: at(1440 + checkOut),
|
|
3227
|
+
// Housekeeping sits between check-out and the next check-in: it costs no night.
|
|
3228
|
+
bufferEndAt: at(1440 + checkIn),
|
|
3229
|
+
canStart: true
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
const from = ratePeriod === "day" /* DAY */ ? openMinute ?? 0 : 0;
|
|
3233
|
+
const to = ratePeriod === "day" /* DAY */ ? closeMinute ?? 1440 : 1440;
|
|
3234
|
+
return {
|
|
3235
|
+
startAt: at(from),
|
|
3236
|
+
endAt: at(to),
|
|
3237
|
+
bufferEndAt: at(to + (slot.bufferMinutes ?? 0)),
|
|
3238
|
+
canStart: true
|
|
3239
|
+
};
|
|
2771
3240
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
3241
|
+
function freeGaps(openAt, closeAt, busy) {
|
|
3242
|
+
const inside = busy.map(({ startAt, bufferEndAt }) => ({ from: +startAt, to: +bufferEndAt })).filter(({ from, to }) => to > openAt && from < closeAt).sort((first, second) => first.from - second.from);
|
|
3243
|
+
const gaps = [];
|
|
3244
|
+
let cursor = openAt;
|
|
3245
|
+
for (const { from, to } of inside) {
|
|
3246
|
+
if (from > cursor) gaps.push({ from: cursor, to: from, endsAtBooking: true });
|
|
3247
|
+
cursor = Math.max(cursor, to);
|
|
3248
|
+
if (cursor >= closeAt) break;
|
|
3249
|
+
}
|
|
3250
|
+
if (cursor < closeAt) gaps.push({ from: cursor, to: closeAt, endsAtBooking: false });
|
|
3251
|
+
return gaps;
|
|
3252
|
+
}
|
|
3253
|
+
function buildGrid(rule, timeZone, from, to, now, busy = []) {
|
|
3254
|
+
if (to.getTime() - from.getTime() > MAX_RANGE_DAYS * 1440 * MINUTE2) {
|
|
3255
|
+
throw new BookingRulesError(`A booking range may span at most ${MAX_RANGE_DAYS} days`);
|
|
3256
|
+
}
|
|
3257
|
+
const { availability, slot, window, ratePeriod } = rule;
|
|
3258
|
+
const periodMode = ratePeriod !== "slot" /* SLOT */;
|
|
3259
|
+
const weeklyHours = availability.weeklyHours ?? [];
|
|
3260
|
+
const notBefore = Math.max(
|
|
3261
|
+
now.getTime() + (window.minNoticeMinutes ?? 0) * MINUTE2,
|
|
3262
|
+
asTime(window.bookableFrom, "window.bookableFrom") ?? 0,
|
|
3263
|
+
asTime(availability.availableFrom, "availability.availableFrom") ?? 0
|
|
3264
|
+
);
|
|
3265
|
+
const notAfter = windowEndsAt(rule, now)?.getTime() ?? Number.POSITIVE_INFINITY;
|
|
3266
|
+
const blocked = (availability.offMarket ?? []).map(({ from: blockFrom, to: blockTo }) => ({
|
|
3267
|
+
from: asTime(blockFrom, "availability.offMarket.from"),
|
|
3268
|
+
to: asTime(blockTo, "availability.offMarket.to")
|
|
3269
|
+
}));
|
|
3270
|
+
const arrivalDays = availability.arrivalDays;
|
|
3271
|
+
const step = slot.lengthMinutes + (slot.bufferMinutes ?? 0);
|
|
3272
|
+
const slots = [];
|
|
3273
|
+
const lastDay = startOfZonedDay(to, timeZone).getTime();
|
|
3274
|
+
let dayStart = startOfZonedDay(from, timeZone);
|
|
3275
|
+
while (dayStart.getTime() <= lastDay) {
|
|
3276
|
+
const weekday = zonedParts(dayStart, timeZone).weekday;
|
|
3277
|
+
const rows = weeklyHours.filter((entry) => entry.weekday === weekday);
|
|
3278
|
+
if (periodMode) {
|
|
3279
|
+
const openToday = weeklyHours.length === 0 || rows.length > 0;
|
|
3280
|
+
if (openToday) {
|
|
3281
|
+
const built = periodSlot(dayStart, timeZone, rule, rows[0]?.openMinute, rows[0]?.closeMinute);
|
|
3282
|
+
slots.push({ ...built, canStart: !arrivalDays?.length || arrivalDays.includes(weekday) });
|
|
3283
|
+
}
|
|
3284
|
+
} else {
|
|
3285
|
+
for (const row of rows) {
|
|
3286
|
+
const openAt = +atZonedMinute(dayStart, row.openMinute, timeZone);
|
|
3287
|
+
const closeAt = +atZonedMinute(dayStart, row.closeMinute, timeZone);
|
|
3288
|
+
for (const gap of freeGaps(openAt, closeAt, busy)) {
|
|
3289
|
+
const mustFit = (gap.endsAtBooking ? step : slot.lengthMinutes) * MINUTE2;
|
|
3290
|
+
for (let start = gap.from; start + mustFit <= gap.to; start += step * MINUTE2) {
|
|
3291
|
+
slots.push({
|
|
3292
|
+
startAt: new Date(start),
|
|
3293
|
+
endAt: new Date(start + slot.lengthMinutes * MINUTE2),
|
|
3294
|
+
bufferEndAt: new Date(start + step * MINUTE2),
|
|
3295
|
+
canStart: true
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
dayStart = nextZonedDay(dayStart, timeZone);
|
|
3302
|
+
}
|
|
3303
|
+
const askedFrom = from.getTime();
|
|
3304
|
+
const askedTo = to.getTime();
|
|
3305
|
+
return slots.filter(({ startAt, bufferEndAt }) => {
|
|
3306
|
+
const start = startAt.getTime();
|
|
3307
|
+
if (start < askedFrom || start >= askedTo) return false;
|
|
3308
|
+
if (start < notBefore || start > notAfter) return false;
|
|
3309
|
+
return !blocked.some(
|
|
3310
|
+
({ from: blockFrom, to: blockTo }) => start < blockTo && bufferEndAt.getTime() > blockFrom
|
|
3311
|
+
);
|
|
3312
|
+
}).sort((first, second) => +first.startAt - +second.startAt);
|
|
3313
|
+
}
|
|
3314
|
+
function slotsForWindow(grid, startAt, endAt) {
|
|
3315
|
+
const startIndex = grid.findIndex((entry) => entry.startAt.getTime() === startAt.getTime());
|
|
3316
|
+
if (startIndex < 0) {
|
|
3317
|
+
throw new BookingRulesError(`${startAt.toISOString()} is not a bookable slot start for these rules`);
|
|
3318
|
+
}
|
|
3319
|
+
if (!grid[startIndex].canStart) {
|
|
3320
|
+
throw new BookingRulesError(`${startAt.toISOString()} is not an arrival day for these rules`);
|
|
3321
|
+
}
|
|
3322
|
+
const covered = [];
|
|
3323
|
+
for (let index = startIndex; index < grid.length; index++) {
|
|
3324
|
+
const entry = grid[index];
|
|
3325
|
+
const previous = covered[covered.length - 1];
|
|
3326
|
+
if (previous && entry.startAt.getTime() !== previous.bufferEndAt.getTime()) break;
|
|
3327
|
+
covered.push(entry);
|
|
3328
|
+
if (entry.endAt.getTime() === endAt.getTime()) return covered;
|
|
3329
|
+
if (entry.endAt.getTime() > endAt.getTime()) break;
|
|
3330
|
+
}
|
|
3331
|
+
throw new BookingRulesError(
|
|
3332
|
+
`${startAt.toISOString()} \u2192 ${endAt.toISOString()} does not line up with the bookable grid`
|
|
3333
|
+
);
|
|
2775
3334
|
}
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
3335
|
+
|
|
3336
|
+
// booking/availability.ts
|
|
3337
|
+
var withinPresence = (slot, timeZone, presence) => {
|
|
3338
|
+
if (!presence?.length) return true;
|
|
3339
|
+
const dayStart = startOfZonedDay(slot.startAt, timeZone);
|
|
3340
|
+
const weekday = zonedParts(dayStart, timeZone).weekday;
|
|
3341
|
+
return presence.some(
|
|
3342
|
+
(shift) => shift.weekday === weekday && slot.startAt.getTime() >= atZonedMinute(dayStart, shift.openMinute, timeZone).getTime() && slot.endAt.getTime() <= atZonedMinute(dayStart, shift.closeMinute, timeZone).getTime()
|
|
3343
|
+
);
|
|
3344
|
+
};
|
|
3345
|
+
var overlaps = (entry, startAt, bufferEndAt) => entry.startAt.getTime() < bufferEndAt.getTime() && entry.bufferEndAt.getTime() > startAt.getTime();
|
|
3346
|
+
function occupancyIn(occupied, productId, startAt, bufferEndAt, shared) {
|
|
3347
|
+
const clashing = occupied.filter((entry) => overlaps(entry, startAt, bufferEndAt));
|
|
3348
|
+
const foreign = shared ? void 0 : clashing.find((entry) => entry.productId !== productId);
|
|
3349
|
+
return {
|
|
3350
|
+
taken: clashing.filter((entry) => shared || entry.productId === productId).reduce((sum, entry) => sum + entry.qty, 0),
|
|
3351
|
+
foreignProductId: foreign?.productId
|
|
3352
|
+
};
|
|
2779
3353
|
}
|
|
2780
|
-
var
|
|
2781
|
-
|
|
3354
|
+
var stateFor = (hasShift, offerable, free) => {
|
|
3355
|
+
if (offerable === 0) {
|
|
3356
|
+
return hasShift() ? "fullyBooked" /* FULLY_BOOKED */ : "noHoursInRange" /* NO_HOURS_IN_RANGE */;
|
|
3357
|
+
}
|
|
3358
|
+
return free === 0 ? "fullyBooked" /* FULLY_BOOKED */ : "available" /* AVAILABLE */;
|
|
2782
3359
|
};
|
|
2783
|
-
function
|
|
2784
|
-
|
|
3360
|
+
function shopDayKey(instant, timeZone) {
|
|
3361
|
+
const { year, month, day } = zonedParts(instant, timeZone);
|
|
3362
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
3363
|
+
return `${year}-${pad(month)}-${pad(day)}`;
|
|
3364
|
+
}
|
|
3365
|
+
function shopDayRange(fromDayKey, count, timeZone) {
|
|
3366
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(fromDayKey ?? "");
|
|
3367
|
+
if (!match) {
|
|
3368
|
+
throw new BookingRulesError(`Booking rules: "${fromDayKey}" is not a shop day (YYYY-MM-DD)`);
|
|
3369
|
+
}
|
|
3370
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
3371
|
+
throw new BookingRulesError(`Booking rules: "${count}" is not a number of days`);
|
|
3372
|
+
}
|
|
3373
|
+
const [, year, month, day] = match.map(Number);
|
|
3374
|
+
const from = instantFromLocal(year, month, day, 0, timeZone);
|
|
3375
|
+
if (shopDayKey(from, timeZone) !== fromDayKey) {
|
|
3376
|
+
throw new BookingRulesError(`Booking rules: "${fromDayKey}" is not a real day`);
|
|
3377
|
+
}
|
|
3378
|
+
return { from, to: instantFromLocal(year, month, day + count, 0, timeZone) };
|
|
3379
|
+
}
|
|
3380
|
+
function buildAvailability({
|
|
3381
|
+
productId,
|
|
3382
|
+
bookingRulesId,
|
|
3383
|
+
bookingType,
|
|
3384
|
+
rule,
|
|
3385
|
+
resources,
|
|
3386
|
+
occupied,
|
|
3387
|
+
from,
|
|
3388
|
+
to,
|
|
3389
|
+
now
|
|
3390
|
+
}) {
|
|
3391
|
+
const shared = rule.slot.sharedAcrossProducts ?? false;
|
|
3392
|
+
const bookableUntil = windowEndsAt(rule, now);
|
|
3393
|
+
const header = {
|
|
3394
|
+
productId,
|
|
3395
|
+
bookingRulesId,
|
|
3396
|
+
bookingType,
|
|
3397
|
+
ratePeriod: rule.ratePeriod,
|
|
3398
|
+
// Passed through for the UI to label: a group slot under minimumToRun may not run.
|
|
3399
|
+
minimumToRun: rule.slot.minimumToRun,
|
|
3400
|
+
minStay: rule.slot.minStay,
|
|
3401
|
+
maxStay: rule.slot.maxStay,
|
|
3402
|
+
// Why a day can come back empty. Past this instant the rule refuses everything, so the UI
|
|
3403
|
+
// can say "beyond the booking window" where it would otherwise claim "nothing free".
|
|
3404
|
+
bookableUntil,
|
|
3405
|
+
// The comparison itself, not just the boundary: deciding whether a day lies past the
|
|
3406
|
+
// window is a rule, and a UI that re-derives it owns a copy of that rule — which is how
|
|
3407
|
+
// the same empty grid ends up explained one way on the till and another in a flow.
|
|
3408
|
+
beyondWindow: !!bookableUntil && from.getTime() > bookableUntil.getTime()
|
|
3409
|
+
};
|
|
3410
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
3411
|
+
for (const entry of occupied) {
|
|
3412
|
+
byResource.set(entry.resourceId, [...byResource.get(entry.resourceId) ?? [], entry]);
|
|
3413
|
+
}
|
|
3414
|
+
const grids = resources.map((resource) => {
|
|
3415
|
+
const mine = byResource.get(resource.id) ?? [];
|
|
3416
|
+
const busy = (resource.seats ?? 1) === 1 ? mine : [];
|
|
3417
|
+
const slots2 = buildGrid(rule, resource.timeZone, from, to, now, busy).filter(
|
|
3418
|
+
(slot) => withinPresence(slot, resource.timeZone, resource.presence)
|
|
3419
|
+
);
|
|
3420
|
+
return { resource, slots: slots2 };
|
|
3421
|
+
});
|
|
3422
|
+
const byInstant = /* @__PURE__ */ new Map();
|
|
3423
|
+
const freeSlotsPerResource = /* @__PURE__ */ new Map();
|
|
3424
|
+
for (const { resource, slots: slots2 } of grids) {
|
|
3425
|
+
const capacity = resource.seats ?? 1;
|
|
3426
|
+
const mine = byResource.get(resource.id) ?? [];
|
|
3427
|
+
for (const slot of slots2) {
|
|
3428
|
+
const { taken, foreignProductId } = occupancyIn(mine, productId, slot.startAt, slot.bufferEndAt, shared);
|
|
3429
|
+
const free = foreignProductId ? 0 : Math.max(capacity - taken, 0);
|
|
3430
|
+
const entry = byInstant.get(+slot.startAt) ?? {
|
|
3431
|
+
...slot,
|
|
3432
|
+
capacity: 0,
|
|
3433
|
+
booked: 0,
|
|
3434
|
+
free: 0,
|
|
3435
|
+
resources: [],
|
|
3436
|
+
dayKey: shopDayKey(slot.startAt, resource.timeZone)
|
|
3437
|
+
};
|
|
3438
|
+
entry.capacity += capacity;
|
|
3439
|
+
entry.booked += foreignProductId ? capacity : taken;
|
|
3440
|
+
entry.free += free;
|
|
3441
|
+
entry.canStart = entry.canStart || slot.canStart;
|
|
3442
|
+
entry.resources.push({ resourceId: resource.id, name: resource.name, capacity, booked: taken, free });
|
|
3443
|
+
if (free > 0) freeSlotsPerResource.set(resource.id, (freeSlotsPerResource.get(resource.id) ?? 0) + 1);
|
|
3444
|
+
byInstant.set(+slot.startAt, entry);
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
const inWindow = grids.map(({ resource, slots: slots2 }) => {
|
|
3448
|
+
const free = freeSlotsPerResource.get(resource.id) ?? 0;
|
|
3449
|
+
return {
|
|
3450
|
+
resourceId: resource.id,
|
|
3451
|
+
name: resource.name,
|
|
3452
|
+
slots: slots2.length,
|
|
3453
|
+
free,
|
|
3454
|
+
// Lazily: the shift grid is a second walk, and only an empty answer needs it.
|
|
3455
|
+
state: stateFor(
|
|
3456
|
+
() => buildGrid(rule, resource.timeZone, from, to, now).filter(
|
|
3457
|
+
(slot) => withinPresence(slot, resource.timeZone, resource.presence)
|
|
3458
|
+
).length > 0,
|
|
3459
|
+
slots2.length,
|
|
3460
|
+
free
|
|
3461
|
+
)
|
|
3462
|
+
};
|
|
3463
|
+
});
|
|
3464
|
+
const slots = [...byInstant.values()].sort((first, second) => +first.startAt - +second.startAt);
|
|
3465
|
+
return {
|
|
3466
|
+
...header,
|
|
3467
|
+
resources: inWindow,
|
|
3468
|
+
slots,
|
|
3469
|
+
/**
|
|
3470
|
+
* The clock every instant above reads on. Returned rather than assumed: without it a caller
|
|
3471
|
+
* has to fetch the zone from somewhere else and hope it is the same one the grid used.
|
|
3472
|
+
*/
|
|
3473
|
+
timeZone: resources[0]?.timeZone ?? null,
|
|
3474
|
+
/** The shop days this answer covers, in order — what a day picker lists. */
|
|
3475
|
+
days: [...new Set(slots.map((slot) => slot.dayKey))]
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
function presentAt(resources, rows, outletId) {
|
|
3479
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
3480
|
+
for (const row of rows) {
|
|
3481
|
+
byResource.set(row.resourceId, [...byResource.get(row.resourceId) ?? [], row]);
|
|
3482
|
+
}
|
|
3483
|
+
return resources.flatMap((resource) => {
|
|
3484
|
+
const own = byResource.get(resource.id) ?? [];
|
|
3485
|
+
if (own.length === 0 || !outletId) return [{ resource }];
|
|
3486
|
+
const here = own.find((row) => row.outletId === outletId);
|
|
3487
|
+
return here ? [{ resource, presence: here.weeklyHours }] : [];
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
// booking/plan.ts
|
|
3492
|
+
var DEFAULT_HOLD_MINUTES = 10;
|
|
3493
|
+
function planWindow({ rule, timeZone, presence, startAt, endAt, now, busy = [] }) {
|
|
3494
|
+
const grid = buildGrid(rule, timeZone, startAt, endAt, now, busy).filter(
|
|
3495
|
+
(slot) => withinPresence(slot, timeZone, presence)
|
|
3496
|
+
);
|
|
3497
|
+
const slots = slotsForWindow(grid, startAt, endAt);
|
|
3498
|
+
const { minStay, maxStay } = rule.slot;
|
|
3499
|
+
if (minStay && slots.length < minStay) {
|
|
3500
|
+
throw new BookingRulesError(`These rules ask for at least ${minStay} period(s), got ${slots.length}`);
|
|
3501
|
+
}
|
|
3502
|
+
if (maxStay && slots.length > maxStay) {
|
|
3503
|
+
throw new BookingRulesError(`These rules allow at most ${maxStay} period(s), got ${slots.length}`);
|
|
3504
|
+
}
|
|
3505
|
+
const last = slots[slots.length - 1];
|
|
3506
|
+
return { slots, endAt: last.endAt, bufferEndAt: last.bufferEndAt };
|
|
2785
3507
|
}
|
|
2786
|
-
var
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
3508
|
+
var holdExpiresAt = (rule, now) => new Date(now.getTime() + (rule.window.holdMinutes ?? DEFAULT_HOLD_MINUTES) * 6e4);
|
|
3509
|
+
|
|
3510
|
+
// constants/adyen.constants.ts
|
|
3511
|
+
var ADYEN_ROLLOUT_COUNTRY_CODES = /* @__PURE__ */ new Set(["CA"]);
|
|
3512
|
+
function isAdyenRolloutCountry(alpha2) {
|
|
3513
|
+
return ADYEN_ROLLOUT_COUNTRY_CODES.has(alpha2);
|
|
2790
3514
|
}
|
|
2791
3515
|
export {
|
|
2792
|
-
ADYEN_JOB_TITLE_ASSOCIATION_TYPES,
|
|
2793
|
-
ADYEN_LEGAL_ENTITY_STATE_PROVINCE_COUNTRIES,
|
|
2794
|
-
ADYEN_NO_TAX_NUMBER_ABSENT_COUNTRIES,
|
|
2795
3516
|
ADYEN_ROLLOUT_COUNTRY_CODES,
|
|
2796
|
-
ADYEN_STORE_STATE_PROVINCE_COUNTRIES,
|
|
2797
|
-
ADYEN_TAX_NUMBER_FORMATS,
|
|
2798
|
-
ADYEN_TAX_NUMBER_MIN_LENGTH,
|
|
2799
3517
|
AttributeType,
|
|
3518
|
+
BookingRatePeriod,
|
|
3519
|
+
BookingResourceKind,
|
|
3520
|
+
BookingResourceStatus,
|
|
3521
|
+
BookingRulesError,
|
|
3522
|
+
BookingType,
|
|
2800
3523
|
CONDITION_OPERATORS,
|
|
2801
3524
|
COUNTRY_CURRENCIES,
|
|
2802
3525
|
CURRENCY_CONFIG,
|
|
@@ -2807,6 +3530,7 @@ export {
|
|
|
2807
3530
|
DEFAULT_CONFIG,
|
|
2808
3531
|
DEFAULT_CROSS_AXIS_RULES,
|
|
2809
3532
|
DEFAULT_DISPLAY_STATE_MAP,
|
|
3533
|
+
DEFAULT_HOLD_MINUTES,
|
|
2810
3534
|
DEFAULT_RESOLVED_CONFIG,
|
|
2811
3535
|
DEFAULT_UNIT_ID,
|
|
2812
3536
|
DualWriteStrategy,
|
|
@@ -2850,8 +3574,10 @@ export {
|
|
|
2850
3574
|
REPORT_NEGATE_PATHS,
|
|
2851
3575
|
ReceiptSendChannel,
|
|
2852
3576
|
ReceiptType,
|
|
3577
|
+
ReservationStatus,
|
|
2853
3578
|
ResidualPaymentMethod,
|
|
2854
3579
|
ResidualStatus,
|
|
3580
|
+
ResourceAvailabilityState,
|
|
2855
3581
|
STATE_SCHEMA,
|
|
2856
3582
|
STRIPE_SUPPORTED_COUNTRIES,
|
|
2857
3583
|
SYSTEM_FAMILIES,
|
|
@@ -2865,23 +3591,29 @@ export {
|
|
|
2865
3591
|
TopMetericsSort,
|
|
2866
3592
|
UserTypes,
|
|
2867
3593
|
addAmounts,
|
|
2868
|
-
adyenLegalEntityRequiresStateOrProvince,
|
|
2869
|
-
adyenRequiresJobTitle,
|
|
2870
|
-
adyenStoreRequiresStateOrProvince,
|
|
2871
|
-
adyenSupportsTaxNumberAbsent,
|
|
2872
|
-
adyenTaxNumberFormat,
|
|
2873
3594
|
amountToString,
|
|
3595
|
+
ancestorChain,
|
|
2874
3596
|
assertUnitSane,
|
|
2875
3597
|
availableIn,
|
|
2876
3598
|
baseFor,
|
|
2877
3599
|
baseFulfillmentDefinitions,
|
|
2878
3600
|
baseOf,
|
|
2879
3601
|
basePaymentDefinitions,
|
|
3602
|
+
buildAvailability,
|
|
3603
|
+
buildGrid,
|
|
2880
3604
|
buildOrderContext,
|
|
2881
3605
|
buildPosPresetConfig,
|
|
2882
3606
|
buildResolvedConfig,
|
|
2883
3607
|
calculatePercentage,
|
|
2884
3608
|
canTransition,
|
|
3609
|
+
cheapestCompositeItem,
|
|
3610
|
+
cheapestCompositePrice,
|
|
3611
|
+
compositeItemCost,
|
|
3612
|
+
compositeItemUnavailable,
|
|
3613
|
+
compositeMakeable,
|
|
3614
|
+
compositeModifierMismatches,
|
|
3615
|
+
compositeReservationMismatches,
|
|
3616
|
+
compositeVariantInStock,
|
|
2885
3617
|
conditionSummary,
|
|
2886
3618
|
convertFixedFeeToLocalCurrency,
|
|
2887
3619
|
createAmount,
|
|
@@ -2916,8 +3648,11 @@ export {
|
|
|
2916
3648
|
getPricingRegionFromCountry,
|
|
2917
3649
|
getSettlementTotal,
|
|
2918
3650
|
getTotalCollected,
|
|
3651
|
+
hasSellableChoice,
|
|
3652
|
+
holdExpiresAt,
|
|
2919
3653
|
inferStatesFromLegacyStatus,
|
|
2920
3654
|
isAdyenRolloutCountry,
|
|
3655
|
+
isChoiceAvailableAtOutlet,
|
|
2921
3656
|
isConditionOperator,
|
|
2922
3657
|
isFulfillmentState,
|
|
2923
3658
|
isPaymentState,
|
|
@@ -2928,25 +3663,39 @@ export {
|
|
|
2928
3663
|
mapToLegacyStatus,
|
|
2929
3664
|
maxPrecisionFor,
|
|
2930
3665
|
mergeFragment,
|
|
3666
|
+
modifiersSellable,
|
|
2931
3667
|
multiplyAmount,
|
|
2932
3668
|
normalizeMongoIdString,
|
|
3669
|
+
occupancyIn,
|
|
2933
3670
|
parseAmountToMinorUnits,
|
|
2934
3671
|
parseCurrencyCode,
|
|
2935
3672
|
parseStripeCurrencyCode,
|
|
3673
|
+
planWindow,
|
|
3674
|
+
poolDemand,
|
|
3675
|
+
poolOf,
|
|
3676
|
+
poolShortfall,
|
|
3677
|
+
poolStockAt,
|
|
3678
|
+
presentAt,
|
|
2936
3679
|
removeFragment,
|
|
2937
3680
|
resolveFamily,
|
|
3681
|
+
resolveModifiersForProduct,
|
|
2938
3682
|
resolveTemplate,
|
|
2939
3683
|
resolveUnit,
|
|
2940
3684
|
roundToPrecision,
|
|
2941
3685
|
sameFamily,
|
|
2942
3686
|
setOrderState,
|
|
3687
|
+
shopDayKey,
|
|
3688
|
+
shopDayRange,
|
|
2943
3689
|
signReportForDisplay,
|
|
3690
|
+
slotsForWindow,
|
|
2944
3691
|
stringifyMinorUnits,
|
|
2945
3692
|
subtractAmounts,
|
|
2946
3693
|
toBase,
|
|
2947
3694
|
toMinorUnits,
|
|
2948
3695
|
validateConfig,
|
|
2949
3696
|
validateFragmentCompatibility,
|
|
2950
|
-
violatesFinancialInvariant
|
|
3697
|
+
violatesFinancialInvariant,
|
|
3698
|
+
windowEndsAt,
|
|
3699
|
+
withinPresence
|
|
2951
3700
|
};
|
|
2952
3701
|
//# sourceMappingURL=index.mjs.map
|