Functional Weave
Code in Python

retail.coupon-validate@1.0.0

impl/python.py

2,371 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import re
from typing import List, Sequence

from .money_compare import compare_money  ← from money.compare ^1.0.0 · built alongside by fune
from .money_sum import sum_money  ← from money.sum ^1.0.0 · built alongside by fune
from .retail_coupon_validate_types import Coupon, CouponLine, CouponCheck

_ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")


def _is_eligible(coupon: Coupon, line: CouponLine) -> bool:
    open_to_all = len(coupon.skus) == 0 and len(coupon.categories) == 0
    listed = open_to_all or line.sku in coupon.skus or line.category in coupon.categories
    excluded = line.sku in coupon.excluded_skus or line.category in coupon.excluded_categories
    return listed and not excluded


def validate_coupon(coupon: Coupon, lines: Sequence[CouponLine], customer_uses: int, on_date: str) -> CouponCheck:
    """Whether a coupon can be used on a basket, with every reason it cannot.

    ISO dates compare correctly as strings.
    """
    if not isinstance(on_date, str) or not _ISO_DATE.match(on_date):
        raise ValueError('onDate must be an ISO date (YYYY-MM-DD), received "%s"' % (on_date,))
    if isinstance(customer_uses, bool) or not isinstance(customer_uses, int) or customer_uses < 0:
        raise ValueError("customerUses must not be negative, received %r" % (customer_uses,))

    eligible_lines = [i for i, line in enumerate(lines) if _is_eligible(coupon, line)]

    reasons: List[str] = []
    if coupon.valid_from is not None and on_date < coupon.valid_from:
        reasons.append("not-yet-valid")
    if coupon.valid_to is not None and on_date > coupon.valid_to:
        reasons.append("expired")
    if coupon.max_uses is not None and coupon.times_used >= coupon.max_uses:
        reasons.append("usage-limit-reached")
    if coupon.max_uses_per_customer is not None and customer_uses >= coupon.max_uses_per_customer:
        reasons.append("customer-limit-reached")
    if len(eligible_lines) == 0:
        reasons.append("no-eligible-items")
    if coupon.minimum_spend is not None:
        if coupon.minimum_spend_on == "eligible":
            counted = [lines[i].total for i in eligible_lines]
        else:
            counted = [line.total for line in lines]
        spend = sum_money(counted, coupon.minimum_spend.currency)
        if compare_money(spend, coupon.minimum_spend) < 0:
            reasons.append("minimum-spend-not-met")

    return CouponCheck(valid=len(reasons) == 0, reasons=reasons, eligible_lines=eligible_lines)