mivita/app/Http/Controllers/SalesController.php
Kevin 68d230d4e5 Stripe: Kundendaten, Rechnungsnummer, manueller Zahlungseingang
Rueckmeldung der Buchhaltung und des Stripe-Calls (dev/2026-08-26-stripe/):
Stripe kannte bisher nur die E-Mail des Kunden, Zahlungen waren im Dashboard
weder ueber die Bestell- noch ueber die Rechnungsnummer auffindbar, und ein
Zahlungseingang mit falschem Verwendungszweck zwang die Buchhaltung dazu, die
Rechnung von Hand zu schreiben.

- SE1: Der Checkout haengt einen Stripe-Customer mit Name, E-Mail und
  Rechnungsanschrift an die Session (customer statt customer_email); die ID
  liegt an shopping_users und wird wiederverwendet. Namensformat "Vorname
  Nachname (Firma)", reine Firmenkunden unter der Firma. Faellt die
  Customer-Anlage aus, laeuft der Checkout auf dem bisherigen Weg weiter.
  DSGVO-Loeschungen entfernen jetzt auch das Customer-Objekt; die Charges
  bleiben als Buchungsbeleg bestehen.
- SE2: StripeMetadataBuilder als einzige Quelle fuer Metadaten und die
  description (Bestellnummer, Referenz, Kundenname) - damit greift die
  Dashboard-Suche, die bisher nichts Lesbares zu fassen bekam.
- SE3: Die Rechnungsnummer entsteht erst nach der Zahlung und wird daher per
  Job am PaymentIntent nachgetragen (Vorab-Reservierung wuerde Luecken im
  Nummernkreis erzeugen). stripe:backfill-invoice-metadata traegt sie
  rueckwirkend ab dem Go-Live 2026-07-10 nach.
- SE4: Neuer Admin-Button "Zahlungseingang erfassen" an jeder unbezahlten
  Bestellung - bisher gab es ihn nur bei Rechnungskauf, und er setzte nur
  paid, ohne Rechnung, Punkte, Abo-Aktivierung oder Mail. Der neue Weg laeuft
  durch paymentStatusPaidAction, verlangt den Verwendungszweck als Beleg und
  laesst einen abweichenden Betrag nur mit ausdruecklicher Bestaetigung zu.
- S6.4 (aus dem Juli-Plan vorgezogen): stripe:sync-payment-status gleicht
  offene Zahlungen aktiv gegen die API ab. Aufgefallen beim Testlauf - ein nie
  zugestellter Webhook war bisher nicht reparierbar, das Geld lag bei Stripe
  waehrend die Bestellung auf offen stand. Laeuft stuendlich im Scheduler.
- 110 Tests (Pest) fuer alle Pfade, Stripe-Client durchgehend gemockt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 09:04:28 +00:00

523 lines
24 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Exceptions\ManualPaymentException;
use App\Http\Requests\Payment\RecordManualPaymentRequest;
use App\Models\PaymentTransaction;
use App\Models\ShoppingOrder;
use App\Models\ShoppingPayment;
use App\Models\ShoppingUser;
use App\Models\UserShop;
use App\Repositories\InvoiceRepository;
use App\Services\AboExternalCompletionService;
use App\Services\BusinessPlan\SalesPointsVolume;
use App\Services\CustomerPriority;
use App\Services\ManualPaymentService;
use App\Services\Payment;
use App\Services\Payment\Stripe\StripeInvoiceReferenceService;
use Illuminate\Http\RedirectResponse;
use Request;
class SalesController extends Controller
{
public function __construct()
{
$this->middleware('admin');
}
public function users()
{
if (Request::get('reset') === 'filter') {
return redirect(route('admin_sales_users'));
}
$data = [];
return view('admin.sales.users', $data);
}
public function usersDetail($id)
{
$ShoppingOrder = ShoppingOrder::find($id);
if ($ShoppingOrder->payment_for === 6 || $ShoppingOrder->payment_for === 7) {
return redirect(route('admin_sales_customers_detail', [$ShoppingOrder->id]));
abort(403, 'Kundenbestellung');
}
/*if($ShoppingOrder->shipped === 0){
$ShoppingOrder->shipped = 1;
$ShoppingOrder->save();
}*/
$data = [
'shopping_order' => $ShoppingOrder,
'isAdmin' => true,
'isView' => 'sales_user',
];
return view('admin.sales.user_detail', $data);
}
public function usersStore($id)
{
exit('keine funktion');
$data = [
'shopping_order' => ShoppingOrder::find($id),
'isAdmin' => true,
];
return view('admin.sales.user_detail', $data);
}
public function usersDatatable()
{
$query = ShoppingOrder::with('shopping_user', 'user_shop', 'shopping_payments')->select('shopping_orders.*')->where('shopping_orders.auth_user_id', '!=', null);
return \DataTables::eloquent($query)
->addColumn('id', function (ShoppingOrder $ShoppingOrder) {
return '<a href="'.route('admin_sales_users_detail', [$ShoppingOrder->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
})
->addColumn('created_at', function (ShoppingOrder $ShoppingOrder) {
return $ShoppingOrder->created_at->format('d.m.Y');
})
->addColumn('txaction', function (ShoppingOrder $ShoppingOrder) {
return Payment::getShoppingOrderBadge($ShoppingOrder);
})
->addColumn('total_shipping', function (ShoppingOrder $ShoppingOrder) {
return '<span class="no-line-break">'.$ShoppingOrder->getFormattedTotalShipping().' €</span>';
})
->addColumn('payment', function (ShoppingOrder $ShoppingOrder) {
return $ShoppingOrder->getLastShoppingPayment('getPaymentType');
})
->addColumn('shipped', function (ShoppingOrder $ShoppingOrder) {
if ($ShoppingOrder->payment_for === 8) {
return '<button type="button" class="btn btn-xs btn-info btn-round" data-toggle="modal" data-target="#modals-load-content"
data-id="'.$ShoppingOrder->id.'"
data-action="shop-user-order-shipping-detail"
data-back=""
data-modal="modal-xl"
data-init_from="user"
data-route="'.route('modal_load').'"><span class="fa fa-eye"></span></button>';
}
return '<span class="badge badge-pill badge-'.$ShoppingOrder->getShippedColor().'">'.$ShoppingOrder->getShippedType().'</span>';
})
->addColumn('dhl_button', function (ShoppingOrder $ShoppingOrder) {
return '<button type="button" class="btn btn-xs btn-'.($ShoppingOrder->hasDhlShipments() ? 'primary' : 'secondary').'" data-toggle="modal" data-target="#modals-load-content"
data-id="'.$ShoppingOrder->id.'"
data-action="create-dhl-shipment"
data-route="'.route('modal_load').'"><span class="fa fa-shipping-fast"></span></button>';
})
->addColumn('payment_for', function (ShoppingOrder $ShoppingOrder) {
return Payment::getPaymentForBadge($ShoppingOrder);
})
->addColumn('invoice', function (ShoppingOrder $ShoppingOrder) {
return $ShoppingOrder->isInvoice() ? '<span class="no-line-break"><a href="'.route('storage_file', [$ShoppingOrder->id, 'invoice', 'download']).'" class="btn btn-primary btn-xs"><i class="fa fa-download"></i></a>
<a href="'.route('storage_file', [$ShoppingOrder->id, 'invoice', 'stream']).'" target="_blank" class="btn btn-warning btn-xs"><i class="fa fa-eye"></i></a></span>' : '-';
})
->addColumn('reference', function (ShoppingOrder $ShoppingOrder) {
return $ShoppingOrder->getLastShoppingPayment('reference');
})
->addColumn('orders', function (ShoppingOrder $ShoppingOrder) {
return $ShoppingOrder->shopping_user ? $ShoppingOrder->shopping_user->orders : '';
})
->addColumn('user_shop_id', function (ShoppingOrder $ShoppingOrder) {
return $ShoppingOrder->user_shop ? '<a href="'.$ShoppingOrder->user_shop->getSubdomain(false).'" target="_blank">'.$ShoppingOrder->user_shop->getSubdomain(false).'</span>' : '';
})
->addColumn('auth_user_shop', function (ShoppingOrder $ShoppingOrder) {
$auth_user_shop = UserShop::whereUserId($ShoppingOrder->auth_user_id)->first();
return $auth_user_shop ? '<a href="'.$auth_user_shop->getSubdomain(false).'" target="_blank">'.$auth_user_shop->getSubdomain(false).'</span>' : '-';
})
->orderColumn('id', 'id $1')
->orderColumn('txaction', 'txaction $1')
->orderColumn('user_shop_id', 'user_shop_id $1')
->orderColumn('shipped', 'shipped $1')
->orderColumn('total_shipping', 'total_shipping $1')
->orderColumn('payment_for', 'payment_for $1')
->rawColumns(['id', 'dhl_button', 'txaction', 'user_shop_id', 'auth_user_shop', 'payment_for', 'total_shipping', 'invoice', 'shipped'])
->make(true);
}
public function customers()
{
if (Request::get('reset') === 'filter') {
set_user_attr('filter_user_shop_id', null);
set_user_attr('filter_txaction', null);
set_user_attr('filter_member_id', null);
return redirect(route('admin_sales_customers'));
}
$filter_user_shops = ShoppingOrder::select('user_shops.id', 'user_shops.slug')
->join('user_shops', 'shopping_orders.user_shop_id', '=', 'user_shops.id')
->orderBy('user_shops.slug')
->distinct()
->pluck('slug', 'id')
->toArray();
$filter_members = ShoppingOrder::join('users', 'member_id', '=', 'users.id')->groupBy('member_id')->join('user_accounts', 'account_id', '=', 'user_accounts.id')->select('users.id', 'users.email', 'user_accounts.first_name', 'user_accounts.last_name')->get();
// ->pluck('email', 'id')->unique()->toArray();
$data = [
'filter_user_shops' => $filter_user_shops,
'filter_members' => $filter_members,
];
return view('admin.sales.customers', $data);
}
public function customersDetail($id)
{
$ShoppingOrder = ShoppingOrder::find($id);
if (! $ShoppingOrder) {
abort(404);
}
if ($ShoppingOrder->payment_for !== 6 && $ShoppingOrder->payment_for !== 7) {
return redirect(route('admin_sales_users_detail', [$ShoppingOrder->id]));
abort(403, 'Beraterbestellung');
}
/*
if($ShoppingOrder->shipped === 0){
$ShoppingOrder->shipped = 1;
$ShoppingOrder->save();
}
*/
$data = [
'shopping_order' => $ShoppingOrder,
'isAdmin' => true,
'isView' => 'sales_customer',
];
return view('admin.sales.customer_detail', $data);
}
public function customersStore($id, AboExternalCompletionService $aboExternalCompletion)
{
$data = Request::all();
$change_member_error = false;
if (($data['action'] ?? null) === 'shopping-order-abo-extern-done') {
$shopping_order = ShoppingOrder::findOrFail($data['id']);
$result = $aboExternalCompletion->complete($shopping_order);
\Session()->flash($result['success'] ? 'alert-success' : 'alert-error', $result['message']);
return redirect(route('admin_sales_customers_detail', [$shopping_order->id]));
}
if ($data['action'] === 'shopping-order-change-member') {
if (! isset($data['change_member_key']) || $data['change_member_key'] !== config('mivita.edit_data_pass')) {
$change_member_error = 'Das Passwort ist falsch.';
} else {
// change
$shopping_order = ShoppingOrder::findOrFail($data['id']);
CustomerPriority::newMemberForOrder($shopping_order, $data['change_member_id'], $data['customer_set_member_for']);
\Session()->flash('alert-save', true);
return redirect(route('admin_sales_customers_detail', [$shopping_order->id]));
}
}
if ($data['action'] === 'shopping-user-is-like-member') {
if (! isset($data['change_member_key']) || $data['change_member_key'] !== config('mivita.edit_data_pass')) {
\Session()->flash('alert-error', 'Das Passwort ist falsch.');
return redirect($data['back']);
} else {
if (! isset($data['is_like_shopping_user_id'])) {
\Session()->flash('alert-error', 'Keine Änderung ausgewählt');
return redirect($data['back']);
}
$shopping_user = ShoppingUser::findOrFail($data['id']);
$set_like_shopping_user = ShoppingUser::findOrFail($data['is_like_shopping_user_id']);
$send_member_mail = isset($data['send_member_mail']) ? true : false;
$change_shopping_user = isset($data['change_shopping_user']) ? true : false;
// Mail send in setIsLike
CustomerPriority::setIsLike($shopping_user, $set_like_shopping_user, $send_member_mail, $change_shopping_user);
\Session()->flash('alert-save', true);
return redirect($data['back']);
}
}
if ($data['action'] === 'shopping-order-change-points') {
if (! isset($data['change_member_key']) || $data['change_member_key'] !== config('mivita.edit_data_pass')) {
\Session()->flash('alert-error', 'Das Passwort ist falsch.');
return back();
} else {
if (! isset($data['change_points'])) {
\Session()->flash('alert-error', 'Keine Änderung ausgewählt');
return back();
}
$shopping_order = ShoppingOrder::findOrFail($data['id']);
SalesPointsVolume::changeSalesPointsVolumeUser($shopping_order, $data['change_member_id']);
return redirect(route('admin_sales_customers_detail', [$shopping_order->id]));
}
}
$data = [
'change_member_error' => $change_member_error,
'shopping_order' => ShoppingOrder::find($id),
'isAdmin' => true,
'isView' => 'sales_customer',
];
return view('admin.sales.customer_detail', $data);
}
public function customersDatatable()
{
$query = ShoppingOrder::with('shopping_user')->select('shopping_orders.*')->where('shopping_orders.auth_user_id', null);
set_user_attr('filter_user_shop_id', Request::get('filter_user_shop_id'));
if (Request::get('filter_user_shop_id') != '') {
$query->where('user_shop_id', '=', Request::get('filter_user_shop_id'));
}
set_user_attr('filter_txaction', Request::get('filter_txaction'));
if (Request::get('filter_txaction') != '') {
if (Request::get('filter_txaction') === 'NULL') {
$query->where('txaction', '=', null);
} else {
$query->where('txaction', '=', Request::get('filter_txaction'));
}
}
set_user_attr('filter_member_id', Request::get('filter_member_id'));
if (Request::get('filter_member_id') != '') {
$query->where('member_id', '=', Request::get('filter_member_id'));
}
return \DataTables::eloquent($query)
->addColumn('id', function (ShoppingOrder $ShoppingOrder) {
return '<a href="'.route('admin_sales_customers_detail', [$ShoppingOrder->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
})
->addColumn('created_at', function (ShoppingOrder $ShoppingOrder) {
return $ShoppingOrder->created_at->format('d.m.Y');
})
->addColumn('txaction', function (ShoppingOrder $ShoppingOrder) {
return Payment::getShoppingOrderBadge($ShoppingOrder);
})
->addColumn('total_shipping', function (ShoppingOrder $ShoppingOrder) {
return '<span class="no-line-break">'.$ShoppingOrder->getFormattedTotalShipping().' €</span>';
})
->addColumn('payment', function (ShoppingOrder $ShoppingOrder) {
if ($ShoppingOrder->txaction === 'extern_paid') {
$shopping_oder_id = isset($ShoppingOrder->api_notice['shopping_order_id']) ? $ShoppingOrder->api_notice['shopping_order_id'] : null;
if ($shopping_oder_id) {
return '<a class="btn btn-xs btn-default btn-round" href="'.route('admin_sales_users_detail', [$shopping_oder_id]).'"><i class="fa fa-check fa-check-circle-o"> '.$shopping_oder_id.'</a>';
}
}
return $ShoppingOrder->getLastShoppingPayment('getPaymentType');
})
->addColumn('shipped', function (ShoppingOrder $ShoppingOrder) {
return '<span class="badge badge-pill badge-'.$ShoppingOrder->getShippedColor().'">'.$ShoppingOrder->getShippedType().'</span>';
})
->addColumn('dhl_button', function (ShoppingOrder $ShoppingOrder) {
return '<button type="button" class="btn btn-xs btn-'.($ShoppingOrder->hasDhlShipments() ? 'primary' : 'secondary').'" data-toggle="modal" data-target="#modals-load-content"
data-id="'.$ShoppingOrder->id.'"
data-action="create-dhl-shipment"
data-route="'.route('modal_load').'"><span class="fa fa-shipping-fast"></span></button>';
})
->addColumn('payment_for', function (ShoppingOrder $ShoppingOrder) {
return Payment::getPaymentForBadge($ShoppingOrder);
})
->addColumn('invoice', function (ShoppingOrder $ShoppingOrder) {
if (($ShoppingOrder->txaction === 'extern' || $ShoppingOrder->txaction === 'extern_paid') && $ShoppingOrder->wp_invoice_path) {
return '<span class="no-line-break"><a href="'.$ShoppingOrder->wp_invoice_path.'" class="btn btn-secondary btn-xs"><i class="fa fa-external-link-alt"></i> <i class="fa fa-download"></i></a> </div>';
}
return $ShoppingOrder->isInvoice() ? '<span class="no-line-break"><a href="'.route('storage_file', [$ShoppingOrder->id, 'invoice', 'download']).'" class="btn btn-primary btn-xs"><i class="fa fa-download"></i></a>
<a href="'.route('storage_file', [$ShoppingOrder->id, 'invoice', 'stream']).'" target="_blank" class="btn btn-warning btn-xs"><i class="fa fa-eye"></i></a></span>' : '-';
})
->addColumn('reference', function (ShoppingOrder $ShoppingOrder) {
return $ShoppingOrder->getLastShoppingPayment('reference');
})
->addColumn('member_id', function (ShoppingOrder $ShoppingOrder) {
if ($ShoppingOrder->member_id && $ShoppingOrder->member) {
return $ShoppingOrder->member ? '<a href="'.route('admin_lead_edit', [$ShoppingOrder->member_id]).'">'.$ShoppingOrder->member->getFullName().'</a>' : 'gelöscht';
}
if ($ShoppingOrder->shopping_user && $ShoppingOrder->shopping_user->is_like) {
return '<button type="button" class="btn btn-xs btn-outline-info" data-toggle="modal" data-target="#modals-load-content"
data-id="'.$ShoppingOrder->shopping_user->id.'"
data-action="shopping-user-is-like-member"
data-back="'.route('admin_sales_customers').'"
data-modal="modal-xl"
data-route="'.route('modal_load').'"><span class="fa fa-edit"></span> Berater zuordnen</button>';
}
return '';
})
->addColumn('user_shop_id', function (ShoppingOrder $ShoppingOrder) {
return $ShoppingOrder->user_shop ? '<a href="'.$ShoppingOrder->user_shop->getSubdomain(false).'" target="_blank">'.$ShoppingOrder->user_shop->getSubdomain(false).'</span>' : '';
})
->orderColumn('id', 'id $1')
->orderColumn('txaction', 'txaction $1')
->orderColumn('user_shop_id', 'user_shop_id $1')
->orderColumn('member_id', 'member_id $1')
->orderColumn('shipped', 'shipped $1')
->orderColumn('payment_for', 'payment_for $1')
->orderColumn('total_shipping', 'total_shipping $1')
->rawColumns(['id', 'dhl_button', 'member_id', 'txaction', 'user_shop_id', 'payment_for', 'payment', 'total_shipping', 'invoice', 'shipped'])
->make(true);
}
public function store()
{
$data = Request::all();
if (! isset($data['id'])) {
abort(404);
}
if (isset($data['action'])) {
if ($data['action'] === 'store_shipped' && isset($data['shipped'])) {
$shopping_order = ShoppingOrder::findOrFail($data['id']);
$shopping_order->shipped = $data['shipped'];
$shopping_order->save();
}
if ($data['action'] === 'store_txaction' && isset($data['txaction']) && isset($data['payment_id'])) {
$shopping_order = ShoppingOrder::findOrFail($data['id']);
$shopping_payment = ShoppingPayment::findOrFail($data['payment_id']);
PaymentTransaction::create([
'shopping_payment_id' => $shopping_payment->id,
'request' => 'transaction',
'txid' => 0,
'userid' => 0,
'status' => 'FNCMIV',
'transmitted_data' => null,
'txaction' => $data['txaction'],
'mode' => $shopping_payment->mode,
]);
$shopping_order->txaction = $data['txaction'];
$shopping_order->paid = true;
$shopping_order->save();
$shopping_payment->txaction = $data['txaction'];
$shopping_payment->save();
// TODO can send MAIL
// Bei Zahlung auf Rechnung wurde die Rechnung schon erstellt,
// wenn muss hier die Storno erstellt werden
// Payment::paymentStatusSendMail($shopping_order, $shopping_payment, $data);
}
}
if (isset($data['back'])) {
return redirect($data['back']);
}
}
/**
* Zahlungseingang manuell erfassen (Plan dev/2026-08-26-stripe/, SE4).
*
* Für Zahlungen, die kein Anbieter melden kann — vor allem Vorkasse mit
* falschem Verwendungszweck. Löst denselben Paid-Pfad aus wie ein
* Anbieter-Callback: Rechnung, Umsatz/Punkte, Abo-Aktivierung und Mail.
*/
public function recordManualPayment(
RecordManualPaymentRequest $request,
ManualPaymentService $manualPayments,
): RedirectResponse {
$shoppingOrder = ShoppingOrder::findOrFail($request->integer('id'));
$shoppingPayment = ShoppingPayment::findOrFail($request->integer('payment_id'));
$back = $request->input('back') ?: route('admin_sales_users_detail', [$shoppingOrder->id]);
try {
$manualPayments->recordIncomingPayment(
$shoppingOrder,
$shoppingPayment,
$request->toServiceData(),
\Auth::user(),
);
\Session()->flash('alert-success', __('payment.manual_success'));
} catch (ManualPaymentException $e) {
\Session()->flash('alert-error', $e->getMessage());
} catch (\Throwable $e) {
\Log::channel('payment')->error('Manueller Zahlungseingang fehlgeschlagen: '.$e->getMessage(), [
'shopping_order_id' => $shoppingOrder->id,
'shopping_payment_id' => $shoppingPayment->id,
]);
\Session()->flash('alert-error', __('payment.manual_error_general'));
}
return redirect($back);
}
/*
Manuelle Rechnung erstellen.*/
public function invoice()
{
$data = Request::all();
if (! isset($data['id'])) {
abort(404);
}
if (isset($data['action'])) {
if ($data['action'] === 'create_invoice') {
$shopping_order = ShoppingOrder::findOrFail($data['id']);
$invoice_repo = new InvoiceRepository($shopping_order);
if ($shopping_order->isInvoice()) {
$invoice_repo->update($data);
} else {
$invoice_repo->createAndSalesVolume($data);
}
// Rechnungsnummer an eine Stripe-Zahlung nachtragen (SE3)
$shopping_order->refresh();
StripeInvoiceReferenceService::dispatchForOrder($shopping_order);
if (isset($data['view']) && $data['view'] === 'sales_customer') {
return redirect(route('admin_sales_customers_detail', [$shopping_order->id]));
}
return redirect(route('admin_sales_users_detail', [$shopping_order->id]));
}
}
}
/**
* Stornorechnung erstellen mit Punktekorrektur
*/
public function invoiceCancellation()
{
$data = Request::all();
if (! isset($data['id'])) {
abort(404);
}
if (isset($data['action']) && $data['action'] === 'create_cancellation_invoice') {
$shopping_order = ShoppingOrder::findOrFail($data['id']);
// Prüfen ob Rechnung existiert
if (! $shopping_order->isInvoice()) {
\Session()->flash('alert-error', 'Es existiert keine Rechnung für diese Bestellung.');
return redirect($data['back'] ?? route('admin_sales_users_detail', [$shopping_order->id]));
}
// Prüfen ob bereits storniert
if ($shopping_order->isCancellationInvoice()) {
\Session()->flash('alert-error', 'Diese Rechnung wurde bereits storniert.');
return redirect($data['back'] ?? route('admin_sales_users_detail', [$shopping_order->id]));
}
try {
$invoice_repo = new InvoiceRepository($shopping_order);
$cancellation_invoice = $invoice_repo->createCancellation($data);
\Session()->flash('alert-success', 'Stornorechnung wurde erfolgreich erstellt und Punkte wurden korrigiert.');
} catch (\Exception $e) {
\Log::error('Fehler beim Erstellen der Stornorechnung: '.$e->getMessage(), [
'order_id' => $shopping_order->id,
'exception' => $e,
]);
\Session()->flash('alert-error', 'Fehler beim Erstellen der Stornorechnung: '.$e->getMessage());
}
return redirect($data['back'] ?? route('admin_sales_users_detail', [$shopping_order->id]));
}
abort(404);
}
}