- 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
129 lines
3.3 KiB
Go
129 lines
3.3 KiB
Go
package store_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/wotra/wotra/internal/domain"
|
|
"github.com/wotra/wotra/internal/store"
|
|
)
|
|
|
|
func TestBalanceAdjustmentStoreCRUD(t *testing.T) {
|
|
db, err := store.Open(":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
s := store.NewBalanceAdjustmentStore(db)
|
|
ctx := context.Background()
|
|
now := time.Now().UnixMilli()
|
|
|
|
// SumDelta on empty table.
|
|
total, count, err := s.SumDelta(ctx)
|
|
if err != nil {
|
|
t.Fatalf("SumDelta empty: %v", err)
|
|
}
|
|
if total != 0 || count != 0 {
|
|
t.Fatalf("empty: want (0,0), got (%d,%d)", total, count)
|
|
}
|
|
|
|
// List on empty table returns nil slice, no error.
|
|
list, err := s.List(ctx)
|
|
if err != nil {
|
|
t.Fatalf("List empty: %v", err)
|
|
}
|
|
if len(list) != 0 {
|
|
t.Errorf("List empty: want 0, got %d", len(list))
|
|
}
|
|
|
|
// Create two adjustments.
|
|
a1 := &domain.BalanceAdjustment{
|
|
ID: "id-1", DeltaMs: 3_600_000, Note: "carry-over",
|
|
EffectiveAt: now - 86400000, CreatedAt: now, UpdatedAt: now,
|
|
}
|
|
a2 := &domain.BalanceAdjustment{
|
|
ID: "id-2", DeltaMs: -1_800_000, Note: "",
|
|
EffectiveAt: now, CreatedAt: now, UpdatedAt: now,
|
|
}
|
|
if err := s.Create(ctx, a1); err != nil {
|
|
t.Fatalf("Create a1: %v", err)
|
|
}
|
|
if err := s.Create(ctx, a2); err != nil {
|
|
t.Fatalf("Create a2: %v", err)
|
|
}
|
|
|
|
// GetByID round-trip.
|
|
got, err := s.GetByID(ctx, "id-1")
|
|
if err != nil {
|
|
t.Fatalf("GetByID: %v", err)
|
|
}
|
|
if got.DeltaMs != a1.DeltaMs || got.Note != a1.Note {
|
|
t.Errorf("GetByID: want {%d,%q}, got {%d,%q}", a1.DeltaMs, a1.Note, got.DeltaMs, got.Note)
|
|
}
|
|
|
|
// GetByID missing.
|
|
_, err = s.GetByID(ctx, "no-such")
|
|
if err != store.ErrAdjustmentNotFound {
|
|
t.Errorf("GetByID missing: want ErrAdjustmentNotFound, got %v", err)
|
|
}
|
|
|
|
// SumDelta with two rows.
|
|
total, count, err = s.SumDelta(ctx)
|
|
if err != nil {
|
|
t.Fatalf("SumDelta: %v", err)
|
|
}
|
|
if count != 2 {
|
|
t.Errorf("count: want 2, got %d", count)
|
|
}
|
|
want := int64(3_600_000 - 1_800_000)
|
|
if total != want {
|
|
t.Errorf("total: want %d, got %d", want, total)
|
|
}
|
|
|
|
// List returns newest effective_at first (a2 has now, a1 has now-1day).
|
|
list, err = s.List(ctx)
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if len(list) != 2 {
|
|
t.Fatalf("List len: want 2, got %d", len(list))
|
|
}
|
|
if list[0].ID != "id-2" {
|
|
t.Errorf("List[0]: want id-2, got %s", list[0].ID)
|
|
}
|
|
|
|
// Update a1.
|
|
a1.DeltaMs = 7_200_000
|
|
a1.Note = "updated"
|
|
a1.UpdatedAt = now + 1000
|
|
if err := s.Update(ctx, a1); err != nil {
|
|
t.Fatalf("Update: %v", err)
|
|
}
|
|
got, _ = s.GetByID(ctx, "id-1")
|
|
if got.DeltaMs != 7_200_000 || got.Note != "updated" {
|
|
t.Errorf("after Update: want {7200000,updated}, got {%d,%q}", got.DeltaMs, got.Note)
|
|
}
|
|
|
|
// Update missing.
|
|
missing := &domain.BalanceAdjustment{ID: "no-such", DeltaMs: 1, UpdatedAt: now}
|
|
if err := s.Update(ctx, missing); err != store.ErrAdjustmentNotFound {
|
|
t.Errorf("Update missing: want ErrAdjustmentNotFound, got %v", err)
|
|
}
|
|
|
|
// Delete a2.
|
|
if err := s.Delete(ctx, "id-2"); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
total, count, _ = s.SumDelta(ctx)
|
|
if count != 1 || total != 7_200_000 {
|
|
t.Errorf("after Delete: want (7200000,1), got (%d,%d)", total, count)
|
|
}
|
|
|
|
// Delete missing.
|
|
if err := s.Delete(ctx, "no-such"); err != store.ErrAdjustmentNotFound {
|
|
t.Errorf("Delete missing: want ErrAdjustmentNotFound, got %v", err)
|
|
}
|
|
}
|