@zeniai/client-epic-state 5.0.90-betaAR7 → 5.0.90-betaAR9
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,112 @@ 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
|
+
* All payee names that could legitimately identify a transaction, across
|
|
228
|
+
* every place TC stores payee data:
|
|
229
|
+
*
|
|
230
|
+
* • localData `lineItemById[*].vendor` / `.customer` — per-line
|
|
231
|
+
* vendor/customer the user has tagged in the grid. Populated for
|
|
232
|
+
* JEs (where the row reads per-line vendor) and sometimes for
|
|
233
|
+
* vendor-typed transactions too (e.g. when an AI vendor
|
|
234
|
+
* recommendation lands on a line).
|
|
235
|
+
* • Persisted `transaction.lines[*].vendor` / `.customer` — same
|
|
236
|
+
* fields on the server snapshot of each line.
|
|
237
|
+
* • localData transaction-level `vendor` / `customer` — what the row
|
|
238
|
+
* displays for vendor-typed transactions (Bills / Expenses /
|
|
239
|
+
* Checks).
|
|
240
|
+
* • Top-level `transaction.vendorName` / `customerName` — pre-edit
|
|
241
|
+
* server snapshot.
|
|
242
|
+
*
|
|
243
|
+
* The function collects names from ALL of these into a single set
|
|
244
|
+
* rather than picking exclusively from the highest-priority source.
|
|
245
|
+
* Two reasons:
|
|
246
|
+
*
|
|
247
|
+
* 1. Different transaction types put the displayed payee in different
|
|
248
|
+
* places. JEs put it on the line; Bills put it at the transaction
|
|
249
|
+
* level. A strict-precedence rule that returns early on the first
|
|
250
|
+
* non-empty source ends up missing the OTHER place when both have
|
|
251
|
+
* data with different values — which is exactly the
|
|
252
|
+
* production failure mode that motivated this fix (a Bill whose
|
|
253
|
+
* localData line item carried an AI-recommended vendor name was
|
|
254
|
+
* excluded because the recommendation won priority over the
|
|
255
|
+
* tx-level "Amazon" the row actually displayed).
|
|
256
|
+
* 2. The single-scalar predecessor missed JE rows entirely because
|
|
257
|
+
* it only looked at the tx level. Unioning the line-level sources
|
|
258
|
+
* in fixes that without re-introducing the "first source wins"
|
|
259
|
+
* brittleness.
|
|
260
|
+
*
|
|
261
|
+
* False positives are bounded: a transaction matches `payee = X` only
|
|
262
|
+
* if `X` literally appears in one of its stored payee fields. Stale
|
|
263
|
+
* recommendation data being included means the filter is slightly more
|
|
264
|
+
* lenient than what the row displays, not arbitrary.
|
|
265
|
+
*
|
|
266
|
+
* Returns `[]` when no source carries any name; the caller falls back
|
|
267
|
+
* to the absent-value contract (only `not_equal` matches; every other
|
|
268
|
+
* operator excludes).
|
|
269
|
+
*/
|
|
270
|
+
const getLinePayeesForTransaction = (transaction) => {
|
|
271
|
+
const localData = transaction?.transactionLocalData?.transactionReviewLocalData;
|
|
272
|
+
// Set used to dedupe — the same vendor name often appears in multiple
|
|
273
|
+
// sources (e.g. localData mirror of a persisted line) and we don't
|
|
274
|
+
// want it to be over-counted in the match list. Iteration order is
|
|
275
|
+
// insertion-order; `.some()` doesn't depend on order so this is fine.
|
|
276
|
+
const payees = new Set();
|
|
277
|
+
const addIfPresent = (name) => {
|
|
278
|
+
if (isNonEmptyString(name)) {
|
|
279
|
+
payees.add(name);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
// localData per-line vendor / customer
|
|
283
|
+
if (localData?.lineItemById != null) {
|
|
284
|
+
Object.values(localData.lineItemById).forEach((lineItem) => {
|
|
285
|
+
addIfPresent(resolveLineLevelPayee(lineItem?.vendor, lineItem?.customer));
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// Persisted per-line vendor / customer (only on the three line
|
|
289
|
+
// shapes that actually carry them — see `isAccountAndClassLine`).
|
|
290
|
+
if (transaction?.transaction?.lines != null &&
|
|
291
|
+
transaction.transaction.lines.length > 0) {
|
|
292
|
+
transaction.transaction.lines.forEach((line) => {
|
|
293
|
+
if (!isAccountAndClassLine(line)) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const typedLine = line;
|
|
297
|
+
addIfPresent(resolveLineLevelPayee(typedLine.vendor, typedLine.customer));
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
// Transaction-level: localData first so unsaved grid edits make it
|
|
301
|
+
// into the match set, then the pre-edit server fields.
|
|
302
|
+
addIfPresent(localData?.vendor?.name);
|
|
303
|
+
addIfPresent(localData?.customer?.name);
|
|
304
|
+
addIfPresent(transaction?.vendorName);
|
|
305
|
+
addIfPresent(transaction?.customerName);
|
|
306
|
+
return Array.from(payees);
|
|
307
|
+
};
|
|
207
308
|
/**
|
|
208
309
|
* Resolve the comparable value for a transaction against a given filter field.
|
|
209
310
|
* Returns undefined when the transaction doesn't carry the field — callers
|
|
210
311
|
* decide whether absence counts as a match (for 'not_equal' it does).
|
|
211
312
|
*
|
|
212
|
-
* NOTE: the `'amount'`, `'category'`, and `'
|
|
213
|
-
* here. All
|
|
214
|
-
* `getLine*ForTransaction` helper and an early-return in
|
|
313
|
+
* NOTE: the `'amount'`, `'category'`, `'class'`, and `'payee'` fields
|
|
314
|
+
* are NOT handled here. All four use per-line matching via the
|
|
315
|
+
* corresponding `getLine*ForTransaction` helper and an early-return in
|
|
215
316
|
* `transactionMatchesCategory` — they would otherwise need to return a
|
|
216
317
|
* non-scalar value that doesn't fit this helper's contract. Cases are
|
|
217
318
|
* preserved so the switch stays exhaustive over `TransactionFilterCategoryField`.
|
|
@@ -224,46 +325,9 @@ const getCategoryValueForTransaction = (key, transaction) => {
|
|
|
224
325
|
return undefined;
|
|
225
326
|
}
|
|
226
327
|
case 'payee': {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
* 1. Matches what the row renders. `TransactionCategorizationListRow`
|
|
231
|
-
* reads `transaction.transactionReviewLocalData.vendor?.name` as
|
|
232
|
-
* its canonical payee source — not `transaction.vendorName`. If
|
|
233
|
-
* the user has manually picked a vendor in the grid (still
|
|
234
|
-
* unsaved), the row updates immediately but the pre-fix filter
|
|
235
|
-
* kept comparing against the stale top-level value, so filtering
|
|
236
|
-
* "Acme" against a row the user just relabeled to "Acme" would
|
|
237
|
-
* silently exclude it.
|
|
238
|
-
*
|
|
239
|
-
* 2. Brings payee in line with the other content-bearing fields.
|
|
240
|
-
* After this change, every field that has a localData mirror
|
|
241
|
-
* (`amount`, `category`, `class`, `payee`) reads localData
|
|
242
|
-
* first. The remaining fields (`payment_account_name`,
|
|
243
|
-
* `payment_account_type`) don't have a localData equivalent
|
|
244
|
-
* yet — when they do, this precedence becomes the universal
|
|
245
|
-
* rule.
|
|
246
|
-
*
|
|
247
|
-
* vendor is checked before customer at each level — TC transactions
|
|
248
|
-
* are overwhelmingly expense-side, so vendor is the common case. A
|
|
249
|
-
* transaction with both a vendor AND a customer locally set is rare
|
|
250
|
-
* and the existing behavior (vendor wins) is preserved.
|
|
251
|
-
*/
|
|
252
|
-
const localData = transaction.transactionLocalData?.transactionReviewLocalData;
|
|
253
|
-
const localVendorName = localData?.vendor?.name;
|
|
254
|
-
if (isNonEmptyString(localVendorName)) {
|
|
255
|
-
return localVendorName;
|
|
256
|
-
}
|
|
257
|
-
const localCustomerName = localData?.customer?.name;
|
|
258
|
-
if (isNonEmptyString(localCustomerName)) {
|
|
259
|
-
return localCustomerName;
|
|
260
|
-
}
|
|
261
|
-
if (isNonEmptyString(transaction.vendorName)) {
|
|
262
|
-
return transaction.vendorName;
|
|
263
|
-
}
|
|
264
|
-
if (isNonEmptyString(transaction.customerName)) {
|
|
265
|
-
return transaction.customerName;
|
|
266
|
-
}
|
|
328
|
+
// Handled per-line in `transactionMatchesCategory` — see
|
|
329
|
+
// `getLinePayeesForTransaction`. The transaction-level localData /
|
|
330
|
+
// top-level chain lives inside that helper's priority-3 fallback.
|
|
267
331
|
return undefined;
|
|
268
332
|
}
|
|
269
333
|
case 'payment_account_name': {
|
|
@@ -339,6 +403,29 @@ const matchAmount = (amount, category) => {
|
|
|
339
403
|
return op === 'not_equal' ? !matched : matched;
|
|
340
404
|
};
|
|
341
405
|
const transactionMatchesCategory = (transaction, category) => {
|
|
406
|
+
if (category.field === 'payee') {
|
|
407
|
+
// Per-line matching for payee — mirrors the amount / category /
|
|
408
|
+
// class pattern. The row's display reads `line.vendor?.name` for
|
|
409
|
+
// non-vendor-typed transactions (Journal Entries, etc.), so the
|
|
410
|
+
// filter has to look at line-level payee data too or it silently
|
|
411
|
+
// misses every JE whose vendor lives on the line. See the JSDoc
|
|
412
|
+
// on `getLinePayeesForTransaction` for the full precedence.
|
|
413
|
+
//
|
|
414
|
+
// `not_equal` reads the same way it does for category/class:
|
|
415
|
+
// "at least one line's payee is NOT one of the filter values".
|
|
416
|
+
// A JE with lines [Amazon, Acme] matches `payee != Amazon` via
|
|
417
|
+
// the Acme line; a bill or JE whose every line says "Amazon"
|
|
418
|
+
// does not match `payee != Amazon`.
|
|
419
|
+
const linePayees = getLinePayeesForTransaction(transaction);
|
|
420
|
+
if (linePayees.length === 0) {
|
|
421
|
+
return category.matchingOperator === 'not_equal';
|
|
422
|
+
}
|
|
423
|
+
const filterValuesAsStrings = new Set(category.values.map((v) => v.toString()));
|
|
424
|
+
return linePayees.some((name) => {
|
|
425
|
+
const inList = filterValuesAsStrings.has(name);
|
|
426
|
+
return category.matchingOperator === 'not_equal' ? !inList : inList;
|
|
427
|
+
});
|
|
428
|
+
}
|
|
342
429
|
if (category.field === 'category' || category.field === 'class') {
|
|
343
430
|
// Per-line matching for category / class — mirrors the amount
|
|
344
431
|
// 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,112 @@ 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
|
+
* All payee names that could legitimately identify a transaction, across
|
|
231
|
+
* every place TC stores payee data:
|
|
232
|
+
*
|
|
233
|
+
* • localData `lineItemById[*].vendor` / `.customer` — per-line
|
|
234
|
+
* vendor/customer the user has tagged in the grid. Populated for
|
|
235
|
+
* JEs (where the row reads per-line vendor) and sometimes for
|
|
236
|
+
* vendor-typed transactions too (e.g. when an AI vendor
|
|
237
|
+
* recommendation lands on a line).
|
|
238
|
+
* • Persisted `transaction.lines[*].vendor` / `.customer` — same
|
|
239
|
+
* fields on the server snapshot of each line.
|
|
240
|
+
* • localData transaction-level `vendor` / `customer` — what the row
|
|
241
|
+
* displays for vendor-typed transactions (Bills / Expenses /
|
|
242
|
+
* Checks).
|
|
243
|
+
* • Top-level `transaction.vendorName` / `customerName` — pre-edit
|
|
244
|
+
* server snapshot.
|
|
245
|
+
*
|
|
246
|
+
* The function collects names from ALL of these into a single set
|
|
247
|
+
* rather than picking exclusively from the highest-priority source.
|
|
248
|
+
* Two reasons:
|
|
249
|
+
*
|
|
250
|
+
* 1. Different transaction types put the displayed payee in different
|
|
251
|
+
* places. JEs put it on the line; Bills put it at the transaction
|
|
252
|
+
* level. A strict-precedence rule that returns early on the first
|
|
253
|
+
* non-empty source ends up missing the OTHER place when both have
|
|
254
|
+
* data with different values — which is exactly the
|
|
255
|
+
* production failure mode that motivated this fix (a Bill whose
|
|
256
|
+
* localData line item carried an AI-recommended vendor name was
|
|
257
|
+
* excluded because the recommendation won priority over the
|
|
258
|
+
* tx-level "Amazon" the row actually displayed).
|
|
259
|
+
* 2. The single-scalar predecessor missed JE rows entirely because
|
|
260
|
+
* it only looked at the tx level. Unioning the line-level sources
|
|
261
|
+
* in fixes that without re-introducing the "first source wins"
|
|
262
|
+
* brittleness.
|
|
263
|
+
*
|
|
264
|
+
* False positives are bounded: a transaction matches `payee = X` only
|
|
265
|
+
* if `X` literally appears in one of its stored payee fields. Stale
|
|
266
|
+
* recommendation data being included means the filter is slightly more
|
|
267
|
+
* lenient than what the row displays, not arbitrary.
|
|
268
|
+
*
|
|
269
|
+
* Returns `[]` when no source carries any name; the caller falls back
|
|
270
|
+
* to the absent-value contract (only `not_equal` matches; every other
|
|
271
|
+
* operator excludes).
|
|
272
|
+
*/
|
|
273
|
+
const getLinePayeesForTransaction = (transaction) => {
|
|
274
|
+
const localData = transaction?.transactionLocalData?.transactionReviewLocalData;
|
|
275
|
+
// Set used to dedupe — the same vendor name often appears in multiple
|
|
276
|
+
// sources (e.g. localData mirror of a persisted line) and we don't
|
|
277
|
+
// want it to be over-counted in the match list. Iteration order is
|
|
278
|
+
// insertion-order; `.some()` doesn't depend on order so this is fine.
|
|
279
|
+
const payees = new Set();
|
|
280
|
+
const addIfPresent = (name) => {
|
|
281
|
+
if (isNonEmptyString(name)) {
|
|
282
|
+
payees.add(name);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
// localData per-line vendor / customer
|
|
286
|
+
if (localData?.lineItemById != null) {
|
|
287
|
+
Object.values(localData.lineItemById).forEach((lineItem) => {
|
|
288
|
+
addIfPresent(resolveLineLevelPayee(lineItem?.vendor, lineItem?.customer));
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
// Persisted per-line vendor / customer (only on the three line
|
|
292
|
+
// shapes that actually carry them — see `isAccountAndClassLine`).
|
|
293
|
+
if (transaction?.transaction?.lines != null &&
|
|
294
|
+
transaction.transaction.lines.length > 0) {
|
|
295
|
+
transaction.transaction.lines.forEach((line) => {
|
|
296
|
+
if (!isAccountAndClassLine(line)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const typedLine = line;
|
|
300
|
+
addIfPresent(resolveLineLevelPayee(typedLine.vendor, typedLine.customer));
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// Transaction-level: localData first so unsaved grid edits make it
|
|
304
|
+
// into the match set, then the pre-edit server fields.
|
|
305
|
+
addIfPresent(localData?.vendor?.name);
|
|
306
|
+
addIfPresent(localData?.customer?.name);
|
|
307
|
+
addIfPresent(transaction?.vendorName);
|
|
308
|
+
addIfPresent(transaction?.customerName);
|
|
309
|
+
return Array.from(payees);
|
|
310
|
+
};
|
|
210
311
|
/**
|
|
211
312
|
* Resolve the comparable value for a transaction against a given filter field.
|
|
212
313
|
* Returns undefined when the transaction doesn't carry the field — callers
|
|
213
314
|
* decide whether absence counts as a match (for 'not_equal' it does).
|
|
214
315
|
*
|
|
215
|
-
* NOTE: the `'amount'`, `'category'`, and `'
|
|
216
|
-
* here. All
|
|
217
|
-
* `getLine*ForTransaction` helper and an early-return in
|
|
316
|
+
* NOTE: the `'amount'`, `'category'`, `'class'`, and `'payee'` fields
|
|
317
|
+
* are NOT handled here. All four use per-line matching via the
|
|
318
|
+
* corresponding `getLine*ForTransaction` helper and an early-return in
|
|
218
319
|
* `transactionMatchesCategory` — they would otherwise need to return a
|
|
219
320
|
* non-scalar value that doesn't fit this helper's contract. Cases are
|
|
220
321
|
* preserved so the switch stays exhaustive over `TransactionFilterCategoryField`.
|
|
@@ -227,46 +328,9 @@ const getCategoryValueForTransaction = (key, transaction) => {
|
|
|
227
328
|
return undefined;
|
|
228
329
|
}
|
|
229
330
|
case 'payee': {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
* 1. Matches what the row renders. `TransactionCategorizationListRow`
|
|
234
|
-
* reads `transaction.transactionReviewLocalData.vendor?.name` as
|
|
235
|
-
* its canonical payee source — not `transaction.vendorName`. If
|
|
236
|
-
* the user has manually picked a vendor in the grid (still
|
|
237
|
-
* unsaved), the row updates immediately but the pre-fix filter
|
|
238
|
-
* kept comparing against the stale top-level value, so filtering
|
|
239
|
-
* "Acme" against a row the user just relabeled to "Acme" would
|
|
240
|
-
* silently exclude it.
|
|
241
|
-
*
|
|
242
|
-
* 2. Brings payee in line with the other content-bearing fields.
|
|
243
|
-
* After this change, every field that has a localData mirror
|
|
244
|
-
* (`amount`, `category`, `class`, `payee`) reads localData
|
|
245
|
-
* first. The remaining fields (`payment_account_name`,
|
|
246
|
-
* `payment_account_type`) don't have a localData equivalent
|
|
247
|
-
* yet — when they do, this precedence becomes the universal
|
|
248
|
-
* rule.
|
|
249
|
-
*
|
|
250
|
-
* vendor is checked before customer at each level — TC transactions
|
|
251
|
-
* are overwhelmingly expense-side, so vendor is the common case. A
|
|
252
|
-
* transaction with both a vendor AND a customer locally set is rare
|
|
253
|
-
* and the existing behavior (vendor wins) is preserved.
|
|
254
|
-
*/
|
|
255
|
-
const localData = transaction.transactionLocalData?.transactionReviewLocalData;
|
|
256
|
-
const localVendorName = localData?.vendor?.name;
|
|
257
|
-
if (isNonEmptyString(localVendorName)) {
|
|
258
|
-
return localVendorName;
|
|
259
|
-
}
|
|
260
|
-
const localCustomerName = localData?.customer?.name;
|
|
261
|
-
if (isNonEmptyString(localCustomerName)) {
|
|
262
|
-
return localCustomerName;
|
|
263
|
-
}
|
|
264
|
-
if (isNonEmptyString(transaction.vendorName)) {
|
|
265
|
-
return transaction.vendorName;
|
|
266
|
-
}
|
|
267
|
-
if (isNonEmptyString(transaction.customerName)) {
|
|
268
|
-
return transaction.customerName;
|
|
269
|
-
}
|
|
331
|
+
// Handled per-line in `transactionMatchesCategory` — see
|
|
332
|
+
// `getLinePayeesForTransaction`. The transaction-level localData /
|
|
333
|
+
// top-level chain lives inside that helper's priority-3 fallback.
|
|
270
334
|
return undefined;
|
|
271
335
|
}
|
|
272
336
|
case 'payment_account_name': {
|
|
@@ -342,6 +406,29 @@ const matchAmount = (amount, category) => {
|
|
|
342
406
|
return op === 'not_equal' ? !matched : matched;
|
|
343
407
|
};
|
|
344
408
|
const transactionMatchesCategory = (transaction, category) => {
|
|
409
|
+
if (category.field === 'payee') {
|
|
410
|
+
// Per-line matching for payee — mirrors the amount / category /
|
|
411
|
+
// class pattern. The row's display reads `line.vendor?.name` for
|
|
412
|
+
// non-vendor-typed transactions (Journal Entries, etc.), so the
|
|
413
|
+
// filter has to look at line-level payee data too or it silently
|
|
414
|
+
// misses every JE whose vendor lives on the line. See the JSDoc
|
|
415
|
+
// on `getLinePayeesForTransaction` for the full precedence.
|
|
416
|
+
//
|
|
417
|
+
// `not_equal` reads the same way it does for category/class:
|
|
418
|
+
// "at least one line's payee is NOT one of the filter values".
|
|
419
|
+
// A JE with lines [Amazon, Acme] matches `payee != Amazon` via
|
|
420
|
+
// the Acme line; a bill or JE whose every line says "Amazon"
|
|
421
|
+
// does not match `payee != Amazon`.
|
|
422
|
+
const linePayees = getLinePayeesForTransaction(transaction);
|
|
423
|
+
if (linePayees.length === 0) {
|
|
424
|
+
return category.matchingOperator === 'not_equal';
|
|
425
|
+
}
|
|
426
|
+
const filterValuesAsStrings = new Set(category.values.map((v) => v.toString()));
|
|
427
|
+
return linePayees.some((name) => {
|
|
428
|
+
const inList = filterValuesAsStrings.has(name);
|
|
429
|
+
return category.matchingOperator === 'not_equal' ? !inList : inList;
|
|
430
|
+
});
|
|
431
|
+
}
|
|
345
432
|
if (category.field === 'category' || category.field === 'class') {
|
|
346
433
|
// Per-line matching for category / class — mirrors the amount
|
|
347
434
|
// 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-betaAR9",
|
|
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",
|