satellite/payments: project charges api extended to show usage and period

Change-Id: I471def779d8b2a896fc43a692029233a2cd839b0
This commit is contained in:
VitaliiShpital 2020-03-04 15:23:10 +02:00
parent 4abbf3198d
commit 56c33f5193
16 changed files with 397 additions and 64 deletions

View File

@ -56,12 +56,12 @@ type StorageNodeUsage struct {
// ProjectUsage consist of period total storage, egress
// and objects count per hour for certain Project in bytes
type ProjectUsage struct {
Storage float64
Egress int64
ObjectCount float64
Storage float64 `json:"storage"`
Egress int64 `json:"egress"`
ObjectCount float64 `json:"objectCount"`
Since time.Time
Before time.Time
Since time.Time `json:"since"`
Before time.Time `json:"before"`
}
// BucketUsage consist of total bucket usage for period

View File

@ -7,6 +7,7 @@ import (
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
"time"
"github.com/gorilla/mux"
@ -88,7 +89,21 @@ func (p *Payments) ProjectsCharges(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
charges, err := p.service.Payments().ProjectsCharges(ctx)
sinceStamp, err := strconv.ParseInt(r.URL.Query().Get("from"), 10, 64)
if err != nil {
p.serveJSONError(w, http.StatusBadRequest, err)
return
}
beforeStamp, err := strconv.ParseInt(r.URL.Query().Get("to"), 10, 64)
if err != nil {
p.serveJSONError(w, http.StatusBadRequest, err)
return
}
since := time.Unix(sinceStamp, 0).UTC()
before := time.Unix(beforeStamp, 0).UTC()
charges, err := p.service.Payments().ProjectsCharges(ctx, since, before)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)

View File

@ -184,7 +184,7 @@ func (paymentService PaymentsService) MakeCreditCardDefault(ctx context.Context,
}
// ProjectsCharges returns how much money current user will be charged for each project which he owns.
func (paymentService PaymentsService) ProjectsCharges(ctx context.Context) (_ []payments.ProjectCharge, err error) {
func (paymentService PaymentsService) ProjectsCharges(ctx context.Context, since, before time.Time) (_ []payments.ProjectCharge, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := GetAuth(ctx)
@ -192,7 +192,7 @@ func (paymentService PaymentsService) ProjectsCharges(ctx context.Context) (_ []
return nil, err
}
return paymentService.service.accounts.ProjectCharges(ctx, auth.User.ID)
return paymentService.service.accounts.ProjectCharges(ctx, auth.User.ID, since, before)
}
// ListCreditCards returns a list of credit cards for a given payment account.

View File

@ -5,6 +5,7 @@ package payments
import (
"context"
"time"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/zeebo/errs"
@ -25,7 +26,7 @@ type Accounts interface {
Balance(ctx context.Context, userID uuid.UUID) (int64, error)
// ProjectCharges returns how much money current user will be charged for each project.
ProjectCharges(ctx context.Context, userID uuid.UUID) ([]ProjectCharge, error)
ProjectCharges(ctx context.Context, userID uuid.UUID, since, before time.Time) ([]ProjectCharge, error)
// Charges returns list of all credit card charges related to account.
Charges(ctx context.Context, userID uuid.UUID) ([]Charge, error)

View File

@ -5,6 +5,7 @@ package mockpayments
import (
"context"
"time"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/spacemonkeygo/monkit/v3"
@ -111,8 +112,8 @@ func (accounts *accounts) Balance(ctx context.Context, userID uuid.UUID) (_ int6
}
// ProjectCharges returns how much money current user will be charged for each project.
func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID) (charges []payments.ProjectCharge, err error) {
defer mon.Task()(&ctx, userID)(&err)
func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID, since, before time.Time) (charges []payments.ProjectCharge, err error) {
defer mon.Task()(&ctx, userID, since, before)(&err)
return []payments.ProjectCharge{}, nil
}

View File

@ -5,15 +5,19 @@ package payments
import (
"github.com/skyrings/skyring-common/tools/uuid"
"storj.io/storj/satellite/accounting"
)
// ProjectCharge shows how much money current project will charge in the end of the month.
// ProjectCharge shows project usage and how much money current project will charge in the end of the month.
type ProjectCharge struct {
accounting.ProjectUsage
ProjectID uuid.UUID `json:"projectId"`
// StorageGbHrs shows how much cents we should pay for storing GB*Hrs.
StorageGbHrs int64 `json:"storage"`
StorageGbHrs int64 `json:"storagePrice"`
// Egress shows how many cents we should pay for Egress.
Egress int64 `json:"egress"`
Egress int64 `json:"egressPrice"`
// ObjectCount shows how many cents we should pay for objects count.
ObjectCount int64 `json:"objectCount"`
ObjectCount int64 `json:"objectPrice"`
}

View File

@ -10,7 +10,6 @@ import (
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/stripe/stripe-go"
"storj.io/storj/private/date"
"storj.io/storj/satellite/payments"
)
@ -96,8 +95,8 @@ func (accounts *accounts) Balance(ctx context.Context, userID uuid.UUID) (_ int6
}
// ProjectCharges returns how much money current user will be charged for each project.
func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID) (charges []payments.ProjectCharge, err error) {
defer mon.Task()(&ctx, userID)(&err)
func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID, since, before time.Time) (charges []payments.ProjectCharge, err error) {
defer mon.Task()(&ctx, userID, since, before)(&err)
// to return empty slice instead of nil if there are no projects
charges = make([]payments.ProjectCharge, 0)
@ -107,11 +106,8 @@ func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID)
return nil, Error.Wrap(err)
}
start, end := date.MonthBoundary(time.Now().UTC())
// TODO: we should improve performance of this block of code. It takes ~4-5 sec to get project charges.
for _, project := range projects {
usage, err := accounts.service.usageDB.GetProjectTotal(ctx, project.ID, start, end)
usage, err := accounts.service.usageDB.GetProjectTotal(ctx, project.ID, since, before)
if err != nil {
return charges, Error.Wrap(err)
}
@ -119,6 +115,8 @@ func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID)
projectPrice := accounts.service.calculateProjectUsagePrice(usage.Egress, usage.Storage, usage.ObjectCount)
charges = append(charges, payments.ProjectCharge{
ProjectUsage: *usage,
ProjectID: project.ID,
Egress: projectPrice.Egress.IntPart(),
ObjectCount: projectPrice.Objects.IntPart(),

View File

@ -4,6 +4,7 @@
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { BillingHistoryItem, CreditCard, PaymentsApi, ProjectCharge, TokenDeposit } from '@/types/payments';
import { HttpClient } from '@/utils/httpClient';
import { toUnixTimestamp } from '@/utils/time';
/**
* PaymentsHttpApi is a http implementation of Payments API.
@ -57,8 +58,10 @@ export class PaymentsHttpApi implements PaymentsApi {
/**
* projectsCharges returns how much money current user will be charged for each project which he owns.
*/
public async projectsCharges(): Promise<ProjectCharge[]> {
const path = `${this.ROOT_PATH}/account/charges`;
public async projectsCharges(start: Date, end: Date): Promise<ProjectCharge[]> {
const since = toUnixTimestamp(start).toString();
const before = toUnixTimestamp(end).toString();
const path = `${this.ROOT_PATH}/account/charges?from=${since}&to=${before}`;
const response = await this.client.get(path);
if (!response.ok) {
@ -73,10 +76,15 @@ export class PaymentsHttpApi implements PaymentsApi {
if (charges) {
return charges.map(charge =>
new ProjectCharge(
charge.projectId,
charge.storage,
new Date(charge.since),
new Date(charge.before),
charge.egress,
charge.objectCount),
charge.storage,
charge.objectCount,
charge.projectId,
charge.storagePrice,
charge.egressPrice,
charge.objectPrice),
);
}

View File

@ -45,6 +45,7 @@ import { Component, Vue } from 'vue-property-decorator';
import UsageChargeItem from '@/components/account/billing/monthlySummary/UsageChargeItem.vue';
import VButton from '@/components/common/VButton.vue';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { ProjectCharge } from '@/types/payments';
@Component({
@ -54,15 +55,29 @@ import { ProjectCharge } from '@/types/payments';
},
})
export default class MonthlyBillingSummary extends Vue {
/**
* Lifecycle hook after initial render.
* Fetches current project usage rollup.
*/
public async mounted(): Promise<void> {
try {
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_CHARGES_CURRENT_ROLLUP);
} catch (error) {
await this.$notify.error(`Unable to fetch project usage. ${error.message}`);
}
}
/**
* areUsageChargesShown indicates if area with all projects is expanded.
*/
private areUsageChargesShown: boolean = false;
/**
* usageCharges is an array of all ProjectCharges.
* usageCharges is an array of all stored ProjectCharges.
*/
public usageCharges: ProjectCharge[] = this.$store.state.paymentsModule.charges;
public get usageCharges(): ProjectCharge[] {
return this.$store.state.paymentsModule.charges;
}
/**
* String representation of current billing period dates range.
@ -201,7 +216,7 @@ export default class MonthlyBillingSummary extends Vue {
cursor: default;
max-height: 228px;
overflow-y: auto;
padding: 0 20px 20px 20px;
padding: 0 20px;
}
}
}

View File

@ -13,21 +13,37 @@
</svg>
<span>{{ projectName }}</span>
</div>
<span class="small-font-size">{{ item.summary() | centsToDollars }}</span>
</div>
<div class="usage-charge-item-container__detailed-info-container" v-if="isDetailedInfoShown">
<div class="usage-charge-item-container__detailed-info-container__item">
<span>Storage</span>
<span>{{ item.storage | centsToDollars }}</span>
<div class="usage-charge-item-container__detailed-info-container__info-header">
<span class="resource-header">RESOURCE</span>
<span class="period-header">PERIOD</span>
<span class="usage-header">USAGE</span>
<span class="cost-header">COST</span>
</div>
<div class="usage-charge-item-container__detailed-info-container__item">
<span>Egress</span>
<span>{{ item.egress | centsToDollars }}</span>
</div>
<div class="usage-charge-item-container__detailed-info-container__item">
<span>Objects</span>
<span>{{ item.objectCount | centsToDollars }}</span>
<div class="usage-charge-item-container__detailed-info-container__content-area">
<div class="usage-charge-item-container__detailed-info-container__content-area__resource-container">
<p>Storage ($0.010 per Gigabyte-Month)</p>
<p>Egress ($0.045 per GB)</p>
<p>Objects ($0.0000022 per Object-Month)</p>
</div>
<div class="usage-charge-item-container__detailed-info-container__content-area__period-container">
<p>{{ period }}</p>
<p>{{ period }}</p>
<p>{{ period }}</p>
</div>
<div class="usage-charge-item-container__detailed-info-container__content-area__usage-container">
<p>{{ storageFormatted }} Gigabyte-month</p>
<p>{{ egressAmountAndDimension }}</p>
<p>{{ objectCountFormatted }} Object-month</p>
</div>
<div class="usage-charge-item-container__detailed-info-container__content-area__cost-container">
<p class="price">{{ item.storagePrice | centsToDollars }}</p>
<p class="price">{{ item.egressPrice | centsToDollars }}</p>
<p class="price">{{ item.objectPrice | centsToDollars }}</p>
</div>
</div>
<span class="usage-charge-item-container__detailed-info-container__summary">{{ item.summary() | centsToDollars }}</span>
</div>
</div>
</template>
@ -37,17 +53,23 @@ import { Component, Prop, Vue } from 'vue-property-decorator';
import { ProjectCharge } from '@/types/payments';
import { Project } from '@/types/projects';
import { Size } from '@/utils/bytesSize';
@Component
export default class UsageChargeItem extends Vue {
/**
* item is an instance of ProjectCharge
* item is an instance of ProjectCharge.
*/
@Prop({default: () => new ProjectCharge()})
private readonly item: ProjectCharge;
/**
* projectName returns project name
* HOURS_IN_MONTH constant shows amount of hours in 30-day month.
*/
private readonly HOURS_IN_MONTH: number = 720;
/**
* projectName returns project name.
*/
public get projectName(): string {
const projects: Project[] = this.$store.state.projectsModule.projects;
@ -56,6 +78,40 @@ export default class UsageChargeItem extends Vue {
return project ? project.name : '';
}
/**
* Returns string of date range.
*/
public get period(): string {
const months: string[] = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const since: string = `${months[this.item.since.getUTCMonth()]} ${this.item.since.getUTCDate()}`;
const before: string = `${months[this.item.before.getUTCMonth()]} ${this.item.before.getUTCDate()}`;
return `${since} - ${before}`;
}
/**
* Returns string of egress amount and dimension.
*/
public get egressAmountAndDimension(): string {
return `${this.egressFormatted.formattedBytes} ${this.egressFormatted.label}`;
}
/**
* Returns formatted storage used in GB x month dimension.
*/
public get storageFormatted(): string {
const bytesInGB: number = 1000000000;
return (this.item.storage / this.HOURS_IN_MONTH / bytesInGB).toFixed(2);
}
/**
* Returns formatted object count in object x month dimension.
*/
public get objectCountFormatted(): string {
return (this.item.objectCount / this.HOURS_IN_MONTH).toFixed(2);
}
/**
* isDetailedInfoShown indicates if area with detailed information about project charges is expanded.
*/
@ -67,19 +123,32 @@ export default class UsageChargeItem extends Vue {
public toggleDetailedInfo(): void {
this.isDetailedInfoShown = !this.isDetailedInfoShown;
}
/**
* Returns formatted egress depending on amount of bytes.
*/
private get egressFormatted(): Size {
return new Size(this.item.egress, 2);
}
}
</script>
<style scoped lang="scss">
p {
margin: 0;
}
.usage-charge-item-container {
font-size: 16px;
line-height: 21px;
padding: 20px 0;
font-family: 'font_regular', sans-serif;
&__summary {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
&__name-container {
display: flex;
@ -100,22 +169,68 @@ export default class UsageChargeItem extends Vue {
justify-content: space-between;
padding: 16px 0 0 26px;
&__item {
&__info-header {
display: flex;
justify-content: space-between;
align-items: center;
justify-content: space-between;
font-size: 14px;
line-height: 19px;
color: #adadad;
border-bottom: 1px solid #b9b9b9;
height: 25px;
width: 100%;
}
&__content-area {
width: 100%;
padding: 10px 0;
border-bottom: 1px solid #b9b9b9;
display: flex;
align-items: center;
justify-content: space-between;
&__resource-container,
&__period-container,
&__cost-container,
&__usage-container {
width: 20%;
font-size: 14px;
line-height: 19px;
color: #354049;
:nth-child(1),
:nth-child(2) {
margin-bottom: 3px;
}
}
&__resource-container {
width: 40%;
}
}
&__summary {
width: 100%;
font-family: 'font_regular', sans-serif;
font-size: 14px;
line-height: 18px;
color: #6f7a83;
margin-top: 5px;
text-align: right;
margin-top: 13px;
}
}
}
.small-font-size {
font-size: 14px;
line-height: 18px;
.resource-header {
width: 40%;
}
.cost-header,
.period-header,
.usage-header {
width: 20%;
}
.cost-header,
.price {
text-align: right;
}
</style>

View File

@ -152,7 +152,7 @@ export default class NewProjectPopup extends Vue {
await this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_NEW_PROJ);
this.checkIfsFirstProject();
this.checkIfUsersFirstProject();
this.isLoading = false;
}
@ -191,10 +191,14 @@ export default class NewProjectPopup extends Vue {
this.$emit('hideNewProjectButton');
}
private checkIfsFirstProject(): void {
const isFirstProject = this.$store.state.projectsModule.projects.length === 1;
/**
* Indicates if user created his first project.
*/
private checkIfUsersFirstProject(): void {
const usersProjects: Project[] = this.$store.state.projectsModule.projects.filter((project: Project) => project.ownerId === this.$store.getters.user.id);
const isUsersFirstProject = usersProjects.length === 1;
isFirstProject
isUsersFirstProject
? this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SUCCESSFUL_PROJECT_CREATION_POPUP)
: this.notifySuccess('Project created successfully!');
}

View File

@ -2,11 +2,14 @@
// See LICENSE for copying information.
import { StoreModule } from '@/store';
import { UsageState } from '@/store/modules/usage';
import { BillingHistoryItem, CreditCard, PaymentsApi, ProjectCharge, TokenDeposit } from '@/types/payments';
import { DateRange } from '@/types/usage';
const PAYMENTS_MUTATIONS = {
SET_BALANCE: 'SET_BALANCE',
SET_CREDIT_CARDS: 'SET_CREDIT_CARDS',
SET_DATE: 'SET_DATE',
CLEAR: 'CLEAR_PAYMENT_INFO',
UPDATE_CARDS_SELECTION: 'UPDATE_CARDS_SELECTION',
UPDATE_CARDS_DEFAULT: 'UPDATE_CARDS_DEFAULT',
@ -27,11 +30,14 @@ export const PAYMENTS_ACTIONS = {
GET_BILLING_HISTORY: 'getBillingHistory',
MAKE_TOKEN_DEPOSIT: 'makeTokenDeposit',
GET_PROJECT_CHARGES: 'getProjectCharges',
GET_PROJECT_CHARGES_CURRENT_ROLLUP: 'getProjectChargesCurrentRollup',
GET_PROJECT_CHARGES_PREVIOUS_ROLLUP: 'getProjectChargesPreviousRollup',
};
const {
SET_BALANCE,
SET_CREDIT_CARDS,
SET_DATE,
CLEAR,
UPDATE_CARDS_SELECTION,
UPDATE_CARDS_DEFAULT,
@ -52,6 +58,8 @@ const {
GET_BILLING_HISTORY,
MAKE_TOKEN_DEPOSIT,
GET_PROJECT_CHARGES,
GET_PROJECT_CHARGES_CURRENT_ROLLUP,
GET_PROJECT_CHARGES_PREVIOUS_ROLLUP,
} = PAYMENTS_ACTIONS;
export class PaymentsState {
@ -62,6 +70,8 @@ export class PaymentsState {
public creditCards: CreditCard[] = [];
public billingHistory: BillingHistoryItem[] = [];
public charges: ProjectCharge[] = [];
public startDate: Date = new Date();
public endDate: Date = new Date();
}
/**
@ -79,6 +89,10 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
[SET_CREDIT_CARDS](state: PaymentsState, creditCards: CreditCard[]): void {
state.creditCards = creditCards;
},
[SET_DATE](state: UsageState, dateRange: DateRange) {
state.startDate = dateRange.startDate;
state.endDate = dateRange.endDate;
},
[UPDATE_CARDS_SELECTION](state: PaymentsState, id: string | null): void {
state.creditCards = state.creditCards.map(card => {
if (card.id === id) {
@ -164,9 +178,40 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
[MAKE_TOKEN_DEPOSIT]: async function({commit}: any, amount: number): Promise<TokenDeposit> {
return await api.makeTokenDeposit(amount);
},
[GET_PROJECT_CHARGES]: async function({commit}: any): Promise<void> {
const charges: ProjectCharge[] = await api.projectsCharges();
[GET_PROJECT_CHARGES]: async function({commit}: any, dateRange: DateRange): Promise<void> {
const now = new Date();
let beforeUTC = new Date(Date.UTC(dateRange.endDate.getFullYear(), dateRange.endDate.getMonth(), dateRange.endDate.getDate(), 23, 59));
if (now.getUTCFullYear() === dateRange.endDate.getUTCFullYear() &&
now.getUTCMonth() === dateRange.endDate.getUTCMonth() &&
now.getUTCDate() <= dateRange.endDate.getUTCDate()) {
beforeUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getMinutes()));
}
const sinceUTC = new Date(Date.UTC(dateRange.startDate.getFullYear(), dateRange.startDate.getMonth(), dateRange.startDate.getDate()));
const charges: ProjectCharge[] = await api.projectsCharges(sinceUTC, beforeUTC);
commit(SET_DATE, dateRange);
commit(SET_PROJECT_CHARGES, charges);
},
[GET_PROJECT_CHARGES_CURRENT_ROLLUP]: async function({commit}: any): Promise<void> {
const now = new Date();
const endUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getMinutes()));
const startUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
const charges: ProjectCharge[] = await api.projectsCharges(startUTC, endUTC);
commit(SET_DATE, new DateRange(startUTC, endUTC));
commit(SET_PROJECT_CHARGES, charges);
},
[GET_PROJECT_CHARGES_PREVIOUS_ROLLUP]: async function({commit}: any): Promise<void> {
const now = new Date();
const startUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1));
const endUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 0, 23, 59, 59));
const charges: ProjectCharge[] = await api.projectsCharges(startUTC, endUTC);
commit(SET_DATE, new DateRange(startUTC, endUTC));
commit(SET_PROJECT_CHARGES, charges);
},
},

View File

@ -23,7 +23,7 @@ export interface PaymentsApi {
/**
* projectsCharges returns how much money current user will be charged for each project which he owns.
*/
projectsCharges(): Promise<ProjectCharge[]>;
projectsCharges(since: Date, before: Date): Promise<ProjectCharge[]>;
/**
* Add credit card
@ -171,18 +171,23 @@ class Amount {
*/
export class ProjectCharge {
public constructor(
public since: Date = new Date(),
public before: Date = new Date(),
public egress: number = 0,
public storage: number = 0,
public objectCount: number = 0,
public projectId: string = '',
// storage shows how much cents we should pay for storing GB*Hrs.
public storage: number = 0,
public storagePrice: number = 0,
// egress shows how many cents we should pay for Egress.
public egress: number = 0,
public egressPrice: number = 0,
// objectCount shows how many cents we should pay for objects count.
public objectCount: number = 0) {}
public objectPrice: number = 0) {}
/**
* summary returns total price for a project in cents.
*/
public summary(): number {
return this.storage + this.egress + this.objectCount;
return this.storagePrice + this.egressPrice + this.objectPrice;
}
}

View File

@ -56,7 +56,7 @@ const {
GET_BALANCE,
GET_CREDIT_CARDS,
GET_BILLING_HISTORY,
GET_PROJECT_CHARGES,
GET_PROJECT_CHARGES_CURRENT_ROLLUP,
} = PAYMENTS_ACTIONS;
@Component({
@ -96,7 +96,7 @@ export default class DashboardArea extends Vue {
balance = await this.$store.dispatch(GET_BALANCE);
creditCards = await this.$store.dispatch(GET_CREDIT_CARDS);
await this.$store.dispatch(GET_BILLING_HISTORY);
await this.$store.dispatch(GET_PROJECT_CHARGES);
await this.$store.dispatch(GET_PROJECT_CHARGES_CURRENT_ROLLUP);
} catch (error) {
await this.$notify.error(error.message);
}

View File

@ -0,0 +1,61 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
import Vuex from 'vuex';
import UsageChargeItem from '@/components/account/billing/monthlySummary/UsageChargeItem.vue';
import { makePaymentsModule } from '@/store/modules/payments';
import { makeProjectsModule } from '@/store/modules/projects';
import { ProjectCharge } from '@/types/payments';
import { Project } from '@/types/projects';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { PaymentsMock } from '../../../mock/api/payments';
import { ProjectsApiMock } from '../../../mock/api/projects';
const localVue = createLocalVue();
localVue.filter('centsToDollars', (cents: number): string => {
return `USD $${(cents / 100).toFixed(2)}`;
});
localVue.use(Vuex);
const projectsApi = new ProjectsApiMock();
const projectsModule = makeProjectsModule(projectsApi);
const paymentsApi = new PaymentsMock();
const paymentsModule = makePaymentsModule(paymentsApi);
const store = new Vuex.Store({ modules: { projectsModule, paymentsModule }});
describe('UsageChargeItem', () => {
const project = new Project('id', 'projectName', 'projectDescription', 'test', 'testOwnerId', true);
projectsApi.setMockProjects([project]);
const date = new Date(Date.UTC(1970, 1, 1));
const projectCharge = new ProjectCharge(date, date, 100, 100, 100, 'id', 100, 100, 100);
it('renders correctly', () => {
const wrapper = shallowMount(UsageChargeItem, {
store,
localVue,
});
expect(wrapper).toMatchSnapshot();
});
it('toggling dropdown works correctly', async () => {
const wrapper = shallowMount(UsageChargeItem, {
store,
localVue,
propsData: {
item: projectCharge,
},
});
await wrapper.find('.usage-charge-item-container__summary').trigger('click');
expect(wrapper).toMatchSnapshot();
await wrapper.find('.usage-charge-item-container__summary').trigger('click');
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -0,0 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UsageChargeItem renders correctly 1`] = `
<div class="usage-charge-item-container">
<div class="usage-charge-item-container__summary">
<div class="usage-charge-item-container__summary__name-container"><svg width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="usage-charge-item-container__summary__name-container__expand-image">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.328889 13.6272C-0.10963 13.1302 -0.10963 12.3243 0.328889 11.8273L4.58792 7L0.328889 2.17268C-0.10963 1.67565 -0.10963 0.869804 0.328889 0.372774C0.767408 -0.124258 1.47839 -0.124258 1.91691 0.372774L7.76396 7L1.91691 13.6272C1.47839 14.1243 0.767409 14.1243 0.328889 13.6272Z" fill="#2683FF"></path>
</svg>
<!----> <span></span></div>
</div>
<!---->
</div>
`;
exports[`UsageChargeItem toggling dropdown works correctly 1`] = `
<div class="usage-charge-item-container">
<div class="usage-charge-item-container__summary">
<div class="usage-charge-item-container__summary__name-container">
<!----> <svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg" class="usage-charge-item-container__summary__name-container__expand-image">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.372773 0.338888C0.869804 -0.112963 1.67565 -0.112963 2.17268 0.338888L7 4.72741L11.8273 0.338888C12.3243 -0.112963 13.1302 -0.112963 13.6272 0.338888C14.1243 0.790739 14.1243 1.52333 13.6272 1.97519L7 8L0.372773 1.97519C-0.124258 1.52333 -0.124258 0.790739 0.372773 0.338888Z" fill="#2683FF"></path>
</svg> <span></span></div>
</div>
<div class="usage-charge-item-container__detailed-info-container">
<div class="usage-charge-item-container__detailed-info-container__info-header"><span class="resource-header">RESOURCE</span> <span class="period-header">PERIOD</span> <span class="usage-header">USAGE</span> <span class="cost-header">COST</span></div>
<div class="usage-charge-item-container__detailed-info-container__content-area">
<div class="usage-charge-item-container__detailed-info-container__content-area__resource-container">
<p>Storage ($0.010 per Gigabyte-Month)</p>
<p>Egress ($0.045 per GB)</p>
<p>Objects ($0.0000022 per Object-Month)</p>
</div>
<div class="usage-charge-item-container__detailed-info-container__content-area__period-container">
<p>Feb 1 - Feb 1</p>
<p>Feb 1 - Feb 1</p>
<p>Feb 1 - Feb 1</p>
</div>
<div class="usage-charge-item-container__detailed-info-container__content-area__usage-container">
<p>0.00 Gigabyte-month</p>
<p>0.10 KB</p>
<p>0.14 Object-month</p>
</div>
<div class="usage-charge-item-container__detailed-info-container__content-area__cost-container">
<p class="price">USD $1.00</p>
<p class="price">USD $1.00</p>
<p class="price">USD $1.00</p>
</div>
</div> <span class="usage-charge-item-container__detailed-info-container__summary">USD $3.00</span>
</div>
</div>
`;
exports[`UsageChargeItem toggling dropdown works correctly 2`] = `
<div class="usage-charge-item-container">
<div class="usage-charge-item-container__summary">
<div class="usage-charge-item-container__summary__name-container"><svg width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="usage-charge-item-container__summary__name-container__expand-image">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.328889 13.6272C-0.10963 13.1302 -0.10963 12.3243 0.328889 11.8273L4.58792 7L0.328889 2.17268C-0.10963 1.67565 -0.10963 0.869804 0.328889 0.372774C0.767408 -0.124258 1.47839 -0.124258 1.91691 0.372774L7.76396 7L1.91691 13.6272C1.47839 14.1243 0.767409 14.1243 0.328889 13.6272Z" fill="#2683FF"></path>
</svg>
<!----> <span></span></div>
</div>
<!---->
</div>
`;