Metadata-Version: 2.4
Name: paylinker
Version: 0.1.0
Summary: Framework-agnostic payment integration SDK for Uzbekistan payment providers (Click, Payme)
Project-URL: Homepage, https://github.com/tohirbek/paylinker
Project-URL: Repository, https://github.com/tohirbek/paylinker
Project-URL: Documentation, https://github.com/tohirbek/paylinker#readme
Project-URL: Issues, https://github.com/tohirbek/paylinker/issues
Project-URL: Changelog, https://github.com/tohirbek/paylinker/blob/main/CHANGELOG.md
Author-email: Tohirbek <turgunovkozimzon30@gmail.com>
Maintainer-email: Tohirbek <turgunovkozimzon30@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: click,fintech,payme,payment,payment-gateway,sdk,uzbekistan
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Office/Business :: Financial :: Point-Of-Sale
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1.0,>=0.27
Requires-Dist: pydantic<3.0,>=2.0
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.13; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.22; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Requires-Dist: twine>=5.1; extra == 'dev'
Description-Content-Type: text/markdown

# paylinker

Framework-agnostic payment integration SDK for Uzbekistan payment providers.

**Currently supported:** Click (SHOP-API + Merchant API), Payme (JSON-RPC Merchant API)

**Planned:** Uzum, and more.

## Features

- Pure Python — works with Django, FastAPI, Flask, Celery, or plain scripts
- Sync and async HTTP clients
- Pydantic v2 schema validation
- Unified exception hierarchy
- **Click:** Signature verification, webhook handler, Merchant API client
- **Payme:** JSON-RPC handler, Basic Auth, full transaction lifecycle
- Fully typed (PEP 561 compatible)

## Installation

```bash
pip install paylinker
```

## Quick Start

### 1. Payment URL generation

```python
from paylinker import ClickConfig, ClickProvider

config = ClickConfig(
    merchant_id=12345,
    service_id=67890,
    secret_key="your-secret-key",
)
provider = ClickProvider(config)

url = provider.generate_payment_url(
    amount=50000.0,
    transaction_param="order-42",
    return_url="https://yoursite.com/done",
)
# Redirect user to this URL
```

### 2. Webhook handling (SHOP-API)

```python
from paylinker import ClickConfig, ClickWebhookHandler
from paylinker.providers.click.webhook import OrderCheckResult
from paylinker.providers.click.enums import ClickErrorCode

class MyOrderValidator:
    def check_order(self, merchant_trans_id, amount):
        order = db.get_order(merchant_trans_id)
        if not order:
            return OrderCheckResult.fail(ClickErrorCode.ORDER_NOT_FOUND, "Not found")
        if order.is_paid:
            return OrderCheckResult.fail(ClickErrorCode.ALREADY_PAID, "Already paid")
        if float(order.amount) != float(amount):
            return OrderCheckResult.fail(ClickErrorCode.INCORRECT_AMOUNT, "Wrong amount")
        return OrderCheckResult.ok()

    def on_prepare(self, click_trans_id, merchant_trans_id, amount):
        return db.create_transaction(click_trans_id, merchant_trans_id, amount).id

    def on_complete(self, click_trans_id, merchant_trans_id, merchant_prepare_id, amount):
        db.confirm_payment(merchant_prepare_id)
        return merchant_prepare_id

    def on_cancelled(self, click_trans_id, merchant_trans_id, merchant_prepare_id):
        db.cancel_transaction(merchant_prepare_id)

handler = ClickWebhookHandler(config, MyOrderValidator())

# In any framework:
result = handler.handle(request_data)  # dict in, dict out
```

### 3. Merchant API (server-to-server)

```python
from paylinker import ClickConfig, ClickMerchantClient

config = ClickConfig(
    merchant_id=12345,
    service_id=67890,
    secret_key="shop-secret",
    merchant_user_id=111,
    merchant_api_secret="api-secret",
)

with ClickMerchantClient(config) as client:
    invoice = client.create_invoice(
        amount=50000.0,
        phone_number="998901234567",
        merchant_trans_id="order-42",
    )
    print(invoice.invoice_id)
```

Async version:

```python
from paylinker import AsyncClickMerchantClient

async with AsyncClickMerchantClient(config) as client:
    invoice = await client.create_invoice(...)
```

### 4. Payme — JSON-RPC handling

```python
from paylinker import PaymeConfig, PaymeRpcHandler
from paylinker.providers.payme.rpc_handler import PaymeError
from paylinker.providers.payme.schemas import CheckPerformResult, CreateTransactionResult

config = PaymeConfig(
    merchant_id="your-merchant-id",
    merchant_key="your-key",
)

class MyTransactionHandler:
    def check_perform(self, amount, account):
        return CheckPerformResult(allow=True)
    def create_transaction(self, payme_id, payme_time, amount, account):
        ...  # Create and return CreateTransactionResult
    def perform_transaction(self, payme_id): ...
    def cancel_transaction(self, payme_id, reason): ...
    def check_transaction(self, payme_id): ...
    def get_statement(self, from_time, to_time): ...

handler = PaymeRpcHandler(config, MyTransactionHandler())

# In any framework — always return HTTP 200:
result = handler.handle(request_body_dict, authorization_header)
```

### 5. Payme — Payment URL

```python
from paylinker import PaymeConfig, PaymeProvider

provider = PaymeProvider(config)
url = provider.generate_payment_url(
    amount=5000000,  # 50,000 UZS in tiyin
    transaction_param="order-123",
)
```

## Project Structure

```
src/paylinker/
├── __init__.py          # Public API
├── config/              # Provider configuration (Pydantic)
├── clients/             # Sync + async HTTP clients
├── providers/
│   ├── _base.py         # Abstract base provider
│   ├── click/           # Click provider
│   │   ├── provider.py  # Payment URL generation
│   │   ├── webhook.py   # SHOP-API webhook handler
│   │   ├── merchant.py  # Merchant API client
│   │   ├── schemas.py   # Request/response models
│   │   ├── signing.py   # Signature utilities
│   │   └── enums.py     # Error codes, action types
│   └── payme/           # Payme provider
│       ├── provider.py  # Payment URL generation (GET + POST)
│       ├── rpc_handler.py  # JSON-RPC method dispatcher
│       ├── auth.py      # Basic Auth verification
│       ├── schemas.py   # JSON-RPC request/response models
│       └── enums.py     # Transaction states, error codes
├── schemas/             # Base schema classes
├── exceptions/          # Unified exception hierarchy
├── types/               # Shared type aliases
└── utils/               # Hashing utilities
```

## Development

```bash
pip install -e ".[dev]"
pytest
ruff check src/ tests/
mypy src/
```

## Building & Publishing

This package uses [Hatchling](https://hatch.pypa.io/) and a single source
of truth for the version (`src/paylinker/_version.py`).

### Local build

```bash
pip install build twine
python -m build              # produces dist/*.whl and dist/*.tar.gz
twine check --strict dist/*  # validate PyPI metadata
```

### Release workflow (recommended)

Releases are automated via GitHub Actions using **PyPI Trusted Publishing**
(no long-lived API tokens). One-time setup on PyPI / TestPyPI:
add this repo as a trusted publisher for the `paylinker` project, with
workflow `publish.yml` and environments `pypi` / `testpypi`.

Then for each release:

```bash
# 1. Bump version in src/paylinker/_version.py
# 2. Update CHANGELOG.md
git commit -am "release: v0.2.0"

# 3. Tag & push  → triggers TestPyPI publish
git tag v0.2.0
git push origin main --tags

# 4. Create a GitHub Release for the tag → triggers real PyPI publish
gh release create v0.2.0 --notes-from-tag
```

### Manual publish (fallback)

```bash
python -m build
twine upload --repository testpypi dist/*    # TestPyPI
twine upload dist/*                          # PyPI
```

### Versioning

Follows [SemVer](https://semver.org/):
`MAJOR.MINOR.PATCH` — breaking / feature / fix.

## License

MIT
