Skip to content

Commit

Permalink
Monthly email report #feature (#4)
Browse files Browse the repository at this point in the history
* ➕ Add dompdf dep

* ✨ Add Report Manager and notification

* ✨ Add task schedule to be last day of every month
  • Loading branch information
saleem-hadad committed Jan 30, 2022
1 parent a6cbe2c commit 8492e24
Show file tree
Hide file tree
Showing 10 changed files with 1,056 additions and 222 deletions.
157 changes: 157 additions & 0 deletions app/BusinessLogic/ReportManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

namespace App\BusinessLogic;

use App\Contracts\ReportManager as ReportManagerContract;
use App\Domain\Ranges\CurrentMonth;
use App\Domain\Ranges\LastMonth;
use App\Models\Brand;
use App\Models\Category;
use App\Models\Transaction;

class ReportManager implements ReportManagerContract
{
protected $data = [];

public function generate()
{
$newBrands = Brand::whereBetween('created_at', [now()->startOfMonth(), now()->endOfMonth()])->pluck('name');

$currentMonthRange = new CurrentMonth;
$lastMonthRange = new LastMonth;

$this->addSection('Overview', $this->getOverviewData());

foreach(Category::all() as $category)
{
$brandsData = [];

foreach($category->brands as $brand) {
$totalCurrentMonth = $brand->transactions()->whereBetween('created_at', [$currentMonthRange->start(), $currentMonthRange->end()])->sum('amount');
$totalLastMonth = $brand->transactions()->whereBetween('created_at', [$lastMonthRange->start(), $lastMonthRange->end()])->sum('amount');
$change = ! $totalLastMonth ? '-' : number_format(($totalCurrentMonth / $totalLastMonth - 1) * 100, 2);

$brandsData[] = [
'name' => $brand->name,
'total_current_month' => $totalCurrentMonth,
'total_previous_month' => $totalLastMonth,
'change' => $change,
'change_color' => $this->getChangeColor($change, $category->type),
'is_new' => $newBrands->contains($brand->name)
];
}

$brandsData = $this->calculateAndAddAllBrandsData($brandsData, $category);

$this->addSection($category->name, $brandsData);
}

return $this->data;
}

protected function addSection($sectionName, $data)
{
$this->data[$sectionName] = $data;
}

protected function getChangeColor($change, $type)
{
if($change == '-') {
return 'gray';
}

if($type == Category::INCOME) {
return $change >= 0 ? 'green' : 'red';
}

return $change >= 0 ? 'red' : 'green';
}

protected function calculateAndAddAllBrandsData($brandsData, $category)
{
$allCurrentMonth = array_reduce($brandsData, function ($carry, $item) {
$carry += $item['total_current_month'];

return $carry;
});

$allLastMonth = array_reduce($brandsData, function ($carry, $item) {
$carry += $item['total_previous_month'];

return $carry;
});

$change = ! $allLastMonth ? '-' : number_format(($allCurrentMonth / $allLastMonth - 1) * 100, 2);

return array_merge([[
'name' => 'All',
'total_current_month' => $allCurrentMonth,
'total_previous_month' => $allLastMonth,
'change' => $change,
'change_color' => $this->getChangeColor($change, $category->type)
]], $brandsData);
}

protected function getOverviewData()
{
return [
$this->getTotalCash(),
$this->getTotalIncome(),
$this->getTotalExpenses(),
];
}

protected function getTotalCash()
{
$totalIncome = Transaction::income()->sum('amount');
$totalExpenses = Transaction::expenses()->sum('amount');

$totalIncomeExcludingThisMonth = Transaction::income()->where('created_at', '<', now()->startOfMonth())->sum('amount');
$totalExpensesExcludingThisMonth = Transaction::expenses()->where('created_at', '<', now()->startOfMonth())->sum('amount');

$totalCashTillNow = $totalIncome - $totalExpenses;
$totalCashExcludingThisMonth = $totalIncomeExcludingThisMonth - $totalExpensesExcludingThisMonth;

$change = ! $totalCashExcludingThisMonth ? '-' : number_format(($totalCashTillNow / $totalCashExcludingThisMonth - 1) * 100, 2);

return [
'name' => 'Total Cash',
'total_current_month' => $totalCashTillNow,
'total_previous_month' => $totalCashExcludingThisMonth,
'change' => $change,
'change_color' => $this->getChangeColor($change, 'INCOME')
];
}

protected function getTotalIncome()
{
$total = Transaction::income()->whereBetween('created_at', [now()->startOfMonth(), now()->endOfMonth()])->sum('amount');
$totalExcludingThisMonth = Transaction::income()->whereBetween('created_at', [now()->subMonth()->startOfMonth(), now()->subMonth()->endOfMonth()])->sum('amount');

$change = ! $totalExcludingThisMonth ? '-' : number_format(($total / $totalExcludingThisMonth - 1) * 100, 2);

return [
'name' => 'Total Income',
'total_current_month' => $total,
'total_previous_month' => $totalExcludingThisMonth,
'change' => $change,
'change_color' => $this->getChangeColor($change, 'INCOME')
];
}

protected function getTotalExpenses()
{
$total = Transaction::expenses()->whereBetween('created_at', [now()->startOfMonth(), now()->endOfMonth()])->sum('amount');
$totalExcludingThisMonth = Transaction::expenses()->whereBetween('created_at', [now()->subMonth()->startOfMonth(), now()->subMonth()->endOfMonth()])->sum('amount');

$change = ! $totalExcludingThisMonth ? '-' : number_format(($total / $totalExcludingThisMonth - 1) * 100, 2);

return [
'name' => 'Total Expenses',
'total_current_month' => $total,
'total_previous_month' => $totalExcludingThisMonth,
'change' => $change,
'change_color' => $this->getChangeColor($change, 'EXPENSES')
];
}
}
46 changes: 46 additions & 0 deletions app/Console/Commands/ReportCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace App\Console\Commands;

use App\Models\User;
use Illuminate\Console\Command;
use App\Notifications\FinanceMonthlyReportNotification;

class ReportCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'finance:report';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Report the finance data for the current month';

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

/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
User::first()->notify(new FinanceMonthlyReportNotification);

return 0;
}
}
2 changes: 1 addition & 1 deletion app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();
$schedule->command('finance:report')->onOneServer()->lastDayOfMonth('23:00');
}

/**
Expand Down
8 changes: 8 additions & 0 deletions app/Contracts/ReportManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace App\Contracts;

interface ReportManager
{
public function generate();
}
69 changes: 69 additions & 0 deletions app/Notifications/FinanceMonthlyReportNotification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace App\Notifications;

use App\Contracts\ReportManager;
use App\Services\PdfRenderer;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class FinanceMonthlyReportNotification extends Notification
{
use Queueable;

/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}

/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}

/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$data = [
'sections' => app(ReportManager::class)->generate(),
'currency' => config('finance.currency'),
'month' => now()->format('M Y')
];

return (new MailMessage)
->subject('Finance report for ' . $data['month'])
->line('Please find the finance report attachment for ' . $data['month'])
->attachData(PdfRenderer::render('report', $data), 'finance-report.pdf');
}

/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}
3 changes: 3 additions & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
use App\BusinessLogic\SmsParser;
use Illuminate\Support\ServiceProvider;
use App\BusinessLogic\SmsTemplateDetector;
use App\BusinessLogic\ReportManager;
use App\BusinessLogic\SmsTransactionProcessor;
use App\Contracts\SmsParser as SmsParserContract;
use App\Contracts\SmsTemplateDetector as SmsTemplateDetectorContract;
use App\Contracts\ReportManager as ReportManagerContract;
use App\Contracts\SmsTransactionProcessor as SmsTransactionProcessorContract;
use App\Domain\Ranges\CurrentMonth;
use App\Domain\Ranges\CurrentYear;
Expand All @@ -27,6 +29,7 @@ public function register()
$this->app->bind(SmsParserContract::class, SmsParser::class);
$this->app->bind(SmsTemplateDetectorContract::class, SmsTemplateDetector::class);
$this->app->bind(SmsTransactionProcessorContract::class, SmsTransactionProcessor::class);
$this->app->bind(ReportManagerContract::class, ReportManager::class);

// TODO: find a better way to load classes based on base class Range
$this->app->bind('findRangeByKey', function($_, $params) {
Expand Down
28 changes: 28 additions & 0 deletions app/Services/PdfRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\Services;

use Dompdf\Dompdf;
use Dompdf\Options;
use Illuminate\Support\Facades\View;

class PdfRenderer
{
public static function render($view, $data)
{
if (! defined('DOMPDF_ENABLE_AUTOLOAD')) {
define('DOMPDF_ENABLE_AUTOLOAD', false);
}

$dompdfOptions = new Options();
$dompdfOptions->setChroot(base_path());

$dompdf = new Dompdf($dompdfOptions);
$dompdf->setPaper('A4');

$dompdf->loadHtml(View::make($view, $data)->render());
$dompdf->render();

return (string) $dompdf->output();
}
}
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"license": "MIT",
"require": {
"php": "^7.3|^8.0",
"aws/aws-sdk-php": "^3.199",
"binarytorch/larecipe": "^2.4",
"dompdf/dompdf": "^1.1",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"inertiajs/inertia-laravel": "^0.4.5",
Expand Down

0 comments on commit 8492e24

Please sign in to comment.