@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/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
- async function getSinkingContributionTotal(t, remainder, last_month_balance) {
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
- total += tg / (schedule.num_months + 1);
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
- if (balance >= totalSinking + totalPayMonthOf || lastMonthGoal < totalSinking + totalPayMonthOf && lastMonthGoal !== 0 && balance >= lastMonthGoal && numSubMonthly > 0) to_budget += Math.round(totalPayMonthOf + totalSinkingBaseContribution);
115150
- else {
115151
- const totalSinkingContribution = await getSinkingContributionTotal(t_sinking, remainder, last_month_balance);
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
- const { period, amount } = p;
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) newBudget = CategoryTemplateContext.runBy(this);
117880
- else newBudget = 0;
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) toBudget = this.removeFraction(toBudget);
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
- toBudget = Math.max(0, toBudget + available);
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 reqCategories = pt.map((t) => t.category.toLowerCase());
118012
- const availNames = (await getCategories$3()).filter((c) => c.is_income).map((c) => c.name.toLocaleLowerCase());
118013
- reqCategories.forEach((n) => {
118014
- if (n === "available funds" || n === "all income") {} else if (!availNames.includes(n)) throw new Error(`Category \x22${n}\x22 is not found in available income categories`);
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 ?? firstDayOfMonth(templateContext.month);
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.toLowerCase();
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.toLowerCase() === cat);
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 Math.round((totalNeeded - templateContext.fromLastMonth) / (shortNumMonths + 1));
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)) categoryTemplates[categoryWithGoalDef.id] = JSON.parse(categoryWithGoalDef.goal_def);
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 processTemplate(month, force, categoryTemplates, categories = []) {
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 budgetList = [];
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) goalList.push({
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
- sticky: true,
118320
- message: "There were errors interpreting some templates:",
118321
- pre: errors.join(`\n\n`)
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
- availBudget = distributeRemainder(templateContexts, availBudget);
118332
- templateContexts.forEach((context) => {
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 ${templateContexts.length} categories`
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() {