Skip to content

Commit 8954459

Browse files
authored
PTHMINT-118: POS receipt and order cancel features (#57)
POS and order cancellation functionality is introduced by adding a PosManager (with get_receipt) and a cancel_transaction method in OrderManager, along with terminal-group scoped authentication (AuthScope/ScopedCredentialResolver), proper encoding of order_id path segments, and new request/response models for cancellations and enriched receipt structures (merchant, order, items, tips, payments, and related transactions). The SDK is updated to integrate PosManager and includes usage examples plus unit and integration tests covering order creation, cancellation, receipt retrieval, and existing order flows. Response models are aligned with POS schemas by removing unsupported fields in CancelTransaction, renaming amount_refunded to amountrefunded, and simplifying response construction and tests accordingly. Backward compatibility is ensured by adding a read-only amount_refunded alias on ReceiptOrder that maps to amountrefunded and by updating from_dict to accept both formats, with tests verifying the new behavior and parsing. The Cloud POS example is also simplified to print the full order object instead of conditional output.
1 parent f643ae0 commit 8954459

29 files changed

Lines changed: 1594 additions & 1 deletion

examples/order_manager/cancel.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright (c) MultiSafepay, Inc. All rights reserved.
2+
3+
# This file is licensed under the Open Software License (OSL) version 3.0.
4+
# For a copy of the license, see the LICENSE.txt file in the project root.
5+
6+
# See the DISCLAIMER.md file for disclaimer details.
7+
8+
"""Create a Cloud POS order, wait 5 seconds, and cancel it."""
9+
10+
import os
11+
import time
12+
13+
from dotenv import load_dotenv
14+
15+
from multisafepay import Sdk
16+
from multisafepay.api.paths.orders.request import OrderRequest
17+
from multisafepay.client import ScopedCredentialResolver
18+
19+
load_dotenv()
20+
21+
DEFAULT_ACCOUNT_API_KEY = os.getenv("API_KEY", "")
22+
TERMINAL_GROUP_DEFAULT_API_KEY = os.getenv("TERMINAL_GROUP_API_KEY_GROUP_DEFAULT", "")
23+
CLOUD_POS_TERMINAL_GROUP_ID = os.getenv("CLOUD_POS_TERMINAL_GROUP_ID", "Default")
24+
TERMINAL_ID = os.getenv("CLOUD_POS_TERMINAL_ID", "")
25+
26+
if __name__ == "__main__":
27+
credential_resolver = ScopedCredentialResolver(
28+
default_api_key=DEFAULT_ACCOUNT_API_KEY,
29+
terminal_group_api_keys={
30+
CLOUD_POS_TERMINAL_GROUP_ID: TERMINAL_GROUP_DEFAULT_API_KEY,
31+
},
32+
)
33+
34+
multisafepay_sdk = Sdk(
35+
is_production=False,
36+
credential_resolver=credential_resolver,
37+
)
38+
order_manager = multisafepay_sdk.get_order_manager()
39+
40+
order_request = (
41+
OrderRequest()
42+
.add_type("redirect")
43+
.add_order_id(f"cloud-pos-cancel-{int(time.time())}")
44+
.add_description("Cloud POS cancel order")
45+
.add_amount(100)
46+
.add_currency("EUR")
47+
.add_gateway_info({"terminal_id": TERMINAL_ID})
48+
)
49+
50+
create_response = order_manager.create(
51+
order_request,
52+
terminal_group_id=CLOUD_POS_TERMINAL_GROUP_ID,
53+
)
54+
order = create_response.get_data()
55+
order_id = order.order_id
56+
57+
print(f"Created Cloud POS order: {order_id}")
58+
print("Waiting 5 seconds before cancel...")
59+
time.sleep(5)
60+
61+
cancel_response = order_manager.cancel_transaction(
62+
order_id,
63+
terminal_group_id=CLOUD_POS_TERMINAL_GROUP_ID,
64+
)
65+
66+
print(f"Canceled Cloud POS order: {order_id}")
67+
print(cancel_response.get_data())
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright (c) MultiSafepay, Inc. All rights reserved.
2+
3+
# This file is licensed under the Open Software License (OSL) version 3.0.
4+
# For a copy of the license, see the LICENSE.txt file in the project root.
5+
6+
# See the DISCLAIMER.md file for disclaimer details.
7+
8+
"""Create a Cloud POS order using terminal-group scoped authentication."""
9+
10+
import os
11+
import time
12+
13+
from dotenv import load_dotenv
14+
15+
from multisafepay import Sdk
16+
from multisafepay.api.paths.orders.request import OrderRequest
17+
from multisafepay.client import ScopedCredentialResolver
18+
19+
load_dotenv()
20+
21+
DEFAULT_ACCOUNT_API_KEY = os.getenv("API_KEY", "")
22+
TERMINAL_GROUP_DEFAULT_API_KEY = os.getenv("TERMINAL_GROUP_API_KEY_GROUP_DEFAULT", "")
23+
CLOUD_POS_TERMINAL_GROUP_ID = os.getenv("CLOUD_POS_TERMINAL_GROUP_ID", "Default")
24+
TERMINAL_ID = os.getenv("CLOUD_POS_TERMINAL_ID", "")
25+
26+
if __name__ == "__main__":
27+
credential_resolver = ScopedCredentialResolver(
28+
default_api_key=DEFAULT_ACCOUNT_API_KEY,
29+
terminal_group_api_keys={
30+
CLOUD_POS_TERMINAL_GROUP_ID: TERMINAL_GROUP_DEFAULT_API_KEY,
31+
},
32+
)
33+
34+
multisafepay_sdk = Sdk(
35+
is_production=False,
36+
credential_resolver=credential_resolver,
37+
)
38+
order_manager = multisafepay_sdk.get_order_manager()
39+
40+
order_request = (
41+
OrderRequest()
42+
.add_type("redirect")
43+
.add_order_id(f"cloud-pos-{int(time.time())}")
44+
.add_description("Cloud POS order")
45+
.add_amount(100)
46+
.add_currency("EUR")
47+
.add_gateway_info({"terminal_id": TERMINAL_ID})
48+
)
49+
50+
create_response = order_manager.create(
51+
order_request,
52+
terminal_group_id=CLOUD_POS_TERMINAL_GROUP_ID,
53+
)
54+
order = create_response.get_data()
55+
56+
print(f"Created Cloud POS order: {order.order_id}")
57+
print(order)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright (c) MultiSafepay, Inc. All rights reserved.
2+
3+
# This file is licensed under the Open Software License (OSL) version 3.0.
4+
# For a copy of the license, see the LICENSE.txt file in the project root.
5+
6+
# See the DISCLAIMER.md file for disclaimer details.
7+
8+
"""Fetch the receipt for an existing Cloud POS order."""
9+
10+
import os
11+
12+
from dotenv import load_dotenv
13+
14+
from multisafepay import Sdk
15+
from multisafepay.client import ScopedCredentialResolver
16+
17+
load_dotenv()
18+
19+
DEFAULT_ACCOUNT_API_KEY = os.getenv("API_KEY", "")
20+
TERMINAL_GROUP_DEFAULT_API_KEY = os.getenv("TERMINAL_GROUP_API_KEY_GROUP_DEFAULT", "")
21+
CLOUD_POS_TERMINAL_GROUP_ID = os.getenv("CLOUD_POS_TERMINAL_GROUP_ID", "Default")
22+
ORDER_ID = os.getenv("CLOUD_POS_ORDER_ID", "")
23+
24+
if __name__ == "__main__":
25+
if not ORDER_ID:
26+
raise SystemExit(
27+
"Set CLOUD_POS_ORDER_ID to a completed Cloud POS order id "
28+
"(receipts are only available for completed orders).",
29+
)
30+
31+
credential_resolver = ScopedCredentialResolver(
32+
default_api_key=DEFAULT_ACCOUNT_API_KEY,
33+
terminal_group_api_keys={
34+
CLOUD_POS_TERMINAL_GROUP_ID: TERMINAL_GROUP_DEFAULT_API_KEY,
35+
},
36+
)
37+
multisafepay_sdk = Sdk(
38+
is_production=False,
39+
credential_resolver=credential_resolver,
40+
)
41+
pos_manager = multisafepay_sdk.get_pos_manager()
42+
43+
receipt_response = pos_manager.get_receipt(
44+
order_id=ORDER_ID,
45+
terminal_group_id=CLOUD_POS_TERMINAL_GROUP_ID,
46+
)
47+
print(receipt_response.get_data())
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copyright (c) MultiSafepay, Inc. All rights reserved.
2+
3+
# This file is licensed under the Open Software License (OSL) version 3.0.
4+
# For a copy of the license, see the LICENSE.txt file in the project root.
5+
6+
# See the DISCLAIMER.md file for disclaimer details.
7+
8+
"""Cancel operations and endpoints for specific orders."""
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright (c) MultiSafepay, Inc. All rights reserved.
2+
3+
# This file is licensed under the Open Software License (OSL) version 3.0.
4+
# For a copy of the license, see the LICENSE.txt file in the project root.
5+
6+
# See the DISCLAIMER.md file for disclaimer details.
7+
8+
"""Request models for order cancellation operations."""
9+
10+
from multisafepay.api.paths.orders.order_id.cancel.request.cancel_transaction_request import (
11+
CancelTransactionRequest,
12+
)
13+
14+
__all__ = [
15+
"CancelTransactionRequest",
16+
]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright (c) MultiSafepay, Inc. All rights reserved.
2+
3+
# This file is licensed under the Open Software License (OSL) version 3.0.
4+
# For a copy of the license, see the LICENSE.txt file in the project root.
5+
6+
# See the DISCLAIMER.md file for disclaimer details.
7+
8+
"""Request model for cancel transaction endpoint."""
9+
10+
from multisafepay.model.request_model import RequestModel
11+
12+
13+
class CancelTransactionRequest(RequestModel):
14+
"""
15+
Represents a request to cancel a POS transaction.
16+
17+
Attributes
18+
----------
19+
order_id (str): The order identifier used in the endpoint path.
20+
21+
"""
22+
23+
order_id: str
24+
25+
def add_order_id(
26+
self: "CancelTransactionRequest",
27+
order_id: str,
28+
) -> "CancelTransactionRequest":
29+
"""
30+
Adds order id to the cancellation request.
31+
32+
Parameters
33+
----------
34+
order_id (str): The order identifier.
35+
36+
Returns
37+
-------
38+
CancelTransactionRequest: The updated request instance.
39+
40+
"""
41+
self.order_id = order_id
42+
return self
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright (c) MultiSafepay, Inc. All rights reserved.
2+
3+
# This file is licensed under the Open Software License (OSL) version 3.0.
4+
# For a copy of the license, see the LICENSE.txt file in the project root.
5+
6+
# See the DISCLAIMER.md file for disclaimer details.
7+
8+
"""Response models for order cancellation outcomes."""
9+
10+
from multisafepay.api.paths.orders.order_id.cancel.response.cancel_transaction import (
11+
CancelTransaction,
12+
)
13+
14+
__all__ = [
15+
"CancelTransaction",
16+
]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright (c) MultiSafepay, Inc. All rights reserved.
2+
3+
# This file is licensed under the Open Software License (OSL) version 3.0.
4+
# For a copy of the license, see the LICENSE.txt file in the project root.
5+
6+
# See the DISCLAIMER.md file for disclaimer details.
7+
8+
"""Response model for order cancellation endpoint payload."""
9+
10+
from typing import Optional
11+
12+
from multisafepay.api.base.decorator import Decorator
13+
from multisafepay.api.paths.orders.response.components.payment_details import (
14+
PaymentDetails,
15+
)
16+
from multisafepay.api.shared.costs import Costs
17+
from multisafepay.api.shared.custom_info import CustomInfo
18+
from multisafepay.api.shared.payment_method import PaymentMethod
19+
from multisafepay.model.response_model import ResponseModel
20+
21+
22+
class CancelTransaction(ResponseModel):
23+
"""
24+
Represents the `data` payload returned by cancel order transaction.
25+
26+
Attributes
27+
----------
28+
costs (Optional[list[Costs]]): The costs of the order.
29+
created (Optional[str]): Creation timestamp.
30+
modified (Optional[str]): Last modification timestamp.
31+
custom_info (Optional[CustomInfo]): Additional custom info.
32+
fastcheckout (Optional[str]): Fastcheckout flag/status.
33+
financial_status (Optional[str]): Financial status.
34+
order_id (Optional[str]): Order identifier.
35+
payment_details (Optional[PaymentDetails]): Payment details.
36+
payment_methods (Optional[list[PaymentMethod]]): Payment methods.
37+
status (Optional[str]): Order status.
38+
39+
"""
40+
41+
costs: Optional[list[Costs]]
42+
created: Optional[str]
43+
modified: Optional[str]
44+
custom_info: Optional[CustomInfo]
45+
fastcheckout: Optional[str]
46+
financial_status: Optional[str]
47+
order_id: Optional[str]
48+
payment_details: Optional[PaymentDetails]
49+
payment_methods: Optional[list[PaymentMethod]]
50+
status: Optional[str]
51+
52+
@staticmethod
53+
def from_dict(d: dict) -> Optional["CancelTransaction"]:
54+
"""
55+
Create a CancelTransaction from dictionary data.
56+
57+
Parameters
58+
----------
59+
d (dict): The cancellation response data.
60+
61+
Returns
62+
-------
63+
Optional[CancelTransaction]: A cancellation response instance or None.
64+
65+
"""
66+
if d is None:
67+
return None
68+
cancel_dependency_adapter = Decorator(dependencies=d)
69+
dependencies = (
70+
cancel_dependency_adapter.adapt_costs(d.get("costs"))
71+
.adapt_custom_info(d.get("custom_info"))
72+
.adapt_payment_details(d.get("payment_details"))
73+
.adapt_payment_methods(d.get("payment_methods"))
74+
.get_dependencies()
75+
)
76+
return CancelTransaction(**dependencies)

0 commit comments

Comments
 (0)