Add balance adjustments (M8)
- 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
This commit is contained in:
100
internal/store/balance_adjustment_store.go
Normal file
100
internal/store/balance_adjustment_store.go
Normal file
@@ -0,0 +1,100 @@
|
||||
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")
|
||||
Reference in New Issue
Block a user