@things-factory/saas 10.1.239 → 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
|
@@ -6,23 +6,10 @@ exports.saveSaasSubscriptions = saveSaasSubscriptions;
|
|
|
6
6
|
exports.saasSubscriptionsOf = saasSubscriptionsOf;
|
|
7
7
|
exports.saveSaasSeats = saveSaasSeats;
|
|
8
8
|
exports.saasSeatsOf = saasSeatsOf;
|
|
9
|
-
exports.saveSaasBill = saveSaasBill;
|
|
10
|
-
exports.deleteSaasBill = deleteSaasBill;
|
|
11
|
-
exports.billsOf = billsOf;
|
|
12
|
-
exports.saasBillsOf = saasBillsOf;
|
|
13
|
-
exports.saasExpected = saasExpected;
|
|
14
|
-
exports.saasCostTrend = saasCostTrend;
|
|
15
|
-
exports.vendorLinesOf = vendorLinesOf;
|
|
16
|
-
exports.saasPaymentsOf = saasPaymentsOf;
|
|
17
|
-
exports.saasTrendsOf = saasTrendsOf;
|
|
18
9
|
exports.saasChangesOf = saasChangesOf;
|
|
19
|
-
exports.saasCandidatesOf = saasCandidatesOf;
|
|
20
|
-
exports.createSaasAccountsFromCandidates = createSaasAccountsFromCandidates;
|
|
21
|
-
exports.hideSaasCandidates = hideSaasCandidates;
|
|
22
10
|
const accounting_1 = require("@things-factory/accounting");
|
|
23
|
-
const asset_1 = require("@things-factory/asset");
|
|
24
11
|
/* The register grids' save, the asset module's - this module saves its grids the same way (one implementation). */
|
|
25
|
-
const
|
|
12
|
+
const asset_1 = require("@things-factory/asset");
|
|
26
13
|
const biz_base_1 = require("@things-factory/biz-base");
|
|
27
14
|
const fact_change_1 = require("@things-factory/fact-change");
|
|
28
15
|
const organization_1 = require("@things-factory/organization");
|
|
@@ -30,8 +17,6 @@ const shell_1 = require("@things-factory/shell");
|
|
|
30
17
|
const typeorm_1 = require("typeorm");
|
|
31
18
|
const saas_access_1 = require("./saas-access");
|
|
32
19
|
const saas_account_1 = require("./saas-account");
|
|
33
|
-
const saas_bill_1 = require("./saas-bill");
|
|
34
|
-
const saas_hidden_vendor_1 = require("./saas-hidden-vendor");
|
|
35
20
|
const saas_rules_1 = require("./saas-rules");
|
|
36
21
|
const saas_seat_1 = require("./saas-seat");
|
|
37
22
|
const saas_subscription_1 = require("./saas-subscription");
|
|
@@ -66,8 +51,8 @@ const field = (patch, name, old) => (patch[name] !== undefined ? patch[name] : o
|
|
|
66
51
|
*/
|
|
67
52
|
exports.SAAS_ACCOUNT_FACT = 'saas-account';
|
|
68
53
|
exports.SAAS_SUBSCRIPTION_FACT = 'saas-subscription';
|
|
69
|
-
const ACCOUNT_TRACKED = ['name', 'counterpartyId', 'identifier', 'parentId', 'paymentAccountId', 'currency', '
|
|
70
|
-
const SUBSCRIPTION_TRACKED = ['name', 'billing', 'vendorKey', 'trial', 'quantity', 'unitPrice', '
|
|
54
|
+
const ACCOUNT_TRACKED = ['name', 'counterpartyId', 'identifier', 'parentId', 'paymentAccountId', 'currency', 'status', 'closedOn'];
|
|
55
|
+
const SUBSCRIPTION_TRACKED = ['name', 'billing', 'vendorKey', 'trial', 'quantity', 'unitPrice', 'startOn', 'renewalOn', 'autoRenew', 'noticeDays', 'departmentId', 'assetId', 'status', 'endedOn'];
|
|
71
56
|
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])));
|
|
72
57
|
async function recordChanges(tx, domain, user, factKind, old, row, fields) {
|
|
73
58
|
if (!old)
|
|
@@ -78,30 +63,22 @@ async function recordChanges(tx, domain, user, factKind, old, row, fields) {
|
|
|
78
63
|
/* ----------------------------------------------------------------------------------------------------------------- */
|
|
79
64
|
/* Accounts (decision 2) - kept by `saas` keepers only */
|
|
80
65
|
async function saveSaasAccounts(tx, domain, user, patches) {
|
|
81
|
-
const saved = await (0,
|
|
66
|
+
const saved = await (0, asset_1.applyPatches)(tx, saas_account_1.SaasAccount, domain, user, patches, async (patch, old) => {
|
|
82
67
|
const row = {};
|
|
83
|
-
const name = (0,
|
|
68
|
+
const name = (0, asset_1.touched)(patch, 'name', old) ? (0, asset_1.textOf)(patch.name) : old.name;
|
|
84
69
|
if (!name)
|
|
85
70
|
throw new shell_1.Refusal('saas-account-name-required', {}, 'Name the service');
|
|
86
71
|
row.name = name;
|
|
87
72
|
for (const one of ['identifier', 'purpose', 'billingEmail', 'secretLocation', 'description'])
|
|
88
|
-
if ((0,
|
|
89
|
-
row[one] = (0,
|
|
90
|
-
if ((0,
|
|
73
|
+
if ((0, asset_1.touched)(patch, one, old))
|
|
74
|
+
row[one] = (0, asset_1.textOf)(patch[one]);
|
|
75
|
+
if ((0, asset_1.touched)(patch, 'currency', old))
|
|
91
76
|
row.currency = currencyOf(patch.currency, 'KRW');
|
|
92
|
-
if ((0,
|
|
93
|
-
row.billingDay = intOf(patch.billingDay, 'saas-billing-day-invalid', 1, 31);
|
|
94
|
-
if ((0, patches_js_1.touched)(patch, 'increaseAlertRate', old)) {
|
|
95
|
-
const rate = (0, patches_js_1.amountOf)(patch.increaseAlertRate, 'saas-alert-rate-invalid');
|
|
96
|
-
if (rate !== null && rate > 10)
|
|
97
|
-
throw new shell_1.Refusal('saas-alert-rate-invalid', {}, 'Give the rate as a part - 0.2 for 20%');
|
|
98
|
-
row.increaseAlertRate = rate;
|
|
99
|
-
}
|
|
100
|
-
if ((0, patches_js_1.touched)(patch, 'counterpartyId', old))
|
|
77
|
+
if ((0, asset_1.touched)(patch, 'counterpartyId', old))
|
|
101
78
|
row.counterparty = await oneOf(tx, biz_base_1.Counterparty, domain, patch.counterpartyId, 'saas-counterparty-not-found');
|
|
102
|
-
if ((0,
|
|
79
|
+
if ((0, asset_1.touched)(patch, 'paymentAccountId', old))
|
|
103
80
|
row.paymentAccount = await oneOf(tx, accounting_1.BankAccount, domain, patch.paymentAccountId, 'saas-payment-account-not-found');
|
|
104
|
-
if ((0,
|
|
81
|
+
if ((0, asset_1.touched)(patch, 'parentId', old)) {
|
|
105
82
|
const parent = await oneOf(tx, saas_account_1.SaasAccount, domain, patch.parentId, 'saas-parent-not-found');
|
|
106
83
|
if (parent && (parent.id === old?.id || (await tx.getRepository(saas_account_1.SaasAccount).findOne({ where: { id: parent.id }, relations: ['parent'] }))?.parent))
|
|
107
84
|
throw new shell_1.Refusal('saas-parent-invalid', {}, 'An account is under a top account only, and not under itself');
|
|
@@ -109,26 +86,22 @@ async function saveSaasAccounts(tx, domain, user, patches) {
|
|
|
109
86
|
throw new shell_1.Refusal('saas-parent-has-subscriptions', {}, 'An account with subscriptions is billed itself - it cannot go under another');
|
|
110
87
|
row.parent = parent;
|
|
111
88
|
}
|
|
112
|
-
if ((0,
|
|
89
|
+
if ((0, asset_1.touched)(patch, 'adminIds', old)) {
|
|
113
90
|
const ids = [...new Set((patch.adminIds ?? []).filter(Boolean))];
|
|
114
91
|
const admins = ids.length ? await tx.getRepository(organization_1.Employee).find({ where: { id: (0, typeorm_1.In)(ids), domain: { id: domain.id } } }) : [];
|
|
115
92
|
if (admins.length !== ids.length)
|
|
116
93
|
throw new shell_1.Refusal('saas-admin-not-found', {}, 'An admin is no employee of this domain');
|
|
117
94
|
row.admins = admins;
|
|
118
95
|
}
|
|
119
|
-
if ((0,
|
|
96
|
+
if ((0, asset_1.touched)(patch, 'status', old))
|
|
120
97
|
row.status = patch.status ?? saas_rules_1.SaasAccountStatus.ACTIVE;
|
|
121
|
-
if ((0,
|
|
122
|
-
row.closedOn = (0,
|
|
98
|
+
if ((0, asset_1.touched)(patch, 'closedOn', old))
|
|
99
|
+
row.closedOn = (0, asset_1.dayOf)(patch.closedOn, 'saas-day-invalid');
|
|
123
100
|
if ((row.status ?? old?.status) === saas_rules_1.SaasAccountStatus.CLOSED && !(row.closedOn !== undefined ? row.closedOn : old?.closedOn))
|
|
124
101
|
throw new shell_1.Refusal('saas-closed-on-required', {}, 'Give the day it was closed');
|
|
125
102
|
await recordChanges(tx, domain, user, exports.SAAS_ACCOUNT_FACT, old, row, ACCOUNT_TRACKED);
|
|
126
103
|
return row;
|
|
127
104
|
}, '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
105
|
return saved;
|
|
133
106
|
}
|
|
134
107
|
/* ----------------------------------------------------------------------------------------------------------------- */
|
|
@@ -137,9 +110,9 @@ async function saveSaasSubscriptions(tx, domain, user, scope, accountId, patches
|
|
|
137
110
|
const account = await (0, saas_access_1.accountIn)(tx, domain, scope, accountId);
|
|
138
111
|
if (account.parentId && patches.some(one => one.cuFlag !== '-'))
|
|
139
112
|
throw new shell_1.Refusal('saas-subscription-on-sub-account', {}, 'An account under another is billed by its parent - subscribe there');
|
|
140
|
-
return (0,
|
|
113
|
+
return (0, asset_1.applyPatches)(tx, saas_subscription_1.SaasSubscription, domain, user, patches, async (patch, old) => {
|
|
141
114
|
const row = old ? {} : { account };
|
|
142
|
-
const name = (0,
|
|
115
|
+
const name = (0, asset_1.touched)(patch, 'name', old) ? (0, asset_1.textOf)(patch.name) : old.name;
|
|
143
116
|
if (!name)
|
|
144
117
|
throw new shell_1.Refusal('saas-subscription-name-required', {}, 'Name the product or plan');
|
|
145
118
|
row.name = name;
|
|
@@ -147,43 +120,36 @@ async function saveSaasSubscriptions(tx, domain, user, scope, accountId, patches
|
|
|
147
120
|
if (!Object.values(saas_rules_1.SaasBilling).includes(billing))
|
|
148
121
|
throw new shell_1.Refusal('saas-billing-invalid', {}, 'Say how it is paid for');
|
|
149
122
|
row.billing = billing;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
123
|
+
/* Reference only - the money is the recurring item's (개정 7-3). */
|
|
124
|
+
for (const one of ['quantity', 'unitPrice'])
|
|
125
|
+
if ((0, asset_1.touched)(patch, one, old))
|
|
126
|
+
row[one] = (0, asset_1.amountOf)(patch[one], 'saas-amount-invalid');
|
|
153
127
|
/* `vendorKey` and `trial` are a sync's (개정 2) - the GraphQL patch does not carry them. */
|
|
154
|
-
if ((0,
|
|
155
|
-
row.vendorKey = (0,
|
|
156
|
-
if ((0,
|
|
128
|
+
if ((0, asset_1.touched)(patch, 'vendorKey', old))
|
|
129
|
+
row.vendorKey = (0, asset_1.textOf)(patch.vendorKey);
|
|
130
|
+
if ((0, asset_1.touched)(patch, 'trial', old))
|
|
157
131
|
row.trial = patch.trial === true;
|
|
158
132
|
const value = (name) => (row[name] !== undefined ? row[name] : old?.[name]);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if ((0, patches_js_1.touched)(patch, 'everyMonths', old))
|
|
166
|
-
row.everyMonths = intOf(patch.everyMonths, 'saas-every-months-invalid', 1, 120) ?? 1;
|
|
167
|
-
row.startOn = (0, patches_js_1.touched)(patch, 'startOn', old) ? (0, patches_js_1.dayOf)(patch.startOn, 'saas-start-on-invalid', true) : old.startOn;
|
|
168
|
-
if ((0, patches_js_1.touched)(patch, 'renewalOn', old))
|
|
169
|
-
row.renewalOn = (0, patches_js_1.dayOf)(patch.renewalOn, 'saas-day-invalid');
|
|
170
|
-
if ((0, patches_js_1.touched)(patch, 'endedOn', old))
|
|
171
|
-
row.endedOn = (0, patches_js_1.dayOf)(patch.endedOn, 'saas-day-invalid');
|
|
172
|
-
if ((0, patches_js_1.touched)(patch, 'autoRenew', old))
|
|
133
|
+
row.startOn = (0, asset_1.touched)(patch, 'startOn', old) ? (0, asset_1.dayOf)(patch.startOn, 'saas-start-on-invalid', true) : old.startOn;
|
|
134
|
+
if ((0, asset_1.touched)(patch, 'renewalOn', old))
|
|
135
|
+
row.renewalOn = (0, asset_1.dayOf)(patch.renewalOn, 'saas-day-invalid');
|
|
136
|
+
if ((0, asset_1.touched)(patch, 'endedOn', old))
|
|
137
|
+
row.endedOn = (0, asset_1.dayOf)(patch.endedOn, 'saas-day-invalid');
|
|
138
|
+
if ((0, asset_1.touched)(patch, 'autoRenew', old))
|
|
173
139
|
row.autoRenew = patch.autoRenew !== false;
|
|
174
|
-
if ((0,
|
|
140
|
+
if ((0, asset_1.touched)(patch, 'noticeDays', old))
|
|
175
141
|
row.noticeDays = intOf(patch.noticeDays, 'saas-notice-days-invalid', 0, 365);
|
|
176
|
-
if ((0,
|
|
177
|
-
row.description = (0,
|
|
178
|
-
if ((0,
|
|
142
|
+
if ((0, asset_1.touched)(patch, 'description', old))
|
|
143
|
+
row.description = (0, asset_1.textOf)(patch.description);
|
|
144
|
+
if ((0, asset_1.touched)(patch, 'departmentId', old))
|
|
179
145
|
row.department = await oneOf(tx, organization_1.Department, domain, patch.departmentId, 'saas-department-not-found');
|
|
180
|
-
if ((0,
|
|
146
|
+
if ((0, asset_1.touched)(patch, 'assetId', old))
|
|
181
147
|
row.asset = await oneOf(tx, asset_1.Asset, domain, patch.assetId, 'saas-asset-not-found');
|
|
182
148
|
if (billing === saas_rules_1.SaasBilling.OWNED && !(row.asset !== undefined ? row.asset : old?.assetId))
|
|
183
|
-
throw new shell_1.Refusal('saas-owned-asset-required', {}, 'A license owned points at its asset in the register
|
|
149
|
+
throw new shell_1.Refusal('saas-owned-asset-required', {}, 'A license owned points at its asset in the register');
|
|
184
150
|
if (billing === saas_rules_1.SaasBilling.COMMITMENT && !value('renewalOn'))
|
|
185
151
|
throw new shell_1.Refusal('saas-term-end-required', {}, 'A commitment takes the day its term ends');
|
|
186
|
-
if ((0,
|
|
152
|
+
if ((0, asset_1.touched)(patch, 'status', old))
|
|
187
153
|
row.status = patch.status ?? saas_rules_1.SaasSubscriptionStatus.ACTIVE;
|
|
188
154
|
const status = row.status ?? old?.status ?? saas_rules_1.SaasSubscriptionStatus.ACTIVE;
|
|
189
155
|
if (status === saas_rules_1.SaasSubscriptionStatus.CANCELLED && !value('endedOn'))
|
|
@@ -196,13 +162,15 @@ async function saveSaasSubscriptions(tx, domain, user, scope, accountId, patches
|
|
|
196
162
|
return row;
|
|
197
163
|
}, 'saas-subscription-not-found', { account: { id: account.id } });
|
|
198
164
|
}
|
|
199
|
-
/** The subscriptions of an account, with the seats held today. */
|
|
165
|
+
/** The subscriptions of an account, with the seats held today and the reference each makes. */
|
|
200
166
|
async function saasSubscriptionsOf(tx, domain, scope, accountId, today) {
|
|
201
167
|
const account = await (0, saas_access_1.accountIn)(tx, domain, scope, accountId);
|
|
202
168
|
const subs = await tx.getRepository(saas_subscription_1.SaasSubscription).find({ where: { domain: { id: domain.id }, account: { id: account.id } }, relations: ['department', 'asset'], order: { name: 'ASC' } });
|
|
203
169
|
const seats = subs.length ? await tx.getRepository(saas_seat_1.SaasSeat).find({ where: { domain: { id: domain.id }, subscription: { id: (0, typeorm_1.In)(subs.map(one => one.id)) } } }) : [];
|
|
204
|
-
for (const sub of subs)
|
|
170
|
+
for (const sub of subs) {
|
|
205
171
|
sub.seatsHeld = seats.filter(seat => seat.subscriptionId === sub.id && (0, saas_rules_1.seatHeld)(seat, today)).length;
|
|
172
|
+
sub.referenceAmount = (0, saas_rules_1.referenceAmountOf)(sub);
|
|
173
|
+
}
|
|
206
174
|
return subs;
|
|
207
175
|
}
|
|
208
176
|
/* ----------------------------------------------------------------------------------------------------------------- */
|
|
@@ -215,20 +183,20 @@ async function subscriptionIn(tx, domain, scope, subscriptionId) {
|
|
|
215
183
|
}
|
|
216
184
|
async function saveSaasSeats(tx, domain, user, scope, subscriptionId, patches) {
|
|
217
185
|
const subscription = await subscriptionIn(tx, domain, scope, subscriptionId);
|
|
218
|
-
return (0,
|
|
186
|
+
return (0, asset_1.applyPatches)(tx, saas_seat_1.SaasSeat, domain, user, patches, async (patch, old) => {
|
|
219
187
|
const row = old ? {} : { subscription };
|
|
220
|
-
if ((0,
|
|
188
|
+
if ((0, asset_1.touched)(patch, 'employeeId', old))
|
|
221
189
|
row.employee = await oneOf(tx, organization_1.Employee, domain, patch.employeeId, 'saas-seat-employee-not-found');
|
|
222
190
|
/* `login` is a sync's (개정 2) - the GraphQL patch does not carry it. */
|
|
223
191
|
for (const one of ['holder', 'login', 'description'])
|
|
224
|
-
if ((0,
|
|
225
|
-
row[one] = (0,
|
|
192
|
+
if ((0, asset_1.touched)(patch, one, old))
|
|
193
|
+
row[one] = (0, asset_1.textOf)(patch[one]);
|
|
226
194
|
if (!(row.employee !== undefined ? row.employee : old?.employeeId) && !(row.holder !== undefined ? row.holder : old?.holder))
|
|
227
195
|
throw new shell_1.Refusal('saas-seat-holder-required', {}, 'Say who holds the seat - an employee, or a name');
|
|
228
|
-
if ((0,
|
|
229
|
-
row.fromOn = (0,
|
|
230
|
-
if ((0,
|
|
231
|
-
row.untilOn = (0,
|
|
196
|
+
if ((0, asset_1.touched)(patch, 'fromOn', old))
|
|
197
|
+
row.fromOn = (0, asset_1.dayOf)(patch.fromOn, 'saas-day-invalid');
|
|
198
|
+
if ((0, asset_1.touched)(patch, 'untilOn', old))
|
|
199
|
+
row.untilOn = (0, asset_1.dayOf)(patch.untilOn, 'saas-day-invalid');
|
|
232
200
|
const from = row.fromOn !== undefined ? row.fromOn : old?.fromOn;
|
|
233
201
|
const until = row.untilOn !== undefined ? row.untilOn : old?.untilOn;
|
|
234
202
|
if (from && until && until < from)
|
|
@@ -240,272 +208,8 @@ async function saasSeatsOf(tx, domain, scope, subscriptionId) {
|
|
|
240
208
|
const sub = await subscriptionIn(tx, domain, scope, subscriptionId);
|
|
241
209
|
return tx.getRepository(saas_seat_1.SaasSeat).find({ where: { domain: { id: domain.id }, subscription: { id: sub.id } }, relations: ['employee'], order: { fromOn: 'DESC' } });
|
|
242
210
|
}
|
|
243
|
-
async function saveSaasBill(tx, domain, user, scope, accountId, input) {
|
|
244
|
-
const account = await (0, saas_access_1.accountIn)(tx, domain, scope, accountId);
|
|
245
|
-
if (account.parentId)
|
|
246
|
-
throw new shell_1.Refusal('saas-bill-on-sub-account', {}, 'An account under another is billed by its parent');
|
|
247
|
-
const month = String(input.month ?? '').trim();
|
|
248
|
-
if (!saas_rules_1.MONTH.test(month))
|
|
249
|
-
throw new shell_1.Refusal('saas-month-invalid', { value: month }, 'Write the month as YYYY-MM');
|
|
250
|
-
const bills = tx.getRepository(saas_bill_1.SaasBill);
|
|
251
|
-
const old = input.id ? await bills.findOne({ where: { domain: { id: domain.id }, id: input.id, account: { id: account.id } } }) : null;
|
|
252
|
-
if (input.id && !old)
|
|
253
|
-
throw new shell_1.Refusal('saas-bill-not-found', {}, 'The bill was not found - reload and try again');
|
|
254
|
-
const twin = await bills.findOne({ where: { domain: { id: domain.id }, account: { id: account.id }, month } });
|
|
255
|
-
if (twin && twin.id !== old?.id)
|
|
256
|
-
throw new shell_1.Refusal('saas-bill-month-taken', { month }, `The account already has a bill for ${month} - change that one`);
|
|
257
|
-
const lines = (input.lines ?? []).filter(line => line && (line.subscriptionId || (0, patches_js_1.textOf)(line.label) || Number(line.amount)));
|
|
258
|
-
if (!lines.length)
|
|
259
|
-
throw new shell_1.Refusal('saas-bill-lines-required', {}, 'A bill takes at least one line');
|
|
260
|
-
const subIds = [...new Set(lines.map(line => line.subscriptionId).filter(Boolean))];
|
|
261
|
-
if (subIds.length) {
|
|
262
|
-
const found = await tx.getRepository(saas_subscription_1.SaasSubscription).count({ where: { domain: { id: domain.id }, account: { id: account.id }, id: (0, typeorm_1.In)(subIds) } });
|
|
263
|
-
if (found !== subIds.length)
|
|
264
|
-
throw new shell_1.Refusal('saas-bill-subscription-not-found', {}, 'A line names a subscription not of this account');
|
|
265
|
-
}
|
|
266
|
-
for (const line of lines)
|
|
267
|
-
if (!Number.isFinite(Number(line.amount)))
|
|
268
|
-
throw new shell_1.Refusal('saas-amount-invalid', {}, 'Give the amount as a number');
|
|
269
|
-
const bill = await bills.save({
|
|
270
|
-
...(old ?? { domain, account, creator: user, currency: account.currency }),
|
|
271
|
-
month,
|
|
272
|
-
billedOn: (0, patches_js_1.dayOf)(input.billedOn, 'saas-day-invalid'),
|
|
273
|
-
description: (0, patches_js_1.textOf)(input.description),
|
|
274
|
-
/* Whoever writes it owns it: a person's save keeps a later sync off it (a sync marks its own, 개정 3). */
|
|
275
|
-
synced: false,
|
|
276
|
-
updater: user
|
|
277
|
-
});
|
|
278
|
-
await tx.getRepository(saas_bill_1.SaasBillLine).delete({ bill: { id: bill.id } });
|
|
279
|
-
await tx.getRepository(saas_bill_1.SaasBillLine).save(lines.map(line => ({ domain, bill: { id: bill.id }, subscription: line.subscriptionId ? { id: line.subscriptionId } : null, label: (0, patches_js_1.textOf)(line.label), amount: Number(line.amount) })));
|
|
280
|
-
return billsOf(tx, domain, [account.id]).then(all => all.find(one => one.id === bill.id));
|
|
281
|
-
}
|
|
282
|
-
async function deleteSaasBill(tx, domain, scope, id) {
|
|
283
|
-
const bill = await tx.getRepository(saas_bill_1.SaasBill).findOne({ where: { domain: { id: domain.id }, id } });
|
|
284
|
-
if (!bill || (scope !== 'all' && !scope.has(bill.accountId)))
|
|
285
|
-
throw new shell_1.Refusal('saas-bill-not-found', {}, 'The bill was not found - reload and try again');
|
|
286
|
-
await tx.getRepository(saas_bill_1.SaasBill).softDelete({ id });
|
|
287
|
-
return true;
|
|
288
|
-
}
|
|
289
|
-
/** The bills of some accounts (all when none named) from a month on, with their lines and totals, latest first. */
|
|
290
|
-
async function billsOf(tx, domain, accountIds, from, to) {
|
|
291
|
-
const query = tx
|
|
292
|
-
.getRepository(saas_bill_1.SaasBill)
|
|
293
|
-
.createQueryBuilder('bill')
|
|
294
|
-
.leftJoinAndSelect('bill.lines', 'line')
|
|
295
|
-
.leftJoinAndSelect('line.subscription', 'subscription')
|
|
296
|
-
.where('bill.domain = :domain', { domain: domain.id });
|
|
297
|
-
if (accountIds) {
|
|
298
|
-
if (!accountIds.length)
|
|
299
|
-
return [];
|
|
300
|
-
query.andWhere('bill.account IN (:...accounts)', { accounts: accountIds });
|
|
301
|
-
}
|
|
302
|
-
if (from)
|
|
303
|
-
query.andWhere('bill.month >= :from', { from });
|
|
304
|
-
if (to)
|
|
305
|
-
query.andWhere('bill.month <= :to', { to });
|
|
306
|
-
const bills = await query.orderBy('bill.month', 'DESC').getMany();
|
|
307
|
-
for (const bill of bills)
|
|
308
|
-
bill.total = (0, saas_rules_1.billTotal)(bill.lines ?? []);
|
|
309
|
-
return bills;
|
|
310
|
-
}
|
|
311
|
-
async function saasBillsOf(tx, domain, scope, accountId) {
|
|
312
|
-
const account = await (0, saas_access_1.accountIn)(tx, domain, scope, accountId);
|
|
313
|
-
return billsOf(tx, domain, [account.id]);
|
|
314
|
-
}
|
|
315
|
-
const factsOf = (bill) => ({
|
|
316
|
-
accountId: bill.accountId,
|
|
317
|
-
month: bill.month,
|
|
318
|
-
currency: bill.currency,
|
|
319
|
-
lines: (bill.lines ?? []).map(line => ({ subscriptionId: line.subscriptionId ?? null, amount: Number(line.amount) }))
|
|
320
|
-
});
|
|
321
|
-
/**
|
|
322
|
-
* What each account is expected to be billed, month by month from `from` to `to`: the bill when one is written for the
|
|
323
|
-
* month, else what its subscriptions say (usage at the average of its latest bills).
|
|
324
|
-
*/
|
|
325
|
-
async function saasExpected(tx, domain, from, to) {
|
|
326
|
-
const accounts = await tx.getRepository(saas_account_1.SaasAccount).find({ where: { domain: { id: domain.id }, status: saas_rules_1.SaasAccountStatus.ACTIVE } });
|
|
327
|
-
const top = accounts.filter(one => !one.parentId);
|
|
328
|
-
if (!top.length)
|
|
329
|
-
return [];
|
|
330
|
-
const subs = await tx.getRepository(saas_subscription_1.SaasSubscription).find({ where: { domain: { id: domain.id }, account: { id: (0, typeorm_1.In)(top.map(one => one.id)) } } });
|
|
331
|
-
const bills = await billsOf(tx, domain, top.map(one => one.id), (0, saas_rules_1.shiftMonth)(from, -12), to);
|
|
332
|
-
const billed = new Map();
|
|
333
|
-
for (const bill of bills)
|
|
334
|
-
if (bill.month < from)
|
|
335
|
-
for (const line of bill.lines ?? [])
|
|
336
|
-
if (line.subscriptionId)
|
|
337
|
-
billed.set(line.subscriptionId, [...(billed.get(line.subscriptionId) ?? []), { month: bill.month, amount: Number(line.amount) }]);
|
|
338
|
-
const rows = [];
|
|
339
|
-
for (const account of top) {
|
|
340
|
-
const own = subs.filter(one => one.accountId === account.id);
|
|
341
|
-
for (const month of (0, saas_rules_1.monthsFrom)(from, to)) {
|
|
342
|
-
const [year, m] = month.split('-').map(Number);
|
|
343
|
-
const bill = bills.find(one => one.accountId === account.id && one.month === month);
|
|
344
|
-
const expected = (0, saas_rules_1.expectedOfMonth)(account, own, billed, year, m);
|
|
345
|
-
if (!bill && !expected)
|
|
346
|
-
continue;
|
|
347
|
-
rows.push({
|
|
348
|
-
accountId: account.id,
|
|
349
|
-
accountName: account.name,
|
|
350
|
-
counterpartyId: account.counterpartyId ?? null,
|
|
351
|
-
paymentAccountId: account.paymentAccountId ?? null,
|
|
352
|
-
month,
|
|
353
|
-
day: bill?.billedOn ?? expected?.day ?? null,
|
|
354
|
-
amount: bill ? bill.total : expected.amount,
|
|
355
|
-
currency: account.currency,
|
|
356
|
-
billed: !!bill
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
return rows;
|
|
361
|
-
}
|
|
362
211
|
/* ----------------------------------------------------------------------------------------------------------------- */
|
|
363
|
-
/*
|
|
364
|
-
/** Whose department each employee is in. */
|
|
365
|
-
async function departmentsOfEmployees(tx, domain, ids) {
|
|
366
|
-
if (!ids.length)
|
|
367
|
-
return new Map();
|
|
368
|
-
const employees = await tx.getRepository(organization_1.Employee).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(ids) }, relations: ['department'] });
|
|
369
|
-
return new Map(employees.filter(one => one.department).map(one => [one.id, { id: one.department.id, name: one.department.name }]));
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* The bills of the months from `from` to `to` as series - all, an account, a subscription - and a department series: a
|
|
373
|
-
* line of a seat plan split by the seats held at the month's end by department, any other line its subscription's
|
|
374
|
-
* department. Plus what the latest month's bills are worth telling.
|
|
375
|
-
*/
|
|
376
|
-
async function saasCostTrend(tx, domain, from, to) {
|
|
377
|
-
if (!saas_rules_1.MONTH.test(from) || !saas_rules_1.MONTH.test(to) || from > to)
|
|
378
|
-
throw new shell_1.Refusal('saas-month-invalid', {}, 'Give the months as YYYY-MM, the first before the last');
|
|
379
|
-
if ((0, saas_rules_1.monthsFrom)(from, to).length > 60)
|
|
380
|
-
throw new shell_1.Refusal('saas-trend-too-long', { max: 60 }, 'Up to 60 months at once');
|
|
381
|
-
const bills = await billsOf(tx, domain, undefined, (0, saas_rules_1.shiftMonth)(from, -1), to);
|
|
382
|
-
const facts = bills.map(factsOf);
|
|
383
|
-
const trend = (0, saas_rules_1.trendOf)(facts, from, to);
|
|
384
|
-
const accounts = await tx.getRepository(saas_account_1.SaasAccount).find({ where: { domain: { id: domain.id } }, withDeleted: true });
|
|
385
|
-
const subs = await tx.getRepository(saas_subscription_1.SaasSubscription).find({ where: { domain: { id: domain.id } }, relations: ['department'], withDeleted: true });
|
|
386
|
-
const seats = subs.length ? await tx.getRepository(saas_seat_1.SaasSeat).find({ where: { domain: { id: domain.id }, subscription: { id: (0, typeorm_1.In)(subs.map(one => one.id)) } } }) : [];
|
|
387
|
-
const departmentOf = await departmentsOfEmployees(tx, domain, [...new Set(seats.map(one => one.employeeId).filter(Boolean))]);
|
|
388
|
-
const months = trend.months;
|
|
389
|
-
const departments = new Map();
|
|
390
|
-
const addTo = (id, name, currency, at, amount) => {
|
|
391
|
-
const key = `${id ?? ''}|${currency}`;
|
|
392
|
-
let one = departments.get(key);
|
|
393
|
-
if (!one)
|
|
394
|
-
departments.set(key, (one = { key, departmentId: id, name, currency, amounts: months.map(() => 0) }));
|
|
395
|
-
one.amounts[at] += amount;
|
|
396
|
-
};
|
|
397
|
-
for (const bill of facts) {
|
|
398
|
-
const at = months.indexOf(bill.month);
|
|
399
|
-
if (at < 0)
|
|
400
|
-
continue;
|
|
401
|
-
const monthEnd = `${bill.month}-31`;
|
|
402
|
-
for (const line of bill.lines) {
|
|
403
|
-
const sub = subs.find(one => one.id === line.subscriptionId);
|
|
404
|
-
const held = sub?.billing === saas_rules_1.SaasBilling.SEAT ? seats.filter(seat => seat.subscriptionId === sub.id && (0, saas_rules_1.seatHeld)(seat, monthEnd) && (!seat.fromOn || seat.fromOn <= monthEnd)) : [];
|
|
405
|
-
if (held.length) {
|
|
406
|
-
for (const seat of held) {
|
|
407
|
-
const department = seat.employeeId ? departmentOf.get(seat.employeeId) : undefined;
|
|
408
|
-
addTo(department?.id ?? sub?.department?.id ?? null, department?.name ?? sub?.department?.name ?? null, bill.currency, at, line.amount / held.length);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
else
|
|
412
|
-
addTo(sub?.department?.id ?? null, sub?.department?.name ?? null, bill.currency, at, line.amount);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
const latest = facts.filter(one => one.month <= to).map(one => one.month).sort().pop();
|
|
416
|
-
const alerts = latest
|
|
417
|
-
? facts
|
|
418
|
-
.filter(one => one.month === latest)
|
|
419
|
-
.flatMap(bill => {
|
|
420
|
-
const account = accounts.find(one => one.id === bill.accountId);
|
|
421
|
-
if (!account)
|
|
422
|
-
return [];
|
|
423
|
-
const previous = facts.find(one => one.accountId === bill.accountId && one.month === (0, saas_rules_1.shiftMonth)(bill.month, -1));
|
|
424
|
-
return (0, saas_rules_1.alertsOf)(account, subs.filter(one => one.accountId === account.id), bill, previous).map(alert => ({ ...alert, accountId: account.id, month: bill.month }));
|
|
425
|
-
})
|
|
426
|
-
: [];
|
|
427
|
-
const name = (id) => accounts.find(one => one.id === id)?.name ?? null;
|
|
428
|
-
const subName = (id) => subs.find(one => one.id === id)?.name ?? null;
|
|
429
|
-
return {
|
|
430
|
-
months,
|
|
431
|
-
all: trend.all,
|
|
432
|
-
accounts: trend.accounts.map(one => ({ ...one, name: name(one.accountId) })),
|
|
433
|
-
subscriptions: trend.subscriptions.map(one => ({ ...one, name: subName(one.subscriptionId), accountName: name(one.accountId) })),
|
|
434
|
-
departments: [...departments.values()].map(one => ({ ...one, total: one.amounts.reduce((sum, amount) => sum + amount, 0) })).sort((a, b) => b.total - a.total),
|
|
435
|
-
alerts: alerts.map(alert => ({ ...alert, accountName: name(alert.accountId), subscriptionName: subName(alert.subscriptionId) }))
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
/* ----------------------------------------------------------------------------------------------------------------- */
|
|
439
|
-
/* A row's months for the list and the 360 (decision 10) and what moved on it (decision 11) */
|
|
440
|
-
/**
|
|
441
|
-
* What went out to an account's vendor (ADR-0177 decision 10, the user 2026-10-30: 「이테크 입금 이력이 있을텐데」;
|
|
442
|
-
* 개정 1) - the bank withdrawals and the card charges (accounting, CONFIRMED and CARD) to the account's counterparty,
|
|
443
|
-
* newest first. A line takes its counterparty when it lands (accounting `saveTransactions`), so a vendor's lines are
|
|
444
|
-
* its linked ones. Only lines in the account's currency count toward its months.
|
|
445
|
-
*/
|
|
446
|
-
async function vendorLinesOf(tx, domain, counterpartyIds, from, to) {
|
|
447
|
-
if (!counterpartyIds.length)
|
|
448
|
-
return [];
|
|
449
|
-
const query = tx
|
|
450
|
-
.getRepository(accounting_1.Transaction)
|
|
451
|
-
.createQueryBuilder('line')
|
|
452
|
-
.leftJoinAndSelect('line.bankAccount', 'bankAccount')
|
|
453
|
-
.where('line.domain = :domain', { domain: domain.id })
|
|
454
|
-
.andWhere('line.category IN (:...categories)', { categories: [accounting_1.TransactionCategory.CONFIRMED, accounting_1.TransactionCategory.CARD] })
|
|
455
|
-
.andWhere('line.type = :type', { type: accounting_1.CashFlowType.EXPENSE })
|
|
456
|
-
.andWhere('line.counterparty IN (:...parties)', { parties: counterpartyIds });
|
|
457
|
-
if (from)
|
|
458
|
-
query.andWhere('line.transactionDate >= :from', { from });
|
|
459
|
-
if (to)
|
|
460
|
-
query.andWhere('line.transactionDate <= :to', { to });
|
|
461
|
-
return query.orderBy('line.transactionDate', 'DESC').addOrderBy('line.amount', 'DESC').getMany();
|
|
462
|
-
}
|
|
463
|
-
/** The bank and card lines that paid an account's vendor, every one - for its 360. */
|
|
464
|
-
async function saasPaymentsOf(tx, domain, scope, accountId) {
|
|
465
|
-
const account = await (0, saas_access_1.accountIn)(tx, domain, scope, accountId);
|
|
466
|
-
return account.counterpartyId ? vendorLinesOf(tx, domain, [account.counterpartyId]) : [];
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Each account's last twelve months before this one and the next three expected (`saasExpected`). A month is its bill
|
|
470
|
-
* when one is written; else, for a reader of the bank lines (`withBank`), what went out to its vendor that month from
|
|
471
|
-
* the bank or a card (decision 10, 개정 1); else empty. One read for all the accounts of a list.
|
|
472
|
-
*/
|
|
473
|
-
async function saasTrendsOf(tx, domain, accounts, today, withBank = false) {
|
|
474
|
-
const trends = new Map();
|
|
475
|
-
if (!accounts.length)
|
|
476
|
-
return trends;
|
|
477
|
-
const now = today.slice(0, 7);
|
|
478
|
-
const months = (0, saas_rules_1.monthsFrom)((0, saas_rules_1.shiftMonth)(now, -12), (0, saas_rules_1.shiftMonth)(now, -1));
|
|
479
|
-
const nextMonths = (0, saas_rules_1.monthsFrom)(now, (0, saas_rules_1.shiftMonth)(now, 2));
|
|
480
|
-
const bills = await billsOf(tx, domain, accounts.map(one => one.id), months[0], months[months.length - 1]);
|
|
481
|
-
const expected = await saasExpected(tx, domain, nextMonths[0], nextMonths[nextMonths.length - 1]);
|
|
482
|
-
const parties = [...new Set(accounts.map(one => one.counterpartyId).filter(Boolean))];
|
|
483
|
-
const paid = withBank ? await vendorLinesOf(tx, domain, parties, `${months[0]}-01`, `${months[months.length - 1]}-31`) : [];
|
|
484
|
-
for (const account of accounts) {
|
|
485
|
-
const own = bills.filter(one => one.accountId === account.id);
|
|
486
|
-
const lines = paid.filter(line => line.counterpartyId === account.counterpartyId && (line.currency || 'KRW') === account.currency);
|
|
487
|
-
const banked = (month) => {
|
|
488
|
-
const of = lines.filter(line => line.transactionDate?.slice(0, 7) === month);
|
|
489
|
-
return of.length ? of.reduce((sum, line) => sum + Number(line.amount), 0) : null;
|
|
490
|
-
};
|
|
491
|
-
const values = months.map(month => own.find(one => one.month === month)?.total ?? banked(month));
|
|
492
|
-
trends.set(account.id, {
|
|
493
|
-
months,
|
|
494
|
-
values,
|
|
495
|
-
sources: months.map((month, i) => own.some(one => one.month === month)
|
|
496
|
-
? 'bill'
|
|
497
|
-
: values[i] === null
|
|
498
|
-
? null
|
|
499
|
-
: lines.some(line => line.transactionDate?.slice(0, 7) === month && line.category === accounting_1.TransactionCategory.CONFIRMED)
|
|
500
|
-
? 'bank'
|
|
501
|
-
: 'card'),
|
|
502
|
-
nextMonths,
|
|
503
|
-
expected: nextMonths.map(month => expected.find(one => one.accountId === account.id && one.month === month)?.amount ?? null),
|
|
504
|
-
currency: account.currency
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
return trends;
|
|
508
|
-
}
|
|
212
|
+
/* What moved on a row (decision 11) */
|
|
509
213
|
/** What moved on an account and on each of its subscriptions, newest first. */
|
|
510
214
|
async function saasChangesOf(tx, domain, scope, accountId) {
|
|
511
215
|
const account = await (0, saas_access_1.accountIn)(tx, domain, scope, accountId);
|
|
@@ -516,96 +220,4 @@ async function saasChangesOf(tx, domain, scope, accountId) {
|
|
|
516
220
|
];
|
|
517
221
|
return rows.sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime());
|
|
518
222
|
}
|
|
519
|
-
/**
|
|
520
|
-
* Whom the bank and card lines of the last twelve months show paid month after month that is no account yet and no
|
|
521
|
-
* recurring item - what a keeper may make an account of with one press. A line on a subject outside profit and loss
|
|
522
|
-
* (a card bill, a tax, a transfer, a loan) pays no vendor, and a person (salary, expenses) is no vendor. With `hidden`,
|
|
523
|
-
* the ones a keeper hid instead.
|
|
524
|
-
*/
|
|
525
|
-
async function saasCandidatesOf(tx, domain, today, hidden = false) {
|
|
526
|
-
const now = today.slice(0, 7);
|
|
527
|
-
const from = `${(0, saas_rules_1.shiftMonth)(now, -12)}-01`;
|
|
528
|
-
const lines = await tx
|
|
529
|
-
.getRepository(accounting_1.Transaction)
|
|
530
|
-
.createQueryBuilder('line')
|
|
531
|
-
.leftJoinAndSelect('line.account', 'subject')
|
|
532
|
-
.where('line.domain = :domain', { domain: domain.id })
|
|
533
|
-
.andWhere('line.category IN (:...categories)', { categories: [accounting_1.TransactionCategory.CONFIRMED, accounting_1.TransactionCategory.CARD] })
|
|
534
|
-
.andWhere('line.type = :type', { type: accounting_1.CashFlowType.EXPENSE })
|
|
535
|
-
.andWhere('line.transactionDate >= :from', { from })
|
|
536
|
-
.getMany();
|
|
537
|
-
const accounts = await tx.getRepository(saas_account_1.SaasAccount).find({ where: { domain: { id: domain.id } } });
|
|
538
|
-
const recurring = await tx.getRepository(accounting_1.RecurringCashItem).find({ where: { domain: { id: domain.id } } });
|
|
539
|
-
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}`));
|
|
540
|
-
const skip = new Set([...accounts.map(one => one.counterpartyId), ...recurring.map(one => one.counterpartyId), ...(hidden ? [] : hiddenKeys)].filter(Boolean));
|
|
541
|
-
const parties = await tx.getRepository(biz_base_1.Counterparty).find({ where: { domain: { id: domain.id } }, select: ['id', 'name', 'aliases', 'kind'] });
|
|
542
|
-
const named = (0, biz_base_1.partyNamed)(parties);
|
|
543
|
-
const people = new Set(parties.filter(one => one.kind === biz_base_1.CounterpartyKind.PERSON).map(one => one.id));
|
|
544
|
-
/* Whom a line paid: its counterparty, else the one its written name is (the rule a line is linked by), else the name. */
|
|
545
|
-
const vendorOf = (line) => {
|
|
546
|
-
const id = line.counterpartyId ?? named(line.counterpartyName)?.id;
|
|
547
|
-
if (id)
|
|
548
|
-
return people.has(id) ? null : { key: id, counterpartyId: id };
|
|
549
|
-
const key = (0, biz_base_1.nameKey)(line.counterpartyName);
|
|
550
|
-
return key && !(0, biz_base_1.looksLikePersonName)(line.counterpartyName) ? { key: `name:${key}`, counterpartyId: null } : null;
|
|
551
|
-
};
|
|
552
|
-
const found = (0, saas_rules_1.candidatesOf)(lines
|
|
553
|
-
.filter(line => line.account?.profitAndLoss !== false)
|
|
554
|
-
.flatMap(line => {
|
|
555
|
-
const vendor = vendorOf(line);
|
|
556
|
-
return vendor ? [{ ...vendor, name: line.counterpartyName, transactionDate: line.transactionDate, amount: Number(line.amount), currency: line.currency, bankAccountId: line.bankAccountId }] : [];
|
|
557
|
-
}), skip).filter(one => hiddenKeys.has(one.key) === hidden);
|
|
558
|
-
return found.map(one => ({ ...one, counterpartyName: parties.find(party => party.id === one.counterpartyId)?.name ?? one.name ?? '', hidden }));
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Make an account of each candidate - named after the vendor, paid from where it was paid most, on the day it was paid
|
|
562
|
-
* most - with one usage subscription budgeted at the monthly average, so the months to come are expected at it until
|
|
563
|
-
* bills say otherwise. A vendor the lines only name is registered as a counterparty first; the lines written with its
|
|
564
|
-
* name are linked to it (accounting `linkLinesByName`). One that is no candidate any more is refused.
|
|
565
|
-
*/
|
|
566
|
-
async function createSaasAccountsFromCandidates(tx, domain, user, keys, today) {
|
|
567
|
-
const candidates = await saasCandidatesOf(tx, domain, today);
|
|
568
|
-
const made = [];
|
|
569
|
-
for (const key of [...new Set(keys)]) {
|
|
570
|
-
const one = candidates.find(candidate => candidate.key === key);
|
|
571
|
-
if (!one)
|
|
572
|
-
throw new shell_1.Refusal('saas-candidate-not-found', {}, 'It is no candidate any more - reload and try again');
|
|
573
|
-
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;
|
|
574
|
-
/* Its lines written with its name and not linked yet are its months. */
|
|
575
|
-
await (0, accounting_1.linkLinesByName)(tx, domain, user, [counterpartyId]);
|
|
576
|
-
const [account] = await saveSaasAccounts(tx, domain, user, [
|
|
577
|
-
{ cuFlag: '+', name: one.counterpartyName, counterpartyId, paymentAccountId: one.paymentAccountId, currency: one.currency, billingDay: one.billingDay, status: saas_rules_1.SaasAccountStatus.ACTIVE }
|
|
578
|
-
]);
|
|
579
|
-
await saveSaasSubscriptions(tx, domain, user, 'all', account.id, [
|
|
580
|
-
{ cuFlag: '+', name: one.counterpartyName, billing: saas_rules_1.SaasBilling.USAGE, budget: one.average, startOn: one.firstDate, status: saas_rules_1.SaasSubscriptionStatus.ACTIVE }
|
|
581
|
-
]);
|
|
582
|
-
made.push(account);
|
|
583
|
-
}
|
|
584
|
-
return made;
|
|
585
|
-
}
|
|
586
|
-
/** Hide candidates - by their keys - or show them again. */
|
|
587
|
-
async function hideSaasCandidates(tx, domain, user, keys, hide) {
|
|
588
|
-
const repo = tx.getRepository(saas_hidden_vendor_1.SaasHiddenVendor);
|
|
589
|
-
const wanted = [...new Set(keys)].filter(Boolean);
|
|
590
|
-
const names = wanted.filter(key => key.startsWith('name:')).map(key => key.slice(5));
|
|
591
|
-
const ids = wanted.filter(key => !key.startsWith('name:'));
|
|
592
|
-
const parties = ids.length ? await tx.getRepository(biz_base_1.Counterparty).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(ids) } }) : [];
|
|
593
|
-
if (parties.length !== ids.length)
|
|
594
|
-
throw new shell_1.Refusal('saas-counterparty-not-found', {}, 'The vendor was not found - reload and try again');
|
|
595
|
-
const rows = await repo.find({ where: { domain: { id: domain.id } } });
|
|
596
|
-
const held = (row) => (row.counterpartyId ? ids.includes(row.counterpartyId) : names.includes(row.nameKey));
|
|
597
|
-
if (!hide) {
|
|
598
|
-
const gone = rows.filter(held);
|
|
599
|
-
if (gone.length)
|
|
600
|
-
await repo.remove(gone);
|
|
601
|
-
return true;
|
|
602
|
-
}
|
|
603
|
-
for (const party of parties)
|
|
604
|
-
if (!rows.some(row => row.counterpartyId === party.id))
|
|
605
|
-
await repo.save({ domain, counterparty: party, creator: user });
|
|
606
|
-
for (const key of names)
|
|
607
|
-
if (!rows.some(row => !row.counterpartyId && row.nameKey === key))
|
|
608
|
-
await repo.save({ domain, nameKey: key, creator: user });
|
|
609
|
-
return true;
|
|
610
|
-
}
|
|
611
223
|
//# sourceMappingURL=saas-service.js.map
|