GraphQL Contacts

This page documents GraphQL operations for managing customer and vendor contacts in Crane Ledger.

Contact Queries

Get Contact by ID

Retrieve a specific contact by unique identifier.

query GetContact($id: ID!) {
  contact(id: $id) {
    id
    name
    contactType
    email
    phone
    taxId
    billingAddress
    billingCity
    billingState
    billingPostalCode
    billingCountry
    createdAt
    updatedAt
    metadata
  }
}

Arguments:

  • id: ID! - Contact unique identifier

Returns:

  • Complete contact information
  • Billing address details
  • Contact type and metadata

List Organization Contacts

Get all contacts for the organization.

query GetAllContacts {
  organization {
    contacts {
      id
      name
      contactType
      email
      phone
      billingCountry
      createdAt
    }
  }
}

Filter Contacts by Type

Get contacts of specific types.

query GetCustomers {
  organization {
    contacts(where: { contactType: { eq: CUSTOMER } }) {
      id
      name
      email
      phone
      billingAddress
      billingCity
      billingCountry
    }
  }
}

query GetVendors {
  organization {
    contacts(where: { contactType: { eq: VENDOR } }) {
      id
      name
      email
      phone
      billingAddress
      billingCity
      billingCountry
    }
  }
}

Contact Relationships

Contact Transactions

Get transactions involving a specific contact.

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

Arguments:

  • limit: Int - Maximum transactions to return (default: 50)

Contact Invoices

Get all invoices for a contact.

query GetContactInvoices($id: ID!) {
  contact(id: $id) {
    id
    name
    invoices {
      id
      invoiceNumber
      invoiceDate
      dueDate
      status
      subtotal
      taxAmount
      total
      currency {
        code
        symbol
      }
      items {
        description
        quantity
        unitPrice
        lineTotal
      }
    }
  }
}

Contact Bills

Get all bills for a contact.

query GetContactBills($id: ID!) {
  contact(id: $id) {
    id
    name
    bills {
      id
      billNumber
      billDate
      dueDate
      status
      subtotal
      taxAmount
      total
      currency {
        code
        symbol
      }
      items {
        description
        quantity
        unitPrice
        lineTotal
      }
    }
  }
}

Contact Mutations

Create Contact

Add a new contact to the organization.

mutation CreateContact(
  $name: String!
  $contactType: ContactType!
  $email: String
  $phone: String
  $taxId: String
) {
  createContact(
    name: $name
    contactType: $contactType
    email: $email
    phone: $phone
    taxId: $taxId
  ) {
    id
    name
    contactType
    email
    phone
    taxId
    createdAt
  }
}

Required Fields:

  • name: Contact display name
  • contactType: CUSTOMER, VENDOR, or EMPLOYEE

Optional Fields:

  • email: Email address
  • phone: Phone number
  • taxId: Tax ID or VAT number

Update Contact

Modify an existing contact.

mutation UpdateContact(
  $id: ID!
  $name: String
  $email: String
  $phone: String
  $taxId: String
) {
  updateContact(
    id: $id
    name: $name
    email: $email
    phone: $phone
    taxId: $taxId
  ) {
    id
    name
    email
    phone
    taxId
    updatedAt
  }
}

Delete Contact

Remove a contact from the organization.

mutation DeleteContact($id: ID!) {
  deleteContact(id: $id)
}

Contact Types

Contact Type Enum

enum ContactType {
  CUSTOMER  # Customers who buy from you
  VENDOR    # Suppliers you buy from
  EMPLOYEE  # Company employees
}

Type-Specific Operations

// Customer-specific operations
const getCustomerInvoices = async (customerId) => {
  const query = `
    query GetCustomerInvoices($id: ID!) {
      contact(id: $id) {
        name
        invoices {
          id
          invoiceNumber
          total
          status
          dueDate
        }
      }
    }
  `;

  return await graphql.request(query, { id: customerId });
};

// Vendor-specific operations
const getVendorBills = async (vendorId) => {
  const query = `
    query GetVendorBills($id: ID!) {
      contact(id: $id) {
        name
        bills {
          id
          billNumber
          total
          status
          dueDate
        }
      }
    }
  `;

  return await graphql.request(query, { id: vendorId });
};

Contact Validation

Contact Name Validation

const validateContactName = (name) => {
  if (!name || name.trim().length === 0) {
    throw new Error('Contact name is required');
  }

  if (name.length > 255) {
    throw new Error('Contact name must be less than 255 characters');
  }

  // Check for valid characters
  const validNameRegex = /^[a-zA-Z0-9\s\-'&.,()]+$/;
  if (!validNameRegex.test(name)) {
    throw new Error('Contact name contains invalid characters');
  }

  return true;
};

Email Validation

const validateEmail = (email) => {
  if (!email) return true; // Email is optional

  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    throw new Error('Invalid email format');
  }

  if (email.length > 255) {
    throw new Error('Email must be less than 255 characters');
  }

  return true;
};

Phone Validation

const validatePhone = (phone) => {
  if (!phone) return true; // Phone is optional

  // Remove all non-digit characters
  const digitsOnly = phone.replace(/\D/g, '');

  // Check for valid length (10-15 digits for international)
  if (digitsOnly.length < 10 || digitsOnly.length > 15) {
    throw new Error('Phone number must be 10-15 digits');
  }

  return true;
};

Tax ID Validation

const validateTaxId = (taxId, country = 'US') => {
  if (!taxId) return true; // Tax ID is optional

  // US EIN format: XX-XXXXXXX
  if (country === 'US') {
    const einRegex = /^\d{2}-\d{7}$/;
    if (!einRegex.test(taxId)) {
      throw new Error('US Tax ID must be in format XX-XXXXXXX');
    }
  }

  // EU VAT format: Country code + number
  if (['DE', 'FR', 'GB', 'IT', 'ES'].includes(country)) {
    const vatRegex = /^[A-Z]{2}\d{8,12}$/;
    if (!vatRegex.test(taxId)) {
      throw new Error('EU VAT number must start with country code followed by 8-12 digits');
    }
  }

  return true;
};

Address Management

Address Fields

Contacts can have billing addresses:

type ContactNode {
  billingAddress: String
  billingCity: String
  billingState: String
  billingPostalCode: String
  billingCountry: String
}

Address Validation

const validateAddress = (address) => {
  const required = ['billingAddress', 'billingCity', 'billingCountry'];

  for (const field of required) {
    if (!address[field] || address[field].trim().length === 0) {
      throw new Error(`${field} is required for complete address`);
    }
  }

  // Postal code validation by country
  if (address.billingCountry === 'US') {
    const usZipRegex = /^\d{5}(-\d{4})?$/;
    if (!usZipRegex.test(address.billingPostalCode || '')) {
      throw new Error('US postal code must be in format 12345 or 12345-6789');
    }
  }

  return true;
};

Complete Contact Creation

const createCompleteContact = async (contactData) => {
  // Validate all fields
  validateContactName(contactData.name);
  validateEmail(contactData.email);
  validatePhone(contactData.phone);
  validateTaxId(contactData.taxId, contactData.billingCountry);

  if (contactData.billingAddress) {
    validateAddress(contactData);
  }

  // Create contact
  const mutation = `
    mutation CreateContact($input: CreateContactInput!) {
      createContact(input: $input) {
        id
        name
        contactType
        email
        phone
        taxId
        billingAddress
        billingCity
        billingState
        billingPostalCode
        billingCountry
      }
    }
  `;

  return await graphql.request(mutation, { input: contactData });
};

Contact Analytics

Contact Summary

Get summary statistics for contacts.

query GetContactSummary {
  organization {
    contacts {
      contactType
      createdAt
    }
  }
}
# Process on client side to count by type, etc.

Active Contacts

Get contacts with recent activity.

query GetActiveContacts {
  organization {
    contacts {
      id
      name
      contactType
      transactions(limit: 1) {
        id
        date
      }
      invoices {
        id
        status
        createdAt
      }
      bills {
        id
        status
        createdAt
      }
    }
  }
}

Contact Aging

Get aging information for customer balances.

query GetCustomerAging {
  organization {
    contacts(where: { contactType: { eq: CUSTOMER } }) {
      id
      name
      transactions(limit: 100) {
        id
        description
        date
        amount
        entries {
          accountId
          entryType
          amount
        }
      }
    }
  }
}
# Process transactions to calculate aging buckets

Contact Import/Export

Bulk Contact Creation

const createBulkContacts = async (contacts) => {
  const mutations = contacts.map((contact, index) => `
    contact${index}: createContact(
      name: "${contact.name}"
      contactType: ${contact.contactType}
      email: "${contact.email || ''}"
      phone: "${contact.phone || ''}"
    ) {
      id
      name
    }
  `);

  const mutation = `
    mutation CreateBulkContacts {
      ${mutations.join('\n')}
    }
  `;

  return await graphql.request(mutation);
};

Contact Export

query ExportContacts {
  organization {
    contacts {
      id
      name
      contactType
      email
      phone
      taxId
      billingAddress
      billingCity
      billingState
      billingPostalCode
      billingCountry
      createdAt
      updatedAt
    }
  }
}

Error Handling

Contact Creation Errors

  • VALIDATION_ERROR: Invalid contact data
  • CONFLICT: Contact with same name/email already exists

Contact Update Errors

  • NOT_FOUND: Contact not found
  • VALIDATION_ERROR: Invalid contact data
  • FORBIDDEN: Insufficient permissions

Contact Deletion Errors

  • NOT_FOUND: Contact not found
  • VALIDATION_ERROR: Contact has active transactions/invoices

Best Practices

Contact Naming Conventions

const formatContactName = (name, contactType) => {
  // Capitalize properly
  const capitalized = name.toLowerCase()
    .split(' ')
    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
    .join(' ');

  // Add type indicators for clarity
  if (contactType === 'VENDOR') {
    return `${capitalized} (Vendor)`;
  }

  return capitalized;
};

Contact Categorization

const categorizeContact = (contact) => {
  const categories = {
    VIP: contact.transactions?.length > 100,
    NEW: new Date(contact.createdAt) > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
    INACTIVE: !contact.transactions?.length,
    OVERDUE: contact.invoices?.some(inv => inv.status === 'OVERDUE')
  };

  return Object.keys(categories).filter(key => categories[key]);
};

Duplicate Prevention

const checkDuplicateContact = async (name, email) => {
  const existingByName = await graphql.request(`
    query CheckName($name: String!) {
      organization {
        contacts(where: { name: { eq: $name } }) {
          id
        }
      }
    }
  `, { name });

  if (existingByName.organization.contacts.length > 0) {
    throw new Error('Contact with this name already exists');
  }

  if (email) {
    const existingByEmail = await graphql.request(`
      query CheckEmail($email: String!) {
        organization {
          contacts(where: { email: { eq: $email } }) {
            id
          }
        }
      }
    `, { email });

    if (existingByEmail.organization.contacts.length > 0) {
      throw new Error('Contact with this email already exists');
    }
  }
};

Contact Maintenance

const cleanupContacts = async () => {
  // Find contacts with no activity
  const inactiveContacts = await graphql.request(`
    query GetInactiveContacts {
      organization {
        contacts {
          id
          name
          transactions(limit: 1) {
            id
          }
          invoices {
            id
          }
          bills {
            id
          }
        }
      }
    }
  `);

  // Filter for contacts with no activity
  const toCleanup = inactiveContacts.organization.contacts.filter(contact =>
    contact.transactions.length === 0 &&
    contact.invoices.length === 0 &&
    contact.bills.length === 0
  );

  // Optionally delete or mark as inactive
  for (const contact of toCleanup) {
    console.log(`Inactive contact: ${contact.name} (${contact.id})`);
    // Could delete or update status
  }

  return toCleanup;
};

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.