Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72e0502bb9 |
@@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Admin\Actions\UpdateTenantTax;
|
|
||||||
|
|
||||||
use App\Enumerations\TaxExemptionReason;
|
|
||||||
|
|
||||||
class UpdateTenantTaxAction
|
|
||||||
{
|
|
||||||
public function __construct(private UpdateTenantTaxRequest $request)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public function execute(): UpdateTenantTaxResponse
|
|
||||||
{
|
|
||||||
$response = new UpdateTenantTaxResponse();
|
|
||||||
|
|
||||||
if ($this->request->taxLiable) {
|
|
||||||
$this->request->tenant->update([
|
|
||||||
'tax_liable' => true,
|
|
||||||
'vat_rate' => $this->clampVatRate($this->request->vatRate),
|
|
||||||
'tax_exemption_reason' => null,
|
|
||||||
'tax_exemption_note' => null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->success = true;
|
|
||||||
$response->message = 'Steuerinformationen wurden gespeichert.';
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
$reason = TaxExemptionReason::tryFrom((string)$this->request->exemptionReason);
|
|
||||||
|
|
||||||
if ($reason === null) {
|
|
||||||
$response->message = 'Bitte einen gültigen Befreiungsgrund auswählen.';
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
$note = trim((string)$this->request->exemptionNote);
|
|
||||||
|
|
||||||
if ($reason->requiresNote() && $note === '') {
|
|
||||||
$response->message = 'Bitte einen Text für den Befreiungsgrund angeben.';
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->request->tenant->update([
|
|
||||||
'tax_liable' => false,
|
|
||||||
'vat_rate' => 0,
|
|
||||||
'tax_exemption_reason' => $reason->value,
|
|
||||||
'tax_exemption_note' => $reason->requiresNote() ? $note : null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->success = true;
|
|
||||||
$response->message = 'Steuerinformationen wurden gespeichert.';
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function clampVatRate(int $vatRate): int
|
|
||||||
{
|
|
||||||
return max(0, min(100, $vatRate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Admin\Actions\UpdateTenantTax;
|
|
||||||
|
|
||||||
use App\Models\Tenant;
|
|
||||||
|
|
||||||
class UpdateTenantTaxRequest
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public Tenant $tenant,
|
|
||||||
public bool $taxLiable,
|
|
||||||
public int $vatRate,
|
|
||||||
public ?string $exemptionReason,
|
|
||||||
public ?string $exemptionNote,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Admin\Actions\UpdateTenantTax;
|
|
||||||
|
|
||||||
class UpdateTenantTaxResponse
|
|
||||||
{
|
|
||||||
public bool $success = false;
|
|
||||||
public string $message = '';
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Admin\Controllers;
|
|
||||||
|
|
||||||
use App\Enumerations\TaxExemptionReason;
|
|
||||||
use App\Scopes\CommonController;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class ManagedTenantTaxGetController extends CommonController
|
|
||||||
{
|
|
||||||
public function __invoke(string $slug, Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$tenant = $this->adminTenants->findBySlug($slug);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'tax_liable' => $tenant->tax_liable,
|
|
||||||
'vat_rate' => $tenant->vat_rate,
|
|
||||||
'tax_exemption_reason' => $tenant->tax_exemption_reason,
|
|
||||||
'tax_exemption_note' => $tenant->tax_exemption_note,
|
|
||||||
'exemption_reasons' => TaxExemptionReason::options(),
|
|
||||||
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/tax',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Admin\Controllers;
|
|
||||||
|
|
||||||
use App\Domains\Admin\Actions\UpdateTenantTax\UpdateTenantTaxAction;
|
|
||||||
use App\Domains\Admin\Actions\UpdateTenantTax\UpdateTenantTaxRequest;
|
|
||||||
use App\Scopes\CommonController;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class ManagedTenantTaxUpdateController extends CommonController
|
|
||||||
{
|
|
||||||
public function __invoke(string $slug, Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$tenant = $this->adminTenants->findBySlug($slug);
|
|
||||||
|
|
||||||
$action = new UpdateTenantTaxAction(new UpdateTenantTaxRequest(
|
|
||||||
tenant: $tenant,
|
|
||||||
taxLiable: $request->boolean('tax_liable'),
|
|
||||||
vatRate: (int)$request->input('vat_rate', 0),
|
|
||||||
exemptionReason: $request->input('tax_exemption_reason'),
|
|
||||||
exemptionNote: $request->input('tax_exemption_note'),
|
|
||||||
));
|
|
||||||
|
|
||||||
$response = $action->execute();
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'status' => $response->success ? 'success' : 'error',
|
|
||||||
'message' => $response->message,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Admin\Controllers;
|
|
||||||
|
|
||||||
use App\Enumerations\TaxExemptionReason;
|
|
||||||
use App\Scopes\CommonController;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class TenantTaxGetController extends CommonController
|
|
||||||
{
|
|
||||||
public function __invoke(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
return response()->json([
|
|
||||||
'tax_liable' => $this->tenant->tax_liable,
|
|
||||||
'vat_rate' => $this->tenant->vat_rate,
|
|
||||||
'tax_exemption_reason' => $this->tenant->tax_exemption_reason,
|
|
||||||
'tax_exemption_note' => $this->tenant->tax_exemption_note,
|
|
||||||
'exemption_reasons' => TaxExemptionReason::options(),
|
|
||||||
'saveEndpoint' => '/api/v1/admin/tenant/tax',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Admin\Controllers;
|
|
||||||
|
|
||||||
use App\Domains\Admin\Actions\UpdateTenantTax\UpdateTenantTaxAction;
|
|
||||||
use App\Domains\Admin\Actions\UpdateTenantTax\UpdateTenantTaxRequest;
|
|
||||||
use App\Scopes\CommonController;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class TenantTaxUpdateController extends CommonController
|
|
||||||
{
|
|
||||||
public function __invoke(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$action = new UpdateTenantTaxAction(new UpdateTenantTaxRequest(
|
|
||||||
tenant: $this->tenant,
|
|
||||||
taxLiable: $request->boolean('tax_liable'),
|
|
||||||
vatRate: (int)$request->input('vat_rate', 0),
|
|
||||||
exemptionReason: $request->input('tax_exemption_reason'),
|
|
||||||
exemptionNote: $request->input('tax_exemption_note'),
|
|
||||||
));
|
|
||||||
|
|
||||||
$response = $action->execute();
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'status' => $response->success ? 'success' : 'error',
|
|
||||||
'message' => $response->message,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Admin\Controllers\UserDetailGetController;
|
||||||
|
use App\Domains\Admin\Controllers\UserListApiController;
|
||||||
|
use App\Domains\Admin\Controllers\UserResetPasswordController;
|
||||||
|
use App\Domains\Admin\Controllers\UserToggleActiveController;
|
||||||
|
use App\Domains\Admin\Controllers\UserUpdateController;
|
||||||
use App\Domains\Admin\Controllers\ManagedTenantContactGetController;
|
use App\Domains\Admin\Controllers\ManagedTenantContactGetController;
|
||||||
use App\Domains\Admin\Controllers\ManagedTenantContactUpdateController;
|
use App\Domains\Admin\Controllers\ManagedTenantContactUpdateController;
|
||||||
use App\Domains\Admin\Controllers\ManagedTenantGdprGetController;
|
use App\Domains\Admin\Controllers\ManagedTenantGdprGetController;
|
||||||
@@ -8,8 +13,6 @@ use App\Domains\Admin\Controllers\ManagedTenantImpressGetController;
|
|||||||
use App\Domains\Admin\Controllers\ManagedTenantImpressUpdateController;
|
use App\Domains\Admin\Controllers\ManagedTenantImpressUpdateController;
|
||||||
use App\Domains\Admin\Controllers\ManagedTenantPaymentGetController;
|
use App\Domains\Admin\Controllers\ManagedTenantPaymentGetController;
|
||||||
use App\Domains\Admin\Controllers\ManagedTenantPaymentUpdateController;
|
use App\Domains\Admin\Controllers\ManagedTenantPaymentUpdateController;
|
||||||
use App\Domains\Admin\Controllers\ManagedTenantTaxGetController;
|
|
||||||
use App\Domains\Admin\Controllers\ManagedTenantTaxUpdateController;
|
|
||||||
use App\Domains\Admin\Controllers\TenantContactGetController;
|
use App\Domains\Admin\Controllers\TenantContactGetController;
|
||||||
use App\Domains\Admin\Controllers\TenantContactUpdateController;
|
use App\Domains\Admin\Controllers\TenantContactUpdateController;
|
||||||
use App\Domains\Admin\Controllers\TenantCreateController;
|
use App\Domains\Admin\Controllers\TenantCreateController;
|
||||||
@@ -21,16 +24,9 @@ use App\Domains\Admin\Controllers\TenantImpressGetController;
|
|||||||
use App\Domains\Admin\Controllers\TenantImpressUpdateController;
|
use App\Domains\Admin\Controllers\TenantImpressUpdateController;
|
||||||
use App\Domains\Admin\Controllers\TenantListApiController;
|
use App\Domains\Admin\Controllers\TenantListApiController;
|
||||||
use App\Domains\Admin\Controllers\TenantPaymentGetController;
|
use App\Domains\Admin\Controllers\TenantPaymentGetController;
|
||||||
|
use App\Domains\Admin\Controllers\TenantPaymentUpdateController;
|
||||||
use App\Domains\Admin\Controllers\TenantPaymentMethodsGetController;
|
use App\Domains\Admin\Controllers\TenantPaymentMethodsGetController;
|
||||||
use App\Domains\Admin\Controllers\TenantPaymentMethodsUpdateController;
|
use App\Domains\Admin\Controllers\TenantPaymentMethodsUpdateController;
|
||||||
use App\Domains\Admin\Controllers\TenantPaymentUpdateController;
|
|
||||||
use App\Domains\Admin\Controllers\TenantTaxGetController;
|
|
||||||
use App\Domains\Admin\Controllers\TenantTaxUpdateController;
|
|
||||||
use App\Domains\Admin\Controllers\UserDetailGetController;
|
|
||||||
use App\Domains\Admin\Controllers\UserListApiController;
|
|
||||||
use App\Domains\Admin\Controllers\UserResetPasswordController;
|
|
||||||
use App\Domains\Admin\Controllers\UserToggleActiveController;
|
|
||||||
use App\Domains\Admin\Controllers\UserUpdateController;
|
|
||||||
use App\Middleware\AdminRoleMiddleware;
|
use App\Middleware\AdminRoleMiddleware;
|
||||||
use App\Middleware\IdentifyTenant;
|
use App\Middleware\IdentifyTenant;
|
||||||
use App\Middleware\LvOnlyMiddleware;
|
use App\Middleware\LvOnlyMiddleware;
|
||||||
@@ -48,8 +44,6 @@ Route::middleware([IdentifyTenant::class, 'auth', AdminRoleMiddleware::class])->
|
|||||||
Route::post('/impress', TenantImpressUpdateController::class);
|
Route::post('/impress', TenantImpressUpdateController::class);
|
||||||
Route::get('/gdpr', TenantGdprGetController::class);
|
Route::get('/gdpr', TenantGdprGetController::class);
|
||||||
Route::post('/gdpr', TenantGdprUpdateController::class);
|
Route::post('/gdpr', TenantGdprUpdateController::class);
|
||||||
Route::get('/tax', TenantTaxGetController::class);
|
|
||||||
Route::post('/tax', TenantTaxUpdateController::class);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::prefix('api/v1/admin/users')->group(function () {
|
Route::prefix('api/v1/admin/users')->group(function () {
|
||||||
@@ -75,8 +69,6 @@ Route::middleware([IdentifyTenant::class, 'auth', AdminRoleMiddleware::class])->
|
|||||||
Route::post('/impress', ManagedTenantImpressUpdateController::class);
|
Route::post('/impress', ManagedTenantImpressUpdateController::class);
|
||||||
Route::get('/gdpr', ManagedTenantGdprGetController::class);
|
Route::get('/gdpr', ManagedTenantGdprGetController::class);
|
||||||
Route::post('/gdpr', ManagedTenantGdprUpdateController::class);
|
Route::post('/gdpr', ManagedTenantGdprUpdateController::class);
|
||||||
Route::get('/tax', ManagedTenantTaxGetController::class);
|
|
||||||
Route::post('/tax', ManagedTenantTaxUpdateController::class);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import {computed, ref} from 'vue';
|
|
||||||
import {useAjax} from "../../../../../resources/js/components/ajaxHandler.js";
|
|
||||||
import RichSelectBox from "../../../../Views/Components/RichSelectBox.vue";
|
|
||||||
import {toast} from "vue3-toastify";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
data: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const {request} = useAjax()
|
|
||||||
|
|
||||||
const editing = ref(false)
|
|
||||||
|
|
||||||
const reasons = computed(() => props.data.exemption_reasons ?? [])
|
|
||||||
|
|
||||||
const liableOptions = [
|
|
||||||
{value: '1', label: 'Ja – Umsatzsteuer wird erhoben'},
|
|
||||||
{value: '0', label: 'Nein – umsatzsteuerbefreit'},
|
|
||||||
]
|
|
||||||
|
|
||||||
const form = ref({
|
|
||||||
tax_liable: props.data.tax_liable ? '1' : '0',
|
|
||||||
vat_rate: props.data.vat_rate ?? 0,
|
|
||||||
tax_exemption_reason: props.data.tax_exemption_reason ?? '',
|
|
||||||
tax_exemption_note: props.data.tax_exemption_note ?? '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const isLiable = computed(() => form.value.tax_liable === '1')
|
|
||||||
const isCustomReason = computed(() => form.value.tax_exemption_reason === 'custom')
|
|
||||||
|
|
||||||
const selectedReason = computed(
|
|
||||||
() => reasons.value.find(r => r.value === form.value.tax_exemption_reason) ?? null
|
|
||||||
)
|
|
||||||
|
|
||||||
// Vorschau des Pflichthinweises nach § 14 Abs. 4 Nr. 8 UStG, der später auf der Rechnung erscheint.
|
|
||||||
const invoiceHint = computed(() => {
|
|
||||||
if (isLiable.value) {
|
|
||||||
return `Umsatzsteuerpflichtig – USt-Satz: ${form.value.vat_rate || 0} %`
|
|
||||||
}
|
|
||||||
if (isCustomReason.value) {
|
|
||||||
return form.value.tax_exemption_note?.trim() || '—'
|
|
||||||
}
|
|
||||||
return selectedReason.value?.text || '—'
|
|
||||||
})
|
|
||||||
|
|
||||||
const reasonLabel = computed(() => selectedReason.value?.label ?? '—')
|
|
||||||
|
|
||||||
async function save() {
|
|
||||||
const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenant/tax'
|
|
||||||
const response = await request(saveUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
tax_liable: isLiable.value,
|
|
||||||
vat_rate: Number(form.value.vat_rate) || 0,
|
|
||||||
tax_exemption_reason: isLiable.value ? null : form.value.tax_exemption_reason,
|
|
||||||
tax_exemption_note: form.value.tax_exemption_note,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response && response.status === 'success') {
|
|
||||||
toast.success(response.message)
|
|
||||||
editing.value = false
|
|
||||||
} else {
|
|
||||||
toast.error(response?.message ?? 'Fehler beim Speichern')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="!editing">
|
|
||||||
<table class="data-table">
|
|
||||||
<tr>
|
|
||||||
<th>Steuerpflicht</th>
|
|
||||||
<td>{{ isLiable ? 'Ja – Umsatzsteuer wird erhoben' : 'Nein – umsatzsteuerbefreit' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="isLiable">
|
|
||||||
<th>USt-Satz</th>
|
|
||||||
<td>{{ form.vat_rate || 0 }} %</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-else>
|
|
||||||
<th>Befreiungsgrund</th>
|
|
||||||
<td>{{ reasonLabel }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Rechnungshinweis</th>
|
|
||||||
<td>{{ invoiceHint }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<button class="btn-edit" @click="editing = true">Bearbeiten</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<table class="data-table">
|
|
||||||
<tr>
|
|
||||||
<th>Steuerpflicht</th>
|
|
||||||
<td>
|
|
||||||
<RichSelectBox v-model="form.tax_liable" :options="liableOptions"/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr v-if="isLiable">
|
|
||||||
<th>USt-Satz (%)</th>
|
|
||||||
<td><input type="number" min="0" max="100" v-model.number="form.vat_rate" class="form-input"/></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<tr>
|
|
||||||
<th>Befreiungsgrund</th>
|
|
||||||
<td>
|
|
||||||
<RichSelectBox
|
|
||||||
v-model="form.tax_exemption_reason"
|
|
||||||
:options="reasons"
|
|
||||||
placeholder="Grund auswählen…"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="isCustomReason">
|
|
||||||
<th>Freitext</th>
|
|
||||||
<td>
|
|
||||||
<textarea
|
|
||||||
v-model="form.tax_exemption_note"
|
|
||||||
class="form-input"
|
|
||||||
rows="3"
|
|
||||||
placeholder="Hinweistext für die Rechnung…"
|
|
||||||
></textarea>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<th>Rechnungshinweis</th>
|
|
||||||
<td class="hint-preview">{{ invoiceHint }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn-save" @click="save">Speichern</button>
|
|
||||||
<button class="btn-cancel" @click="editing = false">Abbrechen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.data-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table th {
|
|
||||||
text-align: left;
|
|
||||||
padding: 8px 12px;
|
|
||||||
width: 200px;
|
|
||||||
color: #374151;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table td {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint-preview {
|
|
||||||
color: #6b7280;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-edit, .btn-save, .btn-cancel {
|
|
||||||
margin-top: 15px;
|
|
||||||
padding: 8px 20px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-edit {
|
|
||||||
background-color: #1d4899;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-edit:hover {
|
|
||||||
background-color: #163a7a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-save {
|
|
||||||
background-color: #16a34a;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-save:hover {
|
|
||||||
background-color: #15803d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel {
|
|
||||||
background-color: #e5e7eb;
|
|
||||||
color: #374151;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel:hover {
|
|
||||||
background-color: #d1d5db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -7,7 +7,6 @@ import TenantPayment from "./Partials/TenantPayment.vue";
|
|||||||
import TenantImpress from "./Partials/TenantImpress.vue";
|
import TenantImpress from "./Partials/TenantImpress.vue";
|
||||||
import TenantGdpr from "./Partials/TenantGdpr.vue";
|
import TenantGdpr from "./Partials/TenantGdpr.vue";
|
||||||
import TenantPaymentMethods from "./Partials/TenantPaymentMethods.vue";
|
import TenantPaymentMethods from "./Partials/TenantPaymentMethods.vue";
|
||||||
import TenantTaxInfo from "./Partials/TenantTaxInfo.vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tenant: Object,
|
tenant: Object,
|
||||||
@@ -29,11 +28,6 @@ const tabs = [
|
|||||||
component: TenantPaymentMethods,
|
component: TenantPaymentMethods,
|
||||||
endpoint: '/api/v1/admin/tenant/payment-methods',
|
endpoint: '/api/v1/admin/tenant/payment-methods',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Steuerinformationen',
|
|
||||||
component: TenantTaxInfo,
|
|
||||||
endpoint: '/api/v1/admin/tenant/tax',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Impressum',
|
title: 'Impressum',
|
||||||
component: TenantImpress,
|
component: TenantImpress,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import TenantContact from "./Partials/TenantContact.vue";
|
|||||||
import TenantPayment from "./Partials/TenantPayment.vue";
|
import TenantPayment from "./Partials/TenantPayment.vue";
|
||||||
import TenantImpress from "./Partials/TenantImpress.vue";
|
import TenantImpress from "./Partials/TenantImpress.vue";
|
||||||
import TenantGdpr from "./Partials/TenantGdpr.vue";
|
import TenantGdpr from "./Partials/TenantGdpr.vue";
|
||||||
import TenantTaxInfo from "./Partials/TenantTaxInfo.vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tenant: Object,
|
tenant: Object,
|
||||||
@@ -31,11 +30,6 @@ const tabs = [
|
|||||||
component: TenantPayment,
|
component: TenantPayment,
|
||||||
endpoint: '/api/v1/admin/tenants/' + slug + '/payment',
|
endpoint: '/api/v1/admin/tenants/' + slug + '/payment',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Steuerinformationen',
|
|
||||||
component: TenantTaxInfo,
|
|
||||||
endpoint: '/api/v1/admin/tenants/' + slug + '/tax',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Impressum',
|
title: 'Impressum',
|
||||||
component: TenantImpress,
|
component: TenantImpress,
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Enumerations;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gründe für eine Umsatzsteuerbefreiung. Jeder Fall kapselt sein UI-Label und den auf der Rechnung
|
|
||||||
* verpflichtenden Hinweis nach § 14 Abs. 4 Nr. 8 UStG, damit Label und Text nur an einer Stelle
|
|
||||||
* gepflegt werden. Gemeinnützigkeit allein befreit nicht – die Befreiung braucht immer einen konkreten
|
|
||||||
* Rechtsgrund; die Vereins-Fälle sind über § 4 Nr. 22a / Nr. 25 abgedeckt.
|
|
||||||
*/
|
|
||||||
enum TaxExemptionReason: string
|
|
||||||
{
|
|
||||||
case SmallBusiness = 'small_business';
|
|
||||||
case Education = 'education';
|
|
||||||
case YouthWelfare = 'youth_welfare';
|
|
||||||
case Custom = 'custom';
|
|
||||||
|
|
||||||
public function label(): string
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::SmallBusiness => 'Kleinunternehmer (§ 19 UStG)',
|
|
||||||
self::Education => 'Bildungs-/Kursleistung (§ 4 Nr. 22a UStG)',
|
|
||||||
self::YouthWelfare => 'Kinder- und Jugendhilfe (§ 4 Nr. 25 UStG)',
|
|
||||||
self::Custom => 'Sonstiger Grund (Freitext)',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pflichthinweis für die Rechnung. Bei einem individuellen Grund (Custom) wird der hinterlegte
|
|
||||||
* Freitext des Tenants verwendet.
|
|
||||||
*/
|
|
||||||
public function invoiceText(?string $customNote = null): string
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::SmallBusiness => 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.',
|
|
||||||
self::Education => 'Die Leistung ist gemäß § 4 Nr. 22 Buchst. a UStG von der Umsatzsteuer befreit.',
|
|
||||||
self::YouthWelfare => 'Die Leistung ist gemäß § 4 Nr. 25 UStG von der Umsatzsteuer befreit.',
|
|
||||||
self::Custom => trim((string)$customNote),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ob dieser Grund einen zusätzlichen Freitext des Tenants erfordert.
|
|
||||||
*/
|
|
||||||
public function requiresNote(): bool
|
|
||||||
{
|
|
||||||
return $this === self::Custom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optionen für das Frontend inkl. Vorschau-Text. Bei Custom ist `text` leer, da der Text erst aus
|
|
||||||
* dem Freitext des Tenants entsteht.
|
|
||||||
*
|
|
||||||
* @return array<int, array{value: string, label: string, text: string}>
|
|
||||||
*/
|
|
||||||
public static function options(): array
|
|
||||||
{
|
|
||||||
return array_map(
|
|
||||||
static fn(self $reason): array => [
|
|
||||||
'value' => $reason->value,
|
|
||||||
'label' => $reason->label(),
|
|
||||||
'text' => $reason->requiresNote() ? '' : $reason->invoiceText(),
|
|
||||||
],
|
|
||||||
self::cases(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-14
@@ -21,10 +21,6 @@ use App\Scopes\CommonModel;
|
|||||||
* @property string $url_participation_rules
|
* @property string $url_participation_rules
|
||||||
* @property boolean $events_allowed
|
* @property boolean $events_allowed
|
||||||
* @property boolean $has_active_instance
|
* @property boolean $has_active_instance
|
||||||
* @property boolean $tax_liable
|
|
||||||
* @property int $vat_rate
|
|
||||||
* @property string|null $tax_exemption_reason
|
|
||||||
* @property string|null $tax_exemption_note
|
|
||||||
*/
|
*/
|
||||||
class Tenant extends CommonModel
|
class Tenant extends CommonModel
|
||||||
{
|
{
|
||||||
@@ -50,15 +46,6 @@ class Tenant extends CommonModel
|
|||||||
'impress_text',
|
'impress_text',
|
||||||
'url_participation_rules',
|
'url_participation_rules',
|
||||||
'is_active_local_group',
|
'is_active_local_group',
|
||||||
'has_active_instance',
|
'has_active_instance'
|
||||||
'tax_liable',
|
|
||||||
'vat_rate',
|
|
||||||
'tax_exemption_reason',
|
|
||||||
'tax_exemption_note',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'tax_liable' => 'boolean',
|
|
||||||
'vat_rate' => 'integer',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration {
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->boolean('tax_liable')->default(false);
|
|
||||||
$table->unsignedTinyInteger('vat_rate')->default(0);
|
|
||||||
$table->string('tax_exemption_reason')->default('youth_welfare');
|
|
||||||
$table->text('tax_exemption_note')->nullable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->dropColumn([
|
|
||||||
'tax_liable',
|
|
||||||
'vat_rate',
|
|
||||||
'tax_exemption_reason',
|
|
||||||
'tax_exemption_note',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use App\Domains\Admin\Actions\UpdateTenantTax\UpdateTenantTaxAction;
|
|
||||||
use App\Domains\Admin\Actions\UpdateTenantTax\UpdateTenantTaxRequest;
|
|
||||||
use App\Enumerations\TaxExemptionReason;
|
|
||||||
use App\Models\Tenant;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class TenantTaxInformationTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
private Tenant $tenant;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->tenant = Tenant::create([
|
|
||||||
'slug' => 'tv',
|
|
||||||
'name' => 'Test',
|
|
||||||
'email' => 't@example.com',
|
|
||||||
'email_finance' => 'finance@example.com',
|
|
||||||
'url' => 'test.local',
|
|
||||||
'account_name' => 'Test e.V.',
|
|
||||||
'account_iban' => 'DE00',
|
|
||||||
'account_bic' => 'XY',
|
|
||||||
'city' => 'Stadt',
|
|
||||||
'postcode' => '00000',
|
|
||||||
'is_active_local_group' => true,
|
|
||||||
'has_active_instance' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
app()->instance('tenant', $this->tenant);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function runAction(bool $taxLiable, int $vatRate, ?string $reason, ?string $note = null): object
|
|
||||||
{
|
|
||||||
return new UpdateTenantTaxAction(new UpdateTenantTaxRequest(
|
|
||||||
tenant: $this->tenant,
|
|
||||||
taxLiable: $taxLiable,
|
|
||||||
vatRate: $vatRate,
|
|
||||||
exemptionReason: $reason,
|
|
||||||
exemptionNote: $note,
|
|
||||||
))->execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_taxable_sets_rate_and_clears_exemption(): void
|
|
||||||
{
|
|
||||||
$this->tenant->update(['tax_exemption_reason' => 'small_business', 'tax_exemption_note' => 'alt']);
|
|
||||||
|
|
||||||
$response = $this->runAction(taxLiable: true, vatRate: 7, reason: null);
|
|
||||||
|
|
||||||
$this->assertTrue($response->success);
|
|
||||||
|
|
||||||
$fresh = $this->tenant->fresh();
|
|
||||||
$this->assertTrue($fresh->tax_liable);
|
|
||||||
$this->assertSame(7, $fresh->vat_rate);
|
|
||||||
$this->assertNull($fresh->tax_exemption_reason);
|
|
||||||
$this->assertNull($fresh->tax_exemption_note);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_exemption_reason_persists_and_clears_rate(): void
|
|
||||||
{
|
|
||||||
$response = $this->runAction(taxLiable: false, vatRate: 19, reason: 'youth_welfare');
|
|
||||||
|
|
||||||
$this->assertTrue($response->success);
|
|
||||||
|
|
||||||
$fresh = $this->tenant->fresh();
|
|
||||||
$this->assertFalse($fresh->tax_liable);
|
|
||||||
$this->assertSame(0, $fresh->vat_rate);
|
|
||||||
$this->assertSame('youth_welfare', $fresh->tax_exemption_reason);
|
|
||||||
$this->assertNull($fresh->tax_exemption_note);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_invalid_exemption_reason_fails(): void
|
|
||||||
{
|
|
||||||
$response = $this->runAction(taxLiable: false, vatRate: 0, reason: 'not_a_reason');
|
|
||||||
|
|
||||||
$this->assertFalse($response->success);
|
|
||||||
$this->assertNull($this->tenant->fresh()->tax_exemption_reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_custom_reason_requires_note(): void
|
|
||||||
{
|
|
||||||
$missing = $this->runAction(taxLiable: false, vatRate: 0, reason: 'custom', note: ' ');
|
|
||||||
$this->assertFalse($missing->success);
|
|
||||||
|
|
||||||
$ok = $this->runAction(taxLiable: false, vatRate: 0, reason: 'custom', note: 'Individueller Grund');
|
|
||||||
$this->assertTrue($ok->success);
|
|
||||||
$this->assertSame('Individueller Grund', $this->tenant->fresh()->tax_exemption_note);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_vat_rate_is_clamped(): void
|
|
||||||
{
|
|
||||||
$this->runAction(taxLiable: true, vatRate: 250, reason: null);
|
|
||||||
$this->assertSame(100, $this->tenant->fresh()->vat_rate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_enum_invoice_text_per_case(): void
|
|
||||||
{
|
|
||||||
$this->assertSame(
|
|
||||||
'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.',
|
|
||||||
TaxExemptionReason::SmallBusiness->invoiceText(),
|
|
||||||
);
|
|
||||||
$this->assertSame(
|
|
||||||
'Die Leistung ist gemäß § 4 Nr. 22 Buchst. a UStG von der Umsatzsteuer befreit.',
|
|
||||||
TaxExemptionReason::Education->invoiceText(),
|
|
||||||
);
|
|
||||||
$this->assertSame(
|
|
||||||
'Die Leistung ist gemäß § 4 Nr. 25 UStG von der Umsatzsteuer befreit.',
|
|
||||||
TaxExemptionReason::YouthWelfare->invoiceText(),
|
|
||||||
);
|
|
||||||
$this->assertSame('Mein Text', TaxExemptionReason::Custom->invoiceText('Mein Text'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user