defmodule Glific.Clients.DigitalGreen do
@moduledoc """
Tweak GCS Bucket name based on group that the contact is in (if any)
"""
import Ecto.Query, warn: false
require Logger
alias Glific.{
Contacts,
Contacts.Contact,
Flows.ContactField,
Partners,
Partners.OrganizationData,
Repo,
Sheets.ApiClient
}
@crp_id_key "dg_crp_ids"
@geographies %{
"en" => %{
"database_key" => "geography_en",
"sheet_link" =>
"https://docs.google.com/spreadsheets/d/e/2PACX-1vTYW2yLqES4FGTIDVDIm21XTWsoOPPDaDR8XO0gv32cgydsjcX1d_AaXCuMTNymJhCPzAU-FT1Mont5/pub?gid=1669998910&single=true&output=csv"
},
"te" => %{
"database_key" => "geography_te",
"sheet_link" =>
"https://docs.google.com/spreadsheets/d/e/2PACX-1vTYW2yLqES4FGTIDVDIm21XTWsoOPPDaDR8XO0gv32cgydsjcX1d_AaXCuMTNymJhCPzAU-FT1Mont5/pub?gid=752391516&single=true&output=csv"
}
}
@doc """
Returns time in second till next defined Timeslot
"""
@spec time_till_next_slot(DateTime.t()) :: non_neg_integer()
def time_till_next_slot(time \\ DateTime.utc_now()) do
current_time = Timex.now() |> Timex.beginning_of_day()
# Morning slot at 6am IST
morning_slot = Timex.shift(current_time, hours: 0, minutes: 30)
# Evening slot at 6:30pm IST
evening_slot = Timex.shift(current_time, hours: 12, minutes: 30)
# the minimum wait unit in Glific is 1 minute
next_slot(time, morning_slot, evening_slot)
|> Timex.diff(time, :seconds)
|> max(61)
end
defp next_slot(time, morning_slot, evening_slot) do
# Setting next defined slot to nearest next defined slot
cond do
Timex.compare(time, morning_slot, :seconds) < 0 ->
morning_slot
Timex.compare(time, evening_slot, :seconds) < 0 ->
evening_slot
# Setting next defined slot to next day morning slot
true ->
Timex.shift(morning_slot, days: 1)
end
end
@doc """
Create a webhook with different signatures, so we can easily implement
additional functionality as needed
"""
@spec webhook(String.t(), map()) :: map()
def webhook("load_crp_ids", fields) do
Glific.parse_maybe_integer!(fields["organization_id"])
|> load_crp_ids()
fields
end
def webhook("validate_crp_id", fields) do
Glific.parse_maybe_integer!(fields["organization_id"])
|> validate_crp_id(fields["crp_id"])
end
def webhook("load_geography", fields) do
org_id = Glific.parse_maybe_integer!(fields["organization_id"])
load_geographies(org_id, @geographies["en"])
load_geographies(org_id, @geographies["te"])
fields
end
def webhook("get_district_list", fields) do
org_id = Glific.parse_maybe_integer!(fields["organization_id"])
region_name = fields["region"]
language = get_language(fields["contact"]["id"])
get_geographies_data(org_id, @geographies[language.locale])
|> get_in([region_name])
|> geographies_list_results()
end
def webhook("get_division_list", fields) do
org_id = Glific.parse_maybe_integer!(fields["organization_id"])
language = get_language(fields["contact"]["id"])
region_name = fields["region"]
district_name = fields["district"]
get_geographies_data(org_id, @geographies[language.locale])
|> get_in([region_name, district_name])
|> geographies_list_results()
end
def webhook("get_mandal_list", fields) do
org_id = Glific.parse_maybe_integer!(fields["organization_id"])
language = get_language(fields["contact"]["id"])
region_name = fields["region"]
district_name = fields["district"]
division_name = fields["division"]
get_geographies_data(org_id, @geographies[language.locale])
|> get_in([region_name, district_name, division_name, "mandals"])
|> geographies_list_results()
end
def webhook("set_geography", fields) do
type = fields["type"]
user_input = fields["selected_index"]
index_map = Jason.decode!(fields["index_map"])
contact_id = Glific.parse_maybe_integer!(fields["contact"]["id"])
if Map.has_key?(index_map, user_input) do
set_geography(type, index_map[user_input], contact_id)
%{error: false, message: "Geography set successfully for #{type}"}
else
index_map
|> Enum.find(fn {_index, value} -> String.downcase(value) == String.downcase(user_input) end)
|> case do
nil ->
%{error: true, message: "Invalid selected index"}
{index, value} ->
set_geography(type, index_map[index], contact_id)
%{error: false, message: "Geography set successfully for #{type} and value #{value}"}
_ ->
%{error: true, message: "Invalid selected index"}
end
end
end
def webhook("push_crop_message", fields) do
crop_age = fields["crop_age"]
{:ok, organization_data} =
Repo.fetch_by(OrganizationData, %{
organization_id: fields["organization_id"],
key: fields["crop"]
})
template_uuid = get_in(organization_data.json, [crop_age, "template_uuid"])
variables = get_in(organization_data.json, [crop_age, "variables"])
crop_stage = get_in(organization_data.json, [crop_age, "crop_stage"])
if template_uuid,
do: %{
is_valid: true,
template_uuid: template_uuid,
crop_stage: crop_stage,
variables: Jason.encode!(variables)
},
else: %{is_valid: false}
end
def webhook("push_crop_calendar_message", fields) do
crop_age = fields["crop_age"]
{:ok, organization_data} =
Repo.fetch_by(OrganizationData, %{
organization_id: fields["organization_id"],
key: fields["crop"]
})
template_uuid = get_in(organization_data.json, [crop_age, "template_uuid"])
variables = get_in(organization_data.json, [crop_age, "variables"])
crop_stage = get_in(organization_data.json, [crop_age, "crop_stage"])
media_url = get_in(organization_data.json, [crop_age, "media_url"])
crop_stage_eng = get_in(organization_data.json, [crop_age, "crop_stage_eng"])
if template_uuid,
do: %{
is_valid: true,
template_uuid: template_uuid,
crop_stage: crop_stage,
variables: Jason.encode!(variables),
media_url: media_url,
crop_age: crop_age,
crop_stage_eng: crop_stage_eng,
organization_id: fields["organization_id"]
},
else: %{is_valid: false}
end
def webhook("set_reminders", fields) do
{:ok, contact_id} = Glific.parse_maybe_integer(fields["contact"]["id"])
with {:ok, contact} <-
Repo.fetch_by(Contact, %{
organization_id: fields["organization_id"],
id: contact_id
}) do
set_contact_reminder(contact.last_message_at)
end
end
def webhook("parse_weather_report", fields) do
weather_report = fields["results"]["weather_report"]
%{report_msg: get_report_msg(weather_report, fields["organization_id"])}
end
def webhook(_, _fields),
do: %{}
@spec find_translation(map(), String.t(), String.t()) :: map()
defp find_translation(translations, type, value) do
geographies = get_in(translations, [type])
Enum.reduce(geographies, %{found: false}, fn geography, acc ->
if geography["telugu"] == value || geography["english"] == value,
do: Map.merge(acc, %{found: true, slug: geography["english"]}),
else: acc
end)
end
@spec set_contact_reminder(DateTime.t() | nil) :: map()
defp set_contact_reminder(nil), do: %{is_inactive: false, send_reminder: false}
defp set_contact_reminder(last_message_at) do
days_since_last_message = Timex.diff(Timex.today(), last_message_at, :days)
is_inactive = if days_since_last_message >= 7, do: true, else: false
send_reminder =
if days_since_last_message != 0 and rem(days_since_last_message, 7) == 0,
do: true,
else: false
%{
is_inactive: is_inactive,
send_reminder: send_reminder
}
end
@spec set_geography(String.t(), String.t(), non_neg_integer()) :: any()
defp set_geography(type, value, contact_id) do
updated_contact =
Contacts.get_contact!(contact_id)
|> ContactField.do_add_contact_field(type, type, value)
{:ok, organization_data} =
Repo.fetch_by(OrganizationData, %{
organization_id: updated_contact.organization_id,
key: "ryss_geography_translations"
})
translation = find_translation(organization_data.json, type, value)
if translation.found,
do:
ContactField.do_add_contact_field(
updated_contact,
"#{type}_slug",
"#{type}_slug",
translation.slug
)
end
@spec get_geographies_data(integer(), map()) :: map()
defp get_geographies_data(org_id, geographies_config) do
{:ok, org_data} =
Repo.fetch_by(OrganizationData, %{
organization_id: org_id,
key: geographies_config["database_key"]
})
org_data.json
end
@spec geographies_list_results(map() | list()) :: map()
defp geographies_list_results(resource_map) when resource_map in [nil, %{}] do
%{error: true, message: "Resource not found"}
end
defp geographies_list_results(resource_list) when is_list(resource_list) do
{index_map, message_list} = format_geographies_message(resource_list)
%{
error: false,
list_message: Enum.join(message_list, "\n"),
index_map: Jason.encode!(index_map)
}
|> Map.merge(convert_to_interactive_message(resource_list))
end
defp geographies_list_results(resource_map) do
{index_map, message_list} =
Map.keys(resource_map)
|> format_geographies_message()
%{
error: false,
list_message: Enum.join(message_list, "\n"),
index_map: Jason.encode!(index_map)
}
|> Map.merge(convert_to_interactive_message(Map.keys(resource_map)))
end
@spec format_geographies_message(list()) :: {map(), list()}
defp format_geographies_message(districts) do
districts
|> Enum.with_index(1)
|> Enum.reduce({%{}, []}, fn {district, index}, {index_map, message_list} ->
{
Map.put(index_map, index, district),
message_list ++ ["Type *#{index}* for #{district}"]
}
end)
end
@spec convert_to_interactive_message(list()) :: map()
defp convert_to_interactive_message(resource_list) do
list_length = length(resource_list)
if(list_length > 10) do
%{
is_interative: false,
interative_items_count: 0
}
else
%{
is_interative: true,
interative_items_count: list_length,
interative_data:
resource_list
|> Enum.with_index()
|> Enum.map(fn {value, index} -> {index + 1, value} end)
|> Enum.into(%{})
}
end
end
@spec load_crp_ids(any) :: %{status: <<_::88>>}
defp load_crp_ids(org_id) do
ApiClient.get_csv_content(url: "https://storage.googleapis.com/dg_voicebot/crp_ids.csv")
|> Enum.reduce(%{}, fn {_, row}, acc ->
crp_id = row["Employee Id"]
if crp_id in [nil, ""], do: acc, else: Map.put(acc, Glific.string_clean(crp_id), row)
end)
|> then(fn crp_data ->
Partners.maybe_insert_organization_data(@crp_id_key, crp_data, org_id)
end)
%{status: "successfull"}
end
@spec validate_crp_id(integer(), nil | integer()) :: map()
defp validate_crp_id(org_id, crp_id) do
crp_id = Glific.string_clean(crp_id)
{:ok, org_data} =
Repo.fetch_by(OrganizationData, %{
organization_id: org_id,
key: @crp_id_key
})
%{
is_valid: Map.has_key?(org_data.json, crp_id)
}
end
@spec load_geographies(non_neg_integer(), map()) ::
{:ok, OrganizationData.t()} | {:error, Ecto.Changeset.t()}
defp load_geographies(org_id, geographies_config) do
ApiClient.get_csv_content(url: geographies_config["sheet_link"])
|> Enum.reduce(%{}, fn {_, row}, acc ->
region = row["Region Name"]
district = row["Proposed District"]
division = row["Proposed Division"]
mandal = row["Mandal"]
region_map = Map.get(acc, region, %{})
district_map = Map.get(region_map, district, %{})
division_map = Map.get(district_map, division, %{})
mandals = Map.get(division_map, "mandals", [])
division_map = Map.merge(division_map, %{"mandals" => mandals ++ [mandal]})
district_map = Map.put(district_map, division, division_map)
region_map = Map.put(region_map, district, district_map)
Map.put(acc, region, region_map)
end)
|> then(fn geographies_data ->
Partners.maybe_insert_organization_data(
geographies_config["database_key"],
geographies_data,
org_id
)
end)
end
@doc """
A callback function to support daily tasks for the client
in the backend.
"""
@spec daily_tasks(non_neg_integer()) :: atom()
def daily_tasks(_org_id) do
# we have added the background flows and now don't need this.
# fetch_contacts_from_farmer_group(org_id)
# |> Enum.each(&run_daily_task/1)
:ok
end
@spec get_language(non_neg_integer()) :: map()
defp get_language(contact_id) do
contact_id = Glific.parse_maybe_integer!(contact_id)
contact =
contact_id
|> Contacts.get_contact!()
|> Repo.preload([:language])
contact.language
end
@doc """
Send template from expression
"""
@spec send_template(String.t(), list()) :: binary
def send_template(uuid, variables) do
%{
uuid: uuid,
variables: variables,
expression: nil
}
|> Jason.encode!()
end
@doc """
Send media template from expression
"""
@spec send_media_template(String.t(), String.t(), non_neg_integer()) :: String.t()
def send_media_template(uuid, day, organization_id) do
{:ok, organization_data} =
Repo.fetch_by(OrganizationData, %{
organization_id: organization_id,
key: "dg_tel_crop_calendar"
})
%{
uuid: uuid,
variables: get_in(organization_data.json, [day, "variables"]),
expression: nil
}
|> Jason.encode!()
end
@doc """
Get weather report message
"""
@spec get_report_msg(map(), non_neg_integer()) :: String.t()
def get_report_msg(weather_report, organization_id) do
timelines = weather_report["data"]["timelines"]["0"]
intervals = timelines["intervals"]
Enum.reduce(intervals, "", fn interval, acc ->
acc <> parse_report(elem(interval, 1), organization_id)
end)
end
@spec parse_report(nil | maybe_improper_list | map, non_neg_integer()) :: String.t()
defp parse_report(interval, organization_id) do
{:ok, organization_data} =
Repo.fetch_by(OrganizationData, %{
organization_id: organization_id,
key: "weather_code"
})
{:ok, time, _days} = DateTime.from_iso8601(interval["startTime"])
start_time = time |> Timex.format!("{0D}/{0M}/{YYYY}")
weather_code = Integer.to_string(interval["values"]["weatherCodeFullDay"])
weather = get_in(organization_data.json, [weather_code])
"""
\n *తేదీ:* #{start_time}
\n *గరిష్ట ఉష్ణోగ్రత:* #{interval["values"]["temperatureMax"]} °C
\n *కనిష్ట ఉష్ణోగ్రత:* #{interval["values"]["temperatureMin"]} °C
\n *వాతావరణ పరిస్థితి:* #{weather}
\n
"""
end
end