food-market/tests/test_journal.py

268 lines
9.7 KiB
Python
Raw Permalink Normal View History

import json
from datetime import date, timedelta
import pytest
from app import journal
from app.models import KIND_FRIEND, KIND_RETAIL, KIND_SELF
def actions(vault):
return [e.action for e in vault.doc.journal]
def last(vault):
return vault.doc.journal[-1]
def changed(entry):
return {c.field: (c.old, c.new) for c in entry.changes}
# --- запись вообще происходит ---------------------------------------------
def test_create_product_is_logged(vault):
journal.create_product(vault, "Булка с маком", cost_price=20, retail_price=35)
entry = last(vault)
assert entry.action == "product.create"
assert entry.label == "Булка с маком"
assert changed(entry)["себестоимость"][1] == "20,00 ₽"
def test_price_change_records_old_and_new(vault, buns):
journal.change_prices(vault, buns["мак"].id, cost_price=25, retail_price=40)
entry = last(vault)
assert entry.action == "product.price_change"
assert changed(entry)["себестоимость"] == ("20,00 ₽", "25,00 ₽")
assert changed(entry)["цена продажи"] == ("35,00 ₽", "40,00 ₽")
def test_price_change_appends_to_history(vault, buns):
product = buns["мак"]
assert len(product.price_history) == 1 # точка при создании
journal.change_prices(vault, product.id, cost_price=25, retail_price=40)
assert len(product.price_history) == 2
assert product.price_history[-1].cost_price == 25
def test_every_operation_touches_the_vault(vault, buns, today, in_two_weeks):
"""revision растёт на каждой мутации — на него опирается автосохранение."""
before = vault.revision
journal.create_batch(
vault, today, in_two_weeks, [{"product_id": buns["мак"].id, "qty": 10, "unit_cost": 20}]
)
assert vault.revision > before
def test_journal_covers_the_full_lifecycle(vault, buns, today, in_two_weeks):
batch = journal.create_batch(
vault, today, in_two_weeks, [{"product_id": buns["мак"].id, "qty": 10, "unit_cost": 20}]
)
sale = journal.create_sale(
vault, today, KIND_RETAIL, [{"product_id": buns["мак"].id, "qty": 2, "unit_price": 35}],
paid_amount=0,
)
journal.add_sale_payment(vault, sale.id, 70)
journal.add_bakery_payment(vault, 200, batch_id=batch.id)
journal.delete_sale(vault, sale.id)
assert actions(vault)[-5:] == [
"batch.create",
"sale.create",
"sale.payment_add",
"bakery_payment.add",
"sale.delete",
]
# --- пустые правки не создают шума ----------------------------------------
def test_noop_update_writes_nothing(vault, buns):
count = len(vault.doc.journal)
journal.update_product(vault, buns["мак"].id, name="Булка с маком")
assert len(vault.doc.journal) == count
def test_noop_price_change_writes_nothing(vault, buns):
count = len(vault.doc.journal)
journal.change_prices(vault, buns["мак"].id, cost_price=20, retail_price=35)
assert len(vault.doc.journal) == count
assert len(buns["мак"].price_history) == 1
def test_noop_edit_leaves_vault_clean(vault, buns):
"""Иначе открыл-закрыл форму — и в git уехал пустой коммит."""
vault.save()
journal.update_product(vault, buns["мак"].id, note="")
assert vault.is_dirty is False
# --- секреты не утекают в журнал ------------------------------------------
def test_token_value_never_reaches_the_journal(vault):
secret = "glpat-СУПЕРСЕКРЕТ12345"
journal.update_settings(vault, git={"token": secret})
entry = last(vault)
assert entry.action == "settings.update"
assert changed(entry)["git.token"] == ("скрыто", "изменён")
# Токен обязан быть в настройках, но нигде в журнале — включая открытый экспорт.
assert vault.doc.settings.git.token == secret
dumped = json.dumps([e.to_dict() for e in vault.doc.journal], ensure_ascii=False)
assert secret not in dumped
def test_non_secret_git_settings_are_logged_plainly(vault):
journal.update_settings(vault, git={"remote_url": "https://gt.ser.gay/kizya/food-market.git"})
assert "gt.ser.gay" in changed(last(vault))["git.remote_url"][1]
def test_settings_keep_types(vault):
journal.update_settings(vault, git={"auto_push_minutes": "30", "enabled": False})
assert vault.doc.settings.git.auto_push_minutes == 30
assert vault.doc.settings.git.enabled is False
# --- проверки корректности ------------------------------------------------
def test_product_in_use_cannot_be_deleted(vault, buns, today, in_two_weeks):
journal.create_batch(
vault, today, in_two_weeks, [{"product_id": buns["мак"].id, "qty": 10, "unit_cost": 20}]
)
with pytest.raises(journal.ValidationError, match="архив"):
journal.delete_product(vault, buns["мак"].id)
def test_unused_product_can_be_deleted(vault, buns):
journal.delete_product(vault, buns["мак"].id)
assert vault.doc.product(buns["мак"].id) is None
assert last(vault).action == "product.delete"
def test_batch_due_date_must_not_precede_purchase(vault, buns, today):
with pytest.raises(journal.ValidationError, match="раньше"):
journal.create_batch(
vault,
today,
today - timedelta(days=1),
[{"product_id": buns["мак"].id, "qty": 10, "unit_cost": 20}],
)
def test_empty_batch_rejected(vault, today, in_two_weeks):
with pytest.raises(journal.ValidationError, match="хотя бы одна"):
journal.create_batch(vault, today, in_two_weeks, [])
def test_zero_quantity_rejected(vault, buns, today, in_two_weeks):
with pytest.raises(journal.ValidationError, match="больше нуля"):
journal.create_batch(
vault, today, in_two_weeks, [{"product_id": buns["мак"].id, "qty": 0, "unit_cost": 20}]
)
def test_counterparty_with_sales_cannot_be_deleted(vault, buns, today):
vasya = journal.create_counterparty(vault, "Вася")
journal.create_sale(
vault, today, KIND_FRIEND,
[{"product_id": buns["мак"].id, "qty": 1, "unit_price": 20}],
counterparty_id=vasya.id,
)
with pytest.raises(journal.ValidationError, match="удалить нельзя"):
journal.delete_counterparty(vault, vasya.id)
def test_batch_numbers_increment(vault, buns, today, in_two_weeks):
lines = [{"product_id": buns["мак"].id, "qty": 1, "unit_cost": 20}]
first = journal.create_batch(vault, today, in_two_weeks, lines)
second = journal.create_batch(vault, today, in_two_weeks, lines)
assert (first.number, second.number) == (1, 2)
# --- поведение вокруг оплат -----------------------------------------------
def test_sale_is_paid_in_full_by_default(vault, buns, today):
sale = journal.create_sale(
vault, today, KIND_RETAIL, [{"product_id": buns["мак"].id, "qty": 2, "unit_price": 35}]
)
assert sale.paid == sale.total == 70
assert sale.debt == 0
def test_sale_on_credit_creates_debt(vault, buns, today):
sale = journal.create_sale(
vault, today, KIND_FRIEND, [{"product_id": buns["мак"].id, "qty": 5, "unit_price": 20}],
paid_amount=0,
)
assert sale.paid == 0
assert sale.debt == 100
journal.add_sale_payment(vault, sale.id, 60)
assert sale.debt == 40
def test_consumption_never_records_payment(vault, buns, today):
"""Съел сам — денег нет по определению, даже если форма что-то прислала."""
sale = journal.create_sale(
vault, today, KIND_SELF, [{"product_id": buns["мак"].id, "qty": 2, "unit_price": 0}],
paid_amount=999,
)
assert sale.payments == []
assert sale.debt == 0
def test_overpayment_does_not_create_negative_debt(vault, buns, today):
sale = journal.create_sale(
vault, today, KIND_RETAIL, [{"product_id": buns["мак"].id, "qty": 1, "unit_price": 35}],
paid_amount=0,
)
journal.add_sale_payment(vault, sale.id, 100)
assert sale.debt == 0
# --- удаление партии не теряет деньги -------------------------------------
def test_deleting_batch_keeps_bakery_payments(vault, buns, today, in_two_weeks):
"""Деньги пекарне реально ушли — они не должны исчезнуть вместе с партией."""
batch = journal.create_batch(
vault, today, in_two_weeks, [{"product_id": buns["мак"].id, "qty": 10, "unit_cost": 20}]
)
payment = journal.add_bakery_payment(vault, 200, batch_id=batch.id)
journal.delete_batch(vault, batch.id)
assert payment in vault.doc.bakery_payments
assert payment.batch_id is None
# --- обслуживание ---------------------------------------------------------
def test_prune_journal(vault, buns):
for entry in vault.doc.journal:
entry.ts = "2020-01-01T00:00:00+03:00"
removed = journal.prune_journal(vault, date(2024, 1, 1))
assert removed == 3
assert [e.action for e in vault.doc.journal] == ["journal.prune"]
def test_prune_nothing_to_remove_is_silent(vault, buns):
count = len(vault.doc.journal)
assert journal.prune_journal(vault, date(2000, 1, 1)) == 0
assert len(vault.doc.journal) == count
def test_journal_entries_survive_save_and_reload(vault, buns):
from app.storage import Vault
vault.save()
reopened = Vault.open(vault.path, "pw")
assert [e.action for e in reopened.doc.journal] == ["product.create"] * 3
assert reopened.doc.journal[0].host