@things-factory/saas 10.1.234 → 10.1.238

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.
Files changed (54) hide show
  1. package/dist-client/components/saas-candidates.d.ts +33 -0
  2. package/dist-client/components/saas-candidates.js +157 -0
  3. package/dist-client/components/saas-candidates.js.map +1 -0
  4. package/dist-client/index.d.ts +1 -0
  5. package/dist-client/index.js +1 -0
  6. package/dist-client/index.js.map +1 -1
  7. package/dist-client/pages/saas-account-list.js +3 -0
  8. package/dist-client/pages/saas-account-list.js.map +1 -1
  9. package/dist-client/pages/saas-account-page.js +7 -2
  10. package/dist-client/pages/saas-account-page.js.map +1 -1
  11. package/dist-client/tsconfig.tsbuildinfo +1 -1
  12. package/dist-server/role-tasks.js +3 -1
  13. package/dist-server/role-tasks.js.map +1 -1
  14. package/dist-server/service/checks.d.ts +4 -1
  15. package/dist-server/service/checks.js +5 -2
  16. package/dist-server/service/checks.js.map +1 -1
  17. package/dist-server/service/index.d.ts +4 -1
  18. package/dist-server/service/index.js +5 -1
  19. package/dist-server/service/index.js.map +1 -1
  20. package/dist-server/service/saas-hidden-vendor.d.ts +19 -0
  21. package/dist-server/service/saas-hidden-vendor.js +53 -0
  22. package/dist-server/service/saas-hidden-vendor.js.map +1 -0
  23. package/dist-server/service/saas-mutation.d.ts +4 -1
  24. package/dist-server/service/saas-mutation.js +54 -0
  25. package/dist-server/service/saas-mutation.js.map +1 -1
  26. package/dist-server/service/saas-query.d.ts +2 -1
  27. package/dist-server/service/saas-query.js +16 -0
  28. package/dist-server/service/saas-query.js.map +1 -1
  29. package/dist-server/service/saas-rules.d.ts +41 -0
  30. package/dist-server/service/saas-rules.js +49 -1
  31. package/dist-server/service/saas-rules.js.map +1 -1
  32. package/dist-server/service/saas-seat.d.ts +1 -0
  33. package/dist-server/service/saas-seat.js +5 -0
  34. package/dist-server/service/saas-seat.js.map +1 -1
  35. package/dist-server/service/saas-service.d.ts +30 -7
  36. package/dist-server/service/saas-service.js +134 -17
  37. package/dist-server/service/saas-service.js.map +1 -1
  38. package/dist-server/service/saas-subscription.d.ts +2 -0
  39. package/dist-server/service/saas-subscription.js +10 -0
  40. package/dist-server/service/saas-subscription.js.map +1 -1
  41. package/dist-server/service/saas-sync.d.ts +34 -0
  42. package/dist-server/service/saas-sync.js +106 -0
  43. package/dist-server/service/saas-sync.js.map +1 -0
  44. package/dist-server/service/saas-type.d.ts +45 -0
  45. package/dist-server/service/saas-type.js +171 -1
  46. package/dist-server/service/saas-type.js.map +1 -1
  47. package/dist-server/tsconfig.tsbuildinfo +1 -1
  48. package/package.json +5 -5
  49. package/translations/en.json +23 -0
  50. package/translations/es.json +23 -0
  51. package/translations/ja.json +23 -0
  52. package/translations/ko.json +23 -0
  53. package/translations/ms.json +23 -0
  54. package/translations/zh.json +23 -0
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.seatHeld = exports.billTotal = exports.monthKey = exports.USAGE_AVERAGE_MONTHS = exports.DEFAULT_INCREASE_ALERT_RATE = exports.MONTH = exports.DATE = exports.SaasSubscriptionStatus = exports.SaasBilling = exports.SaasAccountStatus = void 0;
3
+ exports.CANDIDATE_MIN_MONTHS = exports.seatHeld = exports.billTotal = exports.monthKey = exports.USAGE_AVERAGE_MONTHS = exports.DEFAULT_INCREASE_ALERT_RATE = exports.MONTH = exports.DATE = exports.SaasSubscriptionStatus = exports.SaasBilling = exports.SaasAccountStatus = void 0;
4
4
  exports.shiftMonth = shiftMonth;
5
5
  exports.monthsFrom = monthsFrom;
6
6
  exports.lastBilledMonth = lastBilledMonth;
@@ -12,6 +12,7 @@ exports.changesOf = changesOf;
12
12
  exports.trendOf = trendOf;
13
13
  exports.alertsOf = alertsOf;
14
14
  exports.renewalDue = renewalDue;
15
+ exports.candidatesOf = candidatesOf;
15
16
  const type_graphql_1 = require("type-graphql");
16
17
  /* The day rule of accounting's recurring items, read without its entities (ADR-0177 decision 4: one rule for both). */
17
18
  const recurring_schedule_js_1 = require("@things-factory/accounting/dist-server/service/recurring-cash-item/recurring-schedule.js");
@@ -214,4 +215,51 @@ function renewalDue(sub, today) {
214
215
  /** Whether a seat is held on a day. */
215
216
  const seatHeld = (seat, today) => (!seat.fromOn || seat.fromOn <= today) && (!seat.untilOn || seat.untilOn >= today);
216
217
  exports.seatHeld = seatHeld;
218
+ /** At least this many months of the window paid one vendor before it looks like a subscription. */
219
+ exports.CANDIDATE_MIN_MONTHS = 3;
220
+ const isLastDay = (day) => {
221
+ const [year, month, date] = day.split('-').map(Number);
222
+ return date === new Date(Date.UTC(year, month, 0)).getUTCDate();
223
+ };
224
+ const mostOf = (values) => {
225
+ const counts = new Map();
226
+ for (const value of values)
227
+ counts.set(value, (counts.get(value) ?? 0) + 1);
228
+ return [...counts].sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
229
+ };
230
+ /**
231
+ * Whom the lines say is paid month after month - in at least `CANDIDATE_MIN_MONTHS` months, in the currency paid most -
232
+ * each with what a new account and its subscription would start from. Those whose key is in `skip` (already an
233
+ * account, a recurring item, hidden) are not candidates. Largest first.
234
+ */
235
+ function candidatesOf(lines, skip) {
236
+ const groups = new Map();
237
+ for (const line of lines)
238
+ if (line.key && !skip.has(line.key))
239
+ groups.set(line.key, [...(groups.get(line.key) ?? []), line]);
240
+ const out = [];
241
+ for (const [key, all] of groups) {
242
+ const currency = mostOf(all.map(line => line.currency || 'KRW'));
243
+ const paid = all.filter(line => (line.currency || 'KRW') === currency).sort((a, b) => a.transactionDate.localeCompare(b.transactionDate));
244
+ const months = new Set(paid.map(line => line.transactionDate.slice(0, 7)));
245
+ if (months.size < exports.CANDIDATE_MIN_MONTHS)
246
+ continue;
247
+ const last = paid[paid.length - 1];
248
+ out.push({
249
+ key,
250
+ counterpartyId: all[0].counterpartyId ?? null,
251
+ name: mostOf(paid.map(line => line.name?.trim() || null).filter(Boolean)),
252
+ months: months.size,
253
+ average: Math.round(paid.reduce((sum, line) => sum + num(line.amount), 0) / months.size),
254
+ firstDate: paid[0].transactionDate,
255
+ lastDate: last.transactionDate,
256
+ lastAmount: num(last.amount),
257
+ currency,
258
+ paymentAccountId: mostOf(paid.map(line => line.bankAccountId ?? null)),
259
+ /* A month's last day is 31 - the day a short month clamps to. */
260
+ billingDay: mostOf(paid.map(line => (isLastDay(line.transactionDate) ? 31 : Number(line.transactionDate.slice(8, 10)))))
261
+ });
262
+ }
263
+ return out.sort((a, b) => b.average - a.average);
264
+ }
217
265
  //# sourceMappingURL=saas-rules.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"saas-rules.js","sourceRoot":"","sources":["../../server/service/saas-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import { registerEnumType } from 'type-graphql'\n\n/* The day rule of accounting's recurring items, read without its entities (ADR-0177 decision 4: one rule for both). */\nimport { fallsIn, recurringDay } from '@things-factory/accounting/dist-server/service/recurring-cash-item/recurring-schedule.js'\n\n/*\n * The subscriptions' rules (ADR-0177) - when a subscription is billed and for how much, what a month is still expected\n * to bring, the cost trend from the bills and what in them is worth telling. Apart from the entities, so the screens'\n * words and the tests read them without a type-graphql class. Pure.\n */\n\nexport enum SaasAccountStatus {\n ACTIVE = 'ACTIVE',\n CLOSED = 'CLOSED'\n}\nregisterEnumType(SaasAccountStatus, { name: 'SaasAccountStatus', description: 'Whether an account is in use or closed' })\n\nexport enum SaasBilling {\n SEAT = 'SEAT',\n FIXED = 'FIXED',\n USAGE = 'USAGE',\n COMMITMENT = 'COMMITMENT',\n OWNED = 'OWNED'\n}\nregisterEnumType(SaasBilling, {\n name: 'SaasBilling',\n description: 'How a subscription is paid for - seats times a unit price, a fixed fee, metered usage, a commitment for a term, or a license owned (the asset register carries its cost)'\n})\n\nexport enum SaasSubscriptionStatus {\n ACTIVE = 'ACTIVE',\n CANCELLING = 'CANCELLING',\n CANCELLED = 'CANCELLED'\n}\nregisterEnumType(SaasSubscriptionStatus, {\n name: 'SaasSubscriptionStatus',\n description: 'In use, not to be renewed (it ends on its renewal day) or ended'\n})\n\nexport const DATE = /^\\d{4}-\\d{2}-\\d{2}$/\nexport const MONTH = /^\\d{4}-(0[1-9]|1[0-2])$/\n/** An account's bill rising more than this over the month before is told, unless the account names its own rate. */\nexport const DEFAULT_INCREASE_ALERT_RATE = 0.2\n/** Metered usage is expected at the average of so many latest bills. */\nexport const USAGE_AVERAGE_MONTHS = 3\n\nexport interface SubscriptionFacts {\n id: string\n billing: SaasBilling | string\n quantity?: number | null\n unitPrice?: number | null\n amount?: number | null\n budget?: number | null\n everyMonths?: number | null\n startOn: string\n renewalOn?: string | null\n endedOn?: string | null\n status: SaasSubscriptionStatus | string\n}\n\nexport interface BillFacts {\n accountId: string\n month: string\n currency: string\n lines: { subscriptionId?: string | null; amount: number }[]\n}\n\nconst num = (value: any) => (value === null || value === undefined || value === '' ? 0 : Number(value) || 0)\nconst pad = (n: number) => String(n).padStart(2, '0')\nexport const monthKey = (year: number, month: number) => `${year}-${pad(month)}`\n\n/** The month a number of months from a `YYYY-MM`. */\nexport function shiftMonth(key: string, by: number): string {\n const [year, month] = key.split('-').map(Number)\n const index = year * 12 + (month - 1) + by\n return monthKey(Math.floor(index / 12), (index % 12) + 1)\n}\n\n/** The months from one `YYYY-MM` to another, both in. */\nexport function monthsFrom(from: string, to: string): string[] {\n const months: string[] = []\n for (let key = from; key <= to; key = shiftMonth(key, 1)) months.push(key)\n return months\n}\n\n/**\n * The last month it is billed in: a cancelled one in the month it ended, when the bill of that month falls on or before\n * the day it ended, else the month before; one not to be renewed in the month before its renewal day. Open otherwise.\n */\nexport function lastBilledMonth(sub: SubscriptionFacts, billingDay?: number | null): string | null {\n if (sub.status === SaasSubscriptionStatus.CANCELLED && sub.endedOn) {\n const day = Number(sub.endedOn.slice(8, 10))\n return day >= (billingDay || 1) ? sub.endedOn.slice(0, 7) : shiftMonth(sub.endedOn.slice(0, 7), -1)\n }\n if (sub.status === SaasSubscriptionStatus.CANCELLING && sub.renewalOn) return shiftMonth(sub.renewalOn.slice(0, 7), -1)\n return null\n}\n\n/** Whether it is billed in a month - from the month it started, every so many months, to its last. An owned license never is. */\nexport function billedIn(sub: SubscriptionFacts, year: number, month: number, billingDay?: number | null): boolean {\n if (sub.billing === SaasBilling.OWNED || !sub.startOn) return false\n return fallsIn(\n { id: sub.id, name: '', type: 'EXPENSE', amount: 0, startMonth: sub.startOn.slice(0, 7), endMonth: lastBilledMonth(sub, billingDay), everyMonths: sub.everyMonths ?? 1 },\n year,\n month\n )\n}\n\n/** What one billing of it costs, as the subscription says: seats times the unit price, the fee. Usage says nothing - null. */\nexport function cycleAmountOf(sub: SubscriptionFacts): number | null {\n switch (sub.billing) {\n case SaasBilling.SEAT:\n return num(sub.quantity) * num(sub.unitPrice)\n case SaasBilling.FIXED:\n case SaasBilling.COMMITMENT:\n return num(sub.amount)\n case SaasBilling.OWNED:\n return 0\n default:\n return null\n }\n}\n\n/** Metered usage expected for a month: the average of its latest billed months (decision Q3), else its budget, else nothing. */\nexport function usageEstimate(sub: SubscriptionFacts, billed: { month: string; amount: number }[]): number | null {\n const byMonth = new Map<string, number>()\n for (const line of billed) byMonth.set(line.month, (byMonth.get(line.month) ?? 0) + num(line.amount))\n const latest = [...byMonth.keys()].sort().reverse().slice(0, USAGE_AVERAGE_MONTHS)\n if (latest.length) return latest.reduce((sum, key) => sum + byMonth.get(key)!, 0) / latest.length\n return sub.budget === null || sub.budget === undefined ? null : num(sub.budget)\n}\n\n/**\n * What an account is expected to be billed in a month (decision 4): the subscriptions billed in it, each at what it says\n * or, for usage, at its estimate - on the account's billing day. Nothing for a closed account, a sub-account or a month\n * nothing falls in.\n */\nexport function expectedOfMonth(\n account: { status: string; parentId?: string | null; billingDay?: number | null },\n subs: SubscriptionFacts[],\n billedBySubscription: Map<string, { month: string; amount: number }[]>,\n year: number,\n month: number\n): { amount: number; day: string | null; parts: { subscriptionId: string; amount: number }[] } | null {\n if (account.status !== SaasAccountStatus.ACTIVE || account.parentId) return null\n const parts: { subscriptionId: string; amount: number }[] = []\n for (const sub of subs) {\n if (!billedIn(sub, year, month, account.billingDay)) continue\n const amount = cycleAmountOf(sub) ?? usageEstimate(sub, billedBySubscription.get(sub.id) ?? [])\n if (amount) parts.push({ subscriptionId: sub.id, amount })\n }\n if (!parts.length) return null\n return { amount: parts.reduce((sum, one) => sum + one.amount, 0), day: recurringDay({ dayOfMonth: account.billingDay }, year, month), parts }\n}\n\n/** A bill's total - the sum of its lines, credits below zero. */\nexport const billTotal = (lines: { amount: number }[]) => lines.reduce((sum, line) => sum + num(line.amount), 0)\n\n/** How a series moved: the last month over the one before, and over the same month a year before - null when nothing to compare. */\nexport function changesOf(amounts: number[]): { monthOverMonth: number | null; yearOverYear: number | null } {\n const last = amounts.length - 1\n const ratio = (now: number, before: number | undefined) => (before ? (now - before) / before : null)\n return { monthOverMonth: last >= 1 ? ratio(amounts[last], amounts[last - 1]) : null, yearOverYear: last >= 12 ? ratio(amounts[last], amounts[last - 12]) : null }\n}\n\nexport interface TrendSeries {\n key: string\n accountId?: string | null\n subscriptionId?: string | null\n currency: string\n amounts: number[]\n total: number\n monthOverMonth: number | null\n yearOverYear: number | null\n}\n\n/**\n * The cost trend from the bills (decision 9), month by month from `from` to `to`, in each bill's own currency: one series\n * an account, one a subscription (lines on no subscription - tax, credits - in `rest` of the account), one a currency in\n * all. Computed, never stored.\n */\nexport function trendOf(bills: BillFacts[], from: string, to: string) {\n const months = monthsFrom(from, to)\n const index = new Map(months.map((key, i) => [key, i]))\n const series = new Map<string, TrendSeries>()\n const add = (key: string, base: Omit<TrendSeries, 'amounts' | 'total' | 'monthOverMonth' | 'yearOverYear' | 'key'>, at: number, amount: number) => {\n let one = series.get(key)\n if (!one) series.set(key, (one = { key, ...base, amounts: months.map(() => 0), total: 0, monthOverMonth: null, yearOverYear: null }))\n one.amounts[at] += amount\n }\n for (const bill of bills) {\n const at = index.get(bill.month)\n if (at === undefined) continue\n for (const line of bill.lines) {\n const amount = num(line.amount)\n add(`all|${bill.currency}`, { currency: bill.currency }, at, amount)\n add(`account|${bill.accountId}|${bill.currency}`, { accountId: bill.accountId, currency: bill.currency }, at, amount)\n add(`subscription|${line.subscriptionId ?? `rest:${bill.accountId}`}|${bill.currency}`, { accountId: bill.accountId, subscriptionId: line.subscriptionId ?? null, currency: bill.currency }, at, amount)\n }\n }\n for (const one of series.values()) {\n one.total = one.amounts.reduce((sum, amount) => sum + amount, 0)\n Object.assign(one, changesOf(one.amounts))\n }\n const of = (kind: string) => [...series.values()].filter(one => one.key.startsWith(`${kind}|`)).sort((a, b) => b.total - a.total)\n return { months, all: of('all'), accounts: of('account'), subscriptions: of('subscription') }\n}\n\nexport type SaasAlertKind = 'usage-over-budget' | 'bill-increased' | 'seat-bill-differs'\n\n/**\n * What a month's bill of an account is worth telling (decision 9): usage over its budget, the bill up more than the\n * account's rate over the month before, a seat plan billed other than its seats times its price (a price rise, a seat\n * added by the vendor).\n */\nexport function alertsOf(\n account: { id: string; increaseAlertRate?: number | null; billingDay?: number | null },\n subs: SubscriptionFacts[],\n bill: BillFacts,\n previous?: BillFacts | null\n): { kind: SaasAlertKind; subscriptionId?: string; amount: number; limit: number }[] {\n const alerts: { kind: SaasAlertKind; subscriptionId?: string; amount: number; limit: number }[] = []\n const [year, month] = bill.month.split('-').map(Number)\n for (const sub of subs) {\n const amount = billTotal(bill.lines.filter(line => line.subscriptionId === sub.id))\n if (sub.billing === SaasBilling.USAGE && sub.budget && amount > num(sub.budget)) alerts.push({ kind: 'usage-over-budget', subscriptionId: sub.id, amount, limit: num(sub.budget) })\n if (sub.billing === SaasBilling.SEAT && billedIn(sub, year, month, account.billingDay) && bill.lines.some(line => line.subscriptionId === sub.id)) {\n const expected = cycleAmountOf(sub)!\n if (Math.abs(amount - expected) >= 0.5) alerts.push({ kind: 'seat-bill-differs', subscriptionId: sub.id, amount, limit: expected })\n }\n }\n if (previous) {\n const now = billTotal(bill.lines)\n const before = billTotal(previous.lines)\n const rate = account.increaseAlertRate ?? DEFAULT_INCREASE_ALERT_RATE\n if (before > 0 && (now - before) / before > rate) alerts.push({ kind: 'bill-increased', amount: now, limit: before * (1 + rate) })\n }\n return alerts\n}\n\n/** Whether its renewal is near: from so many days before its renewal day (none - the day itself) to the day, while in use. */\nexport function renewalDue(sub: SubscriptionFacts & { noticeDays?: number | null }, today: string): boolean {\n if (sub.status !== SaasSubscriptionStatus.ACTIVE || !sub.renewalOn || sub.billing === SaasBilling.OWNED) return false\n const from = new Date(`${sub.renewalOn}T00:00:00Z`)\n from.setUTCDate(from.getUTCDate() - Math.max(0, num(sub.noticeDays)))\n return today >= from.toISOString().slice(0, 10) && today <= sub.renewalOn\n}\n\n/** Whether a seat is held on a day. */\nexport const seatHeld = (seat: { fromOn?: string | null; untilOn?: string | null }, today: string) => (!seat.fromOn || seat.fromOn <= today) && (!seat.untilOn || seat.untilOn >= today)\n"]}
1
+ {"version":3,"file":"saas-rules.js","sourceRoot":"","sources":["../../server/service/saas-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import { registerEnumType } from 'type-graphql'\n\n/* The day rule of accounting's recurring items, read without its entities (ADR-0177 decision 4: one rule for both). */\nimport { fallsIn, recurringDay } from '@things-factory/accounting/dist-server/service/recurring-cash-item/recurring-schedule.js'\n\n/*\n * The subscriptions' rules (ADR-0177) - when a subscription is billed and for how much, what a month is still expected\n * to bring, the cost trend from the bills and what in them is worth telling. Apart from the entities, so the screens'\n * words and the tests read them without a type-graphql class. Pure.\n */\n\nexport enum SaasAccountStatus {\n ACTIVE = 'ACTIVE',\n CLOSED = 'CLOSED'\n}\nregisterEnumType(SaasAccountStatus, { name: 'SaasAccountStatus', description: 'Whether an account is in use or closed' })\n\nexport enum SaasBilling {\n SEAT = 'SEAT',\n FIXED = 'FIXED',\n USAGE = 'USAGE',\n COMMITMENT = 'COMMITMENT',\n OWNED = 'OWNED'\n}\nregisterEnumType(SaasBilling, {\n name: 'SaasBilling',\n description: 'How a subscription is paid for - seats times a unit price, a fixed fee, metered usage, a commitment for a term, or a license owned (the asset register carries its cost)'\n})\n\nexport enum SaasSubscriptionStatus {\n ACTIVE = 'ACTIVE',\n CANCELLING = 'CANCELLING',\n CANCELLED = 'CANCELLED'\n}\nregisterEnumType(SaasSubscriptionStatus, {\n name: 'SaasSubscriptionStatus',\n description: 'In use, not to be renewed (it ends on its renewal day) or ended'\n})\n\nexport const DATE = /^\\d{4}-\\d{2}-\\d{2}$/\nexport const MONTH = /^\\d{4}-(0[1-9]|1[0-2])$/\n/** An account's bill rising more than this over the month before is told, unless the account names its own rate. */\nexport const DEFAULT_INCREASE_ALERT_RATE = 0.2\n/** Metered usage is expected at the average of so many latest bills. */\nexport const USAGE_AVERAGE_MONTHS = 3\n\nexport interface SubscriptionFacts {\n id: string\n billing: SaasBilling | string\n quantity?: number | null\n unitPrice?: number | null\n amount?: number | null\n budget?: number | null\n everyMonths?: number | null\n startOn: string\n renewalOn?: string | null\n endedOn?: string | null\n status: SaasSubscriptionStatus | string\n}\n\nexport interface BillFacts {\n accountId: string\n month: string\n currency: string\n lines: { subscriptionId?: string | null; amount: number }[]\n}\n\nconst num = (value: any) => (value === null || value === undefined || value === '' ? 0 : Number(value) || 0)\nconst pad = (n: number) => String(n).padStart(2, '0')\nexport const monthKey = (year: number, month: number) => `${year}-${pad(month)}`\n\n/** The month a number of months from a `YYYY-MM`. */\nexport function shiftMonth(key: string, by: number): string {\n const [year, month] = key.split('-').map(Number)\n const index = year * 12 + (month - 1) + by\n return monthKey(Math.floor(index / 12), (index % 12) + 1)\n}\n\n/** The months from one `YYYY-MM` to another, both in. */\nexport function monthsFrom(from: string, to: string): string[] {\n const months: string[] = []\n for (let key = from; key <= to; key = shiftMonth(key, 1)) months.push(key)\n return months\n}\n\n/**\n * The last month it is billed in: a cancelled one in the month it ended, when the bill of that month falls on or before\n * the day it ended, else the month before; one not to be renewed in the month before its renewal day. Open otherwise.\n */\nexport function lastBilledMonth(sub: SubscriptionFacts, billingDay?: number | null): string | null {\n if (sub.status === SaasSubscriptionStatus.CANCELLED && sub.endedOn) {\n const day = Number(sub.endedOn.slice(8, 10))\n return day >= (billingDay || 1) ? sub.endedOn.slice(0, 7) : shiftMonth(sub.endedOn.slice(0, 7), -1)\n }\n if (sub.status === SaasSubscriptionStatus.CANCELLING && sub.renewalOn) return shiftMonth(sub.renewalOn.slice(0, 7), -1)\n return null\n}\n\n/** Whether it is billed in a month - from the month it started, every so many months, to its last. An owned license never is. */\nexport function billedIn(sub: SubscriptionFacts, year: number, month: number, billingDay?: number | null): boolean {\n if (sub.billing === SaasBilling.OWNED || !sub.startOn) return false\n return fallsIn(\n { id: sub.id, name: '', type: 'EXPENSE', amount: 0, startMonth: sub.startOn.slice(0, 7), endMonth: lastBilledMonth(sub, billingDay), everyMonths: sub.everyMonths ?? 1 },\n year,\n month\n )\n}\n\n/** What one billing of it costs, as the subscription says: seats times the unit price, the fee. Usage says nothing - null. */\nexport function cycleAmountOf(sub: SubscriptionFacts): number | null {\n switch (sub.billing) {\n case SaasBilling.SEAT:\n return num(sub.quantity) * num(sub.unitPrice)\n case SaasBilling.FIXED:\n case SaasBilling.COMMITMENT:\n return num(sub.amount)\n case SaasBilling.OWNED:\n return 0\n default:\n return null\n }\n}\n\n/** Metered usage expected for a month: the average of its latest billed months (decision Q3), else its budget, else nothing. */\nexport function usageEstimate(sub: SubscriptionFacts, billed: { month: string; amount: number }[]): number | null {\n const byMonth = new Map<string, number>()\n for (const line of billed) byMonth.set(line.month, (byMonth.get(line.month) ?? 0) + num(line.amount))\n const latest = [...byMonth.keys()].sort().reverse().slice(0, USAGE_AVERAGE_MONTHS)\n if (latest.length) return latest.reduce((sum, key) => sum + byMonth.get(key)!, 0) / latest.length\n return sub.budget === null || sub.budget === undefined ? null : num(sub.budget)\n}\n\n/**\n * What an account is expected to be billed in a month (decision 4): the subscriptions billed in it, each at what it says\n * or, for usage, at its estimate - on the account's billing day. Nothing for a closed account, a sub-account or a month\n * nothing falls in.\n */\nexport function expectedOfMonth(\n account: { status: string; parentId?: string | null; billingDay?: number | null },\n subs: SubscriptionFacts[],\n billedBySubscription: Map<string, { month: string; amount: number }[]>,\n year: number,\n month: number\n): { amount: number; day: string | null; parts: { subscriptionId: string; amount: number }[] } | null {\n if (account.status !== SaasAccountStatus.ACTIVE || account.parentId) return null\n const parts: { subscriptionId: string; amount: number }[] = []\n for (const sub of subs) {\n if (!billedIn(sub, year, month, account.billingDay)) continue\n const amount = cycleAmountOf(sub) ?? usageEstimate(sub, billedBySubscription.get(sub.id) ?? [])\n if (amount) parts.push({ subscriptionId: sub.id, amount })\n }\n if (!parts.length) return null\n return { amount: parts.reduce((sum, one) => sum + one.amount, 0), day: recurringDay({ dayOfMonth: account.billingDay }, year, month), parts }\n}\n\n/** A bill's total - the sum of its lines, credits below zero. */\nexport const billTotal = (lines: { amount: number }[]) => lines.reduce((sum, line) => sum + num(line.amount), 0)\n\n/** How a series moved: the last month over the one before, and over the same month a year before - null when nothing to compare. */\nexport function changesOf(amounts: number[]): { monthOverMonth: number | null; yearOverYear: number | null } {\n const last = amounts.length - 1\n const ratio = (now: number, before: number | undefined) => (before ? (now - before) / before : null)\n return { monthOverMonth: last >= 1 ? ratio(amounts[last], amounts[last - 1]) : null, yearOverYear: last >= 12 ? ratio(amounts[last], amounts[last - 12]) : null }\n}\n\nexport interface TrendSeries {\n key: string\n accountId?: string | null\n subscriptionId?: string | null\n currency: string\n amounts: number[]\n total: number\n monthOverMonth: number | null\n yearOverYear: number | null\n}\n\n/**\n * The cost trend from the bills (decision 9), month by month from `from` to `to`, in each bill's own currency: one series\n * an account, one a subscription (lines on no subscription - tax, credits - in `rest` of the account), one a currency in\n * all. Computed, never stored.\n */\nexport function trendOf(bills: BillFacts[], from: string, to: string) {\n const months = monthsFrom(from, to)\n const index = new Map(months.map((key, i) => [key, i]))\n const series = new Map<string, TrendSeries>()\n const add = (key: string, base: Omit<TrendSeries, 'amounts' | 'total' | 'monthOverMonth' | 'yearOverYear' | 'key'>, at: number, amount: number) => {\n let one = series.get(key)\n if (!one) series.set(key, (one = { key, ...base, amounts: months.map(() => 0), total: 0, monthOverMonth: null, yearOverYear: null }))\n one.amounts[at] += amount\n }\n for (const bill of bills) {\n const at = index.get(bill.month)\n if (at === undefined) continue\n for (const line of bill.lines) {\n const amount = num(line.amount)\n add(`all|${bill.currency}`, { currency: bill.currency }, at, amount)\n add(`account|${bill.accountId}|${bill.currency}`, { accountId: bill.accountId, currency: bill.currency }, at, amount)\n add(`subscription|${line.subscriptionId ?? `rest:${bill.accountId}`}|${bill.currency}`, { accountId: bill.accountId, subscriptionId: line.subscriptionId ?? null, currency: bill.currency }, at, amount)\n }\n }\n for (const one of series.values()) {\n one.total = one.amounts.reduce((sum, amount) => sum + amount, 0)\n Object.assign(one, changesOf(one.amounts))\n }\n const of = (kind: string) => [...series.values()].filter(one => one.key.startsWith(`${kind}|`)).sort((a, b) => b.total - a.total)\n return { months, all: of('all'), accounts: of('account'), subscriptions: of('subscription') }\n}\n\nexport type SaasAlertKind = 'usage-over-budget' | 'bill-increased' | 'seat-bill-differs'\n\n/**\n * What a month's bill of an account is worth telling (decision 9): usage over its budget, the bill up more than the\n * account's rate over the month before, a seat plan billed other than its seats times its price (a price rise, a seat\n * added by the vendor).\n */\nexport function alertsOf(\n account: { id: string; increaseAlertRate?: number | null; billingDay?: number | null },\n subs: SubscriptionFacts[],\n bill: BillFacts,\n previous?: BillFacts | null\n): { kind: SaasAlertKind; subscriptionId?: string; amount: number; limit: number }[] {\n const alerts: { kind: SaasAlertKind; subscriptionId?: string; amount: number; limit: number }[] = []\n const [year, month] = bill.month.split('-').map(Number)\n for (const sub of subs) {\n const amount = billTotal(bill.lines.filter(line => line.subscriptionId === sub.id))\n if (sub.billing === SaasBilling.USAGE && sub.budget && amount > num(sub.budget)) alerts.push({ kind: 'usage-over-budget', subscriptionId: sub.id, amount, limit: num(sub.budget) })\n if (sub.billing === SaasBilling.SEAT && billedIn(sub, year, month, account.billingDay) && bill.lines.some(line => line.subscriptionId === sub.id)) {\n const expected = cycleAmountOf(sub)!\n if (Math.abs(amount - expected) >= 0.5) alerts.push({ kind: 'seat-bill-differs', subscriptionId: sub.id, amount, limit: expected })\n }\n }\n if (previous) {\n const now = billTotal(bill.lines)\n const before = billTotal(previous.lines)\n const rate = account.increaseAlertRate ?? DEFAULT_INCREASE_ALERT_RATE\n if (before > 0 && (now - before) / before > rate) alerts.push({ kind: 'bill-increased', amount: now, limit: before * (1 + rate) })\n }\n return alerts\n}\n\n/** Whether its renewal is near: from so many days before its renewal day (none - the day itself) to the day, while in use. */\nexport function renewalDue(sub: SubscriptionFacts & { noticeDays?: number | null }, today: string): boolean {\n if (sub.status !== SaasSubscriptionStatus.ACTIVE || !sub.renewalOn || sub.billing === SaasBilling.OWNED) return false\n const from = new Date(`${sub.renewalOn}T00:00:00Z`)\n from.setUTCDate(from.getUTCDate() - Math.max(0, num(sub.noticeDays)))\n return today >= from.toISOString().slice(0, 10) && today <= sub.renewalOn\n}\n\n/** Whether a seat is held on a day. */\nexport const seatHeld = (seat: { fromOn?: string | null; untilOn?: string | null }, today: string) => (!seat.fromOn || seat.fromOn <= today) && (!seat.untilOn || seat.untilOn >= today)\n\n/* ----------------------------------------------------------------------------------------------------------------- */\n/* What the books already say is subscribed to (ADR-0177 개정 1) */\n\n/**\n * A line of money out - a bank withdrawal or a card's charge - and whom it paid: its counterparty, or for a line no\n * counterparty is linked to, the name written on it (`key` is the counterparty id, else `name:` and the name's key).\n */\nexport interface VendorLine {\n key: string\n counterpartyId?: string | null\n name?: string | null\n transactionDate: string\n amount: number\n currency?: string | null\n bankAccountId?: string | null\n}\n\nexport interface CandidateFacts {\n key: string\n /** The counterparty paid; none when the lines only name it - a new account registers it. */\n counterpartyId: string | null\n /** The name written most on the lines. */\n name: string | null\n /** The months money went out in, of the window. */\n months: number\n /** What went out a month, on average over those months. */\n average: number\n /** The first line of the window - the subscription is known to have started by then. */\n firstDate: string\n lastDate: string\n lastAmount: number\n currency: string\n /** The account or card it went out of most. */\n paymentAccountId: string | null\n /** The day of the month it went out on most; a month's last day is 31. */\n billingDay: number | null\n}\n\n/** At least this many months of the window paid one vendor before it looks like a subscription. */\nexport const CANDIDATE_MIN_MONTHS = 3\n\nconst isLastDay = (day: string) => {\n const [year, month, date] = day.split('-').map(Number)\n return date === new Date(Date.UTC(year, month, 0)).getUTCDate()\n}\n\nconst mostOf = <T>(values: T[]): T | null => {\n const counts = new Map<T, number>()\n for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1)\n return [...counts].sort((a, b) => b[1] - a[1])[0]?.[0] ?? null\n}\n\n/**\n * Whom the lines say is paid month after month - in at least `CANDIDATE_MIN_MONTHS` months, in the currency paid most -\n * each with what a new account and its subscription would start from. Those whose key is in `skip` (already an\n * account, a recurring item, hidden) are not candidates. Largest first.\n */\nexport function candidatesOf(lines: readonly VendorLine[], skip: ReadonlySet<string>): CandidateFacts[] {\n const groups = new Map<string, VendorLine[]>()\n for (const line of lines) if (line.key && !skip.has(line.key)) groups.set(line.key, [...(groups.get(line.key) ?? []), line])\n const out: CandidateFacts[] = []\n for (const [key, all] of groups) {\n const currency = mostOf(all.map(line => line.currency || 'KRW'))!\n const paid = all.filter(line => (line.currency || 'KRW') === currency).sort((a, b) => a.transactionDate.localeCompare(b.transactionDate))\n const months = new Set(paid.map(line => line.transactionDate.slice(0, 7)))\n if (months.size < CANDIDATE_MIN_MONTHS) continue\n const last = paid[paid.length - 1]\n out.push({\n key,\n counterpartyId: all[0].counterpartyId ?? null,\n name: mostOf(paid.map(line => line.name?.trim() || null).filter(Boolean)),\n months: months.size,\n average: Math.round(paid.reduce((sum, line) => sum + num(line.amount), 0) / months.size),\n firstDate: paid[0].transactionDate,\n lastDate: last.transactionDate,\n lastAmount: num(last.amount),\n currency,\n paymentAccountId: mostOf(paid.map(line => line.bankAccountId ?? null)),\n /* A month's last day is 31 - the day a short month clamps to. */\n billingDay: mostOf(paid.map(line => (isLastDay(line.transactionDate) ? 31 : Number(line.transactionDate.slice(8, 10)))))\n })\n }\n return out.sort((a, b) => b.average - a.average)\n}\n"]}
@@ -12,6 +12,7 @@ export declare class SaasSeat {
12
12
  employee?: Employee;
13
13
  employeeId?: string;
14
14
  holder?: string;
15
+ login?: string;
15
16
  fromOn?: string;
16
17
  untilOn?: string;
17
18
  description?: string;
@@ -49,6 +49,11 @@ tslib_1.__decorate([
49
49
  (0, type_graphql_1.Field)({ nullable: true, description: 'Who holds it when it is no employee - a contractor, a shared mailbox' }),
50
50
  tslib_1.__metadata("design:type", String)
51
51
  ], SaasSeat.prototype, "holder", void 0);
52
+ tslib_1.__decorate([
53
+ (0, typeorm_1.Column)({ nullable: true }),
54
+ (0, type_graphql_1.Field)({ nullable: true, description: "The login it is held by at the vendor - a Microsoft 365 user name; set by a sync, which opens and closes it" }),
55
+ tslib_1.__metadata("design:type", String)
56
+ ], SaasSeat.prototype, "login", void 0);
52
57
  tslib_1.__decorate([
53
58
  (0, typeorm_1.Column)({ nullable: true }),
54
59
  (0, type_graphql_1.Field)({ nullable: true, description: 'The day it was given - YYYY-MM-DD' }),
@@ -1 +1 @@
1
- {"version":3,"file":"saas-seat.js","sourceRoot":"","sources":["../../server/service/saas-seat.ts"],"names":[],"mappings":"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","sourcesContent":["import { User } from '@things-factory/auth-base'\nimport { Employee } from '@things-factory/organization'\nimport { Domain } from '@things-factory/shell'\nimport { Field, ID, ObjectType } from 'type-graphql'\nimport { Column, CreateDateColumn, DeleteDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'\n\nimport { SaasSubscription } from './saas-subscription'\n\n/** Who holds one place of a subscription, and from when to when (ADR-0177 decision 6) - an employee, or a name. */\n@Entity()\n@Index('ix_saas_seat_0', (seat: SaasSeat) => [seat.domain, seat.subscription])\n@ObjectType({ description: 'Entity for SaasSeat — who holds one place of a subscription' })\nexport class SaasSeat {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the seat' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'The domain the seat belongs to' })\n domain?: Domain\n\n @RelationId((seat: SaasSeat) => seat.domain)\n domainId?: string\n\n @ManyToOne(type => SaasSubscription, { onDelete: 'CASCADE' })\n @Field(type => SaasSubscription, { nullable: true, description: 'The subscription it is a place of' })\n subscription?: SaasSubscription\n\n @RelationId((seat: SaasSeat) => seat.subscription)\n subscriptionId?: string\n\n @ManyToOne(type => Employee, { nullable: true, onDelete: 'SET NULL' })\n @Field(type => Employee, { nullable: true, description: 'The employee who holds it' })\n employee?: Employee\n\n @RelationId((seat: SaasSeat) => seat.employee)\n employeeId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Who holds it when it is no employee - a contractor, a shared mailbox' })\n holder?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'The day it was given - YYYY-MM-DD' })\n fromOn?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'The day it was taken back - YYYY-MM-DD' })\n untilOn?: string\n\n @Column({ type: 'text', nullable: true })\n @Field({ nullable: true, description: 'Notes - the login it uses' })\n description?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'When the seat was registered' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'When the seat last changed' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'When the seat was deleted' })\n deletedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'Who registered it' })\n creator?: User\n\n @RelationId((seat: SaasSeat) => seat.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'Who last changed it' })\n updater?: User\n\n @RelationId((seat: SaasSeat) => seat.updater)\n updaterId?: string\n}\n"]}
1
+ {"version":3,"file":"saas-seat.js","sourceRoot":"","sources":["../../server/service/saas-seat.ts"],"names":[],"mappings":"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","sourcesContent":["import { User } from '@things-factory/auth-base'\nimport { Employee } from '@things-factory/organization'\nimport { Domain } from '@things-factory/shell'\nimport { Field, ID, ObjectType } from 'type-graphql'\nimport { Column, CreateDateColumn, DeleteDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'\n\nimport { SaasSubscription } from './saas-subscription'\n\n/** Who holds one place of a subscription, and from when to when (ADR-0177 decision 6) - an employee, or a name. */\n@Entity()\n@Index('ix_saas_seat_0', (seat: SaasSeat) => [seat.domain, seat.subscription])\n@ObjectType({ description: 'Entity for SaasSeat — who holds one place of a subscription' })\nexport class SaasSeat {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the seat' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'The domain the seat belongs to' })\n domain?: Domain\n\n @RelationId((seat: SaasSeat) => seat.domain)\n domainId?: string\n\n @ManyToOne(type => SaasSubscription, { onDelete: 'CASCADE' })\n @Field(type => SaasSubscription, { nullable: true, description: 'The subscription it is a place of' })\n subscription?: SaasSubscription\n\n @RelationId((seat: SaasSeat) => seat.subscription)\n subscriptionId?: string\n\n @ManyToOne(type => Employee, { nullable: true, onDelete: 'SET NULL' })\n @Field(type => Employee, { nullable: true, description: 'The employee who holds it' })\n employee?: Employee\n\n @RelationId((seat: SaasSeat) => seat.employee)\n employeeId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Who holds it when it is no employee - a contractor, a shared mailbox' })\n holder?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: \"The login it is held by at the vendor - a Microsoft 365 user name; set by a sync, which opens and closes it\" })\n login?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'The day it was given - YYYY-MM-DD' })\n fromOn?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'The day it was taken back - YYYY-MM-DD' })\n untilOn?: string\n\n @Column({ type: 'text', nullable: true })\n @Field({ nullable: true, description: 'Notes - the login it uses' })\n description?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'When the seat was registered' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'When the seat last changed' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'When the seat was deleted' })\n deletedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'Who registered it' })\n creator?: User\n\n @RelationId((seat: SaasSeat) => seat.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'Who last changed it' })\n updater?: User\n\n @RelationId((seat: SaasSeat) => seat.updater)\n updaterId?: string\n}\n"]}
@@ -5,6 +5,7 @@ import { EntityManager } from 'typeorm';
5
5
  import { SaasScope } from './saas-access';
6
6
  import { SaasAccount } from './saas-account';
7
7
  import { SaasBill } from './saas-bill';
8
+ import { CandidateFacts } from './saas-rules';
8
9
  import { SaasSeat } from './saas-seat';
9
10
  import { SaasSubscription } from './saas-subscription';
10
11
  import { SaasTrend } from './saas-trend-type';
@@ -100,20 +101,42 @@ export declare function saasCostTrend(tx: EntityManager, domain: Domain, from: s
100
101
  }[];
101
102
  }>;
102
103
  /**
103
- * What left the bank for an account's vendor (ADR-0177 decision 10, the user 2026-10-30: 「이테크 입금 이력이 있을텐데」)
104
- * - the bank lines (accounting, confirmed) of money out to the account's counterparty, newest first. Only lines in the
105
- * account's currency count toward its months.
104
+ * What went out to an account's vendor (ADR-0177 decision 10, the user 2026-10-30: 「이테크 입금 이력이 있을텐데」;
105
+ * 개정 1) - the bank withdrawals and the card charges (accounting, CONFIRMED and CARD) to the account's counterparty,
106
+ * newest first. A line takes its counterparty when it lands (accounting `saveTransactions`), so a vendor's lines are
107
+ * its linked ones. Only lines in the account's currency count toward its months.
106
108
  */
107
- export declare function bankPaymentsOf(tx: EntityManager, domain: Domain, counterpartyIds: string[], from?: string, to?: string): Promise<Transaction[]>;
108
- /** The bank lines that paid an account's vendor, every one - for its 360. */
109
+ export declare function vendorLinesOf(tx: EntityManager, domain: Domain, counterpartyIds: string[], from?: string, to?: string): Promise<Transaction[]>;
110
+ /** The bank and card lines that paid an account's vendor, every one - for its 360. */
109
111
  export declare function saasPaymentsOf(tx: EntityManager, domain: Domain, scope: SaasScope, accountId: string): Promise<any[] | Transaction[]>;
110
112
  /**
111
113
  * Each account's last twelve months before this one and the next three expected (`saasExpected`). A month is its bill
112
- * when one is written; else, for a reader of the bank lines (`withBank`), what left the bank for its vendor that month
113
- * (decision 10); else empty. One read for all the accounts of a list.
114
+ * when one is written; else, for a reader of the bank lines (`withBank`), what went out to its vendor that month from
115
+ * the bank or a card (decision 10, 개정 1); else empty. One read for all the accounts of a list.
114
116
  */
115
117
  export declare function saasTrendsOf(tx: EntityManager, domain: Domain, accounts: SaasAccount[], today: string, withBank?: boolean): Promise<Map<string, SaasTrend>>;
116
118
  /** What moved on an account and on each of its subscriptions, newest first. */
117
119
  export declare function saasChangesOf(tx: EntityManager, domain: Domain, scope: SaasScope, accountId: string): Promise<(import("@things-factory/fact-change").FactChange & {
118
120
  subject: string;
119
121
  })[]>;
122
+ /** A candidate with its vendor's name, for the list. */
123
+ export type SaasCandidateRow = CandidateFacts & {
124
+ counterpartyName: string;
125
+ hidden: boolean;
126
+ };
127
+ /**
128
+ * Whom the bank and card lines of the last twelve months show paid month after month that is no account yet and no
129
+ * recurring item - what a keeper may make an account of with one press. A line on a subject outside profit and loss
130
+ * (a card bill, a tax, a transfer, a loan) pays no vendor, and a person (salary, expenses) is no vendor. With `hidden`,
131
+ * the ones a keeper hid instead.
132
+ */
133
+ export declare function saasCandidatesOf(tx: EntityManager, domain: Domain, today: string, hidden?: boolean): Promise<SaasCandidateRow[]>;
134
+ /**
135
+ * Make an account of each candidate - named after the vendor, paid from where it was paid most, on the day it was paid
136
+ * most - with one usage subscription budgeted at the monthly average, so the months to come are expected at it until
137
+ * bills say otherwise. A vendor the lines only name is registered as a counterparty first; the lines written with its
138
+ * name are linked to it (accounting `linkLinesByName`). One that is no candidate any more is refused.
139
+ */
140
+ export declare function createSaasAccountsFromCandidates(tx: EntityManager, domain: Domain, user: any, keys: string[], today: string): Promise<SaasAccount[]>;
141
+ /** Hide candidates - by their keys - or show them again. */
142
+ export declare function hideSaasCandidates(tx: EntityManager, domain: Domain, user: any, keys: string[], hide: boolean): Promise<boolean>;
@@ -12,10 +12,13 @@ exports.billsOf = billsOf;
12
12
  exports.saasBillsOf = saasBillsOf;
13
13
  exports.saasExpected = saasExpected;
14
14
  exports.saasCostTrend = saasCostTrend;
15
- exports.bankPaymentsOf = bankPaymentsOf;
15
+ exports.vendorLinesOf = vendorLinesOf;
16
16
  exports.saasPaymentsOf = saasPaymentsOf;
17
17
  exports.saasTrendsOf = saasTrendsOf;
18
18
  exports.saasChangesOf = saasChangesOf;
19
+ exports.saasCandidatesOf = saasCandidatesOf;
20
+ exports.createSaasAccountsFromCandidates = createSaasAccountsFromCandidates;
21
+ exports.hideSaasCandidates = hideSaasCandidates;
19
22
  const accounting_1 = require("@things-factory/accounting");
20
23
  const asset_1 = require("@things-factory/asset");
21
24
  /* The register grids' save, the asset module's - this module saves its grids the same way (one implementation). */
@@ -28,6 +31,7 @@ const typeorm_1 = require("typeorm");
28
31
  const saas_access_1 = require("./saas-access");
29
32
  const saas_account_1 = require("./saas-account");
30
33
  const saas_bill_1 = require("./saas-bill");
34
+ const saas_hidden_vendor_1 = require("./saas-hidden-vendor");
31
35
  const saas_rules_1 = require("./saas-rules");
32
36
  const saas_seat_1 = require("./saas-seat");
33
37
  const saas_subscription_1 = require("./saas-subscription");
@@ -63,7 +67,7 @@ const field = (patch, name, old) => (patch[name] !== undefined ? patch[name] : o
63
67
  exports.SAAS_ACCOUNT_FACT = 'saas-account';
64
68
  exports.SAAS_SUBSCRIPTION_FACT = 'saas-subscription';
65
69
  const ACCOUNT_TRACKED = ['name', 'counterpartyId', 'identifier', 'parentId', 'paymentAccountId', 'currency', 'billingDay', 'increaseAlertRate', 'status', 'closedOn'];
66
- const SUBSCRIPTION_TRACKED = ['name', 'billing', 'quantity', 'unitPrice', 'amount', 'budget', 'everyMonths', 'startOn', 'renewalOn', 'autoRenew', 'noticeDays', 'departmentId', 'assetId', 'status', 'endedOn'];
70
+ const SUBSCRIPTION_TRACKED = ['name', 'billing', 'vendorKey', 'trial', 'quantity', 'unitPrice', 'amount', 'budget', 'everyMonths', 'startOn', 'renewalOn', 'autoRenew', 'noticeDays', 'departmentId', 'assetId', 'status', 'endedOn'];
67
71
  const idsOf = (row) => Object.fromEntries(Object.entries(row).map(([key, value]) => (value && typeof value === 'object' && 'id' in value ? [`${key}Id`, value.id] : value === null && ['counterparty', 'parent', 'paymentAccount', 'department', 'asset'].includes(key) ? [`${key}Id`, null] : [key, value])));
68
72
  async function recordChanges(tx, domain, user, factKind, old, row, fields) {
69
73
  if (!old)
@@ -73,8 +77,8 @@ async function recordChanges(tx, domain, user, factKind, old, row, fields) {
73
77
  }
74
78
  /* ----------------------------------------------------------------------------------------------------------------- */
75
79
  /* Accounts (decision 2) - kept by `saas` keepers only */
76
- function saveSaasAccounts(tx, domain, user, patches) {
77
- return (0, patches_js_1.applyPatches)(tx, saas_account_1.SaasAccount, domain, user, patches, async (patch, old) => {
80
+ async function saveSaasAccounts(tx, domain, user, patches) {
81
+ const saved = await (0, patches_js_1.applyPatches)(tx, saas_account_1.SaasAccount, domain, user, patches, async (patch, old) => {
78
82
  const row = {};
79
83
  const name = (0, patches_js_1.touched)(patch, 'name', old) ? (0, patches_js_1.textOf)(patch.name) : old.name;
80
84
  if (!name)
@@ -121,6 +125,11 @@ function saveSaasAccounts(tx, domain, user, patches) {
121
125
  await recordChanges(tx, domain, user, exports.SAAS_ACCOUNT_FACT, old, row, ACCOUNT_TRACKED);
122
126
  return row;
123
127
  }, 'saas-account-not-found');
128
+ /* The vendor's lines written with its name and not linked yet are the account's months (개정 1). */
129
+ const vendors = patches.filter(one => one.cuFlag !== '-' && one.counterpartyId).map(one => one.counterpartyId);
130
+ if (vendors.length)
131
+ await (0, accounting_1.linkLinesByName)(tx, domain, user, vendors);
132
+ return saved;
124
133
  }
125
134
  /* ----------------------------------------------------------------------------------------------------------------- */
126
135
  /* Subscriptions (decision 3) - by keepers, and by the account's admins */
@@ -141,8 +150,15 @@ async function saveSaasSubscriptions(tx, domain, user, scope, accountId, patches
141
150
  for (const one of ['quantity', 'unitPrice', 'amount', 'budget'])
142
151
  if ((0, patches_js_1.touched)(patch, one, old))
143
152
  row[one] = (0, patches_js_1.amountOf)(patch[one], 'saas-amount-invalid');
153
+ /* `vendorKey` and `trial` are a sync's (개정 2) - the GraphQL patch does not carry them. */
154
+ if ((0, patches_js_1.touched)(patch, 'vendorKey', old))
155
+ row.vendorKey = (0, patches_js_1.textOf)(patch.vendorKey);
156
+ if ((0, patches_js_1.touched)(patch, 'trial', old))
157
+ row.trial = patch.trial === true;
144
158
  const value = (name) => (row[name] !== undefined ? row[name] : old?.[name]);
145
- if (billing === saas_rules_1.SaasBilling.SEAT && !(Number(value('quantity')) > 0 && value('unitPrice') !== null && value('unitPrice') !== undefined))
159
+ /* A plan read from the vendor's tenant may wait for its price (개정 2) - it is expected at nothing until then. */
160
+ const priced = value('unitPrice') !== null && value('unitPrice') !== undefined;
161
+ if (billing === saas_rules_1.SaasBilling.SEAT && !(Number(value('quantity')) > 0 && (priced || value('vendorKey'))))
146
162
  throw new shell_1.Refusal('saas-seat-price-required', {}, 'A seat plan takes its seats and the price of one');
147
163
  if ((billing === saas_rules_1.SaasBilling.FIXED || billing === saas_rules_1.SaasBilling.COMMITMENT) && (value('amount') === null || value('amount') === undefined))
148
164
  throw new shell_1.Refusal('saas-amount-required', {}, 'Give what one billing costs');
@@ -203,7 +219,8 @@ async function saveSaasSeats(tx, domain, user, scope, subscriptionId, patches) {
203
219
  const row = old ? {} : { subscription };
204
220
  if ((0, patches_js_1.touched)(patch, 'employeeId', old))
205
221
  row.employee = await oneOf(tx, organization_1.Employee, domain, patch.employeeId, 'saas-seat-employee-not-found');
206
- for (const one of ['holder', 'description'])
222
+ /* `login` is a sync's (개정 2) - the GraphQL patch does not carry it. */
223
+ for (const one of ['holder', 'login', 'description'])
207
224
  if ((0, patches_js_1.touched)(patch, one, old))
208
225
  row[one] = (0, patches_js_1.textOf)(patch[one]);
209
226
  if (!(row.employee !== undefined ? row.employee : old?.employeeId) && !(row.holder !== undefined ? row.holder : old?.holder))
@@ -419,18 +436,20 @@ async function saasCostTrend(tx, domain, from, to) {
419
436
  /* ----------------------------------------------------------------------------------------------------------------- */
420
437
  /* A row's months for the list and the 360 (decision 10) and what moved on it (decision 11) */
421
438
  /**
422
- * What left the bank for an account's vendor (ADR-0177 decision 10, the user 2026-10-30: 「이테크 입금 이력이 있을텐데」)
423
- * - the bank lines (accounting, confirmed) of money out to the account's counterparty, newest first. Only lines in the
424
- * account's currency count toward its months.
439
+ * What went out to an account's vendor (ADR-0177 decision 10, the user 2026-10-30: 「이테크 입금 이력이 있을텐데」;
440
+ * 개정 1) - the bank withdrawals and the card charges (accounting, CONFIRMED and CARD) to the account's counterparty,
441
+ * newest first. A line takes its counterparty when it lands (accounting `saveTransactions`), so a vendor's lines are
442
+ * its linked ones. Only lines in the account's currency count toward its months.
425
443
  */
426
- async function bankPaymentsOf(tx, domain, counterpartyIds, from, to) {
444
+ async function vendorLinesOf(tx, domain, counterpartyIds, from, to) {
427
445
  if (!counterpartyIds.length)
428
446
  return [];
429
447
  const query = tx
430
448
  .getRepository(accounting_1.Transaction)
431
449
  .createQueryBuilder('line')
450
+ .leftJoinAndSelect('line.bankAccount', 'bankAccount')
432
451
  .where('line.domain = :domain', { domain: domain.id })
433
- .andWhere('line.category = :category', { category: accounting_1.TransactionCategory.CONFIRMED })
452
+ .andWhere('line.category IN (:...categories)', { categories: [accounting_1.TransactionCategory.CONFIRMED, accounting_1.TransactionCategory.CARD] })
434
453
  .andWhere('line.type = :type', { type: accounting_1.CashFlowType.EXPENSE })
435
454
  .andWhere('line.counterparty IN (:...parties)', { parties: counterpartyIds });
436
455
  if (from)
@@ -439,15 +458,15 @@ async function bankPaymentsOf(tx, domain, counterpartyIds, from, to) {
439
458
  query.andWhere('line.transactionDate <= :to', { to });
440
459
  return query.orderBy('line.transactionDate', 'DESC').addOrderBy('line.amount', 'DESC').getMany();
441
460
  }
442
- /** The bank lines that paid an account's vendor, every one - for its 360. */
461
+ /** The bank and card lines that paid an account's vendor, every one - for its 360. */
443
462
  async function saasPaymentsOf(tx, domain, scope, accountId) {
444
463
  const account = await (0, saas_access_1.accountIn)(tx, domain, scope, accountId);
445
- return account.counterpartyId ? bankPaymentsOf(tx, domain, [account.counterpartyId]) : [];
464
+ return account.counterpartyId ? vendorLinesOf(tx, domain, [account.counterpartyId]) : [];
446
465
  }
447
466
  /**
448
467
  * Each account's last twelve months before this one and the next three expected (`saasExpected`). A month is its bill
449
- * when one is written; else, for a reader of the bank lines (`withBank`), what left the bank for its vendor that month
450
- * (decision 10); else empty. One read for all the accounts of a list.
468
+ * when one is written; else, for a reader of the bank lines (`withBank`), what went out to its vendor that month from
469
+ * the bank or a card (decision 10, 개정 1); else empty. One read for all the accounts of a list.
451
470
  */
452
471
  async function saasTrendsOf(tx, domain, accounts, today, withBank = false) {
453
472
  const trends = new Map();
@@ -459,7 +478,7 @@ async function saasTrendsOf(tx, domain, accounts, today, withBank = false) {
459
478
  const bills = await billsOf(tx, domain, accounts.map(one => one.id), months[0], months[months.length - 1]);
460
479
  const expected = await saasExpected(tx, domain, nextMonths[0], nextMonths[nextMonths.length - 1]);
461
480
  const parties = [...new Set(accounts.map(one => one.counterpartyId).filter(Boolean))];
462
- const paid = withBank ? await bankPaymentsOf(tx, domain, parties, `${months[0]}-01`, `${months[months.length - 1]}-31`) : [];
481
+ const paid = withBank ? await vendorLinesOf(tx, domain, parties, `${months[0]}-01`, `${months[months.length - 1]}-31`) : [];
463
482
  for (const account of accounts) {
464
483
  const own = bills.filter(one => one.accountId === account.id);
465
484
  const lines = paid.filter(line => line.counterpartyId === account.counterpartyId && (line.currency || 'KRW') === account.currency);
@@ -471,7 +490,13 @@ async function saasTrendsOf(tx, domain, accounts, today, withBank = false) {
471
490
  trends.set(account.id, {
472
491
  months,
473
492
  values,
474
- sources: months.map((month, i) => (own.some(one => one.month === month) ? 'bill' : values[i] === null ? null : 'bank')),
493
+ sources: months.map((month, i) => own.some(one => one.month === month)
494
+ ? 'bill'
495
+ : values[i] === null
496
+ ? null
497
+ : lines.some(line => line.transactionDate?.slice(0, 7) === month && line.category === accounting_1.TransactionCategory.CONFIRMED)
498
+ ? 'bank'
499
+ : 'card'),
475
500
  nextMonths,
476
501
  expected: nextMonths.map(month => expected.find(one => one.accountId === account.id && one.month === month)?.amount ?? null),
477
502
  currency: account.currency
@@ -489,4 +514,96 @@ async function saasChangesOf(tx, domain, scope, accountId) {
489
514
  ];
490
515
  return rows.sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime());
491
516
  }
517
+ /**
518
+ * Whom the bank and card lines of the last twelve months show paid month after month that is no account yet and no
519
+ * recurring item - what a keeper may make an account of with one press. A line on a subject outside profit and loss
520
+ * (a card bill, a tax, a transfer, a loan) pays no vendor, and a person (salary, expenses) is no vendor. With `hidden`,
521
+ * the ones a keeper hid instead.
522
+ */
523
+ async function saasCandidatesOf(tx, domain, today, hidden = false) {
524
+ const now = today.slice(0, 7);
525
+ const from = `${(0, saas_rules_1.shiftMonth)(now, -12)}-01`;
526
+ const lines = await tx
527
+ .getRepository(accounting_1.Transaction)
528
+ .createQueryBuilder('line')
529
+ .leftJoinAndSelect('line.account', 'subject')
530
+ .where('line.domain = :domain', { domain: domain.id })
531
+ .andWhere('line.category IN (:...categories)', { categories: [accounting_1.TransactionCategory.CONFIRMED, accounting_1.TransactionCategory.CARD] })
532
+ .andWhere('line.type = :type', { type: accounting_1.CashFlowType.EXPENSE })
533
+ .andWhere('line.transactionDate >= :from', { from })
534
+ .getMany();
535
+ const accounts = await tx.getRepository(saas_account_1.SaasAccount).find({ where: { domain: { id: domain.id } } });
536
+ const recurring = await tx.getRepository(accounting_1.RecurringCashItem).find({ where: { domain: { id: domain.id } } });
537
+ const hiddenKeys = new Set((await tx.getRepository(saas_hidden_vendor_1.SaasHiddenVendor).find({ where: { domain: { id: domain.id } } })).map(one => one.counterpartyId ?? `name:${one.nameKey}`));
538
+ const skip = new Set([...accounts.map(one => one.counterpartyId), ...recurring.map(one => one.counterpartyId), ...(hidden ? [] : hiddenKeys)].filter(Boolean));
539
+ const parties = await tx.getRepository(biz_base_1.Counterparty).find({ where: { domain: { id: domain.id } }, select: ['id', 'name', 'aliases', 'kind'] });
540
+ const named = (0, biz_base_1.partyNamed)(parties);
541
+ const people = new Set(parties.filter(one => one.kind === biz_base_1.CounterpartyKind.PERSON).map(one => one.id));
542
+ /* Whom a line paid: its counterparty, else the one its written name is (the rule a line is linked by), else the name. */
543
+ const vendorOf = (line) => {
544
+ const id = line.counterpartyId ?? named(line.counterpartyName)?.id;
545
+ if (id)
546
+ return people.has(id) ? null : { key: id, counterpartyId: id };
547
+ const key = (0, biz_base_1.nameKey)(line.counterpartyName);
548
+ return key && !(0, biz_base_1.looksLikePersonName)(line.counterpartyName) ? { key: `name:${key}`, counterpartyId: null } : null;
549
+ };
550
+ const found = (0, saas_rules_1.candidatesOf)(lines
551
+ .filter(line => line.account?.profitAndLoss !== false)
552
+ .flatMap(line => {
553
+ const vendor = vendorOf(line);
554
+ return vendor ? [{ ...vendor, name: line.counterpartyName, transactionDate: line.transactionDate, amount: Number(line.amount), currency: line.currency, bankAccountId: line.bankAccountId }] : [];
555
+ }), skip).filter(one => hiddenKeys.has(one.key) === hidden);
556
+ return found.map(one => ({ ...one, counterpartyName: parties.find(party => party.id === one.counterpartyId)?.name ?? one.name ?? '', hidden }));
557
+ }
558
+ /**
559
+ * Make an account of each candidate - named after the vendor, paid from where it was paid most, on the day it was paid
560
+ * most - with one usage subscription budgeted at the monthly average, so the months to come are expected at it until
561
+ * bills say otherwise. A vendor the lines only name is registered as a counterparty first; the lines written with its
562
+ * name are linked to it (accounting `linkLinesByName`). One that is no candidate any more is refused.
563
+ */
564
+ async function createSaasAccountsFromCandidates(tx, domain, user, keys, today) {
565
+ const candidates = await saasCandidatesOf(tx, domain, today);
566
+ const made = [];
567
+ for (const key of [...new Set(keys)]) {
568
+ const one = candidates.find(candidate => candidate.key === key);
569
+ if (!one)
570
+ throw new shell_1.Refusal('saas-candidate-not-found', {}, 'It is no candidate any more - reload and try again');
571
+ const counterpartyId = one.counterpartyId ?? (await (0, biz_base_1.saveCounterparties)(tx, domain, user, [{ cuFlag: '+', name: one.counterpartyName, kind: biz_base_1.CounterpartyKind.COMPANY }]))[0].id;
572
+ /* Its lines written with its name and not linked yet are its months. */
573
+ await (0, accounting_1.linkLinesByName)(tx, domain, user, [counterpartyId]);
574
+ const [account] = await saveSaasAccounts(tx, domain, user, [
575
+ { cuFlag: '+', name: one.counterpartyName, counterpartyId, paymentAccountId: one.paymentAccountId, currency: one.currency, billingDay: one.billingDay, status: saas_rules_1.SaasAccountStatus.ACTIVE }
576
+ ]);
577
+ await saveSaasSubscriptions(tx, domain, user, 'all', account.id, [
578
+ { cuFlag: '+', name: one.counterpartyName, billing: saas_rules_1.SaasBilling.USAGE, budget: one.average, startOn: one.firstDate, status: saas_rules_1.SaasSubscriptionStatus.ACTIVE }
579
+ ]);
580
+ made.push(account);
581
+ }
582
+ return made;
583
+ }
584
+ /** Hide candidates - by their keys - or show them again. */
585
+ async function hideSaasCandidates(tx, domain, user, keys, hide) {
586
+ const repo = tx.getRepository(saas_hidden_vendor_1.SaasHiddenVendor);
587
+ const wanted = [...new Set(keys)].filter(Boolean);
588
+ const names = wanted.filter(key => key.startsWith('name:')).map(key => key.slice(5));
589
+ const ids = wanted.filter(key => !key.startsWith('name:'));
590
+ const parties = ids.length ? await tx.getRepository(biz_base_1.Counterparty).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(ids) } }) : [];
591
+ if (parties.length !== ids.length)
592
+ throw new shell_1.Refusal('saas-counterparty-not-found', {}, 'The vendor was not found - reload and try again');
593
+ const rows = await repo.find({ where: { domain: { id: domain.id } } });
594
+ const held = (row) => (row.counterpartyId ? ids.includes(row.counterpartyId) : names.includes(row.nameKey));
595
+ if (!hide) {
596
+ const gone = rows.filter(held);
597
+ if (gone.length)
598
+ await repo.remove(gone);
599
+ return true;
600
+ }
601
+ for (const party of parties)
602
+ if (!rows.some(row => row.counterpartyId === party.id))
603
+ await repo.save({ domain, counterparty: party, creator: user });
604
+ for (const key of names)
605
+ if (!rows.some(row => !row.counterpartyId && row.nameKey === key))
606
+ await repo.save({ domain, nameKey: key, creator: user });
607
+ return true;
608
+ }
492
609
  //# sourceMappingURL=saas-service.js.map