3

My application deals with user payments. In the company, this user has the following status:

  • compliant (user payed all debts so far)
  • overdue/default (user registered for 3 months minimum and has hasn't payed at least 1 debt)
  • inactive (user is registered for less than 3 months and hasn't payed any debt)

How is the best way to deal with those rules in multiple places (and rules) inside the application?

Do I need a field like status_id and a cron to update this every hour?

No status_id field and write the SQL rule in every query that needs to show the status?

Load a User model and call a ->status() method that has the rule? In this case, how can I show "totals", like: We have 3000 overdue users, 15000 inactive users etc...

This is giving me headaches for months and I really need help haha. We currently have a solution but it's too complex to deal with it. As it seems to be something common within apps that deal with payment, there's must be a simplier way to do this :P

Thanks!

Notes

  • Application has currently 90.000 users
  • We need this info in real-time.
  • This info is used in reports to generate chars.
  • This info is showed inside the user profile.
  • This info is showed in listings.
  • Users are notified when a user change between those status (like, "you have debts" when user enters in "overdue").
  • This info is not managed by application users.
  • The status need to be tracked.
Felipe Francisco
  • 1,064
  • 2
  • 21
  • 45

3 Answers3

3

If you are using this field in multiple places, then you should store the status in a single place and update it as appropriate (I would also keep a history of the statuses, but that is another matter).

If the status changes due to some user actions (such as a payment being processed), then you can use a trigger on the action. However, your status changes seem to be based on time after an event. In that case, you should run a regularly scheduled job (as a cron job or database event).

I am a bit confused about why you would do this every hour. It seems that once per day would be most appropriate. If the "debts" are paid at arbitrary times, then the process of payment should update the status. For the downgrading of the status, a single job once per day should be sufficient.

Gordon Linoff
  • 1,242,037
  • 58
  • 646
  • 786
  • "Every hour" was just an example of how often the update process would run. And yes, the status is updated based on user actions, not necessarily some time after the event, so, the update would be immediate. Your solution is great and I'm combining it with @Chris', thanks for your help! – Felipe Francisco Jan 06 '16 at 23:27
1

Interesting question, but also not one with a single answer.

I think the complexity here might come from the surrounding code, rather than the core business logic and requirements. I say this because three status types, all of which are derived from your internal application, isn't too bad.

One possible solution, and I am assuming some level of MVC or similar.

Given your model, user, and extending an ORM like Eloquent (I will Eloquent from Laravel because I am most familiar with it, but any ORM will work):

use Illuminate\Database\Eloquent\Model;
use App\DebtCollector;

public class User extends Model
{
    // Assuming model has the following fields
    // id, status, registration_date, and a one to many
    // relationship with debts 

    protected $fillable = [
        'raw_status',
        'registration_date',
    ];

    public function debts()
    {
        return $this->hasMany(Debt::class);
    }

    public function refreshStatus()
    {
        $dc = new DebtCollector();

        // Business logic inside the "DebtCollector" class
        $this->raw_status = $dc->resolveStatus($this->debts, $this->registration_date);

        // Save value to the underlying datebase
        $this->save();            
    }

    // If you fetch a status directly, it will refresh first, 
    // then return the value
    // 
    public function getStatusAttribute()
    {
        $this->refreshStatus();
        return $this->raw_status;
    }
}


// Schedule task somewhere - ran nightly, or whenever
// 
// This way you can refresh the status only on certain groups
// of data - for example, if the business flow means that once
// they become compliant, they can't go back, there is no need
// to refresh their status anymore 
//
User::where('raw_status', '<>', 'compliant')->refreshStatus();

// Alternatively, the schedule could chunk results and do an update
// only to those not updated in the last 24 hours
//
$date = new DateTime;
$date->modify('-24 hours');
$formatted_date = $date->format('Y-m-d H:i:s');
User::where('last_updated', '>', $formatted_data)->refreshStatus();
Chris
  • 54,599
  • 30
  • 149
  • 186
  • "isn't too bad", it's great to hear that hahaha :P. That's it! Calling `refreshStatus` and building a `StatusResolver` class/method is the way to go. Thanks a lot! I`ll be combining your answer with historical data of @Gordon's answer and hope that we don't need to refactor this any more. – Felipe Francisco Jan 06 '16 at 23:30
1

I would say there are multiple solutions to this problem.

I would suggest not having any defined status. From what I can see you can always "figure out" the current status based on some other data. For example "user payed all debts so far". This is something you simply know just by analyzing all changes for given period. You can aggregate data to figure out all you need to know. So then you don't need to save the status at all. It is just derived from all the changes to the customer's account that happened over specific period.

Same is for totals. You can do this easily on database level or even by using some document based DBs or ElasticSearch.

This, of course, assumes you trace the history of changes. If you do - problem solved. If you don't - you have to save the status into database and will not be able to get historical data.

Aleksander Wons
  • 3,611
  • 18
  • 29
  • Today the application is working like you described. It "figures out" the status every time we need it. The problem is: The "figure out" process involves about 4~5 joins, cause I also need to know things like "the debt that isn't paid is from an order that's active?. Also, some user types may receive a different status that is not necessarily related to the debt itself. Today, we have a sql block that "resolves" the status, but it's quite annoying to maintain and insert it on our queries. What would you suggest (technically) to handle this without saving the status? I'm rly interested in this. – Felipe Francisco Jan 06 '16 at 23:38
  • @FelipeFrancisco You can easily do it by issuing a second query after you already have a list of users. And merge the data before you return it. This way you have a single point of truth. You still need to remember to add status details but this can easily be handled on object level and in tests. – Aleksander Wons Jan 07 '16 at 08:30