prow-authority 3.6.3

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 (168) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/.dockerignore +11 -0
  3. package/.github/workflows/_deploy.yml +191 -0
  4. package/.github/workflows/deploy-dev.yml +30 -0
  5. package/.github/workflows/deploy-prod.yml +34 -0
  6. package/CLAUDE.md +114 -0
  7. package/Dockerfile +61 -0
  8. package/Dockerfile.dev +24 -0
  9. package/README.md +1 -0
  10. package/package.json +61 -0
  11. package/src/App.ts +40 -0
  12. package/src/helpers/api-error.ts +21 -0
  13. package/src/helpers/authority.ts +216 -0
  14. package/src/helpers/config.ts +4 -0
  15. package/src/helpers/holded.ts +105 -0
  16. package/src/helpers/mysql.ts +15 -0
  17. package/src/helpers/notifications.ts +55 -0
  18. package/src/helpers/random.ts +18 -0
  19. package/src/helpers/s3.ts +43 -0
  20. package/src/helpers/stripe.ts +8 -0
  21. package/src/index.ts +47 -0
  22. package/src/v1/common-types.ts +12 -0
  23. package/src/v1/controllers/admin/accounting-seat-controller.ts +148 -0
  24. package/src/v1/controllers/admin/billing-cycle-controller.ts +171 -0
  25. package/src/v1/controllers/admin/invoice-controller.ts +222 -0
  26. package/src/v1/controllers/admin/license-controller.ts +190 -0
  27. package/src/v1/controllers/admin/license-limit-controller.ts +160 -0
  28. package/src/v1/controllers/admin/notification-controller.ts +115 -0
  29. package/src/v1/controllers/admin/organization-controller.ts +210 -0
  30. package/src/v1/controllers/admin/organization-license-controller.ts +224 -0
  31. package/src/v1/controllers/admin/organization-license-limit-controller.ts +196 -0
  32. package/src/v1/controllers/admin/organization-usage-controller.ts +167 -0
  33. package/src/v1/controllers/admin/payment-method-controller.ts +173 -0
  34. package/src/v1/controllers/admin/policy-controller.ts +138 -0
  35. package/src/v1/controllers/admin/policy-version-controller.ts +143 -0
  36. package/src/v1/controllers/admin/schedule-controller.ts +181 -0
  37. package/src/v1/controllers/admin/service-controller.ts +206 -0
  38. package/src/v1/controllers/admin/service-notification-controller.ts +166 -0
  39. package/src/v1/controllers/admin/user-controller.ts +282 -0
  40. package/src/v1/controllers/admin/user-organization-controller.ts +172 -0
  41. package/src/v1/controllers/admin/user-policy-controller.ts +164 -0
  42. package/src/v1/controllers/auth/@deprecated oauth-controller.ts +191 -0
  43. package/src/v1/controllers/auth/identity-token-controller.ts +286 -0
  44. package/src/v1/controllers/auth/organization-token-controller.ts +277 -0
  45. package/src/v1/controllers/auth/temp-code-controller.ts +60 -0
  46. package/src/v1/controllers/billing/billing-controller.ts +805 -0
  47. package/src/v1/controllers/organizations/public-organization-controller.ts +1796 -0
  48. package/src/v1/controllers/policies/public-policy-controller.ts +68 -0
  49. package/src/v1/controllers/services/public-service-controller.ts +177 -0
  50. package/src/v1/controllers/services/service-controller.ts +474 -0
  51. package/src/v1/controllers/users/public-user-controller.ts +471 -0
  52. package/src/v1/daos/@deprecated auth-dao.ts +173 -0
  53. package/src/v1/daos/accounting-seat-dao.ts +111 -0
  54. package/src/v1/daos/billing-cycle-dao.ts +107 -0
  55. package/src/v1/daos/invoice-dao.ts +131 -0
  56. package/src/v1/daos/license-dao.ts +152 -0
  57. package/src/v1/daos/license-limit-dao.ts +120 -0
  58. package/src/v1/daos/organization-dao.ts +109 -0
  59. package/src/v1/daos/organization-license-dao.ts +117 -0
  60. package/src/v1/daos/organization-license-limit-dao.ts +157 -0
  61. package/src/v1/daos/organization-usage-dao.ts +115 -0
  62. package/src/v1/daos/payment-method-dao.ts +110 -0
  63. package/src/v1/daos/policy-dao.ts +105 -0
  64. package/src/v1/daos/policy-version-dao.ts +112 -0
  65. package/src/v1/daos/redis-dao.ts +38 -0
  66. package/src/v1/daos/schema.sql +462 -0
  67. package/src/v1/daos/service-dao.ts +118 -0
  68. package/src/v1/daos/service-notification-dao.ts +120 -0
  69. package/src/v1/daos/user-dao.ts +127 -0
  70. package/src/v1/daos/user-organization-dao.ts +123 -0
  71. package/src/v1/daos/user-policy-dao.ts +109 -0
  72. package/src/v1/middlewares/auth/identity-token-middleware.ts +51 -0
  73. package/src/v1/middlewares/auth/organization-token-middleware.ts +73 -0
  74. package/src/v1/middlewares/auth/service-token-middleware.ts +167 -0
  75. package/src/v1/middlewares/authority-middleware.ts +97 -0
  76. package/src/v1/middlewares/organizations/organization-middleware.ts +94 -0
  77. package/src/v1/middlewares/static/static-middleware.ts +41 -0
  78. package/src/v1/middlewares/users/user-middleware.ts +41 -0
  79. package/src/v1/routes/admin/accounting-seat-router.ts +89 -0
  80. package/src/v1/routes/admin/admin-router.ts +61 -0
  81. package/src/v1/routes/admin/billing-cycle-router.ts +89 -0
  82. package/src/v1/routes/admin/invoice-router.ts +89 -0
  83. package/src/v1/routes/admin/license-limit-router.ts +89 -0
  84. package/src/v1/routes/admin/license-router.ts +89 -0
  85. package/src/v1/routes/admin/notification-router.ts +61 -0
  86. package/src/v1/routes/admin/organization-license-limit-router.ts +89 -0
  87. package/src/v1/routes/admin/organization-license-router.ts +89 -0
  88. package/src/v1/routes/admin/organization-router.ts +89 -0
  89. package/src/v1/routes/admin/payment-method-router.ts +89 -0
  90. package/src/v1/routes/admin/policy-router.ts +89 -0
  91. package/src/v1/routes/admin/policy-version-router.ts +89 -0
  92. package/src/v1/routes/admin/schedule-router.ts +75 -0
  93. package/src/v1/routes/admin/service-notification-router.ts +98 -0
  94. package/src/v1/routes/admin/service-router.ts +89 -0
  95. package/src/v1/routes/admin/user-organization-router.ts +89 -0
  96. package/src/v1/routes/admin/user-policy-router.ts +89 -0
  97. package/src/v1/routes/admin/user-router.ts +89 -0
  98. package/src/v1/routes/auth/@deprecated auth-router.ts +199 -0
  99. package/src/v1/routes/auth/identity-token-router.ts +147 -0
  100. package/src/v1/routes/auth/organization-token-router.ts +70 -0
  101. package/src/v1/routes/billing/billing-router.ts +227 -0
  102. package/src/v1/routes/organizations/invoices-router.ts +48 -0
  103. package/src/v1/routes/organizations/payments-router.ts +104 -0
  104. package/src/v1/routes/organizations/public-organization-router.ts +319 -0
  105. package/src/v1/routes/organizations/users-router.ts +139 -0
  106. package/src/v1/routes/policies/public-policy-router.ts +50 -0
  107. package/src/v1/routes/services/public-service-router.ts +89 -0
  108. package/src/v1/routes/services/service-router.ts +197 -0
  109. package/src/v1/routes/users/public-user-router.ts +230 -0
  110. package/src/v1/routes/v1-router.ts +50 -0
  111. package/src/v1/schemas/admin/accounting-seat-schemas.ts +22 -0
  112. package/src/v1/schemas/admin/billing-cycle-schemas.ts +20 -0
  113. package/src/v1/schemas/admin/invoice-schemas.ts +41 -0
  114. package/src/v1/schemas/admin/license-limit-schemas.ts +25 -0
  115. package/src/v1/schemas/admin/license-schemas.ts +30 -0
  116. package/src/v1/schemas/admin/organization-license-limit-schemas.ts +29 -0
  117. package/src/v1/schemas/admin/organization-license-schemas.ts +43 -0
  118. package/src/v1/schemas/admin/organization-schemas.ts +67 -0
  119. package/src/v1/schemas/admin/organization-usage-schemas.ts +15 -0
  120. package/src/v1/schemas/admin/payment-method-schemas.ts +40 -0
  121. package/src/v1/schemas/admin/policy-schemas.ts +31 -0
  122. package/src/v1/schemas/admin/service-notification-schemas.ts +23 -0
  123. package/src/v1/schemas/admin/service-schemas.ts +56 -0
  124. package/src/v1/schemas/admin/user-organization-schemas.ts +24 -0
  125. package/src/v1/schemas/admin/user-policy-schemas.ts +17 -0
  126. package/src/v1/schemas/admin/user-schemas.ts +52 -0
  127. package/src/v1/schemas/auth/auth-schemas.ts +55 -0
  128. package/src/v1/schemas/billing/billing-schemas.ts +32 -0
  129. package/src/v1/schemas/organizations/public-organization-schemas.ts +77 -0
  130. package/src/v1/schemas/payments/payment-method-schemas.ts +0 -0
  131. package/src/v1/schemas/services/public-service-schemas.ts +5 -0
  132. package/src/v1/schemas/services/service-schemas.ts +27 -0
  133. package/src/v1/schemas/users/public-user-schemas.ts +73 -0
  134. package/src/v1/types/admin/accounting-seat-types.ts +16 -0
  135. package/src/v1/types/admin/billing-cycle-types.ts +14 -0
  136. package/src/v1/types/admin/invoice-types.ts +39 -0
  137. package/src/v1/types/admin/license-limit-types.ts +16 -0
  138. package/src/v1/types/admin/license-types.ts +29 -0
  139. package/src/v1/types/admin/organization-license-limit-types.ts +13 -0
  140. package/src/v1/types/admin/organization-license-types.ts +21 -0
  141. package/src/v1/types/admin/organization-types.ts +60 -0
  142. package/src/v1/types/admin/organization-usage-types.ts +17 -0
  143. package/src/v1/types/admin/payment-method-types.ts +30 -0
  144. package/src/v1/types/admin/policy-types.ts +9 -0
  145. package/src/v1/types/admin/policy-version-types.ts +12 -0
  146. package/src/v1/types/admin/service-notification-types.ts +20 -0
  147. package/src/v1/types/admin/service-types.ts +36 -0
  148. package/src/v1/types/admin/user-organization-types.ts +31 -0
  149. package/src/v1/types/admin/user-types.ts +44 -0
  150. package/src/v1/types/auth/@deprecated auth-types.ts +26 -0
  151. package/src/v1/types/auth/common-types.ts +7 -0
  152. package/src/v1/types/auth/confirmation-types.ts +5 -0
  153. package/src/v1/types/auth/identity-token-types.ts +8 -0
  154. package/src/v1/types/auth/organization-token-types.ts +23 -0
  155. package/src/v1/types/auth/otp-types.ts +12 -0
  156. package/src/v1/types/billing/billing-types.ts +37 -0
  157. package/src/v1/types/billing/ue-types.ts +29 -0
  158. package/src/v1/types/organizations/public-organization-types.ts +76 -0
  159. package/src/v1/types/policies/public-policy-types.ts +7 -0
  160. package/src/v1/types/services/public-service-types.ts +20 -0
  161. package/src/v1/types/services/service-types.ts +43 -0
  162. package/src/v1/types/users/public-user-types.ts +15 -0
  163. package/src/v1/types/users/user-policy-types.ts +11 -0
  164. package/static/error.html +55 -0
  165. package/static/grant.html +117 -0
  166. package/static/login.html +54 -0
  167. package/static/oauth-client-logo.webp +0 -0
  168. package/tsconfig.json +20 -0
@@ -0,0 +1,805 @@
1
+ import { Date2, Formats, Unit } from "@beseif-solutions/utility-functions/dist/utils/date-utils";
2
+ import { Requester } from "@beseif-solutions/utility-functions/dist/utils/http-utils";
3
+ import { Validator } from "@beseif-solutions/utility-functions/dist/utils/validation-utils";
4
+ import _ from "lodash";
5
+ import APIError, { StatusCodes } from "../../../helpers/api-error";
6
+ import authority, { InvoiceEmitted, NotificationEventNames, TrialEnded, TrialEndingSoon } from "../../../helpers/authority";
7
+ import nconf from "../../../helpers/config";
8
+ import holded from "../../../helpers/holded";
9
+ import stripe from "../../../helpers/stripe";
10
+ import { getAccountingSeatsSchema, getInvoicesSchema, invoiceEmittedSchema, invoicePaidSchema, generateOrganizationInvoiceSchema as invoicesSchema, updateInvoiceSchema } from "../../schemas/billing/billing-schemas";
11
+ import { AccountingSeat } from "../../types/admin/accounting-seat-types";
12
+ import { BillingCycle } from "../../types/admin/billing-cycle-types";
13
+ import { Invoice, InvoiceConcept, InvoiceStatus } from "../../types/admin/invoice-types";
14
+ import { LicenseType } from "../../types/admin/license-types";
15
+ import { OrganizationLicense } from "../../types/admin/organization-license-types";
16
+ import { Organization, OrganizationStatus } from "../../types/admin/organization-types";
17
+ import { Billing_GetAccountingSeats_Params, Billing_GetInvoices_Params, Billing_InvoiceEmitted_Params, Billing_InvoicePaid_Params, Billing_PendingLicenses_Params as Billing_Invoices_Params, Billing_UpdateInvoice_Params, PublicAccountingSeat, PublicInvoice } from "../../types/billing/billing-types";
18
+ import { UE_Country, UE_VAT, UE_VAT_Item } from "../../types/billing/ue-types";
19
+ import accountingSeatController from "../admin/accounting-seat-controller";
20
+ import billingCycleController from "../admin/billing-cycle-controller";
21
+ import invoiceController from "../admin/invoice-controller";
22
+ import organizationController from "../admin/organization-controller";
23
+ import organizationLicenseController from "../admin/organization-license-controller";
24
+ import paymentMethodController from "../admin/payment-method-controller";
25
+ import serviceController from "../admin/service-controller";
26
+ import publicOrganizationController from "../organizations/public-organization-controller";
27
+
28
+ class BillingController {
29
+
30
+ public readonly generateCycle = async (organization: Organization, params: Billing_Invoices_Params = {}): Promise<any> => {
31
+ try {
32
+ Validator.validate(invoicesSchema, params);
33
+ const { cycle } = await this.cycleContext(organization, params.current_date);
34
+
35
+ return cycle;
36
+ } catch (e) { throw e; }
37
+ };
38
+
39
+ public readonly generateLicensesInvoice = async (organization: Organization, params: Billing_Invoices_Params = {}): Promise<{ generated: boolean, uuid?: string }> => {
40
+ try {
41
+ Validator.validate(invoicesSchema, params);
42
+
43
+ const { cycle, now, licenses } = await this.licenseContext(organization, params.current_date);
44
+
45
+ // get generated invoices - order by date
46
+ const invoices = await invoiceController.list({
47
+ organization: organization.id,
48
+ date: { gte: new Date(cycle.start_date) as any, lte: new Date(now) as any },
49
+ }, { order: { by: `date`, direction: `ascendent` } });
50
+
51
+ const lastBilledInvoice = _.last(invoices.filter((i) => i.status !== InvoiceStatus.Pending));
52
+ let pendingInvoice = invoices.find((i) => i.status === InvoiceStatus.Pending);
53
+
54
+ // billed licenses = licenses starting after last billed invoice
55
+ const billed = lastBilledInvoice ?
56
+ _.remove(licenses, (l) => new Date2(l.start_date).isBefore(new Date2(lastBilledInvoice.date)))
57
+ : [];
58
+
59
+ const concepts = await this.licensesToConcepts(cycle, licenses, billed);
60
+
61
+ if (concepts.length === 0 || cycle.internal.initial) { return { generated: false }; }
62
+
63
+ // calculate VAT and VIES
64
+ const { code, percentage } = await this.getOrganizationTaxConfiguration(organization);
65
+
66
+ if (!pendingInvoice) {
67
+ pendingInvoice = (await invoiceController.add({
68
+ organization: organization.id,
69
+ date: now.format(Formats.ISO),
70
+ tax_code: code,
71
+ tax_percentage: percentage,
72
+ concepts: concepts,
73
+ billing: organization.billing,
74
+ invoice: null,
75
+ payment: null,
76
+ method: null,
77
+ status: InvoiceStatus.Pending,
78
+ metadata: {},
79
+ internal: {},
80
+ }))[0];
81
+ } else {
82
+ const equal =
83
+ pendingInvoice.tax_code === code &&
84
+ pendingInvoice.tax_percentage === percentage &&
85
+ !_.some(concepts, (c) => !pendingInvoice.concepts.find((oc) => `${oc.name}#${oc.description}#${oc.price}` === `${c.name}#${c.description}#${c.price}`));
86
+
87
+ if (!equal) {
88
+ pendingInvoice = (await invoiceController.update(pendingInvoice.id, {
89
+ date: now.format(Formats.ISO),
90
+ tax_code: code,
91
+ tax_percentage: percentage,
92
+ concepts: concepts,
93
+ billing: organization.billing,
94
+ }))[0];
95
+ }
96
+ }
97
+
98
+ return { generated: true, uuid: pendingInvoice.uuid };
99
+ } catch (e) { throw e; }
100
+ };
101
+
102
+ public readonly getInvoices = async (data: Billing_GetInvoices_Params): Promise<PublicInvoice[]> => {
103
+ try {
104
+ Validator.validate(getInvoicesSchema, data);
105
+
106
+ const now = (data.current_date ? new Date2(data.current_date) : Date2.current()).format(Formats.ISO);
107
+
108
+ const invoices = await invoiceController.list({
109
+ date: { lte: new Date(now) as any },
110
+ status: data.status,
111
+ });
112
+
113
+ const response = await this.parseInvoices(invoices);
114
+ return response;
115
+ } catch (e) { throw e; }
116
+ };
117
+
118
+ public readonly updateInvoice = async (uuid: string, data: Billing_UpdateInvoice_Params): Promise<void> => {
119
+ try {
120
+ Validator.validate(updateInvoiceSchema, data);
121
+
122
+ const invoice = (await invoiceController.list({ uuid: uuid }))[0];
123
+ if (!invoice) {
124
+ throw new APIError({
125
+ code: StatusCodes.NOT_FOUND,
126
+ detail: `Invoice ${uuid} not found`,
127
+ });
128
+ }
129
+
130
+ await invoiceController.update(invoice.id, data);
131
+ } catch (e) { throw e; }
132
+ };
133
+
134
+ public readonly invoiceEmitted = async (uuid: string, data: Billing_InvoiceEmitted_Params): Promise<void> => {
135
+ try {
136
+ const invoice = (await invoiceController.list({
137
+ uuid: uuid,
138
+ status: InvoiceStatus.Pending,
139
+ }))[0];
140
+ if (!invoice) {
141
+ throw new APIError({
142
+ code: StatusCodes.NOT_FOUND,
143
+ detail: `Pending invoice ${uuid} not found`,
144
+ });
145
+ }
146
+
147
+ const organization = (await organizationController.get(invoice.organization))[0];
148
+ if (!organization) {
149
+ throw new APIError({
150
+ code: StatusCodes.BAD_REQUEST,
151
+ detail: `Organization ${invoice.organization} not found`,
152
+ });
153
+ }
154
+
155
+ const { cycle } = await this.cycleContext(organization, invoice.date);
156
+
157
+ if (cycle.trial && invoice.amount > 0) {
158
+ throw new APIError({
159
+ code: StatusCodes.BAD_REQUEST,
160
+ detail: `Cannot emit invoice with amount greater than 0 during trial period`,
161
+ });
162
+ }
163
+
164
+ // validate depending on invoice amount
165
+ Validator.validate(invoiceEmittedSchema(invoice.amount === 0), data);
166
+
167
+ // if an external invoice id is provided, fetch its document data from Holded
168
+ let number: string;
169
+ if (data.invoice) {
170
+ try {
171
+ const holdedInvoice = await holded.getInvoice(data.invoice);
172
+ number = holdedInvoice.docNumber;
173
+ } catch (e) {
174
+ console.log(e?.response?.data || e);
175
+ throw new Error(`Holded invoice ${data.invoice} not found`);
176
+ }
177
+ }
178
+
179
+ await invoiceController.update(invoice.id, {
180
+ invoice: data.invoice || null,
181
+ // store external document number (if available)
182
+ number: number,
183
+ // if trial period no payment required
184
+ status: InvoiceStatus.Emitted,
185
+ });
186
+
187
+ if (data.invoice) {
188
+ setImmediate(async () => {
189
+ try {
190
+ const { data: pdf } = await holded.getInvoicePDF(data.invoice);
191
+
192
+ await authority.notify<InvoiceEmitted>(organization.uuid, {
193
+ event: NotificationEventNames.InvoiceEmitted,
194
+ data: {
195
+ organization: organization,
196
+ invoice: { number: number, amount: invoice.amount },
197
+ cta: {
198
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
199
+ },
200
+ },
201
+ attachments: [
202
+ {
203
+ name: `${number}.pdf`,
204
+ type: `application/pdf`,
205
+ content: pdf,
206
+ },
207
+ ],
208
+ });
209
+ } catch (e) {
210
+ console.log(`Error sending notification:`, e);
211
+ }
212
+ });
213
+ }
214
+
215
+ if (invoice.amount > 0) {
216
+ // generate seats for accounting
217
+ await this.generateAccountingSeats(organization, cycle, invoice);
218
+ }
219
+ } catch (e) { throw e; }
220
+ };
221
+
222
+ public readonly invoicePaid = async (uuid: string, data: Billing_InvoicePaid_Params): Promise<void> => {
223
+ try {
224
+ const invoice = (await invoiceController.list({
225
+ uuid: uuid,
226
+ status: InvoiceStatus.Emitted,
227
+ }))[0];
228
+ if (!invoice) {
229
+ throw new APIError({
230
+ code: StatusCodes.NOT_FOUND,
231
+ detail: `Emitted invoice ${uuid} not found`,
232
+ });
233
+ }
234
+
235
+ // validate depending on invoice amount
236
+ Validator.validate(invoicePaidSchema(invoice.amount === 0), data);
237
+
238
+ const organization = (await organizationController.get(invoice.organization))[0];
239
+ if (!organization) {
240
+ throw new APIError({
241
+ code: StatusCodes.BAD_REQUEST,
242
+ detail: `Organization ${invoice.organization} not found`,
243
+ });
244
+ }
245
+
246
+ let methodID: number;
247
+ if (data.method) {
248
+ const method = (await paymentMethodController.list({
249
+ organization: organization.id,
250
+ internal: { payment_method_id: data.method },
251
+ }))[0];
252
+ if (!method) {
253
+ throw new APIError({
254
+ code: StatusCodes.NOT_FOUND,
255
+ detail: `Payment method ${data.method} not found`,
256
+ });
257
+ }
258
+
259
+ methodID = method.id;
260
+
261
+ // check in stripe
262
+ try {
263
+ const intent = await stripe.paymentIntents.retrieve(data.payment);
264
+ if (intent.status !== `succeeded`) { throw new Error(`Payment intent is ${intent.status}`); }
265
+ } catch (e) {
266
+ console.log(e);
267
+ throw new Error(`Stripe payment ${data.payment} not found or not successful`);
268
+ }
269
+ }
270
+
271
+ await invoiceController.update(invoice.id, {
272
+ payment: data.payment || null,
273
+ method: methodID || null,
274
+ status: InvoiceStatus.Paid,
275
+ });
276
+ } catch (e) {
277
+ console.log(e);
278
+ throw e;
279
+ }
280
+ };
281
+
282
+ public readonly getAccountingSeats = async (data: Billing_GetAccountingSeats_Params): Promise<PublicAccountingSeat[]> => {
283
+ try {
284
+ Validator.validate(getAccountingSeatsSchema, data);
285
+
286
+ const now = (data.current_date ? new Date2(data.current_date) : Date2.current()).format(Formats.ISO);
287
+
288
+ const invoices = await accountingSeatController.list({
289
+ date: { lte: new Date(now) as any },
290
+ done: data.done,
291
+ });
292
+
293
+ const response = await this.parseAccountingSeats(invoices);
294
+ return response;
295
+ } catch (e) { throw e; }
296
+ };
297
+
298
+ public readonly accountingSeatDone = async (uuid: string): Promise<void> => {
299
+ try {
300
+ const seat = (await accountingSeatController.list({ uuid: uuid }))[0];
301
+ if (!seat) {
302
+ throw new APIError({
303
+ code: StatusCodes.NOT_FOUND,
304
+ detail: `Accounting seat ${uuid} not found`,
305
+ });
306
+ }
307
+
308
+ await accountingSeatController.update(seat.id, { done: true });
309
+ } catch (e) { throw e; }
310
+ };
311
+
312
+ private readonly generateAccountingSeats = async (organization: Organization, cycle: BillingCycle, invoice: Invoice): Promise<void> => {
313
+ try {
314
+ const dates: string[] = [];
315
+
316
+ let start = new Date2(invoice.date);
317
+ const end = new Date2(cycle.end_date);
318
+ do {
319
+ dates.push(start.format(Formats.ISO));
320
+ start = start.add(1, Unit.Months);
321
+ } while (start.isBefore(end));
322
+
323
+ const perSeat = invoice.base / dates.length;
324
+
325
+ for (const date of dates) {
326
+ // create a seat
327
+ await accountingSeatController.add({
328
+ organization: organization.id,
329
+ invoice: invoice.id,
330
+ date: date,
331
+ amount: perSeat,
332
+ done: false,
333
+ metadata: {},
334
+ internal: {},
335
+ });
336
+ }
337
+ } catch (e) { throw e; }
338
+ };
339
+
340
+ public readonly organizationAccountingData = async (uuid: string): Promise<{ contact: string }> => {
341
+ try {
342
+ const organization = (await organizationController.list({
343
+ uuid: uuid,
344
+ status: OrganizationStatus.Active,
345
+ }))[0];
346
+ if (!organization) {
347
+ throw new APIError({
348
+ code: StatusCodes.BAD_REQUEST,
349
+ detail: `Organization ${uuid} not found`,
350
+ });
351
+ }
352
+
353
+ // if (!organization.internal.contact) {
354
+ const { id } = await publicOrganizationController.configureHoldedContact(organization);
355
+ organization.internal.contact = id;
356
+ // }
357
+
358
+ return {
359
+ contact: organization.internal.contact,
360
+ };
361
+ } catch (e) { throw e; }
362
+ };
363
+
364
+ public readonly organizationPaymentData = async (uuid: string): Promise<{ customer: string, method: string }> => {
365
+ try {
366
+ const organization = (await organizationController.list({
367
+ uuid: uuid,
368
+ status: OrganizationStatus.Active,
369
+ }))[0];
370
+ if (!organization) {
371
+ throw new APIError({
372
+ code: StatusCodes.BAD_REQUEST,
373
+ detail: `Organization ${uuid} not found`,
374
+ });
375
+ }
376
+
377
+ if (!organization.internal.customer) {
378
+ const { id } = await publicOrganizationController.configureStripeCustomer(organization);
379
+ organization.internal.customer = id;
380
+ }
381
+
382
+ const method = (await paymentMethodController.list({
383
+ organization: organization.id,
384
+ enabled: true,
385
+ }))[0];
386
+ if (!method) {
387
+ throw new APIError({
388
+ code: StatusCodes.BAD_REQUEST,
389
+ detail: `No enabled payment method configured for organization ${organization.id} - does it have any payment method?`,
390
+ });
391
+ }
392
+
393
+ return {
394
+ customer: organization.internal.customer,
395
+ method: method.internal.payment_method_id,
396
+ };
397
+ } catch (e) { throw e; }
398
+ };
399
+
400
+ public readonly blockOrganization = async (uuid: string): Promise<void> => {
401
+ try {
402
+ const organization = (await organizationController.list({
403
+ uuid: uuid,
404
+ status: OrganizationStatus.Active,
405
+ }))[0];
406
+ if (!organization) {
407
+ throw new APIError({
408
+ code: StatusCodes.BAD_REQUEST,
409
+ detail: `Organization ${uuid} not found`,
410
+ });
411
+ }
412
+
413
+ await organizationController.update(organization.id, { status: OrganizationStatus.Blocked });
414
+ } catch (e) { throw e; }
415
+ };
416
+
417
+ public readonly unblockOrganization = async (uuid: string): Promise<void> => {
418
+ try {
419
+ const organization = (await organizationController.list({
420
+ uuid: uuid,
421
+ status: OrganizationStatus.Blocked,
422
+ }))[0];
423
+ if (!organization) {
424
+ throw new APIError({
425
+ code: StatusCodes.BAD_REQUEST,
426
+ detail: `Organization ${uuid} not found`,
427
+ });
428
+ }
429
+
430
+ await organizationController.update(organization.id, { status: OrganizationStatus.Active });
431
+ } catch (e) { throw e; }
432
+ };
433
+
434
+ public readonly cycleContext = async (organization: Organization, currentDate?: string): Promise<{ cycle: BillingCycle, now: Date2 }> => {
435
+ try {
436
+ const now = currentDate ? new Date2(currentDate) : Date2.current();
437
+
438
+ const cycles = await billingCycleController.list({
439
+ organization: organization.id,
440
+ start_date: { lte: now as any },
441
+ end_date: { gt: now as any },
442
+ });
443
+ if (cycles.length > 2) {
444
+ throw new APIError({
445
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
446
+ detail: `Multiple billing cycles found for organization ${organization.id} on ${now}`,
447
+ });
448
+ }
449
+
450
+ let cycle = cycles[0];
451
+ if (!cycle) {
452
+ const base = await this.getBaseLicense(organization);
453
+ if (base.billing === `lifetime`) { throw new Error(`No base cycle found`); }
454
+
455
+ const previous = (await billingCycleController.list({
456
+ end_date: { lte: now as any },
457
+ organization: organization.id,
458
+ }, {
459
+ order: { by: `end_date`, direction: `descendent` },
460
+ limit: { count: `1` },
461
+ }))[0];
462
+ if (!previous) { throw new Error(`No previous cycle found`); }
463
+
464
+ let end = new Date2(previous.end_date);
465
+ do {
466
+ end = end.add(1, base.billing === `monthly` ? Unit.Months : Unit.Years);
467
+
468
+ cycle = (await billingCycleController.add({
469
+ organization: organization.id,
470
+ trial: false,
471
+ start_date: cycle ? cycle.end_date : previous.end_date,
472
+ end_date: end.toMoment().startOf(`day`).format(Formats.ISO),
473
+ metadata: {},
474
+ internal: {},
475
+ }))[0];
476
+ } while (end.isBefore(now));
477
+
478
+ if (previous.trial && !previous.internal?.trial_ended_sent) {
479
+ try {
480
+ await authority.notify<TrialEnded>(organization.uuid, {
481
+ event: NotificationEventNames.TrialEnded,
482
+ data: {
483
+ organization: organization,
484
+ cta: {
485
+ subscription: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_USAGE`),
486
+ billing: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_BILLING`),
487
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
488
+ },
489
+ },
490
+ });
491
+ await billingCycleController.update(previous.id, { internal: { trial_ended_sent: true } });
492
+ } catch (e) {
493
+ console.log(`Error sending trial ended notification:`, e);
494
+ }
495
+ }
496
+ }
497
+
498
+ return {
499
+ now: now,
500
+ cycle: cycle,
501
+ };
502
+ } catch (e) { throw e; }
503
+ };
504
+
505
+ public readonly trialWarningCheck = async (organization: Organization, currentDate?: string): Promise<void> => {
506
+ try {
507
+ const now = currentDate ? new Date2(currentDate) : Date2.current();
508
+ const warningDays = Number(nconf.get(`SERVICES:AUTHORITY:BILLING:TRIAL_WARNING_DAYS`));
509
+
510
+ // find the active trial cycle (if any)
511
+ const cycle = (await billingCycleController.list({
512
+ organization: organization.id,
513
+ trial: true,
514
+ start_date: { lte: now.format(Formats.ISO) },
515
+ end_date: { gt: now.format(Formats.ISO) },
516
+ }))[0];
517
+ if (!cycle) { return; }
518
+
519
+ // only notify once per trial cycle
520
+ if (cycle.internal?.trial_warning_sent) { return; }
521
+
522
+ // days remaining until the trial ends (calendar days)
523
+ const daysRemaining = new Date2(cycle.end_date).toMoment().startOf(`day`).diff(now.toMoment().startOf(`day`), `days`);
524
+
525
+ // notify only and exclusively when exactly the configured days remain
526
+ if (daysRemaining !== warningDays) { return; }
527
+
528
+ try {
529
+ await authority.notify<TrialEndingSoon>(organization.uuid, {
530
+ event: NotificationEventNames.TrialEndingSoon,
531
+ data: {
532
+ organization: organization,
533
+ end_date: new Date2(cycle.end_date).format(Formats.ReadableFormat),
534
+ cta: {
535
+ subscription: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_USAGE`),
536
+ billing: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_BILLING`),
537
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
538
+ },
539
+ },
540
+ });
541
+ await billingCycleController.update(cycle.id, { internal: { trial_warning_sent: true } });
542
+ } catch (e) {
543
+ console.log(`Error sending trial warning notification:`, e);
544
+ }
545
+ } catch (e) { throw e; }
546
+ };
547
+
548
+ private readonly licenseContext = async (organization: Organization, currentDate?: string) => {
549
+ try {
550
+ const { now, cycle } = await this.cycleContext(organization, currentDate);
551
+
552
+ const licensesInCycle = await organizationLicenseController.list({
553
+ organization: organization.id,
554
+ start_date: { lte: now as any },
555
+ end_date: [
556
+ { is: null },
557
+ { gt: new Date(cycle.start_date) as any, lte: new Date(cycle.end_date) as any },
558
+ ],
559
+ });
560
+
561
+ return {
562
+ now: now,
563
+ cycle: cycle,
564
+ licenses: licensesInCycle,
565
+ };
566
+ } catch (e) { throw e; }
567
+ };
568
+
569
+ private readonly licensesToConcepts = async (cycle: BillingCycle, records: OrganizationLicense[], billed: OrganizationLicense[] = []): Promise<InvoiceConcept[]> => {
570
+ try {
571
+ const all = _.orderBy([...billed, ...records], (r) => new Date(r.start_date), `asc`);
572
+
573
+ const missingAssociatedIDs = _.compact(records.map((r) => r.associated).filter((id) => !all.some((l) => l.id === id)));
574
+
575
+ if (missingAssociatedIDs.length > 0) {
576
+ const associated = await organizationLicenseController.list({ id: { in: missingAssociatedIDs } });
577
+ all.push(...associated);
578
+ }
579
+
580
+ const services = await serviceController.list({ id: { in: all.map((r) => r.service) } });
581
+
582
+ const response: InvoiceConcept[] = [];
583
+ for (const record of _.orderBy(records, (r) => new Date(r.start_date), `asc`)) {
584
+ const service = services.find((s) => s.id === record.service);
585
+
586
+ const priceKey = record.billing === `annual` ? `annual_price` : `monthly_price`;
587
+ const parsed: InvoiceConcept = {
588
+ name: `${service.properties.name} - ${record.billing === `annual` ? `Annual` : `Monthly`} ${record.type === LicenseType.Base ? `plan` : `power-up`}`,
589
+ description: record.type === LicenseType.Base ?
590
+ `${record.properties.name} license`
591
+ : record.properties.name,
592
+ price: record[priceKey] || 0,
593
+ };
594
+
595
+ const associated = record.associated ? all.find((l) => l.id === record.associated) : null;
596
+ const isBaseLicense = service.uuid === authority.configuration.uuid && record.type === LicenseType.Base;
597
+
598
+ if (associated) {
599
+ // associated licenses
600
+ const associatedService = services.find((s) => s.id === associated.service);
601
+ const associatedName = `${associatedService.properties.name} ${associated.properties.name} license`;
602
+
603
+ parsed.description = `${parsed.description} - Included in ${associatedName}`;
604
+ } else if (isBaseLicense) {
605
+ // base license
606
+ parsed.description = `${parsed.description} - Included`;
607
+ }
608
+
609
+ // charge difference in upgraded licenses
610
+ const upgradeFrom = record.type === LicenseType.Base ?
611
+ _.last(all.filter((r) =>
612
+ r.type === LicenseType.Base
613
+ && r.id !== record.id
614
+ && r.service === record.service
615
+ && r.end_date
616
+ && (!record.end_date || new Date2(r.end_date).isBefore(new Date2(record.end_date)))
617
+ )) : null;
618
+
619
+ if (upgradeFrom) {
620
+ parsed.description = `${parsed.description} - Upgrade`;
621
+ parsed.price = parsed.price - (upgradeFrom[priceKey] || 0);
622
+ }
623
+
624
+ // charge 0€ for trial period
625
+ if (cycle.trial) {
626
+ parsed.description = `${parsed.description} (Free trial)`;
627
+ parsed.price = 0;
628
+ }
629
+
630
+ response.push(parsed);
631
+ }
632
+ return response;
633
+ } catch (e) { throw e; }
634
+ };
635
+
636
+ public readonly getBaseLicense = async (organization: Organization): Promise<OrganizationLicense> => {
637
+ try {
638
+ // get base license
639
+ const service = (await serviceController.list({ uuid: authority.configuration.uuid }))[0];
640
+ if (!service) {
641
+ throw new APIError({
642
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
643
+ detail: `Could not find service.`,
644
+ });
645
+ }
646
+
647
+ const base = (await organizationLicenseController.list({
648
+ organization: organization.id,
649
+ service: service.id,
650
+ type: LicenseType.Base,
651
+ enabled: true,
652
+ }))[0];
653
+ if (!base) {
654
+ throw new APIError({
655
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
656
+ detail: `Could not find base license.`,
657
+ });
658
+ }
659
+
660
+ return base;
661
+ } catch (e) { throw e; }
662
+ };
663
+
664
+ private readonly parseInvoices = async (records: Invoice[]): Promise<PublicInvoice[]> => {
665
+ try {
666
+ const response: PublicInvoice[] = [];
667
+
668
+ const [organizations, methods] = await Promise.all([
669
+ organizationController.list({ id: { in: _.compact(records.map(r => r.organization)) } }),
670
+ paymentMethodController.list({ id: { in: _.compact(records.map(r => r.method)) } }),
671
+ ]);
672
+ const [parsedOrganizations, parsedMethods] = await Promise.all([
673
+ publicOrganizationController.parse(null, organizations),
674
+ publicOrganizationController.parsePaymentMethods(null, methods),
675
+ ]);
676
+
677
+ const mapOrganizations = Object.assign({}, ...organizations.map((o) => ({ [o.id]: parsedOrganizations.find((p) => p.uuid === o.uuid) })));
678
+ const mapMethods = Object.assign({}, ...methods.map((m) => ({ [m.id]: parsedMethods.find((p) => p.uuid === m.uuid) })));
679
+
680
+ for (const record of records) {
681
+ const organization = record.organization ? mapOrganizations[record.organization] : null;
682
+ if (!organization) { continue; }
683
+
684
+ const method = record.method ? mapMethods[record.method] : null;
685
+
686
+ const parsed: PublicInvoice = {
687
+ uuid: record.uuid,
688
+ organization: organization,
689
+ date: record.date,
690
+ base: record.base,
691
+ tax_code: record.tax_code,
692
+ tax_percentage: record.tax_percentage,
693
+ tax: record.tax,
694
+ amount: record.amount,
695
+ concepts: _.orderBy(record.concepts, (c) => c.price, `desc`),
696
+ billing: record.billing,
697
+ invoice: record.invoice,
698
+ number: record.number || null,
699
+ payment: record.payment,
700
+ method: method || null,
701
+ status: record.status,
702
+ metadata: record.metadata,
703
+ };
704
+ response.push(parsed);
705
+ }
706
+
707
+ return response;
708
+ } catch (e) { throw e; }
709
+ };
710
+
711
+ private readonly parseAccountingSeats = async (records: AccountingSeat[]): Promise<PublicAccountingSeat[]> => {
712
+ try {
713
+ const response: PublicAccountingSeat[] = [];
714
+
715
+ const invoices = await invoiceController.list({ id: { in: _.compact(records.map(r => r.invoice)) } });
716
+ const parsedInvoices = await this.parseInvoices(invoices);
717
+ const mapMethods = Object.assign({}, ...invoices.map((m) => ({ [m.id]: parsedInvoices.find((p) => p.uuid === m.uuid) })));
718
+
719
+ for (const record of records) {
720
+ const invoice = mapMethods[record.invoice];
721
+
722
+ const parsed: PublicAccountingSeat = {
723
+ uuid: record.uuid,
724
+ date: record.date,
725
+ amount: record.amount,
726
+ invoice: invoice,
727
+ done: record.done,
728
+ metadata: record.metadata,
729
+ };
730
+ response.push(parsed);
731
+ }
732
+
733
+ return response;
734
+ } catch (e) { throw e; }
735
+ };
736
+
737
+ private readonly UE_ES = `ES`;
738
+ private readonly getOrganizationTaxConfiguration = async (organization: Organization): Promise<{ code: string, percentage: number }> => {
739
+ try {
740
+ const { address: { country, province }, document } = organization.billing;
741
+
742
+ const { countries } = await Requester.get(`https://ec.europa.eu/taxation_customs/tedb/rest-api/configurations`) as { countries: UE_Country[] };
743
+
744
+ const ueCountry = countries.find((c) => c.defaultCountryCode === country.toUpperCase());
745
+
746
+ if (ueCountry) {
747
+ if (ueCountry.defaultCountryCode !== this.UE_ES) {
748
+ const { isValid: inVIES } = await Requester.get(`https://ec.europa.eu/taxation_customs/vies/rest-api/ms/${ueCountry.defaultCountryCode}/vat/${encodeURIComponent(document)}`);
749
+ if (inVIES) {
750
+ // art. 44 directiva 2006/112/ce
751
+ return { code: `s_iva_intras`, percentage: 0 };
752
+ }
753
+ } else if ([`LAS PALMAS`, `SANTA CRUZ DE TENERIFE`, `CEUTA`, `MELILLA`].includes(province.toUpperCase())) {
754
+ // art. 69 ley 37/1992
755
+ return { code: `s_iva_nosujeto`, percentage: 0 };
756
+ }
757
+
758
+ // get country VAT percentage
759
+ const { result: vatOptions } = await Requester.post(`https://ec.europa.eu/taxation_customs/tedb/rest-api/simpleSearch`, {
760
+ searchForm: {
761
+ selectedTaxTypes: [`VAT`],
762
+ selectedMemberStates: [ueCountry.id],
763
+ historized: false,
764
+ },
765
+ }) as { result: UE_VAT_Item[] };
766
+
767
+ const genericVAT = (vatOptions || []).find((v) => v.genericName === `VAT`);
768
+ if (!genericVAT) { throw new Error(`Could not get generic country VAT percentage (${ueCountry.defaultCountryCode})`); }
769
+
770
+ const rates = await Requester.get(`https://ec.europa.eu/taxation_customs/tedb/rest-api/tax/rate?taxId=${genericVAT.taxId}&versionDate=${genericVAT.versionDate}&isEuro=true`) as UE_VAT;
771
+ const percentage = parseFloat(rates.vatRateStructure.standardRate.rate.value);
772
+ if (typeof percentage === `undefined`) { throw new Error(`Could not get country VAT percentage (${ueCountry.defaultCountryCode})`); }
773
+
774
+ return {
775
+ code: ueCountry.defaultCountryCode === this.UE_ES ?
776
+ `s_iva_${percentage}` :
777
+ `s_iva_${ueCountry.defaultCountryCode.toLowerCase()}_${percentage}`,
778
+ percentage: percentage,
779
+ };
780
+ } else {
781
+ // art. 69 liva
782
+ return { code: `s_iva_export`, percentage: 0 };
783
+ }
784
+ } catch (e) { throw e; }
785
+ };
786
+
787
+ public readonly isBillable = async (organization: Organization): Promise<boolean> => {
788
+ try {
789
+ // check if billing information is defined
790
+ if (!organization.billing?.document) { return false; }
791
+
792
+ // check if a payment method is enabled
793
+ const defaultMethod = (await paymentMethodController.list({
794
+ organization: organization.id,
795
+ enabled: true,
796
+ }))[0];
797
+ if (!defaultMethod) { return false; }
798
+
799
+ return true;
800
+ } catch (e) { throw e; }
801
+ };
802
+ }
803
+
804
+ const billingController = new BillingController();
805
+ export default billingController;