GraphQL Invoices
This page documents GraphQL operations for managing sales invoices in Crane Ledger.
Invoice Queries
Get Invoice by ID
Retrieve a specific invoice with full details.
query GetInvoice($id: ID!) {
invoice(id: $id) {
id
invoiceNumber
contactId
invoiceDate
dueDate
status
subtotal
taxAmount
total
currency {
code
name
symbol
decimalPlaces
}
items {
id
description
quantity
unitPrice
lineTotal
taxAmount
}
notes
createdAt
updatedAt
metadata
}
}
Arguments:
id: ID!- Invoice unique identifier
Returns:
- Complete invoice information
- Line items with pricing
- Tax calculations
- Status and dates
Get Invoice PDF
Get PDF download URL for an invoice.
query GetInvoicePDF($id: ID!) {
invoice(id: $id) {
id
invoiceNumber
pdfUrl
}
}
Get Invoice Payments
Get payment history for an invoice.
query GetInvoicePayments($id: ID!) {
invoice(id: $id) {
id
invoiceNumber
total
payments {
id
amount
paymentDate
paymentMethod
reference
notes
createdAt
}
}
}
List Organization Invoices
Get all invoices for the organization.
query GetAllInvoices {
organization {
invoices {
id
invoiceNumber
contactId
invoiceDate
dueDate
status
total
currency {
code
symbol
}
}
}
}
Filter Invoices by Status
Get invoices with specific statuses.
query GetUnpaidInvoices {
organization {
invoices(where: { status: { in: [SENT, VIEWED, OVERDUE] } }) {
id
invoiceNumber
total
dueDate
status
}
}
}
query GetPaidInvoices {
organization {
invoices(where: { status: { eq: PAID } }) {
id
invoiceNumber
total
paidDate: createdAt # Approximation
}
}
}
Get Contact Invoices
Get all invoices for a specific contact.
query GetContactInvoices($contactId: ID!) {
contact(id: $contactId) {
id
name
invoices {
id
invoiceNumber
invoiceDate
dueDate
status
total
currency {
code
symbol
}
}
}
}
Invoice Mutations
Create Invoice
Create a new sales invoice.
mutation CreateInvoice(
$contactId: ID!
$invoiceDate: DateTime!
$dueDate: DateTime!
$items: [InvoiceItemInput!]!
$notes: String
) {
createInvoice(
contactId: $contactId
invoiceDate: $invoiceDate
dueDate: $dueDate
items: $items
notes: $notes
) {
id
invoiceNumber
contactId
invoiceDate
dueDate
status
subtotal
taxAmount
total
items {
id
description
quantity
unitPrice
lineTotal
taxAmount
}
}
}
Required Fields:
contactId: Customer contact IDinvoiceDate: Invoice creation datedueDate: Payment due dateitems: Array of line items
InvoiceItemInput Structure:
{
description: "Product/Service description"
quantity: 2.0
unitPrice: 50.00
itemId: "ITEM_123" // Optional reference to item catalog
}
Record Invoice Payment
Record a payment toward an invoice.
mutation RecordInvoicePayment(
$invoiceId: ID!
$amount: Decimal!
$paymentDate: DateTime!
$paymentMethod: String!
$reference: String
$notes: String
) {
recordInvoicePayment(
invoiceId: $invoiceId
amount: $amount
paymentDate: $paymentDate
paymentMethod: $paymentMethod
reference: $reference
notes: $notes
) {
id
documentId
amount
paymentDate
paymentMethod
reference
notes
}
}
Invoice Status Management
Invoice Status Types
enum InvoiceStatus {
DRAFT # Invoice is being prepared
SENT # Invoice has been sent to customer
VIEWED # Customer has viewed the invoice
PAID # Invoice is fully paid
OVERDUE # Invoice is past due date
CANCELLED # Invoice has been cancelled
}
Status Transitions
const INVOICE_STATUS_FLOW = {
DRAFT: ['SENT', 'CANCELLED'],
SENT: ['VIEWED', 'PAID', 'OVERDUE', 'CANCELLED'],
VIEWED: ['PAID', 'OVERDUE', 'CANCELLED'],
PAID: [], // Terminal state
OVERDUE: ['PAID', 'CANCELLED'],
CANCELLED: [] // Terminal state
};
const canTransitionStatus = (fromStatus, toStatus) => {
return INVOICE_STATUS_FLOW[fromStatus]?.includes(toStatus) || false;
};
Invoice Calculations
Subtotal Calculation
const calculateInvoiceSubtotal = (items) => {
return items.reduce((total, item) => {
return total + (parseFloat(item.quantity) * parseFloat(item.unitPrice));
}, 0);
};
Tax Calculation
const calculateInvoiceTax = (subtotal, taxRate = 0.10) => {
return subtotal * taxRate;
};
const calculateInvoiceTotal = (subtotal, taxAmount) => {
return subtotal + taxAmount;
};
Line Item Totals
const calculateLineTotal = (quantity, unitPrice) => {
return parseFloat(quantity) * parseFloat(unitPrice);
};
const calculateLineTax = (lineTotal, taxRate = 0.10) => {
return lineTotal * taxRate;
};
Invoice Validation
Date Validation
const validateInvoiceDates = (invoiceDate, dueDate) => {
const invoice = new Date(invoiceDate);
const due = new Date(dueDate);
const today = new Date();
// Invoice date cannot be in the future
if (invoice > today) {
throw new Error('Invoice date cannot be in the future');
}
// Due date must be after invoice date
if (due <= invoice) {
throw new Error('Due date must be after invoice date');
}
// Reasonable due date (not more than 1 year)
const oneYearFromInvoice = new Date(invoice);
oneYearFromInvoice.setFullYear(oneYearFromInvoice.getFullYear() + 1);
if (due > oneYearFromInvoice) {
throw new Error('Due date cannot be more than 1 year from invoice date');
}
return true;
};
Amount Validation
const validateInvoiceAmounts = (items) => {
if (!items || items.length === 0) {
throw new Error('Invoice 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 = calculateInvoiceSubtotal(items);
if (subtotal <= 0) {
throw new Error('Invoice subtotal must be positive');
}
return true;
};
Contact Validation
const validateInvoiceContact = async (contactId) => {
const contact = await getContact(contactId);
if (!contact) {
throw new Error('Contact not found');
}
if (contact.contactType !== 'CUSTOMER') {
throw new Error('Invoices can only be created for customers');
}
if (contact.status !== 'ACTIVE') {
throw new Error('Cannot create invoice for inactive contact');
}
return true;
};
Invoice Numbering
Automatic Invoice Numbers
Invoice numbers are automatically generated:
const generateInvoiceNumber = async () => {
const currentYear = new Date().getFullYear();
const lastInvoice = await getLastInvoiceNumber(currentYear);
if (lastInvoice) {
// Extract sequence number and increment
const sequence = parseInt(lastInvoice.split('-')[1]) + 1;
return `${currentYear}-${sequence.toString().padStart(4, '0')}`;
} else {
// First invoice of the year
return `${currentYear}-0001`;
}
};
Custom Invoice Numbers
For custom numbering schemes:
const validateCustomInvoiceNumber = (number) => {
// Check format: YYYY-NNNN
const invoiceRegex = /^\d{4}-\d{4}$/;
if (!invoiceRegex.test(number)) {
throw new Error('Invoice number must be in format YYYY-NNNN');
}
// Check year is current or recent
const year = parseInt(number.split('-')[0]);
const currentYear = new Date().getFullYear();
if (year < currentYear - 1 || year > currentYear + 1) {
throw new Error('Invoice number year is not valid');
}
return true;
};
Invoice Templates
Standard Invoice Creation
const createStandardInvoice = async (customerId, items, dueDays = 30) => {
const invoiceDate = new Date();
const dueDate = new Date(invoiceDate);
dueDate.setDate(dueDate.getDate() + dueDays);
// Validate inputs
await validateInvoiceContact(customerId);
validateInvoiceDates(invoiceDate.toISOString(), dueDate.toISOString());
validateInvoiceAmounts(items);
// Calculate totals
const subtotal = calculateInvoiceSubtotal(items);
const taxAmount = calculateInvoiceTax(subtotal);
const total = calculateInvoiceTotal(subtotal, taxAmount);
// Create invoice
const mutation = `
mutation CreateInvoice($input: CreateInvoiceInput!) {
createInvoice(input: $input) {
id
invoiceNumber
total
status
}
}
`;
const variables = {
input: {
contactId: customerId,
invoiceDate: invoiceDate.toISOString(),
dueDate: dueDate.toISOString(),
items: items,
notes: `Invoice created on ${invoiceDate.toLocaleDateString()}`
}
};
return await graphql.request(mutation, variables);
};
Recurring Invoice Creation
const createRecurringInvoice = async (customerId, items, frequency) => {
const invoiceDate = new Date();
// Calculate due date based on frequency
const dueDate = new Date(invoiceDate);
switch (frequency) {
case 'monthly':
dueDate.setMonth(dueDate.getMonth() + 1);
break;
case 'quarterly':
dueDate.setMonth(dueDate.getMonth() + 3);
break;
case 'yearly':
dueDate.setFullYear(dueDate.getFullYear() + 1);
break;
}
return await createStandardInvoice(customerId, items, dueDate);
};
Payment Processing
Payment Methods
Common payment methods:
const PAYMENT_METHODS = [
'Check',
'Wire Transfer',
'Credit Card',
'Cash',
'ACH',
'PayPal',
'Stripe',
'Other'
];
Payment Validation
const validatePayment = (invoice, paymentAmount) => {
if (paymentAmount <= 0) {
throw new Error('Payment amount must be positive');
}
// Check if payment exceeds outstanding balance
const outstanding = parseFloat(invoice.total) - getTotalPayments(invoice);
if (paymentAmount > outstanding) {
throw new Error(`Payment amount cannot exceed outstanding balance of ${outstanding}`);
}
return true;
};
Overpayment Handling
const handleOverpayment = (invoice, paymentAmount) => {
const outstanding = calculateOutstandingBalance(invoice);
const overpayment = paymentAmount - outstanding;
if (overpayment > 0) {
// Could create credit note or refund
console.warn(`Overpayment of ${overpayment} on invoice ${invoice.invoiceNumber}`);
// For now, we'll allow it but log the warning
return {
paymentAmount: outstanding,
overpaymentAmount: overpayment,
note: `Partial payment of ${outstanding}, overpayment of ${overpayment} not applied`
};
}
return { paymentAmount, overpaymentAmount: 0 };
};
Invoice Reporting
Aging Report
Get aging information for outstanding invoices:
query GetInvoiceAging {
organization {
invoices(where: { status: { in: [SENT, VIEWED, OVERDUE] } }) {
id
invoiceNumber
contactId
total
dueDate
payments {
amount
paymentDate
}
}
}
}
# Process on client to calculate aging buckets
Revenue Report
Get invoiced revenue by period:
query GetRevenueByPeriod($startDate: DateTime!, $endDate: DateTime!) {
organization {
invoices(where: {
invoiceDate: { gte: $startDate, lte: $endDate }
status: { ne: CANCELLED }
}) {
id
invoiceNumber
invoiceDate
total
status
payments {
amount
paymentDate
}
}
}
}
Error Handling
Invoice Creation Errors
NOT_FOUND: Contact not foundVALIDATION_ERROR: Invalid dates, amounts, or itemsFORBIDDEN: Insufficient permissions
Payment Recording Errors
NOT_FOUND: Invoice not foundVALIDATION_ERROR: Invalid payment data or overpaymentFORBIDDEN: Insufficient permissions
Best Practices
Invoice Creation Workflow
const createInvoiceWorkflow = async (customerId, items) => {
try {
// 1. Validate customer
const customer = await getCustomer(customerId);
if (!customer) throw new Error('Customer not found');
// 2. Check for outstanding invoices
const outstandingInvoices = await getCustomerOutstandingInvoices(customerId);
if (outstandingInvoices.length > 5) {
console.warn(`Customer has ${outstandingInvoices.length} outstanding invoices`);
}
// 3. Create invoice
const invoice = await createStandardInvoice(customerId, items);
// 4. Send notification (could be automated)
await sendInvoiceNotification(invoice.id);
// 5. Log activity
console.log(`Invoice ${invoice.invoiceNumber} created for ${customer.name}`);
return invoice;
} catch (error) {
console.error('Invoice creation failed:', error);
throw error;
}
};
Payment Application
const applyPaymentToInvoice = async (invoiceId, paymentData) => {
try {
// Get invoice details
const invoice = await getInvoice(invoiceId);
if (!invoice) throw new Error('Invoice not found');
// Validate payment
validatePayment(invoice, paymentData.amount);
// Handle overpayments
const paymentResult = handleOverpayment(invoice, paymentData.amount);
// Record payment
const payment = await recordInvoicePayment({
invoiceId,
...paymentData,
amount: paymentResult.paymentAmount
});
// Update invoice status if fully paid
if (calculateOutstandingBalance(invoice) <= 0) {
await updateInvoiceStatus(invoiceId, 'PAID');
}
return payment;
} catch (error) {
console.error('Payment application failed:', error);
throw error;
}
};
Invoice Maintenance
const updateOverdueInvoices = async () => {
const today = new Date();
// Get sent/viewed invoices
const invoices = await getInvoicesByStatus(['SENT', 'VIEWED']);
for (const invoice of invoices) {
const dueDate = new Date(invoice.dueDate);
if (today > dueDate && invoice.status !== 'PAID') {
// Mark as overdue
await updateInvoiceStatus(invoice.id, 'OVERDUE');
// Send reminder notification
await sendOverdueReminder(invoice.id);
}
}
};
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