GraphQL Transactions

This page documents GraphQL operations for managing financial transactions and journal entries in Crane Ledger.

Transaction Queries

Get Transaction by ID

Retrieve a specific transaction with full journal entry details.

query GetTransaction($id: ID!) {
  transaction(id: $id) {
    id
    description
    date
    status
    amount
    currency {
      code
      name
      symbol
    }
    entries {
      id
      accountId
      entryType
      amount
      baseAmount
      description
      reference
      createdAt
    }
    createdAt
    updatedAt
    metadata
  }
}

Arguments:

  • id: ID! - Transaction unique identifier

Returns:

  • Complete transaction information
  • All journal entries (debits and credits)
  • Transaction status and metadata

Get Recent Transactions

Get the most recent transactions for the organization.

query GetRecentTransactions($limit: Int) {
  organization {
    recentTransactions(limit: $limit) {
      id
      description
      date
      status
      amount
      currency {
        code
        symbol
      }
      entries {
        accountId
        entryType
        amount
        description
      }
    }
  }
}

Arguments:

  • limit: Int - Maximum transactions to return (default: 10, max: 100)

Get Account Transactions

Get transactions that affect a specific account.

query GetAccountTransactions($accountId: ID!, $limit: Int) {
  account(id: $accountId) {
    id
    code
    name
    transactions(limit: $limit) {
      id
      description
      date
      amount
      status
      entries {
        entryType
        amount
        description
      }
    }
  }
}

Get Contact Transactions

Get transactions involving a specific contact.

query GetContactTransactions($contactId: ID!, $limit: Int) {
  contact(id: $contactId) {
    id
    name
    transactions(limit: $limit) {
      id
      description
      date
      amount
      status
      entries {
        accountId
        entryType
        amount
      }
    }
  }
}

Transaction Mutations

Create Transaction

Record a new journal entry transaction with debits and credits.

mutation CreateTransaction(
  $description: String!
  $date: DateTime!
  $entries: [LedgerEntryInput!]!
) {
  createTransaction(
    description: $description
    date: $date
    entries: $entries
  ) {
    id
    description
    date
    status
    amount
    entries {
      id
      accountId
      entryType
      amount
      baseAmount
      description
    }
  }
}

Required Fields:

  • description: Transaction description
  • date: Transaction date
  • entries: Array of journal entries

LedgerEntryInput Structure:

{
  accountId: "ACT_123"        # Account to affect
  entryType: DEBIT            # DEBIT or CREDIT
  amount: 100.00             # Positive amount
  description: "Optional entry description"
  reference: "Optional reference (invoice #, etc.)"
}

Double-Entry Bookkeeping

Fundamental Principle

Every transaction must have balanced debits and credits:

const validateTransaction = (entries) => {
  const debits = entries
    .filter(entry => entry.entryType === 'DEBIT')
    .reduce((sum, entry) => sum + parseFloat(entry.amount), 0);

  const credits = entries
    .filter(entry => entry.entryType === 'CREDIT')
    .reduce((sum, entry) => sum + parseFloat(entry.amount), 0);

  return Math.abs(debits - credits) < 0.01; // Allow for rounding
};

Common Transaction Types

1. Revenue Recognition

const createRevenueTransaction = (amount, customerAccountId, revenueAccountId) => ({
  description: "Revenue from services",
  date: new Date().toISOString(),
  entries: [
    {
      accountId: customerAccountId, // Accounts Receivable
      entryType: "DEBIT",
      amount: amount,
      description: "Customer invoice"
    },
    {
      accountId: revenueAccountId, // Service Revenue
      entryType: "CREDIT",
      amount: amount,
      description: "Revenue earned"
    }
  ]
});

2. Expense Recording

const createExpenseTransaction = (amount, expenseAccountId, cashAccountId) => ({
  description: "Office supplies expense",
  date: new Date().toISOString(),
  entries: [
    {
      accountId: expenseAccountId, // Office Supplies Expense
      entryType: "DEBIT",
      amount: amount,
      description: "Office supplies purchase"
    },
    {
      accountId: cashAccountId, // Cash/Bank Account
      entryType: "CREDIT",
      amount: amount,
      description: "Cash payment"
    }
  ]
});

3. Asset Purchase

const createAssetPurchaseTransaction = (amount, assetAccountId, cashAccountId) => ({
  description: "Computer equipment purchase",
  date: new Date().toISOString(),
  entries: [
    {
      accountId: assetAccountId, // Computer Equipment (Asset)
      entryType: "DEBIT",
      amount: amount,
      description: "Computer purchase"
    },
    {
      accountId: cashAccountId, // Cash
      entryType: "CREDIT",
      amount: amount,
      description: "Cash payment"
    }
  ]
});

4. Liability Recording

const createLoanTransaction = (amount, cashAccountId, loanAccountId) => ({
  description: "Bank loan received",
  date: new Date().toISOString(),
  entries: [
    {
      accountId: cashAccountId, // Cash
      entryType: "DEBIT",
      amount: amount,
      description: "Loan proceeds"
    },
    {
      accountId: loanAccountId, // Bank Loan (Liability)
      entryType: "CREDIT",
      amount: amount,
      description: "Loan liability"
    }
  ]
});

Transaction Status

Transaction Status Types

enum TransactionStatus {
  PENDING   # Transaction is created but not yet posted
  POSTED    # Transaction is posted to the ledger
  FAILED    # Transaction failed validation
  REVERSED  # Transaction has been reversed
}

Posting Transactions

Transactions can be posted to make them part of the official ledger:

mutation PostTransaction($id: ID!) {
  # Note: Posting is handled internally by the REST API
  # when transactions are created through proper channels
  # Direct posting via GraphQL not currently exposed
}

Transaction Validation

Balance Validation

Transactions must have equal debits and credits:

const validateTransactionBalance = (entries) => {
  const debitTotal = entries
    .filter(e => e.entryType === 'DEBIT')
    .reduce((sum, e) => sum + parseFloat(e.amount), 0);

  const creditTotal = entries
    .filter(e => e.entryType === 'CREDIT')
    .reduce((sum, e) => sum + parseFloat(e.amount), 0);

  if (Math.abs(debitTotal - creditTotal) > 0.01) {
    throw new Error(`Transaction not balanced. Debits: ${debitTotal}, Credits: ${creditTotal}`);
  }

  return true;
};

Account Validation

All referenced accounts must exist and be active:

const validateTransactionAccounts = async (entries) => {
  for (const entry of entries) {
    const account = await getAccount(entry.accountId);
    if (!account) {
      throw new Error(`Account ${entry.accountId} not found`);
    }
    if (account.status !== 'ACTIVE') {
      throw new Error(`Account ${account.code} is not active`);
    }
  }
  return true;
};

Journal Entry Management

Journal Entry Types

enum EntryType {
  DEBIT   # Increases asset/expense accounts, decreases liability/equity/revenue accounts
  CREDIT  # Increases liability/equity/revenue accounts, decreases asset/expense accounts
}

Account Type Behavior

Account TypeDebit EffectCredit Effect
AssetIncreaseDecrease
LiabilityDecreaseIncrease
EquityDecreaseIncrease
RevenueDecreaseIncrease
ExpenseIncreaseDecrease

Transaction Queries

Transaction History

Get paginated transaction history:

query GetTransactionHistory($limit: Int, $offset: Int) {
  organization {
    recentTransactions(limit: $limit) {
      id
      description
      date
      amount
      status
      entries {
        accountId
        entryType
        amount
        description
      }
    }
  }
}

Transactions by Date Range

query GetTransactionsByDate($startDate: DateTime!, $endDate: DateTime!) {
  organization {
    recentTransactions(limit: 1000) {
      id
      description
      date
      amount
      entries {
        accountId
        entryType
        amount
      }
    }
  }
}
# Note: Date filtering would be implemented in the resolver

Transactions by Account

query GetAccountActivity($accountId: ID!) {
  account(id: $accountId) {
    id
    code
    name
    transactions(limit: 100) {
      id
      description
      date
      amount
      status
      entries {
        entryType
        amount
        description
      }
    }
  }
}

Transactions by Amount

query GetLargeTransactions {
  organization {
    recentTransactions(limit: 100) {
      id
      description
      date
      amount
      entries {
        accountId
        entryType
        amount
      }
    }
  }
}
# Filter for amount > 1000 would be implemented in client

Transaction Reporting

General Ledger

Get all transactions for a date range:

query GetGeneralLedger($startDate: DateTime!, $endDate: DateTime!) {
  organization {
    recentTransactions(limit: 1000) {
      id
      description
      date
      amount
      entries {
        accountId
        entryType
        amount
        description
        reference
      }
    }
  }
}

Account Ledger

Get detailed transaction history for a specific account:

query GetAccountLedger($accountId: ID!) {
  account(id: $accountId) {
    id
    code
    name
    balance {
      amount
      formatted
    }
    transactions(limit: 500) {
      id
      description
      date
      amount
      status
      entries {
        entryType
        amount
        description
        reference
        createdAt
      }
    }
  }
}

Transaction Reversals

Reversing Entries

Transactions can be reversed with opposite entries:

const createReversalTransaction = (originalTransaction) => {
  const reversedEntries = originalTransaction.entries.map(entry => ({
    accountId: entry.accountId,
    entryType: entry.entryType === 'DEBIT' ? 'CREDIT' : 'DEBIT',
    amount: entry.amount,
    description: `Reversal of: ${entry.description}`,
    reference: `REV-${originalTransaction.id}`
  }));

  return {
    description: `Reversal: ${originalTransaction.description}`,
    date: new Date().toISOString(),
    entries: reversedEntries
  };
};

Multi-Currency Transactions

Currency Handling

Transactions can involve multiple currencies:

query GetMultiCurrencyTransactions {
  organization {
    recentTransactions(limit: 50) {
      id
      description
      date
      amount
      currency {
        code
        name
      }
      entries {
        accountId
        entryType
        amount
        currency {
          code
        }
        baseAmount
        description
      }
    }
  }
}

Exchange Rate Application

Base currency amounts are calculated using exchange rates:

const calculateBaseAmount = (amount, fromCurrency, toCurrency, exchangeRate) => {
  if (fromCurrency === toCurrency) {
    return amount;
  }

  // Apply exchange rate
  const baseAmount = amount * exchangeRate;

  // Round to base currency precision
  return Math.round(baseAmount * 100) / 100;
};

Error Handling

Transaction Creation Errors

  • VALIDATION_ERROR: Unbalanced debits/credits, invalid accounts
  • NOT_FOUND: Referenced accounts don't exist
  • FORBIDDEN: Insufficient permissions

Transaction Query Errors

  • NOT_FOUND: Transaction not found
  • FORBIDDEN: Access denied

Journal Entry Errors

  • VALIDATION_ERROR: Invalid entry data
  • NOT_FOUND: Account not found

Best Practices

Transaction Descriptions

Use clear, descriptive transaction descriptions:

const formatTransactionDescription = (type, details) => {
  const templates = {
    invoice: `Invoice ${details.number} - ${details.customer}`,
    payment: `Payment received - ${details.method} ${details.reference}`,
    expense: `${details.category} - ${details.vendor}`,
    journal: details.description
  };

  return templates[type] || details.description;
};

Entry Descriptions

Provide context for each journal entry:

const formatEntryDescription = (transactionType, entry, transaction) => {
  const templates = {
    invoice: entry.entryType === 'DEBIT'
      ? `Customer invoice ${transaction.reference}`
      : `Revenue from services`,
    payment: entry.entryType === 'CREDIT'
      ? `Payment received ${transaction.reference}`
      : `Accounts receivable reduction`,
    expense: entry.entryType === 'DEBIT'
      ? `Expense incurred ${transaction.reference}`
      : `Cash/payment reduction`
  };

  return templates[transactionType] || entry.description;
};

Transaction Batching

Group related transactions:

const createMonthlyEntries = async (entries) => {
  const transactions = entries.map(entry =>
    createTransaction({
      description: entry.description,
      date: entry.date,
      entries: entry.journalEntries
    })
  );

  // Execute all at once
  return await Promise.all(transactions);
};

Audit Trail

Maintain proper audit information:

const createAuditableTransaction = (transactionData, userId) => ({
  ...transactionData,
  metadata: {
    createdBy: userId,
    source: 'graphql_api',
    timestamp: new Date().toISOString(),
    version: '1.0'
  }
});

Transaction Validation

Comprehensive validation before submission:

const validateCompleteTransaction = async (transaction) => {
  // Check balance
  if (!validateTransactionBalance(transaction.entries)) {
    throw new Error('Transaction debits and credits do not balance');
  }

  // Check accounts exist and are active
  await validateTransactionAccounts(transaction.entries);

  // Check date is reasonable
  const transactionDate = new Date(transaction.date);
  const today = new Date();
  const thirtyDaysAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);

  if (transactionDate < thirtyDaysAgo || transactionDate > today) {
    throw new Error('Transaction date is outside reasonable range');
  }

  // Check amounts are positive
  for (const entry of transaction.entries) {
    if (parseFloat(entry.amount) <= 0) {
      throw new Error('All entry amounts must be positive');
    }
  }

  return true;
};

Need help?

Create a free account to access our support portal. Once signed in, use the Support tab in your dashboard to submit a support ticket — our team typically responds within 24 hours.