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 IDbillDate: Bill creation datedueDate: Payment due dateitems: 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 foundVALIDATION_ERROR: Invalid dates, amounts, or itemsFORBIDDEN: Insufficient permissions
Payment Recording Errors
NOT_FOUND: Bill not foundVALIDATION_ERROR: Invalid payment data or overpaymentFORBIDDEN: 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.
- ✨ For LLMs/AI assistants: Read our structured API reference