food-market/tests/test_storage.py
food-market 950ccc33b3 Своя закупочная цена у фасовки
Коробка печенья из 20 штук обходится дешевле, чем 20 штук поодиночке —
ради этого её и берут. Своей была только цена продажи, а себестоимость
выводилась умножением поштучной цены на размер фасовки, то есть опт
считался по цене розницы. В каждой закупке сумму приходилось править
руками.

Теперь у фасовки обе цены свои и обе за упаковку целиком. Ноль означает
«своей цены нет, считай от базовой» — прежнее поведение, поэтому цифры
в уже заведённых базах не поехали.

Карточка товара расшифровывает результат построчно: «коробка = 20 шт ·
закупка 300,00 ₽ (15,00 ₽ за шт) · продажа 500,00 ₽ (25,00 ₽ за шт)».
Цена вводится за упаковку, а думает человек о ней поштучно, и без
пересчёта на виду ошибку в размер фасовки замечаешь уже в закупке.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 15:46:55 +03:00

196 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
from decimal import Decimal
import pytest
from app import crypto, journal
from app.models import SCHEMA_VERSION, Document
from app.storage import Vault, content_hash, migrate
def test_create_and_reopen(tmp_path):
path = tmp_path / "vault.fmdb"
vault = Vault.create(path, "pw")
journal.create_product(vault, "Булка с маком", cost_price=20, retail_price=35)
vault.save()
reopened = Vault.open(path, "pw")
assert [p.name for p in reopened.doc.products] == ["Булка с маком"]
assert reopened.doc.products[0].cost_price == Decimal("20.00")
def test_open_with_wrong_password(tmp_path):
path = tmp_path / "vault.fmdb"
Vault.create(path, "pw")
with pytest.raises(crypto.WrongPassword):
Vault.open(path, "nope")
def test_create_refuses_to_clobber(tmp_path):
path = tmp_path / "vault.fmdb"
Vault.create(path, "pw")
with pytest.raises(crypto.VaultError):
Vault.create(path, "other")
def test_save_skips_write_when_nothing_changed(tmp_path):
"""Главная защита от мусорных коммитов в git.
Шифртекст каждый раз новый, поэтому решение «писать или нет» принимается
по хешу открытых данных, а не по байтам файла.
"""
path = tmp_path / "vault.fmdb"
vault = Vault.create(path, "pw")
journal.create_product(vault, "Булка", cost_price=10, retail_price=20)
assert vault.save() is True
assert vault.save() is False
assert vault.save() is False
def test_save_detects_real_change(tmp_path):
vault = Vault.create(tmp_path / "vault.fmdb", "pw")
vault.save()
journal.create_product(vault, "Булка", cost_price=10, retail_price=20)
assert vault.is_dirty is True
assert vault.save() is True
assert vault.is_dirty is False
def test_freshly_opened_vault_is_not_dirty(tmp_path):
"""Иначе каждое открытие программы порождало бы коммит на пустом месте."""
path = tmp_path / "vault.fmdb"
vault = Vault.create(path, "pw")
journal.create_product(vault, "Булка", cost_price=10, retail_price=20)
vault.save()
reopened = Vault.open(path, "pw")
assert reopened.is_dirty is False
assert reopened.save() is False
def test_content_hash_ignores_key_order():
assert content_hash({"a": 1, "b": 2}) == content_hash({"b": 2, "a": 1})
def test_change_password(tmp_path):
path = tmp_path / "vault.fmdb"
vault = Vault.create(path, "old")
journal.create_product(vault, "Булка", cost_price=10, retail_price=20)
vault.save()
vault.change_password("new")
with pytest.raises(crypto.WrongPassword):
Vault.open(path, "old")
assert Vault.open(path, "new").doc.products[0].name == "Булка"
def test_atomic_write_leaves_backup(tmp_path):
path = tmp_path / "vault.fmdb"
vault = Vault.create(path, "pw")
journal.create_product(vault, "Булка", cost_price=10, retail_price=20)
vault.save()
assert path.with_suffix(".fmdb.bak").exists()
assert not path.with_suffix(".fmdb.tmp").exists()
def test_decimals_survive_round_trip(tmp_path):
"""Деньги ходят через строки, поэтому копейки не должны разъезжаться."""
path = tmp_path / "vault.fmdb"
vault = Vault.create(path, "pw")
journal.create_product(vault, "Булка", cost_price="33.33", retail_price="66.67")
vault.save()
product = Vault.open(path, "pw").doc.products[0]
assert product.cost_price == Decimal("33.33")
assert product.retail_price == Decimal("66.67")
def test_export_plain_json(tmp_path):
vault = Vault.create(tmp_path / "vault.fmdb", "pw")
journal.create_product(vault, "Булка с маком", cost_price=20, retail_price=35)
out = vault.export_plain_json(tmp_path / "export.json")
data = json.loads(out.read_text(encoding="utf-8"))
assert data["products"][0]["name"] == "Булка с маком"
def test_migration_adds_journal():
raw = migrate({"schema_version": 0, "products": []})
assert raw["schema_version"] == SCHEMA_VERSION
assert raw["journal"] == []
def test_migration_adds_packaging_without_changing_behaviour():
"""Старая база должна открыться так, будто всё записано в базовых единицах."""
raw = migrate(
{
"schema_version": 1,
"products": [{"id": "p_1", "name": "Печенье", "unit": "шт", "cost_price": "20"}],
"batches": [
{
"id": "b_1", "number": 1, "date": "2026-08-01", "due_date": "2026-08-15",
"lines": [{"product_id": "p_1", "qty": "10", "unit_cost": "20"}],
}
],
"sales": [
{
"id": "s_1", "date": "2026-08-02", "kind": "retail",
"lines": [{"product_id": "p_1", "qty": "3", "unit_price": "30"}],
}
],
}
)
assert raw["schema_version"] == SCHEMA_VERSION
assert raw["products"][0]["packs"] == []
assert raw["batches"][0]["lines"][0] == {
"product_id": "p_1", "qty": "10", "unit_cost": "20", "uom": "", "uom_size": "1"
}
doc = Document.from_dict(raw)
assert doc.batches[0].lines[0].base_qty == Decimal("10.000")
assert doc.batches[0].lines[0].base_unit_cost == Decimal("20.00")
assert doc.sales[0].lines[0].base_unit_price == Decimal("30.00")
def test_migration_keeps_derived_pack_cost():
"""У фасовки появилась своя закупочная цена — старые цифры не должны поехать.
Ноль означает «своей цены нет, считай от базовой», то есть ровно то, как
работала программа до появления поля.
"""
raw = migrate(
{
"schema_version": 4,
"products": [
{
"id": "p_1", "name": "Печенье", "unit": "шт",
"cost_price": "20", "retail_price": "30",
"packs": [{"name": "пачка", "size": "10", "retail_price": "200"}],
}
],
}
)
assert raw["schema_version"] == SCHEMA_VERSION
assert raw["products"][0]["packs"][0]["cost_price"] == "0"
product = Document.from_dict(raw).products[0]
assert product.cost_for("пачка") == Decimal("200.00") # 20 × 10, как раньше
assert product.retail_for("пачка") == Decimal("200.00")
def test_future_schema_refuses_to_open():
with pytest.raises(crypto.UnsupportedFormat, match="новой версией"):
migrate({"schema_version": 999})
def test_corrupted_json_inside_valid_encryption(tmp_path):
"""GCM подтвердил целостность, но внутри не JSON — сообщаем внятно."""
path = tmp_path / "vault.fmdb"
path.write_bytes(crypto.encrypt(b"not json at all", "pw"))
with pytest.raises(crypto.VaultError, match="повреждено"):
Vault.open(path, "pw")