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