feat(invoice): add send route
This commit is contained in:
1 parent
958979c62b
commit
788571162d
35 files changed
+451
-193
No files matched your search
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package httpinvoce
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (s Service) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("POST /invoice", s.createInvoice)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
type Mail struct {
|
||||
TO string
|
||||
To []string
|
||||
Cc []string
|
||||
Bcc []string
|
||||
Subject string
|
||||
Body string
|
||||
Attachments []Attachment
|
||||
@@ -1,4 +1,4 @@
|
||||
package mailer
|
||||
package email
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+6
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -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"`
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
package invoice
|
||||
package qrbill
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package invoice
|
||||
package qrbill
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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