From 8cf994d44f27e0ea41483c49ff2d995d0433a4e0 Mon Sep 17 00:00:00 2001 From: ParthaI <47887552+ParthaI@users.noreply.github.com> Date: Mon, 25 Nov 2024 20:56:22 +0700 Subject: [PATCH] Add table scaleway_billing_invoice (#122) (#123) Co-authored-by: tdannenmuller <114987187+tdannenmuller@users.noreply.github.com> Co-authored-by: Ved misra <47312748+misraved@users.noreply.github.com> --- docs/tables/scaleway_billing_invoice.md | 229 ++++++++++++++++ scaleway/plugin.go | 1 + scaleway/table_scaleway_billing_invoice.go | 302 +++++++++++++++++++++ 3 files changed, 532 insertions(+) create mode 100644 docs/tables/scaleway_billing_invoice.md create mode 100644 scaleway/table_scaleway_billing_invoice.go diff --git a/docs/tables/scaleway_billing_invoice.md b/docs/tables/scaleway_billing_invoice.md new file mode 100644 index 0000000..932f141 --- /dev/null +++ b/docs/tables/scaleway_billing_invoice.md @@ -0,0 +1,229 @@ +--- +title: Steampipe Table: scaleway_billing_invoice - Query Scaleway Invoices using SQL +description: Enables users to query Scaleway invoices, offering comprehensive billing and usage details for Scaleway cloud services. +--- + +# Table: scaleway_billing_invoice - Query Scaleway invoices using SQL + +Scaleway invoices provide detailed records of charges for using Scaleway's cloud services. These invoices include a complete breakdown of costs for various resources and services within a Scaleway account. + +## Table Usage Guide + +The `scaleway_billing_invoice` table offers insights into billing information in Scaleway. It allows finance managers or cloud administrators to query invoice-specific details such as total amounts, billing periods, and associated organizations. Use this table to track expenses, verify charges, and manage cloud spending across different projects and timeframes. + +## Examples + +### Explore basic details of Scaleway invoices +Retrieve invoice identifiers, associated organizations, and billing periods to track cloud expenses effectively. + +```sql+postgres +select + id, + organization_id, + billing_period, + total_taxed_amount, + state, + currency +from + scaleway_billing_invoice; +``` + +```sql+sqlite +select + id, + organization_id, + billing_period, + total_taxed_amount, + state, + currency +from + scaleway_billing_invoice; +``` + +### Get total billed amount for each organization +Calculate the total amount billed for each organization to analyze spending across different entities. + +```sql+postgres +select + organization_id, + sum(total_taxed_amount) as total_billed, + currency +from + scaleway_billing_invoice +group by + organization_id, + currency; +``` + +```sql+sqlite +select + organization_id, + sum(total_taxed_amount) as total_billed, + currency +from + scaleway_billing_invoice +group by + organization_id, + currency; +``` + +### Find invoices with high discount amounts +Identify invoices with substantial discounts to understand cost-saving opportunities. + +```sql+postgres +select + id, + billing_period, + total_discount_amount, + total_taxed_amount, + currency +from + scaleway_billing_invoice +where + total_discount_amount > 1000 +order by + total_discount_amount desc; +``` + +```sql+sqlite +select + id, + billing_period, + total_discount_amount, + total_taxed_amount, + currency +from + scaleway_billing_invoice +where + total_discount_amount > 1000 +order by + total_discount_amount desc; +``` + +### List invoices within a specific date range +Retrieve invoices for a defined period to assist with financial reviews or audits. + +```sql+postgres +select + id, + billing_period, + total_taxed_amount, + issued_date, + currency +from + scaleway_billing_invoice +where + issued_date between '2023-01-01' and '2023-12-31' +order by + issued_date; +``` + +```sql+sqlite +select + id, + billing_period, + total_taxed_amount, + issued_date, + currency +from + scaleway_billing_invoice +where + issued_date between '2023-01-01' and '2023-12-31' +order by + issued_date; +``` + +### Get the average invoice amount by month +Analyze monthly spending patterns by calculating the average invoice amount. + +```sql+postgres +select + date_trunc('month', issued_date) as month, + avg(total_taxed_amount) as average_invoice_amount, + currency +from + scaleway_billing_invoice +group by + date_trunc('month', issued_date), + currency +order by + month; +``` + +```sql+sqlite +select + strftime('%Y-%m', issued_date) as month, + avg(total_taxed_amount) as average_invoice_amount, + currency +from + scaleway_billing_invoice +group by + strftime('%Y-%m', issued_date), + currency +order by + month; +``` + +### Compare total taxed and untaxed amounts +Analyze the tax impact by comparing taxed and untaxed amounts. + +```sql+postgres +select + id, + total_untaxed_amount, + total_taxed_amount, + total_taxed_amount - total_untaxed_amount as tax_amount, + currency +from + scaleway_billing_invoice +order by + tax_amount desc; +``` + +```sql+sqlite +select + id, + total_untaxed_amount, + total_taxed_amount, + total_taxed_amount - total_untaxed_amount as tax_amount, + currency +from + scaleway_billing_invoice +order by + tax_amount desc; +``` + +### Examine discounts and their impact +Evaluate the effect of discounts on invoices by comparing undiscounted and final taxed amounts. + +```sql+postgres +select + id, + total_undiscount_amount, + total_discount_amount, + total_taxed_amount, + total_discount_amount / total_undiscount_amount * 100 as discount_percentage, + currency +from + scaleway_billing_invoice +where + total_discount_amount > 0 +order by + discount_percentage desc; +``` + +```sql+sqlite +select + id, + total_undiscount_amount, + total_discount_amount, + total_taxed_amount, + total_discount_amount / total_undiscount_amount * 100 as discount_percentage, + currency +from + scaleway_billing_invoice +where + total_discount_amount > 0 +order by + discount_percentage desc; +``` diff --git a/scaleway/plugin.go b/scaleway/plugin.go index e370cc7..2a13c98 100644 --- a/scaleway/plugin.go +++ b/scaleway/plugin.go @@ -23,6 +23,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "scaleway_account_ssh_key": tableScalewayAccountSSHKey(ctx), "scaleway_baremetal_server": tableScalewayBaremetalServer(ctx), "scaleway_billing_consumption": tableScalewayBillingConsumption(ctx), + "scaleway_billing_invoice": tableScalewayBillingInvoice(ctx), "scaleway_iam_api_key": tableScalewayIamAPIKey(ctx), "scaleway_iam_user": tableScalewayIamUser(ctx), "scaleway_instance_image": tableScalewayInstanceImage(ctx), diff --git a/scaleway/table_scaleway_billing_invoice.go b/scaleway/table_scaleway_billing_invoice.go new file mode 100644 index 0000000..06f3ac0 --- /dev/null +++ b/scaleway/table_scaleway_billing_invoice.go @@ -0,0 +1,302 @@ +package scaleway + +import ( + "context" + "fmt" + "math" + + billing "github.com/scaleway/scaleway-sdk-go/api/billing/v2beta1" + + "github.com/scaleway/scaleway-sdk-go/scw" + "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" +) + +func tableScalewayBillingInvoice(ctx context.Context) *plugin.Table { + plugin.Logger(ctx).Debug("Initializing Scaleway invoices table") + return &plugin.Table{ + Name: "scaleway_billing_invoice", + Description: "Scaleway Billing Invoice", + List: &plugin.ListConfig{ + Hydrate: listScalewayInvoices, + KeyColumns: []*plugin.KeyColumn{ + {Name: "organization_id", Require: plugin.Optional}, + {Name: "type", Require: plugin.Optional}, + {Name: "billing_period", Require: plugin.Optional, Operators: []string{">=", "<="}}, + }, + }, + Get: &plugin.GetConfig{ + Hydrate: getScalewayInvoice, + KeyColumns: plugin.SingleColumn("id"), + // When an incorrect Invoice ID is provided, no "not found" error is returned. + // Instead, a timeout error is encountered, as shown below. + // Error: rpc error: code = DeadlineExceeded desc = scaleway-sdk-go: error executing request: Get "https://api.scaleway.com/account/v3/projects/5575456ffhgfh": dial tcp: lookup api.scaleway.com: i/o timeout. + }, + Columns: []*plugin.Column{ + { + Name: "id", + Type: proto.ColumnType_STRING, + Description: "The unique identifier of the invoices.", + Transform: transform.FromField("ID"), + }, + { + Name: "organization_id", + Type: proto.ColumnType_STRING, + Description: "The organization ID associated with the invoices.", + Transform: transform.FromField("OrganizationID"), + }, + { + Name: "type", + Type: proto.ColumnType_STRING, + Description: "The type of the invoices.", + }, + { + Name: "state", + Type: proto.ColumnType_STRING, + Description: "The current state of the invoices.", + }, + { + Name: "number", + Type: proto.ColumnType_INT, + Description: "The invoices number.", + }, + { + Name: "seller_name", + Type: proto.ColumnType_STRING, + Description: "The name of the seller.", + }, + { + Name: "organization_name", + Type: proto.ColumnType_STRING, + Description: "The organization name associated with the invoices.", + }, + { + Name: "start_date", + Type: proto.ColumnType_TIMESTAMP, + Description: "The start date of the billing period.", + }, + { + Name: "stop_date", + Type: proto.ColumnType_TIMESTAMP, + Description: "The end date of the billing period.", + }, + { + Name: "billing_period", + Type: proto.ColumnType_TIMESTAMP, + Description: "The billing period for the invoices.", + }, + { + Name: "issued_date", + Type: proto.ColumnType_TIMESTAMP, + Description: "The date when the invoices were issued.", + }, + { + Name: "due_date", + Type: proto.ColumnType_TIMESTAMP, + Description: "The due date for the invoice payment.", + }, + { + Name: "total_untaxed_amount", + Type: proto.ColumnType_DOUBLE, + Description: "The total untaxed amount of the invoices.", + Transform: transform.FromField("TotalUntaxed").Transform(extractAmount), + }, + { + Name: "total_taxed_amount", + Type: proto.ColumnType_DOUBLE, + Description: "The total taxed amount of the invoices.", + Transform: transform.FromField("TotalTaxed").Transform(extractAmount), + }, + { + Name: "total_discount_amount", + Type: proto.ColumnType_DOUBLE, + Description: "The total discount amount of the invoice (always positive).", + Transform: transform.FromField("TotalDiscount").Transform(extractAmount), + }, + { + Name: "total_undiscount_amount", + Type: proto.ColumnType_DOUBLE, + Description: "The total undiscounted amount of the invoices.", + Transform: transform.FromField("TotalUndiscount").Transform(extractAmount), + }, + { + Name: "currency", + Type: proto.ColumnType_STRING, + Description: "The currency used for all monetary values in the invoices.", + Transform: transform.FromField("TotalTaxed").Transform(extractCurrency), + }, + + // Scaleway standard columns + { + Name: "organization", + Description: "The ID of the organization where the server resides.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("OrganizationID"), + }, + + // Steampipe standard columns + { + Name: "title", + Description: "Title of the resource.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Number"), + }, + }, + } +} + +//// LIST FUNCTION + +func listScalewayInvoices(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + // Get client configuration + client, err := getSessionConfig(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("scaleway_billing_invoice.listScalewayInvoices", "connection_error", err) + return nil, err + } + + billingAPI := billing.NewAPI(client) + + // Prepare the request + req := &billing.ListInvoicesRequest{} + + // Get the organization_id from the config + scalewayConfig := GetConfig(d.Connection) + var organizationID string + if scalewayConfig.OrganizationID != nil { + organizationID = *scalewayConfig.OrganizationID + } + + // Check if organization_id is specified in the query parameter + if d.EqualsQualString("organization_id") != "" { + organizationID = d.EqualsQualString("organization_id") + } + + // Set the organization_id in the request if it's available + if organizationID != "" { + req.OrganizationID = &organizationID + } + + if d.EqualsQualString("type") != "" { + req.InvoiceType = billing.InvoiceType(d.EqualsQualString("type")) + } + + quals := d.Quals + + if quals["billing_period"] != nil { + for _, q := range quals["billing_period"].Quals { + billingPeriod := q.Value.GetTimestampValue().AsTime() + switch q.Operator { + case ">=": + req.BillingPeriodStartAfter = &billingPeriod + case "<=": + req.BillingPeriodStartBefore = &billingPeriod + } + } + } + + var count int + + for { + // Make the API request to list invoices + resp, err := billingAPI.ListInvoices(req) + if err != nil { + plugin.Logger(ctx).Error("scaleway_billing_invoice.listScalewayInvoices", "api_error", err) + return nil, err + } + + for _, invoice := range resp.Invoices { + + d.StreamListItem(ctx, invoice) + + // Increase the resource count by 1 + count++ + + // Context can be canceled due to manual cancellation or the limit has been hit + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + + if resp.TotalCount == uint64(count) { + break + } + req.Page = scw.Int32Ptr(*req.Page + 1) + } + + return nil, nil +} + +//// HYDRATE FUNCTION + +func getScalewayInvoice(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + // Get client configuration + client, err := getSessionConfig(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("scaleway_billing_invoice.getScalewayInvoice", "connection_error", err) + return nil, err + } + + // Empty check + if d.EqualsQualString("id") == "" { + return nil, nil + } + + billingAPI := billing.NewAPI(client) + + // Prepare the request + req := &billing.GetInvoiceRequest{ + InvoiceID: d.EqualsQualString("id"), + } + + resp, err := billingAPI.GetInvoice(req) + if err != nil { + plugin.Logger(ctx).Error("scaleway_billing_invoice.getScalewayInvoice", "api_error", err) + return nil, err + } + + if resp != nil { + return resp, nil + } + + return nil, nil +} + +//// TRANSFORM FUNCTIONS + +func extractAmount(_ context.Context, d *transform.TransformData) (interface{}, error) { + + if d.Value == nil { + return nil, nil + } + + money, ok := d.Value.(*scw.Money) + if !ok || money == nil { + return nil, nil + } + + amount := float64(money.Units) + float64(money.Nanos)/1e9 + if d.ColumnName == "total_discount_amount" { + return math.Abs(amount), nil + } + return amount, nil +} + +func extractCurrency(ctx context.Context, d *transform.TransformData) (interface{}, error) { + + if d.Value == nil { + return nil, nil + } + + switch v := d.Value.(type) { + case *scw.Money: + if v == nil { + return nil, nil + } + return v.CurrencyCode, nil + default: + plugin.Logger(ctx).Warn("extractCurrency: unexpected type", "type", fmt.Sprintf("%T", d.Value)) + return nil, nil + } +}