Metadata-Version: 2.4
Name: python-payway
Version: 0.0.10
Summary: Python client for working with Westpac's PayWay REST API
Author-email: Ben Napper <reppan197@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/napper1/python-payway
Project-URL: Repository, https://github.com/napper1/python-payway
Project-URL: Issues, https://github.com/napper1/python-payway/issues
Keywords: payway,westpac,api,client
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.20.0
Provides-Extra: dev
Requires-Dist: pre-commit>=3.5.0; extra == "dev"
Requires-Dist: ruff==0.6.6; extra == "dev"
Requires-Dist: mypy>=0.971; extra == "dev"
Dynamic: license-file

# PayWay REST API - Python

- Store customers, their card and bank account in PayWay
- Take payment using a stored credit card or bank account
- Process and Capture a pre-authorisation
- Lookup or poll transactions
- Refund transactions
- Void transactions
- Update a customer's payment setup in PayWay

## Install

Requires Python 3.11 or later.

```bash
pip install python-payway
```

## Take payment using a stored credit card

Create a Client class with your PayWay API credentials

```python
from payway.client import Client

client = Client(merchant_id='<your_payway_merchant_id>',
                bank_account_id='<your_payway_bank_account_id>',
                publishable_api_key='<your_payway_publishable_api_key>',
                secret_api_key='<your_payway_secret_api_key>')
```

Create a PayWayCustomer class with your customer's details

```python
customer = PayWayCustomer(custom_id='c981a',
                          customer_name='John Smith',
                          email_address='johnsmith@example.com',
                          send_email_receipts=False,  # not available in sandbox
                          phone_number='0343232323',
                          street='1 Test Street',
                          street2='2 Test Street',
                          city_name='Sydney',
                          state='NSW',
                          postal_code='2000')
```

Create a PayWayCard class with your customer's card details

```python
card = PayWayCard(card_number='',
                  cvn='',
                  card_holder_name='',
                  expiry_date_month='',
                  expiry_date_year='')
```

Create a token from your card and create a customer in PayWay

```python
token_response, errors = client.create_card_token(card)
token = token_response.token        
customer.token = token
payway_customer, customer_errors = client.create_customer(customer)
```

Note the 'payway_customer' object contains the full customer response fields from PayWay.

Create a Payment class with the payment details and process the transaction

```python
customer_number = payway_customer.customer_number
payment = PayWayPayment(customer_number=customer_number,
                        transaction_type='payment',
                        amount='',
                        currency='aud',
                        order_number='',
                        ip_address='')
transaction, errors = client.process_payment(payment)
```

Check the `transaction` for the result

```python
if not errors and transaction.status == 'approved':
    # process successful response
```

## Take payment using a credit card token only

```python
client = Client(merchant_id='',
                bank_account_id='',
                publishable_api_key='',
                secret_api_key='')
card = PayWayCard(card_number='',
                  cvn='',
                  card_holder_name='',
                  expiry_date_month='',
                  expiry_date_year='')
token_response, errors = client.create_card_token(card)
# your customer reference number or a stored PayWay customer number
customer_number = ''    
payment = PayWayPayment(customer_number=customer_number,
                        transaction_type='payment',
                        amount='',
                        currency='aud',
                        order_number='',
                        ip_address='',
                        token=token_response.token,
                        merchant_id=client.merchant_id)
transaction, errors = client.process_payment(payment)
```

## Retries

Retries are off by default. Opt in with `max_retries` (and optionally `retry_delay`, the base
wait in seconds between attempts):

```python
client = Client(merchant_id='<your_payway_merchant_id>',
                bank_account_id='<your_payway_bank_account_id>',
                publishable_api_key='<your_payway_publishable_api_key>',
                secret_api_key='<your_payway_secret_api_key>',
                max_retries=2,
                retry_delay=1.0)
```

This follows PayWay's retry guidance (<https://www.payway.com.au/docs/rest.html#network-errors>):

- Requests are resent on connection errors, timeouts and HTTP 429/503 responses, waiting
  `retry_delay` seconds between attempts (linear backoff, or the response's `Retry-After`
  header when present). PayWay suggests a 20 second wait; keep `retry_delay` small for
  synchronous checkout flows.
- POSTs are only retried when an `idempotency_key` was supplied — the same
  `Idempotency-Key` is resent so PayWay replays the original response instead of
  processing a duplicate payment. POSTs without a key (and PUTs) are never retried.
  GETs are always safe to retry.
- Other errors (including HTTP 500/502/504) are never retried, per PayWay's advice.

```python
transaction, errors = client.process_payment(payment, idempotency_key=str(uuid.uuid4()))
```

## Handling errors

Documented errors (such as 422 Unprocessable entity) are parsed into an PaymentError class that you can use in an customer error message.
For more info, visit <https://www.payway.com.au/docs/rest.html#http-response-codes>

```python
if errors:
    for error in errors: 
        print(error.field_name)
        print(error.message) 
        print(error.field_name)
    # or use a method
    PaymentError().list_to_message(errors) 
```

## Direct Debit

Direct debit transactions are possible by creating a token from a bank account:

```python
bank_account = BankAccount(account_name='Test', bsb='000-000', account_number=123456)
token_response, errors = client.create_bank_account_token(bank_account)
token = token_response.token
```

Store the token with a customer in PayWay using the same process as the card outlined above.

Note: direct debit transactions take days to process so they must be polled regularly for the latest transaction status from the customer's bank.

## Lookup transaction

Poll a transaction using the `get_transaction` method.

```python
transaction, errors = client.get_transaction(transaction.transaction_id)
```

## Search transactions

PayWay has no plain `GET /transactions` resource, only three search paths. Each returns a
paginated list (20 per page, most recent first) with `next`/`prev` links.

```python
response = client.search_transactions_by_customer(customer_number)
response = client.search_transactions_by_receipt(receipt_number)
response = client.search_transactions_by_order(order_number)

transactions = response["data"]
```

Pass `page` to fetch a later page, using the number from the `next`/`prev` links:

```python
response = client.search_transactions_by_customer(customer_number, page=2)
```

`list_customers()` takes the same `page` argument.

## Process and capture a pre-authorisation

To process a credit card pre-authorisation using a credit card stored against a customer use `preAuth` as the `transaction_type` along with the customer's PayWay number, amount and currency.

```python
pre_auth_payment = PayWayPayment(customer_number='',
                                 transaction_type='preAuth',
                                 amount='',
                                 currency='aud',
                                 order_number='',
                                 ip_address='')
transaction, errors = client.process_payment(pre_auth_payment)
```

To capture the pre-authorisation supply a pre-authorisation transaction ID,  `capture` as the `transaction_type` along with an amount to capture.

```python
capture_payment = PayWayPayment(transaction_type='capture',
                                parent_transaction_id='',
                                amount='',
                                order_number='',
                                ip_address='')
transaction, errors = client.process_payment(capture_payment)
```

## Refunds

Refund a transaction by supplying a PayWay transaction ID and the refund amount.

```python
refund, errors = client.refund_transaction(
    transaction_id=transaction.transaction_id,
    amount=transaction.principal_amount,
)
```

## Voiding a transaction

Void a transaction by supplying a PayWay transaction ID.

```python
void_transaction, errors = client.void_transaction(transaction.transaction_id)
```

## Update Payment Setup

Update a customer's payment setup with a new credit card or bank account in PayWay. Supply the new token and an existing PayWay customer number.

```python
payment_setup, errors = client.update_payment_setup(new_token, payway_customer.customer_number)
```

## Additional notes

PayWay API documentation <https://www.payway.com.au/docs/rest.html>

It is recommended to use PayWay's Trusted Frame <https://www.payway.com.au/docs/rest.html#trusted-frame>
when creating a single use token of a card or bank account so your PCI-compliance scope is reduced.  

## Keeping the raw response

Models parsed from a PayWay response keep that response verbatim on `raw`:

```python
transaction, errors = client.process_payment(payment)
transaction.raw  # exactly what PayWay returned
```

Parsing is lossy — keys PayWay sends that the dataclass does not declare are dropped,
absent keys become `None`, and a few are renamed (`maskedCardNumber` is parsed into
`card_number`). Store `raw` rather than `to_dict()` if you are persisting responses for
auditing, reconciliation or dispute resolution. Models you construct yourself, such as a
`PayWayPayment` you are about to send, leave `raw` as `None`.

## Fraud

Please follow PayWay's advice about reducing your risk of fraudulent transactions. <https://www.payway.com.au/docs/card-testing.html#card-testing>

## Running the project

```bash
uv sync
```

uv provisions a suitable Python (3.11+) and installs the dev dependencies automatically.

## Testing

```bash
uv run pytest tests/ -v
```
