diff --git a/politeiawww/api/cms/v1/api.md b/politeiawww/api/cms/v1/api.md index 668338a11..c053fa4a4 100644 --- a/politeiawww/api/cms/v1/api.md +++ b/politeiawww/api/cms/v1/api.md @@ -1765,6 +1765,188 @@ Reply: } ``` +### `Proposal Billing Summary` + +Retrieve all billing information for all approved proposals. + +This retrieves the tokens for approved proposals and uses those tokens to +search through the database for invoices that have line-items that have that +as proposal token added. + +There is also a basic pagination feature implemented with an offset and a +page count of proposals to return. Note, there is a max proposal +spending list page count. If above 20, then it will be set to that max. +These are optional and if both unset, all proposal summaries will be returned. + +Note: This call requires admin privileges. + +**Route:** `GET /v1/proposals/spendingsummary` + +**Params:** + +| Parameter | Type | Description | Required | +|-|-|-|-| +| offset | int | Page offset | No | +| count | int | Page count | No | + +**Results:** + +| | Type | Description | +| - | - | - | +| proposals | Array of ProposalSpending | Aggregated information of spending for all approved proposals. | + +**ProposalSpending:** + +| | Type | Description | +| - | - | - | +| token | string | Censorship record token of proposal. | +| title | string | Title of approved proposal. | +| totalbilled | int64 | Total billed against the proposal (in US Cents) | +| invoices | Array of InvoiceRecord | All (partially filled) invoice records that have line items with the proposal token. | + +**Example** + +Request: + +``` json +{} +``` + +Reply: + +```json +{ + "proposals": [{ + "token": "8d14c77d9a28a1764832d0fcfb86b6af08f6b327347ab4af4803f9e6f7927225", + "title": "Super awesome proposal!", + "totalbilled": 115000, + "invoices": [ + { + "status": 0, + "timestamp": 0, + "userid": "5c36086c-fa22-4c53-aee1-adafc4446751", + "username": "admin", + "publickey": "c0876a34451431b77ee9cd2e65662d0829010e0285d9fe1cc1e3ea20005b88bf", + "signature": "", + "file": null, + "version": "", + "input": { + "version": 0, + "month": 5, + "year": 2020, + "exchangerate": 1411, + "contractorname": "", + "contractorlocation": "", + "contractorcontact": "", + "contractorrate": 5000, + "paymentaddress": "", + "lineitems": [ { + "type": 1, + "domain": "Development", + "subdomain": "dvdddasf", + "description": "sadfasdfsdf", + "proposaltoken": "0de5bd82bcccf22f4ccd1881fc9d88159ace56d0c1cfc7dcd86656e738e46a87", + "subuserid": "", + "subrate": 0, + "labor": 1380, + "expenses": 0 + } + ] + } + } + ] + }] + } +} +``` + +### `Proposal Billing Details` + +Retrieve all billing information for the given proposal token. + +Note: This call requires admin privileges. + +**Route:** `POST /v1/proposals/spendingdetails` + +**Params:** + +| Parameter | Type | Description | Required | +|-|-|-|-| +| token | string | Token for approved proposal. | Yes | + +**Results:** + +| | Type | Description | +| - | - | - | +| details | ProposalSpending | Aggregated information for the given proposal token. | + +**ProposalSpending:** + +| | Type | Description | +| - | - | - | +| token | string | Censorship record token of proposal. | +| title | string | Title of approved proposal. | +| totalbilled | int64 | Total billed against the proposal (in US Cents) | +| invoices | Array of InvoiceRecord | All (partially filled) invoice records that have line items with the proposal token. | + +**Example** + +Request: + +``` json +{ + "token": "0de5bd82bcccf22f4ccd1881fc9d88159ace56d0c1cfc7dcd86656e738e46a87" +} +``` + +Reply: + +```json +{ + "details": { + "token": "8d14c77d9a28a1764832d0fcfb86b6af08f6b327347ab4af4803f9e6f7927225", + "title": "Super awesome proposal!", + "totalbilled": 115000, + "invoices": [ + { + "status": 0, + "timestamp": 0, + "userid": "5c36086c-fa22-4c53-aee1-adafc4446751", + "username": "admin", + "publickey": "c0876a34451431b77ee9cd2e65662d0829010e0285d9fe1cc1e3ea20005b88bf", + "signature": "", + "file": null, + "version": "", + "input": { + "version": 0, + "month": 5, + "year": 2020, + "exchangerate": 1411, + "contractorname": "", + "contractorlocation": "", + "contractorcontact": "", + "contractorrate": 5000, + "paymentaddress": "", + "lineitems": [ { + "type": 1, + "domain": "Development", + "subdomain": "dvdddasf", + "description": "sadfasdfsdf", + "proposaltoken": "0de5bd82bcccf22f4ccd1881fc9d88159ace56d0c1cfc7dcd86656e738e46a87", + "subuserid": "", + "subrate": 0, + "labor": 1380, + "expenses": 0 + } + ] + } + } + ] + } + } + +``` + ### Error codes | Status | Value | Description | diff --git a/politeiawww/api/cms/v1/v1.go b/politeiawww/api/cms/v1/v1.go index 750cfd94a..627e04f44 100644 --- a/politeiawww/api/cms/v1/v1.go +++ b/politeiawww/api/cms/v1/v1.go @@ -22,36 +22,38 @@ const ( APIVersion = 1 // Contractor Management Routes - RouteInviteNewUser = "/invite" - RouteRegisterUser = "/register" - RouteCMSUsers = "/cmsusers" - RouteNewInvoice = "/invoices/new" - RouteEditInvoice = "/invoices/edit" - RouteInvoiceDetails = "/invoices/{token:[A-z0-9]{64}}" - RouteSetInvoiceStatus = "/invoices/{token:[A-z0-9]{64}}/status" - RouteUserInvoices = "/user/invoices" - RouteUserSubContractors = "/user/subcontractors" - RouteNewDCC = "/dcc/new" - RouteDCCDetails = "/dcc/{token:[A-z0-9]{64}}" - RouteGetDCCs = "/dcc" - RouteSupportOpposeDCC = "/dcc/supportoppose" - RouteNewCommentDCC = "/dcc/newcomment" - RouteDCCComments = "/dcc/{token:[A-z0-9]{64}}/comments" - RouteSetDCCStatus = "/dcc/{token:[A-z0-9]{64}}/status" - RouteCastVoteDCC = "/dcc/vote" - RouteVoteDetailsDCC = "/dcc/votedetails" - RouteActiveVotesDCC = "/dcc/activevotes" - RouteStartVoteDCC = "/dcc/startvote" - RouteAdminInvoices = "/admin/invoices" - RouteManageCMSUser = "/admin/managecms" - RouteAdminUserInvoices = "/admin/userinvoices" - RouteGeneratePayouts = "/admin/generatepayouts" - RouteInvoicePayouts = "/admin/invoicepayouts" - RoutePayInvoices = "/admin/payinvoices" - RouteInvoiceComments = "/invoices/{token:[A-z0-9]{64}}/comments" - RouteInvoiceExchangeRate = "/invoices/exchangerate" - RouteProposalOwner = "/proposals/owner" - RouteProposalBilling = "/proposals/billing" + RouteInviteNewUser = "/invite" + RouteRegisterUser = "/register" + RouteCMSUsers = "/cmsusers" + RouteNewInvoice = "/invoices/new" + RouteEditInvoice = "/invoices/edit" + RouteInvoiceDetails = "/invoices/{token:[A-z0-9]{64}}" + RouteSetInvoiceStatus = "/invoices/{token:[A-z0-9]{64}}/status" + RouteUserInvoices = "/user/invoices" + RouteUserSubContractors = "/user/subcontractors" + RouteNewDCC = "/dcc/new" + RouteDCCDetails = "/dcc/{token:[A-z0-9]{64}}" + RouteGetDCCs = "/dcc" + RouteSupportOpposeDCC = "/dcc/supportoppose" + RouteNewCommentDCC = "/dcc/newcomment" + RouteDCCComments = "/dcc/{token:[A-z0-9]{64}}/comments" + RouteSetDCCStatus = "/dcc/{token:[A-z0-9]{64}}/status" + RouteCastVoteDCC = "/dcc/vote" + RouteVoteDetailsDCC = "/dcc/votedetails" + RouteActiveVotesDCC = "/dcc/activevotes" + RouteStartVoteDCC = "/dcc/startvote" + RouteAdminInvoices = "/admin/invoices" + RouteManageCMSUser = "/admin/managecms" + RouteAdminUserInvoices = "/admin/userinvoices" + RouteGeneratePayouts = "/admin/generatepayouts" + RouteInvoicePayouts = "/admin/invoicepayouts" + RoutePayInvoices = "/admin/payinvoices" + RouteInvoiceComments = "/invoices/{token:[A-z0-9]{64}}/comments" + RouteInvoiceExchangeRate = "/invoices/exchangerate" + RouteProposalOwner = "/proposals/owner" + RouteProposalBilling = "/proposals/billing" + RouteProposalBillingSummary = "/proposals/spendingsummary" + RouteProposalBillingDetails = "/proposals/spendingdetails" // Invoice status codes InvoiceStatusInvalid InvoiceStatusT = 0 // Invalid status @@ -174,6 +176,11 @@ const ( // statement contained within a DCC PolicyMaxSponsorStatementLength = 5000 + // ProposalBillingListPageSize is the maximum number of proposal billing + // summaries returned for the routes that return lists of proposal billing + // summaries. + ProposalBillingListPageSize = 50 + ErrorStatusMalformedName www.ErrorStatusT = 1001 ErrorStatusMalformedLocation www.ErrorStatusT = 1002 ErrorStatusInvoiceNotFound www.ErrorStatusT = 1003 @@ -992,3 +999,37 @@ type CastVoteReply struct { Error string `json:"error"` // Error status message ErrorStatus cmsplugin.ErrorStatusT `json:"errorstatus,omitempty"` // Error status code } + +// ProposalBillingSummary allows for all proposal spending to be returned for +// an admin to review. +type ProposalBillingSummary struct { + Offset int `json:"offset"` // Amount to offset for pagination + Count int `json:"count"` // Size of page for pagination +} + +// ProposalBillingSummaryReply returns an array of proposal spending based on +// the list of approved invoices returned from the respective proposals site. +type ProposalBillingSummaryReply struct { + Proposals []ProposalSpending `json:"proposals"` +} + +// ProposalSpending contains all the information about a given proposal's +// spending. +type ProposalSpending struct { + Token string `json:"token"` + Title string `json:"title"` + TotalBilled int64 `json:"totalbilled"` + Invoices []InvoiceRecord `json:"invoices"` +} + +// ProposalBillingDetails returns all the information about the given proposal's +// spending. +type ProposalBillingDetails struct { + Token string `json:"token"` +} + +// ProposalBillingDetailsReply returns the spending information about the +// requested proposal. +type ProposalBillingDetailsReply struct { + Details ProposalSpending `json:"details"` +} diff --git a/politeiawww/cmd/cmswww/cmswww.go b/politeiawww/cmd/cmswww/cmswww.go index 8f57cb329..282eab073 100644 --- a/politeiawww/cmd/cmswww/cmswww.go +++ b/politeiawww/cmd/cmswww/cmswww.go @@ -55,55 +55,57 @@ type cmswww struct { Config shared.Config // Commands - ActiveVotes ActiveVotesCmd `command:"activevotes" description:"(user) get the dccs that are being voted on"` - AdminInvoices AdminInvoicesCmd `command:"admininvoices" description:"(admin) get all invoices (optional by month/year and/or status)"` - BatchProposals shared.BatchProposalsCmd `command:"batchproposals" description:"(user) retrieve a set of proposals"` - CensorComment shared.CensorCommentCmd `command:"censorcomment" description:"(admin) censor a comment"` - ChangePassword shared.ChangePasswordCmd `command:"changepassword" description:"(user) change the password for the logged in user"` - ChangeUsername shared.ChangeUsernameCmd `command:"changeusername" description:"(user) change the username for the logged in user"` - CMSUsers CMSUsersCmd `command:"cmsusers" description:"(user) get a list of cms users"` - DCCComments DCCCommentsCmd `command:"dcccomments" description:"(user) get the comments for a dcc proposal"` - DCCDetails DCCDetailsCmd `command:"dccdetails" description:"(user) get the details of a dcc"` - EditInvoice EditInvoiceCmd `command:"editinvoice" description:"(user) edit a invoice"` - EditUser EditUserCmd `command:"edituser" description:"(user) edit current cms user information"` - GeneratePayouts GeneratePayoutsCmd `command:"generatepayouts" description:"(admin) generate a list of payouts with addresses and amounts to pay"` - GetDCCs GetDCCsCmd `command:"getdccs" description:"(user) get all dccs (optional by status)"` - Help HelpCmd `command:"help" description:" print a detailed help message for a specific command"` - InvoiceComments InvoiceCommentsCmd `command:"invoicecomments" description:"(user) get the comments for a invoice"` - InvoiceExchangeRate InvoiceExchangeRateCmd `command:"invoiceexchangerate" description:"(user) get exchange rate for a given month/year"` - InviteNewUser InviteNewUserCmd `command:"invite" description:"(admin) invite a new user"` - InvoiceDetails InvoiceDetailsCmd `command:"invoicedetails" description:"(public) get the details of a proposal"` - InvoicePayouts InvoicePayoutsCmd `command:"invoicepayouts" description:"(admin) generate paid invoice list for a given date range"` - Login shared.LoginCmd `command:"login" description:"(public) login to Politeia"` - Logout shared.LogoutCmd `command:"logout" description:"(public) logout of Politeia"` - CMSManageUser CMSManageUserCmd `command:"cmsmanageuser" description:"(admin) edit certain properties of the specified user"` - ManageUser shared.ManageUserCmd `command:"manageuser" description:"(admin) edit certain properties of the specified user"` - Me shared.MeCmd `command:"me" description:"(user) get user details for the logged in user"` - NewComment shared.NewCommentCmd `command:"newcomment" description:"(user) create a new comment"` - NewDCC NewDCCCmd `command:"newdcc" description:"(user) creates a new dcc proposal"` - NewDCCComment NewDCCCommentCmd `command:"newdcccomment" description:"(user) creates a new comment on a dcc proposal"` - NewInvoice NewInvoiceCmd `command:"newinvoice" description:"(user) create a new invoice"` - PayInvoices PayInvoicesCmd `command:"payinvoices" description:"(admin) set all approved invoices to paid"` - Policy PolicyCmd `command:"policy" description:"(public) get the server policy"` - ProposalOwner ProposalOwnerCmd `command:"proposalowner" description:"(user) get owners of a proposal"` - ProposalBilling ProposalBillingCmd `command:"proposalbilling" description:"(user) get billing information for a proposal"` - RegisterUser RegisterUserCmd `command:"register" description:"(public) register an invited user to cms"` - ResetPassword shared.ResetPasswordCmd `command:"resetpassword" description:"(public) reset the password for a user that is not logged in"` - SetDCCStatus SetDCCStatusCmd `command:"setdccstatus" description:"(admin) set the status of a DCC"` - SetInvoiceStatus SetInvoiceStatusCmd `command:"setinvoicestatus" description:"(admin) set the status of an invoice"` - StartVote StartVoteCmd `command:"startvote" description:"(admin) start the voting period on a dcc"` - SupportOpposeDCC SupportOpposeDCCCmd `command:"supportopposedcc" description:"(user) support or oppose a given DCC"` - TestRun TestRunCmd `command:"testrun" description:" test cmswww routes"` - TokenInventory shared.TokenInventoryCmd `command:"tokeninventory" description:"(user) get the censorship record tokens of all proposals (passthrough)"` - UpdateUserKey shared.UpdateUserKeyCmd `command:"updateuserkey" description:"(user) generate a new identity for the logged in user"` - UserDetails UserDetailsCmd `command:"userdetails" description:"(user) get current cms user details"` - UserInvoices UserInvoicesCmd `command:"userinvoices" description:"(user) get all invoices submitted by a specific user"` - UserSubContractors UserSubContractorsCmd `command:"usersubcontractors" description:"(user) get all users that are linked to the user"` - Users shared.UsersCmd `command:"users" description:"(user) get a list of users"` - Secret shared.SecretCmd `command:"secret" description:"(user) ping politeiawww"` - Version shared.VersionCmd `command:"version" description:"(public) get server info and CSRF token"` - VoteDCC VoteDCCCmd `command:"votedcc" description:"(user) vote for a given DCC during an all contractor vote"` - VoteDetails VoteDetailsCmd `command:"votedetails" description:"(user) get the details for a dcc vote"` + ActiveVotes ActiveVotesCmd `command:"activevotes" description:"(user) get the dccs that are being voted on"` + AdminInvoices AdminInvoicesCmd `command:"admininvoices" description:"(admin) get all invoices (optional by month/year and/or status)"` + BatchProposals shared.BatchProposalsCmd `command:"batchproposals" description:"(user) retrieve a set of proposals"` + CensorComment shared.CensorCommentCmd `command:"censorcomment" description:"(admin) censor a comment"` + ChangePassword shared.ChangePasswordCmd `command:"changepassword" description:"(user) change the password for the logged in user"` + ChangeUsername shared.ChangeUsernameCmd `command:"changeusername" description:"(user) change the username for the logged in user"` + CMSUsers CMSUsersCmd `command:"cmsusers" description:"(user) get a list of cms users"` + DCCComments DCCCommentsCmd `command:"dcccomments" description:"(user) get the comments for a dcc proposal"` + DCCDetails DCCDetailsCmd `command:"dccdetails" description:"(user) get the details of a dcc"` + EditInvoice EditInvoiceCmd `command:"editinvoice" description:"(user) edit a invoice"` + EditUser EditUserCmd `command:"edituser" description:"(user) edit current cms user information"` + GeneratePayouts GeneratePayoutsCmd `command:"generatepayouts" description:"(admin) generate a list of payouts with addresses and amounts to pay"` + GetDCCs GetDCCsCmd `command:"getdccs" description:"(user) get all dccs (optional by status)"` + Help HelpCmd `command:"help" description:" print a detailed help message for a specific command"` + InvoiceComments InvoiceCommentsCmd `command:"invoicecomments" description:"(user) get the comments for a invoice"` + InvoiceExchangeRate InvoiceExchangeRateCmd `command:"invoiceexchangerate" description:"(user) get exchange rate for a given month/year"` + InviteNewUser InviteNewUserCmd `command:"invite" description:"(admin) invite a new user"` + InvoiceDetails InvoiceDetailsCmd `command:"invoicedetails" description:"(public) get the details of a proposal"` + InvoicePayouts InvoicePayoutsCmd `command:"invoicepayouts" description:"(admin) generate paid invoice list for a given date range"` + Login shared.LoginCmd `command:"login" description:"(public) login to Politeia"` + Logout shared.LogoutCmd `command:"logout" description:"(public) logout of Politeia"` + CMSManageUser CMSManageUserCmd `command:"cmsmanageuser" description:"(admin) edit certain properties of the specified user"` + ManageUser shared.ManageUserCmd `command:"manageuser" description:"(admin) edit certain properties of the specified user"` + Me shared.MeCmd `command:"me" description:"(user) get user details for the logged in user"` + NewComment shared.NewCommentCmd `command:"newcomment" description:"(user) create a new comment"` + NewDCC NewDCCCmd `command:"newdcc" description:"(user) creates a new dcc proposal"` + NewDCCComment NewDCCCommentCmd `command:"newdcccomment" description:"(user) creates a new comment on a dcc proposal"` + NewInvoice NewInvoiceCmd `command:"newinvoice" description:"(user) create a new invoice"` + PayInvoices PayInvoicesCmd `command:"payinvoices" description:"(admin) set all approved invoices to paid"` + Policy PolicyCmd `command:"policy" description:"(public) get the server policy"` + ProposalOwner ProposalOwnerCmd `command:"proposalowner" description:"(user) get owners of a proposal"` + ProposalBilling ProposalBillingCmd `command:"proposalbilling" description:"(user) get billing information for a proposal"` + ProposalBillingDetails ProposalBillingDetailsCmd `command:"proposalbillingdetails" description:"(admin) get billing information for a proposal"` + ProposalBillingSummary ProposalBillingSummaryCmd `command:"proposalbillingsummary" description:"(admin) get all approved proposal billing information"` + RegisterUser RegisterUserCmd `command:"register" description:"(public) register an invited user to cms"` + ResetPassword shared.ResetPasswordCmd `command:"resetpassword" description:"(public) reset the password for a user that is not logged in"` + SetDCCStatus SetDCCStatusCmd `command:"setdccstatus" description:"(admin) set the status of a DCC"` + SetInvoiceStatus SetInvoiceStatusCmd `command:"setinvoicestatus" description:"(admin) set the status of an invoice"` + StartVote StartVoteCmd `command:"startvote" description:"(admin) start the voting period on a dcc"` + SupportOpposeDCC SupportOpposeDCCCmd `command:"supportopposedcc" description:"(user) support or oppose a given DCC"` + TestRun TestRunCmd `command:"testrun" description:" test cmswww routes"` + TokenInventory shared.TokenInventoryCmd `command:"tokeninventory" description:"(user) get the censorship record tokens of all proposals (passthrough)"` + UpdateUserKey shared.UpdateUserKeyCmd `command:"updateuserkey" description:"(user) generate a new identity for the logged in user"` + UserDetails UserDetailsCmd `command:"userdetails" description:"(user) get current cms user details"` + UserInvoices UserInvoicesCmd `command:"userinvoices" description:"(user) get all invoices submitted by a specific user"` + UserSubContractors UserSubContractorsCmd `command:"usersubcontractors" description:"(user) get all users that are linked to the user"` + Users shared.UsersCmd `command:"users" description:"(user) get a list of users"` + Secret shared.SecretCmd `command:"secret" description:"(user) ping politeiawww"` + Version shared.VersionCmd `command:"version" description:"(public) get server info and CSRF token"` + VoteDCC VoteDCCCmd `command:"votedcc" description:"(user) vote for a given DCC during an all contractor vote"` + VoteDetails VoteDetailsCmd `command:"votedetails" description:"(user) get the details for a dcc vote"` } // verifyInvoice verifies a invoice's merkle root, author signature, and diff --git a/politeiawww/cmd/cmswww/proposalbillingdetails.go b/politeiawww/cmd/cmswww/proposalbillingdetails.go new file mode 100644 index 000000000..525685f0c --- /dev/null +++ b/politeiawww/cmd/cmswww/proposalbillingdetails.go @@ -0,0 +1,32 @@ +// Copyright (c) 2017-2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + v1 "github.com/decred/politeia/politeiawww/api/cms/v1" + "github.com/decred/politeia/politeiawww/cmd/shared" +) + +// ProposalBillingCmd gets the invoices for the specified user. +type ProposalBillingDetailsCmd struct { + Args struct { + Token string `positional-arg-name:"token"` // User ID + } `positional-args:"true" required:"true"` +} + +// Execute executes the user invoices command. +func (cmd *ProposalBillingDetailsCmd) Execute(args []string) error { + // Get user invoices + pbr, err := client.ProposalBillingDetails( + &v1.ProposalBillingDetails{ + Token: cmd.Args.Token, + }) + if err != nil { + return err + } + + // Print user invoices + return shared.PrintJSON(pbr) +} diff --git a/politeiawww/cmd/cmswww/proposalbillingsummary.go b/politeiawww/cmd/cmswww/proposalbillingsummary.go new file mode 100644 index 000000000..02bcc1f47 --- /dev/null +++ b/politeiawww/cmd/cmswww/proposalbillingsummary.go @@ -0,0 +1,34 @@ +// Copyright (c) 2017-2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + v1 "github.com/decred/politeia/politeiawww/api/cms/v1" + "github.com/decred/politeia/politeiawww/cmd/shared" +) + +// ProposalBillingCmd gets the invoices for the specified user. +type ProposalBillingSummaryCmd struct { + Args struct { + } `positional-args:"true" required:"true"` + Offset int `long:"offset" optional:"true"` // Offset length + Count int `long:"count" optional:"true"` // Page size +} + +// Execute executes the user invoices command. +func (cmd *ProposalBillingSummaryCmd) Execute(args []string) error { + // Get user invoices + pbsr, err := client.ProposalBillingSummary( + &v1.ProposalBillingSummary{ + Offset: cmd.Offset, + Count: cmd.Count, + }) + if err != nil { + return err + } + + // Print user invoices + return shared.PrintJSON(pbsr) +} diff --git a/politeiawww/cmd/shared/client.go b/politeiawww/cmd/shared/client.go index 73543e089..e2cd734e4 100644 --- a/politeiawww/cmd/shared/client.go +++ b/politeiawww/cmd/shared/client.go @@ -896,6 +896,54 @@ func (c *Client) ProposalBilling(pb *cms.ProposalBilling) (*cms.ProposalBillingR return &pbr, nil } +// ProposalBillingDetails retrieves the billing for the requested proposal +func (c *Client) ProposalBillingDetails(pbd *cms.ProposalBillingDetails) (*cms.ProposalBillingDetailsReply, error) { + responseBody, err := c.makeRequest(http.MethodPost, + cms.APIRoute, cms.RouteProposalBillingDetails, pbd) + if err != nil { + return nil, err + } + + var pbdr cms.ProposalBillingDetailsReply + err = json.Unmarshal(responseBody, &pbdr) + if err != nil { + return nil, fmt.Errorf("unmarshal ProposalBillingDetailsReply: %v", err) + } + + if c.cfg.Verbose { + err := prettyPrintJSON(pbdr) + if err != nil { + return nil, err + } + } + + return &pbdr, nil +} + +// ProposalBillingSummary retrieves the billing for all approved proposals. +func (c *Client) ProposalBillingSummary(pbd *cms.ProposalBillingSummary) (*cms.ProposalBillingSummaryReply, error) { + responseBody, err := c.makeRequest(http.MethodGet, + cms.APIRoute, cms.RouteProposalBillingSummary, pbd) + if err != nil { + return nil, err + } + + var pbdr cms.ProposalBillingSummaryReply + err = json.Unmarshal(responseBody, &pbdr) + if err != nil { + return nil, fmt.Errorf("unmarshal ProposalBillingSummaryReply: %v", err) + } + + if c.cfg.Verbose { + err := prettyPrintJSON(pbdr) + if err != nil { + return nil, err + } + } + + return &pbdr, nil +} + // AdminInvoices retrieves invoices base on possible field set in the request // month/year and/or status func (c *Client) AdminInvoices(ai *cms.AdminInvoices) (*cms.AdminInvoicesReply, error) { diff --git a/politeiawww/cmsdatabase/cockroachdb/cockroachdb.go b/politeiawww/cmsdatabase/cockroachdb/cockroachdb.go index f083d856f..ddd88ac03 100644 --- a/politeiawww/cmsdatabase/cockroachdb/cockroachdb.go +++ b/politeiawww/cmsdatabase/cockroachdb/cockroachdb.go @@ -396,6 +396,8 @@ func (c *cockroachdb) InvoicesByLineItemsProposalToken(token string) ([]*databas invoices.year, invoices.user_id, invoices.public_key, + invoices.contractor_rate, + invoices.exchange_rate, line_items.invoice_token, line_items.type, line_items.domain, @@ -404,7 +406,7 @@ func (c *cockroachdb) InvoicesByLineItemsProposalToken(token string) ([]*databas line_items.proposal_url, line_items.labor, line_items.expenses, - line_items.contractor_rate + line_items.contractor_rate AS sub_rate FROM invoices INNER JOIN line_items ON invoices.token = line_items.invoice_token @@ -447,6 +449,8 @@ type MatchingLineItems struct { Expenses uint ContractorRate uint PublicKey string + ExchangeRate uint + SubRate uint } // Close satisfies the database interface. diff --git a/politeiawww/cmsdatabase/cockroachdb/encoding.go b/politeiawww/cmsdatabase/cockroachdb/encoding.go index 596270c70..e13e89b60 100644 --- a/politeiawww/cmsdatabase/cockroachdb/encoding.go +++ b/politeiawww/cmsdatabase/cockroachdb/encoding.go @@ -265,15 +265,17 @@ func convertMatchingLineItemToInvoices(matching []MatchingLineItems) []*database Labor: vv.Labor, Expenses: vv.Expenses, ProposalURL: vv.ProposalURL, - ContractorRate: vv.ContractorRate, + ContractorRate: vv.SubRate, } inv := &database.Invoice{ - PublicKey: vv.PublicKey, - Token: vv.InvoiceToken, - Month: vv.Month, - Year: vv.Year, - UserID: vv.UserID, - LineItems: li, + PublicKey: vv.PublicKey, + Token: vv.InvoiceToken, + Month: vv.Month, + Year: vv.Year, + UserID: vv.UserID, + LineItems: li, + ContractorRate: vv.ContractorRate, + ExchangeRate: vv.ExchangeRate, } dbInvoices = append(dbInvoices, inv) } diff --git a/politeiawww/cmswww.go b/politeiawww/cmswww.go index 9a1980ca6..f9f0cedb7 100644 --- a/politeiawww/cmswww.go +++ b/politeiawww/cmswww.go @@ -962,6 +962,53 @@ func (p *politeiawww) handlePassThroughBatchProposals(w http.ResponseWriter, r * util.RespondRaw(w, http.StatusOK, data) } +func (p *politeiawww) handleProposalBillingSummary(w http.ResponseWriter, r *http.Request) { + log.Tracef("handleProposalBillingSummary") + + var pbs cms.ProposalBillingSummary + // get version from query string parameters + err := util.ParseGetParams(r, &pbs) + if err != nil { + RespondWithError(w, r, 0, "handleProposalBillingSummary: ParseGetParams", + www.UserError{ + ErrorCode: www.ErrorStatusInvalidInput, + }) + return + } + + pbsr, err := p.processProposalBillingSummary(pbs) + if err != nil { + RespondWithError(w, r, 0, + "handleProposalBillingSummary: processProposalBillingSummary %v", err) + return + } + + util.RespondWithJSON(w, http.StatusOK, pbsr) +} + +func (p *politeiawww) handleProposalBillingDetails(w http.ResponseWriter, r *http.Request) { + log.Tracef("handleProposalBillingDetails") + + var pbd cms.ProposalBillingDetails + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&pbd); err != nil { + RespondWithError(w, r, 0, "handleProposalBillingDetails: unmarshal", + www.UserError{ + ErrorCode: www.ErrorStatusInvalidInput, + }) + return + } + + svr, err := p.processProposalBillingDetails(pbd) + if err != nil { + RespondWithError(w, r, 0, + "handleProposalBillingDetails: processProposalBillingDetails %v", err) + return + } + + util.RespondWithJSON(w, http.StatusOK, svr) +} + // makeProposalsRequest submits pass through requests to the proposals sites // (testnet or mainnet). It takes a http method type, proposals route and a // request interface as arguments. It returns the response body as byte array @@ -1175,4 +1222,10 @@ func (p *politeiawww) setCMSWWWRoutes() { p.addRoute(http.MethodPost, cms.APIRoute, cms.RouteStartVoteDCC, p.handleStartVoteDCC, permissionAdmin) + p.addRoute(http.MethodGet, cms.APIRoute, + cms.RouteProposalBillingSummary, p.handleProposalBillingSummary, + permissionAdmin) + p.addRoute(http.MethodPost, cms.APIRoute, + cms.RouteProposalBillingDetails, p.handleProposalBillingDetails, + permissionAdmin) } diff --git a/politeiawww/invoices.go b/politeiawww/invoices.go index dd1961ad9..c1396d4b3 100644 --- a/politeiawww/invoices.go +++ b/politeiawww/invoices.go @@ -1786,3 +1786,140 @@ func parseInvoiceInput(files []www.File) (*cms.InvoiceInput, error) { } return &invInput, nil } + +func (p *politeiawww) processProposalBillingSummary(pbs cms.ProposalBillingSummary) (*cms.ProposalBillingSummaryReply, error) { + reply := &cms.ProposalBillingSummaryReply{} + + data, err := p.makeProposalsRequest(http.MethodGet, www.RouteTokenInventory, nil) + if err != nil { + return nil, err + } + + var tvr www.TokenInventoryReply + err = json.Unmarshal(data, &tvr) + if err != nil { + return nil, err + } + + approvedProposals := tvr.Approved + + var bpr www.BatchProposalsReply + if len(approvedProposals) > 0 { + // Go fetch proposal information to get name/title. + bp := &www.BatchProposals{ + Tokens: approvedProposals, + } + + data, err := p.makeProposalsRequest(http.MethodPost, www.RouteBatchProposals, bp) + if err != nil { + return nil, err + } + + err = json.Unmarshal(data, &bpr) + if err != nil { + return nil, err + } + } + + count := pbs.Count + if count > cms.ProposalBillingListPageSize { + count = cms.ProposalBillingListPageSize + } + + proposalInvoices := make(map[string][]*database.Invoice, len(approvedProposals)) + for i, prop := range approvedProposals { + if i < pbs.Offset { + continue + } + propInvoices, err := p.cmsDB.InvoicesByLineItemsProposalToken(prop) + if err != nil { + return nil, err + } + if len(propInvoices) > 0 { + proposalInvoices[prop] = propInvoices + } else { + proposalInvoices[prop] = make([]*database.Invoice, 0) + } + + if count != 0 && len(proposalInvoices) >= count { + break + } + } + + spendingSummaries := make([]cms.ProposalSpending, 0, len(proposalInvoices)) + for prop, invoices := range proposalInvoices { + spendingSummary := cms.ProposalSpending{} + spendingSummary.Token = prop + + totalSpent := int64(0) + for _, dbInv := range invoices { + payout, err := calculatePayout(*dbInv) + if err != nil { + return nil, err + } + totalSpent += int64(payout.Total) + } + // Look across approved proposals batch reply for proposal name. + for _, propDetails := range bpr.Proposals { + if propDetails.CensorshipRecord.Token == prop { + spendingSummary.Title = propDetails.Name + break + } + } + spendingSummary.TotalBilled = totalSpent + spendingSummaries = append(spendingSummaries, spendingSummary) + } + + reply.Proposals = spendingSummaries + + return reply, nil +} + +func (p *politeiawww) processProposalBillingDetails(pbd cms.ProposalBillingDetails) (*cms.ProposalBillingDetailsReply, error) { + reply := &cms.ProposalBillingDetailsReply{} + + propInvoices, err := p.cmsDB.InvoicesByLineItemsProposalToken(pbd.Token) + if err != nil { + return nil, err + } + + spendingSummary := cms.ProposalSpending{} + spendingSummary.Token = pbd.Token + + invRecs := make([]cms.InvoiceRecord, 0, len(propInvoices)) + totalSpent := int64(0) + for _, dbInv := range propInvoices { + u, err := p.db.UserGetByPubKey(dbInv.PublicKey) + if err != nil { + log.Errorf("getUserByPubKey: token:%v "+ + "pubKey:%v err:%v", dbInv.PublicKey, err) + } else { + dbInv.Username = u.Username + } + payout, err := calculatePayout(*dbInv) + if err != nil { + return nil, err + } + totalSpent += int64(payout.Total) + invRecs = append(invRecs, *convertDatabaseInvoiceToInvoiceRecord(*dbInv)) + } + + data, err := p.makeProposalsRequest(http.MethodGet, "/proposals/"+pbd.Token, nil) + if err != nil { + fmt.Println("asdfsdf?") + return nil, err + } + + var pdr www.ProposalDetailsReply + err = json.Unmarshal(data, &pdr) + if err != nil { + return nil, err + } + + spendingSummary.Title = pdr.Proposal.Name + spendingSummary.Invoices = invRecs + spendingSummary.TotalBilled = totalSpent + + reply.Details = spendingSummary + return reply, nil +}