|
@@ -1,1678 +0,0 @@
|
|
|
-# ShopRenter Integration Development Plan
|
|
|
|
|
-
|
|
|
|
|
-**Project:** ShopCall.ai - ShopRenter Integration
|
|
|
|
|
-**Platform:** ShopRenter (Hungarian e-commerce platform)
|
|
|
|
|
-**Integration Type:** OAuth 2.0 App with API Access
|
|
|
|
|
-**Documentation Source:** https://doc.shoprenter.hu/development/app-development/
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 📋 Table of Contents
|
|
|
|
|
-
|
|
|
|
|
-1. [Overview](#overview)
|
|
|
|
|
-2. [Registration Requirements](#registration-requirements)
|
|
|
|
|
-3. [Technical Architecture](#technical-architecture)
|
|
|
|
|
-4. [OAuth Flow Implementation](#oauth-flow-implementation)
|
|
|
|
|
-5. [API Integration](#api-integration)
|
|
|
|
|
-6. [Database Schema](#database-schema)
|
|
|
|
|
-7. [Backend Implementation](#backend-implementation)
|
|
|
|
|
-8. [Frontend Implementation](#frontend-implementation)
|
|
|
|
|
-9. [Security Considerations](#security-considerations)
|
|
|
|
|
-10. [Testing Strategy](#testing-strategy)
|
|
|
|
|
-11. [Deployment Plan](#deployment-plan)
|
|
|
|
|
-12. [Timeline & Milestones](#timeline--milestones)
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 🎯 Overview
|
|
|
|
|
-
|
|
|
|
|
-### What is ShopRenter?
|
|
|
|
|
-
|
|
|
|
|
-ShopRenter is a Hungarian e-commerce platform (similar to Shopify) that provides online store solutions. This integration will enable ShopCall.ai to connect with ShopRenter stores and provide AI-powered calling services.
|
|
|
|
|
-
|
|
|
|
|
-### Integration Goals
|
|
|
|
|
-
|
|
|
|
|
-- ✅ Enable ShopRenter merchants to connect their stores via OAuth
|
|
|
|
|
-- ✅ Sync product catalog, customer data, and order information
|
|
|
|
|
-- ✅ Provide AI calling services for ShopRenter merchants
|
|
|
|
|
-- ✅ Support Hungarian language and HUF currency
|
|
|
|
|
-- ✅ Comply with ShopRenter's app requirements and guidelines
|
|
|
|
|
-
|
|
|
|
|
-### Key Features
|
|
|
|
|
-
|
|
|
|
|
-1. **OAuth 2.0 Authentication** - Secure store connection
|
|
|
|
|
-2. **Product Sync** - Automatic product catalog synchronization
|
|
|
|
|
-3. **Customer Data Access** - Customer information for personalized calls
|
|
|
|
|
-4. **Order Information** - Access to order status for customer inquiries
|
|
|
|
|
-5. **Webhook Support** - Real-time updates for orders and products
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 📝 Registration Requirements
|
|
|
|
|
-
|
|
|
|
|
-### Data Required to Register with ShopRenter
|
|
|
|
|
-
|
|
|
|
|
-**Must be sent to:** partnersupport@shoprenter.hu
|
|
|
|
|
-
|
|
|
|
|
-#### 1. Application Information
|
|
|
|
|
-
|
|
|
|
|
-**Application Name:**
|
|
|
|
|
-```
|
|
|
|
|
-ShopCall.ai - AI Phone Assistant
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Short Description (max 70 chars):**
|
|
|
|
|
-```
|
|
|
|
|
-AI-powered phone assistant for automated customer service calls
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Application Details Link:**
|
|
|
|
|
-```
|
|
|
|
|
-https://shopcall.ai
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Application Type:**
|
|
|
|
|
-```
|
|
|
|
|
-Redirected (user redirected to our platform, not iframe)
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-#### 2. Technical Endpoints
|
|
|
|
|
-
|
|
|
|
|
-**⚠️ Note:** The URLs below are examples. These are now configurable via environment variables:
|
|
|
|
|
-- `FRONTEND_URL` - for EntryPoint
|
|
|
|
|
-- `BACKEND_URL` - for RedirectUri and UninstallUri
|
|
|
|
|
-
|
|
|
|
|
-See `.env.example` files for configuration details.
|
|
|
|
|
-
|
|
|
|
|
-**EntryPoint (HTTPS required):**
|
|
|
|
|
-```
|
|
|
|
|
-https://shopcall.ai/integrations
|
|
|
|
|
-```
|
|
|
|
|
-- This is where users land after OAuth installation
|
|
|
|
|
-- ShopRenter will call this URL with authentication parameters: `shopname`, `code`, `timestamp`, `hmac`, `app_url`
|
|
|
|
|
-- Our backend redirects to `app_url` (provided by ShopRenter) with `sr_install` parameter
|
|
|
|
|
-- Frontend displays integration success/configuration page
|
|
|
|
|
-
|
|
|
|
|
-**RedirectUri (HTTPS required):**
|
|
|
|
|
-```
|
|
|
|
|
-https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/oauth-shoprenter-callback
|
|
|
|
|
-```
|
|
|
|
|
-- OAuth callback endpoint
|
|
|
|
|
-- Receives: shopname, code, timestamp, hmac, app_url
|
|
|
|
|
-- Validates HMAC and exchanges code for tokens
|
|
|
|
|
-
|
|
|
|
|
-**UninstallUri (HTTPS required):**
|
|
|
|
|
-```
|
|
|
|
|
-https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/webhook-shoprenter-uninstall
|
|
|
|
|
-```
|
|
|
|
|
-- Called when app is uninstalled
|
|
|
|
|
-- Receives: shopname, code, timestamp, hmac
|
|
|
|
|
-- Cleanup: remove tokens, deactivate services
|
|
|
|
|
-
|
|
|
|
|
-#### 3. Visual Assets
|
|
|
|
|
-
|
|
|
|
|
-**Application Logo:**
|
|
|
|
|
-- Dimensions: **250x150px**
|
|
|
|
|
-- Format: PNG with transparency
|
|
|
|
|
-- Location: `/shopcall.ai-main/public/images/shoprenter-app-logo.png`
|
|
|
|
|
-
|
|
|
|
|
-#### 4. Test Store
|
|
|
|
|
-
|
|
|
|
|
-**Test Store Name:**
|
|
|
|
|
-```
|
|
|
|
|
-shopcall-test-store
|
|
|
|
|
-```
|
|
|
|
|
-- URL will be: `shopcall-test-store.shoprenter.hu`
|
|
|
|
|
-- Request at: https://www.shoprenter.hu/tesztigenyles/?devstore=1
|
|
|
|
|
-
|
|
|
|
|
-#### 5. Required Scopes
|
|
|
|
|
-
|
|
|
|
|
-Based on ShopCall.ai functionality, we need:
|
|
|
|
|
-
|
|
|
|
|
-```
|
|
|
|
|
-product:read # Read product catalog
|
|
|
|
|
-product:write # Update product information (if needed)
|
|
|
|
|
-customer:read # Access customer data (including email, phone, addresses)
|
|
|
|
|
-customer:write # Update customer information (notes, tags)
|
|
|
|
|
-order:read # Read order information (including customer email, phone)
|
|
|
|
|
-order:write # Update order status, add notes
|
|
|
|
|
-category:read # Read product categories
|
|
|
|
|
-inventory:read # Access real-time stock information
|
|
|
|
|
-webhook:read # List existing webhooks
|
|
|
|
|
-webhook:write # Create/update webhooks for real-time sync
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Scope Justification:**
|
|
|
|
|
-- **product:read** - Sync product catalog for AI knowledge base
|
|
|
|
|
-- **product:write** - Future capability to update stock levels, prices
|
|
|
|
|
-- **customer:read** - Access customer data including email, phone numbers, billing/shipping addresses for personalized AI responses
|
|
|
|
|
-- **customer:write** - Add call notes, tags, and interaction history to customer records
|
|
|
|
|
-- **order:read** - Access order details including customer contact info (email, phone) for order status inquiries
|
|
|
|
|
-- **order:write** - Update order notes and status after customer calls
|
|
|
|
|
-- **category:read** - Organize products by category for better AI product recommendations
|
|
|
|
|
-- **inventory:read** - Provide real-time stock availability information during calls
|
|
|
|
|
-- **webhook:read** - List existing webhook registrations to avoid duplicates
|
|
|
|
|
-- **webhook:write** - Register webhooks for real-time product/order/customer updates
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 🏗️ Technical Architecture
|
|
|
|
|
-
|
|
|
|
|
-### System Overview
|
|
|
|
|
-
|
|
|
|
|
-```
|
|
|
|
|
-┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
|
|
|
-│ ShopRenter │ │ ShopCall.ai │ │ Supabase │
|
|
|
|
|
-│ Store │◄───────►│ Backend │◄───────►│ Database │
|
|
|
|
|
-│ │ OAuth │ │ Store │ │
|
|
|
|
|
-└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
|
|
|
- │ │
|
|
|
|
|
- │ Webhooks │ API Calls
|
|
|
|
|
- ▼ ▼
|
|
|
|
|
-┌─────────────────┐ ┌──────────────────┐
|
|
|
|
|
-│ Product Sync │ │ AI Phone │
|
|
|
|
|
-│ Order Updates │ │ Assistant │
|
|
|
|
|
-└─────────────────┘ └──────────────────┘
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### Component Breakdown
|
|
|
|
|
-
|
|
|
|
|
-**Frontend (React/Vite):**
|
|
|
|
|
-- ShopRenter connection button
|
|
|
|
|
-- OAuth initiation
|
|
|
|
|
-- Store configuration UI
|
|
|
|
|
-- Success/error handling
|
|
|
|
|
-
|
|
|
|
|
-**Backend (Supabase Edge Functions - Deno/TypeScript):**
|
|
|
|
|
-- OAuth flow handling
|
|
|
|
|
-- HMAC validation
|
|
|
|
|
-- Token management
|
|
|
|
|
-- API client for ShopRenter
|
|
|
|
|
-- Webhook receivers
|
|
|
|
|
-- Serverless deployment on Supabase infrastructure
|
|
|
|
|
-
|
|
|
|
|
-**Database (Supabase):**
|
|
|
|
|
-- Store credentials storage
|
|
|
|
|
-- Product catalog cache
|
|
|
|
|
-- Sync status tracking
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 🔐 OAuth Flow Implementation
|
|
|
|
|
-
|
|
|
|
|
-### Installation Process Flow
|
|
|
|
|
-
|
|
|
|
|
-```mermaid
|
|
|
|
|
-sequenceDiagram
|
|
|
|
|
- participant Merchant
|
|
|
|
|
- participant ShopRenter
|
|
|
|
|
- participant ShopCall Backend
|
|
|
|
|
- participant Database
|
|
|
|
|
-
|
|
|
|
|
- Merchant->>ShopRenter: Click "Install ShopCall.ai"
|
|
|
|
|
- ShopRenter->>ShopCall Backend: GET /auth/shoprenter/callback?shopname=X&code=Y×tamp=Z&hmac=H&app_url=U
|
|
|
|
|
- ShopCall Backend->>ShopCall Backend: Validate HMAC (sha256)
|
|
|
|
|
- alt HMAC Valid
|
|
|
|
|
- ShopCall Backend->>Database: Store installation data
|
|
|
|
|
- ShopCall Backend->>Merchant: Redirect to app_url
|
|
|
|
|
- ShopRenter->>ShopCall Backend: GET /integrations/shoprenter/entry?shopname=X&code=Y×tamp=Z&hmac=H
|
|
|
|
|
- ShopCall Backend->>ShopCall Backend: Validate HMAC again
|
|
|
|
|
- ShopCall Backend->>ShopCall Backend: Request OAuth token from ShopRenter
|
|
|
|
|
- ShopCall Backend->>Database: Store access token
|
|
|
|
|
- ShopCall Backend->>Merchant: Show success page
|
|
|
|
|
- else HMAC Invalid
|
|
|
|
|
- ShopCall Backend->>Merchant: Show error (403)
|
|
|
|
|
- end
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### HMAC Validation Process
|
|
|
|
|
-
|
|
|
|
|
-**ShopRenter sends:**
|
|
|
|
|
-```
|
|
|
|
|
-GET /auth/shoprenter/callback?shopname=example&code=0907a61c0c8d55e99db179b68161bc00×tamp=1337178173&hmac=d48e5d...&app_url=...
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Validation Algorithm:**
|
|
|
|
|
-
|
|
|
|
|
-```javascript
|
|
|
|
|
-function validateHMAC(query, clientSecret) {
|
|
|
|
|
- // 1. Extract HMAC from query
|
|
|
|
|
- const { hmac, ...params } = query;
|
|
|
|
|
-
|
|
|
|
|
- // 2. Build query string without HMAC (sorted alphabetically)
|
|
|
|
|
- const sortedParams = Object.keys(params)
|
|
|
|
|
- .sort()
|
|
|
|
|
- .map(key => `${key}=${params[key]}`)
|
|
|
|
|
- .join('&');
|
|
|
|
|
-
|
|
|
|
|
- // Example: "code=0907a61c0c8d55e99db179b68161bc00&shopname=example×tamp=1337178173"
|
|
|
|
|
-
|
|
|
|
|
- // 3. Generate HMAC using sha256
|
|
|
|
|
- const crypto = require('crypto');
|
|
|
|
|
- const calculatedHmac = crypto
|
|
|
|
|
- .createHmac('sha256', clientSecret)
|
|
|
|
|
- .update(sortedParams)
|
|
|
|
|
- .digest('hex');
|
|
|
|
|
-
|
|
|
|
|
- // 4. Compare (timing-safe)
|
|
|
|
|
- return crypto.timingSafeEqual(
|
|
|
|
|
- Buffer.from(calculatedHmac),
|
|
|
|
|
- Buffer.from(hmac)
|
|
|
|
|
- );
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Security Notes:**
|
|
|
|
|
-- ⚠️ **Always validate HMAC** before processing request
|
|
|
|
|
-- ⚠️ Use timing-safe comparison to prevent timing attacks
|
|
|
|
|
-- ⚠️ Check timestamp to prevent replay attacks (within 5 minutes)
|
|
|
|
|
-- ⚠️ Store ClientSecret securely in environment variables
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 🔌 API Integration
|
|
|
|
|
-
|
|
|
|
|
-### OAuth Token Acquisition
|
|
|
|
|
-
|
|
|
|
|
-After successful HMAC validation, request access token:
|
|
|
|
|
-
|
|
|
|
|
-**Endpoint:** (To be confirmed with ShopRenter Partner Support)
|
|
|
|
|
-```
|
|
|
|
|
-POST https://{shopname}.shoprenter.hu/oauth/token
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Request Body:**
|
|
|
|
|
-```json
|
|
|
|
|
-{
|
|
|
|
|
- "grant_type": "authorization_code",
|
|
|
|
|
- "client_id": "{ClientId}",
|
|
|
|
|
- "client_secret": "{ClientSecret}",
|
|
|
|
|
- "code": "{code_from_callback}",
|
|
|
|
|
- "redirect_uri": "https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/oauth-shoprenter-callback"
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Response:**
|
|
|
|
|
-```json
|
|
|
|
|
-{
|
|
|
|
|
- "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
|
|
|
|
- "token_type": "Bearer",
|
|
|
|
|
- "expires_in": 3600,
|
|
|
|
|
- "refresh_token": "def50200...",
|
|
|
|
|
- "scope": "product:read customer:read order:read"
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### API Base URL
|
|
|
|
|
-
|
|
|
|
|
-```
|
|
|
|
|
-https://{shopname}.shoprenter.hu/api
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### Common API Endpoints
|
|
|
|
|
-
|
|
|
|
|
-Based on ShopRenter API documentation:
|
|
|
|
|
-
|
|
|
|
|
-#### 1. Products
|
|
|
|
|
-
|
|
|
|
|
-**List Products:**
|
|
|
|
|
-```
|
|
|
|
|
-GET /api/products
|
|
|
|
|
-Authorization: Bearer {access_token}
|
|
|
|
|
-
|
|
|
|
|
-Response:
|
|
|
|
|
-{
|
|
|
|
|
- "items": [
|
|
|
|
|
- {
|
|
|
|
|
- "id": "cHJvZHVjdC1wcm9kdWN0X2lkPTE=",
|
|
|
|
|
- "name": "Product Name",
|
|
|
|
|
- "sku": "SKU-123",
|
|
|
|
|
- "price": "9990.00",
|
|
|
|
|
- "currency": "HUF",
|
|
|
|
|
- "description": "Product description",
|
|
|
|
|
- "stock": 10,
|
|
|
|
|
- "active": true
|
|
|
|
|
- }
|
|
|
|
|
- ],
|
|
|
|
|
- "pagination": {
|
|
|
|
|
- "total": 150,
|
|
|
|
|
- "page": 1,
|
|
|
|
|
- "limit": 25
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-#### 2. Customers
|
|
|
|
|
-
|
|
|
|
|
-**List Customers:**
|
|
|
|
|
-```
|
|
|
|
|
-GET /api/customers
|
|
|
|
|
-Authorization: Bearer {access_token}
|
|
|
|
|
-
|
|
|
|
|
-Response:
|
|
|
|
|
-{
|
|
|
|
|
- "items": [
|
|
|
|
|
- {
|
|
|
|
|
- "id": "Y3VzdG9tZXItY3VzdG9tZXJfaWQ9MQ==",
|
|
|
|
|
- "firstname": "János",
|
|
|
|
|
- "lastname": "Kovács",
|
|
|
|
|
- "email": "janos.kovacs@example.com",
|
|
|
|
|
- "phone": "+36201234567",
|
|
|
|
|
- "created_at": "2024-01-15T10:30:00Z"
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-#### 3. Orders
|
|
|
|
|
-
|
|
|
|
|
-**List Orders:**
|
|
|
|
|
-```
|
|
|
|
|
-GET /api/orders
|
|
|
|
|
-Authorization: Bearer {access_token}
|
|
|
|
|
-
|
|
|
|
|
-Response:
|
|
|
|
|
-{
|
|
|
|
|
- "items": [
|
|
|
|
|
- {
|
|
|
|
|
- "id": "b3JkZXItb3JkZXJfaWQ9MQ==",
|
|
|
|
|
- "order_number": "SR-2024-001",
|
|
|
|
|
- "customer_id": "Y3VzdG9tZXItY3VzdG9tZXJfaWQ9MQ==",
|
|
|
|
|
- "status": "processing",
|
|
|
|
|
- "total": "29990.00",
|
|
|
|
|
- "currency": "HUF",
|
|
|
|
|
- "created_at": "2024-01-20T14:25:00Z",
|
|
|
|
|
- "items": [...]
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-#### 4. Webhooks
|
|
|
|
|
-
|
|
|
|
|
-**Register Webhook:**
|
|
|
|
|
-```
|
|
|
|
|
-POST /api/webhooks
|
|
|
|
|
-Authorization: Bearer {access_token}
|
|
|
|
|
-Content-Type: application/json
|
|
|
|
|
-
|
|
|
|
|
-Body:
|
|
|
|
|
-{
|
|
|
|
|
- "event": "order/create",
|
|
|
|
|
- "address": "https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/webhook-shoprenter-orders",
|
|
|
|
|
- "active": true
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Response:
|
|
|
|
|
-{
|
|
|
|
|
- "id": "d2ViaG9vay13ZWJob29rX2lkPTE=",
|
|
|
|
|
- "event": "order/create",
|
|
|
|
|
- "address": "https://...",
|
|
|
|
|
- "active": true
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Available Webhook Events:**
|
|
|
|
|
-- `order/create` - New order created
|
|
|
|
|
-- `order/update` - Order status changed
|
|
|
|
|
-- `product/create` - New product added
|
|
|
|
|
-- `product/update` - Product information changed
|
|
|
|
|
-- `product/delete` - Product deleted
|
|
|
|
|
-- `customer/create` - New customer registered
|
|
|
|
|
-- `customer/update` - Customer information updated
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 💾 Database Schema
|
|
|
|
|
-
|
|
|
|
|
-### New Table: `shoprenter_tokens`
|
|
|
|
|
-
|
|
|
|
|
-```sql
|
|
|
|
|
-CREATE TABLE shoprenter_tokens (
|
|
|
|
|
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
|
- store_id UUID REFERENCES stores(id) ON DELETE CASCADE,
|
|
|
|
|
-
|
|
|
|
|
- -- OAuth tokens
|
|
|
|
|
- access_token TEXT NOT NULL,
|
|
|
|
|
- refresh_token TEXT,
|
|
|
|
|
- token_type VARCHAR(20) DEFAULT 'Bearer',
|
|
|
|
|
- expires_at TIMESTAMP WITH TIME ZONE,
|
|
|
|
|
-
|
|
|
|
|
- -- Scopes
|
|
|
|
|
- scopes TEXT[], -- ['product:read', 'customer:read', ...]
|
|
|
|
|
-
|
|
|
|
|
- -- Store information
|
|
|
|
|
- shopname VARCHAR(255) NOT NULL, -- e.g., 'example' from 'example.shoprenter.hu'
|
|
|
|
|
- shop_domain VARCHAR(255) NOT NULL, -- Full domain
|
|
|
|
|
-
|
|
|
|
|
- -- Status
|
|
|
|
|
- is_active BOOLEAN DEFAULT true,
|
|
|
|
|
- last_sync_at TIMESTAMP WITH TIME ZONE,
|
|
|
|
|
-
|
|
|
|
|
- -- Metadata
|
|
|
|
|
- created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
|
|
|
- updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
|
|
|
-
|
|
|
|
|
- -- Constraints
|
|
|
|
|
- UNIQUE(store_id)
|
|
|
|
|
-);
|
|
|
|
|
-
|
|
|
|
|
--- Index for fast lookups
|
|
|
|
|
-CREATE INDEX idx_shoprenter_tokens_shopname ON shoprenter_tokens(shopname);
|
|
|
|
|
-CREATE INDEX idx_shoprenter_tokens_active ON shoprenter_tokens(is_active);
|
|
|
|
|
-
|
|
|
|
|
--- Trigger for updated_at
|
|
|
|
|
-CREATE TRIGGER set_shoprenter_tokens_updated_at
|
|
|
|
|
-BEFORE UPDATE ON shoprenter_tokens
|
|
|
|
|
-FOR EACH ROW
|
|
|
|
|
-EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### Update Existing `stores` Table
|
|
|
|
|
-
|
|
|
|
|
-```sql
|
|
|
|
|
--- Add ShopRenter-specific columns
|
|
|
|
|
-ALTER TABLE stores ADD COLUMN IF NOT EXISTS shoprenter_app_id VARCHAR(50);
|
|
|
|
|
-ALTER TABLE stores ADD COLUMN IF NOT EXISTS shoprenter_client_id VARCHAR(255);
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### New Table: `shoprenter_products_cache`
|
|
|
|
|
-
|
|
|
|
|
-```sql
|
|
|
|
|
-CREATE TABLE shoprenter_products_cache (
|
|
|
|
|
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
|
- store_id UUID REFERENCES stores(id) ON DELETE CASCADE,
|
|
|
|
|
-
|
|
|
|
|
- -- Product data from ShopRenter API
|
|
|
|
|
- shoprenter_product_id VARCHAR(255) NOT NULL,
|
|
|
|
|
- name VARCHAR(500),
|
|
|
|
|
- sku VARCHAR(255),
|
|
|
|
|
- price DECIMAL(10,2),
|
|
|
|
|
- currency VARCHAR(3) DEFAULT 'HUF',
|
|
|
|
|
- description TEXT,
|
|
|
|
|
- stock INTEGER,
|
|
|
|
|
- active BOOLEAN,
|
|
|
|
|
-
|
|
|
|
|
- -- Raw data
|
|
|
|
|
- raw_data JSONB, -- Full API response
|
|
|
|
|
-
|
|
|
|
|
- -- Sync metadata
|
|
|
|
|
- last_synced_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
|
|
|
-
|
|
|
|
|
- created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
|
|
|
- updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
|
|
|
-
|
|
|
|
|
- -- Constraints
|
|
|
|
|
- UNIQUE(store_id, shoprenter_product_id)
|
|
|
|
|
-);
|
|
|
|
|
-
|
|
|
|
|
--- Indexes
|
|
|
|
|
-CREATE INDEX idx_shoprenter_products_store ON shoprenter_products_cache(store_id);
|
|
|
|
|
-CREATE INDEX idx_shoprenter_products_sku ON shoprenter_products_cache(sku);
|
|
|
|
|
-CREATE INDEX idx_shoprenter_products_active ON shoprenter_products_cache(active);
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### New Table: `shoprenter_webhooks`
|
|
|
|
|
-
|
|
|
|
|
-```sql
|
|
|
|
|
-CREATE TABLE shoprenter_webhooks (
|
|
|
|
|
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
|
- store_id UUID REFERENCES stores(id) ON DELETE CASCADE,
|
|
|
|
|
-
|
|
|
|
|
- -- Webhook details
|
|
|
|
|
- shoprenter_webhook_id VARCHAR(255),
|
|
|
|
|
- event VARCHAR(100) NOT NULL, -- 'order/create', 'product/update', etc.
|
|
|
|
|
- callback_url TEXT NOT NULL,
|
|
|
|
|
- is_active BOOLEAN DEFAULT true,
|
|
|
|
|
-
|
|
|
|
|
- -- Statistics
|
|
|
|
|
- last_received_at TIMESTAMP WITH TIME ZONE,
|
|
|
|
|
- total_received INTEGER DEFAULT 0,
|
|
|
|
|
-
|
|
|
|
|
- created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
|
|
|
- updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
|
|
|
-
|
|
|
|
|
- UNIQUE(store_id, event)
|
|
|
|
|
-);
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 🔧 Backend Implementation
|
|
|
|
|
-
|
|
|
|
|
-### File Structure
|
|
|
|
|
-
|
|
|
|
|
-**Current Implementation: Supabase Edge Functions (TypeScript/Deno)**
|
|
|
|
|
-
|
|
|
|
|
-```
|
|
|
|
|
-supabase/functions/
|
|
|
|
|
-├── oauth-shoprenter-init/
|
|
|
|
|
-│ └── index.ts # OAuth flow initialization
|
|
|
|
|
-├── oauth-shoprenter-callback/
|
|
|
|
|
-│ └── index.ts # OAuth callback handler
|
|
|
|
|
-├── webhook-shoprenter-uninstall/
|
|
|
|
|
-│ └── index.ts # Uninstall webhook handler
|
|
|
|
|
-├── shoprenter-products/
|
|
|
|
|
-│ └── index.ts # Product sync endpoint
|
|
|
|
|
-├── shoprenter-orders/
|
|
|
|
|
-│ └── index.ts # Order sync endpoint
|
|
|
|
|
-├── shoprenter-customers/
|
|
|
|
|
-│ └── index.ts # Customer sync endpoint
|
|
|
|
|
-├── shoprenter-sync/
|
|
|
|
|
-│ └── index.ts # Manual sync trigger
|
|
|
|
|
-├── shoprenter-scheduled-sync/
|
|
|
|
|
-│ └── index.ts # Automated background sync (pg_cron)
|
|
|
|
|
-└── _shared/
|
|
|
|
|
- └── shoprenter-client.ts # ShopRenter API client library
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Note:** All Edge Functions are deployed as serverless functions on Supabase infrastructure.
|
|
|
|
|
-
|
|
|
|
|
-### 1. Environment Configuration
|
|
|
|
|
-
|
|
|
|
|
-**Environment Variables (Supabase Edge Functions):**
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-// Accessed via Deno.env.get() in Edge Functions
|
|
|
|
|
-const config = {
|
|
|
|
|
- clientId: Deno.env.get('SHOPRENTER_CLIENT_ID'),
|
|
|
|
|
- clientSecret: Deno.env.get('SHOPRENTER_CLIENT_SECRET'),
|
|
|
|
|
-
|
|
|
|
|
- // Supabase URLs
|
|
|
|
|
- supabaseUrl: Deno.env.get('SUPABASE_URL'),
|
|
|
|
|
- supabaseAnonKey: Deno.env.get('SUPABASE_ANON_KEY'),
|
|
|
|
|
- supabaseServiceKey: Deno.env.get('SUPABASE_SERVICE_ROLE_KEY'),
|
|
|
|
|
-
|
|
|
|
|
- // Frontend URL
|
|
|
|
|
- frontendUrl: Deno.env.get('FRONTEND_URL'),
|
|
|
|
|
-
|
|
|
|
|
- // Production endpoints
|
|
|
|
|
- redirectUri: 'https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/oauth-shoprenter-callback',
|
|
|
|
|
- entryPoint: 'https://shopcall.ai/integrations',
|
|
|
|
|
- uninstallUri: 'https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/webhook-shoprenter-uninstall',
|
|
|
|
|
-
|
|
|
|
|
- // Required scopes
|
|
|
|
|
- scopes: [
|
|
|
|
|
- 'product:read',
|
|
|
|
|
- 'customer:read',
|
|
|
|
|
- 'order:read',
|
|
|
|
|
- 'order:write',
|
|
|
|
|
- 'webhook:write'
|
|
|
|
|
- ],
|
|
|
|
|
-
|
|
|
|
|
- // Rate limiting
|
|
|
|
|
- maxRequestsPerSecond: 5,
|
|
|
|
|
- tokenExpiryBuffer: 300, // Refresh 5 min before expiry
|
|
|
|
|
-};
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Configuration in supabase/.env:**
|
|
|
|
|
-```bash
|
|
|
|
|
-SHOPRENTER_CLIENT_ID=your_client_id_here
|
|
|
|
|
-SHOPRENTER_CLIENT_SECRET=your_client_secret_here
|
|
|
|
|
-SUPABASE_URL=https://ztklqodcdjeqpsvhlpud.supabase.co
|
|
|
|
|
-SUPABASE_ANON_KEY=your_anon_key
|
|
|
|
|
-SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
|
|
|
|
|
-FRONTEND_URL=https://shopcall.ai
|
|
|
|
|
-INTERNAL_SYNC_SECRET=your_random_secure_secret
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 2. HMAC Validator (`_shared/shoprenter-client.ts`)
|
|
|
|
|
-
|
|
|
|
|
-**Note:** HMAC validation is implemented in the shared ShopRenter client library used by all Edge Functions.
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-import { createHmac, timingSafeEqual } from "https://deno.land/std@0.177.0/node/crypto.ts";
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * Validate HMAC signature from ShopRenter
|
|
|
|
|
- * @param query - Query parameters from request
|
|
|
|
|
- * @param clientSecret - ShopRenter client secret
|
|
|
|
|
- * @returns True if HMAC is valid
|
|
|
|
|
- */
|
|
|
|
|
-function validateHMAC(query: Record<string, string>, clientSecret: string): boolean {
|
|
|
|
|
- const { hmac, ...params } = query;
|
|
|
|
|
-
|
|
|
|
|
- if (!hmac) {
|
|
|
|
|
- console.error('[ShopRenter] HMAC missing from request');
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Build sorted query string without HMAC
|
|
|
|
|
- const sortedParams = Object.keys(params)
|
|
|
|
|
- .sort()
|
|
|
|
|
- .map(key => `${key}=${params[key]}`)
|
|
|
|
|
- .join('&');
|
|
|
|
|
-
|
|
|
|
|
- // Calculate HMAC using sha256
|
|
|
|
|
- const calculatedHmac = createHmac('sha256', clientSecret)
|
|
|
|
|
- .update(sortedParams)
|
|
|
|
|
- .digest('hex');
|
|
|
|
|
-
|
|
|
|
|
- // Timing-safe comparison
|
|
|
|
|
- try {
|
|
|
|
|
- return timingSafeEqual(
|
|
|
|
|
- new TextEncoder().encode(calculatedHmac),
|
|
|
|
|
- new TextEncoder().encode(hmac)
|
|
|
|
|
- );
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('[ShopRenter] HMAC comparison error:', error);
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * Validate request timestamp (prevent replay attacks)
|
|
|
|
|
- * @param timestamp - Unix timestamp from request
|
|
|
|
|
- * @param maxAgeSeconds - Maximum age allowed (default: 300 = 5 min)
|
|
|
|
|
- * @returns True if timestamp is within valid range
|
|
|
|
|
- */
|
|
|
|
|
-function validateTimestamp(timestamp: string, maxAgeSeconds = 300): boolean {
|
|
|
|
|
- const requestTime = parseInt(timestamp, 10);
|
|
|
|
|
- const currentTime = Math.floor(Date.now() / 1000);
|
|
|
|
|
- const age = currentTime - requestTime;
|
|
|
|
|
-
|
|
|
|
|
- if (age < 0) {
|
|
|
|
|
- console.error('[ShopRenter] Request timestamp is in the future');
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (age > maxAgeSeconds) {
|
|
|
|
|
- console.error(`[ShopRenter] Request timestamp too old: ${age}s > ${maxAgeSeconds}s`);
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return true;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * Validate complete ShopRenter request
|
|
|
|
|
- * @param query - Query parameters
|
|
|
|
|
- * @param clientSecret - ShopRenter client secret
|
|
|
|
|
- * @returns Validation result
|
|
|
|
|
- */
|
|
|
|
|
-function validateRequest(
|
|
|
|
|
- query: Record<string, string>,
|
|
|
|
|
- clientSecret: string
|
|
|
|
|
-): { valid: boolean; error?: string } {
|
|
|
|
|
- // Check required parameters
|
|
|
|
|
- const required = ['shopname', 'code', 'timestamp', 'hmac'];
|
|
|
|
|
- const missing = required.filter(param => !query[param]);
|
|
|
|
|
-
|
|
|
|
|
- if (missing.length > 0) {
|
|
|
|
|
- return {
|
|
|
|
|
- valid: false,
|
|
|
|
|
- error: `Missing required parameters: ${missing.join(', ')}`
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Validate timestamp
|
|
|
|
|
- if (!validateTimestamp(query.timestamp)) {
|
|
|
|
|
- return {
|
|
|
|
|
- valid: false,
|
|
|
|
|
- error: 'Request timestamp invalid or expired'
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Validate HMAC
|
|
|
|
|
- if (!validateHMAC(query, clientSecret)) {
|
|
|
|
|
- return {
|
|
|
|
|
- valid: false,
|
|
|
|
|
- error: 'HMAC validation failed'
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return { valid: true };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export {
|
|
|
|
|
- validateHMAC,
|
|
|
|
|
- validateTimestamp,
|
|
|
|
|
- validateRequest
|
|
|
|
|
-};
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 3. OAuth Handler (Edge Functions)
|
|
|
|
|
-
|
|
|
|
|
-**Implementation:** The OAuth flow is handled by two Edge Functions:
|
|
|
|
|
-- `oauth-shoprenter-init` - Initiates OAuth flow
|
|
|
|
|
-- `oauth-shoprenter-callback` - Handles OAuth callback and token exchange
|
|
|
|
|
-
|
|
|
|
|
-**Example TypeScript Code (simplified from actual implementation):**
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
|
|
|
|
-
|
|
|
|
|
-const supabase = createClient(
|
|
|
|
|
- Deno.env.get('SUPABASE_URL')!,
|
|
|
|
|
- Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
|
|
|
|
-);
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * Exchange authorization code for access token
|
|
|
|
|
- * @param shopname - Store name
|
|
|
|
|
- * @param code - Authorization code
|
|
|
|
|
- * @returns Token response
|
|
|
|
|
- */
|
|
|
|
|
-async function exchangeCodeForToken(shopname: string, code: string) {
|
|
|
|
|
- const tokenUrl = `https://${shopname}.shoprenter.hu/oauth/token`;
|
|
|
|
|
- const clientId = Deno.env.get('SHOPRENTER_CLIENT_ID')!;
|
|
|
|
|
- const clientSecret = Deno.env.get('SHOPRENTER_CLIENT_SECRET')!;
|
|
|
|
|
- const redirectUri = 'https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/oauth-shoprenter-callback';
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- const response = await fetch(tokenUrl, {
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- headers: {
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- 'Accept': 'application/json'
|
|
|
|
|
- },
|
|
|
|
|
- body: JSON.stringify({
|
|
|
|
|
- grant_type: 'authorization_code',
|
|
|
|
|
- client_id: clientId,
|
|
|
|
|
- client_secret: clientSecret,
|
|
|
|
|
- code: code,
|
|
|
|
|
- redirect_uri: redirectUri
|
|
|
|
|
- })
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- if (!response.ok) {
|
|
|
|
|
- throw new Error(`Token exchange failed: ${response.statusText}`);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const data = await response.json();
|
|
|
|
|
- console.log(`[ShopRenter] Token acquired for ${shopname}`);
|
|
|
|
|
- return data;
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('[ShopRenter] Token exchange error:', error);
|
|
|
|
|
- throw new Error('Failed to exchange code for token');
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * Store ShopRenter credentials in database
|
|
|
|
|
- * Note: Actual implementation in oauth-shoprenter-callback Edge Function
|
|
|
|
|
- */
|
|
|
|
|
-async function storeCredentials(userId: string, shopname: string, tokenData: any) {
|
|
|
|
|
- const shopDomain = `${shopname}.shoprenter.hu`;
|
|
|
|
|
- const expiresAt = new Date(Date.now() + (tokenData.expires_in * 1000));
|
|
|
|
|
-
|
|
|
|
|
- // 1. Create or update stores record
|
|
|
|
|
- const { data: store, error: storeError } = await supabase
|
|
|
|
|
- .from('stores')
|
|
|
|
|
- .upsert({
|
|
|
|
|
- user_id: userId,
|
|
|
|
|
- platform_name: 'shoprenter',
|
|
|
|
|
- store_name: shopname,
|
|
|
|
|
- store_url: `https://${shopDomain}`,
|
|
|
|
|
- api_key: tokenData.access_token,
|
|
|
|
|
- api_secret: tokenData.refresh_token,
|
|
|
|
|
- token_expires_at: expiresAt.toISOString(),
|
|
|
|
|
- scopes: tokenData.scope ? tokenData.scope.split(' ') : [],
|
|
|
|
|
- is_active: true,
|
|
|
|
|
- connected_at: new Date().toISOString()
|
|
|
|
|
- })
|
|
|
|
|
- .select()
|
|
|
|
|
- .single();
|
|
|
|
|
-
|
|
|
|
|
- if (storeError) {
|
|
|
|
|
- console.error('[ShopRenter] Error storing store:', storeError);
|
|
|
|
|
- throw storeError;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- console.log(`[ShopRenter] Credentials stored for ${shopname}`);
|
|
|
|
|
- return store;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// Additional functions for token refresh are implemented in the ShopRenter client library
|
|
|
|
|
-// See: supabase/functions/_shared/shoprenter-client.ts
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Note:** The complete OAuth implementation including token refresh, HMAC validation, and credential storage is available in the deployed Edge Functions. The code examples above are simplified for documentation purposes.
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### 4. Deployed Edge Functions
|
|
|
|
|
-
|
|
|
|
|
-All ShopRenter integration functionality is implemented as Supabase Edge Functions (TypeScript/Deno). Below is a summary of each function:
|
|
|
|
|
-
|
|
|
|
|
-#### **oauth-shoprenter-init**
|
|
|
|
|
-- **Purpose:** Initialize OAuth flow
|
|
|
|
|
-- **Endpoint:** `/functions/v1/oauth-shoprenter-init`
|
|
|
|
|
-- **Method:** GET/POST
|
|
|
|
|
-- **Features:** Generates state, stores in database, redirects to ShopRenter OAuth
|
|
|
|
|
-
|
|
|
|
|
-#### **oauth-shoprenter-callback**
|
|
|
|
|
-- **Purpose:** Handle OAuth callback
|
|
|
|
|
-- **Endpoint:** `/functions/v1/oauth-shoprenter-callback`
|
|
|
|
|
-- **Method:** GET
|
|
|
|
|
-- **Parameters:** `shopname`, `code`, `timestamp`, `hmac`, `app_url`
|
|
|
|
|
-- **Features:**
|
|
|
|
|
- - Validates HMAC signature
|
|
|
|
|
- - Validates timestamp (5-minute window)
|
|
|
|
|
- - Exchanges code for access token
|
|
|
|
|
- - Stores credentials in `pending_shoprenter_installs` table
|
|
|
|
|
- - Redirects to frontend with installation_id
|
|
|
|
|
-
|
|
|
|
|
-#### **webhook-shoprenter-uninstall**
|
|
|
|
|
-- **Purpose:** Handle app uninstall
|
|
|
|
|
-- **Endpoint:** `/functions/v1/webhook-shoprenter-uninstall`
|
|
|
|
|
-- **Method:** GET/POST
|
|
|
|
|
-- **Parameters:** `shopname`, `code`, `timestamp`, `hmac`
|
|
|
|
|
-- **Features:**
|
|
|
|
|
- - Validates HMAC signature
|
|
|
|
|
- - Deactivates store connection
|
|
|
|
|
- - Removes stored tokens
|
|
|
|
|
- - Deletes cached data (GDPR compliance)
|
|
|
|
|
-
|
|
|
|
|
-#### **shoprenter-products**
|
|
|
|
|
-- **Purpose:** Fetch products from ShopRenter API
|
|
|
|
|
-- **Endpoint:** `/functions/v1/shoprenter-products`
|
|
|
|
|
-- **Method:** POST
|
|
|
|
|
-- **Body:** `{ store_id: string }`
|
|
|
|
|
-- **Features:** Fetches and caches product data
|
|
|
|
|
-
|
|
|
|
|
-#### **shoprenter-orders**
|
|
|
|
|
-- **Purpose:** Fetch orders from ShopRenter API
|
|
|
|
|
-- **Endpoint:** `/functions/v1/shoprenter-orders`
|
|
|
|
|
-- **Method:** POST
|
|
|
|
|
-- **Body:** `{ store_id: string }`
|
|
|
|
|
-- **Features:** Fetches and caches order data
|
|
|
|
|
-
|
|
|
|
|
-#### **shoprenter-customers**
|
|
|
|
|
-- **Purpose:** Fetch customers from ShopRenter API
|
|
|
|
|
-- **Endpoint:** `/functions/v1/shoprenter-customers`
|
|
|
|
|
-- **Method:** POST
|
|
|
|
|
-- **Body:** `{ store_id: string }`
|
|
|
|
|
-- **Features:** Fetches and caches customer data
|
|
|
|
|
-
|
|
|
|
|
-#### **shoprenter-sync**
|
|
|
|
|
-- **Purpose:** Manual sync trigger
|
|
|
|
|
-- **Endpoint:** `/functions/v1/shoprenter-sync`
|
|
|
|
|
-- **Method:** POST
|
|
|
|
|
-- **Body:** `{ store_id: string }`
|
|
|
|
|
-- **Features:** Triggers full sync (products, orders, customers)
|
|
|
|
|
-
|
|
|
|
|
-#### **shoprenter-scheduled-sync**
|
|
|
|
|
-- **Purpose:** Automated background sync (called by pg_cron)
|
|
|
|
|
-- **Endpoint:** `/functions/v1/shoprenter-scheduled-sync`
|
|
|
|
|
-- **Method:** POST
|
|
|
|
|
-- **Headers:** `Authorization: Bearer {INTERNAL_SYNC_SECRET}`
|
|
|
|
|
-- **Features:**
|
|
|
|
|
- - Syncs all active ShopRenter stores
|
|
|
|
|
- - Updates `sync_logs` table
|
|
|
|
|
- - Handles errors gracefully
|
|
|
|
|
- - Rate limiting and retry logic
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### 5. Old Express.js Routes (DEPRECATED)
|
|
|
|
|
-
|
|
|
|
|
-**The following Express.js/Node.js routes are NO LONGER USED.** They have been replaced by the Supabase Edge Functions listed above.
|
|
|
|
|
-
|
|
|
|
|
-<details>
|
|
|
|
|
-<summary>Click to view deprecated Express.js code (for reference only)</summary>
|
|
|
|
|
-
|
|
|
|
|
-```javascript
|
|
|
|
|
-// DEPRECATED - DO NOT USE
|
|
|
|
|
-// This code is kept for historical reference only
|
|
|
|
|
-const { validateRequest } = require('../lib/shoprenter/hmac-validator');
|
|
|
|
|
-const { exchangeCodeForToken, storeCredentials } = require('../lib/shoprenter/oauth');
|
|
|
|
|
-
|
|
|
|
|
-// ShopRenter OAuth Callback
|
|
|
|
|
-app.get('/auth/shoprenter/callback', async (req, res) => {
|
|
|
|
|
- const { shopname, code, timestamp, hmac, app_url } = req.query;
|
|
|
|
|
-
|
|
|
|
|
- console.log(`[ShopRenter] OAuth callback received for ${shopname}`);
|
|
|
|
|
-
|
|
|
|
|
- // 1. Validate request
|
|
|
|
|
- const validation = validateRequest(req.query);
|
|
|
|
|
- if (!validation.valid) {
|
|
|
|
|
- console.error(`[ShopRenter] Validation failed: ${validation.error}`);
|
|
|
|
|
- return res.status(403).json({
|
|
|
|
|
- error: 'Invalid request',
|
|
|
|
|
- details: validation.error
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- // 2. Exchange code for tokens
|
|
|
|
|
- const tokenData = await exchangeCodeForToken(shopname, code);
|
|
|
|
|
-
|
|
|
|
|
- // 3. Store credentials
|
|
|
|
|
- // Note: We need user_id here. In ShopRenter flow, we might need to:
|
|
|
|
|
- // - Store shopname in a temp Map with code
|
|
|
|
|
- // - Let user authenticate first
|
|
|
|
|
- // For now, we'll redirect to app_url and handle auth there
|
|
|
|
|
-
|
|
|
|
|
- // Store in temporary map for later association
|
|
|
|
|
- const installationId = crypto.randomBytes(16).toString('hex');
|
|
|
|
|
- global.pendingShopRenterInstalls = global.pendingShopRenterInstalls || new Map();
|
|
|
|
|
- global.pendingShopRenterInstalls.set(installationId, {
|
|
|
|
|
- shopname,
|
|
|
|
|
- tokenData,
|
|
|
|
|
- timestamp: Date.now(),
|
|
|
|
|
- expiresAt: Date.now() + (15 * 60 * 1000) // 15 minutes
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 4. Redirect to app_url with installation_id
|
|
|
|
|
- const redirectUrl = new URL(app_url);
|
|
|
|
|
- redirectUrl.searchParams.append('sr_install', installationId);
|
|
|
|
|
-
|
|
|
|
|
- console.log(`[ShopRenter] Redirecting to: ${redirectUrl.toString()}`);
|
|
|
|
|
- res.redirect(redirectUrl.toString());
|
|
|
|
|
-
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('[ShopRenter] OAuth callback error:', error);
|
|
|
|
|
- res.status(500).json({
|
|
|
|
|
- error: 'Installation failed',
|
|
|
|
|
- details: error.message
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// ShopRenter EntryPoint (after redirect from callback)
|
|
|
|
|
-app.get('/integrations/shoprenter/entry', securedSession, async (req, res) => {
|
|
|
|
|
- const { shopname, code, timestamp, hmac, sr_install } = req.query;
|
|
|
|
|
-
|
|
|
|
|
- console.log(`[ShopRenter] EntryPoint accessed for ${shopname}`);
|
|
|
|
|
-
|
|
|
|
|
- // 1. Validate HMAC
|
|
|
|
|
- const validation = validateRequest({ shopname, code, timestamp, hmac });
|
|
|
|
|
- if (!validation.valid) {
|
|
|
|
|
- return res.status(403).json({
|
|
|
|
|
- error: 'Invalid request',
|
|
|
|
|
- details: validation.error
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- // 2. Get installation data from temporary storage
|
|
|
|
|
- if (!sr_install || !global.pendingShopRenterInstalls?.has(sr_install)) {
|
|
|
|
|
- return res.status(400).json({
|
|
|
|
|
- error: 'Installation session not found or expired'
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const installData = global.pendingShopRenterInstalls.get(sr_install);
|
|
|
|
|
-
|
|
|
|
|
- // 3. Store credentials with authenticated user
|
|
|
|
|
- await storeCredentials(req.user.id, installData.shopname, installData.tokenData);
|
|
|
|
|
-
|
|
|
|
|
- // 4. Clean up temporary storage
|
|
|
|
|
- global.pendingShopRenterInstalls.delete(sr_install);
|
|
|
|
|
-
|
|
|
|
|
- // 5. Set up webhooks (async)
|
|
|
|
|
- setupWebhooks(req.user.id, installData.shopname).catch(err => {
|
|
|
|
|
- console.error('[ShopRenter] Webhook setup error:', err);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 6. Start initial sync (async)
|
|
|
|
|
- syncShopRenterData(req.user.id, installData.shopname).catch(err => {
|
|
|
|
|
- console.error('[ShopRenter] Initial sync error:', err);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 7. Redirect to success page
|
|
|
|
|
- res.redirect('https://shopcall.ai/integrations?connected=shoprenter&status=success');
|
|
|
|
|
-
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('[ShopRenter] EntryPoint error:', error);
|
|
|
|
|
- res.redirect('https://shopcall.ai/integrations?connected=shoprenter&status=error');
|
|
|
|
|
- }
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// ShopRenter Uninstall Webhook
|
|
|
|
|
-app.get('/webhooks/shoprenter/uninstall', async (req, res) => {
|
|
|
|
|
- const { shopname, code, timestamp, hmac } = req.query;
|
|
|
|
|
-
|
|
|
|
|
- console.log(`[ShopRenter] Uninstall webhook received for ${shopname}`);
|
|
|
|
|
-
|
|
|
|
|
- // 1. Validate HMAC
|
|
|
|
|
- const validation = validateRequest(req.query);
|
|
|
|
|
- if (!validation.valid) {
|
|
|
|
|
- return res.status(403).json({
|
|
|
|
|
- error: 'Invalid request',
|
|
|
|
|
- details: validation.error
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- // 2. Find store by shopname
|
|
|
|
|
- const { data: tokens } = await supabase
|
|
|
|
|
- .from('shoprenter_tokens')
|
|
|
|
|
- .select('store_id')
|
|
|
|
|
- .eq('shopname', shopname)
|
|
|
|
|
- .single();
|
|
|
|
|
-
|
|
|
|
|
- if (tokens) {
|
|
|
|
|
- // 3. Deactivate store
|
|
|
|
|
- await supabase
|
|
|
|
|
- .from('stores')
|
|
|
|
|
- .update({ is_active: false, updated_at: new Date().toISOString() })
|
|
|
|
|
- .eq('id', tokens.store_id);
|
|
|
|
|
-
|
|
|
|
|
- // 4. Deactivate tokens
|
|
|
|
|
- await supabase
|
|
|
|
|
- .from('shoprenter_tokens')
|
|
|
|
|
- .update({ is_active: false, updated_at: new Date().toISOString() })
|
|
|
|
|
- .eq('store_id', tokens.store_id);
|
|
|
|
|
-
|
|
|
|
|
- console.log(`[ShopRenter] Store ${shopname} uninstalled successfully`);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 5. Respond with 200 (ShopRenter expects this)
|
|
|
|
|
- res.status(200).json({ message: 'Uninstall processed' });
|
|
|
|
|
-
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('[ShopRenter] Uninstall error:', error);
|
|
|
|
|
- // Still respond with 200 to prevent retries
|
|
|
|
|
- res.status(200).json({ message: 'Uninstall processed with errors' });
|
|
|
|
|
- }
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// Cleanup expired installations periodically
|
|
|
|
|
-setInterval(() => {
|
|
|
|
|
- if (!global.pendingShopRenterInstalls) return;
|
|
|
|
|
-
|
|
|
|
|
- const now = Date.now();
|
|
|
|
|
- for (const [key, value] of global.pendingShopRenterInstalls.entries()) {
|
|
|
|
|
- if (now > value.expiresAt) {
|
|
|
|
|
- global.pendingShopRenterInstalls.delete(key);
|
|
|
|
|
- console.log(`[ShopRenter] Cleaned up expired installation: ${key}`);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}, 5 * 60 * 1000); // Every 5 minutes
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-</details>
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 🎨 Frontend Implementation
|
|
|
|
|
-
|
|
|
|
|
-### Integration UI Component
|
|
|
|
|
-
|
|
|
|
|
-Create: `/shopcall.ai-main/src/components/ShopRenterConnect.tsx`
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-import { useState } from 'react';
|
|
|
|
|
-import { Button } from '@/components/ui/button';
|
|
|
|
|
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
|
-import { Input } from '@/components/ui/input';
|
|
|
|
|
-import { Label } from '@/components/ui/label';
|
|
|
|
|
-import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
|
|
|
-import { Loader2, CheckCircle, AlertCircle } from 'lucide-react';
|
|
|
|
|
-
|
|
|
|
|
-export function ShopRenterConnect() {
|
|
|
|
|
- const [shopname, setShopname] = useState('');
|
|
|
|
|
- const [loading, setLoading] = useState(false);
|
|
|
|
|
- const [error, setError] = useState('');
|
|
|
|
|
-
|
|
|
|
|
- const handleConnect = async () => {
|
|
|
|
|
- if (!shopname.trim()) {
|
|
|
|
|
- setError('Kérjük, adja meg a bolt nevét');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- setLoading(true);
|
|
|
|
|
- setError('');
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- // For ShopRenter, the store owner must initiate installation from ShopRenter admin
|
|
|
|
|
- // This would typically open the ShopRenter app store
|
|
|
|
|
- window.open(
|
|
|
|
|
- `https://${shopname}.shoprenter.hu/admin/app/${SHOPRENTER_CLIENT_ID}`,
|
|
|
|
|
- '_blank'
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- // Show instructions
|
|
|
|
|
- setError('');
|
|
|
|
|
-
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- setError('Hiba történt a kapcsolódás során');
|
|
|
|
|
- console.error('ShopRenter connection error:', err);
|
|
|
|
|
- } finally {
|
|
|
|
|
- setLoading(false);
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- return (
|
|
|
|
|
- <Card className="bg-slate-800 border-slate-700">
|
|
|
|
|
- <CardHeader>
|
|
|
|
|
- <div className="flex items-center gap-3">
|
|
|
|
|
- <img
|
|
|
|
|
- src="/images/shoprenter-logo.png"
|
|
|
|
|
- alt="ShopRenter"
|
|
|
|
|
- className="w-10 h-10"
|
|
|
|
|
- />
|
|
|
|
|
- <CardTitle className="text-white">ShopRenter</CardTitle>
|
|
|
|
|
- </div>
|
|
|
|
|
- <p className="text-slate-400">
|
|
|
|
|
- Magyar webáruház platform - AI telefonos asszisztens integráció
|
|
|
|
|
- </p>
|
|
|
|
|
- </CardHeader>
|
|
|
|
|
- <CardContent className="space-y-4">
|
|
|
|
|
- <div className="space-y-2">
|
|
|
|
|
- <Label htmlFor="shopname" className="text-slate-300">
|
|
|
|
|
- Bolt neve
|
|
|
|
|
- </Label>
|
|
|
|
|
- <div className="flex gap-2">
|
|
|
|
|
- <Input
|
|
|
|
|
- id="shopname"
|
|
|
|
|
- placeholder="pelda"
|
|
|
|
|
- value={shopname}
|
|
|
|
|
- onChange={(e) => setShopname(e.target.value)}
|
|
|
|
|
- className="bg-slate-700 border-slate-600 text-white flex-1"
|
|
|
|
|
- />
|
|
|
|
|
- <span className="flex items-center text-slate-400">
|
|
|
|
|
- .shoprenter.hu
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <p className="text-slate-500 text-sm">
|
|
|
|
|
- Add meg a bolt nevét (pl. "pelda" ha a bolt címe pelda.shoprenter.hu)
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {error && (
|
|
|
|
|
- <Alert variant="destructive" className="bg-red-900/20 border-red-900">
|
|
|
|
|
- <AlertCircle className="w-4 h-4" />
|
|
|
|
|
- <AlertDescription>{error}</AlertDescription>
|
|
|
|
|
- </Alert>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- <Button
|
|
|
|
|
- onClick={handleConnect}
|
|
|
|
|
- disabled={loading}
|
|
|
|
|
- className="w-full bg-[#85b218] hover:bg-[#6d9315] text-white"
|
|
|
|
|
- >
|
|
|
|
|
- {loading ? (
|
|
|
|
|
- <>
|
|
|
|
|
- <Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
|
|
|
- Kapcsolódás...
|
|
|
|
|
- </>
|
|
|
|
|
- ) : (
|
|
|
|
|
- 'ShopRenter kapcsolat létrehozása'
|
|
|
|
|
- )}
|
|
|
|
|
- </Button>
|
|
|
|
|
-
|
|
|
|
|
- <div className="bg-slate-700/50 p-4 rounded-lg">
|
|
|
|
|
- <h4 className="text-white font-medium mb-2">Telepítési lépések:</h4>
|
|
|
|
|
- <ol className="text-slate-300 text-sm space-y-1 list-decimal list-inside">
|
|
|
|
|
- <li>Kattints a "Kapcsolat létrehozása" gombra</li>
|
|
|
|
|
- <li>Jelentkezz be a ShopRenter admin felületedre</li>
|
|
|
|
|
- <li>Hagyd jóvá az alkalmazás telepítését</li>
|
|
|
|
|
- <li>Visszairányítunk ide a sikeres telepítés után</li>
|
|
|
|
|
- </ol>
|
|
|
|
|
- </div>
|
|
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
- );
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### Add to Integrations Page
|
|
|
|
|
-
|
|
|
|
|
-Update `/shopcall.ai-main/src/components/IntegrationsContent.tsx`:
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-import { ShopRenterConnect } from './ShopRenterConnect';
|
|
|
|
|
-
|
|
|
|
|
-// In the platforms grid, add:
|
|
|
|
|
-<div className="grid gap-6 md:grid-cols-3">
|
|
|
|
|
- {/* Existing Shopify card */}
|
|
|
|
|
- {/* Existing WooCommerce card */}
|
|
|
|
|
-
|
|
|
|
|
- {/* New ShopRenter card */}
|
|
|
|
|
- <ShopRenterConnect />
|
|
|
|
|
-</div>
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 🔒 Security Considerations
|
|
|
|
|
-
|
|
|
|
|
-### 1. HMAC Validation
|
|
|
|
|
-- ✅ **Always validate HMAC** on every request from ShopRenter
|
|
|
|
|
-- ✅ Use timing-safe comparison (`crypto.timingSafeEqual`)
|
|
|
|
|
-- ✅ Validate timestamp to prevent replay attacks (5-minute window)
|
|
|
|
|
-
|
|
|
|
|
-### 2. Token Storage
|
|
|
|
|
-- ✅ Store access tokens encrypted in database
|
|
|
|
|
-- ✅ Never log or expose tokens in responses
|
|
|
|
|
-- ✅ Implement automatic token refresh
|
|
|
|
|
-- ✅ Revoke tokens on uninstall
|
|
|
|
|
-
|
|
|
|
|
-### 3. API Communication
|
|
|
|
|
-- ✅ Always use HTTPS for all endpoints
|
|
|
|
|
-- ✅ Implement rate limiting (5 req/sec per ShopRenter guidelines)
|
|
|
|
|
-- ✅ Handle API errors gracefully
|
|
|
|
|
-- ✅ Implement retry logic with exponential backoff
|
|
|
|
|
-
|
|
|
|
|
-### 4. Data Privacy (GDPR Compliance)
|
|
|
|
|
-- ✅ Only request necessary scopes
|
|
|
|
|
-- ✅ Implement data deletion on uninstall
|
|
|
|
|
-- ✅ Provide clear privacy policy
|
|
|
|
|
-- ✅ Handle customer data securely
|
|
|
|
|
-
|
|
|
|
|
-### 5. Environment Variables
|
|
|
|
|
-
|
|
|
|
|
-```bash
|
|
|
|
|
-# .env for backend
|
|
|
|
|
-SHOPRENTER_APP_ID=your_app_id_here
|
|
|
|
|
-SHOPRENTER_CLIENT_ID=your_client_id_here
|
|
|
|
|
-SHOPRENTER_CLIENT_SECRET=your_client_secret_here
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**⚠️ Never commit these to version control!**
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 🧪 Testing Strategy
|
|
|
|
|
-
|
|
|
|
|
-### 1. Test Store Setup
|
|
|
|
|
-
|
|
|
|
|
-1. Request test store at: https://www.shoprenter.hu/tesztigenyles/?devstore=1
|
|
|
|
|
-2. Store name: `shopcall-test-store`
|
|
|
|
|
-3. URL: `shopcall-test-store.myshoprenter.hu`
|
|
|
|
|
-
|
|
|
|
|
-### 2. Test Cases
|
|
|
|
|
-
|
|
|
|
|
-#### OAuth Flow Testing
|
|
|
|
|
-
|
|
|
|
|
-```
|
|
|
|
|
-Test Case 1: Successful Installation
|
|
|
|
|
-- Initiate OAuth from ShopRenter admin
|
|
|
|
|
-- Verify HMAC validation passes
|
|
|
|
|
-- Verify token exchange succeeds
|
|
|
|
|
-- Verify credentials stored in database
|
|
|
|
|
-- Verify redirect to success page
|
|
|
|
|
-
|
|
|
|
|
-Test Case 2: Invalid HMAC
|
|
|
|
|
-- Modify HMAC parameter
|
|
|
|
|
-- Verify request rejected with 403
|
|
|
|
|
-- Verify error logged
|
|
|
|
|
-
|
|
|
|
|
-Test Case 3: Expired Timestamp
|
|
|
|
|
-- Use timestamp older than 5 minutes
|
|
|
|
|
-- Verify request rejected
|
|
|
|
|
-- Verify error logged
|
|
|
|
|
-
|
|
|
|
|
-Test Case 4: Uninstall Flow
|
|
|
|
|
-- Trigger uninstall from ShopRenter
|
|
|
|
|
-- Verify webhook received
|
|
|
|
|
-- Verify store deactivated
|
|
|
|
|
-- Verify tokens deactivated
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-#### API Testing
|
|
|
|
|
-
|
|
|
|
|
-```
|
|
|
|
|
-Test Case 5: Product Sync
|
|
|
|
|
-- Request product list from API
|
|
|
|
|
-- Verify products cached in database
|
|
|
|
|
-- Verify product data accuracy
|
|
|
|
|
-
|
|
|
|
|
-Test Case 6: Token Refresh
|
|
|
|
|
-- Wait for token expiry
|
|
|
|
|
-- Make API request
|
|
|
|
|
-- Verify token auto-refreshed
|
|
|
|
|
-- Verify new token stored
|
|
|
|
|
-
|
|
|
|
|
-Test Case 7: Webhook Processing
|
|
|
|
|
-- Create test order in ShopRenter
|
|
|
|
|
-- Verify webhook received
|
|
|
|
|
-- Verify order data processed
|
|
|
|
|
-- Verify response 200 sent
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 3. Integration Testing Tools
|
|
|
|
|
-
|
|
|
|
|
-```javascript
|
|
|
|
|
-// test/shoprenter-integration.test.js
|
|
|
|
|
-const axios = require('axios');
|
|
|
|
|
-const crypto = require('crypto');
|
|
|
|
|
-
|
|
|
|
|
-describe('ShopRenter Integration', () => {
|
|
|
|
|
- const testShop = 'shopcall-test-store';
|
|
|
|
|
- const clientSecret = process.env.SHOPRENTER_CLIENT_SECRET;
|
|
|
|
|
-
|
|
|
|
|
- function generateValidHMAC(params) {
|
|
|
|
|
- const sorted = Object.keys(params)
|
|
|
|
|
- .sort()
|
|
|
|
|
- .map(key => `${key}=${params[key]}`)
|
|
|
|
|
- .join('&');
|
|
|
|
|
-
|
|
|
|
|
- return crypto
|
|
|
|
|
- .createHmac('sha256', clientSecret)
|
|
|
|
|
- .update(sorted)
|
|
|
|
|
- .digest('hex');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- it('should validate correct HMAC', async () => {
|
|
|
|
|
- const params = {
|
|
|
|
|
- shopname: testShop,
|
|
|
|
|
- code: 'test123',
|
|
|
|
|
- timestamp: Math.floor(Date.now() / 1000).toString()
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- params.hmac = generateValidHMAC(params);
|
|
|
|
|
-
|
|
|
|
|
- const response = await axios.get('http://localhost:3000/auth/shoprenter/callback', {
|
|
|
|
|
- params,
|
|
|
|
|
- maxRedirects: 0,
|
|
|
|
|
- validateStatus: () => true
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- expect(response.status).not.toBe(403);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- it('should reject invalid HMAC', async () => {
|
|
|
|
|
- const params = {
|
|
|
|
|
- shopname: testShop,
|
|
|
|
|
- code: 'test123',
|
|
|
|
|
- timestamp: Math.floor(Date.now() / 1000).toString(),
|
|
|
|
|
- hmac: 'invalid_hmac'
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const response = await axios.get('http://localhost:3000/auth/shoprenter/callback', {
|
|
|
|
|
- params,
|
|
|
|
|
- maxRedirects: 0,
|
|
|
|
|
- validateStatus: () => true
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- expect(response.status).toBe(403);
|
|
|
|
|
- });
|
|
|
|
|
-});
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## ✅ Implementation Status
|
|
|
|
|
-
|
|
|
|
|
-### 🎉 Completed (All Core Features Implemented!)
|
|
|
|
|
-
|
|
|
|
|
-#### **Backend Infrastructure**
|
|
|
|
|
-- ✅ All 8 Supabase Edge Functions deployed and operational
|
|
|
|
|
-- ✅ `oauth-shoprenter-init` - OAuth flow initialization
|
|
|
|
|
-- ✅ `oauth-shoprenter-callback` - OAuth callback with HMAC validation
|
|
|
|
|
-- ✅ `webhook-shoprenter-uninstall` - Uninstall webhook handler
|
|
|
|
|
-- ✅ `shoprenter-products` - Product data sync
|
|
|
|
|
-- ✅ `shoprenter-orders` - Order data sync
|
|
|
|
|
-- ✅ `shoprenter-customers` - Customer data sync
|
|
|
|
|
-- ✅ `shoprenter-sync` - Manual full sync trigger
|
|
|
|
|
-- ✅ `shoprenter-scheduled-sync` - Automated hourly background sync (pg_cron)
|
|
|
|
|
-
|
|
|
|
|
-#### **Security & Authentication**
|
|
|
|
|
-- ✅ HMAC SHA256 validation implemented
|
|
|
|
|
-- ✅ Timestamp validation (5-minute window for replay attack prevention)
|
|
|
|
|
-- ✅ Timing-safe comparison for HMAC verification
|
|
|
|
|
-- ✅ Secure token storage with encryption
|
|
|
|
|
-- ✅ Automatic token refresh (5-minute expiry buffer)
|
|
|
|
|
-
|
|
|
|
|
-#### **Database Schema**
|
|
|
|
|
-- ✅ `stores` table updated for ShopRenter
|
|
|
|
|
-- ✅ `pending_shoprenter_installs` table created
|
|
|
|
|
-- ✅ `oauth_states` table for OAuth flow state management
|
|
|
|
|
-- ✅ `shoprenter_products_cache` table for product caching
|
|
|
|
|
-- ✅ `shoprenter_tokens` table for token management
|
|
|
|
|
-- ✅ `shoprenter_webhooks` table for webhook tracking
|
|
|
|
|
-- ✅ `store_sync_config` table for per-store sync settings
|
|
|
|
|
-- ✅ `sync_logs` table for scheduled sync monitoring
|
|
|
|
|
-
|
|
|
|
|
-#### **Data Synchronization**
|
|
|
|
|
-- ✅ Manual sync via UI (immediate sync on demand)
|
|
|
|
|
-- ✅ Scheduled background sync (pg_cron, hourly)
|
|
|
|
|
-- ✅ Per-store sync configuration (frequency, enabled/disabled)
|
|
|
|
|
-- ✅ Sync logging and monitoring
|
|
|
|
|
-- ✅ Rate limiting (5 req/sec per ShopRenter guidelines)
|
|
|
|
|
-- ✅ Retry logic with exponential backoff
|
|
|
|
|
-- ✅ Error handling and recovery
|
|
|
|
|
-
|
|
|
|
|
-#### **Frontend Integration**
|
|
|
|
|
-- ✅ `ShopRenterConnect.tsx` component implemented
|
|
|
|
|
-- ✅ OAuth flow initiation from UI
|
|
|
|
|
-- ✅ Installation success/error handling
|
|
|
|
|
-- ✅ Hungarian language support
|
|
|
|
|
-- ✅ User-friendly store connection interface
|
|
|
|
|
-
|
|
|
|
|
-#### **Documentation**
|
|
|
|
|
-- ✅ `SHOPRENTER_REGISTRATION.md` created (submission-ready)
|
|
|
|
|
-- ✅ `SHOPRENTER.md` updated with actual implementation
|
|
|
|
|
-- ✅ All domain formats corrected (`.shoprenter.hu`)
|
|
|
|
|
-- ✅ All endpoint URLs updated (Supabase Edge Functions)
|
|
|
|
|
-- ✅ Backend architecture updated (Supabase Edge Functions)
|
|
|
|
|
-- ✅ Code examples updated (TypeScript/Deno)
|
|
|
|
|
-
|
|
|
|
|
-### ⚠️ Pending Items
|
|
|
|
|
-
|
|
|
|
|
-#### **Registration Requirements**
|
|
|
|
|
-- ⏳ Logo design and creation (250x150px PNG at `/shopcall.ai-main/public/images/shoprenter-app-logo.png`)
|
|
|
|
|
-- ⏳ Test store approval from ShopRenter
|
|
|
|
|
-- ⏳ Production OAuth credentials (AppId, ClientId, ClientSecret)
|
|
|
|
|
-- ⏳ Final testing with approved test store
|
|
|
|
|
-- ⏳ App Store listing approval
|
|
|
|
|
-
|
|
|
|
|
-#### **Optional Enhancements**
|
|
|
|
|
-- 🔄 Real-time webhooks for product/order updates (infrastructure ready, awaiting registration)
|
|
|
|
|
-- 🔄 Advanced sync scheduling options (15min, 30min intervals)
|
|
|
|
|
-- 🔄 Sync analytics dashboard
|
|
|
|
|
-
|
|
|
|
|
-### 📊 Implementation Summary
|
|
|
|
|
-
|
|
|
|
|
-| Category | Completed | Pending | Total | Progress |
|
|
|
|
|
-|----------|-----------|---------|-------|----------|
|
|
|
|
|
-| Backend Edge Functions | 8 | 0 | 8 | 100% ✅ |
|
|
|
|
|
-| Database Tables | 8 | 0 | 8 | 100% ✅ |
|
|
|
|
|
-| Security Features | 5 | 0 | 5 | 100% ✅ |
|
|
|
|
|
-| Sync Features | 6 | 0 | 6 | 100% ✅ |
|
|
|
|
|
-| Frontend Components | 1 | 0 | 1 | 100% ✅ |
|
|
|
|
|
-| Documentation | 2 | 0 | 2 | 100% ✅ |
|
|
|
|
|
-| Registration Items | 0 | 5 | 5 | 0% ⏳ |
|
|
|
|
|
-
|
|
|
|
|
-**Overall Technical Implementation:** ✅ 100% Complete
|
|
|
|
|
-**Overall Project Status:** ⏳ 83% Complete (awaiting registration approval)
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 🚀 Deployment Plan
|
|
|
|
|
-
|
|
|
|
|
-### ✅ Phase 1: Development (COMPLETED)
|
|
|
|
|
-- ✅ Set up test ShopRenter store
|
|
|
|
|
-- ✅ Implement HMAC validation
|
|
|
|
|
-- ✅ Implement OAuth flow
|
|
|
|
|
-- ✅ Create database tables
|
|
|
|
|
-- ✅ Build API client
|
|
|
|
|
-- ✅ Test with test store
|
|
|
|
|
-
|
|
|
|
|
-### ✅ Phase 2: Integration (COMPLETED)
|
|
|
|
|
-- ✅ Implement product sync
|
|
|
|
|
-- ✅ Implement webhook handlers
|
|
|
|
|
-- ✅ Create frontend UI
|
|
|
|
|
-- ✅ Test end-to-end flow
|
|
|
|
|
-- ✅ Handle error scenarios
|
|
|
|
|
-
|
|
|
|
|
-### ✅ Phase 3: Testing (COMPLETED)
|
|
|
|
|
-- ✅ Unit tests
|
|
|
|
|
-- ✅ Integration tests
|
|
|
|
|
-- ✅ Security audit
|
|
|
|
|
-- ✅ Performance testing
|
|
|
|
|
-- ✅ User acceptance testing
|
|
|
|
|
-
|
|
|
|
|
-### ⏳ Phase 4: Production Registration (IN PROGRESS)
|
|
|
|
|
-- ✅ Prepare application documentation
|
|
|
|
|
-- ⏳ Create logo (250x150px PNG) - **PENDING**
|
|
|
|
|
-- ⏳ Submit app to ShopRenter Partner Support - **READY TO SUBMIT**
|
|
|
|
|
-- ⏳ Wait for approval
|
|
|
|
|
-- ⏳ Receive production credentials
|
|
|
|
|
-
|
|
|
|
|
-### 📋 Phase 5: Launch (READY)
|
|
|
|
|
-- ✅ Deploy to production (Edge Functions already deployed)
|
|
|
|
|
-- ✅ Configure production environment variables
|
|
|
|
|
-- ✅ Monitor logs and errors
|
|
|
|
|
-- ⏳ Soft launch with beta users (awaiting credentials)
|
|
|
|
|
-- ⏳ Public launch
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 📅 Timeline & Milestones
|
|
|
|
|
-
|
|
|
|
|
-| Phase | Milestone | Status | Date |
|
|
|
|
|
-|-------|-----------|--------|------|
|
|
|
|
|
-| 1 | Setup & Planning | ✅ Complete | Oct 2024 |
|
|
|
|
|
-| 2 | OAuth Implementation | ✅ Complete | Oct 2024 |
|
|
|
|
|
-| 3 | API Integration | ✅ Complete | Oct 2024 |
|
|
|
|
|
-| 4 | Scheduled Sync | ✅ Complete | Oct 2024 |
|
|
|
|
|
-| 5 | Testing & QA | ✅ Complete | Oct 2024 |
|
|
|
|
|
-| 6 | Documentation | ✅ Complete | Oct 31, 2025 |
|
|
|
|
|
-| 7 | Registration | ⏳ Pending | Nov 2025 (estimated) |
|
|
|
|
|
-| 8 | Production Launch | 📋 Ready | TBD (after approval) |
|
|
|
|
|
-
|
|
|
|
|
-**Status:** Ready for ShopRenter Partner Support Submission
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 📚 Resources & References
|
|
|
|
|
-
|
|
|
|
|
-### Documentation
|
|
|
|
|
-- ShopRenter Developer Docs: https://doc.shoprenter.hu
|
|
|
|
|
-- API Reference: https://doc.shoprenter.hu/development/api/
|
|
|
|
|
-- App Development Guide: https://doc.shoprenter.hu/development/app-development/
|
|
|
|
|
-
|
|
|
|
|
-### Example Apps
|
|
|
|
|
-- PHP Demo: https://github.com/Shoprenter/sr-demo-app-php
|
|
|
|
|
-- Node.js Demo: https://github.com/Shoprenter/sr-demo-app-node
|
|
|
|
|
-
|
|
|
|
|
-### Contact
|
|
|
|
|
-- Partner Support: partnersupport@shoprenter.hu
|
|
|
|
|
-- Test Store Request: https://www.shoprenter.hu/tesztigenyles/?devstore=1
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## ✅ Checklist for Launch
|
|
|
|
|
-
|
|
|
|
|
-### Pre-Registration
|
|
|
|
|
-- ✅ Review ShopRenter developer documentation
|
|
|
|
|
-- ✅ Prepare application name and description (Hungarian)
|
|
|
|
|
-- ⏳ Design application logo (250x150px) - **PENDING**
|
|
|
|
|
-- ✅ Set up HTTPS endpoints (EntryPoint, RedirectUri, UninstallUri)
|
|
|
|
|
-- ✅ Determine required API scopes
|
|
|
|
|
-- ⏳ Request test store - **READY TO REQUEST**
|
|
|
|
|
-
|
|
|
|
|
-### Development
|
|
|
|
|
-- ✅ Implement HMAC validation
|
|
|
|
|
-- ✅ Implement OAuth flow
|
|
|
|
|
-- ✅ Create database schema
|
|
|
|
|
-- ✅ Build API client
|
|
|
|
|
-- ✅ Implement product sync
|
|
|
|
|
-- ✅ Set up webhook handlers
|
|
|
|
|
-- ✅ Create frontend UI
|
|
|
|
|
-- ✅ Add error handling
|
|
|
|
|
-- ✅ Implement logging
|
|
|
|
|
-- ✅ Implement scheduled sync (pg_cron)
|
|
|
|
|
-- ✅ Implement per-store sync configuration
|
|
|
|
|
-
|
|
|
|
|
-### Testing
|
|
|
|
|
-- ✅ Test OAuth installation
|
|
|
|
|
-- ✅ Test HMAC validation
|
|
|
|
|
-- ✅ Test token refresh
|
|
|
|
|
-- ✅ Test product sync
|
|
|
|
|
-- ✅ Test webhooks
|
|
|
|
|
-- ✅ Test uninstall flow
|
|
|
|
|
-- ✅ Security testing
|
|
|
|
|
-- ✅ Performance testing
|
|
|
|
|
-
|
|
|
|
|
-### Registration
|
|
|
|
|
-- ✅ Prepare SHOPRENTER_REGISTRATION.md document
|
|
|
|
|
-- ⏳ Create logo (250x150px PNG) - **PENDING**
|
|
|
|
|
-- ⏳ Submit to partnersupport@shoprenter.hu - **READY TO SUBMIT**
|
|
|
|
|
-- ⏳ Provide all required information - **DOCUMENTED**
|
|
|
|
|
-- ⏳ Wait for AppId, ClientId, ClientSecret
|
|
|
|
|
-- ⏳ Configure environment variables (when credentials received)
|
|
|
|
|
-
|
|
|
|
|
-### Production
|
|
|
|
|
-- ✅ Deploy backend to production (Supabase Edge Functions)
|
|
|
|
|
-- ✅ Deploy frontend to production
|
|
|
|
|
-- ✅ Configure production URLs
|
|
|
|
|
-- ✅ Monitor logs for errors
|
|
|
|
|
-- ⏳ Test with real store (awaiting credentials)
|
|
|
|
|
-- ✅ Document usage for customers
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 🔄 Post-Launch Maintenance
|
|
|
|
|
-
|
|
|
|
|
-### Regular Tasks
|
|
|
|
|
-- Monitor webhook reliability
|
|
|
|
|
-- Check token refresh logs
|
|
|
|
|
-- Review error rates
|
|
|
|
|
-- Update API client when ShopRenter updates
|
|
|
|
|
-- Sync product catalog daily
|
|
|
|
|
-- Respond to merchant support requests
|
|
|
|
|
-
|
|
|
|
|
-### Scope Updates
|
|
|
|
|
-When new features require additional scopes:
|
|
|
|
|
-1. Email Partner Support with new scope list
|
|
|
|
|
-2. Wait for scope approval
|
|
|
|
|
-3. Direct existing merchants to approve new scopes:
|
|
|
|
|
- `https://{shopName}.shoprenter.hu/admin/app/{clientId}/approveScopes`
|
|
|
|
|
-4. Update app to use new scopes
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 📌 Scheduled Background Sync
|
|
|
|
|
-
|
|
|
|
|
-### Implementation Details
|
|
|
|
|
-
|
|
|
|
|
-The ShopRenter integration includes automated background synchronization using PostgreSQL's `pg_cron` extension.
|
|
|
|
|
-
|
|
|
|
|
-**How it Works:**
|
|
|
|
|
-1. **pg_cron** schedules database jobs at hourly intervals
|
|
|
|
|
-2. **Database trigger function** (`trigger_shoprenter_scheduled_sync()`) makes HTTP request to Edge Function
|
|
|
|
|
-3. **`shoprenter-scheduled-sync` Edge Function** processes all active ShopRenter stores
|
|
|
|
|
-4. **Sync results** are logged to `sync_logs` table
|
|
|
|
|
-
|
|
|
|
|
-**Configuration:**
|
|
|
|
|
-- Runs hourly at minute 0 (`:00`)
|
|
|
|
|
-- Configurable per store via `store_sync_config` table
|
|
|
|
|
-- Sync frequencies: `15min`, `30min`, `hourly`, `6hours`, `daily`
|
|
|
|
|
-- Can be enabled/disabled per store
|
|
|
|
|
-- Choose what to sync: products, orders, customers
|
|
|
|
|
-
|
|
|
|
|
-**Monitoring:**
|
|
|
|
|
-```sql
|
|
|
|
|
--- View recent sync logs
|
|
|
|
|
-SELECT * FROM sync_logs
|
|
|
|
|
-WHERE platform = 'shoprenter'
|
|
|
|
|
-ORDER BY created_at DESC
|
|
|
|
|
-LIMIT 10;
|
|
|
|
|
-
|
|
|
|
|
--- Check store sync configuration
|
|
|
|
|
-SELECT * FROM store_sync_config
|
|
|
|
|
-WHERE store_id IN (
|
|
|
|
|
- SELECT id FROM stores WHERE platform_name = 'shoprenter'
|
|
|
|
|
-);
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Manual Control:**
|
|
|
|
|
-```sql
|
|
|
|
|
--- Enable/disable sync for a store
|
|
|
|
|
-SELECT set_store_sync_enabled('store-uuid', true);
|
|
|
|
|
-
|
|
|
|
|
--- Change sync frequency
|
|
|
|
|
-SELECT set_store_sync_frequency('store-uuid', 'hourly');
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**Database Configuration Required:**
|
|
|
|
|
-```
|
|
|
|
|
--- In Supabase Dashboard → Project Settings → Database → Custom Postgres Configuration
|
|
|
|
|
-app.internal_sync_secret = 'same_as_INTERNAL_SYNC_SECRET_in_env'
|
|
|
|
|
-app.supabase_url = 'https://ztklqodcdjeqpsvhlpud.supabase.co'
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-**Document Version:** 2.0
|
|
|
|
|
-**Last Updated:** 2025-10-31
|
|
|
|
|
-**Author:** ShopCall.ai Development Team
|
|
|
|
|
-**Status:** ✅ Implementation Complete - Ready for Registration
|
|
|
|
|
-
|
|
|