Payments, Tax & Invoicing

The Orders module tracks payments as separate records linked to orders, supporting multiple providers, partial payments, COD collection, refunds, Indian GST-compliant tax calculation, and sequential invoice generation.

Payment Model

Each payment is an OrderPayment record:

Field Type Description
id UUID Primary key
order_id UUID Parent order
provider_module string Payment provider module alias
external_payment_id string Gateway transaction ID
method string Payment method (razorpay, stripe, cod, bank_transfer)
status string pending, completed, failed, refunded
amount decimal Payment amount
currency string Currency code
payment_link_url string Generated payment link URL
payment_link_expires_at datetime Link expiry
metadata jsonb Gateway-specific metadata
paid_at datetime When payment was confirmed

Recording Payments

POST /api/v1/orders/{order}/payments

{
  "method": "razorpay",
  "amount": 2999.00,
  "external_payment_id": "pay_abc123",
  "metadata": {
    "gateway_order_id": "order_xyz",
    "payment_method": "upi"
  }
}

The service automatically reconciles the order's payment_status:

  • Total paid >= order total: paid
  • Total paid > 0 but < total: partial
  • No payments: pending

Payment Links

Generate shareable payment links via integrated providers:

POST /api/v1/orders/{order}/payments/link

{
  "provider_module": "payment-razorpay"
}

This calls the payment provider module through the Module API Bus:

$bus->call($providerModule, 'payment.createLink', [
    'amount' => $order->balance_due,
    'currency' => $order->currency,
    'reference' => $order->order_number,
    'customer_email' => $order->customer->email,
    'customer_phone' => $order->customer->phone,
]);

COD Collection

Record cash collection on delivery:

POST /api/v1/orders/{order}/payments

{
  "method": "cod",
  "amount": 2999.00,
  "collected_by": "delivery_agent_id"
}

Refunds

POST /api/v1/orders/{order}/refund

{
  "payment_id": "uuid",
  "amount": 999.00,
  "reason": "Item defective"
}

Refunds create an OrderRefund record and publish an order.refund_processed event. If the original payment was through an external provider, the refund is forwarded via the bus.

Refund Model

Field Type Description
id UUID Primary key
order_id UUID Parent order
payment_id UUID Original payment (nullable)
provider_module string Refund processor
external_refund_id string Gateway refund ID
amount decimal Refund amount
reason string Refund reason
status string pending, completed, failed
processed_at datetime When refund was processed

Order Accessors

The Order model provides computed payment attributes:

$order->total_paid;    // Sum of completed payments
$order->balance_due;   // total - total_paid + refunded

GST Calculation

The TaxService handles Indian GST rules:

  • Intra-state (buyer and seller in same state): Tax splits into CGST + SGST (half rate each)
  • Inter-state (different states): Full rate charged as IGST

Usage

use Modules\Orders\App\Services\TaxService;

$taxService = app(TaxService::class);

// Intra-state: Rs 1000 @ 18% GST
$result = $taxService->calculateTax(1000, null, true, 18);
// Returns: [rate => 18.0, cgst => 90.0, sgst => 90.0, igst => 0, total_tax => 180.0]

// Inter-state: Rs 1000 @ 18% GST
$result = $taxService->calculateTax(1000, null, false, 18);
// Returns: [rate => 18.0, cgst => 0, sgst => 0, igst => 180.0, total_tax => 180.0]

Method Signature

calculateTax(
    float $amount,          // Taxable amount
    ?string $hsnCode,       // HSN code for rate lookup (optional)
    bool $sameState = true, // true = CGST+SGST, false = IGST
    ?float $rateOverride    // Override rate (skips HSN lookup)
): array

Tax Rate Lookup

Tax rates are resolved in order:

  1. If $rateOverride is provided, use it directly
  2. If $hsnCode is provided, look up from TaxConfiguration table
  3. If no match, use the default GST configuration
  4. If no default exists, fall back to 18%

Supported Indian GST rates: 0%, 5%, 12%, 18%, 28%.

Tax Configuration Model

Field Type Description
id int Primary key
name string Configuration name (e.g., "Standard GST 18%")
rate decimal(5,2) Tax rate percentage
type string gst, vat, sales_tax
is_default boolean Default configuration for this type
hsn_codes jsonb Array of HSN codes this rate applies to

Invoice Generation

Invoice Model

Field Type Description
id UUID Primary key
order_id UUID Parent order
invoice_number string Sequential number (e.g., INV-202603-0001)
type string invoice, credit_note, proforma
status string draft, issued, paid, cancelled
subtotal decimal Pre-tax amount
tax_details jsonb CGST/SGST/IGST breakdown
total_tax decimal Total tax amount
discount decimal Discount amount
total decimal Grand total
billing_name string Customer name
billing_gstin string Customer GSTIN
line_items jsonb Invoice line items
pdf_path string Generated PDF file path
issued_at datetime Issue date
due_at datetime Payment due date

Creating an Invoice

POST /api/v1/orders/{order}/invoices

{
  "type": "invoice"
}

The InvoiceService automatically loads order items, calculates tax, generates a sequential number, and sets a 30-day due date.

Invoice Number Format

Type Prefix Format Example
Invoice INV INV-YYYYMM-NNNN INV-202603-0001
Credit Note CN CN-YYYYMM-NNNN CN-202603-0001
Proforma PI PI-YYYYMM-NNNN PI-202603-0001

Numbers are sequential within each prefix and month.

Credit Notes

Issue a credit note for partial or full refunds:

$invoiceService = app(InvoiceService::class);

$creditNote = $invoiceService->generateCreditNote(
    $order,
    1500.00,
    'Partial refund - defective item'
);

Payment Events

Event When
order.payment_updated Payment recorded, status reconciled
order.refund_processed Refund completed