smx-commerce 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.
Files changed (131) hide show
  1. smx_commerce-0.1.0/MANIFEST.in +30 -0
  2. smx_commerce-0.1.0/PKG-INFO +13 -0
  3. smx_commerce-0.1.0/README.md +88 -0
  4. smx_commerce-0.1.0/pyproject.toml +33 -0
  5. smx_commerce-0.1.0/setup.cfg +4 -0
  6. smx_commerce-0.1.0/src/smx_commerce/__init__.py +214 -0
  7. smx_commerce-0.1.0/src/smx_commerce/admin/__init__.py +31 -0
  8. smx_commerce-0.1.0/src/smx_commerce/admin/amounts.py +51 -0
  9. smx_commerce-0.1.0/src/smx_commerce/admin/auth.py +115 -0
  10. smx_commerce-0.1.0/src/smx_commerce/admin/category_edit.py +78 -0
  11. smx_commerce-0.1.0/src/smx_commerce/admin/home.py +13 -0
  12. smx_commerce-0.1.0/src/smx_commerce/admin/order_edit.py +87 -0
  13. smx_commerce-0.1.0/src/smx_commerce/admin/price_edit.py +79 -0
  14. smx_commerce-0.1.0/src/smx_commerce/admin/product_edit.py +72 -0
  15. smx_commerce-0.1.0/src/smx_commerce/admin/routes.py +32 -0
  16. smx_commerce-0.1.0/src/smx_commerce/admin/safe_delete.py +263 -0
  17. smx_commerce-0.1.0/src/smx_commerce/catalog/__init__.py +29 -0
  18. smx_commerce-0.1.0/src/smx_commerce/catalog/models.py +120 -0
  19. smx_commerce-0.1.0/src/smx_commerce/catalog/objects.py +225 -0
  20. smx_commerce-0.1.0/src/smx_commerce/catalog/repository.py +506 -0
  21. smx_commerce-0.1.0/src/smx_commerce/catalog/routes_admin.py +484 -0
  22. smx_commerce-0.1.0/src/smx_commerce/catalog/routes_public.py +100 -0
  23. smx_commerce-0.1.0/src/smx_commerce/catalog/services.py +68 -0
  24. smx_commerce-0.1.0/src/smx_commerce/checkout/__init__.py +12 -0
  25. smx_commerce-0.1.0/src/smx_commerce/checkout/models.py +50 -0
  26. smx_commerce-0.1.0/src/smx_commerce/checkout/objects.py +80 -0
  27. smx_commerce-0.1.0/src/smx_commerce/checkout/repository.py +212 -0
  28. smx_commerce-0.1.0/src/smx_commerce/checkout/routes.py +181 -0
  29. smx_commerce-0.1.0/src/smx_commerce/checkout/routes_admin.py +178 -0
  30. smx_commerce-0.1.0/src/smx_commerce/checkout/services.py +45 -0
  31. smx_commerce-0.1.0/src/smx_commerce/cli.py +104 -0
  32. smx_commerce-0.1.0/src/smx_commerce/core/__init__.py +17 -0
  33. smx_commerce-0.1.0/src/smx_commerce/core/config.py +37 -0
  34. smx_commerce-0.1.0/src/smx_commerce/core/db.py +42 -0
  35. smx_commerce-0.1.0/src/smx_commerce/core/runtime.py +62 -0
  36. smx_commerce-0.1.0/src/smx_commerce/core/schema.py +39 -0
  37. smx_commerce-0.1.0/src/smx_commerce/core/settings_models.py +30 -0
  38. smx_commerce-0.1.0/src/smx_commerce/core/settings_repository.py +69 -0
  39. smx_commerce-0.1.0/src/smx_commerce/env_config.py +116 -0
  40. smx_commerce-0.1.0/src/smx_commerce/notifications/__init__.py +17 -0
  41. smx_commerce-0.1.0/src/smx_commerce/notifications/emailer.py +68 -0
  42. smx_commerce-0.1.0/src/smx_commerce/notifications/memory_sender.py +11 -0
  43. smx_commerce-0.1.0/src/smx_commerce/notifications/smtp_sender.py +55 -0
  44. smx_commerce-0.1.0/src/smx_commerce/payments/__init__.py +26 -0
  45. smx_commerce-0.1.0/src/smx_commerce/payments/checkout.py +29 -0
  46. smx_commerce-0.1.0/src/smx_commerce/payments/local.py +57 -0
  47. smx_commerce-0.1.0/src/smx_commerce/payments/models.py +36 -0
  48. smx_commerce-0.1.0/src/smx_commerce/payments/objects.py +37 -0
  49. smx_commerce-0.1.0/src/smx_commerce/payments/repository.py +144 -0
  50. smx_commerce-0.1.0/src/smx_commerce/payments/routes.py +114 -0
  51. smx_commerce-0.1.0/src/smx_commerce/payments/services.py +84 -0
  52. smx_commerce-0.1.0/src/smx_commerce/payments/stripe_checkout.py +99 -0
  53. smx_commerce-0.1.0/src/smx_commerce/payments/stripe_verifier.py +62 -0
  54. smx_commerce-0.1.0/src/smx_commerce/payments/verifiers.py +28 -0
  55. smx_commerce-0.1.0/src/smx_commerce/settings.py +51 -0
  56. smx_commerce-0.1.0/src/smx_commerce/templates/admin/categories_list.html +135 -0
  57. smx_commerce-0.1.0/src/smx_commerce/templates/admin/category_edit.html +80 -0
  58. smx_commerce-0.1.0/src/smx_commerce/templates/admin/login.html +44 -0
  59. smx_commerce-0.1.0/src/smx_commerce/templates/admin/order_edit.html +90 -0
  60. smx_commerce-0.1.0/src/smx_commerce/templates/admin/orders_list.html +85 -0
  61. smx_commerce-0.1.0/src/smx_commerce/templates/admin/price_edit.html +88 -0
  62. smx_commerce-0.1.0/src/smx_commerce/templates/admin/product_detail.html +147 -0
  63. smx_commerce-0.1.0/src/smx_commerce/templates/admin/product_edit.html +93 -0
  64. smx_commerce-0.1.0/src/smx_commerce/templates/admin/products_list.html +150 -0
  65. smx_commerce-0.1.0/src/smx_commerce/templates/public/checkout_cancel.html +35 -0
  66. smx_commerce-0.1.0/src/smx_commerce/templates/public/checkout_success.html +41 -0
  67. smx_commerce-0.1.0/src/smx_commerce/templates/public/commerce_home.html +35 -0
  68. smx_commerce-0.1.0/src/smx_commerce/templates/public/product_detail.html +98 -0
  69. smx_commerce-0.1.0/src/smx_commerce/templates/public/product_list.html +44 -0
  70. smx_commerce-0.1.0/src/smx_commerce.egg-info/PKG-INFO +13 -0
  71. smx_commerce-0.1.0/src/smx_commerce.egg-info/SOURCES.txt +129 -0
  72. smx_commerce-0.1.0/src/smx_commerce.egg-info/dependency_links.txt +1 -0
  73. smx_commerce-0.1.0/src/smx_commerce.egg-info/entry_points.txt +2 -0
  74. smx_commerce-0.1.0/src/smx_commerce.egg-info/requires.txt +11 -0
  75. smx_commerce-0.1.0/src/smx_commerce.egg-info/top_level.txt +1 -0
  76. smx_commerce-0.1.0/tests/test_admin_auth.py +63 -0
  77. smx_commerce-0.1.0/tests/test_admin_categories_html.py +111 -0
  78. smx_commerce-0.1.0/tests/test_admin_category_edit_form.py +130 -0
  79. smx_commerce-0.1.0/tests/test_admin_home_route.py +76 -0
  80. smx_commerce-0.1.0/tests/test_admin_login_navigation.py +73 -0
  81. smx_commerce-0.1.0/tests/test_admin_order_edit_form.py +179 -0
  82. smx_commerce-0.1.0/tests/test_admin_order_safe_delete.py +183 -0
  83. smx_commerce-0.1.0/tests/test_admin_orders_html.py +125 -0
  84. smx_commerce-0.1.0/tests/test_admin_price_edit_form.py +135 -0
  85. smx_commerce-0.1.0/tests/test_admin_price_major_amount_forms.py +151 -0
  86. smx_commerce-0.1.0/tests/test_admin_product_categories_form.py +122 -0
  87. smx_commerce-0.1.0/tests/test_admin_product_create_form.py +101 -0
  88. smx_commerce-0.1.0/tests/test_admin_product_detail_price_form.py +129 -0
  89. smx_commerce-0.1.0/tests/test_admin_product_edit_form.py +126 -0
  90. smx_commerce-0.1.0/tests/test_admin_products_html.py +72 -0
  91. smx_commerce-0.1.0/tests/test_admin_safe_delete_feedback.py +164 -0
  92. smx_commerce-0.1.0/tests/test_admin_safe_delete_routes.py +191 -0
  93. smx_commerce-0.1.0/tests/test_admin_safe_delete_ui.py +197 -0
  94. smx_commerce-0.1.0/tests/test_admin_session_auth.py +94 -0
  95. smx_commerce-0.1.0/tests/test_catalog_objects.py +113 -0
  96. smx_commerce-0.1.0/tests/test_catalog_service.py +99 -0
  97. smx_commerce-0.1.0/tests/test_category_admin_routes.py +105 -0
  98. smx_commerce-0.1.0/tests/test_category_repository.py +119 -0
  99. smx_commerce-0.1.0/tests/test_checkout_redirect_html.py +109 -0
  100. smx_commerce-0.1.0/tests/test_checkout_redirect_routes.py +115 -0
  101. smx_commerce-0.1.0/tests/test_checkout_routes.py +183 -0
  102. smx_commerce-0.1.0/tests/test_checkout_service.py +104 -0
  103. smx_commerce-0.1.0/tests/test_checkout_session_provider.py +116 -0
  104. smx_commerce-0.1.0/tests/test_cli_schema_commands.py +47 -0
  105. smx_commerce-0.1.0/tests/test_commerce_runtime.py +70 -0
  106. smx_commerce-0.1.0/tests/test_commerce_settings_repository.py +49 -0
  107. smx_commerce-0.1.0/tests/test_init_commerce_api.py +88 -0
  108. smx_commerce-0.1.0/tests/test_init_commerce_config_modes.py +175 -0
  109. smx_commerce-0.1.0/tests/test_order_admin_routes.py +136 -0
  110. smx_commerce-0.1.0/tests/test_order_confirmation_email_service.py +66 -0
  111. smx_commerce-0.1.0/tests/test_order_repository.py +176 -0
  112. smx_commerce-0.1.0/tests/test_orders_csv_export.py +113 -0
  113. smx_commerce-0.1.0/tests/test_package_contract_workflow.py +202 -0
  114. smx_commerce-0.1.0/tests/test_payment_webhook_routes.py +196 -0
  115. smx_commerce-0.1.0/tests/test_payment_webhook_service.py +130 -0
  116. smx_commerce-0.1.0/tests/test_price_admin_routes.py +137 -0
  117. smx_commerce-0.1.0/tests/test_product_admin_routes.py +124 -0
  118. smx_commerce-0.1.0/tests/test_product_price_repository.py +180 -0
  119. smx_commerce-0.1.0/tests/test_product_repository.py +147 -0
  120. smx_commerce-0.1.0/tests/test_public_checkout_form.py +92 -0
  121. smx_commerce-0.1.0/tests/test_public_commerce_home_navigation.py +51 -0
  122. smx_commerce-0.1.0/tests/test_public_navigation_shell.py +108 -0
  123. smx_commerce-0.1.0/tests/test_public_product_html_routes.py +102 -0
  124. smx_commerce-0.1.0/tests/test_public_product_navigation.py +67 -0
  125. smx_commerce-0.1.0/tests/test_public_product_routes.py +130 -0
  126. smx_commerce-0.1.0/tests/test_schema_readiness.py +31 -0
  127. smx_commerce-0.1.0/tests/test_settings_admin_routes.py +59 -0
  128. smx_commerce-0.1.0/tests/test_smtp_email_sender.py +118 -0
  129. smx_commerce-0.1.0/tests/test_stripe_checkout_provider.py +106 -0
  130. smx_commerce-0.1.0/tests/test_stripe_webhook_verifier.py +15 -0
  131. smx_commerce-0.1.0/tests/test_webhook_order_confirmation_integration.py +195 -0
@@ -0,0 +1,30 @@
1
+ include README.md
2
+ include LICENSE
3
+ include pyproject.toml
4
+
5
+ recursive-include src/smx_commerce/templates *.html
6
+
7
+ recursive-exclude * __pycache__
8
+ recursive-exclude * *.py[cod]
9
+ recursive-exclude * .DS_Store
10
+ recursive-exclude * Thumbs.db
11
+
12
+ exclude .env
13
+ exclude .env.*
14
+ exclude *.db
15
+ exclude *.sqlite
16
+ exclude *.sqlite3
17
+
18
+ exclude patch_*.py
19
+ exclude repair_*.py
20
+ exclude fix_*.py
21
+ exclude inspect_*.py
22
+ exclude _tmp_*.py
23
+
24
+ prune .venv
25
+ prune venv
26
+ prune env
27
+ prune build
28
+ prune dist
29
+ prune data
30
+ prune tests/__pycache__
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: smx-commerce
3
+ Version: 0.1.0
4
+ Summary: Independent commerce package for Flask and SyntaxMatrix-style applications.
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: Flask>=2.3
7
+ Requires-Dist: SQLAlchemy>=2.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest>=8.0; extra == "dev"
10
+ Provides-Extra: stripe
11
+ Requires-Dist: stripe>=8.0; extra == "stripe"
12
+ Provides-Extra: postgres
13
+ Requires-Dist: psycopg[binary]>=3.1; extra == "postgres"
@@ -0,0 +1,88 @@
1
+ # smx-commerce
2
+
3
+ This is a minimal customer-facing demo app showing how a client project can install and initialize `smx-commerce`.
4
+
5
+ ## Admin token and private admin entry point
6
+
7
+ Public visitors should not see an Admin button or Admin navigation on the commerce pages.
8
+
9
+ Admins enter the panel directly through:
10
+
11
+ ```text
12
+ /commerce/admin
13
+ ```
14
+
15
+ If the project has an admin key configured, the system redirects the admin to:
16
+
17
+ ```text
18
+ /commerce/admin/login
19
+ ```
20
+
21
+ The admin must then enter the configured admin key.
22
+
23
+ ### Who creates the admin key?
24
+
25
+ The project owner creates the admin key. `smx-commerce` does not invent a hidden token.
26
+
27
+ For local development, copy:
28
+
29
+ ```text
30
+ .env.smx-commerce.example
31
+ ```
32
+
33
+ to:
34
+
35
+ ```text
36
+ .env.smx-commerce
37
+ ```
38
+
39
+ Then set:
40
+
41
+ ```text
42
+ SMX_COMMERCE_ADMIN_API_KEY=<your-admin-token>
43
+ SMX_COMMERCE_FLASK_SECRET_KEY=<your-session-secret>
44
+ ```
45
+
46
+ Generate strong values with:
47
+
48
+ ```powershell
49
+ python -c "import secrets; print(secrets.token_urlsafe(32))"
50
+ ```
51
+
52
+ Use one generated value for `SMX_COMMERCE_ADMIN_API_KEY` and another generated value for `SMX_COMMERCE_FLASK_SECRET_KEY`.
53
+
54
+ ### Local demo example
55
+
56
+ For this local demo, the admin page is:
57
+
58
+ ```text
59
+ http://127.0.0.1:5055/commerce/admin
60
+ ```
61
+
62
+ The local key is whatever you set in:
63
+
64
+ ```text
65
+ .env.smx-commerce
66
+ ```
67
+
68
+ ### Cloud deployment
69
+
70
+ For cloud deployment, set the same values as environment variables or secrets in the hosting platform.
71
+
72
+ Example names:
73
+
74
+ ```text
75
+ SMX_COMMERCE_ADMIN_API_KEY
76
+ SMX_COMMERCE_FLASK_SECRET_KEY
77
+ ```
78
+
79
+ Recommended practice:
80
+
81
+ ```text
82
+ - Store them in a secret manager.
83
+ - Do not hardcode them in source code.
84
+ - Do not commit them to Git.
85
+ - Rotate the admin key if it is exposed.
86
+ ```
87
+
88
+ After changing secrets, restart the app or service so the new values are loaded.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "smx-commerce"
7
+ version = "0.1.0"
8
+ description = "Independent commerce package for Flask and SyntaxMatrix-style applications."
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "Flask>=2.3",
12
+ "SQLAlchemy>=2.0",
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ dev = [
17
+ "pytest>=8.0",
18
+ ]
19
+ stripe = [
20
+ "stripe>=8.0",
21
+ ]
22
+ postgres = [
23
+ "psycopg[binary]>=3.1",
24
+ ]
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
28
+
29
+ [project.scripts]
30
+ smx-commerce = "smx_commerce.cli:main"
31
+
32
+ [tool.setuptools.package-data]
33
+ smx_commerce = ["templates/**/*.html"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+
3
+ from flask import Blueprint, render_template
4
+
5
+ from smx_commerce.admin import apply_admin_api_key_guard, create_admin_auth_blueprint, create_admin_home_blueprint, create_settings_admin_blueprint, create_product_edit_admin_blueprint, create_price_edit_admin_blueprint, create_category_edit_admin_blueprint, create_safe_delete_admin_blueprint, create_order_edit_admin_blueprint, create_product_edit_admin_blueprint
6
+ from smx_commerce.catalog.routes_admin import (
7
+ create_category_admin_blueprint,
8
+ create_price_admin_blueprint,
9
+ create_product_admin_blueprint,
10
+ )
11
+ from smx_commerce.catalog.routes_public import create_public_catalog_blueprint
12
+ from smx_commerce.checkout.routes import create_checkout_blueprint
13
+ from smx_commerce.checkout.routes_admin import create_order_admin_blueprint
14
+ from smx_commerce.core import CommerceRuntime
15
+ from smx_commerce.env_config import build_commerce_config_from_env, load_env_file
16
+ from smx_commerce.notifications import (
17
+ MemoryEmailSender,
18
+ OrderConfirmationEmailService,
19
+ SMTPEmailSender,
20
+ )
21
+ from smx_commerce.payments import (
22
+ LocalCheckoutProvider,
23
+ PaymentCheckoutProvider,
24
+ StaticSignatureWebhookVerifier,
25
+ StripeCheckoutProvider,
26
+ )
27
+ from smx_commerce.payments.routes import create_payment_webhook_blueprint
28
+ from smx_commerce.payments.verifiers import PaymentWebhookVerifier
29
+
30
+
31
+ def create_commerce_blueprint(
32
+ config=None,
33
+ runtime: CommerceRuntime | None = None,
34
+ init_schema: bool = False,
35
+ payment_webhook_verifier: PaymentWebhookVerifier | None = None,
36
+ payment_checkout_provider: PaymentCheckoutProvider | None = None,
37
+ order_confirmation_service: OrderConfirmationEmailService | None = None,
38
+ admin_api_key: str | None = None,
39
+ ):
40
+ commerce_runtime = runtime or CommerceRuntime.from_mapping(config)
41
+
42
+ if init_schema:
43
+ commerce_runtime.init_schema()
44
+
45
+ resolved_admin_api_key = admin_api_key
46
+ if resolved_admin_api_key is None:
47
+ resolved_admin_api_key = commerce_runtime.config.admin_api_key
48
+
49
+ bp = Blueprint("smx_commerce", __name__)
50
+
51
+ @bp.get("/commerce/health")
52
+ def commerce_health():
53
+ return {
54
+ "status": "ok",
55
+ "package": "smx-commerce",
56
+ }
57
+
58
+ @bp.get("/commerce")
59
+ def commerce_home():
60
+ return render_template("public/commerce_home.html")
61
+
62
+ bp.register_blueprint(create_public_catalog_blueprint(commerce_runtime))
63
+ bp.register_blueprint(
64
+ create_checkout_blueprint(
65
+ commerce_runtime,
66
+ payment_checkout_provider=payment_checkout_provider,
67
+ )
68
+ )
69
+ bp.register_blueprint(
70
+ create_payment_webhook_blueprint(
71
+ commerce_runtime,
72
+ verifier=payment_webhook_verifier,
73
+ order_confirmation_service=order_confirmation_service,
74
+ )
75
+ )
76
+
77
+ admin_bp = Blueprint("smx_commerce_admin", __name__)
78
+ apply_admin_api_key_guard(admin_bp, resolved_admin_api_key)
79
+
80
+ admin_bp.register_blueprint(create_admin_auth_blueprint(resolved_admin_api_key))
81
+ admin_bp.register_blueprint(create_admin_home_blueprint())
82
+ admin_bp.register_blueprint(create_category_admin_blueprint(commerce_runtime))
83
+ admin_bp.register_blueprint(create_category_edit_admin_blueprint(commerce_runtime))
84
+ admin_bp.register_blueprint(create_safe_delete_admin_blueprint(commerce_runtime))
85
+ admin_bp.register_blueprint(create_product_admin_blueprint(commerce_runtime))
86
+ admin_bp.register_blueprint(create_product_edit_admin_blueprint(commerce_runtime))
87
+ admin_bp.register_blueprint(create_price_admin_blueprint(commerce_runtime))
88
+ admin_bp.register_blueprint(create_price_edit_admin_blueprint(commerce_runtime))
89
+ admin_bp.register_blueprint(create_order_admin_blueprint(commerce_runtime))
90
+ admin_bp.register_blueprint(create_order_edit_admin_blueprint(commerce_runtime))
91
+ admin_bp.register_blueprint(create_settings_admin_blueprint(commerce_runtime))
92
+
93
+ bp.register_blueprint(admin_bp, url_prefix="/commerce/admin")
94
+
95
+ return bp
96
+
97
+
98
+ def init_commerce(app, *, config=None, init_schema: bool = False):
99
+ resolved_config = config or {}
100
+
101
+ if resolved_config.get("flask_secret_key") and not app.secret_key:
102
+ app.config["SECRET_KEY"] = resolved_config["flask_secret_key"]
103
+
104
+ app.register_blueprint(
105
+ create_commerce_blueprint(
106
+ config=resolved_config,
107
+ init_schema=init_schema,
108
+ payment_checkout_provider=_build_checkout_provider(resolved_config),
109
+ payment_webhook_verifier=_build_webhook_verifier(resolved_config),
110
+ order_confirmation_service=_build_order_confirmation_service(resolved_config),
111
+ )
112
+ )
113
+
114
+ return app
115
+
116
+
117
+ def init_commerce_from_env(
118
+ app,
119
+ *,
120
+ env_file: str = ".env.smx-commerce",
121
+ init_schema: bool = False,
122
+ prefix: str = "SMX_COMMERCE_",
123
+ ):
124
+ config = build_commerce_config_from_env(
125
+ env_file=env_file,
126
+ prefix=prefix,
127
+ )
128
+
129
+ return init_commerce(
130
+ app,
131
+ config=config,
132
+ init_schema=init_schema,
133
+ )
134
+
135
+
136
+ def _build_checkout_provider(config: dict):
137
+ provider = config.get("payment_provider")
138
+
139
+ if provider in {None, "", "none"}:
140
+ return None
141
+
142
+ if provider == "local":
143
+ return LocalCheckoutProvider(
144
+ checkout_base_url=config.get(
145
+ "local_checkout_base_url",
146
+ "https://local-payments.invalid/checkout",
147
+ )
148
+ )
149
+
150
+ if provider == "stripe":
151
+ return StripeCheckoutProvider(
152
+ api_key=config["stripe_secret_key"],
153
+ )
154
+
155
+ raise ValueError(f"unsupported payment_provider: {provider}")
156
+
157
+
158
+ def _build_webhook_verifier(config: dict):
159
+ provider = config.get("payment_provider")
160
+
161
+ if provider in {None, "", "none"}:
162
+ return None
163
+
164
+ if provider == "local":
165
+ return StaticSignatureWebhookVerifier(
166
+ expected_signature=config.get("local_webhook_signature", "local-signature"),
167
+ )
168
+
169
+ if provider == "stripe":
170
+ return __import__(
171
+ "smx_commerce.payments",
172
+ fromlist=["StripeWebhookVerifier"],
173
+ ).StripeWebhookVerifier(
174
+ webhook_secret=config["stripe_webhook_secret"],
175
+ )
176
+
177
+ raise ValueError(f"unsupported payment_provider: {provider}")
178
+
179
+
180
+ def _build_order_confirmation_service(config: dict):
181
+ email_provider = config.get("email_provider")
182
+
183
+ if email_provider in {None, "", "none"}:
184
+ return None
185
+
186
+ if email_provider == "memory":
187
+ sender = MemoryEmailSender()
188
+ elif email_provider == "smtp":
189
+ sender = SMTPEmailSender(
190
+ host=config["smtp_host"],
191
+ port=int(config.get("smtp_port", 587)),
192
+ default_from_email=config.get("default_from_email"),
193
+ username=config.get("smtp_username"),
194
+ password=config.get("smtp_password"),
195
+ use_tls=bool(config.get("smtp_use_tls", True)),
196
+ use_ssl=bool(config.get("smtp_use_ssl", False)),
197
+ )
198
+ else:
199
+ raise ValueError(f"unsupported email_provider: {email_provider}")
200
+
201
+ return OrderConfirmationEmailService(
202
+ sender,
203
+ from_email=config.get("default_from_email"),
204
+ brand_name=config.get("brand_name", "smx-commerce"),
205
+ )
206
+
207
+
208
+ __all__ = [
209
+ "build_commerce_config_from_env",
210
+ "create_commerce_blueprint",
211
+ "init_commerce",
212
+ "init_commerce_from_env",
213
+ "load_env_file",
214
+ ]
@@ -0,0 +1,31 @@
1
+ from .home import create_admin_home_blueprint
2
+ from .auth import (
3
+ ADMIN_KEY_HEADER,
4
+ ADMIN_SESSION_KEY,
5
+ apply_admin_api_key_guard,
6
+ create_admin_auth_blueprint,
7
+ )
8
+ from .routes import create_settings_admin_blueprint
9
+ from .product_edit import create_product_edit_admin_blueprint
10
+
11
+ __all__ = [
12
+ "create_order_edit_admin_blueprint",
13
+ "create_safe_delete_admin_blueprint",
14
+ "create_category_edit_admin_blueprint",
15
+ "create_price_edit_admin_blueprint",
16
+ "ADMIN_KEY_HEADER",
17
+ "ADMIN_SESSION_KEY",
18
+ "apply_admin_api_key_guard",
19
+ "create_admin_auth_blueprint",
20
+ "create_admin_home_blueprint",
21
+ "create_settings_admin_blueprint",
22
+ "create_product_edit_admin_blueprint",
23
+ ]
24
+
25
+ from .price_edit import create_price_edit_admin_blueprint
26
+
27
+ from .category_edit import create_category_edit_admin_blueprint
28
+
29
+ from .safe_delete import create_safe_delete_admin_blueprint
30
+
31
+ from .order_edit import create_order_edit_admin_blueprint
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
4
+
5
+
6
+ def parse_admin_amount_to_cents(value: str, *, field_name: str = "amount") -> int:
7
+ raw_value = str(value or "").strip()
8
+
9
+ if not raw_value:
10
+ raise ValueError(f"{field_name} is required")
11
+
12
+ try:
13
+ amount = Decimal(raw_value)
14
+ except InvalidOperation as exc:
15
+ raise ValueError(f"{field_name} must be a valid currency amount") from exc
16
+
17
+ if amount < 0:
18
+ raise ValueError(f"{field_name} cannot be negative")
19
+
20
+ cents = (amount * Decimal("100")).quantize(
21
+ Decimal("1"),
22
+ rounding=ROUND_HALF_UP,
23
+ )
24
+
25
+ return int(cents)
26
+
27
+
28
+ def parse_admin_price_amount_from_payload(payload: dict, *, is_form: bool) -> int:
29
+ """
30
+ Browser forms should send amount_major, for example:
31
+ 1 -> 100
32
+ 1.50 -> 150
33
+ 299 -> 29900
34
+
35
+ JSON/API callers keep using amount_cents.
36
+
37
+ For temporary backwards compatibility, form submissions that still send
38
+ amount_cents are also accepted.
39
+ """
40
+ if is_form:
41
+ amount_major = payload.get("amount_major")
42
+
43
+ if amount_major not in {None, ""}:
44
+ return parse_admin_amount_to_cents(amount_major, field_name="amount")
45
+
46
+ if payload.get("amount_cents") not in {None, ""}:
47
+ return int(payload.get("amount_cents", 0) or 0)
48
+
49
+ return parse_admin_amount_to_cents("", field_name="amount")
50
+
51
+ return int(payload.get("amount_cents", 0) or 0)
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ from hmac import compare_digest
4
+
5
+ from flask import Blueprint, current_app, jsonify, redirect, render_template, request, session, url_for
6
+
7
+
8
+ ADMIN_KEY_HEADER = "X-SMX-Commerce-Admin-Key"
9
+ ADMIN_SESSION_KEY = "smx_commerce_admin_authenticated"
10
+
11
+
12
+ def apply_admin_api_key_guard(bp: Blueprint, admin_api_key: str | None) -> None:
13
+ """
14
+ Protect admin routes when admin_api_key is configured.
15
+
16
+ Accepted auth methods:
17
+ 1. X-SMX-Commerce-Admin-Key header, useful for API clients/tests.
18
+ 2. Session login through /commerce/admin/login, useful for browser admin UX.
19
+
20
+ If no key is configured, the guard is inactive.
21
+ """
22
+ if not admin_api_key:
23
+ return
24
+
25
+ expected_key = admin_api_key.strip()
26
+
27
+ @bp.before_request
28
+ def require_admin_auth():
29
+ if _is_auth_route():
30
+ return None
31
+
32
+ supplied_key = request.headers.get(ADMIN_KEY_HEADER, "")
33
+
34
+ if supplied_key and compare_digest(supplied_key, expected_key):
35
+ return None
36
+
37
+ if session.get(ADMIN_SESSION_KEY) is True:
38
+ return None
39
+
40
+ if _wants_html():
41
+ return redirect(url_for("smx_commerce.smx_commerce_admin.smx_commerce_admin_auth.login"))
42
+
43
+ return jsonify({"error": "admin authentication required"}), 401
44
+
45
+ return None
46
+
47
+
48
+ def create_admin_auth_blueprint(admin_api_key: str | None) -> Blueprint:
49
+ bp = Blueprint(
50
+ "smx_commerce_admin_auth",
51
+ __name__,
52
+ template_folder="../templates",
53
+ )
54
+
55
+ @bp.get("/login")
56
+ def login():
57
+ if not admin_api_key:
58
+ return jsonify({"status": "ok", "message": "admin authentication is not configured"})
59
+
60
+ if request.accept_mimetypes.best_match(["text/html", "application/json"]) == "text/html":
61
+ return render_template("admin/login.html")
62
+
63
+ return jsonify({"status": "ok", "message": "admin login is available"})
64
+
65
+ @bp.post("/login")
66
+ def submit_login():
67
+ if not admin_api_key:
68
+ return jsonify({"status": "ok", "message": "admin authentication is not configured"})
69
+
70
+ if not current_app.secret_key:
71
+ return jsonify({"error": "Flask secret_key is required for admin session login"}), 500
72
+
73
+ payload = request.get_json(silent=True) or {}
74
+ supplied_key = request.form.get("admin_api_key") or payload.get("admin_api_key") or ""
75
+
76
+ if not compare_digest(supplied_key, admin_api_key):
77
+ if _wants_html():
78
+ return render_template("admin/login.html", error="Invalid admin key"), 401
79
+
80
+ return jsonify({"error": "invalid admin key"}), 401
81
+
82
+ session[ADMIN_SESSION_KEY] = True
83
+
84
+ if _wants_html():
85
+ return redirect("/commerce/admin/products", code=303)
86
+
87
+ return jsonify({"status": "ok", "authenticated": True})
88
+
89
+ @bp.post("/logout")
90
+ def logout():
91
+ session.pop(ADMIN_SESSION_KEY, None)
92
+
93
+ if _wants_html():
94
+ return redirect("/commerce/admin/login", code=303)
95
+
96
+ return jsonify({"status": "ok", "authenticated": False})
97
+
98
+ return bp
99
+
100
+
101
+ def _is_auth_route() -> bool:
102
+ path = request.path.rstrip("/")
103
+ return path.endswith("/commerce/admin/login") or path.endswith("/commerce/admin/logout")
104
+
105
+
106
+ def _wants_html() -> bool:
107
+ content_type = request.content_type or ""
108
+
109
+ if "application/x-www-form-urlencoded" in content_type:
110
+ return True
111
+
112
+ if "multipart/form-data" in content_type:
113
+ return True
114
+
115
+ return request.accept_mimetypes.best_match(["text/html", "application/json"]) == "text/html"
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from flask import Blueprint, jsonify, redirect, render_template, request
4
+
5
+ from smx_commerce.catalog import CatalogService
6
+ from smx_commerce.core import CommerceRuntime
7
+
8
+
9
+ def create_category_edit_admin_blueprint(runtime: CommerceRuntime) -> Blueprint:
10
+ bp = Blueprint("smx_commerce_category_edit_admin", __name__)
11
+
12
+ @bp.get("/categories/<slug>/edit")
13
+ def edit_category(slug: str):
14
+ with runtime.session_scope() as session:
15
+ catalog = CatalogService(session)
16
+ category = catalog.get_category(slug)
17
+ categories = catalog.list_categories(include_archived=False)
18
+
19
+ if category is None:
20
+ return jsonify({"error": f"category not found: {slug}"}), 404
21
+
22
+ parent_options = [
23
+ item for item in categories
24
+ if item.slug != category.slug
25
+ ]
26
+
27
+ return render_template(
28
+ "admin/category_edit.html",
29
+ category=category,
30
+ parent_options=parent_options,
31
+ )
32
+
33
+ @bp.post("/categories/<slug>/update")
34
+ def update_category_from_form(slug: str):
35
+ payload = request.form
36
+
37
+ parent_slug = payload.get("parent_slug") or None
38
+
39
+ if parent_slug == slug:
40
+ parent_slug = None
41
+
42
+ changes = {
43
+ "name": payload.get("name", ""),
44
+ "description": payload.get("description", ""),
45
+ "status": payload.get("status", "active"),
46
+ "parent_slug": parent_slug,
47
+ "sort_order": int(payload.get("sort_order", 0) or 0),
48
+ }
49
+
50
+ try:
51
+ with runtime.session_scope() as session:
52
+ catalog = CatalogService(session)
53
+ updated = catalog.update_category(slug, **changes)
54
+
55
+ return redirect("/commerce/admin/categories", code=303)
56
+
57
+ except (TypeError, ValueError) as exc:
58
+ with runtime.session_scope() as session:
59
+ catalog = CatalogService(session)
60
+ category = catalog.get_category(slug)
61
+ categories = catalog.list_categories(include_archived=False)
62
+
63
+ if category is None:
64
+ return jsonify({"error": f"category not found: {slug}"}), 404
65
+
66
+ parent_options = [
67
+ item for item in categories
68
+ if item.slug != category.slug
69
+ ]
70
+
71
+ return render_template(
72
+ "admin/category_edit.html",
73
+ category=category,
74
+ parent_options=parent_options,
75
+ error=str(exc),
76
+ ), 400
77
+
78
+ return bp
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from flask import Blueprint, redirect
4
+
5
+
6
+ def create_admin_home_blueprint() -> Blueprint:
7
+ bp = Blueprint("smx_commerce_admin_home", __name__)
8
+
9
+ @bp.get("/", strict_slashes=False)
10
+ def admin_home():
11
+ return redirect("/commerce/admin/products", code=303)
12
+
13
+ return bp