web/satellite: project charges (#3611)

This commit is contained in:
Yehor Butko 2019-11-20 15:46:22 +02:00 committed by GitHub
parent 16f0e998b1
commit 9ca547acb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 164 additions and 42 deletions

View File

@ -23,7 +23,7 @@ var (
mon = monkit.Package()
)
// Payments is an api controller that exposes all payment related functionality
// Payments is an api controller that exposes all payment related functionality.
type Payments struct {
log *zap.Logger
service *console.Service
@ -61,6 +61,8 @@ func (p *Payments) AccountBalance(w http.ResponseWriter, r *http.Request) {
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
balance, err := p.service.Payments().AccountBalance(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
@ -72,7 +74,6 @@ func (p *Payments) AccountBalance(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(&balance)
if err != nil {
p.log.Error("failed to write json balance response", zap.Error(ErrPaymentsAPI.Wrap(err)))
@ -85,6 +86,8 @@ func (p *Payments) ProjectsCharges(w http.ResponseWriter, r *http.Request) {
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
charges, err := p.service.Payments().ProjectsCharges(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
@ -134,6 +137,8 @@ func (p *Payments) ListCreditCards(w http.ResponseWriter, r *http.Request) {
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
cards, err := p.service.Payments().ListCreditCards(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
@ -145,7 +150,6 @@ func (p *Payments) ListCreditCards(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(cards)
if err != nil {
p.log.Error("failed to write json list cards response", zap.Error(ErrPaymentsAPI.Wrap(err)))
@ -208,6 +212,8 @@ func (p *Payments) BillingHistory(w http.ResponseWriter, r *http.Request) {
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
billingHistory, err := p.service.Payments().BillingHistory(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
@ -219,7 +225,6 @@ func (p *Payments) BillingHistory(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(billingHistory)
if err != nil {
p.log.Error("failed to write json billing history response", zap.Error(ErrPaymentsAPI.Wrap(err)))
@ -233,6 +238,8 @@ func (p *Payments) TokenDeposit(w http.ResponseWriter, r *http.Request) {
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
var requestData struct {
Amount string `json:"amount"`
}

View File

@ -11,9 +11,9 @@ import (
type ProjectCharge struct {
ProjectID uuid.UUID `json:"projectId"`
// StorageGbHrs shows how much cents we should pay for storing GB*Hrs.
StorageGbHrs int64
StorageGbHrs int64 `json:"storage"`
// Egress shows how many cents we should pay for Egress.
Egress int64
Egress int64 `json:"egress"`
// ObjectCount shows how many cents we should pay for objects count.
ObjectCount int64
ObjectCount int64 `json:"objectCount"`
}

View File

@ -95,10 +95,10 @@ func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID)
charges = append(charges, payments.ProjectCharge{
ProjectID: project.ID,
Egress: usage.Egress / int64(memory.TB) * accounts.service.EgressPrice,
// TODO: check precision
ObjectCount: int64(usage.ObjectCount) * accounts.service.PerObjectPrice,
StorageGbHrs: int64(usage.Storage) / int64(memory.TB) * accounts.service.TBhPrice,
Egress: usage.Egress * accounts.service.EgressPrice / int64(memory.TB),
ObjectCount: int64(usage.ObjectCount * float64(accounts.service.PerObjectPrice)),
StorageGbHrs: int64(usage.Storage*float64(accounts.service.TBhPrice)) / int64(memory.TB),
})
}

View File

@ -2,7 +2,7 @@
// See LICENSE for copying information.
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { BillingHistoryItem, CreditCard, PaymentsApi } from '@/types/payments';
import { BillingHistoryItem, CreditCard, PaymentsApi, ProjectCharge } from '@/types/payments';
import { HttpClient } from '@/utils/httpClient';
/**
@ -54,7 +54,10 @@ export class PaymentsHttpApi implements PaymentsApi {
throw new Error('can not setup account');
}
public async projectsCharges(): Promise<any> {
/**
* 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`;
const response = await this.client.get(path);
@ -66,8 +69,16 @@ export class PaymentsHttpApi implements PaymentsApi {
throw new Error('can not get projects charges');
}
// TODO: fiish mapping
const charges = await response.json();
if (charges) {
return charges.map(charge =>
new ProjectCharge(
charge.projectId,
charge.storage,
charge.egress,
charge.objectCount),
);
}
return [];
}

View File

@ -6,15 +6,7 @@
<div class="current-month-area__header">
<div class="current-month-area__header__month-info">
<h1 class="current-month-area__header__month-info__title">Current Month</h1>
<h2 class="current-month-area__header__month-info__title-info">{{currentPeriod}}</h2>
</div>
<div class="current-month-area__header__usage-info" v-if="false">
<span class="current-month-area__header__usage-info__data">Usage <b class="current-month-area__header__usage-info__data__bold-text">$12.44</b></span>
<VButton
label="Earn Credits"
width="153px"
height="48px"
/>
<h2 class="current-month-area__header__month-info__title-info">{{ currentPeriod }}</h2>
</div>
</div>
<div class="current-month-area__content">
@ -22,18 +14,25 @@
<div class="current-month-area__content__usage-charges" @click="toggleUsageChargesPopup">
<div class="current-month-area__content__usage-charges__head">
<div class="current-month-area__content__usage-charges__head__name">
<svg class="current-month-area__content__usage-charges__head__name__image" v-if="!areUsageChargesShown" width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
</svg>
<svg class="current-month-area__content__usage-charges__head__name__image" v-if="areUsageChargesShown" width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
</svg>
<span>Usage Charges</span>
<div class="current-month-area__content__usage-charges__head__name__image-container">
<svg class="current-month-area__content__usage-charges__head__name__image-container__image" v-if="!areUsageChargesShown" width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
</svg>
<svg class="current-month-area__content__usage-charges__head__name__image-container__image" v-if="areUsageChargesShown" width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
</svg>
</div>
<span class="current-month-area__content__usage-charges__head__name__title">Usage Charges</span>
</div>
<span>Estimated total $82.44</span>
<span>Estimated total {{ chargesSummary | centsToDollars }}</span>
</div>
<div class="current-month-area__content__usage-charges__content" v-if="areUsageChargesShown" @click.stop>
<UsageChargeItem class="item"></UsageChargeItem>
<UsageChargeItem
v-for="usageCharge in usageCharges"
:item="usageCharge"
:key="usageCharge.projectId"
class="item"
/>
</div>
</div>
</div>
@ -46,6 +45,8 @@ import { Component, Vue } from 'vue-property-decorator';
import UsageChargeItem from '@/components/account/billing/monthlySummary/UsageChargeItem.vue';
import VButton from '@/components/common/VButton.vue';
import { ProjectCharge } from '@/types/payments';
@Component({
components: {
VButton,
@ -53,8 +54,17 @@ import VButton from '@/components/common/VButton.vue';
},
})
export default class MonthlyBillingSummary extends Vue {
/**
* areUsageChargesShown indicates if area with all projects is expanded.
*/
private areUsageChargesShown: boolean = false;
/**
* usageCharges is an array of all ProjectCharges.
*/
public usageCharges: ProjectCharge[] = this.$store.state.paymentsModule.charges;
// TODO: unused.
public get currentPeriod(): string {
const months: string[] = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const now: Date = new Date();
@ -69,6 +79,18 @@ export default class MonthlyBillingSummary extends Vue {
return `${months[monthNumber]} 1 - ${date} ${year}`;
}
/**
* chargesSummary returns summary of all projects.
*/
public get chargesSummary(): number {
const usageItemSummaries = this.usageCharges.map(item => item.summary());
return usageItemSummaries.reduce((accumulator, current) => accumulator + current);
}
/**
* toggleUsageChargesPopup is used to open/close area with list of project charges.
*/
public toggleUsageChargesPopup(): void {
this.areUsageChargesShown = !this.areUsageChargesShown;
}
@ -102,6 +124,7 @@ export default class MonthlyBillingSummary extends Vue {
font-family: 'font_bold', sans-serif;
font-size: 32px;
line-height: 48px;
user-select: none;
}
&__title-info {
@ -132,6 +155,7 @@ export default class MonthlyBillingSummary extends Vue {
font-size: 14px;
line-height: 21px;
color: #afb7c1;
user-select: none;
}
&__usage-charges {
@ -151,9 +175,20 @@ export default class MonthlyBillingSummary extends Vue {
display: flex;
align-items: center;
&__image {
&__image-container {
max-width: 14px;
max-height: 14px;
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
}
&__title {
user-select: none;
}
}
}

View File

@ -11,34 +11,61 @@
<svg class="usage-charge-item-container__summary__name-container__expand-image" v-if="isDetailedInfoShown" width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
</svg>
<span>Project 2</span>
<span>{{ projectName }}</span>
</div>
<span class="small-font-size">$12.88</span>
<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>$18.00</span>
<span>{{ item.storage | centsToDollars }}</span>
</div>
<div class="usage-charge-item-container__detailed-info-container__item">
<span>Egress</span>
<span>$2.00</span>
<span>{{ item.egress | centsToDollars }}</span>
</div>
<div class="usage-charge-item-container__detailed-info-container__item">
<span>Objects</span>
<span>$1.22</span>
<span>{{ item.objectCount | centsToDollars }}</span>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { ProjectCharge } from '@/types/payments';
import { Project } from '@/types/projects';
@Component
export default class UsageChargeItem extends Vue {
/**
* item is an instance of ProjectCharge
*/
@Prop({default: () => new ProjectCharge()})
private readonly item: ProjectCharge;
/**
* projectName returns project name
*/
public get projectName(): string {
const projects: Project[] = this.$store.state.projectsModule.projects;
const project: Project | undefined = projects.find(project => project.id === this.item.projectId);
if (!project) return '';
return project.name;
}
/**
* isDetailedInfoShown indicates if area with detailed information about project charges is expanded.
*/
public isDetailedInfoShown: boolean = false;
/**
* toggleDetailedInfo expands an area with detailed information about project charges.
*/
public toggleDetailedInfo(): void {
this.isDetailedInfoShown = !this.isDetailedInfoShown;
}

View File

@ -39,6 +39,13 @@ Vue.directive('click-outside', {
},
});
/**
* centsToDollars is a Vue filter that converts amount of cents in dollars string.
*/
Vue.filter('centsToDollars', (cents: number): string => {
return `$${(cents / 100).toFixed(2)}`;
});
new Vue({
router,
store,

View File

@ -2,7 +2,7 @@
// See LICENSE for copying information.
import { StoreModule } from '@/store';
import { BillingHistoryItem, CreditCard, PaymentsApi } from '@/types/payments';
import { BillingHistoryItem, CreditCard, PaymentsApi, ProjectCharge } from '@/types/payments';
const PAYMENTS_MUTATIONS = {
SET_BALANCE: 'SET_BALANCE',
@ -11,6 +11,7 @@ const PAYMENTS_MUTATIONS = {
UPDATE_CARDS_SELECTION: 'UPDATE_CARDS_SELECTION',
UPDATE_CARDS_DEFAULT: 'UPDATE_CARDS_DEFAULT',
SET_BILLING_HISTORY: 'SET_BILLING_HISTORY',
SET_PROJECT_CHARGES: 'SET_PROJECT_CHARGES',
};
export const PAYMENTS_ACTIONS = {
@ -24,6 +25,7 @@ export const PAYMENTS_ACTIONS = {
MAKE_CARD_DEFAULT: 'makeCardDefault',
REMOVE_CARD: 'removeCard',
GET_BILLING_HISTORY: 'getBillingHistory',
GET_PROJECT_CHARGES: 'getProjectCharges',
};
const {
@ -33,6 +35,7 @@ const {
UPDATE_CARDS_SELECTION,
UPDATE_CARDS_DEFAULT,
SET_BILLING_HISTORY,
SET_PROJECT_CHARGES,
} = PAYMENTS_MUTATIONS;
const {
@ -46,6 +49,7 @@ const {
MAKE_CARD_DEFAULT,
REMOVE_CARD,
GET_BILLING_HISTORY,
GET_PROJECT_CHARGES,
} = PAYMENTS_ACTIONS;
export class PaymentsState {
@ -55,6 +59,7 @@ export class PaymentsState {
public balance: number = 0;
public creditCards: CreditCard[] = [];
public billingHistory: BillingHistoryItem[] = [];
public charges: ProjectCharge[] = [];
}
/**
@ -101,6 +106,9 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
[SET_BILLING_HISTORY](state: PaymentsState, billingHistory: BillingHistoryItem[]): void {
state.billingHistory = billingHistory;
},
[SET_PROJECT_CHARGES](state: PaymentsState, charges: ProjectCharge[]): void {
state.charges = charges;
},
[CLEAR](state: PaymentsState) {
state.balance = 0;
state.creditCards = [];
@ -149,6 +157,11 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
commit(SET_BILLING_HISTORY, billingHistory);
},
[GET_PROJECT_CHARGES]: async function({commit}: any): Promise<void> {
const charges: ProjectCharge[] = await api.projectsCharges();
commit(SET_PROJECT_CHARGES, charges);
},
},
};
}

View File

@ -21,9 +21,9 @@ export interface PaymentsApi {
getBalance(): Promise<number>;
/**
*
* projectsCharges returns how much money current user will be charged for each project which he owns.
*/
projectsCharges(): Promise<any>;
projectsCharges(): Promise<ProjectCharge[]>;
/**
* Add credit card
@ -122,3 +122,24 @@ export enum BillingHistoryItemType {
// Transaction is a Coinpayments transaction billing item.
Transaction = 1,
}
/**
* ProjectCharge shows how much money current project will charge in the end of the month.
*/
export class ProjectCharge {
public constructor(
public projectId: string = '',
// storage shows how much cents we should pay for storing GB*Hrs.
public storage: number = 0,
// egress shows how many cents we should pay for Egress.
public egress: number = 0,
// objectCount shows how many cents we should pay for objects count.
public objectCount: number = 0) {}
/**
* summary returns total price for a project in cents.
*/
public summary(): number {
return this.storage + this.egress + this.objectCount;
}
}

View File

@ -48,6 +48,7 @@ const {
GET_BALANCE,
GET_CREDIT_CARDS,
GET_BILLING_HISTORY,
GET_PROJECT_CHARGES,
} = PAYMENTS_ACTIONS;
@Component({
@ -75,7 +76,7 @@ export default class DashboardArea extends Vue {
await this.$store.dispatch(GET_BALANCE);
await this.$store.dispatch(GET_CREDIT_CARDS);
await this.$store.dispatch(GET_BILLING_HISTORY);
new PaymentsHttpApi().projectsCharges();
await this.$store.dispatch(GET_PROJECT_CHARGES);
} catch (error) {
if (error instanceof ErrorUnauthorized) {
AuthToken.remove();