Skip to content

4. Deleting a test device

See docs/DEVICE-DELETE-DESIGN.md (Device-Order-Service) for the delete-vs-release decision. Short form: POST /devices/{id}/release (WLPC-25) frees the EUI for re-import but keeps the row forever — sufficient for "re-register this hardware", not sufficient for "this row should stop existing." DELETE /devices/{id} (WLPC-151) is the real delete, and it is dev-only by construction:

curl -s -X DELETE "$DOS/devices/$DEVICE_ID" -H "$AUTH"
  • Refuses (403 HARD_DELETE_DISABLED) unless DOS_ALLOW_HARD_DELETE=true and DOS_ENVIRONMENT is anything other than production — both, independently; the production values file sets neither.
  • Gated on device:import (Org-Admin), same bar as release.
  • BillableEvent rows are untouched — structurally, not by convention: billable_events.device_id has no foreign key to devices.id, so billing history survives the delete with every field intact.
  • Every call is audited, allowed or refused.

Verified live against pallax-dev (real device, real Postgres, via the actual API — not a unit test):

GET  /devices/<id>                        -> 200 (exists)
SELECT count(*) FROM billable_events WHERE device_id = '<id>';   -> 2
DELETE /devices/<id>                       -> 204
GET  /devices/<id>                         -> 404
SELECT count(*) FROM billable_events WHERE device_id = '<id>';   -> 2   (identical)
SELECT count(*) FROM devices        WHERE id = '<id>';           -> 0   (gone)

Guard-off proof (this pod is dev-configured with the flag on, so the refusal was proven separately, through the same code path, against a real Postgres — Device-Order-Service/tests/test_device_delete.py):

flag unset (default)                       -> 403 HARD_DELETE_DISABLED
flag on, DOS_ENVIRONMENT=production         -> 403 HARD_DELETE_DISABLED  (either alone is not enough)

What to do when it fails

403 in a real production deployment is correct, not a bug — there is deliberately no production override path. A 404 on a device you can see in the UI means it belongs to another organization or was already released/deleted; _load's tenant scope answers "not found" rather than "forbidden" either way (see Device-Order-Service/src/deviceorder/api/devices.py).