github.com/decred/politeia@v1.4.0/politeiawww/cmd/cmswww/newinvoice.go (about)

     1  // Copyright (c) 2017-2019 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 main
     6  
     7  import (
     8  	"bufio"
     9  	"encoding/base64"
    10  	"encoding/hex"
    11  	"encoding/json"
    12  	"fmt"
    13  	"os"
    14  	"path/filepath"
    15  	"strconv"
    16  	"strings"
    17  
    18  	"github.com/decred/politeia/politeiad/api/v1/mime"
    19  	cms "github.com/decred/politeia/politeiawww/api/cms/v1"
    20  	www "github.com/decred/politeia/politeiawww/api/www/v1"
    21  	"github.com/decred/politeia/politeiawww/cmd/shared"
    22  	"github.com/decred/politeia/util"
    23  )
    24  
    25  // NewInvoiceCmd submits a new invoice.
    26  type NewInvoiceCmd struct {
    27  	Args struct {
    28  		Month       uint     `positional-arg-name:"month"`           // Invoice Month
    29  		Year        uint     `positional-arg-name:"year"`            // Invoice Year
    30  		CSV         string   `positional-arg-name:"csvfile"`         // Invoice CSV file
    31  		Attachments []string `positional-arg-name:"attachmentfiles"` // Invoice attachment files
    32  	} `positional-args:"true" optional:"true"`
    33  	Name           string `long:"name" optional:"true" description:"Full name of the contractor"`
    34  	Contact        string `long:"contact" optional:"true" description:"Email address or contact of the contractor"`
    35  	Location       string `long:"location" optional:"true" description:"Location (e.g. Dallas, TX, USA) of the contractor"`
    36  	PaymentAddress string `long:"paymentaddress" optional:"true" description:"Payment address for this invoice."`
    37  	Rate           string `long:"rate" optional:"true" description:"Hourly rate for labor."`
    38  }
    39  
    40  // Execute executes the new invoice command.
    41  func (cmd *NewInvoiceCmd) Execute(args []string) error {
    42  	month := cmd.Args.Month
    43  	year := cmd.Args.Year
    44  	csvFile := cmd.Args.CSV
    45  	attachmentFiles := cmd.Args.Attachments
    46  
    47  	if csvFile == "" {
    48  		return errInvoiceCSVNotFound
    49  	}
    50  
    51  	// Check for user identity
    52  	if cfg.Identity == nil {
    53  		return shared.ErrUserIdentityNotFound
    54  	}
    55  
    56  	// Get server public key
    57  	vr, err := client.Version()
    58  	if err != nil {
    59  		return err
    60  	}
    61  
    62  	if cmd.Name == "" || cmd.Location == "" || cmd.PaymentAddress == "" ||
    63  		cmd.Contact == "" || cmd.Rate == "" {
    64  		reader := bufio.NewReader(os.Stdin)
    65  		if cmd.Name == "" {
    66  			fmt.Print("Enter your name to associate with this invoice: ")
    67  			cmd.Name, _ = reader.ReadString('\n')
    68  		}
    69  		if cmd.Contact == "" {
    70  			fmt.Print("Enter your contact information (email/matrix) to associate with this invoice: ")
    71  			cmd.Contact, _ = reader.ReadString('\n')
    72  		}
    73  		if cmd.Location == "" {
    74  			fmt.Print("Enter your location to associate with this invoice: ")
    75  			cmd.Location, _ = reader.ReadString('\n')
    76  		}
    77  		if cmd.PaymentAddress == "" {
    78  			fmt.Print("Enter payment address for this invoice: ")
    79  			cmd.PaymentAddress, _ = reader.ReadString('\n')
    80  		}
    81  		if cmd.Rate == "" {
    82  			fmt.Print("Enter hourly rate for this invoice (in USD): ")
    83  			cmd.Rate, _ = reader.ReadString('\n')
    84  		}
    85  		fmt.Print("\nPlease carefully review your information and ensure it's " +
    86  			"correct. If not, press Ctrl + C to exit. Or, press Enter to continue " +
    87  			"your registration.")
    88  		reader.ReadString('\n')
    89  	}
    90  
    91  	var csv []byte
    92  	files := make([]www.File, 0, www.PolicyMaxImages+1)
    93  	// Read csv file into memory and convert to type File
    94  	fpath := util.CleanAndExpandPath(csvFile)
    95  
    96  	csv, err = os.ReadFile(fpath)
    97  	if err != nil {
    98  		return fmt.Errorf("ReadFile %v: %v", fpath, err)
    99  	}
   100  
   101  	invInput, err := validateParseCSV(csv)
   102  	if err != nil {
   103  		return fmt.Errorf("Parsing CSV failed: %v", err)
   104  	}
   105  
   106  	ier := &cms.InvoiceExchangeRate{
   107  		Month: month,
   108  		Year:  year,
   109  	}
   110  
   111  	// Send request
   112  	ierr, err := client.InvoiceExchangeRate(ier)
   113  	if err != nil {
   114  		return err
   115  	}
   116  
   117  	invInput.Month = month
   118  	invInput.Year = year
   119  	invInput.ExchangeRate = ierr.ExchangeRate
   120  	invInput.ContractorName = strings.TrimSpace(cmd.Name)
   121  	invInput.ContractorLocation = strings.TrimSpace(cmd.Location)
   122  	invInput.ContractorContact = strings.TrimSpace(cmd.Contact)
   123  	invInput.PaymentAddress = strings.TrimSpace(cmd.PaymentAddress)
   124  	invInput.Version = cms.InvoiceInputVersion
   125  
   126  	rate, err := strconv.Atoi(strings.TrimSpace(cmd.Rate))
   127  	if err != nil {
   128  		return fmt.Errorf("invalid rate entered, please try again")
   129  	}
   130  	invInput.ContractorRate = uint(rate * 100)
   131  
   132  	// Print request details
   133  	err = shared.PrintJSON(invInput)
   134  	if err != nil {
   135  		return err
   136  	}
   137  	b, err := json.Marshal(invInput)
   138  	if err != nil {
   139  		return fmt.Errorf("Marshal: %v", err)
   140  	}
   141  
   142  	f := www.File{
   143  		Name:    "invoice.json",
   144  		MIME:    mime.DetectMimeType(b),
   145  		Digest:  hex.EncodeToString(util.Digest(b)),
   146  		Payload: base64.StdEncoding.EncodeToString(b),
   147  	}
   148  
   149  	files = append(files, f)
   150  
   151  	// Read attachment files into memory and convert to type File
   152  	for _, file := range attachmentFiles {
   153  		path := util.CleanAndExpandPath(file)
   154  		attachment, err := os.ReadFile(path)
   155  		if err != nil {
   156  			return fmt.Errorf("ReadFile %v: %v", path, err)
   157  		}
   158  
   159  		f := www.File{
   160  			Name:    filepath.Base(file),
   161  			MIME:    mime.DetectMimeType(attachment),
   162  			Digest:  hex.EncodeToString(util.Digest(attachment)),
   163  			Payload: base64.StdEncoding.EncodeToString(attachment),
   164  		}
   165  
   166  		files = append(files, f)
   167  	}
   168  
   169  	// Compute merkle root and sign it
   170  	sig, err := signedMerkleRoot(files, nil, cfg.Identity)
   171  	if err != nil {
   172  		return fmt.Errorf("SignMerkleRoot: %v", err)
   173  	}
   174  
   175  	// Setup new proposal request
   176  	ni := &cms.NewInvoice{
   177  		Files:     files,
   178  		PublicKey: hex.EncodeToString(cfg.Identity.Public.Key[:]),
   179  		Signature: sig,
   180  		Month:     month,
   181  		Year:      year,
   182  	}
   183  
   184  	// Print request details
   185  	err = shared.PrintJSON(ni)
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	// Send request
   191  	nir, err := client.NewInvoice(ni)
   192  	if err != nil {
   193  		return err
   194  	}
   195  
   196  	// Verify the censorship record
   197  	ir := cms.InvoiceRecord{
   198  		Files:            ni.Files,
   199  		PublicKey:        ni.PublicKey,
   200  		Signature:        ni.Signature,
   201  		CensorshipRecord: nir.CensorshipRecord,
   202  	}
   203  	err = verifyInvoice(ir, vr.PubKey)
   204  	if err != nil {
   205  		return fmt.Errorf("unable to verify invoice %v: %v",
   206  			ir.CensorshipRecord.Token, err)
   207  	}
   208  
   209  	// Print response details
   210  	return shared.PrintJSON(nir)
   211  }
   212  
   213  const newInvoiceHelpMsg = `newinvoice [flags] "csvFile" "attachmentFiles"
   214  
   215  Submit a new invoice to Politeia. Invoice must be a csv file. Accepted
   216  attachment filetypes: png or plain text.
   217  
   218  An invoice csv line item should use the following format:
   219  
   220  type,domain,subdomain,description,proposalToken,labor,expenses,subUserID,subRate
   221  
   222  Valid types   : labor, expense, misc, sub
   223  Labor units   : hours
   224  Expenses units: USD
   225  
   226  Example csv lines:
   227  labor,random,subdomain,description,,180,0,,0
   228  expense,marketing,subdomain,description,,0,1500,,0
   229  
   230  Arguments:
   231  1. month             (string, required)   Month (MM, 01-12)
   232  2. year              (string, required)   Year (YYYY)
   233  3. csvFile           (string, required)   Invoice CSV file
   234  4. attachmentFiles   (string, optional)   Attachments
   235  
   236  Flags:
   237    --name              (string, optional)   Fill in contractor name
   238    --contact           (string, optional)   Fill in email address or contact of the contractor
   239    --location          (string, optional)   Fill in contractor location (e.g. Dallas, TX, USA) of the contractor
   240    --paymentaddress    (string, optional)   Fill in payment address for this invoice.
   241    --rate              (string, optional)   Fill in contractor pay rate for labor (USD).
   242  
   243  Result:
   244  {
   245    "files": [
   246      {
   247        "name":      (string)  Filename
   248        "mime":      (string)  Mime type
   249        "digest":    (string)  File digest
   250        "payload":   (string)  File payload
   251      }
   252    ],
   253    "publickey":   (string)  Public key of user
   254    "signature":   (string)  Signed merkel root of files in invoice
   255  }`