printedparts stage 5: the ledger - restock/adjust with badge attribution
Badge resolver copied from the USB contract (SSO digits, 0<digits>BZ PayNo wrap) with names from the employees directory and the unknown-badge policy setting; deliberately copied rather than cross-imported so the contract test stays green. Restock and adjust write the ledger row and move the cached quantity in one commit - the single-commit invariant every write path must use. Adjust requires a reason and refuses to drive stock below zero. Detail page gains Restock/Adjust modals. Seven tests cover minting, the cache==ledger invariant, badge shapes, policy toggle, and auth.
This commit is contained in:
121
tests/test_plugins/test_printedparts_ledger.py
Normal file
121
tests/test_plugins/test_printedparts_ledger.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Printedparts ledger + badge tests.
|
||||
|
||||
The invariants that make the plugin trustworthy: itemcode minting, the
|
||||
single-commit cache==ledger rule, the below-zero guard, quantity edits
|
||||
forced through the ledger, and the badge contract (SSO digits, PayNo
|
||||
wrap, unknown-badge policy).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from shopdb.api import db
|
||||
from shopdb.core.models import Setting
|
||||
from plugins.printedparts.models import PrintedItem, PrintedItemTransaction
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def item(app, db):
|
||||
with app.app_context():
|
||||
row = PrintedItem(itemcode='3DP-9001', itemname='Test clip',
|
||||
quantityonhand=0, lowstockthreshold=5)
|
||||
db.session.add(row)
|
||||
db.session.commit()
|
||||
yield row.printeditemid
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def directory_employee(app):
|
||||
with app.app_context():
|
||||
from plugins.employees.models import DirectoryEmployee
|
||||
if not db.session.get(DirectoryEmployee, 502000001):
|
||||
db.session.add(DirectoryEmployee(
|
||||
sso=502000001, firstname='Pat', lastname='Printer'))
|
||||
db.session.commit()
|
||||
return '502000001'
|
||||
|
||||
|
||||
def test_create_mints_itemcode(client, auth_headers):
|
||||
response = client.post('/api/printedparts/items', json={'itemname': 'Bracket'},
|
||||
headers=auth_headers)
|
||||
assert response.status_code == 201
|
||||
data = response.get_json()['data']
|
||||
assert data['itemcode'] == f"3DP-{data['printeditemid']:04d}"
|
||||
assert data['quantityonhand'] == 0
|
||||
|
||||
|
||||
def test_update_refuses_quantity(client, auth_headers, item):
|
||||
response = client.put(f'/api/printedparts/items/{item}',
|
||||
json={'quantityonhand': 50}, headers=auth_headers)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_restock_writes_ledger_and_cache(client, auth_headers, app, item,
|
||||
directory_employee):
|
||||
response = client.post(f'/api/printedparts/items/{item}/restock',
|
||||
json={'quantity': 10, 'badge': directory_employee},
|
||||
headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.get_json()['data']['quantityonhand'] == 10
|
||||
with app.app_context():
|
||||
rows = PrintedItemTransaction.query.filter_by(printeditemid=item).all()
|
||||
assert len(rows) == 1
|
||||
assert rows[0].transactiontype == 'restock'
|
||||
assert rows[0].quantitychange == 10
|
||||
assert rows[0].employeename == 'Pat Printer'
|
||||
cached = db.session.get(PrintedItem, item).quantityonhand
|
||||
assert cached == sum(r.quantitychange for r in rows)
|
||||
|
||||
|
||||
def test_payno_badge_shape_resolves(client, auth_headers, item,
|
||||
directory_employee):
|
||||
response = client.post(f'/api/printedparts/items/{item}/restock',
|
||||
json={'quantity': 1,
|
||||
'badge': f'0{directory_employee}BZ'},
|
||||
headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_adjust_requires_reason_and_floors_at_zero(client, auth_headers, item,
|
||||
directory_employee):
|
||||
no_reason = client.post(f'/api/printedparts/items/{item}/adjust',
|
||||
json={'quantitychange': -1,
|
||||
'badge': directory_employee},
|
||||
headers=auth_headers)
|
||||
assert no_reason.status_code == 400
|
||||
|
||||
below_zero = client.post(f'/api/printedparts/items/{item}/adjust',
|
||||
json={'quantitychange': -1, 'reason': 'test',
|
||||
'badge': directory_employee},
|
||||
headers=auth_headers)
|
||||
assert below_zero.status_code == 400
|
||||
|
||||
|
||||
def test_unknown_badge_denied_then_allowed_by_policy(client, auth_headers, app,
|
||||
item):
|
||||
denied = client.post(f'/api/printedparts/items/{item}/restock',
|
||||
json={'quantity': 1, 'badge': '999999999'},
|
||||
headers=auth_headers)
|
||||
assert denied.status_code == 422
|
||||
|
||||
with app.app_context():
|
||||
Setting.set('printedparts_unknown_badge', 'allow',
|
||||
valuetype='string', category='printedparts')
|
||||
db.session.commit()
|
||||
try:
|
||||
allowed = client.post(f'/api/printedparts/items/{item}/restock',
|
||||
json={'quantity': 1, 'badge': '999999999'},
|
||||
headers=auth_headers)
|
||||
assert allowed.status_code == 200
|
||||
assert allowed.get_json()['data']['quantityonhand'] == 1
|
||||
finally:
|
||||
with app.app_context():
|
||||
Setting.set('printedparts_unknown_badge', 'deny',
|
||||
valuetype='string', category='printedparts')
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def test_anonymous_cannot_mutate(client, item):
|
||||
assert client.post('/api/printedparts/items',
|
||||
json={'itemname': 'X'}).status_code == 401
|
||||
assert client.post(f'/api/printedparts/items/{item}/restock',
|
||||
json={'quantity': 1, 'badge': '1'}).status_code == 401
|
||||
Reference in New Issue
Block a user