# garantipay

A modern, fully-typed Python client library for the **Garanti BBVA Virtual POS (GVP)** payment gateway API.

Process credit card payments, refunds, and voids through Garanti BBVA's virtual POS system with a clean, Pythonic API.

## Features

- **High-level API** -- Simple methods for common operations (sale, refund, void)
- **Low-level API** -- Full control over every XML field for advanced use cases
- **3D Secure support** -- Process transactions authenticated via 3D Secure
- **Fully typed** -- Complete type annotations and PEP 561 `py.typed` marker
- **Comprehensive error handling** -- Typed exception hierarchy for all failure modes
- **Zero magic** -- Explicit, readable code with no hidden behavior

## Installation

```bash
pip install garantipay
```

Or install from source:

```bash
git clone https://github.com/hbtechsoftware/garantipay.git
cd garantipay
pip install .
```

For development:

```bash
pip install -e ".[dev]"
```

## Quick Start

### High-Level API (Recommended)

The `GarantiClient` class handles hash computation, request assembly, and provides convenient methods for common transaction types.

#### Sale (Payment)

```python
from garantipay import GarantiClient

client = GarantiClient(
    terminal_id="12345678",       # Terminal No (from Garanti BBVA)
    merchant_id="1234567",        # Isyeri No (from Garanti BBVA)
    prov_user_id="1234567",       # Provisioning user ID
    user_id="99999999999",        # API user ID
    password="your_password",     # PROVAUT user password
    mode="TEST",                  # "TEST" for sandbox, "PROD" for production
)

response = client.sale(
    card_number="1234567890123456",
    card_expire_date="0326",       # MMYY format
    card_cvv2="123",
    amount="100",                  # 100 = 1.00 TRY (last 2 digits are kuruş)
    customer_ip="1.2.3.4",
    currency="TRY",                # optional, defaults to "TRY"
    installment_count="",          # empty for single payment
)

if response.is_successful:
    print(f"Payment approved! Ref: {response.transaction.retref_num}")
else:
    print(f"Declined: {response.error_message}")
```

#### Refund

```python
response = client.refund(
    order_id="ORIGINAL_ORDER_ID",  # order ID from the original sale
    amount="100",                  # refund amount (must not exceed original)
    customer_ip="1.2.3.4",
    currency="TRY",
    prov_user_id="PROVRFN",        # typically "PROVRFN" for refunds
)

if response.is_successful:
    print("Refund approved!")
```

#### Void (Cancel)

```python
response = client.void(
    order_id="ORIGINAL_ORDER_ID",
    amount="100",
    customer_ip="1.2.3.4",
    retref_num="original_retref_num",  # from the sale response
    auth_code="original_auth_code",    # from the sale response
)

if response.is_successful:
    print("Transaction voided!")
```

#### 3D Secure Sale

```python
from garantipay import GarantiClient, Secure3D

client = GarantiClient(
    terminal_id="12345678",
    merchant_id="1234567",
    prov_user_id="PROVAUT",
    user_id="99999999999",
    password="your_password",
    mode="TEST",
)

# Data received from the 3D Secure callback
secure_3d = Secure3D(
    md="merchant_data_from_callback",
    txn_id="transaction_id_from_callback",
    security_level="3D",
    authentication_code="auth_code_from_callback",
)

response = client.sale(
    card_number="1234567890123456",
    card_expire_date="0326",
    card_cvv2="123",
    amount="100",
    customer_ip="1.2.3.4",
    secure_3d=secure_3d,
    cardholder_present_code="13",
)
```

### Low-Level API

For advanced use cases where you need full control over every XML field:

```python
from garantipay import (
    API, Request, Terminal, Card, Customer,
    Transaction, Order, CURRENCIES, sha1,
)

api = API()
request = Request()
request.mode = "TEST"
request.version = "v1.0"

request.terminal = Terminal()
request.terminal.id = "12345678"
request.terminal.merchant_id = "1234567"
request.terminal.user_id = "99999999999"
request.terminal.prov_user_id = "PROVAUT"

request.card = Card()
request.card.number = "1234567890123456"
request.card.expire_date = "0326"
request.card.cvv2 = "123"

request.customer = Customer()
request.customer.ip_address = "1.2.3.4"

request.transaction = Transaction()
request.transaction.amount = "100"
request.transaction.currency_code = CURRENCIES["TRY"]
request.transaction.type = "sales"

request.order = Order()
request.order.order_id = ""

# Compute the required hash
password = "your_password"
hash_password = sha1(password + f"{int(request.terminal.id):09d}").upper()
hash_data = (
    f"{request.order.order_id or ''}"
    f"{request.terminal.id or ''}"
    f"{request.card.number or ''}"
    f"{request.transaction.amount or ''}"
    f"{hash_password}"
)
request.terminal.hash_data = sha1(hash_data).upper()

response = api.transaction(request)
```

## Response Handling

The `Response` object provides convenient properties for checking results:

```python
response = client.sale(...)

# Check success
response.is_successful       # True if response code is "00"

# Access details
response.response_code       # e.g. "00" for success
response.response_message    # e.g. "Approved"
response.error_message       # Error description if failed

# Transaction details (when successful)
response.transaction.retref_num       # Retrieval reference number
response.transaction.auth_code        # Authorization code
response.transaction.card_number_masked  # e.g. "12345678****3456"
```

### Pretty-Print XML Response

```python
from garantipay import response_to_pretty_xml

pretty = response_to_pretty_xml(response)
print(pretty)
```

## Error Handling

All exceptions inherit from `GarantiPayError` for easy catch-all handling:

```python
from garantipay.exceptions import (
    GarantiPayError,
    GarantiPayConnectionError,
    GarantiPayValidationError,
    GarantiPayXMLError,
)

try:
    response = client.sale(...)
except GarantiPayValidationError as exc:
    # Invalid input parameters (caught before any network call)
    print(f"Validation error: {exc}")
except GarantiPayConnectionError as exc:
    # Network/timeout error communicating with the API
    print(f"Connection error: {exc}")
except GarantiPayXMLError as exc:
    # Unexpected response format from the API
    print(f"XML parsing error: {exc}")
except GarantiPayError as exc:
    # Catch-all for any library error
    print(f"Error: {exc}")
```

## Supported Currencies

| Code  | Currency           | ISO 4217 |
|-------|--------------------|----------|
| `TRY` | Turkish Lira       | 949      |
| `USD` | US Dollar          | 840      |
| `EUR` | Euro               | 978      |
| `GBP` | British Pound      | 826      |
| `JPY` | Japanese Yen       | 392      |

Aliases `TL`, `YTL`, and `TRL` also map to Turkish Lira (949).

## Amount Format

The API expects amounts as strings where the **last 2 digits represent the fractional part** (kuruş/cents):

| Amount String | Actual Value |
|---------------|--------------|
| `"100"`       | 1.00 TRY     |
| `"1000"`      | 10.00 TRY    |
| `"1050"`      | 10.50 TRY    |
| `"99999"`     | 999.99 TRY   |

## Logging

The library uses Python's standard `logging` module under the `"garantipay"` logger name. Enable debug logging to see raw XML requests and responses:

```python
import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("garantipay")
logger.setLevel(logging.DEBUG)
```

> **Warning**: Debug logs may contain sensitive data (card numbers, passwords). Never enable debug logging in production.

## Project Structure

```
garantipay/
├── pyproject.toml              # Package metadata and build config
├── README.md                   # This file
├── LICENSE                     # MIT license
├── CHANGELOG.md                # Version history
├── src/
│   └── garantipay/
│       ├── __init__.py         # Public API exports
│       ├── py.typed            # PEP 561 typed package marker
│       ├── client.py           # API and GarantiClient classes
│       ├── constants.py        # Endpoints and currency codes
│       ├── exceptions.py       # Exception hierarchy
│       ├── hash.py             # SHA-1 hashing utilities
│       ├── models.py           # Request/response dataclasses
│       ├── xml_builder.py      # Request XML serializer
│       └── xml_parser.py       # Response XML parser
└── examples/
    ├── sale.py                 # Sale transaction example
    ├── refund.py               # Refund transaction example
    ├── sale_3d_secure.py       # 3D Secure sale example
    └── low_level_api.py        # Low-level API example
```

## Requirements

- Python 3.9+
- `requests` >= 2.28.0

## License

This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
