@silexpert/core 2.0.123 → 2.0.124
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/dist/{index-DY6JXj66.d.mts → index-CL2xcnsB.d.mts} +28 -5
- package/dist/index-CL2xcnsB.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +6 -6
- package/dist/index.mjs.map +1 -1
- package/dist/{product-B35JCiip.mjs → product-dgz1Fsg6.mjs} +2 -2
- package/dist/product-dgz1Fsg6.mjs.map +1 -0
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.mts.map +1 -1
- package/dist/server/index.mjs +15 -15
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
- package/ts/api/resources/Mfa.ts +8 -4
- package/ts/types/Enum/PrestationType.ts +1 -1
- package/ts/types/Interface/api/authentification/AuthenticationResponse.ts +2 -0
- package/ts/types/Interface/api/authentification/ReadAuthenticateCollaborater.ts +8 -1
- package/ts/types/Interface/api/mfa/VerifyCode.ts +6 -1
- package/ts/types/Interface/api/mfa/VerifyRecoveryCode.ts +6 -1
- package/ts/types/Interface/models/IApiError.ts +4 -0
- package/ts/utils/commercialManagement/document.ts +2 -1
- package/ts/utils/commercialManagement/templates/body.ts +1 -1
- package/ts/utils/isHtml.ts +14 -0
- package/ts/utils/tests/document.spec.ts +39 -0
- package/dist/index-DY6JXj66.d.mts.map +0 -1
- package/dist/product-B35JCiip.mjs.map +0 -1
package/package.json
CHANGED
package/ts/api/resources/Mfa.ts
CHANGED
|
@@ -14,12 +14,16 @@ export class UserMfa extends Resource {
|
|
|
14
14
|
return this.axios.$post('/mfa/disable', { userId });
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
verifyCode(userId: number, code: string): Promise<{ success: boolean; token?: string }> {
|
|
18
|
-
return this.axios.$post('/mfa/verify/code', { userId, code });
|
|
17
|
+
verifyCode(userId: number, code: string, mfaChallengeToken?: string): Promise<{ success: boolean; token?: string }> {
|
|
18
|
+
return this.axios.$post('/mfa/verify/code', { userId, code, mfaChallengeToken });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
verifyRecoveryCode(
|
|
22
|
-
|
|
21
|
+
verifyRecoveryCode(
|
|
22
|
+
userId: number,
|
|
23
|
+
recoveryCode: string,
|
|
24
|
+
mfaChallengeToken?: string,
|
|
25
|
+
): Promise<{ success: boolean; token?: string }> {
|
|
26
|
+
return this.axios.$post('/mfa/verify/recovery-code', { userId, recoveryCode, mfaChallengeToken });
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
isMfaEnabled(userId?: number): Promise<{ isEnabled: boolean }> {
|
|
@@ -230,7 +230,6 @@ export const allPrestationsWithBrochure = [
|
|
|
230
230
|
PrestationType.COMPTA_POURCENTAGE_PREMIUM,
|
|
231
231
|
PrestationType.COMPTA_POURCENTAGE_EXPRESS,
|
|
232
232
|
PrestationType.COMPTA_POURCENTAGE_PREMIUM_EXPRESS,
|
|
233
|
-
PrestationType.BILAN_PREVISIONNEL,
|
|
234
233
|
PrestationType.EXCEPTIONNEL,
|
|
235
234
|
PrestationType.DECLARATION_IR,
|
|
236
235
|
];
|
|
@@ -276,6 +275,7 @@ export const allPrestations2025WithCGV = [
|
|
|
276
275
|
PrestationType.ATTESTATION_MASSE_SALARIALE,
|
|
277
276
|
PrestationType.ATTESTATION_1,
|
|
278
277
|
PrestationType.ATTESTATION_NON_REMUNERATION_RECURRENTE,
|
|
278
|
+
PrestationType.BILAN_PREVISIONNEL,
|
|
279
279
|
];
|
|
280
280
|
|
|
281
281
|
export const ClementineSubscriptionAccountancyExpertPrestations: PrestationType[] = [
|
|
@@ -9,6 +9,8 @@ export class IAuthenticationResponse {
|
|
|
9
9
|
expiresAt?: Date;
|
|
10
10
|
/** ID de l'utilisateur */
|
|
11
11
|
userId?: number;
|
|
12
|
+
/** Jeton opaque de défi MFA lié à l'étape mot de passe */
|
|
13
|
+
mfaChallengeToken?: string;
|
|
12
14
|
/** Message informatif */
|
|
13
15
|
message?: string;
|
|
14
16
|
/** Token JWT si authentification réussie */
|
|
@@ -3,7 +3,14 @@ import { PrestationTypeDetail } from '../../../Enum/PrestationType.js';
|
|
|
3
3
|
import { IUser } from '../../models/IUser.js';
|
|
4
4
|
|
|
5
5
|
export class ReadAuthenticateCollaborater {
|
|
6
|
-
|
|
6
|
+
/** JWT, absent tant que la vérification MFA n'est pas faite */
|
|
7
|
+
token?: string;
|
|
8
|
+
/** Vrai si un code MFA doit être vérifié via /mfa/verify/* avant d'obtenir le JWT */
|
|
9
|
+
requiresMfa?: boolean;
|
|
10
|
+
/** ID de l'utilisateur, à renvoyer avec le code MFA */
|
|
11
|
+
userId?: number;
|
|
12
|
+
/** Jeton opaque de défi MFA lié à l'étape mot de passe */
|
|
13
|
+
mfaChallengeToken?: string;
|
|
7
14
|
}
|
|
8
15
|
|
|
9
16
|
export class ReadConnectAs {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IsInt, IsNotEmpty, IsString, Length } from 'class-validator';
|
|
1
|
+
import { IsInt, IsNotEmpty, IsOptional, IsString, Length } from 'class-validator';
|
|
2
2
|
|
|
3
3
|
export class MfaVerifyCode {
|
|
4
4
|
@IsNotEmpty()
|
|
@@ -8,4 +8,9 @@ export class MfaVerifyCode {
|
|
|
8
8
|
@IsString()
|
|
9
9
|
@Length(6, 6)
|
|
10
10
|
code: string;
|
|
11
|
+
|
|
12
|
+
/** Optionnel le temps de la transition des anciennes versions de l'app mobile (SEC-04) */
|
|
13
|
+
@IsOptional()
|
|
14
|
+
@IsString()
|
|
15
|
+
mfaChallengeToken?: string;
|
|
11
16
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IsInt, IsNotEmpty, IsString } from 'class-validator';
|
|
1
|
+
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
|
2
2
|
|
|
3
3
|
export class MfaVerifyRecoveryCode {
|
|
4
4
|
@IsNotEmpty()
|
|
@@ -8,4 +8,9 @@ export class MfaVerifyRecoveryCode {
|
|
|
8
8
|
@IsString()
|
|
9
9
|
@IsNotEmpty()
|
|
10
10
|
recoveryCode: string;
|
|
11
|
+
|
|
12
|
+
/** Optionnel le temps de la transition des anciennes versions de l'app mobile (SEC-04) */
|
|
13
|
+
@IsOptional()
|
|
14
|
+
@IsString()
|
|
15
|
+
mfaChallengeToken?: string;
|
|
11
16
|
}
|
|
@@ -8,6 +8,10 @@ export enum Code {
|
|
|
8
8
|
// HTTP Error
|
|
9
9
|
tokenFailure = 'token_authentication_failure',
|
|
10
10
|
authenticationFailure = 'authentication_credentials_failure',
|
|
11
|
+
/** MFA challenge missing, expired, used or bound to another user: the user must log in again */
|
|
12
|
+
mfaChallengeInvalid = 'mfa_challenge_invalid',
|
|
13
|
+
/** Too many wrong MFA codes for the same challenge: the user must log in again */
|
|
14
|
+
mfaTooManyAttempts = 'mfa_too_many_attempts',
|
|
11
15
|
notFound = 'resource_not_found',
|
|
12
16
|
forbidden = 'forbidden',
|
|
13
17
|
fileNotFound = 'file_not_found',
|
|
@@ -31,6 +31,7 @@ import ejs from 'ejs';
|
|
|
31
31
|
import 'dayjs/locale/fr.js';
|
|
32
32
|
import { orderBy, groupBy, sumBy, round, uniq } from 'lodash-es';
|
|
33
33
|
import sanitize from 'sanitize-html';
|
|
34
|
+
import { isHtmlEmpty } from '../isHtml.js';
|
|
34
35
|
|
|
35
36
|
type brandColorType = {
|
|
36
37
|
primary: string;
|
|
@@ -89,7 +90,7 @@ export function generateCommercialDocument(
|
|
|
89
90
|
paymentLink: payload.paymentLink ?? '#',
|
|
90
91
|
idEstimateInvoiceType,
|
|
91
92
|
title: payload.title ?? null,
|
|
92
|
-
cgv: payload.cgv
|
|
93
|
+
cgv: isHtmlEmpty(payload.cgv) ? null : payload.cgv,
|
|
93
94
|
// EJS `with` blocks throw a ReferenceError when a variable key is entirely absent from the data object, so seller
|
|
94
95
|
// identity/address fields must always be defined even when the payload omits them.
|
|
95
96
|
name: payload.name ?? null,
|
|
@@ -120,7 +120,7 @@ export const bodyTemplate = `<html class="pdf-content">
|
|
|
120
120
|
<br/><%= languageTemplate.template.common.options.approval %> : <br/><br/><br/><br/>
|
|
121
121
|
<% } %>
|
|
122
122
|
|
|
123
|
-
<% if (cgv &&
|
|
123
|
+
<% if (cgv && settings && settings.invoiceCgv) { %>
|
|
124
124
|
<br><div style="page-break-after:always;"></div>
|
|
125
125
|
<div id="WYSIWYG"><%- cgv %></div>
|
|
126
126
|
<% } %>
|
package/ts/utils/isHtml.ts
CHANGED
|
@@ -7,3 +7,17 @@ export function isHtml(text: string): boolean {
|
|
|
7
7
|
const htmlRegex = /<([A-Za-z][A-Za-z0-9]*)\b[^>]*>(.*?)<\/\1>/i;
|
|
8
8
|
return htmlRegex.test(text);
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Verify if an HTML string is considered empty (e.g. "", "<p><br></p>", "<p> </p>", or empty tags with no visible text/image)
|
|
13
|
+
* @param html - The HTML string to verify
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
export function isHtmlEmpty(html?: string | null): boolean {
|
|
17
|
+
if (!html) return true;
|
|
18
|
+
const textWithoutTags = html
|
|
19
|
+
.replace(/<(?!\/?img\b)[^>]*>/gi, '')
|
|
20
|
+
.replace(/&(?:nbsp|#160|#xa0);/gi, '')
|
|
21
|
+
.trim();
|
|
22
|
+
return textWithoutTags.length === 0;
|
|
23
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
2
|
import { generateCommercialDocument } from '../commercialManagement/document.js';
|
|
3
|
+
import { isHtmlEmpty } from '../isHtml.js';
|
|
3
4
|
import { EstimateInvoiceType } from '../../types/Enum/EstimateInvoice.js';
|
|
4
5
|
import { CreateInvoiceDocument } from '../../types/Interface/api/commercialManagement/Create.js';
|
|
5
6
|
import { DiscountType } from '../../types/Enum/CommercialManagement.js';
|
|
@@ -118,6 +119,23 @@ describe('generateCommercialDocument', () => {
|
|
|
118
119
|
expect(result.body).not.toContain('<p>undefined</p>');
|
|
119
120
|
});
|
|
120
121
|
|
|
122
|
+
test.each(['<p><br></p>', '<p><br/></p>', '<p></p>', '<p> </p>', ' '])(
|
|
123
|
+
'should not render CGV or page break when cgv contains empty HTML (%s)',
|
|
124
|
+
(emptyCgv: string): void => {
|
|
125
|
+
const payload: CreateInvoiceDocument = {
|
|
126
|
+
...payloadWithoutSellerAddress,
|
|
127
|
+
cgv: emptyCgv,
|
|
128
|
+
settings: { invoiceCgv: true },
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const result = generateCommercialDocument(EstimateInvoiceType.FACTURE, payload);
|
|
132
|
+
|
|
133
|
+
expect(result.data.cgv).toBeNull();
|
|
134
|
+
expect(result.body).not.toContain('page-break-after:always;');
|
|
135
|
+
expect(result.body).not.toContain('id="WYSIWYG"');
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
121
139
|
test('should calculate VAT from rounded line totals after discounts and fractional quantities', () => {
|
|
122
140
|
const payload = {
|
|
123
141
|
...payloadWithoutSellerAddress,
|
|
@@ -210,3 +228,24 @@ describe('generateCommercialDocument', () => {
|
|
|
210
228
|
expect(result.data.totalTaxIncluded).toBe(-227);
|
|
211
229
|
});
|
|
212
230
|
});
|
|
231
|
+
|
|
232
|
+
describe('isHtmlEmpty', () => {
|
|
233
|
+
test.each([
|
|
234
|
+
[undefined, true],
|
|
235
|
+
[null, true],
|
|
236
|
+
['', true],
|
|
237
|
+
[' ', true],
|
|
238
|
+
['<p><br></p>', true],
|
|
239
|
+
['<p><br/></p>', true],
|
|
240
|
+
['<p></p>', true],
|
|
241
|
+
['<p> </p>', true],
|
|
242
|
+
['<p> </p>', true],
|
|
243
|
+
['<p><br></p><p><br></p>', true],
|
|
244
|
+
['<div><p><br></p></div>', true],
|
|
245
|
+
['<p>Conditions Générales</p>', false],
|
|
246
|
+
['<p><img src="test.png"></p>', false],
|
|
247
|
+
['<img src="test.png" />', false],
|
|
248
|
+
])('isHtmlEmpty(%j) should return %s', (input, expected) => {
|
|
249
|
+
expect(isHtmlEmpty(input as string)).toBe(expected);
|
|
250
|
+
});
|
|
251
|
+
});
|