@memberjunction/actions-bizapps-accounting
Version:
Accounting system integration actions for MemberJunction
195 lines (168 loc) • 7.07 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 { ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
import { BaseAction } from '@memberjunction/actions';
/**
* Base class for all QuickBooks Online actions.
* Handles QB-specific authentication and API interaction patterns.
*/
(BaseAction, 'QuickBooksBaseAction')
export abstract class QuickBooksBaseAction extends BaseAccountingAction {
protected accountingProvider = 'QuickBooks Online';
protected integrationName = 'QuickBooks Online';
/**
* QuickBooks API version
*/
protected apiVersion = 'v3';
/**
* QuickBooks minor version for API compatibility
*/
protected minorVersion = '65'; // Latest as of 2024
/**
* Makes an authenticated request to QuickBooks Online API
*/
protected async makeQBORequest<T = any>(
endpoint: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
body?: any,
contextUser?: UserInfo
): Promise<T> {
if (!contextUser) {
throw new Error('Context user is required for QuickBooks 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 QuickBooks company ID (realm ID) from ExternalSystemID
const realmId = integration.ExternalSystemID || this.getCredentialFromEnv(companyId, 'REALM_ID');
if (!realmId) {
throw new Error('QuickBooks Realm ID not found. Set in CompanyIntegration.ExternalSystemID or environment variable');
}
// Build the full URL using the environment from integration
const baseUrl = await this.getQuickBooksAPIUrl(integration);
const fullUrl = `${baseUrl}/${this.apiVersion}/company/${realmId}/${endpoint}`;
// Prepare headers
const headers: Record<string, string> = {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
};
// Add minor version header for API compatibility
if (this.minorVersion) {
headers['Intuit-Company-ID'] = realmId;
headers['Accept'] = `application/json;minorversion=${this.minorVersion}`;
}
try {
const response = await fetch(fullUrl, {
method,
headers,
body: body ? JSON.stringify(body) : undefined
});
if (!response.ok) {
const errorText = await response.text();
let errorMessage = `QuickBooks API error: ${response.status} ${response.statusText}`;
try {
const errorJson = JSON.parse(errorText);
if (errorJson.Fault && errorJson.Fault.Error) {
const qbError = errorJson.Fault.Error[0];
errorMessage = `QuickBooks API error: ${qbError.Message} (Code: ${qbError.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(`QuickBooks API request failed: ${error}`);
}
}
/**
* Handles QuickBooks query language requests
*/
protected async queryQBO<T = any>(
query: string,
contextUser: UserInfo
): Promise<T> {
const encodedQuery = encodeURIComponent(query);
return this.makeQBORequest<T>(`query?query=${encodedQuery}`, 'GET', undefined, contextUser);
}
/**
* Converts QuickBooks date format to standard ISO format
*/
protected parseQBODate(qboDate: string): Date {
// QuickBooks uses YYYY-MM-DD format
return new Date(qboDate + 'T00:00:00Z');
}
/**
* Formats date for QuickBooks API
*/
protected formatQBODate(date: Date): string {
return date.toISOString().split('T')[0];
}
/**
* Maps QuickBooks account types to standard accounting categories
*/
protected mapAccountType(qboAccountType: string): string {
const typeMap: Record<string, string> = {
'Bank': 'Asset',
'Accounts Receivable': 'Asset',
'Other Current Asset': 'Asset',
'Fixed Asset': 'Asset',
'Other Asset': 'Asset',
'Accounts Payable': 'Liability',
'Credit Card': 'Liability',
'Long Term Liability': 'Liability',
'Other Current Liability': 'Liability',
'Equity': 'Equity',
'Income': 'Revenue',
'Other Income': 'Revenue',
'Cost of Goods Sold': 'Expense',
'Expense': 'Expense',
'Other Expense': 'Expense'
};
return typeMap[qboAccountType] || 'Other';
}
/**
* Gets the appropriate QuickBooks API URL based on configuration
*/
protected async getQuickBooksAPIUrl(integration: CompanyIntegrationEntity): Promise<string> {
// First, check if there's a URL in the Integration entity
// The Integration property should be loaded via the view, not accessed as a sub-property
const integrationNavURL = (integration as any).IntegrationNavigationBaseURL;
if (integrationNavURL) {
return integrationNavURL;
}
// Fall back to environment-based URL
const isSandbox = integration.CustomAttribute1?.toLowerCase() === 'sandbox';
return isSandbox
? 'https://sandbox-quickbooks.api.intuit.com'
: 'https://quickbooks.api.intuit.com';
}
/**
* Store the params for use in other methods
*/
private _params: RunActionParams['Params'];
/**
* Override the required abstract method
*/
protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
// Store params for use in other methods
this._params = params.Params;
// This is an abstract base class, so we don't implement the actual logic here
// Subclasses must implement this method
throw new Error('InternalRunAction must be implemented by subclasses');
}
}