@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
@@ -1 +1,46 @@
1
- "use strict";
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "BUY_X_GET_Y_FREE_STRATEGY", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _examples.BUY_X_GET_Y_FREE_STRATEGY;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "ITEM_REWARD_STRATEGY", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _examples.ITEM_REWARD_STRATEGY;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "PromotionAdapter", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _adapter.PromotionAdapter;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "PromotionEvaluator", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _evaluator.PromotionEvaluator;
28
+ }
29
+ });
30
+ Object.defineProperty(exports, "X_ITEMS_FOR_Y_PRICE_STRATEGY", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _examples.X_ITEMS_FOR_Y_PRICE_STRATEGY;
34
+ }
35
+ });
36
+ Object.defineProperty(exports, "default", {
37
+ enumerable: true,
38
+ get: function () {
39
+ return _adapter.default;
40
+ }
41
+ });
42
+ var _evaluator = require("./evaluator");
43
+ var _adapter = _interopRequireWildcard(require("./adapter"));
44
+ var _examples = require("./examples");
45
+ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
46
+ function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
@@ -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,359 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ var _exportNames = {
7
+ normalizePickupTimingConfig: true,
8
+ resolvePickupTimingDateTime: true,
9
+ getAsapPickupTiming: true,
10
+ pickupTimingSlotToBookingTimes: true,
11
+ computePickupTimingAvailability: true,
12
+ validatePickupTimingSelection: true
13
+ };
14
+ exports.computePickupTimingAvailability = computePickupTimingAvailability;
15
+ exports.getAsapPickupTiming = getAsapPickupTiming;
16
+ exports.normalizePickupTimingConfig = normalizePickupTimingConfig;
17
+ exports.pickupTimingSlotToBookingTimes = pickupTimingSlotToBookingTimes;
18
+ exports.resolvePickupTimingDateTime = resolvePickupTimingDateTime;
19
+ exports.validatePickupTimingSelection = validatePickupTimingSelection;
20
+ var _dayjs = _interopRequireDefault(require("dayjs"));
21
+ var _isSameOrAfter = _interopRequireDefault(require("dayjs/plugin/isSameOrAfter"));
22
+ var _isSameOrBefore = _interopRequireDefault(require("dayjs/plugin/isSameOrBefore"));
23
+ var _getDateIsInSchedule = require("../Schedule/getDateIsInSchedule");
24
+ var _types = require("./types");
25
+ Object.keys(_types).forEach(function (key) {
26
+ if (key === "default" || key === "__esModule") return;
27
+ if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
28
+ if (key in exports && exports[key] === _types[key]) return;
29
+ Object.defineProperty(exports, key, {
30
+ enumerable: true,
31
+ get: function () {
32
+ return _types[key];
33
+ }
34
+ });
35
+ });
36
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
37
+ // The shared Schedule resolver uses these comparisons without registering them.
38
+ _dayjs.default.extend(_isSameOrAfter.default);
39
+ _dayjs.default.extend(_isSameOrBefore.default);
40
+ const MINUTE = 60_000;
41
+ const DAY = 24 * 60 * MINUTE;
42
+ const integerSetting = (value, min, max, fallback) => typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max ? value : fallback;
43
+
44
+ /** Consume effective OpenData values only; legacy switches never enable Pickup. */
45
+ function normalizePickupTimingConfig(openData) {
46
+ const handovers = openData['fulfillment.handover_mode'];
47
+ const scheduleIds = openData['fulfillment.pickup.schedule'];
48
+ const operatingHoursScheduleIds = openData['availability.operating_hours'];
49
+ const mode = openData['fulfillment.pickup.timing_mode'];
50
+ return {
51
+ handoverModes: Array.isArray(handovers) ? [...new Set(handovers.filter(value => typeof value === 'string' && value.length > 0))] : [],
52
+ timingMode: mode === 'scheduled_only' || mode === 'asap_and_scheduled' ? mode : 'asap_only',
53
+ scheduleIds: Array.isArray(scheduleIds) ? [...new Set(scheduleIds.filter(value => typeof value === 'number' && Number.isInteger(value) && value > 0))] : [],
54
+ operatingHoursScheduleIds: Array.isArray(operatingHoursScheduleIds) ? [...new Set(operatingHoursScheduleIds.map(value => typeof value === 'number' || typeof value === 'string' ? Number(value) : NaN).filter(value => Number.isInteger(value) && value > 0))] : [],
55
+ slotIntervalMinutes: integerSetting(openData['fulfillment.pickup.slot_interval_minutes'], 1, 1440, 15),
56
+ preparationTimeMinutes: integerSetting(openData['fulfillment.pickup.preparation_time_minutes'], 0, 1440, 0),
57
+ advanceDays: integerSetting(openData['fulfillment.pickup.advance_days'], 0, 365, 7),
58
+ fulfillmentRefMode: typeof openData['fulfillment.fulfillment_ref_mode'] === 'string' ? openData['fulfillment.fulfillment_ref_mode'] : undefined,
59
+ manualInputType: typeof openData['fulfillment.manual_input_type'] === 'string' ? openData['fulfillment.manual_input_type'] : undefined
60
+ };
61
+ }
62
+ function dateStamp(date) {
63
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return NaN;
64
+ const value = Date.parse(`${date}T00:00:00Z`);
65
+ return Number.isFinite(value) && new Date(value).toISOString().slice(0, 10) === date ? value : NaN;
66
+ }
67
+ function addDays(date, days) {
68
+ return new Date(dateStamp(date) + days * DAY).toISOString().slice(0, 10);
69
+ }
70
+ function timeMinutes(time, allowDayEnd = false) {
71
+ if (!/^\d{2}:\d{2}(?::00)?$/.test(time)) return NaN;
72
+ const [hours, minutes] = time.split(':').map(Number);
73
+ if (allowDayEnd && hours === 24 && minutes === 0) return 1440;
74
+ return hours < 24 && minutes < 60 ? hours * 60 + minutes : NaN;
75
+ }
76
+ function instantStamp(value) {
77
+ // A local string would silently use the device timezone instead of the store.
78
+ if (typeof value === 'string' && !/(Z|[+-]\d{2}:?\d{2})$/i.test(value)) return NaN;
79
+ return value instanceof Date ? value.getTime() : new Date(value).getTime();
80
+ }
81
+
82
+ /** Per-query timezone adapter; no device-offset arithmetic or global time cache. */
83
+ function createClock(timezone) {
84
+ if (!timezone || typeof timezone !== 'string') throw new Error('Store timezone is unavailable.');
85
+ let formatter;
86
+ try {
87
+ formatter = new Intl.DateTimeFormat('en-GB', {
88
+ timeZone: timezone,
89
+ year: 'numeric',
90
+ month: '2-digit',
91
+ day: '2-digit',
92
+ hour: '2-digit',
93
+ minute: '2-digit',
94
+ second: '2-digit',
95
+ hourCycle: 'h23'
96
+ });
97
+ } catch {
98
+ throw new Error('Store timezone is invalid.');
99
+ }
100
+ const offsetsByDate = new Map();
101
+ const local = timestamp => {
102
+ const parts = {};
103
+ formatter.formatToParts(timestamp).forEach(part => {
104
+ parts[part.type] = part.value;
105
+ });
106
+ return {
107
+ date: `${parts.year}-${parts.month}-${parts.day}`,
108
+ time: `${parts.hour}:${parts.minute}`,
109
+ second: parts.second
110
+ };
111
+ };
112
+ const offsets = date => {
113
+ const cached = offsetsByDate.get(date);
114
+ if (cached) return cached;
115
+ const result = new Set();
116
+ const nominal = dateStamp(date);
117
+ // Sampling both sides of the target day discovers pre/post-transition offsets.
118
+ for (let hours = -36; hours <= 36; hours += 6) {
119
+ const sample = nominal + hours * 60 * MINUTE;
120
+ const wall = local(sample);
121
+ result.add(Date.parse(`${wall.date}T${wall.time}:${wall.second}Z`) - sample);
122
+ }
123
+ const values = [...result];
124
+ offsetsByDate.set(date, values);
125
+ return values;
126
+ };
127
+ const resolve = (date, time) => {
128
+ const day = dateStamp(date);
129
+ const minutes = timeMinutes(time);
130
+ if (!Number.isFinite(day) || !Number.isFinite(minutes)) return [];
131
+ const label = time.slice(0, 5);
132
+ const nominal = day + minutes * MINUTE;
133
+ return offsets(date).map(offset => nominal - offset).filter(timestamp => {
134
+ const wall = local(timestamp);
135
+ return wall.date === date && wall.time === label;
136
+ }).sort((a, b) => a - b);
137
+ };
138
+ const slot = timestamp => {
139
+ const {
140
+ date,
141
+ time
142
+ } = local(timestamp);
143
+ const instant = new Date(timestamp).toISOString();
144
+ return {
145
+ id: instant,
146
+ date,
147
+ time,
148
+ instant
149
+ };
150
+ };
151
+ return {
152
+ local,
153
+ resolve,
154
+ slot,
155
+ hasOffsetTransition: date => offsets(date).length > 1
156
+ };
157
+ }
158
+ function resolvePickupTimingDateTime(date, time, timezone, instant) {
159
+ const clock = createClock(timezone);
160
+ const matches = clock.resolve(date, time);
161
+ const selected = instant ? instantStamp(instant) : matches.length === 1 ? matches[0] : NaN;
162
+ return matches.includes(selected) ? clock.slot(selected) : undefined;
163
+ }
164
+ function getAsapPickupTiming({
165
+ config,
166
+ timezone,
167
+ now
168
+ }) {
169
+ const timestamp = instantStamp(now);
170
+ if (!Number.isFinite(timestamp)) throw new Error('Current time is invalid.');
171
+ return createClock(timezone).slot(timestamp + config.preparationTimeMinutes * MINUTE);
172
+ }
173
+ function pickupTimingSlotToBookingTimes(slot) {
174
+ return {
175
+ start_date: slot.date,
176
+ start_time: slot.time,
177
+ end_date: slot.date,
178
+ end_time: slot.time
179
+ };
180
+ }
181
+ function createEvaluation(input) {
182
+ const {
183
+ config,
184
+ timezone
185
+ } = input;
186
+ const clock = createClock(timezone);
187
+ const now = instantStamp(input.now);
188
+ if (!Number.isFinite(now)) throw new Error('Current time is invalid.');
189
+ const today = clock.local(now).date;
190
+ const maxDate = addDays(today, config.advanceDays);
191
+ const earliest = now + config.preparationTimeMinutes * MINUTE;
192
+ const schedules = input.schedules.filter(schedule => config.scheduleIds.includes(schedule.id));
193
+ const windowCache = new Map();
194
+ const windows = date => {
195
+ if (!windowCache.has(date)) {
196
+ windowCache.set(date, (0, _getDateIsInSchedule.getScheduleStartEndTimePoints)(date, schedules));
197
+ }
198
+ return windowCache.get(date);
199
+ };
200
+ const operatingIds = config.operatingHoursScheduleIds || [];
201
+ const operatingSchedules = input.schedules.filter(schedule => operatingIds.includes(schedule.id));
202
+ const operatingWindowCache = new Map();
203
+ const operatingWindows = date => {
204
+ if (!operatingWindowCache.has(date)) {
205
+ const ranges = [];
206
+ for (const offset of [-1, 0]) {
207
+ for (const window of (0, _getDateIsInSchedule.getScheduleStartEndTimePoints)(addDays(date, offset), operatingSchedules)) {
208
+ const start = timeMinutes(window.start_time);
209
+ let end = timeMinutes(window.end_time, true);
210
+ if (!Number.isFinite(start) || !Number.isFinite(end)) continue;
211
+ // Store hours use equal endpoints for a full operating day, while
212
+ // the Pickup Schedule may deliberately contain a single time point.
213
+ if (end <= start) end += 1440;
214
+ ranges.push({
215
+ start: start + offset * 1440,
216
+ end: end + offset * 1440
217
+ });
218
+ }
219
+ }
220
+ operatingWindowCache.set(date, ranges);
221
+ }
222
+ return operatingWindowCache.get(date);
223
+ };
224
+ const slotsForDate = (date, firstOnly = false) => {
225
+ if (!Number.isFinite(dateStamp(date)) || date < today || date > maxDate || !config.handoverModes.includes('pick_up') || config.timingMode === 'asap_only') return [];
226
+ const found = new Map();
227
+ // Missing hours configuration preserves the existing all-day convention.
228
+ // Referenced but missing schedules yield no windows, never an all-day fallback.
229
+ const businessWindows = operatingIds.length ? operatingWindows(date) : undefined;
230
+ // Include the prior day's windows so overnight tails belong to their actual date.
231
+ for (const windowDate of [addDays(date, -1), date]) {
232
+ for (const window of windows(windowDate)) {
233
+ const start = timeMinutes(window.start_time);
234
+ let end = timeMinutes(window.end_time, true);
235
+ if (!Number.isFinite(start) || !Number.isFinite(end)) continue;
236
+ if (end < start) end += 1440;
237
+ let firstFoundInWindow = false;
238
+ for (let minute = start; minute <= end; minute += config.slotIntervalMinutes) {
239
+ const targetDate = minute >= 1440 ? addDays(windowDate, 1) : windowDate;
240
+ if (targetDate !== date) continue;
241
+ const localMinute = minute % 1440;
242
+ // Filter the original Pickup grid rather than moving its anchor to
243
+ // the store opening time. A zero-duration pickup may be at closing.
244
+ if (businessWindows && !businessWindows.some(({
245
+ start,
246
+ end
247
+ }) => localMinute >= start && localMinute <= end)) continue;
248
+ const time = `${String(Math.floor(localMinute / 60)).padStart(2, '0')}:${String(localMinute % 60).padStart(2, '0')}`;
249
+ for (const timestamp of clock.resolve(date, time)) {
250
+ if (timestamp < earliest) continue;
251
+ found.set(timestamp, clock.slot(timestamp));
252
+ firstFoundInWindow = true;
253
+ }
254
+ // Summary needs only an available candidate, not a full year's slots.
255
+ // During a repeated hour, an earlier wall label's second occurrence can
256
+ // be later than a subsequent label's first occurrence. Sort them all.
257
+ if (firstOnly && firstFoundInWindow && !clock.hasOffsetTransition(date)) break;
258
+ }
259
+ }
260
+ }
261
+ const result = [...found.entries()].sort(([a], [b]) => a - b).map(([, slot]) => slot);
262
+ return firstOnly ? result.slice(0, 1) : result;
263
+ };
264
+ return {
265
+ clock,
266
+ now,
267
+ today,
268
+ maxDate,
269
+ slotsForDate
270
+ };
271
+ }
272
+ function computePickupTimingAvailability(input) {
273
+ if (!input.config.handoverModes.includes('pick_up')) {
274
+ const now = instantStamp(input.now);
275
+ if (!Number.isFinite(now)) throw new Error('Current time is invalid.');
276
+ return {
277
+ config: input.config,
278
+ timezone: input.timezone,
279
+ now: new Date(now).toISOString(),
280
+ today: '',
281
+ maxDate: '',
282
+ date: input.date || '',
283
+ dates: [],
284
+ slots: []
285
+ };
286
+ }
287
+ const evaluation = createEvaluation(input);
288
+ const {
289
+ today,
290
+ maxDate,
291
+ slotsForDate
292
+ } = evaluation;
293
+ const date = input.date || today;
294
+ if (!Number.isFinite(dateStamp(date))) throw new Error('Pickup date is invalid.');
295
+ const slots = slotsForDate(date);
296
+ const dates = [];
297
+ let firstSlot;
298
+ for (let day = today; day <= maxDate; day = addDays(day, 1)) {
299
+ const first = day === date ? slots[0] : slotsForDate(day, true)[0];
300
+ dates.push({
301
+ date: day,
302
+ status: first ? 'available' : 'unavailable'
303
+ });
304
+ if (!firstSlot && first) firstSlot = first;
305
+ }
306
+ return {
307
+ config: input.config,
308
+ timezone: input.timezone,
309
+ now: new Date(evaluation.now).toISOString(),
310
+ today,
311
+ maxDate,
312
+ date,
313
+ dates,
314
+ slots,
315
+ firstSlot
316
+ };
317
+ }
318
+ function validatePickupTimingSelection(selection, input) {
319
+ const fail = (code, message) => ({
320
+ valid: false,
321
+ code,
322
+ message
323
+ });
324
+ const {
325
+ config
326
+ } = input;
327
+ if (config.handoverModes.length === 0) return {
328
+ valid: true
329
+ };
330
+ if (!config.handoverModes.includes(selection.serviceType)) {
331
+ return fail('handover_unavailable', 'Choose an available handover method.');
332
+ }
333
+ if (selection.serviceType !== 'pick_up') return {
334
+ valid: true
335
+ };
336
+ if (selection.mode === 'asap') {
337
+ return config.timingMode === 'scheduled_only' ? fail('scheduled_required', 'Choose a pickup date and time.') : {
338
+ valid: true,
339
+ slot: getAsapPickupTiming(input)
340
+ };
341
+ }
342
+ if (selection.mode !== 'scheduled' || config.timingMode === 'asap_only') {
343
+ return fail('mode_unavailable', 'The selected pickup timing mode is unavailable.');
344
+ }
345
+ const slot = resolvePickupTimingDateTime(selection.date || '', selection.time || '', input.timezone, selection.instant);
346
+ if (!slot) return fail('invalid_time', 'Choose a valid pickup date and time.');
347
+ const now = instantStamp(input.now);
348
+ if (!Number.isFinite(now)) throw new Error('Current time is invalid.');
349
+ if (instantStamp(slot.instant) < now) return fail('time_in_past', 'Pickup time cannot be in the past.');
350
+ if (selection.source === 'pos_manual' && input.allowManualOverride) return {
351
+ valid: true,
352
+ slot
353
+ };
354
+ const legal = createEvaluation(input).slotsForDate(slot.date).some(candidate => candidate.instant === slot.instant);
355
+ return legal ? {
356
+ valid: true,
357
+ slot
358
+ } : fail('time_unavailable', 'This pickup time is no longer available. Choose another time.');
359
+ }
@@ -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,5 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
@@ -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';