Skip to content

Commit b374adc

Browse files
committed
feat: repurpose custom bounties as contracts (#120)
1 parent ea563b3 commit b374adc

File tree

14 files changed

+998
-54
lines changed

14 files changed

+998
-54
lines changed

config/config.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ config :algora,
2323
{"/create/org", "/onboarding/org"},
2424
{"/solve", "/onboarding/dev"},
2525
{"/onboarding/solver", "/onboarding/dev"},
26+
{"/:org/contract/:id", "/:org/contracts/:id"},
2627
{"/org/*path", "/*path"},
2728
{"/@/:handle", "/:handle/profile"}
2829
]

lib/algora/bounties/bounties.ex

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ defmodule Algora.Bounties do
5151
amount: Money.t(),
5252
ticket: Ticket.t(),
5353
visibility: Bounty.visibility(),
54-
shared_with: [String.t()]
54+
shared_with: [String.t()],
55+
hours_per_week: integer() | nil
5556
}) ::
5657
{:ok, Bounty.t()} | {:error, atom()}
5758
defp do_create_bounty(%{creator: creator, owner: owner, amount: amount, ticket: ticket} = params) do
@@ -62,7 +63,8 @@ defmodule Algora.Bounties do
6263
owner_id: owner.id,
6364
creator_id: creator.id,
6465
visibility: params[:visibility] || owner.bounty_mode,
65-
shared_with: params[:shared_with] || []
66+
shared_with: params[:shared_with] || [],
67+
hours_per_week: params[:hours_per_week]
6668
})
6769

6870
changeset
@@ -109,7 +111,8 @@ defmodule Algora.Bounties do
109111
command_id: integer(),
110112
command_source: :ticket | :comment,
111113
visibility: Bounty.visibility() | nil,
112-
shared_with: [String.t()] | nil
114+
shared_with: [String.t()] | nil,
115+
hours_per_week: integer() | nil
113116
]
114117
) ::
115118
{:ok, Bounty.t()} | {:error, atom()}
@@ -140,7 +143,8 @@ defmodule Algora.Bounties do
140143
amount: amount,
141144
ticket: ticket,
142145
visibility: opts[:visibility],
143-
shared_with: shared_with
146+
shared_with: shared_with,
147+
hours_per_week: opts[:hours_per_week]
144148
})
145149

146150
:set ->
@@ -190,7 +194,8 @@ defmodule Algora.Bounties do
190194
opts :: [
191195
strategy: strategy(),
192196
visibility: Bounty.visibility() | nil,
193-
shared_with: [String.t()] | nil
197+
shared_with: [String.t()] | nil,
198+
hours_per_week: integer() | nil
194199
]
195200
) ::
196201
{:ok, Bounty.t()} | {:error, atom()}
@@ -209,7 +214,8 @@ defmodule Algora.Bounties do
209214
amount: amount,
210215
ticket: ticket,
211216
visibility: opts[:visibility],
212-
shared_with: shared_with
217+
shared_with: shared_with,
218+
hours_per_week: opts[:hours_per_week]
213219
}),
214220
{:ok, _job} <- notify_bounty(%{owner: owner, bounty: bounty}) do
215221
broadcast()
@@ -862,6 +868,7 @@ defmodule Algora.Bounties do
862868
initialize_charge(%{
863869
id: Nanoid.generate(),
864870
user_id: owner.id,
871+
bounty_id: opts[:bounty_id],
865872
gross_amount: gross_amount,
866873
net_amount: amount,
867874
total_fee: Money.sub!(gross_amount, amount),
@@ -961,23 +968,26 @@ defmodule Algora.Bounties do
961968
end)
962969
end
963970

964-
defp initialize_charge(%{
965-
id: id,
966-
user_id: user_id,
967-
gross_amount: gross_amount,
968-
net_amount: net_amount,
969-
total_fee: total_fee,
970-
line_items: line_items,
971-
group_id: group_id,
972-
idempotency_key: idempotency_key
973-
}) do
971+
defp initialize_charge(
972+
%{
973+
id: id,
974+
user_id: user_id,
975+
gross_amount: gross_amount,
976+
net_amount: net_amount,
977+
total_fee: total_fee,
978+
line_items: line_items,
979+
group_id: group_id,
980+
idempotency_key: idempotency_key
981+
} = params
982+
) do
974983
%Transaction{}
975984
|> change(%{
976985
id: id,
977986
provider: "stripe",
978987
type: :charge,
979988
status: :initialized,
980989
user_id: user_id,
990+
bounty_id: params[:bounty_id],
981991
gross_amount: gross_amount,
982992
net_amount: net_amount,
983993
total_fee: total_fee,

lib/algora/bounties/schemas/bounty.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ defmodule Algora.Bounties.Bounty do
1515
field :visibility, Ecto.Enum, values: [:community, :exclusive, :public], null: false, default: :community
1616
field :shared_with, {:array, :string}, null: false, default: []
1717
field :deadline, :utc_datetime_usec
18+
field :hours_per_week, :integer
1819

1920
belongs_to :ticket, Algora.Workspace.Ticket
2021
belongs_to :owner, User
@@ -33,7 +34,7 @@ defmodule Algora.Bounties.Bounty do
3334

3435
def changeset(bounty, attrs) do
3536
bounty
36-
|> cast(attrs, [:amount, :ticket_id, :owner_id, :creator_id, :visibility, :shared_with])
37+
|> cast(attrs, [:amount, :ticket_id, :owner_id, :creator_id, :visibility, :shared_with, :hours_per_week])
3738
|> validate_required([:amount, :ticket_id, :owner_id, :creator_id])
3839
|> generate_id()
3940
|> foreign_key_constraint(:ticket)
@@ -67,6 +68,8 @@ defmodule Algora.Bounties.Bounty do
6768
Algora.Util.path_from_url(url)
6869
end
6970

71+
def path(_bounty), do: nil
72+
7073
def full_path(%{repository: %{name: name, owner: %{login: login}}, ticket: %{number: number}}) do
7174
"#{login}/#{name}##{number}"
7275
end

lib/algora/organizations/schemas/member.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,6 @@ defmodule Algora.Organizations.Member do
3636
end
3737

3838
def can_create_bounty?(role), do: role in [:admin, :mod]
39+
40+
def can_create_contract?(role), do: role in [:admin, :mod]
3941
end

lib/algora/shared/validations.ex

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,33 @@ defmodule Algora.Validations do
4040
end
4141
end
4242

43+
def validate_github_handle(changeset, field, embed_field \\ nil) do
44+
case get_change(changeset, field) do
45+
handle when not is_nil(handle) ->
46+
# Check if user is already embedded with matching provider_login
47+
existing_user = embed_field && get_field(changeset, embed_field)
48+
49+
if existing_user && existing_user.provider_login == handle do
50+
changeset
51+
else
52+
case Algora.Workspace.ensure_user(Algora.Admin.token!(), handle) do
53+
{:ok, user} ->
54+
if embed_field do
55+
put_embed(changeset, embed_field, user)
56+
else
57+
changeset
58+
end
59+
60+
{:error, error, _, _, _, _} ->
61+
add_error(changeset, field, error)
62+
end
63+
end
64+
65+
_ ->
66+
changeset
67+
end
68+
end
69+
4370
def validate_date_in_future(changeset, field) do
4471
validate_change(changeset, field, fn _, date ->
4572
if date && Date.before?(date, DateTime.utc_now()) do

lib/algora_web/components/layouts/user.html.heex

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -135,35 +135,6 @@
135135
</ul>
136136
<% end %>
137137
</nav>
138-
<%= if main_bounty_form = Map.get(assigns, :main_bounty_form) do %>
139-
<div class="mt-auto mx-auto">
140-
<.button
141-
phx-click="open_main_bounty_form"
142-
class="h-9 w-9 rounded-md flex items-center justify-center relative"
143-
>
144-
<.icon name="tabler-diamond" class="h-6 w-6 shrink-0" />
145-
<.icon
146-
name="tabler-plus"
147-
class="h-[0.8rem] w-[0.8rem] shrink-0 absolute bottom-[0.2rem] right-[0.2rem]"
148-
/>
149-
</.button>
150-
<.drawer
151-
show={@main_bounty_form_open?}
152-
direction="right"
153-
on_cancel="close_main_bounty_form"
154-
>
155-
<.drawer_header>
156-
<.drawer_title>Create new bounty</.drawer_title>
157-
<.drawer_description>
158-
Create and fund a bounty for an issue
159-
</.drawer_description>
160-
</.drawer_header>
161-
<.drawer_content class="mt-4">
162-
<AlgoraWeb.Forms.BountyForm.bounty_form form={main_bounty_form} />
163-
</.drawer_content>
164-
</.drawer>
165-
</div>
166-
<% end %>
167138
</div>
168139

169140
<div class="lg:pl-16">
@@ -237,6 +208,60 @@
237208
</div>
238209
</.link>
239210
<%= if @current_user do %>
211+
<%= if main_contract_form = Map.get(assigns, :main_contract_form) do %>
212+
<div>
213+
<.button
214+
phx-click="open_main_contract_form"
215+
class="h-9 w-9 rounded-md flex items-center justify-center relative"
216+
>
217+
<.icon name="tabler-user-dollar" class="h-6 w-6 shrink-0" />
218+
</.button>
219+
<.drawer
220+
show={@main_contract_form_open?}
221+
direction="right"
222+
on_cancel="close_main_contract_form"
223+
>
224+
<.drawer_header>
225+
<.drawer_title>Create new contract</.drawer_title>
226+
<.drawer_description>
227+
Engage a developer for ongoing work
228+
</.drawer_description>
229+
</.drawer_header>
230+
<.drawer_content class="mt-4">
231+
<AlgoraWeb.Forms.ContractForm.contract_form form={main_contract_form} />
232+
</.drawer_content>
233+
</.drawer>
234+
</div>
235+
<% end %>
236+
<%= if main_bounty_form = Map.get(assigns, :main_bounty_form) do %>
237+
<div>
238+
<.button
239+
phx-click="open_main_bounty_form"
240+
class="h-9 w-9 rounded-md flex items-center justify-center relative"
241+
>
242+
<.icon name="tabler-diamond" class="h-6 w-6 shrink-0" />
243+
<.icon
244+
name="tabler-plus"
245+
class="h-[0.8rem] w-[0.8rem] shrink-0 absolute bottom-[0.2rem] right-[0.2rem]"
246+
/>
247+
</.button>
248+
<.drawer
249+
show={@main_bounty_form_open?}
250+
direction="right"
251+
on_cancel="close_main_bounty_form"
252+
>
253+
<.drawer_header>
254+
<.drawer_title>Create new bounty</.drawer_title>
255+
<.drawer_description>
256+
Create and fund a bounty for an issue
257+
</.drawer_description>
258+
</.drawer_header>
259+
<.drawer_content class="mt-4">
260+
<AlgoraWeb.Forms.BountyForm.bounty_form form={main_bounty_form} />
261+
</.drawer_content>
262+
</.drawer>
263+
</div>
264+
<% end %>
240265
<%!-- {live_render(@socket, AlgoraWeb.Activity.UserNavTimelineLive,
241266
id: "activity-timeline",
242267
session: %{},

0 commit comments

Comments
 (0)