Case Study 1: Building Acme's Credit Limit Approval System

Characters: Priya Okonkwo (Business Analyst), Sandra Chen (VP Sales), Marcus Webb (IT Director) Setting: Acme Corp, Q2 planning cycle


The Business Problem

Every quarter, the sales team at Acme Corp processes dozens of requests from customers asking to increase their credit limits. A higher credit limit means a customer can place larger orders without paying upfront — which is great for sales volume, but creates real financial risk if the customer later struggles to pay.

Until this quarter, the process worked like this:

  1. A sales rep submitted a paper form with the customer's request
  2. The form sat in Sandra Chen's inbox for three to ten days
  3. Sandra approved or rejected it based on her knowledge of the account
  4. The decision was emailed back to the sales rep

It worked well enough when Acme had fifty active accounts. Now they have over three hundred, and Sandra is spending four hours every Friday just working through the credit queue. Worse, her decisions are inconsistent — she applies stricter scrutiny during months when the company is tight on cash, and looser scrutiny during good months. There is no audit trail of why any particular decision was made.

Sandra calls Priya into her office. "I need you to document the actual rules I use when I evaluate these requests, and then I want you to figure out if we can automate the easy ones."


Step 1: Extracting the Business Rules

Priya spends two hours with Sandra in a whiteboard session. By the end, they have translated Sandra's instincts into a written policy with clear, objective thresholds.

Acme Corp Credit Limit Policy v1.0

Automatic Decline (any of the following): - Account is less than 6 months old - More than 3 late payments in the past 12 months - Requested limit exceeds 40% of the customer's stated annual revenue - Requested limit is more than 3× the customer's current limit (regardless of revenue)

Manual Review (escalate to Sandra, target 48-hour turnaround): - Requested limit increase is between 25% and 100% of current limit - Customer has 2–3 late payments in the past 12 months - Customer is in a flagged industry (construction, hospitality, retail)

Automatic Approval: - Requested increase is 25% or less of current limit - No more than 1 late payment in the past 12 months - Requested limit is within revenue guidelines

Priya looks at this list and immediately recognizes something: she has written a series of if/elif/else conditions. Each rule maps to a condition, each outcome maps to a branch.


Step 2: Translating Policy to Code

Priya starts with a script that reads credit limit requests and produces one of three outcomes: APPROVE, REVIEW, or DECLINE. She adds a reason string to every outcome so there is a clear audit trail.

"""
credit_limit_approver.py
Acme Corp Credit Limit Approval System

Encodes Sandra Chen's credit limit policy (Policy v1.0, approved Q2).
Outputs one of three decisions for each request:
    - APPROVE:  Auto-approved, within policy limits
    - REVIEW:   Escalated to Sandra for manual review
    - DECLINE:  Automatically declined, policy violation

Author: Priya Okonkwo, Business Analyst
"""

from dataclasses import dataclass


# ---------------------------------------------------------------------------
# Data Structures
# ---------------------------------------------------------------------------

@dataclass
class CreditRequest:
    """Represents a single credit limit change request from a customer."""
    customer_name: str
    account_age_months: int        # How long they have been a customer
    current_limit: float           # Their existing credit limit in USD
    requested_limit: float         # The limit they are asking for
    annual_revenue: float          # Customer's stated annual revenue
    late_payments_last_12mo: int   # Count of late payments in the past year
    industry: str                  # Customer's business sector


@dataclass
class CreditDecision:
    """The outcome of evaluating a credit limit request."""
    customer_name: str
    outcome: str        # "APPROVE", "REVIEW", or "DECLINE"
    reason: str         # Human-readable explanation for the audit log
    policy_rule: str    # Which specific policy rule triggered this decision


# ---------------------------------------------------------------------------
# Policy Constants
# ---------------------------------------------------------------------------

# Sourced from Policy v1.0 — change here if policy changes
MINIMUM_ACCOUNT_AGE_MONTHS = 6
MAX_LATE_PAYMENTS_FOR_ANY_APPROVAL = 3   # More than this = automatic decline
MAX_LATE_PAYMENTS_FOR_AUTO_APPROVE = 1   # More than this = escalate to review
MAX_LIMIT_AS_PCT_OF_REVENUE = 0.40       # 40% of annual revenue maximum
MAX_LIMIT_MULTIPLE_OF_CURRENT = 3.0      # Cannot triple current limit in one step
AUTO_APPROVE_MAX_INCREASE_PCT = 0.25     # Increases <= 25% auto-approved
REVIEW_MAX_INCREASE_PCT = 1.00           # Increases between 25-100% go to review

# Industries flagged for extra scrutiny (per Finance team guidance)
FLAGGED_INDUSTRIES = {"construction", "hospitality", "retail"}


# ---------------------------------------------------------------------------
# Core Evaluation Function
# ---------------------------------------------------------------------------

def evaluate_credit_request(request: CreditRequest) -> CreditDecision:
    """
    Evaluate a credit limit change request against Acme's policy.

    This function applies rules in a specific order: declines first (most
    severe), then review triggers, then approval. This matches the structure
    of the written policy and ensures the most protective rules are checked
    before the approving ones.

    Args:
        request: A CreditRequest dataclass with all required fields.

    Returns:
        A CreditDecision with outcome, reason, and the specific policy rule.
    """

    # -----------------------------------------------------------------------
    # SECTION 1: AUTOMATIC DECLINES
    # Check for hard violations that result in immediate rejection.
    # Order matters: the most clear-cut violations come first.
    # -----------------------------------------------------------------------

    # Rule D1: Account too new
    if request.account_age_months < MINIMUM_ACCOUNT_AGE_MONTHS:
        return CreditDecision(
            customer_name=request.customer_name,
            outcome="DECLINE",
            reason=(
                f"Account is {request.account_age_months} month(s) old. "
                f"Minimum required: {MINIMUM_ACCOUNT_AGE_MONTHS} months."
            ),
            policy_rule="D1: Minimum account age not met",
        )

    # Rule D2: Excessive late payments
    if request.late_payments_last_12mo > MAX_LATE_PAYMENTS_FOR_ANY_APPROVAL:
        return CreditDecision(
            customer_name=request.customer_name,
            outcome="DECLINE",
            reason=(
                f"Customer has {request.late_payments_last_12mo} late payments "
                f"in the past 12 months. Maximum for any approval: "
                f"{MAX_LATE_PAYMENTS_FOR_ANY_APPROVAL}."
            ),
            policy_rule="D2: Excessive late payment history",
        )

    # Rule D3: Requested limit exceeds revenue-based ceiling
    revenue_ceiling = request.annual_revenue * MAX_LIMIT_AS_PCT_OF_REVENUE
    if request.requested_limit > revenue_ceiling:
        return CreditDecision(
            customer_name=request.customer_name,
            outcome="DECLINE",
            reason=(
                f"Requested limit (${request.requested_limit:,.0f}) exceeds "
                f"{MAX_LIMIT_AS_PCT_OF_REVENUE * 100:.0f}% of annual revenue "
                f"(${revenue_ceiling:,.0f} ceiling on ${request.annual_revenue:,.0f} revenue)."
            ),
            policy_rule="D3: Requested limit exceeds revenue-based ceiling",
        )

    # Rule D4: Requested limit more than 3x current limit
    limit_multiple = request.requested_limit / request.current_limit
    if limit_multiple > MAX_LIMIT_MULTIPLE_OF_CURRENT:
        return CreditDecision(
            customer_name=request.customer_name,
            outcome="DECLINE",
            reason=(
                f"Requested limit (${request.requested_limit:,.0f}) is "
                f"{limit_multiple:.1f}x the current limit (${request.current_limit:,.0f}). "
                f"Maximum single-step multiple: {MAX_LIMIT_MULTIPLE_OF_CURRENT:.0f}x."
            ),
            policy_rule="D4: Requested limit exceeds 3x current limit",
        )

    # -----------------------------------------------------------------------
    # SECTION 2: ESCALATE TO REVIEW
    # If we reach here, no hard violations — check for conditions that need
    # human judgment.
    # -----------------------------------------------------------------------

    increase_pct = (request.requested_limit - request.current_limit) / request.current_limit
    review_reasons = []

    # Rule R1: Large increase
    if increase_pct > REVIEW_MAX_INCREASE_PCT:
        review_reasons.append(
            f"Increase of {increase_pct * 100:.1f}% exceeds 100% auto-review cap."
        )
    elif increase_pct > AUTO_APPROVE_MAX_INCREASE_PCT:
        review_reasons.append(
            f"Increase of {increase_pct * 100:.1f}% is in the manual review band (25–100%)."
        )

    # Rule R2: Elevated late payments (not enough to decline, but warrants a look)
    if request.late_payments_last_12mo > MAX_LATE_PAYMENTS_FOR_AUTO_APPROVE:
        review_reasons.append(
            f"{request.late_payments_last_12mo} late payments in 12 months "
            f"(threshold for auto-approve: {MAX_LATE_PAYMENTS_FOR_AUTO_APPROVE})."
        )

    # Rule R3: Flagged industry
    if request.industry.lower() in FLAGGED_INDUSTRIES:
        review_reasons.append(
            f"Industry '{request.industry}' is in the Finance watchlist."
        )

    # If any review triggers fired, escalate
    if review_reasons:
        reason_text = " | ".join(review_reasons)
        return CreditDecision(
            customer_name=request.customer_name,
            outcome="REVIEW",
            reason=f"Escalated for manual review. Triggers: {reason_text}",
            policy_rule="R1-R3: Manual review criteria met",
        )

    # -----------------------------------------------------------------------
    # SECTION 3: AUTOMATIC APPROVAL
    # If we reach here, all declines checked, no review triggers — approve.
    # -----------------------------------------------------------------------

    return CreditDecision(
        customer_name=request.customer_name,
        outcome="APPROVE",
        reason=(
            f"All policy criteria met. Increase of {increase_pct * 100:.1f}% "
            f"(${request.current_limit:,.0f} → ${request.requested_limit:,.0f}). "
            f"Late payments: {request.late_payments_last_12mo}."
        ),
        policy_rule="Auto-approve: all thresholds within policy",
    )


# ---------------------------------------------------------------------------
# Output / Display
# ---------------------------------------------------------------------------

def print_decision(decision: CreditDecision) -> None:
    """Print a formatted credit decision to the console."""
    outcome_labels = {
        "APPROVE": "AUTO-APPROVED",
        "REVIEW":  "ESCALATED FOR REVIEW",
        "DECLINE": "DECLINED",
    }
    label = outcome_labels.get(decision.outcome, decision.outcome)

    print(f"  Customer : {decision.customer_name}")
    print(f"  Result   : {label}")
    print(f"  Reason   : {decision.reason}")
    print(f"  Rule     : {decision.policy_rule}")
    print()


def run_batch(requests: list) -> None:
    """Process a list of credit requests and display a summary."""
    approved = declined = reviewed = 0

    print("=" * 65)
    print("  ACME CORP — CREDIT LIMIT APPROVAL SYSTEM")
    print("  Policy: v1.0 | Analyst: Priya Okonkwo")
    print("=" * 65)
    print()

    for request in requests:
        decision = evaluate_credit_request(request)
        print("-" * 65)
        print_decision(decision)

        if decision.outcome == "APPROVE":
            approved += 1
        elif decision.outcome == "DECLINE":
            declined += 1
        else:
            reviewed += 1

    print("=" * 65)
    print(f"  BATCH SUMMARY: {len(requests)} requests processed")
    print(f"    Auto-approved : {approved}")
    print(f"    Declined      : {declined}")
    print(f"    For review    : {reviewed}")
    print("=" * 65)


# ---------------------------------------------------------------------------
# Test Data — realistic variety of requests
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    sample_requests = [
        # Straightforward auto-approve: long-standing, clean history, modest ask
        CreditRequest(
            customer_name="Riverside Medical Group",
            account_age_months=36,
            current_limit=50_000,
            requested_limit=60_000,
            annual_revenue=800_000,
            late_payments_last_12mo=0,
            industry="healthcare",
        ),

        # Decline: account too new
        CreditRequest(
            customer_name="Bright Start Consulting",
            account_age_months=3,
            current_limit=10_000,
            requested_limit=15_000,
            annual_revenue=200_000,
            late_payments_last_12mo=0,
            industry="consulting",
        ),

        # Decline: exceeds revenue ceiling
        CreditRequest(
            customer_name="Small But Ambitious LLC",
            account_age_months=24,
            current_limit=20_000,
            requested_limit=50_000,
            annual_revenue=80_000,     # 40% ceiling is $32k — request exceeds it
            late_payments_last_12mo=1,
            industry="retail",
        ),

        # Review: large increase percentage
        CreditRequest(
            customer_name="Momentum Freight Co.",
            account_age_months=18,
            current_limit=25_000,
            requested_limit=55_000,    # 120% increase — above the review threshold
            annual_revenue=500_000,
            late_payments_last_12mo=0,
            industry="logistics",
        ),

        # Review: flagged industry + borderline late payments
        CreditRequest(
            customer_name="Grand Vista Hotels",
            account_age_months=30,
            current_limit=40_000,
            requested_limit=48_000,
            annual_revenue=600_000,
            late_payments_last_12mo=2,
            industry="hospitality",
        ),

        # Decline: too many late payments
        CreditRequest(
            customer_name="Troubled Times Retail",
            account_age_months=48,
            current_limit=30_000,
            requested_limit=35_000,
            annual_revenue=400_000,
            late_payments_last_12mo=5,
            industry="retail",
        ),

        # Auto-approve: Silver tier, clean, reasonable ask
        CreditRequest(
            customer_name="Summit Tech Partners",
            account_age_months=15,
            current_limit=15_000,
            requested_limit=18_000,   # 20% increase
            annual_revenue=300_000,
            late_payments_last_12mo=1,
            industry="technology",
        ),
    ]

    run_batch(sample_requests)

Step 3: Reviewing the Output

Priya runs the script and shares the output with Sandra:

=================================================================
  ACME CORP — CREDIT LIMIT APPROVAL SYSTEM
  Policy: v1.0 | Analyst: Priya Okonkwo
=================================================================

-----------------------------------------------------------------
  Customer : Riverside Medical Group
  Result   : AUTO-APPROVED
  Reason   : All policy criteria met. Increase of 20.0%
             ($50,000 → $60,000). Late payments: 0.
  Rule     : Auto-approve: all thresholds within policy

-----------------------------------------------------------------
  Customer : Bright Start Consulting
  Result   : DECLINED
  Reason   : Account is 3 month(s) old. Minimum required: 6 months.
  Rule     : D1: Minimum account age not met

...

Sandra reviews the output against her own judgments from the past six months. The results match in 94% of cases. The six exceptions are all in the "review" category — cases where Sandra would have applied additional context about the customer relationship that the data does not capture.

"This is exactly what I wanted," Sandra says. "The clear approvals and the clear declines are handled automatically. I only touch the edge cases."


Step 4: What Priya Learned

After building the system, Priya reflects on three lessons that will inform every decision-logic script she writes going forward:

Lesson 1: Write the policy before writing the code. The two hours with Sandra on the whiteboard were more valuable than any hour spent coding. When the business logic is unclear, the code will be unclear too. Start with a written policy that humans agree on, then translate it.

Lesson 2: Order your conditions from most protective to least protective. Declines come before escalations, which come before approvals. This means the harshest consequences are checked first, and the program cannot accidentally approve something that should have been declined by an earlier rule.

Lesson 3: Name everything for the audit trail. Every decision returns a policy_rule string identifying which rule triggered it. When someone asks "why was this request declined?", the answer is immediately available in the log. This is not just good programming practice — in financial decisions, it is a legal and regulatory requirement.


Exercises Based on This Case Study

  1. Sandra wants to add a new rule: VIP customers (those with more than 5 years as a customer) should have their review threshold raised from 25% to 40% increase. Where in the code would you add this rule, and why?

  2. The Finance team asks for a fourth outcome: "CONDITIONAL APPROVAL" — where the increase is approved, but only if the customer provides an updated bank statement. Under what conditions would this outcome apply?

  3. Currently the flagged_industries set is hardcoded. Rewrite the code so the flagged industries are loaded from a variable that could easily be updated without changing the core logic.

  4. Trace through Grand Vista Hotels manually, step by step. Which if condition first evaluates to True? What does this tell you about the order of conditions in the function?