def create_stripe_charge(transaction, card_source_id) -> stripe.Charge: if transaction.status != Transaction.PENDING: raise InternalServerError( f"unexpected status of transaction", log= f"transaction {transaction.id} has unexpected status {transaction.status}" ) stripe_amount = convert_to_stripe_amount(transaction.amount) try: return stripe.Charge.create( amount=stripe_amount, currency=CURRENCY, description=f'charge for transaction id {transaction.id}', source=card_source_id, ) except InvalidRequestError as e: raise_from_stripe_invalid_request_error(e) except CardError as e: error = e.json_body.get('error', {}) raise PaymentFailed(message=error.get("message"), log=f"stripe charge failed: {str(error)}") except StripeError as e: raise InternalServerError( log=f"stripe charge failed (possibly temporarily): {str(e)}")
def process_cart(member_id, cart): contents = [] with localcontext() as ctx: ctx.clear_flags() total_amount = Decimal(0) for item in cart: try: product_id = item['id'] product = db_session.query(Product).filter( Product.id == product_id, Product.deleted_at.is_(None)).one() except NoResultFound: raise NotFound( message=f"Could not find product with id {product_id}.") if product.price < 0: raise InternalServerError( log=f"Product {product_id} has a negative price.") count = item['count'] if count <= 0: raise BadRequest( message=f"Bad product count for product {product_id}.", what=NEGATIVE_ITEM_COUNT) if count % product.smallest_multiple != 0: raise BadRequest( f"Bad count for product {product_id}, must be in multiples " f"of {product.smallest_multiple}, was {count}.", what=INVALID_ITEM_COUNT) if product.filter: PRODUCT_FILTERS[product.filter](cart_item=item, member_id=member_id) amount = product.price * count total_amount += amount content = TransactionContent(product_id=product_id, count=count, amount=amount) contents.append(content) if ctx.flags[Rounded]: # This can possibly happen with huge values, I suppose they will be caught below anyway but it's good to # catch in any case. raise InternalServerError( log="Rounding error when calculating cart sum.") return total_amount, contents
def create_action_required_response(transaction, payment_intent): """ The payment_intent requires customer action to be confirmed. Create response to client""" try: db_session.add( StripePending(transaction_id=transaction.id, stripe_token=payment_intent.id)) if payment_intent.next_action.type == PaymentIntentNextActionType.USE_STRIPE_SDK: return dict(type=PaymentIntentNextActionType.USE_STRIPE_SDK, client_secret=payment_intent.client_secret) elif payment_intent.next_action.type == PaymentIntentNextActionType.REDIRECT_TO_URL: raise InternalServerError( log= f"unexpected next_action type, {payment_intent.next_action.type}" ) else: raise PaymentFailed( log= f"unknown next_action type, {payment_intent.next_action.type}") except Exception: # Fail transaction on all known and unknown errors to be safe, we won't charge a failed transaction. commit_fail_transaction(transaction) logger.info( f"failing transaction {transaction.id}, due to error when processing 3ds card" ) raise
def create(self, data=None, commit=True): if data is None: data = request.json or {} handle_password(data) status, = db_session.execute( "SELECT GET_LOCK('member_number', 20)").fetchone() if not status: raise InternalServerError( "Failed to create member, try again later.", log="failed to aquire member_number lock") try: if data.get('member_number') is None: sql = "SELECT COALESCE(MAX(member_number), 999) FROM membership_members" max_member_number, = db_session.execute(sql).fetchone() data['member_number'] = max_member_number + 1 obj = self.to_obj(self._create_internal(data, commit=commit)) return obj except Exception: # Rollback session if anything went wrong or we can't release the lock. db_session.rollback() raise finally: db_session.execute("DO RELEASE_LOCK('member_number')")
def charge_transaction(transaction, charge): if charge.status != ChargeStatus.SUCCEEDED: raise InternalServerError( log= f"unexpected charge status '{charge.status}' for transaction {transaction.id} " f"this should be handled") payment_success(transaction)
def complete_payment_intent_transaction(transaction, payment_intent): if payment_intent.status != PaymentIntentStatus.SUCCEEDED: raise InternalServerError( log= f"unexpected payment_intent status '{payment_intent.status}' for transaction {transaction.id} " f"this should be handled") if transaction.status == Transaction.PENDING: payment_success(transaction)
def convert_to_stripe_amount(amount: Decimal) -> int: """ Convert decimal amount to stripe amount and return it. Fails if amount is not even cents (ören). """ stripe_amount = amount * STRIPE_CURRENTY_BASE if stripe_amount % 1 != 0: raise InternalServerError( message= f"The amount could not be converted to an even number of ören ({amount}).", log= f"Stripe amount not even number of ören, maybe some product has uneven ören." ) return int(stripe_amount)
def get_source_transaction(source_id): try: return db_session\ .query(Transaction)\ .filter(Transaction.stripe_pending.any(StripePending.stripe_token == source_id))\ .with_for_update()\ .one() except NoResultFound as e: return None except MultipleResultsFound as e: raise InternalServerError( log= f"stripe token {source_id} has multiple transactions, this is a bug" ) from e
def create_client_response(transaction, payment_intent): if payment_intent.status == PaymentIntentStatus.REQUIRES_ACTION: """ Requires further action on client side. """ if not payment_intent.next_action: raise InternalServerError( f"intent next_action is required but missing ({payment_intent.next_action})" ) return create_action_required_response(transaction, payment_intent) elif payment_intent.status == PaymentIntentStatus.REQUIRES_CONFIRMATION: confirmed_intent = stripe.PaymentIntent.confirm(payment_intent.id) assert confirmed_intent.status != PaymentIntentStatus.REQUIRES_CONFIRMATION return create_client_response(transaction, confirmed_intent) elif payment_intent.status == PaymentIntentStatus.SUCCEEDED: payment_success(transaction) logger.info( f"succeeded: payment for transaction {transaction.id}, payment_intent id {payment_intent.id}" ) return None elif payment_intent.status == PaymentIntentStatus.REQUIRES_PAYMENT_METHOD: commit_fail_transaction(transaction) logger.info( f"failed: payment for transaction {transaction.id}, payment_intent id {payment_intent.id}" ) raise BadRequest( log= f"payment_intent requires payment method, either no method provided or the payment failed" ) else: raise InternalServerError( log= f"unexpected stripe payment_intent status {payment_intent.status}, this is a bug" )
def password_reset(reset_token, unhashed_password): try: password_reset_token = db_session.query(PasswordResetToken).filter_by( token=reset_token).one() except NoResultFound: return dict( error_message= "Could not find password reset token, try to request a new reset link." ) except MultipleResultsFound: raise InternalServerError( log=f"Multiple tokens {reset_token} found, this is a bug.") if datetime.utcnow() - password_reset_token.created_at > timedelta( minutes=10): return dict(error_message="Reset link expired, try to request a new.") try: hashed_password = check_and_hash_password(unhashed_password) except ValueError as e: return dict(error_message=str(e)) try: member = db_session.query(Member).get(password_reset_token.member_id) except NoResultFound: raise InternalServerError( log= f"No member with id {password_reset_token.member_id} found, this is a bug." ) member.password = hashed_password db_session.add(member) return {}
def capture_stripe_payment_intent(transaction, payment_intent): """ This is payment_intent is authorized and can be captured synchronously. """ if transaction.status != Transaction.PENDING: raise InternalServerError( f"unexpected status of transaction", log= f"transaction {transaction.id} has unexpected status {transaction.status}" ) try: captured_intent = stripe.PaymentIntent.capture(payment_intent.id) complete_payment_intent_transaction(transaction, captured_intent) except InvalidRequestError as e: raise PaymentFailed( log=f"stripe capture payment_intent failed: {str(e)}", level=EXCEPTION) except StripeError as e: raise InternalServerError( log= f"stripe capture payment_intent failed (possibly temporarily): {str(e)}" )
def create(self, data=None, commit=True): if data is None: data = request.json or {} status, = db_session.execute("SELECT GET_LOCK('display_order', 20)").fetchone() if not status: raise InternalServerError("Failed to create, try again later.", log="failed to aquire display_order lock") try: if data.get('display_order') is None: data['display_order'] = (db_session.query(func.max(self.model.display_order)).scalar() or 0) + 1 obj = self.to_obj(self._create_internal(data, commit=commit)) return obj except Exception: # Rollback session if anything went wrong or we can't release the lock. db_session.rollback() raise finally: db_session.execute("DO RELEASE_LOCK('display_order')")
def stripe_source_event(subtype, event): source = event.data.object transaction = get_pending_source_transaction(source.id) if subtype == Subtype.CHARGEABLE: if source.type == SourceType.THREE_D_SECURE: # Charge card and resolve transaction, don't fail transaction on errors as it may be resolved when we get # callback again. try: charge = create_stripe_charge(transaction, source.id) except PaymentFailed as e: logger.info( f"failing transaction {transaction.id}, permanent error when creating charge: {str(e)}" ) commit_fail_transaction(transaction) else: charge_transaction(transaction, charge) elif source.type == SourceType.CARD: # Non 3d secure cards should be charged synchronously in payment, not here. raise IgnoreEvent( f"transaction {transaction.id} source event of type card is handled synchronously" ) else: raise InternalServerError( log=f"unexpected source type '{source.type}'" f" when handling source event: {source}") elif subtype in (Subtype.FAILED, Subtype.CANCELED): logger.info( f"failing transaction {transaction.id} due to source event subtype {subtype}" ) commit_fail_transaction(transaction) else: raise IgnoreEvent( f"source event subtype {subtype} for transaction {transaction.id}")