@softwear/latestcollectioncore 1.0.199 → 1.0.202

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.
@@ -364,6 +364,51 @@ function runQuery(timeframe, rawAggregations, beginStock, transactions, selected
364
364
  }
365
365
  return rawAggregations;
366
366
  }
367
+ /** True when the bound casts to an ISO instant. A calendar day or a stray "T" does not. */
368
+ function hasClockTime(raw) {
369
+ const value = String(raw).trim();
370
+ const cast = new Date(value);
371
+ if (Number.isNaN(cast.getTime()))
372
+ return false;
373
+ const iso = cast.toISOString();
374
+ // yyyy-MM-dd casts to UTC midnight, but the string is only that date prefix.
375
+ if (iso.slice(0, 10) === value)
376
+ return false;
377
+ // The time designator is the character after yyyy-MM-dd, not a stray "T".
378
+ return value.length > 10 && value[10] === 'T';
379
+ }
380
+ /**
381
+ * Exact begin/end for two ISO instants. The end bound is that instant, not end-of-day.
382
+ */
383
+ function getDateRangeFromIsoInstant(inputDates) {
384
+ const dates = inputDates.slice();
385
+ dates.sort((a, b) => {
386
+ return ('' + a).localeCompare(b);
387
+ });
388
+ const fromDate = new Date(Date.parse(dates[0]));
389
+ const toDate = new Date(Date.parse(dates[1]));
390
+ const beginTimeStamp = fromDate.getTime();
391
+ const endTimeStamp = toDate.getTime();
392
+ const timeFrame = endTimeStamp - beginTimeStamp;
393
+ return {
394
+ beginTimeStamp,
395
+ endTimeStamp,
396
+ timeFrame,
397
+ utcFromYear: fromDate.getUTCFullYear(),
398
+ utcToYear: toDate.getUTCFullYear(),
399
+ utcFromMonth: fromDate.getUTCMonth() + 1,
400
+ utcToMonth: toDate.getUTCMonth() + 1,
401
+ fromYear: parseInt((0, date_fns_1.format)(fromDate, 'yyyy')),
402
+ toYear: parseInt((0, date_fns_1.format)(toDate, 'yyyy')),
403
+ fromMonth: parseInt((0, date_fns_1.format)(fromDate, 'MM')),
404
+ toMonth: parseInt((0, date_fns_1.format)(toDate, 'MM')),
405
+ hrYear: (0, date_fns_1.format)(fromDate, 'yyyy'),
406
+ hrQuarter: (0, date_fns_1.format)(fromDate, 'yyyy-QQQ'),
407
+ hrMonth: (0, date_fns_1.format)(fromDate, 'yyyy-MM'),
408
+ hrWeek: (0, date_fns_1.format)(fromDate, 'RRRR-II'),
409
+ hrDay: (0, date_fns_1.format)(fromDate, 'yyyy-MM-dd'),
410
+ };
411
+ }
367
412
  /**
368
413
  * @param inputDates array of two YYYY-MM-DD strings
369
414
  */
@@ -387,6 +432,8 @@ const getDateRangeFromPicker = function (inputDates) {
387
432
  hrWeek: '',
388
433
  hrDay: '',
389
434
  };
435
+ if (hasClockTime(inputDates[0]) || hasClockTime(inputDates[1]))
436
+ return getDateRangeFromIsoInstant(inputDates);
390
437
  const dates = inputDates.slice();
391
438
  dates.sort((a, b) => {
392
439
  return ('' + a).localeCompare(b);
@@ -439,6 +486,8 @@ const getDateRangeFromDatePair = function (inputDates) {
439
486
  hrWeek: '',
440
487
  hrDay: '',
441
488
  };
489
+ if (hasClockTime(inputDates[0]) || hasClockTime(inputDates[1]))
490
+ return getDateRangeFromIsoInstant(inputDates);
442
491
  const dates = inputDates.slice();
443
492
  dates.sort((a, b) => {
444
493
  return ('' + a).localeCompare(b);
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export { default as hashBrand } from './hashBrand';
13
13
  export { default as hasOnlyDigits } from './hasOnlyDigits';
14
14
  export { default as imageBinder } from './imageBinder';
15
15
  export { default as isean13 } from './isean13';
16
+ export { default as parseScannedBarcode } from './parseScannedBarcode';
16
17
  export { default as pivotTable } from './pivotTable';
17
18
  export { default as projectObject } from './projectObject';
18
19
  export { default as reports, genPDF } from './reports';
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.isAxiosError = exports.exponentialDelay = exports.axiosRetry = exports.createAxiosInstance = exports.lcAxios = exports.shouldSuppressZeroField = exports.resolveLayoutFieldText = exports.isZeroFieldValue = exports.isCurrencyFieldFormat = exports.transaction = exports.sizeToMap = exports.round2 = exports.genPDF = exports.reports = exports.projectObject = exports.pivotTable = exports.isean13 = exports.imageBinder = exports.hasOnlyDigits = exports.hashBrand = exports.getPreferedPropertyMappings = exports.getBrandName = exports.findMetaBrandSetting = exports.findSkuByBarcode = exports.ensureImpliedProperties = exports.ensureArray = exports.edifact = exports.ean13 = exports.deepCopy = exports.buildPropertyMappingFn = exports.articleStatus = void 0;
20
+ exports.isAxiosError = exports.exponentialDelay = exports.axiosRetry = exports.createAxiosInstance = exports.lcAxios = exports.shouldSuppressZeroField = exports.resolveLayoutFieldText = exports.isZeroFieldValue = exports.isCurrencyFieldFormat = exports.transaction = exports.sizeToMap = exports.round2 = exports.genPDF = exports.reports = exports.projectObject = exports.pivotTable = exports.parseScannedBarcode = exports.isean13 = exports.imageBinder = exports.hasOnlyDigits = exports.hashBrand = exports.getPreferedPropertyMappings = exports.getBrandName = exports.findMetaBrandSetting = exports.findSkuByBarcode = exports.ensureImpliedProperties = exports.ensureArray = exports.edifact = exports.ean13 = exports.deepCopy = exports.buildPropertyMappingFn = exports.articleStatus = void 0;
21
21
  var articleStatus_1 = require("./articleStatus");
22
22
  Object.defineProperty(exports, "articleStatus", { enumerable: true, get: function () { return __importDefault(articleStatus_1).default; } });
23
23
  var buildPropertyMappingFn_1 = require("./buildPropertyMappingFn");
@@ -48,6 +48,8 @@ var imageBinder_1 = require("./imageBinder");
48
48
  Object.defineProperty(exports, "imageBinder", { enumerable: true, get: function () { return __importDefault(imageBinder_1).default; } });
49
49
  var isean13_1 = require("./isean13");
50
50
  Object.defineProperty(exports, "isean13", { enumerable: true, get: function () { return __importDefault(isean13_1).default; } });
51
+ var parseScannedBarcode_1 = require("./parseScannedBarcode");
52
+ Object.defineProperty(exports, "parseScannedBarcode", { enumerable: true, get: function () { return __importDefault(parseScannedBarcode_1).default; } });
51
53
  var pivotTable_1 = require("./pivotTable");
52
54
  Object.defineProperty(exports, "pivotTable", { enumerable: true, get: function () { return __importDefault(pivotTable_1).default; } });
53
55
  var projectObject_1 = require("./projectObject");
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Gate for scanner / typed barcode input.
3
+ * Strips a leading `#` (scanner prefix), then accepts only 12- or 13-digit strings.
4
+ * Does not validate the EAN check digit (isean13) and does not pad a leading 0 (findSkuByBarcode).
5
+ */
6
+ export default function parseScannedBarcode(raw: unknown): string | null;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const hasOnlyDigits_1 = __importDefault(require("./hasOnlyDigits"));
7
+ /**
8
+ * Gate for scanner / typed barcode input.
9
+ * Strips a leading `#` (scanner prefix), then accepts only 12- or 13-digit strings.
10
+ * Does not validate the EAN check digit (isean13) and does not pad a leading 0 (findSkuByBarcode).
11
+ */
12
+ function parseScannedBarcode(raw) {
13
+ if (typeof raw !== 'string')
14
+ return null;
15
+ const scan = raw.startsWith('#') ? raw.substring(1) : raw;
16
+ if (scan.length !== 12 && scan.length !== 13)
17
+ return null;
18
+ if (!(0, hasOnlyDigits_1.default)(scan))
19
+ return null;
20
+ return scan;
21
+ }
22
+ exports.default = parseScannedBarcode;
package/dist/types.d.ts CHANGED
@@ -129,6 +129,7 @@ interface SkuI {
129
129
  brandhash?: string;
130
130
  ATTRIBUTES?: object;
131
131
  IMAGES?: Array<ImageI>;
132
+ IMAGE_URLS?: Array<string>;
132
133
  SIZES?: unknown;
133
134
  FOOTPRINT?: number;
134
135
  /** UI-only clustering key for product tiles / matrices; not persisted. Phase 1 mirrors articleCodeSupplier after ensureImpliedProperties. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softwear/latestcollectioncore",
3
- "version": "1.0.199",
3
+ "version": "1.0.202",
4
4
  "description": "Core functions for LatestCollections applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -457,6 +457,51 @@ function runQuery(
457
457
  return rawAggregations
458
458
  }
459
459
 
460
+ /** True when the bound casts to an ISO instant. A calendar day or a stray "T" does not. */
461
+ function hasClockTime(raw: string): boolean {
462
+ const value = String(raw).trim()
463
+ const cast = new Date(value)
464
+ if (Number.isNaN(cast.getTime())) return false
465
+ const iso = cast.toISOString()
466
+ // yyyy-MM-dd casts to UTC midnight, but the string is only that date prefix.
467
+ if (iso.slice(0, 10) === value) return false
468
+ // The time designator is the character after yyyy-MM-dd, not a stray "T".
469
+ return value.length > 10 && value[10] === 'T'
470
+ }
471
+
472
+ /**
473
+ * Exact begin/end for two ISO instants. The end bound is that instant, not end-of-day.
474
+ */
475
+ function getDateRangeFromIsoInstant(inputDates: string[]): HrTimeframeI {
476
+ const dates = inputDates.slice()
477
+ dates.sort((a, b) => {
478
+ return ('' + a).localeCompare(b)
479
+ })
480
+ const fromDate = new Date(Date.parse(dates[0]))
481
+ const toDate = new Date(Date.parse(dates[1]))
482
+ const beginTimeStamp = fromDate.getTime()
483
+ const endTimeStamp = toDate.getTime()
484
+ const timeFrame = endTimeStamp - beginTimeStamp
485
+ return {
486
+ beginTimeStamp,
487
+ endTimeStamp,
488
+ timeFrame,
489
+ utcFromYear: fromDate.getUTCFullYear(),
490
+ utcToYear: toDate.getUTCFullYear(),
491
+ utcFromMonth: fromDate.getUTCMonth() + 1,
492
+ utcToMonth: toDate.getUTCMonth() + 1,
493
+ fromYear: parseInt(format(fromDate, 'yyyy')),
494
+ toYear: parseInt(format(toDate, 'yyyy')),
495
+ fromMonth: parseInt(format(fromDate, 'MM')),
496
+ toMonth: parseInt(format(toDate, 'MM')),
497
+ hrYear: format(fromDate, 'yyyy'),
498
+ hrQuarter: format(fromDate, 'yyyy-QQQ'),
499
+ hrMonth: format(fromDate, 'yyyy-MM'),
500
+ hrWeek: format(fromDate, 'RRRR-II'),
501
+ hrDay: format(fromDate, 'yyyy-MM-dd'),
502
+ }
503
+ }
504
+
460
505
  /**
461
506
  * @param inputDates array of two YYYY-MM-DD strings
462
507
  */
@@ -480,6 +525,7 @@ const getDateRangeFromPicker = function (inputDates: string[]): HrTimeframeI {
480
525
  hrWeek: '',
481
526
  hrDay: '',
482
527
  }
528
+ if (hasClockTime(inputDates[0]) || hasClockTime(inputDates[1])) return getDateRangeFromIsoInstant(inputDates)
483
529
  const dates = inputDates.slice()
484
530
  dates.sort((a, b) => {
485
531
  return ('' + a).localeCompare(b)
@@ -534,6 +580,7 @@ const getDateRangeFromDatePair = function (inputDates: string[]): HrTimeframeI {
534
580
  hrWeek: '',
535
581
  hrDay: '',
536
582
  }
583
+ if (hasClockTime(inputDates[0]) || hasClockTime(inputDates[1])) return getDateRangeFromIsoInstant(inputDates)
537
584
  const dates = inputDates.slice()
538
585
  dates.sort((a, b) => {
539
586
  return ('' + a).localeCompare(b)
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export { default as hashBrand } from './hashBrand'
13
13
  export { default as hasOnlyDigits } from './hasOnlyDigits'
14
14
  export { default as imageBinder } from './imageBinder'
15
15
  export { default as isean13 } from './isean13'
16
+ export { default as parseScannedBarcode } from './parseScannedBarcode'
16
17
  export { default as pivotTable } from './pivotTable'
17
18
  export { default as projectObject } from './projectObject'
18
19
  export { default as reports, genPDF } from './reports'
@@ -0,0 +1,14 @@
1
+ import hasOnlyDigits from './hasOnlyDigits'
2
+
3
+ /**
4
+ * Gate for scanner / typed barcode input.
5
+ * Strips a leading `#` (scanner prefix), then accepts only 12- or 13-digit strings.
6
+ * Does not validate the EAN check digit (isean13) and does not pad a leading 0 (findSkuByBarcode).
7
+ */
8
+ export default function parseScannedBarcode(raw: unknown): string | null {
9
+ if (typeof raw !== 'string') return null
10
+ const scan = raw.startsWith('#') ? raw.substring(1) : raw
11
+ if (scan.length !== 12 && scan.length !== 13) return null
12
+ if (!hasOnlyDigits(scan)) return null
13
+ return scan
14
+ }
package/src/types.ts CHANGED
@@ -138,6 +138,7 @@ interface SkuI {
138
138
  brandhash?: string
139
139
  ATTRIBUTES?: object
140
140
  IMAGES?: Array<ImageI>
141
+ IMAGE_URLS?: Array<string>
141
142
  SIZES?: unknown
142
143
  FOOTPRINT?: number
143
144
  /** UI-only clustering key for product tiles / matrices; not persisted. Phase 1 mirrors articleCodeSupplier after ensureImpliedProperties. */
@@ -549,6 +549,7 @@ describe('articleStatus', () => {
549
549
  it('#getDateRangeFromDatePair', () => {
550
550
  const dateRange = articleStatus.getDateRangeFromDatePair(['2023-12-31T23:00:00.000Z', '2024-12-31T23:00:00.000Z'])
551
551
  expect(dateRange.beginTimeStamp).toBe(1704063600000)
552
+ expect(dateRange.endTimeStamp).toBe(Date.parse('2024-12-31T23:00:00.000Z'))
552
553
  expect(dateRange.fromYear).toBe(2024)
553
554
  expect(dateRange.fromMonth).toBe(1)
554
555
  expect(dateRange.toYear).toBe(2025)
@@ -558,7 +559,39 @@ describe('articleStatus', () => {
558
559
  expect(dateRange.hrMonth).toBe('2024-01')
559
560
  expect(dateRange.hrWeek).toBe('2024-01')
560
561
  expect(dateRange.hrDay).toBe('2024-01-01')
561
- expect(dateRange.timeFrame).toBe(367 * 24 * 3600 * 1000)
562
+ expect(dateRange.timeFrame).toBe(366 * 24 * 3600 * 1000)
563
+ })
564
+
565
+ it('#getDateRangeFromDatePair keeps clock time for since-last-run windows', () => {
566
+ const from = '2026-09-18T11:04:01.000Z'
567
+ const to = '2026-09-21T15:04:32.000Z'
568
+ const dateRange = articleStatus.getDateRangeFromDatePair([from, to])
569
+ expect(dateRange.beginTimeStamp).toBe(Date.parse(from))
570
+ expect(dateRange.endTimeStamp).toBe(Date.parse(to))
571
+ expect(dateRange.timeFrame).toBe(Date.parse(to) - Date.parse(from))
572
+ })
573
+
574
+ it('#getDateRangeFromDatePair date-only still includes the end day', () => {
575
+ const dateRange = articleStatus.getDateRangeFromDatePair(['2024-01-01', '2024-01-01'])
576
+ expect(dateRange.beginTimeStamp).toBe(Date.parse('2024-01-01'))
577
+ expect(dateRange.endTimeStamp).toBe(Date.parse('2024-01-01') + 24 * 3600 * 1000)
578
+ expect(dateRange.timeFrame).toBe(24 * 3600 * 1000)
579
+ })
580
+
581
+ it('#getDateRangeFromPicker delegates ISO bounds to exact timestamps', () => {
582
+ const from = '2026-09-18T11:04:01.000Z'
583
+ const to = '2026-09-21T15:04:32.000Z'
584
+ const dateRange = articleStatus.getDateRangeFromPicker([from, to])
585
+ expect(dateRange.beginTimeStamp).toBe(Date.parse(from))
586
+ expect(dateRange.endTimeStamp).toBe(Date.parse(to))
587
+ })
588
+
589
+ it('#getDateRangeFromDatePair keeps a real ISO instant that is not the canonical spelling', () => {
590
+ const raw = '2024-01-01T00:00:00Z'
591
+ const dateRange = articleStatus.getDateRangeFromDatePair([raw, raw])
592
+ expect(new Date(raw).toISOString()).not.toBe(raw)
593
+ expect(dateRange.beginTimeStamp).toBe(new Date(raw).getTime())
594
+ expect(dateRange.endTimeStamp).toBe(new Date(raw).getTime())
562
595
  })
563
596
 
564
597
  it('should calculate ROI percentage from post-aggregated values', () => {
@@ -0,0 +1,30 @@
1
+ const { parseScannedBarcode } = require('../dist/index')
2
+
3
+ describe('parseScannedBarcode', function () {
4
+ it('returns null for non-strings and empty input', function () {
5
+ expect(parseScannedBarcode(undefined)).toBe(null)
6
+ expect(parseScannedBarcode(null)).toBe(null)
7
+ expect(parseScannedBarcode(1234567890123)).toBe(null)
8
+ expect(parseScannedBarcode('')).toBe(null)
9
+ expect(parseScannedBarcode('#')).toBe(null)
10
+ })
11
+
12
+ it('strips a leading # and accepts 12- or 13-digit scans', function () {
13
+ expect(parseScannedBarcode('195093100888')).toBe('195093100888')
14
+ expect(parseScannedBarcode('0195093100888')).toBe('0195093100888')
15
+ expect(parseScannedBarcode('#195093100888')).toBe('195093100888')
16
+ expect(parseScannedBarcode('#0195093100888')).toBe('0195093100888')
17
+ })
18
+
19
+ it('rejects wrong length, letters, and commas', function () {
20
+ expect(parseScannedBarcode('12345678901')).toBe(null)
21
+ expect(parseScannedBarcode('12345678901234')).toBe(null)
22
+ expect(parseScannedBarcode('19509310088a')).toBe(null)
23
+ expect(parseScannedBarcode('19509310088,8')).toBe(null)
24
+ expect(parseScannedBarcode('12e3456789012')).toBe(null)
25
+ })
26
+
27
+ it('does not require a valid EAN check digit', function () {
28
+ expect(parseScannedBarcode('1234567890129')).toBe('1234567890129')
29
+ })
30
+ })