From cfbb475a4291e9bb4325990e2a111932693944d2 Mon Sep 17 00:00:00 2001 From: schreifuchs Date: Tue, 4 Nov 2025 20:16:44 +0100 Subject: [PATCH] feat: more options --- cmd/invoicer/create.go | 6 +++++- internal/api/httpinvoce/controller.go | 11 +++++++++-- internal/api/httpinvoce/controller_test.go | 20 +++++++++++++++----- internal/api/httpinvoce/resource.go | 3 +-- pkg/invoice/invoice.go | 21 +++++++++------------ pkg/invoice/resource.go | 19 +++++++++++++++++++ 6 files changed, 58 insertions(+), 22 deletions(-) diff --git a/cmd/invoicer/create.go b/cmd/invoicer/create.go index 9fe3d45..6e2571c 100644 --- a/cmd/invoicer/create.go +++ b/cmd/invoicer/create.go @@ -9,6 +9,7 @@ import ( "time" "git.schreifuchs.ch/lou-taylor/accounting/internal/email" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice" ) type createFlags struct { @@ -30,7 +31,10 @@ func create(arguments []string, c any) { 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) + + opts := invoice.DefaultOptions + opts.Mindur = time.Duration(req.DurationThreshold) + invoice, report, err := invoicer.Generate(req.Creditor, req.Debtor, req.HourlyRate, repos, &opts) if err != nil { log.Error(fmt.Sprintf("Error while creating invoice: %v", err)) } diff --git a/internal/api/httpinvoce/controller.go b/internal/api/httpinvoce/controller.go index 1068ab2..302f149 100644 --- a/internal/api/httpinvoce/controller.go +++ b/internal/api/httpinvoce/controller.go @@ -9,6 +9,7 @@ import ( "time" "git.schreifuchs.ch/lou-taylor/accounting/internal/email" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice" ) const bufSize = 1024 * 1024 // 1Mib @@ -28,7 +29,9 @@ 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) + opts := invoice.DefaultOptions + opts.Mindur = time.Duration(req.DurationThreshold) + invoice, report, err := s.invoice.Generate(req.Creditor, &req.Debtor, req.HourlyRate, repos, &opts) if err != nil { s.sendErr(w, http.StatusInternalServerError, "internal server error") return @@ -65,11 +68,15 @@ 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) + + opts := invoice.DefaultOptions + opts.Mindur = time.Duration(req.Invoice.DurationThreshold) + invoice, report, err := s.invoice.Generate(req.Invoice.Creditor, &req.Invoice.Debtor, req.Invoice.HourlyRate, repos, &opts) if err != nil { s.sendErr(w, http.StatusInternalServerError, "error while processing invoice:", err) return } + // if no time has to be billed aka if bill for 0 CHF if report.Total() <= time.Duration(0) { s.sendErr(w, http.StatusNotFound, "no suitable issues to be billed") diff --git a/internal/api/httpinvoce/controller_test.go b/internal/api/httpinvoce/controller_test.go index 131e108..7335ec7 100644 --- a/internal/api/httpinvoce/controller_test.go +++ b/internal/api/httpinvoce/controller_test.go @@ -65,12 +65,12 @@ func (s SendReq) ToEMail() email.Mail { // MockInvoiceService mocks the invoice.Service interface type MockInvoiceService struct { - GenerateFunc func(creditor model.Entity, debtor *model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) + GenerateFunc func(creditor model.Entity, debtor *model.Entity, hourlyRate float64, repos []invoice.Repo, opts *invoice.Options) (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) { +func (m *MockInvoiceService) Generate(creditor model.Entity, debtor *model.Entity, hourlyRate float64, repos []invoice.Repo, opts *invoice.Options) (io.ReadCloser, *report.Report, error) { if m.GenerateFunc != nil { - return m.GenerateFunc(creditor, debtor, durationThreshold, hourlyRate, repos) + return m.GenerateFunc(creditor, debtor, hourlyRate, repos, opts) } return nil, &report.Report{}, nil } @@ -166,7 +166,12 @@ func TestCreateInvoice(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockInvoiceService := &MockInvoiceService{ - GenerateFunc: tt.mockGenerate, + GenerateFunc: func(creditor model.Entity, debtor *model.Entity, hourlyRate float64, repos []invoice.Repo, opts *invoice.Options) (io.ReadCloser, *report.Report, error) { + if opts == nil { + opts = &invoice.DefaultOptions + } + return tt.mockGenerate(creditor, debtor, opts.Mindur, hourlyRate, repos) + }, } service := Service{invoice: mockInvoiceService, log: dummyLogger} // Pass the dummy logger @@ -298,7 +303,12 @@ func TestSendInvoice(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockInvoiceService := &MockInvoiceService{ - GenerateFunc: tt.mockGenerate, + GenerateFunc: func(creditor model.Entity, debtor *model.Entity, hourlyRate float64, repos []invoice.Repo, opts *invoice.Options) (io.ReadCloser, *report.Report, error) { + if opts == nil { + opts = &invoice.DefaultOptions + } + return tt.mockGenerate(creditor, debtor, opts.Mindur, hourlyRate, repos) + }, } mockEmailService := &MockEmailService{ SendFunc: tt.mockSend, diff --git a/internal/api/httpinvoce/resource.go b/internal/api/httpinvoce/resource.go index 0b39b3d..0d8c69b 100644 --- a/internal/api/httpinvoce/resource.go +++ b/internal/api/httpinvoce/resource.go @@ -3,7 +3,6 @@ package httpinvoce import ( "io" "log/slog" - "time" "git.schreifuchs.ch/lou-taylor/accounting/internal/email" "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice" @@ -22,7 +21,7 @@ func New(log *slog.Logger, invoice invoicer, mail mailer) *Service { } type invoicer interface { - Generate(creditor model.Entity, 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, rate float64, repos []invoice.Repo, opts *invoice.Options) (document io.ReadCloser, report *report.Report, err error) } type mailer interface { diff --git a/pkg/invoice/invoice.go b/pkg/invoice/invoice.go index ef73df1..65e34d3 100644 --- a/pkg/invoice/invoice.go +++ b/pkg/invoice/invoice.go @@ -2,7 +2,6 @@ package invoice import ( "io" - "time" "code.gitea.io/sdk/gitea" "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/issue" @@ -10,7 +9,10 @@ import ( "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/report" ) -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) { +func (s *Service) Generate(creditor model.Entity, deptor *model.Entity, rate float64, repos []Repo, config *Options) (document io.ReadCloser, r *report.Report, err error) { + if config == nil { + config = &DefaultOptions + } var is []*gitea.Issue for _, repo := range repos { iss, _, err := s.gitea.ListRepoIssues( @@ -18,9 +20,9 @@ func (s *Service) Generate(creditor model.Entity, deptor *model.Entity, mindur t repo.Repo, gitea.ListIssueOption{ ListOptions: gitea.ListOptions{Page: 0, PageSize: 99999}, - Since: time.Now().AddDate(0, -1, 0), - Before: time.Now(), - State: gitea.StateClosed, + Since: config.Since, + Before: config.Before, + State: config.IssueState, }, ) if err != nil { @@ -30,13 +32,8 @@ func (s *Service) Generate(creditor model.Entity, deptor *model.Entity, mindur t is = append(is, iss...) } - is = filter( - is, - func(i *gitea.Issue) bool { - return i.Closed != nil && i.Closed.After(time.Now().AddDate(0, -1, 0)) - }, - ) - issues := issue.FromGiteas(is, mindur) + is = filter(is, config.IssueFilter) + issues := issue.FromGiteas(is, config.Mindur) r = report.New( issues, creditor, diff --git a/pkg/invoice/resource.go b/pkg/invoice/resource.go index c4098e1..1073067 100644 --- a/pkg/invoice/resource.go +++ b/pkg/invoice/resource.go @@ -3,10 +3,29 @@ package invoice import ( "io" "log/slog" + "time" "code.gitea.io/sdk/gitea" ) +var DefaultOptions = Options{ + Mindur: time.Minute * 15, + Since: time.Now().AddDate(0, -1, 0), + Before: time.Now(), + IssueState: gitea.StateClosed, + IssueFilter: func(i *gitea.Issue) bool { + return i.Closed != nil && i.Closed.After(time.Now().AddDate(0, -1, 0)) + }, +} + +type Options struct { + Mindur time.Duration + Since time.Time + Before time.Time + IssueState gitea.StateType + IssueFilter func(i *gitea.Issue) bool +} + type Repo struct { Owner string `json:"owner"` Repo string `json:"repo"`