package cli import ( "errors" "flag" "fmt" "slices" "strings" "text/tabwriter" "sultan/pkg/accountant" "sultan/pkg/ledger" ) // parseCycle versteht m/q/j bzw. monthly/quarterly/yearly (und deutsche // Varianten); "offen" bzw. "none" = noch nicht eingeordnet. func parseCycle(s string) (string, error) { switch strings.ToLower(strings.TrimSpace(s)) { case "m", "monthly", "monatlich": return ledger.CycleMonthly, nil case "q", "quarterly", "quartal", "quartalsweise": return ledger.CycleQuarterly, nil case "j", "y", "yearly", "jährlich", "jaehrlich": return ledger.CycleYearly, nil case "offen", "none", "-": return ledger.CycleUnclassified, nil } return "", fmt.Errorf("Zahlweise %q unbekannt (m, q, j oder offen)", s) } func cycleLabel(c string) string { switch c { case ledger.CycleMonthly: return "monatlich" case ledger.CycleQuarterly: return "quartalsweise" case ledger.CycleYearly: return "jährlich" } return "nicht eingeordnet" } // customerFields sind die Felder, die add und set gemeinsam haben. type customerFields struct { name, group, note, cycle, flat *string noFlat *bool } func addCustomerFlags(fs *flag.FlagSet) customerFields { return customerFields{ name: fs.String("name", "", "Name"), group: fs.String("group", "", "Kostenverursacher-Gruppe zum Clustern (z. B. extern, intern, familie)"), note: fs.String("note", "", "Notiz, z. B. persönliche Nebenabreden"), cycle: fs.String("cycle", "", "Zahlweise: m (monatlich), q (quartalsweise), j (jährlich), offen"), flat: fs.String("flat-rate", "", "Pauschale je Monat in Euro, z. B. 89,00"), noFlat: fs.Bool("no-flat-rate", false, "keine Pauschale"), } } // apply übernimmt die auf der Befehlszeile gesetzten Felder in c. func (f customerFields) apply(fs *flag.FlagSet, c *ledger.Customer) error { set := map[string]bool{} fs.Visit(func(fl *flag.Flag) { set[fl.Name] = true }) if set["flat-rate"] && *f.noFlat { return usageError{errors.New("--flat-rate und --no-flat-rate schließen sich aus")} } if set["name"] { c.Name = *f.name } if set["group"] { c.Group = *f.group } if set["note"] { c.Note = *f.note } if set["cycle"] { cycle, err := parseCycle(*f.cycle) if err != nil { return usageError{err} } c.Cycle = cycle } if set["flat-rate"] { cents, err := accountant.ParseMoney(*f.flat) if err != nil { return usageError{err} } c.FlatRate = ¢s } if *f.noFlat { c.FlatRate = nil } return nil } func customerAdd(env tokenEnv, args []string) error { fs, dir := accountantFlags("customer add", env) id := fs.String("id", "", "Kunden-ID (a–z, 0–9, - und _), z. B. Kundennummer") fields := addCustomerFlags(fs) if err := parse(fs, args, "id", "name"); err != nil { return err } c := ledger.Customer{ID: *id} if err := fields.apply(fs, &c); err != nil { return err } l, err := openLedger(env, *dir) if err != nil { return err } defer l.Close() if err := l.AddCustomer(env.ctx, c); err != nil { return err } fmt.Fprintf(env.stdout, "Kunde %s angelegt\n", c.ID) return nil } func customerSet(env tokenEnv, args []string) error { fs, dir := accountantFlags("customer set", env) id := fs.String("id", "", "Kunden-ID") fields := addCustomerFlags(fs) if err := parse(fs, args, "id"); err != nil { return err } l, err := openLedger(env, *dir) if err != nil { return err } defer l.Close() c, err := l.GetCustomer(env.ctx, *id) if err != nil { return err } if err := fields.apply(fs, &c); err != nil { return err } if err := l.UpdateCustomer(env.ctx, c); err != nil { return err } fmt.Fprintf(env.stdout, "Kunde %s geändert\n", c.ID) return nil } func customerList(env tokenEnv, args []string) error { fs, dir := accountantFlags("customer list", env) group := fs.String("group", "", "nur Kunden dieser Gruppe") unclassified := fs.Bool("unclassified", false, "nur Kunden ohne Zahlweise (Feedback der Buchhaltung fehlt)") if err := parse(fs, args); err != nil { return err } l, err := openLedger(env, *dir) if err != nil { return err } defer l.Close() customers, err := l.ListCustomers(env.ctx) if err != nil { return err } maps, err := l.ListMappings(env.ctx) if err != nil { return err } tenants := map[string][]string{} for _, c := range customers { tenants[c.ID] = []string{c.ID} // Mandant mit gleicher ID (sofern nicht anders zugeordnet) } for _, m := range maps { name := m.Tenant if m.Payee != "" { name = m.Payee + "/" + m.Tenant } if !slices.Contains(tenants[m.Customer], name) { tenants[m.Customer] = append(tenants[m.Customer], name) } } tw := tabwriter.NewWriter(env.stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(tw, "ID\tNAME\tGROUP\tCYCLE\tFLAT_RATE\tTENANTS\tNOTE") for _, c := range customers { if (*group != "" && c.Group != *group) || (*unclassified && c.Cycle != ledger.CycleUnclassified) { continue } flat := "-" if c.FlatRate != nil { flat = accountant.FormatMoney(*c.FlatRate) } name := c.Name if c.Auto { name += " (automatisch)" } fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", c.ID, name, cmpOrDash(c.Group), cycleLabel(c.Cycle), flat, strings.Join(tenants[c.ID], ","), c.Note) } return tw.Flush() } func cmpOrDash(s string) string { if s == "" { return "-" } return s } func customerDelete(env tokenEnv, args []string) error { fs, dir := accountantFlags("customer delete", env) id := fs.String("id", "", "Kunden-ID") if err := parse(fs, args, "id"); err != nil { return err } l, err := openLedger(env, *dir) if err != nil { return err } defer l.Close() if err := l.DeleteCustomer(env.ctx, *id); err != nil { return err } fmt.Fprintf(env.stdout, "Kunde %s samt Zuordnungen gelöscht\n", *id) return nil } func customerMap(env tokenEnv, args []string) error { fs, dir := accountantFlags("customer map", env) tenant := fs.String("tenant", "", "Mandanten-ID, wie sie der Sultan-Server meldet") payee := fs.String("payee", "", "nur auf diesem Payee (Default: auf allen)") customer := fs.String("customer", "", "Kunden-ID") if err := parse(fs, args, "tenant", "customer"); err != nil { return err } l, err := openLedger(env, *dir) if err != nil { return err } defer l.Close() if err := l.MapTenant(env.ctx, *payee, *tenant, *customer); err != nil { return err } where := "allen Payees" if *payee != "" { where = "Payee " + *payee } fmt.Fprintf(env.stdout, "Mandant %s auf %s → Kunde %s\n", *tenant, where, *customer) if c, err := l.GetCustomer(env.ctx, *tenant); err == nil && c.Auto && c.ID != *customer { fmt.Fprintf(env.stderr, "Hinweis: Kunde %s wurde automatisch angelegt und ist ggf. überflüssig: sultan accountant customer delete --id=%s\n", c.ID, c.ID) } return nil } func customerUnmap(env tokenEnv, args []string) error { fs, dir := accountantFlags("customer unmap", env) tenant := fs.String("tenant", "", "Mandanten-ID") payee := fs.String("payee", "", "Payee der Zuordnung (leer = Zuordnung für alle Payees)") if err := parse(fs, args, "tenant"); err != nil { return err } l, err := openLedger(env, *dir) if err != nil { return err } defer l.Close() if err := l.UnmapTenant(env.ctx, *payee, *tenant); err != nil { return err } fmt.Fprintf(env.stdout, "Zuordnung von %s entfernt\n", *tenant) return nil }