@dizzlkheinz/ynab-mcpb 0.18.2 → 0.18.3
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.
|
@@ -749,20 +749,31 @@ export async function handleCreateTransaction(ynabAPI, deltaCacheOrParams, knowl
|
|
|
749
749
|
return handleTransactionError(error, 'Failed to create transaction');
|
|
750
750
|
}
|
|
751
751
|
}
|
|
752
|
+
const BIG_TICKET_THRESHOLD_MILLIUNITS = 50000;
|
|
753
|
+
const COLLAPSE_THRESHOLD = 5;
|
|
754
|
+
const MAX_ITEMS_PER_MEMO = 5;
|
|
755
|
+
const MAX_MEMO_LENGTH = 150;
|
|
756
|
+
function truncateToLength(str, maxLength) {
|
|
757
|
+
if (str.length <= maxLength) {
|
|
758
|
+
return str;
|
|
759
|
+
}
|
|
760
|
+
const ellipsis = '...';
|
|
761
|
+
return str.substring(0, maxLength - ellipsis.length) + ellipsis;
|
|
762
|
+
}
|
|
752
763
|
function buildItemMemo(item) {
|
|
753
764
|
const quantitySuffix = item.quantity ? ` (x${item.quantity})` : '';
|
|
765
|
+
let result;
|
|
754
766
|
if (item.memo && item.memo.trim().length > 0) {
|
|
755
|
-
|
|
767
|
+
result = `${item.name}${quantitySuffix} - ${item.memo}`;
|
|
756
768
|
}
|
|
757
|
-
if (quantitySuffix) {
|
|
758
|
-
|
|
769
|
+
else if (quantitySuffix) {
|
|
770
|
+
result = `${item.name}${quantitySuffix}`;
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
result = item.name;
|
|
759
774
|
}
|
|
760
|
-
return
|
|
775
|
+
return truncateToLength(result, MAX_MEMO_LENGTH);
|
|
761
776
|
}
|
|
762
|
-
const BIG_TICKET_THRESHOLD_MILLIUNITS = 50000;
|
|
763
|
-
const COLLAPSE_THRESHOLD = 5;
|
|
764
|
-
const MAX_ITEMS_PER_MEMO = 5;
|
|
765
|
-
const MAX_MEMO_LENGTH = 150;
|
|
766
777
|
function applySmartCollapseLogic(categoryCalculations, taxMilliunits) {
|
|
767
778
|
const specialItems = [];
|
|
768
779
|
const remainingItemsByCategory = [];
|
|
@@ -891,6 +902,14 @@ function collapseItemsByCategory(categoryGroup) {
|
|
|
891
902
|
}
|
|
892
903
|
return subtransactions;
|
|
893
904
|
}
|
|
905
|
+
function truncateItemName(name, amountSuffix, maxLength) {
|
|
906
|
+
const ellipsis = '...';
|
|
907
|
+
const availableForName = maxLength - ellipsis.length - amountSuffix.length;
|
|
908
|
+
if (availableForName <= 0) {
|
|
909
|
+
return amountSuffix.substring(0, maxLength);
|
|
910
|
+
}
|
|
911
|
+
return name.substring(0, availableForName) + ellipsis + amountSuffix;
|
|
912
|
+
}
|
|
894
913
|
function buildCollapsedMemo(items) {
|
|
895
914
|
const parts = [];
|
|
896
915
|
let currentLength = 0;
|
|
@@ -899,8 +918,12 @@ function buildCollapsedMemo(items) {
|
|
|
899
918
|
if (!item)
|
|
900
919
|
continue;
|
|
901
920
|
const amount = milliunitsToAmount(item.amount_milliunits);
|
|
902
|
-
const
|
|
921
|
+
const amountSuffix = ` $${amount.toFixed(2)}`;
|
|
922
|
+
let itemStr = `${item.name}${amountSuffix}`;
|
|
903
923
|
const separator = i > 0 ? ', ' : '';
|
|
924
|
+
if (parts.length === 0 && itemStr.length > MAX_MEMO_LENGTH) {
|
|
925
|
+
itemStr = truncateItemName(item.name, amountSuffix, MAX_MEMO_LENGTH);
|
|
926
|
+
}
|
|
904
927
|
const testLength = currentLength + separator.length + itemStr.length;
|
|
905
928
|
if (parts.length > 0 && testLength + 4 > MAX_MEMO_LENGTH) {
|
|
906
929
|
break;
|
package/package.json
CHANGED
|
@@ -2107,6 +2107,89 @@ describe('transactionTools', () => {
|
|
|
2107
2107
|
expect(collapsedSub.memo).toContain('Cheap Item');
|
|
2108
2108
|
expect(collapsedSub.amount).toBe(25); // dry_run returns dollars
|
|
2109
2109
|
});
|
|
2110
|
+
|
|
2111
|
+
it('should truncate single very long item name in itemized mode', async () => {
|
|
2112
|
+
// Create a name that's way longer than 150 chars
|
|
2113
|
+
const veryLongName = 'A'.repeat(200);
|
|
2114
|
+
const params = {
|
|
2115
|
+
budget_id: 'budget-123',
|
|
2116
|
+
account_id: 'account-456',
|
|
2117
|
+
payee_name: 'Store',
|
|
2118
|
+
date: '2025-10-13',
|
|
2119
|
+
receipt_tax: 1.0,
|
|
2120
|
+
receipt_total: 11.0,
|
|
2121
|
+
categories: [
|
|
2122
|
+
{
|
|
2123
|
+
category_id: 'category-groceries',
|
|
2124
|
+
category_name: 'Groceries',
|
|
2125
|
+
items: [{ name: veryLongName, amount: 10.0 }],
|
|
2126
|
+
},
|
|
2127
|
+
],
|
|
2128
|
+
receipt_subtotal: 10.0,
|
|
2129
|
+
dry_run: true,
|
|
2130
|
+
} as const;
|
|
2131
|
+
|
|
2132
|
+
const result = await handleCreateReceiptSplitTransaction(mockYnabAPI, params);
|
|
2133
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
2134
|
+
|
|
2135
|
+
// Should have 2 subtransactions: 1 itemized + 1 tax
|
|
2136
|
+
// (only 1 item, so no collapse, but name gets truncated)
|
|
2137
|
+
expect(parsed.subtransactions).toHaveLength(2);
|
|
2138
|
+
|
|
2139
|
+
const itemMemo = parsed.subtransactions[0].memo;
|
|
2140
|
+
// Memo should be truncated to exactly 150 chars
|
|
2141
|
+
expect(itemMemo.length).toBeLessThanOrEqual(150);
|
|
2142
|
+
// Should contain truncation indicator
|
|
2143
|
+
expect(itemMemo).toContain('...');
|
|
2144
|
+
// Should start with part of the original name
|
|
2145
|
+
expect(itemMemo.startsWith('AAA')).toBe(true);
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
it('should truncate very long item name in collapsed mode while preserving amount', async () => {
|
|
2149
|
+
// Create a name that's way longer than 150 chars
|
|
2150
|
+
const veryLongName = 'B'.repeat(200);
|
|
2151
|
+
const params = {
|
|
2152
|
+
budget_id: 'budget-123',
|
|
2153
|
+
account_id: 'account-456',
|
|
2154
|
+
payee_name: 'Store',
|
|
2155
|
+
date: '2025-10-13',
|
|
2156
|
+
receipt_tax: 1.0,
|
|
2157
|
+
receipt_total: 61.0,
|
|
2158
|
+
categories: [
|
|
2159
|
+
{
|
|
2160
|
+
category_id: 'category-groceries',
|
|
2161
|
+
category_name: 'Groceries',
|
|
2162
|
+
items: [
|
|
2163
|
+
{ name: veryLongName, amount: 10.0 }, // This one has very long name
|
|
2164
|
+
{ name: 'Item2', amount: 10.0 },
|
|
2165
|
+
{ name: 'Item3', amount: 10.0 },
|
|
2166
|
+
{ name: 'Item4', amount: 10.0 },
|
|
2167
|
+
{ name: 'Item5', amount: 10.0 },
|
|
2168
|
+
{ name: 'Item6', amount: 10.0 },
|
|
2169
|
+
],
|
|
2170
|
+
},
|
|
2171
|
+
],
|
|
2172
|
+
receipt_subtotal: 60.0,
|
|
2173
|
+
dry_run: true,
|
|
2174
|
+
} as const;
|
|
2175
|
+
|
|
2176
|
+
const result = await handleCreateReceiptSplitTransaction(mockYnabAPI, params);
|
|
2177
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
2178
|
+
|
|
2179
|
+
// 6 items -> collapse mode, long item first creates its own subtransaction
|
|
2180
|
+
// that gets truncated
|
|
2181
|
+
expect(parsed.subtransactions.length).toBeGreaterThanOrEqual(2);
|
|
2182
|
+
|
|
2183
|
+
// First subtransaction should be the truncated long item
|
|
2184
|
+
const firstMemo = parsed.subtransactions[0].memo;
|
|
2185
|
+
expect(firstMemo.length).toBeLessThanOrEqual(150);
|
|
2186
|
+
// In collapsed mode, should preserve the amount
|
|
2187
|
+
expect(firstMemo).toContain('$10.00');
|
|
2188
|
+
// Should contain truncation indicator
|
|
2189
|
+
expect(firstMemo).toContain('...');
|
|
2190
|
+
// Should start with part of the original name
|
|
2191
|
+
expect(firstMemo.startsWith('BBB')).toBe(true);
|
|
2192
|
+
});
|
|
2110
2193
|
});
|
|
2111
2194
|
});
|
|
2112
2195
|
});
|
|
@@ -1112,29 +1112,43 @@ interface SubtransactionInput {
|
|
|
1112
1112
|
memo?: string;
|
|
1113
1113
|
}
|
|
1114
1114
|
|
|
1115
|
+
/**
|
|
1116
|
+
* Constants for smart collapse logic
|
|
1117
|
+
*/
|
|
1118
|
+
const BIG_TICKET_THRESHOLD_MILLIUNITS = 50000; // $50.00
|
|
1119
|
+
const COLLAPSE_THRESHOLD = 5; // Collapse if 5 or more remaining items
|
|
1120
|
+
const MAX_ITEMS_PER_MEMO = 5;
|
|
1121
|
+
const MAX_MEMO_LENGTH = 150;
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Truncates a string to fit within maxLength, adding ellipsis if truncated
|
|
1125
|
+
*/
|
|
1126
|
+
function truncateToLength(str: string, maxLength: number): string {
|
|
1127
|
+
if (str.length <= maxLength) {
|
|
1128
|
+
return str;
|
|
1129
|
+
}
|
|
1130
|
+
const ellipsis = '...';
|
|
1131
|
+
return str.substring(0, maxLength - ellipsis.length) + ellipsis;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1115
1134
|
function buildItemMemo(item: {
|
|
1116
1135
|
name: string;
|
|
1117
1136
|
quantity: number | undefined;
|
|
1118
1137
|
memo: string | undefined;
|
|
1119
1138
|
}): string | undefined {
|
|
1120
1139
|
const quantitySuffix = item.quantity ? ` (x${item.quantity})` : '';
|
|
1140
|
+
let result: string;
|
|
1121
1141
|
if (item.memo && item.memo.trim().length > 0) {
|
|
1122
|
-
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1142
|
+
result = `${item.name}${quantitySuffix} - ${item.memo}`;
|
|
1143
|
+
} else if (quantitySuffix) {
|
|
1144
|
+
result = `${item.name}${quantitySuffix}`;
|
|
1145
|
+
} else {
|
|
1146
|
+
result = item.name;
|
|
1126
1147
|
}
|
|
1127
|
-
|
|
1148
|
+
// Truncate to MAX_MEMO_LENGTH if needed
|
|
1149
|
+
return truncateToLength(result, MAX_MEMO_LENGTH);
|
|
1128
1150
|
}
|
|
1129
1151
|
|
|
1130
|
-
/**
|
|
1131
|
-
* Constants for smart collapse logic
|
|
1132
|
-
*/
|
|
1133
|
-
const BIG_TICKET_THRESHOLD_MILLIUNITS = 50000; // $50.00
|
|
1134
|
-
const COLLAPSE_THRESHOLD = 5; // Collapse if 5 or more remaining items
|
|
1135
|
-
const MAX_ITEMS_PER_MEMO = 5;
|
|
1136
|
-
const MAX_MEMO_LENGTH = 150;
|
|
1137
|
-
|
|
1138
1152
|
/**
|
|
1139
1153
|
* Applies smart collapse logic to receipt items according to the specification:
|
|
1140
1154
|
* 1. Extract special items (big ticket, returns, discounts)
|
|
@@ -1327,10 +1341,27 @@ function collapseItemsByCategory(categoryGroup: {
|
|
|
1327
1341
|
return subtransactions;
|
|
1328
1342
|
}
|
|
1329
1343
|
|
|
1344
|
+
/**
|
|
1345
|
+
* Truncates an item name to fit within available space
|
|
1346
|
+
* Preserves the amount suffix and adds "..." to indicate truncation
|
|
1347
|
+
*/
|
|
1348
|
+
function truncateItemName(name: string, amountSuffix: string, maxLength: number): string {
|
|
1349
|
+
const ellipsis = '...';
|
|
1350
|
+
// We need: truncatedName + ellipsis + amountSuffix <= maxLength
|
|
1351
|
+
const availableForName = maxLength - ellipsis.length - amountSuffix.length;
|
|
1352
|
+
|
|
1353
|
+
if (availableForName <= 0) {
|
|
1354
|
+
// Edge case: amount suffix alone is too long, just return what we can
|
|
1355
|
+
return amountSuffix.substring(0, maxLength);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
return name.substring(0, availableForName) + ellipsis + amountSuffix;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1330
1361
|
/**
|
|
1331
1362
|
* Builds a collapsed memo from a list of items
|
|
1332
1363
|
* Format: "Item1 $X.XX, Item2 $Y.YY, Item3 $Z.ZZ"
|
|
1333
|
-
* Truncates with "..." if needed
|
|
1364
|
+
* Truncates with "..." if needed (either individual items or the list)
|
|
1334
1365
|
*/
|
|
1335
1366
|
function buildCollapsedMemo(items: ReceiptCategoryCalculation['items'][0][]): string {
|
|
1336
1367
|
const parts: string[] = [];
|
|
@@ -1340,8 +1371,15 @@ function buildCollapsedMemo(items: ReceiptCategoryCalculation['items'][0][]): st
|
|
|
1340
1371
|
const item = items[i];
|
|
1341
1372
|
if (!item) continue;
|
|
1342
1373
|
const amount = milliunitsToAmount(item.amount_milliunits);
|
|
1343
|
-
const
|
|
1374
|
+
const amountSuffix = ` $${amount.toFixed(2)}`;
|
|
1375
|
+
let itemStr = `${item.name}${amountSuffix}`;
|
|
1344
1376
|
const separator = i > 0 ? ', ' : '';
|
|
1377
|
+
|
|
1378
|
+
// For the first item, check if it alone exceeds the limit
|
|
1379
|
+
if (parts.length === 0 && itemStr.length > MAX_MEMO_LENGTH) {
|
|
1380
|
+
itemStr = truncateItemName(item.name, amountSuffix, MAX_MEMO_LENGTH);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1345
1383
|
const testLength = currentLength + separator.length + itemStr.length;
|
|
1346
1384
|
|
|
1347
1385
|
// Check if adding this item would exceed limit
|