Skip to content

Commit b2177e5

Browse files
authored
feat: autopaid tips (#64)
1 parent 31d94e5 commit b2177e5

File tree

3 files changed

+325
-43
lines changed

3 files changed

+325
-43
lines changed

lib/algora/bounties/bounties.ex

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,33 @@ defmodule Algora.Bounties do
560560
) ::
561561
{:ok, String.t()} | {:error, atom()}
562562
def create_tip(%{creator: creator, owner: owner, recipient: recipient, amount: amount}, opts \\ []) do
563+
Repo.transact(fn ->
564+
with {:ok, tip} <-
565+
do_create_tip(
566+
%{
567+
creator: creator,
568+
owner: owner,
569+
recipient: recipient,
570+
amount: amount
571+
},
572+
opts
573+
) do
574+
create_payment_session(
575+
%{owner: owner, amount: amount, description: "Tip payment for OSS contributions"},
576+
ticket_ref: opts[:ticket_ref],
577+
tip_id: tip.id,
578+
recipient: recipient
579+
)
580+
end
581+
end)
582+
end
583+
584+
@spec do_create_tip(
585+
%{creator: User.t(), owner: User.t(), recipient: User.t(), amount: Money.t()},
586+
opts :: [ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()}, installation_id: integer()]
587+
) ::
588+
{:ok, Tip.t()} | {:error, atom()}
589+
def do_create_tip(%{creator: creator, owner: owner, recipient: recipient, amount: amount}, opts \\ []) do
563590
installation_id = opts[:installation_id]
564591

565592
token_res =
@@ -578,29 +605,20 @@ defmodule Algora.Bounties do
578605
{:ok, nil}
579606
end
580607

581-
Repo.transact(fn ->
582-
with {:ok, ticket} <- ticket_res,
583-
{:ok, tip} <-
584-
%Tip{}
585-
|> Tip.changeset(%{
586-
amount: amount,
587-
owner_id: owner.id,
588-
creator_id: creator.id,
589-
recipient_id: recipient.id,
590-
ticket_id: if(ticket, do: ticket.id)
591-
})
592-
|> Repo.insert_with_activity(%{
593-
type: :tip_awarded,
594-
notify_users: [recipient.id]
595-
}) do
596-
create_payment_session(
597-
%{owner: owner, amount: amount, description: "Tip payment for OSS contributions"},
598-
ticket_ref: ticket_ref,
599-
tip_id: tip.id,
600-
recipient: recipient
601-
)
602-
end
603-
end)
608+
with {:ok, ticket} <- ticket_res do
609+
%Tip{}
610+
|> Tip.changeset(%{
611+
amount: amount,
612+
owner_id: owner.id,
613+
creator_id: creator.id,
614+
recipient_id: recipient.id,
615+
ticket_id: if(ticket, do: ticket.id)
616+
})
617+
|> Repo.insert_with_activity(%{
618+
type: :tip_awarded,
619+
notify_users: [recipient.id]
620+
})
621+
end
604622
end
605623

606624
@spec reward_bounty(

lib/algora_web/controllers/webhooks/github_controller.ex

Lines changed: 110 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ defmodule AlgoraWeb.Webhooks.GithubController do
88
alias Algora.Bounties
99
alias Algora.Bounties.Bounty
1010
alias Algora.Bounties.Claim
11+
alias Algora.Bounties.Tip
1112
alias Algora.Github
1213
alias Algora.Github.Webhook
14+
alias Algora.Payments.Customer
15+
alias Algora.PSP.Invoice
1316
alias Algora.Repo
1417
alias Algora.Workspace
1518
alias Algora.Workspace.CommandResponse
@@ -110,11 +113,7 @@ defmodule AlgoraWeb.Webhooks.GithubController do
110113
primary_claim = List.first(claims)
111114

112115
installation =
113-
Repo.one(
114-
from i in Installation,
115-
where: i.provider == "github",
116-
where: i.provider_id == ^to_string(payload["installation"]["id"])
117-
)
116+
Repo.get_by(Installation, provider: "github", provider_id: to_string(payload["installation"]["id"]))
118117

119118
bounties =
120119
Repo.all(
@@ -158,7 +157,7 @@ defmodule AlgoraWeb.Webhooks.GithubController do
158157
claims: claims
159158
),
160159
{:ok, _invoice} <-
161-
Algora.PSP.Invoice.pay(
160+
Invoice.pay(
162161
invoice,
163162
%{
164163
payment_method: autopayable_bounty.owner.customer.default_payment_method.provider_id,
@@ -299,22 +298,114 @@ defmodule AlgoraWeb.Webhooks.GithubController do
299298
defp execute_command(%Webhook{event_action: event_action, payload: payload} = webhook, {:tip, args})
300299
when event_action in ["issue_comment.created", "issue_comment.edited"] do
301300
amount = args[:amount]
302-
# TODO: handle autopay with cooldown
301+
302+
ticket_ref = %{
303+
owner: payload["repository"]["owner"]["login"],
304+
repo: payload["repository"]["name"],
305+
number: payload["issue"]["number"]
306+
}
307+
303308
case get_permissions(webhook) do
304309
{:ok, "admin"} ->
305-
with {:ok, recipient} <- get_tip_recipient(webhook, {:tip, args}) do
306-
Bounties.create_tip_intent(
307-
%{
308-
recipient: recipient,
309-
amount: amount,
310-
ticket_ref: %{
311-
owner: payload["repository"]["owner"]["login"],
312-
repo: payload["repository"]["name"],
313-
number: payload["issue"]["number"]
314-
}
315-
},
316-
installation_id: payload["installation"]["id"]
310+
installation =
311+
Repo.get_by(Installation, provider: "github", provider_id: to_string(payload["installation"]["id"]))
312+
313+
customer =
314+
Repo.one(
315+
from c in Customer,
316+
left_join: p in assoc(c, :default_payment_method),
317+
where: c.user_id == ^installation.connected_user_id,
318+
select_merge: %{default_payment_method: p}
317319
)
320+
321+
{:ok, recipient} = get_tip_recipient(webhook, {:tip, args})
322+
323+
{:ok, token} = Github.get_installation_token(payload["installation"]["id"])
324+
325+
{:ok, ticket} = Workspace.ensure_ticket(token, ticket_ref.owner, ticket_ref.repo, ticket_ref.number)
326+
327+
autopay_cooldown_expired? = fn ->
328+
from(t in Tip,
329+
join: recipient in assoc(t, :recipient),
330+
where: recipient.provider_login == ^recipient,
331+
where: t.ticket_id == ^ticket.id,
332+
where: t.status != :cancelled,
333+
order_by: [desc: t.inserted_at],
334+
limit: 1
335+
)
336+
|> Repo.one()
337+
|> case do
338+
nil -> true
339+
tip -> DateTime.diff(DateTime.utc_now(), tip.inserted_at, :millisecond) > :timer.hours(1)
340+
end
341+
end
342+
343+
autopayable? =
344+
not is_nil(installation) and
345+
not is_nil(customer) and
346+
not is_nil(customer.default_payment_method) and
347+
not is_nil(recipient) and
348+
not is_nil(amount) and
349+
autopay_cooldown_expired?.()
350+
351+
idempotency_key = "tip-#{recipient}-#{webhook.delivery}"
352+
353+
autopay_result =
354+
if autopayable? do
355+
with {:ok, owner} <- Accounts.fetch_user_by(id: installation.connected_user_id),
356+
{:ok, creator} <- Workspace.ensure_user(token, payload["repository"]["owner"]["login"]),
357+
{:ok, recipient} <- Workspace.ensure_user(token, recipient),
358+
{:ok, tip} <-
359+
Bounties.do_create_tip(
360+
%{creator: creator, owner: owner, recipient: recipient, amount: amount},
361+
ticket_ref: ticket_ref,
362+
installation_id: payload["installation"]["id"]
363+
),
364+
{:ok, invoice} <-
365+
Bounties.create_invoice(
366+
%{
367+
owner: owner,
368+
amount: amount,
369+
idempotency_key: idempotency_key
370+
},
371+
ticket_ref: ticket_ref,
372+
tip_id: tip.id,
373+
recipient: recipient
374+
),
375+
{:ok, _invoice} <-
376+
Invoice.pay(
377+
invoice,
378+
%{
379+
payment_method: customer.default_payment_method.provider_id,
380+
off_session: true
381+
},
382+
%{idempotency_key: idempotency_key}
383+
) do
384+
Logger.info("Autopay successful (#{payload["repository"]["full_name"]}##{ticket_ref.number} - #{amount}).")
385+
{:ok, tip}
386+
else
387+
{:error, reason} ->
388+
Logger.error(
389+
"Autopay failed (#{payload["repository"]["full_name"]}##{ticket_ref.number} - #{amount}): #{inspect(reason)}"
390+
)
391+
392+
{:error, reason}
393+
end
394+
end
395+
396+
case autopay_result do
397+
{:ok, tip} ->
398+
{:ok, tip}
399+
400+
_ ->
401+
Bounties.create_tip_intent(
402+
%{
403+
recipient: recipient,
404+
amount: amount,
405+
ticket_ref: ticket_ref
406+
},
407+
installation_id: payload["installation"]["id"]
408+
)
318409
end
319410

320411
{:ok, _permission} ->

0 commit comments

Comments
 (0)