# BIPO HRMS API Integration

## Basic Connector

**Source**: `prototype/src/talentverse/nodes/bipo_connector.py`
**Base URL**: `https://ap9.bipocloud.com/IMC`
**Protocol**: HTTP/requests (sync)
**Auth**: OAuth2 (username + password + client credentials)

### Environment Variables

```env
BIPO_USERNAME=
BIPO_PASSWORD=
BIPO_CLIENT_ID=
BIPO_CLIENT_SECRET=
```

### OAuth2 Token Exchange

```python
def _get_access_token(self) -> str:
    url = f"{self.base_url}/oauth2/webapi/token"
    data = {
        "UserName": self.username,
        "Password": self.password,
        "grant_type": "password",
        "client_id": self.client_id,
        "client_secret": self.client_secret,
    }
    response = requests.post(url, data=data, timeout=120)
    if response.status_code == 200:
        token_data = response.json()
        self._access_token = token_data["access_token"]
        expires_in = token_data.get("expires_in", 3600)
        # Set token expiry
        return self._access_token
    else:
        raise Exception(f"Failed to get access token: {response.text}")
```

### API Endpoints

| Endpoint | Method | Purpose | View/Interface |
|----------|--------|---------|----------------|
| `/api2/BIPOExport/GetView` | POST | Get employee data | `BIPO-AZURE-EMP` |
| `/api2/BIPOExport/GetListItem` | POST | Get company list | `BIPO-CO` |
| `/api2/BIPOExport/GetListItem` | POST | Get department list | `BIPO-DP` |
| `/api2/BIPOExport/GetListItem` | POST | Get designation list | `BIPO-DS` |
| `/api2/BIPOExport/GetList` | POST | Get external training | `BIPO-ETO` |
| `/api2/BIPOExport/GetList` | POST | Get internal training | `BIPO-ITO` |
| `/api2/BIPOExport/GetList` | POST | Get course templates | `BIPO-CT` |
| `/api2/BIPOExport/GetList` | POST | Get course data | `BIPO-COU` |

### Employee Data Request

```python
def _get_employee_data(self, update_from="1900-01-01", update_to=None, is_active=True):
    data = {
        "ViewName": "BIPO-AZURE-EMP",
        "updateonFrom": update_from,
        "updateonTo": update_to or datetime.now().strftime("%Y-%m-%d"),
        "isActive": str(is_active).lower(),
    }
    url = f"{self.base_url}/api2/BIPOExport/GetView"
    return self._make_api_request(endpoint=url, data=data)
```

### Employee Data Fields (from BIPO-AZURE-EMP)

```python
{
    "EmployeeID": "ES23001",
    "FullName": "Kenneth Siow Hui Tan",
    "EmployeeEmail": "kenneth.tan@company.com",
    "JobTitle": "Senior Manager",
    "DepartmentName": "Technology",
    "CompanyName": "TPC Singapore",
    "JobGradeCode": "M3",
    "JobGradeName": "Manager Grade 3",
    "ManagerCode": "ES14001",
    "ManagerName": "John Smith",
    "DateJoin": "2023-01-15",
    "isActive": "true",
    "LoginID": "kenneth.tan",
    "Gender": "Male",
    "DateOfBirth": "1990-05-20",
    "EmploymentTypeName": "Full-Time",
    "DepartmentCode": "TECH",
    "CompanyCode": "SG01",
    "CostCentreCode": "CC001",
    "CostCentreName": "Tech Operations",
    "LastDayofService": "",
    "UpdateOn": "2025-01-15",
    "EffectiveFrom": "2023-01-15",
    "NickName": "Kenneth"
}
```

### Caching System

```python
# File-based cache with TTL
cache_dir = Path.home() / ".talentverse" / "cache" / "bipo"
cache_ttl = 3600  # 1 hour

def _generate_cache_key(self, endpoint, data):
    request_str = f"{endpoint}:{json.dumps(data, sort_keys=True)}"
    return hashlib.md5(request_str.encode()).hexdigest()

def _save_to_cache(self, cache_key, data):
    cache_data = {"data": data, "timestamp": time.time(), "expires_at": time.time() + self.cache_ttl}
    with open(cache_path, "w") as f:
        json.dump(cache_data, f)

def _get_from_cache(self, cache_key):
    with open(cache_path, "r") as f:
        cache_data = json.load(f)
    if time.time() > cache_data.get("expires_at", 0):
        return False, None
    return True, cache_data["data"]
```

---

## Targeted Retrieval

**Source**: `prototype/src/talentverse/nodes/connectors/bipo_targeted_retrieval.py`
**Protocol**: Async HTTP (aiohttp)
**Purpose**: High-performance single employee retrieval with retry logic

### Key Features
- Connection pooling (`TCPConnector(limit=10, ttl_dns_cache=300)`)
- Exponential backoff retry (3 attempts)
- Multi-strategy name search (direct, filtered list, paginated)
- Fuzzy name matching for Asian name variations
- Concurrent reference data enrichment (`asyncio.gather`)

### Async Context Manager Pattern

```python
class TargetedBIPORetriever:
    async def __aenter__(self):
        self._session = ClientSession(
            timeout=ClientTimeout(total=45),
            connector=aiohttp.TCPConnector(limit=10, ttl_dns_cache=300)
        )
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if self._session:
            await self._session.close()

# Usage
async with TargetedBIPORetriever() as retriever:
    employee = await retriever.get_employee_by_name("Kenneth Tan")
```

### Auth Token with Retry

```python
async def _get_auth_token(self) -> str:
    auth_url = f"{self.base_url}/auth/token"
    auth_data = {
        "grant_type": "client_credentials",
        "client_id": self.client_id,
        "client_secret": self.client_secret,
        "username": self.username,
        "password": self.password
    }
    for attempt in range(self.max_retries):
        try:
            async with self._session.post(auth_url, data=auth_data,
                timeout=ClientTimeout(total=15)) as response:
                if response.status == 200:
                    token_data = await response.json()
                    self._access_token = token_data.get('access_token')
                    return self._access_token
        except asyncio.TimeoutError:
            pass
        # Exponential backoff
        delay = self.retry_delay_base ** (attempt + 1)
        await asyncio.sleep(delay)
```

### Concurrent Reference Data Enrichment

```python
reference_tasks = [
    self._get_department_info(auth_token, employee.get('DepartmentCode')),
    self._get_designation_info(auth_token, employee.get('DesignationCode')),
    self._get_company_info(auth_token, employee.get('CompanyCode'))
]
department_info, designation_info, company_info = await asyncio.gather(
    *reference_tasks, return_exceptions=True
)
```

### Name Matching (Fuzzy)

```python
def _names_match(self, name1: str, name2: str) -> bool:
    n1 = name1.lower().strip()
    n2 = name2.lower().strip()
    if n1 == n2:
        return True
    words1 = set(n1.split())
    words2 = set(n2.split())
    shorter_words = words1 if len(words1) <= len(words2) else words2
    longer_words = words2 if len(words1) <= len(words2) else words1
    return shorter_words.issubset(longer_words)
```
