mirror of
https://github.com/rjNemo/rentease.git
synced 2026-06-11 13:16:50 +00:00
Some checks are pending
CI / checks (push) Waiting to run
Introduce backend and frontend support for generating Stripe payment links for outstanding booking balances. Adds a new POST endpoint to create payment links, updates booking view to include a Stripe button, and integrates error handling and feedback for payment link creation. Refactors view models and templates to support the new feature.
169 lines
5.1 KiB
Go
169 lines
5.1 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
|
|
}
|
|
|
|
func (f *fakeStripeClient) CreatePaymentLink(ctx context.Context, params stripeclient.CreatePaymentLinkParams) (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|