Converts the ad-hoc scratch-script verification pattern used throughout this session's development (temp SQLite DB + create_app() + monkeypatched send_message/fiscalize_in_background/get_invoice_status, print-based check() assertions) into a real, repo-committed pytest suite under tests/. Covers: claim_order_paid() atomicity, cross-path payment race dedup (webhook/checkout-poll/reconciliation all observing the same paid invoice), fiscalization error alerting + cooldown, admin login brute-force lockout, the insecure-defaults startup check, supplier-import path-traversal protection, basic checkout smoke, and the supplier restock -> stock/supplier-link bug fixed earlier this session. Adds requirements-dev.txt (pytest only, keeping the existing monkeypatch-only style rather than a new mocking framework), pytest.ini so `pytest` runs from the repo root with no flags, and docs/TESTING.md covering how to run it and what's explicitly not covered yet (the React webapp, bot conversation flows beyond the checkout API, live Checkbox/Monobank/Google Drive integrations). 38 tests, all passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
124 lines
4.8 KiB
Python
124 lines
4.8 KiB
Python
"""bober_bbq.admin.suppliers._apply_stock_topup(): restocking via a
|
||
supplier delivery must credit Ingredient.current_stock, auto-assign
|
||
Ingredient.supplier_id only when it was previously unset (never overwrite
|
||
an existing link), and leave StockMovement.note exactly as the admin
|
||
typed it — NOT overwritten with the supplier's name, which was a real bug
|
||
fixed earlier this session (the note field duplicated information already
|
||
available via the ingredient's own supplier link, surfaced separately in
|
||
inventory reports).
|
||
"""
|
||
|
||
from bober_bbq.admin.suppliers import _apply_stock_topup
|
||
from bober_bbq.extensions import db
|
||
from bober_bbq.models import Ingredient, StockMovement, Supplier, SupplierDelivery
|
||
|
||
|
||
def _make_supplier(name):
|
||
supplier = Supplier(name=name)
|
||
db.session.add(supplier)
|
||
db.session.commit()
|
||
return supplier
|
||
|
||
|
||
def test_topup_credits_stock_and_assigns_unset_supplier(app):
|
||
with app.app_context():
|
||
supplier = _make_supplier("Постачальник Ковбаси")
|
||
ingredient = Ingredient(name="Ковбаса", unit="кг", current_stock=5, supplier_id=None)
|
||
db.session.add(ingredient)
|
||
db.session.commit()
|
||
|
||
delivery = SupplierDelivery(
|
||
supplier_id=supplier.id,
|
||
ingredient_id=ingredient.id,
|
||
product_name="Ковбаса",
|
||
quantity=10,
|
||
delivery_sum=200,
|
||
note="Партія trohy volog",
|
||
)
|
||
db.session.add(delivery)
|
||
db.session.commit()
|
||
|
||
_apply_stock_topup(delivery)
|
||
|
||
assert ingredient.current_stock == 15.0
|
||
assert ingredient.supplier_id == supplier.id, "an unset supplier link must be auto-assigned from the delivery"
|
||
|
||
|
||
def test_topup_never_overwrites_an_existing_supplier_link(app):
|
||
with app.app_context():
|
||
original_supplier = _make_supplier("Оригінальний постачальник")
|
||
other_supplier = _make_supplier("Інший постачальник")
|
||
ingredient = Ingredient(name="Сіль", unit="кг", current_stock=1, supplier_id=original_supplier.id)
|
||
db.session.add(ingredient)
|
||
db.session.commit()
|
||
|
||
delivery = SupplierDelivery(
|
||
supplier_id=other_supplier.id,
|
||
ingredient_id=ingredient.id,
|
||
product_name="Сіль",
|
||
quantity=5,
|
||
delivery_sum=50,
|
||
)
|
||
db.session.add(delivery)
|
||
db.session.commit()
|
||
|
||
_apply_stock_topup(delivery)
|
||
|
||
assert ingredient.current_stock == 6.0
|
||
assert ingredient.supplier_id == original_supplier.id, "an already-set supplier link must survive a topup from a different supplier"
|
||
|
||
|
||
def test_topup_stock_movement_note_is_exactly_what_the_admin_typed(app):
|
||
"""Regression test for a real production bug: the StockMovement.note
|
||
created by a restock was being overwritten with the supplier's name
|
||
instead of preserving the admin's own free-text note."""
|
||
with app.app_context():
|
||
supplier = _make_supplier("Постачальник Пряме Поповнення")
|
||
ingredient = Ingredient(name="Кетчуп", unit="кг", current_stock=5, supplier_id=supplier.id)
|
||
db.session.add(ingredient)
|
||
db.session.commit()
|
||
|
||
admin_note = "накладна №НК-9, часткова оплата готівкою"
|
||
delivery = SupplierDelivery(
|
||
supplier_id=supplier.id,
|
||
ingredient_id=ingredient.id,
|
||
product_name="Кетчуп",
|
||
quantity=10,
|
||
delivery_sum=200,
|
||
reference="НК-9",
|
||
note=admin_note,
|
||
)
|
||
db.session.add(delivery)
|
||
db.session.commit()
|
||
|
||
_apply_stock_topup(delivery)
|
||
|
||
movement = StockMovement.query.filter_by(ingredient_id=ingredient.id, reason="restock").first()
|
||
assert movement is not None
|
||
assert movement.note == admin_note, "the movement note must be exactly what the admin typed, not the supplier's name"
|
||
assert supplier.name not in (movement.note or ""), "the supplier name must not be injected into the note"
|
||
|
||
|
||
def test_topup_with_no_note_leaves_movement_note_blank(app):
|
||
with app.app_context():
|
||
supplier = _make_supplier("Постачальник Без Нотатки")
|
||
ingredient = Ingredient(name="Цукор", unit="кг", current_stock=0, supplier_id=None)
|
||
db.session.add(ingredient)
|
||
db.session.commit()
|
||
|
||
delivery = SupplierDelivery(
|
||
supplier_id=supplier.id,
|
||
ingredient_id=ingredient.id,
|
||
product_name="Цукор",
|
||
quantity=20,
|
||
delivery_sum=400,
|
||
note=None,
|
||
)
|
||
db.session.add(delivery)
|
||
db.session.commit()
|
||
|
||
_apply_stock_topup(delivery)
|
||
|
||
movement = StockMovement.query.filter_by(ingredient_id=ingredient.id, reason="restock").first()
|
||
assert movement is not None
|
||
assert movement.note is None
|