updated commodities

This commit is contained in:
2026-11-12 13:16:57 -07:00
parent c8ac8e63ee
commit 4fbad900a2
4 changed files with 1561 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
"""Public listing of inventory items owners marked as `available`.
Read-only and unauthenticated, like the catalog. Notes are never included: the
listing only reveals the owner's username, quantity and catalog details.
"""
import math
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
from sqlalchemy.orm import Session as DbSession
from app.db import get_db
from app.inventory import SORT_KEYS, enrich_entry
from app.models import InventoryEntry, User
from app.schemas import AvailablePage
router = APIRouter(prefix="/api/v1/available", tags=["available"])
MAX_PAGE_SIZE = 100
@router.get("", response_model=AvailablePage)
def list_available(
category: str | None = None,
search: str | None = None,
sort: str = Query(default="updated", pattern="^(name|category|quantity|updated)$"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=25, ge=1),
db: DbSession = Depends(get_db),
):
page_size = min(page_size, MAX_PAGE_SIZE)
rows = db.execute(
select(InventoryEntry, User.username)
.join(User, User.id == InventoryEntry.user_id)
.where(InventoryEntry.visibility == "available")
).all()
items = []
for entry, username in rows:
enriched = enrich_entry(entry)
if not enriched["available"]:
continue
enriched["username"] = username
items.append(enriched)
if category is not None and category.strip():
wanted = category.strip()
items = [i for i in items if i["category"] == wanted]
if search is not None and search.strip():
needle = search.strip().lower()
items = [
i
for i in items
if needle in i["name"].lower()
or needle in i["class_name"].lower()
or needle in i["username"].lower()
]
items.sort(key=SORT_KEYS[sort])
total = len(items)
total_pages = max(1, math.ceil(total / page_size))
start = (page - 1) * page_size
return {
"total": total,
"page": page,
"page_size": page_size,
"total_pages": total_pages,
"items": items[start : start + page_size],
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
[
{
"className": "Carryable_TBO_FL_1SCU_Commodity_ProcessedGoods_ConstructionMaterials",
"name": "Construction Materials",
"type": "Cargo",
"subType": "Cargo",
"reference": "5ee15a9b-4ce5-445b-a02d-415c63f43764"
},
{
"className": "Carryable_TBO_FL_1SCU_Commodity_Metal_Iron",
"name": "Iron",
"type": "Cargo",
"subType": "Cargo",
"reference": "332096b9-1cbf-436d-b8b0-5ed422c23da9"
},
{
"className": "Carryable_TBO_FL_1SCU_Commodity_Metal_Ore_Iron",
"name": "Iron (Ore)",
"type": "Cargo",
"subType": "Cargo",
"reference": "271effd5-3aa8-4538-b9a0-b0f33dc86d68"
},
{
"className": "Carryable_TBO_FL_1SCU_Commodity_Metal_RMC",
"name": "Recycled Material Composite",
"type": "Cargo",
"subType": "Cargo",
"reference": "2e50dde5-30d4-4faa-af8f-8e5aa8004b96"
}
]

View File

@@ -0,0 +1,130 @@
"""Public available-items listing: open access, owner attribution, no notes."""
from fastapi.testclient import TestClient
from app.api import app
PASSWORD = "correct horse battery staple"
CARROCK = "ANVL_Carrack"
RIFLE = "BEHR_Rifle_Ballistic_01"
IRON_ORE = "Carryable_TBO_FL_1SCU_Commodity_Metal_Ore_Iron"
def authed_client(username: str) -> TestClient:
client = TestClient(app)
resp = client.post(
"/api/v1/auth/register", json={"username": username, "password": PASSWORD}
)
assert resp.status_code == 201, resp.text
return client
def add(client: TestClient, class_name: str, quantity: int = 1, notes: str = ""):
resp = client.post(
"/api/v1/me/inventory",
json={"class_name": class_name, "quantity": quantity, "notes": notes},
)
assert resp.status_code in (200, 201), resp.text
return resp
def offer(client: TestClient, class_name: str, quantity: int = 1):
add(client, class_name, quantity)
resp = client.patch(
f"/api/v1/me/inventory/{class_name}", json={"visibility": "available"}
)
assert resp.status_code == 200, resp.text
def public_items(**params):
return TestClient(app).get("/api/v1/available", params=params).json()
def test_listing_is_public_and_empty_by_default():
body = public_items()
assert body["total"] == 0
assert body["items"] == []
assert body["page"] == 1
def test_private_items_are_not_listed_until_marked_available():
alice = authed_client("alice")
add(alice, CARROCK)
assert public_items()["total"] == 0
alice.patch(f"/api/v1/me/inventory/{CARROCK}", json={"visibility": "available"})
body = public_items()
assert body["total"] == 1
assert body["items"][0]["class_name"] == CARROCK
alice.patch(f"/api/v1/me/inventory/{CARROCK}", json={"visibility": "private"})
assert public_items()["total"] == 0
def test_listing_shows_owner_and_catalog_details_but_not_notes():
alice = authed_client("alice")
offer(alice, CARROCK, quantity=3)
alice.patch(f"/api/v1/me/inventory/{CARROCK}", json={"notes": "hidden stash"})
item = public_items()["items"][0]
assert item["username"] == "alice"
assert item["name"] == "Anvil Carrack"
assert item["category"] == "ships"
assert item["manufacturer"]["code"] == "ANVL"
assert item["quantity"] == 3
assert "notes" not in item
assert "hidden stash" not in str(public_items())
def test_each_owner_appears_as_its_own_offer():
offer(authed_client("alice"), CARROCK, quantity=1)
offer(authed_client("bob"), CARROCK, quantity=2)
items = public_items(sort="quantity")["items"]
assert [(i["username"], i["quantity"], i["class_name"]) for i in items] == [
("bob", 2, CARROCK),
("alice", 1, CARROCK),
]
def test_removing_an_offer_removes_it_from_the_listing():
alice = authed_client("alice")
offer(alice, CARROCK)
assert public_items()["total"] == 1
assert alice.delete(f"/api/v1/me/inventory/{CARROCK}").status_code == 204
assert public_items()["total"] == 0
assert public_items()["items"] == []
def test_listing_filters_by_category_and_searches_owner_or_item():
offer(authed_client("alice"), CARROCK)
offer(authed_client("bob"), RIFLE)
assert [i["class_name"] for i in public_items(category="ships")["items"]] == [CARROCK]
assert [i["username"] for i in public_items(search="bob")["items"]] == ["bob"]
assert [i["class_name"] for i in public_items(search="carrack")["items"]] == [CARROCK]
def test_listing_paginates_and_rejects_unknown_sort():
for quantity, class_name in ((1, CARROCK), (2, RIFLE)):
offer(authed_client(f"pilot{quantity}"), class_name, quantity=quantity)
body = public_items(page=2, page_size=1)
assert body["total"] == 2
assert body["total_pages"] == 2
assert len(body["items"]) == 1
assert public_items(page_size=500)["page_size"] == 100
assert TestClient(app).get("/api/v1/available?sort=bogus").status_code == 422
def test_commodity_offers_are_listed_with_owner():
alice = authed_client("alice")
offer(alice, IRON_ORE, quantity=12)
body = public_items()
assert body["total"] == 1
item = body["items"][0]
assert item["name"] == "Iron (Ore)"
assert item["category"] == "commodities"
assert item["quantity"] == 12
assert item["username"] == "alice"