Deploy Ladill Hosting / deploy (push) Successful in 47s
Treat ns_auto domains as managed DNS, publish subdomain A records via PowerDNS without failing the UX when the local DNS registry is missing, and promote real linked domains over pending-setup.local placeholders. Co-authored-by: Cursor <cursoragent@cursor.com>
664 lines
27 KiB
PHP
664 lines
27 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Hosting;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\CustomerHostingOrder;
|
|
use App\Models\HostingAccount;
|
|
use App\Models\HostingOrder;
|
|
use App\Models\HostingPlanChange;
|
|
use App\Models\HostingProduct;
|
|
use App\Models\RcServiceOrder;
|
|
use App\Services\Hosting\HostingAccessResolver;
|
|
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 = app(HostingAccessResolver::class)->sharedAccountIds($user);
|
|
|
|
$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);
|
|
|
|
$pendingStatuses = ['pending_payment', 'pending_approval', 'approved', 'provisioning'];
|
|
$heroStats = [
|
|
'accounts' => $hostingAccounts->count(),
|
|
'active' => $hostingAccounts->where('status', 'active')->count()
|
|
+ $orders->where('status', 'active')->count()
|
|
+ $rcHostingOrders->where('status', 'active')->count()
|
|
+ $rcServiceOrders->where('status', 'active')->count(),
|
|
'pending' => $orders->whereIn('status', $pendingStatuses)->count()
|
|
+ $rcHostingOrders->whereIn('status', $pendingStatuses)->count()
|
|
+ $rcServiceOrders->whereIn('status', $pendingStatuses)->count(),
|
|
'plans' => count($nativeForm['packages'] ?? []),
|
|
];
|
|
|
|
return view('hosting.type', [
|
|
'title' => $title,
|
|
'type' => $type,
|
|
'orders' => $orders,
|
|
'hostingAccounts' => $hostingAccounts,
|
|
'rcHostingOrders' => $rcHostingOrders,
|
|
'rcServiceOrders' => $rcServiceOrders,
|
|
'marketingCategory' => $marketingCategory,
|
|
'nativeForm' => $nativeForm,
|
|
'userDomains' => $userDomains,
|
|
'heroStats' => $heroStats,
|
|
]);
|
|
}
|
|
|
|
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']);
|
|
|
|
// Heal legacy Sales Centre placeholders (pending-setup.local) that stayed
|
|
// primary after a real domain was linked as an addon.
|
|
app(\App\Services\Hosting\PlaceholderPrimaryDomainService::class)->healAccount($account);
|
|
$account->refresh()->load(['product', 'user', 'node', 'sites']);
|
|
|
|
$placeholders = app(\App\Services\Hosting\PlaceholderPrimaryDomainService::class);
|
|
$maxDomains = $account->maxDomainsLimit();
|
|
$freeEmailAllowance = $account->freeEmailAllowance();
|
|
$mailboxes = $account->loadedMailboxes();
|
|
$mailboxCount = $mailboxes->count();
|
|
$extraMailboxCount = $freeEmailAllowance === null ? 0 : max($mailboxCount - $freeEmailAllowance, 0);
|
|
|
|
return view('hosting.account', [
|
|
'account' => $account,
|
|
'accountLabel' => $placeholders->displayLabel($account),
|
|
'linkedSites' => $account->sites
|
|
->filter(fn ($site) => ! \App\Services\Hosting\PlaceholderPrimaryDomainService::isPlaceholderDomain($site->domain))
|
|
->sortBy(fn ($site) => [$site->type !== 'primary', $site->domain])
|
|
->values(),
|
|
'maxDomains' => $maxDomains,
|
|
'canLinkMoreDomains' => $account->canAddDomain(),
|
|
'ownedDomains' => collect($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('hosting.orders.show', $order),
|
|
]);
|
|
}
|
|
|
|
return redirect()->route('hosting.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('hosting.orders.show', $order),
|
|
]);
|
|
}
|
|
|
|
return redirect()->route('hosting.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): array
|
|
{
|
|
return app(\App\Services\Domains\LadillDomainsClient::class)
|
|
->ownedForUser((string) $user->public_id);
|
|
}
|
|
}
|