0

I have a web app created in Laravel that takes credit card payments.

Every day a scheduled command that I created runs to take "today's" payments (basically it submits one http request for each pending payment to the payment gateway).

Now I need to allow to trigger this payment submission process via a button in a dashboard.

The command takes a random long time to process (depending on the number of payments to process), so call it from the controller I think is not an option.

I'm thinking of just refactor it: move all the command code to a "middleman" class so I could call this class on both the command and the controller.

PaymentsSubmissionHelper::submit()

PaymentsSubmissionCommand: PaymentsSubmissionHelper::submit()
PaymentsSubmissionController: PaymentsSubmissionHelper::submit()

However, the command shows a progress bar and the estimated time to process and I will need to show a progress bar in the html interface as well. In the web interface I will need to make ajax requests to the server to get the current progress but in the command this progress is tracked in a completely different way using:

$bar = $this->output->createProgressBar($totalPayments);
$bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %message%');

and for each processed payment:

$bar->advance();

How can I create keep track of the progress on both the command and the controller?

Any help will be appreciated.

Thanks in advance!

TJ is too short
  • 827
  • 3
  • 15
  • 35

3 Answers3

1

I would suggest using queued event listeners in this use case. You would dispatch an event in your controller and have a listener which could trigger the command. By queueing the listener you avoid a long response time. No need to refactor the command itself!

Regarding a progress bar, you could have a static progess bar that updates on page load where you would read out the status from your DB and display it similarly to how Amazon displays how far along your order is at any moment.

For a real time updated progress bar, I suggest implementing web sockets. Socket.io seems great.

  • Thanks very much for your reply. I totally agree with you but in that way how can I update the front end with a progress bar showing the current status of the command? – TJ is too short Feb 13 '19 at 16:06
  • Why not simply notifying the user when the job is done? For a progress bar, maybe write the progress to DB from your command so you can display it? For real time updates use something like socket.io –  Feb 13 '19 at 16:50
  • @TJistooshort did you see the AJAX progress solution I posted? – Don't Panic Feb 13 '19 at 20:39
  • @Don'tPanic Wouldn't your approach result in massive load on scale, though? Imagine a million users each firing requests every couple seconds simply to update a progress bar. –  Feb 14 '19 at 08:06
  • Possibly - the same as *any* solution which had to handle millions of progress bars. OP hasn't mentioned anything about large scale though? – Don't Panic Feb 14 '19 at 08:21
  • @Don'tPanic I think sockets might be more efficient for real time updates. –  Feb 14 '19 at 08:31
  • You may be right - why not add it to your answer? I'm against pre-optimisation, and I see nothing in this question that suggests a simple AJAX request would be a problem, but I know nothing about socket programming and I'd love to learn more. – Don't Panic Feb 14 '19 at 08:41
  • Thanks guys for your replies. The payment submission can be triggered only by a developer via command line or by a website admin via web interface. There is only 1 admin at the moment so is not a massive issue to use ajax. My main concern is to query the database so many times to calculate the current status. I'm considering to write some info to a JSON file and update it as we move (total payments to process, how many were successful, how many failed, processing payment x of N) and the AJAX request will just read that file. Is this me overcomplicating the issue? – TJ is too short Feb 14 '19 at 09:19
  • @TJistooshort Maybe. Why not simply notifying the admin after the job is done? Do you actually need a progress bar here? Moving long running tasks to a queue is done precisely so that people wouldn't have to stare at progress bars :) –  Feb 14 '19 at 09:25
  • Thanks @Archie. I will suggest that. They asked me to show info about the progress but maybe they don't actually need to stare at it. However I will need to find a way to prevent them to press the button again, otherwise there will be more than 1 process running to update the payments and that probably might lead to other issues. (We know how users are...) – TJ is too short Feb 14 '19 at 09:47
  • A single DB query every 2 seconds is not something you should have to worry about, unless you are running at scale, or you're running on a calculator from the 70s :-) – Don't Panic Feb 14 '19 at 09:50
  • 1
    @TJistooshort Then displaying a static status on the same page would probably work best. Query the status once on page load and display it. If a process is currently running, then show that too and disable the submit button. –  Feb 14 '19 at 09:55
  • I like the idea. I will suggest it. Thanks very much @Archie! – TJ is too short Feb 14 '19 at 09:59
1

As already pointed out in another answer, Laravel's queued event listeners are the way to handle long-running processes on the front end. You shouldn't need to refactor your console command at all.

As to showing progress on the front end, one simple solution would be to set up some AJAX polling. Ever few seconds have AJAX fire off a request to a controller method which simply looks at today's payments, calculates how many are processed (presumably you have some kind of status field which will show you whether or not the running job has handled it yet), and return a number representing the percentage that are done. The AJAX success handler would then update your progress tracker on the page.

// Check status every 2s
var timer = setInterval(function() {
    pollStatus();
}, 2000);

pollStatus = function() {
    $.ajax({
        url: 'somewhere/jobStatus',
        success: function(resp) {
            $('#progress').html(resp . '%');

            if (resp === 100) {
                // We've reached 100%, no need to keep polling now
                clearInterval(timer);
            }
        }
    });
}

It might be wise to somehow make sure polls don't overrun, and maybe you'd want to tweak the frequency of polling.

Don't Panic
  • 13,965
  • 5
  • 32
  • 51
0

As you are using progress bar and advancing it, you will do same in ajax but the progress logic will be different off-course.

The common part in both the cases is handling each card payment. So I will say create separate class or service which takes card payment instance e.g. PaymentProcess, processes it and returns if successful or failed.

Then in command you can do (psuedocode) :

public function handle() { $pendingPayments = Payment::where('status', 'pending');

$bar = $this->output->createProgressBar($pendingPayments->count());

$pendingPayments->chunk(10, function($payments) use($bar){

    $payments->each(function($payment) use ($bar){

        $process = (new PaymentProcess($payment))->process();

        $bar->advance();

    });
});

$bar->finish();

}

Now if you trigger this from frontend, the ajax response should give you an id of current process stored somewhere. Then you will keep sending another ajx requests in an interval of lets say 1 second and get the current progress until it reaches to 100%. (If you are using XMLHttpRequest2 then the logic will differ)

For that you can create another table to store progresses and then keep updating it.

Now similarly you can use the PaymentProcess inside controller. :

public function processPendingPayments(Request $request) { // Authorize request $this->authorize('processPendingPayments', Payment::class);

$pendingPayments = Payment::where('status', 'pending');

// Create a progress entry
$progress = PaymentProgress::create([
    'reference' => str_random('6')
    'total' => $pendingPayments->count(),
    'completed' => 0
]);

$pendingPayments->chunk(10, function($payments) use($bar){

    $payments->each(function($payment) use ($bar){

        $process = (new PaymentProcess($payment))->process();

        // Update a progress entry
        $progress->update([
            'completed' => $progress->completed + 1;
        ]);

    });
});

return response()->json([
    'progress_reference' => $progress->reference
], 200);

}

Now another endpoint to get the progress

public function getProgress(Request $request) { // Authorize request $this->authorize('getProgress', Payment::class);

$request->validate([
    'reference' => 'required|exists:payment_process,reference'
]);

$progress = PaymentProcess::where('reference', $request->reference)->first();

$percentage = $progress->completed / $progress->total * 100;

 return response()->json(compact('percentage'), 200);

}

Mihir Bhende
  • 8,677
  • 1
  • 30
  • 37