package payment import ( "context" "errors" "fmt" "log/slog" "math" "strings" "time" "github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/driver/stripe" "github.com/rjNemo/rentease/internal/service/booking" ) // Payment fetches a payment by id. func (ps Service) Payment(id int) (*Payment, error) { p, err := ps.store.GetPayment(id) if err != nil { return nil, fmt.Errorf("fetch payment %d: %w", id, err) } return p, nil } // CreatePayment creates a manual payment for the provided booking id. func (ps Service) CreatePayment(bid int, amount float64, paymentMethod string) (*Payment, error) { p, err := ps.store.CreatePayment(&Payment{ BookingID: uint(bid), Amount: amount, PaymentMethod: config.PaymentMethod(paymentMethod), }) if err != nil { return nil, err } return p, nil } // UpdatePayment updates amount and method for the provided payment id. func (ps Service) UpdatePayment(id int, amount float64, paymentMethod string) (*Payment, error) { p, err := ps.store.UpdatePayment(id, amount, paymentMethod) if err != nil { return nil, err } return p, nil } // CreateStripePaymentLink generates a Stripe payment link for the outstanding balance of a booking. func (ps Service) CreateStripePaymentLink(ctx context.Context, bookingID int) (string, error) { if ps.stripe == nil { return "", ErrStripeClientNotConfigured } b, err := ps.store.Get(bookingID) if err != nil { return "", err } if b == nil || b.ID == 0 { return "", booking.ErrBookingNotFound } outstanding := calculateOutstandingBalance(b) if outstanding <= 0 { return "", ErrNoOutstandingBalance } description := fmt.Sprintf("Payment for booking %d", b.ID) if name := strings.TrimSpace(b.Name); name != "" { description = fmt.Sprintf("Payment for %s", name) } url, err := ps.stripe.CreatePaymentLink(ctx, stripe.CreatePaymentLinkParams{ Amount: outstanding, Currency: "eur", BookingID: uint(b.ID), Description: description, PaymentMethodTypes: []string{"card", "sepa_debit"}, }) if err != nil { return "", err } return url, nil } // SyncStripePayments pulls Stripe payments within the provided time window and // upserts them into the local datastore. Payments lacking booking metadata are skipped. func (ps Service) SyncStripePayments(ctx context.Context, from, to time.Time) error { if ps.stripe == nil { return ErrStripeClientNotConfigured } payments, err := ps.stripe.ListPayments(ctx, stripe.ListPaymentsParams{From: from, To: to}) if err != nil { return err } var multi error for _, payment := range payments { if payment.BookingID == nil { ps.logger.Warn("stripe payment missing booking metadata", slog.String("payment_id", payment.ID)) continue } bookingID := uint(*payment.BookingID) stripeID := payment.ID status := strings.ToLower(payment.Status) _, err = ps.store.UpsertStripePayment(&Payment{ BookingID: bookingID, Amount: payment.Amount, PaymentMethod: mapStripeMethod(payment.PaymentMethod), StripePaymentID: &stripeID, StripeStatus: &status, }) if err != nil { multi = errors.Join(multi, err) ps.logger.Error("failed to upsert stripe payment", slog.String("payment_id", payment.ID), slog.Any("error", err)) } } return multi } func calculateOutstandingBalance(b *booking.Booking) float64 { if b == nil { return 0 } var total float64 for _, item := range b.Items { total += item.Price * float64(item.Quantity) } var paid float64 for _, payment := range b.Payments { paid += payment.Amount } outstanding := total - paid outstanding = math.Round(outstanding*100) / 100 if outstanding < 0 { return 0 } return outstanding }