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 }