@pisell/pisellos 0.10.49 → 0.10.51

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.
Files changed (62) hide show
  1. package/dist/modules/Discount/types.d.ts +2 -0
  2. package/dist/modules/FulfillmentTiming/index.d.ts +11 -0
  3. package/dist/modules/FulfillmentTiming/index.js +407 -0
  4. package/dist/modules/FulfillmentTiming/types.d.ts +67 -0
  5. package/dist/modules/FulfillmentTiming/types.js +1 -0
  6. package/dist/modules/OpenData/types.d.ts +6 -0
  7. package/dist/modules/Order/index.d.ts +19 -1
  8. package/dist/modules/Order/index.js +362 -217
  9. package/dist/modules/Order/pickupTiming.d.ts +46 -0
  10. package/dist/modules/Order/pickupTiming.js +53 -0
  11. package/dist/modules/Order/types.d.ts +11 -1
  12. package/dist/modules/Order/utils.d.ts +10 -0
  13. package/dist/modules/Order/utils.js +145 -85
  14. package/dist/modules/Rules/index.js +45 -26
  15. package/dist/modules/Rules/types.d.ts +4 -0
  16. package/dist/modules/index.d.ts +1 -0
  17. package/dist/modules/index.js +1 -0
  18. package/dist/server/modules/order/index.d.ts +8 -0
  19. package/dist/server/modules/order/index.js +506 -453
  20. package/dist/server/modules/order/types.d.ts +1 -0
  21. package/dist/server/utils/small-ticket.js +173 -95
  22. package/dist/solution/BaseSales/index.d.ts +23 -0
  23. package/dist/solution/BaseSales/index.js +1216 -1031
  24. package/dist/solution/BaseSales/pickupTiming.d.ts +37 -0
  25. package/dist/solution/BaseSales/pickupTiming.js +482 -0
  26. package/dist/solution/BaseSales/utils/parseSalesResponse.js +3 -0
  27. package/dist/solution/BookingByStep/index.d.ts +1 -1
  28. package/dist/solution/BookingTicket/utils/cartView.js +3 -1
  29. package/dist/solution/UnifiedBookingSales/index.d.ts +2 -0
  30. package/dist/solution/UnifiedBookingSales/index.js +583 -531
  31. package/lib/model/strategy/adapter/promotion/index.js +46 -1
  32. package/lib/modules/Discount/types.d.ts +2 -0
  33. package/lib/modules/FulfillmentTiming/index.d.ts +11 -0
  34. package/lib/modules/FulfillmentTiming/index.js +359 -0
  35. package/lib/modules/FulfillmentTiming/types.d.ts +67 -0
  36. package/lib/modules/FulfillmentTiming/types.js +5 -0
  37. package/lib/modules/OpenData/types.d.ts +6 -0
  38. package/lib/modules/Order/index.d.ts +19 -1
  39. package/lib/modules/Order/index.js +120 -7
  40. package/lib/modules/Order/pickupTiming.d.ts +46 -0
  41. package/lib/modules/Order/pickupTiming.js +61 -0
  42. package/lib/modules/Order/types.d.ts +11 -1
  43. package/lib/modules/Order/utils.d.ts +10 -0
  44. package/lib/modules/Order/utils.js +58 -4
  45. package/lib/modules/Rules/index.js +24 -13
  46. package/lib/modules/Rules/types.d.ts +4 -0
  47. package/lib/modules/index.d.ts +1 -0
  48. package/lib/modules/index.js +11 -0
  49. package/lib/server/modules/order/index.d.ts +8 -0
  50. package/lib/server/modules/order/index.js +19 -0
  51. package/lib/server/modules/order/types.d.ts +1 -0
  52. package/lib/server/utils/small-ticket.js +93 -22
  53. package/lib/solution/BaseSales/index.d.ts +23 -0
  54. package/lib/solution/BaseSales/index.js +104 -1
  55. package/lib/solution/BaseSales/pickupTiming.d.ts +37 -0
  56. package/lib/solution/BaseSales/pickupTiming.js +222 -0
  57. package/lib/solution/BaseSales/utils/parseSalesResponse.js +3 -0
  58. package/lib/solution/BookingByStep/index.d.ts +1 -1
  59. package/lib/solution/BookingTicket/utils/cartView.js +3 -1
  60. package/lib/solution/UnifiedBookingSales/index.d.ts +2 -0
  61. package/lib/solution/UnifiedBookingSales/index.js +20 -0
  62. package/package.json +1 -1
@@ -44,6 +44,7 @@ interface ApplicableProductDetails {
44
44
  original_amount: string;
45
45
  num: number;
46
46
  metadata?: {
47
+ encoded?: string;
47
48
  product_discount_difference?: number;
48
49
  /** 月卡扣除本单新增使用后的剩余次数;不限次数时为 null。 */
49
50
  balance?: number | null;
@@ -135,6 +136,7 @@ export interface Discount {
135
136
  available_products_info?: any[];
136
137
  format_title: Formattitle;
137
138
  metadata?: {
139
+ encoded?: string;
138
140
  discount_card_type?: 'fixed_amount' | 'percent';
139
141
  custom_product_bundle_map_id?: string;
140
142
  validity_type?: 'custom_schedule_validity' | 'fixed_validity';
@@ -0,0 +1,11 @@
1
+ import type { PickupTimingAvailability, PickupTimingBookingTimes, PickupTimingClockInput, PickupTimingComputeInput, PickupTimingConfig, PickupTimingSelection, PickupTimingSlot, PickupTimingValidationResult } from './types';
2
+ export * from './types';
3
+ /** Consume effective OpenData values only; legacy switches never enable Pickup. */
4
+ export declare function normalizePickupTimingConfig(openData: Record<string, unknown>): PickupTimingConfig;
5
+ export declare function resolvePickupTimingDateTime(date: string, time: string, timezone: string, instant?: string): PickupTimingSlot | undefined;
6
+ export declare function getAsapPickupTiming({ config, timezone, now }: PickupTimingClockInput): PickupTimingSlot;
7
+ export declare function pickupTimingSlotToBookingTimes(slot: PickupTimingSlot): PickupTimingBookingTimes;
8
+ export declare function computePickupTimingAvailability(input: PickupTimingComputeInput): PickupTimingAvailability;
9
+ export declare function validatePickupTimingSelection(selection: PickupTimingSelection, input: PickupTimingComputeInput & {
10
+ allowManualOverride?: boolean;
11
+ }): PickupTimingValidationResult;
@@ -0,0 +1,407 @@
1
+ function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
2
+ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
3
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
4
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
5
+ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
6
+ function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
7
+ function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
8
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
9
+ function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
10
+ function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
11
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
12
+ import dayjs from 'dayjs';
13
+ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
14
+ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
15
+ import { getScheduleStartEndTimePoints } from "../Schedule/getDateIsInSchedule";
16
+ export * from "./types";
17
+
18
+ // The shared Schedule resolver uses these comparisons without registering them.
19
+ dayjs.extend(isSameOrAfter);
20
+ dayjs.extend(isSameOrBefore);
21
+ var MINUTE = 60000;
22
+ var DAY = 24 * 60 * MINUTE;
23
+ var integerSetting = function integerSetting(value, min, max, fallback) {
24
+ return typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max ? value : fallback;
25
+ };
26
+
27
+ /** Consume effective OpenData values only; legacy switches never enable Pickup. */
28
+ export function normalizePickupTimingConfig(openData) {
29
+ var handovers = openData['fulfillment.handover_mode'];
30
+ var scheduleIds = openData['fulfillment.pickup.schedule'];
31
+ var operatingHoursScheduleIds = openData['availability.operating_hours'];
32
+ var mode = openData['fulfillment.pickup.timing_mode'];
33
+ return {
34
+ handoverModes: Array.isArray(handovers) ? _toConsumableArray(new Set(handovers.filter(function (value) {
35
+ return typeof value === 'string' && value.length > 0;
36
+ }))) : [],
37
+ timingMode: mode === 'scheduled_only' || mode === 'asap_and_scheduled' ? mode : 'asap_only',
38
+ scheduleIds: Array.isArray(scheduleIds) ? _toConsumableArray(new Set(scheduleIds.filter(function (value) {
39
+ return typeof value === 'number' && Number.isInteger(value) && value > 0;
40
+ }))) : [],
41
+ operatingHoursScheduleIds: Array.isArray(operatingHoursScheduleIds) ? _toConsumableArray(new Set(operatingHoursScheduleIds.map(function (value) {
42
+ return typeof value === 'number' || typeof value === 'string' ? Number(value) : NaN;
43
+ }).filter(function (value) {
44
+ return Number.isInteger(value) && value > 0;
45
+ }))) : [],
46
+ slotIntervalMinutes: integerSetting(openData['fulfillment.pickup.slot_interval_minutes'], 1, 1440, 15),
47
+ preparationTimeMinutes: integerSetting(openData['fulfillment.pickup.preparation_time_minutes'], 0, 1440, 0),
48
+ advanceDays: integerSetting(openData['fulfillment.pickup.advance_days'], 0, 365, 7),
49
+ fulfillmentRefMode: typeof openData['fulfillment.fulfillment_ref_mode'] === 'string' ? openData['fulfillment.fulfillment_ref_mode'] : undefined,
50
+ manualInputType: typeof openData['fulfillment.manual_input_type'] === 'string' ? openData['fulfillment.manual_input_type'] : undefined
51
+ };
52
+ }
53
+ function dateStamp(date) {
54
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return NaN;
55
+ var value = Date.parse("".concat(date, "T00:00:00Z"));
56
+ return Number.isFinite(value) && new Date(value).toISOString().slice(0, 10) === date ? value : NaN;
57
+ }
58
+ function addDays(date, days) {
59
+ return new Date(dateStamp(date) + days * DAY).toISOString().slice(0, 10);
60
+ }
61
+ function timeMinutes(time) {
62
+ var allowDayEnd = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
63
+ if (!/^\d{2}:\d{2}(?::00)?$/.test(time)) return NaN;
64
+ var _time$split$map = time.split(':').map(Number),
65
+ _time$split$map2 = _slicedToArray(_time$split$map, 2),
66
+ hours = _time$split$map2[0],
67
+ minutes = _time$split$map2[1];
68
+ if (allowDayEnd && hours === 24 && minutes === 0) return 1440;
69
+ return hours < 24 && minutes < 60 ? hours * 60 + minutes : NaN;
70
+ }
71
+ function instantStamp(value) {
72
+ // A local string would silently use the device timezone instead of the store.
73
+ if (typeof value === 'string' && !/(Z|[+-]\d{2}:?\d{2})$/i.test(value)) return NaN;
74
+ return value instanceof Date ? value.getTime() : new Date(value).getTime();
75
+ }
76
+
77
+ /** Per-query timezone adapter; no device-offset arithmetic or global time cache. */
78
+ function createClock(timezone) {
79
+ if (!timezone || typeof timezone !== 'string') throw new Error('Store timezone is unavailable.');
80
+ var formatter;
81
+ try {
82
+ formatter = new Intl.DateTimeFormat('en-GB', {
83
+ timeZone: timezone,
84
+ year: 'numeric',
85
+ month: '2-digit',
86
+ day: '2-digit',
87
+ hour: '2-digit',
88
+ minute: '2-digit',
89
+ second: '2-digit',
90
+ hourCycle: 'h23'
91
+ });
92
+ } catch (_unused) {
93
+ throw new Error('Store timezone is invalid.');
94
+ }
95
+ var offsetsByDate = new Map();
96
+ var local = function local(timestamp) {
97
+ var parts = {};
98
+ formatter.formatToParts(timestamp).forEach(function (part) {
99
+ parts[part.type] = part.value;
100
+ });
101
+ return {
102
+ date: "".concat(parts.year, "-").concat(parts.month, "-").concat(parts.day),
103
+ time: "".concat(parts.hour, ":").concat(parts.minute),
104
+ second: parts.second
105
+ };
106
+ };
107
+ var offsets = function offsets(date) {
108
+ var cached = offsetsByDate.get(date);
109
+ if (cached) return cached;
110
+ var result = new Set();
111
+ var nominal = dateStamp(date);
112
+ // Sampling both sides of the target day discovers pre/post-transition offsets.
113
+ for (var hours = -36; hours <= 36; hours += 6) {
114
+ var sample = nominal + hours * 60 * MINUTE;
115
+ var wall = local(sample);
116
+ result.add(Date.parse("".concat(wall.date, "T").concat(wall.time, ":").concat(wall.second, "Z")) - sample);
117
+ }
118
+ var values = _toConsumableArray(result);
119
+ offsetsByDate.set(date, values);
120
+ return values;
121
+ };
122
+ var resolve = function resolve(date, time) {
123
+ var day = dateStamp(date);
124
+ var minutes = timeMinutes(time);
125
+ if (!Number.isFinite(day) || !Number.isFinite(minutes)) return [];
126
+ var label = time.slice(0, 5);
127
+ var nominal = day + minutes * MINUTE;
128
+ return offsets(date).map(function (offset) {
129
+ return nominal - offset;
130
+ }).filter(function (timestamp) {
131
+ var wall = local(timestamp);
132
+ return wall.date === date && wall.time === label;
133
+ }).sort(function (a, b) {
134
+ return a - b;
135
+ });
136
+ };
137
+ var slot = function slot(timestamp) {
138
+ var _local = local(timestamp),
139
+ date = _local.date,
140
+ time = _local.time;
141
+ var instant = new Date(timestamp).toISOString();
142
+ return {
143
+ id: instant,
144
+ date: date,
145
+ time: time,
146
+ instant: instant
147
+ };
148
+ };
149
+ return {
150
+ local: local,
151
+ resolve: resolve,
152
+ slot: slot,
153
+ hasOffsetTransition: function hasOffsetTransition(date) {
154
+ return offsets(date).length > 1;
155
+ }
156
+ };
157
+ }
158
+ export function resolvePickupTimingDateTime(date, time, timezone, instant) {
159
+ var clock = createClock(timezone);
160
+ var matches = clock.resolve(date, time);
161
+ var selected = instant ? instantStamp(instant) : matches.length === 1 ? matches[0] : NaN;
162
+ return matches.includes(selected) ? clock.slot(selected) : undefined;
163
+ }
164
+ export function getAsapPickupTiming(_ref) {
165
+ var config = _ref.config,
166
+ timezone = _ref.timezone,
167
+ now = _ref.now;
168
+ var timestamp = instantStamp(now);
169
+ if (!Number.isFinite(timestamp)) throw new Error('Current time is invalid.');
170
+ return createClock(timezone).slot(timestamp + config.preparationTimeMinutes * MINUTE);
171
+ }
172
+ export function pickupTimingSlotToBookingTimes(slot) {
173
+ return {
174
+ start_date: slot.date,
175
+ start_time: slot.time,
176
+ end_date: slot.date,
177
+ end_time: slot.time
178
+ };
179
+ }
180
+ function createEvaluation(input) {
181
+ var config = input.config,
182
+ timezone = input.timezone;
183
+ var clock = createClock(timezone);
184
+ var now = instantStamp(input.now);
185
+ if (!Number.isFinite(now)) throw new Error('Current time is invalid.');
186
+ var today = clock.local(now).date;
187
+ var maxDate = addDays(today, config.advanceDays);
188
+ var earliest = now + config.preparationTimeMinutes * MINUTE;
189
+ var schedules = input.schedules.filter(function (schedule) {
190
+ return config.scheduleIds.includes(schedule.id);
191
+ });
192
+ var windowCache = new Map();
193
+ var windows = function windows(date) {
194
+ if (!windowCache.has(date)) {
195
+ windowCache.set(date, getScheduleStartEndTimePoints(date, schedules));
196
+ }
197
+ return windowCache.get(date);
198
+ };
199
+ var operatingIds = config.operatingHoursScheduleIds || [];
200
+ var operatingSchedules = input.schedules.filter(function (schedule) {
201
+ return operatingIds.includes(schedule.id);
202
+ });
203
+ var operatingWindowCache = new Map();
204
+ var operatingWindows = function operatingWindows(date) {
205
+ if (!operatingWindowCache.has(date)) {
206
+ var ranges = [];
207
+ for (var _i = 0, _arr = [-1, 0]; _i < _arr.length; _i++) {
208
+ var offset = _arr[_i];
209
+ var _iterator = _createForOfIteratorHelper(getScheduleStartEndTimePoints(addDays(date, offset), operatingSchedules)),
210
+ _step;
211
+ try {
212
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
213
+ var window = _step.value;
214
+ var start = timeMinutes(window.start_time);
215
+ var end = timeMinutes(window.end_time, true);
216
+ if (!Number.isFinite(start) || !Number.isFinite(end)) continue;
217
+ // Store hours use equal endpoints for a full operating day, while
218
+ // the Pickup Schedule may deliberately contain a single time point.
219
+ if (end <= start) end += 1440;
220
+ ranges.push({
221
+ start: start + offset * 1440,
222
+ end: end + offset * 1440
223
+ });
224
+ }
225
+ } catch (err) {
226
+ _iterator.e(err);
227
+ } finally {
228
+ _iterator.f();
229
+ }
230
+ }
231
+ operatingWindowCache.set(date, ranges);
232
+ }
233
+ return operatingWindowCache.get(date);
234
+ };
235
+ var slotsForDate = function slotsForDate(date) {
236
+ var firstOnly = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
237
+ if (!Number.isFinite(dateStamp(date)) || date < today || date > maxDate || !config.handoverModes.includes('pick_up') || config.timingMode === 'asap_only') return [];
238
+ var found = new Map();
239
+ // Missing hours configuration preserves the existing all-day convention.
240
+ // Referenced but missing schedules yield no windows, never an all-day fallback.
241
+ var businessWindows = operatingIds.length ? operatingWindows(date) : undefined;
242
+ // Include the prior day's windows so overnight tails belong to their actual date.
243
+ for (var _i2 = 0, _arr2 = [addDays(date, -1), date]; _i2 < _arr2.length; _i2++) {
244
+ var windowDate = _arr2[_i2];
245
+ var _iterator2 = _createForOfIteratorHelper(windows(windowDate)),
246
+ _step2;
247
+ try {
248
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
249
+ var window = _step2.value;
250
+ var start = timeMinutes(window.start_time);
251
+ var end = timeMinutes(window.end_time, true);
252
+ if (!Number.isFinite(start) || !Number.isFinite(end)) continue;
253
+ if (end < start) end += 1440;
254
+ var firstFoundInWindow = false;
255
+ var _loop = function _loop() {
256
+ var targetDate = minute >= 1440 ? addDays(windowDate, 1) : windowDate;
257
+ if (targetDate !== date) return 0; // continue
258
+ var localMinute = minute % 1440;
259
+ // Filter the original Pickup grid rather than moving its anchor to
260
+ // the store opening time. A zero-duration pickup may be at closing.
261
+ if (businessWindows && !businessWindows.some(function (_ref2) {
262
+ var start = _ref2.start,
263
+ end = _ref2.end;
264
+ return localMinute >= start && localMinute <= end;
265
+ })) return 0; // continue
266
+ var time = "".concat(String(Math.floor(localMinute / 60)).padStart(2, '0'), ":").concat(String(localMinute % 60).padStart(2, '0'));
267
+ var _iterator3 = _createForOfIteratorHelper(clock.resolve(date, time)),
268
+ _step3;
269
+ try {
270
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
271
+ var timestamp = _step3.value;
272
+ if (timestamp < earliest) continue;
273
+ found.set(timestamp, clock.slot(timestamp));
274
+ firstFoundInWindow = true;
275
+ }
276
+ // Summary needs only an available candidate, not a full year's slots.
277
+ // During a repeated hour, an earlier wall label's second occurrence can
278
+ // be later than a subsequent label's first occurrence. Sort them all.
279
+ } catch (err) {
280
+ _iterator3.e(err);
281
+ } finally {
282
+ _iterator3.f();
283
+ }
284
+ if (firstOnly && firstFoundInWindow && !clock.hasOffsetTransition(date)) return 1; // break
285
+ },
286
+ _ret;
287
+ for (var minute = start; minute <= end; minute += config.slotIntervalMinutes) {
288
+ _ret = _loop();
289
+ if (_ret === 0) continue;
290
+ if (_ret === 1) break;
291
+ }
292
+ }
293
+ } catch (err) {
294
+ _iterator2.e(err);
295
+ } finally {
296
+ _iterator2.f();
297
+ }
298
+ }
299
+ var result = _toConsumableArray(found.entries()).sort(function (_ref3, _ref4) {
300
+ var _ref5 = _slicedToArray(_ref3, 1),
301
+ a = _ref5[0];
302
+ var _ref6 = _slicedToArray(_ref4, 1),
303
+ b = _ref6[0];
304
+ return a - b;
305
+ }).map(function (_ref7) {
306
+ var _ref8 = _slicedToArray(_ref7, 2),
307
+ slot = _ref8[1];
308
+ return slot;
309
+ });
310
+ return firstOnly ? result.slice(0, 1) : result;
311
+ };
312
+ return {
313
+ clock: clock,
314
+ now: now,
315
+ today: today,
316
+ maxDate: maxDate,
317
+ slotsForDate: slotsForDate
318
+ };
319
+ }
320
+ export function computePickupTimingAvailability(input) {
321
+ if (!input.config.handoverModes.includes('pick_up')) {
322
+ var now = instantStamp(input.now);
323
+ if (!Number.isFinite(now)) throw new Error('Current time is invalid.');
324
+ return {
325
+ config: input.config,
326
+ timezone: input.timezone,
327
+ now: new Date(now).toISOString(),
328
+ today: '',
329
+ maxDate: '',
330
+ date: input.date || '',
331
+ dates: [],
332
+ slots: []
333
+ };
334
+ }
335
+ var evaluation = createEvaluation(input);
336
+ var today = evaluation.today,
337
+ maxDate = evaluation.maxDate,
338
+ slotsForDate = evaluation.slotsForDate;
339
+ var date = input.date || today;
340
+ if (!Number.isFinite(dateStamp(date))) throw new Error('Pickup date is invalid.');
341
+ var slots = slotsForDate(date);
342
+ var dates = [];
343
+ var firstSlot;
344
+ for (var day = today; day <= maxDate; day = addDays(day, 1)) {
345
+ var first = day === date ? slots[0] : slotsForDate(day, true)[0];
346
+ dates.push({
347
+ date: day,
348
+ status: first ? 'available' : 'unavailable'
349
+ });
350
+ if (!firstSlot && first) firstSlot = first;
351
+ }
352
+ return {
353
+ config: input.config,
354
+ timezone: input.timezone,
355
+ now: new Date(evaluation.now).toISOString(),
356
+ today: today,
357
+ maxDate: maxDate,
358
+ date: date,
359
+ dates: dates,
360
+ slots: slots,
361
+ firstSlot: firstSlot
362
+ };
363
+ }
364
+ export function validatePickupTimingSelection(selection, input) {
365
+ var fail = function fail(code, message) {
366
+ return {
367
+ valid: false,
368
+ code: code,
369
+ message: message
370
+ };
371
+ };
372
+ var config = input.config;
373
+ if (config.handoverModes.length === 0) return {
374
+ valid: true
375
+ };
376
+ if (!config.handoverModes.includes(selection.serviceType)) {
377
+ return fail('handover_unavailable', 'Choose an available handover method.');
378
+ }
379
+ if (selection.serviceType !== 'pick_up') return {
380
+ valid: true
381
+ };
382
+ if (selection.mode === 'asap') {
383
+ return config.timingMode === 'scheduled_only' ? fail('scheduled_required', 'Choose a pickup date and time.') : {
384
+ valid: true,
385
+ slot: getAsapPickupTiming(input)
386
+ };
387
+ }
388
+ if (selection.mode !== 'scheduled' || config.timingMode === 'asap_only') {
389
+ return fail('mode_unavailable', 'The selected pickup timing mode is unavailable.');
390
+ }
391
+ var slot = resolvePickupTimingDateTime(selection.date || '', selection.time || '', input.timezone, selection.instant);
392
+ if (!slot) return fail('invalid_time', 'Choose a valid pickup date and time.');
393
+ var now = instantStamp(input.now);
394
+ if (!Number.isFinite(now)) throw new Error('Current time is invalid.');
395
+ if (instantStamp(slot.instant) < now) return fail('time_in_past', 'Pickup time cannot be in the past.');
396
+ if (selection.source === 'pos_manual' && input.allowManualOverride) return {
397
+ valid: true,
398
+ slot: slot
399
+ };
400
+ var legal = createEvaluation(input).slotsForDate(slot.date).some(function (candidate) {
401
+ return candidate.instant === slot.instant;
402
+ });
403
+ return legal ? {
404
+ valid: true,
405
+ slot: slot
406
+ } : fail('time_unavailable', 'This pickup time is no longer available. Choose another time.');
407
+ }
@@ -0,0 +1,67 @@
1
+ import type { ScheduleItem } from '../Schedule/types';
2
+ export type PickupTimingMode = 'asap_only' | 'scheduled_only' | 'asap_and_scheduled';
3
+ export interface PickupTimingConfig {
4
+ handoverModes: string[];
5
+ timingMode: PickupTimingMode;
6
+ scheduleIds: number[];
7
+ /** Empty or absent means no additional store-hours restriction. */
8
+ operatingHoursScheduleIds?: number[];
9
+ slotIntervalMinutes: number;
10
+ preparationTimeMinutes: number;
11
+ advanceDays: number;
12
+ fulfillmentRefMode?: string;
13
+ manualInputType?: string;
14
+ }
15
+ export interface PickupTimingSelection {
16
+ serviceType: string;
17
+ mode: 'asap' | 'scheduled';
18
+ date?: string;
19
+ time?: string;
20
+ /** The absolute instant disambiguates a repeated local minute during DST. */
21
+ instant?: string;
22
+ source?: 'schedule' | 'pos_manual' | 'asap';
23
+ }
24
+ export interface PickupTimingSlot {
25
+ id: string;
26
+ date: string;
27
+ time: string;
28
+ instant: string;
29
+ }
30
+ export interface PickupTimingAvailability {
31
+ /** Config remains available when Schedule or store timezone cannot be loaded. */
32
+ error?: string;
33
+ config: PickupTimingConfig;
34
+ timezone: string;
35
+ now: string;
36
+ today: string;
37
+ maxDate: string;
38
+ date: string;
39
+ dates: Array<{
40
+ date: string;
41
+ status: 'available' | 'unavailable';
42
+ }>;
43
+ slots: PickupTimingSlot[];
44
+ /** Earliest legal candidate across all permitted dates, for POS prefill. */
45
+ firstSlot?: PickupTimingSlot;
46
+ }
47
+ export interface PickupTimingClockInput {
48
+ config: PickupTimingConfig;
49
+ timezone: string;
50
+ now: Date | string | number;
51
+ }
52
+ export interface PickupTimingComputeInput extends PickupTimingClockInput {
53
+ schedules: ScheduleItem[];
54
+ date?: string;
55
+ }
56
+ export interface PickupTimingValidationResult {
57
+ valid: boolean;
58
+ code?: string;
59
+ message?: string;
60
+ slot?: PickupTimingSlot;
61
+ }
62
+ export interface PickupTimingBookingTimes {
63
+ start_date: string;
64
+ start_time: string;
65
+ end_date: string;
66
+ end_time: string;
67
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -77,6 +77,12 @@ export interface OpenDataConfig {
77
77
  'sale.sale_number_prefix'?: string | null;
78
78
  'sale.enabled_item_rules'?: number[];
79
79
  'fulfillment.enable_pickup'?: boolean;
80
+ 'fulfillment.handover_mode'?: string[];
81
+ 'fulfillment.pickup.timing_mode'?: 'asap_only' | 'scheduled_only' | 'asap_and_scheduled';
82
+ 'fulfillment.pickup.schedule'?: number[];
83
+ 'fulfillment.pickup.advance_days'?: number;
84
+ 'fulfillment.pickup.slot_interval_minutes'?: number;
85
+ 'fulfillment.pickup.preparation_time_minutes'?: number;
80
86
  'fulfillment.overdue_reminder'?: boolean;
81
87
  'fulfillment.manual_input_type'?: string;
82
88
  'fulfillment.enable_auto_complete'?: boolean;
@@ -3,7 +3,8 @@ import { BaseModule } from '../BaseModule';
3
3
  import { OrderModuleAPI, CommitOrderParams, UpdateOrderBookingParams, UpdateOrderProductParams, UpdateOrderProductQuantityParams, CancelOrderParams, ChangeBookingStatusParams, CheckoutOrderParams } from './types';
4
4
  import { CartItem } from '../Cart/types';
5
5
  import { type SubmitPayloadEnhancer } from './utils';
6
- import type { OrderAmountSnapshot, AddProductBookingInput, AddProductToOrderOptions, OrderIdentitySnapshot, OrderPaymentSource, OrderSnapshot, OrderSyncState, SyncPaymentsToOrderParams, SyncPaymentsToOrderResult, SubmitSalesOrderParams, OrderProduct, OrderProductIdentity, OrderSummary, OrderTempOrder, OrderPaymentData, OrderPaymentUpdateOptions, OrderPaymentUpdateResult, SendCustomerPayLinkParams, UpdateTempOrderCustomerInput, OrderCustomerPatch, OrderCustomerView, ReplaceTempOrderOptions, OrderPromotionEvaluator, OrderGiftSelectResolver, OrderUnfulfilledPromotion, OrderLastGiftActions, RemoveProductsFromOrderOptions, SalesNote, SalesNotesMutationSnapshot, SetSalesNoteInput, PickupReferenceMode, ShopServiceData, ShopServiceType } from './types';
6
+ import { type PickupTimingBooking, type PickupTimingBookingInput } from './pickupTiming';
7
+ import type { OrderAmountSnapshot, AddProductBookingInput, AddProductToOrderOptions, OrderIdentitySnapshot, OrderPaymentSource, OrderSnapshot, OrderSyncState, SyncPaymentsToOrderParams, SyncPaymentsToOrderResult, SubmitSalesOrderParams, OrderProduct, OrderProductIdentity, OrderSummary, OrderTempOrder, OrderPaymentData, OrderPaymentUpdateOptions, OrderPaymentUpdateResult, SendCustomerPayLinkParams, UpdateTempOrderCustomerInput, OrderCustomerPatch, OrderCustomerView, ReplaceTempOrderOptions, OrderPromotionEvaluator, OrderGiftSelectResolver, OrderUnfulfilledPromotion, OrderLastGiftActions, RemoveProductsFromOrderOptions, SalesNote, SalesNotesMutationSnapshot, SetSalesNoteInput, PickupReferenceMode, PointCardSnapshotMergeContext, PointCardSnapshotItem, ShopServiceData, ShopServiceType } from './types';
7
8
  import type { ICustomer } from '../AccountList/types';
8
9
  import type { Discount } from '../Discount/types';
9
10
  import { UnavailableReason } from '../Rules/types';
@@ -22,6 +23,8 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
22
23
  private sessionStorageRestoreRecord;
23
24
  /** 只允许当前订单最新的 prepare/config 请求回写客户资产态。 */
24
25
  private customerWalletPrepareConfigEpoch;
26
+ /** Payment 可能早于 prepare/config 返回;先按实例 ID 暂存卡号等待 Snapshot。 */
27
+ private pointCardSnapshotCodeCandidates;
25
28
  private salesSummaryModuleName;
26
29
  private rulesHooksOverride?;
27
30
  private moduleHooksOverride?;
@@ -108,6 +111,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
108
111
  }>;
109
112
  applyDiscount(options?: {
110
113
  reapplyBookingDiscountProductUids?: string[];
114
+ preferredEntitlementPassIds?: number[];
111
115
  }): void;
112
116
  /**
113
117
  * 注入促销评估器。
@@ -187,7 +191,16 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
187
191
  private clearTempOrderCustomerWalletSnapshots;
188
192
  private clearCustomerWalletState;
189
193
  private clearCustomerWalletRuntimeState;
194
+ private rememberPointCardSnapshotCodeCandidates;
195
+ private getRememberedPointCardSnapshotCodeCandidates;
196
+ private getPointCardSnapshotOrderIdentityKey;
197
+ private isPointCardSnapshotMergeContextCurrent;
190
198
  private writeCustomerWalletSnapshotsFromPrepareConfig;
199
+ /**
200
+ * 将 Payment WalletPass 返回的实例 code 回填到当前订单 Snapshot。
201
+ * 这里只按资产 ID 补空值,不新增卡、不覆盖 prepare/config 冻结的余额。
202
+ */
203
+ mergePointCardSnapshotCodes(walletPassList: unknown, context?: PointCardSnapshotMergeContext): PointCardSnapshotItem[];
191
204
  private createExternalSaleNumber;
192
205
  private ensureExternalSaleNumber;
193
206
  private ensureRequestUniqueIdempotencyToken;
@@ -341,6 +354,9 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
341
354
  private updateTempOrderShopServiceData;
342
355
  updateTempOrderPickupReferenceMode(mode: PickupReferenceMode): ShopServiceData;
343
356
  updateTempOrderServiceType(serviceType: ShopServiceType): ShopServiceData;
357
+ getPickupTimingBooking(): PickupTimingBooking | null;
358
+ setPickupTimingBooking(input: PickupTimingBookingInput): PickupTimingBooking;
359
+ clearPickupTimingBooking(): void;
344
360
  updateTempOrderContactsInfo(contactsInfo: Record<string, any> | null): Record<string, any> | null;
345
361
  private buildTempOrderCustomer;
346
362
  private isCustomerBoundDiscount;
@@ -651,3 +667,5 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
651
667
  getVouchers(): any[];
652
668
  }
653
669
  export type { AddProductToOrderOptions, OrderMetaList, PointCardSnapshotItem, VirtualCurrencySnapshotItem, RemoveProductsFromOrderOptions, SalesNote, SalesNoteDraftInput, SalesNoteLocalizedSelection, SalesNoteLocalizedText, SalesNoteRelation, SalesNoteTextInput, SetSalesNoteInput, UpdateOrderBookingParams, UpdateOrderProductParams, UpdateOrderProductQuantityParams, } from './types';
670
+ export { isFulfillmentTimingBooking } from './pickupTiming';
671
+ export type { PickupTimingBooking, PickupTimingBookingInput, PickupTimingBookingMetadata, PickupTimingSelectionMode, PickupTimingSource, } from './pickupTiming';