mirror of
https://github.com/rjNemo/rentease.git
synced 2026-06-11 13:16:50 +00:00
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.
136 lines
3.6 KiB
Go
136 lines
3.6 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stripe/stripe-go/v83"
|
|
)
|
|
|
|
type stubStripeEventService struct {
|
|
intentCalled bool
|
|
chargeCalled bool
|
|
err error
|
|
}
|
|
|
|
func (s *stubStripeEventService) HandlePaymentIntentSucceeded(ctx context.Context, pi *stripe.PaymentIntent) error {
|
|
s.intentCalled = true
|
|
return s.err
|
|
}
|
|
|
|
func (s *stubStripeEventService) HandleChargeRefunded(ctx context.Context, ch *stripe.Charge) error {
|
|
s.chargeCalled = true
|
|
return s.err
|
|
}
|
|
|
|
func TestHandleStripeWebhookPaymentIntent(t *testing.T) {
|
|
secret := "whsec_test"
|
|
payload := map[string]any{
|
|
"id": "evt_test",
|
|
"type": "payment_intent.succeeded",
|
|
"api_version": stripe.APIVersion,
|
|
"data": map[string]any{
|
|
"object": map[string]any{
|
|
"id": "pi_123",
|
|
"amount": 10000,
|
|
"amount_received": 10000,
|
|
"currency": "eur",
|
|
"status": "succeeded",
|
|
"metadata": map[string]string{"booking_id": "42"},
|
|
"payment_method_types": []string{"card"},
|
|
},
|
|
},
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal payload: %v", err)
|
|
}
|
|
|
|
restore := stubConstructEvent(stripe.EventTypePaymentIntentSucceeded, payloadBytes)
|
|
defer restore()
|
|
|
|
service := &stubStripeEventService{}
|
|
handler := handleStripeWebhook(service, secret)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/webhooks/stripe", strings.NewReader(string(payloadBytes)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Stripe-Signature", "test")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
|
|
if !service.intentCalled {
|
|
t.Fatalf("expected payment intent handler to be called")
|
|
}
|
|
}
|
|
|
|
func TestHandleStripeWebhookChargeRefunded(t *testing.T) {
|
|
secret := "whsec_test"
|
|
payload := map[string]any{
|
|
"id": "evt_charge",
|
|
"type": "charge.refunded",
|
|
"api_version": stripe.APIVersion,
|
|
"data": map[string]any{
|
|
"object": map[string]any{
|
|
"id": "ch_123",
|
|
"amount": 5000,
|
|
"amount_refunded": 5000,
|
|
"payment_intent": map[string]any{
|
|
"id": "pi_123",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
payloadBytes, _ := json.Marshal(payload)
|
|
restore := stubConstructEvent(stripe.EventTypeChargeRefunded, payloadBytes)
|
|
defer restore()
|
|
|
|
service := &stubStripeEventService{}
|
|
handler := handleStripeWebhook(service, secret)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/webhooks/stripe", strings.NewReader(string(payloadBytes)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Stripe-Signature", "test")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler(rec, req)
|
|
|
|
if !service.chargeCalled {
|
|
t.Fatalf("expected charge handler to be called")
|
|
}
|
|
}
|
|
|
|
func TestHandleStripeWebhookInvalidSignature(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/webhooks/stripe", strings.NewReader("{}"))
|
|
req.Header.Set("Stripe-Signature", "invalid")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler := handleStripeWebhook(&stubStripeEventService{}, "secret")
|
|
handler(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 status, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func stubConstructEvent(eventType stripe.EventType, payload []byte) func() {
|
|
original := constructEvent
|
|
constructEvent = func(_ []byte, _ string, _ string) (stripe.Event, error) {
|
|
event := stripe.Event{Type: eventType}
|
|
event.Data = &stripe.EventData{Raw: payload}
|
|
return event, nil
|
|
}
|
|
return func() { constructEvent = original }
|
|
}
|