diff --git a/apps/applications/migrations/0001_initial.py b/apps/applications/migrations/0001_initial.py new file mode 100644 index 0000000..c9609ff --- /dev/null +++ b/apps/applications/migrations/0001_initial.py @@ -0,0 +1,60 @@ +# Generated by Django 5.0.7 on 2024-09-12 07:27 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('clients', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Application', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('loan_type', models.CharField(choices=[('PERSONAL_LOAN', 'Personal Loan'), ('BUSINESS_LOAN', 'Business Loan'), ('MORTGAGE', 'Mortgage'), ('STUDENT_LOAN', 'Student Loan'), ('CAR_LOAN', 'Car Loan')], max_length=32)), + ('amount_requested', models.DecimalField(decimal_places=2, max_digits=12)), + ('currency', models.CharField(choices=[('USD', 'US Dollar'), ('UZS', 'Uzbekistani Som'), ('EUR', 'Euro')], default='UZS', max_length=3)), + ('application_date', models.DateField(auto_now_add=True)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected'), ('UNDER_REVIEW', 'Under Review'), ('DISBURSED', 'Disbursed')], default='PENDING', max_length=20)), + ('loan_purpose', models.TextField(blank=True, null=True)), + ('interest_rate', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)), + ('repayment_period_months', models.IntegerField(blank=True, null=True)), + ('reference_number', models.CharField(max_length=128, unique=True)), + ('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='approved_applications', to=settings.AUTH_USER_MODEL)), + ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='clients.client')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_applications', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Loan Application', + 'verbose_name_plural': 'Loan Applications', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='ApplicationSchedule', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('due_date', models.DateField()), + ('installment_amount', models.DecimalField(decimal_places=2, max_digits=12)), + ('is_paid', models.BooleanField(default=False)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='applications.application')), + ], + options={ + 'verbose_name': 'Application Schedule', + 'verbose_name_plural': 'Application Schedules', + 'ordering': ['due_date'], + }, + ), + ] diff --git a/apps/applications/models.py b/apps/applications/models.py index 71a8362..6f841b4 100644 --- a/apps/applications/models.py +++ b/apps/applications/models.py @@ -1,3 +1,87 @@ from django.db import models +from apps.common.models import BaseModel +from apps.clients.models import Client +from apps.users.models import User -# Create your models here. + +class ApplicationStatusChoices(models.TextChoices): + PENDING = "PENDING", "Pending" + APPROVED = "APPROVED", "Approved" + REJECTED = "REJECTED", "Rejected" + UNDER_REVIEW = "UNDER_REVIEW", "Under Review" + DISBURSED = "DISBURSED", "Disbursed" + + +class LoanTypeChoices(models.TextChoices): + PERSONAL_LOAN = "PERSONAL_LOAN", "Personal Loan" + BUSINESS_LOAN = "BUSINESS_LOAN", "Business Loan" + MORTGAGE = "MORTGAGE", "Mortgage" + STUDENT_LOAN = "STUDENT_LOAN", "Student Loan" + CAR_LOAN = "CAR_LOAN", "Car Loan" + + +class CurrencyChoices(models.TextChoices): + USD = "USD", "US Dollar" + UZS = "UZS", "Uzbekistani Som" + EUR = "EUR", "Euro" + + +class Application(BaseModel): + client = models.ForeignKey( + Client, on_delete=models.CASCADE, related_name="applications" + ) + loan_type = models.CharField(max_length=32, choices=LoanTypeChoices.choices) + amount_requested = models.DecimalField(max_digits=12, decimal_places=2) + currency = models.CharField( + max_length=3, choices=CurrencyChoices.choices, default=CurrencyChoices.UZS + ) + + application_date = models.DateField(auto_now_add=True) + status = models.CharField( + max_length=20, + choices=ApplicationStatusChoices.choices, + default=ApplicationStatusChoices.PENDING, + ) + + created_by = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="created_applications" + ) + approved_by = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name="approved_applications", + blank=True, + null=True, + ) + + loan_purpose = models.TextField(blank=True, null=True) + interest_rate = models.DecimalField( + max_digits=5, decimal_places=2, blank=True, null=True + ) + repayment_period_months = models.IntegerField(blank=True, null=True) + reference_number = models.CharField(max_length=128, unique=True) + + def __str__(self): + return f"Application {self.reference_number} - {self.client} - {self.amount_requested} {self.currency}" + + class Meta: + verbose_name = "Loan Application" + verbose_name_plural = "Loan Applications" + ordering = ["-created_at"] + + +class ApplicationSchedule(BaseModel): + application = models.ForeignKey( + Application, on_delete=models.CASCADE, related_name="schedules" + ) + due_date = models.DateField() + installment_amount = models.DecimalField(max_digits=12, decimal_places=2) + is_paid = models.BooleanField(default=False) + + def __str__(self): + return f"Installment {self.id} for Application {self.application.reference_number} due on {self.due_date}" + + class Meta: + verbose_name = "Application Schedule" + verbose_name_plural = "Application Schedules" + ordering = ["due_date"] diff --git a/apps/borrowers/migrations/0001_initial.py b/apps/borrowers/migrations/0001_initial.py new file mode 100644 index 0000000..286b381 --- /dev/null +++ b/apps/borrowers/migrations/0001_initial.py @@ -0,0 +1,76 @@ +# Generated by Django 5.0.7 on 2024-09-12 07:27 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Borrower', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('borrower_type', models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('COMPANY', 'Company')], max_length=32)), + ('full_name', models.CharField(max_length=255)), + ('date_of_birth', models.DateField(blank=True, null=True)), + ('identification_number', models.CharField(max_length=64, unique=True)), + ('contact_email', models.EmailField(blank=True, max_length=254, null=True)), + ('contact_phone', models.CharField(blank=True, max_length=20, null=True)), + ('address', models.TextField(blank=True, null=True)), + ('status', models.CharField(choices=[('ACTIVE', 'Active'), ('INACTIVE', 'Inactive'), ('BLACKLISTED', 'Blacklisted')], default='ACTIVE', max_length=32)), + ('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='approved_borrowers', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_borrowers', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Borrower', + 'verbose_name_plural': 'Borrowers', + 'ordering': ['full_name'], + }, + ), + migrations.CreateModel( + name='BorrowerDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('document_type', models.CharField(max_length=64)), + ('document_number', models.CharField(max_length=128)), + ('document_file', models.FileField(upload_to='borrower_documents/')), + ('expiration_date', models.DateField(blank=True, null=True)), + ('issued_date', models.DateField(blank=True, null=True)), + ('borrower', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='borrowers.borrower')), + ], + options={ + 'verbose_name': 'Borrower Document', + 'verbose_name_plural': 'Borrower Documents', + 'ordering': ['document_type'], + }, + ), + migrations.CreateModel( + name='BorrowerFinancialInfo', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('annual_income', models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True)), + ('credit_score', models.IntegerField(blank=True, null=True)), + ('total_outstanding_debt', models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True)), + ('total_loans', models.IntegerField(default=0)), + ('borrower', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='financial_info', to='borrowers.borrower')), + ], + options={ + 'verbose_name': 'Borrower Financial Info', + 'verbose_name_plural': 'Borrower Financial Infos', + }, + ), + ] diff --git a/apps/borrowers/models.py b/apps/borrowers/models.py index 71a8362..e1fa623 100644 --- a/apps/borrowers/models.py +++ b/apps/borrowers/models.py @@ -1,3 +1,91 @@ from django.db import models +from apps.common.models import BaseModel +from apps.users.models import User -# Create your models here. + +class BorrowerTypeChoices(models.TextChoices): + INDIVIDUAL = "INDIVIDUAL", "Individual" + COMPANY = "COMPANY", "Company" + + +class BorrowerStatusChoices(models.TextChoices): + ACTIVE = "ACTIVE", "Active" + INACTIVE = "INACTIVE", "Inactive" + BLACKLISTED = "BLACKLISTED", "Blacklisted" + + +class Borrower(BaseModel): + borrower_type = models.CharField(max_length=32, choices=BorrowerTypeChoices.choices) + full_name = models.CharField(max_length=255) # For both individuals and companies + date_of_birth = models.DateField(blank=True, null=True) # Only for individuals + identification_number = models.CharField( + max_length=64, unique=True + ) # National ID, tax number, etc. + contact_email = models.EmailField(blank=True, null=True) + contact_phone = models.CharField(max_length=20, blank=True, null=True) + address = models.TextField(blank=True, null=True) + status = models.CharField( + max_length=32, + choices=BorrowerStatusChoices.choices, + default=BorrowerStatusChoices.ACTIVE, + ) + created_by = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="created_borrowers" + ) + approved_by = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name="approved_borrowers", + blank=True, + null=True, + ) + + def __str__(self): + return f"{self.full_name} ({self.borrower_type})" + + class Meta: + verbose_name = "Borrower" + verbose_name_plural = "Borrowers" + ordering = ["full_name"] + + +class BorrowerFinancialInfo(BaseModel): + borrower = models.OneToOneField( + Borrower, on_delete=models.CASCADE, related_name="financial_info" + ) + annual_income = models.DecimalField( + max_digits=15, decimal_places=2, blank=True, null=True + ) # Can apply for both individuals and companies + credit_score = models.IntegerField(blank=True, null=True) + total_outstanding_debt = models.DecimalField( + max_digits=15, decimal_places=2, blank=True, null=True + ) + total_loans = models.IntegerField(default=0) + + def __str__(self): + return f"Financial Info for {self.borrower.full_name}" + + class Meta: + verbose_name = "Borrower Financial Info" + verbose_name_plural = "Borrower Financial Infos" + + +class BorrowerDocument(BaseModel): + borrower = models.ForeignKey( + Borrower, on_delete=models.CASCADE, related_name="documents" + ) + document_type = models.CharField( + max_length=64 + ) # E.g., Passport, Tax Document, Utility Bill, etc. + document_number = models.CharField(max_length=128) + document_file = models.FileField(upload_to="borrower_documents/") + expiration_date = models.DateField(blank=True, null=True) + issued_date = models.DateField(blank=True, null=True) + + def __str__(self): + return f"{self.document_type} for {self.borrower.full_name}" + + class Meta: + verbose_name = "Borrower Document" + verbose_name_plural = "Borrower Documents" + ordering = ["document_type"] diff --git a/apps/loans/migrations/0001_initial.py b/apps/loans/migrations/0001_initial.py new file mode 100644 index 0000000..5d9297b --- /dev/null +++ b/apps/loans/migrations/0001_initial.py @@ -0,0 +1,60 @@ +# Generated by Django 5.0.7 on 2024-09-12 07:27 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('clients', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Loan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('loan_type', models.CharField(choices=[('PERSONAL_LOAN', 'Personal Loan'), ('BUSINESS_LOAN', 'Business Loan'), ('MORTGAGE', 'Mortgage'), ('STUDENT_LOAN', 'Student Loan'), ('CAR_LOAN', 'Car Loan')], max_length=32)), + ('amount_disbursed', models.DecimalField(decimal_places=2, max_digits=12)), + ('currency', models.CharField(choices=[('USD', 'US Dollar'), ('UZS', 'Uzbekistani Som'), ('EUR', 'Euro')], default='UZS', max_length=3)), + ('disbursement_date', models.DateField(auto_now_add=True)), + ('status', models.CharField(choices=[('ACTIVE', 'Active'), ('PAID_OFF', 'Paid Off'), ('DEFAULTED', 'Defaulted'), ('CLOSED', 'Closed'), ('UNDER_REVIEW', 'Under Review'), ('PENDING_DISBURSEMENT', 'Pending Disbursement')], default='PENDING_DISBURSEMENT', max_length=32)), + ('interest_rate', models.DecimalField(decimal_places=2, max_digits=5)), + ('repayment_period_months', models.IntegerField()), + ('total_amount_due', models.DecimalField(decimal_places=2, max_digits=12)), + ('reference_number', models.CharField(max_length=128, unique=True)), + ('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='approved_loans', to=settings.AUTH_USER_MODEL)), + ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='loans', to='clients.client')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_loans', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Loan', + 'verbose_name_plural': 'Loans', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='LoanRepaymentSchedule', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('due_date', models.DateField()), + ('installment_amount', models.DecimalField(decimal_places=2, max_digits=12)), + ('is_paid', models.BooleanField(default=False)), + ('loan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='repayment_schedules', to='loans.loan')), + ], + options={ + 'verbose_name': 'Loan Repayment Schedule', + 'verbose_name_plural': 'Loan Repayment Schedules', + 'ordering': ['due_date'], + }, + ), + ] diff --git a/apps/loans/models.py b/apps/loans/models.py index 71a8362..8c42d43 100644 --- a/apps/loans/models.py +++ b/apps/loans/models.py @@ -1,3 +1,83 @@ from django.db import models +from apps.common.models import BaseModel +from apps.clients.models import Client +from apps.users.models import User -# Create your models here. + +class LoanStatusChoices(models.TextChoices): + ACTIVE = "ACTIVE", "Active" + PAID_OFF = "PAID_OFF", "Paid Off" + DEFAULTED = "DEFAULTED", "Defaulted" + CLOSED = "CLOSED", "Closed" + UNDER_REVIEW = "UNDER_REVIEW", "Under Review" + PENDING_DISBURSEMENT = "PENDING_DISBURSEMENT", "Pending Disbursement" + + +class LoanTypeChoices(models.TextChoices): + PERSONAL_LOAN = "PERSONAL_LOAN", "Personal Loan" + BUSINESS_LOAN = "BUSINESS_LOAN", "Business Loan" + MORTGAGE = "MORTGAGE", "Mortgage" + STUDENT_LOAN = "STUDENT_LOAN", "Student Loan" + CAR_LOAN = "CAR_LOAN", "Car Loan" + + +class CurrencyChoices(models.TextChoices): + USD = "USD", "US Dollar" + UZS = "UZS", "Uzbekistani Som" + EUR = "EUR", "Euro" + + +class Loan(BaseModel): + client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name="loans") + loan_type = models.CharField(max_length=32, choices=LoanTypeChoices.choices) + amount_disbursed = models.DecimalField(max_digits=12, decimal_places=2) + currency = models.CharField( + max_length=3, choices=CurrencyChoices.choices, default=CurrencyChoices.UZS + ) + disbursement_date = models.DateField(auto_now_add=True) + + status = models.CharField( + max_length=32, + choices=LoanStatusChoices.choices, + default=LoanStatusChoices.PENDING_DISBURSEMENT, + ) + interest_rate = models.DecimalField(max_digits=5, decimal_places=2) + repayment_period_months = models.IntegerField() + total_amount_due = models.DecimalField(max_digits=12, decimal_places=2) + reference_number = models.CharField(max_length=128, unique=True) + + created_by = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="created_loans" + ) + approved_by = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name="approved_loans", + blank=True, + null=True, + ) + + def __str__(self): + return f"Loan {self.reference_number} - {self.client} - {self.amount_disbursed} {self.currency}" + + class Meta: + verbose_name = "Loan" + verbose_name_plural = "Loans" + ordering = ["-created_at"] + + +class LoanRepaymentSchedule(BaseModel): + loan = models.ForeignKey( + Loan, on_delete=models.CASCADE, related_name="repayment_schedules" + ) + due_date = models.DateField() + installment_amount = models.DecimalField(max_digits=12, decimal_places=2) + is_paid = models.BooleanField(default=False) + + def __str__(self): + return f"Installment {self.id} for Loan {self.loan.reference_number} due on {self.due_date}" + + class Meta: + verbose_name = "Loan Repayment Schedule" + verbose_name_plural = "Loan Repayment Schedules" + ordering = ["due_date"]