<commit_before>%%% @end
%%% ocs_rating.erl
%%% vim: ts=3
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% @copyright 2016 - 2020 SigScale Global Inc.
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% @doc This library module implements utility functions
%%% 	for handling rating in the {@link //ocs. ocs} application.
%%%
-module(ocs_rating).
-copyright('Copyright (c) 2016 - 2020 SigScale Global Inc.').

-export([rate/12]).
-export([authorize/8]).
-export([session_attributes/1]).

-include("ocs.hrl").
-include("ocs_log.hrl").
-include_lib("radius/include/radius.hrl").

%% support deprecated_time_unit()
-define(MILLISECOND, milli_seconds).
%-define(MILLISECOND, millisecond).

% calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}})
-define(EPOCH, 62167219200).

%% service types for radius
-define(RADIUSLOGIN, 1).
-define(RADIUSFRAMED, 2).
-define(RADIUSVOICE, 12).
%% service types for diameter
-define(DIAMETERDATA, 32251).
-define(DIAMETERVOICE, 32260).
-define(DIAMETERSMS, 32274).

-record(state,
		{buckets = [] :: [#bucket{}],
		product :: #product{},
		chars = [] :: [tuple()],
		service_type :: integer() | binary(),
		charging_key :: integer() | undefined,
		service_network :: string() | undefined,
		roaming_tb_prefix :: string() | undefined,
		session_id :: [tuple()],
		rated = #rated{} :: #rated{} | [#rated{}]}).

-spec rate(Protocol, ServiceType, ChargingKey, ServiceNetwork, SubscriberID,
		Timestamp, Address, Direction, Flag, DebitAmounts, ReserveAmounts,
		SessionAttributes) -> Result
	when
		Protocol :: radius | diameter,
		ServiceType :: integer() | binary(),
		ChargingKey :: integer() | undefined,
		ServiceNetwork :: string() | binary() | undefined,
		SubscriberID :: string() | binary(),
		Timestamp :: calendar:datetime(),
		Address :: string() | binary() | undefined,
		Direction :: answer | originate | undefined,
		Flag :: initial | interim | final,
		DebitAmounts :: [{Type, Amount}],
		ReserveAmounts :: [{Type, Amount}],
		SessionAttributes :: [tuple()],
		Type :: octets | seconds | messages,
		Amount :: integer(),
		Result :: {ok, Service, GrantedAmount}
				| {ok, Service, Rated}
				| {out_of_credit, SessionList}
				| {out_of_credit, SessionList, Rated}
				| {disabled, SessionList}
				| {error, Reason},
		Service :: #service{},
		GrantedAmount :: {Type, Amount},
		Rated :: [#rated{}],
		SessionList :: [{pos_integer(), [tuple()]}],
		Reason :: term().
%% @doc Handle rating and balance management for used and reserved unit amounts.
%%
%% 	Subscriber balance buckets are permanently reduced by the
%% 	amount in `DebitAmounts' and bucket reservations are made of
%% 	the amounts in `ReserveAmounts'. The subscribed product offer
%% 	determines the price used to calculate the amount to be
%% 	permanently debited from available `cents' buckets.
%%
%% 	If no `ReserveAmounts' are provided in `initial' and `interim'
%% 	requests the `Type' and `Amount' are determined by applicable
%% 	product offer price.
%%
%% 	If successful returns `{ok, Service, GrantedAmount}' for `initial'
%% 	and `interim' updates or `{ok, Service, Rated}' for
%% 	`final'.
%%
%% 	If subscriber's balance is insufficient to cover the `DebitAmounts'
%% 	and `ReserveAmounts' returns `{out_of_credit, SessionList}' for interim
%% 	updates and `{out_of_credit, SessionList, Rated}' for final or
%% 	`{disabled, SessionList}' if the subscriber is not enabled. In both
%% 	cases subscriber's balance is debited.  `SessionList' describes the
%% 	known active sessions which should be disconnected.
%%
rate(Protocol, ServiceType, ChargingKey, ServiceNetwork, SubscriberID,
		Timestamp, Address, Direction, Flag, DebitAmounts, ReserveAmounts,
		SessionAttributes) when is_list(SubscriberID) ->
	rate(Protocol, ServiceType, ChargingKey, ServiceNetwork,
		list_to_binary(SubscriberID), Timestamp, Address, Direction,
		Flag, DebitAmounts, ReserveAmounts, SessionAttributes);
rate(Protocol, ServiceType, ChargingKey, ServiceNetwork, SubscriberID,
		Timestamp, Address, Direction, Flag, DebitAmounts, ReserveAmounts,
		SessionAttributes) when is_binary(ServiceNetwork) ->
	rate(Protocol, ServiceType, ChargingKey, binary_to_list(ServiceNetwork),
		SubscriberID, Timestamp, Address, Direction,
		Flag, DebitAmounts, ReserveAmounts, SessionAttributes);
rate(Protocol, ServiceType, ChargingKey, ServiceNetwork, SubscriberID,
		Timestamp, Address, Direction, Flag, DebitAmounts, ReserveAmounts,
		SessionAttributes) when is_binary(Address) ->
	rate(Protocol, ServiceType, ChargingKey, ServiceNetwork,
		SubscriberID, Timestamp, binary_to_list(Address), Direction,
		Flag, DebitAmounts, ReserveAmounts, SessionAttributes);
rate(Protocol, ServiceType, ChargingKey, ServiceNetwork, SubscriberID,
		{{_, _, _}, {_, _, _}} = Timestamp, Address, Direction, Flag,
		DebitAmounts, ReserveAmounts, SessionAttributes)
		when ((Protocol == radius) or (Protocol == diameter)),
		(is_integer(ChargingKey) or (ChargingKey == undefined)),
		(is_list(ServiceNetwork) or (ServiceNetwork == undefined)),
		is_binary(SubscriberID),
		(is_list(Address) or (Address == undefined)),
		((Direction == answer) or (Direction == originate)
				or (Direction == undefined)),
		((Flag == initial) or (Flag == interim) or (Flag == final)),
		is_list(DebitAmounts), is_list(ReserveAmounts),
		length(SessionAttributes) > 0 ->
	F = fun() ->
			case mnesia:read(service, SubscriberID, sticky_write) of
				[#service{product = ProdRef, session_attributes = SessionList} = Service] ->
					case mnesia:read(product, ProdRef, read) of
						[#product{characteristics = Chars, product = OfferId,
								balance = BucketRefs} = Product] ->
							case mnesia:read(offer, OfferId, read) of
								[#offer{} = Offer] ->
									Buckets = lists:flatten([mnesia:read(bucket, Id, sticky_write)
											|| Id <- BucketRefs]),
									F2 = fun(#bucket{units = cents, remain_amount = RM}) when RM < 0 ->
												false;
											(_) ->
												true
									end,
									case lists:all(F2, Buckets) of
										false when Flag == initial ->
											{out_of_credit, SessionList};
										_ ->
											State = #state{buckets = Buckets,
													product  = Product,
													chars = Chars,
													service_type = ServiceType,
													charging_key = ChargingKey,
													service_network = ServiceNetwork,
													session_id = get_session_id(SessionAttributes)},
											rate1(Protocol, Service, Buckets,
													Timestamp, Address, Direction, Offer,
													Flag, DebitAmounts, ReserveAmounts, State)
									end;
								[] ->
									throw(offer_not_found)
							end;
						[] ->
							throw(product_not_found)
					end;

				[] ->
					throw(service_not_found)
			end
	end,
	case mnesia:transaction(F) of
		{atomic, {ok, Sub, Rated}} when is_list(Rated) ->
			{ok, Sub, Rated};
		{atomic, {out_of_credit, SL, Rated}} ->
			{out_of_credit, SL, Rated};
		{atomic, {grant, Sub, {_Units, Amount} = Granted}} when is_integer(Amount) ->
			{ok, Sub, Granted};
		{atomic, {out_of_credit, SL}} ->
			{out_of_credit, SL};
		{atomic, {disabled, SL}} ->
			{disabled, SL};
		{aborted, {throw, Reason}} ->
			{error, Reason};
		{aborted, Reason} ->
			{error, Reason}
	end.
%% @hidden<commit_msg>Revert "only short circuit rating for negative balance on initial"<commit_after>%%% @end
%%% ocs_rating.erl
%%% vim: ts=3
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% @copyright 2016 - 2020 SigScale Global Inc.
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% @doc This library module implements utility functions
%%% 	for handling rating in the {@link //ocs. ocs} application.
%%%
-module(ocs_rating).
-copyright('Copyright (c) 2016 - 2020 SigScale Global Inc.').

-export([rate/12]).
-export([authorize/8]).
-export([session_attributes/1]).

-include("ocs.hrl").
-include("ocs_log.hrl").
-include_lib("radius/include/radius.hrl").

%% support deprecated_time_unit()
-define(MILLISECOND, milli_seconds).
%-define(MILLISECOND, millisecond).

% calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}})
-define(EPOCH, 62167219200).

%% service types for radius
-define(RADIUSLOGIN, 1).
-define(RADIUSFRAMED, 2).
-define(RADIUSVOICE, 12).
%% service types for diameter
-define(DIAMETERDATA, 32251).
-define(DIAMETERVOICE, 32260).
-define(DIAMETERSMS, 32274).

-record(state,
		{buckets = [] :: [#bucket{}],
		product :: #product{},
		chars = [] :: [tuple()],
		service_type :: integer() | binary(),
		charging_key :: integer() | undefined,
		service_network :: string() | undefined,
		roaming_tb_prefix :: string() | undefined,
		session_id :: [tuple()],
		rated = #rated{} :: #rated{} | [#rated{}]}).

-spec rate(Protocol, ServiceType, ChargingKey, ServiceNetwork, SubscriberID,
		Timestamp, Address, Direction, Flag, DebitAmounts, ReserveAmounts,
		SessionAttributes) -> Result
	when
		Protocol :: radius | diameter,
		ServiceType :: integer() | binary(),
		ChargingKey :: integer() | undefined,
		ServiceNetwork :: string() | binary() | undefined,
		SubscriberID :: string() | binary(),
		Timestamp :: calendar:datetime(),
		Address :: string() | binary() | undefined,
		Direction :: answer | originate | undefined,
		Flag :: initial | interim | final,
		DebitAmounts :: [{Type, Amount}],
		ReserveAmounts :: [{Type, Amount}],
		SessionAttributes :: [tuple()],
		Type :: octets | seconds | messages,
		Amount :: integer(),
		Result :: {ok, Service, GrantedAmount}
				| {ok, Service, Rated}
				| {out_of_credit, SessionList}
				| {out_of_credit, SessionList, Rated}
				| {disabled, SessionList}
				| {error, Reason},
		Service :: #service{},
		GrantedAmount :: {Type, Amount},
		Rated :: [#rated{}],
		SessionList :: [{pos_integer(), [tuple()]}],
		Reason :: term().
%% @doc Handle rating and balance management for used and reserved unit amounts.
%%
%% 	Subscriber balance buckets are permanently reduced by the
%% 	amount in `DebitAmounts' and bucket reservations are made of
%% 	the amounts in `ReserveAmounts'. The subscribed product offer
%% 	determines the price used to calculate the amount to be
%% 	permanently debited from available `cents' buckets.
%%
%% 	If no `ReserveAmounts' are provided in `initial' and `interim'
%% 	requests the `Type' and `Amount' are determined by applicable
%% 	product offer price.
%%
%% 	If successful returns `{ok, Service, GrantedAmount}' for `initial'
%% 	and `interim' updates or `{ok, Service, Rated}' for
%% 	`final'.
%%
%% 	If subscriber's balance is insufficient to cover the `DebitAmounts'
%% 	and `ReserveAmounts' returns `{out_of_credit, SessionList}' for interim
%% 	updates and `{out_of_credit, SessionList, Rated}' for final or
%% 	`{disabled, SessionList}' if the subscriber is not enabled. In both
%% 	cases subscriber's balance is debited.  `SessionList' describes the
%% 	known active sessions which should be disconnected.
%%
rate(Protocol, ServiceType, ChargingKey, ServiceNetwork, SubscriberID,
		Timestamp, Address, Direction, Flag, DebitAmounts, ReserveAmounts,
		SessionAttributes) when is_list(SubscriberID) ->
	rate(Protocol, ServiceType, ChargingKey, ServiceNetwork,
		list_to_binary(SubscriberID), Timestamp, Address, Direction,
		Flag, DebitAmounts, ReserveAmounts, SessionAttributes);
rate(Protocol, ServiceType, ChargingKey, ServiceNetwork, SubscriberID,
		Timestamp, Address, Direction, Flag, DebitAmounts, ReserveAmounts,
		SessionAttributes) when is_binary(ServiceNetwork) ->
	rate(Protocol, ServiceType, ChargingKey, binary_to_list(ServiceNetwork),
		SubscriberID, Timestamp, Address, Direction,
		Flag, DebitAmounts, ReserveAmounts, SessionAttributes);
rate(Protocol, ServiceType, ChargingKey, ServiceNetwork, SubscriberID,
		Timestamp, Address, Direction, Flag, DebitAmounts, ReserveAmounts,
		SessionAttributes) when is_binary(Address) ->
	rate(Protocol, ServiceType, ChargingKey, ServiceNetwork,
		SubscriberID, Timestamp, binary_to_list(Address), Direction,
		Flag, DebitAmounts, ReserveAmounts, SessionAttributes);
rate(Protocol, ServiceType, ChargingKey, ServiceNetwork, SubscriberID,
		{{_, _, _}, {_, _, _}} = Timestamp, Address, Direction, Flag,
		DebitAmounts, ReserveAmounts, SessionAttributes)
		when ((Protocol == radius) or (Protocol == diameter)),
		(is_integer(ChargingKey) or (ChargingKey == undefined)),
		(is_list(ServiceNetwork) or (ServiceNetwork == undefined)),
		is_binary(SubscriberID),
		(is_list(Address) or (Address == undefined)),
		((Direction == answer) or (Direction == originate)
				or (Direction == undefined)),
		((Flag == initial) or (Flag == interim) or (Flag == final)),
		is_list(DebitAmounts), is_list(ReserveAmounts),
		length(SessionAttributes) > 0 ->
	F = fun() ->
			case mnesia:read(service, SubscriberID, sticky_write) of
				[#service{product = ProdRef, session_attributes = SessionList} = Service] ->
					case mnesia:read(product, ProdRef, read) of
						[#product{characteristics = Chars, product = OfferId,
								balance = BucketRefs} = Product] ->
							case mnesia:read(offer, OfferId, read) of
								[#offer{} = Offer] ->
									Buckets = lists:flatten([mnesia:read(bucket, Id, sticky_write)
											|| Id <- BucketRefs]),
									F2 = fun(#bucket{units = cents, remain_amount = RM}) when RM < 0 ->
												false;
											(_) ->
												true
									end,
									case lists:all(F2, Buckets) of
										true ->
											State = #state{buckets = Buckets,
													product  = Product,
													chars = Chars,
													service_type = ServiceType,
													charging_key = ChargingKey,
													service_network = ServiceNetwork,
													session_id = get_session_id(SessionAttributes)},
											rate1(Protocol, Service, Buckets,
													Timestamp, Address, Direction, Offer,
													Flag, DebitAmounts, ReserveAmounts, State);
										false ->
											{out_of_credit, SessionList}
									end;
								[] ->
									throw(offer_not_found)
							end;
						[] ->
							throw(product_not_found)
					end;

				[] ->
					throw(service_not_found)
			end
	end,
	case mnesia:transaction(F) of
		{atomic, {ok, Sub, Rated}} when is_list(Rated) ->
			{ok, Sub, Rated};
		{atomic, {out_of_credit, SL, Rated}} ->
			{out_of_credit, SL, Rated};
		{atomic, {grant, Sub, {_Units, Amount} = Granted}} when is_integer(Amount) ->
			{ok, Sub, Granted};
		{atomic, {out_of_credit, SL}} ->
			{out_of_credit, SL};
		{atomic, {disabled, SL}} ->
			{disabled, SL};
		{aborted, {throw, Reason}} ->
			{error, Reason};
		{aborted, Reason} ->
			{error, Reason}
	end.
%% @hidden
{'fraction_non_alphanumeric': 0.15356304334031348, 'fraction_numerical': 0.00875690081857986, 'mean_word_length': 4.23936170212766, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 51, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '465708', 'n_tokens_mistral': 5798, 'n_tokens_neox': 4781, 'n_words': 1350}