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.
- {velocity_python-0.0.217/src/velocity_python.egg-info → velocity_python-0.0.219}/PKG-INFO +1 -1
- {velocity_python-0.0.217 → velocity_python-0.0.219}/pyproject.toml +1 -1
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/__init__.py +1 -1
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/payment/__init__.py +22 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/payment/base_adapter.py +88 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/payment/braintree_adapter.py +81 -0
- velocity_python-0.0.219/src/velocity/payment/profiles.py +71 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/payment/router.py +105 -5
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/payment/stripe_adapter.py +224 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219/src/velocity_python.egg-info}/PKG-INFO +1 -1
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity_python.egg-info/SOURCES.txt +5 -0
- velocity_python-0.0.219/tests/test_payment_braintree_adapter.py +77 -0
- velocity_python-0.0.219/tests/test_payment_profiles.py +72 -0
- velocity_python-0.0.219/tests/test_payment_router.py +107 -0
- velocity_python-0.0.219/tests/test_payment_stripe_adapter.py +186 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/LICENSE +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/README.md +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/setup.cfg +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/invoices.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/orders.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/payments.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/purchase_orders.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/tests/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/tests/test_email_processing.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/logging.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_decorators.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_table_alter.py +0 -0
- {velocity_python-0.0.217 → velocity_python-0.0.219}/tests/test_where_clause_validation.py +0 -0
|
@@ -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
|
# ========================================================================
|
{velocity_python-0.0.217 → velocity_python-0.0.219}/src/velocity/payment/braintree_adapter.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
60
|
+
if processor_type == "braintree":
|
|
58
61
|
return BraintreeAdapter(config)
|
|
59
|
-
|
|
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
|
# ========================================================================
|