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 }