dodil/erp-brick-sales-order

DODIL ERP brick: sales order

tests/test_so51.py

"""SO-51 (spec v9, kept in v10): timesheets delivers approved hours on a service line; a correction moves the line by the
difference; the same delivery_ref records once. timesheets is not built: `TimesheetsFake` is its outbound TS-12 call, checked on
both sides against the signed specs (contract_checks.inbound_check)."""
from decimal import Decimal

from conftest import ADMIN, BILLER, FULFILLER, api, confirm, confirmed, get, hand, must, person, refused
from contract_checks import inbound_check
from test_stories import FEES, TODAY, WIDGETS, D, invoice, line

TIMESHEETS = {**person("timesheets@shop.example", ["so:order:deliver", "so:order:read"]), "X-Dodil-Auth-Source": "platform"}   # timesheets' service client
HOURS = lambda n: [{"product_id": "SETUP", "quantity": str(n), "unit_code": "HUR"}]                                           # noqa: E731  a service line, delivered policy


class TimesheetsFake:
    """What timesheets sends on approval (its TS-12): one delivery per approved line revision, a correction naming the earlier one."""

    def deliver(self, order, line_no, quantity, sheet="4410:2026-W47", rev=1, corrects=None, on=TODAY, who=TIMESHEETS):
        body = {"line_no": line_no, "quantity": f"{Decimal(quantity):.4f}", "source": "timesheets",
                "delivery_ref": f"ts:{sheet}:r{rev}:{line_no}", "delivered_on": on}
        if corrects:
            body["corrects_ref"] = corrects
        path = f"/sales-orders/{order['sales_order_id']}/deliveries"
        inbound_check("timesheets", "POST", path, body)
        return api("POST", path, who, body)


def test_SO_51_hours_are_delivered_once_and_a_correction_moves_the_line_by_the_difference():
    o = confirmed(lines=HOURS(120))
    ts = TimesheetsFake()
    must(ts.deliver(o, 1, 40, rev=0, sheet="4410:2026-W46"))                                  # 40 already delivered
    first = must(ts.deliver(o, 1, 19))
    assert D(line(first, 1)["delivered"]) == 59
    again = must(ts.deliver(o, 1, 19))                                                        # the same ref twice records once
    assert D(line(again, 1)["delivered"]) == 59 and len(again["deliveries"]) == 2 and again["version"] == first["version"]
    fixed = must(ts.deliver(o, 1, -2, rev=2, corrects="ts:4410:2026-W47:r1:1"))
    assert D(line(fixed, 1)["delivered"]) == 57
    assert [(d["delivery_ref"], d["kind"], d["source"]) for d in fixed["deliveries"]][-1] == ("ts:4410:2026-W47:r2:1", "correction", "timesheets")
    assert D(line(get(o["sales_order_id"]), 1)["to_invoice"]) == 57                           # the order invoices delivered quantities (SO-21)


def test_SO_51_a_goods_line_is_refused_and_so_is_more_than_ordered():
    o = confirmed(lines=WIDGETS(12) + HOURS(10))
    ts = TimesheetsFake()
    s, b = ts.deliver(o, 1, 2)
    assert s == 409 and b.get("gate") == "service_line" and b["fix"], b
    must(ts.deliver(o, 2, 8))
    s, b = ts.deliver(o, 2, 3, sheet="4411:2026-W47")
    assert s == 409 and b["gate"] == "within_ordered"
    assert D(line(get(o["sales_order_id"]), 2)["delivered"]) == 8


def test_SO_51_a_correction_below_what_is_invoiced_is_refused_with_the_way_out():
    o = confirmed(lines=HOURS(20))
    ts = TimesheetsFake()
    must(ts.deliver(o, 1, 10))
    must(invoice(get(o["sales_order_id"])))                                                   # 10 hours invoiced
    s, b = ts.deliver(o, 1, -4, rev=2, corrects="ts:4410:2026-W47:r1:1")
    assert s == 409 and b["gate"] == "not_below_invoiced" and "credit the invoice in invoicing" in b["fix"]
    assert D(line(get(o["sales_order_id"]), 1)["delivered"]) == 10


def test_SO_51_only_timesheets_service_client_names_that_source_and_its_ref_is_a_ts_ref():
    o = confirmed(lines=HOURS(10))
    body = {"line_no": 1, "quantity": "2", "source": "timesheets", "delivery_ref": "ts:1:r1:1", "delivered_on": TODAY}
    path = f"/sales-orders/{o['sales_order_id']}/deliveries"
    assert api("POST", path, FULFILLER, body)[0] == 403                                       # a person cannot claim to be timesheets
    assert api("POST", path, TIMESHEETS, {**body, "delivery_ref": "manual-1"})[0] == 422
    assert api("POST", path, TIMESHEETS, {k: v for k, v in body.items() if k != "delivered_on"})[0] == 422
    assert D(line(get(o["sales_order_id"]), 1)["delivered"]) == 0


def test_the_fake_cannot_send_what_the_signed_specs_do_not_allow():
    o = confirmed(lines=HOURS(10))
    path = f"/sales-orders/{o['sales_order_id']}/deliveries"
    import pytest
    with pytest.raises(AssertionError, match="breaks the signed specs"):
        inbound_check("timesheets", "POST", path, {"line_no": 1, "quantity": "1", "source": "manual", "delivery_ref": "ts:1:r1:1", "delivered_on": TODAY})
    from contract_checks import CONTRACT
    CONTRACT.violations.clear()