From d5807a05f7fd2a70f6c449b9d77c7f984a3eb1a9 Mon Sep 17 00:00:00 2001 From: "Ivan Milosavljevic (TheJavaGuy)" Date: Mon, 9 Feb 2026 08:57:29 +0100 Subject: [PATCH] add shipping faker for logistics and tracking data Implement comprehensive Shipping faker module to generate realistic shipping and logistics test data including: - Tracking numbers for major carriers (UPS, FedEx, USPS, DHL) - Shipping methods, statuses, and package types - Package dimensions, weights, and costs - Delivery dates and time windows - Facility locations and tracking events - Handling instructions and exception types Includes 32 test cases with full coverage of all methods. --- .../java/org/thejavaguy/javafaker/Faker.java | 11 + .../org/thejavaguy/javafaker/Shipping.java | 379 ++++++++++++++++++ .../javafaker/service/files/EnFile.java | 1 + src/main/resources/en/shipping.yml | 11 + .../thejavaguy/javafaker/ShippingTest.java | 217 ++++++++++ 5 files changed, 619 insertions(+) create mode 100644 src/main/java/org/thejavaguy/javafaker/Shipping.java create mode 100644 src/main/resources/en/shipping.yml create mode 100644 src/test/java/org/thejavaguy/javafaker/ShippingTest.java diff --git a/src/main/java/org/thejavaguy/javafaker/Faker.java b/src/main/java/org/thejavaguy/javafaker/Faker.java index ff96e56..6b43868 100644 --- a/src/main/java/org/thejavaguy/javafaker/Faker.java +++ b/src/main/java/org/thejavaguy/javafaker/Faker.java @@ -107,6 +107,7 @@ public class Faker { private final Sip sip; private final EnglandFootBall englandfootball; private final Mountain mountain; + private final Shipping shipping; public Faker() { this(Locale.ENGLISH); @@ -223,6 +224,7 @@ public Faker(FakeValuesService fakeValuesService, RandomService random) { this.sip = new Sip(this); this.englandfootball = new EnglandFootBall(this); this.mountain = new Mountain(this); + this.shipping = new Shipping(this); } /** @@ -691,6 +693,15 @@ public StarCraft starCraft() { public Mountain mountain() { return mountain; } + /** + * Returns a Shipping instance for generating shipping-related fake data. + * + * @return a {@link Shipping} instance + */ + public Shipping shipping() { + return shipping; + } + public String resolve(String key) { return this.fakeValuesService.resolve(key, this, this); } diff --git a/src/main/java/org/thejavaguy/javafaker/Shipping.java b/src/main/java/org/thejavaguy/javafaker/Shipping.java new file mode 100644 index 0000000..78f05fb --- /dev/null +++ b/src/main/java/org/thejavaguy/javafaker/Shipping.java @@ -0,0 +1,379 @@ +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; + } + } +} diff --git a/src/main/java/org/thejavaguy/javafaker/service/files/EnFile.java b/src/main/java/org/thejavaguy/javafaker/service/files/EnFile.java index 424be84..f5b0bfe 100644 --- a/src/main/java/org/thejavaguy/javafaker/service/files/EnFile.java +++ b/src/main/java/org/thejavaguy/javafaker/service/files/EnFile.java @@ -145,6 +145,7 @@ public String getPath() { "science.yml", "seinfeld.yml", "shakespeare.yml", + "shipping.yml", "silicon_valley.yml", "simpsons.yml", "singular_siegler.yml", diff --git a/src/main/resources/en/shipping.yml b/src/main/resources/en/shipping.yml new file mode 100644 index 0000000..e1b41f0 --- /dev/null +++ b/src/main/resources/en/shipping.yml @@ -0,0 +1,11 @@ +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"] diff --git a/src/test/java/org/thejavaguy/javafaker/ShippingTest.java b/src/test/java/org/thejavaguy/javafaker/ShippingTest.java new file mode 100644 index 0000000..fb8ad32 --- /dev/null +++ b/src/test/java/org/thejavaguy/javafaker/ShippingTest.java @@ -0,0 +1,217 @@ +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(); + } +}