@things-factory/saas 10.1.240 → 10.1.241
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/README.md +11 -11
- package/dist-client/entity360/saas-account-360.d.ts +0 -1
- package/dist-client/entity360/saas-account-360.js +18 -71
- package/dist-client/entity360/saas-account-360.js.map +1 -1
- package/dist-client/index.d.ts +0 -3
- package/dist-client/index.js +0 -3
- package/dist-client/index.js.map +1 -1
- package/dist-client/pages/saas-account-list.d.ts +1 -1
- package/dist-client/pages/saas-account-list.js +5 -14
- package/dist-client/pages/saas-account-list.js.map +1 -1
- package/dist-client/pages/saas-account-page.d.ts +3 -5
- package/dist-client/pages/saas-account-page.js +30 -55
- package/dist-client/pages/saas-account-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +0 -3
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/words.d.ts +0 -13
- package/dist-client/words.js +0 -17
- package/dist-client/words.js.map +1 -1
- package/dist-server/service/checks.d.ts +1 -9
- package/dist-server/service/checks.js +2 -18
- package/dist-server/service/checks.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -6
- package/dist-server/service/index.js +1 -9
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/saas-account.d.ts +0 -4
- package/dist-server/service/saas-account.js +1 -16
- package/dist-server/service/saas-account.js.map +1 -1
- package/dist-server/service/saas-mutation.d.ts +1 -10
- package/dist-server/service/saas-mutation.js +1 -86
- package/dist-server/service/saas-mutation.js.map +1 -1
- package/dist-server/service/saas-query.d.ts +2 -51
- package/dist-server/service/saas-query.js +2 -65
- package/dist-server/service/saas-query.js.map +1 -1
- package/dist-server/service/saas-rules.d.ts +3 -141
- package/dist-server/service/saas-rules.js +10 -213
- package/dist-server/service/saas-rules.js.map +1 -1
- package/dist-server/service/saas-service.d.ts +2 -125
- package/dist-server/service/saas-service.js +50 -438
- package/dist-server/service/saas-service.js.map +1 -1
- package/dist-server/service/saas-subscription.d.ts +4 -5
- package/dist-server/service/saas-subscription.js +11 -21
- package/dist-server/service/saas-subscription.js.map +1 -1
- package/dist-server/service/saas-sync.d.ts +0 -35
- package/dist-server/service/saas-sync.js +0 -60
- package/dist-server/service/saas-sync.js.map +1 -1
- package/dist-server/service/saas-type.d.ts +0 -93
- package/dist-server/service/saas-type.js +3 -351
- package/dist-server/service/saas-type.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/translations/en.json +3 -81
- package/translations/es.json +3 -81
- package/translations/ja.json +3 -81
- package/translations/ko.json +3 -81
- package/translations/ms.json +3 -81
- package/translations/zh.json +3 -81
- package/dist-client/components/saas-bill-editor.d.ts +0 -59
- package/dist-client/components/saas-bill-editor.js +0 -211
- package/dist-client/components/saas-bill-editor.js.map +0 -1
- package/dist-client/components/saas-candidates.d.ts +0 -33
- package/dist-client/components/saas-candidates.js +0 -157
- package/dist-client/components/saas-candidates.js.map +0 -1
- package/dist-client/pages/saas-cost-trend.d.ts +0 -23
- package/dist-client/pages/saas-cost-trend.js +0 -219
- package/dist-client/pages/saas-cost-trend.js.map +0 -1
- package/dist-server/service/saas-bill.d.ts +0 -42
- package/dist-server/service/saas-bill.js +0 -165
- package/dist-server/service/saas-bill.js.map +0 -1
- package/dist-server/service/saas-hidden-vendor.d.ts +0 -19
- package/dist-server/service/saas-hidden-vendor.js +0 -53
- package/dist-server/service/saas-hidden-vendor.js.map +0 -1
- package/dist-server/service/saas-trend-type.d.ts +0 -9
- package/dist-server/service/saas-trend-type.js +0 -37
- package/dist-server/service/saas-trend-type.js.map +0 -1
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.
|
|
5
|
-
exports.monthsFrom = monthsFrom;
|
|
6
|
-
exports.lastBilledMonth = lastBilledMonth;
|
|
7
|
-
exports.billedIn = billedIn;
|
|
8
|
-
exports.cycleAmountOf = cycleAmountOf;
|
|
9
|
-
exports.usageEstimate = usageEstimate;
|
|
10
|
-
exports.expectedOfMonth = expectedOfMonth;
|
|
11
|
-
exports.changesOf = changesOf;
|
|
12
|
-
exports.trendOf = trendOf;
|
|
13
|
-
exports.alertsOf = alertsOf;
|
|
3
|
+
exports.seatHeld = exports.DATE = exports.SaasSubscriptionStatus = exports.SaasBilling = exports.SaasAccountStatus = void 0;
|
|
4
|
+
exports.referenceAmountOf = referenceAmountOf;
|
|
14
5
|
exports.renewalDue = renewalDue;
|
|
15
|
-
exports.candidatesOf = candidatesOf;
|
|
16
6
|
const type_graphql_1 = require("type-graphql");
|
|
17
|
-
/* The day rule of accounting's recurring items, read without its entities (ADR-0177 decision 4: one rule for both). */
|
|
18
|
-
const recurring_schedule_js_1 = require("@things-factory/accounting/dist-server/service/recurring-cash-item/recurring-schedule.js");
|
|
19
7
|
/*
|
|
20
|
-
* The subscriptions' rules (ADR-0177) -
|
|
21
|
-
*
|
|
22
|
-
*
|
|
8
|
+
* The subscriptions' rules (ADR-0177) - their renewals, their seats and the reference a quantity and a unit price make.
|
|
9
|
+
* The money is the recurring items' (accounting, 개정 7). Apart from the entities, so the screens' words and the tests
|
|
10
|
+
* read them without a type-graphql class. Pure.
|
|
23
11
|
*/
|
|
24
12
|
var SaasAccountStatus;
|
|
25
13
|
(function (SaasAccountStatus) {
|
|
@@ -50,159 +38,15 @@ var SaasSubscriptionStatus;
|
|
|
50
38
|
description: 'In use, not to be renewed (it ends on its renewal day) or ended'
|
|
51
39
|
});
|
|
52
40
|
exports.DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
53
|
-
exports.MONTH = /^\d{4}-(0[1-9]|1[0-2])$/;
|
|
54
|
-
/** An account's bill rising more than this over the month before is told, unless the account names its own rate. */
|
|
55
|
-
exports.DEFAULT_INCREASE_ALERT_RATE = 0.2;
|
|
56
|
-
/** Metered usage is expected at the average of so many latest bills. */
|
|
57
|
-
exports.USAGE_AVERAGE_MONTHS = 3;
|
|
58
41
|
const num = (value) => (value === null || value === undefined || value === '' ? 0 : Number(value) || 0);
|
|
59
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
60
|
-
const monthKey = (year, month) => `${year}-${pad(month)}`;
|
|
61
|
-
exports.monthKey = monthKey;
|
|
62
|
-
/** The month a number of months from a `YYYY-MM`. */
|
|
63
|
-
function shiftMonth(key, by) {
|
|
64
|
-
const [year, month] = key.split('-').map(Number);
|
|
65
|
-
const index = year * 12 + (month - 1) + by;
|
|
66
|
-
return (0, exports.monthKey)(Math.floor(index / 12), (index % 12) + 1);
|
|
67
|
-
}
|
|
68
|
-
/** The months from one `YYYY-MM` to another, both in. */
|
|
69
|
-
function monthsFrom(from, to) {
|
|
70
|
-
const months = [];
|
|
71
|
-
for (let key = from; key <= to; key = shiftMonth(key, 1))
|
|
72
|
-
months.push(key);
|
|
73
|
-
return months;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* 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
|
|
77
|
-
* the day it ended, else the month before; one not to be renewed in the month before its renewal day. Open otherwise.
|
|
78
|
-
*/
|
|
79
|
-
function lastBilledMonth(sub, billingDay) {
|
|
80
|
-
if (sub.status === SaasSubscriptionStatus.CANCELLED && sub.endedOn) {
|
|
81
|
-
const day = Number(sub.endedOn.slice(8, 10));
|
|
82
|
-
return day >= (billingDay || 1) ? sub.endedOn.slice(0, 7) : shiftMonth(sub.endedOn.slice(0, 7), -1);
|
|
83
|
-
}
|
|
84
|
-
if (sub.status === SaasSubscriptionStatus.CANCELLING && sub.renewalOn)
|
|
85
|
-
return shiftMonth(sub.renewalOn.slice(0, 7), -1);
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
/** Whether it is billed in a month - from the month it started, every so many months, to its last. An owned license never is. */
|
|
89
|
-
function billedIn(sub, year, month, billingDay) {
|
|
90
|
-
if (sub.billing === SaasBilling.OWNED || !sub.startOn)
|
|
91
|
-
return false;
|
|
92
|
-
return (0, recurring_schedule_js_1.fallsIn)({ id: sub.id, name: '', type: 'EXPENSE', amount: 0, startMonth: sub.startOn.slice(0, 7), endMonth: lastBilledMonth(sub, billingDay), everyMonths: sub.everyMonths ?? 1 }, year, month);
|
|
93
|
-
}
|
|
94
|
-
/** What one billing of it costs, as the subscription says: seats times the unit price, the fee. Usage says nothing - null. */
|
|
95
|
-
function cycleAmountOf(sub) {
|
|
96
|
-
switch (sub.billing) {
|
|
97
|
-
case SaasBilling.SEAT:
|
|
98
|
-
return num(sub.quantity) * num(sub.unitPrice);
|
|
99
|
-
case SaasBilling.FIXED:
|
|
100
|
-
case SaasBilling.COMMITMENT:
|
|
101
|
-
return num(sub.amount);
|
|
102
|
-
case SaasBilling.OWNED:
|
|
103
|
-
return 0;
|
|
104
|
-
default:
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/** Metered usage expected for a month: the average of its latest billed months (decision Q3), else its budget, else nothing. */
|
|
109
|
-
function usageEstimate(sub, billed) {
|
|
110
|
-
const byMonth = new Map();
|
|
111
|
-
for (const line of billed)
|
|
112
|
-
byMonth.set(line.month, (byMonth.get(line.month) ?? 0) + num(line.amount));
|
|
113
|
-
const latest = [...byMonth.keys()].sort().reverse().slice(0, exports.USAGE_AVERAGE_MONTHS);
|
|
114
|
-
if (latest.length)
|
|
115
|
-
return latest.reduce((sum, key) => sum + byMonth.get(key), 0) / latest.length;
|
|
116
|
-
return sub.budget === null || sub.budget === undefined ? null : num(sub.budget);
|
|
117
|
-
}
|
|
118
42
|
/**
|
|
119
|
-
* What
|
|
120
|
-
* or
|
|
121
|
-
* nothing falls in.
|
|
43
|
+
* What its quantity and unit price make - a reference a person compares with the recurring item's actual (개정 7-3):
|
|
44
|
+
* seats or units times the price, a fee with no quantity once. Null when no price is written; an owned license none.
|
|
122
45
|
*/
|
|
123
|
-
function
|
|
124
|
-
if (
|
|
125
|
-
return null;
|
|
126
|
-
const parts = [];
|
|
127
|
-
for (const sub of subs) {
|
|
128
|
-
if (!billedIn(sub, year, month, account.billingDay))
|
|
129
|
-
continue;
|
|
130
|
-
const amount = cycleAmountOf(sub) ?? usageEstimate(sub, billedBySubscription.get(sub.id) ?? []);
|
|
131
|
-
if (amount)
|
|
132
|
-
parts.push({ subscriptionId: sub.id, amount });
|
|
133
|
-
}
|
|
134
|
-
if (!parts.length)
|
|
46
|
+
function referenceAmountOf(sub) {
|
|
47
|
+
if (sub.billing === SaasBilling.OWNED || sub.unitPrice === null || sub.unitPrice === undefined)
|
|
135
48
|
return null;
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
/** A bill's total - the sum of its lines, credits below zero. */
|
|
139
|
-
const billTotal = (lines) => lines.reduce((sum, line) => sum + num(line.amount), 0);
|
|
140
|
-
exports.billTotal = billTotal;
|
|
141
|
-
/** How a series moved: the last month over the one before, and over the same month a year before - null when nothing to compare. */
|
|
142
|
-
function changesOf(amounts) {
|
|
143
|
-
const last = amounts.length - 1;
|
|
144
|
-
const ratio = (now, before) => (before ? (now - before) / before : null);
|
|
145
|
-
return { monthOverMonth: last >= 1 ? ratio(amounts[last], amounts[last - 1]) : null, yearOverYear: last >= 12 ? ratio(amounts[last], amounts[last - 12]) : null };
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* The cost trend from the bills (decision 9), month by month from `from` to `to`, in each bill's own currency: one series
|
|
149
|
-
* an account, one a subscription (lines on no subscription - tax, credits - in `rest` of the account), one a currency in
|
|
150
|
-
* all. Computed, never stored.
|
|
151
|
-
*/
|
|
152
|
-
function trendOf(bills, from, to) {
|
|
153
|
-
const months = monthsFrom(from, to);
|
|
154
|
-
const index = new Map(months.map((key, i) => [key, i]));
|
|
155
|
-
const series = new Map();
|
|
156
|
-
const add = (key, base, at, amount) => {
|
|
157
|
-
let one = series.get(key);
|
|
158
|
-
if (!one)
|
|
159
|
-
series.set(key, (one = { key, ...base, amounts: months.map(() => 0), total: 0, monthOverMonth: null, yearOverYear: null }));
|
|
160
|
-
one.amounts[at] += amount;
|
|
161
|
-
};
|
|
162
|
-
for (const bill of bills) {
|
|
163
|
-
const at = index.get(bill.month);
|
|
164
|
-
if (at === undefined)
|
|
165
|
-
continue;
|
|
166
|
-
for (const line of bill.lines) {
|
|
167
|
-
const amount = num(line.amount);
|
|
168
|
-
add(`all|${bill.currency}`, { currency: bill.currency }, at, amount);
|
|
169
|
-
add(`account|${bill.accountId}|${bill.currency}`, { accountId: bill.accountId, currency: bill.currency }, at, amount);
|
|
170
|
-
add(`subscription|${line.subscriptionId ?? `rest:${bill.accountId}`}|${bill.currency}`, { accountId: bill.accountId, subscriptionId: line.subscriptionId ?? null, currency: bill.currency }, at, amount);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
for (const one of series.values()) {
|
|
174
|
-
one.total = one.amounts.reduce((sum, amount) => sum + amount, 0);
|
|
175
|
-
Object.assign(one, changesOf(one.amounts));
|
|
176
|
-
}
|
|
177
|
-
const of = (kind) => [...series.values()].filter(one => one.key.startsWith(`${kind}|`)).sort((a, b) => b.total - a.total);
|
|
178
|
-
return { months, all: of('all'), accounts: of('account'), subscriptions: of('subscription') };
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* What a month's bill of an account is worth telling (decision 9): usage over its budget, the bill up more than the
|
|
182
|
-
* account's rate over the month before, a seat plan billed other than its seats times its price (a price rise, a seat
|
|
183
|
-
* added by the vendor).
|
|
184
|
-
*/
|
|
185
|
-
function alertsOf(account, subs, bill, previous) {
|
|
186
|
-
const alerts = [];
|
|
187
|
-
const [year, month] = bill.month.split('-').map(Number);
|
|
188
|
-
for (const sub of subs) {
|
|
189
|
-
const amount = (0, exports.billTotal)(bill.lines.filter(line => line.subscriptionId === sub.id));
|
|
190
|
-
if (sub.billing === SaasBilling.USAGE && sub.budget && amount > num(sub.budget))
|
|
191
|
-
alerts.push({ kind: 'usage-over-budget', subscriptionId: sub.id, amount, limit: num(sub.budget) });
|
|
192
|
-
if (sub.billing === SaasBilling.SEAT && billedIn(sub, year, month, account.billingDay) && bill.lines.some(line => line.subscriptionId === sub.id)) {
|
|
193
|
-
const expected = cycleAmountOf(sub);
|
|
194
|
-
if (Math.abs(amount - expected) >= 0.5)
|
|
195
|
-
alerts.push({ kind: 'seat-bill-differs', subscriptionId: sub.id, amount, limit: expected });
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if (previous) {
|
|
199
|
-
const now = (0, exports.billTotal)(bill.lines);
|
|
200
|
-
const before = (0, exports.billTotal)(previous.lines);
|
|
201
|
-
const rate = account.increaseAlertRate ?? exports.DEFAULT_INCREASE_ALERT_RATE;
|
|
202
|
-
if (before > 0 && (now - before) / before > rate)
|
|
203
|
-
alerts.push({ kind: 'bill-increased', amount: now, limit: before * (1 + rate) });
|
|
204
|
-
}
|
|
205
|
-
return alerts;
|
|
49
|
+
return (sub.quantity === null || sub.quantity === undefined ? 1 : num(sub.quantity)) * num(sub.unitPrice);
|
|
206
50
|
}
|
|
207
51
|
/** Whether its renewal is near: from so many days before its renewal day (none - the day itself) to the day, while in use. */
|
|
208
52
|
function renewalDue(sub, today) {
|
|
@@ -215,51 +59,4 @@ function renewalDue(sub, today) {
|
|
|
215
59
|
/** Whether a seat is held on a day. */
|
|
216
60
|
const seatHeld = (seat, today) => (!seat.fromOn || seat.fromOn <= today) && (!seat.untilOn || seat.untilOn >= today);
|
|
217
61
|
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
|
-
}
|
|
265
62
|
//# 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\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"]}
|
|
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/*\n * The subscriptions' rules (ADR-0177) - their renewals, their seats and the reference a quantity and a unit price make.\n * The money is the recurring items' (accounting, 개정 7). Apart from the entities, so the screens' words and the tests\n * 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}$/\n\nexport interface SubscriptionFacts {\n id: string\n billing: SaasBilling | string\n quantity?: number | null\n unitPrice?: number | null\n startOn: string\n renewalOn?: string | null\n endedOn?: string | null\n status: SaasSubscriptionStatus | string\n}\n\nconst num = (value: any) => (value === null || value === undefined || value === '' ? 0 : Number(value) || 0)\n\n/**\n * What its quantity and unit price make - a reference a person compares with the recurring item's actual (개정 7-3):\n * seats or units times the price, a fee with no quantity once. Null when no price is written; an owned license none.\n */\nexport function referenceAmountOf(sub: SubscriptionFacts): number | null {\n if (sub.billing === SaasBilling.OWNED || sub.unitPrice === null || sub.unitPrice === undefined) return null\n return (sub.quantity === null || sub.quantity === undefined ? 1 : num(sub.quantity)) * num(sub.unitPrice)\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,142 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Patch } from '@things-factory/asset/dist-server/service/patches.js';
|
|
1
|
+
import { Patch } from '@things-factory/asset';
|
|
3
2
|
import { Domain } from '@things-factory/shell';
|
|
4
3
|
import { EntityManager } from 'typeorm';
|
|
5
4
|
import { SaasScope } from './saas-access';
|
|
6
5
|
import { SaasAccount } from './saas-account';
|
|
7
|
-
import { SaasBill } from './saas-bill';
|
|
8
|
-
import { CandidateFacts } from './saas-rules';
|
|
9
6
|
import { SaasSeat } from './saas-seat';
|
|
10
7
|
import { SaasSubscription } from './saas-subscription';
|
|
11
|
-
import { SaasTrend } from './saas-trend-type';
|
|
12
8
|
export declare const SAAS_ACCOUNT_FACT = "saas-account";
|
|
13
9
|
export declare const SAAS_SUBSCRIPTION_FACT = "saas-subscription";
|
|
14
10
|
export declare function saveSaasAccounts(tx: EntityManager, domain: Domain, user: any, patches: Patch[]): Promise<SaasAccount[]>;
|
|
15
11
|
export declare function saveSaasSubscriptions(tx: EntityManager, domain: Domain, user: any, scope: SaasScope, accountId: string, patches: Patch[]): Promise<SaasSubscription[]>;
|
|
16
|
-
/** The subscriptions of an account, with the seats held today. */
|
|
12
|
+
/** The subscriptions of an account, with the seats held today and the reference each makes. */
|
|
17
13
|
export declare function saasSubscriptionsOf(tx: EntityManager, domain: Domain, scope: SaasScope, accountId: string, today: string): Promise<SaasSubscription[]>;
|
|
18
14
|
export declare function saveSaasSeats(tx: EntityManager, domain: Domain, user: any, scope: SaasScope, subscriptionId: string, patches: Patch[]): Promise<SaasSeat[]>;
|
|
19
15
|
export declare function saasSeatsOf(tx: EntityManager, domain: Domain, scope: SaasScope, subscriptionId: string): Promise<SaasSeat[]>;
|
|
20
|
-
export interface SaasBillFields {
|
|
21
|
-
id?: string | null;
|
|
22
|
-
month: string;
|
|
23
|
-
billedOn?: string | null;
|
|
24
|
-
description?: string | null;
|
|
25
|
-
lines: {
|
|
26
|
-
subscriptionId?: string | null;
|
|
27
|
-
label?: string | null;
|
|
28
|
-
amount: number;
|
|
29
|
-
}[];
|
|
30
|
-
}
|
|
31
|
-
export declare function saveSaasBill(tx: EntityManager, domain: Domain, user: any, scope: SaasScope, accountId: string, input: SaasBillFields): Promise<SaasBill>;
|
|
32
|
-
export declare function deleteSaasBill(tx: EntityManager, domain: Domain, scope: SaasScope, id: string): Promise<boolean>;
|
|
33
|
-
/** The bills of some accounts (all when none named) from a month on, with their lines and totals, latest first. */
|
|
34
|
-
export declare function billsOf(tx: EntityManager, domain: Domain, accountIds?: string[], from?: string, to?: string): Promise<SaasBill[]>;
|
|
35
|
-
export declare function saasBillsOf(tx: EntityManager, domain: Domain, scope: SaasScope, accountId: string): Promise<SaasBill[]>;
|
|
36
|
-
export interface SaasExpectedRow {
|
|
37
|
-
accountId: string;
|
|
38
|
-
accountName: string;
|
|
39
|
-
counterpartyId?: string | null;
|
|
40
|
-
paymentAccountId?: string | null;
|
|
41
|
-
month: string;
|
|
42
|
-
day: string | null;
|
|
43
|
-
amount: number;
|
|
44
|
-
currency: string;
|
|
45
|
-
/** A bill is written for the month: the amount is the bill's, not an estimate. */
|
|
46
|
-
billed: boolean;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* What each account is expected to be billed, month by month from `from` to `to`: the bill when one is written for the
|
|
50
|
-
* month, else what its subscriptions say (usage at the average of its latest bills).
|
|
51
|
-
*/
|
|
52
|
-
export declare function saasExpected(tx: EntityManager, domain: Domain, from: string, to: string): Promise<SaasExpectedRow[]>;
|
|
53
|
-
/**
|
|
54
|
-
* The bills of the months from `from` to `to` as series - all, an account, a subscription - and a department series: a
|
|
55
|
-
* line of a seat plan split by the seats held at the month's end by department, any other line its subscription's
|
|
56
|
-
* department. Plus what the latest month's bills are worth telling.
|
|
57
|
-
*/
|
|
58
|
-
export declare function saasCostTrend(tx: EntityManager, domain: Domain, from: string, to: string): Promise<{
|
|
59
|
-
months: string[];
|
|
60
|
-
all: import("./saas-rules").TrendSeries[];
|
|
61
|
-
accounts: {
|
|
62
|
-
name: string;
|
|
63
|
-
key: string;
|
|
64
|
-
accountId?: string | null;
|
|
65
|
-
subscriptionId?: string | null;
|
|
66
|
-
currency: string;
|
|
67
|
-
amounts: number[];
|
|
68
|
-
total: number;
|
|
69
|
-
monthOverMonth: number | null;
|
|
70
|
-
yearOverYear: number | null;
|
|
71
|
-
}[];
|
|
72
|
-
subscriptions: {
|
|
73
|
-
name: string;
|
|
74
|
-
accountName: string;
|
|
75
|
-
key: string;
|
|
76
|
-
accountId?: string | null;
|
|
77
|
-
subscriptionId?: string | null;
|
|
78
|
-
currency: string;
|
|
79
|
-
amounts: number[];
|
|
80
|
-
total: number;
|
|
81
|
-
monthOverMonth: number | null;
|
|
82
|
-
yearOverYear: number | null;
|
|
83
|
-
}[];
|
|
84
|
-
departments: {
|
|
85
|
-
total: number;
|
|
86
|
-
key: string;
|
|
87
|
-
departmentId: string | null;
|
|
88
|
-
name: string | null;
|
|
89
|
-
currency: string;
|
|
90
|
-
amounts: number[];
|
|
91
|
-
}[];
|
|
92
|
-
alerts: {
|
|
93
|
-
accountName: string;
|
|
94
|
-
subscriptionName: string;
|
|
95
|
-
accountId: string;
|
|
96
|
-
month: string;
|
|
97
|
-
kind: import("./saas-rules").SaasAlertKind;
|
|
98
|
-
subscriptionId?: string;
|
|
99
|
-
amount: number;
|
|
100
|
-
limit: number;
|
|
101
|
-
}[];
|
|
102
|
-
}>;
|
|
103
|
-
/**
|
|
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.
|
|
108
|
-
*/
|
|
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. */
|
|
111
|
-
export declare function saasPaymentsOf(tx: EntityManager, domain: Domain, scope: SaasScope, accountId: string): Promise<any[] | Transaction[]>;
|
|
112
|
-
/**
|
|
113
|
-
* Each account's last twelve months before this one and the next three expected (`saasExpected`). A month is its bill
|
|
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.
|
|
116
|
-
*/
|
|
117
|
-
export declare function saasTrendsOf(tx: EntityManager, domain: Domain, accounts: SaasAccount[], today: string, withBank?: boolean): Promise<Map<string, SaasTrend>>;
|
|
118
16
|
/** What moved on an account and on each of its subscriptions, newest first. */
|
|
119
17
|
export declare function saasChangesOf(tx: EntityManager, domain: Domain, scope: SaasScope, accountId: string): Promise<(import("@things-factory/fact-change").FactChange & {
|
|
120
18
|
subject: string;
|
|
121
19
|
})[]>;
|
|
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>;
|