@zeniai/client-epic-state 5.0.90-betaAR6 → 5.0.90-betaAR8
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.
|
@@ -153,14 +153,17 @@ const getLineClassIdentifiersForTransaction = (transaction) => {
|
|
|
153
153
|
return [];
|
|
154
154
|
};
|
|
155
155
|
/**
|
|
156
|
-
* Type-narrowing guard for the three persisted line shapes that
|
|
157
|
-
* `
|
|
158
|
-
*
|
|
156
|
+
* Type-narrowing guard for the three persisted line shapes that extend
|
|
157
|
+
* `TransactionWithAccountAndClassLine` — they carry an `account` + `class`
|
|
158
|
+
* AND `vendor` + `customer` as optional per-line fields. Kept as a single
|
|
159
|
+
* helper so the type list stays in one place if a new categorizable
|
|
160
|
+
* line variant is added; reused by the category, class, and payee
|
|
161
|
+
* per-line resolvers below.
|
|
159
162
|
*
|
|
160
|
-
* Typed against
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
163
|
+
* Typed against a structural `{type?: string}` shape rather than the
|
|
164
|
+
* concrete `Line` union so callers can pass a possibly-sparse element
|
|
165
|
+
* without an extra null check. The cast to
|
|
166
|
+
* `TransactionWithAccountAndClassLine` happens at the call sites.
|
|
164
167
|
*/
|
|
165
168
|
const isAccountAndClassLine = (line) => line?.type === 'transaction_with_account_and_class_line' ||
|
|
166
169
|
line?.type === 'transaction_with_product_or_service_line' ||
|
|
@@ -204,14 +207,108 @@ const resolveClassIdentifier = (classData) => {
|
|
|
204
207
|
}
|
|
205
208
|
return undefined;
|
|
206
209
|
};
|
|
210
|
+
/**
|
|
211
|
+
* Resolve a single line's payee name. Vendor wins over customer
|
|
212
|
+
* (preserved from the single-scalar predecessor — TC transactions are
|
|
213
|
+
* overwhelmingly expense-side). `isNonEmptyString` so an empty-string
|
|
214
|
+
* placeholder on `vendor.name` falls through to `customer.name` rather
|
|
215
|
+
* than shadowing it.
|
|
216
|
+
*/
|
|
217
|
+
const resolveLineLevelPayee = (vendor, customer) => {
|
|
218
|
+
if (isNonEmptyString(vendor?.name)) {
|
|
219
|
+
return vendor?.name;
|
|
220
|
+
}
|
|
221
|
+
if (isNonEmptyString(customer?.name)) {
|
|
222
|
+
return customer?.name;
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Per-line payee names for a transaction. Bridges the row's display
|
|
228
|
+
* logic — Journal Entries (and any future transaction type that
|
|
229
|
+
* carries vendor/customer per line) put the payee on each line, while
|
|
230
|
+
* Bills, Expenses, and Checks carry it at the transaction level. The
|
|
231
|
+
* single-scalar predecessor only consulted the transaction level, so
|
|
232
|
+
* JE rows whose vendor lived on the line silently missed every payee
|
|
233
|
+
* filter even though the row visibly displayed the vendor name (the
|
|
234
|
+
* row's `TransactionCategorizationListRow` already reads
|
|
235
|
+
* `line.vendor?.name` for non-vendor-typed transactions).
|
|
236
|
+
*
|
|
237
|
+
* Source precedence:
|
|
238
|
+
* 1. localData `lineItemById` — locally-edited per-line vendor or
|
|
239
|
+
* customer. Lines with neither field set are dropped from the
|
|
240
|
+
* array; if at least one line has a name, that line set drives
|
|
241
|
+
* matching (we don't mix line-level localData with the transaction-
|
|
242
|
+
* level fallback below).
|
|
243
|
+
* 2. Persisted `transaction.lines` — per-line vendor or customer on
|
|
244
|
+
* the three categorizable line shapes (see `isAccountAndClassLine`).
|
|
245
|
+
* Other line types (`linked_transaction_line`, etc.) are skipped.
|
|
246
|
+
* 3. Transaction-level fallback — localData transaction-level
|
|
247
|
+
* vendor → localData transaction-level customer → `transaction.
|
|
248
|
+
* vendorName` → `transaction.customerName`. Wrapped in a single-
|
|
249
|
+
* element array so the rest of the matcher stays per-line shaped.
|
|
250
|
+
*
|
|
251
|
+
* Returns `[]` when every source resolves empty; the caller falls back
|
|
252
|
+
* to the absent-value contract (only `not_equal` matches; every other
|
|
253
|
+
* operator excludes).
|
|
254
|
+
*/
|
|
255
|
+
const getLinePayeesForTransaction = (transaction) => {
|
|
256
|
+
const localData = transaction?.transactionLocalData?.transactionReviewLocalData;
|
|
257
|
+
// Priority 1: localData line-level vendor / customer
|
|
258
|
+
if (localData?.lineItemById != null) {
|
|
259
|
+
const lineItems = Object.values(localData.lineItemById);
|
|
260
|
+
if (lineItems.length > 0) {
|
|
261
|
+
const linePayees = lineItems
|
|
262
|
+
.map((lineItem) => resolveLineLevelPayee(lineItem?.vendor, lineItem?.customer))
|
|
263
|
+
.filter(isNonEmptyString);
|
|
264
|
+
if (linePayees.length > 0) {
|
|
265
|
+
return linePayees;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Priority 2: persisted line-level vendor / customer
|
|
270
|
+
if (transaction?.transaction?.lines != null &&
|
|
271
|
+
transaction.transaction.lines.length > 0) {
|
|
272
|
+
const linePayees = transaction.transaction.lines
|
|
273
|
+
.map((line) => {
|
|
274
|
+
if (!isAccountAndClassLine(line)) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
const typedLine = line;
|
|
278
|
+
return resolveLineLevelPayee(typedLine.vendor, typedLine.customer);
|
|
279
|
+
})
|
|
280
|
+
.filter(isNonEmptyString);
|
|
281
|
+
if (linePayees.length > 0) {
|
|
282
|
+
return linePayees;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Priority 3: transaction-level fallback. localData wins over the
|
|
286
|
+
// top-level fields to keep the existing precedence intact: unsaved
|
|
287
|
+
// grid edits should drive the filter, not the pre-edit server state.
|
|
288
|
+
const localVendorName = localData?.vendor?.name;
|
|
289
|
+
if (isNonEmptyString(localVendorName)) {
|
|
290
|
+
return [localVendorName];
|
|
291
|
+
}
|
|
292
|
+
const localCustomerName = localData?.customer?.name;
|
|
293
|
+
if (isNonEmptyString(localCustomerName)) {
|
|
294
|
+
return [localCustomerName];
|
|
295
|
+
}
|
|
296
|
+
if (isNonEmptyString(transaction.vendorName)) {
|
|
297
|
+
return [transaction.vendorName];
|
|
298
|
+
}
|
|
299
|
+
if (isNonEmptyString(transaction.customerName)) {
|
|
300
|
+
return [transaction.customerName];
|
|
301
|
+
}
|
|
302
|
+
return [];
|
|
303
|
+
};
|
|
207
304
|
/**
|
|
208
305
|
* Resolve the comparable value for a transaction against a given filter field.
|
|
209
306
|
* Returns undefined when the transaction doesn't carry the field — callers
|
|
210
307
|
* decide whether absence counts as a match (for 'not_equal' it does).
|
|
211
308
|
*
|
|
212
|
-
* NOTE: the `'amount'`, `'category'`, and `'
|
|
213
|
-
* here. All
|
|
214
|
-
* `getLine*ForTransaction` helper and an early-return in
|
|
309
|
+
* NOTE: the `'amount'`, `'category'`, `'class'`, and `'payee'` fields
|
|
310
|
+
* are NOT handled here. All four use per-line matching via the
|
|
311
|
+
* corresponding `getLine*ForTransaction` helper and an early-return in
|
|
215
312
|
* `transactionMatchesCategory` — they would otherwise need to return a
|
|
216
313
|
* non-scalar value that doesn't fit this helper's contract. Cases are
|
|
217
314
|
* preserved so the switch stays exhaustive over `TransactionFilterCategoryField`.
|
|
@@ -224,21 +321,9 @@ const getCategoryValueForTransaction = (key, transaction) => {
|
|
|
224
321
|
return undefined;
|
|
225
322
|
}
|
|
226
323
|
case 'payee': {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (isNonEmptyString(transaction.customerName)) {
|
|
231
|
-
return transaction.customerName;
|
|
232
|
-
}
|
|
233
|
-
const localData = transaction.transactionLocalData?.transactionReviewLocalData;
|
|
234
|
-
const vendorName = localData?.vendor?.name;
|
|
235
|
-
if (isNonEmptyString(vendorName)) {
|
|
236
|
-
return vendorName;
|
|
237
|
-
}
|
|
238
|
-
const customerName = localData?.customer?.name;
|
|
239
|
-
if (isNonEmptyString(customerName)) {
|
|
240
|
-
return customerName;
|
|
241
|
-
}
|
|
324
|
+
// Handled per-line in `transactionMatchesCategory` — see
|
|
325
|
+
// `getLinePayeesForTransaction`. The transaction-level localData /
|
|
326
|
+
// top-level chain lives inside that helper's priority-3 fallback.
|
|
242
327
|
return undefined;
|
|
243
328
|
}
|
|
244
329
|
case 'payment_account_name': {
|
|
@@ -314,6 +399,29 @@ const matchAmount = (amount, category) => {
|
|
|
314
399
|
return op === 'not_equal' ? !matched : matched;
|
|
315
400
|
};
|
|
316
401
|
const transactionMatchesCategory = (transaction, category) => {
|
|
402
|
+
if (category.field === 'payee') {
|
|
403
|
+
// Per-line matching for payee — mirrors the amount / category /
|
|
404
|
+
// class pattern. The row's display reads `line.vendor?.name` for
|
|
405
|
+
// non-vendor-typed transactions (Journal Entries, etc.), so the
|
|
406
|
+
// filter has to look at line-level payee data too or it silently
|
|
407
|
+
// misses every JE whose vendor lives on the line. See the JSDoc
|
|
408
|
+
// on `getLinePayeesForTransaction` for the full precedence.
|
|
409
|
+
//
|
|
410
|
+
// `not_equal` reads the same way it does for category/class:
|
|
411
|
+
// "at least one line's payee is NOT one of the filter values".
|
|
412
|
+
// A JE with lines [Amazon, Acme] matches `payee != Amazon` via
|
|
413
|
+
// the Acme line; a bill or JE whose every line says "Amazon"
|
|
414
|
+
// does not match `payee != Amazon`.
|
|
415
|
+
const linePayees = getLinePayeesForTransaction(transaction);
|
|
416
|
+
if (linePayees.length === 0) {
|
|
417
|
+
return category.matchingOperator === 'not_equal';
|
|
418
|
+
}
|
|
419
|
+
const filterValuesAsStrings = new Set(category.values.map((v) => v.toString()));
|
|
420
|
+
return linePayees.some((name) => {
|
|
421
|
+
const inList = filterValuesAsStrings.has(name);
|
|
422
|
+
return category.matchingOperator === 'not_equal' ? !inList : inList;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
317
425
|
if (category.field === 'category' || category.field === 'class') {
|
|
318
426
|
// Per-line matching for category / class — mirrors the amount
|
|
319
427
|
// pattern below. A multi-line transaction surfaces when ANY of its
|
|
@@ -156,14 +156,17 @@ const getLineClassIdentifiersForTransaction = (transaction) => {
|
|
|
156
156
|
return [];
|
|
157
157
|
};
|
|
158
158
|
/**
|
|
159
|
-
* Type-narrowing guard for the three persisted line shapes that
|
|
160
|
-
* `
|
|
161
|
-
*
|
|
159
|
+
* Type-narrowing guard for the three persisted line shapes that extend
|
|
160
|
+
* `TransactionWithAccountAndClassLine` — they carry an `account` + `class`
|
|
161
|
+
* AND `vendor` + `customer` as optional per-line fields. Kept as a single
|
|
162
|
+
* helper so the type list stays in one place if a new categorizable
|
|
163
|
+
* line variant is added; reused by the category, class, and payee
|
|
164
|
+
* per-line resolvers below.
|
|
162
165
|
*
|
|
163
|
-
* Typed against
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
166
|
+
* Typed against a structural `{type?: string}` shape rather than the
|
|
167
|
+
* concrete `Line` union so callers can pass a possibly-sparse element
|
|
168
|
+
* without an extra null check. The cast to
|
|
169
|
+
* `TransactionWithAccountAndClassLine` happens at the call sites.
|
|
167
170
|
*/
|
|
168
171
|
const isAccountAndClassLine = (line) => line?.type === 'transaction_with_account_and_class_line' ||
|
|
169
172
|
line?.type === 'transaction_with_product_or_service_line' ||
|
|
@@ -207,14 +210,108 @@ const resolveClassIdentifier = (classData) => {
|
|
|
207
210
|
}
|
|
208
211
|
return undefined;
|
|
209
212
|
};
|
|
213
|
+
/**
|
|
214
|
+
* Resolve a single line's payee name. Vendor wins over customer
|
|
215
|
+
* (preserved from the single-scalar predecessor — TC transactions are
|
|
216
|
+
* overwhelmingly expense-side). `isNonEmptyString` so an empty-string
|
|
217
|
+
* placeholder on `vendor.name` falls through to `customer.name` rather
|
|
218
|
+
* than shadowing it.
|
|
219
|
+
*/
|
|
220
|
+
const resolveLineLevelPayee = (vendor, customer) => {
|
|
221
|
+
if (isNonEmptyString(vendor?.name)) {
|
|
222
|
+
return vendor?.name;
|
|
223
|
+
}
|
|
224
|
+
if (isNonEmptyString(customer?.name)) {
|
|
225
|
+
return customer?.name;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Per-line payee names for a transaction. Bridges the row's display
|
|
231
|
+
* logic — Journal Entries (and any future transaction type that
|
|
232
|
+
* carries vendor/customer per line) put the payee on each line, while
|
|
233
|
+
* Bills, Expenses, and Checks carry it at the transaction level. The
|
|
234
|
+
* single-scalar predecessor only consulted the transaction level, so
|
|
235
|
+
* JE rows whose vendor lived on the line silently missed every payee
|
|
236
|
+
* filter even though the row visibly displayed the vendor name (the
|
|
237
|
+
* row's `TransactionCategorizationListRow` already reads
|
|
238
|
+
* `line.vendor?.name` for non-vendor-typed transactions).
|
|
239
|
+
*
|
|
240
|
+
* Source precedence:
|
|
241
|
+
* 1. localData `lineItemById` — locally-edited per-line vendor or
|
|
242
|
+
* customer. Lines with neither field set are dropped from the
|
|
243
|
+
* array; if at least one line has a name, that line set drives
|
|
244
|
+
* matching (we don't mix line-level localData with the transaction-
|
|
245
|
+
* level fallback below).
|
|
246
|
+
* 2. Persisted `transaction.lines` — per-line vendor or customer on
|
|
247
|
+
* the three categorizable line shapes (see `isAccountAndClassLine`).
|
|
248
|
+
* Other line types (`linked_transaction_line`, etc.) are skipped.
|
|
249
|
+
* 3. Transaction-level fallback — localData transaction-level
|
|
250
|
+
* vendor → localData transaction-level customer → `transaction.
|
|
251
|
+
* vendorName` → `transaction.customerName`. Wrapped in a single-
|
|
252
|
+
* element array so the rest of the matcher stays per-line shaped.
|
|
253
|
+
*
|
|
254
|
+
* Returns `[]` when every source resolves empty; the caller falls back
|
|
255
|
+
* to the absent-value contract (only `not_equal` matches; every other
|
|
256
|
+
* operator excludes).
|
|
257
|
+
*/
|
|
258
|
+
const getLinePayeesForTransaction = (transaction) => {
|
|
259
|
+
const localData = transaction?.transactionLocalData?.transactionReviewLocalData;
|
|
260
|
+
// Priority 1: localData line-level vendor / customer
|
|
261
|
+
if (localData?.lineItemById != null) {
|
|
262
|
+
const lineItems = Object.values(localData.lineItemById);
|
|
263
|
+
if (lineItems.length > 0) {
|
|
264
|
+
const linePayees = lineItems
|
|
265
|
+
.map((lineItem) => resolveLineLevelPayee(lineItem?.vendor, lineItem?.customer))
|
|
266
|
+
.filter(isNonEmptyString);
|
|
267
|
+
if (linePayees.length > 0) {
|
|
268
|
+
return linePayees;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Priority 2: persisted line-level vendor / customer
|
|
273
|
+
if (transaction?.transaction?.lines != null &&
|
|
274
|
+
transaction.transaction.lines.length > 0) {
|
|
275
|
+
const linePayees = transaction.transaction.lines
|
|
276
|
+
.map((line) => {
|
|
277
|
+
if (!isAccountAndClassLine(line)) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
const typedLine = line;
|
|
281
|
+
return resolveLineLevelPayee(typedLine.vendor, typedLine.customer);
|
|
282
|
+
})
|
|
283
|
+
.filter(isNonEmptyString);
|
|
284
|
+
if (linePayees.length > 0) {
|
|
285
|
+
return linePayees;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Priority 3: transaction-level fallback. localData wins over the
|
|
289
|
+
// top-level fields to keep the existing precedence intact: unsaved
|
|
290
|
+
// grid edits should drive the filter, not the pre-edit server state.
|
|
291
|
+
const localVendorName = localData?.vendor?.name;
|
|
292
|
+
if (isNonEmptyString(localVendorName)) {
|
|
293
|
+
return [localVendorName];
|
|
294
|
+
}
|
|
295
|
+
const localCustomerName = localData?.customer?.name;
|
|
296
|
+
if (isNonEmptyString(localCustomerName)) {
|
|
297
|
+
return [localCustomerName];
|
|
298
|
+
}
|
|
299
|
+
if (isNonEmptyString(transaction.vendorName)) {
|
|
300
|
+
return [transaction.vendorName];
|
|
301
|
+
}
|
|
302
|
+
if (isNonEmptyString(transaction.customerName)) {
|
|
303
|
+
return [transaction.customerName];
|
|
304
|
+
}
|
|
305
|
+
return [];
|
|
306
|
+
};
|
|
210
307
|
/**
|
|
211
308
|
* Resolve the comparable value for a transaction against a given filter field.
|
|
212
309
|
* Returns undefined when the transaction doesn't carry the field — callers
|
|
213
310
|
* decide whether absence counts as a match (for 'not_equal' it does).
|
|
214
311
|
*
|
|
215
|
-
* NOTE: the `'amount'`, `'category'`, and `'
|
|
216
|
-
* here. All
|
|
217
|
-
* `getLine*ForTransaction` helper and an early-return in
|
|
312
|
+
* NOTE: the `'amount'`, `'category'`, `'class'`, and `'payee'` fields
|
|
313
|
+
* are NOT handled here. All four use per-line matching via the
|
|
314
|
+
* corresponding `getLine*ForTransaction` helper and an early-return in
|
|
218
315
|
* `transactionMatchesCategory` — they would otherwise need to return a
|
|
219
316
|
* non-scalar value that doesn't fit this helper's contract. Cases are
|
|
220
317
|
* preserved so the switch stays exhaustive over `TransactionFilterCategoryField`.
|
|
@@ -227,21 +324,9 @@ const getCategoryValueForTransaction = (key, transaction) => {
|
|
|
227
324
|
return undefined;
|
|
228
325
|
}
|
|
229
326
|
case 'payee': {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (isNonEmptyString(transaction.customerName)) {
|
|
234
|
-
return transaction.customerName;
|
|
235
|
-
}
|
|
236
|
-
const localData = transaction.transactionLocalData?.transactionReviewLocalData;
|
|
237
|
-
const vendorName = localData?.vendor?.name;
|
|
238
|
-
if (isNonEmptyString(vendorName)) {
|
|
239
|
-
return vendorName;
|
|
240
|
-
}
|
|
241
|
-
const customerName = localData?.customer?.name;
|
|
242
|
-
if (isNonEmptyString(customerName)) {
|
|
243
|
-
return customerName;
|
|
244
|
-
}
|
|
327
|
+
// Handled per-line in `transactionMatchesCategory` — see
|
|
328
|
+
// `getLinePayeesForTransaction`. The transaction-level localData /
|
|
329
|
+
// top-level chain lives inside that helper's priority-3 fallback.
|
|
245
330
|
return undefined;
|
|
246
331
|
}
|
|
247
332
|
case 'payment_account_name': {
|
|
@@ -317,6 +402,29 @@ const matchAmount = (amount, category) => {
|
|
|
317
402
|
return op === 'not_equal' ? !matched : matched;
|
|
318
403
|
};
|
|
319
404
|
const transactionMatchesCategory = (transaction, category) => {
|
|
405
|
+
if (category.field === 'payee') {
|
|
406
|
+
// Per-line matching for payee — mirrors the amount / category /
|
|
407
|
+
// class pattern. The row's display reads `line.vendor?.name` for
|
|
408
|
+
// non-vendor-typed transactions (Journal Entries, etc.), so the
|
|
409
|
+
// filter has to look at line-level payee data too or it silently
|
|
410
|
+
// misses every JE whose vendor lives on the line. See the JSDoc
|
|
411
|
+
// on `getLinePayeesForTransaction` for the full precedence.
|
|
412
|
+
//
|
|
413
|
+
// `not_equal` reads the same way it does for category/class:
|
|
414
|
+
// "at least one line's payee is NOT one of the filter values".
|
|
415
|
+
// A JE with lines [Amazon, Acme] matches `payee != Amazon` via
|
|
416
|
+
// the Acme line; a bill or JE whose every line says "Amazon"
|
|
417
|
+
// does not match `payee != Amazon`.
|
|
418
|
+
const linePayees = getLinePayeesForTransaction(transaction);
|
|
419
|
+
if (linePayees.length === 0) {
|
|
420
|
+
return category.matchingOperator === 'not_equal';
|
|
421
|
+
}
|
|
422
|
+
const filterValuesAsStrings = new Set(category.values.map((v) => v.toString()));
|
|
423
|
+
return linePayees.some((name) => {
|
|
424
|
+
const inList = filterValuesAsStrings.has(name);
|
|
425
|
+
return category.matchingOperator === 'not_equal' ? !inList : inList;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
320
428
|
if (category.field === 'category' || category.field === 'class') {
|
|
321
429
|
// Per-line matching for category / class — mirrors the amount
|
|
322
430
|
// pattern below. A multi-line transaction surfaces when ANY of its
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeniai/client-epic-state",
|
|
3
|
-
"version": "5.0.90-
|
|
3
|
+
"version": "5.0.90-betaAR8",
|
|
4
4
|
"description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/esm/index.js",
|