def create_customer( subhub_account: SubHubAccount, user_id: str, email: str, source_token: str, origin_system: str, display_name: str, ) -> Customer: _validate_origin_system(origin_system) # First search Stripe to ensure we don't have an unlinked Stripe record # already in Stripe customer = None customers = vendor.get_customer_list(email=email) for possible_customer in customers.data: if possible_customer.email == email: # If the userid doesn't match, the system is damaged. if possible_customer.metadata.get("userid") != user_id: customer_message = "customer email exists but userid mismatch" raise ServerError(customer_message) customer = possible_customer # If we have a mis-match on the source_token, overwrite with the # new one. if customer.default_source != source_token: vendor.modify_customer( customer_id=customer.id, source_token=source_token, idempotency_key=utils.get_indempotency_key(), ) break # No existing Stripe customer, create one. if not customer: customer = vendor.create_stripe_customer( source_token=source_token, email=email, userid=user_id, name=display_name, idempotency_key=utils.get_indempotency_key(), ) # Link the Stripe customer to the origin system id db_account = subhub_account.new_user(uid=user_id, origin_system=origin_system, cust_id=customer.id) try: new_user = subhub_account.save_user(db_account) if not new_user: # Clean-up the Stripe customer record since we can't link it vendor.delete_stripe_customer(customer_id=customer.id) except IntermittentError("error saving db record") as e: # type: ignore logger.error("unable to save user or link it", error=e) raise e return customer
def existing_payment_source(existing_customer: Customer, source_token: str) -> Customer: """ If Customer does not have an existing Payment Source and has not been Deleted: - Set the Customer's payment source to the new token - return the updated Customer Else - return the provided customer :param existing_customer: :param source_token: :return: """ if not existing_customer.get("sources"): if not existing_customer.get("deleted"): existing_customer = vendor.modify_customer( customer_id=existing_customer["id"], source_token=source_token, idempotency_key=utils.get_indempotency_key(), ) logger.info("add source", existing_customer=existing_customer) else: logger.info( "stripe customer is marked as deleted. cannot add source.") logger.debug("existing payment source", existing_customer=existing_customer) return existing_customer
def subscribe_to_plan(uid: str, data: Dict[str, Any]) -> FlaskResponse: """ Subscribe to a plan given a user id, payment token, email, orig_system :param uid: :param data: :return: current subscriptions for user. """ customer = existing_or_new_customer( g.subhub_account, user_id=uid, email=data["email"], source_token=data["pmt_token"], origin_system=data["origin_system"], display_name=data["display_name"], ) existing_plan = has_existing_plan(customer, plan_id=data["plan_id"]) if existing_plan: return dict(message="User already subscribed."), 409 if not customer.get("deleted"): vendor.build_stripe_subscription(customer.id, data["plan_id"], utils.get_indempotency_key()) updated_customer = fetch_customer(g.subhub_account, user_id=uid) newest_subscription = find_newest_subscription( updated_customer["subscriptions"]) return create_return_data(newest_subscription), 201 return dict(message=None), 400
def test_cancel_immediately_error(self): self.mock_delete_subscription.side_effect = APIError("message") with self.assertRaises(APIError): vendor.cancel_stripe_subscription_immediately( subscription_id="sub_123", idempotency_key=utils.get_indempotency_key() )
def test_reactivate_error(self): self.mock_modify_subscription.side_effect = APIError("message") with self.assertRaises(APIError): vendor.reactivate_stripe_subscription( subscription_id="sub_123", idempotency_key=utils.get_indempotency_key() )
def test_cancel_at_end_error(self): self.mock_modify_subscription.side_effect = APIError("message") with self.assertRaises(APIError): vendor.cancel_stripe_subscription_period_end( subscription_id="sub_123", idempotency_key=utils.get_indempotency_key() )
def cancel_subscription(uid: str, sub_id: str) -> FlaskResponse: """ Cancel an existing subscription for a user. :param uid: :param sub_id: :return: Success or failure message for the cancellation. """ customer = fetch_customer(g.subhub_account, uid) if not customer: return dict(message="Customer does not exist."), 404 for item in customer["subscriptions"]["data"]: if item["id"] == sub_id and item["status"] in [ "active", "trialing", "incomplete", ]: vendor.cancel_stripe_subscription_period_end( sub_id, utils.get_indempotency_key()) updated_customer = fetch_customer(g.subhub_account, uid) logger.info("updated customer", updated_customer=updated_customer) subs = retrieve_stripe_subscriptions(updated_customer) logger.info("subs", subs=subs, type=type(subs)) for sub in subs: if sub["cancel_at_period_end"] and sub["id"] == sub_id: return { "message": "Subscription cancellation successful" }, 201 return dict(message="Subscription not available."), 400
def create_customer( subhub_account: SubHubAccount, user_id: str, email: str, source_token: str, origin_system: str, display_name: str, ) -> Customer: _validate_origin_system(origin_system) # First search Stripe to ensure we don't have an unlinked Stripe record # already in Stripe customer = search_customers(email=email, user_id=user_id) # If we have a mis-match on the source_token, overwrite with the # new one. if customer is not None and customer.default_source != source_token: customer = vendor.modify_customer( customer_id=customer.id, source_token=source_token, idempotency_key=utils.get_indempotency_key(), ) # No existing Stripe customer, create one. if not customer: customer = vendor.create_stripe_customer( source_token=source_token, email=email, userid=user_id, name=display_name, idempotency_key=utils.get_indempotency_key(), ) # Link the Stripe customer to the origin system id db_account = subhub_account.new_user(uid=user_id, origin_system=origin_system, cust_id=customer.id) new_user = subhub_account.save_user(db_account) if not new_user: # Clean-up the Stripe customer record since we can't link it vendor.delete_stripe_customer(customer_id=customer.id) logger.error("unable to save user or link it") raise IntermittentError("error saving db record") logger.debug("create customer", customer=customer) return customer
def test_build_error(self): self.mock_create_subscription.side_effect = APIError("message") with self.assertRaises(APIError): vendor.build_stripe_subscription( customer_id="cust_123", plan_id="plan_123", idempotency_key=utils.get_indempotency_key(), )
def test_modify_error(self): self.modify_customer_mock.side_effect = APIError("message") with self.assertRaises(APIError): vendor.modify_customer( # nosec customer_id="cust_123", source_token="token", idempotency_key=utils.get_indempotency_key(), )
def test_update_error(self): self.mock_modify_subscription.side_effect = APIError("message") with self.assertRaises(APIError): vendor.update_stripe_subscription( subscription=self.subscription, plan_id="plan_123", idempotency_key=utils.get_indempotency_key(), )
def test_modify_success(self): self.modify_customer_mock.side_effect = [APIError("message"), self.customer] customer = vendor.modify_customer( # nosec customer_id="cust_123", source_token="token", idempotency_key=utils.get_indempotency_key(), ) assert customer == self.customer # nosec
def test_cancel_immediately_success(self): self.mock_delete_subscription.side_effect = [ APIError("message"), self.subscription, ] subscription = vendor.cancel_stripe_subscription_immediately( subscription_id="sub_123", idempotency_key=utils.get_indempotency_key() ) assert subscription == self.subscription # nosec
def test_reactivate_success(self): self.mock_modify_subscription.side_effect = [ APIError("message"), self.subscription, ] subscription = vendor.reactivate_stripe_subscription( subscription_id="sub_123", idempotency_key=utils.get_indempotency_key() ) assert subscription == self.subscription # nosec
def test_create_stripe_customer_error(): disable_base() with pytest.raises(APIError): vendor.create_stripe_customer( "token", "*****@*****.**", "abc123", "Anonymous", utils.get_indempotency_key(), ) enable_base()
def test_create_error(self): self.create_customer_mock.side_effect = APIError("message") with self.assertRaises(APIError): vendor.create_stripe_customer( # nosec source_token="token", email="*****@*****.**", userid="user_123", name="Test User", idempotency_key=utils.get_indempotency_key(), )
def test_cancel_at_end_success(self): self.mock_modify_subscription.side_effect = [ APIError("message"), self.subscription, ] subscription = vendor.cancel_stripe_subscription_period_end( subscription_id="sub_123", idempotency_key=utils.get_indempotency_key() ) assert subscription == self.subscription # nosec
def test_create_success(self): self.create_customer_mock.side_effect = [APIError("message"), self.customer] customer = vendor.create_stripe_customer( # nosec source_token="token", email="*****@*****.**", userid="user_123", name="Test User", idempotency_key=utils.get_indempotency_key(), ) assert customer == self.customer # nosec
def subscribe_customer(customer: Customer, plan_id: str) -> Subscription: """ Subscribe Customer to Plan :param customer: :param plan_id: :return: Subscription Object """ sub = vendor.build_stripe_subscription( customer.id, plan_id=plan_id, idempotency_key=utils.get_indempotency_key()) return sub
def existing_payment_source(existing_customer: Customer, source_token: str) -> Customer: if not existing_customer.get("sources"): if not existing_customer.get("deleted"): existing_customer = vendor.modify_customer( customer_id=existing_customer["id"], source_token=source_token, idempotency_key=utils.get_indempotency_key(), ) logger.info("add source", existing_customer=existing_customer) else: logger.info("existing source deleted") return existing_customer
def test_build_success(self): self.mock_create_subscription.side_effect = [ APIError("message"), self.subscription, ] subscription = vendor.build_stripe_subscription( customer_id="cust_123", plan_id="plan_123", idempotency_key=utils.get_indempotency_key(), ) assert subscription == self.subscription # nosec
def reactivate_subscription(uid: str, sub_id: str) -> FlaskResponse: """ Given a user's subscription that is flagged for cancellation, but is still active remove the cancellation flag to ensure the subscription remains active :param uid: User ID :param sub_id: Subscription ID :return: Success or failure message for the activation """ customer = fetch_customer(g.subhub_account, uid) if not customer: response_message = dict(message="Customer does not exist.") logger.debug("reactivate subscription", response_message=response_message) return response_message, 404 active_subscriptions = customer["subscriptions"]["data"] response_message = dict(message="Current subscription not found.") for subscription in active_subscriptions: if subscription["id"] == sub_id: response_message = dict(message="Subscription is already active.") if subscription["cancel_at_period_end"]: reactivate_stripe_subscription(sub_id, utils.get_indempotency_key()) response_message = dict( message="Subscription reactivation was successful.") logger.debug( "reactivate subscription", response_message=response_message, response_code=200, ) return response_message, 200 logger.debug( "reactivate subscription", response_message=response_message, response_code=200, ) return response_message, 200 logger.debug("reactivate subscription", response_message=response_message, response_code=404) return response_message, 404
def update_payment_method(uid, data) -> FlaskResponse: """ Given a user id and a payment token, update user's payment method :param uid: :param data: :return: Success or failure message. """ customer = fetch_customer(g.subhub_account, uid) logger.debug("customer", customer=customer) if not customer: response_message = dict(message="Customer does not exist.") logger.debug( "update payment method", response_message=response_message, response_code=404, ) return response_message, 404 metadata = customer.get("metadata") logger.debug("metadata", metadata=metadata, customer=type(customer)) if metadata: if metadata.get("userid", None) == uid: modify_customer( customer_id=customer.id, source_token=data["pmt_token"], idempotency_key=utils.get_indempotency_key(), ) response_message = dict( message="Payment method updated successfully.") logger.debug( "update payment method", response_message=response_message, response_code=201, ) return response_message, 201 response_message = dict(message="Customer mismatch.") logger.debug("update payment method", response_message=response_message, response_code=400) return response_message, 400
def update_subscription(uid: str, sub_id: str, data: Dict[str, Any]) -> FlaskResponse: """ Update a Customer's Subscription with a new Plan Locate a Stripe Customer from the provided uid and locate the Customer's subscription from the provided sub_id - If the Customer is not found, or the Customer object does not contain a Subscription with the sub_id :return 404 Not Found Determine if the new plan_id can replace the current Subscription Plan: - If the new plan_id and current plan_id are the same : return 400 Bad Request - If the new plan and the old plan have different intervals: : return 400 Bad Request - If the products do not have the same ProductSet metadata :return 400 Bad Request Make call to Stripe to update the Subscription :return 200 OK - Updated Subscription in response body :param uid: :param sub_id: :param data: :return: """ customer = find_customer(g.subhub_account, uid) subscription = find_customer_subscription(customer, sub_id) current_plan = subscription["plan"] new_plan_id = data["plan_id"] new_product = validate_plan_change(current_plan, new_plan_id) updated_subscription = update_stripe_subscription( subscription, new_plan_id, utils.get_indempotency_key()) formatted_subscription = format_subscription( convert_to_dict(updated_subscription), new_product) return formatted_subscription, 200
def delete_customer(uid: str) -> FlaskResponse: """ Delete an existing customer, cancel active subscriptions and delete from payment provider :param uid: :return: Success of failure message for the deletion """ logger.info("delete customer", uid=uid) subscription_user = g.subhub_account.get_user(uid) logger.info("delete customer", subscription_user=subscription_user) if subscription_user is not None: origin = subscription_user.origin_system logger.info("delete origin", origin=origin) if not subscription_user: return dict(message="Customer does not exist."), 404 subscribed_customer = vendor.retrieve_stripe_customer( subscription_user.cust_id) subscribed_customer = subscribed_customer.to_dict() subscription_info: List = [] logger.info( "subscribed customer", subscribed_customer=subscribed_customer, data_type=type(subscribed_customer), ) products = {} # type: Dict for subs in subscribed_customer["subscriptions"]["data"]: try: product = products[subs.plan.product] except KeyError: product = Product.retrieve(subs.plan.product) products[subs.plan.product] = product plan_id = subs.plan.product sub = dict( plan_amount=subs.plan.amount, nickname=format_plan_nickname(subs.plan.nickname, subs.plan.interval), productId=plan_id, current_period_end=subs.current_period_end, current_period_start=subs.current_period_start, subscription_id=subs.id, ) subscription_info.append(sub) vendor.cancel_stripe_subscription_immediately( subs.id, utils.get_indempotency_key()) data = dict( uid=subscribed_customer["metadata"]["userid"], active=False, subscriptionId=subs.id, productId=plan_id, eventId=utils.get_indempotency_key(), eventCreatedAt=int(time.time()), messageCreatedAt=int(time.time()), ) sns_message = Message(json.dumps(data)).route() logger.info("delete message", sns_message=sns_message) else: deleted_payment_customer = vendor.delete_stripe_customer( subscription_user.cust_id) if deleted_payment_customer: deleted_customer = delete_user( user_id=subscribed_customer["metadata"]["userid"], cust_id=subscribed_customer["id"], origin_system=origin, subscription_info=subscription_info, ) user = g.subhub_account.get_user(uid) if deleted_customer and user is None: return dict(message="Customer deleted successfully"), 200 return dict(message="Customer not available"), 400
def test_cancel_stripe_subscription_period_end_error(): disable_base() with pytest.raises(APIError): vendor.cancel_stripe_subscription_period_end( "no_sub", utils.get_indempotency_key()) enable_base()
def test_build_stripe_subscription_error(): disable_base() with pytest.raises(APIError): vendor.build_stripe_subscription("no_one", "no_plan", utils.get_indempotency_key()) enable_base()
def test_modify_customer_error(): disable_base() with pytest.raises(APIError): vendor.modify_customer("no_customer", "tok_nothing", utils.get_indempotency_key()) enable_base()