feat(periods): gate custom month start day behind feature flag
- add CustomMonthStartDay Pennant feature (off by default) - hide the Start of month setting in account settings unless enabled - only validate month_start_day when the feature is active so other profile updates keep working while the option is hidden - share the flag to the frontend and cover both enabled/disabled paths
This commit is contained in:
parent
4557ef5065
commit
fc2ae4fa3e
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*/
|
||||
class CustomMonthStartDay
|
||||
{
|
||||
/**
|
||||
* Resolve the feature's initial value.
|
||||
*/
|
||||
public function resolve(?User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ namespace App\Http\Middleware;
|
|||
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Features\CalculateBalancesOnImport;
|
||||
use App\Features\CustomMonthStartDay;
|
||||
use App\Features\TransactionAnalysis;
|
||||
use App\Models\BankingConnection;
|
||||
use App\Services\CurrencyOptions;
|
||||
|
|
@ -168,18 +169,21 @@ class HandleInertiaRequests extends Middleware
|
|||
return [
|
||||
'cashflow' => true,
|
||||
'calculateBalancesOnImport' => false,
|
||||
'customMonthStartDay' => false,
|
||||
'transactionAnalysis' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$features = Feature::for($user)->values([
|
||||
CalculateBalancesOnImport::class,
|
||||
CustomMonthStartDay::class,
|
||||
TransactionAnalysis::class,
|
||||
]);
|
||||
|
||||
return [
|
||||
'cashflow' => true,
|
||||
'calculateBalancesOnImport' => $features[CalculateBalancesOnImport::class] !== false,
|
||||
'customMonthStartDay' => $features[CustomMonthStartDay::class] !== false,
|
||||
'transactionAnalysis' => $features[TransactionAnalysis::class] !== false,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use App\Features\CustomMonthStartDay;
|
||||
use App\Models\User;
|
||||
use App\Services\CurrencyOptions;
|
||||
use App\Services\UserMonthPeriodService;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
|
|
@ -20,7 +22,7 @@ class ProfileUpdateRequest extends FormRequest
|
|||
{
|
||||
$currencyOptions = app(CurrencyOptions::class);
|
||||
|
||||
return [
|
||||
$rules = [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
|
||||
'email' => [
|
||||
|
|
@ -33,7 +35,12 @@ class ProfileUpdateRequest extends FormRequest
|
|||
],
|
||||
'currency_code' => ['required', 'string', 'max:3', Rule::in($currencyOptions->primaryCodes())],
|
||||
'locale' => ['nullable', 'string', Rule::in(['en', 'es'])],
|
||||
'month_start_day' => ['required', 'integer', Rule::in(UserMonthPeriodService::ALLOWED_START_DAYS)],
|
||||
];
|
||||
|
||||
if (Feature::for($this->user())->active(CustomMonthStartDay::class)) {
|
||||
$rules['month_start_day'] = ['required', 'integer', Rule::in(UserMonthPeriodService::ALLOWED_START_DAYS)];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export default function Account({
|
|||
twoFactorEnabled?: boolean;
|
||||
notifyOnBankTransactionsSynced?: boolean;
|
||||
}) {
|
||||
const { auth, currencies } = usePage<SharedData>().props;
|
||||
const { auth, currencies, features } = usePage<SharedData>().props;
|
||||
const passwordInput = useRef<HTMLInputElement>(null);
|
||||
const currentPasswordInput = useRef<HTMLInputElement>(null);
|
||||
const [notifyBankTransactions, setNotifyBankTransactions] = useState(
|
||||
|
|
@ -218,53 +218,55 @@ export default function Account({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="month_start_day">
|
||||
{__('Start of month')}
|
||||
</Label>
|
||||
{features.customMonthStartDay && (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="month_start_day">
|
||||
{__('Start of month')}
|
||||
</Label>
|
||||
|
||||
<Select
|
||||
name="month_start_day"
|
||||
defaultValue={String(
|
||||
auth.user.month_start_day ?? 1,
|
||||
)}
|
||||
required
|
||||
>
|
||||
<SelectTrigger className="mt-1 w-full">
|
||||
<SelectValue
|
||||
placeholder={__(
|
||||
'Select start of month',
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">
|
||||
{__('Calendar month (1st)')}
|
||||
</SelectItem>
|
||||
{[25, 26, 27, 28].map((day) => (
|
||||
<SelectItem
|
||||
key={day}
|
||||
value={String(day)}
|
||||
>
|
||||
{__('Day :day', {
|
||||
day: String(day),
|
||||
})}
|
||||
<Select
|
||||
name="month_start_day"
|
||||
defaultValue={String(
|
||||
auth.user.month_start_day ?? 1,
|
||||
)}
|
||||
required
|
||||
>
|
||||
<SelectTrigger className="mt-1 w-full">
|
||||
<SelectValue
|
||||
placeholder={__(
|
||||
'Select start of month',
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">
|
||||
{__('Calendar month (1st)')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{[25, 26, 27, 28].map((day) => (
|
||||
<SelectItem
|
||||
key={day}
|
||||
value={String(day)}
|
||||
>
|
||||
{__('Day :day', {
|
||||
day: String(day),
|
||||
})}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{__(
|
||||
'Cashflow periods and analysis will start on this day.',
|
||||
)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{__(
|
||||
'Cashflow periods and analysis will start on this day.',
|
||||
)}
|
||||
</p>
|
||||
|
||||
<InputError
|
||||
className="mt-2"
|
||||
message={errors.month_start_day}
|
||||
/>
|
||||
</div>
|
||||
<InputError
|
||||
className="mt-2"
|
||||
message={errors.month_start_day}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mustVerifyEmail &&
|
||||
auth.user.email_verified_at === null && (
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export interface NavDivider {
|
|||
export interface Features {
|
||||
cashflow: boolean;
|
||||
calculateBalancesOnImport: boolean;
|
||||
customMonthStartDay: boolean;
|
||||
transactionAnalysis: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
|
||||
use App\Features\CustomMonthStartDay;
|
||||
use App\Models\User;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
test('profile page is displayed', function () {
|
||||
$user = User::factory()->create();
|
||||
|
|
@ -14,6 +16,7 @@ test('profile page is displayed', function () {
|
|||
|
||||
test('profile information can be updated', function () {
|
||||
$user = User::factory()->create();
|
||||
Feature::for($user)->activate(CustomMonthStartDay::class);
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
|
|
@ -130,6 +133,7 @@ test('profile rejects bitcoin as primary currency', function () {
|
|||
|
||||
test('profile rejects unsupported month start day', function () {
|
||||
$user = User::factory()->create();
|
||||
Feature::for($user)->activate(CustomMonthStartDay::class);
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
|
|
@ -143,6 +147,25 @@ test('profile rejects unsupported month start day', function () {
|
|||
$response->assertSessionHasErrors(['month_start_day']);
|
||||
});
|
||||
|
||||
test('profile ignores month start day when feature is disabled', function () {
|
||||
$user = User::factory()->create(['month_start_day' => 1]);
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->patch(route('profile.update'), [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'currency_code' => 'USD',
|
||||
'month_start_day' => 25,
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('account.edit'));
|
||||
|
||||
expect($user->refresh()->month_start_day)->toBe(1);
|
||||
});
|
||||
|
||||
test('email verification status is unchanged when the email address is unchanged', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue