@pisell/pisellos 2.2.230 → 2.2.231
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/model/strategy/adapter/promotion/index.js +9 -0
- package/dist/modules/Order/index.js +44 -29
- package/dist/modules/Order/types.d.ts +14 -2
- package/dist/modules/Order/types.js +8 -1
- package/dist/modules/ProductList/index.d.ts +9 -12
- package/dist/modules/ProductList/index.js +122 -59
- package/dist/modules/ProductList/types.d.ts +14 -0
- package/dist/server/index.d.ts +21 -0
- package/dist/server/index.js +154 -34
- package/dist/server/utils/small-ticket.js +113 -29
- package/dist/solution/BookingByStep/index.d.ts +1 -1
- package/dist/solution/BookingTicket/index.d.ts +8 -0
- package/dist/solution/BookingTicket/index.js +191 -154
- package/dist/solution/BookingTicket/types.d.ts +4 -0
- package/dist/solution/BookingTicket/utils/cartView.js +20 -6
- package/dist/solution/BookingTicket/utils/resolveBestAddTimePlan.d.ts +189 -0
- package/dist/solution/BookingTicket/utils/resolveBestAddTimePlan.js +429 -0
- package/lib/modules/Order/index.js +18 -5
- package/lib/modules/Order/types.d.ts +14 -2
- package/lib/modules/ProductList/index.d.ts +9 -12
- package/lib/modules/ProductList/index.js +32 -4
- package/lib/modules/ProductList/types.d.ts +14 -0
- package/lib/server/index.d.ts +21 -0
- package/lib/server/index.js +107 -9
- package/lib/server/utils/small-ticket.js +78 -1
- package/lib/solution/BookingByStep/index.d.ts +1 -1
- package/lib/solution/BookingTicket/index.d.ts +8 -0
- package/lib/solution/BookingTicket/index.js +16 -0
- package/lib/solution/BookingTicket/types.d.ts +4 -0
- package/lib/solution/BookingTicket/utils/cartView.js +14 -7
- package/lib/solution/BookingTicket/utils/resolveBestAddTimePlan.d.ts +189 -0
- package/lib/solution/BookingTicket/utils/resolveBestAddTimePlan.js +241 -0
- package/package.json +1 -1
|
@@ -51,34 +51,41 @@ function indexProductsByUid(lines) {
|
|
|
51
51
|
return acc;
|
|
52
52
|
}, {});
|
|
53
53
|
}
|
|
54
|
+
function isGiftProductLine(line) {
|
|
55
|
+
var _a;
|
|
56
|
+
return Boolean((_a = line == null ? void 0 : line.metadata) == null ? void 0 : _a._giftInfo);
|
|
57
|
+
}
|
|
54
58
|
function buildCartView(lines, bookings) {
|
|
55
59
|
const productLines = Array.isArray(lines) ? lines : [];
|
|
56
60
|
const bookingList = Array.isArray(bookings) ? bookings : [];
|
|
57
61
|
const linesByUid = indexProductsByUid(productLines);
|
|
58
62
|
const claimedUids = /* @__PURE__ */ new Set();
|
|
59
|
-
const cartBookings = bookingList.filter((booking) => booking.parent_id !== 0).
|
|
63
|
+
const cartBookings = bookingList.filter((booking) => booking.parent_id !== 0).reduce((acc, booking) => {
|
|
60
64
|
const uid = booking == null ? void 0 : booking.product_uid;
|
|
61
65
|
let product = null;
|
|
62
66
|
if (uid !== void 0 && uid !== null && uid !== "") {
|
|
63
67
|
const key = String(uid);
|
|
64
68
|
if (!claimedUids.has(key)) {
|
|
65
69
|
const line = linesByUid[key];
|
|
70
|
+
if (isGiftProductLine(line))
|
|
71
|
+
return acc;
|
|
66
72
|
if (line) {
|
|
67
73
|
claimedUids.add(key);
|
|
68
74
|
product = line;
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
|
-
|
|
78
|
+
acc.push({
|
|
73
79
|
...booking,
|
|
74
80
|
_extend: { product }
|
|
75
|
-
};
|
|
76
|
-
|
|
81
|
+
});
|
|
82
|
+
return acc;
|
|
83
|
+
}, []);
|
|
77
84
|
const items = productLines.filter((line) => {
|
|
78
|
-
var _a
|
|
79
|
-
if ((
|
|
85
|
+
var _a;
|
|
86
|
+
if (isGiftProductLine(line))
|
|
80
87
|
return false;
|
|
81
|
-
const uid = (
|
|
88
|
+
const uid = (_a = line.metadata) == null ? void 0 : _a.unique_identification_number;
|
|
82
89
|
if (uid === void 0 || uid === null || uid === "")
|
|
83
90
|
return true;
|
|
84
91
|
return !claimedUids.has(String(uid));
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 计算指定加时时长下的最优购买方案。
|
|
3
|
+
*
|
|
4
|
+
* 输入的商品结构来自旧版 booking addTimeModal 的加时商品配置映射:
|
|
5
|
+
* - `minMinutes`:该商品第一次可选择的最小时长。
|
|
6
|
+
* - `maxMinutes`:单次购买该商品可覆盖的最大时长。
|
|
7
|
+
* - `unitMinutes`:超过最小时长后,每次递增的时长切片。
|
|
8
|
+
* - `price`:每个 `unitMinutes` 切片的价格。
|
|
9
|
+
* - `unlimited`:固定价格商品,可覆盖任意目标时长。
|
|
10
|
+
*
|
|
11
|
+
* 算法流程:
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* products
|
|
15
|
+
* |
|
|
16
|
+
* v
|
|
17
|
+
* 将每个普通商品展开成有限候选档位
|
|
18
|
+
* 商品 A: min=30, max=60, unit=10, price=10
|
|
19
|
+
* => 30m/$30, 40m/$40, 50m/$50, 60m/$60
|
|
20
|
+
* |
|
|
21
|
+
* v
|
|
22
|
+
* 单独加入 unlimited 候选
|
|
23
|
+
* unlimited 商品 => targetMinutes / 固定商品价格
|
|
24
|
+
* |
|
|
25
|
+
* v
|
|
26
|
+
* 按 covered minutes 做动态规划
|
|
27
|
+
* dp[minutes] = 正好覆盖 `minutes` 时的最低价候选组合
|
|
28
|
+
* |
|
|
29
|
+
* v
|
|
30
|
+
* 从所有 dp[minutes >= targetMinutes] 中挑选最优方案
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* 候选档位生成刻意对齐旧版 addTimeModal:
|
|
34
|
+
* `totalPrice = minutes / unitMinutes * price`.
|
|
35
|
+
* 例如 `minMinutes=30`、`unitMinutes=10`、`price=10` 时:
|
|
36
|
+
* - 30 分钟 => 30 / 10 * 10 = 30
|
|
37
|
+
* - 60 分钟 => 60 / 10 * 10 = 60
|
|
38
|
+
*
|
|
39
|
+
* 目标时长不要求精确命中某个档位,只需要覆盖即可。例如目标是 51 分钟时,
|
|
40
|
+
* 60 分钟档位可以作为候选。动态规划也允许跨商品组合,例如商品 A 的 30m
|
|
41
|
+
* 加上商品 B 的 30m。最终方案按以下优先级比较:
|
|
42
|
+
* 1. 总价更低;
|
|
43
|
+
* 2. 超出时长更少,即 `coveredMinutes - targetMinutes` 更小;
|
|
44
|
+
* 3. 结果行数更少,让购买方案尽量简单。
|
|
45
|
+
*
|
|
46
|
+
* 典型匹配示例:
|
|
47
|
+
*
|
|
48
|
+
* 示例 1:精确命中,直接选择更便宜的商品。
|
|
49
|
+
* ```
|
|
50
|
+
* target = 30
|
|
51
|
+
* 商品 A: min=30, max=30, unit=30, price=10 => 30m/$10
|
|
52
|
+
* 商品 B: min=30, max=30, unit=30, price=8 => 30m/$8
|
|
53
|
+
* 结果:选择商品 B,coveredMinutes=30,totalPrice=8
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* 示例 2:目标时长落在两个档位之间,向上选择可覆盖的档位。
|
|
57
|
+
* ```
|
|
58
|
+
* target = 51
|
|
59
|
+
* 商品 A: min=30, max=120, unit=10, price=10
|
|
60
|
+
* 候选:30m/$30, 40m/$40, 50m/$50, 60m/$60 ...
|
|
61
|
+
* 结果:50m 不足以覆盖 51m,因此选择 60m/$60
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* 示例 3:不同商品都能覆盖同一目标时长,比较最终价格。
|
|
65
|
+
* ```
|
|
66
|
+
* target = 51
|
|
67
|
+
* 商品 A: min=30, max=120, unit=10, price=10 => 60m/$60
|
|
68
|
+
* 商品 B: min=30, max=120, unit=30, price=25 => 60m/$50
|
|
69
|
+
* 结果:两个方案都覆盖 60m,但商品 B 更便宜,所以选择商品 B
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* 示例 4:允许跨商品组合,组合价更低时会选择组合。
|
|
73
|
+
* ```
|
|
74
|
+
* target = 90
|
|
75
|
+
* 商品 A: min=30, max=30, unit=30, price=5 => 30m/$5
|
|
76
|
+
* 商品 B: min=60, max=60, unit=60, price=8 => 60m/$8
|
|
77
|
+
* 商品 C: min=90, max=90, unit=90, price=20 => 90m/$20
|
|
78
|
+
* 结果:商品 A + 商品 B = 90m/$13,比商品 C 更便宜
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* 示例 5:允许重复购买同一个候选档位。
|
|
82
|
+
* ```
|
|
83
|
+
* target = 90
|
|
84
|
+
* 商品 A: min=30, max=30, unit=30, price=5 => 30m/$5
|
|
85
|
+
* 结果:购买 3 份商品 A,coveredMinutes=90,totalPrice=15,
|
|
86
|
+
* lines 中会合并为一行:minutes=30,quantity=3
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* 示例 6:unlimited 可以覆盖任意目标时长,并参与价格比较。
|
|
90
|
+
* ```
|
|
91
|
+
* target = 180
|
|
92
|
+
* 商品 A: min=30, max=120, unit=30, price=10
|
|
93
|
+
* 商品 B: unlimited=true, price=35
|
|
94
|
+
* 普通商品可以组合成 180m,例如 120m + 60m,总价 $60
|
|
95
|
+
* unlimited 商品固定 $35
|
|
96
|
+
* 结果:选择 unlimited 商品,coveredMinutes=180,totalPrice=35
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* 示例 7:unlimited 不一定优先,普通商品更便宜时选择普通商品。
|
|
100
|
+
* ```
|
|
101
|
+
* target = 60
|
|
102
|
+
* 商品 A: min=30, max=60, unit=30, price=10 => 60m/$20
|
|
103
|
+
* 商品 B: unlimited=true, price=50
|
|
104
|
+
* 结果:选择商品 A 的 60m 档位
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* 示例 8:价格相同,优先选择超出时长更少的方案。
|
|
108
|
+
* ```
|
|
109
|
+
* target = 51
|
|
110
|
+
* 商品 A: 60m/$20
|
|
111
|
+
* 商品 B: 90m/$20
|
|
112
|
+
* 结果:选择 60m,因为 overage=9,小于 90m 的 overage=39
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* 示例 9:价格和覆盖时长都相同,优先选择结果行数更少的方案。
|
|
116
|
+
* ```
|
|
117
|
+
* target = 60
|
|
118
|
+
* 商品 A: 60m/$20
|
|
119
|
+
* 商品 B: 30m/$10
|
|
120
|
+
* 商品 C: 30m/$10
|
|
121
|
+
* 结果:商品 A 单行方案和 B+C 两行方案价格、时长相同,选择商品 A
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* 示例 10:没有任何有效商品时返回 `NO_PRODUCTS`。
|
|
125
|
+
* ```
|
|
126
|
+
* target = 30
|
|
127
|
+
* products = []
|
|
128
|
+
* 结果:available=false,unavailableReason='NO_PRODUCTS'
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* 示例 11:有商品但没有可用候选时返回 `NO_COVERAGE`。
|
|
132
|
+
* ```
|
|
133
|
+
* target = 30
|
|
134
|
+
* 商品 A: price=0,或 minMinutes 缺失/无效
|
|
135
|
+
* 结果:available=false,unavailableReason='NO_COVERAGE'
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* 示例 12:目标时长小于等于 0 时无需购买。
|
|
139
|
+
* ```
|
|
140
|
+
* target = 0
|
|
141
|
+
* 结果:available=true,coveredMinutes=0,totalPrice=0,lines=[]
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* 搜索边界:
|
|
145
|
+
* 有限档位只需要搜索到 `targetMinutes + maxFiniteCandidateMinutes`。
|
|
146
|
+
* 这里的 `maxFiniteCandidateMinutes` 是所有普通候选档位里的最大时长。
|
|
147
|
+
* 例如目标是 51 分钟,最大普通档位是 120 分钟,则只搜索到 171 分钟。
|
|
148
|
+
* 如果某个组合覆盖了 180 分钟,那么移除其中任意一个普通档位后,剩余时长
|
|
149
|
+
* 仍然有机会覆盖 51 分钟,而且价格一定不会更高,所以 180 分钟这个组合不
|
|
150
|
+
* 可能是最优解。
|
|
151
|
+
*/
|
|
152
|
+
export interface AddTimeProduct {
|
|
153
|
+
id: string | number;
|
|
154
|
+
title?: string;
|
|
155
|
+
price: number;
|
|
156
|
+
minMinutes?: number;
|
|
157
|
+
maxMinutes?: number;
|
|
158
|
+
unitMinutes?: number;
|
|
159
|
+
unlimited?: boolean;
|
|
160
|
+
}
|
|
161
|
+
export interface AddTimePlanLine {
|
|
162
|
+
productId: string | number;
|
|
163
|
+
productTitle?: string;
|
|
164
|
+
minutes: number;
|
|
165
|
+
quantity: number;
|
|
166
|
+
unitPrice: number;
|
|
167
|
+
totalPrice: number;
|
|
168
|
+
unlimited?: boolean;
|
|
169
|
+
}
|
|
170
|
+
export interface AddTimePlan {
|
|
171
|
+
available: boolean;
|
|
172
|
+
unavailableReason?: 'NO_PRODUCTS' | 'NO_COVERAGE';
|
|
173
|
+
targetMinutes: number;
|
|
174
|
+
coveredMinutes: number;
|
|
175
|
+
totalPrice: number;
|
|
176
|
+
lines: AddTimePlanLine[];
|
|
177
|
+
}
|
|
178
|
+
interface AddTimeCandidate {
|
|
179
|
+
productId: string | number;
|
|
180
|
+
productTitle?: string;
|
|
181
|
+
minutes: number;
|
|
182
|
+
unitPrice: number;
|
|
183
|
+
totalPrice: number;
|
|
184
|
+
unlimited?: boolean;
|
|
185
|
+
}
|
|
186
|
+
export declare const ADD_TIME_UNIT_MINUTES: readonly [10, 15, 20, 30, 45, 60];
|
|
187
|
+
export declare function buildAddTimeCandidates(products: AddTimeProduct[]): AddTimeCandidate[];
|
|
188
|
+
export declare function resolveBestAddTimePlan(products: AddTimeProduct[], targetMinutes: number): AddTimePlan;
|
|
189
|
+
export {};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// src/solution/BookingTicket/utils/resolveBestAddTimePlan.ts
|
|
20
|
+
var resolveBestAddTimePlan_exports = {};
|
|
21
|
+
__export(resolveBestAddTimePlan_exports, {
|
|
22
|
+
ADD_TIME_UNIT_MINUTES: () => ADD_TIME_UNIT_MINUTES,
|
|
23
|
+
buildAddTimeCandidates: () => buildAddTimeCandidates,
|
|
24
|
+
resolveBestAddTimePlan: () => resolveBestAddTimePlan
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(resolveBestAddTimePlan_exports);
|
|
27
|
+
var ADD_TIME_UNIT_MINUTES = [10, 15, 20, 30, 45, 60];
|
|
28
|
+
var toPositiveNumber = (value) => {
|
|
29
|
+
const n = Number(value);
|
|
30
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
31
|
+
};
|
|
32
|
+
var normalizeTargetMinutes = (minutes) => {
|
|
33
|
+
const n = Number(minutes);
|
|
34
|
+
return Number.isFinite(n) ? Math.max(0, Math.ceil(n)) : 0;
|
|
35
|
+
};
|
|
36
|
+
var gcd = (a, b) => {
|
|
37
|
+
let x = Math.abs(a);
|
|
38
|
+
let y = Math.abs(b);
|
|
39
|
+
while (y !== 0) {
|
|
40
|
+
const next = x % y;
|
|
41
|
+
x = y;
|
|
42
|
+
y = next;
|
|
43
|
+
}
|
|
44
|
+
return x;
|
|
45
|
+
};
|
|
46
|
+
var getDpStepMinutes = (candidates) => {
|
|
47
|
+
return candidates.reduce(
|
|
48
|
+
(current, candidate) => gcd(current, candidate.minutes),
|
|
49
|
+
0
|
|
50
|
+
) || 1;
|
|
51
|
+
};
|
|
52
|
+
var comparePlans = (left, right) => {
|
|
53
|
+
if (!left)
|
|
54
|
+
return right;
|
|
55
|
+
if (right.totalPrice !== left.totalPrice) {
|
|
56
|
+
return right.totalPrice < left.totalPrice ? right : left;
|
|
57
|
+
}
|
|
58
|
+
const leftOverage = left.coveredMinutes - left.targetMinutes;
|
|
59
|
+
const rightOverage = right.coveredMinutes - right.targetMinutes;
|
|
60
|
+
if (rightOverage !== leftOverage) {
|
|
61
|
+
return rightOverage < leftOverage ? right : left;
|
|
62
|
+
}
|
|
63
|
+
return right.lines.length < left.lines.length ? right : left;
|
|
64
|
+
};
|
|
65
|
+
var toPlanLine = (candidate) => ({
|
|
66
|
+
productId: candidate.productId,
|
|
67
|
+
productTitle: candidate.productTitle,
|
|
68
|
+
minutes: Number.isFinite(candidate.minutes) ? candidate.minutes : 0,
|
|
69
|
+
quantity: 1,
|
|
70
|
+
unitPrice: candidate.unitPrice,
|
|
71
|
+
totalPrice: candidate.totalPrice,
|
|
72
|
+
unlimited: candidate.unlimited
|
|
73
|
+
});
|
|
74
|
+
var mergePlanLines = (candidates) => {
|
|
75
|
+
const map = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const candidate of candidates) {
|
|
77
|
+
const key = `${candidate.productId}-${candidate.minutes}-${candidate.unlimited ? "u" : "n"}`;
|
|
78
|
+
const existing = map.get(key);
|
|
79
|
+
if (existing) {
|
|
80
|
+
existing.quantity += 1;
|
|
81
|
+
existing.totalPrice += candidate.totalPrice;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
map.set(key, toPlanLine(candidate));
|
|
85
|
+
}
|
|
86
|
+
return Array.from(map.values());
|
|
87
|
+
};
|
|
88
|
+
var restoreDpCandidates = (dp, finiteCandidates, index) => {
|
|
89
|
+
const candidates = [];
|
|
90
|
+
let currentIndex = index;
|
|
91
|
+
while (currentIndex > 0) {
|
|
92
|
+
const current = dp[currentIndex];
|
|
93
|
+
if (!current || current.candidateIndex < 0)
|
|
94
|
+
break;
|
|
95
|
+
candidates.push(finiteCandidates[current.candidateIndex]);
|
|
96
|
+
currentIndex = current.prevIndex;
|
|
97
|
+
}
|
|
98
|
+
return candidates.reverse();
|
|
99
|
+
};
|
|
100
|
+
function buildAddTimeCandidates(products) {
|
|
101
|
+
const candidates = [];
|
|
102
|
+
for (const product of products) {
|
|
103
|
+
const price = toPositiveNumber(product.price);
|
|
104
|
+
if (price == null)
|
|
105
|
+
continue;
|
|
106
|
+
if (product.unlimited) {
|
|
107
|
+
candidates.push({
|
|
108
|
+
productId: product.id,
|
|
109
|
+
productTitle: product.title,
|
|
110
|
+
minutes: Number.POSITIVE_INFINITY,
|
|
111
|
+
unitPrice: price,
|
|
112
|
+
totalPrice: price,
|
|
113
|
+
unlimited: true
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const minMinutes = toPositiveNumber(product.minMinutes);
|
|
118
|
+
if (minMinutes == null)
|
|
119
|
+
continue;
|
|
120
|
+
const maxMinutes = toPositiveNumber(product.maxMinutes) ?? minMinutes;
|
|
121
|
+
const unitMinutes = toPositiveNumber(product.unitMinutes) ?? minMinutes;
|
|
122
|
+
const safeMax = Math.max(minMinutes, maxMinutes);
|
|
123
|
+
for (let minutes = minMinutes; minutes <= safeMax; minutes += unitMinutes) {
|
|
124
|
+
candidates.push({
|
|
125
|
+
productId: product.id,
|
|
126
|
+
productTitle: product.title,
|
|
127
|
+
minutes,
|
|
128
|
+
unitPrice: price,
|
|
129
|
+
// 对齐旧版 addTimeModal:每个 unit_duration 切片收一次 price。例:60/10*10=$60。
|
|
130
|
+
totalPrice: minutes / unitMinutes * price
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return candidates;
|
|
135
|
+
}
|
|
136
|
+
function resolveBestAddTimePlan(products, targetMinutes) {
|
|
137
|
+
const normalizedTarget = normalizeTargetMinutes(targetMinutes);
|
|
138
|
+
if (normalizedTarget <= 0) {
|
|
139
|
+
return {
|
|
140
|
+
available: true,
|
|
141
|
+
targetMinutes: 0,
|
|
142
|
+
coveredMinutes: 0,
|
|
143
|
+
totalPrice: 0,
|
|
144
|
+
lines: []
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (!products.length) {
|
|
148
|
+
return {
|
|
149
|
+
available: false,
|
|
150
|
+
unavailableReason: "NO_PRODUCTS",
|
|
151
|
+
targetMinutes: normalizedTarget,
|
|
152
|
+
coveredMinutes: 0,
|
|
153
|
+
totalPrice: 0,
|
|
154
|
+
lines: []
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const candidates = buildAddTimeCandidates(products);
|
|
158
|
+
const finiteCandidates = candidates.filter(
|
|
159
|
+
(item) => Number.isFinite(item.minutes)
|
|
160
|
+
);
|
|
161
|
+
const unlimitedCandidates = candidates.filter((item) => item.unlimited);
|
|
162
|
+
const maxFiniteMinutes = Math.max(
|
|
163
|
+
0,
|
|
164
|
+
...finiteCandidates.map((item) => item.minutes)
|
|
165
|
+
);
|
|
166
|
+
const searchLimit = normalizedTarget + maxFiniteMinutes;
|
|
167
|
+
let best = null;
|
|
168
|
+
for (const unlimited of unlimitedCandidates) {
|
|
169
|
+
best = comparePlans(best, {
|
|
170
|
+
available: true,
|
|
171
|
+
targetMinutes: normalizedTarget,
|
|
172
|
+
coveredMinutes: normalizedTarget,
|
|
173
|
+
totalPrice: unlimited.totalPrice,
|
|
174
|
+
lines: [toPlanLine({ ...unlimited, minutes: normalizedTarget })]
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (finiteCandidates.length && searchLimit > 0) {
|
|
178
|
+
const stepMinutes = getDpStepMinutes(finiteCandidates);
|
|
179
|
+
const searchLimitIndex = Math.ceil(searchLimit / stepMinutes);
|
|
180
|
+
const targetIndex = Math.ceil(normalizedTarget / stepMinutes);
|
|
181
|
+
const finiteCandidateSteps = finiteCandidates.map(
|
|
182
|
+
(candidate) => candidate.minutes / stepMinutes
|
|
183
|
+
);
|
|
184
|
+
const dp = [];
|
|
185
|
+
dp[0] = {
|
|
186
|
+
price: 0,
|
|
187
|
+
itemCount: 0,
|
|
188
|
+
prevIndex: -1,
|
|
189
|
+
candidateIndex: -1
|
|
190
|
+
};
|
|
191
|
+
for (let index = 0; index <= searchLimitIndex; index += 1) {
|
|
192
|
+
const current = dp[index];
|
|
193
|
+
if (!current)
|
|
194
|
+
continue;
|
|
195
|
+
for (let candidateIndex = 0; candidateIndex < finiteCandidates.length; candidateIndex += 1) {
|
|
196
|
+
const nextIndex = index + finiteCandidateSteps[candidateIndex];
|
|
197
|
+
if (nextIndex > searchLimitIndex)
|
|
198
|
+
continue;
|
|
199
|
+
const candidate = finiteCandidates[candidateIndex];
|
|
200
|
+
const nextPrice = current.price + candidate.totalPrice;
|
|
201
|
+
const nextItemCount = current.itemCount + 1;
|
|
202
|
+
const prev = dp[nextIndex];
|
|
203
|
+
if (!prev || nextPrice < prev.price || nextPrice === prev.price && nextItemCount < prev.itemCount) {
|
|
204
|
+
dp[nextIndex] = {
|
|
205
|
+
price: nextPrice,
|
|
206
|
+
itemCount: nextItemCount,
|
|
207
|
+
prevIndex: index,
|
|
208
|
+
candidateIndex
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (let index = targetIndex; index <= searchLimitIndex; index += 1) {
|
|
214
|
+
const current = dp[index];
|
|
215
|
+
if (!current)
|
|
216
|
+
continue;
|
|
217
|
+
const coveredMinutes = index * stepMinutes;
|
|
218
|
+
best = comparePlans(best, {
|
|
219
|
+
available: true,
|
|
220
|
+
targetMinutes: normalizedTarget,
|
|
221
|
+
coveredMinutes,
|
|
222
|
+
totalPrice: current.price,
|
|
223
|
+
lines: mergePlanLines(restoreDpCandidates(dp, finiteCandidates, index))
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return best ?? {
|
|
228
|
+
available: false,
|
|
229
|
+
unavailableReason: "NO_COVERAGE",
|
|
230
|
+
targetMinutes: normalizedTarget,
|
|
231
|
+
coveredMinutes: 0,
|
|
232
|
+
totalPrice: 0,
|
|
233
|
+
lines: []
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
237
|
+
0 && (module.exports = {
|
|
238
|
+
ADD_TIME_UNIT_MINUTES,
|
|
239
|
+
buildAddTimeCandidates,
|
|
240
|
+
resolveBestAddTimePlan
|
|
241
|
+
});
|