-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add recurring transaction service #89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,35 @@ | ||||||||||||||||
<?php | ||||||||||||||||
|
||||||||||||||||
namespace App\Console\Commands; | ||||||||||||||||
|
||||||||||||||||
use App\Services\RecurringTransactionService; | ||||||||||||||||
use Illuminate\Console\Command; | ||||||||||||||||
|
||||||||||||||||
class ProcessRecurringTransactions extends Command | ||||||||||||||||
{ | ||||||||||||||||
/** | ||||||||||||||||
* The name and signature of the console command. | ||||||||||||||||
*/ | ||||||||||||||||
protected $signature = 'transactions:process-recurring'; | ||||||||||||||||
|
||||||||||||||||
/** | ||||||||||||||||
* The console command description. | ||||||||||||||||
*/ | ||||||||||||||||
protected $description = 'Process and schedule recurring transactions that are due'; | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the
Suggested change
|
||||||||||||||||
|
||||||||||||||||
/** | ||||||||||||||||
* Execute the console command. | ||||||||||||||||
*/ | ||||||||||||||||
public function handle(RecurringTransactionService $service): void | ||||||||||||||||
{ | ||||||||||||||||
$this->info('Starting to process recurring transactions ...'); | ||||||||||||||||
|
||||||||||||||||
try { | ||||||||||||||||
// call the service to process all transactions that are due | ||||||||||||||||
$service->processAllDueTransactions(); | ||||||||||||||||
$this->info('Recurring transactions processing completed.'); | ||||||||||||||||
} catch (\Exception $e) { | ||||||||||||||||
$this->error('Error processing recurring transactions: '.$e->getMessage()); | ||||||||||||||||
} | ||||||||||||||||
Comment on lines
+31
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's generally recommended to catch
Suggested change
|
||||||||||||||||
} | ||||||||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -13,6 +13,7 @@ class Kernel extends ConsoleKernel | |||||
protected function schedule(Schedule $schedule): void | ||||||
{ | ||||||
// $schedule->command('inspire')->hourly(); | ||||||
$schedule->command('transactions:process-recurring')->daily(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For daily scheduled tasks, it's often best practice to specify an exact time rather than just
Suggested change
|
||||||
} | ||||||
|
||||||
/** | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -284,9 +284,8 @@ public function store(Request $request): JsonResponse | |
// schedule next task. | ||
$next_date = get_next_transaction_schedule_date($recurring_transaction); | ||
RecurrentTransactionJob::dispatch( | ||
id: (int) $recurring_transaction->id, | ||
recurrence_period: $recurring_transaction->recurrence_period, | ||
recurrence_interval: $recurring_transaction->recurrence_interval, | ||
Comment on lines
-288
to
-289
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @iMercyvlogs the Please test again and ensure the next job (that was already scheduled) does not run if the interval and period changes and let me know if it your new changes handle this scenario. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The update() method in the controller takes the recurrence_period, recurrence_interval, recurrence_ends_at information and checks to see if the recurrence is still valid... if it is, that's when the RecurrenceTransactionJob is triggered with the target rule's ID. |
||
// id: (int) $recurring_transaction->id, | ||
(int) $recurring_transaction->ruleId | ||
)->delay($next_date); | ||
} | ||
|
||
|
@@ -574,9 +573,9 @@ public function update(Request $request, $id): JsonResponse | |
// schedule next task. | ||
$next_date = get_next_transaction_schedule_date($recurring_transaction); | ||
RecurrentTransactionJob::dispatch( | ||
id: (int) $recurring_transaction->id, | ||
recurrence_period: $recurring_transaction->recurrence_period, | ||
recurrence_interval: $recurring_transaction->recurrence_interval)->delay($next_date); | ||
// id: (int) $recurring_transaction->id | ||
(int) $recurring_transaction->ruleId | ||
)->delay($next_date); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,104 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<?php | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
namespace App\Services; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
use App\Models\RecurringTransactionRule; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
use App\Models\Transaction; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
use Carbon\Carbon; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
class RecurringTransactionService | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* This method is called by the job. It finds a specific recurring rule | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* and processes it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
public function generateNextTransaction(int $ruleId): void | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// 1. Find the specific rule in the database by its ID. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// It's linked to the original transaction. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$rule = RecurringTransactionRule::with('transaction')->find($ruleId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// 2. If the rule doesn't exist, we stop here. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (! $rule) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
logger()->info('Recurring transaction rule '.$ruleId.' not found.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// 3. Make sure the rule is still active and hasn't expired. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if ($rule->recurrence_ends_at && $rule->recurrence_ends_at < now()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
logger()->info('Rule '.$ruleId.' has expired. Skipping.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// 4. Create a brand new transaction based on the rule. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$this->createTransactionFromRule($rule); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// 5. Calculate the next time this transaction should happen. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$nextRunDate = $this->calculateNextRunDate($rule); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// 6. Tell Laravel to run the job again at that future date. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
dispatch(new \App\Jobs\RecurrentTransactionJob($rule->id))->delay($nextRunDate); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For consistency and improved readability, using the static
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
logger()->info('Scheduled next transaction for '.$rule->id.' at '.$nextRunDate); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (\Exception $e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
logger()->error('Error processing rule '.$ruleId.': '.$e->getMessage()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* This method is called by the command. It finds ALL rules that are due | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* and dispatches a separate job for each one. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
public function processAllDueTransactions(): void | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// 1. Find all rules that are due to be run. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$rules = RecurringTransactionRule::where('recurrence_ends_at', '>=', now()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
->orWhereNull('recurrence_ends_at') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
->get(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+57
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current To accurately 'process and schedule recurring transactions that are due' (as per the console command description), the The Below is a conceptual code suggestion assuming the
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// 2. For each rule, dispatch a job to handle it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
foreach ($rules as $rule) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Dispatch a job for each rule. This is better than doing all the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// work at once, especially for many transactions. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
dispatch(new \App\Jobs\RecurrentTransactionJob($rule->id)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+58
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This To fix this, the method must first determine if a rule is actually due to run now before dispatching a job for immediate execution. This requires querying or filtering rules based on their
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Creates a new transaction record from a recurring rule. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private function createTransactionFromRule(RecurringTransactionRule $rule): Transaction | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Get the original transaction data. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$originalTransaction = $rule->transaction; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Make a copy of the original transaction. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$newTransaction = $originalTransaction->replicate(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Update the copy with today's date and time. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$newTransaction->created_at = now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$newTransaction->updated_at = now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+83
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current replication process for recurring transactions doesn't reset the
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Save the new copy to the database and synced. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$newTransaction->save(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New transactions created by the recurring service should also be marked as synced, similar to how initial transactions are handled in the
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$newTransaction->markAsSynced(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if ($originalTransaction->categories->isNotEmpty()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$newTransaction->categories()->sync($originalTransaction->categories->pluck('id')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return $newTransaction; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Calculates the date of the next transaction. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private function calculateNextRunDate(RecurringTransactionRule $rule): Carbon | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Use a function that exists in your project to calculate the next date. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return get_next_transaction_schedule_date($rule); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,11 +1,9 @@ | ||||||||||||||
services: | ||||||||||||||
app: | ||||||||||||||
build: | ||||||||||||||
context: . | ||||||||||||||
dockerfile: Dockerfile | ||||||||||||||
args: | ||||||||||||||
HOST_UID: '${HOST_UID}' | ||||||||||||||
HOST_GID: '${HOST_GID}' | ||||||||||||||
image: shinsenter/laravel:php8.2 | ||||||||||||||
environment: | ||||||||||||||
APP_UID: '${APP_UID:-1001}' | ||||||||||||||
APP_GID: '${APP_GID:-1001}' | ||||||||||||||
ports: | ||||||||||||||
- '${APP_PORT:-8000}:80' | ||||||||||||||
tty: true | ||||||||||||||
|
@@ -61,4 +59,4 @@ networks: | |||||||||||||
driver: bridge | ||||||||||||||
volumes: | ||||||||||||||
sailmysql: | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a best practice for text files, including YAML configuration files, to end with a newline character. This ensures proper parsing by some tools and prevents issues when concatenating files.
Suggested change
|
||||||||||||||
driver: local | ||||||||||||||
driver: local |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc block for
$signature
is incomplete. It should typically describe the purpose of the signature itself, rather than being left empty or with only comments about its existence. This is a minor style improvement for consistency.