feat(invoice): add send route

This commit is contained in:
2025-08-26 20:30:48 +02:00
parent 958979c62b
commit 788571162d
35 changed files with 451 additions and 193 deletions

View 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"))
}

View 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)
}

View 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)
}

View File

@@ -0,0 +1,9 @@
package httpinvoce
import (
"net/http"
)
func (s Service) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("POST /invoice", s.createInvoice)
}