Show upgrade UI for assessment analytics instead of bare 403.
Deploy Ladill Care / deploy (push) Successful in 41s

Non-Enterprise orgs hitting /reports/assessments get the in-app plans upsell; CSV export redirects to View plans with an upsell flash.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-20 16:26:43 +00:00
co-authored by Cursor
parent d4b645d85b
commit cebc9c8e4c
4 changed files with 50 additions and 10 deletions
@@ -11,12 +11,16 @@ trait RequiresPlanFeature
{ {
/** /**
* For page views: return an upgrade screen when the org lacks the feature. * For page views: return an upgrade screen when the org lacks the feature.
*
* @param 'pro'|'enterprise' $planHint Which tier to highlight in pricing copy
*/ */
protected function proFeatureUpgradeView( protected function proFeatureUpgradeView(
Request $request, Request $request,
string $feature, string $feature,
string $title, string $title,
string $description, string $description,
string $planHint = 'pro',
?string $upgradeBody = null,
): ?View { ): ?View {
$organization = $this->organization($request); $organization = $this->organization($request);
$plans = app(PlanService::class); $plans = app(PlanService::class);
@@ -25,11 +29,17 @@ trait RequiresPlanFeature
return null; return null;
} }
$isEnterprise = $planHint === 'enterprise';
return view('care.settings.pro-feature', [ return view('care.settings.pro-feature', [
'organization' => $organization, 'organization' => $organization,
'title' => $title, 'title' => $title,
'description' => $description, 'description' => $description,
'proPriceMinor' => $plans->proPricePerBranchMinor(), 'planLabel' => $isEnterprise ? 'Enterprise' : 'Pro',
'proPriceMinor' => $isEnterprise
? $plans->enterprisePricePerBranchMinor()
: $plans->proPricePerBranchMinor(),
'upgradeBody' => $upgradeBody,
]); ]);
} }
+29 -6
View File
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Care; namespace App\Http\Controllers\Care;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Controllers\Care\Concerns\RequiresPlanFeature;
use App\Http\Controllers\Care\Concerns\ScopesToAccount; use App\Http\Controllers\Care\Concerns\ScopesToAccount;
use App\Models\Branch; use App\Models\Branch;
use App\Services\Care\AdminOverviewService; use App\Services\Care\AdminOverviewService;
@@ -10,6 +11,7 @@ use App\Services\Care\CareFeatures;
use App\Services\Care\OrganizationResolver; use App\Services\Care\OrganizationResolver;
use App\Services\Care\PlanService; use App\Services\Care\PlanService;
use App\Services\Care\ReportService; use App\Services\Care\ReportService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\View\View; use Illuminate\View\View;
@@ -17,8 +19,11 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
class ReportController extends Controller class ReportController extends Controller
{ {
use RequiresPlanFeature;
use ScopesToAccount; use ScopesToAccount;
private const ASSESSMENT_ANALYTICS_UPSELL = 'Assessment analytics are part of Care Enterprise. Upgrade to unlock multi-patient clinical form and outcome reports.';
public function __construct( public function __construct(
protected ReportService $reports, protected ReportService $reports,
protected AdminOverviewService $adminOverview, protected AdminOverviewService $adminOverview,
@@ -54,6 +59,21 @@ class ReportController extends Controller
public function show(Request $request, string $type): View public function show(Request $request, string $type): View
{ {
$this->authorizeReport($request, $type); $this->authorizeReport($request, $type);
if ($type === 'assessments') {
if ($upgrade = $this->proFeatureUpgradeView(
$request,
'assessment_analytics',
'Assessment analytics',
'Org-level clinical form and outcome analytics across patients.',
'enterprise',
'GHS '.number_format(app(PlanService::class)->enterprisePricePerBranchMinor() / 100, 0)
.' / branch / month for Enterprise — assessment analytics, priority support, and advanced workflows on top of Pro.',
)) {
return $upgrade;
}
}
$organization = $this->organization($request); $organization = $this->organization($request);
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request)); $branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
$branchId = $request->input('branch_id') ? (int) $request->input('branch_id') : $branchScope; $branchId = $request->input('branch_id') ? (int) $request->input('branch_id') : $branchScope;
@@ -80,9 +100,16 @@ class ReportController extends Controller
]); ]);
} }
public function export(Request $request, string $type): StreamedResponse public function export(Request $request, string $type): StreamedResponse|RedirectResponse
{ {
$this->authorizeReport($request, $type); $this->authorizeReport($request, $type);
if ($type === 'assessments') {
if ($deny = $this->denyWithoutFeature($request, 'assessment_analytics', self::ASSESSMENT_ANALYTICS_UPSELL)) {
return $deny;
}
}
abort_unless( abort_unless(
app(\App\Services\Care\CarePermissions::class)->can($this->member($request), 'reports.finance.export'), app(\App\Services\Care\CarePermissions::class)->can($this->member($request), 'reports.finance.export'),
403, 403,
@@ -155,11 +182,7 @@ class ReportController extends Controller
app(CareFeatures::class)->enabled($organization, CareFeatures::ASSESSMENTS_ENGINE), app(CareFeatures::class)->enabled($organization, CareFeatures::ASSESSMENTS_ENGINE),
404, 404,
); );
abort_unless( // Plan gate: show() renders upgrade UI; export() redirects via denyWithoutFeature.
app(PlanService::class)->hasFeature($organization, 'assessment_analytics'),
403,
'Assessment analytics require an Enterprise plan.',
);
} }
} }
@@ -8,8 +8,12 @@
<x-settings.card title="Upgrade your plan"> <x-settings.card title="Upgrade your plan">
<p class="text-sm text-slate-600"> <p class="text-sm text-slate-600">
GHS {{ number_format($proPriceMinor / 100, 0) }} / branch / month for Pro @if (! empty($upgradeBody))
{{ $upgradeBody }}
@else
GHS {{ number_format($proPriceMinor / 100, 0) }} / branch / month for {{ $planLabel ?? 'Pro' }}
multi-branch locations, laboratory, pharmacy, encounter billing, and in-app service queues. multi-branch locations, laboratory, pharmacy, encounter billing, and in-app service queues.
@endif
</p> </p>
<div class="mt-4 flex flex-wrap items-center gap-3"> <div class="mt-4 flex flex-wrap items-center gap-3">
<a href="{{ route('care.pro.index') }}" class="btn-primary">View plans</a> <a href="{{ route('care.pro.index') }}" class="btn-primary">View plans</a>
+4 -1
View File
@@ -111,7 +111,10 @@ class CareAssessmentExtrasTest extends TestCase
$this->actingAs($this->user) $this->actingAs($this->user)
->get(route('care.reports.show', ['type' => 'assessments'])) ->get(route('care.reports.show', ['type' => 'assessments']))
->assertForbidden(); ->assertOk()
->assertSee('Upgrade your plan')
->assertSee('View plans')
->assertSee('Enterprise');
} }
public function test_fhir_export_web_and_api(): void public function test_fhir_export_web_and_api(): void