CONA Accounting Rules Engine

Overview

The CONA Accounting Rules Engine is a sophisticated system that automatically determines the correct General Ledger (GL) dimensions for financial transactions based on configurable business rules. It eliminates the need for manual accounting entries by intelligently matching transaction data against predefined criteria.

Core Purpose

Why do we need this?

  • Automation: Automatically classify transactions without manual intervention
  • Accuracy: Reduce human error in GL account assignments
  • Consistency: Ensure uniform accounting treatment across all transactions
  • Scalability: Handle thousands of transactions with complex routing logic
  • Auditability: Maintain clear trails of why specific GL accounts were chosen

Accounting Dimensions

What are Accounting Dimensions?

Accounting dimensions are the different ways we can categorize and route financial transactions in the General Ledger (Collection of financial transactions). They represent the “buckets” where transactions should be recorded. Financial transactions are also called accounting impacts, financial entries, journal entries or GL-Impacts.

Types of Dimensions

  1. GL Accounts (account type)

    • The most common dimension type
    • Represents specific Chart of Accounts (Collection of Account-numbers) entries
    • Examples: “Sales Revenue”, “Cost of Goods Sold”, “Accounts Receivable”
  2. Custom Dimensions (other types)

    • Cost centers, departments, projects, etc.
    • Used for additional categorization beyond standard GL accounts

Dimension Structure

interface GLDimension {
  id: string;
  label: string;           // Human-readable name
  type: "account" | string; // Dimension type
  account_nr?: string;      // Account number (for GL accounts)
}

Rules Architecture

What is a Rule?

A Rule is a logical statement that defines when a specific GL dimension should be applied to a transaction. Think of it as an “if-then” statement:

IF transaction meets these criteria THEN assign this GL dimension”

Rule Structure

interface Rule {
  id: string;
  criteria: Criteria[];           // List of conditions to check
  gl_account?: GLAccount;         // Target GL account (for account dimensions)
  set_gl_dimension?: string;      // Target value (for other dimensions)
  is_fallback?: boolean;          // Whether this is a fallback rule
  order: number;                  // Priority order for rule evaluation
}

Rule Types

  1. Standard Rules

    • Contains specific criteria that must be met
    • Evaluated in order of priority
    • First matching rule wins
  2. Fallback Rules

    • Special rules that match when no other rules apply
    • Typically use the all operator to match everything
    • Prevents transactions from becoming “UNMATCHED”

Criteria System

What is a Criterion?

A Criterion is a single condition that checks a specific field in the transaction data against an expected value using a logical operator.

Criterion Structure

interface Criterion {
  id: string;
  column_id: string;    // Which field to check
  operator: string;     // How to compare (=, !=, >, contains, etc.)
  value: string;        // Expected value to compare against
  column: {
    column_type: {
      field_path: string;           // Path to the field in transaction data
      field_type: string;           // Data type (string, number, date, etc.)
      nested_column_type_child?: {  // For nested fields
        field_path: string;
      };
    };
  };
}

Field Path Examples

// Direct field access
field_path: "total_amount"        // data.total_amount
field_path: "currency"            // data.currency

// Nested object access
field_path: "custom_properties"   // data.custom_properties.shopify_payment_type.value
nested_field_path: "shopify_payment_type"

// Array access (first element)
field_path: "line_items"          // data.line_items[0].tax_rate
nested_field_path: "tax_rate"

Operators

What are Operators?

Operators define how to compare the actual transaction data against the expected criterion value.

Operator Types

Equality Operators

  • = (equals) - Exact match
  • != (not equals) - Does not match

Numeric Operators

  • > (greater than)
  • < (less than)
  • >= (greater than or equal)
  • <= (less than or equal)

String Operators

  • *= (contains) - Field contains the value
  • !*= (not contains) - Field does not contain the value
  • ^= (starts with) - Field starts with the value
  • !^= (not starts with) - Field does not start with the value
  • $= (ends with) - Field ends with the value
  • !$= (not ends with) - Field does not end with the value

Special Operators

  • empty - Field is empty or null
  • !empty - Field is not empty
  • all - Always matches (used in fallback rules)

Operator Examples

// Exact match
{ operator: "=", value: "shopify", field_path: "sales_channel" }
// Matches when data.sales_channel === "shopify"

// Contains text
{ operator: "*=", value: "refund", field_path: "description" }
// Matches when data.description contains "refund"

// Numeric comparison
{ operator: ">", value: "100", field_path: "total_amount" }
// Matches when data.total_amount > 100

// Empty check
{ operator: "empty", value: "", field_path: "customer_notes" }
// Matches when data.customer_notes is empty

Rule Evaluation Process

How Rules are Processed

  1. Document Splitting: Complex documents are split into individual documents per line item
  2. Data Extraction: Extract field values from transaction data using field paths
  3. Criterion Evaluation: Check each criterion in the rule against the extracted data
  4. Rule Matching: Rule matches only if ALL criteria are true
  5. Rule Selection: First matching rule (by order) determines the GL dimension
  6. Fallback Handling: If no rules match, use fallback rule if available
  7. Consolidation: Documents with identical GL dimensions are consolidated back together

Document Splitting and Consolidation Process

The accounting rules engine processes complex documents through a split-and-consolidate approach:

1. Document Splitting

When a document contains multiple line items (e.g., an invoice with different product types), the system:

  • Creates separate documents for each line item
  • Each split document inherits the parent document’s metadata (customer, date, etc.)
  • Each split document contains only one line item for targeted rule evaluation

2. Individual Rule Evaluation

Each split document is processed independently:

  • Rules are applied to each document’s specific line item data
  • Different line items may match different rules and get different GL dimensions
  • This allows for precise accounting treatment of mixed transactions

3. Consolidation

After rule evaluation, documents with identical GL dimensions are merged:

  • Documents targeting the same GL account are consolidated
  • Line items are combined while preserving individual amounts
  • This reduces the number of journal entries while maintaining accuracy

Example: Mixed Product Invoice

Original Document:

{
  "id": "INV-001",
  "customer": "ACME Corp",
  "total_amount": 1150.00,
  "line_items": [
    {
      "type": "product",
      "description": "Widget A",
      "amount": 500.00,
      "tax_rate": 0.08
    },
    {
      "type": "shipping",
      "description": "Express Shipping",
      "amount": 50.00,
      "tax_rate": 0.08
    },
    {
      "type": "service",
      "description": "Installation",
      "amount": 600.00,
      "tax_rate": 0.08
    }
  ]
}

After Splitting (3 documents):

  • Document 1: Widget A (product) → Rules evaluate to “Product Sales Revenue”
  • Document 2: Express Shipping (shipping) → Rules evaluate to “Shipping Revenue”
  • Document 3: Installation (service) → Rules evaluate to “Service Revenue”

After Consolidation:

  • 3 separate journal entries created (no consolidation because all dimensions are different)
  • Each targeting their respective revenue accounts

Alternative Scenario - Same Dimensions: If line items 1 and 3 both matched rules pointing to “General Sales Revenue”:

  • Documents 1 and 3 would consolidate into one journal entry
  • Document 2 would remain separate for “Shipping Revenue”
  • Result: 2 journal entries instead of 3

This approach ensures that:

  • Accuracy: Each line item gets the correct accounting treatment
  • Efficiency: Unnecessary journal entry proliferation is avoided
  • Flexibility: Complex documents with mixed line items are handled automatically
  • Auditability: The split-and-consolidate process is tracked and reversible

Practical Examples

Example 1: Simple Sales Revenue Rule

Business Logic: “All Shopify sales should go to Sales Revenue account”

const rule = {
  id: "shopify-sales-revenue",
  order: 1,
  criteria: [
    {
      column_id: "sales_channel",
      operator: "=",
      value: "shopify",
      column: {
        column_type: {
          field_path: "sales_channel",
          field_type: "string"
        }
      }
    }
  ],
  gl_account: {
    id: "4000",
    label: "Sales Revenue",
    account_nr: "4000"
  }
};

Transaction Data:

{
  "sales_channel": "shopify",
  "total_amount": 150.00,
  "currency": "USD"
}

Result: Matches → assigns to “Sales Revenue” account

Example 2: Complex Multi-Criteria Rule

Business Logic: “Shopify refunds over $100 should go to Refunds Expense account”

const rule = {
  id: "shopify-large-refunds",
  order: 1,
  criteria: [
    {
      column_id: "sales_channel",
      operator: "=",
      value: "shopify",
      column: {
        column_type: {
          field_path: "sales_channel",
          field_type: "string"
        }
      }
    },
    {
      column_id: "transaction_type",
      operator: "=",
      value: "refund",
      column: {
        column_type: {
          field_path: "transaction_type",
          field_type: "string"
        }
      }
    },
    {
      column_id: "amount",
      operator: ">",
      value: "100",
      column: {
        column_type: {
          field_path: "total_amount",
          field_type: "number"
        }
      }
    }
  ],
  gl_account: {
    id: "6100",
    label: "Refunds Expense",
    account_nr: "6100"
  }
};

Transaction Data:

{
  "sales_channel": "shopify",
  "transaction_type": "refund",
  "total_amount": 250.00,
  "currency": "USD"
}

Result: Matches → assigns to “Refunds Expense” account

Example 3: Nested Field Access

Business Logic: “Transactions with specific payment methods should go to different accounts”

const rule = {
  id: "credit-card-payments",
  order: 1,
  criteria: [
    {
      column_id: "payment_method",
      operator: "=",
      value: "credit_card",
      column: {
        column_type: {
          field_path: "custom_properties",
          field_type: "string",
          nested_column_type_child: {
            field_path: "shopify_payment_method"
          }
        }
      }
    }
  ],
  gl_account: {
    id: "1200",
    label: "Credit Card Receivables",
    account_nr: "1200"
  }
};

Transaction Data:

{
  "total_amount": 150.00,
  "custom_properties": {
    "shopify_payment_method": {
      "label": "Payment Method",
      "value": "credit_card"
    }
  }
}

Result: Matches → assigns to “Credit Card Receivables” account

Example 4: Fallback Rule

Business Logic: “Any unmatched transactions should go to Unmatched Revenue”

const fallbackRule = {
  id: "unmatched-fallback",
  order: 999,
  is_fallback: true,
  criteria: [
    {
      column_id: "catch_all",
      operator: "all",
      value: "",
      column: {
        column_type: {
          field_path: "total_amount",
          field_type: "number"
        }
      }
    }
  ],
  gl_account: {
    id: "4999",
    label: "Unmatched Revenue",
    account_nr: "4999"
  }
};

Result: Always matches if no other rules match

Advanced Features

Line Item Validation

For transactions with multiple line items, the system can validate against specific line item properties:

// Check if any line item has type "shipping"
const criterion = {
  column_id: "line_item_type",
  operator: "=",
  value: "shipping",
  column: {
    column_type: {
      field_path: "line_items",
      field_type: "array",
      nested_column_type_child: {
        field_path: "type"
      }
    }
  }
};

Custom Property Support

CONA supports custom properties with special handling for the {label, value} format:

// Custom property stored as: {label: "Tax Rate", value: "0.08"}
// The system automatically extracts the .value for comparison

Best Practices

Rule Design

  1. Order Matters: More specific rules should have lower order numbers
  2. Always Have Fallbacks: Include fallback rules to prevent UNMATCHED transactions
  3. Test Thoroughly: Use the rule diagnostic tools to validate logic
  4. Keep It Simple: Avoid overly complex criteria when possible

Performance Considerations

  1. Field Path Caching: The system caches field path computations
  2. Rule Ordering: Most common rules should be evaluated first
  3. Criterion Efficiency: Use exact matches when possible over contains/regex

Error Handling

  1. Validation: Rules are validated before saving
  2. Diagnostics: Built-in tools help identify configuration issues
  3. Logging: Detailed logs show why specific rules matched or didn’t match

Troubleshooting

Common Issues

  1. Rules Not Matching

    • Check field paths are correct
    • Verify data types match expected values
    • Use diagnostic tools to see actual vs expected values
  2. UNMATCHED Transactions

    • Add fallback rules with all operator
    • Check rule order and specificity
  3. Performance Issues

    • Optimize rule ordering
    • Reduce complexity of criteria
    • Check for unnecessary nested field access

Debugging Tools

The system provides diagnostic functions to help troubleshoot:

// Analyze rules and identify issues
const diagnosis = diagnoseRules(rules, transactionData);

// Check individual rule matching
const isMatch = validateRule(transactionData, rule);

Integration Points

Data Flow

  1. Transaction Import: Data comes from integrations (Shopify, PayPal, etc.)
  2. Rule Evaluation: Rules engine processes transaction data
  3. GL Assignment: Determined dimensions are assigned to transactions
  4. Journal Entries: Final GL entries are created for accounting system

API Integration

  • Rules are configured via the posting matrix UI
  • Rule evaluation happens automatically during transaction processing
  • Results are stored and auditable

This accounting rules engine provides the foundation for automated, accurate, and scalable financial transaction processing in CONA.