Invoicing Feature Complete #8

Merged
okorpheus merged 7 commits from invoicing into master 2024-06-29 15:27:15 +00:00
12 changed files with 327 additions and 68 deletions
Showing only changes of commit 830b7362a4 - Show all commits

View File

@ -2,13 +2,20 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\Invoice\InvoiceDataService;
use Illuminate\Support\Facades\Auth;
use function dd;
use function redirect;
class DashboardController extends Controller
{
protected InvoiceDataService $invoiceService;
public function __construct(InvoiceDataService $invoiceService)
{
$this->invoiceService = $invoiceService;
}
public function profile()
{
return view('dashboard.profile');
@ -22,10 +29,23 @@ class DashboardController extends Controller
public function my_school()
{
if (Auth::user()->school) {
return redirect('/schools/' . Auth::user()->school->id);
return redirect('/schools/'.Auth::user()->school->id);
}
$possibilities = Auth::user()->possibleSchools();
if (count($possibilities) < 1) return view('schools.create');
if (count($possibilities) < 1) {
return view('schools.create');
}
return view('dashboard.select_school', ['possibilities' => $possibilities]);
}
public function my_invoice()
{
if (! Auth::user()->school_id) {
return redirect()->route('dashboard')->with('error', 'You do not have a school to get an invoice for');
}
$invoiceData = $this->invoiceService->allData(Auth::user()->school_id);
$school = Auth::user()->school;
return view('dashboard.invoice', compact('school', 'invoiceData'));
}
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Services\AuditionService;
use App\Services\Invoice\InvoiceDataService;
use App\Services\TabulationService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
@ -11,16 +12,22 @@ class TestController extends Controller
{
protected $scoringGuideCacheService;
protected $tabulationService;
protected $invoiceService;
public function __construct(AuditionService $scoringGuideCacheService, TabulationService $tabulationService)
{
public function __construct(
AuditionService $scoringGuideCacheService,
TabulationService $tabulationService,
InvoiceDataService $invoiceService
) {
$this->scoringGuideCacheService = $scoringGuideCacheService;
$this->tabulationService = $tabulationService;
$this->invoiceService = $invoiceService;
}
public function flashTest(Request $request)
{
$auditions = $this->tabulationService->getAuditionsWithStatus();
return view('test', compact('auditions'));
$lines = $this->invoiceService->getLines(12);
$totalFees = $this->invoiceService->getGrandTotal(12);
return view('test', compact('lines','totalFees'));
}
}

View File

@ -17,25 +17,28 @@ class School extends Model
{
return $this->hasMany(User::class);
}
public function users(): HasMany
{
return $this->hasMany(User::class);
}
public function emailDomains(): HasMany
{
return $this->hasMany(SchoolEmailDomain::class);
}
public function initialLetterImageURL($bg_color = '4f46e5', $text_color='fff'): string
public function initialLetterImageURL($bg_color = '4f46e5', $text_color = 'fff'): string
{
$img = "https://ui-avatars.com/api/?background=$bg_color&color=$text_color&name=";
$img .= substr($this->name,0,1);
$img .= substr($this->name, 0, 1);
return $img;
}
public function students(): HasMany
{
return $this->hasMany(Student::class);
return $this->hasMany(Student::class)->orderBy('last_name')->orderBy('first_name');
}
public function entries(): HasManyThrough
@ -48,5 +51,4 @@ class School extends Model
'id',
'id');
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Providers;
use App\Services\EntryService;
use App\Services\Invoice\InvoiceDataService;
use App\Services\Invoice\InvoiceOneFeePerEntry;
use App\Services\Invoice\InvoiceOneFeePerStudent;
use Illuminate\Support\ServiceProvider;
use function auditionSetting;
class InvoiceDataServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$this->app->singleton(InvoiceDataService::class, function ($app) {
// Default binding, can be overridden in booth method
return new InvoiceOneFeePerEntry($app->make(EntryService::class));
});
}
/**
* Bootstrap services.
*/
public function boot(): void
{
$this->app->singleton(InvoiceDataService::class, function ($app) {
return match (auditionSetting('fee_structure')) {
'oneFeePerEntry' => new InvoiceOneFeePerEntry($app->make(EntryService::class)),
'oneFeePerStudent' => new InvoiceOneFeePerStudent($app->make(EntryService::class)),
default => throw new \Exception('Unknown Invoice Method'),
};
});
}
}

View File

@ -1,47 +0,0 @@
<?php
namespace App\Services\Invoice;
use App\Models\School;
use function auditionSetting;
class InvoiceAllStudentsPay implements InvoiceDataService
{
/**
* Create a new class instance.
*/
public function __construct()
{
//
}
public function getLines(School $school)
{
// TODO: Implement getLines() method.
}
public function getLinesTotal(School $school)
{
// TODO: Implement getLinesTotal() method.
}
public function getLateFeesTotal(School $school)
{
// TODO: Implement getLateFeesTotal() method.
}
public function getSchoolFeeTotal(School $school)
{
if (! auditionSetting('school_fee')) {
return 0;
}
return auditionSetting('school_fee');
}
public function getGrandTotal(School $school)
{
// TODO: Implement getGrandTotal() method.
}
}

View File

@ -2,17 +2,17 @@
namespace App\Services\Invoice;
use App\Models\School;
interface InvoiceDataService
{
public function getLines(School $school);
public function allData($schoolId);
public function getLinesTotal(School $school);
public function getLines($schoolId);
public function getLateFeesTotal(School $school);
public function getLinesTotal($schoolId);
public function getSchoolFeeTotal(School $school);
public function getLateFeesTotal($schoolId);
public function getGrandTotal(School $school);
public function getSchoolFeeTotal($schoolId);
public function getGrandTotal($schoolId);
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Services\Invoice;
use App\Models\School;
use App\Services\EntryService;
use Illuminate\Support\Arr;
use function auditionSetting;
class InvoiceOneFeePerEntry implements InvoiceDataService
{
/**
* Create a new class instance.
*/
protected $entryService;
public function __construct(EntryService $entryService)
{
$this->entryService = $entryService;
}
public function allData($schoolId)
{
static $schoolInvoiceData = [];
if (Arr::has($schoolInvoiceData, $schoolId)) {
return $schoolInvoiceData[$schoolId];
}
$school = School::findOrFail($schoolId);
$invoiceData['lines'] = [];
$invoiceData['linesTotal'] = 0;
$invoiceData['lateFeesTotal'] = 0;
/** @noinspection PhpArrayIndexImmediatelyRewrittenInspection */
$invoiceData['grandTotal'] = 0;
$entries = $school->entries()->with('audition')->orderBy('created_at', 'desc')->get()->groupBy('student_id');
foreach ($school->students as $student) {
foreach ($entries[$student->id] as $entry) {
$entryFee = $entry->audition->entry_fee / 100;
$lateFee = $this->entryService->entryIsLate($entry) ? auditionSetting('late_fee') / 100 : 0;
$invoiceData['lines'][] = [
'student_name' => $student->full_name(true),
'audition' => $entry->audition->name,
'entry_timestamp' => $entry->created_at,
'entry_fee' => $entryFee,
'late_fee' => $lateFee,
];
$invoiceData['linesTotal'] += $entryFee;
$invoiceData['lateFeesTotal'] += $lateFee;
}
}
// School Fee Total
if (! auditionSetting('school_fee')) {
$invoiceData['schoolFeeTotal'] = 0;
} else {
$invoiceData['schoolFeeTotal'] = auditionSetting('school_fee') / 100;
}
$invoiceData['grandTotal'] = $invoiceData['linesTotal'] + $invoiceData['lateFeesTotal'] + $invoiceData['schoolFeeTotal'];
$schoolInvoiceData[$school->id] = $invoiceData;
return $invoiceData;
}
public function getLines($schoolId)
{
return $this->allData($schoolId)['lines'];
}
public function getLinesTotal($schoolId)
{
return $this->allData($schoolId)['linesTotal'];
}
public function getLateFeesTotal($schoolId)
{
return $this->allData($schoolId)['lateFeesTotal'];
}
public function getSchoolFeeTotal($schoolId)
{
return $this->allData($schoolId)['schoolFeeTotal'];
}
public function getGrandTotal($schoolId)
{
return $this->allData($schoolId)['grandTotal'];
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace App\Services\Invoice;
use App\Models\School;
use App\Services\EntryService;
use Illuminate\Support\Arr;
use function auditionSetting;
class InvoiceOneFeePerStudent implements InvoiceDataService
{
/**
* Create a new class instance.
*/
protected $entryService;
public function __construct(EntryService $entryService)
{
$this->entryService = $entryService;
}
public function allData($schoolId)
{
static $schoolInvoiceData = [];
if (Arr::has($schoolInvoiceData, $schoolId)) {
return $schoolInvoiceData[$schoolId];
}
$school = School::findOrFail($schoolId);
$invoiceData['lines'] = [];
$invoiceData['linesTotal'] = 0;
$invoiceData['lateFeesTotal'] = 0;
/** @noinspection PhpArrayIndexImmediatelyRewrittenInspection */
$invoiceData['grandTotal'] = 0;
$entries = $school->entries()->with('audition')->orderBy('created_at', 'desc')->get()->groupBy('student_id');
foreach ($school->students as $student) {
$firstEntryForStudent = true;
foreach ($entries[$student->id] as $entry) {
if ($firstEntryForStudent) {
$entryFee = $entry->audition->entry_fee / 100;
$lateFee = $this->entryService->entryIsLate($entry) ? auditionSetting('late_fee') / 100 : 0;
} else {
$entryFee = 0;
$lateFee = 0;
}
$invoiceData['lines'][] = [
'student_name' => $student->full_name(true),
'audition' => $entry->audition->name,
'entry_timestamp' => $entry->created_at,
'entry_fee' => $entryFee,
'late_fee' => $lateFee,
];
$invoiceData['linesTotal'] += $entryFee;
$invoiceData['lateFeesTotal'] += $lateFee;
$firstEntryForStudent = false;
}
}
// School Fee Total
if (! auditionSetting('school_fee')) {
$invoiceData['schoolFeeTotal'] = 0;
} else {
$invoiceData['schoolFeeTotal'] = auditionSetting('school_fee') / 100;
}
$invoiceData['grandTotal'] = $invoiceData['linesTotal'] + $invoiceData['lateFeesTotal'] + $invoiceData['schoolFeeTotal'];
$schoolInvoiceData[$school->id] = $invoiceData;
return $invoiceData;
}
public function getLines($schoolId)
{
return $this->allData($schoolId)['lines'];
}
public function getLinesTotal($schoolId)
{
return $this->allData($schoolId)['linesTotal'];
}
public function getLateFeesTotal($schoolId)
{
return $this->allData($schoolId)['lateFeesTotal'];
}
public function getSchoolFeeTotal($schoolId)
{
return $this->allData($schoolId)['schoolFeeTotal'];
}
public function getGrandTotal($schoolId)
{
return $this->allData($schoolId)['grandTotal'];
}
}

View File

@ -3,4 +3,5 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\FortifyServiceProvider::class,
App\Providers\InvoiceDataServiceProvider::class,
];

View File

@ -0,0 +1,44 @@
@props(['school', 'invoiceData'])
<x-layout.app>
<x-slot:page_title>Invoice - {{ $school->name }}</x-slot:page_title>
@php( dump($invoiceData ))
<x-table.table class="mt-6">
<x-slot:title>Invoice</x-slot:title>
<thead>
<tr>
<x-table.th>Student Name</x-table.th>
<x-table.th>Audition</x-table.th>
<x-table.th>Entry Timestamp</x-table.th>
<x-table.th>Entry Fee</x-table.th>
<x-table.th>Late Fee</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($invoiceData['lines'] as $line)
<tr>
<x-table.td>{{ $line['student_name'] }}</x-table.td>
<x-table.td>{{ $line['audition'] }}</x-table.td>
<x-table.td>{{ $line['entry_timestamp']->setTimezone('America/Chicago')->format('m/d/Y g:i:s A') }}</x-table.td>
<x-table.td>${{ number_format($line['entry_fee'],2) }}</x-table.td>
<x-table.td>${{ number_format($line['late_fee'],2) }}</x-table.td>
</tr>
@endforeach
</x-table.body>
<tfoot>
<tr>
<x-table.th colspan="3" class="text-right">Totals</x-table.th>
<x-table.th>${{ number_format($invoiceData['linesTotal'],2) }}</x-table.th>
<x-table.th>${{ number_format($invoiceData['lateFeesTotal'],2) }}</x-table.th>
</tr>
<tr>
<x-table.th colspan="4" class="text-right">School Fee</x-table.th>
<x-table.th >${{ number_format($invoiceData['schoolFeeTotal'],2) }}</x-table.th>
</tr>
<tr>
<x-table.th colspan="4" class="text-right">Grand Total</x-table.th>
<x-table.th >${{ number_format($invoiceData['grandTotal'],2) }}</x-table.th>
</tr>
</tfoot>
</x-table.table>
</x-layout.app>

View File

@ -16,9 +16,9 @@
<x-layout.app>
<x-slot:page_title>Test Page</x-slot:page_title>
@php
$schools = School::all();
dump($totalFees);
dump($lines);
}
@endphp

View File

@ -12,6 +12,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'dashboard'])->name('dashboard');
Route::get('/profile', [DashboardController::class, 'profile']);
Route::get('/my_school', [DashboardController::class, 'my_school']);
Route::get('/my_invoice', [DashboardController::class, 'my_invoice'])->name('my_invoice');
});
// Entry Related Routes