@softwear/latestcollectioncore 1.0.203 → 1.0.205

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.
@@ -25,11 +25,89 @@ const GLOBAL_PATTERNS = [
25
25
  ['articleDescription', 'articleCode|articleCodeSupplier', 'colorCodeSupplier|colorSupplier'],
26
26
  ['colorCodeSupplier|colorSupplier', 'articleCode|articleCodeSupplier'],
27
27
  ['brand', 'collectionSupplier', 'colorCodeSupplier', 'articleCode|articleCodeSupplier'],
28
- ['brand', 'collectionSupplier', 'colorCodeSupplier', 'articleCode|articleCodeSupplier', 'buyPrice|valuePrice'],
29
28
  ['brand', 'articleDescription', 'colorSupplier'],
30
29
  ]
31
30
 
32
- function prepFilterSkusImages(filteredSkus: SkuI[], merge = true, fileNames: Array<string>) {
31
+ const FIELDS_TO_SKIP: Array<string> = ['__created', '__modified', 'images', 'brandhash', 'active', 'PRODUCT', 'colorFamily', 'price', 'sizeIndex', '_customsize', 'SELECTED']
32
+
33
+ const MERGE_GROUPS: Record<string, string> = {
34
+ 'id|barcode': 'id',
35
+ 'articleCode|articleCodeSupplier': 'articleCode',
36
+ 'colorCode|colorCodeSupplier|colorSupplier': 'color',
37
+ 'size|sizeSupplier|mainSize|subSize|subSizeSupplier|_customsize': 'size',
38
+ 'articleGroup|articleGroupSupplier': 'articleGroup',
39
+ 'collection|collectionSupplier': 'collection',
40
+ 'brand|brandSupplier': 'brand',
41
+ '_gender|genderSupplier': 'gender',
42
+ }
43
+
44
+ // Letters, digits, and apostrophe stay; everything else is a delimiter.
45
+ const DELIMITERS = /[^\p{L}\p{N}']+/gu
46
+
47
+ /** Shared stem: decode URI, then strip from the first known image extension onward. */
48
+ function filenameStem(fileName: string): string {
49
+ let s: string
50
+ try {
51
+ s = decodeURIComponent(fileName)
52
+ } catch {
53
+ s = fileName
54
+ }
55
+ s = s.toLowerCase()
56
+ const extIdx = s.search(/\.(?:jpe?g|png|gif)/i)
57
+ if (extIdx !== -1) s = s.slice(0, extIdx)
58
+ return s
59
+ }
60
+
61
+ function checkPattern(fieldVals: Array<string>, fileName: string) {
62
+
63
+ const fieldIndexes2: Record<string, Array<number>> = {}
64
+
65
+ const sortedFields = fieldVals.sort((a, b) => {
66
+ return b.length - a.length
67
+ })
68
+
69
+ // Return quickly if one of the fields in the pattern is not in the fileName
70
+ for (const fieldVal of sortedFields) {
71
+ const idx = fileName.indexOf(fieldVal)
72
+ if (idx == -1) {
73
+ return false
74
+ }
75
+ fieldIndexes2[fieldVal] = [idx, idx+fieldVal.length]
76
+ }
77
+
78
+ // Check if all fields have a specific place in file name, with no overlap
79
+ for (let i = 1; i < sortedFields.length; i++) {
80
+ const startIndex = fieldIndexes2[sortedFields[i]][0]
81
+ const endIndex = fieldIndexes2[sortedFields[i]][1]
82
+ for (let j = 0; j < i; j++) {
83
+ const startIndex2 = fieldIndexes2[sortedFields[j]][0]
84
+ const endIndex2 = fieldIndexes2[sortedFields[j]][1]
85
+ if (startIndex < endIndex2 && endIndex > startIndex2) {
86
+ const nextIndex = fileName.indexOf(sortedFields[i], endIndex2)
87
+ if (nextIndex == -1) {
88
+ return false
89
+ } else {
90
+ fieldIndexes2[sortedFields[i]] = [nextIndex, nextIndex+sortedFields[i].length]
91
+ break
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ return true
98
+ }
99
+
100
+ function normalizeSkuField(sku: SkuI, fieldName: string) {
101
+ let fieldVal = sku[fieldName]
102
+ if (fieldVal == null || fieldVal == '') return ''
103
+ if (typeof fieldVal == 'string') {
104
+ return fieldVal.replace(DELIMITERS, '').toLowerCase()
105
+ } else {
106
+ return fieldVal.toString()
107
+ }
108
+ }
109
+
110
+ function prepFilterSkusImagesAuto(filteredSkus: SkuI[], fileNames: Array<string>) {
33
111
  const skusCopy = deepCopy(filteredSkus)
34
112
  const preppedSkus: Array<Record<string, any>> = []
35
113
  const fileNames2skip: Array<string> = []
@@ -47,41 +125,92 @@ function prepFilterSkusImages(filteredSkus: SkuI[], merge = true, fileNames: Arr
47
125
 
48
126
  const fieldsToBeMerged = {}
49
127
  for (const fieldName in sku) {
50
- if (fieldName == '__created' || fieldName == '__modified' || fieldName == 'images' || fieldName == 'brandhash' || (merge == true && fieldName == 'colorFamily')) continue
51
- let fieldVal = sku[fieldName]
52
- if (fieldVal == '') continue
128
+ if (FIELDS_TO_SKIP.indexOf(fieldName) != -1 || fieldName.includes('Price')) continue
129
+ if (!(fieldName in sku) || sku[fieldName] == null || sku[fieldName] == undefined) continue
130
+ sku[fieldName] = normalizeSkuField(sku, fieldName)
53
131
 
54
- if (typeof fieldVal == 'string') {
55
- sku[fieldName] = fieldVal.replace(/[^A-Za-zÀ-ÖØ-öø-ÿ0-9]/g, '').toLowerCase()
132
+ const fieldVal = sku[fieldName]
133
+ if (!fieldVal) continue
134
+
135
+ if (fieldVal in fieldsToBeMerged) {
136
+ fieldsToBeMerged[fieldVal].push(fieldName)
56
137
  } else {
57
- sku[fieldName] = fieldVal.toString()
138
+ fieldsToBeMerged[fieldVal] = [fieldName]
58
139
  }
59
-
60
- if (merge) {
61
- fieldVal = sku[fieldName]
62
-
63
- if (fieldVal in fieldsToBeMerged) {
64
- fieldsToBeMerged[fieldVal].push(fieldName)
65
- } else {
66
- fieldsToBeMerged[fieldVal] = [fieldName]
140
+ }
141
+ const newSku = {}
142
+ newSku['originalSku'] = filteredSkus[idx]
143
+ for (const fieldVal in fieldsToBeMerged) {
144
+ const fieldNameArray = fieldsToBeMerged[fieldVal].sort()
145
+ if (fieldNameArray.length > 1) {
146
+ const groups = {}
147
+ for (const fieldName of fieldNameArray) {
148
+ let included = false
149
+ Object.keys(MERGE_GROUPS).forEach((group) => {
150
+ if (group.includes(fieldName)) {
151
+ included = true
152
+ if (!groups[MERGE_GROUPS[group]]) groups[MERGE_GROUPS[group]] = fieldName
153
+ else groups[MERGE_GROUPS[group]] = groups[MERGE_GROUPS[group]] + '|' + fieldName
154
+ }
155
+ })
156
+ if (!included) {
157
+ groups[fieldName] = fieldName
158
+ }
67
159
  }
160
+ Object.keys(groups).forEach((group) => {
161
+ newSku[groups[group]] = fieldVal
162
+ })
163
+ } else {
164
+ newSku[fieldNameArray[0]] = fieldVal
68
165
  }
69
166
  }
70
- if (!merge) {
167
+ preppedSkus.push(newSku)
168
+ })
169
+ const fileNames2Match = fileNames.filter((fileName) => {
170
+ return fileNames2skip.indexOf(fileName) == -1
171
+ })
172
+ return { preppedSkus: preppedSkus, fileNames2Match: fileNames2Match }
173
+ }
174
+
175
+ function prepFilterSkusImagesManual(filteredSkus: SkuI[], fileNames: Array<string>, pattern: Array<string>) {
176
+ const skusCopy = deepCopy(filteredSkus)
177
+ const preppedSkus: Array<Record<string, any>> = []
178
+ const fileNames2skip: Array<string> = []
179
+ skusCopy.forEach((sku, idx) => {
180
+ if (sku.images && sku.images.length > 0) {
181
+ // If a fileName is already matched, we skip it
182
+ fileNames.forEach((fileName) => {
183
+ if (sku.images?.includes(fileName) && fileNames2skip.indexOf(fileName) == -1) fileNames2skip.push(fileName)
184
+ })
185
+ // If a sku has images, we skip it
186
+ sku['skip'] = true
71
187
  preppedSkus.push(sku)
72
- } else {
73
- const newSku = {}
74
- newSku['originalSku'] = filteredSkus[idx]
75
- for (const fieldVal in fieldsToBeMerged) {
76
- const fieldNameArray = fieldsToBeMerged[fieldVal].sort()
77
- if (fieldNameArray.length > 1) {
78
- newSku[fieldNameArray.join('|')] = fieldVal
79
- } else {
80
- newSku[fieldNameArray[0]] = fieldVal
188
+ return
189
+ }
190
+
191
+ const foundValues: Array<string> = []
192
+ for (const fieldName of pattern) {
193
+ if (!(fieldName in sku) || sku[fieldName] == null || sku[fieldName] == undefined) {
194
+ sku['skip'] = true
195
+ break
196
+ }
197
+ sku[fieldName] = normalizeSkuField(sku, fieldName)
198
+ if (!sku[fieldName]) {
199
+ sku['skip'] = true
200
+ break
201
+ }
202
+ for (const value of foundValues) {
203
+ if (value.includes(sku[fieldName]) || sku[fieldName] == value) {
204
+ if (!sku['subStrings']) sku['subStrings'] = []
205
+ sku['subStrings'].push([value, sku[fieldName]])
206
+ } else if (sku[fieldName].includes(value)) {
207
+ if (!sku['subStrings']) sku['subStrings'] = []
208
+ sku['subStrings'].push([sku[fieldName], value])
81
209
  }
82
210
  }
83
- preppedSkus.push(newSku)
211
+ foundValues.push(sku[fieldName])
84
212
  }
213
+ preppedSkus.push(sku)
85
214
  })
86
215
  const fileNames2Match = fileNames.filter((fileName) => {
87
216
  return fileNames2skip.indexOf(fileName) == -1
@@ -90,19 +219,10 @@ function prepFilterSkusImages(filteredSkus: SkuI[], merge = true, fileNames: Arr
90
219
  }
91
220
 
92
221
  function cleanFilename(fileName: string) {
93
- return (
94
- fileName
95
- // ── 1. strip any query string (everything from the first “?” to the end)
96
- .replace(/\?.*$/, '')
97
- // ── 2. strip a terminal image extension (case‑insensitive) .jpeg, .jpg, .png or .gif
98
- .replace(/\.(?:jpe?g|png|gif)$/i, '')
99
- // ── 3. drop every character that is NOT a letter (Unicode!) or a digit
100
- .replace(/[^\p{L}\p{N}]+/gu, '')
101
- .toLowerCase()
102
- )
222
+ return filenameStem(fileName).replace(DELIMITERS, '')
103
223
  }
104
224
 
105
- function checkWithoutLast(fileName: string) {
225
+ function removeLastToken(fileName: string) {
106
226
  // If last token equals any of the following patterns, we will check the fileName without the last token
107
227
  // 1) includes front, back, side or sole
108
228
  // 2) is 1 alphanumerical character
@@ -112,11 +232,9 @@ function checkWithoutLast(fileName: string) {
112
232
  // 5) ends with a number
113
233
  // 6) ends with two numbers
114
234
  // If any of those checks are true, we return the fileName without the last token
115
- // Last token here is defined as the last token in the fileName, split by underscores, dashes, dots, spaces or URL encoded characters (like %20)
116
- const tokens = fileName
117
- .toLowerCase()
118
- .split(/[_\-.\s()]+|%[0-9A-Fa-f]{2}/)
119
- .slice(0, -1)
235
+ // Tokens come from the shared stem (decode + extension strip), split on anything that is
236
+ // not a letter, digit, or apostrophe — same rule as cleanFilename.
237
+ const tokens = filenameStem(fileName).split(DELIMITERS).filter(Boolean)
120
238
  const lastToken = tokens[tokens.length - 1]
121
239
  let returnWithoutLast = false
122
240
  if (lastToken) {
@@ -127,37 +245,22 @@ function checkWithoutLast(fileName: string) {
127
245
  const tokenLength = tokens.length
128
246
  if (returnWithoutLast) {
129
247
  let sliceIndex = -1
130
- if (tokenLength > 3 && tokens[tokenLength - 3] == 'press' && ((tokens[tokenLength - 2] == 'pack') || (tokens[tokenLength - 2] == 'model'))) sliceIndex = -3
248
+ if (tokenLength > 3 && tokens[tokenLength - 3] == 'press' && (tokens[tokenLength - 2] == 'pack' || tokens[tokenLength - 2] == 'model')) sliceIndex = -3
131
249
  else if (tokenLength > 2 && tokens[tokenLength - 2] == 'packshot') sliceIndex = -2
132
- return tokens
133
- .slice(0, sliceIndex)
134
- .join('')
135
- .replace(/[^\p{L}\p{N}]+/gu, '')
250
+ return tokens.slice(0, sliceIndex).join('')
136
251
  }
137
- if (tokenLength > 2 && tokens[tokenLength - 2] == 'press' && ((tokens[tokenLength - 1] == 'pack') || (tokens[tokenLength - 1] == 'model'))) {
138
- return tokens
139
- .slice(0, -2)
140
- .join('')
141
- .replace(/[^\p{L}\p{N}]+/gu, '')
252
+ if (tokenLength > 2 && tokens[tokenLength - 2] == 'press' && (tokens[tokenLength - 1] == 'pack' || tokens[tokenLength - 1] == 'model')) {
253
+ return tokens.slice(0, -2).join('')
142
254
  } else if (tokenLength > 1 && tokens[tokenLength - 1] == 'packshot') {
143
- return tokens
144
- .slice(0, -1)
145
- .join('')
146
- .replace(/[^\p{L}\p{N}]+/gu, '')
255
+ return tokens.slice(0, -1).join('')
147
256
  }
148
257
  if (lastToken.includes('product')) {
149
- const joinedCleanFileName = tokens.join('').replace(/[^\p{L}\p{N}]+/gu, '')
258
+ const joinedCleanFileName = tokens.join('')
150
259
  return joinedCleanFileName.slice(0, joinedCleanFileName.indexOf('product'))
151
260
  } else if (Number(lastToken.slice(-1))) {
152
- return tokens
153
- .join('')
154
- .replace(/[^\p{L}\p{N}]+/gu, '')
155
- .slice(0, -1)
261
+ return tokens.join('').slice(0, -1)
156
262
  } else if (Number(lastToken.slice(-2))) {
157
- return tokens
158
- .join('')
159
- .replace(/[^\p{L}\p{N}]+/gu, '')
160
- .substring(0, -2)
263
+ return tokens.join('').substring(0, -2)
161
264
  }
162
265
  }
163
266
  return ''
@@ -364,7 +467,6 @@ function fallbackSearch(cleanedFileName: string, fileNameWithoutLast: string, pr
364
467
  }
365
468
 
366
469
  function imageBinder(fileNames: Array<string>, filteredSkus: SkuI[], brand: string, specifiedPattern: Array<string>, test = false) {
367
- const totalLength = fileNames.length
368
470
  let count = 1
369
471
  const fileNameIndexMatch = {}
370
472
  let quickCount = 0
@@ -373,32 +475,96 @@ function imageBinder(fileNames: Array<string>, filteredSkus: SkuI[], brand: stri
373
475
  let newPatterns = {}
374
476
  if (specifiedPattern[0] != 'auto') {
375
477
  // If the user/administrator provides a pattern, we use that by default
376
- const prepObject = prepFilterSkusImages(filteredSkus, false, fileNames)
478
+ const prepObject = prepFilterSkusImagesManual(filteredSkus, fileNames, specifiedPattern)
377
479
  const preppedSkus = prepObject.preppedSkus
378
480
  const fileNames2Match = prepObject.fileNames2Match
379
481
  fileNames2Match.forEach((fileName) => {
380
482
  const cleanedFileName = cleanFilename(fileName)
381
- const fileNameWithoutLast = checkWithoutLast(fileName)
382
483
  fileNameIndexMatch[fileName] = []
484
+ const matchingCombinations = {}
485
+ // Check which skus match the specified pattern and store matching fields (combinations)
383
486
  preppedSkus.forEach((sku, idx) => {
384
487
  if (sku['skip']) return
385
488
  let skuStillMatches = true
489
+ let combinationArray: Array<string> = []
386
490
  specifiedPattern.every((fieldName) => {
387
- if (!(fieldName in sku)) skuStillMatches = false
388
- else {
389
- const fieldVal = sku[fieldName]
390
- if (!cleanedFileName.includes(fieldVal) && !fileNameWithoutLast.includes(fieldVal)) skuStillMatches = false
391
- }
491
+ const fieldVal = sku[fieldName]
492
+ if (!cleanedFileName.includes(fieldVal)) skuStillMatches = false
493
+ else combinationArray.push(fieldVal)
392
494
  return skuStillMatches
393
495
  })
394
496
  if (skuStillMatches) {
395
- fileNameIndexMatch[fileName].push(idx)
497
+ let combination = combinationArray.join('|')
498
+ if (combination in matchingCombinations) {
499
+ matchingCombinations[combination].push(idx)
500
+ } else {
501
+ matchingCombinations[combination] = [idx]
502
+ }
396
503
  }
397
504
  })
505
+ let matchingIndices: Array<number> = []
506
+ // If only one combination of specified pattern matches, we use it
507
+ if (Object.keys(matchingCombinations).length == 1) {
508
+ matchingIndices = matchingCombinations[Object.keys(matchingCombinations)[0]] }
509
+ else {
510
+ // In case of multiple combinations, do substring check
511
+ // -> Checks if all fields have a specific place in file name, with no overlap
512
+ const matchesAfterSubstringCheck = {}
513
+ Object.keys(matchingCombinations).forEach((combination) => {
514
+ const combinationArray = combination.split('|')
515
+ const isMatch = checkPattern(combinationArray, cleanedFileName)
516
+ if (isMatch) {
517
+ matchesAfterSubstringCheck[combination] = matchingCombinations[combination]
518
+ }
519
+ })
520
+ // If only one combination is left after substring check, we use it
521
+ if (Object.keys(matchesAfterSubstringCheck).length == 1) {
522
+ matchingIndices = matchesAfterSubstringCheck[Object.keys(matchesAfterSubstringCheck)[0]] }
523
+ else {
524
+ // In case there are still multiple combinations, we check longest combinations
525
+ // -> Longest combinations are more specific and therefore preferred
526
+ let lengthLongest = 0
527
+ let longestCombinations: Array<string> = []
528
+ Object.keys(matchesAfterSubstringCheck).forEach((combination) => {
529
+ if (combination.length > lengthLongest) {
530
+ lengthLongest = combination.length
531
+ longestCombinations = [combination]
532
+ } else if (combination.length == lengthLongest) {
533
+ longestCombinations.push(combination)
534
+ }
535
+ })
536
+ // If only one combination is left after longest combination check, we use it
537
+ if (longestCombinations.length == 1) {
538
+ matchingIndices = matchesAfterSubstringCheck[longestCombinations[0]] }
539
+ else {
540
+ // In case there are still multiple combinations, we check fileNameWithoutLast
541
+ // -> If any combination matches fileNameWithoutLast, we prefer it, as last tokens can be junk
542
+ const fileNameWithoutLast = removeLastToken(fileName)
543
+ const matchesWithoutLast: Record<string, number[]> = {}
544
+ if (fileNameWithoutLast) {
545
+ longestCombinations.forEach((combination) => {
546
+ const combinationArray = combination.split('|')
547
+ if (checkPattern(combinationArray, fileNameWithoutLast)) {
548
+ matchesWithoutLast[combination] = matchesAfterSubstringCheck[combination]
549
+ }
550
+ })
551
+ }
552
+ // If only one combination is left after fileNameWithoutLast check, we use it
553
+ if (Object.keys(matchesWithoutLast).length == 1) { matchingIndices = matchesWithoutLast[Object.keys(matchesWithoutLast)[0]] }
554
+ else {
555
+ Object.keys(matchesAfterSubstringCheck).forEach((combination) => {
556
+ matchingIndices.push(...matchesAfterSubstringCheck[combination])
557
+ })
558
+ }
559
+ }
560
+ }
561
+ }
562
+ if (matchingIndices.length > 0) fileNameIndexMatch[fileName] = matchingIndices
563
+ if (!fileNameIndexMatch[fileName]) fileNameIndexMatch[fileName] = []
398
564
  })
399
565
  } else {
400
566
  // Else, we look for patterns ourselves using a couple of methods, prioritized on efficiency
401
- const prepObject = prepFilterSkusImages(filteredSkus, true, fileNames)
567
+ const prepObject = prepFilterSkusImagesAuto(filteredSkus, fileNames)
402
568
  const preppedSkus = prepObject.preppedSkus
403
569
  const fileNames2Match = prepObject.fileNames2Match
404
570
  let quickSearch = prepQuickSearch(preppedSkus)
@@ -406,7 +572,7 @@ function imageBinder(fileNames: Array<string>, filteredSkus: SkuI[], brand: stri
406
572
  fileNames2Match.forEach((fileName) => {
407
573
  let matchFound = false
408
574
  const cleanedFileName = cleanFilename(fileName)
409
- const fileNameWithoutLast = checkWithoutLast(fileName)
575
+ const fileNameWithoutLast = removeLastToken(fileName)
410
576
  let indicesForFileName: Array<number> = []
411
577
  // Method #1: try quicksearch, an object with patterns as keys and arrays of [proposedFilename, sku] as values
412
578
  // based on existing patterns in GLOBAL_PATTERNS
@@ -469,7 +635,7 @@ function onlySaveIDFromSkus(matchDict: {}): Record<string, number[]> {
469
635
  return out
470
636
  }
471
637
 
472
- async function testBatch(brand: string) {
638
+ async function testBatch(brand: string, specifiedPattern: Array<string> = ['auto']) {
473
639
  const [skusModule, imagesModule, matchedModule] = await Promise.all([
474
640
  import(`../test/imageBinderTestData/brandSkus/${brand}.json`),
475
641
  import(`../test/imageBinderTestData/brandImages/${brand}.json`),
@@ -482,7 +648,7 @@ async function testBatch(brand: string) {
482
648
 
483
649
  // 1) Call imageBinder
484
650
  const t0 = Date.now()
485
- const imageBinderResult = imageBinder(fileNames, filteredSkus, brand, ['auto'], true)
651
+ const imageBinderResult = imageBinder(fileNames, filteredSkus, brand, specifiedPattern, true)
486
652
  const t1 = Date.now()
487
653
  const matchIndices = imageBinderResult.match || {}
488
654
 
package/src/types.ts CHANGED
@@ -130,7 +130,6 @@ interface SkuI {
130
130
  collection?: string
131
131
  collectionSupplier?: string
132
132
  active?: boolean
133
- skuActive?: boolean
134
133
  warehouseLocation?: string
135
134
  tagType?: tagTypeE
136
135
  directDelivery?: boolean
@@ -25,7 +25,6 @@
25
25
  "source": "latestCollection",
26
26
  "mainColorCode": "",
27
27
  "mainColorDescription": "",
28
- "skuActive": true,
29
28
  "valuePrice": 37.66,
30
29
  "supplierName": "",
31
30
  "sizeSupplier": "75C",
@@ -63,7 +62,6 @@
63
62
  "source": "latestCollection",
64
63
  "mainColorCode": "",
65
64
  "mainColorDescription": "",
66
- "skuActive": true,
67
65
  "valuePrice": 37.66,
68
66
  "supplierName": "",
69
67
  "sizeSupplier": "80C",
@@ -101,7 +99,6 @@
101
99
  "source": "latestCollection",
102
100
  "mainColorCode": "",
103
101
  "mainColorDescription": "",
104
- "skuActive": true,
105
102
  "valuePrice": 37.66,
106
103
  "supplierName": "",
107
104
  "sizeSupplier": "85C",
@@ -139,7 +136,6 @@
139
136
  "source": "latestCollection",
140
137
  "mainColorCode": "",
141
138
  "mainColorDescription": "",
142
- "skuActive": true,
143
139
  "valuePrice": 37.66,
144
140
  "supplierName": "",
145
141
  "sizeSupplier": "90C",
@@ -177,7 +173,6 @@
177
173
  "source": "latestCollection",
178
174
  "mainColorCode": "",
179
175
  "mainColorDescription": "",
180
- "skuActive": true,
181
176
  "valuePrice": 37.66,
182
177
  "supplierName": "",
183
178
  "sizeSupplier": "95C",
@@ -215,7 +210,6 @@
215
210
  "source": "latestCollection",
216
211
  "mainColorCode": "",
217
212
  "mainColorDescription": "",
218
- "skuActive": true,
219
213
  "valuePrice": 37.66,
220
214
  "supplierName": "",
221
215
  "sizeSupplier": "100C",
@@ -253,7 +247,6 @@
253
247
  "source": "latestCollection",
254
248
  "mainColorCode": "",
255
249
  "mainColorDescription": "",
256
- "skuActive": true,
257
250
  "valuePrice": 37.66,
258
251
  "supplierName": "",
259
252
  "sizeSupplier": "105C",
@@ -291,7 +284,6 @@
291
284
  "source": "latestCollection",
292
285
  "mainColorCode": "",
293
286
  "mainColorDescription": "",
294
- "skuActive": true,
295
287
  "valuePrice": 37.66,
296
288
  "supplierName": "",
297
289
  "sizeSupplier": "75C",
@@ -329,7 +321,6 @@
329
321
  "source": "latestCollection",
330
322
  "mainColorCode": "",
331
323
  "mainColorDescription": "",
332
- "skuActive": true,
333
324
  "valuePrice": 37.66,
334
325
  "supplierName": "",
335
326
  "sizeSupplier": "80C",
@@ -367,7 +358,6 @@
367
358
  "source": "latestCollection",
368
359
  "mainColorCode": "",
369
360
  "mainColorDescription": "",
370
- "skuActive": true,
371
361
  "valuePrice": 37.66,
372
362
  "supplierName": "",
373
363
  "sizeSupplier": "85C",
@@ -405,7 +395,6 @@
405
395
  "source": "latestCollection",
406
396
  "mainColorCode": "",
407
397
  "mainColorDescription": "",
408
- "skuActive": true,
409
398
  "valuePrice": 37.66,
410
399
  "supplierName": "",
411
400
  "sizeSupplier": "90C",
@@ -443,7 +432,6 @@
443
432
  "source": "latestCollection",
444
433
  "mainColorCode": "",
445
434
  "mainColorDescription": "",
446
- "skuActive": true,
447
435
  "valuePrice": 37.66,
448
436
  "supplierName": "",
449
437
  "sizeSupplier": "95C",
@@ -481,7 +469,6 @@
481
469
  "source": "latestCollection",
482
470
  "mainColorCode": "",
483
471
  "mainColorDescription": "",
484
- "skuActive": true,
485
472
  "valuePrice": 18.81,
486
473
  "supplierName": "",
487
474
  "sizeSupplier": "40",
@@ -518,7 +505,6 @@
518
505
  "source": "latestCollection",
519
506
  "mainColorCode": "",
520
507
  "mainColorDescription": "",
521
- "skuActive": true,
522
508
  "valuePrice": 18.81,
523
509
  "supplierName": "",
524
510
  "sizeSupplier": "42",
@@ -555,7 +541,6 @@
555
541
  "source": "latestCollection",
556
542
  "mainColorCode": "",
557
543
  "mainColorDescription": "",
558
- "skuActive": true,
559
544
  "valuePrice": 18.81,
560
545
  "supplierName": "",
561
546
  "sizeSupplier": "44",
@@ -592,7 +577,6 @@
592
577
  "source": "latestCollection",
593
578
  "mainColorCode": "",
594
579
  "mainColorDescription": "",
595
- "skuActive": true,
596
580
  "valuePrice": 18.81,
597
581
  "supplierName": "",
598
582
  "sizeSupplier": "46",
@@ -629,7 +613,6 @@
629
613
  "source": "latestCollection",
630
614
  "mainColorCode": "",
631
615
  "mainColorDescription": "",
632
- "skuActive": true,
633
616
  "valuePrice": 18.81,
634
617
  "supplierName": "",
635
618
  "sizeSupplier": "48",
@@ -666,7 +649,6 @@
666
649
  "source": "latestCollection",
667
650
  "mainColorCode": "",
668
651
  "mainColorDescription": "",
669
- "skuActive": true,
670
652
  "valuePrice": 18.81,
671
653
  "supplierName": "",
672
654
  "sizeSupplier": "50",
@@ -703,7 +685,6 @@
703
685
  "source": "latestCollection",
704
686
  "mainColorCode": "",
705
687
  "mainColorDescription": "",
706
- "skuActive": true,
707
688
  "valuePrice": 18.81,
708
689
  "supplierName": "",
709
690
  "sizeSupplier": "52",
@@ -15,12 +15,12 @@ describe('imageBinder function', function () {
15
15
  expect(result.Time).toBeLessThan(10)
16
16
  }, 300000)
17
17
  it('should deal with large batches', async function () {
18
- // Tests on 1594 images and 18196 skus, 1593 are quick search
18
+ // Tests on 1594 images and 18196 skus; most are quick search
19
19
  const result = await testBatch('lowa')
20
20
  expect(Object.keys(result).length).toBe(8)
21
21
  expect(result.Mismatches).toBe(0)
22
- expect(result.QuickSearch).toBe(1587)
23
- expect(result.Patterns['articleCode|articleCodeSupplier-colorCodeSupplier|colorSupplier']).toBe(1586)
22
+ expect(result.QuickSearch).toBe(1588)
23
+ expect(result.Patterns['articleCode|articleCodeSupplier-colorCodeSupplier|colorSupplier']).toBe(1588)
24
24
  // Time can vary, so we use a loose check (within 5 seconds)
25
25
  expect(result.Time).toBeGreaterThan(0)
26
26
  expect(result.Time).toBeLessThan(5)
@@ -48,4 +48,53 @@ describe('imageBinder function', function () {
48
48
  expect(result.Time).toBeGreaterThan(0)
49
49
  expect(result.Time).toBeLessThan(5)
50
50
  })
51
+ it('should prefer the longest matching combination for specified patterns (lingadore 8501)', async function () {
52
+ // When articleCodeSupplier values share prefixes (8501 vs 8501B vs 8501BD),
53
+ // images must match only the most specific article code.
54
+ const result = await testBatch('lingadore', ['articleCodeSupplier', 'colorCodeSupplier'])
55
+ expect(Object.keys(result).length).toBe(8)
56
+ expect(result.Brand).toBe('lingadore')
57
+ expect(result.Mismatches).toBe(0)
58
+ expect(result.QuickSearch).toBe(0)
59
+ expect(result.FallbackSearch).toBe(0)
60
+ expect(result.Time).toBeGreaterThan(0)
61
+ expect(result.Time).toBeLessThan(5)
62
+ })
63
+ it('should not match if substrings do not appear in separate positions (pennAndInk)', async function () {
64
+ // e.g. article W26B422 with colors 15/90 must not also match color 42 just because "42"
65
+ // appears inside the article code.
66
+ const result = await testBatch('pennAndInk', ['articleCodeSupplier', 'colorCodeSupplier'])
67
+ expect(Object.keys(result).length).toBe(8)
68
+ expect(result.Brand).toBe('pennAndInk')
69
+ expect(result.Mismatches).toBe(0)
70
+ expect(result.QuickSearch).toBe(0)
71
+ expect(result.FallbackSearch).toBe(0)
72
+ expect(result.Time).toBeGreaterThan(0)
73
+ expect(result.Time).toBeLessThan(5)
74
+ })
75
+ it('should allow adjacent article and colour spans in specified patterns (lowa)', async function () {
76
+ // When article and colour abut in the cleaned filename (e.g. LA830581 + black-grey),
77
+ // checkPattern must treat touching spans as non-overlapping so the real colour wins
78
+ // over shorter substring colours (black).
79
+ const result = await testBatch('lowa', ['articleCodeSupplier', 'colorCodeSupplier'])
80
+ expect(Object.keys(result).length).toBe(8)
81
+ expect(result.Brand).toBe('lowa')
82
+ expect(result.Mismatches).toBe(0)
83
+ expect(result.QuickSearch).toBe(0)
84
+ expect(result.FallbackSearch).toBe(0)
85
+ expect(result.Time).toBeGreaterThan(0)
86
+ expect(result.Time).toBeLessThan(5)
87
+ }, 140000)
88
+ it('should prefer combinations that still match after dropping a junk last token (exxcellent)', async function () {
89
+ // Equal-length colours can both match the full stem (e.g. 001 vs 014 on …001_4).
90
+ // After removeLastToken, only the true colour remains — keep that combination.
91
+ const result = await testBatch('exxcellent', ['articleCodeSupplier', 'colorCodeSupplier'])
92
+ expect(Object.keys(result).length).toBe(8)
93
+ expect(result.Brand).toBe('exxcellent')
94
+ expect(result.Mismatches).toBe(0)
95
+ expect(result.QuickSearch).toBe(0)
96
+ expect(result.FallbackSearch).toBe(0)
97
+ expect(result.Time).toBeGreaterThan(0)
98
+ expect(result.Time).toBeLessThan(5)
99
+ })
51
100
  })