velocity-python 0.0.233__tar.gz → 0.0.236__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 (164) hide show
  1. {velocity_python-0.0.233/src/velocity_python.egg-info → velocity_python-0.0.236}/PKG-INFO +1 -1
  2. {velocity_python-0.0.233 → velocity_python-0.0.236}/pyproject.toml +1 -1
  3. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/__init__.py +1 -1
  4. velocity_python-0.0.236/src/velocity/app/validators/__init__.py +1 -0
  5. velocity_python-0.0.236/src/velocity/app/validators/formbuilder_template.py +204 -0
  6. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/payment/__init__.py +8 -0
  7. velocity_python-0.0.236/src/velocity/payment/charge_rules.py +39 -0
  8. {velocity_python-0.0.233 → velocity_python-0.0.236/src/velocity_python.egg-info}/PKG-INFO +1 -1
  9. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity_python.egg-info/SOURCES.txt +4 -0
  10. velocity_python-0.0.236/tests/test_formbuilder_template_validator.py +240 -0
  11. {velocity_python-0.0.233 → velocity_python-0.0.236}/LICENSE +0 -0
  12. {velocity_python-0.0.233 → velocity_python-0.0.236}/README.md +0 -0
  13. {velocity_python-0.0.233 → velocity_python-0.0.236}/setup.cfg +0 -0
  14. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/app/__init__.py +0 -0
  15. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/app/invoices.py +0 -0
  16. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/app/orders.py +0 -0
  17. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/app/payments.py +0 -0
  18. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/app/purchase_orders.py +0 -0
  19. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/app/tests/__init__.py +0 -0
  20. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/app/tests/test_email_processing.py +0 -0
  21. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  22. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  23. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/__init__.py +0 -0
  24. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/amplify.py +0 -0
  25. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/amplify_build.py +0 -0
  26. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/handlers/__init__.py +0 -0
  27. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/handlers/base_handler.py +0 -0
  28. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/handlers/context.py +0 -0
  29. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/handlers/context_factory.py +0 -0
  30. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/handlers/exceptions.py +0 -0
  31. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  32. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  33. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  34. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  35. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/handlers/perf.py +0 -0
  36. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/handlers/response.py +0 -0
  37. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  38. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/tests/__init__.py +0 -0
  39. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  40. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  41. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/aws/tests/test_response.py +0 -0
  42. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/__init__.py +0 -0
  43. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/core/__init__.py +0 -0
  44. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/core/column.py +0 -0
  45. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/core/database.py +0 -0
  46. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/core/decorators.py +0 -0
  47. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/core/engine.py +0 -0
  48. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/core/result.py +0 -0
  49. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/core/row.py +0 -0
  50. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/core/sequence.py +0 -0
  51. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/core/table.py +0 -0
  52. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/core/transaction.py +0 -0
  53. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/core/view.py +0 -0
  54. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/exceptions.py +0 -0
  55. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/__init__.py +0 -0
  56. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/base/__init__.py +0 -0
  57. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/base/initializer.py +0 -0
  58. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/base/operators.py +0 -0
  59. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/base/sql.py +0 -0
  60. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/base/types.py +0 -0
  61. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/mysql/__init__.py +0 -0
  62. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/mysql/operators.py +0 -0
  63. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/mysql/reserved.py +0 -0
  64. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/mysql/sql.py +0 -0
  65. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/mysql/types.py +0 -0
  66. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/postgres/__init__.py +0 -0
  67. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/postgres/operators.py +0 -0
  68. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/postgres/reserved.py +0 -0
  69. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/postgres/sql.py +0 -0
  70. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/postgres/types.py +0 -0
  71. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  72. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/sqlite/operators.py +0 -0
  73. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  74. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/sqlite/sql.py +0 -0
  75. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/sqlite/types.py +0 -0
  76. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  77. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  78. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  79. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  80. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/sqlserver/types.py +0 -0
  81. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/servers/tablehelper.py +0 -0
  82. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/__init__.py +0 -0
  83. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/common_db_test.py +0 -0
  84. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/__init__.py +0 -0
  85. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/common.py +0 -0
  86. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_column.py +0 -0
  87. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  88. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_database.py +0 -0
  89. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  90. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  91. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  92. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_result.py +0 -0
  93. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_row.py +0 -0
  94. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  95. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  96. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  97. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  98. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  99. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_table.py +0 -0
  100. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  101. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  102. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/sql/__init__.py +0 -0
  103. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/sql/common.py +0 -0
  104. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  105. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  106. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  107. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_db_utils.py +0 -0
  108. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_postgres.py +0 -0
  109. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  110. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  111. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_result_caching.py +0 -0
  112. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  113. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  114. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  115. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  116. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_sql_builder.py +0 -0
  117. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_tablehelper.py +0 -0
  118. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/tests/test_view_helper.py +0 -0
  119. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/db/utils.py +0 -0
  120. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/logging.py +0 -0
  121. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/__init__.py +0 -0
  122. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/conv/__init__.py +0 -0
  123. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/conv/iconv.py +0 -0
  124. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/conv/oconv.py +0 -0
  125. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/db.py +0 -0
  126. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/export.py +0 -0
  127. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/format.py +0 -0
  128. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/mail.py +0 -0
  129. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/merge.py +0 -0
  130. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/tests/__init__.py +0 -0
  131. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/tests/test_db.py +0 -0
  132. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/tests/test_fix.py +0 -0
  133. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/tests/test_format.py +0 -0
  134. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/tests/test_iconv.py +0 -0
  135. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/tests/test_merge.py +0 -0
  136. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/tests/test_oconv.py +0 -0
  137. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/tests/test_original_error.py +0 -0
  138. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/tests/test_timer.py +0 -0
  139. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/timer.py +0 -0
  140. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/misc/tools.py +0 -0
  141. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/payment/authorizenet_adapter.py +0 -0
  142. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/payment/base_adapter.py +0 -0
  143. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/payment/braintree_adapter.py +0 -0
  144. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/payment/demo_profiles.py +0 -0
  145. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/payment/profiles.py +0 -0
  146. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/payment/router.py +0 -0
  147. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity/payment/stripe_adapter.py +0 -0
  148. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  149. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity_python.egg-info/requires.txt +0 -0
  150. {velocity_python-0.0.233 → velocity_python-0.0.236}/src/velocity_python.egg-info/top_level.txt +0 -0
  151. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_amplify_build.py +0 -0
  152. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_decorators.py +0 -0
  153. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_iconv_money_to_cents.py +0 -0
  154. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_lambda_handler.py +0 -0
  155. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_lambda_handler_auth.py +0 -0
  156. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_mixins_import.py +0 -0
  157. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_payment_braintree_adapter.py +0 -0
  158. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_payment_demo_profiles.py +0 -0
  159. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_payment_profiles.py +0 -0
  160. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_payment_router.py +0 -0
  161. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_payment_stripe_adapter.py +0 -0
  162. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  163. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_table_alter.py +0 -0
  164. {velocity_python-0.0.233 → velocity_python-0.0.236}/tests/test_where_clause_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.233
3
+ Version: 0.0.236
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.0.233"
7
+ version = "0.0.236"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.233"
1
+ __version__ = version = "0.0.236"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -0,0 +1 @@
1
+ """velocity.app.validators — domain validators for shared application entities."""
@@ -0,0 +1,204 @@
1
+ """Pure-Python validator for form_builder_template dicts.
2
+
3
+ Authoritative location: velocity.app.validators.formbuilder_template
4
+
5
+ Called by:
6
+ - BackOffice before_write hook (rwx/form_builder_template.py)
7
+ - BackOffice CLI: scripts/validate_formbuilder_templates.py
8
+ - DonateMain CLI: scripts/formbuilder_validator.py (shim — imports from here)
9
+ - Tests: velocity-python/tests/app/validators/test_formbuilder_template.py
10
+ BackOffice.demo/tests/backend/test_form_builder_template_hook.py
11
+ DonateMain.demo/tests/backend/test_formbuilder_validators.py
12
+
13
+ No external dependencies — only the stdlib so it runs everywhere the test suite runs.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from typing import Any, Dict, List
20
+
21
+ # --------------------------------------------------------------------------- #
22
+ # Helpers
23
+ # --------------------------------------------------------------------------- #
24
+
25
+ _EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
26
+ # Also accept RFC 5322 display-name format: "Label <user@example.com>"
27
+ _EMAIL_DISPLAY_RE = re.compile(r"<[^@\s>]+@[^@\s>]+\.[^@\s>]+>$")
28
+ _URL_RE = re.compile(r"^https?://", re.IGNORECASE)
29
+
30
+ # Braintree: descriptor must be <= 22 characters, NAME*TEXT format.
31
+ _BT_DESCRIPTOR_MAX_LEN = 22
32
+
33
+
34
+ def _str_val(fb: Dict[str, Any], key: str) -> str:
35
+ """Return the stripped string value for a key, or '' when None/missing."""
36
+ v = fb.get(key)
37
+ return v.strip() if isinstance(v, str) else ""
38
+
39
+
40
+ # --------------------------------------------------------------------------- #
41
+ # Public API
42
+ # --------------------------------------------------------------------------- #
43
+
44
+ Issue = Dict[str, str] # {field, severity, message}
45
+
46
+
47
+ def validate_formbuilder_template(fb: Dict[str, Any]) -> List[Issue]:
48
+ """Validate a form_builder_template dict for known configuration problems.
49
+
50
+ Returns a list of issue dicts::
51
+
52
+ [{"field": "...", "severity": "error"|"warning", "message": "..."}]
53
+
54
+ An empty list means no issues were found.
55
+
56
+ Severities:
57
+ error – will cause a runtime crash or silent payment failure.
58
+ warning – likely produces a broken/confusing user experience.
59
+ """
60
+ issues: List[Issue] = []
61
+
62
+ def error(field: str, message: str) -> None:
63
+ issues.append({"field": field, "severity": "error", "message": message})
64
+
65
+ def warning(field: str, message: str) -> None:
66
+ issues.append({"field": field, "severity": "warning", "message": message})
67
+
68
+ # ---------------------------------------------------------------------- #
69
+ # Identity
70
+ # ---------------------------------------------------------------------- #
71
+ if not _str_val(fb, "client"):
72
+ error("client", "client is required; KeyError will crash the handler")
73
+
74
+ if not _str_val(fb, "campaign"):
75
+ error("campaign", "campaign is required; KeyError will crash the handler")
76
+
77
+ if not _str_val(fb, "url_slug"):
78
+ error("url_slug", "url_slug is required; template will not be routable")
79
+
80
+ # ---------------------------------------------------------------------- #
81
+ # Receipt email fields
82
+ # (used with direct key access: fb["receipt_from_address"] etc.)
83
+ # ---------------------------------------------------------------------- #
84
+ from_addr = _str_val(fb, "receipt_from_address")
85
+ if not from_addr:
86
+ error(
87
+ "receipt_from_address",
88
+ "receipt_from_address is empty; donation receipt emails will fail to send",
89
+ )
90
+ elif not _EMAIL_RE.match(from_addr) and not _EMAIL_DISPLAY_RE.search(from_addr):
91
+ error(
92
+ "receipt_from_address",
93
+ f"receipt_from_address '{from_addr}' is not a valid email address",
94
+ )
95
+
96
+ if not _str_val(fb, "receipt_subject"):
97
+ warning(
98
+ "receipt_subject",
99
+ "receipt_subject is empty; donation receipt email subject will be blank",
100
+ )
101
+
102
+ if not _str_val(fb, "receipt_body_text"):
103
+ error(
104
+ "receipt_body_text",
105
+ "receipt_body_text is empty; donation receipt emails will have no content",
106
+ )
107
+
108
+ # ---------------------------------------------------------------------- #
109
+ # Braintree transaction descriptor
110
+ # Rules: <= 22 chars, should contain '*', ASCII printable only.
111
+ # ---------------------------------------------------------------------- #
112
+ descriptor = _str_val(fb, "descriptor")
113
+ if descriptor:
114
+ if len(descriptor) > _BT_DESCRIPTOR_MAX_LEN:
115
+ error(
116
+ "descriptor",
117
+ f"descriptor is {len(descriptor)} chars (max {_BT_DESCRIPTOR_MAX_LEN}); "
118
+ "Braintree will reject the transaction",
119
+ )
120
+ if "*" not in descriptor:
121
+ warning(
122
+ "descriptor",
123
+ f"descriptor '{descriptor}' should follow 'NAME*TEXT' format for Braintree",
124
+ )
125
+ bad_chars = [c for c in descriptor if not (32 <= ord(c) <= 126)]
126
+ if bad_chars:
127
+ error(
128
+ "descriptor",
129
+ f"descriptor contains non-ASCII/non-printable characters: {bad_chars!r}",
130
+ )
131
+
132
+ # ---------------------------------------------------------------------- #
133
+ # Content — at least one active donation or metric must be configured
134
+ # ---------------------------------------------------------------------- #
135
+ has_content = _has_donation_content(fb) or _has_metric_content(fb)
136
+ if not has_content:
137
+ warning(
138
+ "donations/metrics",
139
+ "template has no donation amounts or metrics configured; the form will be empty",
140
+ )
141
+
142
+ # ---------------------------------------------------------------------- #
143
+ # Share URL (used in confirmation email)
144
+ # ---------------------------------------------------------------------- #
145
+ share_url = _str_val(fb, "share_url")
146
+ if share_url and not _URL_RE.match(share_url):
147
+ warning(
148
+ "share_url",
149
+ f"share_url '{share_url}' does not look like a valid URL (expected https://...)",
150
+ )
151
+
152
+ # ---------------------------------------------------------------------- #
153
+ # Confirmation page text
154
+ # ---------------------------------------------------------------------- #
155
+ if not _str_val(fb, "confirmation_header_text"):
156
+ warning(
157
+ "confirmation_header_text",
158
+ "confirmation_header_text is empty; the thank-you page header will be blank",
159
+ )
160
+
161
+ return issues
162
+
163
+
164
+ # --------------------------------------------------------------------------- #
165
+ # Internal helpers
166
+ # --------------------------------------------------------------------------- #
167
+
168
+
169
+ def _has_donation_content(fb: Dict[str, Any]) -> bool:
170
+ """Return True if at least one visible donation is configured."""
171
+ donations = fb.get("donations")
172
+ if isinstance(donations, dict):
173
+ for key, item in donations.items():
174
+ hide_key = f"donation_{key}_hide_donation_from_ui"
175
+ if fb.get(hide_key):
176
+ continue
177
+ if isinstance(item, dict) and item.get("label"):
178
+ return True
179
+ # Legacy numbered fields
180
+ for i in range(1, 10):
181
+ if fb.get(f"donation_{i}_hide_donation_from_ui"):
182
+ continue
183
+ if _str_val(fb, f"donation_{i}_label") or fb.get(f"donation_{i}_values"):
184
+ return True
185
+ return False
186
+
187
+
188
+ def _has_metric_content(fb: Dict[str, Any]) -> bool:
189
+ """Return True if at least one visible metric is configured."""
190
+ metrics = fb.get("metrics")
191
+ if isinstance(metrics, dict):
192
+ for key, item in metrics.items():
193
+ hide_key = f"{key}_hide_donation_from_ui"
194
+ if fb.get(hide_key):
195
+ continue
196
+ if isinstance(item, dict) and item.get("label"):
197
+ return True
198
+ # Legacy numbered fields
199
+ for i in range(1, 10):
200
+ if fb.get(f"metric_{i}_hide_donation_from_ui"):
201
+ continue
202
+ if _str_val(fb, f"metric_{i}_label") or fb.get(f"metric_{i}_values"):
203
+ return True
204
+ return False
@@ -55,6 +55,11 @@ from .profiles import (
55
55
  upsert_payment_profile,
56
56
  )
57
57
  from .demo_profiles import resolve_demo_charge_cards, get_card_lookup_id
58
+ from .charge_rules import (
59
+ should_charge_immediately,
60
+ is_below_immediate_charge_minimum,
61
+ is_above_immediate_charge_maximum,
62
+ )
58
63
 
59
64
  __all__ = [
60
65
  # Base classes
@@ -83,6 +88,9 @@ __all__ = [
83
88
  "upsert_payment_profile",
84
89
  "resolve_demo_charge_cards",
85
90
  "get_card_lookup_id",
91
+ "should_charge_immediately",
92
+ "is_below_immediate_charge_minimum",
93
+ "is_above_immediate_charge_maximum",
86
94
  ]
87
95
 
88
96
  __version__ = "1.0.0"
@@ -0,0 +1,39 @@
1
+ """Shared immediate-charge validation helpers for payment flows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def should_charge_immediately(total_charge_amount: Any) -> bool:
9
+ """Return True when an immediate payment should be submitted to a processor."""
10
+ if total_charge_amount in (None, ""):
11
+ return False
12
+ try:
13
+ return float(total_charge_amount) > 0
14
+ except (TypeError, ValueError):
15
+ return False
16
+
17
+
18
+ def is_below_immediate_charge_minimum(
19
+ total_charge_amount: Any, minimum_donation_amount: Any
20
+ ) -> bool:
21
+ """Return True only when an actual immediate charge exists and is below the minimum."""
22
+ if not should_charge_immediately(total_charge_amount):
23
+ return False
24
+ try:
25
+ return float(total_charge_amount) < float(minimum_donation_amount)
26
+ except (TypeError, ValueError):
27
+ return False
28
+
29
+
30
+ def is_above_immediate_charge_maximum(
31
+ total_charge_amount: Any, maximum_donation_amount: Any
32
+ ) -> bool:
33
+ """Return True only when an actual immediate charge exists and is above the maximum."""
34
+ if not should_charge_immediately(total_charge_amount):
35
+ return False
36
+ try:
37
+ return float(total_charge_amount) > float(maximum_donation_amount)
38
+ except (TypeError, ValueError):
39
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.233
3
+ Version: 0.0.236
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -12,6 +12,8 @@ src/velocity/app/tests/__init__.py
12
12
  src/velocity/app/tests/test_email_processing.py
13
13
  src/velocity/app/tests/test_payment_profile_sorting.py
14
14
  src/velocity/app/tests/test_spreadsheet_functions.py
15
+ src/velocity/app/validators/__init__.py
16
+ src/velocity/app/validators/formbuilder_template.py
15
17
  src/velocity/aws/__init__.py
16
18
  src/velocity/aws/amplify.py
17
19
  src/velocity/aws/amplify_build.py
@@ -133,6 +135,7 @@ src/velocity/payment/__init__.py
133
135
  src/velocity/payment/authorizenet_adapter.py
134
136
  src/velocity/payment/base_adapter.py
135
137
  src/velocity/payment/braintree_adapter.py
138
+ src/velocity/payment/charge_rules.py
136
139
  src/velocity/payment/demo_profiles.py
137
140
  src/velocity/payment/profiles.py
138
141
  src/velocity/payment/router.py
@@ -144,6 +147,7 @@ src/velocity_python.egg-info/requires.txt
144
147
  src/velocity_python.egg-info/top_level.txt
145
148
  tests/test_amplify_build.py
146
149
  tests/test_decorators.py
150
+ tests/test_formbuilder_template_validator.py
147
151
  tests/test_iconv_money_to_cents.py
148
152
  tests/test_lambda_handler.py
149
153
  tests/test_lambda_handler_auth.py
@@ -0,0 +1,240 @@
1
+ """Unit tests for velocity.app.validators.formbuilder_template.
2
+
3
+ These tests are the canonical regression suite for the shared validator.
4
+ Consumer-project tests (DonateMain, BackOffice) import from this module via
5
+ velocity so their suites should stay in sync.
6
+
7
+ Runs entirely offline — no database or network access required.
8
+
9
+ python -m pytest tests/test_formbuilder_template_validator.py -v
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import unittest
15
+
16
+ from velocity.app.validators.formbuilder_template import validate_formbuilder_template
17
+
18
+
19
+ # --------------------------------------------------------------------------- #
20
+ # Helpers
21
+ # --------------------------------------------------------------------------- #
22
+
23
+
24
+ def _make_valid_fb(**overrides) -> dict:
25
+ """Return the minimal template dict that passes all validations."""
26
+ base = {
27
+ "client": "TestClient",
28
+ "campaign": "test_campaign",
29
+ "url_slug": "/test/campaign",
30
+ "receipt_from_address": "noreply@example.com",
31
+ "receipt_subject": "Thank you for your donation",
32
+ "receipt_body_text": "<p>Dear {first_name}, thank you for donating!</p>",
33
+ "descriptor": "CaringCent*TEST",
34
+ "confirmation_header_text": "Thank you!",
35
+ "share_url": "https://donate.example.org/campaign",
36
+ "donations": {
37
+ "d1": {"label": "Give $25", "values": {"$25": "25"}},
38
+ },
39
+ }
40
+ base.update(overrides)
41
+ return base
42
+
43
+
44
+ def _errors(issues, field=None):
45
+ return [
46
+ i
47
+ for i in issues
48
+ if i["severity"] == "error" and (field is None or i["field"] == field)
49
+ ]
50
+
51
+
52
+ def _warnings(issues, field=None):
53
+ return [
54
+ i
55
+ for i in issues
56
+ if i["severity"] == "warning" and (field is None or i["field"] == field)
57
+ ]
58
+
59
+
60
+ # --------------------------------------------------------------------------- #
61
+ # Baseline
62
+ # --------------------------------------------------------------------------- #
63
+
64
+
65
+ class ValidTemplatePassesTests(unittest.TestCase):
66
+
67
+ def test_valid_template_has_no_issues(self):
68
+ issues = validate_formbuilder_template(_make_valid_fb())
69
+ self.assertEqual(issues, [], f"Unexpected issues: {issues}")
70
+
71
+ def test_returns_a_list(self):
72
+ result = validate_formbuilder_template(_make_valid_fb())
73
+ self.assertIsInstance(result, list)
74
+
75
+
76
+ # --------------------------------------------------------------------------- #
77
+ # Identity fields
78
+ # --------------------------------------------------------------------------- #
79
+
80
+
81
+ class IdentityFieldTests(unittest.TestCase):
82
+
83
+ def test_missing_client_is_error(self):
84
+ self.assertTrue(_errors(validate_formbuilder_template(_make_valid_fb(client="")), "client"))
85
+
86
+ def test_none_client_is_error(self):
87
+ self.assertTrue(_errors(validate_formbuilder_template(_make_valid_fb(client=None)), "client"))
88
+
89
+ def test_missing_campaign_is_error(self):
90
+ self.assertTrue(_errors(validate_formbuilder_template(_make_valid_fb(campaign="")), "campaign"))
91
+
92
+ def test_missing_url_slug_is_error(self):
93
+ fb = _make_valid_fb()
94
+ del fb["url_slug"]
95
+ self.assertTrue(_errors(validate_formbuilder_template(fb), "url_slug"))
96
+
97
+ def test_none_url_slug_is_error(self):
98
+ self.assertTrue(_errors(validate_formbuilder_template(_make_valid_fb(url_slug=None)), "url_slug"))
99
+
100
+
101
+ # --------------------------------------------------------------------------- #
102
+ # Receipt email fields
103
+ # --------------------------------------------------------------------------- #
104
+
105
+
106
+ class ReceiptEmailFieldTests(unittest.TestCase):
107
+
108
+ def test_empty_from_address_is_error(self):
109
+ self.assertTrue(_errors(validate_formbuilder_template(
110
+ _make_valid_fb(receipt_from_address="")), "receipt_from_address"))
111
+
112
+ def test_none_from_address_is_error(self):
113
+ self.assertTrue(_errors(validate_formbuilder_template(
114
+ _make_valid_fb(receipt_from_address=None)), "receipt_from_address"))
115
+
116
+ def test_invalid_from_address_is_error(self):
117
+ self.assertTrue(_errors(validate_formbuilder_template(
118
+ _make_valid_fb(receipt_from_address="not-an-email")), "receipt_from_address"))
119
+
120
+ def test_display_name_format_passes(self):
121
+ """'Display Name <user@example.com>' is RFC 5322 valid and used in practice."""
122
+ issues = validate_formbuilder_template(
123
+ _make_valid_fb(receipt_from_address="Tigers Athletics <noreply@example.com>"))
124
+ self.assertFalse(_errors(issues, "receipt_from_address"))
125
+
126
+ def test_empty_subject_is_warning(self):
127
+ self.assertTrue(_warnings(validate_formbuilder_template(
128
+ _make_valid_fb(receipt_subject="")), "receipt_subject"))
129
+
130
+ def test_empty_body_is_error(self):
131
+ self.assertTrue(_errors(validate_formbuilder_template(
132
+ _make_valid_fb(receipt_body_text="")), "receipt_body_text"))
133
+
134
+
135
+ # --------------------------------------------------------------------------- #
136
+ # Braintree descriptor
137
+ # --------------------------------------------------------------------------- #
138
+
139
+
140
+ class DescriptorTests(unittest.TestCase):
141
+
142
+ def test_valid_descriptor_passes(self):
143
+ issues = validate_formbuilder_template(_make_valid_fb())
144
+ self.assertFalse(_errors(issues, "descriptor"))
145
+ self.assertFalse(_warnings(issues, "descriptor"))
146
+
147
+ def test_omitted_descriptor_passes(self):
148
+ fb = _make_valid_fb()
149
+ del fb["descriptor"]
150
+ issues = validate_formbuilder_template(fb)
151
+ self.assertFalse(_errors(issues, "descriptor"))
152
+
153
+ def test_descriptor_too_long_is_error(self):
154
+ self.assertTrue(_errors(validate_formbuilder_template(
155
+ _make_valid_fb(descriptor="A" * 23)), "descriptor"))
156
+
157
+ def test_descriptor_exactly_22_chars_passes(self):
158
+ issues = validate_formbuilder_template(_make_valid_fb(descriptor="CaringCent*EXACTLY22CH"))
159
+ self.assertFalse(_errors(issues, "descriptor"))
160
+
161
+ def test_descriptor_without_asterisk_is_warning(self):
162
+ fb = _make_valid_fb(descriptor="CaringCentNOASTERISK")
163
+ issues = validate_formbuilder_template(fb)
164
+ self.assertTrue(_warnings(issues, "descriptor"))
165
+
166
+ def test_descriptor_with_non_ascii_is_error(self):
167
+ self.assertTrue(_errors(validate_formbuilder_template(
168
+ _make_valid_fb(descriptor="CaringCent*\x80TEST")), "descriptor"))
169
+
170
+
171
+ # --------------------------------------------------------------------------- #
172
+ # Content — donations / metrics
173
+ # --------------------------------------------------------------------------- #
174
+
175
+
176
+ class ContentConfigurationTests(unittest.TestCase):
177
+
178
+ def test_no_donations_no_metrics_is_warning(self):
179
+ fb = _make_valid_fb()
180
+ del fb["donations"]
181
+ self.assertTrue(_warnings(validate_formbuilder_template(fb), "donations/metrics"))
182
+
183
+ def test_modern_donations_dict_counts_as_content(self):
184
+ issues = validate_formbuilder_template(_make_valid_fb())
185
+ self.assertFalse(_warnings(issues, "donations/metrics"))
186
+
187
+ def test_hidden_donation_does_not_count_as_content(self):
188
+ fb = _make_valid_fb()
189
+ fb["donations"] = {"d1": {"label": "Hidden", "values": {}}}
190
+ fb["donation_d1_hide_donation_from_ui"] = True
191
+ issues = validate_formbuilder_template(fb)
192
+ self.assertTrue(_warnings(issues, "donations/metrics"))
193
+
194
+ def test_legacy_numbered_donation_fields_count_as_content(self):
195
+ fb = _make_valid_fb()
196
+ del fb["donations"]
197
+ fb["donation_1_label"] = "Give $25"
198
+ issues = validate_formbuilder_template(fb)
199
+ self.assertFalse(_warnings(issues, "donations/metrics"))
200
+
201
+
202
+ # --------------------------------------------------------------------------- #
203
+ # Share URL
204
+ # --------------------------------------------------------------------------- #
205
+
206
+
207
+ class ShareUrlTests(unittest.TestCase):
208
+
209
+ def test_missing_share_url_is_ok(self):
210
+ fb = _make_valid_fb()
211
+ del fb["share_url"]
212
+ self.assertFalse(_warnings(validate_formbuilder_template(fb), "share_url"))
213
+
214
+ def test_valid_https_url_passes(self):
215
+ issues = validate_formbuilder_template(_make_valid_fb())
216
+ self.assertFalse(_warnings(issues, "share_url"))
217
+
218
+ def test_non_url_share_url_is_warning(self):
219
+ self.assertTrue(_warnings(validate_formbuilder_template(
220
+ _make_valid_fb(share_url="not-a-url")), "share_url"))
221
+
222
+
223
+ # --------------------------------------------------------------------------- #
224
+ # Confirmation page text
225
+ # --------------------------------------------------------------------------- #
226
+
227
+
228
+ class ConfirmationTextTests(unittest.TestCase):
229
+
230
+ def test_empty_confirmation_header_is_warning(self):
231
+ self.assertTrue(_warnings(validate_formbuilder_template(
232
+ _make_valid_fb(confirmation_header_text="")), "confirmation_header_text"))
233
+
234
+ def test_valid_confirmation_header_passes(self):
235
+ issues = validate_formbuilder_template(_make_valid_fb())
236
+ self.assertFalse(_warnings(issues, "confirmation_header_text"))
237
+
238
+
239
+ if __name__ == "__main__":
240
+ unittest.main()