Skip to content
Closed
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/
23 changes: 23 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
API_KEY=
PARTNER_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=

# Set CLOUD_POS_TERMINAL_GROUP_ID to skip automatic group lookup when needed.
# CLOUD_POS_TERMINAL_GROUP_ID=<terminal_group_id>
CLOUD_POS_TERMINAL_ID=<terminal_id>

# Optional custom base URL for terminal endpoint E2E.
MSP_SDK_BUILD_PROFILE=dev
MSP_SDK_ALLOW_CUSTOM_BASE_URL=1
MSP_SDK_CUSTOM_BASE_URL=

# 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=
61 changes: 59 additions & 2 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,8 +160,54 @@ 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`.

The e2e suite does not use the shared `API_KEY` variable or the shared `MSP_SDK_*`
custom base URL settings.
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.

### Strict feature-specific E2E credentials

Cloud POS 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="<non_sandbox_https_base_url>"
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>"
```

Terminal endpoint example E2E tests reuse the current custom environment
configuration instead of defining a second set of terminal-specific E2E
variables.

```bash
export API_KEY="<dev_api_key>"
export PARTNER_API_KEY="<dev_partner_affiliate_key>" # optional
export MSP_SDK_CUSTOM_BASE_URL="<custom_https_base_url>"
export E2E_CLOUD_POS_TERMINAL_ID="<terminal_id>"
# Optional: skip automatic group lookup for the terminal-group example.
export CLOUD_POS_TERMINAL_GROUP_ID="<terminal_group_id>"
```

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

Expand Down
89 changes: 89 additions & 0 deletions examples/event_manager/subscribe_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright (c) MultiSafepay, Inc. All rights reserved.

# This file is licensed under the Open Software License (OSL) version 3.0.
# For a copy of the license, see the LICENSE.txt file in the project root.

# See the DISCLAIMER.md file for disclaimer details.

"""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.")
89 changes: 89 additions & 0 deletions examples/order_manager/cancel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright (c) MultiSafepay, Inc. All rights reserved.

# This file is licensed under the Open Software License (OSL) version 3.0.
# For a copy of the license, see the LICENSE.txt file in the project root.

# See the DISCLAIMER.md file for disclaimer details.

"""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())
90 changes: 90 additions & 0 deletions examples/order_manager/cloud_pos_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Copyright (c) MultiSafepay, Inc. All rights reserved.

# This file is licensed under the Open Software License (OSL) version 3.0.
# For a copy of the license, see the LICENSE.txt file in the project root.

# See the DISCLAIMER.md file for disclaimer details.

"""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