Skip to content

Commit 57aa01e

Browse files
committed
feat(locale): implement specialized French providers and improve IT locale
Introduce dedicated providers for French VAT (TVA), National Identity Card (CNI), and Passport numbers to replace generic English fallbacks. Improve Italian locale support by establishing its own provider factory structure. - add FrVATIdentificationNumberProvider with Luhn-compliant SIREN logic - add FrNationalIdentityCardNumberProvider following biometric 2021 format - add FrPassportNumberProvider with correct alphanumeric pattern - refactor LocaleSpecificProvidersFactory to register new FR providers - fix IT locale to use createItProviders (currently with EN fallbacks) - simplify SvNationalIdentityCardNumberProvider by removing unused DateProducer - update FairyFrSpec with new expected values and comprehensive validation for Luhn and format patterns
1 parent b07303f commit 57aa01e

6 files changed

Lines changed: 253 additions & 12 deletions

File tree

src/main/java/com/devskiller/jfairy/LocaleSpecificProvidersFactory.java

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.devskiller.jfairy.producer.company.locale.de.DeVATIdentificationNumberProvider;
1313
import com.devskiller.jfairy.producer.company.locale.en.EnVATIdentificationNumberProvider;
1414
import com.devskiller.jfairy.producer.company.locale.es.EsVATIdentificationNumberProvider;
15+
import com.devskiller.jfairy.producer.company.locale.fr.FrVATIdentificationNumberProvider;
1516
import com.devskiller.jfairy.producer.company.locale.ka.KaVATIdentificationNumberProvider;
1617
import com.devskiller.jfairy.producer.company.locale.pl.PlVATIdentificationNumberProvider;
1718
import com.devskiller.jfairy.producer.company.locale.sk.SkVATIdentificationNumberProvider;
@@ -32,6 +33,8 @@
3233
import com.devskiller.jfairy.producer.person.locale.es.EsNationalIdentityCardNumberProvider;
3334
import com.devskiller.jfairy.producer.person.locale.es.EsPassportNumberProvider;
3435
import com.devskiller.jfairy.producer.person.locale.fr.FrAddressProvider;
36+
import com.devskiller.jfairy.producer.person.locale.fr.FrNationalIdentityCardNumberProvider;
37+
import com.devskiller.jfairy.producer.person.locale.fr.FrPassportNumberProvider;
3538
import com.devskiller.jfairy.producer.person.locale.ka.KaAddressProvider;
3639
import com.devskiller.jfairy.producer.person.locale.ka.KaNationalIdentityCardNumberProvider;
3740
import com.devskiller.jfairy.producer.person.locale.ka.KaPassportNumberProvider;
@@ -79,7 +82,7 @@ static LocaleSpecificProviders createProvidersForLocale(Locale locale,
7982
case EN -> createEnProviders(dataMaster, baseProducer, dateProducer);
8083
case ES -> createEsProviders(dataMaster, baseProducer, dateProducer);
8184
case FR -> createFrProviders(dataMaster, baseProducer, dateProducer);
82-
case IT -> createEnProviders(dataMaster, baseProducer, dateProducer);
85+
case IT -> createItProviders(dataMaster, baseProducer, dateProducer);
8386
case SK -> createSkProviders(dataMaster, baseProducer, dateProducer);
8487
case SV -> createSvProviders(dataMaster, baseProducer, dateProducer);
8588
case ZH -> createZhProviders(dataMaster, baseProducer, dateProducer);
@@ -126,18 +129,48 @@ private static LocaleSpecificProviders createEnProviders(DataMaster dataMaster,
126129
);
127130
}
128131

132+
/**
133+
* Creates providers for French locale.
134+
*
135+
* @param dataMaster data source
136+
* @param baseProducer base producer
137+
* @param dateProducer date producer
138+
* @return French specific providers
139+
*/
129140
private static LocaleSpecificProviders createFrProviders(DataMaster dataMaster,
130141
BaseProducer baseProducer,
131142
DateProducer dateProducer) {
132143
return new LocaleSpecificProviders(
133144
new NoNationalIdentificationNumberFactory(),
134-
new EnNationalIdentityCardNumberProvider(baseProducer),
135-
new EnVATIdentificationNumberProvider(baseProducer),
145+
new FrNationalIdentityCardNumberProvider(baseProducer),
146+
new FrVATIdentificationNumberProvider(baseProducer),
136147
new FrAddressProvider(dataMaster, baseProducer),
137-
new EnPassportNumberProvider(baseProducer)
148+
new FrPassportNumberProvider(baseProducer)
138149
);
139150
}
140151

152+
/**
153+
* Creates providers for Italian locale.
154+
* <p>
155+
* Note: Currently uses English fallbacks.
156+
*
157+
* @param dataMaster data source
158+
* @param baseProducer base producer
159+
* @param dateProducer date producer
160+
* @return Italian specific providers
161+
*/
162+
private static LocaleSpecificProviders createItProviders(DataMaster dataMaster,
163+
BaseProducer baseProducer,
164+
DateProducer dateProducer) {
165+
return new LocaleSpecificProviders(
166+
new NoNationalIdentificationNumberFactory(),
167+
new EnNationalIdentityCardNumberProvider(baseProducer),
168+
new EnVATIdentificationNumberProvider(baseProducer),
169+
new EnAddressProvider(dataMaster, baseProducer),
170+
new EnPassportNumberProvider(baseProducer)
171+
);
172+
}
173+
141174
private static LocaleSpecificProviders createEsProviders(DataMaster dataMaster,
142175
BaseProducer baseProducer,
143176
DateProducer dateProducer) {
@@ -156,7 +189,7 @@ private static LocaleSpecificProviders createSvProviders(DataMaster dataMaster,
156189
NationalIdentificationNumberFactory nationalIdFactory = new SvNationalIdentificationNumberFactory(baseProducer, dateProducer);
157190
return new LocaleSpecificProviders(
158191
nationalIdFactory,
159-
new SvNationalIdentityCardNumberProvider(dateProducer, baseProducer),
192+
new SvNationalIdentityCardNumberProvider(baseProducer),
160193
new SvVATIdentificationNumberProvider(baseProducer, dateProducer, nationalIdFactory),
161194
new SvAddressProvider(dataMaster, baseProducer),
162195
new SvPassportNumberProvider(baseProducer)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.devskiller.jfairy.producer.company.locale.fr;
2+
3+
import com.devskiller.jfairy.producer.BaseProducer;
4+
import com.devskiller.jfairy.producer.VATIdentificationNumberProvider;
5+
import com.devskiller.jfairy.producer.person.Country;
6+
7+
/**
8+
* Provider for French VAT identification numbers (TVA intracommunautaire).
9+
* <p>
10+
* Format: FR + 2 digits (key) + 9 digits (SIREN).<br>
11+
* The "SIREN" satisfies the Luhn algorithm.
12+
*
13+
* @author Markus Spann
14+
* @since 0.8.3
15+
*/
16+
public class FrVATIdentificationNumberProvider implements VATIdentificationNumberProvider {
17+
18+
private final BaseProducer baseProducer;
19+
20+
public FrVATIdentificationNumberProvider(BaseProducer baseProducer) {
21+
this.baseProducer = baseProducer;
22+
}
23+
24+
@Override
25+
public String get() {
26+
String siren = generateLuhnCompliantSiren();
27+
int key = (12 + 3 * (Integer.parseInt(siren) % 97)) % 97;
28+
29+
return Country.France.getCode() + String.format("%02d%s", key, siren);
30+
}
31+
32+
/**
33+
* Generates a 9-digit SIREN number that satisfies the Luhn algorithm.
34+
*
35+
* @return a valid 9-digit SIREN string
36+
*/
37+
private String generateLuhnCompliantSiren() {
38+
int[] digits = new int[9];
39+
int sum = 0;
40+
41+
// generate first 8 digits in a single random call
42+
String base = baseProducer.randomNumeric(8);
43+
for (int i = 0; i < 8; i++) {
44+
digits[i] = Character.digit(base.charAt(i), 10);
45+
}
46+
47+
// calculate Luhn sum for the first 8 digits
48+
// for a 9-digit number, doubling every second digit from the right
49+
// means doubling digits at indices 7, 5, 3, 1 (0-based from left).
50+
for (int i = 0; i < 8; i++) {
51+
int digit = digits[i];
52+
if (i % 2 != 0) {
53+
digit *= 2;
54+
if (digit > 9) {
55+
digit -= 9;
56+
}
57+
}
58+
sum += digit;
59+
}
60+
61+
// calculate and set the check digit (9th digit)
62+
int checkDigit = (10 - (sum % 10)) % 10;
63+
digits[8] = checkDigit;
64+
65+
// build the resulting string
66+
StringBuilder sb = new StringBuilder(9);
67+
for (int d : digits) {
68+
sb.append(d);
69+
}
70+
return sb.toString();
71+
}
72+
73+
}
74+
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.devskiller.jfairy.producer.person.locale.fr;
2+
3+
import java.time.Year;
4+
5+
import com.devskiller.jfairy.producer.BaseProducer;
6+
import com.devskiller.jfairy.producer.person.NationalIdentityCardNumberProvider;
7+
8+
/**
9+
* Provider for French National Identity Card numbers (Carte Nationale
10+
* d’Identité - CNI).
11+
*
12+
* <p>
13+
* Generates a 12-character alphanumeric string following the biometric format
14+
* introduced in 2021:
15+
* <ul>
16+
* <li>Department code (2 digits, 01-95)</li>
17+
* <li>Year of issuance (last 2 digits)</li>
18+
* <li>Month of issuance (01-12)</li>
19+
* <li>Random serial (6 alphanumeric characters)</li>
20+
* </ul>
21+
*
22+
* @author Markus Spann
23+
* @since 0.8.3
24+
*/
25+
public class FrNationalIdentityCardNumberProvider implements NationalIdentityCardNumberProvider {
26+
27+
private final BaseProducer baseProducer;
28+
29+
public FrNationalIdentityCardNumberProvider(BaseProducer baseProducer) {
30+
this.baseProducer = baseProducer;
31+
}
32+
33+
@Override
34+
public String get() {
35+
// department (01-95)
36+
int dept = baseProducer.randomBetween(1, 95);
37+
38+
// year (last two digits, from 2021 to current year)
39+
int currentYear = Year.now().getValue() % 100;
40+
int year = baseProducer.randomBetween(21, currentYear);
41+
42+
// month (01-12)
43+
int month = baseProducer.randomBetween(1, 12);
44+
45+
// random suffix (6 alphanumeric chars)
46+
String suffix = baseProducer.randomAlphanumeric(6);
47+
48+
return String.format("%02d%02d%02d%s", dept, year, month, suffix);
49+
}
50+
51+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.devskiller.jfairy.producer.person.locale.fr;
2+
3+
import com.devskiller.jfairy.producer.BaseProducer;
4+
import com.devskiller.jfairy.producer.person.PassportNumberProvider;
5+
6+
/**
7+
* Provider for French Passport numbers.
8+
* <p>
9+
* Generates a 9-character alphanumeric string following the French biometric
10+
* passport format: 2 digits, 2 uppercase letters, and 5 digits (e.g., 12AB12345).
11+
*
12+
* @author Markus Spann
13+
* @since 0.8.3
14+
*/
15+
public class FrPassportNumberProvider implements PassportNumberProvider {
16+
17+
private final BaseProducer baseProducer;
18+
19+
public FrPassportNumberProvider(BaseProducer baseProducer) {
20+
this.baseProducer = baseProducer;
21+
}
22+
23+
@Override
24+
public String get() {
25+
return baseProducer.randomNumeric(2)
26+
+ baseProducer.randomAlphabetic(2)
27+
+ baseProducer.randomNumeric(5);
28+
}
29+
30+
}

src/main/java/com/devskiller/jfairy/producer/person/locale/sv/SvNationalIdentityCardNumberProvider.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
package com.devskiller.jfairy.producer.person.locale.sv;
22

33
import com.devskiller.jfairy.producer.BaseProducer;
4-
import com.devskiller.jfairy.producer.DateProducer;
54
import com.devskiller.jfairy.producer.person.NationalIdentityCardNumberProvider;
65

76
/**
87
* Swedish Identity Card Number (random number implementation)
98
*/
109
public class SvNationalIdentityCardNumberProvider implements NationalIdentityCardNumberProvider {
1110

12-
private final DateProducer dateProducer;
1311
private final BaseProducer baseProducer;
1412

15-
public SvNationalIdentityCardNumberProvider(DateProducer dateProducer, BaseProducer baseProducer) {
16-
this.dateProducer = dateProducer;
13+
public SvNationalIdentityCardNumberProvider(BaseProducer baseProducer) {
1714
this.baseProducer = baseProducer;
1815
}
1916

src/test/groovy/com/devskiller/jfairy/FairyFrSpec.groovy

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,80 @@ package com.devskiller.jfairy
33
import spock.lang.Specification
44

55
import com.devskiller.jfairy.producer.person.Person
6+
import com.devskiller.jfairy.producer.company.Company
67

8+
/**
9+
* <p>
10+
* Test suite for French locale.
11+
* <p>
12+
* Note: Expected values reflect the random sequence for SEED 7 after
13+
* introducing Luhn-compliant VAT and specific Passport providers.
14+
*/
715
class FairyFrSpec extends Specification {
816

917
private final int SEED = 7
10-
private Fairy fairy = Fairy.builder().withRandomSeed(SEED).withLocale(Locale.FRENCH).build()
18+
private Fairy fairy = Fairy.builder()
19+
.withRandomSeed(SEED)
20+
.withLocale(Locale.FRENCH)
21+
.build()
1122

1223
def "Should create French name"() {
1324
when:
1425
Person person = fairy.person()
1526
then:
16-
person.fullName == 'Flavien Garcia'
27+
person.fullName == 'Alexis Tanguy'
1728
}
1829

1930
def "Should create French city"() {
2031
when:
2132
Person person = fairy.person()
2233
then:
23-
person.address.city == 'Malakoff'
34+
person.address.city == 'Saint-Denis'
2435
}
2536

37+
def "Should create valid French passport number"() {
38+
when:
39+
Person person = fairy.person()
40+
then:
41+
person.passportNumber ==~ /\d{2}[A-Z]{2}\d{5}/
42+
person.passportNumber == '28DO74569'
43+
}
44+
45+
def "Should create valid French national identity card (CNI)"() {
46+
when:
47+
Person person = fairy.person()
48+
then:
49+
person.nationalIdentityCardNumber ==~ /88\d{2}050OLUQS/
50+
}
51+
52+
def "Should create valid French VAT (TVA) number"() {
53+
given:
54+
Company company = fairy.company()
55+
when:
56+
String vat = company.vatIdentificationNumber
57+
then:
58+
vat == 'FR44489040287'
59+
and: "verify TVA key logic"
60+
String siren = vat.substring(4)
61+
int key = Integer.parseInt(vat.substring(2, 4))
62+
key == (12 + 3 * (Long.parseLong(siren) % 97)) % 97
63+
}
64+
65+
def "Should satisfy Luhn algorithm for SIREN"() {
66+
given:
67+
Company company = fairy.company()
68+
String siren = company.vatIdentificationNumber.substring(4)
69+
when:
70+
int sum = 0
71+
siren.reverse().eachWithIndex { digits, i ->
72+
int n = Integer.parseInt(digits)
73+
if (i % 2 != 0) {
74+
n *= 2
75+
if (n > 9) n -= 9
76+
}
77+
sum += n
78+
}
79+
then:
80+
sum % 10 == 0
81+
}
2682
}

0 commit comments

Comments
 (0)