Compare commits

..

No commits in common. "a0b7672e9eda3ac3ae03f416e2e51b2c4e921a0c" and "508de011160cc311fa33d6e2ee176152310fb1cc" have entirely different histories.

10 changed files with 74 additions and 109 deletions

View file

@ -25,18 +25,19 @@ dev: ## Build and run the dev container with live reload (Air)
$(DOCKER_RUN_ENV) $(NAME):dev
test: ## Run Go tests inside the running dev container
go test ./...
docker exec $(NAME) go test ./...
up-deps: ## Update Go dependencies on host
go get -u ./...
format: ## Generate templ files and format code locally
templ generate internal/view
templ fmt .
go fmt ./...
format: ## Generate templ files and format code (dev container must be running)
docker exec $(NAME) templ generate internal/view
docker exec $(NAME) templ fmt .
docker exec $(NAME) go fmt ./...
lint: ## Lint the code using golangci-lint locally
golangci-lint run ./...
lint: ## Lint the code using golangci-lint (dev container must be running)
docker exec $(NAME) golangci-lint run ./...
stop: ## Stop the dev container
-@docker stop $(NAME) >/dev/null 2>&1 || true

View file

@ -74,10 +74,9 @@ use a cloud alternative such as Railway, fly.io, _etc._
# Stripe configuration (optional until you enable automatic sync)
APP_STRIPE_SECRET_KEY=sk_test_your_key
APP_STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
APP_STRIPE_ACCOUNT_ID=acct_your_account_id
```
Leave the Stripe variables blank to continue using manual cash entry only. When set, Rentease will pull payments from Stripe, process webhooks sent to `/webhooks/stripe`, and expose a manual sync endpoint at `POST /api/stripe/sync` (protected by the existing API key middleware). Providing `APP_STRIPE_ACCOUNT_ID` also enables dashboard links for synchronized card payments.
Leave the Stripe variables blank to continue using manual cash entry only. When set, Rentease will pull payments from Stripe, process webhooks sent to `/webhooks/stripe`, and expose a manual sync endpoint at `POST /api/stripe/sync` (protected by the existing API key middleware).
5. Start the application

View file

@ -3,18 +3,17 @@ package config
import "os"
type Host struct {
Items map[string]HostItem
Name string
Address string
City string
ZipCode string
PhoneNumber string
Email string
InvoicePrefix string
PaymentMethods []PaymentMethod
Platforms []Platform
CustomerSeed int
StripeAccountID string
Items map[string]HostItem
Name string
Address string
City string
ZipCode string
PhoneNumber string
Email string
InvoicePrefix string
PaymentMethods []PaymentMethod
Platforms []Platform
CustomerSeed int
}
type HostItem struct {
@ -31,17 +30,16 @@ type HostItem struct {
func NewHost() *Host {
return &Host{
Name: "VillaFleurie",
Address: "4 rue Gerty Archimede",
City: "Le Gosier",
ZipCode: "97190",
PhoneNumber: "+590 690 44 15 30",
Email: "location.villafleurie@gmail.com",
CustomerSeed: 239,
InvoicePrefix: "VFNI",
PaymentMethods: []PaymentMethod{"Card", "Cash", "Cheque", "Transfer"}, // TODO: add to DB
Platforms: []Platform{"Booking", "AirBnb", "TripAdvisor", "Other"}, // TODO: add to DB
StripeAccountID: os.Getenv("APP_STRIPE_ACCOUNT_ID"),
Name: "VillaFleurie",
Address: "4 rue Gerty Archimede",
City: "Le Gosier",
ZipCode: "97190",
PhoneNumber: "+590 690 44 15 30",
Email: "location.villafleurie@gmail.com",
CustomerSeed: 239,
InvoicePrefix: "VFNI",
PaymentMethods: []PaymentMethod{"Card", "Cash", "Cheque", "Transfer"}, // TODO: add to DB
Platforms: []Platform{"Booking", "AirBnb", "TripAdvisor", "Other"}, // TODO: add to DB
Items: map[string]HostItem{ // TODO: move to DB
"T2": {
Name: "T2",

View file

@ -63,23 +63,17 @@ func handleBookingListPage(bs *booking.Service, hc *config.Host) http.HandlerFun
}
}
func paymentViewModelFromBookingPayment(p booking.Payment, stripeAccountID string) *view.PaymentViewModel {
func paymentViewModelFromBookingPayment(p booking.Payment) *view.PaymentViewModel {
stripeStatus := ""
if p.StripeStatus != nil {
stripeStatus = *p.StripeStatus
}
var stripeDashboardURL string
if string(p.PaymentMethod) == "Card" && stripeAccountID != "" && p.StripePaymentID != nil && *p.StripePaymentID != "" {
stripeDashboardURL = fmt.Sprintf("https://dashboard.stripe.com/%s/payments/%s", stripeAccountID, *p.StripePaymentID)
}
return &view.PaymentViewModel{
Amount: strconv.FormatFloat(p.Amount, 'f', 2, 64),
PaymentMethod: string(p.PaymentMethod),
PaymentUrl: fmt.Sprintf("%s/%d", constant.RoutePayment, p.ID),
StripeStatus: stripeStatus,
StripeDashboardURL: stripeDashboardURL,
Amount: strconv.FormatFloat(p.Amount, 'f', 2, 64),
PaymentMethod: string(p.PaymentMethod),
PaymentUrl: fmt.Sprintf("%s/%d", constant.RoutePayment, p.ID),
StripeStatus: stripeStatus,
}
}
@ -205,7 +199,7 @@ func handleBookingPage(bs *booking.Service, hc *config.Host) http.HandlerFunc {
}
}),
Payments: u.Map(b.Payments, func(p booking.Payment) view.PaymentViewModel {
return *paymentViewModelFromBookingPayment(p, hc.StripeAccountID)
return *paymentViewModelFromBookingPayment(p)
}),
},
Total: strconv.FormatFloat(u.Reduce(b.Items, func(i booking.Item, sum float64) float64 {
@ -499,7 +493,7 @@ func handleItemUpdate(bs *booking.Service) http.HandlerFunc {
}
}
func handlePaymentUpdate(bs *booking.Service, hc *config.Host) http.HandlerFunc {
func handlePaymentUpdate(bs *booking.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@ -523,7 +517,7 @@ func handlePaymentUpdate(bs *booking.Service, hc *config.Host) http.HandlerFunc
p := bs.UpdatePayment(id, amount, r.FormValue("paymentMethod"))
if err := renderTempl(w, http.StatusOK, view.PaymentLine(paymentViewModelFromBookingPayment(*p, hc.StripeAccountID))); err != nil {
if err := renderTempl(w, http.StatusOK, view.PaymentLine(paymentViewModelFromBookingPayment(*p))); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

View file

@ -7,12 +7,11 @@ import (
"github.com/go-chi/chi/v5"
u "github.com/rjNemo/underscore"
"github.com/rjNemo/rentease/internal/config"
"github.com/rjNemo/rentease/internal/service/booking"
"github.com/rjNemo/rentease/internal/view"
)
func handleCreatePayment(bs *booking.Service, hc *config.Host) http.HandlerFunc {
func handleCreatePayment(bs *booking.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@ -45,7 +44,7 @@ func handleCreatePayment(bs *booking.Service, hc *config.Host) http.HandlerFunc
component := view.PaymentList(
u.Map(nb.Payments, func(p booking.Payment) *view.PaymentViewModel {
return paymentViewModelFromBookingPayment(p, hc.StripeAccountID)
return paymentViewModelFromBookingPayment(p)
}),
)
@ -55,7 +54,7 @@ func handleCreatePayment(bs *booking.Service, hc *config.Host) http.HandlerFunc
}
}
func handlePaymentForm(bs *booking.Service, hc *config.Host) http.HandlerFunc {
func handlePaymentForm(bs *booking.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil {
@ -64,7 +63,7 @@ func handlePaymentForm(bs *booking.Service, hc *config.Host) http.HandlerFunc {
}
p := bs.OnePayment(id)
form := view.PaymentForm(paymentViewModelFromBookingPayment(*p, hc.StripeAccountID))
form := view.PaymentForm(paymentViewModelFromBookingPayment(*p))
if err := renderTempl(w, http.StatusOK, form); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}

View file

@ -43,9 +43,9 @@ func (s *Server) MountHandlers() {
r.Get("/reports/do", handleReportCompute(s.bs, s.hc))
r.Get("/reports/pdf", handlePdfCreateReport(s.bs))
r.Post("/payments/{id}", handleCreatePayment(s.bs, s.hc))
r.Put("/payments/{id}", handlePaymentUpdate(s.bs, s.hc))
r.Get("/payments/{id}", handlePaymentForm(s.bs, s.hc))
r.Post("/payments/{id}", handleCreatePayment(s.bs))
r.Put("/payments/{id}", handlePaymentUpdate(s.bs))
r.Get("/payments/{id}", handlePaymentForm(s.bs))
})
}

View file

@ -10,17 +10,11 @@ func (bs Service) ParseFromAPI(rawContent string) (*Booking, error) {
return nil, err
}
items := b.Items
itm := b.Items[0]
b = bs.Create(b.From, b.To, b.Name, b.PhoneNumber, b.Email, string(b.Platform), b.CustomerNumber, b.PlatformFees, b.ExternalID)
hostItems := config.NewHost().Items
for _, itm := range items {
hostItem, ok := hostItems[itm.Item]
if !ok {
continue
}
bs.CreateItem(b.ID, hostItem, itm.Quantity, itm.Price, itm.PaymentMethod, b.CustomerNumber, string(b.Platform))
if item, ok := config.NewHost().Items[itm.Item]; ok {
bs.CreateItem(b.ID, item, itm.Quantity, itm.Price, itm.PaymentMethod, b.CustomerNumber, string(b.Platform))
}
return b, nil

View file

@ -30,9 +30,8 @@ type BookingViewModel struct {
}
type PaymentViewModel struct {
Amount string
PaymentMethod string
PaymentUrl string
StripeStatus string
StripeDashboardURL string
Amount string
PaymentMethod string
PaymentUrl string
StripeStatus string
}

View file

@ -7,16 +7,7 @@ templ PaymentLine(payment *PaymentViewModel) {
<td>- { payment.Amount }</td>
<td>
{ payment.PaymentMethod }
if payment.StripeDashboardURL != "" {
<a
href={ payment.StripeDashboardURL }
target="_blank"
rel="noreferrer noopener"
class="badge badge-outline badge-sm ml-2"
>
View in Stripe
</a>
}
<span class="badge badge-outline badge-sm ml-2">{ payment.StripeStatus }</span>
</td>
<td></td>
<td class="flex gap-2">

View file

@ -55,43 +55,33 @@ func PaymentLine(payment *PaymentViewModel) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " <span class=\"badge badge-outline badge-sm ml-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if payment.StripeDashboardURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(payment.StripeDashboardURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/payment.templ`, Line: 12, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"badge badge-outline badge-sm ml-2\">View in Stripe</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(payment.StripeStatus)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/payment.templ`, Line: 10, Col: 73}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</td><td></td><td class=\"flex gap-2\"><button class=\"btn btn-sm btn-outline\" hx-get=\"")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span></td><td></td><td class=\"flex gap-2\"><button class=\"btn btn-sm btn-outline\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(payment.PaymentUrl)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/payment.templ`, Line: 25, Col: 31}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/payment.templ`, Line: 16, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" hx-target=\"closest tr\" hx-swap=\"outerHTML\">Edit</button></td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" hx-target=\"closest tr\" hx-swap=\"outerHTML\">Edit</button></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -120,46 +110,46 @@ func PaymentForm(payment *PaymentViewModel) templ.Component {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<tr class=\"hover\"><form hx-put=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<tr class=\"hover\"><form hx-put=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(payment.PaymentUrl)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/payment.templ`, Line: 37, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/payment.templ`, Line: 28, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" id=\"edit-payment\" hx-target=\"closest tr\" hx-swap=\"outerHTML\"><td></td><td></td><td><input class=\"input input-bordered input-sm w-full\" type=\"number\" inputmode=\"decimal\" step=\"0.01\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" id=\"edit-payment\" hx-target=\"closest tr\" hx-swap=\"outerHTML\"><td></td><td></td><td><input class=\"input input-bordered input-sm w-full\" type=\"number\" inputmode=\"decimal\" step=\"0.01\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(payment.Amount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/payment.templ`, Line: 46, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/payment.templ`, Line: 37, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" name=\"amount\" form=\"edit-payment\"></td><td><input class=\"input input-bordered input-sm w-full\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" name=\"amount\" form=\"edit-payment\"></td><td><input class=\"input input-bordered input-sm w-full\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(payment.PaymentMethod)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/payment.templ`, Line: 54, Col: 34}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/payment.templ`, Line: 45, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" name=\"paymentMethod\" form=\"edit-payment\"></td><td></td><td><button class=\"btn btn-sm btn-primary\" type=\"submit\" form=\"edit-payment\">Save</button></td></form></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" name=\"paymentMethod\" form=\"edit-payment\"></td><td></td><td><button class=\"btn btn-sm btn-primary\" type=\"submit\" form=\"edit-payment\">Save</button></td></form></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -188,7 +178,7 @@ func PaymentList(payments []*PaymentViewModel) templ.Component {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<tbody id=\"payment-lines\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<tbody id=\"payment-lines\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -200,7 +190,7 @@ func PaymentList(payments []*PaymentViewModel) templ.Component {
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</tbody>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}