Skip to content
25 changes: 25 additions & 0 deletions app/app/Enums/ServicePlanStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Enums;

/**
* Service Plan Statuses
*/
abstract class ServicePlanStatus
{
public const ACTIVE = 'ACTIVE';
public const INACTIVE = 'INACTIVE';
public const DECOMMISSIONED = 'DECOMMISSIONED';

/**
* Optional: Helper to get all values for validation
*/
public static function all(): array
{
return [
self::ACTIVE,
self::INACTIVE,
self::DECOMMISSIONED,
];
}
}
17 changes: 17 additions & 0 deletions app/app/Helpers/RabbitMQHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,21 @@ public static function dispatchAnnualInvoiceTask($workspaceIdOrPayload, $trigger

return self::publish(self::INVOICE_QUEUE_ANNUAL, $payload);
}

public static function dispatchWorkspaceUpgrade($workspaceId, $upgradeFee, $subscriptionId, $currentPlan, $scheduledPlan, $scheduledEffectiveDate)
{
$payload = [
'run_id' => 'workspace_upgrade_' . (int) $workspaceId . '_' . time(),
'workspace_id' => (int) $workspaceId,
'subscription_id' => (int) $subscriptionId,
'upgrade_fee' => (int) $upgradeFee,
'current_plan' => (int) $currentPlan,
'scheduled_plan' => (int) $scheduledPlan,
'scheduled_effective_date' => (string) $scheduledEffectiveDate,
];

return self::publish('workspace_upgrades', $payload);
}


}
40 changes: 23 additions & 17 deletions app/app/Helpers/WorkspaceInvoiceHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,27 @@ public static function sendInvoiceForWorkspace(Workspace $workspace, $period, Us
$filePrefix = 'monthly';
}

Mail::send('emails.workspace_invoices', [
'user' => $owner,
'workspace' => $workspace,
'site' => $siteName,
'site_name' => $siteName,
'customizations' => $customizations,
'period' => $period
], function ($message) use ($owner, $mailConfig, $subject, $pdf, $invoice, $filePrefix) {
$message->to($owner->email);
$message->subject($subject);
$from = $mailConfig['from'];
$message->from($from['address'], $from['name']);
$message->attachData($pdf, sprintf('%s_invoice_%s.pdf', $filePrefix, $invoice->invoice_no), [
'mime' => 'application/pdf'
]);
});
try {
Mail::send('emails.workspace_invoices', [
'user' => $owner,
'workspace' => $workspace,
'site' => $siteName,
'site_name' => $siteName,
'customizations' => $customizations,
'period' => $period
], function ($message) use ($owner, $mailConfig, $subject, $pdf, $invoice, $filePrefix) {
$message->to($owner->email);
$message->subject($subject);
$from = $mailConfig['from'];
$message->from($from['address'], $from['name']);
$message->attachData($pdf, sprintf('%s_invoice_%s.pdf', $filePrefix, $invoice->invoice_no), [
'mime' => 'application/pdf'
]);
});
} catch (\Exception $e) {
\Log::error('Failed to send invoice email for workspace #' . $workspace->id . ': ' . $e->getMessage());
throw $e;
}

return [
'workspace_id' => $workspace->id,
Expand Down Expand Up @@ -257,7 +262,8 @@ private static function createInvoice(User $owner, Workspace $workspace, $period
'recording_costs' => $recordingCosts,
'fax_costs' => $faxCosts,
'membership_costs' => $membershipCosts,
'number_costs' => $numberCosts
'number_costs' => $numberCosts,
'source_service' => 'SITE'
]);

$invoice->created_at = $rangeStart;
Expand Down
9 changes: 7 additions & 2 deletions app/app/Http/Controllers/Admin/ServicePlanController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use App\PortNumber;
use App\Subscription;
use App\WorkspaceUser;

use App\Enums\ServicePlanStatus;
use App\Http\Requests\Admin\ServicePlanRequest;
use App\Helpers\MainHelper;
use Datatables;
Expand Down Expand Up @@ -45,7 +47,9 @@ public function create()
$features = $this->getFeatureOptions();
$callDurations = $this->getCallDurations();
$recordingSpace = $this->getRecordingSpaceOptions();
return view('admin.serviceplan.create_edit', compact('features', 'callDurations', 'recordingSpace'));
$statuses = ServicePlanStatus::all();

return view('admin.serviceplan.create_edit', compact('features', 'callDurations', 'recordingSpace', 'statuses'));
}

/**
Expand Down Expand Up @@ -86,12 +90,13 @@ public function edit(ServicePlan $serviceplan)
$features = $this->getFeatureOptions();
$callDurations = $this->getCallDurations();
$recordingSpace = $this->getRecordingSpaceOptions();
$statuses = ServicePlanStatus::all();
$migratePlans = [];
$allPlans = ServicePlan::where('id', '!=', $serviceplan->id)->get()->toArray();
foreach ($allPlans as $key => $plan) {
$migratePlans[$plan['nice_name']] = $plan['nice_name'] . ' (' . $plan['key_name'] . ')';
}
return view('admin.serviceplan.create_edit', compact('serviceplan', 'features', 'callDurations', 'recordingSpace', 'migratePlans'));
return view('admin.serviceplan.create_edit', compact('serviceplan', 'features', 'callDurations', 'recordingSpace', 'migratePlans', 'statuses'));
}
/**
* Migrate all users on this service plan to a target service plan.
Expand Down
6 changes: 1 addition & 5 deletions app/app/Http/Controllers/HomeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,7 @@ public function index()
}
public function pricing(Request $request)
{
$plans = ServicePlan::where('include_in_pricing_pages', '1')
->orderBy('rank')
->orderBy('nice_name')
->get();
$plans = ServicePlan::sortPlansByFeatures($plans);
$plans = ServicePlan::getAvailablePlans(TRUE);
$competitors = Competitor::all();
$savings = CostSaving::select(array('cost_savings.*', DB::raw('competitors.name AS competitor_name')));
$savings = $savings->join('competitors', 'competitors.id', '=', 'cost_savings.competitor_id');
Expand Down
140 changes: 89 additions & 51 deletions app/app/Http/Controllers/MergedController.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
use App\Helpers\DNSHelper;
use App\Helpers\PhoneProvisionHelper;
use App\Helpers\BillingDataHelper;
use App\Helpers\RabbitMQHelper;

use App\Helpers\PortalSearchHelper;
use App\Helpers\SMSHelper;
Expand Down Expand Up @@ -197,85 +198,116 @@ public function plans(Request $request)
public function upgradePlan(Request $request)
{
$json = $request->json()->all();
$planKey = $json['plan'];
$planKey = null;
if (isset($json['plan'])) {
$planKey = $json['plan'];
}

if (!$planKey) {
return $this->response->errorBadRequest("Plan key is required.");
}

$workspace = $this->getWorkspace($request);
$user = $this->getUser($request);
$now = new \DateTime();

// 1. Get the current subscription and the target plan
// 1. Fetch data & early guard clauses
$subscription = Subscription::where('workspace_id', $workspace->id)->first();

if (!$subscription) {
return $this->response->errorBadRequest("Invalid subscription or plan.");
}

if (!empty($subscription->scheduled_plan_id)) {
return $this->response->errorBadRequest("A plan upgrade is already scheduled.");
}

$newPlan = ServicePlan::where('key_name', $planKey)->first();
$oldPlan = ServicePlan::find($subscription->current_plan_id);

if (!$subscription || !$newPlan) {
return $this->response->errorBadRequest("Invalid subscription or plan.");
if (!$newPlan || !$oldPlan) {
return $this->response->errorBadRequest("Invalid subscription or plan config.");
}

// 2. Calculate Proration
// We determine the daily rate difference and multiply by days remaining
$periodEnd = new \DateTime($subscription->current_period_end);
$remainingDays = $now->diff($periodEnd)->days;

// Determine the total days in this cycle to get an accurate daily ratio
// 2. Determine start date cleanly without a ternary
$start_date = $subscription->last_billed_at;
if (!$start_date) {
$start_date = $subscription->created_at;
}

// 3. Proration Calculation using Timestamp Precision
$periodEnd = new \DateTime($subscription->current_period_end);
$lastBilled = new \DateTime($start_date);
$totalCycleDays = $lastBilled->diff($periodEnd)->days;
if ($totalCycleDays == 0) {
$totalCycleDays = 30; // Default to 30 if zero

$totalCycleSeconds = $periodEnd->getTimestamp() - $lastBilled->getTimestamp();
$remainingSeconds = $periodEnd->getTimestamp() - $now->getTimestamp();

if ($totalCycleSeconds <= 0) {
$totalCycleSeconds = 30 * 86400; // Default fallback to 30 days in seconds
}

if ($subscription->billing_cycle === 'ANNUAL') {
// Explicit pricing logic without ternaries
$isAnnual = ($subscription->billing_cycle === 'ANNUAL');
if ($isAnnual) {
$oldPrice = $oldPlan->annual_cost_cents;
$newPrice = $newPlan->annual_cost_cents;
} else {
$oldPrice = $oldPlan->monthly_cost_cents;
$newPrice = $newPlan->monthly_cost_cents;
}

$priceDiff = $newPrice - $oldPrice;

$prorationCents = 0;
if ($priceDiff > 0) {
$prorationCents = ($priceDiff * $remainingDays) / $totalCycleDays;

// 3. Create the pending line item (orphan item with invoice_id = NULL)
UserInvoiceLineItem::create([
'created_at' => $now,
'updated_at' => $now,
'is_recurring' => 0,
'name' => "Upgrade Adjustment: {$oldPlan->name} to {$newPlan->name}",
'cents' => (int) $prorationCents,
'invoice_id' => null,
'key_name' => 'plan_upgrade_proration'
]);
if ($priceDiff > 0 && $remainingSeconds > 0) {
$prorationCents = ($priceDiff * $remainingSeconds) / $totalCycleSeconds;
}

// 4. Update Subscription and Workspace
$subscription->update([
'current_plan_id' => $newPlan->id,
'updated_at' => $now
]);
// 4. Determine Scheduled Effective Date without ternaries
$scheduledEffectiveDate = null;
$customizations = CustomizationsKVStore::getRecord();

$billingFlow = 'ANNIVERSARY';
if (isset($customizations['billing_flow'])) {
$billingFlow = $customizations['billing_flow'];
}

// Update Plan Usage Periods
PlanUsagePeriod::where('workspace_id', $workspace->id)
->whereNull('ended_at')
->update(['ended_at' => $now]);
if ($billingFlow === 'ANNUAL') {
if ($isAnnual) {
$modifier = 'first day of january next year';
} else {
$modifier = 'first day of next month';
}
$scheduledEffectiveDate = (clone $now)->modify($modifier)->setTime(0, 0, 0);
} else {
$anchor_date = $subscription->last_billed_at;
if (!$anchor_date) {
$anchor_date = $subscription->created_at;
}
$anchorDateTime = new \DateTime($anchor_date);

if ($isAnnual) {
$modifier = '+1 year';
} else {
$modifier = '+1 month';
}
$scheduledEffectiveDate = (clone $anchorDateTime)->modify($modifier);
}

PlanUsagePeriod::create([
'workspace_id' => $workspace->id,
'started_at' => $now,
'plan' => $planKey
]);
RabbitMQHelper::dispatchWorkspaceUpgrade(
$workspace->id,
(int) round($prorationCents),
$subscription->id,
$subscription->current_plan_id,
$newPlan->id,
$scheduledEffectiveDate ? $scheduledEffectiveDate->format('Y-m-d H:i:s') : null
);

// Analytics and Communication
// 6. Side Effect Operations (Ideally should be queued background jobs)
$props = [
"billing_status" => "immediate_upgrade_pending_reconciliation",
"new_plan" => $planKey,
"proration_applied" => $prorationCents
"proration_applied" => (int) round($prorationCents)
];
WorkspaceEvent::addEvent($workspace, 'PLAN_UPGRADED', $props);

Expand Down Expand Up @@ -303,7 +335,15 @@ public function dashboard(Request $request)
$workspace = $this->getWorkspace($request);
//$plans = \Config::get("service_plans");
//$plan = $plans[ $workspace->plan ];
$plan = ServicePlan::where('key_name', $workspace->plan)->firstOrFail();
$subscription = Subscription::where('workspace_id', $workspace->id)->first();
$currentPlan = ServicePlan::find($subscription->current_plan_id);
$scheduledPlan = ServicePlan::find($subscription->scheduled_plan_id);
$effectivePlan = $currentPlan;
if (!empty($scheduledPlan)) {
$effectivePlan = $scheduledPlan;
}

$plan = $effectivePlan;
while ($currentDay != $dayCount) {
$labels[] = $cloned1->format("M-d");
$cloned2 = clone $cloned1;
Expand Down Expand Up @@ -467,7 +507,8 @@ public function billing(Request $request)
$cards,
$config,
$billingHistory,
$triggers
$triggers,
$subscription->toArray()
]);
}
public function getCountries() {
Expand Down Expand Up @@ -873,10 +914,7 @@ public function getAllSettings(Request $request) {
}

public function getServicePlans(Request $request) {
$plans = ServicePlan::orderBy('rank')
->orderBy('nice_name')
->get()
->toArray();
$plans = ServicePlan::getAvailablePlans();

$features = [
'fax',
Expand Down Expand Up @@ -916,7 +954,7 @@ public function getServicePlans(Request $request) {
$plan_benefits = [];
// compare the previous plan with the current one to get the benefits
$last_cnt = $cnt - 1;
if ( array_key_exists( $last_cnt, $plans) ) {
if ( isset( $plans[$last_cnt] ) ) {
$last_plan = $plans[$last_cnt];
foreach ($features as $feature) {
$has_feature_1 = $plan[$feature];
Expand Down
14 changes: 14 additions & 0 deletions app/app/ServicePlan.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Database\Eloquent\SoftDeletes;
use App\ApiResource;
use App\Helpers\MainHelper;
use App\Enums\ServicePlanStatus;

class ServicePlan extends Model {
use SoftDeletes;
Expand Down Expand Up @@ -97,4 +98,17 @@ public static function getRecurringMembershipPlans() {
return self::where('pay_as_you_go', '0')->get();
}

public static function getAvailablePlans($pricingPage = false) {
$plans = ServicePlan::where('status', ServicePlanStatus::ACTIVE)
->orderBy('rank')
->orderBy('nice_name');
if ($pricingPage) {
$plans = $plans->where('include_in_pricing_pages', '1');
}

return $plans->get();
}



}
Loading
Loading