From 788571162dd0c956244fee9ab0788405f32fcb56 Mon Sep 17 00:00:00 2001 From: schreifuchs Date: Tue, 26 Aug 2025 20:30:48 +0200 Subject: [PATCH] feat(invoice): add send route --- cmd/invoiceapi/main.go | 26 ++++ config.go | 36 ------ internal/api/api.go | 23 ++++ internal/api/httpinvoce/controller.go | 99 +++++++++++++++ internal/api/httpinvoce/model.go | 69 +++++++++++ internal/api/httpinvoce/resource.go | 29 +++++ internal/api/httpinvoce/routes.go | 9 ++ internal/api/routes.go | 35 ++++++ {mailer => internal/email}/model.go | 6 +- {mailer => internal/email}/resource.go | 2 +- {mailer => internal/email}/send.go | 9 +- main.go | 114 +++--------------- model/model.go | 15 --- pdf/resource.go | 8 +- pkg/invoice/invoice.go | 65 ++++++++++ {issue => pkg/invoice/issue}/issue.go | 0 {issue => pkg/invoice/issue}/resource.go | 0 pkg/invoice/model/model.go | 15 +++ .../invoice/report}/assets/style.css | 0 .../invoice/report}/assets/template.html | 0 {report => pkg/invoice/report}/html.go | 0 {report => pkg/invoice/report}/html_test.go | 0 {report => pkg/invoice/report}/markdown.go | 0 .../invoice/report}/markdown_test.go | 0 .../report/qrbill}/assets/ch-cross.svg | 0 .../report/qrbill}/assets/scissors.svg | 0 .../invoice/report/qrbill}/assets/style.css | 0 .../report/qrbill}/assets/template.html | 0 .../invoice/report/qrbill}/html.go | 2 +- .../invoice/report/qrbill}/invoice.go | 4 +- .../invoice/report/qrbill}/reference.go | 2 +- {report => pkg/invoice/report}/resource.go | 10 +- {report => pkg/invoice/report}/template.go | 11 +- pkg/invoice/resource.go | 34 ++++++ types.go | 21 ---- 35 files changed, 451 insertions(+), 193 deletions(-) create mode 100644 cmd/invoiceapi/main.go delete mode 100644 config.go create mode 100644 internal/api/api.go create mode 100644 internal/api/httpinvoce/controller.go create mode 100644 internal/api/httpinvoce/model.go create mode 100644 internal/api/httpinvoce/resource.go create mode 100644 internal/api/httpinvoce/routes.go create mode 100644 internal/api/routes.go rename {mailer => internal/email}/model.go (69%) rename {mailer => internal/email}/resource.go (98%) rename {mailer => internal/email}/send.go (81%) delete mode 100644 model/model.go create mode 100644 pkg/invoice/invoice.go rename {issue => pkg/invoice/issue}/issue.go (100%) rename {issue => pkg/invoice/issue}/resource.go (100%) create mode 100644 pkg/invoice/model/model.go rename {report => pkg/invoice/report}/assets/style.css (100%) rename {report => pkg/invoice/report}/assets/template.html (100%) rename {report => pkg/invoice/report}/html.go (100%) rename {report => pkg/invoice/report}/html_test.go (100%) rename {report => pkg/invoice/report}/markdown.go (100%) rename {report => pkg/invoice/report}/markdown_test.go (100%) rename {report/invoice => pkg/invoice/report/qrbill}/assets/ch-cross.svg (100%) rename {report/invoice => pkg/invoice/report/qrbill}/assets/scissors.svg (100%) rename {report/invoice => pkg/invoice/report/qrbill}/assets/style.css (100%) rename {report/invoice => pkg/invoice/report/qrbill}/assets/template.html (100%) rename {report/invoice => pkg/invoice/report/qrbill}/html.go (97%) rename {report/invoice => pkg/invoice/report/qrbill}/invoice.go (98%) rename {report/invoice => pkg/invoice/report/qrbill}/reference.go (99%) rename {report => pkg/invoice/report}/resource.go (56%) rename {report => pkg/invoice/report}/template.go (88%) create mode 100644 pkg/invoice/resource.go delete mode 100644 types.go diff --git a/cmd/invoiceapi/main.go b/cmd/invoiceapi/main.go new file mode 100644 index 0000000..cb8db49 --- /dev/null +++ b/cmd/invoiceapi/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "log/slog" + "os" + + "git.schreifuchs.ch/lou-taylor/accounting/internal/api" +) + +func main() { + // var cfg invoice.Config + // file, err := os.Open("config.json") + // if err != nil { + // panic(err) + // } + // defer file.Close() + // decoder := json.NewDecoder(file) + // err = decoder.Decode(&cfg) + // if err != nil { + // panic(err) + // } + // + // invoice.Generate(cfg) + log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + api.Start(log, ":8080") +} diff --git a/config.go b/config.go deleted file mode 100644 index dd0dabc..0000000 --- a/config.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "encoding/json" - "os" - - "git.schreifuchs.ch/lou-taylor/accounting/mailer" - "git.schreifuchs.ch/lou-taylor/accounting/model" -) - -type Config struct { - GiteaURL string `json:"gitea_url"` - GiteaToken string `json:"gitea_token"` - Repos []Repo `json:"repos"` - MinDuration Duration `json:"min_duration"` - Hourly float64 `json:"hourly"` - FromEntity model.Entity `json:"from_entity"` - ToEntity model.Entity `json:"to_entity"` - PdfGeneratorURL string `json:"pdf_generator_url"` - Mailer mailer.Config `json:"mailer"` - Mail mailer.Mail `json:"mail"` - MailBcc []string `json:"mail_bcc"` -} - -func LoadConfig(path string) (Config, error) { - var cfg Config - file, err := os.Open(path) - if err != nil { - return cfg, err - } - defer file.Close() - decoder := json.NewDecoder(file) - err = decoder.Decode(&cfg) - return cfg, err -} - diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000..293650d --- /dev/null +++ b/internal/api/api.go @@ -0,0 +1,23 @@ +package api + +import ( + "fmt" + "log/slog" + "net/http" + "time" +) + +func Start(log *slog.Logger, address string) error { + mux := http.NewServeMux() + RegisterRoutes(log, mux) + + s := &http.Server{ + Addr: address, + Handler: mux, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + log.Info(fmt.Sprintf("Start API on %s", address)) + return s.ListenAndServe() +} diff --git a/internal/api/httpinvoce/controller.go b/internal/api/httpinvoce/controller.go new file mode 100644 index 0000000..ce72b98 --- /dev/null +++ b/internal/api/httpinvoce/controller.go @@ -0,0 +1,99 @@ +package httpinvoce + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" + + "git.schreifuchs.ch/lou-taylor/accounting/internal/email" +) + +const bufSize = 1024 * 1024 // 1Mib + +func (s Service) createInvoice(w http.ResponseWriter, r *http.Request) { + var req invoiceReq + + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + s.sendErrf(w, http.StatusBadRequest, "cannot read body: %v", err) + return + } + + repos, err := req.GetRepos() + if err != nil { + s.sendErr(w, 500, err.Error()) + return + } + + invoice, err := s.invoice.Generate(req.Creditor, req.Debtor, req.DurationThreshold, req.HourlyRate, repos) + if err != nil { + log.Println("error while processing invoice:", err) + fmt.Fprint(w, "internal server error") + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set("Content-type", "application/pdf") + w.Header().Set( + "Content-Disposition", + fmt.Sprintf("attachment; filename=\"%s\"", invoiceName()), + ) + + _, err = io.Copy(w, invoice) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } +} + +func (s Service) sendInvoice(w http.ResponseWriter, r *http.Request) { + var req sendReq + + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + s.sendErrf(w, http.StatusBadRequest, "cannot read body: %v", err) + return + } + + repos, err := req.Invoice.GetRepos() + if err != nil { + s.sendErr(w, 500, err.Error()) + return + } + invoice, err := s.invoice.Generate(req.Invoice.Creditor, req.Invoice.Debtor, req.Invoice.DurationThreshold, req.Invoice.HourlyRate, repos) + if err != nil { + s.sendErr(w, http.StatusInternalServerError, "error while processing invoice:", err) + return + } + + invoiceData, err := io.ReadAll(invoice) + if err != nil { + s.sendErrf(w, http.StatusInternalServerError, "error while creating pdf: %v", err) + return + } + + mail := req.ToEMail() + mail.Attachments = append(mail.Attachments, email.Attachment{ + Name: invoiceName(), + MimeType: "application/pdf", + Content: bytes.NewReader(invoiceData), + }) + + err = s.mail.Send(mail) + if err != nil { + s.sendErrf(w, http.StatusInternalServerError, "error while sending mail: %v", err) + return + } + + _, err = w.Write(invoiceData) + if err != nil { + s.sendErr(w, http.StatusInternalServerError, "") + } +} + +func invoiceName() string { + return fmt.Sprintf("%s_invoice.pdf", time.Now().Format("20060102")) +} diff --git a/internal/api/httpinvoce/model.go b/internal/api/httpinvoce/model.go new file mode 100644 index 0000000..fa12767 --- /dev/null +++ b/internal/api/httpinvoce/model.go @@ -0,0 +1,69 @@ +package httpinvoce + +import ( + "fmt" + "log/slog" + "net/http" + "strings" + "time" + + "git.schreifuchs.ch/lou-taylor/accounting/internal/email" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model" +) + +type invoiceReq struct { + Debtor model.Entity `json:"debtor"` + Creditor model.Entity `json:"creditor"` + DurationThreshold time.Duration `json:"durationThreshold"` + HourlyRate float64 `json:"hourlyRate"` + Repos []string `json:"repositories"` +} + +func (i invoiceReq) GetRepos() (repos []invoice.Repo, err error) { + for i, repo := range i.Repos { + 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 + } + repos = append(repos, invoice.Repo{ + Owner: parts[0], + Repo: parts[1], + }) + } + return +} + +type sendReq struct { + To []string `json:"to"` + Cc []string `json:"cc"` + Bcc []string `json:"bcc"` + Subject string `json:"subjec"` + Body string `json:"body"` + Invoice invoiceReq `json:"invoice"` +} + +func (s sendReq) ToEMail() email.Mail { + return email.Mail{ + To: s.To, + Cc: s.Cc, + Bcc: s.Bcc, + Subject: s.Subject, + Body: s.Body, + } +} + +func (s *Service) sendErrf(w http.ResponseWriter, statusCode int, format string, a ...any) { + msg := fmt.Sprintf(format, a...) + s.log.Error(msg, slog.Any("statusCode", statusCode)) + w.Write([]byte(msg)) + w.WriteHeader(statusCode) +} + +func (s *Service) sendErr(w http.ResponseWriter, statusCode int, a ...any) { + msg := fmt.Sprint(a...) + s.log.Error(msg, slog.Any("statusCode", statusCode)) + w.Write([]byte(msg)) + w.WriteHeader(statusCode) +} diff --git a/internal/api/httpinvoce/resource.go b/internal/api/httpinvoce/resource.go new file mode 100644 index 0000000..f8a0e13 --- /dev/null +++ b/internal/api/httpinvoce/resource.go @@ -0,0 +1,29 @@ +package httpinvoce + +import ( + "io" + "log/slog" + "time" + + "git.schreifuchs.ch/lou-taylor/accounting/internal/email" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model" +) + +type Service struct { + log *slog.Logger + invoice invoicer + mail mailer +} + +func New(log *slog.Logger, invoice invoicer, mail mailer) *Service { + return &Service{log: log, invoice: invoice, mail: mail} +} + +type invoicer interface { + Generate(creditor, deptor model.Entity, mindur time.Duration, rate float64, repos []invoice.Repo) (document io.ReadCloser, err error) +} + +type mailer interface { + Send(m email.Mail) (err error) +} diff --git a/internal/api/httpinvoce/routes.go b/internal/api/httpinvoce/routes.go new file mode 100644 index 0000000..9bfd3bf --- /dev/null +++ b/internal/api/httpinvoce/routes.go @@ -0,0 +1,9 @@ +package httpinvoce + +import ( + "net/http" +) + +func (s Service) RegisterRoutes(mux *http.ServeMux) { + mux.HandleFunc("POST /invoice", s.createInvoice) +} diff --git a/internal/api/routes.go b/internal/api/routes.go new file mode 100644 index 0000000..70dcaf9 --- /dev/null +++ b/internal/api/routes.go @@ -0,0 +1,35 @@ +package api + +import ( + "log/slog" + "net/http" + + "code.gitea.io/sdk/gitea" + "git.schreifuchs.ch/lou-taylor/accounting/internal/api/httpinvoce" + "git.schreifuchs.ch/lou-taylor/accounting/internal/email" + "git.schreifuchs.ch/lou-taylor/accounting/pdf" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice" +) + +func RegisterRoutes(log *slog.Logger, mux *http.ServeMux) error { + gotenberg, err := pdf.New("http://localhost:3030") + if err != nil { + panic(err) + } + giteaC, err := gitea.NewClient( + "https://git.schreifuchs.ch", + gitea.SetToken("6a8ea8f9de039b0950c634bfea40c6f97f94b06b"), + ) + if err != nil { + panic(err) + } + + invoicer := invoice.New(log, giteaC, gotenberg) + mailer, err := email.New(email.Config{}) + if err != nil { + return err + } + + httpinvoce.New(log, invoicer, mailer).RegisterRoutes(mux) + return nil +} diff --git a/mailer/model.go b/internal/email/model.go similarity index 69% rename from mailer/model.go rename to internal/email/model.go index 7b54898..8a4f1a8 100644 --- a/mailer/model.go +++ b/internal/email/model.go @@ -1,9 +1,11 @@ -package mailer +package email import "io" type Mail struct { - TO string + To []string + Cc []string + Bcc []string Subject string Body string Attachments []Attachment diff --git a/mailer/resource.go b/internal/email/resource.go similarity index 98% rename from mailer/resource.go rename to internal/email/resource.go index 5b44583..87a5f27 100644 --- a/mailer/resource.go +++ b/internal/email/resource.go @@ -1,4 +1,4 @@ -package mailer +package email import ( "crypto/tls" diff --git a/mailer/send.go b/internal/email/send.go similarity index 81% rename from mailer/send.go rename to internal/email/send.go index 246e20b..21e028d 100644 --- a/mailer/send.go +++ b/internal/email/send.go @@ -1,4 +1,4 @@ -package mailer +package email import ( "crypto/tls" @@ -7,10 +7,11 @@ import ( "github.com/jordan-wright/email" ) -func (s Service) Send(m Mail, bcc ...string) (err error) { +func (s Service) Send(m Mail) (err error) { e := email.NewEmail() - e.To = []string{m.TO} - e.Bcc = bcc + e.To = m.To + e.Cc = m.Cc + e.Bcc = m.Bcc e.From = s.from e.Subject = m.Subject e.Text = []byte(m.Body) diff --git a/main.go b/main.go index 6745cb9..69d6657 100644 --- a/main.go +++ b/main.go @@ -1,106 +1,22 @@ package main import ( - "time" - - "code.gitea.io/sdk/gitea" - "git.schreifuchs.ch/lou-taylor/accounting/issue" - "git.schreifuchs.ch/lou-taylor/accounting/mailer" - "git.schreifuchs.ch/lou-taylor/accounting/pdf" - "git.schreifuchs.ch/lou-taylor/accounting/report" + "git.schreifuchs.ch/lou-taylor/accounting/internal/api" ) -type Repo struct { - Owner string `json:"owner"` - Repo string `json:"repo"` -} - func main() { - cfg, err := LoadConfig("config.json") - if err != nil { - panic(err) - } - - client, err := gitea.NewClient( - cfg.GiteaURL, - gitea.SetToken(cfg.GiteaToken), - ) - if err != nil { - panic(err) - } - - var is []*gitea.Issue - for _, repo := range cfg.Repos { - iss, _, err := client.ListRepoIssues( - repo.Owner, - repo.Repo, - gitea.ListIssueOption{ - ListOptions: gitea.ListOptions{Page: 0, PageSize: 99999}, - Since: time.Now().AddDate(0, -1, 0), - Before: time.Now(), - State: gitea.StateClosed, - }, - ) - if err != nil { - panic(err) - } - - 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, time.Duration(cfg.MinDuration)) - r := report.New( - issues, - cfg.FromEntity, - cfg.ToEntity, - cfg.Hourly, - ) - html := r.ToHTML() - - pdfs, err := pdf.New(cfg.PdfGeneratorURL) - if err != nil { - panic(err) - } - - document, err := pdfs.HtmlToPdf(html) - if err != nil { - panic(err) - } - - mlr, err := mailer.New(cfg.Mailer) - if err != nil { - panic(err) - } - - mail := cfg.Mail - mail.Attachments = []mailer.Attachment{ - { - Name: "invoice.pdf", - MimeType: "pdf", - Content: document, - }, - } - - err = mlr.Send(mail) - if err != nil { - panic(err) - } -} - -func Filter[T any](slice []T, ok func(T) bool) []T { - out := make([]T, 0, len(slice)) - - for _, item := range slice { - if ok(item) { - out = append(out, item) - } - } - - return out + // var cfg invoice.Config + // file, err := os.Open("config.json") + // if err != nil { + // panic(err) + // } + // defer file.Close() + // decoder := json.NewDecoder(file) + // err = decoder.Decode(&cfg) + // if err != nil { + // panic(err) + // } + // + // invoice.Generate(cfg) + api.Start(":8080") } diff --git a/model/model.go b/model/model.go deleted file mode 100644 index 160f9b6..0000000 --- a/model/model.go +++ /dev/null @@ -1,15 +0,0 @@ -package model - -type Entity struct { - Name string - Address Address - Contact string - IBAN string -} -type Address struct { - Street string - Number string - ZIPCode string - Place string - Country string -} diff --git a/pdf/resource.go b/pdf/resource.go index 3240ccd..bf84526 100644 --- a/pdf/resource.go +++ b/pdf/resource.go @@ -13,12 +13,13 @@ type Service struct { gotenberg *gotenberg.Client } -func New(hostname string) (service Service, err error) { +func New(hostname string) (service *Service, err error) { + service = &Service{} service.gotenberg, err = gotenberg.NewClient(hostname, http.DefaultClient) return } -func (s Service) HtmlToPdf(html string) (pdf io.ReadCloser, err error) { +func (s *Service) HtmlToPdf(html string) (pdf io.ReadCloser, err error) { index, err := document.FromString("index.html", html) if err != nil { return @@ -32,6 +33,9 @@ func (s Service) HtmlToPdf(html string) (pdf io.ReadCloser, err error) { req.EmulatePrintMediaType() resp, err := s.gotenberg.Send(context.Background(), req) + if err != nil { + return + } pdf = resp.Body return diff --git a/pkg/invoice/invoice.go b/pkg/invoice/invoice.go new file mode 100644 index 0000000..46aab6a --- /dev/null +++ b/pkg/invoice/invoice.go @@ -0,0 +1,65 @@ +package invoice + +import ( + "io" + "time" + + "code.gitea.io/sdk/gitea" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/issue" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model" + "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, err error) { + var is []*gitea.Issue + for _, repo := range repos { + iss, _, err := s.gitea.ListRepoIssues( + repo.Owner, + repo.Repo, + gitea.ListIssueOption{ + ListOptions: gitea.ListOptions{Page: 0, PageSize: 99999}, + Since: time.Now().AddDate(0, -1, 0), + Before: time.Now(), + State: gitea.StateClosed, + }, + ) + if err != nil { + return nil, err + } + + 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) + r := report.New( + issues, + creditor, + deptor, + rate, + ) + html, err := r.ToHTML() + if err != nil { + return + } + + document, err = s.pdf.HtmlToPdf(html) + return +} + +func filter[T any](slice []T, ok func(T) bool) []T { + out := make([]T, 0, len(slice)) + + for _, item := range slice { + if ok(item) { + out = append(out, item) + } + } + + return out +} diff --git a/issue/issue.go b/pkg/invoice/issue/issue.go similarity index 100% rename from issue/issue.go rename to pkg/invoice/issue/issue.go diff --git a/issue/resource.go b/pkg/invoice/issue/resource.go similarity index 100% rename from issue/resource.go rename to pkg/invoice/issue/resource.go diff --git a/pkg/invoice/model/model.go b/pkg/invoice/model/model.go new file mode 100644 index 0000000..c9c48f4 --- /dev/null +++ b/pkg/invoice/model/model.go @@ -0,0 +1,15 @@ +package model + +type Entity struct { + Name string `json:"name"` + Address Address `json:"Address"` + Contact string `json:"contact"` + IBAN string `json:"iban,omitempty"` +} +type Address struct { + Street string `json:"street"` + Number string `json:"number"` + ZIPCode string `json:"zipCode"` + Place string `json:"place"` + Country string `json:"country"` +} diff --git a/report/assets/style.css b/pkg/invoice/report/assets/style.css similarity index 100% rename from report/assets/style.css rename to pkg/invoice/report/assets/style.css diff --git a/report/assets/template.html b/pkg/invoice/report/assets/template.html similarity index 100% rename from report/assets/template.html rename to pkg/invoice/report/assets/template.html diff --git a/report/html.go b/pkg/invoice/report/html.go similarity index 100% rename from report/html.go rename to pkg/invoice/report/html.go diff --git a/report/html_test.go b/pkg/invoice/report/html_test.go similarity index 100% rename from report/html_test.go rename to pkg/invoice/report/html_test.go diff --git a/report/markdown.go b/pkg/invoice/report/markdown.go similarity index 100% rename from report/markdown.go rename to pkg/invoice/report/markdown.go diff --git a/report/markdown_test.go b/pkg/invoice/report/markdown_test.go similarity index 100% rename from report/markdown_test.go rename to pkg/invoice/report/markdown_test.go diff --git a/report/invoice/assets/ch-cross.svg b/pkg/invoice/report/qrbill/assets/ch-cross.svg similarity index 100% rename from report/invoice/assets/ch-cross.svg rename to pkg/invoice/report/qrbill/assets/ch-cross.svg diff --git a/report/invoice/assets/scissors.svg b/pkg/invoice/report/qrbill/assets/scissors.svg similarity index 100% rename from report/invoice/assets/scissors.svg rename to pkg/invoice/report/qrbill/assets/scissors.svg diff --git a/report/invoice/assets/style.css b/pkg/invoice/report/qrbill/assets/style.css similarity index 100% rename from report/invoice/assets/style.css rename to pkg/invoice/report/qrbill/assets/style.css diff --git a/report/invoice/assets/template.html b/pkg/invoice/report/qrbill/assets/template.html similarity index 100% rename from report/invoice/assets/template.html rename to pkg/invoice/report/qrbill/assets/template.html diff --git a/report/invoice/html.go b/pkg/invoice/report/qrbill/html.go similarity index 97% rename from report/invoice/html.go rename to pkg/invoice/report/qrbill/html.go index 5e28dac..ea84743 100644 --- a/report/invoice/html.go +++ b/pkg/invoice/report/qrbill/html.go @@ -1,4 +1,4 @@ -package invoice +package qrbill import ( "bytes" diff --git a/report/invoice/invoice.go b/pkg/invoice/report/qrbill/invoice.go similarity index 98% rename from report/invoice/invoice.go rename to pkg/invoice/report/qrbill/invoice.go index c1790ea..d3e2ee5 100644 --- a/report/invoice/invoice.go +++ b/pkg/invoice/report/qrbill/invoice.go @@ -1,4 +1,4 @@ -package invoice +package qrbill import ( "bytes" @@ -7,7 +7,7 @@ import ( "html/template" "strings" - "git.schreifuchs.ch/lou-taylor/accounting/model" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model" "github.com/skip2/go-qrcode" ) diff --git a/report/invoice/reference.go b/pkg/invoice/report/qrbill/reference.go similarity index 99% rename from report/invoice/reference.go rename to pkg/invoice/report/qrbill/reference.go index 7d88399..7ec93ec 100644 --- a/report/invoice/reference.go +++ b/pkg/invoice/report/qrbill/reference.go @@ -1,4 +1,4 @@ -package invoice +package qrbill import ( "crypto/rand" diff --git a/report/resource.go b/pkg/invoice/report/resource.go similarity index 56% rename from report/resource.go rename to pkg/invoice/report/resource.go index 50a20b1..a0f9d00 100644 --- a/report/resource.go +++ b/pkg/invoice/report/resource.go @@ -3,15 +3,15 @@ package report import ( "time" - "git.schreifuchs.ch/lou-taylor/accounting/issue" - "git.schreifuchs.ch/lou-taylor/accounting/model" - "git.schreifuchs.ch/lou-taylor/accounting/report/invoice" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/issue" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model" + "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/report/qrbill" ) type Report struct { Date time.Time Issues []issue.Issue - Invoice invoice.Invoice + Invoice qrbill.Invoice Rate float64 Company model.Entity Client model.Entity @@ -25,7 +25,7 @@ func New(issues []issue.Issue, company, client model.Entity, rate float64) *Repo Company: company, Client: client, } - r.Invoice = invoice.New(r.applyRate(r.Total()), r.Company, &r.Client) + // r.Invoice = qrbill.New(r.applyRate(r.Total()), r.Company, &r.Client) return r } diff --git a/report/template.go b/pkg/invoice/report/template.go similarity index 88% rename from report/template.go rename to pkg/invoice/report/template.go index a7d0be7..b758e9d 100644 --- a/report/template.go +++ b/pkg/invoice/report/template.go @@ -20,7 +20,7 @@ type tmpler struct { Style template.CSS } -func (r Report) ToHTML() string { +func (r Report) ToHTML() (html string, err error) { tmpl := template.Must( template.New("report").Funcs(template.FuncMap{ "md": mdToHTML, @@ -32,11 +32,14 @@ func (r Report) ToHTML() string { }).Parse(htmlTemplate)) buf := new(bytes.Buffer) - err := tmpl.Execute(buf, tmpler{r, style}) + + err = tmpl.Execute(buf, tmpler{r, style}) if err != nil { - panic(err) + return } - return buf.String() + + html = buf.String() + return } func (r Report) applyRate(dur time.Duration) string { diff --git a/pkg/invoice/resource.go b/pkg/invoice/resource.go new file mode 100644 index 0000000..c4098e1 --- /dev/null +++ b/pkg/invoice/resource.go @@ -0,0 +1,34 @@ +package invoice + +import ( + "io" + "log/slog" + + "code.gitea.io/sdk/gitea" +) + +type Repo struct { + Owner string `json:"owner"` + Repo string `json:"repo"` +} + +type Service struct { + log *slog.Logger + gitea giteaClient + pdf pdfGenerator +} + +func New(log *slog.Logger, gitea giteaClient, pdf pdfGenerator) *Service { + return &Service{ + log: log, + gitea: gitea, + pdf: pdf, + } +} + +type giteaClient interface { + ListRepoIssues(owner, repo string, opt gitea.ListIssueOption) ([]*gitea.Issue, *gitea.Response, error) +} +type pdfGenerator interface { + HtmlToPdf(html string) (pdf io.ReadCloser, err error) +} diff --git a/types.go b/types.go deleted file mode 100644 index 8f351af..0000000 --- a/types.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "encoding/json" - "time" -) - -type Duration time.Duration - -func (d *Duration) UnmarshalJSON(b []byte) error { - var s string - if err := json.Unmarshal(b, &s); err != nil { - return err - } - tmp, err := time.ParseDuration(s) - if err != nil { - return err - } - *d = Duration(tmp) - return nil -} \ No newline at end of file