---
# System prepended metadata

title: Connection Apps - Technical Specification

---

# 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
