<repo_name>zyrjdtyrj/casino<file_sep>/app/Http/Controllers/Casino/Game/SlotController.php
<?php

namespace App\Http\Controllers\Casino\Game;

use App\Models\Prize;
use App\Services\Casino;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\App;

class SlotController extends Controller
{
    public function show()
    {

        /**
         * @var $casinoService Casino
         */
        $casinoService = App::make('CasinoService');
        $gambler = $casinoService->getGambler();
        $prizes = Prize::getPrizesOfGambler($gambler);
        //dd($prizes);

        return view('casino.slot', ['message' => null]);

    }

    public function play()
    {
        /**
         * @var Casino $casino
         */
        $casino = App::make('CasinoService');
        $prize = $casino->generatePrize();

        return view('casino.slot', ['message' => $prize->getPrizeName()]);

    }
}
<file_sep>/routes/web.php
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', [\App\Http\Controllers\HomeController::class, 'welcome'])->name('welcome');


Route::get('/casino/bank', [\App\Http\Controllers\Casino\Game\CasinoController::class, 'bank'])
    ->name('casino.bank');

Route::get('/casino/slot', [\App\Http\Controllers\Casino\Game\SlotController::class, 'show'])
    ->middleware('auth')
    ->name('casino');

Route::post('/casino/slot', [\App\Http\Controllers\Casino\Game\SlotController::class, 'play'])
    ->middleware('auth');

Route::get('/casino/prizes', [\App\Http\Controllers\Casino\Game\PrizesController::class, 'prizes'])
    ->middleware('auth')
    ->name('casino.prizes');

Route::post('/casino/prizes', [\App\Http\Controllers\Casino\Game\PrizesController::class, 'prizeCancel'])
    ->middleware('auth');

// TODO: make authentication for admins
Route::get('/casino/admin', [\App\Http\Controllers\Casino\Admin\AdminController::class, 'prizesShow'])
    ->name('casino.admin');
Route::post('/casino/admin', [\App\Http\Controllers\Casino\Admin\AdminController::class, 'prizeAction']);


Auth::routes();


<file_sep>/app/Http/Controllers/HomeController.php
<?php

namespace App\Http\Controllers;

use App\Models\Gambler;
use Illuminate\Support\Facades\Auth;

class HomeController extends Controller
{

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function welcome()
    {
        if (Auth::user()) {
            return redirect()->route('casino');
        } else {
            $users = Gambler::all();

            return view('home', ['users' => $users]);
        }
    }
}
<file_sep>/database/factories/CasinoGiftFactory.php
<?php

namespace Database\Factories;

use App\Models\CasinoGift;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class CasinoGiftFactory extends Factory
{

    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = CasinoGift::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {

        return [
            'name' => self::getRandomName(),
            'amount' => mt_rand(1,10)
        ];
    }

    private function getRandomName ()
    {
        static $names = [
            'POC Obex BC SPIN',
            '1More Stylish',
            'Fellow Carter Mug, 16 oz.',
            'Tern HSD S8i',
            'Tile Sticker',
            'ZSA Planck EZ',
            'One Eleven SWII Solar Three-Hand rPet Watch',
            'Cybex e-Priam',
            'Fujifilm Instax Mini LiPlay',
            'Timbuk2 Tech Tote',
            'IK Multimedia Uno Drum',
            'Industry West 1L Carafe',
            'Anden Cameo Mirror',
            'Roku Smart Soundbar',
            'Civil Doppio 65',
            'Giant Microbes Waterbear',
            'Razer Blade 15',
            'Tecnica Plasma S GTX',
            'Frank DePaula FrankOne Coffee Maker',
            'Form Leather Laptop Case',
            'Nintendo Switch Lite',
            'Sonos Move',
            'DJI Robomaster S1',
            'Denon DP-450USB',
            'Am I Overthinking This?',
            'Sphero Mini Activity Kit',
            'Onewheel Pint',
            'Garmin Fenix 6S Pro',
            'BioLite Headlamp 330',
            'JBL L100 Classic',
            'Master &amp; Dynamic MW65',
            'Sensel Morph With Buchla Thunder Overlay',
            'Craighill Venn Puzzle',
            'Leica Q2',
            'BedJet 3',
            'Yubico YubiKey 5Ci',
            'Vizio P-Series Quantum 4K 65-Inch',
            'Yuki Otoko “Snow Yeti” Sake, 24 oz.',
            'SmartWool  Women\'s Smartloft-X 60 Hoodie Full Zip',
            'Gantri Buddy Light',
            'Google Nest Hub Max',
            'Sandworm',
            'SteelSeries Arctis 1 Wireless',
            'Breville Super Q',
            'OnePlus 7T',
            'Form Swim Goggles (With Display)',
            'Stellar Factory Peek & Push',
            'Raspberry Pi 4',
            'Amazon Kindle',
            'R<NAME>',
            'Black &amp; Decker Furbuster'
        ];
        static $index = 0;

        if (!$index)
            shuffle($names);

        if (++$index >= count($names))
            return Str::random(20);

        return $names[$index];
    }
}
<file_sep>/tests/Unit/PrizeMoneyServiceTest.php
<?php

namespace Tests\Unit;

use App\Services\PrizeFactory;
use PHPUnit\Framework\TestCase;
use App\Services\Prize\PrizeMoneyService;
use Mockery as M;

class PrizeMoneyServiceTest extends TestCase
{
    /**
     * @dataProvider dataForConvertMoneyToBonus
     */
    public function testConvertMoneyToBonus($rate, $moneyAmount, $bonusAmount)
    {
        $prizeFactory = M::mock(PrizeFactory::class);

        $config = [
            'generatePrize' => [
                'amountMin' => 1,
                'amountMax' => 10,
            ],
            'rateConvertToBonus' => $rate
        ];

        $prizeMoneyService = new PrizeMoneyService($prizeFactory, $config);
        $this->assertEquals($bonusAmount, $prizeMoneyService->convertMoneyToBonus($moneyAmount));
    }

    public function dataForConvertMoneyToBonus()
    {
        // rate, money, bonus
        return [
            [1, 1, 1],
            [2, 1, 2],
            [1.5, 10, 15],
            [1.3, 3, 4]
        ];
    }
}
<file_sep>/database/migrations/2020_15_12_102436_create_prizes_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePrizesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('prizes', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('gambler_id');
            $table->enum('type', ['Gift','Bonus','Money']);
            $table->enum('state', ['wait', 'canceled', 'received', 'converted']);
            $table->longText('prize');
            $table->timestamps();
            $table->foreign('gambler_id')
                ->references('id')
                ->on('gamblers')
                ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('prizes');
    }
}
<file_sep>/app/Services/Prize/PrizeMoneyService.php
<?php


namespace App\Services\Prize;


use App\Models\Gambler;
use App\Models\Prize;
use App\Models\PrizeMoney;
use App\Services;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\App;

class PrizeMoneyService extends AbstractPrizeService
{

    /**
     * @var int
     */
    protected $amountPrizeMin;

    /**
     * @var int
     */
    protected $amountPrizeMax;


    /**
     * @var float
     */
    protected $rateConvertToBonus;

    /**
     * PrizeMoneyService constructor.
     * @param Services\PrizeFactory $prizeFactory
     * @param null|array $config
     */
    public function __construct(Services\PrizeFactory $prizeFactory, $config = null)
    {
        parent::__construct($prizeFactory);

        $this->amountPrizeMin = isset($config['generatePrize']['amountMin'])
            ? $config['generatePrize']['amountMin']
            : config('casino.prizeServices.' . static::class . '.generatePrize.amountMin');

        $this->amountPrizeMax = isset($config['generatePrize']['amountMax'])
            ? $config['generatePrize']['amountMax']
            : config('casino.prizeServices.' . static::class . '.generatePrize.amountMax');

        $this->rateConvertToBonus = isset($config['rateConvertToBonus'])
            ? $config['rateConvertToBonus']
            : config('casino.prizeServices.' . static::class . '.rateConvertToBonus');
    }

    public function allowedPrizeGenerate()
    {

        $casinoService = $this->getCasinoService();
        if ($casinoService->moneyGetAvailable() > 0) {
            return true;
        }
        return false;
    }

    /**
     * @param Gambler $gambler
     * @return PrizeMoney
     */
    public function generatePrize($gambler = null)
    {
        $moneyAvailable = $this->getCasinoService()->moneyGetAvailable();
        // Profitable solution for Gambler
        $amountBonus = mt_rand($this->amountPrizeMin, $this->amountPrizeMax);
        $amountBonus = min($amountBonus, $moneyAvailable);
//        // Profitable solution for Casino
//        $amountBonus = mt_rand($this->amountPrizeMin, min($moneyAvailable, $this->amountPrizeMax));

        $prize = $this->prizeFactory->create('Money', ['amount' => $amountBonus], $gambler);
        return $prize;
    }

    /**
     * @param int $limit
     * @return Collection
     */
    public function getPrizesForCrediting($limit = 10)
    {
        return PrizeMoney::where('state', 'wait')->orderBy('id')->limit($limit)->get();
    }

    /** Prize events on created
     * @param PrizeMoney $prize
     */
    public function eventPrizeStateWait(Prize $prize)
    {
        $this->getCasinoService()->moneyReserved($prize->getAmount());
    }

    /** Prize events before change state to Canceled
     * @param PrizeMoney $prize
     */
    public function eventPrizeStateCanceled(Prize $prize)
    {
        $this->getCasinoService()->moneyUnreserved($prize->getAmount());
    }

    /** Prize events before change state to Received
     * @param PrizeMoney $prize
     */
    public function eventPrizeStateReceived(Prize $prize)
    {
        /**
         * @var $bankService Services\BankService
         */
        $bankService = App::make('CasinoBankService');
        $bankService->sendOrderToBank([
            'recipient' => $prize->getGambler()->name,
            'amount' => $prize->getAmount()
        ]);
    }

    /** Prize events before change state to Converted
     * @param PrizeMoney $prize
     */
    public function eventPrizeStateConverted(PrizeMoney $prize)
    {
        $bonusPlus = $this->convertMoneyToBonus($prize->getAmount());

        $casinoService = $this->getCasinoService();
        $casinoService->bonusGamblerPlus($prize->getGambler(), $bonusPlus);
        $casinoService->moneyUnreserved($prize->getAmount());
        $prize->setBonusAmount($bonusPlus);
    }

    public function convertMoneyToBonus($amountMoney)
    {
        return round($this->rateConvertToBonus * $amountMoney, 0);
    }


}
<file_sep>/app/Http/Controllers/Casino/Game/CasinoController.php
<?php

namespace App\Http\Controllers\Casino\Game;

use App\Services\Casino;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\App;

class CasinoController extends Controller
{

    public function bank(Request $request)
    {
        /**
         * @var $casino Casino
         */
        $casino = App::make('CasinoService');

        $gifts = $casino->giftGetAvailable();
        $moneyBank = $casino->moneyGetAvailable();

        return view('casino.bank', ['moneyBank' => $moneyBank, 'gifts' => $gifts]);
    }
}
<file_sep>/app/Services/Prize/AbstractPrizeService.php
<?php


namespace App\Services\Prize;


use App\Models\Gambler;
use App\Models\Prize;
use App\Services\Casino;
use App\Services\PrizeFactory;
use Illuminate\Support\Facades\App;

abstract class AbstractPrizeService
{
    /**
     * @var PrizeFactory
     */
    protected $prizeFactory;

    /**
     * PrizeService constructor.
     * @param PrizeFactory $prizeFactory
     */
    public function __construct(PrizeFactory $prizeFactory)
    {
        $this->prizeFactory = $prizeFactory;
    }

    /**
     * @return bool
     */
    abstract public function allowedPrizeGenerate();

    /**
     * @param Gambler $gambler
     * @return Prize
     */
    abstract public function generatePrize($gambler = null);

    /**
     * @return Casino
     */
    protected function getCasinoService()
    {
        return App::make('CasinoService');
    }

}
<file_sep>/config/casino.php
<?php

return [
    'classes' => [
        'prizeFactory' => \App\Services\PrizeFactory::class,
        'prizeModel' => \App\Models\Prize::class,
        'gamblerModel' => \App\Models\Gambler::class,
        'giftModel' => \App\Models\CasinoGift::class,
        'moneyModel' => \App\Models\CasinoMoney::class,
        'bankService' => \App\Services\BankService::class
    ],
    'prizeServices' => [
        \App\Services\Prize\PrizeMoneyService::class => [
            'generatePrize' => [
                'amountMin' => 1,
                'amountMax' => 10
            ],
            'bankService' => [
                'URL' => 'https://api.bank.site',
                'SecretKey' => '<KEY>',
                'TimeOut' => 10
            ],
            'rateConvertToBonus' => 2
        ],
        \App\Services\Prize\PrizeBonusService::class => [
            'generatePrize' => [
                'amountMin' => 2,
                'amountMax' => 20
            ],
        ],
        \App\Services\Prize\PrizeGiftService::class => [

        ]
    ]
];
<file_sep>/app/Services/BankService.php
<?php


namespace App\Services;


class BankService
{
    protected $URL;
    protected $secretKey;
    protected $timeOut;

    public function __construct($config = null)
    {
        $this->URL = isset ($config['URL'])
            ? $config['URL']
            : config('casino.bankService.URL');

        $this->secretKey = isset ($config['SecretKey'])
            ? $config['SecretKey']
            : config('casino.bankService.SecretKey');

        $this->timeOut = isset ($config['TimeOut'])
            ? $config['TimeOut']
            : config('casino.bankService.TimeOut');
    }

    /**
     * @param $data - eny data for money crediting
     * @return bool
     */
    public function sendOrderToBank($data)
    {
        $request = [
            'SecretKey' => $this->secretKey,
            'data' => $data
        ];
        // TODO make curl request to URL
        sleep(1);

        // Generate fake results, 30% - errors, 70% - successful
        if (mt_rand(1,10)>3)
            return true; // successful
        else
            throw new \LogicException('Error connect to bank (for example)'); // error
    }
}
<file_sep>/app/Services/PrizeFactory.php
<?php


namespace App\Services;


use App\Models\Gambler;
use App\Models\Prize;
use Illuminate\Support\Facades\App;

class PrizeFactory
{
    /**
     * @var array
     */
    protected $prizeTypesMap;

    public function __construct($typeMap = null)
    {
        $this->prizeTypesMap = $typeMap ? $typeMap : Prize::$types;
    }

    /**
     * @param $prizeType
     * @param $prize
     * @param null|Gambler $gambler
     */
    public function create($prizeType, $prizeData, $gambler=null)
    {
        if (!array_key_exists($prizeType, $this->prizeTypesMap)) {
            throw new \InvalidArgumentException('Invalid prize type');
        }

        if (!$gambler) {
            $gambler = App::make('CasinoService')->getGambler();
        }

        $prize = new $this->prizeTypesMap[$prizeType] ([
            'gambler_id' => $gambler->id,
            'type' => $prizeType,
            'prize' => $prizeData,
            'state' => 'wait'
        ]);
        $prize->save();

        return $prize;
    }

}
<file_sep>/app/Models/PrizeGift.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;

class PrizeGift extends Prize
{
    use HasFactory;

    public static $type = 'Gift';

    /**
     * @return int
     */
    public function getGift()
    {
        return CasinoGift::find($this->prize['gift']);
    }

    public function getPrizeName()
    {
        return static::$type . ': ' . $this->prize['name'];
    }


}
<file_sep>/app/Services/Prize/PrizeGiftService.php
<?php


namespace App\Services\Prize;


use App\Models\Gambler;
use App\Models\Prize;
use App\Models\PrizeGift;
use App\Services;
use Illuminate\Database\Eloquent\Collection;

class PrizeGiftService extends AbstractPrizeService
{
    /**
     * @var Services\PrizeFactory
     */
    protected $prizeFactory;

    /**
     * @var int
     */
    protected $amountPrizeMin;

    /**
     * @var int
     */
    protected $amountPrizeMax;

    /**
     * PrizeMoneyService constructor.
     * @param Services\PrizeFactory $prizeFactory
     * @param null|array $config
     */
    public function __construct(Services\PrizeFactory $prizeFactory)
    {
        parent::__construct($prizeFactory);
    }

    /**
     * @return Collection
     */
    protected function getAvailableGifts()
    {
        return $this->getCasinoService()->giftGetAvailable();
    }

    /**
     * @return bool
     */
    public function allowedPrizeGenerate()
    {
        if ($this->getAvailableGifts()->count() > 0)
            return true;

        return false;
    }

    /**
     * @param Gambler $gambler
     * @return PrizeGift
     */
    public function generatePrize($gambler = null)
    {

        $availableGifts = $this->getAvailableGifts();
        $giftRandom = $availableGifts->shuffle()->first();

        $prize = $this->prizeFactory->create(
            'Gift',
            ['gift' => $giftRandom->id, 'name' => $giftRandom->name],
            $gambler
        );
        return $prize;
    }

    public function eventPrizeStateWait(Prize $prize)
    {
        $this->getCasinoService()->giftSetUsed($prize->getGift());
    }

    public function eventPrizeStateCanceled(Prize $prize)
    {
        $this->getCasinoService()->giftSetUnused($prize->getGift());
    }

}
<file_sep>/database/seeders/Prizes.php
<?php

namespace Database\Seeders;

use App\Models\Gambler;
use App\Services\Casino;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\App;

class Prizes extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        /**
         * @var $casinoService Casino
         */
        $casinoService = App::make('CasinoService');

        $gamblers = Gambler::all();
        $count = $gamblers->count() * 10;

        for ($i = 0; $i < $count; $i++) {
            $gambler = $gamblers->random(1)->first();
            $casinoService->generatePrize($gambler);
        }
    }
}
<file_sep>/database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use App\Models\CasinoGift;
use App\Models\Gambler;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call(CasinoMoney::class);
        CasinoGift::factory(50)->create();
        Gambler::factory(10)->create();
        $this->call(Prizes::class);
    }
}
<file_sep>/database/seeders/CasinoMoney.php
<?php

namespace Database\Seeders;

use App\Models\PrizeMoney;
use Illuminate\Database\Seeder;

class CasinoMoney extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        \DB::table('casino_money')
            ->insert([
                'id' => 1,
                'bank' => 10000
            ]);
    }
}
<file_sep>/app/Console/Commands/sendToBankCommand.php
<?php

namespace App\Console\Commands;

use App\Models\PrizeMoney;
use App\Services\Prize\PrizeMoneyService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\App;

class sendToBankCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'casino:send2bank {NumberOrders? : Maximum number orders}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'The command sends orders to the bank for crediting prizes';

    /**
     * @var string
     */
    protected $help = <<<TXT
The only command parameter specifies the maximum number of records to process
Default limit is
TXT;

    /** default limit process records
     * @var int
     */
    protected $limit = 10;

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $arguments = $this->arguments();
        $limit = $arguments['NumberOrders'];

        if ($limit && !is_numeric($limit)) {
            $this->error('Required number');
            return 1;
        } elseif (!$limit) {
            $this->info($this->description . PHP_EOL . $this->help . ' ' . $this->limit);
            $limit = $this->limit;
        }


        if (!$this->confirm('Send to bank [' . $limit . '] orders?', true)) {
            return 0;
        }

        /**
         * @var $moneyService PrizeMoneyService
         */
        $moneyService = App::make('PrizeMoneyService');
        $moneyPrizes = $moneyService->getPrizesForCrediting($limit);

        $errors = [];
        $okCounter = 0;
        $this->withProgressBar($moneyPrizes, function ($prize) use (&$errors, &$okCounter) {
            /**
             * @var $prize PrizeMoney
             */
            try {
                $prize->state = 'received';
                $okCounter++;
            } catch (\Exception $exception) {
                $errors[] = $prize->id . '# [' . $prize->getPrizeName() . '] ' . $exception->getMessage();
            }
        });

        $resultsStr = PHP_EOL . 'Results: Ok-' . $okCounter;
        if (count($errors)) {
            $resultsStr .= ', Errors-' . count($errors);
            $resultsStr .= ', Total-' . ($okCounter + count($errors));
        }
        $resultsStr .= PHP_EOL;
        $this->info($resultsStr);

        if (count($errors)) {
            $errorsStr = 'Errors:' . PHP_EOL;
            foreach ($errors as $error) {
                $errorsStr .= $error . PHP_EOL;
            }
            $this->error($errorsStr);
        }

        return 0;
    }
}
<file_sep>/app/Services/Prize/PrizeBonusService.php
<?php


namespace App\Services\Prize;


use App\Models\Gambler;
use App\Models\PrizeBonus;
use App\Services;

class PrizeBonusService extends AbstractPrizeService
{
    /**
     * @var Services\PrizeFactory
     */
    protected $prizeFactory;

    /**
     * @var int
     */
    protected $amountPrizeMin;

    /**
     * @var int
     */
    protected $amountPrizeMax;

    /**
     * PrizeMoneyService constructor.
     * @param Services\PrizeFactory $prizeFactory
     * @param null|array $config
     */
    public function __construct(Services\PrizeFactory $prizeFactory, $config = null)
    {
        parent::__construct($prizeFactory);

        $this->amountPrizeMin = isset($config['generatePrize']['amountMin'])
            ? $config['generatePrize']['amountMin']
            : config('casino.prizeServices.'.static::class.'.generatePrize.amountMin');

        $this->amountPrizeMax = isset($config['generatePrize']['amountMax'])
            ? $config['generatePrize']['amountMax']
            : config ('casino.prizeServices.'.static::class.'.generatePrize.amountMax');
    }

    public function allowedPrizeGenerate()
    {
        return true;
    }

    /**
     * @param Gambler $gambler
     * @return PrizeBonus
     */
    public function generatePrize($gambler = null)
    {
        $amountBonus = mt_rand($this->amountPrizeMin, $this->amountPrizeMax);
        $prize = $this->prizeFactory->create('Bonus', ['amount' => $amountBonus], $gambler);
        $prize->state = 'received';
        $prize->save();
        return $prize;
    }

    public function eventPrizeStateReceived(PrizeBonus $prize)
    {
        $this->getCasinoService()->bonusGamblerPlus($prize->getGambler(), $prize->getAmount());
    }

    public function eventPrizeStateCanceled(PrizeBonus $prize)
    {
        $this->getCasinoService()->bonusGamblerMinus($prize->getGambler(), $prize->getAmount());
    }



}
<file_sep>/README.md
## Test casino

Author: _[<NAME>](<EMAIL>)_

## Instalation
Install vendors packages.
```
composer install
```
Configure connection to mysql server in file `.env`.

Create database structure and fixtures.
```
php artisan migrate
php artisan db:seed
```

## Start server 

```
php artisan serve
```

## Commandline

Command for sending orders to the bank for money prizes.
Optional parameter *number* limits the number of processed records.
```
php artisan casino:send2bank {number}
```

## Unit tests

```
phpunit
```

<file_sep>/app/Http/Controllers/Casino/Admin/AdminController.php
<?php

namespace App\Http\Controllers\Casino\Admin;

use App\Models\Prize;
use App\Services\Casino;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\App;

class AdminController extends Controller
{

    public function prizesShow(Request $request)
    {
        /**
         * @var $casino Casino
         */

        $prizes = Prize::where('state', 'wait')->orderBy('id')->limit(20)->get();
        // !!! Preloading completed but not used. This is weird and shouldn't be!
        $prizes->load('gambler');

        $message = $request->session()->pull('message');
        $error = $request->session()->pull('error');

        return view('admin.prizes', ['prizes' => $prizes, 'message' => $message, 'error' => $error]);
    }

    public function prizeAction(Request $request)
    {
        $prizeCancelId = request('cancel');
        $prizeSendId = request('send');

        if (!$prizeCancelId && !$prizeSendId) {
            redirect()->back();
        }

        try {
            if ($prizeCancelId) {
                $prize = Prize::find($prizeCancelId);
                $prize->state = 'canceled';
            }  else {
                $prize = Prize::find($prizeSendId);
                $prize->state = 'receive';
            }
            $prize->save();
            $request->session()->flash('message', 'Successful!');

        } catch (\Exception $exception) {
            $request->session()->flash('error', $exception->getMessage());
        }

        return redirect()->back();
    }
}
<file_sep>/app/Services/Casino.php
<?php


namespace App\Services;


use App\Models\CasinoGift;
use App\Models\Gambler;
use App\Models\Prize;
use App\Models\User;
use App\Services\Prize\AbstractPrizeService;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;

class Casino
{
    /**
     * @var PrizeFactory
     */
    protected $prizeFactory;

    /**
     * @var AbstractPrizeService[]
     */
    protected $prizeServices;

    /**
     * @var string
     */
    protected $gamblerModelClass;

    /**
     * @var string
     */
    protected $giftModelClass;

    /**
     * @var string
     */
    protected $moneyModelClass;


    public function __construct(
        $prizeFactoryClass = null,
        $prizeServiceClasses = null,
        $gamblerModelClass = null,
        $giftModelClass = null,
        $moneyModelClass = null,
        $bankServiceClass = null
    ) {
        $prizeFactoryClass = $prizeFactoryClass ? $prizeFactoryClass : config('casino.classes.prizeFactory');
        $this->prizeFactory = $prizeFactory = new $prizeFactoryClass();
        $this->gamblerModelClass = $gamblerModelClass ? $gamblerModelClass : config('casino.classes.gamblerModel');
        $this->giftModelClass = $giftModelClass ? $giftModelClass : config('casino.classes.giftModel');
        $this->moneyModelClass = $moneyModelClass ? $moneyModelClass : config('casino.classes.moneyModel');

        if (!$bankServiceClass)
            $bankServiceClass = config ('casino.classes.bankService');

        App::bind('CasinoBankService', $bankServiceClass);

        $prizeServiceClasses = isset($prizeServiceClasses)
            ? new Collection($prizeServiceClasses)
            : new Collection(array_keys(config('casino.prizeServices')));

        $this->prizeServices = $prizeServiceClasses->map(function($serviceName) use ($prizeFactory) {
            return new $serviceName($prizeFactory);
        });

        // Create singletons to call services by name in the application
        $this->prizeServices->each(function($service) {
            App::singleton(class_basename($service), function() use ($service){
               return $service;
            });
        });
    }

    /**
     * @param Gambler $gambler
     * @return Prize
     * @throws \Exception
     */
    public function generatePrize ($gambler = null)
    {
        $allowedPrizeServices = $this->prizeServices->filter(function($prizeService){
            return $prizeService->allowedPrizeGenerate();
        });

        if (!count($allowedPrizeServices)) {
            throw new \Exception('Not allowed prize services');
        }

        /**
         * @var $randomService AbstractPrizeService
         */
        $randomService = $allowedPrizeServices->random(1)->first();
        $prize = $randomService->generatePrize($gambler);

        return $prize;
    }

    public function giftGetAvailable()
    {
        $giftModelClass = $this->giftModelClass;
        return $giftModelClass::where('amount', '>', '0')->get();
    }

    public function giftSetUsed(CasinoGift $casinoGift)
    {
        if ($casinoGift->amount < 1)
            throw new \LogicException('Gift is no longer available');

        $casinoGift->amount = $casinoGift->amount -1;
        $casinoGift->save();
    }

    public function giftSetUnused(CasinoGift $casinoGift)
    {
        $casinoGift->amount = $casinoGift->amount +1;
        $casinoGift->save();
    }

    /**
     * @return int
     */
    public function moneyGetAvailable()
    {
        return $this->moneyModelClass::find(1)->bank;
    }

    public function moneyReserved($amount)
    {
        $bank = $this->moneyModelClass::find(1);
        if ($bank->bank < $amount) {
            throw new \LogicException('Casino bank haven\'t such money');
        }

        $bank->bank = $bank->bank - $amount;
        $bank->save();
    }

    public function moneyUnreserved($amount)
    {
        $bank = $this->moneyModelClass::find(1);
        $bank->bank = $bank->bank + $amount;
        $bank->save();
    }
    /**
     * @param $gambler Gambler
     * @param $amount int
     */
    public function bonusGamblerPlus(Gambler $gambler, $amount)
    {
        $gambler->bonus = $gambler->bonus + $amount;
        $gambler->save();
    }

    /**
     * @param $gambler Gambler
     * @param $amount int
     */
    public function bonusGamblerMinus (Gambler $gambler, $amount)
    {
        if($gambler->bonus < $amount)
            throw new \LogicException('Not enough bonuses');

        $gambler->bonus = $gambler->bonus - $amount;
        $gambler->save();
    }

    /**
     * @return User
     */
    public function getGambler()
    {
        $gambler = Auth::user();

        return $gambler;
    }
}
<file_sep>/app/Models/PrizeBonus.php
<?php

namespace App\Models;

use App\Services\Prize\PrizeBonusService;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class PrizeBonus extends Prize
{
    use HasFactory;

    public static $type = 'Bonus';

    /**
     * @return int
     */
    public function getAmount()
    {
        return (int)$this->prize['amount'];
    }

    public function getPrizeName()
    {
        return static::$type . ': ' . $this->prize['amount'];
    }


}
<file_sep>/app/Http/Controllers/Casino/Game/PrizesController.php
<?php

namespace App\Http\Controllers\Casino\Game;

use App\Models\Prize;
use App\Services\Casino;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\App;

class PrizesController extends Controller
{

    public function prizes(Request $request)
    {

        /**
         * @var $casinoService Casino
         */
        $casinoService = App::make('CasinoService');
        $gambler = $casinoService->getGambler();
        $bonusBalance = $gambler->bonus;
        $prizes = Prize::getPrizesOfGambler($gambler);
        $message = $request->session()->pull('message');
        $error = $request->session()->pull('error');

        return view('casino.prizes', [
            'prizes' => $prizes,
            'message' => $message,
            'error' => $error,
            'bonusBalance' => $bonusBalance
        ]);

    }

    public function prizeCancel(Request $request)
    {
        $prizeCancelId = request('cancel');
        $prizeConvertId = request('convert');

        if (!$prizeCancelId && !$prizeConvertId) {
            redirect()->back();
        }

        try {
            if ($prizeCancelId) {
                $prize = Prize::find($prizeCancelId);
                $prize->state = 'canceled';
            } else {
                $prize = Prize::find($prizeConvertId);
                $prize->state = 'converted';
            }
            $prize->save();
            $request->session()->flash('message', 'Successful!');

        } catch (\Exception $exception) {
            $request->session()->flash('error', $exception->getMessage());
        }
        return redirect()->route('casino.prizes');
    }
}
<file_sep>/app/Models/Prize.php
<?php

namespace App\Models;

use App\Services\Prize\AbstractPrizeService;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;

class Prize extends Model
{
    use HasFactory;

    public static $types = [
        'Gift'  => PrizeGift::class,
        'Bonus' => PrizeBonus::class,
        'Money' => PrizeMoney::class
    ];

    /** Possible states of an entity
     * @var string[]
     */
    protected static $states = ['wait', 'canceled', 'received'];

    public static $type;

    public $table = 'prizes';

    protected $casts = [
        'prize' => 'array'
    ];

    public $fillable = ['gambler_id', 'prize', 'state', 'type'];

    /** Return name for interface
     * @return string
     */
    public function getPrizeName(){}

    public function gambler()
    {
        return $this->hasOne(Gambler::class, 'id', 'gambler_id');
    }

    public static function boot()
    {
        parent::boot();

        static::addGlobalScope(function ($query) {
            if ($query->getModel()::$type) {
                $query->where('type', $query->getModel()::$type);
            }
        });
    }

    /**
     * @return Gambler
     */
    public function getGambler()
    {
        return Gambler::find($this->gambler_id);
    }

    /** Overloading for generating collections of inherited classes
     *
     * @param array $attributes
     * @param null $connection
     * @return Prize
     */
    public function newFromBuilder($attributes = [], $connection = null)
    {
        if (!static::$type) {
            $model = new static::$types[$attributes->type]([]);
            return $model->newFromBuilder($attributes, $connection);
        }
        return parent::newFromBuilder($attributes, $connection);
    }

    /**
     * @param string $key
     * @param mixed $val
     * @return Prize|mixed
     * @throws \LogicException
     */
    public function setAttribute($key, $val)
    {
        if ($key == 'state') {

            if (!in_array($val, static::$states)) {
                throw new \InvalidArgumentException('State [' . $val . '] is not correct for this Prize');
            }

            if ($this->state != 'wait' && in_array($val,['received', 'cancel', 'converted'])) {
                throw new \LogicException('For change state the prize must be in the state \'wait\'');
            }

            $prizeService = $this->getService();
            $prizeServiceEventMethod = 'eventPrizeState'.Str::ucfirst($val);
            if (method_exists($prizeService, $prizeServiceEventMethod)) {
                $prizeService->$prizeServiceEventMethod($this);
            }
        }
        return parent::setAttribute($key, $val);

    }

    /**
     * @return AbstractPrizeService
     */
    public function getService()
    {
        /**
         * @var $service AbstractPrizeService;
         */
        $service = App::make('Prize'.static::$type.'Service');
        return $service;
    }

    /**
     * @param Gambler $gambler
     * @return Prize
     */
    public static function getPrizesOfGambler(Gambler $gambler)
    {
        return self::where('gambler_id', $gambler->id)->get();
    }

}
<file_sep>/app/Models/PrizeMoney.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;

class PrizeMoney extends Prize
{
    use HasFactory;

    public static $type = 'Money';

    protected static $states = ['wait', 'canceled', 'received', 'converted'];

    /**
     * @return int
     */
    public function getAmount()
    {
        return (int)$this->prize['amount'];
    }

    public function getPrizeName()
    {
        $name = static::$type . ': ' . $this->prize['amount'] . '$';

        if ($this->state == 'converted')
            $name .= ' converted to bonus: ' . $this->prize['bonus'];

        return $name;
    }

    public function setBonusAmount($amount)
    {
        $prizeData = $this->prize;
        $prizeData['bonus'] = $amount;
        $this->prize = $prizeData;
    }

}
