1
0
mirror of https://github.com/quay/quay.git synced 2025-04-18 10:44:06 +03:00
quay/endpoints/api/billing.py
Marcus Kok 6720be4b8c
billing: stop modifying subscription list that is being iterated over (PROJQUAY-8712) (#3725)
Fixes bug where removing a MW02702 sub after all it's quantities have been bound causes the next item in the subscription list to be skipped over, resulting in a malformed api response for the marketplace endpoint.
2025-03-20 13:23:24 -04:00

1180 lines
39 KiB
Python

"""
Billing information, subscriptions, and plan information.
"""
import datetime
import json
import time
import uuid
import stripe
from flask import request
import features
from app import app, billing, marketplace_subscriptions, marketplace_users
from auth import scopes
from auth.auth_context import get_authenticated_user
from auth.permissions import AdministerOrganizationPermission
from data import model
from data.billing import PLANS, get_plan, get_plan_using_rh_sku
from data.model import InvalidOrganizationException, organization_skus
from endpoints.api import (
ApiResource,
abort,
internal_only,
log_action,
nickname,
path_param,
related_user_resource,
require_scope,
require_user_admin,
resource,
show_if,
validate_json_request,
)
from endpoints.api.subscribe import (
change_subscription,
check_repository_usage,
connection_response,
get_price,
subscription_view,
)
from endpoints.exception import InvalidRequest, NotFound, Unauthorized
from util.request import get_request_ip
MILLISECONDS_IN_SECONDS = 1000
def check_internal_api_for_subscription(namespace_user):
"""
Returns subscription from RH marketplace.
None returned if no subscription is found.
"""
plans = []
if namespace_user.organization:
query = organization_skus.get_org_subscriptions(namespace_user.id)
org_subscriptions = list(query.dicts()) if query is not None else []
for subscription in org_subscriptions:
quantity = 1 if subscription.get("quantity") is None else subscription["quantity"]
subscription_id = subscription["subscription_id"]
subscription_details = marketplace_subscriptions.get_subscription_details(
subscription_id
)
sku = subscription_details["sku"]
expiration = subscription_details["expiration_date"]
terminated = subscription_details["terminated_date"]
now_ms = time.time() * 1000
if expiration < now_ms or (terminated is not None and terminated < now_ms):
organization_skus.remove_subscription_from_org(namespace_user.id, subscription_id)
continue
for x in range(quantity):
plans.append(get_plan_using_rh_sku(sku))
pass
else:
user_account_numbers = marketplace_users.get_account_number(namespace_user)
if user_account_numbers:
plans = []
for user_account_number in user_account_numbers:
plans += marketplace_subscriptions.get_list_of_subscriptions(
user_account_number, filter_out_org_bindings=True, convert_to_stripe_plans=True
)
return plans
def get_namespace_plan(namespace):
"""
Returns the plan of the given namespace.
"""
namespace_user = model.user.get_namespace_user(namespace)
if namespace_user is None:
return None
if not namespace_user.stripe_id:
return None
# Ask Stripe for the subscribed plan.
# TODO: Can we cache this or make it faster somehow?
try:
cus = billing.Customer.retrieve(namespace_user.stripe_id)
except stripe.error.APIConnectionError:
abort(503, message="Cannot contact Stripe")
if not cus.subscription:
return None
return get_plan(cus.subscription.plan.id)
def lookup_allowed_private_repos(namespace):
"""
Returns false if the given namespace has used its allotment of private repositories.
"""
repos_allowed = 0
current_plan = get_namespace_plan(namespace)
if features.RH_MARKETPLACE:
namespace_user = model.user.get_namespace_user(namespace)
subscriptions = check_internal_api_for_subscription(namespace_user)
for subscription in subscriptions:
if subscription is not None:
repos_allowed += subscription["privateRepos"]
# Find the number of private repositories used by the namespace and compare it to the
# plan subscribed.
if current_plan is not None:
repos_allowed += current_plan["privateRepos"]
private_repos = model.user.get_private_repo_count(namespace)
return private_repos < repos_allowed
def carderror_response(e):
return {"carderror": str(e)}, 402
def get_card(user):
card_info = {"is_valid": False}
if user.stripe_id:
try:
cus = billing.Customer.retrieve(user.stripe_id)
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
if cus:
if cus.subscription and cus.subscription.default_payment_method:
payment_method = billing.PaymentMethod.retrieve(
cus.subscription.default_payment_method
)
if payment_method.card:
default_card = payment_method.card
card_info = {
"owner": payment_method.billing_details.name,
"type": payment_method.type,
"last4": default_card.last4,
"exp_month": default_card.exp_month,
"exp_year": default_card.exp_year,
}
# Stripe pre-paymentmethod api
# Ref: https://stripe.com/blog/payment-api-design
elif cus.default_card:
default_card = None
for card in cus.cards.data:
if card.id == cus.default_card:
default_card = card
break
if default_card:
card_info = {
"owner": default_card.name,
"type": default_card.type,
"last4": default_card.last4,
"exp_month": default_card.exp_month,
"exp_year": default_card.exp_year,
}
return {"card": card_info}
def get_invoices(customer_id):
def invoice_view(i):
return {
"id": i.id,
"date": i.date,
"period_start": i.period_start,
"period_end": i.period_end,
"paid": i.paid,
"amount_due": i.amount_due,
"next_payment_attempt": i.next_payment_attempt,
"attempted": i.attempted,
"closed": i.closed,
"total": i.total,
"plan": i.lines.data[0].plan.id if i.lines.data[0].plan else None,
}
try:
invoices = billing.Invoice.list(customer=customer_id, count=12)
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
return {"invoices": [invoice_view(i) for i in invoices.data]}
def get_invoice_fields(user):
try:
cus = billing.Customer.retrieve(user.stripe_id)
except stripe.error.APIConnectionError:
abort(503, message="Cannot contact Stripe")
if not "metadata" in cus:
cus.metadata = {}
return json.loads(cus.metadata.get("invoice_fields") or "[]"), cus
def create_billing_invoice_field(user, title, value):
new_field = {"uuid": str(uuid.uuid4()).split("-")[0], "title": title, "value": value}
invoice_fields, cus = get_invoice_fields(user)
invoice_fields.append(new_field)
if not "metadata" in cus:
cus.metadata = {}
cus.metadata["invoice_fields"] = json.dumps(invoice_fields)
cus.save()
return new_field
def delete_billing_invoice_field(user, field_uuid):
invoice_fields, cus = get_invoice_fields(user)
invoice_fields = [field for field in invoice_fields if not field["uuid"] == field_uuid]
if not "metadata" in cus:
cus.metadata = {}
cus.metadata["invoice_fields"] = json.dumps(invoice_fields)
cus.save()
return True
@resource("/v1/plans/")
@show_if(features.BILLING)
class ListPlans(ApiResource):
"""
Resource for listing the available plans.
"""
@nickname("listPlans")
def get(self):
"""
List the avaialble plans.
"""
return {
"plans": PLANS,
}
@resource("/v1/user/card")
@internal_only
@show_if(features.BILLING)
class UserCard(ApiResource):
"""
Resource for managing a user's credit card.
"""
schemas = {
"UserCard": {
"id": "UserCard",
"type": "object",
"description": "Description of a user card",
"required": ["success_url", "cancel_url"],
"properties": {
"success_url": {
"type": "string",
"description": "Redirect url after successful checkout",
},
"cancel_url": {
"type": "string",
"description": "Redirect url after cancelled checkout",
},
},
},
}
@require_user_admin()
@nickname("getUserCard")
def get(self):
"""
Get the user's credit card.
"""
user = get_authenticated_user()
return get_card(user)
@require_user_admin()
@nickname("setUserCard")
@validate_json_request("UserCard")
def post(self):
"""
Update the user's credit card.
"""
user = get_authenticated_user()
assert user.stripe_id
request_data = request.get_json()
success_url = request_data["success_url"]
cancel_url = request_data["cancel_url"]
try:
cus = billing.Customer.retrieve(user.stripe_id)
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
if not cus:
raise InvalidRequest("Invalid Stripe customer")
if not cus.subscription:
raise InvalidRequest("Invalid Stripe subscription")
try:
checkout_session = stripe.checkout.Session.create(
payment_method_types=["card"],
mode="setup",
customer=user.stripe_id,
setup_intent_data={
"metadata": {
"kind": "account_change_cc",
"namespace": user.username,
"performer": user.username,
"ip": get_request_ip(),
"subscription_id": cus.subscription.id,
},
},
success_url=success_url,
cancel_url=cancel_url,
)
return checkout_session
except stripe.error.APIConnectionError as se:
abort(503, message="Cannot contact Stripe: %s" % se)
except Exception as e:
abort(500, message=str(e))
@resource("/v1/organization/<orgname>/card")
@path_param("orgname", "The name of the organization")
@internal_only
@related_user_resource(UserCard)
@show_if(features.BILLING)
class OrganizationCard(ApiResource):
"""
Resource for managing an organization's credit card.
"""
schemas = {
"OrgCard": {
"id": "OrgCard",
"type": "object",
"description": "Description of an Organization card",
"required": ["success_url", "cancel_url"],
"properties": {
"success_url": {
"type": "string",
"description": "Redirect url after successful checkout",
},
"cancel_url": {
"type": "string",
"description": "Redirect url after cancelled checkout",
},
},
},
}
@require_scope(scopes.ORG_ADMIN)
@nickname("getOrgCard")
def get(self, orgname):
"""
Get the organization's credit card.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.organization.get_organization(orgname)
return get_card(organization)
raise Unauthorized()
@nickname("setOrgCard")
@validate_json_request("OrgCard")
def post(self, orgname):
"""
Update the orgnaization's credit card.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.organization.get_organization(orgname)
assert organization.stripe_id
request_data = request.get_json()
success_url = request_data["success_url"]
cancel_url = request_data["cancel_url"]
try:
cus = billing.Customer.retrieve(organization.stripe_id)
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
if not cus:
raise InvalidRequest("Invalid Stripe customer")
if not cus.subscription:
raise InvalidRequest("Invalid Stripe subscription")
try:
checkout_session = stripe.checkout.Session.create(
payment_method_types=["card"],
mode="setup",
customer=organization.stripe_id,
setup_intent_data={
"metadata": {
"kind": "account_change_cc",
"namespace": organization.username,
"performer": get_authenticated_user().username,
"ip": get_request_ip(),
"subscription_id": cus.subscription.id,
},
},
success_url=success_url,
cancel_url=cancel_url,
)
return checkout_session
except stripe.error.APIConnectionError as se:
abort(503, message="Cannot contact Stripe: %s" % se)
except Exception as e:
abort(500, message=str(e))
raise Unauthorized()
@resource("/v1/user/plan")
@internal_only
@show_if(features.BILLING)
class UserPlan(ApiResource):
"""
Resource for managing a user's subscription.
"""
schemas = {
"UserSubscription": {
"id": "UserSubscription",
"type": "object",
"description": "Description of a user card",
"required": ["plan"],
"properties": {
"plan": {
"type": "string",
"description": "Plan name to which the user wants to subscribe",
},
"success_url": {
"type": "string",
"description": "Redirect url after successful checkout",
},
"cancel_url": {
"type": "string",
"description": "Redirect url after cancelled checkout",
},
},
},
}
@require_user_admin()
@nickname("createUserSubscription")
@validate_json_request("UserSubscription")
def post(self):
"""
Create the user's subscription. Returns a Stripe checkout session.
"""
request_data = request.get_json()
plan = request_data["plan"]
success_url = request_data.get("success_url")
cancel_url = request_data.get("cancel_url")
if not success_url or not cancel_url:
raise InvalidRequest()
user = get_authenticated_user()
if not user.stripe_id:
try:
cus = billing.Customer.create(
email=user.email,
)
user.stripe_id = cus.id
user.save()
except stripe.error.APIConnectionError as e:
return connection_response(e)
try:
price = get_price(plan, False)
if not price:
abort(404, message="Plan not found")
checkout_session = stripe.checkout.Session.create(
line_items=[
{
"price": price["stripeId"],
"quantity": 1,
},
],
customer=user.stripe_id,
subscription_data={
"metadata": {
"kind": "account_change_plan",
"namespace": user.username,
"performer": user.username,
"ip": get_request_ip(),
"plan": price["stripeId"],
}
},
mode="subscription",
success_url=success_url,
cancel_url=cancel_url,
)
return checkout_session
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
except Exception as e:
abort(500, message=str(e))
@require_user_admin()
@nickname("updateUserSubscription")
@validate_json_request("UserSubscription")
def put(self):
"""
Update the user's existing subscription.
"""
request_data = request.get_json()
plan = request_data["plan"]
user = get_authenticated_user()
if not user.stripe_id:
raise InvalidRequest()
price = get_price(plan, False)
if not price:
abort(404, message="Plan not found")
return change_subscription(user, price)
@require_user_admin()
@nickname("getUserSubscription")
def get(self):
"""
Fetch any existing subscription for the user.
"""
cus = None
user = get_authenticated_user()
private_repos = model.user.get_private_repo_count(user.username)
if user.stripe_id:
try:
cus = billing.Customer.retrieve(user.stripe_id)
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
if cus.subscription:
return subscription_view(cus.subscription, private_repos)
return {
"hasSubscription": False,
"isExistingCustomer": cus is not None,
"plan": "free",
"usedPrivateRepos": private_repos,
}
@resource("/v1/organization/<orgname>/plan")
@path_param("orgname", "The name of the organization")
@internal_only
@related_user_resource(UserPlan)
@show_if(features.BILLING)
class OrganizationPlan(ApiResource):
"""
Resource for managing a org's subscription.
"""
schemas = {
"OrgSubscription": {
"id": "OrgSubscription",
"type": "object",
"description": "Description of a user card",
"required": ["plan"],
"properties": {
"plan": {
"type": "string",
"description": "Plan name to which the user wants to subscribe",
},
"success_url": {
"type": "string",
"description": "Redirect url after successful checkout",
},
"cancel_url": {
"type": "string",
"description": "Redirect url after cancelled checkout",
},
},
},
}
@require_scope(scopes.ORG_ADMIN)
@nickname("createOrgSubscription")
@validate_json_request("OrgSubscription")
def post(self, orgname):
"""
Create the org's subscription. Returns a Stripe checkout session.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can():
request_data = request.get_json()
plan = request_data["plan"]
success_url = request_data.get("success_url")
cancel_url = request_data.get("cancel_url")
if not success_url or not cancel_url:
raise InvalidRequest()
organization = model.organization.get_organization(orgname)
if not organization.stripe_id:
try:
cus = billing.Customer.create(
email=organization.email,
)
organization.stripe_id = cus.id
organization.save()
except stripe.error.APIConnectionError as e:
return connection_response(e)
try:
price = get_price(plan, True)
if not price:
abort(404, message="Plan not found")
checkout_session = stripe.checkout.Session.create(
line_items=[
{
"price": price["stripeId"],
"quantity": 1,
},
],
customer=organization.stripe_id,
subscription_data={
"metadata": {
"kind": "account_change_plan",
"namespace": organization.username,
"performer": get_authenticated_user().username,
"ip": get_request_ip(),
"plan": price["stripeId"],
"trial_period_days": price["free_trial_days"],
},
},
mode="subscription",
success_url=success_url,
cancel_url=cancel_url,
)
return checkout_session
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
except Exception as e:
abort(500, message=str(e))
raise Unauthorized()
@require_scope(scopes.ORG_ADMIN)
@nickname("updateOrgSubscription")
@validate_json_request("OrgSubscription")
def put(self, orgname):
"""
Update the org's subscription.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can():
request_data = request.get_json()
plan = request_data["plan"]
organization = model.organization.get_organization(orgname)
if not organization.stripe_id:
raise InvalidRequest()
price = get_price(plan, True)
if not price:
abort(404, message="Plan not found")
return change_subscription(organization, price)
raise Unauthorized()
@require_scope(scopes.ORG_ADMIN)
@nickname("getOrgSubscription")
def get(self, orgname):
"""
Fetch any existing subscription for the org.
"""
cus = None
permission = AdministerOrganizationPermission(orgname)
if permission.can():
private_repos = model.user.get_private_repo_count(orgname)
organization = model.organization.get_organization(orgname)
if organization.stripe_id:
try:
cus = billing.Customer.retrieve(organization.stripe_id)
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
if cus.subscription:
return subscription_view(cus.subscription, private_repos)
return {
"hasSubscription": False,
"isExistingCustomer": cus is not None,
"plan": "free",
"usedPrivateRepos": private_repos,
}
raise Unauthorized()
@resource("/v1/user/invoices")
@internal_only
@show_if(features.BILLING)
class UserInvoiceList(ApiResource):
"""
Resource for listing a user's invoices.
"""
@require_user_admin()
@nickname("listUserInvoices")
def get(self):
"""
List the invoices for the current user.
"""
user = get_authenticated_user()
if not user.stripe_id:
raise NotFound()
return get_invoices(user.stripe_id)
@resource("/v1/organization/<orgname>/invoices")
@path_param("orgname", "The name of the organization")
@related_user_resource(UserInvoiceList)
@show_if(features.BILLING)
class OrganizationInvoiceList(ApiResource):
"""
Resource for listing an orgnaization's invoices.
"""
@require_scope(scopes.ORG_ADMIN)
@nickname("listOrgInvoices")
def get(self, orgname):
"""
List the invoices for the specified orgnaization.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.organization.get_organization(orgname)
if not organization.stripe_id:
raise NotFound()
return get_invoices(organization.stripe_id)
raise Unauthorized()
@resource("/v1/user/invoice/fields")
@internal_only
@show_if(features.BILLING)
class UserInvoiceFieldList(ApiResource):
"""
Resource for listing and creating a user's custom invoice fields.
"""
schemas = {
"InvoiceField": {
"id": "InvoiceField",
"type": "object",
"description": "Description of an invoice field",
"required": ["title", "value"],
"properties": {
"title": {
"type": "string",
"description": "The title of the field being added",
},
"value": {
"type": "string",
"description": "The value of the field being added",
},
},
},
}
@require_user_admin()
@nickname("listUserInvoiceFields")
def get(self):
"""
List the invoice fields for the current user.
"""
user = get_authenticated_user()
if not user.stripe_id:
raise NotFound()
return {"fields": get_invoice_fields(user)[0]}
@require_user_admin()
@nickname("createUserInvoiceField")
@validate_json_request("InvoiceField")
def post(self):
"""
Creates a new invoice field.
"""
user = get_authenticated_user()
if not user.stripe_id:
raise NotFound()
data = request.get_json()
created_field = create_billing_invoice_field(user, data["title"], data["value"])
return created_field
@resource("/v1/user/invoice/field/<field_uuid>")
@internal_only
@show_if(features.BILLING)
class UserInvoiceField(ApiResource):
"""
Resource for deleting a user's custom invoice fields.
"""
@require_user_admin()
@nickname("deleteUserInvoiceField")
def delete(self, field_uuid):
"""
Deletes the invoice field for the current user.
"""
user = get_authenticated_user()
if not user.stripe_id:
raise NotFound()
result = delete_billing_invoice_field(user, field_uuid)
if not result:
abort(404)
return "Okay", 201
@resource("/v1/organization/<orgname>/invoice/fields")
@path_param("orgname", "The name of the organization")
@related_user_resource(UserInvoiceFieldList)
@internal_only
@show_if(features.BILLING)
class OrganizationInvoiceFieldList(ApiResource):
"""
Resource for listing and creating an organization's custom invoice fields.
"""
schemas = {
"InvoiceField": {
"id": "InvoiceField",
"type": "object",
"description": "Description of an invoice field",
"required": ["title", "value"],
"properties": {
"title": {
"type": "string",
"description": "The title of the field being added",
},
"value": {
"type": "string",
"description": "The value of the field being added",
},
},
},
}
@require_scope(scopes.ORG_ADMIN)
@nickname("listOrgInvoiceFields")
def get(self, orgname):
"""
List the invoice fields for the organization.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.organization.get_organization(orgname)
if not organization.stripe_id:
raise NotFound()
return {"fields": get_invoice_fields(organization)[0]}
abort(403)
@require_scope(scopes.ORG_ADMIN)
@nickname("createOrgInvoiceField")
@validate_json_request("InvoiceField")
def post(self, orgname):
"""
Creates a new invoice field.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.organization.get_organization(orgname)
if not organization.stripe_id:
raise NotFound()
data = request.get_json()
created_field = create_billing_invoice_field(organization, data["title"], data["value"])
return created_field
abort(403)
@resource("/v1/organization/<orgname>/invoice/field/<field_uuid>")
@path_param("orgname", "The name of the organization")
@related_user_resource(UserInvoiceField)
@internal_only
@show_if(features.BILLING)
class OrganizationInvoiceField(ApiResource):
"""
Resource for deleting an organization's custom invoice fields.
"""
@require_scope(scopes.ORG_ADMIN)
@nickname("deleteOrgInvoiceField")
def delete(self, orgname, field_uuid):
"""
Deletes the invoice field for the current user.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.organization.get_organization(orgname)
if not organization.stripe_id:
raise NotFound()
result = delete_billing_invoice_field(organization, field_uuid)
if not result:
abort(404)
return "Okay", 201
abort(403)
@resource("/v1/organization/<orgname>/marketplace")
@path_param("orgname", "The name of the organization")
@show_if(features.BILLING)
class OrganizationRhSku(ApiResource):
"""
Resource for managing an organization's RH SKU
"""
@require_scope(scopes.ORG_ADMIN)
@nickname("listOrgSkus")
def get(self, orgname):
"""
Get sku assigned to org
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.organization.get_organization(orgname)
query = model.organization_skus.get_org_subscriptions(organization.id)
if query:
subscriptions = list(query.dicts())
for subscription in subscriptions:
subscription_details = marketplace_subscriptions.get_subscription_details(
subscription["subscription_id"]
)
now_ms = time.time() * 1000
expired_at = subscription_details["expiration_date"]
terminated_at = subscription_details["terminated_date"]
if expired_at < now_ms or (
terminated_at is not None and terminated_at < now_ms
):
model.organization_skus.remove_subscription_from_org(
organization.id, subscription["subscription_id"]
)
continue
subscription["sku"] = subscription_details["sku"]
subscription["metadata"] = get_plan_using_rh_sku(subscription_details["sku"])
if subscription.get("quantity") is None:
subscription["quantity"] = 1
return subscriptions
else:
return []
abort(401)
@require_scope(scopes.ORG_ADMIN)
@nickname("bindSkuToOrg")
def post(self, orgname):
"""
Assigns a sku to an org
"""
permission = AdministerOrganizationPermission(orgname)
request_data = request.get_json()
organization = model.organization.get_organization(orgname)
subscriptions = request_data["subscriptions"]
if permission.can():
for subscription in subscriptions:
subscription_id = subscription.get("subscription_id")
if subscription_id is None:
break
user = get_authenticated_user()
account_numbers = marketplace_users.get_account_number(user)
user_available_subscriptions = []
for account_number in account_numbers:
user_available_subscriptions += (
marketplace_subscriptions.get_list_of_subscriptions(
account_number, filter_out_org_bindings=True
)
)
if subscriptions is None:
abort(401, message="no valid subscriptions present")
user_subs = {sub["id"]: sub for sub in user_available_subscriptions}
if int(subscription_id) in user_subs.keys():
# Check if the sku is being split
quantity = subscription.get("quantity")
base_quantity = user_subs.get(subscription_id).get("quantity", 1)
sku = user_subs.get(subscription_id).get("sku")
if quantity is not None:
if sku != "MW02702" and quantity != base_quantity:
abort(403, message="cannot split a non-MW02702 sku")
if quantity > base_quantity:
abort(400, message="quantity cannot exceed available amount")
else:
quantity = base_quantity
try:
model.organization_skus.bind_subscription_to_org(
user_id=user.id,
subscription_id=subscription_id,
org_id=organization.id,
quantity=quantity,
)
except model.OrgSubscriptionBindingAlreadyExists:
abort(400, message="subscription is already bound to an org")
else:
abort(
401,
message=f"subscription {subscription_id} does not belong to {user.username}",
)
return "Okay", 201
abort(401)
@resource("/v1/organization/<orgname>/marketplace/batchremove")
@path_param("orgname", "The name of the organization")
@show_if(features.BILLING)
class OrganizationRhSkuBatchRemoval(ApiResource):
@require_scope(scopes.ORG_ADMIN)
@nickname("batchRemoveSku")
def post(self, orgname):
"""
Batch remove skus from org
"""
permission = AdministerOrganizationPermission(orgname)
request_data = request.get_json()
subscriptions = request_data["subscriptions"]
if permission.can():
try:
organization = model.organization.get_organization(orgname)
except InvalidOrganizationException:
return ("Organization not valid", 400)
for subscription in subscriptions:
subscription_id = int(subscription.get("subscription_id"))
if subscription_id is None:
break
model.organization_skus.remove_subscription_from_org(
organization.id, subscription_id
)
return ("Deleted", 204)
abort(401)
@resource("/v1/organization/<orgname>/marketplace/<subscription_id>")
@path_param("orgname", "The name of the organization")
@path_param("subscription_id", "Marketplace subscription id")
@show_if(features.BILLING)
class OrganizationRhSkuSubscriptionField(ApiResource):
"""
Resource for removing RH skus from an organization
"""
@require_scope(scopes.ORG_ADMIN)
@nickname("removeSkuFromOrg")
def delete(self, orgname, subscription_id):
"""
Remove sku from an org
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
organization = model.organization.get_organization(orgname)
except InvalidOrganizationException:
return ("Organization not valid", 400)
model.organization_skus.remove_subscription_from_org(organization.id, subscription_id)
return ("Deleted", 204)
abort(401)
@resource("/v1/user/marketplace")
@show_if(features.RH_MARKETPLACE)
class UserSkuList(ApiResource):
"""
Resource for listing a user's RH skus
bound to an org
"""
@require_user_admin()
@nickname("getUserMarketplaceSubscriptions")
def get(self):
"""
List the invoices for the current user.
"""
user = get_authenticated_user()
account_numbers = marketplace_users.get_account_number(user)
if not account_numbers:
raise NotFound()
user_subscriptions = []
for account_number in account_numbers:
user_subscriptions += marketplace_subscriptions.get_list_of_subscriptions(
account_number
)
child_subscriptions = []
subscriptions_to_return = []
for subscription in user_subscriptions:
bound_to_org, bindings = organization_skus.subscription_bound_to_org(subscription["id"])
# fill in information for whether a subscription is bound to an org
metadata = get_plan_using_rh_sku(subscription["sku"])
subscription["metadata"] = metadata
if bound_to_org:
# special case for MW02702, which can be split across orgs
if subscription["sku"] == "MW02702":
number_of_bindings = 0
for binding in bindings:
# for each bound org, create a new subscription to add to
# the response body
child_subscription = subscription.copy()
child_subscription["quantity"] = binding["quantity"]
child_subscription[
"assigned_to_org"
] = model.organization.get_organization_by_id(binding["org_id"]).username
child_subscription["metadata"] = metadata
child_subscriptions.append(child_subscription)
number_of_bindings += binding["quantity"]
remaining_unbound = subscription["quantity"] - number_of_bindings
if remaining_unbound > 0:
subscription["quantity"] = remaining_unbound
subscription["assigned_to_org"] = None
subscriptions_to_return.append(subscription)
else:
# default case, only one org is bound
subscription["assigned_to_org"] = model.organization.get_organization_by_id(
bindings[0]["org_id"]
).username
subscriptions_to_return.append(subscription)
else:
subscription["assigned_to_org"] = None
subscriptions_to_return.append(subscription)
return subscriptions_to_return + child_subscriptions