Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pi: Add API to retreive billing status changes. #1526

Merged
merged 5 commits into from
Oct 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 35 additions & 30 deletions politeiad/backendv2/tstorebe/plugins/pi/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ func (p *piPlugin) cmdSetBillingStatus(token []byte, payload string) (string, er
}

// Ensure no billing status already exists
sc, err := p.billingStatusChange(token)
bscs, err := p.billingStatusChanges(token)
if err != nil {
return "", err
}
if sc != nil {
if len(bscs) > 0 {
return "", backend.PluginError{
PluginID: pi.PluginID,
ErrorCode: uint32(pi.ErrorCodeBillingStatusChangeNotAllowed),
Expand Down Expand Up @@ -196,7 +196,11 @@ func (p *piPlugin) cmdSummary(token []byte) (string, error) {
voteStatus = vs.Status
// If vote status is approved fetch billing status change.
if voteStatus == ticketvote.VoteStatusApproved {
bsc, err = p.billingStatusChange(token)
bscs, err := p.billingStatusChanges(token)
if len(bscs) > 0 {
// Get newest billing status change.
bsc = &bscs[len(bscs)-1]
}
if err != nil {
return "", err
}
Expand All @@ -209,24 +213,10 @@ func (p *piPlugin) cmdSummary(token []byte) (string, error) {
return "", err
}

// Get status reason if status should have a reason.
var reason string
switch proposalStatus {
case pi.PropStatusUnvettedAbandoned, pi.PropStatusAbandoned,
pi.PropStatusUnvettedCensored, pi.PropStatusCensored:
reason, err = p.lastStatusChangeReason(r.Metadata)
if err != nil {
return "", err
}
case pi.PropStatusClosed:
reason = bsc.Reason
}

// Prepare reply
sr := pi.SummaryReply{
Summary: pi.ProposalSummary{
Status: proposalStatus,
StatusReason: reason,
Status: proposalStatus,
},
}
reply, err := json.Marshal(sr)
Expand All @@ -237,6 +227,26 @@ func (p *piPlugin) cmdSummary(token []byte) (string, error) {
return string(reply), nil
}

// cmdBillingStatusChanges returns the billing status changes of a proposal.
func (p *piPlugin) cmdBillingStatusChanges(token []byte) (string, error) {
// Get billing status changes
bscs, err := p.billingStatusChanges(token)
if err != nil {
return "", err
}

// Prepare reply
bscsr := pi.BillingStatusChangesReply{
BillingStatusChanges: bscs,
}
reply, err := json.Marshal(bscsr)
if err != nil {
return "", err
}

return string(reply), nil
}

// statusChangeReason returns the last status change reason of a proposal.
// This function assumes the proposal has at least one status change.
func (p *piPlugin) lastStatusChangeReason(metadata []backend.MetadataStream) (string, error) {
Expand Down Expand Up @@ -390,10 +400,8 @@ func (p *piPlugin) billingStatusSave(token []byte, bsc pi.BillingStatusChange) e
return p.tstore.BlobSave(token, *be)
}

// billingStatus returns a pointer to a BillingStatusChange for a record if
// it's billing status was set and nil otherwise. It assumes that a billing
// status can be set only once.
func (p *piPlugin) billingStatusChange(token []byte) (*pi.BillingStatusChange, error) {
// billingStatusChanges returns the billing status changes of a proposal.
func (p *piPlugin) billingStatusChanges(token []byte) ([]pi.BillingStatusChange, error) {
// Retrieve blobs
blobs, err := p.tstore.BlobsByDataDesc(token,
[]string{dataDescriptorBillingStatus})
Expand All @@ -402,25 +410,22 @@ func (p *piPlugin) billingStatusChange(token []byte) (*pi.BillingStatusChange, e
}

// Decode blobs
statuses := make([]pi.BillingStatusChange, 0, len(blobs))
statusChanges := make([]pi.BillingStatusChange, 0, len(blobs))
for _, v := range blobs {
a, err := billingStatusDecode(v)
if err != nil {
return nil, err
}
statuses = append(statuses, *a)
statusChanges = append(statusChanges, *a)
}

// Sanity check. They should already be sorted from oldest to
// newest.
sort.SliceStable(statuses, func(i, j int) bool {
return statuses[i].Timestamp < statuses[j].Timestamp
sort.SliceStable(statusChanges, func(i, j int) bool {
return statusChanges[i].Timestamp < statusChanges[j].Timestamp
})

if len(statuses) > 0 {
return &statuses[0], nil
}
return nil, nil
return statusChanges, nil
}

// billingStatusEncode encodes a BillingStatusChange into a BlobEntry.
Expand Down
7 changes: 4 additions & 3 deletions politeiad/backendv2/tstorebe/plugins/pi/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -587,12 +587,13 @@ func (p *piPlugin) writesAllowedOnApprovedProposal(token []byte, cmd, payload st
// Get billing status to determine whether to allow author updates
// or not.
var bsc *pi.BillingStatusChange
bsc, err := p.billingStatusChange(token)
bscs, err := p.billingStatusChanges(token)
if err != nil {
return err
}
// We assume here that admins can set a billing status only once
if bsc != nil {
if len(bscs) > 0 {
// Get latest billing status change
bsc = &bscs[len(bscs)-1]
if bsc.Status == pi.BillingStatusClosed ||
bsc.Status == pi.BillingStatusCompleted {
// If billing status is set to closed or completed, comment writes
Expand Down
2 changes: 2 additions & 0 deletions politeiad/backendv2/tstorebe/plugins/pi/pi.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ func (p *piPlugin) Cmd(token []byte, cmd, payload string) (string, error) {
return p.cmdSetBillingStatus(token, payload)
case pi.CmdSummary:
return p.cmdSummary(token)
case pi.CmdBillingStatusChanges:
return p.cmdBillingStatusChanges(token)
}

return "", backend.ErrPluginCmdInvalid
Expand Down
69 changes: 54 additions & 15 deletions politeiad/client/pi.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,43 @@ package client
import (
"context"
"encoding/json"
"fmt"

pdv2 "github.com/decred/politeia/politeiad/api/v2"
"github.com/decred/politeia/politeiad/plugins/pi"
)

// PiSetBillingStatus sends the pi plugin SetBillingStatus command to the
// politeiad v2 API.
func (c *Client) PiSetBillingStatus(ctx context.Context, sbs pi.SetBillingStatus) (*pi.SetBillingStatusReply, error) {
// Setup request
b, err := json.Marshal(sbs)
if err != nil {
return nil, err
}
cmd := pdv2.PluginCmd{
Token: sbs.Token,
ID: pi.PluginID,
Command: pi.CmdSetBillingStatus,
Payload: string(b),
}

// Send request
reply, err := c.PluginWrite(ctx, cmd)
if err != nil {
return nil, err
}

// Decode reply
var sbsr pi.SetBillingStatusReply
err = json.Unmarshal([]byte(reply), &sbsr)
if err != nil {
return nil, err
}

return &sbsr, nil
}

// PiSummaries sends a page of pi plugin Summary commands to the politeiad
// v2 API.
func (c *Client) PiSummaries(ctx context.Context, tokens []string) (map[string]pi.SummaryReply, error) {
Expand Down Expand Up @@ -52,33 +84,40 @@ func (c *Client) PiSummaries(ctx context.Context, tokens []string) (map[string]p
return ssr, nil
}

// PiSetBillingStatus sends the pi plugin BillingStatus command to the
// politeiad v2 API.
func (c *Client) PiSetBillingStatus(ctx context.Context, sbs pi.SetBillingStatus) (*pi.SetBillingStatusReply, error) {
// PiBillingStatusChanges sends the pi plugin BillingStatusChanges command
// to the politeiad v2 API.
func (c *Client) PiBillingStatusChanges(ctx context.Context, token string) (*pi.BillingStatusChangesReply, error) {
// Setup request
b, err := json.Marshal(sbs)
cmds := []pdv2.PluginCmd{
{
Token: token,
ID: pi.PluginID,
Command: pi.CmdBillingStatusChanges,
Payload: "",
},
}

// Send request
replies, err := c.PluginReads(ctx, cmds)
if err != nil {
return nil, err
}
cmd := pdv2.PluginCmd{
Token: sbs.Token,
ID: pi.PluginID,
Command: pi.CmdSetBillingStatus,
Payload: string(b),
if len(replies) == 0 {
return nil, fmt.Errorf("no replies found")
}

// Send request
reply, err := c.PluginWrite(ctx, cmd)
pcr := replies[0]
err = extractPluginCmdError(pcr)
if err != nil {
return nil, err
}

// Decode reply
var sbsr pi.SetBillingStatusReply
err = json.Unmarshal([]byte(reply), &sbsr)
var bscsr pi.BillingStatusChangesReply
err = json.Unmarshal([]byte(pcr.Payload), &bscsr)
if err != nil {
return nil, err
}

return &sbsr, nil
return &bscsr, nil

}
22 changes: 16 additions & 6 deletions politeiad/plugins/pi/pi.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ const (

// CmdSummary command returns a summary for a proposal.
CmdSummary = "summary"

// CmdBillingStatusChanges command returns the billing status changes
// of a proposal.
CmdBillingStatusChanges = "billingstatuschanges"
)

// Plugin setting keys can be used to specify custom plugin settings. Default
Expand Down Expand Up @@ -359,13 +363,8 @@ type SummaryReply struct {
}

// ProposalSummary summarizes proposal information.
//
// StatusReason field will be populated if the status change required a
// reason to be given. Examples include when a proposal is censored/abandoned
// or when the billing status of the proposal is set to closed.
type ProposalSummary struct {
Status PropStatusT `json:"status"`
StatusReason string `json:"statusreason"`
Status PropStatusT `json:"status"`
}

// PropStatusT represents the status of a proposal. It combines record and
Expand Down Expand Up @@ -473,3 +472,14 @@ const (
type ProposalUpdateMetadata struct {
Title string `json:"title"`
}

// BillingStatusChanges requests the billing status changes for the provided
// proposal token.
type BillingStatusChanges struct {
Token string `json:"token"`
}

// BillingStatusChangesReply is the reply to the BillingStatusChanges command.
type BillingStatusChangesReply struct {
BillingStatusChanges []BillingStatusChange `json:"billingstatuschanges"`
}
29 changes: 19 additions & 10 deletions politeiawww/api/pi/v1/v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ const (
// RoutePolicy returns the policy for the pi API.
RoutePolicy = "/policy"

// RouteSetBillingStatus sets the record's billing status.
// RouteSetBillingStatus sets the proposal's billing status.
RouteSetBillingStatus = "/setbillingstatus"

// RouteSummaries returns the proposal summary for a page of
// records.
RouteSummaries = "/summaries"

// RouteBillingStatusChanges returns the proposal's billing status changes.
RouteBillingStatusChanges = "/billingstatuschanges"
)

// ErrorCodeT represents a user error code.
Expand Down Expand Up @@ -283,16 +286,16 @@ const (
SummariesPageSize uint32 = 5
)

// Summaries requests the proposal summaries for the provided record tokens.
// Summaries requests the proposal summaries for the provided proposal tokens.
type Summaries struct {
Tokens []string `json:"tokens"`
}

// SummariesReply is the reply to the Summaries command.
//
// Summaries field contains a vote summary for each of the provided tokens.
// Summaries field contains a proposal summary for each of the provided tokens.
// The map will not contain an entry for any tokens that did not correspond
// to an actual record. It is the callers responsibility to ensure that a
// to an actual proposal. It is the callers responsibility to ensure that a
// summary is returned for all provided tokens.
type SummariesReply struct {
Summaries map[string]Summary `json:"summaries"` // [token]Summary
Expand All @@ -302,11 +305,17 @@ type SummariesReply struct {
//
// Status field is the string value of the PropStatusT type which is defined
// along with all of it's possible values in the pi plugin API.
//
// StatusReason field will be populated if the status change required a
// reason to be given. Examples include when a proposal is censored/abandoned
// or when the billing status of the proposal is set to closed.
type Summary struct {
Status string `json:"status"`
StatusReason string `json:"statusreason"`
Status string `json:"status"`
}

// BillingStatusChanges requests the billing status changes for the provided
// proposal token.
type BillingStatusChanges struct {
Token string `json:"token"`
}

// BillingStatusChangesReply is the reply to the BillingStatusChanges command.
type BillingStatusChangesReply struct {
BillingStatusChanges []BillingStatusChange `json:"billingstatuschanges"`
}
18 changes: 18 additions & 0 deletions politeiawww/client/pi.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ func (c *Client) PiSummaries(s piv1.Summaries) (*piv1.SummariesReply, error) {
return &sr, nil
}

// PiBillingStatusChanges sends a pi v1 BillingStatusChanges request to
// politeiawww.
func (c *Client) PiBillingStatusChanges(bscs piv1.BillingStatusChanges) (*piv1.BillingStatusChangesReply, error) {
resBody, err := c.makeReq(http.MethodPost,
piv1.APIRoute, piv1.RouteBillingStatusChanges, bscs)
if err != nil {
return nil, err
}

var bscsr piv1.BillingStatusChangesReply
err = json.Unmarshal(resBody, &bscsr)
if err != nil {
return nil, err
}

return &bscsr, nil
}

// ProposalMetadataDecode decodes and returns the ProposalMetadata from the
// Provided record files. An error returned if a ProposalMetadata is not found.
func ProposalMetadataDecode(files []rcv1.File) (*piv1.ProposalMetadata, error) {
Expand Down
2 changes: 2 additions & 0 deletions politeiawww/cmd/pictl/cmdhelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ func (c *cmdHelp) Execute(args []string) error {
fmt.Printf("%s\n", proposalSetStatusHelpMsg)
case "proposalsetbillingstatus":
fmt.Printf("%s\n", proposalSetBillingStatusHelpMsg)
case "proposalbillingstatuschanges":
fmt.Printf("%s\n", proposalBillingStatusChangesHelpMsg)
case "proposaldetails":
fmt.Printf("%s\n", proposalDetailsHelpMsg)
case "proposaltimestamps":
Expand Down
Loading