package actions import ( "context" "database/sql" "fmt" "regexp" "strconv" "strings" "punchcard/internal/queries" ) // CreateClient creates a new client with the given name and optional email/rate func (a *actionsImpl) CreateClient(ctx context.Context, name, email string, billableRate *float64) (*queries.Client, error) { // Parse name and email if name contains email format "Name " finalName, finalEmail := parseNameAndEmail(name, email) var emailParam sql.NullString if finalEmail != "" { emailParam = sql.NullString{String: finalEmail, Valid: true} } var billableRateParam sql.NullInt64 if billableRate != nil && *billableRate > 0 { rate := int64(*billableRate * 100) // Convert dollars to cents billableRateParam = sql.NullInt64{Int64: rate, Valid: true} } client, err := a.queries.CreateClient(ctx, queries.CreateClientParams{ Name: finalName, Email: emailParam, BillableRate: billableRateParam, }) if err != nil { return nil, fmt.Errorf("failed to create client: %w", err) } return &client, nil } // FindClient finds a client by name or ID func (a *actionsImpl) FindClient(ctx context.Context, nameOrID string) (*queries.Client, error) { // Parse as ID if possible, otherwise use 0 var idParam int64 if id, err := strconv.ParseInt(nameOrID, 10, 64); err == nil { idParam = id } // Search by both ID and name clients, err := a.queries.FindClient(ctx, queries.FindClientParams{ ID: idParam, Name: nameOrID, }) if err != nil { return nil, fmt.Errorf("database error looking up client: %w", err) } // Check results switch len(clients) { case 0: return nil, fmt.Errorf("%w: %s", ErrClientNotFound, nameOrID) case 1: return &clients[0], nil default: return nil, fmt.Errorf("%w: %s matches multiple clients", ErrAmbiguousClient, nameOrID) } } // parseNameAndEmail handles parsing name and email from various input formats func parseNameAndEmail(nameArg, emailArg string) (string, string) { // If separate email provided, use it (but still check for embedded format) finalEmail := emailArg if finalEmail != "" { if matches := emailAndNameRegex.FindStringSubmatch(finalEmail); matches != nil { finalEmail = strings.TrimSpace(matches[2]) } } // Check if name contains embedded email format "Name " finalName := nameArg if matches := emailAndNameRegex.FindStringSubmatch(nameArg); matches != nil { finalName = strings.TrimSpace(matches[1]) if finalEmail == "" { finalEmail = strings.TrimSpace(matches[2]) } } return finalName, finalEmail } var emailAndNameRegex = regexp.MustCompile(`^(.+?)<([^>]+@[^>]+)>$`)