db fix and new update request

This commit is contained in:
samantha42
2026-03-20 23:02:33 +01:00
parent 6ad5df839b
commit 45f4cca485
8 changed files with 187 additions and 81 deletions

View File

@@ -2,23 +2,78 @@ package database
import (
"context"
"database/sql"
"fmt"
"time"
"Engine/internal/model"
)
type BudgetRepo struct {
db *DB
// SQLite stores timestamps as TEXT in the format written by
// strftime('%Y-%m-%dT%H:%M:%SZ','now').
// modernc.org/sqlite does not auto-convert TEXT → time.Time, so we scan
// into string and parse at the repo boundary with parseTime().
const sqliteTimeLayout = "2006-01-02T15:04:05Z"
func parseTime(s string) (time.Time, error) {
t, err := time.Parse(sqliteTimeLayout, s)
if err != nil {
// fallback: SQLite can also produce "2006-01-02 15:04:05"
t, err = time.Parse("2006-01-02 15:04:05", s)
}
return t, err
}
func NewBudgetRepo(db *DB) *BudgetRepo {
// scanBudget scans a budgets row into model.Budget.
// Accepts the 12 columns in SELECT order:
//
// id, fiscal_year, fiscal_period, version, department_id, gl_account_id,
// amount, currency, notes, created_by, created_at, updated_at
func scanBudget(row interface {
Scan(dest ...any) error
}) (*model.Budget, error) {
var b model.Budget
var createdAt, updatedAt string
err := row.Scan(
&b.ID, &b.FiscalYear, &b.FiscalPeriod, &b.Version,
&b.DepartmentID, &b.GLAccountID, &b.Amount, &b.Currency,
&b.Notes, &b.CreatedBy, &createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
if b.CreatedAt, err = parseTime(createdAt); err != nil {
return nil, fmt.Errorf("parse created_at %q: %w", createdAt, err)
}
if b.UpdatedAt, err = parseTime(updatedAt); err != nil {
return nil, fmt.Errorf("parse updated_at %q: %w", updatedAt, err)
}
return &b, nil
}
// ── BudgetRepo ────────────────────────────────────────────────────────────────
type BudgetRepo struct {
db *sql.DB
}
func NewBudgetRepo(db *sql.DB) *BudgetRepo {
return &BudgetRepo{db: db}
}
const budgetSelectCols = `
id, fiscal_year, fiscal_period, version, department_id, gl_account_id,
amount, currency, notes, created_by, created_at, updated_at`
func (r *BudgetRepo) Create(ctx context.Context, req model.CreateBudgetRequest) (*model.Budget, error) {
res, err := r.db.ExecContext(ctx, `
INSERT INTO budgets
(fiscal_year, fiscal_period, version, department_id, gl_account_id, amount, currency, notes, created_by)
(fiscal_year, fiscal_period, version, department_id, gl_account_id,
amount, currency, notes, created_by)
VALUES (?,?,?,?,?,?,?,?,?)`,
req.FiscalYear, req.FiscalPeriod, req.Version,
req.DepartmentID, req.GLAccountID, req.Amount,
@@ -28,76 +83,35 @@ func (r *BudgetRepo) Create(ctx context.Context, req model.CreateBudgetRequest)
return nil, fmt.Errorf("create budget: %w", err)
}
id, _ := res.LastInsertId()
id, err := res.LastInsertId()
if err != nil {
return nil, fmt.Errorf("last insert id: %w", err)
}
b := &model.Budget{}
err = r.db.QueryRowContext(ctx, `
SELECT id, fiscal_year, fiscal_period, version, department_id, gl_account_id,
amount, currency, notes, created_by, created_at, updated_at
FROM budgets WHERE id = ?`, id,
).Scan(
&b.ID, &b.FiscalYear, &b.FiscalPeriod, &b.Version,
&b.DepartmentID, &b.GLAccountID, &b.Amount, &b.Currency,
&b.Notes, &b.CreatedBy, &b.CreatedAt, &b.UpdatedAt,
)
row := r.db.QueryRowContext(ctx,
`SELECT`+budgetSelectCols+`FROM budgets WHERE id = ?`, id)
b, err := scanBudget(row)
if err != nil {
return nil, fmt.Errorf("fetch created budget: %w", err)
}
return b, nil
}
func (r *BudgetRepo) List(ctx context.Context, fiscalYear, fiscalPeriod int, deptCode string, version model.BudgetVersion) ([]model.BudgetRow, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT g.code, g.description, g.type, g.favour_high, b.amount, b.currency
FROM budgets b
JOIN departments d ON d.id = b.department_id
JOIN gl_accounts g ON g.id = b.gl_account_id
WHERE b.fiscal_year = ?
AND b.fiscal_period = ?
AND (? = '' OR d.code = ?)
AND (? = '' OR b.version = ?)
ORDER BY g.code`,
fiscalYear, fiscalPeriod,
deptCode, deptCode,
string(version), string(version),
)
if err != nil {
return nil, fmt.Errorf("list budgets: %w", err)
}
defer rows.Close()
var result []model.BudgetRow
for rows.Next() {
var row model.BudgetRow
if err := rows.Scan(&row.GLCode, &row.GLDescription, &row.GLType, &row.FavourHigh, &row.Amount, &row.Currency); err != nil {
return nil, err
}
result = append(result, row)
}
return result, rows.Err()
}
func (r *BudgetRepo) Update(ctx context.Context, id int, amount float64, notes, _ string) (*model.Budget, error) {
func (r *BudgetRepo) Update(ctx context.Context, id int, req model.UpdateBudgetRequest) (*model.Budget, error) {
_, err := r.db.ExecContext(ctx, `
UPDATE budgets
SET amount=?, notes=?, updated_at=CURRENT_TIMESTAMP
SET version=?, amount=?, notes=?,
updated_at=strftime('%Y-%m-%dT%H:%M:%SZ','now')
WHERE id=?`,
amount, notes, id,
req.Version, req.Amount, req.Notes, id,
)
if err != nil {
return nil, fmt.Errorf("update budget %d: %w", id, err)
return nil, fmt.Errorf("update budget: %w", err)
}
b := &model.Budget{}
err = r.db.QueryRowContext(ctx, `
SELECT id, fiscal_year, fiscal_period, version, department_id, gl_account_id,
amount, currency, notes, created_by, created_at, updated_at
FROM budgets WHERE id = ?`, id,
).Scan(
&b.ID, &b.FiscalYear, &b.FiscalPeriod, &b.Version,
&b.DepartmentID, &b.GLAccountID, &b.Amount, &b.Currency,
&b.Notes, &b.CreatedBy, &b.CreatedAt, &b.UpdatedAt,
)
row := r.db.QueryRowContext(ctx,
`SELECT`+budgetSelectCols+`FROM budgets WHERE id = ?`, id)
b, err := scanBudget(row)
if err != nil {
return nil, fmt.Errorf("fetch updated budget: %w", err)
}
@@ -105,12 +119,95 @@ func (r *BudgetRepo) Update(ctx context.Context, id int, amount float64, notes,
}
func (r *BudgetRepo) Delete(ctx context.Context, id int) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM budgets WHERE id=?`, id)
_, err := r.db.ExecContext(ctx, `DELETE FROM budgets WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete budget: %w", err)
}
if n, _ := res.RowsAffected(); n == 0 {
return fmt.Errorf("budget %d not found", id)
}
return nil
}
func (r *BudgetRepo) GetByID(ctx context.Context, id int) (*model.Budget, error) {
row := r.db.QueryRowContext(ctx,
`SELECT`+budgetSelectCols+`FROM budgets WHERE id = ?`, id)
b, err := scanBudget(row)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("get budget: %w", err)
}
return b, nil
}
func (r *BudgetRepo) ListByPeriod(ctx context.Context, fiscalYear, fiscalPeriod int, version model.BudgetVersion) ([]model.Budget, error) {
rows, err := r.db.QueryContext(ctx,
`SELECT`+budgetSelectCols+`
FROM budgets
WHERE fiscal_year=? AND fiscal_period=? AND version=?
ORDER BY department_id, gl_account_id`,
fiscalYear, fiscalPeriod, version,
)
if err != nil {
return nil, fmt.Errorf("list budgets: %w", err)
}
defer rows.Close()
var budgets []model.Budget
for rows.Next() {
b, err := scanBudget(rows)
if err != nil {
return nil, fmt.Errorf("scan budget row: %w", err)
}
budgets = append(budgets, *b)
}
return budgets, rows.Err()
}
// List returns BudgetRow slice joined with gl_accounts — used by VarianceService.
// Filters by fiscal_year, fiscal_period, version, and optionally dept_code
// (matched against departments.code via JOIN).
func (r *BudgetRepo) List(
ctx context.Context,
fiscalYear, fiscalPeriod int,
deptCode string,
version model.BudgetVersion,
) ([]model.BudgetRow, error) {
q := `
SELECT
g.code,
g.description,
g.type,
g.favour_high,
b.amount,
b.currency
FROM budgets b
JOIN gl_accounts g ON g.id = b.gl_account_id
JOIN departments d ON d.id = b.department_id
WHERE b.fiscal_year = ?
AND b.fiscal_period = ?
AND b.version = ?
AND d.code = ?
ORDER BY g.code`
rows, err := r.db.QueryContext(ctx, q,
fiscalYear, fiscalPeriod, version, deptCode)
if err != nil {
return nil, fmt.Errorf("budget list: %w", err)
}
defer rows.Close()
var out []model.BudgetRow
for rows.Next() {
var br model.BudgetRow
var favourHigh int
if err := rows.Scan(
&br.GLCode, &br.GLDescription, &br.GLType,
&favourHigh, &br.Amount, &br.Currency,
); err != nil {
return nil, fmt.Errorf("budget list scan: %w", err)
}
br.FavourHigh = favourHigh == 1
out = append(out, br)
}
return out, rows.Err()
}