diff --git a/.mypy.ini b/.mypy.ini index 51060591d..ea5da189f 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -16,10 +16,6 @@ ignore_missing_imports = True # https://github.com/shapely/shapely/issues/721 ignore_missing_imports = True -[mypy-stripe.*] -# https://github.com/stripe/stripe-python/issues/650 -ignore_missing_imports = True - [mypy-sqlalchemy.engine.*] ignore_missing_imports = True diff --git a/README.md b/README.md index 7e70e6ccf..4a6204b66 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,10 @@ Or, you can create an account and simultaneously make it an admin by using `./fl E-mail sending is disabled in development (but is printed out on the console). You can also log in directly by setting `BYPASS_LOGIN=True` in `config/development.cfg` and then using a URL of the form e.g. `/login/admin@test.invalid`. +### Testing payments + +The easiest way to test the payments flow (marginally less easy if you don't have a Stripe account) is to use Stripe test keys and pay by card with Stripe's test cards. To test incoming webhooks you can use a service like ngrok or tailscale funnel to expose your local dev instance to the web (temporarily!). + ### Database Migrations - `./flask db migrate -m 'Migration name'` to generate migration scripts when models have been updated. diff --git a/apps/admin/payments.py b/apps/admin/payments.py index 283361ea2..24f75a24d 100644 --- a/apps/admin/payments.py +++ b/apps/admin/payments.py @@ -19,7 +19,7 @@ from sqlalchemy.sql.functions import func -from main import db, stripe +from main import db, get_stripe_client from models.payment import ( Payment, RefundRequest, @@ -190,8 +190,9 @@ def update_payment(payment_id): payment.lock() if payment.provider == "stripe": + stripe_client = get_stripe_client(app.config) try: - stripe_update_payment(payment) + stripe_update_payment(stripe_client, payment) except StripeUpdateConflict as e: app.logger.warn(f"StripeUpdateConflict updating payment: {e}") flash("Unable to update due to a status conflict") @@ -446,9 +447,11 @@ def refund(payment_id): payment.currency, ) + stripe_client = get_stripe_client(app.config) + if form.stripe_refund.data: app.logger.info("Refunding using Stripe") - charge = stripe.Charge.retrieve(payment.charge_id) + charge = stripe_client.charges.retrieve(payment.charge_id) if charge.refunded: # This happened unexpectedly - send the email as usual @@ -506,8 +509,11 @@ def refund(payment_id): if form.stripe_refund.data: try: - stripe_refund = stripe.Refund.create( - charge=payment.charge_id, amount=refund.amount_int + stripe_refund = stripe_client.refunds.create( + params={ + "charge": payment.charge_id, + "amount": refund.amount_int, + } ) except Exception as e: diff --git a/apps/payments/refund.py b/apps/payments/refund.py index 9a5190e7c..7e4a77c55 100644 --- a/apps/payments/refund.py +++ b/apps/payments/refund.py @@ -1,11 +1,11 @@ from decimal import Decimal -from stripe.error import StripeError +from stripe import StripeError from flask import current_app as app, render_template from flask_mailman import EmailMessage from typing import Optional from models.payment import RefundRequest, StripePayment, StripeRefund, BankRefund -from main import stripe, db +from main import get_stripe_client, db from ..common.email import from_email @@ -22,16 +22,21 @@ def create_stripe_refund( ) -> Optional[StripeRefund]: """Initiate a stripe refund, and return the StripeRefund object.""" # TODO: This should probably live in the stripe module. + stripe_client = get_stripe_client(app.config) assert amount > 0 - charge = stripe.Charge.retrieve(payment.charge_id) + charge = stripe_client.charges.retrieve(payment.charge_id) if charge.refunded: return None refund = StripeRefund(payment, amount) try: - stripe_refund = stripe.Refund.create( - charge=payment.charge_id, amount=refund.amount_int, metadata=metadata + stripe_refund = stripe_client.refunds.create( + params={ + "charge": payment.charge_id, + "amount": refund.amount_int, + "metadata": metadata, + } ) except StripeError as e: raise RefundException("Error creating Stripe refund") from e diff --git a/apps/payments/stripe.py b/apps/payments/stripe.py index b721a66ec..8d1167016 100644 --- a/apps/payments/stripe.py +++ b/apps/payments/stripe.py @@ -9,6 +9,7 @@ complicate this code. """ import logging +from typing import Optional from flask import ( render_template, @@ -23,9 +24,9 @@ from flask_mailman import EmailMessage from wtforms import SubmitField from sqlalchemy.orm.exc import NoResultFound -from stripe.error import AuthenticationError +import stripe -from main import db, stripe +from main import db, get_stripe_client from models.payment import StripePayment from ..common import feature_enabled from ..common.email import from_email @@ -78,20 +79,23 @@ def stripe_capture(payment_id): logger.warn("Unable to capture payment as Stripe is disabled") flash("Card payments are currently unavailable. Please try again later") return redirect(url_for("users.purchases")) + stripe_client = get_stripe_client(app.config) if payment.intent_id is None: # Create the payment intent with Stripe. This intent will persist across retries. - intent = stripe.PaymentIntent.create( - amount=payment.amount_int, - currency=payment.currency.upper(), - statement_descriptor_suffix=payment.description, - metadata={"user_id": current_user.id, "payment_id": payment.id}, + intent = stripe_client.payment_intents.create( + params={ + "amount": payment.amount_int, + "currency": payment.currency.upper(), + "statement_descriptor_suffix": payment.description, + "metadata": {"user_id": current_user.id, "payment_id": payment.id}, + }, ) payment.intent_id = intent.id db.session.commit() else: # Reuse a previously-created payment intent - intent = stripe.PaymentIntent.retrieve(payment.intent_id) + intent = stripe_client.payment_intents.retrieve(payment.intent_id) if intent.status == "succeeded": logger.warn(f"Intent already succeeded, not capturing again") payment.state = "charging" @@ -170,8 +174,9 @@ def stripe_waiting(payment_id): @payments.route("/stripe-webhook", methods=["POST"]) def stripe_webhook(): + stripe_client = get_stripe_client(app.config) try: - event = stripe.Webhook.construct_event( + event = stripe_client.construct_event( request.data, request.headers["STRIPE_SIGNATURE"], app.config.get("STRIPE_WEBHOOK_KEY"), @@ -179,7 +184,7 @@ def stripe_webhook(): except ValueError: logger.exception("Error decoding Stripe webhook") abort(400) - except stripe.error.SignatureVerificationError: + except stripe.SignatureVerificationError: logger.exception("Error verifying Stripe webhook signature") abort(400) @@ -212,29 +217,54 @@ def stripe_ping(_type, _obj): return ("", 200) -def stripe_update_payment(payment: StripePayment, intent: stripe.PaymentIntent = None): +def stripe_update_payment( + stripe_client: stripe.StripeClient, + payment: StripePayment, + intent: Optional[stripe.PaymentIntent] = None, +): """Update a Stripe payment. - If a PaymentIntent object is not passed in, this will fetch the payment details from the Stripe API. + If a PaymentIntent object is not passed in, this will fetch the payment details from + the Stripe API. """ + intent_is_fresh = False if intent is None: - intent = stripe.PaymentIntent.retrieve(payment.intent_id) + intent = stripe_client.payment_intents.retrieve( + payment.intent_id, params=dict(expand=["latest_charge"]) + ) + intent_is_fresh = True - if len(intent.charges) == 0: + if intent.latest_charge is None: # Intent does not have a charge (yet?), do nothing return - elif len(intent.charges) > 1: - raise StripeUpdateUnexpected( - f"Payment intent #{intent['id']} has more than one charge" - ) - charge = intent.charges.data[0] + if isinstance(intent.latest_charge, stripe.Charge): + # The payment intent object has been expanded already + charge = intent.latest_charge + else: + charge = stripe_client.charges.retrieve(intent.latest_charge) + + if payment.charge_id is not None and payment.charge_id != charge.id: + # The payment's failed and been retried, and this might be a + # delayed webhook notification for the old charge ID. So we + # need to check whether it's the latest. + if intent_is_fresh: + fresh_intent = intent + else: + fresh_intent = stripe_client.payment_intents.retrieve( + payment.intent_id, params=dict(expand=["latest_charge"]) + ) - if payment.charge_id is not None and payment.charge_id != charge["id"]: - logger.warn( - f"Charge ID for intent {intent['id']} has changed from {payment.charge_id} to {charge['id']}" - ) + if fresh_intent.latest_charge == charge.id: + logger.warn( + f"Charge ID for intent {intent.id} has changed from {payment.charge_id} to {charge.id}" + ) + else: + logger.warn( + f"Charge ID {charge.id} for intent {intent.id} is out of date, ignoring" + ) + return - payment.charge_id = charge["id"] + payment.charge_id = charge.id if charge.refunded: return stripe_payment_refunded(payment) @@ -367,8 +397,9 @@ def stripe_payment_intent_updated(hook_type, intent): payment.id, ) + stripe_client = get_stripe_client(app.config) try: - stripe_update_payment(payment, intent) + stripe_update_payment(stripe_client, payment, intent) except StripeUpdateConflict: abort(409) except StripeUpdateUnexpected: @@ -423,10 +454,11 @@ def stripe_validate(): else: result.append((False, "Webhook key not configured")) + stripe_client = get_stripe_client(app.config) try: - webhooks = stripe.WebhookEndpoint.list() + webhooks = stripe_client.webhook_endpoints.list() result.append((True, "Connection to Stripe API succeeded")) - except AuthenticationError as e: + except stripe.AuthenticationError as e: result.append((False, f"Connecting to Stripe failed: {e}")) return result diff --git a/main.py b/main.py index 4a1d8112a..40dcc81b5 100644 --- a/main.py +++ b/main.py @@ -64,6 +64,13 @@ def include_object(object, name, type_, reflected, compare_to): wise = None +def get_stripe_client(config) -> stripe.StripeClient: + return stripe.StripeClient( + api_key=config["STRIPE_SECRET_KEY"], + stripe_version="2023-10-16", + ) + + def check_cache_configuration(): """Check the cache configuration is appropriate for production""" if cache.cache.__class__.__name__ == "SimpleCache": @@ -153,7 +160,6 @@ def load_user(userid): login_manager.anonymous_user = load_anonymous_user - stripe.api_key = app.config["STRIPE_SECRET_KEY"] global wise wise = pywisetransfer.Client( api_key=app.config["TRANSFERWISE_API_TOKEN"], diff --git a/poetry.lock b/poetry.lock index 2afd6bc12..c5120c6e4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3717,17 +3717,18 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] [[package]] name = "stripe" -version = "2.38.0" +version = "8.0.0" description = "Python bindings for the Stripe API" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" files = [ - {file = "stripe-2.38.0-py2.py3-none-any.whl", hash = "sha256:ea26abb9c4f6eea94dda630157a1f713e5925b9ccec0fbac5e53a9c6e4541bbd"}, - {file = "stripe-2.38.0.tar.gz", hash = "sha256:b37bc34045b2becad6ce40a5ca0abd3c83ee2528e3dce0149026b29f30ac90a6"}, + {file = "stripe-8.0.0-py2.py3-none-any.whl", hash = "sha256:0c6c7ded4ae98340107a9d61f2d029ea0802ee88580c9bebb3693230557e766c"}, + {file = "stripe-8.0.0.tar.gz", hash = "sha256:16d9086c95801fb6931565ff8a3dd7ca3e85beb4cb6fdb2020e555f4254ff643"}, ] [package.dependencies] requests = {version = ">=2.20", markers = "python_version >= \"3.0\""} +typing-extensions = {version = ">=4.5.0", markers = "python_version >= \"3.7\""} [[package]] name = "tinycss2" @@ -4334,4 +4335,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "~3.11" -content-hash = "c587f35b6fe7b37b4420ea56486da928c5348d0af8e98696f292aef2a291b081" +content-hash = "8c0869b665fb0c7e3fea9a9f677069ce3b5060048da3c8eb040aaff709058f82" diff --git a/pyproject.toml b/pyproject.toml index c9762c93e..13d2df9f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ pybarcode = {git = "https://github.com/emfcamp/python-barcode"} pillow = "~=10.2" icalendar = "==3.11.7" pytz = "*" -stripe = "~=2.38.0" +stripe = "~=8.0.0" ofxparse = "==0.16" python-dateutil = "*" slotmachine = {git = "https://github.com/emfcamp/slotmachine.git"} diff --git a/tests/cassettes/test_create_stripe_purchase.yaml b/tests/cassettes/test_create_stripe_purchase.yaml index 0db69b416..5b904ff22 100644 --- a/tests/cassettes/test_create_stripe_purchase.yaml +++ b/tests/cassettes/test_create_stripe_purchase.yaml @@ -1,82 +1,101 @@ interactions: - request: - body: amount=23000¤cy=GBP&statement_descriptor_suffix=EMF+2020+purchase&metadata[user_id]=1&metadata[payment_id]=1 + body: amount=23000¤cy=GBP&statement_descriptor_suffix=EMF+2025+purchase&metadata[user_id]=1&metadata[payment_id]=1 headers: Accept: - '*/*' Accept-Encoding: - - gzip, deflate + - gzip, deflate, br Connection: - keep-alive + Content-Length: + - '114' Content-Type: - application/x-www-form-urlencoded Idempotency-Key: - - 467112d0-3ed0-4d55-90f6-04599a2fdca5 + - 90582ef1-1afd-4010-97ed-ad91e3b01c5d + Stripe-Version: + - '2023-10-16' User-Agent: - - Stripe/v1 PythonBindings/2.38.0 + - Stripe/v1 PythonBindings/8.0.0 X-Stripe-Client-User-Agent: - - '{"bindings_version": "2.38.0", "lang": "python", "publisher": "stripe", "httplib": - "requests", "lang_version": "3.7.6", "platform": "Linux-4.19.76-linuxkit-x86_64-with-debian-10.2", - "uname": "Linux d9e02b76d0f1 4.19.76-linuxkit #1 SMP Thu Oct 17 19:31:58 UTC - 2019 x86_64 "}' + - '{"bindings_version": "8.0.0", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.11.7", "platform": "Linux-5.15.49-linuxkit-aarch64-with-glibc2.31", + "uname": "Linux 5cb7915c0fb8 5.15.49-linuxkit #1 SMP PREEMPT Tue Sep 13 07:51:32 + UTC 2022 aarch64 "}' + authorization: + - DUMMY method: POST uri: https://api.stripe.com/v1/payment_intents response: body: - string: "{\n \"id\": \"pi_1GUslpIcI91cWsdeheAuRsyg\",\n \"object\": \"payment_intent\"\ - ,\n \"allowed_source_types\": [\n \"card\"\n ],\n \"amount\": 23000,\n\ - \ \"amount_capturable\": 0,\n \"amount_received\": 0,\n \"application\"\ - : null,\n \"application_fee_amount\": null,\n \"canceled_at\": null,\n \ - \ \"cancellation_reason\": null,\n \"capture_method\": \"automatic\",\n \ - \ \"charges\": {\n \"object\": \"list\",\n \"data\": [\n\n ],\n \ - \ \"has_more\": false,\n \"total_count\": 0,\n \"url\": \"/v1/charges?payment_intent=pi_1GUslpIcI91cWsdeheAuRsyg\"\ - \n },\n \"client_secret\": \"pi_1GUslpIcI91cWsdeheAuRsyg_secret_jm7QnCtrTMjZKeYmqmmmlXZlj\"\ - ,\n \"confirmation_method\": \"automatic\",\n \"created\": 1586161027,\n\ - \ \"currency\": \"gbp\",\n \"customer\": null,\n \"description\": null,\n\ - \ \"invoice\": null,\n \"last_payment_error\": null,\n \"livemode\": false,\n\ - \ \"metadata\": {\n \"user_id\": \"1\",\n \"payment_id\": \"1\"\n \ - \ },\n \"next_action\": null,\n \"next_source_action\": null,\n \"on_behalf_of\"\ - : null,\n \"payment_method\": null,\n \"payment_method_options\": {\n \ - \ \"card\": {\n \"installments\": null,\n \"request_three_d_secure\"\ - : \"automatic\"\n }\n },\n \"payment_method_types\": [\n \"card\"\n\ - \ ],\n \"receipt_email\": null,\n \"review\": null,\n \"setup_future_usage\"\ - : null,\n \"shipping\": null,\n \"source\": null,\n \"statement_descriptor\"\ - : null,\n \"statement_descriptor_suffix\": \"EMF 2020 purchase\",\n \"status\"\ - : \"requires_source\",\n \"transfer_data\": null,\n \"transfer_group\":\ - \ null\n}\n" + string: "{\n \"id\": \"pi_3OiHFbHz0MWR65Xj0KidZr5i\",\n \"object\": \"payment_intent\",\n + \ \"amount\": 23000,\n \"amount_capturable\": 0,\n \"amount_details\": {\n + \ \"tip\": {}\n },\n \"amount_received\": 0,\n \"application\": null,\n + \ \"application_fee_amount\": null,\n \"automatic_payment_methods\": {\n + \ \"allow_redirects\": \"always\",\n \"enabled\": true\n },\n \"canceled_at\": + null,\n \"cancellation_reason\": null,\n \"capture_method\": \"automatic\",\n + \ \"client_secret\": \"pi_3OiHFbHz0MWR65Xj0KidZr5i_secret_Z9ikdFm8aaFuujx7VMlfTHP4M\",\n + \ \"confirmation_method\": \"automatic\",\n \"created\": 1707574191,\n \"currency\": + \"gbp\",\n \"customer\": null,\n \"description\": null,\n \"invoice\": + null,\n \"last_payment_error\": null,\n \"latest_charge\": null,\n \"livemode\": + false,\n \"metadata\": {\n \"payment_id\": \"1\",\n \"user_id\": \"1\"\n + \ },\n \"next_action\": null,\n \"on_behalf_of\": null,\n \"payment_method\": + null,\n \"payment_method_configuration_details\": null,\n \"payment_method_options\": + {\n \"card\": {\n \"installments\": null,\n \"mandate_options\": + null,\n \"network\": null,\n \"request_three_d_secure\": \"automatic\"\n + \ }\n },\n \"payment_method_types\": [\n \"card\"\n ],\n \"processing\": + null,\n \"receipt_email\": null,\n \"review\": null,\n \"setup_future_usage\": + null,\n \"shipping\": null,\n \"source\": null,\n \"statement_descriptor\": + null,\n \"statement_descriptor_suffix\": \"EMF 2025 purchase\",\n \"status\": + \"requires_payment_method\",\n \"transfer_data\": null,\n \"transfer_group\": + null\n}" headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET,HEAD,PUT,PATCH,POST,DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required, + X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store Connection: - keep-alive + Content-Length: + - '1439' + Content-Security-Policy: + - report-uri https://q.stripe.com/csp-report?p=v1%2Fpayment_intents; block-all-mixed-content; + default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; + img-src 'self'; script-src 'self' 'report-sample'; style-src 'self' Content-Type: - application/json Date: - - Mon, 06 Apr 2020 08:17:07 GMT + - Sat, 10 Feb 2024 14:09:52 GMT + Idempotency-Key: + - 90582ef1-1afd-4010-97ed-ad91e3b01c5d + Original-Request: + - req_reP2uzOE3Oy0vT + Request-Id: + - req_reP2uzOE3Oy0vT Server: - nginx Strict-Transport-Security: - - max-age=31556926; includeSubDomains; preload - access-control-allow-credentials: - - 'true' - access-control-allow-methods: - - GET, POST, HEAD, OPTIONS, DELETE - access-control-allow-origin: - - '*' - access-control-expose-headers: - - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required - access-control-max-age: - - '300' - cache-control: - - no-cache, no-store - idempotency-key: - - 467112d0-3ed0-4d55-90f6-04599a2fdca5 - original-request: - - req_MGgq5mNOSPFvrt - request-id: - - req_MGgq5mNOSPFvrt - stripe-should-retry: + - max-age=63072000; includeSubDomains; preload + Stripe-Should-Retry: - 'false' - stripe-version: - - '2016-07-06' + Stripe-Version: + - '2023-10-16' + Vary: + - Origin + X-Stripe-Non-Api-Overhead-Duration-Ms: + - '142.0' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode status: code: 200 message: OK @@ -86,155 +105,296 @@ interactions: Accept: - '*/*' Accept-Encoding: - - gzip, deflate + - gzip, deflate, br Connection: - keep-alive + Stripe-Version: + - '2023-10-16' User-Agent: - - Stripe/v1 PythonBindings/2.38.0 - X-Stripe-Client-Telemetry: - - '{"last_request_metrics": {"request_id": "req_MGgq5mNOSPFvrt", "request_duration_ms": - 305}}' + - Stripe/v1 PythonBindings/8.0.0 X-Stripe-Client-User-Agent: - - '{"bindings_version": "2.38.0", "lang": "python", "publisher": "stripe", "httplib": - "requests", "lang_version": "3.7.6", "platform": "Linux-4.19.76-linuxkit-x86_64-with-debian-10.2", - "uname": "Linux d9e02b76d0f1 4.19.76-linuxkit #1 SMP Thu Oct 17 19:31:58 UTC - 2019 x86_64 "}' + - '{"bindings_version": "8.0.0", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.11.7", "platform": "Linux-5.15.49-linuxkit-aarch64-with-glibc2.31", + "uname": "Linux 5cb7915c0fb8 5.15.49-linuxkit #1 SMP PREEMPT Tue Sep 13 07:51:32 + UTC 2022 aarch64 "}' + authorization: + - DUMMY method: GET - uri: https://api.stripe.com/v1/charges/ch_1GUslxIcI91cWsde5jePBfRQ + uri: https://api.stripe.com/v1/charges/ch_3OibCXHz0MWR65Xj09SoiLkW response: body: - string: "{\n \"id\": \"ch_1GUslxIcI91cWsde5jePBfRQ\",\n \"object\": \"charge\"\ - ,\n \"amount\": 23000,\n \"amount_refunded\": 0,\n \"application\": null,\n\ - \ \"application_fee\": null,\n \"application_fee_amount\": null,\n \"balance_transaction\"\ - : \"txn_1GUslxIcI91cWsdenOKhyR4i\",\n \"billing_details\": {\n \"address\"\ - : {\n \"city\": null,\n \"country\": null,\n \"line1\": null,\n\ - \ \"line2\": null,\n \"postal_code\": \"42424\",\n \"state\"\ - : null\n },\n \"email\": \"test@example.com\",\n \"name\": \"Test\"\ - ,\n \"phone\": null\n },\n \"calculated_statement_descriptor\": \"EMFCAMP*\ - \ EMF 2020 P\",\n \"captured\": true,\n \"created\": 1586171145,\n \"currency\"\ - : \"gbp\",\n \"customer\": null,\n \"description\": null,\n \"destination\"\ - : null,\n \"dispute\": null,\n \"disputed\": false,\n \"failure_code\"\ - : null,\n \"failure_message\": null,\n \"fraud_details\": {\n },\n \"\ - invoice\": null,\n \"livemode\": false,\n \"metadata\": {\n \"user_id\"\ - : \"1\",\n \"payment_id\": \"2\"\n },\n \"on_behalf_of\": null,\n \"\ - order\": null,\n \"outcome\": {\n \"network_status\": \"approved_by_network\"\ - ,\n \"reason\": null,\n \"risk_level\": \"normal\",\n \"risk_score\"\ - : 55,\n \"seller_message\": \"Payment complete.\",\n \"type\": \"authorized\"\ - \n },\n \"paid\": true,\n \"payment_intent\": \"pi_1GUslpIcI91cWsdeheAuRsyg\"\ - ,\n \"payment_method\": \"pm_1GUslxIcI91cWsdeBYozsFYU\",\n \"payment_method_details\"\ - : {\n \"card\": {\n \"brand\": \"visa\",\n \"checks\": {\n \ - \ \"address_line1_check\": null,\n \"address_postal_code_check\"\ - : \"pass\",\n \"cvc_check\": \"pass\"\n },\n \"country\"\ - : \"US\",\n \"exp_month\": 4,\n \"exp_year\": 2024,\n \"fingerprint\"\ - : \"VKnD4kq2Jm2zibNl\",\n \"funding\": \"credit\",\n \"installments\"\ - : null,\n \"last4\": \"4242\",\n \"network\": \"visa\",\n \"\ - three_d_secure\": null,\n \"wallet\": null\n },\n \"type\": \"\ - card\"\n },\n \"receipt_email\": null,\n \"receipt_number\": null,\n \"\ - receipt_url\": \"https://pay.stripe.com/receipts/acct_14i5G7IcI91cWsde/ch_1GUslxIcI91cWsde5jePBfRQ/rcpt_H2yjkWzJc46LbnTbSqH7RdXWbbaVDSU\"\ - ,\n \"refunded\": false,\n \"refunds\": {\n \"object\": \"list\",\n \ - \ \"data\": [\n\n ],\n \"has_more\": false,\n \"total_count\":\ - \ 0,\n \"url\": \"/v1/charges/ch_1GUslxIcI91cWsde5jePBfRQ/refunds\"\n \ - \ },\n \"review\": null,\n \"shipping\": null,\n \"source\": null,\n \"\ - source_transfer\": null,\n \"statement_descriptor\": null,\n \"statement_descriptor_suffix\"\ - : \"EMF 2020 purchase\",\n \"status\": \"succeeded\",\n \"transfer_data\"\ - : null,\n \"transfer_group\": null\n}\n" + string: "{\n \"id\": \"ch_3OibCXHz0MWR65Xj09SoiLkW\",\n \"object\": \"charge\",\n + \ \"amount\": 21000,\n \"amount_captured\": 21000,\n \"amount_refunded\": + 0,\n \"application\": null,\n \"application_fee\": null,\n \"application_fee_amount\": + null,\n \"balance_transaction\": \"txn_3OibCXHz0MWR65Xj0FGYSo9U\",\n \"billing_details\": + {\n \"address\": {\n \"city\": null,\n \"country\": null,\n \"line1\": + null,\n \"line2\": null,\n \"postal_code\": \"11111\",\n \"state\": + null\n },\n \"email\": \"test@example.com\",\n \"name\": \"Initial Admin + User\",\n \"phone\": null\n },\n \"calculated_statement_descriptor\": + \"STRIPE* EMF 2022 PURCH\",\n \"captured\": true,\n \"created\": 1707650896,\n + \ \"currency\": \"gbp\",\n \"customer\": null,\n \"description\": null,\n + \ \"destination\": null,\n \"dispute\": null,\n \"disputed\": false,\n \"failure_balance_transaction\": + null,\n \"failure_code\": null,\n \"failure_message\": null,\n \"fraud_details\": + {},\n \"invoice\": null,\n \"livemode\": false,\n \"metadata\": {\n \"payment_id\": + \"1\",\n \"user_id\": \"1\"\n },\n \"on_behalf_of\": null,\n \"order\": + null,\n \"outcome\": {\n \"network_status\": \"approved_by_network\",\n + \ \"reason\": null,\n \"risk_level\": \"normal\",\n \"risk_score\": + 47,\n \"seller_message\": \"Payment complete.\",\n \"type\": \"authorized\"\n + \ },\n \"paid\": true,\n \"payment_intent\": \"pi_3OibCXHz0MWR65Xj0sgHN7Ni\",\n + \ \"payment_method\": \"pm_1OibCmHz0MWR65XjLoAoi9pj\",\n \"payment_method_details\": + {\n \"card\": {\n \"amount_authorized\": 21000,\n \"brand\": + \"visa\",\n \"checks\": {\n \"address_line1_check\": null,\n \"address_postal_code_check\": + \"pass\",\n \"cvc_check\": \"pass\"\n },\n \"country\": \"US\",\n + \ \"exp_month\": 1,\n \"exp_year\": 2030,\n \"extended_authorization\": + {\n \"status\": \"disabled\"\n },\n \"fingerprint\": \"qyoEjYqy9x1BibQI\",\n + \ \"funding\": \"credit\",\n \"incremental_authorization\": {\n \"status\": + \"unavailable\"\n },\n \"installments\": null,\n \"last4\": + \"4242\",\n \"mandate\": null,\n \"multicapture\": {\n \"status\": + \"unavailable\"\n },\n \"network\": \"visa\",\n \"network_token\": + {\n \"used\": false\n },\n \"overcapture\": {\n \"maximum_amount_capturable\": + 21000,\n \"status\": \"unavailable\"\n },\n \"three_d_secure\": + null,\n \"wallet\": null\n },\n \"type\": \"card\"\n },\n \"radar_options\": + {},\n \"receipt_email\": null,\n \"receipt_number\": null,\n \"receipt_url\": + \"https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xT2RocXVIejBNV1I2NVhqKIjgoq4GMgYedyoyka46LBahzyoz_mteiHuUGNdxRBku7tg6ApTa33nAeRp-PNF-XsrlCpM5MQnZwei3\",\n + \ \"refunded\": false,\n \"review\": null,\n \"shipping\": null,\n \"source\": + null,\n \"source_transfer\": null,\n \"statement_descriptor\": null,\n \"statement_descriptor_suffix\": + \"EMF 2022 purchase\",\n \"status\": \"succeeded\",\n \"transfer_data\": + null,\n \"transfer_group\": null\n}" headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET,HEAD,PUT,PATCH,POST,DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required, + X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store Connection: - keep-alive + Content-Length: + - '2807' + Content-Security-Policy: + - report-uri https://q.stripe.com/csp-report?p=v1%2Fcharges%2F%3Acharge; block-all-mixed-content; + default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; + img-src 'self'; script-src 'self' 'report-sample'; style-src 'self' Content-Type: - application/json Date: - - Tue, 07 Apr 2020 10:35:03 GMT + - Sun, 11 Feb 2024 11:31:20 GMT + Request-Id: + - req_VdpSuQFycMuN0D Server: - nginx Strict-Transport-Security: - - max-age=31556926; includeSubDomains; preload - access-control-allow-credentials: - - 'true' - access-control-allow-methods: - - GET, POST, HEAD, OPTIONS, DELETE - access-control-allow-origin: - - '*' - access-control-expose-headers: - - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required - access-control-max-age: - - '300' - cache-control: - - no-cache, no-store - request-id: - - req_PTgYlSPVyo8kBl - stripe-version: - - '2016-07-06' + - max-age=63072000; includeSubDomains; preload + Stripe-Version: + - '2023-10-16' + Vary: + - Origin + X-Stripe-Non-Api-Overhead-Duration-Ms: + - '79.99999999999999' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode status: code: 200 message: OK - request: - body: charge=ch_1GUslxIcI91cWsde5jePBfRQ&amount=21000&metadata[refund_request]=1 + body: charge=ch_3OibCXHz0MWR65Xj09SoiLkW&amount=21000&metadata[refund_request]=1 headers: Accept: - '*/*' Accept-Encoding: - - gzip, deflate + - gzip, deflate, br Connection: - keep-alive + Content-Length: + - '74' Content-Type: - application/x-www-form-urlencoded Idempotency-Key: - - 14d0d6b0-d636-49bb-998a-a3a517a392aa + - 37370c9d-b7ea-42d2-b8d0-b70e33e2e0d4 + Stripe-Version: + - '2023-10-16' User-Agent: - - Stripe/v1 PythonBindings/2.38.0 + - Stripe/v1 PythonBindings/8.0.0 X-Stripe-Client-Telemetry: - - '{"last_request_metrics": {"request_id": "req_PTgYlSPVyo8kBl", "request_duration_ms": - 453}}' + - '{"last_request_metrics": {"request_id": "req_VdpSuQFycMuN0D", "request_duration_ms": + 0}}' X-Stripe-Client-User-Agent: - - '{"bindings_version": "2.38.0", "lang": "python", "publisher": "stripe", "httplib": - "requests", "lang_version": "3.7.6", "platform": "Linux-4.19.76-linuxkit-x86_64-with-debian-10.2", - "uname": "Linux d9e02b76d0f1 4.19.76-linuxkit #1 SMP Thu Oct 17 19:31:58 UTC - 2019 x86_64 "}' + - '{"bindings_version": "8.0.0", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.11.7", "platform": "Linux-5.15.49-linuxkit-aarch64-with-glibc2.31", + "uname": "Linux 5cb7915c0fb8 5.15.49-linuxkit #1 SMP PREEMPT Tue Sep 13 07:51:32 + UTC 2022 aarch64 "}' + authorization: + - DUMMY method: POST uri: https://api.stripe.com/v1/refunds response: body: - string: "{\n \"id\": \"re_1GVEloIcI91cWsdemdIEKjOK\",\n \"object\": \"refund\"\ - ,\n \"amount\": 21000,\n \"balance_transaction\": \"txn_1GVEloIcI91cWsdeWsmUp4Fr\"\ - ,\n \"charge\": \"ch_1GUslxIcI91cWsde5jePBfRQ\",\n \"created\": 1586255704,\n\ - \ \"currency\": \"gbp\",\n \"metadata\": {\n \"refund_request\": \"1\"\ - \n },\n \"payment_intent\": \"pi_1GUslpIcI91cWsdeheAuRsyg\",\n \"reason\"\ - : null,\n \"receipt_number\": null,\n \"source_transfer_reversal\": null,\n\ - \ \"status\": \"succeeded\",\n \"transfer_reversal\": null\n}\n" + string: "{\n \"id\": \"re_3OibCXHz0MWR65Xj0KIi9Hq0\",\n \"object\": \"refund\",\n + \ \"amount\": 21000,\n \"balance_transaction\": \"txn_3OibCXHz0MWR65Xj0q14l4Pn\",\n + \ \"charge\": \"ch_3OibCXHz0MWR65Xj09SoiLkW\",\n \"created\": 1707651081,\n + \ \"currency\": \"gbp\",\n \"destination_details\": {\n \"card\": {\n + \ \"reference_status\": \"pending\",\n \"reference_type\": \"acquirer_reference_number\",\n + \ \"type\": \"refund\"\n },\n \"type\": \"card\"\n },\n \"metadata\": + {\n \"refund_request\": \"1\"\n },\n \"payment_intent\": \"pi_3OibCXHz0MWR65Xj0sgHN7Ni\",\n + \ \"reason\": null,\n \"receipt_number\": null,\n \"source_transfer_reversal\": + null,\n \"status\": \"succeeded\",\n \"transfer_reversal\": null\n}" headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET,HEAD,PUT,PATCH,POST,DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required, + X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store Connection: - keep-alive + Content-Length: + - '645' + Content-Security-Policy: + - report-uri https://q.stripe.com/csp-report?p=v1%2Frefunds; block-all-mixed-content; + default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; + img-src 'self'; script-src 'self' 'report-sample'; style-src 'self' Content-Type: - application/json Date: - - Tue, 07 Apr 2020 10:35:04 GMT + - Sun, 11 Feb 2024 11:31:21 GMT + Idempotency-Key: + - 37370c9d-b7ea-42d2-b8d0-b70e33e2e0d4 + Original-Request: + - req_fQu9FhEOR2ZON5 + Request-Id: + - req_fQu9FhEOR2ZON5 Server: - nginx Strict-Transport-Security: - - max-age=31556926; includeSubDomains; preload - access-control-allow-credentials: + - max-age=63072000; includeSubDomains; preload + Stripe-Should-Retry: + - 'false' + Stripe-Version: + - '2023-10-16' + Vary: + - Origin + X-Stripe-Non-Api-Overhead-Duration-Ms: + - '764.0' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + Stripe-Version: + - '2023-10-16' + User-Agent: + - Stripe/v1 PythonBindings/8.0.0 + X-Stripe-Client-User-Agent: + - '{"bindings_version": "8.0.0", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.11.7", "platform": "Linux-5.15.49-linuxkit-aarch64-with-glibc2.31", + "uname": "Linux 5cb7915c0fb8 5.15.49-linuxkit #1 SMP PREEMPT Tue Sep 13 07:51:32 + UTC 2022 aarch64 "}' + authorization: + - DUMMY + method: GET + uri: https://api.stripe.com/v1/charges/ch_3OibCXHz0MWR65Xj09SoiLkW + response: + body: + string: "{\n \"id\": \"ch_3OibCXHz0MWR65Xj09SoiLkW\",\n \"object\": \"charge\",\n + \ \"amount\": 21000,\n \"amount_captured\": 21000,\n \"amount_refunded\": + 21000,\n \"application\": null,\n \"application_fee\": null,\n \"application_fee_amount\": + null,\n \"balance_transaction\": \"txn_3OibCXHz0MWR65Xj0FGYSo9U\",\n \"billing_details\": + {\n \"address\": {\n \"city\": null,\n \"country\": null,\n \"line1\": + null,\n \"line2\": null,\n \"postal_code\": \"11111\",\n \"state\": + null\n },\n \"email\": \"test@example.com\",\n \"name\": \"Initial Admin + User\",\n \"phone\": null\n },\n \"calculated_statement_descriptor\": + \"STRIPE* EMF 2022 PURCH\",\n \"captured\": true,\n \"created\": 1707650896,\n + \ \"currency\": \"gbp\",\n \"customer\": null,\n \"description\": null,\n + \ \"destination\": null,\n \"dispute\": null,\n \"disputed\": false,\n \"failure_balance_transaction\": + null,\n \"failure_code\": null,\n \"failure_message\": null,\n \"fraud_details\": + {},\n \"invoice\": null,\n \"livemode\": false,\n \"metadata\": {\n \"payment_id\": + \"1\",\n \"user_id\": \"1\"\n },\n \"on_behalf_of\": null,\n \"order\": + null,\n \"outcome\": {\n \"network_status\": \"approved_by_network\",\n + \ \"reason\": null,\n \"risk_level\": \"normal\",\n \"risk_score\": + 47,\n \"seller_message\": \"Payment complete.\",\n \"type\": \"authorized\"\n + \ },\n \"paid\": true,\n \"payment_intent\": \"pi_3OibCXHz0MWR65Xj0sgHN7Ni\",\n + \ \"payment_method\": \"pm_1OibCmHz0MWR65XjLoAoi9pj\",\n \"payment_method_details\": + {\n \"card\": {\n \"amount_authorized\": 21000,\n \"brand\": + \"visa\",\n \"checks\": {\n \"address_line1_check\": null,\n \"address_postal_code_check\": + \"pass\",\n \"cvc_check\": \"pass\"\n },\n \"country\": \"US\",\n + \ \"exp_month\": 1,\n \"exp_year\": 2030,\n \"extended_authorization\": + {\n \"status\": \"disabled\"\n },\n \"fingerprint\": \"qyoEjYqy9x1BibQI\",\n + \ \"funding\": \"credit\",\n \"incremental_authorization\": {\n \"status\": + \"unavailable\"\n },\n \"installments\": null,\n \"last4\": + \"4242\",\n \"mandate\": null,\n \"multicapture\": {\n \"status\": + \"unavailable\"\n },\n \"network\": \"visa\",\n \"network_token\": + {\n \"used\": false\n },\n \"overcapture\": {\n \"maximum_amount_capturable\": + 21000,\n \"status\": \"unavailable\"\n },\n \"three_d_secure\": + null,\n \"wallet\": null\n },\n \"type\": \"card\"\n },\n \"radar_options\": + {},\n \"receipt_email\": null,\n \"receipt_number\": null,\n \"receipt_url\": + \"https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xT2RocXVIejBNV1I2NVhqKLbgoq4GMgawEAmvi6s6LBbS3hgKtCJtw4QOESWGae5cq9wU8F8Yv5Zf9dij-gcQ_bDQ_jpJAkZG_QbK\",\n + \ \"refunded\": true,\n \"review\": null,\n \"shipping\": null,\n \"source\": + null,\n \"source_transfer\": null,\n \"statement_descriptor\": null,\n \"statement_descriptor_suffix\": + \"EMF 2022 purchase\",\n \"status\": \"succeeded\",\n \"transfer_data\": + null,\n \"transfer_group\": null\n}" + headers: + Access-Control-Allow-Credentials: - 'true' - access-control-allow-methods: - - GET, POST, HEAD, OPTIONS, DELETE - access-control-allow-origin: + Access-Control-Allow-Methods: + - GET,HEAD,PUT,PATCH,POST,DELETE + Access-Control-Allow-Origin: - '*' - access-control-expose-headers: - - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required - access-control-max-age: + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required, + X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: - '300' - cache-control: + Cache-Control: - no-cache, no-store - idempotency-key: - - 14d0d6b0-d636-49bb-998a-a3a517a392aa - original-request: - - req_2wQMwa8QK6IquD - request-id: - - req_2wQMwa8QK6IquD - stripe-should-retry: - - 'false' - stripe-version: - - '2016-07-06' + Connection: + - keep-alive + Content-Length: + - '2810' + Content-Security-Policy: + - report-uri https://q.stripe.com/csp-report?p=v1%2Fcharges%2F%3Acharge; block-all-mixed-content; + default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; + img-src 'self'; script-src 'self' 'report-sample'; style-src 'self' + Content-Type: + - application/json + Date: + - Sun, 11 Feb 2024 11:32:06 GMT + Request-Id: + - req_JtAfXPsoab9wa9 + Server: + - nginx + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Stripe-Version: + - '2023-10-16' + Vary: + - Origin + X-Stripe-Non-Api-Overhead-Duration-Ms: + - '75.00000000000001' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode status: code: 200 message: OK diff --git a/tests/test_purchase.py b/tests/test_purchase.py index c974233b8..1d7726db3 100644 --- a/tests/test_purchase.py +++ b/tests/test_purchase.py @@ -28,6 +28,14 @@ def load_webhook_fixture(name): return stripe.Event.construct_from(json.load(f), None) +@pytest.fixture(scope="module") +def vcr_config(): + return { + # Replace the Authorization request header with "DUMMY" in cassettes + "filter_headers": [("authorization", "DUMMY")], + } + + # This test uses VCR to automatically store Stripe responses as test fixtures. # It also uses some webhook fixtures which we manually supply. # @@ -46,7 +54,7 @@ def test_create_stripe_purchase(user, app, monkeypatch): db.session.commit() # This matches the intent ID in stored fixtures - intent_id = "pi_1GUslpIcI91cWsdeheAuRsyg" + intent_id = "pi_3OiHFbHz0MWR65Xj0KidZr5i" with app.test_request_context("/tickets/pay"): login_user(user) diff --git a/tests/webhook_fixtures/charge.refunded.json b/tests/webhook_fixtures/charge.refunded.json index 360a11f53..8a0565f2f 100644 --- a/tests/webhook_fixtures/charge.refunded.json +++ b/tests/webhook_fixtures/charge.refunded.json @@ -1,42 +1,45 @@ { - "id": "ch_1GUslxIcI91cWsde5jePBfRQ", + "id": "ch_3OibCXHz0MWR65Xj09SoiLkW", "object": "charge", - "amount": 23000, - "amount_refunded": 21000, + "amount": 11500, + "amount_captured": 11500, + "amount_refunded": 11500, "application": null, "application_fee": null, "application_fee_amount": null, - "balance_transaction": "txn_1GUslxIcI91cWsdenOKhyR4i", + "balance_transaction": "txn_3Oe2zLHz0MWR65Xj1QWdzYX2", "billing_details": { "address": { "city": null, "country": null, "line1": null, "line2": null, - "postal_code": "42424", + "postal_code": "11111", "state": null }, "email": "test@example.com", "name": "Name", "phone": null }, - "calculated_statement_descriptor": "EMFCAMP EMF 2020 P", + "calculated_statement_descriptor": "STRIPE* EMF 2022 PURCH", "captured": true, - "created": 1586171145, + "created": 1706566070, "currency": "gbp", "customer": null, "description": null, "destination": null, "dispute": null, "disputed": false, + "failure_balance_transaction": null, "failure_code": null, "failure_message": null, - "fraud_details": {}, + "fraud_details": { + }, "invoice": null, "livemode": false, "metadata": { - "user_id": "1", - "payment_id": "2" + "payment_id": "3", + "user_id": "1" }, "on_behalf_of": null, "order": null, @@ -44,15 +47,16 @@ "network_status": "approved_by_network", "reason": null, "risk_level": "normal", - "risk_score": 55, + "risk_score": 29, "seller_message": "Payment complete.", "type": "authorized" }, "paid": true, - "payment_intent": "pi_1GUslpIcI91cWsdeheAuRsyg", - "payment_method": "pm_1GUslxIcI91cWsdeBYozsFYU", + "payment_intent": "pi_3OiHFbHz0MWR65Xj0KidZr5i", + "payment_method": "pm_1Oe2zaHz0MWR65Xj9ObFA4hd", "payment_method_details": { "card": { + "amount_authorized": 11500, "brand": "visa", "checks": { "address_line1_check": null, @@ -60,55 +64,48 @@ "cvc_check": "pass" }, "country": "US", - "exp_month": 4, - "exp_year": 2024, - "fingerprint": "VKnD4kq2Jm2zibNl", + "exp_month": 1, + "exp_year": 2029, + "extended_authorization": { + "status": "disabled" + }, + "fingerprint": "qyoEjYqy9x1BibQI", "funding": "credit", + "incremental_authorization": { + "status": "unavailable" + }, "installments": null, "last4": "4242", + "mandate": null, + "multicapture": { + "status": "unavailable" + }, "network": "visa", + "network_token": { + "used": false + }, + "overcapture": { + "maximum_amount_capturable": 11500, + "status": "unavailable" + }, "three_d_secure": null, "wallet": null }, "type": "card" }, + "radar_options": { + }, "receipt_email": null, "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/acct_14i5G7IcI91cWsde/ch_1GUslxIcI91cWsde5jePBfRQ/rcpt_H2yjkWzJc46LbnTbSqH7RdXWbbaVDSU", - "refunded": false, - "refunds": { - "object": "list", - "data": [ - { - "id": "re_1GVEloIcI91cWsdemdIEKjOK", - "object": "refund", - "amount": 21000, - "balance_transaction": "txn_1GVEloIcI91cWsdeWsmUp4Fr", - "charge": "ch_1GUslxIcI91cWsde5jePBfRQ", - "created": 1586255704, - "currency": "gbp", - "metadata": { - "refund_request": "1" - }, - "payment_intent": "pi_1GUslpIcI91cWsdeheAuRsyg", - "reason": null, - "receipt_number": null, - "source_transfer_reversal": null, - "status": "succeeded", - "transfer_reversal": null - } - ], - "has_more": false, - "total_count": 1, - "url": "/v1/charges/ch_1GUslxIcI91cWsde5jePBfRQ/refunds" - }, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xT2RocXVIejBNV1I2NVhqKLnE4K0GMgYuOdb8Tz86LBZHtvpFa6UvmDVEvQaGSKdQfESb2Z3CArkrrX4DqeiYsXdDAocurT4krTtc", + "refunded": true, "review": null, "shipping": null, "source": null, "source_transfer": null, "statement_descriptor": null, - "statement_descriptor_suffix": "EMF 2020 purchase", + "statement_descriptor_suffix": "EMF 2022 purchase", "status": "succeeded", "transfer_data": null, "transfer_group": null -} +} \ No newline at end of file diff --git a/tests/webhook_fixtures/payment_intent.succeeded.json b/tests/webhook_fixtures/payment_intent.succeeded.json index 5717b0bf9..804e9b048 100644 --- a/tests/webhook_fixtures/payment_intent.succeeded.json +++ b/tests/webhook_fixtures/payment_intent.succeeded.json @@ -1,150 +1,60 @@ { - "id": "pi_1GUslpIcI91cWsdeheAuRsyg", + "id": "pi_3OiHFbHz0MWR65Xj0KidZr5i", "object": "payment_intent", - "allowed_source_types": ["card"], - "amount": 23000, + "amount": 11500, "amount_capturable": 0, - "amount_received": 23000, + "amount_details": { + "tip": { + } + }, + "amount_received": 11500, "application": null, "application_fee_amount": null, + "automatic_payment_methods": { + "allow_redirects": "always", + "enabled": true + }, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic", - "charges": { - "object": "list", - "data": [ - { - "id": "ch_1GUslxIcI91cWsde5jePBfRQ", - "object": "charge", - "amount": 23000, - "amount_refunded": 0, - "application": null, - "application_fee": null, - "application_fee_amount": null, - "balance_transaction": "txn_1GUslxIcI91cWsdenOKhyR4i", - "billing_details": { - "address": { - "city": null, - "country": null, - "line1": null, - "line2": null, - "postal_code": "42424", - "state": null - }, - "email": "russ@garrett.co.uk", - "name": "Russ", - "phone": null - }, - "calculated_statement_descriptor": "RUSS.GARRE* EMF 2020 P", - "captured": true, - "created": 1586171145, - "currency": "gbp", - "customer": null, - "description": null, - "destination": null, - "dispute": null, - "disputed": false, - "failure_code": null, - "failure_message": null, - "fraud_details": {}, - "invoice": null, - "livemode": false, - "metadata": { - "user_id": "1", - "payment_id": "2" - }, - "on_behalf_of": null, - "order": null, - "outcome": { - "network_status": "approved_by_network", - "reason": null, - "risk_level": "normal", - "risk_score": 55, - "seller_message": "Payment complete.", - "type": "authorized" - }, - "paid": true, - "payment_intent": "pi_1GUslpIcI91cWsdeheAuRsyg", - "payment_method": "pm_1GUslxIcI91cWsdeBYozsFYU", - "payment_method_details": { - "card": { - "brand": "visa", - "checks": { - "address_line1_check": null, - "address_postal_code_check": "pass", - "cvc_check": "pass" - }, - "country": "US", - "exp_month": 4, - "exp_year": 2024, - "fingerprint": "VKnD4kq2Jm2zibNl", - "funding": "credit", - "installments": null, - "last4": "4242", - "network": "visa", - "three_d_secure": null, - "wallet": null - }, - "type": "card" - }, - "receipt_email": null, - "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/acct_14i5G7IcI91cWsde/ch_1GUslxIcI91cWsde5jePBfRQ/rcpt_H2yjkWzJc46LbnTbSqH7RdXWbbaVDSU", - "refunded": false, - "refunds": { - "object": "list", - "data": [], - "has_more": false, - "total_count": 0, - "url": "/v1/charges/ch_1GUslxIcI91cWsde5jePBfRQ/refunds" - }, - "review": null, - "shipping": null, - "source": null, - "source_transfer": null, - "statement_descriptor": null, - "statement_descriptor_suffix": "EMF 2020 purchase", - "status": "succeeded", - "transfer_data": null, - "transfer_group": null - } - ], - "has_more": false, - "total_count": 1, - "url": "/v1/charges?payment_intent=pi_1GUslpIcI91cWsdeheAuRsyg" - }, - "client_secret": "pi_1GUslpIcI91cWsdeheAuRsyg_secret_jm7QnCtrTMjZKeYmqmmmlXZlj", + "client_secret": "pi_3OiHFbHz0MWR65Xj0KidZr5i_secret_IHdxJI6A145n42CiRybjuCm5o", "confirmation_method": "automatic", - "created": 1586171137, + "created": 1706566055, "currency": "gbp", "customer": null, "description": null, "invoice": null, "last_payment_error": null, + "latest_charge": "ch_3OibCXHz0MWR65Xj09SoiLkW", "livemode": false, "metadata": { - "user_id": "1", - "payment_id": "2" + "payment_id": "3", + "user_id": "1" }, "next_action": null, - "next_source_action": null, "on_behalf_of": null, - "payment_method": "pm_1GUslxIcI91cWsdeBYozsFYU", + "payment_method": "pm_1Oe2zaHz0MWR65Xj9ObFA4hd", + "payment_method_configuration_details": null, "payment_method_options": { "card": { "installments": null, + "mandate_options": null, + "network": null, "request_three_d_secure": "automatic" } }, - "payment_method_types": ["card"], + "payment_method_types": [ + "card" + ], + "processing": null, "receipt_email": null, "review": null, "setup_future_usage": null, "shipping": null, "source": null, "statement_descriptor": null, - "statement_descriptor_suffix": "EMF 2020 purchase", + "statement_descriptor_suffix": "EMF 2022 purchase", "status": "succeeded", "transfer_data": null, "transfer_group": null -} +} \ No newline at end of file