defmodule DaProductAppWeb.MerchantApiController do use DaProductAppWeb, :controller require Logger alias DaProductApp.Groups.Group alias DaProductApp.Brands.Brand alias DaProductApp.Stores.Store alias DaProductApp.Repo alias DaProductApp.PosTerminals.PosTerminal alias DaProductApp.Transactions.Transaction import Ecto.Query def get_store_detail(conn, %{"merchantRefId" => code} = _params) do Logger.info("Fetching store details for merchantRefId: #{code}") query = from g in Group, where: g.code == ^code, join: b in Brand, on: b.group_id == g.id, join: s in Store, on: s.brand_id == b.id, select: %{ store_id: s.id, store_name: s.name, store_code: s.code, brand_id: b.id, brand_name: b.name, neo_merchant_id: s.neo_merchant_id, address_id: s.address_id, created_at: s.inserted_at, updated_at: s.updated_at } case Repo.all(query) do [] -> conn |> put_status(:not_found) |> json(%{ status: "error", count: 0, message: "No stores found for the provided merchantRefId", data: %{ stores: [] } }) stores -> conn |> put_status(:ok) |> json(%{ status: "success", count: length(stores), message: "#{length(stores)} store(s) retrieved successfully", data: %{ stores: stores } }) end end def get_store_detail(conn, _params) do conn |> put_status(:bad_request) |> json(%{ status: "error", count: 0, message: "merchantRefId is required", data: %{ stores: [] } }) end def get_device_detail(conn, %{"merchantRefId" => code} = _params) do Logger.info("Fetching device details for merchantRefId: #{code}") query = from g in Group, where: g.code == ^code, join: b in Brand, on: b.group_id == g.id, join: s in Store, on: s.brand_id == b.id, join: pt in PosTerminal, on: pt.store_id == s.id, select: %{ device_id: pt.id, name: pt.name, serial_number: pt.serial_number, device_type: pt.device_type, neo_terminal_id: pt.neo_terminal_id, provider_id: pt.provider_id, store: %{ store_id: s.id, store_name: s.name, store_code: s.code, brand_id: b.id, brand_name: b.name, neo_merchant_id: s.neo_merchant_id, address_id: s.address_id }, brand: %{ id: b.id, name: b.name, code: b.code }, created_at: pt.inserted_at, updated_at: pt.updated_at } case Repo.all(query) do [] -> conn |> put_status(:not_found) |> json(%{ status: "error", deviceCount: 0, message: "No devices found for the provided merchantRefId", data: %{ devices: [] } }) devices -> conn |> put_status(:ok) |> json(%{ status: "success", deviceCount: length(devices), message: "#{length(devices)} device(s) retrieved successfully", data: %{ devices: devices } }) end end def get_device_detail(conn, _params) do conn |> put_status(:bad_request) |> json(%{ status: "error", deviceCount: 0, message: "merchantRefId is required", data: %{ devices: [] } }) end def get_today_transactions(conn, %{"merchantRefId" => code} = _params) do Logger.info("Fetching today's transactions for merchantRefId: #{code}") today = Date.utc_today() today_start = DateTime.new!(today, ~T[00:00:00.000], "Etc/UTC") today_end = DateTime.new!(today, ~T[23:59:59.999], "Etc/UTC") base_query = from g in Group, where: g.code == ^code, join: b in Brand, on: b.group_id == g.id, join: s in Store, on: s.brand_id == b.id, join: pt in PosTerminal, on: pt.store_id == s.id, left_join: t in Transaction, on: fragment("CAST(? AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci = CAST(? AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci", t.device_id, pt.serial_number) and t.inserted_at >= ^today_start and t.inserted_at <= ^today_end # Get settlement amount settlement_query = from [g, b, s, pt, t] in base_query, where: not is_nil(t.id) and not is_nil(t.settlement_date_time), select: %{ total_settlement_amount: coalesce(sum(t.transaction_amount), 0) } # Get transactions with details transactions_query = from [g, b, s, pt, t] in base_query, where: not is_nil(t.id), select: %{ transaction_id: t.id, transaction_amount: t.transaction_amount, device_id: t.device_id, processing_id: t.processing_id, status: t.status, email: t.email, user_id: t.user_id, transaction_ref_number: t.transaction_ref_number, m_ref_num: t.m_ref_num, name: t.name, merchant_id: t.merchant_id, additional_data: t.additional_data, payment_reference_id: t.payment_reference_id, payload: t.payload, settlement_date_time: t.settlement_date_time, is_settled: not is_nil(t.settlement_date_time), device_details: %{ device_id: pt.id, name: pt.name, serial_number: pt.serial_number, device_type: pt.device_type, neo_terminal_id: pt.neo_terminal_id }, store_details: %{ store_id: s.id, store_name: s.name, store_code: s.code, neo_merchant_id: s.neo_merchant_id }, brand_details: %{ brand_id: b.id, brand_name: b.name, brand_code: b.code }, created_at: t.inserted_at } with settlement = %{total_settlement_amount: settlement_amount} <- Repo.one(settlement_query), transactions when is_list(transactions) <- Repo.all(transactions_query) do total_amount = Enum.reduce(transactions, Decimal.new("0"), fn tx, acc -> Decimal.add(acc, tx.transaction_amount || Decimal.new("0")) end) conn |> put_status(:ok) |> json(%{ status: "success", count: length(transactions), total_amount: total_amount, total_settlement_amount: settlement_amount, message: "#{length(transactions)} transaction(s) retrieved successfully", data: %{ transactions: transactions } }) else [] -> conn |> put_status(:not_found) |> json(%{ status: "error", count: 0, total_amount: "0.00", total_settlement_amount: "0.00", message: "No transactions found for today", data: %{ transactions: [] } }) end end def get_today_transactions(conn, _params) do conn |> put_status(:bad_request) |> json(%{ status: "error", count: 0, total_amount: "0.00", total_settlement_amount: "0.00", message: "merchantRefId is required", data: %{ transactions: [] } }) end def get_total_transactions(conn,params) do merchantRefId = params["merchantRefId"] if is_nil(merchantRefId) do conn |> put_status(:bad_request) |> json(%{ status: "error", message: "merchantRefId is required", data: %{ transactions: [] } }) else Logger.info("Fetching transactions for merchantRefId: #{merchantRefId} with params: #{inspect(params)}") {start_date, end_date} = get_date_range(params) base_query = from g in Group, where: g.code == ^merchantRefId, join: b in Brand, on: b.group_id == g.id, join: s in Store, on: s.brand_id == b.id, join: pt in PosTerminal, on: pt.store_id == s.id, left_join: t in Transaction, on: fragment("CAST(? AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci = CAST(? AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci", t.device_id, pt.serial_number) and t.inserted_at >= ^start_date and t.inserted_at <= ^end_date # Get settlement amount settlement_query = from [g, b, s, pt, t] in base_query, where: not is_nil(t.id) and not is_nil(t.settlement_date_time), select: %{ total_settlement_amount: coalesce(sum(t.transaction_amount), 0) } # Get transactions with details transactions_query = from [g, b, s, pt, t] in base_query, where: not is_nil(t.id), order_by: [desc: t.inserted_at], select: %{ transaction_id: t.id, transaction_amount: t.transaction_amount, device_id: t.device_id, processing_id: t.processing_id, status: t.status, email: t.email, transaction_ref_number: t.transaction_ref_number, m_ref_num: t.m_ref_num, merchant_id: t.merchant_id, payment_reference_id: t.payment_reference_id, settlement_date_time: t.settlement_date_time, is_settled: not is_nil(t.settlement_date_time), provider_name: t.provider_name, device_details: %{ device_id: pt.id, name: pt.name, serial_number: pt.serial_number, device_type: pt.device_type, neo_terminal_id: pt.neo_terminal_id }, store_details: %{ store_id: s.id, store_name: s.name, store_code: s.code, neo_merchant_id: s.neo_merchant_id }, brand_details: %{ brand_id: b.id, brand_name: b.name, brand_code: b.code }, created_at: t.inserted_at, transaction_type: "QR" } with settlement = %{total_settlement_amount: settlement_amount} <- Repo.one(settlement_query), transactions when is_list(transactions) <- Repo.all(transactions_query) do total_amount = Enum.reduce(transactions, Decimal.new("0"), fn tx, acc -> Decimal.add(acc, tx.transaction_amount || Decimal.new("0")) end) conn |> put_status(:ok) |> json(%{ status: "success", count: length(transactions), total_amount: total_amount, total_settlement_amount: settlement_amount, date_range: %{ start_date: DateTime.to_date(start_date), end_date: DateTime.to_date(end_date) }, message: "#{length(transactions)} transaction(s) retrieved successfully", data: %{ transactions: transactions } }) else [] -> conn |> put_status(:not_found) |> json(%{ status: "error", count: 0, total_amount: "0.00", total_settlement_amount: "0.00", date_range: %{ start_date: DateTime.to_date(start_date), end_date: DateTime.to_date(end_date) }, message: "No transactions found for the specified period", data: %{ transactions: [] } }) end end end def get_total_card_transactions(conn, params) do Logger.info("Fetching total card transactions with params: #{inspect(params)}") merchantRefId = params["merchantRefId"] if is_nil(merchantRefId) do conn |> put_status(:bad_request) |> json(%{ status: "error", message: "merchantRefId is required", data: %{ transactions: [] } }) else Logger.info("Fetching card transactions for merchantRefId: #{merchantRefId} with params: #{inspect(params)}") {start_date, end_date} = get_date_range(params) case get_store_and_brand_details(merchantRefId) do {:ok, hierarchy_details} -> # Extract terminal IDs, making sure they're not nil terminal_ids = Enum.map(hierarchy_details, & &1.terminal_id) |> Enum.reject(&is_nil/1) if Enum.empty?(terminal_ids) do conn |> put_status(:not_found) |> json(%{ status: "error", count: 0, total_amount: "0.00", total_settlement_amount: "0.00", date_range: %{ start_date: DateTime.to_date(start_date), end_date: DateTime.to_date(end_date) }, message: "No terminals found for the provided merchantRefId", data: %{ transactions: [] } }) else case get_card_transactions_from_api(terminal_ids, start_date, end_date, merchantRefId, hierarchy_details) do {:ok, transactions} -> total_amount = calculate_total_amount(transactions) settlement_amount = calculate_settlement_amount(transactions) conn |> put_status(:ok) |> json(%{ status: "success", count: length(transactions), total_amount: total_amount, total_settlement_amount: settlement_amount, date_range: %{ start_date: DateTime.to_date(start_date), end_date: DateTime.to_date(end_date) }, message: "#{length(transactions)} transaction(s) retrieved successfully", data: %{ transactions: transactions } }) {:error, reason} -> conn |> put_status(:internal_server_error) |> json(%{ status: "error", message: "Failed to fetch card transactions: #{reason}", data: %{ transactions: [] } }) end end {:error, :not_found} -> conn |> put_status(:not_found) |> json(%{ status: "error", count: 0, total_amount: "0.00", total_settlement_amount: "0.00", date_range: %{ start_date: DateTime.to_date(start_date), end_date: DateTime.to_date(end_date) }, message: "No merchant hierarchy found for the provided merchantRefId", data: %{ transactions: [] } }) end end end # Modify the get_card_transactions_from_api function signature to accept hierarchy info defp get_card_transactions_from_api(terminal_ids, start_date, end_date, merchantRefId, terminals) do base_url = "https://mercurypay.ariticapp.com/tcpayconn/getTransactionsDetails.php" # Format dates to YYYY-MM-DD formatted_start_date = DateTime.to_date(start_date) |> Date.to_iso8601() formatted_end_date = DateTime.to_date(end_date) |> Date.to_iso8601() # Join terminal IDs with comma terminal_ids_string = Enum.join(terminal_ids, ",") query_params = URI.encode_query(%{ "action" => "getAll", "start_date" => formatted_start_date, "end_date" => formatted_end_date, "merchant_id" => merchantRefId, "terminal_id" => terminal_ids_string }) url = "#{base_url}?#{query_params}" Logger.info("Fetching card transactions from URL: #{url}") case HTTPoison.get(url) do {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> Logger.info("Card transactions API response: #{inspect(body)}") case Jason.decode(body) do {:ok, decoded_response} -> case decoded_response do %{"success" => true, "data" => transactions} -> Logger.info("printing terminals: #{inspect(terminals)}") # Merge hierarchy data with each transaction enriched_transactions = Enum.map(transactions, fn transaction -> # Find matching terminal info for this transaction terminal_info = Enum.find(terminals, fn terminal -> # Log the comparison values for debugging Logger.info("Comparing terminal_id: #{terminal.terminal_id} with transaction terminal_id: #{transaction["terminal_id"]}") # Convert both values to strings for comparison String.trim(to_string(terminal.terminal_id)) == String.trim(to_string(transaction["terminal_id"])) end) Logger.info("Found terminal_info: #{inspect(terminal_info)}") if terminal_info do Map.merge(transaction, %{ "store_details" => terminal_info.store_details, "brand_details" => terminal_info.brand_details, "device_details" => terminal_info.device_details, "terminal_id" => terminal_info.terminal_id, "device_id" => terminal_info.device_id, "transaction_type" => "Card" }) else # If no terminal info found, provide empty structures Map.merge(transaction, %{ "store_details" => %{}, "brand_details" => %{}, "terminal_id" => transaction["terminal_id"], "terminal_serial" => nil }) end end) Logger.info("Successfully processed #{length(enriched_transactions)} transactions") {:ok, enriched_transactions} %{"success" => false, "message" => error_message} -> Logger.error("API returned error: #{error_message}") {:error, error_message} _ -> Logger.error("Unexpected response format: #{inspect(decoded_response)}") {:error, "Invalid response format"} end {:error, reason} -> Logger.error("Failed to decode response: #{inspect(reason)}\nBody: #{inspect(body)}") {:error, "Invalid JSON response"} end {:ok, %HTTPoison.Response{status_code: status_code, body: body}} -> Logger.error("API request failed with status code: #{status_code}, body: #{inspect(body)}") {:error, "API request failed with status #{status_code}"} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("API request failed, reason: #{inspect(reason)}") {:error, "API request failed: #{inspect(reason)}"} end end defp enrich_transactions_with_hierarchy(transactions, terminals) do terminals_map = Enum.into(terminals, %{}, & {&1.terminal_id, &1}) Enum.map(transactions, fn transaction -> # Use string key instead of atom terminal_info = Map.get(terminals_map, transaction["terminal_id"]) || %{ store_details: %{}, brand_details: %{} } # Merge the transaction with terminal info and ensure consistent key types Map.merge(transaction, %{ "store_details" => terminal_info.store_details, "brand_details" => terminal_info.brand_details, "terminal_id" => transaction["terminal_id"] }) end) end defp calculate_total_amount(transactions) do Enum.reduce(transactions, Decimal.new("0"), fn tx, acc -> amount = case tx["transaction_amount"] do nil -> Decimal.new("0") amount when is_binary(amount) -> Decimal.new(String.trim(amount)) amount -> Decimal.new(amount) end Decimal.add(acc, amount) end) end defp calculate_settlement_amount(transactions) do transactions |> Enum.filter(& &1["settlement_date"]) |> Enum.reduce(Decimal.new("0"), fn tx, acc -> amount = case tx["transaction_amount"] do nil -> Decimal.new("0") amount when is_binary(amount) -> Decimal.new(String.trim(amount)) amount -> Decimal.new(amount) end Decimal.add(acc, amount) end) end def get_merchant_hierarchy(conn, %{"merchantRefId" => code} = _params) do Logger.info("Fetching merchant hierarchy for merchantRefId: #{code}") query = from g in Group, where: g.code == ^code, join: b in Brand, on: b.group_id == g.id, join: s in Store, on: s.brand_id == b.id, left_join: pt in PosTerminal, on: pt.store_id == s.id, select: %{ group_id: g.id, group_name: g.name, group_code: g.code, brand_id: b.id, brand_name: b.name, brand_code: b.code, store_id: s.id, store_name: s.name, store_code: s.code, neo_merchant_id: s.neo_merchant_id, terminal_id: pt.id, terminal_name: pt.name, serial_number: pt.serial_number, device_type: pt.device_type, neo_terminal_id: pt.neo_terminal_id } case Repo.all(query) do [] -> conn |> put_status(:not_found) |> json(%{ status: "error", message: "No merchant chains found for the provided merchantRefId", data: %{ merchantRefId: code, chains: [] } }) results -> hierarchy = results |> Enum.reduce(%{}, fn record, acc -> brand_key = record.brand_id store_key = record.store_id terminal = if record.terminal_id do %{ id: record.terminal_id, name: record.terminal_name, serial_number: record.serial_number, device_type: record.device_type, neo_terminal_id: record.neo_terminal_id } end acc |> Map.update(brand_key, %{ brand_id: record.brand_id, brand_name: record.brand_name, brand_code: record.brand_code, stores: %{ store_key => %{ store_id: record.store_id, store_name: record.store_name, store_code: record.store_code, neo_merchant_id: record.neo_merchant_id, terminals: [terminal] |> Enum.reject(&is_nil/1) } } }, fn brand -> brand |> Map.update!(:stores, fn stores -> stores |> Map.update(store_key, %{ store_id: record.store_id, store_name: record.store_name, store_code: record.store_code, neo_merchant_id: record.neo_merchant_id, terminals: [terminal] |> Enum.reject(&is_nil/1) }, fn store -> if terminal do Map.update!(store, :terminals, &([terminal | &1])) else store end end) end) end) end) formatted_hierarchy = %{ chain_id: hd(results).group_id, chain_name: hd(results).group_name, chain_code: hd(results).group_code, brands: hierarchy |> Map.values() |> Enum.map(fn brand -> %{brand | stores: brand.stores |> Map.values() |> Enum.map(fn store -> %{store | terminals: Enum.uniq_by(store.terminals, & &1.id) } end) } end) } conn |> put_status(:ok) |> json(%{ status: "success", message: "Merchant chains retrieved successfully", data: %{ merchantRefId: code, chains: formatted_hierarchy } }) end end def get_merchant_hierarchy(conn, _params) do conn |> put_status(:bad_request) |> json(%{ status: "error", message: "merchantRefId is required", data: %{ merchantRefId: nil, chains: [] } }) end def create_group(conn, params) do Logger.info("Creating new group with params: #{inspect(params)}") case validate_required_params(params) do :ok -> changeset = Group.changeset(%Group{}, %{ name: params["name"], code: params["code"], description: params["description"], status: params["status"] || "active", phone_number: params["phone_number"], transaction_currency: "AED" }) case Repo.insert(changeset) do {:ok, group} -> conn |> put_status(:created) |> json(%{ status: "success", success: true, message: "Chain successfully created", chain_id: group.id }) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{ status: "error", success: false, message: "Failed to create chain", errors: Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) }) end {:error, missing_fields} -> conn |> put_status(:bad_request) |> json(%{ status: "error", success: false, message: "Missing required fields", errors: missing_fields }) end end def update_group(conn, params) do Logger.info("Updating group with params: #{inspect(params)}") with {:ok, id} <- validate_id(params["id"]), {:ok, update_params} <- validate_update_params(params), {:ok, group} <- get_group(id) do changeset = Group.changeset(group, %{ name: update_params["chain_name"] || group.name, code: update_params["chain_code"] || group.code, description: update_params["description"] || group.description, phone_number: update_params["phone_number"] || group.phone_number }) case Repo.update(changeset) do {:ok, _updated_group} -> conn |> put_status(:ok) |> json(%{ success: true, message: "Chain updated successfully" }) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{ success: false, message: "Failed to update chain", errors: Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) }) end else {:error, :missing_id} -> conn |> put_status(:bad_request) |> json(%{ success: false, message: "Chain ID is required" }) {:error, :no_params} -> conn |> put_status(:bad_request) |> json(%{ success: false, message: "At least one parameter is required for update" }) {:error, :not_found} -> conn |> put_status(:not_found) |> json(%{ success: false, message: "Chain not found" }) end end def create_store(conn, params) do Logger.info("Creating new store with params: #{inspect(params)}") with {:ok, chain_id} <- validate_chain_id(params["chain_id"]), {:ok, brand_id} <- validate_brand_id(params["brand_id"]), :ok <- validate_store_params(params), :ok <- validate_unique_store_code(params["store_code"]) do changeset = Store.changeset(%Store{}, %{ brand_id: brand_id, name: params["store_name"], code: params["store_code"], address_id: params["address_id"], neo_merchant_id: params["neo_merchant_id"], status: "active" }) case Repo.insert(changeset) do {:ok, store} -> conn |> put_status(:created) |> json(%{ status: "success", success: true, message: "Store added successfully", store_id: store.id, errors: nil }) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{ status: "error", success: false, message: "Failed to create store", store_id: nil, errors: Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) }) end else {:error, :chain_not_found} -> conn |> put_status(:not_found) |> json(%{ status: "error", success: false, message: "Chain not found", store_id: nil, errors: "Chain not found" }) {:error, :brand_not_found} -> conn |> put_status(:not_found) |> json(%{ status: "error", success: false, message: "Brand not found", store_id: nil, errors: "Brand not found" }) {:error, :store_code_exists} -> conn |> put_status(:conflict) |> json(%{ status: "error", success: false, message: "Store code already exists", store_id: nil, errors: "Store code already exists" }) {:error, missing_fields} -> conn |> put_status(:bad_request) |> json(%{ status: "error", success: false, message: "Missing required fields", store_id: nil, errors: "Either store_code, store_name, address_id, neo_merchant_id, chain_id or brand_id is missed", }) end end def update_store(conn, params) do Logger.info("Updating store with params: #{inspect(params)}") with {:ok, id} <- validate_id(params["id"]), {:ok, store} <- get_store(id), :ok <- validate_unique_store_code_for_update(params["store_code"], id) do changeset = Store.changeset(store, %{ name: params["store_name"] || store.name, code: params["store_code"] || store.code, neo_merchant_id: params["neo_merchant_id"] || store.neo_merchant_id }) case Repo.update(changeset) do {:ok, updated_store} -> conn |> put_status(:ok) |> json(%{ status: "success", success: true, message: "Store information updated successfully", store_id: updated_store.id, errors: nil }) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{ status: "error", success: false, message: "Failed to update store", errors: Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end), store_id: params["id"], }) end else {:error, :missing_id} -> conn |> put_status(:bad_request) |> json(%{ status: "error", success: false, message: "Store ID is required", errors: "Store ID is required", store_id: nil }) {:error, :not_found} -> conn |> put_status(:not_found) |> json(%{ status: "error", success: false, message: "Store not found", errors: "Store not found", store_id: params["id"] }) {:error, :store_code_exists} -> conn |> put_status(:conflict) |> json(%{ status: "error", success: false, message: "Store code already exists", errors: "Store code already exists", store_id: params["id"] }) end end # Private functions to handle validation defp validate_id(id) when is_nil(id), do: {:error, :missing_id} defp validate_id(id) do case Integer.parse(to_string(id)) do {id, ""} -> {:ok, id} _ -> {:error, :missing_id} end end defp validate_update_params(params) do update_fields = ["chain_name", "chain_code", "description", "phone_number"] has_updates = Enum.any?(update_fields, &Map.has_key?(params, &1)) if has_updates do {:ok, params} else {:error, :no_params} end end defp get_group(id) do case Repo.get(Group, id) do nil -> {:error, :not_found} group -> {:ok, group} end end defp get_store(id) do case Repo.get(Store, id) do nil -> {:error, :not_found} store -> {:ok, store} end end defp validate_required_params(params) do required_fields = ["name", "code"] missing_fields = Enum.filter(required_fields, fn field -> is_nil(params[field]) || String.trim(params[field] || "") == "" end) case missing_fields do [] -> :ok fields -> errors = Enum.into(fields, %{}, fn field -> {field, "#{field} is required and cannot be empty"} end) {:error, errors} end end defp validate_chain_id(chain_id) when is_nil(chain_id), do: {:error, %{chain_id: "Chain ID is required"}} defp validate_chain_id(chain_id) do case Repo.get(Group, chain_id) do nil -> {:error, :chain_not_found} _group -> {:ok, chain_id} end end defp validate_brand_id(brand_id) when is_nil(brand_id), do: {:error, %{brand_id: "Brand ID is required"}} defp validate_brand_id(brand_id) do case Repo.get(Brand, brand_id) do nil -> {:error, :brand_not_found} _brand -> {:ok, brand_id} end end defp validate_store_params(params) do required_fields = ["store_code", "store_name", "address_id", "neo_merchant_id"] missing_fields = Enum.filter(required_fields, fn field -> is_nil(params[field]) || String.trim(to_string(params[field]) || "") == "" end) case missing_fields do [] -> :ok fields -> errors = Enum.into(fields, %{}, fn field -> {field, "#{field} is required and cannot be empty"} end) {:error, errors} end end defp validate_unique_store_code(store_code) do case Repo.get_by(Store, code: store_code) do nil -> :ok _store -> {:error, :store_code_exists} end end defp validate_unique_store_code_for_update(nil, _current_store_id), do: :ok defp validate_unique_store_code_for_update(store_code, current_store_id) do case Repo.get_by(Store, code: store_code) do nil -> :ok store -> if store.id == current_store_id do :ok else {:error, :store_code_exists} end end end defp get_date_range(%{"start_date" => start_date, "end_date" => end_date}) when not is_nil(start_date) and not is_nil(end_date) and start_date != "" and end_date != "" do { parse_datetime(start_date, :start), parse_datetime(end_date, :end) } end defp get_date_range(_params) do today = Date.utc_today() start_date = Date.beginning_of_month(today) end_date = Date.end_of_month(today) { DateTime.new!(start_date, ~T[00:00:00.000], "Etc/UTC"), DateTime.new!(end_date, ~T[23:59:59.999], "Etc/UTC") } end defp parse_datetime(date_string, bound) do date = Date.from_iso8601!(date_string) time = case bound do :start -> ~T[00:00:00.000] :end -> ~T[23:59:59.999] end DateTime.new!(date, time, "Etc/UTC") end defp get_store_and_brand_details(merchantRefId) do query = from g in Group, where: g.code == ^merchantRefId, join: b in Brand, on: b.group_id == g.id, join: s in Store, on: s.brand_id == b.id, left_join: pt in PosTerminal, on: pt.store_id == s.id, select: %{ terminal_id: pt.neo_terminal_id, device_id: pt.serial_number, device_details: %{ name: pt.name, serial_number: pt.serial_number, device_type: pt.device_type, neo_terminal_id: pt.neo_terminal_id, device_id: pt.id }, store_details: %{ store_id: s.id, store_name: s.name, store_code: s.code, neo_merchant_id: s.neo_merchant_id }, brand_details: %{ brand_id: b.id, brand_name: b.name, brand_code: b.code } } case Repo.all(query) do [] -> {:error, :not_found} details -> valid_details = Enum.filter(details, & &1.terminal_id != nil) {:ok, valid_details} end end def create_group_and_brand(conn, params) do Logger.info("Creating group and brand with params: #{inspect(params)}") # Backward compatibility: support both merchant_refrence_id (typo) and merchant_reference_id (correct) params = if params["merchant_refrence_id"] && !params["merchant_reference_id"] do Map.put(params, "merchant_reference_id", params["merchant_refrence_id"]) else params end # Start a transaction to ensure both records are created or none Ecto.Multi.new() |> Ecto.Multi.insert(:group, fn _ -> Group.changeset(%Group{}, %{ code: params["merchant_reference_id"], name: params["group_name"], status: "active", description: params["group_name"], phone_number: params["phone_number"], transaction_currency: "AED", settlement_currency: "AED" }) end) |> Ecto.Multi.insert(:brand, fn %{group: group} -> Brand.changeset(%Brand{}, %{ code: params["brand_code"], name: params["brand_name"], merchant_reference_id: params["merchant_reference_id"], region: params["region"], group_id: group.id }) end) |> Repo.transaction() |> case do {:ok, %{group: group, brand: brand}} -> conn |> put_status(:created) |> json(%{ status: "success", success: true, message: "Group and Brand created successfully", data: %{ group_id: group.id, brand_id: brand.id, merchant_reference_id: params["merchant_reference_id"] } }) {:error, :group, changeset, _changes} -> conn |> put_status(:unprocessable_entity) |> json(%{ status: "error", success: false, message: "Failed to create group", errors: Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) }) {:error, :brand, changeset, _changes} -> conn |> put_status(:unprocessable_entity) |> json(%{ status: "error", success: false, message: "Failed to create brand", errors: Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) }) end end def add_mcc_code(conn, params) do Logger.info("Adding MCC code with params: #{inspect(params)}") # Backward compatibility: support both merchant_refrence_id (typo) and merchant_reference_id (correct) merchant_reference_id = params["merchant_reference_id"] || params["merchant_refrence_id"] mcc_code = params["mcc_code"] cond do is_nil(merchant_reference_id) || String.trim(merchant_reference_id) == "" -> conn |> put_status(:bad_request) |> json(%{ status: "error", success: false, message: "merchant_reference_id is required" }) is_nil(mcc_code) || String.trim(mcc_code) == "" -> conn |> put_status(:bad_request) |> json(%{ status: "error", success: false, message: "mcc_code is required" }) true -> case Repo.get_by(Group, code: merchant_reference_id) do nil -> conn |> put_status(:not_found) |> json(%{ status: "error", success: false, message: "Group not found for the provided merchant_reference_id" }) group -> changeset = Group.changeset(group, %{mcc_code: mcc_code}) case Repo.update(changeset) do {:ok, _updated_group} -> conn |> put_status(:ok) |> json(%{ status: "success", success: true, message: "MCC code added successfully" }) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{ status: "error", success: false, message: "Failed to add MCC code", errors: Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) }) end end end end end