Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 41 additions & 23 deletions lib/algora/bounties/bounties.ex
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,33 @@ defmodule Algora.Bounties do
) ::
{:ok, String.t()} | {:error, atom()}
def create_tip(%{creator: creator, owner: owner, recipient: recipient, amount: amount}, opts \\ []) do
Repo.transact(fn ->
with {:ok, tip} <-
do_create_tip(
%{
creator: creator,
owner: owner,
recipient: recipient,
amount: amount
},
opts
) do
create_payment_session(
%{owner: owner, amount: amount, description: "Tip payment for OSS contributions"},
ticket_ref: opts[:ticket_ref],
tip_id: tip.id,
recipient: recipient
)
end
end)
end

@spec do_create_tip(
%{creator: User.t(), owner: User.t(), recipient: User.t(), amount: Money.t()},
opts :: [ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()}, installation_id: integer()]
) ::
{:ok, Tip.t()} | {:error, atom()}
def do_create_tip(%{creator: creator, owner: owner, recipient: recipient, amount: amount}, opts \\ []) do
installation_id = opts[:installation_id]

token_res =
Expand All @@ -578,29 +605,20 @@ defmodule Algora.Bounties do
{:ok, nil}
end

Repo.transact(fn ->
with {:ok, ticket} <- ticket_res,
{:ok, tip} <-
%Tip{}
|> Tip.changeset(%{
amount: amount,
owner_id: owner.id,
creator_id: creator.id,
recipient_id: recipient.id,
ticket_id: if(ticket, do: ticket.id)
})
|> Repo.insert_with_activity(%{
type: :tip_awarded,
notify_users: [recipient.id]
}) do
create_payment_session(
%{owner: owner, amount: amount, description: "Tip payment for OSS contributions"},
ticket_ref: ticket_ref,
tip_id: tip.id,
recipient: recipient
)
end
end)
with {:ok, ticket} <- ticket_res do
%Tip{}
|> Tip.changeset(%{
amount: amount,
owner_id: owner.id,
creator_id: creator.id,
recipient_id: recipient.id,
ticket_id: if(ticket, do: ticket.id)
})
|> Repo.insert_with_activity(%{
type: :tip_awarded,
notify_users: [recipient.id]
})
end
end

@spec reward_bounty(
Expand Down
129 changes: 110 additions & 19 deletions lib/algora_web/controllers/webhooks/github_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ defmodule AlgoraWeb.Webhooks.GithubController do
alias Algora.Bounties
alias Algora.Bounties.Bounty
alias Algora.Bounties.Claim
alias Algora.Bounties.Tip
alias Algora.Github
alias Algora.Github.Webhook
alias Algora.Payments.Customer
alias Algora.PSP.Invoice
alias Algora.Repo
alias Algora.Workspace
alias Algora.Workspace.CommandResponse
Expand Down Expand Up @@ -110,11 +113,7 @@ defmodule AlgoraWeb.Webhooks.GithubController do
primary_claim = List.first(claims)

installation =
Repo.one(
from i in Installation,
where: i.provider == "github",
where: i.provider_id == ^to_string(payload["installation"]["id"])
)
Repo.get_by(Installation, provider: "github", provider_id: to_string(payload["installation"]["id"]))

bounties =
Repo.all(
Expand Down Expand Up @@ -158,7 +157,7 @@ defmodule AlgoraWeb.Webhooks.GithubController do
claims: claims
),
{:ok, _invoice} <-
Algora.PSP.Invoice.pay(
Invoice.pay(
invoice,
%{
payment_method: autopayable_bounty.owner.customer.default_payment_method.provider_id,
Expand Down Expand Up @@ -299,22 +298,114 @@ defmodule AlgoraWeb.Webhooks.GithubController do
defp execute_command(%Webhook{event_action: event_action, payload: payload} = webhook, {:tip, args})
when event_action in ["issue_comment.created", "issue_comment.edited"] do
amount = args[:amount]
# TODO: handle autopay with cooldown

ticket_ref = %{
owner: payload["repository"]["owner"]["login"],
repo: payload["repository"]["name"],
number: payload["issue"]["number"]
}

case get_permissions(webhook) do
{:ok, "admin"} ->
with {:ok, recipient} <- get_tip_recipient(webhook, {:tip, args}) do
Bounties.create_tip_intent(
%{
recipient: recipient,
amount: amount,
ticket_ref: %{
owner: payload["repository"]["owner"]["login"],
repo: payload["repository"]["name"],
number: payload["issue"]["number"]
}
},
installation_id: payload["installation"]["id"]
installation =
Repo.get_by(Installation, provider: "github", provider_id: to_string(payload["installation"]["id"]))

customer =
Repo.one(
from c in Customer,
left_join: p in assoc(c, :default_payment_method),
where: c.user_id == ^installation.connected_user_id,
select_merge: %{default_payment_method: p}
)

{:ok, recipient} = get_tip_recipient(webhook, {:tip, args})

{:ok, token} = Github.get_installation_token(payload["installation"]["id"])

{:ok, ticket} = Workspace.ensure_ticket(token, ticket_ref.owner, ticket_ref.repo, ticket_ref.number)

autopay_cooldown_expired? = fn ->
from(t in Tip,
join: recipient in assoc(t, :recipient),
where: recipient.provider_login == ^recipient,
where: t.ticket_id == ^ticket.id,
where: t.status != :cancelled,
order_by: [desc: t.inserted_at],
limit: 1
)
|> Repo.one()
|> case do
nil -> true
tip -> DateTime.diff(DateTime.utc_now(), tip.inserted_at, :millisecond) > :timer.hours(1)
end
end

autopayable? =
not is_nil(installation) and
not is_nil(customer) and
not is_nil(customer.default_payment_method) and
not is_nil(recipient) and
not is_nil(amount) and
autopay_cooldown_expired?.()

idempotency_key = "tip-#{recipient}-#{webhook.delivery}"

autopay_result =
if autopayable? do
with {:ok, owner} <- Accounts.fetch_user_by(id: installation.connected_user_id),
{:ok, creator} <- Workspace.ensure_user(token, payload["repository"]["owner"]["login"]),
{:ok, recipient} <- Workspace.ensure_user(token, recipient),
{:ok, tip} <-
Bounties.do_create_tip(
%{creator: creator, owner: owner, recipient: recipient, amount: amount},
ticket_ref: ticket_ref,
installation_id: payload["installation"]["id"]
),
{:ok, invoice} <-
Bounties.create_invoice(
%{
owner: owner,
amount: amount,
idempotency_key: idempotency_key
},
ticket_ref: ticket_ref,
tip_id: tip.id,
recipient: recipient
),
{:ok, _invoice} <-
Invoice.pay(
invoice,
%{
payment_method: customer.default_payment_method.provider_id,
off_session: true
},
%{idempotency_key: idempotency_key}
) do
Logger.info("Autopay successful (#{payload["repository"]["full_name"]}##{ticket_ref.number} - #{amount}).")
{:ok, tip}
else
{:error, reason} ->
Logger.error(
"Autopay failed (#{payload["repository"]["full_name"]}##{ticket_ref.number} - #{amount}): #{inspect(reason)}"
)

{:error, reason}
end
end

case autopay_result do
{:ok, tip} ->
{:ok, tip}

_ ->
Bounties.create_tip_intent(
%{
recipient: recipient,
amount: amount,
ticket_ref: ticket_ref
},
installation_id: payload["installation"]["id"]
)
end

{:ok, _permission} ->
Expand Down
Loading