Skip to content

Commit

Permalink
fix(billing): return service/asset usage for current period TASK-1327 (
Browse files Browse the repository at this point in the history
…#5326)

### 📣 Summary
Update asset and service usage code to return only a single set of data for the current billing cycle, regardless of whether that cycle is monthly or yearly and adjust frontend accordingly.

### 📖 Description
Currently, the `ServiceUsageSerializer` and `AssetUsageSerializer` return usage information calculated according to a monthly billing cycle and a yearly billing cycle, regardless of what kind of billing cycle a user is actually subscribed to. So if a user is on a monthly plan, we make up a hypothetical yearly cycle and return data for that in addition to the actual monthly cycle they are on. The frontend, in turn, selects the correct cycle information to display based on a separate request fetching the user’s subscription information. The hypothetical cycle is never displayed and doesn’t seem to be used for anything at all. 

This PR merges the functionality of the `get_monthly_billing_dates()` and `get_yearly_billing_dates()` utils into a single function that, once it determines which cycle an account is on, returns the relevant `period_start` and `period_end` dates. 

### 👀 Preview steps
For frontend preview:

1. Create a new user on a Stripe-enabled instance

2. Navigate to usage page. Observe that user is on community plan with start date corresponding to account creation date. Displayed usage period should correspond to either first day of this month or last day of previous month (most likely the latter, though it currently depends on one’s timezone) and the last day of this month.

3. Purchase a monthly plan and sync djstripe Subscriptions, navigate to usage page. Observe that start date is still account creation date. Displayed usage period should now correspond to start date and one month after start date.

4. Cancel plan as the user, then cancel the plan via Stripe (or ask someone to do this), and sync subscriptions. Visit 
usage page and observe dates have not changed (because your community plan dates now correspond to date of cancellation and one month later).

5. Sign up for a yearly plan and sync subscriptions. Visit usage page and observe that start date remains the same, while displayed usage period ends a year from today.
  • Loading branch information
jamesrkiger authored Dec 16, 2024
1 parent ad79e14 commit 4e3e5fd
Show file tree
Hide file tree
Showing 15 changed files with 185 additions and 271 deletions.
24 changes: 7 additions & 17 deletions jsapp/js/account/usage/usage.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ export interface AssetWithUsage {
asset: string;
uid: string;
asset__name: string;
nlp_usage_current_month: {
total_nlp_asr_seconds: number;
total_nlp_mt_characters: number;
};
nlp_usage_current_year: {
nlp_usage_current_period: {
total_nlp_asr_seconds: number;
total_nlp_mt_characters: number;
};
Expand All @@ -27,28 +23,22 @@ export interface AssetWithUsage {
total_nlp_mt_characters: number;
};
storage_bytes: number;
submission_count_current_month: number;
submission_count_current_year: number;
submission_count_current_period: number;
submission_count_all_time: number;
deployment_status: string;
}

export interface UsageResponse {
current_month_start: string;
current_month_end: string;
current_year_start: string;
current_year_end: string;
current_period_start: string;
current_period_end: string;
total_submission_count: {
current_month: number;
current_year: number;
current_period: number;
all_time: number;
};
total_storage_bytes: number;
total_nlp_usage: {
asr_seconds_current_month: number;
mt_characters_current_month: number;
asr_seconds_current_year: number;
mt_characters_current_year: number;
asr_seconds_current_period: number;
mt_characters_current_period: number;
asr_seconds_all_time: number;
mt_characters_all_time: number;
};
Expand Down
22 changes: 4 additions & 18 deletions jsapp/js/account/usage/usage.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import useWhenStripeIsEnabled from 'js/hooks/useWhenStripeIsEnabled.hook';
import {ProductsContext} from '../useProducts.hook';
import {UsageContext} from 'js/account/usage/useUsage.hook';
import {OneTimeAddOnsContext} from '../useOneTimeAddonList.hook';
import moment from 'moment';
import {YourPlan} from 'js/account/usage/yourPlan.component';
import cx from 'classnames';
import LimitNotifications from 'js/components/usageLimits/limitNotifications.component';
Expand Down Expand Up @@ -55,27 +54,14 @@ export default function Usage() {
const location = useLocation();

const dateRange = useMemo(() => {
let startDate: string;
const endDate = usage.billingPeriodEnd
? formatDate(usage.billingPeriodEnd)
: formatDate(
moment(usage.currentMonthStart).add(1, 'month').toISOString()
);
switch (usage.trackingPeriod) {
case 'year':
startDate = formatDate(usage.currentYearStart);
break;
default:
startDate = formatDate(usage.currentMonthStart);
break;
}
const startDate = formatDate(usage.currentPeriodStart);
const endDate = formatDate(usage.currentPeriodEnd);
return t('##start_date## to ##end_date##')
.replace('##start_date##', startDate)
.replace('##end_date##', endDate);
}, [
usage.currentYearStart,
usage.currentMonthStart,
usage.billingPeriodEnd,
usage.currentPeriodStart,
usage.currentPeriodEnd,
usage.trackingPeriod,
]);

Expand Down
10 changes: 3 additions & 7 deletions jsapp/js/account/usage/usageProjectBreakdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,18 +129,14 @@ const ProjectBreakdown = () => {

const renderProjectRow = (project: AssetWithUsage) => {
const periodSubmissions =
project[
`submission_count_current_${usage.trackingPeriod}`
].toLocaleString();
project.submission_count_current_period.toLocaleString();

const periodASRSeconds = convertSecondsToMinutes(
project[`nlp_usage_current_${usage.trackingPeriod}`].total_nlp_asr_seconds
project.nlp_usage_current_period.total_nlp_asr_seconds
).toLocaleString();

const periodMTCharacters =
project[
`nlp_usage_current_${usage.trackingPeriod}`
].total_nlp_mt_characters.toLocaleString();
project.nlp_usage_current_period.total_nlp_mt_characters.toLocaleString();

return (
<tr key={project.asset}>
Expand Down
21 changes: 9 additions & 12 deletions jsapp/js/account/usage/useUsage.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ export interface UsageState {
submissions: number;
transcriptionMinutes: number;
translationChars: number;
currentMonthStart: string;
currentYearStart: string;
billingPeriodEnd: string | null;
currentPeriodStart: string;
currentPeriodEnd: string;
trackingPeriod: RecurringInterval;
lastUpdated?: String | null;
}
Expand All @@ -22,9 +21,8 @@ const INITIAL_USAGE_STATE: UsageState = Object.freeze({
submissions: 0,
transcriptionMinutes: 0,
translationChars: 0,
currentMonthStart: '',
currentYearStart: '',
billingPeriodEnd: null,
currentPeriodStart: '',
currentPeriodEnd: '',
trackingPeriod: 'month',
lastUpdated: '',
});
Expand All @@ -49,15 +47,14 @@ const loadUsage = async (
}
return {
storage: usage.total_storage_bytes,
submissions: usage.total_submission_count[`current_${trackingPeriod}`],
submissions: usage.total_submission_count.current_period,
transcriptionMinutes: convertSecondsToMinutes(
usage.total_nlp_usage[`asr_seconds_current_${trackingPeriod}`]
usage.total_nlp_usage.asr_seconds_current_period
),
translationChars:
usage.total_nlp_usage[`mt_characters_current_${trackingPeriod}`],
currentMonthStart: usage.current_month_start,
currentYearStart: usage.current_year_start,
billingPeriodEnd: usage[`current_${trackingPeriod}_end`],
usage.total_nlp_usage.mt_characters_current_period,
currentPeriodStart: usage.current_period_start,
currentPeriodEnd: usage.current_period_end,
trackingPeriod,
lastUpdated,
};
Expand Down
51 changes: 10 additions & 41 deletions kobo/apps/organizations/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from kpi.models.object_permission import ObjectPermission


def get_monthly_billing_dates(organization: Union['Organization', None]):
"""Returns start and end dates of an organization's monthly billing cycle"""
def get_billing_dates(organization: Union['Organization', None]):
"""Returns start and end dates of an organization's billing cycle."""

now = timezone.now().replace(tzinfo=ZoneInfo('UTC'))
first_of_this_month = datetime(now.year, now.month, 1, tzinfo=ZoneInfo('UTC'))
Expand Down Expand Up @@ -49,7 +49,6 @@ def get_monthly_billing_dates(organization: Union['Organization', None]):
if not billing_details.get('billing_cycle_anchor'):
return first_of_this_month, first_of_next_month

# Subscription is billed monthly, use the current billing period dates
if billing_details.get('recurring_interval') == 'month':
period_start = billing_details.get('current_period_start').replace(
tzinfo=ZoneInfo('UTC')
Expand All @@ -59,38 +58,6 @@ def get_monthly_billing_dates(organization: Union['Organization', None]):
)
return period_start, period_end

# Subscription is billed yearly - count backwards from the end of the
# current billing year
period_start = billing_details.get('current_period_end').replace(
tzinfo=ZoneInfo('UTC')
)
while period_start > now:
period_start -= relativedelta(months=1)
period_end = period_start + relativedelta(months=1)
return period_start, period_end


def get_real_owner(user: User) -> User:
organization = user.organization
if organization.is_mmo:
return organization.owner_user_object
return user


def get_yearly_billing_dates(organization: Union['Organization', None]):
"""Returns start and end dates of an organization's annual billing cycle"""
now = timezone.now().replace(tzinfo=ZoneInfo('UTC'))
first_of_this_year = datetime(now.year, 1, 1, tzinfo=ZoneInfo('UTC'))
first_of_next_year = first_of_this_year + relativedelta(years=1)

if not organization:
return first_of_this_year, first_of_next_year
if not (billing_details := organization.active_subscription_billing_details()):
return first_of_this_year, first_of_next_year
if not (anchor_date := billing_details.get('billing_cycle_anchor')):
return first_of_this_year, first_of_next_year

# Subscription is billed yearly, use the dates from the subscription
if billing_details.get('recurring_interval') == 'year':
period_start = billing_details.get('current_period_start').replace(
tzinfo=ZoneInfo('UTC')
Expand All @@ -100,12 +67,14 @@ def get_yearly_billing_dates(organization: Union['Organization', None]):
)
return period_start, period_end

# Subscription is monthly, calculate this year's start based on anchor date
period_start = anchor_date.replace(tzinfo=ZoneInfo('UTC')) + relativedelta(years=1)
while period_start < now:
anchor_date += relativedelta(years=1)
period_end = period_start + relativedelta(years=1)
return period_start, period_end
return first_of_this_month, first_of_next_month


def get_real_owner(user: User) -> User:
organization = user.organization
if organization.is_mmo:
return organization.owner_user_object
return user


def revoke_org_asset_perms(organization: Organization, user_ids: list[int]):
Expand Down
20 changes: 8 additions & 12 deletions kobo/apps/organizations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
)
from kpi.utils.object_permission import get_database_user
from kpi.views.v2.asset import AssetViewSet
from ..accounts.mfa.models import MfaMethod
from .models import Organization, OrganizationOwner, OrganizationUser
from .permissions import (
HasOrgRolePermission,
IsOrgAdminPermission,
OrganizationNestedHasOrgRolePermission,
)
from .serializers import OrganizationSerializer, OrganizationUserSerializer
from ..accounts.mfa.models import MfaMethod


class OrganizationAssetViewSet(AssetViewSet):
Expand Down Expand Up @@ -129,22 +129,18 @@ def service_usage(self, request, pk=None, *args, **kwargs):
> curl -X GET https://[kpi]/api/v2/organizations/{organization_id}/service_usage/
> {
> "total_nlp_usage": {
> "asr_seconds_current_month": {integer},
> "asr_seconds_current_year": {integer},
> "asr_seconds_current_period": {integer},
> "asr_seconds_all_time": {integer},
> "mt_characters_current_month": {integer},
> "mt_characters_current_year": {integer},
> "mt_characters_current_period": {integer},
> "mt_characters_all_time": {integer},
> },
> "total_storage_bytes": {integer},
> "total_submission_count": {
> "current_month": {integer},
> "current_year": {integer},
> "current_period": {integer},
> "all_time": {integer},
> },
> "current_month_start": {string (date), ISO format},
> "current_year_start": {string (date), ISO format},
> "billing_period_end": {string (date), ISO format}|{None},
> "current_period_start": {string (date), ISO format},
> "current_period_end": {string (date), ISO format}|{None},
> "last_updated": {string (date), ISO format},
> }
### CURRENT ENDPOINT
Expand Down Expand Up @@ -187,7 +183,7 @@ def asset_usage(self, request, pk=None, *args, **kwargs):
> "asset_type": {string},
> "asset": {asset_url},
> "asset_name": {string},
> "nlp_usage_current_month": {
> "nlp_usage_current_period": {
> "total_asr_seconds": {integer},
> "total_mt_characters": {integer},
> }
Expand All @@ -196,7 +192,7 @@ def asset_usage(self, request, pk=None, *args, **kwargs):
> "total_mt_characters": {integer},
> }
> "storage_bytes": {integer},
> "submission_count_current_month": {integer},
> "submission_count_current_period": {integer},
> "submission_count_all_time": {integer},
> "deployment_status": {string},
> },{...}
Expand Down
18 changes: 6 additions & 12 deletions kobo/apps/project_ownership/tests/api/v2/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,35 +365,29 @@ def __add_submissions(self):
def test_account_usage_transferred_to_new_user(self):
expected_data = {
'total_nlp_usage': {
'asr_seconds_current_year': 120,
'mt_characters_current_year': 1000,
'asr_seconds_current_month': 120,
'mt_characters_current_month': 1000,
'asr_seconds_current_period': 120,
'mt_characters_current_period': 1000,
'asr_seconds_all_time': 120,
'mt_characters_all_time': 1000,
},
'total_storage_bytes': 191642,
'total_submission_count': {
'all_time': 1,
'current_year': 1,
'current_month': 1,
'current_period': 1,
},
}

expected_empty_data = {
'total_nlp_usage': {
'asr_seconds_current_year': 0,
'mt_characters_current_year': 0,
'asr_seconds_current_month': 0,
'mt_characters_current_month': 0,
'asr_seconds_current_period': 0,
'mt_characters_current_period': 0,
'asr_seconds_all_time': 0,
'mt_characters_all_time': 0,
},
'total_storage_bytes': 0,
'total_submission_count': {
'all_time': 0,
'current_year': 0,
'current_month': 0,
'current_period': 0,
},
}

Expand Down
Loading

0 comments on commit 4e3e5fd

Please sign in to comment.