in reply to System for calculating timing of reminder messages?

This answer isn't quite what you asked, but might be useful to hear.

It sounds like your legacy system has a whole lot of special use cases that have been identified over the years and received custom features to handle them. While the code might be a technical-debt nightmare, the business logic is probably fairly valuable, and replacing it with a completely new system that would need tailored for these use cases might actually be a worse option than paying off the technical debt.

Here's how you can pay off the technical debt:

Your first main goal should be to move all the logic as-is (as much as possible) to a module with a sane API. In a broad sense, the per-client configuration would be the attributes of the object, there would need to be a data store that keeps a history of the notifications sent to that client, and then the main method of the module would be one that delivers new events. If this code is as bad as it sounds, it probably sends the emails directly. You'll need to factor that out so that it "emits" email data in some way that can be passed to a separate mailer object. One final "hidden" parameter is the current value of 'time', which you need to factor out as well.

In a procedural sense, you would:

  1. Create a new package MyApp::Notify (or any more specific name)
  2. Copy all the event code into a "handle_event" method of this new module
  3. Going line-by-line through "handle_event",
    • If the code references a configuration variable, create an attribute on the object for that, and document what it is and who needs to set it.
    • If the code sends a message, change that to returning a value of some sort describing what needs sent.
    • If the code looks at the database or something to see the last message sent for this event, convert that to a more generic "history lookup" method on this object
    • If the code makes references to the current system time, either change that to use a 'time' parameter passed to the handle_event method, or time attribute on the event object
  4. Avoid any other refactoring in the event logic. Focus on this one change first before getting carried away with other code cleanup.
  5. Find every place that emits events, and have it send that event to this object, created on demand for the current customer. The best way to do that depends on what framework you're using.
  6. Write the adapter that takes the return value of handle_event and generates the notification (email, SMS, etc)
  7. Do one last painful manual test of all of this, and deploy it. Fight through the bugs you introduced for the next month or so.
  8. Finally, begin writing tests for your event module. Create lots of examples of (config, example history, event, expected output) and verify that handle_event generates the correct return value for each event under each set of circumstances. Remember the part above where you converted history lookups to a method of the MyApp::Notify object? That's so now you can mock that method to return the history you want to be tested.
  9. Write more tests
  10. Write even more tests
  11. Begin refactoring the code to be less spaghetti. Use your test suite to validate that you didn't break anything.

Your situation is probably more complicated than that, of course, but that's a general pattern for refactoring a monolith of nasty code which has worked well for me.

Another pro-tip that you might find useful is to use a spool table in your database for outgoing messages. So, instead of the code emitting a message immediately, the code instead writes a message (or parameters to generate one from a template) into a database table with various useful columns like when the message should be delivered, from, to, what caused it to be queued, and so on. Lots of benefits here:

Hope that helps.