# 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