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) { linkParams, err := buildPaymentLinkCreateParams(params) if err != nil { return "", err } 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 buildPaymentLinkCreateParams(params CreatePaymentLinkParams) (*stripe.PaymentLinkCreateParams, error) { if params.Amount <= 0 { return nil, errors.New("amount must be greater than zero") } currency := strings.ToLower(strings.TrimSpace(params.Currency)) if currency == "" { return nil, errors.New("currency is required") } if params.BookingID == 0 { return nil, errors.New("booking id is required") } amountCents := int64(math.Round(params.Amount * 100)) metadata := map[string]string{ "booking_id": strconv.FormatUint(uint64(params.BookingID), 10), } 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: metadata, PaymentIntentData: &stripe.PaymentLinkCreatePaymentIntentDataParams{ Metadata: metadata, }, } 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) } return linkParams, 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 }