#!/usr/bin/env pwsh # # Business Central Data Export via BC API v2.0 # Authenticates to Azure AD and extracts critical business data as JSON # param( [Parameter(Mandatory=$true)] [string]$OutputPath ) # Get configuration from environment variables $tenantId = $env:AZURE_TENANT_ID $clientId = $env:AZURE_CLIENT_ID $clientSecret = $env:AZURE_CLIENT_SECRET $environmentName = $env:BC_ENVIRONMENT_NAME $bcCompanyName = $env:BC_COMPANY_NAME # optional: filter to specific company $baseUrl = "https://api.businesscentral.dynamics.com/v2.0/$tenantId/$environmentName/api/v2.0" # Entities to extract - critical business data $entities = @( "accounts", "customers", "vendors", "items", "salesInvoices", "salesInvoiceLines", "salesOrders", "salesOrderLines", "salesCreditMemos", "salesCreditMemoLines", "purchaseInvoices", "purchaseInvoiceLines", "purchaseOrders", "purchaseOrderLines", "generalLedgerEntries", "bankAccounts", "employees", "dimensions", "dimensionValues", "currencies", "paymentTerms", "paymentMethods", "journals", "countriesRegions" ) function Write-Log { param([string]$Message, [string]$Level = "INFO") $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "[$timestamp] [$Level] $Message" } function Get-AzureADToken { param( [string]$TenantId, [string]$ClientId, [string]$ClientSecret ) Write-Log "Authenticating to Azure AD..." $tokenUrl = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" $body = @{ client_id = $ClientId client_secret = $ClientSecret scope = "https://api.businesscentral.dynamics.com/.default" grant_type = "client_credentials" } try { $response = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body -ContentType "application/x-www-form-urlencoded" Write-Log "Successfully authenticated to Azure AD" return $response.access_token } catch { Write-Log "Failed to authenticate: $_" "ERROR" throw } } function Get-BCData { param( [string]$Token, [string]$Url ) $headers = @{ "Authorization" = "Bearer $Token" "Accept" = "application/json" } $allRecords = @() $currentUrl = $Url while ($currentUrl) { try { $response = Invoke-RestMethod -Uri $currentUrl -Method Get -Headers $headers } catch { Write-Log "API request failed for $currentUrl : $_" "ERROR" throw } if ($response.value) { $allRecords += $response.value } # Handle OData pagination $currentUrl = $response.'@odata.nextLink' } return $allRecords } function Get-Companies { param([string]$Token) Write-Log "Fetching companies..." $companiesUrl = "$baseUrl/companies" $companies = Get-BCData -Token $Token -Url $companiesUrl Write-Log "Found $($companies.Count) company/companies" return $companies } function Export-EntityData { param( [string]$Token, [string]$CompanyId, [string]$CompanyName, [string]$EntityName, [string]$OutputDir ) $entityUrl = "$baseUrl/companies($CompanyId)/$EntityName" Write-Log " Exporting $EntityName..." try { $data = Get-BCData -Token $Token -Url $entityUrl $count = 0 if ($data) { $count = $data.Count } $outputFile = Join-Path $OutputDir "$EntityName.json" $data | ConvertTo-Json -Depth 10 | Out-File -FilePath $outputFile -Encoding utf8 Write-Log " $EntityName : $count records" return $count } catch { Write-Log " Failed to export ${EntityName}: $_" "WARN" # Write empty array so downstream knows it was attempted $outputFile = Join-Path $OutputDir "$EntityName.json" "[]" | Out-File -FilePath $outputFile -Encoding utf8 return 0 } } # Main execution try { Write-Log "=========================================" Write-Log "BC Data Export Script (API v2.0)" Write-Log "=========================================" Write-Log "Environment: $environmentName" Write-Log "Output Path: $OutputPath" Write-Log "Entities to extract: $($entities.Count)" # Create output directory $exportDir = $OutputPath if (-not (Test-Path $exportDir)) { New-Item -ItemType Directory -Path $exportDir -Force | Out-Null } # Step 1: Get Azure AD token $token = Get-AzureADToken -TenantId $tenantId -ClientId $clientId -ClientSecret $clientSecret # Step 2: Get companies $companies = Get-Companies -Token $token if ($companies.Count -eq 0) { Write-Log "No companies found in environment $environmentName" "ERROR" exit 1 } # Save companies list $companies | ConvertTo-Json -Depth 10 | Out-File -FilePath (Join-Path $exportDir "companies.json") -Encoding utf8 # Filter to specific company if configured $targetCompanies = $companies if ($bcCompanyName) { $targetCompanies = $companies | Where-Object { $_.name -eq $bcCompanyName -or $_.displayName -eq $bcCompanyName } if ($targetCompanies.Count -eq 0) { Write-Log "Company '$bcCompanyName' not found. Available: $($companies.name -join ', ')" "ERROR" exit 1 } Write-Log "Filtering to company: $bcCompanyName" } $totalRecords = 0 $totalEntities = 0 $failedEntities = @() # Step 3: Export data for each company foreach ($company in $targetCompanies) { $companyName = $company.name $companyId = $company.id Write-Log "-----------------------------------------" Write-Log "Exporting company: $companyName ($companyId)" # Create company directory (sanitize name for filesystem) $safeName = $companyName -replace '[\\/:*?"<>|]', '_' $companyDir = Join-Path $exportDir $safeName if (-not (Test-Path $companyDir)) { New-Item -ItemType Directory -Path $companyDir -Force | Out-Null } foreach ($entity in $entities) { $count = Export-EntityData ` -Token $token ` -CompanyId $companyId ` -CompanyName $companyName ` -EntityName $entity ` -OutputDir $companyDir $totalRecords += $count $totalEntities++ if ($count -eq 0) { $failedEntities += "$companyName/$entity" } } } # Save export metadata $metadata = @{ exportDate = (Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC" -AsUTC) environment = $environmentName companies = @($targetCompanies | ForEach-Object { $_.name }) entitiesExported = $totalEntities totalRecords = $totalRecords failedEntities = $failedEntities } $metadata | ConvertTo-Json -Depth 5 | Out-File -FilePath (Join-Path $exportDir "export-metadata.json") -Encoding utf8 Write-Log "=========================================" Write-Log "Export completed" Write-Log "Companies: $($targetCompanies.Count)" Write-Log "Entities: $totalEntities" Write-Log "Total records: $totalRecords" if ($failedEntities.Count -gt 0) { Write-Log "Failed/empty: $($failedEntities.Count) entities" "WARN" } Write-Log "=========================================" exit 0 } catch { Write-Log "Unexpected error: $_" "ERROR" Write-Log "Stack trace: $($_.ScriptStackTrace)" "ERROR" exit 1 }