diff --git a/internal/booking/models.go b/internal/booking/models.go index 55b80c2..04ecd4d 100644 --- a/internal/booking/models.go +++ b/internal/booking/models.go @@ -5,6 +5,7 @@ import ( "strconv" "time" + u "github.com/rjNemo/underscore" "gorm.io/gorm" "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)) } +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 { gorm.Model From time.Time diff --git a/internal/booking/report.go b/internal/booking/report.go index 34df037..df09ca6 100644 --- a/internal/booking/report.go +++ b/internal/booking/report.go @@ -2,6 +2,7 @@ package booking import ( "log" + "strconv" "time" u "github.com/rjNemo/underscore" @@ -17,7 +18,33 @@ type Report struct { 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 endDate time.Time @@ -54,3 +81,7 @@ func (bs Service) BuildReport(period string, month, year int) *Report { }, 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) +} diff --git a/internal/booking/service.go b/internal/booking/service.go index f498acb..3007d0e 100644 --- a/internal/booking/service.go +++ b/internal/booking/service.go @@ -6,20 +6,23 @@ import ( "gorm.io/gorm" + "github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/driver/calendar" + "github.com/rjNemo/rentease/internal/driver/pdf" ) type Service struct { store *PgStore 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{ - store: NewPgStore(db), - calendar: calendar, - }, - nil + store: NewPgStore(db), + calendar: calendar, + pdf: pdf, + }, nil } func (bs Service) All() []*Line { @@ -98,3 +101,7 @@ func (bs Service) Cancel(id int) { log.Println(err) } } + +func (bs Service) BuildInvoice(b *Booking, hc *config.Host) error { + return bs.pdf.BuildInvoice(b.Serialize(hc)) +} diff --git a/internal/driver/pdf/service.go b/internal/driver/pdf/service.go new file mode 100644 index 0000000..bf7163e --- /dev/null +++ b/internal/driver/pdf/service.go @@ -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 +} diff --git a/internal/pdf/service.go b/internal/pdf/service.go deleted file mode 100644 index 4b68126..0000000 --- a/internal/pdf/service.go +++ /dev/null @@ -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 -} diff --git a/internal/server/handle_pdf.go b/internal/server/handle_pdf.go index c6c9457..76acf08 100644 --- a/internal/server/handle_pdf.go +++ b/internal/server/handle_pdf.go @@ -10,10 +10,9 @@ import ( "github.com/rjNemo/rentease/internal/booking" "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 { idStr := c.Param("id") id, err := strconv.Atoi(idStr) @@ -23,7 +22,7 @@ func handlePdfCreateInvoice(bs *booking.Service, ps *pdf.PdfService, hc *config. b := bs.One(id) - err = ps.BuildInvoice(b, hc) + err = bs.BuildInvoice(b, hc) if err != nil { 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 { period := c.QueryParam("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) - err = ps.BuildReport(report, period, month, year) + report := bs.Report(period, month, year) + err = bs.BuildReport(report, period, month, year) if err != nil { return err } diff --git a/internal/server/handle_reports.go b/internal/server/handle_reports.go index d161710..4304bcc 100644 --- a/internal/server/handle_reports.go +++ b/internal/server/handle_reports.go @@ -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{ Total: strconv.FormatFloat(r.Total, 'f', 2, 64), diff --git a/internal/server/routes.go b/internal/server/routes.go index a147e87..7b61f82 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -28,11 +28,11 @@ func (s Server) MountHandlers() { private.PUT("/bookings/:id", handleBookingUpdate(s.bs, s.hc)) private.PATCH("/bookings/:id/cancel", handleBookingCancel(s.bs)) 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.PUT("/items/:id", handleItemUpdate(s.bs)) private.GET("/items/:id", handleLineItemForm(s.bs)) private.GET("/reports", handleReportsPage()) 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)) } diff --git a/internal/server/server.go b/internal/server/server.go index 4db1b66..89c7e5c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -20,19 +20,17 @@ import ( "github.com/rjNemo/rentease/internal/auth" "github.com/rjNemo/rentease/internal/booking" "github.com/rjNemo/rentease/internal/config" - "github.com/rjNemo/rentease/internal/pdf" ) type Server struct { Router *echo.Echo bs *booking.Service as *auth.Service - ps *pdf.PdfService hc *config.Host 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) for _, opt := range opts { 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), bs: bs, as: as, - ps: ps, hc: hc, addr: fmt.Sprintf("0.0.0.0:%d", *option.port), } diff --git a/main.go b/main.go index 9e55763..91a3ae5 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( "github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/driver/calendar" "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" ) @@ -62,14 +62,8 @@ func run(c context.Context, getEnv func(string) string) error { return fmt.Errorf("error building calendar client %s", err) } - // build booking service - bs, err := booking.NewService(db, gc) - if err != nil { - return fmt.Errorf("error starting booking service %s", err) - } - - // build pdf service - ps, err := pdf.NewPdfService( + // build pdf client + pc, err := pdf.NewPdfClient( getEnv("HTMLDOCS_PROJECT_ID"), getEnv("HTMLDOCS_REPORT_PROJECT_ID"), 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) } + // 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 as, err := auth.NewService( getEnv("SESSION_SECRET"), @@ -103,7 +103,6 @@ func run(c context.Context, getEnv func(string) string) error { srv, err := server.New( bs, as, - ps, config.NewHost(), // TODO: move to the database at some point server.WithPort(port), server.WithFileSystem(static),