diff --git a/cmd/zeltapp/client.go b/cmd/zeltapp/client.go index a1f5484..d40cfb3 100644 --- a/cmd/zeltapp/client.go +++ b/cmd/zeltapp/client.go @@ -415,6 +415,12 @@ func (c *client) dispatch(method, path string, bodyBytes []byte, out any, retry } } if out != nil && len(rb) > 0 { + // *[]byte callers get the raw body (e.g. PDF downloads), everyone + // else gets JSON decoding. + if b, ok := out.(*[]byte); ok { + *b = rb + return nil + } return json.Unmarshal(rb, out) } return nil diff --git a/cmd/zeltapp/finance.go b/cmd/zeltapp/finance.go index 1044aa4..c75d0d7 100644 --- a/cmd/zeltapp/finance.go +++ b/cmd/zeltapp/finance.go @@ -1,9 +1,12 @@ package main import ( + "bytes" "encoding/json" "fmt" "net/url" + "os" + "path/filepath" "github.com/spf13/cobra" ) @@ -24,6 +27,71 @@ func payslipCmd() *cobra.Command { fmt.Sprintf("/apiv2/users/%d/payslips", id), } })) + cmd.AddCommand(payslipDownloadCmd()) + return cmd +} + +func payslipDownloadCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "download [YYYY-MM ...]", + Aliases: []string{"dl"}, + Short: "Download payslip PDFs as /YYYY-MM.pdf (no args: all months missing from --dir)", + RunE: func(cmd *cobra.Command, args []string) error { + user, _ := cmd.Flags().GetString("user") + dir, _ := cmd.Flags().GetString("dir") + wanted := map[string]bool{} + for _, a := range args { + wanted[a] = true + } + return withClient(func(c *client) error { + id, err := resolveUserOrSelf(c, user) + if err != nil { + return err + } + var slips []struct { + EntryID string `json:"entryId"` + EntryType string `json:"entryType"` + PaymentDate string `json:"paymentDate"` + } + if err := c.do("GET", fmt.Sprintf("/apiv2/users/%d/payslips", id), nil, &slips); err != nil { + return err + } + downloaded := 0 + for _, s := range slips { + if s.EntryType != "Payslip" || s.EntryID == "" || len(s.PaymentDate) < 7 { + continue + } + month := s.PaymentDate[:7] + if len(wanted) > 0 && !wanted[month] { + continue + } + out := filepath.Join(dir, month+".pdf") + if _, err := os.Stat(out); err == nil && len(wanted) == 0 { + continue // no-args mode only fills gaps; name a month to re-download + } + var pdf []byte + p := fmt.Sprintf("/apiv2/payroll/payruns/%s/%d/payslip-pdf/download", s.EntryID, id) + if err := c.do("GET", p, nil, &pdf); err != nil { + return err + } + if !bytes.HasPrefix(pdf, []byte("%PDF")) { + return fmt.Errorf("%s: response is not a PDF (%d bytes)", month, len(pdf)) + } + if err := writeFileAtomic(out, pdf, 0o644); err != nil { + return err + } + fmt.Println(out) + downloaded++ + } + if downloaded == 0 { + fmt.Fprintln(stderr, "nothing to download") + } + return nil + }) + }, + } + cmd.Flags().String("user", "me", "userId, email, or 'me'") + cmd.Flags().String("dir", ".", "directory to save PDFs into") return cmd }