@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.
@@ -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