mirror of
https://github.com/rjNemo/rentease.git
synced 2026-06-12 13:46:51 +00:00
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:
parent
04be887ad8
commit
d4e6b35a96
7 changed files with 65 additions and 47 deletions
|
|
@ -1,9 +1,9 @@
|
||||||
FROM golang:1.23-alpine AS builder
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
RUN apk update && apk add --no-cache \
|
RUN apk update && apk add --no-cache \
|
||||||
build-base \
|
build-base \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& update-ca-certificates
|
&& update-ca-certificates
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ type HostItem struct {
|
||||||
// If true, the item will be added to the calendar
|
// If true, the item will be added to the calendar
|
||||||
MustSyncCalendar bool
|
MustSyncCalendar bool
|
||||||
HasEndDate bool
|
HasEndDate bool
|
||||||
// If true, a tax item will be added to the invoice
|
// Amount of taxes in EUR. If not zero, a tax item will be added to the invoice
|
||||||
Taxable bool // TODO: create taxes auto if taxable item
|
Taxes float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHost() *Host {
|
func NewHost() *Host {
|
||||||
|
|
@ -47,7 +47,7 @@ func NewHost() *Host {
|
||||||
CalendarId: os.Getenv("CALENDAR_ID_T2"),
|
CalendarId: os.Getenv("CALENDAR_ID_T2"),
|
||||||
MustSyncCalendar: true,
|
MustSyncCalendar: true,
|
||||||
HasEndDate: true,
|
HasEndDate: true,
|
||||||
Taxable: true,
|
Taxes: 1.5,
|
||||||
},
|
},
|
||||||
|
|
||||||
"T3": {
|
"T3": {
|
||||||
|
|
@ -56,7 +56,7 @@ func NewHost() *Host {
|
||||||
CalendarId: os.Getenv("CALENDAR_ID_T3"),
|
CalendarId: os.Getenv("CALENDAR_ID_T3"),
|
||||||
MustSyncCalendar: true,
|
MustSyncCalendar: true,
|
||||||
HasEndDate: true,
|
HasEndDate: true,
|
||||||
Taxable: true,
|
Taxes: 1.5,
|
||||||
},
|
},
|
||||||
"Airport": {
|
"Airport": {
|
||||||
Name: "Airport",
|
Name: "Airport",
|
||||||
|
|
@ -70,10 +70,6 @@ func NewHost() *Host {
|
||||||
Name: "Transport",
|
Name: "Transport",
|
||||||
Price: 20.0,
|
Price: 20.0,
|
||||||
},
|
},
|
||||||
"Taxes": { // TODO: remove after auto creation enabled
|
|
||||||
Name: "Taxes",
|
|
||||||
Price: 1.5,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ type Service struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(db *gorm.DB) *Service {
|
func NewService(db *gorm.DB) (*Service, error) {
|
||||||
return &Service{db: db}
|
return &Service{db: db}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bs Service) All() []*Line {
|
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
|
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{
|
i := &Item{
|
||||||
BookingId: bid,
|
BookingId: bookingId,
|
||||||
Item: item,
|
Item: item.Name,
|
||||||
Quantity: qty,
|
Quantity: quantity,
|
||||||
Price: price,
|
Price: price,
|
||||||
PaymentMethod: method,
|
PaymentMethod: paymentMethod,
|
||||||
}
|
}
|
||||||
_ = bs.db.Create(i)
|
_ = 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 {
|
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)
|
arrivalDate := extractDate(`Date d'arrivée `, content)
|
||||||
departureDate := extractDate(`Date de départ `, content)
|
departureDate := extractDate(`Date de départ `, content)
|
||||||
stayLength := extractInt(`Durée de séjour : (\d+) nuits`, 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 := extractString(`Nom du client : \n\s+([\w\s]+)`, content)
|
||||||
customerName = strings.SplitN(customerName, "\n", 2)[0]
|
customerName = strings.SplitN(customerName, "\n", 2)[0]
|
||||||
customerEmail := extractString(`[\w\.\-]+@[\w\.\-]+\.\w+`, content)
|
customerEmail := extractString(`[\w\.\-]+@[\w\.\-]+\.\w+`, content)
|
||||||
customerNumber := extractInt(`Nombre de personnes : \s*\n\s*(\d+)`, content)
|
customerNumber := extractInt(`Nombre de personnes : \s*\n\s*(\d+)`, content)
|
||||||
commissionAmount := extractFloat(`Commission : € (\d+,\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)
|
externalId := extractString(`Numéro de réservation : \n\s+(\d+)`, content)
|
||||||
standardRate := extractFloat(`Standard Rate\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)
|
b := bs.Create(*formatDate(arrivalDate), *formatDate(departureDate), customerName, "", customerEmail, "Booking", customerNumber, commissionAmount, &externalId)
|
||||||
bs.CreateItem(b.Id, item, stayLength, standardRate, "Card")
|
if item, ok := config.NewHost().Items[itemName]; ok {
|
||||||
bs.CreateItem(b.Id, "Taxes", int(taxQty), 1.5, "Cash")
|
bs.CreateItem(b.Id, item, stayLength, standardRate, "Card", customerNumber)
|
||||||
|
}
|
||||||
|
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ func JobMonthlyBookingReport() error {
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
log.Println("Start Monthly Booking Report job at:", 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(
|
ps, err := pdf.NewPdfService(
|
||||||
os.Getenv("HTMLDOCS_PROJECT_ID"),
|
os.Getenv("HTMLDOCS_PROJECT_ID"),
|
||||||
|
|
|
||||||
|
|
@ -139,13 +139,13 @@ func handleBookingPage(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
|
||||||
return sum + i.Price*float64(i.Quantity)
|
return sum + i.Price*float64(i.Quantity)
|
||||||
}, 0.0), 'f', 2, 64),
|
}, 0.0), 'f', 2, 64),
|
||||||
Platforms: hc.Platforms,
|
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 {
|
for _, item := range items {
|
||||||
out = append(out, item.Name)
|
out = append(out, item.Name)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}(hc.Items),
|
}(hc.Items),
|
||||||
func(l, r string) bool { return l < r },
|
func(l, r string) bool { return l > r },
|
||||||
),
|
),
|
||||||
PaymentMethods: hc.PaymentMethods,
|
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)
|
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(
|
if err = cs.Create(
|
||||||
itm.CalendarId,
|
itm.CalendarId,
|
||||||
|
|
@ -269,16 +269,20 @@ func handleCreateItem(bs *booking.Service, cs *calendar.Service, hc *config.Host
|
||||||
captureError(c, err)
|
captureError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderTempl(c, http.StatusCreated, view.LineItem(&view.ItemViewModel{
|
for _, i := range newItems {
|
||||||
Id: strconv.Itoa(i.Id),
|
_ = renderTempl(c, http.StatusCreated, view.LineItem(&view.ItemViewModel{
|
||||||
Item: i.Item,
|
Id: strconv.Itoa(i.Id),
|
||||||
Quantity: strconv.Itoa(i.Quantity),
|
Item: i.Item,
|
||||||
Price: strconv.FormatFloat(i.Price, 'f', 2, 64),
|
Quantity: strconv.Itoa(i.Quantity),
|
||||||
PaymentMethod: i.PaymentMethod,
|
Price: strconv.FormatFloat(i.Price, 'f', 2, 64),
|
||||||
PaymentStatus: i.PaymentStatus,
|
PaymentMethod: i.PaymentMethod,
|
||||||
SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
|
PaymentStatus: i.PaymentStatus,
|
||||||
ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.Id),
|
SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
|
||||||
}))
|
ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.Id),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ func PublicLayout() templ.Component {
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
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 {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +95,7 @@ func PublicLayout() templ.Component {
|
||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(l.Name)
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(l.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|
@ -121,7 +121,7 @@ func PublicLayout() templ.Component {
|
||||||
var templ_7745c5c3_Var5 string
|
var templ_7745c5c3_Var5 string
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(hvm.Name)
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(hvm.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|
@ -143,7 +143,7 @@ func PublicLayout() templ.Component {
|
||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(hvm.PhoneNumber)
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(hvm.PhoneNumber)
|
||||||
if templ_7745c5c3_Err != nil {
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|
@ -165,7 +165,7 @@ func PublicLayout() templ.Component {
|
||||||
var templ_7745c5c3_Var9 string
|
var templ_7745c5c3_Var9 string
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(hvm.Email)
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(hvm.Email)
|
||||||
if templ_7745c5c3_Err != nil {
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|
|
||||||
12
main.go
12
main.go
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
"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)
|
return fmt.Errorf("error connecting to the database %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// build booking service
|
|
||||||
err = db.AutoMigrate(&booking.Booking{}, &booking.BookingRequest{}, &booking.Item{})
|
err = db.AutoMigrate(&booking.Booking{}, &booking.BookingRequest{}, &booking.Item{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error migrating the database %s", err)
|
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
|
// build pdf service
|
||||||
ps, err := pdf.NewPdfService(
|
ps, err := pdf.NewPdfService(
|
||||||
getEnv("HTMLDOCS_PROJECT_ID"),
|
getEnv("HTMLDOCS_PROJECT_ID"),
|
||||||
|
|
@ -85,7 +89,7 @@ func run(c context.Context, getEnv func(string) string) error {
|
||||||
// build calendar service
|
// build calendar service
|
||||||
cs, err := calendar.NewService(ctx, getEnv("CALENDAR_CREDENTIALS"))
|
cs, err := calendar.NewService(ctx, getEnv("CALENDAR_CREDENTIALS"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error starting calendar service %s", err)
|
return fmt.Errorf("error starting calendar service %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// starting server
|
// starting server
|
||||||
|
|
@ -99,7 +103,7 @@ func run(c context.Context, getEnv func(string) string) error {
|
||||||
origins := strings.Split(ogs, ",")
|
origins := strings.Split(ogs, ",")
|
||||||
|
|
||||||
srv, err := server.New(
|
srv, err := server.New(
|
||||||
booking.NewService(db), // TODO: should validate the booking service building
|
bs,
|
||||||
as,
|
as,
|
||||||
ps,
|
ps,
|
||||||
cs,
|
cs,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue