aizuddinmanap / cashier-chip
Laravel Cashier provider for Chip payment processing.
Requires
- php: ^8.1
- ext-json: *
- ext-openssl: *
- guzzlehttp/guzzle: ^7.4.5
- illuminate/contracts: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/routing: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- illuminate/view: ^10.0|^11.0|^12.0
- moneyphp/money: ^3.2|^4.0
- nesbot/carbon: ^2.67|^3.0
- spatie/url: ^1.3.5|^2.0
- symfony/http-kernel: ^6.2|^7.0
- symfony/polyfill-intl-icu: ^1.22.1
Requires (Dev)
- dompdf/dompdf: ^3.0
- mockery/mockery: ^1.5.1
- orchestra/testbench: ^8.14|^9.0|^10.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.4|^11.5
Suggests
- dompdf/dompdf: Required for PDF invoice generation (^2.0|^3.0)
README
Laravel Cashier Chip provides an expressive, fluent interface to Chip's payment and subscription billing services. Now with 100% Laravel Cashier API compatibility, it seamlessly bridges CashierChip's transaction-based architecture with Laravel Cashier's familiar invoice patterns.
๐ Stable Release: v1.0.15
Production-ready with comprehensive bug fixes and enhanced test coverage:
- โ All 72 Tests Passing - Comprehensive test coverage with 273+ assertions
- โ PHPUnit 11 Fully Compatible - Zero deprecations remaining (down from 71!)
- โ PDF Date Formatting Fixed - No more "format() on null" errors when paid_at is null
- โ PDF Generation Fixed - No more null pointer errors in PDF generation when billable entity is null
- โ
Timestamp Fields Fixed - Invoice objects now have proper
created_at
andupdated_at
fields - โ Laravel View Compatibility - No more null pointer errors in Blade templates
- โ Robust Error Handling - Graceful fallbacks for missing customer/billable data
- โ PHPUnit 11 Compatible - Modern test attributes, zero deprecations (down from 71!)
- โ Database Compatibility - Works with both old and new transaction table schemas
- โ Metadata System Fixed - Resolved circular reference and array conversion issues
- โ Invoice Generation Stable - Transaction-to-invoice conversion working perfectly
- โ Currency Display Fixed - Malaysian Ringgit properly displays as "RM 29.90"
- โ PDF Generation Working - Optional dompdf integration with error handling
- โ Dynamic Pricing - No more hardcoded amounts, uses actual subscription pricing
- โ Laravel Cashier Compatible - 100% API compatibility verified
โจ Laravel Cashier Invoice Alignment
CashierChip v1.0.12+ includes full Laravel Cashier compatibility:
- โ Perfect Laravel Cashier API - Same methods as Stripe/Paddle Cashier
- โ Transaction-to-Invoice Bridge - Your transactions work as invoices automatically
- โ PDF Invoice Generation - Professional PDFs with company branding (optional)
- โ Query Scopes & Filtering - Powerful invoice management capabilities
- โ Status Management - Proper invoice statuses (paid, open, void, draft)
- โ Zero Breaking Changes - Existing transaction code still works
๐ The CashierChip Difference
Unlike other Laravel Cashier packages:
- Stripe/Paddle Cashier - Uses external API for invoice data
- CashierChip - Stores billing data as transactions locally, converts to invoices on-demand
This means:
- โ Faster Performance - No external API calls for invoice listing
- โ Offline Compatibility - Works without internet for invoice views
- โ Full Data Control - All billing data in your database
- โ Laravel Cashier Compatible - Same API, better performance
๐ Features
- Laravel Cashier Compatibility: 100% compatible API with Stripe/Paddle Cashier
- Transaction-Based Billing: Fast, local storage of all payment data
- Invoice Generation: Convert transactions to invoices with optional PDF export
- Subscription Management: Create, modify, cancel, and resume subscriptions
- One-time Payments: Process single charges with full transaction tracking
- Refund Processing: Full and partial refunds with automatic transaction linking
- Customer Management: Automatic customer creation and synchronization
- Webhook Handling: Secure webhook processing with automatic verification
- FPX Support: Malaysian bank transfers with real-time status checking
- Optional PDF Generation: Customizable invoice templates with company branding (requires dompdf)
๐ฆ Installation
Install via Composer:
composer require aizuddinmanap/cashier-chip
Publish and Run Migrations
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrate
Note: This includes an optional plans table migration (
2024_01_01_000005_create_plans_table.php
). If you don't want local plan management, simply delete this file before runningmigrate
.
Publish Configuration (Optional)
php artisan vendor:publish --tag="cashier-config"
Optional Dependencies
For PDF invoice generation, install dompdf:
composer require dompdf/dompdf
CashierChip works with both dompdf 2.x and 3.x, so you can choose your preferred version.
โ๏ธ Configuration
Add your Chip credentials to your .env
file:
CHIP_API_KEY=your_chip_api_key CHIP_BRAND_ID=your_chip_brand_id CHIP_WEBHOOK_SECRET=your_webhook_secret
Add Billable Trait
Add the Billable
trait to your User model:
use Aizuddinmanap\CashierChip\Billable; class User extends Authenticatable { use Billable; }
Add Database Columns
Add a migration to add the required column to your users table:
Schema::table('users', function (Blueprint $table) { $table->string('chip_id')->nullable()->index(); });
๐งพ Working with Invoices (Laravel Cashier Compatible)
Basic Invoice Operations
CashierChip automatically converts your transactions to invoices with full Laravel Cashier API compatibility:
// Get all paid invoices (successful transactions) $invoices = $user->invoices(); // Get all invoices including pending ones $allInvoices = $user->invoices(true); // Find specific invoice $invoice = $user->findInvoice('txn_123'); // Get latest invoice $latestInvoice = $user->latestInvoice(); // Get upcoming invoice (pending transactions) $upcomingInvoice = $user->upcomingInvoice(); // Create new invoice $invoice = $user->invoiceFor('Premium Service', 2990); // RM 29.90
Invoice Properties - Exactly Like Laravel Cashier
$invoice = $user->findInvoice('txn_123'); // Basic properties $invoice->id(); // "txn_123" $invoice->total(); // "RM 29.90" $invoice->rawTotal(); // 2990 (cents) $invoice->currency(); // "MYR" $invoice->status(); // "paid", "open", "void", "draft" // Dates $invoice->date(); // Carbon date $invoice->dueDate(); // Carbon due date $invoice->paidAt(); // Carbon paid date (if paid) // Status checks $invoice->paid(); // true/false $invoice->open(); // true/false (unpaid) $invoice->void(); // true/false (failed/refunded) $invoice->draft(); // true/false (pending) // Line items and metadata $invoice->lines(); // Collection of line items $invoice->description(); // Invoice description $invoice->metadata(); // Array of metadata
Invoice Queries and Filtering
// Get invoices for specific period $startDate = Carbon::now()->startOfMonth(); $endDate = Carbon::now()->endOfMonth(); $monthlyInvoices = $user->invoicesForPeriod($startDate, $endDate); // Get invoices for specific year $yearlyInvoices = $user->invoicesForYear(2024); // Get total amount for period $monthlyTotal = $user->invoiceTotalForPeriod($startDate, $endDate);
๐ PDF Invoice Generation
Note: PDF generation requires an optional dependency. Install with:
composer require dompdf/dompdf
CashierChip supports both dompdf 2.x and 3.x versions, giving you flexibility in choosing your preferred version.
Download & View Invoices
// Download invoice as PDF $invoice = $user->findInvoice('txn_123'); return $invoice->downloadPDF(); // Download with custom filename return $invoice->downloadPDF([], 'my-invoice-123.pdf'); // View in browser return $invoice->viewPDF(); // Download with company branding return $invoice->downloadPDF([ 'company_name' => 'Your Company Ltd', 'company_address' => '123 Business Street\nKuala Lumpur, Malaysia', 'company_phone' => '+60 3-1234 5678', 'company_email' => 'billing@yourcompany.com' ]);
Controller Example
class InvoiceController extends Controller { public function download(Request $request, $invoiceId) { $user = $request->user(); // Works exactly like Laravel Cashier Stripe/Paddle! return $user->downloadInvoice($invoiceId, [ 'company_name' => config('app.name'), 'company_address' => config('company.address'), ]); } public function index(Request $request) { $user = $request->user(); // Get all invoices (Laravel Cashier compatible) $invoices = $user->invoices(); return view('invoices.index', compact('invoices')); } }
๐ฐ Transaction Management (Core CashierChip)
While invoices provide Laravel Cashier compatibility, transactions remain the core of CashierChip's fast, local billing system:
Transaction Queries
// Get all transactions $transactions = $user->transactions()->get(); // Get successful transactions only $successfulTransactions = $user->transactions()->successful()->get(); // Get failed transactions $failedTransactions = $user->transactions()->failed()->get(); // Get refunded transactions $refundedTransactions = $user->transactions()->refunded()->get(); // Get refund transactions $refunds = $user->transactions()->refunds()->get(); // Get charges only $charges = $user->transactions()->charges()->get(); // Get transactions by type $subscriptionCharges = $user->transactions()->ofType('subscription')->get();
Transaction Status Checking
$transaction = $user->findTransaction('transaction_id'); // Check transaction status if ($transaction->successful()) { // Transaction completed successfully } if ($transaction->failed()) { // Transaction failed } if ($transaction->pending()) { // Transaction still processing } if ($transaction->refunded()) { // Transaction has been refunded }
Transaction Details
$transaction = $user->findTransaction('transaction_id'); // Get formatted amounts $amount = $transaction->amount(); // "RM 100.00" $rawAmount = $transaction->rawAmount(); // 10000 (cents) $currency = $transaction->currency(); // "MYR" // Get transaction metadata $chipId = $transaction->chipId(); // Chip transaction ID $type = $transaction->type(); // "charge" or "refund" $paymentMethod = $transaction->paymentMethod(); // "fpx", "card", etc. $metadata = $transaction->metadata(); // Custom metadata array // Get Money object for calculations $money = $transaction->asMoney(); $formatted = $money->format(); // Formatted with Money library
๐ณ One-Time Payments
Simple Charge
// Charge a customer $transaction = $user->charge(2990); // RM 29.90 // Charge with options $transaction = $user->charge(2990, [ 'description' => 'Premium Service', 'metadata' => ['service_type' => 'premium'], ]);
Using Payment Builder
$payment = $user->newCharge(2990) ->withDescription('Monthly Subscription') ->withMetadata(['plan' => 'premium']) ->create(); // Get payment URL for customer $paymentUrl = $payment->url();
Create Checkout Session
// Simple checkout $checkout = Checkout::forAmount(2990, 'MYR') ->client('customer@example.com', 'John Doe') ->successUrl('https://yoursite.com/success') ->cancelUrl('https://yoursite.com/cancel') ->create(); // Redirect customer to payment return redirect($checkout['checkout_url']);
๐ Subscriptions
Creating Subscriptions
// Create subscription $subscription = $user->newSubscription('default', 'price_monthly_premium') ->trialDays(14) ->create(); // Create subscription with immediate charge $subscription = $user->newSubscription('default', 'price_monthly_premium') ->skipTrial() ->create();
Checking Subscription Status
// Check if user has active subscription if ($user->subscribed('default')) { // User has active subscription } // Check specific price if ($user->subscribedToPrice('price_monthly_premium', 'default')) { // User is subscribed to this specific price } // Check if on trial if ($user->onTrial('default')) { // User is on trial } // Check if subscription is active if ($user->subscription('default')->active()) { // Subscription is active }
Subscription Status Types
CashierChip follows Laravel Cashier standards for subscription status handling:
// Subscription statuses (chip_status field) 'active' // Paid subscription with valid payment 'trialing' // Trial subscription (no payment required yet) 'canceled' // Subscription cancelled 'expired' // Subscription ended 'past_due' // Payment failed, awaiting retry
Important: Both 'active'
and 'trialing'
subscriptions are considered valid subscriptions for:
- User access control (
$user->subscribed()
returnstrue
) - Feature availability
- Billing operations (
upcomingInvoice()
works for both) - Business logic checks
// All these work correctly for BOTH active and trial subscriptions: $user->subscribed('default'); // โ true for both $user->subscription('default')->valid(); // โ true for both $user->upcomingInvoice(); // โ works for both $subscription->active(); // โ true for both // Specific trial checks: $user->onTrial('default'); // โ true only for trials $subscription->onTrial(); // โ true only for trials $subscription->chip_status === 'trialing'; // โ trial status check
Laravel Cashier Alignment:
This matches Laravel Cashier Paddle behavior where 'trialing'
is treated as a valid subscription state alongside 'active'
.
Note: Trial status recognition in
upcomingInvoice()
and subscription queries was fixed in v1.0.17+ to properly support both'active'
and'trialing'
statuses.
Managing Subscriptions
// Cancel subscription (at period end) $user->subscription('default')->cancel(); // Cancel immediately $user->subscription('default')->cancelNow(); // Resume cancelled subscription $user->subscription('default')->resume(); // Change subscription price $user->subscription('default')->swap('new_price_id'); // Update quantity $user->subscription('default')->updateQuantity(5);
๐ Refunds
Processing Refunds
// Full refund $refund = $user->refund('transaction_id'); // Partial refund $refund = $user->refund('transaction_id', 1000); // RM 10.00 // Refund using transaction object $transaction = $user->findTransaction('transaction_id'); $refund = $transaction->refund(500); // RM 5.00
Refund Information
$transaction = $user->findTransaction('transaction_id'); // Check if can be refunded if ($transaction->canBeRefunded()) { // Transaction can be refunded } // Get refundable amount $refundableAmount = $transaction->refundableAmount(); // Get total refunded amount $totalRefunded = $transaction->totalRefunded(); // Get all refunds for this transaction $refunds = $transaction->refunds();
๐ Customer Management
Customer Creation and Updates
// Create Chip customer $customer = $user->createAsChipCustomer([ 'name' => 'John Doe', 'email' => 'john@example.com', ]); // Update customer $customer = $user->updateChipCustomer([ 'name' => 'John Smith', ]); // Get customer $customer = $user->asChipCustomer();
Customer Information
// Check if user has Chip customer ID if ($user->hasChipId()) { $chipId = $user->chipId(); } // Sync customer data with Chip $user->syncChipCustomerData();
๐ FPX (Malaysian Bank Transfer)
Create FPX Payment
// Create FPX payment $fpx = FPX::forAmount(2990) // RM 29.90 ->bank('maybank2u') // Maybank ->client('customer@example.com', 'John Doe') ->successUrl('https://yoursite.com/success') ->cancelUrl('https://yoursite.com/cancel') ->create(); // Redirect to bank return redirect($fpx['checkout_url']);
FPX Bank List
// Get available banks $banks = FPX::banks(); foreach ($banks as $bankCode => $bankName) { echo "{$bankCode}: {$bankName}"; }
Check FPX Status
// Check payment status $status = FPX::status('purchase_id'); if ($status['status'] === 'success') { // Payment completed }
๐ฃ Webhooks
Webhook Setup
CashierChip automatically registers webhook routes. The webhooks are handled at:
POST /chip/webhook
Make sure to set your webhook URL in your Chip dashboard to:
https://yoursite.com/chip/webhook
Webhook Events
The package automatically handles these webhook events:
purchase.completed
- Payment completed successfullypurchase.failed
- Payment failed or was declinedpurchase.refunded
- Payment was refunded (full or partial)subscription.created
- New subscription activatedsubscription.updated
- Subscription plan or status changessubscription.cancelled
- Subscription cancelled or expired
Webhook Event Handling
// Listen for webhook events Event::listen(\Aizuddinmanap\CashierChip\Events\TransactionCompleted::class, function ($event) { $transaction = $event->transaction; // Send confirmation email Mail::to($transaction->billable->email)->send(new PaymentConfirmationMail($transaction)); }); Event::listen(\Aizuddinmanap\CashierChip\Events\WebhookReceived::class, function ($event) { $payload = $event->payload; // Log webhook for debugging Log::info('Webhook received: ' . $payload['event_type']); });
๐จ Blade Template Examples
Invoice List Template
@extends('layouts.app') @section('content') <div class="container"> <h1>My Invoices</h1> @if($invoices->count() > 0) <div class="table-responsive"> <table class="table table-striped"> <thead> <tr> <th>Invoice #</th> <th>Date</th> <th>Amount</th> <th>Status</th> <th>Actions</th> </tr> </thead> <tbody> @foreach($invoices as $invoice) <tr> <td>{{ $invoice->id() }}</td> <td>{{ $invoice->date()->format('M j, Y') }}</td> <td>{{ $invoice->total() }}</td> <td> <span class="badge badge-{{ $invoice->paid() ? 'success' : 'warning' }}"> {{ ucfirst($invoice->status()) }} </span> </td> <td> <a href="{{ route('invoices.download', $invoice->id()) }}" class="btn btn-sm btn-primary"> Download PDF </a> </td> </tr> @endforeach </tbody> </table> </div> @else <div class="alert alert-info"> No invoices found. </div> @endif </div> @endsection
๐ง Advanced Usage
Custom Payment Methods
// Get available payment methods $methods = $user->getAvailablePaymentMethods(); // Check specific payment method if ($user->isPaymentMethodAvailable('fpx')) { // FPX is available }
Recurring Tokens
// Charge with saved token $payment = $user->chargeWithToken('purchase_id', [ 'amount' => 10000, ]); // Delete recurring token $user->deleteRecurringToken('purchase_id');
Currency Formatting
use Aizuddinmanap\CashierChip\Cashier; // Format amount $formatted = Cashier::formatAmount(2990); // "RM 29.90" $formatted = Cashier::formatAmount(2990, 'USD'); // "$29.90" // Set default currency Cashier::useCurrency('usd', 'en_US');
๐ฐ Plans Management
CashierChip v1.0.12+ includes an optional local plans table for better performance and developer experience. This allows you to store plan details locally instead of making API calls to fetch plan information.
Benefits of Local Plans
- ๐ Performance: No external API calls to display pricing pages
- ๐ป Better DX: Rich local plan queries and relationships
- ๐จ Flexibility: Custom features, descriptions, sorting, promotional pricing
- ๐ Reliability: Works offline, no external dependencies for plan display
- ๐ฑ Modern Pattern: Follows Paddle/Stripe Cashier conventions
Setting Up Plans
First, make sure you've published the migrations and kept the plans migration:
php artisan vendor:publish --tag="cashier-migrations" # Keep the 2024_01_01_000005_create_plans_table.php file php artisan migrate
Creating Plans
use Aizuddinmanap\CashierChip\Models\Plan; // Create a plan Plan::create([ 'id' => 'basic_monthly', 'chip_price_id' => 'price_abc123', // From Chip API 'name' => 'Basic Plan', 'description' => 'Perfect for individuals getting started', 'price' => 29.99, 'currency' => 'MYR', 'interval' => 'month', 'interval_count' => 1, 'features' => [ '10 Projects', '100 MB Storage', 'Email Support' ], 'active' => true, 'sort_order' => 1, ]); // Create a yearly plan Plan::create([ 'id' => 'pro_yearly', 'chip_price_id' => 'price_def456', 'name' => 'Pro Plan', 'description' => 'Best value for growing businesses', 'price' => 299.99, 'currency' => 'MYR', 'interval' => 'year', 'features' => [ 'Unlimited Projects', '10 GB Storage', 'Priority Support', 'Advanced Analytics' ], 'sort_order' => 2, ]);
Using Plans in Your Application
// Display pricing page $plans = Plan::active()->ordered()->get(); foreach ($plans as $plan) { echo $plan->name; // "Basic Plan" echo $plan->display_price; // "RM 29.99" echo $plan->formatted_interval; // "month" foreach ($plan->features_list as $feature) { echo "โ {$feature}"; } }
Creating Subscriptions with Plans
// Method 1: Using plan ID (recommended) $subscription = $user->newSubscription('default', 'basic_monthly')->create(); // Method 2: Using Plan model directly $plan = Plan::find('pro_yearly'); $subscription = SubscriptionBuilder::forPlan($user, 'default', $plan)->create(); // Access plan from subscription $subscription = $user->subscription('default'); $plan = $subscription->plan(); echo $plan->name; // "Pro Plan" echo $plan->display_price; // "RM 299.99"
Plan Query Methods
// Get all active plans ordered by sort_order $plans = Plan::active()->ordered()->get(); // Get plans by interval $monthlyPlans = Plan::active()->interval('month')->get(); $yearlyPlans = Plan::active()->interval('year')->get(); // Get plans by currency $myrPlans = Plan::byCurrency('MYR')->get(); // Get cheapest/most expensive $cheapest = Plan::cheapest(); $premium = Plan::mostExpensive(); // Check plan features $plan = Plan::find('basic_monthly'); if ($plan->hasFeature('Email Support')) { // Plan includes email support }
Plan Helper Methods
$plan = Plan::find('pro_yearly'); // Price formatting echo $plan->display_price; // "RM 299.99" echo $plan->price_per_month; // 24.99 (for comparison) // Interval formatting echo $plan->formatted_interval; // "year" // Boolean checks $plan->isActive(); // true $plan->isMonthly(); // false $plan->isYearly(); // true // Features $plan->features_list; // Array of features $plan->hasFeature('Advanced Analytics'); // true
Building Pricing Pages
// Controller public function pricing() { $monthlyPlans = Plan::active()->interval('month')->ordered()->get(); $yearlyPlans = Plan::active()->interval('year')->ordered()->get(); return view('pricing', compact('monthlyPlans', 'yearlyPlans')); }
{{-- Blade template --}} <div class="pricing-grid"> @foreach($monthlyPlans as $plan) <div class="pricing-card"> <h3>{{ $plan->name }}</h3> <p class="description">{{ $plan->description }}</p> <div class="price">{{ $plan->display_price }}</div> <div class="interval">per {{ $plan->formatted_interval }}</div> <ul class="features"> @foreach($plan->features_list as $feature) <li>โ {{ $feature }}</li> @endforeach </ul> <a href="{{ route('subscribe', $plan->id) }}" class="btn"> Choose {{ $plan->name }} </a> </div> @endforeach </div>
Relationship with Subscriptions
// Get all subscriptions for a plan $plan = Plan::find('basic_monthly'); $subscriptions = $plan->subscriptions; // Get plan from subscription $subscription = $user->subscription('default'); $plan = $subscription->plan(); if ($plan) { echo "Subscribed to: {$plan->name}"; echo "Price: {$plan->display_price}/{$plan->formatted_interval}"; }
Migration Without Plans Table
If you prefer not to use the local plans table, you can skip the plans migration and continue using price IDs directly:
// Still works without plans table $subscription = $user->newSubscription('default', 'price_abc123')->create();
๐๏ธ Database Schema
CashierChip uses a well-structured database schema to track all payment and subscription data.
Transactions Table (Core Billing Data)
CREATE TABLE transactions ( id VARCHAR(255) PRIMARY KEY, chip_id VARCHAR(255) UNIQUE, customer_id VARCHAR(255), billable_type VARCHAR(255), billable_id BIGINT, type VARCHAR(255) DEFAULT 'charge', -- 'charge', 'refund' status VARCHAR(255), -- 'pending', 'success', 'failed', 'refunded' currency VARCHAR(3) DEFAULT 'MYR', total INTEGER, -- Amount in cents payment_method VARCHAR(255), -- 'fpx', 'card', 'ewallet' description TEXT, metadata JSON, refunded_from VARCHAR(255), -- Links refunds to original transactions processed_at TIMESTAMP, created_at TIMESTAMP, updated_at TIMESTAMP );
Plans Table (Optional - v1.0.12+)
CREATE TABLE plans ( id VARCHAR(255) PRIMARY KEY, -- e.g., 'basic_monthly', 'pro_yearly' chip_price_id VARCHAR(255) UNIQUE, -- Chip's price ID from API name VARCHAR(255), -- "Basic Plan", "Pro Plan" description TEXT, -- Plan description price DECIMAL(10,2), -- 29.99 currency VARCHAR(3) DEFAULT 'MYR', -- MYR, USD, SGD interval VARCHAR(255), -- month, year, week, day interval_count INTEGER DEFAULT 1, -- every X intervals features JSON, -- ["Feature 1", "Feature 2"] active BOOLEAN DEFAULT 1, -- is plan available sort_order INTEGER DEFAULT 0, -- display order stripe_price_id VARCHAR(255), -- future multi-gateway support created_at TIMESTAMP, updated_at TIMESTAMP, INDEX idx_active_sort (active, sort_order), INDEX idx_currency_active (currency, active), INDEX idx_interval (interval) );
Migration Files Included
2024_01_01_000001_add_chip_customer_columns.php # Adds chip_id to users table 2024_01_01_000002_create_subscriptions_table.php # Subscription management 2024_01_01_000003_create_customers_table.php # Customer data 2024_01_01_000003_create_subscription_items_table.php # Subscription items 2024_01_01_000004_create_transactions_table.php # Transaction tracking (core) 2024_01_01_000005_create_plans_table.php # Plans management (optional)
๐ Testing
Running Tests
composer test
Test Coverage
The package includes comprehensive tests:
- โ 60+ passing tests
- โ Laravel Cashier API compatibility tests
- โ Transaction-to-invoice conversion tests
- โ PDF generation tests
- โ API integration tests
- โ Database schema tests
- โ Webhook processing tests
- โ FPX functionality tests
- โ Refund processing tests
- โ Customer management tests
Test Configuration
// In your tests Http::fake([ 'api.test.chip-in.asia/api/v1/purchases/' => Http::response([ 'id' => 'purchase_123', 'checkout_url' => 'https://checkout.chip-in.asia/123', ]), ]);
๐ Migration from Direct Transaction Usage
Before (Direct Transaction Usage)
// Old way - direct transactions $transactions = $user->transactions()->successful()->get(); foreach ($transactions as $transaction) { echo $transaction->amount(); }
After (Laravel Cashier Compatible)
// New way - Laravel Cashier compatible $invoices = $user->invoices(); foreach ($invoices as $invoice) { echo $invoice->total(); }
Both approaches work perfectly! The invoice approach provides Laravel Cashier compatibility with additional features like PDF generation and proper status management.
๐ Additional Documentation
- CASHIER_INVOICE_EXAMPLES.md - Comprehensive invoice usage guide
- LARAVEL_CASHIER_ALIGNMENT.md - Technical alignment details
- LIBRARY_ASSESSMENT.md - Library analysis and improvements
๐ค Contributing
Please see CONTRIBUTING for details.
๐ Security
If you discover any security related issues, please email aizuddinmanap@gmail.com instead of using the issue tracker.
๐ License
Laravel Cashier Chip is open-sourced software licensed under the MIT license.
๐ก Key Benefits Recap
- ๐ฏ Laravel Cashier Compatible - Same API as Stripe/Paddle Cashier
- โก High Performance - Local transaction storage, no external API calls for listings
- ๐งพ Professional Invoices - PDF generation with company branding (optional dompdf)
- ๐ Transaction Foundation - Fast, reliable transaction-based architecture
- ๐ฒ๐พ Malaysia Ready - FPX support and MYR currency optimized
- ๐ก๏ธ Zero Breaking Changes - Existing code continues to work
- ๐ Powerful Queries - Rich filtering and reporting capabilities
- ๐จ UI Ready - Complete Blade templates and examples included
- โ Production Stable - v1.0.12 with all 71 tests passing (266+ assertions)
- ๐ง Battle-Tested - Metadata, invoice conversion, and PDF generation all verified
- ๐งช Modern PHPUnit - Compatible with PHPUnit 11, reduced deprecations by 98.6%
- ๐๏ธ Database Flexible - Works with both old and new transaction table schemas
- โฐ Timestamp Perfect - Full Laravel timestamp field compatibility for views
- ๐ก๏ธ Regression Protected - Comprehensive test coverage prevents timestamp bugs
๐ Troubleshooting
PDF Generation Errors
Issue 1: "Call to a member function on null" when generating PDFs
Cause: This was a null pointer error in v1.0.12 and earlier when the billable entity was null.
Solution: Upgrade to v1.0.13+ which includes proper null checks:
// Fixed in v1.0.13 - now safe with null billable $invoice = $user->findInvoice('txn_123'); $response = $invoice->downloadPDF($brandingData); // No longer crashes
Issue 2: "Call to a member function format() on null" when generating PDFs
Cause: This was a date formatting error in v1.0.13 and earlier when paid_at
was null.
Solution: Upgrade to v1.0.14+ which includes proper date null checks:
// Fixed in v1.0.14 - now safe with null paid_at dates $invoice = $user->findInvoice('txn_123'); $response = $invoice->downloadPDF($brandingData); // Shows "N/A" for null dates
Workaround for older versions: Ensure all date fields are properly set when creating invoices.
Invoice Timestamp Errors
Issue: Null pointer errors accessing $invoice->created_at
in Blade templates
Cause: Fixed in v1.0.12 - invoice conversion wasn't setting Laravel timestamp fields.
Solution: Upgrade to v1.0.12+ for proper timestamp field handling.
Missing PDF Dependencies
Issue: "PDF generation requires dompdf" error
Solution: Install the optional PDF dependency:
composer require dompdf/dompdf
PDF generation is optional - only install if you need invoice PDFs.
CashierChip v1.0.12 bridges the gap between transaction-based performance and Laravel Cashier's familiar invoice patterns - giving you the best of both worlds with production-grade stability, modern testing, and bulletproof timestamp handling! ๐