diff --git a/cmd/invoicer/README.md b/cmd/invoicer/README.md new file mode 100644 index 0000000..c001e84 --- /dev/null +++ b/cmd/invoicer/README.md @@ -0,0 +1,55 @@ +# Invoicer CLI + +This command-line tool is used to generate invoices based on GitHub issues. + +## Configuration + +To use the Invoicer CLI, you first need to create a configuration file. This file contains all the necessary information to generate an invoice, such as creditor and debtor details, the repositories to scan for issues, and the hourly rate. + +To generate the configuration file, run the following command: + +```bash +go run . config +``` + +This will start an interactive prompt that will guide you through creating the configuration file. The generated file will be saved to the specified output path. + +### Configuration Options + +The interactive prompt will ask for the following information: + +- **Repositories**: A list of Gitea repositories to scan for issues (e.g., `owner/repo`). +- **Creditor Information**: + - Name (first & last name or company) + - Contact (if for a company: contact person) + - Address (Country, City, Postcode, Street, House Number) + - IBAN +- **Debtor Information (Optional)**: + - Name (first & last name or company) + - Contact (if for a company: contact person) + - Address (Country, City, Postcode, Street, House Number) + - Email Address +- **Duration Threshold**: The minimum duration for an issue to be billed. +- **Hourly Rate**: The price per hour in your currency. + +## Creating an Invoice + +You are able to generate invoices using the configuration file created with the `config` command. + +To create an invoice, run the following command: + +```bash +go run . create +``` + +This will generate a PDF invoice based on the information in the configuration file. The invoice will be saved to the current directory. + +### Sending an Invoice by Email + +To send the invoice by email, you can use the `--email` flag: + +```bash +go run . create --email +``` + +This will send the invoice to the debtor's email address specified in the configuration file. diff --git a/cmd/invoicer/cmd.go b/cmd/invoicer/cmd.go index 6a9a26d..383b29e 100644 --- a/cmd/invoicer/cmd.go +++ b/cmd/invoicer/cmd.go @@ -1,17 +1,39 @@ package main +import ( + "flag" + "log" + "os" + + "github.com/itzg/go-flagsfiller" +) + type cmd struct { - cmd func(any) + cmd func([]string, any) config any } func (c *cmd) Run() { - c.cmd(c.config) + if c.config != nil { + flag.Parse() + filler := flagsfiller.New() + err := filler.Fill(flag.CommandLine, c.config) + if err != nil { + log.Fatal(err) + } + c.cmd(flag.Args()[1:], c.config) + } else { + c.cmd(os.Args[2:], c.config) + } } var commands = map[string]cmd{ - "config": { - config, + "request": { + request, nil, }, + "create": { + create, + &createFlags{}, + }, } diff --git a/cmd/invoicer/create.go b/cmd/invoicer/create.go index f3b5269..9fe3d45 100644 --- a/cmd/invoicer/create.go +++ b/cmd/invoicer/create.go @@ -1,5 +1,92 @@ package main -// TODO: This file contained broken and incomplete code that prevented the project from building. -// It has been temporarily disabled to allow tests and builds to pass. -// The original logic needs to be reviewed and rewritten. \ No newline at end of file +import ( + "encoding/json" + "fmt" + "io" + "log" + "os" + "time" + + "git.schreifuchs.ch/lou-taylor/accounting/internal/email" +) + +type createFlags struct { + Email bool `flag:"email" help:"send invoice by email"` + Output string `flag:"o" help:"output file"` +} + +func create(arguments []string, c any) { + flags, ok := c.(*createFlags) + if !ok { + panic("invalid config injected") + } + req := parseRequest(arguments) + + cfg, log, invoicer, mailer := inject() + + repos, err := req.GetRepos() + if err != nil { + fmt.Printf("could not get repos: %v", err) + return + } + invoice, report, err := invoicer.Generate(req.Creditor, req.Debtor, time.Duration(req.DurationThreshold), req.HourlyRate, repos) + if err != nil { + log.Error(fmt.Sprintf("Error while creating invoice: %v", err)) + } + + // if no time has to be billed aka if bill for 0 CHF + if report.Total() <= time.Duration(0) { + log.Info("no suitable issues to be billed") + return + } + + if flags.Email { + mail := email.Mail{ + To: []string{req.MailTo}, + Subject: fmt.Sprintf("Invoice from %s", cfg.Email.From), + Attachments: []email.Attachment{{Name: "invoice.pdf", MimeType: "application/pdf", Content: invoice}}, + } + err := mailer.Send(mail) + if err != nil { + log.Error(fmt.Sprintf("Error while sending mail: %v", err)) + os.Exit(1) + } + return + } + if len(flags.Output) > 0 { + file, err := os.Create(flags.Output) + if err != nil { + log.Error(fmt.Sprintf("Error opening output file: %v", err)) + os.Exit(1) + } + defer file.Close() + + _, err = io.Copy(file, invoice) + if err != nil { + log.Error(fmt.Sprintf("Error while writing to output file: %v", err)) + os.Exit(1) + } + return + } + io.Copy(os.Stdout, invoice) +} + +func parseRequest(arguments []string) *invoiceRequest { + if len(arguments) < 1 { + fmt.Println("please specify request file") + os.Exit(1) + } + + file, err := os.Open(arguments[0]) + if err != nil { + log.Fatalf("can't open file: %s %v", arguments[0], err) + } + + defer file.Close() + req := &invoiceRequest{} + + json.NewDecoder(file).Decode(req) + + return req +} diff --git a/cmd/invoicer/main.go b/cmd/invoicer/main.go index e4b559b..332e122 100644 --- a/cmd/invoicer/main.go +++ b/cmd/invoicer/main.go @@ -1,11 +1,8 @@ package main import ( - "flag" - "log" + "fmt" "os" - - "github.com/itzg/go-flagsfiller" ) func main() { @@ -13,25 +10,11 @@ func main() { return } - var cmd *cmd - for n, c := range commands { if os.Args[1] == n { - cmd = &c + c.Run() break } } - if cmd == nil { - return - } - - if cmd.config != nil { - filler := flagsfiller.New() - err := filler.Fill(flag.CommandLine, cmd.config) - if err != nil { - log.Fatal(err) - } - } - - cmd.Run() + fmt.Println("cmd not found") } diff --git a/cmd/invoicer/config.go b/cmd/invoicer/request.go similarity index 93% rename from cmd/invoicer/config.go rename to cmd/invoicer/request.go index 9a1779b..b484ab3 100644 --- a/cmd/invoicer/config.go +++ b/cmd/invoicer/request.go @@ -36,9 +36,10 @@ func (i invoiceRequest) GetRepos() (repos []invoice.Repo, err error) { return repos, err } -func config(_ any) { - if len(os.Args) < 3 { +func request(arguments []string, _ any) { + if len(arguments) < 1 { fmt.Println("please specify output file") + return } q := invoiceRequest{} @@ -67,9 +68,9 @@ func config(_ any) { q.DurationThreshold = ask.Duration("Minimum duration for a issue to be billed", time.Duration(0), time.Duration(-1)) q.HourlyRate = ask.Float64("Price per hour in CHF", 0, -1) - file, err := os.Create(os.Args[2]) + file, err := os.Create(arguments[0]) if err != nil { - fmt.Printf("can't open file: %s %v", os.Args[2], err) + fmt.Printf("can't open file: %s %v", arguments[0], err) return } defer file.Close() diff --git a/cmd/invoicer/services.go b/cmd/invoicer/services.go new file mode 100644 index 0000000..c824247 --- /dev/null +++ b/cmd/invoicer/services.go @@ -0,0 +1,63 @@ +package main + +import ( + "log" + "log/slog" + "os" + "path" + + "code.gitea.io/sdk/gitea" + "git.schreifuchs.ch/lou-taylor/accounting/internal/config" + "git.schreifuchs.ch/lou-taylor/accounting/internal/email" + "git.schreifuchs.ch/lou-taylor/accounting/internal/pdf" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice" +) + +func getConfig() (cfg *config.Config) { + cfg, err := config.Load("config.json") + if err == nil { + return cfg + } + cfg, err = config.Load(path.Join(os.Getenv("HOME"), ".config/invoicer/config.json")) + if err == nil { + return cfg + } + cfg, err = config.Load("/etc/invoicer/config.json") + if err == nil { + return cfg + } + + log.Fatal("no cli config found") + return cfg +} + +func getServices() (invoicer *invoice.Service, mailer *email.Service) { + cfg := getConfig() + + logr := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + giteaC, err := gitea.NewClient( + cfg.Gitea.URL, + gitea.SetToken(cfg.Gitea.Token), + ) + if err != nil { + log.Fatal("could not connect to gitea: %v", err) + return invoicer, mailer + } + gotenberg, err := pdf.New(cfg.PDF.Hostname) + if err != nil { + panic(err) + } + if err != nil { + log.Fatal("could not connect to gotenberg: %v", err) + return invoicer, mailer + } + mailer, err = email.New(cfg.Email) + if err != nil { + log.Fatal("could not create mailer: %v", err) + return invoicer, mailer + } + + invoicer = invoice.New(logr, giteaC, gotenberg) + return invoicer, mailer +} diff --git a/cmd/invoicer/setup.go b/cmd/invoicer/setup.go new file mode 100644 index 0000000..b82914c --- /dev/null +++ b/cmd/invoicer/setup.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "log/slog" + "os" + "path" + + "code.gitea.io/sdk/gitea" + "git.schreifuchs.ch/lou-taylor/accounting/internal/config" + "git.schreifuchs.ch/lou-taylor/accounting/internal/email" + "git.schreifuchs.ch/lou-taylor/accounting/internal/pdf" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice" +) + +func inject() (cfg *config.Config, log *slog.Logger, invoicer *invoice.Service, mailer *email.Service) { + log = slog.New(slog.NewTextHandler(os.Stdout, nil)) + + cfgDir, err := os.UserConfigDir() + if err != nil { + log.Error("Unable to load config dir") + os.Exit(1) + } + + cfg, err = config.Load(path.Join(cfgDir, "invoicer/config.json")) + if err != nil { + log.Error(fmt.Sprintf("Unable to parse config: %v", err)) + os.Exit(1) + } + + giteaC, err := gitea.NewClient( + cfg.Gitea.URL, + gitea.SetToken(cfg.Gitea.Token), + ) + if err != nil { + log.Error(fmt.Sprintf("Unable connect to gitea: %v", err)) + os.Exit(2) + } + gotenberg, err := pdf.New(cfg.PDF.Hostname) + if err != nil { + log.Error(fmt.Sprintf("Unable connect to gotenberg: %v", err)) + os.Exit(3) + } + mailer, err = email.New(cfg.Email) + if err != nil { + log.Error(fmt.Sprintf("Unable setup mailer: %v", err)) + os.Exit(4) + } + invoicer = invoice.New(log, giteaC, gotenberg) + + return cfg, log, invoicer, mailer +} diff --git a/internal/api/httpinvoce/controller.go b/internal/api/httpinvoce/controller.go index 6e9f850..1068ab2 100644 --- a/internal/api/httpinvoce/controller.go +++ b/internal/api/httpinvoce/controller.go @@ -28,7 +28,7 @@ func (s Service) createInvoice(w http.ResponseWriter, r *http.Request) { return } - invoice, report, err := s.invoice.Generate(req.Creditor, req.Debtor, time.Duration(req.DurationThreshold), req.HourlyRate, repos) + invoice, report, err := s.invoice.Generate(req.Creditor, &req.Debtor, time.Duration(req.DurationThreshold), req.HourlyRate, repos) if err != nil { s.sendErr(w, http.StatusInternalServerError, "internal server error") return @@ -65,7 +65,7 @@ func (s Service) sendInvoice(w http.ResponseWriter, r *http.Request) { s.sendErr(w, 500, err.Error()) return } - invoice, report, err := s.invoice.Generate(req.Invoice.Creditor, req.Invoice.Debtor, time.Duration(req.Invoice.DurationThreshold), req.Invoice.HourlyRate, repos) + invoice, report, err := s.invoice.Generate(req.Invoice.Creditor, &req.Invoice.Debtor, time.Duration(req.Invoice.DurationThreshold), req.Invoice.HourlyRate, repos) if err != nil { s.sendErr(w, http.StatusInternalServerError, "error while processing invoice:", err) return diff --git a/internal/api/httpinvoce/controller_test.go b/internal/api/httpinvoce/controller_test.go index 9cb6168..131e108 100644 --- a/internal/api/httpinvoce/controller_test.go +++ b/internal/api/httpinvoce/controller_test.go @@ -22,11 +22,11 @@ import ( // Exported versions of the request structs for testing type InvoiceReq struct { - Debtor model.Entity `json:"debtor"` - Creditor model.Entity `json:"creditor"` - DurationThreshold string `json:"durationThreshold"` // Changed to string - HourlyRate float64 `json:"hourlyRate"` - Repos []string `json:"repositories"` + Debtor model.Entity `json:"debtor"` + Creditor model.Entity `json:"creditor"` + DurationThreshold string `json:"durationThreshold"` // Changed to string + HourlyRate float64 `json:"hourlyRate"` + Repos []string `json:"repositories"` } func (i InvoiceReq) GetRepos() (repos []invoice.Repo, err error) { @@ -34,14 +34,14 @@ func (i InvoiceReq) GetRepos() (repos []invoice.Repo, err error) { parts := strings.Split(repo, "/") if len(parts) != 2 { err = fmt.Errorf("cannot read body: repo with index %d does not split into 2 parts", i) - return + return repos, err } repos = append(repos, invoice.Repo{ Owner: parts[0], Repo: parts[1], }) } - return + return repos, err } type SendReq struct { @@ -65,10 +65,10 @@ func (s SendReq) ToEMail() email.Mail { // MockInvoiceService mocks the invoice.Service interface type MockInvoiceService struct { - GenerateFunc func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) + GenerateFunc func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) } -func (m *MockInvoiceService) Generate(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { +func (m *MockInvoiceService) Generate(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { if m.GenerateFunc != nil { return m.GenerateFunc(creditor, debtor, durationThreshold, hourlyRate, repos) } @@ -94,23 +94,23 @@ func TestCreateInvoice(t *testing.T) { tests := []struct { name string requestBody InvoiceReq - mockGenerate func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) + mockGenerate func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) expectedStatus int expectedBody string // For error messages or specific content }{ { name: "successful invoice creation", requestBody: InvoiceReq{ - Creditor: model.Entity{Name: "Creditor"}, - Debtor: model.Entity{Name: "Debtor"}, + Creditor: model.Entity{Name: "Creditor"}, + Debtor: model.Entity{Name: "Debtor"}, DurationThreshold: "1h", // Changed to string - HourlyRate: 100, - Repos: []string{"owner/repo1"}, + HourlyRate: 100, + Repos: []string{"owner/repo1"}, }, - mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { + mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { pdfContent := "mock PDF content" // Create a report with a positive total duration - mockReport := report.New([]issue.Issue{{Duration: time.Hour}}, model.Entity{}, model.Entity{}, 0) + mockReport := report.New([]issue.Issue{{Duration: time.Hour}}, model.Entity{}, &model.Entity{}, 0) return io.NopCloser(bytes.NewReader([]byte(pdfContent))), mockReport, nil }, expectedStatus: http.StatusOK, @@ -119,26 +119,26 @@ func TestCreateInvoice(t *testing.T) { { name: "invalid request body", requestBody: InvoiceReq{ // Malformed request body - Creditor: model.Entity{Name: "Creditor"}, - Debtor: model.Entity{Name: "Debtor"}, + Creditor: model.Entity{Name: "Creditor"}, + Debtor: model.Entity{Name: "Debtor"}, DurationThreshold: "invalid", // Changed to string - HourlyRate: 100, - Repos: []string{"owner/repo1"}, + HourlyRate: 100, + Repos: []string{"owner/repo1"}, }, - mockGenerate: nil, // Not called for invalid body + mockGenerate: nil, // Not called for invalid body expectedStatus: http.StatusBadRequest, expectedBody: "cannot read body", // Partial match for error message }, { name: "invoice generation error", requestBody: InvoiceReq{ - Creditor: model.Entity{Name: "Creditor"}, - Debtor: model.Entity{Name: "Debtor"}, + Creditor: model.Entity{Name: "Creditor"}, + Debtor: model.Entity{Name: "Debtor"}, DurationThreshold: "1h", // Changed to string - HourlyRate: 100, - Repos: []string{"owner/repo1"}, + HourlyRate: 100, + Repos: []string{"owner/repo1"}, }, - mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { + mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { return nil, nil, errors.New("failed to generate invoice") }, expectedStatus: http.StatusInternalServerError, @@ -147,15 +147,15 @@ func TestCreateInvoice(t *testing.T) { { name: "no suitable issues to be billed", requestBody: InvoiceReq{ - Creditor: model.Entity{Name: "Creditor"}, - Debtor: model.Entity{Name: "Debtor"}, + Creditor: model.Entity{Name: "Creditor"}, + Debtor: model.Entity{Name: "Debtor"}, DurationThreshold: "1h", // Changed to string - HourlyRate: 100, - Repos: []string{"owner/repo1"}, + HourlyRate: 100, + Repos: []string{"owner/repo1"}, }, - mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { + mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { // Create a report with zero total duration - mockReport := report.New([]issue.Issue{}, model.Entity{}, model.Entity{}, 0) + mockReport := report.New([]issue.Issue{}, model.Entity{}, &model.Entity{}, 0) return io.NopCloser(bytes.NewReader([]byte("mock PDF content"))), mockReport, nil }, expectedStatus: http.StatusNotFound, @@ -196,7 +196,7 @@ func TestSendInvoice(t *testing.T) { tests := []struct { name string requestBody SendReq - mockGenerate func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) + mockGenerate func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) mockSend func(mail email.Mail) error expectedStatus int expectedBody string @@ -208,16 +208,16 @@ func TestSendInvoice(t *testing.T) { Subject: "Test Invoice", Body: "Here is your invoice.", Invoice: InvoiceReq{ - Creditor: model.Entity{Name: "Creditor"}, - Debtor: model.Entity{Name: "Debtor"}, + Creditor: model.Entity{Name: "Creditor"}, + Debtor: model.Entity{Name: "Debtor"}, DurationThreshold: "1h", // Changed to string - HourlyRate: 100, - Repos: []string{"owner/repo1"}, + HourlyRate: 100, + Repos: []string{"owner/repo1"}, }, }, - mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { + mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { pdfContent := "mock PDF content for send" - mockReport := report.New([]issue.Issue{{Duration: time.Hour}}, model.Entity{}, model.Entity{}, 0) + mockReport := report.New([]issue.Issue{{Duration: time.Hour}}, model.Entity{}, &model.Entity{}, 0) return io.NopCloser(bytes.NewReader([]byte(pdfContent))), mockReport, nil }, mockSend: func(mail email.Mail) error { @@ -233,14 +233,14 @@ func TestSendInvoice(t *testing.T) { Subject: "Test Invoice", Body: "Here is your invoice.", Invoice: InvoiceReq{ - Creditor: model.Entity{Name: "Creditor"}, - Debtor: model.Entity{Name: "Debtor"}, + Creditor: model.Entity{Name: "Creditor"}, + Debtor: model.Entity{Name: "Debtor"}, DurationThreshold: "1h", // Changed to string - HourlyRate: 100, - Repos: []string{"owner/repo1"}, + HourlyRate: 100, + Repos: []string{"owner/repo1"}, }, }, - mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { + mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { return nil, nil, errors.New("failed to generate invoice for send") }, mockSend: nil, // Not called @@ -254,16 +254,16 @@ func TestSendInvoice(t *testing.T) { Subject: "Test Invoice", Body: "Here is your invoice.", Invoice: InvoiceReq{ - Creditor: model.Entity{Name: "Creditor"}, - Debtor: model.Entity{Name: "Debtor"}, + Creditor: model.Entity{Name: "Creditor"}, + Debtor: model.Entity{Name: "Debtor"}, DurationThreshold: "1h", // Changed to string - HourlyRate: 100, - Repos: []string{"owner/repo1"}, + HourlyRate: 100, + Repos: []string{"owner/repo1"}, }, }, - mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { + mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { pdfContent := "mock PDF content for send" - mockReport := report.New([]issue.Issue{{Duration: time.Hour}}, model.Entity{}, model.Entity{}, 0) + mockReport := report.New([]issue.Issue{{Duration: time.Hour}}, model.Entity{}, &model.Entity{}, 0) return io.NopCloser(bytes.NewReader([]byte(pdfContent))), mockReport, nil }, mockSend: func(mail email.Mail) error { @@ -279,15 +279,15 @@ func TestSendInvoice(t *testing.T) { Subject: "Test Invoice", Body: "Here is your invoice.", Invoice: InvoiceReq{ - Creditor: model.Entity{Name: "Creditor"}, - Debtor: model.Entity{Name: "Debtor"}, + Creditor: model.Entity{Name: "Creditor"}, + Debtor: model.Entity{Name: "Debtor"}, DurationThreshold: "1h", // Changed to string - HourlyRate: 100, - Repos: []string{"owner/repo1"}, + HourlyRate: 100, + Repos: []string{"owner/repo1"}, }, }, - mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { - mockReport := report.New([]issue.Issue{}, model.Entity{}, model.Entity{}, 0) + mockGenerate: func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) { + mockReport := report.New([]issue.Issue{}, model.Entity{}, &model.Entity{}, 0) return io.NopCloser(bytes.NewReader([]byte("mock PDF content"))), mockReport, nil }, expectedStatus: http.StatusNotFound, diff --git a/internal/api/httpinvoce/resource.go b/internal/api/httpinvoce/resource.go index 875b87a..0b39b3d 100644 --- a/internal/api/httpinvoce/resource.go +++ b/internal/api/httpinvoce/resource.go @@ -22,7 +22,7 @@ func New(log *slog.Logger, invoice invoicer, mail mailer) *Service { } type invoicer interface { - Generate(creditor, deptor model.Entity, mindur time.Duration, rate float64, repos []invoice.Repo) (document io.ReadCloser, report *report.Report, err error) + Generate(creditor model.Entity, deptor *model.Entity, mindur time.Duration, rate float64, repos []invoice.Repo) (document io.ReadCloser, report *report.Report, err error) } type mailer interface { diff --git a/invoice.pdf b/invoice.pdf new file mode 100644 index 0000000..e03954d Binary files /dev/null and b/invoice.pdf differ diff --git a/invoicer b/invoicer new file mode 100755 index 0000000..1a516ff Binary files /dev/null and b/invoicer differ diff --git a/pkg/invoice/invoice.go b/pkg/invoice/invoice.go index 126de81..ef73df1 100644 --- a/pkg/invoice/invoice.go +++ b/pkg/invoice/invoice.go @@ -10,7 +10,7 @@ import ( "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/report" ) -func (s *Service) Generate(creditor, deptor model.Entity, mindur time.Duration, rate float64, repos []Repo) (document io.ReadCloser, r *report.Report, err error) { +func (s *Service) Generate(creditor model.Entity, deptor *model.Entity, mindur time.Duration, rate float64, repos []Repo) (document io.ReadCloser, r *report.Report, err error) { var is []*gitea.Issue for _, repo := range repos { iss, _, err := s.gitea.ListRepoIssues( diff --git a/pkg/invoice/report/resource.go b/pkg/invoice/report/resource.go index 06f8e09..c918351 100644 --- a/pkg/invoice/report/resource.go +++ b/pkg/invoice/report/resource.go @@ -14,10 +14,10 @@ type Report struct { Invoice qrbill.Invoice Rate float64 Company model.Entity - Client model.Entity + Client *model.Entity } -func New(issues []issue.Issue, company, client model.Entity, rate float64) *Report { +func New(issues []issue.Issue, company model.Entity, client *model.Entity, rate float64) *Report { r := &Report{ Date: time.Now(), Issues: issues, @@ -25,7 +25,7 @@ func New(issues []issue.Issue, company, client model.Entity, rate float64) *Repo Company: company, Client: client, } - r.Invoice = qrbill.New(r.applyRate(r.Total()), r.Company, &r.Client) + r.Invoice = qrbill.New(r.applyRate(r.Total()), r.Company, r.Client) return r }