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.
- getpaid_simulator/__init__.py +3 -0
- getpaid_simulator/__main__.py +112 -0
- getpaid_simulator/app.py +130 -0
- getpaid_simulator/core/__init__.py +1 -0
- getpaid_simulator/core/config.py +57 -0
- getpaid_simulator/core/discovery.py +12 -0
- getpaid_simulator/core/state.py +58 -0
- getpaid_simulator/core/storage.py +155 -0
- getpaid_simulator/core/webhooks.py +60 -0
- getpaid_simulator/plugins.py +216 -0
- getpaid_simulator/spi.py +31 -0
- getpaid_simulator/ui/__init__.py +1 -0
- getpaid_simulator/ui/routes.py +68 -0
- getpaid_simulator/ui/static/.gitkeep +0 -0
- getpaid_simulator/ui/static/style.css +262 -0
- getpaid_simulator/ui/templates/.gitkeep +0 -0
- getpaid_simulator/ui/templates/authorize.html +24 -0
- getpaid_simulator/ui/templates/base.html +31 -0
- getpaid_simulator/ui/templates/components/dashboard_payment_card.html +9 -0
- getpaid_simulator/ui/templates/components/payment_card.html +17 -0
- getpaid_simulator/ui/templates/components/status_badge.html +3 -0
- getpaid_simulator/ui/templates/dashboard.html +45 -0
- python_getpaid_simulator-3.0.0a3.dist-info/METADATA +112 -0
- python_getpaid_simulator-3.0.0a3.dist-info/RECORD +26 -0
- python_getpaid_simulator-3.0.0a3.dist-info/WHEEL +4 -0
- python_getpaid_simulator-3.0.0a3.dist-info/entry_points.txt +2 -0
|
@@ -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)
|
getpaid_simulator/spi.py
ADDED
|
@@ -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>
|