UNPKG

@memberjunction/actions-bizapps-accounting

Version:

Accounting system integration actions for MemberJunction

313 lines (276 loc) 11 kB
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 */ @RegisterClass(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; } }