Skip to main content

Overview

The SaleService class handles the complete lifecycle of sales transactions in the ERP system. It orchestrates inventory movements, journal entries, receivables, invoicing, and NCF (fiscal control number) generation in a single atomic transaction. Namespace: App\Services\Sales\SalesServices\SaleService Location: app/Services/Sales/SalesServices/SaleService.php

Dependencies

The service is constructed with the following dependencies injected via Laravel’s dependency injection:
  • InventoryMovementService - Manages inventory stock movements
  • JournalEntryService - Creates accounting journal entries
  • ReceivableService - Handles accounts receivable for credit sales
  • InvoiceService - Generates invoices from sales
  • NcfGeneratorInterface - Generates fiscal control numbers (NCF)

Methods

create()

Creates a new sale transaction with all related entities (items, inventory movements, accounting entries, and receivables).
public function create(array $data): Sale
data
array
required
Sale data array with the following structure:
return
Sale
Returns the created Sale model with all relationships loaded
Transaction Flow:
  1. Document Number Generation - Generates next sequential sale number from DocumentType
  2. Sale Record Creation - Creates the sale record with status 'completed'
  3. NCF Generation - If ncf_type_id provided, generates fiscal control number
  4. Sale Items Creation - Creates all sale item records
  5. Inventory Movements - Registers output movements for each product
  6. Accounting Treatment:
    • Cash Sales: Creates immediate journal entry (Debit: Cash Account, Credit: Income Account 4.1)
    • Credit Sales: Creates receivable record with 30-day due date
  7. Invoice Generation - Creates corresponding invoice record

Example Usage

use App\Services\Sales\SalesServices\SaleService;

public function __construct(
    protected SaleService $saleService
) {}

public function processSale()
{
    $sale = $this->saleService->create([
        'client_id' => 1,
        'warehouse_id' => 1,
        'total_amount' => 250.00,
        'payment_type' => 'cash',
        'tipo_pago_id' => 1,
        'cash_received' => 300.00,
        'cash_change' => 50.00,
        'sale_date' => now(),
        'items' => [
            [
                'product_id' => 10,
                'quantity' => 2,
                'price' => 75.00,
            ],
            [
                'product_id' => 15,
                'quantity' => 1,
                'price' => 100.00,
            ],
        ],
        'ncf_type_id' => 1,
        'notes' => 'Sale processed from POS terminal 3',
    ]);

    return $sale;
}
// Credit sale example
$creditSale = $this->saleService->create([
    'client_id' => 5,
    'warehouse_id' => 1,
    'total_amount' => 1500.00,
    'payment_type' => 'credit',
    'items' => [
        [
            'product_id' => 20,
            'quantity' => 5,
            'price' => 300.00,
        ],
    ],
]);

cancel()

Cancels an existing sale and reverses all related accounting, inventory, and receivable transactions.
public function cancel(Sale $sale, ?string $reason = null): bool
sale
Sale
required
The Sale model instance to cancel
reason
string
Cancellation reason for audit trail. Defaults to “Anulación de venta manual”
return
bool
Returns true if cancellation was successful
Validation Rules:
  • Sale must not already be cancelled
  • For credit sales, the receivable must not have any payments applied
  • If receivable has partial payments, throws exception: “No se puede anular: El cliente ya tiene abonos.”
Reversal Process:
  1. Receivable Cancellation - For credit sales, cancels the receivable if no payments exist
  2. NCF Status Update - Updates NCF log status to 'voided' with cancellation reason
  3. Accounting Reversal - Creates reversal journal entry:
    • Debit: Income Account (4.1)
    • Credit: Cash/Receivable Account (depending on original payment type)
  4. Inventory Reversal - Creates adjustment movements to restore stock
  5. Invoice Cancellation - Marks related invoice as cancelled
  6. Sale Status Update - Sets sale status to 'canceled'

Example Usage

use App\Models\Sales\Sale;
use App\Services\Sales\SalesServices\SaleService;

public function __construct(
    protected SaleService $saleService
) {}

public function cancelSale($saleId)
{
    $sale = Sale::findOrFail($saleId);
    
    try {
        $result = $this->saleService->cancel(
            $sale,
            'Customer requested cancellation - duplicate order'
        );
        
        if ($result) {
            return response()->json([
                'message' => 'Sale cancelled successfully',
                'sale_number' => $sale->number,
            ]);
        }
    } catch (\Exception $e) {
        return response()->json([
            'error' => $e->getMessage()
        ], 400);
    }
}

Protected Methods

These methods are used internally by the service and are not meant to be called directly.

generateSaleAccountingEntry()

Generates the accounting journal entry for cash sales.
protected function generateSaleAccountingEntry(Sale $sale)
Accounting Logic:
  • Debits the payment method’s accounting account (or default 1.1.01 Cash)
  • Credits Income Account 4.1 (Sales Revenue)
  • Description format: “Venta Contado () -
  • Status: 'posted' (immediately posted)
Source Code Reference: SaleService.php:147-169

generateCancellationAccountingEntry()

Generates the reversal accounting journal entry when cancelling a sale.
protected function generateCancellationAccountingEntry(Sale $sale)
Accounting Logic:
  • Debits Income Account 4.1 (reverses revenue recognition)
  • Credits:
    • Cash Account (1.1.01) for cash sales
    • Client’s accounting account or default Receivable Account (1.1.02) for credit sales
  • Reference format: “REV-
  • Description: “Anulación Venta
Source Code Reference: SaleService.php:171-191

Sale Model Constants

Payment Types

Sale::PAYMENT_CASH   // 'cash'
Sale::PAYMENT_CREDIT // 'credit'

Status Constants

Sale::STATUS_COMPLETED // 'completed'
Sale::STATUS_CANCELED  // 'canceled'

Exception Handling

The service may throw exceptions in the following scenarios:
Exception: "Configuración contable incompleta."Cause: Missing required accounting accounts (Income Account 4.1 or Cash Account 1.1.01)Resolution: Ensure all required accounting accounts are configured in the system
Exception: "La venta ya se encuentra anulada."Cause: Attempting to cancel a sale that is already cancelledResolution: Check sale status before attempting cancellation
Exception: "No se puede anular: El cliente ya tiene abonos."Cause: Attempting to cancel a credit sale that has received partial or full paymentResolution: Payments must be reversed before sale cancellation, or use a different business process for handling paid invoices
Cause: Product quantity exceeds available warehouse stock during sale creationResolution: Thrown by InventoryMovementService - verify stock availability before processing sale

Integration Points

InventoryMovementService

  • On Sale Creation: Registers TYPE_OUTPUT movements for each product
  • On Sale Cancellation: Registers TYPE_ADJUSTMENT movements to restore inventory
  • See InventoryMovementService for details

JournalEntryService

  • Cash Sales: Creates posted journal entries for immediate revenue recognition
  • Sale Cancellation: Creates reversal entries to negate original transaction
  • See JournalEntryService for details

ReceivableService

  • Credit Sales: Creates receivable records with 30-day due date
  • Cancellation: Cancels receivables if no payments have been applied
  • See ReceivableService for details

InvoiceService

  • Creates invoice record from completed sale
  • Cancels invoice when sale is cancelled

NcfGeneratorInterface

  • Generates fiscal control numbers (NCF) when ncf_type_id is provided
  • Updates NCF log status on cancellation

Database Transaction Safety

Both create() and cancel() methods wrap all operations in database transactions using DB::transaction(). If any operation fails, all changes are rolled back automatically, ensuring data consistency.

Build docs developers (and LLMs) love