Compare commits
4 Commits
ed14fba6ea
...
e9069c97a8
| Author | SHA1 | Date | |
|---|---|---|---|
| e9069c97a8 | |||
|
|
30a894a78f | ||
|
|
901173a337 | ||
|
|
9e06341564 |
@@ -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
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
@@ -298,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
|
||||
@@ -323,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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user