diff --git a/app/app/Http/Controllers/MergedController.php b/app/app/Http/Controllers/MergedController.php index 5c8e84f0a..856c805af 100755 --- a/app/app/Http/Controllers/MergedController.php +++ b/app/app/Http/Controllers/MergedController.php @@ -375,6 +375,21 @@ public function dashboard(Request $request) $checklist['added_billing_info'] = TRUE; } + // Add metrics to API result + $today = date('Y-m-d'); + $callsToday = Call::where('workspace_id', $workspace->id) + ->whereDate('created_at', '=', $today) + ->count(); + + $activeDIDs = DIDNumber::where('workspace_id', $workspace->id)->count(); + + $activeMembers = WorkspaceUser::where('workspace_id', $workspace->id)->count(); + + $metrics = [ + 'calls_today' => $callsToday, + 'active_did_numbers' => $activeDIDs, + 'active_members' => $activeMembers + ]; return $this->response->array([ $graph, @@ -382,7 +397,8 @@ public function dashboard(Request $request) $self->toArray(TRUE), $checklist, $plan, - $workspace->toArrayWithRoles($user) + $workspace->toArrayWithRoles($user), + $metrics ]); } @@ -1015,7 +1031,6 @@ public function getServicePlans(Request $request) { $trialInterval = new \DateInterval('P' . $trialDurationDays . 'D'); $nextMonthlyDateWithTrial = clone $currentDate; - $nextMonthlyDateWithTrial->add(new \DateInterval('P1M')); // Handle date clamping for monthly $originalDay = $currentDate->format('d'); @@ -1029,7 +1044,6 @@ public function getServicePlans(Request $request) { $billingDates['next_monthly_billing_date_w_trial_formatted'] = $nextMonthlyDateWithTrial->format('M d, Y'); $nextAnnualDateWithTrial = clone $currentDate; - $nextAnnualDateWithTrial->add(new \DateInterval('P1Y')); // Handle date clamping for annual (leap years) $originalDay = $currentDate->format('d'); diff --git a/app/app/Http/Controllers/RegisterController.php b/app/app/Http/Controllers/RegisterController.php index a851af0e7..0ab4c31c3 100755 --- a/app/app/Http/Controllers/RegisterController.php +++ b/app/app/Http/Controllers/RegisterController.php @@ -231,6 +231,7 @@ public function userSpinup(Request $request) 'default_region' => $region->code ]); + $billingFlow = $customizations['billing_flow']; Log::info('adding new user to SIP proxy database tables.'); $result = SIPRouterHelper::updateProxyToEnableWorkspace($user, $workspace, $info['proxy']); @@ -247,29 +248,54 @@ public function userSpinup(Request $request) $now = new DateTime(); $anchorDay = (int)$now->format('j'); + $isTrial = false; + + if ($customizations['is_trial_enabled'] && !$plan['free_trial_exempt']) { + $isTrial = TRUE; + } - if ($billingCycle === 'ANNUAL') { - $periodEnd = (clone $now)->modify('+1 year')->setTime(0,0,0); - $recurringCost = $plan->annual_cost_cents; - } else { - // FIXED: Look ahead to prevent native PHP +1 month edge-case rollover anomalies - $nextMonth = (clone $now)->modify('+1 month'); - $daysInNextMonth = (int)$nextMonth->format('t'); + $trialDurationDays = $customizations['trial_duration_days']; - if ($anchorDay > $daysInNextMonth) { - // Clamp period end to absolute upper bound of the target calendar block - $periodEnd = $nextMonth->setDate((int)$nextMonth->format('Y'), (int)$nextMonth->format('n'), $daysInNextMonth)->setTime(0,0,0); + if ($billingFlow === 'ANNUAL') { + if ($billingCycle === 'ANNUAL') { + $periodEnd = (clone $now)->modify('+1 year')->setTime(0,0,0); + $recurringCost = $plan->annual_cost_cents; } else { - $periodEnd = (clone $now)->modify('+1 month')->setTime(0,0,0); + // FIXED: Look ahead to prevent native PHP +1 month edge-case rollover anomalies + $nextMonth = (clone $now)->modify('+1 month'); + $daysInNextMonth = (int)$nextMonth->format('t'); + + if ($anchorDay > $daysInNextMonth) { + // Clamp period end to absolute upper bound of the target calendar block + $periodEnd = $nextMonth->setDate((int)$nextMonth->format('Y'), (int)$nextMonth->format('n'), $daysInNextMonth)->setTime(0,0,0); + } else { + $periodEnd = (clone $now)->modify('+1 month')->setTime(0,0,0); + } + $recurringCost = $plan->monthly_cost_cents; + } + } else if ($billingFlow === 'ANNIVERSARY') { + + if ($isTrial) { + if ($billingCycle === 'ANNUAL') { + $periodEnd = (clone $now)->modify('+1 year'); + $recurringCost = $plan->annual_cost_cents; + } else { + $periodEnd = (clone $now)->modify('+1 month'); + $recurringCost = $plan->monthly_cost_cents; + } + } else { + $periodEnd = (clone $now)->modify(sprintf('+%d days', $trialDurationDays)); + if ($billingCycle === 'ANNUAL') { + $recurringCost = $plan->annual_cost_cents; + } else { + $recurringCost = $plan->monthly_cost_cents; + } } - $recurringCost = $plan->monthly_cost_cents; } //$billingFlow = MainHelper::getBillingFlow($customizations); - $billingFlow = $customizations['billing_flow']; $nextBillingDateStr = $periodEnd->format('Y-m-d'); - - $subscription = Subscription::create([ + $subscriptionParams = [ 'workspace_id' => $workspace->id, 'current_plan_id' => $plan->id, 'status' => 'ACTIVE', @@ -277,7 +303,15 @@ public function userSpinup(Request $request) 'current_period_end' => $periodEnd, 'next_billing_date' => $nextBillingDateStr, 'billing_anchor_day' => $anchorDay - ]); + ]; + + if ($isTrial) { + $subscriptionParams['is_free_trial_active'] = true; + $subscriptionParams['free_trial_start_date'] = $now; + $subscriptionParams['free_trial_end_date'] = $periodEnd; + } + + $subscription = Subscription::create($subscriptionParams); Log::info("Recurring cost: {$recurringCost} cents"); $recurringCostInDollars = $recurringCost / 100; @@ -292,16 +326,18 @@ public function userSpinup(Request $request) Log::info("Amount to charge for signup: {$amountToCharge} dollars"); try { - RabbitMQHelper::dispatchImmediateBilling( - $workspace, - $subscription, - $user, - $plan, - $billingCycle, - $amountToCharge, - $nextBillingDateStr - ); - Log::info("Signup Billing Queued: Workspace {$workspace->id}, Amount: {$amountToCharge}"); + if (!$isTrial) { + RabbitMQHelper::dispatchImmediateBilling( + $workspace, + $subscription, + $user, + $plan, + $billingCycle, + $amountToCharge, + $nextBillingDateStr + ); + Log::info("Signup Billing Queued: Workspace {$workspace->id}, Amount: {$amountToCharge}"); + } } catch (\Exception $e) { Log::error("RabbitMQ Billing Dispatch Failed: " . $e->getMessage()); } diff --git a/app/app/ServicePlan.php b/app/app/ServicePlan.php index 60ca0d310..a964523f5 100755 --- a/app/app/ServicePlan.php +++ b/app/app/ServicePlan.php @@ -28,7 +28,8 @@ class ServicePlan extends Model { 'featured_plan' => 'boolean', 'pay_as_you_go' => 'boolean', 'registration_plan' => 'boolean', - 'include_in_pricing_pages' => 'boolean' + 'include_in_pricing_pages' => 'boolean', + 'free_trial_exempt' => 'boolean' ); protected $table ='service_plans'; public static function sortPlansByFeatures( $plans ) diff --git a/app/app/Subscription.php b/app/app/Subscription.php index cea0a4ebc..76d615226 100755 --- a/app/app/Subscription.php +++ b/app/app/Subscription.php @@ -15,6 +15,7 @@ class Subscription extends Model { protected $guarded = array('id'); protected $table = "subscriptions"; protected $casts = array( + 'is_free_trial_active' => 'boolean', ); public function toArray() diff --git a/app/database/migrations/2026_06_25_172544_add_free_trial_flag_to_subscriptions.php b/app/database/migrations/2026_06_25_172544_add_free_trial_flag_to_subscriptions.php new file mode 100644 index 000000000..84801b23b --- /dev/null +++ b/app/database/migrations/2026_06_25_172544_add_free_trial_flag_to_subscriptions.php @@ -0,0 +1,36 @@ +boolean('is_free_trial_active')->default(false); + $table->dateTime('free_trial_start_date')->nullable(); + $table->dateTime('free_trial_end_date')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('subscriptions', function (Blueprint $table) { + $table->dropColumn('is_free_trial_active'); + $table->dropColumn('free_trial_start_date'); + $table->dropColumn('free_trial_end_date'); + }); + } +}