1 | const Utils = require('q3-schema-utils');
|
2 |
|
3 | const getRemainder = (a, b, c) => {
|
4 | if (a <= b && a < c) return a;
|
5 | if (a > b && b < c) return b;
|
6 | if (!b && a < c) return a;
|
7 | return c;
|
8 | };
|
9 |
|
10 | const sofar = (a) => a.reduce((all, curr) => all + curr, 0);
|
11 |
|
12 | const compact = (a) => a.filter((v) => v && v !== '');
|
13 |
|
14 | const hasLength = (a) =>
|
15 | Array.isArray(a) && compact(a).length;
|
16 |
|
17 | class RebateDecorator {
|
18 | static async findApplicable(
|
19 | couponCode,
|
20 | items,
|
21 | opts = {},
|
22 | ) {
|
23 | const rebates = await this.find({
|
24 | ...(couponCode && { couponCode }),
|
25 | ...this.getDateQuery(),
|
26 | ...opts,
|
27 | }).exec();
|
28 |
|
29 | return rebates.filter(
|
30 | (rebate) =>
|
31 | rebate.hasRequiredSkus(items) &&
|
32 | rebate.hasConditionalSkus(items),
|
33 | );
|
34 | }
|
35 |
|
36 | static async reduceQualifiedRebates(
|
37 | couponCode,
|
38 | items,
|
39 | opts,
|
40 | interceptor,
|
41 | ) {
|
42 | const rebates = await this.findApplicable(
|
43 | couponCode,
|
44 | items,
|
45 | opts,
|
46 | );
|
47 |
|
48 | return rebates
|
49 | .map((rebate) =>
|
50 | typeof interceptor === 'function'
|
51 | ? interceptor(rebate)
|
52 | : rebate,
|
53 | )
|
54 | .map((rebate) => {
|
55 | const redact = rebate.redactItems(items);
|
56 | const sorted = rebate.greatestPotentialValue(
|
57 | redact,
|
58 | );
|
59 | const amounts = rebate.getMaximumAmounts(sorted);
|
60 | const values = rebate.getPriceValues(sorted);
|
61 |
|
62 |
|
63 | const output = rebate.toJSON();
|
64 | output.applicableTo = sorted.map((item, i) => ({
|
65 | id: item.id,
|
66 | value: values[i],
|
67 | amount: amounts[i],
|
68 | }));
|
69 |
|
70 | return output;
|
71 | });
|
72 | }
|
73 |
|
74 | reduceItems(items) {
|
75 | const redact = this.redactItems(items);
|
76 | const sorted = this.greatestPotentialValue(redact);
|
77 | const amounts = this.getMaximumAmounts(sorted);
|
78 | const values = this.getPriceValues(sorted);
|
79 |
|
80 | this.applicableTo = sorted.map((item, i) => ({
|
81 | id: item.id,
|
82 | value: values[i],
|
83 | amount: amounts[i],
|
84 | }));
|
85 |
|
86 | return this;
|
87 | }
|
88 |
|
89 | hasRequiredSkus(items) {
|
90 | return hasLength(this.requiredSkus)
|
91 | ? items.some(this.matchItemSku.bind(this))
|
92 | : true;
|
93 | }
|
94 |
|
95 | hasConditionalSkus(items) {
|
96 | if (!hasLength(this.conditionalSkus)) return true;
|
97 |
|
98 | const total = items.reduce(
|
99 | (acc, { sku, quantity }) =>
|
100 | Utils.isMatch(sku, compact(this.conditionalSkus))
|
101 | ? acc + quantity
|
102 | : acc,
|
103 | 0,
|
104 | );
|
105 |
|
106 | return (
|
107 | (!this.conditionalSkuThreshold && total.length) ||
|
108 | this.conditionalSkuThreshold <= total
|
109 | );
|
110 | }
|
111 |
|
112 | matchItemSku(item) {
|
113 | return Utils.isMatch(
|
114 | item.sku,
|
115 | compact(this.requiredSkus),
|
116 | );
|
117 | }
|
118 |
|
119 | redactItems(items) {
|
120 | return items.filter(this.matchItemSku.bind(this));
|
121 | }
|
122 |
|
123 | getMaximumAmounts(items, query) {
|
124 | const { maximumPerProduct } = this;
|
125 | const redeemable = this.getRedeemable(query);
|
126 |
|
127 | return items.reduce(
|
128 | (accumulator, next) =>
|
129 | accumulator.concat(
|
130 | Math.max(
|
131 | 0,
|
132 | getRemainder(
|
133 | redeemable - sofar(accumulator),
|
134 | maximumPerProduct,
|
135 | next.quantity,
|
136 | ),
|
137 | ),
|
138 | ),
|
139 | [],
|
140 | );
|
141 | }
|
142 |
|
143 | getPriceValues(items) {
|
144 | return items.reduce(
|
145 | (accumulator, next) =>
|
146 | accumulator.concat(this.evaluate(next)),
|
147 | [],
|
148 | );
|
149 | }
|
150 |
|
151 | greatestPotentialValue(items) {
|
152 | const redeemable = this.getRedeemable();
|
153 | const { maximumPerProduct } = this;
|
154 |
|
155 | const multiply = (item) => {
|
156 | const quantity = getRemainder(
|
157 | redeemable,
|
158 | maximumPerProduct,
|
159 | item.quantity,
|
160 | );
|
161 |
|
162 | return (
|
163 | this.evaluate({ ...item, quantity }) * quantity
|
164 | );
|
165 | };
|
166 |
|
167 | return items.sort((a, b) => multiply(b) - multiply(a));
|
168 | }
|
169 |
|
170 | evaluate({ price, quantity }) {
|
171 | const { tiers = [] } = this;
|
172 | let { value } = this;
|
173 | let sum = price;
|
174 |
|
175 | tiers.forEach((tier) => {
|
176 | if (tier.quantity <= quantity) value = tier.value;
|
177 | });
|
178 |
|
179 | if (this.symbol === '%') {
|
180 | sum = price * (value / 100);
|
181 | } else {
|
182 | if (this.symbol === '=') sum = price - value;
|
183 | if (this.symbol === '$') sum = value;
|
184 | }
|
185 |
|
186 | return Utils.round(sum);
|
187 | }
|
188 |
|
189 | getRedeemable() {
|
190 | const {
|
191 | maximumPerOrder,
|
192 | maximumPerHistory,
|
193 | historicalCount = 0,
|
194 | } = this;
|
195 | let redeemable = Infinity;
|
196 |
|
197 | if (maximumPerOrder) {
|
198 | redeemable = maximumPerOrder;
|
199 | }
|
200 |
|
201 | if (maximumPerHistory) {
|
202 | redeemable =
|
203 | redeemable > maximumPerHistory - historicalCount
|
204 | ? maximumPerHistory - historicalCount
|
205 | : redeemable;
|
206 | }
|
207 |
|
208 | return redeemable;
|
209 | }
|
210 |
|
211 | sum() {
|
212 | const { applicableTo = [] } = this;
|
213 | return applicableTo.reduce(
|
214 | (prev, next) => prev + next.value * next.amount,
|
215 | 0,
|
216 | );
|
217 | }
|
218 |
|
219 | async setHistoricalCount() {
|
220 | if (
|
221 | this.historicalCount ||
|
222 | typeof this.queryHistory !== 'function'
|
223 | )
|
224 | return;
|
225 |
|
226 | this.historicalCount = await this.queryHistory(this);
|
227 | }
|
228 | }
|
229 |
|
230 | module.exports = RebateDecorator;
|