@silexpert/core 2.0.120 → 2.0.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/{index-BvApezAZ.d.mts → index-CQ4tY4GS.d.mts} +1176 -1113
  2. package/dist/index-CQ4tY4GS.d.mts.map +1 -0
  3. package/dist/index.d.mts +2 -2
  4. package/dist/index.mjs +6 -6
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/server/index.d.mts +1 -1
  7. package/package.json +1 -1
  8. package/ts/api/index.ts +19 -2
  9. package/ts/api/resources/TeamSystemSignature.ts +16 -0
  10. package/ts/api/resources.ts +16 -0
  11. package/ts/types/Enum/CustomerSignature.ts +1 -0
  12. package/ts/types/Enum/LetterType.ts +8 -0
  13. package/ts/types/Enum/MealExpenseThresholds.ts +4 -0
  14. package/ts/types/Interface/api/appointment/Appointment.ts +1 -1
  15. package/ts/types/Interface/api/commercialManagement/Create.ts +6 -1
  16. package/ts/types/Interface/api/commercialManagement/CreateProductInvoice.ts +60 -0
  17. package/ts/types/Interface/api/product/CreateProduct.ts +3 -34
  18. package/ts/types/Interface/api/product/ProductLineInputs.ts +37 -0
  19. package/ts/types/Interface/api/refund/QueryRefundPaginated.ts +5 -0
  20. package/ts/types/Interface/api/teamSystemSignature/ReadTeamSystemSignature.ts +18 -0
  21. package/ts/types/Interface/api/teamSystemSignature/SendTeamSystemSignature.ts +23 -0
  22. package/ts/types/Interface/api/teamSystemSignature/index.ts +2 -0
  23. package/ts/types/Interface/models/ICustomerSignature.ts +2 -0
  24. package/ts/types/Interface/models/IEstimateInvoice.ts +4 -0
  25. package/ts/types/index.ts +3 -0
  26. package/ts/utils/tests/payment.spec.ts +0 -70
  27. package/ts/utils/validators/nestedArray.validator.ts +42 -0
  28. package/dist/index-BvApezAZ.d.mts.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { t as __name } from "../rolldown-runtime-BhWHdk3m.mjs";
2
- import { BJ as CreateMailHeader, HJ as CreatePreSaleDocument, Uq as ReadCommercialDocumentTemplate, Wq as ReadCommercialMailTemplate, eW as IUser, sX as EstimateInvoiceType, zJ as CreateInvoiceDocument } from "../index-BvApezAZ.mjs";
2
+ import { KJ as CreateInvoiceDocument, Xq as ReadCommercialDocumentTemplate, YJ as CreatePreSaleDocument, Zq as ReadCommercialMailTemplate, mX as EstimateInvoiceType, qJ as CreateMailHeader, sW as IUser } from "../index-CQ4tY4GS.mjs";
3
3
  import "dayjs/locale/fr.js";
4
4
  //#region ts/utils/email.d.ts
5
5
  export declare function getHoraires(user: IUser): "Du lundi au vendredi de 9h à 18h" | "Du lundi au jeudi de 9h à 17h<br/>Le vendredi de 9h à 12h";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silexpert/core",
3
- "version": "2.0.120",
3
+ "version": "2.0.121",
4
4
  "description": "Silex Core perform HTTP Request and object binding. Silex core helps developers to retrieve and return formatted object",
5
5
  "keywords": [
6
6
  "accounting",
package/ts/api/index.ts CHANGED
@@ -41,11 +41,28 @@ export const Silex = (params: { url: string; loginWith?: any; setToken?: any })
41
41
  },
42
42
  });
43
43
 
44
+ const wrappedAxios = wrapAxios(axiosInstance);
45
+
46
+ const instantiateResource = (value: any): any => {
47
+ if (typeof value === 'function') {
48
+ return new value(wrappedAxios);
49
+ }
50
+ if (typeof value === 'object' && value !== null) {
51
+ return Object.entries(value).reduce(
52
+ (acc, [k, v]) => ({
53
+ ...acc,
54
+ [k]: instantiateResource(v),
55
+ }),
56
+ {},
57
+ );
58
+ }
59
+ return value;
60
+ };
61
+
44
62
  const _resources = Object.entries(resources).reduce(
45
63
  (acc, [name, value]) => ({
46
64
  ...acc,
47
-
48
- [name]: new value(wrapAxios(axiosInstance)),
65
+ [name]: instantiateResource(value),
49
66
  }),
50
67
  {},
51
68
  );
@@ -0,0 +1,16 @@
1
+ import { SendTeamSystemSignature, ReadTeamSystemSignature } from '../../types/index.js';
2
+ import { Resource } from '../Resource.js';
3
+
4
+ export class TeamSystemSignature extends Resource {
5
+ sendForSignature(idDocument: number, payload: SendTeamSystemSignature): Promise<ReadTeamSystemSignature> {
6
+ return this.axios.$post(`/connectors/team-system/signatures/documents/${idDocument}/send-for-signature`, payload);
7
+ }
8
+
9
+ startSession(token: string): Promise<{ url: string }> {
10
+ return this.axios.$get(`/connectors/team-system/signatures/start-session/${token}`);
11
+ }
12
+
13
+ getSignatureStatus(idDocument: number): Promise<ReadTeamSystemSignature> {
14
+ return this.axios.$get(`/connectors/team-system/signatures/documents/${idDocument}/signature-status`);
15
+ }
16
+ }
@@ -118,6 +118,8 @@ import { Prestations } from './resources/Prestations.js';
118
118
  import { StatisticCustomerSuccess } from './resources/statistic/CustomerSuccess.js';
119
119
  import { CommercialImport } from './resources/CommercialImport.js';
120
120
  import { Team } from './resources/Team.js';
121
+ import { TeamSystemSignature } from './resources/TeamSystemSignature.js';
122
+
121
123
  import { CashAndExpense } from './resources/CashAndExpense.js';
122
124
  import { Archive } from './resources/Archive.js';
123
125
  import { AccountsSector } from './resources/AccountsSectors.js';
@@ -392,6 +394,9 @@ export const resources = {
392
394
  docoon: Docoon,
393
395
  usersHistory: UsersHistory,
394
396
  acd: Acd,
397
+ teamSystem: {
398
+ signature: TeamSystemSignature,
399
+ },
395
400
  zendesk: Zendesk,
396
401
  formalism: Formalism,
397
402
  financialProjection: FinancialProjection,
@@ -403,3 +408,14 @@ export const resources = {
403
408
  };
404
409
 
405
410
  export * from './resources/Cache.js';
411
+ export * from './resources/TeamSystemSignature.js';
412
+
413
+ export type SilexResourceInstance<T> = T extends new (...args: any) => infer R
414
+ ? R
415
+ : T extends object
416
+ ? { [K in keyof T]: SilexResourceInstance<T[K]> }
417
+ : never;
418
+
419
+ export type SilexResources = {
420
+ [K in keyof typeof resources]: SilexResourceInstance<(typeof resources)[K]>;
421
+ };
@@ -19,6 +19,7 @@ export enum SignatureType {
19
19
  EQUIPMENT = 'equipment',
20
20
  MANDAT = 'mandat', // N'est plus utilisé, permet de distinguer les mandats signés avant ajout de la signature Clementine
21
21
  MANDAT_PA_EC = 'mandat_pa_ec',
22
+ COMMERCIAL_DOCUMENT = 'commercial_document',
22
23
  }
23
24
 
24
25
  export const SignatureStatusDetail = {
@@ -47,4 +47,12 @@ export const LetterType = {
47
47
  id: 12,
48
48
  name: 'Bilan avec observation',
49
49
  },
50
+ RESIGNATION_OF_MISSION_LRAR: {
51
+ id: 13,
52
+ name: 'LRAR démission de la mission',
53
+ },
54
+ TERMINATION_MANDATE_ELECTRONIC_INVOICING: {
55
+ id: 14,
56
+ name: 'Résiliation mandat facturation électronique',
57
+ },
50
58
  };
@@ -30,4 +30,8 @@ export const MealExpenseThresholds: IMealExpenseThresholds = {
30
30
  min: 5.5,
31
31
  max: 21.1,
32
32
  },
33
+ 2026: {
34
+ min: 5.5,
35
+ max: 21.4,
36
+ },
33
37
  };
@@ -24,12 +24,12 @@ export class CreateAppointmentLink {
24
24
 
25
25
  export interface AppointmentContext {
26
26
  isProspect: boolean;
27
- isSoftware: boolean;
28
27
  isInCreation: boolean;
29
28
  hasAcceptedLab: boolean;
30
29
  hasJuridicalPrestation: boolean;
31
30
  hasExpertAccountantPrestation: boolean;
32
31
  existingAppointments: Array<{ id: number; idTimeSlot: number; startDate: Date }>;
32
+ hasSocietyCreation: boolean;
33
33
  }
34
34
 
35
35
  export class QueryAppointmentAvailability {
@@ -29,6 +29,8 @@ import { IThirdParty } from '../../models/IThirdParty.js';
29
29
  import { IEstimateInvoice } from '../../models/IEstimateInvoice.js';
30
30
  import { ToBoolean } from '../../../../utils/transforms/boolean.transform.js';
31
31
  import { ToNumericEnum } from '../../../../utils/transforms/numericEnum.transform.js';
32
+ import { ValidateNestedArrayOf } from '../../../../utils/validators/nestedArray.validator.js';
33
+ import { CreateProductInvoice } from './CreateProductInvoice.js';
32
34
 
33
35
  export type IProductInvoiceToCreate = IProductInvoice & { save?: boolean };
34
36
 
@@ -430,8 +432,11 @@ export class CommercialDocumentInputs {
430
432
  @ApiProperty({ description: 'Conditions de réglement' })
431
433
  termsOfPayment: string | null;
432
434
 
433
- @ApiProperty()
435
+ @ApiProperty({ type: [CreateProductInvoice] })
434
436
  @IsArray()
437
+ @ValidateNestedArrayOf(CreateProductInvoice)
438
+ // Lines are validated as CreateProductInvoice, but stay typed as IProductInvoice because the same objects feed the
439
+ // document rendering helpers (ReadProductEditing) shared with the front-end apps
435
440
  products: Array<IProductInvoiceToCreate>;
436
441
 
437
442
  @ApiProperty({ description: 'Escompte' })
@@ -0,0 +1,60 @@
1
+ import { IsBoolean, IsEnum, IsIn, IsInt, IsNumber, IsOptional, IsString } from 'class-validator';
2
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
3
+ import { DiscountType } from '../../../Enum/CommercialManagement.js';
4
+ import { Vat } from '../../../Enum/Vat.js';
5
+ import type { VatType } from '../../../Enum/Vat.js';
6
+ import { ProductLineInputs } from '../product/ProductLineInputs.js';
7
+
8
+ const VAT_IDS: number[] = (Object.values(Vat) as VatType[]).map((vat) => vat.id);
9
+
10
+ export class CreateProductInvoice extends ProductLineInputs {
11
+ @ApiPropertyOptional({
12
+ description: 'Absente lorsque la société ne facture pas la TVA : la ligne est alors Hors-Taxe',
13
+ })
14
+ @IsIn(VAT_IDS)
15
+ @IsOptional()
16
+ idVat?: number | null;
17
+
18
+ @ApiProperty({ description: 'Peut être négatif (remise, avoir)' })
19
+ @IsNumber()
20
+ priceTaxExcluded: number;
21
+
22
+ @ApiProperty()
23
+ @IsNumber()
24
+ quantity: number;
25
+
26
+ @ApiPropertyOptional()
27
+ @IsString()
28
+ @IsOptional()
29
+ itemSaleName?: string | null;
30
+
31
+ @ApiPropertyOptional({ description: "Référence de l'article" })
32
+ @IsString()
33
+ @IsOptional()
34
+ itemCodeProduct?: string | null;
35
+
36
+ @ApiPropertyOptional()
37
+ @IsInt()
38
+ @IsOptional()
39
+ idProduct?: number | null;
40
+
41
+ @ApiPropertyOptional()
42
+ @IsNumber()
43
+ @IsOptional()
44
+ discount2?: number | null;
45
+
46
+ @ApiPropertyOptional({ enum: DiscountType })
47
+ @IsEnum(DiscountType)
48
+ @IsOptional()
49
+ discountType?: DiscountType | null;
50
+
51
+ @ApiPropertyOptional()
52
+ @IsInt()
53
+ @IsOptional()
54
+ reorder?: number | null;
55
+
56
+ @ApiPropertyOptional({ description: "Enregistre l'article dans le catalogue de produits" })
57
+ @IsBoolean()
58
+ @IsOptional()
59
+ save?: boolean;
60
+ }
@@ -1,10 +1,9 @@
1
- import { IsBoolean, IsInt, IsNumber, IsOptional, IsString, Min } from 'class-validator';
2
- import { ProductType, Unit } from '../../../index.js';
1
+ import { IsBoolean, IsNumber, IsOptional, IsString, Min } from 'class-validator';
3
2
  import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
4
3
  import { ToBoolean } from '../../../../utils/transforms/boolean.transform.js';
5
- import { IsObjectId } from '../../../../utils/validators/objectId.validator.js';
4
+ import { ProductLineInputs } from './ProductLineInputs.js';
6
5
 
7
- export class CreateProduct {
6
+ export class CreateProduct extends ProductLineInputs {
8
7
  @ApiProperty()
9
8
  @IsString()
10
9
  name: string;
@@ -19,44 +18,14 @@ export class CreateProduct {
19
18
  @IsNumber()
20
19
  idVat: number;
21
20
 
22
- @ApiPropertyOptional()
23
- @IsNumber()
24
- @IsOptional()
25
- marginVatType?: number;
26
-
27
- @ApiPropertyOptional()
28
- @IsNumber()
29
- @IsOptional()
30
- amountOfPurchase?: number;
31
-
32
21
  @ApiPropertyOptional()
33
22
  @IsBoolean()
34
23
  @ToBoolean()
35
24
  @IsOptional()
36
25
  isFree?: boolean;
37
26
 
38
- @ApiProperty()
39
- @IsNumber()
40
- @IsObjectId(Unit)
41
- unit: number;
42
-
43
- @ApiProperty()
44
- @IsNumber()
45
- @IsObjectId(ProductType)
46
- type: number;
47
-
48
- @ApiPropertyOptional()
49
- @IsString()
50
- @IsOptional()
51
- comment?: string;
52
-
53
27
  @ApiPropertyOptional()
54
28
  @IsString()
55
29
  @IsOptional()
56
30
  codeProduct?: string;
57
-
58
- @ApiPropertyOptional()
59
- @IsInt()
60
- @IsOptional()
61
- idAccount?: number;
62
31
  }
@@ -0,0 +1,37 @@
1
+ import { IsInt, IsNumber, IsOptional, IsString } from 'class-validator';
2
+ import { ProductType, Unit } from '../../../index.js';
3
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
4
+ import { IsObjectId } from '../../../../utils/validators/objectId.validator.js';
5
+
6
+ // Fields shared by a catalog product and a commercial document line, validated the same way in both contexts
7
+ export class ProductLineInputs {
8
+ @ApiProperty()
9
+ @IsNumber()
10
+ @IsObjectId(Unit)
11
+ unit: number;
12
+
13
+ @ApiProperty()
14
+ @IsNumber()
15
+ @IsObjectId(ProductType)
16
+ type: number;
17
+
18
+ @ApiPropertyOptional()
19
+ @IsNumber()
20
+ @IsOptional()
21
+ marginVatType?: number;
22
+
23
+ @ApiPropertyOptional()
24
+ @IsNumber()
25
+ @IsOptional()
26
+ amountOfPurchase?: number | null;
27
+
28
+ @ApiPropertyOptional()
29
+ @IsString()
30
+ @IsOptional()
31
+ comment?: string | null;
32
+
33
+ @ApiPropertyOptional()
34
+ @IsInt()
35
+ @IsOptional()
36
+ idAccount?: number | null;
37
+ }
@@ -14,6 +14,11 @@ export class QueryRefundPaginated extends RequiredQueryPaginate implements Order
14
14
  @IsOptional()
15
15
  motif?: number;
16
16
 
17
+ @ApiPropertyOptional()
18
+ @IsOptional()
19
+ @IsInt()
20
+ idSociety?: number;
21
+
17
22
  @ApiPropertyOptional()
18
23
  @IsOptional()
19
24
  startDate?: Date;
@@ -0,0 +1,18 @@
1
+ import { ApiPropertyOptional } from '@nestjs/swagger';
2
+
3
+ export class ReadTeamSystemSignature {
4
+ @ApiPropertyOptional()
5
+ tsDocumentId?: string | null;
6
+
7
+ @ApiPropertyOptional()
8
+ tsSignStatus?: string | null;
9
+
10
+ @ApiPropertyOptional()
11
+ tsSignUrl?: string | null;
12
+
13
+ @ApiPropertyOptional()
14
+ tsSignedAt?: Date | null;
15
+
16
+ @ApiPropertyOptional()
17
+ success?: boolean;
18
+ }
@@ -0,0 +1,23 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
3
+
4
+ export class SendTeamSystemSignature {
5
+ @ApiProperty({ description: 'Prénom du signataire' })
6
+ @IsString()
7
+ @IsNotEmpty()
8
+ signerFirstName: string;
9
+
10
+ @ApiProperty({ description: 'Nom du signataire' })
11
+ @IsString()
12
+ @IsNotEmpty()
13
+ signerLastName: string;
14
+
15
+ @ApiProperty({ description: 'Email du signataire' })
16
+ @IsEmail()
17
+ signerEmail: string;
18
+
19
+ @ApiProperty({ description: 'Numéro de téléphone du signataire' })
20
+ @IsString()
21
+ @IsNotEmpty()
22
+ signerPhone: string;
23
+ }
@@ -0,0 +1,2 @@
1
+ export * from './SendTeamSystemSignature.js';
2
+ export * from './ReadTeamSystemSignature.js';
@@ -16,6 +16,8 @@ export class ICustomerSignature {
16
16
  tokenSigner?: string;
17
17
  mailSigner: string;
18
18
  phoneSigner: string;
19
+ firstNameSigner?: string | null;
20
+ lastNameSigner?: string | null;
19
21
  idSendMethod: number;
20
22
  idStatusSignature: number;
21
23
  idPrestation?: number | null;
@@ -8,6 +8,7 @@ import { IInvoiceRecurrence } from './IInvoiceRecurrence.js';
8
8
  import type { ReadEstimateInvoiceStatus } from '../api/commercialManagement/Read.js';
9
9
  import { AccountingCategoryId, InvoiceLanguage } from '../../Enum/CommercialManagement.js';
10
10
  import { IReminder } from './IReminder.js';
11
+ import { ICustomerSignature } from './ICustomerSignature.js';
11
12
 
12
13
  // estimatesInvoices
13
14
  export class IEstimateInvoice {
@@ -57,9 +58,12 @@ export class IEstimateInvoice {
57
58
  updatedAt?: Date | null;
58
59
  deletedAt?: Date | null;
59
60
 
61
+ idCustomerSignature?: number | null;
62
+
60
63
  // Associations
61
64
  products?: IProductInvoice[];
62
65
  thirdParty?: IThirdParty;
66
+ signature?: ICustomerSignature | null;
63
67
  type?: IEstimateInvoiceType;
64
68
  customerPayment?: IPaymentExpectationCustomer;
65
69
  society?: ISociety;
package/ts/types/index.ts CHANGED
@@ -289,6 +289,7 @@ export * from './Interface/models/IRevenueMovement.js';
289
289
  export * from './Interface/models/IRevenueSnapshot.js';
290
290
  export * from './Interface/models/IEInvoicingEligibilityRequest.js';
291
291
  export * from './Interface/models/IUsersHistory.js';
292
+ export * from './Interface/api/teamSystemSignature/index.js';
292
293
  export * from './Interface/models/IEInvoicingKycAndMandate.js';
293
294
  export * from './Interface/models/IConnectorTracking.js';
294
295
  export * from './Interface/models/IEInvoicing.js';
@@ -894,6 +895,7 @@ export * from './Interface/api/vehicle/CreateSocietyVehicle.js';
894
895
  export * from './Interface/api/vehicle/UpdateSocietyVehicle.js';
895
896
  export * from './Interface/api/fecFile/QueryFecFile.js';
896
897
  export * from './Interface/api/product/CreateProduct.js';
898
+ export * from './Interface/api/product/ProductLineInputs.js';
897
899
  export * from './Interface/api/product/UpdateProduct.js';
898
900
  export * from './Interface/api/product/QueryProduct.js';
899
901
  export * from './Interface/api/registration/ReadRegistration.js';
@@ -983,6 +985,7 @@ export * from './Interface/api/customerActivity/CreateCustomerActivityReport.js'
983
985
  export * from './Interface/api/customerActivity/Query.js';
984
986
  export * from './Interface/api/customerActivity/Create.js';
985
987
  export * from './Interface/api/commercialManagement/Create.js';
988
+ export * from './Interface/api/commercialManagement/CreateProductInvoice.js';
986
989
  export * from './Interface/api/commercialManagement/Read.js';
987
990
  export * from './Interface/api/commercialManagement/Query.js';
988
991
  export * from './Interface/api/commercialManagement/Update.js';
@@ -13,7 +13,6 @@ describe('Payment', () => {
13
13
  const idSociety = 1;
14
14
  const yearlyAmountNonExpress = 1800;
15
15
  const nextYearlyAmountNonExpress = 1800;
16
- const appCost = 14.9;
17
16
 
18
17
  const firstExerciceNonExpresssEndDate = dayjs().add(11, 'month').endOf('month').toDate();
19
18
  const prestationList = {
@@ -290,39 +289,6 @@ describe('Payment', () => {
290
289
  });
291
290
  // annual price increase
292
291
  // @ts-ignore
293
- test('should apply annual price increase (2,75%) if signed before last 3 months of civil year', () => {
294
- const mockedDate = dayjs('2024-08-15').toDate();
295
- MockDate.set(mockedDate);
296
-
297
- const newFirstExerciceNonExpresssEndDate = dayjs().add(11, 'month').endOf('month').toDate();
298
-
299
- const payments = calculatePaymentsAuto({
300
- idTypeBrand: 1,
301
- firstExerciceNonExpresssEndDate: newFirstExerciceNonExpresssEndDate,
302
- idPrestation,
303
- prestationList,
304
- idVat,
305
- listPartialExercicePrestationExpress: [],
306
- listRegulations: [
307
- {
308
- amount: 50,
309
- amountUsed: 0,
310
- } as IRegulation,
311
- ],
312
- yearlyAmountNonExpress,
313
- nextYearlyAmountNonExpress,
314
- });
315
-
316
- const listNonExpressPayments = payments.filter(
317
- (p) => dayjs(p.closingDate).diff(newFirstExerciceNonExpresssEndDate) === 0,
318
- );
319
- const firstPaymentWithTaxRateForced = listNonExpressPayments[1].amount * 1.0275;
320
- // @ts-ignore
321
- expect(firstPaymentWithTaxRateForced - listNonExpressPayments.splice(-2)[0].amount).toBeLessThanOrEqual(0.01); //Permet de gérer le pb d'arrondi
322
-
323
- MockDate.reset();
324
- });
325
- // @ts-ignore
326
292
  test('should not apply annual price increase (2,75%) if signed in last 3 months of civil year', () => {
327
293
  const mockedDate = dayjs('2024-11-15').toDate();
328
294
  MockDate.set(mockedDate);
@@ -353,42 +319,6 @@ describe('Payment', () => {
353
319
  MockDate.reset();
354
320
  });
355
321
 
356
- // @ts-ignore
357
- test('should apply 14,9€ HT increase for the second exercice created (only offered on first exo) (no rules for 3 last months)', () => {
358
- const mockedDate = dayjs('2024-06-15').toDate();
359
- MockDate.set(mockedDate);
360
-
361
- const newFirstExerciceNonExpresssEndDate = dayjs().add(12, 'month').endOf('month').toDate();
362
-
363
- const payments = calculatePaymentsAuto({
364
- idTypeBrand: 1,
365
- firstExerciceNonExpresssEndDate: newFirstExerciceNonExpresssEndDate,
366
- idPrestation,
367
- prestationList,
368
- idVat,
369
- listPartialExercicePrestationExpress: [],
370
- listRegulations: [
371
- {
372
- amount: 50,
373
- amountUsed: 50,
374
- } as IRegulation,
375
- ],
376
- yearlyAmountNonExpress,
377
- nextYearlyAmountNonExpress,
378
- });
379
-
380
- const paymentsOfFirstExo =
381
- payments.filter((p) => dayjs(p.datePayment).diff(dayjs('2025-06-01')) === 0)[0].amount ?? 0;
382
- const paymentsOfSecondExo =
383
- payments.filter((p) => dayjs(p.datePayment).diff(dayjs('2025-07-01')) === 0).splice(-1)[0].amount ?? 0;
384
- // @ts-ignore
385
- expect(
386
- Math.round(100 * (paymentsOfFirstExo + getTaxesIncludedAmount(appCost, idVat) * 1.0275)) / 100,
387
- ).toStrictEqual(paymentsOfSecondExo);
388
-
389
- MockDate.reset();
390
- });
391
-
392
322
  // misc
393
323
  // @ts-ignore
394
324
  test('should payments been generated sorted by exercices start date', () => {
@@ -0,0 +1,42 @@
1
+ import { plainToInstance } from 'class-transformer';
2
+ import type { ClassConstructor } from 'class-transformer';
3
+ import { registerDecorator, validateSync } from 'class-validator';
4
+ import type { ValidationArguments, ValidationError, ValidationOptions } from 'class-validator';
5
+
6
+ function getItemErrors<T extends object>(cls: ClassConstructor<T>, item: unknown): string[] {
7
+ if (typeof item !== 'object' || item === null) return ['must be an object'];
8
+
9
+ // Spread into a plain object: the API implicit conversion may have rebuilt the item as an Array instance
10
+ const errors: ValidationError[] = validateSync(plainToInstance(cls, { ...item }));
11
+ return errors.flatMap((error) =>
12
+ Object.values(error.constraints ?? {}).map((constraint) => `${error.property}: ${constraint}`),
13
+ );
14
+ }
15
+
16
+ // Validates each array item against `cls`. Replaces `@ValidateNested()` + `@Type()`: `@Type()` requires `reflect-metadata`
17
+ // at runtime (breaks browser / SSR consumers), and without it the implicit conversion does not instantiate the items.
18
+ export function ValidateNestedArrayOf<T extends object>(
19
+ cls: ClassConstructor<T>,
20
+ validationOptions?: ValidationOptions,
21
+ ) {
22
+ return function validator(object: object, propertyName: string) {
23
+ registerDecorator({
24
+ name: 'ValidateNestedArrayOf',
25
+ target: object.constructor,
26
+ propertyName,
27
+ options: validationOptions,
28
+ validator: {
29
+ validate(value: unknown) {
30
+ return Array.isArray(value) && value.every((item) => getItemErrors(cls, item).length === 0);
31
+ },
32
+ defaultMessage(args: ValidationArguments) {
33
+ if (!Array.isArray(args.value)) return `${propertyName} must be an array`;
34
+
35
+ return (args.value as unknown[])
36
+ .flatMap((item, index) => getItemErrors(cls, item).map((error) => `${propertyName}[${index}].${error}`))
37
+ .join(', ');
38
+ },
39
+ },
40
+ });
41
+ };
42
+ }