Files
ubicloud/model/billing_record.rb
Jeremy Evans 718370f4c9 Convert ResourceMethods to a plugin
Allow plugin to take an etc_type keyword argument for using the
TYPE_ETC ubid type, and remove the separate definitions in every
model that uses the TYPE_ETC ubid type.

This was the cleanest way to DRY things up.  You cannot extend
the models with a module to do this before including ResourceMethods,
because then ResourceMethods::ClassMethods will override it, and
you cannot extend the models with a module to do this after
including ResourceMethods, because the inclusion will not work
correctly due to the eager definition of @ubid_format.

Best reviewed without whitespace differences.
2025-06-04 04:55:45 +09:00

60 lines
2.5 KiB
Ruby

# frozen_string_literal: true
require_relative "../model"
class BillingRecord < Sequel::Model
many_to_one :project
dataset_module do
where(:active, Sequel.function(:upper, :span) => nil)
end
plugin ResourceMethods
def duration(begin_time, end_time)
# Billing logic differs based on the resource type: some are billed by duration, others
# by amount. For 'VmVCpu' billing records, the core counts are stored in the amount
# column and charges are based on duration. For example, 10 minutes of standard-4 vm
# usage would be calculated as `10 (duration) x 2 (amount) x rate_for_standard_core`.
# 'GitHubRunnerMinutes' records, on the other hand, store the used minutes in the
# 'amount' column, and billing is based on that amount.
# For records billed by amount, the duration is always set to 1.
return 1 if billing_rate["billed_by"] == "amount"
# begin_time and end_time refers to begin and end of the billing window. Duration of
# BillingRecord is subjective to the billing window we are querying for. For example
# if span of the BillingRecord is ['2023-06-15', '2023-08-20'] and billing window is
# ['2023-06-01', '2023-07-01'], then we are only interested in duration of billing
# record that was active in June. This is effectively calculating the intersection of
# two time ranges.
# One complication comes from the fact billing window might have nil end point if it
# is still active. We check for that case with span.unbounded_end?.
duration_begin = [span.begin, begin_time].max
duration_end = span.unbounded_end? ? end_time : [span.end, end_time].min
(duration_end - duration_begin) / 60
end
CURRENT_SPAN = Sequel.lit("tstzrange(lower(span), now())").freeze
def finalize
this.update(span: CURRENT_SPAN)
end
def billing_rate
@billing_rate ||= BillingRate.from_id(billing_rate_id)
end
end
# Table: billing_record
# Columns:
# id | uuid | PRIMARY KEY
# project_id | uuid | NOT NULL
# resource_id | uuid | NOT NULL
# resource_name | text | NOT NULL
# span | tstzrange | NOT NULL DEFAULT tstzrange(now(), NULL::timestamp with time zone, '[)'::text)
# amount | numeric | NOT NULL
# billing_rate_id | uuid | NOT NULL
# Indexes:
# billing_record_pkey | PRIMARY KEY btree (id)
# billing_record_project_id_index | btree (project_id)
# billing_record_resource_id_index | btree (resource_id)
# billing_record_span_index | gist (span)