<repo_name>Mastergalen/Zugy-Laravel-eCommerce-Store<file_sep>/app/Listeners/NotifyDriversOfOrder.php
<?php

namespace App\Listeners;

use App\Events\OrderWasPlaced;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Pushbullet\Pushbullet;

class NotifyDriversOfOrder
{
    /**
     * @var Pushbullet
     */
    private $pb;

    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        $this->pb = new Pushbullet(config('services.pushbullet.token'));
    }

    /**
     * Handle the event.
     *
     * @param  OrderWasPlaced  $event
     * @return void
     */
    public function handle(OrderWasPlaced $event)
    {
        $orderUrl = action('Admin\OrderController@show', $event->order->id);
        $channel = config('site.pushbullet.channels.orders');

        $this->pb->channel($channel)->pushLink(
            "A new order has been placed on Zugy",
            $orderUrl,
            ("Total: {$event->order->total} Euro")
        );
    }
}
<file_sep>/app/Zugy/Repos/DbRepository.php
<?php

namespace Zugy\Repos;

abstract class DbRepository
{
    public function find($id) {
        return $this->model->findOrFail($id);
    }

    public function all() {
        return $this->model->all();
    }

    public function paginate($perPage) {
        return $this->model->paginate($perPage);
    }

    public function with($model) {
        return $this->model->with($model);
    }

    public function orderBy($table, $direction)
    {
        return $this->model->orderBy($table, $direction);
    }
}<file_sep>/tests/AddressTest.php
<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class AddressTest extends TestCase
{
    use DatabaseTransactions;

    private $user;

    protected function setUp()
    {
        parent::setUp();

        $this->user = factory(App\User::class)->create();
        $this->actingAs($this->user);

        //Add item to cart to bypass checkout middleware, which requires a non-empty cart
        $product = factory(App\Product::class)->create();
        $product->translations()->save(factory(App\ProductTranslation::class)->make());

        $this->json('POST', 'api/v1/cart', [
            'id' => $product->id,
            'qty' => 1, //Qty 1
        ])->seeJson(['status' => 'success']);
    }

    /**
     * Test adding a simple address
     */
    public function testCreateAddress()
    {
        $service = new App\Services\CreateOrUpdateAddress();

        $address = factory(App\Address::class)->make([
            'country' => 'ITA'
        ]);

        $service->delivery($address->toArray());

        $database = $address->toArray();
        unset($database['country']);

        $this->seeInDatabase('addresses', $database);
    }

    /**
     * Test trying to add a delivery outside delivery area
     */
    public function testCreateAddressOutsideDeliveryArea()
    {
        $service = new App\Services\CreateOrUpdateAddress();

        $address = factory(App\Address::class)->make([
            'country' => 'ITA',
            'postcode' => 12345, //Outside delivery area
        ]);

        $service->delivery($address->toArray());

        $database = $address->toArray();
        unset($database['country']);

        $this->dontSeeInDatabase('addresses', $database);
    }

    /**
     * Test user filling out address form with a billing address that is different from the delivery address
     */
    public function testCreateDeliveryAddressWithDifferentBillingAddress() {
        $this->visit('en/checkout/address')->see('Delivery Address');

        $deliveryAddress = factory(App\Address::class)->make([
            'country' => 'ITA'
        ]);

        $billingAddress = factory(App\Address::class)->make([
            'country' => 'ITA'
        ]);

        $this->type($deliveryAddress->name, 'delivery[name]')
            ->type($deliveryAddress->line_1, 'delivery[line_1]')
            ->type($deliveryAddress->postcode, 'delivery[postcode]')
            ->type($deliveryAddress->city, 'delivery[city]')
            ->type($deliveryAddress->delivery_instructions, 'delivery[delivery_instructions]')
            ->type($deliveryAddress->phone, 'delivery[phone]')
            
            ->uncheck('delivery[billing_same]')

            ->type($billingAddress->name, 'billing[name]')
            ->type($billingAddress->line_1, 'billing[line_1]')
            ->type($billingAddress->postcode, 'billing[postcode]')
            ->type($billingAddress->city, 'billing[city]')
            
            ->press('Proceed')
        ;

        \Log::debug($deliveryAddress);
        \Log::debug($billingAddress);

        $databaseDeliveryAddress = $deliveryAddress->toArray();
        unset($databaseDeliveryAddress['country']);
        $databaseDeliveryAddress['isShippingPrimary'] = 1;
        $databaseDeliveryAddress['isBillingPrimary'] = 0;

        $databaseBillingAddress = $billingAddress->toArray();
        unset($databaseBillingAddress['country']);
        unset($databaseBillingAddress['delivery_instructions']);
        unset($databaseBillingAddress['phone']);
        $databaseBillingAddress['isShippingPrimary'] = 0;
        $databaseBillingAddress['isBillingPrimary'] = 1;

        $this->seeInDatabase('addresses', $databaseDeliveryAddress);
        $this->seeInDatabase('addresses', $databaseBillingAddress);
    }
}
<file_sep>/app/Http/Controllers/OrderController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use Zugy\Repos\Order\OrderRepository;

class OrderController extends Controller
{
    /**
     * @var OrderRepository
     */
    private $orderRepo;

    /**
     * OrderController constructor.
     */
    public function __construct(OrderRepository $orderRepository)
    {
        $this->orderRepo = $orderRepository;
    }


    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        $order = $this->orderRepo->find($id);

        $order->vat;

        return view('pages.order.show')->with(compact('order'));
    }

}
<file_sep>/app/Order.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Order extends Model
{
    use SoftDeletes;

    protected $table = 'orders';

    protected $with = ['items', 'coupon'];

    protected $appends = ['total'];

    protected $dates = ['order_placed', 'order_completed', 'delivery_time'];

    protected $statuses = [
        0 => 'New order',
        1 => 'Being processed',
        2 => 'Out for delivery',
        3 => 'Delivered',
        4 => 'Cancelled',
    ];

    public function user()
    {
        return $this->belongsTo('App\User');
    }

    public function items()
    {
        return $this->hasMany('App\OrderItem');
    }

    public function payment()
    {
        return $this->hasOne('App\Payment');
    }

    public function coupon()
    {
        return $this->belongsTo('App\Coupon');
    }

    public function activity()
    {
        return $this->hasMany('App\ActivityLog', 'content_id')->where('content_type', '=', 'Order');
    }

    public function getVatAttribute()
    {
        $totalVat = 0;

        //Calculate VAT for items
        foreach($this->items()->get() as $item) {
            $net = $item->final_price / ((100 + $item->tax) / 100);
            $totalVat += round($item->final_price - $net, 2);
        }

        //Calculate VAT for shipping
        $fee = $this->attributes['shipping_fee'];
        $shippingTax = config('site.shippingTax');

        $net = $fee / ((100 + $shippingTax) / 100);

        $vatShipping = round($fee - $net, 2);

        $totalVat += $vatShipping;

        return $totalVat;
    }

    public function getTotalAttribute()
    {
        return $this->items()->sum('final_price');
    }

    public function getGrandTotalAttribute()
    {
        return $this->getTotalAttribute() + $this->attributes['shipping_fee'] - $this->attributes['coupon_deduction'];
    }

    public function scopeUnprocessed($query) {
        return $query->where('order_status', '=', 0);
    }

    public function scopeIncomplete($query) {
        return $query->whereIn('order_status', [0, 1, 2]);
    }

    public function scopeUncancelled($query) {
        return $query->where('order_status', '!=', 4);
    }

    public function getActivityDescriptionForEvent($eventName)
    {
        return $eventName;
    }
}
<file_sep>/resources/lang/it/your-account.php
<?php

return [
    'title' => 'Il tuo account',
    'orders' => 'Ordini',
    'orders.desc' => 'Controlla, rintraccia o cancella un ordine',
    'orders.dispatch-to' => 'Invia a',
    'payment.manage' => 'Gestisci i tuoi metodi di pagamento',
    'settings.title' => 'Impostazioni',
    'settings.email' => 'Cambia l\'indirizzo email o la password',
];<file_sep>/app/Zugy/Cart/CartServiceProvider.php
<?php

namespace Zugy\Cart;

use Illuminate\Support\ServiceProvider;

class CartServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app['zugyCart'] = $this->app->share(function($app)
        {
            $session = $app['session'];
            $events = $app['events'];
            return new Cart($session, $events);
        });
    }
}<file_sep>/app/Zugy/AlgoliaEloquentTrait/ZugyModelHelper.php
<?php

namespace Zugy\AlgoliaEloquentTrait;

use AlgoliaSearch\Laravel\ModelHelper;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\App;

class ZugyModelHelper extends ModelHelper
{
    public function getFinalIndexName(Model $model, $indexName)
    {
        $appEnv = App::environment('testing') ? 'local' : App::environment();

        $env_suffix = property_exists($model, 'perEnvironment') && $model::$perEnvironment === true ? '_' . $appEnv : '';

        return $indexName.$env_suffix;
    }
}<file_sep>/app/Zugy/Repos/Order/OrderRepository.php
<?php

namespace Zugy\Repos\Order;

interface OrderRepository {
    /**
     * Fetch incomplete orders
     * @return mixed
     */
    public function incomplete();
}<file_sep>/app/Http/Controllers/API/OrderController.php
<?php

namespace App\Http\Controllers\Api;

use App\Events\OrderStatusChanged;
use App\Http\Controllers\Controller;
use App\Order;
use Zugy\Repos\Order\OrderRepository;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;

class OrderController extends Controller
{
    /**
     * @var OrderRepository
     */
    protected $orderRepository;

    /**
     * OrderController constructor.
     */
    public function __construct(OrderRepository $orderRepository)
    {
        $this->orderRepository = $orderRepository;
    }

    public function update(Request $request, $orderId)
    {
        $order = Order::findOrFail($orderId);

        if(Gate::denies('adminUpdate', $order)) {
            abort(403);
        }

        $validator = Validator::make($request->all(), [
            'order_status' => 'integer',
        ]);

        if($validator->fails()) {
            return response()->json(['status' => 'error', 'errors' => $validator->errors()], 422);
        }

        if($order->order_status == $request->input('order_status')) {
            return response()->json(['status' => 'error', 'message' => trans('order.api.error.status-same')], 422);
        }

        if($request->input('order_status') == 3) {//Set status to delivered, update payment to "paid"
            $order->payment()->update(['status' => 1, 'paid' => \Carbon::now()]);
            $order->order_completed = \Carbon::now();
        }

        $order->order_status = $request->input('order_status');

        $order->save();

        \Event::fire(new OrderStatusChanged($order));

        return ['status' => 'success', 'message' => trans('order.api.update.success')];
    }

    public function show(Request $request, $orderId) {
        $order = Order::findOrFail($orderId);

        if(Gate::denies('show', $order)) {
            abort(403);
        }

        return $order;
    }
    
    public function index(Request $request) {
        if(Gate::denies('index', new Order())) {
            abort(403);
        }

        return $this->orderRepository->orderBy('order_placed', 'desc')->paginate(30);
    }
}<file_sep>/app/Zugy/Facades/PaymentGateway.php
<?php

namespace Zugy\Facades;

use Illuminate\Support\Facades\Facade;

class PaymentGateway extends Facade
{
    protected static function getFacadeAccessor() { return 'paymentGateway'; }
}<file_sep>/app/Zugy/Checkout/Checkout.php
<?php

namespace Zugy\Checkout;

use App\Address;
use App\Coupon;
use App\Exceptions\Coupons\CouponExpiredException;
use App\Exceptions\Coupons\CouponNotStartedException;
use App\Exceptions\Coupons\CouponOrderMinimumException;
use App\Exceptions\Coupons\CouponUsedException;
use App\PaymentMethod;
use App\User;
use Carbon\Carbon;
use Zugy\Facades\Cart;

class Checkout
{
    protected $sessionKey = 'checkout';

    protected $session;

    public function __construct($session) {
        $this->session  = $session;
    }

    public function setShippingAddress(Address $address) {
        $this->session->put($this->sessionKey . '.address.shipping', $address);
    }

    public function setBillingAddress(Address $address) {
        $this->session->put($this->sessionKey . '.address.billing', $address);
    }

    public function setPaymentMethod(PaymentMethod $paymentMethod) {
        $this->session->put($this->sessionKey . '.payment', $paymentMethod);
    }

    public function getShippingAddress(User $user = null) {
        if($user !== null) {
            return  $user->addresses()->where('isShippingPrimary', '=', 1)->first();
        }

        $content = ($this->session->has($this->sessionKey . '.address.shipping')) ? $this->session->get($this->sessionKey . '.address.shipping') : null;

        if($content === null && auth()->check()) {
            $content = auth()->user()->addresses()->where('isShippingPrimary', '=', 1)->first();
        }

        return $content;
    }

    public function forgetShippingAddress() {
        $this->session->forget($this->sessionKey . 'address.shipping');
    }

    public function getBillingAddress(User $user = null) {
        if($user !== null) {
            return  $user->addresses()->where('isBillingPrimary', '=', 1)->first();
        }

        $content = ($this->session->has($this->sessionKey . '.address.billing')) ? $this->session->get($this->sessionKey . '.address.billing') : null;

        if($content === null && auth()->check()) {
            $content = auth()->user()->addresses()->where('isBillingPrimary', '=', 1)->first();
        }

        return $content;
    }

    public function forgetBillingAddress() {
        $this->session->forget($this->sessionKey . 'address.billing');
    }

    public function getPaymentMethod() {
        $content = ($this->session->has($this->sessionKey . '.payment')) ? $this->session->get($this->sessionKey . '.payment') : null;

        if($content === null && auth()->check()) {
            $content = auth()->user()->payment_methods()->default()->get()->first();
        }

        return $content;
    }

    public function setCoupon(Coupon $coupon)
    {
        self::validateCoupon($coupon);

        $this->session->put($this->sessionKey . '.coupon', $coupon);
    }

    public function hasCoupon()
    {
        return $this->session->has($this->sessionKey . '.coupon');
    }

    public function validateCoupon(Coupon $coupon) {
        //Check coupon has started
        if($coupon->starts != null && Carbon::now() < $coupon->starts) {
            throw new CouponNotStartedException();
        }

        //Check coupon is still valid
        if($coupon->expires != null && Carbon::now() > $coupon->expires) {
            throw new CouponExpiredException();
        }

        //Check uses
        if($coupon->max_uses != null && $coupon->uses >= $coupon->max_uses) {
            throw new CouponUsedException();
        }

        //Check minimum order total
        if($coupon->minimumTotal != null && Cart::total() < $coupon->minimumTotal) {
            throw new CouponOrderMinimumException();
        }

        return true;
    }

    public function getCoupon()
    {
        return ($this->session->has($this->sessionKey . '.coupon')) ? $this->session->get($this->sessionKey . '.coupon') : null;
    }

    public function forgetCoupon()
    {
        $this->session->forget($this->sessionKey . '.coupon');
    }


    public function setDeliveryDate($date) {
        $this->session->put($this->sessionKey . '.delivery.date', $date);
    }

    public function setDeliveryTime($time) {
        $this->session->put($this->sessionKey . '.delivery.time', $time);
    }

    public function getDeliveryDate() {
        return $this->session->get($this->sessionKey . '.delivery.date');
    }

    public function getDeliveryTime() {
        return $this->session->get($this->sessionKey . '.delivery.time');
    }

    /**
     * Forget entire shopping cart settings
     */
    public function forget() {
        $this->session->forget($this->sessionKey);
        \Cart::destroy();
    }
}<file_sep>/app/Payment.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

/**
 * Payment Status:
 * 0 | Unpaid
 * 1 | Paid
 * 2 | Refunded
 * 3 | Payment on delivery
 */

class Payment extends Model
{
    protected $table = 'order_payments';

    protected $fillable = ['status'];

    protected $casts = [
        'metadata' => 'json',
    ];

    public function getFormatted()
    {
        switch($this->attributes['method']) {
            case 'stripe':
                return [
                    'method' => 'card',
                    'card' => [
                        'brand' => $this->metadata['source']['brand'],
                        'last4' => $this->metadata['source']['last4']
                    ],
                ];
            case 'paypal':
                return [
                    'method' => 'paypal'
                ];
            case 'cash':
                return [
                    'method' => 'cash'
                ];
            default:
                throw new \Exception('Payment method does not exist:' . $this->attributes['method']);
                break;
        }
    }
}
<file_sep>/resources/assets/js/setup.js
/*
 * Add CSRF token to ajax request headers
 */
$.ajaxSetup({
    beforeSend: function(xhr) {
        xhr.setRequestHeader('X-CSRF-TOKEN', $('meta[name="_token"]').attr('content'));
    }
});

//Pjax default timeout increase
$.pjax.defaults.timeout = 4000;

$(document).on('ready pjax:success', function() {
    $('[data-toggle="popover"]').popover();
});

/**
 * Fetch GET variables from URL
 * @param qs
 * @returns {{}}
 */
function getQueryParams(qs) {
    qs = qs.split("+").join(" ");
    var params = {},
        tokens,
        re = /[?&]?([^=]+)=([^&]*)/g;

    while (tokens = re.exec(qs)) {
        params[decodeURIComponent(tokens[1])]
            = decodeURIComponent(tokens[2]);
    }

    return params;
}

<file_sep>/app/Policies/AddressPolicy.php
<?php

namespace App\Policies;

use App\Address;
use App\User;

class AddressPolicy extends BasePolicy
{
    public function update(User $user, Address $address)
    {
        return $user->id === $address->user_id || $this->isAdmin($user);
    }

    public function show(User $user, Address $address)
    {
        return $user->id === $address->user_id || $this->isAdmin($user);
    }
}<file_sep>/app/Exceptions/EmptyCartException.php
<?php

namespace App\Exceptions;


class EmptyCartException extends \Exception
{
    public function __construct($message = "Cart cannot be empty")
    {
        parent::__construct($message);
    }
}<file_sep>/app/ProductImage.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\FileNotFoundException;

class ProductImage extends Model
{
    protected $table = 'product_images';

    protected $appends = ['url'];

    public function getURLAttribute() {
        if(config('filesystems.default') == 's3') {
            return "https://s3." . config('services.aws.region') . ".amazonaws.com/" . config('services.aws.bucket') . "/" . $this->attributes['location'];
        } else {
            return url('uploads/' . $this->attributes['location']);
        }
    }

    public function product() {
        return $this->belongsTo('App\Product');
    }

    /**
     * Override parent delete
     * Updates product references and deletes disk storage
     * @throws FileNotFoundException
     */
    public function delete() {
        //Update parent product if has thumbnail set to the image to be deleted
        $products = $this->product()->get();

        foreach($products as $p) {
            if($p->thumbnail_id == $this->attributes['id']) {
                $p->thumbnail_id = null;
                $p->save();
            }
        }

        try {
            Storage::disk(config('filesystems.default'))->delete($this->attributes['location']);
        } catch(FileNotFoundException $e) {
            //Carry on
        }

        parent::delete();
    }
}
<file_sep>/app/Providers/AppServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Foundation\AliasLoader;
use Illuminate\Support\ServiceProvider;
use App\Services\AuthenticateUser;


class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        if($this->app->environment('local')) {
            $this->app->register('Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider');
            $this->app->register('Barryvdh\Debugbar\ServiceProvider');
            AliasLoader::getInstance()->alias('Debugbar', 'Barryvdh\Debugbar\Facade'); //Register alias

            $this->app->register('Fitztrev\QueryTracer\Providers\QueryTracerServiceProvider');
        }

        $this->app->bind(AuthenticateUser::class, function() {
            return new AuthenticateUser();
        });

        $this->app->bind('\AlgoliaSearch\Laravel\ModelHelper', 'Zugy\AlgoliaEloquentTrait\ZugyModelHelper');

        $this->app->bind('Zugy\Repos\Order\OrderRepository', 'Zugy\Repos\Order\DbOrderRepository');
        $this->app->bind('Zugy\Repos\Category\CategoryRepository', 'Zugy\Repos\Category\DbCategoryRepository');
        $this->app->bind('Zugy\Repos\Product\ProductRepository', 'Zugy\Repos\Product\DbProductRepository');
    }
}
<file_sep>/resources/lang/en/postcode.php
<?php

return [
    'check.success' => 'We deliver to you!',
    'browse-store' => 'Browse the store',
    'check.error.title' => 'We currently do not deliver to your area. Sorry!',
    'check.error.desc' => 'You can still browse our store, however.',
    'api.error.postcode.invalid' => 'The postcode was invalid.',
];<file_sep>/resources/lang/en/status.php
<?php

return [
    'payment.0' => 'Unpaid',
    'payment.1' => 'Paid',
    'payment.2' => 'Refunded',
    'payment.3' => 'Payment on delivery',
];<file_sep>/app/Http/Middleware/ShopClosed.php
<?php

namespace App\Http\Middleware;

use Carbon\Carbon;
use Closure;

class ShopClosed
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $reopeningTime = Carbon::create(2017, 9, 28, 4, 0, 0); //4 am Wednesday

        if(Carbon::now() < $reopeningTime) {
            $request->session()->put('error', "Sorry, we are closed for a short summer break! We will be back open soon!");
        }

        return $next($request);
    }
}
<file_sep>/app/Exceptions/Coupons/CouponUsedException.php
<?php

namespace App\Exceptions\Coupons;

class CouponUsedException extends \Exception
{

}<file_sep>/app/Listeners/LogSuccessfulLogin.php
<?php

namespace App\Listeners;

use Carbon\Carbon;
use Illuminate\Auth\Events\Login;

class LogSuccessfulLogin
{
    /**
     * Handle the event.
     *
     * @param  Login  $event
     * @return void
     */
    public function handle(Login $event)
    {
        \Log::info('User logged in', [
            'id' => auth()->user()->id,
            'ip' => $_SERVER['REMOTE_ADDR']
        ]);

        auth()->user()->last_login = Carbon::now();
        auth()->user()->last_login_ip = $_SERVER['REMOTE_ADDR'];
        auth()->user()->save();
    }
}
<file_sep>/database/seeds/CategoriesSeeder.php
<?php
use Illuminate\Database\Seeder;

class CategoriesSeeder extends Seeder
{
    public function run() {
        DB::table('categories')->insert([
            ['id' => 1, 'parent_id' => null],
            ['id' => 2, 'parent_id' => null],
            ['id' => 3, 'parent_id' => 1],
            ['id' => 4, 'parent_id' => 1],
            ['id' => 5, 'parent_id' => 3],
            ['id' => 6, 'parent_id' => 3],
            ['id' => 7, 'parent_id' => 1],
            ['id' => 8, 'parent_id' => 3],
            ['id' => 9, 'parent_id' => 3],
            ['id' => 10, 'parent_id' => 1], // Special
            ['id' => 11, 'parent_id' => 3],
            ['id' => 12, 'parent_id' => null], //Mixers
            ['id' => 13, 'parent_id' => 12], //Soda
            ['id' => 14, 'parent_id' => 12], //Juice
            ['id' => 15, 'parent_id' => 12], //Energy drinks
            ['id' => 16, 'parent_id' => null], //Deals
            ['id' => 17, 'parent_id' => 16], //Packs
            ['id' => 18, 'parent_id' => 7], //Craft Beer
            ['id' => 19, 'parent_id' => null], // Other products
            ['id' => 20, 'parent_id' => 1],
            ['id' => 22, 'parent_id' => null], //Summer sales
        ]);

        DB::table('category_translations')->insert([
            ['category_id' => 1, 'locale' => 'en',  'name' => 'Alcohol', 'slug' => 'alcohol'],
            ['category_id' => 1, 'locale' => 'it',  'name' => 'Alcol', 'slug' => 'alcol'],

            ['category_id' => 2, 'locale' => 'en',  'name' => 'Food', 'slug' => 'food'],
            ['category_id' => 2, 'locale' => 'it',  'name' => 'Cibo', 'slug' => 'cibo'],

            ['category_id' => 3, 'locale' => 'en',  'name' => 'Spirits', 'slug' => 'spirits'],
            ['category_id' => 3, 'locale' => 'it',  'name' => 'Superalcolici', 'slug' => 'superalcolici'],

            ['category_id' => 4, 'locale' => 'en',  'name' => 'Wine & champagne', 'slug' => 'wine-and-champagne'],
            ['category_id' => 4, 'locale' => 'it',  'name' => 'Vino e champagne', 'slug' => 'vino-e-champagne'],

            ['category_id' => 5, 'locale' => 'en',  'name' => 'Vodka', 'slug' => 'vodka'],
            ['category_id' => 5, 'locale' => 'it',  'name' => 'Vodka', 'slug' => 'vodka'],

            ['category_id' => 6, 'locale' => 'en',  'name' => 'Rum', 'slug' => 'rum'],
            ['category_id' => 6, 'locale' => 'it',  'name' => 'Rum', 'slug' => 'rum'],

            ['category_id' => 7, 'locale' => 'en',  'name' => 'Beer', 'slug' => 'beer'],
            ['category_id' => 7, 'locale' => 'it',  'name' => 'Birra', 'slug' => 'birra'],

            ['category_id' => 8, 'locale' => 'en',  'name' => 'Tequila', 'slug' => 'tequila'],
            ['category_id' => 8, 'locale' => 'it',  'name' => 'Tequila', 'slug' => 'tequila'],

            ['category_id' => 9, 'locale' => 'en',  'name' => 'Whiskey', 'slug' => 'whiskey'],
            ['category_id' => 9, 'locale' => 'it',  'name' => 'Whisky', 'slug' => 'whisky'],

            ['category_id' => 10, 'locale' => 'en',  'name' => 'Other Spirits', 'slug' => 'other-spirits'],
            ['category_id' => 10, 'locale' => 'it',  'name' => 'Altri liquori', 'slug' => 'altri-liquori'],

            ['category_id' => 11, 'locale' => 'en',  'name' => 'Gin', 'slug' => 'gin'],
            ['category_id' => 11, 'locale' => 'it',  'name' => 'Gin', 'slug' => 'gin'],

            ['category_id' => 12, 'locale' => 'en',  'name' => 'Mixers', 'slug' => 'mixers'],
            ['category_id' => 12, 'locale' => 'it',  'name' => 'Mixers', 'slug' => 'mixers'],

            ['category_id' => 13, 'locale' => 'en',  'name' => 'Soda', 'slug' => 'soda'],
            ['category_id' => 13, 'locale' => 'it',  'name' => 'Soda', 'slug' => 'soda'],

            ['category_id' => 14, 'locale' => 'en',  'name' => 'Juice', 'slug' => 'juice'],
            ['category_id' => 14, 'locale' => 'it',  'name' => 'Succhi', 'slug' => 'succhi'],

            ['category_id' => 15, 'locale' => 'en',  'name' => 'Energy Drinks', 'slug' => 'energy-drinks'],
            ['category_id' => 15, 'locale' => 'it',  'name' => 'Bevande energetiche', 'slug' => 'bevande-energetiche'],

            ['category_id' => 16, 'locale' => 'en',  'name' => 'Deals', 'slug' => 'deals'],
            ['category_id' => 16, 'locale' => 'it',  'name' => 'Offerte', 'slug' => 'offerte'],

            ['category_id' => 17, 'locale' => 'en',  'name' => 'Packs', 'slug' => 'packs'],
            ['category_id' => 17, 'locale' => 'it',  'name' => 'Packs', 'slug' => 'packs'],

            ['category_id' => 18, 'locale' => 'en',  'name' => 'Craft Beer', 'slug' => 'craft-beer'],
            ['category_id' => 18, 'locale' => 'it',  'name' => 'Birra artigianale', 'slug' => 'birra-artigianale'],

            ['category_id' => 19, 'locale' => 'en',  'name' => 'Other products', 'slug' => 'other-products'],
            ['category_id' => 19, 'locale' => 'it',  'name' => 'Altri prodotti', 'slug' => 'alti-prodotti'],

            ['category_id' => 20, 'locale' => 'en',  'name' => 'Premium', 'slug' => 'premium'],
            ['category_id' => 20, 'locale' => 'it',  'name' => 'Premium', 'slug' => 'premium'],

            ['category_id' => 22, 'locale' => 'en',  'name' => 'Summer Sales', 'slug' => 'summer-sales'],
            ['category_id' => 22, 'locale' => 'it',  'name' => 'Summer Sales', 'slug' => 'summer-sales'],
        ]);
    }
}
<file_sep>/resources/lang/en/address.php
<?php

return [
    'empty' => 'No address set',

    'api.update.success' => 'Updated address successfully',
    'api.destroy.success' => 'Address removed successfully',
    'api.error.404' => 'Address does not exist',
    'api.error.invalid' => 'Address was invalid',
];<file_sep>/app/Zugy/PaymentGateway/Gateways/AbstractGateway.php
<?php

namespace Zugy\PaymentGateway\Gateways;

use App\Payment;
use App\PaymentMethod;

abstract class AbstractGateway
{
    /**
     * Unique identifier for this payment method e.g. 'stripe' or 'paypal'
     * @var string
     */
    protected $methodName;

    /**
     * Stores an instance of the PaymentMethod model.
     * @var PaymentMethod
     */
    protected $paymentMethod;

    /**
     * AbstractGateway constructor.
     * @param PaymentMethod $paymentMethod
     */
    public function __construct(PaymentMethod $paymentMethod = null)
    {
        $this->paymentMethod = $paymentMethod;
    }


    /**
     * Adds or updates the payment method to the currently signed in user
     */
    abstract public function addOrUpdateMethod();

    /**
     * Fetch the PaymentMethod from DB
     * @return PaymentMethod | null
     */
    public function fetchPaymentMethod() {
        return auth()->user()->payment_methods()->where('method', '=', $this->methodName)->first();
    }

    /**
     * Store payment method in DB
     * @param array|null $payload
     * @return PaymentMethod
     */
    public function createPaymentMethod(array $payload = null) {
        return auth()->user()->payment_methods()->create([
            'method' => $this->methodName,
            'payload' => $payload
        ]);
    }


    /**
     * Update payment method in DB
     * @param array|null $payload
     * @return PaymentMethod
     */
    public function updatePaymentMethod(array $payload = null) {
        $this->paymentMethod->payload = $payload;
        $this->paymentMethod->save();

        return $this->paymentMethod;
    }

    /**
     * Sets the current method to default in DB
     * @param bool $setAsDefault
     */
    public function setAsDefault($setAsDefault = true) {

        //Make other payment methods false for that user first
        if($setAsDefault == true) {
            $userId = $this->paymentMethod->user_id;

            PaymentMethod::where('user_id', '=', $userId)->update(['isDefault' => false]);
        }
        $this->paymentMethod->isDefault = $setAsDefault;
        $this->paymentMethod->save();
    }

    /**
     * Charge the payment method
     * @param PaymentMethod $paymentMethod
     * @param String $amount E.g. '15.99'
     * @return Payment
     */
    abstract public function charge($amount);

    /**
     * Retrieve printable information on the payment method
     * @param PaymentMethod $paymentMethod
     * @return array
     */
    abstract public function getFormatted();
}<file_sep>/app/Http/routes.php
<?php

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the controller to call when that URI is requested.
|
*/

use Illuminate\Support\Facades\Route;

Route::group([
    'prefix' => (app()->environment('testing') ? 'en' : Localization::setLocale()),
    'middleware' => ['web', 'localeSessionRedirect', 'localizationRedirect' ]
], function() {
    Route::get('/', function () {
        return view('pages.home');
    });
});

Route::group([
    'prefix' => (app()->environment('testing') ? 'en' : Localization::setLocale()),
    'middleware' => [ 'web', 'localize', 'localeSessionRedirect', 'setLocale'] // Route translate middleware
],
function() {
    /*
     * Account
     */
    Route::group(['middleware' => 'auth'], function () {
        Route::get(Localization::transRoute('routes.account.index'), ['as' => 'your-account', 'uses' => 'AccountController@getHome']);
        Route::get(Localization::transRoute('routes.account.settings'), ['as' => 'your-account', 'uses' => 'AccountController@getSettings']);
        Route::get(Localization::transRoute('routes.account.orders'), ['uses' => 'AccountController@getOrders']);
    });

    Route::get(Localization::transRoute('routes.product'), ['uses' => 'ProductController@show']);

    Route::get(Localization::transRoute('routes.search'), ['uses' => 'ProductController@search']);
    /*
     * Shop
     */
    Route::get(Localization::transRoute('routes.shop.index'), ['uses' => 'ShopController@index', 'as' => 'shop']);
    Route::get(Localization::transRoute('routes.shop.category'), ['uses' => 'ShopController@category']);

    /*
     * Checkout
     */
    Route::get(Localization::transRoute('routes.checkout.landing'), ['uses' => 'CheckoutController@getCheckout']);

    Route::get(Localization::transRoute('routes.checkout.address'), ['uses' => 'CheckoutController@getCheckoutAddress']);
    Route::post(Localization::transRoute('routes.checkout.address'), ['uses' => 'CheckoutController@postCheckoutAddress']);
    Route::get(Localization::transRoute('routes.checkout.payment'), ['uses' => 'CheckoutController@getCheckoutPayment']);
    Route::post(Localization::transRoute('routes.checkout.payment'), ['uses' => 'CheckoutController@postCheckoutPayment']);
    Route::get(Localization::transRoute('routes.checkout.review'), ['uses' => 'CheckoutController@getCheckoutReview']);
    Route::post(Localization::transRoute('routes.checkout.review'), ['uses' => 'CheckoutController@postCheckoutReview']);
    Route::get(Localization::transRoute('routes.checkout.confirmation'), ['uses' => 'CheckoutController@getCheckoutConfirmation']);
    Route::get(Localization::transRoute('routes.checkout.gatewayReturn'), ['uses' => 'CheckoutController@getGatewayReturn']);

    Route::get(Localization::transRoute('routes.order.show'), ['uses' => 'OrderController@show']);

    /* Cart */
    Route::get(Localization::transRoute('routes.cart'), ['uses' => 'PageController@getCart']);

    Route::get(Localization::transRoute('routes.about-us'), function () {
        return view('pages.about-us');
    });

    Route::get(Localization::transRoute('routes.contact'), function () {
        return view('pages.contact');
    });

    Route::get(Localization::transRoute('routes.terms-and-conditions'), function() {
        return view('pages.terms-and-conditions.index');
    });
    Route::get(Localization::transRoute('routes.privacy-policy'), function() {
        return view('pages.privacy-policy.index');
    });

    Route::group(['prefix' => 'auth'], function () {
        Route::get('login', ['as' => 'login', 'uses' => 'Auth\AuthController@getLogin']);
        Route::post('login', ['uses' => 'Auth\AuthController@postLogin']);

        Route::get('register', ['uses' => 'Auth\AuthController@getRegister']);
        Route::post('register', ['uses' => 'Auth\AuthController@postRegister']);

        /*
         * Password reset
         */
        Route::get('password/email', ['uses' => 'Auth\PasswordController@getEmail']);
        Route::post('password/email', ['uses' => 'Auth\PasswordController@postEmail']);
        Route::get('password/reset/{token}', ['uses' => 'Auth\PasswordController@getReset']);
        Route::post('password/reset', ['uses' => 'Auth\PasswordController@postReset']);
        Route::post('password/change', ['uses' => 'Auth\PasswordController@postPasswordChange']);
    });
});

/*
 * No localisation needed
 */
Route::group(['prefix' => 'auth', 'middleware' => ['web']], function () {
    Route::get('login/facebook', 'Auth\AuthController@facebookLogin');
    Route::get('login/google', 'Auth\AuthController@googleLogin');
    Route::get('logout', ['as' => 'logout', 'uses' => 'Auth\AuthController@getLogout']);

    Route::post('adminLogin', ['uses' => 'Auth\AuthController@postAdminLogin']);
});

/* API */
Route::group(['prefix' => 'api', 'middleware' => ['api']], function () {
    Route::group(['prefix' => 'v1'], function () {
        //No auth required
        Route::patch('cart', 'API\CartController@bulkUpdate');
        Route::resource('cart', 'API\CartController', ['except' => [
            'update',
            'show'
        ]]);
        Route::get('postcode/check/{postcode}', ['uses' => 'API\PostcodeController@checkPostcode']);

        //Auth required
        Route::group(['middleware' => ['auth']], function () {
            Route::resource('address', 'API\AddressController');
            Route::resource('order', 'API\OrderController');
            Route::post('coupon/apply', 'API\CouponController@apply');
        });

        Route::group(['prefix' => 'shop'], function() {
            Route::get('statistics', 'API\ShopStatisticsController@getStatistics');
        });
    });
});

Route::group(['prefix' => 'admin', 'middleware' => ['web', 'auth', 'admin', 'setLocale']], function () {
    Route::get('', ['uses' => 'Admin\DashboardController@getDashboard']);

    Route::resource('catalogue', 'Admin\CatalogueController');
    Route::resource('customer', 'Admin\CustomerController');
    Route::resource('order', 'Admin\OrderController');
    Route::post('image/upload', 'Admin\ImageController@upload');
    Route::delete('image/{id}', 'Admin\ImageController@destroy');
});

Route::group(['middleware' => ['web']], function () {
    Route::get('test', function() {
    });
});
<file_sep>/resources/lang/en/ageSplash.php
<?php

return [
    'prompt' => 'Please verify your age to enter.',
    'over' => 'Over :age',
    'under' => 'Under :age',
    'sorry.title' => 'Over 18s only, sorry!',
    'sorry.desc' => 'Sorry, we only sell alcohol and deliver within an hour to those who are over the age of 18 only.',
    'sorry.party' => 'We will, however, be there with you to celebrate your 18th birthday!'
];<file_sep>/resources/lang/it/payment.php
<?php

return [
    'card-number' => 'Numero',
    'cardholder-name' => '<NAME>',
    'payment-method' => 'Metodo di pagamento',
];<file_sep>/app/Zugy/Cart/Cart.php
<?php

namespace Zugy\Cart;

use Gloudemans\Shoppingcart\Cart as BaseCart;
use App\Product;
use Zugy\Facades\Checkout;

class Cart extends BaseCart
{
    /**
     * Get shipping costs
     * @return int|mixed
     */
    public function shipping() {
        if($this->total() < config('site.minimumFreeShipping')) {
            return config('site.shippingFee');
        } else {
            return 0;
        }
    }

    public function totalBeforeVat()
    {
        return $this->grandTotal() - $this->vat();
    }

    public function grandTotal() {
        $grandTotal = $this->shipping() + $this->total() - $this->couponDeduction();

        if($grandTotal < 0) {
            throw new \RuntimeException("Total cannot be negative");
        }

        return $grandTotal;
    }

    public function couponDeduction()
    {
        $coupon = Checkout::getCoupon();

        if($coupon === null) {
            return 0;
        }

        if($coupon->percentageDiscount != null) {
            return round($this->total() * ($coupon->percentageDiscount / 100), 2);
        }

        if($coupon->flatDiscount != null) {
            if($coupon->flatDiscount > $this->total()) {
                return $this->total();
            }

            return $coupon->flatDiscount;
        }

        throw new \RuntimeException("Coupon type not set");
    }

    /**
     * Calculate VAT, including shipping fees
     * @return float
     */
    public function vat() {
        return $this->vatItems() + $this->vatShipping();
    }

    /**
     * Sum up VAT for items in cart
     * @return float
     */
    public function vatItems()
    {
        $totalVAT = 0;

        $productIDs = [];
        $cartProducts = [];
        foreach($this->content() as $row) {
            $productIDs[] = $row->id;
            $cartProducts[$row->id] = ['subtotal' => $row->subtotal];
        }

        $products = Product::with(['tax_class' => function($query) {
            $query->select('id', 'tax_rate');
        }])->select('id', 'tax_class_id')->whereIn('id', $productIDs)->get();

        foreach($products as $p) {
            $net = $cartProducts[$p->id]['subtotal'] / ((100 + $p->tax_class->tax_rate) / 100);
            $vat = $cartProducts[$p->id]['subtotal'] - $net;
            $totalVAT += round($vat, 2);
        }

        $coupon = Checkout::getCoupon();

        if($coupon != null && $coupon->percentageDiscount != null) {
            $totalVAT = round($totalVAT * ($coupon->percentageDiscount / 100), 2);
        } elseif($coupon != null && $coupon->flatDiscount != null) {
            $overallPercentageVat = $totalVAT / $this->total();
            $totalVAT = round(($this->total() - $this->couponDeduction()) * $overallPercentageVat, 2);
        }

        return $totalVAT;
    }

    public function vatShipping()
    {
        $fee = $this->shipping();
        $tax = config('site.shippingTax');

        $net = $fee / ((100 + $tax) / 100);

        $vatShipping = round($fee - $net, 2);

        return $vatShipping;
    }
}<file_sep>/resources/lang/it/cart.php
<?php

return [
    'shopping-cart' => 'Carrello',
    'free-shipping-reminder' => 'Ordina più di 20&euro; e  <b>la consegna diventa gratis</b>!',
    'mini-cart-empty' => 'Il tuo carrello è vuoto',
    'empty-cart-msg' => 'Non hai nulla nel carrello. Visita il <a  href=":storeUrl">negozio</a> per riempirlo!',
    'api.error.out-of-stock' => 'Impossibile aggiungere al carrello. Abbiamo solo :quantity lasciati in azione.',
];<file_sep>/database/seeds/ProductSeeder.php
<?php
use App\Product;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Storage;

class ProductSeeder extends Seeder
{
    public function run() {
        $product = Product::create([
            'stock_quantity' => 10,
            'price' => 12.99,
            'compare_price' => 19.99,
            'weight' => 0.50,
            'tax_class_id' => 1,

            'en' => [
                'title' => 'Absolut Vodka',
                'slug' => 'absolut-vodka',
                'description' => 'Absolut Vodka is a Swedish vodka made exclusively from natural ingredients, and unlike some other vodkas, it doesn\'t contain any added sugar. In fact Absolut is as clean as vodka can be. Still, it has a certain taste: Rich, full-bodied and complex, yet smooth and mellow with a distinct character of grain, followed by a hint of dried fruit.',
                'meta_description' => 'Buy Absolut Vokda online from Zugy today.',
            ],
            'it' => [
                'title' => 'Absolut Vodka',
                'slug' => 'absolut-vodka',
                'description' => 'Absolut una marca svedese di vodka, della V&S Group, prodotta a hus, Scania nel sud della Svezia.',
                'meta_description' => 'Acquista online da Absolut vokda Zugy oggi.',
            ]
        ]);

        $product->categories()->attach(5); //Vodka category

        $product->attributes()->attach(1, ['value' => 0.350]); //Volume
        $product->attributes()->attach(2, ['value' => 40.0]); //% Vol

        $img = Storage::disk('public')->get('img/demo/products/absolut-vodka/absolut-vodka.jpeg');
        Storage::disk(env('FILE_DISC'))->put('demo/absolut-vodka/absolut-vodka.jpeg', $img);

        $product->images()->create([
           'location' => 'demo/absolut-vodka/absolut-vodka.jpeg'
        ]);

        ####

        $product = Product::create([
            'stock_quantity' => 10,
            'price' => 15.00,
            'compare_price' => 16.36,
            'weight' => 0.50,
            'tax_class_id' => 1,

            'en' => [
                'title' => 'Bacardi 70cl',
                'slug' => 'bacardi',
                'description' => "<p>Bacardi's original, the Superior is a highly versatile white Rum which has been around since 1862.</p><p>Aged in oak barrels, look out for the smooth taste and the almond and tropical fruit aroma.</p>",
                'meta_description' => '',
            ],
            'it' => [
                'title' => 'Bacardi 70cl',
                'slug' => 'bacardi',
                'description' => '',
                'meta_description' => '',
            ]
        ]);

        $product->categories()->attach(6); //Rum category

        $product->attributes()->attach(1, ['value' => 0.700]); //Volume
        $product->attributes()->attach(2, ['value' => 37.5]); //% Vol

        $img = Storage::disk('public')->get('img/demo/products/bacardi/bacardi.jpg');
        Storage::disk(env('FILE_DISC'))->put('demo/bacardi/bacardi.jpg', $img);

        $product->images()->create([
            'location' => 'demo/bacardi/bacardi.jpg'
        ]);

        ####

        $product = Product::create([
            'stock_quantity' => 10,
            'price' => 15.00,
            'compare_price' => 16.57,
            'weight' => 0.80,
            'tax_class_id' => 1,

            'en' => [
                'title' => 'Havana Club 70cl',
                'slug' => 'havana',
                'description' => "",
                'meta_description' => '',
            ],
            'it' => [
                'title' => 'Havana Club 70cl',
                'slug' => 'havana',
                'description' => '',
                'meta_description' => '',
            ]
        ]);

        $product->categories()->attach(6); //Rum category

        $product->attributes()->attach(1, ['value' => 0.700]); //Volume
        $product->attributes()->attach(2, ['value' => 40.0]); //% Vol

        $img = Storage::disk('public')->get('img/demo/products/havana/havana.jpg');
        Storage::disk(env('FILE_DISC'))->put('demo/havana/havana.jpg', $img);

        $product->images()->create([
            'location' => 'demo/havana/havana.jpg'
        ]);

        ####

        $product = Product::create([
            'stock_quantity' => 10,
            'price' => 3.00,
            'compare_price' => null,
            'weight' => 0.60,
            'tax_class_id' => 1,

            'en' => [
                'title' => 'Heineken',
                'slug' => 'heineken',
                'description' => "",
                'meta_description' => '',
            ],
            'it' => [
                'title' => 'Heineken',
                'slug' => 'heineken',
                'description' => '',
                'meta_description' => '',
            ]
        ]);

        $product->categories()->attach(7); //Beer category

        $product->attributes()->attach(1, ['value' => 0.660]); //Volume
        $product->attributes()->attach(2, ['value' => 5]); //% Vol

        $img = Storage::disk('public')->get('img/demo/products/heineken/heineken.jpg');
        Storage::disk(env('FILE_DISC'))->put('demo/heineken/heineken.jpg', $img);

        $product->images()->create([
            'location' => 'demo/heineken/heineken.jpg'
        ]);

        ###

        $product = Product::create([
            'stock_quantity' => 10,
            'price' => 2.50,
            'compare_price' => null,
            'weight' => 0.60,
            'tax_class_id' => 1,

            'en' => [
                'title' => 'Peroni',
                'slug' => 'peroni',
                'description' => "",
                'meta_description' => '',
            ],
            'it' => [
                'title' => 'Peroni',
                'slug' => 'peroni',
                'description' => '',
                'meta_description' => '',
            ]
        ]);

        $product->categories()->attach(7); //Beer category

        $product->attributes()->attach(1, ['value' => 0.660]); //Volume
        $product->attributes()->attach(2, ['value' => 5.1]); //% Vol

        $img = Storage::disk('public')->get('img/demo/products/peroni/peroni.jpg');
        Storage::disk(env('FILE_DISC'))->put('demo/peroni/peroni.jpg', $img);

        $product->images()->create([
            'location' => 'demo/peroni/peroni.jpg'
        ]);

        ###

        $product = Product::create([
            'stock_quantity' => 10,
            'price' => 28.00,
            'compare_price' => 37.15,
            'weight' => 0.60,
            'tax_class_id' => 1,

            'en' => [
                'title' => 'Sauza Silver Tequila 100cl',
                'slug' => 'sauza-silver',
                'description' => "",
                'meta_description' => '',
            ],
            'it' => [
                'title' => 'Sauza Silver Tequila 100cl',
                'slug' => 'sauza-silver',
                'description' => '',
                'meta_description' => '',
            ]
        ]);

        $product->categories()->attach(8); //Tequila category

        $product->attributes()->attach(1, ['value' => 1.0]); //Volume
        $product->attributes()->attach(2, ['value' => 40.0]); //% Vol

        $img = Storage::disk('public')->get('img/demo/products/sauza-tequila/sauza-tequila.jpg');
        Storage::disk(env('FILE_DISC'))->put('demo/sauza-tequila/sauza-tequila.jpg', $img);

        $product->images()->create([
            'location' => 'demo/sauza-tequila/sauza-tequila.jpg'
        ]);

        ###

        $product = Product::create([
            'stock_quantity' => 10,
            'price' => 90.00,
            'compare_price' => 128.10,
            'weight' => 0.60,
            'tax_class_id' => 1,

            'en' => [
                'title' => 'Veuve Clicquot Champagne',
                'slug' => 'veuve-clicquot',
                'description' => "Veuve Clicquot",
                'meta_description' => '',
            ],
            'it' => [
                'title' => 'Peroni',
                'slug' => 'veuve-clicquot',
                'description' => '',
                'meta_description' => '',
            ]
        ]);

        $product->categories()->attach(4); //Wine & champagne category

        $product->attributes()->attach(1, ['value' => 0.750]); //Volume
        $product->attributes()->attach(2, ['value' => 12]); //% Vol

        $img = Storage::disk('public')->get('img/demo/products/veuve-clicquot/veuve-clicquot.jpg');
        Storage::disk(env('FILE_DISC'))->put('demo/veuve-clicquot/veuve-clicquot.jpg', $img);

        $product->images()->create([
            'location' => 'demo/veuve-clicquot/veuve-clicquot.jpg'
        ]);

        ###

        $product = Product::create([
            'stock_quantity' => 10,
            'price' => 90.00,
            'compare_price' => 128.10,
            'weight' => 0.60,
            'tax_class_id' => 1,

            'en' => [
                'title' => 'Moet And Chandon Brut Imperial Non Vintage Champagne',
                'slug' => 'moet-chandon-imperial',
                'description' => "",
                'meta_description' => '',
            ],
            'it' => [
                'title' => 'Moet And Chandon Brut Imperial Champagne',
                'slug' => 'moet-chandon-imperial',
                'description' => '',
                'meta_description' => '',
            ]
        ]);

        $product->categories()->attach(4); //Wine & champagne category

        $product->attributes()->attach(1, ['value' => 0.750]); //Volume
        $product->attributes()->attach(2, ['value' => 12]); //% Vol

        $img = Storage::disk('public')->get('img/demo/products/moet-chandon-imperial/moet-chandon-imperial.jpg');
        Storage::disk(env('FILE_DISC'))->put('demo/moet-chandon-imperial/moet-chandon-imperial.jpg', $img);

        $product->images()->create([
            'location' => 'demo/moet-chandon-imperial/moet-chandon-imperial.jpg'
        ]);
    }
}<file_sep>/resources/lang/it/opening-times.php
<?php

return [
    'title-delivery-time' => 'Orari di consegna',
    'when' => 'Ogni giorno dall\'una di pomeriggio fino alle 2 di notte',
    'closed' => 'Al momento non consegnamo.',
    'times' => 'I nostri orari di consegna sono:',
    'delivery-time' => 'Puoi ordinare adesso e selezionare un orario di consegna piú comodo per te, magari stasera  ( ͡ᵔ ͜ʖ ͡ᵔ )',
    'prompt-select' => 'Seleziona un orario per la consegna',
    'asap-closed-explanation' => 'Seleziona "ASAP" e ti spediremo l\'ordine appena saremo aperti',
];<file_sep>/resources/lang/it/order.php
<?php

return [
    'status.0' => 'Nuovo ordine',
    'status.1' => 'Stiamo elaborando',
    'status.2' => 'In consegna',
    'status.3' => 'Consegnato',
    'status.4' => 'Anullato',
    'number' => 'Numero ordine',
    'date' => 'Data ordine',
    'create' => 'Ordine effettuato',
    'completed' => 'Ordine completato',
    'details' => 'Dettagli',
    'summary' => 'Riassunto ordine',
    'history' => 'Cronologia ordini',
    'api.update.success' => 'Ordine aggiornato',
    'api.error.status-same' => 'l\'ordine è già stato impostato',
    'email.confirmation.subject' => 'Conferma di ordine Zugy ricevuta [#:id]',
    'email.confirmation.paid' => 'Totale Pagato',
    'email.delivery.subject' => 'Il tuo ordine [#:id] è in consegna',
    'email.delivery.driver' => 'Il nostro Driver è in cammino',
    'email.delivery.trouble' => 'Ti contatteremo qualora ci fossero problemi a <b>:phone</b>',
    'email.cancelled.subject' => 'Il tuo ordine [#:id] è anullato',
    'email.cancelled.questions' => 'Se hai qualche domanda rispetto all\'anullamento rispondi a questa e-mail o chiamaci :phone.',
    'email.track.description' => 'Puoi rintracciare il tuo ordine cliccando il tasto sotto',
    'email.track' => 'Rintraccia ordine',
    'sign-in' => '<a href=":loginUrl">Accedi per vedere</a> più informazione sul tuo ordine',
    'total-before-vat' => 'Totale senza IVA',
    'vat' => 'IVA',
    'include-vat' => 'Ordine totale include',
    'status-changed' => 'Stato modificato in <b>:status</b>',
];<file_sep>/resources/assets/js/order.js
(function( order, $, undefined ) {
    var apiEndpoint = '/api/v1/order';

    /**
     * Check if postcode is in delivery range
     * @param orderId
     * @param status
     */
    order.updateStatus = function(orderId, status) {
        $.ajax({
            type: 'PATCH',
            url: apiEndpoint + '/' + orderId,
            data: {
                order_status: status
            },
            success: function(data) {
                swal({
                    title: data.message,
                    type: 'success',
                    timer: 1000,
                    showConfirmButton: false
                });
                $.pjax.reload('#container');
            },
            error: function(xhr, status, error) {
                var err = eval("(" + xhr.responseText + ")");
                swal(err.message, JSON.stringify(err.errors), "error");
            }
        });
    };

}( window.order = window.order || {}, jQuery ));<file_sep>/app/Zugy/Repos/Product/DbProductRepository.php
<?php

namespace Zugy\Repos\Product;

use App\Category;
use App\Language;
use App\Product;
use App\ProductDescription;
use App\ProductTranslation;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\DB;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
use Zugy\Repos\Category\CategoryRepository;
use Zugy\Repos\DbRepository;

class DbProductRepository extends DbRepository implements ProductRepository
{
    /**
     * @var CategoryRepository
     */
    protected $categoryRepo;
    /**
     * @var Product
     */
    protected $model;

    /**
     * DbProductRepository constructor.
     * @param $categoryRepo CategoryRepository
     */
    public function __construct(Product $model, CategoryRepository $categoryRepo)
    {
        $this->categoryRepo = $categoryRepo;
        $this->model = $model;
    }

    /**
     * Fetch all products
     * @param string $sort
     * @param string $direction
     * @return
     */
    public function all($sort = 'sales', $direction = 'desc')
    {
        $query = $this->productQueryBuilder()->orderBy($sort, $direction);

        $orderedProductIds = collect($query->get())->pluck('id');

        $orderedProductIdsStr = $orderedProductIds->implode(',');
        $products = $this->model->with('translations')
            ->whereIn('id', $orderedProductIds)
            ->orderByRaw("FIELD(id, $orderedProductIdsStr)")
            ->paginate(15);

        return $products;
    }

    /**
     * Fetch all products for a category slug
     * @param $category_slug
     * @param string $sort
     * @param string $direction
     * @return
     */
    public function category($category_slug, $sort = 'sales', $direction = 'desc')
    {
        $category = $this->categoryRepo->getBySlug($category_slug);

        $categoryIds = $this->categoryRepo->children($category->id);

        $productIds = collect(
            DB::table('products_to_categories')
                ->whereIn('category_id', $categoryIds)
                ->get()
            )->unique('product_id')->pluck('product_id');

        $query = $this->productQueryBuilder()
                    ->orderBy($sort, $direction)
                    ->whereIn('products.id', $productIds); // Category filter;

        $orderedProductIds = collect($query->get())->pluck('id');

        if($orderedProductIds->isEmpty()) {
            return new Paginator([], 15);
        }

        $orderedProductIdsStr = $orderedProductIds->implode(',');
        $products = $this->model->with(['translations', 'images'])
                                ->whereIn('id', $orderedProductIds)
                                ->orderByRaw("FIELD(id, $orderedProductIdsStr)")
                                ->paginate(15);

        return $products;
    }

    /**
     * Fetch a product by its slug
     * @param $slug
     * @return mixed
     */
    public function getBySlug($slug)
    {
        $product = $this->model->with(['images', 'attributes', 'translations'])->whereHas('translations', function($query) use ($slug) {
           $query->where('locale', '=', LaravelLocalization::getCurrentLocale())
                 ->where('slug', '=', $slug);
        })->first();

        return $product;
    }

    public function search($query)
    {
        $algoliaResults = \App\Product::search($query, [
            'restrictSearchableAttributes' => ['translation_' . \Localization::getCurrentLocale()],
        ]);

        $productIds = [];
        foreach($algoliaResults['hits'] as $hit) {
            $productIds[] = $hit['objectID'];
        }

        return $this->model->whereIn('id', $productIds)->paginate(15);
    }

    /**
     * Build joined table so that sales for product can be calculated
     * @return mixed
     */
    private function productQueryBuilder() {
         return DB::table('products')
            ->leftJoin('order_items', 'products.id', '=', 'order_items.product_id')
            ->leftJoin('orders', 'orders.id', '=', 'order_items.order_id')
            ->selectRaw('products.*, SUM(
                CASE WHEN orders.order_status = 4 THEN 
                    0 
                ELSE 
                    order_items.quantity 
                END) as sales') //Ignore cancelled orders
            ->groupBy('products.id')
            ->where('products.stock_quantity', '>', 0); //Hide items that are out of stock
    }
}<file_sep>/resources/lang/en/product.php
<?php

return [
    'product' => 'Product',
    'search-results' => 'Search results for <i>:query</i>',
    'count' => '{0} No products|{1} 1 product|[2,Inf] :count products',
    'stock' => 'Stock Quantity',
    'in-stock' => 'In Stock',
    'out-of-stock' => 'Out of stock',
    'stock-warning' => 'Only :count left in stock.',
    'item-weight' => 'Item weight',
    'tabs.details' => 'Details',
    'quantity' => 'Quantity',
    'price' => 'Price',
    'edit' => 'Edit product',
    'search.empty' => 'No results matching the query <b>":query"</b>',
    'category.empty' => 'No products in this category',
    'category.title' => 'Category',
    '404' => 'That product does not exist',

    'form.name.label' => 'Product Name',
    'form.add.label' => 'Add a product',

    'filter.search' => 'Searching for <b>:name</b>. :count result(s) found.',

    'sort.popular' => 'Most popular',
    'sort.price.highest' => 'Highest price',
    'sort.price.lowest' => 'Lowest price',

    'title.category' => 'Shop for :category',
    'title.search' => 'Search results for :query',
    'title.shop' => 'Shop for Alcohol'
];<file_sep>/resources/lang/en/errors.php
<?php

return [
    'unknown' => 'An unknown error occurred. Please try again',
    'stripe.invalid_number' =>       "The card number is not a valid credit card number.",
    'stripe.invalid_expiry_month' => "The card's expiration month is invalid.",
    'stripe.invalid_expiry_year' =>  "The card's expiration year is invalid.",
    'stripe.invalid_cvc' =>          "The card's security code is invalid.",
    'stripe.incorrect_number' =>     "The card number is incorrect.",
    'stripe.expired_card' =>         "The card has expired.",
    'stripe.incorrect_cvc' =>        "The card's security code is incorrect.",
    'stripe.incorrect_zip' =>        "The card's zip code failed validation.",
    'stripe.card_declined' =>        "The card was declined.",
    'stripe.missing' =>              "There is no card on a customer that is being charged.",
    'stripe.processing_error' =>     "An error occurred while processing the card.",
    '404.message' => 'Page not found',
    '404.description' => 'We were unable to find the page you were looking for, sorry!',
    '500.message' => 'An internal error occurred',
    '500.description' => 'An error occurred, this error has been reported to us and we will fix it as soon as we can - sorry!',
];<file_sep>/config/services.php
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Third Party Services
    |--------------------------------------------------------------------------
    |
    | This file is for storing the credentials for third party services such
    | as Stripe, Mailgun, Mandrill, and others. This file provides a sane
    | default location for this type of information, allowing packages
    | to have a conventional place to find your various credentials.
    |
    */

    'aws' => [
        'region' => env('AWS_REGION'),
        'bucket' => env('AWS_BUCKET')
    ],

    'mailgun' => [
        'domain' => env('MAILGUN_DOMAIN'),
        'secret' => env('MAILGUN_SECRET'),
    ],

    'ses' => [
        'key'    => '',
        'secret' => '',
        'region' => 'us-east-1',
    ],

    'stripe' => [
        'secret' => env('STRIPE_SECRET'),
        'public' => env('STRIPE_PUBLIC'),
    ],

    'google' => [
        'client_id' => env('GOOGLE_CLIENT_ID'),
        'client_secret' => env('GOOGLE_SECRET'),
        'redirect' => env('APP_URL') . '/auth/login/google',
    ],
    'facebook' => [
        'client_id' => env('FACEBOOK_CLIENT_ID'),
        'client_secret' => env('FACEBOOK_SECRET'),
        'redirect' => env('APP_URL') . '/auth/login/facebook',
    ],

    'rollbar' => [
        'access_token' => env('ROLLBAR_ACCESS_TOKEN'),
        'post_client_item' => env('ROLLBAR_POST_CLIENT_ITEM'),
        'level' => 'error',
    ],

    'paypal' => [
        'username'  => env('PAYPAL_USERNAME'),
        'password'  => env('PAYPAL_PASSWORD'),
        'signature' => env('PAYPAL_SIGNATURE'),
        'testMode'  => env('PAYPAL_TESTMODE') == "true",
    ],

    'pushbullet' => [
        'token' => env('PUSHBULLET_TOKEN'),
        'channels' => [
            'order' => env('PUSHBULLET_ORDER_CHANNEL'),
        ]
    ]

];
<file_sep>/app/Exceptions/TooManyOrdersException.php
<?php

namespace App\Exceptions;


class TooManyOrdersException extends \Exception
{

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

return  [
    "name" => 'Zugy',
    'address' => 'Azalia Naviglio Grande 36, 20141 Milan, Italy',
    'phone' => '(+39) 320 8908315',

    'maxImageSize' => 10000000, //10MB

    /*
     *  Cart
     */
    'minimumFreeShipping' => 20, //Minimum order value in Euros for free shipping
    'shippingFee' => 3, //Cart fee
    'shippingTax' => 22,

    /*
     * Email
     */
    'email' => [
        'support' => '<EMAIL>',
        'logo_path' => env('APP_URL') . '/img/zugy-logo-dark.png', //Full URL to logo
    ],

    /*
     * Pushbullet
     */
    'pushbullet' => [
        'channels' => [
            'orders' => env('PUSHBULLET_ORDER_CHANNEL'),
        ],
    ],
];<file_sep>/app/OAuthAuthorisations.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class OAuthAuthorisations extends Model
{
    protected $table = 'users_to_oauth_authorisations';

    protected $fillable = ['network', 'network_user_id', 'user_id'];

    public function user() {
        $this->belongsTo('App\User');
    }
}
<file_sep>/app/Zugy/Helpers/Maps/Maps.php
<?php

namespace Zugy\Helpers\Maps;


class Maps
{
    protected $apiUrl = 'http://maps.google.com/maps?&q=';

    public function getGoogleMapsUrl($line_1, $line_2 = null, $city, $postcode) {
        $address = [$line_1];

        if($line_2) $address[] = $line_2;

        $address[] = $city;
        $address[] = $postcode;

        return $this->apiUrl . urlencode(implode(', ', $address));
    }
}<file_sep>/app/Zugy/PaymentGateway/Gateways/PayPal.php
<?php

namespace Zugy\PaymentGateway\Gateways;

use App\Payment;
use App\PaymentMethod;
use Omnipay\Omnipay;
use Carbon\Carbon;
use Zugy\PaymentGateway\Exceptions\PaymentFailedException;

class PayPal extends AbstractGateway
{
    protected $methodName = 'paypal';

    protected $gateway;

    /**
     * PayPal constructor.
     * @param string $methodName
     */
    public function __construct(PaymentMethod $paymentMethod = null)
    {
        parent::__construct($paymentMethod);

        $this->gateway = Omnipay::create('PayPal_Express');

        $this->gateway->initialize([
            'username'  => config('services.paypal.username'),
            'password'  => config('services.paypal.password'),
            'signature' => config('services.paypal.signature'),
            'testMode'  => config('services.paypal.testMode'),
        ]);
    }


    public function addOrUpdateMethod()
    {
        $this->paymentMethod = $this->fetchPaymentMethod();

        if($this->paymentMethod === null) {
            $this->paymentMethod = $this->createPaymentMethod();
        }

        $this->setAsDefault(request('defaultPayment') !== null);

        return $this->paymentMethod;
    }

    public function charge($amount)
    {
        $parameters = [
            'cancelUrl' => localize_url('routes.checkout.gatewayReturn'), //TODO Implement cancel URL
            'returnUrl' => localize_url('routes.checkout.gatewayReturn'),
            'description' => 'Zugy Order',
            'cardReference' => 'ZUGY',
            'amount'    => $amount,
            'currency'  => 'EUR'
        ];

        if(request()->has('token')) {
            $response = $this->gateway->completePurchase($parameters)->send();

            $data = $response->getData();

            if($data['ACK'] !== 'Success') {
                \Log::warning('Payment failed');
                throw new PaymentFailedException();
            }

            $payment = new Payment();

            $payment->paid = Carbon::now();

            $payment->metadata = [
                'id' => $data['PAYMENTINFO_0_TRANSACTIONID'],
                'currency' => $data['PAYMENTINFO_0_CURRENCYCODE'],
            ];

            $payment->status = 1; //Mark as paid

            $payment->amount = $data['PAYMENTINFO_0_AMT'];
            $payment->currency = $data['PAYMENTINFO_0_CURRENCYCODE'];
            $payment->method = $this->paymentMethod->method;

            \Log::info('Payment success');

            return $payment;
        }

        $response = $this->gateway->purchase($parameters)->send();

        return redirect($response->getRedirectUrl()); //Redirect to PayPal
    }

    public function getFormatted()
    {
        return [
            'method' => $this->methodName,
        ];
    }

}<file_sep>/app/Http/Controllers/API/AddressController.php
<?php

namespace App\Http\Controllers\Api;

use App\Address;
use App\Services\CreateOrUpdateAddress;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Gate;
use Zugy\Facades\Checkout;


class AddressController extends Controller
{
    public function index()
    {
        return auth()->user()->addresses()->get();
    }

    public function show($id)
    {
        $address = Address::findOrFail($id);

        if(Gate::denies('show', $address)) {
            abort(403);
        }

        return $address;
    }

    public function update(Request $request, CreateOrUpdateAddress $addressService, $id)
    {
        $address = Address::findOrFail($id);

        if(Gate::denies('update', $address)) {
            abort(403);
        }

        $result = $addressService->delivery($request->all(), $address);

        if( ! $result instanceof Address) {
            return response()->json(['status' => 'error', 'message' => trans('address.api.error.invalid'), 'errors' => $result->errors()], 422);
        }

        return ['status' => 'success', 'message' => trans('address.api.update.success')];
    }

    public function destroy($id)
    {
        try {
            $address = Address::findOrFail($id);
        } catch(ModelNotFoundException $e) {
            return response()->json(['status' => 'failure', 'message' => trans('address.api.error.404')], 404);
        }

        //Unset session addresses
        if(Checkout::getBillingAddress()->id == $id) Checkout::forgetBillingAddress();
        if(Checkout::getShippingAddress()->id == $id) Checkout::forgetShippingAddress();

        $address->delete();

        return ['status' => 'success', 'message' => trans('address.api.destroy.success')];
    }
}<file_sep>/app/Http/Controllers/CheckoutController.php
<?php

namespace App\Http\Controllers;

use App\Address;
use App\Exceptions\OutOfStockException;
use App\Exceptions\TooManyOrdersException;
use App\Order;
use App\Services\CreateOrUpdateAddress;
use App\Services\Order\PlaceOrder;
use App\Services\Payment\PaymentMethodService;
use Illuminate\Http\Request;

use App\Http\Controllers\Controller;
use Cart;
use Checkout;
use Zugy\Facades\PaymentGateway;
use Zugy\PaymentGateway\Exceptions\PaymentFailedException;

class CheckoutController extends Controller
{
    public function __construct() {
        $this->middleware('checkout', ['except' => ['getCheckout']]);
    }

    /**
     * GET /checkout
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
     */
    public function getCheckout(Request $request)
    {
        if(Cart::count() === 0) {
            return redirect(localize_url('routes.cart'));
        }

        if(auth()->guest()) {
            session()->put('url.intended', localize_url('routes.checkout.address'));
            return view('pages.checkout.before');
        }

        if(Checkout::getShippingAddress() === null) {
            return redirect(localize_url('routes.checkout.address'));
        }

        if(Checkout::getPaymentMethod() === null) {
            return redirect(localize_url('routes.checkout.payment'));
        }

        return redirect(localize_url('routes.checkout.review'));
    }

    /**
     * /checkout/address
     */
    public function getCheckoutAddress()
    {
        $addresses = auth()->user()->addresses()->get();

        return view('pages.checkout.address')->with(['addresses' => $addresses]);
    }

    public function postCheckoutAddress(Request $request, CreateOrUpdateAddress $handler) {
        if($request->has('delivery.addressId')) {
            $delivery = Address::find($request->input('delivery.addressId'));

            Checkout::setShippingAddress($delivery);
            return redirect(localize_url('routes.checkout.payment'));
        } else {
            $delivery = $handler->delivery($request->input('delivery'));

            if(! $delivery instanceof Address) {
                return redirect()->back()->withErrors($delivery)->withInput();
            }

            if(!$request->has('delivery.billing_same')) {
                $billing = $handler->billing($request->input('billing'));
                if(! $billing instanceof Address) {
                    return redirect()->back()->withErrors($billing)->withInput();
                }
            } else {
                $billing = $delivery;
            }
        }

        Checkout::setShippingAddress($delivery);
        Checkout::setBillingAddress($billing);

        return redirect(localize_url('routes.checkout.payment'));
    }

    /**
     * GET /checkout/payment
     * @return \Illuminate\View\View
     */
    public function getCheckoutPayment()
    {
        $paymentMethods = auth()->user()->payment_methods()->get();

        $stripe = $paymentMethods->where('method', 'stripe')->first();

        if($stripe !== null) {
            $cards = PaymentGateway::set($stripe)->listCards($stripe);
        }

        if(Checkout::getShippingAddress() === null) {
            return redirect(localize_url('routes.checkout.address'))->with('info', trans('checkout.address.prompt'));
        }

        return view('pages.checkout.payment')->with(compact('cards'));
    }

    /**
     * POST /checkout/payment
     * @param Request $request
     * @param PaymentMethodService $handler
     * @return $this|bool|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     * @throws \Exception
     */
    public function postCheckoutPayment(Request $request, PaymentMethodService $paymentHandler)
    {
        try {
            $result = $paymentHandler->setMethod($request);
        } catch(\Stripe\Error\InvalidRequest $e) {
            return redirect()->back()->withErrors([trans('errors.unknown')]);
        } catch(\Stripe\Error\Card $e) {
            $errorMsg = trans('errors.stripe.' . $e->getStripeCode());
            return redirect()->back()->withErrors([$errorMsg])->withInput();
        } catch(Exception $e) {
            return redirect()->back()->withErrors([trans('errors.unknown') . $e->getMessage()])->withInput();
        }

        if($result !== true) return $result;

        return redirect(localize_url('routes.checkout.review'));
    }


    /**
     * /checkout/review
     * @return \Illuminate\View\View
     */
    public function getCheckoutReview()
    {
        //Fetch delivery address information
        $shippingAddress = Checkout::getShippingAddress();
        $paymentMethod = Checkout::getPaymentMethod();

        if($shippingAddress === null) {
            return redirect(localize_url('routes.checkout.address'))->with('info', trans('checkout.address.prompt'));
        }

        if($paymentMethod === null) {
            return redirect(localize_url('routes.checkout.payment'))->with('info', trans('checkout.payment.form.title'));
        }

        $cart = Cart::content();

        //Fetch payment information
        $payment = $paymentMethod->getFormatted();

        return view('pages.checkout.review')->with([
            'cart' => $cart,
            'shippingAddress' => $shippingAddress,
            'payment' => $payment,
            'coupon' => Checkout::getCoupon()
        ]);
    }

    /**
     * Place the actual order finally
     * POST /checkout/review
     * @param Request $request
     * @param PlaceOrder $service
     * @return $this|Order
     */
    public function postCheckoutReview(Request $request, PlaceOrder $service)
    {
        $user = auth()->user();

        try {
            $result = $service->handler($user);
        } catch(OutOfStockException $e) {
            return redirect(localize_url('routes.checkout.review'))->withErrors($e->getErrorMessages());
        } catch(\Stripe\Error\Card $e) { //Card was rejected
            $stripeErrorMsg = trans('errors.stripe.' . $e->getStripeCode());
            return redirect(localize_url('routes.checkout.review'))->withErrors([
                $stripeErrorMsg,
                trans('checkout.payment.form.error.different', ['paymentUrl' => localize_url('routes.checkout.payment')])
            ]);
        } catch(TooManyOrdersException $e) {
            return redirect(localize_url('routes.checkout.review'))
                ->withErrors([trans('checkout.review.error.too-fast')]);
        } catch(PaymentFailedException $e) {
            return redirect(localize_url('routes.checkout.review'))
                ->withErrors([trans('checkout.payment.form.error.different', ['paymentUrl' => localize_url('routes.checkout.payment')])]);
        } catch(\Exception $e) {
            \Log::alert($e);
            return redirect()->back()->withErrors([trans('errors.unknown')]);
        }

        if(!$result instanceof Order) return $result;

        return redirect(localize_url('routes.order.show', ['id' => $result->id]))
            ->with([
                'success' => trans('checkout.order.success'),
                'orderConfirmation' => true
            ]);
    }

    /**
     * Redirect back here from payment gateway e.g. PayPal
     */
    public function getGatewayReturn(Request $request, PlaceOrder $service)
    {
        return $this->postCheckoutReview($request, $service);
    }

    public function getCheckoutConfirmation(Request $request) {
        if(!auth()->guest()) {
            $order = auth()->user()->orders()->with('items')->find($request->get('order'));
        } else {
            if(!session()->has('order.id')) {
                return redirect(localize_url('routes.cart'));
            } else {
                $order = Order::with('items')->find(session('order.id'));
            }
        }

        return view('pages.checkout.confirmation')->with(['order' => $order]);
    }
}
<file_sep>/app/Zugy/ActivityLogParser/ActivityLogParserServiceProvider.php
<?php

namespace Zugy\ActivityLogParser;

use Illuminate\Support\ServiceProvider;

class ActivityLogParserServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind('activityLogParser', function()
        {
            return new ActivityLogParser();
        });
    }
}<file_sep>/database/seeds/AttributesSeeder.php
<?php
use App\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Seeder;

class AttributesSeeder extends Seeder
{
    public function run() {
        Model::unguard();

        Attribute::create([
            'id' => 1,
            'en' => [
                'name' => 'Volume',
                'unit' => 'litres'
            ],
            'it' => [
                'name' => 'Volume',
                'unit' => 'litri'
            ]
        ]);

        Attribute::create([
            'id' => 2,
            'en' => [
                'name' => 'Alcohol Content',
                'unit' => '% Vol'
            ],
            'it' => [
                'name' => 'Grado alcolico',
                'unit' => '% Vol'
            ]
        ]);
    }
}<file_sep>/app/Zugy/DeliveryTime/Exceptions/ClosedException.php
<?php

namespace Zugy\DeliveryTime\Exceptions;

class ClosedException extends \Exception
{

}<file_sep>/app/Listeners/LogOrderStatus.php
<?php

namespace App\Listeners;

use App\Events\OrderStatusChanged;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class LogOrderStatus
{
    /**
     * Handle the event.
     *
     * @param  OrderStatusChanged  $event
     * @return void
     */
    public function handle(OrderStatusChanged $event)
    {
        \Activity::log([
            'contentId'   => $event->order->id,
            'contentType' => 'Order',
            'action'      => 'status-change',
            'description' => $event->order->order_status,
        ]);
    }
}
<file_sep>/tests/AdminOrdersTest.php
<?php

class AdminOrdersTest extends TestCase
{
    protected $admin;

    public function setUp()
    {
        parent::setUp();

        $this->admin = factory(App\User::class, 'admin')->make();
    }

    public function testOrdersIndex()
    {
        $this->actingAs($this->admin)->visit('admin/order');
    }
}<file_sep>/tests/FilePermissionsTest.php
<?php

class FilePermissionsTest extends TestCase
{
    public function testProductImageUpload()
    {
        $path = public_path() . '/uploads';

        $this->assertTrue(is_writable($path));
    }

    public function testBootstrapCache()
    {
        $path = base_path() . '/bootstrap/cache';

        $this->assertTrue(is_writable($path));
    }

    public function testStorage()
    {
        $path = storage_path();

        $this->assertTrue(is_writable($path));
    }
}<file_sep>/resources/lang/it/admin.php
<?php
return [
    'incomplete-orders' => 'ordini incompleti',
    'revenue-yesterday' => 'entrate ieri',
    'revenue-this-month' => 'Ricavi mensili',
    'details' => 'Dettagli',
    'time' => 'Tempo',
    'user' => 'Utente',
    'description' => 'Descrizione',
    'meta_description' => 'Meta descrizione',
    'meta_description.desc' => 'Questa è la descrizione che comparirà sugli elenchi dei motori di ricerca.',
    'upload-images' => 'Caricare immagini',
    'pricing' => 'Prezzi',
    'incl-tax' => 'Tasse incluse',
    'compare-price.label' => 'filtra per prezzo',
    'tax_class' => 'Tipo di tassa',
    'attributes.legend' => 'caratteristiche',
    'attributes.desc' => 'Inserisci dettagli prodotto come volume, grado alcolico, etc.',
    'organisation' => 'Organizzazione',
    'miscellaneous' => 'Misto',
    'created_at' => 'Creato il',
    'last_login' => 'Ultimo accesso',
    'current_basket' => 'Articoli presenti nel carrello',
    'dashboard.title' => 'Panello di controllo',
    'catalogue.title' => 'Catalogo',
    'catalogue.image.thumbnail' => 'Impostare questo come miniatura',
    'catalogue.image.delete.confirmation' => 'Sei sicuro di voler eliminare questa immagine?',
    'catalogue.product.delete.confirmation' => 'Sicuro di voler cancellare questo prodotto?',
    'catalogue.product.delete.success' => 'Prodotto cancellato',
    'customers.title' => 'Clienti',
    'customers.singular' => 'Cliente',
    'customers.name.label' => 'Nome cliente',
    'customers.total' => 'Totale ordine',
    'customers.address.default' => 'Indirizzo preimpostato',
    'customers.revenue.lifetime' => 'Totale netto entrate',
    'customers.orders_total' => 'Totale ordini',
    'orders.title' => 'Ordini',
    'orders.show.title' => 'ordine',
    'orders.placed-by' => 'Piazzato per',
    'orders.paid' => 'Pagato',
    'orders.form.id.label' => 'Identificativo ordine',
    'orders.filter.incomplete' => 'Filtro incompleto',
    'orders.filter.incomplete.info' => 'Visualizzazioni ordini incomplete',
];<file_sep>/resources/assets/js/search.js
$('.getFullSearch').click(function () {
    $('#search-full').addClass('active')
        .find('input').focus();

})

$('#search-close').click(function () {
   $(this).parent().removeClass('active');
});

$('#search-form').submit(function (e) {
    e.preventDefault();

    var $field = $(this).find('input').eq(0);
    var query = $field.val();

    if(query == '') {
        return;
    }

    var searchUrl = $field.data('search-url') + '/' + query;

    window.open(searchUrl, "_self");
});<file_sep>/readme.md
# Zugy eCommerce

Laravel project for Zugy eCommerce store.

## Features

* Admin panel for managing products
* Coupons
* Rider push notifications when an order is placed
* Order status tracking
* Payment methods
  * Stripe
  * Paypal
  * Cash

## Docker

To start the Docker environment run

```
docker-compose up -d nginx mysql redis
```

Inside `.env` set `DB_HOST` and `REDIS_HOST`

```
DB_HOST=mysql

REDIS_HOST=redis
```

Connect to a container:

```
docker exec -it workspace bash
```

## Installation

Install composer dependencies:

```
composer install
```

Migrate Database tables

```
php artisan migrate
```

Seed the Database

```
php artisan db:seed
```

## Run tests

```
phpunit
```

## Deployment Checklist

* Verify all API keys are working and are not test keys
* Verify database backups are working
* Product table should be synced with Algolia
* Redis is installed and running
* Following locales should be installed (check with `locale -a`):
  * it_IT.utf8
  * en_GB.utf8
* Run tests on production
* Ensure that Real Visitor IP is revealed by installing additional modules for Apache or nginx due to Cloudflare
<file_sep>/app/Http/Controllers/ShopController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use Zugy\Repos\Category\CategoryRepository;
use Zugy\Repos\Product\ProductRepository;

class ShopController extends Controller
{
    /**
     * @var ProductRepository
     */
    private $productRepo;
    /**
     * @var CategoryRepository
     */
    private $categoryRepo;

    /**
     * ShopController constructor.
     * @param $productRepo ProductRepository
     */
    public function __construct(ProductRepository $productRepo, CategoryRepository $categoryRepo)
    {
        $this->productRepo = $productRepo;
        $this->categoryRepo = $categoryRepo;
    }

    /**
     * Display a listing of the resource.
     *
     * @return Response
     */
    public function index(Request $request)
    {
        if($request->has('sort')) {
            $sort = $request->input('sort');
        } else {
            $sort = 'sales';
        }

        if($request->has('direction')) {
            $direction = $request->input('direction');
        } else {
            $direction = 'desc';
        }

        $products = $this->productRepo->all($sort, $direction);

        return view('pages.product.product-list')->with(compact('products', 'category'));
    }

    public function category(Request $request, $category_slug) {
        if($request->has('sort')) {
            $sort = $request->input('sort');
        } else {
            $sort = 'sales';
        }

        if($request->has('direction')) {
            $direction = $request->input('direction');
        } else {
            $direction = 'desc';
        }

        $category = $this->categoryRepo->getBySlug($category_slug);

        $translations = $category->translations()->pluck('slug', 'locale');

        foreach($translations as $locale => $slug) {
            $translations[$locale] = \Localization::getURLFromRouteNameTranslated($locale, 'routes.shop.category', ['slug' => $slug]);
        }

        $products = $this->productRepo->category($category_slug, $sort, $direction);

        return view('pages.product.product-list')->with(compact('products', 'category', 'translations'));
    }
}
<file_sep>/app/Zugy/Charts/ChartsServiceProvider.php
<?php

namespace Zugy\Charts;

use Illuminate\Support\ServiceProvider;

class ChartsServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind('charts', function()
        {
            return new Charts();
        });
    }
}<file_sep>/app/Http/Controllers/Admin/CatalogueController.php
<?php

namespace App\Http\Controllers\Admin;

use App\Attribute;
use App\Language;
use App\Product;
use App\Services\CreateOrUpdateProduct;
use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Gate;
use Zugy\Repos\Product\ProductRepository;

class CatalogueController extends Controller
{
    protected $productRepo;

    /**
     * CatalogueController constructor.
     * @param $productRepo ProductRepository
     */
    public function __construct(ProductRepository $productRepo)
    {
        $this->productRepo = $productRepo;
    }

    /**
     * Display a listing of the resource.
     *
     * @return Response
     */
    public function index(Request $request)
    {
        if($request->has('product_name')) {
            $products = $this->productRepo->search($request->input('product_name'));
        } else {
            $products = Product::with('translations')->paginate(30);
        }

        return view('admin.pages.catalogue.index')->with(['products' => $products]);
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return Response
     */
    public function create()
    {
        if(Gate::denies('create', new Product())) {
            abort(403);
        }

        $languages = Language::all();
        $attributes = Attribute::all();

        return view('admin.pages.catalogue.create')->with([
            'product' => new Product(),
            'languages' => $languages,
            'attributes' => $attributes,
            'translations' => null,
            'category' => null,
        ]);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @return Response
     */
    public function store(Request $request, CreateOrUpdateProduct $service)
    {
        if(Gate::denies('create', new Product())) {
            return redirect()->back()->withErrors('You do not have permission to create new products');
        }

        return $service->handler($request);
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function edit($id)
    {
        $product = \App\Product::with(['translations', 'categories', 'attributes', 'images'])->findOrFail($id);
        $languages = Language::all();
        $attributes = Attribute::all();
        $category = $product->categories()->first();

        $translations = $product->translations->keyBy('locale');

        $productAttributes = $product->attributes()->get()->keyBy('id');

        return view('admin.pages.catalogue.edit')->with([
            'product' => $product,
            'languages' => $languages,
            'attributes' => $attributes,
            'productAttributes' => $productAttributes,
            'translations' => $translations,
            'category' => $category,
        ]);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  int  $id
     * @return Response
     */
    public function update(Request $request, CreateOrUpdateProduct $service, $productId)
    {
        return $service->handler($request, $productId);
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return Response
     */
    public function destroy(Request $request, $id)
    {
        $product = \App\Product::findOrFail($id);

        //Delete images
        $images = $product->images()->get();

        foreach($images as $i) {
            $i->delete();
        }

        \Log::info('Deleting product', ['user' => auth()->user()->id, 'ip' => $_SERVER['REMOTE_ADDR'], 'product' => $product->id]);

        //Soft delete product
        $product->delete($id);

        if($request->ajax()) {
            return response()->json(['status' => 'success']);
        } else {
            return redirect(action('Admin\CatalogueController@index'))->withSuccess(trans('admin.catalogue.product.delete.success'));
        }
    }
}
<file_sep>/resources/lang/it/pages.php
<?php

return [
    'about-us.title' => 'Chi siamo',
    'about-us.desc' => '<p>Noi di Zugy operiamo proprio come un normale negozio di liquori al dettaglio ma con una piccola significativa differenza: visiti il nostro sito web invece di andare in un negozio. Qui troverai un\'ampia selezione di bevande; potrai scegliere tra circa 50 tipologie di liquori diversi. Basta scegliere quello che desiderate. Fate il vostro ordine e noi lo consegneremo alla vostra porta. Alla consegna puoi pagare con carta di credito, paypal o in contanti.</p>
<p>La tecnologia ha trasformato molte attività ma non ancora quella degli alcolici che non ha subito trasformazioni negli ultimi cento anni. Zugy sta rivoluzionando il modo con cui le persone degustano le bevande attraverso l\'uso di una moderna tecnologia che caratterizza il nostro modo di operare.</p>',
    'about-us.find-out-more' => 'Per saperne di più sui social media',
    'about-us.partners' => 'Soci',
    'about-us.partners-desc' => '<p>Zugy è orgogliosa di essere partner di <a href="http://securitycourier.it/">Security Courier Express</a> che ti porterà a casa  il tuo ordine.</p>
<p>Usiamo solo corrieri in bicicletta, garantendo la consegna senza emissione di CO2 <i class="fa fa-leaf"></i>.</p>',
    'about-us.mission-statement.title' => 'Obiettivi',
    'about-us.mission-statement.desc' => '<p>In Zugy, il nostro obiettivo è e sarà sempre rendere felici i nostri clienti. Vogliamo regalare ricordi ed emozioni, siano questi una bottiglia per una bella chiacchierata o un party pack per rendere un compleanno il migliore di sempre.</p>
<p>Non importa se sarà una nottata da urlo o una tranquilla serata con gli amici, noi speriamo che sceglierete Zugy per rendere la vostra notte indimenticabile.</p>',
    'contact.title' => 'Contatti',
    'contact.address' => 'Zugy Srl. <br>Alzaia Navigli Grande 36<br> Milano, 20141 <br> Italia',
    'contact.customer-service' => 'Di seguito troverete il numero di telefono relativo al servizio clienti. Saremo felici di rispondere alle vostre domande, commenti o dubbi.',
    'contact.email.general' => 'Per richieste generali:',
    'contact.email.investors' => 'Per gli investitori:',
    'shop.title' => 'Negozio',
    'home.title' => 'Home',
    'home.meta_description' => 'Zugy è un negozio di liquori online a Milano che consegna nel giro di un\'ora.',
    'home.meta-title' => 'Alcool a domicilio Milano',
    'home.tagline' => 'La tua birra, vino e liquore preferiti, consegnati direttamente a casa tua.',
    'home.marketing.address.title' => 'Imposta l\'indirizzo',
    'home.marketing.address.desc' => 'Inserisci il codice postale dove desideri ricevere l\'ordine',
    'home.marketing.time.title' => 'Ordina in pochi minuti',
    'home.marketing.time.desc' => 'Aggiungi ció che desideri al tuo carrello, paga alla cassa ed é fatto.',
    'home.marketing.delivery.title' => 'Consegna a domicilio',
    'home.marketing.delivery.desc' => 'Inizieremo subito a preparare il vostro ordine e lo consegneremo a casa vostra. Devi solo presentare un documento d\'identitá ai nostri simpatici fattorini!',
    'home.exclusive' => 'Attualmente serviamo solo Milano',
    'home.milan' => 'Milano',
    'home.expansion' => 'Molti progetti da sviluppare',
    'privacy-policy' => 'Informativa sulla privacy',
    'team' => 'Team',
    'terms-and-conditions' => 'Termini e condizioni',
];
<file_sep>/app/Zugy/DeliveryTime/DeliveryTimeServiceProvider.php
<?php

namespace Zugy\DeliveryTime;

use Illuminate\Support\ServiceProvider;

class DeliveryTimeServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind('deliveryTime', function()
        {
            return new DeliveryTime();
        });
    }
}<file_sep>/app/Services/CreateOrUpdateProduct.php
<?php

namespace App\Services;

use App\Product;
use App\ProductImage;
use Illuminate\Http\Request;
use Mcamara\LaravelLocalization\LaravelLocalization;
use Illuminate\Support\Facades\Validator;

class CreateOrUpdateProduct
{
    public function handler(Request $request, $productId = null)
    {
        $languages = \Localization::getSupportedLanguagesKeys();

        if ($productId === null) { //Create new product
            $product = new Product();
        } else {
            $product = Product::findOrFail($productId);
        }

        $rules = [
            'price' => 'required|numeric|min:0',
            'compare_price' => 'numeric|min:0',
            'tax_class_id' => 'required|exists:tax_classes,id',
            'stock_quantity' => 'required|integer|min:0',
            'images' => 'required|integerArray',
            'category_id' => 'required|exists:categories,id',
            'thumbnail_id' => 'exists:product_images,id',
            'attributes.*' => 'max:255',
        ];

        foreach ($languages as $l) {
            $rules["meta.{$l}.title"] = "required|max:255";
            $rules["meta.{$l}.slug"] = "required|max:255|alpha_dash|unique:product_translations,slug,{$product->translateOrNew($l)['slug']},slug,locale,$l";
            $rules["meta.{$l}.description"] = "required|max:65535";
            $rules["meta.{$l}.meta_description"] = "required|max:255";
        }

        $validator = Validator::make($request->all(), $rules);

        if($validator->fails()) {
            return redirect()->back()->withErrors($validator)->withInput();
        } else {
            $product->stock_quantity = $request->input('stock_quantity');
            $product->price = $request->input('price');
            $product->compare_price = $request->input('compare_price');
            $product->tax_class_id = $request->input('tax_class_id');

            if($request->input('thumbnail_id') != "") {
                $product->thumbnail_id = $request->input('thumbnail_id');
            }

            foreach ($languages as $l) {
                $product->translateOrNew($l)->title = $request->input("meta.{$l}.title");
                $product->translateOrNew($l)->slug = $request->input("meta.{$l}.slug");
                $product->translateOrNew($l)->description = $request->input("meta.{$l}.description");
                $product->translateOrNew($l)->meta_description = $request->input("meta.{$l}.meta_description");
            }

            $product->save();

            $product->categories()->sync([$request->input('category_id')]);

            //TODO Validate if attribute IDs exist
            $sync = [];
            foreach($request->input('attributes') as $key => $value) {
                if(trim($value) == "") continue;
                $sync[$key] = ['value' => $value];
            }

            $product->attributes()->sync($sync);

            $product->save();

            //Ensure that thumbnail ID gets updated with product ID
            $productImages = $request->input('images');
            if(!in_array((int) $request->input('thumbnail_id'), $productImages)) {
                $productImages[] = (int) $request->input('thumbnail_id');
            }
            ProductImage::whereIn('id', $productImages)->update(['product_id' => $product->id]);
        }

        if($productId != null) {
            \Log::info('Product updated', ['user' => auth()->user()->id, 'ip' => $_SERVER['REMOTE_ADDR'], 'product' => $product]);
            return redirect()->back()->with('success', 'Product updated');
        } else {
            \Log::info('Product created', ['user' => auth()->user()->id, 'ip' => $_SERVER['REMOTE_ADDR'], 'product' => $product]);
            return redirect()->action('Admin\CatalogueController@index')->with('success', 'Product added successfully');
        }
    }
}
<file_sep>/app/Services/Payment/PaymentMethodService.php
<?php

namespace App\Services\Payment;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Zugy\Facades\Checkout;
use Zugy\Facades\PaymentGateway;

class PaymentMethodService
{
    public function setMethod(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'method' => 'required',
        ]);

        $validator->sometimes('stripeToken', 'required', function($input) {
            return ($input->method == 'stripe') && ($input->cardId === null);
        });

        $validator->sometimes('cardId', 'required', function($input) {
            return ($input->method == 'stripe') && ($input->stripeToken === null);
        });

        if($validator->fails()) {
            return redirect()->back()->withErrors($validator)->withInput();
        }

        $paymentMethod = PaymentGateway::set($request->input('method'))->addOrUpdateMethod();

        Checkout::setPaymentMethod($paymentMethod);

        return true;
    }
}<file_sep>/app/Http/Controllers/Auth/PasswordController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;

class PasswordController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Password Reset Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling password reset requests
    | and uses a simple trait to include this behavior. You're free to
    | explore this trait and override any methods you wish to tweak.
    |
    */

    use ResetsPasswords;

    /**
     * Create a new password controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest', ['except' =>
            'postPasswordChange'
        ]);
    }

    public function redirectPath()
    {
        return localize_url('routes.account.index');
    }

    public function getEmailSubject()
    {
        return trans('auth.reset.email.subject');
    }

    public function getReset(Request $request, $token = null)
    {
        return $this->showResetForm($request, $token);
    }

    public function postPasswordChange(Request $request)
    {
        $validator = \Validator::make($request->all(), [
            'password' => '<PASSWORD>',
        ]);

        $validator->sometimes('current_password', 'required|correct_password', function($input) {
            return(auth()->user()->password !== null);
        });

        if($validator->fails()) {
            return redirect()->back()->withErrors($validator);
        }

        auth()->user()->password = <PASSWORD>($request->input('password'));
        auth()->user()->save();

        return redirect(localize_url('routes.account.settings'))->withSuccess(trans('auth.form.password.change.success'));
    }
}
<file_sep>/app/Zugy/DeliveryTime/DeliveryTime.php
<?php

namespace Zugy\DeliveryTime;

use Carbon\Carbon;

class DeliveryTime
{
    /**
     * Validate delivery time, throws exceptions if it fails
     * @param Carbon $delivery_time
     * @throws Exceptions\ClosedException
     * @throws Exceptions\PastDeliveryTimeException
     */
    public function isValidDeliveryTime(Carbon $delivery_time) {
        //Check that delivery time is in future
        $cutoff_time = Carbon::now()->addMinutes(30);

        if($delivery_time->lt($cutoff_time)) {
            throw new Exceptions\PastDeliveryTimeException();
        }

        if(!$this->isOpen($delivery_time)) {
            throw new Exceptions\ClosedException();
        }
    }

    /**
     * Check if for given time we are delivering or not
     * Open everyday from 1pm to 1am
     * @param Carbon $time
     * @return bool
     */
    public function isOpen(Carbon $time) {
        $hour = $time->hour;

        switch ($time->dayOfWeek) {
            case Carbon::SATURDAY:
            case Carbon::SUNDAY:
            case Carbon::MONDAY:
                return ($hour < 1 || $hour >= 13);
            default:
                return ($hour < 1 || $hour >= 13);
        }
    }

    /**
     * Generate array of days for <select>
     * @param $days
     * @return array
     */
    public function daySelect($days) {
        $return = [];
        
        for($i = 0; $i < $days; $i++) {
            $currentDay = Carbon::now()->addDays($i);
        
            $dayThreshold = $currentDay->copy();
            $dayThreshold->hour = 23;
            $dayThreshold->minute = 5;
            if($i == 0 && $currentDay->gt($dayThreshold) ) continue; //Don't show day if time is after 23:05

            $return[] = $currentDay;
        }
        
        return $return;
    }
}<file_sep>/app/Zugy/Stock/StockServiceProvider.php
<?php

namespace Zugy\Stock;

use Illuminate\Support\ServiceProvider;

class StockServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind('stock', function()
        {
            return new Stock();
        });
    }
}<file_sep>/database/migrations/2015_09_13_141415_create_order_payments_table.php
<?php

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

class CreateOrderPaymentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('order_payments', function (Blueprint $table) {
            $table->increments('id');

            $table->integer('order_id')->unsigned()->nullable();
            $table->foreign('order_id')
                ->references('id')->on('orders')
                ->onDelete('cascade');

            $table->tinyInteger('status')->unsigned();

            $table->decimal('amount', 10, 2);
            $table->char('currency', 3);
            $table->string('method', 32);
            $table->text('metadata');
            $table->dateTime('paid')->nullable();

            $table->string('billing_name', 64);
            $table->string('billing_line_1', 64);
            $table->string('billing_line_2', 64)->nullable();
            $table->string('billing_city', 32);
            $table->string('billing_postcode', 10);
            $table->string('billing_state', 32)->nullable();
            $table->string('billing_phone', 32);
            $table->integer('billing_country_id');
            $table->foreign('billing_country_id')
                ->references('id')->on('countries')
                ->onDelete('no action');

            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('order_payments');
    }
}
<file_sep>/resources/lang/en/routes.php
<?php

return [
    'about-us' => 'about-us',
    'account' => [
        'index' => 'your-account',
        'orders' => 'your-account/orders',
        'settings' => 'your-account/settings',
    ],
    'cart' => 'cart',
    'checkout' => [
        'landing'       => 'checkout',
        'address'       => 'checkout/address',
        'payment'       => 'checkout/payment',
        'review'        => 'checkout/review',
        'confirmation'  => 'checkout/confirmation',
        'gatewayReturn' => 'checkout/gatewayReturn',
    ],
    'contact' => 'contact',
    'order' => [
        'show' => 'order/{id}',
    ],
    'privacy-policy' => 'privacy-policy',
    'product' => 'product/{slug}',
    'search' => 'search/{query}',
    'shop' => [
        'index' => 'shop',
        'category' => 'shop/category/{slug}',
    ],
    'team' => 'team',
    'terms-and-conditions' => 'terms-and-conditions',
];<file_sep>/resources/lang/en/cart.php
<?php

return [
    'shopping-cart' => 'Shopping Cart',
    'free-shipping-reminder' => 'Order more than 20&euro; worth and get <b>free shipping</b>!',
    'mini-cart-empty' => 'Nothing in your cart yet',
    'empty-cart-msg' => 'You don\'t have anything in your cart yet. Visit the <a href=":storeUrl">shop</a> to start filling it up!',

    'api.error.out-of-stock' => 'Unable to add to cart. We only have :quantity left in stock.',
];<file_sep>/resources/lang/it/errors.php
<?php

return [
    'unknown' => 'Si è verificato un errore sconosciuto. Riprova',
    'stripe.invalid_number' => 'Il numero della carta di credito non è valida.',
    'stripe.invalid_expiry_month' => 'Mese di scadenza della carta non è valido.',
    'stripe.invalid_expiry_year' => 'Anno di scadenza della carta non è valido.',
    'stripe.invalid_cvc' => 'Il codice di sicurezza della scheda non è valido.',
    'stripe.incorrect_number' => 'Il numero della carta non è corretto.',
    'stripe.expired_card' => 'La carta è scaduta.',
    'stripe.incorrect_cvc' => 'Il codice di sicurezza della carta non è corretto.',
    'stripe.incorrect_zip' => 'Codice postale della scheda non è riuscito ha convalidare.',
    'stripe.card_declined' => 'La carta è stata rifiutata.',
    'stripe.missing' => 'Non c`è alcuna carta sul cliente che è in carica.',
    'stripe.processing_error' => 'Si è verificato un errore durante l\'elaborazione della scheda.',
    '404.message' => 'Pagina non trovata',
    '404.description' => 'Spiacenti la pagina richiesta non è disponibile.',
    '500.message' => 'Si è verificato un errore interno',
    '500.description' => 'Si è verificato un errore, questo errore è stato segnalato a noi e noi risolverlo nel più breve tempo possibile - ci dispiace!',
];<file_sep>/resources/lang/en/checkout.php
<?php

return [
    'title' => 'Checkout',
    'items' => 'Items',
    'total' => 'Total',
    'subtotal' => 'Subtotal',
    'shipping' => 'Shipping',
    'address.title' => 'Address',
    'address.prompt' => 'Please enter your address',
    'address.choose-delivery' => 'Choose a delivery address',
    'address.select-delivery' => 'Deliver to this address',
    'address.deliver-new' => 'Deliver to a new address',
    'address.deliver-new-desc' => 'Add a new delivery address to your address book.',
    'address.form.delivery' => 'Delivery Address',
    'address.form.billing' => 'Billing Address',
    'address.form.name' => 'Full name',
    'address.form.line_1' => 'Address Line 1',
    'address.form.line_1.desc' => 'House name/number and street, P.O. box, company name, c/o',
    'address.form.line_2' => 'Address Line 2',
    'address.form.line_2.desc' => 'Apartment, suite, unit, building, floor, etc.',
    'address.form.zip' => 'Zip / Postcal Code',
    'address.form.town' => 'Town / City',
    'address.form.country' => 'Country',
    'address.form.instructions' => 'Delivery instructions',
    'address.form.instructions.desc' => 'Additional information for our driver',
    'address.form.phone' => 'Phone',
    'address.form.phone.desc' => 'In case our driver needs to contact you for delivery',
    'address.form.default' => 'Use this address as default for future orders',
    'address.form.same' => 'Same as delivery address',
    'address.form.country.italy' => 'Italy',
    'payment.title' => 'Payment',
    'payment.form.title' => 'Select payment method',
    'payment.form.card' => 'Card',
    'payment.form.card.new' => 'Add a new card',
    'payment.form.card.show' => '<b>:brand</b> ending in :last4',
    'payment.form.card.expires' => 'Expires',
    'payment.form.name' => '<NAME>',
    'payment.form.cvc' => 'CVC',
    'payment.form.expiration' => 'Expiration',
    'payment.form.default' => 'Set this as the default payment method',
    'payment.form.card.button' => 'Use credit card',
    'payment.form.pay-with' => 'Pay with :name',
    'payment.form.cash' => 'Cash',
    'payment.form.cash.desc' => 'Pay with cash on delivery',
    'payment.form.error.different' => 'There was an error processing your payment. Try again or use another payment method. <a href=":paymentUrl?change">Change</a>',

    'review.title' => 'Review order',
    'review.age-warning' => 'We only sell alcohol to people <b>older than the age of 18</b>, even though the legal drinking age is 16 in Italy. Our drivers will ask you for your photo ID to verify your age at delivery.',
    'review.coupon' => 'Coupon',
    'review.coupon.desc' => 'Enter a coupon code if you have one',
    'review.coupon.placeholder' => 'Enter code',
    'review.items' => 'Review items',
    'review.place-order' => 'Place order',
    'review.accept' => 'By placing your order you agree to :siteName\'s <a href=":privacyPolicyUrl">Privacy Policy</a> and <a href=":termsAndConditionsUrl">Terms and Conditions</a>.',
    'review.delivery-time' => 'Delivery time',
    'review.delivery-time.label' => 'When do you want your order to be delivered?',
    'review.delivery-time.asap' => 'As soon as possible',
    'review.delivery-time.slot' => 'Choose a delivery slot',
    'review.delivery-time.error.late' => 'It\'s too late to choose this delivery time. Please select a later time',
    'review.delivery-time.error.closed' => 'We are closed during this delivery time. Please select a different time',
    'review.error.too-fast' => 'You are placing too many orders in a short period of time. Please wait a minute before placing an order again',
    'order' => 'Order',
    'order.success' => 'Your order has been placed. We will notify you via email when your order is out for delivery.'
];<file_sep>/app/Http/Controllers/API/CouponController.php
<?php

namespace App\Http\Controllers\Api;

use App\Coupon;
use App\Exceptions\Coupons\CouponExpiredException;
use App\Exceptions\Coupons\CouponNotStartedException;
use App\Exceptions\Coupons\CouponOrderMinimumException;
use App\Exceptions\Coupons\CouponUsedException;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Zugy\Facades\Checkout;

class CouponController extends Controller
{
    /**
     * Applies a coupon code to the current
     * @param $code
     * @return \Illuminate\Http\JsonResponse
     */
    public function apply(Request $request) {
        $code = "";
        if($request->has('coupon')) {
            $code = trim($request->input('coupon'));
        }

        $coupon = Coupon::where('code', '=', $code)->first();

        if($coupon == null) {
            return response()->json(['status' => 'error', 'message' => trans('coupon.404')], 400);
        }

        try{
            Checkout::setCoupon($coupon);
        } catch(CouponUsedException $e) {
            return response()->json(['status' => 'error', 'message' => trans('coupon.claimed')], 400);
        } catch(CouponExpiredException $e) {
            return response()->json(['status' => 'error', 'message' => trans('coupon.expired', ['date' => $coupon->expires->toFormattedDateString()])], 400);
        } catch(CouponNotStartedException $e) {
            return response()->json(['status' => 'error', 'message' => trans('coupon.notActive', ['date' => $coupon->starts])], 400);
        } catch(CouponOrderMinimumException $e) {
            return response()->json(['status' => 'error', 'message' => trans('coupon.minimum', ['min' => $coupon->minimumTotal])], 400);
        }

        return response()->json(['status' => 'success']);
    }
}<file_sep>/app/Providers/AuthServiceProvider.php
<?php

namespace App\Providers;

use App\Address;
use App\Order;
use App\Policies\AuthenticationPolicy;
use App\Product;
use App\Policies\AddressPolicy;
use App\Policies\ProductPolicy;
use App\Policies\OrderPolicy;
use App\User;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        Product::class => ProductPolicy::class,
        Order::class   => OrderPolicy::class,
        Address::class => AddressPolicy::class,
        User::class => AuthenticationPolicy::class
    ];

    /**
     * Register any application authentication / authorization services.
     *
     * @param  \Illuminate\Contracts\Auth\Access\Gate  $gate
     * @return void
     */
    public function boot(GateContract $gate)
    {
        parent::registerPolicies($gate);
    }
}<file_sep>/app/Http/Controllers/Admin/DashboardController.php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Order;
use Zugy\Facades\Charts;

class DashboardController extends Controller
{
    public function getDashboard()
    {
        $orders = new Order();

        $revenueYesterday = $orders->uncancelled()->where('order_placed', '>', \Carbon::yesterday())->where('order_placed', '<', \Carbon::today())->get()->sum('grand_total');
        $revenueThisMonth = $orders->uncancelled()->where('order_placed', '>', new \Carbon('first day of this month'))->where('order_placed', '<', \Carbon::today())->get()->sum('grand_total');

        $chart = [
            '30-days' => Charts::revenueByDay()
        ];

        return view('admin.pages.dash')->with([
            'orders' => $orders,
            'revenueYesterday' => $revenueYesterday,
            'revenueThisMonth' => $revenueThisMonth,
            'chart' => $chart
        ]);
    }
}<file_sep>/app/PaymentMethod.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Zugy\Facades\PaymentGateway;

class PaymentMethod extends Model
{
    protected $table = 'users_payment_method';

    protected $casts = [
        'payload' => 'json',
    ];

    protected $fillable = ['method', 'payload'];

    /**
     * Get the default payment method
     * @param $query
     * @return mixed
     */
    public function scopeDefault($query) {
        return $query->where('isDefault', '=', 1);
    }

    public function getFormatted()
    {
        $formattedPayment = PaymentGateway::set($this)->getFormatted();

        return $formattedPayment;
    }
}
<file_sep>/tests/PostcodeApiTest.php
<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class PostcodeApiTest extends TestCase
{
    private $apiEndpoint = '/api/v1/postcode';

    public function testPostcodePass()
    {
        $this->get("{$this->apiEndpoint}/check/20121")
             ->seeJson([
                 'delivery' => true,
             ]);
    }

    public function testPostcodeFail()
    {
        $this->get("{$this->apiEndpoint}/check/12345")
            ->seeJson([
                'delivery' => false,
            ]);
    }
}
<file_sep>/tests/StockTest.php
<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Zugy\Facades\Stock;

class StockTest extends TestCase
{
    use DatabaseTransactions;

    /**
     * Check that stock check passes
     *
     * @return void
     */
    public function testStockPositive()
    {
        $productA = factory(App\Product::class)->create(['stock_quantity' => 4]);
        $productA->translations()->save(factory(App\ProductTranslation::class)->make());

        $productB = factory(App\Product::class)->create(['stock_quantity' => 3]);
        $productB->translations()->save(factory(App\ProductTranslation::class)->make());

        //Add 4 of productA to cart
        $this->json('POST', 'api/v1/cart', [
            'id' => $productA->id,
            'qty' => 4,
        ])->seeJson(['status' => 'success']);

        //Add 2 of productB to cart
        $this->json('POST', 'api/v1/cart', [
            'id' => $productB->id,
            'qty' => 2,
        ])->seeJson(['status' => 'success']);

        $this->assertTrue(Stock::checkCartStock());
    }

    /**
     * Check that stock check throws correct error
     */
    public function testStockNegative() {
        $this->setExpectedException('App\Exceptions\OutOfStockException');

        $productA = factory(App\Product::class)->create(['stock_quantity' => 4]);
        $productA->translations()->save(factory(App\ProductTranslation::class)->make());

        $productB = factory(App\Product::class)->create(['stock_quantity' => 3]);
        $productB->translations()->save(factory(App\ProductTranslation::class)->make());

        //Add 4 of productA to cart
        $this->json('POST', 'api/v1/cart', [
            'id' => $productA->id,
            'qty' => 4,
        ])->seeJson(['status' => 'success']);

        //Add 2 of productB to cart
        $this->json('POST', 'api/v1/cart', [
            'id' => $productB->id,
            'qty' => 2,
        ])->seeJson(['status' => 'success']);

        \Log::debug('Updating product stock', [$productA->id]);
        $productA->stock_quantity = 3; //Another customer bought 1, only 3 left
        $productA->save();

        Stock::checkCartStock(); //Should throw OutOfStockException
    }
}
<file_sep>/database/migrations/2016_07_06_220419_make_billing_phone_nullable.php
<?php

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

/**
 * Class MakeBillingPhoneNullable
 * Fixes a bug where if a customer had a billing address different from his delivery address, the billing address
 * would not have a phone.
 * When inserting a billing address into order_payments, phone was not allowed to be null.
 */
class MakeBillingPhoneNullable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('order_payments', function (Blueprint $table) {
            $table->string('billing_phone', 32)->nullable()->change();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('order_payments', function (Blueprint $table) {
            $table->string('billing_phone', 32)->change();
        });
    }
}
<file_sep>/database/seeds/LanguageSeeder.php
<?php
use Illuminate\Database\Seeder;

/**
 * User: <NAME>
 * Date: 24.07.2015
 * Time: 15:48
 */
class LanguageSeeder extends Seeder
{
    public function run() {
        DB::table('languages')->insert([
            ['id' => 1, 'name' => 'English', 'code' => 'en', 'flag' => 'gb'],
            ['id' => 2, 'name' => 'Italian', 'code' => 'it', 'flag' => 'it']
        ]);
    }
}<file_sep>/app/Services/AuthenticateUser.php
<?php

namespace App\Services;

use App\OAuthAuthorisations;
use App\User;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;

class AuthenticateUser
{
    public function __construct()
    {
        //
    }

    public function requestToken($oauth_provider) {
        return Socialite::with($oauth_provider)->redirect();
    }

    public function facebookLogin(Request $request) {
        if(!$request->has('code')) {
            return $this->requestToken('facebook');
        }

        return $this->loginOrCreateUser('facebook', Socialite::driver('facebook')->user());
    }

    public function googleLogin(Request $request) {
        if(!$request->has('code')) {
            return $this->requestToken('google');
        }

        return $this->loginOrCreateUser('google', Socialite::driver('google')->user());
    }

    public function loginOrCreateUser($oauth_provider, $oauth_user) {

        $oauth_authorisation = OAuthAuthorisations::where('network', '=', $oauth_provider)
                               ->where('network_user_id', '=', (string) $oauth_user->id)
                               ->first();

        if($oauth_authorisation === null) {
            $user = User::where('email', '=', $oauth_user->email)->first();

            if($user === null) {
                //Create user
                $user = $this->createUser($oauth_user->name, $oauth_user->email);
            }

            OAuthAuthorisations::create([
                'network' => $oauth_provider,
                'network_user_id' => $oauth_user->id,
                'user_id' => $user->id
            ]);

            auth()->loginUsingId($user->id, true);
        } else {
            auth()->loginUsingId($oauth_authorisation->user_id, true);
        }

        return redirect()->intended(localize_url('routes.shop.index'))->with('success', trans('auth.login.success'));
    }

    public function createUser($name, $email) {
        $user = User::create([
            'name' => $name,
            'email' => $email
        ]);

        return $user;
    }
}

<file_sep>/app/Attribute.php
<?php

namespace App;

use Dimsav\Translatable\Translatable;
use Illuminate\Database\Eloquent\Model;

class Attribute extends Model
{
    use Translatable;

    public $translatedAttributes = ['name', 'unit'];
}
<file_sep>/resources/lang/en/payment.php
<?php

return [
    'card-number' => 'Card Number',
    'cardholder-name' => '<NAME>',
    'payment-method' => 'Payment method'
];<file_sep>/app/Services/CreateOrUpdateAddress.php
<?php
/**
 * User: <NAME>
 * Date: 08.09.2015
 * Time: 20:50
 */

namespace App\Services;
use App\Address;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Validator;
use Webpatser\Countries\Countries;

class CreateOrUpdateAddress
{
    protected $defaultRules = [
        'name' => 'required|string|max:64',
        'line_1' => 'required|string|max:64',
        'line_2' => 'string|max:64',
        'postcode' => 'required|max:5|deliveryPostcode',
        'city' => 'required|max:32',
        'country' => 'required|size:3|exists:countries,iso_3166_3', //TODO validate is Italy
    ];

    public function delivery(array $deliveryInput, Address $address = null)
    {
        $deliveryRules = $this->defaultRules;
        $deliveryRules['delivery_instructions'] = 'max:1000';
        $deliveryRules['phone'] = 'required'; //TODO Validate valid phone

        $validator = Validator::make($deliveryInput, $deliveryRules);

        if($validator->fails()) {
            return $validator;
        }

        if($address == null) {
            $address = new Address($deliveryInput);
        } else {
            $address->fill($deliveryInput);
        }

        $address->country_id = Countries::where('iso_3166_3', '=', $deliveryInput['country'])->first()->id;

        if(isset($deliveryInput['default'])) {
            auth()->user()->addresses()->update(['isShippingPrimary' => false]);
            $address->isShippingPrimary = true;
        }

        if(isset($deliveryInput['billing_same'])) {
            auth()->user()->addresses()->update(['isBillingPrimary' => false]);
            $address->isBillingPrimary = true;
        }

        auth()->user()->addresses()->save($address);

        return $address;
    }

    public function billing(array $billingInput, Address $address = null)
    {
        $billingRules = $this->defaultRules;
        $billingRules['postcode'] = 'required|max:5';

        $validator = Validator::make($billingInput, $billingRules);

        if($validator->fails()) {
            return $validator;
        }

        if($address == null) {
            $address = new Address($billingInput);
        } else {
            $address->fill($billingInput);
        }

        $address->country_id = Countries::where('iso_3166_3', '=', $billingInput['country'])->first()->id;

        if(isset($billingInput['default'])) {
            auth()->user()->addresses()->update(['isBillingPrimary' => false]);
            $address->isBillingPrimary = true;
        }

        auth()->user()->addresses()->save($address);

        return $address;
    }
}<file_sep>/app/Providers/ValidatorServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Validator;

class ValidatorServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        Validator::extend('integerArray', function($attribute, $value, $parameters)
        {
            foreach($value as $v) {
                if(!is_numeric($v) && !is_int((int)$v)) return false;
            }

            if(count($value) == 0) return false;

            return true;
        });

        Validator::extend('deliveryPostcode', function($attribute, $value, $parameters)
        {
            return \Zugy\Validators\PostcodeValidator::isInDeliveryRange($value);
        });

        Validator::extend('correct_password', function($attribute, $value, $parameters)
        {
            return auth()->attempt(['id' => auth()->user()->id, 'password' => $value]);
        });

        Validator::replacer('correct_password', function($message, $attribute, $rule, $parameters)
        {
            return trans('auth.form.current_password.error');
        });
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}
<file_sep>/resources/lang/en/coupon.php
<?php

return [
    '404' => 'The coupon does not exist',
    'claimed' => 'This coupon has already been claimed, sorry',
    'expired' => 'This coupon expired on :date',
    'notActive' => 'This coupon becomes active at :date',
    'minimum' => 'You order total needs to be at least :min Euro to use this coupon',
    'using' => 'Using coupon code <b>:code</b>',
    'flatDiscountDescription' => '-:amount&euro; off order',
    'percentageDiscountDescription' => ':percentage% off'
];<file_sep>/app/Http/Controllers/Admin/CustomerController.php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\User;

class CustomerController extends Controller
{
    public function index(Request $request)
    {
        if($request->has('name')) {
            $customers = User::where('name', 'LIKE', "%{$request->input('name')}%")->paginate(30);
        } else {
            $customers = User::paginate(30);
        }

        return view('admin.pages.customer.index-customer')->with(['customers' => $customers]);
    }

    public function show($id)
    {
        $customer = User::findOrFail($id);

        return view('admin.pages.customer.show-customer')->with(['customer' => $customer]);
    }
}<file_sep>/resources/lang/en/email.php
<?php

return [
    'thanks' => 'Thank you for using :name.',
    'receipt' => 'Here is your receipt.',
    'footer.tos' => 'View the :name <a href=":tosUrl">Terms of Service</a>',
    'footer.questions' => 'Questions? Email <a href="mailto::email">:email</a>',
];<file_sep>/tests/DeliveryTimeTest.php
<?php

use Carbon\Carbon;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Zugy\Facades\DeliveryTime;

class DeliveryTimeTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testExample()
    {
        $this->assertTrue(true);
    }

    public function testTrueOpeningTimes()
    {
        for($i = 0; $i < 6; $i++) {
            //Any day at 13:00
            $this->assertTrue(DeliveryTime::isOpen(Carbon::now()->startOfWeek()->addDay($i)->hour(13)->minute(0)));

            //Any day at 18:59
            $this->assertTrue(DeliveryTime::isOpen(Carbon::now()->hour(18)->minute(59)));
        }

        //Monday 00:59
        $this->assertTrue(DeliveryTime::isOpen(Carbon::now()->startOfWeek()->hour(0)->minute(59)));
    }

    public function testFalseOpeningTimes()
    {
        for($i = 0; $i < 6; $i++) {
            //Any day at 02:01
            $this->assertFalse(DeliveryTime::isOpen(Carbon::now()->startOfWeek()->addDay($i)->hour(2)->minute(1)));
        }

        //Tuesday 01:30
        $this->assertFalse(DeliveryTime::isOpen(Carbon::now()->startOfWeek()->addDay(1)->hour(1)->minute(30)));

        //Friday 01:30
        $this->assertFalse(DeliveryTime::isOpen(Carbon::now()->startOfWeek()->addDay(4)->hour(1)->minute(30)));

        //Monday 01:01
        $this->assertFalse(DeliveryTime::isOpen(Carbon::now()->startOfWeek()->hour(1)->minute(1)));

        //Sunday 01:01
        $this->assertFalse(DeliveryTime::isOpen(Carbon::now()->endOfWeek()->hour(1)->minute(1)));
    }
}
<file_sep>/app/Zugy/Repos/Category/DbCategoryRepository.php
<?php
/**
 * User: <NAME>
 * Date: 18.09.2015
 * Time: 15:07
 */

namespace Zugy\Repos\Category;

use App\CategoryTranslation;
use App\Exceptions\NoTranslationException;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Zugy\Repos\DbRepository;
use App\Category;

class DbCategoryRepository extends DbRepository implements CategoryRepository
{
    /**
     * @var Category
     */
    private $model;

    /**
     * DbCategoryRepository constructor.
     */
    public function __construct(Category $model)
    {
        $this->model = $model;
    }

    public function children($categoryId)
    {
        $categories = $this->model->all();

        return $this->recurseChildren($categories, $categoryId);
    }

    private function recurseChildren($categories, $parentId) {
        $children = $categories->where('parent_id', $parentId);

        if($children->count() === 0) return [$parentId];

        $ids = [$parentId];

        foreach($children as $c) {
            $ids = array_merge($ids, $this->recurseChildren($categories, $c->id));
        }

        return $ids;
    }

    public function getBySlug($slug) {
        $category = CategoryTranslation::where('slug', '=', $slug)
            ->where('locale', '=', LaravelLocalization::getCurrentLocale())->first();

        if(is_null($category)) {
            throw new NotFoundHttpException();
        }

        $category = $this->model->find($category->category_id);

        return $category;
    }

}<file_sep>/tests/AdminCustomersTest.php
<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class AdminCustomersTest extends TestCase
{
    use DatabaseTransactions;

    public function testViewCustomer()
    {
        $customer = factory(App\User::class)->create();

        $admin = factory(App\User::class, 'admin')->make();

        $this->actingAs($admin)->visit('admin/customer/' . $customer['id'])->see($customer['name']);
    }
}
<file_sep>/app/Exceptions/NoTranslationException.php
<?php

namespace App\Exceptions;


class NoTranslationException extends \Exception
{

}<file_sep>/app/User.php
<?php

namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

/**
 * Class User
 *
 * Group ID:
 * 0 - Normal user
 * 1 - Super admin
 * 2 - Admin
 * 3 - Driver
 *
 * @package App
 */

class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract
{
    use Authenticatable, Authorizable, CanResetPassword;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name', 'email', 'password', 'settings'];

    protected $casts = [
        'settings' => 'json'
    ];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = ['password', 'remember_token'];

    protected $adminGroupIds = [1, 2, 3];

    /**
     * Get the user settings.
     *
     * @return Settings
     */
    public function settings()
    {
        return new Settings($this);
    }

    /**
     * Define relationship
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function orders()
    {
        return $this->hasMany('App\Order');
    }

    public function oauth_authorisations()
    {
        return $this->hasMany('App\OAuthAuthorisations');
    }

    /**
     * Define relationship
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function addresses()
    {
        return $this->hasMany('App\Address');
    }

    /**
     * Define relationship
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function payment_methods()
    {
        return $this->hasMany('App\PaymentMethod');
    }

    /**
     * Define relationship
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function basket()
    {
        return $this->hasMany('App\Basket');
    }

    /**
     * Define accessor for user language setting
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function getLanguageCodeAttribute()
    {
        return strtolower(Language::find(auth()->user()->settings()->language)->pluck('code'));
    }

    public function getIsAdminAttribute()
    {
        return in_array($this->attributes['group_id'], $this->adminGroupIds);
    }
}
<file_sep>/app/ProductTranslation.php
<?php

namespace App;

class ProductTranslation extends \Eloquent
{
    public $timestamps = false;
    protected $fillable = ['slug', 'title', 'description', 'meta_description'];
}<file_sep>/app/Zugy/Validators/PostcodeValidator.php
<?php

namespace Zugy\Validators;

class PostcodeValidator
{
    static public function isInDeliveryRange($postcode) {
        return ($postcode >= 20121 && $postcode <= 20162);
    }
}<file_sep>/.env.example
APP_ENV=local
APP_DEBUG=true
APP_KEY=
APP_URL=http://homestead.app

DB_HOST=localhost
DB_DATABASE=zugy
DB_USERNAME=homestead
DB_PASSWORD=<PASSWORD>

CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_DRIVER=sync

MAIL_DRIVER=mailgun

FILE_DISC=local_uploads

####################
# APPLICATION KEYS #
####################

MAILGUN_DOMAIN=
MAILGUN_SECRET=

GOOGLE_CLIENT_ID=
GOOGLE_SECRET=

FACEBOOK_CLIENT_ID=
FACEBOOK_SECRET=

STRIPE_SECRET=
STRIPE_PUBLIC=

PAYPAL_TESTMODE=true
PAYPAL_USERNAME=
PAYPAL_PASSWORD=
PAYPAL_SIGNATURE=

PUSHBULLET_TOKEN=

ALGOLIA_ID=
ALGOLIA_SEARCH_KEY=
ALGOLIA_KEY=

RECAPTCHA_PUBLIC_KEY=
RECAPTCHA_PRIVATE_KEY=

ROLLBAR_ACCESS_TOKEN=
ROLLBAR_POST_CLIENT_ITEM=

AWS_KEY=
AWS_SECRET=
AWS_REGION=eu-central-1
AWS_BUCKET=zugy-production
AWS_BACKUP_BUCKET=zugy-backups
<file_sep>/resources/assets/js/address.js
(function( address, $, undefined ) {
    var apiEndpoint = '/api/v1/address';

    address.update = function(addressId, address) {
        return $.ajax({
            type: 'PATCH',
            url: apiEndpoint + '/' + addressId,
            data: address,
            success: function(data) {
                swal(data.message, null, "success");
            },
            error: function(xhr, status, error) {
                var err = eval("(" + xhr.responseText + ")");
                swal({
                    title: err.message,
                    type: 'error',
                    text: JSON.stringify(err.errors, null, "\t"),
                });
            }
        });
    };

    address.delete = function(addressId) {
        return $.ajax({
            type: 'DELETE',
            url: apiEndpoint + '/' + addressId,
            error: function(xhr, status, error) {
                var err = eval("(" + xhr.responseText + ")");
                alert(err.message);
            }
        });
    };

}( window.address = window.address || {}, jQuery ));<file_sep>/app/Http/Controllers/Admin/OrderController.php
<?php

namespace App\Http\Controllers\Admin;

use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Zugy\Repos\Order\OrderRepository;

class OrderController extends Controller
{
    /**
     * @var OrderRepository
     */
    protected $orderRepository;

    /**
     * OrderController constructor.
     */
    public function __construct(OrderRepository $orderRepository)
    {
        $this->orderRepository = $orderRepository;
    }


    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index(Request $request)
    {
        if($request->has('filter')) {
            $orders = $this->orderRepository->incomplete()->orderBy('delivery_time', 'asc')->orderBy('order_placed', 'asc')->paginate(30);
        } else {
            $orders = $this->orderRepository->orderBy('order_placed', 'desc')->paginate(30);
        }

        return view('admin.pages.order.index')->with(compact('orders'));
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        //
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        $order = $this->orderRepository->with('activity')->findOrFail($id);

        return view('admin.pages.order.show')->with(compact('order'));
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function edit($id)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        //
    }
}
<file_sep>/resources/lang/en/buttons.php
<?php

return [
    'add-cart' => 'Add to cart',
    'shop-now' => 'Shop now',
    'update-cart' => 'Update cart',
    'continue-shopping' => 'Continue shopping',
    'proceed-to-checkout' => 'Proceed to checkout',
    'view-cart' => 'View cart',
    'edit' => 'Edit',
    'back-to-shop' => 'Back to Shop',
    'add-new-address' => 'Add new address',
    'proceed' => 'Proceed',
    'change' => 'Change',
    'login' => 'Login',
    'more-info' => 'More info',
    'got-it' => 'Got it!',
    'toggle-navigation' => 'Toggle navigation',
    'view' => 'View',
    'mark-as' => 'Mark as...',
    'update' => 'Update',
    'show-google-maps' => 'Show on Google Maps',
    'save' => 'Save',
    'return-home' => 'Return to the homepage',
    'apply' => 'Apply',
    'cancel' => 'Cancel',
    'delete' => 'Delete',
    'success' => 'Success',
    'search.prompt' => 'start typing and search'
];<file_sep>/app/Zugy/Facades/Stock.php
<?php

namespace Zugy\Facades;

use Illuminate\Support\Facades\Facade;

class Stock extends Facade
{
    protected static function getFacadeAccessor() { return 'stock'; }
}<file_sep>/app/Http/Controllers/ProductController.php
<?php

namespace App\Http\Controllers;

use App\Http\Requests;
use Zugy\Repos\Product\ProductRepository;

class ProductController extends Controller
{
    /**
     * @var ProductRepository
     */
    private $productRepo;

    /**
     * ProductController constructor.
     * @param ProductRepository $productRepo
     */
    public function __construct(ProductRepository $productRepo)
    {
        $this->productRepo = $productRepo;
    }


    /**
     * Display the specified resource.
     *
     * @param  String  $slug
     * @return Response
     */
    public function show($slug)
    {
        $product = $this->productRepo->getBySlug($slug);

        if($product === null) abort(404, trans('product.404'));

        $thumbnail = $product->images()->where('id', $product->thumbnail_id)->first();

        $translations = $product->translations()->pluck('slug', 'locale');

        foreach($translations as $locale => $slug) {
            $translations[$locale] = \Localization::getURLFromRouteNameTranslated($locale, 'routes.product', ['slug' => $slug]);
        }

        return view('pages.product.product-show')->with(['product' => $product, 'thumbnail' => $thumbnail, 'translations' => $translations]);
    }

    public function search($query)
    {
        $products = $this->productRepo->search($query);

        return view('pages.product.product-list')->with(['products' => $products, 'query' => $query]);
    }
}
<file_sep>/resources/lang/it/shipping.php
<?php

return [
    'shipping-method' => 'Metodo di spedizione',
    'time' => 'Tempo di spedizione',
    'hour-delivery' => 'Consegna in 45 minuti',
    'max-duration' => '45 minuti',
    'cost' => 'Costo',
    'standard' => 'Standard',
    'hour' => '{1} 1 ora |[0,Inf] :count ore',
    'free-shipping' => 'Gratis se l\'ordine è superiore ai 20&euro;, altrimenti 3&euro;',
    'free' => 'Spedizione gratuita',
    'no-minimum-order' => 'Non c\'è un ordine minimo.',
];<file_sep>/app/Zugy/Facades/DeliveryTime.php
<?php

namespace Zugy\Facades;

use Illuminate\Support\Facades\Facade;

class DeliveryTime extends Facade
{
    protected static function getFacadeAccessor() { return 'deliveryTime'; }
}<file_sep>/resources/lang/it/product.php
<?php

return [
    'product' => 'Prodotto',
    'search-results' => 'Risultati della ricerca di <i>:query</i>',
    'count' => '{0} zero prodotti |{1} 1 prodotto|[2,Inf] :count i prodotti',
    'stock' => 'Offerta Quantità',
    'in-stock' => 'Disponibile',
    'out-of-stock' => 'Non disponibile',
    'stock-warning' => 'Solo :count disponibili nello store.',
    'item-weight' => 'Peso dell\'oggetto',
    'tabs.details' => 'Dettagli',
    'quantity' => 'Quantità',
    'price' => 'Prezzo',
    'edit' => 'Modifica prodotto',
    'search.empty' => 'Non ci sono risultati che corrispondono alla query <b>":query"</b>',
    'category.empty' => 'Non ci sono prodotti in questa categoria',
    'category.title' => 'Categoria',
    '404' => 'Tale prodotto non esiste',
    'form.name.label' => 'Nome del prodotto',
    'form.add.label' => 'Aggiungere un prodotto',
    'filter.search' => 'Ricerca di <b>:name</b>. :count risultati del conteggio trovati.',
    'sort.popular' => 'Più popolare',
    'sort.price.highest' => 'prezzo più alto',
    'sort.price.lowest' => 'Prezzo più basso',
    'title.category' => 'Acquista :category',
    'title.search' => 'Mostra risultati per :query',
    'title.shop' => 'Compra alcolici',
];<file_sep>/app/Zugy/Checkout/CheckoutServiceProvider.php
<?php

namespace Zugy\Checkout;

use Illuminate\Support\ServiceProvider;

class CheckoutServiceProvider extends ServiceProvider
{

    public function register()
    {
        $this->app['checkout'] = $this->app->share(function($app)
        {
            $session = $app['session'];
            return new Checkout($session);
        });
    }
}<file_sep>/tests/EnvTest.php
<?php

class EnvTest extends TestCase
{
    public function testFacebook()
    {
        $this->assertTrue(config('services.facebook.client_id') !== null);
        $this->assertTrue(config('services.facebook.client_secret') !== null);
    }

    public function testStripe()
    {
        $this->assertTrue(config('services.stripe.secret') !== null);
        $this->assertTrue(config('services.stripe.public') !== null);
    }

    public function testMailgun()
    {
        $this->assertTrue(config('services.mailgun.domain') !== null);
        $this->assertTrue(config('services.mailgun.secret') !== null);
    }

    public function testGoogle()
    {
        $this->assertTrue(config('services.google.client_id') !== null);
        $this->assertTrue(config('services.google.client_secret') !== null);
    }

    public function testPaypal()
    {
        if(env('APP_ENV') == 'production') {
            $this->assertFalse(config('services.paypal.testMode'));
        } else {
            $this->assertTrue(config('services.paypal.testMode'));
        }

        $this->assertTrue(config('services.paypal.username') !== null);
        $this->assertTrue(config('services.paypal.password') !== null);
        $this->assertTrue(config('services.paypal.signature') !== null);
    }

    public function testAlgolia()
    {
        $this->assertTrue(env('ALGOLIA_ID') !== null);
        $this->assertTrue(env('ALGOLIA_SEARCH_KEY') !== null);
        $this->assertTrue(env('ALGOLIA_KEY') !== null);
    }

    public function testRollbar()
    {
        $this->assertNotNull(config('services.rollbar.access_token'));
        $this->assertNotNull(config('services.rollbar.post_client_item'));
    }

    public function testAWS()
    {
        $this->assertNotNull(config('filesystems.default'));

        if(env('FILE_DISC') == 's3') {
            $this->assertNotNull(config('services.aws.region'));
            $this->assertNotNull(config('services.aws.bucket'));
            $this->assertNotNull(env('AWS_KEY') !== null);
            $this->assertNotNull(env('AWS_SECRET') !== null);
        }
    }
}
<file_sep>/resources/lang/en/admin.php
<?php
return [
    'incomplete-orders' => 'Incomplete Orders',
    'revenue-yesterday' => 'Revenue yesterday',
    'revenue-this-month' => 'Revenue this month so far',
    'details' => 'Details',
    'time' => 'Time',
    'user' => 'User',
    'description' => 'Description',
    'meta_description' => 'Meta description',
    'meta_description.desc' => 'This is the description that will appear on search engine listings.',
    'upload-images' => 'Upload images',
    'pricing' => 'Pricing',
    'incl-tax' => 'incl. tax',
    'compare-price.label' => 'Compare at price',
    'tax_class' => 'Tax class',
    'attributes.legend' => 'Attributes',
    'attributes.desc' => 'Give the product attributes like volume, alcohol content, etc.',
    'organisation' => 'Organisation',
    'miscellaneous' => 'Miscellaneous',
    'created_at' => 'Created at',
    'last_login' => 'Last login',
    'current_basket' => 'Current items in cart',

    'dashboard.title' => 'Dashboard',

    'catalogue.title' => 'Catalogue',
    'catalogue.image.thumbnail' => 'Set this as thumbnail',
    'catalogue.image.delete.confirmation' => 'Are you sure you want to delete this image?',
    'catalogue.product.delete.confirmation' => 'Are you sure you want to delete this product?',
    'catalogue.product.delete.success' => 'Product deleted',

    'customers.title' => 'Customers',
    'customers.singular' => 'Customer',
    'customers.name.label' => 'Customer Name',
    'customers.total' => 'Total spent',
    'customers.address.default' => 'Default address',
    'customers.revenue.lifetime' => 'Lifetime Revenue',
    'customers.orders_total' => 'Total orders',
    'orders.title' => 'Orders',
    'orders.show.title' => 'Order',
    'orders.placed-by' => 'Placed by',
    'orders.paid' => 'Paid',
    'orders.form.id.label' => 'Order ID',
    'orders.filter.incomplete' => 'Filter incomplete',
    'orders.filter.incomplete.info' => 'Currently showing incomplete orders only.',
];<file_sep>/app/Helpers/helpers.php
<?php

if ( ! function_exists('localize_url'))
{
    function localize_url($transKeyName, array $attributes = [])
    {
        return Localization::getURLFromRouteNameTranslated(Localization::getCurrentLocale(), $transKeyName, $attributes);
    }
}

if( ! function_exists('is_bot'))
{
    function is_bot() {
        return (isset($_SERVER['HTTP_USER_AGENT'])
            && (preg_match('/bot|crawl|slurp|spider/i', $_SERVER['HTTP_USER_AGENT']) || str_contains($_SERVER['HTTP_USER_AGENT'], 'Google Page Speed Insights'))
        );
    }
}<file_sep>/app/Listeners/SendOrderStatusMail.php
<?php

namespace App\Listeners;

use App\Events\OrderStatusChanged;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendOrderStatusMail
{
    /**
     * Handle the event.
     *
     * @param  OrderStatusChanged $event
     * @throws \Exception
     */
    public function handle(OrderStatusChanged $event)
    {
        switch($event->order->order_status) {
            case 2: //Out for delivery
                $view = 'emails.order.status.out-for-delivery';
                $subject = trans('order.email.delivery.subject', ['id' => $event->order->id]);
                break;
            case 4: //Cancelled
                $view = 'emails.order.status.cancelled';
                $subject = trans('order.email.cancelled.subject', ['id' => $event->order->id]);
                break;
            default:
                return;
        }

        try {
            \Mail::send($view, ['order' => $event->order], function($m) use($event, $subject) {
                $m->from(config('site.email.support'), config('site.name'));
                $m->to($event->order->email)->subject($subject);
            });
        } catch (\Exception $e) {
            \Log::critical('Could not send order status email');
        }
    }
}
<file_sep>/resources/lang/en/shipping.php
<?php

return [
    'shipping-method' => 'Shipping method',
    'time' => 'Shipping time',
    'hour-delivery' => 'Delivery within 45 minutes',
    'max-duration' => '45 minutes',
    'cost' => 'Cost',
    'standard' => 'Standard',
    'hour' => '{1} 1 hour|[0,Inf] :count hours',
    'free-shipping' => 'FREE if order is over 20&euro;, otherwise 3&euro;',
    'free' => 'Free shipping',
    'no-minimum-order' => 'No minimum order.'
];<file_sep>/app/Zugy/Repos/Order/DbOrderRepository.php
<?php

namespace Zugy\Repos\Order;

use App\Order;
use Zugy\Repos\DbRepository;

class DbOrderRepository extends DbRepository implements OrderRepository
{
    /**
     * @var Order
     */
    protected $model;

    /**
     * DbOrderRepository constructor.
     */
    public function __construct(Order $model)
    {
        $this->model = $model;
    }

    public function incomplete()
    {
        return $this->model->incomplete();
    }


}<file_sep>/resources/lang/it/email.php
<?php

return [
    'thanks' => 'Grazie per aver usato :name.',
    'receipt' => 'Ecco la sua ricevuta.',
    'footer.tos' => 'Visualizza il :name <a href=":tosUrl">Termini di servizio</a>',
    'footer.questions' => 'Domande? Email <a href="mailto::email">:email</a>',
];<file_sep>/tests/AgeSplashTest.php
<?php

class AgeSplashTest extends TestCase
{
    //TODO Write tests for age splash
    // @ignore
    public function testSplash()
    {
        $this->visit('en');
    }
}<file_sep>/tests/CartTest.php
<?php

use Illuminate\Foundation\Testing\DatabaseTransactions;

class CartTest extends TestCase
{
    use DatabaseTransactions;

    private $user;
    private $faker;

    protected function setUp()
    {
        parent::setUp();

        $this->faker = Faker\Factory::create();

        $this->user = factory(App\User::class)->create();
        $this->actingAs($this->user);
    }

    /**
     * Test adding something to cart
     *
     * @return void
     */
    public function testAddToCart()
    {
        $product = factory(App\Product::class)->create();
        $product->translations()->save(factory(App\ProductTranslation::class)->make());

        $quantity = $this->faker->numberBetween(1, $product->stock_quantity);

        //Add item to cart
        $this->json('POST', 'api/v1/cart', [
            'id' => $product->id,
            'qty' => $quantity,
        ])->seeJson(['status' => 'success']);

        $this->json('GET', 'api/v1/cart')->seeJson([
            'id' => $product->id,
            'qty' => $quantity,
            'price' => number_format($product->price,2),
            'subtotal' => round($product->price * $quantity, 2)
        ]);
    }

    /**
     * Test updating quantity in cart
     */
    public function testAddExistingCart() {
        $product = factory(App\Product::class)->create([
            'stock_quantity' => 10,
        ]);
        $product->translations()->save(factory(App\ProductTranslation::class)->make());

        $quantity = 5;

        //Add item to cart
        $this->json('POST', 'api/v1/cart', [
            'id' => $product->id,
            'qty' => $quantity,
        ])->seeJson(['status' => 'success']);

        $secondQuantity = 2;

        $this->json('POST', 'api/v1/cart', [
            'id' => $product->id,
            'qty' => $secondQuantity,
        ])->seeJson(['status' => 'success']);

        $this->json('GET', 'api/v1/cart')->seeJson([
            'id' => $product->id,
            'qty' => $quantity + $secondQuantity,
            'price' => number_format($product->price,2),
            'subtotal' => round($product->price * ($quantity + $secondQuantity), 2)
        ]);
    }

    /**
     * Test if out stock error works when bulk updating existing items in cart
     */
    public function testUpdateOutOfStock() {
        $product = factory(App\Product::class)->create([
            'stock_quantity' => 10,
        ]);
        $product->translations()->save(factory(App\ProductTranslation::class)->make());

        $quantity = 5;

        //Add item to cart
        $response = $this->json('POST', 'api/v1/cart', [
            'id' => $product->id,
            'qty' => $quantity,
        ])->seeJson(['status' => 'success']);

        $json = json_decode($response->response->getContent(), true);

        $rowId = key($json['cart']);

        $newQuantity = 4;

        $this->json('PATCH', 'api/v1/cart', [
            'items' => [
                [
                    'rowId' => $rowId,
                    'qty' => $newQuantity,
                ]
            ]
        ])->seeJson(['status' => 'success', 'qty' => $newQuantity]);
    }

    /**
     * Test if out of stock error works when adding new item
     * @return void
     */
    public function testAddOutOfStock() {
        $product = factory(App\Product::class)->create();
        $product->translations()->save(factory(App\ProductTranslation::class)->make());

        $quantity = $product->stock_quantity + 1; //Just one over the max. stock

        //Add item to cart
        $this->json('POST', 'api/v1/cart', [
            'id' => $product->id,
            'qty' => $quantity,
        ])->seeJson(['status' => 'failure', 'message' => 'Out of stock']);

        $this->json('GET', 'api/v1/cart')->seeJsonEquals([]); //Should be empty
    }
}
<file_sep>/resources/lang/it/auth.php
<?php

return [
    'failed' => 'Queste credenziali non corrispondono i nostri dischi.',
    'throttle' => 'Troppi tentativi di accesso. Riprova in :seconds secondi.',
    'already-logged-in' => 'Già effettuato il login',
    'login.title' => 'Accesso o <a href=":registerUrl">crea un account</a>',
    'login.create-email' => 'Apri un nuovo account con la tua email',
    'login.success' => 'Connesso',
    'login.or' => 'o',
    'form.email.label' => 'E-Mail',
    'form.password.change' => 'Cambia la password',
    'form.password_current.label' => 'Password attuale',
    'form.password.label' => 'Password',
    'form.confirm-password.label' => 'Conferma password',
    'form.remember' => 'Ricordami',
    'form.forgot' => 'Dimenticato password?',
    'form.current_password.error' => 'La password corrente non era corretta',
    'form.password.change.success' => 'La tua password è stata modificata',
    'register.title' => 'Creare un nuovo account',
    'register.advantages' => 'Crea un account per checkout facile e veloce accesso alla cronologia degli ordini.',
    'register.social-tip' => 'È possibile utilizzare Facebook e Google per creare un account più veloce!',
    'register.button' => 'Elenco',
    'register.success' => 'Il tuo account è pronto! Adesso divertiti!',
    'reset.title' => 'Resetta la password',
    'reset.button' => 'Invia Password Reset link',
    'reset.email.subject' => 'La vostra Password Reset link',
    'reset.email.click' => 'Clicca qui per reimpostare la password:',
    'cookies-consent' => 'Questo sito web utilizza i cookie per essere sicuri di ottenere la migliore esperienza sul nostro sito',
];<file_sep>/app/Http/Middleware/RefreshCart.php
<?php

namespace App\Http\Middleware;

use App\Basket;
use Closure;
use Gloudemans\Shoppingcart\Exceptions\ShoppingcartInvalidItemException;
use Zugy\Facades\Cart;

class RefreshCart
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if(auth()->check() && Cart::count(false) == 0) {
            $this->refreshCart();
        }

        return $next($request);
    }

    private function refreshCart() {
        foreach(auth()->user()->basket()->get() as $row) {
            try {
                Cart::associate('Product', 'App')->add($row->product_id, $row->name, $row->quantity, $row->price, $row->options);
            } catch(ShoppingcartInvalidItemException $e) {
                Basket::where('product_id', '=', $row->product_id)->where('user_id', '=', $row->user_id)->delete();
            }
        }
    }
}
<file_sep>/app/Exceptions/PaymentMethodDeclined.php
<?php
/**
 * User: <NAME>
 * Date: 11.09.2015
 * Time: 16:24
 */

namespace App\Exceptions;


class PaymentMethodDeclined extends \Exception
{

}<file_sep>/app/Http/Middleware/VerifyAge.php
<?php

namespace App\Http\Middleware;

use Closure;

class VerifyAge
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if(app()->environment("testing")) {
            return $next($request);
        }

        //Allow search crawlers, AJAX requests and Google Page Speed Insights
        if(is_bot() || $request->ajax()) {
            return $next($request);
        }

        if(!isset($_COOKIE['isOver18']) || $_COOKIE['isOver18'] != "true") {
            return response()->view('pages.age-splash', [], 412);
        }

        return $next($request);
    }
}
<file_sep>/resources/lang/en/forms.php
<?php

return [
    'prompts.select-quantity' => 'Select quantity',
    'prompts.select-how-many' => 'Please select how many you want!',
    'prompts.postcode' => 'Enter your postcode',
    'prompts.postcode.desc' => 'We currently only deliver in Milan, Italy',
    'action' => 'Action',
    'status' => 'Status',

    'error.slug' => 'The slug can only be alphanumeric and use dashes.',

    'danger-zone' => 'Danger Zone',
];<file_sep>/resources/assets/js/cart.js
(function( cart, $, undefined ) {
    var apiEndpoint = '/api/v1/cart';

    cart.add = function (productId, quantity) {
        return $.ajax({
            type: 'POST',
            url: apiEndpoint,
            data: {
                'id': productId,
                'qty': quantity
            },
            success: updateMiniCart,
            error: function(xhr, status, error) {
                var err = eval("(" + xhr.responseText + ")");
                swal(err.message, null, "error");
            }
        });
    };

    /**
     * Update an array of items in cart
     * @returns {*}
     * @param items
     */
    cart.update = function(items) {
        return $.ajax({
            type: 'PATCH',
            url: apiEndpoint,
            data: {
                items: items
            },
            error: function(xhr, status, error) {
                var err = eval("(" + xhr.responseText + ")");
                swal({
                    type: 'error',
                    title: err.message,
                    text: err.description,
                });
            }
        });
    };

    cart.delete = function(rowId) {
        console.log("Deleting", rowId);
        $.ajax({
            type: 'DELETE',
            url: apiEndpoint + '/' + rowId,
            success: updateMiniCart,
            error: function(xhr, status, error) {
                var err = eval("(" + xhr.responseText + ")");
                alert(err.message);
            }
        });
    };

    cart.addToCartAnimation = function($productImage) {
        var $cartTarget = $('.navbar .cart-icon:visible').eq(0);

        if($productImage) {
            var $imgClone = $productImage.clone().offset({
                top: $productImage.offset().top,
                left: $productImage.offset().left
            }).css({
                'opacity': '0.5',
                'position': 'absolute',
                'height': '150px',
                'width': '150px',
                'z-index': '100'
            }).appendTo($('body')).animate({
                'top': $cartTarget.offset().top + 10,
                'left': $cartTarget.offset().left + 10,
                'width': 75,
                'height': 75
            }, 1000, 'easeInOutExpo');

            $imgClone.animate({
                'width': 0,
                'height': 0
            }, function () {
                $(this).detach()
            });
        }
    };

    function updateMiniCart() {
        $.pjax.reload('#mini-cart-container', {timeout: 1500}).done(function() {
            var subtotal = $('#cart-subtotal').text();
            $('.cart-subtotal').text(subtotal);
            $('#mini-cart-container .products').mCustomScrollbar(); //Re-apply custom scroll bar when reloading
        });
    }

    /*
     * Make mini cart disappear when clicking anywhere else
     */
    var $navbarCart = $('#navbar-cart');

    function navbarCartHideClickHandler() {
        $navbarCart.collapse('hide');
    }

    $navbarCart.on('show.bs.collapse', function () {
        $(document).click(navbarCartHideClickHandler);
        $('.dropdown-toggle').click(navbarCartHideClickHandler);

        $navbarCart.click(function(e) {
            e.stopPropagation();
        });
    });

    $navbarCart.on('hide.bs.collapse', function () {
        $(document).unbind('click', navbarCartHideClickHandler);
        $('.dropdown-toggle').unbind('click', navbarCartHideClickHandler);
    });


}( window.cart = window.cart || {}, jQuery ));<file_sep>/resources/lang/en/your-account.php
<?php

return [
    'title' => 'Your account',
    'orders' => 'Orders',
    'orders.desc' => 'View, track or cancel an order',
    'orders.dispatch-to' => 'Dispatch to',
    'payment.manage' => 'Manage your payment methods',
    'settings.title' => 'Settings',
    'settings.email' => 'Change E-Mail address or password',
];<file_sep>/app/Zugy/PaymentGateway/Gateways/Stripe.php
<?php

namespace Zugy\PaymentGateway\Gateways;

use App\Payment;
use App\PaymentMethod;
use Carbon\Carbon;
use Stripe\Customer;

class Stripe extends AbstractGateway
{
    protected $methodName = 'stripe';

    /**
     * StripeService constructor.
     * @param $paymentMethod PaymentMethod
     */
    public function __construct(PaymentMethod $paymentMethod = null)
    {
        parent::__construct($paymentMethod);

        \Stripe\Stripe::setApiKey(config('services.stripe.secret'));
    }

    public function addOrUpdateMethod()
    {
        $this->paymentMethod = $this->fetchPaymentMethod();

        if($this->paymentMethod === null) {
            $customer = \Stripe\Customer::create([
                "source" => request('stripeToken'),
                "description" => auth()->user()->name,
                "email" => auth()->user()->email,
            ]);

            $this->paymentMethod = $this->createPaymentMethod([
                'stripeId' => $customer->id,
                'sources' => $customer->sources
            ]);
        } else {
            $customer = \Stripe\Customer::retrieve($this->paymentMethod->payload['stripeId']);

            if(request('stripeToken') !== null) { //Add a new card
                $card = $customer->sources->create([
                    "source" => request('stripeToken')
                ]);

                if(request('defaultPayment', false) == "true") {
                    $this->setCardAsDefault($customer, $card->id);
                }

                $customer = \Stripe\Customer::retrieve($this->paymentMethod->payload['stripeId']);
            } else { //Select a different card
                $customer = $this->setCardAsDefault($customer, request('cardId'));
            }

            $this->updatePaymentMethod([
                'stripeId' => $customer->id,
                'sources' => $customer->sources
            ]);

        }

        $this->setAsDefault(request('defaultPayment') !== null);

        return $this->paymentMethod;
    }

    public function charge($amount)
    {
        $payment = new Payment();

        $result = \Stripe\Charge::create([
            'amount' => (int) ($amount * 100), //Convert 15.99 to 1599
            'currency' => 'eur',
            'customer' => $this->paymentMethod->payload['stripeId']
        ]);

        $payment->paid = Carbon::now();

        $payment->metadata = [
            'id' => $result->id,
            'source' => $result->source
        ];

        $payment->status = 1; //Mark as paid

        $payment->amount = $result->amount / 100; //Convert back to decimal form
        $payment->currency = $result->currency;
        $payment->method = $this->paymentMethod->method;

        return $payment;
    }

    public function getFormatted()
    {
        //First card in sources array is always default card
        return [
            'method' => 'card',
            'card' => [
                'brand' => $this->paymentMethod->payload['sources']['data'][0]['brand'],
                'last4' => $this->paymentMethod->payload['sources']['data'][0]['last4'],
                'expiry' => $this->paymentMethod->payload['sources']['data'][0]['exp_month'] . '/' . $this->paymentMethod->payload['sources']['data'][0]['exp_year']
            ]
        ];
    }

    public function listCards()
    {
        $customer = \Stripe\Customer::retrieve($this->paymentMethod->payload['stripeId']);

        $cards = [];

        foreach($customer['sources']['data'] as $card) {
            if($card['object'] == 'card') $cards[] = $card;
        }

        return $cards;
    }

    /**
     * Set the specified card as default
     * @param Customer $customer
     * @param $cardId
     * @return Customer
     */
    public function setCardAsDefault(Customer $customer, $cardId) {
        $customer->default_source = $cardId;
        return $customer->save();
    }
}<file_sep>/app/Exceptions/Coupons/CouponNotStartedException.php
<?php

namespace App\Exceptions\Coupons;

class CouponNotStartedException extends \Exception
{

}<file_sep>/app/Zugy/PaymentGateway/Exceptions/PaymentFailedException.php
<?php

namespace Zugy\PaymentGateway\Exceptions;


class PaymentFailedException extends  \Exception
{

}<file_sep>/resources/lang/en/footer.php
<?php

return [
    'email' => 'Email',
    'information' => 'Information',
    'my-account' => 'My Account',
    'my-orders' => 'My orders',
    'newsletter-subscribe-btn' => 'Subscribe',
    'rights-reserved' => 'All rights reserved.',
    'stay-in-touch' => 'Stay In Touch',
    'support' => 'Support',
    'tax-code' => 'Tax Code',
];<file_sep>/app/Http/Middleware/Checkout.php
<?php

namespace App\Http\Middleware;

use Closure;
use Zugy\Facades\Cart;
use Illuminate\Contracts\Auth\Guard;
use Carbon\Carbon;

class Checkout
{
    /**
     * The Guard implementation.
     *
     * @var Guard
     */
    protected $auth;

    /**
     * Create a new filter instance.
     *
     * @param  Guard  $auth
     * @return void
     */
    public function __construct(Guard $auth)
    {
        $this->auth = $auth;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $reopeningTime = Carbon::create(2017, 9, 28, 4, 0, 0); //4 am Wednesday

        if(Carbon::now() < $reopeningTime) {
            return response()->view('pages.holiday-closed', [], 503);
        }

        if(Cart::count() == 0) {
            return redirect(localize_url('routes.cart'));
        }

        if ($this->auth->guest() && !session()->has('checkout.guest')) {
            return redirect()->guest(localize_url('routes.checkout.landing'));
        }

        return $next($request);
    }
}
<file_sep>/resources/lang/en/order.php
<?php

return [
    'status.0' => 'New order',
    'status.1' => 'Being processed',
    'status.2' => 'Out for delivery',
    'status.3' => 'Delivered',
    'status.4' => 'Cancelled',

    'number'    => 'Order number',
    'date'      => 'Order date',
    'create'    => 'Order placed',
    'completed' => 'Order completed',
    'details'   => 'Order details',
    'summary'   => 'Order summary',
    'history'   => 'Order history',

    'api.update.success' => 'Order updated',
    'api.error.status-same' => 'Order already set to this status',

    'email.confirmation.subject' => 'Your Zugy order confirmation and receipt [#:id]',
    'email.confirmation.paid' => 'Total paid',
    'email.delivery.subject' => 'Your Zugy order [#:id] is out for delivery',
    'email.delivery.driver' => 'Our driver is now on his way to you!',
    'email.delivery.trouble' => 'If anything goes wrong or we have trouble finding you, we will call you on your phone at <b>:phone</b>',
    'email.cancelled.subject' => 'Your Zugy order [#:id] was cancelled',
    'email.cancelled.questions' => 'If you have any questions regarding this cancellation you can reply to this email or call us at :phone.',
    'email.track.description' => 'You can track the current status of your order by clicking the button below.',
    'email.track' => 'Track order',

    'sign-in' => '<a href=":loginUrl">Sign in</a> to view more information on your order.',

    'total-before-vat' => 'Total before VAT',
    'vat' => 'VAT',
    'include-vat' => 'Order totals include',
];<file_sep>/app/Http/Controllers/API/PostcodeController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use Zugy\Validators\PostcodeValidator;

class PostcodeController extends Controller
{
    /**
     * GET api/v1/postcode/{postcode}
     * @param $postcode
     * @return mixed
     */
    public function checkPostcode($postcode)
    {
        $validator = Validator::make(['postcode' => $postcode], [
            'postcode' => 'integer|digits:5'
        ]);

        if($validator->fails()) {
            return response()->json(['status' => 'failure', 'message' => trans('postcode.api.error.postcode.invalid'), 'errors' => $validator->getMessageBag()], 400);
        }

        $postcode = (int) $postcode;

        if(PostcodeValidator::isInDeliveryRange($postcode)) {
            $response = response()->json([
                'status' => 'success',
                'delivery' => true,
                'storeUrl' => localize_url('routes.shop.index'),
                'message' => trans('postcode.check.success'),
            ]);

        } else {
            $response = response()->json([
                'status' => 'success',
                'delivery' => false,
                'message' => trans('postcode.check.error.title'),
                'description' => trans('postcode.check.error.desc'),
                'storeUrl' => localize_url('routes.shop.index'),
                'confirmButtonText' => trans('postcode.browse-store'),
            ]);
        }

        return $response->withCookie(cookie()->forever('postcode', $postcode,  $path = null, $domain = null, $secure = false, $httpOnly = false));
    }
}<file_sep>/app/Exceptions/Coupons/CouponExpiredException.php
<?php

namespace App\Exceptions\Coupons;

class CouponExpiredException extends \Exception
{

}<file_sep>/resources/lang/it/passwords.php
<?php

return [
    'user' => 'Non esiste un utente associato a questo indirizzo e-mail.',
    'password' => 'Le password devono essere di almeno 6 caratteri e devono coincidere.',
    'token' => 'Questo token per la reimpostazione della password non è valido.',
    'sent' => 'Promemoria della password inviato!',
    'reset' => 'La password è stata reimpostata!',
];
<file_sep>/resources/lang/it/status.php
<?php

return [
    'payment.0' => 'Non pagato',
    'payment.1' => 'Pagato',
    'payment.2' => 'Rimborsato',
    'payment.3' => 'Pagamento alla consegna',
];<file_sep>/app/Zugy/Facades/Maps.php
<?php

namespace Zugy\Facades;

use Illuminate\Support\Facades\Facade;

class Maps extends Facade
{
    protected static function getFacadeAccessor() { return 'maps'; }
}<file_sep>/app/Category.php
<?php

namespace App;

use Dimsav\Translatable\Translatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;

class Category extends Model
{
    use Translatable;

    protected $table = 'categories';

    public $translatedAttributes = ['name', 'slug', 'meta_description'];

    protected $casts = [
        'id' => 'integer',
        'parent_id' => 'integer',
    ];

    public function products() {
        return $this->belongsToMany('App\Product', 'products_to_categories');
    }

    static public function buildTree() {
        $refs = [];
        $list = [];

        $rows = Category::with('translations')->get();

        foreach($rows as $row) {
            $ref = & $refs[$row->id];

            $ref['id'] = $row->id;
            $ref['parent_id']  = $row->parent_id;
            $ref['position'] = $row->position;
            $ref['name'] = $row->name;
            $ref['slug'] = $row->slug;
            $ref['product_count'] = $row->products()->inStock()->count();

            if($row->parent_id == null) {
                $list[$row->id] = & $ref;
            } else {
                $refs[$row->parent_id]['children'][$row->id] = & $ref;

                //Update parent product count
                $parent = & $refs[$row->parent_id];
                while(1) {
                    $parent['product_count'] += $ref['product_count'];

                    if($parent['parent_id'] == null) break;
                    $parent = & $refs[$parent['parent_id']];
                }

                //FIXME If parent is added afterwards, it needs to sum up its existing children
            }
        }

        return $list;
    }

    /**
     * Print the category menu from cache if available
     *
     * @return mixed
     *
     * Print nested list of all categories
     */
    static public function printList() {
        return Cache::remember('ui.category-sidebar.lang-' . \Localization::getCurrentLocale(), 5, function() {
            //Sort by position column, 0 comes first
            $list = collect(Category::buildTree())->sortBy(function($menuItem, $key) {
                if($menuItem['position'] === null) return PHP_INT_MAX;
                else return $menuItem['position'];
            });

            return self::toUL($list->toArray());
        });
    }

    static private function toUL(array $array)
    {
        $html = '<ul class="list-group">' . PHP_EOL;

        foreach ($array as $value)
        {
            if($value['product_count'] == 0) continue; //Skip empty categories

            $html .= '<li class="list-group-item"><a href="' . localize_url('routes.shop.category', ['slug' => $value['slug']]) . '">' . "{$value['name']} ({$value['product_count']})";
            if (!empty($value['children']))
            {
                $html .= self::toUL($value['children']);
            }
            $html .= '</a></li>' . PHP_EOL;
        }

        $html .= '</ul>' . PHP_EOL;

        return $html;
    }

    static public function printSelect() {
        $tree = Category::buildTree();

        function printTree($tree, $depth = 0, $list = []) {
            foreach($tree as $t) {
                $dash = ($t['parent_id'] == 0) ? '' : str_repeat("&nbsp;&nbsp;&nbsp;&nbsp;", $depth) .' ';
                $list[] =  [$t['id'], $dash . $t['name']];

                if(isset($t['children'])) {
                    $list = array_merge($list, printTree($t['children'], $depth + 1));
                }
            }
            return $list;
        }

        $result = printTree($tree);

        $array = [];

        foreach($result as $v) {
            $array[$v[0]] = $v[1];
        }

        return $array;
    }

    static public function getDirectSubCategories(self $category) {
        return self::where('parent_id', '=', $category->id)->get();
    }

    /**
     * Cache the categories menu
     *
     * 0 => [
     *     'parent' => []
     *     'children' => []
     * ]
     * @param $category_id int
     * @return array
     */
    static public function cacheMegaMenu($category_id) {
        $cache = Cache::remember('categories.menu.lang-' . \Localization::getCurrentLocale(), 5, function() {
            $categories = self::all();
            $return = [];

            foreach($categories as $parent) {
                $children = self::getDirectSubCategories($parent);

                $children->load('translations');

                $return[$parent->id] = [
                    'parent' => $parent,
                    'children' => $children
                ];
            }

            return $return;
        });

        return $cache[$category_id];
    }
}
<file_sep>/resources/lang/it/notifications.php
<?php

return [
    'success' => 'Successo',
    'info' => 'Informazioni',
    'error' => 'Errore',
    'warning' => 'Attenzione',
];<file_sep>/database/migrations/2016_01_23_174515_create_coupons_table.php
<?php

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

class CreateCouponsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('coupons', function (Blueprint $table) {
            $table->increments('id');
            $table->string('code', 255)->unique();
            $table->dateTime('starts')->nullable();
            $table->dateTime('expires')->nullable();
            $table->decimal('minimumTotal', 10, 2)->unsigned()->nullable();
            $table->integer('percentageDiscount')->unsigned()->nullable();
            $table->decimal('flatDiscount', 10, 2)->unsigned()->nullable();
            $table->integer('max_uses')->unsigned()->nullable();
            $table->integer('uses')->unsigned()->default(0);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('coupons');
    }
}
<file_sep>/tests/AdminProductTest.php
<?php

class AdminProductTest extends TestCase
{
    protected $admin;

    public function setUp()
    {
        parent::setUp();

        $this->admin = factory(App\User::class, 'admin')->make();
    }


    public function testIndexProductView()
    {
        $this->actingAs($this->admin)->visit('admin/catalogue')->see('Catalogue');
    }

    public function testAddProductView()
    {
        $this->actingAs($this->admin)->visit('admin/catalogue/create')->see('Add a product');
    }
}<file_sep>/resources/lang/it/ageSplash.php
<?php

return [
    'prompt' => 'Inserisci la tua età per entrare.',
    'over' => 'Sopra :age',
    'under' => 'Sotto :age',
    'sorry.title' => 'Siamo spiacenti, solo per maggiorenni!',
    'sorry.desc' => 'Siamo spiacenti, ma vendiamo e consegniamo alcolici solo a maggiorenni.',
    'sorry.party' => 'Noi, comunque, saremo lì con voi per festeggiare il tuo 18esimo compleanno!',
];<file_sep>/app/Coupon.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Coupon extends Model
{
    protected $dates = ['starts', 'expires'];

    public function getDescriptionAttribute()
    {
        if($this->attributes['percentageDiscount'] != null) {
            return trans('coupon.percentageDiscountDescription', ['percentage' => $this->attributes['percentageDiscount']]);
        } elseif($this->attributes['flatDiscount'] != null) {
            return trans('coupon.flatDiscountDescription', ['amount' => $this->attributes['flatDiscount']]);
        }

        throw new \RuntimeException();
    }
}<file_sep>/app/Zugy/PaymentGateway/PaymentGatewayServiceProvider.php
<?php

namespace Zugy\PaymentGateway;

use Illuminate\Support\ServiceProvider;

class PaymentGatewayServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind('paymentGateway', function()
        {
            return new PaymentGateway();
        });
    }
}<file_sep>/database/seeds/TaxClassSeeder.php
<?php
use Illuminate\Database\Seeder;

class TaxClassSeeder extends Seeder
{
    public function run() {
        DB::table('tax_classes')->insert([
            ['id' => 1, 'title' => 'Alcohol', 'description' => 'Alcoholic Beverages', 'tax_rate' => '22.0'],
            ['id' => 2, 'title' => 'Food', 'description' => '', 'tax_rate' => '4.0']
        ]);
    }
}<file_sep>/app/Basket.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class Basket extends Model
{
    protected $table = 'users_basket';

    protected $casts = [
        'options' => 'json'
    ];

    protected $fillable = [
        'product_id',
        'name',
        'quantity',
        'price',
        'options',
    ];

    public function product() {
        return $this->belongsTo('App\Product');
    }

    /**
     * Set the keys for a save update query.
     * This is a fix for tables with composite keys
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    protected function setKeysForSaveQuery(Builder $query)
    {
        $query
            //Put appropriate values for your keys here:
            ->where('user_id', '=', $this->user_id)
            ->where('product_id', '=', $this->product_id);

        return $query;
    }
}
<file_sep>/app/Zugy/PaymentGateway/PaymentGateway.php
<?php

namespace Zugy\PaymentGateway;

use App\PaymentMethod;
use Zugy\PaymentGateway\Gateways\AbstractGateway;
use Zugy\PaymentGateway\Gateways\Cash;
use Zugy\PaymentGateway\Gateways\PayPal;
use Zugy\PaymentGateway\Gateways\Stripe;
use Zugy\PaymentGateway\Exceptions\PaymentMethodUndefinedException;

class PaymentGateway
{
    /**
     * Sets the payment gateway
     * @param $gateway
     * @return AbstractGateway|string
     * @throws PaymentMethodUndefinedException
     */
    public function set($gateway) {
        $paymentMethod = null;

        if($gateway instanceof PaymentMethod) {
            $paymentMethod = $gateway;
            $gateway = $paymentMethod->method;
        }

        switch($gateway) {
            case 'stripe':
                return new Stripe($paymentMethod);
                break;
            case 'cash':
                return new Cash($paymentMethod);
                break;
            case 'paypal':
                return new PayPal($paymentMethod);
                break;
            default:
                throw new PaymentMethodUndefinedException("Using $gateway");
                break;
        }
    }
}<file_sep>/app/Exceptions/Coupons/CouponOrderMinimumException.php
<?php

namespace App\Exceptions\Coupons;

class CouponOrderMinimumException extends \Exception
{

}<file_sep>/app/Policies/ProductPolicy.php
<?php

namespace App\Policies;

use App\Product;
use App\User;

class ProductPolicy extends BasePolicy
{
    protected $writeAccessGroupIds = [1,2]; //Super Admins, Admins

    public function create(User $user, Product $product)
    {
        return $this->hasWriteAccess($user);
    }

    public function update(User $user, Product $product)
    {
        return $this->hasWriteAccess($user);
    }
}<file_sep>/app/Zugy/Facades/Checkout.php
<?php

namespace Zugy\Facades;


use Illuminate\Support\Facades\Facade;

class Checkout extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor() { return 'checkout'; }
}<file_sep>/database/migrations/2015_06_19_101843_create_orders_table.php
<?php

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

class CreateOrdersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->engine = 'InnoDB';

            $table->increments('id');

            $table->integer('user_id')->unsigned()->nullable();
            $table->foreign('user_id')
                ->references('id')->on('users')
                ->onDelete('no action');

            $table->tinyInteger('order_status')->unsigned();
            $table->dateTime('order_placed');
            $table->dateTime('order_completed')->nullable();
            $table->text('comments');

            $table->string('email');
            $table->string('delivery_name', 64);
            $table->string('delivery_line_1', 64);
            $table->string('delivery_line_2', 64)->nullable();
            $table->string('delivery_city', 32);
            $table->string('delivery_postcode', 10);
            $table->string('delivery_state', 32)->nullable();
            $table->string('delivery_phone', 32);
            $table->integer('delivery_country_id');
            $table->foreign('delivery_country_id')
                ->references('id')->on('countries')
                ->onDelete('no action');
            $table->text('delivery_instructions')->nullable();

            $table->decimal('shipping_fee', 10, 2)->unsigned();

            $table->char('currency', 3);

            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('orders');
    }
}
<file_sep>/resources/lang/it/forms.php
<?php

return [
    'prompts.select-quantity' => 'Seleziona quantità',
    'prompts.select-how-many' => 'Si prega di selezionare il numero che si desidera!',
    'prompts.postcode' => 'Inserisci codice postale',
    'prompts.postcode.desc' => 'Al momento consegniamo solo a Milano, Italia',
    'action' => 'Azione',
    'status' => 'Stato',
    'error.slug' => 'La lumaca può essere solo trattini alfanumerici e utilizzare.',
    'danger-zone' => 'Occhio!',
];<file_sep>/app/Zugy/ActivityLogParser/ActivityLogParser.php
<?php

namespace Zugy\ActivityLogParser;

use Illuminate\Database\Eloquent\Collection;

class ActivityLogParser
{
    public function order(Collection $activity) {
        $return = [];

        foreach($activity as $a) {
            switch($a->action) {
                case 'status-change':
                    $status = trans('order.status.' . $a->description);
                    $description = trans('activityLogParser.order.status-changed', ['status' => $status]);
                    $title = $status;
                    $type = $this->getOrderStatusType($a->description);
                    $icon  = $this->getOrderStatusIcon($a->description);
                    break;

                case 'create':
                    $type = '';
                    $description = trans('order.create');
                    $title = $description;
                    $icon = 'navicon';
                    break;
                default:
                    throw new \Exception("Activity action does not exist");
            }

            $return[] = [
                'user' => $a->user,
                'timestamp' => $a->created_at,
                'title' => $title,
                'description' => $description,
                'type' => $type,
                'icon' => $icon,
            ];
        }

        return $return;
    }

    private function getOrderStatusType($statusId)
    {
        switch($statusId) {
            case 1:
                return 'warning';
            case 2:
                return 'primary';
            case 3:
                return 'success';
            case 4:
                return 'danger';
            default:
                return '';
        }
    }

    private function getOrderStatusIcon($statusId)
    {
        switch($statusId) {
            case 1:
                return 'ellipsis-h';
            case 2:
                return 'truck';
            case 3:
                return 'check';
            case 4:
                return 'times';
            default:
                return 'check';
        }
    }
}<file_sep>/app/Zugy/Repos/Product/ProductRepository.php
<?php
/**
 * User: <NAME>
 * Date: 18.09.2015
 * Time: 02:21
 */

namespace Zugy\Repos\Product;


interface ProductRepository
{
    public function all($sort = 'sales', $direction = 'desc');
    public function category($category_slug, $sort = 'sales', $direction = 'desc');
    public function getBySlug($slug);
    public function search($query);
}<file_sep>/resources/lang/it/menu.php
<?php

return [
    'my-account' => 'Il mio account',
    'admin-dashboard' => 'Dashboard amministrativo',
    'sign-out' => 'Disconnessione',
    'sign-in' => 'Registrati',
    'your-account' => 'Il tuo account',
    'account-settings' => 'Impostazioni del account',
    'cart' => 'Carrello',
    'shop' => 'Negozio',
    'admin.settings' => 'Impostazioni',
    'admin.settings.tax' => 'Tassa',
];<file_sep>/resources/lang/it/coupon.php
<?php

return [
    '404' => 'Il coupon non esiste',
    'claimed' => 'Questo coupon è già stato rivendicato, ci dispiace',
    'expired' => 'Questo coupon è scaduto il :date',
    'notActive' => 'Questo coupon diventa attivo a :date',
    'minimum' => 'Si ordina fabbisogno totale di essere almeno :min Euro per utilizzare questo coupon',
    'using' => 'Codice coupon utilizzando <b>:code</b>',
    'flatDiscountDescription' => '-:amount&euro; di meno',
    'percentageDiscountDescription' => ':percentage% di meno',
];<file_sep>/app/Listeners/CartEventsListener.php
<?php

namespace App\Listeners;

use Zugy\Facades\Cart;
use Illuminate\Support\Facades\Request;
use Zugy\Facades\Checkout;

class CartEventsListener
{
    public function onAddCart($id, $name, $quantity, $price, $options  = null) {
        if(auth()->check() && Request::ajax()) {
            $row = auth()->user()->basket()->firstOrNew(['product_id' => $id]);

            $row->fill([
                'quantity' => $quantity,
                'price' => $price,
                'name' => $name,
                'options' => $options
            ])->save();

            $this->forgetCoupon();
        }
    }

    public function onUpdateCart($rowId) {
        if(auth()->check()) {
            $item = Cart::get($rowId);

            $row = auth()->user()->basket()->firstOrNew(['product_id' => $item->id]);
            $row->price = $item->price;
            $row->quantity = $item->qty;
            $row->options = $item->options;

            \Log::debug('Updating basket in DB', ['productId' => $item->id, 'quantity' => $item->qty]);

            $row->save();

            $this->forgetCoupon();
        }
    }

    public function onRemoveCart($rowId) {
        if(auth()->check()) {
            $item = Cart::get($rowId);

            auth()->user()->basket()->where('product_id', '=', $item->id)->delete();
        }

        $this->forgetCoupon();
    }

    public function onDestroyCart() {
        if(auth()->check()) {
            auth()->user()->basket()->delete();
        }

        $this->forgetCoupon();
    }

    /**
     * Synchronise guest cart to database when user logs in
     */
    public function onLogin()
    {
        $guestCart = Cart::content();

        \Log::debug('Guest cart content', ['cart' => $guestCart]);

        $guestProductIds = [];
        foreach($guestCart as $item) {
            $row = auth()->user()->basket()->firstOrNew(['product_id' => $item->id]);

            $row->fill([
                'quantity' => $item->qty,
                'price' => $item->price,
                'name' => $item->name,
                'options' => $item->options
            ])->save();

            $guestProductIds[] = $item->id;
        }

        $missingDbProducts = auth()->user()->basket()->whereNotIn('product_id', $guestProductIds)->get();

        foreach($missingDbProducts as $p) {
            \Log::debug('Adding missing product to cart', ['name' => $p->name]);
            Cart::associate('Product', 'App')->add($p->product_id, $p->name, $p->quantity, $p->price, $p->options);
        }
    }

    private function forgetCoupon()
    {
        Checkout::forgetCoupon();
    }

    public function subscribe($events) {
        $events->listen(
            'cart.added',
            'App\Listeners\CartEventsListener@onAddCart'
        );

        $events->listen(
            'cart.updated',
            'App\Listeners\CartEventsListener@onUpdateCart'
        );

        $events->listen(
            'cart.remove',
            'App\Listeners\CartEventsListener@onRemoveCart'
        );

        $events->listen(
            'cart.destroy',
            'App\Listeners\CartEventsListener@onDestroyCart'
        );

        $events->listen(
            'Illuminate\Auth\Events\Login',
            'App\Listeners\CartEventsListener@onLogin'
        );
    }
}<file_sep>/app/Exceptions/OutOfStockException.php
<?php

namespace App\Exceptions;

use App\Language;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;

class OutOfStockException extends \Exception
{
    protected $products = []; //Array of products that are out of stock

    public function __construct($products) {
        $this->products = $products;
    }

    public function getProducts() {
        return $this->products;
    }

    public function getErrorMessages() {
        $errors = [];
        $language_id = Language::getLanguageId(LaravelLocalization::getCurrentLocale());

        foreach($this->products as $p) {
            if($p->stock_quantity == 0) {
                $errors[] = "{$p->title} is out of stock. Please remove the item from your cart to place your order.";
            } else {
                $errors[] = "{$p->title} only has {$p->stock_quantity} units left in stock. Please reduce the order quantity in your cart to place your order.";
            }
        }

        return $errors;
    }
}<file_sep>/resources/lang/it/checkout.php
<?php

return [
    'title' => 'Convalida ordine',
    'items' => 'Articoli',
    'total' => 'Totale',
    'subtotal' => 'subtotale',
    'shipping' => 'Spedizione',
    'address.title' => 'Indirizzo',
    'address.prompt' => 'Per favore inserisci il tuo indirizzo',
    'address.choose-delivery' => 'Scegli un indirizzo di consegna',
    'address.select-delivery' => 'Spedisci a questo indirizzo',
    'address.deliver-new' => 'Spedisci ad un nuovo indirizzo',
    'address.deliver-new-desc' => 'Aggiungi un nuovo indirizzo di consegna nella rubrica',
    'address.form.delivery' => 'Indirizzo spedizione',
    'address.form.billing' => 'Indirizzo fatturazione',
    'address.form.name' => 'Nome completo',
    'address.form.line_1' => 'Indirizzo numero 1',
    'address.form.line_1.desc' => 'Numero civico e via, P.O. box, azienda, c/o',
    'address.form.line_2' => 'Indirizzo numero 2',
    'address.form.line_2.desc' => 'Numero appartamento,citofono, scala, piano, etc.',
    'address.form.zip' => 'CAP/ Codice postale',
    'address.form.town' => 'Località / città',
    'address.form.country' => 'Nazione',
    'address.form.instructions' => 'Istruzioni per la consegna',
    'address.form.instructions.desc' => 'Ulteriori informazioni per il nostro autista',
    'address.form.phone' => 'Telefono',
    'address.form.phone.desc' => 'Nel caso in cui il nostro autista ha bisogno di contattare l\'utente per la consegna',
    'address.form.default' => 'Utilizzare questo indirizzo di default per i futuri ordini',
    'address.form.same' => 'Stesso come indirizzo di consegna',
    'address.form.country.italy' => 'Italia',
    'payment.title' => 'Pagamento',
    'payment.form.title' => 'Selezionare il metodo di pagamento',
    'payment.form.card' => 'Carta',
    'payment.form.card.new' => 'Aggiungere una nuova carta',
    'payment.form.card.show' => '<b>:brand</b> finito in :last4',
    'payment.form.card.expires' => 'Scade',
    'payment.form.name' => 'nome dell \'intestatario della carta',
    'payment.form.cvc' => 'CVC',
    'payment.form.expiration' => 'Data scad.',
    'payment.form.default' => 'Impostare questo come il metodo di pagamento predefinito',
    'payment.form.card.button' => 'Utilizzare carta di credito',
    'payment.form.pay-with' => 'Paga con :name',
    'payment.form.cash' => 'Contanti',
    'payment.form.cash.desc' => 'Pagare in contanti al momento della consegna',
    'payment.form.error.different' => 'Si è verificato un errore durante l\'elaborazione del vostro pagamento. Riprovare o utilizzare un altro metodo di pagamento. <a href=":paymentUrl?change">Cambiare </a>',
    'review.title' => 'Rivedere ordine',
    'review.age-warning' => 'Vendiamo solo l\'alcol a persone <b>di età superiore a 18 anni</b>, anche se l\'età legale per bere è 16 in Italia. I nostri autisti vi chiederà per la tua foto ID per verificare la tua età.',
    'review.coupon' => 'Buono',
    'review.coupon.desc' => 'Inserisci un codice coupon se ne hai uno',
    'review.coupon.placeholder' => 'Inserisci il codice',
    'review.items' => 'Rivedere elementi',
    'review.place-order' => 'Invia ordine',
    'review.accept' => 'Effettuando l\'ordine l\'utente accetta di :siteName\'s <a href=":privacyPolicyUrl">Informativa sulla privacy</a> and <a href=":termsAndConditionsUrl">Termini e condizioni</a>.',
    'review.delivery-time' => 'Tempo di consegna',
    'review.delivery-time.label' => 'Quando volete che il vostro ordine da consegnare?',
    'review.delivery-time.asap' => 'Il prima possibile',
    'review.delivery-time.slot' => 'Scegliere uno slot di consegna',
    'review.delivery-time.error.late' => 'È troppo tardi per scegliere questo tempo di consegna. Si prega di selezionare un altro momento',
    'review.delivery-time.error.closed' => 'Siamo spiacenti ma il servizio è attivo sono dall\'una del pomeriggio all\'una di mattina. Riprova piú tardi ¯\_(ツ)_/¯',
    'order' => 'Ordine',
    'order.success' => 'Il tuo ordine è stato inoltrato. Ti manderemo una notifica via email quando è fuori per la consegna.',
];<file_sep>/app/Policies/OrderPolicy.php
<?php

namespace App\Policies;

use App\Order;
use App\User;

class OrderPolicy extends BasePolicy
{
    public function before(User $user, $ability) {
        if($this->isSuperAdmin($user)) {
            return true;
        }
    }

    public function show(User $user, Order $order)
    {
        return $user->id === $order->user_id || $this->isAdmin($user);
    }

    public function adminUpdate(User $user, Order $order) {
        return $this->isAdmin($user);
    }

    public function index(User $user, Order $order) {
        return $this->isAdmin($user);
    }
}<file_sep>/app/Http/Middleware/SetLocaleFromSession.php
<?php

namespace App\Http\Middleware;

use Closure;

/**
 * Set the locale based on the session variable, only for API routes
 * For the Laravel Localisation package by mcamara
 * Class SetLocaleFromSession
 * @package App\Http\Middleware
 */
class SetLocaleFromSession
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     * @throws \Exception
     */
    public function handle($request, Closure $next)
    {
        $locale = session('locale');

        if(!is_null($locale)) {
            \Localization::setLocale($locale);
        }

        \Carbon::setLocale(\Localization::getCurrentLocale());

        switch (\Localization::getCurrentLocale()) {
            case 'it':
                $success = setlocale(LC_TIME, 'it_IT.utf8');
                break;
            case 'en':
                $success = setlocale(LC_TIME, 'en_GB.utf8');
                break;
            default:
                throw new \Exception("Locale not found");
        }

        if(!$success) throw new \Exception("Failed setting locale: " . \Localization::getCurrentLocale() . " | Current Locale: " . setlocale(LC_ALL, 0));

        return $next($request);
    }
}
<file_sep>/resources/lang/en/auth.php
<?php

return [
    'failed' => 'These credentials do not match our records.',
    'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',

    'already-logged-in' => 'Already logged in',
    'login.title' => 'Login or <a href=":registerUrl"> create an account</a>',
    'login.create-email' => 'Create account with email',
    'login.success' => 'Logged in',
    'login.or' => 'or',

    'form.email.label' => 'E-Mail',
    'form.password.change' => 'Change password',
    'form.password_current.label' => 'Current Password',
    'form.password.label' => 'Password',
    'form.confirm-password.label' => 'Confirm Password',
    'form.remember' => 'Remember me',
    'form.forgot' => 'Forgot password?',
    'form.current_password.error' => 'Your current password was incorrect',
    'form.password.change.success' => 'Your password has been changed',

    'register.title' => 'Create a new account',
    'register.advantages' => 'Create an account for fast checkout and easy access to order history.',
    'register.social-tip' => 'You can use Facebook and Google to create an account faster!',
    'register.button' => 'Register',
    'register.success' => 'Created an account successfully, have fun shopping!',

    'reset.title' => 'Reset Password',
    'reset.button' => 'Send Password Reset Link',
    'reset.email.subject' => 'Your Password Reset Link',
    'reset.email.click' => 'Click here to reset your password:',

    'cookies-consent' => 'This website uses cookies to ensure you get the best experience on our website',
];<file_sep>/app/Console/Commands/SyncAlgolia.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class SyncAlgolia extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'algolia:sync {model}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Synchronise an Algolia model to the Algolia servers.';

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

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $model = "App\\" . $this->argument('model');

        $model = new $model();

        $usedTraits = class_uses($model);

        if(!isset($usedTraits['AlgoliaSearch\Laravel\AlgoliaEloquentTrait'])) {
            $this->error('The model is not using the AlgoliaEloquentTrait!');
            return;
        }

        $elements = $model->all();

        $bar = $this->output->createProgressBar($elements->count());
        $bar->setFormat("%message%\n %current%/%max% [%bar%] %percent:3s%%");
        $bar->setMessage('Starting synchronisation');
        $bar->start();

        foreach($elements as $element) {
            $bar->setMessage("Syncing {$element->title}\n");
            $element->pushToIndex();
            $bar->advance();
        }

        $model::setSettings(); //Push index settings

        $bar->finish();
    }
}
<file_sep>/app/Http/Controllers/AccountController.php
<?php

namespace App\Http\Controllers;


class AccountController extends Controller
{
    public function getHome()
    {
        return view('pages.account.home');
    }

    public function getOrders()
    {
        $orders = auth()->user()->orders()->orderBy('order_placed', 'desc')->get();

        return view('pages.account.orders')->with(['orders' => $orders]);
    }

    public function getSettings()
    {
        return view('pages.account.settings');
    }
}<file_sep>/resources/lang/en/pages.php
<?php

return [
    'about-us.title' => 'About',
    'about-us.desc' => '<p>We at Zugy operate just like a regular retail liquor store with a small but significant twist: you visit our website instead of walking into a store. You still have an equally large selection of drinks; we have about 50 different choices in stock. Simply pick what you need, place your order and we deliver it right to your front door. You can pay by card, PayPal or cash on delivery.</p>
<p>Technology has changed many industries in the past decade but it has yet to be involved in the alcohol industry. With Zugy we are revolutionizing the way in which people enjoy beverages by incorporating current technology into our operations.</p>',
    'about-us.find-out-more' => 'Find out more on social media',
    'about-us.partners' => 'Partners',
    'about-us.partners-desc' => '<p>Zugy is proudly partnered with <a href="http://securitycourier.it/">Security Courier Express</a>, who deliver your order from our fulfilment centre to your front door.</p>
<p>We only use bicycle couriers, ensuring our deliveries arrive within 45 minutes and with zero CO2 emissions <i class="fa fa-leaf"></i>.</p>',
    'about-us.mission-statement.title' => 'Mission Statement',
    'about-us.mission-statement.desc' => '<p>At Zugy, our goal has always and will always be to make our customers happy. We want to make the memories last, be it that special bottle that leads to great conversation or the party pack that makes for an unforgettable birthday.</p> 
    <p>Whether it\'s a crazy night or you\'re just spending time with your loved ones, we hope you choose Zugy to make your night last forever.</p>',
    'contact.title' => 'Contact',
    'contact.address' => 'Zugy Srl. <br>Alzaia Navigli Grande 36<br> Milan, 20141 <br> Italy',
    'contact.customer-service' => 'Below is the customer service line – you can direct any questions, comments, or concerns to this number :',
    'contact.email.general' => 'For general inquiries:',
    'contact.email.investors' => 'For investors:',
    'shop.title' => 'Shop',
    'home.title' => 'Home',
    'home.meta_description' => 'Zugy is an online liquor store in Milan that delivers within an hour.',
    'home.meta-title' => 'Alcohol Delivery on-demand for Milan',
    'home.tagline' => 'Your favorite beer, wine and spirits delivered to your doorstep',
    'home.marketing.address.title' => 'Set your address',
    'home.marketing.address.desc' => "Select from a wide range of alcholic drinks to be delivered to your door step.",
    'home.marketing.time.title' => 'Order in minutes',
    'home.marketing.time.desc' => 'Add what you want to your basket, pay at the checkout and you\'re done.',
    'home.marketing.delivery.title' => 'Delivery to your door',
    'home.marketing.delivery.desc' => 'We\'ll start preparing your order right away, and deliver it to your doorstep. Simply present your photo ID to our friendly drivers and enjoy!',
    'home.exclusive' => 'Currently serving Milan exclusively',
    'home.milan' => 'Milan',
    'home.expansion' => 'With many plans to expand',
    'privacy-policy' => 'Privacy Policy',
    'team' => 'Team',
    'terms-and-conditions' => 'Terms & Conditions',
];
<file_sep>/app/Http/Controllers/Admin/ImageController.php
<?php

namespace App\Http\Controllers\Admin;

use App\ProductImage;
use Carbon\Carbon;
use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;

class ImageController extends Controller
{
    public function upload(Request $request)
    {
        $this->validate($request, [
            'file' => 'required|image|max:' . config('site.maxImageSize'),
        ]);

        $location = $this->generateFilePath($request->file('file'));

        //Avoid hash collision
        while(Storage::disk(config('filesystems.default'))->exists($location)) {
            $location = $this->generateFilePath($request->file('file'));
        }

        Storage::disk(config('filesystems.default'))->put($location, file_get_contents($request->file('file')->getRealPath()));

        $image = new ProductImage();
        $image->location = $location;
        $image->save();

        $this->garbageCollect();

        \Log::info('Uploading new image', ['user_id' => auth()->user()->id, 'ip' => $_SERVER['REMOTE_ADDR'], 'image_id' => $image->id]);

        return response()->json(['status' => 'success', 'id' => $image->id]);
    }

    public function destroy($id)
    {
        $image = ProductImage::findOrFail($id);

        $image->delete();

        \Log::info('Deleting image', ['user_id' => auth()->user()->id, 'ip' => $_SERVER['REMOTE_ADDR'], 'image_id' => $id]);

        return response()->json(['status' => 'success', 'id' => $id]);
    }

    /**
     * Delete orphaned images
     */
    public function garbageCollect()
    {
        $toBeDeleted = ProductImage::whereNull('product_id')
            ->where('created_at', '<', Carbon::now()->subHour(24)); //Orphaned images older than 24h deleted

        foreach($toBeDeleted->get() as $d) {
            Storage::disk(config('filesystems.default'))->delete($d['location']);
        }

        $toBeDeleted->delete();
    }

    public function generateFilePath($file)
    {
        $now = Carbon::now();

        $path = "product-images/" .
            "{$now->year}/{$now->month}/{$now->day}/" .
            str_random(32) . "." . $file->guessClientExtension();

        return $path;
    }
}
<file_sep>/app/Address.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Address extends Model
{
    protected $table = 'addresses';

    protected $fillable = [
        'isShippingPrimary', 'isBillingPrimary', 'name', 'line_1', 'line_2', 'city', 'postcode', 'state', 'phone', 'country_id', 'delivery_instructions', 'created_at', 'updated_at'];

    public function user() {
        return $this->belongsTo('App\User');
    }

    public function country() {
        return $this->belongsTo('App\Country');
    }

    public function scopeBilling($query) {
        return $query->where('isBillingPrimary', '=', 1)->first();
    }
}
<file_sep>/resources/lang/it/routes.php
<?php

return [
    'about-us' => 'riguardo-a-noi',
    'account' => [
        'index' => 'conto',
        'orders' => 'conto/ordini',
        'settings' => 'conto/impostazioni'
    ],
    'cart' => 'carrello',
    'checkout' => [
        'landing'      => 'cassa',
        'address'      => 'cassa/indirizzo',
        'payment'      => 'cassa/pagamento',
        'review'       => 'cassa/rivedere',
        'confirmation' => 'cassa/conferma',
        'gatewayReturn' => 'cassa/gatewayReturn',
    ],
    'contact' => 'contatto',
    'order' => [
        'show' => 'ordini/{id}',
    ],
    'privacy-policy' => 'informativa-sulla-privacy',
    'product' => 'prodotto/{slug}',
    'search' => 'ricerca/{query}',
    'shop' => [
        'index' => 'negozio',
        'category' => 'negozio/categoria/{slug}',
    ],
    'team' => 'team',
    'terms-and-conditions' => 'termini-e-condizioni',
];<file_sep>/resources/lang/it/postcode.php
<?php

return [
    'check.success' => 'Consegniamo a te!',
    'browse-store' => 'Visitare lo Store',
    'check.error.title' => 'Al momento non consegniamo alla vostra zona. Scusate!',
    'check.error.desc' => 'È ancora possibile di visitare il nostro store.',
    'api.error.postcode.invalid' => 'Il Cap non è valido.',
];<file_sep>/app/Zugy/PaymentGateway/Gateways/Cash.php
<?php

namespace Zugy\PaymentGateway\Gateways;

use App\Payment;

class Cash extends AbstractGateway
{
    protected $methodName = 'cash';

    public function addOrUpdateMethod() {
        $this->paymentMethod = $this->fetchPaymentMethod();

        if($this->paymentMethod === null) {
            $this->paymentMethod = $this->createPaymentMethod();
        }

        $this->setAsDefault(request('defaultPayment') !== null);

        return $this->paymentMethod;
    }

    public function charge($amount)
    {
        $payment = new Payment();

        $payment->status = 3; //Mark as "Payment on Delivery"

        $payment->amount = $amount;
        $payment->currency = 'EUR';
        $payment->method = $this->paymentMethod->method;

        return $payment;
    }

    public function getFormatted()
    {
        return [
            'method' => $this->methodName,
        ];
    }
}<file_sep>/resources/assets/js/events.js
/**
 * Add click listener for deleting items from mini cart
 */



$('#mini-cart-container').on('click', '.delete button', function() {
    $(this).prop('disabled', true);

    var rowId = $(this).data('row-id');

    cart.delete(rowId);

    $(this).prop('disabled', false);
}).on('pjax:start', function() { $(this).fadeOut(200); })
  .on('pjax:end',   function() { $(this).fadeIn(200); });<file_sep>/app/Zugy/DeliveryTime/Exceptions/PastDeliveryTimeException.php
<?php

namespace Zugy\DeliveryTime\Exceptions;


class PastDeliveryTimeException extends \Exception
{

}<file_sep>/app/Listeners/SendOrderConfirmationMail.php
<?php

namespace App\Listeners;

use App\Events\OrderWasPlaced;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendOrderConfirmationMail
{

    /**
     * Handle the event.
     *
     * @param  OrderWasPlaced  $event
     * @return void
     */
    public function handle(OrderWasPlaced $event)
    {
        \Activity::log([
            'contentId'   => $event->order->id,
            'contentType' => 'Order',
            'action'      => 'create',
        ]);

        try {
            \Mail::send('emails.order-confirmation', ['order' => $event->order], function($m) use($event) {
                $m->from(config('site.email.support'), config('site.name'));
                $m->to($event->order->email)->subject(trans('order.email.confirmation.subject', ['id' => $event->order->id]));
            });
        } catch (\Exception $e) {
            \Log::critical($e);
        }
    }
}
<file_sep>/app/Zugy/Facades/PaymentProcessor.php
<?php
namespace Zugy\Facades;

use Illuminate\Support\Facades\Facade;

class PaymentProcessor extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor() { return 'paymentProcessor'; }
}<file_sep>/tests/SearchTest.php
<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;

class SearchTest extends TestCase
{
    use WithoutMiddleware;

    public function testVodkaSearch()
    {

        $this->visit('en/search/vodka')->see('vodka');
    }
}<file_sep>/resources/lang/it/address.php
<?php

return [
    'empty' => 'Nessun indirizzo',
    'api.update.success' => 'Indirizzo aggiornato con successo',
    'api.destroy.success' => 'Indirizzo rimosso con successo',
    'api.error.404' => 'Indirizzo non esiste',
    'api.error.invalid' => 'Indirizzo non è valido',
];<file_sep>/resources/lang/it/footer.php
<?php

return [
    'email' => 'Email',
    'information' => 'Informazioni',
    'my-account' => 'Il mio account',
    'my-orders' => 'i miei ordini',
    'newsletter-subscribe-btn' => 'Abbonarsi',
    'rights-reserved' => 'Tutti i diritti sono riservati.',
    'stay-in-touch' => 'Resta in contatto',
    'support' => 'Supporto',
    'tax-code' => 'Partita IVA',
];<file_sep>/app/Product.php
<?php

namespace App;

use Dimsav\Translatable\Translatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;

class Product extends Model
{
    use SoftDeletes;
    use Translatable;
    use \AlgoliaSearch\Laravel\AlgoliaEloquentTrait;

    protected $table = 'products';

    protected $appends = ['thumbnail_url'];

    public static $perEnvironment = true; // Index name will be 'products_{\App::environment()}';

    public $algoliaSettings = [
        'attributesToIndex' => [
            'translation_en',
            'translation_it',
            'categories',
            'stock_quantity',
            'price',
            'weight',
        ],
        'customRanking' => [

        ],
    ];

    public function getAlgoliaRecord()
    {
        /**
         * Load the categories relation so that it's available
         *  in the laravel toArray method
         */
        $fields = [
            'stock_quantity' => $this->stock_quantity,
            'price' => (float) $this->price,
            'weight' => (float) $this->weight,
            'categories' => $this->categories,
        ];

        $fields['categories'] = [];
        foreach($this->categories as $c) {
            $fields['categories'][] = $c->id;
        }

        foreach($this->translations as $translation) {
            $fields['translation_' . $translation->locale] = [
                'title' => $translation->title,
                'slug' => $translation->slug,
                'description' => $translation->meta_description,
                'meta_description' => $translation->meta_description
            ];
        }

        return $fields;
    }

    public function getFinalIndexName(Model $model, $indexName)
    {
        $environment = \App::environment();
        if($environment == "testing") $environment = 'local';

        $env_suffix = property_exists($model, 'perEnvironment') && $model::$perEnvironment === true ? '_'. $environment() : '';

        return $indexName.$env_suffix;
    }

    public $translatedAttributes = ['slug', 'title', 'description', 'meta_description'];

    protected $fillable = ['slug', 'title', 'description', 'meta_description'];

    protected $with = ['tax_class'];

    /*
     * Relations
     */

    public function images()
    {
        return $this->hasMany('App\ProductImage');
    }

    public function tax_class() {
        return $this->belongsTo('App\TaxClass');
    }

    public function attributes()
    {
        return $this->belongsToMany('App\Attribute', 'products_to_attributes')->withPivot('value');
    }

    public function categories()
    {
        return $this->belongsToMany('App\Category', 'products_to_categories');
    }

    public function getUrl($language_code = null)
    {
        if(isset($this->attributes['url'])) {
            return $this->attributes['url'];
        }

        if($language_code === null) $language_code = LaravelLocalization::getCurrentLocale();

        $url = LaravelLocalization::getURLFromRouteNameTranslated($language_code, 'routes.product', ['slug' => $this->slug]);

        $this->attributes['url'] = $url;

        return $url;
    }

    /*
     * Actions
     */

    /**
     * Also delete all basket items containing this product
     * @throws \Exception
     */
    public function delete()
    {
        Basket::where('product_id', '=', $this->attributes['id'])->delete();

        parent::delete(); //Soft delete
    }

    /*
     * Accessors
     */

    /**
     * Fetch URL thumbnail for product
     * @return string
     */
    public function getThumbnailUrlAttribute() {
        if(isset($this->attributes['thumbnail_url'])) {
            return $this->attributes['thumbnail_url'];
        }

        $thumbnailId = $this->attributes['thumbnail_id'];

        if($thumbnailId !== null) {
            $url = $this->images->find($thumbnailId)->url;
        } else {
            $images = $this->images;

            if($images->count() === 0) {
                $url =  asset('/img/zugy-placeholder-image.png');
            } else {
                $url = $images->first()->url;
            }
        }

        $this->attributes['thumbnail_url'] = $url;

        return $url;
    }

    /**
     * Generate array to form breadcrumbs
     * @return array
     */
    public function getBreadcrumbsAttribute()
    {
        $category_id = $this->categories()->first()->id;

        $categories = Category::with('translations')->get()->keyBy('id');

        $node = $categories[$category_id];

        $parent_id = $node->parent_id;

        $breadcrumbs = [];

        while($parent_id !== null) {
            array_unshift($breadcrumbs, ['name' => $node->name, 'slug' => $node->slug]);
            $node = $categories[$parent_id];
            $parent_id = $node->parent_id;
        }

        array_unshift($breadcrumbs, ['name' => $node->name, 'slug' => $node->slug]);

        return $breadcrumbs;
    }

    /**
     * Calculate how many times product has been sold
     * @return mixed
     */
    public function getSalesAttribute()
    {
        return DB::table('order_items')
                   ->join('orders', 'order_items.order_id', '=', 'orders.id')
                   ->select('order_items.*', 'orders.order_status')
                   ->where('product_id', '=', $this->attributes['id'])
                   ->where('orders.order_status', '!=', 4) //Ignore cancelled orders
                   ->sum('order_items.quantity');
    }

    /**
     * Only include products that are in stock
     * @param $query
     */
    public function scopeInStock($query) {
        return $query->where('stock_quantity', '>', 0);
    }
}
<file_sep>/app/Zugy/Facades/ActivityLogParser.php
<?php

namespace Zugy\Facades;

use Illuminate\Support\Facades\Facade;

class ActivityLogParser extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor() { return 'activityLogParser'; }
}<file_sep>/app/Zugy/Charts/Charts.php
<?php

namespace Zugy\Charts;

use Carbon\Carbon;
use Illuminate\Support\Facades\DB;

class Charts
{
    public function chart30dRevenue() {

    }

    public function revenueByDay($start = null, $end = null) {
        if($start === null) {
            $start = Carbon::now()->subDays(30);
        }

        if($end === null) {
            $end = Carbon::now();
        }

        //Ignore cancelled orders
        $rows = DB::select("
            SELECT DATE(t.date) AS day, SUM(t.total) AS total
            FROM (
                SELECT o.id, o.created_at AS 'date', (SUM(i.final_price) + o.shipping_fee - IFNULL(o.coupon_deduction, 0)) AS 'total'
                FROM orders o
                JOIN order_items i
                ON o.id = i.order_id
                WHERE o.order_status != 4 AND o.created_at BETWEEN ? AND ?
                GROUP BY o.id
            ) t
            GROUP BY day
        ", [$start, $end]);

        //Fill in missing days with zero

        $interval = \DateInterval::createFromDateString('1 day');
        $period = new \DatePeriod($start, $interval, $end);

        $result = [
            'x' => [],
            'y' => []
        ];
        $i = 0;
        foreach($period as $day) {
            $date = $day->format('Y-m-d');

            if(isset($rows[$i]) && $rows[$i]->day == $date) {
                $result['x'][] = $date;
                $result['y'][] = $rows[$i]->total;
                $i++;
            } else {
                $result['x'][] = $date;
                $result['y'][] = 0;
            }
        }

        return $result;
    }
}<file_sep>/resources/lang/it/activityLogParser.php
<?php

return [
    'order.status-changed' => 'Status cambiato <b>:status</b>',
];<file_sep>/app/Http/Controllers/API/CartController.php
<?php

namespace App\Http\Controllers\Api;

use App\Product;
use Gloudemans\Shoppingcart\Exceptions\ShoppingcartInvalidRowIDException;
use Illuminate\Http\Response;
use Zugy\Facades\Cart;
use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;

class CartController extends Controller
{
    /**
     * List all items in cart
     *
     * @return Response
     */
    public function index()
    {
        return Cart::content();
    }

    /**
     * POST /api/v1/cart
     * Add a new item to cart
     *
     * @param  Request  $request
     * @return Response
     */
    public function store(Request $request)
    {
        $this->validate($request, [
            'id' => 'required|integer|exists:products',
            'qty' => 'required|integer|min:1',
            'options' => 'array',
        ]);

        $qty = (int) $request->input('qty');

        \Log::debug("Trying to add item to cart", ['productId' =>$request->input('id'),  'qty' => $qty]);

        $product = Product::find($request->input('id'));

        $cartItem = [
            'id' => (int) $request->input('id'),
            'name' => $product->title,
            'qty' => (int) $request->input('qty'),
            'price' => $product->price,
        ];

        //Search for product ID, if exists, update quantity
        if($request->has('options')) {
            $searchResult = Cart::search(['id' => (int) $request->input('id'), 'options' => $request->input('options')]);
            $cartItem['options'] = $request->input('options');
        } else {
            $searchResult = Cart::search(['id' => (int) $request->input('id')]);
        }

        if($searchResult === false) {
            if ($qty > $product->stock_quantity) {
                return response()->json([
                    'status' => 'failure', 'message' => trans('product.out-of-stock', ['quantity' => $product->stock_quantity])
                ], 422); //Unprocessable entity
            }

            Cart::associate('Product', 'App')->add($cartItem);
        } else {
            $rowId = $searchResult[0];

            $totalQuantity = (int) Cart::get($rowId)->qty + $qty;

            if($totalQuantity > $product->stock_quantity) {
                return response()->json([
                    'status' => 'failure', 'message' => trans('product.out-of-stock', ['quantity' => $product->stock_quantity])
                ], 422); //Unprocessable Entity
            }

            Cart::update($rowId, (int) $totalQuantity);

            $cart = $request->session()->get('cart.main');

            $request->session()->put('cart.main', $cart);
        }

        return response()->json(['status' => 'success', 'cart' => Cart::content()]);
    }

    /**
     * Update multiple items in the cart
     * PATCH /api/v1/cart
     * @param  Request  $request
     * @param  int  $id
     * @return Response
     */
    public function bulkUpdate(Request $request)
    {
        $this->validate($request, [
            'items.*.rowId' => 'required',
            'items.*.name' => 'max:255',
            'items.*.qty' => 'integer|min:0',
            'items.*.options' => 'array',
        ]);

        foreach($request->input('items') as $item) {
            $update = [];

            if(isset($item['name'])) $update['name'] =  $item['name'];
            if(isset($item['qty'])) {
                $update['qty'] = (int) $item['qty'];

                //Check stock
                $stock_quantity = Cart::get($item['rowId'])->product->stock_quantity;

                if($update['qty'] > $stock_quantity) {
                    return response()->json([
                        'status' => 'failure',
                        'message' => trans('product.out-of-stock') . ": " . Cart::get($item['rowId'])->product->title,
                        'description' => trans('product.stock-warning', ['count' => $stock_quantity])
                    ], 422); //Unprocessable Entity
                }
            }
            if(isset($item['options'])) $update['options'] =  $item['options'];

            \Log::debug('Updating cart', ['parameters' => $update]);

            try {
                Cart::update($item['rowId'], $update);
            } catch(ShoppingcartInvalidRowIDException $e) {
                return response()->json(['status' => 'failure', 'message' => 'The row ID was invalid'], 400);
            }
        }
        return response()->json(['status' => 'success', 'cart' => Cart::content()]);
    }

    /**
     * Remove an item from the cart.
     *
     * @param  int  $rowId
     * @return Response
     */
    public function destroy($rowId)
    {
        try {
            Cart::remove($rowId);
        } catch(ShoppingcartInvalidRowIDException $e) {
            return response()->json(['status' => 'failure', 'message' => 'The row ID was invalid'], 400);
        }


        return response()->json(['status' => 'success']);
    }
}
<file_sep>/database/factories/ModelFactory.php
<?php

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| Here you may define all of your model factories. Model factories give
| you a convenient way to create models for testing and seeding your
| database. Just tell the factory how a default model should look.
|
*/

$factory->define(App\User::class, function ($faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->email,
        'password' => <PASSWORD>(10),
        'remember_token' => str_random(10),
        'group_id' => 0
    ];
});

$factory->defineAs(App\User::class, 'admin', function($faker)  use ($factory) {
    $user = $factory->raw(App\User::class);

    return array_merge($user, ['group_id' => 2]);
});


$factory->define(App\Address::class, function($faker) {
    return [
        'name' => $faker->name,
        'line_1' => $faker->streetAddress,
        'city' => 'Milan',
        'postcode' => 20121,
        'country_id' => 380, //Italy
        'phone' => $faker->phoneNumber,
        'delivery_instructions' => $faker->text(),
    ];
});

$factory->define(App\Product::class, function($faker) {
    return [
        'stock_quantity' => $faker->numberBetween(1, 10000),
        'price' => $faker->randomNumber(2),
        'compare_price' => null,
        'weight' => $faker->randomFloat(2, 0, 100),
        'tax_class_id' => 1,
        'thumbnail_id' => null
    ];
});

$factory->define(App\ProductTranslation::class, function($faker) {
    return [
        'locale' => 'en',
        'slug' => $faker->slug,
        'title' => $faker->sentence(),
        'description' => $faker->sentence(),
        'meta_description' => $faker->sentence()
    ];
});<file_sep>/resources/lang/en/opening-times.php
<?php

return [
    'title-delivery-time' => 'Delivery Times',
    'closed' => 'We currently do not deliver.',
    'times' => 'Our delivery times are:',
    'delivery-time' => 'You can place your order now and select a delivery time for later during checkout.',
    'prompt-select' => 'Please select a delivery time slot during our opening times',
    'asap-closed-explanation' => 'If you select ASAP now, we will deliver your order when we open again at 1pm.'
];<file_sep>/app/Zugy/PaymentGateway/Exceptions/PaymentMethodUndefinedException.php
<?php

namespace Zugy\PaymentGateway\Exceptions;

class PaymentMethodUndefinedException extends \Exception
{

}<file_sep>/resources/lang/en/menu.php
<?php

return [
    'my-account' => 'My account',
    'admin-dashboard' => 'Admin dashboard',
    'sign-out' => 'Sign out',
    'sign-in' => 'Sign in',
    'your-account' => 'Your account',
    'account-settings' => 'Account settings',
    'cart' => 'Cart',
    'shop' => 'Shop',

    'admin.settings' => 'Settings',
    'admin.settings.tax' => 'Tax',
];<file_sep>/app/Policies/BasePolicy.php
<?php

namespace App\Policies;

use App\User;

class BasePolicy
{
    private $superAdminGroupId = 1;
    private $adminGroupId = 2;
    private $driverGroupId = 3;

    private $adminGroups;

    public function __construct()
    {
        $this->adminGroups = [$this->superAdminGroupId, $this->adminGroupId, $this->driverGroupId];
    }

    protected function isSuperAdmin(User $user)
    {
        return $user->group_id === $this->superAdminGroupId;
    }

    protected function isAdmin(User $user)
    {
        return in_array($user->group_id, $this->adminGroups);
    }

    protected function hasWriteAccess(User $user)
    {
        return in_array($user->group_id, [
            $this->superAdminGroupId,
            $this->adminGroupId
        ]);
    }
}<file_sep>/app/Zugy/Stock/Stock.php
<?php

namespace Zugy\Stock;

use App\Product;

use Zugy\Facades\Cart;

use App\Exceptions\EmptyCartException;
use App\Exceptions\OutOfStockException;

class Stock
{
    /**
     * Check if the cart of the current user has sufficient stock
     * @return boolean
     * @throws EmptyCartException
     * @throws OutOfStockException
     */
    public function checkCartStock() {
        $productIds = [];
        $cart = [];

        \Log::debug('Checking stock...');
        
        if(Cart::count() === 0) {
            throw new EmptyCartException();
        }

        foreach (Cart::content() as $item) {
            $productIds[] = $item->id;
            $cart[] = ['id' => $item->id, 'qty' => $item->qty];
        }

        $productStock = Product::findMany($productIds, ['id', 'stock_quantity'])->keyBy('id');

        $outOfStockProducts = [];
        foreach(Cart::content() as $product) {
            \Log::debug('Checking stock for', ['id' => $product->id, 'in_stock' => $productStock[$product->id]['stock_quantity'], 'in_cart' => $product['qty']]);
            if($productStock[$product->id]['stock_quantity'] < $product['qty']) {
                $outOfStockProducts[] = $product;
            }
        }

        if(count($outOfStockProducts) > 0) throw new OutOfStockException($outOfStockProducts);

        \Log::debug('Sufficient stock available');

        return true;
    }
}<file_sep>/tests/ProductTest.php
<?php

class ProductTest extends TestCase
{
    public function testProductShow()
    {
        $product = \App\Product::first();

        $this->visit($product->getUrl())
             ->see($product->title);

        //$this->visit('it/prodotto/absolut-vodka')->see('Absolut Vodka');
    }

    public function testAlcoholCategoryShow()
    {
        $this->visit('en/shop/category/alcohol')->see('Alcohol');
    }
}<file_sep>/app/Language.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
use Cache;

class Language extends Model
{
    static public function getLanguageId($language_code = null) {
        if($language_code == null) $language_code = LaravelLocalization::getCurrentLocale();

        $languages = \Cache::remember('languages', 60, function() {
            return Language::all();
        });

        return $languages->where('code', $language_code)->first()->id;
    }

    public function getCodeAttribute($value) {
        return strtolower($value);
    }
}
<file_sep>/app/Services/Order/PlaceOrder.php
<?php

namespace App\Services\Order;

use App\Exceptions\TooManyOrdersException;
use Illuminate\Support\Facades\Validator;
use Log;
use App\Events\OrderWasPlaced;
use App\Payment;
use App\User;
use App\Order;
use App\OrderItem;

use Carbon\Carbon;
use Zugy\DeliveryTime\Exceptions\ClosedException;
use Zugy\DeliveryTime\Exceptions\PastDeliveryTimeException;
use Zugy\Facades\Cart;
use Zugy\Facades\Stock;

use Zugy\Facades\Checkout;
use Zugy\Facades\DeliveryTime;
use Zugy\Facades\PaymentGateway;

class PlaceOrder
{
    protected $user;

    protected $deliveryAddress;
    protected $billingAddress;
    protected $paymentMethod;

    private $checkoutReviewUrl;

    public function __construct()
    {
        $this->checkoutReviewUrl = localize_url('routes.checkout.review');
    }

    public function handler(User $user) {
        $this->user = $user;

        $this->deliveryAddress = Checkout::getShippingAddress();
        $this->billingAddress = Checkout::getBillingAddress();
        $this->paymentMethod = Checkout::getPaymentMethod();

        Log::debug('Trying to place order', ['user' => $user]);

        if($this->deliveryAddress === null || $this->paymentMethod === null)
        {
            Log::debug('Missing details, redirecting.', [
                'delivery' => $this->deliveryAddress,
                'payment' => $this->paymentMethod
            ]);

            return redirect(localize_url('routes.checkout.landing'));
        }

        //Use delivery address as billing address if billing address is null
        if($this->billingAddress === null) {
            $this->billingAddress = $this->deliveryAddress;
        }

        //Prevent duplicate orders, have a cooldown of 30 seconds
        if($user->orders()->where('created_at', '>', Carbon::now()->subSeconds(30))->count() > 0) {
            Log::info('User tried to place too many orders in a short period of time', ['user' => $user]);
            throw new TooManyOrdersException();
        } 

        Stock::checkCartStock();

        //Load delivery time session if it not set
        if(empty(request('delivery_date')) && empty(request('delivery_time'))) {
            request()->merge(['delivery_date' => Checkout::getDeliveryDate(), 'delivery_time' => Checkout::getDeliveryTime()]);
        }

        $validator = Validator::make(request()->all(), [
            //Create validator with empty rules
        ]);

        $validator->sometimes('delivery_date', 'required|date', function($input) {
            return $input->delivery_date !== 'asap';
        });

        $validator->sometimes('delivery_time', 'required', function($input) {
            return $input->delivery_date !== 'asap';
        });

        if($validator->fails()) {
            return redirect(localize_url('routes.checkout.review'))->withErrors($validator)->withInput();
        }

        //Validate delivery time
        if(request('delivery_date') === 'asap') {
            if(!DeliveryTime::isOpen(Carbon::now())) {
                return redirect($this->checkoutReviewUrl)->withError(trans('opening-times.prompt-select'));
            }
        } else {
            $delivery_time = Carbon::parse(request('delivery_date') . " " . request('delivery_time'));

            try {
                DeliveryTime::isValidDeliveryTime($delivery_time);
            } catch (PastDeliveryTimeException $e) {
                return redirect($this->checkoutReviewUrl)->withErrors(['delivery_date' => trans('checkout.review.delivery-time.error.late')]);
            } catch (ClosedException $e) {
                return redirect($this->checkoutReviewUrl)->withErrors(['delivery_date' => trans('checkout.review.delivery-time.error.closed')]);
            }
        }

        //Set delivery time session variables
        Checkout::setDeliveryDate(request('delivery_date'));
        Checkout::setDeliveryTime(request('delivery_time'));

        $coupon = Checkout::getCoupon();

        if($coupon != null) {
            //Validate coupon
            Checkout::validateCoupon($coupon);
        }

        $payment = $this->processPayment();

        if(!$payment instanceof Payment) return $payment; //E.g. for PayPal, return redirect to gateway processor

        $order = $this->saveOrderToDB($payment);

        //Empty checkout session settings
        Checkout::forget();

        event(new OrderWasPlaced($order));

        return $order;
    }

    public function processPayment() {
        Log::info("Charging customer", ['total' => Cart::grandTotal()]);
        $payment = PaymentGateway::set($this->paymentMethod)->charge(Cart::grandTotal());

        return $payment;
    }

    public function saveOrderToDB(Payment $payment) {
        $order = new Order();
        $order->order_status = 0; //New order status
        $order->order_placed = Carbon::now();

        $order->email                   = $this->user->email;

        $order->delivery_name           = $this->deliveryAddress->name;
        $order->delivery_line_1         = $this->deliveryAddress->line_1;
        $order->delivery_line_2         = $this->deliveryAddress->line_2;
        $order->delivery_city           = $this->deliveryAddress->city;
        $order->delivery_postcode       = $this->deliveryAddress->postcode;
        $order->delivery_state          = $this->deliveryAddress->state;
        $order->delivery_phone          = $this->deliveryAddress->phone;
        $order->delivery_country_id     = $this->deliveryAddress->country_id;
        $order->delivery_instructions   = $this->deliveryAddress->delivery_instructions;

        $order->shipping_fee            = Cart::shipping();

        $coupon = Checkout::getCoupon();

        if($coupon != null) {
            $order->coupon_id        = $coupon->id;
            $order->coupon_deduction = Cart::couponDeduction();
        }

        $order->currency                = 'EUR';

        //Delivery time
        if(request('delivery_date') == 'asap') {
            $order->delivery_time = null;
        } else {
            $order->delivery_time = Carbon::parse(request('delivery_date') . " " . request('delivery_time'));
        }

        $order = $this->user->orders()->save($order);

        //Add payment to DB, save billing address in same table
        $payment->billing_name           = $this->billingAddress->name;
        $payment->billing_line_1         = $this->billingAddress->line_1;
        $payment->billing_line_2         = $this->billingAddress->line_2;
        $payment->billing_city           = $this->billingAddress->city;
        $payment->billing_postcode       = $this->billingAddress->postcode;
        $payment->billing_state          = $this->billingAddress->state;
        $payment->billing_phone          = $this->billingAddress->phone;
        $payment->billing_country_id     = $this->billingAddress->country_id;

        $order->payment()->save($payment);

        $items = [];
        foreach(Cart::content() as $item) {
            //Update stock quantity
            $item->product->decrement('stock_quantity', $item->qty);

            $orderItem = new OrderItem();
            $orderItem->product_id = $item->id;
            $orderItem->quantity = $item->qty;
            $orderItem->price = $item->price;
            $orderItem->final_price = $item->subtotal;
            $orderItem->tax = $item->product->tax_class->tax_rate;

            $items[] = $orderItem;
        }

        $order->items()->saveMany($items);

        //Increment coupon uses
        if($coupon != null) {
            $coupon->increment('uses');
        }

        return $order;
    }
}<file_sep>/app/Zugy/Helpers/Maps/MapsServiceProvider.php
<?php

namespace Zugy\Helpers\Maps;

use Illuminate\Support\ServiceProvider;

class MapsServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind('maps', function($app) {
            return new Maps();
        });
    }
}
<file_sep>/tests/PlaceOrderTest.php
<?php

use Carbon\Carbon;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class PlaceOrderTest extends TestCase
{
    use DatabaseTransactions;

    private $user;

    protected function setUp()
    {
        parent::setUp();

        $this->user = factory(App\User::class)->create();
        $this->actingAs($this->user);
    }

    /**
     * Test an order that should be placed without issues.
     * Should fire event OrderWasPlaced
     */
    public function testPlaceOrderPositive()
    {
        $this->expectsEvents(App\Events\OrderWasPlaced::class);

        $product = factory(App\Product::class)->create([
            'stock_quantity' => 10,
        ]);
        $product->translations()->save(factory(App\ProductTranslation::class)->make());

        $quantity = 5;

        //Add item to cart
        $this->json('POST', 'api/v1/cart', [
            'id' => $product->id,
            'qty' => $quantity,
        ])->seeJson(['status' => 'success']);

        $address = factory(App\Address::class)->make();

        $this->user->addresses()->save($address);

        Checkout::setShippingAddress($address);
        Checkout::setBillingAddress($address);

        //Set payment method
        $this->post('en/checkout/payment', [
            'method' => 'cash',
        ]);

        $this->post('/en/checkout/review', [
            'delivery_date' => Carbon::now()->addDay(1)->toDateString(),
            'delivery_time' => '18:00'
        ]);

        //Assert that product billing is calculated correctly
        $this->seeInDatabase('order_payments', ['amount' => $quantity * $product->price]);
    }

    /**
     * Should allow order to be placed with ASAP selected when store is open, need to mock time
     */
    public function testPlaceOrderPositiveAsap() {

    }

    /**
     * Should NOT allow order to be placed with ASAP selected when store is closed, need to mock time
     */
    public function testPlaceOrderNegativeAsap() {

    }

    /**
     * Test shipping fee is added correctly for orders under free delivery limit
     */
    public function testPlaceOrderShippingFeePositive()
    {
        $this->expectsEvents(App\Events\OrderWasPlaced::class);

        $product = factory(App\Product::class)->create([
            'stock_quantity' => 10,
            'price' => 5.10
        ]);
        $product->translations()->save(factory(App\ProductTranslation::class)->make());

        \Log::debug('product', [$product]);

        $quantity = 2;

        //Add item to cart
        $this->json('POST', 'api/v1/cart', [
            'id' => $product->id,
            'qty' => $quantity,
        ])->seeJson(['status' => 'success']);

        $address = factory(App\Address::class)->make();

        $this->user->addresses()->save($address);

        Checkout::setShippingAddress($address);
        Checkout::setBillingAddress($address);

        //Set payment method
        $this->post('en/checkout/payment', [
            'method' => 'cash',
        ]);

        $this->post('/en/checkout/review', [
            'delivery_date' => Carbon::now()->addDay(1)->toDateString(),
            'delivery_time' => '18:00'
        ]);

        //Assert that product billing is calculated correctly with shipping fee
        $this->seeInDatabase('order_payments', ['amount' => 10.20 + config('site.shippingFee')]);
    }

    public function testPlaceOrderOutOfHoursNegative()
    {
        $product = factory(App\Product::class)->create([
            'stock_quantity' => 10,
        ]);
        $product->translations()->save(factory(App\ProductTranslation::class)->make());

        $quantity = 5;

        //Add item to cart
        $this->json('POST', 'api/v1/cart', [
            'id' => $product->id,
            'qty' => $quantity,
        ])->seeJson(['status' => 'success']);

        $address = factory(App\Address::class)->make();

        $this->user->addresses()->save($address);

        Checkout::setShippingAddress($address);
        Checkout::setBillingAddress($address);

        //Set payment method
        $this->post('en/checkout/payment', [
            'method' => 'cash',
        ]);

        $this->post('/en/checkout/review', [
            'delivery_date' => Carbon::now()->addDay(1)->toDateString(),
            'delivery_time' => '06:00' // Out of hours, should fail here
        ], ['HTTP_REFERER' => url('en/checkout/review')])
            ->assertRedirectedTo('en/checkout/review')
            ->followRedirects()
            ->see(trans('checkout.review.delivery-time.error.closed'));
    }

    /**
     * This test should fail as "asap" delivery time orders cannot be placed when the store is closed
     */
    public function testPlaceAsapOrderOutOfHoursNegative()
    {
        $this->doesntExpectEvents(App\Events\OrderWasPlaced::class);

        $product = factory(App\Product::class)->create([
            'stock_quantity' => 10,
        ]);
        $product->translations()->save(factory(App\ProductTranslation::class)->make());

        \Log::debug('product', [$product]);

        $quantity = 5;

        //Add item to cart
        $this->json('POST', 'api/v1/cart', [
            'id' => $product->id,
            'qty' => $quantity,
        ])->seeJson(['status' => 'success']);

        $address = factory(App\Address::class)->make();

        $this->user->addresses()->save($address);

        Checkout::setShippingAddress($address);
        Checkout::setBillingAddress($address);

        //Set payment method
        $this->post('en/checkout/payment', [
            'method' => 'cash',
        ]);

        //Set mock time that is when the store is closed
        $outOfHourTime = Carbon::now()->tomorrow()->hour(4);
        Carbon::setTestNow($outOfHourTime);

        $this->post('/en/checkout/review', [
            'delivery_date' => 'asap'
        ])->followRedirects()
            ->see(trans('opening-times.prompt-select')); //Should see the out of hours error message
    }

    public function testOrderOutOfStockNegative()
    {
        $product = factory(App\Product::class)->create([
            'stock_quantity' => 10,
        ]);
        $product->translations()->save(factory(App\ProductTranslation::class)->make());

        $quantity = 5;

        //Add item to cart
        $this->json('POST', 'api/v1/cart', [
            'id' => $product->id,
            'qty' => $quantity,
        ])->seeJson(['status' => 'success']);

        $address = factory(App\Address::class)->make();

        $this->user->addresses()->save($address);

        Checkout::setShippingAddress($address);
        Checkout::setBillingAddress($address);

        //Set payment method
        $this->post('en/checkout/payment', [
            'method' => 'cash',
        ]);

        $product->stock_quantity = 3; //Another customer bought 1, only 3 left
        $product->save();

        $this->post('/en/checkout/review', [
            'delivery_date' => Carbon::now()->addDay(1)->toDateString(),
            'delivery_time' => '06:00' // Out of hours, should fail here
        ], ['HTTP_REFERER' => url('en/checkout/review')])
            ->assertRedirectedTo('en/checkout/review')
            ->followRedirects()
            ->see('out of stock');
    }
}
<file_sep>/app/Zugy/Repos/Category/CategoryRepository.php
<?php

namespace Zugy\Repos\Category;


interface CategoryRepository
{
    /**
     * Return all child category IDs and itself
     * @param $categoryId
     * @return mixed
     */
    public function children($categoryId);

    /**
     * Return the category matching the slug
     * @param $slug
     * @return mixed
     */
    public function getBySlug($slug);
}<file_sep>/tests/PagesTest.php
<?php

class PagesTest extends TestCase
{

    public function testHomepage()
    {
        $this->visit('en');
    }

    public function testCart()
    {
        $this->visit('en/cart')->see('Shopping Cart');
    }

    public function testPrivacyPolicy()
    {
        $this->visit('en/privacy-policy')->see('Privacy Policy');
    }

    public function testTermsConditions()
    {
        $this->visit('en/terms-and-conditions')->see('Terms and Conditions');
    }
}<file_sep>/resources/lang/it/buttons.php
<?php

return [
    'search.prompt' => 'digita e cerca',
    'proceed' => 'Procedi',
    'add-cart' => 'Aggiungi nel carrello',
    'shop-now' => 'Acquistare ora',
    'update-cart' => 'Aggiorna carrello',
    'continue-shopping' => 'Continua a fare acquisti',
    'proceed-to-checkout' => 'Procedere al checkout',
    'view-cart' => 'Il tuo carrello',
    'edit' => 'Modifica',
    'back-to-shop' => 'Torna al negozio',
    'add-new-address' => 'Aggiungi un nuovo indirizzo',
    'change' => 'Cambiamento',
    'login' => 'Accesso',
    'more-info' => 'Ulteriori informazioni',
    'got-it' => 'Fatto!',
    'toggle-navigation' => 'navigazione Toggle',
    'view' => 'Vere',
    'mark-as' => 'Segna come ...',
    'update' => 'Aggiornare',
    'show-google-maps' => 'Mostra su Google Maps',
    'save' => 'Salvare',
    'return-home' => 'Ritorna alla home page',
    'apply' => 'Applicare',
    'cancel' => 'Annulla',
    'delete' => 'Cancellare',
    'success' => 'Successo',
];<file_sep>/app/Policies/AuthenticationPolicy.php
<?php

namespace App\Policies;

use Illuminate\Auth\Access\HandlesAuthorization;
use App\User;

class AuthenticationPolicy extends BasePolicy
{
    use HandlesAuthorization;

    /**
     * Create a new policy instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Is user permitted to log into other users' accounts?
     * @param User $user
     * @param User $toBeSignedInAsUser
     * @return bool
     */
    public function signInAsUser(User $user, User $toBeSignedInAsUser) {
        return $this->isSuperAdmin($user);
    }
}
<file_sep>/app/Http/Controllers/API/ShopStatisticsController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;

use App\Order;

class ShopStatisticsController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('admin');
    }

    /**
     * GET api/v1/shop/statistics
     */
    public function getStatistics() {
        return [
            'status' => "success",
            "payload" => [
                'revenue' => [
                    'yesterday' => Order::uncancelled()->where('order_placed', '>', \Carbon::yesterday())->where('order_placed', '<', \Carbon::today())->get()->sum('grand_total'),
                    'thisMonth'  => Order::uncancelled()->where('order_placed', '>', new \Carbon('first day of this month'))->where('order_placed', '<', \Carbon::today())->get()->sum('grand_total')
                ],
                'orders' => [
                    'incomplete' => Order::incomplete()->count()
                ]
            ]
        ];
    }
}