mirror of
https://github.com/rjNemo/rentease.git
synced 2026-06-10 04:36:50 +00:00
Introduce Stripe integration for automatic payment ingestion and refund tracking. Adds new fields to the payment model for Stripe IDs and status, Stripe client driver, sync service, cron job, manual API endpoint, and public webhook handler for real-time updates. Includes tests and documentation. Manual cash entry remains supported.
166 lines
4 KiB
Go
166 lines
4 KiB
Go
package stripe
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
stripe "github.com/stripe/stripe-go/v79"
|
|
stripeclient "github.com/stripe/stripe-go/v79/client"
|
|
)
|
|
|
|
// Option configures a Client instance.
|
|
type Option func(*Client)
|
|
|
|
// WithAccount sets the Stripe connected account identifier used for requests.
|
|
func WithAccount(account string) Option {
|
|
return func(c *Client) {
|
|
c.account = strings.TrimSpace(account)
|
|
}
|
|
}
|
|
|
|
// 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 *stripeclient.API
|
|
account string
|
|
}
|
|
|
|
// 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 := stripeclient.New(trimmed, nil)
|
|
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))
|
|
}
|
|
|
|
if c.account != "" {
|
|
listParams.SetStripeAccount(c.account)
|
|
}
|
|
|
|
iter := c.api.PaymentIntents.List(listParams)
|
|
payments := make([]Payment, 0)
|
|
|
|
for iter.Next() {
|
|
pi := iter.PaymentIntent()
|
|
if pi == nil {
|
|
continue
|
|
}
|
|
|
|
payments = append(payments, NormalizePaymentIntent(pi))
|
|
}
|
|
|
|
if err := iter.Err(); err != nil {
|
|
return nil, fmt.Errorf("stripe payment intents iteration failed: %w", err)
|
|
}
|
|
|
|
return payments, 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
|
|
}
|