Skip to main content

Overview

Cash expenses record operational costs paid directly from cash registers, card terminals, or bank accounts during the operating day. They reduce the session’s closing balance and provide visibility into daily spending.

Expense Fields

From CashExpense.php:13-28:
FieldTypeRequiredDescription
cash_session_idForeign KeyYesLinks to the daily session
tender_typeEnumYesCASH, CARD, or TRANSFER
amountDecimalYesExpense amount (4 decimal places)
categoryStringYesExpense category (e.g., “Supplies”, “Maintenance”)
vendorStringNoVendor or supplier name
referenceStringNoInvoice number or receipt ID
notesTextNoAdditional context
card_terminal_idForeign KeyIf CARDTerminal used for payment
bank_account_idForeign KeyIf TRANSFERAccount used for payment
incurred_atTimestampYesWhen the expense occurred
created_byForeign KeyYesUser who recorded the expense
posted_byForeign KeyNoUser who approved (nullable until posted)
posted_atTimestampNoWhen finalized (nullable until posted)
metaJSONNoCustom metadata

Tender Types

From CashExpense.php:37-42:
public const TENDER_CASH = 'CASH';
public const TENDER_CARD = 'CARD';
public const TENDER_TRANSFER = 'TRANSFER';
TypeDescriptionRequired Field
CASHPaid from register cash drawerNone
CARDCharged to card terminalcard_terminal_id
TRANSFERPaid from bank accountbank_account_id
Expenses decrease the session’s closing balance regardless of tender type.

Create an Expense

Endpoint

POST /api/v1/cash-expenses

Cash Expense Example

{
  "cash_session_id": 1,
  "tender_type": "CASH",
  "amount": 150.00,
  "category": "Supplies",
  "vendor": "Local Supply Store",
  "reference": "INV-2026-123",
  "notes": "Emergency cleaning supplies",
  "incurred_at": "2026-03-06T14:30:00-06:00"
}

Card Expense Example

{
  "cash_session_id": 1,
  "tender_type": "CARD",
  "amount": 250.00,
  "category": "Maintenance",
  "vendor": "Plumbing Service Co.",
  "reference": "INV-PLUMB-456",
  "card_terminal_id": 2,
  "notes": "Urgent sink repair",
  "incurred_at": "2026-03-06T11:00:00-06:00"
}

Transfer Expense Example

{
  "cash_session_id": 1,
  "tender_type": "TRANSFER",
  "amount": 500.00,
  "category": "Utilities",
  "vendor": "Electric Company",
  "reference": "BILL-MARCH-2026",
  "bank_account_id": 1,
  "notes": "Monthly electricity payment",
  "incurred_at": "2026-03-06T09:00:00-06:00"
}
  • When tender_type is CARD, card_terminal_id is required
  • When tender_type is TRANSFER, bank_account_id is required
  • When tender_type is CASH, both fields should be null

Post an Expense

Posting finalizes the expense and includes it in the session’s closing balance calculation.

Endpoint

POST /api/v1/cash-expenses/{id}/post
// Post the expense
const response = await api.post(`/cash-expenses/${expenseId}/post`);

// Response includes posted_by and posted_at
{
  "message": "Expense posted successfully",
  "data": {
    "id": 1,
    "amount": "150.0000",
    "category": "Supplies",
    "posted_by": 5,
    "posted_at": "2026-03-06T23:00:00Z"
  }
}
Only posted expenses are included in the session’s calculateClosingBalance() method.

Query Scopes

From CashExpense.php:86-122:
// Filter by status
CashExpense::posted()->get();
CashExpense::draft()->get();

// Filter by category
CashExpense::byCategory('Supplies')->get();

// Filter by tender type
CashExpense::byTenderType('CASH')->get();

// Filter by date range
CashExpense::byDateRange('2026-03-01', '2026-03-31')->get();

Helper Methods

Status Check

From CashExpense.php:126-130:
$expense->isPosted(); // Returns true if posted_at is not null

Tender Type Checks

From CashExpense.php:133-155:
$expense->isCash();     // true if tender_type === CASH
$expense->isCard();     // true if tender_type === CARD
$expense->isTransfer(); // true if tender_type === TRANSFER

Relationships

From CashExpense.php:47-82:
  • cashSession: Parent session
  • cardTerminal: Terminal for CARD expenses (nullable)
  • bankAccount: Account for TRANSFER expenses (nullable)
  • createdBy: User who recorded the expense
  • postedBy: User who approved the expense (nullable until posted)

Expense Categories

Common expense categories:
CategoryDescriptionExamples
SuppliesOperating suppliesCleaning products, paper goods, packaging
MaintenanceEquipment and facility repairsPlumbing, HVAC, appliance repair
UtilitiesRegular utility billsElectricity, water, internet
DeliveryDelivery-related costsGas, vehicle maintenance, courier fees
MarketingPromotional expensesFlyers, local advertising
MiscellaneousOther operational costsLicenses, permits, small equipment
Categories are free-form strings. Consider defining a standard list in your application config.

Workflow Diagram

Use Cases

Emergency Supply Purchase

Pay cash from register for urgent supplies:
{
  "cash_session_id": 1,
  "tender_type": "CASH",
  "amount": 75.00,
  "category": "Supplies",
  "vendor": "Local Hardware Store",
  "reference": "RECEIPT-789",
  "notes": "Emergency light bulbs - main dining area",
  "incurred_at": "2026-03-06T15:00:00-06:00"
}

Service Charge via Terminal

Pay for repair using card terminal:
{
  "cash_session_id": 1,
  "tender_type": "CARD",
  "amount": 320.00,
  "category": "Maintenance",
  "vendor": "HVAC Specialists",
  "reference": "SVC-2026-045",
  "card_terminal_id": 1,
  "notes": "AC unit maintenance - kitchen area",
  "incurred_at": "2026-03-06T10:30:00-06:00"
}

Utility Payment via Bank

Pay monthly bill from business account:
{
  "cash_session_id": 1,
  "tender_type": "TRANSFER",
  "amount": 650.00,
  "category": "Utilities",
  "vendor": "City Water Department",
  "reference": "BILL-MARCH-2026",
  "bank_account_id": 1,
  "notes": "Monthly water bill",
  "incurred_at": "2026-03-06T09:00:00-06:00"
}

Impact on Session Balance

Expenses reduce the closing balance:
// Session with opening balance
const session = {
  opening_balance: 500.00,
  status: 'DRAFT'
};

// Record income adjustment
await api.post('/cash-adjustments', {
  cash_session_id: session.id,
  direction: 'INFLOW',
  lines: [
    { tender_type: 'CASH', amount: 2000.00 }
  ]
});

// Record expense
await api.post('/cash-expenses', {
  cash_session_id: session.id,
  tender_type: 'CASH',
  amount: 150.00,
  category: 'Supplies'
});

// Closing balance calculation:
// 500 (opening) + 2000 (inflows) - 0 (outflows) - 150 (expenses) = 2350

Best Practices

Always capture invoice/receipt numbers in the reference field for audit trails
Record expenses when they occur (use incurred_at), even if posting happens later
Use consistent vendor names to enable spending analysis by supplier
Define standard categories in your application to ensure consistent reporting
Keep expenses as drafts until reviewed by a manager, then post with their user ID
Match tender type to actual payment method - use CARD for terminal charges, not CASH

Batch Recording

Record multiple expenses at once:
const expenses = [
  {
    tender_type: 'CASH',
    amount: 50.00,
    category: 'Supplies',
    vendor: 'Office Depot',
    notes: 'Printer paper'
  },
  {
    tender_type: 'CASH',
    amount: 25.00,
    category: 'Supplies',
    vendor: 'Local Market',
    notes: 'Coffee for staff room'
  },
  {
    tender_type: 'CARD',
    amount: 180.00,
    category: 'Maintenance',
    vendor: 'Locksmith',
    card_terminal_id: 1,
    notes: 'Replace back door lock'
  }
];

// Create all expenses
const created = await Promise.all(
  expenses.map(expense => 
    api.post('/cash-expenses', {
      cash_session_id: sessionId,
      incurred_at: new Date().toISOString(),
      ...expense
    })
  )
);

// Post all after review
await Promise.all(
  created.map(({ data }) => 
    api.post(`/cash-expenses/${data.id}/post`)
  )
);

Reporting Queries

Expenses by Category

SELECT 
  category,
  COUNT(*) as expense_count,
  SUM(amount) as total_amount
FROM cash_expenses
WHERE posted_at IS NOT NULL
  AND incurred_at >= '2026-03-01'
  AND incurred_at < '2026-04-01'
GROUP BY category
ORDER BY total_amount DESC;

Expenses by Vendor

SELECT 
  vendor,
  category,
  COUNT(*) as transaction_count,
  SUM(amount) as total_spent
FROM cash_expenses
WHERE posted_at IS NOT NULL
  AND vendor IS NOT NULL
GROUP BY vendor, category
ORDER BY total_spent DESC;

Daily Expense Trend

SELECT 
  DATE(incurred_at) as expense_date,
  tender_type,
  COUNT(*) as expense_count,
  SUM(amount) as total_amount
FROM cash_expenses
WHERE posted_at IS NOT NULL
  AND incurred_at >= '2026-03-01'
GROUP BY DATE(incurred_at), tender_type
ORDER BY expense_date DESC, tender_type;

Error Handling

{
  "message": "Validation error",
  "errors": {
    "card_terminal_id": [
      "The card_terminal_id field is required when tender_type is CARD"
    ]
  }
}

Next Steps

Cash Sessions

See how expenses affect session closing balance

Bank Accounts

Configure accounts for transfer expenses

Build docs developers (and LLMs) love