rentease/thoughts/shared/plans/2025-10-03-stripe-payment-sync.md
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

11 KiB
Raw Blame History

Stripe Payment Sync Implementation Plan

Overview

Enable Stripe-backed payment ingestion so card transactions flow automatically into Rentease while preserving manual cash entry. The system will store Stripe identifiers, support manual/cron backfills, and react to Stripe webhooks for real-time updates and refunds.

Current State Analysis

  • Booking payments are entered manually via /payments/:id, persisting amount and payment_method only; no external identifiers exist (internal/server/handle_payments.go:16, internal/service/booking/payment.go:17).
  • Reports and booking summaries rely on totals from the payments table, especially card totals computed via SQL joins (internal/repository/booking/pg_store.go:65, internal/service/booking/report.go:76).
  • No Stripe configuration or drivers; config.Config lacks keys, and main.go wires only database, parser, pdf clients (internal/config/config.go:11, main.go:52).
  • Routes bundle payment handlers inside auth-protected groups, meaning Stripe webhooks need a dedicated public endpoint (internal/server/routes.go:8).
  • Cron binary exists for scheduled jobs but has no payment sync job (pkg/cron/cron.go:9, cmd/cron/main.go:13).

Desired End State

  • payments records include Stripe IDs and status for card transactions, allowing idempotent updates and refund tracking.
  • Configurable Stripe client (secret key, webhook secret) powers both range-based syncs and webhook ingestion.
  • Manual sync endpoint/job triggers Stripe List APIs for a given time window and upserts records; the job can be run via API or mounted in the cron scheduler.
  • Public webhook endpoint verifies Stripe signatures and handles payment_intent.succeeded and charge.refunded, updating local payments accordingly.
  • Manual payment modal remains for cash/cheque/transfer; card payments can now be imported automatically.

Key Discoveries

  • Manual payment creation flow is htmx-based and fully server-side (internal/view/booking_by_id.templ:130).
  • Repository lacks idempotent upsert capability; new helper needed to match on Stripe ID (internal/repository/booking/pg_store.go:150).
  • config.Host.PaymentMethods already enumerates methods shown in UI; Stripe integration must normalise to these values (internal/config/host.go:41).

Out of Scope

  • Customer-level Stripe data (customer objects, saved cards).
  • UI changes for displaying Stripe-specific metadata beyond status/method.
  • Handling non-card Stripe events (disputes, payouts) beyond refunds.

Implementation Approach

Augment the data model with Stripe identifiers, introduce a Stripe driver/service for syncing, expose both manual and scheduled sync triggers, and add a webhook for near real-time updates. Ensure idempotency via unique Stripe IDs and keep manual cash entry unaffected.

Phase 1: Data Model & Repository Extensions

Overview

Add fields to persist Stripe metadata and repository logic for idempotent upserts based on Stripe IDs.

Changes Required

File: internal/service/booking/models.go Changes: Add StripePaymentID *string and StripeStatus *string fields to Payment (nullable to support manual entries) with unique index on Stripe ID. Ensure gorm tags reflect uniqueness and index requirements.

File: internal/repository/booking/pg_store.go Changes:

  • Update CreatePayment to respect new fields.
  • Add UpsertStripePayment method: find by stripe_payment_id, update status/amount/method if exists; otherwise create.
  • Update GetPayment/UpdatePayment logic to preload new fields.

File: main.go Changes: Trigger database.Migrate after model changes to add new columns (AutoMigrate will handle column additions).

File: scripts/payment_migration.sql Changes: (Optional) Document SQL backfill to populate Stripe IDs later if needed.

// Payment model sketch
type Payment struct {
    gorm.Model
    BookingID       uint    `gorm:"not null;index"`
    Booking         Booking `gorm:"foreignKey:BookingID;constraint:OnDelete:CASCADE"`
    Amount          float64
    PaymentMethod   config.PaymentMethod
    StripePaymentID *string `gorm:"uniqueIndex"`
    StripeStatus    *string
}

Success Criteria

Automated Verification

  • go test ./internal/service/booking/...
  • go test ./internal/repository/booking/...
  • golangci-lint run

Manual Verification

  • Run application; AutoMigrate adds new columns without data loss.
  • Existing manual payment entry still works and shows in UI.

Phase 2: Stripe Client & Sync Service

Overview

Introduce Stripe SDK usage, configuration, and a service method to fetch payments for a time range, process them, and upsert into storage. Expose a job callable via API and cron.

Changes Required

File: go.mod Changes: Add github.com/stripe/stripe-go/v79 dependency (or latest version) plus go mod tidy.

File: internal/config/config.go Changes: Add fields StripeSecretKey, StripeWebhookSecret, optional StripeConnectAccount (if needed). Update env tags and documentation.

File: internal/driver/stripe/client.go (new) Changes: Implement wrapper around Stripe client to list PaymentIntent (or Charge) objects within a Created range, retrieving status, amount, currency, and metadata/booking reference if available.

File: internal/service/booking/payment.go Changes: Add SyncStripePayments(ctx, params) method that:

  • Calls driver to list payments between from/to timestamps.
  • Maps Stripe payment method/status to internal values.
  • Resolves booking association (e.g., via metadata booking_id or manual mapping; define behaviour if not found).
  • Calls repository UpsertStripePayment (with fallback logging if booking unknown).

File: internal/service/booking/service.go Changes: Extend interface with UpsertStripePayment and register driver in constructor (add parameter for Stripe client).

File: cmd/cron/main.go Changes: Add job (e.g., StripePaymentSync) that invokes service with last-run timestamp stored persistently (file/db) or via parameter. For initial implementation, fetch daily by default.

File: pkg/cron/cron.go Changes: Optionally enhance scheduler to accept context cancellation or job-specific config; ensure job invocation errors bubble through channels.

File: internal/server/handle_api_sync.go Changes: Add new handler handleStripeSync(bs *booking.Service) bound to POST /api/stripe/sync expecting payload (from, to). Validate API key via existing middleware and trigger sync job synchronously (or enqueue background routine).

Success Criteria

Automated Verification

  • Unit tests mocking Stripe client to validate SyncStripePayments mapping and repository calls.
  • Route handler tests confirming 401 protection and sync invocation (can use Echo context test helpers).
  • go test ./cmd/cron for scheduler wiring.

Manual Verification

  • Configure Stripe test key; run API sync for a test window; verify payments appear in booking detail.
  • Cron job logs success when run with shortened schedule in dev.

Phase 3: Webhook Endpoint & Event Processing

Overview

Accept Stripe webhook notifications to keep payment status current (successes and refunds) with signature verification.

Changes Required

File: internal/server/routes.go Changes: Register POST /webhooks/stripe prior to auth middleware so Stripe can reach it; ensure CORS is acceptable.

File: internal/server/handle_stripe_webhook.go (new) Changes:

  • Parse body, verify signature using StripeWebhookSecret via Stripe SDK helper.
  • Handle payment_intent.succeeded: locate booking association (metadata); call service upsert with status Succeeded.
  • Handle charge.refunded (and partial refunds) by updating payment amount/status (set StripeStatus = "refunded", adjust amounts if partial, record refund metadata).
  • Return 2xx quickly; log and capture errors.

File: internal/service/booking/payment.go Changes: Add helper ApplyStripeEvent to centralise webhook logic, reuse upsert.

File: internal/config/config.go Changes: Document required webhook secret env var.

File: main.go Changes: Inject Stripe client + webhook secret into server (extend server.New options or add to booking service).

Success Criteria

Automated Verification

  • Unit tests for webhook handler verifying signature failure -> 400, success -> upsert call.
  • Service tests ensuring refunds update amounts correctly.

Manual Verification

  • Use Stripe CLI stripe listen to forward events locally; confirm succeeded and refund events update bookings.
  • Invalid signature events rejected (check logs).

Phase 4: Admin Controls, UI Confirmation, and Regression Testing

Overview

Expose manual triggers, ensure UI reflects new automatic payments, and validate the mixed workflow.

Changes Required

File: internal/view/booking_by_id.templ Changes: Optionally add indicator for Stripe-synced payments (e.g., status badge) while keeping manual modal intact.

File: internal/server/handle_payments.go Changes: Ensure manual flow still sets StripePaymentID/StripeStatus nil. Consider preventing duplicate manual card entries by clarifying methods.

File: docs/ (new doc) Changes: Document Stripe setup, env vars, webhook configuration, manual sync API usage, cron schedule.

File: internal/server/handle_reports.go Changes: Confirm card totals include Stripe payments; adjust if status filtering needed (e.g., exclude refunded amounts from revenue).

Success Criteria

Automated Verification

  • Snapshot or template tests (if available) confirm payment list includes expected fields.
  • go test ./internal/view/... (if templ tests) or run templ generate to ensure templates compile.

Manual Verification

  • Booking page shows Stripe-imported payment with correct method/status.
  • Manual cash entry unaffected.
  • Documentation reviewed for completeness.

Testing Strategy

  • Unit tests mocking Stripe client and repository behaviour to ensure idempotency and status handling.
  • Integration test hitting /api/stripe/sync with fake Stripe driver to verify end-to-end upsert (could use in-memory driver for tests).
  • Webhook handler tests using Stripes official test helpers for signature verification.
  • Manual QA in Stripe test mode: create payment intent with metadata linking to booking ID, trigger success/refund, verify UI/report updates.

Performance Considerations

  • Stripe list API pagination: ensure sync job handles pagination and rate limits (backoff on errors).
  • Webhook handler should be lightweight; consider asynchronous processing (goroutine or queue) if high volume, though initial volume likely low.

Migration Notes

  • AutoMigrate adds nullable columns; ensure production deploy runs with Stripe env vars set (or feature toggled off until configured).
  • Consider backfilling existing Stripe payments manually post-deploy using the new API sync endpoint.

References

  • Research doc: thoughts/shared/research/2025-10-03-stripe-payment-sync.md
  • Manual payment handler: internal/server/handle_payments.go:16
  • Booking service payment logic: internal/service/booking/payment.go:17
  • Reports card totals: internal/repository/booking/pg_store.go:65