@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.
- package/dist/modules/Discount/types.d.ts +2 -0
- package/dist/modules/FulfillmentTiming/index.d.ts +11 -0
- package/dist/modules/FulfillmentTiming/index.js +407 -0
- package/dist/modules/FulfillmentTiming/types.d.ts +67 -0
- package/dist/modules/FulfillmentTiming/types.js +1 -0
- package/dist/modules/OpenData/types.d.ts +6 -0
- package/dist/modules/Order/index.d.ts +19 -1
- package/dist/modules/Order/index.js +362 -217
- package/dist/modules/Order/pickupTiming.d.ts +46 -0
- package/dist/modules/Order/pickupTiming.js +53 -0
- package/dist/modules/Order/types.d.ts +11 -1
- package/dist/modules/Order/utils.d.ts +10 -0
- package/dist/modules/Order/utils.js +145 -85
- package/dist/modules/Rules/index.js +45 -26
- package/dist/modules/Rules/types.d.ts +4 -0
- package/dist/modules/index.d.ts +1 -0
- package/dist/modules/index.js +1 -0
- package/dist/server/modules/order/index.d.ts +8 -0
- package/dist/server/modules/order/index.js +506 -453
- package/dist/server/modules/order/types.d.ts +1 -0
- package/dist/server/utils/small-ticket.js +173 -95
- package/dist/solution/BaseSales/index.d.ts +23 -0
- package/dist/solution/BaseSales/index.js +1216 -1031
- package/dist/solution/BaseSales/pickupTiming.d.ts +37 -0
- package/dist/solution/BaseSales/pickupTiming.js +482 -0
- package/dist/solution/BaseSales/utils/parseSalesResponse.js +3 -0
- package/dist/solution/BookingByStep/index.d.ts +1 -1
- package/dist/solution/BookingTicket/utils/cartView.js +3 -1
- package/dist/solution/UnifiedBookingSales/index.d.ts +2 -0
- package/dist/solution/UnifiedBookingSales/index.js +583 -531
- package/lib/model/strategy/adapter/promotion/index.js +46 -1
- package/lib/modules/Discount/types.d.ts +2 -0
- package/lib/modules/FulfillmentTiming/index.d.ts +11 -0
- package/lib/modules/FulfillmentTiming/index.js +359 -0
- package/lib/modules/FulfillmentTiming/types.d.ts +67 -0
- package/lib/modules/FulfillmentTiming/types.js +5 -0
- package/lib/modules/OpenData/types.d.ts +6 -0
- package/lib/modules/Order/index.d.ts +19 -1
- package/lib/modules/Order/index.js +120 -7
- package/lib/modules/Order/pickupTiming.d.ts +46 -0
- package/lib/modules/Order/pickupTiming.js +61 -0
- package/lib/modules/Order/types.d.ts +11 -1
- package/lib/modules/Order/utils.d.ts +10 -0
- package/lib/modules/Order/utils.js +58 -4
- package/lib/modules/Rules/index.js +24 -13
- package/lib/modules/Rules/types.d.ts +4 -0
- package/lib/modules/index.d.ts +1 -0
- package/lib/modules/index.js +11 -0
- package/lib/server/modules/order/index.d.ts +8 -0
- package/lib/server/modules/order/index.js +19 -0
- package/lib/server/modules/order/types.d.ts +1 -0
- package/lib/server/utils/small-ticket.js +93 -22
- package/lib/solution/BaseSales/index.d.ts +23 -0
- package/lib/solution/BaseSales/index.js +104 -1
- package/lib/solution/BaseSales/pickupTiming.d.ts +37 -0
- package/lib/solution/BaseSales/pickupTiming.js +222 -0
- package/lib/solution/BaseSales/utils/parseSalesResponse.js +3 -0
- package/lib/solution/BookingByStep/index.d.ts +1 -1
- package/lib/solution/BookingTicket/utils/cartView.js +3 -1
- package/lib/solution/UnifiedBookingSales/index.d.ts +2 -0
- package/lib/solution/UnifiedBookingSales/index.js +20 -0
- 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
|
+
}
|
|
@@ -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
|
|
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';
|