Extract Ladill Servers as standalone app at servers.ladill.com.
VPS and dedicated server ordering, managed panels, SSO, billing, and launcher integration — forked from hosting infrastructure with server-focused routes and dashboard. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,648 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Hosting;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CustomerHostingOrder;
|
||||
use App\Models\HostingAccount;
|
||||
use App\Models\HostingAccountMember;
|
||||
use App\Models\HostingOrder;
|
||||
use App\Models\HostingPlanChange;
|
||||
use App\Models\HostingProduct;
|
||||
use App\Models\RcServiceOrder;
|
||||
use App\Services\Hosting\HostingOrderFulfillmentService;
|
||||
use App\Services\Hosting\HostingPlanChangeService;
|
||||
use App\Services\Hosting\HostingRenewalCheckoutService;
|
||||
use App\Services\Hosting\ServerOrderService;
|
||||
use App\Services\Hosting\UpsellRecommendationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HostingProductController extends Controller
|
||||
{
|
||||
public function singleDomain(Request $request): View
|
||||
{
|
||||
return $this->renderHostingPage($request, HostingProduct::TYPE_SINGLE_DOMAIN, 'Single Domain Hosting');
|
||||
}
|
||||
|
||||
public function multiDomain(Request $request): View
|
||||
{
|
||||
return $this->renderHostingPage($request, HostingProduct::TYPE_MULTI_DOMAIN, 'Multi Domain Hosting');
|
||||
}
|
||||
|
||||
public function wordpress(Request $request): View
|
||||
{
|
||||
return $this->renderHostingPage($request, HostingProduct::TYPE_WORDPRESS, 'WordPress Hosting');
|
||||
}
|
||||
|
||||
public function vps(Request $request): View
|
||||
{
|
||||
return $this->renderServerPage($request, HostingProduct::TYPE_VPS, 'VPS');
|
||||
}
|
||||
|
||||
public function dedicated(Request $request): View
|
||||
{
|
||||
return $this->renderServerPage($request, HostingProduct::TYPE_DEDICATED, 'Dedicated Server');
|
||||
}
|
||||
|
||||
private function renderHostingPage(Request $request, string $type, string $title): View
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
// New Hosting Orders
|
||||
$orders = CustomerHostingOrder::query()
|
||||
->forUser($user->id)
|
||||
->whereHas('product', fn ($q) => $q->ofType($type))
|
||||
->with(['product', 'domain', 'hostingAccount', 'hostingNode'])
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
// Admin-assigned Hosting Accounts
|
||||
$sharedAccountIds = HostingAccountMember::query()
|
||||
->where('user_id', $user->id)
|
||||
->pluck('hosting_account_id');
|
||||
|
||||
$hostingAccounts = HostingAccount::query()
|
||||
->where(function ($query) use ($user, $sharedAccountIds) {
|
||||
$query->where('user_id', $user->id);
|
||||
|
||||
if ($sharedAccountIds->isNotEmpty()) {
|
||||
$query->orWhereIn('id', $sharedAccountIds);
|
||||
}
|
||||
})
|
||||
->whereHas('product', fn ($q) => $q->ofType($type))
|
||||
->with(['product', 'sites'])
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
// RC Hosting Orders (Legacy)
|
||||
$legacyHostingType = $this->getLegacyHostingTypeForType($type);
|
||||
$rcCategory = $this->getRcCategoryForType($type);
|
||||
$rcHostingOrders = collect();
|
||||
$rcServiceOrders = collect();
|
||||
|
||||
if ($legacyHostingType) {
|
||||
$rcHostingOrders = HostingOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('hosting_type', $legacyHostingType)
|
||||
->latest()
|
||||
->get();
|
||||
}
|
||||
|
||||
if ($rcCategory && Schema::hasTable('rc_service_orders')) {
|
||||
$rcServiceOrders = RcServiceOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('category', $rcCategory)
|
||||
->visibleToCustomer()
|
||||
->latest()
|
||||
->get();
|
||||
}
|
||||
|
||||
$marketingCategory = $this->getMarketingOrderCategory($type);
|
||||
|
||||
// Native form data for in-dashboard ordering
|
||||
$nativeForm = $this->nativeHostingProductForm($type);
|
||||
$userDomains = $this->getUserDomains($user);
|
||||
|
||||
return view('hosting.type', [
|
||||
'title' => $title,
|
||||
'type' => $type,
|
||||
'orders' => $orders,
|
||||
'hostingAccounts' => $hostingAccounts,
|
||||
'rcHostingOrders' => $rcHostingOrders,
|
||||
'rcServiceOrders' => $rcServiceOrders,
|
||||
'marketingCategory' => $marketingCategory,
|
||||
'nativeForm' => $nativeForm,
|
||||
'userDomains' => $userDomains,
|
||||
]);
|
||||
}
|
||||
|
||||
private function renderServerPage(Request $request, string $type, string $title): View
|
||||
{
|
||||
$user = $request->user();
|
||||
$serverOrders = app(ServerOrderService::class);
|
||||
|
||||
// New Hosting Orders
|
||||
$orders = CustomerHostingOrder::query()
|
||||
->forUser($user->id)
|
||||
->whereHas('product', fn ($q) => $q->ofType($type))
|
||||
->with(['product', 'vpsInstance'])
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
// RC Service Orders (Legacy)
|
||||
$rcCategory = $this->getRcCategoryForType($type);
|
||||
$rcServiceOrders = collect();
|
||||
|
||||
if ($rcCategory && Schema::hasTable('rc_service_orders')) {
|
||||
$rcServiceOrders = RcServiceOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('category', $rcCategory)
|
||||
->visibleToCustomer()
|
||||
->latest()
|
||||
->get();
|
||||
}
|
||||
|
||||
$marketingCategory = $this->getMarketingOrderCategory($type);
|
||||
|
||||
// Native form data for in-dashboard ordering
|
||||
$nativeForm = $this->nativeHostingProductForm($type);
|
||||
$userDomains = $this->getUserDomains($user);
|
||||
$products = HostingProduct::query()
|
||||
->active()
|
||||
->visible()
|
||||
->where('type', $type)
|
||||
->ordered()
|
||||
->get();
|
||||
|
||||
$serverOrderPayloads = [];
|
||||
foreach ($products as $product) {
|
||||
$serverOrderPayloads[$product->id] = $serverOrders->frontendPayload($product);
|
||||
}
|
||||
|
||||
$billingCycles = [
|
||||
CustomerHostingOrder::CYCLE_MONTHLY => 'Monthly',
|
||||
CustomerHostingOrder::CYCLE_QUARTERLY => 'Quarterly (3 months)',
|
||||
CustomerHostingOrder::CYCLE_SEMIANNUAL => 'Semiannual (6 months)',
|
||||
CustomerHostingOrder::CYCLE_YEARLY => 'Yearly (12 months)',
|
||||
];
|
||||
|
||||
return view('hosting.server-type', [
|
||||
'title' => $title,
|
||||
'type' => $type,
|
||||
'orders' => $orders,
|
||||
'rcServiceOrders' => $rcServiceOrders,
|
||||
'marketingCategory' => $marketingCategory,
|
||||
'nativeForm' => $nativeForm,
|
||||
'serverOrderPayloads' => $serverOrderPayloads,
|
||||
'billingCycles' => $billingCycles,
|
||||
'userDomains' => $userDomains,
|
||||
]);
|
||||
}
|
||||
|
||||
public function showAccount(
|
||||
Request $request,
|
||||
HostingAccount $account,
|
||||
?UpsellRecommendationService $upsells = null,
|
||||
?HostingRenewalCheckoutService $renewals = null
|
||||
): View
|
||||
{
|
||||
Gate::forUser($request->user())->authorize('viewAccount', $account);
|
||||
$upsells ??= app(UpsellRecommendationService::class);
|
||||
$renewals ??= app(HostingRenewalCheckoutService::class);
|
||||
|
||||
$account->load(['product', 'user', 'node', 'sites']);
|
||||
$maxDomains = $account->resource_limits['max_domains'] ?? $account->product?->max_domains ?? 1;
|
||||
$freeEmailAllowance = $account->freeEmailAllowance();
|
||||
$mailboxes = $account->loadedMailboxes();
|
||||
$mailboxCount = $mailboxes->count();
|
||||
$extraMailboxCount = $freeEmailAllowance === null ? 0 : max($mailboxCount - $freeEmailAllowance, 0);
|
||||
|
||||
return view('hosting.account', [
|
||||
'account' => $account,
|
||||
'linkedSites' => $account->sites->sortBy(fn ($site) => [$site->type !== 'primary', $site->domain])->values(),
|
||||
'maxDomains' => $maxDomains,
|
||||
'canLinkMoreDomains' => $account->sites->count() < $maxDomains,
|
||||
'ownedDomains' => $this->getUserDomains($request->user()),
|
||||
'emailUsage' => [
|
||||
'mailbox_count' => $mailboxCount,
|
||||
'free_allowance' => $freeEmailAllowance,
|
||||
'extra_count' => $extraMailboxCount,
|
||||
'paid_count' => $mailboxes->where('is_paid', true)->count(),
|
||||
'extra_total' => round($extraMailboxCount * 5, 2),
|
||||
],
|
||||
'upsellRecommendations' => $upsells->recommendationsForAccount($account),
|
||||
'renewalOptions' => $account->canBeRenewed() ? $renewals->availableRenewalOptions($account) : [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function changeAccountPlan(Request $request, HostingAccount $account, HostingPlanChangeService $planChanges): RedirectResponse
|
||||
{
|
||||
Gate::forUser($request->user())->authorize('viewAccount', $account);
|
||||
|
||||
$validated = $request->validate([
|
||||
'target_product_id' => ['required', 'integer', 'exists:hosting_products,id'],
|
||||
]);
|
||||
|
||||
$account->load(['product', 'latestOrder', 'sites', 'databases', 'node']);
|
||||
$targetProduct = HostingProduct::query()->findOrFail($validated['target_product_id']);
|
||||
|
||||
try {
|
||||
$quote = $planChanges->quote($account, $targetProduct);
|
||||
$result = $planChanges->apply($account, $targetProduct, $request->user());
|
||||
} catch (ValidationException $exception) {
|
||||
return redirect()
|
||||
->route('hosting.accounts.show', $account)
|
||||
->withErrors($exception->errors())
|
||||
->with('error', $exception->getMessage());
|
||||
} catch (\Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return redirect()
|
||||
->route('hosting.accounts.show', $account)
|
||||
->with('error', 'Unable to change the hosting plan right now. Please try again.');
|
||||
}
|
||||
|
||||
$message = match ($quote['direction']) {
|
||||
HostingPlanChange::DIRECTION_UPGRADE => $quote['charge_amount'] > 0
|
||||
? sprintf(
|
||||
'Plan upgraded to %s. A GHS %s invoice has been added for the prorated difference.',
|
||||
$targetProduct->name,
|
||||
number_format((float) $quote['charge_amount'], 2)
|
||||
)
|
||||
: sprintf('Plan upgraded to %s.', $targetProduct->name),
|
||||
HostingPlanChange::DIRECTION_DOWNGRADE => $quote['credit_amount'] > 0
|
||||
? sprintf(
|
||||
'Plan changed to %s. A GHS %s credit has been recorded for the unused balance.',
|
||||
$targetProduct->name,
|
||||
number_format((float) $quote['credit_amount'], 2)
|
||||
)
|
||||
: sprintf('Plan changed to %s.', $targetProduct->name),
|
||||
default => 'Plan updated successfully.',
|
||||
};
|
||||
|
||||
return redirect()
|
||||
->route('hosting.accounts.show', $result['account'])
|
||||
->with('success', $message);
|
||||
}
|
||||
|
||||
public function renewAccount(Request $request, HostingAccount $account, HostingRenewalCheckoutService $renewals): RedirectResponse|JsonResponse
|
||||
{
|
||||
Gate::forUser($request->user())->authorize('viewAccount', $account);
|
||||
|
||||
$durationMonths = $request->has('duration_months')
|
||||
? (int) $request->input('duration_months')
|
||||
: null;
|
||||
|
||||
try {
|
||||
$checkout = $renewals->beginCheckout(
|
||||
$account,
|
||||
$request->user(),
|
||||
route('hosting.accounts.renew.callback', $account),
|
||||
$durationMonths
|
||||
);
|
||||
} catch (ValidationException $exception) {
|
||||
if ($request->wantsJson()) {
|
||||
return response()->json(['error' => $exception->getMessage()], 422);
|
||||
}
|
||||
return back()->withErrors($exception->errors())->with('error', $exception->getMessage());
|
||||
} catch (\Throwable $exception) {
|
||||
report($exception);
|
||||
if ($request->wantsJson()) {
|
||||
return response()->json(['error' => 'Unable to start renewal payment. Please try again.'], 422);
|
||||
}
|
||||
return back()->with('error', 'Unable to start renewal payment. Please try again.');
|
||||
}
|
||||
|
||||
if ($request->wantsJson()) {
|
||||
return response()->json(['checkout_url' => $checkout['authorization_url']]);
|
||||
}
|
||||
|
||||
return redirect()->away($checkout['authorization_url']);
|
||||
}
|
||||
|
||||
public function renewAccountCallback(Request $request, HostingAccount $account, HostingRenewalCheckoutService $renewals): RedirectResponse
|
||||
{
|
||||
Gate::forUser($request->user())->authorize('viewAccount', $account);
|
||||
|
||||
$reference = (string) $request->query('reference', '');
|
||||
if ($reference === '') {
|
||||
return redirect()
|
||||
->route('hosting.accounts.show', $account)
|
||||
->with('error', 'Missing renewal payment reference.');
|
||||
}
|
||||
|
||||
try {
|
||||
$invoice = $renewals->completeCheckout($account, $request->user(), $reference);
|
||||
} catch (ValidationException $exception) {
|
||||
return redirect()
|
||||
->route('hosting.accounts.show', $account)
|
||||
->withErrors($exception->errors())
|
||||
->with('error', $exception->getMessage());
|
||||
} catch (\Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return redirect()
|
||||
->route('hosting.accounts.show', $account)
|
||||
->with('error', 'Unable to verify renewal payment. If you were charged, contact support with your payment reference.');
|
||||
}
|
||||
|
||||
$months = (int) data_get($invoice->metadata, 'months', $account->assignedDurationMonths() ?? 0);
|
||||
|
||||
return redirect()
|
||||
->route('hosting.accounts.show', $account)
|
||||
->with('success', "Hosting renewal payment confirmed. Your account has been renewed for {$months} month(s).");
|
||||
}
|
||||
|
||||
public function show(Request $request, CustomerHostingOrder $order): View
|
||||
{
|
||||
abort_if($order->user_id !== $request->user()->id, 403);
|
||||
|
||||
$order->load(['product', 'domain', 'hostingAccount', 'hostingNode', 'vpsInstance']);
|
||||
|
||||
$viewName = $order->product?->requiresContabo() ? 'hosting.server-order' : 'hosting.order';
|
||||
|
||||
return view($viewName, [
|
||||
'order' => $order,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse|RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'product_id' => 'required|exists:hosting_products,id',
|
||||
'domain_name' => 'required|string|max:255',
|
||||
'billing_cycle' => 'required|in:monthly,quarterly,yearly,biennial',
|
||||
]);
|
||||
|
||||
$product = HostingProduct::findOrFail($validated['product_id']);
|
||||
|
||||
abort_unless($product->is_active && $product->is_visible, 404, 'Product not available');
|
||||
|
||||
$price = $product->getPriceForCycle($validated['billing_cycle']);
|
||||
abort_unless($price !== null, 400, 'Invalid billing cycle for this product');
|
||||
|
||||
$order = CustomerHostingOrder::create([
|
||||
'user_id' => $request->user()->id,
|
||||
'hosting_product_id' => $product->id,
|
||||
'domain_name' => $validated['domain_name'],
|
||||
'billing_cycle' => $validated['billing_cycle'],
|
||||
'amount_paid' => $price + ($product->setup_fee ?? 0),
|
||||
'currency' => $product->display_currency,
|
||||
'status' => CustomerHostingOrder::STATUS_PENDING_APPROVAL,
|
||||
]);
|
||||
|
||||
$fulfillment = app(HostingOrderFulfillmentService::class);
|
||||
$fulfilled = $fulfillment->attemptAutoFulfillment($order)['fulfilled'];
|
||||
$customerMessage = $fulfilled
|
||||
? 'Order received. We are setting up your hosting.'
|
||||
: 'Order received. Your order is pending.';
|
||||
|
||||
if ($request->wantsJson()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $customerMessage,
|
||||
'order' => $order->fresh(),
|
||||
'redirect' => route('servers.orders.show', $order),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('servers.orders.show', $order)
|
||||
->with('success', $customerMessage);
|
||||
}
|
||||
|
||||
public function storeServer(Request $request): JsonResponse|RedirectResponse
|
||||
{
|
||||
$serverOrders = app(ServerOrderService::class);
|
||||
$validated = $request->validate([
|
||||
'product_id' => 'required|exists:hosting_products,id',
|
||||
'server_name' => 'required|string|max:255',
|
||||
'billing_cycle' => 'required|in:monthly,quarterly,semiannual,yearly',
|
||||
'region' => 'required|string|max:50',
|
||||
'image' => 'required|string|max:255',
|
||||
'license' => 'required|string|max:100',
|
||||
'application' => 'required|string|max:100',
|
||||
'default_user' => 'required|string|max:50',
|
||||
'additional_ip' => 'required|string|max:50',
|
||||
'private_networking' => 'required|string|max:50',
|
||||
'storage_type' => 'required|string|max:50',
|
||||
'object_storage' => 'required|string|max:50',
|
||||
'server_password' => ['required', 'string', 'confirmed', 'regex:'.$serverOrders->passwordPattern()],
|
||||
]);
|
||||
|
||||
$product = HostingProduct::findOrFail($validated['product_id']);
|
||||
|
||||
abort_unless($product->is_active && $product->is_visible, 404, 'Product not available');
|
||||
abort_unless($product->requiresContabo(), 400, 'Invalid product type for server order');
|
||||
|
||||
$selection = $serverOrders->selectionAndQuote($product, $validated);
|
||||
$quote = $selection['quote'];
|
||||
|
||||
$order = CustomerHostingOrder::create([
|
||||
'user_id' => $request->user()->id,
|
||||
'hosting_product_id' => $product->id,
|
||||
'domain_name' => $validated['server_name'],
|
||||
'billing_cycle' => $validated['billing_cycle'],
|
||||
'amount_paid' => (float) $quote['total'],
|
||||
'currency' => $product->display_currency,
|
||||
'status' => CustomerHostingOrder::STATUS_PENDING_APPROVAL,
|
||||
'meta' => [
|
||||
'region' => $selection['selection']['region'],
|
||||
'server_order' => [
|
||||
'selection' => $selection['selection'],
|
||||
'selection_summary' => $selection['selection_summary'],
|
||||
'quote' => $quote,
|
||||
'panel_snapshot' => $selection['panel_snapshot'] ?? [],
|
||||
'server_password_encrypted' => encrypt($validated['server_password']),
|
||||
],
|
||||
'server_panel_runtime' => $selection['panel_snapshot'] ?? [],
|
||||
],
|
||||
]);
|
||||
|
||||
$fulfillment = app(HostingOrderFulfillmentService::class);
|
||||
$fulfilled = $fulfillment->attemptAutoFulfillment($order)['fulfilled'];
|
||||
$customerMessage = $fulfilled
|
||||
? 'Order received. We are setting up your server.'
|
||||
: 'Order received. Your order is pending.';
|
||||
|
||||
if ($request->wantsJson()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $customerMessage,
|
||||
'order' => $order->fresh(),
|
||||
'redirect' => route('servers.orders.show', $order),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('servers.orders.show', $order)
|
||||
->with('success', $customerMessage);
|
||||
}
|
||||
|
||||
public function cancel(Request $request, CustomerHostingOrder $order): JsonResponse|RedirectResponse
|
||||
{
|
||||
abort_if($order->user_id !== $request->user()->id, 403);
|
||||
abort_unless($order->canBeCancelled(), 400, 'This order cannot be cancelled');
|
||||
|
||||
$order->cancel($request->input('reason'));
|
||||
|
||||
if ($request->wantsJson()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Order cancelled successfully.',
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Order cancelled successfully.');
|
||||
}
|
||||
|
||||
private function getLegacyHostingTypeForType(string $type): ?string
|
||||
{
|
||||
return match ($type) {
|
||||
HostingProduct::TYPE_SINGLE_DOMAIN => HostingOrder::TYPE_SINGLE_DOMAIN,
|
||||
HostingProduct::TYPE_MULTI_DOMAIN => HostingOrder::TYPE_MULTI_DOMAIN,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function getRcCategoryForType(string $type): ?string
|
||||
{
|
||||
return match ($type) {
|
||||
HostingProduct::TYPE_SINGLE_DOMAIN => 'single-domain-hosting',
|
||||
HostingProduct::TYPE_MULTI_DOMAIN => 'multi-domain-hosting',
|
||||
HostingProduct::TYPE_WORDPRESS => 'wordpress-hosting',
|
||||
HostingProduct::TYPE_VPS => 'vps',
|
||||
HostingProduct::TYPE_DEDICATED => 'dedicated-server',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function getMarketingOrderCategory(string $type): ?string
|
||||
{
|
||||
// Map internal hosting types to marketing order categories
|
||||
return match ($type) {
|
||||
HostingProduct::TYPE_SINGLE_DOMAIN => 'single-domain-hosting',
|
||||
HostingProduct::TYPE_MULTI_DOMAIN => 'multi-domain-hosting',
|
||||
HostingProduct::TYPE_WORDPRESS => 'wordpress-hosting',
|
||||
HostingProduct::TYPE_VPS => 'vps',
|
||||
HostingProduct::TYPE_DEDICATED => 'dedicated-server',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build native form definition from HostingProduct records.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function nativeHostingProductForm(string $productType): array
|
||||
{
|
||||
$products = HostingProduct::query()
|
||||
->active()
|
||||
->visible()
|
||||
->where('type', $productType)
|
||||
->ordered()
|
||||
->get();
|
||||
|
||||
$serverOrders = app(ServerOrderService::class);
|
||||
|
||||
$packages = $products->map(function (HostingProduct $product) use ($serverOrders) {
|
||||
if ($product->requiresContabo()) {
|
||||
$payload = $serverOrders->frontendPayload($product);
|
||||
$currency = strtoupper((string) ($payload['currency'] ?? $product->display_currency));
|
||||
$monthly = (float) data_get($payload, 'prices.monthly', 0);
|
||||
$quarterly = (float) data_get($payload, 'prices.quarterly', 0);
|
||||
$semiannual = (float) data_get($payload, 'prices.semiannual', 0);
|
||||
$yearly = (float) data_get($payload, 'prices.yearly', 0);
|
||||
$setupFees = (array) ($payload['setup_fees'] ?? []);
|
||||
|
||||
$prices = [];
|
||||
$totals = [];
|
||||
|
||||
if ($monthly > 0) {
|
||||
$prices['1'] = $currency.' '.number_format($monthly, 2);
|
||||
$totals['1'] = $currency.' '.number_format($monthly + (float) ($setupFees['monthly'] ?? 0), 2);
|
||||
}
|
||||
if ($quarterly > 0) {
|
||||
$prices['3'] = $currency.' '.number_format($quarterly / 3, 2);
|
||||
$totals['3'] = $currency.' '.number_format($quarterly + (float) ($setupFees['quarterly'] ?? 0), 2);
|
||||
}
|
||||
if ($semiannual > 0) {
|
||||
$prices['6'] = $currency.' '.number_format($semiannual / 6, 2);
|
||||
$totals['6'] = $currency.' '.number_format($semiannual + (float) ($setupFees['semiannual'] ?? 0), 2);
|
||||
}
|
||||
if ($yearly > 0) {
|
||||
$prices['12'] = $currency.' '.number_format($yearly / 12, 2);
|
||||
$totals['12'] = $currency.' '.number_format($yearly + (float) ($setupFees['yearly'] ?? 0), 2);
|
||||
}
|
||||
|
||||
return [
|
||||
'value' => (string) $product->id,
|
||||
'label' => $product->name,
|
||||
'summary' => $this->nativeProductSummary($product),
|
||||
'starting_price' => $currency.' '.number_format($monthly, 2),
|
||||
'starting_term' => '1',
|
||||
'starting_label' => $currency.' '.number_format($monthly, 2).'/mo',
|
||||
'prices' => $prices,
|
||||
'totals' => $totals,
|
||||
'pricing_source' => 'contabo_dynamic',
|
||||
];
|
||||
}
|
||||
|
||||
$currency = $product->display_currency;
|
||||
$monthly = $product->getPriceForCycle('monthly');
|
||||
$quarterly = $product->getPriceForCycle('quarterly');
|
||||
$yearly = $product->getPriceForCycle('yearly');
|
||||
$biennial = $product->getPriceForCycle('biennial');
|
||||
|
||||
$prices = [];
|
||||
$totals = [];
|
||||
|
||||
if ($monthly) {
|
||||
$prices['1'] = $currency.' '.number_format($monthly, 2);
|
||||
$totals['1'] = $currency.' '.number_format($monthly, 2);
|
||||
}
|
||||
if ($quarterly) {
|
||||
$prices['3'] = $currency.' '.number_format($quarterly / 3, 2);
|
||||
$totals['3'] = $currency.' '.number_format($quarterly, 2);
|
||||
}
|
||||
if ($yearly) {
|
||||
$prices['12'] = $currency.' '.number_format($yearly / 12, 2);
|
||||
$totals['12'] = $currency.' '.number_format($yearly, 2);
|
||||
}
|
||||
if ($biennial) {
|
||||
$prices['24'] = $currency.' '.number_format($biennial / 24, 2);
|
||||
$totals['24'] = $currency.' '.number_format($biennial, 2);
|
||||
}
|
||||
|
||||
$summary = $this->nativeProductSummary($product);
|
||||
|
||||
return [
|
||||
'value' => (string) $product->id,
|
||||
'label' => $product->name,
|
||||
'summary' => $summary,
|
||||
'starting_price' => $currency.' '.number_format($product->display_price_monthly, 2),
|
||||
'starting_term' => '1',
|
||||
'starting_label' => $currency.' '.number_format($product->display_price_monthly, 2).'/mo',
|
||||
'prices' => $prices,
|
||||
'totals' => $totals,
|
||||
];
|
||||
})->values()->all();
|
||||
|
||||
return [
|
||||
'packages' => $packages,
|
||||
'has_packages' => $products->isNotEmpty(),
|
||||
'region' => 'Global',
|
||||
'requires_domain' => true,
|
||||
'supports_os' => false,
|
||||
'os_options' => [],
|
||||
'supports_addons' => false,
|
||||
'addon_options' => [],
|
||||
'supports_ssl_toggle' => false,
|
||||
'supports_dedicated_ip' => false,
|
||||
];
|
||||
}
|
||||
|
||||
private function nativeProductSummary(HostingProduct $product): ?string
|
||||
{
|
||||
return $product->catalogSummary();
|
||||
}
|
||||
|
||||
private function getUserDomains($user): \Illuminate\Support\Collection
|
||||
{
|
||||
return \App\Models\Domain::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereNull('hosting_account_id')
|
||||
->whereDoesntHave('hostedSites')
|
||||
->where(function ($query) {
|
||||
$query->where('source', 'purchased')
|
||||
->orWhereNotNull('email_domain_id');
|
||||
})
|
||||
->orderBy('host')
|
||||
->get(['id', 'host', 'status', 'onboarding_state']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user