Direct payments for invoices

Events can be moved to archive and moved back
Fixed validation
This commit is contained in:
2026-05-12 16:04:15 +02:00
parent e2fb616565
commit 0cf9602958
42 changed files with 851 additions and 132 deletions
@@ -42,6 +42,7 @@ class CreateInvoiceCommand {
'travel_reason' => $this->request->travelReason,
'passengers' => $this->request->passengers,
'transportation' => $this->request->transportations,
'payment_purpose' => $this->request->paymentPurpose,
'document_filename' => $this->request->receiptFile !== null ? $this->request->receiptFile->fullPath : null,
]);
@@ -23,6 +23,7 @@ class CreateInvoiceRequest {
public bool $isDonation;
public ?int $userId;
public ?string $travelReason;
public ?string $paymentPurpose;
public function __construct(
@@ -43,6 +44,7 @@ class CreateInvoiceRequest {
?int $passengers = null,
?int $transportations,
?string $travelReason = null,
?string $paymentPurpose = null,
) {
$this->costUnit = $costUnit;
@@ -62,6 +64,7 @@ class CreateInvoiceRequest {
$this->isDonation = $isDonation;
$this->userId = $userId;
$this->travelReason = $travelReason;
$this->paymentPurpose = $paymentPurpose;
if ($accountIban === 'undefined') {
$this->accountIban = null;
@@ -34,102 +34,4 @@ class NewInvoiceController extends CommonController {
]);
return $inertiaProvider->render();
}
public function saveInvoice(Request $request, int $costUnitId, string $invoiceType) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId, true);
if (null === $costUnit) {
return response()->json([
'status' => 'error',
'message' => 'Beim Speichern ist ein Fehler aufgetreten. Bitte starte den Vorgang erneut.'
]);
}
$uploadedFile = null;
if (null !== $request->file('receipt')) {
$validation = sprintf('%1$s|%2$s|max:%3$s',
'required',
'mimes:pdf',
env('MAX_INVOICE_FILE_SIZE', 16)*10
);
$request->validate([
'receipt' => $validation
]);
$uploadFileProvider = new UploadFileProvider($request->file('receipt'), $costUnit);
$uploadedFile = $uploadFileProvider->saveUploadedFile();
}
switch ($invoiceType) {
case InvoiceType::INVOICE_TYPE_TRAVELLING:
if ($uploadedFile !== null) {
$amount = Amount::fromString($request->input('amount'))->getAmount();
$distance = null;
} else {
$distance = Amount::fromString($request->input('amount'))->getRoundedAmount();
$amount = $distance * $costUnit->distance_allowance;
}
$createInvoiceRequest = new CreateInvoiceRequest(
$costUnit,
$request->input('name'),
InvoiceType::INVOICE_TYPE_TRAVELLING,
$amount,
$uploadedFile,
'donation' === $request->input('decision') ? true : false,
$this->users->getCurrentUserDetails()['userId'],
$request->input('email'),
$request->input('telephone'),
$request->input('accountOwner'),
$request->input('accountIban'),
null,
$request->input('otherText'),
$distance,
$request->input('havePassengers'),
$request->input('materialTransportation'),
$request->input('travelReason'),
);
break;
default:
$createInvoiceRequest = new CreateInvoiceRequest(
$costUnit,
$request->input('name'),
$invoiceType,
Amount::fromString($request->input('amount'))->getAmount(),
$uploadedFile,
'donation' === $request->input('decision') ? true : false,
$this->users->getCurrentUserDetails()['userId'],
$request->input('email'),
$request->input('telephone'),
$request->input('accountOwner'),
$request->input('accountIban'),
$request->input('otherText'),
null,
null,
$request->input('havePassengers'),
$request->input('materialTransportation'),
null
);
break;
}
$command = new CreateInvoiceCommand($createInvoiceRequest);
$response = $command->execute();
if ($response->success) {
new FlashMessageProvider(
'Die Abrechnung wurde erfolgreich angelegt.' . PHP_EOL . PHP_EOL . 'Sollten wir Rückfragen haben, melden wir uns bei dir',
'success'
);
return response()->json([
'status' => 'success',
'message' => 'Alright'
]);
}
}
}
@@ -0,0 +1,128 @@
<?php
namespace App\Domains\Invoice\Controllers;
use App\Domains\Invoice\Actions\CreateInvoice\CreateInvoiceCommand;
use App\Domains\Invoice\Actions\CreateInvoice\CreateInvoiceRequest;
use App\Enumerations\InvoiceType;
use App\Providers\FlashMessageProvider;
use App\Providers\UploadFileProvider;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class SaveInvoiceController extends CommonController
{
public function __invoke(Request $request, int $costUnitId, string $invoiceType) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId, true);
$paymentPurpose = $request->input('paymentPurpose') ?? null;
if (null === $costUnit) {
return response()->json([
'status' => 'error',
'message' => 'Beim Speichern ist ein Fehler aufgetreten. Bitte starte den Vorgang erneut.'
]);
}
$uploadedFile = null;
if (null !== $request->file('receipt')) {
$maxFileSize = env('MAX_INVOICE_FILE_SIZE', 16);
$validation = sprintf('%1$s|%2$s|max:%3$s',
'required',
'mimes:pdf',
$maxFileSize * 1024
);
$validator = Validator::make($request->all(), [
'receipt' => $validation
]);
if ($validator->fails()) {
return response()->json([
'status' => 'error',
'message' => sprintf(
'Der Beleg konnte nicht hochgeladen werden. Bitte beachte die Maximale Dateigröße von %1$s MB sowie die Tatsache, dass ausschließlich PDF-Dateien akzeptiert werden.',
$maxFileSize
)
]);
}
$uploadFileProvider = new UploadFileProvider($request->file('receipt'), $costUnit);
$uploadedFile = $uploadFileProvider->saveUploadedFile();
}
switch ($invoiceType) {
case InvoiceType::INVOICE_TYPE_TRAVELLING:
if ($uploadedFile !== null) {
$amount = Amount::fromString($request->input('amount'))->getAmount();
$distance = null;
} else {
$distance = Amount::fromString($request->input('amount'))->getRoundedAmount();
$amount = $distance * $costUnit->distance_allowance;
}
$createInvoiceRequest = new CreateInvoiceRequest(
$costUnit,
$request->input('name'),
InvoiceType::INVOICE_TYPE_TRAVELLING,
$amount,
$uploadedFile,
'donation' === $request->input('decision') ? true : false,
$this->users->getCurrentUserDetails()['userId'],
$request->input('email'),
$request->input('telephone'),
$request->input('accountOwner'),
$request->input('accountIban'),
null,
$request->input('otherText'),
$distance,
$request->input('havePassengers'),
$request->input('materialTransportation'),
$request->input('travelReason'),
);
break;
default:
$createInvoiceRequest = new CreateInvoiceRequest(
$costUnit,
$request->input('name'),
$invoiceType,
Amount::fromString($request->input('amount'))->getAmount(),
$uploadedFile,
'donation' === $request->input('decision') ? true : false,
$this->users->getCurrentUserDetails()['userId'],
$request->input('email'),
$request->input('telephone'),
$request->input('accountOwner'),
$request->input('accountIban'),
$request->input('otherText'),
null,
null,
$request->input('havePassengers'),
$request->input('materialTransportation'),
null,
$paymentPurpose,
);
break;
}
$command = new CreateInvoiceCommand($createInvoiceRequest);
$response = $command->execute();
if ($response->success) {
new FlashMessageProvider(
'Die Abrechnung wurde erfolgreich angelegt.' . PHP_EOL . PHP_EOL . 'Sollten wir Rückfragen haben, melden wir uns bei dir',
'success'
);
return response()->json([
'status' => 'success',
'message' => 'Alright'
]);
}
}
}
+2 -1
View File
@@ -6,13 +6,14 @@ use App\Domains\Invoice\Controllers\ChangeStateController;
use App\Domains\Invoice\Controllers\EditController;
use App\Domains\Invoice\Controllers\ListMyInvoicesController;
use App\Domains\Invoice\Controllers\NewInvoiceController;
use App\Domains\Invoice\Controllers\SaveInvoiceController;
use App\Domains\Invoice\Controllers\ShowInvoiceController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('api/v1/invoice')->group(function () {
Route::post('/new/{costUnitId}/{invoiceType}', [NewInvoiceController::class, 'saveInvoice']);
Route::post('/new/{costUnitId}/{invoiceType}', SaveInvoiceController::class);
Route::middleware(['auth'])->group(function () {
Route::get('/details/{invoiceId}', ShowInvoiceController::class);
Route::get('/showReceipt/{invoiceId}', [ShowInvoiceController::class, 'showReceipt']);
+20 -1
View File
@@ -5,6 +5,7 @@ import Modal from "../../../Views/Components/Modal.vue";
import {onMounted, reactive, ref} from "vue";
import ExpenseAccounting from "./Partials/newInvoice/expense-accounting.vue";
import TravelExpenseAccounting from "./Partials/newInvoice/travel-expense-accounting.vue";
import DirectPayment from "./Partials/newInvoice/direct-payment.vue";
const props = defineProps({
currentEvents: Object,
@@ -42,6 +43,24 @@ const invoiceType = ref('');
</div>
</div>
<div
v-if="eventId === 0"
class="invoice-type-layer" style="width: calc(100% - 60px); margin: auto"
@click="isOpen = true;invoiceType='direct-payment'"
>
<TextResource textName="NEW_DIRECT_PAYMENT" />
</div>
<DirectPayment v-if="invoiceType === 'direct-payment' && eventId !== 0"
:eventId="eventId"
:userName="props.userName"
:userEmail="props.userEmail"
:userTelephone="props.userTelephone"
:userAccountIban="props.userAccountIban"
:userAccountOwner="props.userAccountOwner"
:userId="props.userId"
/>
<ExpenseAccounting v-if="invoiceType === 'expense-accounting' && eventId !== 0"
:eventId="eventId"
:userName="props.userName"
@@ -62,7 +81,7 @@ const invoiceType = ref('');
:userId="props.userId"
/>
<Modal :show="isOpen" title="Veranstaltung auswählen" @close="isOpen = false">
<Modal :show="isOpen" title="Veranstaltung auswählen" @close="isOpen = false" width="450px">
<select v-model="eventId" @change="isOpen=false" style="width: 100%">
<option value="0" disabled>Bitte auswählen</option>
<optgroup label="Laufende Tätigkeiten">
@@ -97,7 +97,7 @@ const emit = defineEmits(["accept", "deny", "fix", "reopen"])
<td>{{props.data.accountIban}}</td>
<td>Buchungsinformationen:</td>
<td v-if="props.data.donation">Als Spende gebucht</td>
<td v-else-if="props.data.alreadyPaid">Beleg ohne Auszahlung</td>
<td v-else-if="props.data.externalPayment">Rechnungszahlung</td>
<td v-else>Klassische Auszahlung</td>
</tr>
<tr>
@@ -81,7 +81,7 @@ const emit = defineEmits(["accept", "deny", "fix", "reopen"])
<td>{{props.data.accountIban}}</td>
<td>Buchungsinformationen:</td>
<td v-if="props.data.donation">Als Spende gebucht</td>
<td v-else-if="props.data.alreadyPaid">Beleg ohne Auszahlung</td>
<td v-else-if="props.data.externalPayment">Rechnungszahlung</td>
<td v-else>Klassische Auszahlung</td>
</tr>
<tr>
@@ -56,7 +56,7 @@
</td>
<td style="width: 150px;">
<Icon v-if="invoice.donation" name="hand-holding-dollar" style="color: #ffffff; background-color: green" />
<Icon v-if="invoice.alreadyPaid" name="comments-dollar" style="color: #ffffff; background-color: green" />
<Icon v-if="invoice.externalPayment" name="comments-dollar" style="color: #ffffff; background-color: green" />
</td>
<td>
{{invoice.contactName}}<br />
@@ -0,0 +1,126 @@
<script setup>
import { ref, onMounted, reactive } from 'vue'
import {checkFilesize} from "../../../../../../resources/js/components/InvoiceUploadChecks.js";
import RefundData from "./refund-data.vue";
import AmountInput from "../../../../../Views/Components/AmountInput.vue";
import {useAjax} from "../../../../../../resources/js/components/ajaxHandler.js";
import InfoIcon from '../../../../../Views/Components/InfoIcon.vue'
import {toast} from "vue3-toastify";
import PaymentData from "./payment-data.vue";
const data = defineProps({
eventId: Number,
userName: String,
userEmail: String,
userTelephone: String,
userAccountIban: String,
userAccountOwner: String,
})
const { request } = useAjax();
const amount = ref(0.00);
const invoiceType = ref(null);
const otherText = ref('');
const receipt = ref(null)
const finalStep = ref(false)
const invoiceTypeCollection = reactive({
invoiceTypes: {}
});
onMounted(async () => {
const response = await fetch('/api/v1/core/retrieve-invoice-types');
const data = await response.json();
Object.assign(invoiceTypeCollection, data);
});
function handleFileChange(event) {
if (checkFilesize('receipt')) {
receipt.value = event.target.files[0]
finalStep.value = true
} else {
event.target.value = null
}
}
</script>
<template>
<fieldset>
<legend><span style="font-weight: bolder;">Was ist der Anlass der Rechnung</span></legend>
<p v-for="availableInvoiceType in invoiceTypeCollection.invoiceTypes">
<input
name="invpice_type"
type="radio"
:value="availableInvoiceType.slug"
:id="'invoice_type_' + availableInvoiceType.slug"
v-model="invoiceType"
>
<label :for="'invoice_type_' + availableInvoiceType.slug">{{ availableInvoiceType.name }}</label>
<InfoIcon :text="'INFO_INVOICE_TYPE_' + availableInvoiceType.slug" /><br />
</p>
<label for="invoice_type_other">
<input
type="text"
class="width-full"
name="kostengruppe_sonstiges"
placeholder="Sonstige"
for="invoice_type_other"
v-model="otherText"
@focus="invoiceType = 'other'"
/>
</label>
</fieldset><br /><br />
<fieldset>
<legend><span style="font-weight: bolder;">Wie hoch ist der Betrag</span></legend>
<AmountInput v-model="amount" class="width-small" id="amount" name="amount" /> Euro
<info-icon></info-icon><br /><br />
<input
v-if="amount != '' && invoiceType !== null"
class="mareike-button"
onclick="document.getElementById('receipt').click();"
type="button"
value="Beleg auswählen und fortfahren" />
<input accept="application/pdf" type="file" id="receipt" name="receipt" @change="handleFileChange"
style="display: none"/>
</fieldset><br />
<PaymentData
v-if="finalStep"
:eventId="data.eventId"
:invoice-type="invoiceType"
:amount="amount"
:other-text="otherText"
:userName="data.userName"
:userEmail="data.userEmail"
:userTelephone="data.userTelephone"
:userAccountIban="data.userAccountIban"
:userAccountOwner="data.userAccountOwner"
:receipt="receipt"
travelReason=""
@close="finalStep = false"
/>
</template>
<style scoped>
</style>
@@ -0,0 +1,161 @@
<script setup>
import { ref } from 'vue'
import Modal from "../../../../../Views/Components/Modal.vue";
import IbanInput from "../../../../../Views/Components/IbanInput.vue";
import {useAjax} from "../../../../../../resources/js/components/ajaxHandler.js";
import TextResource from "../../../../../Views/Components/TextResource.vue";
import {invoiceCheckContactName} from "../../../../../../resources/js/components/InvoiceUploadChecks.js";
import {toast} from "vue3-toastify";
import ErrorText from "../../../../../Views/Components/ErrorText.vue";
const { request } = useAjax();
const emit = defineEmits(['close'])
const props = defineProps({
eventId: Number,
invoiceType: String,
amount: [String, Number],
otherText: String,
receipt: File,
userName: String,
userEmail: String,
userTelephone: String,
userAccountOwner: String,
userAccountIban: String,
havePassengers: Number,
materialTransportation: Boolean,
travelReason: String,
})
const finalStep = ref(true)
const userName = ref(props.userName)
const userEmail = ref(props.userEmail)
const userTelephone = ref(props.userTelephone)
const userIban = ref('')
const userAccountOwner = ref('')
const paymentPurpose = ref('')
const sending = ref(false)
const success = ref(false)
const decision = ref('')
const errorMsg = ref('')
const confirmation = ref(null)
const receiptError = ref('')
async function sendData() {
if (!userName.value) return
sending.value = true
errorMsg.value = ''
success.value = false
const formData = new FormData()
formData.append('name', userName.value)
formData.append('email', userEmail.value)
formData.append('telephone', userTelephone.value)
formData.append('amount', props.amount)
formData.append('otherText', props.otherText)
formData.append('decision', decision.value)
formData.append('accountOwner', userAccountOwner.value)
formData.append('accountIban', userIban.value)
formData.append('paymentPurpose', paymentPurpose.value)
formData.append('havePassengers', props.havePassengers ? 1 : 0)
formData.append('materialTransportation', props.materialTransportation ? 1 : 0)
formData.append('travelReason', props.travelReason)
if (props.receipt) {
formData.append('receipt', props.receipt)
}
try {
const response = await request('/api/v1/invoice/new/' + props.eventId + '/' + props.invoiceType, {
method: 'POST',
body: formData
})
if (response.status === 'success') {
window.location.href = '/';
} else {
receiptError.value = response.message;
}
} catch (err) {
toast.error(result.message);
} finally {
sending.value = false
}
}
</script>
<template>
<Modal :show="finalStep" title='Bitte gib deine Daten ein' @close="emit('close')" width="600px">
<label>
<strong>Dein Name Name (kein Pfadiname):</strong>
</label><br />
<input
type="text"
@keyup="invoiceCheckContactName();"
id="contact_name"
name="contact_name" v-model="userName"
style="font-size: 14pt; width: 550px;" /><br /><br />
<label v-if="userName !== ''">
<strong>E-Mail-Adresse (Für Rückfragen):</strong>
</label><br />
<input
v-if="userName !== ''"
type="email"
name="contact_email"
v-model="userEmail"
style="font-size: 14pt; width: 550px;" /><br /><br />
<label v-if="userName !== ''">
<strong>Telefonnummer (für Rückfragen):</strong>
</label><br />
<input
v-if="userName !== ''"
type="text"
id="contact_telephone"
name="contact_telephone" v-model="userTelephone"
style="font-size: 14pt; width: 550px;" /><br /><br />
<span id="confirm_payment" v-if="userEmail !== '' && userTelephone !== ''">
<label>
<strong>Zahlungsempfänger*in:</strong>
</label><br />
<input type="text" name="account_owner" id="account_owner" v-model="userAccountOwner" style="font-size: 14pt; width: 550px;" /><br /><br />
<label>
<strong>IBAN:</strong>
</label><br />
<IbanInput id="account_iban" name="account_iban" v-model="userIban" style="font-size: 14pt; width: 550px;" /><br /><br />
<label>
<strong>Überweisungstext (laut Rechnung):</strong>
</label><br />
<input
type="text"
id="payment_purpose"
name="payment_purpose"
v-model="paymentPurpose"
style="font-size: 14pt; width: 550px;"
/><br /><br />
<ErrorText :message="receiptError" />
<span v-if="paymentPurpose != '' && userAccountOwner != '' && userIban && userIban.length === 27"><br />
<input type="radio" name="confirmation_radio" value="payment" id="confirmation_radio_payment" v-model="confirmation">
<TextResource belongsTo="confirmation_radio_payment" textName="CONFIRMATION_DIRECT_PAYMENT" /><br /><br />
<input type="button" v-if="confirmation !== null && !sending" @click="sendData" value="Beleg einreichen" />
</span>
</span>
</Modal>
</template>
<style scoped>
/* optional styling */
</style>
@@ -6,6 +6,7 @@ import {useAjax} from "../../../../../../resources/js/components/ajaxHandler.js"
import TextResource from "../../../../../Views/Components/TextResource.vue";
import {invoiceCheckContactName} from "../../../../../../resources/js/components/InvoiceUploadChecks.js";
import {toast} from "vue3-toastify";
import ErrorText from "../../../../../Views/Components/ErrorText.vue";
const { request } = useAjax();
@@ -39,7 +40,7 @@ const success = ref(false)
const decision = ref('')
const errorMsg = ref('')
const confirmation = ref(null)
const receiptError = ref('')
async function sendData() {
if (!userName.value) return
@@ -75,6 +76,8 @@ async function sendData() {
if (response.status === 'success') {
window.location.href = '/';
} else {
receiptError.value = response.message;
}
} catch (err) {
toast.error(result.message);
@@ -87,7 +90,7 @@ async function sendData() {
</script>
<template>
<Modal :show="finalStep" title='Bitte gib deine Daten ein' @close="emit('close')">
<Modal :show="finalStep" title='Bitte gib deine Daten ein' @close="emit('close')" width="600px">
<label>
<strong>Dein Name Name (kein Pfadiname):</strong>
</label><br />
@@ -143,7 +146,7 @@ async function sendData() {
<strong>IBAN:</strong>
</label><br />
<IbanInput id="account_iban" name="account_iban" v-model="userIban" style="font-size: 14pt; width: 550px;" /><br /><br />
<ErrorText :message="receiptError" />
<span v-if="userAccountOwner != '' && userIban && userIban.length === 27"><br />
<input type="radio" name="confirmation_radio" value="payment" id="confirmation_radio_payment" v-model="confirmation">
<TextResource belongsTo="confirmation_radio_payment" textName="CONFIRMATION_PAYMENT" /><br /><br />