- New balance_adjustments table with CRUD store, sync logging, and service methods
- SQL migrations restructured: embed fs.FS from internal/store/migrations/, apply in order via user_version
- WeekService.Balance combines closed-weeks delta + adjustments delta; BalanceSummary breakdown
- Four REST routes: GET/POST /api/balance/adjustments, PUT/DELETE /api/balance/adjustments/{id}
- Dexie schema v2 + sync apply cases for balance_adjustments
- API client: BalanceAdjustment type, balance namespace (list/create/update/delete)
- utils: composeDeltaMs / decomposeDeltaMs helpers + 8 new Vitest tests (19 total, all passing)
- History page: balance card breakdown line + full adjustments section with inline add/edit/delete
101 lines
3.0 KiB
Go
101 lines
3.0 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
|
|
"github.com/wotra/wotra/internal/domain"
|
|
)
|
|
|
|
// BalanceAdjustmentStore handles persistence for balance_adjustments.
|
|
type BalanceAdjustmentStore struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewBalanceAdjustmentStore(db *sql.DB) *BalanceAdjustmentStore {
|
|
return &BalanceAdjustmentStore{db: db}
|
|
}
|
|
|
|
func (s *BalanceAdjustmentStore) Create(ctx context.Context, a *domain.BalanceAdjustment) error {
|
|
_, err := s.db.ExecContext(ctx,
|
|
`INSERT INTO balance_adjustments (id, delta_ms, note, effective_at, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
a.ID, a.DeltaMs, a.Note, a.EffectiveAt, a.CreatedAt, a.UpdatedAt,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *BalanceAdjustmentStore) Update(ctx context.Context, a *domain.BalanceAdjustment) error {
|
|
res, err := s.db.ExecContext(ctx,
|
|
`UPDATE balance_adjustments SET delta_ms=?, note=?, effective_at=?, updated_at=? WHERE id=?`,
|
|
a.DeltaMs, a.Note, a.EffectiveAt, a.UpdatedAt, a.ID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return ErrAdjustmentNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *BalanceAdjustmentStore) Delete(ctx context.Context, id string) error {
|
|
res, err := s.db.ExecContext(ctx, `DELETE FROM balance_adjustments WHERE id=?`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return ErrAdjustmentNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *BalanceAdjustmentStore) GetByID(ctx context.Context, id string) (*domain.BalanceAdjustment, error) {
|
|
row := s.db.QueryRowContext(ctx,
|
|
`SELECT id, delta_ms, note, effective_at, created_at, updated_at
|
|
FROM balance_adjustments WHERE id=?`, id)
|
|
var a domain.BalanceAdjustment
|
|
err := row.Scan(&a.ID, &a.DeltaMs, &a.Note, &a.EffectiveAt, &a.CreatedAt, &a.UpdatedAt)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrAdjustmentNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &a, nil
|
|
}
|
|
|
|
// List returns all adjustments ordered by effective_at DESC.
|
|
func (s *BalanceAdjustmentStore) List(ctx context.Context) ([]*domain.BalanceAdjustment, error) {
|
|
rows, err := s.db.QueryContext(ctx,
|
|
`SELECT id, delta_ms, note, effective_at, created_at, updated_at
|
|
FROM balance_adjustments ORDER BY effective_at DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var result []*domain.BalanceAdjustment
|
|
for rows.Next() {
|
|
var a domain.BalanceAdjustment
|
|
if err := rows.Scan(&a.ID, &a.DeltaMs, &a.Note, &a.EffectiveAt, &a.CreatedAt, &a.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, &a)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
// SumDelta returns the sum of delta_ms and row count across all adjustments.
|
|
func (s *BalanceAdjustmentStore) SumDelta(ctx context.Context) (totalDeltaMs int64, count int, err error) {
|
|
err = s.db.QueryRowContext(ctx,
|
|
`SELECT COALESCE(SUM(delta_ms), 0), COUNT(*) FROM balance_adjustments`,
|
|
).Scan(&totalDeltaMs, &count)
|
|
return
|
|
}
|
|
|
|
// ErrAdjustmentNotFound is returned when no balance_adjustment row matches the given ID.
|
|
var ErrAdjustmentNotFound = errors.New("balance adjustment not found")
|