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:
Jeremy Wharton 2023-01-11 21:50:31 -06:00 committed by Storj Robot
parent 5d656e66bf
commit 6142b1cd12
17 changed files with 210 additions and 40 deletions

View File

@ -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(),
)

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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 }}">

View File

@ -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.
*

View File

@ -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) {

View File

@ -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) {

View File

@ -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;
}
/**

View File

@ -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;
}
/**

View File

@ -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);

View File

@ -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 = '',
) { }
}

View 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 : '');
}

View File

@ -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, {

View File

@ -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>

View File

@ -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');
}

View 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');
});
});
});