fix(categories): expose cashflow setting on create (#448)
## Summary - Always show a simplified Cashflow and charts explanation in the category form - Explain how each category type affects cashflow, income/spending charts, top spending categories, savings, and investments - For transfer categories, keep a cashflow chart visibility selector so users can choose hidden/inflow/outflow - Show savings and investment categories as outflows in the cashflow chart and store default/new categories as outflow - Add a new migration to update existing production savings/investment categories to outflow - Avoid user-facing Sankey terminology and update Spanish copy to use dinero/saldo instead of efectivo - Add missing Spanish translations and browser/feature coverage ## Tests - php artisan test --compact tests/Feature/LocalizationTest.php - php artisan test --compact tests/Feature/Console/ResetUserCategoriesCommandTest.php - php artisan test --compact tests/Feature/Settings/CategoryTest.php --filter="migration updates existing default saving and investment categories|migration sets saving and investment categories to cashflow outflow|users can create savings and investment categories|default categories are created when user registers" - php artisan test --compact tests/Feature/CashflowAnalyticsTest.php --filter="sankey includes savings and investment categories on the expense side|sankey includes outflow transfer categories on the expense side" - php artisan test --compact tests/Browser/CategoriesTest.php --filter="explains savings and investment category cashflow impact in the create dialog|can create a new transfer category with a cashflow analytics direction" - php artisan test --compact tests/Browser/CategoriesTest.php - ./vendor/bin/pest --testsuite=Performance --filter="account show page does not exceed query threshold" - vendor/bin/pint --dirty --format agent - npx prettier --write resources/js/components/categories/category-cashflow-direction-fields.tsx - npm run build (assets emitted; Sentry sourcemap upload reports local SSL error) ## Video - screenshots/category-create-cashflow-setting.webm ## Notes - npm run types still fails on existing unrelated TypeScript errors.
This commit is contained in:
parent
0b9406714e
commit
5119528149
|
|
@ -282,18 +282,21 @@ class CreateDefaultCategories
|
|||
'icon' => 'LineChart',
|
||||
'color' => 'lime',
|
||||
'type' => CategoryType::Investment->value,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Outflow->value,
|
||||
],
|
||||
[
|
||||
'name' => 'Savings',
|
||||
'icon' => 'PiggyBank',
|
||||
'color' => 'lime',
|
||||
'type' => CategoryType::Savings->value,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Outflow->value,
|
||||
],
|
||||
[
|
||||
'name' => 'Other investments',
|
||||
'icon' => 'TrendingUp',
|
||||
'color' => 'lime',
|
||||
'type' => CategoryType::Investment->value,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Outflow->value,
|
||||
],
|
||||
[
|
||||
'name' => 'Financial services and commission',
|
||||
|
|
|
|||
|
|
@ -233,8 +233,12 @@ class CashflowAnalyticsController extends Controller
|
|||
|
||||
$regularCategories = $transactions
|
||||
->filter(function (Transaction $transaction) use ($type): bool {
|
||||
$categoryType = $this->categoryType($transaction);
|
||||
|
||||
return $transaction->category_id !== null
|
||||
&& $this->categoryType($transaction) === $type;
|
||||
&& ($categoryType === $type
|
||||
|| ($type === CategoryType::Expense
|
||||
&& in_array($categoryType, [CategoryType::Savings, CategoryType::Investment], true)));
|
||||
})
|
||||
->groupBy('category_id')
|
||||
->map(function (Collection $transactions) use ($userCurrency): array {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,17 @@ class StoreCategoryRequest extends FormRequest
|
|||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->input('type') !== CategoryType::Transfer->value) {
|
||||
$type = CategoryType::tryFrom((string) $this->input('type'));
|
||||
|
||||
if (in_array($type, [CategoryType::Savings, CategoryType::Investment], true)) {
|
||||
$this->merge([
|
||||
'cashflow_direction' => CategoryCashflowDirection::Outflow->value,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type !== CategoryType::Transfer) {
|
||||
$this->merge([
|
||||
'cashflow_direction' => CategoryCashflowDirection::Hidden->value,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,17 @@ class UpdateCategoryRequest extends FormRequest
|
|||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->input('type') !== CategoryType::Transfer->value) {
|
||||
$type = CategoryType::tryFrom((string) $this->input('type'));
|
||||
|
||||
if (in_array($type, [CategoryType::Savings, CategoryType::Investment], true)) {
|
||||
$this->merge([
|
||||
'cashflow_direction' => CategoryCashflowDirection::Outflow->value,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type !== CategoryType::Transfer) {
|
||||
$this->merge([
|
||||
'cashflow_direction' => CategoryCashflowDirection::Hidden->value,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\CategoryCashflowDirection;
|
||||
use App\Enums\CategoryType;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('categories')
|
||||
->whereIn('type', [
|
||||
CategoryType::Savings->value,
|
||||
CategoryType::Investment->value,
|
||||
])
|
||||
->update([
|
||||
'cashflow_direction' => CategoryCashflowDirection::Outflow->value,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('categories')
|
||||
->whereIn('type', [
|
||||
CategoryType::Savings->value,
|
||||
CategoryType::Investment->value,
|
||||
])
|
||||
->where('cashflow_direction', CategoryCashflowDirection::Outflow->value)
|
||||
->update([
|
||||
'cashflow_direction' => CategoryCashflowDirection::Hidden->value,
|
||||
]);
|
||||
}
|
||||
};
|
||||
49
lang/es.json
49
lang/es.json
|
|
@ -286,11 +286,12 @@
|
|||
"Canadian Dollar": "Dólar canadiense",
|
||||
"Cancel": "Cancelar",
|
||||
"Carry Over": "Acumular",
|
||||
"Cash inflow": "Entrada de efectivo",
|
||||
"Cash outflow": "Salida de efectivo",
|
||||
"Cash inflow": "Entrada de dinero",
|
||||
"Cash outflow": "Salida de dinero",
|
||||
"Cashflow": "Flujo de Caja",
|
||||
"Cashflow Trend": "Tendencia del Flujo de Caja",
|
||||
"Cashflow analytics": "Analitica de flujo de caja",
|
||||
"Cashflow and charts": "Flujo de caja y gráficos",
|
||||
"Cashflow at a Glance": "Flujo de Caja de un Vistazo",
|
||||
"Cashflow income vs expenses bar chart": "Gráfico de barras de ingresos frente a gastos",
|
||||
"Cashflow periods and analysis will start on this day.": "Los períodos y análisis de flujo de caja empezarán este día.",
|
||||
|
|
@ -306,6 +307,7 @@
|
|||
"Category name": "Nombre de categoría",
|
||||
"Change": "Cambiar",
|
||||
"Change category": "Cambiar categoría",
|
||||
"Change the category type above to change how this category appears in cashflow analytics.": "Cambia el tipo de categoría arriba para cambiar cómo aparece esta categoría en la analítica de flujo de caja.",
|
||||
"Chart color scheme": "Esquema de colores del gráfico",
|
||||
"Chart type": "Tipo de gráfico",
|
||||
"Check Demo": "Ver Demo",
|
||||
|
|
@ -315,7 +317,12 @@
|
|||
"Checking": "Cuenta Corriente",
|
||||
"Chilean Peso": "Peso chileno",
|
||||
"Chinese Yuan": "Yuan chino",
|
||||
"Choose Sankey chart visibility": "Elige la visibilidad en el gráfico Sankey",
|
||||
"Choose a category type to see how it affects cashflow and charts.": "Elige un tipo de categoría para ver cómo afecta al flujo de caja y a los gráficos.",
|
||||
"Choose a category type to see how this category will affect cashflow analytics.": "Elige un tipo de categoría para ver cómo afectará esta categoría a la analítica de flujo de caja.",
|
||||
"Choose cashflow chart visibility": "Elige la visibilidad en el gráfico de flujo de caja",
|
||||
"Choose how to handle each account from :bank. You can create new accounts, link to existing ones, or skip.": "Elige cómo gestionar cada cuenta de :bank. Puedes crear nuevas cuentas, vincularlas a las existentes u omitirlas.",
|
||||
"Choose how to report this category": "Elige cómo reportar esta categoría",
|
||||
"Choose how to report this transfer": "Elige como mostrar esta transferencia",
|
||||
"Choose the account where transactions will be imported": "Elige la cuenta donde se importarán las transacciones",
|
||||
"Choose the color palette for your charts": "Elige la paleta de colores para tus gráficos",
|
||||
|
|
@ -494,6 +501,8 @@
|
|||
"Discord": "Discord",
|
||||
"Discord for 80% off": "Discord con 80% de descuento",
|
||||
"Do not show": "No mostrar",
|
||||
"Do not show in Sankey": "No mostrar en Sankey",
|
||||
"Do not show in cashflow chart": "No mostrar en el gráfico de flujo de caja",
|
||||
"Don't have an account?": "¿No tienes una cuenta?",
|
||||
"Download as CSV or Excel format": "Descargar en formato CSV o Excel",
|
||||
"Drop your CSV or Excel file here, or click to browse": "Arrastra tu archivo CSV o Excel aquí, o haz clic para buscar",
|
||||
|
|
@ -578,6 +587,8 @@
|
|||
"Everything you need. Nothing you don't.": "Todo lo que necesitas. Nada que no.",
|
||||
"Expense": "Gasto",
|
||||
"Expense Categories": "Categorías de Gastos",
|
||||
"Expense categories automatically appear as cash outflows in cashflow analytics.": "Las categorías de gasto aparecen automáticamente como salidas de dinero en la analítica de flujo de caja.",
|
||||
"Expense categories count as cash outflow and appear in spending charts, including top spending categories.": "Las categorías de gasto cuentan como salida de dinero y aparecen en los gráficos de gasto, incluidas las categorías con más gasto.",
|
||||
"Expense tracking": "Seguimiento de gastos",
|
||||
"Expenses": "Gastos",
|
||||
"Expires": "Vence",
|
||||
|
|
@ -747,6 +758,8 @@
|
|||
"Income": "Ingresos",
|
||||
"Income (Salary, Freelance, Investments)": "Ingresos (Salario, Freelance, Inversiones)",
|
||||
"Income Sources": "Fuentes de Ingresos",
|
||||
"Income categories automatically appear as cash inflows in cashflow analytics.": "Las categorías de ingreso aparecen automáticamente como entradas de dinero en la analítica de flujo de caja.",
|
||||
"Income categories count as cash inflow and appear in income charts. They do not appear in top spending categories.": "Las categorías de ingreso cuentan como entrada de dinero y aparecen en los gráficos de ingresos. No aparecen en las categorías con más gasto.",
|
||||
"Income minus expenses": "Ingresos menos gastos",
|
||||
"Indian Rupee": "Rupia india",
|
||||
"Information about how you use our service, including access times and features used": "Información sobre cómo usas nuestro servicio, incluyendo horarios de acceso y funciones utilizadas",
|
||||
|
|
@ -761,6 +774,9 @@
|
|||
"Invested amount": "Cantidad invertida",
|
||||
"Investment": "Inversión",
|
||||
"Investment Portfolio": "Cartera de Inversión",
|
||||
"Investment categories appear as invested money at the top of cashflow and as cash outflow in the cashflow chart. They stay out of income, expenses, and top spending categories.": "Las categorías de inversión aparecen como dinero invertido en la parte superior del flujo de caja y como salida de dinero en el gráfico de flujo de caja. Quedan fuera de ingresos, gastos y categorías con más gasto.",
|
||||
"Investment categories stay out of income and expense totals in cashflow analytics.": "Las categorías de inversión quedan fuera de los totales de ingresos y gastos en la analítica de flujo de caja.",
|
||||
"Investment categories stay out of income, expenses, cashflow totals, and top spending categories.": "Las categorías de inversión quedan fuera de ingresos, gastos, totales de flujo de caja y categorías con más gasto.",
|
||||
"Is Whisper Money open source?": "¿Es Whisper Money de código abierto?",
|
||||
"Is the app meeting your expectations?": "¿La app cumple con tus expectativas?",
|
||||
"Is the encryption confusing?": "¿La encriptación es confusa?",
|
||||
|
|
@ -781,6 +797,7 @@
|
|||
"Keep me logged in": "Mantenerme conectado",
|
||||
"Keep me logged in (convenient)": "Mantenerme conectado (conveniente)",
|
||||
"Keep me logged in (less secure)": "Mantenerme conectado (menos seguro)",
|
||||
"Keep this category out of cashflow analytics.": "Mantén esta categoría fuera de la analítica de flujo de caja.",
|
||||
"Keep this transfer out of cashflow analytics.": "Mantiene esta transferencia fuera de la analitica de flujo de caja.",
|
||||
"Key Storage": "Almacenamiento de Clave",
|
||||
"Label (Optional)": "Etiqueta (Opcional)",
|
||||
|
|
@ -885,8 +902,8 @@
|
|||
"Monthly": "Mensual",
|
||||
"Monthly Payment": "Pago Mensual",
|
||||
"Monthly code": "Código mensual",
|
||||
"Monthly income, expenses, and net cashflow for the selected period": "Ingresos, gastos y flujo de efectivo neto mensual para el período seleccionado",
|
||||
"Monthly income, expenses, and net cashflow over the last 12 months": "Ingresos, gastos y flujo de efectivo neto mensual durante los últimos 12 meses",
|
||||
"Monthly income, expenses, and net cashflow for the selected period": "Ingresos, gastos y flujo de dinero neto mensual para el período seleccionado",
|
||||
"Monthly income, expenses, and net cashflow over the last 12 months": "Ingresos, gastos y flujo de dinero neto mensual durante los últimos 12 meses",
|
||||
"Monthly income, expenses, tracked transfers, and net cashflow over the last 12 months": "Ingresos, gastos, transferencias seguidas y flujo de caja neto mensual durante los ultimos 12 meses",
|
||||
"More": "Más",
|
||||
"More actions": "Más acciones",
|
||||
|
|
@ -937,7 +954,7 @@
|
|||
"No bank connections yet. Connect a bank to automatically sync your transactions.": "Aún no hay conexiones bancarias. Conecta un banco para sincronizar automáticamente tus transacciones.",
|
||||
"No bank found.": "No se encontró ningún banco.",
|
||||
"No banks found.": "No se encontraron bancos.",
|
||||
"No cashflow data for this period": "Sin datos de flujo de efectivo para este período",
|
||||
"No cashflow data for this period": "Sin datos de flujo de dinero para este período",
|
||||
"No categories found.": "No se encontraron categorías.",
|
||||
"No category found.": "No se encontró categoría.",
|
||||
"No category set": "Sin categoría definida",
|
||||
|
|
@ -1152,6 +1169,9 @@
|
|||
"Savings": "Ahorros",
|
||||
"Savings Rate": "Tasa de Ahorro",
|
||||
"Savings Transfer": "Transferencia a Ahorros",
|
||||
"Savings categories appear as saved money at the top of cashflow and as cash outflow in the cashflow chart. They stay out of income, expenses, and top spending categories.": "Las categorías de ahorro aparecen como dinero ahorrado en la parte superior del flujo de caja y como salida de dinero en el gráfico de flujo de caja. Quedan fuera de ingresos, gastos y categorías con más gasto.",
|
||||
"Savings categories stay out of income and expense totals in cashflow analytics.": "Las categorías de ahorro quedan fuera de los totales de ingresos y gastos en la analítica de flujo de caja.",
|
||||
"Savings categories stay out of income, expenses, cashflow totals, and top spending categories.": "Las categorías de ahorro quedan fuera de ingresos, gastos, totales de flujo de caja y categorías con más gasto.",
|
||||
"Savings rate": "Tasa de ahorro",
|
||||
"Search": "Buscar",
|
||||
"Search bank...": "Buscar banco...",
|
||||
|
|
@ -1244,10 +1264,14 @@
|
|||
"Shared": "Compartido",
|
||||
"Shared with": "Compartido con",
|
||||
"Shopping (Clothing, Electronics, Gifts)": "Compras (Ropa, Electrónica, Regalos)",
|
||||
"Show as cash inflow": "Mostrar como entrada de efectivo",
|
||||
"Show as cash outflow": "Mostrar como salida de efectivo",
|
||||
"Show as cash inflow": "Mostrar como entrada de dinero",
|
||||
"Show as cash outflow": "Mostrar como salida de dinero",
|
||||
"Show in account currency (:currency)": "Mostrar en moneda de la cuenta (:currency)",
|
||||
"Show in cashflow chart as inflow": "Mostrar en el gráfico de flujo de caja como entrada",
|
||||
"Show in cashflow chart as outflow": "Mostrar en el gráfico de flujo de caja como salida",
|
||||
"Show in your currency (:currency)": "Mostrar en tu moneda (:currency)",
|
||||
"Show this transfer category as money entering available cash.": "Muestra esta categoría de transferencia como dinero que entra en el saldo disponible.",
|
||||
"Show this transfer category as money leaving available cash.": "Muestra esta categoría de transferencia como dinero que sale del saldo disponible.",
|
||||
"Sign up": "Crear cuenta",
|
||||
"Simple, transparent pricing": "Precios simples y transparentes",
|
||||
"Single-use. Works on monthly or yearly plans. Tied to your email — please keep it private.": "Un solo uso. Funciona con el plan mensual o anual. Está vinculado a tu email — guárdalo en privado.",
|
||||
|
|
@ -1372,8 +1396,11 @@
|
|||
"This is your exclusive invitation to get full access to everything:": "Esta es tu invitación exclusiva para obtener acceso completo a todo:",
|
||||
"This month's income and expenses": "Ingresos y gastos de este mes",
|
||||
"This password will encrypt all your financial data. Make it strong and memorable — we can't recover it for you.": "Esta contraseña encriptará todos tus datos financieros. Hazla fuerte y memorable: no podemos recuperarla por ti.",
|
||||
"This setting follows the selected category type and cannot be changed here.": "Esta configuración sigue el tipo de categoría seleccionado y no se puede cambiar aquí.",
|
||||
"This transaction was imported from a file. The description cannot be modified.": "Esta transacción fue importada de un archivo. La descripción no se puede modificar.",
|
||||
"This transaction was imported from a\\n file. The description cannot be\\n modified.": "Esta transacción fue importada desde un archivo. La descripción no se puede modificar.",
|
||||
"This transfer category stays out of the Sankey money flow chart.": "Esta categoría de transferencia queda fuera del gráfico Sankey de flujo de dinero.",
|
||||
"This transfer category stays out of the cashflow chart.": "Esta categoría de transferencia queda fuera del gráfico de flujo de caja.",
|
||||
"This will only take a moment.": "Esto solo tardará un momento.",
|
||||
"This will remove your encryption key from this browser session. You'll need to enter your password again to unlock encrypted content.": "Esto eliminará tu clave de encriptación de esta sesión del navegador. Necesitarás ingresar tu contraseña nuevamente para desbloquear el contenido encriptado.",
|
||||
"This will revoke access to your bank account data from :bank.": "Esto revocará el acceso a los datos de tu cuenta bancaria de :bank.",
|
||||
|
|
@ -1413,8 +1440,8 @@
|
|||
"Track all your finances in one place \\u2014 checking,\\n savings, credit cards, investments, and more.": "Realiza un seguimiento de todas tus finanzas en un solo lugar: cuentas corrientes, ahorros, tarjetas de crédito, inversiones y más.",
|
||||
"Track all your finances in one place — checking, savings, credit cards, investments, and more.": "Rastrea todas tus finanzas en un solo lugar: cuenta corriente, ahorros, tarjetas de crédito, inversiones y más.",
|
||||
"Track credit card spending": "Rastrea los gastos de tarjeta de crédito",
|
||||
"Track money entering your available cash without counting it as income.": "Sigue el dinero que entra en tu efectivo disponible sin contarlo como ingreso.",
|
||||
"Track money leaving your available cash, like investments or savings transfers.": "Sigue el dinero que sale de tu efectivo disponible, como inversiones o transferencias a ahorros.",
|
||||
"Track money entering your available cash without counting it as income.": "Sigue el dinero que entra en tu saldo disponible sin contarlo como ingreso.",
|
||||
"Track money leaving your available cash, like investments or savings transfers.": "Sigue el dinero que sale de tu saldo disponible, como inversiones o transferencias a ahorros.",
|
||||
"Track your budget progress": "Sigue el progreso de tu presupuesto",
|
||||
"Track your income, expenses, and savings": "Rastrea tus ingresos, gastos y ahorros",
|
||||
"Track your property market value over time. Transactions are not supported.": "Rastrea el valor de mercado de tu propiedad a lo largo del tiempo. Las transacciones no están soportadas.",
|
||||
|
|
@ -1437,9 +1464,11 @@
|
|||
"Transaction\\n details, budgets, categories, and other\\n financial information you manually enter\\n into the application": "Detalles de transacciones, presupuestos, categorías y otra información financiera que introduces manualmente en la aplicación",
|
||||
"Transactions": "Transacciones",
|
||||
"Transactions + Balance": "Transacciones + Balance",
|
||||
"Transactions in this category will not be counted in top expenses or income. Transfer categories are mainly used for transactions between accounts.": "Estas transacciones no se contará como gasto ni como ingreso.",
|
||||
"Transactions in this category will not be counted in top expenses or income. Transfer categories are mainly used for transactions between accounts.": "Las transacciones en esta categoría no se contarán entre los principales gastos o ingresos. Las categorías de transferencia se usan principalmente para transacciones entre cuentas.",
|
||||
"Transactions in this category will\\n not be counted in top expenses or\\n income. Transfer categories are\\n mainly used for transactions between\\n accounts.": "Las transacciones en esta categoría no se contarán entre los principales gastos o ingresos. Las categorías de transferencia se usan principalmente para transacciones entre cuentas.",
|
||||
"Transfer": "Transferencia",
|
||||
"Transfer categories are excluded from income, expenses, and top spending categories. Choose whether to show them in the Sankey money flow chart.": "Las categorías de transferencia se excluyen de ingresos, gastos y categorías con más gasto. Elige si quieres mostrarlas en el gráfico Sankey de flujo de dinero.",
|
||||
"Transfer categories are excluded from income, expenses, and top spending categories. Choose whether to show them in the cashflow chart.": "Las categorías de transferencia se excluyen de ingresos, gastos y categorías con más gasto. Elige si quieres mostrarlas en el gráfico de flujo de caja.",
|
||||
"Transfer categories stay out of income and expense totals, but you can still track them in the money flow chart.": "Las categorías de transferencia quedan fuera de los totales de ingresos y gastos, pero aún puedes seguirlas en el gráfico de flujo de dinero.",
|
||||
"Transfer categories stay out of income and expense totals, but you can still track them in the monthly chart.": "Las categorias de transferencia quedan fuera de los totales de ingresos y gastos, pero aun puedes seguirlas en el grafico mensual.",
|
||||
"Transfers (Between accounts, Savings)": "Transferencias (Entre cuentas, Ahorros)",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -9,32 +8,29 @@ import {
|
|||
} from '@/components/ui/select';
|
||||
import { CategoryCashflowDirection, CategoryType } from '@/types/category';
|
||||
import { __ } from '@/utils/i18n';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const cashflowDirectionOptions: Array<{
|
||||
const transferCashflowOptions: Array<{
|
||||
value: CategoryCashflowDirection;
|
||||
label: string;
|
||||
description: string;
|
||||
}> = [
|
||||
{
|
||||
value: 'hidden',
|
||||
label: __('Do not show'),
|
||||
description: __('Keep this transfer out of cashflow analytics.'),
|
||||
label: 'Do not show in cashflow chart',
|
||||
description: 'This transfer category stays out of the cashflow chart.',
|
||||
},
|
||||
{
|
||||
value: 'outflow',
|
||||
label: __('Show as cash outflow'),
|
||||
description: __(
|
||||
'Track money leaving your available cash, like investments or savings transfers.',
|
||||
),
|
||||
label: 'Show in cashflow chart as outflow',
|
||||
description:
|
||||
'Show this transfer category as money leaving available cash.',
|
||||
},
|
||||
{
|
||||
value: 'inflow',
|
||||
label: __('Show as cash inflow'),
|
||||
description: __(
|
||||
'Track money entering your available cash without counting it as income.',
|
||||
),
|
||||
label: 'Show in cashflow chart as inflow',
|
||||
description:
|
||||
'Show this transfer category as money entering available cash.',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -43,69 +39,111 @@ interface CategoryCashflowDirectionFieldsProps {
|
|||
defaultValue?: CategoryCashflowDirection;
|
||||
}
|
||||
|
||||
function getTypeEffectDescription(selectedType: CategoryType | ''): string {
|
||||
switch (selectedType) {
|
||||
case 'income':
|
||||
return __(
|
||||
'Income categories count as cash inflow and appear in income charts. They do not appear in top spending categories.',
|
||||
);
|
||||
case 'expense':
|
||||
return __(
|
||||
'Expense categories count as cash outflow and appear in spending charts, including top spending categories.',
|
||||
);
|
||||
case 'transfer':
|
||||
return __(
|
||||
'Transfer categories are excluded from income, expenses, and top spending categories. Choose whether to show them in the cashflow chart.',
|
||||
);
|
||||
case 'savings':
|
||||
return __(
|
||||
'Savings categories appear as saved money at the top of cashflow and as cash outflow in the cashflow chart. They stay out of income, expenses, and top spending categories.',
|
||||
);
|
||||
case 'investment':
|
||||
return __(
|
||||
'Investment categories appear as invested money at the top of cashflow and as cash outflow in the cashflow chart. They stay out of income, expenses, and top spending categories.',
|
||||
);
|
||||
default:
|
||||
return __(
|
||||
'Choose a category type to see how it affects cashflow and charts.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function CategoryCashflowDirectionFields({
|
||||
selectedType,
|
||||
defaultValue = 'hidden',
|
||||
}: CategoryCashflowDirectionFieldsProps) {
|
||||
const [cashflowDirection, setCashflowDirection] =
|
||||
useState<CategoryCashflowDirection>(defaultValue);
|
||||
const selectedOption = cashflowDirectionOptions.find(
|
||||
const isTransfer = selectedType === 'transfer';
|
||||
const selectedOption = transferCashflowOptions.find(
|
||||
(option) => option.value === cashflowDirection,
|
||||
);
|
||||
|
||||
if (selectedType !== 'transfer') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3 rounded-lg border border-border/60 bg-muted/20 p-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="cashflow_direction">
|
||||
{__('Cashflow analytics')}
|
||||
{__('Cashflow and charts')}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{__(
|
||||
'Transfer categories stay out of income and expense totals, but you can still track them in the money flow chart.',
|
||||
)}
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||
{getTypeEffectDescription(selectedType)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
name="cashflow_direction"
|
||||
defaultValue={defaultValue}
|
||||
onValueChange={(value) =>
|
||||
setCashflowDirection(value as CategoryCashflowDirection)
|
||||
}
|
||||
required
|
||||
>
|
||||
<SelectTrigger id="cashflow_direction">
|
||||
<SelectValue
|
||||
placeholder={__('Choose how to report this transfer')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{cashflowDirectionOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{selectedOption && (
|
||||
<p className="px-1 text-xs leading-relaxed text-muted-foreground">
|
||||
{selectedOption.description}
|
||||
</p>
|
||||
{!isTransfer && (
|
||||
<input
|
||||
type="hidden"
|
||||
name="cashflow_direction"
|
||||
value={
|
||||
selectedType === 'savings' ||
|
||||
selectedType === 'investment'
|
||||
? 'outflow'
|
||||
: 'hidden'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Alert>
|
||||
<Info className="h-4 w-4 opacity-50" />
|
||||
<AlertDescription className="text-xs leading-relaxed">
|
||||
{__(
|
||||
'Tracked transfers appear in the money flow Sankey chart. They are not counted as income or expenses.',
|
||||
{isTransfer && (
|
||||
<>
|
||||
<Select
|
||||
name="cashflow_direction"
|
||||
value={cashflowDirection}
|
||||
onValueChange={(value) =>
|
||||
setCashflowDirection(
|
||||
value as CategoryCashflowDirection,
|
||||
)
|
||||
}
|
||||
required
|
||||
>
|
||||
<SelectTrigger
|
||||
id="cashflow_direction"
|
||||
data-testid="cashflow-direction-trigger"
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={__(
|
||||
'Choose cashflow chart visibility',
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{transferCashflowOptions.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{__(option.label)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{selectedOption && (
|
||||
<p className="px-1 text-xs leading-relaxed text-muted-foreground">
|
||||
{__(selectedOption.description)}
|
||||
</p>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { store } from '@/actions/App/Http/Controllers/Settings/CategoryController';
|
||||
import { CategoryCashflowDirectionFields } from '@/components/categories/category-cashflow-direction-fields';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CreateButton } from '@/components/ui/create-button';
|
||||
|
|
@ -32,7 +31,6 @@ import {
|
|||
import { __ } from '@/utils/i18n';
|
||||
import { Form } from '@inertiajs/react';
|
||||
import * as Icons from 'lucide-react';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function CreateCategoryDialog({
|
||||
|
|
@ -180,20 +178,11 @@ export function CreateCategoryDialog({
|
|||
{errors.type}
|
||||
</p>
|
||||
)}
|
||||
{selectedType === 'transfer' && (
|
||||
<Alert>
|
||||
<Info className="h-4 w-4 opacity-50" />
|
||||
<AlertDescription className="text-sm">
|
||||
{__(
|
||||
'Transactions in this category will\n not be counted in top expenses or\n income. Transfer categories are\n mainly used for transactions between\n accounts.',
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CategoryCashflowDirectionFields
|
||||
selectedType={selectedType}
|
||||
defaultValue="hidden"
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
|
|
@ -206,9 +195,7 @@ export function CreateCategoryDialog({
|
|||
{__('Cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={processing}>
|
||||
{processing
|
||||
? __('Creating...')
|
||||
: __('Create')}
|
||||
{processing ? __('Saving...') : __('Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { update } from '@/actions/App/Http/Controllers/Settings/CategoryController';
|
||||
import { CategoryCashflowDirectionFields } from '@/components/categories/category-cashflow-direction-fields';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
|
|
@ -31,7 +30,6 @@ import {
|
|||
import { __ } from '@/utils/i18n';
|
||||
import { Form } from '@inertiajs/react';
|
||||
import * as Icons from 'lucide-react';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface EditCategoryDialogProps {
|
||||
|
|
@ -193,16 +191,6 @@ export function EditCategoryDialog({
|
|||
{errors.type}
|
||||
</p>
|
||||
)}
|
||||
{selectedType === 'transfer' && (
|
||||
<Alert>
|
||||
<Info className="h-4 w-4 opacity-50" />
|
||||
<AlertDescription className="text-sm">
|
||||
{__(
|
||||
'Transactions in this category will\n not be counted in top expenses or\n income. Transfer categories are\n mainly used for transactions between\n accounts.',
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CategoryCashflowDirectionFields
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ it('can create a new category', function () {
|
|||
->wait(0.5)
|
||||
->click('//div[@role="option"][contains(., "Expense")]')
|
||||
->wait(0.3)
|
||||
->assertSee('Cashflow and charts')
|
||||
->assertSee('Expense categories count as cash outflow and appear in spending charts, including top spending categories')
|
||||
->click('button[type="submit"]')
|
||||
->wait(2)
|
||||
->assertSee('Entertainment')
|
||||
|
|
@ -105,6 +107,94 @@ it('can create a new category', function () {
|
|||
'name' => 'Entertainment',
|
||||
'color' => 'purple',
|
||||
'type' => 'expense',
|
||||
'cashflow_direction' => 'hidden',
|
||||
]);
|
||||
});
|
||||
|
||||
it('explains savings and investment category cashflow impact in the create dialog', function (string $type, string $expectedText) {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
|
||||
actingAs($user);
|
||||
|
||||
$page = visit('/settings/categories');
|
||||
|
||||
$page->assertSee('Categories settings')
|
||||
->click('Create Category')
|
||||
->wait(0.5)
|
||||
->click('Select a type')
|
||||
->wait(0.5)
|
||||
->click("//div[@role=\"option\"][contains(., \"{$type}\")]")
|
||||
->wait(0.3)
|
||||
->assertSee('Cashflow and charts')
|
||||
->assertSee($expectedText)
|
||||
->assertSee('stay out of income, expenses, and top spending categories')
|
||||
->assertNoJavascriptErrors();
|
||||
})->with([
|
||||
['Savings', 'Savings categories appear as saved money at the top of cashflow and as cash outflow in the cashflow chart'],
|
||||
['Investment', 'Investment categories appear as invested money at the top of cashflow and as cash outflow in the cashflow chart'],
|
||||
]);
|
||||
|
||||
it('shows locked income cashflow direction in the create dialog', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
|
||||
actingAs($user);
|
||||
|
||||
$page = visit('/settings/categories');
|
||||
|
||||
$page->assertSee('Categories settings')
|
||||
->click('Create Category')
|
||||
->wait(0.5)
|
||||
->assertSee('Cashflow and charts')
|
||||
->assertSee('Choose a category type to see how it affects cashflow and charts')
|
||||
->click('Select a type')
|
||||
->wait(0.5)
|
||||
->click('//div[@role="option"][contains(., "Income")]')
|
||||
->wait(0.3)
|
||||
->assertSee('Income categories count as cash inflow and appear in income charts')
|
||||
->assertSee('They do not appear in top spending categories')
|
||||
->assertNoJavascriptErrors();
|
||||
});
|
||||
|
||||
it('can create a new transfer category with a cashflow analytics direction', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
|
||||
actingAs($user);
|
||||
|
||||
$page = visit('/settings/categories');
|
||||
|
||||
$page->assertSee('Categories settings')
|
||||
->click('Create Category')
|
||||
->wait(0.5)
|
||||
->fill('name', 'Emergency Fund')
|
||||
->click('Select an icon')
|
||||
->wait(0.5)
|
||||
->click('//div[@role="option"][contains(., "PiggyBank")]')
|
||||
->wait(0.3)
|
||||
->click('Select a color')
|
||||
->wait(0.5)
|
||||
->click('//div[@role="option"][contains(., "lime")]')
|
||||
->wait(0.3)
|
||||
->click('Select a type')
|
||||
->wait(0.5)
|
||||
->click('//div[@role="option"][contains(., "Transfer")]')
|
||||
->wait(0.3)
|
||||
->assertSee('Cashflow and charts')
|
||||
->assertSee('Choose whether to show them in the cashflow chart')
|
||||
->click('Do not show in cashflow chart')
|
||||
->wait(0.5)
|
||||
->click('//div[@role="option"][contains(., "Show in cashflow chart as outflow")]')
|
||||
->wait(0.3)
|
||||
->click('Save')
|
||||
->wait(2)
|
||||
->assertSee('Emergency Fund')
|
||||
->assertNoJavascriptErrors();
|
||||
|
||||
$this->assertDatabaseHas('categories', [
|
||||
'user_id' => $user->id,
|
||||
'name' => 'Emergency Fund',
|
||||
'color' => 'lime',
|
||||
'type' => 'transfer',
|
||||
'cashflow_direction' => 'outflow',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -190,8 +280,8 @@ it('shows transfer type description when transfer type is selected in create dia
|
|||
->wait(0.5)
|
||||
->click('//div[@role="option"][contains(., "Transfer")]')
|
||||
->wait(0.5)
|
||||
->assertSee('Transactions in this category will not be counted in top expenses or income')
|
||||
->assertSee('Transfer categories are mainly used for transactions between accounts')
|
||||
->assertSee('Transfer categories are excluded from income, expenses, and top spending categories')
|
||||
->assertSee('Choose whether to show them in the cashflow chart')
|
||||
->assertNoJavascriptErrors();
|
||||
});
|
||||
|
||||
|
|
@ -219,7 +309,7 @@ it('shows transfer type description when transfer type is selected in edit dialo
|
|||
->wait(0.5)
|
||||
->click('//div[@role="option"][contains(., "Transfer")]')
|
||||
->wait(0.5)
|
||||
->assertSee('Transactions in this category will not be counted in top expenses or income')
|
||||
->assertSee('Transfer categories are mainly used for transactions between accounts')
|
||||
->assertSee('Transfer categories are excluded from income, expenses, and top spending categories')
|
||||
->assertSee('Choose whether to show them in the cashflow chart')
|
||||
->assertNoJavascriptErrors();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1095,6 +1095,73 @@ test('sankey includes outflow transfer categories on the expense side', function
|
|||
expect(collect($data['income_categories'])->pluck('category.name'))->not->toContain('Investments');
|
||||
});
|
||||
|
||||
test('sankey includes savings and investment categories on the expense side', function () {
|
||||
$incomeCategory = Category::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'type' => CategoryType::Income,
|
||||
'name' => 'Salary',
|
||||
]);
|
||||
$savingsCategory = Category::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'type' => CategoryType::Savings,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Hidden,
|
||||
'name' => 'Emergency Savings',
|
||||
]);
|
||||
$investmentCategory = Category::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'type' => CategoryType::Investment,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Hidden,
|
||||
'name' => 'Brokerage',
|
||||
]);
|
||||
|
||||
$account = Account::factory()->create(['user_id' => $this->user->id]);
|
||||
|
||||
Transaction::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'account_id' => $account->id,
|
||||
'category_id' => $incomeCategory->id,
|
||||
'amount' => 300000,
|
||||
'transaction_date' => now(),
|
||||
]);
|
||||
Transaction::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'account_id' => $account->id,
|
||||
'category_id' => $savingsCategory->id,
|
||||
'amount' => -40000,
|
||||
'transaction_date' => now(),
|
||||
]);
|
||||
Transaction::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'account_id' => $account->id,
|
||||
'category_id' => $investmentCategory->id,
|
||||
'amount' => -60000,
|
||||
'transaction_date' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/cashflow/sankey?'.http_build_query([
|
||||
'from' => now()->startOfMonth()->toDateString(),
|
||||
'to' => now()->endOfMonth()->toDateString(),
|
||||
]));
|
||||
|
||||
$response->assertOk();
|
||||
$data = $response->json();
|
||||
|
||||
expect($data['total_income'])->toBe(300000);
|
||||
expect($data['total_expense'])->toBe(100000);
|
||||
|
||||
$savingsExpense = collect($data['expense_categories'])->firstWhere('category.name', 'Emergency Savings');
|
||||
expect($savingsExpense)->not->toBeNull();
|
||||
expect($savingsExpense['amount'])->toBe(40000);
|
||||
|
||||
$investmentExpense = collect($data['expense_categories'])->firstWhere('category.name', 'Brokerage');
|
||||
expect($investmentExpense)->not->toBeNull();
|
||||
expect($investmentExpense['amount'])->toBe(60000);
|
||||
|
||||
expect(collect($data['income_categories'])->pluck('category.name'))
|
||||
->not->toContain('Emergency Savings')
|
||||
->not->toContain('Brokerage');
|
||||
});
|
||||
|
||||
test('sankey includes inflow transfer categories on the income side', function () {
|
||||
$expenseCategory = Category::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ test('command resets categories for a user by ID', function () {
|
|||
|
||||
expect($user->categories()->where('name', 'Investments')->first())
|
||||
->type->toBe(CategoryType::Investment)
|
||||
->cashflow_direction->toBe(CategoryCashflowDirection::Hidden);
|
||||
->cashflow_direction->toBe(CategoryCashflowDirection::Outflow);
|
||||
expect($user->categories()->where('name', 'From account of relatives')->first())
|
||||
->type->toBe(CategoryType::Transfer)
|
||||
->cashflow_direction->toBe(CategoryCashflowDirection::Inflow);
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ test('users can create savings and investment categories', function (string $typ
|
|||
'user_id' => $user->id,
|
||||
'name' => ucfirst($type),
|
||||
'type' => $type,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Hidden->value,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Outflow->value,
|
||||
]);
|
||||
})->with([
|
||||
CategoryType::Savings->value,
|
||||
|
|
@ -431,6 +431,44 @@ test('migration updates existing default saving and investment categories', func
|
|||
->cashflow_direction->toBe(CategoryCashflowDirection::Outflow);
|
||||
});
|
||||
|
||||
test('migration sets saving and investment categories to cashflow outflow', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$savings = Category::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'type' => CategoryType::Savings,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Hidden,
|
||||
]);
|
||||
$investment = Category::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'type' => CategoryType::Investment,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Hidden,
|
||||
]);
|
||||
$transfer = Category::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'type' => CategoryType::Transfer,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Inflow,
|
||||
]);
|
||||
$expense = Category::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'type' => CategoryType::Expense,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Hidden,
|
||||
]);
|
||||
|
||||
$migration = require database_path('migrations/2026_05_29_085835_update_saving_and_investment_category_cashflow_direction.php');
|
||||
$migration->up();
|
||||
|
||||
expect($savings->refresh()->cashflow_direction)->toBe(CategoryCashflowDirection::Outflow);
|
||||
expect($investment->refresh()->cashflow_direction)->toBe(CategoryCashflowDirection::Outflow);
|
||||
expect($transfer->refresh()->cashflow_direction)->toBe(CategoryCashflowDirection::Inflow);
|
||||
expect($expense->refresh()->cashflow_direction)->toBe(CategoryCashflowDirection::Hidden);
|
||||
|
||||
$migration->down();
|
||||
|
||||
expect($savings->refresh()->cashflow_direction)->toBe(CategoryCashflowDirection::Hidden);
|
||||
expect($investment->refresh()->cashflow_direction)->toBe(CategoryCashflowDirection::Hidden);
|
||||
});
|
||||
|
||||
test('users cannot update categories they do not own', function () {
|
||||
$user = User::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
|
|
@ -500,13 +538,13 @@ test('default categories are created when user registers', function () {
|
|||
|
||||
expect($user->categories()->firstWhere('name', 'Investments'))
|
||||
->type->toBe(CategoryType::Investment)
|
||||
->cashflow_direction->toBe(CategoryCashflowDirection::Hidden);
|
||||
->cashflow_direction->toBe(CategoryCashflowDirection::Outflow);
|
||||
expect($user->categories()->firstWhere('name', 'Savings'))
|
||||
->type->toBe(CategoryType::Savings)
|
||||
->cashflow_direction->toBe(CategoryCashflowDirection::Hidden);
|
||||
->cashflow_direction->toBe(CategoryCashflowDirection::Outflow);
|
||||
expect($user->categories()->firstWhere('name', 'Other investments'))
|
||||
->type->toBe(CategoryType::Investment)
|
||||
->cashflow_direction->toBe(CategoryCashflowDirection::Hidden);
|
||||
->cashflow_direction->toBe(CategoryCashflowDirection::Outflow);
|
||||
expect($user->categories()->firstWhere('name', 'From account of relatives'))
|
||||
->type->toBe(CategoryType::Transfer)
|
||||
->cashflow_direction->toBe(CategoryCashflowDirection::Inflow);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ test('accounts index page does not exceed query threshold', function () {
|
|||
test('account show page does not exceed query threshold', function () {
|
||||
$account = $this->user->accounts()->first();
|
||||
|
||||
assertMaxQueries(17, function () use ($account) {
|
||||
assertMaxQueries(18, function () use ($account) {
|
||||
$this->get(route('accounts.show', $account))->assertOk();
|
||||
}, 'Account Show');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue