summaryrefslogtreecommitdiff
path: root/internal/actions/clients.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/actions/clients.go')
-rw-r--r--internal/actions/clients.go92
1 files changed, 92 insertions, 0 deletions
diff --git a/internal/actions/clients.go b/internal/actions/clients.go
new file mode 100644
index 0000000..bc77139
--- /dev/null
+++ b/internal/actions/clients.go
@@ -0,0 +1,92 @@
+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 <email>"
+ 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 <email@domain.com>"
+ 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(`^(.+?)<([^>]+@[^>]+)>$`) \ No newline at end of file