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 }`