Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions app/app/Console/Commands/RabbitMQEventConsumer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use App\WorkspaceUser;
use App\Mail\WorkspaceSuspendedNotification;
use Carbon\Carbon;
use App\ServicePlan;
use Exception;
use Log;

Expand Down Expand Up @@ -51,6 +52,7 @@ public function handle()
$channel->queue_declare('subscription_updates', false, true, false, false);
$channel->queue_declare('payment_receipts', false, true, false, false);
$channel->queue_declare('call_quality_surveys', false, true, false, false);
$channel->queue_declare('workspace_upgrades', false, true, false, false);
$channel->queue_declare(self::CALL_ACTIVITY_QUEUE, false, true, false, false);
$channel->queue_declare(RabbitMQHelper::INVOICE_QUEUE_MONTHLY, false, true, false, false);
$channel->queue_declare(RabbitMQHelper::INVOICE_QUEUE_ANNUAL, false, true, false, false);
Expand Down Expand Up @@ -78,6 +80,8 @@ public function handle()
$channel->basic_consume('subscription_updates', '', false, false, false, false, [$this, 'handleSubscriptionUpdate']);
$channel->basic_consume('payment_receipts', '', false, false, false, false, [$this, 'handlePaymentReceipt']);
$channel->basic_consume('call_quality_surveys', '', false, false, false, false, [$this, 'handleSurvey']);
$channel->basic_consume('workspace_upgrades', '', false, false, false, false, [$this, 'handleWorkspaceUpgrade']);

$channel->basic_consume(self::CALL_ACTIVITY_QUEUE, '', false, false, false, false, [$this, 'handleCallActivity']);
$channel->basic_consume(RabbitMQHelper::INVOICE_QUEUE_MONTHLY, '', false, false, false, false, [$this, 'handleMonthlyInvoiceTask']);
$channel->basic_consume(RabbitMQHelper::INVOICE_QUEUE_ANNUAL, '', false, false, false, false, [$this, 'handleAnnualInvoiceTask']);
Expand Down Expand Up @@ -604,6 +608,93 @@ private function logConsumerError($message, array $context = [])
Log::error($message, $context);
}

/**
* Handler for Workspace Upgrades
*/
public function handleWorkspaceUpgrade($msg)
{
try {
$data = json_decode($msg->body, true);

if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
$this->logConsumerError(' [WORKSPACE_UPGRADE] Malformed JSON: ' . json_last_error_msg(), [
'body' => $msg->body
]);
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
return;
}

if (!isset($data['workspace_id']) || !isset($data['creator_id'])) {
$this->logConsumerError(' [WORKSPACE_UPGRADE] Missing workspace_id or creator_id.', [
'payload' => $data
]);
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
return;
}

$workspaceId = (int) $data['workspace_id'];
$userId = (int) $data['creator_id'];

$this->logConsumerInfo(' [WORKSPACE_UPGRADE] Entire payload:' . json_encode(['payload' => $data]));

if ($workspaceId <= 0 || $userId <= 0) {
$this->logConsumerError(' [WORKSPACE_UPGRADE] Invalid workspace_id or creator_id.', [
'payload' => $data
]);
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
return;
}

$this->info(" [WORKSPACE_UPGRADE] Received upgrade for workspace ID: " . $workspaceId);

$action = isset($data['action']) ? $data['action'] : null;
$this->info(" [WORKSPACE_UPGRADE] Action: " . $action);

switch ($action) {
case 'SUCCESSFUL_UPGRADE':
$this->info(" [WORKSPACE_UPGRADE] Processing successful upgrade for workspace ID: " . $workspaceId);
// Implementation for successful upgrade logic goes here
$user = \App\User::find($userId);
$newPlan = \App\ServicePlan::find($data['plan_id']);
if ($user && $newPlan) {
$planKey = $newPlan->key_name;
$emailData = ['user' => $user, 'plan' => $planKey];
$subject = \App\Helpers\MainHelper::createEmailSubject(sprintf("Upgraded plan to %s", $planKey));
\App\Helpers\EmailHelper::sendEmail($subject, $user->email, 'plan_upgraded', $emailData);
}
break;

case 'FAILED_UPGRADE':
$this->error(" [WORKSPACE_UPGRADE] Processing failed upgrade for workspace ID: " . $workspaceId);
// Implementation for failed upgrade logic goes here
$user = \App\User::find($userId);
$newPlan = \App\ServicePlan::find($data['plan_id']);
if ($user && $newPlan) {
$planKey = $newPlan->key_name;
$emailData = ['user' => $user, 'plan' => $planKey];
$subject = \App\Helpers\MainHelper::createEmailSubject(sprintf("Failed to upgrade plan to %s", $planKey));
\App\Helpers\EmailHelper::sendEmail($subject, $user->email, 'failed_upgrade', $emailData);
}
break;

default:
$this->logConsumerError(' [WORKSPACE_UPGRADE] Unknown action: ' . $action, [
'action' => $action,
'workspace_id' => $workspaceId
]);
break;
}

$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
$this->info(" [v] Workspace upgrade event acknowledged for workspace #" . $workspaceId);
} catch (Exception $e) {
$this->logConsumerError(' [WORKSPACE_UPGRADE] ' . $e->getMessage(), [
'exception' => get_class($e)
]);
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
}
}

public function handleMonthlyInvoiceTask($msg)
{
$this->handleInvoiceTask($msg, 'MONTHLY');
Expand Down
26 changes: 13 additions & 13 deletions app/app/Enums/WorkspaceSuspensionStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@
*/
abstract class WorkspaceSuspensionStatus
{
public const NOT_SUSPENDED = '0';
public const PENDING_SUSPENSION = 'pending_suspension';
public const REAL_SUSPENSION = 'real_suspension';
public const INITIATED = 'INITIATED';
public const SUSPENDED = 'SUSPENDED';
public const LIFTED = 'LIFTED';

public static function all(): array
{
return [
self::NOT_SUSPENDED,
self::PENDING_SUSPENSION,
self::REAL_SUSPENSION,
self::INITIATED,
self::SUSPENDED,
self::LIFTED,
];
}

public static function activeValues(): array
{
return [
self::REAL_SUSPENSION,
self::SUSPENDED,
true,
1,
'1',
Expand All @@ -33,15 +33,15 @@ public static function activeValues(): array
public static function activeDatabaseValues(): array
{
return [
self::REAL_SUSPENSION,
self::SUSPENDED,
'1',
];
}

public static function isActive($status): bool
{
return in_array($status, [
self::REAL_SUSPENSION,
self::SUSPENDED,
true,
1,
'1',
Expand All @@ -51,13 +51,13 @@ public static function isActive($status): bool
public static function label($status): string
{
if (self::isActive($status)) {
return 'Real Suspension';
return 'Suspended';
}

if ($status === self::PENDING_SUSPENSION) {
return 'Pending Suspension';
if ($status === self::INITIATED) {
return 'Initiated';
}

return 'Not Suspended';
return 'Lifted';
}
}
101 changes: 100 additions & 1 deletion app/app/Helpers/BillingDataHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use App\CustomizationsKVStore;
use App\ApiCredentialKVStore;
use App\ServicePlan;
use App\Settings;
use App\UserCredit;
use App\UserDebit;
Expand All @@ -34,7 +35,7 @@ public static function updateWorkspaceBilling($gateway, $cardData, $user, $works
}
public static function getBillingHistory($workspace) {
//$data = DB::select(sprintf('select * from (select status, cents, created_at, \'credit\' as type, user_id from users_credits union select status, cents, created_at, \'invoice\' as type, user_id from users_invoices order by created_at desc) as U where U.user_id = "%s";', $user->id)); $data = DB::select(sprintf('select * from (select status, cents, created_at, \'credit\' as type, user_id from users_credits union select status, cents, created_at, \'invoice\' as type, user_id from users_invoices order by created_at desc) as U where U.user_id = "%s";', $user->id));
$data = DB::select(sprintf('select id, status, cents, created_at, \'invoice\' as type, user_id from users_invoices order by created_at desc where workspace_id = "%s";', $workspace->id));
$data = DB::select(sprintf('select id, status, cents, created_at, \'invoice\' as type, user_id from users_invoices where workspace_id = "%s" order by created_at desc;', $workspace->id));
$data = array_map(function($item) {
$array = (array) $item;
$array['dollars'] = self::toDollars($array['cents']);
Expand Down Expand Up @@ -292,4 +293,102 @@ public static function refundInvoice($invoice)
$invoice->status = PaymentStatus::REFUNDED;
$invoice->save();
}

public static function getUpgradeData($subscription, $planKey, $workspace) {
$newPlan = ServicePlan::where('key_name', $planKey)->first();
$oldPlan = ServicePlan::find($subscription->current_plan_id);

$now = new \DateTime();

// 2. Determine start date cleanly without a ternary
$last_billable_date = $subscription->last_billed_at;
if (!$last_billable_date) {
$last_billable_date = $subscription->created_at;
}

// 3. Proration Calculation using Timestamp Precision
// Calculate period end in real time based on billing flow and cycle
$customizations = CustomizationsKVStore::getRecord();
$billingFlow = $customizations['billing_flow'];

$isAnnualSubscription = false;
if ($subscription->billing_cycle === 'ANNUAL') {
$isAnnualSubscription = true;
}

if ($billingFlow === 'ANNUAL') {
if ($isAnnualSubscription) {
$periodEnd = (clone $now)->modify('first day of january next year')->setTime(0, 0, 0);
} else {
$periodEnd = (clone $now)->modify('first day of next month')->setTime(0, 0, 0);
}
} else {
// ANNIVERSARY billing flow
$anchorDate = new \DateTime($last_billable_date);
if ($isAnnualSubscription) {
$periodEnd = (clone $anchorDate)->modify('+1 year');
} else {
$periodEnd = (clone $anchorDate)->modify('+1 month');
}
}

$lastBilled = new \DateTime($last_billable_date);

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

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

// Explicit pricing logic without ternaries
$isAnnualSubscription = false;
if ($subscription->billing_cycle === 'ANNUAL') {
$isAnnualSubscription = true;
}

if ($isAnnualSubscription) {
$oldPrice = $oldPlan->annual_cost_cents;
$newPrice = $newPlan->annual_cost_cents;
} else {
$oldPrice = $oldPlan->monthly_cost_cents;
$newPrice = $newPlan->monthly_cost_cents;
}

\Log::info('Plan upgrade pricing calculation', [
'workspace_id' => $workspace->id,
'old_plan_id' => $oldPlan->id,
'new_plan_id' => $newPlan->id,
'is_annual' => $isAnnualSubscription,
'old_price_cents' => $oldPrice,
'new_price_cents' => $newPrice,
]);

$priceDiff = $newPrice - $oldPrice;

$prorationCents = 0;
if ($priceDiff > 0 && $remainingSeconds > 0) {
$prorationCents = ($priceDiff * $remainingSeconds) / $totalCycleSeconds;
}

\Log::info('Proration calculation', [
'workspace_id' => $workspace->id,
'price_diff_cents' => $priceDiff,
'remaining_seconds' => $remainingSeconds,
'total_cycle_seconds' => $totalCycleSeconds,
'proration_cents' => round($prorationCents),
]);



$feesCents = (int) round($prorationCents);
return [
'fees' => $feesCents,
'fees_dollars' => MainHelper::toDollars($feesCents),
'new_plan_cost_cents' => $newPrice,
'new_plan_cost_dollars' => MainHelper::toDollars($newPrice),
'new_plan' => $newPlan,
'old_plan' => $oldPlan,
];
}
}
51 changes: 21 additions & 30 deletions app/app/Helpers/WorkspaceSuspensionHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,19 @@ public static function getGlobalGracePeriod()

public static function getActiveSuspension($workspaceId)
{
if (!Schema::hasTable('workspace_suspensions')) {
return null;
}

$suspensions = WorkspaceSuspension::where('workspace_id', $workspaceId)
$suspension = WorkspaceSuspension::where('workspace_id', $workspaceId)
->where('status', '=', WorkspaceSuspensionStatus::SUSPENDED)
->orderBy('suspended_at', 'desc')
->get();
->first();

foreach ($suspensions as $suspension) {
if (WorkspaceSuspensionStatus::isActive($suspension->status)) {
return $suspension;
}
}

return null;
return $suspension;
}

public static function getGracePeriodExtension($workspaceId)
{
if (Schema::hasColumn('workspaces', 'grace_period_extension')) {
$workspace = Workspace::find($workspaceId);
if ($workspace && $workspace->grace_period_extension !== null) {
return (int) $workspace->grace_period_extension;
}
$workspace = Workspace::find($workspaceId);
if ($workspace && $workspace->grace_period_extension !== null) {
return (int) $workspace->grace_period_extension;
}

$suspension = self::getActiveSuspension($workspaceId);
Expand Down Expand Up @@ -76,30 +65,32 @@ public static function saveGracePeriodExtension(Workspace $workspace, $value)
$extension = (int) $value;
}

if (Schema::hasColumn('workspaces', 'grace_period_extension')) {
$workspace->grace_period_extension = $extension;
$workspace->save();
}
$workspace->grace_period_extension = $extension;
$workspace->save();

if ($extension !== null && !$workspace->active) {
$workspace->active = true;
$workspace->save();
}

if (!Schema::hasTable('workspace_suspensions')) {
return $workspace;
}

$suspensions = WorkspaceSuspension::where('workspace_id', $workspace->id)->get();
$suspensions = WorkspaceSuspension::where('workspace_id', $workspace->id)
->where('status', '!=', WorkspaceSuspensionStatus::LIFTED)
->get();

if ($suspensions->isEmpty()) {
return $workspace;
}

foreach ($suspensions as $suspension) {
$suspension->grace_period_extension = $extension;
if ($extension !== null) {
$suspension->status = WorkspaceSuspensionStatus::NOT_SUSPENDED;
$suspension->grace_period_extension = $extension;

$now = new DateTime();
$gracePeriodEnd = (new DateTime($suspension->suspension_initiated_at))->modify("+{$extension} days");
if ($now < $gracePeriodEnd) {
$suspension->status = WorkspaceSuspensionStatus::INITIATED;
$suspension->suspended_at = null;
}
}
$suspension->save();
}
Expand All @@ -109,7 +100,7 @@ public static function saveGracePeriodExtension(Workspace $workspace, $value)

public static function suspendWorkspace(Workspace $workspace, $invoice = null, $daysPastDue = null, $threshold = null)
{
if (!Schema::hasTable('workspace_suspensions')) {
if (!Schema::hasTable('workspaces_suspensions')) {
return null;
}

Expand Down
Loading
Loading