|
|
@@ -0,0 +1,1513 @@
|
|
|
+# ShopRenter Payment API Implementation Plan
|
|
|
+
|
|
|
+**Project:** ShopCall.ai - ShopRenter Payment Integration
|
|
|
+**Version:** 1.0
|
|
|
+**Date:** 2025-11-11
|
|
|
+**Status:** Planning Phase
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📋 Table of Contents
|
|
|
+
|
|
|
+1. [Executive Summary](#executive-summary)
|
|
|
+2. [Background & Context](#background--context)
|
|
|
+3. [Critical Requirements](#critical-requirements)
|
|
|
+4. [Architecture Overview](#architecture-overview)
|
|
|
+5. [Database Schema](#database-schema)
|
|
|
+6. [Backend Implementation](#backend-implementation)
|
|
|
+7. [Payment Flow](#payment-flow)
|
|
|
+8. [Store Validation](#store-validation)
|
|
|
+9. [Security Considerations](#security-considerations)
|
|
|
+10. [Testing Strategy](#testing-strategy)
|
|
|
+11. [Deployment Plan](#deployment-plan)
|
|
|
+12. [Timeline & Milestones](#timeline--milestones)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🎯 Executive Summary
|
|
|
+
|
|
|
+This document outlines the implementation plan for integrating the ShopRenter Payment API into ShopCall.ai. The integration will enable **ShopRenter stores only** to subscribe to ShopCall.ai services and be billed directly through ShopRenter's payment infrastructure.
|
|
|
+
|
|
|
+### Key Objectives
|
|
|
+
|
|
|
+- ✅ Enable subscription-based billing for ShopRenter stores
|
|
|
+- ✅ Process payments exclusively through ShopRenter Payment API
|
|
|
+- ✅ Enforce strict store validation (ShopRenter stores only)
|
|
|
+- ✅ Implement automatic invoicing and VAT handling
|
|
|
+- ✅ Support both one-time and recurring payment plans
|
|
|
+- ✅ Maintain GDPR compliance and data security
|
|
|
+
|
|
|
+### Core Principle
|
|
|
+
|
|
|
+**⚠️ CRITICAL RULE:** The ShopRenter Payment API **MUST ONLY** be used with ShopRenter stores. Any non-ShopRenter store attempting to use this payment method must be rejected with appropriate error messages.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📖 Background & Context
|
|
|
+
|
|
|
+### What is ShopRenter Payment API?
|
|
|
+
|
|
|
+The ShopRenter Payment API is a comprehensive payment processing toolkit provided by ShopRenter for application developers. It enables:
|
|
|
+
|
|
|
+- **Card payment processing** (one-time and recurring)
|
|
|
+- **Automatic invoicing** with VAT calculation
|
|
|
+- **Subscription management** with payment plans
|
|
|
+- **Financial statistics** and income tracking
|
|
|
+- **Webhook notifications** for payment events
|
|
|
+
|
|
|
+### Current ShopCall.ai Integration Status
|
|
|
+
|
|
|
+ShopCall.ai already has a **fully implemented ShopRenter store integration** including:
|
|
|
+
|
|
|
+- ✅ OAuth 2.0 authentication flow
|
|
|
+- ✅ HMAC validation for security
|
|
|
+- ✅ Product, order, and customer data synchronization
|
|
|
+- ✅ Webhook handlers for data updates
|
|
|
+- ✅ Scheduled background sync (pg_cron)
|
|
|
+- ✅ Database tables for storing ShopRenter credentials and data
|
|
|
+
|
|
|
+**Reference:** See `docs/shoprenter_app_development.md` and `SHOPRENTER.md` for complete details.
|
|
|
+
|
|
|
+### Why Payment API Integration?
|
|
|
+
|
|
|
+Currently, ShopCall.ai uses a generic payment processor (e.g., Stripe) for all stores. Integrating ShopRenter Payment API provides:
|
|
|
+
|
|
|
+1. **Native billing** for ShopRenter merchants (billed through their ShopRenter account)
|
|
|
+2. **Simplified checkout** (no need to enter payment details separately)
|
|
|
+3. **Unified invoicing** (all invoices in one place - ShopRenter admin)
|
|
|
+4. **Better conversion** (trust and familiarity with ShopRenter billing)
|
|
|
+5. **Automatic VAT handling** (compliant with Hungarian tax laws)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## ⚠️ Critical Requirements
|
|
|
+
|
|
|
+### 1. Store Platform Validation (MANDATORY)
|
|
|
+
|
|
|
+**Rule:** The ShopRenter Payment API can **ONLY** be used with stores that have `platform_name = 'shoprenter'` in the `stores` table.
|
|
|
+
|
|
|
+**Enforcement Points:**
|
|
|
+- Payment page rendering (frontend)
|
|
|
+- Payment initiation API endpoint (backend)
|
|
|
+- Subscription creation endpoint (backend)
|
|
|
+- Webhook handlers (backend)
|
|
|
+
|
|
|
+**Validation Logic:**
|
|
|
+```typescript
|
|
|
+async function validateStoreForShopRenterPayment(storeId: string): Promise<boolean> {
|
|
|
+ const { data: store } = await supabase
|
|
|
+ .from('stores')
|
|
|
+ .select('platform_name, is_active')
|
|
|
+ .eq('id', storeId)
|
|
|
+ .single();
|
|
|
+
|
|
|
+ if (!store) {
|
|
|
+ throw new Error('Store not found');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!store.is_active) {
|
|
|
+ throw new Error('Store is inactive');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (store.platform_name !== 'shoprenter') {
|
|
|
+ throw new Error('ShopRenter Payment API can only be used with ShopRenter stores');
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 2. ShopRenter App Registration
|
|
|
+
|
|
|
+Before using the Payment API, the ShopCall.ai app must be:
|
|
|
+- ✅ Registered with ShopRenter Partner Support
|
|
|
+- ✅ Approved for use in production
|
|
|
+- ✅ Configured with Payment API access
|
|
|
+
|
|
|
+**Current Status:** ShopCall.ai is ready for registration (see `SHOPRENTER_REGISTRATION.md`).
|
|
|
+
|
|
|
+### 3. Required Credentials
|
|
|
+
|
|
|
+The Payment API will require additional credentials beyond the OAuth credentials:
|
|
|
+
|
|
|
+- `SHOPRENTER_CLIENT_ID` - OAuth client ID (already configured)
|
|
|
+- `SHOPRENTER_CLIENT_SECRET` - OAuth secret (already configured)
|
|
|
+- `SHOPRENTER_PAYMENT_API_KEY` - Payment API specific key (to be obtained)
|
|
|
+- `SHOPRENTER_PAYMENT_SECRET` - Payment API webhook secret (to be obtained)
|
|
|
+
|
|
|
+### 4. Scope Requirements
|
|
|
+
|
|
|
+The app must request payment-related scopes during OAuth:
|
|
|
+- `payment:read` - Read payment information
|
|
|
+- `payment:write` - Create payment plans and initiate charges
|
|
|
+- `billing:read` - Access billing data
|
|
|
+- `billing:write` - Update billing information
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🏗️ Architecture Overview
|
|
|
+
|
|
|
+### System Components
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────┐
|
|
|
+│ ShopRenter │
|
|
|
+│ Store Owner │
|
|
|
+└──────────┬──────────┘
|
|
|
+ │
|
|
|
+ │ 1. Connect Store (OAuth)
|
|
|
+ ▼
|
|
|
+┌─────────────────────┐
|
|
|
+│ ShopCall.ai │
|
|
|
+│ Frontend │
|
|
|
+└──────────┬──────────┘
|
|
|
+ │
|
|
|
+ │ 2. Select Subscription Plan
|
|
|
+ ▼
|
|
|
+┌─────────────────────┐
|
|
|
+│ ShopCall.ai │
|
|
|
+│ Backend │
|
|
|
+│ (Edge Function) │
|
|
|
+└──────────┬──────────┘
|
|
|
+ │
|
|
|
+ │ 3. Create Payment Plan
|
|
|
+ │ (via ShopRenter Payment API)
|
|
|
+ ▼
|
|
|
+┌─────────────────────┐
|
|
|
+│ ShopRenter │
|
|
|
+│ Payment API │
|
|
|
+└──────────┬──────────┘
|
|
|
+ │
|
|
|
+ │ 4. Process Payment
|
|
|
+ │ (Automatic Recurring Billing)
|
|
|
+ ▼
|
|
|
+┌─────────────────────┐
|
|
|
+│ ShopRenter │
|
|
|
+│ Payment Gateway │
|
|
|
+└──────────┬──────────┘
|
|
|
+ │
|
|
|
+ │ 5. Webhook Notification
|
|
|
+ │ (payment.success, payment.failed, etc.)
|
|
|
+ ▼
|
|
|
+┌─────────────────────┐
|
|
|
+│ ShopCall.ai │
|
|
|
+│ Webhook Handler │
|
|
|
+│ (Edge Function) │
|
|
|
+└──────────┬──────────┘
|
|
|
+ │
|
|
|
+ │ 6. Update Subscription Status
|
|
|
+ ▼
|
|
|
+┌─────────────────────┐
|
|
|
+│ Supabase │
|
|
|
+│ Database │
|
|
|
+└─────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### Integration Flow
|
|
|
+
|
|
|
+1. **Store Connection**: Merchant connects ShopRenter store via OAuth (already implemented)
|
|
|
+2. **Subscription Selection**: Merchant selects a ShopCall.ai subscription plan
|
|
|
+3. **Platform Validation**: Backend validates `platform_name = 'shoprenter'`
|
|
|
+4. **Payment Plan Creation**: Backend creates payment plan via ShopRenter Payment API
|
|
|
+5. **Billing Data**: ShopRenter uses merchant's existing billing information
|
|
|
+6. **Automatic Billing**: ShopRenter charges merchant's card on schedule
|
|
|
+7. **Webhook Events**: ShopRenter sends payment status updates to ShopCall.ai
|
|
|
+8. **Subscription Management**: ShopCall.ai updates subscription status based on webhook events
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 💾 Database Schema
|
|
|
+
|
|
|
+### New Table: `shoprenter_payment_plans`
|
|
|
+
|
|
|
+Stores payment plans configured through ShopRenter Payment API.
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE shoprenter_payment_plans (
|
|
|
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
+
|
|
|
+ -- Store reference
|
|
|
+ store_id UUID NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
|
|
|
+
|
|
|
+ -- ShopRenter Payment API identifiers
|
|
|
+ payment_plan_id VARCHAR(255) NOT NULL, -- ID from ShopRenter Payment API
|
|
|
+ app_payment_id VARCHAR(255), -- Application-specific payment ID
|
|
|
+
|
|
|
+ -- Plan details
|
|
|
+ plan_name VARCHAR(255) NOT NULL, -- e.g., "Basic Plan", "Pro Plan"
|
|
|
+ plan_type VARCHAR(50) NOT NULL, -- "one_time", "recurring"
|
|
|
+ amount DECIMAL(10, 2) NOT NULL, -- Price in HUF
|
|
|
+ currency VARCHAR(3) DEFAULT 'HUF',
|
|
|
+ billing_frequency VARCHAR(50), -- "monthly", "yearly", "quarterly"
|
|
|
+
|
|
|
+ -- Status
|
|
|
+ status VARCHAR(50) NOT NULL DEFAULT 'pending', -- pending, active, cancelled, failed, expired
|
|
|
+
|
|
|
+ -- Timestamps from ShopRenter
|
|
|
+ trial_ends_at TIMESTAMPTZ, -- Trial period end (if applicable)
|
|
|
+ next_billing_date TIMESTAMPTZ, -- Next scheduled charge
|
|
|
+ activated_at TIMESTAMPTZ, -- When subscription became active
|
|
|
+ cancelled_at TIMESTAMPTZ, -- When subscription was cancelled
|
|
|
+ expires_at TIMESTAMPTZ, -- Expiration date
|
|
|
+
|
|
|
+ -- Metadata
|
|
|
+ raw_data JSONB, -- Full response from ShopRenter Payment API
|
|
|
+ created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
+
|
|
|
+ -- Constraints
|
|
|
+ UNIQUE(store_id, payment_plan_id)
|
|
|
+);
|
|
|
+
|
|
|
+-- Indexes
|
|
|
+CREATE INDEX idx_shoprenter_payment_plans_store ON shoprenter_payment_plans(store_id);
|
|
|
+CREATE INDEX idx_shoprenter_payment_plans_status ON shoprenter_payment_plans(status);
|
|
|
+CREATE INDEX idx_shoprenter_payment_plans_next_billing ON shoprenter_payment_plans(next_billing_date);
|
|
|
+
|
|
|
+-- Trigger for updated_at
|
|
|
+CREATE TRIGGER set_shoprenter_payment_plans_updated_at
|
|
|
+BEFORE UPDATE ON shoprenter_payment_plans
|
|
|
+FOR EACH ROW
|
|
|
+EXECUTE FUNCTION update_updated_at_column();
|
|
|
+```
|
|
|
+
|
|
|
+### New Table: `shoprenter_payment_transactions`
|
|
|
+
|
|
|
+Tracks individual payment transactions and webhook events.
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE shoprenter_payment_transactions (
|
|
|
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
+
|
|
|
+ -- References
|
|
|
+ store_id UUID NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
|
|
|
+ payment_plan_id UUID REFERENCES shoprenter_payment_plans(id) ON DELETE SET NULL,
|
|
|
+
|
|
|
+ -- ShopRenter identifiers
|
|
|
+ transaction_id VARCHAR(255) NOT NULL, -- Transaction ID from ShopRenter
|
|
|
+ charge_id VARCHAR(255), -- Charge ID from Payment API
|
|
|
+
|
|
|
+ -- Transaction details
|
|
|
+ transaction_type VARCHAR(50) NOT NULL, -- "charge", "refund", "update"
|
|
|
+ amount DECIMAL(10, 2) NOT NULL,
|
|
|
+ currency VARCHAR(3) DEFAULT 'HUF',
|
|
|
+ status VARCHAR(50) NOT NULL, -- "pending", "success", "failed", "refunded"
|
|
|
+
|
|
|
+ -- Payment method
|
|
|
+ payment_method VARCHAR(50), -- "card", "bank_transfer", etc.
|
|
|
+ card_last4 VARCHAR(4), -- Last 4 digits of card
|
|
|
+ card_brand VARCHAR(50), -- "visa", "mastercard", etc.
|
|
|
+
|
|
|
+ -- Invoice
|
|
|
+ invoice_number VARCHAR(255),
|
|
|
+ invoice_url TEXT,
|
|
|
+
|
|
|
+ -- Error handling
|
|
|
+ error_code VARCHAR(50),
|
|
|
+ error_message TEXT,
|
|
|
+
|
|
|
+ -- Timestamps
|
|
|
+ processed_at TIMESTAMPTZ,
|
|
|
+ refunded_at TIMESTAMPTZ,
|
|
|
+
|
|
|
+ -- Metadata
|
|
|
+ raw_data JSONB, -- Full webhook payload
|
|
|
+ created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
+
|
|
|
+ -- Constraints
|
|
|
+ UNIQUE(transaction_id)
|
|
|
+);
|
|
|
+
|
|
|
+-- Indexes
|
|
|
+CREATE INDEX idx_shoprenter_transactions_store ON shoprenter_payment_transactions(store_id);
|
|
|
+CREATE INDEX idx_shoprenter_transactions_plan ON shoprenter_payment_transactions(payment_plan_id);
|
|
|
+CREATE INDEX idx_shoprenter_transactions_status ON shoprenter_payment_transactions(status);
|
|
|
+CREATE INDEX idx_shoprenter_transactions_date ON shoprenter_payment_transactions(created_at);
|
|
|
+```
|
|
|
+
|
|
|
+### Update Existing `stores` Table
|
|
|
+
|
|
|
+Add payment-related fields to track subscription status.
|
|
|
+
|
|
|
+```sql
|
|
|
+ALTER TABLE stores
|
|
|
+ ADD COLUMN IF NOT EXISTS shoprenter_payment_plan_id UUID REFERENCES shoprenter_payment_plans(id),
|
|
|
+ ADD COLUMN IF NOT EXISTS subscription_status VARCHAR(50) DEFAULT 'inactive',
|
|
|
+ ADD COLUMN IF NOT EXISTS subscription_started_at TIMESTAMPTZ,
|
|
|
+ ADD COLUMN IF NOT EXISTS subscription_ends_at TIMESTAMPTZ;
|
|
|
+
|
|
|
+-- Index for subscription queries
|
|
|
+CREATE INDEX IF NOT EXISTS idx_stores_subscription_status ON stores(subscription_status);
|
|
|
+```
|
|
|
+
|
|
|
+### Migration Script
|
|
|
+
|
|
|
+```sql
|
|
|
+-- Migration: Add ShopRenter Payment API support
|
|
|
+-- Version: 20251111_shoprenter_payment_api.sql
|
|
|
+
|
|
|
+BEGIN;
|
|
|
+
|
|
|
+-- Create payment plans table
|
|
|
+CREATE TABLE IF NOT EXISTS shoprenter_payment_plans (
|
|
|
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
+ store_id UUID NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
|
|
|
+ payment_plan_id VARCHAR(255) NOT NULL,
|
|
|
+ app_payment_id VARCHAR(255),
|
|
|
+ plan_name VARCHAR(255) NOT NULL,
|
|
|
+ plan_type VARCHAR(50) NOT NULL,
|
|
|
+ amount DECIMAL(10, 2) NOT NULL,
|
|
|
+ currency VARCHAR(3) DEFAULT 'HUF',
|
|
|
+ billing_frequency VARCHAR(50),
|
|
|
+ status VARCHAR(50) NOT NULL DEFAULT 'pending',
|
|
|
+ trial_ends_at TIMESTAMPTZ,
|
|
|
+ next_billing_date TIMESTAMPTZ,
|
|
|
+ activated_at TIMESTAMPTZ,
|
|
|
+ cancelled_at TIMESTAMPTZ,
|
|
|
+ expires_at TIMESTAMPTZ,
|
|
|
+ raw_data JSONB,
|
|
|
+ created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
+ UNIQUE(store_id, payment_plan_id)
|
|
|
+);
|
|
|
+
|
|
|
+CREATE INDEX idx_shoprenter_payment_plans_store ON shoprenter_payment_plans(store_id);
|
|
|
+CREATE INDEX idx_shoprenter_payment_plans_status ON shoprenter_payment_plans(status);
|
|
|
+CREATE INDEX idx_shoprenter_payment_plans_next_billing ON shoprenter_payment_plans(next_billing_date);
|
|
|
+
|
|
|
+-- Create payment transactions table
|
|
|
+CREATE TABLE IF NOT EXISTS shoprenter_payment_transactions (
|
|
|
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
+ store_id UUID NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
|
|
|
+ payment_plan_id UUID REFERENCES shoprenter_payment_plans(id) ON DELETE SET NULL,
|
|
|
+ transaction_id VARCHAR(255) NOT NULL,
|
|
|
+ charge_id VARCHAR(255),
|
|
|
+ transaction_type VARCHAR(50) NOT NULL,
|
|
|
+ amount DECIMAL(10, 2) NOT NULL,
|
|
|
+ currency VARCHAR(3) DEFAULT 'HUF',
|
|
|
+ status VARCHAR(50) NOT NULL,
|
|
|
+ payment_method VARCHAR(50),
|
|
|
+ card_last4 VARCHAR(4),
|
|
|
+ card_brand VARCHAR(50),
|
|
|
+ invoice_number VARCHAR(255),
|
|
|
+ invoice_url TEXT,
|
|
|
+ error_code VARCHAR(50),
|
|
|
+ error_message TEXT,
|
|
|
+ processed_at TIMESTAMPTZ,
|
|
|
+ refunded_at TIMESTAMPTZ,
|
|
|
+ raw_data JSONB,
|
|
|
+ created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
+ UNIQUE(transaction_id)
|
|
|
+);
|
|
|
+
|
|
|
+CREATE INDEX idx_shoprenter_transactions_store ON shoprenter_payment_transactions(store_id);
|
|
|
+CREATE INDEX idx_shoprenter_transactions_plan ON shoprenter_payment_transactions(payment_plan_id);
|
|
|
+CREATE INDEX idx_shoprenter_transactions_status ON shoprenter_payment_transactions(status);
|
|
|
+CREATE INDEX idx_shoprenter_transactions_date ON shoprenter_payment_transactions(created_at);
|
|
|
+
|
|
|
+-- Update stores table
|
|
|
+ALTER TABLE stores
|
|
|
+ ADD COLUMN IF NOT EXISTS shoprenter_payment_plan_id UUID REFERENCES shoprenter_payment_plans(id),
|
|
|
+ ADD COLUMN IF NOT EXISTS subscription_status VARCHAR(50) DEFAULT 'inactive',
|
|
|
+ ADD COLUMN IF NOT EXISTS subscription_started_at TIMESTAMPTZ,
|
|
|
+ ADD COLUMN IF NOT EXISTS subscription_ends_at TIMESTAMPTZ;
|
|
|
+
|
|
|
+CREATE INDEX IF NOT EXISTS idx_stores_subscription_status ON stores(subscription_status);
|
|
|
+
|
|
|
+-- Create trigger for updated_at
|
|
|
+CREATE TRIGGER set_shoprenter_payment_plans_updated_at
|
|
|
+BEFORE UPDATE ON shoprenter_payment_plans
|
|
|
+FOR EACH ROW
|
|
|
+EXECUTE FUNCTION update_updated_at_column();
|
|
|
+
|
|
|
+COMMIT;
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🔧 Backend Implementation
|
|
|
+
|
|
|
+### File Structure
|
|
|
+
|
|
|
+```
|
|
|
+supabase/functions/
|
|
|
+├── shoprenter-payment-create/
|
|
|
+│ └── index.ts # Create payment plan endpoint
|
|
|
+├── shoprenter-payment-cancel/
|
|
|
+│ └── index.ts # Cancel subscription endpoint
|
|
|
+├── shoprenter-payment-update/
|
|
|
+│ └── index.ts # Update payment plan endpoint
|
|
|
+├── shoprenter-payment-webhook/
|
|
|
+│ └── index.ts # Payment webhook handler
|
|
|
+├── shoprenter-payment-status/
|
|
|
+│ └── index.ts # Check payment status endpoint
|
|
|
+└── _shared/
|
|
|
+ └── shoprenter-payment-client.ts # ShopRenter Payment API client
|
|
|
+```
|
|
|
+
|
|
|
+### 1. Payment API Client (`_shared/shoprenter-payment-client.ts`)
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
|
|
+
|
|
|
+interface PaymentPlanParams {
|
|
|
+ storeId: string;
|
|
|
+ planName: string;
|
|
|
+ planType: 'one_time' | 'recurring';
|
|
|
+ amount: number;
|
|
|
+ currency?: string;
|
|
|
+ billingFrequency?: 'monthly' | 'yearly' | 'quarterly';
|
|
|
+ trialDays?: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface PaymentPlanResponse {
|
|
|
+ paymentPlanId: string;
|
|
|
+ status: string;
|
|
|
+ activatedAt?: string;
|
|
|
+ nextBillingDate?: string;
|
|
|
+ invoiceUrl?: string;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * ShopRenter Payment API Client
|
|
|
+ * Handles all payment-related API calls to ShopRenter
|
|
|
+ */
|
|
|
+export class ShopRenterPaymentClient {
|
|
|
+ private apiKey: string;
|
|
|
+ private apiSecret: string;
|
|
|
+ private supabase: any;
|
|
|
+
|
|
|
+ constructor() {
|
|
|
+ this.apiKey = Deno.env.get('SHOPRENTER_PAYMENT_API_KEY')!;
|
|
|
+ this.apiSecret = Deno.env.get('SHOPRENTER_PAYMENT_SECRET')!;
|
|
|
+ this.supabase = createClient(
|
|
|
+ Deno.env.get('SUPABASE_URL')!,
|
|
|
+ Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Validate that store is ShopRenter platform
|
|
|
+ */
|
|
|
+ async validateShopRenterStore(storeId: string): Promise<void> {
|
|
|
+ const { data: store, error } = await this.supabase
|
|
|
+ .from('stores')
|
|
|
+ .select('platform_name, is_active, store_name')
|
|
|
+ .eq('id', storeId)
|
|
|
+ .single();
|
|
|
+
|
|
|
+ if (error || !store) {
|
|
|
+ throw new Error('Store not found');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!store.is_active) {
|
|
|
+ throw new Error('Store is inactive');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (store.platform_name !== 'shoprenter') {
|
|
|
+ throw new Error(
|
|
|
+ 'ShopRenter Payment API can only be used with ShopRenter stores. ' +
|
|
|
+ `Current platform: ${store.platform_name}`
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get ShopRenter access token for store
|
|
|
+ */
|
|
|
+ async getAccessToken(storeId: string): Promise<string> {
|
|
|
+ const { data: store, error } = await this.supabase
|
|
|
+ .from('stores')
|
|
|
+ .select('api_key, token_expires_at, store_name')
|
|
|
+ .eq('id', storeId)
|
|
|
+ .single();
|
|
|
+
|
|
|
+ if (error || !store) {
|
|
|
+ throw new Error('Store credentials not found');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if token needs refresh
|
|
|
+ const expiresAt = new Date(store.token_expires_at);
|
|
|
+ const now = new Date();
|
|
|
+ const bufferMinutes = 5;
|
|
|
+
|
|
|
+ if (expiresAt.getTime() - now.getTime() < bufferMinutes * 60 * 1000) {
|
|
|
+ // Token expired or expiring soon - refresh it
|
|
|
+ // Implementation depends on ShopRenter OAuth refresh flow
|
|
|
+ console.log(`[ShopRenter Payment] Token refresh needed for ${store.store_name}`);
|
|
|
+ // TODO: Implement token refresh
|
|
|
+ }
|
|
|
+
|
|
|
+ return store.api_key;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create a payment plan via ShopRenter Payment API
|
|
|
+ */
|
|
|
+ async createPaymentPlan(params: PaymentPlanParams): Promise<PaymentPlanResponse> {
|
|
|
+ // 1. Validate store platform
|
|
|
+ await this.validateShopRenterStore(params.storeId);
|
|
|
+
|
|
|
+ // 2. Get store details
|
|
|
+ const { data: store } = await this.supabase
|
|
|
+ .from('stores')
|
|
|
+ .select('store_name, store_url')
|
|
|
+ .eq('id', params.storeId)
|
|
|
+ .single();
|
|
|
+
|
|
|
+ const accessToken = await this.getAccessToken(params.storeId);
|
|
|
+
|
|
|
+ // 3. Call ShopRenter Payment API
|
|
|
+ // NOTE: Actual API endpoint URL and structure to be confirmed with ShopRenter docs
|
|
|
+ const apiUrl = `https://${store.store_name}.api.shoprenter.hu/payment/plans`;
|
|
|
+
|
|
|
+ const response = await fetch(apiUrl, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${accessToken}`,
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'X-ShopRenter-API-Key': this.apiKey,
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ name: params.planName,
|
|
|
+ type: params.planType,
|
|
|
+ amount: params.amount,
|
|
|
+ currency: params.currency || 'HUF',
|
|
|
+ billing_frequency: params.billingFrequency,
|
|
|
+ trial_days: params.trialDays || 0,
|
|
|
+ return_url: `${Deno.env.get('FRONTEND_URL')}/billing/success`,
|
|
|
+ cancel_url: `${Deno.env.get('FRONTEND_URL')}/billing/cancel`,
|
|
|
+ notification_url: `${Deno.env.get('SUPABASE_URL')}/functions/v1/shoprenter-payment-webhook`,
|
|
|
+ }),
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ const error = await response.text();
|
|
|
+ console.error('[ShopRenter Payment] API error:', error);
|
|
|
+ throw new Error(`Payment plan creation failed: ${response.statusText}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+ console.log('[ShopRenter Payment] Plan created:', data);
|
|
|
+
|
|
|
+ // 4. Store payment plan in database
|
|
|
+ await this.supabase
|
|
|
+ .from('shoprenter_payment_plans')
|
|
|
+ .insert({
|
|
|
+ store_id: params.storeId,
|
|
|
+ payment_plan_id: data.id,
|
|
|
+ plan_name: params.planName,
|
|
|
+ plan_type: params.planType,
|
|
|
+ amount: params.amount,
|
|
|
+ currency: params.currency || 'HUF',
|
|
|
+ billing_frequency: params.billingFrequency,
|
|
|
+ status: data.status || 'pending',
|
|
|
+ next_billing_date: data.next_billing_date,
|
|
|
+ raw_data: data,
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ paymentPlanId: data.id,
|
|
|
+ status: data.status,
|
|
|
+ activatedAt: data.activated_at,
|
|
|
+ nextBillingDate: data.next_billing_date,
|
|
|
+ invoiceUrl: data.invoice_url,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Cancel a payment plan
|
|
|
+ */
|
|
|
+ async cancelPaymentPlan(storeId: string, paymentPlanId: string): Promise<void> {
|
|
|
+ // 1. Validate store platform
|
|
|
+ await this.validateShopRenterStore(storeId);
|
|
|
+
|
|
|
+ // 2. Get store details
|
|
|
+ const { data: store } = await this.supabase
|
|
|
+ .from('stores')
|
|
|
+ .select('store_name')
|
|
|
+ .eq('id', storeId)
|
|
|
+ .single();
|
|
|
+
|
|
|
+ const accessToken = await this.getAccessToken(storeId);
|
|
|
+
|
|
|
+ // 3. Call ShopRenter Payment API
|
|
|
+ const apiUrl = `https://${store.store_name}.api.shoprenter.hu/payment/plans/${paymentPlanId}/cancel`;
|
|
|
+
|
|
|
+ const response = await fetch(apiUrl, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${accessToken}`,
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'X-ShopRenter-API-Key': this.apiKey,
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ const error = await response.text();
|
|
|
+ console.error('[ShopRenter Payment] Cancel error:', error);
|
|
|
+ throw new Error(`Payment plan cancellation failed: ${response.statusText}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. Update database
|
|
|
+ await this.supabase
|
|
|
+ .from('shoprenter_payment_plans')
|
|
|
+ .update({
|
|
|
+ status: 'cancelled',
|
|
|
+ cancelled_at: new Date().toISOString(),
|
|
|
+ updated_at: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ .eq('store_id', storeId)
|
|
|
+ .eq('payment_plan_id', paymentPlanId);
|
|
|
+
|
|
|
+ console.log(`[ShopRenter Payment] Plan cancelled: ${paymentPlanId}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get payment plan status
|
|
|
+ */
|
|
|
+ async getPaymentPlanStatus(storeId: string, paymentPlanId: string): Promise<any> {
|
|
|
+ // 1. Validate store platform
|
|
|
+ await this.validateShopRenterStore(storeId);
|
|
|
+
|
|
|
+ // 2. Get store details
|
|
|
+ const { data: store } = await this.supabase
|
|
|
+ .from('stores')
|
|
|
+ .select('store_name')
|
|
|
+ .eq('id', storeId)
|
|
|
+ .single();
|
|
|
+
|
|
|
+ const accessToken = await this.getAccessToken(storeId);
|
|
|
+
|
|
|
+ // 3. Call ShopRenter Payment API
|
|
|
+ const apiUrl = `https://${store.store_name}.api.shoprenter.hu/payment/plans/${paymentPlanId}`;
|
|
|
+
|
|
|
+ const response = await fetch(apiUrl, {
|
|
|
+ method: 'GET',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${accessToken}`,
|
|
|
+ 'X-ShopRenter-API-Key': this.apiKey,
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`Failed to fetch payment plan status: ${response.statusText}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ return await response.json();
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 2. Create Payment Plan Endpoint (`shoprenter-payment-create/index.ts`)
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
|
|
|
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
|
|
+import { ShopRenterPaymentClient } from '../_shared/shoprenter-payment-client.ts';
|
|
|
+
|
|
|
+serve(async (req) => {
|
|
|
+ // CORS headers
|
|
|
+ if (req.method === 'OPTIONS') {
|
|
|
+ return new Response(null, {
|
|
|
+ headers: {
|
|
|
+ 'Access-Control-Allow-Origin': '*',
|
|
|
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
|
+ 'Access-Control-Allow-Headers': 'authorization, content-type',
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 1. Authenticate user
|
|
|
+ const authHeader = req.headers.get('Authorization');
|
|
|
+ if (!authHeader) {
|
|
|
+ return new Response(JSON.stringify({ error: 'Missing authorization header' }), {
|
|
|
+ status: 401,
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const supabase = createClient(
|
|
|
+ Deno.env.get('SUPABASE_URL')!,
|
|
|
+ Deno.env.get('SUPABASE_ANON_KEY')!,
|
|
|
+ {
|
|
|
+ global: {
|
|
|
+ headers: { Authorization: authHeader },
|
|
|
+ },
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
|
+ if (authError || !user) {
|
|
|
+ return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
|
|
+ status: 401,
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. Parse request body
|
|
|
+ const { store_id, plan_name, plan_type, amount, billing_frequency } = await req.json();
|
|
|
+
|
|
|
+ if (!store_id || !plan_name || !plan_type || !amount) {
|
|
|
+ return new Response(JSON.stringify({ error: 'Missing required fields' }), {
|
|
|
+ status: 400,
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. Verify store ownership
|
|
|
+ const { data: store } = await supabase
|
|
|
+ .from('stores')
|
|
|
+ .select('id, platform_name')
|
|
|
+ .eq('id', store_id)
|
|
|
+ .eq('user_id', user.id)
|
|
|
+ .single();
|
|
|
+
|
|
|
+ if (!store) {
|
|
|
+ return new Response(JSON.stringify({ error: 'Store not found or access denied' }), {
|
|
|
+ status: 403,
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. Validate ShopRenter platform (CRITICAL CHECK!)
|
|
|
+ if (store.platform_name !== 'shoprenter') {
|
|
|
+ return new Response(
|
|
|
+ JSON.stringify({
|
|
|
+ error: 'Invalid payment method',
|
|
|
+ message: 'ShopRenter Payment API can only be used with ShopRenter stores',
|
|
|
+ platform: store.platform_name,
|
|
|
+ }),
|
|
|
+ {
|
|
|
+ status: 400,
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. Create payment plan
|
|
|
+ const paymentClient = new ShopRenterPaymentClient();
|
|
|
+ const result = await paymentClient.createPaymentPlan({
|
|
|
+ storeId: store_id,
|
|
|
+ planName: plan_name,
|
|
|
+ planType: plan_type,
|
|
|
+ amount: amount,
|
|
|
+ billingFrequency: billing_frequency,
|
|
|
+ });
|
|
|
+
|
|
|
+ return new Response(
|
|
|
+ JSON.stringify({
|
|
|
+ success: true,
|
|
|
+ payment_plan_id: result.paymentPlanId,
|
|
|
+ status: result.status,
|
|
|
+ next_billing_date: result.nextBillingDate,
|
|
|
+ invoice_url: result.invoiceUrl,
|
|
|
+ }),
|
|
|
+ {
|
|
|
+ status: 200,
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ }
|
|
|
+ );
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[ShopRenter Payment] Error:', error);
|
|
|
+ return new Response(
|
|
|
+ JSON.stringify({
|
|
|
+ error: 'Payment plan creation failed',
|
|
|
+ message: error.message,
|
|
|
+ }),
|
|
|
+ {
|
|
|
+ status: 500,
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 3. Payment Webhook Handler (`shoprenter-payment-webhook/index.ts`)
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
|
|
|
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
|
|
+import { createHmac } from 'https://deno.land/std@0.177.0/node/crypto.ts';
|
|
|
+
|
|
|
+serve(async (req) => {
|
|
|
+ if (req.method !== 'POST') {
|
|
|
+ return new Response('Method not allowed', { status: 405 });
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 1. Validate webhook signature (HMAC)
|
|
|
+ const body = await req.text();
|
|
|
+ const signature = req.headers.get('X-ShopRenter-Signature');
|
|
|
+ const secret = Deno.env.get('SHOPRENTER_PAYMENT_SECRET')!;
|
|
|
+
|
|
|
+ const expectedSignature = createHmac('sha256', secret)
|
|
|
+ .update(body)
|
|
|
+ .digest('hex');
|
|
|
+
|
|
|
+ if (signature !== expectedSignature) {
|
|
|
+ console.error('[ShopRenter Payment Webhook] Invalid signature');
|
|
|
+ return new Response('Invalid signature', { status: 403 });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. Parse webhook payload
|
|
|
+ const payload = JSON.parse(body);
|
|
|
+ const { event, data } = payload;
|
|
|
+
|
|
|
+ console.log(`[ShopRenter Payment Webhook] Received: ${event}`, data);
|
|
|
+
|
|
|
+ const supabase = createClient(
|
|
|
+ Deno.env.get('SUPABASE_URL')!,
|
|
|
+ Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
|
|
+ );
|
|
|
+
|
|
|
+ // 3. Process webhook event
|
|
|
+ switch (event) {
|
|
|
+ case 'payment.success':
|
|
|
+ await handlePaymentSuccess(supabase, data);
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'payment.failed':
|
|
|
+ await handlePaymentFailed(supabase, data);
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'subscription.activated':
|
|
|
+ await handleSubscriptionActivated(supabase, data);
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'subscription.cancelled':
|
|
|
+ await handleSubscriptionCancelled(supabase, data);
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'subscription.expired':
|
|
|
+ await handleSubscriptionExpired(supabase, data);
|
|
|
+ break;
|
|
|
+
|
|
|
+ default:
|
|
|
+ console.log(`[ShopRenter Payment Webhook] Unhandled event: ${event}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ return new Response(JSON.stringify({ success: true }), {
|
|
|
+ status: 200,
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[ShopRenter Payment Webhook] Error:', error);
|
|
|
+ return new Response(JSON.stringify({ error: error.message }), {
|
|
|
+ status: 500,
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+async function handlePaymentSuccess(supabase: any, data: any) {
|
|
|
+ console.log('[ShopRenter Payment] Payment successful:', data);
|
|
|
+
|
|
|
+ // 1. Record transaction
|
|
|
+ await supabase.from('shoprenter_payment_transactions').insert({
|
|
|
+ store_id: data.store_id,
|
|
|
+ payment_plan_id: data.payment_plan_id,
|
|
|
+ transaction_id: data.transaction_id,
|
|
|
+ charge_id: data.charge_id,
|
|
|
+ transaction_type: 'charge',
|
|
|
+ amount: data.amount,
|
|
|
+ currency: data.currency || 'HUF',
|
|
|
+ status: 'success',
|
|
|
+ payment_method: data.payment_method,
|
|
|
+ card_last4: data.card_last4,
|
|
|
+ card_brand: data.card_brand,
|
|
|
+ invoice_number: data.invoice_number,
|
|
|
+ invoice_url: data.invoice_url,
|
|
|
+ processed_at: new Date().toISOString(),
|
|
|
+ raw_data: data,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 2. Update payment plan status
|
|
|
+ await supabase
|
|
|
+ .from('shoprenter_payment_plans')
|
|
|
+ .update({
|
|
|
+ status: 'active',
|
|
|
+ next_billing_date: data.next_billing_date,
|
|
|
+ updated_at: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ .eq('payment_plan_id', data.payment_plan_id);
|
|
|
+
|
|
|
+ // 3. Update store subscription status
|
|
|
+ await supabase
|
|
|
+ .from('stores')
|
|
|
+ .update({
|
|
|
+ subscription_status: 'active',
|
|
|
+ subscription_started_at: new Date().toISOString(),
|
|
|
+ updated_at: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ .eq('id', data.store_id);
|
|
|
+}
|
|
|
+
|
|
|
+async function handlePaymentFailed(supabase: any, data: any) {
|
|
|
+ console.log('[ShopRenter Payment] Payment failed:', data);
|
|
|
+
|
|
|
+ // 1. Record failed transaction
|
|
|
+ await supabase.from('shoprenter_payment_transactions').insert({
|
|
|
+ store_id: data.store_id,
|
|
|
+ payment_plan_id: data.payment_plan_id,
|
|
|
+ transaction_id: data.transaction_id,
|
|
|
+ transaction_type: 'charge',
|
|
|
+ amount: data.amount,
|
|
|
+ currency: data.currency || 'HUF',
|
|
|
+ status: 'failed',
|
|
|
+ error_code: data.error_code,
|
|
|
+ error_message: data.error_message,
|
|
|
+ processed_at: new Date().toISOString(),
|
|
|
+ raw_data: data,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 2. Update payment plan status
|
|
|
+ await supabase
|
|
|
+ .from('shoprenter_payment_plans')
|
|
|
+ .update({
|
|
|
+ status: 'failed',
|
|
|
+ updated_at: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ .eq('payment_plan_id', data.payment_plan_id);
|
|
|
+
|
|
|
+ // 3. Update store subscription status
|
|
|
+ await supabase
|
|
|
+ .from('stores')
|
|
|
+ .update({
|
|
|
+ subscription_status: 'payment_failed',
|
|
|
+ updated_at: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ .eq('id', data.store_id);
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSubscriptionActivated(supabase: any, data: any) {
|
|
|
+ console.log('[ShopRenter Payment] Subscription activated:', data);
|
|
|
+
|
|
|
+ await supabase
|
|
|
+ .from('shoprenter_payment_plans')
|
|
|
+ .update({
|
|
|
+ status: 'active',
|
|
|
+ activated_at: new Date().toISOString(),
|
|
|
+ next_billing_date: data.next_billing_date,
|
|
|
+ updated_at: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ .eq('payment_plan_id', data.payment_plan_id);
|
|
|
+
|
|
|
+ await supabase
|
|
|
+ .from('stores')
|
|
|
+ .update({
|
|
|
+ subscription_status: 'active',
|
|
|
+ subscription_started_at: new Date().toISOString(),
|
|
|
+ updated_at: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ .eq('id', data.store_id);
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSubscriptionCancelled(supabase: any, data: any) {
|
|
|
+ console.log('[ShopRenter Payment] Subscription cancelled:', data);
|
|
|
+
|
|
|
+ await supabase
|
|
|
+ .from('shoprenter_payment_plans')
|
|
|
+ .update({
|
|
|
+ status: 'cancelled',
|
|
|
+ cancelled_at: new Date().toISOString(),
|
|
|
+ updated_at: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ .eq('payment_plan_id', data.payment_plan_id);
|
|
|
+
|
|
|
+ await supabase
|
|
|
+ .from('stores')
|
|
|
+ .update({
|
|
|
+ subscription_status: 'cancelled',
|
|
|
+ updated_at: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ .eq('id', data.store_id);
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSubscriptionExpired(supabase: any, data: any) {
|
|
|
+ console.log('[ShopRenter Payment] Subscription expired:', data);
|
|
|
+
|
|
|
+ await supabase
|
|
|
+ .from('shoprenter_payment_plans')
|
|
|
+ .update({
|
|
|
+ status: 'expired',
|
|
|
+ expires_at: new Date().toISOString(),
|
|
|
+ updated_at: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ .eq('payment_plan_id', data.payment_plan_id);
|
|
|
+
|
|
|
+ await supabase
|
|
|
+ .from('stores')
|
|
|
+ .update({
|
|
|
+ subscription_status: 'expired',
|
|
|
+ subscription_ends_at: new Date().toISOString(),
|
|
|
+ updated_at: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ .eq('id', data.store_id);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 💳 Payment Flow
|
|
|
+
|
|
|
+### User Journey
|
|
|
+
|
|
|
+```
|
|
|
+1. Merchant connects ShopRenter store (OAuth) → Already implemented
|
|
|
+ ↓
|
|
|
+2. Merchant navigates to "Billing" page in ShopCall.ai
|
|
|
+ ↓
|
|
|
+3. Merchant selects subscription plan (Basic, Pro, Enterprise)
|
|
|
+ ↓
|
|
|
+4. Frontend checks store platform: if (platform_name !== 'shoprenter') → show Stripe payment
|
|
|
+ ↓
|
|
|
+5. If ShopRenter store → Show "Pay via ShopRenter" button
|
|
|
+ ↓
|
|
|
+6. Merchant clicks "Subscribe"
|
|
|
+ ↓
|
|
|
+7. Frontend calls: POST /functions/v1/shoprenter-payment-create
|
|
|
+ ↓
|
|
|
+8. Backend validates store platform (MANDATORY CHECK!)
|
|
|
+ ↓
|
|
|
+9. Backend creates payment plan via ShopRenter Payment API
|
|
|
+ ↓
|
|
|
+10. ShopRenter redirects merchant to payment page (card details)
|
|
|
+ ↓
|
|
|
+11. Merchant enters card details or confirms existing card
|
|
|
+ ↓
|
|
|
+12. ShopRenter processes first payment
|
|
|
+ ↓
|
|
|
+13. ShopRenter sends webhook: payment.success
|
|
|
+ ↓
|
|
|
+14. Backend updates subscription status → "active"
|
|
|
+ ↓
|
|
|
+15. Merchant redirected back to ShopCall.ai → Subscription active!
|
|
|
+```
|
|
|
+
|
|
|
+### Recurring Billing
|
|
|
+
|
|
|
+After initial payment:
|
|
|
+
|
|
|
+1. ShopRenter automatically charges the merchant's card on schedule (monthly/yearly)
|
|
|
+2. ShopRenter sends webhook: `payment.success` or `payment.failed`
|
|
|
+3. ShopCall.ai updates subscription status accordingly
|
|
|
+4. If payment fails:
|
|
|
+ - Send email notification to merchant
|
|
|
+ - Mark subscription as "payment_failed"
|
|
|
+ - Allow grace period (e.g., 3 days) before suspension
|
|
|
+ - ShopRenter retries payment automatically
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🔒 Security Considerations
|
|
|
+
|
|
|
+### 1. Store Platform Validation (CRITICAL!)
|
|
|
+
|
|
|
+**Enforcement Strategy:**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// Middleware for all payment endpoints
|
|
|
+async function validateShopRenterPayment(storeId: string, userId: string) {
|
|
|
+ const supabase = createClient(
|
|
|
+ Deno.env.get('SUPABASE_URL')!,
|
|
|
+ Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
|
|
+ );
|
|
|
+
|
|
|
+ // 1. Verify store exists and belongs to user
|
|
|
+ const { data: store, error } = await supabase
|
|
|
+ .from('stores')
|
|
|
+ .select('id, platform_name, is_active, user_id')
|
|
|
+ .eq('id', storeId)
|
|
|
+ .single();
|
|
|
+
|
|
|
+ if (error || !store) {
|
|
|
+ throw new Error('Store not found');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (store.user_id !== userId) {
|
|
|
+ throw new Error('Access denied: You do not own this store');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!store.is_active) {
|
|
|
+ throw new Error('Store is inactive');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. CRITICAL: Validate ShopRenter platform
|
|
|
+ if (store.platform_name !== 'shoprenter') {
|
|
|
+ throw new Error(
|
|
|
+ `Invalid payment method for platform: ${store.platform_name}. ` +
|
|
|
+ 'ShopRenter Payment API can only be used with ShopRenter stores.'
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return store;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Apply to ALL payment endpoints:**
|
|
|
+- ✅ `shoprenter-payment-create`
|
|
|
+- ✅ `shoprenter-payment-cancel`
|
|
|
+- ✅ `shoprenter-payment-update`
|
|
|
+- ✅ `shoprenter-payment-status`
|
|
|
+- ✅ `shoprenter-payment-webhook` (validate via payment_plan_id lookup)
|
|
|
+
|
|
|
+### 2. Webhook Signature Validation
|
|
|
+
|
|
|
+```typescript
|
|
|
+function validateWebhookSignature(body: string, signature: string): boolean {
|
|
|
+ const secret = Deno.env.get('SHOPRENTER_PAYMENT_SECRET')!;
|
|
|
+
|
|
|
+ const expectedSignature = createHmac('sha256', secret)
|
|
|
+ .update(body)
|
|
|
+ .digest('hex');
|
|
|
+
|
|
|
+ // Timing-safe comparison
|
|
|
+ try {
|
|
|
+ return timingSafeEqual(
|
|
|
+ new TextEncoder().encode(expectedSignature),
|
|
|
+ new TextEncoder().encode(signature)
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3. Environment Variables
|
|
|
+
|
|
|
+```bash
|
|
|
+# Required for ShopRenter Payment API
|
|
|
+SHOPRENTER_PAYMENT_API_KEY=your_payment_api_key
|
|
|
+SHOPRENTER_PAYMENT_SECRET=your_webhook_secret
|
|
|
+
|
|
|
+# Existing ShopRenter OAuth (already configured)
|
|
|
+SHOPRENTER_CLIENT_ID=your_client_id
|
|
|
+SHOPRENTER_CLIENT_SECRET=your_client_secret
|
|
|
+
|
|
|
+# Supabase (already configured)
|
|
|
+SUPABASE_URL=https://ztklqodcdjeqpsvhlpud.supabase.co
|
|
|
+SUPABASE_ANON_KEY=your_anon_key
|
|
|
+SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
|
|
|
+
|
|
|
+# Frontend URL (already configured)
|
|
|
+FRONTEND_URL=https://shopcall.ai
|
|
|
+```
|
|
|
+
|
|
|
+### 4. Data Privacy & GDPR
|
|
|
+
|
|
|
+- ✅ Store minimal payment data (no full card numbers)
|
|
|
+- ✅ Log transactions for audit trail
|
|
|
+- ✅ Delete payment data on store deletion (CASCADE)
|
|
|
+- ✅ Provide data export for merchants
|
|
|
+- ✅ Handle refund requests properly
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🧪 Testing Strategy
|
|
|
+
|
|
|
+### 1. Test Store Setup
|
|
|
+
|
|
|
+Use existing ShopRenter test store:
|
|
|
+- Store name: `shopcall-test-store`
|
|
|
+- URL: `shopcall-test-store.myshoprenter.hu`
|
|
|
+- Already connected via OAuth
|
|
|
+
|
|
|
+### 2. Test Cases
|
|
|
+
|
|
|
+#### Store Validation Tests
|
|
|
+
|
|
|
+```typescript
|
|
|
+describe('Store Platform Validation', () => {
|
|
|
+ it('should accept ShopRenter stores', async () => {
|
|
|
+ // Given: ShopRenter store
|
|
|
+ const store = { platform_name: 'shoprenter', is_active: true };
|
|
|
+
|
|
|
+ // When: Creating payment plan
|
|
|
+ const result = await createPaymentPlan({ storeId: store.id });
|
|
|
+
|
|
|
+ // Then: Should succeed
|
|
|
+ expect(result.success).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should reject non-ShopRenter stores', async () => {
|
|
|
+ // Given: Shopify store
|
|
|
+ const store = { platform_name: 'shopify', is_active: true };
|
|
|
+
|
|
|
+ // When: Creating payment plan
|
|
|
+ const result = await createPaymentPlan({ storeId: store.id });
|
|
|
+
|
|
|
+ // Then: Should fail with specific error
|
|
|
+ expect(result.error).toContain('ShopRenter Payment API can only be used with ShopRenter stores');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should reject WooCommerce stores', async () => {
|
|
|
+ // Given: WooCommerce store
|
|
|
+ const store = { platform_name: 'woocommerce', is_active: true };
|
|
|
+
|
|
|
+ // When: Creating payment plan
|
|
|
+ const result = await createPaymentPlan({ storeId: store.id });
|
|
|
+
|
|
|
+ // Then: Should fail
|
|
|
+ expect(result.error).toContain('ShopRenter Payment API');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should reject inactive stores', async () => {
|
|
|
+ // Given: Inactive ShopRenter store
|
|
|
+ const store = { platform_name: 'shoprenter', is_active: false };
|
|
|
+
|
|
|
+ // When: Creating payment plan
|
|
|
+ const result = await createPaymentPlan({ storeId: store.id });
|
|
|
+
|
|
|
+ // Then: Should fail
|
|
|
+ expect(result.error).toContain('inactive');
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### Payment Flow Tests
|
|
|
+
|
|
|
+```typescript
|
|
|
+describe('Payment Plan Creation', () => {
|
|
|
+ it('should create one-time payment plan', async () => {
|
|
|
+ const params = {
|
|
|
+ storeId: 'test-store-id',
|
|
|
+ planName: 'Basic Plan',
|
|
|
+ planType: 'one_time',
|
|
|
+ amount: 9990,
|
|
|
+ currency: 'HUF',
|
|
|
+ };
|
|
|
+
|
|
|
+ const result = await paymentClient.createPaymentPlan(params);
|
|
|
+
|
|
|
+ expect(result.paymentPlanId).toBeDefined();
|
|
|
+ expect(result.status).toBe('pending');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should create recurring payment plan', async () => {
|
|
|
+ const params = {
|
|
|
+ storeId: 'test-store-id',
|
|
|
+ planName: 'Pro Plan',
|
|
|
+ planType: 'recurring',
|
|
|
+ amount: 29990,
|
|
|
+ currency: 'HUF',
|
|
|
+ billingFrequency: 'monthly',
|
|
|
+ };
|
|
|
+
|
|
|
+ const result = await paymentClient.createPaymentPlan(params);
|
|
|
+
|
|
|
+ expect(result.paymentPlanId).toBeDefined();
|
|
|
+ expect(result.nextBillingDate).toBeDefined();
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### Webhook Tests
|
|
|
+
|
|
|
+```typescript
|
|
|
+describe('Payment Webhooks', () => {
|
|
|
+ it('should handle payment.success webhook', async () => {
|
|
|
+ const payload = {
|
|
|
+ event: 'payment.success',
|
|
|
+ data: {
|
|
|
+ store_id: 'test-store-id',
|
|
|
+ payment_plan_id: 'plan-123',
|
|
|
+ transaction_id: 'txn-456',
|
|
|
+ amount: 9990,
|
|
|
+ currency: 'HUF',
|
|
|
+ invoice_number: 'INV-2025-001',
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await handleWebhook(payload);
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+
|
|
|
+ // Verify database updates
|
|
|
+ const plan = await getPlanFromDB('plan-123');
|
|
|
+ expect(plan.status).toBe('active');
|
|
|
+
|
|
|
+ const store = await getStoreFromDB('test-store-id');
|
|
|
+ expect(store.subscription_status).toBe('active');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should handle payment.failed webhook', async () => {
|
|
|
+ const payload = {
|
|
|
+ event: 'payment.failed',
|
|
|
+ data: {
|
|
|
+ store_id: 'test-store-id',
|
|
|
+ payment_plan_id: 'plan-123',
|
|
|
+ error_code: 'insufficient_funds',
|
|
|
+ error_message: 'Insufficient funds',
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await handleWebhook(payload);
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+
|
|
|
+ const plan = await getPlanFromDB('plan-123');
|
|
|
+ expect(plan.status).toBe('failed');
|
|
|
+
|
|
|
+ const store = await getStoreFromDB('test-store-id');
|
|
|
+ expect(store.subscription_status).toBe('payment_failed');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should reject webhook with invalid signature', async () => {
|
|
|
+ const payload = { event: 'payment.success', data: {} };
|
|
|
+ const invalidSignature = 'invalid_hmac';
|
|
|
+
|
|
|
+ const response = await handleWebhook(payload, invalidSignature);
|
|
|
+
|
|
|
+ expect(response.status).toBe(403);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 3. Manual Testing Checklist
|
|
|
+
|
|
|
+- [ ] Connect test ShopRenter store via OAuth
|
|
|
+- [ ] Navigate to billing page
|
|
|
+- [ ] Select subscription plan
|
|
|
+- [ ] Verify "Pay via ShopRenter" option appears
|
|
|
+- [ ] Click subscribe button
|
|
|
+- [ ] Verify redirect to ShopRenter payment page
|
|
|
+- [ ] Complete test payment with test card
|
|
|
+- [ ] Verify redirect back to ShopCall.ai
|
|
|
+- [ ] Verify subscription status is "active"
|
|
|
+- [ ] Verify payment transaction recorded in database
|
|
|
+- [ ] Test cancellation flow
|
|
|
+- [ ] Test payment failure handling
|
|
|
+- [ ] Verify webhook events processed correctly
|
|
|
+- [ ] Test with non-ShopRenter store (should reject)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🚀 Deployment Plan
|
|
|
+
|
|
|
+### Phase 1: Preparation (Week 1)
|
|
|
+
|
|
|
+- [x] Review ShopRenter Payment API documentation
|
|
|
+- [ ] Create implementation plan (this document)
|
|
|
+- [ ] Design database schema
|
|
|
+- [ ] Set up test environment
|
|
|
+- [ ] Request Payment API credentials from ShopRenter
|
|
|
+
|
|
|
+### Phase 2: Development (Weeks 2-3)
|
|
|
+
|
|
|
+- [ ] Create database migration
|
|
|
+- [ ] Implement Payment API client library
|
|
|
+- [ ] Implement Edge Functions:
|
|
|
+ - [ ] `shoprenter-payment-create`
|
|
|
+ - [ ] `shoprenter-payment-cancel`
|
|
|
+ - [ ] `shoprenter-payment-status`
|
|
|
+ - [ ] `shoprenter-payment-webhook`
|
|
|
+- [ ] Implement store validation middleware
|
|
|
+- [ ] Add error handling and logging
|
|
|
+
|
|
|
+### Phase 3: Frontend Integration (Week 4)
|
|
|
+
|
|
|
+- [ ] Create billing page UI
|
|
|
+- [ ] Add subscription plan selection
|
|
|
+- [ ] Implement platform-specific payment method selection
|
|
|
+- [ ] Add ShopRenter payment button
|
|
|
+- [ ] Implement success/failure callbacks
|
|
|
+- [ ] Add subscription management UI
|
|
|
+
|
|
|
+### Phase 4: Testing (Week 5)
|
|
|
+
|
|
|
+- [ ] Unit tests for validation logic
|
|
|
+- [ ] Integration tests for payment flow
|
|
|
+- [ ] Webhook handler tests
|
|
|
+- [ ] Manual end-to-end testing
|
|
|
+- [ ] Security audit
|
|
|
+- [ ] Performance testing
|
|
|
+
|
|
|
+### Phase 5: Production Deployment (Week 6)
|
|
|
+
|
|
|
+- [ ] Deploy database migration
|
|
|
+- [ ] Deploy Edge Functions
|
|
|
+- [ ] Configure environment variables
|
|
|
+- [ ] Enable payment webhooks
|
|
|
+- [ ] Monitor logs for errors
|
|
|
+- [ ] Soft launch with beta users
|
|
|
+
|
|
|
+### Phase 6: Launch (Week 7)
|
|
|
+
|
|
|
+- [ ] Full production launch
|
|
|
+- [ ] Monitor payment success rates
|
|
|
+- [ ] Customer support readiness
|
|
|
+- [ ] Documentation updates
|
|
|
+- [ ] Marketing announcement
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📅 Timeline & Milestones
|
|
|
+
|
|
|
+| Week | Milestone | Deliverables |
|
|
|
+|------|-----------|--------------|
|
|
|
+| 1 | Planning Complete | ✅ Implementation plan document |
|
|
|
+| 2 | Database & Backend Foundation | Schema migration, Payment client library |
|
|
|
+| 3 | Core Payment Features | Create/cancel/status Edge Functions |
|
|
|
+| 4 | Frontend Integration | Billing UI, Payment buttons, Callbacks |
|
|
|
+| 5 | Testing & QA | Test suite, Manual testing, Security audit |
|
|
|
+| 6 | Deployment | Production deployment, Monitoring setup |
|
|
|
+| 7 | Launch | Public launch, User documentation |
|
|
|
+
|
|
|
+**Total Duration:** 7 weeks
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📚 References & Resources
|
|
|
+
|
|
|
+### ShopRenter Documentation
|
|
|
+
|
|
|
+- **Payment API Introduction**: `https://doc.shoprenter.hu/paymentapi/docs/a_introduce.html`
|
|
|
+- **Payment API Docs**: `https://doc.shoprenter.hu/paymentapi/`
|
|
|
+- **App Development Guide**: `https://doc.shoprenter.hu/development/app-development/`
|
|
|
+- **OAuth Documentation**: `docs/shoprenter_app_development.md`
|
|
|
+
|
|
|
+### Internal Documentation
|
|
|
+
|
|
|
+- **ShopRenter Integration Plan**: `SHOPRENTER.md`
|
|
|
+- **Registration Requirements**: `SHOPRENTER_REGISTRATION.md`
|
|
|
+- **API Resources**: `docs/shoprenter_api_resources.md`
|
|
|
+- **Payment Overview**: `docs/shoprenter_payment.md`
|
|
|
+
|
|
|
+### Project Documentation
|
|
|
+
|
|
|
+- **Project Overview**: `CLAUDE.md`
|
|
|
+- **Database Schema**: See Supabase migrations in `supabase/migrations/`
|
|
|
+- **Edge Functions**: `supabase/functions/`
|
|
|
+
|
|
|
+### Contact & Support
|
|
|
+
|
|
|
+- **ShopRenter Partner Support**: `partnersupport@shoprenter.hu`
|
|
|
+- **ShopRenter Developer Portal**: `https://doc.shoprenter.hu`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## ✅ Next Steps
|
|
|
+
|
|
|
+1. **Review this plan** with the team
|
|
|
+2. **Request Payment API credentials** from ShopRenter Partner Support
|
|
|
+3. **Create database migration** for new tables
|
|
|
+4. **Set up test environment** with test ShopRenter store
|
|
|
+5. **Begin Phase 2 development** (Backend implementation)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🔄 Change Log
|
|
|
+
|
|
|
+| Date | Version | Changes |
|
|
|
+|------|---------|---------|
|
|
|
+| 2025-11-11 | 1.0 | Initial implementation plan created |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+**Document Status:** ✅ Ready for Review
|
|
|
+**Author:** Claude (AI Development Assistant)
|
|
|
+**Approved By:** _Pending_
|
|
|
+**Next Review Date:** _TBD_
|