UNPKG

@memberjunction/actions-bizapps-accounting

Version:

Accounting system integration actions for MemberJunction

366 lines (326 loc) 12.4 kB
import { RegisterClass } from '@memberjunction/global'; import { BusinessCentralBaseAction } from '../business-central-base.action'; import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base'; import { BaseAction } from '@memberjunction/actions'; /** * Interface for a General Ledger Entry from Business Central */ export interface BCGeneralLedgerEntry { id: string; entryNumber: number; postingDate: Date; documentNumber: string; documentType: string; accountId: string; accountNumber: string; accountName: string; description: string; debitAmount: number; creditAmount: number; amount: number; lastModifiedDateTime: Date; dimensionSetLines?: BCDimensionSetLine[]; } export interface BCDimensionSetLine { id: string; code: string; displayName: string; valueId: string; valueCode: string; valueDisplayName: string; } /** * Action to retrieve General Ledger Entries from Microsoft Dynamics 365 Business Central */ @RegisterClass(BaseAction, 'GetBusinessCentralGeneralLedgerEntriesAction') export class GetBusinessCentralGeneralLedgerEntriesAction extends BusinessCentralBaseAction { /** * Description of the action */ public get Description(): string { return 'Retrieves General Ledger Entries (journal entries) from Microsoft Dynamics 365 Business Central'; } /** * Main execution method */ protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> { try { const contextUser = params.ContextUser; if (!contextUser) { return { Success: false, ResultCode: 'ERROR', Message: 'Context user is required for Business Central API calls' }; } // Store params for base class methods this.params = params.Params; // Build filters based on parameters const filters: string[] = []; // Date range filters const startDate = this.getParamValue(params.Params, 'StartDate'); if (startDate) { filters.push(`postingDate ge ${this.formatBCDate(new Date(startDate))}`); } const endDate = this.getParamValue(params.Params, 'EndDate'); if (endDate) { filters.push(`postingDate le ${this.formatBCDate(new Date(endDate))}`); } // Account filter const accountNumber = this.getParamValue(params.Params, 'AccountNumber'); if (accountNumber) { filters.push(`accountNumber eq '${accountNumber}'`); } // Document number filter const documentNumber = this.getParamValue(params.Params, 'DocumentNumber'); if (documentNumber) { filters.push(`documentNumber eq '${documentNumber}'`); } // Document type filter const documentType = this.getParamValue(params.Params, 'DocumentType'); if (documentType) { filters.push(`documentType eq '${documentType}'`); } // Amount filters const minAmount = this.getParamValue(params.Params, 'MinAmount'); if (minAmount !== undefined) { filters.push(`amount ge ${minAmount}`); } const maxAmount = this.getParamValue(params.Params, 'MaxAmount'); if (maxAmount !== undefined) { filters.push(`amount le ${maxAmount}`); } // Select fields to retrieve const select = [ 'id', 'entryNumber', 'postingDate', 'documentNumber', 'documentType', 'accountId', 'accountNumber', 'description', 'debitAmount', 'creditAmount', 'amount', 'lastModifiedDateTime' ]; // Expand dimensions if requested const expand = []; const includeDimensions = this.getParamValue(params.Params, 'IncludeDimensions'); if (includeDimensions) { expand.push('dimensionSetLines'); } // Order by const orderBy = 'postingDate desc,entryNumber desc'; // Max results const maxResults = this.getParamValue(params.Params, 'MaxResults') || 500; // Execute the query const response = await this.queryBC<{ value: any[] }>( 'generalLedgerEntries', filters, select, expand, orderBy, maxResults, contextUser ); const bcEntries = response.value || []; const glEntries: BCGeneralLedgerEntry[] = []; // Map entries and get account names for (const entry of bcEntries) { const mappedEntry = await this.mapBCEntryToGLEntry(entry, contextUser); glEntries.push(mappedEntry); } // Calculate summary const summary = this.calculateEntrySummary(glEntries); // Create output parameters if (!params.Params.find(p => p.Name === 'GLEntries')) { params.Params.push({ Name: 'GLEntries', Type: 'Output', Value: glEntries }); } else { params.Params.find(p => p.Name === 'GLEntries')!.Value = glEntries; } if (!params.Params.find(p => p.Name === 'TotalCount')) { params.Params.push({ Name: 'TotalCount', Type: 'Output', Value: glEntries.length }); } else { params.Params.find(p => p.Name === 'TotalCount')!.Value = glEntries.length; } if (!params.Params.find(p => p.Name === 'Summary')) { params.Params.push({ Name: 'Summary', Type: 'Output', Value: summary }); } else { params.Params.find(p => p.Name === 'Summary')!.Value = summary; } return { Success: true, ResultCode: 'SUCCESS', Message: `Successfully retrieved ${glEntries.length} general ledger entries from Business Central` }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { Success: false, ResultCode: 'ERROR', Message: errorMessage }; } } /** * Maps a Business Central GL entry to our standard interface */ private async mapBCEntryToGLEntry(bcEntry: any, contextUser: any): Promise<BCGeneralLedgerEntry> { // Get account name if not included let accountName = bcEntry.accountName || ''; if (!accountName && bcEntry.accountId) { try { const accountResponse = await this.makeBCRequest<any>( `generalLedgerAccounts(${bcEntry.accountId})`, 'GET', undefined, contextUser ); accountName = accountResponse.displayName || ''; } catch (error) { // If we can't get the account name, just use the number accountName = bcEntry.accountNumber; } } return { id: bcEntry.id, entryNumber: bcEntry.entryNumber, postingDate: this.parseBCDate(bcEntry.postingDate), documentNumber: bcEntry.documentNumber || '', documentType: bcEntry.documentType || '', accountId: bcEntry.accountId, accountNumber: bcEntry.accountNumber, accountName: accountName, description: bcEntry.description || '', debitAmount: bcEntry.debitAmount || 0, creditAmount: bcEntry.creditAmount || 0, amount: bcEntry.amount || 0, lastModifiedDateTime: this.parseBCDate(bcEntry.lastModifiedDateTime), dimensionSetLines: bcEntry.dimensionSetLines ? this.mapDimensionSetLines(bcEntry.dimensionSetLines) : undefined }; } /** * Map dimension set lines */ private mapDimensionSetLines(dimensions: any[]): BCDimensionSetLine[] { return dimensions.map(dim => ({ id: dim.id, code: dim.code, displayName: dim.displayName || dim.code, valueId: dim.valueId, valueCode: dim.valueCode, valueDisplayName: dim.valueDisplayName || dim.valueCode })); } /** * Calculate entry summary statistics */ private calculateEntrySummary(entries: BCGeneralLedgerEntry[]): any { const summary = { totalEntries: entries.length, totalDebits: 0, totalCredits: 0, netAmount: 0, earliestDate: null as Date | null, latestDate: null as Date | null, entriesByType: {} as Record<string, number>, entriesByAccount: {} as Record<string, { count: number; debit: number; credit: number }>, isBalanced: true }; entries.forEach(entry => { // Sum totals summary.totalDebits += entry.debitAmount; summary.totalCredits += entry.creditAmount; summary.netAmount += entry.amount; // Track dates if (!summary.earliestDate || entry.postingDate < summary.earliestDate) { summary.earliestDate = entry.postingDate; } if (!summary.latestDate || entry.postingDate > summary.latestDate) { summary.latestDate = entry.postingDate; } // Count by type const type = entry.documentType || 'Other'; summary.entriesByType[type] = (summary.entriesByType[type] || 0) + 1; // Sum by account const accountKey = `${entry.accountNumber} - ${entry.accountName}`; if (!summary.entriesByAccount[accountKey]) { summary.entriesByAccount[accountKey] = { count: 0, debit: 0, credit: 0 }; } summary.entriesByAccount[accountKey].count++; summary.entriesByAccount[accountKey].debit += entry.debitAmount; summary.entriesByAccount[accountKey].credit += entry.creditAmount; }); // Check if balanced (debits = credits) summary.isBalanced = Math.abs(summary.totalDebits - summary.totalCredits) < 0.01; return summary; } /** * Define the parameters for this action */ public get Params(): ActionParam[] { const baseParams = this.getCommonAccountingParams(); const specificParams: ActionParam[] = [ { Name: 'StartDate', Type: 'Input', Value: null }, { Name: 'EndDate', Type: 'Input', Value: null }, { Name: 'AccountNumber', Type: 'Input', Value: null }, { Name: 'DocumentNumber', Type: 'Input', Value: null }, { Name: 'DocumentType', Type: 'Input', Value: null }, { Name: 'MinAmount', Type: 'Input', Value: null }, { Name: 'MaxAmount', Type: 'Input', Value: null }, { Name: 'IncludeDimensions', Type: 'Input', Value: false }, { Name: 'MaxResults', Type: 'Input', Value: 500 } ]; return [...baseParams, ...specificParams]; } }