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)) {
|
if (! $this->subscriptions->canAddLocation($account, $owner)) {
|
||||||
return redirect()->route('pos.pro.index')
|
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([
|
$data = $request->validate([
|
||||||
|
|||||||
@@ -111,6 +111,32 @@ class SubscriptionService
|
|||||||
return PosLocation::owned($ownerRef)->count();
|
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
|
public function canUseMultiLocation(User $user): bool
|
||||||
{
|
{
|
||||||
return $this->isPro($user);
|
return $this->isPro($user);
|
||||||
@@ -118,11 +144,29 @@ class SubscriptionService
|
|||||||
|
|
||||||
public function canAddLocation(User $user, string $ownerRef): bool
|
public function canAddLocation(User $user, string $ownerRef): bool
|
||||||
{
|
{
|
||||||
if (! $this->canUseMultiLocation($user)) {
|
$max = $this->maxLocations($user);
|
||||||
return $this->locationCount($ownerRef) < (int) config('pos.free.max_locations', 1);
|
|
||||||
|
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
|
public function canManageTeam(User $user): bool
|
||||||
|
|||||||
+10
-2
@@ -19,8 +19,16 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
'plans' => [
|
'plans' => [
|
||||||
'pro' => ['price_minor' => (int) env('POS_PRO_PRICE_MINOR', 79000)],
|
'pro' => [
|
||||||
'enterprise' => ['price_minor' => (int) env('POS_ENTERPRISE_PRICE_MINOR', 249000)],
|
'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],
|
'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">
|
<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>.
|
Multi-branch POS is available on <a href="{{ route('pos.pro.index') }}" class="font-semibold underline">Pro or Business</a>.
|
||||||
</p>
|
</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
|
@endunless
|
||||||
</x-settings.card>
|
</x-settings.card>
|
||||||
</x-settings.page>
|
</x-settings.page>
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
<x-plan-tier-price :currency="$currency" :monthly-minor="$proPriceMinor" />
|
<x-plan-tier-price :currency="$currency" :monthly-minor="$proPriceMinor" />
|
||||||
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-700">
|
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-700">
|
||||||
<li>Unlimited products</li>
|
<li>Unlimited products</li>
|
||||||
|
<li>Up to 3 locations</li>
|
||||||
<li>Restaurant mode</li>
|
<li>Restaurant mode</li>
|
||||||
<li>Catalog import</li>
|
<li>Catalog import</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
<x-plan-tier-price :currency="$currency" :monthly-minor="$enterprisePriceMinor" dark />
|
<x-plan-tier-price :currency="$currency" :monthly-minor="$enterprisePriceMinor" dark />
|
||||||
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-200">
|
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-200">
|
||||||
<li>Everything in Pro</li>
|
<li>Everything in Pro</li>
|
||||||
<li>Multi-location</li>
|
<li>Unlimited locations</li>
|
||||||
<li>Advanced reporting</li>
|
<li>Advanced reporting</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
|
|||||||
@@ -47,6 +47,14 @@
|
|||||||
<a href="{{ route('pos.pro.index') }}" class="font-semibold underline">Pro or Business</a>.
|
<a href="{{ route('pos.pro.index') }}" class="font-semibold underline">Pro or Business</a>.
|
||||||
Your account is limited to one branch.
|
Your account is limited to one branch.
|
||||||
</p>
|
</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
|
@endunless
|
||||||
</div>
|
</div>
|
||||||
</x-settings.card>
|
</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
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
@@ -104,6 +118,58 @@ class PosMultiBranchTest extends TestCase
|
|||||||
$this->assertSame(2, PosLocation::owned($owner->public_id)->count());
|
$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
|
public function test_cashier_is_limited_to_assigned_branch_sales(): void
|
||||||
{
|
{
|
||||||
$owner = $this->owner();
|
$owner = $this->owner();
|
||||||
|
|||||||
Reference in New Issue
Block a user