Skip to content

Commit 22973fb

Browse files
committed
feat: bounty claims
1 parent eca9ed3 commit 22973fb

File tree

11 files changed

+246
-25
lines changed

11 files changed

+246
-25
lines changed

config/config.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ config :algora, Oban,
3232
comment_consumers: 1,
3333
github_og_image: 5,
3434
notify_bounty: 1,
35-
notify_tip_intent: 1
35+
notify_tip_intent: 1,
36+
notify_claim: 1
3637
]
3738

3839
# Configures the mailer

lib/algora/bounties/bounties.ex

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,88 @@ defmodule Algora.Bounties do
121121
|> Oban.insert()
122122
end
123123

124+
@spec do_claim_bounty(%{user: User.t(), ticket: Ticket.t(), pull_request: map()}) ::
125+
{:ok, Claim.t()} | {:error, atom()}
126+
defp do_claim_bounty(%{user: user, ticket: ticket, pull_request: pull_request}) do
127+
# TODO: ensure user is pull request author
128+
id = Nanoid.generate()
129+
130+
changeset =
131+
Claim.changeset(%Claim{}, %{
132+
ticket_id: ticket.id,
133+
user_id: user.id,
134+
provider: "github",
135+
provider_id: pull_request["id"],
136+
provider_meta: pull_request,
137+
title: pull_request["title"],
138+
url: pull_request["html_url"],
139+
group_id: id,
140+
merged_at: Util.to_date(pull_request["merged_at"])
141+
})
142+
143+
case Repo.insert(changeset) do
144+
{:ok, claim} ->
145+
{:ok, claim}
146+
147+
{:error, %{errors: [ticket_id: {_, [constraint: :unique, constraint_name: _]}]}} ->
148+
{:error, :already_exists}
149+
150+
{:error, _changeset} ->
151+
{:error, :internal_server_error}
152+
end
153+
end
154+
155+
@spec claim_bounty(
156+
%{
157+
user: User.t(),
158+
ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()},
159+
pull_request: map()
160+
},
161+
opts :: [installation_id: integer()]
162+
) ::
163+
{:ok, Bounty.t()} | {:error, atom()}
164+
def claim_bounty(
165+
%{
166+
user: user,
167+
ticket_ref: %{owner: repo_owner, repo: repo_name, number: number} = ticket_ref,
168+
pull_request: pull_request
169+
},
170+
opts \\ []
171+
) do
172+
installation_id = opts[:installation_id]
173+
174+
token_res =
175+
if installation_id,
176+
do: Github.get_installation_token(installation_id),
177+
else: Accounts.get_access_token(user)
178+
179+
Repo.transact(fn ->
180+
with {:ok, token} <- token_res,
181+
{:ok, ticket} <- Workspace.ensure_ticket(token, repo_owner, repo_name, number),
182+
{:ok, claim} <- do_claim_bounty(%{user: user, ticket: ticket, pull_request: pull_request}),
183+
{:ok, _job} <- notify_claim(%{ticket_ref: ticket_ref}, installation_id: installation_id) do
184+
broadcast()
185+
{:ok, claim}
186+
else
187+
{:error, _reason} = error -> error
188+
end
189+
end)
190+
end
191+
192+
@spec notify_claim(
193+
%{ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()}},
194+
opts :: [installation_id: integer()]
195+
) ::
196+
{:ok, Oban.Job.t()} | {:error, atom()}
197+
def notify_claim(%{ticket_ref: ticket_ref}, opts \\ []) do
198+
%{
199+
ticket_ref: %{owner: ticket_ref.owner, repo: ticket_ref.repo, number: ticket_ref.number},
200+
installation_id: opts[:installation_id]
201+
}
202+
|> Jobs.NotifyClaim.new()
203+
|> Oban.insert()
204+
end
205+
124206
@spec create_tip_intent(
125207
%{
126208
recipient: String.t(),
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule Algora.Bounties.Jobs.NotifyClaim do
2+
@moduledoc false
3+
use Oban.Worker, queue: :notify_claim
4+
5+
alias Algora.Github
6+
7+
require Logger
8+
9+
@impl Oban.Worker
10+
def perform(%Oban.Job{args: %{"ticket_ref" => _ticket_ref, "installation_id" => nil}}) do
11+
:ok
12+
end
13+
14+
@impl Oban.Worker
15+
def perform(%Oban.Job{args: %{"ticket_ref" => ticket_ref, "installation_id" => installation_id}}) do
16+
with {:ok, token} <- Github.get_installation_token(installation_id) do
17+
body = "Claimed!"
18+
19+
Github.create_issue_comment(
20+
token,
21+
ticket_ref["owner"],
22+
ticket_ref["repo"],
23+
ticket_ref["number"],
24+
body
25+
)
26+
end
27+
end
28+
end

lib/algora/bounties/schemas/bounty.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ defmodule Algora.Bounties.Bounty do
1414
belongs_to :owner, User
1515
belongs_to :creator, User
1616
has_many :attempts, Algora.Bounties.Attempt
17-
has_many :claims, Algora.Bounties.Claim
1817
has_many :transactions, Algora.Payments.Transaction
1918

2019
timestamps()

lib/algora/bounties/schemas/claim.ex

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,64 @@ defmodule Algora.Bounties.Claim do
66

77
@derive {Inspect, except: [:provider_meta]}
88
typed_schema "claims" do
9-
field :provider, :string
10-
field :provider_id, :string
11-
field :provider_meta, :map
12-
13-
field :type, Ecto.Enum, values: [:code, :video, :design, :article]
14-
15-
field :status, Ecto.Enum, values: [:pending, :merged, :approved, :rejected, :charged, :paid]
9+
field :provider, :string, null: false
10+
field :provider_id, :string, null: false
11+
field :provider_meta, :map, null: false
1612

1713
field :merged_at, :utc_datetime_usec
1814
field :approved_at, :utc_datetime_usec
1915
field :rejected_at, :utc_datetime_usec
2016
field :charged_at, :utc_datetime_usec
2117
field :paid_at, :utc_datetime_usec
2218

23-
field :title, :string
19+
field :title, :string, null: false
2420
field :description, :string
25-
field :url, :string
26-
field :group_id, :string
21+
field :url, :string, null: false
22+
field :group_id, :string, null: false
2723

28-
belongs_to :bounty, Algora.Bounties.Bounty
29-
belongs_to :user, Algora.Accounts.User
24+
belongs_to :ticket, Algora.Workspace.Ticket, null: false
25+
belongs_to :user, Algora.Accounts.User, null: false
3026
# has_one :transaction, Algora.Payments.Transaction
3127

3228
timestamps()
3329
end
3430

3531
def changeset(claim, attrs) do
3632
claim
37-
|> cast(attrs, [:bounty_id, :user_id])
38-
|> validate_required([:bounty_id, :user_id])
33+
|> cast(attrs, [
34+
:ticket_id,
35+
:user_id,
36+
:provider,
37+
:provider_id,
38+
:provider_meta,
39+
:merged_at,
40+
:approved_at,
41+
:rejected_at,
42+
:charged_at,
43+
:paid_at,
44+
:title,
45+
:description,
46+
:url,
47+
:group_id
48+
])
49+
|> validate_required([
50+
:ticket_id,
51+
:user_id,
52+
:provider,
53+
:provider_id,
54+
:provider_meta,
55+
:title,
56+
:url,
57+
:group_id
58+
])
59+
|> foreign_key_constraint(:ticket_id)
60+
|> foreign_key_constraint(:user_id)
61+
|> unique_constraint([:ticket_id, :user_id])
3962
end
4063

4164
def rewarded(query \\ Claim) do
4265
from c in query,
43-
where: c.status == :approved and not is_nil(c.charged_at)
66+
where: c.state == :approved and not is_nil(c.charged_at)
4467
end
4568

4669
def filter_by_org_id(query, nil), do: query

lib/algora/shared/util.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ defmodule Algora.Util do
4545
date |> DateTime.shift_zone!(timezone) |> Calendar.strftime("%Y-%m-%d %I:%M %p")
4646
end
4747

48+
def to_date(nil), do: nil
49+
50+
def to_date(date) do
51+
case DateTime.from_iso8601(date) do
52+
{:ok, datetime, _offset} -> datetime
53+
{:error, _reason} = error -> error
54+
end
55+
end
56+
4857
def format_pct(percentage) do
4958
percentage
5059
|> Decimal.mult(100)

lib/algora/workspace/schemas/ticket.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ defmodule Algora.Workspace.Ticket do
1818

1919
belongs_to :repository, Algora.Workspace.Repository
2020
has_many :bounties, Algora.Bounties.Bounty
21+
has_many :claims, Algora.Bounties.Claim
2122

2223
timestamps()
2324
end

lib/algora_web/controllers/webhooks/github_controller.ex

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,21 @@ defmodule AlgoraWeb.Webhooks.GithubController do
113113
end
114114
end
115115

116-
defp execute_command({:claim, args}, _author, _params) when not is_nil(args) do
117-
owner = Keyword.get(args, :owner)
118-
repo = Keyword.get(args, :repo)
119-
number = Keyword.get(args, :number)
116+
defp execute_command({:claim, args}, author, params) when not is_nil(args) do
117+
installation_id = params["installation"]["id"]
118+
pull_request = params["pull_request"]
120119

121-
Logger.info("Claim #{owner}/#{repo}##{number}")
120+
with {:ok, token} <- Github.get_installation_token(installation_id),
121+
{:ok, user} <- Workspace.ensure_user(token, author["login"]) do
122+
Bounties.claim_bounty(
123+
%{
124+
user: user,
125+
ticket_ref: %{owner: args[:owner], repo: args[:repo], number: args[:number]},
126+
pull_request: pull_request
127+
},
128+
installation_id: installation_id
129+
)
130+
end
122131
end
123132

124133
defp execute_command({command, _} = args, _author, _params),
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
defmodule Algora.Repo.Migrations.RecreateClaims do
2+
use Ecto.Migration
3+
4+
def up do
5+
drop index(:claims, [:bounty_id])
6+
drop index(:claims, [:user_id])
7+
drop table(:claims)
8+
9+
create table(:claims) do
10+
add :provider, :string, null: false
11+
add :provider_id, :string, null: false
12+
add :provider_meta, :map, null: false
13+
14+
add :opened_at, :utc_datetime_usec
15+
add :merged_at, :utc_datetime_usec
16+
add :approved_at, :utc_datetime_usec
17+
add :rejected_at, :utc_datetime_usec
18+
add :charged_at, :utc_datetime_usec
19+
add :paid_at, :utc_datetime_usec
20+
21+
add :title, :string, null: false
22+
add :description, :string
23+
add :url, :string, null: false
24+
add :group_id, :string, null: false
25+
26+
add :ticket_id, references(:tickets, on_delete: :nothing), null: false
27+
add :user_id, references(:users, on_delete: :nothing), null: false
28+
29+
timestamps()
30+
end
31+
32+
create unique_index(:claims, [:ticket_id, :user_id])
33+
create index(:claims, [:ticket_id])
34+
create index(:claims, [:user_id])
35+
end
36+
37+
def down do
38+
drop unique_index(:claims, [:ticket_id, :user_id])
39+
drop index(:claims, [:ticket_id])
40+
drop index(:claims, [:user_id])
41+
drop table(:claims)
42+
43+
create table(:claims) do
44+
add :provider, :string
45+
add :provider_id, :string
46+
add :provider_meta, :map
47+
48+
add :type, :string
49+
50+
add :status, :string
51+
52+
add :merged_at, :utc_datetime_usec
53+
add :approved_at, :utc_datetime_usec
54+
add :rejected_at, :utc_datetime_usec
55+
add :charged_at, :utc_datetime_usec
56+
add :paid_at, :utc_datetime_usec
57+
58+
add :title, :string
59+
add :description, :string
60+
add :url, :string
61+
add :group_id, :string
62+
63+
add :bounty_id, references(:bounties, on_delete: :nothing)
64+
add :user_id, references(:users, on_delete: :nothing)
65+
66+
timestamps()
67+
end
68+
69+
create index(:claims, [:bounty_id])
70+
create index(:claims, [:user_id])
71+
end
72+
end

priv/repo/seeds.exs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,6 @@ for {repo_name, issues} <- repos do
399399
insert!(:claim, %{
400400
bounty_id: bounty.id,
401401
user_id: carver.id,
402-
status: if(paid, do: :paid, else: :pending),
403402
title: "Implementation for #{issue_title}",
404403
description: "Here's my solution to this issue.",
405404
url: "https://github.com/piedpiper/#{repo_name}/pull/#{index}"

0 commit comments

Comments
 (0)