Metadata-Version: 2.4
Name: python-payway
Version: 0.0.11
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)
```

## Renewing the secret API key

Secret API keys expire one year after they are created. PayWay generates the replacement
40 days before that, so an application that asks for the latest key once a day and stores
what it gets back rolls onto the new key without an administrator creating one in the
PayWay website.

```python
api_key, errors = client.get_latest_api_key()
if api_key and api_key.key != stored_secret_api_key:
    # Persist api_key.key. Log api_key.key_name - it is masked; the key itself is a password.
    save_secret_api_key(api_key.key)
```

Usually the key returned is the one that authenticated the call. Renewal chains off the
live key, so if the stored key is left to expire the call fails with a `PaywayError` and
recovery means minting a key by hand — poll daily and alert on repeated failures.

To test your renewal code, create two secret API keys in the PayWay website, configure the
first, and confirm the application switches to the second on its own.

## 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
```
