move pdf to driver layer

This commit is contained in:
Ruidy 2024-09-11 11:04:34 +02:00
parent 916f2b1c47
commit 876d54d7bb
No known key found for this signature in database
GPG key ID: E00F51288CB857CC
10 changed files with 236 additions and 220 deletions

View file

@ -5,6 +5,7 @@ import (
"strconv" "strconv"
"time" "time"
u "github.com/rjNemo/underscore"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/config"
@ -30,6 +31,37 @@ func (b Booking) InvoiceNumber(hc *config.Host) string {
return fmt.Sprintf("%s%04s", hc.InvoicePrefix, strconv.Itoa(b.Id+hc.CustomerSeed)) return fmt.Sprintf("%s%04s", hc.InvoicePrefix, strconv.Itoa(b.Id+hc.CustomerSeed))
} }
func (b Booking) Serialize(hc *config.Host) map[string]any {
return map[string]any{
"host": map[string]any{
"name": hc.Name,
"address": hc.Address,
"zip": hc.ZipCode,
"city": hc.City,
"phone": hc.PhoneNumber,
"email": hc.Email,
},
"id": b.InvoiceNumber(hc),
"name": b.Name,
"phone_number": b.PhoneNumber,
"customers_number": b.CustomerNumber,
"platform": b.Platform,
"from": b.From.Format("02/01/2006"),
"to": b.To.Format("02/01/2006"),
"lines": u.Map(b.Items, func(i Item) map[string]any {
return map[string]any{
"name": i.Item,
"quantity": i.Quantity,
"price": i.Price,
"total": i.Price * float64(i.Quantity),
}
}),
"total": strconv.FormatFloat(u.Reduce(b.Items, func(i Item, sum float64) float64 {
return sum + i.Price*float64(i.Quantity)
}, 0.0), 'f', 2, 64),
}
}
type BookingRequest struct { type BookingRequest struct {
gorm.Model gorm.Model
From time.Time From time.Time

View file

@ -2,6 +2,7 @@ package booking
import ( import (
"log" "log"
"strconv"
"time" "time"
u "github.com/rjNemo/underscore" u "github.com/rjNemo/underscore"
@ -17,7 +18,33 @@ type Report struct {
BookingFees float64 BookingFees float64
} }
func (bs Service) BuildReport(period string, month, year int) *Report { func (r Report) Serialize(month, year int) map[string]any {
return map[string]any{
"month": month,
"year": year,
"total": strconv.FormatFloat(r.Total, 'f', 2, 64),
"platform_fees": strconv.FormatFloat(r.PlatformFees, 'f', 2, 64),
"fee": strconv.FormatFloat(r.Fee, 'f', 2, 64),
"profit": strconv.FormatFloat(r.Profit, 'f', 2, 64),
"card_total": strconv.FormatFloat(r.CardTotal, 'f', 2, 64),
"booking_fees": strconv.FormatFloat(r.BookingFees, 'f', 2, 64),
"lines": u.Map(r.Lines, func(l *Line) map[string]any {
return map[string]any{
"id": l.Id,
"name": l.CustomerName,
"from": l.From.Format("02/01/2006"),
"to": l.To.Format("02/01/2006"),
"total": strconv.FormatFloat(l.Total, 'f', 2, 64),
"platform": l.Platform,
"platform_fees": strconv.FormatFloat(l.PlatformFees, 'f', 2, 64),
"fee": strconv.FormatFloat(l.Fee(), 'f', 2, 64),
"profit": strconv.FormatFloat(l.Profit(), 'f', 2, 64),
}
}),
}
}
func (bs Service) Report(period string, month, year int) *Report {
var startDate time.Time var startDate time.Time
var endDate time.Time var endDate time.Time
@ -54,3 +81,7 @@ func (bs Service) BuildReport(period string, month, year int) *Report {
}, 0.0), }, 0.0),
} }
} }
func (bs Service) BuildReport(report *Report, period string, month, year int) error {
return bs.pdf.BuildReport(report.Serialize(month, year), period, month, year)
}

View file

@ -6,20 +6,23 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"github.com/rjNemo/rentease/internal/config"
"github.com/rjNemo/rentease/internal/driver/calendar" "github.com/rjNemo/rentease/internal/driver/calendar"
"github.com/rjNemo/rentease/internal/driver/pdf"
) )
type Service struct { type Service struct {
store *PgStore store *PgStore
calendar calendar.Client calendar calendar.Client
pdf pdf.Client
} }
func NewService(db *gorm.DB, calendar calendar.Client) (*Service, error) { func NewService(db *gorm.DB, calendar calendar.Client, pdf pdf.Client) (*Service, error) {
return &Service{ return &Service{
store: NewPgStore(db), store: NewPgStore(db),
calendar: calendar, calendar: calendar,
}, pdf: pdf,
nil }, nil
} }
func (bs Service) All() []*Line { func (bs Service) All() []*Line {
@ -98,3 +101,7 @@ func (bs Service) Cancel(id int) {
log.Println(err) log.Println(err)
} }
} }
func (bs Service) BuildInvoice(b *Booking, hc *config.Host) error {
return bs.pdf.BuildInvoice(b.Serialize(hc))
}

View file

@ -0,0 +1,142 @@
package pdf
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"github.com/labstack/gommon/log"
)
type Client interface {
BuildInvoice(context map[string]any) error
BuildReport(context map[string]any, period string, month, year int) error
}
type PdfClient struct {
path string
invoiceId string
reportId string
url string
apiKey string
}
func NewPdfClient(pid, rid, url, key string) (*PdfClient, error) {
if pid == "" || rid == "" || url == "" || key == "" {
return nil, errors.New("error building Pdf service. Verify your env variables")
}
return &PdfClient{
path: "index.html",
invoiceId: pid,
reportId: rid,
url: url,
apiKey: key,
}, nil
}
func (ps PdfClient) BuildInvoice(context map[string]any) error {
data := struct {
Context map[string]any `json:"context"`
Path string `json:"path"`
ProjectId string `json:"projectId"`
}{
Context: context,
Path: ps.path,
ProjectId: ps.invoiceId,
}
payload, err := json.Marshal(data)
if err != nil {
log.Warnf("Error marshalling JSON: %s", err)
return err
}
return ps.sendData(payload)
}
func (ps PdfClient) BuildReport(context map[string]any, period string, month, year int) error {
data := struct {
Context map[string]any `json:"context"`
Path string `json:"path"`
ProjectId string `json:"projectId"`
}{
Context: context,
Path: ps.path,
ProjectId: ps.reportId,
}
payload, err := json.Marshal(data)
if err != nil {
log.Warnf("Error marshalling JSON: %s", err)
return err
}
return ps.sendData(payload)
}
func (ps PdfClient) sendData(payload []byte) error {
req, err := http.NewRequest("POST", ps.url, bytes.NewBuffer(payload))
if err != nil {
log.Warnf("Error creating request: %s", err)
return err
}
req.Header.Set("Authorization", "Bearer "+ps.apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Warnf("Error sending request: %s", err)
return err
}
defer resp.Body.Close()
res := new(struct {
Url string `json:"url"`
Error string `json:"error"`
})
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
err = json.Unmarshal(body, res)
if err != nil {
log.Warnf("error decoding response: %s", err)
return err
}
if res.Error != "" {
log.Warnf("error building pdf file %s", err)
return errors.New(res.Error)
}
resp, err = http.Get(res.Url)
if err != nil {
log.Warnf("Error retrieving file")
return err
}
defer resp.Body.Close()
file, err := os.Create("tmp.pdf")
if err != nil {
log.Fatal(err)
}
defer file.Close()
body, err = io.ReadAll(resp.Body)
if err != nil {
return err
}
_, err = file.Write(body)
if err != nil {
log.Error("Error copying file content")
return err
}
return nil
}

View file

@ -1,191 +0,0 @@
package pdf
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"strconv"
"github.com/labstack/gommon/log"
u "github.com/rjNemo/underscore"
"github.com/rjNemo/rentease/internal/booking"
"github.com/rjNemo/rentease/internal/config"
)
type PdfService struct {
path string
invoiceId string
reportId string
url string
apiKey string
}
func NewPdfService(pid, rid, url, key string) (*PdfService, error) {
if pid == "" || rid == "" || url == "" || key == "" {
return nil, errors.New("error building Pdf service. Verify your env variables")
}
return &PdfService{
path: "index.html",
invoiceId: pid,
reportId: rid,
url: url,
apiKey: key,
}, nil
}
func (ps PdfService) BuildInvoice(b *booking.Booking, hc *config.Host) error {
data := struct {
Context map[string]any `json:"context"`
Path string `json:"path"`
ProjectId string `json:"projectId"`
}{
Context: map[string]any{
"host": map[string]any{
"name": hc.Name,
"address": hc.Address,
"zip": hc.ZipCode,
"city": hc.City,
"phone": hc.PhoneNumber,
"email": hc.Email,
},
"id": b.InvoiceNumber(hc),
"name": b.Name,
"phone_number": b.PhoneNumber,
"customers_number": b.CustomerNumber,
"platform": b.Platform,
"from": b.From.Format("02/01/2006"),
"to": b.To.Format("02/01/2006"),
"lines": u.Map(b.Items, func(i booking.Item) map[string]any {
return map[string]any{
"name": i.Item,
"quantity": i.Quantity,
"price": i.Price,
"total": i.Price * float64(i.Quantity),
}
}),
"total": strconv.FormatFloat(u.Reduce(b.Items, func(i booking.Item, sum float64) float64 {
return sum + i.Price*float64(i.Quantity)
}, 0.0), 'f', 2, 64),
},
Path: ps.path,
ProjectId: ps.invoiceId,
}
payload, err := json.Marshal(data)
if err != nil {
log.Warnf("Error marshalling JSON: %s", err)
return err
}
return ps.sendData(payload)
}
func (ps PdfService) BuildReport(r *booking.Report, period string, month, year int) error {
data := struct {
Context map[string]any `json:"context"`
Path string `json:"path"`
ProjectId string `json:"projectId"`
}{
Context: map[string]any{
"month": month,
"year": year,
"total": strconv.FormatFloat(r.Total, 'f', 2, 64),
"platform_fees": strconv.FormatFloat(r.PlatformFees, 'f', 2, 64),
"fee": strconv.FormatFloat(r.Fee, 'f', 2, 64),
"profit": strconv.FormatFloat(r.Profit, 'f', 2, 64),
"card_total": strconv.FormatFloat(r.CardTotal, 'f', 2, 64),
"booking_fees": strconv.FormatFloat(r.BookingFees, 'f', 2, 64),
"lines": u.Map(r.Lines, func(l *booking.Line) map[string]any {
return map[string]any{
"id": l.Id,
"name": l.CustomerName,
"from": l.From.Format("02/01/2006"),
"to": l.To.Format("02/01/2006"),
"total": strconv.FormatFloat(l.Total, 'f', 2, 64),
"platform": l.Platform,
"platform_fees": strconv.FormatFloat(l.PlatformFees, 'f', 2, 64),
"fee": strconv.FormatFloat(l.Fee(), 'f', 2, 64),
"profit": strconv.FormatFloat(l.Profit(), 'f', 2, 64),
}
}),
},
Path: ps.path,
ProjectId: ps.reportId,
}
payload, err := json.Marshal(data)
if err != nil {
log.Warnf("Error marshalling JSON: %s", err)
return err
}
return ps.sendData(payload)
}
func (ps PdfService) sendData(payload []byte) error {
req, err := http.NewRequest("POST", ps.url, bytes.NewBuffer(payload))
if err != nil {
log.Warnf("Error creating request: %s", err)
return err
}
req.Header.Set("Authorization", "Bearer "+ps.apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Warnf("Error sending request: %s", err)
return err
}
defer resp.Body.Close()
res := new(struct {
Url string `json:"url"`
Error string `json:"error"`
})
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
err = json.Unmarshal(body, res)
if err != nil {
log.Warnf("error decoding response: %s", err)
return err
}
if res.Error != "" {
log.Warnf("error building pdf file %s", err)
return errors.New(res.Error)
}
resp, err = http.Get(res.Url)
if err != nil {
log.Warnf("Error retrieving file")
return err
}
defer resp.Body.Close()
file, err := os.Create("tmp.pdf")
if err != nil {
log.Fatal(err)
}
defer file.Close()
body, err = io.ReadAll(resp.Body)
if err != nil {
return err
}
_, err = file.Write(body)
if err != nil {
log.Error("Error copying file content")
return err
}
return nil
}

View file

@ -10,10 +10,9 @@ import (
"github.com/rjNemo/rentease/internal/booking" "github.com/rjNemo/rentease/internal/booking"
"github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/config"
"github.com/rjNemo/rentease/internal/pdf"
) )
func handlePdfCreateInvoice(bs *booking.Service, ps *pdf.PdfService, hc *config.Host) echo.HandlerFunc { func handlePdfCreateInvoice(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
idStr := c.Param("id") idStr := c.Param("id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
@ -23,7 +22,7 @@ func handlePdfCreateInvoice(bs *booking.Service, ps *pdf.PdfService, hc *config.
b := bs.One(id) b := bs.One(id)
err = ps.BuildInvoice(b, hc) err = bs.BuildInvoice(b, hc)
if err != nil { if err != nil {
return err return err
} }
@ -31,7 +30,7 @@ func handlePdfCreateInvoice(bs *booking.Service, ps *pdf.PdfService, hc *config.
} }
} }
func handlePdfCreateReport(bs *booking.Service, ps *pdf.PdfService) echo.HandlerFunc { func handlePdfCreateReport(bs *booking.Service) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
period := c.QueryParam("period") period := c.QueryParam("period")
if !u.Contains([]string{"month", "year"}, period) { if !u.Contains([]string{"month", "year"}, period) {
@ -59,8 +58,8 @@ func handlePdfCreateReport(bs *booking.Service, ps *pdf.PdfService) echo.Handler
} }
} }
report := bs.BuildReport(period, month, year) report := bs.Report(period, month, year)
err = ps.BuildReport(report, period, month, year) err = bs.BuildReport(report, period, month, year)
if err != nil { if err != nil {
return err return err
} }

View file

@ -61,7 +61,7 @@ func handleReportCompute(bs *booking.Service, hc *config.Host) echo.HandlerFunc
} }
} }
r := bs.BuildReport(period, month, year) r := bs.Report(period, month, year)
reportVm := &view.ReportViewModel{ reportVm := &view.ReportViewModel{
Total: strconv.FormatFloat(r.Total, 'f', 2, 64), Total: strconv.FormatFloat(r.Total, 'f', 2, 64),

View file

@ -28,11 +28,11 @@ func (s Server) MountHandlers() {
private.PUT("/bookings/:id", handleBookingUpdate(s.bs, s.hc)) private.PUT("/bookings/:id", handleBookingUpdate(s.bs, s.hc))
private.PATCH("/bookings/:id/cancel", handleBookingCancel(s.bs)) private.PATCH("/bookings/:id/cancel", handleBookingCancel(s.bs))
private.POST("/bookings/:id/items", handleCreateItem(s.bs, s.hc)) private.POST("/bookings/:id/items", handleCreateItem(s.bs, s.hc))
private.GET("/bookings/pdf/:id", handlePdfCreateInvoice(s.bs, s.ps, s.hc)) private.GET("/bookings/pdf/:id", handlePdfCreateInvoice(s.bs, s.hc))
private.POST("/items/:id", handleItemPay(s.bs)) private.POST("/items/:id", handleItemPay(s.bs))
private.PUT("/items/:id", handleItemUpdate(s.bs)) private.PUT("/items/:id", handleItemUpdate(s.bs))
private.GET("/items/:id", handleLineItemForm(s.bs)) private.GET("/items/:id", handleLineItemForm(s.bs))
private.GET("/reports", handleReportsPage()) private.GET("/reports", handleReportsPage())
private.GET("/reports/do", handleReportCompute(s.bs, s.hc)) private.GET("/reports/do", handleReportCompute(s.bs, s.hc))
private.GET("/reports/pdf", handlePdfCreateReport(s.bs, s.ps)) private.GET("/reports/pdf", handlePdfCreateReport(s.bs))
} }

View file

@ -20,19 +20,17 @@ import (
"github.com/rjNemo/rentease/internal/auth" "github.com/rjNemo/rentease/internal/auth"
"github.com/rjNemo/rentease/internal/booking" "github.com/rjNemo/rentease/internal/booking"
"github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/config"
"github.com/rjNemo/rentease/internal/pdf"
) )
type Server struct { type Server struct {
Router *echo.Echo Router *echo.Echo
bs *booking.Service bs *booking.Service
as *auth.Service as *auth.Service
ps *pdf.PdfService
hc *config.Host hc *config.Host
addr string addr string
} }
func New(bs *booking.Service, as *auth.Service, ps *pdf.PdfService, hc *config.Host, opts ...Option) (*Server, error) { func New(bs *booking.Service, as *auth.Service, hc *config.Host, opts ...Option) (*Server, error) {
option := new(options) option := new(options)
for _, opt := range opts { for _, opt := range opts {
err := opt(option) err := opt(option)
@ -45,7 +43,6 @@ func New(bs *booking.Service, as *auth.Service, ps *pdf.PdfService, hc *config.H
Router: NewRouter(*option.fs, *option.debug, *option.secretKey, option.origins), Router: NewRouter(*option.fs, *option.debug, *option.secretKey, option.origins),
bs: bs, bs: bs,
as: as, as: as,
ps: ps,
hc: hc, hc: hc,
addr: fmt.Sprintf("0.0.0.0:%d", *option.port), addr: fmt.Sprintf("0.0.0.0:%d", *option.port),
} }

19
main.go
View file

@ -16,7 +16,7 @@ import (
"github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/config"
"github.com/rjNemo/rentease/internal/driver/calendar" "github.com/rjNemo/rentease/internal/driver/calendar"
"github.com/rjNemo/rentease/internal/driver/database" "github.com/rjNemo/rentease/internal/driver/database"
"github.com/rjNemo/rentease/internal/pdf" "github.com/rjNemo/rentease/internal/driver/pdf"
"github.com/rjNemo/rentease/internal/server" "github.com/rjNemo/rentease/internal/server"
) )
@ -62,14 +62,8 @@ func run(c context.Context, getEnv func(string) string) error {
return fmt.Errorf("error building calendar client %s", err) return fmt.Errorf("error building calendar client %s", err)
} }
// build booking service // build pdf client
bs, err := booking.NewService(db, gc) pc, err := pdf.NewPdfClient(
if err != nil {
return fmt.Errorf("error starting booking service %s", err)
}
// build pdf service
ps, err := pdf.NewPdfService(
getEnv("HTMLDOCS_PROJECT_ID"), getEnv("HTMLDOCS_PROJECT_ID"),
getEnv("HTMLDOCS_REPORT_PROJECT_ID"), getEnv("HTMLDOCS_REPORT_PROJECT_ID"),
getEnv("HTMLDOCS_URL"), getEnv("HTMLDOCS_URL"),
@ -79,6 +73,12 @@ func run(c context.Context, getEnv func(string) string) error {
return fmt.Errorf("error starting pdf service %s", err) return fmt.Errorf("error starting pdf service %s", err)
} }
// build booking service
bs, err := booking.NewService(db, gc, pc)
if err != nil {
return fmt.Errorf("error starting booking service %s", err)
}
// build authentication service // build authentication service
as, err := auth.NewService( as, err := auth.NewService(
getEnv("SESSION_SECRET"), getEnv("SESSION_SECRET"),
@ -103,7 +103,6 @@ func run(c context.Context, getEnv func(string) string) error {
srv, err := server.New( srv, err := server.New(
bs, bs,
as, as,
ps,
config.NewHost(), // TODO: move to the database at some point config.NewHost(), // TODO: move to the database at some point
server.WithPort(port), server.WithPort(port),
server.WithFileSystem(static), server.WithFileSystem(static),