velocity-python 0.0.217__tar.gz → 0.0.219__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 (157) hide show
  1. {velocity_python-0.0.217/src/velocity_python.egg-info → velocity_python-0.0.219}/PKG-INFO +1 -1
  2. {velocity_python-0.0.217 → velocity_python-0.0.219}/pyproject.toml +1 -1
  3. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/payment/__init__.py +22 -0
  5. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/payment/base_adapter.py +88 -0
  6. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/payment/braintree_adapter.py +81 -0
  7. velocity_python-0.0.219/src/velocity/payment/profiles.py +71 -0
  8. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/payment/router.py +105 -5
  9. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/payment/stripe_adapter.py +224 -0
  10. {velocity_python-0.0.217 → velocity_python-0.0.219/src/velocity_python.egg-info}/PKG-INFO +1 -1
  11. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity_python.egg-info/SOURCES.txt +5 -0
  12. velocity_python-0.0.219/tests/test_payment_braintree_adapter.py +77 -0
  13. velocity_python-0.0.219/tests/test_payment_profiles.py +72 -0
  14. velocity_python-0.0.219/tests/test_payment_router.py +107 -0
  15. velocity_python-0.0.219/tests/test_payment_stripe_adapter.py +186 -0
  16. {velocity_python-0.0.217 → velocity_python-0.0.219}/LICENSE +0 -0
  17. {velocity_python-0.0.217 → velocity_python-0.0.219}/README.md +0 -0
  18. {velocity_python-0.0.217 → velocity_python-0.0.219}/setup.cfg +0 -0
  19. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/__init__.py +0 -0
  20. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/invoices.py +0 -0
  21. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/orders.py +0 -0
  22. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/payments.py +0 -0
  23. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/purchase_orders.py +0 -0
  24. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/tests/__init__.py +0 -0
  25. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/tests/test_email_processing.py +0 -0
  26. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  27. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  28. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/__init__.py +0 -0
  29. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/amplify.py +0 -0
  30. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/amplify_build.py +0 -0
  31. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/__init__.py +0 -0
  32. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/base_handler.py +0 -0
  33. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/context.py +0 -0
  34. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/context_factory.py +0 -0
  35. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/exceptions.py +0 -0
  36. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  37. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  38. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  39. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  40. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/perf.py +0 -0
  41. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/response.py +0 -0
  42. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  43. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/tests/__init__.py +0 -0
  44. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  45. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  46. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/tests/test_response.py +0 -0
  47. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/__init__.py +0 -0
  48. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/__init__.py +0 -0
  49. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/column.py +0 -0
  50. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/database.py +0 -0
  51. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/decorators.py +0 -0
  52. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/engine.py +0 -0
  53. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/result.py +0 -0
  54. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/row.py +0 -0
  55. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/sequence.py +0 -0
  56. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/table.py +0 -0
  57. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/transaction.py +0 -0
  58. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/view.py +0 -0
  59. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/exceptions.py +0 -0
  60. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/__init__.py +0 -0
  61. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/base/__init__.py +0 -0
  62. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/base/initializer.py +0 -0
  63. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/base/operators.py +0 -0
  64. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/base/sql.py +0 -0
  65. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/base/types.py +0 -0
  66. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/mysql/__init__.py +0 -0
  67. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/mysql/operators.py +0 -0
  68. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/mysql/reserved.py +0 -0
  69. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/mysql/sql.py +0 -0
  70. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/mysql/types.py +0 -0
  71. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/postgres/__init__.py +0 -0
  72. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/postgres/operators.py +0 -0
  73. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/postgres/reserved.py +0 -0
  74. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/postgres/sql.py +0 -0
  75. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/postgres/types.py +0 -0
  76. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  77. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlite/operators.py +0 -0
  78. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  79. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlite/sql.py +0 -0
  80. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlite/types.py +0 -0
  81. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  82. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  83. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  84. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  85. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlserver/types.py +0 -0
  86. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/tablehelper.py +0 -0
  87. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/__init__.py +0 -0
  88. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/common_db_test.py +0 -0
  89. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/__init__.py +0 -0
  90. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/common.py +0 -0
  91. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_column.py +0 -0
  92. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  93. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_database.py +0 -0
  94. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  95. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  96. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  97. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_result.py +0 -0
  98. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_row.py +0 -0
  99. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  100. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  101. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  102. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  103. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  104. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_table.py +0 -0
  105. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  106. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  107. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/sql/__init__.py +0 -0
  108. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/sql/common.py +0 -0
  109. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  110. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  111. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  112. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_db_utils.py +0 -0
  113. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_postgres.py +0 -0
  114. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  115. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  116. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_result_caching.py +0 -0
  117. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  118. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  119. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  120. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  121. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_sql_builder.py +0 -0
  122. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_tablehelper.py +0 -0
  123. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_view_helper.py +0 -0
  124. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/utils.py +0 -0
  125. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/logging.py +0 -0
  126. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/__init__.py +0 -0
  127. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/conv/__init__.py +0 -0
  128. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/conv/iconv.py +0 -0
  129. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/conv/oconv.py +0 -0
  130. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/db.py +0 -0
  131. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/export.py +0 -0
  132. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/format.py +0 -0
  133. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/mail.py +0 -0
  134. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/merge.py +0 -0
  135. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/__init__.py +0 -0
  136. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_db.py +0 -0
  137. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_fix.py +0 -0
  138. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_format.py +0 -0
  139. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_iconv.py +0 -0
  140. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_merge.py +0 -0
  141. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_oconv.py +0 -0
  142. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_original_error.py +0 -0
  143. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_timer.py +0 -0
  144. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/timer.py +0 -0
  145. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tools.py +0 -0
  146. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  147. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity_python.egg-info/requires.txt +0 -0
  148. {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity_python.egg-info/top_level.txt +0 -0
  149. {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_amplify_build.py +0 -0
  150. {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_decorators.py +0 -0
  151. {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_iconv_money_to_cents.py +0 -0
  152. {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_lambda_handler.py +0 -0
  153. {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_lambda_handler_auth.py +0 -0
  154. {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_mixins_import.py +0 -0
  155. {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  156. {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_table_alter.py +0 -0
  157. {velocity_python-0.0.217 → velocity_python-0.0.219}/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.217
3
+ Version: 0.0.219
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.217"
7
+ version = "0.0.219"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.217"
1
+ __version__ = version = "0.0.219"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -32,6 +32,18 @@ from .router import (
32
32
  get_processor_config,
33
33
  get_revenue_split_percentage,
34
34
  get_processor_account_id,
35
+ charge_stored_payment_method,
36
+ get_or_create_customer_profile,
37
+ attach_payment_method,
38
+ detach_payment_method,
39
+ delete_customer_profile,
40
+ )
41
+ from .profiles import (
42
+ normalize_payment_processor,
43
+ money_to_cents,
44
+ get_payment_profile_sources,
45
+ build_stripe_payment_profile,
46
+ upsert_payment_profile,
35
47
  )
36
48
 
37
49
  __all__ = [
@@ -46,6 +58,16 @@ __all__ = [
46
58
  "get_processor_config",
47
59
  "get_revenue_split_percentage",
48
60
  "get_processor_account_id",
61
+ "charge_stored_payment_method",
62
+ "get_or_create_customer_profile",
63
+ "attach_payment_method",
64
+ "detach_payment_method",
65
+ "delete_customer_profile",
66
+ "normalize_payment_processor",
67
+ "money_to_cents",
68
+ "get_payment_profile_sources",
69
+ "build_stripe_payment_profile",
70
+ "upsert_payment_profile",
49
71
  ]
50
72
 
51
73
  __version__ = "1.0.0"
@@ -153,6 +153,94 @@ class PaymentProcessorAdapter(ABC):
153
153
  """
154
154
  pass
155
155
 
156
+ @abstractmethod
157
+ def charge_stored_payment_method(
158
+ self, tx, payment_data: Dict[str, Any]
159
+ ) -> Dict[str, Any]:
160
+ """
161
+ Charge a previously stored payment method immediately.
162
+
163
+ This is used for vault/off-session payment flows such as saved donor cards,
164
+ recurring charges, and queued month transactions.
165
+
166
+ Args:
167
+ tx: velocity-python transaction object
168
+ payment_data: Dictionary containing:
169
+ - amount: Amount in cents (integer)
170
+ - currency: ISO currency code (default: 'usd')
171
+ - payment_method: Stored processor payment method token/ID
172
+ - customer_profile_id: Stored processor customer ID (optional)
173
+ - client_id: Internal client identifier
174
+ - processor_account_id: External account identifier (optional)
175
+ - revenue_split_percentage: Platform fee percentage (optional)
176
+ - metadata: Additional transaction metadata
177
+ - donor_email: Donor email address
178
+ - donor_name: Donor full name
179
+ - description: Transaction description (optional)
180
+ - descriptor: Processor descriptor object (optional)
181
+
182
+ Returns:
183
+ Dictionary containing:
184
+ - processor_transaction_id: External transaction/payment intent identifier
185
+ - processor_charge_id: External charge identifier when available
186
+ - status: Processor status string
187
+ - success: Boolean indicating a completed charge
188
+ - amount: Amount charged in cents
189
+ - currency: Transaction currency
190
+ - authorization_code: Processor authorization code when available
191
+ - platform_fee: Platform fee amount in cents
192
+ - client_amount: Net client amount in cents
193
+ - error_message: Error details if failed (optional)
194
+ - metadata: Additional processor-specific data
195
+ """
196
+ pass
197
+
198
+ def get_or_create_customer_profile(
199
+ self, tx, customer_data: Dict[str, Any]
200
+ ) -> Dict[str, Any]:
201
+ """Get or create a vaulted customer profile for a processor.
202
+
203
+ Processors that do not support customer profiles directly may override this
204
+ or leave the default unsupported implementation.
205
+ """
206
+ raise ProcessorError(
207
+ processor=self.processor_name,
208
+ error_code="unsupported_operation",
209
+ error_message="Customer profile management is not supported by this processor.",
210
+ )
211
+
212
+ def attach_payment_method(
213
+ self,
214
+ tx,
215
+ customer_profile_id: str,
216
+ payment_method_id: str,
217
+ payment_data: Optional[Dict[str, Any]] = None,
218
+ ) -> Dict[str, Any]:
219
+ """Attach a payment method to a vaulted customer profile."""
220
+ raise ProcessorError(
221
+ processor=self.processor_name,
222
+ error_code="unsupported_operation",
223
+ error_message="Payment method attachment is not supported by this processor.",
224
+ )
225
+
226
+ def detach_payment_method(
227
+ self, tx, payment_method_id: str, payment_data: Optional[Dict[str, Any]] = None
228
+ ) -> Dict[str, Any]:
229
+ """Detach a payment method from a vaulted customer profile."""
230
+ raise ProcessorError(
231
+ processor=self.processor_name,
232
+ error_code="unsupported_operation",
233
+ error_message="Payment method detachment is not supported by this processor.",
234
+ )
235
+
236
+ def delete_customer_profile(self, tx, customer_profile_id: str) -> Dict[str, Any]:
237
+ """Delete a vaulted customer profile."""
238
+ raise ProcessorError(
239
+ processor=self.processor_name,
240
+ error_code="unsupported_operation",
241
+ error_message="Customer profile deletion is not supported by this processor.",
242
+ )
243
+
156
244
  # ========================================================================
157
245
  # PAYMENT CAPTURE
158
246
  # ========================================================================
@@ -370,6 +370,87 @@ class BraintreeAdapter(PaymentProcessorAdapter):
370
370
  "metadata": {"error_type": type(e).__name__},
371
371
  }
372
372
 
373
+ def charge_stored_payment_method(self, tx, payment_data: Dict[str, Any]) -> Dict[str, Any]:
374
+ """Charge a stored Braintree payment method token immediately."""
375
+ try:
376
+ amount_cents = payment_data["amount"]
377
+ amount_dollars = amount_cents / 100
378
+ transaction_params = {
379
+ "payment_method_token": payment_data["payment_method"],
380
+ "amount": f"{amount_dollars:.2f}",
381
+ "options": {"submit_for_settlement": True},
382
+ }
383
+
384
+ descriptor = payment_data.get("descriptor")
385
+ if descriptor:
386
+ transaction_params["descriptor"] = descriptor
387
+
388
+ metadata = payment_data.get("metadata") or {}
389
+ if metadata:
390
+ transaction_params["custom_fields"] = {
391
+ str(key): str(value)
392
+ for key, value in metadata.items()
393
+ if value is not None
394
+ }
395
+
396
+ result = self.gateway.transaction.sale(transaction_params)
397
+
398
+ if result.is_success:
399
+ transaction = result.transaction
400
+ charged_amount = int(float(transaction.amount) * 100)
401
+ return {
402
+ "processor_transaction_id": transaction.id,
403
+ "processor_charge_id": transaction.id,
404
+ "status": transaction.status,
405
+ "success": True,
406
+ "amount": charged_amount,
407
+ "currency": transaction.currency_iso_code.lower(),
408
+ "authorization_code": transaction.processor_authorization_code,
409
+ "platform_fee": 0,
410
+ "client_amount": charged_amount,
411
+ "error_message": None,
412
+ "metadata": {
413
+ "transaction": transaction,
414
+ },
415
+ }
416
+
417
+ error_message = result.message
418
+ if result.transaction:
419
+ error_message = result.transaction.processor_response_text
420
+ return {
421
+ "processor_transaction_id": None,
422
+ "processor_charge_id": None,
423
+ "status": "failed",
424
+ "success": False,
425
+ "amount": payment_data.get("amount"),
426
+ "currency": payment_data.get("currency", "usd"),
427
+ "authorization_code": None,
428
+ "platform_fee": 0,
429
+ "client_amount": 0,
430
+ "error_message": error_message,
431
+ "metadata": {
432
+ "braintree_errors": [
433
+ f"{error.code}: {error.message}"
434
+ for error in result.errors.deep_errors
435
+ ]
436
+ },
437
+ }
438
+
439
+ except Exception as e:
440
+ return {
441
+ "processor_transaction_id": None,
442
+ "processor_charge_id": None,
443
+ "status": "failed",
444
+ "success": False,
445
+ "amount": payment_data.get("amount"),
446
+ "currency": payment_data.get("currency", "usd"),
447
+ "authorization_code": None,
448
+ "platform_fee": 0,
449
+ "client_amount": 0,
450
+ "error_message": str(e),
451
+ "metadata": {"error_type": type(e).__name__},
452
+ }
453
+
373
454
  # ========================================================================
374
455
  # PAYMENT CAPTURE
375
456
  # ========================================================================
@@ -0,0 +1,71 @@
1
+ """Payment profile utilities shared across payment processors."""
2
+
3
+ from decimal import Decimal
4
+
5
+
6
+ PROCESSOR_PROFILE_SOURCES = {
7
+ "stripe": ["ST"],
8
+ "braintree": ["BT", "AN"],
9
+ }
10
+
11
+
12
+ def normalize_payment_processor(value):
13
+ raw = str(value or "").strip().lower()
14
+ if not raw or raw == "braintree":
15
+ return "braintree"
16
+ if raw == "stripe":
17
+ return "stripe"
18
+ return raw
19
+
20
+
21
+ def money_to_cents(value):
22
+ amount = Decimal(str(value or 0))
23
+ return int((amount * Decimal("100")).quantize(Decimal("1")))
24
+
25
+
26
+ def get_payment_profile_sources(processor_type):
27
+ processor = normalize_payment_processor(processor_type)
28
+ return list(PROCESSOR_PROFILE_SOURCES.get(processor, ["BT", "AN"]))
29
+
30
+
31
+ def build_stripe_payment_profile(
32
+ customer_id, payment_method, email_address, is_default=True
33
+ ):
34
+ card = getattr(payment_method, "card", None)
35
+ billing_details = getattr(payment_method, "billing_details", None)
36
+ data = {
37
+ "src": "ST",
38
+ "card_type": (
39
+ str(getattr(card, "brand", "Stripe") or "Stripe")
40
+ .replace("_", " ")
41
+ .title()
42
+ ),
43
+ "card_number": getattr(card, "last4", None),
44
+ "expiration_date": (
45
+ f"{int(getattr(card, 'exp_year', 0)):04d}-{int(getattr(card, 'exp_month', 0)):02d}"
46
+ if getattr(card, "exp_year", None) and getattr(card, "exp_month", None)
47
+ else None
48
+ ),
49
+ "payment_profile_id": payment_method.id,
50
+ "customer_profile_id": customer_id,
51
+ "email_address": email_address,
52
+ "is_default": is_default,
53
+ }
54
+ name = getattr(billing_details, "name", None) if billing_details else None
55
+ if name:
56
+ parts = str(name).split()
57
+ if parts:
58
+ data["first_name"] = " ".join(parts[:-1]) or parts[0]
59
+ data["last_name"] = parts[-1] if len(parts) > 1 else ""
60
+ return data
61
+
62
+
63
+ def upsert_payment_profile(tx, profile_data, key=None):
64
+ lookup_key = key or {"payment_profile_id": profile_data["payment_profile_id"]}
65
+ try:
66
+ tx.table("payment_profiles").upsert(profile_data, lookup_key)
67
+ except Exception:
68
+ data = dict(profile_data)
69
+ data.update(lookup_key)
70
+ tx.table("payment_profiles").insert(data)
71
+ tx.table("payment_profiles").create_index("payment_profile_id", unique=True)
@@ -15,6 +15,7 @@ from typing import Dict, Any, Optional
15
15
  from .base_adapter import PaymentProcessorAdapter
16
16
  from .stripe_adapter import StripeAdapter
17
17
  from .braintree_adapter import BraintreeAdapter
18
+ from .profiles import money_to_cents
18
19
 
19
20
 
20
21
  def get_processor_adapter(
@@ -48,16 +49,17 @@ def get_processor_adapter(
48
49
  f"Payment processor '{processor_type}' is not enabled in feature flags"
49
50
  )
50
51
 
51
- # Get processor configuration
52
+ return _build_adapter(processor_type)
53
+
54
+
55
+ def _build_adapter(processor_type: str) -> PaymentProcessorAdapter:
52
56
  config = get_processor_config(processor_type)
53
57
 
54
- # Instantiate appropriate adapter
55
58
  if processor_type == "stripe":
56
59
  return StripeAdapter(config)
57
- elif processor_type == "braintree":
60
+ if processor_type == "braintree":
58
61
  return BraintreeAdapter(config)
59
- else:
60
- raise ValueError(f"Unsupported payment processor type: {processor_type}")
62
+ raise ValueError(f"Unsupported payment processor type: {processor_type}")
61
63
 
62
64
 
63
65
  def _determine_processor_type(
@@ -276,3 +278,101 @@ def get_processor_account_id(
276
278
  )
277
279
 
278
280
  return payment_account["processor_account_id"] if payment_account else None
281
+
282
+
283
+ def charge_stored_payment_method(
284
+ tx,
285
+ client_id: str,
286
+ payment_profile: Dict[str, Any],
287
+ amount,
288
+ *,
289
+ donor_email: Optional[str] = None,
290
+ donor_name: Optional[str] = None,
291
+ description: Optional[str] = None,
292
+ metadata: Optional[Dict[str, Any]] = None,
293
+ processor_type: Optional[str] = None,
294
+ descriptor: Optional[Dict[str, Any]] = None,
295
+ ) -> Dict[str, Any]:
296
+ """Charge a stored payment profile using the routed processor adapter."""
297
+ resolved_processor = processor_type or _determine_processor_type(tx, client_id)
298
+ if not _is_processor_enabled(tx, resolved_processor):
299
+ raise ValueError(
300
+ f"Payment processor '{resolved_processor}' is not enabled in feature flags"
301
+ )
302
+
303
+ adapter = _build_adapter(resolved_processor)
304
+ amount_cents = money_to_cents(amount)
305
+
306
+ return adapter.charge_stored_payment_method(
307
+ tx,
308
+ {
309
+ "amount": amount_cents,
310
+ "currency": "usd",
311
+ "payment_method": payment_profile["payment_profile_id"],
312
+ "customer_profile_id": payment_profile.get("customer_profile_id"),
313
+ "client_id": client_id,
314
+ "processor_account_id": get_processor_account_id(
315
+ tx, client_id, resolved_processor
316
+ ),
317
+ "revenue_split_percentage": get_revenue_split_percentage(tx, client_id),
318
+ "donor_email": donor_email,
319
+ "donor_name": donor_name,
320
+ "description": description,
321
+ "descriptor": descriptor,
322
+ "metadata": metadata or {},
323
+ },
324
+ )
325
+
326
+
327
+ def get_or_create_customer_profile(
328
+ tx, processor_type: str, customer_data: Dict[str, Any]
329
+ ) -> Dict[str, Any]:
330
+ """Get or create a vaulted processor customer profile."""
331
+ if not _is_processor_enabled(tx, processor_type):
332
+ raise ValueError(
333
+ f"Payment processor '{processor_type}' is not enabled in feature flags"
334
+ )
335
+ adapter = _build_adapter(processor_type)
336
+ return adapter.get_or_create_customer_profile(tx, customer_data)
337
+
338
+
339
+ def attach_payment_method(
340
+ tx,
341
+ processor_type: str,
342
+ customer_profile_id: str,
343
+ payment_method_id: str,
344
+ payment_data: Optional[Dict[str, Any]] = None,
345
+ ) -> Dict[str, Any]:
346
+ """Attach a payment method to a vaulted processor customer profile."""
347
+ if not _is_processor_enabled(tx, processor_type):
348
+ raise ValueError(
349
+ f"Payment processor '{processor_type}' is not enabled in feature flags"
350
+ )
351
+ adapter = _build_adapter(processor_type)
352
+ return adapter.attach_payment_method(
353
+ tx, customer_profile_id, payment_method_id, payment_data
354
+ )
355
+
356
+
357
+ def detach_payment_method(
358
+ tx, processor_type: str, payment_method_id: str
359
+ ) -> Dict[str, Any]:
360
+ """Detach a payment method from a vaulted processor customer profile."""
361
+ if not _is_processor_enabled(tx, processor_type):
362
+ raise ValueError(
363
+ f"Payment processor '{processor_type}' is not enabled in feature flags"
364
+ )
365
+ adapter = _build_adapter(processor_type)
366
+ return adapter.detach_payment_method(tx, payment_method_id)
367
+
368
+
369
+ def delete_customer_profile(
370
+ tx, processor_type: str, customer_profile_id: str
371
+ ) -> Dict[str, Any]:
372
+ """Delete a vaulted processor customer profile."""
373
+ if not _is_processor_enabled(tx, processor_type):
374
+ raise ValueError(
375
+ f"Payment processor '{processor_type}' is not enabled in feature flags"
376
+ )
377
+ adapter = _build_adapter(processor_type)
378
+ return adapter.delete_customer_profile(tx, customer_profile_id)
@@ -50,6 +50,146 @@ class StripeAdapter(PaymentProcessorAdapter):
50
50
  # ACCOUNT MANAGEMENT
51
51
  # ========================================================================
52
52
 
53
+ def get_or_create_customer_profile(
54
+ self, tx, customer_data: Dict[str, Any]
55
+ ) -> Dict[str, Any]:
56
+ """Get an existing Stripe customer or create one for vaulted payment methods."""
57
+ try:
58
+ email_address = customer_data["email_address"]
59
+ existing = (
60
+ tx.table("payment_profiles")
61
+ .select(
62
+ columns=["customer_profile_id"],
63
+ where={"email_address": email_address, "src": "ST"},
64
+ orderby="is_default desc, sys_id desc",
65
+ )
66
+ .one()
67
+ )
68
+ if existing and existing.get("customer_profile_id"):
69
+ return {
70
+ "customer_profile_id": existing["customer_profile_id"],
71
+ "created": False,
72
+ }
73
+
74
+ full_name = customer_data.get("name") or " ".join(
75
+ part
76
+ for part in [
77
+ customer_data.get("first_name"),
78
+ customer_data.get("last_name"),
79
+ ]
80
+ if part
81
+ ).strip()
82
+
83
+ customer = stripe.Customer.create(
84
+ email=email_address,
85
+ name=full_name or email_address,
86
+ metadata={
87
+ "platform": "caringcent",
88
+ "email_address": email_address,
89
+ **(customer_data.get("metadata") or {}),
90
+ },
91
+ )
92
+ return {
93
+ "customer_profile_id": customer.id,
94
+ "created": True,
95
+ "customer": customer,
96
+ }
97
+ except stripe.error.StripeError as e:
98
+ raise ProcessorError(
99
+ processor="stripe",
100
+ error_code=e.code or "unknown",
101
+ error_message=str(e),
102
+ metadata={"email_address": customer_data.get("email_address")},
103
+ )
104
+
105
+ def attach_payment_method(
106
+ self,
107
+ tx,
108
+ customer_profile_id: str,
109
+ payment_method_id: str,
110
+ payment_data: Optional[Dict[str, Any]] = None,
111
+ ) -> Dict[str, Any]:
112
+ """Attach a Stripe payment method to a customer and optionally make it default."""
113
+ payment_data = payment_data or {}
114
+ try:
115
+ payment_method = stripe.PaymentMethod.retrieve(payment_method_id)
116
+ attached_customer = getattr(payment_method, "customer", None)
117
+ if attached_customer and str(attached_customer) != str(customer_profile_id):
118
+ raise ProcessorError(
119
+ processor="stripe",
120
+ error_code="payment_method_attached_elsewhere",
121
+ error_message="This payment method is already attached to another Stripe customer.",
122
+ )
123
+
124
+ if not attached_customer:
125
+ payment_method = stripe.PaymentMethod.attach(
126
+ payment_method_id, customer=customer_profile_id
127
+ )
128
+
129
+ if payment_data.get("set_default", True):
130
+ stripe.Customer.modify(
131
+ customer_profile_id,
132
+ email=payment_data.get("email_address"),
133
+ name=payment_data.get("name") or payment_data.get("email_address"),
134
+ invoice_settings={"default_payment_method": payment_method.id},
135
+ )
136
+
137
+ return {
138
+ "customer_profile_id": customer_profile_id,
139
+ "payment_profile_id": payment_method.id,
140
+ "payment_method": payment_method,
141
+ }
142
+ except ProcessorError:
143
+ raise
144
+ except stripe.error.StripeError as e:
145
+ raise ProcessorError(
146
+ processor="stripe",
147
+ error_code=e.code or "unknown",
148
+ error_message=str(e),
149
+ metadata={
150
+ "customer_profile_id": customer_profile_id,
151
+ "payment_method_id": payment_method_id,
152
+ },
153
+ )
154
+
155
+ def detach_payment_method(
156
+ self, tx, payment_method_id: str, payment_data: Optional[Dict[str, Any]] = None
157
+ ) -> Dict[str, Any]:
158
+ """Detach a Stripe payment method from any attached customer."""
159
+ try:
160
+ payment_method = stripe.PaymentMethod.retrieve(payment_method_id)
161
+ if getattr(payment_method, "customer", None):
162
+ payment_method = stripe.PaymentMethod.detach(payment_method_id)
163
+ return {
164
+ "payment_profile_id": payment_method_id,
165
+ "detached": True,
166
+ "payment_method": payment_method,
167
+ }
168
+ except stripe.error.StripeError as e:
169
+ raise ProcessorError(
170
+ processor="stripe",
171
+ error_code=e.code or "unknown",
172
+ error_message=str(e),
173
+ metadata={"payment_method_id": payment_method_id},
174
+ )
175
+
176
+ def delete_customer_profile(self, tx, customer_profile_id: str) -> Dict[str, Any]:
177
+ """Delete a Stripe vaulted customer."""
178
+ try:
179
+ result = stripe.Customer.delete(customer_profile_id)
180
+ return {
181
+ "customer_profile_id": customer_profile_id,
182
+ "deleted": bool(getattr(result, "deleted", False)),
183
+ "customer": result,
184
+ }
185
+ except stripe.error.StripeError as e:
186
+ raise ProcessorError(
187
+ processor="stripe",
188
+ error_code=e.code or "unknown",
189
+ error_message=str(e),
190
+ metadata={"customer_profile_id": customer_profile_id},
191
+ )
192
+
53
193
  def create_account(self, tx, client_data: Dict[str, Any]) -> Dict[str, Any]:
54
194
  """
55
195
  Create a Stripe Express Connected Account for a client school.
@@ -314,6 +454,90 @@ class StripeAdapter(PaymentProcessorAdapter):
314
454
  },
315
455
  }
316
456
 
457
+ def charge_stored_payment_method(self, tx, payment_data: Dict[str, Any]) -> Dict[str, Any]:
458
+ """Charge a stored Stripe PaymentMethod using an off-session PaymentIntent."""
459
+ try:
460
+ amount_cents = payment_data["amount"]
461
+ currency = payment_data.get("currency", "usd")
462
+ payment_method = payment_data["payment_method"]
463
+ customer_profile_id = payment_data.get("customer_profile_id")
464
+ processor_account_id = payment_data.get("processor_account_id")
465
+ revenue_split_percentage = payment_data.get("revenue_split_percentage", 0)
466
+
467
+ if not customer_profile_id:
468
+ raise ProcessorError(
469
+ processor="stripe",
470
+ error_code="missing_customer_profile",
471
+ error_message="Stored Stripe charges require a customer_profile_id.",
472
+ )
473
+ if not processor_account_id:
474
+ raise ProcessorError(
475
+ processor="stripe",
476
+ error_code="missing_processor_account",
477
+ error_message="Stripe processor account is not configured for this client.",
478
+ )
479
+
480
+ platform_fee_cents = self.calculate_platform_fee(
481
+ amount_cents, revenue_split_percentage
482
+ )
483
+
484
+ payment_intent = stripe.PaymentIntent.create(
485
+ amount=amount_cents,
486
+ currency=currency,
487
+ customer=customer_profile_id,
488
+ payment_method=payment_method,
489
+ confirm=True,
490
+ off_session=True,
491
+ application_fee_amount=platform_fee_cents,
492
+ transfer_data={"destination": processor_account_id},
493
+ description=payment_data.get("description"),
494
+ metadata={
495
+ "client_id": str(payment_data.get("client_id")),
496
+ "donor_email": payment_data.get("donor_email"),
497
+ "donor_name": payment_data.get("donor_name"),
498
+ "platform": "caringcent",
499
+ "environment": self.environment,
500
+ **(payment_data.get("metadata") or {}),
501
+ },
502
+ )
503
+
504
+ charge_id = getattr(payment_intent, "latest_charge", None)
505
+ return {
506
+ "processor_transaction_id": payment_intent.id,
507
+ "processor_charge_id": charge_id,
508
+ "status": payment_intent.status,
509
+ "success": payment_intent.status == "succeeded",
510
+ "amount": payment_intent.amount,
511
+ "currency": payment_intent.currency,
512
+ "authorization_code": payment_intent.id,
513
+ "platform_fee": payment_intent.application_fee_amount or platform_fee_cents,
514
+ "client_amount": payment_intent.amount - (payment_intent.application_fee_amount or platform_fee_cents),
515
+ "error_message": None,
516
+ "metadata": {
517
+ "payment_intent": payment_intent,
518
+ },
519
+ }
520
+
521
+ except ProcessorError:
522
+ raise
523
+ except stripe.error.StripeError as e:
524
+ return {
525
+ "processor_transaction_id": None,
526
+ "processor_charge_id": None,
527
+ "status": "failed",
528
+ "success": False,
529
+ "amount": payment_data.get("amount"),
530
+ "currency": payment_data.get("currency", "usd"),
531
+ "authorization_code": None,
532
+ "platform_fee": 0,
533
+ "client_amount": 0,
534
+ "error_message": str(e),
535
+ "metadata": {
536
+ "error_code": e.code,
537
+ "error_type": type(e).__name__,
538
+ },
539
+ }
540
+
317
541
  # ========================================================================
318
542
  # PAYMENT CAPTURE
319
543
  # ========================================================================