mirror of
https://github.com/rjNemo/rentease.git
synced 2026-06-06 02:36:49 +00:00
feat(stripe): add payment link creation for bookings
Some checks are pending
CI / checks (push) Waiting to run
Some checks are pending
CI / checks (push) Waiting to run
Introduce backend and frontend support for generating Stripe payment links for outstanding booking balances. Adds a new POST endpoint to create payment links, updates booking view to include a Stripe button, and integrates error handling and feedback for payment link creation. Refactors view models and templates to support the new feature.
This commit is contained in:
parent
5cfe9b4169
commit
91a9a74750
10 changed files with 327 additions and 104 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -113,6 +114,66 @@ func (c *Client) ListPayments(ctx context.Context, params ListPaymentsParams) ([
|
|||
return payments, nil
|
||||
}
|
||||
|
||||
// CreatePaymentLinkParams encapsulates the data required to generate a Stripe payment link.
|
||||
type CreatePaymentLinkParams struct {
|
||||
Amount float64
|
||||
Currency string
|
||||
BookingID uint
|
||||
Description string
|
||||
PaymentMethodTypes []string
|
||||
}
|
||||
|
||||
// CreatePaymentLink creates a payment link for the provided booking metadata and amount.
|
||||
func (c *Client) CreatePaymentLink(ctx context.Context, params CreatePaymentLinkParams) (string, error) {
|
||||
if params.Amount <= 0 {
|
||||
return "", errors.New("amount must be greater than zero")
|
||||
}
|
||||
currency := strings.ToLower(strings.TrimSpace(params.Currency))
|
||||
if currency == "" {
|
||||
return "", errors.New("currency is required")
|
||||
}
|
||||
if params.BookingID == 0 {
|
||||
return "", errors.New("booking id is required")
|
||||
}
|
||||
|
||||
amountCents := int64(math.Round(params.Amount * 100))
|
||||
|
||||
linkParams := &stripe.PaymentLinkCreateParams{
|
||||
LineItems: []*stripe.PaymentLinkCreateLineItemParams{
|
||||
{
|
||||
PriceData: &stripe.PaymentLinkCreateLineItemPriceDataParams{
|
||||
Currency: stripe.String(currency),
|
||||
ProductData: &stripe.PaymentLinkCreateLineItemPriceDataProductDataParams{
|
||||
Name: stripe.String(strings.TrimSpace(params.Description)),
|
||||
},
|
||||
UnitAmount: stripe.Int64(amountCents),
|
||||
},
|
||||
Quantity: stripe.Int64(1),
|
||||
},
|
||||
},
|
||||
Metadata: map[string]string{
|
||||
"booking_id": strconv.FormatUint(uint64(params.BookingID), 10),
|
||||
},
|
||||
}
|
||||
|
||||
if linkParams.LineItems[0].PriceData.ProductData.Name == nil || *linkParams.LineItems[0].PriceData.ProductData.Name == "" {
|
||||
linkParams.LineItems[0].PriceData.ProductData.Name = stripe.String(fmt.Sprintf("Booking %d", params.BookingID))
|
||||
}
|
||||
|
||||
if len(params.PaymentMethodTypes) > 0 {
|
||||
linkParams.PaymentMethodTypes = stripe.StringSlice(params.PaymentMethodTypes)
|
||||
}
|
||||
|
||||
linkParams.Context = ctx
|
||||
|
||||
pl, err := c.api.V1PaymentLinks.Create(ctx, linkParams)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create payment link: %w", err)
|
||||
}
|
||||
|
||||
return pl.URL, nil
|
||||
}
|
||||
|
||||
func deriveMethod(pi *stripe.PaymentIntent) string {
|
||||
if pi == nil {
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package server
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
|
|
@ -142,21 +143,22 @@ func handleBookingPage(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
|
|||
eid = *b.ExternalID
|
||||
}
|
||||
bvm := &view.BookingViewModel{
|
||||
Id: b.InvoiceNumber(hc),
|
||||
Name: b.Name,
|
||||
PhoneNumber: b.PhoneNumber,
|
||||
CustomerNumber: strconv.Itoa(b.CustomerNumber),
|
||||
Email: b.Email,
|
||||
From: b.From.Format(time.DateOnly),
|
||||
To: b.To.Format(time.DateOnly),
|
||||
Platform: string(b.Platform),
|
||||
ExternalId: eid,
|
||||
Canceled: b.Canceled,
|
||||
PlatformFees: strconv.FormatFloat(b.PlatformFees, 'f', 2, 64),
|
||||
Url: templ.EscapeString(fmt.Sprintf("%s/%d", constant.RouteBooking, b.ID)),
|
||||
PdfUrl: templ.SafeURL(fmt.Sprintf("%s/pdf/%d", constant.RouteBooking, b.ID)),
|
||||
CancelUrl: fmt.Sprintf("%s/%d/cancel", constant.RouteBooking, b.ID),
|
||||
PaymentUrl: fmt.Sprintf("%s/%d", constant.RoutePayment, b.ID),
|
||||
Id: b.InvoiceNumber(hc),
|
||||
Name: b.Name,
|
||||
PhoneNumber: b.PhoneNumber,
|
||||
CustomerNumber: strconv.Itoa(b.CustomerNumber),
|
||||
Email: b.Email,
|
||||
From: b.From.Format(time.DateOnly),
|
||||
To: b.To.Format(time.DateOnly),
|
||||
Platform: string(b.Platform),
|
||||
ExternalId: eid,
|
||||
Canceled: b.Canceled,
|
||||
PlatformFees: strconv.FormatFloat(b.PlatformFees, 'f', 2, 64),
|
||||
Url: templ.EscapeString(fmt.Sprintf("%s/%d", constant.RouteBooking, b.ID)),
|
||||
PdfUrl: templ.SafeURL(fmt.Sprintf("%s/pdf/%d", constant.RouteBooking, b.ID)),
|
||||
CancelUrl: fmt.Sprintf("%s/%d/cancel", constant.RouteBooking, b.ID),
|
||||
PaymentUrl: fmt.Sprintf("%s/%d", constant.RoutePayment, b.ID),
|
||||
StripePaymentLinkUrl: fmt.Sprintf("%s/%d/stripe/payment-link", constant.RouteBooking, b.ID),
|
||||
Items: view.ItemListViewModel{
|
||||
Items: u.Map(b.Items, func(i booking.Item) view.ItemViewModel {
|
||||
return view.ItemViewModel{
|
||||
|
|
@ -191,6 +193,32 @@ func handleBookingPage(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func handleBookingStripePaymentLink(bs *booking.Service) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid booking id")
|
||||
}
|
||||
|
||||
url, err := bs.CreateStripePaymentLink(c.Request().Context(), id)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, booking.ErrStripeClientNotConfigured):
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "stripe is not configured")
|
||||
case errors.Is(err, booking.ErrBookingNotFound):
|
||||
return echo.NewHTTPError(http.StatusNotFound, "booking not found")
|
||||
case errors.Is(err, booking.ErrNoOutstandingBalance):
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "booking has no outstanding balance")
|
||||
default:
|
||||
return fmt.Errorf("failed to create payment link: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusCreated, map[string]string{"url": url})
|
||||
}
|
||||
}
|
||||
|
||||
func handleBookingUpdate(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
type UpdateBooking struct {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ func (s Server) MountHandlers() {
|
|||
private.GET("/bookings/new", handleBookingCreatePage(s.hc))
|
||||
private.POST("/bookings/new", handleBookingCreate(s.bs))
|
||||
private.GET("/bookings/:id", handleBookingPage(s.bs, s.hc))
|
||||
private.POST("/bookings/:id/stripe/payment-link", handleBookingStripePaymentLink(s.bs))
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -79,19 +79,6 @@ func NewRouter(fs embed.FS, debug bool, secret string, origins []string) *echo.E
|
|||
// config
|
||||
e.HideBanner = !debug
|
||||
e.Debug = debug
|
||||
e.HTTPErrorHandler = func(err error, c echo.Context) {
|
||||
captureError(c, err)
|
||||
|
||||
code := http.StatusInternalServerError
|
||||
var he *echo.HTTPError
|
||||
if errors.As(err, &he) {
|
||||
code = he.Code
|
||||
}
|
||||
errorPage := fmt.Sprintf("assets/html/HTTP%d.html", code)
|
||||
if err := c.File(errorPage); err != nil {
|
||||
c.Logger().Error(err)
|
||||
}
|
||||
}
|
||||
// middlewares
|
||||
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||
Format: "${time_rfc3339} [${method}: ${status}] ${uri}; ip=${remote_ip}; ${latency_human}; ${user_agent}\n",
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ type Store interface {
|
|||
|
||||
type StripeClient interface {
|
||||
ListPayments(ctx context.Context, params stripeclient.ListPaymentsParams) ([]stripeclient.Payment, error)
|
||||
CreatePaymentLink(ctx context.Context, params stripeclient.CreatePaymentLinkParams) (string, error)
|
||||
}
|
||||
|
||||
type PdfClient interface {
|
||||
|
|
|
|||
75
internal/service/booking/stripe_payment_link.go
Normal file
75
internal/service/booking/stripe_payment_link.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package booking
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
stripeclient "github.com/rjNemo/rentease/internal/driver/stripe"
|
||||
)
|
||||
|
||||
// ErrBookingNotFound indicates that a booking could not be retrieved from the datastore.
|
||||
var ErrBookingNotFound = errors.New("booking not found")
|
||||
|
||||
// ErrNoOutstandingBalance indicates that the booking has already been fully paid.
|
||||
var ErrNoOutstandingBalance = errors.New("booking has no outstanding balance")
|
||||
|
||||
// CreateStripePaymentLink generates a Stripe payment link for the outstanding balance of a booking.
|
||||
func (bs Service) CreateStripePaymentLink(ctx context.Context, bookingID int) (string, error) {
|
||||
if bs.stripe == nil {
|
||||
return "", ErrStripeClientNotConfigured
|
||||
}
|
||||
|
||||
b := bs.store.Get(bookingID)
|
||||
if b == nil || b.ID == 0 {
|
||||
return "", ErrBookingNotFound
|
||||
}
|
||||
|
||||
outstanding := calculateOutstandingBalance(b)
|
||||
if outstanding <= 0 {
|
||||
return "", ErrNoOutstandingBalance
|
||||
}
|
||||
|
||||
description := fmt.Sprintf("Payment for booking %d", b.ID)
|
||||
if name := strings.TrimSpace(b.Name); name != "" {
|
||||
description = fmt.Sprintf("Payment for %s", name)
|
||||
}
|
||||
|
||||
url, err := bs.stripe.CreatePaymentLink(ctx, stripeclient.CreatePaymentLinkParams{
|
||||
Amount: outstanding,
|
||||
Currency: "eur",
|
||||
BookingID: uint(b.ID),
|
||||
Description: description,
|
||||
PaymentMethodTypes: []string{"card", "sepa_debit"},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func calculateOutstandingBalance(b *Booking) float64 {
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var total float64
|
||||
for _, item := range b.Items {
|
||||
total += item.Price * float64(item.Quantity)
|
||||
}
|
||||
|
||||
var paid float64
|
||||
for _, payment := range b.Payments {
|
||||
paid += payment.Amount
|
||||
}
|
||||
|
||||
outstanding := total - paid
|
||||
outstanding = math.Round(outstanding*100) / 100
|
||||
if outstanding < 0 {
|
||||
return 0
|
||||
}
|
||||
return outstanding
|
||||
}
|
||||
|
|
@ -22,6 +22,10 @@ func (f *fakeStripeClient) ListPayments(ctx context.Context, params stripeclient
|
|||
return f.payments, f.err
|
||||
}
|
||||
|
||||
func (f *fakeStripeClient) CreatePaymentLink(ctx context.Context, params stripeclient.CreatePaymentLinkParams) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
type mockStore struct {
|
||||
upserts []*Payment
|
||||
err error
|
||||
|
|
|
|||
|
|
@ -22,14 +22,6 @@ templ BookingById(booking *BookingViewModel) {
|
|||
>
|
||||
<img src="/static/icons/whatsapp.png" class="w-6 h-6"/>
|
||||
</a>
|
||||
<a
|
||||
href="https://dashboard.stripe.com/payments/new"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="btn btn-ghost btn-sm btn-square"
|
||||
>
|
||||
<img src="/static/icons/stripe.png" class="w-6 h-6"/>
|
||||
</a>
|
||||
if booking.Canceled {
|
||||
<span class="badge badge-error">Canceled</span>
|
||||
} else {
|
||||
|
|
@ -43,13 +35,24 @@ templ BookingById(booking *BookingViewModel) {
|
|||
@BookingForm(*booking)
|
||||
</section>
|
||||
<section class="card bg-base-100 shadow-md p-6 mb-8">
|
||||
<hgroup class="flex justify-between">
|
||||
<hgroup class="flex justify-between items-center">
|
||||
<h2
|
||||
class="text-xl font-semibold mb-4 border-b pb-2 border-base-content/10"
|
||||
>
|
||||
Line Items
|
||||
</h2>
|
||||
<button class="btn btn-secondary btn-sm " onclick="document.getElementById('payment_modal').showModal()">Add Payment</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-secondary btn-sm " onclick="document.getElementById('payment_modal').showModal()">Add Payment</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm btn-square"
|
||||
data-payment-link-url={ booking.StripePaymentLinkUrl }
|
||||
aria-label="Create Stripe payment link"
|
||||
onclick="createStripePaymentLink(event)"
|
||||
>
|
||||
<img src="/static/icons/stripe.png" class="w-6 h-6"/>
|
||||
</button>
|
||||
</div>
|
||||
</hgroup>
|
||||
<div class="overflow-x-auto mb-6">
|
||||
<table class="table w-full">
|
||||
|
|
@ -123,6 +126,55 @@ templ BookingById(booking *BookingViewModel) {
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
async function createStripePaymentLink(event) {
|
||||
const button = event.currentTarget;
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
const endpoint = button.dataset.paymentLinkUrl;
|
||||
if (!endpoint) {
|
||||
return;
|
||||
}
|
||||
button.disabled = true;
|
||||
button.classList.add("loading");
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const message = await extractStripePaymentLinkError(response);
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data && data.url) {
|
||||
window.open(data.url, "_blank", "noopener");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create Stripe payment link", error);
|
||||
alert("Unable to create the Stripe payment link. Please try again.");
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.classList.remove("loading");
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStripePaymentLinkError(response) {
|
||||
try {
|
||||
const payload = await response.json();
|
||||
if (payload && payload.message) {
|
||||
return payload.message;
|
||||
}
|
||||
} catch (_error) {
|
||||
// ignore parsing errors
|
||||
}
|
||||
return response.statusText || "Unexpected error while creating payment link.";
|
||||
}
|
||||
</script>
|
||||
@PaymentModal(booking.PaymentUrl)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ func BookingById(booking *BookingViewModel) templ.Component {
|
|||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" target=\"_blank\">Create PDF</a> <a href=\"https://web.whatsapp.com/\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"btn btn-ghost btn-sm btn-square\"><img src=\"/static/icons/whatsapp.png\" class=\"w-6 h-6\"></a> <a href=\"https://dashboard.stripe.com/payments/new\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"btn btn-ghost btn-sm btn-square\"><img src=\"/static/icons/stripe.png\" class=\"w-6 h-6\"></a> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" target=\"_blank\">Create PDF</a> <a href=\"https://web.whatsapp.com/\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"btn btn-ghost btn-sm btn-square\"><img src=\"/static/icons/whatsapp.png\" class=\"w-6 h-6\"></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ func BookingById(booking *BookingViewModel) templ.Component {
|
|||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(booking.CancelUrl)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 36, Col: 66}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 28, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
|
@ -121,7 +121,20 @@ func BookingById(booking *BookingViewModel) templ.Component {
|
|||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</section><section class=\"card bg-base-100 shadow-md p-6 mb-8\"><hgroup class=\"flex justify-between\"><h2 class=\"text-xl font-semibold mb-4 border-b pb-2 border-base-content/10\">Line Items</h2><button class=\"btn btn-secondary btn-sm \" onclick=\"document.getElementById('payment_modal').showModal()\">Add Payment</button></hgroup><div class=\"overflow-x-auto mb-6\"><table class=\"table w-full\"><thead><tr><th>Item</th><th>Quantity</th><th>Price (€)</th><th>Payment Method</th><th>Sub-total (€)</th><th class=\"text-right\">Actions</th></tr></thead>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</section><section class=\"card bg-base-100 shadow-md p-6 mb-8\"><hgroup class=\"flex justify-between items-center\"><h2 class=\"text-xl font-semibold mb-4 border-b pb-2 border-base-content/10\">Line Items</h2><div class=\"flex items-center gap-2\"><button class=\"btn btn-secondary btn-sm \" onclick=\"document.getElementById('payment_modal').showModal()\">Add Payment</button> <button type=\"button\" class=\"btn btn-ghost btn-sm btn-square\" data-payment-link-url=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(booking.StripePaymentLinkUrl)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 49, Col: 58}
|
||||
}
|
||||
_, 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, 10, "\" aria-label=\"Create Stripe payment link\" onclick=\"createStripePaymentLink(event)\"><img src=\"/static/icons/stripe.png\" class=\"w-6 h-6\"></button></div></hgroup><div class=\"overflow-x-auto mb-6\"><table class=\"table w-full\"><thead><tr><th>Item</th><th>Quantity</th><th>Price (€)</th><th>Payment Method</th><th>Sub-total (€)</th><th class=\"text-right\">Actions</th></tr></thead>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
|
@ -129,69 +142,69 @@ func BookingById(booking *BookingViewModel) templ.Component {
|
|||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<tfoot><tr><td colspan=\"4\" class=\"text-right font-bold\">Total:</td><td colspan=\"2\" class=\"font-bold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(booking.Total)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 70, Col: 56}
|
||||
}
|
||||
_, 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, 11, "</td></tr></tfoot></table></div></section><div class=\"border-t pt-4 border-base-content/10\"><h3 class=\"text-lg font-semibold mb-3\">Add New Line Item</h3><form hx-post=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<tfoot><tr><td colspan=\"4\" class=\"text-right font-bold\">Total:</td><td colspan=\"2\" class=\"font-bold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s/items", booking.Url))
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(booking.Total)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 79, Col: 50}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 73, Col: 56}
|
||||
}
|
||||
_, 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, 12, "\" hx-target=\"#line-items\" hx-swap=\"afterend\" hx-on::after-request=\"if(event.detail.successful) this.reset()\" class=\"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 items-end\"><div class=\"form-control w-full\"><label class=\"label\" for=\"new-line-item\"><span class=\"label-text\">Item</span></label> <select class=\"select select-bordered w-full\" name=\"item\" id=\"new-line-item\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</td></tr></tfoot></table></div></section><div class=\"border-t pt-4 border-base-content/10\"><h3 class=\"text-lg font-semibold mb-3\">Add New Line Item</h3><form hx-post=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s/items", booking.Url))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 82, Col: 50}
|
||||
}
|
||||
_, 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, 13, "\" hx-target=\"#line-items\" hx-swap=\"afterend\" hx-on::after-request=\"if(event.detail.successful) this.reset()\" class=\"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 items-end\"><div class=\"form-control w-full\"><label class=\"label\" for=\"new-line-item\"><span class=\"label-text\">Item</span></label> <select class=\"select select-bordered w-full\" name=\"item\" id=\"new-line-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range booking.ItemList {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<option value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 91, Col: 27}
|
||||
}
|
||||
_, 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, 14, "\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<option value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 91, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 94, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</option>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 94, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</select></div><div class=\"form-control w-full\"><label class=\"label\" for=\"new-line-quantity\"><span class=\"label-text\">Quantity</span></label> <input type=\"number\" name=\"quantity\" id=\"new-line-quantity\" required class=\"input input-bordered w-full\"></div><div class=\"form-control w-full\"><label class=\"label\" for=\"new-line-price\"><span class=\"label-text\">Price (€)</span></label> <input type=\"number\" name=\"price\" inputmode=\"decimal\" step=\"0.01\" id=\"new-line-price\" required class=\"input input-bordered w-full\"></div><div class=\"flex justify-end md:justify-start\"><button type=\"submit\" class=\"btn btn-secondary\">Add Line Item</button></div></form></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</select></div><div class=\"form-control w-full\"><label class=\"label\" for=\"new-line-quantity\"><span class=\"label-text\">Quantity</span></label> <input type=\"number\" name=\"quantity\" id=\"new-line-quantity\" required class=\"input input-bordered w-full\"></div><div class=\"form-control w-full\"><label class=\"label\" for=\"new-line-price\"><span class=\"label-text\">Price (€)</span></label> <input type=\"number\" name=\"price\" inputmode=\"decimal\" step=\"0.01\" id=\"new-line-price\" required class=\"input input-bordered w-full\"></div><div class=\"flex justify-end md:justify-start\"><button type=\"submit\" class=\"btn btn-secondary\">Add Line Item</button></div></form></div><script>\n\t\t\tasync function createStripePaymentLink(event) {\n\t\t\t\tconst button = event.currentTarget;\n\t\t\t\tif (!button) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst endpoint = button.dataset.paymentLinkUrl;\n\t\t\t\tif (!endpoint) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tbutton.disabled = true;\n\t\t\t\tbutton.classList.add(\"loading\");\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await fetch(endpoint, {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\"Accept\": \"application/json\",\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tif (!response.ok) {\n\t\t\t\t\t\tconst message = await extractStripePaymentLinkError(response);\n\t\t\t\t\t\talert(message);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst data = await response.json();\n\t\t\t\t\tif (data && data.url) {\n\t\t\t\t\t\twindow.open(data.url, \"_blank\", \"noopener\");\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\"Failed to create Stripe payment link\", error);\n\t\t\t\t\talert(\"Unable to create the Stripe payment link. Please try again.\");\n\t\t\t\t} finally {\n\t\t\t\t\tbutton.disabled = false;\n\t\t\t\t\tbutton.classList.remove(\"loading\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tasync function extractStripePaymentLinkError(response) {\n\t\t\t\ttry {\n\t\t\t\t\tconst payload = await response.json();\n\t\t\t\t\tif (payload && payload.message) {\n\t\t\t\t\t\treturn payload.message;\n\t\t\t\t\t}\n\t\t\t\t} catch (_error) {\n\t\t\t\t\t// ignore parsing errors\n\t\t\t\t}\n\t\t\t\treturn response.statusText || \"Unexpected error while creating payment link.\";\n\t\t\t}\n\t\t</script> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
|
@ -225,25 +238,25 @@ func PaymentModal(paymentUrl string) templ.Component {
|
|||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var11 == nil {
|
||||
templ_7745c5c3_Var11 = templ.NopComponent
|
||||
templ_7745c5c3_Var12 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var12 == nil {
|
||||
templ_7745c5c3_Var12 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<dialog id=\"payment_modal\" class=\"modal\"><div class=\"modal-box\"><h3 class=\"text-lg font-bold\">Add Payment</h3><form class=\"py-4 space-y-4\" hx-post=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<dialog id=\"payment_modal\" class=\"modal\"><div class=\"modal-box\"><h3 class=\"text-lg font-bold\">Add Payment</h3><form class=\"py-4 space-y-4\" hx-post=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(paymentUrl)
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(paymentUrl)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 136, Col: 24}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 188, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" hx-target=\"#payment-lines\" hx-swap=\"outerHTML\" hx-on::after-request=\"if(event.detail.successful) payment_modal.close()\"><div class=\"form-control w-full\"><label class=\"label\"><span class=\"label-text\">Amount</span></label> <input type=\"number\" step=\"0.01\" name=\"amount\" class=\"input input-bordered w-full\" required autofocus></div><div class=\"form-control w-full\"><label class=\"label\"><span class=\"label-text\">Payment Method</span></label> <select name=\"paymentMethod\" class=\"select select-bordered w-full\" required><option value=\"\">Select payment method</option> <option value=\"Cash\">Cash</option> <option value=\"Card\">Card</option> <option value=\"Cheque\">Cheque</option> <option value=\"Transfer\">Bank Transfer</option></select></div><button type=\"submit\" class=\"btn btn-primary\">Add Payment </button></form></div></dialog>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" hx-target=\"#payment-lines\" hx-swap=\"outerHTML\" hx-on::after-request=\"if(event.detail.successful) payment_modal.close()\"><div class=\"form-control w-full\"><label class=\"label\"><span class=\"label-text\">Amount</span></label> <input type=\"number\" step=\"0.01\" name=\"amount\" class=\"input input-bordered w-full\" required autofocus></div><div class=\"form-control w-full\"><label class=\"label\"><span class=\"label-text\">Payment Method</span></label> <select name=\"paymentMethod\" class=\"select select-bordered w-full\" required><option value=\"\">Select payment method</option> <option value=\"Cash\">Cash</option> <option value=\"Card\">Card</option> <option value=\"Cheque\">Cheque</option> <option value=\"Transfer\">Bank Transfer</option></select></div><button type=\"submit\" class=\"btn btn-primary\">Add Payment </button></form></div></dialog>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,26 +6,27 @@ import (
|
|||
)
|
||||
|
||||
type BookingViewModel struct {
|
||||
Id string
|
||||
Name string
|
||||
PhoneNumber string
|
||||
CustomerNumber string
|
||||
Email string
|
||||
Canceled bool
|
||||
From string
|
||||
To string
|
||||
Platform string
|
||||
ExternalId string
|
||||
Platforms []string
|
||||
PlatformFees string
|
||||
Items ItemListViewModel
|
||||
ItemList []string
|
||||
PaymentMethods []config.PaymentMethod
|
||||
Url string
|
||||
PdfUrl templ.SafeURL
|
||||
PaymentUrl string
|
||||
CancelUrl string
|
||||
Total string
|
||||
Id string
|
||||
Name string
|
||||
PhoneNumber string
|
||||
CustomerNumber string
|
||||
Email string
|
||||
Canceled bool
|
||||
From string
|
||||
To string
|
||||
Platform string
|
||||
ExternalId string
|
||||
Platforms []string
|
||||
PlatformFees string
|
||||
Items ItemListViewModel
|
||||
ItemList []string
|
||||
PaymentMethods []config.PaymentMethod
|
||||
Url string
|
||||
PdfUrl templ.SafeURL
|
||||
PaymentUrl string
|
||||
StripePaymentLinkUrl string
|
||||
CancelUrl string
|
||||
Total string
|
||||
}
|
||||
|
||||
type PaymentViewModel struct {
|
||||
|
|
|
|||
Loading…
Reference in a new issue