feat(invoice): add send route
This commit is contained in:
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
|
||||
}
|
||||
17
internal/email/model.go
Normal file
17
internal/email/model.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package email
|
||||
|
||||
import "io"
|
||||
|
||||
type Mail struct {
|
||||
To []string
|
||||
Cc []string
|
||||
Bcc []string
|
||||
Subject string
|
||||
Body string
|
||||
Attachments []Attachment
|
||||
}
|
||||
type Attachment struct {
|
||||
Name string
|
||||
MimeType string
|
||||
Content io.Reader
|
||||
}
|
||||
52
internal/email/resource.go
Normal file
52
internal/email/resource.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/smtp"
|
||||
|
||||
"github.com/jordan-wright/email"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
SMTP SMTPConfig
|
||||
From string
|
||||
}
|
||||
|
||||
type SMTPConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
from string
|
||||
pool *email.Pool
|
||||
cfg Config
|
||||
}
|
||||
|
||||
func New(cfg Config) (s *Service, err error) {
|
||||
p, err := email.NewPool(
|
||||
cfg.SMTP.Host+":"+cfg.SMTP.Port,
|
||||
1,
|
||||
smtp.PlainAuth(
|
||||
"",
|
||||
cfg.SMTP.User,
|
||||
cfg.SMTP.Password,
|
||||
cfg.SMTP.Host,
|
||||
), &tls.Config{
|
||||
InsecureSkipVerify: true, // set false in production
|
||||
ServerName: cfg.SMTP.Host, // must match the SMTP host
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s = &Service{
|
||||
from: cfg.From,
|
||||
pool: p,
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
34
internal/email/send.go
Normal file
34
internal/email/send.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/smtp"
|
||||
|
||||
"github.com/jordan-wright/email"
|
||||
)
|
||||
|
||||
func (s Service) Send(m Mail) (err error) {
|
||||
e := email.NewEmail()
|
||||
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)
|
||||
|
||||
for _, a := range m.Attachments {
|
||||
e.Attach(a.Content, a.Name, a.MimeType)
|
||||
}
|
||||
err = e.SendWithTLS(s.cfg.SMTP.Host+":"+s.cfg.SMTP.Port, smtp.PlainAuth(
|
||||
s.cfg.SMTP.User,
|
||||
s.cfg.SMTP.User,
|
||||
s.cfg.SMTP.Password,
|
||||
s.cfg.SMTP.Host,
|
||||
), &tls.Config{
|
||||
ServerName: s.cfg.SMTP.Host,
|
||||
})
|
||||
|
||||
// err = s.pool.Send(e, time.Second*5)
|
||||
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user