feed-common 1.10.5 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,180 +1,172 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
2
  import {
2
- OptionType,
3
- OptionTypeItem,
4
- OptionTypeGroup,
5
- ProductUploadProfile,
6
- ProductUploadMapping,
7
- ProductUploadRuleFilterType,
8
- RuleOperatorsType,
9
- ProductUploadRuleItem,
10
- ProductUploadRuleSection,
11
- ProductUploadRules,
3
+ OptionType,
4
+ OptionTypeItem,
5
+ OptionTypeGroup,
6
+ ProductUploadProfile,
7
+ ProductUploadMapping,
8
+ ProductUploadRuleFilterType,
9
+ RuleOperatorsType,
10
+ ProductUploadRuleItem,
11
+ ProductUploadRuleSection,
12
+ ProductUploadRules,
12
13
  } from '../types/profile.types.js';
13
14
  import { ProductUploadMappings } from '../constants/profile.constants.js';
14
15
  import { code } from './utils.js';
15
16
 
16
- export function checkRuleDuplication (rules: ProductUploadRuleItem[]): string[] {
17
- return rules
18
- .map((r) => {
19
- for (const rule of rules) {
20
- if (
21
- r.id !== rule.id &&
22
- r.attribute === rule.attribute &&
23
- r.operator === rule.operator &&
24
- r.value === rule.value
25
- ) {
26
- return r.id;
27
- }
28
- }
29
- return null;
30
- })
31
- .filter(Boolean) as string[];
17
+ /**
18
+ * Check for rules duplication
19
+ * @param rules
20
+ * @returns
21
+ */
22
+ export function checkRuleDuplication(rules: ProductUploadRuleItem[]): string[] {
23
+ return rules
24
+ .map(r => {
25
+ for (const rule of rules) {
26
+ if (
27
+ r.id !== rule.id &&
28
+ r.attribute === rule.attribute &&
29
+ r.operator === rule.operator &&
30
+ r.value === rule.value
31
+ ) {
32
+ return r.id;
33
+ }
34
+ }
35
+ return null;
36
+ })
37
+ .filter(Boolean) as string[];
32
38
  }
33
39
 
34
- export function checkRuleAlwaysness (rules: ProductUploadRuleItem[]): string[] {
35
- return rules
36
- .map((r) => {
37
- for (const rule of rules) {
38
- if (isExlusive(r, rule)) {
39
- return r.id;
40
- }
41
- }
42
- return null;
43
- })
44
- .filter(Boolean) as string[];
40
+ /**
41
+ * Check if rules mutually exclusive
42
+ * @param rules
43
+ * @returns
44
+ */
45
+ export function checkRuleAlwaysness(rules: ProductUploadRuleItem[]): string[] {
46
+ return rules
47
+ .map(r => {
48
+ for (const rule of rules) {
49
+ if (isExclusive(r, rule)) {
50
+ return r.id;
51
+ }
52
+ }
53
+ return null;
54
+ })
55
+ .filter(Boolean) as string[];
45
56
  }
46
57
 
47
- export function checkRuleDeadlocks (
48
- sections: ProductUploadRuleSection[]
49
- ): string[] {
50
- let totalList = [] as string[];
51
-
52
- for (let i = 0; i < sections.length; i++) {
53
- const currentSection = sections[i];
54
- const currentList = [] as string[];
55
-
56
- for (let y = 0; y < sections.length; y++) {
57
- if (i === y) {
58
- continue;
59
- }
60
-
61
- from_start: for (const ruleA of currentSection.ruleItems) {
62
- for (const ruleB of sections[y].ruleItems) {
63
- if (isExlusive(ruleA, ruleB)) {
64
- currentList.push(ruleA.id);
65
- continue from_start;
66
- }
58
+ /**
59
+ * Checks for contradictions in rules, e.g A == 1 and A != 1
60
+ * @param sections
61
+ * @returns
62
+ */
63
+ export function checkRuleDeadlocks(input: ProductUploadRuleSection[]): string[] {
64
+ const totalList = [] as string[][];
65
+ const sections = structuredClone(input);
66
+
67
+ for (let i = 0; i < sections.length - 1; i++) {
68
+ const currentSection = sections[i];
69
+
70
+ for (let y = i + 1; y < sections.length; y++) {
71
+ from_start: for (let x = 0; x < currentSection.ruleItems.length; x++) {
72
+ const ruleA = currentSection.ruleItems[x];
73
+ for (let z = 0; z < sections[y].ruleItems.length; z++) {
74
+ const ruleB = sections[y].ruleItems[z];
75
+ if (isExclusive(ruleA, ruleB)) {
76
+ totalList.push([ruleA.id, ruleB.id]);
77
+ currentSection.ruleItems.splice(x, 1);
78
+ sections[y].ruleItems.splice(z, 1);
79
+ continue from_start;
80
+ }
81
+ }
82
+ }
67
83
  }
68
- }
69
84
  }
70
85
 
71
- if (currentList.length === currentSection.ruleItems.length) {
72
- totalList = [...totalList, ...currentList];
73
- }
74
- }
75
-
76
- return totalList;
86
+ return sections.some(s => s.ruleItems.length === 0) ? totalList.flat() : [];
77
87
  }
78
88
 
79
- function isExlusive (
80
- rule1: ProductUploadRuleItem,
81
- rule2: ProductUploadRuleItem
82
- ): boolean {
83
- return (
84
- rule1.id !== rule2.id &&
85
- rule1.attribute === rule2.attribute &&
86
- rule1.operator ===
87
- getOperatorCounterpart(rule2.operator as keyof RuleOperatorsType) &&
88
- rule1.value === rule2.value
89
- );
89
+ function isExclusive(rule1: ProductUploadRuleItem, rule2: ProductUploadRuleItem): boolean {
90
+ return (
91
+ rule1.id !== rule2.id &&
92
+ rule1.attribute === rule2.attribute &&
93
+ rule1.operator === getOperatorCounterpart(rule2.operator as keyof RuleOperatorsType) &&
94
+ rule1.value === rule2.value
95
+ );
90
96
  }
91
97
 
92
- function getOperatorCounterpart (
93
- operator: keyof RuleOperatorsType
94
- ): keyof RuleOperatorsType {
95
- switch (operator) {
96
- case 'equals':
97
- return 'notEquals';
98
- case 'notEquals':
99
- return 'equals';
100
- case 'contains':
101
- return 'notContains';
102
- case 'notContains':
103
- return 'contains';
104
- case 'startsWith':
105
- return 'notStartsWith';
106
- case 'notStartsWith':
107
- return 'startsWith';
108
- case 'endsWith':
109
- return 'notEndsWith';
110
- case 'notEndsWith':
111
- return 'endsWith';
112
- case 'in':
113
- return 'notIn';
114
- case 'notIn':
115
- return 'in';
116
- case 'greater':
117
- return 'less';
118
- case 'less':
119
- return 'greater';
120
- case 'greaterOrEqual':
121
- return 'lessOrEqual';
122
- case 'lessOrEqual':
123
- return 'greaterOrEqual';
124
- default:
125
- throw new Error(`Unknown operator ${String(operator)}`);
126
- }
98
+ export function getOperatorCounterpart(operator: keyof RuleOperatorsType): keyof RuleOperatorsType {
99
+ switch (operator) {
100
+ case 'equals':
101
+ return 'notEquals';
102
+ case 'notEquals':
103
+ return 'equals';
104
+ case 'contains':
105
+ return 'notContains';
106
+ case 'notContains':
107
+ return 'contains';
108
+ case 'startsWith':
109
+ return 'notStartsWith';
110
+ case 'notStartsWith':
111
+ return 'startsWith';
112
+ case 'endsWith':
113
+ return 'notEndsWith';
114
+ case 'notEndsWith':
115
+ return 'endsWith';
116
+ case 'in':
117
+ return 'notIn';
118
+ case 'notIn':
119
+ return 'in';
120
+ case 'greater':
121
+ return 'less';
122
+ case 'less':
123
+ return 'greater';
124
+ case 'greaterOrEqual':
125
+ return 'lessOrEqual';
126
+ case 'lessOrEqual':
127
+ return 'greaterOrEqual';
128
+ default:
129
+ throw new Error(`Unknown operator ${String(operator)}`);
130
+ }
127
131
  }
128
132
 
129
- export function getEmptyRuleItem (
130
- attribute: ProductUploadRuleFilterType['value'] = ''
131
- ): ProductUploadRuleItem {
132
- return { attribute, operator: '', value: '', id: code() };
133
+ export function getEmptyRuleItem(attribute: ProductUploadRuleFilterType['value'] = ''): ProductUploadRuleItem {
134
+ return { attribute, operator: '', value: '', id: code() };
133
135
  }
134
136
 
135
- export function sortMappings (
136
- a: ProductUploadMapping,
137
- b: ProductUploadMapping
138
- ): number {
139
- if (!a.attribute) {
140
- return 1;
141
- }
142
-
143
- if (!b.attribute) {
144
- return -1;
145
- }
146
-
147
- const sourceA = ProductUploadMappings.find(
148
- (m) => m.attribute === a.attribute
149
- );
150
- const sourceB = ProductUploadMappings.find(
151
- (m) => m.attribute === b.attribute
152
- );
153
-
154
- if (sourceA?.required && !sourceB?.required) {
155
- return -1;
156
- }
157
-
158
- if (sourceB?.required && !sourceA?.required) {
159
- return 1;
160
- }
161
-
162
- if (a.attribute < b.attribute) {
163
- return -1;
164
- }
165
-
166
- if (a.attribute > b.attribute) {
167
- return 1;
168
- }
169
-
170
- return 0;
137
+ export function sortMappings(a: ProductUploadMapping, b: ProductUploadMapping): number {
138
+ if (!a.attribute) {
139
+ return 1;
140
+ }
141
+
142
+ if (!b.attribute) {
143
+ return -1;
144
+ }
145
+
146
+ const sourceA = ProductUploadMappings.find(m => m.attribute === a.attribute);
147
+ const sourceB = ProductUploadMappings.find(m => m.attribute === b.attribute);
148
+
149
+ if (sourceA?.required && !sourceB?.required) {
150
+ return -1;
151
+ }
152
+
153
+ if (sourceB?.required && !sourceA?.required) {
154
+ return 1;
155
+ }
156
+
157
+ if (a.attribute < b.attribute) {
158
+ return -1;
159
+ }
160
+
161
+ if (a.attribute > b.attribute) {
162
+ return 1;
163
+ }
164
+
165
+ return 0;
171
166
  }
172
167
 
173
- export function hasRules (rules: ProductUploadRules): boolean {
174
- return (
175
- Array.isArray(rules?.sections) &&
176
- rules.sections.some((section) => section.ruleItems.length > 0)
177
- );
168
+ export function hasRules(rules: ProductUploadRules): boolean {
169
+ return Array.isArray(rules?.sections) && rules.sections.some(section => section.ruleItems.length > 0);
178
170
  }
179
171
 
180
172
  /**
@@ -183,170 +175,148 @@ export function hasRules (rules: ProductUploadRules): boolean {
183
175
  * @param b
184
176
  * @returns
185
177
  */
186
- export function compareRules (
187
- a: ProductUploadRules = { sections: [] },
188
- b: ProductUploadRules = { sections: [] }
178
+ export function compareRules(
179
+ a: ProductUploadRules = { sections: [] },
180
+ b: ProductUploadRules = { sections: [] }
189
181
  ): boolean {
190
- const matchedSections = [] as number[];
191
- const sameRule = (
192
- rA: ProductUploadRuleItem,
193
- rB: ProductUploadRuleItem
194
- ): boolean => {
195
- return (
196
- rA.attribute === rB.attribute &&
197
- rA.operator === rB.operator &&
198
- compareValues(rA.value, rB.value)
199
- );
200
- };
182
+ const matchedSections = [] as number[];
183
+ const sameRule = (rA: ProductUploadRuleItem, rB: ProductUploadRuleItem): boolean => {
184
+ return rA.attribute === rB.attribute && rA.operator === rB.operator && compareValues(rA.value, rB.value);
185
+ };
201
186
 
202
- a.sections = a.sections.filter((s) => s.ruleItems.length > 0);
203
- b.sections = b.sections.filter((s) => s.ruleItems.length > 0);
187
+ a.sections = a.sections.filter(s => s.ruleItems.length > 0);
188
+ b.sections = b.sections.filter(s => s.ruleItems.length > 0);
204
189
 
205
- if (a.sections.length !== b.sections.length) {
206
- return false;
207
- }
190
+ if (a.sections.length !== b.sections.length) {
191
+ return false;
192
+ }
208
193
 
209
- for (let i = 0; i < a.sections.length; i++) {
210
- const rulesA = a.sections[i].ruleItems;
194
+ for (let i = 0; i < a.sections.length; i++) {
195
+ const rulesA = a.sections[i].ruleItems;
211
196
 
212
- for (let j = 0; j < b.sections.length; j++) {
213
- if (matchedSections.includes(j)) {
214
- continue;
215
- }
197
+ for (let j = 0; j < b.sections.length; j++) {
198
+ if (matchedSections.includes(j)) {
199
+ continue;
200
+ }
216
201
 
217
- const rulesB = b.sections[j].ruleItems;
202
+ const rulesB = b.sections[j].ruleItems;
218
203
 
219
- if (rulesA.length !== rulesB.length) {
220
- continue;
221
- }
204
+ if (rulesA.length !== rulesB.length) {
205
+ continue;
206
+ }
222
207
 
223
- if (
224
- rulesA.every((ruleA) => rulesB.some((ruleB) => sameRule(ruleA, ruleB)))
225
- ) {
226
- matchedSections.push(j);
227
- break;
228
- }
208
+ if (rulesA.every(ruleA => rulesB.some(ruleB => sameRule(ruleA, ruleB)))) {
209
+ matchedSections.push(j);
210
+ break;
211
+ }
212
+ }
229
213
  }
230
- }
231
214
 
232
- return matchedSections.length === a.sections.length;
215
+ return matchedSections.length === a.sections.length;
233
216
  }
234
217
 
235
- function compareValues (a: any, b: any): boolean {
236
- if (Array.isArray(a) && Array.isArray(b)) {
237
- return a.length === b.length && a.every((i) => b.includes(i));
238
- }
218
+ function compareValues(a: any, b: any): boolean {
219
+ if (Array.isArray(a) && Array.isArray(b)) {
220
+ return a.length === b.length && a.every(i => b.includes(i));
221
+ }
239
222
 
240
- return a === b;
223
+ return a === b;
241
224
  }
242
225
 
243
- export function compareMappings (
244
- a: ProductUploadMapping[],
245
- b: ProductUploadMapping[]
246
- ): boolean {
247
- const matchedMappings = [] as number[];
248
-
249
- if (a.length !== b.length) {
250
- return false;
251
- }
252
-
253
- for (let i = 0; i < a.length; i++) {
254
- const mappingA = a[i];
255
-
256
- for (let j = 0; j < b.length; j++) {
257
- if (matchedMappings.includes(j)) {
258
- continue;
259
- }
260
-
261
- const mappingB = b[j];
262
-
263
- if (
264
- mappingA.attribute === mappingB.attribute &&
265
- compareValues(mappingA.value, mappingB.value) &&
266
- compareRules(mappingA.rules, mappingB.rules)
267
- ) {
268
- matchedMappings.push(j);
269
- break;
270
- }
226
+ export function compareMappings(a: ProductUploadMapping[], b: ProductUploadMapping[]): boolean {
227
+ const matchedMappings = [] as number[];
228
+
229
+ if (a.length !== b.length) {
230
+ return false;
231
+ }
232
+
233
+ for (let i = 0; i < a.length; i++) {
234
+ const mappingA = a[i];
235
+
236
+ for (let j = 0; j < b.length; j++) {
237
+ if (matchedMappings.includes(j)) {
238
+ continue;
239
+ }
240
+
241
+ const mappingB = b[j];
242
+
243
+ if (
244
+ mappingA.attribute === mappingB.attribute &&
245
+ compareValues(mappingA.value, mappingB.value) &&
246
+ compareRules(mappingA.rules, mappingB.rules)
247
+ ) {
248
+ matchedMappings.push(j);
249
+ break;
250
+ }
251
+ }
271
252
  }
272
- }
273
253
 
274
- return matchedMappings.length === a.length;
254
+ return matchedMappings.length === a.length;
275
255
  }
276
- export function optionIsString (option: OptionType): option is string {
277
- return typeof option === 'string';
256
+
257
+ export function optionIsString(option: OptionType): option is string {
258
+ return typeof option === 'string';
278
259
  }
279
260
 
280
- export function optionIsItem (option: OptionType): option is OptionTypeItem {
281
- return typeof option !== 'string' && 'value' in option;
261
+ export function optionIsItem(option: OptionType): option is OptionTypeItem {
262
+ return typeof option !== 'string' && 'value' in option;
282
263
  }
283
264
 
284
- export function optionIsGroup (option: OptionType): option is OptionTypeGroup {
285
- return typeof option !== 'string' && 'options' in option;
265
+ export function optionIsGroup(option: OptionType): option is OptionTypeGroup {
266
+ return typeof option !== 'string' && 'options' in option;
286
267
  }
287
- export function detectProfileChange (
288
- a: ProductUploadProfile,
289
- b: ProductUploadProfile
268
+
269
+ export function detectProfileChange(
270
+ a: ProductUploadProfile,
271
+ b: ProductUploadProfile
290
272
  ): { name: boolean; rules: boolean; mappings: boolean; id: boolean } {
291
- const changes = {
292
- name: false,
293
- rules: false,
294
- mappings: false,
295
- id: false,
296
- };
297
-
298
- if (a.name !== b.name) {
299
- changes.name = true;
300
- }
301
-
302
- if (a.mappings !== b.mappings) {
303
- const mappingsA = a.mappings?.filter(
304
- (m) =>
305
- m.attribute !== 'targetCountry' && m.attribute !== 'contentLanguage'
306
- );
307
- const mappingsB = b.mappings?.filter(
308
- (m) =>
309
- m.attribute !== 'targetCountry' && m.attribute !== 'contentLanguage'
310
- );
311
- changes.mappings = !compareMappings(mappingsA, mappingsB);
312
- }
273
+ const changes = {
274
+ name: false,
275
+ rules: false,
276
+ mappings: false,
277
+ id: false,
278
+ };
279
+
280
+ if (a.name !== b.name) {
281
+ changes.name = true;
282
+ }
313
283
 
314
- if (
315
- ['targetCountry', 'contentLanguage'].some((key) => {
316
- const mappingA = a.mappings?.filter((m) => m.attribute === key) ?? [];
317
- const mappingB = b.mappings?.filter((m) => m.attribute === key) ?? [];
284
+ if (
285
+ ProductUploadMappings.filter(m => m.required)
286
+ .map(m => m.attribute)
287
+ .some(key => {
288
+ const mappingA = a.mappings?.filter(m => m.attribute === key) ?? [];
289
+ const mappingB = b.mappings?.filter(m => m.attribute === key) ?? [];
290
+ return !compareMappings(mappingA, mappingB);
291
+ })
292
+ ) {
293
+ changes.id = true;
294
+ }
318
295
 
319
- return !compareMappings(mappingA, mappingB);
320
- })
321
- ) {
322
- changes.id = true;
323
- }
296
+ const requiredAttrs = ProductUploadMappings.filter(m => m.required).map(m => m.attribute);
297
+ const mappingsA = a.mappings?.filter(m => !requiredAttrs.includes(m.attribute));
298
+ const mappingsB = b.mappings?.filter(m => !requiredAttrs.includes(m.attribute));
299
+ changes.mappings = !compareMappings(mappingsA, mappingsB);
324
300
 
325
- if (!compareRules(a.rules, b.rules)) {
326
- changes.rules = true;
327
- }
301
+ if (!compareRules(a.rules, b.rules)) {
302
+ changes.rules = true;
303
+ }
328
304
 
329
- return changes;
305
+ return changes;
330
306
  }
331
307
 
332
- export function sanitizeUploadProfile (
333
- profile: ProductUploadProfile
334
- ): ProductUploadProfile {
335
- if (profile.rules?.sections) {
336
- profile.rules.sections = profile.rules.sections.filter(
337
- (section) => section.ruleItems.length > 0
338
- );
339
- }
340
-
341
- profile.mappings = profile.mappings?.map((mapping) => {
342
- if (mapping.rules?.sections) {
343
- mapping.rules.sections = mapping.rules.sections.filter(
344
- (section) => section.ruleItems.length > 0
345
- );
308
+ export function sanitizeUploadProfile(profile: ProductUploadProfile): ProductUploadProfile {
309
+ if (profile.rules?.sections) {
310
+ profile.rules.sections = profile.rules.sections.filter(section => section.ruleItems.length > 0);
346
311
  }
347
312
 
348
- return mapping;
349
- });
313
+ profile.mappings = profile.mappings?.map(mapping => {
314
+ if (mapping.rules?.sections) {
315
+ mapping.rules.sections = mapping.rules.sections.filter(section => section.ruleItems.length > 0);
316
+ }
317
+
318
+ return mapping;
319
+ });
350
320
 
351
- return profile;
321
+ return profile;
352
322
  }
@@ -0,0 +1,49 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import {
3
+ extractSubscriptionCode,
4
+ makeSubscriptionCode,
5
+ makeSubscriptionName,
6
+ parseSubscriptionName,
7
+ } from '../src/utils/company.js';
8
+ import { ShopifyRecurringCharge } from '../src/types/shopify.types.js';
9
+
10
+ describe('Company', () => {
11
+ test('Make subscription code', () => {
12
+ expect(
13
+ makeSubscriptionCode({
14
+ code: 'G',
15
+ level: 20,
16
+ products: 100,
17
+ profiles: 100,
18
+ price: 100,
19
+ })
20
+ ).toBe('G20');
21
+ });
22
+
23
+ test('Make subscription name', () => {
24
+ expect(
25
+ makeSubscriptionName({
26
+ code: 'G',
27
+ level: 10,
28
+ products: 100,
29
+ profiles: 100,
30
+ price: 100,
31
+ })
32
+ ).toBe('G10');
33
+ });
34
+
35
+ test('Extract subscription code', () => {
36
+ expect(
37
+ extractSubscriptionCode({
38
+ name: 'G50 subscription',
39
+ price: 100,
40
+ status: 'active',
41
+ trial_ends_at: '2022-01-01',
42
+ } as unknown as ShopifyRecurringCharge)
43
+ ).toBe('G50');
44
+ });
45
+
46
+ test('Parse subscription name', () => {
47
+ expect(parseSubscriptionName('G50')).toEqual({ code: 'G', level: '50' });
48
+ });
49
+ });