1079 lines
35 KiB
Markdown
1079 lines
35 KiB
Markdown
# BankFlow Project Blueprint
|
||
|
||
## 1. Purpose
|
||
|
||
This document is the implementation blueprint for the BankFlow challenge. It converts the approved architectural decisions into a fixed Laravel project structure, domain model, workflow design, database schema, API boundary, and test plan.
|
||
|
||
Implementation must remain consistent with:
|
||
|
||
- [BankFlow requirements](./bank-flow.md)
|
||
- [BankFlow API reference](./api-routes.md)
|
||
|
||
When this blueprint and a general Laravel convention differ, this blueprint controls the project-specific architecture. The functional requirements and API contract remain the source of truth for observable behavior.
|
||
|
||
## 2. Fixed technical decisions
|
||
|
||
| Concern | Decision |
|
||
| --- | --- |
|
||
| Framework | Laravel 13 on PHP 8.4 |
|
||
| Database | PostgreSQL |
|
||
| Persistence | Eloquent ORM |
|
||
| Architecture | Layered Laravel architecture with loan-domain grouping |
|
||
| Application operations | Single-purpose Action classes |
|
||
| Service layer | No generic `LoanService` |
|
||
| Repository pattern | Not used; Eloquent is the only persistence implementation |
|
||
| Workflow | Ordered collection of independent Stage classes |
|
||
| Stage state | Stateless |
|
||
| Workflow definitions | Versioned database configuration per `loan_type_id` |
|
||
| Business-rule values | Validated JSON on configured Workflow steps |
|
||
| Stage implementation registry | Trusted code mapping in `config/workflow-stages.php` |
|
||
| Domain values | PHP backed Enums, except database-managed Loan types |
|
||
| Processing model | Synchronous |
|
||
| Queue | Not used |
|
||
| Transaction boundary | One database transaction for the complete processing operation |
|
||
| Concurrency control | Row lock plus database uniqueness constraints |
|
||
| Business history | Stored in `loan_histories` |
|
||
| Operational logging | Standard Laravel logging for unexpected failures |
|
||
| Tests | Pest unit and feature tests |
|
||
|
||
This database-managed configuration design supersedes the earlier proposal to store Workflow order in `config/workflows.php` and rule values in `config/loan.php`. Those files must not be introduced as a second source of truth.
|
||
|
||
### 2.1 Explicitly excluded patterns
|
||
|
||
The initial implementation must not introduce:
|
||
|
||
- a Repository layer over Eloquent;
|
||
- a generic or oversized `LoanService`;
|
||
- asynchronous workflow Jobs;
|
||
- Horizon, Kafka, RabbitMQ, or another broker;
|
||
- a generic Rule Engine or executable rule language;
|
||
- Clean Architecture or Hexagonal boilerplate;
|
||
- event sourcing, CQRS, or microservices.
|
||
|
||
These patterns solve problems outside the challenge scope and would add implementation and operational complexity without improving the required behavior.
|
||
|
||
## 3. Architectural flow
|
||
|
||
```text
|
||
HTTP Request
|
||
↓
|
||
Form Request (transport structure only)
|
||
↓
|
||
LoanController
|
||
↓
|
||
Single-purpose Action
|
||
↓
|
||
WorkflowEngine or Eloquent Model
|
||
↓
|
||
PostgreSQL
|
||
```
|
||
|
||
Workflow processing follows this flow:
|
||
|
||
```text
|
||
ProcessLoanAction
|
||
↓
|
||
Database transaction + loan row lock
|
||
↓
|
||
Loan's immutable WorkflowConfiguration
|
||
↓
|
||
WorkflowEngine
|
||
↓
|
||
Ordered stateless Stage instances
|
||
↓
|
||
Loan + LoanHistory persistence
|
||
```
|
||
|
||
### Layer responsibilities
|
||
|
||
| Layer | Responsibility |
|
||
| --- | --- |
|
||
| HTTP | Parse requests, authorize access, invoke an Action, and return API Resources |
|
||
| Actions | Implement one application use case and own its transaction boundary |
|
||
| Domain | Define enums, execution results, workflow orchestration, and business stages |
|
||
| Models | Persist loans and their execution history through Eloquent |
|
||
| Configuration models | Store versioned Workflow order and rules per Loan type |
|
||
|
||
Controllers must not contain business rules. Models must not orchestrate the Workflow. Stage classes must not produce HTTP responses or write logs as business history.
|
||
|
||
## 4. Project structure
|
||
|
||
```text
|
||
app/
|
||
├── Actions/
|
||
│ ├── Loan/
|
||
│ │ ├── CreateLoanAction.php
|
||
│ │ ├── GetLoanAction.php
|
||
│ │ ├── GetLoanHistoryAction.php
|
||
│ │ └── ProcessLoanAction.php
|
||
│ └── WorkflowConfiguration/
|
||
│ ├── CreateWorkflowConfigurationAction.php
|
||
│ ├── PublishWorkflowConfigurationAction.php
|
||
│ └── UpdateWorkflowConfigurationAction.php
|
||
│
|
||
├── Domain/
|
||
│ └── Loan/
|
||
│ ├── Contracts/
|
||
│ │ └── StageInterface.php
|
||
│ ├── Data/
|
||
│ │ └── ExecutionResult.php
|
||
│ ├── Enums/
|
||
│ │ ├── LoanStage.php
|
||
│ │ ├── LoanStatus.php
|
||
│ │ ├── StageResultType.php
|
||
│ │ └── WorkflowConfigurationStatus.php
|
||
│ ├── Exceptions/
|
||
│ │ ├── InvalidWorkflowConfiguration.php
|
||
│ │ └── WorkflowException.php
|
||
│ └── Workflow/
|
||
│ ├── Stages/
|
||
│ │ ├── CreditCheckStage.php
|
||
│ │ ├── FraudCheckStage.php
|
||
│ │ ├── GuarantorCheckStage.php
|
||
│ │ ├── ManagerApprovalStage.php
|
||
│ │ └── ValidationStage.php
|
||
│ ├── WorkflowConfigurationResolver.php
|
||
│ └── WorkflowEngine.php
|
||
│
|
||
├── Http/
|
||
│ ├── Controllers/
|
||
│ │ ├── Admin/
|
||
│ │ │ └── WorkflowConfigurationController.php
|
||
│ │ └── Api/
|
||
│ │ └── V1/
|
||
│ │ └── LoanController.php
|
||
│ ├── Requests/
|
||
│ │ ├── Admin/
|
||
│ │ │ ├── StoreWorkflowConfigurationRequest.php
|
||
│ │ │ └── UpdateWorkflowConfigurationRequest.php
|
||
│ │ └── StoreLoanRequest.php
|
||
│ └── Resources/
|
||
│ ├── LoanHistoryResource.php
|
||
│ ├── LoanProcessResource.php
|
||
│ └── LoanResource.php
|
||
│
|
||
├── Models/
|
||
│ ├── Loan.php
|
||
│ ├── LoanHistory.php
|
||
│ ├── LoanType.php
|
||
│ ├── StageDefinition.php
|
||
│ ├── WorkflowConfiguration.php
|
||
│ └── WorkflowConfigurationStep.php
|
||
│
|
||
└── Providers/
|
||
└── AppServiceProvider.php
|
||
|
||
config/
|
||
└── workflow-stages.php
|
||
|
||
database/
|
||
├── factories/
|
||
│ ├── LoanFactory.php
|
||
│ ├── LoanHistoryFactory.php
|
||
│ ├── LoanTypeFactory.php
|
||
│ └── WorkflowConfigurationFactory.php
|
||
├── migrations/
|
||
│ ├── *_create_loan_types_table.php
|
||
│ ├── *_create_stage_definitions_table.php
|
||
│ ├── *_create_workflow_configurations_table.php
|
||
│ ├── *_create_workflow_configuration_steps_table.php
|
||
│ ├── *_create_loans_table.php
|
||
│ └── *_create_loan_histories_table.php
|
||
└── seeders/
|
||
└── DatabaseSeeder.php
|
||
|
||
routes/
|
||
├── api.php
|
||
└── web.php
|
||
|
||
tests/
|
||
├── Feature/
|
||
│ └── Loan/
|
||
│ ├── CreateLoanTest.php
|
||
│ ├── GetLoanHistoryTest.php
|
||
│ ├── GetLoanTest.php
|
||
│ ├── HealthCheckTest.php
|
||
│ └── ProcessLoanTest.php
|
||
└── Unit/
|
||
└── Domain/
|
||
└── Loan/
|
||
├── Workflow/
|
||
│ ├── CreditCheckStageTest.php
|
||
│ ├── FraudCheckStageTest.php
|
||
│ ├── GuarantorCheckStageTest.php
|
||
│ ├── ManagerApprovalStageTest.php
|
||
│ ├── ValidationStageTest.php
|
||
│ └── WorkflowEngineTest.php
|
||
├── WorkflowConfigurationResolverTest.php
|
||
└── WorkflowConfigurationPublishingTest.php
|
||
|
||
Dockerfile
|
||
README.md
|
||
DESIGN.md
|
||
ENGINEERING_DECISIONS.md
|
||
TESTING.md
|
||
```
|
||
|
||
### Naming rules
|
||
|
||
| Concern | Convention | Example |
|
||
| --- | --- | --- |
|
||
| Controller | Singular resource + `Controller` | `LoanController` |
|
||
| Action | Verb + resource + `Action` | `ProcessLoanAction` |
|
||
| Stage | Business name + `Stage` | `FraudCheckStage` |
|
||
| Enum | Domain concept | `LoanStatus` |
|
||
| Data object | Meaning of the returned data | `ExecutionResult` |
|
||
| Engine | Orchestrated concept + `Engine` | `WorkflowEngine` |
|
||
|
||
Do not introduce alternate names such as `WorkflowManager`, `LoanProcessor`, or `StageResult` for concepts already named here.
|
||
|
||
## 5. Domain design
|
||
|
||
Three domain concepts must remain separate:
|
||
|
||
| Concept | Purpose | Examples |
|
||
| --- | --- | --- |
|
||
| Loan status | Current lifecycle state of a loan | `APPROVED`, `REJECTED` |
|
||
| Workflow stage | Business operation being executed | `FRAUD_CHECK`, `CREDIT_CHECK` |
|
||
| Stage result | Outcome returned by a stage | `PASS`, `FAIL`, `MANUAL_REVIEW` |
|
||
|
||
### 5.1 `LoanStatus`
|
||
|
||
```text
|
||
SUBMITTED
|
||
IN_PROGRESS
|
||
MANUAL_REVIEW
|
||
APPROVED
|
||
REJECTED
|
||
```
|
||
|
||
Terminal automatic-processing statuses are:
|
||
|
||
```text
|
||
MANUAL_REVIEW
|
||
APPROVED
|
||
REJECTED
|
||
```
|
||
|
||
### 5.2 `LoanStage`
|
||
|
||
```text
|
||
VALIDATION
|
||
FRAUD_CHECK
|
||
GUARANTOR_CHECK
|
||
CREDIT_CHECK
|
||
MANAGER_APPROVAL
|
||
```
|
||
|
||
`MANUAL_REVIEW` is a result and loan status, not an executable Stage.
|
||
|
||
### 5.3 `StageResultType`
|
||
|
||
```text
|
||
PASS
|
||
FAIL
|
||
MANUAL_REVIEW
|
||
```
|
||
|
||
### 5.4 Loan types
|
||
|
||
Loan types are database records, not a PHP Enum, because administrators configure a Workflow by `loan_type_id`. The initial seeded codes are:
|
||
|
||
```text
|
||
PERSONAL
|
||
BUSINESS
|
||
```
|
||
|
||
The `code` remains a stable machine identifier even when the display name changes.
|
||
|
||
### 5.5 `WorkflowConfigurationStatus`
|
||
|
||
```text
|
||
DRAFT
|
||
PUBLISHED
|
||
ARCHIVED
|
||
```
|
||
|
||
Only draft configurations are editable. Published and archived configurations are immutable.
|
||
|
||
### 5.6 `ExecutionResult`
|
||
|
||
`ExecutionResult` is an immutable data object returned by every Stage. It is not an Eloquent model.
|
||
|
||
It contains:
|
||
|
||
| Property | Type | Purpose |
|
||
| --- | --- | --- |
|
||
| `type` | `StageResultType` | Stage outcome |
|
||
| `reason` | `string` | `SUCCESS` or a business reason code |
|
||
|
||
The Workflow Engine maps the result to the next status and stage. Stages must not directly choose HTTP status codes.
|
||
|
||
### 5.7 `StageInterface`
|
||
|
||
All Workflow stages implement one contract:
|
||
|
||
```php
|
||
interface StageInterface
|
||
{
|
||
/** @param array<string, mixed> $rules */
|
||
public function execute(Loan $loan, array $rules): ExecutionResult;
|
||
}
|
||
```
|
||
|
||
Stage requirements:
|
||
|
||
- one business responsibility per class;
|
||
- no mutable internal state;
|
||
- no database transaction management;
|
||
- no HTTP concerns;
|
||
- no direct selection of the next Stage;
|
||
- all configurable numbers read from the validated step rules supplied by the Engine;
|
||
- deterministic output for the same Loan and rule snapshot.
|
||
|
||
## 6. Workflow configuration
|
||
|
||
Administrators configure Workflow order and rules in the database for a specific `loan_type_id`. Configuration is versioned so edits cannot change the behavior of a Loan that is already being processed.
|
||
|
||
### 6.1 Configuration ownership
|
||
|
||
```text
|
||
LoanType
|
||
└── WorkflowConfiguration
|
||
└── WorkflowConfigurationStep
|
||
└── StageDefinition
|
||
```
|
||
|
||
Each `WorkflowConfiguration` belongs to exactly one `LoanType`. A Loan stores the exact published `workflow_configuration_id` selected when the Loan is created.
|
||
|
||
### 6.2 Lifecycle
|
||
|
||
1. An administrator creates a `DRAFT` configuration for a Loan type.
|
||
2. The administrator selects supported Stage definitions, sets their order, and supplies Stage-specific rules.
|
||
3. The system validates the complete configuration.
|
||
4. Publishing occurs inside a transaction.
|
||
5. The previously published configuration for that Loan type becomes `ARCHIVED`.
|
||
6. The selected draft becomes `PUBLISHED` and immutable.
|
||
7. New Loans use the published configuration; existing Loans keep their assigned configuration.
|
||
|
||
Administrators never edit a published configuration in place. A change creates a new version.
|
||
|
||
### 6.3 Trusted Stage registry
|
||
|
||
The database stores stable Stage codes, not PHP class names or executable expressions. `config/workflow-stages.php` maps each trusted code to an application handler:
|
||
|
||
```php
|
||
return [
|
||
'VALIDATION' => ValidationStage::class,
|
||
'FRAUD_CHECK' => FraudCheckStage::class,
|
||
'GUARANTOR_CHECK' => GuarantorCheckStage::class,
|
||
'CREDIT_CHECK' => CreditCheckStage::class,
|
||
'MANAGER_APPROVAL' => ManagerApprovalStage::class,
|
||
];
|
||
```
|
||
|
||
An administrator can select only active `StageDefinition` records whose codes exist in this registry. Admin input must never contain PHP, SQL, a handler class name, or an unrestricted expression.
|
||
|
||
### 6.4 Per-step rules
|
||
|
||
Rules are stored as JSON on `workflow_configuration_steps`. Every Stage handler owns the schema and validation of its rules.
|
||
|
||
Examples:
|
||
|
||
```json
|
||
{
|
||
"rejectBelow": 500,
|
||
"manualReviewMin": 500,
|
||
"manualReviewMax": 649,
|
||
"approveFrom": 650
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"activationThreshold": 500000000,
|
||
"incomeMultiplier": 20
|
||
}
|
||
```
|
||
|
||
Publishing must fail if:
|
||
|
||
- a required Stage is missing;
|
||
- a Stage or position is duplicated;
|
||
- a configured Stage code has no registered handler;
|
||
- a rule has the wrong type or an invalid value;
|
||
- credit ranges overlap or contain an unintended gap;
|
||
- the Workflow cannot reach a terminal outcome.
|
||
|
||
### 6.5 Initial configurations
|
||
|
||
Seed initial published configurations matching the challenge requirements:
|
||
|
||
```text
|
||
PERSONAL:
|
||
VALIDATION → FRAUD_CHECK → CREDIT_CHECK → MANAGER_APPROVAL
|
||
|
||
BUSINESS:
|
||
VALIDATION → FRAUD_CHECK → GUARANTOR_CHECK
|
||
→ CREDIT_CHECK → MANAGER_APPROVAL
|
||
```
|
||
|
||
The initial rule values are:
|
||
|
||
| Rule | Default |
|
||
| --- | ---: |
|
||
| Credit rejection boundary | `500` |
|
||
| Manual-review minimum | `500` |
|
||
| Manual-review maximum | `649` |
|
||
| Credit approval boundary | `650` |
|
||
| Manager activation threshold | `500000000` |
|
||
| Income multiplier | `20` |
|
||
|
||
### 6.6 Adding a future Stage
|
||
|
||
Adding a new Stage such as `AML_CHECK` requires:
|
||
|
||
1. adding its `LoanStage` enum value;
|
||
2. creating a class implementing `StageInterface`;
|
||
3. registering its trusted code and class in `config/workflow-stages.php`;
|
||
4. seeding or creating its `StageDefinition`;
|
||
5. letting an administrator add it to a new draft Workflow version;
|
||
6. adding Stage unit tests and Workflow integration tests.
|
||
|
||
Existing Stage classes and the Workflow Engine must not require modification.
|
||
|
||
## 7. Stage behavior
|
||
|
||
### 7.1 `ValidationStage`
|
||
|
||
| Field | Rule | Failure reason |
|
||
| --- | --- | --- |
|
||
| `customerId` | Required and not empty | `INVALID_CUSTOMER_ID` |
|
||
| `amount` | Greater than `0` | `INVALID_AMOUNT` |
|
||
| `phone` | Exactly 11 digits, starts with `09`, digits only | `INVALID_PHONE` |
|
||
| `loanType` | `PERSONAL` or `BUSINESS` | `INVALID_LOAN_TYPE` |
|
||
| `monthlyIncome` | Greater than or equal to `0` | `INVALID_MONTHLY_INCOME` |
|
||
| `creditScore` | Between `0` and `1000`, inclusive | `INVALID_CREDIT_SCORE` |
|
||
|
||
These are business Workflow rules. They must produce a `LoanHistory` entry and must not be implemented as HTTP validation that returns `422 Unprocessable Entity`.
|
||
|
||
`StoreLoanRequest` validates only the transport contract: valid JSON shape, required keys, and values that can be safely passed into the domain. Semantic acceptance remains the responsibility of `ValidationStage`.
|
||
|
||
### 7.2 `FraudCheckStage`
|
||
|
||
| Condition | Result |
|
||
| --- | --- |
|
||
| `customerId` starts with `FRAUD` | `FAIL` |
|
||
| `customerId` starts with `REVIEW` | `MANUAL_REVIEW` |
|
||
| Otherwise | `PASS` |
|
||
|
||
### 7.3 `GuarantorCheckStage`
|
||
|
||
This Stage exists only in the `BUSINESS` Workflow.
|
||
|
||
| Condition | Result |
|
||
| --- | --- |
|
||
| `hasGuarantor` is `false` | `FAIL` |
|
||
| `hasGuarantor` is `true` | `PASS` |
|
||
|
||
### 7.4 `CreditCheckStage`
|
||
|
||
| Configured condition | Result |
|
||
| --- | --- |
|
||
| Below `manualReviewMin` | `FAIL` |
|
||
| Between `manualReviewMin` and `manualReviewMax` | `MANUAL_REVIEW` |
|
||
| At least `approveFrom` | `PASS` |
|
||
|
||
All boundaries come from the validated rules of the assigned `CREDIT_CHECK` configuration step.
|
||
|
||
### 7.5 `ManagerApprovalStage`
|
||
|
||
The Stage is skipped when:
|
||
|
||
```text
|
||
amount <= activationThreshold
|
||
```
|
||
|
||
When it applies:
|
||
|
||
| Condition | Result |
|
||
| --- | --- |
|
||
| `amount > monthlyIncome × incomeMultiplier` | `FAIL` |
|
||
| Otherwise | `PASS` |
|
||
|
||
The Engine may ask a Stage whether it applies, or the Stage may return a successful no-op result. The chosen mechanism must be consistent for all conditional Stages and must not create a history record for a Stage that was not executed.
|
||
|
||
## 8. Workflow Engine
|
||
|
||
### Responsibilities
|
||
|
||
`WorkflowEngine` is responsible for:
|
||
|
||
- loading the ordered steps from the Loan's assigned `WorkflowConfiguration`;
|
||
- resolving every `StageDefinition.code` through the trusted handler registry;
|
||
- passing each step's validated rules to its Stage handler;
|
||
- finding the current executable Stage;
|
||
- skipping Stages that do not apply;
|
||
- executing each applicable Stage once;
|
||
- recording one history entry for each executed Stage;
|
||
- stopping on `FAIL` or `MANUAL_REVIEW`;
|
||
- approving the Loan after all applicable Stages pass;
|
||
- keeping `status` and `current_workflow_configuration_step_id` consistent.
|
||
|
||
It is not responsible for:
|
||
|
||
- parsing HTTP input;
|
||
- opening the database transaction;
|
||
- defining business thresholds;
|
||
- implementing Stage-specific rules;
|
||
- returning JSON responses.
|
||
|
||
### Processing algorithm
|
||
|
||
`ProcessLoanAction` performs the following operation:
|
||
|
||
```text
|
||
BEGIN TRANSACTION
|
||
↓
|
||
SELECT loan FOR UPDATE
|
||
↓
|
||
If status is terminal, return current state
|
||
↓
|
||
Load Loan's assigned published WorkflowConfiguration
|
||
↓
|
||
Set status to IN_PROGRESS
|
||
↓
|
||
For every remaining applicable configuration step:
|
||
set current_workflow_configuration_step_id
|
||
resolve trusted Stage handler
|
||
execute Stage with the step's rules
|
||
insert LoanHistory with rule snapshot
|
||
|
||
PASS → continue
|
||
FAIL → status = REJECTED; current step = null; stop
|
||
MANUAL_REVIEW → status = MANUAL_REVIEW; current step = null; stop
|
||
↓
|
||
All applicable Stages passed
|
||
↓
|
||
status = APPROVED; current step = null
|
||
COMMIT
|
||
```
|
||
|
||
If an unexpected exception occurs, the complete transaction rolls back. No partial history or half-updated Loan state may remain.
|
||
|
||
### Synchronous execution
|
||
|
||
`POST /api/v1/loans/{loanId}/process` executes all remaining Stages in the request and returns the resulting terminal state. The client does not call the endpoint once per Stage.
|
||
|
||
No Stage is dispatched to a queue in this version.
|
||
|
||
## 9. Idempotency and concurrency
|
||
|
||
The process endpoint must be safe when called repeatedly or concurrently.
|
||
|
||
### Application safeguards
|
||
|
||
- Return immediately when the Loan already has a terminal status.
|
||
- Execute only Stages without an existing history record.
|
||
- Lock the Loan row for the duration of processing.
|
||
|
||
### Database safeguard
|
||
|
||
The database must enforce:
|
||
|
||
```text
|
||
UNIQUE (loan_id, workflow_configuration_step_id)
|
||
```
|
||
|
||
on `loan_histories`.
|
||
|
||
Together, the row lock and unique constraint guarantee that:
|
||
|
||
- a configured step is not executed twice for the same Loan;
|
||
- duplicate history is not stored;
|
||
- concurrent requests cannot overwrite the final state;
|
||
- reprocessing a terminal Loan returns its existing state.
|
||
|
||
## 10. Database design
|
||
|
||
### 10.1 `loan_types`
|
||
|
||
| Column | Type | Constraints |
|
||
| --- | --- | --- |
|
||
| `id` | `bigint` | Primary key |
|
||
| `code` | `string` | Unique, stable machine identifier |
|
||
| `name` | `string` | Display name |
|
||
| `is_active` | `boolean` | Required, indexed |
|
||
| `created_at` | `timestamp` | Required |
|
||
| `updated_at` | `timestamp` | Required |
|
||
|
||
Seed `PERSONAL` and `BUSINESS`. Deactivating a type prevents new Loans but does not affect existing Loans.
|
||
|
||
### 10.2 `stage_definitions`
|
||
|
||
| Column | Type | Constraints |
|
||
| --- | --- | --- |
|
||
| `id` | `bigint` | Primary key |
|
||
| `code` | `string` | Unique; must exist in the trusted registry |
|
||
| `name` | `string` | Admin-facing display name |
|
||
| `is_active` | `boolean` | Required, indexed |
|
||
| `created_at` | `timestamp` | Required |
|
||
| `updated_at` | `timestamp` | Required |
|
||
|
||
This table controls what administrators can select. It does not store executable class names.
|
||
|
||
### 10.3 `workflow_configurations`
|
||
|
||
| Column | Type | Constraints |
|
||
| --- | --- | --- |
|
||
| `id` | `bigint` | Primary key |
|
||
| `loan_type_id` | `bigint` | Foreign key to `loan_types.id`, indexed |
|
||
| `name` | `string` | Admin-facing name |
|
||
| `version` | `integer` | Positive version number |
|
||
| `status` | `string` | Cast to `WorkflowConfigurationStatus`, indexed |
|
||
| `published_at` | `timestamp`, nullable | Set only when published |
|
||
| `created_by` | `bigint`, nullable | Foreign key to `users.id` |
|
||
| `created_at` | `timestamp` | Required |
|
||
| `updated_at` | `timestamp` | Required |
|
||
|
||
Constraints:
|
||
|
||
```text
|
||
UNIQUE (loan_type_id, version)
|
||
UNIQUE (id, loan_type_id)
|
||
```
|
||
|
||
PostgreSQL should enforce at most one `PUBLISHED` configuration per Loan type with a partial unique index. Publishing archives the previous version in the same transaction.
|
||
|
||
### 10.4 `workflow_configuration_steps`
|
||
|
||
| Column | Type | Constraints |
|
||
| --- | --- | --- |
|
||
| `id` | `bigint` | Primary key |
|
||
| `workflow_configuration_id` | `bigint` | Foreign key, indexed |
|
||
| `stage_definition_id` | `bigint` | Foreign key, indexed |
|
||
| `position` | `integer` | Positive execution order |
|
||
| `rules` | `jsonb` | Stage-specific validated rules |
|
||
| `is_enabled` | `boolean` | Required |
|
||
| `created_at` | `timestamp` | Required |
|
||
| `updated_at` | `timestamp` | Required |
|
||
|
||
Constraints:
|
||
|
||
```text
|
||
UNIQUE (workflow_configuration_id, position)
|
||
UNIQUE (workflow_configuration_id, stage_definition_id)
|
||
UNIQUE (id, workflow_configuration_id)
|
||
```
|
||
|
||
`rules` is cast to an array by Eloquent. Only draft steps may be created, updated, reordered, enabled, disabled, or deleted.
|
||
|
||
### 10.5 `loans`
|
||
|
||
| Column | Type | Constraints |
|
||
| --- | --- | --- |
|
||
| `id` | `bigint` | Primary key |
|
||
| `public_id` | `string` | Unique; exposed as API `loanId` |
|
||
| `customer_id` | `string` | Indexed |
|
||
| `loan_type_id` | `bigint` | Foreign key to `loan_types.id`, indexed |
|
||
| `workflow_configuration_id` | `bigint` | Immutable foreign key, indexed |
|
||
| `current_workflow_configuration_step_id` | `bigint`, nullable | Foreign key, indexed |
|
||
| `amount` | `bigint` | Required |
|
||
| `phone` | `string(11)` | Required |
|
||
| `monthly_income` | `bigint` | Required |
|
||
| `credit_score` | `integer` | Required |
|
||
| `has_guarantor` | `boolean` | Required |
|
||
| `status` | `string` | Required, indexed; cast to `LoanStatus` |
|
||
| `created_at` | `timestamp` | Required |
|
||
| `updated_at` | `timestamp` | Required |
|
||
|
||
`workflow_configuration_id` is assigned when the Loan is created and must never change. The configuration's `loan_type_id` must match the Loan's `loan_type_id`, and the current step must belong to that configuration. Enforce both invariants with composite foreign keys in PostgreSQL as well as validation in the creation and processing Actions.
|
||
|
||
```text
|
||
FOREIGN KEY (workflow_configuration_id, loan_type_id)
|
||
REFERENCES workflow_configurations(id, loan_type_id)
|
||
|
||
FOREIGN KEY (current_workflow_configuration_step_id, workflow_configuration_id)
|
||
REFERENCES workflow_configuration_steps(id, workflow_configuration_id)
|
||
```
|
||
|
||
The API's `loanType` and `currentStage` values are derived from the related `LoanType.code` and current step's `StageDefinition.code`.
|
||
|
||
### 10.6 `loan_histories`
|
||
|
||
| Column | Type | Constraints |
|
||
| --- | --- | --- |
|
||
| `id` | `bigint` | Primary key |
|
||
| `loan_id` | `bigint` | Foreign key to `loans.id`, indexed |
|
||
| `workflow_configuration_step_id` | `bigint` | Foreign key, indexed |
|
||
| `stage_code` | `string` | Immutable audit snapshot |
|
||
| `rules_snapshot` | `jsonb` | Immutable rules used for execution |
|
||
| `result` | `string` | Cast to `StageResultType` |
|
||
| `reason` | `string` | Required |
|
||
| `executed_at` | `timestamp` | Required, indexed |
|
||
| `created_at` | `timestamp` | Required |
|
||
| `updated_at` | `timestamp` | Required |
|
||
|
||
Constraints:
|
||
|
||
```text
|
||
FOREIGN KEY (loan_id) REFERENCES loans(id) ON DELETE CASCADE
|
||
UNIQUE (loan_id, workflow_configuration_step_id)
|
||
INDEX (loan_id, executed_at)
|
||
```
|
||
|
||
The snapshots preserve the exact Stage code and rule values used for the decision even if display metadata changes later. History is ordered by `executed_at`, then by `id` as a deterministic tie-breaker.
|
||
|
||
Published or referenced configurations and steps must never be hard-deleted. Archive them so Loan execution and audit history remain reproducible.
|
||
|
||
### Relationships
|
||
|
||
```text
|
||
LoanType hasMany WorkflowConfiguration
|
||
LoanType hasMany Loan
|
||
|
||
WorkflowConfiguration belongsTo LoanType
|
||
WorkflowConfiguration hasMany WorkflowConfigurationStep
|
||
WorkflowConfiguration hasMany Loan
|
||
|
||
WorkflowConfigurationStep belongsTo WorkflowConfiguration
|
||
WorkflowConfigurationStep belongsTo StageDefinition
|
||
WorkflowConfigurationStep hasMany LoanHistory
|
||
|
||
StageDefinition hasMany WorkflowConfigurationStep
|
||
|
||
Loan belongsTo LoanType
|
||
Loan belongsTo WorkflowConfiguration
|
||
Loan belongsTo WorkflowConfigurationStep as currentStep
|
||
Loan hasMany LoanHistory
|
||
|
||
LoanHistory belongsTo Loan
|
||
LoanHistory belongsTo WorkflowConfigurationStep
|
||
```
|
||
|
||
All Eloquent relationships require explicit Laravel relationship return types. JSON, boolean, status, and timestamp columns require model casts.
|
||
|
||
## 11. Actions
|
||
|
||
### `CreateLoanAction`
|
||
|
||
- Accepts transport-safe request data.
|
||
- Resolves the active `LoanType` by its stable code.
|
||
- Resolves that type's single published `WorkflowConfiguration`.
|
||
- Fails clearly when no published configuration exists.
|
||
- Generates a unique public Loan identifier.
|
||
- Stores the immutable `loan_type_id` and `workflow_configuration_id` assignments.
|
||
- Stores the Loan with `SUBMITTED` status and the first configured step as its current step.
|
||
- Does not execute the Workflow.
|
||
|
||
### `ProcessLoanAction`
|
||
|
||
- Opens the database transaction.
|
||
- loads and locks the Loan row;
|
||
- returns an existing terminal Loan without changing it;
|
||
- eager-loads the assigned configuration, ordered steps, and Stage definitions;
|
||
- invokes `WorkflowEngine`;
|
||
- returns the final Loan state.
|
||
|
||
### `GetLoanAction`
|
||
|
||
- Finds a Loan by `public_id`.
|
||
- Throws the application’s not-found exception when it does not exist.
|
||
|
||
### `GetLoanHistoryAction`
|
||
|
||
- Finds the Loan by `public_id`.
|
||
- Returns its history in deterministic chronological order.
|
||
- Eager-loads required relationships and avoids N+1 queries.
|
||
|
||
### `CreateWorkflowConfigurationAction`
|
||
|
||
- Creates the next draft version for a `loan_type_id`.
|
||
- Copies an existing version's steps when requested.
|
||
- Never creates a second draft version number.
|
||
|
||
### `UpdateWorkflowConfigurationAction`
|
||
|
||
- Updates only a `DRAFT` configuration.
|
||
- Validates selected Stage definitions, positions, and Stage-specific rules.
|
||
- Replaces or updates steps atomically so a partial ordering is never exposed.
|
||
|
||
### `PublishWorkflowConfigurationAction`
|
||
|
||
- Locks the Loan type's configurations.
|
||
- Fully validates the draft and its ordered steps.
|
||
- Archives the currently published version.
|
||
- Publishes the selected draft with `published_at` and `created_by` audit data.
|
||
- Performs all status changes inside one database transaction.
|
||
|
||
## 12. HTTP and API design
|
||
|
||
The detailed contract is defined in [api-routes.md](./api-routes.md).
|
||
|
||
### Routes
|
||
|
||
```text
|
||
POST /api/v1/loans
|
||
POST /api/v1/loans/{loanId}/process
|
||
GET /api/v1/loans/{loanId}
|
||
GET /api/v1/loans/{loanId}/history
|
||
GET /health
|
||
```
|
||
|
||
### Controller rules
|
||
|
||
`LoanController` must:
|
||
|
||
- remain thin;
|
||
- use route model binding or an Action-based public-ID lookup consistently;
|
||
- invoke exactly one Action per endpoint;
|
||
- return API Resources;
|
||
- contain no Workflow or validation business rules.
|
||
|
||
### HTTP validation boundary
|
||
|
||
| Failure | Handling |
|
||
| --- | --- |
|
||
| Malformed JSON or unusable transport structure | `400 INVALID_REQUEST` |
|
||
| Semantically invalid amount, phone, score, or other loan value | Loan is created; `ValidationStage` later returns `FAIL` |
|
||
| Unknown Loan public ID | `404 LOAN_NOT_FOUND` |
|
||
|
||
Do not return Laravel’s default HTML error pages or default `422` validation payload for business validation failures.
|
||
|
||
### Admin configuration boundary
|
||
|
||
The admin panel operates on `LoanType`, `WorkflowConfiguration`, and `WorkflowConfigurationStep` through the dedicated admin Controller, Form Requests, and Actions. It must support:
|
||
|
||
- listing configurations by `loan_type_id`;
|
||
- creating a draft or copying an existing version;
|
||
- selecting active Stage definitions;
|
||
- ordering steps;
|
||
- editing Stage-specific rules;
|
||
- validating a draft;
|
||
- publishing a draft;
|
||
- viewing archived versions and their assigned Loans.
|
||
|
||
All admin mutations require authorization. Published and archived configurations are read-only. The public loan API must never accept a client-supplied `workflow_configuration_id`; the server selects the published configuration for the requested Loan type.
|
||
|
||
## 13. Logging and error handling
|
||
|
||
Business history and operational logs are separate concerns.
|
||
|
||
### `loan_histories`
|
||
|
||
Stores expected business outcomes:
|
||
|
||
- successful Stage execution;
|
||
- validation failure;
|
||
- fraud rejection;
|
||
- manual-review decision;
|
||
- credit or manager rejection.
|
||
|
||
### Laravel logs
|
||
|
||
Store unexpected operational failures:
|
||
|
||
- database errors;
|
||
- invalid Workflow configuration;
|
||
- unresolvable Stage classes;
|
||
- unexpected exceptions.
|
||
|
||
Operational logs should include `loanId`, current Stage, and exception context where available, but must not replace persisted business history.
|
||
|
||
## 14. Test blueprint
|
||
|
||
The project uses Pest 4.
|
||
|
||
### Unit tests
|
||
|
||
Each Stage is tested independently with all boundary conditions.
|
||
|
||
#### `ValidationStageTest`
|
||
|
||
- valid application passes;
|
||
- empty customer ID fails;
|
||
- zero and negative amounts fail;
|
||
- valid and invalid phone formats;
|
||
- invalid loan type fails;
|
||
- negative monthly income fails;
|
||
- credit scores below `0` and above `1000` fail.
|
||
|
||
#### `FraudCheckStageTest`
|
||
|
||
- `FRAUD` prefix fails;
|
||
- `REVIEW` prefix requests manual review;
|
||
- any other prefix passes.
|
||
|
||
#### `GuarantorCheckStageTest`
|
||
|
||
- missing guarantor fails;
|
||
- existing guarantor passes.
|
||
|
||
#### `CreditCheckStageTest`
|
||
|
||
- score below manual range fails;
|
||
- lower and upper manual-range boundaries request manual review;
|
||
- score at the approval boundary passes;
|
||
- overridden configuration changes behavior.
|
||
|
||
#### `ManagerApprovalStageTest`
|
||
|
||
- Stage does not apply below or at its activation threshold;
|
||
- excessive income multiple fails;
|
||
- acceptable income multiple passes;
|
||
- overridden configuration changes behavior.
|
||
|
||
#### `WorkflowEngineTest`
|
||
|
||
- runs Stages in configured order;
|
||
- stops on `FAIL`;
|
||
- stops on `MANUAL_REVIEW`;
|
||
- approves after all Stages pass;
|
||
- skips non-applicable Stages;
|
||
- records each executed Stage once;
|
||
- does not record skipped Stages.
|
||
|
||
#### `WorkflowConfigurationResolverTest`
|
||
|
||
- resolves the Loan's assigned configuration rather than the latest version;
|
||
- resolves steps in ascending position;
|
||
- maps every Stage code to a handler implementing `StageInterface`;
|
||
- rejects missing or inactive Stage definitions;
|
||
- rejects invalid rule schemas.
|
||
|
||
#### `WorkflowConfigurationPublishingTest`
|
||
|
||
- only drafts can be changed or published;
|
||
- publishing archives the previous version;
|
||
- only one version per Loan type is published;
|
||
- invalid Stage order and rule ranges cannot be published;
|
||
- a published configuration is immutable.
|
||
|
||
### Feature tests
|
||
|
||
#### `CreateLoanTest`
|
||
|
||
- creates a Loan with the expected initial state;
|
||
- assigns the published configuration for the selected `loan_type_id`;
|
||
- rejects creation when the Loan type has no published configuration;
|
||
- does not accept a client-selected configuration ID;
|
||
- generates a unique public ID;
|
||
- returns `400 INVALID_REQUEST` for malformed JSON;
|
||
- accepts semantically invalid business values for later Workflow validation.
|
||
|
||
#### `ProcessLoanTest`
|
||
|
||
- approves a valid personal Loan;
|
||
- approves a valid business Loan with a guarantor;
|
||
- rejects each validation failure;
|
||
- handles fraud failure and manual review;
|
||
- rejects a business Loan without a guarantor;
|
||
- handles all credit-score outcomes;
|
||
- executes conditional manager approval;
|
||
- returns `404 LOAN_NOT_FOUND`;
|
||
- remains idempotent after every terminal status;
|
||
- does not create duplicate history.
|
||
|
||
#### `GetLoanTest`
|
||
|
||
- returns the complete Loan representation;
|
||
- returns `404 LOAN_NOT_FOUND` for an unknown ID.
|
||
|
||
#### `GetLoanHistoryTest`
|
||
|
||
- returns Stage history chronologically;
|
||
- returns each Stage once;
|
||
- returns `404 LOAN_NOT_FOUND` for an unknown ID.
|
||
|
||
#### `HealthCheckTest`
|
||
|
||
- returns `200 OK` with `{"status":"UP"}`.
|
||
|
||
#### Admin Workflow configuration tests
|
||
|
||
- creates and updates a draft for a Loan type;
|
||
- copies an existing version without sharing mutable steps;
|
||
- rejects duplicate Stage positions and definitions;
|
||
- rejects invalid Stage-specific rule JSON;
|
||
- publishes atomically;
|
||
- prevents modification of published and archived versions;
|
||
- keeps in-progress Loans assigned to their original version;
|
||
- assigns newly created Loans to the newly published version;
|
||
- rejects unauthorized configuration mutations.
|
||
|
||
### Persistence verification
|
||
|
||
Feature tests use the real database integration path rather than mocking Eloquent. Persistence behavior must also be verified through the Dockerized PostgreSQL environment.
|
||
|
||
## 15. Docker and runtime
|
||
|
||
The application must be buildable and runnable using:
|
||
|
||
```bash
|
||
docker build -t bankflow .
|
||
docker run -p 8080:8080 bankflow
|
||
```
|
||
|
||
Requirements:
|
||
|
||
- the service listens on port `8080`;
|
||
- PostgreSQL-backed data survives an application process restart;
|
||
- the trusted Stage registry is loaded at startup and published Workflow configurations are loaded from PostgreSQL;
|
||
- the health endpoint becomes available within 60 seconds;
|
||
- the production image contains only runtime requirements;
|
||
- migrations are run through an explicit, documented startup strategy.
|
||
|
||
Docker Compose may be provided for local development, but it must not replace the required `docker build` and `docker run` workflow.
|
||
|
||
## 16. Required documentation
|
||
|
||
| File | Purpose |
|
||
| --- | --- |
|
||
| `README.md` | Build, run, test, dependencies, and project overview |
|
||
| `DESIGN.md` | Architecture, Workflow model, persistence, and extensibility |
|
||
| `ENGINEERING_DECISIONS.md` | Decisions, alternatives, and trade-offs |
|
||
| `TESTING.md` | Unit and feature test strategy and coverage |
|
||
|
||
These files must describe the implemented system, not an aspirational design that differs from the code.
|
||
|
||
## 17. Implementation order
|
||
|
||
Implementation should proceed in small, reviewable phases:
|
||
|
||
1. Project configuration and PostgreSQL connection.
|
||
2. Domain Enums and `ExecutionResult`.
|
||
3. Loan and LoanHistory migrations, models, factories, and relationships.
|
||
4. Loan type, Stage definition, Workflow configuration, and step models.
|
||
5. Trusted Stage registry and initial Workflow seed data.
|
||
6. `StageInterface` and individual stateless Stages.
|
||
7. `WorkflowConfigurationResolver` and publishing validation.
|
||
8. `WorkflowEngine`.
|
||
9. Loan Actions and transaction/concurrency behavior.
|
||
10. Admin Workflow configuration Actions and HTTP boundary.
|
||
11. Public Form Request, API Resources, Controller, and routes.
|
||
12. Unit tests.
|
||
13. Feature and persistence tests.
|
||
14. Docker image and runtime verification.
|
||
15. Required project documentation.
|
||
|
||
Each phase must pass its focused tests before the next phase begins.
|
||
|
||
## 18. Definition of done
|
||
|
||
The implementation is complete when:
|
||
|
||
- all documented routes conform to the API contract;
|
||
- all Workflow stages execute synchronously in configured order;
|
||
- business validation is recorded as Workflow history;
|
||
- terminal processing is idempotent;
|
||
- concurrent processing cannot duplicate Stage execution;
|
||
- Loan state and history remain consistent after failures;
|
||
- data persists after application restart;
|
||
- business thresholds can change without modifying Stage code;
|
||
- administrators can publish a versioned Workflow per Loan type;
|
||
- Loans already in progress retain their originally assigned configuration;
|
||
- a new Stage can be added without rewriting the Engine or existing Stages;
|
||
- all unit and feature tests pass;
|
||
- the Docker commands in the challenge work as documented;
|
||
- required documentation matches the final implementation.
|