github.com/decred/politeia@v1.4.0/politeiawww/legacy/invoices.go (about)

     1  // Copyright (c) 2019-2020 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package legacy
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"crypto/sha256"
    11  	"encoding/base64"
    12  	"encoding/hex"
    13  	"encoding/json"
    14  	"errors"
    15  	"fmt"
    16  	"net/http"
    17  	"regexp"
    18  	"sort"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/decred/dcrd/dcrutil/v3"
    24  	"github.com/decred/dcrtime/merkle"
    25  	pd "github.com/decred/politeia/politeiad/api/v1"
    26  	"github.com/decred/politeia/politeiad/api/v1/identity"
    27  	"github.com/decred/politeia/politeiad/backend/gitbe/decredplugin"
    28  	cms "github.com/decred/politeia/politeiawww/api/cms/v1"
    29  	www "github.com/decred/politeia/politeiawww/api/www/v1"
    30  	"github.com/decred/politeia/politeiawww/legacy/cmsdatabase"
    31  	database "github.com/decred/politeia/politeiawww/legacy/cmsdatabase"
    32  	"github.com/decred/politeia/politeiawww/legacy/mdstream"
    33  	"github.com/decred/politeia/politeiawww/legacy/user"
    34  	"github.com/decred/politeia/util"
    35  )
    36  
    37  const (
    38  	// invoiceFile contains the file name of the invoice file
    39  	invoiceFile = "invoice.json"
    40  
    41  	// Sanity check for Contractor Rates
    42  	minRate = 500   // 5 USD (in cents)
    43  	maxRate = 50000 // 500 USD (in cents)
    44  
    45  	domainInvoiceLimit = time.Minute * 60 * 24 * 180 // 180 days in minutes
    46  )
    47  
    48  var (
    49  	// This covers the possible valid status transitions for any invoices.  If
    50  	// a current invoice's status does not fall into these 3 categories, then
    51  	// an admin will not be able to update their status.  For example,
    52  	// paid or approved invoices cannot have their status changed.
    53  	validStatusTransitions = map[cms.InvoiceStatusT][]cms.InvoiceStatusT{
    54  		// New invoices may only be updated to approved, rejected or disputed.
    55  		cms.InvoiceStatusNew: {
    56  			cms.InvoiceStatusApproved,
    57  			cms.InvoiceStatusRejected,
    58  			cms.InvoiceStatusDisputed,
    59  		},
    60  		// Updated invoices may only be updated to approved, rejected or disputed.
    61  		cms.InvoiceStatusUpdated: {
    62  			cms.InvoiceStatusApproved,
    63  			cms.InvoiceStatusRejected,
    64  			cms.InvoiceStatusDisputed,
    65  		},
    66  	}
    67  	// The invalid contractor types for new invoice submission
    68  	invalidNewInvoiceContractorType = map[cms.ContractorTypeT]bool{
    69  		cms.ContractorTypeNominee:         true,
    70  		cms.ContractorTypeInvalid:         true,
    71  		cms.ContractorTypeSubContractor:   true,
    72  		cms.ContractorTypeTempDeactivated: true,
    73  	}
    74  
    75  	// The valid contractor types for domain invoice viewing
    76  	validDomainInvoiceViewingContractorType = map[cms.ContractorTypeT]bool{
    77  		cms.ContractorTypeDirect:        true,
    78  		cms.ContractorTypeSupervisor:    true,
    79  		cms.ContractorTypeSubContractor: true,
    80  	}
    81  
    82  	validInvoiceField = regexp.MustCompile(createInvoiceFieldRegex())
    83  	validName         = regexp.MustCompile(createNameRegex())
    84  	validLocation     = regexp.MustCompile(createLocationRegex())
    85  	validContact      = regexp.MustCompile(createContactRegex())
    86  )
    87  
    88  func convertPDFileFromWWW(f www.File) pd.File {
    89  	return pd.File{
    90  		Name:    f.Name,
    91  		MIME:    f.MIME,
    92  		Digest:  f.Digest,
    93  		Payload: f.Payload,
    94  	}
    95  }
    96  
    97  func convertPDFilesFromWWW(f []www.File) []pd.File {
    98  	files := make([]pd.File, 0, len(f))
    99  	for _, v := range f {
   100  		files = append(files, convertPDFileFromWWW(v))
   101  	}
   102  	return files
   103  }
   104  
   105  func convertPDCensorFromWWW(f www.CensorshipRecord) pd.CensorshipRecord {
   106  	return pd.CensorshipRecord{
   107  		Token:     f.Token,
   108  		Merkle:    f.Merkle,
   109  		Signature: f.Signature,
   110  	}
   111  }
   112  
   113  func convertWWWFileFromPD(f pd.File) www.File {
   114  	return www.File{
   115  		Name:    f.Name,
   116  		MIME:    f.MIME,
   117  		Digest:  f.Digest,
   118  		Payload: f.Payload,
   119  	}
   120  }
   121  
   122  func convertWWWFilesFromPD(f []pd.File) []www.File {
   123  	files := make([]www.File, 0, len(f))
   124  	for _, v := range f {
   125  		files = append(files, convertWWWFileFromPD(v))
   126  	}
   127  	return files
   128  }
   129  
   130  func convertWWWCensorFromPD(f pd.CensorshipRecord) www.CensorshipRecord {
   131  	return www.CensorshipRecord{
   132  		Token:     f.Token,
   133  		Merkle:    f.Merkle,
   134  		Signature: f.Signature,
   135  	}
   136  }
   137  
   138  func convertNewCommentToDecredPlugin(nc www.NewComment) decredplugin.NewComment {
   139  	return decredplugin.NewComment{
   140  		Token:     nc.Token,
   141  		ParentID:  nc.ParentID,
   142  		Comment:   nc.Comment,
   143  		Signature: nc.Signature,
   144  		PublicKey: nc.PublicKey,
   145  	}
   146  }
   147  
   148  func convertCommentFromDecred(c decredplugin.Comment) www.Comment {
   149  	// Upvotes, Downvotes, UserID, and Username are filled in as zero
   150  	// values since a decred plugin comment does not contain this data.
   151  	return www.Comment{
   152  		Token:       c.Token,
   153  		ParentID:    c.ParentID,
   154  		Comment:     c.Comment,
   155  		Signature:   c.Signature,
   156  		PublicKey:   c.PublicKey,
   157  		CommentID:   c.CommentID,
   158  		Receipt:     c.Receipt,
   159  		Timestamp:   c.Timestamp,
   160  		ResultVotes: 0,
   161  		Upvotes:     0,
   162  		Downvotes:   0,
   163  		UserID:      "",
   164  		Username:    "",
   165  		Censored:    c.Censored,
   166  	}
   167  }
   168  
   169  func convertDatabaseInvoiceToInvoiceRecord(dbInvoice cmsdatabase.Invoice) (cms.InvoiceRecord, error) {
   170  	invRec := cms.InvoiceRecord{}
   171  	invRec.Status = dbInvoice.Status
   172  	invRec.Timestamp = dbInvoice.Timestamp
   173  	invRec.UserID = dbInvoice.UserID
   174  	invRec.Username = dbInvoice.Username
   175  	invRec.PublicKey = dbInvoice.PublicKey
   176  	invRec.Version = dbInvoice.Version
   177  	invRec.Signature = dbInvoice.UserSignature
   178  	invRec.CensorshipRecord = www.CensorshipRecord{
   179  		Token: dbInvoice.Token,
   180  	}
   181  	invInput := cms.InvoiceInput{
   182  		ContractorContact:  dbInvoice.ContractorContact,
   183  		ContractorRate:     dbInvoice.ContractorRate,
   184  		ContractorName:     dbInvoice.ContractorName,
   185  		ContractorLocation: dbInvoice.ContractorLocation,
   186  		PaymentAddress:     dbInvoice.PaymentAddress,
   187  		Month:              dbInvoice.Month,
   188  		Year:               dbInvoice.Year,
   189  		ExchangeRate:       dbInvoice.ExchangeRate,
   190  	}
   191  	invInputLineItems := make([]cms.LineItemsInput, 0, len(dbInvoice.LineItems))
   192  	for _, dbLineItem := range dbInvoice.LineItems {
   193  		lineItem := cms.LineItemsInput{
   194  			Type:          dbLineItem.Type,
   195  			Domain:        dbLineItem.Domain,
   196  			Subdomain:     dbLineItem.Subdomain,
   197  			Description:   dbLineItem.Description,
   198  			ProposalToken: dbLineItem.ProposalURL,
   199  			Labor:         dbLineItem.Labor,
   200  			Expenses:      dbLineItem.Expenses,
   201  			SubRate:       dbLineItem.ContractorRate,
   202  			SubUserID:     dbLineItem.SubUserID,
   203  		}
   204  		invInputLineItems = append(invInputLineItems, lineItem)
   205  	}
   206  
   207  	payout, err := calculatePayout(dbInvoice)
   208  	if err != nil {
   209  		return invRec, err
   210  	}
   211  	invRec.Total = int64(payout.Total)
   212  
   213  	invInput.LineItems = invInputLineItems
   214  	invRec.Input = invInput
   215  	invRec.Input.LineItems = invInputLineItems
   216  	txIDs := strings.Split(dbInvoice.Payments.TxIDs, ",")
   217  	payment := cms.PaymentInformation{
   218  		Token:           dbInvoice.Payments.InvoiceToken,
   219  		Address:         dbInvoice.Payments.Address,
   220  		TxIDs:           txIDs,
   221  		AmountReceived:  dcrutil.Amount(dbInvoice.Payments.AmountReceived),
   222  		TimeLastUpdated: dbInvoice.Payments.TimeLastUpdated,
   223  	}
   224  	invRec.Payment = payment
   225  	return invRec, nil
   226  }
   227  
   228  func convertInvoiceRecordToDatabaseInvoice(invRec *cms.InvoiceRecord) *cmsdatabase.Invoice {
   229  	dbInvoice := &cmsdatabase.Invoice{}
   230  	dbInvoice.Status = invRec.Status
   231  	dbInvoice.Timestamp = invRec.Timestamp
   232  	dbInvoice.UserID = invRec.UserID
   233  	dbInvoice.PublicKey = invRec.PublicKey
   234  	dbInvoice.Version = invRec.Version
   235  	dbInvoice.ContractorContact = invRec.Input.ContractorContact
   236  	dbInvoice.ContractorRate = invRec.Input.ContractorRate
   237  	dbInvoice.ContractorName = invRec.Input.ContractorName
   238  	dbInvoice.ContractorLocation = invRec.Input.ContractorLocation
   239  	dbInvoice.PaymentAddress = invRec.Input.PaymentAddress
   240  	dbInvoice.Month = invRec.Input.Month
   241  	dbInvoice.Year = invRec.Input.Year
   242  	dbInvoice.ExchangeRate = invRec.Input.ExchangeRate
   243  	dbInvoice.Token = invRec.CensorshipRecord.Token
   244  	dbInvoice.ServerSignature = invRec.Signature
   245  
   246  	dbInvoice.LineItems = make([]cmsdatabase.LineItem, 0, len(invRec.Input.LineItems))
   247  	for _, lineItem := range invRec.Input.LineItems {
   248  		dbLineItem := cmsdatabase.LineItem{
   249  			Type:           lineItem.Type,
   250  			Domain:         lineItem.Domain,
   251  			Subdomain:      lineItem.Subdomain,
   252  			Description:    lineItem.Description,
   253  			ProposalURL:    lineItem.ProposalToken,
   254  			Labor:          lineItem.Labor,
   255  			Expenses:       lineItem.Expenses,
   256  			ContractorRate: lineItem.SubRate,
   257  			SubUserID:      lineItem.SubUserID,
   258  		}
   259  		dbInvoice.LineItems = append(dbInvoice.LineItems, dbLineItem)
   260  	}
   261  	return dbInvoice
   262  }
   263  
   264  func convertLineItemsToDatabase(token string, l []cms.LineItemsInput) []cmsdatabase.LineItem {
   265  	dl := make([]cmsdatabase.LineItem, 0, len(l))
   266  	for _, v := range l {
   267  		dl = append(dl, cmsdatabase.LineItem{
   268  			InvoiceToken: token,
   269  			Type:         v.Type,
   270  			Domain:       v.Domain,
   271  			Subdomain:    v.Subdomain,
   272  			Description:  v.Description,
   273  			ProposalURL:  v.ProposalToken,
   274  			Labor:        v.Labor,
   275  			Expenses:     v.Expenses,
   276  			// If subrate is populated, use the existing contractor rate field.
   277  			ContractorRate: v.SubRate,
   278  			SubUserID:      v.SubUserID,
   279  		})
   280  	}
   281  	return dl
   282  }
   283  
   284  func convertRecordToDatabaseInvoice(p pd.Record) (*cmsdatabase.Invoice, error) {
   285  	dbInvoice := cmsdatabase.Invoice{
   286  		Files:           convertWWWFilesFromPD(p.Files),
   287  		Token:           p.CensorshipRecord.Token,
   288  		ServerSignature: p.CensorshipRecord.Signature,
   289  		Version:         p.Version,
   290  	}
   291  
   292  	// Decode invoice file
   293  	for _, v := range p.Files {
   294  		if v.Name == invoiceFile {
   295  			b, err := base64.StdEncoding.DecodeString(v.Payload)
   296  			if err != nil {
   297  				return nil, err
   298  			}
   299  
   300  			var ii cms.InvoiceInput
   301  			err = json.Unmarshal(b, &ii)
   302  			if err != nil {
   303  				return nil, www.UserError{
   304  					ErrorCode: www.ErrorStatusInvalidInput,
   305  				}
   306  			}
   307  
   308  			dbInvoice.Month = ii.Month
   309  			dbInvoice.Year = ii.Year
   310  			dbInvoice.ExchangeRate = ii.ExchangeRate
   311  			dbInvoice.LineItems = convertLineItemsToDatabase(dbInvoice.Token,
   312  				ii.LineItems)
   313  			dbInvoice.ContractorContact = ii.ContractorContact
   314  			dbInvoice.ContractorLocation = ii.ContractorLocation
   315  			dbInvoice.ContractorRate = ii.ContractorRate
   316  			dbInvoice.ContractorName = ii.ContractorName
   317  			dbInvoice.PaymentAddress = ii.PaymentAddress
   318  		}
   319  	}
   320  	payout, err := calculatePayout(dbInvoice)
   321  	if err != nil {
   322  		return nil, err
   323  	}
   324  	payment := cmsdatabase.Payments{
   325  		InvoiceToken: dbInvoice.Token,
   326  		Address:      dbInvoice.PaymentAddress,
   327  		AmountNeeded: int64(payout.DCRTotal),
   328  	}
   329  	for _, m := range p.Metadata {
   330  		switch m.ID {
   331  		case mdstream.IDRecordStatusChange:
   332  			// Ignore initial stream change since it's just the automatic change from
   333  			// unvetted to vetted
   334  			continue
   335  		case mdstream.IDInvoiceGeneral:
   336  			var mdGeneral mdstream.InvoiceGeneral
   337  			err := json.Unmarshal([]byte(m.Payload), &mdGeneral)
   338  			if err != nil {
   339  				return nil, fmt.Errorf("could not decode metadata '%v' token '%v': %v",
   340  					p.Metadata, p.CensorshipRecord.Token, err)
   341  			}
   342  
   343  			dbInvoice.Timestamp = mdGeneral.Timestamp
   344  			dbInvoice.PublicKey = mdGeneral.PublicKey
   345  			dbInvoice.UserSignature = mdGeneral.Signature
   346  		case mdstream.IDInvoiceStatusChange:
   347  			sc, err := mdstream.DecodeInvoiceStatusChange([]byte(m.Payload))
   348  			if err != nil {
   349  				return nil, fmt.Errorf("could not decode metadata '%v' token '%v': %v",
   350  					m, p.CensorshipRecord.Token, err)
   351  			}
   352  
   353  			invChanges := make([]cmsdatabase.InvoiceChange, 0, len(sc))
   354  			for _, s := range sc {
   355  				invChange := cmsdatabase.InvoiceChange{
   356  					AdminPublicKey: s.AdminPublicKey,
   357  					NewStatus:      s.NewStatus,
   358  					Reason:         s.Reason,
   359  					Timestamp:      s.Timestamp,
   360  				}
   361  				invChanges = append(invChanges, invChange)
   362  				// Capture information about payments
   363  				dbInvoice.Status = s.NewStatus
   364  				if s.NewStatus == cms.InvoiceStatusApproved {
   365  					payment.Status = cms.PaymentStatusWatching
   366  					payment.TimeStarted = s.Timestamp
   367  				} else if s.NewStatus == cms.InvoiceStatusPaid {
   368  					payment.Status = cms.PaymentStatusPaid
   369  				}
   370  			}
   371  
   372  		case mdstream.IDInvoicePayment:
   373  			ip, err := mdstream.DecodeInvoicePayment([]byte(m.Payload))
   374  			if err != nil {
   375  				return nil, fmt.Errorf("could not decode metadata '%v' token '%v': %v",
   376  					m, p.CensorshipRecord.Token, err)
   377  			}
   378  
   379  			// We don't need all of the payments.
   380  			// Just the most recent one.
   381  			for _, s := range ip {
   382  				payment.TxIDs = s.TxIDs
   383  				payment.TimeLastUpdated = s.Timestamp
   384  				payment.AmountReceived = s.AmountReceived
   385  			}
   386  		default:
   387  			// Log error but proceed
   388  			log.Errorf("initializeInventory: invalid "+
   389  				"metadata stream ID %v token %v",
   390  				m.ID, p.CensorshipRecord.Token)
   391  		}
   392  	}
   393  	dbInvoice.Payments = payment
   394  
   395  	return &dbInvoice, nil
   396  }
   397  
   398  func convertDatabaseInvoiceToProposalLineItems(inv cmsdatabase.Invoice) cms.ProposalLineItems {
   399  	return cms.ProposalLineItems{
   400  		Month:          int(inv.Month),
   401  		Year:           int(inv.Year),
   402  		UserID:         inv.UserID,
   403  		Username:       inv.Username,
   404  		ContractorRate: inv.ContractorRate,
   405  		LineItem: cms.LineItemsInput{
   406  			Type:          inv.LineItems[0].Type,
   407  			Domain:        inv.LineItems[0].Domain,
   408  			Subdomain:     inv.LineItems[0].Subdomain,
   409  			Description:   inv.LineItems[0].Description,
   410  			ProposalToken: inv.LineItems[0].ProposalURL,
   411  			Labor:         inv.LineItems[0].Labor,
   412  			Expenses:      inv.LineItems[0].Expenses,
   413  			SubRate:       inv.LineItems[0].ContractorRate,
   414  		},
   415  	}
   416  }
   417  
   418  // formatInvoiceField normalizes an invoice field without leading and
   419  // trailing spaces.
   420  func formatInvoiceField(field string) string {
   421  	return strings.TrimSpace(field)
   422  }
   423  
   424  // validateInvoiceField verifies that a field filled out in invoice.json is
   425  // valid
   426  func validateInvoiceField(field string) bool {
   427  	if field != formatInvoiceField(field) {
   428  		log.Tracef("validateInvoiceField: not normalized: %s %s",
   429  			field, formatInvoiceField(field))
   430  		return false
   431  	}
   432  	if len(field) > cms.PolicyMaxLineItemColLength ||
   433  		len(field) < cms.PolicyMinLineItemColLength {
   434  		log.Tracef("validateInvoiceField: not within bounds: %s",
   435  			field)
   436  		return false
   437  	}
   438  	if !validInvoiceField.MatchString(field) {
   439  		log.Tracef("validateInvoiceField: not valid: %s %s",
   440  			field, validInvoiceField.String())
   441  		return false
   442  	}
   443  	return true
   444  }
   445  
   446  // createInvoiceFieldRegex generates a regex based on the policy supplied for
   447  // valid characters invoice field.
   448  func createInvoiceFieldRegex() string {
   449  	var buf bytes.Buffer
   450  	buf.WriteString("^[")
   451  
   452  	for _, supportedChar := range cms.PolicyInvoiceFieldSupportedChars {
   453  		if len(supportedChar) > 1 {
   454  			buf.WriteString(supportedChar)
   455  		} else {
   456  			buf.WriteString(`\` + supportedChar)
   457  		}
   458  	}
   459  	buf.WriteString("]{")
   460  	buf.WriteString(strconv.Itoa(cms.PolicyMinLineItemColLength) + ",")
   461  	buf.WriteString(strconv.Itoa(cms.PolicyMaxLineItemColLength) + "}$")
   462  
   463  	return buf.String()
   464  }
   465  
   466  // createNameRegex generates a regex based on the policy supplied for valid
   467  // characters in a user's name.
   468  func createNameRegex() string {
   469  	var buf bytes.Buffer
   470  	buf.WriteString("^[")
   471  
   472  	for _, supportedChar := range cms.PolicyCMSNameLocationSupportedChars {
   473  		if len(supportedChar) > 1 {
   474  			buf.WriteString(supportedChar)
   475  		} else {
   476  			buf.WriteString(`\` + supportedChar)
   477  		}
   478  	}
   479  	buf.WriteString("]{")
   480  	buf.WriteString(strconv.Itoa(cms.PolicyMinNameLength) + ",")
   481  	buf.WriteString(strconv.Itoa(cms.PolicyMaxNameLength) + "}$")
   482  
   483  	return buf.String()
   484  }
   485  
   486  // createLocationRegex generates a regex based on the policy supplied for valid
   487  // characters in a user's location.
   488  func createLocationRegex() string {
   489  	var buf bytes.Buffer
   490  	buf.WriteString("^[")
   491  
   492  	for _, supportedChar := range cms.PolicyCMSNameLocationSupportedChars {
   493  		if len(supportedChar) > 1 {
   494  			buf.WriteString(supportedChar)
   495  		} else {
   496  			buf.WriteString(`\` + supportedChar)
   497  		}
   498  	}
   499  	buf.WriteString("]{")
   500  	buf.WriteString(strconv.Itoa(cms.PolicyMinLocationLength) + ",")
   501  	buf.WriteString(strconv.Itoa(cms.PolicyMaxLocationLength) + "}$")
   502  
   503  	return buf.String()
   504  }
   505  
   506  // createContactRegex generates a regex based on the policy supplied for valid
   507  // characters in a user's contact information.
   508  func createContactRegex() string {
   509  	var buf bytes.Buffer
   510  	buf.WriteString("^[")
   511  
   512  	for _, supportedChar := range cms.PolicyCMSContactSupportedChars {
   513  		if len(supportedChar) > 1 {
   514  			buf.WriteString(supportedChar)
   515  		} else {
   516  			buf.WriteString(`\` + supportedChar)
   517  		}
   518  	}
   519  	buf.WriteString("]{")
   520  	buf.WriteString(strconv.Itoa(cms.PolicyMinContactLength) + ",")
   521  	buf.WriteString(strconv.Itoa(cms.PolicyMaxContactLength) + "}$")
   522  
   523  	return buf.String()
   524  }
   525  
   526  // formatName normalizes a contractor name to lowercase without leading and
   527  // trailing spaces.
   528  func formatName(name string) string {
   529  	return strings.ToLower(strings.TrimSpace(name))
   530  }
   531  
   532  func validateName(name string) error {
   533  	if len(name) < cms.PolicyMinNameLength ||
   534  		len(name) > cms.PolicyMaxNameLength {
   535  		log.Debugf("Name not within bounds: %s", name)
   536  		return www.UserError{
   537  			ErrorCode: cms.ErrorStatusMalformedName,
   538  		}
   539  	}
   540  
   541  	if !validName.MatchString(name) {
   542  		log.Debugf("Name not valid: %s %s", name, validName.String())
   543  		return www.UserError{
   544  			ErrorCode: cms.ErrorStatusMalformedName,
   545  		}
   546  	}
   547  
   548  	return nil
   549  }
   550  
   551  // formatLocation normalizes a contractor location to lowercase without leading and
   552  // trailing spaces.
   553  func formatLocation(location string) string {
   554  	return strings.ToLower(strings.TrimSpace(location))
   555  }
   556  
   557  func validateLocation(location string) error {
   558  	if len(location) < cms.PolicyMinLocationLength ||
   559  		len(location) > cms.PolicyMaxLocationLength {
   560  		log.Debugf("Location not within bounds: %s", location)
   561  		return www.UserError{
   562  			ErrorCode: cms.ErrorStatusMalformedLocation,
   563  		}
   564  	}
   565  
   566  	if !validLocation.MatchString(location) {
   567  		log.Debugf("Location not valid: %s %s", location, validLocation.String())
   568  		return www.UserError{
   569  			ErrorCode: cms.ErrorStatusMalformedLocation,
   570  		}
   571  	}
   572  
   573  	return nil
   574  }
   575  
   576  // formatContact normalizes a contractor contact to lowercase without leading and
   577  // trailing spaces.
   578  func formatContact(contact string) string {
   579  	return strings.ToLower(strings.TrimSpace(contact))
   580  }
   581  
   582  func validateContact(contact string) error {
   583  	if len(contact) < cms.PolicyMinContactLength ||
   584  		len(contact) > cms.PolicyMaxContactLength {
   585  		log.Debugf("Contact not within bounds: %s", contact)
   586  		return www.UserError{
   587  			ErrorCode: cms.ErrorStatusInvoiceMalformedContact,
   588  		}
   589  	}
   590  
   591  	if !validContact.MatchString(contact) {
   592  		log.Debugf("Contact not valid: %s %s", contact, validContact.String())
   593  		return www.UserError{
   594  			ErrorCode: cms.ErrorStatusInvoiceMalformedContact,
   595  		}
   596  	}
   597  
   598  	return nil
   599  }
   600  
   601  // processNewInvoice tries to submit a new invoice to politeiad.
   602  func (p *Politeiawww) processNewInvoice(ctx context.Context, ni cms.NewInvoice, u *user.User) (*cms.NewInvoiceReply, error) {
   603  	log.Tracef("processNewInvoice")
   604  
   605  	cmsUser, err := p.getCMSUserByIDRaw(u.ID.String())
   606  	if err != nil {
   607  		return nil, err
   608  	}
   609  
   610  	// Ensure that the user is not unauthorized to create invoices
   611  	if _, ok := invalidNewInvoiceContractorType[cms.ContractorTypeT(
   612  		cmsUser.ContractorType)]; ok {
   613  		return nil, www.UserError{
   614  			ErrorCode: cms.ErrorStatusInvalidUserNewInvoice,
   615  		}
   616  	}
   617  	err = p.validateInvoice(ni, cmsUser)
   618  	if err != nil {
   619  		return nil, err
   620  	}
   621  
   622  	// Dupe address check.
   623  	invInput, err := parseInvoiceInput(ni.Files)
   624  	if err != nil {
   625  		return nil, err
   626  	}
   627  
   628  	invoiceAddress, err := p.cmsDB.InvoicesByAddress(invInput.PaymentAddress)
   629  	if err != nil {
   630  		return nil, www.UserError{
   631  			ErrorCode: cms.ErrorStatusInvalidPaymentAddress,
   632  		}
   633  	}
   634  	if len(invoiceAddress) > 0 {
   635  		return nil, www.UserError{
   636  			ErrorCode: cms.ErrorStatusDuplicatePaymentAddress,
   637  		}
   638  	}
   639  
   640  	m := mdstream.InvoiceGeneral{
   641  		Version:   mdstream.VersionInvoiceGeneral,
   642  		Timestamp: time.Now().Unix(),
   643  		PublicKey: ni.PublicKey,
   644  		Signature: ni.Signature,
   645  	}
   646  	md, err := mdstream.EncodeInvoiceGeneral(m)
   647  	if err != nil {
   648  		return nil, err
   649  	}
   650  
   651  	sc := mdstream.InvoiceStatusChange{
   652  		Version:   mdstream.IDInvoiceStatusChange,
   653  		Timestamp: time.Now().Unix(),
   654  		NewStatus: cms.InvoiceStatusNew,
   655  	}
   656  	scb, err := mdstream.EncodeInvoiceStatusChange(sc)
   657  	if err != nil {
   658  		return nil, err
   659  	}
   660  
   661  	// Setup politeiad request
   662  	challenge, err := util.Random(pd.ChallengeSize)
   663  	if err != nil {
   664  		return nil, err
   665  	}
   666  	n := pd.NewRecord{
   667  		Challenge: hex.EncodeToString(challenge),
   668  		Metadata: []pd.MetadataStream{
   669  			{
   670  				ID:      mdstream.IDInvoiceGeneral,
   671  				Payload: string(md),
   672  			},
   673  			{
   674  				ID:      mdstream.IDInvoiceStatusChange,
   675  				Payload: string(scb),
   676  			},
   677  		},
   678  		Files: convertPDFilesFromWWW(ni.Files),
   679  	}
   680  
   681  	// Handle test case
   682  	if p.test {
   683  		tokenBytes, err := util.Random(pd.TokenSize)
   684  		if err != nil {
   685  			return nil, err
   686  		}
   687  
   688  		testReply := pd.NewRecordReply{
   689  			CensorshipRecord: pd.CensorshipRecord{
   690  				Token: hex.EncodeToString(tokenBytes),
   691  			},
   692  		}
   693  
   694  		return &cms.NewInvoiceReply{
   695  			CensorshipRecord: convertWWWCensorFromPD(testReply.CensorshipRecord),
   696  		}, nil
   697  	}
   698  
   699  	// Send the newrecord politeiad request
   700  	responseBody, err := p.makeRequest(ctx, http.MethodPost,
   701  		pd.NewRecordRoute, n)
   702  	if err != nil {
   703  		return nil, err
   704  	}
   705  
   706  	log.Infof("Submitted invoice: %v %v-%v",
   707  		u.Username, ni.Month, ni.Year)
   708  	for k, f := range n.Files {
   709  		log.Infof("%02v: %v %v", k, f.Name, f.Digest)
   710  	}
   711  
   712  	// Handle newRecord response
   713  	var pdReply pd.NewRecordReply
   714  	err = json.Unmarshal(responseBody, &pdReply)
   715  	if err != nil {
   716  		return nil, fmt.Errorf("Unmarshal NewInvoiceReply: %v", err)
   717  	}
   718  
   719  	// Verify NewRecord challenge
   720  	err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response)
   721  	if err != nil {
   722  		return nil, err
   723  	}
   724  
   725  	// Change politeiad record status to public. Invoices
   726  	// do not need to be reviewed before becoming public.
   727  	// An admin pubkey and signature are not included for
   728  	// this reason.
   729  	c := mdstream.RecordStatusChangeV2{
   730  		Version:   mdstream.VersionRecordStatusChange,
   731  		Timestamp: time.Now().Unix(),
   732  		NewStatus: pd.RecordStatusPublic,
   733  	}
   734  	blob, err := mdstream.EncodeRecordStatusChangeV2(c)
   735  	if err != nil {
   736  		return nil, err
   737  	}
   738  
   739  	challenge, err = util.Random(pd.ChallengeSize)
   740  	if err != nil {
   741  		return nil, err
   742  	}
   743  
   744  	sus := pd.SetUnvettedStatus{
   745  		Token:     pdReply.CensorshipRecord.Token,
   746  		Status:    pd.RecordStatusPublic,
   747  		Challenge: hex.EncodeToString(challenge),
   748  		MDAppend: []pd.MetadataStream{
   749  			{
   750  				ID:      mdstream.IDRecordStatusChange,
   751  				Payload: string(blob),
   752  			},
   753  		},
   754  	}
   755  
   756  	// Send SetUnvettedStatus request to politeiad
   757  	responseBody, err = p.makeRequest(ctx, http.MethodPost,
   758  		pd.SetUnvettedStatusRoute, sus)
   759  	if err != nil {
   760  		return nil, err
   761  	}
   762  
   763  	var pdSetUnvettedStatusReply pd.SetUnvettedStatusReply
   764  	err = json.Unmarshal(responseBody, &pdSetUnvettedStatusReply)
   765  	if err != nil {
   766  		return nil, fmt.Errorf("Could not unmarshal SetUnvettedStatusReply: %v",
   767  			err)
   768  	}
   769  
   770  	// Verify the SetUnvettedStatus challenge.
   771  	err = util.VerifyChallenge(p.cfg.Identity, challenge,
   772  		pdSetUnvettedStatusReply.Response)
   773  	if err != nil {
   774  		return nil, err
   775  	}
   776  
   777  	r := pd.Record{
   778  		Metadata:         n.Metadata,
   779  		Files:            n.Files,
   780  		CensorshipRecord: pdReply.CensorshipRecord,
   781  		Version:          "1",
   782  	}
   783  	ir, err := convertRecordToDatabaseInvoice(r)
   784  	if err != nil {
   785  		return nil, err
   786  	}
   787  	// Set UserID for current user
   788  	ir.UserID = u.ID.String()
   789  	ir.Status = cms.InvoiceStatusNew
   790  
   791  	err = p.cmsDB.NewInvoice(ir)
   792  	if err != nil {
   793  		return nil, err
   794  	}
   795  	cr := convertWWWCensorFromPD(pdReply.CensorshipRecord)
   796  
   797  	return &cms.NewInvoiceReply{
   798  		CensorshipRecord: cr,
   799  	}, nil
   800  }
   801  
   802  func merkleRoot(files []www.File) (string, error) {
   803  	// Calculate file digests
   804  	digests := make([]*[sha256.Size]byte, 0, len(files))
   805  	for _, f := range files {
   806  		b, err := base64.StdEncoding.DecodeString(f.Payload)
   807  		if err != nil {
   808  			return "", err
   809  		}
   810  		digest := util.Digest(b)
   811  		var hf [sha256.Size]byte
   812  		copy(hf[:], digest)
   813  		digests = append(digests, &hf)
   814  	}
   815  
   816  	// Return merkle root
   817  	return hex.EncodeToString(merkle.Root(digests)[:]), nil
   818  }
   819  
   820  func (p *Politeiawww) validateInvoice(ni cms.NewInvoice, u *user.CMSUser) error {
   821  	log.Tracef("validateInvoice")
   822  
   823  	// Obtain signature
   824  	sig, err := util.ConvertSignature(ni.Signature)
   825  	if err != nil {
   826  		return www.UserError{
   827  			ErrorCode: www.ErrorStatusInvalidSignature,
   828  		}
   829  	}
   830  
   831  	// Verify public key
   832  	if u.PublicKey() != ni.PublicKey {
   833  		return www.UserError{
   834  			ErrorCode: www.ErrorStatusInvalidSigningKey,
   835  		}
   836  	}
   837  
   838  	pk, err := identity.PublicIdentityFromBytes(u.ActiveIdentity().Key[:])
   839  	if err != nil {
   840  		return err
   841  	}
   842  
   843  	// Check for at least 1 markdown file with a non-empty payload.
   844  	if len(ni.Files) == 0 || ni.Files[0].Payload == "" {
   845  		return www.UserError{
   846  			ErrorCode: www.ErrorStatusProposalMissingFiles,
   847  		}
   848  	}
   849  
   850  	// verify if there are duplicate names
   851  	filenames := make(map[string]int, len(ni.Files))
   852  	// Check that the file number policy is followed.
   853  	var (
   854  		numCSVs, numImages, numInvoiceFiles    int
   855  		csvExceedsMaxSize, imageExceedsMaxSize bool
   856  	)
   857  	for _, v := range ni.Files {
   858  		filenames[v.Name]++
   859  		var (
   860  			data []byte
   861  			err  error
   862  		)
   863  		if strings.HasPrefix(v.MIME, "image/") {
   864  			numImages++
   865  			data, err = base64.StdEncoding.DecodeString(v.Payload)
   866  			if err != nil {
   867  				return err
   868  			}
   869  			if len(data) > cms.PolicyMaxImageSize {
   870  				imageExceedsMaxSize = true
   871  			}
   872  		} else {
   873  			numCSVs++
   874  
   875  			if v.Name == invoiceFile {
   876  				numInvoiceFiles++
   877  			}
   878  
   879  			data, err = base64.StdEncoding.DecodeString(v.Payload)
   880  			if err != nil {
   881  				return err
   882  			}
   883  			if len(data) > cms.PolicyMaxMDSize {
   884  				csvExceedsMaxSize = true
   885  			}
   886  
   887  			// Check to see if the data can be parsed properly into InvoiceInput
   888  			// struct.
   889  			var invInput cms.InvoiceInput
   890  			if err := json.Unmarshal(data, &invInput); err != nil {
   891  				return www.UserError{
   892  					ErrorCode: cms.ErrorStatusMalformedInvoiceFile,
   893  				}
   894  			}
   895  
   896  			// Validate that the input month is a valid month
   897  			if invInput.Month < 1 || invInput.Month > 12 {
   898  				return www.UserError{
   899  					ErrorCode: cms.ErrorStatusInvalidInvoiceMonthYear,
   900  				}
   901  			}
   902  
   903  			// Validate month/year to make sure the first day of the following
   904  			// month is after the current date.  For example, if a user submits
   905  			// an invoice for 03/2019, the first time that they could submit an
   906  			// invoice would be approx. 12:01 AM 04/01/2019
   907  			startOfFollowingMonth := time.Date(int(invInput.Year),
   908  				time.Month(invInput.Month+1), 0, 0, 0, 0, 0, time.UTC)
   909  			if startOfFollowingMonth.After(time.Now()) {
   910  				return www.UserError{
   911  					ErrorCode: cms.ErrorStatusInvalidInvoiceMonthYear,
   912  				}
   913  			}
   914  
   915  			// Validate Payment Address
   916  			_, err := dcrutil.DecodeAddress(strings.TrimSpace(invInput.PaymentAddress), p.params)
   917  			if err != nil {
   918  				return www.UserError{
   919  					ErrorCode: cms.ErrorStatusInvalidPaymentAddress,
   920  				}
   921  			}
   922  
   923  			// Verify that the submitted monthly average matches the value
   924  			// was calculated server side.
   925  			monthAvg, err := p.cmsDB.ExchangeRate(int(invInput.Month),
   926  				int(invInput.Year))
   927  			if err != nil {
   928  				return www.UserError{
   929  					ErrorCode: cms.ErrorStatusInvalidExchangeRate,
   930  				}
   931  			}
   932  			if monthAvg.ExchangeRate != invInput.ExchangeRate {
   933  				return www.UserError{
   934  					ErrorCode: cms.ErrorStatusInvalidExchangeRate,
   935  				}
   936  			}
   937  
   938  			// Validate provided contractor name
   939  			if invInput.ContractorName == "" {
   940  				return www.UserError{
   941  					ErrorCode: cms.ErrorStatusInvoiceMissingName,
   942  				}
   943  			}
   944  			name := formatName(invInput.ContractorName)
   945  			err = validateName(name)
   946  			if err != nil {
   947  				return www.UserError{
   948  					ErrorCode: cms.ErrorStatusMalformedName,
   949  				}
   950  			}
   951  
   952  			location := formatLocation(invInput.ContractorLocation)
   953  			err = validateLocation(location)
   954  			if err != nil {
   955  				return www.UserError{
   956  					ErrorCode: cms.ErrorStatusMalformedLocation,
   957  				}
   958  			}
   959  
   960  			// Validate provided contractor email/contact
   961  			if invInput.ContractorContact == "" {
   962  				return www.UserError{
   963  					ErrorCode: cms.ErrorStatusInvoiceMissingContact,
   964  				}
   965  			}
   966  			contact := formatContact(invInput.ContractorContact)
   967  			err = validateContact(contact)
   968  			if err != nil {
   969  				return www.UserError{
   970  					ErrorCode: cms.ErrorStatusInvoiceMalformedContact,
   971  				}
   972  			}
   973  
   974  			// Validate hourly rate
   975  			if invInput.ContractorRate == 0 {
   976  				return www.UserError{
   977  					ErrorCode: cms.ErrorStatusInvoiceMissingRate,
   978  				}
   979  			}
   980  			if invInput.ContractorRate < uint(minRate) || invInput.ContractorRate > uint(maxRate) {
   981  				return www.UserError{
   982  					ErrorCode: cms.ErrorStatusInvoiceInvalidRate,
   983  				}
   984  			}
   985  
   986  			// Validate line items
   987  			if len(invInput.LineItems) < 1 {
   988  				return www.UserError{
   989  					ErrorCode: cms.ErrorStatusInvoiceRequireLineItems,
   990  				}
   991  			}
   992  			for _, lineInput := range invInput.LineItems {
   993  				domain := formatInvoiceField(lineInput.Domain)
   994  				if !validateInvoiceField(domain) {
   995  					return www.UserError{
   996  						ErrorCode: cms.ErrorStatusMalformedDomain,
   997  					}
   998  				}
   999  				subdomain := formatInvoiceField(lineInput.Subdomain)
  1000  				if !validateInvoiceField(subdomain) {
  1001  					return www.UserError{
  1002  						ErrorCode: cms.ErrorStatusMalformedSubdomain,
  1003  					}
  1004  				}
  1005  
  1006  				description := formatInvoiceField(lineInput.Description)
  1007  				if !validateInvoiceField(description) {
  1008  					return www.UserError{
  1009  						ErrorCode: cms.ErrorStatusMalformedDescription,
  1010  					}
  1011  				}
  1012  
  1013  				piToken := formatInvoiceField(lineInput.ProposalToken)
  1014  				if piToken != "" && !validateInvoiceField(piToken) {
  1015  					return www.UserError{
  1016  						ErrorCode: cms.ErrorStatusMalformedProposalToken,
  1017  					}
  1018  				}
  1019  
  1020  				switch lineInput.Type {
  1021  				case cms.LineItemTypeLabor:
  1022  					if lineInput.Expenses != 0 {
  1023  						return www.UserError{
  1024  							ErrorCode: cms.ErrorStatusInvalidLaborExpense,
  1025  						}
  1026  					}
  1027  					if lineInput.SubRate != 0 {
  1028  						return www.UserError{
  1029  							ErrorCode: cms.ErrorStatusInvoiceInvalidRate,
  1030  						}
  1031  					}
  1032  					if lineInput.SubUserID != "" {
  1033  						return www.UserError{
  1034  							ErrorCode: cms.ErrorStatusInvalidSubUserIDLineItem,
  1035  						}
  1036  					}
  1037  				case cms.LineItemTypeExpense:
  1038  					fallthrough
  1039  				case cms.LineItemTypeMisc:
  1040  					if lineInput.Labor != 0 {
  1041  						return www.UserError{
  1042  							ErrorCode: cms.ErrorStatusInvalidLaborExpense,
  1043  						}
  1044  					}
  1045  				case cms.LineItemTypeSubHours:
  1046  					if u.ContractorType != int(cms.ContractorTypeSupervisor) {
  1047  						return www.UserError{
  1048  							ErrorCode: cms.ErrorStatusInvalidTypeSubHoursLineItem,
  1049  						}
  1050  					}
  1051  					if lineInput.SubUserID == "" {
  1052  						return www.UserError{
  1053  							ErrorCode: cms.ErrorStatusMissingSubUserIDLineItem,
  1054  						}
  1055  					}
  1056  					subUser, err := p.getCMSUserByIDRaw(lineInput.SubUserID)
  1057  					if err != nil {
  1058  						return err
  1059  					}
  1060  					found := false
  1061  					for _, superUserIds := range subUser.SupervisorUserIDs {
  1062  						if superUserIds.String() == u.ID.String() {
  1063  							found = true
  1064  							break
  1065  						}
  1066  					}
  1067  					if !found {
  1068  						return www.UserError{
  1069  							ErrorCode: cms.ErrorStatusInvalidSubUserIDLineItem,
  1070  						}
  1071  					}
  1072  					if lineInput.Labor == 0 {
  1073  						return www.UserError{
  1074  							ErrorCode: cms.ErrorStatusInvalidLaborExpense,
  1075  						}
  1076  					}
  1077  					if lineInput.SubRate < uint(minRate) || lineInput.SubRate > uint(maxRate) {
  1078  						return www.UserError{
  1079  							ErrorCode: cms.ErrorStatusInvoiceInvalidRate,
  1080  						}
  1081  					}
  1082  				default:
  1083  					return www.UserError{
  1084  						ErrorCode: cms.ErrorStatusInvalidLineItemType,
  1085  					}
  1086  				}
  1087  			}
  1088  		}
  1089  	}
  1090  
  1091  	// verify duplicate file names
  1092  	if len(ni.Files) > 1 {
  1093  		var repeated []string
  1094  		for name, count := range filenames {
  1095  			if count > 1 {
  1096  				repeated = append(repeated, name)
  1097  			}
  1098  		}
  1099  		if len(repeated) > 0 {
  1100  			return www.UserError{
  1101  				ErrorCode:    www.ErrorStatusProposalDuplicateFilenames,
  1102  				ErrorContext: repeated,
  1103  			}
  1104  		}
  1105  	}
  1106  
  1107  	// we expect one index file
  1108  	if numInvoiceFiles == 0 {
  1109  		return www.UserError{
  1110  			ErrorCode:    www.ErrorStatusProposalMissingFiles,
  1111  			ErrorContext: []string{www.PolicyIndexFilename},
  1112  		}
  1113  	}
  1114  
  1115  	if numCSVs > www.PolicyMaxMDs {
  1116  		return www.UserError{
  1117  			ErrorCode: www.ErrorStatusMaxMDsExceededPolicy,
  1118  		}
  1119  	}
  1120  
  1121  	if numImages > cms.PolicyMaxImages {
  1122  		return www.UserError{
  1123  			ErrorCode: www.ErrorStatusMaxImagesExceededPolicy,
  1124  		}
  1125  	}
  1126  
  1127  	if csvExceedsMaxSize {
  1128  		return www.UserError{
  1129  			ErrorCode: www.ErrorStatusMaxMDSizeExceededPolicy,
  1130  		}
  1131  	}
  1132  
  1133  	if imageExceedsMaxSize {
  1134  		return www.UserError{
  1135  			ErrorCode: www.ErrorStatusMaxImageSizeExceededPolicy,
  1136  		}
  1137  	}
  1138  
  1139  	// Note that we need validate the string representation of the merkle
  1140  	mr, err := merkleRoot(ni.Files)
  1141  	if err != nil {
  1142  		return err
  1143  	}
  1144  	if !pk.VerifyMessage([]byte(mr), sig) {
  1145  		return www.UserError{
  1146  			ErrorCode: www.ErrorStatusInvalidSignature,
  1147  		}
  1148  	}
  1149  
  1150  	return nil
  1151  }
  1152  
  1153  func filterDomainInvoice(inv *cms.InvoiceRecord, requestedDomain int) cms.InvoiceRecord {
  1154  	inv.Files = nil
  1155  	inv.Input.ContractorContact = ""
  1156  	inv.Input.ContractorLocation = ""
  1157  	inv.Input.ContractorName = ""
  1158  	inv.Input.ContractorRate = 0
  1159  	inv.Input.PaymentAddress = ""
  1160  
  1161  	filteredLineItems := make([]cms.LineItemsInput, 0, len(inv.Input.LineItems))
  1162  	for _, li := range inv.Input.LineItems {
  1163  		// Get the Supported CMS Domain from API
  1164  		var cmsDomain cms.AvailableDomain
  1165  		for _, domain := range cms.PolicySupportedCMSDomains {
  1166  			if int(domain.Type) == requestedDomain {
  1167  				cmsDomain = domain
  1168  			}
  1169  		}
  1170  
  1171  		// Filter out any line item that doesn't match the requested Domain
  1172  		if strings.ToLower(li.Domain) == cmsDomain.Description {
  1173  			li.Expenses = 0
  1174  			li.SubRate = 0
  1175  			filteredLineItems = append(filteredLineItems, li)
  1176  		}
  1177  	}
  1178  	inv.Input.LineItems = filteredLineItems
  1179  	inv.Payment = cms.PaymentInformation{}
  1180  	inv.Total = 0
  1181  	return *inv
  1182  }
  1183  
  1184  // processInvoiceDetails fetches a specific proposal version from the invoice
  1185  // db and returns it.
  1186  func (p *Politeiawww) processInvoiceDetails(invDetails cms.InvoiceDetails, u *user.User) (*cms.InvoiceDetailsReply, error) {
  1187  	log.Tracef("processInvoiceDetails")
  1188  
  1189  	requestingUser, err := p.getCMSUserByIDRaw(u.ID.String())
  1190  	if err != nil {
  1191  		return nil, err
  1192  	}
  1193  
  1194  	// Version is an optional query param. Fetch latest version
  1195  	// when query param is not specified.
  1196  	var invRec *cms.InvoiceRecord
  1197  	if invDetails.Version == "" {
  1198  		invRec, err = p.getInvoice(invDetails.Token)
  1199  	} else {
  1200  		invRec, err = p.getInvoiceVersion(invDetails.Token, invDetails.Version)
  1201  	}
  1202  	if err != nil {
  1203  		return nil, err
  1204  	}
  1205  
  1206  	// Calculate the payout from the invoice record
  1207  	dbInv := convertInvoiceRecordToDatabaseInvoice(invRec)
  1208  	var reply cms.InvoiceDetailsReply
  1209  
  1210  	if u.Admin || dbInv.UserID == u.ID.String() {
  1211  		payout, err := calculatePayout(*dbInv)
  1212  		if err != nil {
  1213  			return nil, err
  1214  		}
  1215  		payout.Username = u.Username
  1216  
  1217  		// Setup reply
  1218  		reply.Invoice = *invRec
  1219  		reply.Payout = payout
  1220  	} else {
  1221  		reply.Invoice = filterDomainInvoice(invRec, requestingUser.Domain)
  1222  	}
  1223  	return &reply, nil
  1224  }
  1225  
  1226  // processSetInvoiceStatus updates the status of the specified invoice.
  1227  func (p *Politeiawww) processSetInvoiceStatus(ctx context.Context, sis cms.SetInvoiceStatus, u *user.User) (*cms.SetInvoiceStatusReply, error) {
  1228  	log.Tracef("processSetInvoiceStatus")
  1229  
  1230  	invRec, err := p.getInvoice(sis.Token)
  1231  	if err != nil {
  1232  		return nil, err
  1233  	}
  1234  
  1235  	// Ensure the provided public key is the user's active key.
  1236  	if sis.PublicKey != u.PublicKey() {
  1237  		return nil, www.UserError{
  1238  			ErrorCode: www.ErrorStatusInvalidSigningKey,
  1239  		}
  1240  	}
  1241  
  1242  	// Validate signature
  1243  	msg := fmt.Sprintf("%v%v%v%v", sis.Token, invRec.Version,
  1244  		sis.Status, sis.Reason)
  1245  	err = validateSignature(sis.PublicKey, sis.Signature, msg)
  1246  	if err != nil {
  1247  		return nil, err
  1248  	}
  1249  
  1250  	dbInvoice, err := p.cmsDB.InvoiceByToken(sis.Token)
  1251  	if err != nil {
  1252  		if errors.Is(err, database.ErrInvoiceNotFound) {
  1253  			err = www.UserError{
  1254  				ErrorCode: cms.ErrorStatusInvoiceNotFound,
  1255  			}
  1256  		}
  1257  		return nil, err
  1258  	}
  1259  	err = validateStatusTransition(dbInvoice.Status, sis.Status, sis.Reason)
  1260  	if err != nil {
  1261  		return nil, err
  1262  	}
  1263  
  1264  	// Create the change record.
  1265  	c := mdstream.InvoiceStatusChange{
  1266  		Version:        mdstream.VersionInvoiceStatusChange,
  1267  		AdminPublicKey: u.PublicKey(),
  1268  		Timestamp:      time.Now().Unix(),
  1269  		NewStatus:      sis.Status,
  1270  		Reason:         sis.Reason,
  1271  	}
  1272  	blob, err := mdstream.EncodeInvoiceStatusChange(c)
  1273  	if err != nil {
  1274  		return nil, err
  1275  	}
  1276  
  1277  	challenge, err := util.Random(pd.ChallengeSize)
  1278  	if err != nil {
  1279  		return nil, err
  1280  	}
  1281  
  1282  	pdCommand := pd.UpdateVettedMetadata{
  1283  		Challenge: hex.EncodeToString(challenge),
  1284  		Token:     sis.Token,
  1285  		MDAppend: []pd.MetadataStream{
  1286  			{
  1287  				ID:      mdstream.IDInvoiceStatusChange,
  1288  				Payload: string(blob),
  1289  			},
  1290  		},
  1291  	}
  1292  
  1293  	responseBody, err := p.makeRequest(ctx, http.MethodPost, pd.UpdateVettedMetadataRoute, pdCommand)
  1294  	if err != nil {
  1295  		return nil, err
  1296  	}
  1297  
  1298  	var pdReply pd.UpdateVettedMetadataReply
  1299  	err = json.Unmarshal(responseBody, &pdReply)
  1300  	if err != nil {
  1301  		return nil, fmt.Errorf("Could not unmarshal UpdateVettedMetadataReply: %v",
  1302  			err)
  1303  	}
  1304  
  1305  	// Verify the UpdateVettedMetadata challenge.
  1306  	err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response)
  1307  	if err != nil {
  1308  		return nil, err
  1309  	}
  1310  
  1311  	// Update the database with the metadata changes.
  1312  	dbInvoice.Changes = append(dbInvoice.Changes, database.InvoiceChange{
  1313  		Timestamp:      c.Timestamp,
  1314  		AdminPublicKey: c.AdminPublicKey,
  1315  		NewStatus:      c.NewStatus,
  1316  		Reason:         c.Reason,
  1317  	})
  1318  	dbInvoice.StatusChangeReason = c.Reason
  1319  	dbInvoice.Status = c.NewStatus
  1320  
  1321  	// Calculate amount of DCR needed
  1322  	payout, err := calculatePayout(*dbInvoice)
  1323  	if err != nil {
  1324  		return nil, err
  1325  	}
  1326  	payout.Username = u.Username
  1327  	// If approved then update Invoice's Payment table in DB
  1328  	if c.NewStatus == cms.InvoiceStatusApproved {
  1329  		dbInvoice.Payments = database.Payments{
  1330  			InvoiceToken: dbInvoice.Token,
  1331  			Address:      strings.TrimSpace(dbInvoice.PaymentAddress),
  1332  			TimeStarted:  time.Now().Unix(),
  1333  			Status:       cms.PaymentStatusWatching,
  1334  			AmountNeeded: int64(payout.DCRTotal),
  1335  		}
  1336  	}
  1337  
  1338  	err = p.cmsDB.UpdateInvoice(dbInvoice)
  1339  	if err != nil {
  1340  		return nil, err
  1341  	}
  1342  
  1343  	if dbInvoice.Status == cms.InvoiceStatusApproved ||
  1344  		dbInvoice.Status == cms.InvoiceStatusRejected ||
  1345  		dbInvoice.Status == cms.InvoiceStatusDisputed {
  1346  		invoiceUser, err := p.db.UserGetByUsername(invRec.Username)
  1347  		if err != nil {
  1348  			return nil, fmt.Errorf("failed to get user by username %v %v",
  1349  				invRec.Username, err)
  1350  		}
  1351  		// If approved and successfully entered into DB, start watcher for address
  1352  		if c.NewStatus == cms.InvoiceStatusApproved {
  1353  			p.addWatchAddress(dbInvoice.PaymentAddress)
  1354  		}
  1355  
  1356  		// Emit event notification for invoice status update
  1357  		p.events.Emit(eventInvoiceStatusUpdate,
  1358  			dataInvoiceStatusUpdate{
  1359  				token: dbInvoice.Token,
  1360  				email: invoiceUser.Email,
  1361  			})
  1362  	}
  1363  
  1364  	dbInvoice.Username = invRec.Username
  1365  	// Return the reply.
  1366  
  1367  	dbRec, err := convertDatabaseInvoiceToInvoiceRecord(*dbInvoice)
  1368  	if err != nil {
  1369  		return nil, err
  1370  	}
  1371  
  1372  	sisr := cms.SetInvoiceStatusReply{
  1373  		Invoice: dbRec,
  1374  	}
  1375  	return &sisr, nil
  1376  }
  1377  
  1378  func validateStatusTransition(
  1379  	oldStatus cms.InvoiceStatusT,
  1380  	newStatus cms.InvoiceStatusT,
  1381  	reason string,
  1382  ) error {
  1383  	validStatuses, ok := validStatusTransitions[oldStatus]
  1384  	if !ok {
  1385  		log.Debugf("status not supported: %v", oldStatus)
  1386  		return www.UserError{
  1387  			ErrorCode: cms.ErrorStatusInvalidInvoiceStatusTransition,
  1388  		}
  1389  	}
  1390  
  1391  	if !statusInSlice(validStatuses, newStatus) {
  1392  		return www.UserError{
  1393  			ErrorCode: cms.ErrorStatusInvalidInvoiceStatusTransition,
  1394  		}
  1395  	}
  1396  
  1397  	if newStatus == cms.InvoiceStatusRejected && reason == "" {
  1398  		return www.UserError{
  1399  			ErrorCode: cms.ErrorStatusReasonNotProvided,
  1400  		}
  1401  	}
  1402  
  1403  	return nil
  1404  }
  1405  
  1406  func statusInSlice(arr []cms.InvoiceStatusT, status cms.InvoiceStatusT) bool {
  1407  	for _, s := range arr {
  1408  		if status == s {
  1409  			return true
  1410  		}
  1411  	}
  1412  
  1413  	return false
  1414  }
  1415  
  1416  // processEditInvoice attempts to edit a proposal on politeiad.
  1417  func (p *Politeiawww) processEditInvoice(ctx context.Context, ei cms.EditInvoice, u *user.User) (*cms.EditInvoiceReply, error) {
  1418  	log.Tracef("processEditInvoice %v", ei.Token)
  1419  
  1420  	invRec, err := p.getInvoice(ei.Token)
  1421  	if err != nil {
  1422  		return nil, err
  1423  	}
  1424  
  1425  	if invRec.Status == cms.InvoiceStatusPaid || invRec.Status == cms.InvoiceStatusApproved ||
  1426  		invRec.Status == cms.InvoiceStatusRejected {
  1427  		return nil, www.UserError{
  1428  			ErrorCode: cms.ErrorStatusWrongInvoiceStatus,
  1429  		}
  1430  	}
  1431  	// Ensure user is the invoice author
  1432  	if invRec.UserID != u.ID.String() {
  1433  		return nil, www.UserError{
  1434  			ErrorCode: www.ErrorStatusUserNotAuthor,
  1435  		}
  1436  	}
  1437  
  1438  	// Make sure that the edit being submitted is different than the current invoice.
  1439  	// So check the Files to see if the digests are different at all.
  1440  	if len(ei.Files) == len(invRec.Files) {
  1441  		sameFiles := true
  1442  		for i, recFile := range invRec.Files {
  1443  			if recFile.Digest != ei.Files[i].Digest {
  1444  				sameFiles = false
  1445  			}
  1446  		}
  1447  		if sameFiles {
  1448  			return nil, www.UserError{
  1449  				ErrorCode: cms.ErrorStatusInvoiceDuplicate,
  1450  			}
  1451  		}
  1452  	}
  1453  
  1454  	cmsUser, err := p.getCMSUserByIDRaw(u.ID.String())
  1455  	if err != nil {
  1456  		return nil, err
  1457  	}
  1458  	// Validate invoice. Convert it to cms.NewInvoice so that
  1459  	// we can reuse the function validateProposal.
  1460  	ni := cms.NewInvoice{
  1461  		Files:     ei.Files,
  1462  		PublicKey: ei.PublicKey,
  1463  		Signature: ei.Signature,
  1464  	}
  1465  	err = p.validateInvoice(ni, cmsUser)
  1466  	if err != nil {
  1467  		return nil, err
  1468  	}
  1469  
  1470  	// Check to see that the month/year of the editted invoice is the same as
  1471  	// the previous record.
  1472  	month, year := getInvoiceMonthYear(ei.Files)
  1473  	if month != invRec.Input.Month || year != invRec.Input.Year {
  1474  		return nil, www.UserError{
  1475  			ErrorCode: cms.ErrorStatusInvalidInvoiceEditMonthYear,
  1476  		}
  1477  	}
  1478  
  1479  	// Dupe address check.
  1480  	invInput, err := parseInvoiceInput(ei.Files)
  1481  	if err != nil {
  1482  		return nil, err
  1483  	}
  1484  
  1485  	invoiceAddress, err := p.cmsDB.InvoicesByAddress(invInput.PaymentAddress)
  1486  	if err != nil {
  1487  		return nil, www.UserError{
  1488  			ErrorCode: cms.ErrorStatusInvalidPaymentAddress,
  1489  		}
  1490  	}
  1491  
  1492  	// Only disregard any duplicate hits to InvoicesByAddress if it's not the
  1493  	// current invoice being edited.
  1494  	for _, v := range invoiceAddress {
  1495  		if v.Token != ei.Token {
  1496  			return nil, www.UserError{
  1497  				ErrorCode: cms.ErrorStatusDuplicatePaymentAddress,
  1498  			}
  1499  		}
  1500  	}
  1501  
  1502  	m := mdstream.InvoiceGeneral{
  1503  		Version:   mdstream.VersionInvoiceGeneral,
  1504  		Timestamp: time.Now().Unix(),
  1505  		PublicKey: ei.PublicKey,
  1506  		Signature: ei.Signature,
  1507  	}
  1508  	md, err := mdstream.EncodeInvoiceGeneral(m)
  1509  	if err != nil {
  1510  		return nil, err
  1511  	}
  1512  
  1513  	mds := []pd.MetadataStream{{
  1514  		ID:      mdstream.IDInvoiceGeneral,
  1515  		Payload: string(md),
  1516  	}}
  1517  
  1518  	// Check if any files need to be deleted
  1519  	var delFiles []string
  1520  	for _, v := range invRec.Files {
  1521  		found := false
  1522  		for _, c := range ei.Files {
  1523  			if v.Name == c.Name {
  1524  				found = true
  1525  			}
  1526  		}
  1527  		if !found {
  1528  			delFiles = append(delFiles, v.Name)
  1529  		}
  1530  	}
  1531  
  1532  	// Setup politeiad request
  1533  	challenge, err := util.Random(pd.ChallengeSize)
  1534  	if err != nil {
  1535  		return nil, err
  1536  	}
  1537  
  1538  	e := pd.UpdateRecord{
  1539  		Token:       ei.Token,
  1540  		Challenge:   hex.EncodeToString(challenge),
  1541  		MDOverwrite: mds,
  1542  		FilesAdd:    convertPDFilesFromWWW(ei.Files),
  1543  		FilesDel:    delFiles,
  1544  	}
  1545  
  1546  	// Send politeiad request
  1547  	responseBody, err := p.makeRequest(ctx, http.MethodPost, pd.UpdateVettedRoute, e)
  1548  	if err != nil {
  1549  		return nil, err
  1550  	}
  1551  
  1552  	// Handle response
  1553  	var pdReply pd.UpdateRecordReply
  1554  	err = json.Unmarshal(responseBody, &pdReply)
  1555  	if err != nil {
  1556  		return nil, fmt.Errorf("Unmarshal UpdateUnvettedReply: %v", err)
  1557  	}
  1558  
  1559  	err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response)
  1560  	if err != nil {
  1561  		return nil, err
  1562  	}
  1563  
  1564  	// Create the change record.
  1565  	c := mdstream.InvoiceStatusChange{
  1566  		Version:        mdstream.VersionInvoiceStatusChange,
  1567  		AdminPublicKey: u.PublicKey(),
  1568  		Timestamp:      time.Now().Unix(),
  1569  		NewStatus:      cms.InvoiceStatusUpdated,
  1570  	}
  1571  	blob, err := mdstream.EncodeInvoiceStatusChange(c)
  1572  	if err != nil {
  1573  		return nil, err
  1574  	}
  1575  
  1576  	challenge, err = util.Random(pd.ChallengeSize)
  1577  	if err != nil {
  1578  		return nil, err
  1579  	}
  1580  
  1581  	pdCommand := pd.UpdateVettedMetadata{
  1582  		Challenge: hex.EncodeToString(challenge),
  1583  		Token:     ei.Token,
  1584  		MDAppend: []pd.MetadataStream{
  1585  			{
  1586  				ID:      mdstream.IDInvoiceStatusChange,
  1587  				Payload: string(blob),
  1588  			},
  1589  		},
  1590  	}
  1591  
  1592  	var updateMetaReply pd.UpdateVettedMetadataReply
  1593  	responseBody, err = p.makeRequest(ctx, http.MethodPost,
  1594  		pd.UpdateVettedMetadataRoute, pdCommand)
  1595  	if err != nil {
  1596  		return nil, err
  1597  	}
  1598  
  1599  	err = json.Unmarshal(responseBody, &updateMetaReply)
  1600  	if err != nil {
  1601  		return nil, fmt.Errorf("Could not unmarshal UpdateVettedMetadataReply: %v",
  1602  			err)
  1603  	}
  1604  
  1605  	// Verify the UpdateVettedMetadata challenge.
  1606  	err = util.VerifyChallenge(p.cfg.Identity, challenge, updateMetaReply.Response)
  1607  	if err != nil {
  1608  		return nil, err
  1609  	}
  1610  
  1611  	version, err := strconv.Atoi(invRec.Version)
  1612  	if err != nil {
  1613  		return nil, err
  1614  	}
  1615  
  1616  	dbInvoice, err := convertRecordToDatabaseInvoice(pd.Record{
  1617  		Files:            convertPDFilesFromWWW(ei.Files),
  1618  		Metadata:         mds,
  1619  		CensorshipRecord: convertPDCensorFromWWW(invRec.CensorshipRecord),
  1620  		// Increment the version
  1621  		Version: strconv.Itoa(version + 1),
  1622  	})
  1623  	if err != nil {
  1624  		return nil, err
  1625  	}
  1626  
  1627  	dbInvoice.UserID = u.ID.String()
  1628  	dbInvoice.Status = cms.InvoiceStatusUpdated
  1629  
  1630  	// Since we want to retain all versions of an invoice, don't update,
  1631  	// create a new entry.
  1632  	err = p.cmsDB.NewInvoice(dbInvoice)
  1633  	if err != nil {
  1634  		return nil, err
  1635  	}
  1636  
  1637  	// Get updated invoice from the database
  1638  	inv, err := p.getInvoiceVersion(dbInvoice.Token, dbInvoice.Version)
  1639  	if err != nil {
  1640  		log.Errorf("processEditInvoice: getInvoice %v: %v",
  1641  			dbInvoice.Token, err)
  1642  	}
  1643  
  1644  	return &cms.EditInvoiceReply{
  1645  		Invoice: *inv,
  1646  	}, nil
  1647  }
  1648  
  1649  // processGeneratePayouts looks for all approved invoices and uses the provided
  1650  // exchange rate to generate a list of addresses and amounts for an admin to
  1651  // process payments.
  1652  func (p *Politeiawww) processGeneratePayouts(gp cms.GeneratePayouts, u *user.User) (*cms.GeneratePayoutsReply, error) {
  1653  	log.Tracef("processGeneratePayouts")
  1654  
  1655  	dbInvs, err := p.cmsDB.InvoicesByStatus(int(cms.InvoiceStatusApproved))
  1656  	if err != nil {
  1657  		return nil, err
  1658  	}
  1659  
  1660  	reply := &cms.GeneratePayoutsReply{}
  1661  	payouts := make([]cms.Payout, 0, len(dbInvs))
  1662  	for _, inv := range dbInvs {
  1663  		payout, err := calculatePayout(inv)
  1664  		if err != nil {
  1665  			return nil, err
  1666  		}
  1667  		payout.Username = u.Username
  1668  		payouts = append(payouts, payout)
  1669  	}
  1670  	sort.Slice(payouts, func(i, j int) bool {
  1671  		return payouts[i].ApprovedTime > payouts[j].ApprovedTime
  1672  	})
  1673  	reply.Payouts = payouts
  1674  	return reply, err
  1675  }
  1676  
  1677  // getInvoice gets the most recent verions of the given invoice from the db
  1678  // then fills in any missing user fields before returning the invoice record.
  1679  func (p *Politeiawww) getInvoice(token string) (*cms.InvoiceRecord, error) {
  1680  	// Get invoice from db
  1681  	r, err := p.cmsDB.InvoiceByToken(token)
  1682  	if err != nil {
  1683  		return nil, err
  1684  	}
  1685  	i, err := convertDatabaseInvoiceToInvoiceRecord(*r)
  1686  	if err != nil {
  1687  		return nil, err
  1688  	}
  1689  	// Fill in userID and username fields
  1690  	u, err := p.db.UserGetByPubKey(i.PublicKey)
  1691  	if err != nil {
  1692  		log.Errorf("getInvoice: getUserByPubKey: token:%v "+
  1693  			"pubKey:%v err:%v", token, i.PublicKey, err)
  1694  	} else {
  1695  		i.UserID = u.ID.String()
  1696  		i.Username = u.Username
  1697  	}
  1698  
  1699  	return &i, nil
  1700  }
  1701  
  1702  // getInvoiceVersion gets a specific version of an invoice from the db.
  1703  func (p *Politeiawww) getInvoiceVersion(token, version string) (*cms.InvoiceRecord, error) {
  1704  	log.Tracef("getInvoiceVersion: %v %v", token, version)
  1705  
  1706  	r, err := p.cmsDB.InvoiceByTokenVersion(token, version)
  1707  	if err != nil {
  1708  		return nil, err
  1709  	}
  1710  
  1711  	i, err := convertDatabaseInvoiceToInvoiceRecord(*r)
  1712  	if err != nil {
  1713  		return nil, err
  1714  	}
  1715  
  1716  	// Fill in userID and username fields
  1717  	u, err := p.db.UserGetByPubKey(i.PublicKey)
  1718  	if err != nil {
  1719  		log.Errorf("getInvoice: getUserByPubKey: token:%v "+
  1720  			"pubKey:%v err:%v", token, i.PublicKey, err)
  1721  	} else {
  1722  		i.UserID = u.ID.String()
  1723  		i.Username = u.Username
  1724  	}
  1725  
  1726  	return &i, nil
  1727  }
  1728  
  1729  // processUserInvoices fetches all invoices that are currently stored in the
  1730  // cmsdb for the logged in user.
  1731  func (p *Politeiawww) processUserInvoices(user *user.User) (*cms.UserInvoicesReply, error) {
  1732  	log.Tracef("processUserInvoices")
  1733  
  1734  	dbInvs, err := p.cmsDB.InvoicesByUserID(user.ID.String())
  1735  	if err != nil {
  1736  		return nil, err
  1737  	}
  1738  
  1739  	invRecs := make([]cms.InvoiceRecord, 0, len(dbInvs))
  1740  	for _, v := range dbInvs {
  1741  		inv, err := p.getInvoice(v.Token)
  1742  		if err != nil {
  1743  			return nil, err
  1744  		}
  1745  		invRecs = append(invRecs, *inv)
  1746  	}
  1747  
  1748  	// Setup reply
  1749  	reply := cms.UserInvoicesReply{
  1750  		Invoices: invRecs,
  1751  	}
  1752  	return &reply, nil
  1753  }
  1754  
  1755  // processAdminUserInvoices fetches all invoices that are currently stored in the
  1756  // cmsdb for the logged in user.
  1757  func (p *Politeiawww) processAdminUserInvoices(aui cms.AdminUserInvoices) (*cms.AdminUserInvoicesReply, error) {
  1758  	log.Tracef("processAdminUserInvoices")
  1759  
  1760  	dbInvs, err := p.cmsDB.InvoicesByUserID(aui.UserID)
  1761  	if err != nil {
  1762  		return nil, err
  1763  	}
  1764  
  1765  	invRecs := make([]cms.InvoiceRecord, 0, len(dbInvs))
  1766  	for _, v := range dbInvs {
  1767  		inv, err := p.getInvoice(v.Token)
  1768  		if err != nil {
  1769  			return nil, err
  1770  		}
  1771  		invRecs = append(invRecs, *inv)
  1772  	}
  1773  
  1774  	// Setup reply
  1775  	reply := cms.AdminUserInvoicesReply{
  1776  		Invoices: invRecs,
  1777  	}
  1778  	return &reply, nil
  1779  }
  1780  
  1781  // processInvoices fetches all invoices that are currently stored in the
  1782  // cmsdb for an administrator, based on request fields (month/year,
  1783  // starttime/endtime, userid and/or status).
  1784  func (p *Politeiawww) processInvoices(ai cms.Invoices, u *user.User) (*cms.UserInvoicesReply, error) {
  1785  	log.Tracef("processInvoices")
  1786  
  1787  	requestingUser, err := p.getCMSUserByIDRaw(u.ID.String())
  1788  	if err != nil {
  1789  		return nil, err
  1790  	}
  1791  
  1792  	// Ensure that the user is authorized to view domain invoices.
  1793  	if _, ok := validDomainInvoiceViewingContractorType[cms.ContractorTypeT(
  1794  		requestingUser.ContractorType)]; !ok && !u.Admin {
  1795  		return nil, www.UserError{
  1796  			ErrorCode: www.ErrorStatusUserActionNotAllowed,
  1797  		}
  1798  	}
  1799  
  1800  	// Make sure month AND year are set, if any.
  1801  	if (ai.Month == 0 && ai.Year != 0) || (ai.Month != 0 && ai.Year == 0) {
  1802  		return nil, www.UserError{
  1803  			ErrorCode: cms.ErrorStatusInvalidMonthYearRequest,
  1804  		}
  1805  	}
  1806  
  1807  	// Make sure month and year are sensible inputs
  1808  	if ai.Month < 0 || ai.Month > 12 {
  1809  		return nil, www.UserError{
  1810  			ErrorCode: cms.ErrorStatusInvalidMonthYearRequest,
  1811  		}
  1812  	}
  1813  
  1814  	// Only accept year inputs for years +/- some constant from the current year.
  1815  	const acceptableYearRange = 2
  1816  	if ai.Year != 0 && (ai.Year < uint16(time.Now().Year()-acceptableYearRange) ||
  1817  		ai.Year > uint16(time.Now().Year()+acceptableYearRange)) {
  1818  		return nil, www.UserError{
  1819  			ErrorCode: cms.ErrorStatusInvalidMonthYearRequest,
  1820  		}
  1821  	}
  1822  
  1823  	// Make sure if month and year populated that start and end ARE NOT
  1824  	if (ai.Month != 0 && ai.Year != 0) && (ai.StartTime != 0 && ai.EndTime != 0) {
  1825  		return nil, www.UserError{
  1826  			ErrorCode: cms.ErrorStatusInvalidMonthYearRequest,
  1827  		}
  1828  	}
  1829  
  1830  	var dbInvs []database.Invoice
  1831  	switch {
  1832  	case (ai.Month != 0 && ai.Year != 0) && ai.Status != 0:
  1833  		dbInvs, err = p.cmsDB.InvoicesByMonthYearStatus(ai.Month, ai.Year, int(ai.Status))
  1834  		if err != nil {
  1835  			return nil, err
  1836  		}
  1837  	case (ai.Month != 0 && ai.Year != 0) && ai.Status == 0:
  1838  		dbInvs, err = p.cmsDB.InvoicesByMonthYear(ai.Month, ai.Year)
  1839  		if err != nil {
  1840  			return nil, err
  1841  		}
  1842  	case (ai.StartTime != 0 && ai.EndTime != 0) && ai.Status == 0:
  1843  		dbInvs, err = p.cmsDB.InvoicesByDateRange(ai.StartTime, ai.EndTime)
  1844  		if err != nil {
  1845  			return nil, err
  1846  		}
  1847  	case (ai.Month == 0 && ai.Year == 0) && ai.Status != 0:
  1848  		dbInvs, err = p.cmsDB.InvoicesByStatus(int(ai.Status))
  1849  		if err != nil {
  1850  			return nil, err
  1851  		}
  1852  	case (ai.StartTime != 0 && ai.EndTime != 0) && ai.Status != 0:
  1853  		dbInvs, err = p.cmsDB.InvoicesByDateRangeStatus(ai.StartTime,
  1854  			ai.EndTime, int(ai.Status))
  1855  		if err != nil {
  1856  			return nil, err
  1857  		}
  1858  	default:
  1859  		dbInvs, err = p.cmsDB.InvoicesAll()
  1860  		if err != nil {
  1861  			return nil, err
  1862  		}
  1863  	}
  1864  
  1865  	// Sort returned invoices by time submitted
  1866  	sort.Slice(dbInvs, func(a, b int) bool {
  1867  		return dbInvs[a].Timestamp < dbInvs[b].Timestamp
  1868  	})
  1869  
  1870  	invRecs := make([]cms.InvoiceRecord, 0, len(dbInvs))
  1871  	for _, v := range dbInvs {
  1872  		// Only return up to max page size if start time and end time are
  1873  		// provided.
  1874  		if (ai.StartTime != 0 && ai.EndTime != 0) &&
  1875  			len(invRecs) > cms.InvoiceListPageSize {
  1876  			break
  1877  		}
  1878  
  1879  		inv, err := convertDatabaseInvoiceToInvoiceRecord(v)
  1880  		if err != nil {
  1881  			return nil, err
  1882  		}
  1883  		invUser, err := p.db.UserGetByPubKey(inv.PublicKey)
  1884  		if err != nil {
  1885  			log.Errorf("getInvoice: getUserByPubKey: token:%v "+
  1886  				"pubKey:%v err:%v", v.Token, inv.PublicKey, err)
  1887  		} else {
  1888  			inv.Username = invUser.Username
  1889  		}
  1890  
  1891  		// If the user is not an admin AND not the invoice owner
  1892  		// filter out the information for domain viewing an only allow to see
  1893  		// invoices that are less than 6 months old.
  1894  
  1895  		if !u.Admin && inv.UserID != u.ID.String() {
  1896  			date := time.Date(int(inv.Input.Year), time.Month(inv.Input.Month), 0, 0, 0, 0, 0, time.UTC)
  1897  
  1898  			// Skip if month/year of invoice is BEFORE the current time minus
  1899  			// the domain invoice limit duration.
  1900  			if date.Before(time.Now().Add(-1 * domainInvoiceLimit)) {
  1901  				continue
  1902  			}
  1903  
  1904  			inv = filterDomainInvoice(&inv, requestingUser.Domain)
  1905  		}
  1906  		// Only return invoices that have non-zero line items after filtering.
  1907  		if len(inv.Input.LineItems) > 0 {
  1908  			invRecs = append(invRecs, inv)
  1909  		}
  1910  	}
  1911  
  1912  	// Setup reply
  1913  	reply := cms.UserInvoicesReply{
  1914  		Invoices: invRecs,
  1915  	}
  1916  	return &reply, nil
  1917  }
  1918  
  1919  // processNewCommentInvoice sends a new comment decred plugin command to politeaid
  1920  // then fetches the new comment from the cache and returns it.
  1921  func (p *Politeiawww) processNewCommentInvoice(ctx context.Context, nc www.NewComment, u *user.User) (*www.NewCommentReply, error) {
  1922  	log.Tracef("processNewComment: %v %v", nc.Token, u.ID)
  1923  
  1924  	ir, err := p.getInvoice(nc.Token)
  1925  	if err != nil {
  1926  		if errors.Is(err, cmsdatabase.ErrInvoiceNotFound) {
  1927  			err = www.UserError{
  1928  				ErrorCode: cms.ErrorStatusInvoiceNotFound,
  1929  			}
  1930  		}
  1931  		return nil, err
  1932  	}
  1933  
  1934  	// Check to make sure the user is either an admin or the
  1935  	// author of the invoice.
  1936  	if !u.Admin && (ir.Username != u.Username) {
  1937  		return nil, www.UserError{
  1938  			ErrorCode: www.ErrorStatusUserActionNotAllowed,
  1939  		}
  1940  	}
  1941  
  1942  	// Ensure the public key is the user's active key
  1943  	if nc.PublicKey != u.PublicKey() {
  1944  		return nil, www.UserError{
  1945  			ErrorCode: www.ErrorStatusInvalidSigningKey,
  1946  		}
  1947  	}
  1948  
  1949  	// Validate signature
  1950  	msg := nc.Token + nc.ParentID + nc.Comment
  1951  	err = validateSignature(nc.PublicKey, nc.Signature, msg)
  1952  	if err != nil {
  1953  		return nil, err
  1954  	}
  1955  
  1956  	// Validate comment
  1957  	err = validateNewComment(nc)
  1958  	if err != nil {
  1959  		return nil, err
  1960  	}
  1961  
  1962  	// Check to make sure that invoice isn't already approved or paid.
  1963  	if ir.Status == cms.InvoiceStatusApproved || ir.Status == cms.InvoiceStatusPaid {
  1964  		return nil, www.UserError{
  1965  			ErrorCode: cms.ErrorStatusWrongInvoiceStatus,
  1966  		}
  1967  	}
  1968  
  1969  	// Setup plugin command
  1970  	challenge, err := util.Random(pd.ChallengeSize)
  1971  	if err != nil {
  1972  		return nil, err
  1973  	}
  1974  
  1975  	dnc := convertNewCommentToDecredPlugin(nc)
  1976  	payload, err := decredplugin.EncodeNewComment(dnc)
  1977  	if err != nil {
  1978  		return nil, err
  1979  	}
  1980  
  1981  	pc := pd.PluginCommand{
  1982  		Challenge: hex.EncodeToString(challenge),
  1983  		ID:        decredplugin.ID,
  1984  		Command:   decredplugin.CmdNewComment,
  1985  		CommandID: decredplugin.CmdNewComment,
  1986  		Payload:   string(payload),
  1987  	}
  1988  
  1989  	// Send polieiad request
  1990  	responseBody, err := p.makeRequest(ctx, http.MethodPost,
  1991  		pd.PluginCommandRoute, pc)
  1992  	if err != nil {
  1993  		return nil, err
  1994  	}
  1995  
  1996  	// Handle response
  1997  	var reply pd.PluginCommandReply
  1998  	err = json.Unmarshal(responseBody, &reply)
  1999  	if err != nil {
  2000  		return nil, fmt.Errorf("could not unmarshal "+
  2001  			"PluginCommandReply: %v", err)
  2002  	}
  2003  
  2004  	err = util.VerifyChallenge(p.cfg.Identity, challenge, reply.Response)
  2005  	if err != nil {
  2006  		return nil, err
  2007  	}
  2008  
  2009  	ncr, err := decredplugin.DecodeNewCommentReply([]byte(reply.Payload))
  2010  	if err != nil {
  2011  		return nil, err
  2012  	}
  2013  
  2014  	// Get comment
  2015  	comments, err := p.getInvoiceComments(ctx, nc.Token)
  2016  	if err != nil {
  2017  		return nil, fmt.Errorf("getComment: %v", err)
  2018  	}
  2019  	var c www.Comment
  2020  	for _, v := range comments {
  2021  		if v.CommentID == ncr.CommentID {
  2022  			c = v
  2023  			break
  2024  		}
  2025  	}
  2026  
  2027  	if u.Admin {
  2028  		invoiceUser, err := p.db.UserGetByUsername(ir.Username)
  2029  		if err != nil {
  2030  			return nil, fmt.Errorf("failed to get user by username %v %v",
  2031  				ir.Username, err)
  2032  		}
  2033  		// Emit event notification for a invoice comment
  2034  		p.events.Emit(eventInvoiceComment,
  2035  			dataInvoiceComment{
  2036  				token: nc.Token,
  2037  				email: invoiceUser.Email,
  2038  			})
  2039  	}
  2040  	return &www.NewCommentReply{
  2041  		Comment: c,
  2042  	}, nil
  2043  }
  2044  
  2045  // processCommentsGet returns all comments for a given proposal. If the user is
  2046  // logged in the user's last access time for the given comments will also be
  2047  // returned.
  2048  func (p *Politeiawww) processInvoiceComments(ctx context.Context, token string, u *user.User) (*www.GetCommentsReply, error) {
  2049  	log.Tracef("ProcessCommentGet: %v", token)
  2050  
  2051  	ir, err := p.getInvoice(token)
  2052  	if err != nil {
  2053  		if errors.Is(err, database.ErrInvoiceNotFound) {
  2054  			err = www.UserError{
  2055  				ErrorCode: cms.ErrorStatusInvoiceNotFound,
  2056  			}
  2057  		}
  2058  		return nil, err
  2059  	}
  2060  
  2061  	// Check to make sure the user is either an admin or the
  2062  	// invoice author.
  2063  	if !u.Admin && (ir.Username != u.Username) {
  2064  		err := www.UserError{
  2065  			ErrorCode: www.ErrorStatusUserActionNotAllowed,
  2066  		}
  2067  		return nil, err
  2068  	}
  2069  
  2070  	// Fetch proposal comments from cache
  2071  	c, err := p.getInvoiceComments(ctx, token)
  2072  	if err != nil {
  2073  		return nil, err
  2074  	}
  2075  
  2076  	// Get the last time the user accessed these comments. This is
  2077  	// a public route so a user may not exist.
  2078  	var accessTime int64
  2079  	if u != nil {
  2080  		if u.ProposalCommentsAccessTimes == nil {
  2081  			u.ProposalCommentsAccessTimes = make(map[string]int64)
  2082  		}
  2083  		accessTime = u.ProposalCommentsAccessTimes[token]
  2084  		u.ProposalCommentsAccessTimes[token] = time.Now().Unix()
  2085  		err = p.db.UserUpdate(*u)
  2086  		if err != nil {
  2087  			return nil, err
  2088  		}
  2089  	}
  2090  
  2091  	return &www.GetCommentsReply{
  2092  		Comments:   c,
  2093  		AccessTime: accessTime,
  2094  	}, nil
  2095  }
  2096  
  2097  func (p *Politeiawww) getInvoiceComments(ctx context.Context, token string) ([]www.Comment, error) {
  2098  	log.Tracef("getInvoiceComments: %v", token)
  2099  
  2100  	dc, err := p.decredGetComments(ctx, token)
  2101  	if err != nil {
  2102  		return nil, fmt.Errorf("decredGetComments: %v", err)
  2103  	}
  2104  
  2105  	// Convert comments and fill in author info.
  2106  	comments := make([]www.Comment, 0, len(dc))
  2107  	for _, v := range dc {
  2108  		c := convertCommentFromDecred(v)
  2109  		u, err := p.db.UserGetByPubKey(c.PublicKey)
  2110  		if err != nil {
  2111  			log.Errorf("getInvoiceComments: UserGetByPubKey: "+
  2112  				"token:%v commentID:%v pubKey:%v err:%v",
  2113  				token, c.CommentID, c.PublicKey, err)
  2114  		} else {
  2115  			c.UserID = u.ID.String()
  2116  			c.Username = u.Username
  2117  		}
  2118  		comments = append(comments, c)
  2119  	}
  2120  
  2121  	return comments, nil
  2122  }
  2123  
  2124  // processPayInvoices looks for all approved invoices and then goes about
  2125  // changing their statuses' to paid.
  2126  func (p *Politeiawww) processPayInvoices(ctx context.Context, u *user.User) (*cms.PayInvoicesReply, error) {
  2127  	log.Tracef("processPayInvoices")
  2128  
  2129  	dbInvs, err := p.cmsDB.InvoicesByStatus(int(cms.InvoiceStatusApproved))
  2130  	if err != nil {
  2131  		return nil, err
  2132  	}
  2133  
  2134  	reply := &cms.PayInvoicesReply{}
  2135  	for _, inv := range dbInvs {
  2136  		// Create the change record.
  2137  		c := mdstream.InvoiceStatusChange{
  2138  			Version:        mdstream.VersionInvoiceStatusChange,
  2139  			AdminPublicKey: u.PublicKey(),
  2140  			Timestamp:      time.Now().Unix(),
  2141  			NewStatus:      cms.InvoiceStatusPaid,
  2142  		}
  2143  		blob, err := mdstream.EncodeInvoiceStatusChange(c)
  2144  		if err != nil {
  2145  			return nil, err
  2146  		}
  2147  
  2148  		challenge, err := util.Random(pd.ChallengeSize)
  2149  		if err != nil {
  2150  			return nil, err
  2151  		}
  2152  
  2153  		pdCommand := pd.UpdateVettedMetadata{
  2154  			Challenge: hex.EncodeToString(challenge),
  2155  			Token:     inv.Token,
  2156  			MDAppend: []pd.MetadataStream{
  2157  				{
  2158  					ID:      mdstream.IDInvoiceStatusChange,
  2159  					Payload: string(blob),
  2160  				},
  2161  			},
  2162  		}
  2163  
  2164  		responseBody, err := p.makeRequest(ctx, http.MethodPost,
  2165  			pd.UpdateVettedMetadataRoute, pdCommand)
  2166  		if err != nil {
  2167  			return nil, err
  2168  		}
  2169  
  2170  		var pdReply pd.UpdateVettedMetadataReply
  2171  		err = json.Unmarshal(responseBody, &pdReply)
  2172  		if err != nil {
  2173  			return nil,
  2174  				fmt.Errorf("Could not unmarshal UpdateVettedMetadataReply: %v",
  2175  					err)
  2176  		}
  2177  
  2178  		// Verify the UpdateVettedMetadata challenge.
  2179  		err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response)
  2180  		if err != nil {
  2181  			return nil, err
  2182  		}
  2183  
  2184  		// Update the database with the metadata changes.
  2185  		inv.Changes = append(inv.Changes, database.InvoiceChange{
  2186  			Timestamp:      c.Timestamp,
  2187  			AdminPublicKey: c.AdminPublicKey,
  2188  			NewStatus:      c.NewStatus,
  2189  			Reason:         c.Reason,
  2190  		})
  2191  		inv.StatusChangeReason = c.Reason
  2192  		inv.Status = c.NewStatus
  2193  
  2194  		err = p.cmsDB.UpdateInvoice(&inv)
  2195  		if err != nil {
  2196  			return nil, err
  2197  		}
  2198  	}
  2199  	return reply, err
  2200  }
  2201  
  2202  // processInvoicePayouts looks for all paid invoices within the given start and end dates.
  2203  func (p *Politeiawww) processInvoicePayouts(lip cms.InvoicePayouts) (*cms.InvoicePayoutsReply, error) {
  2204  	reply := &cms.InvoicePayoutsReply{}
  2205  
  2206  	// check for valid dates
  2207  	if lip.StartTime > lip.EndTime {
  2208  		return nil, www.UserError{
  2209  			ErrorCode: cms.ErrorStatusInvalidDatesRequested,
  2210  		}
  2211  	}
  2212  	dbInvs, err := p.cmsDB.InvoicesByDateRangeStatus(lip.StartTime, lip.EndTime,
  2213  		int(cms.InvoiceStatusPaid))
  2214  	if err != nil {
  2215  		return nil, err
  2216  	}
  2217  	invoices := make([]cms.InvoiceRecord, 0, len(dbInvs))
  2218  	for _, inv := range dbInvs {
  2219  		invRec, err := convertDatabaseInvoiceToInvoiceRecord(inv)
  2220  		if err != nil {
  2221  			return nil, err
  2222  		}
  2223  
  2224  		invoices = append(invoices, invRec)
  2225  	}
  2226  	reply.Invoices = invoices
  2227  	return reply, nil
  2228  }
  2229  
  2230  // processProposalBilling ensures that the request user is either an admin or
  2231  // listed as an owner of the requested proposal.
  2232  func (p *Politeiawww) processProposalBilling(pb cms.ProposalBilling, u *user.User) (*cms.ProposalBillingReply, error) {
  2233  	reply := &cms.ProposalBillingReply{}
  2234  
  2235  	cmsUser, err := p.getCMSUserByID(u.ID.String())
  2236  	if err != nil {
  2237  		return nil, err
  2238  	}
  2239  
  2240  	// Check to see if the user currently listed as owning the proposal
  2241  	propOwned := false
  2242  	for _, prop := range cmsUser.ProposalsOwned {
  2243  		if prop == pb.Token {
  2244  			propOwned = true
  2245  		}
  2246  	}
  2247  	// If it's not owned and it's not an admin requesting return an error.
  2248  	if !cmsUser.Admin && !propOwned {
  2249  		err := www.UserError{
  2250  			ErrorCode: www.ErrorStatusUserActionNotAllowed,
  2251  		}
  2252  		return nil, err
  2253  	}
  2254  
  2255  	invoices, err := p.cmsDB.InvoicesByLineItemsProposalToken(pb.Token)
  2256  	if err != nil {
  2257  		return nil, err
  2258  	}
  2259  	propBilling := make([]cms.ProposalLineItems, 0, len(invoices))
  2260  	for _, inv := range invoices {
  2261  		// All invoices should have only 1 line item returned from that function
  2262  		if len(inv.LineItems) > 1 {
  2263  			continue
  2264  		}
  2265  		lineItem := convertDatabaseInvoiceToProposalLineItems(inv)
  2266  		u, err := p.db.UserGetByPubKey(inv.PublicKey)
  2267  		if err != nil {
  2268  			log.Errorf("processProposalBilling: getUserByPubKey: token:%v "+
  2269  				"pubKey:%v err:%v", pb.Token, inv.PublicKey, err)
  2270  		} else {
  2271  			lineItem.Username = u.Username
  2272  		}
  2273  		propBilling = append(propBilling, lineItem)
  2274  	}
  2275  
  2276  	// Sort returned invoices by month/year submitted
  2277  	sort.Slice(propBilling, func(a, b int) bool {
  2278  		return propBilling[a].Year < propBilling[b].Year ||
  2279  			propBilling[a].Month < propBilling[b].Month
  2280  	})
  2281  
  2282  	reply.BilledLineItems = propBilling
  2283  	return reply, nil
  2284  }
  2285  
  2286  // getInvoiceMonthYear will return the first invoice.json month/year that is
  2287  // found, otherwise 0, 0 in the event of any error.
  2288  func getInvoiceMonthYear(files []www.File) (uint, uint) {
  2289  	for _, v := range files {
  2290  		if v.Name != invoiceFile {
  2291  			continue
  2292  		}
  2293  		data, err := base64.StdEncoding.DecodeString(v.Payload)
  2294  		if err != nil {
  2295  			return 0, 0
  2296  		}
  2297  
  2298  		var invInput cms.InvoiceInput
  2299  		if err := json.Unmarshal(data, &invInput); err != nil {
  2300  			return 0, 0
  2301  		}
  2302  		return invInput.Month, invInput.Year
  2303  	}
  2304  	return 0, 0
  2305  }
  2306  
  2307  func calculatePayout(inv database.Invoice) (cms.Payout, error) {
  2308  	payout := cms.Payout{}
  2309  	var err error
  2310  	var totalLaborMinutes uint
  2311  	var totalExpenses uint
  2312  	var totalSubContractorLabor uint
  2313  	for _, lineItem := range inv.LineItems {
  2314  		switch lineItem.Type {
  2315  		case cms.LineItemTypeLabor:
  2316  			totalLaborMinutes += lineItem.Labor
  2317  		case cms.LineItemTypeSubHours:
  2318  			// If SubContractor line item calculate them per line item and total
  2319  			// them up.
  2320  			totalSubContractorLabor += lineItem.Labor *
  2321  				lineItem.ContractorRate / 60
  2322  		case cms.LineItemTypeExpense, cms.LineItemTypeMisc:
  2323  			totalExpenses += lineItem.Expenses
  2324  		}
  2325  	}
  2326  
  2327  	payout.LaborTotal = totalLaborMinutes * inv.ContractorRate / 60
  2328  	// Add in subcontractor line items to total for payout.
  2329  	payout.LaborTotal += totalSubContractorLabor
  2330  
  2331  	payout.ContractorRate = inv.ContractorRate
  2332  	payout.ExpenseTotal = totalExpenses
  2333  
  2334  	payout.Address = inv.PaymentAddress
  2335  	payout.Token = inv.Token
  2336  	payout.ContractorName = inv.ContractorName
  2337  
  2338  	payout.Month = inv.Month
  2339  	payout.Year = inv.Year
  2340  	payout.Total = payout.LaborTotal + payout.ExpenseTotal
  2341  	if inv.ExchangeRate > 0 {
  2342  		payout.DCRTotal, err = dcrutil.NewAmount(float64(payout.Total) /
  2343  			float64(inv.ExchangeRate))
  2344  		if err != nil {
  2345  			log.Errorf("calculatePayout %v: NewAmount: %v",
  2346  				inv.Token, err)
  2347  		}
  2348  	}
  2349  
  2350  	payout.ExchangeRate = inv.ExchangeRate
  2351  
  2352  	// Range through invoice's documented status changes to find the
  2353  	// time in which the invoice was approved.
  2354  	for _, change := range inv.Changes {
  2355  		if change.NewStatus == cms.InvoiceStatusApproved {
  2356  			payout.ApprovedTime = change.Timestamp
  2357  			break
  2358  		}
  2359  	}
  2360  
  2361  	return payout, nil
  2362  }
  2363  
  2364  func parseInvoiceInput(files []www.File) (*cms.InvoiceInput, error) {
  2365  	data, err := base64.StdEncoding.DecodeString(files[0].Payload)
  2366  	if err != nil {
  2367  		return nil, err
  2368  	}
  2369  
  2370  	// Check to see if the data can be parsed properly into InvoiceInput
  2371  	// struct.
  2372  	var invInput cms.InvoiceInput
  2373  	if err := json.Unmarshal(data, &invInput); err != nil {
  2374  		return nil, www.UserError{
  2375  			ErrorCode: cms.ErrorStatusMalformedInvoiceFile,
  2376  		}
  2377  	}
  2378  	return &invInput, nil
  2379  }
  2380  
  2381  func (p *Politeiawww) processProposalBillingSummary(pbs cms.ProposalBillingSummary) (*cms.ProposalBillingSummaryReply, error) {
  2382  	reply := &cms.ProposalBillingSummaryReply{}
  2383  
  2384  	data, err := p.makeProposalsRequest(http.MethodGet, www.RouteTokenInventory, nil)
  2385  	if err != nil {
  2386  		return nil, err
  2387  	}
  2388  
  2389  	var tvr www.TokenInventoryReply
  2390  	err = json.Unmarshal(data, &tvr)
  2391  	if err != nil {
  2392  		return nil, err
  2393  	}
  2394  
  2395  	approvedProposals := tvr.Approved
  2396  
  2397  	approvedProposalDetails := make([]www.ProposalRecord, 0, len(approvedProposals))
  2398  	if len(approvedProposals) > 0 {
  2399  		startOffset := 0
  2400  		endOffset := www.ProposalListPageSize
  2401  		if endOffset > len(approvedProposals) {
  2402  			endOffset = len(approvedProposals)
  2403  		}
  2404  		for i := endOffset; i <= len(approvedProposals); {
  2405  			// Go fetch proposal information to get name/title.
  2406  			bp := &www.BatchProposals{
  2407  				Tokens: approvedProposals[startOffset:i],
  2408  			}
  2409  
  2410  			data, err := p.makeProposalsRequest(http.MethodPost, www.RouteBatchProposals, bp)
  2411  			if err != nil {
  2412  				return nil, err
  2413  			}
  2414  
  2415  			var bpr www.BatchProposalsReply
  2416  			err = json.Unmarshal(data, &bpr)
  2417  			if err != nil {
  2418  				return nil, err
  2419  			}
  2420  			approvedProposalDetails = append(approvedProposalDetails, bpr.Proposals...)
  2421  
  2422  			startOffset = i
  2423  			i += www.ProposalListPageSize
  2424  			if i > len(approvedProposals) {
  2425  				i = len(approvedProposals)
  2426  			}
  2427  			if i == startOffset {
  2428  				break
  2429  			}
  2430  		}
  2431  	}
  2432  
  2433  	count := pbs.Count
  2434  	if count > cms.ProposalBillingListPageSize {
  2435  		count = cms.ProposalBillingListPageSize
  2436  	}
  2437  
  2438  	proposalInvoices := make(map[string][]database.Invoice, len(approvedProposals))
  2439  	for i, prop := range approvedProposals {
  2440  		if i < pbs.Offset {
  2441  			continue
  2442  		}
  2443  		propInvoices, err := p.cmsDB.InvoicesByLineItemsProposalToken(prop)
  2444  		if err != nil {
  2445  			return nil, err
  2446  		}
  2447  		if len(propInvoices) > 0 {
  2448  			proposalInvoices[prop] = propInvoices
  2449  		} else {
  2450  			proposalInvoices[prop] = make([]database.Invoice, 0)
  2451  		}
  2452  
  2453  		if count != 0 && len(proposalInvoices) >= count {
  2454  			break
  2455  		}
  2456  	}
  2457  
  2458  	spendingSummaries := make([]cms.ProposalSpending, 0, len(proposalInvoices))
  2459  	for prop, invoices := range proposalInvoices {
  2460  		spendingSummary := cms.ProposalSpending{}
  2461  		spendingSummary.Token = prop
  2462  
  2463  		totalSpent := int64(0)
  2464  		for _, dbInv := range invoices {
  2465  			payout, err := calculatePayout(dbInv)
  2466  			if err != nil {
  2467  				return nil, err
  2468  			}
  2469  			totalSpent += int64(payout.Total)
  2470  		}
  2471  		// Look across approved proposals batch reply for proposal name.
  2472  		for _, propDetails := range approvedProposalDetails {
  2473  			if propDetails.CensorshipRecord.Token == prop {
  2474  				spendingSummary.Title = propDetails.Name
  2475  				break
  2476  			}
  2477  		}
  2478  		spendingSummary.TotalBilled = totalSpent
  2479  		spendingSummaries = append(spendingSummaries, spendingSummary)
  2480  	}
  2481  
  2482  	reply.Proposals = spendingSummaries
  2483  
  2484  	return reply, nil
  2485  }
  2486  
  2487  func (p *Politeiawww) processProposalBillingDetails(pbd cms.ProposalBillingDetails) (*cms.ProposalBillingDetailsReply, error) {
  2488  	reply := &cms.ProposalBillingDetailsReply{}
  2489  
  2490  	propInvoices, err := p.cmsDB.InvoicesByLineItemsProposalToken(pbd.Token)
  2491  	if err != nil {
  2492  		return nil, err
  2493  	}
  2494  
  2495  	spendingSummary := cms.ProposalSpending{}
  2496  	spendingSummary.Token = pbd.Token
  2497  
  2498  	invRecs := make([]cms.InvoiceRecord, 0, len(propInvoices))
  2499  	totalSpent := int64(0)
  2500  	for _, dbInv := range propInvoices {
  2501  		u, err := p.db.UserGetByPubKey(dbInv.PublicKey)
  2502  		if err != nil {
  2503  			log.Errorf("getUserByPubKey: token:%v "+
  2504  				"pubKey:%v err:%v", dbInv.PublicKey, err)
  2505  		} else {
  2506  			dbInv.Username = u.Username
  2507  		}
  2508  		payout, err := calculatePayout(dbInv)
  2509  		if err != nil {
  2510  			return nil, err
  2511  		}
  2512  		totalSpent += int64(payout.Total)
  2513  		invRec, err := convertDatabaseInvoiceToInvoiceRecord(dbInv)
  2514  		if err != nil {
  2515  			return nil, err
  2516  		}
  2517  		invRecs = append(invRecs, invRec)
  2518  	}
  2519  
  2520  	data, err := p.makeProposalsRequest(http.MethodGet, "/proposals/"+pbd.Token, nil)
  2521  	if err != nil {
  2522  		return nil, err
  2523  	}
  2524  
  2525  	var pdr www.ProposalDetailsReply
  2526  	err = json.Unmarshal(data, &pdr)
  2527  	if err != nil {
  2528  		return nil, err
  2529  	}
  2530  
  2531  	spendingSummary.Title = pdr.Proposal.Name
  2532  	spendingSummary.Invoices = invRecs
  2533  	spendingSummary.TotalBilled = totalSpent
  2534  
  2535  	reply.Details = spendingSummary
  2536  	return reply, nil
  2537  }