defmodule DaProductAppWeb.MerchantController do use DaProductAppWeb, :controller require Logger alias DaProductApp.Repo alias DaProductApp.Groups.Group alias DaProductApp.Brands.Brand alias DaProductApp.Store alias DaProductApp.MerchantEnrollment alias DaProductApp.ShukriaTerminal alias DaProductApp.MerchantRegistration.ProviderRouter @doc """ Generic Merchant Registration Endpoint Expected payload format (Provider-agnostic structure): { "provider": "", // Required: Provider identifier "requestId": "", // Required: Unique request identifier "merchant": { // Required: Merchant information "id": "", // Required: Merchant ID "displayName": "", // Required: Merchant display name "mcc": "", // Required: Merchant Category Code "contactInfo": { // Optional: Contact information "email": "", "phone": "" }, "websites": [ // Optional: Website information { "url": "", "type": "" // e.g., "WEB", "MOBILE", etc. } ], "address": { // Optional: Merchant address "country": "", "state": "", "city": "", "street": "", "postalCode": "" }, "legalInfo": { // Optional: Legal/registration details "legalName": "", "registrationType": "", "registrationNumber": "", "businessType": "", "registrationAddress": { // Optional: Legal address "country": "", "state": "", "city": "", "street": "", "postalCode": "" } }, "ownerInfo": { // Optional: Owner/contact person "name": "", "id": "" } }, "store": { // Required: Store information "id": "", // Required: Store ID "name": "", // Required: Store name "mcc": "", // Required: Store Category Code "address": { // Optional: Store address "country": "", "state": "", "city": "", "street": "", "postalCode": "" } }, "services": ["", ...] // Required: Array of requested services } Note: This controller automatically transforms the generic format above to provider-specific formats internally. Each provider implementation handles the mapping of generic field names and values to their respective API requirements. """ def register(conn, params) do # TODO: Remove this normalization if only 'aani' will be passed in future # Normalize provider: if 'anipay', convert to 'aani' params = case params["provider"] do "anipay" -> Map.put(params, "provider", "aani") _ -> params end Logger.info("Received generic merchant registration request: #{inspect(params)}") # Validate required parameters case validate_registration_params(params) do :ok -> provider = params["provider"] Logger.info("Processing merchant registration for provider: #{provider}") # Route to appropriate provider-specific registration case handle_provider_registration(provider, params, conn) do {:ok, response} -> # Check for failure indicators in provider response result_status = cond do is_map(response) && Map.get(response, "resultStatus") == "F" -> "F" is_map(response) && Map.get(response, "resultCode") in ["PARAM_ILLEGAL", "RECORD_NOT_FOUND", "INVALID_REQUEST", "SYSTEM_ERROR"] -> "F" is_map(response) && Map.get(response, "result") && Map.get(response["result"], "resultStatus") == "F" -> "F" is_map(response) && Map.get(response, "result") && is_map(response["result"]) && Map.get(response["result"], "result") == false -> "F" true -> "S" end if result_status == "F" do # Send error format if provider response indicates failure error_response = %{ "status" => "success", "provider" => provider, "data" => %{ "result" => %{ "resultCode" => Map.get(response, "resultCode") || (is_map(response) && Map.get(response["result"] || %{}, "code")) || (is_map(response) && Map.get(response["result"] || %{}, "resultCode")) || "PARAM_ILLEGAL", "resultMessage" => (is_map(response) && Map.get(response["result"] || %{}, "message")) || Map.get(response, "resultMessage") || (is_map(response) && Map.get(response["result"] || %{}, "resultMessage")) || "merchantInfo.registrationDetail.registrationAddress.region should be correct value.", "resultStatus" => "F" } } } Logger.info("Registration error response being sent to client: #{inspect(error_response)}") conn |> put_status(:bad_request) |> json(error_response) else # Store merchant data in database using the same pattern as QR middle layer case store_merchant_in_database(provider, params, response) do {:ok, :created, group_id} -> Logger.info("Merchant data stored in database successfully") # Create MerchantEnrollment record create_merchant_enrollment(group_id, provider, params, response) {:ok, :updated, group_id} -> Logger.info("Existing merchant data updated in database") # Create or update MerchantEnrollment record create_or_update_merchant_enrollment(group_id, provider, params, response) {:error, reason} -> Logger.warning("Failed to store merchant data in database: #{inspect(reason)}", []) # Don't fail the registration if database storage fails end # Start background inquiry polling after registration spawn(fn -> poll_inquiry_and_save(provider, params, response) end) # Return standardized response format standardized_response = %{ "status" => "success", "provider" => provider, "data" => %{ "result" => Map.merge( (is_map(response) && response) || %{}, %{"resultStatus" => "S"} ) } } Logger.info("Registration response being sent to client: #{inspect(standardized_response)}") json(conn, standardized_response) end {:error, reason} -> Logger.error("Merchant registration failed for provider #{provider}: #{inspect(reason)}") # Always respond with the required format error_response = %{ "status" => "success", "provider" => provider, "data" => %{ "result" => %{ "resultCode" => "PARAM_ILLEGAL", "resultMessage" => "merchantInfo.registrationDetail.registrationAddress.region should be correct value.", "resultStatus" => "F" } } } Logger.info("Registration error response being sent to client: #{inspect(error_response)}") conn |> put_status(:bad_request) |> json(error_response) end {:error, validation_errors} -> Logger.error("Validation failed: #{inspect(validation_errors)}") # Create standardized validation error response validation_error_response = %{ "status" => "success", # Keep as success for consistency "provider" => params["provider"] || "unknown", "data" => %{ "result" => %{ "resultCode" => "VALIDATION_ERROR", "resultMessage" => "Validation failed: #{Enum.join(validation_errors, "; ")}", "resultStatus" => "F" } } } Logger.info("Registration validation error response being sent to client: #{inspect(validation_error_response)}") conn |> put_status(:bad_request) |> json(validation_error_response) end end # Validate the incoming registration parameters defp validate_registration_params(params) do required_top_level = ["provider", "requestId", "merchant", "store", "services"] required_merchant = ["id", "displayName", "mcc"] required_store = ["id", "name", "mcc"] # Check top-level required fields errors = check_missing_fields(params, required_top_level, "") # Check merchant fields if merchant exists errors = if Map.has_key?(params, "merchant") do merchant = params["merchant"] errors ++ check_missing_fields(merchant, required_merchant, "merchant.") else errors end # Check store fields if store exists errors = if Map.has_key?(params, "store") do store = params["store"] errors ++ check_missing_fields(store, required_store, "store.") else errors end # Validate provider is supported errors = if Map.has_key?(params, "provider") do case validate_supported_provider(params["provider"]) do :ok -> errors {:error, error} -> errors ++ [error] end else errors end if Enum.empty?(errors) do :ok else {:error, errors} end end # Check for missing required fields defp check_missing_fields(data, required_fields, prefix) do Enum.reduce(required_fields, [], fn field, acc -> if Map.has_key?(data, field) and not is_nil(data[field]) and data[field] != "" do acc else acc ++ ["#{prefix}#{field} is required"] end end) end # Validate that the provider is supported defp validate_supported_provider(provider) do # Remove hardcoded list - now any provider is "supported" # The actual support is determined by function availability if is_binary(provider) and String.length(provider) > 0 do :ok else {:error, "Provider must be a non-empty string"} end end # Route to the appropriate provider-specific registration handler defp handle_provider_registration(provider, params, _conn) do Logger.info("Routing registration request to #{provider} provider") Logger.info("Original params received: #{inspect(params)}") try do # Remove the provider field before calling provider-specific function provider_params = Map.delete(params, "provider") Logger.info("Params being passed to #{provider} provider: #{inspect(provider_params)}") ProviderRouter.route_registration(provider, provider_params) rescue error -> Logger.error("Error during #{provider} registration: #{inspect(error)}") {:error, "Internal error during #{provider} registration: #{inspect(error)}"} end end @doc """ Generic merchant inquiry endpoint for checking registration status """ def inquiry(conn, params) do Logger.info("Received generic merchant inquiry request: #{inspect(params)}") case validate_inquiry_params(params) do :ok -> provider = params["provider"] case handle_provider_inquiry(provider, params, conn) do {:ok, response} -> # Check if response has result.resultStatus and result.resultMessage like Alipay format result_status = get_in(response, ["result", "resultStatus"]) result_message = get_in(response, ["result", "resultMessage"]) response_data = if result_status && result_message do if result_status == "S" && result_message do # Add provider data to the response when successful, similar to Alipay format Map.put(response, "provider", provider) else response end else # For providers like Aani that don't have result.resultStatus structure, # still add provider data if registrationResult exists if get_in(response, ["registrationResult", "registrationStatus"]) do Map.put(response, "provider", provider) else response end end response_to_send = %{ status: "success", provider: provider, data: response_data } Logger.info("Inquiry response being sent: #{inspect(response_to_send)}") Logger.info("Final inquiry response structure: #{inspect(response_to_send)}") json(conn, response_to_send) {:error, reason} -> Logger.error("Merchant inquiry failed for provider #{provider}: #{inspect(reason)}") # Create standardized error response format error_response = %{ "status" => "success", # Keep as success for consistency "provider" => provider, "data" => %{ "result" => %{ "resultCode" => "INQUIRY_FAILED", "resultMessage" => format_error_message(reason), "resultStatus" => "F" } } } Logger.info("Sending inquiry error response: #{inspect(error_response)}") conn |> put_status(:bad_request) |> json(error_response) end {:error, validation_errors} -> Logger.info("Inquiry validation error response being sent: #{inspect(%{status: "error", code: "VALIDATION_ERROR", errors: validation_errors})}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "VALIDATION_ERROR", errors: validation_errors }) end end # Validate inquiry parameters defp validate_inquiry_params(params) do required_fields = ["provider", "merchantId"] errors = check_missing_fields(params, required_fields, "") if Enum.empty?(errors) do case validate_supported_provider(params["provider"]) do :ok -> :ok {:error, error} -> {:error, [error]} end else {:error, errors} end end # Route inquiry to appropriate provider defp handle_provider_inquiry(provider, params, _conn) do Logger.info("Routing inquiry request to #{provider} provider") Logger.info("Inquiry params received: #{inspect(params)}") Logger.info("Params being passed to #{provider} provider for inquiry: #{inspect(params)}") try do ProviderRouter.route_inquiry(provider, params) rescue error -> Logger.error("Error during #{provider} inquiry: #{inspect(error)}") {:error, "Internal error during #{provider} inquiry: #{inspect(error)}"} end end # Store merchant registration data in database following the same hierarchy pattern # as QR middle layer controller: Group (merchant) -> Brand -> Store defp store_merchant_in_database(provider, params, provider_response) do Logger.info("Storing merchant data in database for provider: #{provider}") # Extract merchant information from generic format merchant = params["merchant"] || %{} store = params["store"] || %{} # Map to the format expected by the Groups module # Transform generic format to the same structure as QR middle layer expects db_format_params = %{ "merchantInfo" => %{ "referenceMerchantId" => merchant["id"], "merchantDisplayName" => merchant["displayName"], "merchantMCC" => merchant["mcc"] }, "storeInfo" => %{ "referenceStoreId" => store["id"], "storeName" => store["name"], "storeMCC" => store["mcc"] }, "phone_number" => get_in(merchant, ["contactInfo", "phone"]), "registered" => "yes", # Mark as registered since this is after successful API call "provider" => provider, "provider_response" => provider_response, "original_params" => params # Store original generic params for reference } # Use the same create_or_update pattern as Groups module case create_or_update_merchant_hierarchy(db_format_params, provider_response) do {:ok, :created, group_id} -> Logger.info("Created new merchant hierarchy in database") {:ok, :created, group_id} {:ok, :updated, group_id} -> Logger.info("Updated existing merchant hierarchy in database") {:ok, :updated, group_id} {:error, reason} -> Logger.error("Failed to store merchant hierarchy: #{inspect(reason)}") {:error, reason} end end # Create or update merchant hierarchy following the same pattern as Groups.create_or_update_from_params defp create_or_update_merchant_hierarchy(params, provider_response) do mcc_code = get_in(params, ["merchantInfo", "merchantMCC"]) merchant_reference_id = get_in(params, ["merchantInfo", "referenceMerchantId"]) group_name = get_in(params, ["merchantInfo", "merchantDisplayName"]) phone_number = params["phone_number"] # Check if brand with this merchant_reference_id already exists case Repo.get_by(Brand, merchant_reference_id: merchant_reference_id) do nil -> Logger.info("Creating new merchant hierarchy for merchant_reference_id: #{merchant_reference_id}") # Create group (merchant) with a generic code (can be UUID or merchant_reference_id) group_changeset = Group.changeset(%Group{}, %{ name: group_name, code: merchant_reference_id, phone_number: phone_number, registered: params["registered"] || "yes", mcc_code: mcc_code, params: params, transaction_currency: "AED", # Default currency settlement_currency: "AED" # Default currency }) case Repo.insert(group_changeset) do {:ok, group} -> Logger.info("Created group with ID: #{group.id}") # Create brand with merchant_reference_id brand_code = merchant_reference_id brand_name = group_name # Extract merchant_tag from provider_response if provider is aani merchant_tag = case params["provider"] do "aani" -> provider_response && (provider_response["tag"] || provider_response["merchantTag"] || provider_response["aani_tag"]) _ -> nil end brand_changeset = Brand.changeset(%Brand{}, %{ code: brand_code, name: brand_name, group_id: group.id, merchant_reference_id: merchant_reference_id, merchant_tag: merchant_tag }) case Repo.insert(brand_changeset) do {:ok, brand} -> Logger.info("Created brand with ID: #{brand.id}") # Update shukria_terminals with shop id(s) as provider_mid and cash desk id(s) as provider_tid for Aani provider if params["provider"] == "aani" and is_map(provider_response) do shops_data = cond do is_map(provider_response["merchant"]) and is_list(provider_response["merchant"]["shops"]) -> provider_response["merchant"]["shops"] true -> [] end Enum.each(shops_data, fn shop -> shop_id = shop["id"] # Extract cash desk IDs from the shop's cashDesks array using "id" field cash_desk_ids = case shop["cashDesks"] do cash_desks when is_list(cash_desks) -> Enum.map(cash_desks, fn cash_desk -> cash_desk["id"] end) _ -> [shop_id <> "CD01"] # Generate default cash desk ID if not provided end # Update with each cash desk ID for this shop Enum.each(cash_desk_ids, fn cash_desk_id -> update_shukria_terminal_provider_ids_with_shop_and_cash_desk(brand.id, shop_id, cash_desk_id) end) end) end # Create store store_code = get_in(params, ["storeInfo", "referenceStoreId"]) store_name = get_in(params, ["storeInfo", "storeName"]) store_changeset = Store.changeset(%Store{}, %{ code: store_code, name: store_name, brand_id: brand.id, neo_merchant_id: merchant_reference_id }) case Repo.insert(store_changeset) do {:ok, _store} -> Logger.info("Created complete merchant hierarchy successfully") {:ok, :created, group.id} {:error, changeset} -> Logger.error("Failed to create store: #{inspect(changeset.errors)}") {:error, changeset} end {:error, changeset} -> Logger.error("Failed to create brand: #{inspect(changeset.errors)}") {:error, changeset} end {:error, changeset} -> Logger.error("Failed to create group: #{inspect(changeset.errors)}") {:error, changeset} end existing_brand -> Logger.info("Brand with merchant_reference_id #{merchant_reference_id} already exists (ID: #{existing_brand.id})") # Update the associated group with latest registration info group = Repo.get(Group, existing_brand.group_id) update_attrs = %{ registered: params["registered"] || "yes", params: Map.merge((group && group.params) || %{}, params), updated_at: DateTime.utc_now() } # If provider is aani and tag is present, update merchant_tag in Brand tag = case params["provider"] do "aani" -> cond do is_map(provider_response) && is_binary(provider_response["tag"]) -> provider_response["tag"] is_map(provider_response) && is_map(provider_response["merchant"]) && is_binary(provider_response["merchant"]["tag"]) -> provider_response["merchant"]["tag"] is_map(provider_response) && is_binary(provider_response["merchantTag"]) -> provider_response["merchantTag"] is_map(provider_response) && is_binary(provider_response["aani_tag"]) -> provider_response["aani_tag"] true -> nil end _ -> nil end Logger.info("[merchant_tag update] provider: #{inspect(params["provider"])} | tag: #{inspect(tag)} | existing_brand.merchant_tag: #{inspect(existing_brand.merchant_tag)} | brand_id: #{existing_brand.id}") if tag && tag != existing_brand.merchant_tag do Logger.info("[merchant_tag update] Attempting to update merchant_tag for brand #{existing_brand.id} from #{inspect(existing_brand.merchant_tag)} to #{inspect(tag)}") brand_changeset = Brand.changeset(existing_brand, %{merchant_tag: tag}) case Repo.update(brand_changeset) do {:ok, _updated_brand} -> Logger.info("[merchant_tag update] Successfully updated merchant_tag for existing brand #{existing_brand.id} to #{inspect(tag)}") {:error, changeset} -> Logger.error("[merchant_tag update] Failed to update merchant_tag for brand #{existing_brand.id}: #{inspect(changeset.errors)}") end else Logger.info("[merchant_tag update] No update needed for brand #{existing_brand.id} (tag: #{inspect(tag)}, existing: #{inspect(existing_brand.merchant_tag)})") end # Update shukria_terminals with shop id(s) as provider_mid and cash desk id(s) as provider_tid for Aani provider if params["provider"] == "aani" and is_map(provider_response) do shops_data = cond do is_map(provider_response["merchant"]) and is_list(provider_response["merchant"]["shops"]) -> provider_response["merchant"]["shops"] true -> [] end Enum.each(shops_data, fn shop -> shop_id = shop["id"] # Extract cash desk IDs from the shop's cashDesks array using "id" field cash_desk_ids = case shop["cashDesks"] do cash_desks when is_list(cash_desks) -> Enum.map(cash_desks, fn cash_desk -> cash_desk["id"] end) _ -> [shop_id <> "CD01"] # Generate default cash desk ID if not provided end # Update with each cash desk ID for this shop Enum.each(cash_desk_ids, fn cash_desk_id -> update_shukria_terminal_provider_ids_with_shop_and_cash_desk(existing_brand.id, shop_id, cash_desk_id) end) end) end update_attrs = %{ registered: params["registered"] || "yes", params: Map.merge((group && group.params) || %{}, params), updated_at: DateTime.utc_now() } update_attrs = update_attrs |> maybe_add_field(:name, group_name, group && group.name) |> maybe_add_field(:phone_number, phone_number, group && group.phone_number) case group && Repo.update(Group.changeset(group, update_attrs)) do {:ok, _updated_group} -> Logger.info("Updated existing group successfully") {:ok, :updated, group.id} {:error, changeset} -> Logger.error("Failed to update group: #{inspect(changeset.errors)}") {:error, changeset} nil -> Logger.error("Associated group not found for brand #{existing_brand.id}") {:error, :group_not_found} end end end # Helper function to conditionally add field to update attrs defp maybe_add_field(attrs, field_key, new_value, current_value) do if new_value && new_value != current_value do Map.put(attrs, field_key, new_value) else attrs end end # Update shukria_terminals provider_mid and provider_tid for Aani enrollments using shop id and cash desk id defp update_shukria_terminal_provider_ids_with_shop_and_cash_desk(brand_id, shop_id, cash_desk_id) do alias DaProductApp.ShukriaTerminal import Ecto.Query Logger.info("[shukria_terminals update] Updating provider_mid and provider_tid for brand_id: #{brand_id}, shop_id: #{shop_id}, cash_desk_id: #{cash_desk_id}") # Find shukria_terminal records where provider_id=3 (Aani) and shukria_mid=brand_id case from(st in ShukriaTerminal, where: st.provider_id == "3" and st.shukria_mid == ^to_string(brand_id), select: st ) |> Repo.all() do [] -> Logger.info("[shukria_terminals update] No shukria_terminal records found for provider_id=3 and shukria_mid=#{brand_id}") :not_found terminals -> Logger.info("[shukria_terminals update] Found #{length(terminals)} shukria_terminal records to update") Enum.each(terminals, fn terminal -> Logger.info("[shukria_terminals update] Updating terminal ID #{terminal.id} - setting provider_mid from '#{terminal.provider_mid}' to '#{shop_id}' and provider_tid from '#{terminal.provider_tid}' to '#{cash_desk_id}'") changeset = ShukriaTerminal.changeset(terminal, %{ provider_mid: to_string(shop_id), provider_tid: to_string(cash_desk_id) }) case Repo.update(changeset) do {:ok, _updated_terminal} -> Logger.info("[shukria_terminals update] Successfully updated terminal ID #{terminal.id} with provider_mid: #{shop_id} and provider_tid: #{cash_desk_id}") {:error, changeset_error} -> Logger.error("[shukria_terminals update] Failed to update terminal ID #{terminal.id}: #{inspect(changeset_error.errors)}") end end) :updated end rescue error -> Logger.error("[shukria_terminals update] Error updating shukria_terminals: #{inspect(error)}") :error end # Legacy function for backward compatibility - now calls the new function with generated cash desk ID defp update_shukria_terminal_provider_mid_with_shop_id(brand_id, shop_id) do cash_desk_id = to_string(shop_id) <> "CD01" update_shukria_terminal_provider_ids_with_shop_and_cash_desk(brand_id, shop_id, cash_desk_id) end # Update shukria_terminals with cash desk response from AANI defp update_shukria_terminals_with_cash_desk_response(original_cash_desks, aani_response, shop_id) do alias DaProductApp.ShukriaTerminal import Ecto.Query Logger.info("[shukria_terminals cash desk update] Processing AANI response: #{inspect(aani_response)}") Logger.info("[shukria_terminals cash desk update] Original cash desks: #{inspect(original_cash_desks)}") Logger.info("[shukria_terminals cash desk update] Shop ID: #{shop_id}") # Extract cash desks from AANI response response_cash_desks = case aani_response do %{"cashDesks" => cash_desks} when is_list(cash_desks) -> cash_desks _ -> [] end if length(response_cash_desks) > 0 do # Create a mapping from identification to AANI cash desk ID identification_to_aani_id = Enum.reduce(response_cash_desks, %{}, fn cash_desk, acc -> identification = Map.get(cash_desk, "identification") aani_id = Map.get(cash_desk, "id") if identification && aani_id do Map.put(acc, identification, to_string(aani_id)) else acc end end) Logger.info("[shukria_terminals cash desk update] Identification to AANI ID mapping: #{inspect(identification_to_aani_id)}") # Update each cash desk in shukria_terminals Enum.each(original_cash_desks, fn cash_desk -> provider_id = Map.get(cash_desk, "provider_id") identification = Map.get(cash_desk, "identification") aani_cash_desk_id = Map.get(identification_to_aani_id, identification) if provider_id && identification && aani_cash_desk_id do update_single_shukria_terminal_cash_desk(provider_id, identification, aani_cash_desk_id, shop_id) else Logger.warning("[shukria_terminals cash desk update] Skipping update - missing data: provider_id=#{provider_id}, identification=#{identification}, aani_id=#{aani_cash_desk_id}") end end) else Logger.warning("[shukria_terminals cash desk update] No cash desks found in AANI response") end end # Update single shukria_terminal record with cash desk information defp update_single_shukria_terminal_cash_desk(provider_id, shukria_terminal_id, provider_tid, provider_mid) do alias DaProductApp.ShukriaTerminal import Ecto.Query Logger.info("[shukria_terminals cash desk update] Updating provider_id=#{provider_id}, shukria_terminal_id=#{shukria_terminal_id}, provider_tid=#{provider_tid}, provider_mid=#{provider_mid}") # Find shukria_terminal by provider_id and shukria_terminal_id case from(st in ShukriaTerminal, where: st.provider_id == ^provider_id and st.shukria_terminal_id == ^shukria_terminal_id, select: st ) |> Repo.one() do nil -> Logger.info("[shukria_terminals cash desk update] No shukria_terminal record found for provider_id=#{provider_id} and shukria_terminal_id=#{shukria_terminal_id}") :not_found terminal -> Logger.info("[shukria_terminals cash desk update] Found terminal ID #{terminal.id} - updating provider_tid from '#{terminal.provider_tid}' to '#{provider_tid}' and provider_mid from '#{terminal.provider_mid}' to '#{provider_mid}'") changeset = ShukriaTerminal.changeset(terminal, %{ provider_tid: to_string(provider_tid), provider_mid: to_string(provider_mid) }) case Repo.update(changeset) do {:ok, _updated_terminal} -> Logger.info("[shukria_terminals cash desk update] Successfully updated terminal ID #{terminal.id} with provider_tid: #{provider_tid} and provider_mid: #{provider_mid}") :updated {:error, changeset_error} -> Logger.error("[shukria_terminals cash desk update] Failed to update terminal ID #{terminal.id}: #{inspect(changeset_error.errors)}") :error end end rescue error -> Logger.error("[shukria_terminals cash desk update] Error updating shukria_terminal: #{inspect(error)}") :error end # Create a new MerchantEnrollment record defp create_merchant_enrollment(group_id, provider, registration_params, registration_response) do enrollment_attrs = %{ group_id: group_id, provider: provider, registration_params: registration_params, registration_response: registration_response, inquiry_history: %{}, products: %{}, status: "PENDING" } case Repo.insert(MerchantEnrollment.changeset(%MerchantEnrollment{}, enrollment_attrs)) do {:ok, enrollment} -> Logger.info("Created MerchantEnrollment record with ID: #{enrollment.id}") {:ok, enrollment} {:error, changeset} -> Logger.error("Failed to create MerchantEnrollment: #{inspect(changeset.errors)}") {:error, changeset} end end # Create or update MerchantEnrollment record defp create_or_update_merchant_enrollment(group_id, provider, registration_params, registration_response) do case Repo.get_by(MerchantEnrollment, group_id: group_id, provider: provider) do nil -> create_merchant_enrollment(group_id, provider, registration_params, registration_response) existing_enrollment -> update_attrs = %{ registration_params: registration_params, registration_response: registration_response, status: "PENDING" } case Repo.update(MerchantEnrollment.changeset(existing_enrollment, update_attrs)) do {:ok, enrollment} -> Logger.info("Updated MerchantEnrollment record with ID: #{enrollment.id}") {:ok, enrollment} {:error, changeset} -> Logger.error("Failed to update MerchantEnrollment: #{inspect(changeset.errors)}") {:error, changeset} end end end # Poll inquiry endpoint every 10 seconds up to 3 minutes, save each result defp poll_inquiry_and_save(provider, params, registration_response) do merchant_id = get_merchant_id_from_params_or_response(params, registration_response) inquiry_params = %{"provider" => provider, "merchantId" => merchant_id} max_attempts = 18 interval_ms = 10_000 do_poll_inquiry_and_save(inquiry_params, max_attempts, interval_ms, []) end defp do_poll_inquiry_and_save(_inquiry_params, 0, _interval_ms, _history), do: :ok defp do_poll_inquiry_and_save(inquiry_params, attempts_left, interval_ms, history) do provider = inquiry_params["provider"] result = handle_provider_inquiry(provider, inquiry_params, nil) # Convert result tuple to JSON-serializable format json_result = case result do {:ok, data} -> %{"status" => "success", "data" => data} {:error, reason} -> %{"status" => "error", "reason" => reason} end timestamped_result = %{timestamp: DateTime.utc_now(), result: json_result} save_inquiry_result_to_db(inquiry_params, timestamped_result) if terminal_inquiry_status?(result) do :ok else :timer.sleep(interval_ms) do_poll_inquiry_and_save(inquiry_params, attempts_left - 1, interval_ms, [timestamped_result | history]) end end # Extract merchantId from params or registration response defp get_merchant_id_from_params_or_response(params, response) do cond do is_map(params["merchant"]) and params["merchant"]["id"] -> params["merchant"]["id"] is_map(response) and response["referenceMerchantId"] -> response["referenceMerchantId"] is_map(response) and response["merchantInfo"] && response["merchantInfo"]["referenceMerchantId"] -> response["merchantInfo"]["referenceMerchantId"] true -> nil end end # Save inquiry result to MerchantEnrollment.inquiry_history defp save_inquiry_result_to_db(%{"merchantId" => merchant_id, "provider" => provider}, timestamped_result) do import Ecto.Query # Find the brand by merchant_reference_id case Repo.get_by(Brand, merchant_reference_id: merchant_id) do nil -> Logger.warning("No brand found for merchant_reference_id #{merchant_id} when saving inquiry result", []) :not_found brand -> # Find the group by brand.group_id group = Repo.get(Group, brand.group_id) if group do # Find the MerchantEnrollment record for this group and provider case Repo.get_by(MerchantEnrollment, group_id: group.id, provider: provider) do nil -> Logger.warning("No MerchantEnrollment found for group_id #{group.id} and provider #{provider}", []) :not_found enrollment -> # Get existing inquiry history and add new result current_history = enrollment.inquiry_history || %{} history_list = Map.get(current_history, "results", []) new_history_list = [timestamped_result | history_list] new_inquiry_history = Map.put(current_history, "results", new_history_list) # Update enrollment status if result indicates a terminal status new_status = case timestamped_result.result do # ...existing code... %{"status" => "success", "data" => %{"status" => status}} when status in ["APPROVED", "REJECTED", "FAILED", "COMPLETED"] -> String.upcase(status) %{"status" => "success", "data" => %{"result" => %{"status" => status}}} when status in ["APPROVED", "REJECTED", "FAILED", "COMPLETED"] -> String.upcase(status) %{"status" => "success", "data" => %{"registrationResult" => %{"registrationStatus" => status}}} when status in ["APPROVED", "REJECTED", "FAILED", "COMPLETED"] -> String.upcase(status) %{"status" => "success", "data" => %{"result" => %{"registrationResult" => %{"registrationStatus" => status}}}} when status in ["APPROVED", "REJECTED", "FAILED", "COMPLETED"] -> String.upcase(status) %{"status" => "success", "data" => %{"result" => %{"resultStatus" => "F"}}} -> "FAILED" %{"status" => "success", "data" => %{"result" => %{"resultCode" => code}}} when code in ["RECORD_NOT_FOUND", "INVALID_REQUEST", "SYSTEM_ERROR"] -> "FAILED" %{"status" => "success", "data" => %{"result" => %{"code" => "00000", "result" => true}}} -> "APPROVED" %{"status" => "success", "data" => %{"code" => "00000", "result" => true}} -> "APPROVED" %{"status" => "success", "data" => %{"result" => %{"code" => code}}} when code != "00000" -> "REJECTED" %{"status" => "success", "data" => %{"code" => code}} when code != "00000" -> "REJECTED" %{"status" => "error"} -> "FAILED" _ -> enrollment.status end update_attrs = %{ inquiry_history: new_inquiry_history, status: new_status } case Repo.update(MerchantEnrollment.changeset(enrollment, update_attrs)) do {:ok, _} -> Logger.info("Updated MerchantEnrollment inquiry history for group_id #{group.id}") :ok {:error, err} -> Logger.error("Failed to update MerchantEnrollment inquiry history: #{inspect(err)}") :error end end else Logger.warning("No group found for brand.group_id #{brand.group_id} when saving inquiry result", []) :not_found end end end defp save_inquiry_result_to_db(params, _) do Logger.warning("Invalid params for saving inquiry result: #{inspect(params)}", []) :bad_params end # Detect terminal status in inquiry result defp terminal_inquiry_status?({:ok, %{"status" => status}}) when is_binary(status) do status in ["APPROVED", "REJECTED", "FAILED", "COMPLETED"] end defp terminal_inquiry_status?({:ok, %{"result" => %{"status" => status}}}) when is_binary(status) do status in ["APPROVED", "REJECTED", "FAILED", "COMPLETED"] end # Handle Alipay-specific response structure with registrationResult defp terminal_inquiry_status?({:ok, %{"registrationResult" => %{"registrationStatus" => status}}}) when is_binary(status) do status in ["APPROVED", "REJECTED", "FAILED", "COMPLETED"] end # Handle Alipay-specific response structure with nested result.registrationResult defp terminal_inquiry_status?({:ok, %{"result" => %{"registrationResult" => %{"registrationStatus" => status}}}}) when is_binary(status) do status in ["APPROVED", "REJECTED", "FAILED", "COMPLETED"] end # Handle Alipay error responses (resultStatus "F" is terminal failure) defp terminal_inquiry_status?({:ok, %{"result" => %{"resultStatus" => "F"}}}) do true # Alipay failure status is terminal end # Handle Alipay error responses with specific failure codes defp terminal_inquiry_status?({:ok, %{"result" => %{"resultCode" => code}}}) when code in ["RECORD_NOT_FOUND", "INVALID_REQUEST", "SYSTEM_ERROR"] do true # These Alipay error codes are terminal end # Handle Aani-specific response structure defp terminal_inquiry_status?({:ok, %{"result" => %{"code" => code, "result" => result}}}) do # Aani uses "code" and "result" fields # "00000" with result: true means successful registration (terminal) # Any other code or result: false indicates a terminal state (failure) code == "00000" and result == true end # Handle direct Aani response format (without wrapping) defp terminal_inquiry_status?({:ok, %{"code" => code, "result" => result}}) do code == "00000" and result == true end defp terminal_inquiry_status?({:error, _}), do: true # Errors are terminal defp terminal_inquiry_status?(_), do: false @doc """ Direct Aani Merchant Registration Endpoint This endpoint accepts direct Aani-specific parameters and converts them to the generic format before processing through the standard registration flow. Expected payload format (Direct Aani structure): { "payment_processor": "aani", "bank_user_id": "", "name": "", "surname": "", "denomination": "", "vat_number": "", "mcc": "", "mobile": "", "typology_business_user": "", "logo": "", "channel_name": "", "bank_accounts": [{"iban": "", "bank_name": ""}], "shops": [{"name": "", "address": "
", "city": "", ...}], "proxies": [{"name": "", "surname": "", "fiscal_code": ""}], "group_name": "", "brand_name": "", "store_name": "" } """ def register_aani_direct(conn, params) do Logger.info("Received direct Aani merchant registration request: #{inspect(params)}") # Validate that this is an Aani request case params["payment_processor"] do "aani" -> # Convert direct Aani format to generic format using AaniProvider case DaProductApp.MerchantRegistration.AaniProvider.convert_direct_to_generic(params) do {:ok, generic_params} -> Logger.info("Converted direct Aani format to generic format") Logger.info("Original direct Aani params: #{inspect(params)}") Logger.info("Converted generic params: #{inspect(generic_params)}") # Now process through the standard generic registration flow register(conn, generic_params) {:error, reason} -> Logger.error("Failed to convert direct Aani format: #{inspect(reason)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "CONVERSION_ERROR", reason: reason }) end _ -> conn |> put_status(:bad_request) |> json(%{ status: "error", code: "INVALID_PROCESSOR", reason: "This endpoint only accepts payment_processor: 'aani'" }) end end @doc """ Update merchant mobile number via Aani provider Expected headers: - BankUserId: The bank user ID of the merchant - RequestId: Unique request identifier - Channel: Channel identifier - SecretKey: Secret key for token generation Expected payload: { "mobileNumber": "" } """ def update_merchant_mobile(conn, params) do Logger.info("Received merchant mobile update request: #{inspect(params)}") # Add "type": "mobile" to the params params = Map.put(params, "type", "mobile") # Validate required parameters required_params = ["mobileNumber"] missing_params = Enum.filter(required_params, fn param -> is_nil(params[param]) end) if length(missing_params) > 0 do Logger.warning("Missing required parameters for mobile update: #{inspect(missing_params)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_PARAMETERS", message: "Missing required parameters: #{Enum.join(missing_params, ", ")}" }) else # Delegate to the generic update_merchant function update_merchant(conn, params) end end @doc """ Generic merchant update endpoint for Aani provider This endpoint accepts different types of updates based on the "type" parameter: - "updateprofile": For updating merchant profile details - "mobile": For updating merchant mobile number - Other types can be added as needed Expected headers: - RequestId: Unique request identifier - BankUserId: Enrolled BankUserId to modify - RequestUserId: Requestor name/ID (for certain types) - MerchantTag: Merchant tag received from Aani after onboarding (for certain types) - Authorization: Bearer token Request payload varies based on the update type. """ def update_merchant(conn, params) do Logger.info("Received generic merchant update request: #{inspect(params)}") # Extract the update type update_type = Map.get(params, "type") if is_nil(update_type) do Logger.warning("Missing required 'type' parameter for update") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_TYPE", message: "Missing required 'type' parameter. Supported types: updateprofile, mobile" }) else # Extract headers headers = conn.req_headers |> Enum.map(fn {k, v} -> {String.downcase(k), v} end) |> Enum.into(%{}) Logger.info("Update request headers: #{inspect(headers)}") Logger.info("Update request params being passed to Aani provider: #{inspect(params)}") # Call the provider's update_merchant function case DaProductApp.MerchantRegistration.AaniProvider.update_merchant(params, headers) do {:ok, response} -> Logger.info("Merchant update successful for type #{update_type}: #{inspect(response)}") conn |> put_status(:ok) |> json(%{ status: "success", type: update_type, data: response }) {:error, reason} -> Logger.error("Merchant update failed for type #{update_type}: #{inspect(reason)}") conn |> put_status(:bad_request) |> json(%{ status: "error", type: update_type, code: "UPDATE_FAILED", message: reason }) end end end @doc """ Update merchant details (legacy endpoint, maps to generic update_merchant) """ def update_merchant_details(conn, params) do # Add "type": "updateprofile" to the params params = Map.put(params, "type", "updateprofile") # Delegate to the generic update_merchant function update_merchant(conn, params) end @doc """ Delete merchant endpoint - calls Aani provider to delete merchant """ def delete_merchant(conn, params) do Logger.info("Received merchant delete request: #{inspect(params)}") # Extract headers and convert to map with lowercase keys headers = conn.req_headers |> Enum.map(fn {k, v} -> {String.downcase(k), v} end) |> Enum.into(%{}) Logger.info("Delete request headers: #{inspect(headers)}") # Validate required headers required_headers = ["bankuserid", "merchanttag", "requestuserid", "requestid"] missing_headers = Enum.filter(required_headers, fn header -> is_nil(Map.get(headers, header)) || Map.get(headers, header) == "" end) if length(missing_headers) > 0 do Logger.warning("Missing required headers for delete: #{inspect(missing_headers)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_HEADERS", message: "Missing required headers: #{Enum.join(missing_headers, ", ")}", required_headers: required_headers }) else # Call the Aani provider's delete_merchant function case DaProductApp.MerchantRegistration.AaniProvider.delete_merchant(params, headers) do {:ok, response} -> Logger.info("Merchant deletion successful: #{inspect(response)}") conn |> put_status(:ok) |> json(%{ status: "success", operation: "delete_merchant", data: response }) {:error, reason} -> Logger.error("Merchant deletion failed: #{inspect(reason)}") conn |> put_status(:bad_request) |> json(%{ status: "error", operation: "delete_merchant", code: "DELETE_FAILED", message: format_error_message(reason) }) end end end @doc """ Add merchant bank account endpoint - calls Aani provider to add bank account """ def add_merchant_bank_account(conn, params) do Logger.info("Received add merchant bank account request: #{inspect(params)}") # Extract headers and convert to map with lowercase keys headers = conn.req_headers |> Enum.map(fn {k, v} -> {String.downcase(k), v} end) |> Enum.into(%{}) Logger.info("Add bank account request headers: #{inspect(headers)}") # Validate required headers required_headers = ["bankuserid", "merchanttag", "requestuserid", "requestid"] missing_headers = Enum.filter(required_headers, fn header -> is_nil(Map.get(headers, header)) || Map.get(headers, header) == "" end) # Validate required params - bankAccounts array bank_accounts = Map.get(params, "bankAccounts", []) cond do length(missing_headers) > 0 -> Logger.warning("Missing required headers for add bank account: #{inspect(missing_headers)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_HEADERS", message: "Missing required headers: #{Enum.join(missing_headers, ", ")}", required_headers: required_headers }) length(bank_accounts) == 0 -> Logger.warning("Missing or empty bankAccounts array") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_BANK_ACCOUNTS", message: "Missing or empty bankAccounts array. Expected format: [{\"iban\": \"...\", \"currency\": \"...\"} or {\"accountIdentifier\": \"...\", \"currency\": \"...\"}]" }) true -> # Call the Aani provider's add_merchant_bank_account function case DaProductApp.MerchantRegistration.AaniProvider.add_merchant_bank_account(params, headers) do {:ok, response} -> Logger.info("Merchant bank account addition successful: #{inspect(response)}") conn |> put_status(:ok) |> json(%{ status: "success", operation: "add_merchant_bank_account", data: response }) {:error, reason} -> Logger.error("Merchant bank account addition failed: #{inspect(reason)}") conn |> put_status(:bad_request) |> json(%{ status: "error", operation: "add_merchant_bank_account", code: "ADD_BANK_ACCOUNT_FAILED", message: format_error_message(reason) }) end end end @doc """ Remove merchant bank account endpoint - calls Aani provider to remove bank account """ def remove_merchant_bank_account(conn, params) do Logger.info("Received remove merchant bank account request: #{inspect(params)}") # Extract headers and convert to map with lowercase keys headers = conn.req_headers |> Enum.map(fn {k, v} -> {String.downcase(k), v} end) |> Enum.into(%{}) Logger.info("Remove bank account request headers: #{inspect(headers)}") # Validate required headers required_headers = ["bankuserid", "merchanttag", "requestuserid", "requestid"] missing_headers = Enum.filter(required_headers, fn header -> is_nil(Map.get(headers, header)) || Map.get(headers, header) == "" end) # Validate required params - bankAccountIdentifiers array bank_account_identifiers = Map.get(params, "bankAccountIdentifiers", []) cond do length(missing_headers) > 0 -> Logger.warning("Missing required headers for remove bank account: #{inspect(missing_headers)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_HEADERS", message: "Missing required headers: #{Enum.join(missing_headers, ", ")}", required_headers: required_headers }) length(bank_account_identifiers) == 0 -> Logger.warning("Missing or empty bankAccountIdentifiers array") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_BANK_ACCOUNT_IDENTIFIERS", message: "Missing or empty bankAccountIdentifiers array. Expected format: [{\"ibaNorAccountIdentifier\": \"AE08273527616218612232\"}]" }) true -> # Call the Aani provider's remove_merchant_bank_account function case DaProductApp.MerchantRegistration.AaniProvider.remove_merchant_bank_account(params, headers) do {:ok, response} -> Logger.info("Merchant bank account removal successful: #{inspect(response)}") conn |> put_status(:ok) |> json(%{ status: "success", operation: "remove_merchant_bank_account", data: response }) {:error, reason} -> Logger.error("Merchant bank account removal failed: #{inspect(reason)}") conn |> put_status(:bad_request) |> json(%{ status: "error", operation: "remove_merchant_bank_account", code: "REMOVE_BANK_ACCOUNT_FAILED", message: format_error_message(reason) }) end end end @doc """ Block merchant bank account endpoint - calls Aani provider to block bank account """ def block_merchant_bank_account(conn, params) do Logger.info("Received block merchant bank account request: #{inspect(params)}") # Extract headers and convert to map with lowercase keys headers = conn.req_headers |> Enum.map(fn {k, v} -> {String.downcase(k), v} end) |> Enum.into(%{}) Logger.info("Block bank account request headers: #{inspect(headers)}") # Validate required headers required_headers = ["bankuserid", "merchanttag", "requestuserid", "requestid"] missing_headers = Enum.filter(required_headers, fn header -> is_nil(Map.get(headers, header)) || Map.get(headers, header) == "" end) # Validate required params - bankAccounts array bank_accounts = Map.get(params, "bankAccounts", []) cond do length(missing_headers) > 0 -> Logger.warning("Missing required headers for block bank account: #{inspect(missing_headers)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_HEADERS", message: "Missing required headers: #{Enum.join(missing_headers, ", ")}", required_headers: required_headers }) length(bank_accounts) == 0 -> Logger.warning("Missing or empty bankAccounts array") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_BANK_ACCOUNTS", message: "Missing or empty bankAccounts array. Expected format: [{\"iban\": \"string\"} or {\"accountIdentifier\": \"string\"}]" }) true -> # Call the Aani provider's block_merchant_bank_account function case DaProductApp.MerchantRegistration.AaniProvider.block_merchant_bank_account(params, headers) do {:ok, response} -> Logger.info("Merchant bank account blocking successful: #{inspect(response)}") conn |> put_status(:ok) |> json(%{ status: "success", operation: "block_merchant_bank_account", data: response }) {:error, reason} -> Logger.error("Merchant bank account blocking failed: #{inspect(reason)}") conn |> put_status(:bad_request) |> json(%{ status: "error", operation: "block_merchant_bank_account", code: "BLOCK_BANK_ACCOUNT_FAILED", message: format_error_message(reason) }) end end end @doc """ Add merchant shop endpoint - calls Aani provider to add merchant shop """ def add_merchant_shop(conn, params) do Logger.info("Received add merchant shop request: #{inspect(params)}") # Extract headers and convert to map with lowercase keys headers = conn.req_headers |> Enum.map(fn {k, v} -> {String.downcase(k), v} end) |> Enum.into(%{}) Logger.info("Add shop request headers: #{inspect(headers)}") # Validate required headers required_headers = ["bankuserid", "merchanttag", "requestuserid", "requestid"] missing_headers = Enum.filter(required_headers, fn header -> is_nil(Map.get(headers, header)) || Map.get(headers, header) == "" end) # Validate required params - shop object shop_data = Map.get(params, "shop", %{}) # Validate required shop fields required_shop_fields = ["label", "mid", "type", "mcc"] missing_shop_fields = Enum.filter(required_shop_fields, fn field -> is_nil(Map.get(shop_data, field)) || Map.get(shop_data, field) == "" end) cond do length(missing_headers) > 0 -> Logger.warning("Missing required headers for add shop: #{inspect(missing_headers)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_HEADERS", message: "Missing required headers: #{Enum.join(missing_headers, ", ")}", required_headers: required_headers }) shop_data == %{} -> Logger.warning("Missing or empty shop object") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_SHOP_DATA", message: "Missing or empty shop object. Expected format: {\"label\": \"string\", \"mid\": \"string\", \"type\": \"string\", \"mcc\": \"string\", ...}" }) length(missing_shop_fields) > 0 -> Logger.warning("Missing required shop fields: #{inspect(missing_shop_fields)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_SHOP_FIELDS", message: "Missing required shop fields: #{Enum.join(missing_shop_fields, ", ")}", required_shop_fields: required_shop_fields }) true -> # Call the Aani provider's add_merchant_shop function case DaProductApp.MerchantRegistration.AaniProvider.add_merchant_shop(params, headers) do {:ok, response} -> Logger.info("Merchant shop addition successful: #{inspect(response)}") conn |> put_status(:ok) |> json(%{ status: "success", operation: "add_merchant_shop", data: response }) {:error, reason} -> Logger.error("Merchant shop addition failed: #{inspect(reason)}") conn |> put_status(:bad_request) |> json(%{ status: "error", operation: "add_merchant_shop", code: "ADD_SHOP_FAILED", message: format_error_message(reason) }) end end end @doc """ Update merchant shop endpoint - calls Aani provider to update merchant shop """ def update_merchant_shop(conn, params) do Logger.info("Received update merchant shop request: #{inspect(params)}") # Extract shop ID from path params shop_id = Map.get(params, "shop_id") # Extract headers and convert to map with lowercase keys headers = conn.req_headers |> Enum.map(fn {k, v} -> {String.downcase(k), v} end) |> Enum.into(%{}) Logger.info("Update shop request headers: #{inspect(headers)}") # Validate required headers required_headers = ["bankuserid", "merchanttag", "requestuserid", "requestid"] missing_headers = Enum.filter(required_headers, fn header -> is_nil(Map.get(headers, header)) || Map.get(headers, header) == "" end) # Validate required params - shop object and shop ID shop_data = Map.get(params, "shop", %{}) # Validate required shop fields for update required_shop_fields = ["label", "mid", "mcc"] missing_shop_fields = Enum.filter(required_shop_fields, fn field -> is_nil(Map.get(shop_data, field)) || Map.get(shop_data, field) == "" end) cond do is_nil(shop_id) || shop_id == "" -> Logger.warning("Missing shop ID in URL path") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_SHOP_ID", message: "Shop ID is required in the URL path" }) length(missing_headers) > 0 -> Logger.warning("Missing required headers for update shop: #{inspect(missing_headers)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_HEADERS", message: "Missing required headers: #{Enum.join(missing_headers, ", ")}", required_headers: required_headers }) shop_data == %{} -> Logger.warning("Missing or empty shop object") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_SHOP_DATA", message: "Missing or empty shop object. Expected format: {\"shop\": {\"label\": \"string\", \"mid\": \"string\", \"mcc\": \"string\", \"address\": {...}}}" }) length(missing_shop_fields) > 0 -> Logger.warning("Missing required shop fields: #{inspect(missing_shop_fields)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_SHOP_FIELDS", message: "Missing required shop fields: #{Enum.join(missing_shop_fields, ", ")}", required_shop_fields: required_shop_fields }) true -> # Add shop ID to params for the provider update_params = Map.put(params, "shopId", shop_id) # Call the Aani provider's update_merchant_shop function case DaProductApp.MerchantRegistration.AaniProvider.update_merchant_shop(update_params, headers) do {:ok, response} -> Logger.info("Merchant shop update successful: #{inspect(response)}") conn |> put_status(:ok) |> json(%{ status: "success", operation: "update_merchant_shop", shop_id: shop_id, data: response }) {:error, reason} -> Logger.error("Merchant shop update failed: #{inspect(reason)}") conn |> put_status(:bad_request) |> json(%{ status: "error", operation: "update_merchant_shop", shop_id: shop_id, code: "UPDATE_SHOP_FAILED", message: format_error_message(reason) }) end end end @doc """ Delete merchant shop endpoint - calls Aani provider to delete merchant shop """ def delete_merchant_shop(conn, params) do Logger.info("Received delete merchant shop request: #{inspect(params)}") # Extract shop ID from path params shop_id = Map.get(params, "shop_id") # Extract headers and convert to map with lowercase keys headers = conn.req_headers |> Enum.map(fn {k, v} -> {String.downcase(k), v} end) |> Enum.into(%{}) Logger.info("Delete shop request headers: #{inspect(headers)}") # Validate required headers required_headers = ["bankuserid", "merchanttag", "requestuserid", "requestid"] missing_headers = Enum.filter(required_headers, fn header -> is_nil(Map.get(headers, header)) || Map.get(headers, header) == "" end) cond do is_nil(shop_id) || shop_id == "" -> Logger.warning("Missing shop ID in URL path") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_SHOP_ID", message: "Shop ID is required in the URL path" }) length(missing_headers) > 0 -> Logger.warning("Missing required headers for delete shop: #{inspect(missing_headers)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_HEADERS", message: "Missing required headers: #{Enum.join(missing_headers, ", ")}", required_headers: required_headers }) true -> # Add shop ID to params for the provider delete_params = Map.put(params, "shopId", shop_id) # Call the Aani provider's delete_merchant_shop function case DaProductApp.MerchantRegistration.AaniProvider.delete_merchant_shop(delete_params, headers) do {:ok, response} -> Logger.info("Merchant shop deletion successful: #{inspect(response)}") conn |> put_status(:ok) |> json(%{ status: "success", operation: "delete_merchant_shop", shop_id: shop_id, data: response }) {:error, reason} -> Logger.error("Merchant shop deletion failed: #{inspect(reason)}") conn |> put_status(:bad_request) |> json(%{ status: "error", operation: "delete_merchant_shop", shop_id: shop_id, code: "DELETE_SHOP_FAILED", message: format_error_message(reason) }) end end end @doc """ Add merchant shop cash desk endpoint - calls Aani provider to add cash desk to merchant shop """ def add_merchant_shop_cash_desk(conn, params) do Logger.info("Received add merchant shop cash desk request: #{inspect(params)}") # Extract shop ID from path params shop_id = Map.get(params, "shop_id") # Extract headers and convert to map with lowercase keys headers = conn.req_headers |> Enum.map(fn {k, v} -> {String.downcase(k), v} end) |> Enum.into(%{}) Logger.info("Add cash desk request headers: #{inspect(headers)}") # Validate required headers required_headers = ["bankuserid", "merchanttag", "requestuserid", "requestid"] missing_headers = Enum.filter(required_headers, fn header -> is_nil(Map.get(headers, header)) || Map.get(headers, header) == "" end) # Validate required params - cashDesks array cash_desks_data = Map.get(params, "cashDesks", []) # Validate cash desks array structure - now expecting provider_id and identification invalid_cash_desks = Enum.filter(cash_desks_data, fn cash_desk -> not is_map(cash_desk) || is_nil(Map.get(cash_desk, "identification")) || Map.get(cash_desk, "identification") == "" || is_nil(Map.get(cash_desk, "provider_id")) || Map.get(cash_desk, "provider_id") == "" end) cond do is_nil(shop_id) || shop_id == "" -> Logger.warning("Missing shop ID in URL path") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_SHOP_ID", message: "Shop ID is required in the URL path" }) length(missing_headers) > 0 -> Logger.warning("Missing required headers for add cash desk: #{inspect(missing_headers)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_HEADERS", message: "Missing required headers: #{Enum.join(missing_headers, ", ")}", required_headers: required_headers }) cash_desks_data == [] -> Logger.warning("Missing or empty cashDesks array") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_CASH_DESKS_DATA", message: "Missing or empty cashDesks array. Expected format: {\"cashDesks\": [{\"provider_id\": \"3\", \"identification\": \"string\"}]}" }) length(invalid_cash_desks) > 0 -> Logger.warning("Invalid cash desk objects: #{inspect(invalid_cash_desks)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "INVALID_CASH_DESKS", message: "All cashDesks must have valid 'provider_id' and 'identification' fields", invalid_cash_desks: invalid_cash_desks }) true -> # Store original cash desks for shukria_terminals update original_cash_desks = cash_desks_data # Filter cash desks for AANI - remove provider_id, keep only identification aani_cash_desks = Enum.map(cash_desks_data, fn cash_desk -> %{"identification" => Map.get(cash_desk, "identification")} end) # Prepare params for AANI provider (only identification) add_params = %{ "shopId" => shop_id, "cashDesks" => aani_cash_desks } Logger.info("Original cashDesks: #{inspect(original_cash_desks)}") Logger.info("Filtered cashDesks for AANI: #{inspect(aani_cash_desks)}") # Call the Aani provider's add_merchant_shop_cash_desk function case DaProductApp.MerchantRegistration.AaniProvider.add_merchant_shop_cash_desk(add_params, headers) do {:ok, response} -> Logger.info("Merchant shop cash desk addition successful: #{inspect(response)}") # Update shukria_terminals with the response update_shukria_terminals_with_cash_desk_response(original_cash_desks, response, shop_id) conn |> put_status(:ok) |> json(%{ status: "success", operation: "add_merchant_shop_cash_desk", shop_id: shop_id, cash_desks_count: length(cash_desks_data), data: response }) {:error, reason} -> Logger.error("Merchant shop cash desk addition failed: #{inspect(reason)}") conn |> put_status(:bad_request) |> json(%{ status: "error", operation: "add_merchant_shop_cash_desk", shop_id: shop_id, code: "ADD_CASH_DESK_FAILED", message: format_error_message(reason) }) end end end @doc """ Update merchant shop cash desk endpoint - calls Aani provider to update cash desk in merchant shop """ def update_merchant_shop_cash_desk(conn, params) do Logger.info("Received update merchant shop cash desk request: #{inspect(params)}") # Extract shop ID from path params shop_id = Map.get(params, "shop_id") # Extract headers and convert to map with lowercase keys headers = conn.req_headers |> Enum.map(fn {k, v} -> {String.downcase(k), v} end) |> Enum.into(%{}) Logger.info("Update cash desk request headers: #{inspect(headers)}") # Validate required headers required_headers = ["bankuserid", "merchanttag", "requestuserid", "requestid"] missing_headers = Enum.filter(required_headers, fn header -> is_nil(Map.get(headers, header)) || Map.get(headers, header) == "" end) # Validate required params - cashDesks array cash_desks_data = Map.get(params, "cashDesks", []) # Validate cash desks array structure for update (must have id and identification) invalid_cash_desks = Enum.filter(cash_desks_data, fn cash_desk -> not is_map(cash_desk) || is_nil(Map.get(cash_desk, "id")) || is_nil(Map.get(cash_desk, "identification")) || Map.get(cash_desk, "identification") == "" end) cond do is_nil(shop_id) || shop_id == "" -> Logger.warning("Missing shop ID in URL path") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_SHOP_ID", message: "Shop ID is required in the URL path" }) length(missing_headers) > 0 -> Logger.warning("Missing required headers for update cash desk: #{inspect(missing_headers)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_HEADERS", message: "Missing required headers: #{Enum.join(missing_headers, ", ")}", required_headers: required_headers }) cash_desks_data == [] -> Logger.warning("Missing or empty cashDesks array") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_CASH_DESKS_DATA", message: "Missing or empty cashDesks array. Expected format: {\"cashDesks\": [{\"id\": 21532435, \"identification\": \"string\"}]}" }) length(invalid_cash_desks) > 0 -> Logger.warning("Invalid cash desk objects: #{inspect(invalid_cash_desks)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "INVALID_CASH_DESKS", message: "All cashDesks must have valid 'id' and 'identification' fields for update", invalid_cash_desks: invalid_cash_desks }) true -> # Add shop ID to params for the provider update_params = Map.put(params, "shopId", shop_id) # Call the Aani provider's update_merchant_shop_cash_desk function case DaProductApp.MerchantRegistration.AaniProvider.update_merchant_shop_cash_desk(update_params, headers) do {:ok, response} -> Logger.info("Merchant shop cash desk update successful: #{inspect(response)}") conn |> put_status(:ok) |> json(%{ status: "success", operation: "update_merchant_shop_cash_desk", shop_id: shop_id, cash_desks_count: length(cash_desks_data), data: response }) {:error, reason} -> Logger.error("Merchant shop cash desk update failed: #{inspect(reason)}") conn |> put_status(:bad_request) |> json(%{ status: "error", operation: "update_merchant_shop_cash_desk", shop_id: shop_id, code: "UPDATE_CASH_DESK_FAILED", message: format_error_message(reason) }) end end end @doc """ Delete merchant shop cash desk endpoint - calls Aani provider to delete cash desk from merchant shop """ def delete_merchant_shop_cash_desk(conn, params) do Logger.info("Received delete merchant shop cash desk request: #{inspect(params)}") # Extract shop ID from path params shop_id = Map.get(params, "shop_id") # Extract headers and convert to map with lowercase keys headers = conn.req_headers |> Enum.map(fn {k, v} -> {String.downcase(k), v} end) |> Enum.into(%{}) Logger.info("Delete cash desk request headers: #{inspect(headers)}") # Validate required headers required_headers = ["bankuserid", "merchanttag", "requestuserid", "requestid"] missing_headers = Enum.filter(required_headers, fn header -> is_nil(Map.get(headers, header)) || Map.get(headers, header) == "" end) # Validate required params - cashDesks array cash_desks_data = Map.get(params, "cashDesks", []) # Validate cash desks array structure for deletion (must have id) invalid_cash_desks = Enum.filter(cash_desks_data, fn cash_desk -> not is_map(cash_desk) || is_nil(Map.get(cash_desk, "id")) end) cond do is_nil(shop_id) || shop_id == "" -> Logger.warning("Missing shop ID in URL path") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_SHOP_ID", message: "Shop ID is required in the URL path" }) length(missing_headers) > 0 -> Logger.warning("Missing required headers for delete cash desk: #{inspect(missing_headers)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_HEADERS", message: "Missing required headers: #{Enum.join(missing_headers, ", ")}", required_headers: required_headers }) cash_desks_data == [] -> Logger.warning("Missing or empty cashDesks array") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "MISSING_CASH_DESKS_DATA", message: "Missing or empty cashDesks array. Expected format: {\"cashDesks\": [{\"id\": \"253643\"}]}" }) length(invalid_cash_desks) > 0 -> Logger.warning("Invalid cash desk objects: #{inspect(invalid_cash_desks)}") conn |> put_status(:bad_request) |> json(%{ status: "error", code: "INVALID_CASH_DESKS", message: "All cashDesks must have a valid 'id' field for deletion", invalid_cash_desks: invalid_cash_desks }) true -> # Add shop ID to params for the provider delete_params = Map.put(params, "shopId", shop_id) # Call the Aani provider's delete_merchant_shop_cash_desk function case DaProductApp.MerchantRegistration.AaniProvider.delete_merchant_shop_cash_desk(delete_params, headers) do {:ok, response} -> Logger.info("Merchant shop cash desk deletion successful: #{inspect(response)}") conn |> put_status(:ok) |> json(%{ status: "success", operation: "delete_merchant_shop_cash_desk", shop_id: shop_id, cash_desks_count: length(cash_desks_data), data: response }) {:error, reason} -> Logger.error("Merchant shop cash desk deletion failed: #{inspect(reason)}") conn |> put_status(:bad_request) |> json(%{ status: "error", operation: "delete_merchant_shop_cash_desk", shop_id: shop_id, code: "DELETE_CASH_DESK_FAILED", message: format_error_message(reason) }) end end end # Helper function to format error messages for standardized response defp format_error_message(reason) when is_binary(reason), do: reason defp format_error_message(reason) when is_map(reason) do case reason do %{"message" => message} -> message %{"error" => error} -> format_error_message(error) %{"reason" => reason} -> format_error_message(reason) _ -> "Registration failed: #{inspect(reason)}" end end defp format_error_message(reason), do: "Registration failed: #{inspect(reason)}" # Direct Aani format conversion functions moved to AaniProvider module end