Tighten Pro alert caps and move custom badges to Enterprise.
Deploy Ladill Frontdesk / deploy (push) Has been cancelled
Deploy Ladill Frontdesk / deploy (push) Has been cancelled
Pro includes 5,000 email and 5,000 SMS host alerts per month with hard monthly caps; Enterprise is unlimited. Custom badge templates are Enterprise-only, and plan benefit copy matches the new limits.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Frontdesk;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Frontdesk\Concerns\RequiresPlanFeature;
|
||||
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
|
||||
use App\Models\Visit;
|
||||
use App\Services\Frontdesk\BadgeRenderService;
|
||||
@@ -14,11 +15,24 @@ use Illuminate\View\View;
|
||||
|
||||
class BadgeController extends Controller
|
||||
{
|
||||
use RequiresPlanFeature;
|
||||
use ScopesToAccount;
|
||||
|
||||
private const FEATURE = 'custom_badge_templates';
|
||||
|
||||
public function editTemplate(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'settings.view');
|
||||
|
||||
if ($upgrade = $this->proFeatureUpgradeView(
|
||||
$request,
|
||||
self::FEATURE,
|
||||
'Custom badge templates',
|
||||
'Custom badge layouts and branding are included with Frontdesk Enterprise.',
|
||||
)) {
|
||||
return $upgrade;
|
||||
}
|
||||
|
||||
$organization = $this->organization($request);
|
||||
$canManage = app(FrontdeskPermissions::class)->isAdmin($this->member($request));
|
||||
|
||||
@@ -41,6 +55,15 @@ class BadgeController extends Controller
|
||||
public function updateTemplate(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'settings.manage');
|
||||
|
||||
if ($deny = $this->denyWithoutFeature(
|
||||
$request,
|
||||
self::FEATURE,
|
||||
'Custom badge templates require Frontdesk Enterprise.',
|
||||
)) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
|
||||
@@ -32,6 +32,7 @@ class SettingsController extends Controller
|
||||
$monthlyUsage = $usage->forOrganization($organization);
|
||||
$plan = $plans->plan($organization);
|
||||
$freeEmailAllowance = $plans->freeEmailsPerMonth($organization);
|
||||
$freeSmsAllowance = $plans->freeSmsPerMonth($organization);
|
||||
$member = $this->member($request);
|
||||
$permissions = app(FrontdeskPermissions::class);
|
||||
|
||||
@@ -56,6 +57,7 @@ class SettingsController extends Controller
|
||||
'proPriceMinor' => $plans->proPricePerBranchMinor(),
|
||||
'monthlyUsage' => $monthlyUsage,
|
||||
'freeEmailAllowance' => $freeEmailAllowance,
|
||||
'freeSmsAllowance' => $freeSmsAllowance,
|
||||
'emailCostFormatted' => $pricing->formatMinor($pricing->emailCostMinor()),
|
||||
'smsCostFormatted' => $pricing->formatMinor(
|
||||
$pricing->smsCostMinor('Sample SMS message for pricing display.'),
|
||||
|
||||
@@ -29,6 +29,14 @@ class NotificationBillingService
|
||||
|
||||
public function emailCostMinor(Organization $organization): int
|
||||
{
|
||||
// Paid plan monthly cap exhausted — do not offer overage.
|
||||
if ($this->plans->emailAllowanceExhausted(
|
||||
$organization,
|
||||
$this->usage->emailCountThisMonth($organization),
|
||||
)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($this->usesCustomerEmail($organization)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -41,8 +49,36 @@ class NotificationBillingService
|
||||
return $this->pricing->emailCostMinor();
|
||||
}
|
||||
|
||||
public function smsCostMinor(Organization $organization, string $message): int
|
||||
{
|
||||
if ($this->plans->smsAllowanceExhausted(
|
||||
$organization,
|
||||
$this->usage->smsCountThisMonth($organization),
|
||||
)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($this->usesCustomerSms($organization)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$allowance = $this->plans->freeSmsPerMonth($organization);
|
||||
if ($allowance === null || $this->usage->smsCountThisMonth($organization) < $allowance) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->pricing->smsCostMinor($message);
|
||||
}
|
||||
|
||||
public function canAffordEmail(Organization $organization): bool
|
||||
{
|
||||
if ($this->plans->emailAllowanceExhausted(
|
||||
$organization,
|
||||
$this->usage->emailCountThisMonth($organization),
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->usesCustomerEmail($organization)) {
|
||||
return true;
|
||||
}
|
||||
@@ -57,11 +93,18 @@ class NotificationBillingService
|
||||
|
||||
public function canAffordSms(Organization $organization, string $message): bool
|
||||
{
|
||||
if ($this->plans->smsAllowanceExhausted(
|
||||
$organization,
|
||||
$this->usage->smsCountThisMonth($organization),
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->usesCustomerSms($organization)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$cost = $this->pricing->smsCostMinor($message);
|
||||
$cost = $this->smsCostMinor($organization, $message);
|
||||
if ($cost <= 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -75,6 +118,13 @@ class NotificationBillingService
|
||||
*/
|
||||
public function chargeEmail(Organization $organization, string $reference, string $description): bool
|
||||
{
|
||||
if ($this->plans->emailAllowanceExhausted(
|
||||
$organization,
|
||||
$this->usage->emailCountThisMonth($organization),
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->usesCustomerEmail($organization)) {
|
||||
$this->usage->recordEmail($organization, 0);
|
||||
|
||||
@@ -99,13 +149,20 @@ class NotificationBillingService
|
||||
|
||||
public function chargeSms(Organization $organization, string $message, string $reference, string $description): bool
|
||||
{
|
||||
if ($this->plans->smsAllowanceExhausted(
|
||||
$organization,
|
||||
$this->usage->smsCountThisMonth($organization),
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->usesCustomerSms($organization)) {
|
||||
$this->usage->recordSms($organization, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$cost = $this->pricing->smsCostMinor($message);
|
||||
$cost = $this->smsCostMinor($organization, $message);
|
||||
if ($cost <= 0) {
|
||||
$this->usage->recordSms($organization, 0);
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ class NotificationDispatcher
|
||||
'organization_id' => $organization->id,
|
||||
]);
|
||||
} elseif (! $this->billing->canAffordEmail($organization)) {
|
||||
Log::info('Frontdesk host email skipped — insufficient wallet balance', [
|
||||
Log::info('Frontdesk host email skipped — plan allowance exhausted or insufficient wallet balance', [
|
||||
'host_id' => $host->id,
|
||||
'organization_id' => $organization->id,
|
||||
]);
|
||||
@@ -261,7 +261,7 @@ class NotificationDispatcher
|
||||
'organization_id' => $organization->id,
|
||||
]);
|
||||
} elseif (! $this->billing->canAffordSms($organization, $smsBody)) {
|
||||
Log::info('Frontdesk host SMS skipped — insufficient wallet balance', [
|
||||
Log::info('Frontdesk host SMS skipped — plan allowance exhausted or insufficient wallet balance', [
|
||||
'host_id' => $host->id,
|
||||
'organization_id' => $organization->id,
|
||||
]);
|
||||
|
||||
@@ -27,6 +27,11 @@ class NotificationUsageService
|
||||
return (int) $this->forOrganization($organization)->email_count;
|
||||
}
|
||||
|
||||
public function smsCountThisMonth(Organization $organization): int
|
||||
{
|
||||
return (int) $this->forOrganization($organization)->sms_count;
|
||||
}
|
||||
|
||||
public function recordEmail(Organization $organization, int $chargedMinor = 0): void
|
||||
{
|
||||
$usage = $this->forOrganization($organization);
|
||||
@@ -36,7 +41,7 @@ class NotificationUsageService
|
||||
}
|
||||
}
|
||||
|
||||
public function recordSms(Organization $organization, int $chargedMinor): void
|
||||
public function recordSms(Organization $organization, int $chargedMinor = 0): void
|
||||
{
|
||||
$usage = $this->forOrganization($organization);
|
||||
$usage->increment('sms_count');
|
||||
|
||||
@@ -49,7 +49,10 @@ class PlanService
|
||||
];
|
||||
}
|
||||
|
||||
/** Null means unlimited included host emails (Pro / Enterprise). */
|
||||
/**
|
||||
* Included host emails per calendar month.
|
||||
* Null means unlimited (Enterprise). Free/Pro use finite allowances.
|
||||
*/
|
||||
public function freeEmailsPerMonth(Organization $organization): ?int
|
||||
{
|
||||
$value = config('frontdesk.plans.'.$this->planKey($organization).'.free_emails_per_month', 100);
|
||||
@@ -57,6 +60,44 @@ class PlanService
|
||||
return $value === null ? null : (int) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Included host SMS per calendar month.
|
||||
* Null means unlimited (Enterprise). 0 means none included.
|
||||
*/
|
||||
public function freeSmsPerMonth(Organization $organization): ?int
|
||||
{
|
||||
$plan = (array) config('frontdesk.plans.'.$this->planKey($organization), []);
|
||||
if (! array_key_exists('free_sms_per_month', $plan)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$value = $plan['free_sms_per_month'];
|
||||
|
||||
return $value === null ? null : (int) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paid plans with a finite allowance stop sending once the monthly cap is hit.
|
||||
* Free keeps pay-as-you-go after its free email allowance.
|
||||
*/
|
||||
public function emailAllowanceExhausted(Organization $organization, int $sentThisMonth): bool
|
||||
{
|
||||
$allowance = $this->freeEmailsPerMonth($organization);
|
||||
|
||||
return $this->hasPaidPlan($organization)
|
||||
&& $allowance !== null
|
||||
&& $sentThisMonth >= $allowance;
|
||||
}
|
||||
|
||||
public function smsAllowanceExhausted(Organization $organization, int $sentThisMonth): bool
|
||||
{
|
||||
$allowance = $this->freeSmsPerMonth($organization);
|
||||
|
||||
return $this->hasPaidPlan($organization)
|
||||
&& $allowance !== null
|
||||
&& $sentThisMonth >= $allowance;
|
||||
}
|
||||
|
||||
public function activeBranchCount(Organization $organization): int
|
||||
{
|
||||
return Branch::query()
|
||||
|
||||
@@ -132,6 +132,8 @@ return [
|
||||
'max_branches' => 1,
|
||||
'max_kiosk_devices' => 1,
|
||||
'free_emails_per_month' => (int) env('FRONTDESK_FREE_EMAILS_PER_MONTH', 100),
|
||||
// Free has no included SMS — billed per segment (or customer SMS key).
|
||||
'free_sms_per_month' => (int) env('FRONTDESK_FREE_SMS_PER_MONTH', 0),
|
||||
'features' => [
|
||||
'check_in',
|
||||
'hosts',
|
||||
@@ -152,9 +154,9 @@ return [
|
||||
),
|
||||
'max_branches' => null,
|
||||
'max_kiosk_devices' => null,
|
||||
'free_emails_per_month' => env('FRONTDESK_PRO_FREE_EMAILS_PER_MONTH') !== null
|
||||
? (int) env('FRONTDESK_PRO_FREE_EMAILS_PER_MONTH')
|
||||
: null,
|
||||
// 5,000 email + 5,000 SMS host alerts / month (10,000 combined).
|
||||
'free_emails_per_month' => (int) env('FRONTDESK_PRO_FREE_EMAILS_PER_MONTH', 5000),
|
||||
'free_sms_per_month' => (int) env('FRONTDESK_PRO_FREE_SMS_PER_MONTH', 5000),
|
||||
'features' => [
|
||||
'check_in',
|
||||
'hosts',
|
||||
@@ -165,7 +167,6 @@ return [
|
||||
'integrations',
|
||||
'webhooks',
|
||||
'scheduled_reports',
|
||||
'custom_badge_templates',
|
||||
],
|
||||
],
|
||||
'enterprise' => [
|
||||
@@ -174,7 +175,9 @@ return [
|
||||
'price_minor_per_branch' => (int) env('FRONTDESK_ENTERPRISE_PRICE_PER_BRANCH_MINOR', 199000),
|
||||
'max_branches' => null,
|
||||
'max_kiosk_devices' => null,
|
||||
// null = unlimited host email / SMS alerts.
|
||||
'free_emails_per_month' => null,
|
||||
'free_sms_per_month' => null,
|
||||
'features' => [
|
||||
'check_in',
|
||||
'hosts',
|
||||
|
||||
@@ -101,10 +101,10 @@
|
||||
<li>Ladill-provided visitor kiosks</li>
|
||||
<li>Multi-branch locations & buildings</li>
|
||||
<li>Team roles & invitations</li>
|
||||
<li>Unlimited host email alerts</li>
|
||||
<li>5,000 host email alerts / month</li>
|
||||
<li>5,000 host SMS alerts / month</li>
|
||||
<li>Webhooks & iCal feeds</li>
|
||||
<li>Scheduled report exports</li>
|
||||
<li>Custom badge templates</li>
|
||||
<li>Unlimited kiosk software (billed per branch)</li>
|
||||
</ul>
|
||||
<div class="mt-6">
|
||||
@@ -119,7 +119,7 @@
|
||||
@endif
|
||||
.
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-slate-400">SMS host alerts bill at platform rates from your wallet.</p>
|
||||
<p class="mt-2 text-xs text-slate-400">Pro includes 5,000 email and 5,000 SMS host alerts per month. Upgrade to Enterprise for unlimited alerts.</p>
|
||||
@if ($canManage)
|
||||
<form method="post" action="{{ route('frontdesk.pro.expand-branches') }}" class="mt-4 space-y-2"
|
||||
x-data="{ seats: {{ (int) $billedBranches + 1 }}, rate: {{ (int) $proPricePerBranchMinor }}, current: {{ (int) $billedBranches }} }">
|
||||
@@ -172,7 +172,8 @@
|
||||
</p>
|
||||
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-200">
|
||||
<li>Everything in Pro</li>
|
||||
<li>Ladill-provided visitor kiosks</li>
|
||||
<li>Unlimited host email & SMS alerts</li>
|
||||
<li>Custom badge templates</li>
|
||||
<li>Custom integrations</li>
|
||||
<li>Volume SMS rates</li>
|
||||
<li>Dedicated support & SLA</li>
|
||||
|
||||
@@ -8,8 +8,14 @@
|
||||
<x-settings.card title="Plan & usage" description="Subscription limits and notification billing for {{ $organization->name }}.">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<p class="text-sm leading-6 text-slate-600">
|
||||
@if ($hasPaidPlan)
|
||||
<span class="font-medium text-indigo-700">{{ $plan['label'] ?? 'Pro' }}</span> — Ladill-provided visitor kiosks, unlimited kiosk software, integrations, and unlimited host emails, billed per branch (SMS still billed per segment).
|
||||
@if ($isEnterprise)
|
||||
<span class="font-medium text-indigo-700">Enterprise</span> — unlimited host email & SMS alerts, custom badge templates, integrations, billed per branch.
|
||||
@if ($billedBranches > 0)
|
||||
Billed for {{ $billedBranches }} {{ str('branch')->plural($billedBranches) }}
|
||||
({{ $activeBranchCount }} active).
|
||||
@endif
|
||||
@elseif ($hasPaidPlan)
|
||||
<span class="font-medium text-indigo-700">Pro</span> — Ladill-provided visitor kiosks, integrations, and 5,000 email + 5,000 SMS host alerts / month, billed per branch.
|
||||
@if ($billedBranches > 0)
|
||||
Billed for {{ $billedBranches }} {{ str('branch')->plural($billedBranches) }}
|
||||
({{ $activeBranchCount }} active).
|
||||
@@ -29,20 +35,34 @@
|
||||
<dt class="text-slate-500">Host emails this month</dt>
|
||||
<dd class="mt-0.5 font-semibold text-slate-900">
|
||||
@if ($freeEmailAllowance === null)
|
||||
{{ number_format($monthlyUsage->email_count) }} sent (unlimited on paid plans)
|
||||
{{ number_format($monthlyUsage->email_count) }} sent (unlimited)
|
||||
@else
|
||||
{{ number_format($monthlyUsage->email_count) }} / {{ number_format($freeEmailAllowance) }} free
|
||||
{{ number_format($monthlyUsage->email_count) }} / {{ number_format($freeEmailAllowance) }} included
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-slate-500">Host SMS this month</dt>
|
||||
<dd class="mt-0.5 font-semibold text-slate-900">{{ number_format($monthlyUsage->sms_count) }} sent</dd>
|
||||
<dd class="mt-0.5 font-semibold text-slate-900">
|
||||
@if ($freeSmsAllowance === null)
|
||||
{{ number_format($monthlyUsage->sms_count) }} sent (unlimited)
|
||||
@elseif ($freeSmsAllowance > 0)
|
||||
{{ number_format($monthlyUsage->sms_count) }} / {{ number_format($freeSmsAllowance) }} included
|
||||
@else
|
||||
{{ number_format($monthlyUsage->sms_count) }} sent
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p class="mt-4 text-xs leading-5 text-slate-500">
|
||||
@if ($isEnterprise)
|
||||
Host email and SMS alerts are unlimited on Enterprise.
|
||||
@elseif ($hasPaidPlan)
|
||||
Pro includes 5,000 email and 5,000 SMS alerts per month. Upgrade to Enterprise for unlimited alerts and custom badge templates.
|
||||
@else
|
||||
Email {{ $emailCostFormatted }} each after free allowance · SMS from {{ $smsCostFormatted }} per segment — charged to your
|
||||
<a href="{{ route('frontdesk.wallet') }}" class="font-medium text-indigo-600 underline">Ladill wallet</a>.
|
||||
@endif
|
||||
</p>
|
||||
</x-settings.card>
|
||||
@endif
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
<span class="font-medium text-slate-900">{{ $title }}</span>
|
||||
</div>
|
||||
|
||||
<x-settings.card title="Upgrade to Pro">
|
||||
<x-settings.card title="Upgrade your plan">
|
||||
<p class="text-sm text-slate-600">
|
||||
GHS {{ number_format($proPriceMinor / 100, 0) }} / branch / month from your Ladill wallet —
|
||||
provided visitor kiosks, multi-branch locations, team roles, integrations, and more.
|
||||
GHS {{ number_format($proPriceMinor / 100, 0) }} / branch / month for Pro from your Ladill wallet —
|
||||
provided visitor kiosks, multi-branch locations, team roles, and integrations. Enterprise adds unlimited alerts and custom badge templates.
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap items-center gap-3">
|
||||
<a href="{{ route('frontdesk.pro.index') }}" class="btn-primary">View Pro plans</a>
|
||||
<a href="{{ route('frontdesk.pro.index') }}" class="btn-primary">View plans</a>
|
||||
<a href="{{ route('frontdesk.wallet') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">Top up wallet</a>
|
||||
</div>
|
||||
</x-settings.card>
|
||||
|
||||
@@ -242,7 +242,7 @@ class FrontdeskNotificationBillingTest extends TestCase
|
||||
->assertSee('Upgrade to Pro');
|
||||
}
|
||||
|
||||
public function test_pro_plan_emails_record_usage_without_wallet_debit(): void
|
||||
public function test_pro_plan_emails_within_allowance_skip_wallet_debit(): void
|
||||
{
|
||||
Http::fake([
|
||||
'billing.test/can-afford*' => Http::response(['affordable' => true]),
|
||||
@@ -260,7 +260,7 @@ class FrontdeskNotificationBillingTest extends TestCase
|
||||
NotificationUsage::create([
|
||||
'organization_id' => $this->organization->id,
|
||||
'period' => now()->format('Y-m'),
|
||||
'email_count' => 10_000,
|
||||
'email_count' => 4_999,
|
||||
]);
|
||||
|
||||
$host = Host::create([
|
||||
@@ -284,10 +284,96 @@ class FrontdeskNotificationBillingTest extends TestCase
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), '/debit'));
|
||||
|
||||
$usage = NotificationUsage::where('organization_id', $this->organization->id)->first();
|
||||
$this->assertSame(10_001, $usage->email_count);
|
||||
$this->assertSame(5_000, $usage->email_count);
|
||||
$this->assertSame(0, $usage->email_charged_minor);
|
||||
}
|
||||
|
||||
public function test_pro_plan_stops_email_after_monthly_allowance(): void
|
||||
{
|
||||
Http::fake([
|
||||
'billing.test/can-afford*' => Http::response(['affordable' => true]),
|
||||
'billing.test/debit' => Http::response(['ok' => true]),
|
||||
'*/api/smtp/send' => Http::response(['success' => true]),
|
||||
]);
|
||||
|
||||
$this->organization->update([
|
||||
'settings' => array_merge($this->organization->settings ?? [], [
|
||||
'plan' => 'pro',
|
||||
'plan_expires_at' => now()->addMonth()->toIso8601String(),
|
||||
]),
|
||||
]);
|
||||
|
||||
NotificationUsage::create([
|
||||
'organization_id' => $this->organization->id,
|
||||
'period' => now()->format('Y-m'),
|
||||
'email_count' => 5_000,
|
||||
]);
|
||||
|
||||
$host = Host::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Capped Host',
|
||||
'email' => 'capped@example.com',
|
||||
'is_available' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->post(route('frontdesk.visits.store'), [
|
||||
'full_name' => 'Capped Guest',
|
||||
'host_id' => $host->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => '1',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), '/api/smtp/send'));
|
||||
|
||||
$usage = NotificationUsage::where('organization_id', $this->organization->id)->first();
|
||||
$this->assertSame(5_000, $usage->email_count);
|
||||
}
|
||||
|
||||
public function test_enterprise_plan_emails_are_unlimited(): void
|
||||
{
|
||||
Http::fake([
|
||||
'billing.test/can-afford*' => Http::response(['affordable' => true]),
|
||||
'billing.test/debit' => Http::response(['ok' => true]),
|
||||
'*/api/smtp/send' => Http::response(['success' => true]),
|
||||
]);
|
||||
|
||||
$this->organization->update([
|
||||
'settings' => array_merge($this->organization->settings ?? [], [
|
||||
'plan' => 'enterprise',
|
||||
'plan_expires_at' => now()->addMonth()->toIso8601String(),
|
||||
]),
|
||||
]);
|
||||
|
||||
NotificationUsage::create([
|
||||
'organization_id' => $this->organization->id,
|
||||
'period' => now()->format('Y-m'),
|
||||
'email_count' => 50_000,
|
||||
]);
|
||||
|
||||
$host = Host::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Ent Host',
|
||||
'email' => 'ent@example.com',
|
||||
'is_available' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->post(route('frontdesk.visits.store'), [
|
||||
'full_name' => 'Ent Guest',
|
||||
'host_id' => $host->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => '1',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/api/smtp/send'));
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), '/debit'));
|
||||
}
|
||||
|
||||
public function test_linked_user_still_gets_in_app_notification(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
@@ -61,8 +61,15 @@ class FrontdeskPhase8Test extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_badge_template_settings_can_be_updated(): void
|
||||
public function test_badge_template_settings_can_be_updated_on_enterprise(): void
|
||||
{
|
||||
$this->organization->update([
|
||||
'settings' => array_merge($this->organization->settings ?? [], [
|
||||
'plan' => 'enterprise',
|
||||
'plan_expires_at' => now()->addYear()->toIso8601String(),
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->put(route('frontdesk.settings.badge.update'), [
|
||||
'show_photo' => '1',
|
||||
@@ -80,6 +87,30 @@ class FrontdeskPhase8Test extends TestCase
|
||||
$this->assertFalse($settings['badge_template']['show_company']);
|
||||
}
|
||||
|
||||
public function test_badge_template_requires_enterprise(): void
|
||||
{
|
||||
$this->organization->update([
|
||||
'settings' => array_merge($this->organization->settings ?? [], [
|
||||
'plan' => 'pro',
|
||||
'plan_expires_at' => now()->addMonth()->toIso8601String(),
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->get(route('frontdesk.settings.badge'))
|
||||
->assertOk()
|
||||
->assertSee('Upgrade your plan');
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->put(route('frontdesk.settings.badge.update'), [
|
||||
'show_photo' => '1',
|
||||
'show_qr' => '1',
|
||||
'primary_color' => '#00ff00',
|
||||
])
|
||||
->assertRedirect(route('frontdesk.pro.index'))
|
||||
->assertSessionHas('error');
|
||||
}
|
||||
|
||||
public function test_check_in_stores_photo_from_base64(): void
|
||||
{
|
||||
$png = 'data:image/png;base64,'.base64_encode(UploadedFile::fake()->image('photo.png')->getContent());
|
||||
|
||||
Reference in New Issue
Block a user