Metadata-Version: 2.4
Name: ottu-py
Version: 2.1.0
Summary: Python bindings for the Ottu Checkout API
Author-email: Jerin Peter George <jerinpetergeorge@gmail.com>
Project-URL: Homepage, https://github.com/ottuco/ottu-py
Classifier: Environment :: Web Environment
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.25.0
Provides-Extra: django
Requires-Dist: django<4.3; extra == "django"
Requires-Dist: asgiref>=3.6.0; extra == "django"
Provides-Extra: async
Requires-Dist: asgiref>=3.6.0; extra == "async"
Provides-Extra: all
Requires-Dist: ottu-py[async,django]; extra == "all"
Provides-Extra: test
Requires-Dist: pytest==7.4.3; extra == "test"
Requires-Dist: pytest-django==4.7.0; extra == "test"
Requires-Dist: pytest-httpx==0.27.0; extra == "test"
Requires-Dist: pytest-cov==4.1.0; extra == "test"
Requires-Dist: pytest-mock==3.12.0; extra == "test"
Requires-Dist: pytest-asyncio==0.21.1; extra == "test"
Provides-Extra: lint-and-formatting
Requires-Dist: black; extra == "lint-and-formatting"
Requires-Dist: flake8; extra == "lint-and-formatting"
Requires-Dist: flake8-print; extra == "lint-and-formatting"
Requires-Dist: isort; extra == "lint-and-formatting"
Requires-Dist: pre-commit; extra == "lint-and-formatting"
Requires-Dist: mypy; extra == "lint-and-formatting"
Provides-Extra: dev
Requires-Dist: ottu-py[all,lint-and-formatting,test]; extra == "dev"
Requires-Dist: bump2version~=1.0.1; extra == "dev"
Dynamic: license-file

# ottu-py

[![PyPI version](https://img.shields.io/pypi/v/ottu-py.svg)](https://pypi.python.org/pypi/ottu-py)

## Installation

```bash
pip install ottu-py
```

For Django integration (includes async support):

```bash
pip install ottu-py[django]
```

For async support without Django:

```bash
pip install ottu-py[async]
```

The SDK supports both synchronous and asynchronous operations out of the box.

# APIs

## Initialization

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key")
)
```

Instead of `APIKeyAuth`, you can also use other authentication methods or your own custom auth classes. For more details, see [authentication](#authentication) section.


Apart from these `merchant_id` and `auth`, you can also pass the `customer_id` and `is_sandbox` values. This is used
while calling the checkout API and other situations.

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    customer_id="your-customer-id"
)
```

## APIs

### Checkout (aka Session Create)

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth
from ottu.enums import TxnType

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    customer_id="your-customer-id"
)
response = ottu.checkout(
    txn_type=TxnType.PAYMENT_REQUEST,
    amount="20.23",
    currency_code="KWD",
    pg_codes=["mpgs", "ottu_pg"],
    customer_phone="+96550000000",
    order_no="1234567890",
    ...
)
print(response)
```

**Response**

```
{
    "success": true,
    "status_code": 201,
    "endpoint": "/b/checkout/v1/pymt-txn/",
    "response": {
        "amount": "20.230",
        "checkout_url": "https://hotfix.ottu.dev/b/checkout/redirect/start/?session_id=809429a6c912990b195e4e60652436fcae587757",
        "currency_code": "KWD",
        "expiration_time": "00:50:00",
        "session_id": "809429a6c912990b195e4e60652436fcae587757",
        "type": "payment_request",
        ...
    },
    "error": {}
}
```

You can also access these `Session` attributes using `ottu.session` property.

```python
print(ottu.session.session_id)
# 809429a6c912990b195e4e60652436fcae587757

print(ottu.session.checkout_url)
# https://hotfix.ottu.dev/b/checkout/redirect/start/?session_id=809429a6c912990b195e4e60652436fcae587757
```

It is also possible to use the `Session.create(...)` to create a new checkout session. That is, `ottu.checkout(...)` is
an aliase for `ottu.session.create(...)` method.

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth
from ottu.enums import TxnType

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    customer_id="your-customer-id"
)
response = ottu.session.create(
    txn_type=TxnType.PAYMENT_REQUEST,
    amount="20.23",
    currency_code="KWD",
    pg_codes=["mpgs", "ottu_pg"],
    customer_phone="+96550000000",
    order_no="1234567890",
    ...
)
print(response)
```
#### Checkout API with dynamic argument(s)

Developers can pass any number of dynamic arguments to the `checkout(...)` method. The dynamic arguments will be passed to the API as it is.

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth
from ottu.enums import TxnType

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    customer_id="your-customer-id"
)
response = ottu.checkout(
    txn_type=TxnType.PAYMENT_REQUEST,
    amount="20.23",
    currency_code="KWD",
    pg_codes=["mpgs", "ottu_pg"],
    customer_phone="+96550000000",
    order_no="1234567890",

    extra_name="John", # dynamic argument 1
    extra_age="25", # dynamic argument 2
    extra_country="Kuwait", # dynamic argument 3
)
print(response)
```
Here, `extra_name`, `extra_age`, and `extra_country` are the dynamic arguments which are not supported by the SDK, but may be supported by the API.
```python

### Session Retrieve

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
response = ottu.session.retrieve(session_id="your-session-id")
print(response)
```

### Session Update

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
response = ottu.session.update(
    session_id="your-session-id",
    amount="1234.23",
)
print(response)
```

### Attachments (Special Case)

You can attach the file to session either while creating the session or updating the session using `attachment`
argument.

`attachment` argument accepts a string that represents the path to the file.

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth
from ottu.enums import TxnType

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    customer_id="your-customer-id"
)
ottu.session.create(
    txn_type=TxnType.PAYMENT_REQUEST,
    amount="20.23",
    currency_code="KWD",
    pg_codes=["mpgs", "ottu_pg"],
    customer_phone="+96550000000",
    order_no="1234567890",
    attachment="path/to/file.pdf",  # or "/path/to/file.pdf"
)
# update the session with updated attachment
response = ottu.session.update(
    attachment="path/to/file-1234.pdf",  # or "/path/to/file
)
print(response)
```

### Accessing the payment methods

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth
from ottu.enums import TxnType

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    customer_id="your-customer-id"
)
ottu.session.create(
    txn_type=TxnType.PAYMENT_REQUEST,
    amount="20.23",
    currency_code="KWD",
    pg_codes=["mpgs", "ottu_pg"],
    customer_phone="+96550000000",
    order_no="1234567890",
    attachment="path/to/file.pdf",  # or "/path/to/file.pdf"
)
print(ottu.session.payment_methods)
```

### Operations

All operations are performed on the `ottu.session` object. Also, these methods accept either `session_id`
or `order_no` as an argument. If `session_id` is not passed, then it will use the `session_id` from the `ottu.session`
object.

#### Cancel

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
ottu.session.retrieve(session_id="your-session-id")
response = ottu.session.cancel()
print(response)
```

To specify the `session_id` while canceling the session, you can pass the `session_id` as an argument to
the `cancel(...)` method.

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
response = ottu.session.cancel(session_id="your-session-id")
print(response)
```

To specify the `order_id` while canceling the session, you can pass the `order_id` as an argument to the `cancel(...)`
method.

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
response = ottu.session.cancel(order_id="your-order-id")
print(response)
```

#### Expire

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
response = ottu.session.expire(session_id="your-session-id")
print(response)
```

#### Delete

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
response = ottu.session.delete(session_id="your-session-id")
print(response)
```

#### Capture

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
response = ottu.session.capture(session_id="your-session-id", amount="20.23", tracking_key="your-tracking-key")
print(response)
```

#### Refund

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
response = ottu.session.refund(session_id="your-session-id", amount="20.23", tracking_key="your-tracking-key")
print(response)
```

#### Void

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
response = ottu.session.void(session_id="your-session-id", tracking_key="your-tracking-key")
print(response)
```

#### Payment Status Query (PSQ)

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
response = ottu.session.psq(session_id="your-session-id")
print(response)
```

To specify the `order_id` while querying, you can pass the `order_id` as an argument to the `psq(...)` method.

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
response = ottu.session.psq(order_id="your-order-id")
print(response)
```

### Cards

#### List all cards for a customer

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    customer_id="your-customer-id"
)
response = ottu.cards.list()
print(response)
```

### Get latest card for a customer

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    customer_id="your-customer-id"
)
response = ottu.cards.get()
print(response)
```

### Delete a card for a customer using token

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    customer_id="your-customer-id"
)
response = ottu.cards.delete(token="your-card-token")
print(response)
```


### Checkout Autoflow
Create a checkout session without specifying the PG codes. The PG codes will be automatically
selected based on the currency code, either from the cache or payment method API.

Behind the scenes, `ottu-py` will automatically fetch the payment methods from the cache or payment method API and
select the PG codes based on the currency code.
```python
from ottu import Ottu
from ottu.auth import APIKeyAuth
from ottu.enums import TxnType

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    customer_id="your-customer-id"
)
response = ottu.checkout_autoflow(
    txn_type=TxnType.PAYMENT_REQUEST,
    amount="20.23",
    currency_code="KWD",
)
print(response)
```

### Auto-debit Autoflow
Completing the auto-debit transaction without specifying the PG codes. The PG codes will be automatically
selected based on the currency code from the payment method API. You must pass the value for `token` that represents the saved card token.

Behind the scenes, `ottu-py` will automatically fetch the token from the database (only if you use the Django integration) and
select the PG codes based on the currency code by calling the payment method API.

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth
from ottu.enums import TxnType

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    customer_id="your-customer-id"
)
response = ottu.auto_debit_autoflow(
   txn_type=TxnType.PAYMENT_REQUEST,
   amount="20.23",
   currency_code="KWD",
   token="your-card-token",
   agreement={
         "id": "test-agreement-id",
         # other agreement attributes
     },

)
print(response)
```

Optionally, you can pass the `pg_codes` to `auto_debit_autoflow(...)` method. If you pass the `pg_codes`, then the
`ottu-py` will not call the payment method API to fetch the PG codes.

```python
from ottu import Ottu
from ottu.auth import APIKeyAuth
from ottu.enums import TxnType

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    customer_id="your-customer-id"
)
response = ottu.auto_debit_autoflow(
   txn_type=TxnType.PAYMENT_REQUEST,
   amount="20.23",
   currency_code="KWD",
   token="your-card-token",
   pg_codes=["ottu_pg"],
   agreement={
      "id": "test-agreement-id",
      # other agreement attributes
   },

)
print(response)
```

## Async Support

The SDK provides full asynchronous support through the `OttuAsync` class using `asgiref.sync.sync_to_async` wrapper. This ensures 100% identical behavior between sync and async versions while providing proper async/await support.

### Installation

For async support, install with the async extra:

```bash
pip install 'ottu-py[async]'
```

Async support is also automatically included with Django extra.

### Basic Async Usage

```python
from ottu import OttuAsync
from ottu.auth import APIKeyAuth
from ottu.enums import TxnType

async def main():
    async with OttuAsync(
        merchant_id="merchant.id.ottu.dev",
        auth=APIKeyAuth("your-secret-api-key"),
        is_sandbox=True
    ) as ottu:
        # Create checkout session
        response = await ottu.checkout(
            txn_type=TxnType.PAYMENT_REQUEST,
            amount="20.23",
            currency_code="KWD",
            pg_codes=["mpgs", "ottu_pg"],
            customer_phone="+96550000000",
            order_no="1234567890",
        )
        print(response)

import asyncio
asyncio.run(main())
```

### Async Context Manager

Always use `async with` when working with `OttuAsync` to ensure proper cleanup:

```python
async with OttuAsync(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key")
) as ottu:
    # All operations here
    pass
```

### Identical API

Since `OttuAsync` wraps the sync implementation with `sync_to_async`, all methods have identical signatures and behavior:

```python
async with OttuAsync(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key")
) as ottu:
    # All sync methods are available as async
    response = await ottu.checkout(...)
    cards = await ottu.cards.list()
    session = await ottu.session.retrieve(session_id="...")
    await ottu.session.capture(session_id="...")
```

### Error Handling

Async operations follow the exact same error handling pattern as sync operations:

```python
async with OttuAsync(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key")
) as ottu:
    response = await ottu.checkout(
        txn_type=TxnType.PAYMENT_REQUEST,
        amount="20.23",
        currency_code="KWD"
    )

    if response.success:
        print(f"Checkout successful: {response.response}")
    else:
        print(f"Checkout failed: {response.error}")
```

### Framework Integration

This async implementation works seamlessly with:
- **FastAPI**: Install with `pip install ottu-py[async]` for native async/await support
- **Django**: Install with `pip install ottu-py[django]` for Django's async views (includes async support)
- **aiohttp**: Install with `pip install ottu-py[async]` for async support
- **Any async framework**: Standard async/await pattern with `pip install ottu-py[async]`

### Design Principles

- **100% Behavioral Consistency**: Uses the same underlying sync code wrapped with `sync_to_async`
- **Zero Code Duplication**: No separate async implementation to maintain
- **Standard Patterns**: Uses Django's proven `sync_to_async` approach
- **Framework Agnostic**: Works with any async Python framework

## Django Integration

Add `ottu.contrib.django` to your `INSTALLED_APPS` setting.

```python
INSTALLED_APPS = [
    ...,
    "ottu.contrib.django",
    ...,
]
```

Set values for the following settings variables.

### Settings

#### Authentication Settings

`OTTU_AUTH` - The variable `OTTU_AUTH` is a dictionary that represents the authentication settings. It should contain mandatory field named `class` which represents the _dotted path_ to the authentication class.
`OTTU_AUTH` can also contain other fields that are required by the authentication class.

```python
# settings.py

OTTU_AUTH = {
    "class": "ottu.auth.BasicAuth",
    "username": "dj_username",
    "password": "dj_password",
}
```
OR
```python
# settings.py

OTTU_AUTH = {
    "class": "ottu.auth.APIKeyAuth",
    "api_key": "my-api-key",
}
```
#### Other Settings

* `OTTU_MERCHANT_ID` - Merchant ID (example: `merchant.id.ottu.dev`)
* `OTTU_WEBHOOK_KEY` - Webhook Key (example: `my-secret-webhook-key`)
* `OTTU_WEBHOOK_URL` - Webhook URL (example: `https://your-host.com/path/to/view/`)
* `OTTU_IS_SANDBOX` - Sandbox environment or not (example: `True` or `False`). Default is `False`.

In the case of authentication, it is mandatory to set any set of authentication settings.

### Checkout

```python
# any_module.py
from ottu.contrib.django.core.ottu import ottu

response = ottu.checkout(
    txn_type=TxnType.PAYMENT_REQUEST,
    amount="20.23",
    currency_code="KWD",
    pg_codes=["mpgs", "ottu_pg"],
    customer_id="your-customer-id",
    customer_phone="+96550000000",
    order_no="1234567890",
    ...
)
```

**Note**: The checkout sessions will be automatically saved (and updated if you configure the webhooks) to the database
if you use the `ottu` instance.

### Auto-debit Autoflow
Same as [Auto-debit Autoflow](#auto-debit-autoflow) but it is optional to pass the `token`. If you don't pass the `token`, `ottu-py` will automatically fetch the token from the DB. The token is identified by the `customer_id` and `agreement.id`.
```python
from ottu.contrib.django.core.ottu import ottu

response = ottu.auto_debit_autoflow(
      txn_type=TxnType.PAYMENT_REQUEST,
      amount="20.23",
      currency_code="KWD",
      customer_id="your-customer-id",
      agreement={
            "id": "test-agreement-id",
            # other agreement attributes
        },
      ...
)
```

### Webhooks

To accept webhooks, you must set both `OTTU_WEBHOOK_KEY` and `OTTU_WEBHOOK_URL` settings variables. Also, you must
setup the webhook receiver view that comes with the package.

```python
# views.py
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from ottu.contrib.django.views import WebhookViewAbstractView


@method_decorator(csrf_exempt, name="dispatch")
class WebhookReceiverView(WebhookViewAbstractView):
    pass


# urls.py
from django.urls import path
from .views import WebhookReceiverView

urlpatterns = [
    path("wh-view/", WebhookReceiverView.as_view(), name="wh-view"),
]
```

The view verifies the body `signature` only (see [Webhook Verification](#webhook-verification)). If your receiver
uses `session_id`, `payment_type`, `extra.*`, `token.*` or `agreement.id`, verify the `Ottu-Signature` header instead. `v2` signs every byte of the body, so the header
check alone is enough. Override `verify()` in the view above, pass the raw bytes (`self.request.body`, not
`self.data`), and keep `csrf_exempt`: Ottu posts server-to-server with no CSRF token.

#### Before you switch to header verification

Older Connect versions do not send the `Ottu-Signature` header. Header-only verification against such a version
rejects every webhook with HTTP 401.

Check your Connect version before you change the view. Use one of these two checks:

1. Ask the Ottu team to confirm that your Connect version sends the `Ottu-Signature` header and signs it with `v2`.
2. Log the headers of one real webhook. Find a header that matches this form:
   `Ottu-Signature: t=1792417440,v2=9ccd4a0297b242c1132e09e382fa23fc5c56720ba60ca5af028088d0bd46d823`

- If the header is missing, keep the default view. It verifies the body `signature`.
- If the header is present, use `verify_ottu_signature_raw` in the view. Keep the header required on every request.

> **Warning:** Never accept the body `signature` in place of a missing header. An attacker can remove the header and
> change fields that the body `signature` does not cover, such as `session_id` or `agreement.id`. The view then
> accepts the forged webhook.

```python
# views.py
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt

from ottu.contrib.django import conf
from ottu.contrib.django.views import WebhookViewAbstractView
from ottu.utils.webhooks import verify_ottu_signature_raw


@method_decorator(csrf_exempt, name="dispatch")
class WebhookReceiverView(WebhookViewAbstractView):
    def verify(self) -> bool:
        return verify_ottu_signature_raw(
            body=self.request.body,
            header=self.request.headers.get("Ottu-Signature"),
            webhook_key=conf.WEBHOOK_KEY,
        )
```

#### Reading v3 events

On webhook format 3, every body is one event envelope. Verify the header on the raw bytes first, then parse:

```python
import json

from ottu.utils.webhooks import InvalidOttuEvent, OttuEvent, verify_ottu_signature_raw

if not verify_ottu_signature_raw(request.body, request.headers.get("Ottu-Signature"), webhook_key):
    return HttpResponse(status=401)
try:
    event = OttuEvent.from_dict(json.loads(request.body))
# A deeply nested body raises RecursionError, not ValueError.
except (ValueError, RecursionError):  # InvalidOttuEvent is a ValueError
    return HttpResponse(status=400)
if event.merchant_id != MERCHANT_ID or event.is_sandbox != IS_SANDBOX:
    return HttpResponse(status=400)  # signed, but meant for another merchant or environment
if event.type == "payment.paid":
    payment = event.data.get("payment")
```

`event.type` can be any string: Connect adds event types without a new ottu-py release. Ignore the types you do
not handle. `event.id` is the same on every retry of one event; store it and skip an id you already processed.

Formats 1 and 2 are unchanged: keep verifying them with `verify_signature` or the header check above.

### Miscellaneous

### Authentication

The `ottu-py` supports a few authentication methods, usually suitable in most of the cases. `BasicAuth`, `APIKeyAuth`, `KeycloakPasswordAuth` and `KeycloakClientAuth` are the available authentication methods.
* `BasicAuth` - Basic authentication, using `username` and `password`.
   ```python
   from ottu import Ottu
   from ottu.auth import BasicAuth

   ottu = Ottu(
       merchant_id="merchant.id.ottu.dev",
       auth=BasicAuth(
           username="your-username",
           password="your-password"
       )
   )
   ```
* `BasicAuthorizationHeaderAuth` - Basic auth setup for header based authentication. You can pass the entire`Authorization` header value into the constructor.
  ```python
  from ottu import Ottu
  from ottu.auth import BasicAuthorizationHeaderAuth

  token = "Your-Token-Here"
  ottu = Ottu(
      merchant_id="merchant.id.ottu.dev",
      auth=BasicAuthorizationHeaderAuth(
          header=f"Token {token}"
      )
  )
  ```
* `TokenAuth` - Same as `BasicAuthorizationHeaderAuth`, but, the constructor accepts the token and prefix as separate arguments. This is useful when you want to use a different prefix than `Token`.
  ```python
  from ottu import Ottu
  from ottu.auth import TokenAuth

  token = "Your-Token-Here"
  ottu = Ottu(
      merchant_id="merchant.id.ottu.dev",
      auth=TokenAuth(
          token=token,
          prefix="Token",
      )
  )
  ```
* `APIKeyAuth` - Authentication using API Key. This is also same as `TokenAuth` but, constructor accepts the `api_key` instead of `token`.
   ```python
   from ottu import Ottu
   from ottu.auth import APIKeyAuth

   ottu = Ottu(
       merchant_id="merchant.id.ottu.dev",
       auth=APIKeyAuth(
          api_key="your-api-key"
       )
   )
   ```
* `KeycloakPasswordAuth` - Authentication using Keycloak using `password` grant type.
   ```python
   from ottu import Ottu
   from ottu.auth import KeycloakPasswordAuth

   ottu = Ottu(
       merchant_id="merchant.id.ottu.dev",
       auth=KeycloakPasswordAuth(
            username="your-username",
            password="your-password",
            client_id="your-client-id",
       )
   )
   ```
  You may also need to supply `client_secret` if it is required by the Keycloak server.
    ```python
    from ottu import Ottu
    from ottu.auth import KeycloakPasswordAuth

    ottu = Ottu(
        merchant_id="merchant.id.ottu.dev",
        auth=KeycloakPasswordAuth(
            username="your-username",
            password="your-password",
            client_id="your-client-id",
            client_secret="your-client-secret"
        )
    )
    ```
* `KeycloakClientAuth` - Authentication using Keycloak using `client_credentials` grant type.
    ```python
    from ottu import Ottu
    from ottu.auth import KeycloakClientAuth

    ottu = Ottu(
        merchant_id="merchant.id.ottu.dev",
        auth=KeycloakClientAuth(
            client_id="your-client-id",
            client_secret="your-client-secret"
        )
    )
    ```
### Payment Methods
You can call the `Ottu.get_payment_methods(...)` method to get the available payment methods for the given merchant.
```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
response = ottu.get_payment_methods(
    plugin="payment_request",
    currencies=["KWD", "USD"],
    customer_id="your-customer-id",
    operation="purchase",
    tokenizable=False,
    pg_names=[]
)
print(response)
```

### Webhook Verification

Verify the body `signature` using `verify_signature(...)`. It checks the documented 18-field recipe only. It returns `False` for a missing, non-string or non-ASCII signature, and for an empty or non-string `webhook_key`.

```python
from ottu.utils.webhooks import verify_signature

webhook_data_received = {
   "amount":"86.000",
   "currency_code":"KWD",
   "customer_first_name":"example-customer",
   ...,  # other attributes
   "signature":"6143b8ad4bd283540721ab000f6de746e722231aaaa90bc38f639081d3ff9f67"
}
verified = verify_signature(
   payload=webhook_data_received,
   signature=webhook_data_received["signature"], # Usually, signature will be sent along with the webhook data.
   webhook_key=hmac_secret_key_received_from_ottu  # HMAC Secret Key received from Ottu
)
```

#### `Ottu-Signature` header

Newer Connect versions add a second signature to each webhook delivery in the `Ottu-Signature` HTTP header.
Older versions do not send it, so read
[Before you switch to header verification](#before-you-switch-to-header-verification) first. The header has this
form: `t=<unix_seconds>,v2=<hex>`. `v2` is HMAC-SHA256, with the same key as the body `signature`, over `<t>.` followed by
the exact raw request body. It covers every field in the body, including those the body `signature` does not
(`session_id`, `payment_type`, `extra.*`, `token.*`, `agreement.id`), so a receiver that uses those fields must verify
the header.

```python
from ottu.utils.webhooks import verify_ottu_signature_raw

verified = verify_ottu_signature_raw(
    body=request.body,  # the raw bytes as received, NOT the parsed JSON
    header=request.headers.get("Ottu-Signature"),
    webhook_key=hmac_secret_key_received_from_ottu,
)
```

- Pass the bytes exactly as received. Parsing the JSON and dumping it again changes the bytes (spaces, key order,
  `\u` escapes), and the check fails.
- It returns `False` and never raises on bad input: a body that is not `bytes`, a missing or malformed header, no
  `v2` pair, a bad `t`, or a signature mismatch. Other versions in the header (a future `v3`) are ignored.
  An empty or non-string `webhook_key` also returns `False`.
- `t` must be within `tolerance_seconds` of the current time, in the past or in the future (default `300`).
  Pass `tolerance_seconds=None` to switch the age check off.
- Changing the recipe would be a new version (`v3`), never an edit of `v2`.

### API Response Structure

All API calls must have the following structure.

```json
{
   "success": true,
   "status_code": 200,
   "endpoint": "/path/to/api/endpoint/",
   "response": {},
   "error": {}
}
```
* `success` - Boolean value that represents the success of the API call.
* `status_code` - HTTP status code of the API call.
* `endpoint` - API endpoint of the last API call.
* `response` - Response from the API call.
* `error` - Error from the API call.

If the call was `success`, the `response` field will be present and the `error` field will be empty, and vice-versa.

In most of the cases, the `error` will be a JSON object with a key of `detail` and value of the error message.

```json
{
   "success": false,
   "status_code": 400,
   "endpoint": "/path/to/api/endpoint/",
   "response": {},
   "error": {
      "detail": "Error message"
   }
}
```
### Timeout

You can set the `timeout` value for all requests made by the SDK. The default value is `30` seconds.

**Method 1** - Set the timeout value while initializing the `Ottu` object.
```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

ottu = Ottu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
    timeout=60 # Set the timeout to 60 seconds
)
```

**Method 2** - Set the timeout value using the `timeout` property.
```python
from ottu import Ottu
from ottu.auth import APIKeyAuth

class MyOttu(Ottu):
  default_timeout = 50 # Set the default timeout to 50 seconds

ottu = MyOttu(
    merchant_id="merchant.id.ottu.dev",
    auth=APIKeyAuth("your-secret-api-key"),
)
```

**Note:** The timeout value set using the `timeout` parameter will override the value set by the `default_timeout` attribute.


## Test

```bash
# Install the dependencies
pip install .[test]

# Run tests
python -m pytest
```

## Release

`main` accepts changes only through an approved pull request, so the version bump goes in the release PR.

```bash
# 1. On the PR branch: date the top CHANGELOG heading, then bump without a tag
bump2version --dry-run --verbose --no-tag [major|minor|patch]
bump2version --verbose --no-tag [major|minor|patch]
git push

# 2. After the PR is approved and merged, tag the merge commit on main
git checkout main && git pull --ff-only
git tag -a vX.Y.Z -m "Version X.Y.Z"
git push origin vX.Y.Z

# 3. Publish the GitHub release. The publish workflow uploads the package to PyPI.
gh release create vX.Y.Z --title vX.Y.Z --notes "<CHANGELOG entry>"
```

Tag only after the merge. A tag made on the PR branch can point at a commit that `main` does not contain.
