@silexpert/core 2.0.120 → 2.0.122
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-BvApezAZ.d.mts → index-BTRchHCY.d.mts} +1187 -1114
- package/dist/index-BTRchHCY.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-BCFrBxXc.mjs → product-B35JCiip.mjs} +2 -2
- package/dist/product-B35JCiip.mjs.map +1 -0
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.mjs +3 -3
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
- package/ts/api/index.ts +19 -2
- package/ts/api/resources/Exercice.ts +5 -0
- package/ts/api/resources/TeamSystemSignature.ts +16 -0
- package/ts/api/resources.ts +16 -0
- package/ts/types/Enum/CustomerSignature.ts +1 -0
- package/ts/types/Enum/EInvoicingStatusCode.ts +15 -1
- package/ts/types/Enum/LetterType.ts +8 -0
- package/ts/types/Enum/MealExpenseThresholds.ts +4 -0
- package/ts/types/Enum/Unit.ts +7 -0
- package/ts/types/Interface/api/appointment/Appointment.ts +1 -1
- package/ts/types/Interface/api/balanceSheet/UpdateBalanceSheet.ts +5 -0
- package/ts/types/Interface/api/commercialManagement/Create.ts +6 -1
- package/ts/types/Interface/api/commercialManagement/CreateProductInvoice.ts +60 -0
- package/ts/types/Interface/api/commercialManagement/Read.ts +1 -0
- package/ts/types/Interface/api/exercice/QueryExercice.ts +8 -2
- package/ts/types/Interface/api/product/CreateProduct.ts +3 -34
- package/ts/types/Interface/api/product/ProductLineInputs.ts +37 -0
- package/ts/types/Interface/api/refund/QueryRefundPaginated.ts +5 -0
- package/ts/types/Interface/api/teamSystemSignature/ReadTeamSystemSignature.ts +18 -0
- package/ts/types/Interface/api/teamSystemSignature/SendTeamSystemSignature.ts +23 -0
- package/ts/types/Interface/api/teamSystemSignature/index.ts +2 -0
- package/ts/types/Interface/models/IBalanceSheet.ts +1 -0
- package/ts/types/Interface/models/ICustomerSignature.ts +2 -0
- package/ts/types/Interface/models/IEstimateInvoice.ts +4 -0
- package/ts/types/index.ts +3 -0
- package/ts/utils/amount.ts +7 -0
- package/ts/utils/commercialManagement/document.ts +1 -1
- package/ts/utils/commercialManagement/product.ts +7 -3
- package/ts/utils/commercialManagement/vat.ts +9 -11
- package/ts/utils/tests/document.spec.ts +95 -0
- package/ts/utils/tests/payment.spec.ts +0 -70
- package/ts/utils/validators/nestedArray.validator.ts +42 -0
- package/dist/index-BvApezAZ.d.mts.map +0 -1
- package/dist/product-BCFrBxXc.mjs.map +0 -1
|
@@ -3,7 +3,7 @@ import { IProductInvoice, ReadProductEditing } from '../../types/index.js';
|
|
|
3
3
|
import { Unit } from '../../types/Enum/Unit.js';
|
|
4
4
|
import { DiscountType, VatMarginCategyList } from '../../types/Enum/CommercialManagement.js';
|
|
5
5
|
import { getById } from './vat.js';
|
|
6
|
-
import { integerToFormattedAmountString } from '../amount.js';
|
|
6
|
+
import { integerToFormattedAmountString, roundAmountToInteger } from '../amount.js';
|
|
7
7
|
|
|
8
8
|
export function getDiscount(product: IProductInvoice): number {
|
|
9
9
|
if (product.discount2 === null || product.discount2 === undefined || product.discount2 <= 0) return 0;
|
|
@@ -24,16 +24,20 @@ export function getTotalTaxExcluded(product: IProductInvoice): number {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function productToReadProductEditing(payload: IProductInvoice): ReadProductEditing {
|
|
27
|
+
const totalTaxExcluded = getTotalTaxExcluded(payload);
|
|
28
|
+
const totalTaxExcludedRounded = roundAmountToInteger(totalTaxExcluded);
|
|
29
|
+
|
|
27
30
|
return {
|
|
28
31
|
...payload,
|
|
29
|
-
totalTaxExcluded
|
|
32
|
+
totalTaxExcluded,
|
|
33
|
+
totalTaxExcludedRounded,
|
|
30
34
|
unitFormatted: Object.values(Unit).find((unit) => unit.id === payload.unit)?.shortName ?? '',
|
|
31
35
|
vatFormatted: getById(payload.idVat)?.saleName ?? '',
|
|
32
36
|
priceTaxExcludedFormatted: isDefined(payload.priceTaxExcluded)
|
|
33
37
|
? integerToFormattedAmountString(payload.priceTaxExcluded)
|
|
34
38
|
: null,
|
|
35
39
|
totalTaxExcludedFormatted: isDefined(payload.priceTaxExcluded)
|
|
36
|
-
? integerToFormattedAmountString(
|
|
40
|
+
? integerToFormattedAmountString(totalTaxExcludedRounded)
|
|
37
41
|
: null,
|
|
38
42
|
discountFormatted:
|
|
39
43
|
payload.discount2 && payload.discount2 > 0 ? integerToFormattedAmountString(getDiscount(payload)) : null,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sumBy } from 'lodash-es';
|
|
2
2
|
import { Vat, VatType } from '../../types/Enum/Vat.js';
|
|
3
|
-
import { integerToFormattedAmountString } from '../amount.js';
|
|
3
|
+
import { integerToFormattedAmountString, roundAmountToInteger } from '../amount.js';
|
|
4
4
|
import { ReadProductEditing, ReadVatEditing } from '../../types/index.js';
|
|
5
5
|
|
|
6
6
|
export function getAmount(amount: number | null, vatValue: number): number {
|
|
@@ -15,19 +15,17 @@ export function getById(id: number): VatType | undefined {
|
|
|
15
15
|
|
|
16
16
|
export function vatToReadVatEditing(products: ReadProductEditing[]): ReadVatEditing {
|
|
17
17
|
const { idVat } = products[0];
|
|
18
|
+
const base = sumBy(products, 'totalTaxExcludedRounded');
|
|
19
|
+
const amount = roundAmountToInteger(getAmount(base, getById(idVat)?.value ?? 0));
|
|
20
|
+
|
|
18
21
|
return {
|
|
19
22
|
id: idVat,
|
|
20
23
|
name: getById(idVat)?.saleName ?? '',
|
|
21
|
-
amount
|
|
24
|
+
amount,
|
|
22
25
|
value: getById(idVat)?.value ?? 0,
|
|
23
|
-
baseFormatted: integerToFormattedAmountString(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
(sumBy(products, (p) => (p.totalTaxExcluded ? +p.totalTaxExcluded : 0)) * (getById(idVat)?.value ?? 0)) / 100,
|
|
28
|
-
),
|
|
29
|
-
toPay:
|
|
30
|
-
(sumBy(products, (p) => (p.totalTaxExcluded ? +p.totalTaxExcluded : 0)) * (getById(idVat)?.value ?? 0)) / 100,
|
|
31
|
-
base: sumBy(products, (p) => (p.totalTaxExcluded ? +p.totalTaxExcluded : 0)),
|
|
26
|
+
baseFormatted: integerToFormattedAmountString(base),
|
|
27
|
+
amountFormatted: integerToFormattedAmountString(amount),
|
|
28
|
+
toPay: amount,
|
|
29
|
+
base,
|
|
32
30
|
};
|
|
33
31
|
}
|
|
@@ -2,6 +2,9 @@ import { describe, expect, test } from 'vitest';
|
|
|
2
2
|
import { generateCommercialDocument } from '../commercialManagement/document.js';
|
|
3
3
|
import { EstimateInvoiceType } from '../../types/Enum/EstimateInvoice.js';
|
|
4
4
|
import { CreateInvoiceDocument } from '../../types/Interface/api/commercialManagement/Create.js';
|
|
5
|
+
import { DiscountType } from '../../types/Enum/CommercialManagement.js';
|
|
6
|
+
import { Unit } from '../../types/Enum/Unit.js';
|
|
7
|
+
import { Vat } from '../../types/Enum/Vat.js';
|
|
5
8
|
|
|
6
9
|
// Payload reproducing a real-world case where the seller address fields (`name`, `street`,
|
|
7
10
|
// `number`, `zipCode`, `city`) are entirely absent from the object, not just `null`.
|
|
@@ -113,4 +116,96 @@ describe('generateCommercialDocument', () => {
|
|
|
113
116
|
|
|
114
117
|
expect(result.body).not.toContain('<p>undefined</p>');
|
|
115
118
|
});
|
|
119
|
+
|
|
120
|
+
test('should calculate VAT from rounded line totals after discounts and fractional quantities', () => {
|
|
121
|
+
const payload = {
|
|
122
|
+
...payloadWithoutSellerAddress,
|
|
123
|
+
products: Array.from({ length: 6 }, (_, index) => ({
|
|
124
|
+
idVat: Vat.TX_20.id,
|
|
125
|
+
unit: Unit.ITEM.id,
|
|
126
|
+
itemSaleName: `Rounded line ${index + 1}`,
|
|
127
|
+
priceTaxExcluded: 101,
|
|
128
|
+
quantity: 1.0049,
|
|
129
|
+
discount2: 100,
|
|
130
|
+
discountType: DiscountType.PERCENT,
|
|
131
|
+
reorder: index,
|
|
132
|
+
})),
|
|
133
|
+
} as unknown as CreateInvoiceDocument;
|
|
134
|
+
|
|
135
|
+
const result = generateCommercialDocument(EstimateInvoiceType.FACTURE, payload);
|
|
136
|
+
|
|
137
|
+
expect(result.body).toContain('1,00€');
|
|
138
|
+
expect(result.data.vats).toEqual(
|
|
139
|
+
expect.arrayContaining([
|
|
140
|
+
expect.objectContaining({ base: 600, amount: 120, baseFormatted: '6,00', amountFormatted: '1,20' }),
|
|
141
|
+
]),
|
|
142
|
+
);
|
|
143
|
+
expect(result.data.totalTaxExcluded).toBe(600);
|
|
144
|
+
expect(result.data.totalTaxIncluded).toBe(720);
|
|
145
|
+
expect(result.data.totalTaxIncludedFormatted).toBe('7,20');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('should use the rounded half-cent line amount in the PDF and VAT recap', () => {
|
|
149
|
+
const payload = {
|
|
150
|
+
...payloadWithoutSellerAddress,
|
|
151
|
+
products: [
|
|
152
|
+
{
|
|
153
|
+
idVat: Vat.TX_20.id,
|
|
154
|
+
unit: Unit.ITEM.id,
|
|
155
|
+
itemSaleName: 'Half-cent line',
|
|
156
|
+
priceTaxExcluded: 201,
|
|
157
|
+
quantity: 0.5,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
} as unknown as CreateInvoiceDocument;
|
|
161
|
+
|
|
162
|
+
const result = generateCommercialDocument(EstimateInvoiceType.FACTURE, payload);
|
|
163
|
+
|
|
164
|
+
expect(result.body).toContain('1,01€');
|
|
165
|
+
expect(result.data.products[0]).toMatchObject({
|
|
166
|
+
totalTaxExcluded: 100.5,
|
|
167
|
+
totalTaxExcludedRounded: 101,
|
|
168
|
+
totalTaxExcludedFormatted: '1,01',
|
|
169
|
+
});
|
|
170
|
+
expect(result.data.vats).toEqual(
|
|
171
|
+
expect.arrayContaining([expect.objectContaining({ base: 101, baseFormatted: '1,01' })]),
|
|
172
|
+
);
|
|
173
|
+
expect(result.data.totalTaxExcluded).toBe(101);
|
|
174
|
+
expect(result.data.totalTaxExcludedFormatted).toBe('1,01');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('should round negative credit note lines symmetrically across VAT rates', () => {
|
|
178
|
+
const payload = {
|
|
179
|
+
...payloadWithoutSellerAddress,
|
|
180
|
+
products: [
|
|
181
|
+
{
|
|
182
|
+
idVat: Vat.TX_5_5.id,
|
|
183
|
+
unit: Unit.ITEM.id,
|
|
184
|
+
itemSaleName: 'Half-cent VAT line',
|
|
185
|
+
priceTaxExcluded: -200,
|
|
186
|
+
quantity: 0.5,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
idVat: Vat.TX_20.id,
|
|
190
|
+
unit: Unit.ITEM.id,
|
|
191
|
+
itemSaleName: 'Half-cent line',
|
|
192
|
+
priceTaxExcluded: -201,
|
|
193
|
+
quantity: 0.5,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
} as unknown as CreateInvoiceDocument;
|
|
197
|
+
|
|
198
|
+
const result = generateCommercialDocument(EstimateInvoiceType.AVOIR, payload);
|
|
199
|
+
|
|
200
|
+
expect(result.body).toContain('- 1,00€');
|
|
201
|
+
expect(result.body).toContain('- 1,01€');
|
|
202
|
+
expect(result.data.vats).toEqual(
|
|
203
|
+
expect.arrayContaining([
|
|
204
|
+
expect.objectContaining({ value: 5.5, base: -100, amount: -6, baseFormatted: '- 1,00' }),
|
|
205
|
+
expect.objectContaining({ value: 20, base: -101, amount: -20, baseFormatted: '- 1,01' }),
|
|
206
|
+
]),
|
|
207
|
+
);
|
|
208
|
+
expect(result.data.totalTaxExcluded).toBe(-201);
|
|
209
|
+
expect(result.data.totalTaxIncluded).toBe(-227);
|
|
210
|
+
});
|
|
116
211
|
});
|
|
@@ -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
|
+
}
|