import re from typing import Optional, Sequence from .math_round_div import round_div from .money_amount import Money, money from .money_compare import compare_money from .retail_volumetric_weight import chargeable_weight from .retail_shipping_rate_types import ShippingBand, Parcel, ShippingQuote _ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}") def _whole(value: object) -> bool: return isinstance(value, int) and not isinstance(value, bool) def _chargeable_in(band: ShippingBand, parcel: Parcel) -> int: if band.volumetric_divisor is None: return round_div(parcel.actual_grams, band.round_up_to_grams, "up") * band.round_up_to_grams return chargeable_weight( parcel.length_mm, parcel.width_mm, parcel.height_mm, parcel.actual_grams, band.volumetric_divisor, band.round_up_to_grams, ).chargeable_grams def shipping_rate( bands: Sequence[ShippingBand], service: str, zone: str, parcel: Parcel, basket_total: Money, on_date: str, ) -> ShippingQuote: """The delivery charge for a parcel: the smallest band on the rate card in force on the order date that takes it, free when the basket reaches the band's threshold. """ if not isinstance(on_date, str) or not _ISO_DATE.fullmatch(on_date): raise ValueError('onDate must be an ISO date (YYYY-MM-DD), received "%s"' % (on_date,)) for d in (parcel.length_mm, parcel.width_mm, parcel.height_mm): if not _whole(d) or d < 1: raise ValueError("dimensions must be 1 mm or more, received %r" % (d,)) if not _whole(parcel.actual_grams) or parcel.actual_grams < 0: raise ValueError("actualGrams must not be negative, received %r" % (parcel.actual_grams,)) longest = max(parcel.length_mm, parcel.width_mm, parcel.height_mm) best: Optional[ShippingBand] = None best_grams = 0 for band in bands: if band.service != service or band.zone != zone: continue if on_date < band.valid_from or (band.valid_to is not None and on_date > band.valid_to): continue if band.max_length_mm is not None and longest > band.max_length_mm: continue grams = _chargeable_in(band, parcel) if grams > band.max_grams: continue if ( best is None or band.max_grams < best.max_grams or (band.max_grams == best.max_grams and compare_money(band.price, best.price) < 0) ): best = band best_grams = grams if best is None: raise ValueError( 'no shipping band for service "%s" to zone "%s" on %s fits this parcel' % (service, zone, on_date) ) free = best.free_over is not None and compare_money(basket_total, best.free_over) >= 0 return ShippingQuote( service=service, zone=zone, chargeable_grams=best_grams, band_max_grams=best.max_grams, standard_price=best.price, price=money(0, best.price.currency) if free else best.price, free=free, )