Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ source =
tests
omit =
env/
examples/
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
API_KEY=
E2E_API_KEY=
E2E_BASE_URL=https://testapi.multisafepay.com/v1/
# Temporary override for strict feature examples not yet supported on testapi
E2E_NO_SANDBOX_BASE_URL=
PARTNER_API_KEY=
CLOUD_POS_TERMINAL_GROUP_ID=<terminal_group_id>

# Dedicated env vars for strict feature-specific E2E tests (temporary split)
E2E_PARTNER_API_KEY=
E2E_CLOUD_POS_TERMINAL_ID=<terminal_id>
E2E_TERMINAL_GROUP_API_KEY_GROUP_DEFAULT=

# Terminal group API keys — one entry per group_id
TERMINAL_GROUP_API_KEY_GROUP_DEFAULT=
# TERMINAL_GROUP_API_KEY_GROUP_A=
# TERMINAL_GROUP_API_KEY_GROUP_B=
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ In any non-dev profile (including default `release`), custom base URLs are block

Go to the folder `examples` to see how to use the SDK.

### Scoped credential behavior in examples

Some examples support multiple credential scopes through `ScopedCredentialResolver`.

- `ScopedCredentialResolver` supports any key combination as long as at least one credential is configured.
- Cloud POS order lifecycle examples execute manager calls with terminal-group scope and therefore require `TERMINAL_GROUP_API_KEY_GROUP_DEFAULT`.
- Those examples still read `API_KEY` and `PARTNER_API_KEY` and pass them to the resolver when available, but they are not validated as required for those specific flows.
- `terminal_manager/create.py` calls `create_terminal` with default scope and therefore requires `API_KEY`.
- `terminal_manager/get_terminals.py` and `terminal_group_manager/get_terminals_by_group.py` call partner-affiliate scope and require either `PARTNER_API_KEY` or `API_KEY`.
- `PARTNER_API_KEY` is optional in those examples and must be passed via `partner_affiliate_api_key`; it does not replace `default_api_key`.

## Code quality checks

### Linting
Expand Down Expand Up @@ -149,9 +160,45 @@ make test-e2e
`E2E_BASE_URL` is optional and can point to any HTTPS base URL used for E2E.
When omitted, E2E defaults to `testapi.multisafepay.com`.

For strict feature-specific examples (Cloud POS and terminal endpoints), you can
set a separate URL with `E2E_NO_SANDBOX_BASE_URL`. If omitted, those tests reuse
`E2E_BASE_URL`.

This split is a temporary workaround: some feature-specific flows are not yet
supported on the default `testapi` environment.

The e2e suite does not use the shared `API_KEY` variable or the shared `MSP_SDK_*`
custom base URL settings.

### Strict feature-specific E2E credentials

Cloud POS and terminal example E2E tests use a strict, isolated credential set. If
any required variable is missing, those tests fail fast during setup.

These tests are intentionally isolated because they currently depend on
features that may not be available in `testapi` yet.

`terminal_group_id` is resolved automatically from `/json/terminals` using
`E2E_CLOUD_POS_TERMINAL_ID`.

```bash
export E2E_API_KEY="m_<merchant_key>"
export E2E_NO_SANDBOX_BASE_URL="https://api.dev.multisafepay.com/v1/"
export E2E_PARTNER_API_KEY="<partner_affiliate_key>"
export E2E_CLOUD_POS_TERMINAL_ID="<terminal_id>"
export E2E_TERMINAL_GROUP_API_KEY_GROUP_DEFAULT="<terminal_group_api_key>"
```

You can run only the strict feature-specific examples with:

```bash
poetry run pytest \
tests/multisafepay/e2e/examples/terminal_manager/test_get_terminals.py \
tests/multisafepay/e2e/examples/terminal_group_manager/test_get_terminals_by_group.py \
tests/multisafepay/e2e/examples/order_manager/test_cloud_pos_order.py \
tests/multisafepay/e2e/examples/order_manager/test_cancel.py -q
```

## Support

Create an issue on this repository or email <a href="mailto:integration@multisafepay.com">
Expand Down
82 changes: 82 additions & 0 deletions examples/event_manager/subscribe_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Create a Cloud POS order and subscribe to its event stream."""

import os
import time

from dotenv import load_dotenv
from multisafepay import Sdk
from multisafepay.api.paths.orders.request import OrderRequest
from multisafepay.client import ScopedCredentialResolver
Comment thread
zulquer marked this conversation as resolved.

# Load environment variables from a .env file
load_dotenv()

DEFAULT_ACCOUNT_API_KEY = (os.getenv("API_KEY") or "").strip()
PARTNER_AFFILIATE_API_KEY = (os.getenv("PARTNER_API_KEY") or "").strip()
TERMINAL_GROUP_DEFAULT_API_KEY = (
os.getenv("TERMINAL_GROUP_API_KEY_GROUP_DEFAULT") or ""
).strip()
CLOUD_POS_TERMINAL_GROUP_ID = os.getenv(
"CLOUD_POS_TERMINAL_GROUP_ID",
"Default",
)

if __name__ == "__main__":
# This example executes Cloud POS calls with terminal-group scope.
scoped_terminal_group_id = CLOUD_POS_TERMINAL_GROUP_ID
resolver_kwargs = {
"default_api_key": DEFAULT_ACCOUNT_API_KEY,
"partner_affiliate_api_key": PARTNER_AFFILIATE_API_KEY,
}
if scoped_terminal_group_id:
resolver_kwargs["terminal_group_api_keys"] = {
scoped_terminal_group_id: TERMINAL_GROUP_DEFAULT_API_KEY,
}

credential_resolver = ScopedCredentialResolver(**resolver_kwargs)

multisafepay_sdk = Sdk(
is_production=False,
credential_resolver=credential_resolver,
)
order_manager = multisafepay_sdk.get_order_manager()
event_manager = multisafepay_sdk.get_event_manager()

terminal_id = "<terminal_id>"
# Temporary override for local runs via .env.
terminal_id = os.getenv("CLOUD_POS_TERMINAL_ID", terminal_id)

order_id = f"cloud-pos-{int(time.time())}"

order_request = (
OrderRequest()
.add_type("redirect")
.add_order_id(order_id)
.add_description("Cloud POS order")
.add_amount(100)
.add_currency("EUR")
.add_gateway_info(
{
"terminal_id": terminal_id,
},
Comment thread
zulquer marked this conversation as resolved.
)
Comment thread
zulquer marked this conversation as resolved.
)

create_response = order_manager.create(
order_request,
terminal_group_id=scoped_terminal_group_id,
)
order = create_response.get_data()

if order is None:
raise RuntimeError("Order creation did not return order data")

print(f"Created Cloud POS order: {order.order_id}")
print("Listening for events. Press Ctrl+C to stop.")

try:
with event_manager.subscribe_order_events(order, timeout=45.0) as stream:
for event in stream:
print(event)
except KeyboardInterrupt:
print("Stream interrupted by user.")
82 changes: 82 additions & 0 deletions examples/order_manager/cancel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Create a Cloud POS order, wait 5 seconds, and cancel it."""

import os
import time

from dotenv import load_dotenv

from multisafepay import Sdk
from multisafepay.api.paths.orders.request import OrderRequest
from multisafepay.client import ScopedCredentialResolver
Comment thread
zulquer marked this conversation as resolved.

# Load environment variables from a .env file
load_dotenv()

DEFAULT_ACCOUNT_API_KEY = (os.getenv("API_KEY") or "").strip()
PARTNER_AFFILIATE_API_KEY = (os.getenv("PARTNER_API_KEY") or "").strip()
TERMINAL_GROUP_DEFAULT_API_KEY = (
os.getenv("TERMINAL_GROUP_API_KEY_GROUP_DEFAULT") or ""
).strip()
CLOUD_POS_TERMINAL_GROUP_ID = os.getenv(
"CLOUD_POS_TERMINAL_GROUP_ID",
"Default",
)

if __name__ == "__main__":
# This example executes Cloud POS calls with terminal-group scope.
scoped_terminal_group_id = CLOUD_POS_TERMINAL_GROUP_ID
resolver_kwargs = {
"default_api_key": DEFAULT_ACCOUNT_API_KEY,
"partner_affiliate_api_key": PARTNER_AFFILIATE_API_KEY,
}
if scoped_terminal_group_id:
resolver_kwargs["terminal_group_api_keys"] = {
scoped_terminal_group_id: TERMINAL_GROUP_DEFAULT_API_KEY,
}

credential_resolver = ScopedCredentialResolver(**resolver_kwargs)

multisafepay_sdk = Sdk(
is_production=False,
credential_resolver=credential_resolver,
)
order_manager = multisafepay_sdk.get_order_manager()

# Temporary override for local runs; comment this line to force literal placeholder.
terminal_id = os.getenv("CLOUD_POS_TERMINAL_ID", "<terminal_id>")

order_request = (
OrderRequest()
.add_type("redirect")
.add_order_id(f"cloud-pos-cancel-{int(time.time())}")
.add_description("Cloud POS cancel order")
.add_amount(100)
.add_currency("EUR")
.add_gateway_info(
{
"terminal_id": terminal_id,
},
)
)

create_response = order_manager.create(
order_request,
terminal_group_id=scoped_terminal_group_id,
)
order = create_response.get_data()

if order is None or not order.order_id:
raise RuntimeError("Order creation did not return order_id")

order_id = order.order_id
print(f"Created Cloud POS order: {order_id}")
print("Waiting 5 seconds before cancel...")
time.sleep(5)

cancel_response = order_manager.cancel_transaction(
order_id,
terminal_group_id=scoped_terminal_group_id,
)

print(f"Canceled Cloud POS order: {order_id}")
print(cancel_response.get_data())
83 changes: 83 additions & 0 deletions examples/order_manager/cloud_pos_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Create a Cloud POS order and print its event stream credentials."""

import os
import time

from dotenv import load_dotenv
from multisafepay import Sdk
from multisafepay.api.paths.orders.request import OrderRequest
from multisafepay.client import ScopedCredentialResolver
Comment thread
zulquer marked this conversation as resolved.

# Load environment variables from a .env file
load_dotenv()

DEFAULT_ACCOUNT_API_KEY = (os.getenv("API_KEY") or "").strip()
PARTNER_AFFILIATE_API_KEY = (os.getenv("PARTNER_API_KEY") or "").strip()
TERMINAL_GROUP_DEFAULT_API_KEY = (
os.getenv("TERMINAL_GROUP_API_KEY_GROUP_DEFAULT") or ""
).strip()
CLOUD_POS_TERMINAL_GROUP_ID = os.getenv(
"CLOUD_POS_TERMINAL_GROUP_ID",
"Default",
)

if __name__ == "__main__":
# This example executes Cloud POS calls with terminal-group scope.
scoped_terminal_group_id = CLOUD_POS_TERMINAL_GROUP_ID
# Reuse one SDK for mixed traffic. The resolver is the source of truth for
# which key is used per endpoint/scope.
resolver_kwargs = {
"default_api_key": DEFAULT_ACCOUNT_API_KEY,
"partner_affiliate_api_key": PARTNER_AFFILIATE_API_KEY,
}
if scoped_terminal_group_id:
resolver_kwargs["terminal_group_api_keys"] = {
scoped_terminal_group_id: TERMINAL_GROUP_DEFAULT_API_KEY,
}

credential_resolver = ScopedCredentialResolver(**resolver_kwargs)

multisafepay_sdk = Sdk(
is_production=False,
credential_resolver=credential_resolver,
)
order_manager = multisafepay_sdk.get_order_manager()

terminal_id = "<terminal_id>"
# Uncomment this line to override with CLOUD_POS_TERMINAL_ID from .env.
# terminal_id = os.getenv("CLOUD_POS_TERMINAL_ID", terminal_id)

order_id = f"cloud-pos-{int(time.time())}"

order_request = (
OrderRequest()
.add_type("redirect")
.add_order_id(order_id)
.add_description("Cloud POS order")
.add_amount(100)
.add_currency("EUR")
.add_gateway_info(
{
"terminal_id": terminal_id,
},
)
)

create_response = order_manager.create(
order_request,
terminal_group_id=scoped_terminal_group_id,
)
order = create_response.get_data()

if order is None:
raise RuntimeError("Order creation did not return order data")

print(f"Created Cloud POS order: {order.order_id}")

events_token = order.events_token or order.event_token
events_stream_url = order.events_stream_url or order.event_stream_url

if events_token and events_stream_url:
print("Event stream credentials:")
print(f"EVENTS_TOKEN={events_token}")
print(f"EVENTS_STREAM_URL={events_stream_url}")
Loading
Loading