add billable rate restriction interfaces for billing extension

This commit is contained in:
Gregor Vostrak
2026-10-06 17:20:33 +02:00
parent d54296e66a
commit e793b81b35
36 changed files with 635 additions and 48 deletions

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Enums\Role;
use App\Models\Organization;
use App\Service\DashboardService;
use App\Service\PermissionStore;
@@ -159,7 +158,7 @@ class ChartController extends Controller
$this->checkPermission($organization, 'charts:view:own');
$user = $this->user();
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
$showBillableRate = $this->canSeeBillableRates($organization);
if (! $showBillableRate) {
throw new AuthorizationException('You do not have permission to view billable rates.');
}

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Enums\Role;
use App\Exceptions\Api\FeatureIsNotAvailableInFreePlanApiException;
use App\Models\Organization;
use App\Service\BillingContract;
use App\Service\PermissionStore;
@@ -49,4 +51,30 @@ class Controller extends \App\Http\Controllers\Controller
{
return app(BillingContract::class)->hasSubscription($organization) || app(BillingContract::class)->hasTrial($organization);
}
protected function canUseBillableRates(Organization $organization): bool
{
return app(BillingContract::class)->canUseBillableRates($organization);
}
protected function canSeeBillableRates(Organization $organization): bool
{
if (! $this->canUseBillableRates($organization)) {
return false;
}
return $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
}
/**
* Organizations that can not use billable rates may keep or remove an existing rate, but not set a new one
*
* @throws FeatureIsNotAvailableInFreePlanApiException
*/
protected function checkBillableRateChange(Organization $organization, ?int $oldBillableRate, ?int $newBillableRate): void
{
if ($newBillableRate !== null && $newBillableRate !== $oldBillableRate && ! $this->canUseBillableRates($organization)) {
throw new FeatureIsNotAvailableInFreePlanApiException;
}
}
}

View File

@@ -83,6 +83,7 @@ class MemberController extends Controller
$this->checkPermission($organization, 'members:update', $member);
if ($request->has('billable_rate') && $member->billable_rate !== $request->getBillableRate()) {
$this->checkBillableRateChange($organization, $member->billable_rate, $request->getBillableRate());
$member->billable_rate = $request->getBillableRate();
$billableRateService->updateTimeEntriesBillableRateForMember($member);

View File

@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Enums\Role;
use App\Events\AfterCreateOrganization;
use App\Exceptions\Api\FeatureIsNotAvailableInFreePlanApiException;
use App\Http\Requests\V1\Organization\OrganizationDestroyRequest;
use App\Http\Requests\V1\Organization\OrganizationStoreRequest;
use App\Http\Requests\V1\Organization\OrganizationUpdateRequest;
@@ -32,7 +32,7 @@ class OrganizationController extends Controller
{
$this->checkPermission($organization, 'organizations:view');
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
$showBillableRate = $this->canSeeBillableRates($organization);
return new OrganizationResource($organization, $showBillableRate);
}
@@ -54,7 +54,10 @@ class OrganizationController extends Controller
if ($request->getCurrency() !== null) {
$organization->currency = $request->getCurrency();
}
if ($request->getEmployeesCanSeeBillableRates() !== null) {
if ($request->getEmployeesCanSeeBillableRates() !== null && $request->getEmployeesCanSeeBillableRates() !== $organization->employees_can_see_billable_rates) {
if (! $this->canUseBillableRates($organization)) {
throw new FeatureIsNotAvailableInFreePlanApiException;
}
$organization->employees_can_see_billable_rates = $request->getEmployeesCanSeeBillableRates();
}
if ($request->getEmployeesCanManageTasks() !== null) {
@@ -83,6 +86,7 @@ class OrganizationController extends Controller
}
$hasBillableRate = $request->has('billable_rate');
if ($hasBillableRate) {
$this->checkBillableRateChange($organization, $organization->billable_rate, $request->getBillableRate());
$oldBillableRate = $organization->billable_rate;
$organization->billable_rate = $request->getBillableRate();
}

View File

@@ -65,7 +65,7 @@ class ProjectController extends Controller
->orderBy('id')
->paginate(config('app.pagination_per_page_default'));
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
$showBillableRate = $this->canSeeBillableRates($organization);
return new ProjectCollection($projects, $showBillableRate);
}
@@ -99,6 +99,7 @@ class ProjectController extends Controller
public function store(Organization $organization, ProjectStoreRequest $request): JsonResource
{
$this->checkPermission($organization, 'projects:create');
$this->checkBillableRateChange($organization, null, $request->getBillableRate());
$project = new Project;
$project->name = $request->input('name');
$project->color = $request->input('color');
@@ -125,6 +126,7 @@ class ProjectController extends Controller
public function update(Organization $organization, Project $project, ProjectUpdateRequest $request, BillableRateService $billableRateService): JsonResource
{
$this->checkPermission($organization, 'projects:update', $project);
$this->checkBillableRateChange($organization, $project->billable_rate, $request->getBillableRate());
$project->name = $request->input('name');
$project->color = $request->input('color');
$project->is_billable = (bool) $request->input('is_billable');
@@ -139,14 +141,17 @@ class ProjectController extends Controller
}
$oldBillableRate = $project->billable_rate;
$clientIdChanged = false;
$project->billable_rate = $request->getBillableRate();
// Organizations that can not use billable rates do not receive the rate of the project, so the incoming value is ignored to keep the existing rate
if ($this->canUseBillableRates($organization)) {
$project->billable_rate = $request->getBillableRate();
}
if ($project->client_id !== $request->input('client_id')) {
$project->client_id = $request->input('client_id');
$clientIdChanged = true;
}
$project->save();
if ($oldBillableRate !== $request->getBillableRate()) {
if ($oldBillableRate !== $project->billable_rate) {
$billableRateService->updateTimeEntriesBillableRateForProject($project);
}
if ($clientIdChanged) {

View File

@@ -65,6 +65,7 @@ class ProjectMemberController extends Controller
public function store(Organization $organization, Project $project, ProjectMemberStoreRequest $request, BillableRateService $billableRateService): JsonResource
{
$this->checkPermission($organization, 'project-members:create', $project);
$this->checkBillableRateChange($organization, null, $request->getBillableRate());
$member = Member::findOrFail((string) $request->input('member_id'));
if ($member->user->is_placeholder) {
@@ -98,6 +99,7 @@ class ProjectMemberController extends Controller
public function update(Organization $organization, ProjectMember $projectMember, ProjectMemberUpdateRequest $request, BillableRateService $billableRateService): JsonResource
{
$this->checkPermission($organization, 'project-members:update', projectMember: $projectMember);
$this->checkBillableRateChange($organization, $projectMember->billable_rate, $request->getBillableRate());
$oldBillableRate = $projectMember->billable_rate;
$projectMember->billable_rate = $request->getBillableRate();
$projectMember->save();

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Enums\ExportFormat;
use App\Enums\Role;
use App\Enums\TimeEntryType;
use App\Exceptions\Api\FeatureIsNotAvailableInFreePlanApiException;
use App\Exceptions\Api\OverlappingTimeEntryApiException;
@@ -240,7 +239,7 @@ class TimeEntryController extends Controller
}
$user = $this->user();
$timezone = $user->timezone;
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
$showBillableRate = $this->canSeeBillableRates($organization);
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
@@ -386,7 +385,7 @@ class TimeEntryController extends Controller
}
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
$user = $this->user();
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
$showBillableRate = $this->canSeeBillableRates($organization);
$group1Type = $request->getGroup();
$group2Type = $request->getSubGroup();
@@ -441,7 +440,7 @@ class TimeEntryController extends Controller
}
$debug = $request->getDebug();
$user = $this->user();
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
$showBillableRate = $this->canSeeBillableRates($organization);
$group = $request->getGroup();
$subGroup = $request->getSubGroup();