rentease/internal/service/booking/stripe_sync_test.go
Ruidy 8384d85e3e
feat(stripe): add Stripe payment sync and webhook support
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.
2025-10-03 21:39:59 +02:00

165 lines
5 KiB
Go

package booking
import (
"context"
"errors"
"log/slog"
"testing"
"time"
"gorm.io/gorm"
"github.com/rjNemo/rentease/internal/config"
stripeclient "github.com/rjNemo/rentease/internal/driver/stripe"
)
type fakeStripeClient struct {
payments []stripeclient.Payment
err error
}
func (f *fakeStripeClient) ListPayments(ctx context.Context, params stripeclient.ListPaymentsParams) ([]stripeclient.Payment, error) {
return f.payments, f.err
}
type mockStore struct {
upserts []*Payment
err error
byStripeID map[string]*Payment
}
func (m *mockStore) record(p *Payment) (*Payment, error) {
cp := *p
m.upserts = append(m.upserts, &cp)
if cp.StripePaymentID != nil {
if m.byStripeID == nil {
m.byStripeID = make(map[string]*Payment)
}
clone := cp
m.byStripeID[*cp.StripePaymentID] = &clone
}
if m.err != nil {
return nil, m.err
}
return &cp, nil
}
func (m *mockStore) All() []*Line { return nil }
func (m *mockStore) Search(string) []*Line { return nil }
func (m *mockStore) List(time.Time, time.Time) ([]*Line, error) { return nil, nil }
func (m *mockStore) CardTotal(time.Time, time.Time) (float64, error) { return 0, nil }
func (m *mockStore) Get(int) *Booking { return nil }
func (m *mockStore) Create(*Booking) error { return nil }
func (m *mockStore) Update(*Booking) error { return nil }
func (m *mockStore) Cancel(int) error { return nil }
func (m *mockStore) CreateItem(*Item) error { return nil }
func (m *mockStore) PayItem(int) (*Item, error) { return nil, nil }
func (m *mockStore) GetItem(int) (*Item, error) { return nil, nil }
func (m *mockStore) UpdateItem(int, string, string, string, int, float64) (*Item, error) {
return nil, nil
}
func (m *mockStore) CreatePayment(*Payment) (*Payment, error) { return nil, nil }
func (m *mockStore) GetPayment(int) (*Payment, error) { return nil, nil }
func (m *mockStore) UpdatePayment(int, float64, string) (*Payment, error) { return nil, nil }
func (m *mockStore) UpsertStripePayment(p *Payment) (*Payment, error) { return m.record(p) }
func (m *mockStore) FindStripePayment(id string) (*Payment, error) {
if m.byStripeID == nil {
return nil, gorm.ErrRecordNotFound
}
if p, ok := m.byStripeID[id]; ok {
clone := *p
return &clone, nil
}
return nil, gorm.ErrRecordNotFound
}
func TestSyncStripePayments(t *testing.T) {
bookingID := uint(42)
stripePayments := []stripeclient.Payment{
{
ID: "pi_123",
Amount: 120.50,
PaymentMethod: "card",
Status: "succeeded",
BookingID: &bookingID,
},
}
store := &mockStore{}
stripe := &fakeStripeClient{payments: stripePayments}
logger := slog.New(slog.DiscardHandler)
svc, err := NewService(logger, store, nil, nil, stripe)
if err != nil {
t.Fatalf("NewService returned error: %v", err)
}
if err := svc.SyncStripePayments(context.Background(), time.Now().Add(-time.Hour), time.Now()); err != nil {
t.Fatalf("SyncStripePayments returned error: %v", err)
}
if len(store.upserts) != 1 {
t.Fatalf("expected 1 upsert, got %d", len(store.upserts))
}
upsert := store.upserts[0]
if upsert.Amount != 120.50 {
t.Errorf("unexpected amount: %v", upsert.Amount)
}
if upsert.PaymentMethod != config.PaymentMethod("Card") {
t.Errorf("unexpected payment method: %v", upsert.PaymentMethod)
}
if upsert.StripePaymentID == nil || *upsert.StripePaymentID != "pi_123" {
t.Errorf("stripe payment id not set correctly: %v", upsert.StripePaymentID)
}
}
func TestSyncStripePaymentsSkipsMissingBooking(t *testing.T) {
stripePayments := []stripeclient.Payment{
{ID: "pi_123", Amount: 10},
}
store := &mockStore{}
stripe := &fakeStripeClient{payments: stripePayments}
logger := slog.New(slog.DiscardHandler)
svc, err := NewService(logger, store, nil, nil, stripe)
if err != nil {
t.Fatalf("NewService returned error: %v", err)
}
if err := svc.SyncStripePayments(context.Background(), time.Now().Add(-time.Hour), time.Now()); err != nil {
t.Fatalf("SyncStripePayments returned error: %v", err)
}
if len(store.upserts) != 0 {
t.Fatalf("expected 0 upserts, got %d", len(store.upserts))
}
}
func TestSyncStripePaymentsReturnsAggregatedError(t *testing.T) {
bookingID := uint(7)
stripePayments := []stripeclient.Payment{
{
ID: "pi_err",
Amount: 50,
PaymentMethod: "card",
Status: "succeeded",
BookingID: &bookingID,
},
}
store := &mockStore{err: errors.New("db failure")}
stripe := &fakeStripeClient{payments: stripePayments}
logger := slog.New(slog.DiscardHandler)
svc, err := NewService(logger, store, nil, nil, stripe)
if err != nil {
t.Fatalf("NewService returned error: %v", err)
}
err = svc.SyncStripePayments(context.Background(), time.Now().Add(-time.Hour), time.Now())
if err == nil {
t.Fatalf("expected error, got nil")
}
}