Metadata-Version: 2.4
Name: bayarcash
Version: 1.0.0
Summary: Python SDK for the Bayarcash payment gateway.
Project-URL: Homepage, https://bayarcash.com
Project-URL: Documentation, https://api.webimpian.support/bayarcash
Project-URL: Source, https://github.com/bayarcash/bayarcash-python
Author-email: Web Impian <infrastructure@webimpian.com>
License: MIT
License-File: LICENSE
Keywords: bayarcash,duitnow,fpx,gateway,malaysia,payment
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
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 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Requires-Dist: requests>=2.20
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: responses>=0.23; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Description-Content-Type: text/markdown

# Bayarcash Payment Gateway Python SDK

[![PyPI version](https://img.shields.io/pypi/v/bayarcash.svg)](https://pypi.org/project/bayarcash/)
[![Python versions](https://img.shields.io/pypi/pyversions/bayarcash.svg)](https://pypi.org/project/bayarcash/)
[![License](https://img.shields.io/pypi/l/bayarcash.svg)](https://pypi.org/project/bayarcash/)

The [Bayarcash](https://bayarcash.com/) SDK provides an expressive interface for
interacting with Bayarcash's Payment Gateway API. It supports both API **v2**
(default) and **v3**, with additional query features available in v3. This is a
feature-parity Python port of the official [Bayarcash PHP SDK](https://packagist.org/packages/bayarcash/php-sdk).

## Table of Contents

- [Requirements](#requirements)
- [Installation](#installation)
- [Getting Started](#getting-started)
  - [Configuration](#configuration)
- [Quick Start: Accept a Payment](#quick-start-accept-a-payment)
- [Payment Channels](#payment-channels)
- [Creating a Payment Intent](#creating-a-payment-intent)
- [Handling Callbacks](#handling-callbacks)
- [Payment & Transaction Status](#payment--transaction-status)
- [Transactions](#transactions)
- [FPX Direct Debit](#fpx-direct-debit)
- [Manual Bank Transfer](#manual-bank-transfer)
- [Portals & FPX Banks](#portals--fpx-banks)
- [Error Handling](#error-handling)
- [Response Objects](#response-objects)
- [Security Recommendations](#security-recommendations)
- [Support](#support)

## Requirements

- Python 3.8+
- `requests`

## Installation

Install from PyPI:

```bash
pip install bayarcash
```

You will need two credentials from your Bayarcash console:

- **API token** — used to authenticate SDK requests.
- **API secret key** — used to generate request checksums and verify callbacks.

## Getting Started

```python
from bayarcash import Bayarcash

bayarcash = Bayarcash("YOUR_API_TOKEN", secret_key="YOUR_API_SECRET_KEY")
bayarcash.use_sandbox()  # remove this line in production
```

The constructor accepts optional keyword arguments:

```python
bayarcash = Bayarcash(
    "YOUR_API_TOKEN",
    secret_key="YOUR_API_SECRET_KEY",
    sandbox=True,        # default False
    api_version="v3",    # "v2" (default) or "v3"
    timeout=60,          # request timeout in seconds (default 30)
)
```

### Configuration

The same settings are available as chainable setters:

```python
(bayarcash
    .use_sandbox()           # switch to the sandbox environment
    .set_api_version("v3")   # "v2" (default) or "v3"
    .set_timeout(60))        # request timeout in seconds (default 30)

bayarcash.get_api_version()  # read back the current version
```

> Call `use_sandbox()` / `set_api_version()` **before** making requests. Omit
> `use_sandbox()` in production to hit the live gateway.

When you construct the client with a `secret_key`, checksum and callback methods
can be called with `None` as the secret argument and will use the stored key.

## Quick Start: Accept a Payment

A complete FPX payment flow, from creating the payment to redirecting the payer:

```python
from bayarcash import Bayarcash

bayarcash = Bayarcash("YOUR_API_TOKEN", secret_key="YOUR_API_SECRET_KEY")
bayarcash.use_sandbox()

# 1. Build the payment request
data = {
    "portal_key": "your_portal_key",
    "payment_channel": Bayarcash.FPX,
    "order_number": "INV-1001",
    "amount": "10.00",
    "payer_name": "Ahmad bin Abdullah",
    "payer_email": "ahmad@example.com",
    "payer_telephone_number": "0123456789",
    "return_url": "https://your-site.com/payment/return",
    "callback_url": "https://your-site.com/payment/callback",
}

# 2. Sign it (recommended). Pass None to use the client's stored secret_key.
data["checksum"] = bayarcash.create_payment_intent_checksum_value(None, data)

# 3. Create the payment intent and redirect the payer to Bayarcash
payment_intent = bayarcash.create_payment_intent(data)
# e.g. in a web framework: redirect(payment_intent.url)
print(payment_intent.url)
```

After payment, Bayarcash calls your `callback_url` (server-to-server) and
redirects the payer to your `return_url`. Verify both — see
[Handling Callbacks](#handling-callbacks).

## Payment Channels

Pass one of these constants (or a list of them) as `payment_channel`:

```python
Bayarcash.FPX                 # FPX Online Banking
Bayarcash.MANUAL_TRANSFER     # Manual Bank Transfer
Bayarcash.FPX_DIRECT_DEBIT    # FPX Direct Debit
Bayarcash.FPX_LINE_OF_CREDIT  # FPX Line of Credit
Bayarcash.DUITNOW_DOBW        # DuitNow Online Banking
Bayarcash.DUITNOW_QR          # DuitNow QR
Bayarcash.SPAYLATER           # ShopeePayLater
Bayarcash.BOOST_PAYFLEX       # Boost PayFlex
Bayarcash.QRISOB              # QRIS Online Banking
Bayarcash.QRISWALLET          # QRIS Wallet
Bayarcash.NETS                # NETS
Bayarcash.CREDIT_CARD         # Credit Card
Bayarcash.ALIPAY              # Alipay
Bayarcash.WECHATPAY           # WeChat Pay
Bayarcash.PROMPTPAY           # PromptPay
Bayarcash.TOUCH_N_GO          # Touch 'n Go eWallet
Bayarcash.BOOST_WALLET        # Boost Wallet
Bayarcash.GRABPAY             # GrabPay
Bayarcash.GRABPL              # Grab PayLater
Bayarcash.SHOPEE_PAY          # ShopeePay
```

A `PaymentChannel` `IntEnum` is also available: `from bayarcash import PaymentChannel`.

## Creating a Payment Intent

```python
payment_intent = bayarcash.create_payment_intent(data)
```

**Request fields:**

| Field | Required | Description |
|---|---|---|
| `portal_key` | ✅ | Your portal key. |
| `order_number` | ✅ | Your reference. Max 30 chars. |
| `amount` | ✅ | String with up to 2 decimals, e.g. `"10.00"`. Range `1.00`–`30000.00` (min differs for some channels). |
| `payer_name` | ✅ | Max 150 chars. |
| `payer_email` | ✅ | Valid email, max 250 chars. |
| `payment_channel` | ➖ | A `Bayarcash.*` channel id, or a list of ids. If omitted, the payer chooses on the Bayarcash page. |
| `payer_telephone_number` | ➖ | Required for e-wallet / DuitNow channels. Max 20 chars. |
| `return_url` | ➖ | Where the payer's browser is redirected after payment. |
| `callback_url` | ➖ | Server-to-server notification URL. |
| `metadata` | ➖ | Any extra data you want echoed back. |
| `checksum` | ➖ | Recommended. See below. |

### Checksum

The checksum protects the request from tampering. Generate it **after** building
the request and append it as `checksum`:

```python
data["checksum"] = bayarcash.create_payment_intent_checksum_value(secret_key, data)
```

The checksum is computed from `payment_channel`, `order_number`, `amount`,
`payer_name`, and `payer_email`.

## Handling Callbacks

Bayarcash sends **two kinds** of notification. Always verify them with your API
secret key before trusting the data.

| Notification | How it arrives | Read it from |
|---|---|---|
| `callback_url` (transaction) | Server-to-server **POST** (form-encoded) | request form body |
| `return_url` (payer redirect) | Browser redirect — **POST** on v2, **GET** query on v3 | request form/query |

```python
callback_data = dict(request.form)  # framework-specific; a plain dict

# Transaction callback (sent to your callback_url)
if bayarcash.verify_transaction_callback_data(callback_data, secret_key):
    ...  # Data is authentic — safe to process.

# Payer redirect (sent to your return_url)
if bayarcash.verify_return_url_callback_data(callback_data, secret_key):
    ...

# Pre-transaction callback (sent before the transaction record)
if bayarcash.verify_pre_transaction_callback_data(callback_data, secret_key):
    ...
```

Each verifier returns `True` only when the checksum matches. See
[FPX Direct Debit](#fpx-direct-debit) for mandate-specific callback verifiers.
The verifiers can also be imported as standalone functions from
`bayarcash` if you prefer not to instantiate a client.

## Payment & Transaction Status

Transaction status is an integer code. Use the `Fpx` helper instead of
hardcoding numbers:

```python
from bayarcash import Fpx

Fpx.STATUS_NEW        # 0
Fpx.STATUS_PENDING    # 1
Fpx.STATUS_FAILED     # 2
Fpx.STATUS_SUCCESS    # 3
Fpx.STATUS_CANCELLED  # 4

if int(callback_data["status"]) == Fpx.STATUS_SUCCESS:
    ...  # Payment successful

print(Fpx.get_status_text(int(callback_data["status"])))  # e.g. "Successful"
```

`Dobw` (DuitNow) exposes the same status helpers: `from bayarcash import Dobw`.

## Transactions

```python
# Get a single transaction (v2 and v3)
transaction = bayarcash.get_transaction("transaction_id")
```

The following query helpers require **API v3** and raise `BayarcashError` on v2:

```python
bayarcash.set_api_version("v3")

result = bayarcash.get_all_transactions({
    "order_number": "INV-1001",
    "status": "3",
    "payment_channel": Bayarcash.FPX,
    "exchange_reference_number": "REF123",
    "payer_email": "ahmad@example.com",
})
# result["data"] => list[TransactionResource], result["meta"] => pagination meta

by_order   = bayarcash.get_transaction_by_order_number("INV-1001")
by_email   = bayarcash.get_transactions_by_payer_email("ahmad@example.com")
by_status  = bayarcash.get_transactions_by_status("3")
by_channel = bayarcash.get_transactions_by_payment_channel(Bayarcash.FPX)
by_ref     = bayarcash.get_transaction_by_reference_number("REF123")  # single or None

# Get a payment intent by id (v3 only)
intent = bayarcash.get_payment_intent("payment_intent_id")

# Cancel a payment intent (v3 only)
bayarcash.cancel_payment_intent("payment_intent_id")
```

## FPX Direct Debit

FPX Direct Debit lets you set up a recurring mandate and later maintain or
terminate it. Constants live on the `FpxDirectDebit` class:

```python
from bayarcash import FpxDirectDebit

# Payer ID type
FpxDirectDebit.NRIC                   # 1 (New IC)
FpxDirectDebit.OLD_IC                 # 2
FpxDirectDebit.PASSPORT               # 3
FpxDirectDebit.BUSINESS_REGISTRATION  # 4
FpxDirectDebit.OTHERS                 # 5

# Frequency mode
FpxDirectDebit.MODE_DAILY    # "DL"
FpxDirectDebit.MODE_WEEKLY   # "WK"
FpxDirectDebit.MODE_MONTHLY  # "MT"
FpxDirectDebit.MODE_YEARLY   # "YR"
```

### 1. Enrolment

```python
data = {
    "portal_key": "your_portal_key",
    "order_number": "DD-1001",
    "amount": "10.00",  # range 5.00–30000.00
    "payer_name": "Ahmad bin Abdullah",
    "payer_id_type": FpxDirectDebit.NRIC,
    "payer_id": "900101011234",
    "payer_email": "ahmad@example.com",  # max 27 chars
    "payer_telephone_number": "0123456789",
    "application_reason": "Monthly subscription",
    "frequency_mode": FpxDirectDebit.MODE_MONTHLY,
    "effective_date": "2026-08-01",  # optional, YYYY-MM-DD
    "expiry_date": "2027-08-01",     # optional, YYYY-MM-DD
    "return_url": "https://your-site.com/mandate/return",
}

data["checksum"] = bayarcash.create_fpx_direct_debit_enrolment_checksum_value(secret_key, data)

mandate = bayarcash.create_fpx_direct_debit_enrollment(data)
# redirect the payer to mandate.url
```

### 2. Maintenance

Update an existing mandate (identified by its mandate id):

```python
data = {
    "amount": "15.00",
    "payer_email": "ahmad@example.com",
    "payer_telephone_number": "0123456789",
    "application_reason": "Update amount",
    "frequency_mode": FpxDirectDebit.MODE_MONTHLY,
}

data["checksum"] = bayarcash.create_fpx_direct_debit_maintenance_checksum_value(secret_key, data)

mandate = bayarcash.create_fpx_direct_debit_maintenance(mandate_id, data)
# redirect the payer to mandate.url
```

### 3. Termination

```python
mandate = bayarcash.create_fpx_direct_debit_termination(mandate_id, {
    "application_reason": "Customer cancelled",
})
# redirect the payer to mandate.url
```

### Retrieving mandates & verifying mandate callbacks

```python
mandate     = bayarcash.get_fpx_direct_debit(mandate_id)
transaction = bayarcash.get_fpx_direct_debit_transaction(transaction_id)

# Mandate callback verifiers
bayarcash.verify_direct_debit_bank_approval_callback_data(callback_data, secret_key)
bayarcash.verify_direct_debit_authorization_callback_data(callback_data, secret_key)
bayarcash.verify_direct_debit_transaction_callback_data(callback_data, secret_key)
```

## Manual Bank Transfer

Submit a manual (offline) bank transfer with proof of payment:

```python
response = bayarcash.create_manual_bank_transfer({
    "portal_key": "your_portal_key",
    "payment_gateway": Bayarcash.MANUAL_TRANSFER,  # must be 2
    "order_no": "MT-1001",
    "buyer_name": "Ahmad bin Abdullah",
    "buyer_email": "ahmad@example.com",
    "buyer_tel_no": "0123456789",  # optional
    "order_amount": "10.00",
    "merchant_bank_name": "Maybank",
    "merchant_bank_account": "1234567890",
    "merchant_bank_account_holder": "Your Company Sdn Bhd",
    "bank_transfer_type": "Internet Banking",  # or "Cash Deposit Machine (CDM)"
    "bank_transfer_notes": "Payment for order MT-1001",
    "bank_transfer_date": "2026-07-22",  # optional, defaults to today
    "proof_of_payment": "/path/to/receipt.jpg",  # jpeg/png/gif/pdf, max 10 MB
})
```

Update the status of an existing transfer:

```python
from bayarcash import Fpx

bayarcash.update_manual_bank_transfer_status(
    "ref_no_here",
    str(Fpx.STATUS_SUCCESS),
    "10.00",
)
```

## Portals & FPX Banks

```python
# All portals for your account
portals = bayarcash.get_portals()

# Payment channels available for a portal
channels = bayarcash.get_channels("your_portal_key")

# FPX banks (for building a bank selector)
banks = bayarcash.fpx_banks_list()
```

## Error Handling

Failed API calls raise typed exceptions. Catch them to handle errors gracefully:

```python
from bayarcash import (
    ValidationError,
    FailedActionError,
    NotFoundError,
    RateLimitError,
    APIError,
    BayarcashError,
)

try:
    payment_intent = bayarcash.create_payment_intent(data)
except ValidationError as e:
    errors = e.errors        # 422 — invalid request data
except NotFoundError:
    ...                      # 404 — resource not found
except RateLimitError as e:
    reset_at = e.rate_limit_resets_at  # 429 — unix timestamp or None
except FailedActionError as e:
    message = str(e)         # 400 — request failed
except APIError as e:
    status = e.status_code   # any other non-2xx (e.g. 500)
```

Every exception subclasses `BayarcashError`, so `except BayarcashError` catches
them all.

| Exception | HTTP | Meaning |
|---|---|---|
| `ValidationError` | 422 | Invalid data. Read `.errors` for details. |
| `FailedActionError` | 400 | Request failed. `str(e)` has the reason. |
| `NotFoundError` | 404 | Resource not found. |
| `RateLimitError` | 429 | Rate limited. `.rate_limit_resets_at` holds the reset time. |
| `APIError` | other | Any other non-2xx status. `.status_code` holds the code. |

## Response Objects

API methods return typed resource objects with snake_case attributes. Any
missing field reads back as `None`.

**`PaymentIntentResource`** (from `create_payment_intent` / `get_payment_intent`)

```python
payment_intent.url           # checkout URL to redirect the payer to
payment_intent.id
payment_intent.status
payment_intent.amount
payment_intent.order_number
payment_intent.payer_name
payment_intent.payer_email
```

**`TransactionResource`** (from `get_transaction` / transaction queries)

```python
transaction.id
transaction.status                     # int status code — see Fpx constants
transaction.status_description
transaction.amount
transaction.order_number
transaction.exchange_reference_number
transaction.payer_name
transaction.payer_email
```

Convert a resource (including nested resources) to a dict:

```python
transaction.to_dict()
```

## Security Recommendations

1. Always send a `checksum` with payment and mandate requests.
2. Verify **every** callback with the provided verification methods before
   acting on it.
3. Store and check transaction ids to prevent duplicate processing.
4. Use HTTPS for your `return_url` and `callback_url`.
5. Keep your API token and secret key out of source control.

## API Documentation

For full API details, see the
[Official Bayarcash API Documentation](https://api.webimpian.support/bayarcash).

## Support

For support questions, contact Bayarcash support or open an issue in this
repository.

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for the version history.

## License

Open-sourced software licensed under the [MIT license](LICENSE).
