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:
- Refuses (403
HARD_DELETE_DISABLED) unlessDOS_ALLOW_HARD_DELETE=trueandDOS_ENVIRONMENTis anything other thanproduction— both, independently; the production values file sets neither. - Gated on
device:import(Org-Admin), same bar as release. BillableEventrows are untouched — structurally, not by convention:billable_events.device_idhas no foreign key todevices.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).