Ctrl K
ComunidadGuías…

internal/repository/clickhouse/reports_metrics.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package clickhouse

import (
	"strings"

	"github.com/flexprice/flexprice/internal/types"
)

// billingSQL holds a metric's ClickHouse fragments.
type billingSQL struct {
	Table   string
	TimeCol string
	Measure string
	// Aggregate name and argument.
	Agg string
	Arg string
	// Conditional Measure for ratio metrics.
	CompareMeasure func(cond string) string
	Where          []string
	// Dimension expressions differing from name.
	DimExprs map[string]string
	// Replaces the generic builder.
	Build func(buildInput) (*reportQuery, error)
}

func (s *billingSQL) dimExpr(d string) string {
	if e, ok := s.DimExprs[d]; ok {
		return e
	}
	return d
}

const agingBucketExpr = "multiIf(nullIf(due_date, toDateTime64(0, 6)) IS NULL, 'No due date', " +
	"dateDiff('day', due_date, now64(6, 'UTC'), 'UTC') <= 0, 'Current', " +
	"dateDiff('day', due_date, now64(6, 'UTC'), 'UTC') <= 30, '1-30', " +
	"dateDiff('day', due_date, now64(6, 'UTC'), 'UTC') <= 60, '31-60', " +
	"dateDiff('day', due_date, now64(6, 'UTC'), 'UTC') <= 90, '61-90', '90+')"

func invoiceFinalizedTime(prefix string) string { return prefix + "finalized_at" }

func invoiceTotalSQL(timeCol string) *billingSQL {
	return &billingSQL{
		Table:   "invoices",
		TimeCol: timeCol,
		Measure: "sum(total)",
		Agg:     "sum",
		Arg:     "total",
		Where:   []string{"invoice_status = '" + string(types.InvoiceStatusFinalized) + "'"},
	}
}

func netBillingSQL(m types.Metric, invoiceTime func(prefix string) string) *billingSQL {
	return &billingSQL{
		Build: func(in buildInput) (*reportQuery, error) {
			return buildNetBilling(m, invoiceTime, in)
		},
	}
}

var (
	paidIn = "payment_status IN ('" + string(types.PaymentStatusSucceeded) + "','" + string(types.PaymentStatusOverpaid) + "')"
	// Open invoices with a balance.
	receivableWhere = []string{
		"invoice_status = '" + string(types.InvoiceStatusFinalized) + "'",
		"amount_remaining > 0",
	}
	creditTopupReasons = []types.TransactionReason{
		types.TransactionReasonPurchasedCreditDirect,
		types.TransactionReasonPurchasedCreditInvoiced,
		types.TransactionReasonPurchasedCreditBonus,
		types.TransactionReasonFreeCredit,
		types.TransactionReasonSubscriptionCredit,
	}
)

func ratioIf(col, cond string) string {
	return "if(uniqExactIf(customer_id, " + cond + ") = 0, NULL, sumIf(" + col + ", " + cond + ") / uniqExactIf(customer_id, " + cond + "))"
}

func quotedList[T ~string](vs []T) string {
	q := make([]string, len(vs))
	for i, v := range vs {
		q[i] = "'" + string(v) + "'"
	}
	return strings.Join(q, ",")
}

var billingSQLs = map[types.Metric]*billingSQL{
	types.MetricTotalReceivables: {
		Table:   "invoices",
		TimeCol: "finalized_at",
		Measure: "sum(amount_remaining)",
		Agg:     "sum",
		Arg:     "amount_remaining",
		Where:   receivableWhere,
	},
	types.MetricReceivablesAging: {
		Table:    "invoices",
		TimeCol:  "finalized_at",
		Measure:  "sum(amount_remaining)",
		Agg:      "sum",
		Arg:      "amount_remaining",
		Where:    receivableWhere,
		DimExprs: map[string]string{"aging_bucket": agingBucketExpr},
	},
	types.MetricDaysToPay: {
		Table:   "invoices",
		TimeCol: "paid_at",
		Measure: "avg(dateDiff('day', issue_date, paid_at, 'UTC'))",
		Agg:     "avg",
		Arg:     "dateDiff('day', issue_date, paid_at, 'UTC')",
		Where: []string{
			paidIn,
			"paid_at > " + chEpoch,
			"issue_date > " + chEpoch,
		},
	},
	types.MetricARPUPaid: {
		Table:   "invoices",
		TimeCol: "paid_at",
		Measure: "sum(amount_paid) / uniqExact(customer_id)",
		CompareMeasure: func(c string) string {
			return ratioIf("amount_paid", c)
		},
		Where: []string{paidIn, "paid_at > " + chEpoch},
	},
	types.MetricARPU: {
		Table:   "invoices",
		TimeCol: "finalized_at",
		Measure: "sum(total) / uniqExact(customer_id)",
		CompareMeasure: func(c string) string {
			return ratioIf("total", c)
		},
		Where: []string{"invoice_status = '" + string(types.InvoiceStatusFinalized) + "'"},
	},
	types.MetricCreditConsumption: {
		Build: buildCreditConsumption,
	},
	types.MetricCreditTopups: {
		Table:   "wallet_transactions",
		TimeCol: "created_at",
		Measure: "sum(amount)",
		Agg:     "sum",
		Arg:     "amount",
		Where: []string{
			"type = '" + string(types.TransactionTypeCredit) + "'",
			"transaction_status = '" + string(types.TransactionStatusCompleted) + "'",
			"transaction_reason IN (" + quotedList(creditTopupReasons) + ")",
		},
	},
	types.MetricInvoiceTotal:       invoiceTotalSQL("finalized_at"),
	types.MetricInvoiceTotalIssued: invoiceTotalSQL("finalized_at"),
	types.MetricNetBilling:         netBillingSQL(types.MetricNetBilling, invoiceFinalizedTime),
	types.MetricNetBillingIssued:   netBillingSQL(types.MetricNetBillingIssued, invoiceFinalizedTime),
	types.MetricActiveCustomers:    {Build: buildActiveCustomers},
	types.MetricEventsVolume:       {Build: buildEventsVolume},
}