mirror of
https://github.com/chenasraf/cospend-cli.git
synced 2026-05-18 01:39:03 +00:00
feat: specify date for add command
This commit is contained in:
@@ -177,6 +177,12 @@ cospend add "Hotel" 150.00 -p vacation -m "credit card" -o "2 nights"
|
||||
|
||||
# Add an expense in a different currency
|
||||
cospend add "Souvenirs" 30.00 -p vacation -C usd
|
||||
|
||||
# Add an expense with a specific date
|
||||
cospend add "Lunch" 15.00 -p myproject -d 2026-03-15
|
||||
cospend add "Lunch" 15.00 -p myproject -d 03-15 # assumes current year
|
||||
cospend add "Lunch" 15.00 -p myproject -d -1d # yesterday
|
||||
cospend add "Lunch" 15.00 -p myproject -d +2d # 2 days from now
|
||||
```
|
||||
|
||||
#### Add Command Flags
|
||||
@@ -190,6 +196,7 @@ cospend add "Souvenirs" 30.00 -p vacation -C usd
|
||||
| `-C` | `--convert` | Currency to convert to (by ID, name, or code like `usd`) |
|
||||
| `-m` | `--method` | Payment method by ID or case-insensitive name |
|
||||
| `-o` | `--comment` | Additional details about the bill |
|
||||
| `-d` | `--date` | Date of expense (`YYYY-MM-DD`, `MM-DD`, or relative like `-1d`, `+2w`) |
|
||||
| `-h` | `--help` | Display help information |
|
||||
|
||||
---
|
||||
|
||||
54
cmd/add.go
54
cmd/add.go
@@ -20,6 +20,7 @@ var (
|
||||
convertTo string
|
||||
paymentMethod string
|
||||
comment string
|
||||
addDate string
|
||||
)
|
||||
|
||||
// NewAddCommand creates the add command
|
||||
@@ -42,6 +43,7 @@ Examples:
|
||||
cmd.Flags().StringVarP(&convertTo, "convert", "C", "", "Currency to convert to")
|
||||
cmd.Flags().StringVarP(&paymentMethod, "method", "m", "", "Payment method by ID or name")
|
||||
cmd.Flags().StringVarP(&comment, "comment", "o", "", "Additional details about the bill")
|
||||
cmd.Flags().StringVarP(&addDate, "date", "d", "", "Date of expense (YYYY-MM-DD, MM-DD, or relative like -1d, +2w)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -118,13 +120,23 @@ func runAdd(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve date
|
||||
billDate := time.Now().Format("2006-01-02")
|
||||
if addDate != "" {
|
||||
parsed, err := parseDate(addDate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
billDate = parsed
|
||||
}
|
||||
|
||||
// Build bill
|
||||
bill := api.Bill{
|
||||
What: expenseName,
|
||||
Amount: amount,
|
||||
PayerID: payerID,
|
||||
OwedTo: owedIDs,
|
||||
Date: time.Now().Format("2006-01-02"),
|
||||
Date: billDate,
|
||||
}
|
||||
|
||||
// Resolve optional category
|
||||
@@ -215,5 +227,45 @@ func runAdd(cmd *cobra.Command, args []string) error {
|
||||
if bill.Comment != "" {
|
||||
_, _ = fmt.Fprintf(out, " Comment: %s\n", bill.Comment)
|
||||
}
|
||||
if addDate != "" {
|
||||
_, _ = fmt.Fprintf(out, " Date: %s\n", bill.Date)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseDate(s string) (string, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
// Try relative date: -1d, +2d, -1w, +2w, -1m, +2m
|
||||
if len(s) >= 2 && (s[0] == '+' || s[0] == '-') {
|
||||
unit := s[len(s)-1]
|
||||
valueStr := s[1 : len(s)-1]
|
||||
value, err := strconv.Atoi(valueStr)
|
||||
if err == nil {
|
||||
if s[0] == '-' {
|
||||
value = -value
|
||||
}
|
||||
now := time.Now()
|
||||
switch unit {
|
||||
case 'd':
|
||||
return now.AddDate(0, 0, value).Format("2006-01-02"), nil
|
||||
case 'w':
|
||||
return now.AddDate(0, 0, value*7).Format("2006-01-02"), nil
|
||||
case 'm':
|
||||
return now.AddDate(0, value, 0).Format("2006-01-02"), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try full date YYYY-MM-DD
|
||||
if _, err := time.Parse("2006-01-02", s); err == nil {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Try short date MM-DD (assume current year)
|
||||
if t, err := time.Parse("01-02", s); err == nil {
|
||||
return fmt.Sprintf("%d-%s", time.Now().Year(), t.Format("01-02")), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("invalid date: %s (expected YYYY-MM-DD, MM-DD, or relative like -1d, +2w)", s)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/chenasraf/cospend-cli/internal/api"
|
||||
)
|
||||
@@ -41,6 +43,7 @@ func resetFlags() {
|
||||
convertTo = ""
|
||||
paymentMethod = ""
|
||||
comment = ""
|
||||
addDate = ""
|
||||
infoCached = false
|
||||
}
|
||||
|
||||
@@ -72,7 +75,7 @@ func TestNewAddCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check flags exist (project is now a persistent flag on root)
|
||||
flags := []string{"category", "by", "for", "convert", "method", "comment"}
|
||||
flags := []string{"category", "by", "for", "convert", "method", "comment", "date"}
|
||||
for _, flag := range flags {
|
||||
if cmd.Flags().Lookup(flag) == nil {
|
||||
t.Errorf("Missing flag: %s", flag)
|
||||
@@ -87,6 +90,7 @@ func TestNewAddCommand(t *testing.T) {
|
||||
"C": "convert",
|
||||
"m": "method",
|
||||
"o": "comment",
|
||||
"d": "date",
|
||||
}
|
||||
for short, long := range shortFlags {
|
||||
flag := cmd.Flags().ShorthandLookup(short)
|
||||
@@ -484,3 +488,92 @@ func TestAddCommandAPIError(t *testing.T) {
|
||||
t.Error("Expected error from API")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantDate string
|
||||
wantErr bool
|
||||
}{
|
||||
{"full date", "2026-03-15", "2026-03-15", false},
|
||||
{"short date", "03-15", fmt.Sprintf("%d-03-15", time.Now().Year()), false},
|
||||
{"with spaces", " 2026-01-01 ", "2026-01-01", false},
|
||||
{"relative -1d", "-1d", time.Now().AddDate(0, 0, -1).Format("2006-01-02"), false},
|
||||
{"relative +2d", "+2d", time.Now().AddDate(0, 0, 2).Format("2006-01-02"), false},
|
||||
{"relative -1w", "-1w", time.Now().AddDate(0, 0, -7).Format("2006-01-02"), false},
|
||||
{"relative +2w", "+2w", time.Now().AddDate(0, 0, 14).Format("2006-01-02"), false},
|
||||
{"relative -1m", "-1m", time.Now().AddDate(0, -1, 0).Format("2006-01-02"), false},
|
||||
{"relative +3m", "+3m", time.Now().AddDate(0, 3, 0).Format("2006-01-02"), false},
|
||||
{"invalid", "not-a-date", "", true},
|
||||
{"invalid short", "13-40", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseDate(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseDate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && got != tt.wantDate {
|
||||
t.Errorf("parseDate() = %v, want %v", got, tt.wantDate)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddCommandWithDate(t *testing.T) {
|
||||
project := api.Project{
|
||||
ID: "test-project",
|
||||
Name: "Test Project",
|
||||
Members: []api.Member{
|
||||
{ID: 1, Name: "testuser", UserID: "testuser"},
|
||||
},
|
||||
}
|
||||
|
||||
var receivedBill map[string]string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/ocs/v2.php/apps/cospend/api/v1/projects/test-project" {
|
||||
_ = json.NewEncoder(w).Encode(makeOCSResponse(200, project))
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/ocs/v2.php/cloud/user" {
|
||||
_ = json.NewEncoder(w).Encode(makeOCSResponse(200, map[string]string{"locale": "en_US", "language": "en"}))
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/ocs/v2.php/apps/cospend/api/v1/projects/test-project/bills" {
|
||||
_ = r.ParseForm()
|
||||
receivedBill = make(map[string]string)
|
||||
for k, v := range r.Form {
|
||||
if len(v) > 0 {
|
||||
receivedBill[k] = v[0]
|
||||
}
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(makeOCSResponse(200, map[string]int{"id": 1}))
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cleanup := setupTestEnv(t, server.URL)
|
||||
defer cleanup()
|
||||
|
||||
ProjectID = "test-project"
|
||||
cmd := NewAddCommand()
|
||||
var stdout bytes.Buffer
|
||||
cmd.SetOut(&stdout)
|
||||
cmd.SetArgs([]string{"Groceries", "25.50", "-d", "2026-06-15"})
|
||||
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if receivedBill["date"] != "2026-06-15" {
|
||||
t.Errorf("Wrong date: got %s, want 2026-06-15", receivedBill["date"])
|
||||
}
|
||||
if !bytes.Contains(stdout.Bytes(), []byte("Date: 2026-06-15")) {
|
||||
t.Errorf("Output should show date, got:\n%s", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user