Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
56061cf
adding new contract flow
zcesur Apr 17, 2025
96d1521
implement authorization flow
zcesur Apr 17, 2025
2b6cb55
handle uncaptured charges
zcesur Apr 17, 2025
998a730
handle release flow
zcesur Apr 17, 2025
a402b82
implement 3 step flow
zcesur Apr 17, 2025
b2b5acb
feat: enhance contributor queries to exclude organization members
zcesur Apr 17, 2025
1cd1d40
refactor: update contract form labels and enhance dashboard live cond…
zcesur Apr 17, 2025
0eee9e9
add missing event
zcesur Apr 18, 2025
46d7110
add missing typespecs for params
zcesur Apr 18, 2025
7fcbc63
add missing mock fields
zcesur Apr 18, 2025
1846814
display user card only on marketplace mode
zcesur Apr 18, 2025
2a77d99
fix: handle nil case
zcesur Apr 18, 2025
8765771
update finalize contract steps
zcesur Apr 18, 2025
25e0bd6
hide buttons based on current_user
zcesur Apr 18, 2025
2fc0131
style: dashboard buttons
zcesur Apr 18, 2025
a7b470d
add missing forms
zcesur Apr 19, 2025
31c6706
handle drawer events in preview nav
zcesur Apr 19, 2025
f5366a1
update user layout to place drawers on top level
zcesur Apr 19, 2025
0f120cb
fix og
zcesur Apr 19, 2025
b39f1a7
remove unused vars
zcesur Apr 19, 2025
6611aa4
update match card to show total cost per week
zcesur Apr 22, 2025
7fba6ab
misc improvements
zcesur Apr 22, 2025
0212f7d
in midst of updating matches section
zcesur Apr 22, 2025
577a7d8
misc improvements
zcesur Apr 22, 2025
f05492e
allow any payment at any time after initial contract cycle
zcesur Apr 22, 2025
0693041
feat: optimize go page for screenshot
zcesur Apr 23, 2025
18cf103
add make payment btn on initial contract page
zcesur Apr 23, 2025
79e46f5
use manual flow for contract bounties
zcesur Apr 23, 2025
a076dfa
log all admin alerts
zcesur Apr 23, 2025
35255a4
feat: add contract type to bounties and update related forms and schemas
zcesur Apr 23, 2025
a70ac05
simplify code
zcesur Apr 23, 2025
fccdb02
add admin fn to release payment
zcesur Apr 23, 2025
93f47b5
adapt line items based on contract type
zcesur Apr 23, 2025
3bb4366
remove redundant param
zcesur Apr 23, 2025
a9a970f
redirect user back to contrat upon payment
zcesur Apr 23, 2025
6291cae
misc contract calc
zcesur Apr 23, 2025
c69dda6
delete obsolete assertion
zcesur Apr 24, 2025
2446934
Merge branch 'main' of github.com:algora-io/algora into feat/contracts
zcesur Apr 24, 2025
f470358
update copy
zcesur Apr 24, 2025
7c30f9a
add missing session check
zcesur Apr 24, 2025
d234139
fix screenshot param
zcesur Apr 24, 2025
fd2801b
misc
zcesur Apr 24, 2025
a87883e
make email campaign dynamic
zcesur Apr 24, 2025
17787c2
miscellanea
zcesur Apr 24, 2025
5d553a4
add bcc
zcesur Apr 24, 2025
e776892
use scheduled_at instead of schedule_in
zcesur Apr 24, 2025
bbfe7f6
misc
zcesur Apr 24, 2025
d1bd19d
conditionally display matches
zcesur Apr 25, 2025
2a1ac5b
update visibility of GitHub star link and adjust responsive classes
zcesur Apr 25, 2025
646f796
Merge branch 'main' of github.com:algora-io/algora into feat/contracts
zcesur Apr 25, 2025
643e586
remove bcc
zcesur Apr 25, 2025
9ad6340
Merge branch 'main' of github.com:algora-io/algora into feat/contracts
zcesur Apr 25, 2025
38a9de6
misc
zcesur Apr 25, 2025
f18ac93
Merge branch 'main' of github.com:algora-io/algora into feat/contracts
zcesur Apr 25, 2025
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
4 changes: 3 additions & 1 deletion lib/algora/accounts/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,15 @@ defmodule Algora.Accounts do
where: tx.type == :credit,
where: tx.status == :succeeded,
where: tx.user_id == ^user.id,
join: ltx in assoc(tx, :linked_transaction),
left_join: bounty in assoc(tx, :bounty),
left_join: tip in assoc(tx, :tip),
join: t in Ticket,
on: t.id == bounty.ticket_id or t.id == tip.ticket_id,
left_join: r in assoc(t, :repository),
as: :r,
left_join: ro in assoc(r, :user),
left_join: ro in User,
on: fragment("? = (case when ? is null then ? else ? end)", ro.id, r.user_id, ltx.user_id, r.user_id),
# order_by: ^[desc: order_by],
order_by: [desc: sum(tx.net_amount)],
group_by: [ro.id],
Expand Down
1 change: 1 addition & 0 deletions lib/algora/activities/schemas/activity.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule Algora.Activities.Activity do
contract_renewed
identity_created
user_migrated
user_online
bounty_posted
bounty_repriced
claim_submitted
Expand Down
45 changes: 45 additions & 0 deletions lib/algora/admin/admin.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule Algora.Admin do
alias Algora.Github
alias Algora.Parser
alias Algora.Payments
alias Algora.Payments.Transaction
alias Algora.Repo
alias Algora.Util
alias Algora.Workspace
Expand All @@ -21,6 +22,48 @@ defmodule Algora.Admin do

require Logger

def release_payment(tx_id) do
Repo.transact(fn ->
{_, [tx]} =
Repo.update_all(from(t in Transaction, where: t.id == ^tx_id, select: t),
set: [status: :succeeded, succeeded_at: DateTime.utc_now()]
)

Repo.update_all(from(b in Bounty, where: b.id == ^tx.bounty_id), set: [status: :paid])

activities_result = Repo.insert_activity(tx, %{type: :transaction_succeeded, notify_users: [tx.user_id]})

jobs_result =
case Payments.fetch_active_account(tx.user_id) do
{:ok, _account} ->
%{credit_id: tx.id}
|> Payments.Jobs.ExecutePendingTransfer.new()
|> Oban.insert()

{:error, :no_active_account} ->
Logger.warning("No active account for user #{tx.user_id}")

%{credit_id: tx.id}
|> Bounties.Jobs.PromptPayoutConnect.new()
|> Oban.insert()
end

with {:ok, _} <- activities_result,
{:ok, _} <- jobs_result do
Payments.broadcast()
{:ok, nil}
else
{:error, reason} ->
Logger.error("Failed to update transactions: #{inspect(reason)}")
{:error, :failed_to_update_transactions}

error ->
Logger.error("Failed to update transactions: #{inspect(error)}")
{:error, :failed_to_update_transactions}
end
end)
end

def refresh_bounty(url) do
with %{owner: owner, repo: repo, number: number} <- parse_ticket_url(url),
{:ok, ticket} <- Workspace.ensure_ticket(token_for(owner), owner, repo, number) do
Expand Down Expand Up @@ -396,6 +439,8 @@ defmodule Algora.Admin do
end

def alert(message, severity) do
Logger.info(message)

%{
payload: %{
embeds: [
Expand Down
169 changes: 125 additions & 44 deletions lib/algora/bounties/bounties.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ defmodule Algora.Bounties do
ticket: Ticket.t(),
visibility: Bounty.visibility(),
shared_with: [String.t()],
hours_per_week: integer() | nil
hours_per_week: integer() | nil,
hourly_rate: Money.t() | nil,
contract_type: Bounty.contract_type() | nil
}) ::
{:ok, Bounty.t()} | {:error, atom()}
defp do_create_bounty(%{creator: creator, owner: owner, amount: amount, ticket: ticket} = params) do
Expand All @@ -64,7 +66,9 @@ defmodule Algora.Bounties do
creator_id: creator.id,
visibility: params[:visibility] || owner.bounty_mode,
shared_with: params[:shared_with] || [],
hours_per_week: params[:hours_per_week]
hours_per_week: params[:hours_per_week],
hourly_rate: params[:hourly_rate],
contract_type: params[:contract_type]
})

changeset
Expand Down Expand Up @@ -112,7 +116,9 @@ defmodule Algora.Bounties do
command_source: :ticket | :comment,
visibility: Bounty.visibility() | nil,
shared_with: [String.t()] | nil,
hours_per_week: integer() | nil
hourly_rate: Money.t() | nil,
hours_per_week: integer() | nil,
contract_type: Bounty.contract_type() | nil
]
) ::
{:ok, Bounty.t()} | {:error, atom()}
Expand Down Expand Up @@ -144,7 +150,9 @@ defmodule Algora.Bounties do
ticket: ticket,
visibility: opts[:visibility],
shared_with: shared_with,
hours_per_week: opts[:hours_per_week]
hourly_rate: opts[:hourly_rate],
hours_per_week: opts[:hours_per_week],
contract_type: opts[:contract_type]
})

:set ->
Expand Down Expand Up @@ -195,7 +203,9 @@ defmodule Algora.Bounties do
strategy: strategy(),
visibility: Bounty.visibility() | nil,
shared_with: [String.t()] | nil,
hours_per_week: integer() | nil
hours_per_week: integer() | nil,
hourly_rate: Money.t() | nil,
contract_type: Bounty.contract_type() | nil
]
) ::
{:ok, Bounty.t()} | {:error, atom()}
Expand All @@ -215,7 +225,9 @@ defmodule Algora.Bounties do
ticket: ticket,
visibility: opts[:visibility],
shared_with: shared_with,
hours_per_week: opts[:hours_per_week]
hours_per_week: opts[:hours_per_week],
hourly_rate: opts[:hourly_rate],
contract_type: opts[:contract_type]
}),
{:ok, _job} <- notify_bounty(%{owner: owner, bounty: bounty}) do
broadcast()
Expand Down Expand Up @@ -769,7 +781,12 @@ defmodule Algora.Bounties do
bounty: Bounty.t(),
claims: [Claim.t()]
},
opts :: [ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()}, recipient: User.t()]
opts :: [
ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()},
recipient: User.t(),
success_url: String.t(),
cancel_url: String.t()
]
) ::
{:ok, String.t()} | {:error, atom()}
def reward_bounty(%{owner: owner, amount: amount, bounty: bounty, claims: claims}, opts \\ []) do
Expand All @@ -778,7 +795,37 @@ defmodule Algora.Bounties do
ticket_ref: opts[:ticket_ref],
bounty: bounty,
claims: claims,
recipient: opts[:recipient]
recipient: opts[:recipient],
success_url: opts[:success_url],
cancel_url: opts[:cancel_url]
)
end

@spec authorize_payment(
%{
owner: User.t(),
amount: Money.t(),
bounty: Bounty.t(),
claims: [Claim.t()]
},
opts :: [
ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()},
recipient: User.t(),
success_url: String.t(),
cancel_url: String.t()
]
) ::
{:ok, String.t()} | {:error, atom()}
def authorize_payment(%{owner: owner, amount: amount, bounty: bounty, claims: claims}, opts \\ []) do
create_payment_session(
%{owner: owner, amount: amount, description: "Bounty payment for OSS contributions"},
ticket_ref: opts[:ticket_ref],
bounty: bounty,
claims: claims,
recipient: opts[:recipient],
capture_method: :manual,
success_url: opts[:success_url],
cancel_url: opts[:cancel_url]
)
end

Expand All @@ -801,61 +848,81 @@ defmodule Algora.Bounties do
description = if(ticket_ref, do: "#{ticket_ref[:repo]}##{ticket_ref[:number]}")

platform_fee_pct =
if bounty && Date.before?(bounty.inserted_at, ~D[2025-04-16]) do
if bounty && Date.before?(bounty.inserted_at, ~D[2025-04-16]) && is_nil(bounty.contract_type) do
Decimal.div(owner.fee_pct_prev, 100)
else
Decimal.div(owner.fee_pct, 100)
end

transaction_fee_pct = Payments.get_transaction_fee_pct()

payouts =
if recipient do
case opts[:bounty] do
%{contract_type: :marketplace} ->
[
%LineItem{
amount: amount,
title: "Payment to @#{recipient.provider_login}",
description: description,
title: "Contract payment - @#{recipient.provider_login}",
description: "(includes all platform and payment processing fees)",
image: recipient.avatar_url,
type: :payout
}
]
else
Enum.map(claims, fn claim ->
%LineItem{
# TODO: ensure shares are normalized
amount: Money.mult!(amount, claim.group_share),
title: "Payment to @#{claim.user.provider_login}",
description: description,
image: claim.user.avatar_url,
type: :payout
}
end)
end

payouts ++
[
%LineItem{
amount: Money.mult!(amount, platform_fee_pct),
title: "Algora platform fee (#{Util.format_pct(platform_fee_pct)})",
type: :fee
},
%LineItem{
amount: Money.mult!(amount, transaction_fee_pct),
title: "Transaction fee (#{Util.format_pct(transaction_fee_pct)})",
type: :fee
}
]
_ ->
if recipient do
[
%LineItem{
amount: amount,
title: "Payment to @#{recipient.provider_login}",
description: description,
image: recipient.avatar_url,
type: :payout
}
]
else
Enum.map(claims, fn claim ->
%LineItem{
# TODO: ensure shares are normalized
amount: Money.mult!(amount, claim.group_share),
title: "Payment to @#{claim.user.provider_login}",
description: description,
image: claim.user.avatar_url,
type: :payout
}
end)
end ++
[
%LineItem{
amount: Money.mult!(amount, platform_fee_pct),
title: "Algora platform fee (#{Util.format_pct(platform_fee_pct)})",
type: :fee
},
%LineItem{
amount: Money.mult!(amount, transaction_fee_pct),
title: "Transaction fee (#{Util.format_pct(transaction_fee_pct)})",
type: :fee
}
]
end
end

def calculate_contract_amount(amount), do: Money.mult!(amount, Decimal.new("1.13"))

def final_contract_amount(:marketplace, amount), do: amount

def final_contract_amount(:bring_your_own, amount), do: calculate_contract_amount(amount)

@spec create_payment_session(
%{owner: User.t(), amount: Money.t(), description: String.t()},
opts :: [
ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()},
tip_id: String.t(),
bounty: Bounty.t(),
claims: [Claim.t()],
recipient: User.t()
recipient: User.t(),
capture_method: :automatic | :automatic_async | :manual,
success_url: String.t(),
cancel_url: String.t()
]
) ::
{:ok, String.t()} | {:error, atom()}
Expand All @@ -870,6 +937,19 @@ defmodule Algora.Bounties do
bounty: opts[:bounty]
)

payment_intent_data = %{
description: description,
metadata: %{"version" => Payments.metadata_version(), "group_id" => tx_group_id}
}

{payment_intent_data, session_opts} =
if capture_method = opts[:capture_method] do
{Map.put(payment_intent_data, :capture_method, capture_method),
[success_url: opts[:success_url], cancel_url: opts[:cancel_url]]}
else
{payment_intent_data, []}
end

gross_amount = LineItem.gross_amount(line_items)

bounty_id = if bounty = opts[:bounty], do: bounty.id
Expand Down Expand Up @@ -899,10 +979,12 @@ defmodule Algora.Bounties do
group_id: tx_group_id
}),
{:ok, session} <-
Payments.create_stripe_session(owner, Enum.map(line_items, &LineItem.to_stripe/1), %{
description: description,
metadata: %{"version" => Payments.metadata_version(), "group_id" => tx_group_id}
}) do
Payments.create_stripe_session(
owner,
Enum.map(line_items, &LineItem.to_stripe/1),
payment_intent_data,
session_opts
) do
{:ok, session.url}
end
end)
Expand Down Expand Up @@ -1012,7 +1094,6 @@ defmodule Algora.Bounties do
})
|> Algora.Validations.validate_positive(:gross_amount)
|> Algora.Validations.validate_positive(:net_amount)
|> Algora.Validations.validate_positive(:total_fee)
|> foreign_key_constraint(:user_id)
|> unique_constraint([:idempotency_key])
|> Repo.insert()
Expand Down
Loading