Sesi 4: Business Logic - BPRS Baktimakmur Indah

Sesi 4: Business Logic

Nasabah, Pembiayaan, & Upload File CSV/XLSX

1 Database Models (Customer & Loan)
ORM
/www/wwwroot/app-collection/ao/app/models/customer.py
from sqlalchemy import Column, BigInteger, String, Date, Text, Numeric from app.models.base import TimestampMixin, Base class Customer(Base, TimestampMixin): __tablename__ = "customers" id = Column(BigInteger, primary_key=True, index=True) national_id = Column(String(30), unique=True, nullable=True, index=True) name = Column(String(150), nullable=False) birth_date = Column(Date, nullable=True) phone = Column(String(30), nullable=True) email = Column(String(150), nullable=True) address = Column(Text, nullable=True) business_type = Column(String(100), nullable=True) monthly_income = Column(Numeric(18, 2), nullable=True) assets_value = Column(Numeric(18, 2), nullable=True)

/www/wwwroot/app-collection/ao/app/models/loan.py
from sqlalchemy import Column, BigInteger, Integer, String, Date, Numeric, Enum, ForeignKey from sqlalchemy.orm import relationship from app.models.base import TimestampMixin, Base import enum class LoanStatus(str, enum.Enum): active = "active" settled = "settled" defaulted = "defaulted" cancelled = "cancelled" class Loan(Base, TimestampMixin): __tablename__ = "loans" id = Column(BigInteger, primary_key=True, index=True) customer_id = Column(BigInteger, ForeignKey("customers.id"), nullable=False) branch_id = Column(Integer, ForeignKey("branches.id"), nullable=False) product_name = Column(String(100), nullable=True) principal_amount = Column(Numeric(18, 2), nullable=False) profit_rate = Column(Numeric(5, 2), nullable=False) # Contoh: 12.5 untuk 12.5% tenor_months = Column(Integer, nullable=False) start_date = Column(Date, nullable=False) end_date = Column(Date, nullable=True) collateral_desc = Column(Text, nullable=True) status = Column(Enum(LoanStatus), default=LoanStatus.active) # ML Fields (diisi nanti oleh model prediksi) credit_score = Column(Numeric(6, 2), nullable=True) default_probability = Column(Numeric(5, 4), nullable=True) segment = Column(String(10), nullable=True) # 'low', 'medium', 'high' # Relationships customer = relationship("Customer", backref="loans") payments = relationship("Payment", back_populates="loan") class Payment(Base, TimestampMixin): __tablename__ = "payments" id = Column(BigInteger, primary_key=True, index=True) loan_id = Column(BigInteger, ForeignKey("loans.id"), nullable=False) schedule_date = Column(Date, nullable=False) paid_date = Column(Date, nullable=True) amount_due = Column(Numeric(18, 2), nullable=False) amount_paid = Column(Numeric(18, 2), default=0) status = Column(String(20), default="on_time") # on_time, late, default loan = relationship("Loan", back_populates="payments")
2 Pydantic Schemas (Validation)
Validation
/www/wwwroot/app-collection/ao/app/schemas/loan.py
from pydantic import BaseModel, Field from datetime import date, datetime from decimal import Decimal from typing import Optional class CustomerBase(BaseModel): name: str = Field(..., min_length=3) phone: Optional[str] = None email: Optional[str] = None business_type: Optional[str] = None monthly_income: Optional[Decimal] = None assets_value: Optional[Decimal] = None class CustomerCreate(CustomerBase): national_id: Optional[str] = None class CustomerResponse(CustomerBase): id: int created_at: datetime class Config: from_attributes = True class LoanCreate(BaseModel): customer_id: int branch_id: int product_name: Optional[str] = None principal_amount: Decimal = Field(..., gt=0) profit_rate: Decimal = Field(..., ge=0) # Rate dalam persen (misal 15.5) tenor_months: int = Field(..., gt=0) start_date: date collateral_desc: Optional[str] = None class LoanResponse(BaseModel): id: int customer_id: int principal_amount: Decimal profit_rate: Decimal tenor_months: int monthly_installment: Optional[Decimal] = None status: str class Config: from_attributes = True
3 Services Layer (Core Logic)
Logic
Catatan Rumus Pembiayaan (Flat Rate):
Total Margin = Principal * (Rate% / 100)
Total Bayar = Principal + Total Margin
Angsuran Per Bulan = Total Bayar / Tenor
/www/wwwroot/app-collection/ao/app/services/loan_service.py
from sqlalchemy.orm import Session from datetime import date, timedelta from decimal import Decimal from app.models.loan import Loan, LoanStatus, Payment from app.models.customer import Customer from app.schemas.loan import LoanCreate def calculate_flat_rate(principal: Decimal, rate_percent: Decimal, tenor: int) -> Decimal: """ Menghitung angsuran bulanan dengan metode Flat Rate. Rumus: (Principal * (1 + (Rate/100))) / Tenor """ # Hitung total margin/profit total_profit = principal * (rate_percent / Decimal(100)) # Total utang + margin total_debt = principal + total_profit # Angsuran per bulan monthly = total_debt / Decimal(tenor) return monthly def create_loan_service(db: Session, loan_data: LoanCreate, current_user_id: int): # 1. Cek nasabah ada customer = db.query(Customer).filter(Customer.id == loan_data.customer_id).first() if not customer: raise ValueError("Customer not found") # 2. Hitung angsuran monthly_installment = calculate_flat_rate( loan_data.principal_amount, loan_data.profit_rate, loan_data.tenor_months ) # 3. Tentukan End Date (Start Date + Tenor Months) # Simple logic: Tambahkan bulan end_date = loan_data.start_date # Logic sederhana menambah bulan (bisa gunakan library dateutil relatifelta untuk lebih akurat) # Di sini kita pakai loop sederhana untuk clarity from dateutil.relativedelta import relativedelta end_date = loan_data.start_date + relativedelta(months=+loan_data.tenor_months) # 4. Buat Object Loan new_loan = Loan( **loan_data.dict(), end_date=end_date, status=LoanStatus.active ) db.add(new_loan) db.flush() # Flush agar dapat ID loan untuk membuat payments # 5. Buat Jadwal Pembayaran (Payments) # Generate schedule untuk setiap bulan current_sched_date = loan_data.start_date + relativedelta(months=+1) # Angsuran pertama bulan depan for i in range(loan_data.tenor_months): payment = Payment( loan_id=new_loan.id, schedule_date=current_sched_date, amount_due=monthly_installment, amount_paid=0, status="on_time" ) db.add(payment) current_sched_date = current_sched_date + relativedelta(months=+1) db.commit() db.refresh(new_loan) return new_loan

/www/wwwroot/app-collection/ao/app/services/file_service.py
import os import pandas as pd from datetime import datetime from fastapi import UploadFile, HTTPException # Konfigurasi direktori upload (Pastikan folder ini ada di server) UPLOAD_DIR = "/www/wwwroot/app-collection/ao/uploads" os.makedirs(UPLOAD_DIR, exist_ok=True) async def save_upload_file(file: UploadFile): # Validasi tipe file if not (file.filename.endswith('.csv') or file.filename.endswith('.xlsx') or file.filename.endswith('.xls')): raise HTTPException(status_code=400, detail="Only CSV or Excel files are allowed") # Buat nama file unik: timestamp_filename.ext file_location = f"{UPLOAD_DIR}/{int(datetime.now().timestamp())}_{file.filename}" # Simpan file ke disk try: with open(file_location, "wb+") as file_object: file_object.write(file.file.read()) except Exception as e: raise HTTPException(status_code=500, detail=f"Could not save file: {str(e)}") # Baca file untuk preview summary try: if file.filename.endswith('.csv'): df = pd.read_csv(file_location) else: df = pd.read_excel(file_location) return { "filename": file.filename, "stored_path": file_location, "rows_count": len(df), "columns": list(df.columns), "preview": df.head(5).to_dict(orient="records") } except Exception as e: # Jika file terbaca error tapi file tersimpan return { "filename": file.filename, "stored_path": file_location, "error": "File saved but could not parse content for preview", "details": str(e) }
4 API Routes
API
/www/wwwroot/app-collection/ao/app/api/customers.py
from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List from db import get_db from app.models.customer import Customer from app.schemas.loan import CustomerCreate, CustomerResponse from app.api.deps import get_current_active_user router = APIRouter() @router.post("/", response_model=CustomerResponse) def create_customer( customer: CustomerCreate, db: Session = Depends(get_db), current_user = Depends(get_current_active_user) # Harus login ): # Cek NIK duplikat jika diisi if customer.national_id: existing = db.query(Customer).filter(Customer.national_id == customer.national_id).first() if existing: raise HTTPException(status_code=400, detail="National ID already registered") db_customer = Customer(**customer.dict()) db.add(db_customer) db.commit() db.refresh(db_customer) return db_customer @router.get("/", response_model=List[CustomerResponse]) def list_customers(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): customers = db.query(Customer).offset(skip).limit(limit).all() return customers

/www/wwwroot/app-collection/ao/app/api/loans.py
from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from db import get_db from app.schemas.loan import LoanCreate, LoanResponse from app.services.loan_service import create_loan_service from app.api.deps import get_current_active_user router = APIRouter() @router.post("/submit", response_model=LoanResponse) def submit_loan( loan_data: LoanCreate, db: Session = Depends(get_db), current_user = Depends(get_current_active_user) ): try: new_loan = create_loan_service(db, loan_data, current_user.id) return new_loan except ValueError as e: from fastapi import HTTPException raise HTTPException(status_code=404, detail=str(e)) except Exception as e: from fastapi import HTTPException raise HTTPException(status_code=500, detail=f"Internal Server Error: {str(e)}")

/www/wwwroot/app-collection/ao/app/api/files.py
from fastapi import APIRouter, Depends, UploadFile, File from app.services.file_service import save_upload_file from app.api.deps import get_current_active_user router = APIRouter() @router.post("/upload") async def upload_file( file: UploadFile = File(...), current_user = Depends(get_current_active_user) ): """ Upload CSV atau XLSX untuk prediksi massal. File akan disimpan di server dan dikembalikan ringkasan isinya. """ result = await save_upload_file(file) return result
5 Integrasi Main.py
Finalize

Update main.py untuk memasukkan router baru.

/www/wwwroot/app-collection/ao/main.py
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from db import engine import config # Import Routers from app.api.auth import router as auth_router from app.api.customers import router as cust_router from app.api.loans import router as loan_router from app.api.files import router as file_router app = FastAPI(title=config.get_settings().APP_NAME, version="1.0.0") # CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Include Routes app.include_router(auth_router, prefix="/api/v1/auth", tags=["Authentication"]) app.include_router(cust_router, prefix="/api/v1/customers", tags=["Customers"]) app.include_router(loan_router, prefix="/api/v1/loans", tags=["Loans"]) app.include_router(file_router, prefix="/api/v1/files", tags=["Files & Uploads"]) @app.on_event("startup") def on_startup(): try: with engine.connect() as conn: print("✅ DB Connected!") # Buat folder uploads jika belum ada import os os.makedirs("/www/wwwroot/app-collection/ao/uploads", exist_ok=True) print("✅ Uploads directory checked/created.") except Exception as e: print(f"❌ Error: {e}") @app.get("/") def root(): return {"status": "BPRS API Running", "session": "4 - Business Logic Active"}
6 Testing Flow (Scenario)
Test

Restart server (Ctrl+C lalu python main.py) dan buka Swagger UI.

1. Create Customer

  • POST /api/v1/customers/
  • Gunakan token login dari sesi sebelumnya (Authorize).
  • Body JSON:
    {
      "national_id": "3501010101010001",
      "name": "Budi Santoso",
      "phone": "081234567890",
      "business_type": "Retail",
      "monthly_income": 5000000,
      "assets_value": 10000000
    }
  • Simpan id nasabah yang baru dibuat (misal: 1).

2. Submit Loan

  • POST /api/v1/loans/submit
  • Body JSON:
    {
      "customer_id": 1,
      "branch_id": 1,
      "product_name": "Mudharabah Toko",
      "principal_amount": 10000000,
      "profit_rate": 15,
      "tenor_months": 12,
      "start_date": "2023-11-01"
    }
  • Cek response: Anda akan melihat monthly_installment dihitung otomatis.

3. Upload File

  • Buat file Excel sederhana di laptop Anda (atau CSV).
  • POST /api/v1/files/upload.
  • Pilih file Anda.
  • Lihat responsenya: sistem akan memberi tahu jumlah baris dan kolom yang terbaca.
Code copied to clipboard!