Skip to content

feat: add delete a facility functionality per ticket 683 - #1199

Merged
carddev81 merged 2 commits into
mainfrom
carddev81/ticket_id683_deletefacility
Aug 20, 2026
Merged

feat: add delete a facility functionality per ticket 683#1199
carddev81 merged 2 commits into
mainfrom
carddev81/ticket_id683_deletefacility

Conversation

@carddev81

Copy link
Copy Markdown
Contributor

Pre-Submission PR Checklist

  • No debug/console/fmt.Println statements
  • Unnecessary development comments removed
  • All acceptance criteria verified
  • Functions according to ticket specifications
  • Tested manually where applicable
  • Branch rebased with latest main
  • No business logic exists within the database layer

Description of the change

Added the functionality for being able to delete a facility. If the facility has any associations the facility will not be allowed to be deleted.

Screenshot(s)

image

@carddev81
carddev81 requested a review from a team as a code owner July 27, 2026 01:42
@carddev81
carddev81 requested review from corypride and removed request for a team July 27, 2026 01:42
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fbe91ba8-7bfc-4ad8-8348-5631a92162e3

📥 Commits

Reviewing files that changed from the base of the PR and between 0608f67 and 1405f65.

📒 Files selected for processing (1)
  • backend/src/handlers/facilities_handler.go
💤 Files with no reviewable changes (1)
  • backend/src/handlers/facilities_handler.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added facility deletion controls with confirmation dialogs, progress states, success/error notifications, and explanatory disabled states.
    • Added deletion eligibility indicators to facility listings.
    • Added a facility deletion check endpoint that reports blocking records.
  • Bug Fixes

    • Prevented deletion when associated records would be affected, with clear conflict responses.
    • Updated permissions so department administrators can delete facilities while facility administrators cannot.
  • Tests

    • Added coverage for successful deletion, blocking associations, permissions, eligibility indicators, and soft deletion behavior.

Walkthrough

Facility deletion now checks related records, exposes deletion eligibility, enforces department-admin authorization, returns blocker details, and adds frontend confirmation and conflict handling. Integration tests cover deletion outcomes, associations, roles, and listing state.

Changes

Facility deletion guards

Layer / File(s) Summary
Blocking data and eligibility
backend/src/models/delete_guards.go, backend/src/models/facilities.go, backend/src/database/delete_guards.go, backend/src/database/facilities.go, frontend/src/types/facility.ts
The backend counts deletion-blocking records and computes can_delete. The frontend types expose the same eligibility field.
Authorization and delete API
backend/src/handlers/facilities_handler.go, backend/src/handlers/server.go
The API adds the delete-check endpoint, moves deletion to department-admin routing, rechecks blockers before deletion, and returns 409 Conflict with blocker counts.
Facility management deletion UI
frontend/src/pages/admin/FacilityManagement.tsx
The page disables blocked delete actions, shows the blocker message, confirms deletion, handles API results, and refreshes the facility list.
Deletion guard integration coverage
backend/tests/integration/facility_delete_guard_test.go
Integration tests cover successful deletion, blocking users, rooms, and classes, ignored login activity, listing eligibility, role authorization, and preservation of blocked facilities.

Merge Risk: ⚪ Minimal · up to 1405f

The change adds facility deletion while preventing deletion when associations exist; no actionable merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: adding facility deletion functionality for the referenced ticket.
Description check ✅ Passed The description directly explains facility deletion and the rule that facilities with associations cannot be deleted.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/database/facilities.go`:
- Around line 30-43: The can_delete calculation in the facilities query adds
seven correlated EXISTS checks that execute for every row produced by the
existing joins. Restructure the query around the can_delete logic, such as
evaluating these checks after facility-level aggregation or through a
facility-keyed precomputed relation, so each facility’s checks run once while
preserving the existing deletion criteria and aggregate counts.

In `@backend/src/handlers/server.go`:
- Around line 609-620: Replace the near-duplicate
writeFacilityDeleteConflictResponse and writeDeleteConflictResponse
implementations with one generic conflict-response writer parameterized by the
blocker payload type. Reuse models.Resource[T] while preserving the existing
JSON headers, 409 status, message/data fields, and error wrapping behavior;
update all callers to use the generic helper.

In `@backend/tests/integration/facility_delete_guard_test.go`:
- Around line 71-75: Update the facility deletion assertions in the scoped count
checks and the corresponding block around the second count to use Unscoped(),
then verify the facility record remains present and has a populated deleted_at
value. Preserve the existing facility ID filtering and ensure the assertions
distinguish soft deletion from hard deletion.
- Around line 190-195: Update the response assertions near the byID map
construction to explicitly verify that both empty.ID and withResident.ID are
present in the map before checking CanDelete. Capture each lookup’s presence
result and assert it, then retain the existing eligibility assertions.
- Around line 153-157: Update the login-activity fixture creation in the
facility deletion guard test to assert that suite.env.DB.Create returns no
error. Ensure the test fails immediately when the LoginActivity insert cannot be
created, while preserving the existing fixture values and subsequent
non-blocking deletion assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b3c66253-8ed0-451f-86a6-264013bc699e

📥 Commits

Reviewing files that changed from the base of the PR and between 12b35ac and 0608f67.

📒 Files selected for processing (10)
  • backend/src/database/delete_guards.go
  • backend/src/database/facilities.go
  • backend/src/handlers/auth.go
  • backend/src/handlers/facilities_handler.go
  • backend/src/handlers/server.go
  • backend/src/models/delete_guards.go
  • backend/src/models/facilities.go
  • backend/tests/integration/facility_delete_guard_test.go
  • frontend/src/pages/admin/FacilityManagement.tsx
  • frontend/src/types/facility.ts

Comment on lines 30 to +43
const query = `SELECT
f.id, f.name, f.timezone, f.created_at, f.updated_at,
COUNT(DISTINCT CASE WHEN p.is_active = true AND p.archived_at IS NULL AND p.deleted_at IS NULL THEN p.id END) AS active_programs,
COUNT(DISTINCT CASE WHEN pc.status IN ('Active') AND pc.archived_at IS NULL AND pc.deleted_at IS NULL THEN pc.id END) AS active_classes,
COUNT(DISTINCT CASE WHEN u.role = 'student' AND u.deleted_at IS NULL AND u.deactivated_at IS NULL THEN u.id END) AS total_residents
COUNT(DISTINCT CASE WHEN u.role = 'student' AND u.deleted_at IS NULL AND u.deactivated_at IS NULL THEN u.id END) AS total_residents,
NOT (
EXISTS(SELECT 1 FROM users WHERE facility_id = f.id AND deleted_at IS NULL)
OR EXISTS(SELECT 1 FROM program_classes WHERE facility_id = f.id AND deleted_at IS NULL)
OR EXISTS(SELECT 1 FROM facilities_programs WHERE facility_id = f.id AND deleted_at IS NULL)
OR EXISTS(SELECT 1 FROM rooms WHERE facility_id = f.id AND deleted_at IS NULL)
OR EXISTS(SELECT 1 FROM open_content_activities WHERE facility_id = f.id)
OR EXISTS(SELECT 1 FROM open_content_favorites WHERE facility_id = f.id)
OR EXISTS(SELECT 1 FROM user_account_history WHERE facility_id = f.id)
) AS can_delete

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial

Correlated EXISTS subqueries stack on top of existing join multiplication.

The pre-existing LEFT JOINs to facilities_programs/programs/program_classes/users already multiply rows before GROUP BY; the 7 new EXISTS(...) subqueries are now evaluated per multiplied row rather than once per facility. Fine at current scale (a handful of facilities), but worth keeping in mind if facility/user counts grow substantially.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/database/facilities.go` around lines 30 - 43, The can_delete
calculation in the facilities query adds seven correlated EXISTS checks that
execute for every row produced by the existing joins. Restructure the query
around the can_delete logic, such as evaluating these checks after
facility-level aggregation or through a facility-keyed precomputed relation, so
each facility’s checks run once while preserving the existing deletion criteria
and aggregate counts.

Comment on lines +609 to +620
func writeFacilityDeleteConflictResponse(w http.ResponseWriter, message string, blockers models.FacilityBlockingChildren) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusConflict)
resp := models.Resource[models.FacilityBlockingChildren]{
Message: message,
Data: blockers,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
return newResponseServiceError(err)
}
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Near-duplicate of writeDeleteConflictResponse; consider making it generic.

This function is identical to the existing writeDeleteConflictResponse except for the payload type. Since models.Resource[T] is already generic, a single generic writer would remove the duplication.

♻️ Suggested refactor
-func writeDeleteConflictResponse(w http.ResponseWriter, message string, blockers models.DeleteBlockingChildren) error {
-	w.Header().Set("Content-Type", "application/json")
-	w.WriteHeader(http.StatusConflict)
-	resp := models.Resource[models.DeleteBlockingChildren]{
-		Message: message,
-		Data:    blockers,
-	}
-	if err := json.NewEncoder(w).Encode(resp); err != nil {
-		return newResponseServiceError(err)
-	}
-	return nil
-}
-
-func writeFacilityDeleteConflictResponse(w http.ResponseWriter, message string, blockers models.FacilityBlockingChildren) error {
-	w.Header().Set("Content-Type", "application/json")
-	w.WriteHeader(http.StatusConflict)
-	resp := models.Resource[models.FacilityBlockingChildren]{
-		Message: message,
-		Data:    blockers,
-	}
-	if err := json.NewEncoder(w).Encode(resp); err != nil {
-		return newResponseServiceError(err)
-	}
-	return nil
-}
+func writeDeleteConflictResponse[T any](w http.ResponseWriter, message string, blockers T) error {
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusConflict)
+	resp := models.Resource[T]{
+		Message: message,
+		Data:    blockers,
+	}
+	if err := json.NewEncoder(w).Encode(resp); err != nil {
+		return newResponseServiceError(err)
+	}
+	return nil
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/handlers/server.go` around lines 609 - 620, Replace the
near-duplicate writeFacilityDeleteConflictResponse and
writeDeleteConflictResponse implementations with one generic conflict-response
writer parameterized by the blocker payload type. Reuse models.Resource[T] while
preserving the existing JSON headers, 409 status, message/data fields, and error
wrapping behavior; update all callers to use the generic helper.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be worth, just to stay as DRY as possible, not a deal breaker though.

Comment on lines +71 to +75
var live int64
suite.env.DB.Model(&models.Facility{}).
Where("id = ? AND deleted_at IS NULL", facility.ID).
Count(&live)
suite.Equal(int64(0), live, "facility should be soft-deleted")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the soft-delete contract, not only disappearance.

Both scoped counts also pass after a hard delete. Query Unscoped() and verify the facility still exists with deleted_at populated.

Also applies to: 165-169

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/integration/facility_delete_guard_test.go` around lines 71 -
75, Update the facility deletion assertions in the scoped count checks and the
corresponding block around the second count to use Unscoped(), then verify the
facility record remains present and has a populated deleted_at value. Preserve
the existing facility ID filtering and ensure the assertions distinguish soft
deletion from hard deletion.

Comment on lines +153 to +157
suite.env.DB.Create(&models.LoginActivity{
TimeInterval: time.Now(),
FacilityID: facility.ID,
TotalLogins: 1,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fail when the login-activity fixture cannot be created.

Ignoring Create(...).Error lets this test pass with an empty facility, so it would not prove that login activity is non-blocking. Require a nil error from the insert.

Proposed fix
- suite.env.DB.Create(&models.LoginActivity{
+ suite.Require().NoError(suite.env.DB.Create(&models.LoginActivity{
    TimeInterval: time.Now(),
    FacilityID:   facility.ID,
    TotalLogins:  1,
- })
+ }).Error)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
suite.env.DB.Create(&models.LoginActivity{
TimeInterval: time.Now(),
FacilityID: facility.ID,
TotalLogins: 1,
})
suite.Require().NoError(suite.env.DB.Create(&models.LoginActivity{
TimeInterval: time.Now(),
FacilityID: facility.ID,
TotalLogins: 1,
}).Error)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/integration/facility_delete_guard_test.go` around lines 153 -
157, Update the login-activity fixture creation in the facility deletion guard
test to assert that suite.env.DB.Create returns no error. Ensure the test fails
immediately when the LoginActivity insert cannot be created, while preserving
the existing fixture values and subsequent non-blocking deletion assertion.

Comment on lines +190 to +195
byID := map[uint]models.FacilityWithStats{}
for _, f := range resp.GetData() {
byID[f.ID] = f
}
suite.True(byID[empty.ID].CanDelete, "empty facility should be deletable")
suite.False(byID[withResident.ID].CanDelete, "facility with a resident should not be deletable")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that both facilities are present in the response.

A missing withResident entry yields the zero-value struct, whose CanDelete is false; the blocked-facility assertion would pass despite the list omitting it. Require map presence before checking eligibility.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/integration/facility_delete_guard_test.go` around lines 190 -
195, Update the response assertions near the byID map construction to explicitly
verify that both empty.ID and withResident.ID are present in the map before
checking CanDelete. Capture each lookup’s presence result and assert it, then
retain the existing eligibility assertions.

@carddev81
carddev81 requested review from CK-7vn and removed request for corypride August 17, 2026 18:24

@CK-7vn CK-7vn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worked great! Couple trivial cleanup things that you could do, or not do, whatever works for you my friend.

* when it is not, what is blocking the delete.
*/
func (srv *Server) handleGetFacilityDeleteCheck(w http.ResponseWriter, r *http.Request, log sLog) error {
if !userCanManageFacilities(r) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can delete this because this is a newDeptAdminRoute and it attaches that resolver that checks claims for canSwitchFacility. This doesn't break anything, but it is a double check.

*/
func (srv *Server) handleDeleteFacility(w http.ResponseWriter, r *http.Request, log sLog) error {
if !userIsSystemAdmin(r) {
if !userCanManageFacilities(r) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to delete this one as well.

newAdminRoute("GET /api/facilities/{id}", srv.handleShowFacility),
newDeptAdminRoute("POST /api/facilities", srv.handleCreateFacility),
newSystemAdminRoute("DELETE /api/facilities/{id}", srv.handleDeleteFacility),
newDeptAdminRoute("GET /api/facilities/{id}/delete-check", srv.handleGetFacilityDeleteCheck),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we calling/using delete-check anywhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

*/
func (srv *Server) handleDeleteFacility(w http.ResponseWriter, r *http.Request, log sLog) error {
if !userIsSystemAdmin(r) {
if !userCanManageFacilities(r) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to delete this one as well.

@carddev81
carddev81 merged commit 33e716c into main Aug 20, 2026
11 checks passed
@carddev81
carddev81 deleted the carddev81/ticket_id683_deletefacility branch August 20, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants