        lo = float(self._cfg("shipping_min", 5.0))
        hi = float(self._cfg("shipping_max", 50.0))
        if hi < lo:
            lo, hi = hi, lo
        return round(self.rng.uniform(lo, hi), 2)

    def generate_random_name(self) -> str:
        first = ("James", "Mary", "Robert", "Patricia", "John", "Jennifer", "Michael",
                 "Linda", "David", "Elizabeth", "William", "Barbara", "Richard", "Susan",
                 "Joseph", "Jessica", "Thomas", "Sarah", "Charles", "Karen", "Ava", "Noah")
        last = ("Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller",
                "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez",
                "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin")
        return f"{self.rng.choice(first)} {self.rng.choice(last)}"

    # ------------------------------------------------------------------ substitution
    def substitute(self, text: str, recipient: dict, generated: GeneratedValues,
                   custom_fields: Optional[dict] = None, extra: Optional[dict] = None) -> SubstitutionResult:
        recipient = recipient or {}
        custom_fields = custom_fields or {}
        extra = extra or {}
        unknown, missing, used = [], [], []
        pool = dict(recipient)
        pool.update(custom_fields)
        pool.update(extra)

        def replace_fn(m):
            var = m.group(1)
            if var in ("RandomInvoiceNumber", "RandomTotal", "RandomShippingCharge", "RandomName",
                       "InvoiceDate", "DueDate", "UnsubscribeLink", "CompanyPhone"):
                return m.group(0)  # handled below by generated values pass
            if var in pool:
                val = pool.get(var)
                if val is None or str(val).strip() == "":
                    if var in KNOWN_VARIABLES:
                        missing.append(var)
                    return f"[{var}]"
                used.append(var)
                return str(val)
            if var in KNOWN_VARIABLES:
                missing.append(var)
                return f"[{var}]"
            unknown.append(var)
            return f"[UNKNOWN:{var}]"

        text = re.sub(VARIABLE_PATTERN, replace_fn, text)
