python-getpaid-simulator 3.0.0a3__py3-none-any.whl

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,216 @@
1
+ """Plugin loading for simulator provider integrations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass
8
+ from dataclasses import field
9
+ from importlib.metadata import entry_points
10
+ from typing import Any
11
+
12
+ from getpaid_simulator.core.config import SimulatorConfig
13
+ from getpaid_simulator.spi import SIMULATOR_PLUGIN_API_VERSION
14
+ from getpaid_simulator.spi import SimulatorProviderPlugin
15
+
16
+
17
+ ENTRY_POINT_GROUP = "getpaid.simulator.providers"
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class ProviderLoadFailure:
22
+ """Structured plugin load failure for logging and UI reporting."""
23
+
24
+ slug: str
25
+ stage: str
26
+ error: str
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class PluginLoadResult:
31
+ """Loaded plugins, failures, and validated provider configs."""
32
+
33
+ loaded_plugins: tuple[SimulatorProviderPlugin, ...] = ()
34
+ failed_plugins: tuple[ProviderLoadFailure, ...] = ()
35
+ provider_configs: dict[str, Mapping[str, Any]] = field(default_factory=dict)
36
+
37
+
38
+ class PluginLoadError(RuntimeError):
39
+ """Raised in strict mode when a simulator plugin cannot be loaded."""
40
+
41
+ def __init__(self, failure: ProviderLoadFailure):
42
+ self.failure = failure
43
+ super().__init__(
44
+ f"Failed to load simulator plugin {failure.slug!r} during "
45
+ f"{failure.stage}: {failure.error}"
46
+ )
47
+
48
+
49
+ def load_provider_plugins(
50
+ config: SimulatorConfig,
51
+ env: Mapping[str, str] | None = None,
52
+ ) -> PluginLoadResult:
53
+ """Load simulator provider plugins from entry points."""
54
+ environment = env or os.environ
55
+ loaded_plugins: list[SimulatorProviderPlugin] = []
56
+ failed_plugins: list[ProviderLoadFailure] = []
57
+ provider_configs: dict[str, Mapping[str, Any]] = {}
58
+
59
+ for entry_point in sorted(
60
+ entry_points(group=ENTRY_POINT_GROUP),
61
+ key=lambda candidate: candidate.name,
62
+ ):
63
+ slug = entry_point.name or "<unknown>"
64
+
65
+ try:
66
+ plugin_factory = entry_point.load()
67
+ except Exception as exc:
68
+ _handle_failure(
69
+ config=config,
70
+ failed_plugins=failed_plugins,
71
+ failure=ProviderLoadFailure(
72
+ slug=slug,
73
+ stage="import",
74
+ error=str(exc),
75
+ ),
76
+ )
77
+ continue
78
+
79
+ try:
80
+ plugin = (
81
+ plugin_factory() if callable(plugin_factory) else plugin_factory
82
+ )
83
+ except Exception as exc:
84
+ _handle_failure(
85
+ config=config,
86
+ failed_plugins=failed_plugins,
87
+ failure=ProviderLoadFailure(
88
+ slug=slug,
89
+ stage="factory",
90
+ error=str(exc),
91
+ ),
92
+ )
93
+ continue
94
+
95
+ failure = _validate_plugin(slug, plugin)
96
+ if failure is not None:
97
+ _handle_failure(
98
+ config=config,
99
+ failed_plugins=failed_plugins,
100
+ failure=failure,
101
+ )
102
+ continue
103
+
104
+ normalized_plugin = _normalize_plugin(plugin)
105
+
106
+ try:
107
+ provider_configs[normalized_plugin.slug] = dict(
108
+ normalized_plugin.load_config(environment)
109
+ )
110
+ except Exception as exc:
111
+ _handle_failure(
112
+ config=config,
113
+ failed_plugins=failed_plugins,
114
+ failure=ProviderLoadFailure(
115
+ slug=normalized_plugin.slug,
116
+ stage="config",
117
+ error=str(exc),
118
+ ),
119
+ )
120
+ continue
121
+
122
+ loaded_plugins.append(normalized_plugin)
123
+
124
+ return PluginLoadResult(
125
+ loaded_plugins=tuple(loaded_plugins),
126
+ failed_plugins=tuple(failed_plugins),
127
+ provider_configs=provider_configs,
128
+ )
129
+
130
+
131
+ def _validate_plugin(
132
+ entry_point_slug: str,
133
+ plugin: object,
134
+ ) -> ProviderLoadFailure | None:
135
+ required_fields = {
136
+ "api_version",
137
+ "slug",
138
+ "display_name",
139
+ "api_handlers",
140
+ "ui_handlers",
141
+ "transitions",
142
+ "load_config",
143
+ }
144
+ missing_fields = [
145
+ field_name
146
+ for field_name in sorted(required_fields)
147
+ if not hasattr(plugin, field_name)
148
+ ]
149
+ if missing_fields:
150
+ return ProviderLoadFailure(
151
+ slug=entry_point_slug,
152
+ stage="factory",
153
+ error=(
154
+ "plugin object is missing required fields: "
155
+ f"{', '.join(missing_fields)}"
156
+ ),
157
+ )
158
+
159
+ plugin_api_version = str(getattr(plugin, "api_version"))
160
+ plugin_slug = str(getattr(plugin, "slug"))
161
+ if plugin_api_version != SIMULATOR_PLUGIN_API_VERSION:
162
+ return ProviderLoadFailure(
163
+ slug=plugin_slug,
164
+ stage="compatibility",
165
+ error=(
166
+ "plugin API version "
167
+ f"{plugin_api_version!r} is incompatible with host "
168
+ f"{SIMULATOR_PLUGIN_API_VERSION!r}"
169
+ ),
170
+ )
171
+
172
+ if plugin_slug != entry_point_slug:
173
+ return ProviderLoadFailure(
174
+ slug=entry_point_slug,
175
+ stage="compatibility",
176
+ error=(
177
+ f"plugin slug {plugin_slug!r} does not match entry point "
178
+ f"name {entry_point_slug!r}"
179
+ ),
180
+ )
181
+
182
+ return None
183
+
184
+
185
+ def _normalize_plugin(plugin: object) -> SimulatorProviderPlugin:
186
+ transitions = {
187
+ str(status): set(next_statuses)
188
+ for status, next_statuses in dict(
189
+ getattr(plugin, "transitions")
190
+ ).items()
191
+ }
192
+ authorize_path_template = getattr(plugin, "authorize_path_template", None)
193
+ if authorize_path_template is not None:
194
+ authorize_path_template = str(authorize_path_template)
195
+
196
+ return SimulatorProviderPlugin(
197
+ api_version=str(getattr(plugin, "api_version")),
198
+ slug=str(getattr(plugin, "slug")),
199
+ display_name=str(getattr(plugin, "display_name")),
200
+ api_handlers=tuple(getattr(plugin, "api_handlers")),
201
+ ui_handlers=tuple(getattr(plugin, "ui_handlers")),
202
+ transitions=transitions,
203
+ load_config=getattr(plugin, "load_config"),
204
+ authorize_path_template=authorize_path_template,
205
+ )
206
+
207
+
208
+ def _handle_failure(
209
+ *,
210
+ config: SimulatorConfig,
211
+ failed_plugins: list[ProviderLoadFailure],
212
+ failure: ProviderLoadFailure,
213
+ ) -> None:
214
+ if config.plugin_failure_mode == "strict":
215
+ raise PluginLoadError(failure)
216
+ failed_plugins.append(failure)
@@ -0,0 +1,31 @@
1
+ """Stable plugin API for simulator provider integrations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+
11
+ SIMULATOR_PLUGIN_API_VERSION = "1"
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class SimulatorProviderPlugin:
16
+ """Declarative simulator provider plugin descriptor."""
17
+
18
+ api_version: str
19
+ slug: str
20
+ display_name: str
21
+ api_handlers: tuple[Any, ...]
22
+ ui_handlers: tuple[Any, ...]
23
+ transitions: dict[str, set[str]]
24
+ load_config: Callable[[Mapping[str, str]], Mapping[str, Any]]
25
+ authorize_path_template: str | None = None
26
+
27
+ def build_authorize_path(self, entity_id: str) -> str | None:
28
+ """Return the provider-specific authorize URL for dashboard links."""
29
+ if self.authorize_path_template is None:
30
+ return None
31
+ return self.authorize_path_template.format(entity_id=entity_id)
@@ -0,0 +1 @@
1
+ """UI components."""
@@ -0,0 +1,68 @@
1
+ from typing import Optional
2
+
3
+ from litestar import get
4
+ from litestar.datastructures import State
5
+ from litestar.response import Template
6
+
7
+ from getpaid_simulator.core.storage import SimulatorStorage
8
+ from getpaid_simulator.plugins import ProviderLoadFailure
9
+ from getpaid_simulator.spi import SimulatorProviderPlugin
10
+
11
+
12
+ @get(["/sim/", "/sim/dashboard"])
13
+ async def dashboard(state: State, provider: Optional[str] = None) -> Template:
14
+ """Render payments dashboard."""
15
+ storage: SimulatorStorage = state.storage
16
+ loaded_plugins: dict[str, SimulatorProviderPlugin] = state.loaded_plugins
17
+ failed_plugins: tuple[ProviderLoadFailure, ...] = state.failed_plugins
18
+ if provider:
19
+ orders_data = storage.list_orders_by_provider(provider)
20
+ else:
21
+ orders_data = storage.list_orders()
22
+
23
+ provider_filters = [
24
+ {
25
+ "slug": slug,
26
+ "display_name": plugin.display_name,
27
+ }
28
+ for slug, plugin in loaded_plugins.items()
29
+ ]
30
+
31
+ orders = []
32
+ for order in orders_data:
33
+ amount_raw = order.get("amount", order.get("totalAmount", 0))
34
+ currency = order.get("currency", order.get("currencyCode", "PLN"))
35
+ try:
36
+ val = float(amount_raw) / 100
37
+ formatted = f"{val:.2f} {currency}"
38
+ except (ValueError, TypeError):
39
+ formatted = str(amount_raw)
40
+
41
+ provider_slug = str(order.get("provider", "unknown"))
42
+ plugin = loaded_plugins.get(provider_slug)
43
+ orders.append(
44
+ {
45
+ "id": order["id"],
46
+ "provider": provider_slug,
47
+ "provider_display_name": (
48
+ plugin.display_name if plugin is not None else provider_slug
49
+ ),
50
+ "status": order.get("status", "NEW"),
51
+ "formatted_amount": formatted,
52
+ "authorize_url": (
53
+ plugin.build_authorize_path(str(order["id"]))
54
+ if plugin is not None
55
+ else None
56
+ ),
57
+ }
58
+ )
59
+
60
+ return Template(
61
+ template_name="dashboard.html",
62
+ context={
63
+ "orders": orders,
64
+ "current_provider": provider,
65
+ "provider_filters": provider_filters,
66
+ "failed_plugins": failed_plugins,
67
+ },
68
+ )
File without changes
@@ -0,0 +1,262 @@
1
+ :root {
2
+ --bg-dark: #0d0d0d;
3
+ --bg-card: #1a1a1a;
4
+ --text-main: #e0e0e0;
5
+ --text-muted: #888888;
6
+
7
+ --amber: #ff9900;
8
+ --amber-dim: rgba(255, 153, 0, 0.2);
9
+
10
+ --neon-green: #00ff88;
11
+ --neon-green-dim: rgba(0, 255, 136, 0.2);
12
+
13
+ --neon-red: #ff3344;
14
+ --neon-red-dim: rgba(255, 51, 68, 0.2);
15
+
16
+ --border-radius: 4px;
17
+ --font-mono: 'Courier New', Courier, monospace, system-ui;
18
+ }
19
+
20
+ * {
21
+ box-sizing: border-box;
22
+ margin: 0;
23
+ padding: 0;
24
+ }
25
+
26
+ body {
27
+ background-color: var(--bg-dark);
28
+ color: var(--text-main);
29
+ font-family: var(--font-mono);
30
+ line-height: 1.6;
31
+ min-height: 100vh;
32
+ display: flex;
33
+ flex-direction: column;
34
+ }
35
+
36
+ /* Layout */
37
+ .container {
38
+ max-width: 800px;
39
+ margin: 0 auto;
40
+ padding: 2rem 1rem;
41
+ width: 100%;
42
+ flex: 1;
43
+ }
44
+
45
+ /* Header & Footer */
46
+ .simulator-header {
47
+ border-bottom: 2px solid var(--amber);
48
+ padding: 1rem;
49
+ text-align: center;
50
+ background-color: var(--bg-card);
51
+ box-shadow: 0 4px 12px var(--amber-dim);
52
+ }
53
+
54
+ .simulator-header h1 {
55
+ color: var(--amber);
56
+ font-size: 1.5rem;
57
+ text-transform: uppercase;
58
+ letter-spacing: 2px;
59
+ margin-bottom: 0.5rem;
60
+ }
61
+
62
+ .simulator-provider {
63
+ font-size: 0.9rem;
64
+ color: var(--text-muted);
65
+ }
66
+
67
+ .simulator-footer {
68
+ text-align: center;
69
+ padding: 1rem;
70
+ background-color: var(--neon-red-dim);
71
+ border-top: 1px solid var(--neon-red);
72
+ color: var(--neon-red);
73
+ font-weight: bold;
74
+ font-size: 0.9rem;
75
+ text-transform: uppercase;
76
+ letter-spacing: 1px;
77
+ }
78
+
79
+ /* Warning Badge */
80
+ .sim-badge {
81
+ display: inline-block;
82
+ background-color: var(--amber);
83
+ color: var(--bg-dark);
84
+ padding: 0.25rem 0.5rem;
85
+ font-weight: bold;
86
+ border-radius: var(--border-radius);
87
+ text-transform: uppercase;
88
+ font-size: 0.8rem;
89
+ }
90
+
91
+ /* Typography elements */
92
+ h2, h3 {
93
+ color: var(--text-main);
94
+ margin-bottom: 1rem;
95
+ font-weight: normal;
96
+ border-bottom: 1px dashed var(--text-muted);
97
+ padding-bottom: 0.5rem;
98
+ }
99
+
100
+ /* Card Component */
101
+ .payment-card {
102
+ background-color: var(--bg-card);
103
+ border: 1px solid var(--text-muted);
104
+ border-left: 4px solid var(--amber);
105
+ border-radius: var(--border-radius);
106
+ padding: 1.5rem;
107
+ margin-bottom: 1.5rem;
108
+ transition: all 0.2s ease-in-out;
109
+ }
110
+
111
+ .payment-card:hover {
112
+ box-shadow: 0 0 15px var(--amber-dim);
113
+ border-color: var(--amber);
114
+ }
115
+
116
+ .payment-card-header {
117
+ display: flex;
118
+ justify-content: space-between;
119
+ align-items: center;
120
+ margin-bottom: 1rem;
121
+ padding-bottom: 0.5rem;
122
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
123
+ }
124
+
125
+ .payment-amount {
126
+ font-size: 1.5rem;
127
+ color: var(--text-main);
128
+ font-weight: bold;
129
+ }
130
+
131
+ .payment-details {
132
+ display: grid;
133
+ grid-template-columns: auto 1fr;
134
+ gap: 0.5rem 1rem;
135
+ font-size: 0.9rem;
136
+ }
137
+
138
+ .detail-label {
139
+ color: var(--text-muted);
140
+ }
141
+
142
+ .detail-value {
143
+ color: var(--text-main);
144
+ }
145
+
146
+ /* Status Badges */
147
+ .status-badge {
148
+ display: inline-block;
149
+ padding: 0.25rem 0.75rem;
150
+ border-radius: 1rem;
151
+ font-size: 0.8rem;
152
+ font-weight: bold;
153
+ text-transform: uppercase;
154
+ letter-spacing: 1px;
155
+ }
156
+
157
+ .status-badge.status-pending {
158
+ background-color: var(--amber-dim);
159
+ color: var(--amber);
160
+ border: 1px solid var(--amber);
161
+ animation: pulse 2s infinite;
162
+ }
163
+
164
+ .status-badge.status-waiting_for_confirmation {
165
+ background-color: var(--amber-dim);
166
+ color: var(--amber);
167
+ border: 1px solid var(--amber);
168
+ }
169
+
170
+ .status-badge.status-completed {
171
+ background-color: var(--neon-green-dim);
172
+ color: var(--neon-green);
173
+ border: 1px solid var(--neon-green);
174
+ box-shadow: 0 0 8px var(--neon-green-dim);
175
+ }
176
+
177
+ .status-badge.status-canceled {
178
+ background-color: var(--neon-red-dim);
179
+ color: var(--neon-red);
180
+ border: 1px solid var(--neon-red);
181
+ }
182
+
183
+ /* Animations */
184
+ @keyframes pulse {
185
+ 0% { opacity: 0.8; box-shadow: 0 0 0 0 rgba(255, 153, 0, 0.4); }
186
+ 50% { opacity: 1; box-shadow: 0 0 0 4px rgba(255, 153, 0, 0); }
187
+ 100% { opacity: 0.8; box-shadow: 0 0 0 0 rgba(255, 153, 0, 0); }
188
+ }
189
+
190
+ /* Buttons */
191
+ .btn {
192
+ display: inline-block;
193
+ padding: 0.75rem 1.5rem;
194
+ border: 1px solid var(--text-muted);
195
+ border-radius: var(--border-radius);
196
+ background-color: transparent;
197
+ color: var(--text-main);
198
+ font-family: inherit;
199
+ font-size: 1rem;
200
+ cursor: pointer;
201
+ text-decoration: none;
202
+ text-transform: uppercase;
203
+ transition: all 0.2s;
204
+ text-align: center;
205
+ }
206
+
207
+ .btn:hover {
208
+ background-color: rgba(255, 255, 255, 0.05);
209
+ }
210
+
211
+ .btn-approve {
212
+ border-color: var(--neon-green);
213
+ color: var(--neon-green);
214
+ }
215
+
216
+ .btn-approve:hover {
217
+ background-color: var(--neon-green-dim);
218
+ box-shadow: 0 0 10px var(--neon-green-dim);
219
+ }
220
+
221
+ .btn-reject {
222
+ border-color: var(--neon-red);
223
+ color: var(--neon-red);
224
+ }
225
+
226
+ .btn-reject:hover {
227
+ background-color: var(--neon-red-dim);
228
+ box-shadow: 0 0 10px var(--neon-red-dim);
229
+ }
230
+
231
+ .btn-neutral {
232
+ border-color: var(--amber);
233
+ color: var(--amber);
234
+ }
235
+
236
+ .btn-neutral:hover {
237
+ background-color: var(--amber-dim);
238
+ box-shadow: 0 0 10px var(--amber-dim);
239
+ }
240
+
241
+ .action-bar {
242
+ display: flex;
243
+ gap: 1rem;
244
+ margin-top: 1.5rem;
245
+ padding-top: 1rem;
246
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
247
+ }
248
+
249
+ /* Responsive */
250
+ @media (max-width: 480px) {
251
+ .simulator-header h1 {
252
+ font-size: 1.2rem;
253
+ }
254
+
255
+ .action-bar {
256
+ flex-direction: column;
257
+ }
258
+
259
+ .btn {
260
+ width: 100%;
261
+ }
262
+ }
File without changes
@@ -0,0 +1,24 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="authorization-container">
5
+ <div class="header">
6
+ <h1>Authorize Payment</h1>
7
+ <div class="provider-badge">{{ provider }}</div>
8
+ </div>
9
+
10
+ {% include "components/payment_card.html" %}
11
+
12
+ <form method="POST" action="" class="action-buttons">
13
+ <button type="submit" name="action" value="approve" class="btn btn-approve">
14
+ <span class="btn-text">Approve Payment</span>
15
+ <div class="btn-glow"></div>
16
+ </button>
17
+
18
+ <button type="submit" name="action" value="reject" class="btn btn-reject">
19
+ <span class="btn-text">Reject Payment</span>
20
+ <div class="btn-glow"></div>
21
+ </button>
22
+ </form>
23
+ </div>
24
+ {% endblock %}
@@ -0,0 +1,31 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="theme-color" content="#0d0d0d">
7
+ <meta name="color-scheme" content="dark">
8
+ <title>{% block title %}Simulator{% endblock %} | GetPaid Simulator</title>
9
+ <link rel="stylesheet" href="/static/style.css">
10
+ </head>
11
+ <body>
12
+ <header class="simulator-header">
13
+ <h1>⚡ GETPAID SIMULATOR</h1>
14
+ <div class="simulator-provider">
15
+ {% if provider %}
16
+ Simulating Provider: <strong>{{ provider }}</strong>
17
+ {% else %}
18
+ Test Environment
19
+ {% endif %}
20
+ </div>
21
+ </header>
22
+
23
+ <main class="container">
24
+ {% block content %}{% endblock %}
25
+ </main>
26
+
27
+ <footer class="simulator-footer">
28
+ SIMULATOR MODE — NOT A REAL PAYMENT GATEWAY
29
+ </footer>
30
+ </body>
31
+ </html>
@@ -0,0 +1,9 @@
1
+ {% extends "components/payment_card.html" %}
2
+
3
+ {% block card_actions %}
4
+ {% if authorize_url %}
5
+ <a href="{{ authorize_url }}" class="btn btn-neutral">Authorize</a>
6
+ {% else %}
7
+ <span style="color: var(--text-muted);">No UI flow</span>
8
+ {% endif %}
9
+ {% endblock %}
@@ -0,0 +1,17 @@
1
+ <div class="payment-card">
2
+ <div class="payment-card-header">
3
+ <div class="payment-amount">{{ amount }}</div>
4
+ {% include "components/status_badge.html" with context %}
5
+ </div>
6
+ <div class="payment-details">
7
+ <div class="detail-label">Order ID:</div>
8
+ <div class="detail-value">{{ order_id }}</div>
9
+
10
+ <div class="detail-label">Provider:</div>
11
+ <div class="detail-value">{{ provider }}</div>
12
+ </div>
13
+
14
+ <div class="action-bar">
15
+ {% block card_actions %}{% endblock %}
16
+ </div>
17
+ </div>
@@ -0,0 +1,3 @@
1
+ <span class="status-badge status-{{ status | lower }}">
2
+ {{ status }}
3
+ </span>