File: /home/xedaptot/ai.naniguide.com/app/Http/Controllers/Refactor/Admin/PlanController.php
<?php
namespace App\Http\Controllers\Refactor\Admin;
use App\Http\Controllers\Controller;
use App\Library\RateLimit;
use App\Model\EmailVerificationServer;
use App\Model\Plan;
use App\Model\SendingServer;
use App\Services\Plans\Credits\CreditKey;
use App\Services\Plans\Entitlements\EntitlementKey;
use App\Services\Plans\Entitlements\EntitlementsService;
use App\Services\Plans\PlansService;
use App\Services\Plans\Quotas\QuotaKey;
use App\Services\Plans\RateLimits\RateLimitKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class PlanController extends Controller
{
public function index(Request $request)
{
if (Gate::denies('read', Plan::newDefaultPlan())) {
return $this->notAuthorized();
}
// Admin sees ALL plans (active + inactive) of type=general only
$allPlans = Plan::query();
$stats = [
'total' => (clone $allPlans)->count(),
'active' => (clone $allPlans)->where('plans.status', 'active')->count(),
'visible' => (clone $allPlans)->where('plans.visible', true)->count(),
'subscriptions' => \App\Model\Subscription::count(),
];
return view('refactor.pages.admin.plans.index', compact('stats'));
}
public function listing(Request $request)
{
if (Gate::denies('read', Plan::newDefaultPlan())) {
return response('Unauthorized', 403);
}
// Admin sees ALL plans of type=general (verification plans live under
// /admin/plan-verifications). Without this filter, /plans/{uid}/general
// can be reached for a verification UID and crash with BadMethodCallException.
$query = Plan::query();
// Search
if ($keyword = $request->keyword) {
$query = $query->where(function ($q) use ($keyword) {
$q->where('plans.name', 'like', "%{$keyword}%")
->orWhere('plans.description', 'like', "%{$keyword}%");
});
}
// Status filter
if ($request->status && $request->status !== 'all') {
$query = $query->where('plans.status', $request->status);
}
// Sort
$sortBy = $request->sort_by ?? 'plans.created_at';
$sortDir = $request->sort_direction ?? 'desc';
$query = $query->orderBy($sortBy, $sortDir);
$plans = $query->paginate($request->per_page ?? 15);
return view('refactor.pages.admin.plans._list', compact('plans'));
}
public function create(Request $request)
{
if (Gate::denies('create', Plan::newDefaultPlan())) {
return $this->notAuthorized();
}
$plan = Plan::newDefaultPlan();
return view('refactor.pages.admin.plans.create', compact('plan'));
}
public function store(Request $request)
{
if (Gate::denies('create', Plan::newDefaultPlan())) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$validator = \Validator::make($request->all(), [
'plan.general.name' => 'required|string|max:255',
'plan.general.price' => 'required|numeric|min:0',
'plan.general.currency_id' => 'required|exists:currencies,id',
'plan.general.frequency_amount' => 'required|integer|min:1',
'plan.general.frequency_unit' => 'required|in:day,week,month,year',
'plan.general.trial_amount' => 'nullable|integer|min:0',
'plan.general.trial_unit' => 'nullable|in:day,week,month,year',
]);
if ($validator->fails()) {
return response()->json([
'errors' => $validator->errors()->toArray(),
], 422);
}
$data = $request->input('plan.general', []);
$plan = app(PlansService::class)->create([
'name' => $data['name'],
'description' => $data['description'] ?? '',
'price' => $data['price'],
'currency_id' => $data['currency_id'],
'frequency_amount' => $data['frequency_amount'],
'frequency_unit' => $data['frequency_unit'],
'trial_amount' => $data['trial_amount'] ?? 0,
'trial_unit' => $data['trial_unit'] ?? 'day',
]);
$plan->checkStatus();
return response()->json([
'message' => trans('refactor/admin_plans.flash.created'),
'redirect' => route('refactor.admin.plans.general', $plan->uid),
]);
}
public function general(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) {
return $this->notAuthorized();
}
return view('refactor.pages.admin.plans.general', compact('plan'));
}
// Legacy `quota` / `security` tabs removed — replaced by plan_quotas / plan_rate_limits tabs.
/**
* Toggle the plan between "system" and "own" sending-server mode.
* Internally this writes only the HAS_OWN_SENDING_SERVER entitlement — kept
* as a dedicated endpoint (instead of asking admins to dig into the generic
* Entitlements tab) because this toggle has outsized behavioral impact.
*/
public function setSendingServerMode(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) {
return $this->notAuthorized();
}
$mode = $request->input('mode') === 'own' ? 'own' : 'system';
$service = app(PlansService::class);
// Preserve unrelated entitlements; we only touch HAS_OWN_SENDING_SERVER
// + sending_providers (ALLOW_SENDING_*).
$byKey = app(EntitlementsService::class)->entitlementsOfPlan($plan);
$byKey[EntitlementKey::HAS_OWN_SENDING_SERVER->value] = ($mode === 'own');
// In "own" mode read per-provider toggles from the form; in "system" mode
// force them all off (they're irrelevant — customer uses system servers).
foreach (EntitlementKey::cases() as $key) {
if ($key->group() !== 'sending_providers') continue;
if ($mode === 'own') {
$byKey[$key->value] = (bool) $request->input("providers.{$key->value}", false);
} else {
$byKey[$key->value] = false;
}
}
$service->setEntitlements($plan, $byKey);
// MAX_SENDING_SERVERS quota — preserve every other quota; only touch this key.
$quotaByKey = app(\App\Services\Plans\Quotas\QuotasService::class)->quotasOfPlan($plan);
if ($mode === 'own') {
$unlimited = (string) $request->input('max_sending_servers_unlimited') === '1';
$raw = $request->input('max_sending_servers');
if (!$unlimited && (!is_numeric($raw) || (int) $raw < 1)) {
return redirect()->back()
->with('error', trans('refactor/admin_plans.sending_servers.max_servers_validation_error'))
->withInput();
}
$quotaByKey[QuotaKey::MAX_SENDING_SERVERS->value] = $unlimited
? null
: (int) $raw;
} else {
// system mode: customer can't bring any own server
$quotaByKey[QuotaKey::MAX_SENDING_SERVERS->value] = 0;
}
$service->setQuotas($plan, $quotaByKey);
return redirect()->route('refactor.admin.plans.sending_servers', $plan->uid)
->with('success', trans('refactor/admin_plans.flash.saved'));
}
public function sendingServers(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) {
return $this->notAuthorized();
}
// System sending servers already assigned
$planSendingServers = $plan->plansSendingServers()->with('sendingServer')->get();
// Available system servers for adding — system (admin-owned) only.
// Customer-owned servers can't be plan-bound because a plan applies
// to all customers subscribed to it.
$existingIds = $planSendingServers->pluck('sending_server_id')->toArray();
$availableServers = SendingServer::system()
->where('status', 'active')
->whereNotIn('id', $existingIds)
->get();
// ALLOW_SENDING_* entitlements (only meaningful when HAS_OWN_SENDING_SERVER)
$providerKeys = array_values(array_filter(
EntitlementKey::cases(),
fn ($k) => $k->group() === 'sending_providers'
));
$providerState = app(EntitlementsService::class)->entitlementsOfPlan($plan);
// MAX_SENDING_SERVERS quota (only meaningful in own mode). limitForPlan
// returns false when row absent; legacy view treats null=unlimited / 0=not-configured
// — normalize false→null since "no row" here is conceptually "not yet configured"
// (own-mode form lets admin set it next). View distinguishes null vs int correctly.
$maxServersRaw = app(\App\Services\Plans\Quotas\QuotasService::class)
->limitForPlan($plan, QuotaKey::MAX_SENDING_SERVERS);
$maxServersLimit = $maxServersRaw === false ? null : $maxServersRaw;
return view('refactor.pages.admin.plans.sending_servers', compact(
'plan', 'planSendingServers', 'availableServers', 'providerKeys', 'providerState', 'maxServersLimit'
));
}
public function addSendingServer(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
if ($request->isMethod('get')) {
$existingIds = $plan->plansSendingServers()->pluck('sending_server_id')->toArray();
// System (admin-owned) servers only — customer-owned servers can't
// be plan-bound because plans apply to all customers of the plan.
$availableServers = SendingServer::system()
->where('status', 'active')
->whereNotIn('id', $existingIds)
->get();
return view('refactor.pages.admin.plans.add_sending_server', compact('plan', 'availableServers'));
}
$server = SendingServer::findByUid($request->sending_server_uid);
if (!$server) {
return response()->json(['error' => 'Server not found'], 404);
}
// Check duplicate
$exists = $plan->plansSendingServers()
->where('sending_server_id', $server->id)
->exists();
if ($exists) {
return response()->json(['error' => trans('refactor/admin_plans.sending_servers.already_added')], 422);
}
$plan->addSendingServerByUid($server->uid);
$plan->checkStatus();
return response()->json([
'message' => trans('refactor/admin_plans.sending_servers.flash.added'),
'redirect' => route('refactor.admin.plans.sending_servers', $plan->uid),
]);
}
public function removeSendingServer(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$plan->removeSendingServerByUid($request->sending_server_uid);
$plan->checkStatus();
return response()->json([
'message' => trans('refactor/admin_plans.sending_servers.flash.removed'),
'redirect' => route('refactor.admin.plans.sending_servers', $plan->uid),
]);
}
public function setPrimarySendingServer(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$plan->setPrimarySendingServer($request->sending_server_uid);
$plan->checkStatus();
return response()->json([
'message' => trans('refactor/admin_plans.sending_servers.flash.primary_set'),
'redirect' => route('refactor.admin.plans.sending_servers', $plan->uid),
]);
}
public function fitness(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
if ($request->isMethod('post')) {
$plan->updateFitnesses($request->sending_servers);
$plan->checkStatus();
return response()->json([
'message' => trans('refactor/admin_plans.sending_servers.flash.fitness_saved'),
'redirect' => route('refactor.admin.plans.sending_servers', $plan->uid),
]);
}
$planSendingServers = $plan->plansSendingServers()->with('sendingServer')->get();
return view('refactor.pages.admin.plans.fitness', compact('plan', 'planSendingServers'));
}
// Legacy `sendingServerOption` / `saveSendingServerOwn` tabs removed — replaced by
// plan_entitlements (HAS_OWN_SENDING_SERVER + ALLOW_SENDING_* per provider) tab.
public function pieChart(Request $request)
{
if (Gate::denies('read', Plan::newDefaultPlan())) {
return response()->json([], 403);
}
$admin = $request->user()->admin;
$result = ['data' => []];
foreach (Plan::active()->get() as $plan) {
$count = $admin->getAllSubscriptionsByPlan($plan)->count();
if ($count) {
$result['data'][] = ['value' => $count, 'name' => $plan->name];
}
}
usort($result['data'], function ($a, $b) {
return $b['value'] - $a['value'];
});
return response()->json($result);
}
// Legacy `emailFooter` / `emailVerification` tabs removed — footer text lives on
// plans.footer_html/footer_text columns (gated by HAS_CUSTOM_FOOTER entitlement),
// and email-verification server plan settings moved to plan_entitlements
// (HAS_OWN_EMAIL_VERIFICATION_SERVER + HAS_ALL_EMAIL_VERIFICATION_SERVERS).
public function tos(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) {
return $this->notAuthorized();
}
return view('refactor.pages.admin.plans.tos', compact('plan'));
}
// ===== New 4-concept plan management tabs =====
public function planCredits(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) return $this->notAuthorized();
$credits = app(\App\Services\Plans\Credits\CreditsService::class)->creditsOfPlan($plan);
return view('refactor.pages.admin.plans.plan_credits', [
'plan' => $plan,
'credits' => $credits,
'keys' => CreditKey::cases(),
]);
}
public function savePlanCredits(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) return $this->notAuthorized();
$errors = [];
$byKey = [];
foreach (CreditKey::cases() as $key) {
$enabled = (string) $request->input("credits_enabled.{$key->value}") === '1';
if (! $enabled) {
continue; // key absent → setCredits() deletes the plan_credits row
}
$unlimited = (string) $request->input("credits_unlimited.{$key->value}") === '1';
if ($unlimited) {
$byKey[$key->value] = null;
continue;
}
$raw = $request->input("credits.{$key->value}");
if ($raw === null || $raw === '') {
$errors["credits.{$key->value}"] = [trans('refactor/admin_plans.credit.validation.required')];
continue;
}
if (!is_numeric($raw) || (int) $raw < 0 || (string) (int) $raw !== (string) $raw) {
$errors["credits.{$key->value}"] = [trans('refactor/admin_plans.credit.validation.invalid')];
continue;
}
$byKey[$key->value] = (int) $raw;
}
if ($errors) {
throw \Illuminate\Validation\ValidationException::withMessages($errors);
}
app(PlansService::class)->setCredits($plan, $byKey);
return redirect()->route('refactor.admin.plans.plan_credits', $plan->uid)
->with('success', trans('refactor/admin_plans.flash.saved'));
}
public function planQuotas(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) return $this->notAuthorized();
$quotaByKey = app(\App\Services\Plans\Quotas\QuotasService::class)->quotasOfPlan($plan);
$requiresTd = (bool) \App\Model\PlanEntitlement::where('plan_id', $plan->id)
->where('entitlement_key', EntitlementKey::REQUIRES_TRACKING_DOMAIN->value)
->value('enabled');
return view('refactor.pages.admin.plans.plan_quotas', [
'plan' => $plan,
'quotaByKey' => $quotaByKey,
'keys' => QuotaKey::cases(),
'requiresTrackingDomain' => $requiresTd,
]);
}
public function savePlanQuotas(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) return $this->notAuthorized();
// Preserve MAX_SENDING_SERVERS — it's managed in the Sending Servers tab
// (depends on HAS_OWN_SENDING_SERVER entitlement) and isn't exposed here.
$preserved = app(\App\Services\Plans\Quotas\QuotasService::class)->quotasOfPlan($plan);
$errors = [];
$byKey = [];
foreach (QuotaKey::cases() as $key) {
if ($key === QuotaKey::MAX_SENDING_SERVERS) {
if (array_key_exists($key->value, $preserved)) {
$byKey[$key->value] = $preserved[$key->value];
}
continue;
}
$enabled = (string) $request->input("quotas_enabled.{$key->value}") === '1';
if (! $enabled) {
continue; // key absent → setQuotas() deletes the plan_quotas row
}
$unlimited = (string) $request->input("quotas_unlimited.{$key->value}") === '1';
if ($unlimited) {
$byKey[$key->value] = null;
continue;
}
$raw = $request->input("quotas.{$key->value}");
if ($raw === null || $raw === '') {
$errors["quotas.{$key->value}"] = [trans('refactor/admin_plans.quota.validation.required')];
continue;
}
if (!is_numeric($raw) || (int) $raw < 0 || (string) (int) $raw !== (string) $raw) {
$errors["quotas.{$key->value}"] = [trans('refactor/admin_plans.quota.validation.invalid')];
continue;
}
$byKey[$key->value] = (int) $raw;
}
if ($errors) {
throw \Illuminate\Validation\ValidationException::withMessages($errors);
}
try {
app(PlansService::class)->setQuotas($plan, $byKey);
} catch (\InvalidArgumentException $e) {
throw \Illuminate\Validation\ValidationException::withMessages([
'quotas.' . QuotaKey::MAX_TRACKING_DOMAINS->value => [$e->getMessage()],
]);
}
return redirect()->route('refactor.admin.plans.plan_quotas', $plan->uid)
->with('success', trans('refactor/admin_plans.flash.saved'));
}
public function planEntitlements(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) return $this->notAuthorized();
// Ensure full catalog seeded
app(EntitlementsService::class)->seedCatalogForPlan($plan->id);
$entitlements = app(EntitlementsService::class)->entitlementsOfPlan($plan);
$trackingDomainQuota = app(\App\Services\Plans\Quotas\QuotasService::class)->limitForPlan($plan, QuotaKey::MAX_TRACKING_DOMAINS);
return view('refactor.pages.admin.plans.plan_entitlements', [
'plan' => $plan,
'entitlements' => $entitlements,
'keys' => EntitlementKey::cases(),
'trackingDomainQuota' => $trackingDomainQuota,
]);
}
public function savePlanEntitlements(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) return $this->notAuthorized();
// Preserve sending_providers (ALLOW_SENDING_*) entitlements — those are
// managed exclusively in the Sending Servers tab; this form doesn't expose them.
$preserved = app(EntitlementsService::class)->entitlementsOfPlan($plan);
$byKey = [];
foreach (EntitlementKey::cases() as $key) {
if ($key->group() === 'sending_providers') {
$byKey[$key->value] = $preserved[$key->value] ?? false;
continue;
}
$byKey[$key->value] = (bool) $request->input("entitlements.{$key->value}", false);
}
try {
app(PlansService::class)->setEntitlements($plan, $byKey);
} catch (\InvalidArgumentException $e) {
throw \Illuminate\Validation\ValidationException::withMessages([
'entitlements.' . EntitlementKey::REQUIRES_TRACKING_DOMAIN->value => [$e->getMessage()],
]);
}
return redirect()->route('refactor.admin.plans.plan_entitlements', $plan->uid)
->with('success', trans('refactor/admin_plans.flash.saved'));
}
public function planRateLimits(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) return $this->notAuthorized();
$rates = app(\App\Services\Plans\RateLimits\RateLimitsService::class)->configsOfPlan($plan);
return view('refactor.pages.admin.plans.plan_rate_limits', [
'plan' => $plan,
'rates' => $rates,
'keys' => RateLimitKey::cases(),
]);
}
public function savePlanRateLimits(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) return $this->notAuthorized();
$service = app(PlansService::class);
foreach (RateLimitKey::cases() as $key) {
$limit = $request->input("rates.{$key->value}.limit");
$amount = $request->input("rates.{$key->value}.amount");
$unit = $request->input("rates.{$key->value}.unit");
if ($limit === null || $limit === '' || (int) $limit <= 0) {
$service->setRateLimit($plan, $key, null);
continue;
}
$service->setRateLimit($plan, $key, new RateLimit(
(int) $limit,
(int) ($amount ?: 1),
in_array($unit, ['second', 'minute', 'hour', 'day'], true) ? $unit : 'minute',
));
}
return redirect()->route('refactor.admin.plans.plan_rate_limits', $plan->uid)
->with('success', trans('refactor/admin_plans.flash.saved'));
}
// ===== End new 4-concept tabs =====
/**
* Save general plan attributes (name/price/frequency/currency/trial). The old
* options-based saveAll pipeline is gone; each 4-concept tab saves via its own
* controller method.
*/
public function save(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) {
return $this->notAuthorized();
}
app(PlansService::class)->updateBase($plan, $request->input('plan.general', []));
$redirectTab = $request->input('_redirect_tab', 'general');
return redirect()->route('refactor.admin.plans.' . $redirectTab, $plan->uid)
->with('success', trans('refactor/admin_plans.flash.saved'));
}
public function saveTos(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (Gate::denies('update', $plan)) {
return $this->notAuthorized();
}
$validator = $plan->updateTos($request->all());
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
return redirect()->route('refactor.admin.plans.tos', $plan->uid)
->with('success', trans('refactor/admin_plans.flash.saved'));
}
public function enable(Request $request)
{
$service = app(PlansService::class);
$uids = is_array($request->uids) ? $request->uids : [$request->uids];
foreach ($uids as $uid) {
$plan = Plan::findByUid($uid);
if ($plan) {
$service->enable($plan);
}
}
return response()->json([
'message' => trans('refactor/admin_plans.flash.enabled'),
]);
}
public function disable(Request $request)
{
$service = app(PlansService::class);
$uids = is_array($request->uids) ? $request->uids : [$request->uids];
foreach ($uids as $uid) {
$plan = Plan::findByUid($uid);
if ($plan) {
$service->disable($plan);
}
}
return response()->json([
'message' => trans('refactor/admin_plans.flash.disabled'),
]);
}
public function delete(Request $request)
{
$uids = is_array($request->uids) ? $request->uids : [$request->uids];
foreach ($uids as $uid) {
$plan = Plan::findByUid($uid);
if ($plan && Gate::allows('delete', $plan)) {
$plan->delete();
}
}
return response()->json([
'message' => trans('refactor/admin_plans.flash.deleted'),
]);
}
public function visibleOn(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if ($plan) {
if ($plan->useSystemSendingServer() && !$plan->hasPrimarySendingServer()) {
return response()->json(['error' => trans('refactor/admin_plans.warning.cannot_show_no_server')], 422);
}
$plan->visible = true;
$plan->save();
}
return response()->json(['message' => trans('refactor/admin_plans.flash.visible_on')]);
}
public function visibleOff(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if ($plan) {
$plan->visible = false;
$plan->save();
}
return response()->json(['message' => trans('refactor/admin_plans.flash.visible_off')]);
}
public function copy(Request $request, $uid)
{
$plan = Plan::findByUid($uid);
if (!$plan) {
return response()->json(['error' => 'Plan not found'], 404);
}
$newPlan = $plan->replicate();
$newPlan->name = $plan->name . ' (Copy)';
$newPlan->uid = uniqid();
$newPlan->save();
return response()->json([
'message' => trans('refactor/admin_plans.flash.copied'),
]);
}
public function payment(Request $request, $uid)
{
$plan = \App\Model\Plan::findByUid($uid);
if (!$plan || \Gate::denies('update', $plan)) {
return redirect()->route('refactor.admin.plans.general', $uid);
}
// View never existed in old codebase — redirect to general settings
return redirect()->route('refactor.admin.plans.general', $uid);
}
}