UNPKG

5.24 kBJavaScriptView Raw
1const Utils = require('q3-schema-utils');
2
3const 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
10const sofar = (a) => a.reduce((all, curr) => all + curr, 0);
11
12const compact = (a) => a.filter((v) => v && v !== '');
13
14const hasLength = (a) =>
15 Array.isArray(a) && compact(a).length;
16
17class 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 // eslint-disable-next-line
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
230module.exports = RebateDecorator;