# 7. PowerShell CSV Export (MVP Solution)

### Overview

Since Graph API does not expose emergency locations or policy listings, the MVP uses a **PowerShell export script** that users run periodically to generate CSV reference files. The CLI reads these files for validation, display, and tab completion.

**Key Principle:** Locations and policies are admin-configured and rarely change. A static export is acceptable for MVP.

### Architecture

```
┌─────────────────────────────────────────────────────────┐
│  One-time / Periodic Setup                              │
│                                                         │
│  User runs: Export-TeamsPhoneData.ps1                   │
│                                                         │
│  PowerShell Script:                                     │
│  ├── Connect-MicrosoftTeams (cert/token auth)           │
│  ├── Get-CsOnlineLisCivicAddress  → locations.csv       │
│  ├── Get-CsOnlineLisLocation      → locations.csv       │
│  ├── Get-CsTeamsCallingPolicy     → policies.csv        │
│  ├── Get-CsOnlineVoicemailPolicy  → policies.csv        │
│  ├── Get-CsCallingLineIdentity    → policies.csv        │
│  ├── Get-CsTenantDialPlan         → policies.csv        │
│  ├── Get-CsOnlineVoiceRoutingPolicy → policies.csv      │
│  ├── Get-CsTeamsEmergencyCallingPolicy → policies.csv   │
│  └── Get-CsTeamsEmergencyCallRoutingPolicy → policies   │
└──────────────────────┬──────────────────────────────────┘
                       ↓
                ~/.teams-phone/cache/
                ├── locations.csv
                ├── policies.csv
                └── export-metadata.json
                       ↓
┌──────────────────────┴──────────────────────────────────┐
│  CLI Runtime (Graph API only)                           │
│                                                         │
│  • Reads CSVs for validation and display                │
│  • Warns if files missing or stale                      │
│  • Still works without CSVs (degraded mode)             │
└─────────────────────────────────────────────────────────┘
```

### PowerShell Cmdlet Output Schemas - VERIFIED

The following schemas were verified via Microsoft documentation research (2026-01-27).

#### Get-CsOnlineLisLocation Output

| Property | Type | Description |
|----------|------|-------------|
| `LocationId` | GUID | Unique location identifier (used in number assignment) |
| `CivicAddressId` | GUID | Parent civic address ID |
| `Location` | String | Location name (e.g., "3rd Floor Cafe", "Main Lobby") |
| `Description` | String | Administrator-defined description |
| `City` | String | City name |
| `CompanyName` | String | Organization name |
| `CountryOrRegion` | String | Country code (e.g., "US") |
| `StateOrProvince` | String | State/province code |
| `PostalCode` | String | Postal/ZIP code |
| `HouseNumber` | String | Street number |
| `HouseNumberSuffix` | String | Street number suffix (e.g., "A", "B") |
| `PreDirectional` | String | Directional prefix (e.g., "NE") |
| `StreetName` | String | Street name |
| `StreetSuffix` | String | Street type (e.g., "Ave", "St", "Blvd") |
| `PostDirectional` | String | Directional suffix (e.g., "NE") |
| `Elin` | String | Emergency Location Identification Number |
| `Latitude` | Decimal | Geographic latitude |
| `Longitude` | Decimal | Geographic longitude |

#### Get-CsOnlineLisCivicAddress Output

| Property | Type | Description |
|----------|------|-------------|
| `CivicAddressId` | GUID | Unique civic address identifier |
| `CompanyName` | String | Organization name (required) |
| `Description` | String | Administrator-defined description |
| `HouseNumber` | String | Street number |
| `HouseNumberSuffix` | String | e.g., "A", "B" |
| `PreDirectional` | String | e.g., "NE" |
| `StreetName` | String | Street name |
| `StreetSuffix` | String | e.g., "Ave", "St" |
| `PostDirectional` | String | e.g., "NE" |
| `City` | String | City name |
| `CityAlias` | String | City alias |
| `StateOrProvince` | String | State/province code |
| `PostalCode` | String | Postal/ZIP code |
| `CountryOrRegion` | String | Country code (required) |
| `Latitude` | Decimal | Geographic latitude (required) |
| `Longitude` | Decimal | Geographic longitude (required) |
| `DefaultLocationId` | GUID | Default location for this address |

#### Policy Cmdlet Common Output Pattern

All policy cmdlets return objects with these common properties:

| Property | Type | Description |
|----------|------|-------------|
| `Identity` | String | Policy name (e.g., "Global", "AllowCalling") |
| `Description` | String | Administrator-defined description |

**Policy-Specific Additional Properties:**

| Cmdlet | Key Additional Properties |
|--------|---------------------------|
| `Get-CsTeamsCallingPolicy` | AllowPrivateCalling, AllowVoicemail, AllowCallForwardingToPhone, AllowCallGroups, AllowDelegation, AllowCallRedirect, BusyOnBusyEnabledType, etc. |
| `Get-CsOnlineVoicemailPolicy`* | EnableTranscription, EnableTranscriptionProfanityMasking, EnableTranscriptionTranslation, MaximumRecordingLength, etc. |
| `Get-CsCallingLineIdentity` | CallingIDSubstitute, EnableUserOverride, BlockIncomingPstnCallerID, ResourceAccount, CompanyName |
| `Get-CsTenantDialPlan` | SimpleName, NormalizationRules (collection) |
| `Get-CsOnlineVoiceRoutingPolicy` | OnlinePstnUsages (collection), RouteType |
| `Get-CsTeamsEmergencyCallingPolicy` | NotificationGroup, NotificationDialOutNumber, NotificationMode, EnhancedEmergencyServiceDisclaimer |
| `Get-CsTeamsEmergencyCallRoutingPolicy` | EmergencyNumbers (collection), AllowEnhancedEmergencyServices |

*Note: The voicemail policy cmdlet is `Get-CsOnlineVoicemailPolicy`, NOT `Get-CsTeamsVoicemailPolicy`. However, the Graph API uses `TeamsVoicemailPolicy` as the policyType value.

### CSV Schemas

#### 7.1 locations.csv

| Column | Type | PowerShell Source | Description |
|--------|------|-------------------|-------------|
| `locationId` | String (GUID) | `LocationId` from Get-CsOnlineLisLocation | Unique location identifier (used in number assignment) |
| `civicAddressId` | String (GUID) | `CivicAddressId` from Get-CsOnlineLisLocation | Parent civic address ID |
| `description` | String | `Location` or `Description` from Get-CsOnlineLisLocation | Location name/description |
| `companyName` | String | `CompanyName` from Get-CsOnlineLisCivicAddress | Company name |
| `address` | String | Computed: `HouseNumber` + `StreetName` + `StreetSuffix` | Full street address |
| `city` | String | `City` from Get-CsOnlineLisLocation | City name |
| `stateOrProvince` | String | `StateOrProvince` from Get-CsOnlineLisLocation | State/province code |
| `postalCode` | String | `PostalCode` from Get-CsOnlineLisLocation | Postal/ZIP code |
| `countryOrRegion` | String | `CountryOrRegion` from Get-CsOnlineLisLocation | Country code (e.g., "US") |
| `isDefault` | Boolean | Compare `LocationId` == `DefaultLocationId` from CivicAddress | Whether this is the default location |

**Example:**
```csv
locationId,civicAddressId,description,companyName,address,city,stateOrProvince,postalCode,countryOrRegion,isDefault
93cb8a70-b4af-41df-9928-d07607e21776,a1b2c3d4-...,Main Office - Floor 3,Contoso Ltd,123 Main St,Seattle,WA,98101,US,true
f47ac10b-58cc-4372-a567-0e02b2c3d479,a1b2c3d4-...,Branch Office,Contoso Ltd,456 Oak Ave,Portland,OR,97201,US,false
```

#### 7.2 policies.csv

| Column | Type | Source | Description |
|--------|------|--------|-------------|
| `policyType` | String | Computed | Policy type identifier (see below) |
| `policyName` | String | Identity | Policy name (what users see/use) |
| `policyId` | String | Computed | Policy ID for API calls (may equal policyName) |
| `description` | String | Description | Policy description |
| `isGlobal` | Boolean | Identity = "Global" | Whether this is the global/default policy |

**Policy Types (VERIFIED):**
| policyType Value | PowerShell Cmdlet | Graph policyType | Batch Assign |
|------------------|-------------------|------------------|--------------|
| `TeamsCallingPolicy` | Get-CsTeamsCallingPolicy | TeamsCallingPolicy | ✓ |
| `TeamsVoicemailPolicy` | Get-CsOnlineVoicemailPolicy* | TeamsVoicemailPolicy | ✗ |
| `CallingLineIdentity` | Get-CsCallingLineIdentity | CallingLineIdentity | ✓ |
| `TenantDialPlan` | Get-CsTenantDialPlan | TenantDialPlan | ✓ |
| `OnlineVoiceRoutingPolicy` | Get-CsOnlineVoiceRoutingPolicy | OnlineVoiceRoutingPolicy | ✓ |
| `TeamsEmergencyCallingPolicy` | Get-CsTeamsEmergencyCallingPolicy | TeamsEmergencyCallingPolicy | ✓ |
| `TeamsEmergencyCallRoutingPolicy` | Get-CsTeamsEmergencyCallRoutingPolicy | TeamsEmergencyCallRoutingPolicy | ✓ |

*Note: PowerShell cmdlet is `Get-CsOnlineVoicemailPolicy` but Graph API uses `TeamsVoicemailPolicy` as the type.

**Example:**
```csv
policyType,policyName,policyId,description,isGlobal
TeamsCallingPolicy,Global,Global,Default calling policy,true
TeamsCallingPolicy,AllowCalling,AllowCalling,Allows all calling features,false
TeamsCallingPolicy,RestrictedCalling,RestrictedCalling,Restricted calling for contractors,false
TeamsVoicemailPolicy,Global,Global,Default voicemail policy,true
TenantDialPlan,US-Seattle,US-Seattle,Seattle normalization rules,false
```

#### 7.3 export-metadata.json

```json
{
  "exportedAt": "2026-01-27T15:30:00Z",
  "exportedBy": "admin@contoso.com",
  "tenantId": "12345678-1234-1234-1234-123456789012",
  "tenantName": "Contoso Ltd",
  "scriptVersion": "1.0.0",
  "counts": {
    "locations": 15,
    "policies": 23
  }
}
```

### CLI Behavior

| Scenario | CLI Behavior |
|----------|--------------|
| CSVs present and fresh (<30 days) | Full validation, tab completion, location names in output |
| CSVs present but stale (>30 days) | Warning: "Reference data is X days old. Run Export-TeamsPhoneData.ps1 to refresh." |
| CSVs missing | Warning on first use: "Location/policy reference data not found. Commands will work but cannot validate IDs. Run Export-TeamsPhoneData.ps1 to enable validation." |
| locationId not in CSV | Warning: "Location ID not found in local cache. Proceeding anyway - ensure ID is valid." |

### PowerShell Authentication - VERIFIED

**Documentation:** [Application-based authentication in Teams PowerShell](https://learn.microsoft.com/en-us/microsoftteams/teams-powershell-application-authentication)

The MicrosoftTeams PowerShell module supports three authentication methods for unattended/service principal use:

| Method | Requirement | Security | Introduced |
|--------|-------------|----------|------------|
| **CertificateThumbprint** | Cert in Windows user cert store | High | v4.7.1-preview |
| **Certificate** (object) | Load cert from file/KeyVault | High | v4.9.2-preview |
| **AccessTokens** | Pre-obtained Graph + Teams tokens | Medium | v4.7.1-preview |

**Certificate Thumbprint (Windows only):**
```powershell
Connect-MicrosoftTeams `
  -CertificateThumbprint "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" `
  -ApplicationId "00000000-0000-0000-0000-000000000000" `
  -TenantId "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"
```
- Certificate must be installed in the **user certificate store** on Windows
- User running the script needs access to that cert store

**Certificate Object (Cross-platform):**
```powershell
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("C:\cert.pfx", $password)
Connect-MicrosoftTeams -Certificate $cert -ApplicationId $appId -TenantId $tenantId
```
- Certificate can be loaded from file or Azure KeyVault
- Works on Windows, Linux, macOS

**Access Tokens (Client Secret flow):**
```powershell
# Obtain tokens using client secret
$body = @{
  Grant_Type    = "client_credentials"
  Scope         = "https://graph.microsoft.com/.default"
  Client_Id     = $ApplicationId
  Client_Secret = $ClientSecret
}
$graphToken = (Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" -Method POST -Body $body).access_token

# Similar for Teams token with scope "48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default"
Connect-MicrosoftTeams -AccessTokens @("$graphToken", "$teamsToken")
```
- Less secure than certificates but easier to set up
- Works cross-platform

### Authorization Requirements - VERIFIED

**Important:** Uses **Entra ID roles**, NOT API permissions.

| Requirement | Details |
|-------------|---------|
| **App Registration** | Required in Microsoft Entra ID |
| **Role Assignment** | Assign **Teams Administrator** role to the service principal |
| **API Permissions** | Do NOT add "Skype and Teams Tenant Admin API" permission (causes failures) |
| **New Permissions (Sept 2025)** | `RoleManagement.Read.Directory` + `GroupMember.Read.All` required |

**Role Assignment Steps:**
1. Azure Portal → Microsoft Entra ID → Roles and administrators
2. Find "Teams Administrator" role
3. Add assignment → Select your app registration's service principal

### Cross-Platform Support - VERIFIED

| Platform | PowerShell Version | Status |
|----------|-------------------|--------|
| Windows | PowerShell 5.1 | **Recommended** - Best experience |
| Windows | PowerShell 7.2+ | Supported with known issues |
| Linux | PowerShell 7.2+ (`pwsh`) | Supported with known issues |
| macOS | PowerShell 7.2+ (`pwsh`) | Supported with known issues |

**Recommendation:** Use Windows PowerShell 5.1 for production scripts. PowerShell Core has known bugs with the Teams module.

### Unsupported Cmdlets (App Auth)

The following cmdlets do NOT work with application-based authentication:
- `New-Team`
- `Set-CsOnlineApplicationInstance`
- `*PolicyPackage*`
- `*-CsTeamsShiftsConnection*`
- `*-CsBatchTeamsDeployment*`

**Good news:** All `Get-Cs*` cmdlets for locations and policies ARE supported.

### Script Requirements

The export script (`Export-TeamsPhoneData.ps1`) must:

1. **Support service principal auth** - Certificate or access token method
2. **Accept parameters:**
   - `-TenantId` - Target tenant
   - `-ApplicationId` - App registration ID
   - `-CertificateThumbprint` (Windows) or `-CertificatePath` (cross-platform)
   - `-OutputPath` - Where to write CSVs (default: `~/.teams-phone/cache/`)
3. **Handle pagination** for large tenants
4. **Generate metadata.json** with export timestamp

**Example usage (Certificate Thumbprint - Windows):**
```powershell
./Export-TeamsPhoneData.ps1 `
  -TenantId "12345678-..." `
  -ApplicationId "87654321-..." `
  -CertificateThumbprint "ABC123..." `
  -OutputPath "~/.teams-phone/cache/"
```

**Example usage (Certificate File - Cross-platform):**
```powershell
./Export-TeamsPhoneData.ps1 `
  -TenantId "12345678-..." `
  -ApplicationId "87654321-..." `
  -CertificatePath "/path/to/cert.pfx" `
  -CertificatePassword (ConvertTo-SecureString "password" -AsPlainText -Force) `
  -OutputPath "~/.teams-phone/cache/"
```

### Future Enhancement

Post-MVP, if Microsoft adds Graph API endpoints for locations/policies, the CLI can:
1. Prefer real-time Graph calls
2. Fall back to CSV if Graph fails
3. Deprecate CSV approach

---

