Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show storage add-ons in usage limits #4660

Merged
merged 17 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
85 changes: 63 additions & 22 deletions jsapp/js/account/stripe.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ export async function getSubscriptionInterval() {
subscriptionStore.fetchSubscriptionInfo();
}
await when(() => subscriptionStore.isInitialised);
const subscriptionList: SubscriptionInfo[] =
subscriptionStore.subscriptionResponse;
const subscriptionList: SubscriptionInfo[] = subscriptionStore.planResponse;
const activeSubscription = subscriptionList.find((sub) =>
ACTIVE_STRIPE_STATUSES.includes(sub.status)
);
Expand All @@ -120,17 +119,52 @@ export async function getSubscriptionInterval() {
return 'month';
}

const defaultLimits: AccountLimit = Object.freeze({
LMNTL marked this conversation as resolved.
Show resolved Hide resolved
submission_limit: 'unlimited',
nlp_seconds_limit: 'unlimited',
nlp_character_limit: 'unlimited',
storage_bytes_limit: 'unlimited',
});

function getLimitsForMetadata(
metadata: {[key: string]: string},
limitsToCompare: false | AccountLimit = false
) {
const limits: Partial<AccountLimit> = {};
LMNTL marked this conversation as resolved.
Show resolved Hide resolved
for (const [key, value] of Object.entries(metadata)) {
// if we need to compare limits, make sure we're not overwriting a higher limit from somewhere else
if (limitsToCompare) {
if (
!limitsToCompare?.[key as keyof AccountLimit] ||
(value !== 'unlimited' &&
LMNTL marked this conversation as resolved.
Show resolved Hide resolved
value <= limitsToCompare[key as keyof AccountLimit])
) {
continue;
}
}
// only use metadata needed for limit calculations
if (key in defaultLimits) {
limits[key as keyof AccountLimit] =
value === 'unlimited' ? value : parseInt(value);
LMNTL marked this conversation as resolved.
Show resolved Hide resolved
}
}
return limits;
}

export async function getAccountLimits() {
await when(() => subscriptionStore.isInitialised);
const subscriptions = [...subscriptionStore.subscriptionResponse];
const activeSubscriptions = subscriptions.filter((subscription) =>
const plans = [...subscriptionStore.planResponse];
const activeSubscriptions = plans.filter((subscription) =>
ACTIVE_STRIPE_STATUSES.includes(subscription.status)
);
let metadata;
let hasFreeTier = false;
if (activeSubscriptions.length) {
// get metadata from the user's subscription
metadata = activeSubscriptions[0].items[0].price.product.metadata;
// get metadata from the user's subscription (prioritize price metadata over product metadata)
metadata = {
...activeSubscriptions[0].items[0].price.product.metadata,
...activeSubscriptions[0].items[0].price.metadata,
};
} else {
// the user has no subscription, so get limits from the free monthly price
hasFreeTier = true;
Expand All @@ -152,29 +186,36 @@ export async function getAccountLimits() {
}

// initialize to unlimited
const limits: AccountLimit = {
submission_limit: 'unlimited',
nlp_seconds_limit: 'unlimited',
nlp_character_limit: 'unlimited',
storage_bytes_limit: 'unlimited',
};
let limits: AccountLimit = {...defaultLimits};

// get the limits from the metadata
for (const [key, value] of Object.entries(metadata)) {
if (Object.keys(limits).includes(key)) {
limits[key as keyof AccountLimit] =
value === 'unlimited' ? value : parseInt(value);
}
}
// apply any limits from the metadata
limits = {...limits, ...getLimitsForMetadata(metadata)};

// if the user is on the free tier, overwrite their limits with whatever free tier limits exist
if (hasFreeTier) {
// if the user is on the free tier, overwrite their limits with whatever free tier limits exist
await when(() => envStore.isReady);
const thresholds = envStore.data.free_tier_thresholds;
thresholds.storage && (limits['storage_bytes_limit'] = thresholds.storage);
thresholds.data && (limits['submission_limit'] = thresholds.data);
thresholds.translation_chars && (limits['nlp_character_limit'] = thresholds.translation_chars);
thresholds.transcription_minutes && (limits['nlp_seconds_limit'] = thresholds.transcription_minutes * 60);
thresholds.translation_chars &&
(limits['nlp_character_limit'] = thresholds.translation_chars);
thresholds.transcription_minutes &&
(limits['nlp_seconds_limit'] = thresholds.transcription_minutes * 60);

// if the user has active recurring add-ons, use those as the final say on their limits
let activeAddOns = [...subscriptionStore.addOnsResponse];
activeAddOns = activeAddOns.filter((subscription) =>
ACTIVE_STRIPE_STATUSES.includes(subscription.status)
);
if (activeAddOns.length) {
activeAddOns.forEach((addOn) => {
metadata = {
...addOn.items[0].price.product.metadata,
...addOn.items[0].price.metadata,
};
limits = {...limits, ...getLimitsForMetadata(metadata, limits)};
});
}
}

return limits;
Expand Down
6 changes: 3 additions & 3 deletions jsapp/js/account/stripe.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ export async function hasActiveSubscription() {
}

await when(() => subscriptionStore.isInitialised);
const subscriptions = subscriptionStore.subscriptionResponse;
if (!subscriptions.length) {
const plans = subscriptionStore.planResponse;
if (!plans.length) {
return false;
}

return (
subscriptions.filter(
plans.filter(
(sub) =>
ACTIVE_STRIPE_STATUSES.includes(sub.status) &&
sub.items?.[0].price.unit_amount > 0
Expand Down
16 changes: 12 additions & 4 deletions jsapp/js/account/subscriptionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ export async function fetchProducts() {
}

class SubscriptionStore {
public subscriptionResponse: SubscriptionInfo[] = [];
public planResponse: SubscriptionInfo[] = [];
public addOnsResponse: SubscriptionInfo[] = [];
public subscribedProduct: BaseProduct | null = null;
public productsResponse: Product[] | null = null;
public isPending = false;
public isInitialised = false;

Expand Down Expand Up @@ -121,8 +121,16 @@ class SubscriptionStore {
private onFetchSubscriptionInfoDone(
response: PaginatedResponse<SubscriptionInfo>
) {
this.subscriptionResponse = response.results;
this.subscribedProduct = response.results[0]?.plan?.product || null;
// get any plan subscriptions for the user
this.planResponse = response.results.filter(
(sub) => sub.items[0]?.price.product.metadata?.product_type == 'plan'
);
// get any recurring add-on subscriptions for the user
this.addOnsResponse = response.results.filter(
(sub) => sub.items[0]?.price.product.metadata?.product_type == 'addon'
);
this.subscribedProduct =
this.planResponse[0]?.items[0]?.price.product || null;
this.isPending = false;
this.isInitialised = true;
}
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/components/usageLimits/useExceedingLimits.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const useExceedingLimits = () => {
() => subscriptionStore.isInitialised,
() => {
dispatch({
prodData: subscriptionStore.subscriptionResponse,
prodData: subscriptionStore.planResponse,
});
}
),
Expand Down
32 changes: 21 additions & 11 deletions kpi/views/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from rest_framework.views import APIView

from hub.utils.i18n import I18nUtils
from kobo.apps.organizations.models import OrganizationOwner
from kobo.static_lists import COUNTRIES
from kobo.apps.accounts.mfa.models import MfaAvailableToUser
from kobo.apps.constance_backends.utils import to_python_object
Expand Down Expand Up @@ -174,17 +175,26 @@ def process_other_configs(request):
)

# If the user isn't eligible for the free tier override, don't send free tier data to the frontend
if request.user.id and request.user.date_joined.date() > constance.config.FREE_TIER_CUTOFF_DATE:
data['free_tier_thresholds'] = {
'storage': None,
'data': None,
'transcription_minutes': None,
'translation_chars': None,
}
data['free_tier_display'] = {
'name': None,
'feature_list': [],
}
if request.user.id:
# default to checking the user's join date
date_joined = request.user.date_joined.date()
# if the user is in an organization, use the organization owner's join date instead
if request.user.organizations_organization:
LMNTL marked this conversation as resolved.
Show resolved Hide resolved
owner = OrganizationOwner.objects.filter(organization__organization_users=request.user.id).first()
if owner:
date_joined = owner.organization_user.user.date_joined.date()
# if they didn't register before FREE_TIER_CUTOFF_DATE, don't display the custom free tier
if date_joined > constance.config.FREE_TIER_CUTOFF_DATE:
data['free_tier_thresholds'] = {
'storage': None,
'data': None,
'transcription_minutes': None,
'translation_chars': None,
}
data['free_tier_display'] = {
'name': None,
'feature_list': [],
}

return data

Expand Down