Bug fix
- Surface the localized "network already connected" message on the
Facebook/Instagram/InstagramFacebook/LinkedInPage/Threads/YouTube OAuth
callbacks: catch NetworkAlreadyConnectedException before the generic catch
so the conflict no longer falls through to a generic error + Log::error.
Scope / dead code
- Remove the orphaned BillingController::checkout() + app.billing.checkout route
(onboarding starts checkout directly); delete the now-dead DiscordWidget and
useFeatureAccess composable; prune orphaned i18n keys left by removing the
plan picker / upgrade dialog / count limits (billing.subscribe.*,
accounts.limit_reached, workspaces.limit_reached, common.discord.*).
- Drop the unused `plan` prop from the usage page and the unused `label` from
the onboarding persona payload (labels come from i18n); remove
Persona::options()/label().
Conventions
- declare(strict_types=1) on the two new migrations.
- Extract autofill validation into AutofillBrandRequest (FormRequest).
- Drop the unused $plan param from AccountPolicy::swapPlan.
- CreateUser: drop the stale config('cashier.trial_days', 7) fallback (now 8).
- Rename LimitEnforcementTest to InvitePermissionTest; use Pest mock() helper in
WorkspaceQuantitySyncTest; move shared test helpers into Pest.php.
- Memoize BillingCycle window() + subscription lookup.
90 lines
2.8 KiB
PHP
90 lines
2.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Enums\Plan\Slug;
|
|
use App\Models\Account;
|
|
use App\Models\AiUsageLog;
|
|
use App\Models\Plan;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Policies\AccountPolicy;
|
|
|
|
beforeEach(function () {
|
|
$this->policy = new AccountPolicy;
|
|
$this->account = Account::factory()->create([
|
|
'plan_id' => Plan::where('slug', Slug::Workspace)->value('id'),
|
|
]);
|
|
$this->owner = User::factory()->create(['account_id' => $this->account->id]);
|
|
$this->account->update(['owner_id' => $this->owner->id]);
|
|
});
|
|
|
|
test('swapPlan allows the account owner', function () {
|
|
$response = $this->policy->swapPlan($this->owner, $this->account);
|
|
|
|
expect($response->allowed())->toBeTrue();
|
|
});
|
|
|
|
test('swapPlan denies a non-owner', function () {
|
|
$member = User::factory()->create(['account_id' => $this->account->id]);
|
|
|
|
$response = $this->policy->swapPlan($member, $this->account);
|
|
|
|
expect($response->denied())->toBeTrue();
|
|
expect($response->message())->toBe(__('billing.flash.cannot_manage'));
|
|
});
|
|
|
|
test('useAi allows when subscribed and credits remain', function () {
|
|
config()->set('trypost.self_hosted', false);
|
|
Workspace::factory()->create([
|
|
'account_id' => $this->account->id,
|
|
'user_id' => $this->owner->id,
|
|
]);
|
|
subscribeAccount($this->account);
|
|
|
|
$response = $this->policy->useAi($this->owner, $this->account->fresh());
|
|
|
|
expect($response->allowed())->toBeTrue();
|
|
});
|
|
|
|
test('useAi denies when there is no active subscription', function () {
|
|
config()->set('trypost.self_hosted', false);
|
|
Workspace::factory()->create([
|
|
'account_id' => $this->account->id,
|
|
'user_id' => $this->owner->id,
|
|
]);
|
|
|
|
$response = $this->policy->useAi($this->owner, $this->account->fresh());
|
|
|
|
expect($response->denied())->toBeTrue();
|
|
expect($response->message())->toBe(__('billing.flash.subscription_required'));
|
|
});
|
|
|
|
test('useAi denies when monthly credits are exhausted', function () {
|
|
config()->set('trypost.self_hosted', false);
|
|
$workspace = Workspace::factory()->create([
|
|
'account_id' => $this->account->id,
|
|
'user_id' => $this->owner->id,
|
|
]);
|
|
subscribeAccount($this->account);
|
|
|
|
AiUsageLog::factory()->text(credits: 2500)->create([
|
|
'account_id' => $this->account->id,
|
|
'workspace_id' => $workspace->id,
|
|
]);
|
|
|
|
$response = $this->policy->useAi($this->owner, $this->account->fresh());
|
|
|
|
expect($response->denied())->toBeTrue();
|
|
expect($response->message())->toBe(__('billing.flash.credits_exhausted', [
|
|
'limit' => '2500',
|
|
]));
|
|
});
|
|
|
|
test('useAi always allows when self-hosted', function () {
|
|
config()->set('trypost.self_hosted', true);
|
|
|
|
$response = $this->policy->useAi($this->owner, $this->account);
|
|
|
|
expect($response->allowed())->toBeTrue();
|
|
});
|