@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
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.PickupTimingController = void 0;
7
+ var _lodashEs = require("lodash-es");
8
+ var _FulfillmentTiming = require("../../modules/FulfillmentTiming");
9
+ async function withTimeout(request) {
10
+ let timer;
11
+ try {
12
+ return await Promise.race([request, new Promise((_, reject) => {
13
+ timer = setTimeout(() => reject(new Error('Unable to load pickup times. Please try again.')), 30000);
14
+ })]);
15
+ } finally {
16
+ if (timer !== undefined) clearTimeout(timer);
17
+ }
18
+ }
19
+
20
+ /** Coordinates fresh configuration with the independent appointment; UI keeps edit drafts. */
21
+ class PickupTimingController {
22
+ latest;
23
+ requests = new Map();
24
+ writeSequence = 0;
25
+ constructor(host) {
26
+ this.host = host;
27
+ }
28
+ now() {
29
+ return this.host.now?.() ?? new Date();
30
+ }
31
+ async snapshot(force, includeSchedule, tolerateScheduleError = false) {
32
+ const scope = this.host.getScope();
33
+ const key = `${scope}:${force}:${includeSchedule}:${tolerateScheduleError}`;
34
+ let request = this.requests.get(key);
35
+ if (!request) {
36
+ request = (async () => {
37
+ const config = (0, _FulfillmentTiming.normalizePickupTimingConfig)(await withTimeout(this.host.loadOpenData(force)));
38
+ const timezone = this.host.getTimezone();
39
+ let schedules = [];
40
+ let error;
41
+ if (includeSchedule && config.handoverModes.includes('pick_up') && config.timingMode !== 'asap_only' && config.scheduleIds.length) {
42
+ try {
43
+ const schedule = this.host.getSchedule();
44
+ if (!schedule) throw new Error('Pickup schedule is unavailable. Please try again.');
45
+ await withTimeout(schedule.loadAllSchedule({
46
+ useCache: false
47
+ }));
48
+ const ids = [...new Set([...config.scheduleIds, ...(config.operatingHoursScheduleIds || [])])];
49
+ schedules = (0, _lodashEs.cloneDeep)(schedule.getScheduleListByIds(ids) || []);
50
+ } catch (reason) {
51
+ if (!tolerateScheduleError) throw reason;
52
+ error = reason instanceof Error ? reason.message : 'Pickup schedule is unavailable. Please try again.';
53
+ }
54
+ }
55
+ if (scope !== this.host.getScope()) throw new Error('Store settings changed. Please try again.');
56
+ return {
57
+ config,
58
+ timezone,
59
+ schedules,
60
+ scope,
61
+ error
62
+ };
63
+ })();
64
+ this.requests.set(key, request);
65
+ }
66
+ try {
67
+ const result = await request;
68
+ if (scope !== this.host.getScope()) throw new Error('Store settings changed. Please try again.');
69
+ this.latest = result;
70
+ return result;
71
+ } finally {
72
+ if (this.requests.get(key) === request) this.requests.delete(key);
73
+ }
74
+ }
75
+ async getAvailability(params = {}) {
76
+ const snapshot = await this.snapshot(params.force !== false, true, true);
77
+ const now = this.now();
78
+ try {
79
+ return {
80
+ ...(0, _FulfillmentTiming.computePickupTimingAvailability)({
81
+ ...snapshot,
82
+ date: params.date,
83
+ now
84
+ }),
85
+ error: snapshot.error
86
+ };
87
+ } catch (reason) {
88
+ // Keep the handover selector usable when only Pickup's timezone is unavailable.
89
+ return {
90
+ config: snapshot.config,
91
+ timezone: snapshot.timezone,
92
+ now: now.toISOString(),
93
+ today: '',
94
+ maxDate: '',
95
+ date: params.date || '',
96
+ dates: [],
97
+ slots: [],
98
+ error: reason instanceof Error ? reason.message : 'Pickup times are unavailable. Please try again.'
99
+ };
100
+ }
101
+ }
102
+ getSelection() {
103
+ const order = this.host.getOrder();
104
+ const tempOrder = order.getTempOrder();
105
+ const booking = order.getPickupTimingBooking();
106
+ const config = this.latest?.scope === this.host.getScope() ? this.latest.config : undefined;
107
+ const storedServiceType = String(tempOrder?.metadata?.shop_service_data?.service_type || '');
108
+ const serviceType = config?.handoverModes.length ? config.handoverModes.includes(storedServiceType) ? storedServiceType : config.handoverModes[0] : storedServiceType;
109
+ const details = serviceType === 'pick_up' ? booking?.metadata.fulfillment_timing : undefined;
110
+ const mode = config?.timingMode === 'asap_only' ? 'asap' : config?.timingMode === 'scheduled_only' ? 'scheduled' : details?.mode ?? 'asap';
111
+ const sameMode = details?.mode === mode;
112
+ return {
113
+ serviceType,
114
+ mode,
115
+ source: mode === 'asap' ? 'asap' : sameMode && details?.source || 'schedule',
116
+ ...(sameMode && booking ? {
117
+ date: booking.start_date,
118
+ time: booking.start_time,
119
+ instant: details?.instant
120
+ } : {})
121
+ };
122
+ }
123
+ async validateSnapshot(selection) {
124
+ const snapshot = await this.snapshot(true, selection.serviceType === 'pick_up' && selection.mode === 'scheduled' && selection.source !== 'pos_manual');
125
+ const now = this.now();
126
+ const result = (0, _FulfillmentTiming.validatePickupTimingSelection)(selection, {
127
+ ...snapshot,
128
+ now,
129
+ allowManualOverride: this.host.isPos()
130
+ });
131
+ return {
132
+ snapshot,
133
+ now,
134
+ result
135
+ };
136
+ }
137
+ async validate(selection) {
138
+ return (await this.validateSnapshot(selection)).result;
139
+ }
140
+ async setSelection(selection, options = {}) {
141
+ const order = this.host.getOrder();
142
+ const tempOrder = order.ensureTempOrder();
143
+ const sequence = ++this.writeSequence;
144
+ const {
145
+ snapshot,
146
+ now,
147
+ result
148
+ } = await this.validateSnapshot(selection);
149
+ if (sequence !== this.writeSequence || order.getTempOrder() !== tempOrder || snapshot.scope !== this.host.getScope()) {
150
+ throw new Error('The order changed. Please choose the pickup time again.');
151
+ }
152
+ if (!result.valid) throw new Error(result.message || 'Please choose a valid pickup time.');
153
+ if (!snapshot.config.handoverModes.length) {
154
+ order.clearPickupTimingBooking();
155
+ return this.getSelection();
156
+ }
157
+ if (selection.serviceType !== 'pick_up') {
158
+ order.updateTempOrderServiceType(selection.serviceType);
159
+ return this.getSelection();
160
+ }
161
+ const slot = selection.mode === 'asap' ? (0, _FulfillmentTiming.getAsapPickupTiming)({
162
+ ...snapshot,
163
+ now
164
+ }) : result.slot;
165
+ if (!slot) throw new Error('Please choose a pickup time.');
166
+ // Validate everything before either field is changed; notify once after the combined update.
167
+ tempOrder.metadata = {
168
+ ...(tempOrder.metadata || {}),
169
+ shop_service_data: {
170
+ ...(tempOrder.metadata?.shop_service_data || {}),
171
+ service_type: 'pick_up'
172
+ }
173
+ };
174
+ order.setPickupTimingBooking({
175
+ mode: selection.mode,
176
+ source: selection.mode === 'asap' ? 'asap' : selection.source === 'pos_manual' ? 'pos_manual' : 'schedule',
177
+ date: slot.date,
178
+ time: slot.time,
179
+ instant: slot.instant,
180
+ timezone: snapshot.timezone,
181
+ frozen: options.frozen ?? false
182
+ });
183
+ return this.getSelection();
184
+ }
185
+
186
+ /** Called before a new checkout/payment; payment callbacks keep their already prepared appointment. */
187
+ async prepare(options = {}) {
188
+ const order = this.host.getOrder();
189
+ const tempOrder = order.getTempOrder();
190
+ const sequence = this.writeSequence;
191
+ if (order.getOrderIdentity()?.isSubmitted) return this.getSelection();
192
+ const booking = order.getPickupTimingBooking();
193
+ if (booking?.metadata.fulfillment_timing?.frozen) return this.getSelection();
194
+ // An actual payment callback must never revalidate or move the time after funds have been taken.
195
+ const paymentStatus = order.getTempOrder()?.payment_status;
196
+ if (paymentStatus === 'paid' || paymentStatus === 'partially_paid') {
197
+ if (booking?.metadata.fulfillment_timing) {
198
+ const detail = booking.metadata.fulfillment_timing;
199
+ order.setPickupTimingBooking({
200
+ ...detail,
201
+ date: booking.start_date,
202
+ time: booking.start_time,
203
+ frozen: true
204
+ });
205
+ }
206
+ return this.getSelection();
207
+ }
208
+ const snapshot = await this.snapshot(true, false);
209
+ if (order !== this.host.getOrder() || order.getTempOrder() !== tempOrder || sequence !== this.writeSequence) {
210
+ throw new Error('The order changed. Please choose the pickup time again.');
211
+ }
212
+ const selection = this.getSelection();
213
+ if (!snapshot.config.handoverModes.length) {
214
+ order.clearPickupTimingBooking();
215
+ return selection;
216
+ }
217
+ return this.setSelection(selection, {
218
+ frozen: options.freeze ?? false
219
+ });
220
+ }
221
+ }
222
+ exports.PickupTimingController = PickupTimingController;
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.parseSalesResponse = parseSalesResponse;
7
7
  var _orderCollectionIdentity = require("../../../modules/Order/utils/orderCollectionIdentity");
8
+ var _pickupTiming = require("../../../modules/Order/pickupTiming");
8
9
  /**
9
10
  * 把 `/order/sales/{id}` 响应(OrderData)解析为 ISalesDetail。
10
11
  *
@@ -64,6 +65,7 @@ function pickCustomer(raw) {
64
65
  */
65
66
  function isRootBooking(booking) {
66
67
  if (!booking || typeof booking !== 'object') return false;
68
+ if ((0, _pickupTiming.isFulfillmentTimingBooking)(booking)) return false;
67
69
  const parentId = booking.parent_id;
68
70
  if (parentId === 0 || parentId === null || parentId === undefined) return true;
69
71
  return false;
@@ -71,6 +73,7 @@ function isRootBooking(booking) {
71
73
  function indexBookingsByProductUid(bookings) {
72
74
  const result = {};
73
75
  for (const b of bookings) {
76
+ if ((0, _pickupTiming.isFulfillmentTimingBooking)(b)) continue;
74
77
  const uid = b && (b.product_uid || b.metadata?.product_uid) || '';
75
78
  if (!uid) continue;
76
79
  const key = String(uid);
@@ -326,7 +326,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
326
326
  date: string;
327
327
  status: string;
328
328
  week: string;
329
- weekNum: 0 | 2 | 1 | 3 | 4 | 5 | 6;
329
+ weekNum: 0 | 1 | 2 | 3 | 4 | 5 | 6;
330
330
  }[]>;
331
331
  submitTimeSlot(timeSlots: TimeSliceItem): void;
332
332
  private getScheduleDataByIds;
@@ -8,12 +8,14 @@ exports.buildCustomerView = buildCustomerView;
8
8
  exports.indexBookingsByProductUid = indexBookingsByProductUid;
9
9
  exports.isWalkInCustomer = isWalkInCustomer;
10
10
  var _addTimeAvailability = require("./addTimeAvailability");
11
+ var _pickupTiming = require("../../../modules/Order/pickupTiming");
11
12
  /**
12
13
  * 仅按 booking.product_uid 建索引,避免发明 booking 与商品行之间的其它等价关系。
13
14
  */
14
15
  function indexBookingsByProductUid(bookings) {
15
16
  if (!Array.isArray(bookings) || bookings.length === 0) return {};
16
17
  return bookings.reduce((acc, booking) => {
18
+ if ((0, _pickupTiming.isFulfillmentTimingBooking)(booking)) return acc;
17
19
  const uid = booking?.product_uid;
18
20
  if (uid === undefined || uid === null || uid === '') return acc;
19
21
  const key = String(uid);
@@ -105,7 +107,7 @@ function isGiftProductLine(line) {
105
107
  function buildCartView(lines, bookings, options = {}) {
106
108
  const productLines = Array.isArray(lines) ? lines : [];
107
109
  const bookingList = Array.isArray(bookings) ? bookings : [];
108
- const childBookings = bookingList.filter(booking => booking.parent_id !== 0);
110
+ const childBookings = bookingList.filter(booking => booking.parent_id !== 0 && !(0, _pickupTiming.isFulfillmentTimingBooking)(booking));
109
111
  const bookingsByProductUid = indexBookingsByProductUid(childBookings);
110
112
  const linesByUid = indexProductsByUid(productLines);
111
113
  const addTimeLinesByBookingUid = indexAddTimeProductsByBookingUid(productLines);
@@ -59,6 +59,7 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
59
59
  protected defaultName: string;
60
60
  protected defaultVersion: string;
61
61
  protected store: UnifiedBookingSalesState;
62
+ protected pickupTimingEnabled: boolean;
62
63
  private unifiedProductCatalog;
63
64
  private availabilityRuntimeOffsetMinutes;
64
65
  private availabilityScheduleCatalog;
@@ -148,6 +149,7 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
148
149
  */
149
150
  loadOpenData(params: UnifiedBookingSalesLoadOpenDataParams): Promise<OpenDataConfig>;
150
151
  getOpenData(): Promise<OpenDataConfig | null>;
152
+ protected loadPickupTimingOpenData(force: boolean): Promise<Record<string, unknown>>;
151
153
  private resetAllergyItemRuleState;
152
154
  private fetchAllergyItemRuleConfigs;
153
155
  private ensureAllergyItemRuleConfigsLoaded;
@@ -1199,6 +1199,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1199
1199
  defaultName = 'unifiedBookingSales';
1200
1200
  defaultVersion = '1.0.0';
1201
1201
  store = {};
1202
+ pickupTimingEnabled = true;
1202
1203
  unifiedProductCatalog = [];
1203
1204
  availabilityRuntimeOffsetMinutes = null;
1204
1205
  availabilityScheduleCatalog = null;
@@ -1747,6 +1748,25 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1747
1748
  channel
1748
1749
  });
1749
1750
  }
1751
+ async loadPickupTimingOpenData(force) {
1752
+ const scope = this.getPickupTimingScope();
1753
+ // A fresh validation must not borrow a request started for a previous store/context.
1754
+ if (force && this.loadOpenDataInFlight) await this.loadOpenDataInFlight;
1755
+ if (scope !== this.getPickupTimingScope()) {
1756
+ throw new Error('Store settings changed. Please try again.');
1757
+ }
1758
+ const businessCode = String(this.otherParams?.businessCode ?? this.otherParams?.business_code ?? '').trim();
1759
+ const channel = String(this.getSubmitOrderSalesChannel() ?? '').trim();
1760
+ const config = await this.loadOpenData({
1761
+ businessCode,
1762
+ channel,
1763
+ force
1764
+ });
1765
+ if (scope !== this.getPickupTimingScope()) {
1766
+ throw new Error('Store settings changed. Please try again.');
1767
+ }
1768
+ return config;
1769
+ }
1750
1770
  resetAllergyItemRuleState() {
1751
1771
  this.allergyItemRuleConfigs = [];
1752
1772
  this.allergyItemRuleConfigsPromise = null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@pisell/pisellos",
4
- "version": "0.10.49",
4
+ "version": "0.10.51",
5
5
  "description": "一个可扩展的前端模块化SDK框架,支持插件系统",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",