python-getpaid-simulator 3.0.0a3__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.
Files changed (54) hide show
  1. python_getpaid_simulator-3.0.0a3/.gitignore +77 -0
  2. python_getpaid_simulator-3.0.0a3/Dockerfile +29 -0
  3. python_getpaid_simulator-3.0.0a3/PKG-INFO +112 -0
  4. python_getpaid_simulator-3.0.0a3/README.md +89 -0
  5. python_getpaid_simulator-3.0.0a3/docker-compose.yml +18 -0
  6. python_getpaid_simulator-3.0.0a3/pyproject.toml +125 -0
  7. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/__init__.py +3 -0
  8. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/__main__.py +112 -0
  9. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/app.py +130 -0
  10. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/core/__init__.py +1 -0
  11. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/core/config.py +57 -0
  12. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/core/discovery.py +12 -0
  13. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/core/state.py +58 -0
  14. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/core/storage.py +155 -0
  15. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/core/webhooks.py +60 -0
  16. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/plugins.py +216 -0
  17. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/spi.py +31 -0
  18. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/ui/__init__.py +1 -0
  19. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/ui/routes.py +68 -0
  20. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/ui/static/.gitkeep +0 -0
  21. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/ui/static/style.css +262 -0
  22. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/ui/templates/.gitkeep +0 -0
  23. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/ui/templates/authorize.html +24 -0
  24. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/ui/templates/base.html +31 -0
  25. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/ui/templates/components/dashboard_payment_card.html +9 -0
  26. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/ui/templates/components/payment_card.html +17 -0
  27. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/ui/templates/components/status_badge.html +3 -0
  28. python_getpaid_simulator-3.0.0a3/src/getpaid_simulator/ui/templates/dashboard.html +45 -0
  29. python_getpaid_simulator-3.0.0a3/tests/__init__.py +1 -0
  30. python_getpaid_simulator-3.0.0a3/tests/conftest.py +12 -0
  31. python_getpaid_simulator-3.0.0a3/tests/e2e/__init__.py +1 -0
  32. python_getpaid_simulator-3.0.0a3/tests/e2e/conftest.py +199 -0
  33. python_getpaid_simulator-3.0.0a3/tests/e2e/test_e2e_dashboard.py +48 -0
  34. python_getpaid_simulator-3.0.0a3/tests/e2e/test_e2e_payu_flow.py +80 -0
  35. python_getpaid_simulator-3.0.0a3/tests/test_app.py +160 -0
  36. python_getpaid_simulator-3.0.0a3/tests/test_cli.py +148 -0
  37. python_getpaid_simulator-3.0.0a3/tests/test_config.py +95 -0
  38. python_getpaid_simulator-3.0.0a3/tests/test_discovery.py +110 -0
  39. python_getpaid_simulator-3.0.0a3/tests/test_legacy_provider_modules.py +18 -0
  40. python_getpaid_simulator-3.0.0a3/tests/test_paynow_payments.py +133 -0
  41. python_getpaid_simulator-3.0.0a3/tests/test_paynow_refunds.py +248 -0
  42. python_getpaid_simulator-3.0.0a3/tests/test_paynow_signing.py +243 -0
  43. python_getpaid_simulator-3.0.0a3/tests/test_paynow_webhooks.py +287 -0
  44. python_getpaid_simulator-3.0.0a3/tests/test_payu_lifecycle.py +171 -0
  45. python_getpaid_simulator-3.0.0a3/tests/test_payu_oauth.py +298 -0
  46. python_getpaid_simulator-3.0.0a3/tests/test_payu_orders.py +78 -0
  47. python_getpaid_simulator-3.0.0a3/tests/test_payu_refunds.py +184 -0
  48. python_getpaid_simulator-3.0.0a3/tests/test_payu_webhooks.py +298 -0
  49. python_getpaid_simulator-3.0.0a3/tests/test_smoke.py +22 -0
  50. python_getpaid_simulator-3.0.0a3/tests/test_state.py +130 -0
  51. python_getpaid_simulator-3.0.0a3/tests/test_storage.py +146 -0
  52. python_getpaid_simulator-3.0.0a3/tests/test_ui_authorize.py +188 -0
  53. python_getpaid_simulator-3.0.0a3/tests/test_ui_dashboard.py +74 -0
  54. python_getpaid_simulator-3.0.0a3/tests/test_webhooks.py +174 -0
@@ -0,0 +1,77 @@
1
+ *.py[cod]
2
+ __pycache__
3
+
4
+ # C extensions
5
+ *.so
6
+
7
+ # Packages
8
+ *.egg
9
+ *.egg-info
10
+ dist
11
+ build
12
+ eggs
13
+ parts
14
+ bin
15
+ var
16
+ sdist
17
+ develop-eggs
18
+ .installed.cfg
19
+ lib
20
+ lib64
21
+
22
+ # Installer logs
23
+ pip-log.txt
24
+
25
+ # Unit test / coverage reports
26
+ .coverage
27
+ coverage.xml
28
+ .tox
29
+ nosetests.xml
30
+ htmlcov
31
+
32
+ # Translations
33
+ *.mo
34
+
35
+ # Mr Developer
36
+ .mr.developer.cfg
37
+ .project
38
+ .pydevproject
39
+
40
+ # Pycharm/Intellij
41
+ .idea
42
+
43
+ # Complexity
44
+ output/*.html
45
+ output/*/index.html
46
+
47
+ # Sphinx
48
+ docs/_build
49
+
50
+ # Default Django Database
51
+ db.sqlite3
52
+
53
+ # pipenv
54
+ /Pipfile
55
+ /Pipfile.lock
56
+
57
+ # pyenv
58
+ .python-version
59
+
60
+ # virtualenv
61
+ venv
62
+ .venv
63
+ virtualenv
64
+ .virtualenv
65
+
66
+ # poetry
67
+ /poetry.lock
68
+
69
+ # Codacy
70
+ /.codacy-coverage
71
+ /codacy-coverage.json
72
+
73
+ # Sublime text (editor)
74
+ *.sublime-project
75
+ *.sublime-workspace
76
+ .sisyphus
77
+ uv.lock
@@ -0,0 +1,29 @@
1
+ FROM python:3.12-slim
2
+
3
+ # Install uv
4
+ RUN pip install --no-cache-dir uv
5
+
6
+ # Create non-root user
7
+ RUN useradd -m -u 1000 simulator
8
+
9
+ # Copy all sibling packages (build context is parent directory)
10
+ COPY getpaid-core/ /app/getpaid-core/
11
+ COPY getpaid-payu/ /app/getpaid-payu/
12
+ COPY getpaid-paynow/ /app/getpaid-paynow/
13
+ COPY getpaid-simulator/ /app/getpaid-simulator/
14
+
15
+ # Set working directory
16
+ WORKDIR /app/getpaid-simulator
17
+
18
+ # Install dependencies (no dev tools, include e2e group for PayU/PayNow)
19
+ RUN uv sync --group e2e --no-dev
20
+
21
+ # Switch to non-root user (AFTER all RUN/COPY)
22
+ USER simulator
23
+
24
+ # Expose port
25
+ EXPOSE 9000
26
+
27
+ # Run simulator with virtual environment
28
+ ENV PATH="/app/getpaid-simulator/.venv/bin:$PATH"
29
+ CMD ["python", "-m", "getpaid_simulator"]
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-getpaid-simulator
3
+ Version: 3.0.0a3
4
+ Summary: Payment gateway simulator for testing the python-getpaid ecosystem.
5
+ Project-URL: Homepage, https://github.com/django-getpaid/python-getpaid-simulator
6
+ Project-URL: Repository, https://github.com/django-getpaid/python-getpaid-simulator
7
+ Author-email: Dominik Kozaczko <dominik@kozaczko.info>
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Office/Business :: Financial
15
+ Classifier: Topic :: Office/Business :: Financial :: Point-Of-Sale
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: httpx>=0.27.0
19
+ Requires-Dist: jinja2>=3.0
20
+ Requires-Dist: litestar>=2.0
21
+ Requires-Dist: uvicorn[standard]>=0.30.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # python-getpaid-simulator
25
+
26
+ Payment gateway simulator host for testing the python-getpaid ecosystem.
27
+
28
+ The simulator no longer owns provider implementations directly. It starts a
29
+ generic Litestar host, discovers provider plugins from installed packages, and
30
+ mounts each provider's API routes and UI flows through entry points.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install python-getpaid-simulator
36
+ ```
37
+
38
+ The host only becomes useful when at least one provider package exposing the
39
+ simulator plugin entry point is installed in the same environment.
40
+
41
+ ## Plugin Model
42
+
43
+ Provider packages must expose an entry point in the
44
+ `getpaid.simulator.providers` group.
45
+
46
+ Example from a provider package:
47
+
48
+ ```toml
49
+ [project.entry-points."getpaid.simulator.providers"]
50
+ payu = "getpaid_payu.simulator:get_plugin"
51
+ ```
52
+
53
+ The entry point must resolve to a factory returning a
54
+ `getpaid_simulator.spi.SimulatorProviderPlugin`.
55
+
56
+ The current stable plugin contract requires:
57
+
58
+ - `api_version`
59
+ - `slug`
60
+ - `display_name`
61
+ - `api_handlers`
62
+ - `ui_handlers`
63
+ - `transitions`
64
+ - `load_config(env)`
65
+ - optional `authorize_path_template`
66
+
67
+ The host validates the plugin slug against the entry point name and rejects
68
+ plugins that do not match `getpaid_simulator.spi.SIMULATOR_PLUGIN_API_VERSION`.
69
+
70
+ ## Startup Failure Modes
71
+
72
+ Plugin loading is controlled by `SIMULATOR_PLUGIN_FAILURE_MODE` or the CLI flag
73
+ `--plugin-failure-mode`.
74
+
75
+ - `warn` (default): broken plugins are skipped, the simulator starts in
76
+ degraded mode, and failed providers appear in logs, `/`, `/sim/status`, and
77
+ the dashboard.
78
+ - `strict`: the first plugin import, factory, compatibility, or config failure
79
+ aborts startup with `PluginLoadError`.
80
+
81
+ Examples:
82
+
83
+ ```bash
84
+ getpaid-simulator --plugin-failure-mode strict
85
+ SIMULATOR_PLUGIN_FAILURE_MODE=warn getpaid-simulator
86
+ ```
87
+
88
+ ## Provider Packaging
89
+
90
+ Provider packages should keep simulator support in an optional dependency group
91
+ such as `simulator`, rather than forcing simulator host and Litestar
92
+ dependencies into normal runtime installs.
93
+
94
+ Example:
95
+
96
+ ```toml
97
+ [project.optional-dependencies]
98
+ simulator = [
99
+ "python-getpaid-simulator>=3.0.0a3",
100
+ "litestar>=2.0",
101
+ ]
102
+ ```
103
+
104
+ ## Development Notes
105
+
106
+ - Wave 1 storage remains in-memory behind the host storage interface.
107
+ - Provider-local simulator code lives in provider repositories, not under
108
+ `getpaid_simulator.providers`.
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,89 @@
1
+ # python-getpaid-simulator
2
+
3
+ Payment gateway simulator host for testing the python-getpaid ecosystem.
4
+
5
+ The simulator no longer owns provider implementations directly. It starts a
6
+ generic Litestar host, discovers provider plugins from installed packages, and
7
+ mounts each provider's API routes and UI flows through entry points.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install python-getpaid-simulator
13
+ ```
14
+
15
+ The host only becomes useful when at least one provider package exposing the
16
+ simulator plugin entry point is installed in the same environment.
17
+
18
+ ## Plugin Model
19
+
20
+ Provider packages must expose an entry point in the
21
+ `getpaid.simulator.providers` group.
22
+
23
+ Example from a provider package:
24
+
25
+ ```toml
26
+ [project.entry-points."getpaid.simulator.providers"]
27
+ payu = "getpaid_payu.simulator:get_plugin"
28
+ ```
29
+
30
+ The entry point must resolve to a factory returning a
31
+ `getpaid_simulator.spi.SimulatorProviderPlugin`.
32
+
33
+ The current stable plugin contract requires:
34
+
35
+ - `api_version`
36
+ - `slug`
37
+ - `display_name`
38
+ - `api_handlers`
39
+ - `ui_handlers`
40
+ - `transitions`
41
+ - `load_config(env)`
42
+ - optional `authorize_path_template`
43
+
44
+ The host validates the plugin slug against the entry point name and rejects
45
+ plugins that do not match `getpaid_simulator.spi.SIMULATOR_PLUGIN_API_VERSION`.
46
+
47
+ ## Startup Failure Modes
48
+
49
+ Plugin loading is controlled by `SIMULATOR_PLUGIN_FAILURE_MODE` or the CLI flag
50
+ `--plugin-failure-mode`.
51
+
52
+ - `warn` (default): broken plugins are skipped, the simulator starts in
53
+ degraded mode, and failed providers appear in logs, `/`, `/sim/status`, and
54
+ the dashboard.
55
+ - `strict`: the first plugin import, factory, compatibility, or config failure
56
+ aborts startup with `PluginLoadError`.
57
+
58
+ Examples:
59
+
60
+ ```bash
61
+ getpaid-simulator --plugin-failure-mode strict
62
+ SIMULATOR_PLUGIN_FAILURE_MODE=warn getpaid-simulator
63
+ ```
64
+
65
+ ## Provider Packaging
66
+
67
+ Provider packages should keep simulator support in an optional dependency group
68
+ such as `simulator`, rather than forcing simulator host and Litestar
69
+ dependencies into normal runtime installs.
70
+
71
+ Example:
72
+
73
+ ```toml
74
+ [project.optional-dependencies]
75
+ simulator = [
76
+ "python-getpaid-simulator>=3.0.0a3",
77
+ "litestar>=2.0",
78
+ ]
79
+ ```
80
+
81
+ ## Development Notes
82
+
83
+ - Wave 1 storage remains in-memory behind the host storage interface.
84
+ - Provider-local simulator code lives in provider repositories, not under
85
+ `getpaid_simulator.providers`.
86
+
87
+ ## License
88
+
89
+ MIT
@@ -0,0 +1,18 @@
1
+ version: "3.9"
2
+
3
+ services:
4
+ simulator:
5
+ build:
6
+ context: ..
7
+ dockerfile: getpaid-simulator/Dockerfile
8
+ ports:
9
+ - "9000:9000"
10
+ environment:
11
+ - SIMULATOR_HOST=0.0.0.0
12
+ - SIMULATOR_PORT=9000
13
+ - SIMULATOR_LOG_LEVEL=info
14
+ healthcheck:
15
+ test: ["CMD", "curl", "-f", "http://localhost:9000/sim/dashboard"]
16
+ interval: 10s
17
+ timeout: 5s
18
+ retries: 3
@@ -0,0 +1,125 @@
1
+ [project]
2
+ name = 'python-getpaid-simulator'
3
+ dynamic = ["version"]
4
+ description = 'Payment gateway simulator for testing the python-getpaid ecosystem.'
5
+ readme = 'README.md'
6
+ license = {text = 'MIT'}
7
+ authors = [
8
+ {name = 'Dominik Kozaczko', email = 'dominik@kozaczko.info'},
9
+ ]
10
+ requires-python = '>=3.12'
11
+ classifiers = [
12
+ 'Development Status :: 3 - Alpha',
13
+ 'Intended Audience :: Developers',
14
+ 'License :: OSI Approved :: MIT License',
15
+ 'Programming Language :: Python :: 3.12',
16
+ 'Programming Language :: Python :: 3.13',
17
+ 'Topic :: Office/Business :: Financial',
18
+ 'Topic :: Office/Business :: Financial :: Point-Of-Sale',
19
+ 'Typing :: Typed',
20
+ ]
21
+ dependencies = [
22
+ 'litestar>=2.0',
23
+ 'jinja2>=3.0',
24
+ 'httpx>=0.27.0',
25
+ 'uvicorn[standard]>=0.30.0',
26
+ ]
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ 'pytest>=8.0',
31
+ 'pytest-asyncio>=0.24.0',
32
+ 'pytest-cov>=5.0',
33
+ 'ruff>=0.9.0',
34
+ 'pre-commit>=4.0',
35
+ 'ty>=0.0.16',
36
+ 'litestar[testing]',
37
+ 'respx>=0.21.0',
38
+ "playwright>=1.40.0",
39
+ 'python-getpaid-payu>=3.0.0a3',
40
+ 'python-getpaid-paynow>=3.0.0a3',
41
+ ]
42
+ e2e = [
43
+ {include-group = "dev"},
44
+ 'python-getpaid-core>=3.0.0a3',
45
+ 'python-getpaid-payu',
46
+ 'python-getpaid-paynow',
47
+ ]
48
+
49
+ [project.scripts]
50
+ getpaid-simulator = "getpaid_simulator.__main__:main"
51
+
52
+ [project.urls]
53
+ Homepage = 'https://github.com/django-getpaid/python-getpaid-simulator'
54
+ Repository = 'https://github.com/django-getpaid/python-getpaid-simulator'
55
+
56
+ [build-system]
57
+ requires = ['hatchling']
58
+ build-backend = 'hatchling.build'
59
+
60
+ [tool.hatch.build.targets.wheel]
61
+ packages = ['src/getpaid_simulator']
62
+
63
+ [tool.hatch.version]
64
+ path = "src/getpaid_simulator/__init__.py"
65
+
66
+ [tool.uv.sources]
67
+ python-getpaid-core = { path = "../getpaid-core", editable = true }
68
+ python-getpaid-payu = { path = "../getpaid-payu", editable = true }
69
+ python-getpaid-paynow = { path = "../getpaid-paynow", editable = true }
70
+
71
+ [tool.pytest.ini_options]
72
+ testpaths = ['tests']
73
+ asyncio_mode = 'auto'
74
+
75
+ [tool.coverage.run]
76
+ branch = true
77
+ source = ['getpaid_simulator']
78
+
79
+ [tool.coverage.report]
80
+ show_missing = true
81
+
82
+ [tool.ruff]
83
+ target-version = 'py312'
84
+ line-length = 80
85
+ src = ['src', 'tests']
86
+
87
+ [tool.ruff.lint]
88
+ select = [
89
+ 'E', # pycodestyle errors
90
+ 'W', # pycodestyle warnings
91
+ 'F', # pyflakes
92
+ 'I', # isort
93
+ 'N', # pep8-naming
94
+ 'UP', # pyupgrade
95
+ 'B', # flake8-bugbear
96
+ 'A', # flake8-builtins
97
+ 'SIM', # flake8-simplify
98
+ 'TC', # type-checking imports
99
+ 'RUF', # ruff-specific
100
+ ]
101
+ ignore = [
102
+ 'N818', # Exception names match getpaid-core convention
103
+ 'B027', # Empty methods in ABC are intentional default no-ops
104
+ ]
105
+
106
+ [tool.ruff.lint.per-file-ignores]
107
+ 'tests/**' = ['TC001', 'RUF012']
108
+
109
+ [tool.ruff.lint.isort]
110
+ force-single-line = true
111
+ lines-after-imports = 2
112
+ known-first-party = ['getpaid_simulator']
113
+
114
+ # --- ty type checker ---
115
+ [tool.ty.environment]
116
+ python-version = '3.12'
117
+
118
+ [tool.ty.terminal]
119
+ error-on-warning = true
120
+
121
+ [[tool.ty.overrides]]
122
+ include = ['tests/**']
123
+ [tool.ty.overrides.rules]
124
+ unresolved-attribute = 'ignore'
125
+ invalid-argument-type = 'ignore'
@@ -0,0 +1,3 @@
1
+ """Payment gateway simulator for testing the python-getpaid ecosystem."""
2
+
3
+ __version__ = "3.0.0a3"
@@ -0,0 +1,112 @@
1
+ """CLI entry point for getpaid-simulator."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ import uvicorn
7
+
8
+ from getpaid_simulator import __version__
9
+ from getpaid_simulator.app import create_app
10
+ from getpaid_simulator.core.config import SimulatorConfig
11
+ from getpaid_simulator.plugins import load_provider_plugins
12
+
13
+
14
+ def _print_startup_banner(
15
+ config: SimulatorConfig,
16
+ loaded_providers: list[str],
17
+ failed_providers: list[str],
18
+ ) -> None:
19
+ """Print startup banner with discovered providers and dashboard URL.
20
+
21
+ Args:
22
+ config: Simulator configuration.
23
+ loaded_providers: Display names for loaded simulator plugins.
24
+ failed_providers: Slugs for failed plugins.
25
+ """
26
+ providers_str = ", ".join(loaded_providers) if loaded_providers else "none"
27
+ failed_str = ", ".join(failed_providers) if failed_providers else "none"
28
+
29
+ dashboard_url = f"http://{config.host}:{config.port}/sim/"
30
+ status_label = "DEGRADED" if failed_providers else "READY"
31
+
32
+ banner = f"""
33
+ ╔══════════════════════════════════════╗
34
+ ║ 🔶 getpaid-simulator v{__version__:<18}║
35
+ ║ ⚠ SIMULATOR — NOT REAL PAYMENTS ║
36
+ ║ Status: {status_label:<27}║
37
+ ║ Providers: {providers_str:<23}║
38
+ ║ Failed: {failed_str:<26}║
39
+ ║ Dashboard: {dashboard_url:<23}║
40
+ ╚══════════════════════════════════════╝
41
+ """
42
+ print(banner)
43
+
44
+
45
+ def main() -> None:
46
+ """CLI entry point for getpaid-simulator."""
47
+ parser = argparse.ArgumentParser(
48
+ description="GetPaid Payment Gateway Simulator",
49
+ prog="getpaid-simulator",
50
+ )
51
+ parser.add_argument(
52
+ "--host",
53
+ default=None,
54
+ help="Server host (default: 0.0.0.0)",
55
+ )
56
+ parser.add_argument(
57
+ "--port",
58
+ type=int,
59
+ default=None,
60
+ help="Server port (default: 9000)",
61
+ )
62
+ parser.add_argument(
63
+ "--log-level",
64
+ choices=["DEBUG", "INFO", "WARNING", "ERROR"],
65
+ default=None,
66
+ help="Logging level (default: INFO)",
67
+ )
68
+ parser.add_argument(
69
+ "--plugin-failure-mode",
70
+ choices=["strict", "warn"],
71
+ default=None,
72
+ help="Plugin failure mode (default: warn)",
73
+ )
74
+
75
+ args = parser.parse_args()
76
+
77
+ # Load config from environment variables
78
+ config = SimulatorConfig.from_env()
79
+
80
+ # Override with CLI arguments if provided
81
+ if args.host is not None:
82
+ config.host = args.host
83
+ if args.port is not None:
84
+ config.port = args.port
85
+ if args.log_level is not None:
86
+ config.log_level = args.log_level
87
+ if args.plugin_failure_mode is not None:
88
+ config.plugin_failure_mode = args.plugin_failure_mode
89
+
90
+ plugin_load_result = load_provider_plugins(config)
91
+ _print_startup_banner(
92
+ config,
93
+ loaded_providers=[
94
+ plugin.display_name for plugin in plugin_load_result.loaded_plugins
95
+ ],
96
+ failed_providers=[
97
+ failure.slug for failure in plugin_load_result.failed_plugins
98
+ ],
99
+ )
100
+
101
+ # Create and run app
102
+ app = create_app(config, plugin_load_result)
103
+ uvicorn.run(
104
+ app,
105
+ host=config.host,
106
+ port=config.port,
107
+ log_level=config.log_level.lower(),
108
+ )
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,130 @@
1
+ """Litestar application for payment gateway simulator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ from litestar import Litestar
9
+ from litestar import get
10
+ from litestar.contrib.jinja import JinjaTemplateEngine
11
+ from litestar.datastructures import State
12
+ from litestar.template.config import TemplateConfig
13
+
14
+ from getpaid_simulator.core.config import SimulatorConfig
15
+ from getpaid_simulator.core.state import InvalidTransitionError
16
+ from getpaid_simulator.core.state import PaymentStateMachine
17
+ from getpaid_simulator.core.storage import SimulatorStorage
18
+ from getpaid_simulator.core.webhooks import WebhookTransport
19
+ from getpaid_simulator.plugins import PluginLoadResult
20
+ from getpaid_simulator.plugins import load_provider_plugins
21
+ from getpaid_simulator.ui import routes as ui_routes
22
+
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ @get("/")
28
+ async def health(state: State) -> dict[str, object]:
29
+ """Health check endpoint."""
30
+ loaded_plugins = list(state.loaded_plugins)
31
+ failed_plugins = [failure.slug for failure in state.failed_plugins]
32
+ status = "degraded" if failed_plugins else "ok"
33
+ return {
34
+ "status": status,
35
+ "service": "getpaid-simulator",
36
+ "loadedProviders": loaded_plugins,
37
+ "failedProviders": failed_plugins,
38
+ }
39
+
40
+
41
+ @get("/sim/status")
42
+ async def simulator_status(state: State) -> dict[str, object]:
43
+ """Detailed status endpoint for the simulator host."""
44
+ return {
45
+ "status": "degraded" if state.failed_plugins else "ok",
46
+ "loadedProviders": [
47
+ {
48
+ "slug": slug,
49
+ "displayName": plugin.display_name,
50
+ }
51
+ for slug, plugin in state.loaded_plugins.items()
52
+ ],
53
+ "failedProviders": [
54
+ {
55
+ "slug": failure.slug,
56
+ "stage": failure.stage,
57
+ "error": failure.error,
58
+ }
59
+ for failure in state.failed_plugins
60
+ ],
61
+ }
62
+
63
+
64
+ def create_app(
65
+ config: SimulatorConfig | None = None,
66
+ plugin_load_result: PluginLoadResult | None = None,
67
+ ) -> Litestar:
68
+ config = config or SimulatorConfig.from_env()
69
+ plugin_load_result = plugin_load_result or load_provider_plugins(config)
70
+ loaded_plugins = {
71
+ plugin.slug: plugin for plugin in plugin_load_result.loaded_plugins
72
+ }
73
+ state_machine = PaymentStateMachine(SimulatorStorage())
74
+ for plugin in plugin_load_result.loaded_plugins:
75
+ state_machine.register_provider(plugin.slug, plugin.transitions)
76
+
77
+ route_handlers = [health, simulator_status, ui_routes.dashboard]
78
+ for plugin in plugin_load_result.loaded_plugins:
79
+ route_handlers.extend(plugin.api_handlers)
80
+ route_handlers.extend(plugin.ui_handlers)
81
+
82
+ loaded_display_names = [
83
+ plugin.display_name for plugin in plugin_load_result.loaded_plugins
84
+ ]
85
+ failed_slugs = [
86
+ failure.slug for failure in plugin_load_result.failed_plugins
87
+ ]
88
+ if loaded_display_names:
89
+ logger.info(
90
+ "Loaded simulator plugins: %s",
91
+ ", ".join(loaded_display_names),
92
+ )
93
+ else:
94
+ logger.info("Loaded simulator plugins: none")
95
+ if failed_slugs:
96
+ logger.warning(
97
+ "Failed simulator plugins: %s",
98
+ ", ".join(failed_slugs),
99
+ )
100
+
101
+ storage = state_machine.storage
102
+ webhook_transport = WebhookTransport(
103
+ timeout=config.webhook_timeout,
104
+ max_retries=config.webhook_max_retries,
105
+ )
106
+ state = State(
107
+ {
108
+ "storage": storage,
109
+ "state_machine": state_machine,
110
+ "webhook_transport": webhook_transport,
111
+ "config": config,
112
+ "loaded_plugins": loaded_plugins,
113
+ "provider_configs": plugin_load_result.provider_configs,
114
+ "failed_plugins": plugin_load_result.failed_plugins,
115
+ "invalid_transition_error": InvalidTransitionError,
116
+ }
117
+ )
118
+
119
+ return Litestar(
120
+ route_handlers=route_handlers,
121
+ state=state,
122
+ template_config=TemplateConfig(
123
+ engine=JinjaTemplateEngine(
124
+ directory=Path(__file__).parent / "ui" / "templates"
125
+ )
126
+ ),
127
+ )
128
+
129
+
130
+ app = create_app()
@@ -0,0 +1 @@
1
+ """Core simulator components."""