@softwear/latestcollectioncore 1.0.197 → 1.0.199

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.
@@ -1,8 +1,4 @@
1
1
  import { TransactionI, RaGI, HrTimeframeI, TimeGranularityE, SkuI } from './types';
2
- /**
3
- * The propFunction for FiltreX needs some data that can't be passed as parameters. That's why the 'globalsRelations' variable is needed.
4
- * TODO: Functional: Experiment with curried functions
5
- */
6
2
  declare function propFunction(propertyName: string, getPropertyName: Function, obj: Record<string, any>): any;
7
3
  declare function whichShardsToProcess(dateRange: any, granularity: 'year' | 'month'): any[];
8
4
  /**
@@ -29,16 +29,20 @@ let globalNrAggegationExpressions = 0; // tracks the # aggregations rows so we c
29
29
  * The propFunction for FiltreX needs some data that can't be passed as parameters. That's why the 'globalsRelations' variable is needed.
30
30
  * TODO: Functional: Experiment with curried functions
31
31
  */
32
+ function resolvePath(obj, path) {
33
+ return path.reduce((o, key) => o === null || o === void 0 ? void 0 : o[key], obj);
34
+ }
32
35
  function propFunction(propertyName, getPropertyName, obj) {
33
36
  var _a, _b;
34
37
  const positionOfDot = propertyName.indexOf('.');
35
38
  if (positionOfDot < 0)
36
39
  return obj[propertyName];
37
40
  const table = propertyName.substring(0, positionOfDot);
38
- const field = propertyName.substring(positionOfDot + 1);
41
+ const path = propertyName.substring(positionOfDot + 1).split('.');
39
42
  if (table == 'transaction')
40
- return obj[field];
41
- return ((_b = (_a = globalRelations[table]) === null || _a === void 0 ? void 0 : _a[obj[globalJoinableTables[table]]]) === null || _b === void 0 ? void 0 : _b[field]) || '~ ?';
43
+ return resolvePath(obj, path);
44
+ const joinedRecord = (_a = globalRelations[table]) === null || _a === void 0 ? void 0 : _a[obj[globalJoinableTables[table]]];
45
+ return (_b = resolvePath(joinedRecord, path)) !== null && _b !== void 0 ? _b : '~ ?';
42
46
  }
43
47
  const lower = (s) => s.toLowerCase();
44
48
  const upper = (s) => s.toUpperCase();
@@ -155,10 +159,14 @@ function aggregator({ beginTimestamp, endTimestamp, filterExpression, aggregatio
155
159
  .filter((join) => filterExpression.includes(join[0] + '.'))
156
160
  .map((join) => join[1]);
157
161
  }
158
- const aggregationEvaluator = buildAggregationEvaluator(aggregationExpression);
159
- const aggregationGranularity = Object.entries(globalJoinableTables)
160
- .filter((join) => aggregationExpression.includes(join[0] + '.'))
161
- .map((join) => join[1]);
162
+ // Empty aggregation expression = no groupers → only the totals row
163
+ const totalsOnly = !aggregationExpression;
164
+ const aggregationEvaluator = totalsOnly ? undefined : buildAggregationEvaluator(aggregationExpression);
165
+ const aggregationGranularity = totalsOnly
166
+ ? []
167
+ : Object.entries(globalJoinableTables)
168
+ .filter((join) => aggregationExpression.includes(join[0] + '.'))
169
+ .map((join) => join[1]);
162
170
  const aggregationTrieRoot = {};
163
171
  return function (transaction) {
164
172
  if (filter) {
@@ -171,11 +179,6 @@ function aggregator({ beginTimestamp, endTimestamp, filterExpression, aggregatio
171
179
  return;
172
180
  }
173
181
  }
174
- const aggregateKeyLeaf = getTrieLeaf(aggregationTrieRoot, transaction, aggregationGranularity);
175
- if (!aggregateKeyLeaf.aggregateKey) {
176
- aggregateKeyLeaf.aggregateKey = aggregationEvaluator(transaction);
177
- }
178
- const aggregateKey = aggregateKeyLeaf.aggregateKey;
179
182
  // Prepare joined values for derived fields
180
183
  if (transaction.type == types_1.transactionTypeE.SALE) {
181
184
  const sku = globalRelations['sku'][transaction.ean];
@@ -190,8 +193,16 @@ function aggregator({ beginTimestamp, endTimestamp, filterExpression, aggregatio
190
193
  v[AMOUNT_SOLD_MAX] = (0, index_1.round2)((((sku === null || sku === void 0 ? void 0 : sku.price) || 0) / ((100 + (transaction.vat || (sku === null || sku === void 0 ? void 0 : sku.vat) || 21)) / 100)) * v[QTY_SOLD]);
191
194
  v[AMOUNT_SOLD_DISCOUNT] = (0, index_1.round2)(v[AMOUNT_SOLD_MAX] - v[AMOUNT_SOLD_EXCL]);
192
195
  }
193
- if (totals)
196
+ if (totals || totalsOnly)
194
197
  addTransactionToAggregations(beginTimestamp, endTimestamp, '** TOTAL **', rawAggregations, transaction, maxRows);
198
+ // No detail/subtotal rows when there are no groupers
199
+ if (totalsOnly)
200
+ return;
201
+ const aggregateKeyLeaf = getTrieLeaf(aggregationTrieRoot, transaction, aggregationGranularity);
202
+ if (!aggregateKeyLeaf.aggregateKey) {
203
+ aggregateKeyLeaf.aggregateKey = aggregationEvaluator(transaction);
204
+ }
205
+ const aggregateKey = aggregateKeyLeaf.aggregateKey;
195
206
  // Single subtotals are subtotals for first grouper level
196
207
  // Example: for 4 groupers we have rows like
197
208
  // g0 g1 g2 g3 <- normal row with no subtotal
package/dist/types.d.ts CHANGED
@@ -230,6 +230,7 @@ interface BrandSettingI {
230
230
  orderMethods?: string[];
231
231
  propertyMapping?: PropertyMappingI[];
232
232
  fashionCloudId?: string;
233
+ tapprApiHost?: string;
233
234
  sizeSystemPrefix?: string;
234
235
  supplier?: string;
235
236
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softwear/latestcollectioncore",
3
- "version": "1.0.197",
3
+ "version": "1.0.199",
4
4
  "description": "Core functions for LatestCollections applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -97,13 +97,18 @@ let globalNrAggegationExpressions = 0 // tracks the # aggregations rows so we ca
97
97
  * The propFunction for FiltreX needs some data that can't be passed as parameters. That's why the 'globalsRelations' variable is needed.
98
98
  * TODO: Functional: Experiment with curried functions
99
99
  */
100
+ function resolvePath(obj: Record<string, any> | undefined, path: string[]): any {
101
+ return path.reduce((o, key) => o?.[key], obj)
102
+ }
103
+
100
104
  function propFunction(propertyName: string, getPropertyName: Function, obj: Record<string, any>): any {
101
105
  const positionOfDot = propertyName.indexOf('.')
102
106
  if (positionOfDot < 0) return obj[propertyName]
103
107
  const table = propertyName.substring(0, positionOfDot)
104
- const field = propertyName.substring(positionOfDot + 1)
105
- if (table == 'transaction') return obj[field]
106
- return globalRelations[table]?.[obj[globalJoinableTables[table]]]?.[field] || '~ ?'
108
+ const path = propertyName.substring(positionOfDot + 1).split('.')
109
+ if (table == 'transaction') return resolvePath(obj, path)
110
+ const joinedRecord = globalRelations[table]?.[obj[globalJoinableTables[table]]]
111
+ return resolvePath(joinedRecord, path) ?? '~ ?'
107
112
  }
108
113
 
109
114
  const lower = (s: string) => s.toLowerCase()
@@ -237,10 +242,14 @@ function aggregator({
237
242
  .filter((join) => filterExpression.includes(join[0] + '.'))
238
243
  .map((join) => join[1])
239
244
  }
240
- const aggregationEvaluator = buildAggregationEvaluator(aggregationExpression)
241
- const aggregationGranularity = Object.entries(globalJoinableTables)
242
- .filter((join) => aggregationExpression.includes(join[0] + '.'))
243
- .map((join) => join[1])
245
+ // Empty aggregation expression = no groupers → only the totals row
246
+ const totalsOnly = !aggregationExpression
247
+ const aggregationEvaluator = totalsOnly ? undefined : buildAggregationEvaluator(aggregationExpression)
248
+ const aggregationGranularity = totalsOnly
249
+ ? []
250
+ : Object.entries(globalJoinableTables)
251
+ .filter((join) => aggregationExpression.includes(join[0] + '.'))
252
+ .map((join) => join[1])
244
253
  const aggregationTrieRoot: AggregateKeyTrieNode = {}
245
254
 
246
255
  return function (transaction: TransactionI): void {
@@ -252,11 +261,6 @@ function aggregator({
252
261
  if (filterLeaf.filterResult === -1) return
253
262
  }
254
263
  }
255
- const aggregateKeyLeaf = getTrieLeaf<AggregateKeyTrieNode>(aggregationTrieRoot, transaction, aggregationGranularity)
256
- if (!aggregateKeyLeaf.aggregateKey) {
257
- aggregateKeyLeaf.aggregateKey = aggregationEvaluator(transaction) as string
258
- }
259
- const aggregateKey = aggregateKeyLeaf.aggregateKey as string
260
264
  // Prepare joined values for derived fields
261
265
  if (transaction.type == transactionTypeE.SALE) {
262
266
  const sku = globalRelations['sku'][transaction.ean]
@@ -271,7 +275,16 @@ function aggregator({
271
275
  v[AMOUNT_SOLD_DISCOUNT] = round2(v[AMOUNT_SOLD_MAX] - v[AMOUNT_SOLD_EXCL])
272
276
  }
273
277
 
274
- if (totals) addTransactionToAggregations(beginTimestamp, endTimestamp, '** TOTAL **', rawAggregations, transaction, maxRows)
278
+ if (totals || totalsOnly) addTransactionToAggregations(beginTimestamp, endTimestamp, '** TOTAL **', rawAggregations, transaction, maxRows)
279
+
280
+ // No detail/subtotal rows when there are no groupers
281
+ if (totalsOnly) return
282
+
283
+ const aggregateKeyLeaf = getTrieLeaf<AggregateKeyTrieNode>(aggregationTrieRoot, transaction, aggregationGranularity)
284
+ if (!aggregateKeyLeaf.aggregateKey) {
285
+ aggregateKeyLeaf.aggregateKey = aggregationEvaluator!(transaction) as string
286
+ }
287
+ const aggregateKey = aggregateKeyLeaf.aggregateKey as string
275
288
 
276
289
  // Single subtotals are subtotals for first grouper level
277
290
  // Example: for 4 groupers we have rows like
package/src/types.ts CHANGED
@@ -244,6 +244,7 @@ interface BrandSettingI {
244
244
  orderMethods?: string[]
245
245
  propertyMapping?: PropertyMappingI[]
246
246
  fashionCloudId?: string
247
+ tapprApiHost?: string
247
248
  sizeSystemPrefix?: string
248
249
  supplier?: string
249
250
  }
@@ -178,6 +178,29 @@ describe('articleStatus', () => {
178
178
  expect(result[2].vector[articleStatus.VALUE_AVG_STOCK]).toBe(84.15000000000002)
179
179
  })
180
180
 
181
+ it('should calculate only the totals row when no groupers are selected', () => {
182
+ const rawAggregations = {}
183
+ articleStatus.runQuery(
184
+ { beginTimeStamp: 1569316408000, endTimeStamp: 1569316409000 + msYear },
185
+ rawAggregations,
186
+ true,
187
+ TRANSACTIONS,
188
+ [],
189
+ SKUS,
190
+ {},
191
+ {},
192
+ false,
193
+ 'transaction.ean=="000558000143"',
194
+ false
195
+ )
196
+
197
+ expect(Object.keys(rawAggregations)).toEqual(['** TOTAL **'])
198
+ const vector = [].slice.call(rawAggregations['** TOTAL **'])
199
+ articleStatus.postAgg(vector, msYear)
200
+ expect(vector[articleStatus.QTY_STOCK]).toBe(18)
201
+ expect(vector[articleStatus.QTY_END_STOCK]).toBe(17)
202
+ })
203
+
181
204
  it('should calculate aggregations', () => {
182
205
  const rawAggregations = {}
183
206
  articleStatus.runQuery(
@@ -271,6 +294,61 @@ describe('articleStatus', () => {
271
294
  expect(result[2].vector[articleStatus.MARGIN]).toBe(50)
272
295
  })
273
296
 
297
+ it('should resolve nested property paths like customer.invoiceAddress.countrycode when grouping', () => {
298
+ const CUSTOMERS = {
299
+ '*zBXU': { invoiceAddress: { countrycode: 'NL' } },
300
+ }
301
+ const rawAggregations = {}
302
+ articleStatus.runQuery(
303
+ { beginTimeStamp: 1569316408000, endTimeStamp: 1569316409000 + msYear },
304
+ rawAggregations,
305
+ true,
306
+ TRANSACTIONS,
307
+ ['customer.invoiceAddress.countrycode'],
308
+ SKUS,
309
+ {},
310
+ CUSTOMERS,
311
+ false,
312
+ '',
313
+ false
314
+ )
315
+
316
+ // Sales (custnr '*zBXU') resolve the nested path to 'NL',
317
+ // beginStock transactions have no custnr and fall back to '~ ?'
318
+ expect(Object.keys(rawAggregations)).toEqual(['** TOTAL **', '~ ?', 'NL'])
319
+ expect(rawAggregations['NL'][articleStatus.QTY_SOLD]).toBe(2)
320
+ })
321
+
322
+ it('should resolve nested property paths in filter expressions', () => {
323
+ const CUSTOMERS = {
324
+ '*zBXU': { invoiceAddress: { countrycode: 'NL' } },
325
+ }
326
+ const run = (countrycode) => {
327
+ const rawAggregations = {}
328
+ articleStatus.runQuery(
329
+ { beginTimeStamp: 1569316408000, endTimeStamp: 1569316409000 + msYear },
330
+ rawAggregations,
331
+ false,
332
+ TRANSACTIONS,
333
+ ['transaction.ean'],
334
+ SKUS,
335
+ {},
336
+ CUSTOMERS,
337
+ false,
338
+ 'customer.invoiceAddress.countrycode == "' + countrycode + '"',
339
+ false
340
+ )
341
+ return rawAggregations
342
+ }
343
+
344
+ const matching = run('NL')
345
+ expect(Object.keys(matching)).toEqual(['** TOTAL **', '000558000143', '8700558000143'])
346
+ expect(matching['000558000143'][articleStatus.QTY_SOLD]).toBe(1)
347
+
348
+ const nonMatching = run('DE')
349
+ expect(Object.keys(nonMatching)).toEqual([])
350
+ })
351
+
274
352
  it('should calculate shelfStock correctly with type 10 (beginShelfStock) and type 94 (picklist)', () => {
275
353
  // Test scenario:
276
354
  // - beginStock (type 0): 100 units