@actual-app/api 26.5.0 → 26.6.0-nightly.20260504
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/dist/.tsbuildinfo +1 -1
- package/dist/index.js +206 -73
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -109162,12 +109162,22 @@ function getStatus(nextDate, completed, hasTrans, upcomingLength = "7") {
|
|
|
109162
109162
|
else if (nextDate < today) return "missed";
|
|
109163
109163
|
else return "scheduled";
|
|
109164
109164
|
}
|
|
109165
|
+
/**
|
|
109166
|
+
* Builds a query to check if each schedule already has a matching transaction.
|
|
109167
|
+
*
|
|
109168
|
+
* The date lower-bound varies:
|
|
109169
|
+
* - `dateCond.op === 'is'` (one-time): exact `next_date`, no lookback.
|
|
109170
|
+
* - `posts_transaction` (auto-posted recurring): exact `next_date`, since
|
|
109171
|
+
* auto-posted dates are always precise. A lookback here would cause
|
|
109172
|
+
* yesterday's transaction to falsely match today's occurrence.
|
|
109173
|
+
* - Otherwise (manual recurring): 2-day lookback to catch early payments.
|
|
109174
|
+
*/
|
|
109165
109175
|
function getHasTransactionsQuery(schedules) {
|
|
109166
109176
|
const filters = schedules.map((schedule) => {
|
|
109167
109177
|
const dateCond = schedule._conditions?.find((c) => c.field === "date");
|
|
109168
109178
|
return { $and: {
|
|
109169
109179
|
schedule: schedule.id,
|
|
109170
|
-
date: { $gte: dateCond && dateCond.op === "is" ? schedule.next_date : subDays(schedule.next_date, 2) }
|
|
109180
|
+
date: { $gte: dateCond && dateCond.op === "is" ? schedule.next_date : schedule.posts_transaction ? schedule.next_date : subDays(schedule.next_date, 2) }
|
|
109171
109181
|
} };
|
|
109172
109182
|
});
|
|
109173
109183
|
return q$1("transactions").options({ splits: "all" }).filter({ $or: filters }).orderBy({ date: "desc" }).select(["schedule", "date"]);
|
|
@@ -115080,8 +115090,9 @@ function getPayMonthOfTotal(t) {
|
|
|
115080
115090
|
for (const schedule of schedules) total += schedule.target;
|
|
115081
115091
|
return total;
|
|
115082
115092
|
}
|
|
115083
|
-
|
|
115093
|
+
function getSinkingContributionBreakdown(t, remainder, last_month_balance) {
|
|
115084
115094
|
let total = 0;
|
|
115095
|
+
const perSchedule = /* @__PURE__ */ new Map();
|
|
115085
115096
|
for (const [index, schedule] of t.entries()) {
|
|
115086
115097
|
remainder = index === 0 ? schedule.target - last_month_balance : schedule.target - remainder;
|
|
115087
115098
|
let tg = 0;
|
|
@@ -115092,41 +115103,37 @@ async function getSinkingContributionTotal(t, remainder, last_month_balance) {
|
|
|
115092
115103
|
tg = 0;
|
|
115093
115104
|
remainder = Math.abs(remainder);
|
|
115094
115105
|
}
|
|
115095
|
-
|
|
115106
|
+
const contribution = tg / (schedule.num_months + 1);
|
|
115107
|
+
total += contribution;
|
|
115108
|
+
perSchedule.set(schedule.name.trim(), (perSchedule.get(schedule.name.trim()) ?? 0) + contribution);
|
|
115109
|
+
}
|
|
115110
|
+
return {
|
|
115111
|
+
total,
|
|
115112
|
+
perSchedule
|
|
115113
|
+
};
|
|
115114
|
+
}
|
|
115115
|
+
function getMonthlyBaseContribution(schedule) {
|
|
115116
|
+
let prevDate;
|
|
115117
|
+
let intervalMonths;
|
|
115118
|
+
switch (schedule.target_frequency) {
|
|
115119
|
+
case "yearly": return schedule.target / schedule.target_interval / 12;
|
|
115120
|
+
case "monthly": return schedule.target / schedule.target_interval;
|
|
115121
|
+
case "weekly":
|
|
115122
|
+
prevDate = subWeeks(schedule.next_date_string, schedule.target_interval);
|
|
115123
|
+
intervalMonths = differenceInCalendarMonths(schedule.next_date_string, prevDate);
|
|
115124
|
+
if (intervalMonths === 0) intervalMonths = 1;
|
|
115125
|
+
return schedule.target / intervalMonths;
|
|
115126
|
+
case "daily":
|
|
115127
|
+
prevDate = subDays(schedule.next_date_string, schedule.target_interval);
|
|
115128
|
+
intervalMonths = differenceInCalendarMonths(schedule.next_date_string, prevDate);
|
|
115129
|
+
if (intervalMonths === 0) intervalMonths = 1;
|
|
115130
|
+
return schedule.target / intervalMonths;
|
|
115131
|
+
default: return schedule.target / schedule.target_interval;
|
|
115096
115132
|
}
|
|
115097
|
-
return total;
|
|
115098
115133
|
}
|
|
115099
115134
|
function getSinkingBaseContributionTotal(t) {
|
|
115100
115135
|
let total = 0;
|
|
115101
|
-
for (const schedule of t)
|
|
115102
|
-
let monthlyAmount = 0;
|
|
115103
|
-
let prevDate;
|
|
115104
|
-
let intervalMonths;
|
|
115105
|
-
switch (schedule.target_frequency) {
|
|
115106
|
-
case "yearly":
|
|
115107
|
-
monthlyAmount = schedule.target / schedule.target_interval / 12;
|
|
115108
|
-
break;
|
|
115109
|
-
case "monthly":
|
|
115110
|
-
monthlyAmount = schedule.target / schedule.target_interval;
|
|
115111
|
-
break;
|
|
115112
|
-
case "weekly":
|
|
115113
|
-
prevDate = subWeeks(schedule.next_date_string, schedule.target_interval);
|
|
115114
|
-
intervalMonths = differenceInCalendarMonths(schedule.next_date_string, prevDate);
|
|
115115
|
-
if (intervalMonths === 0) intervalMonths = 1;
|
|
115116
|
-
monthlyAmount = schedule.target / intervalMonths;
|
|
115117
|
-
break;
|
|
115118
|
-
case "daily":
|
|
115119
|
-
prevDate = subDays(schedule.next_date_string, schedule.target_interval);
|
|
115120
|
-
intervalMonths = differenceInCalendarMonths(schedule.next_date_string, prevDate);
|
|
115121
|
-
if (intervalMonths === 0) intervalMonths = 1;
|
|
115122
|
-
monthlyAmount = schedule.target / intervalMonths;
|
|
115123
|
-
break;
|
|
115124
|
-
default:
|
|
115125
|
-
monthlyAmount = schedule.target / schedule.target_interval;
|
|
115126
|
-
break;
|
|
115127
|
-
}
|
|
115128
|
-
total += monthlyAmount;
|
|
115129
|
-
}
|
|
115136
|
+
for (const schedule of t) total += getMonthlyBaseContribution(schedule);
|
|
115130
115137
|
return total;
|
|
115131
115138
|
}
|
|
115132
115139
|
function getSinkingTotal(t) {
|
|
@@ -115146,16 +115153,26 @@ async function runSchedule(template_lines, current_month, balance, remainder, la
|
|
|
115146
115153
|
const totalSinking = getSinkingTotal(t_sinking);
|
|
115147
115154
|
const totalSinkingBaseContribution = getSinkingBaseContributionTotal(t_sinking);
|
|
115148
115155
|
const lastMonthGoal = await getSheetValue(sheetForMonth(subMonths(current_month, 1)), `goal-${category.id}`);
|
|
115149
|
-
|
|
115150
|
-
|
|
115151
|
-
|
|
115156
|
+
const perScheduleMonthly = /* @__PURE__ */ new Map();
|
|
115157
|
+
const addContribution = (name, amount) => {
|
|
115158
|
+
perScheduleMonthly.set(name.trim(), (perScheduleMonthly.get(name.trim()) ?? 0) + amount);
|
|
115159
|
+
};
|
|
115160
|
+
if (balance >= totalSinking + totalPayMonthOf || lastMonthGoal < totalSinking + totalPayMonthOf && lastMonthGoal !== 0 && balance >= lastMonthGoal && numSubMonthly > 0) {
|
|
115161
|
+
to_budget += Math.round(totalPayMonthOf + totalSinkingBaseContribution);
|
|
115162
|
+
for (const c of t_payMonthOf) addContribution(c.name, c.target);
|
|
115163
|
+
for (const c of t_sinking) addContribution(c.name, getMonthlyBaseContribution(c));
|
|
115164
|
+
} else {
|
|
115165
|
+
const { total: totalSinkingContribution, perSchedule: sinkingPerSchedule } = getSinkingContributionBreakdown(t_sinking, remainder, last_month_balance);
|
|
115152
115166
|
if (t_sinking.length === 0) to_budget += Math.round(totalPayMonthOf + totalSinkingContribution) - last_month_balance;
|
|
115153
115167
|
else to_budget += Math.round(totalPayMonthOf + totalSinkingContribution);
|
|
115168
|
+
for (const c of t_payMonthOf) addContribution(c.name, c.target);
|
|
115169
|
+
for (const [name, amount] of sinkingPerSchedule) addContribution(name, amount);
|
|
115154
115170
|
}
|
|
115155
115171
|
return {
|
|
115156
115172
|
to_budget,
|
|
115157
115173
|
errors,
|
|
115158
|
-
remainder
|
|
115174
|
+
remainder,
|
|
115175
|
+
perScheduleMonthly
|
|
115159
115176
|
};
|
|
115160
115177
|
}
|
|
115161
115178
|
//#endregion
|
|
@@ -117670,7 +117687,7 @@ async function getCategoriesWithTemplates() {
|
|
|
117670
117687
|
return templatesForCategory;
|
|
117671
117688
|
}
|
|
117672
117689
|
function prefixFromPriority(priority) {
|
|
117673
|
-
return priority === null ? TEMPLATE_PREFIX : `${TEMPLATE_PREFIX}-${priority}`;
|
|
117690
|
+
return priority === null || priority === 0 ? TEMPLATE_PREFIX : `${TEMPLATE_PREFIX}-${priority}`;
|
|
117674
117691
|
}
|
|
117675
117692
|
async function unparse(templates) {
|
|
117676
117693
|
const refill = templates.find((t) => t.type === "refill");
|
|
@@ -117760,9 +117777,7 @@ function limitToString(limit) {
|
|
|
117760
117777
|
}
|
|
117761
117778
|
}
|
|
117762
117779
|
function periodToString(p) {
|
|
117763
|
-
|
|
117764
|
-
if (amount === 1) return period;
|
|
117765
|
-
return `${amount} ${period}s`;
|
|
117780
|
+
return `${p.amount} ${p.period}s`;
|
|
117766
117781
|
}
|
|
117767
117782
|
function repeatToString(annual, repeat) {
|
|
117768
117783
|
if (annual === void 0) return null;
|
|
@@ -117851,9 +117866,12 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
117851
117866
|
const t = this.templates.filter((t) => t.directive === "template" && t.priority === priority);
|
|
117852
117867
|
let available = budgetAvail || 0;
|
|
117853
117868
|
let toBudget = 0;
|
|
117869
|
+
const perTemplateLocal = /* @__PURE__ */ new Map();
|
|
117854
117870
|
let byFlag = false;
|
|
117855
117871
|
let remainder = 0;
|
|
117856
117872
|
let scheduleFlag = false;
|
|
117873
|
+
let schedulePerTemplate = null;
|
|
117874
|
+
let byPerTemplate = null;
|
|
117857
117875
|
for (const template of t) {
|
|
117858
117876
|
let newBudget = 0;
|
|
117859
117877
|
switch (template.type) {
|
|
@@ -117876,8 +117894,11 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
117876
117894
|
newBudget = await CategoryTemplateContext.runPercentage(template, availStart, this);
|
|
117877
117895
|
break;
|
|
117878
117896
|
case "by":
|
|
117879
|
-
if (!byFlag)
|
|
117880
|
-
|
|
117897
|
+
if (!byFlag) {
|
|
117898
|
+
const ret = CategoryTemplateContext.runBy(this);
|
|
117899
|
+
newBudget = ret.toBudget;
|
|
117900
|
+
byPerTemplate = ret.perTemplateNeed;
|
|
117901
|
+
} else newBudget = 0;
|
|
117881
117902
|
byFlag = true;
|
|
117882
117903
|
break;
|
|
117883
117904
|
case "schedule":
|
|
@@ -117886,6 +117907,7 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
117886
117907
|
const ret = await runSchedule(t, this.month, budgeted, remainder, this.fromLastMonth, toBudget, [], this.category, this.currency);
|
|
117887
117908
|
newBudget = ret.to_budget - toBudget;
|
|
117888
117909
|
remainder = ret.remainder;
|
|
117910
|
+
schedulePerTemplate = ret.perScheduleMonthly;
|
|
117889
117911
|
scheduleFlag = true;
|
|
117890
117912
|
}
|
|
117891
117913
|
break;
|
|
@@ -117896,24 +117918,51 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
117896
117918
|
}
|
|
117897
117919
|
available = available - newBudget;
|
|
117898
117920
|
toBudget += newBudget;
|
|
117921
|
+
perTemplateLocal.set(template, (perTemplateLocal.get(template) ?? 0) + newBudget);
|
|
117899
117922
|
}
|
|
117923
|
+
redistributeBatch(perTemplateLocal, t, "by", (template) => {
|
|
117924
|
+
if (template.type !== "by") return 0;
|
|
117925
|
+
return Math.max(0, byPerTemplate?.get(template) ?? 0);
|
|
117926
|
+
});
|
|
117927
|
+
redistributeBatch(perTemplateLocal, t, "schedule", (template) => {
|
|
117928
|
+
if (template.type !== "schedule") return 0;
|
|
117929
|
+
const monthly = schedulePerTemplate?.get(template.name.trim()) ?? 0;
|
|
117930
|
+
return Math.max(0, monthly);
|
|
117931
|
+
});
|
|
117932
|
+
let scale = 1;
|
|
117900
117933
|
if (this.limitCheck) {
|
|
117901
117934
|
if (toBudget + this.toBudgetAmount + this.fromLastMonth >= this.limitAmount) {
|
|
117902
117935
|
const orig = toBudget;
|
|
117903
117936
|
toBudget = this.limitAmount - this.toBudgetAmount - this.fromLastMonth;
|
|
117904
117937
|
this.limitMet = true;
|
|
117905
117938
|
available = available + orig - toBudget;
|
|
117939
|
+
if (orig > 0) scale *= toBudget / orig;
|
|
117906
117940
|
}
|
|
117907
117941
|
}
|
|
117908
|
-
if (this.hideDecimal)
|
|
117942
|
+
if (this.hideDecimal) {
|
|
117943
|
+
const preRound = toBudget;
|
|
117944
|
+
toBudget = this.removeFraction(toBudget);
|
|
117945
|
+
if (preRound !== 0) scale *= toBudget / preRound;
|
|
117946
|
+
}
|
|
117909
117947
|
if (priority > 0 && available < 0 && !this.category.is_income) {
|
|
117910
117948
|
this.fullAmount = (this.fullAmount || 0) + toBudget;
|
|
117911
|
-
|
|
117949
|
+
const adjusted = Math.max(0, toBudget + available);
|
|
117950
|
+
if (toBudget > 0) scale *= adjusted / toBudget;
|
|
117951
|
+
toBudget = adjusted;
|
|
117912
117952
|
this.toBudgetAmount += toBudget;
|
|
117913
117953
|
} else {
|
|
117914
117954
|
this.fullAmount = (this.fullAmount || 0) + toBudget;
|
|
117915
117955
|
this.toBudgetAmount += toBudget;
|
|
117916
117956
|
}
|
|
117957
|
+
const perRowScale = Math.max(0, scale);
|
|
117958
|
+
const items = Array.from(perTemplateLocal);
|
|
117959
|
+
let remaining = Math.max(0, toBudget);
|
|
117960
|
+
items.forEach(([template, value], i) => {
|
|
117961
|
+
const share = i === items.length - 1 ? remaining : Math.max(0, Math.min(remaining, Math.round(value * perRowScale)));
|
|
117962
|
+
const existing = this.perTemplateContribution.get(template) ?? 0;
|
|
117963
|
+
this.perTemplateContribution.set(template, existing + share);
|
|
117964
|
+
remaining -= share;
|
|
117965
|
+
});
|
|
117917
117966
|
return this.category.is_income ? -toBudget : toBudget;
|
|
117918
117967
|
}
|
|
117919
117968
|
runRemainder(budgetAvail, perWeight) {
|
|
@@ -117931,6 +117980,17 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
117931
117980
|
this.limitMet = true;
|
|
117932
117981
|
}
|
|
117933
117982
|
}
|
|
117983
|
+
if (toBudget > 0 && this.remainderWeight > 0) {
|
|
117984
|
+
let remaining = toBudget;
|
|
117985
|
+
for (let i = 0; i < this.remainder.length; i++) {
|
|
117986
|
+
const template = this.remainder[i];
|
|
117987
|
+
const share = i === this.remainder.length - 1 ? remaining : Math.round(toBudget * (template.weight / this.remainderWeight));
|
|
117988
|
+
const allocated = Math.max(0, Math.min(share, remaining));
|
|
117989
|
+
const existing = this.perTemplateContribution.get(template) ?? 0;
|
|
117990
|
+
this.perTemplateContribution.set(template, existing + allocated);
|
|
117991
|
+
remaining -= allocated;
|
|
117992
|
+
}
|
|
117993
|
+
}
|
|
117934
117994
|
this.toBudgetAmount += toBudget;
|
|
117935
117995
|
return toBudget;
|
|
117936
117996
|
}
|
|
@@ -117939,7 +117999,8 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
117939
117999
|
return {
|
|
117940
118000
|
budgeted: this.toBudgetAmount,
|
|
117941
118001
|
goal: this.goalAmount,
|
|
117942
|
-
longGoal: this.isLongGoal
|
|
118002
|
+
longGoal: this.isLongGoal,
|
|
118003
|
+
perTemplateContribution: this.perTemplateContribution
|
|
117943
118004
|
};
|
|
117944
118005
|
}
|
|
117945
118006
|
category;
|
|
@@ -117951,6 +118012,7 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
117951
118012
|
hideDecimal = false;
|
|
117952
118013
|
remainderWeight = 0;
|
|
117953
118014
|
toBudgetAmount = 0;
|
|
118015
|
+
perTemplateContribution = /* @__PURE__ */ new Map();
|
|
117954
118016
|
fullAmount = null;
|
|
117955
118017
|
isLongGoal = null;
|
|
117956
118018
|
goalAmount = null;
|
|
@@ -118008,10 +118070,15 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
118008
118070
|
static async checkPercentage(templates) {
|
|
118009
118071
|
const pt = templates.filter((t) => t.type === "percentage");
|
|
118010
118072
|
if (pt.length === 0) return;
|
|
118011
|
-
const
|
|
118012
|
-
const availNames =
|
|
118013
|
-
|
|
118014
|
-
|
|
118073
|
+
const incomeCategories = (await getCategories$3()).filter((c) => c.is_income);
|
|
118074
|
+
const availNames = new Set(incomeCategories.map((c) => c.name.toLocaleLowerCase()));
|
|
118075
|
+
const availIds = new Set(incomeCategories.map((c) => c.id));
|
|
118076
|
+
const specialSources = new Set(["all income", "available funds"]);
|
|
118077
|
+
pt.forEach((t) => {
|
|
118078
|
+
const raw = t.category;
|
|
118079
|
+
const lowered = raw.toLocaleLowerCase();
|
|
118080
|
+
if (specialSources.has(lowered) || availNames.has(lowered) || availIds.has(raw)) return;
|
|
118081
|
+
throw new Error(`Category \x22${raw}\x22 is not found in available income categories`);
|
|
118015
118082
|
});
|
|
118016
118083
|
}
|
|
118017
118084
|
checkLimit(templates) {
|
|
@@ -118075,7 +118142,7 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
118075
118142
|
const amount = amountToInteger$1(template.amount, templateContext.currency.decimalPlaces);
|
|
118076
118143
|
const period = template.period.period;
|
|
118077
118144
|
const numPeriods = template.period.amount;
|
|
118078
|
-
let date = template.starting
|
|
118145
|
+
let date = template.starting && template.starting.length > 0 ? template.starting : firstDayOfMonth(templateContext.month);
|
|
118079
118146
|
let dateShiftFunction;
|
|
118080
118147
|
switch (period) {
|
|
118081
118148
|
case "day":
|
|
@@ -118128,7 +118195,7 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
118128
118195
|
}
|
|
118129
118196
|
static async runPercentage(template, availableFunds, templateContext) {
|
|
118130
118197
|
const percent = template.percent;
|
|
118131
|
-
const cat = template.category.
|
|
118198
|
+
const cat = template.category.toLocaleLowerCase();
|
|
118132
118199
|
const prev = template.previous;
|
|
118133
118200
|
let sheetName;
|
|
118134
118201
|
let monthlyIncome;
|
|
@@ -118137,7 +118204,7 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
118137
118204
|
if (cat === "all income") monthlyIncome = await getSheetValue(sheetName, `total-income`);
|
|
118138
118205
|
else if (cat === "available funds") monthlyIncome = availableFunds;
|
|
118139
118206
|
else {
|
|
118140
|
-
const incomeCat = (await getCategories$3()).find((c) => c.is_income && c.name.
|
|
118207
|
+
const incomeCat = (await getCategories$3()).find((c) => c.is_income && (c.id === template.category || c.name.toLocaleLowerCase() === cat));
|
|
118141
118208
|
if (!incomeCat) throw new Error(`Income category "${template.category}" not found for percentage template`);
|
|
118142
118209
|
monthlyIncome = await getSheetValue(sheetName, `sum-amount-${incomeCat.id}`);
|
|
118143
118210
|
}
|
|
@@ -118182,6 +118249,7 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
118182
118249
|
if (workingShortNumMonths === void 0 || numMonths < workingShortNumMonths) workingShortNumMonths = numMonths;
|
|
118183
118250
|
}
|
|
118184
118251
|
const shortNumMonths = workingShortNumMonths || 0;
|
|
118252
|
+
const perTemplateNeed = /* @__PURE__ */ new Map();
|
|
118185
118253
|
for (let i = 0; i < byTemplates.length; i++) {
|
|
118186
118254
|
const template = byTemplates[i];
|
|
118187
118255
|
const numMonths = savedInfo[i].numMonths;
|
|
@@ -118190,11 +118258,43 @@ var CategoryTemplateContext = class CategoryTemplateContext {
|
|
|
118190
118258
|
if (numMonths > shortNumMonths && period) amount = Math.round(amountToInteger$1(template.amount, templateContext.currency.decimalPlaces) / period * (period - numMonths + shortNumMonths));
|
|
118191
118259
|
else if (numMonths > shortNumMonths) amount = Math.round(amountToInteger$1(template.amount, templateContext.currency.decimalPlaces) / (numMonths + 1) * (shortNumMonths + 1));
|
|
118192
118260
|
else amount = amountToInteger$1(template.amount, templateContext.currency.decimalPlaces);
|
|
118261
|
+
perTemplateNeed.set(template, amount);
|
|
118193
118262
|
totalNeeded += amount;
|
|
118194
118263
|
}
|
|
118195
|
-
return
|
|
118264
|
+
return {
|
|
118265
|
+
toBudget: Math.round((totalNeeded - templateContext.fromLastMonth) / (shortNumMonths + 1)),
|
|
118266
|
+
perTemplateNeed
|
|
118267
|
+
};
|
|
118196
118268
|
}
|
|
118197
118269
|
};
|
|
118270
|
+
function redistributeBatch(perTemplateLocal, templates, type, weightOf) {
|
|
118271
|
+
const siblings = templates.filter((template) => template.type === type);
|
|
118272
|
+
if (siblings.length < 2) return;
|
|
118273
|
+
let total = 0;
|
|
118274
|
+
for (const sibling of siblings) {
|
|
118275
|
+
total += perTemplateLocal.get(sibling) ?? 0;
|
|
118276
|
+
perTemplateLocal.set(sibling, 0);
|
|
118277
|
+
}
|
|
118278
|
+
if (total === 0) return;
|
|
118279
|
+
const totalWeight = siblings.reduce((sum, s) => sum + weightOf(s), 0);
|
|
118280
|
+
if (totalWeight <= 0) {
|
|
118281
|
+
let remaining = total;
|
|
118282
|
+
siblings.forEach((sibling, i) => {
|
|
118283
|
+
const share = i === siblings.length - 1 ? remaining : Math.round(total / siblings.length);
|
|
118284
|
+
const allocated = Math.max(0, Math.min(share, remaining));
|
|
118285
|
+
perTemplateLocal.set(sibling, (perTemplateLocal.get(sibling) ?? 0) + allocated);
|
|
118286
|
+
remaining -= allocated;
|
|
118287
|
+
});
|
|
118288
|
+
return;
|
|
118289
|
+
}
|
|
118290
|
+
let remaining = total;
|
|
118291
|
+
siblings.forEach((sibling, i) => {
|
|
118292
|
+
const share = i === siblings.length - 1 ? remaining : Math.round(total * weightOf(sibling) / totalWeight);
|
|
118293
|
+
const allocated = Math.max(0, Math.min(share, remaining));
|
|
118294
|
+
perTemplateLocal.set(sibling, (perTemplateLocal.get(sibling) ?? 0) + allocated);
|
|
118295
|
+
remaining -= allocated;
|
|
118296
|
+
});
|
|
118297
|
+
}
|
|
118198
118298
|
//#endregion
|
|
118199
118299
|
//#region ../loot-core/src/server/budget/goal-template.ts
|
|
118200
118300
|
function distributeRemainder(templateContexts, availBudget) {
|
|
@@ -118216,7 +118316,7 @@ async function storeTemplates({ categoriesWithTemplates, source }) {
|
|
|
118216
118316
|
await batchMessages(async () => {
|
|
118217
118317
|
for (const { id, templates } of categoriesWithTemplates) await updateWithSchema("categories", {
|
|
118218
118318
|
id,
|
|
118219
|
-
goal_def: JSON.stringify(templates),
|
|
118319
|
+
goal_def: templates.length > 0 ? JSON.stringify(templates) : null,
|
|
118220
118320
|
template_settings: { source }
|
|
118221
118321
|
});
|
|
118222
118322
|
});
|
|
@@ -118249,7 +118349,10 @@ async function getCategories$2() {
|
|
|
118249
118349
|
async function getTemplates(filter = () => true) {
|
|
118250
118350
|
const { data: categoriesWithGoalDef } = await aqlQuery$1(q$1("categories").filter({ goal_def: { $ne: null } }).select("*"));
|
|
118251
118351
|
const categoryTemplates = {};
|
|
118252
|
-
for (const categoryWithGoalDef of categoriesWithGoalDef.filter(filter))
|
|
118352
|
+
for (const categoryWithGoalDef of categoriesWithGoalDef.filter(filter)) {
|
|
118353
|
+
if (!categoryWithGoalDef.goal_def) continue;
|
|
118354
|
+
categoryTemplates[categoryWithGoalDef.id] = JSON.parse(categoryWithGoalDef.goal_def);
|
|
118355
|
+
}
|
|
118253
118356
|
return categoryTemplates;
|
|
118254
118357
|
}
|
|
118255
118358
|
async function getTemplatesForCategory(categoryId) {
|
|
@@ -118278,15 +118381,14 @@ async function setGoals(month, templateGoal) {
|
|
|
118278
118381
|
});
|
|
118279
118382
|
});
|
|
118280
118383
|
}
|
|
118281
|
-
async function
|
|
118384
|
+
async function computeTemplates(month, force, categoryTemplates, categories = []) {
|
|
118282
118385
|
const isTracking = isTrackingBudget();
|
|
118283
118386
|
if (!categories.length) categories = (await getCategories$2()).filter((c) => isTracking || !c.is_income);
|
|
118284
118387
|
const templateContexts = [];
|
|
118285
118388
|
let availBudget = await getSheetValue(sheetForMonth(month), `to-budget`);
|
|
118286
118389
|
const prioritiesSet = /* @__PURE__ */ new Set();
|
|
118287
118390
|
const errors = [];
|
|
118288
|
-
const
|
|
118289
|
-
const goalList = [];
|
|
118391
|
+
const orphanGoals = [];
|
|
118290
118392
|
for (const category of categories) {
|
|
118291
118393
|
const { id } = category;
|
|
118292
118394
|
const sheetName = sheetForMonth(month);
|
|
@@ -118302,23 +118404,16 @@ async function processTemplate(month, force, categoryTemplates, categories = [])
|
|
|
118302
118404
|
} catch (e) {
|
|
118303
118405
|
errors.push(`${category.name}: ${e.message}`);
|
|
118304
118406
|
}
|
|
118305
|
-
else if (existingGoal !== null && !templates)
|
|
118407
|
+
else if (existingGoal !== null && !templates) orphanGoals.push({
|
|
118306
118408
|
category: id,
|
|
118307
118409
|
goal: null,
|
|
118308
118410
|
longGoal: null
|
|
118309
118411
|
});
|
|
118310
118412
|
}
|
|
118311
|
-
if (templateContexts.length === 0 && errors.length === 0) {
|
|
118312
|
-
if (goalList.length > 0) setGoals(month, goalList);
|
|
118313
|
-
return {
|
|
118314
|
-
type: "message",
|
|
118315
|
-
message: "Everything is up to date"
|
|
118316
|
-
};
|
|
118317
|
-
}
|
|
118318
118413
|
if (errors.length > 0) return {
|
|
118319
|
-
|
|
118320
|
-
|
|
118321
|
-
|
|
118414
|
+
contexts: templateContexts,
|
|
118415
|
+
errors,
|
|
118416
|
+
orphanGoals
|
|
118322
118417
|
};
|
|
118323
118418
|
const priorities = new Int32Array([...prioritiesSet]).sort((a, b) => a - b);
|
|
118324
118419
|
for (const priority of priorities) {
|
|
@@ -118328,8 +118423,30 @@ async function processTemplate(month, force, categoryTemplates, categories = [])
|
|
|
118328
118423
|
availBudget -= budget;
|
|
118329
118424
|
}
|
|
118330
118425
|
}
|
|
118331
|
-
|
|
118332
|
-
|
|
118426
|
+
distributeRemainder(templateContexts, availBudget);
|
|
118427
|
+
return {
|
|
118428
|
+
contexts: templateContexts,
|
|
118429
|
+
errors,
|
|
118430
|
+
orphanGoals
|
|
118431
|
+
};
|
|
118432
|
+
}
|
|
118433
|
+
async function processTemplate(month, force, categoryTemplates, categories = []) {
|
|
118434
|
+
const { contexts, errors, orphanGoals } = await computeTemplates(month, force, categoryTemplates, categories);
|
|
118435
|
+
if (contexts.length === 0 && errors.length === 0) {
|
|
118436
|
+
if (orphanGoals.length > 0) await setGoals(month, orphanGoals);
|
|
118437
|
+
return {
|
|
118438
|
+
type: "message",
|
|
118439
|
+
message: "Everything is up to date"
|
|
118440
|
+
};
|
|
118441
|
+
}
|
|
118442
|
+
if (errors.length > 0) return {
|
|
118443
|
+
sticky: true,
|
|
118444
|
+
message: "There were errors interpreting some templates:",
|
|
118445
|
+
pre: errors.join(`\n\n`)
|
|
118446
|
+
};
|
|
118447
|
+
const budgetList = [];
|
|
118448
|
+
const goalList = [...orphanGoals];
|
|
118449
|
+
contexts.forEach((context) => {
|
|
118333
118450
|
const values = context.getValues();
|
|
118334
118451
|
budgetList.push({
|
|
118335
118452
|
category: context.category.id,
|
|
@@ -118345,7 +118462,22 @@ async function processTemplate(month, force, categoryTemplates, categories = [])
|
|
|
118345
118462
|
await setGoals(month, goalList);
|
|
118346
118463
|
return {
|
|
118347
118464
|
type: "message",
|
|
118348
|
-
message: `Successfully applied templates to ${
|
|
118465
|
+
message: `Successfully applied templates to ${contexts.length} categories`
|
|
118466
|
+
};
|
|
118467
|
+
}
|
|
118468
|
+
async function dryRunCategoryTemplate({ month, categoryId, templates }) {
|
|
118469
|
+
const allCategoryTemplates = await getTemplates();
|
|
118470
|
+
allCategoryTemplates[categoryId] = templates;
|
|
118471
|
+
const { contexts } = await computeTemplates(month, true, allCategoryTemplates, []);
|
|
118472
|
+
const ctx = contexts.find((c) => c.category.id === categoryId);
|
|
118473
|
+
if (!ctx) return {
|
|
118474
|
+
budgeted: 0,
|
|
118475
|
+
perTemplate: templates.map(() => 0)
|
|
118476
|
+
};
|
|
118477
|
+
const values = ctx.getValues();
|
|
118478
|
+
return {
|
|
118479
|
+
budgeted: values.budgeted,
|
|
118480
|
+
perTemplate: templates.map((t) => values.perTemplateContribution.get(t) ?? 0)
|
|
118349
118481
|
};
|
|
118350
118482
|
}
|
|
118351
118483
|
//#endregion
|
|
@@ -118389,6 +118521,7 @@ app$14.method("category-group-delete", mutator(undoable(deleteCategoryGroup$1)))
|
|
|
118389
118521
|
app$14.method("must-category-transfer", isCategoryTransferRequired);
|
|
118390
118522
|
app$14.method("budget/get-category-automations", getTemplatesForCategory);
|
|
118391
118523
|
app$14.method("budget/set-category-automations", mutator(undoable(storeTemplates)));
|
|
118524
|
+
app$14.method("budget/dry-run-category-template", dryRunCategoryTemplate);
|
|
118392
118525
|
app$14.method("budget/store-note-templates", mutator(storeNoteTemplates));
|
|
118393
118526
|
app$14.method("budget/render-note-templates", unparse);
|
|
118394
118527
|
async function getCategories$1() {
|