Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions pay-slip-app/backend/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,8 @@ paths:

delete:
tags: [PaySlips]
summary: Delete metadata [admin only]
summary: Delete a pay slip [admin only]
description: Deletes the pay slip metadata from the database and the actual file from Firebase Storage.
parameters:
- name: id
in: path
Expand All @@ -358,6 +359,10 @@ paths:
type: string
responses:
"204":
description: Deleted
description: Deleted successfully
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
19 changes: 19 additions & 0 deletions pay-slip-app/backend/internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package database

import (
"context"
"database/sql"
"fmt"
"log"
Expand Down Expand Up @@ -103,6 +104,24 @@ func (db *Database) QueryRow(query string, args ...any) *sql.Row {
return db.Conn.QueryRow(query, args...)
}

func (db *Database) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
db.mu.Lock()
defer db.mu.Unlock()
return db.Conn.ExecContext(ctx, query, args...)
}

func (db *Database) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
db.mu.Lock()
defer db.mu.Unlock()
return db.Conn.QueryContext(ctx, query, args...)
}

func (db *Database) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
db.mu.Lock()
defer db.mu.Unlock()
return db.Conn.QueryRowContext(ctx, query, args...)
}

func (db *Database) Ping() error {
db.mu.Lock()
defer db.mu.Unlock()
Expand Down
24 changes: 21 additions & 3 deletions pay-slip-app/backend/internal/handlers/payslip_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
"pay-slip-app/internal/constants"
"pay-slip-app/internal/models"
"strconv"
"strings"
"time"

"cloud.google.com/go/storage"
)

// ── PaySlip handlers ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -230,13 +233,28 @@ func (h *Handler) DeletePaySlip(w http.ResponseWriter, r *http.Request) {
}

id := r.PathValue("id")
if _, err := h.PaySlipService.GetPaySlipByID(id); err != nil {
ps, err := h.PaySlipService.GetPaySlipByID(id)
if err != nil {
http.Error(w, "Pay slip not found", http.StatusNotFound)
return
}

if err := h.PaySlipService.DeletePaySlip(id); err != nil {
http.Error(w, "Failed to delete pay slip", http.StatusInternalServerError)
// 1. Delete from Storage first
ctx := r.Context()
if err := h.Storage.DeleteFile(ctx, ps.FilePath); err != nil {
// If the object does not exist, we can proceed to delete the DB record.
// For any other error, we abort to prevent an orphaned DB record.
if err != storage.ErrObjectNotExist {
log.Printf("Failed to delete file from storage for path %q: %v", ps.FilePath, err)
http.Error(w, "Failed to delete file from storage", http.StatusInternalServerError)
return
}
log.Printf("File %q not found in storage, proceeding with DB deletion.", ps.FilePath)
}

// 2. Delete from DB
if err := h.PaySlipService.DeletePaySlip(ctx, id); err != nil {
http.Error(w, "Failed to delete pay slip record", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
Expand Down
5 changes: 3 additions & 2 deletions pay-slip-app/backend/internal/services/payslip_service.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package services

import (
"context"
"database/sql"
"pay-slip-app/internal/database"
"pay-slip-app/internal/models"
Expand Down Expand Up @@ -40,8 +41,8 @@ func (s *PaySlipService) UpdatePaySlipFile(id, filePath, uploadedBy string) erro
return err
}

func (s *PaySlipService) DeletePaySlip(id string) error {
_, err := s.db.Exec("DELETE FROM pay_slips WHERE id = ?", id)
func (s *PaySlipService) DeletePaySlip(ctx context.Context, id string) error {
_, err := s.db.ExecContext(ctx, "DELETE FROM pay_slips WHERE id = ?", id)
return err
}

Expand Down
5 changes: 5 additions & 0 deletions pay-slip-app/backend/internal/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ func (s *FirebaseStorage) GetSignedURL(objectPath string) (string, error) {
return url, nil
}

// DeleteFile deletes an object from Firebase Storage.
func (s *FirebaseStorage) DeleteFile(ctx context.Context, objectPath string) error {
return s.client.Bucket(s.bucket).Object(objectPath).Delete(ctx)
}

// FirebaseStorage wraps a GCS client scoped to a single bucket.
type FirebaseStorage struct {
client *storage.Client
Expand Down