Skip to content

Commit

Permalink
Update stripe to latest package/api version
Browse files Browse the repository at this point in the history
  • Loading branch information
wlcx committed Jan 30, 2024
1 parent 1f67b64 commit 6f0afc4
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 48 deletions.
4 changes: 0 additions & 4 deletions .mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 11 additions & 5 deletions apps/admin/payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
15 changes: 10 additions & 5 deletions apps/payments/refund.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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
Expand Down
66 changes: 39 additions & 27 deletions apps/payments/stripe.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
complicate this code.
"""
import logging
from typing import Optional

from flask import (
render_template,
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -170,16 +174,17 @@ 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"),
)
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)

Expand Down Expand Up @@ -212,29 +217,34 @@ 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.
"""
if intent is None:
intent = stripe.PaymentIntent.retrieve(payment.intent_id)

if len(intent.charges) == 0:
intent = stripe_client.payment_intents.retrieve(
payment.intent_id, params=dict(expand=["latest_charge"])
)
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"]:
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']}"
f"Charge ID for intent {intent.id} has changed from {payment.charge_id} to {charge.id}"
)

payment.charge_id = charge["id"]
payment.charge_id = charge.id

if charge.refunded:
return stripe_payment_refunded(payment)
Expand Down Expand Up @@ -367,8 +377,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:
Expand Down Expand Up @@ -423,10 +434,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

Expand Down
8 changes: 7 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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"],
Expand Down
11 changes: 6 additions & 5 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pybarcode = {git = "https://github.com/emfcamp/python-barcode"}
pillow = "~=10.0"
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"}
Expand Down

0 comments on commit 6f0afc4

Please sign in to comment.