satellite/payments: project charges api extended to show usage and period
Change-Id: I471def779d8b2a896fc43a692029233a2cd839b0
This commit is contained in:
parent
4abbf3198d
commit
56c33f5193
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
@ -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(),
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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!');
|
||||
}
|
||||
|
@ -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);
|
||||
},
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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>
|
||||
`;
|
Loading…
Reference in New Issue
Block a user