feat(transactions): release transaction analysis to all users
Remove the TransactionAnalysis Pennant feature flag and its gating across the API, Inertia shared data, and the transactions UI so the analysis drawer and saved filters are available to everyone.
This commit is contained in:
parent
5db6cdc6d2
commit
8b790013a9
|
|
@ -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;
|
||||
|
|
@ -178,19 +177,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