Skip to content

Commit

Permalink
Merge pull request #309 from liberu-billing/sweep/Enhanced-Subscripti…
Browse files Browse the repository at this point in the history
…on-Management-and-Billing-System

Enhanced Subscription Management and Billing System
  • Loading branch information
curtisdelicata authored Dec 24, 2024
2 parents e9d6031 + 667de85 commit e73ea41
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 50 deletions.
112 changes: 72 additions & 40 deletions app/Filament/Pages/ManageSubscriptionPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,83 +3,115 @@
namespace App\Filament\Pages;

use Filament\Pages\Page;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Card;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\DatePicker;
use Filament\Notifications\Notification;
use App\Models\Subscription;
use App\Models\PaymentMethod;
use App\Models\Products_Service;
use Illuminate\Support\Facades\Auth;

class ManageSubscriptionPage extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-credit-card';
protected static string $view = 'filament.pages.manage-subscription';

public $subscription;
public $card_number;
public $expiry_month;
public $expiry_year;
public $cvv;
public $selectedProduct;
public $renewalPeriod;
public $autoRenew;
public $startDate;

public function mount()
{
$this->subscription = Auth::user()->subscription;
if ($this->subscription) {
$this->selectedProduct = $this->subscription->product_service_id;
$this->renewalPeriod = $this->subscription->renewal_period;
$this->autoRenew = $this->subscription->auto_renew;
$this->startDate = $this->subscription->start_date;
}
}

protected function getFormSchema(): array
{
return [
TextInput::make('card_number')
->label('Card Number')
->required()
->maxLength(16),
Select::make('expiry_month')
->label('Expiry Month')
->options(array_combine(range(1, 12), range(1, 12)))
->required(),
Select::make('expiry_year')
->label('Expiry Year')
->options(array_combine(range(date('Y'), date('Y') + 10), range(date('Y'), date('Y') + 10)))
->required(),
TextInput::make('cvv')
->label('CVV')
->required()
->maxLength(4),
Card::make()
->schema([
Grid::make(2)
->schema([
Select::make('selectedProduct')
->label('Service')
->options(Products_Service::pluck('name', 'id'))
->required(),

Select::make('renewalPeriod')
->label('Billing Cycle')
->options([
'monthly' => 'Monthly',
'quarterly' => 'Quarterly',
'semi-annually' => 'Semi-annually',
'annually' => 'Annually',
])
->required(),

DatePicker::make('startDate')
->label('Start Date')
->required(),

Toggle::make('autoRenew')
->label('Auto Renew')
->default(true),
]),
]),
];
}

public function updatePaymentMethod()
public function save()
{
$this->validate();

// Here you would typically interact with your payment gateway to update the payment method
// For this example, we'll just create a new PaymentMethod record
PaymentMethod::create([
'user_id' => Auth::id(),
'card_last_four' => substr($this->card_number, -4),
'card_expiration' => $this->expiry_month . '/' . $this->expiry_year,
// Don't store the full card number or CVV for security reasons
$data = $this->validate();

$product = Products_Service::findOrFail($this->selectedProduct);

if (!$this->subscription) {
$this->subscription = new Subscription();
}

$this->subscription->fill([
'customer_id' => Auth::user()->customer->id,
'product_service_id' => $this->selectedProduct,
'start_date' => $this->startDate,
'renewal_period' => $this->renewalPeriod,
'auto_renew' => $this->autoRenew,
'price' => $product->price,
'currency' => $product->currency,
'status' => 'active',
]);


$this->subscription->save();

Notification::make()
->title('Payment method updated successfully')
->title('Subscription updated successfully')
->success()
->send();
}

public function cancelSubscription()
public function cancel()
{
$this->subscription->cancel();

$this->subscription?->cancel();
Notification::make()
->title('Subscription cancelled successfully')
->success()
->send();
}

public function resumeSubscription()
public function resume()
{
$this->subscription->resume();

$this->subscription?->resume();
Notification::make()
->title('Subscription resumed successfully')
->success()
Expand Down
33 changes: 33 additions & 0 deletions app/Jobs/ProcessSubscriptionBilling.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@


<?php

namespace App\Jobs;

use App\Models\Subscription;
use App\Services\BillingService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessSubscriptionBilling implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public function handle(BillingService $billingService)
{
Subscription::query()
->where('status', 'active')
->where('auto_renew', true)
->get()
->each(function ($subscription) use ($billingService) {
if ($subscription->needsBilling()) {
$invoice = $billingService->generateInvoice($subscription);
$billingService->processAutomaticPayment($invoice);
$subscription->renew();
}
});
}
}
59 changes: 51 additions & 8 deletions app/Models/Subscription.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,18 @@ class Subscription extends Model
'end_date',
'renewal_period',
'status',
'domain_name',
'domain_registrar',
'domain_expiration_date',
'price',
'currency',
'auto_renew',
'last_billed_at',
];

protected $dates = ['start_date', 'end_date', 'domain_expiration_date'];
protected $dates = ['start_date', 'end_date', 'last_billed_at'];

protected $casts = [
'auto_renew' => 'boolean',
'price' => 'decimal:2',
];

public function customer()
{
Expand All @@ -34,22 +40,59 @@ public function productService()
return $this->belongsTo(Products_Service::class, 'product_service_id');
}

public function invoices()
{
return $this->hasMany(Invoice::class);
}

public function renew()
{
if (!$this->auto_renew || $this->status === 'cancelled') {
return false;
}

$renewalPeriod = $this->getRenewalPeriod();
$this->end_date = Carbon::parse($this->end_date)->add($renewalPeriod);
$this->last_billed_at = now();
$this->status = 'active';
$this->save();
return $this->save();
}

public function cancel()
{
$this->auto_renew = false;
$this->status = 'cancelled';
return $this->save();
}

public function suspend()
{
$this->status = 'suspended';
return $this->save();
}

public function resume()
{
if ($this->status === 'suspended') {
$this->status = 'active';
return $this->save();
}
return false;
}

public function isActive()
{
return $this->status === 'active' && $this->end_date->isFuture();
}

public function isDomainActive()
public function needsBilling()
{
return $this->domain_name && $this->domain_expiration_date && $this->domain_expiration_date->isFuture();
if (!$this->last_billed_at) {
return true;
}

$nextBillingDate = Carbon::parse($this->last_billed_at)->add($this->getRenewalPeriod());
return $nextBillingDate->isPast() && $this->isActive();
}

private function getRenewalPeriod()
Expand All @@ -64,7 +107,7 @@ private function getRenewalPeriod()
case 'annually':
return '1 year';
default:
return '1 month'; // Default to monthly if not specified
return '1 month';
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ public function up(): void
$table->foreignId('product_service_id')->constrained('products_services')->onDelete('cascade');
$table->date('start_date');
$table->date('end_date')->nullable();
$table->enum('renewal_period', ['monthly', 'yearly']);
$table->enum('status', ['active', 'cancelled', 'expired']);
$table->enum('renewal_period', ['monthly', 'quarterly', 'semi-annually', 'annually']);
$table->enum('status', ['active', 'suspended', 'cancelled', 'expired']);
$table->decimal('price', 10, 2);
$table->string('currency', 3)->default('USD');
$table->boolean('auto_renew')->default(true);
$table->timestamp('last_billed_at')->nullable();
$table->timestamps();
});
}
Expand Down

0 comments on commit e73ea41

Please sign in to comment.