make app database
This commit is contained in:
15
README.md
15
README.md
@@ -6,7 +6,7 @@ Each loan is assigned an immutable published workflow configuration when it is c
|
||||
|
||||
## Project status
|
||||
|
||||
The repository is currently in the **blueprint and scaffolding phase**. The Laravel application skeleton and project documentation exist, but the BankFlow models, API routes, workflow engine, PostgreSQL schema, admin configuration endpoints, and Docker image have not yet been implemented.
|
||||
The repository is currently under implementation. The domain enums, Eloquent models, factories, database schema, and local development seeder are implemented. The API routes, workflow engine, admin configuration endpoints, and Docker image have not yet been implemented.
|
||||
|
||||
The approved implementation plan is documented in [Project Blueprint](docs/project-blueprint.md).
|
||||
|
||||
@@ -180,12 +180,23 @@ DB_USERNAME=postgres
|
||||
DB_PASSWORD=
|
||||
```
|
||||
|
||||
After the BankFlow migrations and seeders are implemented, initialize the database with:
|
||||
Initialize the database and load development sample data with:
|
||||
|
||||
```bash
|
||||
php artisan migrate --seed
|
||||
```
|
||||
|
||||
`DatabaseSeeder` runs `BankFlowDevelopmentSeeder` only in the local environment. It creates:
|
||||
|
||||
- an admin user (`admin@bankflow.test` / `password`);
|
||||
- `PERSONAL` and `BUSINESS` loan types;
|
||||
- all supported stage definitions;
|
||||
- one published workflow per loan type with configured rules;
|
||||
- submitted, approved, rejected, and manual-review loan examples;
|
||||
- processing histories linked to the exact workflow steps used.
|
||||
|
||||
For a clean development rebuild, use `php artisan migrate:fresh --seed`. This command deletes all existing database data.
|
||||
|
||||
Build frontend assets when required:
|
||||
|
||||
```bash
|
||||
|
||||
12
app/Domain/Loan/Enums/LoanStage.php
Normal file
12
app/Domain/Loan/Enums/LoanStage.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Loan\Enums;
|
||||
|
||||
enum LoanStage: string
|
||||
{
|
||||
case Validation = 'VALIDATION';
|
||||
case FraudCheck = 'FRAUD_CHECK';
|
||||
case GuarantorCheck = 'GUARANTOR_CHECK';
|
||||
case CreditCheck = 'CREDIT_CHECK';
|
||||
case ManagerApproval = 'MANAGER_APPROVAL';
|
||||
}
|
||||
20
app/Domain/Loan/Enums/LoanStatus.php
Normal file
20
app/Domain/Loan/Enums/LoanStatus.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Loan\Enums;
|
||||
|
||||
enum LoanStatus: string
|
||||
{
|
||||
case Submitted = 'SUBMITTED';
|
||||
case InProgress = 'IN_PROGRESS';
|
||||
case ManualReview = 'MANUAL_REVIEW';
|
||||
case Approved = 'APPROVED';
|
||||
case Rejected = 'REJECTED';
|
||||
|
||||
public function isTerminal(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::ManualReview, self::Approved, self::Rejected => true,
|
||||
self::Submitted, self::InProgress => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
10
app/Domain/Loan/Enums/StageResultType.php
Normal file
10
app/Domain/Loan/Enums/StageResultType.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Loan\Enums;
|
||||
|
||||
enum StageResultType: string
|
||||
{
|
||||
case Pass = 'PASS';
|
||||
case Fail = 'FAIL';
|
||||
case ManualReview = 'MANUAL_REVIEW';
|
||||
}
|
||||
10
app/Domain/Loan/Enums/WorkflowConfigurationStatus.php
Normal file
10
app/Domain/Loan/Enums/WorkflowConfigurationStatus.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Loan\Enums;
|
||||
|
||||
enum WorkflowConfigurationStatus: string
|
||||
{
|
||||
case Draft = 'DRAFT';
|
||||
case Published = 'PUBLISHED';
|
||||
case Archived = 'ARCHIVED';
|
||||
}
|
||||
73
app/Models/Loan.php
Normal file
73
app/Models/Loan.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Domain\Loan\Enums\LoanStatus;
|
||||
use Database\Factories\LoanFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable([
|
||||
'public_id',
|
||||
'customer_id',
|
||||
'loan_type_id',
|
||||
'workflow_configuration_id',
|
||||
'current_workflow_configuration_step_id',
|
||||
'amount',
|
||||
'phone',
|
||||
'monthly_income',
|
||||
'credit_score',
|
||||
'has_guarantor',
|
||||
'status',
|
||||
])]
|
||||
class Loan extends Model
|
||||
{
|
||||
/** @use HasFactory<LoanFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $attributes = [
|
||||
'status' => LoanStatus::Submitted->value,
|
||||
];
|
||||
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'public_id';
|
||||
}
|
||||
|
||||
public function loanType(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(LoanType::class);
|
||||
}
|
||||
|
||||
public function workflowConfiguration(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WorkflowConfiguration::class);
|
||||
}
|
||||
|
||||
public function currentStep(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WorkflowConfigurationStep::class, 'current_workflow_configuration_step_id');
|
||||
}
|
||||
|
||||
public function histories(): HasMany
|
||||
{
|
||||
return $this->hasMany(LoanHistory::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'amount' => 'integer',
|
||||
'monthly_income' => 'integer',
|
||||
'credit_score' => 'integer',
|
||||
'has_guarantor' => 'boolean',
|
||||
'status' => LoanStatus::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
49
app/Models/LoanHistory.php
Normal file
49
app/Models/LoanHistory.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Domain\Loan\Enums\LoanStage;
|
||||
use App\Domain\Loan\Enums\StageResultType;
|
||||
use Database\Factories\LoanHistoryFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Fillable([
|
||||
'loan_id',
|
||||
'workflow_configuration_step_id',
|
||||
'stage_code',
|
||||
'rules_snapshot',
|
||||
'result',
|
||||
'reason',
|
||||
'executed_at',
|
||||
])]
|
||||
class LoanHistory extends Model
|
||||
{
|
||||
/** @use HasFactory<LoanHistoryFactory> */
|
||||
use HasFactory;
|
||||
|
||||
public function loan(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Loan::class);
|
||||
}
|
||||
|
||||
public function workflowConfigurationStep(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WorkflowConfigurationStep::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'stage_code' => LoanStage::class,
|
||||
'rules_snapshot' => 'array',
|
||||
'result' => StageResultType::class,
|
||||
'executed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
36
app/Models/LoanType.php
Normal file
36
app/Models/LoanType.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\LoanTypeFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['code', 'name', 'is_active'])]
|
||||
class LoanType extends Model
|
||||
{
|
||||
/** @use HasFactory<LoanTypeFactory> */
|
||||
use HasFactory;
|
||||
|
||||
public function workflowConfigurations(): HasMany
|
||||
{
|
||||
return $this->hasMany(WorkflowConfiguration::class);
|
||||
}
|
||||
|
||||
public function loans(): HasMany
|
||||
{
|
||||
return $this->hasMany(Loan::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
31
app/Models/StageDefinition.php
Normal file
31
app/Models/StageDefinition.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\StageDefinitionFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['code', 'name', 'is_active'])]
|
||||
class StageDefinition extends Model
|
||||
{
|
||||
/** @use HasFactory<StageDefinitionFactory> */
|
||||
use HasFactory;
|
||||
|
||||
public function workflowConfigurationSteps(): HasMany
|
||||
{
|
||||
return $this->hasMany(WorkflowConfigurationStep::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Models/WorkflowConfiguration.php
Normal file
58
app/Models/WorkflowConfiguration.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Domain\Loan\Enums\WorkflowConfigurationStatus;
|
||||
use Database\Factories\WorkflowConfigurationFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['loan_type_id', 'name', 'version', 'status', 'published_at', 'created_by'])]
|
||||
class WorkflowConfiguration extends Model
|
||||
{
|
||||
/** @use HasFactory<WorkflowConfigurationFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $attributes = [
|
||||
'status' => WorkflowConfigurationStatus::Draft->value,
|
||||
];
|
||||
|
||||
public function loanType(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(LoanType::class);
|
||||
}
|
||||
|
||||
public function steps(): HasMany
|
||||
{
|
||||
return $this->hasMany(WorkflowConfigurationStep::class)->orderBy('position');
|
||||
}
|
||||
|
||||
public function loans(): HasMany
|
||||
{
|
||||
return $this->hasMany(Loan::class);
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function isEditable(): bool
|
||||
{
|
||||
return $this->status === WorkflowConfigurationStatus::Draft;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => WorkflowConfigurationStatus::class,
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
43
app/Models/WorkflowConfigurationStep.php
Normal file
43
app/Models/WorkflowConfigurationStep.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\WorkflowConfigurationStepFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['workflow_configuration_id', 'stage_definition_id', 'position', 'rules', 'is_enabled'])]
|
||||
class WorkflowConfigurationStep extends Model
|
||||
{
|
||||
/** @use HasFactory<WorkflowConfigurationStepFactory> */
|
||||
use HasFactory;
|
||||
|
||||
public function workflowConfiguration(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WorkflowConfiguration::class);
|
||||
}
|
||||
|
||||
public function stageDefinition(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(StageDefinition::class);
|
||||
}
|
||||
|
||||
public function loanHistories(): HasMany
|
||||
{
|
||||
return $this->hasMany(LoanHistory::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'rules' => 'array',
|
||||
'is_enabled' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
73
database/factories/LoanFactory.php
Normal file
73
database/factories/LoanFactory.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Domain\Loan\Enums\LoanStatus;
|
||||
use App\Domain\Loan\Enums\StageResultType;
|
||||
use App\Models\Loan;
|
||||
use App\Models\LoanHistory;
|
||||
use App\Models\WorkflowConfiguration;
|
||||
use App\Models\WorkflowConfigurationStep;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<Loan>
|
||||
*/
|
||||
class LoanFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'public_id' => 'L-'.fake()->unique()->numerify('#####'),
|
||||
'customer_id' => 'C-'.fake()->unique()->numerify('####'),
|
||||
'workflow_configuration_id' => WorkflowConfiguration::factory()->published(),
|
||||
'loan_type_id' => static fn (array $attributes): int => WorkflowConfiguration::query()
|
||||
->findOrFail($attributes['workflow_configuration_id'])
|
||||
->loan_type_id,
|
||||
'current_workflow_configuration_step_id' => null,
|
||||
'amount' => fake()->numberBetween(1_000_000, 1_000_000_000),
|
||||
'phone' => '09'.fake()->numerify('#########'),
|
||||
'monthly_income' => fake()->numberBetween(1_000_000, 100_000_000),
|
||||
'credit_score' => fake()->numberBetween(0, 1000),
|
||||
'has_guarantor' => fake()->boolean(),
|
||||
'status' => LoanStatus::Submitted,
|
||||
];
|
||||
}
|
||||
|
||||
public function forWorkflow(WorkflowConfiguration $workflowConfiguration): static
|
||||
{
|
||||
return $this
|
||||
->for($workflowConfiguration->loanType, 'loanType')
|
||||
->for($workflowConfiguration, 'workflowConfiguration');
|
||||
}
|
||||
|
||||
public function atStep(WorkflowConfigurationStep $step): static
|
||||
{
|
||||
return $this->forWorkflow($step->workflowConfiguration)->state([
|
||||
'current_workflow_configuration_step_id' => $step->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function withHistory(
|
||||
WorkflowConfigurationStep $step,
|
||||
StageResultType $result = StageResultType::Pass,
|
||||
string $reason = 'SUCCESS',
|
||||
): static {
|
||||
return $this->has(
|
||||
LoanHistory::factory()
|
||||
->for($step, 'workflowConfigurationStep')
|
||||
->state([
|
||||
'stage_code' => $step->stageDefinition->code,
|
||||
'rules_snapshot' => $step->rules,
|
||||
'result' => $result,
|
||||
'reason' => $reason,
|
||||
]),
|
||||
'histories',
|
||||
);
|
||||
}
|
||||
}
|
||||
45
database/factories/LoanHistoryFactory.php
Normal file
45
database/factories/LoanHistoryFactory.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Domain\Loan\Enums\StageResultType;
|
||||
use App\Models\Loan;
|
||||
use App\Models\LoanHistory;
|
||||
use App\Models\WorkflowConfigurationStep;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<LoanHistory>
|
||||
*/
|
||||
class LoanHistoryFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'loan_id' => Loan::factory(),
|
||||
'workflow_configuration_step_id' => static function (array $attributes): int {
|
||||
$loan = Loan::query()->findOrFail($attributes['loan_id']);
|
||||
|
||||
return WorkflowConfigurationStep::factory()
|
||||
->for($loan->workflowConfiguration)
|
||||
->create()
|
||||
->getKey();
|
||||
},
|
||||
'stage_code' => static fn (array $attributes): string => WorkflowConfigurationStep::query()
|
||||
->findOrFail($attributes['workflow_configuration_step_id'])
|
||||
->stageDefinition
|
||||
->code,
|
||||
'rules_snapshot' => static fn (array $attributes): array => WorkflowConfigurationStep::query()
|
||||
->findOrFail($attributes['workflow_configuration_step_id'])
|
||||
->rules,
|
||||
'result' => StageResultType::Pass,
|
||||
'reason' => 'SUCCESS',
|
||||
'executed_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
33
database/factories/LoanTypeFactory.php
Normal file
33
database/factories/LoanTypeFactory.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\LoanType;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<LoanType>
|
||||
*/
|
||||
class LoanTypeFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'code' => fake()->unique()->bothify('TYPE_????'),
|
||||
'name' => fake()->words(2, true),
|
||||
'is_active' => true,
|
||||
];
|
||||
}
|
||||
|
||||
public function inactive(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'is_active' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
44
database/factories/StageDefinitionFactory.php
Normal file
44
database/factories/StageDefinitionFactory.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Domain\Loan\Enums\LoanStage;
|
||||
use App\Models\StageDefinition;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<StageDefinition>
|
||||
*/
|
||||
class StageDefinitionFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$stage = fake()->randomElement(LoanStage::cases());
|
||||
|
||||
return [
|
||||
'code' => $stage->value,
|
||||
'name' => str($stage->name)->headline()->toString(),
|
||||
'is_active' => true,
|
||||
];
|
||||
}
|
||||
|
||||
public function forStage(LoanStage $stage): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'code' => $stage->value,
|
||||
'name' => str($stage->name)->headline()->toString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function inactive(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'is_active' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
65
database/factories/WorkflowConfigurationFactory.php
Normal file
65
database/factories/WorkflowConfigurationFactory.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Domain\Loan\Enums\WorkflowConfigurationStatus;
|
||||
use App\Models\LoanType;
|
||||
use App\Models\StageDefinition;
|
||||
use App\Models\WorkflowConfiguration;
|
||||
use App\Models\WorkflowConfigurationStep;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<WorkflowConfiguration>
|
||||
*/
|
||||
class WorkflowConfigurationFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'loan_type_id' => LoanType::factory(),
|
||||
'name' => fake()->words(3, true),
|
||||
'version' => 1,
|
||||
'status' => WorkflowConfigurationStatus::Draft,
|
||||
'published_at' => null,
|
||||
'created_by' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function published(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => WorkflowConfigurationStatus::Published,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function archived(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => WorkflowConfigurationStatus::Archived,
|
||||
'published_at' => fake()->dateTimeBetween('-1 year', '-1 day'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $rules
|
||||
*/
|
||||
public function withStep(StageDefinition $stageDefinition, int $position, array $rules = []): static
|
||||
{
|
||||
return $this->has(
|
||||
WorkflowConfigurationStep::factory()
|
||||
->for($stageDefinition, 'stageDefinition')
|
||||
->state([
|
||||
'position' => $position,
|
||||
'rules' => $rules,
|
||||
]),
|
||||
'steps',
|
||||
);
|
||||
}
|
||||
}
|
||||
37
database/factories/WorkflowConfigurationStepFactory.php
Normal file
37
database/factories/WorkflowConfigurationStepFactory.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\StageDefinition;
|
||||
use App\Models\WorkflowConfiguration;
|
||||
use App\Models\WorkflowConfigurationStep;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<WorkflowConfigurationStep>
|
||||
*/
|
||||
class WorkflowConfigurationStepFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'workflow_configuration_id' => WorkflowConfiguration::factory(),
|
||||
'stage_definition_id' => StageDefinition::factory(),
|
||||
'position' => fake()->unique()->numberBetween(1, 1000),
|
||||
'rules' => [],
|
||||
'is_enabled' => true,
|
||||
];
|
||||
}
|
||||
|
||||
public function disabled(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'is_enabled' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('loan_types', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code')->unique();
|
||||
$table->string('name');
|
||||
$table->boolean('is_active')->default(true)->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('loan_types');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('stage_definitions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code')->unique();
|
||||
$table->string('name');
|
||||
$table->boolean('is_active')->default(true)->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('stage_definitions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use App\Domain\Loan\Enums\WorkflowConfigurationStatus;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('workflow_configurations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('loan_type_id')->constrained()->restrictOnDelete();
|
||||
$table->string('name');
|
||||
$table->unsignedInteger('version');
|
||||
$table->string('status')->default(WorkflowConfigurationStatus::Draft->value)->index();
|
||||
$table->timestamp('published_at')->nullable()->index();
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['loan_type_id', 'version']);
|
||||
$table->unique(['id', 'loan_type_id']);
|
||||
});
|
||||
|
||||
if (DB::getDriverName() === 'pgsql') {
|
||||
DB::statement("CREATE UNIQUE INDEX workflow_configurations_one_published_per_loan_type ON workflow_configurations (loan_type_id) WHERE status = 'PUBLISHED'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('workflow_configurations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('workflow_configuration_steps', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('workflow_configuration_id')->constrained()->restrictOnDelete();
|
||||
$table->foreignId('stage_definition_id')->constrained()->restrictOnDelete();
|
||||
$table->unsignedInteger('position');
|
||||
$table->json('rules');
|
||||
$table->boolean('is_enabled')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['workflow_configuration_id', 'position']);
|
||||
$table->unique(['workflow_configuration_id', 'stage_definition_id']);
|
||||
$table->unique(['id', 'workflow_configuration_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('workflow_configuration_steps');
|
||||
}
|
||||
};
|
||||
49
database/migrations/2026_07_22_114910_create_loans_table.php
Normal file
49
database/migrations/2026_07_22_114910_create_loans_table.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use App\Domain\Loan\Enums\LoanStatus;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('loans', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('public_id')->unique();
|
||||
$table->string('customer_id')->index();
|
||||
$table->foreignId('loan_type_id');
|
||||
$table->foreignId('workflow_configuration_id');
|
||||
$table->foreignId('current_workflow_configuration_step_id')->nullable();
|
||||
$table->unsignedBigInteger('amount');
|
||||
$table->string('phone', 11);
|
||||
$table->unsignedBigInteger('monthly_income');
|
||||
$table->unsignedInteger('credit_score');
|
||||
$table->boolean('has_guarantor');
|
||||
$table->string('status')->default(LoanStatus::Submitted->value)->index();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign(['workflow_configuration_id', 'loan_type_id'])
|
||||
->references(['id', 'loan_type_id'])
|
||||
->on('workflow_configurations')
|
||||
->restrictOnDelete();
|
||||
$table->foreign(['current_workflow_configuration_step_id', 'workflow_configuration_id'])
|
||||
->references(['id', 'workflow_configuration_id'])
|
||||
->on('workflow_configuration_steps')
|
||||
->restrictOnDelete();
|
||||
$table->index('current_workflow_configuration_step_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('loans');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('loan_histories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('loan_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('workflow_configuration_step_id')->constrained()->restrictOnDelete();
|
||||
$table->string('stage_code');
|
||||
$table->json('rules_snapshot');
|
||||
$table->string('result');
|
||||
$table->string('reason');
|
||||
$table->timestamp('executed_at')->index();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['loan_id', 'workflow_configuration_step_id']);
|
||||
$table->index(['loan_id', 'executed_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('loan_histories');
|
||||
}
|
||||
};
|
||||
180
database/seeders/BankFlowDevelopmentSeeder.php
Normal file
180
database/seeders/BankFlowDevelopmentSeeder.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Domain\Loan\Enums\LoanStage;
|
||||
use App\Domain\Loan\Enums\LoanStatus;
|
||||
use App\Domain\Loan\Enums\StageResultType;
|
||||
use App\Models\Loan;
|
||||
use App\Models\LoanType;
|
||||
use App\Models\StageDefinition;
|
||||
use App\Models\User;
|
||||
use App\Models\WorkflowConfiguration;
|
||||
use App\Models\WorkflowConfigurationStep;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class BankFlowDevelopmentSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
if (! app()->environment('local')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$admin = User::factory()->create([
|
||||
'name' => 'BankFlow Admin',
|
||||
'email' => 'admin@bankflow.test',
|
||||
]);
|
||||
|
||||
$personalLoanType = LoanType::factory()->create([
|
||||
'code' => 'PERSONAL',
|
||||
'name' => 'Personal Loan',
|
||||
]);
|
||||
|
||||
$businessLoanType = LoanType::factory()->create([
|
||||
'code' => 'BUSINESS',
|
||||
'name' => 'Business Loan',
|
||||
]);
|
||||
|
||||
/** @var Collection<int, StageDefinition> $stageDefinitions */
|
||||
$stageDefinitions = collect(LoanStage::cases())
|
||||
->mapWithKeys(fn (LoanStage $stage): array => [
|
||||
$stage->value => StageDefinition::factory()->forStage($stage)->create(),
|
||||
]);
|
||||
|
||||
$personalWorkflow = WorkflowConfiguration::factory()
|
||||
->for($personalLoanType, 'loanType')
|
||||
->for($admin, 'creator')
|
||||
->published()
|
||||
->withStep($stageDefinitions[LoanStage::Validation->value], 1)
|
||||
->withStep($stageDefinitions[LoanStage::FraudCheck->value], 2, [
|
||||
'fraudPrefix' => 'FRAUD',
|
||||
'manualReviewPrefix' => 'REVIEW',
|
||||
])
|
||||
->withStep($stageDefinitions[LoanStage::CreditCheck->value], 3, [
|
||||
'rejectBelow' => 500,
|
||||
'manualReviewMin' => 500,
|
||||
'manualReviewMax' => 649,
|
||||
'approveFrom' => 650,
|
||||
])
|
||||
->withStep($stageDefinitions[LoanStage::ManagerApproval->value], 4, [
|
||||
'activationThreshold' => 500_000_000,
|
||||
'incomeMultiplier' => 20,
|
||||
])
|
||||
->create([
|
||||
'name' => 'Personal Loan Workflow',
|
||||
'version' => 1,
|
||||
]);
|
||||
|
||||
$businessWorkflow = WorkflowConfiguration::factory()
|
||||
->for($businessLoanType, 'loanType')
|
||||
->for($admin, 'creator')
|
||||
->published()
|
||||
->withStep($stageDefinitions[LoanStage::Validation->value], 1)
|
||||
->withStep($stageDefinitions[LoanStage::FraudCheck->value], 2, [
|
||||
'fraudPrefix' => 'FRAUD',
|
||||
'manualReviewPrefix' => 'REVIEW',
|
||||
])
|
||||
->withStep($stageDefinitions[LoanStage::GuarantorCheck->value], 3, [
|
||||
'guarantorRequired' => true,
|
||||
])
|
||||
->withStep($stageDefinitions[LoanStage::CreditCheck->value], 4, [
|
||||
'rejectBelow' => 500,
|
||||
'manualReviewMin' => 500,
|
||||
'manualReviewMax' => 649,
|
||||
'approveFrom' => 650,
|
||||
])
|
||||
->withStep($stageDefinitions[LoanStage::ManagerApproval->value], 5, [
|
||||
'activationThreshold' => 500_000_000,
|
||||
'incomeMultiplier' => 20,
|
||||
])
|
||||
->create([
|
||||
'name' => 'Business Loan Workflow',
|
||||
'version' => 1,
|
||||
]);
|
||||
|
||||
$personalSteps = $this->stepsByCode($personalWorkflow);
|
||||
$businessSteps = $this->stepsByCode($businessWorkflow);
|
||||
|
||||
Loan::factory()
|
||||
->count(8)
|
||||
->atStep($personalSteps[LoanStage::Validation->value])
|
||||
->create();
|
||||
|
||||
Loan::factory()
|
||||
->count(8)
|
||||
->atStep($businessSteps[LoanStage::Validation->value])
|
||||
->create();
|
||||
|
||||
Loan::factory()
|
||||
->forWorkflow($personalWorkflow)
|
||||
->withHistory($personalSteps[LoanStage::Validation->value])
|
||||
->withHistory($personalSteps[LoanStage::FraudCheck->value])
|
||||
->withHistory($personalSteps[LoanStage::CreditCheck->value])
|
||||
->create([
|
||||
'status' => LoanStatus::Approved,
|
||||
'current_workflow_configuration_step_id' => null,
|
||||
'amount' => 400_000_000,
|
||||
'credit_score' => 720,
|
||||
]);
|
||||
|
||||
Loan::factory()
|
||||
->forWorkflow($personalWorkflow)
|
||||
->withHistory($personalSteps[LoanStage::Validation->value])
|
||||
->withHistory(
|
||||
$personalSteps[LoanStage::FraudCheck->value],
|
||||
StageResultType::ManualReview,
|
||||
'CUSTOMER_REQUIRES_REVIEW',
|
||||
)
|
||||
->create([
|
||||
'customer_id' => 'REVIEW-CUSTOMER',
|
||||
'status' => LoanStatus::ManualReview,
|
||||
'current_workflow_configuration_step_id' => null,
|
||||
]);
|
||||
|
||||
Loan::factory()
|
||||
->forWorkflow($businessWorkflow)
|
||||
->withHistory($businessSteps[LoanStage::Validation->value])
|
||||
->withHistory($businessSteps[LoanStage::FraudCheck->value])
|
||||
->withHistory(
|
||||
$businessSteps[LoanStage::GuarantorCheck->value],
|
||||
StageResultType::Fail,
|
||||
'GUARANTOR_REQUIRED',
|
||||
)
|
||||
->create([
|
||||
'has_guarantor' => false,
|
||||
'status' => LoanStatus::Rejected,
|
||||
'current_workflow_configuration_step_id' => null,
|
||||
]);
|
||||
|
||||
Loan::factory()
|
||||
->forWorkflow($personalWorkflow)
|
||||
->withHistory($personalSteps[LoanStage::Validation->value])
|
||||
->withHistory($personalSteps[LoanStage::FraudCheck->value])
|
||||
->withHistory($personalSteps[LoanStage::CreditCheck->value])
|
||||
->withHistory($personalSteps[LoanStage::ManagerApproval->value])
|
||||
->create([
|
||||
'amount' => 600_000_000,
|
||||
'monthly_income' => 50_000_000,
|
||||
'credit_score' => 750,
|
||||
'status' => LoanStatus::Approved,
|
||||
'current_workflow_configuration_step_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<string, WorkflowConfigurationStep>
|
||||
*/
|
||||
private function stepsByCode(WorkflowConfiguration $workflowConfiguration): Collection
|
||||
{
|
||||
return $workflowConfiguration
|
||||
->steps()
|
||||
->with('stageDefinition')
|
||||
->get()
|
||||
->keyBy(fn (WorkflowConfigurationStep $step): string => $step->stageDefinition->code);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
@@ -15,11 +14,8 @@ class DatabaseSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
if (app()->environment('local')) {
|
||||
$this->call(BankFlowDevelopmentSeeder::class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user