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,1796 @@
1
+ /* eslint-disable @typescript-eslint/no-var-requires */
2
+ import { Resource } from "@beseif-solutions/prow-authority-for-services";
3
+ import { Date2, Formats } from "@beseif-solutions/utility-functions/dist/utils/date-utils";
4
+ import { Requester } from "@beseif-solutions/utility-functions/dist/utils/http-utils";
5
+ import { Where } from "@beseif-solutions/utility-functions/dist/utils/sql-utils";
6
+ import { DeepPartial } from "@beseif-solutions/utility-functions/dist/utils/typing-utils";
7
+ import { Schema, Validator } from "@beseif-solutions/utility-functions/dist/utils/validation-utils";
8
+ import _ from "lodash";
9
+ import Stripe from "stripe";
10
+ import APIError, { StatusCodes } from "../../../helpers/api-error";
11
+ import authority, { BillingChanged, LicenseDowngraded, LicenseDowngradeScheduled, LicenseLimitNames, LicenseUpgraded, MemberRemoved, NotificationEventNames, OrganizationAdded, OrganizationRemoved, PowerUpAdded, PowerUpRemoved, ResourceModelName } from "../../../helpers/authority";
12
+ import nconf from "../../../helpers/config";
13
+ import holded, { Holded } from "../../../helpers/holded";
14
+ import stripe from "../../../helpers/stripe";
15
+ import { informationResponseSchema } from "../../schemas/admin/service-schemas";
16
+ import { activateLicenseSchema, activatePowerUpSchema, addPaymentMethodSchema, createOrganizationSchema, licenseParamsSchema, updateOrganizationSchema, updatePaymentMethodSchema } from "../../schemas/organizations/public-organization-schemas";
17
+ import { changeOwnershipSchema, inviteUserSchema, updateUserAccessSchema, updateUserOrganizationSchema } from "../../schemas/users/public-user-schemas";
18
+ import { BillingCycle } from "../../types/admin/billing-cycle-types";
19
+ import { Invoice } from "../../types/admin/invoice-types";
20
+ import { License, LicenseType, Prices } from "../../types/admin/license-types";
21
+ import { OrganizationLicense } from "../../types/admin/organization-license-types";
22
+ import { Organization, OrganizationStatus, ServiceInformation } from "../../types/admin/organization-types";
23
+ import { PaymentMethod, PaymentMethodType } from "../../types/admin/payment-method-types";
24
+ import { UserOrganization, UserOrganizationRole } from "../../types/admin/user-organization-types";
25
+ import { User, UserStatus } from "../../types/admin/user-types";
26
+ import { Environment } from "../../types/auth/organization-token-types";
27
+ import { ActivateLicense, ActivatePowerUp, AddPaymentMethod, InviteUser, LicenseParams, PublicInvoice, PublicOrganization, PublicOrganizationLicense, PublicOrganizationUser, PublicPaymentMethod, UpdatePaymentMethod, UpdateUserAccess, UpdateUserOrganization } from "../../types/organizations/public-organization-types";
28
+ import { PublicService, PublicServiceInformation } from "../../types/services/public-service-types";
29
+ import { Quota, QuotaParams, ServiceEnvironment } from "../../types/services/service-types";
30
+ import billingCycleController from "../admin/billing-cycle-controller";
31
+ import invoiceController from "../admin/invoice-controller";
32
+ import licenseController from "../admin/license-controller";
33
+ import licenseLimitController from "../admin/license-limit-controller";
34
+ import notificationController from "../admin/notification-controller";
35
+ import organizationController from "../admin/organization-controller";
36
+ import organizationLicenseController from "../admin/organization-license-controller";
37
+ import organizationLicenseLimitController from "../admin/organization-license-limit-controller";
38
+ import paymentMethodController from "../admin/payment-method-controller";
39
+ import adminServiceController from "../admin/service-controller";
40
+ import userController from "../admin/user-controller";
41
+ import userOrganizationController from "../admin/user-organization-controller";
42
+ import billingController from "../billing/billing-controller";
43
+ import publicServiceController from "../services/public-service-controller";
44
+ import serviceController from "../services/service-controller";
45
+ import publicUserController from "../users/public-user-controller";
46
+
47
+ const { getCountry } = require(`country-list-spanish`);
48
+ const { getName } = require(`country-list`);
49
+
50
+ type Create = Omit<PublicOrganization, `uuid` | `status` | `created_date` | `modified_date`>;
51
+ type Update = DeepPartial<Omit<PublicOrganization, `uuid` | `status` | `created_date` | `modified_date`>>;
52
+
53
+ class PublicOrganizationCtrl {
54
+
55
+ // authority endpoint: organization resources
56
+ public readonly authority = async (organizationUUID: string): Promise<Resource<ResourceModelName>[]> => {
57
+ try {
58
+ const organization = (await organizationController.list({ uuid: organizationUUID }))[0];
59
+ if (!organization) {
60
+ throw new APIError({
61
+ code: StatusCodes.NOT_FOUND,
62
+ detail: `Organization ${organizationUUID} not found.`,
63
+ });
64
+ }
65
+
66
+ const [seats] = await Promise.all([
67
+ userOrganizationController.list({ organization: organization.id }),
68
+ ]);
69
+
70
+ const response: Resource<ResourceModelName>[] = [
71
+ {
72
+ type: `owner`,
73
+ model: `organization.seat`,
74
+ count: seats.length,
75
+ },
76
+ ];
77
+ return response;
78
+ } catch (e) { throw e; }
79
+ };
80
+
81
+ public readonly getInformation = async (env: Environment): Promise<PublicServiceInformation[]> => {
82
+ try {
83
+ const services = await adminServiceController.list({ enabled: true });
84
+
85
+ const response: PublicServiceInformation[] = _.compact(await Promise.all(services.map(async (service) => {
86
+ try {
87
+ let information: any[] = [];
88
+ if (service.internal.host && service.internal.organization?.information) {
89
+ information = await Requester.get(`${service.internal.organization?.information}/${env.organization.uuid}`, {
90
+ baseURL: service.internal.host,
91
+ headers: {
92
+ "x-service-uuid": service.uuid,
93
+ "x-service-token": service.token,
94
+ },
95
+ });
96
+ Validator.validate(informationResponseSchema, information);
97
+ }
98
+
99
+ return {
100
+ service: (await publicServiceController.parseServices([service]))[0],
101
+ information: information,
102
+ };
103
+ } catch (e) { return null; }
104
+ })));
105
+ return response;
106
+ } catch (e) { throw e; }
107
+ };
108
+
109
+ private readonly deleteInformation = async (env: Environment): Promise<void> => {
110
+ try {
111
+ const services = await adminServiceController.list({ enabled: true });
112
+
113
+ await Promise.all(services.map(async (service) => {
114
+ try {
115
+ if (service.internal.host && service.internal.organization?.deleted) {
116
+ await Requester.delete(`${service.internal.organization.deleted}/${env.organization.uuid}`, {
117
+ baseURL: service.internal.host,
118
+ headers: {
119
+ "x-service-uuid": service.uuid,
120
+ "x-service-token": service.token,
121
+ },
122
+ });
123
+ }
124
+ } catch (e) {
125
+ // TODO: what to do when error?
126
+ }
127
+ }));
128
+ } catch (e) { throw e; }
129
+ };
130
+
131
+ public readonly create = async (user: User, data: Create): Promise<PublicOrganization> => {
132
+ let organization: Organization;
133
+ try {
134
+ Validator.validate(createOrganizationSchema, data);
135
+
136
+ // create organization
137
+ organization = (await organizationController.add({
138
+ ...data,
139
+ status: OrganizationStatus.Active,
140
+ internal: {
141
+ services: {},
142
+ },
143
+ }, { timestamp: true }))[0];
144
+
145
+ // get base license
146
+ const service = (await adminServiceController.list({ uuid: authority.configuration.uuid }))[0];
147
+ if (!service) {
148
+ throw new APIError({
149
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
150
+ detail: `Could not find service.`,
151
+ });
152
+ }
153
+
154
+ const base = (await licenseController.list({
155
+ service: service.id,
156
+ type: LicenseType.Base,
157
+ enabled: true,
158
+ }))[0];
159
+ if (!base) {
160
+ throw new APIError({
161
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
162
+ detail: `Could not find base license.`,
163
+ });
164
+ }
165
+
166
+ // assign base license
167
+ await this.configureLicense(organization, base, {
168
+ billing: `lifetime`, // set lifetime until billing is configured
169
+ start_date: Date2.current(),
170
+ internal: {},
171
+ });
172
+
173
+ // add organization owner
174
+ await userOrganizationController.add({
175
+ organization: organization.id,
176
+ user: user.id,
177
+ role: UserOrganizationRole.Owner,
178
+ invited_by: null,
179
+ invite: null,
180
+ enabled: true,
181
+ metadata: {},
182
+ });
183
+
184
+ // update quota usage
185
+ await authority.register(organization.uuid, {
186
+ name: LicenseLimitNames.Seats,
187
+ count: +1,
188
+ });
189
+
190
+ await authority.notify<OrganizationAdded>(organization.uuid, {
191
+ event: NotificationEventNames.OrganizationAdded,
192
+ data: {
193
+ cta: {
194
+ dashboard: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:DASHBOARD`),
195
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
196
+ },
197
+ organization: organization,
198
+ },
199
+ });
200
+
201
+ return (await publicOrganizationController.parse(null, [organization]))[0];
202
+ } catch (e) {
203
+ if (organization) { await organizationController.delete(organization.id); }
204
+ throw e;
205
+ }
206
+ };
207
+
208
+ public readonly get = async (env: Environment): Promise<PublicOrganization> => {
209
+ try {
210
+ const organization = (await organizationController.get(env.organization.id, { timestamp: true }))[0];
211
+ const [response] = await this.parse(env, [organization]);
212
+ return response;
213
+ } catch (e) { throw e; }
214
+ };
215
+
216
+ public readonly update = async (env: Environment, data: Update): Promise<PublicOrganization> => {
217
+ try {
218
+ Validator.validate(updateOrganizationSchema, data);
219
+ const updated = (await organizationController.update(env.organization.id, data, { timestamp: true }))[0];
220
+
221
+ if (!_.isEqual(env.organization.billing, updated.billing) && updated.billing?.document) {
222
+ // update customer
223
+ await Promise.all([
224
+ this.configureStripeCustomer(updated),
225
+ this.configureHoldedContact(updated),
226
+ ]);
227
+ }
228
+
229
+ const [response] = await this.parse(env, [updated]);
230
+ return response;
231
+ } catch (e) { throw e; }
232
+ };
233
+
234
+ public readonly delete = async (env: Environment, force: boolean): Promise<void> => {
235
+ try {
236
+ const ownService = (await adminServiceController.list({ uuid: authority.configuration.uuid }))[0];
237
+ if (!ownService) {
238
+ throw new APIError({
239
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
240
+ detail: `Could not find base service.`,
241
+ });
242
+ }
243
+
244
+ const activeLicenses = await organizationLicenseController.list({
245
+ organization: env.organization.id,
246
+ enabled: true,
247
+ service: { is: ownService.id, inverse: true },
248
+ });
249
+
250
+ if (activeLicenses.filter((l) => l.end_date === null && l.type === LicenseType.Base).length > 0 && !force) {
251
+ throw new APIError({
252
+ code: StatusCodes.CONFLICT,
253
+ detail: `Organization has active licenses.`,
254
+ });
255
+ }
256
+
257
+ await authority.notify<OrganizationRemoved>(env.organization.uuid, {
258
+ event: NotificationEventNames.OrganizationRemoved,
259
+ data: {
260
+ cta: {
261
+ dashboard: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_DASHBOARD`),
262
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
263
+ },
264
+ organization: env.organization,
265
+ },
266
+ });
267
+
268
+ const deleteDate = Date2.current().format(Formats.ISO);
269
+
270
+ // end all organization licenses
271
+ await organizationLicenseController.update(env.license.id, { end_date: deleteDate });
272
+ for (const license of activeLicenses) {
273
+ await organizationLicenseController.update(license.id, { end_date: deleteDate });
274
+ }
275
+
276
+ // TODO: last payment?
277
+
278
+ // remove accesses
279
+ const accesses = await userOrganizationController.list({ organization: env.organization.id });
280
+ for (const access of accesses) { await userOrganizationController.delete(access.id); }
281
+
282
+ // mark organization as deleted
283
+ await organizationController.update(env.organization.id, {
284
+ status: `deleted` as any,
285
+ internal: { deleted_date: deleteDate },
286
+ }, { deletion: true });
287
+
288
+ // TODO: schedule the deletion of the organization
289
+ // await this.deleteInformation(env);
290
+ } catch (e) {
291
+ console.log(e);
292
+ throw e;
293
+ }
294
+ };
295
+
296
+ public readonly getServiceLicenses = async (env: Environment, serviceUUID: string, params: LicenseParams = {}): Promise<PublicOrganizationLicense[]> => {
297
+ try {
298
+ Validator.validate(licenseParamsSchema, params);
299
+
300
+ return this.getLicenses(env, serviceUUID, {
301
+ type: LicenseType.Base,
302
+ ...params.enabled ? { enabled: params.enabled === `true` } : {},
303
+ });
304
+ } catch (e) { throw e; }
305
+ };
306
+
307
+ public readonly activateServiceLicense = async (env: Environment, serviceUUID: string, data: ActivateLicense): Promise<PublicOrganizationLicense[]> => {
308
+ let organizationLicense: OrganizationLicense;
309
+ try {
310
+ Validator.validate(activateLicenseSchema, data);
311
+
312
+ const context = await this.licenseActivationContext(env, serviceUUID, { ...data, type: LicenseType.Base });
313
+
314
+ const isBillable = await billingController.isBillable(env.organization);
315
+ if ((context.cycle.trial && data.renew && !isBillable)
316
+ || (!context.cycle.trial && !isBillable)) {
317
+ throw new APIError({
318
+ code: StatusCodes.BAD_REQUEST,
319
+ detail: `Organization must configure billing and payment before doing this action.`,
320
+ });
321
+ }
322
+
323
+ let startDate: Date2;
324
+ let endDate: Date2;
325
+
326
+ const now = Date2.current();
327
+
328
+ if (context.enabled) {
329
+ // determine immediate or end of cycle change
330
+ const key: keyof Prices = context.enabled.billing === `monthly` ? `monthly_price` : `annual_price`;
331
+ const immediateChange =
332
+ // upgrade (and not scheduled)
333
+ ((context.license[key] >= context.enabled[key]) && !data.scheduled)
334
+ // trial period license change
335
+ || (context.cycle.trial);
336
+
337
+ if (context.future) {
338
+ // override previously selected future license
339
+ await organizationLicenseController.delete(context.future.id);
340
+ }
341
+
342
+ if (!context.enabled.end_date || immediateChange) {
343
+ context.enabled = (await organizationLicenseController.update(context.enabled.id, {
344
+ end_date: immediateChange ? now.format(Formats.ISO) : context.cycle.end_date,
345
+ }))[0];
346
+ }
347
+
348
+ // start future license when the enabled license ends
349
+ startDate = new Date2(context.enabled.end_date, Formats.ISO);
350
+ } else {
351
+ // start license now
352
+ startDate = now;
353
+ }
354
+
355
+ if (context.enabled && context.cycle.trial) {
356
+ // end trial when license change
357
+ context.cycle = await this.internal_endTrial(env, context.cycle, now);
358
+ }
359
+
360
+ if (!data.renew) { endDate = new Date2(context.cycle.end_date, Formats.ISO); }
361
+
362
+ const liveNotification = startDate.isEqual(now);
363
+
364
+ organizationLicense = await this.configureLicense(env.organization, context.license, {
365
+ billing: data.billing,
366
+ start_date: startDate,
367
+ end_date: endDate,
368
+ metadata: data.metadata,
369
+ // flag notification for later if not live
370
+ internal: liveNotification ? {} : { notified: false },
371
+ });
372
+
373
+ // notify license activation on live
374
+ if (liveNotification) {
375
+ if (context.setup && context.service.internal.host && context.service.internal.organization?.created) {
376
+ // setup organization on service
377
+ setImmediate(async () => {
378
+ try {
379
+ await Requester.post(`${context.service.internal.organization?.created}`,
380
+ (await this.parse(env, [env.organization]))[0],
381
+ {
382
+ baseURL: context.service.internal.host,
383
+ headers: {
384
+ "x-service-uuid": context.service.uuid,
385
+ "x-service-token": context.service.token,
386
+ },
387
+ });
388
+ } catch { /* TODO: what to do if the organization setup fails on service? */ }
389
+ });
390
+ } else if (context.service.internal.host && context.service.internal.organization?.modified) {
391
+ // update organization setup on service
392
+ setImmediate(async () => {
393
+ try {
394
+ await Requester.patch(`${context.service.internal.organization?.modified}/${env.organization.uuid}`,
395
+ null,
396
+ {
397
+ baseURL: context.service.internal.host,
398
+ headers: {
399
+ "x-service-uuid": context.service.uuid,
400
+ "x-service-token": context.service.token,
401
+ },
402
+ });
403
+ } catch { /* TODO: what to do if the organization setup fails on service? */ }
404
+ });
405
+ }
406
+
407
+ // notify license upgrade or downgrade when changing from one license to another
408
+ if (context.enabled) {
409
+ setImmediate(async () => {
410
+ try {
411
+ const key: keyof Prices = context.enabled.billing === `monthly` ? `monthly_price` : `annual_price`;
412
+ const downgrade = context.license[key] < context.enabled[key];
413
+
414
+ if (downgrade) {
415
+ await authority.notify<LicenseDowngraded>(env.organization.uuid, {
416
+ event: NotificationEventNames.LicenseDowngraded,
417
+ data: {
418
+ cta: {
419
+ dashboard: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_DASHBOARD`),
420
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
421
+ },
422
+ organization: env.organization,
423
+ },
424
+ });
425
+ } else {
426
+ await authority.notify<LicenseUpgraded>(env.organization.uuid, {
427
+ event: NotificationEventNames.LicenseUpgraded,
428
+ data: {
429
+ cta: {
430
+ dashboard: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_DASHBOARD`),
431
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
432
+ },
433
+ organization: env.organization,
434
+ },
435
+ });
436
+ }
437
+ } catch { }
438
+ });
439
+ }
440
+ } else if (context.enabled) {
441
+ // scheduled change at end of current cycle: notify only on downgrade
442
+ setImmediate(async () => {
443
+ try {
444
+ const key = context.enabled.billing === `monthly` ? `monthly_price` : `annual_price`;
445
+ const downgrade = context.license[key] < context.enabled[key];
446
+
447
+ if (downgrade) {
448
+ await authority.notify<LicenseDowngradeScheduled>(env.organization.uuid, {
449
+ event: NotificationEventNames.LicenseDowngradeScheduled,
450
+ data: {
451
+ organization: env.organization,
452
+ scheduled_date: startDate.format(Formats.ReadableFormat),
453
+ cta: {
454
+ subscription: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_USAGE`),
455
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
456
+ },
457
+ },
458
+ });
459
+ }
460
+ } catch { }
461
+ });
462
+ }
463
+
464
+ setImmediate(async () => {
465
+ try {
466
+ // generate invoice once license is activated
467
+ await billingController.generateLicensesInvoice(env.organization);
468
+ } catch { }
469
+ });
470
+
471
+ return await this.getServiceLicenses(env, serviceUUID);
472
+ } catch (e) {
473
+ if (organizationLicense) { await organizationLicenseController.delete(organizationLicense.id); }
474
+ throw e;
475
+ }
476
+ };
477
+
478
+ public readonly renewServiceLicense = async (env: Environment, serviceUUID: string, licenseUUID: string): Promise<PublicOrganizationLicense> => {
479
+ try {
480
+ if (!await billingController.isBillable(env.organization)) {
481
+ throw new APIError({
482
+ code: StatusCodes.BAD_REQUEST,
483
+ detail: `Organization must configure billing and payment before doing this action.`,
484
+ });
485
+ }
486
+
487
+ const service = (await adminServiceController.list({ uuid: serviceUUID }))[0];
488
+ if (!service) {
489
+ throw new APIError({
490
+ code: StatusCodes.BAD_REQUEST,
491
+ detail: `No service ${serviceUUID} found.`,
492
+ });
493
+ }
494
+
495
+ let license = (await organizationLicenseController.list({
496
+ uuid: licenseUUID,
497
+ organization: env.organization.id,
498
+ service: service.id,
499
+ type: LicenseType.Base,
500
+ enabled: true,
501
+ }))[0];
502
+ if (!license) {
503
+ throw new APIError({
504
+ code: StatusCodes.NOT_FOUND,
505
+ detail: `No license ${licenseUUID} found.`,
506
+ });
507
+ }
508
+
509
+ if (license.end_date) {
510
+ license = (await organizationLicenseController.update(license.id, { end_date: null }))[0];
511
+ }
512
+
513
+ return (await this.parseLicenses(env.organization, [license]))[0];
514
+ } catch (e) { throw e; }
515
+ };
516
+
517
+ public readonly cancelServiceLicense = async (env: Environment, serviceUUID: string, licenseUUID: string): Promise<PublicOrganizationLicense | void> => {
518
+ try {
519
+ return await this.cancelLicense(env, serviceUUID, licenseUUID, LicenseType.Base);
520
+ } catch (e) { throw e; }
521
+ };
522
+
523
+ public readonly activateServicePowerUp = async (env: Environment, serviceUUID: string, data: ActivatePowerUp): Promise<PublicOrganizationLicense[]> => {
524
+ let organizationLicense: OrganizationLicense;
525
+ try {
526
+ Validator.validate(activatePowerUpSchema, data);
527
+
528
+ const context = await this.licenseActivationContext(env, serviceUUID, { ...data, type: LicenseType.Variant });
529
+
530
+ if (context.cycle.trial) {
531
+ throw new APIError({
532
+ code: StatusCodes.BAD_REQUEST,
533
+ detail: `Cannot activate power-up during trial period.`,
534
+ });
535
+ }
536
+
537
+ if (!await billingController.isBillable(env.organization)) {
538
+ throw new APIError({
539
+ code: StatusCodes.BAD_REQUEST,
540
+ detail: `Organization must configure billing and payment before doing this action.`,
541
+ });
542
+ }
543
+
544
+ let endDate: Date2;
545
+ if (!data.renew) { endDate = new Date2(context.cycle.end_date, Formats.ISO); }
546
+
547
+ organizationLicense = await this.configureLicense(env.organization, context.license, {
548
+ billing: data.billing,
549
+ start_date: Date2.current(),
550
+ end_date: endDate,
551
+ metadata: data.metadata,
552
+ internal: {},
553
+ });
554
+
555
+ setImmediate(async () => {
556
+ try {
557
+ await authority.notify<PowerUpAdded>(env.organization.uuid, {
558
+ event: NotificationEventNames.PowerUpAdded,
559
+ data: {
560
+ cta: {
561
+ dashboard: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_DASHBOARD`),
562
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
563
+ },
564
+ organization: env.organization,
565
+ },
566
+ });
567
+ } catch { }
568
+ });
569
+
570
+ setImmediate(async () => {
571
+ try {
572
+ // generate invoice once license is activated
573
+ await billingController.generateLicensesInvoice(env.organization);
574
+ } catch { }
575
+ });
576
+
577
+ return await this.getLicenses(env, serviceUUID, { id: organizationLicense.id });
578
+ } catch (e) {
579
+ if (organizationLicense) { await organizationLicenseController.delete(organizationLicense.id); }
580
+ throw e;
581
+ }
582
+ };
583
+
584
+ public readonly getServicePowerUps = async (env: Environment, serviceUUID: string, params: LicenseParams = {}): Promise<PublicOrganizationLicense[]> => {
585
+ try {
586
+ Validator.validate(licenseParamsSchema, params);
587
+
588
+ return this.getLicenses(env, serviceUUID, {
589
+ type: LicenseType.Variant,
590
+ ...params.enabled ? { enabled: params.enabled === `true` } : {},
591
+ });
592
+ } catch (e) { throw e; }
593
+ };
594
+
595
+ public readonly cancelServicePowerUp = async (env: Environment, serviceUUID: string, licenseUUID: string): Promise<PublicOrganizationLicense | void> => {
596
+ try {
597
+ return await this.cancelLicense(env, serviceUUID, licenseUUID, LicenseType.Variant);
598
+ } catch (e) { throw e; }
599
+ };
600
+
601
+ public readonly endTrial = async (env: Environment): Promise<void> => {
602
+ try {
603
+ const { cycle, now } = await billingController.cycleContext(env.organization);
604
+ if (!cycle.trial) {
605
+ throw new APIError({
606
+ code: StatusCodes.BAD_REQUEST,
607
+ detail: `No active trial.`,
608
+ });
609
+ }
610
+
611
+ if (!await billingController.isBillable(env.organization)) {
612
+ throw new APIError({
613
+ code: StatusCodes.BAD_REQUEST,
614
+ detail: `Organization must configure billing and payment before ending trial.`,
615
+ });
616
+ }
617
+
618
+ await this.internal_endTrial(env, cycle, now);
619
+
620
+ setImmediate(async () => {
621
+ try {
622
+ // generate invoice once trial ends
623
+ await billingController.generateLicensesInvoice(env.organization);
624
+ } catch { }
625
+ });
626
+ } catch (e) { throw e; }
627
+ };
628
+
629
+ private readonly internal_endTrial = async (env: Environment, cycle: BillingCycle, now: Date2): Promise<BillingCycle> => {
630
+ try {
631
+ // end trial cycle and suppress trial_ended notification: trial ended by upgrade, not by expiration
632
+ await billingCycleController.update(cycle.id, {
633
+ end_date: now.format(Formats.ISO),
634
+ internal: { trial_ended_sent: true },
635
+ });
636
+
637
+ // start a new one
638
+ const { cycle: newCycle } = await billingController.cycleContext(env.organization, now.format(Formats.ISO));
639
+ return newCycle;
640
+ } catch (e) { throw e; }
641
+ };
642
+
643
+ public readonly getServices = async (env: Environment): Promise<(PublicService & { blocked: boolean })[]> => {
644
+ try {
645
+ const enabledLicenses = await organizationLicenseController.list({
646
+ organization: env.organization.id,
647
+ enabled: true,
648
+ });
649
+
650
+ const services = await adminServiceController.list({ id: { in: _.uniq(enabledLicenses.map((el) => el.service)) }, enabled: true });
651
+ return (await publicServiceController.parseServices(services))
652
+ .map((ps) => {
653
+ const information: ServiceInformation = _.get(env.organization.internal, `services.${ps.uuid}`);
654
+ return {
655
+ ...ps,
656
+ blocked: information?.blocked || false,
657
+ };
658
+ });
659
+ } catch (e) { throw e; }
660
+ };
661
+
662
+ public readonly getServiceQuota = async (env: Environment, serviceUUID: string, params: QuotaParams): Promise<Quota[]> => {
663
+ try {
664
+ const service = (await adminServiceController.list({ uuid: serviceUUID }))[0];
665
+ if (!service) {
666
+ throw new APIError({
667
+ code: StatusCodes.BAD_REQUEST,
668
+ detail: `No service ${serviceUUID} found.`,
669
+ });
670
+ }
671
+
672
+ const enabledLicenses = await organizationLicenseController.list({
673
+ organization: env.organization.id,
674
+ service: service.id,
675
+ enabled: true,
676
+ });
677
+
678
+ if (enabledLicenses.length === 0) { return []; }
679
+
680
+ return await serviceController.getQuota({
681
+ organization: env.organization,
682
+ service: service,
683
+ licenses: enabledLicenses,
684
+ }, params);
685
+ } catch (e) { throw e; }
686
+ };
687
+
688
+ public readonly checkServiceQuota = async (env: Environment, serviceUUID: string): Promise<Quota[]> => {
689
+ try {
690
+ const service = (await adminServiceController.list({ uuid: serviceUUID }))[0];
691
+ if (!service) {
692
+ throw new APIError({
693
+ code: StatusCodes.BAD_REQUEST,
694
+ detail: `No service ${serviceUUID} found.`,
695
+ });
696
+ }
697
+
698
+ const enabledLicenses = await organizationLicenseController.list({
699
+ organization: env.organization.id,
700
+ service: service.id,
701
+ enabled: true,
702
+ });
703
+
704
+ if (enabledLicenses.length === 0) { return []; }
705
+
706
+ const sEnv: ServiceEnvironment = {
707
+ organization: env.organization,
708
+ service: service,
709
+ licenses: enabledLicenses,
710
+ };
711
+
712
+ await serviceController.checkQuota(sEnv);
713
+
714
+ return await serviceController.getQuota(sEnv, {});
715
+ } catch (e) { throw e; }
716
+ };
717
+
718
+ public readonly updateOverconsume = async (env: Environment, serviceUUID: string, body: { limit: string, overconsume: boolean }): Promise<PublicOrganizationLicense[]> => {
719
+ try {
720
+ Validator.validate(Schema.object({
721
+ limit: Schema.requiredString(),
722
+ overconsume: Schema.requiredBool(),
723
+ }), body);
724
+
725
+ const service = (await adminServiceController.list({ uuid: serviceUUID }))[0];
726
+ if (!service) {
727
+ throw new APIError({
728
+ code: StatusCodes.BAD_REQUEST,
729
+ detail: `No service ${serviceUUID} found.`,
730
+ });
731
+ }
732
+
733
+ const enabledLicenses = await organizationLicenseController.list({
734
+ organization: env.organization.id,
735
+ service: service.id,
736
+ enabled: true,
737
+ });
738
+
739
+ const limits = (await organizationLicenseLimitController.list({
740
+ organization: env.organization.id,
741
+ license: { in: enabledLicenses.map((l) => l.id) },
742
+ name: body.limit,
743
+ })).filter((ll) => ll.overconsume_annual_price_per_unit !== null || ll.overconsume_monthly_price_per_unit !== null);
744
+
745
+ if (limits.length === 0) {
746
+ throw new APIError({
747
+ code: StatusCodes.BAD_REQUEST,
748
+ detail: `Limit ${body.limit} can't be configured to overconsume.`,
749
+ });
750
+ } else if (limits.length > 1) {
751
+ throw new APIError({
752
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
753
+ detail: `Unexpected error: multiple limits found. Please contact support.`,
754
+ });
755
+ }
756
+
757
+ const limit = limits[0];
758
+ await organizationLicenseLimitController.update(limit.id, {
759
+ overconsume: body.overconsume,
760
+ });
761
+
762
+ const response = enabledLicenses.find((l) => l.id === limit.license);
763
+ return await this.parseLicenses(env.organization, [response]);
764
+ } catch (e) { throw e; }
765
+ };
766
+
767
+ public readonly getMe = async (env: Environment): Promise<PublicOrganizationUser> => {
768
+ try {
769
+ const response = (await this.parseOrganizationUsers([env.access], {
770
+ userCache: [env.user],
771
+ preferences: true,
772
+ }))[0];
773
+ return response;
774
+ } catch (e) { throw e; }
775
+ };
776
+
777
+ public readonly updateMe = async (env: Environment, data: UpdateUserOrganization): Promise<PublicOrganizationUser> => {
778
+ try {
779
+ Validator.validate(updateUserOrganizationSchema, data);
780
+
781
+ // omnipotent accesses are synthetic: can't be updated
782
+ if (env.user.omnipotent) { throw new Error(`Omnipotent user access can't be updated.`); }
783
+
784
+ const { access } = await this.getUserAndAccess(env, env.user.uuid);
785
+ const updated = await userOrganizationController.update(access.id, data);
786
+ return (await this.parseOrganizationUsers(updated, { preferences: true }))[0];
787
+ } catch (e) { throw e; }
788
+ };
789
+
790
+ public readonly getOwner = async (env: Environment): Promise<PublicOrganizationUser> => {
791
+ try {
792
+ if (env.access.role === UserOrganizationRole.Owner && !env.user.omnipotent) { return await this.getMe(env); }
793
+
794
+ const owner = (await userOrganizationController.list({
795
+ organization: env.organization.id,
796
+ role: UserOrganizationRole.Owner,
797
+ }))[0];
798
+ if (!owner) {
799
+ throw new APIError({
800
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
801
+ detail: `Could not find organization owner.`,
802
+ });
803
+ }
804
+
805
+ return (await this.parseOrganizationUsers([owner]))[0];
806
+ } catch (e) { throw e; }
807
+ };
808
+
809
+ public readonly getUsers = async (env: Environment, options: { all: boolean }): Promise<PublicOrganizationUser[]> => {
810
+ try {
811
+ const accesses = await userOrganizationController.list({
812
+ organization: env.organization.id,
813
+ ...(!options.all ? { invite: { is: null } } : {}),
814
+ });
815
+
816
+ return await this.parseOrganizationUsers(accesses);
817
+ } catch (e) { throw e; }
818
+ };
819
+
820
+ public readonly inviteUser = async (env: Environment, data: InviteUser): Promise<PublicOrganizationUser> => {
821
+ try {
822
+ Validator.validate(inviteUserSchema, data);
823
+
824
+ let user = (await userController.list({ email: data.email }, { password: false }))[0];
825
+
826
+ if (!user) {
827
+ // create user
828
+ user = (await userController.add({ email: data.email }, { password: false }))[0];
829
+ }
830
+
831
+ if (user.omnipotent) {
832
+ throw new APIError({
833
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
834
+ detail: `Could not complete your request.`,
835
+ });
836
+ }
837
+
838
+ const exists = (await userOrganizationController.list({
839
+ organization: env.organization.id,
840
+ user: user.id,
841
+ }))[0];
842
+ if (exists) {
843
+ throw new APIError({
844
+ code: StatusCodes.CONFLICT,
845
+ detail: `User is already on organization.`,
846
+ });
847
+ }
848
+
849
+ const invite = await userOrganizationController.getFreeInvite(env.organization.id);
850
+ const access = await userOrganizationController.add({
851
+ user: user.id,
852
+ organization: env.organization.id,
853
+ role: data.role,
854
+ invite: invite,
855
+ invited_by: env.user.id,
856
+ enabled: data.enabled,
857
+ metadata: data.metadata,
858
+ preferences: {},
859
+ });
860
+
861
+ await authority.register(env.organization.uuid, {
862
+ name: LicenseLimitNames.Seats,
863
+ count: +1,
864
+ });
865
+
866
+ await this.sendInvitation(env, user.uuid);
867
+
868
+ return (await this.parseOrganizationUsers(access))[0];
869
+ } catch (e) { throw e; }
870
+ };
871
+
872
+ public readonly sendInvitation = async (env: Environment, userUUID: string): Promise<void> => {
873
+ try {
874
+ const { user, access } = await this.getUserAndAccess(env, userUUID);
875
+ if (!access.invite) {
876
+ throw new APIError({
877
+ code: StatusCodes.BAD_REQUEST,
878
+ detail: `User doesn't have a pending invite.`,
879
+ });
880
+ }
881
+
882
+ await notificationController.invitation(user, access, env.organization);
883
+ } catch (e) { throw e; }
884
+ };
885
+
886
+ public readonly updateUser = async (env: Environment, userUUID: string, data: UpdateUserAccess): Promise<PublicOrganizationUser> => {
887
+ try {
888
+ Validator.validate(updateUserAccessSchema, data);
889
+ const { access } = await this.getUserAndAccess(env, userUUID);
890
+
891
+ if (access.role === UserOrganizationRole.Owner) {
892
+ throw new APIError({
893
+ code: StatusCodes.BAD_REQUEST,
894
+ detail: `Owner's access can't be edited.`,
895
+ });
896
+ }
897
+
898
+ const updated = await userOrganizationController.update(access.id, data);
899
+ return (await this.parseOrganizationUsers(updated))[0];
900
+ } catch (e) { throw e; }
901
+ };
902
+
903
+ public readonly deleteUser = async (env: Environment, userUUID: string): Promise<void> => {
904
+ try {
905
+ const { user, access } = await this.getUserAndAccess(env, userUUID);
906
+
907
+ if (access.role === UserOrganizationRole.Owner) {
908
+ throw new APIError({
909
+ code: StatusCodes.BAD_REQUEST,
910
+ detail: `Owner's access can't be deleted.`,
911
+ });
912
+ }
913
+
914
+ const { deleted } = await userOrganizationController.delete(access.id);
915
+ if (!deleted) {
916
+ throw new APIError({
917
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
918
+ detail: `Could not delete user access to organization.`,
919
+ });
920
+ }
921
+
922
+ await authority.register(env.organization.uuid, {
923
+ name: LicenseLimitNames.Seats,
924
+ count: -1,
925
+ });
926
+
927
+ await authority.notify<MemberRemoved>(env.organization.uuid, {
928
+ event: NotificationEventNames.MemberRemoved,
929
+ data: {
930
+ user: user,
931
+ organization: env.organization,
932
+ cta: {
933
+ members: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_MEMBERS`),
934
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
935
+ },
936
+ },
937
+ });
938
+ } catch (e) { throw e; }
939
+ };
940
+
941
+ public readonly ownership = async (env: Environment, data: { user: string }): Promise<void> => {
942
+ try {
943
+ Validator.validate(changeOwnershipSchema, data);
944
+
945
+ const { access } = await this.getUserAndAccess(env, data.user);
946
+
947
+ if (access.invite) {
948
+ throw new APIError({
949
+ code: StatusCodes.BAD_REQUEST,
950
+ detail: `User has a pending invite and can't be owner.`,
951
+ });
952
+ }
953
+
954
+ if (access.role !== UserOrganizationRole.Owner) {
955
+ await userOrganizationController.update(env.access.id, { role: UserOrganizationRole.Admin });
956
+ await userOrganizationController.update(access.id, { role: UserOrganizationRole.Owner });
957
+ }
958
+ } catch (e) { throw e; }
959
+ };
960
+
961
+ public readonly parse = async (env: Environment, records: Organization[]): Promise<PublicOrganization[]> => {
962
+ try {
963
+ const response: PublicOrganization[] = [];
964
+
965
+ for (const record of records) {
966
+ const p: PublicOrganization = {
967
+ uuid: record.uuid,
968
+ properties: record.properties,
969
+ preferences: record.preferences,
970
+ billing: record.billing,
971
+ created_date: new Date2(record.created_date).format(Formats.ISO),
972
+ modified_date: new Date2(record.modified_date).format(Formats.ISO),
973
+ status: record.status,
974
+ metadata: record.metadata,
975
+ };
976
+
977
+ if (!env || env.access.role === UserOrganizationRole.Staff) {
978
+ delete p.billing;
979
+ }
980
+
981
+ response.push(p);
982
+ }
983
+ return response;
984
+ } catch (e) { throw e; }
985
+ };
986
+
987
+ public readonly parseOrganizationUsers = async (records: UserOrganization[], options: { userCache?: User[], preferences?: boolean } = {}): Promise<PublicOrganizationUser[]> => {
988
+ try {
989
+ const missingUserIDs = _.compact(
990
+ _.uniq(records
991
+ .filter((access) => !(options.userCache || []).some((u) => u.id === access.user || u.id === access.invited_by))
992
+ .map((r) => [r.user, r.invited_by])
993
+ .flat()));
994
+ const users = [
995
+ ...options.userCache || [],
996
+ ...missingUserIDs.length ? await userController.list({ id: { in: missingUserIDs } }, { password: false, timestamp: true }) : [],
997
+ ];
998
+
999
+ const response: PublicOrganizationUser[] = [];
1000
+ for (const record of records) {
1001
+ const accessUser = users.find((u) => u.id === record.user);
1002
+ if (!accessUser) { continue; }
1003
+
1004
+ response.push({
1005
+ user: (await publicUserController.parseUser([accessUser]))[0],
1006
+ access: (await publicUserController.parseAccess([record], {
1007
+ userCache: users,
1008
+ preferences: options.preferences,
1009
+ }))[0],
1010
+ });
1011
+ }
1012
+
1013
+ return response;
1014
+ } catch (e) { throw e; }
1015
+ };
1016
+
1017
+ public readonly parseLicenses = async (organization: Organization, records: OrganizationLicense[]): Promise<PublicOrganizationLicense[]> => {
1018
+ try {
1019
+ let cycle: BillingCycle;
1020
+ try {
1021
+ const context = await billingController.cycleContext(organization);
1022
+ cycle = context.cycle;
1023
+ } catch { }
1024
+
1025
+ const [services, limits, associatedLicenses] = await Promise.all([
1026
+ adminServiceController.list({ id: { in: records.map((r) => r.service) } }),
1027
+ organizationLicenseLimitController.list({ license: { in: records.map((l) => l.id) } }),
1028
+ organizationLicenseController.list({ id: { in: _.compact(records.map((r) => r.associated)) } }),
1029
+ ]);
1030
+
1031
+ const response: PublicOrganizationLicense[] = [];
1032
+ for (const record of records) {
1033
+ const service = services.find((s) => s.id === record.service);
1034
+ if (!service) { continue; }
1035
+
1036
+ const licenseLimits = limits.filter((l) => l.license === record.id);
1037
+
1038
+ response.push({
1039
+ uuid: record.uuid,
1040
+ service: (await publicServiceController.parseServices([service]))[0],
1041
+ type: record.type,
1042
+ associated: record.associated ?
1043
+ associatedLicenses.find((l) => l.id === record.associated)?.uuid || null
1044
+ : null,
1045
+ billing: record.billing,
1046
+ properties: record.properties,
1047
+ data: record.data,
1048
+ annual_price: record.annual_price,
1049
+ monthly_price: record.monthly_price,
1050
+ limits: licenseLimits.map((ll) => ({
1051
+ name: ll.name,
1052
+ limit: ll.limit,
1053
+ period: ll.period,
1054
+ monthly_price_abs: ll.monthly_price_abs,
1055
+ annual_price_abs: ll.annual_price_abs,
1056
+ overconsume: ll.overconsume,
1057
+ overconsume_monthly_price_per_unit: ll.overconsume_monthly_price_per_unit,
1058
+ overconsume_annual_price_per_unit: ll.overconsume_annual_price_per_unit,
1059
+ metadata: ll.metadata,
1060
+ })),
1061
+ start_date: record.start_date,
1062
+ ...cycle ? {
1063
+ current_billing_start_date: cycle.start_date,
1064
+ current_billing_end_date: cycle.end_date,
1065
+ current_billing_trial: cycle.trial,
1066
+ } : {
1067
+ current_billing_start_date: null,
1068
+ current_billing_end_date: null,
1069
+ current_billing_trial: false,
1070
+ },
1071
+ end_date: record.end_date,
1072
+ enabled: record.enabled,
1073
+ metadata: record.metadata,
1074
+ });
1075
+ }
1076
+ return response;
1077
+ } catch (e) { throw e; }
1078
+ };
1079
+
1080
+ public readonly parsePaymentMethods = async (env: Environment, records: PaymentMethod[]): Promise<PublicPaymentMethod[]> => {
1081
+ try {
1082
+ const response: PublicPaymentMethod[] = [];
1083
+
1084
+ for (const record of records) {
1085
+ const p: PublicPaymentMethod = {
1086
+ uuid: record.uuid,
1087
+ type: record.type,
1088
+ properties: {
1089
+ name: record.properties.name,
1090
+ alias: record.properties.alias,
1091
+ brand: record.properties.brand,
1092
+ country: record.properties.country,
1093
+ exp_year: record.properties.exp_year,
1094
+ exp_month: record.properties.exp_month,
1095
+ },
1096
+ created_date: new Date2(record.created_date).format(Formats.ISO),
1097
+ modified_date: new Date2(record.modified_date).format(Formats.ISO),
1098
+ enabled: record.enabled,
1099
+ metadata: record.metadata,
1100
+ };
1101
+ response.push(p);
1102
+ }
1103
+ return response;
1104
+ } catch (e) { throw e; }
1105
+ };
1106
+
1107
+ public readonly parseInvoices = async (env: Environment, records: Invoice[]): Promise<PublicInvoice[]> => {
1108
+ try {
1109
+ const response: PublicInvoice[] = [];
1110
+
1111
+ const methods = await paymentMethodController.list({
1112
+ id: { in: _.uniq(_.compact(records.map((r) => r.method))) },
1113
+ });
1114
+
1115
+ for (const record of records) {
1116
+ const method = methods.find((m) => m.id === record.method);
1117
+
1118
+ const p: PublicInvoice = {
1119
+ uuid: record.uuid,
1120
+ date: new Date2(record.date).format(Formats.ISO),
1121
+ base: record.base,
1122
+ tax_percentage: record.tax_percentage,
1123
+ tax: record.tax,
1124
+ amount: record.amount,
1125
+ concepts: _.orderBy(record.concepts, (c) => c.price, `desc`),
1126
+ billing: record.billing,
1127
+ method: method ? (await this.parsePaymentMethods(env, [method]))[0] : null,
1128
+ invoice: !!record.invoice,
1129
+ number: record.number || null,
1130
+ status: record.status,
1131
+ metadata: record.metadata,
1132
+ };
1133
+ response.push(p);
1134
+ }
1135
+ return response;
1136
+ } catch (e) { throw e; }
1137
+ };
1138
+
1139
+ // license helpers -----------------------------------------------------------
1140
+
1141
+ private readonly getLicenses = async (env: Environment, serviceUUID: string, params: Where<OrganizationLicense>): Promise<PublicOrganizationLicense[]> => {
1142
+ try {
1143
+ const service = (await adminServiceController.list({ uuid: serviceUUID }))[0];
1144
+ if (!service) {
1145
+ throw new APIError({
1146
+ code: StatusCodes.BAD_REQUEST,
1147
+ detail: `No service ${serviceUUID} found.`,
1148
+ });
1149
+ }
1150
+
1151
+ const serviceLicenses = await organizationLicenseController.list({
1152
+ ...params,
1153
+ organization: env.organization.id,
1154
+ service: service.id,
1155
+ });
1156
+
1157
+ return await this.parseLicenses(env.organization, serviceLicenses);
1158
+ } catch (e) { throw e; }
1159
+ };
1160
+
1161
+ private readonly licenseActivationContext = async (env: Environment, serviceUUID: string, data: { license: string, billing: OrganizationLicense[`billing`], type: OrganizationLicense[`type`] }) => {
1162
+ try {
1163
+ const service = (await adminServiceController.list({ uuid: serviceUUID }))[0];
1164
+ if (!service) {
1165
+ throw new APIError({
1166
+ code: StatusCodes.BAD_REQUEST,
1167
+ detail: `No service ${serviceUUID} found.`,
1168
+ });
1169
+ }
1170
+
1171
+ // the base license of the quota service can't be activated/edited
1172
+ if (service.uuid === authority.configuration.uuid
1173
+ && data.type === LicenseType.Base) {
1174
+ throw new APIError({
1175
+ code: StatusCodes.BAD_REQUEST,
1176
+ detail: `Invalid license activation request.`,
1177
+ });
1178
+ }
1179
+
1180
+ const license = (await licenseController.list({
1181
+ uuid: data.license,
1182
+ service: service.id,
1183
+ type: data.type,
1184
+ enabled: true,
1185
+ }))[0];
1186
+ if (!license || license.type !== data.type) {
1187
+ throw new APIError({
1188
+ code: StatusCodes.NOT_FOUND,
1189
+ detail: `No license ${data.license} found.`,
1190
+ });
1191
+ }
1192
+
1193
+ const base = await billingController.getBaseLicense(env.organization);
1194
+ let cycle: BillingCycle;
1195
+
1196
+ // if organization billing yet not configured
1197
+ if (base.billing === `lifetime`) {
1198
+ // set organization billing cycle
1199
+ await organizationLicenseController.update(base.id, { billing: data.billing });
1200
+
1201
+ // start 14 day trial
1202
+ const now = new Date2().toMoment().startOf(`day`);
1203
+ const trial = 14;
1204
+
1205
+ cycle = (await billingCycleController.add({
1206
+ organization: env.organization.id,
1207
+ start_date: now.format(Formats.ISO),
1208
+ end_date: now.add(trial, `days`).format(Formats.ISO),
1209
+ trial: true,
1210
+ metadata: {},
1211
+ internal: {},
1212
+ }))[0];
1213
+ } else if (base.billing !== data.billing) {
1214
+ throw new APIError({
1215
+ code: StatusCodes.BAD_REQUEST,
1216
+ detail: `All active licenses must be billed on the same billing cycle: ${base.billing}.`,
1217
+ });
1218
+ } else {
1219
+ // get organization cycle
1220
+ const context = await billingController.cycleContext(env.organization);
1221
+ cycle = context.cycle;
1222
+ }
1223
+
1224
+ // get all licenses for service
1225
+ const allServiceLicenses = (await organizationLicenseController.list({
1226
+ service: service.id,
1227
+ type: LicenseType.Base,
1228
+ organization: env.organization.id,
1229
+ }));
1230
+
1231
+ // filter service bases: it can only have 2 bases, one enabled and one future
1232
+ const currentAndFutureLicenses = allServiceLicenses.filter((l) => (!l.end_date || new Date2(l.end_date).isFuture()));
1233
+ if (currentAndFutureLicenses.length > 2) {
1234
+ throw new APIError({
1235
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
1236
+ detail: `Organization has an invalid license configuration.`,
1237
+ });
1238
+ }
1239
+
1240
+ const [enabled, future] = [currentAndFutureLicenses.find((eb) => eb.enabled), currentAndFutureLicenses.find((eb) => !eb.enabled)];
1241
+
1242
+ // power-ups require a base license
1243
+ if (!enabled && data.type === LicenseType.Variant) {
1244
+ throw new APIError({
1245
+ code: StatusCodes.BAD_REQUEST,
1246
+ detail: `Organization has no active license for this service. Activate a base license before activating power-ups.`,
1247
+ });
1248
+ }
1249
+
1250
+ return {
1251
+ cycle: cycle,
1252
+ service: service,
1253
+ license: license,
1254
+ setup: license.type === LicenseType.Base && !allServiceLicenses.some((l) => l.service === service.id && l.end_date && new Date2(l.end_date).isPast()),
1255
+ enabled: enabled,
1256
+ future: future,
1257
+ };
1258
+ } catch (e) { throw e; }
1259
+ };
1260
+
1261
+ private readonly configureLicense = async (organization: Organization, license: License, data: {
1262
+ associated?: OrganizationLicense,
1263
+ billing: OrganizationLicense[`billing`],
1264
+ start_date: Date2,
1265
+ end_date?: Date2,
1266
+ metadata?: OrganizationLicense[`metadata`],
1267
+ internal: OrganizationLicense[`internal`],
1268
+ }): Promise<OrganizationLicense> => {
1269
+ try {
1270
+ let associatedOrganizationLicense: OrganizationLicense;
1271
+ if (license.associated) {
1272
+ associatedOrganizationLicense = (await organizationLicenseController.list({
1273
+ organization: organization.id,
1274
+ original: license.associated,
1275
+ ...data.associated ? { id: data.associated.id } : { enabled: true },
1276
+ }))[0];
1277
+ }
1278
+
1279
+ const organizationLicense = (await organizationLicenseController.add({
1280
+ organization: organization.id,
1281
+ service: license.service,
1282
+ type: license.type,
1283
+ associated: associatedOrganizationLicense?.id,
1284
+ original: license.id,
1285
+ billing: data.billing,
1286
+ properties: license.properties,
1287
+ data: license.data,
1288
+ start_date: data.start_date.format(Formats.ISO),
1289
+ end_date: data.end_date ? data.end_date.format(Formats.ISO) : null,
1290
+ metadata: data.metadata,
1291
+ internal: data.internal,
1292
+ }))[0];
1293
+
1294
+ const licenseLimits = await licenseLimitController.list({ license: license.id });
1295
+ for (const ll of licenseLimits) {
1296
+ await organizationLicenseLimitController.add({
1297
+ organization: organization.id,
1298
+ license: organizationLicense.id,
1299
+ name: ll.name,
1300
+ limit: ll.limit,
1301
+ period: ll.period,
1302
+ monthly_price_abs: ll.monthly_price_abs,
1303
+ annual_price_abs: ll.annual_price_abs,
1304
+ overconsume: false, // default to false
1305
+ overconsume_monthly_price_per_unit: ll.overconsume_monthly_price_per_unit,
1306
+ overconsume_annual_price_per_unit: ll.overconsume_annual_price_per_unit,
1307
+ metadata: ll.metadata,
1308
+ });
1309
+ }
1310
+
1311
+ // configure also associated licenses
1312
+ const associatedLicenses = await licenseController.list({
1313
+ associated: license.id,
1314
+ enabled: true,
1315
+ });
1316
+
1317
+ for (const associated of associatedLicenses) {
1318
+ await this.configureLicense(organization, associated, {
1319
+ associated: organizationLicense,
1320
+ billing: data.billing,
1321
+ start_date: data.start_date,
1322
+ end_date: data.end_date,
1323
+ metadata: {},
1324
+ internal: {},
1325
+ });
1326
+ }
1327
+
1328
+ return organizationLicense;
1329
+ } catch (e) { throw e; }
1330
+ };
1331
+
1332
+ private readonly cancelLicense = async (env: Environment, serviceUUID: string, licenseUUID: string, type: OrganizationLicense[`type`]): Promise<PublicOrganizationLicense | void> => {
1333
+ try {
1334
+ const service = (await adminServiceController.list({ uuid: serviceUUID }))[0];
1335
+ if (!service) {
1336
+ throw new APIError({
1337
+ code: StatusCodes.BAD_REQUEST,
1338
+ detail: `No service ${serviceUUID} found.`,
1339
+ });
1340
+ }
1341
+
1342
+ if (service.uuid === authority.configuration.uuid
1343
+ && type === LicenseType.Base) {
1344
+ throw new APIError({
1345
+ code: StatusCodes.BAD_REQUEST,
1346
+ detail: `Base license can't be cancelled.`,
1347
+ });
1348
+ }
1349
+
1350
+ let license = (await organizationLicenseController.list({
1351
+ uuid: licenseUUID,
1352
+ organization: env.organization.id,
1353
+ service: service.id,
1354
+ type: type,
1355
+ end_date: [
1356
+ { is: null },
1357
+ { gt: Date2.current().format(Formats.ISO) },
1358
+ ],
1359
+ }))[0];
1360
+ if (!license || license.type !== type) {
1361
+ throw new APIError({
1362
+ code: StatusCodes.NOT_FOUND,
1363
+ detail: `No license ${licenseUUID} found.`,
1364
+ });
1365
+ }
1366
+
1367
+ if (license.associated) {
1368
+ throw new APIError({
1369
+ code: StatusCodes.BAD_REQUEST,
1370
+ detail: `Associated license can't be cancelled independently. Please, cancel the main license.`,
1371
+ });
1372
+ }
1373
+
1374
+ // delete future licenses
1375
+ if (new Date2(license.start_date).isFuture()) {
1376
+ await organizationLicenseController.delete(license.id);
1377
+ return;
1378
+ }
1379
+
1380
+ if (!license.enabled) {
1381
+ throw new APIError({
1382
+ code: StatusCodes.BAD_REQUEST,
1383
+ detail: `License is already cancelled.`,
1384
+ });
1385
+ }
1386
+
1387
+ const { cycle } = await billingController.cycleContext(env.organization);
1388
+
1389
+ license = (await organizationLicenseController.update(license.id, {
1390
+ end_date: cycle.end_date,
1391
+ }))[0];
1392
+
1393
+ // notify power-up removal
1394
+ if (type === LicenseType.Variant) {
1395
+ await authority.notify<PowerUpRemoved>(env.organization.uuid, {
1396
+ event: NotificationEventNames.PowerUpRemoved,
1397
+ data: {
1398
+ cta: {
1399
+ dashboard: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_DASHBOARD`),
1400
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
1401
+ },
1402
+ organization: env.organization,
1403
+ },
1404
+ });
1405
+ }
1406
+
1407
+ return (await this.parseLicenses(env.organization, [license]))[0];
1408
+ } catch (e) { throw e; }
1409
+ };
1410
+
1411
+ // user helpers --------------------------------------------------------
1412
+ private readonly getUserAndAccess = async (env: Environment, userUUID: string): Promise<{ user: User, access: UserOrganization }> => {
1413
+ try {
1414
+ const user = (await userController.list({ uuid: userUUID, status: { is: UserStatus.Blocked, inverse: true } }, { password: true }))[0];
1415
+ if (!user) {
1416
+ throw new APIError({
1417
+ code: StatusCodes.NOT_FOUND,
1418
+ detail: `No active user found.`,
1419
+ });
1420
+ }
1421
+
1422
+ const access = (await userOrganizationController.list({
1423
+ organization: env.organization.id,
1424
+ user: user.id,
1425
+ }))[0];
1426
+ if (!access) {
1427
+ throw new APIError({
1428
+ code: StatusCodes.BAD_REQUEST,
1429
+ detail: `User is not on organization.`,
1430
+ });
1431
+ }
1432
+
1433
+ return { user: user, access: access };
1434
+ } catch (e) { throw e; }
1435
+ };
1436
+
1437
+ // payments --------------------------------------------------------
1438
+ public readonly setupPaymentMethod = async (env: Environment): Promise<Stripe.SetupIntent> => {
1439
+ try {
1440
+ // don't use env organization -> may be cached
1441
+ const organization = (await organizationController.get(env.organization.id))[0];
1442
+
1443
+ let customer = organization.internal.customer;
1444
+ if (!customer) {
1445
+ // setup customer
1446
+ customer = (await this.configureStripeCustomer(organization)).id;
1447
+ }
1448
+
1449
+ const response = await stripe.setupIntents.create({
1450
+ customer: customer,
1451
+ usage: `off_session`,
1452
+ });
1453
+ return response;
1454
+ } catch (e) { throw e; }
1455
+ };
1456
+
1457
+ public readonly getPaymentMethod = async (env: Environment, uuid: string): Promise<PublicPaymentMethod[]> => {
1458
+ try {
1459
+ const methods = await paymentMethodController.list({ organization: env.organization.id, uuid: uuid });
1460
+ return await this.parsePaymentMethods(env, methods);
1461
+ } catch (e) { throw e; }
1462
+ };
1463
+
1464
+ public readonly listPaymentMethods = async (env: Environment): Promise<PublicPaymentMethod[]> => {
1465
+ try {
1466
+ const methods = await paymentMethodController.list({ organization: env.organization.id });
1467
+ return await this.parsePaymentMethods(env, methods);
1468
+ } catch (e) { throw e; }
1469
+ };
1470
+
1471
+ public readonly addPaymentMethod = async (env: Environment, data: AddPaymentMethod): Promise<PublicPaymentMethod> => {
1472
+ try {
1473
+ Validator.validate(addPaymentMethodSchema, data);
1474
+
1475
+ // don't use env organization -> may be cached
1476
+ const organization = (await organizationController.get(env.organization.id))[0];
1477
+
1478
+ const method = await stripe.paymentMethods.retrieve(data.payment_method_id);
1479
+ if (!method) {
1480
+ throw new APIError({
1481
+ code: StatusCodes.NOT_FOUND,
1482
+ detail: `Payment method not found.`,
1483
+ });
1484
+ }
1485
+ if (method.customer !== organization.internal.customer) {
1486
+ throw new APIError({
1487
+ code: StatusCodes.BAD_REQUEST,
1488
+ detail: `Payment method does not belong to the organization.`,
1489
+ });
1490
+ }
1491
+ if (method.type !== `card`) {
1492
+ throw new APIError({
1493
+ code: StatusCodes.BAD_REQUEST,
1494
+ detail: `Unsupported payment method type: ${method.type}.`,
1495
+ });
1496
+ }
1497
+ if (!method.card) {
1498
+ throw new APIError({
1499
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
1500
+ detail: `Payment method has no card information.`,
1501
+ });
1502
+ }
1503
+
1504
+ // disable previously enabled payment method
1505
+ if (data.enabled) {
1506
+ const enabled = (await paymentMethodController.list({
1507
+ organization: organization.id,
1508
+ enabled: true,
1509
+ }))[0];
1510
+ if (enabled) { await paymentMethodController.update(enabled.id, { enabled: false }); }
1511
+ }
1512
+
1513
+ const created = await paymentMethodController.add({
1514
+ organization: organization.id,
1515
+ type: PaymentMethodType.Card,
1516
+ properties: {
1517
+ alias: data.properties?.alias,
1518
+ name: method.card.last4,
1519
+ brand: method.card.brand,
1520
+ exp_month: method.card.exp_month,
1521
+ exp_year: method.card.exp_year,
1522
+ country: method.card.country,
1523
+ },
1524
+ enabled: data.enabled,
1525
+ metadata: data.metadata,
1526
+ internal: {
1527
+ payment_method_id: method.id,
1528
+ fingerprint: method.card.fingerprint,
1529
+ },
1530
+ });
1531
+
1532
+ return (await this.parsePaymentMethods(env, created))[0];
1533
+ } catch (e) { throw e; }
1534
+ };
1535
+
1536
+ public readonly updatePaymentMethod = async (env: Environment, uuid: string, data: UpdatePaymentMethod): Promise<PublicPaymentMethod> => {
1537
+ try {
1538
+ Validator.validate(updatePaymentMethodSchema, data);
1539
+
1540
+ const old = (await paymentMethodController.list({ organization: env.organization.id, uuid: uuid }))[0];
1541
+ if (!old) {
1542
+ throw new APIError({
1543
+ code: StatusCodes.NOT_FOUND,
1544
+ detail: `Payment method not found.`,
1545
+ });
1546
+ }
1547
+
1548
+ // disable previously enabled payment method
1549
+ if (!old.enabled && data.enabled) {
1550
+ const enabled = (await paymentMethodController.list({
1551
+ organization: env.organization.id,
1552
+ enabled: true,
1553
+ }))[0];
1554
+ if (enabled) { await paymentMethodController.update(enabled.id, { enabled: false }); }
1555
+ }
1556
+
1557
+ const updated = await paymentMethodController.update(old.id, data);
1558
+
1559
+ await authority.notify<BillingChanged>(env.organization.uuid, {
1560
+ event: NotificationEventNames.BillingChanged,
1561
+ data: {
1562
+ organization: env.organization,
1563
+ cta: {
1564
+ billing: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_BILLING`),
1565
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
1566
+ },
1567
+ },
1568
+ });
1569
+
1570
+ return (await this.parsePaymentMethods(env, updated))[0];
1571
+ } catch (e) { throw e; }
1572
+ };
1573
+
1574
+ public readonly deletePaymentMethod = async (env: Environment, uuid: string): Promise<void> => {
1575
+ try {
1576
+ const old = (await paymentMethodController.list({ organization: env.organization.id, uuid: uuid }))[0];
1577
+ if (!old) {
1578
+ throw new APIError({
1579
+ code: StatusCodes.NOT_FOUND,
1580
+ detail: `Payment method not found.`,
1581
+ });
1582
+ }
1583
+
1584
+ const { deleted } = await paymentMethodController.delete(old.id);
1585
+ if (!deleted) {
1586
+ throw new APIError({
1587
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
1588
+ detail: `Could not delete payment method.`,
1589
+ });
1590
+ }
1591
+ } catch (e) { throw e; }
1592
+ };
1593
+
1594
+ public readonly listInvoices = async (env: Environment): Promise<PublicInvoice[]> => {
1595
+ try {
1596
+ const invoices = await invoiceController.list({ organization: env.organization.id }, { order: { by: `date`, direction: `descendent` } });
1597
+ return await this.parseInvoices(env, invoices);
1598
+ } catch (e) { throw e; }
1599
+ };
1600
+
1601
+ public readonly getInvoicePDF = async (env: Environment, uuid: string): Promise<{
1602
+ name: string,
1603
+ data: string,
1604
+ }> => {
1605
+ try {
1606
+ const invoice = (await invoiceController.list({
1607
+ organization: env.organization.id,
1608
+ uuid: uuid,
1609
+ }, { order: { by: `date`, direction: `descendent` } }))[0];
1610
+
1611
+ if (!invoice) {
1612
+ throw new APIError({
1613
+ code: StatusCodes.NOT_FOUND,
1614
+ detail: `Invoice ${uuid} not found.`,
1615
+ });
1616
+ }
1617
+ if (!invoice.invoice) {
1618
+ throw new APIError({
1619
+ code: StatusCodes.BAD_REQUEST,
1620
+ detail: `Invoice ${uuid} does not have a PDF available.`,
1621
+ });
1622
+ }
1623
+
1624
+ // get invoice from holded
1625
+ const [holdedInvoice, { data: pdf }] = await Promise.all([
1626
+ holded.getInvoice(invoice.invoice),
1627
+ holded.getInvoicePDF(invoice.invoice),
1628
+ ]);
1629
+
1630
+ if (!holdedInvoice.docNumber) {
1631
+ throw new APIError({
1632
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
1633
+ detail: `Invoice ${uuid} does not have a document number.`,
1634
+ });
1635
+ }
1636
+
1637
+ return {
1638
+ name: `${holdedInvoice.docNumber}.pdf`,
1639
+ data: pdf,
1640
+ };
1641
+ } catch (e) { throw e; }
1642
+ };
1643
+
1644
+ public readonly configureHoldedContact = async (organization: Organization): Promise<Holded.Contact> => {
1645
+ try {
1646
+ if (!organization.billing.document) {
1647
+ throw new APIError({
1648
+ code: StatusCodes.BAD_REQUEST,
1649
+ detail: `Configure your billing information first.`,
1650
+ });
1651
+ }
1652
+
1653
+ let contact = (await holded.listContacts({ customId: organization.uuid }))[0];
1654
+
1655
+ // TODO: change this to country code when upgrading to holded v2
1656
+ const country = getCountry(organization.billing.address.country) || getName(organization.billing.address.country) || organization.billing.address.country;
1657
+
1658
+ if (!contact) {
1659
+ // create client record
1660
+ const account = await holded.createAccount({
1661
+ prefix: 4300,
1662
+ name: organization.billing.name,
1663
+ });
1664
+
1665
+ // create contact
1666
+ contact = await holded.createContact({
1667
+ customId: organization.uuid,
1668
+ name: organization.billing.name,
1669
+ code: organization.billing.document,
1670
+ email: organization.billing.email,
1671
+ phone: organization.billing.phone,
1672
+ type: `client`,
1673
+ isperson: organization.billing.individual,
1674
+ clientRecord: account.accountNum,
1675
+ billAddress: {
1676
+ address: organization.billing.address.address,
1677
+ city: organization.billing.address.city,
1678
+ postalCode: organization.billing.address.zip,
1679
+ province: organization.billing.address.province,
1680
+ country: country,
1681
+ },
1682
+ });
1683
+ } else {
1684
+ let clientRecord: number;
1685
+ if (!contact.clientRecord) {
1686
+ // create client record
1687
+ const account = await holded.createAccount({
1688
+ prefix: 4300,
1689
+ name: organization.billing.name,
1690
+ });
1691
+ clientRecord = account.accountNum;
1692
+ }
1693
+
1694
+ // update contact
1695
+ contact = await holded.updateContacts(contact.id, {
1696
+ name: organization.billing.name,
1697
+ code: organization.billing.document,
1698
+ email: organization.billing.email,
1699
+ phone: organization.billing.phone,
1700
+ type: `client`,
1701
+ isperson: organization.billing.individual,
1702
+ clientRecord: clientRecord,
1703
+ billAddress: {
1704
+ address: organization.billing.address.address,
1705
+ city: organization.billing.address.city,
1706
+ postalCode: organization.billing.address.zip,
1707
+ province: organization.billing.address.province,
1708
+ country: country,
1709
+ },
1710
+ });
1711
+ }
1712
+
1713
+ await organizationController.update(organization.id, {
1714
+ internal: { contact: contact.id },
1715
+ });
1716
+
1717
+ return contact;
1718
+ } catch (e) { throw e; }
1719
+ };
1720
+
1721
+ public readonly configureStripeCustomer = async (organization: Organization): Promise<Stripe.Customer> => {
1722
+ try {
1723
+ if (!organization.billing.document) {
1724
+ throw new APIError({
1725
+ code: StatusCodes.BAD_REQUEST,
1726
+ detail: `Configure your billing information first.`,
1727
+ });
1728
+ }
1729
+
1730
+ let { data: [customer] } = await stripe.customers.search({
1731
+ query: `metadata['uuid']:'${organization.uuid}'`,
1732
+ limit: 1,
1733
+ });
1734
+
1735
+ if (!customer) {
1736
+ customer = await stripe.customers.create({
1737
+ name: organization.billing.name,
1738
+ ...(organization.billing.individual ?? true) ? {
1739
+ individual_name: organization.billing.name,
1740
+ business_name: null,
1741
+ } : {
1742
+ individual_name: null,
1743
+ business_name: organization.billing.name,
1744
+ },
1745
+ email: organization.billing.email,
1746
+ phone: organization.billing.phone,
1747
+ address: {
1748
+ line1: organization.billing.address.address,
1749
+ city: organization.billing.address.city,
1750
+ postal_code: organization.billing.address.zip,
1751
+ state: organization.billing.address.province,
1752
+ country: organization.billing.address.country,
1753
+ },
1754
+ metadata: {
1755
+ uuid: organization.uuid,
1756
+ document: organization.billing.document,
1757
+ },
1758
+ });
1759
+ } else {
1760
+ // update information
1761
+ customer = await stripe.customers.update(customer.id, {
1762
+ name: organization.billing.name,
1763
+ ...(organization.billing.individual ?? true) ? {
1764
+ individual_name: organization.billing.name,
1765
+ business_name: null,
1766
+ } : {
1767
+ individual_name: null,
1768
+ business_name: organization.billing.name,
1769
+ },
1770
+ email: organization.billing.email,
1771
+ phone: organization.billing.phone,
1772
+ address: {
1773
+ line1: organization.billing.address.address,
1774
+ city: organization.billing.address.city,
1775
+ postal_code: organization.billing.address.zip,
1776
+ state: organization.billing.address.province,
1777
+ country: organization.billing.address.country,
1778
+ },
1779
+ metadata: {
1780
+ uuid: organization.uuid,
1781
+ document: organization.billing.document,
1782
+ },
1783
+ });
1784
+ }
1785
+
1786
+ await organizationController.update(organization.id, {
1787
+ internal: { customer: customer.id },
1788
+ });
1789
+
1790
+ return customer;
1791
+ } catch (e) { throw e; }
1792
+ };
1793
+ }
1794
+
1795
+ const publicOrganizationController = new PublicOrganizationCtrl();
1796
+ export default publicOrganizationController;