Limit Pro to 3 branches and leave Business unlimited.
Deploy Ladill POS / deploy (push) Successful in 2m15s
Deploy Ladill POS / deploy (push) Successful in 2m15s
Enforce plan location caps in SubscriptionService, surface upgrade copy on plans and branch settings, and cover the limits in tests.
This commit is contained in:
@@ -55,7 +55,7 @@ class LocationController extends Controller
|
||||
|
||||
if (! $this->subscriptions->canAddLocation($account, $owner)) {
|
||||
return redirect()->route('pos.pro.index')
|
||||
->with('upsell', 'Multi-branch POS requires Ladill POS Pro or Business.');
|
||||
->with('upsell', $this->subscriptions->locationLimitMessage($account));
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
|
||||
@@ -111,6 +111,32 @@ class SubscriptionService
|
||||
return PosLocation::owned($ownerRef)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Max branches for the account's plan. null = unlimited.
|
||||
*/
|
||||
public function maxLocations(User $user): ?int
|
||||
{
|
||||
if (! $this->gatingActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$key = $this->planKey($user);
|
||||
|
||||
if ($key === 'free') {
|
||||
return (int) config('pos.free.max_locations', 1);
|
||||
}
|
||||
|
||||
if ($key === ProSubscription::PLAN_ENTERPRISE) {
|
||||
$max = config('pos.plans.enterprise.max_locations');
|
||||
|
||||
return $max === null ? null : (int) $max;
|
||||
}
|
||||
|
||||
$max = config('pos.plans.pro.max_locations', 3);
|
||||
|
||||
return $max === null ? null : (int) $max;
|
||||
}
|
||||
|
||||
public function canUseMultiLocation(User $user): bool
|
||||
{
|
||||
return $this->isPro($user);
|
||||
@@ -118,11 +144,29 @@ class SubscriptionService
|
||||
|
||||
public function canAddLocation(User $user, string $ownerRef): bool
|
||||
{
|
||||
if (! $this->canUseMultiLocation($user)) {
|
||||
return $this->locationCount($ownerRef) < (int) config('pos.free.max_locations', 1);
|
||||
$max = $this->maxLocations($user);
|
||||
|
||||
if ($max === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $this->locationCount($ownerRef) < $max;
|
||||
}
|
||||
|
||||
public function locationLimitMessage(User $user): string
|
||||
{
|
||||
$key = $this->planKey($user);
|
||||
$max = $this->maxLocations($user);
|
||||
|
||||
if ($key === 'free') {
|
||||
return 'Your Free plan allows 1 branch. Upgrade to Pro for up to 3 branches, or Business for unlimited.';
|
||||
}
|
||||
|
||||
if ($key === ProSubscription::PLAN_PRO && $max !== null) {
|
||||
return "Your Pro plan allows up to {$max} branches. Upgrade to Business for unlimited locations.";
|
||||
}
|
||||
|
||||
return 'Branch limit reached for your current plan.';
|
||||
}
|
||||
|
||||
public function canManageTeam(User $user): bool
|
||||
|
||||
+10
-2
@@ -19,8 +19,16 @@ return [
|
||||
],
|
||||
|
||||
'plans' => [
|
||||
'pro' => ['price_minor' => (int) env('POS_PRO_PRICE_MINOR', 79000)],
|
||||
'enterprise' => ['price_minor' => (int) env('POS_ENTERPRISE_PRICE_MINOR', 249000)],
|
||||
'pro' => [
|
||||
'price_minor' => (int) env('POS_PRO_PRICE_MINOR', 79000),
|
||||
// null = unlimited
|
||||
'max_locations' => (int) env('POS_PRO_MAX_LOCATIONS', 3),
|
||||
],
|
||||
'enterprise' => [
|
||||
'price_minor' => (int) env('POS_ENTERPRISE_PRICE_MINOR', 249000),
|
||||
// null = unlimited branches
|
||||
'max_locations' => null,
|
||||
],
|
||||
],
|
||||
|
||||
'prepaid_months' => [6, 12, 24],
|
||||
|
||||
@@ -52,6 +52,14 @@
|
||||
<p class="mt-4 rounded-xl bg-amber-50 px-4 py-3 text-sm text-amber-900">
|
||||
Multi-branch POS is available on <a href="{{ route('pos.pro.index') }}" class="font-semibold underline">Pro or Business</a>.
|
||||
</p>
|
||||
@else
|
||||
@if (! $canAddBranch)
|
||||
<p class="mt-4 rounded-xl bg-amber-50 px-4 py-3 text-sm text-amber-900">
|
||||
You've reached the branch limit on Pro (up to 3).
|
||||
<a href="{{ route('pos.pro.index') }}" class="font-semibold underline">Upgrade to Business</a>
|
||||
for unlimited locations.
|
||||
</p>
|
||||
@endif
|
||||
@endunless
|
||||
</x-settings.card>
|
||||
</x-settings.page>
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
<x-plan-tier-price :currency="$currency" :monthly-minor="$proPriceMinor" />
|
||||
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-700">
|
||||
<li>Unlimited products</li>
|
||||
<li>Up to 3 locations</li>
|
||||
<li>Restaurant mode</li>
|
||||
<li>Catalog import</li>
|
||||
</ul>
|
||||
@@ -81,7 +82,7 @@
|
||||
<x-plan-tier-price :currency="$currency" :monthly-minor="$enterprisePriceMinor" dark />
|
||||
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-200">
|
||||
<li>Everything in Pro</li>
|
||||
<li>Multi-location</li>
|
||||
<li>Unlimited locations</li>
|
||||
<li>Advanced reporting</li>
|
||||
</ul>
|
||||
<div class="mt-6">
|
||||
|
||||
@@ -47,6 +47,14 @@
|
||||
<a href="{{ route('pos.pro.index') }}" class="font-semibold underline">Pro or Business</a>.
|
||||
Your account is limited to one branch.
|
||||
</p>
|
||||
@else
|
||||
@if (! empty($canManageBranches) && ! $canAddBranch)
|
||||
<p class="rounded-xl bg-amber-50 px-4 py-3 text-sm text-amber-900">
|
||||
You've reached the branch limit on Pro (up to 3).
|
||||
<a href="{{ route('pos.pro.index') }}" class="font-semibold underline">Upgrade to Business</a>
|
||||
for unlimited locations.
|
||||
</p>
|
||||
@endif
|
||||
@endunless
|
||||
</div>
|
||||
</x-settings.card>
|
||||
|
||||
@@ -47,6 +47,20 @@ class PosMultiBranchTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
private function activateBusiness(User $user): void
|
||||
{
|
||||
ProSubscription::create([
|
||||
'user_id' => $user->id,
|
||||
'plan' => ProSubscription::PLAN_ENTERPRISE,
|
||||
'status' => ProSubscription::STATUS_ACTIVE,
|
||||
'price_minor' => 24900,
|
||||
'currency' => 'GHS',
|
||||
'auto_renew' => true,
|
||||
'started_at' => now(),
|
||||
'current_period_end' => now()->addMonth(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -104,6 +118,58 @@ class PosMultiBranchTest extends TestCase
|
||||
$this->assertSame(2, PosLocation::owned($owner->public_id)->count());
|
||||
}
|
||||
|
||||
public function test_pro_account_cannot_add_fourth_branch(): void
|
||||
{
|
||||
$owner = $this->owner();
|
||||
$this->activatePro($owner);
|
||||
|
||||
foreach (['Main', 'Mall', 'Airport'] as $i => $name) {
|
||||
PosLocation::create([
|
||||
'owner_ref' => $owner->public_id,
|
||||
'name' => $name,
|
||||
'currency' => 'GHS',
|
||||
'is_default' => $i === 0,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->assertSame(3, PosLocation::owned($owner->public_id)->count());
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('pos.branches.store'), [
|
||||
'name' => 'Fourth branch',
|
||||
'currency' => 'GHS',
|
||||
'service_style' => 'retail',
|
||||
])
|
||||
->assertRedirect(route('pos.pro.index'));
|
||||
|
||||
$this->assertSame(3, PosLocation::owned($owner->public_id)->count());
|
||||
}
|
||||
|
||||
public function test_business_account_can_add_beyond_three_branches(): void
|
||||
{
|
||||
$owner = $this->owner();
|
||||
$this->activateBusiness($owner);
|
||||
|
||||
foreach (['Main', 'Mall', 'Airport'] as $i => $name) {
|
||||
PosLocation::create([
|
||||
'owner_ref' => $owner->public_id,
|
||||
'name' => $name,
|
||||
'currency' => 'GHS',
|
||||
'is_default' => $i === 0,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('pos.branches.store'), [
|
||||
'name' => 'Fourth branch',
|
||||
'currency' => 'GHS',
|
||||
'service_style' => 'retail',
|
||||
])
|
||||
->assertRedirect(route('pos.settings').'#branches');
|
||||
|
||||
$this->assertSame(4, PosLocation::owned($owner->public_id)->count());
|
||||
}
|
||||
|
||||
public function test_cashier_is_limited_to_assigned_branch_sales(): void
|
||||
{
|
||||
$owner = $this->owner();
|
||||
|
||||
Reference in New Issue
Block a user