From 9e063415643ff0e8a871a95c41fd2d8fa5077e85 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Sun, 1 Mar 2026 05:22:56 +0000 Subject: [PATCH 1/3] feat: cumulative balance with carried_over in finance summary --- internal/model/finance.go | 1 + internal/repository/finance.go | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/internal/model/finance.go b/internal/model/finance.go index 4a6210e..3fdfc16 100644 --- a/internal/model/finance.go +++ b/internal/model/finance.go @@ -73,6 +73,7 @@ type UpdateFinanceTransactionRequest struct { } type FinanceSummary struct { + CarriedOver float64 `json:"carried_over"` Balance float64 `json:"balance"` TotalIncome float64 `json:"total_income"` TotalExpense float64 `json:"total_expense"` diff --git a/internal/repository/finance.go b/internal/repository/finance.go index 17bcc7a..ac4a885 100644 --- a/internal/repository/finance.go +++ b/internal/repository/finance.go @@ -237,15 +237,34 @@ func (r *FinanceRepository) DeleteTransaction(id, userID int64) error { func (r *FinanceRepository) GetSummary(userID int64, month, year int) (*model.FinanceSummary, error) { summary := &model.FinanceSummary{} + // First day of selected month and last day of selected month + firstDay := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) + lastDay := firstDay.AddDate(0, 1, -1) + + // Carried over: balance of all transactions BEFORE the selected month + err := r.db.QueryRow(`SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END), 0) + FROM finance_transactions WHERE user_id=$1 AND date < $2`, + userID, firstDay).Scan(&summary.CarriedOver) + if err != nil { + return nil, err + } + // Total income & expense for the month - err := r.db.QueryRow(`SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END),0), + err = r.db.QueryRow(`SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END),0), COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END),0) FROM finance_transactions WHERE user_id=$1 AND EXTRACT(MONTH FROM date)=$2 AND EXTRACT(YEAR FROM date)=$3`, userID, month, year).Scan(&summary.TotalIncome, &summary.TotalExpense) if err != nil { return nil, err } - summary.Balance = summary.TotalIncome - summary.TotalExpense + + // Cumulative balance: all transactions up to end of selected month + err = r.db.QueryRow(`SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END), 0) + FROM finance_transactions WHERE user_id=$1 AND date <= $2`, + userID, lastDay).Scan(&summary.Balance) + if err != nil { + return nil, err + } // By category rows, err := r.db.Query(`SELECT c.id, c.name, c.emoji, t.type, SUM(t.amount) as amount From 901173a337a8fbb4e0d5c96941b249328b0318ca Mon Sep 17 00:00:00 2001 From: Cosmo Date: Sun, 1 Mar 2026 05:31:05 +0000 Subject: [PATCH 2/3] fix: analytics avg_daily_expense uses selected month/year instead of current --- internal/handler/finance.go | 14 ++++++++++++-- internal/repository/finance.go | 14 +++++++------- internal/service/finance.go | 4 ++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/internal/handler/finance.go b/internal/handler/finance.go index 32ca323..d2da18a 100644 --- a/internal/handler/finance.go +++ b/internal/handler/finance.go @@ -253,11 +253,21 @@ func (h *FinanceHandler) Analytics(w http.ResponseWriter, r *http.Request) { if !ok { return } - months, _ := strconv.Atoi(r.URL.Query().Get("months")) + q := r.URL.Query() + months, _ := strconv.Atoi(q.Get("months")) if months <= 0 { months = 6 } - analytics, err := h.svc.GetAnalytics(userID, months) + now := time.Now() + month, _ := strconv.Atoi(q.Get("month")) + year, _ := strconv.Atoi(q.Get("year")) + if month <= 0 { + month = int(now.Month()) + } + if year <= 0 { + year = now.Year() + } + analytics, err := h.svc.GetAnalytics(userID, months, month, year) if err != nil { writeError(w, "failed to get analytics", http.StatusInternalServerError) return diff --git a/internal/repository/finance.go b/internal/repository/finance.go index ac4a885..1bc1fad 100644 --- a/internal/repository/finance.go +++ b/internal/repository/finance.go @@ -317,7 +317,7 @@ func (r *FinanceRepository) GetSummary(userID int64, month, year int) (*model.Fi return summary, nil } -func (r *FinanceRepository) GetAnalytics(userID int64, months int) (*model.FinanceAnalytics, error) { +func (r *FinanceRepository) GetAnalytics(userID int64, months, month, year int) (*model.FinanceAnalytics, error) { analytics := &model.FinanceAnalytics{} // Monthly trend @@ -342,27 +342,27 @@ func (r *FinanceRepository) GetAnalytics(userID int64, months int) (*model.Finan analytics.MonthlyTrend = []model.MonthlyTrend{} } - // Avg daily expense for current month - now := time.Now() + // Avg daily expense for selected month var totalExpense float64 var dayCount int r.db.QueryRow(`SELECT COALESCE(SUM(amount),0), COUNT(DISTINCT date) FROM finance_transactions WHERE user_id=$1 AND type='expense' AND EXTRACT(MONTH FROM date)=$2 AND EXTRACT(YEAR FROM date)=$3`, - userID, int(now.Month()), now.Year()).Scan(&totalExpense, &dayCount) + userID, month, year).Scan(&totalExpense, &dayCount) if dayCount > 0 { analytics.AvgDailyExpense = totalExpense / float64(dayCount) } // Comparison with previous month - prevMonth := now.AddDate(0, -1, 0) + selectedMonth := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) + prevMonthTime := selectedMonth.AddDate(0, -1, 0) var currentMonthExp, prevMonthExp float64 r.db.QueryRow(`SELECT COALESCE(SUM(amount),0) FROM finance_transactions WHERE user_id=$1 AND type='expense' AND EXTRACT(MONTH FROM date)=$2 AND EXTRACT(YEAR FROM date)=$3`, - userID, int(now.Month()), now.Year()).Scan(¤tMonthExp) + userID, month, year).Scan(¤tMonthExp) r.db.QueryRow(`SELECT COALESCE(SUM(amount),0) FROM finance_transactions WHERE user_id=$1 AND type='expense' AND EXTRACT(MONTH FROM date)=$2 AND EXTRACT(YEAR FROM date)=$3`, - userID, int(prevMonth.Month()), prevMonth.Year()).Scan(&prevMonthExp) + userID, int(prevMonthTime.Month()), prevMonthTime.Year()).Scan(&prevMonthExp) analytics.ComparisonPrevMonth = model.Comparison{ Current: currentMonthExp, diff --git a/internal/service/finance.go b/internal/service/finance.go index a046d1f..1bd4fd8 100644 --- a/internal/service/finance.go +++ b/internal/service/finance.go @@ -162,6 +162,6 @@ func (s *FinanceService) GetSummary(userID int64, month, year int) (*model.Finan return s.repo.GetSummary(userID, month, year) } -func (s *FinanceService) GetAnalytics(userID int64, months int) (*model.FinanceAnalytics, error) { - return s.repo.GetAnalytics(userID, months) +func (s *FinanceService) GetAnalytics(userID int64, months, month, year int) (*model.FinanceAnalytics, error) { + return s.repo.GetAnalytics(userID, months, month, year) } From 30a894a78f0ca31f180ff782c240b57b4f0cf073 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Thu, 26 Mar 2026 18:33:43 +0000 Subject: [PATCH 3/3] ci: add lint, coverage check and proper deploy workflow --- .gitea/workflows/ci.yml | 24 +++++++++++++++++------- .gitea/workflows/deploy-prod.yml | 25 ++++++++++++++++--------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 26cf36e..2014b42 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -2,10 +2,12 @@ name: CI on: push: - branches: [dev] + branches-ignore: [main] + pull_request: + branches: [main] jobs: - ci: + lint-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -17,11 +19,19 @@ jobs: - name: Tidy run: go mod tidy - - name: Vet - run: go vet ./... + - name: Lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest - name: Test - run: go test ./... -v + run: go test ./... -coverprofile=coverage.out -covermode=atomic - - name: Build - run: CGO_ENABLED=0 go build -o main ./cmd/api + - name: Coverage Check + run: | + COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%') + echo "Coverage: ${COVERAGE}%" + if (( $(echo "$COVERAGE < 85" | bc -l) )); then + echo "::error::Coverage ${COVERAGE}% is below 85%" + exit 1 + fi diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml index 8c17f1d..3002337 100644 --- a/.gitea/workflows/deploy-prod.yml +++ b/.gitea/workflows/deploy-prod.yml @@ -1,4 +1,4 @@ -name: Deploy Production +name: Deploy on: push: @@ -8,11 +8,18 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-go@v5 - with: - go-version: '1.22' - - - name: Build - run: CGO_ENABLED=0 go build -o main ./cmd/api + - name: Deploy pulse-api + run: | + cd /opt/digital-home/homelab-api + docker compose pull + docker compose up -d --build + echo "Waiting for healthcheck..." + sleep 5 + STATUS=$(docker inspect --format='{{.State.Health.Status}}' homelab-api 2>/dev/null || echo "no-healthcheck") + echo "Container status: $STATUS" + if [ "$STATUS" = "unhealthy" ]; then + echo "::error::Container is unhealthy after deploy" + docker logs homelab-api --tail=20 + exit 1 + fi + echo "Deploy successful!"