package commands import ( "context" "database/sql" "testing" "time" "git.tjp.lol/punchcard/internal/queries" ) func TestTimeEntryBillableRateCoalescing(t *testing.T) { tests := []struct { name string clientRate *int64 // nil means NULL, values in cents projectRate *int64 // nil means NULL, values in cents explicitRate *int64 // nil means NULL, values in cents expectedRate *int64 // nil means NULL, values in cents expectError bool }{ { name: "no rates anywhere - should be NULL", clientRate: nil, projectRate: nil, explicitRate: nil, expectedRate: nil, expectError: false, }, { name: "only client rate - should be NULL (no explicit override)", clientRate: func() *int64 { f := int64(10000); return &f }(), // $100.00 projectRate: nil, explicitRate: nil, expectedRate: nil, // NULL because no explicit rate was provided expectError: false, }, { name: "client and project rates - should be NULL (no explicit override)", clientRate: func() *int64 { f := int64(10000); return &f }(), // $100.00 projectRate: func() *int64 { f := int64(15000); return &f }(), // $150.00 explicitRate: nil, expectedRate: nil, // NULL because no explicit rate was provided expectError: false, }, { name: "all rates provided - should use explicit rate", clientRate: func() *int64 { f := int64(10000); return &f }(), // $100.00 projectRate: func() *int64 { f := int64(15000); return &f }(), // $150.00 explicitRate: func() *int64 { f := int64(20000); return &f }(), // $200.00 expectedRate: func() *int64 { f := int64(20000); return &f }(), expectError: false, }, { name: "explicit rate overrides even when zero", clientRate: func() *int64 { f := int64(10000); return &f }(), // $100.00 projectRate: func() *int64 { f := int64(15000); return &f }(), // $150.00 explicitRate: func() *int64 { f := int64(0); return &f }(), // $0.00 expectedRate: func() *int64 { f := int64(0); return &f }(), expectError: false, }, { name: "only project rate with no client rate - should be NULL (no explicit override)", clientRate: nil, projectRate: func() *int64 { f := int64(12500); return &f }(), // $125.00 explicitRate: nil, expectedRate: nil, // NULL because no explicit rate was provided expectError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup fresh database for each test q, cleanup := setupTestDB(t) defer cleanup() // Create client with optional billable rate var clientBillableRate sql.NullInt64 if tt.clientRate != nil { clientBillableRate = sql.NullInt64{Int64: *tt.clientRate, Valid: true} } client, err := q.CreateClient(context.Background(), queries.CreateClientParams{ Name: "TestClient", Email: sql.NullString{}, BillableRate: clientBillableRate, }) if err != nil { t.Fatalf("Failed to create client: %v", err) } // Create project with optional billable rate var projectBillableRate sql.NullInt64 if tt.projectRate != nil { projectBillableRate = sql.NullInt64{Int64: *tt.projectRate, Valid: true} } project, err := q.CreateProject(context.Background(), queries.CreateProjectParams{ Name: "TestProject", ClientID: client.ID, BillableRate: projectBillableRate, }) if err != nil { t.Fatalf("Failed to create project: %v", err) } // Create time entry with optional explicit billable rate var explicitBillableRate sql.NullInt64 if tt.explicitRate != nil { explicitBillableRate = sql.NullInt64{Int64: *tt.explicitRate, Valid: true} } timeEntry, err := q.CreateTimeEntry(context.Background(), queries.CreateTimeEntryParams{ Description: sql.NullString{String: "Test work", Valid: true}, ClientID: client.ID, ProjectID: sql.NullInt64{Int64: project.ID, Valid: true}, BillableRate: explicitBillableRate, }) // Check error expectation if tt.expectError { if err == nil { t.Errorf("Expected error but got none") } return } if err != nil { t.Errorf("Unexpected error: %v", err) return } // Verify the coalesced billable rate if tt.expectedRate == nil { if timeEntry.BillableRate.Valid { t.Errorf("Expected NULL billable_rate, got %d", timeEntry.BillableRate.Int64) } } else { if !timeEntry.BillableRate.Valid { t.Errorf("Expected billable_rate %d, got NULL", *tt.expectedRate) } else if timeEntry.BillableRate.Int64 != *tt.expectedRate { t.Errorf("Expected billable_rate %d, got %d", *tt.expectedRate, timeEntry.BillableRate.Int64) } } }) } } func TestTimeEntryWithTimesCoalescing(t *testing.T) { // Test CreateTimeEntryWithTimes also applies coalescing q, cleanup := setupTestDB(t) defer cleanup() // Create client with rate $100.00 (10000 cents) client, err := q.CreateClient(context.Background(), queries.CreateClientParams{ Name: "RateClient", Email: sql.NullString{}, BillableRate: sql.NullInt64{Int64: 10000, Valid: true}, }) if err != nil { t.Fatalf("Failed to create client: %v", err) } // Create project with rate $150.00 (15000 cents) project, err := q.CreateProject(context.Background(), queries.CreateProjectParams{ Name: "RateProject", ClientID: client.ID, BillableRate: sql.NullInt64{Int64: 15000, Valid: true}, }) if err != nil { t.Fatalf("Failed to create project: %v", err) } // Create time entry with times but no explicit rate - should use project rate now := time.Now().UTC() timeEntry, err := q.CreateTimeEntryWithTimes(context.Background(), queries.CreateTimeEntryWithTimesParams{ StartTime: now.Format(time.DateTime), EndTime: now.Add(time.Hour).Format(time.DateTime), Description: sql.NullString{String: "Test work", Valid: true}, ClientID: client.ID, ProjectID: sql.NullInt64{Int64: project.ID, Valid: true}, BillableRate: sql.NullInt64{}, // No explicit rate }) if err != nil { t.Fatalf("Failed to create time entry: %v", err) } // Should be NULL since no explicit rate was provided (even though client/project have rates) if timeEntry.BillableRate.Valid { t.Errorf("Expected NULL billable_rate (no explicit override), got %d", timeEntry.BillableRate.Int64) } } func TestTimeEntryCoalescingWithoutProject(t *testing.T) { // Test coalescing when no project is specified q, cleanup := setupTestDB(t) defer cleanup() // Create client with rate $75.50 (7550 cents) client, err := q.CreateClient(context.Background(), queries.CreateClientParams{ Name: "NoProjectClient", Email: sql.NullString{}, BillableRate: sql.NullInt64{Int64: 7550, Valid: true}, }) if err != nil { t.Fatalf("Failed to create client: %v", err) } // Create time entry without project - should use client rate timeEntry, err := q.CreateTimeEntry(context.Background(), queries.CreateTimeEntryParams{ Description: sql.NullString{String: "Client work", Valid: true}, ClientID: client.ID, ProjectID: sql.NullInt64{}, // No project BillableRate: sql.NullInt64{}, // No explicit rate }) if err != nil { t.Fatalf("Failed to create time entry: %v", err) } // Should be NULL since no explicit rate was provided (even though client has a rate) if timeEntry.BillableRate.Valid { t.Errorf("Expected NULL billable_rate (no explicit override), got %d", timeEntry.BillableRate.Int64) } }