@@ -0,0 +1,103 @@
%%%-------------------------------------------------------------------
%%% @copyright
%%% @doc
%%%
%%% @end
%%% @contributors
%%%-------------------------------------------------------------------
-module(kz_srs_filter_prefix).

-export([handle_req/5]).

-include("stepswitch_resource_selectors.hrl").

-define(MOD_NAME, <<"filter_prefix">>).
-define(ALLOWED_FILTER_MODES, [<<"empty_ok">>
,<<"empty_fail">>
]).
-define(DEFAULT_FILTER_MODE, <<"empty_fail">>).

-spec handle_req(stepswitch_resources:resources(), ne_binary(), kapi_offnet_resource:req(), ne_binary(), kz_json:object()) ->
stepswitch_resources:resources().
handle_req(Resources, Number, OffnetJObj, DB, Params) ->
SourceA = kz_srs_util:get_source(kz_json:get_value(<<"value_a">>, Params)),
ValueA = kz_srs_util:get_value(SourceA, Resources, Number, OffnetJObj, DB, <<>>),
'ok' = kz_srs_util:check_value(fun is_binary/1, ValueA),
SourceB = maybe_db_type(kz_srs_util:get_source(kz_json:get_value(<<"value_b">>, Params)), Resources, ValueA),
ValueB = kz_srs_util:get_value(SourceB, Resources, Number, OffnetJObj, DB, []),
'ok' = kz_srs_util:check_value(fun is_list/1, ValueB),
Action = kz_srs_util:select_filter_action(Params),
PrefixMode = kz_srs_util:select_filter_mode(Params, ?ALLOWED_FILTER_MODES, ?DEFAULT_FILTER_MODE),
case SourceB of
{'database', _, _} -> filter_by_db_prefix(Resources, ValueB, Action, PrefixMode);
_ -> filter_by_prefix(Resources, ValueA, ValueB, Action, PrefixMode)
end.

filter_by_db_prefix(Resources, Result, Action, PrefixMode) ->
lists:filter(fun(R) ->
Id = stepswitch_resources:get_resrc_id(R),
Value = props:get_value(Id, Result),
match_db_prefixes(Id, Value, Action, PrefixMode)
end
,Resources
).

filter_by_prefix(Resources, ValueA, ValueB, Action, PrefixMode) ->
SetA = sets:from_list(build_prefixes(ValueA)),
lists:filter(fun(R) ->
Id = stepswitch_resources:get_resrc_id(R),
SetB = sets:from_list(props:get_value(Id, ValueB, [])),
match_prefixes(Id, sets:intersection(SetA, SetB), Action, PrefixMode)
end
,Resources
).

maybe_db_type({'database', SelectorName}, Resources, PrefixSrc) ->
Prefixes = build_prefixes(PrefixSrc),
Keys = [[stepswitch_resources:get_resrc_id(R), SelectorName, P] || R <- Resources, P <- Prefixes],
Options = [{'keys', Keys}],
View = <<"selectors/resource_name_data_listing">>,
{'database', View, Options};
maybe_db_type(Other, _Resources, _PrefixSrc) -> Other.

-spec build_prefixes(ne_binary()) -> ne_binaries().
build_prefixes(<<"+", Rest/binary>>) -> build_prefixes(Rest);
build_prefixes(<<D:1/binary, Rest/binary>>) ->
build_prefixes(Rest, D, [D]).

-spec build_prefixes(ne_binary(), ne_binary(), ne_binaries()) -> ne_binaries().
build_prefixes(<<D:1/binary, Rest/binary>>, Prefix, Acc) ->
build_prefixes(Rest, <<Prefix/binary, D/binary>>, [<<Prefix/binary, D/binary>> | Acc]);
build_prefixes(<<>>, _, Acc) -> Acc.

match_prefixes(Id, Set, 'keep', _PrefixMode) ->
case sets:size(Set) of
0 ->
lager:debug("resource ~s dont match prefix, droping", [Id]),
'false';
_ ->
lager:debug("resource ~s matched prefix, keeping", [Id]),
'true'
end;
match_prefixes(Id, Set, 'drop', _PrefixMode) ->
case sets:size(Set) of
0 ->
lager:debug("resource ~s dont match prefix, keeping", [Id]),
'true';
_ ->
lager:debug("resource ~s matched prefix, droping", [Id]),
'false'
end.

match_db_prefixes(Id, 'undefined', 'keep', _PrefixMode) ->
lager:debug("resource ~s dont match prefix, droping", [Id]),
'false';
match_db_prefixes(Id, [_|_], 'keep', _PrefixMode) ->
lager:debug("resource ~s matched prefix, keeping", [Id]),
'true';
match_db_prefixes(Id, 'undefined', 'drop', _PrefixMode) ->
lager:debug("resource ~s dont match prefix, keeping", [Id]),
'true';
match_db_prefixes(Id, [_|_], 'drop', _PrefixMode) ->
lager:debug("resource ~s matched prefix, droping", [Id]),
'false'.
@@ -0,0 +1,99 @@
%%%-------------------------------------------------------------------
%%% @copyright
%%% @doc
%%%
%%% @end
%%% @contributors
%%%-------------------------------------------------------------------
-module(kz_srs_filter_regex).

-export([handle_req/5]).

-include("stepswitch_resource_selectors.hrl").

-define(MOD_NAME, <<"filter_regex">>).
-define(ALLOWED_FILTER_MODES, [<<"empty_ok">>
,<<"empty_fail">>
]).
-define(DEFAULT_FILTER_MODE, <<"empty_fail">>).

-spec handle_req(stepswitch_resources:resources()
,ne_binary()
,kapi_offnet_resource:req()
,ne_binary()
,kz_json:object()
) -> stepswitch_resources:resources().
handle_req(Resources, Number, OffnetJObj, DB, Params) ->
SourceA = kz_srs_util:get_source(kz_json:get_value(<<"value_a">>, Params)),
ValueA = kz_srs_util:get_value(SourceA, Resources, Number, OffnetJObj, DB, <<>>),
'ok' = kz_srs_util:check_value(fun is_binary/1, ValueA),
SourceB = kz_srs_util:get_source(kz_json:get_value(<<"value_b">>, Params)),
ValueB = kz_srs_util:get_value(SourceB, Resources, Number, OffnetJObj, DB, []),
'ok' = kz_srs_util:check_value(fun is_list/1, ValueB),
Action = kz_srs_util:select_filter_action(Params),
EmptyMode = kz_srs_util:select_filter_mode(Params, ?ALLOWED_FILTER_MODES, ?DEFAULT_FILTER_MODE),
filter_by_regex(Resources, ValueA, ValueB, Action, EmptyMode).

-spec filter_by_regex(stepswitch_resources:resources()
,ne_binary()
,kz_proplists()
,atom()
,ne_binary()
) -> stepswitch_resources:resources().
filter_by_regex(Resources, ValueA, Regexes, Action, EmptyMode) ->
lager:debug("filter resources by ~s with regex rules, and ~s matched", [ValueA, Action]),
lists:filtermap(fun(R) ->
Id = stepswitch_resources:get_resrc_id(R),
Rules = props:get_value(Id, Regexes, []),
evaluate_rules(Id, Rules, ValueA, Action, EmptyMode)
end
,Resources
).

-spec evaluate_rules(ne_binary(), re:mp(), ne_binary(), atom(), ne_binary()) -> boolean().
evaluate_rules(Id, [], _Data, 'keep', <<"empty_fail">>) ->
lager:debug("resource ~s has empty rules, dropping", [Id]),
'false';
evaluate_rules(Id, [], _Data, 'keep', <<"empty_ok">>) ->
lager:debug("resource ~s has empty rules, keeping", [Id]),
'true';
evaluate_rules(Id, Rules, Data, 'keep', _EmptyMode) ->
case do_evaluate_rules(Rules, Data) of
{'error', 'no_match'} ->
lager:debug("resource ~s does not match request, dropping", [Id]),
'false';
{'ok', Match} ->
lager:debug("resource ~s does match (~p) request, keeping", [Id, Match]),
'true'
end;
evaluate_rules(Id, [], _Data, 'drop', <<"empty_fail">>) ->
lager:debug("resource ~s has empty rules, keeping", [Id]),
'false';
evaluate_rules(Id, [], _Data, 'drop', <<"empty_ok">>) ->
lager:debug("resource ~s has empty rules, dropping", [Id]),
'true';
evaluate_rules(Id, Rules, Data, 'drop', _EmptyMode) ->
case do_evaluate_rules(Rules, Data) of
{'error', 'no_match'} ->
lager:debug("resource ~s does not match request, keeping", [Id]),
'true';
{'ok', Match} ->
lager:debug("resource ~s does match (~p) request, dropping", [Id, Match]),
'false'
end.

-spec do_evaluate_rules(re:mp(), ne_binary()) ->
{'ok', ne_binary()} |
{'error', 'no_match'}.
do_evaluate_rules([], _) -> {'error', 'no_match'};
do_evaluate_rules([Rule|Rules], Data) ->
case re:run(Data, Rule) of
{'match', [{Start,End}]} ->
{'ok', binary:part(Data, Start, End)};
{'match', CaptureGroups} ->
%% find the largest matching group if present by sorting the position of the
%% matching groups by list, reverse so head is largest, then take the head of the list
{Start, End} = hd(lists:reverse(lists:keysort(2, tl(CaptureGroups)))),
{'ok', binary:part(Data, Start, End)};
_ -> do_evaluate_rules(Rules, Data)
end.
@@ -0,0 +1,48 @@
%%%-------------------------------------------------------------------
%%% @copyright
%%% @doc
%%%
%%% @end
%%% @contributors
%%%-------------------------------------------------------------------
-module(kz_srs_get_resources).

-export([handle_req/5]).

-include("stepswitch.hrl").

-spec handle_req(stepswitch_resources:resources(), ne_binary(), kapi_offnet_resource:req(), ne_binary(), kz_proplist()) ->
stepswitch_resources:resources().
handle_req(Resources, _Number, OffnetJObj, _DB, _Params) ->
NewResources = case kapi_offnet_resource:hunt_account_id(OffnetJObj) of
'undefined' -> get_resources('undefined');
HuntAccount ->
AccountId = kapi_offnet_resource:account_id(OffnetJObj),
maybe_get_local_resources(HuntAccount, AccountId)
end,
lists:append(Resources, NewResources).

-spec maybe_get_local_resources(ne_binary(), ne_binary()) -> kz_json:objects().
maybe_get_local_resources(HuntAccount, AccountId) ->
case kz_util:is_in_account_hierarchy(HuntAccount, AccountId, 'true') of
'false' ->
lager:info("account ~s attempted to use local resources of ~s, but it is not allowed"
,[AccountId, HuntAccount]
),
[];
'true' ->
lager:info("account ~s is using the local resources of ~s", [AccountId, HuntAccount]),
get_resources(HuntAccount)
end.

-spec get_resources(api_binary()) -> stepswitch_resources:resources().
get_resources('undefined') ->
case kz_cache:fetch_local(?CACHE_NAME, 'global_resources') of
{'ok', Resources} -> Resources;
{'error', 'not_found'} -> stepswitch_resources:fetch_global_resources()
end;
get_resources(AccountId) ->
case kz_cache:fetch_local(?CACHE_NAME, {'local_resources', AccountId}) of
{'ok', Resources} -> Resources;
{'error', 'not_found'} -> stepswitch_resources:fetch_local_resources(AccountId)
end.
@@ -0,0 +1,17 @@
%%%-------------------------------------------------------------------
%%% @copyright
%%% @doc
%%%
%%% @end
%%% @contributors
%%%-------------------------------------------------------------------
-module(kz_srs_null).

-export([handle_req/5]).

-include("stepswitch.hrl").

-spec handle_req(stepswitch_resources:resources(), ne_binary(), kapi_offnet_resource:req(), ne_binary(), kz_json:object()) ->
stepswitch_resources:resources().
handle_req(_Resources, _Number, _OffnetJObj, _Db, _Params) ->
[].
@@ -0,0 +1,60 @@

%%%-------------------------------------------------------------------
%%% @copyright
%%% @doc
%%%
%%% @end
%%% @contributors
%%%-------------------------------------------------------------------
-module(kz_srs_order).

-export([handle_req/5]).

-include("stepswitch.hrl").

-define(MOD_NAME, <<"order_by">>).
-define(DEFAULT_SORT_ORDER, <<"ascend">>).
-define(DEFAULT_DESC_WEIGHT, 0).
-define(DEFAULT_ASC_WEIGHT, 9999).

-spec handle_req(stepswitch_resources:resources(), ne_binary(), kapi_offnet_resource:req(), ne_binary(), kz_json:object()) ->
stepswitch_resources:resources().
handle_req([], _Number, _OffnetJObj, _DB, _Params) ->
lager:warning("empty resource list", []),
[];
handle_req([Resource], _Number, _OffnetJObj, _DB, _Params) ->
Id = stepswitch_resources:get_resrc_id(Resource),
lager:debug("resource list contains only one resource (~p), skip sorting", [Id]),
[Resource];
handle_req(Resources, Number, OffnetJObj, DB, Params) ->
Source = kz_srs_util:get_source(kz_json:get_value(<<"value">>, Params)),
'ok' = check_source(Source),
Values = kz_srs_util:get_value(Source, Resources, Number, OffnetJObj, DB, 1),
SortOrder = kz_json:get_ne_value(<<"direction">>, Params, ?DEFAULT_SORT_ORDER),
order_by(Resources, Values, SortOrder).

-spec order_by(stepswitch_resources:resources(), kz_json:object(), ne_binary()) ->
stepswitch_resources:resources().
order_by(Resources, Values, SortOrder) ->
lists:sort(fun(R1, R2) ->
Id1 = stepswitch_resources:get_resrc_id(R1),
Id2 = stepswitch_resources:get_resrc_id(R2),
Weight1 = kz_json:get_value(Id1, Values, default_weight(SortOrder)),
Weight2 = kz_json:get_value(Id2, Values, default_weight(SortOrder)),
sort(Weight1, Weight2, SortOrder)
end
,Resources
).

-spec default_weight(ne_binary()) -> integer().
default_weight(<<"ascend">>) -> ?DEFAULT_ASC_WEIGHT;
default_weight(<<"descend">>) -> ?DEFAULT_DESC_WEIGHT.

-spec check_source(tuple() | any()) -> 'ok'.
check_source({'database', _}) -> 'ok';
check_source({'resource', _}) -> 'ok';
check_source(Source) -> throw({invalid_source, Source}).

-spec sort(integer(), integer(), ne_binary()) -> boolean().
sort(Lesser, Greater, <<"ascend">>) -> Lesser =< Greater;
sort(Greater, Lesser, <<"descend">>) -> Lesser =< Greater.
@@ -21,6 +21,8 @@
-define(CACHE_NAME, 'stepswitch_cache').
-define(STEPSWITCH_CNAM_POOL, 'stepswitch_cnam_pool').

-define(DEFAULT_ROUTE_BY, <<"stepswitch_resources">>).

-define(CCV(Key), [<<"Custom-Channel-Vars">>, Key]).

-type direction() :: 'inbound' | 'outbound' | 'both'.
@@ -144,7 +144,8 @@ maybe_force_outbound_sms(Props, OffnetReq) ->
%%--------------------------------------------------------------------
-spec maybe_bridge(ne_binary(), kapi_offnet_resource:req()) -> any().
maybe_bridge(Number, OffnetReq) ->
case stepswitch_resources:endpoints(Number, OffnetReq) of
RouteBy = stepswitch_util:route_by(),
case RouteBy:endpoints(Number, OffnetReq) of
[] -> maybe_correct_shortdial(Number, OffnetReq);
Endpoints -> stepswitch_request_sup:bridge(Endpoints, OffnetReq)
end.
@@ -171,7 +172,8 @@ maybe_correct_shortdial(Number, OffnetReq) ->
%%--------------------------------------------------------------------
-spec maybe_sms(ne_binary(), kapi_offnet_resource:req()) -> any().
maybe_sms(Number, OffnetReq) ->
case stepswitch_resources:endpoints(Number, OffnetReq) of
RouteBy = stepswitch_util:route_by(),
case RouteBy:endpoints(Number, OffnetReq) of
[] -> publish_no_resources(OffnetReq);
Endpoints -> stepswitch_request_sup:sms(Endpoints, OffnetReq)
end.
@@ -205,7 +207,8 @@ local_sms(Props, OffnetReq) ->
%%--------------------------------------------------------------------
-spec maybe_originate(ne_binary(), kapi_offnet_resource:req()) -> any().
maybe_originate(Number, OffnetReq) ->
case stepswitch_resources:endpoints(Number, OffnetReq) of
RouteBy = stepswitch_util:route_by(),
case RouteBy:endpoints(Number, OffnetReq) of
[] -> publish_no_resources(OffnetReq);
Endpoints -> stepswitch_request_sup:originate(Endpoints, OffnetReq)
end.
@@ -0,0 +1,124 @@
%%%-------------------------------------------------------------------
%%% @copyright
%%% @doc
%%%
%%% @end
%%% @contributors
%%%-------------------------------------------------------------------
-module(stepswitch_resource_selectors).

-export([endpoints/2]).

-include("stepswitch_resource_selectors.hrl").

-define(MOD_NAME, <<"resource_selectors">>).
-define(SRS_CONFIG_CAT, <<?SS_CONFIG_CAT/binary, ".", ?MOD_NAME/binary>>).
-define(MOD_PREFIX, "kz_srs_").
-define(SRS_RULES_DOC, <<"resource_selector_rules">>).
-define(DEFAULT_SRS_RULES, [{[{<<"get_resources">>
,{[]}
}]}
,{[{<<"filter_list">>
,{[{<<"value_a">>,<<"request:Flags">>}
,{<<"value_b">>,<<"resource:flags">>}
,{<<"action">>,<<"keep">>}
]}
}]}
,{[{<<"filter_regex">>
,{[{<<"value_a">>,<<"number">>}
,{<<"value_b">>,<<"resource:rules">>}
,{<<"action">>,<<"keep">>}
,{<<"mode">>,<<"empty_fail">>}
]}
}]}
,{[{<<"filter_regex">>
,{[{<<"value_a">>,<<"cid_number">>}
,{<<"value_b">>,<<"resource:cid_rules">>}
,{<<"action">>,<<"keep">>}
,{<<"mode">>,<<"empty_ok">>}
]}
}]}
,{[{<<"order">>
,{[{<<"value">>, <<"resource:weight_cost">>}
,{<<"direction">>, <<"ascend">>}
]}
}]}
]
).

-spec endpoints(ne_binary(), kapi_offnet_resource:req()) -> kz_json:objects().
endpoints(Number, OffnetJObj) ->
HuntAccountId = maybe_get_hunt_account(OffnetJObj),
SelectorsDb = kz_util:format_resource_selectors_db(HuntAccountId),
case get_selector_rules(HuntAccountId) of
{'ok', SelectorRules} ->
Resources = foldl_modules(Number, OffnetJObj, SelectorsDb, SelectorRules),
stepswitch_util:resources_to_endpoints(Resources, Number, OffnetJObj);
{'error', _E} -> []
end.

-spec foldl_modules(ne_binary(), kapi_offnet_resource:req(), ne_binary(), kz_json:objects()) -> stepswitch_resources:resources().
foldl_modules(Number, OffnetJObj, SelectorsDb, SelectorRules) ->
lists:foldl(fun(JObj, Resources) ->
[Module|_] = kz_json:get_keys(JObj),
ModuleName = real_module_name(Module),
ModuleParams = kz_json:get_value(Module, JObj),
try Res = ModuleName:handle_req(Resources
,Number
,OffnetJObj
,SelectorsDb
,ModuleParams
),
lager:info("module ~p return resources: ~p"
,[Module, [ stepswitch_resources:get_resrc_id(R) ||
R <- Res ]
]),
Res
catch
'error':R ->
ST = erlang:get_stacktrace(),
lager:error("failed to run module: ~p, error: ~p",[Module, R]),
kz_util:log_stacktrace(ST),
[];
'throw':T ->
lager:error("module ~p (~p) throw exception: ~p",[Module, ModuleName, T]),
[]
end
end
,[]
,SelectorRules
).

-spec maybe_get_hunt_account(kapi_offnet_resource:req()) -> api_binary().
maybe_get_hunt_account(OffnetJObj) ->
HuntAccountId = kapi_offnet_resource:hunt_account_id(OffnetJObj),
AccountId = kapi_offnet_resource:account_id(OffnetJObj),
{'ok', MasterAccountId} = kapps_util:get_master_account_id(),
case kz_util:is_not_empty(HuntAccountId)
andalso kz_util:is_in_account_hierarchy(HuntAccountId, AccountId, 'true')
of
'true' -> HuntAccountId;
'false' -> MasterAccountId
end.

-spec get_selector_rules(api_binary()) -> {'ok', kz_json:object()} | {'error', any()}.
get_selector_rules(HuntAccountId) ->
Db = kz_util:format_account_db(HuntAccountId),
case kz_datamgr:open_doc(Db, ?SRS_RULES_DOC) of
{'ok', Doc} ->
Rules = kz_json:get_value(<<"rules">>, Doc, ?DEFAULT_SRS_RULES),
{'ok', Rules};
{'error', 'not_found'} ->
Doc = {[{<<"_id">>, ?SRS_RULES_DOC}
,{<<"rules">>, ?DEFAULT_SRS_RULES}
]},
_ = kz_datamgr:save_doc(Db, Doc),
{'ok', ?DEFAULT_SRS_RULES};
{'error', E} ->
lager:error("failed to get resource selector rules from ~s: ~p", [Db, E]),
{'error', E}
end.

-spec real_module_name(binary()) -> atom().
real_module_name(Module) when is_binary(Module) ->
kz_util:to_atom(<<?MOD_PREFIX, Module/binary>>, 'true').
@@ -0,0 +1,11 @@
-ifndef(STEPSWITCH_RESOURCE_SELECTORS_HRL).
-include("stepswitch.hrl").

-define(ALLOWED_RESOURCE_FIELDS,[{<<"weight_cost">>, 'get_resrc_weight'}
,{<<"rules">>, 'get_resrc_rules'}
,{<<"cid_rules">>, 'get_resrc_cid_rules'}
,{<<"flags">>, 'get_resrc_flags'}
]).

-define(STEPSWITCH_RESOURCE_SELECTORS_HRL, 'true').
-endif.
@@ -14,6 +14,59 @@
-export([fetch_global_resources/0
,fetch_local_resources/1
]).
-export([maybe_add_proxies/3]).
-export([gateways_to_endpoints/4]).
-export([check_diversion_fields/1]).

-export([get_resrc_id/1
,get_resrc_rev/1
,get_resrc_name/1
,get_resrc_weight/1
,get_resrc_grace_period/1
,get_resrc_flags/1
,get_resrc_rules/1
,get_resrc_raw_rules/1
,get_resrc_cid_rules/1
,get_resrc_cid_raw_rules/1
,get_resrc_gateways/1
,get_resrc_is_emergency/1
,get_resrc_require_flags/1
,get_resrc_global/1
,get_resrc_format_from_uri/1
,get_resrc_from_uri_realm/1
,get_resrc_from_account_realm/1
,get_resrc_fax_option/1
,get_resrc_codecs/1
,get_resrc_bypass_media/1
,get_resrc_formatters/1
,get_resrc_proxies/1
,get_resrc_selector_marks/1
]).

-export([set_resrc_id/2
,set_resrc_rev/2
,set_resrc_name/2
,set_resrc_weight/2
,set_resrc_grace_period/2
,set_resrc_flags/2
,set_resrc_rules/2
,set_resrc_raw_rules/2
,set_resrc_cid_rules/2
,set_resrc_cid_raw_rules/2
,set_resrc_gateways/2
,set_resrc_is_emergency/2
,set_resrc_require_flags/2
,set_resrc_global/2
,set_resrc_format_from_uri/2
,set_resrc_from_uri_realm/2
,set_resrc_from_account_realm/2
,set_resrc_fax_option/2
,set_resrc_codecs/2
,set_resrc_bypass_media/2
,set_resrc_formatters/2
,set_resrc_proxies/2
,set_resrc_selector_marks/2
]).

-include("stepswitch.hrl").

@@ -76,6 +129,7 @@
,bypass_media = 'false' :: boolean()
,formatters :: api_objects()
,proxies = [] :: kz_proplist()
,selector_marks = [] :: [tuple()]
}).

-type resource() :: #resrc{}.
@@ -84,6 +138,12 @@
-type gateway() :: #gateway{}.
-type gateways() :: [#gateway{}].

-export_type([resource/0
,resources/0
,gateway/0
,gateways/0
]).

-compile({'no_auto_import', [get/0, get/1]}).

-spec get_props() -> kz_proplists().
@@ -1053,3 +1113,99 @@ gateway_dialstring(#gateway{route='undefined'
gateway_dialstring(#gateway{route=Route}, _) ->
lager:debug("using pre-configured gateway route ~s", [Route]),
Route.

-spec get_resrc_id(resource()) -> api_binary().
-spec get_resrc_rev(resource()) -> api_binary().
-spec get_resrc_name(resource()) -> api_binary().
-spec get_resrc_weight(resource()) -> non_neg_integer().
-spec get_resrc_grace_period(resource()) -> non_neg_integer().
-spec get_resrc_flags(resource()) -> list().
-spec get_resrc_rules(resource()) -> list().
-spec get_resrc_raw_rules(resource()) -> list().
-spec get_resrc_cid_rules(resource()) -> list().
-spec get_resrc_cid_raw_rules(resource()) -> list().
-spec get_resrc_gateways(resource()) -> list().
-spec get_resrc_is_emergency(resource()) -> boolean().
-spec get_resrc_require_flags(resource()) -> boolean().
-spec get_resrc_global(resource()) -> boolean().
-spec get_resrc_format_from_uri(resource()) -> boolean().
-spec get_resrc_from_uri_realm(resource()) -> api_binary().
-spec get_resrc_from_account_realm(resource()) -> boolean().
-spec get_resrc_fax_option(resource()) -> ne_binary() | boolean().
-spec get_resrc_codecs(resource()) -> ne_binaries().
-spec get_resrc_bypass_media(resource()) -> boolean().
-spec get_resrc_formatters(resource()) -> api_objects().
-spec get_resrc_proxies(resource()) -> kz_proplist().
-spec get_resrc_selector_marks(resource()) -> kz_proplist().

get_resrc_id(#resrc{id=Id}) -> Id.
get_resrc_rev(#resrc{rev=Rev}) -> Rev.
get_resrc_name(#resrc{name=Name}) -> Name.
get_resrc_weight(#resrc{weight=Weight}) -> Weight.
get_resrc_grace_period(#resrc{grace_period=GracePeriod}) -> GracePeriod.
get_resrc_flags(#resrc{flags=Flags}) -> Flags.
get_resrc_rules(#resrc{rules=Rules}) -> Rules.
get_resrc_raw_rules(#resrc{raw_rules=RawRules}) -> RawRules.
get_resrc_cid_rules(#resrc{cid_rules=CIDRules}) -> CIDRules.
get_resrc_cid_raw_rules(#resrc{cid_raw_rules=CIDRawRules}) -> CIDRawRules.
get_resrc_gateways(#resrc{gateways=Gateways}) -> Gateways.
get_resrc_is_emergency(#resrc{is_emergency=IsEmergency}) -> IsEmergency.
get_resrc_require_flags(#resrc{require_flags=RequireFlags}) -> RequireFlags.
get_resrc_global(#resrc{global=Global}) -> Global.
get_resrc_format_from_uri(#resrc{format_from_uri=FormatFromUri}) -> FormatFromUri.
get_resrc_from_uri_realm(#resrc{from_uri_realm=FromUriRealm}) -> FromUriRealm.
get_resrc_from_account_realm(#resrc{from_account_realm=FromAccountRealm}) -> FromAccountRealm.
get_resrc_fax_option(#resrc{fax_option=FaxOption}) -> FaxOption.
get_resrc_codecs(#resrc{codecs=Codecs}) -> Codecs.
get_resrc_bypass_media(#resrc{bypass_media=BypassMedia}) -> BypassMedia.
get_resrc_formatters(#resrc{formatters=Formatters}) -> Formatters.
get_resrc_proxies(#resrc{proxies=Proxies}) -> Proxies.
get_resrc_selector_marks(#resrc{selector_marks=Marks}) -> Marks.

-spec set_resrc_id(resource(), api_binary()) -> resource().
-spec set_resrc_rev(resource(), api_binary()) -> resource().
-spec set_resrc_name(resource(), api_binary()) -> resource().
-spec set_resrc_weight(resource(), non_neg_integer()) -> resource().
-spec set_resrc_grace_period(resource(), non_neg_integer()) -> resource().
-spec set_resrc_flags(resource(), list()) -> resource().
-spec set_resrc_rules(resource(), list()) -> resource().
-spec set_resrc_raw_rules(resource(), list()) -> resource().
-spec set_resrc_cid_rules(resource(), list()) -> resource().
-spec set_resrc_cid_raw_rules(resource(), list()) -> resource().
-spec set_resrc_gateways(resource(), list()) -> resource().
-spec set_resrc_is_emergency(resource(), boolean()) -> resource().
-spec set_resrc_require_flags(resource(), boolean()) -> resource().
-spec set_resrc_global(resource(), boolean()) -> resource().
-spec set_resrc_format_from_uri(resource(), boolean()) -> resource().
-spec set_resrc_from_uri_realm(resource(), api_binary()) -> resource().
-spec set_resrc_from_account_realm(resource(), boolean()) -> resource().
-spec set_resrc_fax_option(resource(), ne_binary() | boolean()) -> resource().
-spec set_resrc_codecs(resource(), ne_binaries()) -> resource().
-spec set_resrc_bypass_media(resource(), boolean()) -> resource().
-spec set_resrc_formatters(resource(), api_objects()) -> resource().
-spec set_resrc_proxies(resource(), kz_proplist()) -> resource().
-spec set_resrc_selector_marks(resource(), kz_proplist()) -> resource().

set_resrc_id(Resource, Id) -> Resource#resrc{id=Id}.
set_resrc_rev(Resource, Rev) -> Resource#resrc{rev=Rev}.
set_resrc_name(Resource, Name) -> Resource#resrc{name=Name}.
set_resrc_weight(Resource, Weight) -> Resource#resrc{weight=Weight}.
set_resrc_grace_period(Resource, GracePeriod) -> Resource#resrc{grace_period=GracePeriod}.
set_resrc_flags(Resource, Flags) -> Resource#resrc{flags=Flags}.
set_resrc_rules(Resource, Rules) -> Resource#resrc{rules=Rules}.
set_resrc_raw_rules(Resource, RawRules) -> Resource#resrc{raw_rules=RawRules}.
set_resrc_cid_rules(Resource, CIDRules) -> Resource#resrc{cid_rules=CIDRules}.
set_resrc_cid_raw_rules(Resource, CIDRawRules) -> Resource#resrc{cid_raw_rules=CIDRawRules}.
set_resrc_gateways(Resource, Gateways) -> Resource#resrc{gateways=Gateways}.
set_resrc_is_emergency(Resource, IsEmergency) -> Resource#resrc{is_emergency=IsEmergency}.
set_resrc_require_flags(Resource, RequireFlags) -> Resource#resrc{require_flags=RequireFlags}.
set_resrc_global(Resource, Global) -> Resource#resrc{global=Global}.
set_resrc_format_from_uri(Resource, FormatFromUri) -> Resource#resrc{format_from_uri=FormatFromUri}.
set_resrc_from_uri_realm(Resource, FromUriRealm) -> Resource#resrc{from_uri_realm=FromUriRealm}.
set_resrc_from_account_realm(Resource, FromAccountRealm) -> Resource#resrc{from_account_realm=FromAccountRealm}.
set_resrc_fax_option(Resource, FaxOption) -> Resource#resrc{fax_option=FaxOption}.
set_resrc_codecs(Resource, Codecs) -> Resource#resrc{codecs=Codecs}.
set_resrc_bypass_media(Resource, BypassMedia) -> Resource#resrc{bypass_media=BypassMedia}.
set_resrc_formatters(Resource, Formatters) -> Resource#resrc{formatters=Formatters}.
set_resrc_proxies(Resource, Proxies) -> Resource#resrc{proxies=Proxies}.
set_resrc_selector_marks(Resource, Marks) -> Resource#resrc{selector_marks=Marks}.
@@ -14,6 +14,8 @@
-export([get_sip_headers/1]).
-export([format_endpoints/4]).
-export([default_realm/1]).
-export([route_by/0]).
-export([resources_to_endpoints/3]).

-include("stepswitch.hrl").
-include_lib("kazoo/src/kz_json.hrl").
@@ -341,3 +343,64 @@ get_endpoint_format_from(OffnetReq, CCVs) ->
'true' -> DefaultRealm;
'false' -> kz_json:get_value(<<"From-URI-Realm">>, CCVs, DefaultRealm)
end.

-spec route_by() -> atom().
route_by() ->
RouteBy = kapps_config:get_ne_binary(?SS_CONFIG_CAT, <<"route_by">>, ?DEFAULT_ROUTE_BY),
case kz_util:try_load_module(RouteBy) of
'false' -> kz_util:to_atom(?DEFAULT_ROUTE_BY);
Module -> Module
end.

-spec resources_to_endpoints(stepswitch_resources:resources(), ne_binary(), kapi_offnet_resource:req()) ->
kz_json:objects().
-spec resources_to_endpoints(stepswitch_resources:resources(), ne_binary(), kapi_offnet_resource:req(), kz_json:objects()) ->
kz_json:objects().
resources_to_endpoints(Resources, Number, OffnetJObj) ->
resources_to_endpoints(Resources, Number, OffnetJObj, []).

resources_to_endpoints([], _Number, _OffnetJObj, Endpoints) ->
lists:reverse(Endpoints);
resources_to_endpoints([Resource|Resources], Number, OffnetJObj, Endpoints) ->
MoreEndpoints = maybe_resource_to_endpoints(Resource, Number, OffnetJObj, Endpoints),
resources_to_endpoints(Resources, Number, OffnetJObj, MoreEndpoints).

-spec maybe_resource_to_endpoints(stepswitch_resources:resource(), ne_binary(), kapi_offnet_resource:req(), kz_json:objects()) ->
kz_json:objects().
maybe_resource_to_endpoints(Resource
,Number
,OffnetJObj
,Endpoints
) ->
Id = stepswitch_resources:get_resrc_id(Resource),
Name = stepswitch_resources:get_resrc_name(Resource),
Gateways = stepswitch_resources:get_resrc_gateways(Resource),
Global = stepswitch_resources:get_resrc_global(Resource),
Weight = stepswitch_resources:get_resrc_weight(Resource),
Proxies = stepswitch_resources:get_resrc_proxies(Resource),
%% TODO: update CID Number from regex_cid_rules result
%% DestinationNumber = maybe_update_number(Resource, Number),
DestinationNumber = Number,
lager:debug("building resource ~s endpoints", [Id]),
CCVUpdates = [{<<"Global-Resource">>, kz_util:to_binary(Global)}
,{<<"Resource-ID">>, Id}
,{<<"E164-Destination">>, DestinationNumber}
,{<<"Original-Number">>, kapi_offnet_resource:to_did(OffnetJObj)}
],
Updates = [{<<"Name">>, Name}
,{<<"Weight">>, Weight}
],
EndpointList = [kz_json:set_values(Updates ,update_ccvs(Endpoint, CCVUpdates))
|| Endpoint <- stepswitch_resources:gateways_to_endpoints(DestinationNumber, Gateways, OffnetJObj, [])
],
stepswitch_resources:maybe_add_proxies(EndpointList, Proxies, Endpoints).

-spec update_ccvs(kz_json:object(), kz_proplist()) -> kz_json:object().
update_ccvs(Endpoint, Updates) ->
CCVs = kz_json:get_value(<<"Custom-Channel-Vars">>, Endpoint, kz_json:new()),
kz_json:set_value(<<"Custom-Channel-Vars">>, kz_json:set_values(Updates, CCVs), Endpoint).

%%-spec maybe_update_number(stepswitch_resources:resource(), ne_binary()) -> stepswitch_resources:resource().
%%maybe_update_number(Resource, Number) ->
%% SelectorsResult = stepswitch_resources:get_resrc_selector_marks(Resource),
%% props:get_value('regex_number_match', SelectorsResult, Number).
@@ -4,6 +4,7 @@
-define(DOC_CREATED, <<"doc_created">>).
-define(DOC_DELETED, <<"doc_deleted">>).

-define(DB_EDITED, <<"db_edited">>).
-define(DB_CREATED, <<"db_created">>).
-define(DB_DELETED, <<"db_deleted">>).

@@ -313,6 +313,40 @@
<<(Year):4/binary, (Month):1/binary, "-", (Account)/binary>> %% FIXME: add missing size
).

-define(MATCH_RESOURCE_SELECTORS_RAW(Account),
<<(Account):32/binary, "-selectors">>
).
-define(MATCH_RESOURCE_SELECTORS_UNENCODED(Account),
<<"account/", (Account):34/binary, "-selectors">>
).
-define(MATCH_RESOURCE_SELECTORS_ENCODED(Account),
<<"account%2F", (Account):38/binary, "-selectors">>
).
-define(MATCH_RESOURCE_SELECTORS_encoded(Account),
<<"account%2f", (Account):38/binary, "-selectors">>
).

-define(MATCH_RESOURCE_SELECTORS_RAW(A, B, Rest),
<<(A):2/binary, (B):2/binary, (Rest):28/binary
,"-selectors"
>>
).
-define(MATCH_RESOURCE_SELECTORS_UNENCODED(A, B, Rest),
<<"account/", (A):2/binary, "/", (B):2/binary, "/", (Rest):28/binary
,"-selectors"
>>
).
-define(MATCH_RESOURCE_SELECTORS_ENCODED(A, B, Rest),
<<"account%2F", (A):2/binary, "%2F", (B):2/binary, "%2F", (Rest):28/binary
,"-selectors"
>>
).
-define(MATCH_RESOURCE_SELECTORS_encoded(A, B, Rest),
<<"account%2f", (A):2/binary, "%2f", (B):2/binary, "%2f", (Rest):28/binary
,"-selectors"
>>
).

%% KZ_NODES types
-record(whapp_info, {startup :: gregorian_seconds()}).

@@ -48,6 +48,7 @@
,{<<"Event-Name">>, [?DOC_EDITED
,?DOC_CREATED
,?DOC_DELETED
,?DB_EDITED
,?DB_CREATED
,?DB_DELETED
]}
@@ -14,6 +14,8 @@
,format_account_mod_id/1, format_account_mod_id/2, format_account_mod_id/3
,format_account_db/1
,format_account_modb/1, format_account_modb/2
,format_resource_selectors_id/1, format_resource_selectors_id/2
,format_resource_selectors_db/1
,normalize_account_name/1
,account_update/1, account_update/2
]).
@@ -272,6 +274,12 @@ raw_account_id(?MATCH_MODB_SUFFIX_ENCODED(A, B, Rest, _, _)) ->
?MATCH_ACCOUNT_RAW(A, B, Rest);
raw_account_id(?MATCH_MODB_SUFFIX_UNENCODED(A, B, Rest, _, _)) ->
?MATCH_ACCOUNT_RAW(A, B, Rest);
raw_account_id(?MATCH_RESOURCE_SELECTORS_RAW(AccountId)) ->
AccountId;
raw_account_id(?MATCH_RESOURCE_SELECTORS_UNENCODED(A, B, Rest)) ->
?MATCH_RESOURCE_SELECTORS_RAW(A, B, Rest);
raw_account_id(?MATCH_RESOURCE_SELECTORS_ENCODED(A, B, Rest)) ->
?MATCH_RESOURCE_SELECTORS_RAW(A, B, Rest);
raw_account_id(<<"number/", _/binary>>=Other) ->
Other;
raw_account_id(Other) ->
@@ -293,6 +301,72 @@ raw_account_modb(?MATCH_MODB_SUFFIX_ENCODED(A, B, Rest, Year, Month)) ->
raw_account_modb(?MATCH_MODB_SUFFIX_UNENCODED(A, B, Rest, Year, Month)) ->
?MATCH_MODB_SUFFIX_RAW(A, B, Rest, Year, Month).

%%--------------------------------------------------------------------
%% @public
%% @doc
%% Given a representation of an account resource_selectors return it in a 'encoded',
%% unencoded or 'raw' format.
%% @end
%%--------------------------------------------------------------------
-spec format_resource_selectors_id(api_binary()) -> api_binary().
-spec format_resource_selectors_id(api_binary(), account_format()) -> api_binary();
(api_binary(), gregorian_seconds()) -> api_binary(). %% MODb!

format_resource_selectors_id(Account) ->
format_resource_selectors_id(Account, 'raw').

format_resource_selectors_id('undefined', _Encoding) -> 'undefined';

format_resource_selectors_id(?MATCH_RESOURCE_SELECTORS_RAW(_)=AccountId, 'raw') ->
AccountId;
format_resource_selectors_id(?MATCH_RESOURCE_SELECTORS_ENCODED(_)=AccountDb, 'encoded') ->
AccountDb;
format_resource_selectors_id(?MATCH_RESOURCE_SELECTORS_UNENCODED(_)=AccountDbUn, 'unencoded') ->
AccountDbUn;
format_resource_selectors_id(?MATCH_ACCOUNT_RAW(A, B, Rest), 'raw') ->
?MATCH_RESOURCE_SELECTORS_RAW(A, B, Rest);
format_resource_selectors_id(?MATCH_ACCOUNT_RAW(A, B, Rest), 'encoded') ->
?MATCH_RESOURCE_SELECTORS_ENCODED(A, B, Rest);
format_resource_selectors_id(?MATCH_ACCOUNT_RAW(A, B, Rest), 'unencoded') ->
?MATCH_RESOURCE_SELECTORS_UNENCODED(A, B, Rest);

format_resource_selectors_id(AccountId, 'raw') ->
raw_resource_selectors_id(AccountId);
format_resource_selectors_id(AccountId, 'unencoded') ->
?MATCH_RESOURCE_SELECTORS_RAW(A,B,Rest) = raw_resource_selectors_id(AccountId),
to_binary(["account/", A, "/", B, "/", Rest]);
format_resource_selectors_id(AccountId, 'encoded') ->
?MATCH_RESOURCE_SELECTORS_RAW(A,B,Rest) = raw_resource_selectors_id(AccountId),
to_binary(["account%2F", A, "%2F", B, "%2F", Rest]).

%% @private
%% Returns account_id() | any()
%% Passes input along if not account_id() | account_db() | account_db_unencoded().
-spec raw_resource_selectors_id(ne_binary()) -> ne_binary().
raw_resource_selectors_id(?MATCH_RESOURCE_SELECTORS_RAW(AccountId)) ->
AccountId;
raw_resource_selectors_id(?MATCH_RESOURCE_SELECTORS_UNENCODED(A, B, Rest)) ->
?MATCH_RESOURCE_SELECTORS_RAW(A, B, Rest);
raw_resource_selectors_id(?MATCH_RESOURCE_SELECTORS_ENCODED(A, B, Rest)) ->
?MATCH_RESOURCE_SELECTORS_RAW(A, B, Rest);
raw_resource_selectors_id(Other) ->
case lists:member(Other, ?KZ_SYSTEM_DBS) of
'true' -> Other;
'false' ->
lager:warning("raw account resource_selectors id doesn't process '~p'", [Other]),
Other
end.

%%--------------------------------------------------------------------
%% @public
%% @doc
%% Given a representation of an account resource_selectors return it in a 'encoded',
%% @end
%%--------------------------------------------------------------------
-spec format_resource_selectors_db(api_binary()) -> api_binary().
format_resource_selectors_db(AccountId) ->
format_resource_selectors_id(AccountId, 'encoded').

%%--------------------------------------------------------------------
%% @public
%% @doc
@@ -89,7 +89,7 @@

-type db_classifications() :: 'account' | 'modb' | 'acdc' |
'numbers' | 'aggregate' | 'system' |
'deprecated' | 'undefined'.
'resource_selectors' | 'deprecated' | 'undefined'.

-type db_create_options() :: [{'q',integer()} | {'n',integer()}].

@@ -58,6 +58,7 @@ get_dataplan(DBName) ->
case kzs_util:db_classification(DBName) of
'modb' -> account_modb_dataplan(DBName);
'account' -> account_dataplan(DBName);
'resource_selectors' -> account_dataplan(DBName);
Else -> system_dataplan(DBName, Else)
end.

@@ -67,6 +68,7 @@ get_dataplan(DBName, DocType) ->
case kzs_util:db_classification(DBName) of
'modb' -> account_modb_dataplan(DBName, DocType);
'account' -> account_dataplan(DBName, DocType);
'resource_selectors' -> account_dataplan(DBName, DocType);
Else -> system_dataplan(DBName, Else)
end.

@@ -76,6 +78,7 @@ get_dataplan(DBName, DocType, DocOwner) ->
case kzs_util:db_classification(DBName) of
'modb' -> account_modb_dataplan(DBName, DocType, DocOwner);
'account' -> account_dataplan(DBName, DocType, DocOwner);
'resource_selectors' -> account_dataplan(DBName, DocType, DocOwner);
Else -> system_dataplan(DBName, Else)
end.

@@ -46,6 +46,10 @@ db_classification(?MATCH_MODB_SUFFIX_RAW(_Account,_Year,_Month)) -> 'modb';% r
db_classification(?MATCH_ACCOUNT_UNENCODED(_AccountId)) -> 'account';
db_classification(?MATCH_ACCOUNT_encoded(_AccountId)) -> 'account';
db_classification(?MATCH_ACCOUNT_ENCODED(_AccountId)) -> 'account';
db_classification(?MATCH_RESOURCE_SELECTORS_UNENCODED(_AccountId)) -> 'resource_selectors';
db_classification(?MATCH_RESOURCE_SELECTORS_encoded(_AccountId)) -> 'resource_selectors';
db_classification(?MATCH_RESOURCE_SELECTORS_ENCODED(_AccountId)) -> 'resource_selectors';
db_classification(?MATCH_RESOURCE_SELECTORS_RAW(_AccountId)) -> 'resource_selectors';
db_classification(?KZ_RATES_DB) -> 'system';
db_classification(?KZ_OFFNET_DB) -> 'system';
db_classification(?KZ_ANONYMOUS_CDR_DB) -> 'system';