GraphQL Currencies

This page documents GraphQL operations for managing currencies and exchange rates in Crane Ledger.

Currency Queries

List All Currencies

Get all supported currencies in the system.

query GetCurrencies {
  currencies {
    code
    name
    symbol
    decimalPlaces
  }
}

Returns:

  • All active currencies
  • Currency codes (ISO 4217)
  • Display names and symbols
  • Decimal precision for each currency

Get Currency by Code

Get a specific currency (though this is less common as currencies are system-wide).

query GetCurrency($code: String!) {
  currencies(where: { code: { eq: $code } }) {
    code
    name
    symbol
    decimalPlaces
  }
}

Currency Information

Currency Properties

Each currency has the following properties:

const CURRENCY_PROPERTIES = {
  USD: { name: 'US Dollar', symbol: '$', decimalPlaces: 2 },
  EUR: { name: 'Euro', symbol: '€', decimalPlaces: 2 },
  GBP: { name: 'British Pound', symbol: '£', decimalPlaces: 2 },
  JPY: { name: 'Japanese Yen', symbol: '¥', decimalPlaces: 0 },
  CAD: { name: 'Canadian Dollar', symbol: 'C$', decimalPlaces: 2 },
  AUD: { name: 'Australian Dollar', symbol: 'A$', decimalPlaces: 2 },
  CHF: { name: 'Swiss Franc', symbol: 'Fr', decimalPlaces: 2 },
  CNY: { name: 'Chinese Yuan', symbol: '¥', decimalPlaces: 2 }
};

Decimal Precision

Different currencies have different decimal requirements:

const getCurrencyPrecision = (currencyCode) => {
  const precisionMap = {
    JPY: 0,  // Japanese Yen (no cents)
    KRW: 0,  // Korean Won (no cents)
    VND: 0,  // Vietnamese Dong (no cents)
    BIF: 0,  // Burundian Franc (no cents)
    CLP: 0,  // Chilean Peso (no cents)
    // Most currencies use 2 decimal places
    default: 2
  };

  return precisionMap[currencyCode] || precisionMap.default;
};

Organization Base Currency

Get Organization Currency

Every organization has a base currency for all financial operations.

query GetOrganizationCurrency {
  organization {
    baseCurrency {
      code
      name
      symbol
      decimalPlaces
    }
  }
}

Important:

  • All monetary amounts are stored in the organization's base currency
  • Multi-currency transactions are converted to base currency for storage
  • Reports show amounts in base currency

Currency Conversion

Transactions in foreign currencies are converted to base currency:

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

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

  // Round to base currency precision
  const precision = getCurrencyPrecision(toCurrency);
  const multiplier = Math.pow(10, precision);

  return Math.round(convertedAmount * multiplier) / multiplier;
};

Multi-Currency Transactions

Transaction Currency Handling

Transactions can be recorded in any currency:

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

Fields:

  • amount: Original amount in transaction currency
  • currency: Transaction currency
  • baseAmount: Converted amount in organization's base currency

Currency Exchange Rates

Exchange rates are used for currency conversion:

// Example exchange rate structure
const exchangeRates = {
  'USD/EUR': 0.85,  // 1 USD = 0.85 EUR
  'EUR/USD': 1.18,  // 1 EUR = 1.18 USD
  'GBP/USD': 1.27,  // 1 GBP = 1.27 USD
  'USD/GBP': 0.79,  // 1 USD = 0.79 GBP
};

// Get exchange rate between two currencies
const getExchangeRate = (fromCurrency, toCurrency) => {
  if (fromCurrency === toCurrency) return 1;

  const directRate = exchangeRates[`${fromCurrency}/${toCurrency}`];
  if (directRate) return directRate;

  // Try inverse rate
  const inverseRate = exchangeRates[`${toCurrency}/${fromCurrency}`];
  if (inverseRate) return 1 / inverseRate;

  // Try cross rates through USD
  const toUSD = exchangeRates[`${fromCurrency}/USD`] || (1 / exchangeRates[`USD/${fromCurrency}`]);
  const fromUSD = exchangeRates[`USD/${toCurrency}`] || (1 / exchangeRates[`${toCurrency}/USD`]);

  return toUSD * fromUSD;
};

Currency Formatting

Localized Formatting

Format amounts according to currency conventions:

const formatCurrency = (amount, currencyCode) => {
  const currency = getCurrencyInfo(currencyCode);
  const precision = getCurrencyPrecision(currencyCode);

  // Format with proper decimal places
  const formattedAmount = amount.toFixed(precision);

  // Add currency symbol in correct position
  const symbolPosition = getSymbolPosition(currencyCode);

  if (symbolPosition === 'before') {
    return `${currency.symbol}${formattedAmount}`;
  } else {
    return `${formattedAmount} ${currency.symbol}`;
  }
};

const getSymbolPosition = (currencyCode) => {
  // Most currencies put symbol before amount
  const afterSymbolCurrencies = ['EUR', 'GBP', 'CHF'];
  return afterSymbolCurrencies.includes(currencyCode) ? 'after' : 'before';
};

Examples

formatCurrency(1234.56, 'USD'); // "$1234.56"
formatCurrency(1234.56, 'EUR'); // "1234.56 €"
formatCurrency(1234.56, 'JPY'); // "¥1235" (rounded to 0 decimals)
formatCurrency(1234.56, 'BHD'); // ".د.ب1234.560" (3 decimals for Bahrain Dinar)

Currency Validation

Supported Currencies

Only predefined currencies are supported:

const SUPPORTED_CURRENCIES = [
  'USD', 'EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'CHF', 'CNY',
  'SEK', 'NZD', 'MXN', 'SGD', 'HKD', 'NOK', 'KRW', 'TRY',
  'RUB', 'INR', 'BRL', 'ZAR', 'EGP', 'THB', 'IDR', 'MYR',
  'PHP', 'VND', 'CZK', 'PLN', 'HUF', 'ISK', 'RON', 'HRK',
  'DKK', 'BGN', 'EEK', 'LVL', 'LTL', 'MTL', 'CYP', 'SKK'
];

const isSupportedCurrency = (currencyCode) => {
  return SUPPORTED_CURRENCIES.includes(currencyCode.toUpperCase());
};

Currency Code Validation

Validate currency codes using ISO 4217 standard:

const validateCurrencyCode = (code) => {
  if (!code || typeof code !== 'string') {
    throw new Error('Currency code must be a non-empty string');
  }

  if (code.length !== 3) {
    throw new Error('Currency code must be exactly 3 characters');
  }

  if (!/^[A-Z]{3}$/.test(code)) {
    throw new Error('Currency code must contain only uppercase letters');
  }

  if (!isSupportedCurrency(code)) {
    throw new Error(`Currency code ${code} is not supported`);
  }

  return true;
};

Exchange Rate Management

Current Exchange Rates

Get current exchange rates (when implemented):

query GetExchangeRates {
  exchangeRates {
    fromCurrency
    toCurrency
    rate
    effectiveDate
    source
  }
}

Rate Update Process

Exchange rates are typically updated from external sources:

const updateExchangeRates = async () => {
  // Fetch rates from external API
  const rates = await fetchExchangeRatesFromAPI();

  // Validate rates
  validateExchangeRates(rates);

  // Store in database
  await storeExchangeRates(rates);

  // Update cached rates
  await updateRateCache(rates);

  return rates;
};

const validateExchangeRates = (rates) => {
  for (const rate of rates) {
    if (rate.rate <= 0) {
      throw new Error(`Invalid exchange rate for ${rate.fromCurrency}/${rate.toCurrency}: ${rate.rate}`);
    }

    if (rate.rate > 1000) {
      throw new Error(`Exchange rate seems unreasonably high: ${rate.rate}`);
    }
  }
};

Multi-Currency Reporting

Currency Conversion in Reports

Reports show amounts in base currency, but can include original currencies:

query GetMultiCurrencyReport {
  organization {
    transactions(limit: 100) {
      id
      description
      amount
      currency {
        code
      }
      baseAmount
      entries {
        accountId
        amount
        baseAmount
        currency {
          code
        }
      }
    }
  }
}

Foreign Currency Balances

Track balances in foreign currencies:

const getForeignCurrencyBalances = async () => {
  const transactions = await getAllTransactions();

  const balances = {};

  for (const transaction of transactions) {
    const currency = transaction.currency.code;

    if (currency !== getBaseCurrency()) {
      balances[currency] = (balances[currency] || 0) + transaction.amount;
    }
  }

  return balances;
};

Currency Risk Management

Exposure Analysis

Analyze foreign currency exposure:

const analyzeCurrencyExposure = async () => {
  const foreignBalances = await getForeignCurrencyBalances();

  const exposure = {
    currencies: Object.keys(foreignBalances),
    totalExposure: 0,
    largestExposure: null,
    exposureByCurrency: {}
  };

  for (const [currency, balance] of Object.entries(foreignBalances)) {
    const baseAmount = convertToBaseCurrency(balance, currency, getBaseCurrency());
    exposure.exposureByCurrency[currency] = baseAmount;
    exposure.totalExposure += Math.abs(baseAmount);

    if (!exposure.largestExposure ||
        Math.abs(baseAmount) > Math.abs(exposure.largestExposure.amount)) {
      exposure.largestExposure = { currency, amount: baseAmount };
    }
  }

  return exposure;
};

Hedging Recommendations

Generate hedging recommendations based on exposure:

const generateHedgingRecommendations = (exposure) => {
  const recommendations = [];

  // Recommend hedging if exposure > 10% of total assets
  const totalAssets = getTotalAssets();
  const exposureThreshold = totalAssets * 0.1;

  if (exposure.totalExposure > exposureThreshold) {
    recommendations.push({
      type: 'hedge',
      message: `Consider hedging ${exposure.largestExposure.currency} exposure of ${formatCurrency(exposure.largestExposure.amount)}`,
      action: 'forward_contract'
    });
  }

  // Recommend diversification if single currency exposure > 50% of total exposure
  const largestPercentage = Math.abs(exposure.largestExposure.amount) / exposure.totalExposure;
  if (largestPercentage > 0.5) {
    recommendations.push({
      type: 'diversify',
      message: `High concentration in ${exposure.largestExposure.currency}. Consider diversifying currency exposure.`,
      action: 'currency_basket'
    });
  }

  return recommendations;
};

Currency in Financial Reports

Balance Sheet Currency

Balance sheets show values in base currency:

query GetBalanceSheet {
  organization {
    balanceSheet {
      generatedAt
      totalAssets
      totalLiabilities
      totalEquity
      # All amounts in base currency
    }
  }
}

Income Statement Currency

Income statements show revenues and expenses in base currency:

query GetIncomeStatement {
  organization {
    incomeStatement {
      totalRevenue
      totalExpenses
      netIncome
      # All amounts in base currency
    }
  }
}

Multi-Currency Footnotes

Reports can include currency information:

const addCurrencyFootnotes = (report) => {
  const foreignCurrencies = getForeignCurrenciesUsed();

  if (foreignCurrencies.length > 0) {
    report.footnotes = report.footnotes || [];
    report.footnotes.push({
      type: 'currency',
      text: `Report amounts are in ${getBaseCurrency()}. Foreign currency transactions were converted using period-end exchange rates.`,
      currencies: foreignCurrencies
    });
  }

  return report;
};

Error Handling

Currency Errors

  • VALIDATION_ERROR: Invalid currency code
  • NOT_FOUND: Currency not supported
  • INTERNAL_ERROR: Exchange rate unavailable

Conversion Errors

  • VALIDATION_ERROR: Invalid exchange rate
  • INTERNAL_ERROR: Currency conversion failed

Best Practices

Currency Selection

Choose base currency carefully:

const recommendBaseCurrency = (businessInfo) => {
  const { country, industry, customerBase } = businessInfo;

  // For US businesses, USD is obvious choice
  if (country === 'US') return 'USD';

  // For EU businesses, EUR is common
  if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(country)) return 'EUR';

  // For international businesses, consider USD or EUR
  if (customerBase === 'global') return 'USD';

  // Default to USD for most businesses
  return 'USD';
};

Exchange Rate Updates

Keep exchange rates current:

const EXCHANGE_RATE_UPDATE_SCHEDULE = {
  major: '0 */4 * * *',    // Every 4 hours for major currencies
  minor: '0 9 * * 1-5',    // Weekdays at 9 AM for minor currencies
  weekend: '0 12 * * 0,6'  // Weekends at noon
};

const scheduleRateUpdates = async () => {
  // Update major currency pairs frequently
  await scheduleJob('update_major_rates', EXCHANGE_RATE_UPDATE_SCHEDULE.major);

  // Update minor currencies daily
  await scheduleJob('update_minor_rates', EXCHANGE_RATE_UPDATE_SCHEDULE.minor);

  // Update on weekends
  await scheduleJob('update_weekend_rates', EXCHANGE_RATE_UPDATE_SCHEDULE.weekend);
};

Currency Audit Trail

Maintain audit trail for currency operations:

const logCurrencyOperation = async (operation, details) => {
  const auditEntry = {
    operation,
    timestamp: new Date().toISOString(),
    user: getCurrentUser(),
    details,
    exchangeRates: await getCurrentExchangeRates()
  };

  await writeToAuditLog(auditEntry);
};

// Example usage
await logCurrencyOperation('transaction_conversion', {
  transactionId: 'TXN_123',
  fromCurrency: 'EUR',
  toCurrency: 'USD',
  originalAmount: 1000,
  convertedAmount: 1180,
  exchangeRate: 1.18
});

Rounding Policies

Implement consistent rounding for currency operations:

const ROUNDING_POLICIES = {
  bank: 'round_half_up',     // Standard banking round
  cash: 'round_half_up',     // For cash transactions
  tax: 'round_half_up',      // For tax calculations
  report: 'round_half_up'    // For financial reports
};

const roundCurrency = (amount, currencyCode, context = 'bank') => {
  const policy = ROUNDING_POLICIES[context];
  const precision = getCurrencyPrecision(currencyCode);

  switch (policy) {
    case 'round_half_up':
      return Math.round(amount * Math.pow(10, precision)) / Math.pow(10, precision);
    case 'round_down':
      return Math.floor(amount * Math.pow(10, precision)) / Math.pow(10, precision);
    case 'round_up':
      return Math.ceil(amount * Math.pow(10, precision)) / Math.pow(10, precision);
    default:
      return Math.round(amount * Math.pow(10, precision)) / Math.pow(10, precision);
  }
};

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.