|
@@ -0,0 +1,409 @@
|
|
|
|
|
+# Unified Webshop API Response Format
|
|
|
|
|
+
|
|
|
|
|
+**Related:** Issue #53 - Unified webshop API response format
|
|
|
|
|
+
|
|
|
|
|
+## Overview
|
|
|
|
|
+
|
|
|
|
|
+All Shop Data API endpoints now return responses in a **consistent, unified format** regardless of the e-commerce platform (Shopify, WooCommerce, or ShopRenter). This standardization makes it easier to build platform-agnostic applications and handle responses uniformly.
|
|
|
|
|
+
|
|
|
|
|
+## Response Structure
|
|
|
|
|
+
|
|
|
|
|
+### Base Response Interface
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+interface UnifiedApiResponse<T> {
|
|
|
|
|
+ success: boolean; // Request success status
|
|
|
|
|
+ data: T | T[]; // Response data (single item or array)
|
|
|
|
|
+ metadata: ResponseMetadata; // Request metadata
|
|
|
|
|
+ pagination?: Pagination; // Only for list endpoints
|
|
|
|
|
+ error?: ErrorDetails; // Only when success=false
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Metadata
|
|
|
|
|
+
|
|
|
|
|
+Every response includes comprehensive metadata:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+interface ResponseMetadata {
|
|
|
|
|
+ platform?: string; // "shopify" | "woocommerce" | "shoprenter"
|
|
|
|
|
+ store_id?: string; // Store UUID (for data endpoints)
|
|
|
|
|
+ resource_type: string; // "customers" | "orders" | "products" | "stores"
|
|
|
|
|
+ auth_type: "user" | "internal"; // Authentication type used
|
|
|
|
|
+ fetched_at: string; // ISO 8601 timestamp
|
|
|
|
|
+ request_id?: string; // Unique request tracking ID
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Pagination (List Endpoints)
|
|
|
|
|
+
|
|
|
|
|
+Paginated responses include:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+interface Pagination {
|
|
|
|
|
+ page: number; // Current page (1-indexed)
|
|
|
|
|
+ limit: number; // Items per page
|
|
|
|
|
+ total?: number; // Total items (if available)
|
|
|
|
|
+ has_more: boolean; // Whether more pages exist
|
|
|
|
|
+ next_page: number | null; // Next page number or null
|
|
|
|
|
+ prev_page: number | null; // Previous page number or null
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Error Response
|
|
|
|
|
+
|
|
|
|
|
+Failed requests return:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+interface ErrorDetails {
|
|
|
|
|
+ code: string; // Error code for programmatic handling
|
|
|
|
|
+ message: string; // Human-readable error message
|
|
|
|
|
+ details?: unknown; // Additional error context
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## Response Examples
|
|
|
|
|
+
|
|
|
|
|
+### List Response (Paginated)
|
|
|
|
|
+
|
|
|
|
|
+**Request:**
|
|
|
|
|
+```bash
|
|
|
|
|
+GET /shop-data-api/products?store_id={uuid}&page=1&limit=25
|
|
|
|
|
+Authorization: Bearer int_shopcall_xxx
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Response:**
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "success": true,
|
|
|
|
|
+ "data": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": "7654321",
|
|
|
|
|
+ "platform": "shopify",
|
|
|
|
|
+ "title": "Awesome Product",
|
|
|
|
|
+ "price": 29.99,
|
|
|
|
|
+ "currency": "USD",
|
|
|
|
|
+ "status": "active",
|
|
|
|
|
+ ...
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": "7654322",
|
|
|
|
|
+ "platform": "shopify",
|
|
|
|
|
+ "title": "Another Product",
|
|
|
|
|
+ "price": 49.99,
|
|
|
|
|
+ "currency": "USD",
|
|
|
|
|
+ "status": "active",
|
|
|
|
|
+ ...
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ "metadata": {
|
|
|
|
|
+ "platform": "shopify",
|
|
|
|
|
+ "store_id": "73ca58c0-e47f-4caa-bcdb-2d0b1fda27ce",
|
|
|
|
|
+ "resource_type": "products",
|
|
|
|
|
+ "auth_type": "internal",
|
|
|
|
|
+ "fetched_at": "2025-11-01T13:45:30.123Z",
|
|
|
|
|
+ "request_id": "req_1730462730_a8f3c9d2"
|
|
|
|
|
+ },
|
|
|
|
|
+ "pagination": {
|
|
|
|
|
+ "page": 1,
|
|
|
|
|
+ "limit": 25,
|
|
|
|
|
+ "has_more": true,
|
|
|
|
|
+ "next_page": 2,
|
|
|
|
|
+ "prev_page": null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Single Item Response
|
|
|
|
|
+
|
|
|
|
|
+**Request:**
|
|
|
|
|
+```bash
|
|
|
|
|
+GET /shop-data-api/orders/12345?store_id={uuid}
|
|
|
|
|
+Authorization: Bearer api_shopcall_xxx
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Response:**
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "success": true,
|
|
|
|
|
+ "data": {
|
|
|
|
|
+ "id": "12345",
|
|
|
|
|
+ "platform": "woocommerce",
|
|
|
|
|
+ "order_number": "1001",
|
|
|
|
|
+ "status": "completed",
|
|
|
|
|
+ "total_price": 129.99,
|
|
|
|
|
+ "currency": "USD",
|
|
|
|
|
+ "customer_name": "John Doe",
|
|
|
|
|
+ "customer_email": "john@example.com",
|
|
|
|
|
+ ...
|
|
|
|
|
+ },
|
|
|
|
|
+ "metadata": {
|
|
|
|
|
+ "platform": "woocommerce",
|
|
|
|
|
+ "store_id": "73ca58c0-e47f-4caa-bcdb-2d0b1fda27ce",
|
|
|
|
|
+ "resource_type": "orders",
|
|
|
|
|
+ "auth_type": "user",
|
|
|
|
|
+ "fetched_at": "2025-11-01T13:45:30.123Z",
|
|
|
|
|
+ "request_id": "req_1730462730_b9e4d1c3"
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Error Response
|
|
|
|
|
+
|
|
|
|
|
+**Request:**
|
|
|
|
|
+```bash
|
|
|
|
|
+GET /shop-data-api/customers?store_id=invalid-uuid
|
|
|
|
|
+Authorization: Bearer int_shopcall_xxx
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Response (404):**
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "success": false,
|
|
|
|
|
+ "data": [],
|
|
|
|
|
+ "metadata": {
|
|
|
|
|
+ "resource_type": "customers",
|
|
|
|
|
+ "auth_type": "internal",
|
|
|
|
|
+ "fetched_at": "2025-11-01T13:45:30.123Z"
|
|
|
|
|
+ },
|
|
|
|
|
+ "error": {
|
|
|
|
|
+ "code": "STORE_NOT_FOUND",
|
|
|
|
|
+ "message": "Store not found or access denied"
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Stores List Response
|
|
|
|
|
+
|
|
|
|
|
+**Request:**
|
|
|
|
|
+```bash
|
|
|
|
|
+GET /shop-data-api/stores
|
|
|
|
|
+Authorization: Bearer int_shopcall_xxx
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Response:**
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "success": true,
|
|
|
|
|
+ "data": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": "uuid-1",
|
|
|
|
|
+ "store_name": "My Shopify Store",
|
|
|
|
|
+ "store_url": "mystore.myshopify.com",
|
|
|
|
|
+ "platform_name": "shopify",
|
|
|
|
|
+ "is_active": true,
|
|
|
|
|
+ "connected_at": "2025-10-01T10:00:00Z",
|
|
|
|
|
+ "sync_status": "completed"
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": "uuid-2",
|
|
|
|
|
+ "store_name": "WooCommerce Shop",
|
|
|
|
|
+ "store_url": "woo.example.com",
|
|
|
|
|
+ "platform_name": "woocommerce",
|
|
|
|
|
+ "is_active": true,
|
|
|
|
|
+ "connected_at": "2025-10-15T14:30:00Z",
|
|
|
|
|
+ "sync_status": "idle"
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ "metadata": {
|
|
|
|
|
+ "resource_type": "stores",
|
|
|
|
|
+ "auth_type": "internal",
|
|
|
|
|
+ "fetched_at": "2025-11-01T13:45:30.123Z",
|
|
|
|
|
+ "request_id": "req_1730462730_c7f5e2d4"
|
|
|
|
|
+ },
|
|
|
|
|
+ "pagination": {
|
|
|
|
|
+ "page": 1,
|
|
|
|
|
+ "limit": 2,
|
|
|
|
|
+ "total": 2,
|
|
|
|
|
+ "has_more": false,
|
|
|
|
|
+ "next_page": null,
|
|
|
|
|
+ "prev_page": null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## Error Codes
|
|
|
|
|
+
|
|
|
|
|
+Common error codes returned by the API:
|
|
|
|
|
+
|
|
|
|
|
+| Code | HTTP Status | Description |
|
|
|
|
|
+|------|-------------|-------------|
|
|
|
|
|
+| `METHOD_NOT_ALLOWED` | 405 | Only GET requests are supported |
|
|
|
|
|
+| `INVALID_ENDPOINT` | 404 | Invalid API endpoint |
|
|
|
|
|
+| `MISSING_STORE_ID` | 400 | Required parameter `store_id` not provided |
|
|
|
|
|
+| `INVALID_RESOURCE` | 400 | Invalid resource type (must be customers, orders, products, or stores) |
|
|
|
|
|
+| `INVALID_PAGINATION` | 400 | Invalid pagination parameters |
|
|
|
|
|
+| `STORE_NOT_FOUND` | 404 | Store not found or access denied |
|
|
|
|
|
+| `ACCESS_DENIED` | 403 | User does not have permission to access this resource |
|
|
|
|
|
+| `UNSUPPORTED_PLATFORM` | 400 | Platform is not supported |
|
|
|
|
|
+| `FETCH_ERROR` | 500 | Error fetching data from e-commerce platform |
|
|
|
|
|
+| `MISSING_API_KEY` | 401 | No API key provided in Authorization header |
|
|
|
|
|
+
|
|
|
|
|
+## Benefits of Unified Format
|
|
|
|
|
+
|
|
|
|
|
+### 1. **Platform-Agnostic Code**
|
|
|
|
|
+
|
|
|
|
|
+You can write the same code to handle responses from any platform:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+async function fetchProducts(storeId: string) {
|
|
|
|
|
+ const response = await fetch(
|
|
|
|
|
+ `/shop-data-api/products?store_id=${storeId}`,
|
|
|
|
|
+ { headers: { Authorization: `Bearer ${apiKey}` } }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ // Same handling for Shopify, WooCommerce, ShopRenter
|
|
|
|
|
+ if (result.success) {
|
|
|
|
|
+ return result.data.map(product => ({
|
|
|
|
|
+ id: product.id,
|
|
|
|
|
+ name: product.title,
|
|
|
|
|
+ price: product.price,
|
|
|
|
|
+ platform: result.metadata.platform // Track which platform
|
|
|
|
|
+ }));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new Error(result.error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2. **Consistent Error Handling**
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+function handleApiError(response: UnifiedApiResponse<any>) {
|
|
|
|
|
+ if (!response.success && response.error) {
|
|
|
|
|
+ switch (response.error.code) {
|
|
|
|
|
+ case 'STORE_NOT_FOUND':
|
|
|
|
|
+ alert('Store not found');
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'ACCESS_DENIED':
|
|
|
|
|
+ alert('You do not have permission to access this resource');
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ alert(`Error: ${response.error.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 3. **Uniform Pagination**
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+function renderPagination(pagination: Pagination) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ {pagination.prev_page && (
|
|
|
|
|
+ <button onClick={() => loadPage(pagination.prev_page)}>
|
|
|
|
|
+ Previous
|
|
|
|
|
+ </button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <span>Page {pagination.page}</span>
|
|
|
|
|
+ {pagination.next_page && (
|
|
|
|
|
+ <button onClick={() => loadPage(pagination.next_page)}>
|
|
|
|
|
+ Next
|
|
|
|
|
+ </button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 4. **Request Tracking**
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// Track requests for debugging
|
|
|
|
|
+console.log(`Request ID: ${response.metadata.request_id}`);
|
|
|
|
|
+console.log(`Fetched at: ${response.metadata.fetched_at}`);
|
|
|
|
|
+console.log(`Platform: ${response.metadata.platform}`);
|
|
|
|
|
+console.log(`Auth type: ${response.metadata.auth_type}`);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## Available Endpoints
|
|
|
|
|
+
|
|
|
|
|
+All endpoints use the unified response format:
|
|
|
|
|
+
|
|
|
|
|
+- `GET /shop-data-api/stores` - List all stores
|
|
|
|
|
+- `GET /shop-data-api/products?store_id={uuid}` - List products
|
|
|
|
|
+- `GET /shop-data-api/products/{id}?store_id={uuid}` - Get single product
|
|
|
|
|
+- `GET /shop-data-api/orders?store_id={uuid}` - List orders
|
|
|
|
|
+- `GET /shop-data-api/orders/{id}?store_id={uuid}` - Get single order
|
|
|
|
|
+- `GET /shop-data-api/customers?store_id={uuid}` - List customers
|
|
|
|
|
+- `GET /shop-data-api/customers/{id}?store_id={uuid}` - Get single customer
|
|
|
|
|
+
|
|
|
|
|
+### Query Parameters
|
|
|
|
|
+
|
|
|
|
|
+- `store_id` (required for data endpoints) - Store UUID
|
|
|
|
|
+- `page` (optional, default: 1) - Page number (1-indexed)
|
|
|
|
|
+- `limit` (optional, default: 25, max: 100) - Items per page
|
|
|
|
|
+- `status` (optional, orders only) - Filter by order status
|
|
|
|
|
+
|
|
|
|
|
+## Implementation Details
|
|
|
|
|
+
|
|
|
|
|
+### Files
|
|
|
|
|
+
|
|
|
|
|
+- **Response types:** `supabase/functions/_shared/unified-response.ts`
|
|
|
|
|
+- **API implementation:** `supabase/functions/shop-data-api/index.ts`
|
|
|
|
|
+- **Platform adapters:** `supabase/functions/_shared/platform-adapters.ts`
|
|
|
|
|
+
|
|
|
|
|
+### Helper Functions
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// Create success response for single item
|
|
|
|
|
+createSuccessResponse(data, metadata)
|
|
|
|
|
+
|
|
|
|
|
+// Create list response with pagination
|
|
|
|
|
+createListResponse(data, metadata, pagination)
|
|
|
|
|
+
|
|
|
|
|
+// Create error response
|
|
|
|
|
+createErrorResponse(code, message, details?, status?)
|
|
|
|
|
+
|
|
|
|
|
+// Convert to HTTP Response
|
|
|
|
|
+toHttpResponse(response, status?)
|
|
|
|
|
+
|
|
|
|
|
+// Calculate has_more flag
|
|
|
|
|
+calculateHasMore(returnedCount, requestedLimit)
|
|
|
|
|
+
|
|
|
|
|
+// Generate unique request ID
|
|
|
|
|
+generateRequestId()
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## Migration Guide
|
|
|
|
|
+
|
|
|
|
|
+If you have existing code that relies on the old response format:
|
|
|
|
|
+
|
|
|
|
|
+### Before (Old Format)
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+const response = await fetch('/shop-data-api/products');
|
|
|
|
|
+const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+// Different structure per platform
|
|
|
|
|
+if (data.success) {
|
|
|
|
|
+ const products = data.data;
|
|
|
|
|
+ const page = data.pagination?.page;
|
|
|
|
|
+ const platform = data.platform; // May or may not exist
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### After (Unified Format)
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+const response = await fetch('/shop-data-api/products?store_id=uuid');
|
|
|
|
|
+const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+// Same structure for all platforms
|
|
|
|
|
+if (data.success) {
|
|
|
|
|
+ const products = data.data;
|
|
|
|
|
+ const page = data.pagination.page;
|
|
|
|
|
+ const platform = data.metadata.platform; // Always exists
|
|
|
|
|
+ const requestId = data.metadata.request_id; // New: track requests
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## See Also
|
|
|
|
|
+
|
|
|
|
|
+- [Internal API Keys Documentation](INTERNAL_API_KEYS.md)
|
|
|
|
|
+- [Platform Adapters](supabase/functions/_shared/platform-adapters.ts)
|
|
|
|
|
+- Issue #50 - Non user-based API keys
|
|
|
|
|
+- Issue #53 - Unified webshop API
|