@memberjunction/actions-bizapps-accounting
Version:
Accounting system integration actions for MemberJunction
219 lines (186 loc) • 7.39 kB
text/typescript
import { RegisterClass } from '@memberjunction/global';
import { BaseAccountingAction } from '../../base/base-accounting-action';
import { UserInfo } from '@memberjunction/core';
import { CompanyIntegrationEntity } from '@memberjunction/core-entities';
import { BaseAction } from '@memberjunction/actions';
/**
* Base class for all Microsoft Dynamics 365 Business Central actions.
* Handles BC-specific authentication and API interaction patterns.
*/
(BaseAction, 'BusinessCentralBaseAction')
export abstract class BusinessCentralBaseAction extends BaseAccountingAction {
protected accountingProvider = 'Business Central';
protected integrationName = 'Microsoft Dynamics 365 Business Central';
/**
* Business Central API version
*/
protected apiVersion = 'v2.0';
/**
* Makes an authenticated request to Business Central API
*/
protected async makeBCRequest<T = any>(
endpoint: string,
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET',
body?: any,
contextUser?: UserInfo
): Promise<T> {
if (!contextUser) {
throw new Error('Context user is required for Business Central API calls');
}
// Get company ID from action params
const companyId = this.getParamValue(this.params, 'CompanyID');
if (!companyId) {
throw new Error('CompanyID parameter is required');
}
// Get the integration credentials
const integration = await this.getCompanyIntegration(companyId, contextUser);
// Get OAuth tokens (from env vars or database)
const { accessToken } = await this.getOAuthTokens(integration);
// Get Business Central environment and company info
const environment = integration.CustomAttribute1 || 'production';
const bcCompanyId = integration.ExternalSystemID;
const tenantId = integration.CustomAttribute1 || this.getCredentialFromEnv(companyId, 'TENANT_ID');
if (!bcCompanyId) {
throw new Error('Business Central Company ID not found. Set in CompanyIntegration.ExternalSystemID');
}
if (!tenantId) {
throw new Error('Tenant ID not found. Set in CompanyIntegration.CustomAttribute1 or environment variable');
}
// Build the full URL
const baseUrl = await this.getBusinessCentralAPIUrl(integration, tenantId, environment);
const fullUrl = `${baseUrl}/companies(${bcCompanyId})/${endpoint}`;
// Prepare headers
const headers: Record<string, string> = {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
};
// Add API version header
headers['api-version'] = this.apiVersion;
try {
const response = await fetch(fullUrl, {
method,
headers,
body: body ? JSON.stringify(body) : undefined
});
if (!response.ok) {
const errorText = await response.text();
let errorMessage = `Business Central API error: ${response.status} ${response.statusText}`;
try {
const errorJson = JSON.parse(errorText);
if (errorJson.error) {
errorMessage = `Business Central API error: ${errorJson.error.message} (Code: ${errorJson.error.code})`;
}
} catch {
errorMessage += ` - ${errorText}`;
}
throw new Error(errorMessage);
}
const result = await response.json();
return result as T;
} catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error(`Business Central API request failed: ${error}`);
}
}
/**
* Handles Business Central OData queries
*/
protected async queryBC<T = any>(
resource: string,
filters?: string[],
select?: string[],
expand?: string[],
orderBy?: string,
top?: number,
contextUser?: UserInfo
): Promise<T> {
const queryParams: string[] = [];
if (filters && filters.length > 0) {
queryParams.push(`$filter=${filters.join(' and ')}`);
}
if (select && select.length > 0) {
queryParams.push(`$select=${select.join(',')}`);
}
if (expand && expand.length > 0) {
queryParams.push(`$expand=${expand.join(',')}`);
}
if (orderBy) {
queryParams.push(`$orderby=${orderBy}`);
}
if (top) {
queryParams.push(`$top=${top}`);
}
const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
return this.makeBCRequest<T>(`${resource}${queryString}`, 'GET', undefined, contextUser);
}
/**
* Formats date for Business Central API (ISO 8601)
*/
protected formatBCDate(date: Date): string {
return date.toISOString().split('T')[0];
}
/**
* Parses Business Central date format
*/
protected parseBCDate(dateString: string): Date {
return new Date(dateString);
}
/**
* Maps Business Central account types to standard categories
*/
protected mapAccountType(bcAccountType: string): string {
const typeMap: Record<string, string> = {
'Posting': 'Posting',
'Heading': 'Header',
'Total': 'Total',
'Begin-Total': 'Subtotal',
'End-Total': 'Subtotal'
};
return typeMap[bcAccountType] || 'Other';
}
/**
* Maps Business Central account category to standard type
*/
protected mapAccountCategory(category: string): string {
const categoryMap: Record<string, string> = {
'Assets': 'Asset',
'Liabilities': 'Liability',
'Equity': 'Equity',
'Income': 'Revenue',
'Cost of Goods Sold': 'Expense',
'Expense': 'Expense'
};
return categoryMap[category] || 'Other';
}
/**
* Gets the appropriate Business Central API URL
*/
protected async getBusinessCentralAPIUrl(
integration: CompanyIntegrationEntity,
tenantId: string,
environment: string
): Promise<string> {
// Default Business Central API URL pattern
// Format: https://api.businesscentral.dynamics.com/v2.0/{tenant-id}/{environment}/api/v2.0
return `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environment}/api/${this.apiVersion}`;
}
/**
* Helper to build OData filter expressions
*/
protected buildFilterExpression(field: string, operator: string, value: any): string {
if (typeof value === 'string') {
return `${field} ${operator} '${value}'`;
} else if (value instanceof Date) {
return `${field} ${operator} ${value.toISOString()}`;
} else {
return `${field} ${operator} ${value}`;
}
}
/**
* Current action parameters (set by the framework)
*/
protected params: any;
}