@arsedizioni/ars-utils 22.5.72 → 22.6.75
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/fesm2022/arsedizioni-ars-utils-core.validators.mjs +251 -2
- package/fesm2022/arsedizioni-ars-utils-core.validators.mjs.map +1 -1
- package/fesm2022/arsedizioni-ars-utils-ui.navigation.mjs +2 -2
- package/fesm2022/arsedizioni-ars-utils-ui.navigation.mjs.map +1 -1
- package/package.json +1 -1
- package/types/arsedizioni-ars-utils-core.validators.d.ts +60 -1
|
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { input, Directive, forwardRef, effect, inject, DestroyRef } from '@angular/core';
|
|
3
3
|
import { NG_VALIDATORS } from '@angular/forms';
|
|
4
4
|
import { SystemUtils } from '@arsedizioni/ars-utils/core';
|
|
5
|
-
import { endOfDay } from 'date-fns';
|
|
5
|
+
import { endOfDay, startOfDay } from 'date-fns';
|
|
6
6
|
import { validate } from '@angular/forms/signals';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -732,6 +732,155 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
732
732
|
}]
|
|
733
733
|
}] });
|
|
734
734
|
|
|
735
|
+
/**
|
|
736
|
+
* @file italian.ts
|
|
737
|
+
*
|
|
738
|
+
* The two check algorithms behind the `italianVat` and `italianFiscalCode` rules, kept apart from
|
|
739
|
+
* the validators that use them for the same reason `emptiness.ts` is: the rule must exist in one
|
|
740
|
+
* place only, so that no second copy of it can drift. They pull in nothing — not `@angular/forms`,
|
|
741
|
+
* not `@angular/core` — and they live here rather than in `SystemUtils` so that `core`, which sits
|
|
742
|
+
* on the boot path of every application, does not carry code only a form needs.
|
|
743
|
+
*/
|
|
744
|
+
/**
|
|
745
|
+
* Digits an "omocodia" substitution replaces with a letter, in digit order: `0` becomes `L`, `1`
|
|
746
|
+
* becomes `M`, and so on.
|
|
747
|
+
*
|
|
748
|
+
* The Agenzia delle Entrate applies it when two people would otherwise share the same fiscal code,
|
|
749
|
+
* so a perfectly legitimate code can carry letters where the format expects numbers. Ignoring this
|
|
750
|
+
* is the classic way a validator rejects a real person.
|
|
751
|
+
*/
|
|
752
|
+
const OMOCODIA_CHARS = 'LMNPQRSTUV';
|
|
753
|
+
/** Letters usable as month of birth, one per month from January (`A`) to December (`T`). */
|
|
754
|
+
const MONTH_CHARS = 'ABCDEHLMPRST';
|
|
755
|
+
/** Alphabet the control character of a fiscal code is drawn from. */
|
|
756
|
+
const CONTROL_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
757
|
+
/** Weight of each character sitting in an ODD position (1-based) of the first 15 of a fiscal code. */
|
|
758
|
+
const ODD_WEIGHTS = {
|
|
759
|
+
'0': 1, '1': 0, '2': 5, '3': 7, '4': 9, '5': 13, '6': 15, '7': 17, '8': 19, '9': 21,
|
|
760
|
+
A: 1, B: 0, C: 5, D: 7, E: 9, F: 13, G: 15, H: 17, I: 19, J: 21, K: 2, L: 4, M: 18,
|
|
761
|
+
N: 20, O: 11, P: 3, Q: 6, R: 8, S: 12, T: 14, U: 16, V: 10, W: 22, X: 25, Y: 24, Z: 23,
|
|
762
|
+
};
|
|
763
|
+
/** Weight of each character sitting in an EVEN position (1-based) of the first 15 of a fiscal code. */
|
|
764
|
+
const EVEN_WEIGHTS = {
|
|
765
|
+
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
|
|
766
|
+
A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, H: 7, I: 8, J: 9, K: 10, L: 11, M: 12,
|
|
767
|
+
N: 13, O: 14, P: 15, Q: 16, R: 17, S: 18, T: 19, U: 20, V: 21, W: 22, X: 23, Y: 24, Z: 25,
|
|
768
|
+
};
|
|
769
|
+
/**
|
|
770
|
+
* Shape of a fiscal code of a natural person, omocodia included: every group that is normally
|
|
771
|
+
* numeric also admits the ten letters that stand in for a digit.
|
|
772
|
+
*/
|
|
773
|
+
const FISCAL_CODE_REGEX = /^[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$/;
|
|
774
|
+
/**
|
|
775
|
+
* Strips what a user types around a code without changing the code itself: surrounding and inner
|
|
776
|
+
* spaces, dots and dashes, and the letter case.
|
|
777
|
+
*
|
|
778
|
+
* Accepting a pasted `"IT 123 456 789 01"` is not indulgence — the value arrives from an invoice,
|
|
779
|
+
* a registry export or a colleague's e-mail far more often than it is typed character by
|
|
780
|
+
* character, and rejecting it teaches nothing to the person who pasted it.
|
|
781
|
+
*
|
|
782
|
+
* @param value - The raw value as the user entered it.
|
|
783
|
+
* @returns The value in upper case with spaces, dots and dashes removed.
|
|
784
|
+
*/
|
|
785
|
+
function normalize(value) {
|
|
786
|
+
return (value ?? '').toUpperCase().replace(/[\s.\-/]/g, '');
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Turns the letters an omocodia substitution left behind back into the digits they stand for.
|
|
790
|
+
*
|
|
791
|
+
* @param value - A group of characters of a fiscal code that is numeric in the plain form.
|
|
792
|
+
* @returns The same group with every substitution letter replaced by its digit.
|
|
793
|
+
*/
|
|
794
|
+
function decodeOmocodia(value) {
|
|
795
|
+
let decoded = '';
|
|
796
|
+
for (const char of value) {
|
|
797
|
+
const index = OMOCODIA_CHARS.indexOf(char);
|
|
798
|
+
decoded += index === -1 ? char : String(index);
|
|
799
|
+
}
|
|
800
|
+
return decoded;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Tells whether a value is a valid Italian VAT number (partita IVA).
|
|
804
|
+
*
|
|
805
|
+
* Eleven digits, of which the last one is the check digit of the Luhn-like algorithm of
|
|
806
|
+
* D.P.R. 633/72: the digits in even position (1-based) are doubled and folded back under ten, the
|
|
807
|
+
* sum is completed to the next multiple of ten, and what is missing is the check digit. An
|
|
808
|
+
* optional `IT` prefix is accepted, because that is how the number travels on invoices and in
|
|
809
|
+
* VIES exports.
|
|
810
|
+
*
|
|
811
|
+
* The seven digits of the sequential number are required not to be all zeros, which is the one
|
|
812
|
+
* combination the checksum happily accepts and the Agenzia delle Entrate never issues. The office
|
|
813
|
+
* code (positions 8 to 10) is deliberately NOT checked against the list of the provinces: that
|
|
814
|
+
* list grows, and a validator that rejects a number issued last month is worse than one that lets
|
|
815
|
+
* a fabricated office code through.
|
|
816
|
+
*
|
|
817
|
+
* @param value - The value to check, with or without the `IT` prefix and any spacing.
|
|
818
|
+
* @returns `true` when the value is a well-formed VAT number.
|
|
819
|
+
*/
|
|
820
|
+
function isValidItalianVat(value) {
|
|
821
|
+
const normalized = normalize(value);
|
|
822
|
+
const digits = normalized.startsWith('IT') ? normalized.slice(2) : normalized;
|
|
823
|
+
if (!/^\d{11}$/.test(digits))
|
|
824
|
+
return false;
|
|
825
|
+
if (digits.startsWith('0000000'))
|
|
826
|
+
return false;
|
|
827
|
+
let sum = 0;
|
|
828
|
+
for (let i = 0; i < 10; i++) {
|
|
829
|
+
let digit = digits.charCodeAt(i) - 48;
|
|
830
|
+
// Positions are 1-based in the norm, so the doubled ones are those at an even index here.
|
|
831
|
+
if (i % 2 === 1) {
|
|
832
|
+
digit *= 2;
|
|
833
|
+
if (digit > 9)
|
|
834
|
+
digit -= 9;
|
|
835
|
+
}
|
|
836
|
+
sum += digit;
|
|
837
|
+
}
|
|
838
|
+
const check = (10 - (sum % 10)) % 10;
|
|
839
|
+
return check === digits.charCodeAt(10) - 48;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Tells whether a value is a valid Italian fiscal code (codice fiscale), in either of the two
|
|
843
|
+
* shapes a form can receive.
|
|
844
|
+
*
|
|
845
|
+
* A natural person has the sixteen-character code of D.M. 23/12/1976: the shape is checked, the
|
|
846
|
+
* month letter and the day of birth are checked (41 to 71 for a woman, which is the day plus
|
|
847
|
+
* forty), and the sixteenth character is recomputed from the first fifteen and compared. Letters
|
|
848
|
+
* left by an omocodia substitution are accepted and decoded before the date is judged, so a
|
|
849
|
+
* legitimately altered code is not turned away.
|
|
850
|
+
*
|
|
851
|
+
* A company or a public body has no such code: its fiscal code IS its VAT number, eleven digits
|
|
852
|
+
* checked by {@link isValidItalianVat}. Both are accepted here because a single "codice fiscale"
|
|
853
|
+
* field in a registry form receives both, and asking the user which kind they are about to type is
|
|
854
|
+
* a question the code can answer by itself.
|
|
855
|
+
*
|
|
856
|
+
* @param value - The value to check, in any case and with any spacing.
|
|
857
|
+
* @returns `true` when the value is a well-formed fiscal code of either shape.
|
|
858
|
+
*/
|
|
859
|
+
function isValidItalianFiscalCode(value) {
|
|
860
|
+
const code = normalize(value);
|
|
861
|
+
// A company or a public body: its fiscal code is the VAT number.
|
|
862
|
+
if (/^\d{11}$/.test(code))
|
|
863
|
+
return isValidItalianVat(code);
|
|
864
|
+
if (code.length !== 16 || !FISCAL_CODE_REGEX.test(code))
|
|
865
|
+
return false;
|
|
866
|
+
// The day carries the sex: 01-31 for a man, 41-71 for a woman (the day plus forty).
|
|
867
|
+
const day = parseInt(decodeOmocodia(code.substring(9, 11)), 10);
|
|
868
|
+
const isValidDay = (day >= 1 && day <= 31) || (day >= 41 && day <= 71);
|
|
869
|
+
if (!isValidDay)
|
|
870
|
+
return false;
|
|
871
|
+
// The month letter is already constrained by the regex; this keeps the two in step if it changes.
|
|
872
|
+
if (!MONTH_CHARS.includes(code[8]))
|
|
873
|
+
return false;
|
|
874
|
+
let sum = 0;
|
|
875
|
+
for (let i = 0; i < 15; i++) {
|
|
876
|
+
// Odd positions in the norm are the even indexes here. The character is weighted as it is
|
|
877
|
+
// written, substitution letters included: the control character is computed on the altered
|
|
878
|
+
// code, not on the original one.
|
|
879
|
+
sum += i % 2 === 0 ? ODD_WEIGHTS[code[i]] : EVEN_WEIGHTS[code[i]];
|
|
880
|
+
}
|
|
881
|
+
return CONTROL_CHARS[sum % 26] === code[15];
|
|
882
|
+
}
|
|
883
|
+
|
|
735
884
|
/**
|
|
736
885
|
* Default texts of the rules declared here, in one place so that the validators and the fallback
|
|
737
886
|
* map of `SignalsUtils.getFieldErrorMessage` cannot drift apart: a rule added below without a
|
|
@@ -757,6 +906,7 @@ const ARS_VALIDATOR_MESSAGES = {
|
|
|
757
906
|
date: 'Data non valida',
|
|
758
907
|
dateRange: 'Intervallo non valido',
|
|
759
908
|
notFuture: 'La data non può essere futura',
|
|
909
|
+
notPast: 'La data non può essere passata',
|
|
760
910
|
url: 'Indirizzo non valido',
|
|
761
911
|
maxTerms: 'Troppe parole',
|
|
762
912
|
fileSize: 'Dimensione del file non ammessa',
|
|
@@ -765,6 +915,8 @@ const ARS_VALIDATOR_MESSAGES = {
|
|
|
765
915
|
time: 'Orario non valido',
|
|
766
916
|
equals: 'I due valori non coincidono',
|
|
767
917
|
otp: 'Codice non valido',
|
|
918
|
+
italianVat: 'Partita IVA non valida',
|
|
919
|
+
italianFiscalCode: 'Codice fiscale non valido',
|
|
768
920
|
};
|
|
769
921
|
/**
|
|
770
922
|
* Requires the value to be a well-formed GUID / UUID.
|
|
@@ -1006,6 +1158,38 @@ function notFuture(path, config) {
|
|
|
1006
1158
|
return endOfDay(parsed) <= endOfDay(new Date()) ? null : invalid;
|
|
1007
1159
|
});
|
|
1008
1160
|
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Requires the value to be a date that is not in the past.
|
|
1163
|
+
*
|
|
1164
|
+
* The mirror image of {@link notFuture}, for the fields that can only look forward: an expiry, a
|
|
1165
|
+
* deadline, the date of a course still to be held. Today counts as valid — the comparison is made
|
|
1166
|
+
* on the start of the day, so a date picked this afternoon for today does not become invalid
|
|
1167
|
+
* because the clock has moved on.
|
|
1168
|
+
*
|
|
1169
|
+
* Like {@link date}, and unlike the older {@link notFuture}, it accepts both the `Date` a Material
|
|
1170
|
+
* datepicker writes into the model and the text of a plain input. An empty value passes: saying
|
|
1171
|
+
* "obbligatorio" is the job of `required()`.
|
|
1172
|
+
*
|
|
1173
|
+
* @param path - Path of the field to validate.
|
|
1174
|
+
* @param config - Optional message override and `when` condition.
|
|
1175
|
+
* @returns void
|
|
1176
|
+
* @example
|
|
1177
|
+
* const f = form(this.model, p => { required(p.scadenza); notPast(p.scadenza); });
|
|
1178
|
+
*/
|
|
1179
|
+
function notPast(path, config) {
|
|
1180
|
+
validate(path, ctx => {
|
|
1181
|
+
if (config?.when && !config.when(ctx))
|
|
1182
|
+
return null;
|
|
1183
|
+
const input = ctx.value();
|
|
1184
|
+
if (input === undefined || input === null || input === '')
|
|
1185
|
+
return null;
|
|
1186
|
+
const parsed = SystemUtils.parseDate(input);
|
|
1187
|
+
const invalid = { kind: 'notPast', message: config?.message ?? ARS_VALIDATOR_MESSAGES['notPast'] };
|
|
1188
|
+
if (!parsed)
|
|
1189
|
+
return invalid;
|
|
1190
|
+
return startOfDay(parsed) >= startOfDay(new Date()) ? null : invalid;
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1009
1193
|
/**
|
|
1010
1194
|
* Requires the value to be a well-formed URL. An empty value passes, as everywhere else here.
|
|
1011
1195
|
*
|
|
@@ -1231,6 +1415,71 @@ function otp(path, config) {
|
|
|
1231
1415
|
: { kind: 'otp', message: config?.message ?? ARS_VALIDATOR_MESSAGES['otp'] };
|
|
1232
1416
|
});
|
|
1233
1417
|
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Requires the value to be a valid Italian VAT number (partita IVA).
|
|
1420
|
+
*
|
|
1421
|
+
* Eleven digits whose last one is the check digit of D.P.R. 633/72, so a mistyped or invented
|
|
1422
|
+
* number is caught here rather than by the backend that will refuse the invoice. An `IT` prefix
|
|
1423
|
+
* and the spacing of a pasted value are tolerated, because that is the shape the number arrives
|
|
1424
|
+
* in; what is stored is still whatever the user typed, since this rule only judges.
|
|
1425
|
+
*
|
|
1426
|
+
* An empty value passes, as everywhere else here: declaring the field mandatory is `required()`'s
|
|
1427
|
+
* job.
|
|
1428
|
+
*
|
|
1429
|
+
* @param path - Path of the field to validate.
|
|
1430
|
+
* @param config - Optional message override and `when` condition.
|
|
1431
|
+
* @returns void
|
|
1432
|
+
* @example
|
|
1433
|
+
* const f = form(this.model, p => { required(p.partitaIva); italianVat(p.partitaIva); });
|
|
1434
|
+
*/
|
|
1435
|
+
function italianVat(path, config) {
|
|
1436
|
+
validate(path, ctx => {
|
|
1437
|
+
if (config?.when && !config.when(ctx))
|
|
1438
|
+
return null;
|
|
1439
|
+
const input = ctx.value();
|
|
1440
|
+
if (!input || input.length === 0)
|
|
1441
|
+
return null;
|
|
1442
|
+
return isValidItalianVat(input)
|
|
1443
|
+
? null
|
|
1444
|
+
: { kind: 'italianVat', message: config?.message ?? ARS_VALIDATOR_MESSAGES['italianVat'] };
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Requires the value to be a valid Italian fiscal code (codice fiscale).
|
|
1449
|
+
*
|
|
1450
|
+
* Both shapes a registry form receives are accepted: the sixteen-character code of a natural
|
|
1451
|
+
* person, control character recomputed and compared, and the eleven digits of a company or a
|
|
1452
|
+
* public body, whose fiscal code IS its VAT number. Nothing has to tell the rule which one is
|
|
1453
|
+
* coming — the value says it by its own length.
|
|
1454
|
+
*
|
|
1455
|
+
* Codes altered by an "omocodia" substitution (digits replaced by letters when two people would
|
|
1456
|
+
* otherwise share a code) are accepted: they are issued by the Agenzia delle Entrate and rejecting
|
|
1457
|
+
* them is the classic way a form turns away a real person.
|
|
1458
|
+
*
|
|
1459
|
+
* An empty value passes. Pair with `required()` when the field is mandatory, and reach for
|
|
1460
|
+
* {@link italianVat} instead on a field that is specifically a VAT number.
|
|
1461
|
+
*
|
|
1462
|
+
* @param path - Path of the field to validate.
|
|
1463
|
+
* @param config - Optional message override and `when` condition.
|
|
1464
|
+
* @returns void
|
|
1465
|
+
* @example
|
|
1466
|
+
* const f = form(this.model, p => { required(p.codiceFiscale); italianFiscalCode(p.codiceFiscale); });
|
|
1467
|
+
*/
|
|
1468
|
+
function italianFiscalCode(path, config) {
|
|
1469
|
+
validate(path, ctx => {
|
|
1470
|
+
if (config?.when && !config.when(ctx))
|
|
1471
|
+
return null;
|
|
1472
|
+
const input = ctx.value();
|
|
1473
|
+
if (!input || input.length === 0)
|
|
1474
|
+
return null;
|
|
1475
|
+
return isValidItalianFiscalCode(input)
|
|
1476
|
+
? null
|
|
1477
|
+
: {
|
|
1478
|
+
kind: 'italianFiscalCode',
|
|
1479
|
+
message: config?.message ?? ARS_VALIDATOR_MESSAGES['italianFiscalCode'],
|
|
1480
|
+
};
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1234
1483
|
/** Helpers around signal forms that are not validators themselves. */
|
|
1235
1484
|
class SignalsUtils {
|
|
1236
1485
|
/**
|
|
@@ -1290,5 +1539,5 @@ class SignalsUtils {
|
|
|
1290
1539
|
* Generated bundle index. Do not edit.
|
|
1291
1540
|
*/
|
|
1292
1541
|
|
|
1293
|
-
export { ARS_VALIDATOR_MESSAGES, EmailsValidatorDirective, EqualsValidatorDirective, FileSizeValidatorDirective, GuidValidatorDirective, MIN_VALID_YEAR, MaxTermsValidatorDirective, NotEmptyValidatorDirective, NotEqualValidatorDirective, NotFutureValidatorDirective, PasswordValidatorDirective, SignalsUtils, SqlDateValidatorDirective, TimeValidatorDirective, UrlValidatorDirective, ValidIfDirective, ValidatorDirective, date, dateRange, emails, equals, fileSize, guid, maxTerms, notEmpty, notEqual, notFuture, otp, password, sqlDate, time, url, validIf };
|
|
1542
|
+
export { ARS_VALIDATOR_MESSAGES, EmailsValidatorDirective, EqualsValidatorDirective, FileSizeValidatorDirective, GuidValidatorDirective, MIN_VALID_YEAR, MaxTermsValidatorDirective, NotEmptyValidatorDirective, NotEqualValidatorDirective, NotFutureValidatorDirective, PasswordValidatorDirective, SignalsUtils, SqlDateValidatorDirective, TimeValidatorDirective, UrlValidatorDirective, ValidIfDirective, ValidatorDirective, date, dateRange, emails, equals, fileSize, guid, italianFiscalCode, italianVat, maxTerms, notEmpty, notEqual, notFuture, notPast, otp, password, sqlDate, time, url, validIf };
|
|
1294
1543
|
//# sourceMappingURL=arsedizioni-ars-utils-core.validators.mjs.map
|