rentease/internal/driver/stripe/client.go

169 lines
4.1 KiB
Go

package stripe
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/stripe/stripe-go/v83"
)
// 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 *stripe.Client
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 := 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))
}
if c.account != "" {
listParams.SetStripeAccount(c.account)
}
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
}
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
}