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:
- If
$rateOverrideis provided, use it directly - If
$hsnCodeis provided, look up fromTaxConfigurationtable - If no match, use the default GST configuration
- 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 |