GraphQL Bills

This page documents GraphQL operations for managing purchase bills in Crane Ledger.

Bill Queries

Get Bill by ID

Retrieve a specific bill with full details.

query GetBill($id: ID!) {
  bill(id: $id) {
    id
    billNumber
    contactId
    billDate
    dueDate
    status
    subtotal
    taxAmount
    total
    currency {
      code
      name
      symbol
      decimalPlaces
    }
    items {
      id
      description
      quantity
      unitPrice
      lineTotal
      taxAmount
    }
    notes
    createdAt
    updatedAt
    metadata
  }
}

Arguments:

  • id: ID! - Bill unique identifier

Returns:

  • Complete bill information
  • Line items with pricing
  • Tax calculations
  • Status and dates

Get Bill PDF

Get PDF download URL for a bill.

query GetBillPDF($id: ID!) {
  bill(id: $id) {
    id
    billNumber
    pdfUrl
  }
}

Get Bill Payments

Get payment history for a bill.

query GetBillPayments($id: ID!) {
  bill(id: $id) {
    id
    billNumber
    total
    payments {
      id
      amount
      paymentDate
      paymentMethod
      reference
      notes
      createdAt
    }
  }
}

List Organization Bills

Get all bills for the organization.

query GetAllBills {
  organization {
    bills {
      id
      billNumber
      contactId
      billDate
      dueDate
      status
      total
      currency {
        code
        symbol
      }
    }
  }
}

Filter Bills by Status

Get bills with specific statuses.

query GetUnpaidBills {
  organization {
    bills(where: { status: { in: [RECEIVED, PARTIAL, OVERDUE] } }) {
      id
      billNumber
      total
      dueDate
      status
    }
  }
}

query GetPaidBills {
  organization {
    bills(where: { status: { eq: PAID } }) {
      id
      billNumber
      total
      paidDate: createdAt # Approximation
    }
  }
}

Get Contact Bills

Get all bills for a specific contact.

query GetContactBills($contactId: ID!) {
  contact(id: $contactId) {
    id
    name
    bills {
      id
      billNumber
      billDate
      dueDate
      status
      total
      currency {
        code
        symbol
      }
    }
  }
}

Bill Mutations

Create Bill

Create a new purchase bill.

mutation CreateBill(
  $contactId: ID!
  $billDate: DateTime!
  $dueDate: DateTime!
  $items: [BillItemInput!]!
  $notes: String
) {
  createBill(
    contactId: $contactId
    billDate: $billDate
    dueDate: $dueDate
    items: $items
    notes: $notes
  ) {
    id
    billNumber
    contactId
    billDate
    dueDate
    status
    subtotal
    taxAmount
    total
    items {
      id
      description
      quantity
      unitPrice
      lineTotal
      taxAmount
    }
  }
}

Required Fields:

  • contactId: Vendor contact ID
  • billDate: Bill creation date
  • dueDate: Payment due date
  • items: Array of line items

BillItemInput Structure:

{
  description: "Product/Service description"
  quantity: 2.0
  unitPrice: 50.00
  itemId: "ITEM_123" // Optional reference to item catalog
}

Record Bill Payment

Record a payment toward a bill.

mutation RecordBillPayment(
  $billId: ID!
  $amount: Decimal!
  $paymentDate: DateTime!
  $paymentMethod: String!
  $reference: String
  $notes: String
) {
  recordBillPayment(
    billId: $billId
    amount: $amount
    paymentDate: $paymentDate
    paymentMethod: $paymentMethod
    reference: $reference
    notes: $notes
  ) {
    id
    documentId
    amount
    paymentDate
    paymentMethod
    reference
    notes
  }
}

Bill Status Management

Bill Status Types

enum BillStatus {
  DRAFT      # Bill is being prepared
  RECEIVED   # Bill has been received from vendor
  PARTIAL    # Bill has partial payment
  PAID       # Bill is fully paid
  OVERDUE    # Bill is past due date
  CANCELLED  # Bill has been cancelled
}

Status Transitions

const BILL_STATUS_FLOW = {
  DRAFT: ['RECEIVED', 'CANCELLED'],
  RECEIVED: ['PARTIAL', 'PAID', 'OVERDUE', 'CANCELLED'],
  PARTIAL: ['PAID', 'OVERDUE', 'CANCELLED'],
  PAID: [], // Terminal state
  OVERDUE: ['PAID', 'CANCELLED'],
  CANCELLED: [] // Terminal state
};

const canTransitionBillStatus = (fromStatus, toStatus) => {
  return BILL_STATUS_FLOW[fromStatus]?.includes(toStatus) || false;
};

Bill Calculations

Subtotal Calculation

const calculateBillSubtotal = (items) => {
  return items.reduce((total, item) => {
    return total + (parseFloat(item.quantity) * parseFloat(item.unitPrice));
  }, 0);
};

Tax Calculation

const calculateBillTax = (subtotal, taxRate = 0.10) => {
  return subtotal * taxRate;
};

const calculateBillTotal = (subtotal, taxAmount) => {
  return subtotal + taxAmount;
};

Line Item Totals

const calculateBillLineTotal = (quantity, unitPrice) => {
  return parseFloat(quantity) * parseFloat(unitPrice);
};

const calculateBillLineTax = (lineTotal, taxRate = 0.10) => {
  return lineTotal * taxRate;
};

Bill Validation

Date Validation

const validateBillDates = (billDate, dueDate) => {
  const bill = new Date(billDate);
  const due = new Date(dueDate);
  const today = new Date();

  // Bill date cannot be too far in the future
  const thirtyDaysFromNow = new Date(today);
  thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30);

  if (bill > thirtyDaysFromNow) {
    throw new Error('Bill date cannot be more than 30 days in the future');
  }

  // Due date must be after bill date
  if (due <= bill) {
    throw new Error('Due date must be after bill date');
  }

  // Reasonable due date (not more than 180 days)
  const maxDueDate = new Date(bill);
  maxDueDate.setDate(maxDueDate.getDate() + 180);

  if (due > maxDueDate) {
    throw new Error('Due date cannot be more than 180 days from bill date');
  }

  return true;
};

Amount Validation

const validateBillAmounts = (items) => {
  if (!items || items.length === 0) {
    throw new Error('Bill must have at least one item');
  }

  for (const item of items) {
    if (parseFloat(item.quantity) <= 0) {
      throw new Error('Item quantity must be positive');
    }

    if (parseFloat(item.unitPrice) < 0) {
      throw new Error('Item unit price cannot be negative');
    }

    if (!item.description || item.description.trim().length === 0) {
      throw new Error('Item description is required');
    }
  }

  const subtotal = calculateBillSubtotal(items);
  if (subtotal <= 0) {
    throw new Error('Bill subtotal must be positive');
  }

  return true;
};

Contact Validation

const validateBillContact = async (contactId) => {
  const contact = await getContact(contactId);

  if (!contact) {
    throw new Error('Contact not found');
  }

  if (contact.contactType !== 'VENDOR') {
    throw new Error('Bills can only be created for vendors');
  }

  if (contact.status !== 'ACTIVE') {
    throw new Error('Cannot create bill for inactive contact');
  }

  return true;
};

Bill Numbering

Automatic Bill Numbers

Bill numbers are automatically generated:

const generateBillNumber = async () => {
  const currentYear = new Date().getFullYear();
  const lastBill = await getLastBillNumber(currentYear);

  if (lastBill) {
    // Extract sequence number and increment
    const sequence = parseInt(lastBill.split('-')[1]) + 1;
    return `BILL-${currentYear}-${sequence.toString().padStart(4, '0')}`;
  } else {
    // First bill of the year
    return `BILL-${currentYear}-0001`;
  }
};

Bill Processing Workflow

Three-Way Match

Standard accounts payable processing:

const validateThreeWayMatch = async (bill, purchaseOrder, receipt) => {
  // 1. Bill matches purchase order
  if (bill.total !== purchaseOrder.total) {
    throw new Error('Bill amount does not match purchase order');
  }

  // 2. Bill matches receipt
  if (bill.items.length !== receipt.items.length) {
    throw new Error('Bill items do not match received items');
  }

  // 3. Verify quantities and amounts
  for (let i = 0; i < bill.items.length; i++) {
    const billItem = bill.items[i];
    const poItem = purchaseOrder.items[i];
    const receiptItem = receipt.items[i];

    if (billItem.quantity !== poItem.quantity ||
        billItem.quantity !== receiptItem.quantity) {
      throw new Error(`Quantity mismatch for item ${billItem.description}`);
    }
  }

  return true;
};

Approval Workflow

Multi-step approval process:

const APPROVAL_THRESHOLDS = {
  500: ['manager'],
  5000: ['manager', 'director'],
  25000: ['manager', 'director', 'cfo']
};

const getRequiredApprovals = (billTotal) => {
  for (const [threshold, roles] of Object.entries(APPROVAL_THRESHOLDS)) {
    if (billTotal >= parseFloat(threshold)) {
      return roles;
    }
  }
  return []; // No approval required
};

const canApproveBill = (bill, userRoles) => {
  const requiredRoles = getRequiredApprovals(bill.total);
  return requiredRoles.every(role => userRoles.includes(role));
};

Payment Processing

Bill Payment Methods

Common payment methods for bills:

const BILL_PAYMENT_METHODS = [
  'Check',
  'Wire Transfer',
  'ACH',
  'Credit Card',
  'Cash',
  'PayPal',
  'Stripe',
  'Other'
];

Payment Validation

const validateBillPayment = (bill, paymentAmount) => {
  if (paymentAmount <= 0) {
    throw new Error('Payment amount must be positive');
  }

  // Check if payment exceeds outstanding balance
  const outstanding = parseFloat(bill.total) - getTotalBillPayments(bill);
  if (paymentAmount > outstanding) {
    throw new Error(`Payment amount cannot exceed outstanding balance of ${outstanding}`);
  }

  return true;
};

Partial Payments

Handle partial payments on bills:

const recordPartialBillPayment = async (billId, paymentAmount, paymentData) => {
  const bill = await getBill(billId);
  const outstanding = calculateBillOutstandingBalance(bill);

  if (paymentAmount < outstanding) {
    // This is a partial payment
    await updateBillStatus(billId, 'PARTIAL');
  } else if (paymentAmount === outstanding) {
    // This completes the bill
    await updateBillStatus(billId, 'PAID');
  }

  return await recordBillPayment({
    billId,
    amount: paymentAmount,
    ...paymentData
  });
};

Bill Reporting

Accounts Payable Aging

Get aging information for outstanding bills:

query GetBillAging {
  organization {
    bills(where: { status: { in: [RECEIVED, PARTIAL, OVERDUE] } }) {
      id
      billNumber
      contactId
      total
      dueDate
      payments {
        amount
        paymentDate
      }
    }
  }
}
# Process on client to calculate aging buckets

Vendor Payment Report

Get payment information by vendor:

query GetVendorPayments($vendorId: ID!) {
  contact(id: $vendorId) {
    name
    bills {
      id
      billNumber
      total
      status
      payments {
        amount
        paymentDate
        paymentMethod
      }
    }
  }
}

Cash Flow Impact

Get bills affecting cash flow:

query GetCashFlowBills($startDate: DateTime!, $endDate: DateTime!) {
  organization {
    bills(where: {
      dueDate: { gte: $startDate, lte: $endDate }
      status: { ne: PAID }
    }) {
      id
      billNumber
      total
      dueDate
      contactId
      status
    }
  }
}

Error Handling

Bill Creation Errors

  • NOT_FOUND: Contact not found
  • VALIDATION_ERROR: Invalid dates, amounts, or items
  • FORBIDDEN: Insufficient permissions

Payment Recording Errors

  • NOT_FOUND: Bill not found
  • VALIDATION_ERROR: Invalid payment data or overpayment
  • FORBIDDEN: Insufficient permissions

Best Practices

Bill Creation Workflow

const createBillWorkflow = async (vendorId, items) => {
  try {
    // 1. Validate vendor
    const vendor = await getVendor(vendorId);
    if (!vendor) throw new Error('Vendor not found');

    // 2. Create bill
    const bill = await createStandardBill(vendorId, items);

    // 3. Check approval requirements
    const requiredApprovals = getRequiredApprovals(bill.total);
    if (requiredApprovals.length > 0) {
      await submitForApproval(bill.id, requiredApprovals);
    }

    // 4. Log activity
    console.log(`Bill ${bill.billNumber} created for ${vendor.name}`);

    return bill;

  } catch (error) {
    console.error('Bill creation failed:', error);
    throw error;
  }
};

Standard Bill Creation

const createStandardBill = async (vendorId, items, dueDays = 30) => {
  const billDate = new Date();
  const dueDate = new Date(billDate);
  dueDate.setDate(dueDate.getDate() + dueDays);

  // Validate inputs
  await validateBillContact(vendorId);
  validateBillDates(billDate.toISOString(), dueDate.toISOString());
  validateBillAmounts(items);

  // Calculate totals
  const subtotal = calculateBillSubtotal(items);
  const taxAmount = calculateBillTax(subtotal);
  const total = calculateBillTotal(subtotal, taxAmount);

  // Create bill
  const mutation = `
    mutation CreateBill($input: CreateBillInput!) {
      createBill(input: $input) {
        id
        billNumber
        total
        status
      }
    }
  `;

  const variables = {
    input: {
      contactId: vendorId,
      billDate: billDate.toISOString(),
      dueDate: dueDate.toISOString(),
      items: items,
      notes: `Bill created on ${billDate.toLocaleDateString()}`
    }
  };

  return await graphql.request(mutation, variables);
};

Payment Application

const applyPaymentToBill = async (billId, paymentData) => {
  try {
    // Get bill details
    const bill = await getBill(billId);
    if (!bill) throw new Error('Bill not found');

    // Validate payment
    validateBillPayment(bill, paymentData.amount);

    // Record payment
    const payment = await recordBillPayment({
      billId,
      ...paymentData
    });

    // Update bill status
    const outstanding = calculateBillOutstandingBalance(bill);
    if (outstanding <= 0) {
      await updateBillStatus(billId, 'PAID');
    } else if (getTotalBillPayments(bill) > 0) {
      await updateBillStatus(billId, 'PARTIAL');
    }

    return payment;

  } catch (error) {
    console.error('Payment application failed:', error);
    throw error;
  }
};

Bill Maintenance

const updateOverdueBills = async () => {
  const today = new Date();

  // Get received/partial bills
  const bills = await getBillsByStatus(['RECEIVED', 'PARTIAL']);

  for (const bill of bills) {
    const dueDate = new Date(bill.dueDate);

    if (today > dueDate && bill.status !== 'PAID') {
      // Mark as overdue
      await updateBillStatus(bill.id, 'OVERDUE');

      // Send vendor notification
      await notifyVendorOfOverdueBill(bill.id);
    }
  }
};

Vendor Performance

Track vendor payment terms and performance:

const calculateVendorMetrics = async (vendorId) => {
  const bills = await getVendorBills(vendorId);

  const paidBills = bills.filter(bill => bill.status === 'PAID');
  const overdueBills = bills.filter(bill => bill.status === 'OVERDUE');

  const averagePayTime = paidBills.reduce((total, bill) => {
    const payDate = new Date(getLatestPaymentDate(bill));
    const dueDate = new Date(bill.dueDate);
    const daysDiff = (payDate - dueDate) / (1000 * 60 * 60 * 24);
    return total + daysDiff;
  }, 0) / paidBills.length;

  return {
    totalBills: bills.length,
    paidBills: paidBills.length,
    overdueBills: overdueBills.length,
    averagePayTime,
    overduePercentage: (overdueBills.length / bills.length) * 100
  };
};

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.