web/satellite/vuetify-poc: add project usages to billing

This change adds project usage and cost per project to the vuetify app.

Issue: https://github.com/storj/storj/issues/6117

Change-Id: I8921aacb6bb24b41794008100ea6e52deed76b60
This commit is contained in:
Wilfred Asomani 2023-08-23 18:25:31 +00:00 committed by Storj Robot
parent 4964ca5ffb
commit f1e8cdfe3e
3 changed files with 251 additions and 37 deletions

View File

@ -0,0 +1,29 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-row>
<v-col>
<h4 class="mt-4">Costs per project</h4>
</v-col>
</v-row>
<v-row>
<v-col>
<usage-and-charges-item-component v-for="projectID of projectIds" :key="projectID" :project-id="projectID" />
</v-col>
</v-row>
</template>
<script setup lang="ts">
import {
VRow,
VCol,
} from 'vuetify/components';
import UsageAndChargesItemComponent from '@poc/components/billing/UsageAndChargesItemComponent.vue';
const props = defineProps<{
projectIds: string[],
}>();
</script>

View File

@ -0,0 +1,205 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-card rounded="lg" variant="flat" :border="true" class="mb-4">
<v-expansion-panels>
<v-expansion-panel rounded="lg">
<v-expansion-panel-title>
<v-row justify="space-between" align="center">
<v-col>
{{ projectName }}
</v-col>
<v-col class="text-start text-sm-end">
<span class="align-end">
Estimated Total &nbsp;
<span
class="font-weight-bold"
>{{ centsToDollars(projectCharges.getProjectPrice(projectId)) }}
</span>
</span>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-table density="compact">
<thead>
<tr>
<th class="text-left">
Resource
</th>
<th class="text-left d-none d-md-table-cell">
Period
</th>
<th class="text-left d-none d-sm-table-cell">
Usage
</th>
<th class="text-right">
Cost
</th>
</tr>
</thead>
<tbody>
<tr
v-for="[partner, charge] in partnerCharges"
:key="partner"
>
<td>
<p>Storage <span class="d-none d-md-inline">({{ getStoragePrice(partner) }} per Gigabyte-Month)</span></p>
<p>Egress <span class="d-none d-md-inline">({{ getEgressPrice(partner) }} per GB)</span></p>
<p>Segments <span class="d-none d-md-inline">({{ getSegmentPrice(partner) }} per Segment-Month)</span></p>
</td>
<td class="d-none d-md-table-cell"><p v-for="i in 3" :key="i">{{ getPeriod(charge) }}</p></td>
<td class="d-none d-sm-table-cell">
<p>{{ getStorageFormatted(charge) }} Gigabyte-month</p>
<p>{{ getEgressAmountAndDimension(charge) }}</p>
<p>{{ getSegmentCountFormatted(charge) }} Segment-month</p>
</td>
<td class="text-right">
<p>{{ centsToDollars(charge.storagePrice) }}</p>
<p>{{ centsToDollars(charge.egressPrice) }}</p>
<p>{{ centsToDollars(charge.segmentPrice) }}</p>
</td>
</tr>
</tbody>
</v-table>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import {
VCard,
VCol,
VExpansionPanel,
VExpansionPanels,
VExpansionPanelText,
VExpansionPanelTitle,
VRow,
VTable,
} from 'vuetify/components';
import { CENTS_MB_TO_DOLLARS_GB_SHIFT, centsToDollars, decimalShift, formatPrice } from '@/utils/strings';
import { ProjectCharge, ProjectCharges, ProjectUsagePriceModel } from '@/types/payments';
import { Project } from '@/types/projects';
import { Size } from '@/utils/bytesSize';
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
import { useBillingStore } from '@/store/modules/billingStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
/**
* HOURS_IN_MONTH constant shows amount of hours in 30-day month.
*/
const HOURS_IN_MONTH = 720;
const props = withDefaults(defineProps<{
/**
* The ID of the project for which to show the usage and charge information.
*/
projectId?: string;
}>(), {
projectId: '',
});
const billingStore = useBillingStore();
const projectsStore = useProjectsStore();
/**
* An array of tuples containing the partner name and usage charge for the specified project ID.
*/
const partnerCharges = computed((): [partner: string, charge: ProjectCharge][] => {
const arr = billingStore.state.projectCharges.toArray();
arr.sort(([partner1], [partner2]) => partner1.localeCompare(partner2));
const tuple = arr.find(tuple => tuple[0] === props.projectId);
return tuple ? tuple[1] : [];
});
/**
* projectName returns project name.
*/
const projectName = computed((): string => {
const projects: Project[] = projectsStore.state.projects;
const project: Project | undefined = projects.find(project => project.id === props.projectId);
return project?.name || '';
});
/**
* Returns project usage price model from store.
*/
const projectCharges = computed((): ProjectCharges => {
return billingStore.state.projectCharges as ProjectCharges;
});
/**
* Returns project usage price model from store.
*/
function getPriceModel(partner: string): ProjectUsagePriceModel {
return projectCharges.value.getUsagePriceModel(partner) || billingStore.state.usagePriceModel;
}
/**
* Returns string of date range.
*/
function getPeriod(charge: ProjectCharge): string {
const since = `${SHORT_MONTHS_NAMES[charge.since.getUTCMonth()]} ${charge.since.getUTCDate()}`;
const before = `${SHORT_MONTHS_NAMES[charge.before.getUTCMonth()]} ${charge.before.getUTCDate()}`;
return `${since} - ${before}`;
}
/**
* Returns formatted egress depending on amount of bytes.
*/
function egressFormatted(charge: ProjectCharge): Size {
return new Size(charge.egress, 2);
}
/**
* Returns formatted storage used in GB x month dimension.
*/
function getStorageFormatted(charge: ProjectCharge): string {
const bytesInGB = 1000000000;
return (charge.storage / HOURS_IN_MONTH / bytesInGB).toFixed(2);
}
/**
* Returns formatted segment count in segment x month dimension.
*/
function getSegmentCountFormatted(charge: ProjectCharge): string {
return (charge.segmentCount / HOURS_IN_MONTH).toFixed(2);
}
/**
* Returns storage price per GB.
*/
function getStoragePrice(partner: string): string {
return formatPrice(decimalShift(getPriceModel(partner).storageMBMonthCents, CENTS_MB_TO_DOLLARS_GB_SHIFT));
}
/**
* Returns egress price per GB.
*/
function getEgressPrice(partner: string): string {
return formatPrice(decimalShift(getPriceModel(partner).egressMBCents, CENTS_MB_TO_DOLLARS_GB_SHIFT));
}
/**
* Returns segment price.
*/
function getSegmentPrice(partner: string): string {
return formatPrice(decimalShift(getPriceModel(partner).segmentMonthCents, 2));
}
/**
* Returns string of egress amount and dimension.
*/
function getEgressAmountAndDimension(charge: ProjectCharge): string {
const egress = egressFormatted(charge);
return `${egress.formattedBytes} ${egress.label}`;
}
</script>

View File

@ -119,43 +119,7 @@
</v-col>
</v-row>
<v-row>
<v-col>
<h4 class="mt-4">Costs per project</h4>
</v-col>
</v-row>
<v-row>
<v-col>
<v-card rounded="lg" variant="flat" :border="true" class="mb-4">
<v-expansion-panels>
<v-expansion-panel
title="My First Project"
text="Costs..."
rounded="lg"
/>
</v-expansion-panels>
</v-card>
<v-card rounded="lg" variant="flat" :border="true" class="mb-4">
<v-expansion-panels>
<v-expansion-panel
title="Storj Labs"
text="Costs..."
rounded="lg"
/>
</v-expansion-panels>
</v-card>
<v-card rounded="lg" variant="flat" :border="true" class="mb-4">
<v-expansion-panels>
<v-expansion-panel
title="Pictures"
text="Costs..."
rounded="lg"
/>
</v-expansion-panels>
</v-card>
</v-col>
</v-row>
<usage-and-charges-component :project-ids="projectIDs" />
</v-window-item>
<v-window-item>
@ -304,15 +268,18 @@ import { Coupon, CouponDuration, CreditCard } from '@/types/payments';
import { centsToDollars } from '@/utils/strings';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
import { useProjectsStore } from '@/store/modules/projectsStore';
import CreditCardComponent from '@poc/components/CreditCardComponent.vue';
import AddCreditCardComponent from '@poc/components/AddCreditCardComponent.vue';
import UsageAndChargesComponent from '@poc/components/billing/UsageAndChargesComponent.vue';
const tab = ref<string>('Overview');
const search = ref<string>('');
const selected = ref([]);
const billingStore = useBillingStore();
const projectsStore = useProjectsStore();
const { isLoading, withLoading } = useLoading();
const notify = useNotify();
@ -349,6 +316,16 @@ const invoices = [
},
];
/**
* projectIDs is an array of all of the project IDs for which there exist project usage charges.
*/
const projectIDs = computed((): string[] => {
return projectsStore.state.projects
.filter(proj => billingStore.state.projectCharges.hasProject(proj.id))
.sort((proj1, proj2) => proj1.name.localeCompare(proj2.name))
.map(proj => proj.id);
});
/**
* Returns price summary of all project usages.
*/
@ -420,6 +397,9 @@ onMounted(() => {
billingStore.getBalance(),
billingStore.getCoupon(),
billingStore.getCreditCards(),
projectsStore.getProjects(),
billingStore.getProjectUsageAndChargesCurrentRollup(),
billingStore.getProjectUsagePriceModel(),
]);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.BILLING_AREA);