diff --git a/.gitignore b/.gitignore
index 5077d27..51689c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ target
/dependency-reduced-pom.xml
META-INF/
bin/
+**/settings.local.json
diff --git a/specs/MARKDOWN_FAKER_SPEC.md b/specs/MARKDOWN_FAKER_SPEC.md
new file mode 100644
index 0000000..88d1a4d
--- /dev/null
+++ b/specs/MARKDOWN_FAKER_SPEC.md
@@ -0,0 +1,672 @@
+# SPEC: Markdown Faker
+
+COMPONENT: Markdown faker for realistic Markdown-formatted content (headers, paragraphs, lists, code blocks, links, images, tables, blockquotes)
+DEPENDENCIES: Lorem faker for text content
+EXISTING: src/main/resources/en/markdown.yml (extend with additional data)
+
+## FILES
+
+YAML_DATA: src/main/resources/en/markdown.yml
+YAML_REGISTRATION: src/main/java/org/thejavaguy/javafaker/service/files/EnFile.java (add "markdown.yml" to FILES list)
+JAVA_CLASS: src/main/java/org/thejavaguy/javafaker/Markdown.java
+TEST_CLASS: src/test/java/org/thejavaguy/javafaker/MarkdownTest.java
+FAKER_REGISTRATION: src/main/java/org/thejavaguy/javafaker/Faker.java
+
+## IMPLEMENTATION_APPROACH
+
+**CRITICAL**: Follow incremental testing to catch issues early:
+
+1. Create YAML file with proper square bracket format
+2. Register "markdown.yml" in EnFile.java FILES list
+3. Verify YAML loads correctly with a simple test
+4. Create Java class with one method
+5. Add full test suite
+6. Complete remaining methods
+
+**Key Learnings Applied**:
+- YAML must be registered in EnFile.java or fetchString() returns null
+- Use square bracket notation for arrays: ["item1", "item2"]
+- Test YAML loading BEFORE writing full implementation
+
+## YAML_EXTENSION
+
+```yaml
+en:
+ faker:
+ markdown:
+ headers: ["#", "##", "###", "####", "#####", "######"]
+ emphasis: ["_", "~", "*", "**"]
+ # Code block languages
+ code_languages: ["java", "python", "javascript", "typescript", "bash", "sql", "json", "yaml", "xml", "html", "css", "ruby", "go", "rust", "kotlin", "swift", "c", "cpp", "csharp", "php"]
+ # Table column headers for sample tables
+ table_headers:
+ - ["Name", "Age", "City"]
+ - ["ID", "Product", "Price"]
+ - ["Date", "Event", "Status"]
+ - ["Title", "Author", "Year"]
+ - ["Feature", "Description", "Priority"]
+ # Sample image alt texts
+ image_alt_texts: ["Sample image", "Screenshot", "Diagram", "Logo", "Chart", "Photo", "Illustration", "Icon"]
+ # Blockquote attributions
+ blockquote_attributions: ["Anonymous", "Unknown Author", "Traditional", "Ancient Proverb"]
+```
+
+## JAVA_IMPLEMENTATION
+
+```java
+package org.thejavaguy.javafaker;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Generates realistic Markdown-formatted fake content.
+ *
+ *
This faker produces properly formatted Markdown that can be
+ * rendered by any standard Markdown processor.
+ *
+ * @since 1.x.x
+ */
+public class Markdown {
+
+ private final Faker faker;
+
+ protected Markdown(Faker faker) {
+ this.faker = faker;
+ }
+
+ /**
+ * Generates a random Markdown header (h1-h6).
+ *
+ * @return a header string, e.g., "## Lorem Ipsum"
+ */
+ public String header() {
+ String prefix = faker.fakeValuesService().fetchString("markdown.headers");
+ String text = capitalizeWords(faker.lorem().words(faker.random().nextInt(3) + 2));
+ return prefix + " " + text;
+ }
+
+ /**
+ * Generates a header of specific level (1-6).
+ *
+ * @param level the header level (1-6)
+ * @return a header string, e.g., "### Lorem Ipsum"
+ */
+ public String header(int level) {
+ if (level < 1 || level > 6) {
+ throw new IllegalArgumentException("Header level must be between 1 and 6");
+ }
+ String prefix = "#".repeat(level);
+ String text = capitalizeWords(faker.lorem().words(faker.random().nextInt(3) + 2));
+ return prefix + " " + text;
+ }
+
+ /**
+ * Generates emphasized text using random emphasis markers.
+ *
+ * @return emphasized text, e.g., "*important*" or "**bold**"
+ */
+ public String emphasis() {
+ String marker = faker.fakeValuesService().fetchString("markdown.emphasis");
+ String text = faker.lorem().word();
+ return marker + text + marker;
+ }
+
+ /**
+ * Generates bold text.
+ *
+ * @return bold text, e.g., "**important**"
+ */
+ public String bold() {
+ return "**" + faker.lorem().word() + "**";
+ }
+
+ /**
+ * Generates italic text.
+ *
+ * @return italic text, e.g., "*emphasis*"
+ */
+ public String italic() {
+ return "*" + faker.lorem().word() + "*";
+ }
+
+ /**
+ * Generates strikethrough text.
+ *
+ * @return strikethrough text, e.g., "~~removed~~"
+ */
+ public String strikethrough() {
+ return "~~" + faker.lorem().word() + "~~";
+ }
+
+ /**
+ * Generates inline code.
+ *
+ * @return inline code, e.g., "`variable`"
+ */
+ public String inlineCode() {
+ return "`" + faker.lorem().word() + "`";
+ }
+
+ /**
+ * Generates a fenced code block with random language.
+ *
+ * @return a code block with language hint
+ */
+ public String codeBlock() {
+ String language = faker.fakeValuesService().fetchString("markdown.code_languages");
+ return codeBlock(language);
+ }
+
+ /**
+ * Generates a fenced code block with specified language.
+ *
+ * @param language the language for syntax highlighting
+ * @return a code block with the specified language
+ */
+ public String codeBlock(String language) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("```").append(language).append("\n");
+ // Generate 2-5 lines of pseudo-code
+ int lines = faker.random().nextInt(4) + 2;
+ for (int i = 0; i < lines; i++) {
+ sb.append(faker.lorem().sentence()).append("\n");
+ }
+ sb.append("```");
+ return sb.toString();
+ }
+
+ /**
+ * Generates an unordered (bullet) list.
+ *
+ * @return an unordered list with 3-5 items
+ */
+ public String unorderedList() {
+ return unorderedList(faker.random().nextInt(3) + 3);
+ }
+
+ /**
+ * Generates an unordered list with specified number of items.
+ *
+ * @param items number of list items
+ * @return an unordered list
+ */
+ public String unorderedList(int items) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < items; i++) {
+ sb.append("- ").append(capitalizeFirst(faker.lorem().sentence()));
+ if (i < items - 1) {
+ sb.append("\n");
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Generates an ordered (numbered) list.
+ *
+ * @return an ordered list with 3-5 items
+ */
+ public String orderedList() {
+ return orderedList(faker.random().nextInt(3) + 3);
+ }
+
+ /**
+ * Generates an ordered list with specified number of items.
+ *
+ * @param items number of list items
+ * @return an ordered list
+ */
+ public String orderedList(int items) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < items; i++) {
+ sb.append(i + 1).append(". ").append(capitalizeFirst(faker.lorem().sentence()));
+ if (i < items - 1) {
+ sb.append("\n");
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Generates a task list (checkbox list).
+ *
+ * @return a task list with 3-5 items, some checked
+ */
+ public String taskList() {
+ return taskList(faker.random().nextInt(3) + 3);
+ }
+
+ /**
+ * Generates a task list with specified number of items.
+ *
+ * @param items number of list items
+ * @return a task list with random checked/unchecked items
+ */
+ public String taskList(int items) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < items; i++) {
+ boolean checked = faker.random().nextBoolean();
+ sb.append("- [").append(checked ? "x" : " ").append("] ");
+ sb.append(capitalizeFirst(faker.lorem().sentence()));
+ if (i < items - 1) {
+ sb.append("\n");
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Generates a Markdown link.
+ *
+ * @return a link, e.g., "[Lorem Ipsum](https://example.com)"
+ */
+ public String link() {
+ String text = capitalizeWords(faker.lorem().words(faker.random().nextInt(2) + 1));
+ String url = faker.internet().url();
+ return "[" + text + "](" + url + ")";
+ }
+
+ /**
+ * Generates a Markdown link with specified text and URL.
+ *
+ * @param text the link text
+ * @param url the URL
+ * @return a link string
+ */
+ public String link(String text, String url) {
+ return "[" + text + "](" + url + ")";
+ }
+
+ /**
+ * Generates a Markdown image reference.
+ *
+ * @return an image, e.g., ""
+ */
+ public String image() {
+ String alt = faker.fakeValuesService().fetchString("markdown.image_alt_texts");
+ String url = faker.internet().image();
+ return "";
+ }
+
+ /**
+ * Generates a Markdown image with specified alt text and URL.
+ *
+ * @param altText the alt text
+ * @param url the image URL
+ * @return an image string
+ */
+ public String image(String altText, String url) {
+ return "";
+ }
+
+ /**
+ * Generates a blockquote.
+ *
+ * @return a blockquote, e.g., "> Lorem ipsum dolor sit amet."
+ */
+ public String blockquote() {
+ return "> " + capitalizeFirst(faker.lorem().sentence());
+ }
+
+ /**
+ * Generates a multi-line blockquote.
+ *
+ * @param lines number of lines in the blockquote
+ * @return a multi-line blockquote
+ */
+ public String blockquote(int lines) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < lines; i++) {
+ sb.append("> ").append(capitalizeFirst(faker.lorem().sentence()));
+ if (i < lines - 1) {
+ sb.append("\n");
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Generates a horizontal rule.
+ *
+ * @return a horizontal rule string
+ */
+ public String horizontalRule() {
+ String[] rules = {"---", "***", "___"};
+ return rules[faker.random().nextInt(rules.length)];
+ }
+
+ /**
+ * Generates a simple Markdown table.
+ *
+ * @return a table with 3 columns and 3-5 rows
+ */
+ public String table() {
+ return table(3, faker.random().nextInt(3) + 3);
+ }
+
+ /**
+ * Generates a Markdown table with specified dimensions.
+ *
+ * @param columns number of columns
+ * @param rows number of data rows (excluding header)
+ * @return a formatted Markdown table
+ */
+ public String table(int columns, int rows) {
+ StringBuilder sb = new StringBuilder();
+
+ // Header row
+ sb.append("|");
+ for (int c = 0; c < columns; c++) {
+ sb.append(" ").append(capitalizeFirst(faker.lorem().word())).append(" |");
+ }
+ sb.append("\n");
+
+ // Separator row
+ sb.append("|");
+ for (int c = 0; c < columns; c++) {
+ sb.append(" --- |");
+ }
+ sb.append("\n");
+
+ // Data rows
+ for (int r = 0; r < rows; r++) {
+ sb.append("|");
+ for (int c = 0; c < columns; c++) {
+ sb.append(" ").append(faker.lorem().word()).append(" |");
+ }
+ if (r < rows - 1) {
+ sb.append("\n");
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Generates a paragraph of Markdown text.
+ *
+ * @return a paragraph, e.g., "Lorem ipsum dolor sit amet..."
+ */
+ public String paragraph() {
+ return faker.lorem().paragraph();
+ }
+
+ /**
+ * Generates multiple paragraphs separated by blank lines.
+ *
+ * @param count number of paragraphs
+ * @return paragraphs separated by double newlines
+ */
+ public String paragraphs(int count) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < count; i++) {
+ sb.append(faker.lorem().paragraph());
+ if (i < count - 1) {
+ sb.append("\n\n");
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Generates a complete Markdown document with mixed content.
+ *
+ * @return a sample Markdown document
+ */
+ public String document() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(header(1)).append("\n\n");
+ sb.append(paragraph()).append("\n\n");
+ sb.append(header(2)).append("\n\n");
+ sb.append(unorderedList(3)).append("\n\n");
+ sb.append(header(2)).append("\n\n");
+ sb.append(paragraph()).append("\n\n");
+ sb.append(codeBlock()).append("\n\n");
+ sb.append(blockquote());
+ return sb.toString();
+ }
+
+ private String capitalizeFirst(String text) {
+ if (text == null || text.isEmpty()) {
+ return text;
+ }
+ return Character.toUpperCase(text.charAt(0)) + text.substring(1);
+ }
+
+ private String capitalizeWords(List words) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < words.size(); i++) {
+ sb.append(capitalizeFirst(words.get(i)));
+ if (i < words.size() - 1) {
+ sb.append(" ");
+ }
+ }
+ return sb.toString();
+ }
+}
+```
+
+## FAKER_REGISTRATION
+
+FIELD: private final Markdown markdown;
+CONSTRUCTOR: this.markdown = new Markdown(this);
+GETTER: public Markdown markdown() { return markdown; }
+
+## TESTS
+
+```java
+package org.thejavaguy.javafaker;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.jupiter.api.Test;
+
+public class MarkdownTest extends AbstractFakerTest {
+
+ @Test
+ void header() {
+ String header = faker.markdown().header();
+ assertThat(header).matches("#{1,6} .+");
+ }
+
+ @Test
+ void headerWithLevel() {
+ for (int level = 1; level <= 6; level++) {
+ String header = faker.markdown().header(level);
+ assertThat(header).startsWith("#".repeat(level) + " ");
+ }
+ }
+
+ @Test
+ void headerWithInvalidLevel() {
+ assertThatThrownBy(() -> faker.markdown().header(0))
+ .isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> faker.markdown().header(7))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void emphasis() {
+ String emphasis = faker.markdown().emphasis();
+ assertThat(emphasis).matches("[_~*]{1,2}\\w+[_~*]{1,2}");
+ }
+
+ @Test
+ void bold() {
+ String bold = faker.markdown().bold();
+ assertThat(bold).matches("\\*\\*\\w+\\*\\*");
+ }
+
+ @Test
+ void italic() {
+ String italic = faker.markdown().italic();
+ assertThat(italic).matches("\\*\\w+\\*");
+ }
+
+ @Test
+ void strikethrough() {
+ String strike = faker.markdown().strikethrough();
+ assertThat(strike).matches("~~\\w+~~");
+ }
+
+ @Test
+ void inlineCode() {
+ String code = faker.markdown().inlineCode();
+ assertThat(code).matches("`\\w+`");
+ }
+
+ @Test
+ void codeBlock() {
+ String block = faker.markdown().codeBlock();
+ assertThat(block).startsWith("```");
+ assertThat(block).endsWith("```");
+ assertThat(block).contains("\n");
+ }
+
+ @Test
+ void codeBlockWithLanguage() {
+ String block = faker.markdown().codeBlock("java");
+ assertThat(block).startsWith("```java\n");
+ assertThat(block).endsWith("```");
+ }
+
+ @Test
+ void unorderedList() {
+ String list = faker.markdown().unorderedList();
+ assertThat(list).contains("- ");
+ String[] lines = list.split("\n");
+ assertThat(lines.length).isBetween(3, 5);
+ }
+
+ @Test
+ void unorderedListWithCount() {
+ String list = faker.markdown().unorderedList(5);
+ String[] lines = list.split("\n");
+ assertThat(lines).hasSize(5);
+ for (String line : lines) {
+ assertThat(line).startsWith("- ");
+ }
+ }
+
+ @Test
+ void orderedList() {
+ String list = faker.markdown().orderedList();
+ assertThat(list).matches("(?s)1\\. .+");
+ }
+
+ @Test
+ void orderedListWithCount() {
+ String list = faker.markdown().orderedList(4);
+ String[] lines = list.split("\n");
+ assertThat(lines).hasSize(4);
+ for (int i = 0; i < lines.length; i++) {
+ assertThat(lines[i]).startsWith((i + 1) + ". ");
+ }
+ }
+
+ @Test
+ void taskList() {
+ String list = faker.markdown().taskList();
+ assertThat(list).matches("(?s).*- \\[[x ]\\] .+.*");
+ }
+
+ @Test
+ void taskListWithCount() {
+ String list = faker.markdown().taskList(3);
+ String[] lines = list.split("\n");
+ assertThat(lines).hasSize(3);
+ for (String line : lines) {
+ assertThat(line).matches("- \\[[x ]\\] .+");
+ }
+ }
+
+ @Test
+ void link() {
+ String link = faker.markdown().link();
+ assertThat(link).matches("\\[.+\\]\\(.+\\)");
+ }
+
+ @Test
+ void linkWithParams() {
+ String link = faker.markdown().link("Click here", "https://example.com");
+ assertThat(link).isEqualTo("[Click here](https://example.com)");
+ }
+
+ @Test
+ void image() {
+ String image = faker.markdown().image();
+ assertThat(image).matches("!\\[.+\\]\\(.+\\)");
+ }
+
+ @Test
+ void imageWithParams() {
+ String image = faker.markdown().image("Alt text", "https://example.com/img.jpg");
+ assertThat(image).isEqualTo("");
+ }
+
+ @Test
+ void blockquote() {
+ String quote = faker.markdown().blockquote();
+ assertThat(quote).startsWith("> ");
+ }
+
+ @Test
+ void blockquoteMultiline() {
+ String quote = faker.markdown().blockquote(3);
+ String[] lines = quote.split("\n");
+ assertThat(lines).hasSize(3);
+ for (String line : lines) {
+ assertThat(line).startsWith("> ");
+ }
+ }
+
+ @Test
+ void horizontalRule() {
+ String rule = faker.markdown().horizontalRule();
+ assertThat(rule).matches("---|(\\*{3})|(_{3})");
+ }
+
+ @Test
+ void table() {
+ String table = faker.markdown().table();
+ assertThat(table).contains("|");
+ assertThat(table).contains("---");
+ }
+
+ @Test
+ void tableWithDimensions() {
+ String table = faker.markdown().table(4, 2);
+ String[] lines = table.split("\n");
+ // Header + separator + 2 data rows = 4 lines
+ assertThat(lines).hasSize(4);
+ // Each line should have 5 pipes (for 4 columns)
+ for (String line : lines) {
+ assertThat(line.chars().filter(c -> c == '|').count()).isEqualTo(5);
+ }
+ }
+
+ @Test
+ void paragraph() {
+ String para = faker.markdown().paragraph();
+ assertThat(para).isNotBlank();
+ }
+
+ @Test
+ void paragraphs() {
+ String paras = faker.markdown().paragraphs(3);
+ String[] parts = paras.split("\n\n");
+ assertThat(parts).hasSize(3);
+ }
+
+ @Test
+ void document() {
+ String doc = faker.markdown().document();
+ assertThat(doc).isNotBlank();
+ assertThat(doc).contains("#"); // Has headers
+ assertThat(doc).contains("```"); // Has code block
+ assertThat(doc).contains(">"); // Has blockquote
+ }
+}
+```
diff --git a/specs/SHIPPING_FAKER_SPEC.md b/specs/SHIPPING_FAKER_SPEC.md
new file mode 100644
index 0000000..5d7dc3d
--- /dev/null
+++ b/specs/SHIPPING_FAKER_SPEC.md
@@ -0,0 +1,761 @@
+# Shipping Faker Implementation Specification
+
+## Overview
+
+The Shipping faker generates realistic shipping and logistics-related test data including tracking numbers, carrier names, shipping methods, package dimensions, and delivery statuses.
+
+### Use Cases
+- Testing e-commerce order fulfillment systems
+- Shipping carrier integration testing
+- Package tracking interfaces
+- Warehouse management systems
+- Logistics and delivery applications
+- Order confirmation emails with tracking info
+
+---
+
+## File Locations
+
+| Component | Path |
+|-----------|------|
+| YAML Data | `src/main/resources/en/shipping.yml` |
+| **YAML Registration** | **`src/main/java/org/thejavaguy/javafaker/service/files/EnFile.java`** (add `"shipping.yml"` to `FILES` list) |
+| Java Class | `src/main/java/org/thejavaguy/javafaker/Shipping.java` |
+| Test Class | `src/test/java/org/thejavaguy/javafaker/ShippingTest.java` |
+| Registration | `src/main/java/org/thejavaguy/javafaker/Faker.java` |
+
+---
+
+## Implementation Approach
+
+**CRITICAL**: Follow incremental testing to catch issues early:
+
+1. Create YAML file with proper square bracket format (see below)
+2. **Register `"shipping.yml"` in `EnFile.java` FILES list** (required or `fetchString()` returns null!)
+3. Verify YAML loads correctly with a simple test
+4. Create Java class with one method
+5. Add full test suite
+6. Complete remaining methods
+
+**Key Learnings Applied**:
+- YAML must be registered in `EnFile.java` or `fetchString()` returns null
+- Use square bracket notation for arrays: `["item1", "item2"]`
+- Test YAML loading BEFORE writing full implementation
+
+---
+
+## YAML Data Structure
+
+**IMPORTANT**: Use square bracket format `["item1", "item2"]`, NOT dash format!
+
+```yaml
+en:
+ faker:
+ shipping:
+ carriers: ["UPS", "FedEx", "USPS", "DHL", "Amazon Logistics", "OnTrac", "LaserShip", "Purolator", "Canada Post", "Royal Mail", "Deutsche Post", "Japan Post", "Australia Post", "China Post", "SF Express"]
+ shipping_methods: ["Standard Ground", "Express", "Overnight", "Two-Day", "Priority", "Economy", "First Class", "Freight", "Same Day", "Next Day Air", "Ground Advantage", "Parcel Select", "Media Mail", "International Economy", "International Express"]
+ statuses: ["pending", "label_created", "picked_up", "in_transit", "out_for_delivery", "delivered", "attempted_delivery", "exception", "returned", "on_hold", "customs_clearance", "at_local_facility"]
+ package_types: ["Box", "Envelope", "Padded Envelope", "Tube", "Flat Rate Box", "Poly Mailer", "Bubble Mailer", "Crate", "Pallet", "Barrel", "Bag", "Custom"]
+ handling_instructions: ["Fragile", "Handle With Care", "This Side Up", "Do Not Stack", "Keep Dry", "Temperature Controlled", "Refrigerate", "No Signature Required", "Signature Required", "Adult Signature Required", "Leave at Door", "Do Not Bend"]
+ exception_types: ["Address Unknown", "Recipient Unavailable", "Weather Delay", "Customs Hold", "Damaged in Transit", "Missing Documentation", "Incorrect Address", "Package Refused", "Business Closed", "Security Delay"]
+ weight_units: ["lb", "oz", "kg", "g"]
+ dimension_units: ["in", "cm", "mm"]
+ # Tracking number patterns by carrier
+ tracking_patterns:
+ ups: "1Z???###########"
+ fedex_express: "####-####-####"
+ fedex_ground: "############"
+ usps: "################"
+ dhl: "##########"
+```
+
+---
+
+## Java Class Implementation
+
+```java
+package org.thejavaguy.javafaker;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Generates realistic shipping and logistics fake data.
+ *
+ * @since 1.x.x
+ */
+public class Shipping {
+
+ private final Faker faker;
+
+ protected Shipping(Faker faker) {
+ this.faker = faker;
+ }
+
+ /**
+ * Returns a random shipping carrier name.
+ *
+ * @return carrier name, e.g., "UPS", "FedEx"
+ */
+ public String carrier() {
+ return faker.fakeValuesService().fetchString("shipping.carriers");
+ }
+
+ /**
+ * Returns a random shipping method.
+ *
+ * @return shipping method, e.g., "Standard Ground", "Overnight"
+ */
+ public String shippingMethod() {
+ return faker.fakeValuesService().fetchString("shipping.shipping_methods");
+ }
+
+ /**
+ * Returns a random shipping status.
+ *
+ * @return status, e.g., "in_transit", "delivered"
+ */
+ public String status() {
+ return faker.fakeValuesService().fetchString("shipping.statuses");
+ }
+
+ /**
+ * Returns a random package type.
+ *
+ * @return package type, e.g., "Box", "Envelope"
+ */
+ public String packageType() {
+ return faker.fakeValuesService().fetchString("shipping.package_types");
+ }
+
+ /**
+ * Returns a random handling instruction.
+ *
+ * @return handling instruction, e.g., "Fragile", "Handle With Care"
+ */
+ public String handlingInstruction() {
+ return faker.fakeValuesService().fetchString("shipping.handling_instructions");
+ }
+
+ /**
+ * Returns a random shipping exception type.
+ *
+ * @return exception type, e.g., "Address Unknown", "Weather Delay"
+ */
+ public String exceptionType() {
+ return faker.fakeValuesService().fetchString("shipping.exception_types");
+ }
+
+ /**
+ * Generates a tracking number in UPS format.
+ *
+ * @return UPS tracking number, e.g., "1ZABC1234567890123456"
+ */
+ public String trackingNumberUps() {
+ // UPS: 1Z + 6 alphanumeric + 2 digits + 8 digits
+ return "1Z" + faker.bothify("??????", true) + faker.numerify("##########");
+ }
+
+ /**
+ * Generates a tracking number in FedEx Express format.
+ *
+ * @return FedEx tracking number, e.g., "1234-5678-9012"
+ */
+ public String trackingNumberFedEx() {
+ return faker.numerify("####-####-####");
+ }
+
+ /**
+ * Generates a tracking number in USPS format.
+ *
+ * @return USPS tracking number, e.g., "9400111899223100001234"
+ */
+ public String trackingNumberUsps() {
+ // USPS typically 20-22 digits
+ return faker.numerify("94001118992231########");
+ }
+
+ /**
+ * Generates a tracking number in DHL format.
+ *
+ * @return DHL tracking number, e.g., "1234567890"
+ */
+ public String trackingNumberDhl() {
+ return faker.numerify("##########");
+ }
+
+ /**
+ * Generates a tracking number for a random carrier.
+ *
+ * @return tracking number in carrier-appropriate format
+ */
+ public String trackingNumber() {
+ int choice = faker.random().nextInt(4);
+ switch (choice) {
+ case 0: return trackingNumberUps();
+ case 1: return trackingNumberFedEx();
+ case 2: return trackingNumberUsps();
+ default: return trackingNumberDhl();
+ }
+ }
+
+ /**
+ * Generates an order/shipment ID.
+ *
+ * @return shipment ID, e.g., "SHP-20241115-ABC123"
+ */
+ public String shipmentId() {
+ String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+ return "SHP-" + date + "-" + faker.bothify("???###", true);
+ }
+
+ /**
+ * Generates a package weight.
+ *
+ * @return weight in pounds, e.g., 2.5
+ */
+ public BigDecimal weight() {
+ return weight(0.1, 50.0);
+ }
+
+ /**
+ * Generates a package weight within range.
+ *
+ * @param min minimum weight
+ * @param max maximum weight
+ * @return weight as BigDecimal with 1 decimal place
+ */
+ public BigDecimal weight(double min, double max) {
+ double w = min + (faker.random().nextDouble() * (max - min));
+ return BigDecimal.valueOf(w).setScale(1, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * Generates formatted weight with unit.
+ *
+ * @return formatted weight, e.g., "2.5 lb"
+ */
+ public String weightFormatted() {
+ String unit = faker.fakeValuesService().fetchString("shipping.weight_units");
+ return weight() + " " + unit;
+ }
+
+ /**
+ * Generates package dimensions (length x width x height).
+ *
+ * @return dimensions string, e.g., "12 x 8 x 4 in"
+ */
+ public String dimensions() {
+ int length = faker.random().nextInt(30) + 4;
+ int width = faker.random().nextInt(20) + 3;
+ int height = faker.random().nextInt(15) + 2;
+ String unit = faker.fakeValuesService().fetchString("shipping.dimension_units");
+ return length + " x " + width + " x " + height + " " + unit;
+ }
+
+ /**
+ * Generates individual dimension values.
+ *
+ * @return array of [length, width, height] in inches
+ */
+ public int[] dimensionValues() {
+ int length = faker.random().nextInt(30) + 4;
+ int width = faker.random().nextInt(20) + 3;
+ int height = faker.random().nextInt(15) + 2;
+ return new int[]{length, width, height};
+ }
+
+ /**
+ * Generates a shipping cost.
+ *
+ * @return shipping cost as BigDecimal
+ */
+ public BigDecimal shippingCost() {
+ return shippingCost(5.99, 99.99);
+ }
+
+ /**
+ * Generates a shipping cost within range.
+ *
+ * @param min minimum cost
+ * @param max maximum cost
+ * @return shipping cost as BigDecimal with 2 decimal places
+ */
+ public BigDecimal shippingCost(double min, double max) {
+ double cost = min + (faker.random().nextDouble() * (max - min));
+ return BigDecimal.valueOf(cost).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * Generates formatted shipping cost.
+ *
+ * @return formatted cost, e.g., "$12.99"
+ */
+ public String shippingCostFormatted() {
+ return "$" + shippingCost().toString();
+ }
+
+ /**
+ * Generates an estimated delivery date (3-14 days from now).
+ *
+ * @return estimated delivery date
+ */
+ public LocalDate estimatedDeliveryDate() {
+ int daysAhead = faker.random().nextInt(12) + 3;
+ return LocalDate.now().plusDays(daysAhead);
+ }
+
+ /**
+ * Generates a ship date (within last 7 days).
+ *
+ * @return ship date
+ */
+ public LocalDate shipDate() {
+ int daysBack = faker.random().nextInt(7);
+ return LocalDate.now().minusDays(daysBack);
+ }
+
+ /**
+ * Generates an actual delivery timestamp.
+ *
+ * @return delivery datetime
+ */
+ public LocalDateTime deliveryDateTime() {
+ LocalDate date = LocalDate.now().minusDays(faker.random().nextInt(7));
+ int hour = faker.random().nextInt(12) + 8; // 8 AM to 8 PM
+ int minute = faker.random().nextInt(60);
+ return date.atTime(hour, minute);
+ }
+
+ /**
+ * Generates a formatted delivery time window.
+ *
+ * @return delivery window, e.g., "2:00 PM - 6:00 PM"
+ */
+ public String deliveryWindow() {
+ String[] windows = {
+ "8:00 AM - 12:00 PM",
+ "9:00 AM - 1:00 PM",
+ "12:00 PM - 4:00 PM",
+ "2:00 PM - 6:00 PM",
+ "4:00 PM - 8:00 PM"
+ };
+ return windows[faker.random().nextInt(windows.length)];
+ }
+
+ /**
+ * Generates a package insured value.
+ *
+ * @return insured value as BigDecimal
+ */
+ public BigDecimal insuredValue() {
+ double value = faker.random().nextDouble() * 1000;
+ return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * Generates a COD (Cash on Delivery) amount.
+ *
+ * @return COD amount as BigDecimal
+ */
+ public BigDecimal codAmount() {
+ double amount = 10 + (faker.random().nextDouble() * 490);
+ return BigDecimal.valueOf(amount).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * Generates a reference number (PO number, order number).
+ *
+ * @return reference number, e.g., "PO-12345678"
+ */
+ public String referenceNumber() {
+ String[] prefixes = {"PO-", "REF-", "ORD-", "INV-", ""};
+ String prefix = prefixes[faker.random().nextInt(prefixes.length)];
+ return prefix + faker.numerify("########");
+ }
+
+ /**
+ * Generates a location/facility name for tracking updates.
+ *
+ * @return location, e.g., "Chicago, IL Distribution Center"
+ */
+ public String facilityLocation() {
+ String city = faker.address().city();
+ String state = faker.address().stateAbbr();
+ String[] types = {"Distribution Center", "Hub", "Sort Facility", "Processing Center", "Delivery Station"};
+ String type = types[faker.random().nextInt(types.length)];
+ return city + ", " + state + " " + type;
+ }
+
+ /**
+ * Generates a barcode number for package labels.
+ *
+ * @return barcode number (20 digits)
+ */
+ public String barcode() {
+ return faker.numerify("####################");
+ }
+
+ /**
+ * Generates a zone number for shipping rate calculation.
+ *
+ * @return zone number 1-8
+ */
+ public int zone() {
+ return faker.random().nextInt(8) + 1;
+ }
+
+ /**
+ * Returns whether signature is required (random).
+ *
+ * @return true or false
+ */
+ public boolean signatureRequired() {
+ return faker.random().nextBoolean();
+ }
+
+ /**
+ * Returns whether the package contains hazardous materials (random, low probability).
+ *
+ * @return true ~10% of the time
+ */
+ public boolean hazardousMaterials() {
+ return faker.random().nextInt(10) == 0;
+ }
+
+ /**
+ * Generates a tracking event description.
+ *
+ * @return tracking event, e.g., "Package arrived at Chicago, IL Hub"
+ */
+ public String trackingEvent() {
+ String status = status();
+ String location = facilityLocation();
+
+ switch (status) {
+ case "label_created":
+ return "Shipping label created";
+ case "picked_up":
+ return "Package picked up from sender";
+ case "in_transit":
+ return "Package arrived at " + location;
+ case "out_for_delivery":
+ return "Out for delivery";
+ case "delivered":
+ return "Delivered";
+ case "attempted_delivery":
+ return "Delivery attempted - " + faker.fakeValuesService().fetchString("shipping.exception_types");
+ default:
+ return "Package in transit at " + location;
+ }
+ }
+}
+```
+
+---
+
+## Faker.java Registration
+
+### Field Declaration (add with other fields around line 109)
+
+```java
+private final Shipping shipping;
+```
+
+### Constructor Initialization (add in constructor around line 225)
+
+```java
+this.shipping = new Shipping(this);
+```
+
+### Getter Method (add with other getters around line 692)
+
+```java
+/**
+ * Returns a Shipping instance for generating shipping-related fake data.
+ *
+ * @return a {@link Shipping} instance
+ */
+public Shipping shipping() {
+ return shipping;
+}
+```
+
+---
+
+## Test Cases
+
+```java
+package org.thejavaguy.javafaker;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+import org.junit.jupiter.api.Test;
+
+public class ShippingTest extends AbstractFakerTest {
+
+ @Test
+ void carrier() {
+ assertThat(faker.shipping().carrier()).isNotBlank();
+ }
+
+ @Test
+ void shippingMethod() {
+ assertThat(faker.shipping().shippingMethod()).isNotBlank();
+ }
+
+ @Test
+ void status() {
+ String status = faker.shipping().status();
+ assertThat(status).isNotBlank();
+ assertThat(status).isIn("pending", "label_created", "picked_up", "in_transit",
+ "out_for_delivery", "delivered", "attempted_delivery", "exception",
+ "returned", "on_hold", "customs_clearance", "at_local_facility");
+ }
+
+ @Test
+ void packageType() {
+ assertThat(faker.shipping().packageType()).isNotBlank();
+ }
+
+ @Test
+ void handlingInstruction() {
+ assertThat(faker.shipping().handlingInstruction()).isNotBlank();
+ }
+
+ @Test
+ void exceptionType() {
+ assertThat(faker.shipping().exceptionType()).isNotBlank();
+ }
+
+ @Test
+ void trackingNumberUps() {
+ String tracking = faker.shipping().trackingNumberUps();
+ assertThat(tracking).startsWith("1Z");
+ assertThat(tracking).hasSize(18);
+ }
+
+ @Test
+ void trackingNumberFedEx() {
+ String tracking = faker.shipping().trackingNumberFedEx();
+ assertThat(tracking).matches("\\d{4}-\\d{4}-\\d{4}");
+ }
+
+ @Test
+ void trackingNumberUsps() {
+ String tracking = faker.shipping().trackingNumberUsps();
+ assertThat(tracking).matches("\\d{22}");
+ }
+
+ @Test
+ void trackingNumberDhl() {
+ String tracking = faker.shipping().trackingNumberDhl();
+ assertThat(tracking).matches("\\d{10}");
+ }
+
+ @Test
+ void trackingNumber() {
+ // Just verify it generates something
+ assertThat(faker.shipping().trackingNumber()).isNotBlank();
+ }
+
+ @Test
+ void shipmentId() {
+ String id = faker.shipping().shipmentId();
+ assertThat(id).startsWith("SHP-");
+ assertThat(id).matches("SHP-\\d{8}-[A-Z]{3}\\d{3}");
+ }
+
+ @Test
+ void weight() {
+ BigDecimal weight = faker.shipping().weight();
+ assertThat(weight).isGreaterThanOrEqualTo(BigDecimal.valueOf(0.1));
+ assertThat(weight).isLessThanOrEqualTo(BigDecimal.valueOf(50.0));
+ }
+
+ @Test
+ void weightWithRange() {
+ BigDecimal weight = faker.shipping().weight(1.0, 5.0);
+ assertThat(weight).isGreaterThanOrEqualTo(BigDecimal.valueOf(1.0));
+ assertThat(weight).isLessThanOrEqualTo(BigDecimal.valueOf(5.0));
+ }
+
+ @Test
+ void weightFormatted() {
+ String weight = faker.shipping().weightFormatted();
+ assertThat(weight).matches("\\d+\\.\\d+ (lb|oz|kg|g)");
+ }
+
+ @Test
+ void dimensions() {
+ String dims = faker.shipping().dimensions();
+ assertThat(dims).matches("\\d+ x \\d+ x \\d+ (in|cm|mm)");
+ }
+
+ @Test
+ void dimensionValues() {
+ int[] dims = faker.shipping().dimensionValues();
+ assertThat(dims).hasSize(3);
+ assertThat(dims[0]).isGreaterThan(0); // length
+ assertThat(dims[1]).isGreaterThan(0); // width
+ assertThat(dims[2]).isGreaterThan(0); // height
+ }
+
+ @Test
+ void shippingCost() {
+ BigDecimal cost = faker.shipping().shippingCost();
+ assertThat(cost).isGreaterThanOrEqualTo(BigDecimal.valueOf(5.99));
+ assertThat(cost).isLessThanOrEqualTo(BigDecimal.valueOf(99.99));
+ assertThat(cost.scale()).isEqualTo(2);
+ }
+
+ @Test
+ void shippingCostFormatted() {
+ String cost = faker.shipping().shippingCostFormatted();
+ assertThat(cost).matches("\\$\\d+\\.\\d{2}");
+ }
+
+ @Test
+ void estimatedDeliveryDate() {
+ LocalDate date = faker.shipping().estimatedDeliveryDate();
+ assertThat(date).isAfter(LocalDate.now().plusDays(2));
+ assertThat(date).isBeforeOrEqualTo(LocalDate.now().plusDays(14));
+ }
+
+ @Test
+ void shipDate() {
+ LocalDate date = faker.shipping().shipDate();
+ assertThat(date).isBeforeOrEqualTo(LocalDate.now());
+ assertThat(date).isAfterOrEqualTo(LocalDate.now().minusDays(7));
+ }
+
+ @Test
+ void deliveryDateTime() {
+ LocalDateTime dt = faker.shipping().deliveryDateTime();
+ assertThat(dt).isNotNull();
+ assertThat(dt.getHour()).isBetween(8, 19);
+ }
+
+ @Test
+ void deliveryWindow() {
+ String window = faker.shipping().deliveryWindow();
+ assertThat(window).matches("\\d{1,2}:\\d{2} [AP]M - \\d{1,2}:\\d{2} [AP]M");
+ }
+
+ @Test
+ void insuredValue() {
+ BigDecimal value = faker.shipping().insuredValue();
+ assertThat(value).isGreaterThanOrEqualTo(BigDecimal.ZERO);
+ assertThat(value.scale()).isEqualTo(2);
+ }
+
+ @Test
+ void codAmount() {
+ BigDecimal amount = faker.shipping().codAmount();
+ assertThat(amount).isGreaterThanOrEqualTo(BigDecimal.valueOf(10.00));
+ assertThat(amount.scale()).isEqualTo(2);
+ }
+
+ @Test
+ void referenceNumber() {
+ String ref = faker.shipping().referenceNumber();
+ assertThat(ref).matches("(PO-|REF-|ORD-|INV-)?\\d{8}");
+ }
+
+ @Test
+ void facilityLocation() {
+ String location = faker.shipping().facilityLocation();
+ assertThat(location).isNotBlank();
+ assertThat(location).contains(","); // City, State
+ }
+
+ @Test
+ void barcode() {
+ String barcode = faker.shipping().barcode();
+ assertThat(barcode).matches("\\d{20}");
+ }
+
+ @Test
+ void zone() {
+ int zone = faker.shipping().zone();
+ assertThat(zone).isBetween(1, 8);
+ }
+
+ @Test
+ void signatureRequired() {
+ boolean sig = faker.shipping().signatureRequired();
+ assertThat(sig).isIn(true, false);
+ }
+
+ @Test
+ void hazardousMaterials() {
+ boolean hazmat = faker.shipping().hazardousMaterials();
+ assertThat(hazmat).isIn(true, false);
+ }
+
+ @Test
+ void trackingEvent() {
+ String event = faker.shipping().trackingEvent();
+ assertThat(event).isNotBlank();
+ }
+}
+```
+
+---
+
+## Example Outputs
+
+```java
+faker.shipping().carrier() // "FedEx"
+faker.shipping().shippingMethod() // "Standard Ground"
+faker.shipping().status() // "in_transit"
+faker.shipping().packageType() // "Box"
+faker.shipping().handlingInstruction() // "Fragile"
+faker.shipping().exceptionType() // "Weather Delay"
+faker.shipping().trackingNumberUps() // "1ZABC1234567890123"
+faker.shipping().trackingNumberFedEx() // "1234-5678-9012"
+faker.shipping().trackingNumberUsps() // "9400111899223100001234"
+faker.shipping().trackingNumberDhl() // "1234567890"
+faker.shipping().trackingNumber() // (random format)
+faker.shipping().shipmentId() // "SHP-20241115-ABC123"
+faker.shipping().weight() // 2.5 (BigDecimal)
+faker.shipping().weightFormatted() // "2.5 lb"
+faker.shipping().dimensions() // "12 x 8 x 4 in"
+faker.shipping().shippingCost() // 12.99 (BigDecimal)
+faker.shipping().shippingCostFormatted()// "$12.99"
+faker.shipping().estimatedDeliveryDate()// 2024-11-20 (LocalDate)
+faker.shipping().shipDate() // 2024-11-10 (LocalDate)
+faker.shipping().deliveryWindow() // "2:00 PM - 6:00 PM"
+faker.shipping().insuredValue() // 250.00 (BigDecimal)
+faker.shipping().referenceNumber() // "PO-12345678"
+faker.shipping().facilityLocation() // "Chicago, IL Distribution Center"
+faker.shipping().barcode() // "12345678901234567890"
+faker.shipping().zone() // 4
+faker.shipping().signatureRequired() // true
+faker.shipping().hazardousMaterials() // false
+faker.shipping().trackingEvent() // "Package arrived at Chicago, IL Hub"
+```
+
+---
+
+## Verification Checklist
+
+- [ ] YAML file created at `src/main/resources/en/shipping.yml`
+- [ ] **CRITICAL: YAML registered in `EnFile.java` FILES list**
+- [ ] **Verify YAML loads correctly with simple test before implementing full class**
+- [ ] Java class created at `src/main/java/org/thejavaguy/javafaker/Shipping.java`
+- [ ] Field added to `Faker.java`
+- [ ] Constructor initialization added to `Faker.java`
+- [ ] Getter method added to `Faker.java`
+- [ ] Test class created at `src/test/java/org/thejavaguy/javafaker/ShippingTest.java`
+- [ ] All tests pass: `./mvnw test -Dtest=ShippingTest`
+- [ ] Tracking numbers follow carrier-specific formats
+- [ ] Dates are within expected ranges
+- [ ] Costs and weights have correct decimal precision
diff --git a/specs/SUBSCRIPTION_FAKER_SPEC.md b/specs/SUBSCRIPTION_FAKER_SPEC.md
new file mode 100644
index 0000000..884778f
--- /dev/null
+++ b/specs/SUBSCRIPTION_FAKER_SPEC.md
@@ -0,0 +1,659 @@
+# Subscription Faker Implementation Specification
+
+## Overview
+
+The Subscription faker generates realistic subscription and SaaS-related test data including plan names, billing cycles, payment statuses, subscription states, and pricing tiers.
+
+### Use Cases
+- Testing SaaS billing systems
+- Subscription management platforms
+- Payment processing integrations
+- Customer portal interfaces
+- Recurring billing workflows
+- Trial and upgrade flows
+
+---
+
+## File Locations
+
+| Component | Path |
+|-----------|------|
+| YAML Data | `src/main/resources/en/subscription.yml` |
+| **YAML Registration** | **`src/main/java/org/thejavaguy/javafaker/service/files/EnFile.java`** (add `"subscription.yml"` to `FILES` list) |
+| Java Class | `src/main/java/org/thejavaguy/javafaker/Subscription.java` |
+| Test Class | `src/test/java/org/thejavaguy/javafaker/SubscriptionTest.java` |
+| Registration | `src/main/java/org/thejavaguy/javafaker/Faker.java` |
+
+---
+
+## Implementation Approach
+
+**CRITICAL**: Follow incremental testing to catch issues early:
+
+1. Create YAML file with proper square bracket format (see below)
+2. **Register `"subscription.yml"` in `EnFile.java` FILES list** (required or `fetchString()` returns null!)
+3. Verify YAML loads correctly with a simple test
+4. Create Java class with one method
+5. Add full test suite
+6. Complete remaining methods
+
+**Key Learnings Applied**:
+- YAML must be registered in `EnFile.java` or `fetchString()` returns null
+- Use square bracket notation for arrays: `["item1", "item2"]`
+- Test YAML loading BEFORE writing full implementation
+
+---
+
+## YAML Data Structure
+
+**IMPORTANT**: Use square bracket format `["item1", "item2"]`, NOT dash format!
+
+```yaml
+en:
+ faker:
+ subscription:
+ plan_names: ["Free", "Starter", "Basic", "Standard", "Professional", "Pro", "Premium", "Business", "Enterprise", "Ultimate", "Growth", "Scale", "Team", "Individual", "Student", "Family", "Unlimited"]
+ statuses: ["active", "inactive", "canceled", "expired", "past_due", "trialing", "paused", "pending", "suspended"]
+ payment_methods: ["Credit Card", "Debit Card", "PayPal", "Bank Transfer", "ACH", "Wire Transfer", "Apple Pay", "Google Pay", "Invoice", "Check", "Cryptocurrency"]
+ billing_cycles: ["monthly", "quarterly", "semi-annually", "annually", "bi-annually", "weekly", "bi-weekly"]
+ payment_terms: ["Net 15", "Net 30", "Net 45", "Net 60", "Due on Receipt", "Prepaid"]
+ currencies: ["USD", "EUR", "GBP", "CAD", "AUD", "JPY", "CHF", "INR", "BRL", "MXN"]
+ features: ["Unlimited Users", "API Access", "Priority Support", "Custom Branding", "Advanced Analytics", "SSO Integration", "Audit Logs", "Data Export", "Dedicated Account Manager", "Custom Integrations", "99.9% SLA", "24/7 Support", "White Label", "Multi-tenant", "HIPAA Compliance", "SOC 2 Compliance", "Unlimited Storage", "Advanced Security", "Role-based Access", "Custom Workflows"]
+ subscription_types: ["SaaS", "Streaming", "Membership", "License", "Service", "Content", "Software", "Platform"]
+ cancellation_reasons: ["Too expensive", "Not using enough", "Found alternative", "Missing features", "Poor customer support", "Technical issues", "Company closed", "Temporary pause", "Switching plans", "Budget cuts"]
+ renewal_types: ["Auto-renew", "Manual renewal", "No renewal"]
+```
+
+---
+
+## Java Class Implementation
+
+```java
+package org.thejavaguy.javafaker;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Generates realistic subscription and SaaS-related fake data.
+ *
+ * @since 1.x.x
+ */
+public class Subscription {
+
+ private final Faker faker;
+
+ protected Subscription(Faker faker) {
+ this.faker = faker;
+ }
+
+ /**
+ * Returns a random subscription plan name.
+ *
+ * @return plan name, e.g., "Professional", "Enterprise"
+ */
+ public String planName() {
+ return faker.fakeValuesService().fetchString("subscription.plan_names");
+ }
+
+ /**
+ * Returns a random subscription status.
+ *
+ * @return status, e.g., "active", "canceled", "trialing"
+ */
+ public String status() {
+ return faker.fakeValuesService().fetchString("subscription.statuses");
+ }
+
+ /**
+ * Returns a random payment method.
+ *
+ * @return payment method, e.g., "Credit Card", "PayPal"
+ */
+ public String paymentMethod() {
+ return faker.fakeValuesService().fetchString("subscription.payment_methods");
+ }
+
+ /**
+ * Returns a random billing cycle.
+ *
+ * @return billing cycle, e.g., "monthly", "annually"
+ */
+ public String billingCycle() {
+ return faker.fakeValuesService().fetchString("subscription.billing_cycles");
+ }
+
+ /**
+ * Returns a random payment term.
+ *
+ * @return payment term, e.g., "Net 30", "Due on Receipt"
+ */
+ public String paymentTerm() {
+ return faker.fakeValuesService().fetchString("subscription.payment_terms");
+ }
+
+ /**
+ * Returns a random currency code.
+ *
+ * @return currency code, e.g., "USD", "EUR"
+ */
+ public String currency() {
+ return faker.fakeValuesService().fetchString("subscription.currencies");
+ }
+
+ /**
+ * Returns a random subscription feature.
+ *
+ * @return feature, e.g., "Unlimited Users", "API Access"
+ */
+ public String feature() {
+ return faker.fakeValuesService().fetchString("subscription.features");
+ }
+
+ /**
+ * Returns a random subscription type.
+ *
+ * @return subscription type, e.g., "SaaS", "Streaming"
+ */
+ public String subscriptionType() {
+ return faker.fakeValuesService().fetchString("subscription.subscription_types");
+ }
+
+ /**
+ * Returns a random cancellation reason.
+ *
+ * @return cancellation reason, e.g., "Too expensive"
+ */
+ public String cancellationReason() {
+ return faker.fakeValuesService().fetchString("subscription.cancellation_reasons");
+ }
+
+ /**
+ * Returns a random renewal type.
+ *
+ * @return renewal type, e.g., "Auto-renew", "Manual renewal"
+ */
+ public String renewalType() {
+ return faker.fakeValuesService().fetchString("subscription.renewal_types");
+ }
+
+ /**
+ * Generates a random subscription ID.
+ *
+ * @return subscription ID, e.g., "sub_1234567890abcdef"
+ */
+ public String subscriptionId() {
+ return "sub_" + faker.regexify("[a-z0-9]{16}");
+ }
+
+ /**
+ * Generates a random customer ID.
+ *
+ * @return customer ID, e.g., "cus_1234567890abcdef"
+ */
+ public String customerId() {
+ return "cus_" + faker.regexify("[a-z0-9]{14}");
+ }
+
+ /**
+ * Generates a random invoice ID.
+ *
+ * @return invoice ID, e.g., "inv_1234567890abcdef"
+ */
+ public String invoiceId() {
+ return "inv_" + faker.regexify("[a-z0-9]{14}");
+ }
+
+ /**
+ * Generates a random monthly price between $5 and $500.
+ *
+ * @return price as BigDecimal with 2 decimal places
+ */
+ public BigDecimal price() {
+ return price(5.00, 500.00);
+ }
+
+ /**
+ * Generates a random price between min and max.
+ *
+ * @param min minimum price
+ * @param max maximum price
+ * @return price as BigDecimal with 2 decimal places
+ */
+ public BigDecimal price(double min, double max) {
+ double price = min + (faker.random().nextDouble() * (max - min));
+ return BigDecimal.valueOf(price).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * Generates a formatted price string with currency.
+ *
+ * @return formatted price, e.g., "$99.99"
+ */
+ public String priceFormatted() {
+ return "$" + price().toString();
+ }
+
+ /**
+ * Generates a formatted price with specified currency symbol.
+ *
+ * @param currencySymbol the currency symbol to use
+ * @return formatted price, e.g., "€99.99"
+ */
+ public String priceFormatted(String currencySymbol) {
+ return currencySymbol + price().toString();
+ }
+
+ /**
+ * Generates a subscription start date within the last year.
+ *
+ * @return start date as LocalDate
+ */
+ public LocalDate startDate() {
+ int daysBack = faker.random().nextInt(365);
+ return LocalDate.now().minusDays(daysBack);
+ }
+
+ /**
+ * Generates a formatted subscription start date.
+ *
+ * @param pattern the date format pattern
+ * @return formatted start date string
+ */
+ public String startDate(String pattern) {
+ return startDate().format(DateTimeFormatter.ofPattern(pattern));
+ }
+
+ /**
+ * Generates a subscription end date in the future.
+ *
+ * @return end date as LocalDate (1 month to 2 years from now)
+ */
+ public LocalDate endDate() {
+ int daysAhead = faker.random().nextInt(730) + 30;
+ return LocalDate.now().plusDays(daysAhead);
+ }
+
+ /**
+ * Generates a formatted subscription end date.
+ *
+ * @param pattern the date format pattern
+ * @return formatted end date string
+ */
+ public String endDate(String pattern) {
+ return endDate().format(DateTimeFormatter.ofPattern(pattern));
+ }
+
+ /**
+ * Generates a next billing date in the future.
+ *
+ * @return next billing date as LocalDate (1-30 days from now)
+ */
+ public LocalDate nextBillingDate() {
+ int daysAhead = faker.random().nextInt(30) + 1;
+ return LocalDate.now().plusDays(daysAhead);
+ }
+
+ /**
+ * Generates a trial end date.
+ *
+ * @return trial end date as LocalDate (7-30 days from now)
+ */
+ public LocalDate trialEndDate() {
+ int daysAhead = faker.random().nextInt(24) + 7;
+ return LocalDate.now().plusDays(daysAhead);
+ }
+
+ /**
+ * Generates a random trial period in days.
+ *
+ * @return trial period in days (7, 14, or 30)
+ */
+ public int trialPeriodDays() {
+ int[] periods = {7, 14, 30};
+ return periods[faker.random().nextInt(periods.length)];
+ }
+
+ /**
+ * Generates a random discount percentage.
+ *
+ * @return discount percentage (5-50)
+ */
+ public int discountPercent() {
+ int[] discounts = {5, 10, 15, 20, 25, 30, 40, 50};
+ return discounts[faker.random().nextInt(discounts.length)];
+ }
+
+ /**
+ * Generates a promo/coupon code.
+ *
+ * @return coupon code, e.g., "SAVE20", "PROMO2024"
+ */
+ public String couponCode() {
+ String[] prefixes = {"SAVE", "PROMO", "DEAL", "OFF", "DISCOUNT", "SPECIAL"};
+ String prefix = prefixes[faker.random().nextInt(prefixes.length)];
+ int number = faker.random().nextInt(50) + 10;
+ return prefix + number;
+ }
+
+ /**
+ * Generates a random user/seat count for a subscription.
+ *
+ * @return user count between 1 and 100
+ */
+ public int userCount() {
+ return faker.random().nextInt(100) + 1;
+ }
+
+ /**
+ * Generates a random user count within a range.
+ *
+ * @param min minimum users
+ * @param max maximum users
+ * @return user count within range
+ */
+ public int userCount(int min, int max) {
+ return faker.random().nextInt(max - min + 1) + min;
+ }
+
+ /**
+ * Checks if subscription should auto-renew (random).
+ *
+ * @return true or false randomly
+ */
+ public boolean autoRenew() {
+ return faker.random().nextBoolean();
+ }
+
+ /**
+ * Generates a full plan description combining name and billing cycle.
+ *
+ * @return plan description, e.g., "Professional (Monthly)"
+ */
+ public String planDescription() {
+ String plan = planName();
+ String cycle = billingCycle();
+ String capitalizedCycle = cycle.substring(0, 1).toUpperCase() + cycle.substring(1);
+ return plan + " (" + capitalizedCycle + ")";
+ }
+}
+```
+
+---
+
+## Faker.java Registration
+
+### Field Declaration (add with other fields around line 109)
+
+```java
+private final Subscription subscription;
+```
+
+### Constructor Initialization (add in constructor around line 225)
+
+```java
+this.subscription = new Subscription(this);
+```
+
+### Getter Method (add with other getters around line 692)
+
+```java
+/**
+ * Returns a Subscription instance for generating subscription-related fake data.
+ *
+ * @return a {@link Subscription} instance
+ */
+public Subscription subscription() {
+ return subscription;
+}
+```
+
+---
+
+## Test Cases
+
+```java
+package org.thejavaguy.javafaker;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+import org.junit.jupiter.api.Test;
+
+public class SubscriptionTest extends AbstractFakerTest {
+
+ @Test
+ void planName() {
+ assertThat(faker.subscription().planName()).isNotBlank();
+ }
+
+ @Test
+ void status() {
+ String status = faker.subscription().status();
+ assertThat(status).isNotBlank();
+ assertThat(status).isIn("active", "inactive", "canceled", "expired",
+ "past_due", "trialing", "paused", "pending", "suspended");
+ }
+
+ @Test
+ void paymentMethod() {
+ assertThat(faker.subscription().paymentMethod()).isNotBlank();
+ }
+
+ @Test
+ void billingCycle() {
+ String cycle = faker.subscription().billingCycle();
+ assertThat(cycle).isNotBlank();
+ assertThat(cycle).isIn("monthly", "quarterly", "semi-annually",
+ "annually", "bi-annually", "weekly", "bi-weekly");
+ }
+
+ @Test
+ void paymentTerm() {
+ assertThat(faker.subscription().paymentTerm()).isNotBlank();
+ }
+
+ @Test
+ void currency() {
+ String currency = faker.subscription().currency();
+ assertThat(currency).matches("[A-Z]{3}");
+ }
+
+ @Test
+ void feature() {
+ assertThat(faker.subscription().feature()).isNotBlank();
+ }
+
+ @Test
+ void subscriptionType() {
+ assertThat(faker.subscription().subscriptionType()).isNotBlank();
+ }
+
+ @Test
+ void cancellationReason() {
+ assertThat(faker.subscription().cancellationReason()).isNotBlank();
+ }
+
+ @Test
+ void renewalType() {
+ assertThat(faker.subscription().renewalType()).isNotBlank();
+ }
+
+ @Test
+ void subscriptionId() {
+ String id = faker.subscription().subscriptionId();
+ assertThat(id).matches("sub_[a-z0-9]{16}");
+ }
+
+ @Test
+ void customerId() {
+ String id = faker.subscription().customerId();
+ assertThat(id).matches("cus_[a-z0-9]{14}");
+ }
+
+ @Test
+ void invoiceId() {
+ String id = faker.subscription().invoiceId();
+ assertThat(id).matches("inv_[a-z0-9]{14}");
+ }
+
+ @Test
+ void price() {
+ BigDecimal price = faker.subscription().price();
+ assertThat(price).isGreaterThanOrEqualTo(BigDecimal.valueOf(5.00));
+ assertThat(price).isLessThanOrEqualTo(BigDecimal.valueOf(500.00));
+ assertThat(price.scale()).isEqualTo(2);
+ }
+
+ @Test
+ void priceWithRange() {
+ BigDecimal price = faker.subscription().price(10.00, 50.00);
+ assertThat(price).isGreaterThanOrEqualTo(BigDecimal.valueOf(10.00));
+ assertThat(price).isLessThanOrEqualTo(BigDecimal.valueOf(50.00));
+ }
+
+ @Test
+ void priceFormatted() {
+ String price = faker.subscription().priceFormatted();
+ assertThat(price).matches("\\$\\d+\\.\\d{2}");
+ }
+
+ @Test
+ void priceFormattedWithCurrency() {
+ String price = faker.subscription().priceFormatted("€");
+ assertThat(price).matches("€\\d+\\.\\d{2}");
+ }
+
+ @Test
+ void startDate() {
+ LocalDate date = faker.subscription().startDate();
+ assertThat(date).isBeforeOrEqualTo(LocalDate.now());
+ assertThat(date).isAfterOrEqualTo(LocalDate.now().minusYears(1));
+ }
+
+ @Test
+ void startDateFormatted() {
+ String date = faker.subscription().startDate("yyyy-MM-dd");
+ assertThat(date).matches("\\d{4}-\\d{2}-\\d{2}");
+ }
+
+ @Test
+ void endDate() {
+ LocalDate date = faker.subscription().endDate();
+ assertThat(date).isAfter(LocalDate.now());
+ }
+
+ @Test
+ void nextBillingDate() {
+ LocalDate date = faker.subscription().nextBillingDate();
+ assertThat(date).isAfter(LocalDate.now());
+ assertThat(date).isBeforeOrEqualTo(LocalDate.now().plusDays(30));
+ }
+
+ @Test
+ void trialEndDate() {
+ LocalDate date = faker.subscription().trialEndDate();
+ assertThat(date).isAfter(LocalDate.now().plusDays(6));
+ assertThat(date).isBeforeOrEqualTo(LocalDate.now().plusDays(31));
+ }
+
+ @Test
+ void trialPeriodDays() {
+ int days = faker.subscription().trialPeriodDays();
+ assertThat(days).isIn(7, 14, 30);
+ }
+
+ @Test
+ void discountPercent() {
+ int discount = faker.subscription().discountPercent();
+ assertThat(discount).isBetween(5, 50);
+ }
+
+ @Test
+ void couponCode() {
+ String code = faker.subscription().couponCode();
+ assertThat(code).matches("[A-Z]+\\d+");
+ }
+
+ @Test
+ void userCount() {
+ int count = faker.subscription().userCount();
+ assertThat(count).isBetween(1, 100);
+ }
+
+ @Test
+ void userCountWithRange() {
+ int count = faker.subscription().userCount(5, 10);
+ assertThat(count).isBetween(5, 10);
+ }
+
+ @Test
+ void autoRenew() {
+ // Just verify it returns a boolean (will be true or false)
+ boolean autoRenew = faker.subscription().autoRenew();
+ assertThat(autoRenew).isIn(true, false);
+ }
+
+ @Test
+ void planDescription() {
+ String desc = faker.subscription().planDescription();
+ assertThat(desc).isNotBlank();
+ assertThat(desc).contains("(");
+ assertThat(desc).contains(")");
+ }
+}
+```
+
+---
+
+## Example Outputs
+
+```java
+faker.subscription().planName() // "Professional"
+faker.subscription().status() // "active"
+faker.subscription().paymentMethod() // "Credit Card"
+faker.subscription().billingCycle() // "monthly"
+faker.subscription().paymentTerm() // "Net 30"
+faker.subscription().currency() // "USD"
+faker.subscription().feature() // "Unlimited Users"
+faker.subscription().subscriptionType() // "SaaS"
+faker.subscription().cancellationReason()// "Too expensive"
+faker.subscription().renewalType() // "Auto-renew"
+faker.subscription().subscriptionId() // "sub_1a2b3c4d5e6f7g8h"
+faker.subscription().customerId() // "cus_abc123def456"
+faker.subscription().invoiceId() // "inv_xyz789abc012"
+faker.subscription().price() // 99.99 (BigDecimal)
+faker.subscription().priceFormatted() // "$99.99"
+faker.subscription().startDate() // 2024-06-15 (LocalDate)
+faker.subscription().endDate() // 2025-06-15 (LocalDate)
+faker.subscription().nextBillingDate() // 2024-12-01 (LocalDate)
+faker.subscription().trialEndDate() // 2024-11-30 (LocalDate)
+faker.subscription().trialPeriodDays() // 14
+faker.subscription().discountPercent() // 20
+faker.subscription().couponCode() // "SAVE20"
+faker.subscription().userCount() // 25
+faker.subscription().autoRenew() // true
+faker.subscription().planDescription() // "Enterprise (Annually)"
+```
+
+---
+
+## Verification Checklist
+
+- [ ] YAML file created at `src/main/resources/en/subscription.yml`
+- [ ] **CRITICAL: YAML registered in `EnFile.java` FILES list**
+- [ ] **Verify YAML loads correctly with simple test before implementing full class**
+- [ ] Java class created at `src/main/java/org/thejavaguy/javafaker/Subscription.java`
+- [ ] Field added to `Faker.java`
+- [ ] Constructor initialization added to `Faker.java`
+- [ ] Getter method added to `Faker.java`
+- [ ] Test class created at `src/test/java/org/thejavaguy/javafaker/SubscriptionTest.java`
+- [ ] All tests pass: `./mvnw test -Dtest=SubscriptionTest`
+- [ ] Subscription IDs follow expected format patterns
+- [ ] Prices have correct decimal precision
+- [ ] Dates are within expected ranges
diff --git a/specs/VEHICLE_FAKER_SPEC.md b/specs/VEHICLE_FAKER_SPEC.md
new file mode 100644
index 0000000..159ecca
--- /dev/null
+++ b/specs/VEHICLE_FAKER_SPEC.md
@@ -0,0 +1,441 @@
+# Vehicle Faker Implementation Specification
+
+## Overview
+
+The Vehicle faker generates realistic vehicle-related test data including makes, models, VINs, license plates, colors, fuel types, and transmission types.
+
+### Use Cases
+- Testing automotive inventory systems
+- E-commerce platforms with vehicle listings
+- Insurance application forms
+- DMV or registration systems
+- Fleet management software
+
+---
+
+## File Locations
+
+| Component | Path |
+|-----------|------|
+| YAML Data | `src/main/resources/en/vehicle.yml` |
+| **YAML Registration** | **`src/main/java/org/thejavaguy/javafaker/service/files/EnFile.java`** (add `"vehicle.yml"` to `FILES` list) |
+| Java Class | `src/main/java/org/thejavaguy/javafaker/Vehicle.java` |
+| Test Class | `src/test/java/org/thejavaguy/javafaker/VehicleTest.java` |
+| Registration | `src/main/java/org/thejavaguy/javafaker/Faker.java` |
+
+---
+
+## Implementation Approach
+
+**CRITICAL**: Follow incremental testing to catch issues early:
+
+1. Create YAML file with proper square bracket format (see below)
+2. **Register `"vehicle.yml"` in `EnFile.java` FILES list** (required or `fetchString()` returns null!)
+3. Verify YAML loads correctly with a simple test
+4. Create Java class with one method
+5. Add full test suite
+6. Complete remaining methods
+
+**Key Learnings Applied**:
+- YAML must be registered in `EnFile.java` or `fetchString()` returns null
+- Use square bracket notation for arrays: `["item1", "item2"]`
+- Test YAML loading BEFORE writing full implementation
+
+---
+
+## YAML Data Structure
+
+**IMPORTANT**: Use square bracket format `["item1", "item2"]`, NOT dash format!
+
+```yaml
+en:
+ faker:
+ vehicle:
+ makes: ["Toyota", "Honda", "Ford", "Chevrolet", "BMW", "Mercedes-Benz", "Audi", "Volkswagen", "Nissan", "Hyundai", "Kia", "Subaru", "Mazda", "Lexus", "Jeep", "Ram", "GMC", "Dodge", "Chrysler", "Buick", "Cadillac", "Lincoln", "Acura", "Infiniti", "Volvo", "Porsche", "Land Rover", "Jaguar", "Tesla", "Rivian"]
+ models: ["Camry", "Accord", "Civic", "Corolla", "F-150", "Silverado", "RAV4", "CR-V", "Mustang", "Camaro", "3 Series", "5 Series", "C-Class", "E-Class", "A4", "Q5", "Golf", "Jetta", "Altima", "Rogue", "Elantra", "Tucson", "Sportage", "Outback", "CX-5", "RX", "Wrangler", "Grand Cherokee", "Model 3", "Model Y", "Model S", "Model X", "Cybertruck", "R1T", "R1S"]
+ colors: ["Black", "White", "Silver", "Gray", "Red", "Blue", "Green", "Yellow", "Orange", "Brown", "Beige", "Gold", "Burgundy", "Navy", "Teal", "Pearl White", "Metallic Gray", "Midnight Blue", "Racing Red", "Forest Green"]
+ fuel_types: ["Gasoline", "Diesel", "Electric", "Hybrid", "Plug-in Hybrid", "Hydrogen", "E85 Flex Fuel", "Natural Gas"]
+ transmissions: ["Automatic", "Manual", "CVT", "Semi-Automatic", "Dual-Clutch", "Direct Drive"]
+ drive_types: ["Front-Wheel Drive", "Rear-Wheel Drive", "All-Wheel Drive", "Four-Wheel Drive"]
+ vehicle_types: ["Sedan", "SUV", "Truck", "Coupe", "Convertible", "Hatchback", "Wagon", "Van", "Minivan", "Crossover", "Sports Car", "Luxury", "Electric"]
+ doors: ["2", "3", "4", "5"]
+ car_options: ["Leather Seats", "Sunroof", "Navigation System", "Backup Camera", "Bluetooth", "Heated Seats", "Premium Audio", "Apple CarPlay", "Android Auto", "Blind Spot Monitor", "Lane Departure Warning", "Adaptive Cruise Control", "Parking Sensors", "Remote Start", "Keyless Entry"]
+ standard_specs: ["Air Conditioning", "Power Windows", "Power Locks", "AM/FM Radio", "USB Port", "Airbags", "ABS Brakes", "Traction Control", "Stability Control", "Tire Pressure Monitor"]
+ license_plate_patterns: ["???-####", "### ???", "?##-???#", "##?-?##?"]
+```
+
+---
+
+## Java Class Implementation
+
+```java
+package org.thejavaguy.javafaker;
+
+/**
+ * Generates realistic vehicle-related fake data.
+ *
+ * @since 1.x.x
+ */
+public class Vehicle {
+
+ private final Faker faker;
+
+ protected Vehicle(Faker faker) {
+ this.faker = faker;
+ }
+
+ /**
+ * Returns a random vehicle manufacturer name.
+ *
+ * @return vehicle make, e.g., "Toyota", "BMW"
+ */
+ public String make() {
+ return faker.fakeValuesService().fetchString("vehicle.makes");
+ }
+
+ /**
+ * Returns a random vehicle model name.
+ *
+ * @return vehicle model, e.g., "Camry", "Mustang"
+ */
+ public String model() {
+ return faker.fakeValuesService().fetchString("vehicle.models");
+ }
+
+ /**
+ * Returns a random make and model combination.
+ *
+ * @return make and model, e.g., "Toyota Camry"
+ */
+ public String makeAndModel() {
+ return make() + " " + model();
+ }
+
+ /**
+ * Returns a random vehicle color.
+ *
+ * @return color name, e.g., "Red", "Metallic Gray"
+ */
+ public String color() {
+ return faker.fakeValuesService().fetchString("vehicle.colors");
+ }
+
+ /**
+ * Returns a random fuel type.
+ *
+ * @return fuel type, e.g., "Gasoline", "Electric"
+ */
+ public String fuelType() {
+ return faker.fakeValuesService().fetchString("vehicle.fuel_types");
+ }
+
+ /**
+ * Returns a random transmission type.
+ *
+ * @return transmission type, e.g., "Automatic", "Manual"
+ */
+ public String transmission() {
+ return faker.fakeValuesService().fetchString("vehicle.transmissions");
+ }
+
+ /**
+ * Returns a random drive type.
+ *
+ * @return drive type, e.g., "All-Wheel Drive"
+ */
+ public String driveType() {
+ return faker.fakeValuesService().fetchString("vehicle.drive_types");
+ }
+
+ /**
+ * Returns a random vehicle type/body style.
+ *
+ * @return vehicle type, e.g., "Sedan", "SUV"
+ */
+ public String vehicleType() {
+ return faker.fakeValuesService().fetchString("vehicle.vehicle_types");
+ }
+
+ /**
+ * Returns a random door count.
+ *
+ * @return door count as string, e.g., "4", "2"
+ */
+ public String doors() {
+ return faker.fakeValuesService().fetchString("vehicle.doors");
+ }
+
+ /**
+ * Returns a random car option/feature.
+ *
+ * @return car option, e.g., "Sunroof", "Navigation System"
+ */
+ public String carOption() {
+ return faker.fakeValuesService().fetchString("vehicle.car_options");
+ }
+
+ /**
+ * Returns a random standard specification.
+ *
+ * @return standard spec, e.g., "Air Conditioning", "ABS Brakes"
+ */
+ public String standardSpec() {
+ return faker.fakeValuesService().fetchString("vehicle.standard_specs");
+ }
+
+ /**
+ * Generates a random 17-character VIN (Vehicle Identification Number).
+ * Follows the standard VIN format: WMI (3) + VDS (6) + VIS (8).
+ * Uses valid WMI characters (no I, O, Q in positions 1-3, 10).
+ *
+ * @return a 17-character VIN string, e.g., "1HGBH41JXMN109186"
+ */
+ public String vin() {
+ // VIN uses all alphanumeric except I, O, Q
+ return faker.regexify("[A-HJ-NPR-Z0-9]{17}");
+ }
+
+ /**
+ * Generates a random license plate number based on common patterns.
+ *
+ * @return license plate, e.g., "ABC-1234", "123 XYZ"
+ */
+ public String licensePlate() {
+ String pattern = faker.fakeValuesService().fetchString("vehicle.license_plate_patterns");
+ return faker.bothify(pattern);
+ }
+
+ /**
+ * Returns a random model year between the specified range.
+ *
+ * @param minYear minimum year (inclusive)
+ * @param maxYear maximum year (inclusive)
+ * @return a year between minYear and maxYear
+ */
+ public int year(int minYear, int maxYear) {
+ return faker.random().nextInt(maxYear - minYear + 1) + minYear;
+ }
+
+ /**
+ * Returns a random model year between 1990 and current year.
+ *
+ * @return a model year
+ */
+ public int year() {
+ int currentYear = java.time.Year.now().getValue();
+ return year(1990, currentYear);
+ }
+
+ /**
+ * Returns a random mileage/odometer reading.
+ *
+ * @param min minimum mileage
+ * @param max maximum mileage
+ * @return mileage value
+ */
+ public int mileage(int min, int max) {
+ return faker.random().nextInt(max - min + 1) + min;
+ }
+
+ /**
+ * Returns a random mileage between 0 and 200,000.
+ *
+ * @return mileage value
+ */
+ public int mileage() {
+ return mileage(0, 200000);
+ }
+
+ /**
+ * Returns a random engine displacement in liters.
+ *
+ * @return engine size string, e.g., "2.0L", "3.5L"
+ */
+ public String engine() {
+ double[] sizes = {1.0, 1.4, 1.5, 1.6, 1.8, 2.0, 2.4, 2.5, 3.0, 3.5, 4.0, 5.0, 5.7, 6.2};
+ double size = sizes[faker.random().nextInt(sizes.length)];
+ return String.format("%.1fL", size);
+ }
+}
+```
+
+---
+
+## Faker.java Registration
+
+### Field Declaration (add with other fields around line 109)
+
+```java
+private final Vehicle vehicle;
+```
+
+### Constructor Initialization (add in constructor around line 225)
+
+```java
+this.vehicle = new Vehicle(this);
+```
+
+### Getter Method (add with other getters around line 692)
+
+```java
+/**
+ * Returns a Vehicle instance for generating vehicle-related fake data.
+ *
+ * @return a {@link Vehicle} instance
+ */
+public Vehicle vehicle() {
+ return vehicle;
+}
+```
+
+---
+
+## Test Cases
+
+```java
+package org.thejavaguy.javafaker;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+public class VehicleTest extends AbstractFakerTest {
+
+ @Test
+ void make() {
+ assertThat(faker.vehicle().make()).isNotBlank();
+ }
+
+ @Test
+ void model() {
+ assertThat(faker.vehicle().model()).isNotBlank();
+ }
+
+ @Test
+ void makeAndModel() {
+ String result = faker.vehicle().makeAndModel();
+ assertThat(result).isNotBlank();
+ assertThat(result).contains(" ");
+ }
+
+ @Test
+ void color() {
+ assertThat(faker.vehicle().color()).isNotBlank();
+ }
+
+ @Test
+ void fuelType() {
+ assertThat(faker.vehicle().fuelType()).isNotBlank();
+ }
+
+ @Test
+ void transmission() {
+ assertThat(faker.vehicle().transmission()).isNotBlank();
+ }
+
+ @Test
+ void driveType() {
+ assertThat(faker.vehicle().driveType()).isNotBlank();
+ }
+
+ @Test
+ void vehicleType() {
+ assertThat(faker.vehicle().vehicleType()).isNotBlank();
+ }
+
+ @Test
+ void doors() {
+ assertThat(faker.vehicle().doors()).matches("[2-5]");
+ }
+
+ @Test
+ void carOption() {
+ assertThat(faker.vehicle().carOption()).isNotBlank();
+ }
+
+ @Test
+ void standardSpec() {
+ assertThat(faker.vehicle().standardSpec()).isNotBlank();
+ }
+
+ @Test
+ void vin() {
+ // VIN is exactly 17 characters, alphanumeric except I, O, Q
+ assertThat(faker.vehicle().vin()).matches("[A-HJ-NPR-Z0-9]{17}");
+ }
+
+ @Test
+ void licensePlate() {
+ assertThat(faker.vehicle().licensePlate()).matches("[A-Z0-9\\-\\s]+");
+ }
+
+ @Test
+ void year() {
+ int year = faker.vehicle().year();
+ int currentYear = java.time.Year.now().getValue();
+ assertThat(year).isBetween(1990, currentYear);
+ }
+
+ @Test
+ void yearWithRange() {
+ int year = faker.vehicle().year(2010, 2020);
+ assertThat(year).isBetween(2010, 2020);
+ }
+
+ @Test
+ void mileage() {
+ int mileage = faker.vehicle().mileage();
+ assertThat(mileage).isBetween(0, 200000);
+ }
+
+ @Test
+ void mileageWithRange() {
+ int mileage = faker.vehicle().mileage(10000, 50000);
+ assertThat(mileage).isBetween(10000, 50000);
+ }
+
+ @Test
+ void engine() {
+ assertThat(faker.vehicle().engine()).matches("\\d+\\.\\d+L");
+ }
+}
+```
+
+---
+
+## Example Outputs
+
+```java
+faker.vehicle().make() // "Toyota"
+faker.vehicle().model() // "Camry"
+faker.vehicle().makeAndModel() // "Honda Accord"
+faker.vehicle().color() // "Midnight Blue"
+faker.vehicle().fuelType() // "Hybrid"
+faker.vehicle().transmission() // "Automatic"
+faker.vehicle().driveType() // "All-Wheel Drive"
+faker.vehicle().vehicleType() // "SUV"
+faker.vehicle().doors() // "4"
+faker.vehicle().carOption() // "Sunroof"
+faker.vehicle().standardSpec() // "Air Conditioning"
+faker.vehicle().vin() // "1HGBH41JXMN109186"
+faker.vehicle().licensePlate() // "ABC-1234"
+faker.vehicle().year() // 2019
+faker.vehicle().mileage() // 45230
+faker.vehicle().engine() // "2.0L"
+```
+
+---
+
+## Verification Checklist
+
+- [ ] YAML file created at `src/main/resources/en/vehicle.yml`
+- [ ] **CRITICAL: YAML registered in `EnFile.java` FILES list**
+- [ ] **Verify YAML loads correctly with simple test before implementing full class**
+- [ ] Java class created at `src/main/java/org/thejavaguy/javafaker/Vehicle.java`
+- [ ] Field added to `Faker.java`
+- [ ] Constructor initialization added to `Faker.java`
+- [ ] Getter method added to `Faker.java`
+- [ ] Test class created at `src/test/java/org/thejavaguy/javafaker/VehicleTest.java`
+- [ ] All tests pass: `./mvnw test -Dtest=VehicleTest`
+- [ ] VIN format generates valid 17-character strings
+- [ ] License plate format varies based on patterns
diff --git a/specs/pre-commit-hooks-plan.md b/specs/pre-commit-hooks-plan.md
new file mode 100644
index 0000000..e3b36ee
--- /dev/null
+++ b/specs/pre-commit-hooks-plan.md
@@ -0,0 +1,110 @@
+# Pre-commit Hooks Setup Plan for java-faker
+
+## Overview
+Set up Lefthook (Go-based, no Python dependency) with pre-commit hooks that integrate with the existing `justfile` workflow.
+
+## Components to Add
+
+### 1. Lefthook Configuration
+**File:** `lefthook.yml` (project root)
+
+```yaml
+pre-commit:
+ parallel: true
+ commands:
+ compile:
+ run: just build
+
+ format-check:
+ glob: "*.java"
+ run: just format-check
+
+ yaml-validate:
+ glob: "*.{yml,yaml}"
+ run: just yaml-check
+
+commit-msg:
+ commands:
+ conventional:
+ run: just commit-msg-check {1}
+```
+
+### 2. Code Formatting (Google Java Format via Spotless)
+**File:** `pom.xml` - Add Spotless Maven Plugin
+
+Spotless will:
+- Format Java code using Google Java Format style
+- Run as `mvn spotless:check` (verify) or `mvn spotless:apply` (fix)
+
+### 3. Justfile Updates
+**File:** `justfile` - Add new recipes
+
+| Recipe | Command | Purpose |
+|--------|---------|---------|
+| `format` | `mvn spotless:apply` | Auto-format all Java files |
+| `format-check` | `mvn spotless:check` | Verify formatting (for CI/hooks) |
+| `yaml-check` | Validate YAML files | Check locale files syntax |
+| `commit-msg-check` | Validate commit message | Enforce conventional commits (optional) |
+| `hooks-install` | `lefthook install` | Set up git hooks |
+
+### 4. Documentation
+**File:** `README.md` or `CONTRIBUTING.md` - Add setup instructions
+
+---
+
+## Files to Modify
+
+| File | Action |
+|------|--------|
+| `lefthook.yml` | Create new |
+| `pom.xml` | Add Spotless plugin |
+| `justfile` | Add format/hook recipes |
+
+---
+
+## Installation Steps (for contributors)
+
+1. Install Lefthook: `brew install lefthook` (or download binary)
+2. Install hooks: `just hooks-install`
+3. (Optional) Format existing code: `just format`
+
+---
+
+## What Each Hook Does
+
+### Pre-commit Hooks
+1. **compile** - Runs `mvn compile` to catch syntax errors
+2. **format-check** - Verifies Java files are formatted correctly (fails if not)
+3. **yaml-validate** - Validates YAML syntax in locale files
+
+### Commit-msg Hook (Optional)
+- Enforces conventional commit format (e.g., `feat:`, `fix:`, `docs:`)
+
+---
+
+## Verification
+
+After implementation:
+1. Run `just hooks-install` to install hooks
+2. Make a test commit with unformatted code → should fail
+3. Run `just format` then commit again → should pass
+4. Run `just format-check` in CI to ensure consistency
+
+---
+
+## Trade-offs Considered
+
+| Decision | Choice | Rationale |
+|----------|--------|-----------|
+| Hook framework | Lefthook | No Python dependency, single Go binary |
+| Formatter | Spotless + Google Java Format | Maven-native, widely adopted style |
+| YAML validation | Simple syntax check | Sufficient for locale files |
+| Test execution | Not in pre-commit | Too slow; rely on CI |
+
+---
+
+## Optional Extensions (Not Included)
+
+- Checkstyle for additional static analysis
+- License header checks
+- Commit message linting (can add if desired)