@memberjunction/actions-bizapps-accounting
Version:
Accounting system integration actions for MemberJunction
313 lines (276 loc) • 11 kB
text/typescript
import { RegisterClass } from '@memberjunction/global';
import { QuickBooksBaseAction } from '../quickbooks-base.action';
import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
import { BaseAction } from '@memberjunction/actions';
/**
* Standard transaction interface
*/
export interface Transaction {
id: string;
transactionType: string;
transactionNumber?: string;
transactionDate: Date;
amount: number;
currency: string;
status?: string;
referenceNumber?: string;
memo?: string;
entityName?: string; // Customer/Vendor name
entityId?: string;
lines: TransactionLine[];
metadata: Record<string, any>; // Store original QB data
}
export interface TransactionLine {
id: string;
lineNumber: number;
description?: string;
amount: number;
accountId?: string;
accountName?: string;
itemId?: string;
itemName?: string;
quantity?: number;
rate?: number;
}
/**
* Action to retrieve transactions from QuickBooks Online
*/
(BaseAction, 'GetQuickBooksTransactionsAction')
export class GetQuickBooksTransactionsAction extends QuickBooksBaseAction {
/**
* Description of the action
*/
public get Description(): string {
return 'Retrieves transactions from QuickBooks Online with flexible filtering options';
}
/**
* Main execution method
*/
protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
try {
const contextUser = params.ContextUser;
if (!contextUser) {
return {
Success: false,
ResultCode: 'ERROR_NO_CONTEXT_USER',
Message: 'Context user is required for QuickBooks API calls',
Params: params.Params
};
}
// Store params for use in other methods
(this as any)._params = params.Params;
const transactionType = this.getParamValue(params.Params, 'TransactionType');
const transactions: Transaction[] = [];
if (transactionType) {
// Get specific transaction type
const result = await this.getTransactionsByType(transactionType, params, contextUser);
transactions.push(...result);
} else {
// Get all transaction types
const types = ['Invoice', 'Bill', 'Payment', 'JournalEntry', 'Deposit', 'Purchase'];
for (const type of types) {
try {
const result = await this.getTransactionsByType(type, params, contextUser);
transactions.push(...result);
} catch (error) {
// Log but continue with other types
console.warn(`Failed to retrieve ${type} transactions:`, error);
}
}
}
// Sort by date descending
transactions.sort((a, b) => b.transactionDate.getTime() - a.transactionDate.getTime());
// Apply max results limit
const maxResults = Math.min(this.getParamValue(params.Params, 'MaxResults') || 100, 1000);
const limitedTransactions = transactions.slice(0, maxResults);
// Create output parameters
const outputParams: ActionParam[] = [
{
Name: 'Transactions',
Value: limitedTransactions,
Type: 'Output'
},
{
Name: 'TotalCount',
Value: limitedTransactions.length,
Type: 'Output'
},
{
Name: 'HasMore',
Value: transactions.length > maxResults,
Type: 'Output'
}
];
return {
Success: true,
ResultCode: 'SUCCESS',
Message: `Successfully retrieved ${limitedTransactions.length} transactions`,
Params: [...params.Params, ...outputParams]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
return {
Success: false,
ResultCode: 'ERROR',
Message: errorMessage,
Params: params.Params
};
}
}
/**
* Get transactions of a specific type
*/
private async getTransactionsByType(
type: string,
params: RunActionParams,
contextUser: any
): Promise<Transaction[]> {
// Build query for specific transaction type
let query = `SELECT * FROM ${type}`;
const conditions: string[] = [];
// Add date range filter
const startDate = this.getParamValue(params.Params, 'StartDate');
const endDate = this.getParamValue(params.Params, 'EndDate');
if (startDate) {
conditions.push(`TxnDate >= '${this.formatQBODate(new Date(startDate))}'`);
}
if (endDate) {
conditions.push(`TxnDate <= '${this.formatQBODate(new Date(endDate))}'`);
}
// Add amount filters
const minAmount = this.getParamValue(params.Params, 'MinAmount');
const maxAmount = this.getParamValue(params.Params, 'MaxAmount');
if (minAmount !== undefined) {
conditions.push(`TotalAmt >= ${minAmount}`);
}
if (maxAmount !== undefined) {
conditions.push(`TotalAmt <= ${maxAmount}`);
}
// Add entity filter based on transaction type
const entityId = this.getParamValue(params.Params, 'EntityID');
if (entityId) {
if (['Invoice', 'Payment', 'Deposit'].includes(type)) {
conditions.push(`CustomerRef = '${entityId}'`);
} else if (['Bill', 'Purchase'].includes(type)) {
conditions.push(`VendorRef = '${entityId}'`);
}
}
// Add conditions to query
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
// Add ordering and limit
query += ' ORDER BY TxnDate DESC';
query += ` MAXRESULTS ${this.getParamValue(params.Params, 'MaxResults') || 100}`;
// Execute the query
const response = await this.queryQBO<{ QueryResponse: { [key: string]: any[] } }>(
query,
contextUser
);
const qbTransactions = response.QueryResponse?.[type] || [];
return qbTransactions.map(qbTxn => this.mapQuickBooksTransaction(type, qbTxn));
}
/**
* Map QuickBooks transaction to standard format
*/
private mapQuickBooksTransaction(type: string, qbTxn: any): Transaction {
const transaction: Transaction = {
id: qbTxn.Id,
transactionType: type,
transactionNumber: qbTxn.DocNumber,
transactionDate: this.parseQBODate(qbTxn.TxnDate),
amount: qbTxn.TotalAmt || 0,
currency: qbTxn.CurrencyRef?.value || 'USD',
status: this.getTransactionStatus(type, qbTxn),
referenceNumber: qbTxn.PrivateNote || qbTxn.CustomerMemo?.value,
memo: qbTxn.Memo,
entityName: this.getEntityName(type, qbTxn),
entityId: this.getEntityId(type, qbTxn),
lines: this.mapTransactionLines(type, qbTxn),
metadata: qbTxn
};
return transaction;
}
/**
* Get transaction status based on type
*/
private getTransactionStatus(type: string, qbTxn: any): string {
switch (type) {
case 'Invoice':
return qbTxn.Balance > 0 ? 'Unpaid' : 'Paid';
case 'Bill':
return qbTxn.Balance > 0 ? 'Unpaid' : 'Paid';
case 'Payment':
return 'Completed';
case 'JournalEntry':
return 'Posted';
default:
return qbTxn.TxnStatus || 'Unknown';
}
}
/**
* Get entity name based on transaction type
*/
private getEntityName(type: string, qbTxn: any): string | undefined {
if (['Invoice', 'Payment', 'Deposit'].includes(type)) {
return qbTxn.CustomerRef?.name;
} else if (['Bill', 'Purchase'].includes(type)) {
return qbTxn.VendorRef?.name;
}
return undefined;
}
/**
* Get entity ID based on transaction type
*/
private getEntityId(type: string, qbTxn: any): string | undefined {
if (['Invoice', 'Payment', 'Deposit'].includes(type)) {
return qbTxn.CustomerRef?.value;
} else if (['Bill', 'Purchase'].includes(type)) {
return qbTxn.VendorRef?.value;
}
return undefined;
}
/**
* Map transaction lines
*/
private mapTransactionLines(type: string, qbTxn: any): TransactionLine[] {
const lines: TransactionLine[] = [];
const qbLines = qbTxn.Line || [];
qbLines.forEach((line: any, index: number) => {
// Skip summary lines
if (line.DetailType === 'SubTotalLineDetail') {
return;
}
const mappedLine: TransactionLine = {
id: line.Id || `${qbTxn.Id}-${index}`,
lineNumber: index + 1,
description: line.Description,
amount: line.Amount || 0,
accountId: this.getLineAccountId(line),
accountName: this.getLineAccountName(line),
itemId: line.SalesItemLineDetail?.ItemRef?.value || line.ItemBasedExpenseLineDetail?.ItemRef?.value,
itemName: line.SalesItemLineDetail?.ItemRef?.name || line.ItemBasedExpenseLineDetail?.ItemRef?.name,
quantity: line.SalesItemLineDetail?.Qty || line.ItemBasedExpenseLineDetail?.Qty,
rate: line.SalesItemLineDetail?.UnitPrice || line.ItemBasedExpenseLineDetail?.UnitPrice
};
lines.push(mappedLine);
});
return lines;
}
/**
* Get account ID from line detail
*/
private getLineAccountId(line: any): string | undefined {
return line.AccountBasedExpenseLineDetail?.AccountRef?.value ||
line.DepositLineDetail?.AccountRef?.value ||
line.JournalEntryLineDetail?.AccountRef?.value;
}
/**
* Get account name from line detail
*/
private getLineAccountName(line: any): string | undefined {
return line.AccountBasedExpenseLineDetail?.AccountRef?.name ||
line.DepositLineDetail?.AccountRef?.name ||
line.JournalEntryLineDetail?.AccountRef?.name;
}
}