Steuerberater Modul tax
This commit is contained in:
parent
0f82fea88a
commit
245c281541
22 changed files with 1489 additions and 139 deletions
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Cron;
|
||||
|
||||
use App\Http\Controllers\Pay\PayoneController;
|
||||
use App\Models\PaymentTransaction;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingOrderItem;
|
||||
use App\Models\UserAbo;
|
||||
|
|
@ -74,6 +75,7 @@ class UserMakeOrder
|
|||
$this->pay->setAboPayment($this->userAbo, $amount, 'EUR');
|
||||
$this->pay->setPersonalData();
|
||||
$response = $this->pay->onlyPaymentResponse();
|
||||
$this->recordPaymentTransaction($response);
|
||||
\Log::info('Response: '.json_encode($response));
|
||||
// $response = $this->pay->ResponseData(true);
|
||||
|
||||
|
|
@ -86,6 +88,33 @@ class UserMakeOrder
|
|||
}
|
||||
}
|
||||
|
||||
private function recordPaymentTransaction(mixed $response): void
|
||||
{
|
||||
$shoppingPayment = $this->getShoppingPayment();
|
||||
if (! $shoppingPayment) {
|
||||
return;
|
||||
}
|
||||
|
||||
$responseData = is_object($response) ? (array) $response : $response;
|
||||
if (! is_array($responseData) || ! isset($responseData['status'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
PaymentTransaction::create([
|
||||
'shopping_payment_id' => $shoppingPayment->id,
|
||||
'request' => 'authorization',
|
||||
'txid' => $responseData['txid'] ?? 0,
|
||||
'userid' => $responseData['userid'] ?? $this->userAbo->payone_userid ?? 0,
|
||||
'status' => $responseData['status'],
|
||||
'txaction' => $responseData['txaction'] ?? null,
|
||||
'transmitted_data' => $responseData,
|
||||
'errorcode' => $responseData['errorcode'] ?? null,
|
||||
'errormessage' => $responseData['errormessage'] ?? null,
|
||||
'customermessage' => $responseData['customermessage'] ?? null,
|
||||
'mode' => $shoppingPayment->mode,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getShoppingPayment()
|
||||
{
|
||||
Log::info('Rufe Zahlungsinformationen ab für UserAbo ID: '.$this->userAbo->id);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use App\Models\UserAbo;
|
|||
use App\Repositories\AboRepository;
|
||||
use App\Services\AboItemHistoryService;
|
||||
use App\Services\AboOrderCart;
|
||||
use App\Services\AboRetryPaymentService;
|
||||
use App\Services\Shop;
|
||||
use Request;
|
||||
|
||||
|
|
@ -102,6 +103,16 @@ class AboController extends Controller
|
|||
return redirect(route('admin_abos_detail', [$id]));
|
||||
}
|
||||
|
||||
public function retryPayment($id, AboRetryPaymentService $retryPaymentService)
|
||||
{
|
||||
$user_abo = UserAbo::findOrFail($id);
|
||||
$result = $retryPaymentService->retry($user_abo);
|
||||
|
||||
\Session()->flash($result['success'] ? 'alert-success' : 'alert-error', $result['message']);
|
||||
|
||||
return redirect(route('admin_abos_detail', [$id]));
|
||||
}
|
||||
|
||||
public function datatable()
|
||||
{
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,8 @@ class AboController extends Controller
|
|||
$data = Request::all();
|
||||
$user_abo = UserAbo::findOrFail($id);
|
||||
$this->checkPermissions($view, $user_abo);
|
||||
$isAddOnlyMode = AboHelper::isAddOnlyMode($user_abo, $view);
|
||||
$editView = \Auth::user()?->isAdmin() ? 'admin' : $view;
|
||||
$isAddOnlyMode = AboHelper::isAddOnlyMode($user_abo, $editView);
|
||||
|
||||
if (isset($data['action'])) {
|
||||
if ($data['action'] === 'abo_update_settings') {
|
||||
|
|
@ -127,7 +128,7 @@ class AboController extends Controller
|
|||
$qtyBefore = $UserAboItem->qty;
|
||||
$UserAboItem->qty = $UserAboItem->qty + 1;
|
||||
$UserAboItem->save();
|
||||
AboItemHistoryService::logProductAdded($user_abo, $UserAboItem, $qtyBefore, $view);
|
||||
AboItemHistoryService::logProductAdded($user_abo, $UserAboItem, $qtyBefore, $editView);
|
||||
} else {
|
||||
$newItem = UserAboItem::create([
|
||||
'user_abo_id' => $user_abo->id,
|
||||
|
|
@ -136,7 +137,7 @@ class AboController extends Controller
|
|||
'qty' => 1,
|
||||
'status' => 1,
|
||||
]);
|
||||
AboItemHistoryService::logProductAdded($user_abo, $newItem, 0, $view);
|
||||
AboItemHistoryService::logProductAdded($user_abo, $newItem, 0, $editView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +157,7 @@ class AboController extends Controller
|
|||
}
|
||||
$UserAboItem->qty = $qty;
|
||||
$UserAboItem->save();
|
||||
AboItemHistoryService::logQtyChanged($user_abo, $UserAboItem, $qtyBefore, $qty, $view);
|
||||
AboItemHistoryService::logQtyChanged($user_abo, $UserAboItem, $qtyBefore, $qty, $editView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -181,7 +182,7 @@ class AboController extends Controller
|
|||
$message = __('abo.need_basis_product');
|
||||
}
|
||||
if (! $message) {
|
||||
AboItemHistoryService::logProductRemoved($user_abo, $userAboItem, $view);
|
||||
AboItemHistoryService::logProductRemoved($user_abo, $userAboItem, $editView);
|
||||
$userAboItem->delete();
|
||||
$user_abo->refresh(); // Abo neu laden um die aktualisierten Items zu erhalten
|
||||
}
|
||||
|
|
@ -193,7 +194,7 @@ class AboController extends Controller
|
|||
$UserAboItem->product_id = $data['comp_product_id'];
|
||||
$UserAboItem->save();
|
||||
$UserAboItem->load('product');
|
||||
AboItemHistoryService::logCompProductChanged($user_abo, $UserAboItem, $oldProduct, $UserAboItem->product, $view);
|
||||
AboItemHistoryService::logCompProductChanged($user_abo, $UserAboItem, $oldProduct, $UserAboItem->product, $editView);
|
||||
} else {
|
||||
$newItem = UserAboItem::create([
|
||||
'user_abo_id' => $user_abo->id,
|
||||
|
|
@ -202,7 +203,7 @@ class AboController extends Controller
|
|||
'qty' => 1,
|
||||
'status' => 1,
|
||||
]);
|
||||
AboItemHistoryService::logProductAdded($user_abo, $newItem, 0, $view);
|
||||
AboItemHistoryService::logProductAdded($user_abo, $newItem, 0, $editView);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace App\Repositories;
|
|||
|
||||
use App\Models\UserAbo;
|
||||
use App\Services\AboHelper;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class AboRepository extends BaseRepository
|
||||
{
|
||||
|
|
@ -27,8 +28,10 @@ class AboRepository extends BaseRepository
|
|||
if ($data['action'] === 'abo_update_settings') {
|
||||
if ($this->validate($data)) {
|
||||
$this->updateStatus($data);
|
||||
$this->model->abo_interval = $data['abo_interval'];
|
||||
$nextDate = $this->calculateNewNextDate($data['abo_interval']);
|
||||
$this->model->abo_interval = (int) $data['abo_interval'];
|
||||
$nextDate = $this->isAdminUpdate($data)
|
||||
? $this->calculateAdminNextDate($data)
|
||||
: $this->calculateNewNextDate((int) $data['abo_interval']);
|
||||
$this->model->next_date = $nextDate;
|
||||
$this->model->save();
|
||||
|
||||
|
|
@ -49,10 +52,12 @@ class AboRepository extends BaseRepository
|
|||
|
||||
private function updateStatus($data)
|
||||
{
|
||||
$isAdminUpdate = $this->isAdminUpdate($data);
|
||||
|
||||
// Handle cancellation
|
||||
if (isset($data['abo_cancel']) && $data['abo_cancel'] == 'true') {
|
||||
// Sperre: 3 Tage vor Ausführung kann nicht mehr pausiert/gekündigt werden
|
||||
if ($this->model->next_date) {
|
||||
if (! $isAdminUpdate && $this->model->next_date) {
|
||||
$daysUntil = (int) now()->diffInDays(\Carbon\Carbon::parse($this->model->next_date), false);
|
||||
if ($daysUntil >= 0 && $daysUntil < self::LOCK_DAYS_PAUSE_CANCEL) {
|
||||
\Session()->flash('alert-error', __('abo.error_cancel_locked', ['days' => $daysUntil]));
|
||||
|
|
@ -72,7 +77,7 @@ class AboRepository extends BaseRepository
|
|||
|
||||
$active = (isset($data['abo_is_active']) && $data['abo_is_active']) ? true : false;
|
||||
// Sperre: 3 Tage vor Ausführung kann nicht mehr pausiert werden
|
||||
if ($this->model->active && ! $active && $this->model->next_date) {
|
||||
if (! $isAdminUpdate && $this->model->active && ! $active && $this->model->next_date) {
|
||||
$daysUntil = (int) now()->diffInDays(\Carbon\Carbon::parse($this->model->next_date), false);
|
||||
if ($daysUntil >= 0 && $daysUntil < self::LOCK_DAYS_PAUSE_CANCEL) {
|
||||
\Session()->flash('alert-error', __('abo.error_pause_locked', ['days' => $daysUntil]));
|
||||
|
|
@ -122,12 +127,28 @@ class AboRepository extends BaseRepository
|
|||
return false;
|
||||
}
|
||||
}
|
||||
if (! in_array($data['abo_interval'], \App\Models\UserAbo::$aboDeliveryDays)) {
|
||||
if (! isset($data['abo_interval']) || ! in_array((int) $data['abo_interval'], \App\Models\UserAbo::$aboDeliveryDays, true)) {
|
||||
\Session()->flash('alert-error', __('abo.error_abo_interval'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isAdminUpdate($data)) {
|
||||
if (! isset($data['abo_next_month']) || ! in_array($data['abo_next_month'], $this->getAdminExecutionMonths(), true)) {
|
||||
\Session()->flash('alert-error', __('abo.error_next_date'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->calculateAdminNextDate($data)->startOfDay()->lt(now()->startOfDay())) {
|
||||
\Session()->flash('alert-error', __('abo.error_next_date'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sperre: 10 Tage vor nächster Ausführung keine Änderungen mehr (Pakete werden vorgepackt)
|
||||
if ($this->model->next_date) {
|
||||
$daysUntilExecution = (int) now()->diffInDays(\Carbon\Carbon::parse($this->model->next_date), false);
|
||||
|
|
@ -170,4 +191,26 @@ class AboRepository extends BaseRepository
|
|||
|
||||
return AboHelper::setNextDate($referenceDate, $aboInterval);
|
||||
}
|
||||
|
||||
private function calculateAdminNextDate(array $data): Carbon
|
||||
{
|
||||
return Carbon::createFromFormat('Y-m-d', $data['abo_next_month'].'-01')
|
||||
->startOfMonth()
|
||||
->addDays(((int) $data['abo_interval']) - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function getAdminExecutionMonths(): array
|
||||
{
|
||||
return collect(range(0, 3))
|
||||
->map(fn (int $offset): string => now()->copy()->startOfMonth()->addMonths($offset)->format('Y-m'))
|
||||
->all();
|
||||
}
|
||||
|
||||
private function isAdminUpdate(array $data): bool
|
||||
{
|
||||
return ($data['view'] ?? null) === 'admin';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ class CheckoutRepository extends BaseRepository
|
|||
'subtotal_ws' => $ShoppingCollectOrder->price_total_net,
|
||||
'tax' => $ShoppingCollectOrder->tax_total,
|
||||
'tax_split' => $ShoppingCollectOrder->tax_split,
|
||||
'net_split' => $ShoppingCollectOrder->net_split,
|
||||
'total_shipping' => Yard::instance($this->instance)->totalWithShipping(2, '.', ''),
|
||||
'points' => round($ShoppingCollectOrder->points, 2),
|
||||
'weight' => 0,
|
||||
|
|
|
|||
238
app/Services/AboRetryPaymentService.php
Normal file
238
app/Services/AboRetryPaymentService.php
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Cron\UserMakeOrder;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AboRetryPaymentService
|
||||
{
|
||||
/**
|
||||
* @return array{success: bool, message: string, order_id?: int}
|
||||
*/
|
||||
public function retry(UserAbo $userAbo): array
|
||||
{
|
||||
if ($userAbo->status !== 3) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('abo.retry_only_hold'),
|
||||
];
|
||||
}
|
||||
|
||||
if (! $userAbo->active) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('abo.retry_only_active'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->alreadyPaidToday($userAbo)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('abo.retry_already_paid_today'),
|
||||
];
|
||||
}
|
||||
|
||||
\Log::channel('abo_order')->info('AboRetryPaymentService: Starte erneuten Zahlungsversuch', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'email' => $userAbo->email,
|
||||
'payone_userid' => $userAbo->payone_userid,
|
||||
]);
|
||||
|
||||
AboHelper::ensureUserAboItemsFromLatestOrder($userAbo);
|
||||
|
||||
$shoppingOrder = null;
|
||||
$paymentAttemptRecorded = false;
|
||||
$userOrder = new UserMakeOrder($userAbo);
|
||||
|
||||
try {
|
||||
if (! $userOrder->createShoppingUser()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('abo.retry_error_shopping_user'),
|
||||
];
|
||||
}
|
||||
|
||||
$shoppingOrder = $userOrder->makeShoppingOrder();
|
||||
if (! $shoppingOrder) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('abo.retry_error_order'),
|
||||
];
|
||||
}
|
||||
|
||||
$response = $this->normalizePaymentResponse($userOrder->makePayment());
|
||||
|
||||
if (($response['status'] ?? null) === 'APPROVED') {
|
||||
$this->markAboSuccess($userAbo, $shoppingOrder);
|
||||
$paymentAttemptRecorded = true;
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => __('abo.retry_success', ['order' => $shoppingOrder->id]),
|
||||
'order_id' => $shoppingOrder->id,
|
||||
];
|
||||
}
|
||||
|
||||
$this->logPaymentError($userAbo, $shoppingOrder, $response);
|
||||
$this->markAboError($userAbo, $shoppingOrder);
|
||||
$paymentAttemptRecorded = true;
|
||||
$this->sendPaymentErrorMail($userOrder, $shoppingOrder, $response);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('abo.retry_failed', [
|
||||
'error' => $this->formatPaymentError($response),
|
||||
'order' => $shoppingOrder->id,
|
||||
]),
|
||||
'order_id' => $shoppingOrder->id,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
\Log::channel('abo_order')->error('AboRetryPaymentService: Exception beim erneuten Zahlungsversuch', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder?->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
if ($shoppingOrder && ! $paymentAttemptRecorded) {
|
||||
$this->markAboError($userAbo, $shoppingOrder);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('abo.retry_exception', ['error' => $e->getMessage()]),
|
||||
'order_id' => $shoppingOrder?->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function alreadyPaidToday(UserAbo $userAbo): bool
|
||||
{
|
||||
return UserAboOrder::where('user_abo_id', $userAbo->id)
|
||||
->whereDate('created_at', now()->toDateString())
|
||||
->where('paid', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function normalizePaymentResponse(mixed $response): array
|
||||
{
|
||||
if (is_object($response)) {
|
||||
return (array) $response;
|
||||
}
|
||||
|
||||
return is_array($response) ? $response : [];
|
||||
}
|
||||
|
||||
private function markAboSuccess(UserAbo $userAbo, mixed $shoppingOrder): void
|
||||
{
|
||||
DB::transaction(function () use ($userAbo, $shoppingOrder): void {
|
||||
$userAbo->update([
|
||||
'status' => 2,
|
||||
'active' => true,
|
||||
'next_date' => AboHelper::setNextDate(now(), $userAbo->abo_interval),
|
||||
'last_date' => now(),
|
||||
]);
|
||||
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $shoppingOrder->id,
|
||||
'status' => 1,
|
||||
'paid' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
try {
|
||||
IncentiveTracker::trackAboActivated($shoppingOrder);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::channel('abo_order')->error('AboRetryPaymentService: Incentive-Tracking nach erfolgreichem Retry fehlgeschlagen', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function markAboError(UserAbo $userAbo, mixed $shoppingOrder): void
|
||||
{
|
||||
DB::transaction(function () use ($userAbo, $shoppingOrder): void {
|
||||
$userAbo->update([
|
||||
'status' => 3,
|
||||
'last_date' => now(),
|
||||
]);
|
||||
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $shoppingOrder->id,
|
||||
'status' => 3,
|
||||
'paid' => false,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $response
|
||||
*/
|
||||
private function logPaymentError(UserAbo $userAbo, mixed $shoppingOrder, array $response): void
|
||||
{
|
||||
\Log::channel('abo_order')->error('AboRetryPaymentService: Zahlungsfehler beim erneuten Versuch', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'response' => $response,
|
||||
]);
|
||||
|
||||
MyLog::writeLog(
|
||||
'userabo',
|
||||
'error',
|
||||
'Error:AboRetryPaymentService::retry / makePayment Error',
|
||||
$response
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $response
|
||||
*/
|
||||
private function sendPaymentErrorMail(UserMakeOrder $userOrder, mixed $shoppingOrder, array $response): void
|
||||
{
|
||||
$shoppingPayment = $userOrder->getShoppingPayment();
|
||||
if (! $shoppingPayment) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Payment::paymentStatusSendMail($shoppingOrder, $shoppingPayment, [
|
||||
'mode' => $shoppingPayment->mode,
|
||||
'txaction' => 'error',
|
||||
'send_link' => false,
|
||||
'payment_error' => $response,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::channel('abo_order')->error('AboRetryPaymentService: Fehlermail nach Zahlungsfehler konnte nicht gesendet werden', [
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'payment_id' => $shoppingPayment->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $response
|
||||
*/
|
||||
private function formatPaymentError(array $response): string
|
||||
{
|
||||
$errorCode = $response['errorcode'] ?? null;
|
||||
$errorMessage = $response['errormessage'] ?? $response['customermessage'] ?? ($response['status'] ?? __('payment.unknown'));
|
||||
|
||||
if ($errorCode) {
|
||||
return '['.$errorCode.'] '.$errorMessage;
|
||||
}
|
||||
|
||||
return (string) $errorMessage;
|
||||
}
|
||||
}
|
||||
|
|
@ -221,7 +221,7 @@ class DatevExportService
|
|||
$csvContent = $this->buildCsv($export);
|
||||
$filename = $this->generateFilename($month, $year);
|
||||
$storagePath = $export->getStoragePath();
|
||||
$fullPath = $storagePath . '/' . $filename;
|
||||
$fullPath = $storagePath.'/'.$filename;
|
||||
|
||||
Storage::disk(config('datev.storage_disk'))->makeDirectory($storagePath);
|
||||
Storage::disk(config('datev.storage_disk'))->put($fullPath, $csvContent);
|
||||
|
|
@ -254,6 +254,7 @@ class DatevExportService
|
|||
'shopping_order.country.country',
|
||||
'shopping_order.shopping_user',
|
||||
'shopping_order.auth_user.account',
|
||||
'shopping_order.shopping_collect_order',
|
||||
])
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
|
|
@ -289,13 +290,15 @@ class DatevExportService
|
|||
|
||||
// Tax-Split vorhanden? -> Mehrere Zeilen pro Steuersatz
|
||||
if ($order->tax_split && is_array($order->tax_split) && count($order->tax_split) > 0) {
|
||||
$netSplit = $this->resolveNetSplit($order);
|
||||
|
||||
foreach ($order->tax_split as $taxRate => $taxAmount) {
|
||||
$taxRate = intval($taxRate);
|
||||
$taxAmountFloat = $this->parseNumber($taxAmount);
|
||||
$taxAmountFloat = $this->parseNumber($taxAmount, 'ek_tax');
|
||||
$netAmount = 0;
|
||||
|
||||
if ($order->net_split && isset($order->net_split[$taxRate])) {
|
||||
$netAmount = $this->parseNumber($order->net_split[$taxRate]);
|
||||
if ($netSplit && isset($netSplit[$taxRate])) {
|
||||
$netAmount = $this->parseNumber($netSplit[$taxRate], 'ek_net');
|
||||
}
|
||||
|
||||
$grossAmount = round($netAmount + $taxAmountFloat, 2);
|
||||
|
|
@ -389,7 +392,7 @@ class DatevExportService
|
|||
|
||||
// Steuerstatus des Beraters ermitteln
|
||||
$taxStatus = $this->determineCommissionTaxStatus($account);
|
||||
$buSchluessel = config('datev.commission_tax_keys.' . $taxStatus, 9);
|
||||
$buSchluessel = config('datev.commission_tax_keys.'.$taxStatus, 9);
|
||||
|
||||
// USt-ID für Reverse Charge
|
||||
$euUstid = null;
|
||||
|
|
@ -487,6 +490,7 @@ class DatevExportService
|
|||
'shopping_order.country.country',
|
||||
'shopping_order.shopping_user',
|
||||
'shopping_order.auth_user.account',
|
||||
'shopping_order.shopping_collect_order',
|
||||
])
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
|
|
@ -500,7 +504,7 @@ class DatevExportService
|
|||
}
|
||||
|
||||
$gegenkonto = $this->determineCounterAccountForOrder($order);
|
||||
$buchungstext = 'STORNO ' . $this->buildBuchungstext($order);
|
||||
$buchungstext = 'STORNO '.$this->buildBuchungstext($order);
|
||||
$buchungstext = mb_substr($buchungstext, 0, 60);
|
||||
$belegdatum = $this->parseBelegdatum($invoice);
|
||||
|
||||
|
|
@ -512,13 +516,15 @@ class DatevExportService
|
|||
$hasValidUstid = ! empty($euUstid);
|
||||
|
||||
if ($order->tax_split && is_array($order->tax_split) && count($order->tax_split) > 0) {
|
||||
$netSplit = $this->resolveNetSplit($order);
|
||||
|
||||
foreach ($order->tax_split as $taxRate => $taxAmount) {
|
||||
$taxRate = intval($taxRate);
|
||||
$taxAmountFloat = $this->parseNumber($taxAmount);
|
||||
$taxAmountFloat = $this->parseNumber($taxAmount, 'ek_tax');
|
||||
$netAmount = 0;
|
||||
|
||||
if ($order->net_split && isset($order->net_split[$taxRate])) {
|
||||
$netAmount = $this->parseNumber($order->net_split[$taxRate]);
|
||||
if ($netSplit && isset($netSplit[$taxRate])) {
|
||||
$netAmount = $this->parseNumber($netSplit[$taxRate], 'ek_net');
|
||||
}
|
||||
|
||||
$grossAmount = round($netAmount + $taxAmountFloat, 2);
|
||||
|
|
@ -598,18 +604,18 @@ class DatevExportService
|
|||
}
|
||||
|
||||
// Zeile 1: Header
|
||||
$output .= $this->buildHeaderLine($export) . $lineEnding;
|
||||
$output .= $this->buildHeaderLine($export).$lineEnding;
|
||||
|
||||
// Zeile 2: Spaltenüberschriften
|
||||
$output .= implode($delimiter, self::COLUMN_HEADERS) . $lineEnding;
|
||||
$output .= implode($delimiter, self::COLUMN_HEADERS).$lineEnding;
|
||||
|
||||
// Zeile 3+: Datenzeilen
|
||||
$lines = $export->lines()->orderBy('line_number')->get();
|
||||
foreach ($lines as $line) {
|
||||
if ($line->row_csv) {
|
||||
$output .= $line->row_csv . $lineEnding;
|
||||
$output .= $line->row_csv.$lineEnding;
|
||||
} else {
|
||||
$output .= $this->renderCsvRow($line->toArray()) . $lineEnding;
|
||||
$output .= $this->renderCsvRow($line->toArray()).$lineEnding;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -722,7 +728,7 @@ class DatevExportService
|
|||
'user_id' => $line['user_id'] ?? null,
|
||||
'belegfeld1' => $line['belegfeld1'] ?? null,
|
||||
];
|
||||
$lineRef = ($meta['source_type'] ?? '?') . ' #' . ($meta['source_id'] ?? '?');
|
||||
$lineRef = ($meta['source_type'] ?? '?').' #'.($meta['source_id'] ?? '?');
|
||||
|
||||
if (empty($line['belegdatum'])) {
|
||||
$errors[] = $this->buildValidationEntry("{$lineRef}: Belegdatum fehlt.", $meta);
|
||||
|
|
@ -790,7 +796,7 @@ class DatevExportService
|
|||
|
||||
// Gruppierte Zusammenfassung nach Konto + BU
|
||||
$grouped = $allLines->groupBy(function ($line) {
|
||||
return $line['konto'] . '-' . $line['bu_schluessel'] . '-' . $line['soll_haben'];
|
||||
return $line['konto'].'-'.$line['bu_schluessel'].'-'.$line['soll_haben'];
|
||||
})->map(function ($group, $key) {
|
||||
$first = $group->first();
|
||||
|
||||
|
|
@ -869,7 +875,7 @@ class DatevExportService
|
|||
// Zusammengesetzter Key aus clearingtype + wallettype
|
||||
$key = $payment->clearingtype;
|
||||
if ($payment->wallettype) {
|
||||
$key .= '_' . $payment->wallettype;
|
||||
$key .= '_'.$payment->wallettype;
|
||||
}
|
||||
|
||||
$map = config('datev.counteraccount_map', []);
|
||||
|
|
@ -984,7 +990,7 @@ class DatevExportService
|
|||
{
|
||||
if ($order->shopping_user) {
|
||||
$name = trim(
|
||||
($order->shopping_user->billing_lastname ?? '') . ' ' .
|
||||
($order->shopping_user->billing_lastname ?? '').' '.
|
||||
($order->shopping_user->billing_firstname ?? '')
|
||||
);
|
||||
|
||||
|
|
@ -1005,7 +1011,7 @@ class DatevExportService
|
|||
$parts = [];
|
||||
|
||||
if ($account) {
|
||||
$name = trim(($account->last_name ?? '') . ' ' . ($account->first_name ?? ''));
|
||||
$name = trim(($account->last_name ?? '').' '.($account->first_name ?? ''));
|
||||
if (! empty($name)) {
|
||||
$parts[] = $name;
|
||||
}
|
||||
|
|
@ -1013,7 +1019,7 @@ class DatevExportService
|
|||
|
||||
$userId = $user?->id ?? $creditUserId;
|
||||
if ($userId) {
|
||||
$parts[] = 'Nr.' . $userId;
|
||||
$parts[] = 'Nr.'.$userId;
|
||||
}
|
||||
|
||||
$text = implode('; ', $parts);
|
||||
|
|
@ -1038,20 +1044,60 @@ class DatevExportService
|
|||
return Carbon::create($invoice->year, $invoice->month, 1)->format('Y-m-d');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt den Netto-Split. Historische Sammelrechnungen haben ihn teils nur
|
||||
* an der ShoppingCollectOrder, nicht an der erzeugten ShoppingOrder.
|
||||
*/
|
||||
private function resolveNetSplit($order): ?array
|
||||
{
|
||||
if ($order->net_split && is_array($order->net_split)) {
|
||||
return $order->net_split;
|
||||
}
|
||||
|
||||
$collectOrderNetSplit = $order->shopping_collect_order?->net_split;
|
||||
|
||||
if ($collectOrderNetSplit && is_array($collectOrderNetSplit)) {
|
||||
return $collectOrderNetSplit;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst einen formatierten Zahlenwert (z.B. "5.00" oder "5,00") zu float.
|
||||
* Behandelt sowohl einfache Werte als auch tax_split Arrays (homeparty).
|
||||
*/
|
||||
private function parseNumber($value): float
|
||||
private function parseNumber($value, ?string $preferredArrayKey = null): float
|
||||
{
|
||||
if (is_array($value)) {
|
||||
// Homeparty tax_split Format: ['vk_tax' => '5.00', 'ek_tax' => '2.00']
|
||||
return floatval(str_replace(',', '.', $value['vk_tax'] ?? 0));
|
||||
$arrayKeys = $this->getSplitArrayKeys($preferredArrayKey);
|
||||
|
||||
foreach ($arrayKeys as $arrayKey) {
|
||||
if (array_key_exists($arrayKey, $value)) {
|
||||
return $this->parseNumber($value[$arrayKey]);
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return floatval(str_replace(',', '.', $value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function getSplitArrayKeys(?string $preferredArrayKey): array
|
||||
{
|
||||
return match ($preferredArrayKey) {
|
||||
'ek_tax' => ['ek_tax', 'vk_tax', 'ek_net', 'vk_net'],
|
||||
'ek_net' => ['ek_net', 'vk_net', 'ek_tax', 'vk_tax'],
|
||||
'vk_tax' => ['vk_tax', 'ek_tax', 'vk_net', 'ek_net'],
|
||||
'vk_net' => ['vk_net', 'ek_net', 'vk_tax', 'ek_tax'],
|
||||
default => ['vk_tax', 'vk_net', 'ek_tax', 'ek_net'],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Escaped ein CSV-Feld (Semikolon, Anführungszeichen, Newlines).
|
||||
*/
|
||||
|
|
@ -1061,7 +1107,7 @@ class DatevExportService
|
|||
$value = str_replace('"', '""', $value);
|
||||
|
||||
if (str_contains($value, ';') || str_contains($value, '"') || str_contains($value, "\n")) {
|
||||
return '"' . $value . '"';
|
||||
return '"'.$value.'"';
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
|
@ -1074,6 +1120,6 @@ class DatevExportService
|
|||
{
|
||||
$monthPad = str_pad($month, 2, '0', STR_PAD_LEFT);
|
||||
|
||||
return "EXTF_Buchungsstapel_{$year}_{$monthPad}_" . date('YmdHis') . '.csv';
|
||||
return "EXTF_Buchungsstapel_{$year}_{$monthPad}_".date('YmdHis').'.csv';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue