link,[object Object]
Skip to content

Stripe Integration

Overview

AcqMarketplace integrates with Stripe for subscription management, payment processing, and billing operations. The integration includes automatic subscription synchronization, real-time status monitoring, and comprehensive error handling.

Architecture

Core Components

  1. Subscription Management

    • useSubscription hook - Main subscription state management
    • useSubscriptionMonitor hook - Background monitoring and manual sync
    • BillingTab component - User-facing subscription interface
  2. Edge Functions

    Subscription & Billing:

    • check-subscription - Syncs subscription status with Stripe
    • stripe-sync - Comprehensive Stripe data synchronization
    • daily-subscription-sync - Automated daily subscription checks
    • create-checkout - Creates subscription checkout sessions
    • customer-portal - Manages billing portal access
    • stripe-webhook - Handles Stripe webhook events
    • sync-invoices - Syncs invoice data from Stripe

    Stripe Connect (Marketplace):

    • connect-dashboard-link - Generates Stripe Connect dashboard links
    • connect-onboarding-link - Creates onboarding links for sellers
    • refresh-kyc-status - Updates KYC verification status

    Payment Processing:

    • create-deal-checkout - Creates checkout for deal payments
    • create-deal-payment - Processes deal payments
    • payment-status - Checks payment status
    • refund-deal-payment - Handles payment refunds
    • release-deal-payout - Releases funds to sellers

    Utility Functions:

    • get-price-id - Retrieves Stripe price IDs
    • test-stripe - Stripe integration testing
    • auto-fix-subscription-dates - Fixes subscription date inconsistencies
  3. Database Tables

    • subscriptions - Primary subscription data (Stripe source of truth)
    • profiles - User profile with subscription fields (synced from subscriptions)

Subscription Sync System

Automatic Synchronization

On Authentication

When a user successfully logs in, the system automatically:

  1. Triggers check-subscription Edge Function
  2. Syncs current subscription status with Stripe
  3. Updates both subscriptions and profiles tables
  4. Shows immediate notification if subscription has actually expired and there was a previous paid period (no deduplication; new users without a prior period do NOT see a toast)
typescript
// AuthContext.tsx - Automatic sync on login
if (session?.user && event === 'SIGNED_IN') {
  window.requestIdleCallback(async () => {
    queryClient.invalidateQueries({ queryKey: ['subscription'] });
    const { data, error } = await supabase.functions.invoke('check-subscription');
    // Handle sync result and notifications
  }, { timeout: 1000 });
}

Background Monitoring

The useSubscriptionMonitor hook provides:

  • Periodic Checks: Every 60 minutes in background
  • Manual Trigger: "Check Status" button for instant sync (no notification)
  • Smart Throttling: Prevents excessive API calls
  • Notification Deduplication: Shows notification only if 55+ minutes passed since last one
  • Error Handling: Graceful failure with user feedback
typescript
// useSubscriptionMonitor.ts - Background monitoring
const SUBSCRIPTION_CHECK_INTERVAL = 60 * 60 * 1000; // 60 minutes

useEffect(() => {
  const intervalId = setInterval(() => {
    checkSubscriptionStatus();
  }, SUBSCRIPTION_CHECK_INTERVAL);
  
  return () => clearInterval(intervalId);
}, [user, checkSubscriptionStatus]);

Data Consistency

Priority System

The system uses a clear data priority hierarchy:

  1. Primary Source: subscriptions table (Stripe data)
  2. Fallback: profiles table (synced data)
  3. Default: Free plan if no data exists
typescript
// BillingTab.tsx - Data priority
const rawLevel = subscription?.subscription_level || 'free';
const rawStatus = subscription?.subscription_status || 'inactive';
const rawEnd = subscription?.current_period_end || 
               profile?.subscription_current_period_end || 
               profile?.subscription_end_date;

Sync Process

The check-subscription function ensures data consistency:

  1. Fetch from Stripe: Gets latest subscription data
  2. Update subscriptions table: Primary data store
  3. Update profiles table: Sync all relevant fields
  4. Return consistent data: Unified response format

Duplicate Subscription Guards (New)

Problem: Stripe allows multiple active subscriptions for the same customer, which led to duplicates (e.g., Starter + Pro in parallel) and UI confusion.

Changes implemented:

  • UI guard in PricingPlans:

    • If user already has an active subscription (hasActiveSubscription), the purchase button NO LONGER opens Checkout.
    • Instead, redirects to Account Settings → Billing for management.
    • Code: src/components/pricing/PricingPlans.tsx:133-142.
  • Server guard in create-checkout Edge Function:

    • Before creating a Checkout session, verifies in Stripe the existence of an ACTIVE subscription for the customer.
    • If it exists, does NOT create a new checkout, but generates a Billing Portal session and returns the portal URL (reason: 'already_active').
    • Code: supabase/functions/create-checkout/index.ts:127-151 (block that lists stripe.subscriptions.list({ status: 'active' }) and returns billingPortal.sessions.create).
  • Plan selection rule in UI (useSubscription):

    • When multiple subscriptions exist in DB, we choose deterministically:
      1. Prefer ACTIVE ones
      2. Among active ones, prefer LOWER price (Starter before Pro)
      3. At equal price, choose the most recent current_period_end
      4. Fallback: most recently registered
    • Code: src/hooks/useSubscription.ts:70-92.

Impact:

  • Prevents subscription duplication on purchase
  • UI displays "Current Plan" consistently (doesn't over-report Pro when Starter is active)
  • Maintains control through portal for upgrade/downgrade/cancellation

Invoice Date Logic (New)

Context: all synchronized invoices appeared to be from the same day due to using finalized_at/created for created_at.

Change:

  • sync-invoices was adjusted to use paid_at (payment date) as priority, then finalized_at, then created.
  • Code: supabase/functions/sync-invoices/index.ts:75-91 (calculation of chosenSec from status_transitions.paid_at || finalized_at || created).

Result: the "Created" column in UI correctly reflects actual payment moments, not aggregated on the same date.

Stripe Connect Integration

Marketplace Payments

AcqMarketplace uses Stripe Connect to facilitate payments between buyers and sellers:

Seller Onboarding

  • Connect Onboarding: Sellers complete Stripe Connect onboarding
  • KYC Verification: Required for payout eligibility (separate from platform verification)
  • Dashboard Access: Sellers can manage their Stripe account

Dual Verification System

AcqMarketplace implements two complementary verification systems:

  1. Platform Verification (seller_verification_status):

    • Identity and legitimacy verification
    • Required for listing visibility
    • Managed by platform admins
  2. Stripe KYC (kyc_status):

    • Financial compliance verification
    • Required for receiving payouts
    • Managed by Stripe Connect

Both verifications are required for full platform access and payout eligibility.

Payment Flow

  1. Deal Creation: Buyer initiates deal with seller
  2. Payment Processing: Funds held in escrow via Stripe
  3. Due Diligence: Both parties complete verification
  4. Fund Release: Automatic payout to seller upon completion

KYC Management

  • Status Tracking: Real-time KYC verification status
  • Compliance: Ensures regulatory compliance
  • Payout Control: KYC required for fund releases

Connect Functions

typescript
// Connect dashboard access
const { data } = await supabase.functions.invoke('connect-dashboard-link');

// KYC status refresh
const { data } = await supabase.functions.invoke('refresh-kyc-status');

Payment Processing

Deal Payments

The system handles complex payment flows for marketplace transactions:

Escrow System

  • Fund Holding: Payments held securely until deal completion
  • Automatic Release: Funds released upon successful completion
  • Refund Handling: Automatic refunds for failed deals

Payment Functions

typescript
// Create deal payment
const { data } = await supabase.functions.invoke('create-deal-payment', {
  deal_id: 'deal-123',
  amount: 50000, // $500.00 in cents
  currency: 'usd'
});

// Release payout to seller
const { data } = await supabase.functions.invoke('release-deal-payout', {
  deal_id: 'deal-123',
  seller_id: 'seller-456'
});

Notification System

Notification Logic

The system implements a sophisticated notification system to inform users about subscription status changes:

Authentication Notifications

  • Trigger: When user successfully logs in
  • Behavior: Immediate notification only if subscription is expired AND there is a previous period (subscription_end) in the past (new users without previous period don't see toast)
  • Deduplication: None (displays on login if truly expired)
  • Purpose: Ensures user notification when a previous subscription has expired

Periodic Notifications

  • Trigger: Every 60 minutes via background monitoring
  • Behavior: Notification only if subscription expired AND 55+ minutes since last notification
  • Deduplication: Prevents spam by tracking last notification time
  • Purpose: Periodic reminders without overwhelming the user

Manual Check Notifications

  • Trigger: When user clicks "Check Status" button
  • Behavior: No subscription notifications (only sync confirmation)
  • Purpose: Allow users to sync without triggering notifications

Implementation Details

typescript
// Gating + deduplication logic (simplified)
const checkSubscriptionStatus = async (isPeriodicCheck = false) => {
  // ... sync logic ...
  const hadPreviousPeriod = Boolean(data?.subscription_end);
  const expiredInPast = hadPreviousPeriod
    ? new Date(data.subscription_end).getTime() < Date.now()
    : false;

  // Periodic notification only when truly expired and not a new user
  if (data && !data.subscribed && data.subscription_status === 'inactive' && isPeriodicCheck && expiredInPast) {
    const now = Date.now();
    const timeSinceLastNotification = now - lastNotificationRef.current;
    if (timeSinceLastNotification >= 55 * 60 * 1000) {
      toast({
        title: 'Subscription Expired',
        description: 'Your subscription has expired. You have been switched to the free plan.',
        variant: 'destructive',
        duration: 10000
      });
      lastNotificationRef.current = now;
    }
  }
};

Notification Scenarios

ScenarioNotificationDeduplication
User logs in with expired subscription (with previous period)✅ ImmediateNo
User logs in without previous period (new user)❌ No toastN/A
Background check finds expired subscription✅ If 55+ min since lastYes
Manual "Check Status" click❌ No notificationN/A
Page reload with expired subscription✅ Immediate (via login)No

Subscription Status Management

Status Types

  • active: Subscription is current and valid
  • inactive: No active subscription (free plan)
  • expired: Subscription has passed end date
  • past_due: Payment failed, grace period active
  • canceled: Subscription was canceled

Expiration Handling

When a subscription expires:

  1. Status automatically changes to "expired"
  2. User is switched to free plan
  3. Immediate notification on authentication (no deduplication)
  4. Periodic notifications every 60 minutes (with 55-minute deduplication)
  5. UI updates to reflect free tier limitations
typescript
// Expiration check logic
if (rawEnd) {
  const endDate = new Date(rawEnd as string);
  if (new Date() > endDate && rawStatus === 'active') {
    subscriptionStatus = 'expired';
  }
}

User Interface

Billing Tab Features

Current Plan Display

  • Shows subscription level (Free/Starter/Pro)
  • Displays expiration date
  • Color-coded status badges

Manual Sync

  • "Check Status" button for instant sync
  • Loading states during sync
  • Success/error feedback via toasts

Subscription Management

  • "Manage" button opens Stripe Customer Portal
  • Direct access to billing history
  • Invoice download capabilities

Real-time Updates

  • Automatic UI refresh after sync
  • Query invalidation for fresh data
  • Optimistic updates for better UX

Error Handling

Sync Failures

  • Graceful degradation on API failures
  • User-friendly error messages
  • Retry mechanisms for transient errors

Data Inconsistencies

  • Automatic correction during sync
  • Logging for debugging
  • Fallback to safe defaults

Network Issues

  • Offline state detection
  • Queue sync operations when online
  • Background retry with exponential backoff

Testing

E2E Test Coverage

  • Complete subscription flow testing
  • Sync functionality validation
  • Error scenario handling
  • UI state verification

Manual Testing

  • Stripe webhook simulation
  • Subscription status changes
  • Expiration scenarios
  • Payment failure handling

Monitoring & Observability

Logging

  • Detailed sync operation logs
  • Error tracking and alerting
  • Performance metrics
  • User action tracking

Metrics

  • Sync success/failure rates
  • Response times
  • Error frequency
  • User engagement with billing features

Security Considerations

Data Protection

  • No sensitive data in frontend
  • Secure API key management
  • Row Level Security (RLS) enforcement
  • Input validation and sanitization

Access Control

  • User-specific data access
  • Admin-only functions protected
  • Audit trail for all operations
  • Secure webhook verification

Future Enhancements

Planned Features

  • Real-time subscription updates via webhooks
  • Advanced billing analytics
  • Subscription upgrade/downgrade flows
  • Proration handling for plan changes

Technical Improvements

  • Caching layer for subscription data
  • Batch sync operations
  • Advanced error recovery
  • Performance optimizations

Related Documentation: