179 lines
5.6 KiB
Go
179 lines
5.6 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
)
|
|
|
|
// ── Domain types ──────────────────────────────────────────────────────────────
|
|
|
|
type Department struct {
|
|
ID int `json:"id"`
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
CostCenter string `json:"cost_center"`
|
|
Active bool `json:"active"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
type GLAccount struct {
|
|
ID int `json:"id"`
|
|
Code string `json:"code"`
|
|
Description string `json:"description"`
|
|
Type string `json:"type"` // revenue | cogs | opex | capex | headcount
|
|
FavourHigh bool `json:"favour_high"` // true = over-budget is good (revenue accounts)
|
|
Active bool `json:"active"`
|
|
}
|
|
|
|
// ── ReferenceRepo ─────────────────────────────────────────────────────────────
|
|
|
|
type ReferenceRepo struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewReferenceRepo(db *sql.DB) *ReferenceRepo {
|
|
return &ReferenceRepo{db: db}
|
|
}
|
|
|
|
// ── Department operations ─────────────────────────────────────────────────────
|
|
|
|
func (r *ReferenceRepo) CreateDepartment(ctx context.Context, d Department) (*Department, error) {
|
|
res, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO departments (code, name, cost_center, active)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT(code) DO UPDATE
|
|
SET name = excluded.name,
|
|
cost_center = excluded.cost_center,
|
|
active = excluded.active`,
|
|
d.Code, d.Name, d.CostCenter, boolToInt(d.Active),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("upsert department: %w", err)
|
|
}
|
|
|
|
id, err := res.LastInsertId()
|
|
if err != nil || id == 0 {
|
|
// ON CONFLICT branch — fetch existing row
|
|
return r.getDepartmentByCode(ctx, d.Code)
|
|
}
|
|
d.ID = int(id)
|
|
return &d, nil
|
|
}
|
|
|
|
func (r *ReferenceRepo) getDepartmentByCode(ctx context.Context, code string) (*Department, error) {
|
|
var d Department
|
|
var active int
|
|
err := r.db.QueryRowContext(ctx,
|
|
`SELECT id, code, name, cost_center, active, created_at FROM departments WHERE code = ?`, code,
|
|
).Scan(&d.ID, &d.Code, &d.Name, &d.CostCenter, &active, &d.CreatedAt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetch department by code: %w", err)
|
|
}
|
|
d.Active = active == 1
|
|
return &d, nil
|
|
}
|
|
|
|
func (r *ReferenceRepo) ListDepartments(ctx context.Context) ([]Department, error) {
|
|
rows, err := r.db.QueryContext(ctx,
|
|
`SELECT id, code, name, cost_center, active, created_at
|
|
FROM departments ORDER BY code`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var depts []Department
|
|
for rows.Next() {
|
|
var d Department
|
|
var active int
|
|
if err := rows.Scan(&d.ID, &d.Code, &d.Name, &d.CostCenter, &active, &d.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
d.Active = active == 1
|
|
depts = append(depts, d)
|
|
}
|
|
return depts, rows.Err()
|
|
}
|
|
|
|
func (r *ReferenceRepo) DeleteDepartment(ctx context.Context, id int) error {
|
|
_, err := r.db.ExecContext(ctx, `DELETE FROM departments WHERE id = ?`, id)
|
|
return err
|
|
}
|
|
|
|
// ── GL Account operations ─────────────────────────────────────────────────────
|
|
|
|
func (r *ReferenceRepo) CreateGLAccount(ctx context.Context, a GLAccount) (*GLAccount, error) {
|
|
res, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO gl_accounts (code, description, type, favour_high, active)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON CONFLICT(code) DO UPDATE
|
|
SET description = excluded.description,
|
|
type = excluded.type,
|
|
favour_high = excluded.favour_high,
|
|
active = excluded.active`,
|
|
a.Code, a.Description, a.Type, boolToInt(a.FavourHigh), boolToInt(a.Active),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("upsert gl_account: %w", err)
|
|
}
|
|
|
|
id, err := res.LastInsertId()
|
|
if err != nil || id == 0 {
|
|
return r.getGLAccountByCode(ctx, a.Code)
|
|
}
|
|
a.ID = int(id)
|
|
return &a, nil
|
|
}
|
|
|
|
func (r *ReferenceRepo) getGLAccountByCode(ctx context.Context, code string) (*GLAccount, error) {
|
|
var a GLAccount
|
|
var favourHigh, active int
|
|
err := r.db.QueryRowContext(ctx,
|
|
`SELECT id, code, description, type, favour_high, active FROM gl_accounts WHERE code = ?`, code,
|
|
).Scan(&a.ID, &a.Code, &a.Description, &a.Type, &favourHigh, &active)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetch gl_account by code: %w", err)
|
|
}
|
|
a.FavourHigh = favourHigh == 1
|
|
a.Active = active == 1
|
|
return &a, nil
|
|
}
|
|
|
|
func (r *ReferenceRepo) ListGLAccounts(ctx context.Context) ([]GLAccount, error) {
|
|
rows, err := r.db.QueryContext(ctx,
|
|
`SELECT id, code, description, type, favour_high, active
|
|
FROM gl_accounts ORDER BY code`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var accts []GLAccount
|
|
for rows.Next() {
|
|
var a GLAccount
|
|
var favourHigh, active int
|
|
if err := rows.Scan(&a.ID, &a.Code, &a.Description, &a.Type, &favourHigh, &active); err != nil {
|
|
return nil, err
|
|
}
|
|
a.FavourHigh = favourHigh == 1
|
|
a.Active = active == 1
|
|
accts = append(accts, a)
|
|
}
|
|
return accts, rows.Err()
|
|
}
|
|
|
|
func (r *ReferenceRepo) DeleteGLAccount(ctx context.Context, id int) error {
|
|
_, err := r.db.ExecContext(ctx, `DELETE FROM gl_accounts WHERE id = ?`, id)
|
|
return err
|
|
}
|
|
|
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
func boolToInt(b bool) int {
|
|
if b {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|