web/satellite: show overridden usage prices in the satellite UI
This change allows users who register with a partner that has different project usage prices to see the correct prices in the satellite UI. Resolves storj/storj-private#90 Change-Id: I06bde50db474b25396671a27e282ef5637efe85b
This commit is contained in:
parent
5d656e66bf
commit
6142b1cd12
@ -646,7 +646,6 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
|
||||
accountFreezeService,
|
||||
peer.Console.Listener,
|
||||
config.Payments.StripeCoinPayments.StripePublicKey,
|
||||
config.Payments.UsagePrice,
|
||||
peer.URL(),
|
||||
)
|
||||
|
||||
|
@ -432,6 +432,30 @@ func (p *Payments) WalletPayments(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetProjectUsagePriceModel returns the project usage price model for the user.
|
||||
func (p *Payments) GetProjectUsagePriceModel(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
pricing, err := p.service.Payments().GetProjectUsagePriceModel(ctx)
|
||||
if err != nil {
|
||||
if console.ErrUnauthorized.Has(err) {
|
||||
p.serveJSONError(w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
p.serveJSONError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.NewEncoder(w).Encode(pricing); err != nil {
|
||||
p.log.Error("failed to encode project usage price model", zap.Error(ErrPaymentsAPI.Wrap(err)))
|
||||
}
|
||||
}
|
||||
|
||||
// serveJSONError writes JSON error to response output stream.
|
||||
func (p *Payments) serveJSONError(w http.ResponseWriter, status int, err error) {
|
||||
web.ServeJSONError(p.log, w, status, err)
|
||||
|
@ -42,7 +42,6 @@ import (
|
||||
"storj.io/storj/satellite/console/consoleweb/consolewebauth"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
"storj.io/storj/satellite/oidc"
|
||||
"storj.io/storj/satellite/payments/paymentsconfig"
|
||||
"storj.io/storj/satellite/rewards"
|
||||
)
|
||||
|
||||
@ -134,8 +133,6 @@ type Server struct {
|
||||
|
||||
stripePublicKey string
|
||||
|
||||
usagePrice paymentsconfig.ProjectUsagePrice
|
||||
|
||||
schema graphql.Schema
|
||||
|
||||
templatesCache *templates
|
||||
@ -205,7 +202,7 @@ func (a *apiAuth) RemoveAuthCookie(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
// NewServer creates new instance of console server.
|
||||
func NewServer(logger *zap.Logger, config Config, service *console.Service, oidcService *oidc.Service, mailService *mailservice.Service, partners *rewards.PartnersService, analytics *analytics.Service, abTesting *abtesting.Service, accountFreezeService *console.AccountFreezeService, listener net.Listener, stripePublicKey string, usagePrice paymentsconfig.ProjectUsagePrice, nodeURL storj.NodeURL) *Server {
|
||||
func NewServer(logger *zap.Logger, config Config, service *console.Service, oidcService *oidc.Service, mailService *mailservice.Service, partners *rewards.PartnersService, analytics *analytics.Service, abTesting *abtesting.Service, accountFreezeService *console.AccountFreezeService, listener net.Listener, stripePublicKey string, nodeURL storj.NodeURL) *Server {
|
||||
server := Server{
|
||||
log: logger,
|
||||
config: config,
|
||||
@ -219,7 +216,6 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
|
||||
ipRateLimiter: web.NewIPRateLimiter(config.RateLimit, logger),
|
||||
userIDRateLimiter: NewUserIDRateLimiter(config.RateLimit, logger),
|
||||
nodeURL: nodeURL,
|
||||
usagePrice: usagePrice,
|
||||
}
|
||||
|
||||
logger.Debug("Starting Satellite UI.", zap.Stringer("Address", server.listener.Addr()))
|
||||
@ -320,6 +316,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
|
||||
paymentsRouter.HandleFunc("/billing-history", paymentController.BillingHistory).Methods(http.MethodGet)
|
||||
paymentsRouter.Handle("/coupon/apply", server.userIDRateLimiter.Limit(http.HandlerFunc(paymentController.ApplyCouponCode))).Methods(http.MethodPatch)
|
||||
paymentsRouter.HandleFunc("/coupon", paymentController.GetCoupon).Methods(http.MethodGet)
|
||||
paymentsRouter.HandleFunc("/pricing", paymentController.GetProjectUsagePriceModel).Methods(http.MethodGet)
|
||||
|
||||
bucketsController := consoleapi.NewBuckets(logger, service)
|
||||
bucketsRouter := router.PathPrefix("/api/v0/buckets").Subrouter()
|
||||
@ -452,9 +449,6 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
|
||||
FileBrowserFlowDisabled bool
|
||||
LinksharingURL string
|
||||
PathwayOverviewEnabled bool
|
||||
StorageTBPrice string
|
||||
EgressTBPrice string
|
||||
SegmentPrice string
|
||||
RegistrationRecaptchaEnabled bool
|
||||
RegistrationRecaptchaSiteKey string
|
||||
RegistrationHcaptchaEnabled bool
|
||||
@ -499,9 +493,6 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data.PathwayOverviewEnabled = server.config.PathwayOverviewEnabled
|
||||
data.DefaultPaidStorageLimit = server.config.UsageLimits.Storage.Paid
|
||||
data.DefaultPaidBandwidthLimit = server.config.UsageLimits.Bandwidth.Paid
|
||||
data.StorageTBPrice = server.usagePrice.StorageTB
|
||||
data.EgressTBPrice = server.usagePrice.EgressTB
|
||||
data.SegmentPrice = server.usagePrice.Segment
|
||||
data.RegistrationRecaptchaEnabled = server.config.Captcha.Registration.Recaptcha.Enabled
|
||||
data.RegistrationRecaptchaSiteKey = server.config.Captcha.Registration.Recaptcha.SiteKey
|
||||
data.RegistrationHcaptchaEnabled = server.config.Captcha.Registration.Hcaptcha.Enabled
|
||||
|
@ -3043,6 +3043,19 @@ func (payment Payments) WalletPayments(ctx context.Context) (_ WalletPayments, e
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetProjectUsagePriceModel returns the project usage price model for the user.
|
||||
func (payment Payments) GetProjectUsagePriceModel(ctx context.Context) (_ *payments.ProjectUsagePriceModel, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
user, err := GetUser(ctx)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
model := payment.service.accounts.GetProjectUsagePriceModel(user.UserAgent)
|
||||
return &model, nil
|
||||
}
|
||||
|
||||
func findMembershipByProjectID(memberships []ProjectMember, projectID uuid.UUID) (ProjectMember, bool) {
|
||||
for _, membership := range memberships {
|
||||
if membership.ProjectID == projectID {
|
||||
|
@ -20,9 +20,6 @@
|
||||
<meta name="coupon-code-signup-ui-enabled" content="{{ .CouponCodeSignupUIEnabled }}">
|
||||
<meta name="file-browser-flow-disabled" content="{{ .FileBrowserFlowDisabled }}">
|
||||
<meta name="linksharing-url" content="{{ .LinksharingURL }}">
|
||||
<meta name="storage-tb-price" content="{{ .StorageTBPrice }}">
|
||||
<meta name="egress-tb-price" content="{{ .EgressTBPrice }}">
|
||||
<meta name="segment-price" content="{{ .SegmentPrice }}">
|
||||
<meta name="registration-recaptcha-enabled" content="{{ .RegistrationRecaptchaEnabled }}">
|
||||
<meta name="registration-recaptcha-site-key" content="{{ .RegistrationRecaptchaSiteKey }}">
|
||||
<meta name="registration-hcaptcha-enabled" content="{{ .RegistrationHcaptchaEnabled }}">
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
PaymentsApi,
|
||||
PaymentsHistoryItem,
|
||||
ProjectUsageAndCharges,
|
||||
ProjectUsagePriceModel,
|
||||
TokenAmount,
|
||||
NativePaymentHistoryItem,
|
||||
Wallet,
|
||||
@ -97,6 +98,25 @@ export class PaymentsHttpApi implements PaymentsApi {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* projectUsagePriceModel returns usage and how much money current user will be charged for each project which he owns.
|
||||
*/
|
||||
public async projectUsagePriceModel(): Promise<ProjectUsagePriceModel> {
|
||||
const path = `${this.ROOT_PATH}/pricing`;
|
||||
const response = await this.client.get(path);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('cannot get project usage price model');
|
||||
}
|
||||
|
||||
const model = await response.json();
|
||||
if (model) {
|
||||
return new ProjectUsagePriceModel(model.storageMBMonthCents, model.egressMBCents, model.segmentMonthCents);
|
||||
}
|
||||
|
||||
return new ProjectUsagePriceModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add credit card.
|
||||
*
|
||||
|
@ -133,6 +133,7 @@ export default class BillingArea extends Vue {
|
||||
|
||||
try {
|
||||
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP);
|
||||
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_PRICE_MODEL);
|
||||
|
||||
this.isDataFetching = false;
|
||||
} catch (error) {
|
||||
|
@ -61,6 +61,7 @@ export default class EstimatedCostsAndCredits extends Vue {
|
||||
|
||||
try {
|
||||
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP);
|
||||
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_PRICE_MODEL);
|
||||
|
||||
this.isDataFetching = false;
|
||||
} catch (error) {
|
||||
|
@ -49,11 +49,11 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import { ProjectUsageAndCharges } from '@/types/payments';
|
||||
import { ProjectUsageAndCharges, ProjectUsagePriceModel } from '@/types/payments';
|
||||
import { Project } from '@/types/projects';
|
||||
import { Size } from '@/utils/bytesSize';
|
||||
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
|
||||
import { MetaUtils } from '@/utils/meta';
|
||||
import { decimalShift } from '@/utils/strings';
|
||||
import { AnalyticsHttpApi } from '@/api/analytics';
|
||||
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
|
||||
|
||||
@ -83,9 +83,10 @@ export default class UsageAndChargesItem extends Vue {
|
||||
private readonly HOURS_IN_MONTH: number = 720;
|
||||
|
||||
/**
|
||||
* GB_IN_TB constant shows amount of GBs in one TB.
|
||||
* CENTS_MB_TO_DOLLARS_GB_SHIFT constant represents how many places to the left
|
||||
* a decimal point must be shifted to convert from cents/MB to dollars/GB.
|
||||
*/
|
||||
private readonly GB_IN_TB = 1000;
|
||||
private readonly CENTS_MB_TO_DOLLARS_GB_SHIFT = -1;
|
||||
|
||||
/**
|
||||
* projectName returns project name.
|
||||
@ -133,22 +134,29 @@ export default class UsageAndChargesItem extends Vue {
|
||||
/**
|
||||
* Returns storage price per GB.
|
||||
*/
|
||||
public get storagePrice(): number {
|
||||
return parseInt(MetaUtils.getMetaContent('storage-tb-price')) / this.GB_IN_TB;
|
||||
public get storagePrice(): string {
|
||||
return decimalShift(this.priceModel.storageMBMonthCents, this.CENTS_MB_TO_DOLLARS_GB_SHIFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns egress price per GB.
|
||||
*/
|
||||
public get egressPrice(): number {
|
||||
return parseInt(MetaUtils.getMetaContent('egress-tb-price')) / this.GB_IN_TB;
|
||||
public get egressPrice(): string {
|
||||
return decimalShift(this.priceModel.egressMBCents, this.CENTS_MB_TO_DOLLARS_GB_SHIFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns segment price.
|
||||
*/
|
||||
public get segmentPrice(): string {
|
||||
return MetaUtils.getMetaContent('segment-price');
|
||||
return decimalShift(this.priceModel.segmentMonthCents, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns project usage price model from store.
|
||||
*/
|
||||
private get priceModel(): ProjectUsagePriceModel {
|
||||
return this.$store.state.paymentsModule.usagePriceModel;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,11 +54,11 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import { ProjectUsageAndCharges } from '@/types/payments';
|
||||
import { ProjectUsageAndCharges, ProjectUsagePriceModel } from '@/types/payments';
|
||||
import { Project } from '@/types/projects';
|
||||
import { Size } from '@/utils/bytesSize';
|
||||
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
|
||||
import { MetaUtils } from '@/utils/meta';
|
||||
import { decimalShift } from '@/utils/strings';
|
||||
|
||||
import GreyChevron from '@/../static/images/common/greyChevron.svg';
|
||||
|
||||
@ -81,9 +81,10 @@ export default class UsageAndChargesItem2 extends Vue {
|
||||
private readonly HOURS_IN_MONTH: number = 720;
|
||||
|
||||
/**
|
||||
* GB_IN_TB constant shows amount of GBs in one TB.
|
||||
* CENTS_MB_TO_DOLLARS_GB_SHIFT constant represents how many places to the left
|
||||
* a decimal point must be shifted to convert from cents/MB to dollars/GB.
|
||||
*/
|
||||
private readonly GB_IN_TB = 1000;
|
||||
private readonly CENTS_MB_TO_DOLLARS_GB_SHIFT = -1;
|
||||
|
||||
public paymentMethod = 'test';
|
||||
|
||||
@ -133,22 +134,29 @@ export default class UsageAndChargesItem2 extends Vue {
|
||||
/**
|
||||
* Returns storage price per GB.
|
||||
*/
|
||||
public get storagePrice(): number {
|
||||
return parseInt(MetaUtils.getMetaContent('storage-tb-price')) / this.GB_IN_TB;
|
||||
public get storagePrice(): string {
|
||||
return decimalShift(this.priceModel.storageMBMonthCents, this.CENTS_MB_TO_DOLLARS_GB_SHIFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns egress price per GB.
|
||||
*/
|
||||
public get egressPrice(): number {
|
||||
return parseInt(MetaUtils.getMetaContent('egress-tb-price')) / this.GB_IN_TB;
|
||||
public get egressPrice(): string {
|
||||
return decimalShift(this.priceModel.egressMBCents, this.CENTS_MB_TO_DOLLARS_GB_SHIFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns segment price.
|
||||
*/
|
||||
public get segmentPrice(): string {
|
||||
return MetaUtils.getMetaContent('segment-price');
|
||||
return decimalShift(this.priceModel.segmentMonthCents, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns project usage price model from store.
|
||||
*/
|
||||
private get priceModel(): ProjectUsagePriceModel {
|
||||
return this.$store.state.paymentsModule.usagePriceModel;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
PaymentsHistoryItemStatus,
|
||||
PaymentsHistoryItemType,
|
||||
ProjectUsageAndCharges,
|
||||
ProjectUsagePriceModel,
|
||||
NativePaymentHistoryItem,
|
||||
Wallet,
|
||||
} from '@/types/payments';
|
||||
@ -27,6 +28,7 @@ export const PAYMENTS_MUTATIONS = {
|
||||
SET_PAYMENTS_HISTORY: 'SET_PAYMENTS_HISTORY',
|
||||
SET_NATIVE_PAYMENTS_HISTORY: 'SET_NATIVE_PAYMENTS_HISTORY',
|
||||
SET_PROJECT_USAGE_AND_CHARGES: 'SET_PROJECT_USAGE_AND_CHARGES',
|
||||
SET_PROJECT_USAGE_PRICE_MODEL: 'SET_PROJECT_USAGE_PRICE_MODEL',
|
||||
SET_CURRENT_ROLLUP_PRICE: 'SET_CURRENT_ROLLUP_PRICE',
|
||||
SET_PREVIOUS_ROLLUP_PRICE: 'SET_PREVIOUS_ROLLUP_PRICE',
|
||||
SET_PRICE_SUMMARY: 'SET_PRICE_SUMMARY',
|
||||
@ -51,6 +53,7 @@ export const PAYMENTS_ACTIONS = {
|
||||
GET_PROJECT_USAGE_AND_CHARGES: 'getProjectUsageAndCharges',
|
||||
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP: 'getProjectUsageAndChargesCurrentRollup',
|
||||
GET_PROJECT_USAGE_AND_CHARGES_PREVIOUS_ROLLUP: 'getProjectUsageAndChargesPreviousRollup',
|
||||
GET_PROJECT_USAGE_PRICE_MODEL: 'getProjectUsagePriceModel',
|
||||
APPLY_COUPON_CODE: 'applyCouponCode',
|
||||
GET_COUPON: `getCoupon`,
|
||||
};
|
||||
@ -66,6 +69,7 @@ const {
|
||||
SET_PAYMENTS_HISTORY,
|
||||
SET_NATIVE_PAYMENTS_HISTORY,
|
||||
SET_PROJECT_USAGE_AND_CHARGES,
|
||||
SET_PROJECT_USAGE_PRICE_MODEL: SET_PROJECT_USAGE_PRICE_MODEL,
|
||||
SET_PRICE_SUMMARY,
|
||||
SET_PRICE_SUMMARY_FOR_SELECTED_PROJECT,
|
||||
SET_COUPON,
|
||||
@ -87,6 +91,7 @@ const {
|
||||
GET_NATIVE_PAYMENTS_HISTORY,
|
||||
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP,
|
||||
GET_PROJECT_USAGE_AND_CHARGES_PREVIOUS_ROLLUP,
|
||||
GET_PROJECT_USAGE_PRICE_MODEL: GET_PROJECT_USAGE_PRICE_MODEL,
|
||||
APPLY_COUPON_CODE,
|
||||
GET_COUPON,
|
||||
} = PAYMENTS_ACTIONS;
|
||||
@ -100,6 +105,7 @@ export class PaymentsState {
|
||||
public paymentsHistory: PaymentsHistoryItem[] = [];
|
||||
public nativePaymentsHistory: NativePaymentHistoryItem[] = [];
|
||||
public usageAndCharges: ProjectUsageAndCharges[] = [];
|
||||
public usagePriceModel: ProjectUsagePriceModel = new ProjectUsagePriceModel();
|
||||
public priceSummary = 0;
|
||||
public priceSummaryForSelectedProject = 0;
|
||||
public startDate: Date = new Date();
|
||||
@ -175,6 +181,9 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState,
|
||||
[SET_PROJECT_USAGE_AND_CHARGES](state: PaymentsState, usageAndCharges: ProjectUsageAndCharges[]): void {
|
||||
state.usageAndCharges = usageAndCharges;
|
||||
},
|
||||
[SET_PROJECT_USAGE_PRICE_MODEL](state: PaymentsState, model: ProjectUsagePriceModel): void {
|
||||
state.usagePriceModel = model;
|
||||
},
|
||||
[SET_PRICE_SUMMARY](state: PaymentsState, charges: ProjectUsageAndCharges[]): void {
|
||||
if (charges.length === 0) {
|
||||
state.priceSummary = 0;
|
||||
@ -209,6 +218,7 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState,
|
||||
state.paymentsHistory = [];
|
||||
state.nativePaymentsHistory = [];
|
||||
state.usageAndCharges = [];
|
||||
state.usagePriceModel = new ProjectUsagePriceModel();
|
||||
state.priceSummary = 0;
|
||||
state.priceSummaryForSelectedProject = 0;
|
||||
state.startDate = new Date();
|
||||
@ -302,6 +312,10 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState,
|
||||
commit(SET_PROJECT_USAGE_AND_CHARGES, usageAndCharges);
|
||||
commit(SET_PRICE_SUMMARY, usageAndCharges);
|
||||
},
|
||||
[GET_PROJECT_USAGE_PRICE_MODEL]: async function({ commit }: PaymentsContext): Promise<void> {
|
||||
const model: ProjectUsagePriceModel = await api.projectUsagePriceModel();
|
||||
commit(SET_PROJECT_USAGE_PRICE_MODEL, model);
|
||||
},
|
||||
[APPLY_COUPON_CODE]: async function({ commit }: PaymentsContext, code: string): Promise<void> {
|
||||
const coupon = await api.applyCouponCode(code);
|
||||
commit(SET_COUPON, coupon);
|
||||
|
@ -25,6 +25,11 @@ export interface PaymentsApi {
|
||||
*/
|
||||
projectsUsageAndCharges(since: Date, before: Date): Promise<ProjectUsageAndCharges[]>;
|
||||
|
||||
/**
|
||||
* projectUsagePriceModel returns the project usage price model for the user.
|
||||
*/
|
||||
projectUsagePriceModel(): Promise<ProjectUsagePriceModel>;
|
||||
|
||||
/**
|
||||
* Add credit card
|
||||
* @param token - stripe token used to add a credit card as a payment method
|
||||
@ -409,3 +414,14 @@ export class TokenAmount {
|
||||
return Number.parseFloat(this._value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ProjectUsagePriceModel represents price model for project usage.
|
||||
*/
|
||||
export class ProjectUsagePriceModel {
|
||||
public constructor(
|
||||
public readonly storageMBMonthCents: string = '',
|
||||
public readonly egressMBCents: string = '',
|
||||
public readonly segmentMonthCents: string = '',
|
||||
) { }
|
||||
}
|
||||
|
41
web/satellite/src/utils/strings.ts
Normal file
41
web/satellite/src/utils/strings.ts
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
/**
|
||||
* decimalShift shifts the decimal point of a number represented by the given string.
|
||||
* @param decimal - the string representation of the number
|
||||
* @param places - the amount that the decimal point is shifted left
|
||||
*/
|
||||
export function decimalShift(decimal: string, places: number): string {
|
||||
let sign = '';
|
||||
if (decimal[0] == '-') {
|
||||
sign = '-';
|
||||
decimal = decimal.substring(1);
|
||||
}
|
||||
|
||||
const whole = decimal.replace('.', '');
|
||||
const dotIdx = (decimal.includes('.') ? decimal.indexOf('.') : decimal.length) - places;
|
||||
|
||||
if (dotIdx < 0) {
|
||||
const frac = whole.padStart(whole.length-dotIdx, '0').replace(/0+$/, '');
|
||||
if (!frac) {
|
||||
return '0';
|
||||
}
|
||||
return sign + '0.' + frac;
|
||||
}
|
||||
|
||||
if (dotIdx >= whole.length) {
|
||||
const int = whole.padEnd(dotIdx, '0').replace(/^0+/, '');
|
||||
if (!int) {
|
||||
return '0';
|
||||
}
|
||||
return sign + int;
|
||||
}
|
||||
|
||||
const int = whole.substring(0, dotIdx).replace(/^0+/, '');
|
||||
const frac = whole.substring(dotIdx).replace(/0+$/, '');
|
||||
if (!int && !frac) {
|
||||
return '0';
|
||||
}
|
||||
return sign + (int || '0') + (frac ? '.' + frac : '');
|
||||
}
|
@ -7,11 +7,10 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import { PaymentsMock } from '../../../mock/api/payments';
|
||||
import { ProjectsApiMock } from '../../../mock/api/projects';
|
||||
|
||||
import { makePaymentsModule } from '@/store/modules/payments';
|
||||
import { makePaymentsModule, PAYMENTS_ACTIONS } from '@/store/modules/payments';
|
||||
import { makeProjectsModule } from '@/store/modules/projects';
|
||||
import { ProjectUsageAndCharges } from '@/types/payments';
|
||||
import { Project } from '@/types/projects';
|
||||
import { MetaUtils } from '@/utils/meta';
|
||||
|
||||
import UsageAndChargesItem from '@/components/account/billing/estimatedCostsAndCredits/UsageAndChargesItem.vue';
|
||||
|
||||
@ -34,8 +33,7 @@ describe('UsageAndChargesItem', (): void => {
|
||||
projectsApi.setMockProjects([project]);
|
||||
const date = new Date(Date.UTC(1970, 1, 1));
|
||||
const projectCharge = new ProjectUsageAndCharges(date, date, 100, 100, 100, 'id', 100, 100, 100);
|
||||
|
||||
MetaUtils.getMetaContent = jest.fn().mockReturnValue('1');
|
||||
store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_PRICE_MODEL);
|
||||
|
||||
it('renders correctly', (): void => {
|
||||
const wrapper = shallowMount(UsageAndChargesItem, {
|
||||
|
@ -26,9 +26,9 @@ exports[`UsageAndChargesItem toggling dropdown works correctly 1`] = `
|
||||
<div class="usage-charges-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-charges-item-container__detailed-info-container__content-area">
|
||||
<div class="usage-charges-item-container__detailed-info-container__content-area__resource-container">
|
||||
<p>Storage ($0.001 per Gigabyte-Month)</p>
|
||||
<p>Egress ($0.001 per GB)</p>
|
||||
<p>Segments ($1 per Segment-Month)</p>
|
||||
<p>Storage ($10 per Gigabyte-Month)</p>
|
||||
<p>Egress ($10 per GB)</p>
|
||||
<p>Segments ($0.01 per Segment-Month)</p>
|
||||
</div>
|
||||
<div class="usage-charges-item-container__detailed-info-container__content-area__period-container">
|
||||
<p>Feb 1 - Feb 1</p>
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
PaymentsApi,
|
||||
PaymentsHistoryItem,
|
||||
ProjectUsageAndCharges,
|
||||
ProjectUsagePriceModel,
|
||||
TokenDeposit,
|
||||
NativePaymentHistoryItem,
|
||||
Wallet,
|
||||
@ -35,6 +36,10 @@ export class PaymentsMock implements PaymentsApi {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
projectUsagePriceModel(): Promise<ProjectUsagePriceModel> {
|
||||
return Promise.resolve(new ProjectUsagePriceModel('1', '1', '1'));
|
||||
}
|
||||
|
||||
addCreditCard(_token: string): Promise<void> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
34
web/satellite/tests/unit/utils/strings.spec.ts
Normal file
34
web/satellite/tests/unit/utils/strings.spec.ts
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { decimalShift } from '@/utils/strings';
|
||||
|
||||
describe('decimalShift', (): void => {
|
||||
it('shifts integers correctly', function() {
|
||||
['', '-'].forEach(sign => {
|
||||
const decimal = sign+'123';
|
||||
expect(decimalShift(decimal, 0)).toBe(sign+'123');
|
||||
expect(decimalShift(decimal, 2)).toBe(sign+'1.23');
|
||||
expect(decimalShift(decimal, 5)).toBe(sign+'0.00123');
|
||||
expect(decimalShift(decimal, -2)).toBe(sign+'12300');
|
||||
});
|
||||
});
|
||||
|
||||
it('shifts decimals correctly', function() {
|
||||
['', '-'].forEach(sign => {
|
||||
const decimal = sign+'1.23';
|
||||
expect(decimalShift(decimal, 0)).toBe(sign+'1.23');
|
||||
expect(decimalShift(decimal, -2)).toBe(sign+'123');
|
||||
expect(decimalShift(decimal, 3)).toBe(sign+'0.00123');
|
||||
expect(decimalShift(decimal, -4)).toBe(sign+'12300');
|
||||
});
|
||||
});
|
||||
|
||||
it('trims unnecessary characters', function() {
|
||||
['', '-'].forEach(sign => {
|
||||
expect(decimalShift(sign+'0.0012300', -2)).toBe(sign+'0.123');
|
||||
expect(decimalShift(sign+'12300', 2)).toBe(sign+'123');
|
||||
expect(decimalShift(sign+'000.000', 1)).toBe('0');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user