diff --git a/applications/braintree/src/braintree_collect_recurring_req.erl b/applications/braintree/src/braintree_collect_recurring_req.erl new file mode 100644 index 00000000000..10397764b28 --- /dev/null +++ b/applications/braintree/src/braintree_collect_recurring_req.erl @@ -0,0 +1,37 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2013-2019, 2600Hz +%%% @doc +%%% @end +%%%----------------------------------------------------------------------------- +-module(braintree_collect_recurring_req). + +-export([handle_req/2]). + +-include("braintree.hrl"). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec handle_req(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_req(JObj, _Props) -> + 'true' = kapi_bookkeepers:collect_recurring_req_v(JObj), + Response = kz_json:from_list( + [{<<"Account-ID">>, kz_json:get_ne_binary_value(<<"Account-ID">>, JObj)} + ,{<<"Bookkeeper-ID">>, kz_json:get_ne_binary_value(<<"Bookkeeper-ID">>, JObj)} + ,{<<"Bookkeeper-Type">>, kz_json:get_ne_binary_value(<<"Bookkeeper-Type">>, JObj)} + ,{<<"Message">>, <<"Braintree performs recurring charges automatically">>} + ,{<<"Msg-ID">>, kz_json:get_value(<<"Msg-ID">>, JObj)} + ,{<<"Status">>, kz_services_recurring:status_good()} + ] ++ kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ), + RespQ = kz_json:get_ne_binary_value(<<"Server-ID">>, JObj), + + PublishFun = fun(P) -> kapi_bookkeepers:publish_collect_recurring_resp(RespQ, P) end, + + case kz_json:get_ne_binary_value(<<"Bookkeeper-Type">>, JObj) =:= ?APP_NAME of + 'false' -> + lager:debug("skipping collect recurring for another bookkeeper"); + 'true' -> + kz_amqp_worker:cast(Response, PublishFun) + end. diff --git a/applications/braintree/src/braintree_shared_listener.erl b/applications/braintree/src/braintree_shared_listener.erl index 4a1c57a3911..38e93849d30 100644 --- a/applications/braintree/src/braintree_shared_listener.erl +++ b/applications/braintree/src/braintree_shared_listener.erl @@ -26,7 +26,10 @@ -define(BINDINGS, [{'bookkeepers', []} ,{'self', []} ]). --define(RESPONDERS, [{'braintree_sale' +-define(RESPONDERS, [{'braintree_collect_recurring_req' + ,[{<<"bookkeepers">>, <<"collect_recurring_req">>}] + } + ,{'braintree_sale' ,[{<<"bookkeepers">>, <<"sale_req">>}] } ,{'braintree_refund' diff --git a/applications/crossbar/priv/api/swagger.json b/applications/crossbar/priv/api/swagger.json index e7d917ca18b..1d712df7b72 100644 --- a/applications/crossbar/priv/api/swagger.json +++ b/applications/crossbar/priv/api/swagger.json @@ -8939,6 +8939,99 @@ ], "type": "object" }, + "kapi.bookkeepers.collect_recurring_req_definition": { + "description": "AMQP API for bookkeepers.collect_recurring_req_definition", + "properties": { + "Account-ID": { + "type": "string" + }, + "Audit-Log": { + "type": "string" + }, + "Bookkeeper-ID": { + "type": "string" + }, + "Bookkeeper-Type": { + "type": "string" + }, + "Due-Timestamp": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "bookkeepers" + ], + "type": "string" + }, + "Event-Name": { + "enum": [ + "collect_recurring_req" + ], + "type": "string" + }, + "Vendor-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Bookkeeper-ID", + "Bookkeeper-Type", + "Vendor-ID" + ], + "type": "object" + }, + "kapi.bookkeepers.collect_recurring_resp_definition": { + "description": "AMQP API for bookkeepers.collect_recurring_resp_definition", + "properties": { + "Account-ID": { + "type": "string" + }, + "Bookkeeper-ID": { + "type": "string" + }, + "Bookkeeper-Type": { + "type": "string" + }, + "Details": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "bookkeepers" + ], + "type": "string" + }, + "Event-Name": { + "enum": [ + "collect_recurring_resp" + ], + "type": "string" + }, + "Message": { + "type": "string" + }, + "Reason": { + "type": "string" + }, + "Status": { + "type": "string" + }, + "Transaction-DB": { + "type": "string" + }, + "Transaction-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Bookkeeper-ID", + "Bookkeeper-Type", + "Status" + ], + "type": "object" + }, "kapi.bookkeepers.refund_req_definition": { "description": "AMQP API for bookkeepers.refund_req_definition", "properties": { diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.bookkeepers.collect_recurring_req_definition.json b/applications/crossbar/priv/couchdb/schemas/kapi.bookkeepers.collect_recurring_req_definition.json new file mode 100644 index 00000000000..bdfdf0f2975 --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.bookkeepers.collect_recurring_req_definition.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.bookkeepers.collect_recurring_req_definition", + "description": "AMQP API for bookkeepers.collect_recurring_req_definition", + "properties": { + "Account-ID": { + "type": "string" + }, + "Audit-Log": { + "type": "string" + }, + "Bookkeeper-ID": { + "type": "string" + }, + "Bookkeeper-Type": { + "type": "string" + }, + "Due-Timestamp": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "bookkeepers" + ], + "type": "string" + }, + "Event-Name": { + "enum": [ + "collect_recurring_req" + ], + "type": "string" + }, + "Vendor-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Bookkeeper-ID", + "Bookkeeper-Type", + "Vendor-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.bookkeepers.collect_recurring_resp_definition.json b/applications/crossbar/priv/couchdb/schemas/kapi.bookkeepers.collect_recurring_resp_definition.json new file mode 100644 index 00000000000..31fa05232d4 --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.bookkeepers.collect_recurring_resp_definition.json @@ -0,0 +1,53 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.bookkeepers.collect_recurring_resp_definition", + "description": "AMQP API for bookkeepers.collect_recurring_resp_definition", + "properties": { + "Account-ID": { + "type": "string" + }, + "Bookkeeper-ID": { + "type": "string" + }, + "Bookkeeper-Type": { + "type": "string" + }, + "Details": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "bookkeepers" + ], + "type": "string" + }, + "Event-Name": { + "enum": [ + "collect_recurring_resp" + ], + "type": "string" + }, + "Message": { + "type": "string" + }, + "Reason": { + "type": "string" + }, + "Status": { + "type": "string" + }, + "Transaction-DB": { + "type": "string" + }, + "Transaction-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Bookkeeper-ID", + "Bookkeeper-Type", + "Status" + ], + "type": "object" +} diff --git a/applications/tasks/src/modules/kt_bill_early.erl b/applications/tasks/src/modules/kt_bill_early.erl index 9cd12e7a2ea..e59e2621c6b 100644 --- a/applications/tasks/src/modules/kt_bill_early.erl +++ b/applications/tasks/src/modules/kt_bill_early.erl @@ -14,11 +14,6 @@ %% Triggerables -export([handle_req/1]). -%% Maunal Trgiggerables --export([send_reminder/1 - ,bill_early/1 - ]). - -include("tasks.hrl"). -define(MOD_CAT, <<(?CONFIG_CAT)/binary, ".bill_early">>). @@ -45,38 +40,12 @@ handle_req(AccountDb) -> AccountId = kz_util:format_account_id(AccountDb), EarlyDays = kapps_config:get_integer(?MOD_CAT, <<"how_many_early_days">>, 5), - {DueDate, IsDaysEarlyYet} = is_days_early_yet(EarlyDays), + {DueTimestamp, IsDaysEarlyYet} = is_days_early_yet(EarlyDays), ShouldBill = kapps_account_config:get_global(AccountId, ?MOD_CAT, <<"bill_early_enabled">>, 'false'), ShouldRemind = kapps_account_config:get_global(AccountId, ?MOD_CAT, <<"reminder_enabled">>, 'false'), - handle_req(AccountId, DueDate, IsDaysEarlyYet, ShouldBill, ShouldRemind). - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec bill_early(kz_term:ne_binary()) -> 'ok'. -bill_early(Account) -> - AccountId = kz_util:format_account_id(Account), - - EarlyDays = kapps_config:get_integer(?MOD_CAT, <<"how_many_early_days">>, 5), - {DueDate, _} = is_days_early_yet(EarlyDays), - Services = get_services(AccountId), - do_bill_early(AccountId, Services, DueDate). - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec send_reminder(kz_term:ne_binary()) -> 'ok'. -send_reminder(Account) -> - AccountId = kz_util:format_account_id(Account), - - EarlyDays = kapps_config:get_integer(?MOD_CAT, <<"how_many_early_days">>, 5), - {DueDate, _} = is_days_early_yet(EarlyDays), - Services = get_services(AccountId), - do_send_reminder(AccountId, Services, DueDate). + handle_req(AccountId, DueTimestamp, IsDaysEarlyYet, ShouldBill, ShouldRemind). %%%============================================================================= %%% Internal functions @@ -86,13 +55,15 @@ send_reminder(Account) -> %% @doc %% @end %%------------------------------------------------------------------------------ --spec handle_req(kz_term:ne_binary(), non_neg_integer(), boolean(), boolean(), boolean()) -> 'ok'. +-spec handle_req(kz_term:ne_binary(), kz_time:gregorian_seconds(), boolean(), boolean(), boolean()) -> 'ok'. handle_req(_, _, 'false', _ShouldBill, _ShouldRemind) -> 'ok'; -handle_req(AccountId, DueDate, 'true', 'true', _ShouldRemind) -> - bill_early(AccountId, DueDate, is_already_ran_account(AccountId)); -handle_req(AccountId, DueDate, 'true', 'false', 'true') -> - send_reminder(AccountId, DueDate, is_already_ran_account(AccountId)); +handle_req(AccountId, DueTimestamp, 'true', 'true', _ShouldRemind) -> + _ = kz_services_recurring:early_collect(AccountId, DueTimestamp), + 'ok'; +handle_req(AccountId, DueTimestamp, 'true', 'false', 'true') -> + _ = kz_services_recurring:send_early_reminder(AccountId, DueTimestamp), + 'ok'; handle_req(_, _, _, _ShouldBill, _ShouldRemind) -> 'ok'. @@ -100,171 +71,17 @@ handle_req(_, _, _, _ShouldBill, _ShouldRemind) -> %% @doc %% @end %%------------------------------------------------------------------------------ --spec is_days_early_yet(non_neg_integer()) -> {non_neg_integer(), boolean()}. +-spec is_days_early_yet(non_neg_integer()) -> kz_time:gregorian_seconds(). is_days_early_yet(EarlyDays) -> - {Year, Month, Day} = erlang:date(), + is_days_early_yet(erlang:date(), EarlyDays). + +-spec is_days_early_yet(calendar:date(), non_neg_integer()) -> kz_time:gregorian_seconds(). +is_days_early_yet({Year, Month, Day}, EarlyDays) -> LastDay = calendar:last_day_of_the_month(Year, Month), - DueTimestamp = calendar:datetime_to_gregorian_seconds({kz_date:normalize({Year, Month, LastDay + 1}), {0, 0, 0}}), + DueTimestamp = + calendar:datetime_to_gregorian_seconds({kz_date:normalize({Year, Month, LastDay + 1}) + ,{0, 0, 1} + }), {DueTimestamp, LastDay - EarlyDays < Day}. -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec get_services(kz_term:ne_binary()) -> kz_services:services(). -get_services(AccountId) -> - FetchOptions = ['hydrate_account_quantities' - ,'hydrate_cascade_quantities' - ,'hydrate_plans' - ,'hydrate_invoices' - ], - kz_services:fetch(AccountId, FetchOptions). - --spec is_already_ran_account(kz_term:ne_binary()) -> boolean(). -is_already_ran_account(AccountId) -> - case kzd_accounts:fetch(AccountId) of - {'ok', JObj} -> - case kzd_accounts:bill_early_task_timestamp(JObj) of - 'undefined' -> 'false'; - DueDate -> - %% If DueDate is in future (payment due day) then we already visited - %% this account before for the current bill cycle, otherwise this is the first time - %% we visited this account for this current bill cycle. - kz_time:now_s() =< DueDate - end; - {'error', _R} -> - lager:debug("can't check early bill/reminder was ran for ~s, lets check it tomorrow again" - ,[AccountId] - ), - 'true' - end. - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec bill_early(kz_term:ne_binary(), non_neg_integer(), boolean()) -> 'ok'. -bill_early(AccountId, DueDate, 'false') -> - lager:debug("attempting to early billing ~s", [AccountId]), - Services = get_services(AccountId), - case kz_services_plans:is_empty(kz_services:plans(Services)) of - 'true' -> - lager:debug("account ~s has no plan assigned, ignoring", [AccountId]), - set_bill_early_task_timestamp(AccountId, DueDate); - 'false' -> - do_bill_early(AccountId, Services, DueDate) - end; -bill_early(_, _, 'true') -> - 'ok'. - --spec do_bill_early(kz_term:ne_binary(), kz_services:services(), non_neg_integer()) -> 'ok'. -do_bill_early(AccountId, Services, DueDate) -> - Invoices = kz_services:invoices(Services), - _ = kz_services_invoices:foldl(fun(Invoice, _Acc) -> - do_bill_early(AccountId, Services, Invoice, DueDate) - end - ,'ok' - ,Invoices - ), - 'ok'. - --spec do_bill_early(kz_term:ne_binary(), kz_services:services(), kz_services_invoice:invoice(), non_neg_integer()) -> 'ok'. -do_bill_early(AccountId, Services, Invoice, DueDate) -> - lager:debug("trying to sync bookkeeper for ~s", [AccountId]), - Request = [{<<"Account-ID">>, kz_services:account_id(Services)} - ,{<<"Bookkeeper-ID">>, kz_services_invoice:bookkeeper_id(Invoice)} - ,{<<"Bookkeeper-Type">>, kz_services_invoice:bookkeeper_type(Invoice)} - ,{<<"Vendor-ID">>, kz_services_invoice:bookkeeper_vendor_id(Invoice)} - ,{<<"Items">> - ,kz_services_items:public_json( - kz_services_invoice:items(Invoice) - ) - } - ,{<<"Call-ID">>, kz_util:get_callid()} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ], - Resp = kz_amqp_worker:call(Request - ,fun kapi_bookkeepers:publish_update_req/1 - ,fun kapi_bookkeepers:update_resp_v/1 - ,20 * ?MILLISECONDS_IN_SECOND - ), - check_bokkkeeper_response(AccountId, DueDate, Resp). - -check_bokkkeeper_response(_AccountId, _, {'error', 'timeout'}) -> - lager:debug("timeout when running early bill for ~s, trying again tomorrow", [_AccountId]); -check_bokkkeeper_response(AccountId, DueDate, Resp) -> - case kz_json:get_ne_binary_value(<<"Status">>, Resp, <<"error">>) of - <<"error">> -> - lager:debug("failed to run early bill for ~s with reason ~s, trying again tomorrow" - ,[AccountId, kz_json:get_ne_binary_value(<<"Reason">>, Resp, <<"unknown">>)] - ); - _ -> - lager:debug("successfully billed early ~s", [AccountId]), - set_bill_early_task_timestamp(AccountId, DueDate) - end. - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec send_reminder(kz_term:ne_binary(), non_neg_integer(), boolean()) -> 'ok'. -send_reminder(AccountId, DueDate, 'false') -> - lager:debug("attempting to send bill reminder ~s", [AccountId]), - Services = get_services(AccountId), - case kz_services_plans:is_empty(kz_services:plans(Services)) of - 'true' -> - lager:debug("account ~s has no plan assigned, ignoring", [AccountId]), - set_bill_early_task_timestamp(AccountId, DueDate); - 'false' -> - do_send_reminder(AccountId, Services, DueDate) - end; -send_reminder(_, _, 'true') -> - 'ok'. - --spec do_send_reminder(kz_term:ne_binary(), kz_services:services(), non_neg_integer()) -> 'ok'. -do_send_reminder(AccountId, Services, DueDate) -> - Invoices = kz_services:invoices(Services), - _ = kz_services_invoices:foldl(fun(Invoice, _Acc) -> - do_notify_reseller(AccountId, Services, Invoice, DueDate) - end - ,'ok' - ,Invoices - ), - set_bill_early_task_timestamp(AccountId, DueDate). - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec do_notify_reseller(kz_term:ne_binary(), kz_services:services(), kz_services_invoice:invoice(), non_neg_integer()) -> 'ok'. -do_notify_reseller(AccountId, Services, Invoice, DueDate) -> - lager:debug("sending bill reminder notification to account ~s", [AccountId]), - Props = [{<<"Account-ID">>, AccountId} - ,{<<"Due-Date">>, DueDate} - ,{<<"Items">> - ,kz_services_items:public_json( - kz_services_invoice:items(Invoice) - ) - } - ,{<<"Timestamp">>, kz_time:now_s()} - | maybe_add_payment_token(Services, Invoice) - ++ kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ], - kapps_notify_publisher:cast(props:filter_undefined(Props), fun kapi_notifications:publish_bill_reminder/1). - --spec maybe_add_payment_token(kz_services:services(), kz_services_invoice:invoice()) -> - kz_term:proplist(). -maybe_add_payment_token(Services, Invoice) -> - case kz_services_invoice:bookkeeper_type(Invoice) of - 'undefined' -> []; - Bookkeeper -> - [{<<"Payment-Token">>, kz_services_payment_tokens:default(Services, Bookkeeper)}] - end. - --spec set_bill_early_task_timestamp(kz_term:ne_binary(), non_neg_integer()) -> 'ok'. -set_bill_early_task_timestamp(AccountId, DueDate) -> - Update = [{kzd_accounts:path_bill_early_task_timestamp(), DueDate}], - _ = kzd_accounts:update(AccountId, Update), - 'ok'. - %%% End of Module. diff --git a/core/kazoo_amqp/src/api/kapi_bookkeepers.erl b/core/kazoo_amqp/src/api/kapi_bookkeepers.erl index 56e318a83ba..d9cc7b3e41b 100644 --- a/core/kazoo_amqp/src/api/kapi_bookkeepers.erl +++ b/core/kazoo_amqp/src/api/kapi_bookkeepers.erl @@ -10,6 +10,14 @@ -export([api_definitions/0, api_definition/1]). +-export([collect_recurring_req/1, collect_recurring_req_v/1 + ,publish_collect_recurring_req/1, publish_collect_recurring_req/2 + ]). + +-export([collect_recurring_resp/1, collect_recurring_resp_v/1 + ,publish_collect_recurring_resp/2, publish_collect_recurring_resp/3 + ]). + -export([sale_req/1, sale_req_v/1 ,publish_sale_req/1, publish_sale_req/2 ]). @@ -43,6 +51,65 @@ %%% Internal Bookkeeper Definitions %%%============================================================================= +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec collect_recurring_req_definition() -> kapi_definition:api(). +collect_recurring_req_definition() -> + #kapi_definition{name = <<"collect_recurring_req">> + ,friendly_name = <<"Collect Recurring Charges Request">> + ,description = <<"Will trigger the appropriate bookkeeper to collect recurring charges">> + ,build_fun = fun collect_recurring_req/1 + ,validate_fun = fun collect_recurring_req_v/1 + ,publish_fun = fun collect_recurring_req/1 + ,binding = ?BINDING_STRING(<<"collect_recurring">>, <<"request">>) + ,restrict_to = 'collect_recurring' + ,required_headers = [<<"Account-ID">> + ,<<"Bookkeeper-ID">> + ,<<"Bookkeeper-Type">> + ,<<"Due-Timestamp">> + ,<<"Vendor-ID">> + ] + ,optional_headers = [<<"Audit-Log">> + ] + ,values = [{<<"Event-Category">>, <<"bookkeepers">>} + ,{<<"Event-Name">>, <<"collect_recurring_req">>} + ] + ,types = [] + }. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec collect_recurring_resp_definition() -> kapi_definition:api(). +collect_recurring_resp_definition() -> + #kapi_definition{name = <<"collect_recurring_resp">> + ,friendly_name = <<"Collect Recurring Charges Response">> + ,description = <<"Result of request to collect recurring charges via the bookkeeper">> + ,build_fun = fun collect_recurring_resp/1 + ,validate_fun = fun collect_recurring_resp_v/1 + ,publish_fun = fun publish_collect_recurring_resp/2 + ,binding = ?BINDING_STRING(<<"collect_recurring">>, <<"response">>) + ,restrict_to = 'collect_recurring' + ,required_headers = [<<"Account-ID">> + ,<<"Bookkeeper-ID">> + ,<<"Bookkeeper-Type">> + ,<<"Status">> + ] + ,optional_headers = [<<"Details">> + ,<<"Message">> + ,<<"Reason">> + ,<<"Transaction-ID">> + ,<<"Transaction-DB">> + ] + ,values = [{<<"Event-Category">>, <<"bookkeepers">>} + ,{<<"Event-Name">>, <<"collect_recurring_resp">>} + ] + ,types = [] + }. + %%------------------------------------------------------------------------------ %% @doc %% @end @@ -256,7 +323,9 @@ standing_resp_definition() -> %%------------------------------------------------------------------------------ -spec api_definitions() -> kapi_definition:apis(). api_definitions() -> - [sale_req_definition() + [collect_recurring_req_definition() + ,collect_recurring_resp_definition() + ,sale_req_definition() ,sale_resp_definition() ,refund_req_definition() ,refund_resp_definition() @@ -276,6 +345,10 @@ api_definition(Name) when is_atom(Name) -> api_definition(kz_term:to_binary(Name)); api_definition(Name) when is_list(Name) -> api_definition(kz_term:to_binary(Name)); +api_definition(<<"collect_recurring_req">>) -> + collect_recurring_req_definition(); +api_definition(<<"collect_recurring_resp">>) -> + collect_recurring_resp_definition(); api_definition(<<"sale_req">>) -> sale_req_definition(); api_definition(<<"sale_resp">>) -> @@ -395,6 +468,48 @@ validate(JObj, Definition) -> %%% Internal Bookkeepers Functions %%%============================================================================= +%%------------------------------------------------------------------------------ +%% @doc Collect Recurring. +%% Takes prop-list, creates JSON string and publish it on AMQP. +%% @end +%%------------------------------------------------------------------------------ +-spec collect_recurring_req(kz_term:api_terms()) -> api_formatter_return(). +collect_recurring_req(Prop) -> + build_message(Prop, collect_recurring_req_definition()). + +-spec collect_recurring_req_v(kz_term:api_terms()) -> boolean(). +collect_recurring_req_v(Prop) -> + validate(Prop, collect_recurring_req_definition()). + +-spec publish_collect_recurring_req(kz_term:api_terms()) -> 'ok'. +publish_collect_recurring_req(JObj) -> + publish_collect_recurring_req(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_collect_recurring_req(kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. +publish_collect_recurring_req(API, ContentType) -> + #kapi_definition{binding = Binding + ,values = Values + } = collect_recurring_req_definition(), + {'ok', Payload} = kz_api:prepare_api_payload(API, Values, fun collect_recurring_req/1), + kz_amqp_util:bookkeepers_publish(Binding, Payload, ContentType). + +-spec collect_recurring_resp(kz_term:api_terms()) -> api_formatter_return(). +collect_recurring_resp(Prop) -> + build_message(Prop, collect_recurring_resp_definition()). + +-spec collect_recurring_resp_v(kz_term:api_terms()) -> boolean(). +collect_recurring_resp_v(Prop) -> + validate(Prop, collect_recurring_resp_definition()). + +-spec publish_collect_recurring_resp(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. +publish_collect_recurring_resp(RespQ, JObj) -> + publish_collect_recurring_resp(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_collect_recurring_resp(kz_term:ne_binary(), kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. +publish_collect_recurring_resp(RespQ, API, ContentType) -> + #kapi_definition{values = Values} = collect_recurring_resp_definition(), + {'ok', Payload} = kz_api:prepare_api_payload(API, Values, fun collect_recurring_resp/1), + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). %%------------------------------------------------------------------------------ %% @doc Sale diff --git a/core/kazoo_services/src/kz_services_recurring.erl b/core/kazoo_services/src/kz_services_recurring.erl new file mode 100644 index 00000000000..48cdb30ac6b --- /dev/null +++ b/core/kazoo_services/src/kz_services_recurring.erl @@ -0,0 +1,388 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2012-2019, 2600Hz +%%% @doc +%%% @end +%%%----------------------------------------------------------------------------- +-module(kz_services_recurring). + +-export([early_collect/2 + ,send_early_reminder/2 + + ,force_collect/1, force_collect/2 + ,force_reminder/1, force_reminder/2 + + ,status_good/0 + ]). + +-export([is_current_month_collected/1 + + ,is_last_month_collected/1 + ,is_last_month_collected/3 + + ,is_collected/3 + ]). + +-include("services.hrl"). + +-define(COLLECT_RECURRING_MARKER_ID, <<"collect_recurring_was_here">>). +-define(REMINDER_MARKER_ID, <<"early_bill_reminder_was_here">>). + +-type error_details() :: #{message := kz_term:ne_binary() + ,reason := kz_term:ne_binary() + }. + +-type run_return() :: {'ok', kz_term:ne_binary()} | + {'error', error_details()}. + +%%------------------------------------------------------------------------------ +%% @doc Attempt to collect recurring charges for the due timestamp. +%% +%% This get services for the provided account ID and send collect recurring AMQP +%% messages to all invoice's bookkeeper. +%% +%% `DueTimestamp' is due date for recurring, and will be used to check to get +%% previous month database to check if the money is collected already or not. +%% @end +%%------------------------------------------------------------------------------ +-spec early_collect(kz_term:ne_binary(), kz_time:gregorian_seconds()) -> run_return(). +early_collect(AccountId, DueTimestamp) -> + {PrevYear, PrevMonth, _} = get_previous_month(DueTimestamp), + ShouldCollect = should_process(AccountId, ?COLLECT_RECURRING_MARKER_ID, PrevYear, PrevMonth), + early_collect(AccountId, DueTimestamp, PrevYear, PrevMonth, ShouldCollect). + +%%------------------------------------------------------------------------------ +%% @doc Attempt to send a remonder for recurring charges for the due timestamp. +%% +%% `DueTimestamp' is due date for recurring, and will be used to check to get +%% previous month database to check if the notification is send already or not. +%% @end +%%------------------------------------------------------------------------------ +-spec send_early_reminder(kz_term:ne_binary(), non_neg_integer()) -> {'ok', kz_term:ne_binary()}. +send_early_reminder(AccountId, DueTimestamp) -> + {PrevYear, PrevMonth, _} = get_previous_month(DueTimestamp), + ShouldCollect = should_process(AccountId, ?REMINDER_MARKER_ID, PrevYear, PrevMonth), + send_early_reminder(AccountId, DueTimestamp, PrevYear, PrevMonth, ShouldCollect). + +%%------------------------------------------------------------------------------ +%% @doc Collect recurring charges now without checking if it already collect. +%% +%% It doesn't not check and set the marker to see if the it ran before. +%% @end +%%------------------------------------------------------------------------------ +-spec force_collect(kz_term:ne_binary()) -> run_return(). +force_collect(AccountId) -> + force_collect(AccountId, kz_time:now_s()). + +%%------------------------------------------------------------------------------ +%% @doc Collect recurring charges for `DueTimestamp' without checking if +%% it already collect. +%% +%% It doesn't not check and set the marker to see if the it ran before. +%% @end +%%------------------------------------------------------------------------------ +-spec force_collect(kz_term:ne_binary(), kz_time:gregorian_seconds()) -> run_return(). +force_collect(AccountId, DueTimestamp) -> + collect(AccountId, DueTimestamp). + +%%------------------------------------------------------------------------------ +%% @doc Send recurring charges reminder now without checking if it +%% already collect. +%% +%% It doesn't not check and set the marker to see if the it ran before. +%% @end +%%------------------------------------------------------------------------------ +-spec force_reminder(kz_term:ne_binary()) -> {'ok', kz_term:ne_binary()}. +force_reminder(AccountId) -> + force_reminder(AccountId, kz_time:now_s()). + +%%------------------------------------------------------------------------------ +%% @doc Send recurring charges reminder with `DueTimestamp' without checking if +%% it already collect. +%% +%% It doesn't not check and set the marker to see if the it ran before. +%% @end +%%------------------------------------------------------------------------------ +-spec force_reminder(kz_term:ne_binary(), kz_time:gregorian_seconds()) -> {'ok', kz_term:ne_binary()}. +force_reminder(AccountId, DueTimestamp) -> + send_reminder(AccountId, DueTimestamp). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec status_good() -> kz_term:ne_binary(). +status_good() -> <<"successful">>. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec is_current_month_collected(kz_term:ne_binary()) -> boolean(). +is_current_month_collected(?MATCH_MODB_SUFFIX_RAW(_, Year, Month) = AccountMODB) -> + is_collected(AccountMODB, Year, Month); +is_current_month_collected(?MATCH_MODB_SUFFIX_UNENCODED(_, Year, Month) = AccountMODB) -> + is_collected(AccountMODB, Year, Month); +is_current_month_collected(?MATCH_MODB_SUFFIX_ENCODED(_, Year, Month) = AccountMODB) -> + is_collected(AccountMODB, Year, Month); +is_current_month_collected(Account) -> + ?MATCH_MODB_SUFFIX_ENCODED(_, Year, Month) = kz_util:format_account_mod_id(Account), + is_collected(Account, Year, Month). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec is_last_month_collected(kz_term:ne_binary()) -> boolean(). +is_last_month_collected(?MATCH_MODB_SUFFIX_RAW(_, Year, Month) = AccountMODB) -> + is_last_month_collected(AccountMODB, Year, Month); +is_last_month_collected(?MATCH_MODB_SUFFIX_UNENCODED(_, Year, Month) = AccountMODB) -> + is_last_month_collected(AccountMODB, Year, Month); +is_last_month_collected(?MATCH_MODB_SUFFIX_ENCODED(_, Year, Month) = AccountMODB) -> + is_last_month_collected(AccountMODB, Year, Month); +is_last_month_collected(Account) -> + MODB = ?MATCH_MODB_SUFFIX_ENCODED(_, _, _) = kz_util:format_account_mod_id(Account), + is_last_month_collected(MODB). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec is_last_month_collected(kz_term:ne_binary(), kz_term:ne_binary() | kz_time:year(), kz_term:ne_binary() | kz_time:month()) -> + boolean(). +is_last_month_collected(Account, Year, Month) -> + Timestamp = + calendar:datetime_to_gregorian_seconds({{kz_term:to_integer(Year), kz_term:to_integer(Month), 1} + ,{0, 0, 0} + }), + {PrevYear, PrevMonth, _} = get_previous_month(Timestamp), + is_collected(Account, PrevYear, PrevMonth). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec is_collected(kz_term:ne_binary(), kz_term:ne_binary() | kz_time:year(), kz_term:ne_binary() | kz_time:month()) -> + boolean(). +is_collected(Account, Year, Month) -> + AccountId = kz_util:format_account_id(Account), + is_proccessed(AccountId + ,?COLLECT_RECURRING_MARKER_ID + ,kz_term:to_integer(Year) + ,kz_term:to_integer(Month) + ). + +%%%============================================================================= +%%% Collect Internal functions +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec early_collect(kz_term:ne_binary(), kz_time:gregorian_seconds(), kz_time:year(), kz_time:month(), boolean()) -> run_return(). +early_collect(AccountId, DueTimestamp, Year, Month, 'false') -> + case set_marker(AccountId, ?COLLECT_RECURRING_MARKER_ID, Year, Month) of + {'ok', _} -> + collect(AccountId, DueTimestamp); + {'error', _Reason} -> + ErrMessage = <<"unabled to set marker to collect recurring charges">>, + lager:debug("charging account ~s failed: ~s", [AccountId, ErrMessage]), + {'error', #{message => ErrMessage + ,reason => <<"set_marker_fault">> + } + } + end; +early_collect(_, _, _, _, 'true') -> + {'ok', <<"account is already processed">>}. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec collect(kz_term:ne_binary(), kz_time:gregorian_seconds()) -> run_return(). +collect(AccountId, DueTimestamp) -> + lager:debug("attempting to collect recurring charges for ~s", [AccountId]), + Services = kz_services:fetch(AccountId, ['hydrate_plans']), + Invoices = kz_services:invoices(Services), + Results = kz_services_invoices:foldl(collect_invoices_fold_fun(Services, DueTimestamp), [], Invoices), + handle_collect_bookkeeper_results(Results). + +-type collect_invoice_fold() :: fun((kz_services_invoice:invoice(), kz_amqp_worker:request_return()) -> [kz_amqp_worker:request_return()]). +-spec collect_invoices_fold_fun(kz_services:services(), kz_time:gregorian_seconds()) -> collect_invoice_fold(). +collect_invoices_fold_fun(Services, DueTimestamp) -> + fun(Invoice, Results) -> + Type = kz_services_invoice:bookkeeper_type(Invoice), + case kzd_services:default_bookkeeper_type() =:= Type of + 'true' -> + Result = kz_json:from_list([{<<"Status">>, status_good()}]), + [{'ok', Result} | Results]; + 'false' -> + Result = collect_bookkeeper(Invoice, Services, DueTimestamp), + [Result | Results] + end + end. + +-spec collect_bookkeeper(kz_services_invoice:invoice(), kz_services:services(), kz_time:gregorian_seconds()) -> + kz_amqp_worker:request_return(). +collect_bookkeeper(Invoice, Services, DueTimestamp) -> + Request = [{<<"Account-ID">>, kz_services:account_id(Services)} + ,{<<"Bookkeeper-ID">>, kz_services_invoice:bookkeeper_id(Invoice)} + ,{<<"Bookkeeper-Type">>, kz_services_invoice:bookkeeper_type(Invoice)} + ,{<<"Call-ID">>, kz_util:get_callid()} + ,{<<"Due-Timestamp">>, DueTimestamp} + ,{<<"Vendor-ID">>, kz_services_invoice:bookkeeper_vendor_id(Invoice)} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kz_amqp_worker:call(Request + ,fun kapi_bookkeepers:publish_collect_recurring_req/1 + ,fun kapi_bookkeepers:collect_recurring_resp_v/1 + ,2 * ?MILLISECONDS_IN_SECOND + ). + +-spec handle_collect_bookkeeper_results(any()) -> run_return(). +handle_collect_bookkeeper_results([]) -> + {'ok', status_good()}; +handle_collect_bookkeeper_results([{'ok', _Result} | Results]) -> + handle_collect_bookkeeper_results(Results); +handle_collect_bookkeeper_results([_Error | Results]) -> + lager:debug("unexpected bookkeeper result: ~p", [_Error]), + handle_collect_bookkeeper_results(Results). + +%%%============================================================================= +%%% Reminder Internal functions +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec send_early_reminder(kz_term:ne_binary(), kz_time:gregorian_seconds(), kz_time:year(), kz_time:month(), boolean()) -> + {'ok', kz_term:ne_binary()}. +send_early_reminder(AccountId, DueTimestamp, Year, Month, 'false') -> + lager:debug("attempting to send bill reminder ~s", [AccountId]), + _ = send_reminder(AccountId, DueTimestamp), + _ = set_marker(AccountId, ?REMINDER_MARKER_ID, Year, Month), + {'ok', status_good()}; +send_early_reminder(_, _, _, _, 'true') -> + {'ok', <<"account is already processed">>}. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec send_reminder(kz_term:ne_binary(), non_neg_integer()) -> {'ok', kz_term:ne_binary()}. +send_reminder(AccountId, DueTimestamp) -> + FetchOptions = ['hydrate_account_quantities' + ,'hydrate_cascade_quantities' + ,'hydrate_plans' + ,'hydrate_invoices' + ,'skip_cache' + ], + Services = kz_services:fetch(AccountId, FetchOptions), + Invoices = kz_services:invoices(Services), + _ = kz_services_invoices:foldl(fun(Invoice, _Acc) -> + do_notify_reseller(AccountId, Services, Invoice, DueTimestamp) + end + ,'ok' + ,Invoices + ), + {'ok', status_good()}. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec do_notify_reseller(kz_term:ne_binary(), kz_services:services(), kz_services_invoice:invoice(), non_neg_integer()) -> 'ok'. +do_notify_reseller(AccountId, Services, Invoice, DueTimestamp) -> + lager:debug("sending bill reminder notification to account ~s", [AccountId]), + Props = [{<<"Account-ID">>, AccountId} + ,{<<"Due-Date">>, DueTimestamp} + ,{<<"Items">> + ,kz_services_items:public_json( + kz_services_invoice:items(Invoice) + ) + } + ,{<<"Timestamp">>, kz_time:now_s()} + | maybe_add_payment_token(Services, Invoice) + ++ kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kapps_notify_publisher:cast(props:filter_undefined(Props), fun kapi_notifications:publish_bill_reminder/1). + +-spec maybe_add_payment_token(kz_services:services(), kz_services_invoice:invoice()) -> + kz_term:proplist(). +maybe_add_payment_token(Services, Invoice) -> + case kz_services_invoice:bookkeeper_type(Invoice) of + 'undefined' -> []; + Bookkeeper -> + [{<<"Payment-Token">>, kz_services_payment_tokens:default(Services, Bookkeeper)}] + end. + +%%%============================================================================= +%%% Internal functions +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec get_previous_month(kz_time:gregorian_seconds()) -> kz_time:date(). +get_previous_month(DueTimestamp) -> + {{DueYear, DueMonth, _}, _} = calendar:gregorian_seconds_to_datetime(DueTimestamp), + kz_date:normalize({DueYear, DueMonth - 1,1}). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec should_process(kz_term:ne_binary(), kz_term:ne_binary(), kz_time:year(), kz_time:month()) -> boolean(). +should_process(AccountId, MarkerId, Year, Month) -> + is_account_enabled(AccountId) + andalso is_proccessed(AccountId, MarkerId, Year, Month). + +-spec is_account_enabled(kz_term:ne_binary()) -> 'ok'. +is_account_enabled(AccountId) -> + case kzd_accounts:is_enabled(AccountId) of + 'false' -> + lager:debug("skipping disabled account ~s", [AccountId]), + 'false'; + 'true' -> + 'true' + end. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec is_proccessed(kz_term:ne_binary(), kz_term:ne_binary(), kz_time:year(), kz_time:month()) -> boolean(). +is_proccessed(AccountId, MarkerId, Year, Month) -> + AccountMODB = kz_util:format_account_mod_id(AccountId, Year, Month), + case kazoo_modb:open_doc(AccountMODB, MarkerId) of + {'ok', JObj} -> + lager:debug("recurring charges for account ~s for ~b-~b is processed on ~s" + ,[AccountId, Year, Month, kz_time:format_datetime(kz_doc:created(JObj))] + ), + 'true'; + {'error', 'not_found'} -> + 'false' + end. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec set_marker(kz_term:ne_binary(), kz_term:ne_binary(), kz_time:year(), kz_time:month()) -> 'ok'. +set_marker(AccountId, MarkerId, Year, Month) -> + AccountMODB = kz_util:format_account_mod_id(AccountId, Year, Month), + PvtOptions = [{'account_id', AccountId} + ,{'crossbar_doc_vsn', 1} + ,{'id', MarkerId} + ,{'type', <<"services_recurring_marker">>} + ], + JObj = kz_doc:update_pvt_parameters(kz_json:new(), AccountMODB, PvtOptions), + lager:debug("attempting to mark recurring marker in modb ~s", [AccountMODB]), + case kazoo_modb:save_doc(AccountMODB, JObj) of + {'ok', _}=OK -> OK; + {'error', _Reason}=Error -> + lager:error("failed to save bill early/reminder maker for ~s: ", [AccountId, _Reason]), + Error + end.