mirror of
https://github.com/rjNemo/rentease.git
synced 2026-06-11 13:16:50 +00:00
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.
218 lines
5.7 KiB
Go
218 lines
5.7 KiB
Go
package stripe
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/stripe/stripe-go/v83"
|
|
)
|
|
|
|
// Option configures a Client instance.
|
|
type Option func(*Client)
|
|
|
|
// Client wraps Stripe's SDK to expose the subset of functionality needed by the
|
|
// application while keeping the rest of the codebase decoupled from the SDK.
|
|
type Client struct {
|
|
api *stripe.Client
|
|
}
|
|
|
|
// New constructs a Client using the provided secret key. The key must not be empty.
|
|
func New(secretKey string, opts ...Option) (*Client, error) {
|
|
trimmed := strings.TrimSpace(secretKey)
|
|
if trimmed == "" {
|
|
return nil, errors.New("stripe secret key is required")
|
|
}
|
|
|
|
api := stripe.NewClient(trimmed)
|
|
client := &Client{api: api}
|
|
for _, opt := range opts {
|
|
opt(client)
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
// Payment represents the subset of payment intent data consumed by the booking service.
|
|
type Payment struct {
|
|
ID string
|
|
Amount float64
|
|
Currency string
|
|
Status string
|
|
PaymentMethod string
|
|
BookingID *uint
|
|
Created time.Time
|
|
}
|
|
|
|
// NormalizePaymentIntent converts a Stripe payment intent into the simplified Payment structure used
|
|
// by the application. Fields that are absent default to their zero values.
|
|
func NormalizePaymentIntent(pi *stripe.PaymentIntent) Payment {
|
|
if pi == nil {
|
|
return Payment{}
|
|
}
|
|
|
|
amount := float64(pi.AmountReceived) / 100.0
|
|
if amount == 0 {
|
|
amount = float64(pi.Amount) / 100.0
|
|
}
|
|
|
|
return Payment{
|
|
ID: pi.ID,
|
|
Amount: amount,
|
|
Currency: strings.ToUpper(string(pi.Currency)),
|
|
Status: string(pi.Status),
|
|
PaymentMethod: deriveMethod(pi),
|
|
BookingID: extractBookingID(pi.Metadata),
|
|
Created: time.Unix(int64(pi.Created), 0),
|
|
}
|
|
}
|
|
|
|
// ListPaymentsParams defines the time boundaries used when fetching Stripe payments.
|
|
type ListPaymentsParams struct {
|
|
From time.Time
|
|
To time.Time
|
|
}
|
|
|
|
// ListPayments fetches payment intents created within the provided time range. The
|
|
// results are normalised into Payment structs suitable for downstream processing.
|
|
func (c *Client) ListPayments(ctx context.Context, params ListPaymentsParams) ([]Payment, error) {
|
|
listParams := &stripe.PaymentIntentListParams{}
|
|
listParams.Context = ctx
|
|
listParams.AddExpand("data.latest_charge")
|
|
listParams.AddExpand("data.payment_method")
|
|
|
|
if !params.From.IsZero() {
|
|
listParams.Filters.AddFilter("created", "gte", strconv.FormatInt(params.From.Unix(), 10))
|
|
}
|
|
if !params.To.IsZero() {
|
|
listParams.Filters.AddFilter("created", "lte", strconv.FormatInt(params.To.Unix(), 10))
|
|
}
|
|
|
|
seq := c.api.V1PaymentIntents.List(ctx, listParams)
|
|
payments := make([]Payment, 0)
|
|
var listErr error
|
|
|
|
seq(func(pi *stripe.PaymentIntent, err error) bool {
|
|
if err != nil {
|
|
listErr = err
|
|
return false
|
|
}
|
|
if pi == nil {
|
|
return true
|
|
}
|
|
payments = append(payments, NormalizePaymentIntent(pi))
|
|
return true
|
|
})
|
|
|
|
if listErr != nil {
|
|
return nil, fmt.Errorf("stripe payment intents iteration failed: %w", listErr)
|
|
}
|
|
|
|
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 ""
|
|
}
|
|
|
|
if pi.LatestCharge != nil && pi.LatestCharge.PaymentMethodDetails != nil {
|
|
typ := pi.LatestCharge.PaymentMethodDetails.Type
|
|
if typ != "" {
|
|
return string(typ)
|
|
}
|
|
}
|
|
|
|
if pi.PaymentMethod != nil && pi.PaymentMethod.Type != "" {
|
|
return string(pi.PaymentMethod.Type)
|
|
}
|
|
|
|
if len(pi.PaymentMethodTypes) > 0 {
|
|
return pi.PaymentMethodTypes[0]
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func extractBookingID(metadata map[string]string) *uint {
|
|
if len(metadata) == 0 {
|
|
return nil
|
|
}
|
|
|
|
keys := []string{"booking_id", "bookingId", "bookingID"}
|
|
for _, key := range keys {
|
|
if raw, ok := metadata[key]; ok {
|
|
if raw == "" {
|
|
continue
|
|
}
|
|
if id, err := strconv.ParseUint(raw, 10, 32); err == nil {
|
|
value := uint(id)
|
|
return &value
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|