satellite/{console,consoleweb,consoleapi}: add new endpoint for paged invoices
This change adds a new endpoint for listing invoices for billing history This endpoint will replace the billing-history endpoint used on the front end since were only interested in listing invoices. Issue: https://github.com/storj/storj/issues/5479 Change-Id: I4730f5dc497245c6730e60b7f9986554479d1d3b
This commit is contained in:
parent
f0829d5961
commit
6219aba40c
@ -21,6 +21,27 @@ type BillingHistoryItem struct {
|
||||
Type BillingHistoryItemType `json:"type"`
|
||||
}
|
||||
|
||||
// BillingHistoryCursor holds info for billing history
|
||||
// cursor pagination.
|
||||
type BillingHistoryCursor struct {
|
||||
Limit int
|
||||
|
||||
// StartingAfter is the last ID of the previous page.
|
||||
// The next page will start after this ID.
|
||||
StartingAfter string
|
||||
// EndingBefore is the id before which a page should end.
|
||||
EndingBefore string
|
||||
}
|
||||
|
||||
// BillingHistoryPage returns paginated billing history items.
|
||||
type BillingHistoryPage struct {
|
||||
Items []BillingHistoryItem `json:"items"`
|
||||
// Next indicates whether there are more events to retrieve.
|
||||
Next bool `json:"next"`
|
||||
// Previous indicates whether there are previous items.
|
||||
Previous bool `json:"previous"`
|
||||
}
|
||||
|
||||
// BillingHistoryItemType indicates type of billing history item.
|
||||
type BillingHistoryItemType int
|
||||
|
||||
|
@ -342,6 +342,52 @@ func (p *Payments) BillingHistory(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// InvoiceHistory returns a paged list of invoice history items for payment account.
|
||||
func (p *Payments) InvoiceHistory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
limitParam := query.Get("limit")
|
||||
if limitParam == "" {
|
||||
p.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("parameter 'limit' is required"))
|
||||
return
|
||||
}
|
||||
|
||||
limit, pErr := strconv.ParseUint(limitParam, 10, 32)
|
||||
if pErr != nil {
|
||||
p.serveJSONError(ctx, w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
startParam := query.Get("starting_after")
|
||||
endParam := query.Get("ending_before")
|
||||
|
||||
history, err := p.service.Payments().InvoiceHistory(ctx, console.BillingHistoryCursor{
|
||||
Limit: int(limit),
|
||||
StartingAfter: startParam,
|
||||
EndingBefore: endParam,
|
||||
})
|
||||
if err != nil {
|
||||
if console.ErrUnauthorized.Has(err) {
|
||||
p.serveJSONError(ctx, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(history)
|
||||
if err != nil {
|
||||
p.log.Error("failed to write json history response", zap.Error(ErrPaymentsAPI.Wrap(err)))
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyCouponCode applies a coupon code to the user's account.
|
||||
func (p *Payments) ApplyCouponCode(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
@ -249,6 +249,7 @@ func TestPayments(t *testing.T) {
|
||||
"/payments/cards",
|
||||
"/payments/account/balance",
|
||||
"/payments/billing-history",
|
||||
"/payments/invoice-history",
|
||||
"/payments/account/charges?from=1619827200&to=1620844320",
|
||||
} {
|
||||
resp, body := test.request(http.MethodGet, path, nil)
|
||||
@ -277,6 +278,12 @@ func TestPayments(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
{ // Get_InvoiceHistory
|
||||
resp, body := test.request(http.MethodGet, "/payments/invoice-history?limit=1", nil)
|
||||
require.Contains(t, body, "items")
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
{ // Get_AccountChargesByDateRange
|
||||
resp, body := test.request(http.MethodGet, "/payments/account/charges?from=1619827200&to=1620844320", nil)
|
||||
require.Contains(t, body, "egress")
|
||||
|
@ -336,6 +336,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
|
||||
paymentsRouter.HandleFunc("/wallet/payments", paymentController.WalletPayments).Methods(http.MethodGet, http.MethodOptions)
|
||||
paymentsRouter.HandleFunc("/wallet/payments-with-confirmations", paymentController.WalletPaymentsWithConfirmations).Methods(http.MethodGet, http.MethodOptions)
|
||||
paymentsRouter.HandleFunc("/billing-history", paymentController.BillingHistory).Methods(http.MethodGet, http.MethodOptions)
|
||||
paymentsRouter.HandleFunc("/invoice-history", paymentController.InvoiceHistory).Methods(http.MethodGet, http.MethodOptions)
|
||||
paymentsRouter.Handle("/coupon/apply", server.userIDRateLimiter.Limit(http.HandlerFunc(paymentController.ApplyCouponCode))).Methods(http.MethodPatch, http.MethodOptions)
|
||||
paymentsRouter.HandleFunc("/coupon", paymentController.GetCoupon).Methods(http.MethodGet, http.MethodOptions)
|
||||
paymentsRouter.HandleFunc("/pricing", paymentController.GetProjectUsagePriceModel).Methods(http.MethodGet, http.MethodOptions)
|
||||
|
@ -571,6 +571,45 @@ func (payment Payments) BillingHistory(ctx context.Context) (billingHistory []*B
|
||||
return billingHistory, nil
|
||||
}
|
||||
|
||||
// InvoiceHistory returns a paged list of invoices for payment account.
|
||||
func (payment Payments) InvoiceHistory(ctx context.Context, cursor BillingHistoryCursor) (history *BillingHistoryPage, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
user, err := payment.service.getUserAndAuditLog(ctx, "get invoice history")
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
page, err := payment.service.accounts.Invoices().ListPaged(ctx, user.ID, payments.InvoiceCursor{
|
||||
Limit: cursor.Limit,
|
||||
StartingAfter: cursor.StartingAfter,
|
||||
EndingBefore: cursor.EndingBefore,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
var historyItems []BillingHistoryItem
|
||||
for _, invoice := range page.Invoices {
|
||||
historyItems = append(historyItems, BillingHistoryItem{
|
||||
ID: invoice.ID,
|
||||
Description: invoice.Description,
|
||||
Amount: invoice.Amount,
|
||||
Status: invoice.Status,
|
||||
Link: invoice.Link,
|
||||
End: invoice.End,
|
||||
Start: invoice.Start,
|
||||
Type: Invoice,
|
||||
})
|
||||
}
|
||||
|
||||
return &BillingHistoryPage{
|
||||
Items: historyItems,
|
||||
Next: page.Next,
|
||||
Previous: page.Previous,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// checkOutstandingInvoice returns if the payment account has any unpaid/outstanding invoices or/and invoice items.
|
||||
func (payment Payments) checkOutstandingInvoice(ctx context.Context) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
@ -35,6 +35,8 @@ type Invoices interface {
|
||||
Pay(ctx context.Context, invoiceID, paymentMethodID string) (*Invoice, error)
|
||||
// List returns a list of invoices for a given payment account.
|
||||
List(ctx context.Context, userID uuid.UUID) ([]Invoice, error)
|
||||
// ListPaged returns a paged list of invoices.
|
||||
ListPaged(ctx context.Context, userID uuid.UUID, cursor InvoiceCursor) (*InvoicePage, error)
|
||||
// ListFailed returns a list of failed invoices.
|
||||
ListFailed(ctx context.Context, userID *uuid.UUID) ([]Invoice, error)
|
||||
// ListWithDiscounts returns a list of invoices and coupon usages for a given payment account.
|
||||
@ -59,6 +61,27 @@ type Invoice struct {
|
||||
End time.Time `json:"end"`
|
||||
}
|
||||
|
||||
// InvoiceCursor holds info for invoices
|
||||
// cursor pagination.
|
||||
type InvoiceCursor struct {
|
||||
Limit int
|
||||
|
||||
// StartingAfter is the last invoice ID of the previous page.
|
||||
// The next page will start after this ID.
|
||||
StartingAfter string
|
||||
// EndingBefore is the id before which a page should end.
|
||||
EndingBefore string
|
||||
}
|
||||
|
||||
// InvoicePage returns paginated invoices.
|
||||
type InvoicePage struct {
|
||||
Invoices []Invoice
|
||||
// Next indicates whether there are more events to retrieve.
|
||||
Next bool
|
||||
// Previous indicates whether there are previous items.
|
||||
Previous bool
|
||||
}
|
||||
|
||||
// CouponUsage describes the usage of a coupon on an invoice.
|
||||
type CouponUsage struct {
|
||||
Coupon Coupon
|
||||
|
@ -268,6 +268,61 @@ func (invoices *invoices) ListFailed(ctx context.Context, userID *uuid.UUID) (in
|
||||
return invoicesList, nil
|
||||
}
|
||||
|
||||
func (invoices *invoices) ListPaged(ctx context.Context, userID uuid.UUID, cursor payments.InvoiceCursor) (page *payments.InvoicePage, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
customerID, err := invoices.service.db.Customers().GetCustomerID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
page = &payments.InvoicePage{}
|
||||
params := &stripe.InvoiceListParams{
|
||||
ListParams: stripe.ListParams{Context: ctx},
|
||||
Customer: &customerID,
|
||||
}
|
||||
|
||||
params.Limit = stripe.Int64(int64(cursor.Limit))
|
||||
params.Single = true
|
||||
if cursor.StartingAfter != "" {
|
||||
page.Previous = true
|
||||
params.StartingAfter = stripe.String(cursor.StartingAfter)
|
||||
} else if cursor.EndingBefore != "" {
|
||||
params.EndingBefore = stripe.String(cursor.EndingBefore)
|
||||
}
|
||||
|
||||
invoicesIterator := invoices.service.stripeClient.Invoices().List(params)
|
||||
for invoicesIterator.Next() {
|
||||
stripeInvoice := invoicesIterator.Invoice()
|
||||
|
||||
total := stripeInvoice.Total
|
||||
for _, line := range stripeInvoice.Lines.Data {
|
||||
// If amount is negative, this is a coupon or a credit line item.
|
||||
// Add them to the total.
|
||||
if line.Amount < 0 {
|
||||
total -= line.Amount
|
||||
}
|
||||
}
|
||||
|
||||
page.Invoices = append(page.Invoices, payments.Invoice{
|
||||
ID: stripeInvoice.ID,
|
||||
CustomerID: stripeInvoice.Customer.ID,
|
||||
Description: stripeInvoice.Description,
|
||||
Amount: total,
|
||||
Status: string(stripeInvoice.Status),
|
||||
Link: stripeInvoice.InvoicePDF,
|
||||
Start: time.Unix(stripeInvoice.PeriodStart, 0),
|
||||
})
|
||||
}
|
||||
|
||||
if err = invoicesIterator.Err(); err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
page.Next = invoicesIterator.Meta().HasMore || cursor.EndingBefore != ""
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// ListWithDiscounts returns a list of invoices and coupon usages for a given payment account.
|
||||
func (invoices *invoices) ListWithDiscounts(ctx context.Context, userID uuid.UUID) (invoicesList []payments.Invoice, couponUsages []payments.CouponUsage, err error) {
|
||||
defer mon.Task()(&ctx, userID)(&err)
|
||||
@ -410,6 +465,10 @@ func convertStatus(stripestatus stripe.InvoiceStatus) string {
|
||||
|
||||
// isInvoiceFailed returns whether an invoice has failed.
|
||||
func (invoices *invoices) isInvoiceFailed(invoice *stripe.Invoice) bool {
|
||||
if invoice.Status != stripe.InvoiceStatusOpen {
|
||||
return false
|
||||
}
|
||||
|
||||
if invoice.DueDate > 0 {
|
||||
// https://github.com/storj/storj/blob/77bf88e916a10dc898ebb594eafac667ed4426cd/satellite/payments/stripecoinpayments/service.go#L781-L787
|
||||
invoices.service.log.Info("Skipping invoice marked for manual payment",
|
||||
|
Loading…
Reference in New Issue
Block a user