Event-Driven Architecture
How domain events, listeners, and the workflow engine compose automation in the FlexPoint API.
The portal uses Laravel's event system to decouple what happened (a domain event) from what should happen next (listeners and the workflow engine).
The canonical event: LoanStatusChanged
App\Events\LoanStatusChanged is fired after a loan's lifecycle status has
transitioned and been persisted. It carries everything a listener needs:
final class LoanStatusChanged
{
use Dispatchable;
public function __construct(
public readonly Loan $loan,
public readonly LoanStatus $from,
public readonly LoanStatus $to,
public readonly User $actor,
) {}
}Two listeners, two responsibilities
LoanStatusChanged fans out to two listeners that stay deliberately separate:
| Listener | Responsibility |
|---|---|
App\Listeners\SendLoanStatusNotifications | Notifications stay a dedicated path. |
App\Listeners\RunWorkflows | Bridges the event into the generic workflow engine for every other automation. |
RunWorkflows translates the domain event into a workflow trigger and builds the
context the engine's conditions and actions read from:
public function handle(LoanStatusChanged $event): void
{
$this->engine->dispatch(WorkflowTrigger::LoanStatusChanged, [
'loan_id' => $event->loan->id,
'loan_number' => $event->loan->loan_number,
'from_status' => $event->from->value,
'to_status' => $event->to->value,
'actor_id' => $event->actor->id,
]);
}The workflow engine
App\Services\Workflow\WorkflowEngine is the generic automation layer. A
WorkflowTrigger (such as LoanStatusChanged) dispatches to configured
workflows whose conditions are evaluated against the event context; matching
workflows run their actions (for example
App\Services\Workflow\Actions\CreateConditionWorkflowAction). This is where the
former app/Workflows/ engine now lives, under app/Services/Workflow/.
Conditions are a direct state change today
No ConditionResolved event (yet)
Resolving an underwriting condition is currently a direct state
change, not its own domain event. App\Actions\Loans\ResolveCondition
sets the condition's status to Cleared or Waived and
records cleared_by / cleared_at — it does not emit a
ConditionResolved event. Automation keyed off condition state
therefore runs through the loan-status path, not a condition event. If a future
feature needs to react to condition sign-off directly, add a domain event here.
See the product-level view of these events in Product → Events & Workflows.
Disclosures
Architecture of the disclosure feature — the DisclosureProvider port, the MeridianLink Document Framework adapter and its operation map, the caller-ticket model, and driver config.
MLM Change Webhook & Freshness
How the portal stays current with MeridianLink — the inbound change webhook, the sLastModifiedTimestamp self-write guard, and what a webhook re-syncs.