Create taxes for taxable items automatically (#16)

* refactor return error when building booking service

* fix the description

* set taxable item by amount

* auto create tax items if the item is taxable

* fix linter

* remove legacy tax entry

* display multiple items

* use the price from the form

* improve item sorting

* lintfix
This commit is contained in:
Ruidy 2024-08-26 21:44:31 +02:00 committed by GitHub
parent 04be887ad8
commit d4e6b35a96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 65 additions and 47 deletions

View file

@ -1,9 +1,9 @@
FROM golang:1.23-alpine AS builder
RUN apk update && apk add --no-cache \
build-base \
ca-certificates \
&& update-ca-certificates
build-base \
ca-certificates \
&& update-ca-certificates
WORKDIR /app

View file

@ -24,8 +24,8 @@ type HostItem struct {
// If true, the item will be added to the calendar
MustSyncCalendar bool
HasEndDate bool
// If true, a tax item will be added to the invoice
Taxable bool // TODO: create taxes auto if taxable item
// Amount of taxes in EUR. If not zero, a tax item will be added to the invoice
Taxes float64
}
func NewHost() *Host {
@ -47,7 +47,7 @@ func NewHost() *Host {
CalendarId: os.Getenv("CALENDAR_ID_T2"),
MustSyncCalendar: true,
HasEndDate: true,
Taxable: true,
Taxes: 1.5,
},
"T3": {
@ -56,7 +56,7 @@ func NewHost() *Host {
CalendarId: os.Getenv("CALENDAR_ID_T3"),
MustSyncCalendar: true,
HasEndDate: true,
Taxable: true,
Taxes: 1.5,
},
"Airport": {
Name: "Airport",
@ -70,10 +70,6 @@ func NewHost() *Host {
Name: "Transport",
Price: 20.0,
},
"Taxes": { // TODO: remove after auto creation enabled
Name: "Taxes",
Price: 1.5,
},
},
}
}

View file

@ -20,8 +20,8 @@ type Service struct {
db *gorm.DB
}
func NewService(db *gorm.DB) *Service {
return &Service{db: db}
func NewService(db *gorm.DB) (*Service, error) {
return &Service{db: db}, nil
}
func (bs Service) All() []*Line {
@ -97,16 +97,30 @@ func (bs Service) Update(id int, From time.Time, To time.Time, Name string, Phon
return b
}
func (bs Service) CreateItem(bid int, item string, qty int, price float64, method string) *Item {
func (bs Service) CreateItem(bookingId int, item config.HostItem, quantity int, price float64, paymentMethod string, customerNumber int) (items []*Item) {
i := &Item{
BookingId: bid,
Item: item,
Quantity: qty,
BookingId: bookingId,
Item: item.Name,
Quantity: quantity,
Price: price,
PaymentMethod: method,
PaymentMethod: paymentMethod,
}
_ = bs.db.Create(i)
return i
items = append(items, i)
if item.Taxes != 0.0 {
ti := &Item{
BookingId: bookingId,
Item: "Taxes",
Quantity: quantity * customerNumber,
Price: item.Taxes,
PaymentMethod: "Cash",
}
_ = bs.db.Create(ti)
items = append(items, ti)
}
return items
}
func (bs Service) PayItem(id int) *Item {
@ -257,20 +271,19 @@ func (bs Service) ParseFromApi(rawContent string) (*Booking, error) {
arrivalDate := extractDate(`Date d'arrivée `, content)
departureDate := extractDate(`Date de départ `, content)
stayLength := extractInt(`Durée de séjour : (\d+) nuits`, content)
totalAmount := extractFloat(`Montant total € (\d+)`, content)
customerName := extractString(`Nom du client : \n\s+([\w\s]+)`, content)
customerName = strings.SplitN(customerName, "\n", 2)[0]
customerEmail := extractString(`[\w\.\-]+@[\w\.\-]+\.\w+`, content)
customerNumber := extractInt(`Nombre de personnes : \s*\n\s*(\d+)`, content)
commissionAmount := extractFloat(`Commission : € (\d+,\d+)`, content)
item := extractString(`Maison 1 Chambre \((T2|T3) -`, content)
itemName := extractString(`Maison 1 Chambre \((T2|T3) -`, content)
externalId := extractString(`Numéro de réservation : \n\s+(\d+)`, content)
standardRate := extractFloat(`Standard Rate\n\s+€ (\d+)`, content)
taxQty := (totalAmount - standardRate*float64(stayLength)) / 1.5
b := bs.Create(*formatDate(arrivalDate), *formatDate(departureDate), customerName, "", customerEmail, "Booking", customerNumber, commissionAmount, &externalId)
bs.CreateItem(b.Id, item, stayLength, standardRate, "Card")
bs.CreateItem(b.Id, "Taxes", int(taxQty), 1.5, "Cash")
if item, ok := config.NewHost().Items[itemName]; ok {
bs.CreateItem(b.Id, item, stayLength, standardRate, "Card", customerNumber)
}
return b, nil
}

View file

@ -23,7 +23,8 @@ func JobMonthlyBookingReport() error {
now := time.Now()
log.Println("Start Monthly Booking Report job at:", now)
report := booking.NewService(db).BuildReport("monthly", int(now.Month()), now.Year())
service, _ := booking.NewService(db)
report := service.BuildReport("monthly", int(now.Month()), now.Year())
ps, err := pdf.NewPdfService(
os.Getenv("HTMLDOCS_PROJECT_ID"),

View file

@ -139,13 +139,13 @@ func handleBookingPage(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
return sum + i.Price*float64(i.Quantity)
}, 0.0), 'f', 2, 64),
Platforms: hc.Platforms,
ItemList: u.OrderBy(func(items map[string]config.HostItem) (out []string) {
ItemList: u.OrderBy(func(items map[string]config.HostItem) (out []string) { // TODO: return the full item to prefill the form
for _, item := range items {
out = append(out, item.Name)
}
return out
}(hc.Items),
func(l, r string) bool { return l < r },
func(l, r string) bool { return l > r },
),
PaymentMethods: hc.PaymentMethods,
}
@ -257,7 +257,7 @@ func handleCreateItem(bs *booking.Service, cs *calendar.Service, hc *config.Host
return fmt.Errorf("invalid item name %q", ni.Item)
}
i := bs.CreateItem(b.Id, ni.Item, ni.Quantity, ni.Price, ni.PaymentMethod)
newItems := bs.CreateItem(b.Id, itm, ni.Quantity, ni.Price, ni.PaymentMethod, b.CustomerNumber)
if err = cs.Create(
itm.CalendarId,
@ -269,16 +269,20 @@ func handleCreateItem(bs *booking.Service, cs *calendar.Service, hc *config.Host
captureError(c, err)
}
return renderTempl(c, http.StatusCreated, view.LineItem(&view.ItemViewModel{
Id: strconv.Itoa(i.Id),
Item: i.Item,
Quantity: strconv.Itoa(i.Quantity),
Price: strconv.FormatFloat(i.Price, 'f', 2, 64),
PaymentMethod: i.PaymentMethod,
PaymentStatus: i.PaymentStatus,
SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.Id),
}))
for _, i := range newItems {
_ = renderTempl(c, http.StatusCreated, view.LineItem(&view.ItemViewModel{
Id: strconv.Itoa(i.Id),
Item: i.Item,
Quantity: strconv.Itoa(i.Quantity),
Price: strconv.FormatFloat(i.Price, 'f', 2, 64),
PaymentMethod: i.PaymentMethod,
PaymentStatus: i.PaymentStatus,
SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.Id),
}))
}
return nil
}
}

View file

@ -74,7 +74,7 @@ func PublicLayout() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" | Locations de vacances au Gosier en Guadeloupe</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"description\" content=\"Locations de vacances au Gosier en Guadeloupe\"><link rel=\"icon\" href=\"/static/icons/favicon.png\"><link rel=\"stylesheet\" href=\"/static/css/pico.min.css\"><link rel=\"stylesheet\" href=\"/static/css/auth.css\"><script src=\"/static/js/htmx.js\" defer></script></head><body hx-boost=\"false\"><nav class=\"container-fluid\"><ul><li><a href=\"/\"><img src=\"/static/img/logo.png\" alt=\"logo de villafleurie\" width=\"50px\"></a></li></ul><ul><li><details class=\"dropdown\"><summary>Logements</summary><ul dir=\"rtl\">")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" | Locations de vacances au Gosier en Guadeloupe</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"description\" content=\"Locations de vacances au Gosier en Guadeloupe\"><link rel=\"icon\" href=\"/static/icons/favicon.png\"><link rel=\"stylesheet\" href=\"/static/css/pico.min.css\"><script src=\"/static/js/htmx.js\" defer></script></head><body hx-boost=\"false\"><nav class=\"container-fluid\"><ul><li><a href=\"/\"><img src=\"/static/img/logo.png\" alt=\"logo de villafleurie\" width=\"50px\"></a></li></ul><ul><li><details class=\"dropdown\"><summary>Logements</summary><ul dir=\"rtl\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -95,7 +95,7 @@ func PublicLayout() templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(l.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/layout/public.templ`, Line: 64, Col: 39}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/layout/public.templ`, Line: 63, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@ -121,7 +121,7 @@ func PublicLayout() templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(hvm.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/layout/public.templ`, Line: 76, Col: 21}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/layout/public.templ`, Line: 75, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@ -143,7 +143,7 @@ func PublicLayout() templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(hvm.PhoneNumber)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/layout/public.templ`, Line: 76, Col: 93}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/layout/public.templ`, Line: 75, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@ -165,7 +165,7 @@ func PublicLayout() templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(hvm.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/layout/public.templ`, Line: 76, Col: 148}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/layout/public.templ`, Line: 75, Col: 148}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {

12
main.go
View file

@ -4,7 +4,6 @@ import (
"context"
"embed"
"fmt"
"log"
"os"
"os/signal"
"strconv"
@ -54,12 +53,17 @@ func run(c context.Context, getEnv func(string) string) error {
return fmt.Errorf("error connecting to the database %s", err)
}
// build booking service
err = db.AutoMigrate(&booking.Booking{}, &booking.BookingRequest{}, &booking.Item{})
if err != nil {
return fmt.Errorf("error migrating the database %s", err)
}
// build booking service
bs, err := booking.NewService(db)
if err != nil {
return fmt.Errorf("error starting booking service %s", err)
}
// build pdf service
ps, err := pdf.NewPdfService(
getEnv("HTMLDOCS_PROJECT_ID"),
@ -85,7 +89,7 @@ func run(c context.Context, getEnv func(string) string) error {
// build calendar service
cs, err := calendar.NewService(ctx, getEnv("CALENDAR_CREDENTIALS"))
if err != nil {
log.Fatalf("error starting calendar service %s", err)
return fmt.Errorf("error starting calendar service %s", err)
}
// starting server
@ -99,7 +103,7 @@ func run(c context.Context, getEnv func(string) string) error {
origins := strings.Split(ogs, ",")
srv, err := server.New(
booking.NewService(db), // TODO: should validate the booking service building
bs,
as,
ps,
cs,