From e04689acca1931dfb5e8fc0f0ab09fa980c5a5f4 Mon Sep 17 00:00:00 2001 From: Jp Date: Mon, 16 Feb 2026 01:22:00 +0800 Subject: [PATCH 1/3] refactor(sales): replace service with command and action pattern - Replace SaleService with CreateSaleCommand and CreateSaleAction for better separation of concerns - Move sale creation logic into dedicated command class following command pattern - Update CreateSale.php to use new action instead of direct service call - Wrap sale creation in database transaction for data consistency --- app/Actions/Sales/CreateSaleAction.php | 22 +++++++++++++ app/Commands/Sales/CreateSaleCommand.php | 27 ++++++++++++++++ .../SaleResource/Pages/CreateSale.php | 3 +- app/Services/Sales/SaleService.php | 31 ------------------- 4 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 app/Actions/Sales/CreateSaleAction.php create mode 100644 app/Commands/Sales/CreateSaleCommand.php delete mode 100644 app/Services/Sales/SaleService.php diff --git a/app/Actions/Sales/CreateSaleAction.php b/app/Actions/Sales/CreateSaleAction.php new file mode 100644 index 0000000..6f46b0f --- /dev/null +++ b/app/Actions/Sales/CreateSaleAction.php @@ -0,0 +1,22 @@ +createSaleCommand->execute($data); + } +} diff --git a/app/Commands/Sales/CreateSaleCommand.php b/app/Commands/Sales/CreateSaleCommand.php new file mode 100644 index 0000000..e8f0719 --- /dev/null +++ b/app/Commands/Sales/CreateSaleCommand.php @@ -0,0 +1,27 @@ +id, + ); + + return Sale::create($tData->toArray()); + }); + + } +} diff --git a/app/Filament/Resources/SaleResource/Pages/CreateSale.php b/app/Filament/Resources/SaleResource/Pages/CreateSale.php index 2278109..189f1fb 100644 --- a/app/Filament/Resources/SaleResource/Pages/CreateSale.php +++ b/app/Filament/Resources/SaleResource/Pages/CreateSale.php @@ -2,6 +2,7 @@ namespace App\Filament\Resources\SaleResource\Pages; +use App\Actions\Sales\CreateSaleAction; use App\Actions\Transactions\CreateTransactionAction; use App\DataObjects\CreateTransactionDTO; use App\Filament\Resources\ClientResource; @@ -83,7 +84,7 @@ class CreateSale extends CreateRecord { try { DB::beginTransaction(); - $record = app(SaleService::class)->create($this->getFormDataMutation($data)); + $record = app(CreateSaleAction::class)($this->getFormDataMutation($data)); $branch = $record->branch; foreach ($transactions as $transaction) { diff --git a/app/Services/Sales/SaleService.php b/app/Services/Sales/SaleService.php deleted file mode 100644 index 0f51c61..0000000 --- a/app/Services/Sales/SaleService.php +++ /dev/null @@ -1,31 +0,0 @@ -id, - ); - return Sale::create($tData->toArray()); - } -} From 7bcfaff31132ab90549fa871c2103b32137d4570 Mon Sep 17 00:00:00 2001 From: Jp Date: Mon, 16 Feb 2026 01:48:25 +0800 Subject: [PATCH 2/3] refactor: extract transaction creation and account syncing to actions - Introduce CreateRecordTransactionsAction to handle transaction creation for any model - Introduce SyncAccountsAction to encapsulate account synchronization logic - Refactor CreateSaleAction to use new actions and handle full sale creation flow - Simplify CreateExpense and CreateSale pages by delegating to actions - Ensure proper transaction handling with database rollback on failure --- app/Actions/Sales/CreateSaleAction.php | 45 ++++++++++++++++--- app/Actions/Sales/SyncAccountsAction.php | 16 +++++++ .../CreateRecordTransactionsAction.php | 30 +++++++++++++ .../ExpenseResource/Pages/CreateExpense.php | 23 +--------- .../SaleResource/Pages/CreateSale.php | 41 ++--------------- 5 files changed, 90 insertions(+), 65 deletions(-) create mode 100644 app/Actions/Sales/SyncAccountsAction.php create mode 100644 app/Actions/Transactions/CreateRecordTransactionsAction.php diff --git a/app/Actions/Sales/CreateSaleAction.php b/app/Actions/Sales/CreateSaleAction.php index 6f46b0f..77ccf50 100644 --- a/app/Actions/Sales/CreateSaleAction.php +++ b/app/Actions/Sales/CreateSaleAction.php @@ -2,21 +2,54 @@ namespace App\Actions\Sales; +use App\Actions\Transactions\CreateRecordTransactionsAction; use App\Commands\Sales\CreateSaleCommand; +use App\Commands\Series\CreateSeriesCommand; use App\Models\Sale; +use Illuminate\Support\Facades\DB; class CreateSaleAction { /** * Create a new class instance. */ - public function __construct(private CreateSaleCommand $createSaleCommand) - { - // - } + public function __construct( + private CreateSaleCommand $createSaleCommand, + private CreateSeriesCommand $createSeriesCommand, + ){ } - public function __invoke(array $data): Sale + public function __invoke(array $data, array $transactions): Sale { - return $this->createSaleCommand->execute($data); + $record = $this->createSaleCommand->execute($data); + + try { + DB::beginTransaction(); + + //create transactions for the sale + app(CreateRecordTransactionsAction::class)($record, $transactions); + + $accountIds = collect($transactions) + ->pluck('account_id') + ->filter() + ->unique() + ->values() + ->all(); + + //sync accounts to sale + app(SyncAccountsAction::class)($record, $accountIds); + + //increment current_series + $this->createSeriesCommand->execute([ + 'branch_id' => $record->branch_id, + 'series' => $record->reference_number, + ]); + + DB::commit(); + } catch (\Exception $exception) { + DB::rollBack(); + throw new \Exception('Failed to save transactions : '.$exception->getMessage()); + } + + return $record; } } diff --git a/app/Actions/Sales/SyncAccountsAction.php b/app/Actions/Sales/SyncAccountsAction.php new file mode 100644 index 0000000..95277bc --- /dev/null +++ b/app/Actions/Sales/SyncAccountsAction.php @@ -0,0 +1,16 @@ +accounts()->sync($accounts); + }, attempts: 2); + } +} diff --git a/app/Actions/Transactions/CreateRecordTransactionsAction.php b/app/Actions/Transactions/CreateRecordTransactionsAction.php new file mode 100644 index 0000000..f855086 --- /dev/null +++ b/app/Actions/Transactions/CreateRecordTransactionsAction.php @@ -0,0 +1,30 @@ + $record->branch_id, + 'happened_on' => $record->happened_on, + ...$transaction, + ]; + + $payload = new CreateTransactionDTO(data: $tData, transactionable: $record); + + Pipeline::send(passable: $payload)->through( + [ + CreateTransactionAction::class, + ] + )->thenReturn(); + } + } +} + diff --git a/app/Filament/Resources/ExpenseResource/Pages/CreateExpense.php b/app/Filament/Resources/ExpenseResource/Pages/CreateExpense.php index 8e0acc6..e133af8 100644 --- a/app/Filament/Resources/ExpenseResource/Pages/CreateExpense.php +++ b/app/Filament/Resources/ExpenseResource/Pages/CreateExpense.php @@ -2,15 +2,13 @@ namespace App\Filament\Resources\ExpenseResource\Pages; -use App\Actions\Transactions\CreateTransactionAction; -use App\DataObjects\CreateTransactionDTO; +use App\Actions\Transactions\CreateRecordTransactionsAction; use App\Filament\Resources\ClientResource; use App\Filament\Resources\ExpenseResource; use App\Models\Client; use Exception; use Filament\Resources\Pages\CreateRecord; use Illuminate\Support\Arr; -use Illuminate\Support\Facades\Pipeline; use Symfony\Component\Console\Exception\LogicException; class CreateExpense extends CreateRecord @@ -76,24 +74,7 @@ class CreateExpense extends CreateRecord $transactions = $this->form->getState()['transactions'] ?? []; try { - $branch = $this->getRecord()->branch; - - foreach ($transactions as $transaction) { - - $data = [ - 'branch_id' => $branch->id, - 'happened_on' => $this->getRecord()->happened_on, - ...$transaction, - ]; - - $payload = new CreateTransactionDTO(data: $data, transactionable: $this->getRecord()); - - Pipeline::send(passable: $payload)->through( - [ - CreateTransactionAction::class, - ] - )->thenReturn(); - } + app(CreateRecordTransactionsAction::class)($this->getRecord(), $transactions); $accountIds = collect($transactions) ->pluck('account_id') diff --git a/app/Filament/Resources/SaleResource/Pages/CreateSale.php b/app/Filament/Resources/SaleResource/Pages/CreateSale.php index 189f1fb..0699b82 100644 --- a/app/Filament/Resources/SaleResource/Pages/CreateSale.php +++ b/app/Filament/Resources/SaleResource/Pages/CreateSale.php @@ -3,8 +3,8 @@ namespace App\Filament\Resources\SaleResource\Pages; use App\Actions\Sales\CreateSaleAction; -use App\Actions\Transactions\CreateTransactionAction; -use App\DataObjects\CreateTransactionDTO; +use App\Actions\Sales\SyncAccountsAction; +use App\Actions\Transactions\CreateRecordTransactionsAction; use App\Filament\Resources\ClientResource; use App\Filament\Resources\SaleResource; use App\Models\Branch; @@ -15,7 +15,6 @@ use Filament\Resources\Pages\CreateRecord; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Pipeline; class CreateSale extends CreateRecord { @@ -82,41 +81,7 @@ class CreateSale extends CreateRecord public function processCreate(array $data, array $transactions): Model { - try { - DB::beginTransaction(); - $record = app(CreateSaleAction::class)($this->getFormDataMutation($data)); - $branch = $record->branch; - - foreach ($transactions as $transaction) { - $tData = [ - 'branch_id' => $branch->id, - 'happened_on' => $record->happened_on, - ...$transaction, - ]; - - $payload = new CreateTransactionDTO(data: $tData, transactionable: $record); - - Pipeline::send(passable: $payload)->through( - [ - CreateTransactionAction::class, - ] - )->thenReturn(); - } - - $accountIds = collect($transactions) - ->pluck('account_id') - ->filter() - ->unique() - ->values() - ->all(); - - $record->accounts()->sync($accountIds); - - DB::commit(); - } catch (\Exception $exception) { - DB::rollBack(); - throw new \Exception('Failed to save transactions : '.$exception->getMessage()); - } + $record = app(CreateSaleAction::class)($this->getFormDataMutation($data), $transactions); return $record; } From 8c6fa6cb08c00e5fb89431ce13ee253f5663c33b Mon Sep 17 00:00:00 2001 From: Jp Date: Mon, 16 Feb 2026 02:02:26 +0800 Subject: [PATCH 3/3] refactor(client): extract base account generation to command Move the logic for generating base accounts for a new client from the ClientObserver into a dedicated command class (GenerateBaseAccountCommand). This improves code organization and reusability. - The command is now used in the ClientObserver::created method. - The command is also made available as a manual action in the AccountsRelationManager table header, allowing admins to generate base accounts for existing clients that lack them. - Added necessary imports to the CreateSale page, though the command is not directly used there in this diff, suggesting preparatory work for future integration. --- .../Clients/GenerateBaseAccountCommand.php | 64 +++++++++++++++++++ .../AccountsRelationManager.php | 15 +++++ .../SaleResource/Pages/CreateSale.php | 3 + app/Observers/ClientObserver.php | 41 +----------- 4 files changed, 84 insertions(+), 39 deletions(-) create mode 100644 app/Commands/Clients/GenerateBaseAccountCommand.php diff --git a/app/Commands/Clients/GenerateBaseAccountCommand.php b/app/Commands/Clients/GenerateBaseAccountCommand.php new file mode 100644 index 0000000..f688412 --- /dev/null +++ b/app/Commands/Clients/GenerateBaseAccountCommand.php @@ -0,0 +1,64 @@ +accounts()->createMany([ + [ + 'account_type_id' => 1, + 'account' => 'Cash', + 'normal_balance' => 'debit', + ], + [ + 'account_type_id' => 1, + 'account' => 'Input Tax', + 'normal_balance' => 'debit', + ], + [ + 'account_type_id' => 1, + 'account' => 'Creditable Withholding Tax', + 'normal_balance' => 'debit', + ], + [ + 'account_type_id' => 2, + 'account' => 'Output Tax', + 'normal_balance' => 'credit', + ], + [ + 'account_type_id' => 2, + 'account' => 'Payable Withholding Tax', + 'normal_balance' => 'credit', + ], + [ + 'account_type_id' => 5, + 'account' => 'Vat Exempt Revenue', + 'normal_balance' => 'credit', + ], + [ + 'account_type_id' => 4, + 'account' => 'Sales Discount', + 'normal_balance' => 'debit', + ], + ]); + }); + } + +} diff --git a/app/Filament/Resources/ClientResource/RelationManagers/AccountsRelationManager.php b/app/Filament/Resources/ClientResource/RelationManagers/AccountsRelationManager.php index 548121d..a34a448 100644 --- a/app/Filament/Resources/ClientResource/RelationManagers/AccountsRelationManager.php +++ b/app/Filament/Resources/ClientResource/RelationManagers/AccountsRelationManager.php @@ -2,6 +2,7 @@ namespace App\Filament\Resources\ClientResource\RelationManagers; +use App\Commands\Clients\GenerateBaseAccountCommand; use App\Filament\Exports\ClientAccountsExporter; use App\Models\Account; use Filament\Actions\Exports\Enums\ExportFormat; @@ -43,6 +44,20 @@ class AccountsRelationManager extends RelationManager // ]) ->headerActions([ + Tables\Actions\Action::make('generate-base-accounts') + ->requiresConfirmation() + ->label('Generate Base Accounts') + ->action(function () { + $client = $this->getOwnerRecord(); + if (! $client ) { + return; + } + + if($client->accounts()->count() > 0) { + return; + } + app(GenerateBaseAccountCommand::class)->execute($client); + }), Tables\Actions\ExportAction::make('Export Accounts')->exporter(ClientAccountsExporter::class)->formats([ExportFormat::Csv]), Tables\Actions\CreateAction::make()->label('New Account')->icon('heroicon-o-plus')->slideOver(), ]) diff --git a/app/Filament/Resources/SaleResource/Pages/CreateSale.php b/app/Filament/Resources/SaleResource/Pages/CreateSale.php index 0699b82..b6f063f 100644 --- a/app/Filament/Resources/SaleResource/Pages/CreateSale.php +++ b/app/Filament/Resources/SaleResource/Pages/CreateSale.php @@ -5,12 +5,15 @@ namespace App\Filament\Resources\SaleResource\Pages; use App\Actions\Sales\CreateSaleAction; use App\Actions\Sales\SyncAccountsAction; use App\Actions\Transactions\CreateRecordTransactionsAction; +use App\Commands\Clients\GenerateBaseAccountCommand; use App\Filament\Resources\ClientResource; use App\Filament\Resources\SaleResource; use App\Models\Branch; use App\Models\Client; use App\Models\Sale; use App\Services\Sales\SaleService; +use Filament\Actions\Action; +use Filament\Forms\Components\Actions; use Filament\Resources\Pages\CreateRecord; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; diff --git a/app/Observers/ClientObserver.php b/app/Observers/ClientObserver.php index 19678c2..7db967a 100644 --- a/app/Observers/ClientObserver.php +++ b/app/Observers/ClientObserver.php @@ -2,6 +2,7 @@ namespace App\Observers; +use App\Commands\Clients\GenerateBaseAccountCommand; use App\Models\Client; use Illuminate\Support\Facades\DB; @@ -12,45 +13,7 @@ class ClientObserver */ public function created(Client $client): void { - DB::transaction(function () use ($client) { - $client->accounts()->createMany([ - [ - 'account_type_id' => 1, - 'account' => 'Cash', - 'normal_balance' => 'debit', - ], - [ - 'account_type_id' => 1, - 'account' => 'Input Tax', - 'normal_balance' => 'debit', - ], - [ - 'account_type_id' => 1, - 'account' => 'Creditable Withholding Tax', - 'normal_balance' => 'debit', - ], - [ - 'account_type_id' => 2, - 'account' => 'Output Tax', - 'normal_balance' => 'credit', - ], - [ - 'account_type_id' => 2, - 'account' => 'Payable Withholding Tax', - 'normal_balance' => 'credit', - ], - [ - 'account_type_id' => 5, - 'account' => 'Vat Exempt Revenue', - 'normal_balance' => 'credit', - ], - [ - 'account_type_id' => 4, - 'account' => 'Sales Discount', - 'normal_balance' => 'debit', - ], - ]); - }); + app(GenerateBaseAccountCommand::class)->execute($client); } /**