lib/wechat/pay/pay.ex

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

  [官方文档](https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml)

  ** 注意 ** 未经上线测试,请谨慎使用
  """

  @typedoc "商户号"
  @type mch_id :: binary
  @typedoc "平台证书的序列号"
  @type serial_no :: binary
  @typedoc "平台证书列表"
  @type cacerts :: [binary]
  @typedoc "商户API证书"
  @type client_cert :: binary
  @typedoc "商户API私钥"
  @type client_key :: binary
  @type client :: module()
  @type options :: Enumerable.t()

  @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
    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
  end

  def put_requester_opts(client, id, serial_no) do
    name = finch_name(client, id)

    :persistent_term.put({:wechat, {client, :requester_opts}}, %{
      id: id,
      name: name,
      serial_no: serial_no
    })
  end

  def get_requester_opts(client) do
    :persistent_term.get({:wechat, {client, :requester_opts}})
  end

  # 保存平台证书 serial_no => cert 的对应关系
  def put_cert(client, serial_no, cert) do
    :persistent_term.put({:wechat, {client, serial_no}}, cert)
  end

  # 获取平台证书 serial_no 对应的 cert
  def get_cert(client, serial_no) do
    :persistent_term.get({:wechat, {client, serial_no}})
  end

  def remove_cert(client, serial_no) do
    :persistent_term.erase({:wechat, {client, serial_no}})
  end

  defp finch_name(client, id), do: :"#{client}.Finch.#{id}"

  def get_requester_spec(id, client, cacerts) when is_atom(id) do
    name = finch_name(client, id)

    finch_pool =
      Application.get_env(:wechat, :finch_pool, size: 32, count: 8) ++
        [
          conn_opts: [
            transport_opts: [
              cacerts: cacerts,
              cert: client.client_cert(),
              key: client.client_key()
            ]
          ]
        ]

    options = [name: name, pools: %{:default => finch_pool}]
    spec = Finch.child_spec(options)
    %{spec | id: id}
  end

  # [平台证书更新指引](https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay5_0.shtml)
  # * 证书切换
  #  * 通过 Supervisor 开启新的 Finch 进程
  #  * 然后 将新的 Finch 进程名写入到 :persistent_term 保存
  #  * 请求的时候,从 :persistent_term 获取 Finch 进程名,然后再请求
  def start_next_requester(client, opts) do
    %{id: now_id} = get_requester_opts(client)
    id = List.delete([:A, :B], now_id) |> hd()
    finch_spec = get_requester_spec(id, client, opts.cacerts)
    sup = :"#{client}.Supervisor"

    with :ok <- Supervisor.terminate_child(sup, id),
         :ok <- Supervisor.delete_child(sup, id),
         {:ok, _} = return <- Supervisor.start_child(sup, finch_spec) do
      put_requester_opts(client, id, opts.serial_no)
      return
    end
  end

  def init_cacerts2storage(client, cacerts) do
    storage = client.storage()
    storage.store(client.mch_id(), :cacerts, cacerts)
  end
end