587 lines
25 KiB
PHP
587 lines
25 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Bo;
|
|
|
|
use App\Enums\Order\DeliveryModeEnum;
|
|
use App\Enums\Order\DeliveryStatus;
|
|
use App\Enums\Order\PaymentModeEnum;
|
|
use App\Enums\Order\TransactionStatus;
|
|
use App\Enums\Order\TransactionType;
|
|
use App\Enums\Order\ValidationStatus;
|
|
use App\Events\OrderIsRefunded;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\Bo\Orders\ChangeOrderDeliveryStatusRequest;
|
|
use App\Http\Requests\Bo\Orders\ChangeOrderValidationStatusRequest;
|
|
use App\Http\Requests\Bo\PrepareOrderRequest;
|
|
use App\Http\Requests\Bo\ResolveOrderRequest;
|
|
use App\Jobs\Order\RefundOrder;
|
|
use App\Lib\Helpers\ToolboxHelper;
|
|
use App\Models\Payment\Transaction;
|
|
use App\Models\Shop\Order;
|
|
use Gloudemans\Shoppingcart\CartItem;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\View\View;
|
|
|
|
class OrderController extends Controller
|
|
{
|
|
/**
|
|
* If true the index action will only display today orders
|
|
* @var false
|
|
*/
|
|
private $indexModeTodayOnly = false;
|
|
|
|
/**
|
|
* Display a listing of the resource.
|
|
*
|
|
* @param \Illuminate\Http\Request $request
|
|
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
|
|
*/
|
|
public function index(Request $request): View
|
|
{
|
|
/** @var \Illuminate\Database\Eloquent\Builder $orders */
|
|
$orders = Order::query();
|
|
|
|
if ($request->search) {
|
|
$this->searchQuery(
|
|
$orders,
|
|
$request->search,
|
|
function (Builder $q) use ($request) {
|
|
$q->orWhereHas('transactions', function (Builder $q) use ($request) {
|
|
$foreignIds = \collect(PaymentModeEnum::toArray())
|
|
->map(function (PaymentModeEnum $paymentMode) use ($request) {
|
|
return Transaction::getPaymentForeignKeyName(
|
|
$paymentMode,
|
|
$request->search
|
|
);
|
|
});
|
|
$q->whereIn('foreign_id', $foreignIds)
|
|
->orWhereIn('foreign_refund_id', $foreignIds);
|
|
});
|
|
},
|
|
'reference',
|
|
'email',
|
|
);
|
|
}//end if
|
|
|
|
$this->sortQuery($orders);
|
|
$searchFields = trans('Réference,Email,Pay.Ref');
|
|
|
|
if ($this->indexModeTodayOnly) {
|
|
$orders->whereHas('timeslot', function (Builder $q) {
|
|
$q->whereDate('slot', \now()->today());
|
|
});
|
|
}
|
|
|
|
$orders->orderBy('created_at', 'DESC');
|
|
$orders->with(['deliveryMode', 'trashedTimeSlot.shop']);
|
|
$orders = $orders->paginate(10);
|
|
|
|
return view('back::pages.orders.index', compact('orders', 'searchFields'));
|
|
}
|
|
|
|
/**
|
|
* Display a listing of the resource.
|
|
*
|
|
* @param \Illuminate\Http\Request $request
|
|
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
|
|
*/
|
|
public function indexToday(Request $request): View
|
|
{
|
|
$this->indexModeTodayOnly = true;
|
|
/** @var \Illuminate\View\View $response */
|
|
$response = $this->callAction('index', ['request' => $request]);
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Display the specified resource.
|
|
*
|
|
* @param \App\Models\Shop\Order $order
|
|
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
|
|
*/
|
|
public function show(Order $order): View
|
|
{
|
|
$order->load('transactions');
|
|
return view('back::pages.orders.show', compact('order'));
|
|
}
|
|
|
|
/**
|
|
* Mettre a jour le status de validation d'une commande
|
|
*
|
|
* @param \App\Http\Requests\Bo\Orders\ChangeOrderValidationStatusRequest $request
|
|
* @param \App\Models\Shop\Order $order
|
|
* @return \Illuminate\Http\RedirectResponse
|
|
*/
|
|
public function changeValidationStatus(ChangeOrderValidationStatusRequest $request, Order $order): RedirectResponse
|
|
{
|
|
$this->authorize('changeValidationStatus', $order);
|
|
return DB::transaction(function () use ($request, $order) {
|
|
$order->validation_status = $request->validation_status;
|
|
switch ($request->validation_status) {
|
|
case ValidationStatus::canceled()->value:
|
|
$order->delivery_status = DeliveryStatus::canceled();
|
|
\session()->flash('bo.warning', trans('La commande est marquée comme annulée'));
|
|
// Fall through.
|
|
case ValidationStatus::validated()->value:
|
|
$order->saveOrFail();
|
|
return \response()->redirectToRoute('bo.orders.show', $order)->with('bo.success', trans(
|
|
':model a été mis à jour',
|
|
['model' => Str::singular(trans('models.classes.orders'))]
|
|
));
|
|
case ValidationStatus::pending()->value:
|
|
return \back()->with('bo.errors', \trans('Il faut choisir un statut pour cette commande.'));
|
|
default:
|
|
throw new \RuntimeException("Unhandled status `{$request->validation_status}`");
|
|
}//end switch
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Mettre a jour le status de livraison d'une commande
|
|
*
|
|
* @param \App\Http\Requests\Bo\Orders\ChangeOrderDeliveryStatusRequest $request
|
|
* @param \App\Models\Shop\Order $order
|
|
* @return \Illuminate\Http\RedirectResponse
|
|
*/
|
|
public function changeDeliveryStatus(ChangeOrderDeliveryStatusRequest $request, Order $order): RedirectResponse
|
|
{
|
|
$this->authorize('changeDeliveryStatus', $order);
|
|
return DB::transaction(function () use ($request, $order) {
|
|
$order->delivery_status = $request->delivery_status;
|
|
switch (true) {
|
|
// * La commande va être préparé ou est déja en cours de préparation
|
|
case $order->delivery_status->equals(DeliveryStatus::preparation()) or
|
|
$order->delivery_status->equals(DeliveryStatus::preparation()):
|
|
$order->delivery_status = DeliveryStatus::preparation();
|
|
$order->shipped_basket = '';
|
|
$order->saveOrFail();
|
|
return \redirect()->route('bo.orders.prepare', $order);
|
|
// * La commande est récupérée.
|
|
case $order->delivery_status->equals(DeliveryStatus::recovered()):
|
|
// * Déclarer la commande comme réglée si récupération à la boutique.
|
|
if ($order->deliveryMode->mode->equals(DeliveryModeEnum::inShop())) {
|
|
// ! There should be one and only one inShop transaction !
|
|
/** @var \App\Models\Payment\Transaction|null */
|
|
$inShopTransaction = $order->transactions()
|
|
->where('type', TransactionType::debit())
|
|
->where('payment_mode', PaymentModeEnum::inShop())
|
|
->latest()->first();
|
|
$paidTransactions = $order->transactions()->where('type', TransactionType::debit())
|
|
->where('status', TransactionStatus::validated())->count();
|
|
// * Check if there are not paid transactions then
|
|
// * we should have one pending inShop transaction.
|
|
if (!$paidTransactions and !$inShopTransaction) {
|
|
throw new \RuntimeException(
|
|
"There should be one inshop transaction for order '{$order->id}'"
|
|
);
|
|
}
|
|
if ($inShopTransaction) {
|
|
$inShopTransaction->status = TransactionStatus::validated();
|
|
$inShopTransaction->saveOrFail();
|
|
}
|
|
} else {
|
|
throw new \RuntimeException("Unhandled delivery mode other than in shop");
|
|
} //end if
|
|
// Fall through.
|
|
// * La commande est prête.
|
|
case $order->delivery_status->equals(DeliveryStatus::ready()):
|
|
$order->saveOrFail();
|
|
return \back()->with('bo.success', trans(
|
|
':model a été mis à jour',
|
|
['model' => Str::singular(trans('models.classes.orders'))]
|
|
));
|
|
default:
|
|
throw new \RuntimeException("Unhandled status `{$request->delivery_status}`");
|
|
}//end switch
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show the order resolve form
|
|
*
|
|
* @param \App\Models\Shop\Order $order
|
|
* @return \Illuminate\View\View
|
|
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
|
|
*/
|
|
public function resolve(Order $order): View
|
|
{
|
|
// phpcs:enable
|
|
$this->authorize('resolve', $order);
|
|
$transactionModel = $order->transactions()->where('needs_validation', true)->first();
|
|
|
|
// * Diff between Basket - (Delivered items - refunded transaction items)
|
|
$remainingItems = $order->getRemainingUnrefundedItems();
|
|
|
|
return \view('back::pages.orders.resolve', \compact('order', 'transactionModel', 'remainingItems'));
|
|
}
|
|
|
|
/**
|
|
* Validate order resolution
|
|
*
|
|
* @param \App\Http\Requests\Bo\ResolveOrderRequest $request
|
|
* @param \App\Models\Shop\Order $order
|
|
* @return \Illuminate\Http\RedirectResponse
|
|
*/
|
|
public function resolveSubmit(ResolveOrderRequest $request, Order $order): RedirectResponse
|
|
{
|
|
return DB::transaction(function () use ($request, $order) {
|
|
/** @var \App\Models\Payment\Transaction */
|
|
$transaction = $request->transaction;
|
|
/** @var \Illuminate\Support\Collection<object> */
|
|
$orderedItems = \collect($order->basket);
|
|
// * getRemainingUnrefundedItems Modifie les rowId originaux, il faut les restaurer pour la comparaison.
|
|
/** @var \Illuminate\Support\Collection<string,string> */
|
|
$refundedItems = $request->collect('items')->mapWithKeys(function (array $item) use ($orderedItems) {
|
|
$item = (object)$item;
|
|
$rowId = $orderedItems->where('id', '=', $item->index)->first()->rowId;
|
|
return [$rowId => $item->quantity];
|
|
});
|
|
/** @var \Illuminate\Support\Collection<object> */
|
|
$refundedTransactionsItems = $orderedItems->map(function (object $basketItem) use ($refundedItems) {
|
|
if ($refundedItems->has($basketItem->rowId)) {
|
|
$basketItem->qty = \intval($refundedItems->get($basketItem->rowId));
|
|
return $basketItem;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
$refundedTransactionsItems = \collect(ToolboxHelper::updateBasketAmounts(
|
|
$orderedItems,
|
|
$refundedItems->all()
|
|
));
|
|
|
|
// ? La transaction concerne des produits ?
|
|
if ($refundedTransactionsItems->count()) {
|
|
// * Estampiller les ligne avec la raison
|
|
$refundedTransactionsItems->map(function (object $basketItem) use ($request) {
|
|
$basketItem->name = "{$request->reason} - {$basketItem->name}";
|
|
return $basketItem;
|
|
});
|
|
}
|
|
|
|
$montantRembourse = \intval($refundedTransactionsItems->sum('subtotal'));
|
|
|
|
// ? Le montant des produits remboursé est inférieur a celui de la transaction.
|
|
if ($montantRembourse < \intval($transaction->amount->getAmount())) {
|
|
// * Créer un dummy product pour représenter la transaction.
|
|
$cartItemOptions = \array_merge(static::$cartItemOptionsSkel, [
|
|
'virtual' => true,
|
|
'price_ttc' => \intval($transaction->amount->getAmount()) - $montantRembourse,
|
|
]);
|
|
|
|
$cartItem = CartItem::fromArray([
|
|
'id' => \str_replace('.', '', \uniqid('', true)),
|
|
'name' => $request->reason,
|
|
'price' => \intval($transaction->amount->getAmount()) - $montantRembourse,
|
|
'weight' => 0,
|
|
'options' => $cartItemOptions
|
|
]);
|
|
$cartItem->setQuantity(1);
|
|
|
|
// * On envoie la ligne virtuelle en haut de la liste.
|
|
$refundedTransactionsItems->put($cartItem->rowId, \json_decode($cartItem->toJson()));
|
|
$refundedTransactionsItems = $refundedTransactionsItems->reverse();
|
|
} //end if
|
|
|
|
$transaction->items = $refundedTransactionsItems->map(function (object $basketItem) {
|
|
$basketItem->tax = $basketItem->tax * -1;
|
|
$basketItem->price = $basketItem->price * -1;
|
|
$basketItem->subtotal = $basketItem->subtotal * -1;
|
|
$basketItem->options->price_ttc = $basketItem->options->price_ttc * -1;
|
|
$basketItem->options->undiscounted_price_ttc =
|
|
$basketItem->options->undiscounted_price_ttc * -1;
|
|
return $basketItem;
|
|
});
|
|
$transaction->needs_validation = false;
|
|
|
|
$transaction->saveOrFail();
|
|
|
|
OrderIsRefunded::dispatch(
|
|
$order,
|
|
$transaction
|
|
);
|
|
|
|
return \response()->redirectToRoute('bo.orders.show', $order)
|
|
->with('success', \trans('La transaction est marquée comme résolue'));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show the order prepare form
|
|
*
|
|
* @param \App\Models\Shop\Order $order
|
|
* @return \Illuminate\View\View
|
|
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
|
|
*/
|
|
public function prepare(Order $order): View
|
|
{
|
|
// phpcs:enable
|
|
$this->authorize('prepare', $order);
|
|
$remainingItems = $order->getRemainingUnrefundedItems();
|
|
return \view('back::pages.orders.prepare', \compact('order', 'remainingItems'));
|
|
}
|
|
|
|
/**
|
|
* Validate order preparation
|
|
*
|
|
* @param \App\Http\Requests\Bo\PrepareOrderRequest $request
|
|
* @param \App\Models\Shop\Order $order
|
|
* @return \Illuminate\Http\RedirectResponse
|
|
*/
|
|
public function prepareSubmit(PrepareOrderRequest $request, Order $order): RedirectResponse
|
|
{
|
|
$this->authorize('prepare', $order);
|
|
return DB::transaction(function () use ($request, $order) {
|
|
|
|
// * Les items qu'il reste sur la commande (non remboursés).
|
|
$remainingItems = $order->getRemainingUnrefundedItems();
|
|
|
|
// * On met à jour cette liste avec les items effectivement préparés dans la commande.
|
|
$shipped_basket = \collect(ToolboxHelper::updateBasketAmounts(
|
|
$remainingItems,
|
|
$request->items
|
|
));
|
|
|
|
// * On met à jour la liste avec les items qui vont être remboursés.
|
|
$itemsToRefund = $remainingItems->pluck('qty', 'id')
|
|
->mapWithKeys(function ($qty, string $id) use ($request) {
|
|
return [
|
|
$id => \intval($qty) - \intval(\collect($request->items)->get($id, 0))
|
|
];
|
|
});
|
|
$refundedTransactionsItems = \collect(ToolboxHelper::updateBasketAmounts(
|
|
$remainingItems,
|
|
$itemsToRefund->all()
|
|
));
|
|
|
|
$order->shipped_basket = $shipped_basket;
|
|
$order->delivery_status = DeliveryStatus::ready();
|
|
// * Save Shipped basket
|
|
$order->saveQuietly();
|
|
|
|
// ? Le montant va exceder ce que le client a payé ?
|
|
if ($request->willExceedOrderAmount) {
|
|
// * Annulation et remboursement du reste.
|
|
// * Créer un dummy product pour représenter la transaction.
|
|
$cartItemOptions = \array_merge(static::$cartItemOptionsSkel, [
|
|
'virtual' => true,
|
|
'price_ttc' => $order->total_ttc - $order->total_refund,
|
|
]);
|
|
|
|
$cartItem = CartItem::fromArray([
|
|
'id' => \str_replace('.', '', \uniqid('', true)),
|
|
'name' => trans('Remboursement commande'),
|
|
'price' => $order->total_ttc - $order->total_refund,
|
|
'weight' => 0,
|
|
'options' => $cartItemOptions
|
|
]);
|
|
$cartItem->setQuantity(1);
|
|
|
|
$this->refundTransaction(
|
|
$order,
|
|
$order->total_ttc - $order->total_refund,
|
|
// * Negate all items prices (already refunded)
|
|
\collect([
|
|
$cartItem->rowId => \json_decode($cartItem->toJson())
|
|
])->map(function (object $basketItem) {
|
|
$basketItem->tax = $basketItem->tax * -1;
|
|
$basketItem->price = $basketItem->price * -1;
|
|
$basketItem->subtotal = $basketItem->subtotal * -1;
|
|
$basketItem->options->price_ttc = $basketItem->options->price_ttc * -1;
|
|
$basketItem->options->undiscounted_price_ttc =
|
|
$basketItem->options->undiscounted_price_ttc * -1;
|
|
return $basketItem;
|
|
})
|
|
);
|
|
|
|
// * Mettre a jour la commande.
|
|
$order->saveOrFail();
|
|
|
|
// * Cancel Order completely.
|
|
/** @var \Illuminate\Http\RedirectResponse */
|
|
return $this->callAction('changeValidationStatus', [
|
|
ChangeOrderValidationStatusRequest::create(
|
|
\route('bo.orders.validation-status', $order),
|
|
'POST',
|
|
[
|
|
'validation_status' => ValidationStatus::canceled()->value
|
|
]
|
|
),
|
|
$order
|
|
]);
|
|
} //end if
|
|
|
|
// ? La commande va devoir être remboursé completement ?
|
|
if ($request->willNeedCompleteRefund) {
|
|
$this->refundTransaction(
|
|
$order,
|
|
$order->total_ttc - $order->total_refund,
|
|
// * Negate all items prices (already refunded)
|
|
$order->getRemainingUnrefundedItems()->map(function (object $basketItem) {
|
|
$basketItem->tax = $basketItem->tax * -1;
|
|
$basketItem->price = $basketItem->price * -1;
|
|
$basketItem->subtotal = $basketItem->subtotal * -1;
|
|
$basketItem->options->price_ttc = $basketItem->options->price_ttc * -1;
|
|
$basketItem->options->undiscounted_price_ttc =
|
|
$basketItem->options->undiscounted_price_ttc * -1;
|
|
return $basketItem;
|
|
})
|
|
);
|
|
|
|
// * Mettre a jour la commande.
|
|
$order->saveOrFail();
|
|
|
|
// * Cancel Order completely.
|
|
/** @var \Illuminate\Http\RedirectResponse */
|
|
return $this->callAction('changeValidationStatus', [
|
|
ChangeOrderValidationStatusRequest::create(
|
|
\route('bo.orders.validation-status', $order),
|
|
'POST',
|
|
[
|
|
'validation_status' => ValidationStatus::canceled()->value
|
|
]
|
|
),
|
|
$order
|
|
]);
|
|
} //end if
|
|
|
|
// ? Il faudra emettre un remboursement pour le reste ?
|
|
if ($request->willNeedRefund and (bool)intval($refundedTransactionsItems->sum('subtotal'))) {
|
|
$this->refundTransaction(
|
|
$order,
|
|
$refundedTransactionsItems->sum('subtotal'),
|
|
// * Negate all items prices (already refunded)
|
|
$refundedTransactionsItems->map(function (object $basketItem) {
|
|
$basketItem->tax = $basketItem->tax * -1;
|
|
$basketItem->price = $basketItem->price * -1;
|
|
$basketItem->subtotal = $basketItem->subtotal * -1;
|
|
$basketItem->options->price_ttc = $basketItem->options->price_ttc * -1;
|
|
$basketItem->options->undiscounted_price_ttc =
|
|
$basketItem->options->undiscounted_price_ttc * -1;
|
|
return $basketItem;
|
|
})
|
|
);
|
|
|
|
// * Mettre a jour la commande.
|
|
$order->saveOrFail();
|
|
}
|
|
|
|
// * Mettre a jour la commande.
|
|
$order->saveOrFail();
|
|
|
|
return \redirect()->route('bo.orders.show', $order)->with('bo.success', trans(
|
|
':model a été mis à jour',
|
|
['model' => Str::singular(trans('models.classes.orders'))]
|
|
));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Complete order refund and cancel
|
|
*
|
|
* @param \App\Models\Shop\Order $order
|
|
* @return \Illuminate\Http\RedirectResponse
|
|
*/
|
|
public function cancelRefundSubmit(Order $order): RedirectResponse
|
|
{
|
|
$this->authorize('refund', $order);
|
|
return DB::transaction(function () use ($order) {
|
|
$this->callAction('refundSubmit', [$order]);
|
|
// * Cancel Order completely.
|
|
/** @var \Illuminate\Http\RedirectResponse */
|
|
return $this->callAction('changeValidationStatus', [
|
|
ChangeOrderValidationStatusRequest::create(
|
|
\route('bo.orders.validation-status', $order),
|
|
'POST',
|
|
[
|
|
'validation_status' => ValidationStatus::canceled()->value
|
|
]
|
|
),
|
|
$order
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Complete order refund
|
|
*
|
|
* @param \App\Models\Shop\Order $order
|
|
* @return \Illuminate\Http\RedirectResponse
|
|
*/
|
|
public function refundSubmit(Order $order): RedirectResponse
|
|
{
|
|
$this->authorize('refund', $order);
|
|
return DB::transaction(function () use ($order) {
|
|
$this->refundTransaction(
|
|
$order,
|
|
$order->total_ttc - $order->total_refund,
|
|
$order->getItemForOrderCancelling()
|
|
);
|
|
return \back()->with('bo.success', trans(
|
|
':model a été mis à jour',
|
|
['model' => Str::singular(trans('models.classes.orders'))]
|
|
));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Refund a transaction
|
|
*
|
|
* @param \App\Models\Shop\Order $order
|
|
* @param integer $amount
|
|
* @param object $items
|
|
* @return void
|
|
* @throws \RuntimeException If the payment transaction cannot be fetched for the concerned order.
|
|
*/
|
|
private function refundTransaction(
|
|
Order $order,
|
|
int $amount,
|
|
object $items
|
|
): void {
|
|
if (!($transaction = $order->getTransactionUsedForPayment())) {
|
|
throw new \RuntimeException("Could not fetch paiment transaction for order {$order->id}");
|
|
}
|
|
|
|
// ? Le paiment se fait en boutique ?
|
|
if ($transaction->payment_mode->equals(PaymentModeEnum::inShop())) {
|
|
// * Execute juste l'adaptateur.
|
|
$newTransaction = $transaction->getAdapter()->doRefundCustomer(
|
|
$transaction,
|
|
$amount,
|
|
$items
|
|
);
|
|
// ? On est sur la même transaction ?
|
|
if ($newTransaction->exists and $newTransaction->id === $transaction->id) {
|
|
// * Elle a été anullée.
|
|
$previousMessage = session()->get('bo.warning', '');
|
|
session()->flash('bo.warning', \trans(
|
|
'La commande est annulée.'
|
|
) . " {$previousMessage}");
|
|
return;
|
|
}
|
|
// * Une nouvelle à été créé.
|
|
$previousMessage = session()->get('bo.error', '');
|
|
session()->flash(
|
|
'bo.info',
|
|
\trans(
|
|
'Une nouvelle transaction est crée avec les produits effectivement préparés.'
|
|
) . " {$previousMessage}"
|
|
);
|
|
return;
|
|
} //end if
|
|
RefundOrder::dispatchSync(
|
|
$order,
|
|
$transaction,
|
|
$amount,
|
|
$items
|
|
);
|
|
$previousMessage = session()->get('bo.info', '');
|
|
session()->flash('bo.info', \trans(
|
|
'Un remboursement va être émit au client'
|
|
) . " \n{$previousMessage}");
|
|
}
|
|
}
|