feat(transactions): release transaction analysis to all users (#579)
## Summary Removes the `TransactionAnalysis` Pennant feature flag and all its gating. After this PR the transaction analysis feature (analysis drawer + saved filters) is available to every user. ## Changes - Delete `app/Features/TransactionAnalysis.php`. - `TransactionAnalysisController` — drop the `abort_unless(...403)` flag gate and Pennant imports. - `HandleInertiaRequests` — stop sharing the `transactionAnalysis` flag in Inertia props. - `Features` TS type — remove the `transactionAnalysis` field. - `transaction-actions-menu.tsx` / `transaction-filters.tsx` — remove the `features.transactionAnalysis &&` gates so the Analysis button, the analysis drawer, and saved filters always render. - Tests updated: dropped the endpoint-gating test and the "hidden when flag off" UI test; adjusted shared-flag expectations. ## Verification - `./vendor/bin/pest` — 23 passed (affected suites) - Vitest — affected component/page tests pass - Pint, Prettier, ESLint clean ## Notes - The orphaned value left in Pennant's `features` DB table is harmless and not addressed here.
This commit is contained in:
parent
d11aa2dfe5
commit
ae6f869611
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*/
|
||||
class TransactionAnalysis
|
||||
{
|
||||
/**
|
||||
* Resolve the feature's initial value.
|
||||
*/
|
||||
public function resolve(?User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Features\TransactionAnalysis;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\IndexTransactionRequest;
|
||||
use App\Models\Label;
|
||||
|
|
@ -12,7 +11,6 @@ use App\Services\ExchangeRateService;
|
|||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
class TransactionAnalysisController extends Controller
|
||||
{
|
||||
|
|
@ -37,8 +35,6 @@ class TransactionAnalysisController extends Controller
|
|||
{
|
||||
$user = $request->user();
|
||||
|
||||
abort_unless(Feature::for($user)->active(TransactionAnalysis::class), 403);
|
||||
|
||||
$validated = $request->validated();
|
||||
$currency = $user->currency_code;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ namespace App\Http\Middleware;
|
|||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Enums\BankingProvider;
|
||||
use App\Features\CalculateBalancesOnImport;
|
||||
use App\Features\TransactionAnalysis;
|
||||
use App\Models\BankingConnection;
|
||||
use App\Services\CurrencyOptions;
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
|
|
@ -179,19 +178,16 @@ class HandleInertiaRequests extends Middleware
|
|||
return [
|
||||
'cashflow' => true,
|
||||
'calculateBalancesOnImport' => false,
|
||||
'transactionAnalysis' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$features = Feature::for($user)->values([
|
||||
CalculateBalancesOnImport::class,
|
||||
TransactionAnalysis::class,
|
||||
]);
|
||||
|
||||
return [
|
||||
'cashflow' => true,
|
||||
'calculateBalancesOnImport' => $features[CalculateBalancesOnImport::class] !== false,
|
||||
'transactionAnalysis' => $features[TransactionAnalysis::class] !== false,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { type TransactionFilters } from '@/types/transaction';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type React from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { TransactionActionsMenu } from './transaction-actions-menu';
|
||||
|
||||
const features = { transactionAnalysis: true };
|
||||
|
||||
vi.mock('@/actions/App/Http/Controllers/TransactionController', () => ({
|
||||
categorize: { url: () => '/transactions/categorize' },
|
||||
}));
|
||||
|
|
@ -22,7 +20,6 @@ vi.mock('@inertiajs/react', () => ({
|
|||
Link: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
usePage: () => ({ props: { features } }),
|
||||
}));
|
||||
|
||||
vi.mock('./import-transactions-drawer', () => ({
|
||||
|
|
@ -61,17 +58,6 @@ function renderMenu(filters: TransactionFilters) {
|
|||
}
|
||||
|
||||
describe('TransactionActionsMenu analysis button', () => {
|
||||
beforeEach(() => {
|
||||
features.transactionAnalysis = true;
|
||||
});
|
||||
|
||||
it('is hidden when the TransactionAnalysis feature flag is off', () => {
|
||||
features.transactionAnalysis = false;
|
||||
renderMenu(emptyFilters);
|
||||
|
||||
expect(screen.queryByText('Analysis')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('is disabled when no filter is applied', () => {
|
||||
renderMenu(emptyFilters);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import { useIsMobile } from '@/hooks/use-mobile';
|
|||
import { useReEvaluateAllTransactions } from '@/hooks/use-re-evaluate-all-transactions';
|
||||
import { hasActiveFilters } from '@/lib/transaction-filter-serialization';
|
||||
|
||||
import { type SharedData } from '@/types';
|
||||
import { type Account, type Bank } from '@/types/account';
|
||||
import { type AutomationRule } from '@/types/automation-rule';
|
||||
import { type Category } from '@/types/category';
|
||||
|
|
@ -26,7 +25,7 @@ import {
|
|||
type TransactionFilters,
|
||||
} from '@/types/transaction';
|
||||
import { __ } from '@/utils/i18n';
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import {
|
||||
BarChart3,
|
||||
ChevronDown,
|
||||
|
|
@ -61,7 +60,6 @@ export function TransactionActionsMenu({
|
|||
onImportComplete,
|
||||
filters,
|
||||
}: TransactionActionsMenuProps) {
|
||||
const { features } = usePage<SharedData>().props;
|
||||
const isMobile = useIsMobile();
|
||||
const [importDrawerOpen, setImportDrawerOpen] = useState(false);
|
||||
const [analysisDrawerOpen, setAnalysisDrawerOpen] = useState(false);
|
||||
|
|
@ -106,65 +104,60 @@ export function TransactionActionsMenu({
|
|||
return (
|
||||
<>
|
||||
<ButtonGroup>
|
||||
{features.transactionAnalysis &&
|
||||
(isMobile ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip
|
||||
open={!canAnalyze && analysisHintOpen}
|
||||
onOpenChange={setAnalysisHintOpen}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={
|
||||
!canAnalyze
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}
|
||||
aria-disabled={!canAnalyze}
|
||||
onClick={handleAnalysisClick}
|
||||
>
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
{__('Analysis')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{!canAnalyze && (
|
||||
<TooltipContent>
|
||||
{__(
|
||||
'Apply a filter to enable this button',
|
||||
)}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={
|
||||
!canAnalyze
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}
|
||||
aria-disabled={!canAnalyze}
|
||||
onClick={handleAnalysisClick}
|
||||
>
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
{__('Analysis')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{!canAnalyze && (
|
||||
<TooltipContent>
|
||||
{__(
|
||||
'Apply a filter to enable this button',
|
||||
)}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
{isMobile ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip
|
||||
open={!canAnalyze && analysisHintOpen}
|
||||
onOpenChange={setAnalysisHintOpen}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={
|
||||
!canAnalyze
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}
|
||||
aria-disabled={!canAnalyze}
|
||||
onClick={handleAnalysisClick}
|
||||
>
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
{__('Analysis')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{!canAnalyze && (
|
||||
<TooltipContent>
|
||||
{__('Apply a filter to enable this button')}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={
|
||||
!canAnalyze
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}
|
||||
aria-disabled={!canAnalyze}
|
||||
onClick={handleAnalysisClick}
|
||||
>
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
{__('Analysis')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{!canAnalyze && (
|
||||
<TooltipContent>
|
||||
{__('Apply a filter to enable this button')}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
|
|
@ -273,13 +266,11 @@ export function TransactionActionsMenu({
|
|||
onImportComplete={onImportComplete}
|
||||
/>
|
||||
|
||||
{features.transactionAnalysis && (
|
||||
<TransactionAnalysisDrawer
|
||||
open={analysisDrawerOpen}
|
||||
onOpenChange={setAnalysisDrawerOpen}
|
||||
filters={filters}
|
||||
/>
|
||||
)}
|
||||
<TransactionAnalysisDrawer
|
||||
open={analysisDrawerOpen}
|
||||
onOpenChange={setAnalysisDrawerOpen}
|
||||
filters={filters}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { __ } from '@/utils/i18n';
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import { format } from 'date-fns';
|
||||
import * as Icons from 'lucide-react';
|
||||
import { ChevronsUpDown, Tag, X } from 'lucide-react';
|
||||
|
|
@ -32,7 +31,6 @@ import {
|
|||
toggleCategorySelection,
|
||||
} from '@/lib/category-tree';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type SharedData } from '@/types';
|
||||
import { type Account } from '@/types/account';
|
||||
import { type Category, getCategoryColorClasses } from '@/types/category';
|
||||
import { getLabelColorClasses, type Label } from '@/types/label';
|
||||
|
|
@ -60,7 +58,6 @@ export function TransactionFilters({
|
|||
hideAccountFilter = false,
|
||||
enableSavedFilters = false,
|
||||
}: TransactionFiltersProps) {
|
||||
const { features } = usePage<SharedData>().props;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [categoryDropdownOpen, setCategoryDropdownOpen] = useState(false);
|
||||
const [categorySearch, setCategorySearch] = useState('');
|
||||
|
|
@ -733,7 +730,7 @@ export function TransactionFilters({
|
|||
/>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{enableSavedFilters && features.transactionAnalysis && (
|
||||
{enableSavedFilters && (
|
||||
<SavedFilters
|
||||
filters={filters}
|
||||
onLoad={onFiltersChange}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ vi.mock('@inertiajs/react', () => ({
|
|||
features: {
|
||||
cashflow: true,
|
||||
calculateBalancesOnImport: false,
|
||||
transactionAnalysis: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ export interface NavDivider {
|
|||
export interface Features {
|
||||
cashflow: boolean;
|
||||
calculateBalancesOnImport: boolean;
|
||||
transactionAnalysis: boolean;
|
||||
}
|
||||
|
||||
export interface ExpiredBankingConnectionNotification {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ test('shared feature flags do not include coinbase flag', function () {
|
|||
expect($props['features'])->toBe([
|
||||
'cashflow' => true,
|
||||
'calculateBalancesOnImport' => false,
|
||||
'transactionAnalysis' => false,
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\CategoryType;
|
||||
use App\Features\TransactionAnalysis;
|
||||
use App\Models\Account;
|
||||
use App\Models\Bank;
|
||||
use App\Models\Category;
|
||||
|
|
@ -9,14 +8,12 @@ use App\Models\Label;
|
|||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
beforeEach(function () {
|
||||
Http::fake();
|
||||
|
||||
$this->user = User::factory()->create(['currency_code' => 'USD']);
|
||||
$this->actingAs($this->user);
|
||||
Feature::for($this->user)->activate(TransactionAnalysis::class);
|
||||
|
||||
$this->account = Account::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
|
|
@ -34,12 +31,6 @@ function makeTransaction(array $attributes = []): Transaction
|
|||
]);
|
||||
}
|
||||
|
||||
test('analysis endpoint is gated behind the TransactionAnalysis feature flag', function () {
|
||||
Feature::for($this->user)->deactivate(TransactionAnalysis::class);
|
||||
|
||||
$this->getJson('/api/transactions/analysis')->assertForbidden();
|
||||
});
|
||||
|
||||
test('analysis response is not cached between users', function () {
|
||||
$this->getJson('/api/transactions/analysis')
|
||||
->assertOk()
|
||||
|
|
|
|||
Loading…
Reference in New Issue