web/multinode: payout api

WHAT: multinode payout types, api and store

WHY: preparation of logic for mnd payouts page implementation

Change-Id: I4f2ea78056eab84c482853ef7a6c4cab4fb4c04f
This commit is contained in:
NickolaiYurchenko 2021-04-23 21:43:23 +03:00
parent c08ca361d8
commit ce075a1d53
7 changed files with 244 additions and 33 deletions

View File

@ -1,6 +1,8 @@
// Copyright (C) 2021 Storj Labs, Inc. // Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information. // See LICENSE for copying information.
import { HttpClient } from '@/private/http/client';
/** /**
* ErrorUnauthorized is a custom error type for performing unauthorized operations. * ErrorUnauthorized is a custom error type for performing unauthorized operations.
*/ */
@ -27,3 +29,37 @@ export class InternalError extends Error {
super(message); super(message);
} }
} }
/**
* APIClient is base client that holds http client and error handler.
*/
export class APIClient {
protected readonly http: HttpClient = new HttpClient();
/**
* handles error due to response code.
* @param response - response from server.
*
* @throws {@link BadRequestError}
* This exception is thrown if the input is not a valid ISBN number.
*
* @throws {@link UnauthorizedError}
* Thrown if the ISBN number is valid, but no such book exists in the catalog.
*
* @throws {@link InternalError}
* Thrown if the ISBN number is valid, but no such book exists in the catalog.
*
* @private
*/
protected async handleError(response: Response): Promise<void> {
const body = await response.json();
switch (response.status) {
case 401: throw new UnauthorizedError(body.error);
case 400: throw new BadRequestError(body.error);
case 500:
default:
throw new InternalError(body.error);
}
}
}

View File

@ -1,15 +1,13 @@
// Copyright (C) 2021 Storj Labs, Inc. // Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information. // See LICENSE for copying information.
import { BadRequestError, InternalError, UnauthorizedError } from '@/api/index'; import { APIClient } from '@/api/index';
import { CreateNodeFields, Node, NodeURL } from '@/nodes'; import { CreateNodeFields, Node, NodeURL } from '@/nodes';
import { HttpClient } from '@/private/http/client';
/** /**
* client for nodes controller of MND api. * client for nodes controller of MND api.
*/ */
export class NodesClient { export class NodesClient extends APIClient {
private readonly http: HttpClient = new HttpClient();
private readonly ROOT_PATH: string = '/api/v0/nodes'; private readonly ROOT_PATH: string = '/api/v0/nodes';
/** /**
@ -176,31 +174,4 @@ export class NodesClient {
url.Name, url.Name,
)); ));
} }
/**
* handles error due to response code.
* @param response - response from server.
*
* @throws {@link BadRequestError}
* This exception is thrown if the input is not a valid ISBN number.
*
* @throws {@link UnauthorizedError}
* Thrown if the ISBN number is valid, but no such book exists in the catalog.
*
* @throws {@link InternalError}
* Thrown if the ISBN number is valid, but no such book exists in the catalog.
*
* @private
*/
private async handleError(response: Response): Promise<void> {
const body = await response.json();
switch (response.status) {
case 401: throw new UnauthorizedError(body.error);
case 400: throw new BadRequestError(body.error);
case 500:
default:
throw new InternalError(body.error);
}
}
} }

View File

@ -0,0 +1,38 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
import { APIClient } from '@/api/index';
import { PayoutsSummary } from '@/payouts';
/**
* client for nodes controller of MND api.
*/
export class PayoutsClient extends APIClient {
private readonly ROOT_PATH: string = '/api/v0/payouts';
/**
* handles fetch of payouts summary information.
*
* @param satelliteId - satellite id.
* @param period - selected period.
*
* @throws {@link BadRequestError}
* This exception is thrown if the input is not a valid.
*
* @throws {@link UnauthorizedError}
* Thrown if the auth cookie is missing or invalid.
*
* @throws {@link InternalError}
* Thrown if something goes wrong on server side.
*/
public async summary(satelliteId: string | null, period: string | null): Promise<PayoutsSummary> {
const path = `${this.ROOT_PATH}/summary`;
const response = await this.http.get(path);
if (!response.ok) {
await this.handleError(response);
}
return await response.json();
}
}

View File

@ -5,8 +5,11 @@ import Vue from 'vue';
import Vuex, { ModuleTree, Store, StoreOptions } from 'vuex'; import Vuex, { ModuleTree, Store, StoreOptions } from 'vuex';
import { NodesClient } from '@/api/nodes'; import { NodesClient } from '@/api/nodes';
import { PayoutsClient } from '@/api/payouts';
import { NodesModule, NodesState } from '@/app/store/nodes'; import { NodesModule, NodesState } from '@/app/store/nodes';
import { PayoutsModule, PayoutsState } from '@/app/store/payouts';
import { Nodes } from '@/nodes/service'; import { Nodes } from '@/nodes/service';
import { Payouts } from '@/payouts/service';
Vue.use(Vuex); Vue.use(Vuex);
@ -15,6 +18,7 @@ Vue.use(Vuex);
*/ */
export class RootState { export class RootState {
nodes: NodesState; nodes: NodesState;
payouts: PayoutsState;
} }
/** /**
@ -25,13 +29,15 @@ export class MultinodeStoreOptions implements StoreOptions<RootState> {
public readonly state: RootState; public readonly state: RootState;
public readonly modules: ModuleTree<RootState>; public readonly modules: ModuleTree<RootState>;
public constructor(nodes: NodesModule) { public constructor(nodes: NodesModule, payouts: PayoutsModule) {
this.strict = true; this.strict = true;
this.state = { this.state = {
nodes: nodes.state, nodes: nodes.state,
payouts: payouts.state,
}; };
this.modules = { this.modules = {
nodes, nodes,
payouts,
}; };
} }
} }
@ -39,11 +45,14 @@ export class MultinodeStoreOptions implements StoreOptions<RootState> {
// Services // Services
const nodesClient: NodesClient = new NodesClient(); const nodesClient: NodesClient = new NodesClient();
const nodesService: Nodes = new Nodes(nodesClient); const nodesService: Nodes = new Nodes(nodesClient);
const payoutsClient: PayoutsClient = new PayoutsClient();
const payoutsService: Payouts = new Payouts(payoutsClient);
// Modules // Modules
const nodesModule: NodesModule = new NodesModule(nodesService); const nodesModule: NodesModule = new NodesModule(nodesService);
const payoutsModule: PayoutsModule = new PayoutsModule(payoutsService);
// Store // Store
export const store: Store<RootState> = new Vuex.Store<RootState>( export const store: Store<RootState> = new Vuex.Store<RootState>(
new MultinodeStoreOptions(nodesModule), new MultinodeStoreOptions(nodesModule, payoutsModule),
); );

View File

@ -0,0 +1,60 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
import { ActionContext, ActionTree, GetterTree, Module, MutationTree } from 'vuex';
import { RootState } from '@/app/store/index';
import { PayoutsSummary } from '@/payouts';
import { Payouts } from '@/payouts/service';
/**
* PayoutsState is a representation of payouts module state.
*/
export class PayoutsState {
public summary: PayoutsSummary;
}
/**
* NodesModule is a part of a global store that encapsulates all nodes related logic.
*/
export class PayoutsModule implements Module<PayoutsState, RootState> {
public readonly namespaced: boolean;
public readonly state: PayoutsState;
public readonly getters?: GetterTree<PayoutsState, RootState>;
public readonly actions: ActionTree<PayoutsState, RootState>;
public readonly mutations: MutationTree<PayoutsState>;
private readonly payouts: any;
public constructor(payouts: Payouts) {
this.payouts = payouts;
this.namespaced = true;
this.state = new PayoutsState();
this.mutations = {
populate: this.populate,
};
this.actions = {
getSummary: this.getSummary.bind(this),
};
}
/**
* populate mutation will set payouts state.
* @param state - state of the module.
* @param summary - payouts summary information depends on selected time and satellite.
*/
public populate(state: PayoutsState, summary: PayoutsSummary): void {
state.summary = summary;
}
/**
* getSummary action loads payouts summary information.
* @param ctx - context of the Vuex action.
*/
public async getSummary(ctx: ActionContext<PayoutsState, RootState>): Promise<void> {
// @ts-ignore
const summary = await this.payouts.summary(ctx.rootState.nodes.selectedSatellite.id, '');
ctx.commit('populate', summary);
}
}

View File

@ -0,0 +1,50 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
/**
* PayoutsSummary is a representation of summary of payout information for node.
*/
export class NodePayoutsSummary {
public constructor(
public nodeID: string = '',
public nodeName: string = '',
public held: number = 0,
public paid: number = 0,
) {}
}
/**
* PayoutsSummary is a representation of summary of payout information for all selected nodes and list of payout summaries by nodes.
*/
export class PayoutsSummary {
public constructor(
public totalEarned: number = 0,
public totalHeld: number = 0,
public totalPaid: number = 0,
public nodeSummary: NodePayoutsSummary[] = [],
) {}
}
/**
* Represents payout period month and year.
*/
export class PayoutPeriod {
public constructor(
public year: number = new Date().getUTCFullYear(),
public month: number = new Date().getUTCMonth(),
) {}
public get period(): string {
return this.month < 9 ? `${this.year}-0${this.month + 1}` : `${this.year}-${this.month + 1}`;
}
/**
* Parses PayoutPeriod from string.
* @param period string
*/
public static fromString(period: string): PayoutPeriod {
const periodArray = period.split('-');
return new PayoutPeriod(parseInt(periodArray[0]), parseInt(periodArray[1]) - 1);
}
}

View File

@ -0,0 +1,47 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
import { PayoutsClient } from '@/api/payouts';
import { NodePayoutsSummary, PayoutsSummary } from '@/payouts/index';
/**
* exposes all payouts related logic
*/
export class Payouts {
private readonly payouts: PayoutsClient;
public constructor(payouts: PayoutsClient) {
this.payouts = payouts;
}
/**
* fetches of payouts summary information.
*
* @param satelliteId - satellite id.
* @param period - selected period.
*
* @throws {@link BadRequestError}
* This exception is thrown if the input is not a valid.
*
* @throws {@link UnauthorizedError}
* Thrown if the auth cookie is missing or invalid.
*
* @throws {@link InternalError}
* Thrown if something goes wrong on server side.
*/
public async summary(satelliteId: string | null, period: string | null): Promise<PayoutsSummary> {
const result = await this.payouts.summary(satelliteId, period);
return new PayoutsSummary(
result.totalEarned,
result.totalHeld,
result.totalPaid,
result.nodeSummary.map(item => new NodePayoutsSummary(
item.nodeID,
item.nodeName,
item.held,
item.paid,
)),
);
}
}