@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.
Files changed (76) hide show
  1. package/README.md +11 -11
  2. package/dist-client/entity360/saas-account-360.d.ts +0 -1
  3. package/dist-client/entity360/saas-account-360.js +18 -71
  4. package/dist-client/entity360/saas-account-360.js.map +1 -1
  5. package/dist-client/index.d.ts +0 -3
  6. package/dist-client/index.js +0 -3
  7. package/dist-client/index.js.map +1 -1
  8. package/dist-client/pages/saas-account-list.d.ts +1 -1
  9. package/dist-client/pages/saas-account-list.js +5 -14
  10. package/dist-client/pages/saas-account-list.js.map +1 -1
  11. package/dist-client/pages/saas-account-page.d.ts +3 -5
  12. package/dist-client/pages/saas-account-page.js +30 -55
  13. package/dist-client/pages/saas-account-page.js.map +1 -1
  14. package/dist-client/route.d.ts +1 -1
  15. package/dist-client/route.js +0 -3
  16. package/dist-client/route.js.map +1 -1
  17. package/dist-client/tsconfig.tsbuildinfo +1 -1
  18. package/dist-client/words.d.ts +0 -13
  19. package/dist-client/words.js +0 -17
  20. package/dist-client/words.js.map +1 -1
  21. package/dist-server/service/checks.d.ts +1 -9
  22. package/dist-server/service/checks.js +2 -18
  23. package/dist-server/service/checks.js.map +1 -1
  24. package/dist-server/service/index.d.ts +1 -6
  25. package/dist-server/service/index.js +1 -9
  26. package/dist-server/service/index.js.map +1 -1
  27. package/dist-server/service/saas-account.d.ts +0 -4
  28. package/dist-server/service/saas-account.js +1 -16
  29. package/dist-server/service/saas-account.js.map +1 -1
  30. package/dist-server/service/saas-mutation.d.ts +1 -10
  31. package/dist-server/service/saas-mutation.js +1 -86
  32. package/dist-server/service/saas-mutation.js.map +1 -1
  33. package/dist-server/service/saas-query.d.ts +2 -51
  34. package/dist-server/service/saas-query.js +2 -65
  35. package/dist-server/service/saas-query.js.map +1 -1
  36. package/dist-server/service/saas-rules.d.ts +3 -141
  37. package/dist-server/service/saas-rules.js +10 -213
  38. package/dist-server/service/saas-rules.js.map +1 -1
  39. package/dist-server/service/saas-service.d.ts +2 -125
  40. package/dist-server/service/saas-service.js +50 -438
  41. package/dist-server/service/saas-service.js.map +1 -1
  42. package/dist-server/service/saas-subscription.d.ts +4 -5
  43. package/dist-server/service/saas-subscription.js +11 -21
  44. package/dist-server/service/saas-subscription.js.map +1 -1
  45. package/dist-server/service/saas-sync.d.ts +0 -35
  46. package/dist-server/service/saas-sync.js +0 -60
  47. package/dist-server/service/saas-sync.js.map +1 -1
  48. package/dist-server/service/saas-type.d.ts +0 -93
  49. package/dist-server/service/saas-type.js +3 -351
  50. package/dist-server/service/saas-type.js.map +1 -1
  51. package/dist-server/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +10 -10
  53. package/translations/en.json +3 -81
  54. package/translations/es.json +3 -81
  55. package/translations/ja.json +3 -81
  56. package/translations/ko.json +3 -81
  57. package/translations/ms.json +3 -81
  58. package/translations/zh.json +3 -81
  59. package/dist-client/components/saas-bill-editor.d.ts +0 -59
  60. package/dist-client/components/saas-bill-editor.js +0 -211
  61. package/dist-client/components/saas-bill-editor.js.map +0 -1
  62. package/dist-client/components/saas-candidates.d.ts +0 -33
  63. package/dist-client/components/saas-candidates.js +0 -157
  64. package/dist-client/components/saas-candidates.js.map +0 -1
  65. package/dist-client/pages/saas-cost-trend.d.ts +0 -23
  66. package/dist-client/pages/saas-cost-trend.js +0 -219
  67. package/dist-client/pages/saas-cost-trend.js.map +0 -1
  68. package/dist-server/service/saas-bill.d.ts +0 -42
  69. package/dist-server/service/saas-bill.js +0 -165
  70. package/dist-server/service/saas-bill.js.map +0 -1
  71. package/dist-server/service/saas-hidden-vendor.d.ts +0 -19
  72. package/dist-server/service/saas-hidden-vendor.js +0 -53
  73. package/dist-server/service/saas-hidden-vendor.js.map +0 -1
  74. package/dist-server/service/saas-trend-type.d.ts +0 -9
  75. package/dist-server/service/saas-trend-type.js +0 -37
  76. 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 patches_js_1 = require("@things-factory/asset/dist-server/service/patches.js");
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', 'billingDay', 'increaseAlertRate', 'status', 'closedOn'];
70
- const SUBSCRIPTION_TRACKED = ['name', 'billing', 'vendorKey', 'trial', 'quantity', 'unitPrice', 'amount', 'budget', 'everyMonths', 'startOn', 'renewalOn', 'autoRenew', 'noticeDays', 'departmentId', 'assetId', 'status', 'endedOn'];
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, patches_js_1.applyPatches)(tx, saas_account_1.SaasAccount, domain, user, patches, async (patch, old) => {
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, patches_js_1.touched)(patch, 'name', old) ? (0, patches_js_1.textOf)(patch.name) : old.name;
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, patches_js_1.touched)(patch, one, old))
89
- row[one] = (0, patches_js_1.textOf)(patch[one]);
90
- if ((0, patches_js_1.touched)(patch, 'currency', old))
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, patches_js_1.touched)(patch, 'billingDay', old))
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, patches_js_1.touched)(patch, 'paymentAccountId', old))
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, patches_js_1.touched)(patch, 'parentId', old)) {
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, patches_js_1.touched)(patch, 'adminIds', old)) {
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, patches_js_1.touched)(patch, 'status', old))
96
+ if ((0, asset_1.touched)(patch, 'status', old))
120
97
  row.status = patch.status ?? saas_rules_1.SaasAccountStatus.ACTIVE;
121
- if ((0, patches_js_1.touched)(patch, 'closedOn', old))
122
- row.closedOn = (0, patches_js_1.dayOf)(patch.closedOn, 'saas-day-invalid');
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, patches_js_1.applyPatches)(tx, saas_subscription_1.SaasSubscription, domain, user, patches, async (patch, old) => {
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, patches_js_1.touched)(patch, 'name', old) ? (0, patches_js_1.textOf)(patch.name) : old.name;
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
- for (const one of ['quantity', 'unitPrice', 'amount', 'budget'])
151
- if ((0, patches_js_1.touched)(patch, one, old))
152
- row[one] = (0, patches_js_1.amountOf)(patch[one], 'saas-amount-invalid');
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, patches_js_1.touched)(patch, 'vendorKey', old))
155
- row.vendorKey = (0, patches_js_1.textOf)(patch.vendorKey);
156
- if ((0, patches_js_1.touched)(patch, 'trial', old))
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
- /* A plan read from the vendor's tenant may wait for its price (개정 2) - it is expected at nothing until then. */
160
- const priced = value('unitPrice') !== null && value('unitPrice') !== undefined;
161
- if (billing === saas_rules_1.SaasBilling.SEAT && !(Number(value('quantity')) > 0 && (priced || value('vendorKey'))))
162
- throw new shell_1.Refusal('saas-seat-price-required', {}, 'A seat plan takes its seats and the price of one');
163
- if ((billing === saas_rules_1.SaasBilling.FIXED || billing === saas_rules_1.SaasBilling.COMMITMENT) && (value('amount') === null || value('amount') === undefined))
164
- throw new shell_1.Refusal('saas-amount-required', {}, 'Give what one billing costs');
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, patches_js_1.touched)(patch, 'noticeDays', old))
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, patches_js_1.touched)(patch, 'description', old))
177
- row.description = (0, patches_js_1.textOf)(patch.description);
178
- if ((0, patches_js_1.touched)(patch, 'departmentId', old))
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, patches_js_1.touched)(patch, 'assetId', old))
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 - its cost is there');
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, patches_js_1.touched)(patch, 'status', old))
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, patches_js_1.applyPatches)(tx, saas_seat_1.SaasSeat, domain, user, patches, async (patch, old) => {
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, patches_js_1.touched)(patch, 'employeeId', old))
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, patches_js_1.touched)(patch, one, old))
225
- row[one] = (0, patches_js_1.textOf)(patch[one]);
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, patches_js_1.touched)(patch, 'fromOn', old))
229
- row.fromOn = (0, patches_js_1.dayOf)(patch.fromOn, 'saas-day-invalid');
230
- if ((0, patches_js_1.touched)(patch, 'untilOn', old))
231
- row.untilOn = (0, patches_js_1.dayOf)(patch.untilOn, 'saas-day-invalid');
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
- /* The cost trend (decision 9) */
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