mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-19 19:24:03 +00:00
Duplicate expense
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\V1\Admin\Expense;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\DuplicateExpenseRequest;
|
||||||
|
use App\Http\Resources\ExpenseResource;
|
||||||
|
use App\Models\CompanySetting;
|
||||||
|
use App\Models\ExchangeRateLog;
|
||||||
|
use App\Models\Expense;
|
||||||
|
|
||||||
|
class DuplicateExpenseController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Duplicate an expense, appending " (copy)" to the note (description).
|
||||||
|
*/
|
||||||
|
public function __invoke(DuplicateExpenseRequest $request, Expense $expense): ExpenseResource
|
||||||
|
{
|
||||||
|
$this->authorize('view', $expense);
|
||||||
|
$this->authorize('create', Expense::class);
|
||||||
|
|
||||||
|
$expense->load('fields');
|
||||||
|
|
||||||
|
$companyCurrency = CompanySetting::getSetting('currency', $request->header('company'));
|
||||||
|
$currentCurrency = $expense->currency_id;
|
||||||
|
$exchangeRate = $companyCurrency != $currentCurrency ? $expense->exchange_rate : 1;
|
||||||
|
|
||||||
|
$notes = trim((string) $expense->notes);
|
||||||
|
$duplicatedNotes = $notes === '' ? '(copy)' : $notes.' (copy)';
|
||||||
|
|
||||||
|
$newExpense = Expense::query()->create([
|
||||||
|
'expense_date' => $request->validated('expense_date'),
|
||||||
|
'expense_number' => null,
|
||||||
|
'expense_category_id' => $expense->expense_category_id,
|
||||||
|
'payment_method_id' => $expense->payment_method_id,
|
||||||
|
'amount' => $expense->amount,
|
||||||
|
'customer_id' => $expense->customer_id,
|
||||||
|
'notes' => $duplicatedNotes,
|
||||||
|
'currency_id' => $expense->currency_id,
|
||||||
|
'creator_id' => $request->user()->id,
|
||||||
|
'company_id' => $request->header('company'),
|
||||||
|
'exchange_rate' => $exchangeRate,
|
||||||
|
'base_amount' => $expense->amount * $exchangeRate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ((string) $newExpense->currency_id !== (string) $companyCurrency) {
|
||||||
|
ExchangeRateLog::addExchangeRateLog($newExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expense->fields()->exists()) {
|
||||||
|
$customFields = [];
|
||||||
|
|
||||||
|
foreach ($expense->fields as $data) {
|
||||||
|
$customFields[] = [
|
||||||
|
'id' => $data->custom_field_id,
|
||||||
|
'value' => $data->defaultAnswer,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$newExpense->addCustomFields($customFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ExpenseResource($newExpense);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Http/Requests/DuplicateExpenseRequest.php
Normal file
31
app/Http/Requests/DuplicateExpenseRequest.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class DuplicateExpenseRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, array<int, string>>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'expense_date' => [
|
||||||
|
'required',
|
||||||
|
'date_format:Y-m-d',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -677,6 +677,10 @@
|
|||||||
"no_expenses": "No expenses yet!",
|
"no_expenses": "No expenses yet!",
|
||||||
"list_of_expenses": "This section will contain the list of expenses.",
|
"list_of_expenses": "This section will contain the list of expenses.",
|
||||||
"confirm_delete": "You will not be able to recover this Expense | You will not be able to recover these Expenses",
|
"confirm_delete": "You will not be able to recover this Expense | You will not be able to recover these Expenses",
|
||||||
|
"duplicate_expense": "Duplicate",
|
||||||
|
"duplicate_expense_title": "Duplicate expense",
|
||||||
|
"duplicate_expense_modal_hint": "Change the date if you need to. (copy) is added to the note.",
|
||||||
|
"duplicated_message": "Expense duplicated successfully",
|
||||||
"created_message": "Expense created successfully",
|
"created_message": "Expense created successfully",
|
||||||
"updated_message": "Expense updated successfully",
|
"updated_message": "Expense updated successfully",
|
||||||
"deleted_message": "Expense deleted successfully | Expenses deleted successfully",
|
"deleted_message": "Expense deleted successfully | Expenses deleted successfully",
|
||||||
|
|||||||
@@ -21,6 +21,18 @@
|
|||||||
</BaseDropdownItem>
|
</BaseDropdownItem>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<!-- duplicate expense -->
|
||||||
|
<BaseDropdownItem
|
||||||
|
v-if="userStore.hasAbilities(abilities.CREATE_EXPENSE)"
|
||||||
|
@click="onDuplicateExpense(row)"
|
||||||
|
>
|
||||||
|
<BaseIcon
|
||||||
|
name="DocumentDuplicateIcon"
|
||||||
|
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||||
|
/>
|
||||||
|
{{ $t('expenses.duplicate_expense') }}
|
||||||
|
</BaseDropdownItem>
|
||||||
|
|
||||||
<!-- delete expense -->
|
<!-- delete expense -->
|
||||||
<BaseDropdownItem
|
<BaseDropdownItem
|
||||||
v-if="userStore.hasAbilities(abilities.DELETE_EXPENSE)"
|
v-if="userStore.hasAbilities(abilities.DELETE_EXPENSE)"
|
||||||
@@ -37,10 +49,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
import { useModalStore } from '@/scripts/stores/modal'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useExpenseStore } from '@/scripts/admin/stores/expense'
|
import { useExpenseStore } from '@/scripts/admin/stores/expense'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { inject } from 'vue'
|
import { inject } from 'vue'
|
||||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||||
import abilities from '@/scripts/admin/stub/abilities'
|
import abilities from '@/scripts/admin/stub/abilities'
|
||||||
@@ -61,15 +73,24 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
const notificationStore = useNotificationStore()
|
const modalStore = useModalStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const expenseStore = useExpenseStore()
|
const expenseStore = useExpenseStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const $utils = inject('utils')
|
const $utils = inject('utils')
|
||||||
|
|
||||||
|
function onDuplicateExpense(row) {
|
||||||
|
modalStore.openModal({
|
||||||
|
title: t('expenses.duplicate_expense_title'),
|
||||||
|
componentName: 'DuplicateExpenseModal',
|
||||||
|
data: row,
|
||||||
|
size: 'sm',
|
||||||
|
refreshData: props.loadData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function removeExpense(id) {
|
function removeExpense(id) {
|
||||||
dialogStore
|
dialogStore
|
||||||
.openDialog({
|
.openDialog({
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<BaseModal
|
||||||
|
:show="modalActive"
|
||||||
|
:initial-focus="initialFocusRef"
|
||||||
|
@close="closeModal"
|
||||||
|
@open="onModalOpen"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-between w-full">
|
||||||
|
{{ modalStore.title }}
|
||||||
|
<BaseIcon
|
||||||
|
name="XMarkIcon"
|
||||||
|
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||||
|
@click="closeModal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<form action="" @submit.prevent="submitDuplicate">
|
||||||
|
<div
|
||||||
|
ref="initialFocusRef"
|
||||||
|
class="sr-only outline-none focus:outline-none"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="px-8 py-6 sm:p-6">
|
||||||
|
<p class="mb-6 text-sm text-gray-600">
|
||||||
|
{{ $t('expenses.duplicate_expense_modal_hint') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<BaseInputGroup
|
||||||
|
:label="$t('expenses.expense_date')"
|
||||||
|
variant="vertical"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<BaseDatePicker
|
||||||
|
v-model="selectedExpenseDate"
|
||||||
|
:calendar-button="true"
|
||||||
|
calendar-button-icon="calendar"
|
||||||
|
/>
|
||||||
|
</BaseInputGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
z-0
|
||||||
|
flex
|
||||||
|
justify-end
|
||||||
|
px-4
|
||||||
|
py-4
|
||||||
|
border-t border-gray-200 border-solid
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<BaseButton
|
||||||
|
class="mr-2"
|
||||||
|
variant="primary-outline"
|
||||||
|
type="button"
|
||||||
|
@click="closeModal"
|
||||||
|
>
|
||||||
|
{{ $t('general.cancel') }}
|
||||||
|
</BaseButton>
|
||||||
|
|
||||||
|
<BaseButton
|
||||||
|
:loading="isDuplicating"
|
||||||
|
:disabled="isDuplicating || !selectedExpenseDate"
|
||||||
|
variant="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<template #left="slotProps">
|
||||||
|
<BaseIcon
|
||||||
|
v-if="!isDuplicating"
|
||||||
|
name="DocumentDuplicateIcon"
|
||||||
|
:class="slotProps.class"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
{{ $t('expenses.duplicate_expense') }}
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</BaseModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import moment from 'moment'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useModalStore } from '@/scripts/stores/modal'
|
||||||
|
import { useExpenseStore } from '@/scripts/admin/stores/expense'
|
||||||
|
|
||||||
|
const modalStore = useModalStore()
|
||||||
|
const expenseStore = useExpenseStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const selectedExpenseDate = ref('')
|
||||||
|
const isDuplicating = ref(false)
|
||||||
|
const initialFocusRef = ref(null)
|
||||||
|
|
||||||
|
const modalActive = computed(
|
||||||
|
() => modalStore.active && modalStore.componentName === 'DuplicateExpenseModal'
|
||||||
|
)
|
||||||
|
|
||||||
|
function toYmd(value) {
|
||||||
|
if (!value) {
|
||||||
|
return moment().format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
|
||||||
|
const str = String(value)
|
||||||
|
|
||||||
|
if (str.length >= 10 && /^\d{4}-\d{2}-\d{2}/.test(str)) {
|
||||||
|
return str.slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
return moment(value).format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onModalOpen() {
|
||||||
|
selectedExpenseDate.value = toYmd(modalStore.data?.expense_date)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDuplicate() {
|
||||||
|
if (!modalStore.data?.id || !selectedExpenseDate.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isDuplicating.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expenseStore.duplicateExpense({
|
||||||
|
id: modalStore.data.id,
|
||||||
|
expense_date: selectedExpenseDate.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
modalStore.refreshData && modalStore.refreshData()
|
||||||
|
closeModal()
|
||||||
|
router.push('/admin/expenses')
|
||||||
|
} finally {
|
||||||
|
isDuplicating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
modalStore.closeModal()
|
||||||
|
selectedExpenseDate.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
21
resources/scripts/admin/stores/expense.js
vendored
21
resources/scripts/admin/stores/expense.js
vendored
@@ -91,6 +91,27 @@ export const useExpenseStore = (useWindow = false) => {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
duplicateExpense({ id, expense_date }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
http
|
||||||
|
.post(`/api/v1/expenses/${id}/duplicate`, { expense_date })
|
||||||
|
.then((response) => {
|
||||||
|
const notificationStore = useNotificationStore()
|
||||||
|
|
||||||
|
notificationStore.showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: global.t('expenses.duplicated_message'),
|
||||||
|
})
|
||||||
|
|
||||||
|
resolve(response)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
handleError(err)
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
addExpense(data) {
|
addExpense(data) {
|
||||||
const formData = utils.toFormData(data)
|
const formData = utils.toFormData(data)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasePage>
|
<BasePage>
|
||||||
|
<DuplicateExpenseModal />
|
||||||
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<BasePageHeader :title="$t('expenses.title')">
|
<BasePageHeader :title="$t('expenses.title')">
|
||||||
<BaseBreadcrumb>
|
<BaseBreadcrumb>
|
||||||
@@ -231,6 +233,7 @@ import { useUserStore } from '@/scripts/admin/stores/user'
|
|||||||
import abilities from '@/scripts/admin/stub/abilities'
|
import abilities from '@/scripts/admin/stub/abilities'
|
||||||
|
|
||||||
import UFOIcon from '@/scripts/components/icons/empty/UFOIcon.vue'
|
import UFOIcon from '@/scripts/components/icons/empty/UFOIcon.vue'
|
||||||
|
import DuplicateExpenseModal from '@/scripts/admin/components/modal-components/DuplicateExpenseModal.vue'
|
||||||
import ExpenseDropdown from '@/scripts/admin/components/dropdowns/ExpenseIndexDropdown.vue'
|
import ExpenseDropdown from '@/scripts/admin/components/dropdowns/ExpenseIndexDropdown.vue'
|
||||||
|
|
||||||
const companyStore = useCompanyStore()
|
const companyStore = useCompanyStore()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
static
|
static
|
||||||
class="fixed inset-0 z-20 overflow-y-auto"
|
class="fixed inset-0 z-20 overflow-y-auto"
|
||||||
:open="show"
|
:open="show"
|
||||||
|
:initial-focus="initialFocus"
|
||||||
@close="$emit('close')"
|
@close="$emit('close')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -106,6 +107,14 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Optional ref (template ref) for the element that should receive focus when the dialog opens.
|
||||||
|
* When omitted, Headless UI focuses the first focusable control (often an input).
|
||||||
|
*/
|
||||||
|
initialFocus: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use App\Http\Controllers\V1\Admin\ExchangeRate\GetActiveProviderController;
|
|||||||
use App\Http\Controllers\V1\Admin\ExchangeRate\GetExchangeRateController;
|
use App\Http\Controllers\V1\Admin\ExchangeRate\GetExchangeRateController;
|
||||||
use App\Http\Controllers\V1\Admin\ExchangeRate\GetSupportedCurrenciesController;
|
use App\Http\Controllers\V1\Admin\ExchangeRate\GetSupportedCurrenciesController;
|
||||||
use App\Http\Controllers\V1\Admin\ExchangeRate\GetUsedCurrenciesController;
|
use App\Http\Controllers\V1\Admin\ExchangeRate\GetUsedCurrenciesController;
|
||||||
|
use App\Http\Controllers\V1\Admin\Expense\DuplicateExpenseController;
|
||||||
use App\Http\Controllers\V1\Admin\Expense\ExpenseCategoriesController;
|
use App\Http\Controllers\V1\Admin\Expense\ExpenseCategoriesController;
|
||||||
use App\Http\Controllers\V1\Admin\Expense\ExpensesController;
|
use App\Http\Controllers\V1\Admin\Expense\ExpensesController;
|
||||||
use App\Http\Controllers\V1\Admin\Expense\ShowReceiptController;
|
use App\Http\Controllers\V1\Admin\Expense\ShowReceiptController;
|
||||||
@@ -312,6 +313,8 @@ Route::prefix('/v1')->group(function () {
|
|||||||
|
|
||||||
Route::post('/expenses/delete', [ExpensesController::class, 'delete']);
|
Route::post('/expenses/delete', [ExpensesController::class, 'delete']);
|
||||||
|
|
||||||
|
Route::post('/expenses/{expense}/duplicate', DuplicateExpenseController::class);
|
||||||
|
|
||||||
Route::apiResource('expenses', ExpensesController::class);
|
Route::apiResource('expenses', ExpensesController::class);
|
||||||
|
|
||||||
Route::apiResource('categories', ExpenseCategoriesController::class);
|
Route::apiResource('categories', ExpenseCategoriesController::class);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\V1\Admin\Expense\DuplicateExpenseController;
|
||||||
use App\Http\Controllers\V1\Admin\Expense\ExpensesController;
|
use App\Http\Controllers\V1\Admin\Expense\ExpensesController;
|
||||||
|
use App\Http\Requests\DuplicateExpenseRequest;
|
||||||
use App\Http\Requests\ExpenseRequest;
|
use App\Http\Requests\ExpenseRequest;
|
||||||
use App\Models\Expense;
|
use App\Models\Expense;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@@ -114,6 +116,79 @@ test('search expenses', function () {
|
|||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('duplicate expense', function () {
|
||||||
|
$expense = Expense::factory()->create([
|
||||||
|
'expense_date' => '2019-02-05',
|
||||||
|
'notes' => 'Monthly rent',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = postJson("api/v1/expenses/{$expense->id}/duplicate", [
|
||||||
|
'expense_date' => '2019-02-05',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201);
|
||||||
|
|
||||||
|
$newId = $response->json('data.id');
|
||||||
|
|
||||||
|
expect($newId)->not->toBe($expense->id);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('expenses', [
|
||||||
|
'id' => $newId,
|
||||||
|
'expense_date' => '2019-02-05',
|
||||||
|
'notes' => 'Monthly rent (copy)',
|
||||||
|
'expense_category_id' => $expense->expense_category_id,
|
||||||
|
'amount' => $expense->amount,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('duplicate expense with empty note uses copy as note', function () {
|
||||||
|
$expense = Expense::factory()->create([
|
||||||
|
'expense_date' => '2019-02-05',
|
||||||
|
'notes' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
postJson("api/v1/expenses/{$expense->id}/duplicate", [
|
||||||
|
'expense_date' => '2019-02-05',
|
||||||
|
])
|
||||||
|
->assertStatus(201)
|
||||||
|
->assertJsonPath('data.notes', '(copy)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('duplicate expense uses submitted expense date', function () {
|
||||||
|
$expense = Expense::factory()->create([
|
||||||
|
'expense_date' => '2019-02-05',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = postJson("api/v1/expenses/{$expense->id}/duplicate", [
|
||||||
|
'expense_date' => '2024-03-10',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('expenses', [
|
||||||
|
'id' => $response->json('data.id'),
|
||||||
|
'expense_date' => '2024-03-10',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('duplicate expense requires expense date', function () {
|
||||||
|
$expense = Expense::factory()->create([
|
||||||
|
'expense_date' => '2019-02-05',
|
||||||
|
]);
|
||||||
|
|
||||||
|
postJson("api/v1/expenses/{$expense->id}/duplicate", [])
|
||||||
|
->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors(['expense_date']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('duplicate validates using a form request', function () {
|
||||||
|
$this->assertActionUsesFormRequest(
|
||||||
|
DuplicateExpenseController::class,
|
||||||
|
'__invoke',
|
||||||
|
DuplicateExpenseRequest::class
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('delete multiple expenses', function () {
|
test('delete multiple expenses', function () {
|
||||||
$expenses = Expense::factory()->count(3)->create([
|
$expenses = Expense::factory()->count(3)->create([
|
||||||
'expense_date' => '2019-02-05',
|
'expense_date' => '2019-02-05',
|
||||||
|
|||||||
Reference in New Issue
Block a user