# Cardisk — USSD Framework for Python

A modern, intuitive Python framework for building USSD applications with FastAPI. Cardisk provides a clean, decorator-based API for creating interactive USSD menus and flows.

## Features

-  **Simple API** - Decorator-based routing similar to Flask/FastAPI
-  **Menu System** - Built-in support for interactive menus with automatic navigation
-  **Session Management** - Automatic session handling with in-memory or Redis storage
-  **Monitoring** - Built-in Prometheus metrics endpoint
-  **Error Handling** - Robust error handling middleware
-  **Type Safe** - Full type hints support
-  **Fast** - Built on FastAPI and Uvicorn

## Installation

### Development Install
```bash
pip install -e .
```

### Production Install
```bash
pip install cardisk
```

## Quick Start

Create a file `app.py`:

```python
from cardisk import Cardisk, ussd

app = Cardisk()

@app.route("/")
def main_menu():
    ussd.menu("Welcome to Wolke Services", [
        ("Loans", "/loans"),
        ("Savings", "/savings"),
        ("Insurance", "/insurance")
    ])

@app.route("/loans")
def loans():
    ussd.menu("Loans Menu", [
        ("Apply for loan", "/loan/apply"),
        ("Check loan status", "/loan/status"),
        ("Repay loan", "/loan/repay")
    ])

@app.route("/loan/apply")
def loan_apply():
    ussd.screen(f"Enter loan amount for {ussd.msisdn}")
    ussd.next("/loan/confirm")

@app.route("/loan/confirm")
def loan_confirm():
    amount = ussd.input
    ussd.screen(f"Loan of ${amount} submitted successfully for {ussd.msisdn}")
    ussd.end()
```

Run the application:

```bash
cardisk runserver app.py
```

Or with uvicorn directly:

```bash
uvicorn app:app.app --reload --port 8000
```

## API Reference

### Application Setup

```python
from cardisk import Cardisk, ussd

app = Cardisk()
```

### Route Decorator

Define routes using the `@app.route()` decorator:

```python
@app.route("/path")
def handler():
    # Your logic here
    pass
```

- Routes should start with `/`
- Use `/` for the main menu (entry point)

### USSD Object

The `ussd` object provides access to request context and response methods:

#### Properties

- `ussd.msisdn` - User's phone number (str)
- `ussd.session` - Current session ID (str)
- `ussd.input` - User's input from the previous screen (str)

#### Methods

##### `ussd.menu(title, options)`

Display a numbered menu to the user:

```python
ussd.menu("Main Menu", [
    ("Option 1", "/route1"),
    ("Option 2", "/route2"),
    ("Option 3", "/route3")
])
```

- `title` (str): Menu heading
- `options` (List[Tuple[str, str]]): List of (label, route) tuples

The menu automatically handles user input (1, 2, 3, etc.) and routes to the corresponding path.

##### `ussd.screen(text)`

Display a text screen:

```python
ussd.screen(f"Hello {ussd.msisdn}!")
```

##### `ussd.next(route)`

Set the next route to navigate to after user input:

```python
ussd.screen("Enter your PIN:")
ussd.next("/verify-pin")
```

##### `ussd.end()`

End the USSD session:

```python
ussd.screen("Thank you!")
ussd.end()
```

## USSD XML Protocol

Cardisk uses XML for USSD communication:

### Request Format

```xml
<ussd>
    <type>1</type>  <!-- 1=new session, 2=continue -->
    <msisdn>254712345678</msisdn>
    <sessionid>unique-session-id</sessionid>
    <msg>user-input</msg>
</ussd>
```

### Response Format

```xml
<ussd>
    <type>2</type>  <!-- 2=continue, 3=end -->
    <msg>Response text</msg>
</ussd>
```

## Testing Your USSD App

### Using cURL

**1. Start a new session:**
```bash
curl -X POST http://localhost:8000/ \
  -H "Content-Type: application/xml" \
  -d '<ussd><type>1</type><msisdn>254712345678</msisdn><sessionid>test123</sessionid><msg></msg></ussd>'
```

**2. Select a menu option (e.g., option 1):**
```bash
curl -X POST http://localhost:8000/ \
  -H "Content-Type: application/xml" \
  -d '<ussd><type>2</type><msisdn>254712345678</msisdn><sessionid>test123</sessionid><msg>1</msg></ussd>'
```

**3. Enter input:**
```bash
curl -X POST http://localhost:8000/ \
  -H "Content-Type: application/xml" \
  -d '<ussd><type>2</type><msisdn>254712345678</msisdn><sessionid>test123</sessionid><msg>500</msg></ussd>'
```

### Using Python

```python
import httpx

client = httpx.Client(base_url="http://localhost:8000")

# Start session
response = client.post("/", content="""
<ussd>
    <type>1</type>
    <msisdn>254712345678</msisdn>
    <sessionid>test123</sessionid>
    <msg></msg>
</ussd>
""", headers={"Content-Type": "application/xml"})

print(response.text)

# Continue session
response = client.post("/", content="""
<ussd>
    <type>2</type>
    <msisdn>254712345678</msisdn>
    <sessionid>test123</sessionid>
    <msg>1</msg>
</ussd>
""", headers={"Content-Type": "application/xml"})

print(response.text)
```

## Example Application

Check out the full example in `examples/mybank/main.py`:

```bash
cd examples/mybank
cardisk runserver main.py
```

Or:

```bash
cd examples/mybank
uvicorn main:app.app --reload
```

## Session Management

### In-Memory Sessions (Default)

By default, Cardisk uses in-memory session storage, suitable for development and single-instance deployments.

### Redis Sessions

For production with multiple instances, use Redis:

```bash
export CARDISK_USE_REDIS=true
export CARDISK_REDIS_URL=redis://localhost:6379/0

cardisk runserver app.py
```

## Monitoring

### Prometheus Metrics

Cardisk exposes metrics at `/metrics`:

```bash
curl http://localhost:8000/metrics
```

Available metrics:
- `cardisk_requests_total` - Total USSD requests by type

## Docker

### Build

```bash
docker build -t cardisk-app .
```

### Run

```bash
docker run -p 8000:8000 cardisk-app
```

### With Redis

```bash
docker run -p 8000:8000 \
  -e CARDISK_USE_REDIS=true \
  -e CARDISK_REDIS_URL=redis://redis:6379/0 \
  cardisk-app
```

## CLI Commands

### Create a new project

```bash
cardisk new myproject
```

### Run the server

```bash
cardisk runserver app.py --host 0.0.0.0 --port 8000
```

## Advanced Usage

### Custom Error Handling

```python
@app.route("/transfer")
def transfer():
    try:
        amount = float(ussd.input)
        if amount <= 0:
            ussd.screen("Invalid amount. Please try again.")
            ussd.next("/transfer")
        else:
            # Process transfer
            ussd.screen(f"Transfer of ${amount} successful!")
            ussd.end()
    except ValueError:
        ussd.screen("Invalid input. Please enter a number.")
        ussd.next("/transfer")
```

### Multi-Step Forms

```python
@app.route("/register")
def register_name():
    ussd.screen("Enter your name:")
    ussd.next("/register/age")

@app.route("/register/age")
def register_age():
    name = ussd.input
    ussd.screen(f"Hello {name}! Enter your age:")
    ussd.next("/register/confirm")

@app.route("/register/confirm")
def register_confirm():
    age = ussd.input
    ussd.screen(f"Registration complete! Age: {age}")
    ussd.end()
```

## Testing

### Run Tests

```bash
pytest
```

### Run with Coverage

```bash
pytest --cov=cardisk --cov-report=html
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

MIT License

## Support

For issues and questions, please open an issue on GitHub.
