<repo_name>BinaryKitten/BBMembershipSystem<file_sep>/app/Services/MemberSubscriptionCharges.php
<?php namespace BB\Services;

use BB\Entities\User;
use BB\Events\MemberBalanceChanged;
use BB\Events\SubscriptionPayment;
use BB\Helpers\GoCardlessHelper;
use BB\Repo\PaymentRepository;
use BB\Repo\SubscriptionChargeRepository;
use BB\Repo\UserRepository;
use Carbon\Carbon;

class MemberSubscriptionCharges
{

    /**
     * @var UserRepository
     */
    private $userRepository;
    /**
     * @var SubscriptionChargeRepository
     */
    private $subscriptionChargeRepository;
    /**
     * @var GoCardlessHelper
     */
    private $goCardless;
    /**
     * @var PaymentRepository
     */
    private $paymentRepository;

    function __construct(UserRepository $userRepository, SubscriptionChargeRepository $subscriptionChargeRepository, GoCardlessHelper $goCardless, PaymentRepository $paymentRepository)
    {
        $this->userRepository = $userRepository;
        $this->subscriptionChargeRepository = $subscriptionChargeRepository;
        $this->goCardless = $goCardless;
        $this->paymentRepository = $paymentRepository;
    }

    /**
     * Create the sub charge for each member, only do this for members with dates matching the supplied date
     *
     * @param Carbon $targetDate
     */
    public function createSubscriptionCharges($targetDate)
    {
        $users = $this->userRepository->getBillableActive();
        foreach ($users as $user) {
            if (($user->payment_day == $targetDate->day) && ( ! $this->subscriptionChargeRepository->chargeExists($user->id, $targetDate))) {
                $this->subscriptionChargeRepository->createCharge($user->id, $targetDate);
            }
        }
    }

    /**
     * Locate all charges that are for today or the past and mark them as due
     */
    public function makeChargesDue()
    {
        $subCharges = $this->subscriptionChargeRepository->getPending();
        foreach ($subCharges as $charge) {
            if ($charge->charge_date->isToday() || $charge->charge_date->isPast()) {
                $this->subscriptionChargeRepository->setDue($charge->id);
            }
        }
    }

    /**
     * Bill members based on the sub charges that are due
     */
    public function billMembers()
    {
        $subCharges = $this->subscriptionChargeRepository->getDue();

        //Check each of the due charges, if they have previous attempted payments ignore them
        // we don't want to retry failed payments as for DD's this will generate bank charges
        $subCharges->reject(function ($charge) {
            return $this->paymentRepository->getPaymentsByReference($charge->id)->count() > 0;
        });

        //Filter the list into two gocardless and balance subscriptions
        $goCardlessUsers = $subCharges->filter(function ($charge) {
            return $charge->user->payment_method == 'gocardless-variable';
        });

        $balanceUsers = $subCharges->filter(function ($charge) {
            return $charge->user->payment_method == 'balance';
        });


        //Charge the balance users
        foreach ($balanceUsers as $charge) {
            if (($charge->user->monthly_subscription * 100) > $charge->user->cash_balance) {
                //user doesn't have enough money

                //If they have a secondary payment method of gocardless try that
                if ($charge->user->secondary_payment_method == 'gocardless-variable') {

                    //Add the charge to the gocardless list for processing
                    $goCardlessUsers->push($charge);

                    event(new SubscriptionPayment\InsufficientFundsTryingDirectDebit($charge->user->id, $charge->id));
                } else {
                    event(new SubscriptionPayment\FailedInsufficientFunds($charge->user->id, $charge->id));
                }
                continue;
            }

            $this->paymentRepository->recordSubscriptionPayment($charge->user->id, 'balance', '', $charge->user->monthly_subscription, 'paid', 0, $charge->id);

            event(new MemberBalanceChanged($charge->user->id));
        }


        //Charge the gocardless users
        foreach ($goCardlessUsers as $charge) {
            $bill = $this->goCardless->newBill($charge->user->subscription_id, $charge->user->monthly_subscription, $this->goCardless->getNameFromReason('subscription'));
            if ($bill) {
                $this->paymentRepository->recordSubscriptionPayment($charge->user->id, 'gocardless-variable', $bill->id, $bill->amount, $bill->status, $bill->gocardless_fees, $charge->id);
            }
        };

    }


    /**
     * Get a users latest sub payment
     * @param $userId
     * @return bool
     */
    public function lastUserChargeExpires($userId)
    {
        $charge = User::where('user_id', $userId)->where('status', ['processing', 'paid'])->orderBy('charge_date', 'DESC')->first();
        if ($charge) {
            return $charge->charge_date->addMonth();
        }
        return false;
    }
}<file_sep>/app/Entities/Settings.php
<?php namespace BB\Entities;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;

/**
 * Class Activity
 *
 * @property integer $id
 * @property integer $key_fob_id
 * @property integer $user_id
 * @property User    $user
 * @property string  $service
 * @property string  $response
 * @property bool    $delayed
 * @property Carbon  $created_at
 * @package BB\Entities
 */
class Settings extends Model
{

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

    protected $primaryKey = 'key';

    public $incrementing = false;

    public $timestamps = false;

    protected $fillable = ['key', 'value'];

    public static function put($key, $value)
    {
        self::create(['key' => $key, 'value' => $value]);
    }

    public static function get($key)
    {
        $setting = self::findOrFail($key);
        return $setting->value;
    }


}
<file_sep>/app/Http/Controllers/GoCardlessWebhookController.php
<?php namespace BB\Http\Controllers;


use BB\Entities\Payment;
use BB\Entities\User;
use BB\Helpers\GoCardlessHelper;
use BB\Repo\PaymentRepository;
use BB\Repo\SubscriptionChargeRepository;
use \Carbon\Carbon;

class GoCardlessWebhookController extends Controller
{

    /**
     * @var PaymentRepository
     */
    private $paymentRepository;
    /**
     * @var SubscriptionChargeRepository
     */
    private $subscriptionChargeRepository;
    /**
     * @var GoCardlessHelper
     */
    private $goCardless;

    public function __construct(GoCardlessHelper $goCardless, PaymentRepository $paymentRepository, SubscriptionChargeRepository $subscriptionChargeRepository)
    {
        $this->goCardless = $goCardless;
        $this->paymentRepository = $paymentRepository;
        $this->subscriptionChargeRepository = $subscriptionChargeRepository;
    }

    public function receive()
    {
        $request = \Request::instance();

        if ( ! $this->goCardless->validateWebhook($request->getContent())) {
            return \Response::make('', 403);
        }

        $parser = new \BB\Services\Payment\GoCardlessWebhookParser();
        $parser->parseResponse($request->getContent());

        switch ($parser->getResourceType()) {
            case 'bill':

                switch ($parser->getAction()) {
                    case 'created':

                        $this->processNewBills($parser->getBills());

                        break;
                    case 'paid':

                        $this->processPaidBills($parser->getBills());

                        break;
                    default:

                        $this->processBills($parser->getAction(), $parser->getBills());
                }

                break;
            case 'pre_authorization':

                    $this->processPreAuths($parser->getAction(), $parser->getPreAuthList());

                break;
            case 'subscription':

                    $this->processSubscriptions($parser->getSubscriptions());

                break;
        }

        return \Response::make('Success', 200);
    }


    /**
     * A Bill has been created, these will always start within the system except for subscription payments
     *
     * @param array $bills
     */
    private function processNewBills(array $bills)
    {
        //We have new bills/payment
        foreach ($bills as $bill) {
            //Ignore non subscription payment creations
            if ($bill['source_type'] != 'subscription') {
                continue;
            }
            try {

                //Locate the user through their subscription id
                $user = User::where('payment_method', 'gocardless')->where('subscription_id', $bill['source_id'])->first();

                if ( ! $user) {
                    \Log::warning("GoCardless new sub payment notification for unmatched user. Bill ID: " . $bill['id']);

                    continue;
                }

                $ref = null;

                $this->paymentRepository->recordSubscriptionPayment($user->id, 'gocardless', $bill['id'], $bill['amount'], $bill['status'], ($bill['amount'] - $bill['amount_minus_fees']), $ref);


            } catch (\Exception $e) {
                \Log::error($e);
            }
        }
    }


    private function processPaidBills(array $bills)
    {
        //When a bill is paid update the status on the local record and the connected sub charge (if there is one)

        foreach ($bills as $bill) {

            $existingPayment = $this->paymentRepository->getPaymentBySourceId($bill['id']);
            if ($existingPayment) {

                if ($bill['paid_at']) {
                    $paymentDate = new Carbon($bill['paid_at']);
                } else {
                    $paymentDate = new Carbon();
                }

                $this->paymentRepository->markPaymentPaid($existingPayment->id, $paymentDate);

            } else {
                \Log::info("GoCardless Webhook received for unknown payment: " . $bill['id']);
            }
        }
    }

    /**
     * @param string $action
     */
    private function processBills($action, array $bills)
    {
        foreach ($bills as $bill) {
            $existingPayment = $this->paymentRepository->getPaymentBySourceId($bill['id']);
            if ($existingPayment) {
                if (($bill['status'] == 'failed') || ($bill['status'] == 'cancelled')) {
                    //Payment failed or cancelled - either way we don't have the money!
                    //We need to retrieve the payment from the user somehow but don't want to cancel the subscription.
                    //$this->handleFailedCancelledBill($existingPayment);

                    $this->paymentRepository->recordPaymentFailure($existingPayment->id, $bill['status']);

                } elseif (($bill['status'] == 'pending') && ($action == 'retried')) {
                    //Failed payment is being retried
                    $subCharge = $this->subscriptionChargeRepository->getById($existingPayment->reference);
                    if ($subCharge) {
                        if ($subCharge->amount == $bill['amount']) {
                            $this->subscriptionChargeRepository->markChargeAsProcessing($subCharge->id);
                        } else {
                            //@TODO: Handle partial payments
                            \Log::warning("Sub charge handling - gocardless partial payment");
                        }
                    }
                } elseif ($bill['status'] == 'refunded') {
                    //Payment refunded
                    //Update the payment record and possible the user record
                } elseif ($bill['status'] == 'withdrawn') {
                    //Money taken out - not our concern
                }
            } else {
                \Log::info("GoCardless Webhook received for unknown payment: " . $bill['id']);
            }
        }

    }

    /**
     * @param string $action
     */
    private function processPreAuths($action, $preAuthList)
    {
        //Preauths are handled at creation
        foreach ($preAuthList as $preAuth) {
            if ($preAuth['status'] == 'cancelled') {
                $user = User::where('payment_method', 'gocardless-variable')->where('subscription_id', $preAuth['id'])->first();
                if ($user) {
                    $user->cancelSubscription();
                }
            }
        }
    }


    private function processSubscriptions($subscriptions)
    {
        foreach ($subscriptions as $sub) {
            //Setup messages aren't used as we deal with them directly.
            if ($sub['status'] == 'cancelled') {
                //Make sure our local record is correct
                $user = User::where('payment_method', 'gocardless')->where('subscription_id', $sub['id'])->first();
                if ($user) {
                    $user->cancelSubscription();
                }
            }
        }
    }

    /**
     * The bill has been cancelled or failed, update the user records to compensate
     *
     * @param $existingPayment
     */
    private function handleFailedCancelledBill(Payment $existingPayment)
    {
        if ($existingPayment->reason == 'subscription') {
            //If the payment is a subscription payment then we need to take action and warn the user
            $user         = $existingPayment->user()->first();
            $user->status = 'suspended';

            //Rollback the users subscription expiry date or set it to today
            $expiryDate = \BB\Helpers\MembershipPayments::lastUserPaymentExpires($user->id);
            if ($expiryDate) {
                $user->subscription_expires = $expiryDate;
            } else {
                $user->subscription_expires = new Carbon();
            }

            $user->save();

            //Update the subscription charge to reflect the payment failure
            $subCharge = $this->subscriptionChargeRepository->getById($existingPayment->reference);
            if ($subCharge) {
                $this->subscriptionChargeRepository->paymentFailed($subCharge->id);
            }

        } elseif ($existingPayment->reason == 'induction') {
            //We still need to collect the payment from the user
        } elseif ($existingPayment->reason == 'box-deposit') {

        } elseif ($existingPayment->reason == 'key-deposit') {

        }
    }

}<file_sep>/app/Helpers/GoCardlessHelper.php
<?php namespace BB\Helpers;

class GoCardlessHelper
{

    private $account_details;

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

    public function setup()
    {
        $this->account_details = array(
            'app_id'        => env('GOCARDLESS_APP_ID', ''),
            'app_secret'    => env('GOCARDLESS_APP_SECRET', ''),
            'merchant_id'   => env('GOCARDLESS_MERCHANT_ID', ''),
            'access_token'  => env('GOCARDLESS_ACCESS_TOKEN', ''),
        );

        // Initialize GoCardless
        if (\App::environment() == 'production') {
            \GoCardless::$environment = 'production';
        }
        \GoCardless::set_account_details($this->account_details);
    }

    public function newPreAuthUrl($paymentDetails)
    {
        $baseDetails = array(
            'max_amount'        => 250,
            'interval_length'   => 1,
            'interval_unit'     => 'month',
        );
        $paymentDetails = array_merge($baseDetails, $paymentDetails);
        return \GoCardless::new_pre_authorization_url($paymentDetails);
    }

    public function newSubUrl($payment_details)
    {
        return \GoCardless::new_subscription_url($payment_details);
    }

    public function confirmResource($confirm_params)
    {
        return \GoCardless::confirm_resource($confirm_params);
    }

    public function cancelSubscription($id)
    {
        return \GoCardless_Subscription::find($id)->cancel();
    }

    /**
     * Accept the raw json response and validate it
     * @param $webhook
     * @return bool
     */
    public function validateWebhook($webhook)
    {
        $webhook_array = json_decode($webhook, true);
        return \GoCardless::validate_webhook($webhook_array['payload']);
    }

    public function newBillUrl($payment_details)
    {
        return \GoCardless::new_bill_url($payment_details);
    }

    public function getSubscriptionFirstBill($id)
    {
        $bills = \GoCardless_Merchant::find($this->account_details['merchant_id'])->bills(array('source_id' => $id));
        if (count($bills) > 0) {
            return $bills[0];
        }
        return false;
    }

    /**
     * Create a new payment against a preauth
     * @param             $preauthId
     * @param             $amount
     * @param null|string $name
     * @param null|string $description
     * @return bool|mixed
     */
    public function newBill($preauthId, $amount, $name = null, $description = null)
    {
        try {
            $preAuth = \GoCardless_PreAuthorization::find($preauthId);
            $details = [
                'amount'      => $amount,
                'name'        => $name,
                'description' => $description,
            ];
            return $preAuth->create_bill($details);
        } catch (\GoCardless_ApiException $e) {
            \Log::error($e);
            return false;
        }
    }

    public function cancelPreAuth($preauthId)
    {
        try {
            $preAuth = \GoCardless_PreAuthorization::find($preauthId);
            $preAuthStatus = $preAuth->cancel();
            return ($preAuthStatus->status == 'cancelled');
        } catch (\GoCardless_ApiException $e) {
            \Log::error($e);
            return false;
        }
    }

    /**
     * @param string $reason
     * @return null|string
     */
    public function getNameFromReason($reason)
    {
        switch ($reason) {
            case 'subscription':
                return 'Monthly subscription';
            case 'balance':
                return 'Balance top up';
            case 'equipment-fee':
                return 'Equipment access fee';
            case 'door-key':
                return 'Door key';
        }

        return null;
    }
} <file_sep>/app/Http/Controllers/PaymentController.php
<?php namespace BB\Http\Controllers;

use BB\Entities\Induction;
use BB\Entities\Payment;
use BB\Entities\User;
use BB\Exceptions\NotImplementedException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;

class PaymentController extends Controller
{


    /**
     *
     * @TODO: Workout exactly what this is used for - I think most of the functionality has been moved elsewhere
     *
     */


    /**
     * @var \BB\Repo\EquipmentRepository
     */
    private $equipmentRepository;
    /**
     * @var \BB\Repo\PaymentRepository
     */
    private $paymentRepository;
    /**
     * @var \BB\Repo\UserRepository
     */
    private $userRepository;
    /**
     * @var \BB\Repo\SubscriptionChargeRepository
     */
    private $subscriptionChargeRepository;

    function __construct(
        \BB\Helpers\GoCardlessHelper $goCardless,
        \BB\Repo\EquipmentRepository $equipmentRepository,
        \BB\Repo\PaymentRepository $paymentRepository,
        \BB\Repo\UserRepository $userRepository,
        \BB\Repo\SubscriptionChargeRepository $subscriptionChargeRepository
    ) {
        $this->goCardless                   = $goCardless;
        $this->equipmentRepository          = $equipmentRepository;
        $this->paymentRepository            = $paymentRepository;
        $this->userRepository               = $userRepository;
        $this->subscriptionChargeRepository = $subscriptionChargeRepository;

        $this->middleware('role:member', array('only' => ['create', 'destroy']));

    }


    public function index()
    {
        $sortBy       = \Request::get('sortBy', 'created_at');
        $direction    = \Request::get('direction', 'desc');
        $dateFilter   = \Request::get('date_filter', '');
        $memberFilter = \Request::get('member_filter', '');
        $reasonFilter = \Request::get('reason_filter', '');
        $this->paymentRepository->setPerPage(50);

        if ($dateFilter) {
            $startDate = \Carbon\Carbon::createFromFormat('Y-m-d', $dateFilter)->setTime(0, 0, 0);
            $this->paymentRepository->dateFilter($startDate, $startDate->copy()->addMonth());
        }

        if ($memberFilter) {
            $this->paymentRepository->memberFilter($memberFilter);
        }

        if ($reasonFilter) {
            $this->paymentRepository->reasonFilter($reasonFilter);
        }

        $payments = $this->paymentRepository->getPaginated(compact('sortBy', 'direction'));

        $paymentTotal = $this->paymentRepository->getTotalAmount();

        $dateRangeEarliest = \Carbon\Carbon::create(2009, 07, 01);
        $dateRangeStart    = \Carbon\Carbon::now();
        $dateRange         = [];
        while ($dateRangeStart->gt($dateRangeEarliest)) {
            $dateRange[$dateRangeStart->toDateString()] = $dateRangeStart->format('F Y');
            $dateRangeStart->subMonth();
        }

        $memberList = $this->userRepository->getAllAsDropdown();

        $reasonList = [
            'subscription'  => 'Subscription',
            'induction'     => 'Equipment Access Fee',
            'balance'       => 'Balance',
            'door-key'      => 'Key Deposit',
            'storage-box'   => 'Storage Box Deposit',
            'equipment-fee' => 'Equipment Costs'
        ];

        return \View::make('payments.index')->with('payments', $payments)->with('dateRange', $dateRange)
            ->with('memberList', $memberList)->with('reasonList', $reasonList)->with('paymentTotal', $paymentTotal);
    }


    /**
     * Start the creation of a new gocardless payment
     *   Details get posted into this method and the redirected to gocardless
     *
     * @depreciated
     * @param $userId
     * @throws \BB\Exceptions\AuthenticationException
     * @throws \BB\Exceptions\FormValidationException
     * @throws \BB\Exceptions\NotImplementedException
     */
    public function create($userId)
    {
        $user = User::findWithPermission($userId);
        if (\Input::get('source') == 'gocardless') {
            $reason = \Input::get('reason');
            if ($reason == 'subscription') {
                //must go via the gocardless payment controller
                throw new \BB\Exceptions\NotImplementedException('Attempted GoCardless subscription payment');
            } elseif ($reason == 'induction') {
                //Payments must go via the balance
                throw new \BB\Exceptions\NotImplementedException('Attempted GoCardless induction payment');
            } elseif ($reason == 'door-key') {
                //Payments must go via the balance
                throw new \BB\Exceptions\NotImplementedException('Attempted GoCardless door payment');
            } elseif ($reason == 'storage-box') {
                //Payments must go via the balance
                throw new \BB\Exceptions\NotImplementedException('Attempted GoCardless storage box payment');
            } elseif ($reason == 'balance') {
                $amount = \Input::get('amount') * 1; //convert the users amount into a number
                if ( ! is_numeric($amount)) {
                    $exceptionErrors = new \Illuminate\Support\MessageBag(['amount' => 'Invalid amount']);
                    throw new \BB\Exceptions\FormValidationException('Not a valid amount', $exceptionErrors);
                }
                $name        = strtoupper('BBBALANCE' . $user->id);
                $description = 'BB Credit Payment';
                $ref         = null;
            } else {
                throw new \BB\Exceptions\NotImplementedException();
            }
            $payment_details = array(
                'amount'       => $amount,
                'name'         => $name,
                'description'  => $description,
                'redirect_uri' => route('account.payment.confirm-payment', [$user->id]),
                'user'         => [
                    'first_name'       => $user->given_name,
                    'last_name'        => $user->family_name,
                    'billing_address1' => $user->address_line_1,
                    'billing_address2' => $user->address_line_2,
                    'billing_town'     => $user->address_line_3,
                    'billing_postcode' => $user->address_postcode,
                    'country_code'     => 'GB'
                ],
                'state'        => $reason . ':' . $ref
            );

            return \Redirect::to($this->goCardless->newBillUrl($payment_details));
        } else {
            //Unsupported for now
            // perhaps manual payments or maybe they should go straight to store
            throw new \BB\Exceptions\NotImplementedException();
        }
    }


    /**
     * Confirm a gocardless payment and create a payment record
     *
     * @depreciated
     * @param $userId
     * @return Illuminate\Http\RedirectResponse
     * @throws \BB\Exceptions\AuthenticationException
     * @throws \BB\Exceptions\NotImplementedException
     */
    public function confirmPayment($userId)
    {
        $confirm_params = array(
            'resource_id'   => $_GET['resource_id'],
            'resource_type' => $_GET['resource_type'],
            'resource_uri'  => $_GET['resource_uri'],
            'signature'     => $_GET['signature']
        );

        // State is optional
        if (isset($_GET['state'])) {
            $confirm_params['state'] = $_GET['state'];
        }

        $user = User::findWithPermission($userId);

        try {
            $confirmed_resource = $this->goCardless->confirmResource($confirm_params);
        } catch (\Exception $e) {
            $errors = $e->getMessage();
            \Notification::error($errors);

            return \Redirect::route('account.show', [$user->id]);
        }

        $details = explode(':', \Input::get('state'));
        $reason  = $details[0];
        $ref     = $details[1];

        \Log::debug('Old PaymentController@confirmPayment method used. Reason: ' . $reason);

        $payment = new Payment([
            'reason'           => $reason,
            'source'           => 'gocardless',
            'source_id'        => $confirmed_resource->id,
            'amount'           => $confirmed_resource->amount,
            'fee'              => ($confirmed_resource->amount - $confirmed_resource->amount_minus_fees),
            'amount_minus_fee' => $confirmed_resource->amount_minus_fees,
            'status'           => $confirmed_resource->status
        ]);
        $user->payments()->save($payment);

        if ($reason == 'subscription') {
            $user->status = 'active';
            $user->active = true;
            $user->save();
        } elseif ($reason == 'induction') {
            //Payments must go via the balance
            throw new \BB\Exceptions\NotImplementedException('Attempted GoCardless induction payment');
        } elseif ($reason == 'door-key') {
            //Payments must go via the balance
            throw new \BB\Exceptions\NotImplementedException('Attempted GoCardless dor key payment');
        } elseif ($reason == 'storage-box') {
            //Payments must go via the balance
            throw new \BB\Exceptions\NotImplementedException('Attempted GoCardless storage box payment');
        } elseif ($reason == 'balance') {
            $memberCreditService = \App::make('\BB\Services\Credit');
            $memberCreditService->setUserId($user->id);
            $memberCreditService->recalculate();

            //This needs to be improved
            \Notification::success('Payment recorded');

            return \Redirect::route('account.bbcredit.index', $user->id);
        } else {
            throw new \BB\Exceptions\NotImplementedException();
        }

        \Notification::success('Payment made');

        return \Redirect::route('account.show', [$user->id]);
    }


    /**
     * Store a manual payment
     *
     * @param $userId
     * @throws \BB\Exceptions\AuthenticationException
     * @throws \BB\Exceptions\FormValidationException
     * @throws \BB\Exceptions\NotImplementedException
     * @return Illuminate\Http\RedirectResponse
     * @deprecated
     */
    public function store($userId)
    {
        $user = User::findWithPermission($userId);

        if ( ! \Auth::user()->hasRole('admin') &&  ! \Auth::user()->hasRole('finance')) {
            throw new \BB\Exceptions\AuthenticationException;
        }

        \Log::debug('Manual payment endpoint getting hit. account/{id}/payment. paymentController@store '.json_encode(\Input::all()));

        $reason = \Input::get('reason');

        if ($reason == 'subscription') {
            $payment = new Payment([
                'reason'           => $reason,
                'source'           => \Input::get('source'),
                'source_id'        => '',
                'amount'           => $user->monthly_subscription,
                'amount_minus_fee' => $user->monthly_subscription,
                'status'           => 'paid'
            ]);
            $user->payments()->save($payment);

            $user->extendMembership(\Input::get('source'), \Carbon\Carbon::now()->addMonth());

        } elseif ($reason == 'induction') {
            if (\Input::get('source') == 'manual') {
                $ref = \Input::get('induction_key');
                ($item = $this->equipmentRepository->findBySlug($ref)) || App::abort(404);
                $payment = new Payment([
                    'reason'           => $reason,
                    'source'           => 'manual',
                    'source_id'        => '',
                    'amount'           => $item->cost,
                    'amount_minus_fee' => $item->cost,
                    'status'           => 'paid'
                ]);
                $payment = $user->payments()->save($payment);
                Induction::create([
                    'user_id'    => $user->id,
                    'key'        => $ref,
                    'paid'       => true,
                    'payment_id' => $payment->id
                ]);
            } else {
                throw new \BB\Exceptions\NotImplementedException();
            }
        } elseif ($reason == 'door-key') {
            $payment = new Payment([
                'reason'           => $reason,
                'source'           => \Input::get('source'),
                'source_id'        => '',
                'amount'           => 10,
                'amount_minus_fee' => 10,
                'status'           => 'paid'
            ]);
            $user->payments()->save($payment);

            $user->key_deposit_payment_id = $payment->id;
            $user->save();

        } elseif ($reason == 'storage-box') {
            $payment = new Payment([
                'reason'           => $reason,
                'source'           => \Input::get('source'),
                'source_id'        => '',
                'amount'           => 5,
                'amount_minus_fee' => 5,
                'status'           => 'paid'
            ]);
            $user->payments()->save($payment);

            $user->storage_box_payment_id = $payment->id;
            $user->save();
        } elseif ($reason == 'balance') {
            $amount = \Input::get('amount') * 1; //convert the users amount into a number
            if ( ! is_numeric($amount)) {
                $exceptionErrors = new \Illuminate\Support\MessageBag(['amount' => 'Invalid amount']);
                throw new \BB\Exceptions\FormValidationException('Not a valid amount', $exceptionErrors);
            }
            $payment = new Payment([
                'reason'           => 'balance',
                'source'           => \Input::get('source'),
                'source_id'        => '',
                'amount'           => $amount,
                'amount_minus_fee' => $amount,
                'status'           => 'paid'
            ]);
            $user->payments()->save($payment);

            $memberCreditService = \App::make('\BB\Services\Credit');
            $memberCreditService->setUserId($user->id);
            $memberCreditService->recalculate();

            //This needs to be improved
            \Notification::success('Payment recorded');

            return \Redirect::route('account.bbcredit.index', $user->id);
        } else {
            throw new \BB\Exceptions\NotImplementedException();
        }
        \Notification::success('Payment recorded');

        return \Redirect::route('account.show', [$user->id]);
    }


    /**
     * 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)
    {
        //
    }


    /**
     * Update a payment
     * Change where the money goes by altering the original record or creating a secondary payment
     *
     * @param Request $request
     * @param  int    $paymentId
     *
     * @return Illuminate\Http\RedirectResponse
     * @throws NotImplementedException
     * @throws \BB\Exceptions\PaymentException
     */
    public function update(Request $request, $paymentId)
    {
        $payment = $this->paymentRepository->getById($paymentId);

        switch($request->get('change')) {
            case 'assign-unknown-to-user':
                $newUserId = $request->get('user_id');
                try {
                    $newUser = $this->userRepository->getById($newUserId);
                } catch (ModelNotFoundException $e) {
                    \Notification::error('User not found');
                    break;
                }

                $this->paymentRepository->assignPaymentToUser($paymentId, $newUser->id);

                \Notification::success('Payment updated');

                break;

            case 'refund-to-balance':

                if ($payment->reason === 'induction') {
                    throw new NotImplementedException('Please refund via the member induction list');
                }

                $this->paymentRepository->refundPaymentToBalance($paymentId);

                \Notification::success('Payment updated');

                break;

            default:
                throw new NotImplementedException('This hasn\'t been built yet');
        }

        return \Redirect::back();
    }


    /**
     * Remove the specified payment
     *
     * @param  int $id
     * @return Illuminate\Http\RedirectResponse
     * @throws \BB\Exceptions\ValidationException
     */
    public function destroy($id)
    {
        $payment = $this->paymentRepository->getById($id);

        //we can only allow some records to get deleted, only cash payments can be removed, everything else must be refunded off
        if ($payment->source != 'cash') {
            throw new \BB\Exceptions\ValidationException('Only cash payments can be deleted');
        }
        if ($payment->reason != 'balance') {
            throw new \BB\Exceptions\ValidationException('Currently only payments to the members balance can be deleted');
        }

        //The delete event will broadcast an event and allow related actions to occur
        $this->paymentRepository->delete($id);

        \Notification::success('Payment deleted');

        return \Redirect::back();
    }


    /**
     * This is a method for migrating user to the variable gocardless subscription
     * It will cancel the existing direct debit and direct the user to setup a pre auth
     *
     * @return Illuminate\Http\RedirectResponse
     */
    public function migrateDD()
    {
        $user = \Auth::user();

        //cancel the existing dd
        try {
            $subscription = $this->goCardless->cancelSubscription($user->subscription_id);
            if ($subscription->status != 'cancelled') {
                \Notification::error('Could not cancel the existing subscription');

                return \Redirect::back();
            }
        } catch (\GoCardless_ApiException $e) {

        }

        $user->payment_method  = '';
        $user->subscription_id = '';
        $user->save();

        $payment_details = array(
            'redirect_uri' => route('account.subscription.store', $user->id),
            'user'         => [
                'first_name'       => $user->given_name,
                'last_name'        => $user->family_name,
                'billing_address1' => $user->address->line_1,
                'billing_address2' => $user->address->line_2,
                'billing_town'     => $user->address->line_3,
                'billing_postcode' => $user->address->postcode,
                'country_code'     => 'GB'
            ]
        );

        return \Redirect::to($this->goCardless->newPreAuthUrl($payment_details));
    }

}
<file_sep>/app/Services/Payment/GoCardlessWebhookParser.php
<?php namespace BB\Services\Payment;

class GoCardlessWebhookParser
{

    /**
     * @var string
     */
    private $rawResponse = null;

    /**
     * @var array
     */
    private $response;

    /**
     * @var string
     */
    private $action = null;

    /**
     * @var string
     */
    private $resourceType = null;

    /**
     * @var array
     */
    private $bills = [];

    /**
     * @var array
     */
    private $subscriptions = [];

    /**
     * @var array
     */
    private $preAuthList = [];

    public function parseResponse($paymentPaidPayload)
    {
        $this->rawResponse = $paymentPaidPayload;
        $response = json_decode($this->rawResponse, true);
        if ( ! is_array($response)) {
            return;
        }
        $this->response = $response;

        $this->action = $this->response['payload']['action'];
        $this->resourceType = $this->response['payload']['resource_type'];

        if ($this->resourceType == 'bill') {
            $this->setBills($this->response['payload']['bills']);
        } else if ($this->resourceType == 'subscription') {
            $this->subscriptions = $this->response['payload']['subscriptions'];
        } else if ($this->resourceType == 'pre_authorization') {
            $this->preAuthList = $this->response['payload']['pre_authorizations'];
        }
    }

    /**
     * @return null
     */
    public function getAction()
    {
        return $this->action;
    }

    /**
     * @return null
     */
    public function getResourceType()
    {
        return $this->resourceType;
    }

    /**
     * @return array
     */
    public function getBills()
    {
        return $this->bills;
    }

    /**
     * @return array
     */
    public function getSubscriptions()
    {
        return $this->subscriptions;
    }

    /**
     * @return array
     */
    public function getPreAuthList()
    {
        return $this->preAuthList;
    }

    private function setBills($bills)
    {
        foreach ($bills as $i=>$bill) {
            if ( ! isset($bills[$i]['source_type'])) {
                $bills[$i]['source_type'] = '';
            }
            if ( ! isset($bills[$i]['source_id'])) {
                $bills[$i]['source_id'] = '';
            }
            if ( ! isset($bills[$i]['paid_at'])) {
                $bills[$i]['paid_at'] = '';
            }
        }
        $this->bills = $bills;
    }
}<file_sep>/app/Repo/UserRepository.php
<?php namespace BB\Repo;

use BB\Entities\User;
use Carbon\Carbon;

class UserRepository extends DBRepository
{

    /**
     * @var User
     */
    protected $model;
    /**
     * @var AddressRepository
     */
    private $addressRepository;
    /**
     * @var ProfileDataRepository
     */
    private $profileDataRepository;
    /**
     * @var SubscriptionChargeRepository
     */
    private $subscriptionChargeRepository;

    public function __construct(User $model, AddressRepository $addressRepository, ProfileDataRepository $profileDataRepository, SubscriptionChargeRepository $subscriptionChargeRepository)
    {
        $this->model = $model;
        $this->perPage = 150;
        $this->addressRepository = $addressRepository;
        $this->profileDataRepository = $profileDataRepository;
        $this->subscriptionChargeRepository = $subscriptionChargeRepository;
    }

    public function getActive()
    {
        return $this->model->active()->get();
    }

    public function getBillableActive()
    {
        return $this->model->active()->notSpecialCase()->get();
    }

    public function getPaginated(array $params)
    {
        $model = $this->model->with('roles')->with('profile');

        if ($params['showLeft']) {
            $model = $model->where('status', 'left');
        } else {
            $model = $model->where('status', '!=', 'left');
        }

        if ($this->isSortable($params)) {
            return $model->orderBy($params['sortBy'], $params['direction'])->simplePaginate($this->perPage);
        }
        return $model->simplePaginate($this->perPage);
    }


    /**
     * Return a collection of members for public display
     * @param bool $showPrivateMembers Some members don't want to listed on public pages, set to true to show everyone
     * @return mixed
     */
    public function getActivePublicList($showPrivateMembers = false)
    {
        if ($showPrivateMembers) {
            return $this->model->with('profile', 'roles')->active()->where('status', '!=', 'leaving')->orderBy('given_name')->get();
        } else {
            return $this->model->with('profile', 'roles')->active()->where('status', '!=', 'leaving')->where('profile_private', 0)->orderBy('given_name')->get();
        }
    }

    public function getTrustedMissingPhotos()
    {
        return \DB::table('users')->join('profile_data', 'users.id', '=', 'profile_data.user_id')->where('key_holder', '1')->where('active', '1')->where('profile_data.profile_photo', 0)->get();
    }

    /**
     * Get a list of active members suitable for use in a dropdown
     * @return array
     */
    public function getAllAsDropdown()
    {
        $members = $this->getActive();
        $memberDropdown = [];
        foreach ($members as $member) {
            $memberDropdown[$member->id] = $member->name;
        }
        return $memberDropdown;
    }

    /**
     * @param array   $memberData The new members details
     * @param boolean $isAdminCreating Is the user making the change an admin
     * @return User
     */
    public function registerMember(array $memberData, $isAdminCreating)
    {
        if (empty($memberData['profile_photo_private'])) {
            $memberData['profile_photo_private'] = false;
        }

        if (empty($memberData['password'])) {
            unset($memberData['password']);
        }

        $memberData['hash'] = str_random(30);

        $memberData['rules_agreed'] = $memberData['rules_agreed']? Carbon::now(): null;

        $user = $this->model->create($memberData);
        $this->profileDataRepository->createProfile($user->id);

        $this->addressRepository->saveUserAddress($user->id, $memberData['address'], $isAdminCreating);

        return $user;
    }

    /**
     * The user has setup a payment method of some kind so they are now considered active
     * This will kick off the automated member checking processes
     *
     * @param integer $userId
     */
    public function ensureMembershipActive($userId)
    {
        $user = $this->getById($userId);

        //user needs to have a recent sub charge and one that was paid or is due

        $user->active = true;
        $user->status = 'active';
        $user->save();

        $outstandingCharges = $this->subscriptionChargeRepository->hasOutstandingCharges($userId);

        //If the user doesn't have any charges currently processing or they dont have an expiry date or are past their expiry data create a charge
        if ( ! $outstandingCharges && ( ! $user->subscription_expires || $user->subscription_expires->lt(Carbon::now()))) {
            //create a charge

            $chargeDate = Carbon::now();

            //If we are passed the end of month cutoff push the charge date forward to their actual charge date
            if ((Carbon::now()->day > 28) && $user->payment_day === 1) {
                $chargeDate = $chargeDate->day(1)->addMonth();
            }

            $this->subscriptionChargeRepository->createChargeAndBillDD($userId, $chargeDate, $user->monthly_subscription, 'due', $user->subscription_id);
        }
    }

    /**
     * @param integer $userId           The ID of the user to be updated
     * @param array   $recordData       The data to be updated
     * @param boolean $isAdminUpdating  Is the user making the change an admin
     */
    public function updateMember($userId, array $recordData, $isAdminUpdating)
    {
        //If the password field hasn't been filled in unset it so it doesn't get set to a blank password
        if (empty($recordData['password'])) {
            unset($recordData['password']);
        }

        //Update the main user record
        $this->update($userId, $recordData);

        //Update the user address
        if (isset($recordData['address']) && is_array($recordData['address'])) {
            $this->addressRepository->updateUserAddress($userId, $recordData['address'], $isAdminUpdating);
        }
    }

    /**
     * The member has left, disable their account and cancel any out stand sub charge records
     * The payment day is also cleared so when they start again the payment is charge happens at restart time
     *
     * @param $userId
     */
    public function memberLeft($userId)
    {
        $user = $this->getById($userId);
        $user->active       = false;
        $user->status       = 'left';
        $user->payment_day  = '';
        $user->save();

        $this->subscriptionChargeRepository->cancelOutstandingCharges($userId);
    }

    /**
     * Record the new gocardless preauth id against the user and make sure their payment method reflects this
     *
     * @param integer $userId
     * @param string  $subscriptionId
     */
    public function recordGoCardlessVariableDetails($userId, $subscriptionId)
    {
        $user = $this->getById($userId);
        if (empty($user->payment_day)) {
            $user->payment_day    = Carbon::now()->day;
        }
        $user->subscription_id    = $subscriptionId;
        $user->payment_method     = 'gocardless-variable';
        $user->save();
    }

    /**
     * Record the fact that the user has agreed to the member induction and the rules
     *
     * @param $userId
     */
    public function recordInductionCompleted($userId)
    {
        $user = $this->getById($userId);

        $user->induction_completed = true;

        $user->rules_agreed = $user->rules_agreed? $user->rules_agreed: Carbon::now();

        $user->save();
    }

    public function getPendingInductionConfirmation()
    {
        return $this->model
            ->where('status', '!=', 'left')
            ->where('induction_completed', true)
            ->where('inducted_by', null)
            ->get();
    }

}<file_sep>/app/Repo/SubscriptionChargeRepository.php
<?php namespace BB\Repo;

use BB\Entities\SubscriptionCharge;
use BB\Events\SubscriptionChargePaid;
use BB\Helpers\GoCardlessHelper;
use Carbon\Carbon;

class SubscriptionChargeRepository extends DBRepository
{

    /**
     * @var SubscriptionCharge
     */
    protected $model;
    /**
     * @var PaymentRepository
     */
    private $paymentRepository;
    /**
     * @var GoCardlessHelper
     */
    private $goCardless;

    public function __construct(SubscriptionCharge $model, PaymentRepository $paymentRepository, GoCardlessHelper $goCardless)
    {
        $this->model = $model;
        $this->paymentRepository = $paymentRepository;
        $this->goCardless = $goCardless;
    }

    /**
     * @param integer   $userId
     * @param \DateTime $date
     * @param integer   $amount
     * @param string    $status
     * @return SubscriptionCharge
     */
    public function createCharge($userId, \DateTime $date, $amount = 0, $status = 'pending')
    {
        return $this->model->create(['charge_date' => $date, 'user_id' => $userId, 'amount' => $amount, 'status'=>$status]);
    }

    /**
     * Create a charge and immediately bill it through the direct debit process
     *
     * @param integer   $userId
     * @param \DateTime $date
     * @param integer   $amount
     * @param string    $status
     * @param string    $DDAuthId
     * @return SubscriptionCharge
     */
    public function createChargeAndBillDD($userId, $date, $amount, $status, $DDAuthId)
    {
        $charge = $this->createCharge($userId, $date, $amount, $status);

        $bill = $this->goCardless->newBill($DDAuthId, $amount, $this->goCardless->getNameFromReason('subscription'));
        if ($bill) {
            $this->paymentRepository->recordSubscriptionPayment($userId, 'gocardless-variable', $bill->id,
                $bill->amount, $bill->status, $bill->gocardless_fees, $charge->id);
        }
    }

    /**
     * Does a charge already exist for the user and date
     * @param $userId
     * @param Carbon $date
     * @return bool
     */
    public function chargeExists($userId, $date)
    {
        if ($this->model->where('user_id', $userId)->where('charge_date', $date->format('Y-m-d'))->count() !== 0) {
            return true;
        }
        return false;
    }

    /**
     * Locate the next payment the user has to pay off
     *
     * @param $userId
     * @param Carbon $paymentDate
     * @return mixed
     */
    public function findCharge($userId, $paymentDate = null)
    {
        //find any existing payment that hasn't been paid
        //Subscription payments will always be used to pay of bills

        return $this->model->where('user_id', $userId)->whereIn('status', ['pending', 'due'])->orderBy('charge_date', 'ASC')->first();
    }

    /**
     * @param integer      $chargeId
     * @param              $paymentDate
     * @param null|integer $amount
     */
    public function markChargeAsPaid($chargeId, $paymentDate = null, $amount = null)
    {
        if (is_null($paymentDate)) {
            $paymentDate = new Carbon();
        }
        $subCharge = $this->getById($chargeId);
        $subCharge->payment_date = $paymentDate;
        $subCharge->status = 'paid';
        if ($amount) {
            $subCharge->amount = $amount;
        }
        $subCharge->save();

        event(new SubscriptionChargePaid($subCharge));
    }

    /**
     * @param $chargeId
     */
    public function markChargeAsProcessing($chargeId)
    {
        $subCharge = $this->getById($chargeId);
        $subCharge->status = 'processing';
        $subCharge->save();

        \Event::fire('sub-charge.processing', array($chargeId, $subCharge->user_id, $subCharge->charge_date, $subCharge->amount));
    }

    /**
     * If a payment has failed update the sub charge to reflect this
     *
     * @param $chargeId
     */
    public function paymentFailed($chargeId)
    {
        $subCharge = $this->getById($chargeId);
        //If the charge has already been cancelled dont touch it
        if ($subCharge->status != 'cancelled') {
            $subCharge->payment_date = null;
            $subCharge->status       = 'due';
            $subCharge->amount       = 0;
            $subCharge->save();
        } else {
            \Log::debug('Sub charge not updated after payment failure, already cancelled. Charge ID: ' . $chargeId);
        }

        \Event::fire('sub-charge.payment-failed', array($chargeId, $subCharge->user_id, $subCharge->charge_date, $subCharge->amount));
    }

    /**
     * Return a paginated list of member payments
     *
     * @param integer $userId
     * @return mixed
     */
    public function getMemberChargesPaginated($userId)
    {
        return $this->model->where('user_id', $userId)->orderBy('charge_date', 'DESC')->paginate();
    }

    /**
     * Return a paginated list of member payments
     *
     * @param integer $userId
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function getMemberCharges($userId)
    {
        return $this->model->where('user_id', $userId)->orderBy('charge_date', 'DESC')->get();
    }

    /**
     * Return a members most recent paid up subscription charge record
     *
     * @param $userId
     * @return SubscriptionCharge
     */
    public function getMembersLatestPaidCharge($userId)
    {
        return $this->model->where('user_id', $userId)->where('status', 'paid')->orderBy('charge_date', 'DESC')->first();
    }

    /**
     * Return a paginated list of member payments
     *
     * @return mixed
     */
    public function getChargesPaginated()
    {
        return $this->model->orderBy('charge_date', 'DESC')->paginate();
    }

    /**
     * Get all the charges which are due payment
     *
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function getDue()
    {
        return $this->model->where('status', 'due')->get();
    }

    /**
     * Get charges that are newly created and pending
     *
     * @return mixed
     */
    public function getPending()
    {
        return $this->model->where('status', 'pending')->get();
    }

    /**
     * Update a charge and mark it as due
     *
     * @param $chargeId
     */
    public function setDue($chargeId)
    {
        $subCharge = $this->getById($chargeId);
        $subCharge->status = 'due';
        $subCharge->save();
    }

    /**
     * Cancel all outstanding (due and pending) charges for a user
     * Used when leaving
     *
     * @param $userId
     */
    public function cancelOutstandingCharges($userId)
    {
        $this->model->where('user_id', $userId)->whereIn('status', ['pending', 'due'])->update(['status'=>'cancelled']);
    }

    /**
     * Does the user have any active or outstanding charges
     *
     * @param integer $userId
     * @return bool
     */
    public function hasOutstandingCharges($userId)
    {
        return ($this->model->where('user_id', $userId)->whereIn('status', ['pending', 'due', 'processing'])->count() > 0);
    }

    /**
     * @param integer $chargeId
     * @param integer $newAmount
     */
    public function updateAmount($chargeId, $newAmount)
    {
        $subCharge = $this->getById($chargeId);
        $subCharge->amount = $newAmount;
        $subCharge->save();
    }

}<file_sep>/app/Presenters/PaymentPresenter.php
<?php namespace BB\Presenters;

use Laracasts\Presenter\Presenter;

class PaymentPresenter extends Presenter
{

    public function reason()
    {
        switch ($this->entity->reason) {
            case 'subscription':
                return 'Subscription';
            case 'unknown':
                return 'Unknown';
            case 'induction':
                return 'Equipment Access Fee';
            case 'door-key':
                return 'Key Deposit';
            case 'storage-box':
                return 'Storage Box Deposit';
            case 'balance':
                return 'Balance Top Up';
            case 'equipment-fee':
                return 'Equipment Costs';
            case 'withdrawal':
                return 'Withdrawal';
            case 'consumables':
                return 'Consumables (' . $this->entity->reference . ')';
            case 'transfer':
                return 'Transfer (to user:' . $this->entity->reference . ')';
            case 'donation':
                return 'Donation';
            default:
                return $this->entity->reason;
        }
    }

    public function status()
    {
        switch ($this->entity->status) {
            case 'pending':
                return 'Pending Confirmation';
            case 'cancelled':
                return 'Cancelled';
            case 'paid':
            case 'withdrawn':
                return 'Paid';
            default:
                return $this->entity->status;
        }
    }

    public function date()
    {
        return $this->entity->created_at->toFormattedDateString();
    }

    public function method()
    {
        switch ($this->entity->source) {
            case 'gocardless':
            case 'gocardless-variable':
                return 'Direct Debit';
            case 'stripe':
                return 'Credit/Debit Card';
            case 'paypal':
                return 'PayPal';
            case 'standing-order':
                return 'Standing Order';
            case 'manual':
                return 'Manual';
            case 'cash':
                return 'Cash' . ($this->entity->source_id? ' (' . $this->entity->source_id . ')':'');
            case 'other':
                return 'Other';
            case 'balance':
                return 'BB Balance';
            case 'reimbursement':
                return 'Reimbursement';
            case 'transfer':
                return 'Transfer (from user:' . $this->entity->reference . ')';
            default:
                return $this->entity->source;
        }
    }

    public function amount()
    {
        return '&pound;' . $this->entity->amount;
    }

    public function balanceAmount()
    {
        if ($this->entity->source == 'balance') {
            return '-&pound;' . $this->entity->amount;
        } elseif ($this->entity->reason == 'balance') {
            return '&pound;' . $this->entity->amount;
        }
    }

    public function balanceRowClass()
    {
        if ($this->entity->source == 'balance') {
            return 'danger';
        } elseif ($this->entity->reason == 'balance') {
            return 'success';
        }
    }
} <file_sep>/app/Console/Commands/CreateTodaysSubCharges.php
<?php namespace BB\Console\Commands;

use Carbon\Carbon;
use Illuminate\Console\Command;

class CreateTodaysSubCharges extends Command
{

    /**
     * The console command name.
     *
     * @var string
     */
    protected $signature = 'bb:create-todays-sub-charges {dayOffset=3}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create all the subscription changes for today';

    /**
     * @var \BB\Services\MemberSubscriptionCharges
     */
    private $subscriptionChargeService;

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

        $this->subscriptionChargeService = \App::make('\BB\Services\MemberSubscriptionCharges');
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $dayOffset = $this->argument('dayOffset');

        $targetDate = Carbon::now()->addDays($dayOffset);

        $this->info("Generating charges for " . $targetDate);

        $this->subscriptionChargeService->createSubscriptionCharges($targetDate);

        //in case yesterdays process failed we will rerun the past seven days, this should pickup and stragglers
        $this->subscriptionChargeService->createSubscriptionCharges($targetDate->subDay()); //-1 day
        $this->subscriptionChargeService->createSubscriptionCharges($targetDate->subDay()); //-2 days
        $this->subscriptionChargeService->createSubscriptionCharges($targetDate->subDay()); //-3 days
        $this->subscriptionChargeService->createSubscriptionCharges($targetDate->subDay()); //-4 days
        $this->subscriptionChargeService->createSubscriptionCharges($targetDate->subDay()); //-5 days
        $this->subscriptionChargeService->createSubscriptionCharges($targetDate->subDay()); //-6 days
        $this->subscriptionChargeService->createSubscriptionCharges($targetDate->subDay()); //-7 days
    }
}
<file_sep>/app/Http/Controllers/ACS/StatusController.php
<?php

namespace BB\Http\Controllers\ACS;

use BB\Entities\KeyFob;
use BB\Exceptions\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use BB\Http\Requests;
use BB\Http\Controllers\Controller;

class StatusController extends Controller
{

    /**
     * Display the specified resource.
     *
     * @param  string $tagId
     *
     * @return \Illuminate\Http\Response
     * @throws ValidationException
     *
     * @SWG\Get(
     *     path="/acs/status/{tagId}",
     *     tags={"acs"},
     *     description="Get information about a specific tag and its user",
     *     consumes={"application/json"},
     *     @SWG\Parameter(name="tagId", in="path", type="string"),
     *     @SWG\Response(response="200", description="Tag found"),
     *     @SWG\Response(response="404", description="Tag not found"),
     *     security={{"api_key": {}}}
     * )
     *
     */
    public function show($tagId)
    {
        try {
            $keyFob = KeyFob::lookup($tagId);
        } catch (\Exception $e) {
            $oldTagId = substr('BB' . $tagId, 0, 12);
            try {
                $keyFob = KeyFob::lookup($oldTagId);
            } catch (\Exception $e) {

                //The ids coming in will have no checksum (last 2 digits) and the first digit will be incorrect

                //Remove the first character
                $tagId = substr($tagId, 1);

                try {
                    $keyFob = KeyFob::lookupPartialTag($tagId);
                } catch (\Exception $e) {
                    throw new ModelNotFoundException('Key fob ID not found');
                }
            }
        }

        return ['user' => [
            'id' => $keyFob->user->id,
            'name' => $keyFob->user->name,
            'status' => $keyFob->user->status,
            'active' => $keyFob->user->active,
            'key_holder' => $keyFob->user->key_holder,
            'cash_balance' => $keyFob->user->cash_balance,
            'profile_private' => $keyFob->user->profile_private,
        ]];
    }

}
<file_sep>/app/Http/Controllers/CCTVController.php
<?php

namespace BB\Http\Controllers;

use BB\Http\Controllers\Controller;

class CCTVController extends Controller
{

    /**
     * Store a newly created resource in storage.
     *
     * @return Response
     */
    public function store()
    {
        $s3 = AWS::get('s3');
        $s3Bucket = 'buildbrighton-bbms';

        if (Request::hasFile('image')) {
            $file = Request::file('image');
            $event = Request::get('textevent');
            $time = Request::get('time');

            $fileData = Image::make($file)->encode('jpg', 80);


            $date = Carbon::createFromFormat('YmdHis', $event);
            $folderName = $date->hour . ':' . $date->minute . ':' . $date->second;

            try {
                $newFilename = \App::environment() . '/cctv/' . $date->year . '/' . $date->month . '/' . $date->day . '/' . $folderName . '/' . $time . '.jpg';
                $s3->putObject(array(
                    'Bucket'        => $s3Bucket,
                    'Key'           => $newFilename,
                    //'Body'          => file_get_contents($file),
                    'Body'          => $fileData,
                    'ACL'           => 'public-read',
                    'ContentType'   => 'image/jpg',
                    'ServerSideEncryption' => 'AES256',
                ));
            } catch(\Exception $e) {
                \Log::exception($e);
            }
            //Log::debug('Image saved :https://s3-eu-west-1.amazonaws.com/buildbrighton-bbms/'.$newFilename);
        }
        if (Request::get('eventend') == 'true') {

            $event = Request::get('textevent');

            $date = Carbon::createFromFormat('YmdHis', $event);
            $folderName = $date->hour . ':' . $date->minute . ':' . $date->second;

            $iterator = $s3->getIterator(
                'ListObjects',
                array(
                    'Bucket' => $s3Bucket,
                    'Prefix' => \App::environment() . '/cctv/' . $date->year . '/' . $date->month . '/' . $date->day . '/' . $folderName,
                    //'Prefix' => 'production/camera-photos/20150410222028',
                )
            );

            $images         = [];
            $imageDurations = [];
            foreach ($iterator as $object) {
                $images[]         = 'https://s3-eu-west-1.amazonaws.com/buildbrighton-bbms/' . $object['Key'];
                $imageDurations[] = 35;
            }

            if (count($images) <= 2) {
                //only two images, probably two bad frames
                //delete them
                foreach ($iterator as $object) {
                    Log::debug("Deleting small event image " . $object['Key']);
                    $s3->deleteObject([
                        'Bucket'    => $s3Bucket,
                        'Key'       => $object['Key'],
                    ]);
                }
                return;
            }

            $gc = new GifCreator();
            $gc->create($images, $imageDurations, 0);
            $gifBinary = $gc->getGif();

            //Delete the individual frames now we have the gif
            foreach ($iterator as $object) {
                //Log::debug("Processed gif, deleting frame, ".$object['Key']);
                $s3->deleteObject([
                    'Bucket'    => $s3Bucket,
                    'Key'       => $object['Key'],
                ]);
            }

            //Save the gif
            $newFilename = \App::environment() . '/cctv/' . $date->year . '/' . $date->month . '/' . $date->day . '/' . $folderName . '.gif';
            $s3->putObject(
                array(
                    'Bucket'               => $s3Bucket,
                    'Key'                  => $newFilename,
                    'Body'                 => $gifBinary,
                    'ACL'                  => 'public-read',
                    'ContentType'          => 'image/gif',
                    'ServerSideEncryption' => 'AES256',
                )
            );




            //Log::debug('Event Gif generated :https://s3-eu-west-1.amazonaws.com/buildbrighton-bbms/'.$newFilename);

            \Slack::to("#cctv")->attach(['image_url'=>'https://s3-eu-west-1.amazonaws.com/buildbrighton-bbms/' . $newFilename, 'color'=>'warning'])->send('Movement detected');

        }
    }

}
<file_sep>/app/Http/Controllers/ACS/ActivityController.php
<?php

namespace BB\Http\Controllers\ACS;

use BB\Events\MemberActivity;
use BB\Repo\ACSNodeRepository;
use BB\Repo\EquipmentLogRepository;
use BB\Services\KeyFobAccess;
use Illuminate\Http\Request;
use BB\Http\Requests;
use BB\Http\Controllers\Controller;
use Swagger\Annotations as SWG;

class ActivityController extends Controller
{
    /**
     * @var ACSNodeRepository
     */
    private $ACSNodeRepository;
    /**
     * @var EquipmentLogRepository
     */
    private $equipmentLogRepository;
    /**
     * @var KeyFobAccess
     */
    private $fobAccess;

    /**
     * NodeController constructor.
     *
     * @param ACSNodeRepository $ACSNodeRepository
     */
    public function __construct(ACSNodeRepository $ACSNodeRepository, EquipmentLogRepository $equipmentLogRepository, KeyFobAccess $fobAccess)
    {
        $this->ACSNodeRepository = $ACSNodeRepository;
        $this->equipmentLogRepository = $equipmentLogRepository;
        $this->fobAccess = $fobAccess;
    }

    /**
     * Record the start of a new session
     *
     * @return \Illuminate\Http\Response
     *
     * @SWG\Post(
     *     path="/acs/activity",
     *     tags={"activity"},
     *     description="Record the start of a period of activity, e.g. someone signing into the laser cutter",
     *     @SWG\Parameter(name="activity", in="body", required=true, @SWG\Schema(ref="#/definitions/Activity")),
     *     @SWG\Response(response="201", description="Activity started, the body will contain the new activityId"),
     *     @SWG\Response(response="404", description="Key fob not found"),
     *     security={{"api_key": {}}}
     * )
     */
    public function store(Request $request)
    {
        $activityRequest = new Requests\ACS\Activity($request);

        $keyFob = $this->fobAccess->extendedKeyFobLookup($activityRequest->getTagId());

        $activityId = $this->equipmentLogRepository->recordStartCloseExisting($keyFob->user->id, $keyFob->id, $activityRequest->getDevice());

        event(new MemberActivity($keyFob, $activityRequest->getDevice()));

        return response()->json([
            'activityId' => $activityId,
            'user'       => [
                'id'              => $keyFob->user->id,
                'name'            => $keyFob->user->name,
                'status'          => $keyFob->user->status,
                'active'          => $keyFob->user->active,
                'key_holder'      => $keyFob->user->key_holder,
                'cash_balance'    => $keyFob->user->cash_balance,
                'profile_private' => $keyFob->user->profile_private,
            ]
        ], 201);
    }

    /**
     * Update an ongoing activity
     *
     * @return \Illuminate\Http\Response
     *
     * @SWG\Put(
     *     path="/acs/activity/{activityId}",
     *     tags={"activity"},
     *     description="Record a heartbeat message for a period of activity, used to ensure activity periods are correctly recorded",
     *     @SWG\Parameter(name="activityId", in="path", type="string", required=true),
     *     @SWG\Response(response="200", description="Activity Heartbeat recorded"),
     *     security={{"api_key": {}}}
     * )
     */
    public function update(Request $request, $activityId)
    {
        $keyFob = $this->fobAccess->extendedKeyFobLookup($request->get('tagId'));

        $this->equipmentLogRepository->recordActivity($activityId);

        return response()->json([], 200);
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     *
     * @SWG\Delete(
     *     path="/acs/activity/{activityId}",
     *     tags={"activity"},
     *     description="End a period of an activity",
     *     @SWG\Parameter(name="activityId", in="path", type="string", required=true),
     *     @SWG\Response(response="204", description="Activity ended/deleted"),
     *     @SWG\Response(response="400", description="Session invalid"),
     *     security={{"api_key": {}}}
     * )
     */
    public function destroy(Request $request, $activityId)
    {
        $keyFob = $this->fobAccess->extendedKeyFobLookup($request->get('tagId'));

        $this->equipmentLogRepository->endSession($activityId);

        return response()->json([], 204);
    }

}
<file_sep>/app/Http/Controllers/GoCardlessPaymentController.php
<?php namespace BB\Http\Controllers;

use BB\Entities\User;

class GoCardlessPaymentController extends Controller
{
    /**
     * @var \BB\Repo\PaymentRepository
     */
    private $paymentRepository;
    /**
     * @var \BB\Helpers\GoCardlessHelper
     */
    private $goCardless;

    function __construct(\BB\Repo\PaymentRepository $paymentRepository, \BB\Helpers\GoCardlessHelper $goCardless)
    {
        $this->paymentRepository = $paymentRepository;

        $this->middleware('role:member', array('only' => ['create', 'store']));
        $this->goCardless = $goCardless;
    }

    /**
     * Main entry point for all gocardless payments - not subscriptions
     * @param $userId
     * @return mixed
     * @throws \BB\Exceptions\AuthenticationException
     */
    public function create($userId)
    {
        $user = User::findWithPermission($userId);

        $requestData = \Request::only(['reason', 'amount', 'return_path']);

        $reason = $requestData['reason'];
        $amount = ($requestData['amount'] * 1) / 100;
        $returnPath = $requestData['return_path'];
        $ref = $this->getReference($reason);


        if ($user->payment_method == 'gocardless-variable') {

            return $this->handleBill($amount, $reason, $user, $ref, $returnPath);

        } elseif ($user->payment_method == 'gocardless') {

            return $this->ddMigratePrompt($returnPath);

        } else {

            return $this->handleManualBill($amount, $reason, $user, $ref, $returnPath);

        }
    }

    /**
     * Processes the return for old gocardless payments
     *
     * @param $userId
     * @return \Illuminate\Http\RedirectResponse
     * @throws \BB\Exceptions\AuthenticationException
     */
    public function handleManualReturn($userId)
    {
        $user = User::findWithPermission($userId);

        $confirm_params = array(
            'resource_id'    => $_GET['resource_id'],
            'resource_type'  => $_GET['resource_type'],
            'resource_uri'   => $_GET['resource_uri'],
            'signature'      => $_GET['signature']
        );

        // State is optional
        if (isset($_GET['state'])) {
            $confirm_params['state'] = $_GET['state'];
        }

        //Get the details, reason, reference and return url
        $details = explode(':', \Input::get('state'));
        $reason  = 'unknown';
        $ref     = null;
        $returnPath = route('account.show', [$user->id], false);
        if (is_array($details)) {
            if (isset($details[0])) {
                $reason = $details[0];
            }
            if (isset($details[1])) {
                $ref = $details[1];
            }
            if (isset($details[2])) {
                $returnPath = $details[2];
            }
        }


        //Confirm the resource
        try {
            $confirmed_resource = $this->goCardless->confirmResource($confirm_params);
        } catch (\Exception $e) {
            \Notification::error($e->getMessage());
            return \Redirect::to($returnPath);
        }


        //Store the payment
        $fee = ($confirmed_resource->amount - $confirmed_resource->amount_minus_fees);
        $paymentSourceId = $confirmed_resource->id;
        $amount = $confirmed_resource->amount;
        $status = $confirmed_resource->status;

        //The record payment process will make the necessary record updates
        $this->paymentRepository->recordPayment($reason, $userId, 'gocardless', $paymentSourceId, $amount, $status, $fee, $ref);

        \Notification::success("Payment made");
        return \Redirect::to($returnPath);
    }

    /**
     * Process a manual gocardless payment
     *
     * @param $amount
     * @param $reason
     * @param User $user
     * @param $ref
     * @param $returnPath
     * @return mixed
     * @throws \BB\Exceptions\NotImplementedException
     */
    private function handleManualBill($amount, $reason, $user, $ref, $returnPath)
    {
        $payment_details = array(
            'amount'       => $amount,
            'name'         => $this->getName($reason, $user->id),
            'description'  => $this->getDescription($reason),
            'redirect_uri' => route('account.payment.gocardless.manual-return', [$user->id]),
            'user'         => [
                'first_name'       => $user->given_name,
                'last_name'        => $user->family_name,
                'billing_address1' => $user->address_line_1,
                'billing_address2' => $user->address_line_2,
                'billing_town'     => $user->address_line_3,
                'billing_postcode' => $user->address_postcode,
                'country_code'     => 'GB'
            ],
            'state'        => $reason . ':' . $ref . ':' . $returnPath
        );
        $redirectUrl = $this->goCardless->newBillUrl($payment_details);

        if (\Request::wantsJson()) {
            return \Response::json(['url' => $redirectUrl], 303);
        }

        return \Redirect::to($redirectUrl);
    }


    private function ddMigratePrompt($returnPath)
    {
        if (\Request::wantsJson()) {
            return \Response::json(['error' => 'Please visit the "Your Membership" page and migrate your Direct Debit first, then return and make the payment'], 400);
        }
        \Notification::error("Please visit the \"Your Membership\" page and migrate your Direct Debit first, then return and make the payment");
        return \Redirect::to($returnPath);
    }

    /**
     * Process a direct debit payment when we have a preauth
     *
     * @param $amount
     * @param $reason
     * @param User $user
     * @param $ref
     * @param $returnPath
     * @return mixed
     */
    private function handleBill($amount, $reason, $user, $ref, $returnPath)
    {
        $bill = $this->goCardless->newBill($user->subscription_id, $amount, $this->goCardless->getNameFromReason($reason));

        if ($bill) {
            //Store the payment
            $fee = ($bill->amount - $bill->amount_minus_fees);
            $paymentSourceId = $bill->id;
            $amount = $bill->amount;
            $status = $bill->status;

            //The record payment process will make the necessary record updates
            $this->paymentRepository->recordPayment($reason, $user->id, 'gocardless-variable', $paymentSourceId, $amount, $status, $fee, $ref);

            if (\Request::wantsJson()) {
                return \Response::json(['message' => 'The payment was submitted successfully']);
            }

            \Notification::success("The payment was submitted successfully");
        } else {
            //something went wrong - we still have the pre auth though

            if (\Request::wantsJson()) {
                return \Response::json(['error' => 'There was a problem charging your account'], 400);
            }

            \Notification::error("There was a problem charging your account");
        }

        return \Redirect::to($returnPath);
    }



    private function getDescription($reason)
    {
        if ($reason == 'subscription') {
            return "Monthly Subscription Fee - Manual";
        } elseif ($reason == 'induction') {
            return strtoupper(\Input::get('induction_key')) . " Induction Fee";
        } elseif ($reason == 'door-key') {
            return "Door Key Deposit";
        } elseif ($reason == 'storage-box') {
            return "Storage Box Deposit";
        } elseif ($reason == 'balance') {
            return "BB Credit Payment";
        } else {
            throw new \BB\Exceptions\NotImplementedException();
        }
    }

    private function getName($reason, $userId)
    {
        if ($reason == 'subscription') {
            return strtoupper("BBSUB" . $userId . ":MANUAL");
        } elseif ($reason == 'induction') {
            return strtoupper("BBINDUCTION" . $userId . ":" . \Request::get('induction_key'));
        } elseif ($reason == 'door-key') {
            return strtoupper("BBDOORKEY" . $userId);
        } elseif ($reason == 'storage-box') {
            return strtoupper("BBSTORAGEBOX" . $userId);
        } elseif ($reason == 'balance') {
            return strtoupper("BBBALANCE" . $userId);
        } else {
            throw new \BB\Exceptions\NotImplementedException();
        }
    }

    private function getReference($reason)
    {
        if ($reason == 'induction') {
            return \Request::get('ref');
        } elseif ($reason == 'balance') {
            return \Request::get('reference');
        }
        return false;
    }
}
<file_sep>/app/Services/KeyFobAccess.php
<?php namespace BB\Services;

use BB\Entities\KeyFob;
use BB\Entities\User;
use BB\Events\MemberActivity;
use BB\Exceptions\ValidationException;
use BB\Repo\ActivityRepository;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class KeyFobAccess
{

    /**
     * The key fob string
     * @var string
     */
    protected  $keyFobId;

    /**
     * The key fob record
     * @var KeyFob
     */
    protected $keyFob;

    /**
     * The key for the selected device
     * @var string
     */
    protected $deviceKey;

    /**
     * The selected device record
     * The device that's being acted apon
     * @var
     */
    protected $device;

    /**
     * The action of the current session
     * @var string
     */
    protected $action;

    /**
     * @var User
     */
    protected $user;

    /**
     * @var ActivityRepository
     */
    protected $activityRepository;


    protected $messageDelayed = false;


    protected $memberName;

    /**
     * @var Carbon
     */
    protected $time;


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


    /**
     * @param string $keyFobId
     */
    public function setKeyFobId($keyFobId)
    {
        $this->keyFobId = $keyFobId;
    }

    /**
     * @param string $deviceKey
     */
    public function setDeviceKey($deviceKey)
    {
        $this->deviceKey = $deviceKey;
    }

    /**
     * @param string $action
     */
    public function setAction($action)
    {
        $this->action = $action;
    }

    /**
     * @return string
     */
    public function getKeyFobId()
    {
        return $this->keyFobId;
    }

    /**
     * @return
     */
    public function getKeyFob()
    {
        return $this->keyFob;
    }

    /**
     * @return string
     */
    public function getDeviceKey()
    {
        return $this->deviceKey;
    }

    /**
     * @return string
     */
    public function getAction()
    {
        return $this->action;
    }

    /**
     * @return User
     */
    public function getUser()
    {
        return $this->user;
    }

    /**
     * Check a fob id is valid for door entry and return the member if it is
     * @param $keyId
     * @param string $doorName
     * @param $time
     * @return \User
     * @throws ValidationException
     */
    public function verifyForEntry($keyId, $doorName, $time)
    {
        $this->keyFob = $this->lookupKeyFob($keyId);

        $this->setAccessTime($time);

        //Make sure the user is active
        $this->user = $this->keyFob->user;
        if ( ! $this->user || ! $this->user->active) {
            $this->logFailure();
            throw new ValidationException('Not a member');
        }

        if ( ! $this->user->trusted) {
            $this->logFailure();
            throw new ValidationException('Not a keyholder');
        }

        if ( ! $this->user->key_holder) {
            $this->logFailure();
            throw new ValidationException('Not a keyholder');
        }

        if ( ! ($this->user->profile->profile_photo || $this->user->profile->profile_photo_on_wall)) {
            $this->logFailure();
            throw new ValidationException('Member not trusted');
        }

        $this->memberName = $this->user->given_name;


        //Fetch any commands that need to be returned to the device

        return $this->keyFob->user;
    }


    public function lookupKeyFob($keyId)
    {
        try {
            $keyFob = KeyFob::lookup($keyId);
            return $keyFob;
        } catch (\Exception $e) {
            $keyId = substr('BB' . $keyId, 0, 12);
            try {
                $keyFob = KeyFob::lookup($keyId);
            } catch (\Exception $e) {
                throw new ValidationException('Key fob ID not valid');
            }
            return $keyFob;
        }
    }

    /**
     * @param $keyId
     *
     * @return KeyFob
     */
    public function extendedKeyFobLookup($keyId)
    {
        try {
            $keyFob = KeyFob::lookup($keyId);
        } catch (\Exception $e) {
            $oldTagId = substr('BB' . $keyId, 0, 12);
            try {
                $keyFob = KeyFob::lookup($oldTagId);
            } catch (\Exception $e) {

                //The ids coming in will have no checksum (last 2 digits) and the first digit will be incorrect

                //Remove the first character
                $keyId = substr($keyId, 1);

                try {
                    $keyFob = KeyFob::lookupPartialTag($keyId);
                } catch (\Exception $e) {
                    throw new ModelNotFoundException('Key fob ID not found');
                }
            }
        }
        return $keyFob;
    }


    public function logFailure()
    {
        $log               = [];
        $log['key_fob_id'] = $this->keyFob->id;
        $log['user_id']    = $this->user->id;
        $log['service']    = 'main-door';
        $log['delayed']    = $this->messageDelayed;
        $log['response']   = 402;
        $log['created_at'] = $this->time;
        $this->activityRepository->logAccessAttempt($log);

    }

    public function logSuccess()
    {
        event(new MemberActivity($this->keyFob, 'main-door', $this->time, $this->messageDelayed));

        /*
        $activity = $this->activityRepository->recordMemberActivity($this->user->id, $this->keyFob->id, 'main-door', $this->time);

        if ($this->messageDelayed) {
            $activity->delayed = true;
            $activity->save();
        }
        */
    }

    /**
     * @return mixed
     */
    public function getMemberName()
    {
        return $this->memberName;
    }

    /**
     * Set the time to a specific timestamp - the new entry system will be passing a local time with the requests
     * @param null $time
     */
    protected function setAccessTime($time = null)
    {
        if ( ! empty($time)) {
            $this->time = Carbon::createFromTimestamp($time);
        } else {
            $this->time = Carbon::now();
        }
    }

} <file_sep>/app/Validators/ACSValidator.php
<?php namespace BB\Validators;

class ACSValidator extends FormValidator
{

    protected $rules = [
        'device'    => 'required|max:25|exists:acs_nodes,device_id',
        'service'   => 'required|in:entry,usage,consumable,shop,status,device-scanner,sensor',
        'message'   => 'required|in:boot,heartbeat,lookup,start,stop,charge,error,update',
        'tag'       => 'max:15',
        'time'      => 'integer|date_format:U',
        'signature' => '',
        'nonce'     => 'max:12',
    ];

} 
<file_sep>/readme.md
[![Build Status](http://img.shields.io/travis/ArthurGuy/BBMembershipSystem.svg?style=flat-square)](https://travis-ci.org/ArthurGuy/BBMembershipSystem)
[![Code Climate](http://img.shields.io/codeclimate/github/ArthurGuy/BBMembershipSystem.svg?style=flat-square)](https://codeclimate.com/github/ArthurGuy/BBMembershipSystem)
[![Code Quality](http://img.shields.io/scrutinizer/g/ArthurGuy/BBMembershipSystem.svg?style=flat-square)](https://scrutinizer-ci.com/g/ArthurGuy/BBMembershipSystem)

BBMS (Build Brighton Member System)
====================

The Build Brighton membership management system

New members can join and create accounts, payments are tracked and managed through the system and GoCardless


###Features
* Member signup form which collects full name and address, emergency contact and profile photo.
* Direct Debit setup and payment collection through GoCardless
* Regular monthly direct debit payment runs for each user
* PayPal IPN notifications are also received and used to extend member subscriptions.
* The ability for the user to edit all their details allowing for self management
* Various user statuses to cater for active members, members who have left or been banned as well as tracking founders and honorary members
* Handling of the induction/equipment training procedures and collection of payments.
* Tracking of who trains who
* Member grid to see who is a member
* The ability for members to cancel their subscription and leave
* Collect deposit payments for door keys
* Manage member storage box assignments and deposit payments
* RFID door entry control and tracking
* Member credit system for paying for various services
* Member credit topup using direct debit payments and credit/debit card payments
* Member role system for managing delegated duties
* RFID access control for equipment and usage logging
* Auto billing for equipment usage
* Proposal system for member voting
* Equipment/asset management
* Member expense reimbursement


###Member Statuses
There are a variety of member statuses which are used for various scenarios.
* Setting Up - just signed up, no subscription setup, no access to space
* Active
* Suspended - missed payment - dd is still active but the member doesn't have access to the workshop
* Leaving - The user has said they are leaving or they were in a payment warning state
* Left - Leaving users move here once their last payment expires.


###Other Maker spaces
This system can be used with only minor modifications by other spaces.<br />
The Build Brighton naming is hardcoded into the pages and pieces of text will need to be altered.<br />
It has been designed to work primarily with GoCardless but the PayPal integration is OK and would be good enough on its own.<br />
The system also has support for scanning and processing payments from bank statements


###Seting It Up
The system is build on the Laravel 5 framework so familiarity with that would help.

A .env file needs to be setup, please take a look at the example one for the options that are needed.
This file can be renamed by removing the .example from the end.

Composer needs to be available and the install command run to load the required assets.

The storage directory needs to be writable. 

Some of the config options wont be needed.<br />
AWS is used for file storage although a lcal option can be specified.<br />
The system is built for a MySQL DB but a similar system will work<br />
GoCardless as above<br />
MailGun is completely optional<br />
The encryption key is essential<br /><file_sep>/app/Entities/User.php
<?php namespace BB\Entities;

use BB\Exceptions\AuthenticationException;
use BB\Traits\UserRoleTrait;
use Carbon\Carbon;
use DateTime;
use Illuminate\Database\Eloquent\Model;
use Laracasts\Presenter\PresentableTrait;
use Auth;
use Hash;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

/**
 * Class User
 *
 * @property integer $id
 * @property string $email
 * @property string $slack_username
 * @property string $name
 * @property string $given_name
 * @property string $family_name
 * @property string $hash
 * @property bool $active
 * @property bool $key_holder
 * @property bool $trusted
 * @property bool $banned
 * @property bool $email_verified
 * @property bool $induction_completed
 * @property integer $inducted_by
 * @property integer $payment_day
 * @property string $status
 * @property string $payment_method
 * @property string $subscription_id
 * @property Carbon $subscription_expires
 * @property Carbon $banned_date
 * @property string $phone
 * @property integer $storage_box_payment_id
 * @property ProfileData $profile
 * @package BB\Entities
 */
class User extends Model implements AuthenticatableContract, CanResetPasswordContract {

    use UserRoleTrait, PresentableTrait, Authenticatable, CanResetPassword;


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

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

    protected $auditFields = array('induction_completed', 'trusted', 'key_holder');

    protected $presenter = 'BB\Presenters\UserPresenter';


    /**
     * Fillable fields
     *
     * @var array
     */
    protected $fillable = [
        'given_name', 'family_name', 'email', 'secondary_email', 'password', 'emergency_contact', 'phone',
        'monthly_subscription', 'profile_private', 'hash', 'rules_agreed',
        'key_holder', 'key_deposit_payment_id', 'trusted', 'induction_completed', 'payment_method', 'active', 'status'
    ];


    protected $attributes = [
        'status'                => 'setting-up',
        'active'                => 0,
        'key_holder'            => 0,
        'trusted'               => 0,
        'email_verified'        => 0,
        'founder'               => 0,
        'induction_completed'   => 0,
        'payment_day'           => 0,
        'profile_private'       => 0,
        'cash_balance'          => 0,
    ];


    public function getDates()
    {
        return array('created_at', 'updated_at', 'subscription_expires', 'banned_date', 'rules_agreed');
    }


    public static function statuses()
    {
        return [
            'setting-up'        => 'Setting Up',
            'active'            => 'Active',
            'payment-warning'   => 'Payment Warning',
            'suspended'         => 'Suspended',
            'leaving'           => 'Leaving',
            'on-hold'           => 'On Hold',
            'left'              => 'Left',
            'honorary'          => 'Honorary'
        ];
    }



    /*
    |--------------------------------------------------------------------------
    | Relationships
    |--------------------------------------------------------------------------
    |
    | The connections between this model and others
    |
    */

    public function payments()
    {
        return $this->hasMany('\BB\Entities\Payment')->orderBy('created_at', 'desc');
    }

    public function inductions()
    {
        return $this->hasMany('\BB\Entities\Induction');
    }

    public function keyFob()
    {
        return $this->hasMany('\BB\Entities\KeyFob')->where('active', true)->first();
    }

    public function profile()
    {
        return $this->hasOne('\BB\Entities\ProfileData');
    }

    public function address()
    {
        return $this->hasOne('\BB\Entities\Address')->orderBy('approved', 'asc');
    }

    public function notifications()
    {
        return $this->hasMany(Notification::class)->orderBy('created_at', 'desc');
    }



    /*
    |--------------------------------------------------------------------------
    | Attribute Getters and Setters and Model Extensions
    |--------------------------------------------------------------------------
    |
    | Useful properties and methods to have on a user model
    |
    */

    public function getNameAttribute()
    {
        return $this->attributes['given_name'] . ' ' . $this->attributes['family_name'];
    }

    public function setPasswordAttribute($password)
    {
        $this->attributes['password'] = <PASSWORD>::make($password);
    }

    public function setPaymentDayAttribute($value)
    {
        //Ensure the payment date will always exist on any month payment_date
        if ($value > 28) {
            $value = 1;
        }
        $this->attributes['payment_day'] = $value;
    }

    /**
     * Can the user see protected member photos?
     * Only available to active members
     *
     * @return bool
     */
    public function shouldMemberSeeProtectedPhoto()
    {
        switch ($this->attributes['status']) {
            case 'active':
            case 'payment-warning':
            case 'honorary':
                return true;
            default:
                return false;
        }
    }

    /**
     * Is the user on a payment method that allows their subscription amount to be changed
     *
     * @return bool
     */
    public function canMemberChangeSubAmount()
    {
        return in_array($this->attributes['payment_method'], ['gocardless-variable', 'balance']);
    }

    /**
     * Is this user considered a keyholder - can they use the space on their own
     * This may have been replaced by a repo method which checks the photo as well
     *
     * @return bool
     */
    public function keyholderStatus()
    {
        return ($this->active && $this->key_holder && $this->trusted);
    }

    /**
     * Is the user part of the admin group
     *
     * @return bool
     */
    public function isAdmin()
    {
        return Auth::user()->hasRole('admin');
    }

    /**
     * Should GoCardless be promoted to the user
     *
     * @return bool
     */
    public function promoteGoCardless()
    {
        return (($this->payment_method != 'balance' && $this->payment_method != 'gocardless' && $this->payment_method != 'gocardless-variable') && ($this->status == 'active'));
    }

    /**
     * Should we be promoting the new variable gocardless to users?
     *
     * @return bool
     */
    public function promoteVariableGoCardless()
    {
        return (($this->status == 'active') && ($this->payment_method == 'gocardless'));
    }

    /**
     * Is the user eligible for a key?
     *
     * @return bool
     */
    public function promoteGetAKey()
    {
        return ($this->trusted && ! $this->key_holder && ($this->status == 'active'));
    }

    /**
     * Get an array of alerts for the user
     *
     * @return array
     */
    public function getAlerts()
    {
        $alerts = [];
        if ( ! $this->profile->profile_photo && ! $this->profile->new_profile_photo) {
            $alerts[] = 'missing-profile-photo';
        }
        if (empty($this->phone)) {
            $alerts[] = 'missing-phone';
        }
        return $alerts;
    }

    /**
     * @return bool
     */
    public function isBanned()
    {
        return $this->banned;
    }

    /**
     * @return bool
     */
    public function isSuspended()
    {
        return ($this->status == 'suspended');
    }

    /**
     * @return bool
     */
    public function isInducted()
    {
        return $this->induction_completed && $this->inducted_by;
    }

    public function inductedBy()
    {
        return User::findOrFail($this->inducted_by);
    }


    /*
    |--------------------------------------------------------------------------
    | Scopes
    |--------------------------------------------------------------------------
    */

    public function scopeActive($query)
    {
        return $query->whereActive(true);
    }

    public function scopeNotSpecialCase($query)
    {
        return $query->where('status', '!=', 'honorary');
    }

    public function scopeLeaving($query)
    {
        return $query->where('status', '=', 'leaving');
    }

    public function scopePaymentWarning($query)
    {
        return $query->where('status', '=', 'payment-warning');
    }

    public function scopeSuspended($query)
    {
        return $query->where('status', '=', 'suspended');
    }


    /*
    |--------------------------------------------------------------------------
    | Methods
    |--------------------------------------------------------------------------
    */

    /**
     * @param $paymentMethod
     * @param $paymentDay
     * @deprecated
     */
    public function updateSubscription($paymentMethod, $paymentDay)
    {
        if ($paymentDay > 28) {
            $paymentDay = 1;
        }

        $this->attributes['payment_method'] = $paymentMethod;
        $this->attributes['payment_day']    = $paymentDay;

        $this->save();
    }

    /**
     * @param $amount
     * @deprecated
     */
    public function updateSubAmount($amount)
    {
        $this->attributes['monthly_subscription'] = $amount;
        $this->save();
    }

    /**
     * @deprecated
     */
    public function cancelSubscription()
    {
        $this->payment_method = '';
        $this->subscription_id = '';
        $this->payment_day = '';
        $this->status = 'leaving';
        $this->save();
    }

    /**
     * @deprecated
     */
    public function setLeaving()
    {
        $this->status = 'leaving';
        $this->save();
    }

    /**
     * @deprecated
     */
    public function setSuspended()
    {
        $this->status = 'suspended';
        $this->active = false;
        $this->save();
    }

    /**
     * @deprecated
     */
    public function rejoin()
    {
        $this->status = 'setting-up';
        $this->save();
    }

    public function emailConfirmed()
    {
        $this->email_verified = true;
        $this->save();
    }

    /**
     * Fetch a user record, performs a permission check
     *
     * @param integer|null $id
     * @param string       $role
     *
     * @return User
     * @throws AuthenticationException
     */
    public static function findWithPermission($id = null, $role = 'admin')
    {
        if (empty($id)) {
            //Return the logged in user
            return Auth::user();
        }

        $requestedUser = self::findOrFail($id);
        if (Auth::user()->id == $requestedUser->id) {
            //The user they are after is themselves
            return $requestedUser;
        }

        //They are requesting a user that isn't them
        if (Auth::user()->hasRole($role)) {
            //They are an admin so that's alright
            return $requestedUser;
        }

        throw new AuthenticationException();
    }

    public function extendMembership($paymentMethod = null, DateTime $expiry = null)
    {
        if (empty($expiry)) {
            $expiry = Carbon::now()->addMonth();
        }
        $this->status = 'active';
        $this->active = true;
        if ($paymentMethod) {
            $this->payment_method = $paymentMethod;
        }
        $this->subscription_expires = $expiry;
        $this->save();
    }

    /**
     * @return array
     */
    public function getAuditFields()
    {
        return $this->auditFields;
    }


}
<file_sep>/app/Http/Controllers/SubscriptionController.php
<?php namespace BB\Http\Controllers;

use BB\Entities\User;
use BB\Repo\SubscriptionChargeRepository;

class SubscriptionController extends Controller
{


    /**
     * @var SubscriptionChargeRepository
     */
    private $subscriptionChargeRepository;
    /**
     * @var \BB\Repo\UserRepository
     */
    private $userRepository;

    function __construct(\BB\Helpers\GoCardlessHelper $goCardless, SubscriptionChargeRepository $subscriptionChargeRepository, \BB\Repo\UserRepository $userRepository)
    {
        $this->goCardless = $goCardless;
        $this->subscriptionChargeRepository = $subscriptionChargeRepository;
        $this->userRepository = $userRepository;

        $this->middleware('role:member', array('only' => ['create', 'destroy']));
    }

    /**
     * Setup a new pre auth
     *
     * @return \Illuminate\Http\RedirectResponse
     */
    public function create($userId)
    {
        $user = User::findWithPermission($userId);
        $payment_details = array(
            'redirect_uri'      => route('account.subscription.store', $user->id),
            'user'              => [
                'first_name'        =>  $user->given_name,
                'last_name'         =>  $user->family_name,
                'billing_address1'  =>  $user->address->line_1,
                'billing_address2'  =>  $user->address->line_2,
                'billing_town'      =>  $user->address->line_3,
                'billing_postcode'  =>  $user->address->postcode,
                'country_code'      => 'GB'
            ]
        );

        return \Redirect::to($this->goCardless->newPreAuthUrl($payment_details));
    }

    /**
     * Store a newly created resource in storage.
     *
     * @return \Illuminate\Http\RedirectResponse
     */
    public function store($userId)
    {
        $confirm_params = array(
            'resource_id'    => \Request::get('resource_id'),
            'resource_type'  => \Request::get('resource_type'),
            'resource_uri'   => \Request::get('resource_uri'),
            'signature'      => \Request::get('signature'),
        );

        // State is optional
        if (\Request::get('state')) {
            $confirm_params['state'] = \Request::get('state');
        }

        $user = User::findWithPermission($userId);

        try {
            $confirmed_resource = $this->goCardless->confirmResource($confirm_params);
        } catch (\Exception $e) {
            \Notification::error($e->getMessage());
            return \Redirect::route('account.show', $user->id);
        }

        if (strtolower($confirmed_resource->status) != 'active') {
            \Notification::error('Something went wrong, you can try again or get in contact');
            return \Redirect::route('account.show', $user->id);
        }

        $this->userRepository->recordGoCardlessVariableDetails($user->id, $confirmed_resource->id);

        //all we need for a valid member is an active dd so make sure the user account is active
        $this->userRepository->ensureMembershipActive($user->id);

        return \Redirect::route('account.show', [$user->id]);
    }


    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return Illuminate\Http\RedirectResponse
     */
    public function destroy($userId, $id = null)
    {

        /**
         * TODO: Check for and cancel pending sub charges
         */
        $user = User::findWithPermission($userId);
        if ($user->payment_method == 'gocardless') {
            try {
                $subscription = $this->goCardless->cancelSubscription($user->subscription_id);
                if ($subscription->status == 'cancelled') {
                    $user->cancelSubscription();
                    \Notification::success('Your subscription has been cancelled');
                    return \Redirect::back();
                }
            } catch (\GoCardless_ApiException $e) {
                if ($e->getCode() == 404) {
                    $user->cancelSubscription();
                    \Notification::success('Your subscription has been cancelled');
                    return \Redirect::back();
                }
            }
        } elseif ($user->payment_method == 'gocardless-variable') {
            $status = $this->goCardless->cancelPreAuth($user->subscription_id);
            if ($status) {
                $user->subscription_id = null;
                $user->payment_method = '';
                $user->save();

                $user->setLeaving();

                $this->subscriptionChargeRepository->cancelOutstandingCharges($userId);

                \Notification::success('Your direct debit has been cancelled');
                return \Redirect::back();
            }
        }
        \Notification::error('Sorry, we were unable to cancel your subscription, please get in contact');
        return \Redirect::back();
    }


    public function listCharges()
    {
        $charges = $this->subscriptionChargeRepository->getChargesPaginated();
        return \View::make('payments.sub-charges')->with('charges', $charges);
    }

    public function updatePaymentMethod($id)
    {
        $user = User::findWithPermission($id);
        $paymentMethod = \Input::get('payment_method');

        if ($paymentMethod === 'balance' && $user->payment_method != $paymentMethod) {
            $status = $this->goCardless->cancelPreAuth($user->subscription_id);
            if ($status) {
                $user->subscription_id = null;
                $user->payment_method  = 'balance';
                $user->save();

                // If we don't cancel the sub charge the balance process may pick it up
                //$this->subscriptionChargeRepository->cancelOutstandingCharges($userId);
            }
        }

        \Notification::success('Details Updated');
        return \Redirect::route('account.show', [$user->id]);
    }
}
<file_sep>/app/Repo/EquipmentRepository.php
<?php namespace BB\Repo;

use BB\Entities\Equipment;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class EquipmentRepository extends DBRepository
{

    /**
     * @var Equipment
     */
    protected $model;

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

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

    public function getRequiresInduction()
    {
        return $this->model->where('requires_induction', true)->get();
    }

    public function getDoesntRequireInduction()
    {
        return $this->model->where('requires_induction', false)->get();
    }


    public function allPaid()
    {
        return $this->model->where('access_fee', '!=', 0)->get();
    }

    /**
     * Return a device by its slug
     * @param $slug
     * @return Equipment
     */
    public function findBySLug($slug)
    {
        $record = $this->model->where('slug', $slug)->first();
        if ($record) {
            return $record;
        }
        throw new ModelNotFoundException();
    }
} <file_sep>/app/Http/Controllers/ACS/TestController.php
<?php

namespace BB\Http\Controllers\ACS;

use Illuminate\Http\Request;
use BB\Http\Requests;
use BB\Http\Controllers\Controller;

/**
 * @SWG\Info(title="Build Brighton API", version="1")
 * @SWG\SecurityScheme(
 *   securityDefinition="api_key",
 *   type="apiKey",
 *   in="header",
 *   name="api_key"
 * )
 */
class TestController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     *
     * @SWG\Get(
     *     path="/acs/test",
     *     tags={"acs"},
     *     description="Returns an OK message, useful for verifying the access token works",
     *     @SWG\Response(response="200", description="OK")
     * )
     */
    public function index()
    {
        return ['OK'];
    }
}
<file_sep>/app/Http/Controllers/CashPaymentController.php
<?php namespace BB\Http\Controllers;

use BB\Entities\User;

class CashPaymentController extends Controller
{


    /**
     * @var \BB\Repo\PaymentRepository
     */
    private $paymentRepository;
    /**
     * @var \BB\Services\Credit
     */
    private $bbCredit;

    function __construct(\BB\Repo\PaymentRepository $paymentRepository, \BB\Services\Credit $bbCredit)
    {
        $this->paymentRepository = $paymentRepository;
        $this->bbCredit = $bbCredit;
    }


    /**
     * Start the creation of a new gocardless payment
     *   Details get posted into this method and the redirected to gocardless
     *
     * @param $userId
     * @throws \BB\Exceptions\AuthenticationException
     * @throws \BB\Exceptions\FormValidationException
     * @throws \BB\Exceptions\NotImplementedException
     */
    public function store($userId)
    {
        User::findWithPermission($userId);

        $amount     = \Request::get('amount');
        $reason     = \Request::get('reason');
        $sourceId   = \Request::get('source_id');
        $returnPath = \Request::get('return_path');

        $this->paymentRepository->recordPayment($reason, $userId, 'cash', $sourceId, $amount);

        \Notification::success("Payment recorded");

        return \Redirect::to($returnPath);
    }

    /**
     * Remove cash from the users balance
     *
     * @param $userId
     * @return mixed
     * @throws \BB\Exceptions\AuthenticationException
     * @throws \BB\Exceptions\InvalidDataException
     */
    public function destroy($userId)
    {
        $user = User::findWithPermission($userId);
        $this->bbCredit->setUserId($userId);

        $amount     = \Request::get('amount');
        $returnPath = \Request::get('return_path');
        $ref = \Request::get('ref');

        $minimumBalance = $this->bbCredit->acceptableNegativeBalance('withdrawal');

        if (($user->cash_balance + ($minimumBalance * 100)) < ($amount * 100)) {
            \Notification::error("Not enough money");
            return \Redirect::to($returnPath);
        }

        $this->paymentRepository->recordPayment('withdrawal', $userId, 'balance', '', $amount, 'paid', 0, $ref);

        $this->bbCredit->recalculate();

        \Notification::success("Payment recorded");

        return \Redirect::to($returnPath);
    }
}
<file_sep>/app/Listeners/RecordMemberActivity.php
<?php

namespace BB\Listeners;

use BB\Events\MemberActivity;
use BB\Repo\ActivityRepository;use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class RecordMemberActivity
{
    /**
     * @var ActivityRepository
     */
    private $activityRepository;

    /**
     * Create the event listener.
     *
     * @param ActivityRepository $activityRepository
     */
    public function __construct(ActivityRepository $activityRepository)
    {
        $this->activityRepository = $activityRepository;
    }

    /**
     * Handle the event.
     *
     * @param MemberActivity $event
     */
    public function handle(MemberActivity $event)
    {
        $activity = $this->activityRepository->recordMemberActivity($event->keyFob->user->id, $event->keyFob->id, $event->service);

        //The old door entry system may send over historical records, make sure these are marked as such
        if ($event->delayed) {
            $activity->delayed = true;
            $activity->save();
        }
    }
}
<file_sep>/app/Http/Controllers/ActivityController.php
<?php namespace BB\Http\Controllers;

use BB\Entities\Settings;

class ActivityController extends Controller
{

    /**
     * @var \BB\Repo\ActivityRepository
     */
    private $activityRepository;

    function __construct(\BB\Repo\ActivityRepository $activityRepository)
    {
        $this->activityRepository = $activityRepository;
    }

    /**
     * Display a listing of the resource.
     *
     * @return Response
     */
    public function index()
    {
        $date = \Input::get('date', \Carbon\Carbon::now()->format('Y-m-d'));
        $date = \Carbon\Carbon::createFromFormat('Y-m-d', $date)->setTime(0, 0, 0);
        $today = \Carbon\Carbon::now()->setTime(0, 0, 0);

        $logEntries = $this->activityRepository->getForDate($date);

        $nextDate = null;
        if ($date->lt($today)) {
            $nextDate = $date->copy()->addDay();
        }
        $previousDate = $date->copy()->subDay();

        $doorPin = Settings::get('emergency_door_key_storage_pin');

        return \View::make('activity.index')
            ->with('logEntries', $logEntries)
            ->with('date', $date)
            ->with('nextDate', $nextDate)
            ->with('previousDate', $previousDate)
            ->with('doorPin', $doorPin);
    }


    /**
     * @return Response
     */
    public function realtime()
    {
        return \View::make('activity.realtime');
    }



}
<file_sep>/app/Entities/ACSNode.php
<?php namespace BB\Entities;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;

/**
 * Class ACSNode
 *
 * @property string $name
 * @property string $device_id
 * @property string $queued_command
 * @property bool   $monitor_heartbeat
 * @property string $api_key
 * @property Carbon $last_boot
 * @property Carbon $last_heartbeat
 * @property Carbon $created_at
 * @property Carbon $updated_at
 * @package BB\Entities
 */
class ACSNode extends Model
{

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


    /**
     * Fillable fields
     *
     * @var array
     */
    protected $fillable = [
        'last_heartbeat', 'last_boot'
    ];

    public function getDates()
    {
        return array('created_at', 'updated_at', 'last_boot', 'last_heartbeat');
    }

    public function heartbeatWarning()
    {
        //If the last heartbeat was more than an hour ago there is an issue
        if ( $this->monitor_heartbeat && ($this->last_heartbeat < Carbon::now()->subHour())) {
            return true;
        }

        return false;
    }

} <file_sep>/app/Http/Requests/ACS/Activity.php
<?php namespace BB\Http\Requests\ACS;

use Symfony\Component\HttpFoundation\Request;
use Swagger\Annotations as SWG;

/**
 * @SWG\Definition(@SWG\Xml(name="Activity"))
 */
class Activity
{
    /**
     * @SWG\Property(description="The RFID Tag ID")
     * @var string
     */
    private $tagId;

    /**
     * @SWG\Property(description="The String of the device being controlled")
     * @var string
     */
    private $device;

    /**
     * @SWG\Property(description="Date Time of the event, YYYY-MM-DD HH:mm:ss")
     * @var string
     */
    private $occurredAt;

    /**
     * Activity constructor.
     */
    public function __construct(Request $request)
    {
        $this->tagId      = $request->get('tagId');
        $this->device     = $request->get('device');
        $this->occurredAt = $request->get('occurredAt');
    }

    /**
     * @return string
     */
    public function getTagId()
    {
        return $this->tagId;
    }

    /**
     * @return string
     */
    public function getDevice()
    {
        return $this->device;
    }

    /**
     * @return string
     */
    public function getOccurredAt()
    {
        return $this->occurredAt;
    }
}<file_sep>/app/Http/Controllers/AccountController.php
<?php namespace BB\Http\Controllers;


use BB\Entities\Notification;
use BB\Entities\User;
use BB\Events\MemberGivenTrustedStatus;
use BB\Events\MemberPhotoWasDeclined;
use BB\Exceptions\ValidationException;
use BB\Validators\InductionValidator;

class AccountController extends Controller
{

    protected $layout = 'layouts.main';

    protected $userForm;

    /**
     * @var \BB\Helpers\UserImage
     */
    private $userImage;
    /**
     * @var \BB\Validators\UserDetails
     */
    private $userDetailsForm;
    /**
     * @var \BB\Repo\ProfileDataRepository
     */
    private $profileRepo;
    /**
     * @var \BB\Repo\InductionRepository
     */
    private $inductionRepository;
    /**
     * @var \BB\Repo\EquipmentRepository
     */
    private $equipmentRepository;
    /**
     * @var \BB\Repo\UserRepository
     */
    private $userRepository;
    /**
     * @var \BB\Validators\ProfileValidator
     */
    private $profileValidator;
    /**
     * @var \BB\Repo\AddressRepository
     */
    private $addressRepository;
    /**
     * @var \BB\Repo\SubscriptionChargeRepository
     */
    private $subscriptionChargeRepository;


    function __construct(
        \BB\Validators\UserValidator $userForm,
        \BB\Validators\UpdateSubscription $updateSubscriptionAdminForm,
        \BB\Helpers\GoCardlessHelper $goCardless,
        \BB\Helpers\UserImage $userImage,
        \BB\Validators\UserDetails $userDetailsForm,
        \BB\Repo\ProfileDataRepository $profileRepo,
        \BB\Repo\InductionRepository $inductionRepository,
        \BB\Repo\EquipmentRepository $equipmentRepository,
        \BB\Repo\UserRepository $userRepository,
        \BB\Validators\ProfileValidator $profileValidator,
        \BB\Repo\AddressRepository $addressRepository,
        \BB\Repo\SubscriptionChargeRepository $subscriptionChargeRepository,
        \BB\Services\Credit $bbCredit)
    {
        $this->userForm = $userForm;
        $this->updateSubscriptionAdminForm = $updateSubscriptionAdminForm;
        $this->goCardless = $goCardless;
        $this->userImage = $userImage;
        $this->userDetailsForm = $userDetailsForm;
        $this->profileRepo = $profileRepo;
        $this->inductionRepository = $inductionRepository;
        $this->equipmentRepository = $equipmentRepository;
        $this->userRepository = $userRepository;
        $this->profileValidator = $profileValidator;
        $this->addressRepository = $addressRepository;
        $this->subscriptionChargeRepository = $subscriptionChargeRepository;
        $this->bbCredit = $bbCredit;

        //This tones down some validation rules for admins
        $this->userForm->setAdminOverride( ! \Auth::guest() && \Auth::user()->hasRole('admin'));

        $this->middleware('role:member', array('except' => ['create', 'store']));
        $this->middleware('role:admin', array('only' => ['index']));
        //$this->middleware('guest', array('only' => ['create', 'store']));

        $paymentMethods = [
            'gocardless'    => 'GoCardless',
            'paypal'        => 'PayPal',
            'bank-transfer' => 'Manual Bank Transfer',
            'other'         => 'Other'
        ];
        \View::share('paymentMethods', $paymentMethods);
        \View::share('paymentDays', array_combine(range(1, 31), range(1, 31)));

    }

    /**
     * Display a listing of the resource.
     *
     * @return Response
     */
    public function index()
    {
        $sortBy = \Request::get('sortBy');
        $direction = \Request::get('direction', 'asc');
        $showLeft = \Request::get('showLeft', 0);
        $users = $this->userRepository->getPaginated(compact('sortBy', 'direction', 'showLeft'));
        return \View::make('account.index')->withUsers($users);
    }


    /**
     * Show the form for creating a new resource.
     *
     * @return Response
     */
    public function create()
    {
        \View::share('body_class', 'register_login');
        return \View::make('account.create');
    }


    /**
     * Store a newly created resource in storage.
     *
     * @return Illuminate\Http\RedirectResponse
     */
    public function store()
    {
        $input = \Input::only(
            'given_name',
            'family_name',
            'email',
            'secondary_email',
            '<PASSWORD>',
            'phone',
            'address.line_1',
            'address.line_2',
            'address.line_3',
            'address.line_4',
            'address.postcode',
            'monthly_subscription',
            'emergency_contact',
            'new_profile_photo',
            'profile_photo_private',
            'rules_agreed',
            'visited_space'
        );

        $this->userForm->validate($input);
        $this->profileValidator->validate($input);


        $user = $this->userRepository->registerMember($input, ! \Auth::guest() && \Auth::user()->hasRole('admin'));

        if (\Input::file('new_profile_photo')) {
            try {
                $this->userImage->uploadPhoto($user->hash, \Input::file('new_profile_photo')->getRealPath(), true);

                $this->profileRepo->update($user->id, ['new_profile_photo'=>1, 'profile_photo_private'=>$input['profile_photo_private']]);
            } catch (\Exception $e) {
                \Log::error($e);
            }
        }

        //If this isn't an admin user creating the record log them in
        if (\Auth::guest() || ! \Auth::user()->isAdmin()) {
            \Auth::login($user);
        }

        return \Redirect::route('account.show', [$user->id]);
    }


    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        $user = User::findWithPermission($id);

        $inductions = $this->equipmentRepository->getRequiresInduction();

        $userInductions = $user->inductions()->get();
        foreach ($inductions as $i=>$induction) {
            $inductions[$i]->userInduction = false;
            foreach ($userInductions as $userInduction) {
                if ($userInduction->key == $induction->induction_category) {
                    $inductions[$i]->userInduction = $userInduction;
                }
            }
        }

        //get pending address if any
        $newAddress = $this->addressRepository->getNewUserAddress($id);

        //Get the member subscription payments
        $subscriptionCharges = $this->subscriptionChargeRepository->getMemberChargesPaginated($id);

        //Get the members balance
        $this->bbCredit->setUserId($user->id);
        $memberBalance = $this->bbCredit->getBalanceFormatted();

        return \View::make('account.show')
            ->with('user', $user)
            ->with('inductions', $inductions)
            ->with('newAddress', $newAddress)
            ->with('subscriptionCharges', $subscriptionCharges)
            ->with('memberBalance', $memberBalance);
    }


    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function edit($id)
    {
        $user = User::findWithPermission($id);

        //We need to access the address here so its available in the view
        $user->address;

        return \View::make('account.edit')->with('user', $user);
    }


    /**
     * Update the specified resource in storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\RedirectResponse
     */
    public function update($id)
    {
        $user = User::findWithPermission($id);
        $input = \Input::only('given_name', 'family_name', 'email', 'secondary_email', 'password', 'phone', 'address.line_1', 'address.line_2', 'address.line_3', 'address.line_4', 'address.postcode', 'emergency_contact', 'profile_private');

        $this->userForm->validate($input, $user->id);

        $this->userRepository->updateMember($id, $input, \Auth::user()->hasRole('admin'));

        \Notification::success('Details Updated');
        return \Redirect::route('account.show', [$user->id]);
    }



    public function adminUpdate($id)
    {
        $user = User::findWithPermission($id);

        $madeTrusted = false;


        if (\Input::has('trusted')) {
            if ( ! $user->trusted && \Input::get('trusted')) {
                //User has been made a trusted member
                $madeTrusted = true;
            }
            $user->trusted = \Input::get('trusted');
        }

        if (\Input::has('key_holder')) {
            $user->key_holder = \Input::get('key_holder');
        }

        if (\Input::has('induction_completed')) {
            $user->induction_completed = \Input::get('induction_completed');
        }

        if (\Input::has('profile_photo_on_wall')) {
            $profileData = $user->profile()->first();
            $profileData->profile_photo_on_wall = \Input::get('profile_photo_on_wall');
            $profileData->save();
        }

        if (\Input::has('photo_approved')) {
            $profile = $user->profile()->first();

            if (\Input::get('photo_approved')) {
                $this->userImage->approveNewImage($user->hash);
                $profile->update(['new_profile_photo' => false, 'profile_photo' => true]);
            } else {
                $profile->update(['new_profile_photo' => false]);
                event(new MemberPhotoWasDeclined($user));
            }
        }

        $user->save();

        if (\Input::has('approve_new_address')) {
            if (\Input::get('approve_new_address') == 'Approve') {
                $this->addressRepository->approvePendingMemberAddress($id);
            } elseif (\Input::get('approve_new_address') == 'Decline') {
                $this->addressRepository->declinePendingMemberAddress($id);
            }
        }

        if ($madeTrusted) {
            $message = 'You have been made a trusted member at Build Brighton';
            $notificationHash = 'trusted_status';
            Notification::logNew($user->id, $message, 'trusted_status', $notificationHash);
            event(new MemberGivenTrustedStatus($user));
        }


        if (\Request::wantsJson()) {
            return \Response::json('Updated', 200);
        } else {
            \Notification::success('Details Updated');
            return \Redirect::route('account.show', [$user->id]);
        }
    }


    public function alterSubscription($id)
    {
        $user = User::findWithPermission($id);
        $input = \Input::all();

        $this->updateSubscriptionAdminForm->validate($input, $user->id);

        if (($user->payment_method == 'gocardless') && ($input['payment_method'] != 'gocardless')) {
            //Changing away from GoCardless
            $subscription = $this->goCardless->cancelSubscription($user->subscription_id);
            if ($subscription->status == 'cancelled') {
                $user->cancelSubscription();
            }
        }

        $user->updateSubscription($input['payment_method'], $input['payment_day']);

        \Notification::success('Details Updated');
        return \Redirect::route('account.show', [$user->id]);
    }

    public function confirmEmail($id, $hash)
    {
        $user = User::find($id);
        if ($user && $user->hash == $hash) {
            $user->emailConfirmed();
            \Notification::success('Email address confirmed, thank you');
            return \Redirect::route('account.show', $user->id);
        }
        \Notification::error('Error confirming email address');
        return \Redirect::route('home');
    }



    public function destroy($id)
    {
        $user = User::findWithPermission($id);

        //No one will ever leaves the system but we can at least update their status to left.
        $user->setLeaving();

        \Notification::success('Updated status to leaving');
        return \Redirect::route('account.show', [$user->id]);
    }


    public function rejoin($id)
    {
        $user = User::findWithPermission($id);
        $user->rejoin();
        \Notification::success('Details Updated');
        return \Redirect::route('account.show', [$user->id]);
    }

    public function updateSubscriptionAmount($id)
    {
        $amount = \Input::get('monthly_subscription');

        if ($amount < 5) {
            throw new ValidationException('The minimum subscription is 5 GBP');
        } elseif (!\Auth::user()->isAdmin() && ($amount < 15)) {
            throw new ValidationException('The minimum subscription is 15 GBP, please contact the trustees for a lower amount. <EMAIL>');
        }

        $user = User::findWithPermission($id);
        $user->updateSubAmount(\Input::get('monthly_subscription'));
        \Notification::success('Details Updated');
        return \Redirect::route('account.show', [$user->id]);
    }
}
<file_sep>/app/Repo/PaymentRepository.php
<?php namespace BB\Repo;

use BB\Entities\Payment;
use BB\Events\MemberBalanceChanged;
use BB\Exceptions\NotImplementedException;
use BB\Exceptions\PaymentException;
use Carbon\Carbon;

class PaymentRepository extends DBRepository
{

    /**
     * @var Payment
     */
    protected $model;

    public static $SUBSCRIPTION = 'subscription';
    public static $INDUCTION = 'induction';

    private $reason = null;
    private $source = null;

    /**
     * @param Payment $model
     */
    public function __construct(Payment $model)
    {
        $this->model = $model;
        $this->perPage = 10;
    }


    public function getPaginated(array $params)
    {
        $model = $this->model;

        if ($this->hasDateFilter()) {
            $model = $model->where('created_at', '>=', $this->startDate)->where('created_at', '<=', $this->endDate);
        }

        if ($this->hasMemberFilter()) {
            $model = $model->where('user_id', $this->memberId);
        }

        if ($this->hasReasonFilter()) {
            $model = $model->where('reason', $this->reason);
        }

        if ($this->hasSourceFilter()) {
            $model = $model->where('source', $this->source);
        }

        if ($this->isSortable($params)) {
            return $model->orderBy($params['sortBy'], $params['direction'])->paginate($this->perPage);
        }
        return $model->paginate($this->perPage);
    }


    public function getTotalAmount()
    {
        $model = $this->model;

        if ($this->hasDateFilter()) {
            $model = $model->where('created_at', '>=', $this->startDate)->where('created_at', '<=', $this->endDate);
        }

        if ($this->hasMemberFilter()) {
            $model = $model->where('user_id', $this->memberId);
        }

        if ($this->hasReasonFilter()) {
            $model = $model->where('reason', $this->reason);
        }

        if ($this->hasSourceFilter()) {
            $model = $model->where('source', $this->source);
        }

        return $model->get()->sum('amount');
    }


    /**
     * Record a payment against a user record
     *
     * @param string   $reason What was the reason. subscription, induction, etc...
     * @param int      $userId The users ID
     * @param string   $source gocardless, paypal
     * @param string   $sourceId A reference for the source
     * @param double   $amount Amount received before a fee
     * @param string   $status paid, pending, cancelled, refunded
     * @param double   $fee The fee charged by the payment provider
     * @param string   $ref
     * @param Carbon $paidDate
     * @return int The ID of the payment record
     */
    public function recordPayment($reason, $userId, $source, $sourceId, $amount, $status = 'paid', $fee = 0.0, $ref = '', Carbon $paidDate = null)
    {
        if ($paidDate == null) {
            $paidDate = new Carbon();
        }
        //If we have an existing similer record dont create another, except for when there is no source id
        $existingRecord = $this->model->where('source', $source)->where('source_id', $sourceId)->where('user_id', $userId)->first();
        if ( ! $existingRecord || empty($sourceId)) {
            $record                   = new $this->model;
            $record->user_id          = $userId;
            $record->reason           = $reason;
            $record->source           = $source;
            $record->source_id        = $sourceId;
            $record->amount           = $amount;
            $record->amount_minus_fee = ($amount - $fee);
            $record->fee              = $fee;
            $record->status           = $status;
            $record->reference        = $ref;
            if ($status == 'paid') {
                $record->paid_at = $paidDate;
            }
            $record->save();
        } else {
            $record = $existingRecord;
        }

        //Emit an event so that things like the balance updater can run
        \Event::fire('payment.create', array($userId, $reason, $ref, $record->id, $status));

        return $record->id;
    }

    /**
     * Record a subscription payment
     *
     * @param int    $userId The users ID
     * @param string $source gocardless, paypal
     * @param string $sourceId A reference for the source
     * @param double $amount Amount received before a fee
     * @param string $status paid, pending, cancelled, refunded
     * @param double $fee The fee charged by the payment provider
     * @param string|null   $ref
     * @param Carbon $paidDate
     * @return int  The ID of the payment record
     */
    public function recordSubscriptionPayment($userId, $source, $sourceId, $amount, $status = 'paid', $fee = 0.0, $ref = '', Carbon $paidDate = null)
    {
        return $this->recordPayment('subscription', $userId, $source, $sourceId, $amount, $status, $fee, $ref, $paidDate);
    }

    /**
     * An existing payment has been set to paid
     *
     * @param $paymentId
     * @param Carbon $paidDate
     */
    public function markPaymentPaid($paymentId, $paidDate)
    {
        $payment = $this->getById($paymentId);
        $payment->status = 'paid';
        $payment->paid_at = $paidDate;
        $payment->save();

        \Event::fire('payment.paid', array($payment->user_id, $paymentId, $payment->reason, $payment->reference, $paidDate));
    }

    /**
     * Record a payment failure or cancellation
     *
     * @param int    $paymentId
     * @param string $status
     */
    public function recordPaymentFailure($paymentId, $status = 'failed')
    {
        $this->update($paymentId, ['status' => $status]);

        $payment = $this->getById($paymentId);

        \Event::fire('payment.cancelled', array($paymentId, $payment->user_id, $payment->reason, $payment->reference, $status));
    }

    /**
     * Assign an unassigned payment to a user
     *
     * @param int $paymentId
     * @param int $userId
     *
     * @throws PaymentException
     */
    public function assignPaymentToUser($paymentId, $userId)
    {
        $payment = $this->getById($paymentId);

        if (!empty($payment->user_id)) {
            throw new PaymentException('Payment already assigned to user');
        }

        $this->update($paymentId, ['user_id' => $userId]);
    }

    /**
     * Take a payment that has been used for something and reassign it to the balance
     * @param $paymentId
     *
     * @throws NotImplementedException
     */
    public function refundPaymentToBalance($paymentId)
    {
        $payment = $this->getById($paymentId);

        if ($payment->reason === 'donation') {
            $this->update($paymentId, ['reason' => 'balance']);
            event(new MemberBalanceChanged($payment->user_id));
            return;
        }

        if ($payment->reason === 'induction') {
            //This method must only be used if the induction record has been cancelled first
            // otherwise an orphned record will be left behind
            $this->update($paymentId, ['reason' => 'balance']);
            event(new MemberBalanceChanged($payment->user_id));
            return;
        }

        throw new NotImplementedException('This hasn\'t been built yet');
    }


    /**
     * Fetch the users latest payment of a particular type
     * @param integer $userId
     * @param string  $reason
     * @return mixed
     */
    public function latestUserPayment($userId, $reason = 'subscription')
    {
        return $this->model->where('user_id', $userId)
            ->whereRaw('reason = ? and (status = ? or status = ? or status = ?)', [$reason, 'paid', 'pending', 'withdrawn'])
            ->orderBy('created_at', 'desc')
            ->first();
    }


    /**
     * Get all user payments of a specific reason
     * @param $userId
     * @param string $reason
     * @return mixed
     */
    public function getUserPaymentsByReason($userId, $reason)
    {
        return $this->model->where('user_id', $userId)
            ->whereRaw('reason = ? and (status = ? or status = ? or status = ?)', [$reason, 'paid', 'pending', 'withdrawn'])
            ->orderBy('created_at', 'desc')
            ->get();
    }


    /**
     * @param string $source
     */
    public function getUserPaymentsBySource($userId, $source)
    {
        return $this->model->where('user_id', $userId)
            ->whereRaw('source = ? and (status = ? or status = ? or status = ?)', [$source, 'paid', 'pending', 'withdrawn'])
            ->orderBy('created_at', 'desc')
            ->get();
    }

    /**
     * Get all payments with a specific reference
     * @param string $reference
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function getPaymentsByReference($reference)
    {
        return $this->model->where('reference', $reference)->get();
    }

    /**
     * @param string $referencePrefix
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function getEquipmentFeePayments($referencePrefix)
    {
        return $this->model->where('reason', 'equipment-fee')->get()->filter(function($payment) use($referencePrefix) {
            return strpos($payment->reference, ':' . $referencePrefix) !== false;
        });
    }


    /**
     * Return a paginated list of balance affecting payment for a user
     * @param $userId
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function getBalancePaymentsPaginated($userId)
    {
        return $this->model->where('user_id', $userId)
            ->whereRaw('(source = ? or reason = ?) and (status = ? or status = ? or status = ?)', ['balance', 'balance', 'paid', 'pending', 'withdrawn'])
            ->orderBy('created_at', 'desc')
            ->simplePaginate($this->perPage);
    }


    /**
     * Return a collection of payments specifically for storage boxes
     * @param integer $userId
     * @return mixed
     */
    public function getStorageBoxPayments($userId)
    {
        return $this->model->where('user_id', $userId)
            ->whereRaw('reason = ? and (status = ? or status = ? or status = ?)', ['storage-box', 'paid', 'pending', 'withdrawn'])
            ->orderBy('created_at', 'desc')
            ->get();
    }

    public function dateFilter($startDate, $endDate)
    {
        $this->startDate = $startDate;
        $this->endDate = $endDate;
    }

    private function hasDateFilter()
    {
        return ($this->startDate && $this->endDate);
    }

    /**
     * Delete a record
     * @param $recordId
     * @return bool|null
     * @throws \Exception
     */
    public function delete($recordId)
    {
        $payment = $this->getById($recordId);

        $state = $payment->delete();

        //Fire an event, allows the balance to get updated
        \Event::fire('payment.delete', array($payment->user_id, $payment->source, $payment->reason, $payment->id));

        return $state;
    }

    public function canDelete($recordId)
    {
        throw new NotImplementedException();
    }

    /**
     * Used for the getPaginated and getTotalAmount method
     * @param $reasonFilter
     */
    public function reasonFilter($reasonFilter)
    {
        $this->reason = $reasonFilter;
    }

    private function hasReasonFilter()
    {
        return ! is_null($this->reason);
    }

    /**
     * Used for the getPaginated and getTotalAmount method
     * @param string $sourceFilter
     */
    public function sourceFilter($sourceFilter)
    {
        $this->source = $sourceFilter;
    }


    private function hasSourceFilter()
    {
        return ! is_null($this->source);
    }

    /**
     * Used for the getPaginated and getTotalAmount method
     */
    public function resetFilters()
    {
        $this->source = null;
        $this->reason = null;
        $this->memberId = null;
        $this->startDate = null;
        $this->endDate = null;
    }

    /**
     * Fetch a payment record using the id provided by the payment provider
     *
     * @param $sourceId
     * @return Payment
     */
    public function getPaymentBySourceId($sourceId)
    {
        return $this->model->where('source_id', $sourceId)->first();
    }

    /**
     * Record a balance payment transfer between two users
     * 
     * @param integer $sourceUserId
     * @param integer $targetUserId
     * @param double $amount
     */
    public function recordBalanceTransfer($sourceUserId, $targetUserId, $amount)
    {
        $paymentId = $this->recordPayment('transfer', $sourceUserId, 'balance', '', $amount, 'paid', 0, $targetUserId);
        $this->recordPayment('balance', $targetUserId, 'transfer', $paymentId, $amount, 'paid', 0, $sourceUserId);

        //Both of these events aren't needed adn the balance payment fires its own
        // but for the sake of neatness they are here
        event(new MemberBalanceChanged($sourceUserId));
        event(new MemberBalanceChanged($targetUserId));
    }
} <file_sep>/app/Handlers/SubChargeEventHandler.php
<?php namespace BB\Handlers;

use BB\Helpers\MembershipPayments;
use BB\Repo\SubscriptionChargeRepository;
use BB\Repo\UserRepository;
use Carbon\Carbon;

class SubChargeEventHandler
{

    /**
     * @var UserRepository
     */
    private $userRepository;
    /**
     * @var SubscriptionChargeRepository
     */
    private $subscriptionChargeRepository;

    /**
     * @param UserRepository               $userRepository
     * @param SubscriptionChargeRepository $subscriptionChargeRepository
     */
    public function __construct(UserRepository $userRepository, SubscriptionChargeRepository $subscriptionChargeRepository)
    {
        $this->userRepository = $userRepository;
        $this->subscriptionChargeRepository = $subscriptionChargeRepository;
    }


    /**
     * A subscription charge has been marked as paid
     *
     * @param integer $chargeId
     * @param integer $userId
     * @param Carbon  $paymentDate
     * @param double  $amount
     */
    public function onPaid($chargeId, $userId, Carbon $paymentDate, $amount)
    {
        $user = $this->userRepository->getById($userId);
        /** @var $user \BB\Entities\User */

        $user->extendMembership(null, $paymentDate->addMonth());
    }

    /**
     * A subscription charge has been marked as processing
     *
     * @param integer $chargeId
     * @param integer $userId
     * @param Carbon  $paymentDate
     * @param double  $amount
     */
    public function onProcessing($chargeId, $userId, Carbon $paymentDate, $amount)
    {
        $user = $this->userRepository->getById($userId);
        /** @var $user \BB\Entities\User */

        $user->extendMembership(null, $paymentDate->addMonth());
    }

    /**
     * A sub charge has been rolled back as a payment failed
     *
     * @param integer $chargeId
     * @param integer $userId
     * @param Carbon  $paymentDate
     * @param double  $amount
     */
    public function onPaymentFailure($chargeId, $userId, Carbon $paymentDate, $amount)
    {
        $paidUntil = MembershipPayments::lastUserPaymentExpires($userId);
        if ($paidUntil) {
            $user = $this->userRepository->getById($userId);
            /** @var $user \BB\Entities\User */
            $user->extendMembership(null, $paidUntil);
        } else {
            \Log::info('Payment cancelled, expiry date rollback failed as there is no previous payment. User ID:' . $userId);
        }
    }

} <file_sep>/app/Http/Controllers/ACSController.php
<?php namespace BB\Http\Controllers;

use BB\Entities\DetectedDevice;
use BB\Repo\ACSNodeRepository;
use BB\Validators\ACSValidator;

class ACSController extends Controller
{

    /**
     * @var ACSNodeRepository
     */
    private $deviceRepository;
    /**
     * @var ACSValidator
     */
    private $ACSValidator;
    /**
     * @var \BB\Services\KeyFobAccess
     */
    private $keyFobAccess;

    function __construct(
        ACSNodeRepository $acsNodeRepository,
        ACSValidator $ACSValidator,
        \BB\Services\KeyFobAccess $keyFobAccess
    ) {
        $this->acsNodeRepository = $acsNodeRepository;
        $this->ACSValidator     = $ACSValidator;
        $this->keyFobAccess     = $keyFobAccess;
    }

    public function get()
    {

    }

    public function store()
    {
        $data = \Request::only('device', 'service', 'message', 'tag', 'time', 'payload', 'signature', 'nonce');

        //device = the device id from the devices table - unique to each piece of hardware
        //service = what the request is for, entry, usage, consumable
        //message = system message, heartbeat, boot
        //tag = the keyfob id
        //time = the time of the action
        //payload = any extra data relavent to the request
        //signature = an encoded value generated using a secret key - oauth style
        //nonce = a unique value suitable to stop replay attacks

        $this->ACSValidator->validate($data);





        //System messages
        if (in_array($data['message'], ['boot', 'heartbeat'])) {
            return $this->handleSystemCheckIn($data['message'], $data['device'], $data['service']);
        }


        switch ($data['service']) {
            case 'entry':
                return $this->handleDoor($data);
            case 'usage':
                return $this->handleDevice($data);
            case 'consumable':

                break;
            case 'shop':

                break;
            case 'status':
                return $this->returnMemberStatus($data);

                break;
            case 'device-scanner':
                $this->logDetectedDevices($data);
                break;
            default:
                \Log::debug(json_encode($data));
        }

        $responseArray = [
            'time'      => time(),
            'command'   => null,      //stored command for the device to process
            'valid'     => true,      //member request
            'available' => true,      //device status - remote shutdown,
            'member'    => null,      //member name
        ];

    }

    private function handleDoor($data)
    {
        $error = false;

        //Door entry is quite simple - this will just deal with lookups

        try {
            $this->keyFobAccess->verifyForEntry($data['tag'], 'main-door', $data['time']);

            $this->keyFobAccess->logSuccess();
        } catch (\Exception $e) {
            $error = true;
        }

        $cmd = $this->acsNodeRepository->popCommand($data['device']);

        if ( ! $error) {
            $responseData = ['member' => $this->keyFobAccess->getMemberName(), 'valid' => '1', 'cmd' => $cmd];
        } else {
            $responseData = ['valid' => '0', 'cmd' => $cmd];
        }

        return $this->sendResponse($responseData);
    }

    private function handleDevice($data)
    {
        $device = $this->acsNodeRepository->getByName($data['device']);

        if ($data['message'] == 'boot') {
            $this->acsNodeRepository->logBoot($data['device']);
        } elseif ($data['message'] == 'heartbeat') {
            $this->acsNodeRepository->logHeartbeat($data['device']);
        }

        //$member = $this->keyFobAccess->verifyForDevice($data['tag'], 'laser');

        $deviceStatus = 'ok';

        $responseData = ['deviceStatus' => $deviceStatus];

        return $this->sendResponse($responseData);
    }


    /**
     * System checkins are common across all devices
     * Record the time and return pending status messages
     *
     * @param $message
     * @param $device
     * @return Response
     */
    private function handleSystemCheckIn($message, $device, $service)
    {
        switch ($message) {
            case 'boot':
                $this->acsNodeRepository->logBoot($device);
                break;
            case 'heartbeat':
                $this->acsNodeRepository->logHeartbeat($device);
                break;
        }

        //The command comes from the database and will instruct the door entry system to clear its memory if set
        $cmd = $this->acsNodeRepository->popCommand($device);

        switch ($service) {
            case 'entry':
                //we don't have a system for this at the moment but could have a global shutdown option
                $deviceStatus = '1';
                break;
            case 'usage':
                //lookup the piece of equipment from the device id and get the status
                $deviceStatus = '1';
                break;
            default:
                $deviceStatus = '1';
        }

        $responseData = ['cmd' => $cmd, 'deviceStatus' => $deviceStatus];

        return $this->sendResponse($responseData);
    }

    /**
     * Json encode the response data and return
     *
     * @param array $responseData
     * @return \Response
     */
    private function sendResponse(array $responseData)
    {
        $responseData['time'] = time();
        $response             = \Response::json($responseData);
        $response->headers->set('Content-Length', strlen($response->getContent()));

        return $response;
    }

    private function logDetectedDevices($data)
    {
        //this isn't strictly a heartbeat but the updates occur at a regular interval so they will do
        $this->acsNodeRepository->logHeartbeat($data['device']);

        //See if any devices have been detected, if so log them
        foreach (array_keys($data['payload']['bluetooth_devices']) as $macAddress) {
            DetectedDevice::create([
                'type'         => 'bluetooth',
                'mac_address'  => $macAddress,
                'display_name' => $data['payload']['bluetooth_devices'][$macAddress],
            ]);
        }


    }

    private function returnMemberStatus($data)
    {
        try {
            $user = $this->keyFobAccess->verifyForEntry($data['tag'], 'main-door', $data['time']);
        } catch (\Exception $e) {
            $responseData = ['valid' => '0', 'cmd' => ''];
            return $this->sendResponse($responseData);
        }

        $responseData = ['member' => $this->keyFobAccess->getMemberName(), 'valid' => '1', 'cmd' => ''];

        return $this->sendResponse($responseData);
    }

} 