@mx-cartographer/experiences 9.3.2 → 9.3.4
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.
- package/CHANGELOG.md +11 -0
- package/dist/core/types/localization/SettingsCopy.d.ts +4 -0
- package/dist/core/utils/SettingsUtil.d.ts +1 -0
- package/dist/core/utils/SettingsUtil.es.js +21 -10
- package/dist/core/utils/SettingsUtil.es.js.map +1 -1
- package/dist/core/utils/TransactionUtils.es.js +31 -30
- package/dist/core/utils/TransactionUtils.es.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## [9.3.4] - 08-12-2026
|
|
2
|
+
|
|
3
|
+
- **FIXED** - Blank rows in "Manage Insights Notifications": 10 members of `InsightNotificationTypes` were rendered by the settings list but had no entry in `getProfileLabel`'s `labelMap`, so they displayed a working, toggleable notification row with an empty title (MEW2-2872)
|
|
4
|
+
- **ADDED** - `category_budget`, `goal_progress`, `spending_plan_budget_exceeded`, and `spending_plan_category_spending` settings copy keys; the other six newly-labeled types reuse existing copy
|
|
5
|
+
- **ADDED** - `src/core/utils/__tests__/SettingsUtil.test.ts` asserting every `InsightNotificationTypes` member resolves to a non-empty label, so enum/label-map drift fails CI instead of shipping a blank row
|
|
6
|
+
|
|
7
|
+
## [9.3.3] - 08-11-2026
|
|
8
|
+
|
|
9
|
+
- **FIXED** - Exported transaction CSV showed the post date one day early for users in Western (negative-offset) timezones because the date was rendered from the raw epoch in local time; the Date column now uses the UTC calendar day (MEW-3961)
|
|
10
|
+
|
|
1
11
|
## [9.3.2] - 08-11-2026
|
|
2
12
|
|
|
3
13
|
- **ADDED** - Reset password feature in the Settings profile tab, gated behind `display_password_options_in_settings` config flag, with client-side validation (minimum length, letter, number, match) and show/hide password toggles (MEW-3722)
|
|
@@ -24,6 +34,7 @@
|
|
|
24
34
|
- **CHANGED** - Pinned `msw` to an exact `2.15.0` to match the vendored `mockServiceWorker.js`, and `Beats.js` → `Beats.ts`
|
|
25
35
|
- **ADDED** - `src/core/api/__tests__/ApiClasses.test.ts` exercising the real `src/core/api/*` classes against MSW; coverage now includes `src/core/api/**` in the denominator (the MSW migration runs the real API classes) with the REVIEW.md floors kept at lines 85 / statements 85 / functions 80 / branches 78 (tests added to clear them rather than lowering), and `coverage.cleanOnRerun` set to `true` so a re-run can't satisfy the floors with stale data
|
|
26
36
|
- **CHANGED** - Dependency/structure hygiene: removed the unused `path` userland shim (configs use `node:path`); documented why `@typescript/typescript6` is retained (vite-plugin-dts needs the JS Compiler API that TS 7 native doesn't yet expose); consolidated the root `mockData/` fixtures into `mock/data/`
|
|
37
|
+
|
|
27
38
|
## [9.2.9] - 08-09-2026
|
|
28
39
|
|
|
29
40
|
- **FIXED** - Category selector drawer kept the previously expanded accordion and highlighted selection when reopened for a different transaction; it now resets and expands to the current transaction's category each time it opens
|
|
@@ -113,11 +113,13 @@ export interface SettingsCopy {
|
|
|
113
113
|
large_transaction: string;
|
|
114
114
|
account_balance_low: string;
|
|
115
115
|
bill_amount_not_standard: string;
|
|
116
|
+
category_budget: string;
|
|
116
117
|
category_spending: string;
|
|
117
118
|
credit_card_close_to_limit: string;
|
|
118
119
|
debt_payment_reminder: string;
|
|
119
120
|
debt_payment_reminder_v3: string;
|
|
120
121
|
duplicate_payment: string;
|
|
122
|
+
goal_progress: string;
|
|
121
123
|
monthly_spend_comparison: string;
|
|
122
124
|
monthly_spend_to_income_comparison: string;
|
|
123
125
|
monthly_subscription_aggregate: string;
|
|
@@ -125,6 +127,8 @@ export interface SettingsCopy {
|
|
|
125
127
|
save_an_extra_100_dollars: string;
|
|
126
128
|
savings_opportunity: string;
|
|
127
129
|
set_up_direct_deposit: string;
|
|
130
|
+
spending_plan_budget_exceeded: string;
|
|
131
|
+
spending_plan_category_spending: string;
|
|
128
132
|
switch_direct_deposit: string;
|
|
129
133
|
uncategorized_transactions: string;
|
|
130
134
|
unified_deposit: string;
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { Account, ClientCommunicationProfile, DetailedNotificationProfile, NotificationProfile, SettingsCopy, User, UserCommunicationProfile } from '../types';
|
|
2
|
+
export declare const PARENT_ENTITY = "PARENT-ENTITY";
|
|
2
3
|
export declare const augmentNotificationProfiles: (accounts: Account[], clientCommunicationsProfile: ClientCommunicationProfile, copy: SettingsCopy, profiles: NotificationProfile[], userCommunicationsProfile: UserCommunicationProfile, user: User) => DetailedNotificationProfile[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AccountNotificationTypes as n, BudgetNotificationTypes as u, InsightNotificationTypes as t } from "../types/Notification.es.js";
|
|
2
|
-
var
|
|
2
|
+
var P = "PARENT-ENTITY", S = (o, e) => ({
|
|
3
3
|
[u.BudgetExceeded]: e.notification_budget_exceeded,
|
|
4
4
|
[u.BudgetOffTarget]: e.notification_budget_off_target,
|
|
5
5
|
[n.DebtPaymentReminder]: e.notification_debt_payment_reminder,
|
|
@@ -10,11 +10,17 @@ var g = "PARENT-ENTITY", D = (o, e) => ({
|
|
|
10
10
|
[n.TransactionFeeCharge]: e.notification_transaction_fee,
|
|
11
11
|
[n.InternationalTransaction]: e.notification_transaction_is_international,
|
|
12
12
|
[t.AccountBalanceLow]: e.account_balance_low,
|
|
13
|
+
[t.AccountDepositHigh]: e.notification_large_deposit,
|
|
13
14
|
[t.BillAmountNotStandard]: e.bill_amount_not_standard,
|
|
15
|
+
[t.BudgetExceeded]: e.notification_budget_exceeded,
|
|
16
|
+
[t.BudgetOffTarget]: e.notification_budget_off_target,
|
|
17
|
+
[t.CategoryBudget]: e.category_budget,
|
|
14
18
|
[t.CategorySpending]: e.category_spending,
|
|
15
19
|
[t.CreditCardCloseToLimit]: e.credit_card_close_to_limit,
|
|
16
20
|
[t.DebtPaymentReminder]: e.debt_payment_reminder,
|
|
21
|
+
[t.DebtPaymentReminderV3]: e.debt_payment_reminder_v3,
|
|
17
22
|
[t.DuplicatePayment]: e.duplicate_payment,
|
|
23
|
+
[t.GoalProgress]: e.goal_progress,
|
|
18
24
|
[t.MonthlySpendComparison]: e.monthly_spend_comparison,
|
|
19
25
|
[t.MonthlySpendToIncomeComparison]: e.monthly_spend_to_income_comparison,
|
|
20
26
|
[t.MonthlySubscriptionAggregate]: e.monthly_subscription_aggregate,
|
|
@@ -22,7 +28,11 @@ var g = "PARENT-ENTITY", D = (o, e) => ({
|
|
|
22
28
|
[t.SaveAnExtra100Dollars]: e.save_an_extra_100_dollars,
|
|
23
29
|
[t.SavingsOpportunity]: e.savings_opportunity,
|
|
24
30
|
[t.SetUpDirectDeposit]: e.set_up_direct_deposit,
|
|
31
|
+
[t.SpendingPlanBudgetExceeded]: e.spending_plan_budget_exceeded,
|
|
32
|
+
[t.SpendingPlanCategorySpending]: e.spending_plan_category_spending,
|
|
25
33
|
[t.SwitchDirectDeposit]: e.switch_direct_deposit,
|
|
34
|
+
[t.TransactionFeeCharge]: e.notification_transaction_fee,
|
|
35
|
+
[t.TransactionIsInternational]: e.notification_transaction_is_international,
|
|
26
36
|
[t.UncategorizedTransactions]: e.uncategorized_transactions,
|
|
27
37
|
[t.UnifiedDeposit]: e.unified_deposit,
|
|
28
38
|
[t.UpcomingBill]: e.upcoming_bill,
|
|
@@ -32,24 +42,25 @@ var g = "PARENT-ENTITY", D = (o, e) => ({
|
|
|
32
42
|
[t.TransactionExpenseLarge]: e.large_transaction,
|
|
33
43
|
[t.PromotionalCampaignMessages]: e.promotional_campaign_messages,
|
|
34
44
|
[t.TransparentOverdraft]: e.transparent_overdraft
|
|
35
|
-
})[o] || "", A = (o, e,
|
|
36
|
-
const
|
|
37
|
-
for (const i of l.filter((
|
|
38
|
-
const
|
|
39
|
-
|
|
45
|
+
})[o] || "", A = (o, e, _, l, m, g) => {
|
|
46
|
+
const c = [], { email_notifications_enabled: f, sms_notifications_enabled: p, push_notifications_enabled: h } = e, { opted_out_of_email_notifications: b, opted_out_of_sms_notifications: T, opted_out_of_push_notifications: v } = m;
|
|
47
|
+
for (const i of l.filter((r) => r.entity_guid === P)) {
|
|
48
|
+
const r = S(i.notification_type, _), E = l.filter((a) => a.entity_guid !== "PARENT-ENTITY" && a.notification_type === i.notification_type && o.some((d) => d.guid === a.entity_guid)), s = [];
|
|
49
|
+
f && !b && i.email_channel && g.email && s.push(_.delivery_method_email_label), p && !T && i.sms_channel && g.phone && s.push(_.delivery_method_sms_label), h && !v && i.push_channel && s.push(_.delivery_method_push_label), c.push({
|
|
40
50
|
...i,
|
|
41
51
|
children: E.map((a) => ({
|
|
42
52
|
...a,
|
|
43
53
|
account: o.find((d) => d.guid === a.entity_guid),
|
|
44
|
-
label:
|
|
54
|
+
label: r
|
|
45
55
|
})),
|
|
46
|
-
deliveryMethodsLabel:
|
|
47
|
-
label:
|
|
56
|
+
deliveryMethodsLabel: s.join(", "),
|
|
57
|
+
label: r
|
|
48
58
|
});
|
|
49
59
|
}
|
|
50
|
-
return
|
|
60
|
+
return c;
|
|
51
61
|
};
|
|
52
62
|
export {
|
|
63
|
+
P as PARENT_ENTITY,
|
|
53
64
|
A as augmentNotificationProfiles
|
|
54
65
|
};
|
|
55
66
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SettingsUtil.es.js","names":[],"sources":["../../../src/core/utils/SettingsUtil.ts"],"sourcesContent":["import {\n type Account,\n type ClientCommunicationProfile,\n type DetailedNotificationProfile,\n type NotificationProfile,\n type SettingsCopy,\n type User,\n type UserCommunicationProfile,\n AccountNotificationTypes,\n BudgetNotificationTypes,\n InsightNotificationTypes,\n} from '../types'\n\
|
|
1
|
+
{"version":3,"file":"SettingsUtil.es.js","names":[],"sources":["../../../src/core/utils/SettingsUtil.ts"],"sourcesContent":["import {\n type Account,\n type ClientCommunicationProfile,\n type DetailedNotificationProfile,\n type NotificationProfile,\n type SettingsCopy,\n type User,\n type UserCommunicationProfile,\n AccountNotificationTypes,\n BudgetNotificationTypes,\n InsightNotificationTypes,\n} from '../types'\n\nexport const PARENT_ENTITY = 'PARENT-ENTITY'\n\nconst getProfileLabel = (notificationType: number, copy: SettingsCopy) => {\n const labelMap: Record<number, string> = {\n [BudgetNotificationTypes.BudgetExceeded]: copy.notification_budget_exceeded,\n [BudgetNotificationTypes.BudgetOffTarget]: copy.notification_budget_off_target,\n [AccountNotificationTypes.DebtPaymentReminder]: copy.notification_debt_payment_reminder,\n [AccountNotificationTypes.AccountBalanceLow]: copy.notification_low_balance,\n [AccountNotificationTypes.AccountDepositLargePosted]: copy.notification_large_deposit,\n [AccountNotificationTypes.AccountDepositLarge]: copy.notification_large_deposit,\n [AccountNotificationTypes.TransactionExpenseLarge]: copy.notification_large_expense,\n [AccountNotificationTypes.TransactionFeeCharge]: copy.notification_transaction_fee,\n [AccountNotificationTypes.InternationalTransaction]:\n copy.notification_transaction_is_international,\n [InsightNotificationTypes.AccountBalanceLow]: copy.account_balance_low,\n [InsightNotificationTypes.AccountDepositHigh]: copy.notification_large_deposit,\n [InsightNotificationTypes.BillAmountNotStandard]: copy.bill_amount_not_standard,\n [InsightNotificationTypes.BudgetExceeded]: copy.notification_budget_exceeded,\n [InsightNotificationTypes.BudgetOffTarget]: copy.notification_budget_off_target,\n [InsightNotificationTypes.CategoryBudget]: copy.category_budget,\n [InsightNotificationTypes.CategorySpending]: copy.category_spending,\n [InsightNotificationTypes.CreditCardCloseToLimit]: copy.credit_card_close_to_limit,\n [InsightNotificationTypes.DebtPaymentReminder]: copy.debt_payment_reminder,\n [InsightNotificationTypes.DebtPaymentReminderV3]: copy.debt_payment_reminder_v3,\n [InsightNotificationTypes.DuplicatePayment]: copy.duplicate_payment,\n [InsightNotificationTypes.GoalProgress]: copy.goal_progress,\n [InsightNotificationTypes.MonthlySpendComparison]: copy.monthly_spend_comparison,\n [InsightNotificationTypes.MonthlySpendToIncomeComparison]:\n copy.monthly_spend_to_income_comparison,\n [InsightNotificationTypes.MonthlySubscriptionAggregate]: copy.monthly_subscription_aggregate,\n [InsightNotificationTypes.MonthlyTopMerchantSpending]: copy.monthly_top_merchant_spending,\n [InsightNotificationTypes.SaveAnExtra100Dollars]: copy.save_an_extra_100_dollars,\n [InsightNotificationTypes.SavingsOpportunity]: copy.savings_opportunity,\n [InsightNotificationTypes.SetUpDirectDeposit]: copy.set_up_direct_deposit,\n [InsightNotificationTypes.SpendingPlanBudgetExceeded]: copy.spending_plan_budget_exceeded,\n [InsightNotificationTypes.SpendingPlanCategorySpending]: copy.spending_plan_category_spending,\n [InsightNotificationTypes.SwitchDirectDeposit]: copy.switch_direct_deposit,\n [InsightNotificationTypes.TransactionFeeCharge]: copy.notification_transaction_fee,\n [InsightNotificationTypes.TransactionIsInternational]:\n copy.notification_transaction_is_international,\n [InsightNotificationTypes.UncategorizedTransactions]: copy.uncategorized_transactions,\n [InsightNotificationTypes.UnifiedDeposit]: copy.unified_deposit,\n [InsightNotificationTypes.UpcomingBill]: copy.upcoming_bill,\n [InsightNotificationTypes.WeekendSpendingSummary]: copy.weekend_spending_summary,\n [InsightNotificationTypes.WeeklyNewMerchants]: copy.weekly_new_merchants,\n [InsightNotificationTypes.DiscoveredAccounts]: copy.discovered_account,\n [InsightNotificationTypes.TransactionExpenseLarge]: copy.large_transaction,\n [InsightNotificationTypes.PromotionalCampaignMessages]: copy.promotional_campaign_messages,\n [InsightNotificationTypes.TransparentOverdraft]: copy.transparent_overdraft,\n }\n\n return labelMap[notificationType] || ''\n}\n\nexport const augmentNotificationProfiles = (\n accounts: Account[],\n clientCommunicationsProfile: ClientCommunicationProfile,\n copy: SettingsCopy,\n profiles: NotificationProfile[],\n userCommunicationsProfile: UserCommunicationProfile,\n user: User,\n) => {\n const detailedProfiles = [] as DetailedNotificationProfile[]\n const {\n email_notifications_enabled: clientEmailEnabled,\n sms_notifications_enabled: clientSmsEnabled,\n push_notifications_enabled: clientPushEnabled,\n } = clientCommunicationsProfile\n\n const {\n opted_out_of_email_notifications: userEmailOptedOut,\n opted_out_of_sms_notifications: userSmsOptedOut,\n opted_out_of_push_notifications: userPushOptedOut,\n } = userCommunicationsProfile\n\n for (const profile of profiles.filter((profile) => profile.entity_guid === PARENT_ENTITY)) {\n const label = getProfileLabel(profile.notification_type, copy)\n const children = profiles.filter(\n (child) =>\n child.entity_guid !== PARENT_ENTITY &&\n child.notification_type === profile.notification_type &&\n accounts.some((account) => account.guid === child.entity_guid),\n )\n\n const deliveryMethods = [] as string[]\n if (clientEmailEnabled && !userEmailOptedOut && profile.email_channel && user.email) {\n deliveryMethods.push(copy.delivery_method_email_label)\n }\n if (clientSmsEnabled && !userSmsOptedOut && profile.sms_channel && user.phone) {\n deliveryMethods.push(copy.delivery_method_sms_label)\n }\n if (clientPushEnabled && !userPushOptedOut && profile.push_channel) {\n deliveryMethods.push(copy.delivery_method_push_label)\n }\n\n detailedProfiles.push({\n ...profile,\n children: children.map((child) => ({\n ...child,\n account: accounts.find((a) => a.guid === child.entity_guid),\n label,\n })),\n deliveryMethodsLabel: deliveryMethods.join(', '),\n label,\n })\n }\n\n return detailedProfiles\n}\n"],"mappings":"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"}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { CategoryGuidToName as
|
|
1
|
+
import { CategoryGuidToName as h, CategoryGuids as y } from "../constants/Category.es.js";
|
|
2
2
|
import { DATE_FORMATS_INTL as m, formatDate as f } from "../constants/DateFormats.es.js";
|
|
3
|
-
import { TransactionStatus as
|
|
4
|
-
import { formatCurrency as
|
|
3
|
+
import { TransactionStatus as C, TransactionType as v } from "../types/Transaction.es.js";
|
|
4
|
+
import { formatCurrency as A } from "./NumberFormatting.es.js";
|
|
5
|
+
import { fromUnixUTC as b } from "./DateUtil.es.js";
|
|
5
6
|
import { TABLE_CONST as E } from "../constants/Table.es.js";
|
|
6
7
|
import { COLUMN_NAMES as l } from "../constants/Transaction.es.js";
|
|
7
8
|
import { format as O } from "date-fns/format";
|
|
8
9
|
import { startOfMonth as S } from "date-fns/startOfMonth";
|
|
9
10
|
import { startOfToday as D } from "date-fns/startOfToday";
|
|
10
11
|
import { endOfToday as M } from "date-fns/endOfToday";
|
|
11
|
-
import { fromUnixTime as
|
|
12
|
+
import { fromUnixTime as U } from "date-fns/fromUnixTime";
|
|
12
13
|
import { getUnixTime as _ } from "date-fns/getUnixTime";
|
|
13
|
-
var
|
|
14
|
+
var X = (r, t, n, a = []) => {
|
|
14
15
|
const s = t.reduce((o, e) => ({
|
|
15
16
|
...o,
|
|
16
17
|
[e.guid]: e
|
|
@@ -37,14 +38,14 @@ var K = (r, t, n, a = []) => {
|
|
|
37
38
|
accountIsClosed: e?.is_closed,
|
|
38
39
|
accountIsHidden: e?.is_hidden,
|
|
39
40
|
category: u?.name || "Uncategorized",
|
|
40
|
-
isIncome: o.top_level_category_guid ===
|
|
41
|
-
isPending: o.feed_status ===
|
|
41
|
+
isIncome: o.top_level_category_guid === y.INCOME,
|
|
42
|
+
isPending: o.feed_status === C.PENDING,
|
|
42
43
|
number: e?.account_number || "",
|
|
43
44
|
payee: o.description,
|
|
44
45
|
tags: c
|
|
45
46
|
};
|
|
46
47
|
});
|
|
47
|
-
}, N = "uncategorized",
|
|
48
|
+
}, N = "uncategorized", ee = (r, t, n, a, s, d) => {
|
|
48
49
|
const i = I(r, s, d), o = L(i, n, a);
|
|
49
50
|
return t.forEach((e) => {
|
|
50
51
|
const u = o.findIndex((g) => g.guid === e), c = d.filter((g) => g.parent_guid === e);
|
|
@@ -58,8 +59,8 @@ var K = (r, t, n, a = []) => {
|
|
|
58
59
|
return n.filter((e) => o ? !e.has_been_split : !e.parent_guid).filter((e) => !!a.find((u) => u === e.account_guid)).filter((e) => e.date >= _(s.start) && e.date <= _(s.end)).filter(d).filter((e) => {
|
|
59
60
|
if (i === N && e.has_been_split) return !1;
|
|
60
61
|
if (/^spl/i.test(i) && (e.has_been_split || e.parent_guid)) return !0;
|
|
61
|
-
const u = e.tags.some((g) => p(i, t.find((
|
|
62
|
-
return p(i, e.payee) || p(i, e.feed_description) || p(i, e.category) || p(i, f(c, m.MONTH)) || p(i, f(c, m.DAY)) || p(i, f(c, m.FULL_MONTH_DAY)) || p(i, f(c, m.MONTH_DAY_YEAR)) || p(i, O(c, "do")) || p(i, e.account) || p(i,
|
|
62
|
+
const u = e.tags.some((g) => p(i, t.find((T) => T.guid === g)?.name)), c = U(e.date);
|
|
63
|
+
return p(i, e.payee) || p(i, e.feed_description) || p(i, e.category) || p(i, f(c, m.MONTH)) || p(i, f(c, m.DAY)) || p(i, f(c, m.FULL_MONTH_DAY)) || p(i, f(c, m.MONTH_DAY_YEAR)) || p(i, O(c, "do")) || p(i, e.account) || p(i, A(e.amount, "0,0.00")) || p(i, e.memo) || u;
|
|
63
64
|
});
|
|
64
65
|
}, L = (r, t, n) => [...r].sort((a, s) => {
|
|
65
66
|
const d = t, i = (a[d] || "") > (s[d] || "");
|
|
@@ -69,45 +70,45 @@ var K = (r, t, n, a = []) => {
|
|
|
69
70
|
const n = r.toLowerCase().split(" "), a = t?.toString().toLowerCase() ?? "";
|
|
70
71
|
return n.every((s) => a.indexOf(s) > -1);
|
|
71
72
|
};
|
|
72
|
-
function
|
|
73
|
+
function R(r) {
|
|
73
74
|
return new Set(r.filter((t) => t.parent_guid !== void 0).map((t) => t.parent_guid));
|
|
74
75
|
}
|
|
75
|
-
function
|
|
76
|
+
function w(r) {
|
|
76
77
|
return r.reduce((t, n) => (n.parent_guid && (t[n.parent_guid] || (t[n.parent_guid] = []), t[n.parent_guid].push(n)), t), {});
|
|
77
78
|
}
|
|
78
|
-
function
|
|
79
|
-
const n =
|
|
79
|
+
function te(r, t) {
|
|
80
|
+
const n = R(t), a = w(t), s = /* @__PURE__ */ new Set(), d = [];
|
|
80
81
|
for (const i of r) n.has(i.guid) ? (a[i.guid] ?? []).slice().sort((o, e) => Math.abs(e.amount) - Math.abs(o.amount)).forEach((o) => {
|
|
81
82
|
s.has(o.guid) || (d.push(o), s.add(o.guid));
|
|
82
83
|
}) : s.has(i.guid) || (d.push(i), s.add(i.guid));
|
|
83
84
|
return d;
|
|
84
85
|
}
|
|
85
|
-
var
|
|
86
|
-
const n =
|
|
86
|
+
var re = (r, t) => {
|
|
87
|
+
const n = P(j(r, t)), a = new Blob([n], { type: "text/csv;charset=utf-8;" }), s = document.createElement("a");
|
|
87
88
|
s.href = URL.createObjectURL(a), s.setAttribute("download", "transactions.csv"), document.body.appendChild(s), s.click(), document.body.removeChild(s), URL.revokeObjectURL(s.href);
|
|
88
|
-
},
|
|
89
|
-
Date: f(
|
|
90
|
-
Description:
|
|
89
|
+
}, j = (r, t) => r.map((n) => ({
|
|
90
|
+
Date: f(b(n.date), m.YEAR_MONTH_DAY),
|
|
91
|
+
Description: x(n),
|
|
91
92
|
"Original Description": n.feed_description,
|
|
92
93
|
Amount: `${n.amount}`,
|
|
93
|
-
Type: n.transaction_type ===
|
|
94
|
-
"Parent Category":
|
|
94
|
+
Type: n.transaction_type === v.CREDIT ? "Credit" : "Debit",
|
|
95
|
+
"Parent Category": h[n.top_level_category_guid || ""],
|
|
95
96
|
Category: n.category,
|
|
96
97
|
Account: n.account,
|
|
97
|
-
Tags:
|
|
98
|
+
Tags: B(n, t),
|
|
98
99
|
Memo: n.memo,
|
|
99
100
|
Pending: n.isPending ? "TRUE" : "FALSE"
|
|
100
|
-
})),
|
|
101
|
-
`) + r.map((t) => Object.values(t).map(
|
|
102
|
-
`),
|
|
101
|
+
})), G = (r) => r ? /[,"\n\r]/.test(r) ? `"${r.replace(/"/g, '""')}"` : r : "", P = (r) => r.length === 0 ? "" : Object.keys(r[0]).join(",").concat(`\r
|
|
102
|
+
`) + r.map((t) => Object.values(t).map(G).join(",")).join(`\r
|
|
103
|
+
`), x = (r) => {
|
|
103
104
|
let t = r.parent_guid ? "Split: " : "";
|
|
104
105
|
return t += r.is_hidden ? "(Excluded) " : "", t += r.description, t;
|
|
105
|
-
},
|
|
106
|
+
}, B = (r, t) => r.tags.map((n) => t.filter((a) => a.guid === n)[0]?.name ?? "").join(" ");
|
|
106
107
|
export {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
X as buildDetailedTransactions,
|
|
109
|
+
re as exportTransactionsToCSV,
|
|
110
|
+
te as flattenTransactionsWithSplits,
|
|
111
|
+
ee as sortAndFilterTransactions
|
|
111
112
|
};
|
|
112
113
|
|
|
113
114
|
//# sourceMappingURL=TransactionUtils.es.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TransactionUtils.es.js","names":[],"sources":["../../../src/core/utils/TransactionUtils.ts"],"sourcesContent":["import { endOfToday } from 'date-fns/endOfToday'\nimport { format } from 'date-fns/format'\nimport { fromUnixTime } from 'date-fns/fromUnixTime'\nimport { getUnixTime } from 'date-fns/getUnixTime'\nimport { startOfMonth } from 'date-fns/startOfMonth'\nimport { startOfToday } from 'date-fns/startOfToday'\n\nimport { TABLE_CONST } from '../constants/Table'\nimport {\n CategoryGuids,\n CategoryGuidToName,\n DATE_FORMATS_INTL,\n formatDate,\n ORDINAL_DATE_FORMAT,\n} from '../constants'\nimport { COLUMN_NAMES } from '../constants/Transaction'\nimport { TransactionStatus, TransactionType } from '../types'\nimport type { Account, Category, Tag, Tagging, Transaction } from '../types'\n\nimport { type TransactionFilter } from '../stores/TransactionStore'\n\nimport { formatCurrency } from './NumberFormatting'\n\ninterface AccountsMap {\n [key: string]: Account\n}\n\ninterface CategoriesMap {\n [key: string]: Category\n}\n\ninterface TaggingsMap {\n [key: string]: string[]\n}\n\nexport const buildDetailedTransactions = (\n transactions: Transaction[],\n accounts: Account[],\n categories: Category[],\n taggings: Tagging[] = [],\n) => {\n const accountsMap = accounts.reduce((acc: AccountsMap, account: Account) => {\n return { ...acc, [account.guid]: account }\n }, {})\n\n const categoriesMap = categories.reduce((acc: CategoriesMap, category: Category) => {\n return { ...acc, [category.guid]: category }\n }, {})\n\n const taggingsMap = taggings.reduce((acc: TaggingsMap, tagging: Tagging) => {\n const { transaction_guid, tag_guid } = tagging\n\n if (!acc[transaction_guid]) {\n return {\n ...acc,\n [transaction_guid]: [tag_guid],\n }\n }\n const tags = [...acc[transaction_guid], tag_guid] as string[]\n\n return {\n ...acc,\n [transaction_guid]: tags,\n }\n }, {})\n\n return transactions.map((transaction: Transaction) => {\n const account = accountsMap[transaction.account_guid]\n const category = transaction.category_guid ? categoriesMap[transaction.category_guid] : null\n const tags = taggingsMap[transaction.guid] ?? []\n\n return {\n ...transaction,\n account: account?.user_name || 'Unknown',\n accountIsClosed: account?.is_closed,\n accountIsHidden: account?.is_hidden,\n category: category?.name || 'Uncategorized',\n isIncome: transaction.top_level_category_guid === CategoryGuids.INCOME,\n isPending: transaction.feed_status === TransactionStatus.PENDING,\n number: account?.account_number || '',\n payee: transaction.description,\n tags,\n }\n })\n}\n\nconst UNCATEGORIZED_TERM = 'uncategorized'\n\nexport const sortAndFilterTransactions = (\n filter: TransactionFilter,\n expandedSplits: string[],\n sortColumn: string,\n sortDirection: string,\n tags: Tag[],\n transactions: Transaction[],\n) => {\n const filteredTransactions = filterTransactions(filter, tags, transactions)\n\n const sortedTransactions = sortTransactions(filteredTransactions, sortColumn, sortDirection)\n\n expandedSplits.forEach((guid) => {\n const index = sortedTransactions.findIndex((t) => t.guid === guid)\n const children = transactions.filter((t) => t.parent_guid === guid)\n sortedTransactions.splice(index + 1, 0, ...children)\n })\n return sortedTransactions\n}\n\nconst filterTransactions = (\n filter: TransactionFilter,\n tags: Tag[],\n transactions: Transaction[],\n) => {\n const {\n accounts = [],\n dateRange = { start: startOfMonth(startOfToday()), end: endOfToday() },\n custom = () => true,\n searchTerm: searchFilter = '',\n showSplits = false,\n } = filter\n\n return (\n transactions\n // Filters out parent of split transactions when showSplits is true and filters out split\n // transaction children when showSplits is false\n .filter((t) => (showSplits ? !t.has_been_split : !t.parent_guid))\n // Filter for selected accounts\n .filter((t) => Boolean(accounts.find((guid) => guid === t.account_guid)))\n // Filter for Date Range\n .filter((t) => t.date >= getUnixTime(dateRange.start) && t.date <= getUnixTime(dateRange.end))\n // Custom filter\n .filter(custom)\n // Search filter\n .filter((transaction) => {\n // Don't return uncategorized split transactions\n if (searchFilter === UNCATEGORIZED_TERM && transaction.has_been_split) {\n return false\n }\n\n // Split check - When searching for \"Splits\" and transaction is a split or is a parent of a split(s)\n const splitPattern = /^spl/i\n if (\n splitPattern.test(searchFilter) &&\n (transaction.has_been_split || Boolean(transaction.parent_guid))\n ) {\n return true\n }\n\n // Check transactions tags\n const isTagIncludedInSearch = transaction.tags.some((guid) =>\n isIncludedInSearch(searchFilter, tags.find((tag) => tag.guid === guid)?.name),\n )\n const date = fromUnixTime(transaction.date)\n return (\n isIncludedInSearch(searchFilter, transaction.payee) ||\n isIncludedInSearch(searchFilter, transaction.feed_description) ||\n isIncludedInSearch(searchFilter, transaction.category) ||\n isIncludedInSearch(searchFilter, formatDate(date, DATE_FORMATS_INTL.MONTH)) ||\n isIncludedInSearch(searchFilter, formatDate(date, DATE_FORMATS_INTL.DAY)) ||\n isIncludedInSearch(searchFilter, formatDate(date, DATE_FORMATS_INTL.FULL_MONTH_DAY)) ||\n isIncludedInSearch(searchFilter, formatDate(date, DATE_FORMATS_INTL.MONTH_DAY_YEAR)) ||\n isIncludedInSearch(searchFilter, format(date, ORDINAL_DATE_FORMAT)) ||\n isIncludedInSearch(searchFilter, transaction.account) ||\n isIncludedInSearch(searchFilter, formatCurrency(transaction.amount, '0,0.00')) ||\n isIncludedInSearch(searchFilter, transaction.memo) ||\n isTagIncludedInSearch\n )\n })\n )\n}\n\nconst sortTransactions = (\n transactions: Transaction[],\n sortColumn: string,\n sortDirection: string,\n) => {\n return [...transactions].sort((a: Transaction, b: Transaction) => {\n const column = sortColumn as keyof Transaction\n const sortCompare = (a[column] || '') > (b[column] || '')\n\n let sortASC = sortCompare ? -1 : 1\n let sortDESC = sortCompare ? 1 : -1\n\n if (sortColumn === COLUMN_NAMES.DATE || sortColumn === COLUMN_NAMES.AMOUNT) {\n sortASC = sortCompare ? 1 : -1\n sortDESC = sortCompare ? -1 : 1\n }\n\n return sortDirection === TABLE_CONST.ASC ? sortASC : sortDESC\n })\n}\n\n/**\n * Determines if a value is included within the terms set in the search bar\n */\nconst isIncludedInSearch = (searchFilter: string, value: string | null | undefined) => {\n const lowerCasedTerms = searchFilter.toLowerCase().split(' ')\n const lowerCasedValue = value?.toString().toLowerCase() ?? ''\n\n return lowerCasedTerms.every((filter) => lowerCasedValue.indexOf(filter) > -1)\n}\n\n// TODO: Change the way formatting data is exported in order to handle localization\ntype FormattedTransaction = {\n Date: string\n Description: string\n 'Original Description': string\n Amount: string\n Type: string\n 'Parent Category': string\n Category: string\n Account: string\n Tags: string\n Memo: string\n Pending: string\n}\n\n/**\n * Collects the GUIDs of all transactions that act as parents\n * to one or more split (child) transactions.\n */\nfunction extractParentTransactionGuids(transactions: Transaction[]): Set<string> {\n return new Set(\n transactions.filter((tx) => tx.parent_guid !== undefined).map((tx) => tx.parent_guid!),\n )\n}\n\n/**\n * Organizes child transactions by their parent transaction GUID.\n * Returns a map from parent GUID to an array of its child transactions.\n */\nfunction mapChildTransactionsByParent(transactions: Transaction[]): Record<string, Transaction[]> {\n return transactions.reduce<Record<string, Transaction[]>>((accumulator, tx) => {\n if (tx.parent_guid) {\n if (!accumulator[tx.parent_guid]) accumulator[tx.parent_guid] = []\n accumulator[tx.parent_guid].push(tx)\n }\n return accumulator\n }, {})\n}\n\n/**\n * Produces a list of transactions based on the given sorted transactions,\n * where parent transactions that have split children are replaced by those\n * children sorted by descending absolute amount. All other transactions\n * are included as-is, with duplicates removed.\n */\nexport function flattenTransactionsWithSplits(\n sortedTransactions: Transaction[],\n allTransactions: Transaction[],\n): Transaction[] {\n // Set of parent transaction GUIDs that have split child transactions\n const parentTransactionGuidsWithSplits = extractParentTransactionGuids(allTransactions)\n\n // Map from parent GUID to array of its child split transactions\n const childTransactionsGroupedByParent = mapChildTransactionsByParent(allTransactions)\n\n // Track GUIDs of transactions already included in the result to avoid duplicates\n const processedTransactionGuids = new Set<string>()\n\n // Resulting list of transactions with parents replaced by their split children\n const flattenedTransactionList: Transaction[] = []\n\n for (const transaction of sortedTransactions) {\n if (parentTransactionGuidsWithSplits.has(transaction.guid)) {\n // This transaction is a parent with splits, so replace it with its children\n const splitChildTransactions = childTransactionsGroupedByParent[transaction.guid] ?? []\n\n splitChildTransactions\n .slice()\n .sort((a, b) => Math.abs(b.amount) - Math.abs(a.amount))\n .forEach((childTransaction) => {\n if (!processedTransactionGuids.has(childTransaction.guid)) {\n flattenedTransactionList.push(childTransaction)\n processedTransactionGuids.add(childTransaction.guid)\n }\n })\n } else if (!processedTransactionGuids.has(transaction.guid)) {\n // Normal transaction (no splits), add as-is if not already added\n flattenedTransactionList.push(transaction)\n processedTransactionGuids.add(transaction.guid)\n }\n }\n\n return flattenedTransactionList\n}\n\nexport const exportTransactionsToCSV = (transactions: Transaction[], tags: Tag[]) => {\n const csvContent = structureCSVData(formatTransactions(transactions, tags))\n\n const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })\n const link = document.createElement('a')\n link.href = URL.createObjectURL(blob)\n link.setAttribute('download', 'transactions.csv')\n\n document.body.appendChild(link)\n link.click()\n document.body.removeChild(link)\n\n URL.revokeObjectURL(link.href)\n}\n\n/**\n * Formats a list of transactions into a list that will be used when\n * downloaded to a CSV file.\n */\nconst formatTransactions = (transactions: Transaction[], tags: Tag[]) => {\n return transactions.map((transaction) => ({\n Date: formatDate(fromUnixTime(transaction.date), DATE_FORMATS_INTL.YEAR_MONTH_DAY),\n Description: getDescription(transaction),\n 'Original Description': transaction.feed_description,\n Amount: `${transaction.amount}`,\n Type: transaction.transaction_type === TransactionType.CREDIT ? 'Credit' : 'Debit',\n 'Parent Category': CategoryGuidToName[transaction.top_level_category_guid || ''],\n Category: transaction.category,\n Account: transaction.account,\n Tags: getTags(transaction, tags),\n Memo: transaction.memo,\n Pending: transaction.isPending ? 'TRUE' : 'FALSE',\n })) as FormattedTransaction[]\n}\n\n/**\n * Escapes a value for safe CSV output\n */\nconst escapeCsvField = (field?: string | null): string => {\n if (!field) return ''\n return /[,\"\\n\\r]/.test(field) ? `\"${field.replace(/\"/g, '\"\"')}\"` : field\n}\n\n/**\n * Structures the CSV file data using the formatted transactions\n */\nconst structureCSVData = (formattedTransactions: FormattedTransaction[]) => {\n const csvHeader = Object.keys(formattedTransactions[0]).join(',').concat('\\r\\n')\n const csvRows = formattedTransactions\n .map((transaction) => Object.values(transaction).map(escapeCsvField).join(','))\n .join('\\r\\n')\n\n return csvHeader + csvRows\n}\n\n/**\n * Get the description for a transaction that will be used within the CSV copy.\n * We want to highlight that split transactions are a split as well as\n * hidden transactions are hidden.\n */\nconst getDescription = (transaction: Transaction) => {\n let description = transaction.parent_guid ? 'Split: ' : ''\n description += transaction.is_hidden ? '(Excluded) ' : ''\n description += transaction.description\n\n return description\n}\n\nconst getTags = (transaction: Transaction, tags: Tag[]) => {\n return transaction.tags\n .map((tagGuid) => {\n const tag = tags.filter((t) => t.guid === tagGuid)[0]\n\n return tag?.name ?? ''\n })\n .join(' ')\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TransactionUtils.es.js","names":[],"sources":["../../../src/core/utils/TransactionUtils.ts"],"sourcesContent":["import { endOfToday } from 'date-fns/endOfToday'\nimport { format } from 'date-fns/format'\nimport { fromUnixTime } from 'date-fns/fromUnixTime'\nimport { getUnixTime } from 'date-fns/getUnixTime'\nimport { startOfMonth } from 'date-fns/startOfMonth'\nimport { startOfToday } from 'date-fns/startOfToday'\n\nimport { TABLE_CONST } from '../constants/Table'\nimport {\n CategoryGuids,\n CategoryGuidToName,\n DATE_FORMATS_INTL,\n formatDate,\n ORDINAL_DATE_FORMAT,\n} from '../constants'\nimport { COLUMN_NAMES } from '../constants/Transaction'\nimport { TransactionStatus, TransactionType } from '../types'\nimport type { Account, Category, Tag, Tagging, Transaction } from '../types'\n\nimport { type TransactionFilter } from '../stores/TransactionStore'\n\nimport { fromUnixUTC } from './DateUtil'\nimport { formatCurrency } from './NumberFormatting'\n\ninterface AccountsMap {\n [key: string]: Account\n}\n\ninterface CategoriesMap {\n [key: string]: Category\n}\n\ninterface TaggingsMap {\n [key: string]: string[]\n}\n\nexport const buildDetailedTransactions = (\n transactions: Transaction[],\n accounts: Account[],\n categories: Category[],\n taggings: Tagging[] = [],\n) => {\n const accountsMap = accounts.reduce((acc: AccountsMap, account: Account) => {\n return { ...acc, [account.guid]: account }\n }, {})\n\n const categoriesMap = categories.reduce((acc: CategoriesMap, category: Category) => {\n return { ...acc, [category.guid]: category }\n }, {})\n\n const taggingsMap = taggings.reduce((acc: TaggingsMap, tagging: Tagging) => {\n const { transaction_guid, tag_guid } = tagging\n\n if (!acc[transaction_guid]) {\n return {\n ...acc,\n [transaction_guid]: [tag_guid],\n }\n }\n const tags = [...acc[transaction_guid], tag_guid] as string[]\n\n return {\n ...acc,\n [transaction_guid]: tags,\n }\n }, {})\n\n return transactions.map((transaction: Transaction) => {\n const account = accountsMap[transaction.account_guid]\n const category = transaction.category_guid ? categoriesMap[transaction.category_guid] : null\n const tags = taggingsMap[transaction.guid] ?? []\n\n return {\n ...transaction,\n account: account?.user_name || 'Unknown',\n accountIsClosed: account?.is_closed,\n accountIsHidden: account?.is_hidden,\n category: category?.name || 'Uncategorized',\n isIncome: transaction.top_level_category_guid === CategoryGuids.INCOME,\n isPending: transaction.feed_status === TransactionStatus.PENDING,\n number: account?.account_number || '',\n payee: transaction.description,\n tags,\n }\n })\n}\n\nconst UNCATEGORIZED_TERM = 'uncategorized'\n\nexport const sortAndFilterTransactions = (\n filter: TransactionFilter,\n expandedSplits: string[],\n sortColumn: string,\n sortDirection: string,\n tags: Tag[],\n transactions: Transaction[],\n) => {\n const filteredTransactions = filterTransactions(filter, tags, transactions)\n\n const sortedTransactions = sortTransactions(filteredTransactions, sortColumn, sortDirection)\n\n expandedSplits.forEach((guid) => {\n const index = sortedTransactions.findIndex((t) => t.guid === guid)\n const children = transactions.filter((t) => t.parent_guid === guid)\n sortedTransactions.splice(index + 1, 0, ...children)\n })\n return sortedTransactions\n}\n\nconst filterTransactions = (\n filter: TransactionFilter,\n tags: Tag[],\n transactions: Transaction[],\n) => {\n const {\n accounts = [],\n dateRange = { start: startOfMonth(startOfToday()), end: endOfToday() },\n custom = () => true,\n searchTerm: searchFilter = '',\n showSplits = false,\n } = filter\n\n return (\n transactions\n // Filters out parent of split transactions when showSplits is true and filters out split\n // transaction children when showSplits is false\n .filter((t) => (showSplits ? !t.has_been_split : !t.parent_guid))\n // Filter for selected accounts\n .filter((t) => Boolean(accounts.find((guid) => guid === t.account_guid)))\n // Filter for Date Range.\n // NOTE: bounds are local-midnight (getUnixTime of a local Date) while dates\n // are rendered/exported on the UTC calendar day (fromUnixUTC). For a\n // midnight-UTC row near a month boundary in a negative-offset browser the\n // two can disagree by a day. Left as-is here to keep this fix low-risk;\n // unifying range + search on UTC is tracked with the core-data ISO\n // migration (MEW-3961 follow-up).\n .filter((t) => t.date >= getUnixTime(dateRange.start) && t.date <= getUnixTime(dateRange.end))\n // Custom filter\n .filter(custom)\n // Search filter\n .filter((transaction) => {\n // Don't return uncategorized split transactions\n if (searchFilter === UNCATEGORIZED_TERM && transaction.has_been_split) {\n return false\n }\n\n // Split check - When searching for \"Splits\" and transaction is a split or is a parent of a split(s)\n const splitPattern = /^spl/i\n if (\n splitPattern.test(searchFilter) &&\n (transaction.has_been_split || Boolean(transaction.parent_guid))\n ) {\n return true\n }\n\n // Check transactions tags\n const isTagIncludedInSearch = transaction.tags.some((guid) =>\n isIncludedInSearch(searchFilter, tags.find((tag) => tag.guid === guid)?.name),\n )\n const date = fromUnixTime(transaction.date)\n return (\n isIncludedInSearch(searchFilter, transaction.payee) ||\n isIncludedInSearch(searchFilter, transaction.feed_description) ||\n isIncludedInSearch(searchFilter, transaction.category) ||\n isIncludedInSearch(searchFilter, formatDate(date, DATE_FORMATS_INTL.MONTH)) ||\n isIncludedInSearch(searchFilter, formatDate(date, DATE_FORMATS_INTL.DAY)) ||\n isIncludedInSearch(searchFilter, formatDate(date, DATE_FORMATS_INTL.FULL_MONTH_DAY)) ||\n isIncludedInSearch(searchFilter, formatDate(date, DATE_FORMATS_INTL.MONTH_DAY_YEAR)) ||\n isIncludedInSearch(searchFilter, format(date, ORDINAL_DATE_FORMAT)) ||\n isIncludedInSearch(searchFilter, transaction.account) ||\n isIncludedInSearch(searchFilter, formatCurrency(transaction.amount, '0,0.00')) ||\n isIncludedInSearch(searchFilter, transaction.memo) ||\n isTagIncludedInSearch\n )\n })\n )\n}\n\nconst sortTransactions = (\n transactions: Transaction[],\n sortColumn: string,\n sortDirection: string,\n) => {\n return [...transactions].sort((a: Transaction, b: Transaction) => {\n const column = sortColumn as keyof Transaction\n const sortCompare = (a[column] || '') > (b[column] || '')\n\n let sortASC = sortCompare ? -1 : 1\n let sortDESC = sortCompare ? 1 : -1\n\n if (sortColumn === COLUMN_NAMES.DATE || sortColumn === COLUMN_NAMES.AMOUNT) {\n sortASC = sortCompare ? 1 : -1\n sortDESC = sortCompare ? -1 : 1\n }\n\n return sortDirection === TABLE_CONST.ASC ? sortASC : sortDESC\n })\n}\n\n/**\n * Determines if a value is included within the terms set in the search bar\n */\nconst isIncludedInSearch = (searchFilter: string, value: string | null | undefined) => {\n const lowerCasedTerms = searchFilter.toLowerCase().split(' ')\n const lowerCasedValue = value?.toString().toLowerCase() ?? ''\n\n return lowerCasedTerms.every((filter) => lowerCasedValue.indexOf(filter) > -1)\n}\n\n// TODO: Change the way formatting data is exported in order to handle localization\ntype FormattedTransaction = {\n Date: string\n Description: string\n 'Original Description': string\n Amount: string\n Type: string\n 'Parent Category': string\n Category: string\n Account: string\n Tags: string\n Memo: string\n Pending: string\n}\n\n/**\n * Collects the GUIDs of all transactions that act as parents\n * to one or more split (child) transactions.\n */\nfunction extractParentTransactionGuids(transactions: Transaction[]): Set<string> {\n return new Set(\n transactions.filter((tx) => tx.parent_guid !== undefined).map((tx) => tx.parent_guid!),\n )\n}\n\n/**\n * Organizes child transactions by their parent transaction GUID.\n * Returns a map from parent GUID to an array of its child transactions.\n */\nfunction mapChildTransactionsByParent(transactions: Transaction[]): Record<string, Transaction[]> {\n return transactions.reduce<Record<string, Transaction[]>>((accumulator, tx) => {\n if (tx.parent_guid) {\n if (!accumulator[tx.parent_guid]) accumulator[tx.parent_guid] = []\n accumulator[tx.parent_guid].push(tx)\n }\n return accumulator\n }, {})\n}\n\n/**\n * Produces a list of transactions based on the given sorted transactions,\n * where parent transactions that have split children are replaced by those\n * children sorted by descending absolute amount. All other transactions\n * are included as-is, with duplicates removed.\n */\nexport function flattenTransactionsWithSplits(\n sortedTransactions: Transaction[],\n allTransactions: Transaction[],\n): Transaction[] {\n // Set of parent transaction GUIDs that have split child transactions\n const parentTransactionGuidsWithSplits = extractParentTransactionGuids(allTransactions)\n\n // Map from parent GUID to array of its child split transactions\n const childTransactionsGroupedByParent = mapChildTransactionsByParent(allTransactions)\n\n // Track GUIDs of transactions already included in the result to avoid duplicates\n const processedTransactionGuids = new Set<string>()\n\n // Resulting list of transactions with parents replaced by their split children\n const flattenedTransactionList: Transaction[] = []\n\n for (const transaction of sortedTransactions) {\n if (parentTransactionGuidsWithSplits.has(transaction.guid)) {\n // This transaction is a parent with splits, so replace it with its children\n const splitChildTransactions = childTransactionsGroupedByParent[transaction.guid] ?? []\n\n splitChildTransactions\n .slice()\n .sort((a, b) => Math.abs(b.amount) - Math.abs(a.amount))\n .forEach((childTransaction) => {\n if (!processedTransactionGuids.has(childTransaction.guid)) {\n flattenedTransactionList.push(childTransaction)\n processedTransactionGuids.add(childTransaction.guid)\n }\n })\n } else if (!processedTransactionGuids.has(transaction.guid)) {\n // Normal transaction (no splits), add as-is if not already added\n flattenedTransactionList.push(transaction)\n processedTransactionGuids.add(transaction.guid)\n }\n }\n\n return flattenedTransactionList\n}\n\nexport const exportTransactionsToCSV = (transactions: Transaction[], tags: Tag[]) => {\n const csvContent = structureCSVData(formatTransactions(transactions, tags))\n\n const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })\n const link = document.createElement('a')\n link.href = URL.createObjectURL(blob)\n link.setAttribute('download', 'transactions.csv')\n\n document.body.appendChild(link)\n link.click()\n document.body.removeChild(link)\n\n URL.revokeObjectURL(link.href)\n}\n\n/**\n * Formats a list of transactions into a list that will be used when\n * downloaded to a CSV file.\n */\nconst formatTransactions = (transactions: Transaction[], tags: Tag[]) => {\n return transactions.map((transaction) => ({\n // Post dates are calendar days; format on the UTC day so the export can't\n // shift to the prior day in a Western-offset browser.\n Date: formatDate(fromUnixUTC(transaction.date), DATE_FORMATS_INTL.YEAR_MONTH_DAY),\n Description: getDescription(transaction),\n 'Original Description': transaction.feed_description,\n Amount: `${transaction.amount}`,\n Type: transaction.transaction_type === TransactionType.CREDIT ? 'Credit' : 'Debit',\n 'Parent Category': CategoryGuidToName[transaction.top_level_category_guid || ''],\n Category: transaction.category,\n Account: transaction.account,\n Tags: getTags(transaction, tags),\n Memo: transaction.memo,\n Pending: transaction.isPending ? 'TRUE' : 'FALSE',\n })) as FormattedTransaction[]\n}\n\n/**\n * Escapes a value for safe CSV output\n */\nconst escapeCsvField = (field?: string | null): string => {\n if (!field) return ''\n return /[,\"\\n\\r]/.test(field) ? `\"${field.replace(/\"/g, '\"\"')}\"` : field\n}\n\n/**\n * Structures the CSV file data using the formatted transactions\n */\nconst structureCSVData = (formattedTransactions: FormattedTransaction[]) => {\n // Guard the empty export: Object.keys(undefined) would throw for a zero-row\n // list (e.g. an over-filtered range), so emit an empty file instead.\n if (formattedTransactions.length === 0) return ''\n\n const csvHeader = Object.keys(formattedTransactions[0]).join(',').concat('\\r\\n')\n const csvRows = formattedTransactions\n .map((transaction) => Object.values(transaction).map(escapeCsvField).join(','))\n .join('\\r\\n')\n\n return csvHeader + csvRows\n}\n\n/**\n * Get the description for a transaction that will be used within the CSV copy.\n * We want to highlight that split transactions are a split as well as\n * hidden transactions are hidden.\n */\nconst getDescription = (transaction: Transaction) => {\n let description = transaction.parent_guid ? 'Split: ' : ''\n description += transaction.is_hidden ? '(Excluded) ' : ''\n description += transaction.description\n\n return description\n}\n\nconst getTags = (transaction: Transaction, tags: Tag[]) => {\n return transaction.tags\n .map((tagGuid) => {\n const tag = tags.filter((t) => t.guid === tagGuid)[0]\n\n return tag?.name ?? ''\n })\n .join(' ')\n}\n"],"mappings":"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"}
|