# Connection Apps - Technical Specification **Version:** 2.0 **Last Updated:** 24 November 2025 **Status:** Ready for Development --- ## 📋 Overview Fitur **Connection Apps** mempersimpel setup koneksi antara Membership System dengan Booking Engine di level Corporate dashboard. **Key Features:** - View connection status untuk semua branch - Setup wizard 3 langkah untuk create connection - Auto-generate product dan access token - Manage existing connections (edit, regenerate token, delete) - Support legacy connections (commerce_site) --- ## 🎯 Business Requirements ### User Story **As a** Corporate level user **I want to** setup dan manage connections ke Booking Engine **So that** branch dan merchant saya bisa terintegrasi dengan mudah ### Functional Requirements 1. **View Connection Status** - List semua branch dengan status koneksi - Status: Connected (legacy/new) atau Not Connected - Display connection details jika ada 2. **Create Connection** - 3-step wizard: Branch/Merchant → Property ID → Credentials - Auto-generate product (price: 0) - Auto-generate access token - Support existing atau new branch/merchant 3. **Manage Connection** - Edit property_id - Regenerate access token - Soft delete connection 4. **Legacy Support** - Detect existing connections via commerce_site - Allow upgrade to new system - Maintain backward compatibility --- ## 🗄️ Database Schema ### Table: `app_connections` (Tenant Database) ```sql CREATE TABLE app_connections ( id UUID PRIMARY KEY, corporate_id UUID NOT NULL, branch_id UUID NOT NULL, merchant_id UUID NOT NULL, product_id UUID NULL, property_id VARCHAR(255) NULL, access_token_id BIGINT UNSIGNED NULL, status ENUM('active', 'inactive') DEFAULT 'active', created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, deleted_at TIMESTAMP NULL, FOREIGN KEY (corporate_id) REFERENCES corporates(id) ON DELETE CASCADE, FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE, FOREIGN KEY (merchant_id) REFERENCES merchants(id) ON DELETE CASCADE, FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE SET NULL, FOREIGN KEY (access_token_id) REFERENCES personal_access_tokens(id) ON DELETE SET NULL, UNIQUE KEY unique_branch_merchant (branch_id, merchant_id), INDEX idx_corporate_id (corporate_id), INDEX idx_branch_id (branch_id), INDEX idx_merchant_id (merchant_id), INDEX idx_property_id (property_id), INDEX idx_status (status), INDEX idx_deleted_at (deleted_at) ); ``` **Column Descriptions:** | Column | Type | Description | |--------|------|-------------| | `id` | UUID | Primary key | | `corporate_id` | UUID | Corporate owner (auto-resolved from auth user) | | `branch_id` | UUID | Connected branch | | `merchant_id` | UUID | Connected merchant | | `product_id` | UUID | Auto-generated product ID | | `property_id` | VARCHAR(20) | Property ID dari Booking Engine (numeric only, max 20 digits) | | `access_token_id` | BIGINT | Reference ke personal_access_tokens | | `status` | ENUM | active/inactive | | `deleted_at` | TIMESTAMP | Soft delete support | **Business Rules:** - One connection per branch+merchant combination - Cascade delete when branch/merchant deleted - Product auto-generated with price 0 - Access token can be regenerated --- ## 🔌 API Endpoints ### Base Configuration - **Route Prefix:** `/api/connection-apps` - **Authentication:** Bearer Token (Sanctum) - **Authorization:** Corporate Level Only - **Corporate ID:** Auto-resolved from authenticated user ### Common Error Responses **401 Unauthorized:** ```json { "success": false, "message": "Unauthenticated" } ``` **403 Forbidden:** ```json { "success": false, "message": "Unauthorized. Corporate level access required." } ``` **404 Not Found:** ```json { "success": false, "message": "Resource not found" } ``` **422 Validation Error:** ```json { "success": false, "message": "Validation failed", "errors": { "field_name": ["Error message"] } } ``` **500 Server Error:** ```json { "success": false, "message": "Internal server error" } ``` --- ## 📋 API Flow & Endpoints ### Flow 1: View Existing Connections **Step 1:** User opens Connection Apps page **API Call:** `GET /api/connection-apps` --- ### Flow 2: Create New Connection (Use Existing Branch/Merchant) **Step 1:** Get available branches **Step 2:** Select branch & merchant **Step 3:** Input property ID & create connection **Step 4:** Display credentials --- ### Flow 3: Create New Connection (Create New Branch/Merchant) **Step 1:** Input new branch & merchant data **Step 2:** Input property ID & create connection **Step 3:** Display credentials --- ## 📡 Detailed API Endpoints ### 1. GET List Connections **Endpoint:** `GET /api/connection-apps` **Description:** List semua branch dengan status koneksi **Headers:** ``` Authorization: Bearer {token} X-Tenant-Domain: {tenant-subdomain} ``` **Response Success (200):** ```json { "success": true, "message": "Connections retrieved successfully", "data": [ { "branch_id": "uuid-branch-1", "branch_name": "Branch Jakarta", "branch_code": "JKT001", "connection_status": "connected", "connection_type": "new", "connection": { "id": "uuid-connection-1", "merchant_id": "uuid-merchant-1", "merchant_name": "Merchant A", "property_id": "PROP-12345", "accommodation_id": "uuid-product-123", "product_name": "Connection Product - PROP-12345", "status": "active", "created_at": "2025-11-24T10:00:00Z" } }, { "branch_id": "uuid-branch-2", "branch_name": "Branch Bandung", "branch_code": "BDG001", "connection_status": "connected", "connection_type": "legacy", "connection": { "merchant_id": "uuid-merchant-2", "merchant_name": "Merchant B", "commerce_site": "https://booking-engine.com/property/456" } }, { "branch_id": "uuid-branch-3", "branch_name": "Branch Surabaya", "branch_code": "SBY001", "connection_status": "not_connected", "connection_type": "none", "connection": null } ] } ``` **Response Error (403):** ```json { "success": false, "message": "Unauthorized. Corporate level access required." } ``` **Response Error (500):** ```json { "success": false, "message": "Failed to retrieve connections" } ``` **Connection Status Logic:** - `connected` if merchant has `commerce_site` (legacy) OR `app_connection` (new) - `not_connected` if no commerce_site and no app_connection - Priority: new connection over legacy --- ### 2. GET Available Branches **Endpoint:** `GET /api/connection-apps/available-branches` **Description:** Get list branch yang tersedia untuk create connection (untuk Step 1 wizard) **Headers:** ``` Authorization: Bearer {token} X-Tenant-Domain: {tenant-subdomain} ``` **Response Success (200):** ```json { "success": true, "message": "Available branches retrieved successfully", "data": [ { "branch_id": "uuid-branch-1", "branch_name": "Branch Jakarta", "branch_code": "JKT001", "has_connection": false, "merchants": [ { "merchant_id": "uuid-merchant-1", "merchant_name": "Merchant A", "merchant_code": "MRC001", "has_connection": false, "connection_type": "none" }, { "merchant_id": "uuid-merchant-2", "merchant_name": "Merchant B", "merchant_code": "MRC002", "has_connection": true, "connection_type": "legacy" } ] }, { "branch_id": "uuid-branch-2", "branch_name": "Branch Bandung", "branch_code": "BDG001", "has_connection": true, "merchants": [ { "merchant_id": "uuid-merchant-3", "merchant_name": "Merchant C", "merchant_code": "MRC003", "has_connection": true, "connection_type": "new" } ] } ] } ``` **Response Error (403):** ```json { "success": false, "message": "Unauthorized. Corporate level access required." } ``` **Response Error (500):** ```json { "success": false, "message": "Failed to retrieve available branches" } ``` **Usage:** Frontend calls this endpoint saat user membuka Step 1 wizard untuk populate dropdown branch dan merchant --- ### 3. GET Connection Detail **Endpoint:** `GET /api/connection-apps/{id}` **Description:** Get detail connection tertentu **Headers:** ``` Authorization: Bearer {token} X-Tenant-Domain: {tenant-subdomain} ``` **Path Parameters:** - `id` (UUID, required): Connection ID **Response Success (200):** ```json { "success": true, "message": "Connection retrieved successfully", "data": { "id": "uuid-connection-1", "branch_id": "uuid-branch-1", "merchant_id": "uuid-merchant-1", "product_id": "uuid-product-123", "property_id": "12345", "accommodation_id": "uuid-product-123", "status": "active", "created_at": "2025-11-24T10:00:00Z", "updated_at": "2025-11-24T10:00:00Z", "branch": { "id": "uuid-branch-1", "name": "Branch Jakarta", "code": "JKT001" }, "merchant": { "id": "uuid-merchant-1", "name": "Merchant A", "code": "MRC001" }, "product": { "id": "uuid-product-123", "name": "Connection Product - 12345", "price": 0 } } } ``` **Response Error (404):** ```json { "success": false, "message": "Connection not found" } ``` **Response Error (403):** ```json { "success": false, "message": "Unauthorized to access this connection" } ``` --- ### 4. POST Setup Step 1 (Branch & Merchant) **Endpoint:** `POST /api/connection-apps/setup/step-1` **Description:** Setup branch dan merchant (pilih existing atau buat baru) **Headers:** ``` Authorization: Bearer {token} X-Tenant-Domain: {tenant-subdomain} Content-Type: application/json ``` **Request Body (Use Existing):** ```json { "setup_type": "existing", "branch_id": "uuid-branch-1", "merchant_id": "uuid-merchant-1" } ``` **Request Body (Create New):** ```json { "setup_type": "new", "branch": { "code": "BDG002", "name": "Branch Bandung 2", "address": "Jl. Sudirman No. 123", "state": "West Java", "country": "Indonesia", "phone": "+62211234567", "fax": "+62211234568", "website": "https://branch.com", "city": "Bandung", "postcode": "40123", "logo": null }, "merchant": { "name": "Merchant Bandung 2", "code": "MRC002", "address": "Jl. Sudirman No. 123", "state": "West Java", "country": "Indonesia", "phone": "+62211234567", "fax": "+62211234568", "website": "https://merchant.com", "city": "Bandung", "postcode": "40123", "logo": null } } ``` **Validation Rules:** **Common:** - `setup_type`: required, string, in:existing,new **If setup_type = "existing":** - `branch_id`: required, uuid, exists:branches,id - `merchant_id`: required, uuid, exists:merchants,id - Validate branch belongs to user's corporate - Validate merchant belongs to selected branch **If setup_type = "new":** - `branch.code`: required, string, max:10, unique:branches,code - `branch.name`: required, string, max:45 - `branch.address`: required, string, max:255 - `branch.state`: required, string, max:45 - `branch.country`: required, string, max:45 - `branch.phone`: required, string, max:45 - `branch.fax`: required, string, max:45 - `branch.website`: nullable, string, url, max:255 - `branch.city`: required, string, max:45 - `branch.postcode`: required, string, max:10 - `branch.logo`: nullable, string (base64) - `merchant.*`: same validation as branch **Response Success (201):** ```json { "success": true, "message": "Step 1 completed successfully", "data": { "branch_id": "uuid-branch-1", "branch_name": "Branch Bandung 2", "branch_code": "BDG002", "merchant_id": "uuid-merchant-1", "merchant_name": "Merchant Bandung 2", "merchant_code": "MRC002", "next_step": 2 } } ``` **Response Error (422) - Validation:** ```json { "success": false, "message": "Validation failed", "errors": { "setup_type": ["The setup type field is required."], "branch_id": ["The selected branch id is invalid."], "branch.code": ["The branch code has already been taken."] } } ``` **Response Error (403):** ```json { "success": false, "message": "Branch does not belong to your corporate" } ``` **Response Error (500):** ```json { "success": false, "message": "Failed to setup branch and merchant" } ``` --- ### 5. POST Setup Step 2 (Create Connection) **Endpoint:** `POST /api/connection-apps/setup/step-2` **Description:** Create connection dengan auto-generate product dan access token **Headers:** ``` Authorization: Bearer {token} X-Tenant-Domain: {tenant-subdomain} Content-Type: application/json ``` **Request Body:** ```json { "branch_id": "uuid-branch-1", "merchant_id": "uuid-merchant-1", "property_id": "12345", "token_name": "Connection Token - Branch Jakarta" } ``` **Validation Rules:** - `branch_id`: required, uuid, exists:branches,id - `merchant_id`: required, uuid, exists:merchants,id - `property_id`: required, numeric, digits_between:1,20 - `token_name`: required, string, max:255 - Validate branch belongs to user's corporate - Validate merchant belongs to branch - Validate no duplicate connection (branch+merchant combination) **Response Success (201):** ```json { "success": true, "message": "Connection created successfully", "data": { "connection_id": "uuid-connection-1", "product_id": "uuid-product-auto-generated", "product_name": "Connection Product - 12345", "access_token": "1|xxxxxxxxxxxxxxxxxxxxxx", "next_step": 3 } } ``` **Response Error (422) - Validation:** ```json { "success": false, "message": "Validation failed", "errors": { "property_id": ["The property id field is required."], "token_name": ["The token name field is required."] } } ``` **Response Error (409) - Duplicate:** ```json { "success": false, "message": "Connection already exists for this branch and merchant" } ``` **Response Error (403):** ```json { "success": false, "message": "Merchant does not belong to the selected branch" } ``` **Response Error (500):** ```json { "success": false, "message": "Failed to create connection" } ``` **Business Logic:** 1. Validate no duplicate connection (branch+merchant) 2. Auto-generate product: - name: "Connection Product - {property_id}" - price: 0 - merchant_id: selected merchant 3. Generate access token via Sanctum with abilities: ['connection-app'] 4. Get corporate_id from authenticated user 5. Create app_connection record 6. Return plain text token (⚠️ only shown once!) --- ### 6. GET Connection Credentials **Endpoint:** `GET /api/connection-apps/{id}/credentials` **Description:** Get credentials untuk display di Step 3 wizard atau untuk reference **Headers:** ``` Authorization: Bearer {token} X-Tenant-Domain: {tenant-subdomain} ``` **Path Parameters:** - `id` (UUID, required): Connection ID **Response Success (200) - After Creation:** ```json { "success": true, "message": "Credentials retrieved successfully", "data": { "access_token": "1|xxxxxxxxxxxxxxxxxxxxxx", "property_id": "12345", "accommodation_id": "uuid-product-123", "x_tenant_domain": "tenant.membership.com" }, "note": "Access token shown only once. Save it securely." } ``` **Response Success (200) - Existing Connection:** ```json { "success": true, "message": "Credentials retrieved successfully", "data": { "access_token": null, "property_id": "12345", "accommodation_id": "uuid-product-123", "x_tenant_domain": "tenant.membership.com" }, "note": "Access token not available. Please regenerate if needed." } ``` **Response Error (404):** ```json { "success": false, "message": "Connection not found" } ``` **Response Error (403):** ```json { "success": false, "message": "Unauthorized to access this connection" } ``` **Note:** - Access token (plain text) hanya tersedia **immediately after creation** di Step 2 - Untuk existing connections, access_token akan null - User harus regenerate token jika kehilangan --- ### 7. PUT Update Connection **Endpoint:** `PUT /api/connection-apps/{id}` **Description:** Update property_id atau status connection **Headers:** ``` Authorization: Bearer {token} X-Tenant-Domain: {tenant-subdomain} Content-Type: application/json ``` **Path Parameters:** - `id` (UUID, required): Connection ID **Request Body:** ```json { "property_id": "67890", "status": "active" } ``` **Validation Rules:** - `property_id`: nullable, numeric, digits_between:1,20 - `status`: nullable, string, in:active,inactive - At least one field must be provided **Response Success (200):** ```json { "success": true, "message": "Connection updated successfully", "data": { "id": "uuid-connection-1", "property_id": "67890", "product_id": "uuid-product-123", "product_name": "Connection Product - 67890", "status": "active", "updated_at": "2025-11-24T12:00:00Z" } } ``` **Response Error (422) - Validation:** ```json { "success": false, "message": "Validation failed", "errors": { "status": ["The selected status is invalid."] } } ``` **Response Error (404):** ```json { "success": false, "message": "Connection not found" } ``` **Response Error (403):** ```json { "success": false, "message": "Unauthorized to update this connection" } ``` **Response Error (500):** ```json { "success": false, "message": "Failed to update connection" } ``` **Business Logic:** - If property_id changed, auto-update product name to "Connection Product - {new_property_id}" - Branch and merchant **cannot be changed** after creation - Product remains tied to connection - Property ID must be numeric only (no letters or special characters) --- ### 8. POST Regenerate Token **Endpoint:** `POST /api/connection-apps/{id}/regenerate-token` **Description:** Regenerate access token untuk connection (revoke old, create new) **Headers:** ``` Authorization: Bearer {token} X-Tenant-Domain: {tenant-subdomain} Content-Type: application/json ``` **Path Parameters:** - `id` (UUID, required): Connection ID **Request Body:** ```json { "token_name": "New Connection Token - Branch Jakarta" } ``` **Validation Rules:** - `token_name`: required, string, max:255 **Response Success (200):** ```json { "success": true, "message": "Access token regenerated successfully", "data": { "connection_id": "uuid-connection-1", "access_token": "2|yyyyyyyyyyyyyyyyyyyy", "token_name": "New Connection Token - Branch Jakarta", "regenerated_at": "2025-11-24T13:00:00Z" }, "note": "Save this token securely. It will not be shown again." } ``` **Response Error (422) - Validation:** ```json { "success": false, "message": "Validation failed", "errors": { "token_name": ["The token name field is required."] } } ``` **Response Error (404):** ```json { "success": false, "message": "Connection not found" } ``` **Response Error (403):** ```json { "success": false, "message": "Unauthorized to regenerate token for this connection" } ``` **Response Error (500):** ```json { "success": false, "message": "Failed to regenerate token" } ``` **Business Logic:** 1. Find connection and validate ownership 2. Revoke old token (delete from personal_access_tokens) 3. Generate new token via Sanctum with abilities: ['connection-app'] 4. Update access_token_id in app_connections 5. Return new plain text token (⚠️ only shown once!) --- ### 9. DELETE Connection **Endpoint:** `DELETE /api/connection-apps/{id}` **Description:** Soft delete connection (revoke token, delete product) **Headers:** ``` Authorization: Bearer {token} X-Tenant-Domain: {tenant-subdomain} ``` **Path Parameters:** - `id` (UUID, required): Connection ID **Response Success (200):** ```json { "success": true, "message": "Connection deleted successfully" } ``` **Response Error (404):** ```json { "success": false, "message": "Connection not found" } ``` **Response Error (403):** ```json { "success": false, "message": "Unauthorized to delete this connection" } ``` **Response Error (500):** ```json { "success": false, "message": "Failed to delete connection" } ``` **Business Logic:** 1. Find connection and validate ownership 2. Soft delete connection (set deleted_at) 3. Revoke access token (delete from personal_access_tokens) 4. Soft delete associated product 5. Status auto-set to inactive **Note:** This is a soft delete. Data remains in database with deleted_at timestamp. --- ## 🏗️ Backend Architecture ### File Structure ``` membership-api/ ├── app/ │ ├── Domains/ │ │ └── AppConnectionDomain.php # NEW │ ├── Http/Controllers/ │ │ └── AppConnectionController.php # NEW │ ├── Models/ │ │ └── AppConnection.php # NEW │ └── Policies/ │ └── AppConnectionPolicy.php # NEW ├── database/migrations/tenant/ │ └── 2025_11_24_000001_create_app_connections_table.php # NEW └── routes/ └── api.php # UPDATE ``` ### Domain: AppConnectionDomain.php **Responsibilities:** - Business logic untuk connection management - Orchestrate calls ke existing domains (Branch, Merchant, Product, AccessToken) - Handle connection status detection (legacy + new) **Key Methods:** ```php public function getConnectionsList() public function getConnectionById($id) public function setupBranchMerchant($data) public function createConnection($data) public function getConnectionCredentials($id) public function updateConnection($id, $data) public function regenerateAccessToken($id, $tokenName) public function deleteConnection($id) public function getAvailableBranches() ``` **Reuse Existing Domains:** - `BranchDomain::create()` - Create new branch - `MerchantDomain::create()` - Create new merchant - `ProductDomain::create()` - Auto-generate connection product - User's `createToken()` - Generate access token (Sanctum) --- ### Controller: AppConnectionController.php **Methods:** ```php public function index(Request $request) // GET / public function show($id) // GET /{id} public function setupStep1(Request $request) // POST /setup/step-1 public function setupStep2(Request $request) // POST /setup/step-2 public function getCredentials($id) // GET /{id}/credentials public function update(Request $request, $id) // PUT /{id} public function regenerateToken(Request $request, $id) // POST /{id}/regenerate-token public function destroy($id) // DELETE /{id} public function getAvailableBranches(Request $request) // GET /available-branches ``` --- ### Model: AppConnection.php **Relationships:** ```php public function corporate() // belongsTo Corporate public function branch() // belongsTo Branch public function merchant() // belongsTo Merchant public function product() // belongsTo Product public function accessToken() // belongsTo PersonalAccessToken ``` **Scopes:** ```php public function scopeActive($query) public function scopeByCorporate($query, $corporateId) ``` **Accessors:** ```php public function getAccommodationIdAttribute() // Returns product_id public function getAuthUrlAttribute() // Construct from config + property_id ``` --- ### Policy: AppConnectionPolicy.php **Authorization Rules:** - User must be corporate level - User can only access connections owned by their corporate - Validate ownership before update/delete **Methods:** ```php public function viewAny(User $user) public function view(User $user, AppConnection $connection) public function create(User $user) public function update(User $user, AppConnection $connection) public function delete(User $user, AppConnection $connection) ``` --- ## 🎨 Frontend Architecture ### File Structure ``` membership-vue/ ├── src/ │ ├── views/ConnectionApps/ │ │ ├── ConnectionAppsList.vue # NEW - Main list page │ │ ├── ConnectionSetupWizard.vue # NEW - 3-step wizard │ │ └── ConnectionDetail.vue # NEW - Detail & edit page │ ├── components/ConnectionApps/ │ │ ├── ConnectionStatusCard.vue # NEW - Status display card │ │ ├── BranchMerchantSetup.vue # NEW - Step 1 component │ │ ├── ConnectionSetup.vue # NEW - Step 2 component │ │ └── CredentialsDisplay.vue # NEW - Step 3 component │ ├── services/ │ │ └── connectionAppsService.js # NEW - API service │ └── router/ │ └── index.js # UPDATE - Add routes ``` ### Service: connectionAppsService.js ```javascript import apiClient from '@/services/apiClient' export default { getConnectionsList() { return apiClient.get('/connection-apps') }, getConnectionById(id) { return apiClient.get(`/connection-apps/${id}`) }, setupStep1(data) { return apiClient.post('/connection-apps/setup/step-1', data) }, setupStep2(data) { return apiClient.post('/connection-apps/setup/step-2', data) }, getCredentials(id) { return apiClient.get(`/connection-apps/${id}/credentials`) }, updateConnection(id, data) { return apiClient.put(`/connection-apps/${id}`, data) }, regenerateToken(id, tokenName) { return apiClient.post(`/connection-apps/${id}/regenerate-token`, { token_name: tokenName }) }, deleteConnection(id) { return apiClient.delete(`/connection-apps/${id}`) }, getAvailableBranches() { return apiClient.get('/connection-apps/available-branches') } } ``` ### Router Configuration ```javascript { path: '/connection-apps', name: 'ConnectionApps', component: () => import('@/views/ConnectionApps/ConnectionAppsList.vue'), meta: { requiresAuth: true, level: 'corporate' } }, { path: '/connection-apps/setup', name: 'ConnectionSetup', component: () => import('@/views/ConnectionApps/ConnectionSetupWizard.vue'), meta: { requiresAuth: true, level: 'corporate' } }, { path: '/connection-apps/:id', name: 'ConnectionDetail', component: () => import('@/views/ConnectionApps/ConnectionDetail.vue'), meta: { requiresAuth: true, level: 'corporate' } } ``` ### Component Overview #### 1. ConnectionAppsList.vue **Purpose:** Main page displaying all branches with connection status **Features:** - List branches in cards/table - Status badges (Connected/Not Connected) - Connection type indicator (Legacy/New) - Action buttons based on status - Filter and search #### 2. ConnectionSetupWizard.vue **Purpose:** 3-step wizard for creating connections **Features:** - Stepper UI (Step 1, 2, 3) - Navigation (Next, Back, Cancel) - Progress indicator - Form validation per step **Steps:** - Step 1: BranchMerchantSetup.vue - Step 2: ConnectionSetup.vue - Step 3: CredentialsDisplay.vue #### 3. BranchMerchantSetup.vue (Step 1) **Purpose:** Select or create branch and merchant **Features:** - Radio: "Use Existing" vs "Create New" - Dropdown for existing branches/merchants - Form for creating new branch/merchant - Reuse existing form validation logic #### 4. ConnectionSetup.vue (Step 2) **Purpose:** Input property ID and generate connection **Features:** - Input: Property ID (required) - Input: Token Name (auto-generated, editable) - Info text about auto-generated product - Preview selected branch & merchant **Default Token Name:** ```javascript `Connection Token - ${branchName} - ${new Date().toISOString().split('T')[0]}` ``` #### 5. CredentialsDisplay.vue (Step 3) **Purpose:** Display credentials for Booking Engine **Features:** - Display 4 fields with copy buttons: 1. Access Token (with show/hide toggle) 2. Property ID 3. Accommodation ID (Product ID) 4. X-Tenant-Domain - Copy to clipboard functionality - Warning: "Save token - shown only once" - Button "Done" to return to list #### 6. ConnectionDetail.vue **Purpose:** View and edit existing connection **Features:** - Display connection info (read-only: branch, merchant, product) - Editable: property_id, status - Display computed auth_url (read-only) - Button "Regenerate Token" - Button "Delete Connection" - Confirmation modals --- ## 🎨 UI/UX Guidelines ### Design Principles - **Spacing over decoration**: Prioritize spacing and typography - **Minimal shadows**: Only for necessary depth - **No excessive gradients**: Use solid colors - **High contrast**: For readability - **Consistent design**: Follow existing design system ### Color Scheme - **Primary**: CTA buttons (Create, Save, Regenerate) - **Success**: Status "Connected" and success messages - **Warning**: Warning messages - **Danger**: Delete actions - **Neutral**: Status "Not Connected" ### Typography - **Headings**: Clear hierarchy (h1, h2, h3) - **Body text**: 14-16px readable font - **Labels**: Consistent styling - **Code/Credentials**: Monospace font ### Spacing - **Card padding**: 24px - **Section spacing**: 32px - **Form field spacing**: 16px - **Button spacing**: 12px --- ## 🔐 Security & Authorization ### Authentication - All endpoints require Bearer token (Sanctum) - Token validated via middleware ### Authorization - Corporate level users only - Policy checks ownership before operations - Corporate ID auto-resolved from authenticated user ### Token Management - Access tokens generated via Sanctum - Plain text token shown only once - Tokens can be regenerated - Old tokens revoked on regeneration ### Data Validation - Input validation at controller level - UUID validation for all IDs - URL format validation for property_id - Unique constraint enforcement (branch+merchant) ### Tenant Isolation - All queries scoped to tenant - Corporate ID from authenticated user - No cross-tenant access possible --- ## 🔄 Connection Status Logic ### Status Determination Branch is **"connected"** if: 1. Has merchant with `commerce_site` not null/empty (legacy), OR 2. Has merchant with active `app_connection` (new) Branch is **"not_connected"** if: - No merchants, OR - All merchants have no commerce_site and no app_connection ### Priority Logic ```php // Check connection type if ($merchant->appConnection) { return 'new'; // Prioritize new system } elseif (!empty($merchant->businessProfile->commerce_site)) { return 'legacy'; // Fallback to legacy } else { return 'none'; } ``` ### Model Methods ```php // Merchant Model public function hasActiveConnection() { // Check new connection if ($this->appConnection()->exists()) { return true; } // Check legacy connection if ($this->businessProfile && !empty($this->businessProfile->commerce_site)) { return true; } return false; } // Branch Model public function hasActiveConnection() { return $this->merchant() ->where(function($query) { $query->whereHas('appConnection', function($q) { $q->where('status', 'active')->whereNull('deleted_at'); }) ->orWhereHas('businessProfile', function($q) { $q->whereNotNull('commerce_site')->where('commerce_site', '!=', ''); }); }) ->exists(); } ``` --- ## 🔧 Auto-Generated Product Logic ### Product Specification ```php [ 'name' => "Connection Product - {property_id}", 'price' => 0, 'merchant_id' => $merchantId ] // Example: // property_id: "12345" // Product name: "Connection Product - 12345" ``` ### Creation Logic ```php // Auto-generate during Step 2 $product = Product::create([ 'name' => "Connection Product - {$data['property_id']}", 'price' => 0, 'merchant_id' => $data['merchant_id'] ]); ``` ### Update Logic ```php // If property_id changes, update product name if ($data['property_id'] !== $connection->property_id) { $connection->product->update([ 'name' => "Connection Product - {$data['property_id']}" ]); } ``` ### Delete Logic ```php // Cascade delete product when connection deleted if ($connection->product_id) { Product::find($connection->product_id)->delete(); // soft delete } ``` --- ## 📊 Field Mapping: Membership vs Booking Engine | Membership System | Booking Engine | Description | |-------------------|----------------|-------------| | Property ID (input) | Property ID | Property identifier | | Product ID (generated) | Accommodation ID | Room/Unit type | | Branch | Property | Hotel/Branch | | Merchant | Property Management | Management entity | | Access Token | Auth Token | API authentication | ### Credentials Output (Step 3) 1. **Access Token**: Generated Sanctum token 2. **Property ID**: User input from Step 2 3. **Accommodation ID**: Auto-generated Product ID 4. **X-Tenant-Domain**: Tenant subdomain --- ## 🔄 Auth URL Construction ### Concept Property ID stored in database, Auth URL constructed at runtime. ### Backend Logic ```php // AppConnection Model - Accessor public function getAuthUrlAttribute() { if (!$this->property_id) { return null; } $baseUrl = config('app.booking_engine_host'); return rtrim($baseUrl, '/') . '/' . $this->property_id; } ``` ### Environment Config ```env # .env BOOKING_ENGINE_HOST=http://localhost:18081/api/public/membership/ # Production BOOKING_ENGINE_HOST=https://booking-engine.production.com/api/membership/ ``` ### Example **Input:** Property ID = "12345" **Stored:** `property_id = "12345"` **Computed:** `auth_url = "http://localhost:18081/api/public/membership/12345"` --- ## 🚀 Implementation Checklist ### Phase 1: Backend Foundation - [ ] Create migration: `app_connections` table - [ ] Create model: `AppConnection.php` - [ ] Create domain: `AppConnectionDomain.php` - [ ] Create controller: `AppConnectionController.php` - [ ] Create policy: `AppConnectionPolicy.php` - [ ] Add routes to `api.php` ### Phase 2: Backend API - [ ] Implement list connections endpoint - [ ] Implement setup step 1 endpoint - [ ] Implement setup step 2 endpoint - [ ] Implement get credentials endpoint - [ ] Implement update connection endpoint - [ ] Implement regenerate token endpoint - [ ] Implement delete connection endpoint - [ ] Implement available branches endpoint ### Phase 3: Backend Testing - [ ] Unit tests for domain methods - [ ] Feature tests for API endpoints - [ ] Policy authorization tests - [ ] Tenant isolation tests - [ ] Connection status logic tests ### Phase 4: Frontend Foundation - [ ] Create service: `connectionAppsService.js` - [ ] Create views structure - [ ] Create components structure - [ ] Add routes to router - [ ] Setup navigation menu ### Phase 5: Frontend Implementation - [ ] Implement `ConnectionAppsList.vue` - [ ] Implement `ConnectionSetupWizard.vue` - [ ] Implement `BranchMerchantSetup.vue` (Step 1) - [ ] Implement `ConnectionSetup.vue` (Step 2) - [ ] Implement `CredentialsDisplay.vue` (Step 3) - [ ] Implement `ConnectionDetail.vue` - [ ] Implement shared components ### Phase 6: Frontend Styling - [ ] Apply design system - [ ] Implement responsive design - [ ] Add loading states - [ ] Add error handling - [ ] Add success feedback - [ ] Polish UI/UX ### Phase 7: Integration Testing - [ ] Test complete flow end-to-end - [ ] Test error scenarios - [ ] Test edge cases - [ ] Cross-browser testing - [ ] Mobile responsive testing ### Phase 8: Deployment - [ ] Run migrations on staging - [ ] Deploy to staging - [ ] UAT (User Acceptance Testing) - [ ] Deploy to production - [ ] Monitor and verify --- ## 📝 Migration Script ```php <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateAppConnectionsTable extends Migration { public function up() { Schema::create('app_connections', function (Blueprint $table) { $table->uuid('id')->primary(); $table->uuid('corporate_id'); $table->uuid('branch_id'); $table->uuid('merchant_id'); $table->uuid('product_id')->nullable(); $table->string('property_id', 20)->nullable(); // Numeric only, max 20 digits $table->unsignedBigInteger('access_token_id')->nullable(); $table->enum('status', ['active', 'inactive'])->default('active'); $table->timestamps(); $table->softDeletes(); $table->foreign('corporate_id')->references('id')->on('corporates')->onDelete('cascade'); $table->foreign('branch_id')->references('id')->on('branches')->onDelete('cascade'); $table->foreign('merchant_id')->references('id')->on('merchants')->onDelete('cascade'); $table->foreign('product_id')->references('id')->on('products')->onDelete('set null'); $table->foreign('access_token_id')->references('id')->on('personal_access_tokens')->onDelete('set null'); $table->index('corporate_id'); $table->index('branch_id'); $table->index('merchant_id'); $table->index('property_id'); $table->index('status'); $table->index('deleted_at'); $table->unique(['branch_id', 'merchant_id'], 'unique_branch_merchant'); }); } public function down() { Schema::dropIfExists('app_connections'); } } ``` --- ## 📚 Summary ### Key Features ✅ 3-step wizard untuk setup connection ✅ Auto-generate product dan access token ✅ Support legacy connections (commerce_site) ✅ Manage connections (edit, regenerate, delete) ✅ Corporate level authorization ✅ Tenant isolation ✅ Backward compatibility ### Technical Highlights ✅ Clean API design dengan RESTful conventions ✅ Domain-driven architecture ✅ Policy-based authorization ✅ Sanctum token management ✅ Soft delete support ✅ Auto-generated products with price 0 ✅ Property ID + config untuk construct auth URL ### Business Value ✅ Simplified connection setup ✅ Centralized management ✅ Better tracking dan audit trail ✅ Token regeneration capability ✅ Gradual migration dari legacy system ✅ No breaking changes untuk existing integrations --- **Document Version:** 2.0 **Status:** Ready for Development **Last Updated:** 24 November 2025 --- ## 🔄 Complete API Flow Examples ### Example 1: Create Connection with Existing Branch/Merchant **Step 1: User opens wizard** ``` Frontend: User clicks "Create Connection" ``` **Step 2: Get available branches** ```http GET /api/connection-apps/available-branches Authorization: Bearer {token} ``` **Response:** ```json { "success": true, "data": [ { "branch_id": "uuid-branch-1", "branch_name": "Branch Jakarta", "merchants": [ { "merchant_id": "uuid-merchant-1", "merchant_name": "Merchant A", "has_connection": false } ] } ] } ``` **Step 3: User selects branch & merchant** ```http POST /api/connection-apps/setup/step-1 Authorization: Bearer {token} Content-Type: application/json { "setup_type": "existing", "branch_id": "uuid-branch-1", "merchant_id": "uuid-merchant-1" } ``` **Response:** ```json { "success": true, "message": "Step 1 completed successfully", "data": { "branch_id": "uuid-branch-1", "branch_name": "Branch Jakarta", "merchant_id": "uuid-merchant-1", "merchant_name": "Merchant A", "next_step": 2 } } ``` **Step 4: User inputs property ID** ```http POST /api/connection-apps/setup/step-2 Authorization: Bearer {token} Content-Type: application/json { "branch_id": "uuid-branch-1", "merchant_id": "uuid-merchant-1", "property_id": "12345", "token_name": "Connection Token - Branch Jakarta - 2025-11-24" } ``` **Response:** ```json { "success": true, "message": "Connection created successfully", "data": { "connection_id": "uuid-connection-1", "product_id": "uuid-product-123", "product_name": "Connection Product - 12345", "access_token": "1|abc123def456ghi789jkl012mno345pqr678stu901vwx234yz", "next_step": 3 } } ``` **Step 5: Display credentials** ```http GET /api/connection-apps/uuid-connection-1/credentials Authorization: Bearer {token} ``` **Response:** ```json { "success": true, "data": { "access_token": "1|abc123def456ghi789jkl012mno345pqr678stu901vwx234yz", "property_id": "12345", "accommodation_id": "uuid-product-123", "x_tenant_domain": "tenant.membership.com" } } ``` --- ### Example 2: Create Connection with New Branch/Merchant **Step 1: User opens wizard and selects "Create New"** **Step 2: User fills branch & merchant forms** ```http POST /api/connection-apps/setup/step-1 Authorization: Bearer {token} Content-Type: application/json { "setup_type": "new", "branch": { "code": "BDG001", "name": "Branch Bandung", "address": "Jl. Asia Afrika No. 1", "state": "West Java", "country": "Indonesia", "phone": "+62221234567", "fax": "+62221234568", "website": "https://bandung.example.com", "city": "Bandung", "postcode": "40111", "logo": null }, "merchant": { "name": "Merchant Bandung", "code": "MRC001", "address": "Jl. Asia Afrika No. 1", "state": "West Java", "country": "Indonesia", "phone": "+62221234567", "fax": "+62221234568", "website": "https://merchant-bandung.example.com", "city": "Bandung", "postcode": "40111", "logo": null } } ``` **Response:** ```json { "success": true, "message": "Step 1 completed successfully", "data": { "branch_id": "uuid-new-branch", "branch_name": "Branch Bandung", "branch_code": "BDG001", "merchant_id": "uuid-new-merchant", "merchant_name": "Merchant Bandung", "merchant_code": "MRC001", "next_step": 2 } } ``` **Step 3: Continue with Step 2 (same as Example 1)** --- ### Example 3: Update Connection Property ID **Request:** ```http PUT /api/connection-apps/uuid-connection-1 Authorization: Bearer {token} Content-Type: application/json { "property_id": "67890" } ``` **Response:** ```json { "success": true, "message": "Connection updated successfully", "data": { "id": "uuid-connection-1", "property_id": "67890", "product_name": "Connection Product - 67890", "status": "active", "updated_at": "2025-11-24T14:30:00Z" } } ``` **Note:** Product name automatically updated from "Connection Product - 12345" to "Connection Product - 67890" --- ### Example 4: Regenerate Access Token **Request:** ```http POST /api/connection-apps/uuid-connection-1/regenerate-token Authorization: Bearer {token} Content-Type: application/json { "token_name": "Regenerated Token - Branch Jakarta - 2025-11-24" } ``` **Response:** ```json { "success": true, "message": "Access token regenerated successfully", "data": { "connection_id": "uuid-connection-1", "access_token": "2|xyz789abc456def123ghi890jkl567mno234pqr901stu678vwx", "token_name": "Regenerated Token - Branch Jakarta - 2025-11-24", "regenerated_at": "2025-11-24T15:00:00Z" }, "note": "Save this token securely. It will not be shown again." } ``` --- ### Example 5: Delete Connection **Request:** ```http DELETE /api/connection-apps/uuid-connection-1 Authorization: Bearer {token} ``` **Response:** ```json { "success": true, "message": "Connection deleted successfully" } ``` **What happens:** 1. Connection soft deleted (deleted_at set) 2. Access token revoked 3. Product soft deleted 4. Status set to inactive --- ### Example 6: View All Connections **Request:** ```http GET /api/connection-apps Authorization: Bearer {token} ``` **Response:** ```json { "success": true, "message": "Connections retrieved successfully", "data": [ { "branch_id": "uuid-branch-1", "branch_name": "Branch Jakarta", "branch_code": "JKT001", "connection_status": "connected", "connection_type": "new", "connection": { "id": "uuid-connection-1", "merchant_id": "uuid-merchant-1", "merchant_name": "Merchant A", "property_id": "PROP-12345", "accommodation_id": "uuid-product-123", "product_name": "Connection Product - PROP-12345", "status": "active", "created_at": "2025-11-24T10:00:00Z" } }, { "branch_id": "uuid-branch-2", "branch_name": "Branch Bandung", "branch_code": "BDG001", "connection_status": "connected", "connection_type": "legacy", "connection": { "merchant_id": "uuid-merchant-2", "merchant_name": "Merchant B", "commerce_site": "https://booking-engine.com/property/456" } }, { "branch_id": "uuid-branch-3", "branch_name": "Branch Surabaya", "branch_code": "SBY001", "connection_status": "not_connected", "connection_type": "none", "connection": null } ] } ``` --- ## 🚨 Error Handling Guide ### Common Error Scenarios #### 1. Duplicate Connection **Scenario:** User tries to create connection for branch+merchant that already has one **Request:** ```http POST /api/connection-apps/setup/step-2 { "branch_id": "uuid-branch-1", "merchant_id": "uuid-merchant-1", "property_id": "12345", "token_name": "Token" } ``` **Response (409):** ```json { "success": false, "message": "Connection already exists for this branch and merchant" } ``` **Frontend Action:** Show error message, suggest editing existing connection --- #### 2. Invalid Branch/Merchant Relationship **Scenario:** Merchant doesn't belong to selected branch **Request:** ```http POST /api/connection-apps/setup/step-2 { "branch_id": "uuid-branch-1", "merchant_id": "uuid-merchant-from-different-branch", "property_id": "12345", "token_name": "Token" } ``` **Response (403):** ```json { "success": false, "message": "Merchant does not belong to the selected branch" } ``` **Frontend Action:** Show error, reset merchant selection --- #### 3. Unauthorized Access **Scenario:** User tries to access connection from different corporate **Request:** ```http GET /api/connection-apps/uuid-connection-from-other-corporate ``` **Response (403):** ```json { "success": false, "message": "Unauthorized to access this connection" } ``` **Frontend Action:** Redirect to list page, show error message --- #### 4. Validation Errors **Scenario:** Missing required fields **Request:** ```http POST /api/connection-apps/setup/step-2 { "branch_id": "uuid-branch-1", "merchant_id": "uuid-merchant-1" } ``` **Response (422):** ```json { "success": false, "message": "Validation failed", "errors": { "property_id": ["The property id field is required."], "token_name": ["The token name field is required."] } } ``` **Frontend Action:** Display validation errors next to respective fields --- #### 5. Resource Not Found **Scenario:** Connection ID doesn't exist **Request:** ```http GET /api/connection-apps/invalid-uuid ``` **Response (404):** ```json { "success": false, "message": "Connection not found" } ``` **Frontend Action:** Redirect to list page, show "Connection not found" message --- #### 6. Server Error **Scenario:** Database error or unexpected exception **Request:** ```http POST /api/connection-apps/setup/step-2 ``` **Response (500):** ```json { "success": false, "message": "Failed to create connection" } ``` **Frontend Action:** Show generic error message, suggest retry or contact support --- ## 📋 Frontend Error Handling Implementation ### Error Handler Service ```javascript // errorHandler.js export const handleApiError = (error, context) => { if (!error.response) { return { title: 'Network Error', message: 'Unable to connect to server. Please check your connection.', type: 'error' } } const { status, data } = error.response switch (status) { case 401: // Redirect to login window.location.href = '/login' return null case 403: return { title: 'Access Denied', message: data.message || 'You do not have permission to perform this action.', type: 'error' } case 404: return { title: 'Not Found', message: data.message || 'The requested resource was not found.', type: 'error' } case 409: return { title: 'Conflict', message: data.message || 'This resource already exists.', type: 'warning' } case 422: return { title: 'Validation Error', message: 'Please check your input and try again.', errors: data.errors, type: 'warning' } case 500: return { title: 'Server Error', message: data.message || 'An unexpected error occurred. Please try again later.', type: 'error' } default: return { title: 'Error', message: data.message || 'An error occurred.', type: 'error' } } } ``` ### Usage in Components ```javascript // ConnectionSetupWizard.vue import { handleApiError } from '@/utils/errorHandler' import connectionAppsService from '@/services/connectionAppsService' export default { methods: { async createConnection() { try { this.loading = true const response = await connectionAppsService.setupStep2(this.formData) // Success this.showSuccess('Connection created successfully!') this.goToStep3(response.data) } catch (error) { const errorInfo = handleApiError(error, 'create_connection') if (errorInfo) { if (errorInfo.errors) { // Display validation errors this.validationErrors = errorInfo.errors } else { // Display general error this.showError(errorInfo.title, errorInfo.message) } } } finally { this.loading = false } } } } ``` --- --- ## 📝 Additional Notes ### Property ID Format **Important:** Property ID must be **numeric only** (no letters or special characters) **Valid Examples:** - `12345` - `67890` - `123` - `9999999999` **Invalid Examples:** - ❌ `PROP-12345` (contains letters and dash) - ❌ `12345-A` (contains letter) - ❌ `12.345` (contains dot) - ❌ `12,345` (contains comma) **Validation:** - Type: Numeric - Min: 1 digit - Max: 20 digits - No letters, spaces, or special characters ### Headers Required All API requests must include: ``` Authorization: Bearer {token} X-Tenant-Domain: {tenant-subdomain} ``` **Example:** ``` Authorization: Bearer 1|abc123def456... X-Tenant-Domain: tenant-demo.membership.com ``` **Note:** X-Tenant-Domain is used for multi-tenant context and routing. --- **Document Version:** 2.0 **Status:** Ready for Development **Last Updated:** 24 November 2025 **Revision:** Added X-Tenant-Domain header, Changed property_id to numeric only