pofsis-pay 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: pofsis-pay
3
+ Version: 0.1.0
4
+ Summary: Internal POFSIS client for IAM-authenticated POFPay integrations
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: Programming Language :: Python :: 3.12
7
+ Classifier: Operating System :: OS Independent
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: boto3<2,>=1.35
11
+ Requires-Dist: httpx<1,>=0.28
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest<10,>=8; extra == "dev"
14
+
15
+ # pofsis-pay
16
+
17
+ Private Python SDK for IAM-authenticated POFPay machine requests and signed callback validation.
18
+
19
+ ```python
20
+ from pofsis_pofpay import POFPayClient
21
+
22
+ client = POFPayClient(
23
+ api_base_url="https://pofpay.example.com",
24
+ region="ap-southeast-1",
25
+ )
26
+ checkout = client.create_payment_request(snapshot, "tool:order-id")
27
+ ```
28
+
29
+ Lambda uses the standard AWS credential chain, so no access key is stored in application
30
+ configuration. Install the same pinned version locally and in the published Python 3.12 Layer.
31
+
32
+ `buildspec-publish.yml` tests, builds, validates, and publishes an immutable package version to
33
+ PyPI using a protected `PYPI_API_TOKEN`. `buildspec-layer.yml` installs that exact public version
34
+ from `https://pypi.org/simple` and publishes the shared Lambda Layer. Runtime Lambdas need no PyPI
35
+ credentials or package-registry access.
36
+
37
+ The PyPI distribution is `pofsis-pay`; the Python import remains `pofsis_pofpay`.
@@ -0,0 +1,23 @@
1
+ # pofsis-pay
2
+
3
+ Private Python SDK for IAM-authenticated POFPay machine requests and signed callback validation.
4
+
5
+ ```python
6
+ from pofsis_pofpay import POFPayClient
7
+
8
+ client = POFPayClient(
9
+ api_base_url="https://pofpay.example.com",
10
+ region="ap-southeast-1",
11
+ )
12
+ checkout = client.create_payment_request(snapshot, "tool:order-id")
13
+ ```
14
+
15
+ Lambda uses the standard AWS credential chain, so no access key is stored in application
16
+ configuration. Install the same pinned version locally and in the published Python 3.12 Layer.
17
+
18
+ `buildspec-publish.yml` tests, builds, validates, and publishes an immutable package version to
19
+ PyPI using a protected `PYPI_API_TOKEN`. `buildspec-layer.yml` installs that exact public version
20
+ from `https://pypi.org/simple` and publishes the shared Lambda Layer. Runtime Lambdas need no PyPI
21
+ credentials or package-registry access.
22
+
23
+ The PyPI distribution is `pofsis-pay`; the Python import remains `pofsis_pofpay`.
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pofsis-pay"
7
+ version = "0.1.0"
8
+ description = "Internal POFSIS client for IAM-authenticated POFPay integrations"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Operating System :: OS Independent",
15
+ ]
16
+ dependencies = [
17
+ "boto3>=1.35,<2",
18
+ "httpx>=0.28,<1",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ dev = ["pytest>=8,<10"]
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+
27
+ [tool.pytest.ini_options]
28
+ pythonpath = ["src"]
29
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: pofsis-pay
3
+ Version: 0.1.0
4
+ Summary: Internal POFSIS client for IAM-authenticated POFPay integrations
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: Programming Language :: Python :: 3.12
7
+ Classifier: Operating System :: OS Independent
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: boto3<2,>=1.35
11
+ Requires-Dist: httpx<1,>=0.28
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest<10,>=8; extra == "dev"
14
+
15
+ # pofsis-pay
16
+
17
+ Private Python SDK for IAM-authenticated POFPay machine requests and signed callback validation.
18
+
19
+ ```python
20
+ from pofsis_pofpay import POFPayClient
21
+
22
+ client = POFPayClient(
23
+ api_base_url="https://pofpay.example.com",
24
+ region="ap-southeast-1",
25
+ )
26
+ checkout = client.create_payment_request(snapshot, "tool:order-id")
27
+ ```
28
+
29
+ Lambda uses the standard AWS credential chain, so no access key is stored in application
30
+ configuration. Install the same pinned version locally and in the published Python 3.12 Layer.
31
+
32
+ `buildspec-publish.yml` tests, builds, validates, and publishes an immutable package version to
33
+ PyPI using a protected `PYPI_API_TOKEN`. `buildspec-layer.yml` installs that exact public version
34
+ from `https://pypi.org/simple` and publishes the shared Lambda Layer. Runtime Lambdas need no PyPI
35
+ credentials or package-registry access.
36
+
37
+ The PyPI distribution is `pofsis-pay`; the Python import remains `pofsis_pofpay`.
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/pofsis_pay.egg-info/PKG-INFO
4
+ src/pofsis_pay.egg-info/SOURCES.txt
5
+ src/pofsis_pay.egg-info/dependency_links.txt
6
+ src/pofsis_pay.egg-info/requires.txt
7
+ src/pofsis_pay.egg-info/top_level.txt
8
+ src/pofsis_pofpay/__init__.py
9
+ src/pofsis_pofpay/callbacks.py
10
+ src/pofsis_pofpay/client.py
11
+ src/pofsis_pofpay/errors.py
12
+ src/pofsis_pofpay/models.py
13
+ tests/test_sdk.py
@@ -0,0 +1,5 @@
1
+ boto3<2,>=1.35
2
+ httpx<1,>=0.28
3
+
4
+ [dev]
5
+ pytest<10,>=8
@@ -0,0 +1 @@
1
+ pofsis_pofpay
@@ -0,0 +1,15 @@
1
+ from .callbacks import validate_payment_snapshot, verify_callback
2
+ from .client import POFPayClient, derive_idempotency_key
3
+ from .errors import POFPayError
4
+ from .models import CallbackEvent, PaymentRequestCreated, PaymentRequestStatus
5
+
6
+ __all__ = [
7
+ "CallbackEvent",
8
+ "POFPayClient",
9
+ "POFPayError",
10
+ "PaymentRequestCreated",
11
+ "PaymentRequestStatus",
12
+ "derive_idempotency_key",
13
+ "validate_payment_snapshot",
14
+ "verify_callback",
15
+ ]
@@ -0,0 +1,98 @@
1
+ import hashlib
2
+ import hmac
3
+ import json
4
+ import time
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from .errors import POFPayError
9
+ from .models import CallbackEvent, PaymentRequestStatus
10
+
11
+
12
+ def verify_callback(
13
+ raw_body: bytes,
14
+ headers: Mapping[str, str],
15
+ callback_secret: str,
16
+ tolerance_seconds: int = 300,
17
+ *,
18
+ now: int | None = None,
19
+ ) -> CallbackEvent:
20
+ if not callback_secret:
21
+ raise POFPayError(
22
+ "POFPay callback verification is not configured",
23
+ status_code=503,
24
+ code="POFPAY_CALLBACK_NOT_CONFIGURED",
25
+ )
26
+ normalized = {str(key).lower(): str(value) for key, value in headers.items()}
27
+ timestamp = normalized.get("pofpay-timestamp", "")
28
+ signature = normalized.get("pofpay-signature", "")
29
+ event_id = normalized.get("pofpay-event-id", "")
30
+ try:
31
+ issued_at = int(timestamp)
32
+ except ValueError as exc:
33
+ raise POFPayError(
34
+ "Invalid callback timestamp",
35
+ status_code=401,
36
+ code="INVALID_CALLBACK_SIGNATURE",
37
+ ) from exc
38
+ current_time = int(time.time()) if now is None else now
39
+ if abs(current_time - issued_at) > tolerance_seconds:
40
+ raise POFPayError(
41
+ "Expired callback timestamp",
42
+ status_code=401,
43
+ code="INVALID_CALLBACK_SIGNATURE",
44
+ )
45
+ supplied = signature.removeprefix("v1=")
46
+ expected = hmac.new(
47
+ callback_secret.encode(),
48
+ timestamp.encode() + b"." + raw_body,
49
+ hashlib.sha256,
50
+ ).hexdigest()
51
+ if not signature.startswith("v1=") or not hmac.compare_digest(expected, supplied):
52
+ raise POFPayError(
53
+ "Invalid callback signature",
54
+ status_code=401,
55
+ code="INVALID_CALLBACK_SIGNATURE",
56
+ )
57
+ try:
58
+ payload = json.loads(raw_body)
59
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
60
+ raise POFPayError(
61
+ "Invalid callback body",
62
+ status_code=400,
63
+ code="INVALID_CALLBACK_BODY",
64
+ ) from exc
65
+ if (
66
+ not isinstance(payload, dict)
67
+ or not isinstance(payload.get("data"), dict)
68
+ or not event_id
69
+ or payload.get("id") != event_id
70
+ or payload.get("type") != "payment_request.status_changed"
71
+ ):
72
+ raise POFPayError(
73
+ "Callback event does not match its headers",
74
+ status_code=400,
75
+ code="INVALID_CALLBACK_BODY",
76
+ )
77
+ return CallbackEvent(event_id=event_id, event_type=payload["type"], data=payload["data"])
78
+
79
+
80
+ def validate_payment_snapshot(
81
+ received: PaymentRequestStatus | Mapping[str, Any],
82
+ expected: Mapping[str, Any],
83
+ ) -> None:
84
+ values = received.__dict__ if isinstance(received, PaymentRequestStatus) else received
85
+ fields = ("request_ref", "source_tool", "reference_type", "reference_id", "base_amount", "currency")
86
+ for field in fields:
87
+ actual = values.get(field)
88
+ wanted = expected.get(field)
89
+ if field in {"base_amount"} and actual is not None and wanted is not None:
90
+ actual, wanted = int(actual), int(wanted)
91
+ if field == "currency" and actual is not None and wanted is not None:
92
+ actual, wanted = str(actual).upper(), str(wanted).upper()
93
+ if actual != wanted:
94
+ raise POFPayError(
95
+ f"POFPay {field} does not match the local payment snapshot",
96
+ status_code=502,
97
+ code="POFPAY_SNAPSHOT_MISMATCH",
98
+ )
@@ -0,0 +1,159 @@
1
+ import json
2
+ import time
3
+ from datetime import datetime
4
+ from typing import Any
5
+ from urllib.parse import quote
6
+
7
+ import boto3
8
+ import httpx
9
+ from botocore.auth import SigV4Auth
10
+ from botocore.awsrequest import AWSRequest
11
+
12
+ from .errors import POFPayError
13
+ from .models import PaymentRequestCreated, PaymentRequestStatus
14
+
15
+ RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
16
+
17
+
18
+ class POFPayClient:
19
+ def __init__(
20
+ self,
21
+ api_base_url: str,
22
+ region: str = "ap-southeast-1",
23
+ timeout: float = 10.0,
24
+ *,
25
+ credential_provider: Any | None = None,
26
+ transport: httpx.BaseTransport | None = None,
27
+ max_attempts: int = 3,
28
+ backoff_seconds: float = 0.2,
29
+ ) -> None:
30
+ self.api_base_url = api_base_url.rstrip("/")
31
+ self.region = region
32
+ self.timeout = timeout
33
+ self.credential_provider = credential_provider or boto3.Session()
34
+ self.transport = transport
35
+ self.max_attempts = max(1, max_attempts)
36
+ self.backoff_seconds = max(0.0, backoff_seconds)
37
+
38
+ def _credentials(self):
39
+ credentials = self.credential_provider.get_credentials()
40
+ if credentials is None:
41
+ raise POFPayError(
42
+ "AWS credentials are unavailable",
43
+ status_code=503,
44
+ code="POFPAY_AUTH_UNAVAILABLE",
45
+ )
46
+ return credentials.get_frozen_credentials()
47
+
48
+ def _signed_headers(self, method: str, url: str, body: bytes, headers: dict[str, str]) -> dict[str, str]:
49
+ request = AWSRequest(method=method, url=url, data=body, headers=headers)
50
+ SigV4Auth(self._credentials(), "execute-api", self.region).add_auth(request)
51
+ return dict(request.headers.items())
52
+
53
+ @staticmethod
54
+ def _error(response: httpx.Response) -> POFPayError:
55
+ code = "POFPAY_REQUEST_REJECTED"
56
+ message = "POFPay rejected the request"
57
+ request_ref = None
58
+ try:
59
+ payload = response.json()
60
+ detail = payload.get("error") or payload.get("detail") or payload
61
+ if isinstance(detail, dict):
62
+ code = str(detail.get("code", code))
63
+ message = str(detail.get("message", message))
64
+ request_ref = detail.get("request_ref")
65
+ except ValueError:
66
+ pass
67
+ status = response.status_code if 400 <= response.status_code < 500 else 502
68
+ return POFPayError(message, status_code=status, code=code, request_ref=request_ref)
69
+
70
+ def _request(self, method: str, path: str, *, payload: dict[str, Any] | None = None, headers=None):
71
+ body = (
72
+ json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
73
+ if payload is not None
74
+ else b""
75
+ )
76
+ url = f"{self.api_base_url}{path}"
77
+ base_headers = {"Accept": "application/json", **(headers or {})}
78
+ if payload is not None:
79
+ base_headers["Content-Type"] = "application/json"
80
+ last_error: Exception | None = None
81
+ for attempt in range(self.max_attempts):
82
+ signed_headers = self._signed_headers(method, url, body, base_headers)
83
+ try:
84
+ with httpx.Client(timeout=self.timeout, transport=self.transport) as client:
85
+ response = client.request(method, url, content=body or None, headers=signed_headers)
86
+ except httpx.RequestError as exc:
87
+ last_error = exc
88
+ if attempt + 1 < self.max_attempts:
89
+ time.sleep(self.backoff_seconds * (2**attempt))
90
+ continue
91
+ raise POFPayError(
92
+ "POFPay is temporarily unavailable",
93
+ code="POFPAY_UNAVAILABLE",
94
+ ) from exc
95
+ if response.status_code in RETRYABLE_STATUS_CODES and attempt + 1 < self.max_attempts:
96
+ time.sleep(self.backoff_seconds * (2**attempt))
97
+ continue
98
+ if response.status_code >= 400:
99
+ raise self._error(response)
100
+ try:
101
+ data = response.json()
102
+ except ValueError as exc:
103
+ raise POFPayError(
104
+ "POFPay returned an invalid response",
105
+ code="POFPAY_INVALID_RESPONSE",
106
+ ) from exc
107
+ if not isinstance(data, dict):
108
+ raise POFPayError("POFPay returned an invalid response", code="POFPAY_INVALID_RESPONSE")
109
+ return data
110
+ raise POFPayError("POFPay is temporarily unavailable", code="POFPAY_UNAVAILABLE") from last_error
111
+
112
+ def create_payment_request(
113
+ self,
114
+ snapshot: dict[str, Any],
115
+ idempotency_key: str,
116
+ ) -> PaymentRequestCreated:
117
+ data = self._request(
118
+ "POST",
119
+ "/v1/payment-requests",
120
+ payload=snapshot,
121
+ headers={"Idempotency-Key": idempotency_key},
122
+ )
123
+ try:
124
+ return PaymentRequestCreated(
125
+ request_ref=str(data["request_ref"]),
126
+ status=str(data["status"]),
127
+ checkout_url=str(data["checkout_url"]),
128
+ checkout_expires_at=datetime.fromisoformat(
129
+ str(data["checkout_expires_at"]).replace("Z", "+00:00")
130
+ ),
131
+ )
132
+ except (KeyError, TypeError, ValueError) as exc:
133
+ raise POFPayError("POFPay returned an invalid checkout", code="POFPAY_INVALID_RESPONSE") from exc
134
+
135
+ def get_payment_request(self, request_ref: str) -> PaymentRequestStatus:
136
+ data = self._request("GET", f"/v1/payment-requests/{quote(request_ref, safe='')}")
137
+ try:
138
+ paid_at = data.get("paid_at")
139
+ return PaymentRequestStatus(
140
+ request_ref=str(data["request_ref"]),
141
+ source_tool=str(data["source_tool"]),
142
+ reference_type=str(data["reference_type"]),
143
+ reference_id=str(data["reference_id"]),
144
+ status=str(data["status"]),
145
+ base_amount=int(data["base_amount"]),
146
+ total_amount=int(data["total_amount"]),
147
+ currency=str(data["currency"]),
148
+ paid_at=datetime.fromisoformat(str(paid_at).replace("Z", "+00:00")) if paid_at else None,
149
+ )
150
+ except (KeyError, TypeError, ValueError) as exc:
151
+ raise POFPayError("POFPay returned an invalid status", code="POFPAY_INVALID_RESPONSE") from exc
152
+
153
+
154
+ def derive_idempotency_key(source_tool: str, reference_id: str) -> str:
155
+ source = source_tool.strip().replace("_", "-")
156
+ reference = reference_id.strip()
157
+ if not source or not reference:
158
+ raise ValueError("source_tool and reference_id are required")
159
+ return f"{source}:{reference}"
@@ -0,0 +1,13 @@
1
+ class POFPayError(RuntimeError):
2
+ def __init__(
3
+ self,
4
+ message: str,
5
+ *,
6
+ status_code: int = 502,
7
+ code: str = "POFPAY_ERROR",
8
+ request_ref: str | None = None,
9
+ ) -> None:
10
+ super().__init__(message)
11
+ self.status_code = status_code
12
+ self.code = code
13
+ self.request_ref = request_ref
@@ -0,0 +1,31 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import Any
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class PaymentRequestCreated:
8
+ request_ref: str
9
+ status: str
10
+ checkout_url: str
11
+ checkout_expires_at: datetime
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class PaymentRequestStatus:
16
+ request_ref: str
17
+ source_tool: str
18
+ reference_type: str
19
+ reference_id: str
20
+ status: str
21
+ base_amount: int
22
+ total_amount: int
23
+ currency: str
24
+ paid_at: datetime | None = None
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class CallbackEvent:
29
+ event_id: str
30
+ event_type: str
31
+ data: dict[str, Any]
@@ -0,0 +1,81 @@
1
+ import hashlib
2
+ import hmac
3
+ import json
4
+ from types import SimpleNamespace
5
+
6
+ import httpx
7
+ import pytest
8
+ from botocore.credentials import Credentials
9
+
10
+ from pofsis_pofpay import POFPayClient, POFPayError, verify_callback
11
+
12
+
13
+ class CredentialsProvider:
14
+ def get_credentials(self):
15
+ return Credentials("access", "secret", "token")
16
+
17
+
18
+ def test_client_sigv4_signs_payment_request():
19
+ def handler(request: httpx.Request) -> httpx.Response:
20
+ assert request.headers["authorization"].startswith("AWS4-HMAC-SHA256 Credential=access/")
21
+ assert request.headers["x-amz-security-token"] == "token"
22
+ assert request.headers["idempotency-key"] == "orbit-store:purchase-1"
23
+ assert json.loads(request.content) == {"base_amount": 49900}
24
+ return httpx.Response(
25
+ 201,
26
+ json={
27
+ "request_ref": "payreq_1",
28
+ "status": "OPEN",
29
+ "checkout_url": "https://pay.example.com/pay/token",
30
+ "checkout_expires_at": "2026-10-21T10:00:00Z",
31
+ },
32
+ )
33
+
34
+ client = POFPayClient(
35
+ "https://api.example.com",
36
+ credential_provider=CredentialsProvider(),
37
+ transport=httpx.MockTransport(handler),
38
+ backoff_seconds=0,
39
+ )
40
+
41
+ result = client.create_payment_request({"base_amount": 49900}, "orbit-store:purchase-1")
42
+ assert result.request_ref == "payreq_1"
43
+
44
+
45
+ def test_callback_verifies_signature_event_and_raw_body():
46
+ body = b'{"id":"event-1","type":"payment_request.status_changed","data":{"status":"PAID"}}'
47
+ timestamp = "1000"
48
+ digest = hmac.new(b"secret", timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
49
+
50
+ event = verify_callback(
51
+ body,
52
+ {
53
+ "POFPay-Event-Id": "event-1",
54
+ "POFPay-Timestamp": timestamp,
55
+ "POFPay-Signature": f"v1={digest}",
56
+ },
57
+ "secret",
58
+ now=1000,
59
+ )
60
+
61
+ assert event.event_id == "event-1"
62
+ assert event.data["status"] == "PAID"
63
+
64
+
65
+ def test_callback_rejects_header_body_event_mismatch():
66
+ body = b'{"id":"event-1","type":"payment_request.status_changed","data":{}}'
67
+ timestamp = "1000"
68
+ digest = hmac.new(b"secret", timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
69
+
70
+ with pytest.raises(POFPayError) as error:
71
+ verify_callback(
72
+ body,
73
+ {
74
+ "POFPay-Event-Id": "event-2",
75
+ "POFPay-Timestamp": timestamp,
76
+ "POFPay-Signature": f"v1={digest}",
77
+ },
78
+ "secret",
79
+ now=1000,
80
+ )
81
+ assert error.value.code == "INVALID_CALLBACK_BODY"