Skip to content

Credit Notes

Location: dg_smart_pos_api/modules/CreditNotes (the directory has no space; the stub’s Credit Notes was a typo). Owns credit notes: returning one or more line items from an already-paid sale and resolving the credit as a refund (cash/M-Pesa), an exchange for other stock, or a mix of both. It coordinates with Inventory to put returned stock back (InventoryAdjustmentService, InventorySerialNumberService) rather than owning stock itself, and with ReceiptSettings to print the credit note receipt.

  • CreditNote: store, sale_id (the original sale), credit_note_number, status (CreditNoteStatus enum: resolved/voided), reason, credit_total, refund_cash/refund_mpesa/refund_total, exchange_total, additional_payment_cash/additional_payment_mpesa (customer pays the difference when exchange value exceeds credit), balance_refunded. Has a legacy_sale_id column alongside sale_id, and a polymorphic receipts() relation into Modules\ReceiptSettings\Models\Receipt.
  • CreditNoteItem: one returned line: credit_note_id, stock_id, original_sale_stock_id (links back to the new_sale_stocks row it was sold on), quantity, unit_price, broker, serial_numbers (array), tax fields (tax_type_code, tax_rate, taxable_amount, tax_amount).
  • CreditNoteResolution: one resolution line: type (ResolutionType enum: refund/exchange), and either refund fields (refund_amount, refund_method, refund_ref) or exchange fields (stock_id, quantity, unit_price).

CreditNoteController::store (StoreCreditNoteRequest) takes reason, return_items[] (stock_id, optional sale_line_id, quantity, unit_price, optional broker/serials[]), and resolutions[] (type: refund|exchange; refund needs refund_amount+refund_method; exchange needs stock_id+quantity+unit_price), plus optional additional_payment_cash/additional_payment_mpesa for when an exchange costs more than the credit.

CreditNoteService::createCreditNote runs the whole thing in one transaction: rejects the sale if it isn’t SaleStatus::Paid or doesn’t belong to the target store, validates returned quantities don’t exceed what was actually sold, computes credit_total from the return items and exchange_total/refund_total from the resolutions, and enforces that if exchange value exceeds credit the caller supplied enough additional_payment_* to cover the difference (throwing otherwise). GenerateCreditNoteReceiptJob is queued afterward to print the credit note receipt via ReceiptService.

index lists credit notes for a store with from_date/to_date/status/pagination filters. show returns one with items/resolutions loaded. void (CreditNoteService::voidCreditNote) flips status to voided after confirming the credit note belongs to the given store.

All under auth:sanctum, prefix credit-notes/stores/{store}, registered via modules/CreditNotes/Routes/credit_notes.php (required once from routes/tenant.php, no duplicate registration found for this module):

Method Path Controller method
GET credit-notes index
GET credit-notes/{creditNote} show
POST sales/{sale}/credit-notes store
POST credit-notes/{creditNote}/void void
  • Sales: credit notes are issued against a Sale; see the caution above on the parallel legacy return-and-replace flow
  • Inventory: returned stock and serials go back through InventoryAdjustmentService/InventorySerialNumberService
  • ReceiptSettings: credit note receipt generation and printing
  • Approvals: not itself an approvable action in config/approvals.php, unlike sales.void/sales.discount