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.
- package/.claude/settings.local.json +11 -0
- package/.dockerignore +11 -0
- package/.github/workflows/_deploy.yml +191 -0
- package/.github/workflows/deploy-dev.yml +30 -0
- package/.github/workflows/deploy-prod.yml +34 -0
- package/CLAUDE.md +114 -0
- package/Dockerfile +61 -0
- package/Dockerfile.dev +24 -0
- package/README.md +1 -0
- package/package.json +61 -0
- package/src/App.ts +40 -0
- package/src/helpers/api-error.ts +21 -0
- package/src/helpers/authority.ts +216 -0
- package/src/helpers/config.ts +4 -0
- package/src/helpers/holded.ts +105 -0
- package/src/helpers/mysql.ts +15 -0
- package/src/helpers/notifications.ts +55 -0
- package/src/helpers/random.ts +18 -0
- package/src/helpers/s3.ts +43 -0
- package/src/helpers/stripe.ts +8 -0
- package/src/index.ts +47 -0
- package/src/v1/common-types.ts +12 -0
- package/src/v1/controllers/admin/accounting-seat-controller.ts +148 -0
- package/src/v1/controllers/admin/billing-cycle-controller.ts +171 -0
- package/src/v1/controllers/admin/invoice-controller.ts +222 -0
- package/src/v1/controllers/admin/license-controller.ts +190 -0
- package/src/v1/controllers/admin/license-limit-controller.ts +160 -0
- package/src/v1/controllers/admin/notification-controller.ts +115 -0
- package/src/v1/controllers/admin/organization-controller.ts +210 -0
- package/src/v1/controllers/admin/organization-license-controller.ts +224 -0
- package/src/v1/controllers/admin/organization-license-limit-controller.ts +196 -0
- package/src/v1/controllers/admin/organization-usage-controller.ts +167 -0
- package/src/v1/controllers/admin/payment-method-controller.ts +173 -0
- package/src/v1/controllers/admin/policy-controller.ts +138 -0
- package/src/v1/controllers/admin/policy-version-controller.ts +143 -0
- package/src/v1/controllers/admin/schedule-controller.ts +181 -0
- package/src/v1/controllers/admin/service-controller.ts +206 -0
- package/src/v1/controllers/admin/service-notification-controller.ts +166 -0
- package/src/v1/controllers/admin/user-controller.ts +282 -0
- package/src/v1/controllers/admin/user-organization-controller.ts +172 -0
- package/src/v1/controllers/admin/user-policy-controller.ts +164 -0
- package/src/v1/controllers/auth/@deprecated oauth-controller.ts +191 -0
- package/src/v1/controllers/auth/identity-token-controller.ts +286 -0
- package/src/v1/controllers/auth/organization-token-controller.ts +277 -0
- package/src/v1/controllers/auth/temp-code-controller.ts +60 -0
- package/src/v1/controllers/billing/billing-controller.ts +805 -0
- package/src/v1/controllers/organizations/public-organization-controller.ts +1796 -0
- package/src/v1/controllers/policies/public-policy-controller.ts +68 -0
- package/src/v1/controllers/services/public-service-controller.ts +177 -0
- package/src/v1/controllers/services/service-controller.ts +474 -0
- package/src/v1/controllers/users/public-user-controller.ts +471 -0
- package/src/v1/daos/@deprecated auth-dao.ts +173 -0
- package/src/v1/daos/accounting-seat-dao.ts +111 -0
- package/src/v1/daos/billing-cycle-dao.ts +107 -0
- package/src/v1/daos/invoice-dao.ts +131 -0
- package/src/v1/daos/license-dao.ts +152 -0
- package/src/v1/daos/license-limit-dao.ts +120 -0
- package/src/v1/daos/organization-dao.ts +109 -0
- package/src/v1/daos/organization-license-dao.ts +117 -0
- package/src/v1/daos/organization-license-limit-dao.ts +157 -0
- package/src/v1/daos/organization-usage-dao.ts +115 -0
- package/src/v1/daos/payment-method-dao.ts +110 -0
- package/src/v1/daos/policy-dao.ts +105 -0
- package/src/v1/daos/policy-version-dao.ts +112 -0
- package/src/v1/daos/redis-dao.ts +38 -0
- package/src/v1/daos/schema.sql +462 -0
- package/src/v1/daos/service-dao.ts +118 -0
- package/src/v1/daos/service-notification-dao.ts +120 -0
- package/src/v1/daos/user-dao.ts +127 -0
- package/src/v1/daos/user-organization-dao.ts +123 -0
- package/src/v1/daos/user-policy-dao.ts +109 -0
- package/src/v1/middlewares/auth/identity-token-middleware.ts +51 -0
- package/src/v1/middlewares/auth/organization-token-middleware.ts +73 -0
- package/src/v1/middlewares/auth/service-token-middleware.ts +167 -0
- package/src/v1/middlewares/authority-middleware.ts +97 -0
- package/src/v1/middlewares/organizations/organization-middleware.ts +94 -0
- package/src/v1/middlewares/static/static-middleware.ts +41 -0
- package/src/v1/middlewares/users/user-middleware.ts +41 -0
- package/src/v1/routes/admin/accounting-seat-router.ts +89 -0
- package/src/v1/routes/admin/admin-router.ts +61 -0
- package/src/v1/routes/admin/billing-cycle-router.ts +89 -0
- package/src/v1/routes/admin/invoice-router.ts +89 -0
- package/src/v1/routes/admin/license-limit-router.ts +89 -0
- package/src/v1/routes/admin/license-router.ts +89 -0
- package/src/v1/routes/admin/notification-router.ts +61 -0
- package/src/v1/routes/admin/organization-license-limit-router.ts +89 -0
- package/src/v1/routes/admin/organization-license-router.ts +89 -0
- package/src/v1/routes/admin/organization-router.ts +89 -0
- package/src/v1/routes/admin/payment-method-router.ts +89 -0
- package/src/v1/routes/admin/policy-router.ts +89 -0
- package/src/v1/routes/admin/policy-version-router.ts +89 -0
- package/src/v1/routes/admin/schedule-router.ts +75 -0
- package/src/v1/routes/admin/service-notification-router.ts +98 -0
- package/src/v1/routes/admin/service-router.ts +89 -0
- package/src/v1/routes/admin/user-organization-router.ts +89 -0
- package/src/v1/routes/admin/user-policy-router.ts +89 -0
- package/src/v1/routes/admin/user-router.ts +89 -0
- package/src/v1/routes/auth/@deprecated auth-router.ts +199 -0
- package/src/v1/routes/auth/identity-token-router.ts +147 -0
- package/src/v1/routes/auth/organization-token-router.ts +70 -0
- package/src/v1/routes/billing/billing-router.ts +227 -0
- package/src/v1/routes/organizations/invoices-router.ts +48 -0
- package/src/v1/routes/organizations/payments-router.ts +104 -0
- package/src/v1/routes/organizations/public-organization-router.ts +319 -0
- package/src/v1/routes/organizations/users-router.ts +139 -0
- package/src/v1/routes/policies/public-policy-router.ts +50 -0
- package/src/v1/routes/services/public-service-router.ts +89 -0
- package/src/v1/routes/services/service-router.ts +197 -0
- package/src/v1/routes/users/public-user-router.ts +230 -0
- package/src/v1/routes/v1-router.ts +50 -0
- package/src/v1/schemas/admin/accounting-seat-schemas.ts +22 -0
- package/src/v1/schemas/admin/billing-cycle-schemas.ts +20 -0
- package/src/v1/schemas/admin/invoice-schemas.ts +41 -0
- package/src/v1/schemas/admin/license-limit-schemas.ts +25 -0
- package/src/v1/schemas/admin/license-schemas.ts +30 -0
- package/src/v1/schemas/admin/organization-license-limit-schemas.ts +29 -0
- package/src/v1/schemas/admin/organization-license-schemas.ts +43 -0
- package/src/v1/schemas/admin/organization-schemas.ts +67 -0
- package/src/v1/schemas/admin/organization-usage-schemas.ts +15 -0
- package/src/v1/schemas/admin/payment-method-schemas.ts +40 -0
- package/src/v1/schemas/admin/policy-schemas.ts +31 -0
- package/src/v1/schemas/admin/service-notification-schemas.ts +23 -0
- package/src/v1/schemas/admin/service-schemas.ts +56 -0
- package/src/v1/schemas/admin/user-organization-schemas.ts +24 -0
- package/src/v1/schemas/admin/user-policy-schemas.ts +17 -0
- package/src/v1/schemas/admin/user-schemas.ts +52 -0
- package/src/v1/schemas/auth/auth-schemas.ts +55 -0
- package/src/v1/schemas/billing/billing-schemas.ts +32 -0
- package/src/v1/schemas/organizations/public-organization-schemas.ts +77 -0
- package/src/v1/schemas/payments/payment-method-schemas.ts +0 -0
- package/src/v1/schemas/services/public-service-schemas.ts +5 -0
- package/src/v1/schemas/services/service-schemas.ts +27 -0
- package/src/v1/schemas/users/public-user-schemas.ts +73 -0
- package/src/v1/types/admin/accounting-seat-types.ts +16 -0
- package/src/v1/types/admin/billing-cycle-types.ts +14 -0
- package/src/v1/types/admin/invoice-types.ts +39 -0
- package/src/v1/types/admin/license-limit-types.ts +16 -0
- package/src/v1/types/admin/license-types.ts +29 -0
- package/src/v1/types/admin/organization-license-limit-types.ts +13 -0
- package/src/v1/types/admin/organization-license-types.ts +21 -0
- package/src/v1/types/admin/organization-types.ts +60 -0
- package/src/v1/types/admin/organization-usage-types.ts +17 -0
- package/src/v1/types/admin/payment-method-types.ts +30 -0
- package/src/v1/types/admin/policy-types.ts +9 -0
- package/src/v1/types/admin/policy-version-types.ts +12 -0
- package/src/v1/types/admin/service-notification-types.ts +20 -0
- package/src/v1/types/admin/service-types.ts +36 -0
- package/src/v1/types/admin/user-organization-types.ts +31 -0
- package/src/v1/types/admin/user-types.ts +44 -0
- package/src/v1/types/auth/@deprecated auth-types.ts +26 -0
- package/src/v1/types/auth/common-types.ts +7 -0
- package/src/v1/types/auth/confirmation-types.ts +5 -0
- package/src/v1/types/auth/identity-token-types.ts +8 -0
- package/src/v1/types/auth/organization-token-types.ts +23 -0
- package/src/v1/types/auth/otp-types.ts +12 -0
- package/src/v1/types/billing/billing-types.ts +37 -0
- package/src/v1/types/billing/ue-types.ts +29 -0
- package/src/v1/types/organizations/public-organization-types.ts +76 -0
- package/src/v1/types/policies/public-policy-types.ts +7 -0
- package/src/v1/types/services/public-service-types.ts +20 -0
- package/src/v1/types/services/service-types.ts +43 -0
- package/src/v1/types/users/public-user-types.ts +15 -0
- package/src/v1/types/users/user-policy-types.ts +11 -0
- package/static/error.html +55 -0
- package/static/grant.html +117 -0
- package/static/login.html +54 -0
- package/static/oauth-client-logo.webp +0 -0
- 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;
|