lib/wechat/pay/pay.ex

defmodule WeChat.Pay do
  @moduledoc """
  微信支付

  ## 添加依赖

      def deps do
        [
          {:wechat, "~> x.x", hex: :wechat_sdk},
          {:saxy, "~> 1.2"},
          {:x509, "~> x.x"}
        ]
      end

  ## 定义支付 Client 模块

      defmodule YourApp.WeChatAppCodeName do
        @moduledoc "CodeName"
        use WeChat.Pay,
          mch_id: "1900000109",
          api_secret_v2_key: "api_secret_v2_key",
          api_secret_key: "api_secret_v3_key",
          client_serial_no: "client_serial_no",
          client_key: {:file, "apiclient_key.pem"}
      end

  定义参数说明请看 `t:options/0`

  ## V2 SSL 配置

  部分 v2 的接口请求时需要用到证书,如:撤销订单,因此如果有使用到这部分接口,必须添加下面的配置

      config :wechat, YourApp.WeChatAppCodeName,
        v2_ssl: [
          certfile: "apiclient_cert.pem", # or {:app_dir, App, "priv/cert/apiclient_cert.pem"}
          keyfile: "apiclient_key.pem" # or {:app_dir, App, "priv/cert/apiclient_key.pem"}
        ]

  ## 启动支付 Client 进程

      defmodule YourApp.Application do
        def start(_type, _args) do
          children = [
            # ...
            YourApp.WeChatAppCodeName,
            # or
            {YourApp.WeChatAppCodeName, start_options}
          ]

          Supervisor.start_link(children, strategy: :one_for_one, name: YourApp.Supervisor)
        end
      end

  启动参数说明请看 `t:start_options/0`

  ## 处理回调消息

  详情请看 `WeChat.Pay.EventHandler`
  """
  require Logger
  alias WeChat.Utils
  alias WeChat.Pay.Certificates

  @typedoc "商户号"
  @type mch_id :: binary
  @typedoc """
  平台 证书序列号 -
  [官方文档](https://pay.weixin.qq.com/docs/merchant/development/interface-rules/certificate-faqs.html){:target="_blank"}
  """
  @type platform_serial_no :: serial_no
  @typedoc """
  商户API 证书序列号 -
  [官方文档](https://pay.weixin.qq.com/docs/merchant/development/interface-rules/certificate-faqs.html){:target="_blank"}
  """
  @type client_serial_no :: serial_no
  @typedoc "证书的序列号"
  @type serial_no :: binary
  @typedoc """
  平台证书列表 -
  [官方文档](https://pay.weixin.qq.com/docs/merchant/development/interface-rules/wechatpay-certificates.html){:target="_blank"}
  """
  @type cacerts :: list(binary)
  @typedoc """
  商户 API 私钥 -
  [官方文档](https://pay.weixin.qq.com/docs/merchant/development/interface-rules/privatekey-and-certificate.html){:target="_blank"}
  """
  @type client_key :: pem_file
  @typedoc """
  API 密钥 -
  [官方文档](https://pay.weixin.qq.com/docs/merchant/development/interface-rules/apiv3key.html){:target="_blank"}
  """
  @type api_secret_key :: binary | WeChat.env_option()
  @type client :: module
  @type pem_file ::
          {:binary, binary} | {:file, Path.t()} | {:app_dir, Application.app(), Path.t()}

  @typedoc """
  构建参数

  ## 参数说明

  - `mch_id`: 商户ID, 必填
  - `api_secret_v2_key`: API v2密钥, 必填
  - `api_secret_key`: API v3密钥, 必填
  - `client_serial_no`: 客户端证书序列号, 必填
  - `client_key`: 客户端私钥, 必填
  - `storage`: 存储器,默认值: `WeChat.Storage.PayFile`
  - `requester`: 请求客户端, 默认值: `WeChat.Requester.Pay`
  """
  @type options :: [
          mch_id: mch_id,
          api_secret_v2_key: api_secret_key,
          api_secret_key: api_secret_key,
          client_serial_no: client_serial_no,
          client_key: client_key,
          requester: module,
          storage: module
        ]

  @typedoc """
  启动参数

  - `refresher`: 刷新器, 可选, 默认值: `WeChat.Refresher.Pay`
  - `finch_pool`: Finch Pool 配置, 可选, 默认值: `[size: 32, count: 8]`
  - `v2_ssl`: V2 SSL 配置, 可选
  """
  @type start_options :: [
          refresher: module,
          finch_pool: Keyword.t(),
          v2_ssl: Keyword.t()
        ]
  @type requester_id :: :A | :B
  @type requester_opts :: %{id: requester_id, name: atom}

  @doc false
  defmacro __using__(options \\ []) do
    quote do
      use WeChat.Builder.Pay, unquote(options)
    end
  end

  @doc "动态构建 client"
  @spec build_client(client, options) :: {:ok, client}
  def build_client(client, options) do
    Code.compiler_options(ignore_module_conflict: true)

    result =
      with {:module, module, _binary, _term} <-
             Module.create(
               client,
               quote do
                 @moduledoc false
                 use WeChat.Builder.Pay, unquote(Macro.escape(options))
               end,
               Macro.Env.location(__ENV__)
             ) do
        {:ok, module}
      end

    Code.compiler_options(ignore_module_conflict: false)
    result
  end

  @doc "动态启动 client"
  @spec start_client(Supervisor.supervisor(), client, start_options) ::
          Supervisor.on_start_child()
  def start_client(supervisor, client, options \\ []) do
    Supervisor.start_child(supervisor, {client, options})
  end

  @doc "动态关闭 client"
  @spec shutdown_client(Supervisor.supervisor(), client) :: :ok | {:error, error :: any()}
  def shutdown_client(supervisor, client) do
    with :ok <- Supervisor.terminate_child(supervisor, client) do
      Supervisor.delete_child(supervisor, client)
    end
  end

  @doc false
  # v3
  def finch_name(client), do: :"#{client}.Finch"
  @doc false
  def v2_ssl_finch_name(client), do: :"#{client}.v2ssl.Finch"

  @spec get_requester_specs(client, opts :: map) :: [map]
  def get_requester_specs(client, opts) do
    client_config =
      case Application.fetch_env(:wechat, client) do
        {:ok, config} -> config
        _ -> %{}
      end

    finch_pool =
      case Map.get(opts, :finch_pool, client_config[:finch_pool]) do
        finch_pool when is_list(finch_pool) or is_map(finch_pool) -> finch_pool
        _ -> Application.get_env(:wechat, :finch_pool, size: 32, count: 8)
      end

    # v3
    v3_spec =
      Finch.child_spec(name: finch_name(client), pools: %{:default => finch_pool})
      |> Map.put(:id, Finch)

    v2_spec =
      case Map.get(opts, :v2_ssl, client_config[:v2_ssl]) do
        ssl when is_list(ssl) ->
          certfile =
            Keyword.get_lazy(ssl, :certfile, fn ->
              raise ArgumentError, "missing :certfile for v2_ssl in config :wechat, #{client}"
            end)
            |> Utils.expand_file()
            |> case do
              {:ok, file} ->
                file

              {:error, error} ->
                raise ArgumentError, "certfile #{error} for v2_ssl in config :wechat, #{client}"
            end

          keyfile =
            Keyword.get_lazy(ssl, :keyfile, fn ->
              raise ArgumentError, "missing :keyfile for v2_ssl in config :wechat, #{client}"
            end)
            |> Utils.expand_file()
            |> case do
              {:ok, file} ->
                file

              {:error, error} ->
                raise ArgumentError, "keyfile #{error} for v2_ssl in config :wechat, #{client}"
            end

          v2_finch_pool =
            finch_pool ++
              [
                conn_opts: [
                  transport_opts: [certfile: certfile, keyfile: keyfile, versions: [:"tlsv1.2"]]
                ]
              ]

          Finch.child_spec(name: v2_ssl_finch_name(client), pools: %{:default => v2_finch_pool})
          |> Map.put(:id, V2.Finch)

        _ ->
          nil
      end

    if v2_spec do
      [v3_spec, v2_spec]
    else
      [v3_spec]
    end
  end

  @doc "初始化平台证书"
  @spec init_certs(client) :: {:ok, cacerts :: list(map)}
  def init_certs(client) do
    with {:ok, certs} when is_list(certs) <- Certificates.certificates(client, true) do
      Certificates.put_certs(certs, client)
      storage = client.storage()
      result = storage.store(client.mch_id(), :certs, certs)
      Logger.info("store cacerts for mch_id:#{client.mch_id()} result: #{inspect(result)}.")
      {:ok, certs}
    else
      error ->
        Logger.warning("Init certificates error: #{inspect(error)}.")
        error
    end
  end
end