feat(invoice): add send route
This commit is contained in:
26
cmd/invoiceapi/main.go
Normal file
26
cmd/invoiceapi/main.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
36
config.go
36
config.go
@@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
23
internal/api/api.go
Normal file
23
internal/api/api.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
99
internal/api/httpinvoce/controller.go
Normal file
99
internal/api/httpinvoce/controller.go
Normal file
@@ -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"))
|
||||||
|
}
|
||||||
69
internal/api/httpinvoce/model.go
Normal file
69
internal/api/httpinvoce/model.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
29
internal/api/httpinvoce/resource.go
Normal file
29
internal/api/httpinvoce/resource.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
9
internal/api/httpinvoce/routes.go
Normal file
9
internal/api/httpinvoce/routes.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package httpinvoce
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s Service) RegisterRoutes(mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc("POST /invoice", s.createInvoice)
|
||||||
|
}
|
||||||
35
internal/api/routes.go
Normal file
35
internal/api/routes.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package mailer
|
package email
|
||||||
|
|
||||||
import "io"
|
import "io"
|
||||||
|
|
||||||
type Mail struct {
|
type Mail struct {
|
||||||
TO string
|
To []string
|
||||||
|
Cc []string
|
||||||
|
Bcc []string
|
||||||
Subject string
|
Subject string
|
||||||
Body string
|
Body string
|
||||||
Attachments []Attachment
|
Attachments []Attachment
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package mailer
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package mailer
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
@@ -7,10 +7,11 @@ import (
|
|||||||
"github.com/jordan-wright/email"
|
"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 := email.NewEmail()
|
||||||
e.To = []string{m.TO}
|
e.To = m.To
|
||||||
e.Bcc = bcc
|
e.Cc = m.Cc
|
||||||
|
e.Bcc = m.Bcc
|
||||||
e.From = s.from
|
e.From = s.from
|
||||||
e.Subject = m.Subject
|
e.Subject = m.Subject
|
||||||
e.Text = []byte(m.Body)
|
e.Text = []byte(m.Body)
|
||||||
114
main.go
114
main.go
@@ -1,106 +1,22 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"git.schreifuchs.ch/lou-taylor/accounting/internal/api"
|
||||||
|
|
||||||
"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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repo struct {
|
|
||||||
Owner string `json:"owner"`
|
|
||||||
Repo string `json:"repo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg, err := LoadConfig("config.json")
|
// var cfg invoice.Config
|
||||||
if err != nil {
|
// file, err := os.Open("config.json")
|
||||||
panic(err)
|
// if err != nil {
|
||||||
}
|
// panic(err)
|
||||||
|
// }
|
||||||
client, err := gitea.NewClient(
|
// defer file.Close()
|
||||||
cfg.GiteaURL,
|
// decoder := json.NewDecoder(file)
|
||||||
gitea.SetToken(cfg.GiteaToken),
|
// err = decoder.Decode(&cfg)
|
||||||
)
|
// if err != nil {
|
||||||
if err != nil {
|
// panic(err)
|
||||||
panic(err)
|
// }
|
||||||
}
|
//
|
||||||
|
// invoice.Generate(cfg)
|
||||||
var is []*gitea.Issue
|
api.Start(":8080")
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -13,12 +13,13 @@ type Service struct {
|
|||||||
gotenberg *gotenberg.Client
|
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)
|
service.gotenberg, err = gotenberg.NewClient(hostname, http.DefaultClient)
|
||||||
return
|
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)
|
index, err := document.FromString("index.html", html)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -32,6 +33,9 @@ func (s Service) HtmlToPdf(html string) (pdf io.ReadCloser, err error) {
|
|||||||
req.EmulatePrintMediaType()
|
req.EmulatePrintMediaType()
|
||||||
|
|
||||||
resp, err := s.gotenberg.Send(context.Background(), req)
|
resp, err := s.gotenberg.Send(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
pdf = resp.Body
|
pdf = resp.Body
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
65
pkg/invoice/invoice.go
Normal file
65
pkg/invoice/invoice.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
15
pkg/invoice/model/model.go
Normal file
15
pkg/invoice/model/model.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -1,4 +1,4 @@
|
|||||||
package invoice
|
package qrbill
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package invoice
|
package qrbill
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.schreifuchs.ch/lou-taylor/accounting/model"
|
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model"
|
||||||
"github.com/skip2/go-qrcode"
|
"github.com/skip2/go-qrcode"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package invoice
|
package qrbill
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@@ -3,15 +3,15 @@ package report
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.schreifuchs.ch/lou-taylor/accounting/issue"
|
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/issue"
|
||||||
"git.schreifuchs.ch/lou-taylor/accounting/model"
|
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model"
|
||||||
"git.schreifuchs.ch/lou-taylor/accounting/report/invoice"
|
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/report/qrbill"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Report struct {
|
type Report struct {
|
||||||
Date time.Time
|
Date time.Time
|
||||||
Issues []issue.Issue
|
Issues []issue.Issue
|
||||||
Invoice invoice.Invoice
|
Invoice qrbill.Invoice
|
||||||
Rate float64
|
Rate float64
|
||||||
Company model.Entity
|
Company model.Entity
|
||||||
Client model.Entity
|
Client model.Entity
|
||||||
@@ -25,7 +25,7 @@ func New(issues []issue.Issue, company, client model.Entity, rate float64) *Repo
|
|||||||
Company: company,
|
Company: company,
|
||||||
Client: client,
|
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
|
return r
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ type tmpler struct {
|
|||||||
Style template.CSS
|
Style template.CSS
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Report) ToHTML() string {
|
func (r Report) ToHTML() (html string, err error) {
|
||||||
tmpl := template.Must(
|
tmpl := template.Must(
|
||||||
template.New("report").Funcs(template.FuncMap{
|
template.New("report").Funcs(template.FuncMap{
|
||||||
"md": mdToHTML,
|
"md": mdToHTML,
|
||||||
@@ -32,11 +32,14 @@ func (r Report) ToHTML() string {
|
|||||||
}).Parse(htmlTemplate))
|
}).Parse(htmlTemplate))
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
err := tmpl.Execute(buf, tmpler{r, style})
|
|
||||||
|
err = tmpl.Execute(buf, tmpler{r, style})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return
|
||||||
}
|
}
|
||||||
return buf.String()
|
|
||||||
|
html = buf.String()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Report) applyRate(dur time.Duration) string {
|
func (r Report) applyRate(dur time.Duration) string {
|
||||||
34
pkg/invoice/resource.go
Normal file
34
pkg/invoice/resource.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
21
types.go
21
types.go
@@ -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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user