test_spoolman_cost_preserved_2591.py 4.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. """A Spoolman-priced cost survives the two recalculations (#2591).
  2. ``spoolman_tracking`` prices an archive from the linked spools at completion.
  3. Both cost recalculations rebuild a print's cost from ``SpoolUsageHistory``, and
  4. Spoolman mode never writes rows there -- the built-in usage tracker is handed
  5. ``spoolman_owns_usage`` at print start. Their catalogue-or-default fallback
  6. would therefore overwrite the Spoolman figure with a default-rate one on the
  7. next rescan or bulk recalculate, silently undoing the fix, and the per-slot
  8. spool resolution it came from is transient and cannot be rebuilt from the
  9. archive row.
  10. """
  11. import pytest
  12. from httpx import AsyncClient
  13. from backend.app.models.settings import Settings
  14. @pytest.fixture
  15. async def spoolman_mode(db_session):
  16. """Spoolman owns pricing; the built-in catalogue is empty, as the reporter's was."""
  17. db_session.add(Settings(key="spoolman_enabled", value="true"))
  18. db_session.add(Settings(key="default_filament_cost", value="25"))
  19. await db_session.commit()
  20. yield
  21. await db_session.rollback()
  22. class TestRecalculateCostsPreservesSpoolmanPricing:
  23. @pytest.mark.asyncio
  24. @pytest.mark.integration
  25. async def test_bulk_recalculate_keeps_the_spoolman_figure(
  26. self, async_client: AsyncClient, spoolman_mode, archive_factory, printer_factory, db_session
  27. ):
  28. """100 g at the 25/kg default would be 2.50. The archive says 4.00
  29. because the linked spool cost 40.00 a kilo, and a recalculate with no
  30. usage history to read must not drag it back down."""
  31. printer = await printer_factory()
  32. archive = await archive_factory(printer.id, print_name="SpoolmanPriced", status="completed", cost=4.0)
  33. archive.filament_used_grams = 100.0
  34. archive.filament_type = "PLA"
  35. await db_session.commit()
  36. response = await async_client.post("/api/v1/archives/recalculate-costs")
  37. assert response.status_code == 200
  38. assert response.json()["preserved"] >= 1
  39. after = await async_client.get(f"/api/v1/archives/{archive.id}")
  40. assert after.status_code == 200
  41. assert after.json()["cost"] == 4.0
  42. @pytest.mark.asyncio
  43. @pytest.mark.integration
  44. async def test_an_archive_with_no_cost_is_still_priced(
  45. self, async_client: AsyncClient, spoolman_mode, archive_factory, printer_factory, db_session
  46. ):
  47. """The guard preserves a figure; it does not stop one being produced.
  48. An archive that never got a cost still falls to the default rate."""
  49. printer = await printer_factory()
  50. archive = await archive_factory(printer.id, print_name="NeverPriced", status="completed", cost=None)
  51. archive.filament_used_grams = 100.0
  52. archive.filament_type = "PLA"
  53. await db_session.commit()
  54. response = await async_client.post("/api/v1/archives/recalculate-costs")
  55. assert response.status_code == 200
  56. after = await async_client.get(f"/api/v1/archives/{archive.id}")
  57. assert after.json()["cost"] == 2.5
  58. class TestRecalculateCostsWithoutSpoolman:
  59. @pytest.mark.asyncio
  60. @pytest.mark.integration
  61. async def test_internal_mode_recalculates_as_before(
  62. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  63. ):
  64. """With Spoolman off, the catalogue/default path is the only source of
  65. truth and must still overwrite a stale cost."""
  66. db_session.add(Settings(key="default_filament_cost", value="25"))
  67. await db_session.commit()
  68. printer = await printer_factory()
  69. archive = await archive_factory(printer.id, print_name="InternalMode", status="completed", cost=999.0)
  70. archive.filament_used_grams = 100.0
  71. archive.filament_type = "PLA"
  72. await db_session.commit()
  73. response = await async_client.post("/api/v1/archives/recalculate-costs")
  74. assert response.status_code == 200
  75. after = await async_client.get(f"/api/v1/archives/{archive.id}")
  76. assert after.json()["cost"] == 2.5
  77. await db_session.rollback()