From c85b00c3be5a7e095f7b4bb3d963ee4f68e90d72 Mon Sep 17 00:00:00 2001 From: zafer Date: Fri, 2 May 2025 18:42:46 +0300 Subject: [PATCH 1/5] update contract flow --- lib/algora/bounties/bounties.ex | 4 +- lib/algora_web/forms/contract_form.ex | 4 +- lib/algora_web/live/contract_live.ex | 158 ++++++++++++++++++++-- lib/algora_web/live/org/dashboard_live.ex | 22 +-- lib/algora_web/live/org/nav.ex | 6 +- 5 files changed, 159 insertions(+), 35 deletions(-) diff --git a/lib/algora/bounties/bounties.ex b/lib/algora/bounties/bounties.ex index e69b680da..a9bc44ce6 100644 --- a/lib/algora/bounties/bounties.ex +++ b/lib/algora/bounties/bounties.ex @@ -906,11 +906,9 @@ defmodule Algora.Bounties do 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) + def final_contract_amount(:bring_your_own, amount), do: Money.mult!(amount, Decimal.new("1.13")) @spec create_payment_session( %{owner: User.t(), amount: Money.t(), description: String.t()}, diff --git a/lib/algora_web/forms/contract_form.ex b/lib/algora_web/forms/contract_form.ex index dd4aa8daf..cb04d5d71 100644 --- a/lib/algora_web/forms/contract_form.ex +++ b/lib/algora_web/forms/contract_form.ex @@ -225,9 +225,7 @@ defmodule AlgoraWeb.Forms.ContractForm do
- {Money.to_string!( - Bounties.calculate_contract_amount(get_change(@form.source, :amount)) - )} + {Money.to_string!(get_change(@form.source, :amount))}
diff --git a/lib/algora_web/live/contract_live.ex b/lib/algora_web/live/contract_live.ex index c38627bd6..4b9a7c545 100644 --- a/lib/algora_web/live/contract_live.ex +++ b/lib/algora_web/live/contract_live.ex @@ -42,6 +42,25 @@ defmodule AlgoraWeb.ContractLive do end end + defmodule ReleaseBountyForm do + @moduledoc false + use Ecto.Schema + + import Ecto.Changeset + + @primary_key false + embedded_schema do + field :amount, USD + end + + def changeset(form, attrs) do + form + |> cast(attrs, [:amount]) + |> validate_required([:amount]) + |> Algora.Validations.validate_money_positive(:amount) + end + end + @impl true def mount(%{"id" => bounty_id}, _session, socket) do bounty = @@ -110,6 +129,7 @@ defmodule AlgoraWeb.ContractLive do ticket_body_html = Algora.Markdown.render(bounty.ticket.description) reward_changeset = RewardBountyForm.changeset(%RewardBountyForm{}, %{amount: bounty.amount, tip_percentage: 0}) + release_changeset = ReleaseBountyForm.changeset(%ReleaseBountyForm{}, %{amount: bounty.amount}) {:ok, thread} = Chat.get_or_create_bounty_thread(bounty) messages = thread.id |> Chat.list_messages() |> Repo.preload(:sender) @@ -139,12 +159,15 @@ defmodule AlgoraWeb.ContractLive do |> assign(:ticket_body_html, ticket_body_html) |> assign(:show_reward_modal, false) |> assign(:show_authorize_modal, false) + |> assign(:show_release_modal, false) |> assign(:selected_context, nil) + |> assign(:tx_id, nil) |> assign(:line_items, []) |> assign(:thread, thread) |> assign(:messages, messages) |> assign(:participants, participants) |> assign(:reward_form, to_form(reward_changeset)) + |> assign(:release_form, to_form(release_changeset)) |> assign_contractor(bounty.shared_with) |> assign_transactions() |> assign_line_items(reward_changeset)} @@ -206,6 +229,14 @@ defmodule AlgoraWeb.ContractLive do {:noreply, assign(socket, :show_authorize_modal, true)} end + @impl true + def handle_event("release", %{"tx_id" => tx_id}, socket) do + {:noreply, + socket + |> assign(:show_release_modal, true) + |> assign(:tx_id, tx_id)} + end + @impl true def handle_event("close_drawer", _params, socket) do {:noreply, close_drawers(socket)} @@ -221,6 +252,16 @@ defmodule AlgoraWeb.ContractLive do |> assign_line_items(changeset)} end + @impl true + def handle_event("validate_release", %{"release_bounty_form" => params}, socket) do + changeset = ReleaseBountyForm.changeset(%ReleaseBountyForm{}, params) + + {:noreply, + socket + |> assign(:release_form, to_form(changeset)) + |> assign_line_items(changeset)} + end + @impl true def handle_event("pay_with_stripe", %{"reward_bounty_form" => params}, socket) do changeset = RewardBountyForm.changeset(%RewardBountyForm{}, params) @@ -254,19 +295,28 @@ defmodule AlgoraWeb.ContractLive do end @impl true - def handle_event("release_funds", %{"tx_id" => tx_id}, socket) do - with tx when not is_nil(tx) <- Enum.find(socket.assigns.transactions, &(&1.id == tx_id)), - {:ok, charge} <- Algora.PSP.Charge.retrieve(tx.provider_charge_id), - {:ok, _} <- Algora.Payments.process_charge("charge.succeeded", charge, tx.group_id) do - {:noreply, socket} - else - {:error, reason} -> - Logger.error("Failed to release funds: #{inspect(reason)}") - {:noreply, put_flash(socket, :error, "Something went wrong")} + def handle_event("release_funds", %{"release_bounty_form" => params}, socket) do + dbg(params) + changeset = ReleaseBountyForm.changeset(%ReleaseBountyForm{}, params) - _ -> - Logger.error("Failed to release funds: transaction not found") - {:noreply, put_flash(socket, :error, "Something went wrong")} + case apply_action(changeset, :save) do + {:ok, _data} -> + with tx when not is_nil(tx) <- Enum.find(socket.assigns.transactions, &(&1.id == socket.assigns.tx_id)), + {:ok, charge} <- Algora.PSP.Charge.retrieve(tx.provider_charge_id), + {:ok, _} <- Algora.Payments.process_charge("charge.succeeded", charge, tx.group_id) do + {:noreply, socket |> assign(:show_release_modal, false) |> put_flash(:info, "Funds released!")} + else + {:error, reason} -> + Logger.error("Failed to release funds: #{inspect(reason)}") + {:noreply, put_flash(socket, :error, "Something went wrong")} + + _ -> + Logger.error("Failed to release funds: transaction not found") + {:noreply, put_flash(socket, :error, "Something went wrong")} + end + + {:error, changeset} -> + {:noreply, assign(socket, :release_form, to_form(changeset))} end end @@ -484,8 +534,7 @@ defmodule AlgoraWeb.ContractLive do transaction.status == :requires_release } size="sm" - phx-click="release_funds" - phx-disable-with="Releasing..." + phx-click="release" phx-value-tx_id={transaction.id} > Release funds @@ -616,6 +665,86 @@ defmodule AlgoraWeb.ContractLive do + <.drawer :if={@current_user} show={@show_release_modal} on_cancel="close_drawer"> + <.drawer_header> + <.drawer_title>Release funds + <.drawer_description> + {@contractor.name} will be paid once you release the funds. + + + <.drawer_content class="mt-4"> + <.form for={@release_form} phx-change="validate_release" phx-submit="release_funds"> +
+
+ <.card> + <.card_header> + <.card_title>Release Details + + <.card_content class="pt-0"> +
+ <.input + label="Amount" + icon="tabler-currency-dollar" + field={@release_form[:amount]} + /> +
+ + + <.card> + <.card_header> + <.card_title>Payment Summary + + <.card_content class="pt-0"> +
+ <%= for line_item <- @line_items do %> +
+
+ <%= if line_item.image do %> + <.avatar> + <.avatar_image src={line_item.image} /> + <.avatar_fallback> + {Util.initials(line_item.title)} + + + <% else %> +
+ <% end %> +
+
{line_item.title}
+
{line_item.description}
+
+
+
+ {Money.to_string!(line_item.amount)} +
+
+ <% end %> +
+
+
+
+
Total due
+
+
+ {LineItem.gross_amount(@line_items)} +
+
+
+ + +
+
+ <.button variant="secondary" phx-click="close_drawer" type="button"> + Cancel + + <.button type="submit"> + Release funds <.icon name="tabler-arrow-right" class="-mr-1 ml-2 h-4 w-4" /> + +
+
+ + + <.drawer :if={@current_user} show={@show_authorize_modal} on_cancel="close_drawer"> <.drawer_header> <.drawer_title>Authorize payment @@ -840,6 +969,7 @@ defmodule AlgoraWeb.ContractLive do socket |> assign(:show_reward_modal, false) |> assign(:show_authorize_modal, false) + |> assign(:show_release_modal, false) end defp assign_contractor(socket, shared_with) do diff --git a/lib/algora_web/live/org/dashboard_live.ex b/lib/algora_web/live/org/dashboard_live.ex index 6a31081a9..99e67cb2c 100644 --- a/lib/algora_web/live/org/dashboard_live.ex +++ b/lib/algora_web/live/org/dashboard_live.ex @@ -1445,11 +1445,13 @@ defmodule AlgoraWeb.Org.DashboardLive do
- Total payment for {@match.user.hours_per_week || 30} - hours -
- (includes all platform and payment processing fees) -
+ Minimum payment to collaborate:
+ + {@match[:hourly_rate] + |> Money.mult!(@match.user.hours_per_week || 30) + |> Money.to_string!()} + + ({@match.user.hours_per_week || 30} hours booked)
<.button @@ -1463,12 +1465,12 @@ defmodule AlgoraWeb.Org.DashboardLive do >
Offer contract -
- {@match[:hourly_rate] - |> Money.mult!(@match.user.hours_per_week || 30) - |> Bounties.calculate_contract_amount() - |> Money.to_string!(no_fraction_if_integer: false)} / week +
+ {@match[:hourly_rate]}/hr
+
+ (includes all fees) +
diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex index e1bbbb9ae..0e16a40c3 100644 --- a/lib/algora_web/live/org/nav.ex +++ b/lib/algora_web/live/org/nav.ex @@ -132,11 +132,7 @@ defmodule AlgoraWeb.Org.Nav do bounty_res = Bounties.create_bounty( %{ - amount: - case data.contract_type do - :marketplace -> Bounties.calculate_contract_amount(amount) - :bring_your_own -> amount - end, + amount: amount, creator: socket.assigns.current_user, owner: socket.assigns.current_org, title: data.title, From 90bbbe245c8e390944ea830596fedd0e21bf27e0 Mon Sep 17 00:00:00 2001 From: zafer Date: Fri, 2 May 2025 19:27:52 +0300 Subject: [PATCH 2/5] add hours input --- lib/algora_web/live/contract_live.ex | 67 ++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/lib/algora_web/live/contract_live.ex b/lib/algora_web/live/contract_live.ex index 4b9a7c545..d692aa7e6 100644 --- a/lib/algora_web/live/contract_live.ex +++ b/lib/algora_web/live/contract_live.ex @@ -51,12 +51,14 @@ defmodule AlgoraWeb.ContractLive do @primary_key false embedded_schema do field :amount, USD + field :hours, :decimal end def changeset(form, attrs) do form - |> cast(attrs, [:amount]) + |> cast(attrs, [:amount, :hours]) |> validate_required([:amount]) + |> validate_number(:hours, greater_than: 0) |> Algora.Validations.validate_money_positive(:amount) end end @@ -129,7 +131,9 @@ defmodule AlgoraWeb.ContractLive do ticket_body_html = Algora.Markdown.render(bounty.ticket.description) reward_changeset = RewardBountyForm.changeset(%RewardBountyForm{}, %{amount: bounty.amount, tip_percentage: 0}) - release_changeset = ReleaseBountyForm.changeset(%ReleaseBountyForm{}, %{amount: bounty.amount}) + + release_changeset = + ReleaseBountyForm.changeset(%ReleaseBountyForm{}, %{amount: bounty.amount, hours: bounty.hours_per_week}) {:ok, thread} = Chat.get_or_create_bounty_thread(bounty) messages = thread.id |> Chat.list_messages() |> Repo.preload(:sender) @@ -252,6 +256,55 @@ defmodule AlgoraWeb.ContractLive do |> assign_line_items(changeset)} end + @impl true + def handle_event("validate_hours", %{"release_bounty_form" => %{"hours" => hours}}, socket) do + hours = + Decimal.new( + case hours do + "" -> "0" + hours -> hours + end + ) + + changeset = + Ecto.Changeset.change(socket.assigns.release_form.source, %{ + hours: hours, + amount: Money.mult!(socket.assigns.bounty.amount, Decimal.div(hours, socket.assigns.bounty.hours_per_week)) + }) + + {:noreply, + socket + |> assign(:release_form, to_form(changeset)) + |> assign_line_items(changeset)} + end + + @impl true + def handle_event("validate_amount", %{"release_bounty_form" => %{"amount" => amount}}, socket) do + amount = + Money.new( + :USD, + case amount do + "" -> "0" + amount -> amount + end + ) + + changeset = + Ecto.Changeset.change(socket.assigns.release_form.source, %{ + amount: amount, + hours: + amount + |> Money.to_decimal() + |> Decimal.mult(socket.assigns.bounty.hours_per_week) + |> Decimal.div(Money.to_decimal(socket.assigns.bounty.amount)) + }) + + {:noreply, + socket + |> assign(:release_form, to_form(changeset)) + |> assign_line_items(changeset)} + end + @impl true def handle_event("validate_release", %{"release_bounty_form" => params}, socket) do changeset = ReleaseBountyForm.changeset(%ReleaseBountyForm{}, params) @@ -296,7 +349,6 @@ defmodule AlgoraWeb.ContractLive do @impl true def handle_event("release_funds", %{"release_bounty_form" => params}, socket) do - dbg(params) changeset = ReleaseBountyForm.changeset(%ReleaseBountyForm{}, params) case apply_action(changeset, :save) do @@ -537,7 +589,7 @@ defmodule AlgoraWeb.ContractLive do phx-click="release" phx-value-tx_id={transaction.id} > - Release funds + Partial release
@@ -682,10 +734,17 @@ defmodule AlgoraWeb.ContractLive do <.card_content class="pt-0">
+ <.input + label="Hours" + icon="tabler-clock" + field={@release_form[:hours]} + phx-change="validate_hours" + /> <.input label="Amount" icon="tabler-currency-dollar" field={@release_form[:amount]} + phx-change="validate_amount" />
From 103b1b692345ff814dc3af5ae349cc375c93b972 Mon Sep 17 00:00:00 2001 From: zafer Date: Fri, 2 May 2025 19:54:03 +0300 Subject: [PATCH 3/5] misc --- lib/algora/settings/settings.ex | 4 +++- lib/algora_web/live/org/dashboard_live.ex | 13 ++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/algora/settings/settings.ex b/lib/algora/settings/settings.ex index 54b9b164e..239d16290 100644 --- a/lib/algora/settings/settings.ex +++ b/lib/algora/settings/settings.ex @@ -127,6 +127,7 @@ defmodule Algora.Settings do projects = Accounts.list_contributed_projects(user, limit: 2) avatar_url = profile["avatar_url"] || user.avatar_url hourly_rate = match["hourly_rate"] || profile["hourly_rate"] + hours_per_week = match["hours_per_week"] || profile["hours_per_week"] || user.hours_per_week [ %{ @@ -134,7 +135,8 @@ defmodule Algora.Settings do projects: projects, badge_variant: match["badge_variant"], badge_text: match["badge_text"], - hourly_rate: if(hourly_rate, do: Money.new(:USD, hourly_rate, no_fraction_if_integer: true)) + hourly_rate: if(hourly_rate, do: Money.new(:USD, hourly_rate, no_fraction_if_integer: true)), + hours_per_week: hours_per_week } ] else diff --git a/lib/algora_web/live/org/dashboard_live.ex b/lib/algora_web/live/org/dashboard_live.ex index 99e67cb2c..e19aacb44 100644 --- a/lib/algora_web/live/org/dashboard_live.ex +++ b/lib/algora_web/live/org/dashboard_live.ex @@ -390,7 +390,7 @@ defmodule AlgoraWeb.Org.DashboardLive do
  • <.icon name="tabler-circle-number-3 mr-2" class="size-6 text-success-400 shrink-0" /> - Release/withhold escrow end of week + Release/withhold escrow as you go
  • @@ -674,8 +674,7 @@ defmodule AlgoraWeb.Org.DashboardLive do developer = Enum.find(socket.assigns.developers, &(&1.id == user_id)) match = Enum.find(socket.assigns.matches, &(&1.user.id == user_id)) hourly_rate = match[:hourly_rate] - - hours_per_week = developer.hours_per_week || 30 + hours_per_week = match[:hours_per_week] || developer.hours_per_week || 30 {:noreply, socket @@ -691,8 +690,8 @@ defmodule AlgoraWeb.Org.DashboardLive do hourly_rate: hourly_rate, contractor_handle: developer.provider_login, hours_per_week: hours_per_week, - title: "#{socket.assigns.current_org.name} OSS Development", - description: "Open source contribution to #{socket.assigns.current_org.name} for a week" + title: "#{socket.assigns.current_org.name} Development", + description: "Contribution to #{socket.assigns.current_org.name} for #{hours_per_week} hours" }) |> to_form() )} @@ -1448,10 +1447,10 @@ defmodule AlgoraWeb.Org.DashboardLive do Minimum payment to collaborate:
    {@match[:hourly_rate] - |> Money.mult!(@match.user.hours_per_week || 30) + |> Money.mult!(@match.hours_per_week || 30) |> Money.to_string!()} - ({@match.user.hours_per_week || 30} hours booked) + ({@match.hours_per_week || 30} hours booked)
    <.button From 7bd89b857402562bb9d865b6cff7efb7f0c4353d Mon Sep 17 00:00:00 2001 From: zafer Date: Fri, 2 May 2025 22:16:46 +0300 Subject: [PATCH 4/5] list top oss contributions, impl partial release --- lib/algora/bounties/bounties.ex | 8 ++- lib/algora/payments/payments.ex | 80 +++++++++++++++++++++- lib/algora_web/live/contract_live.ex | 44 ++++++++++-- lib/algora_web/live/org/dashboard_live.ex | 81 ++++++++++++++++++++++- 4 files changed, 201 insertions(+), 12 deletions(-) diff --git a/lib/algora/bounties/bounties.ex b/lib/algora/bounties/bounties.ex index a9bc44ce6..3cc78049c 100644 --- a/lib/algora/bounties/bounties.ex +++ b/lib/algora/bounties/bounties.ex @@ -429,8 +429,12 @@ defmodule Algora.Bounties do @spec notify_bounty(%{owner: User.t(), bounty: Bounty.t()}, opts :: []) :: {:ok, nil} | {:error, atom()} - def notify_bounty(%{owner: _owner, bounty: bounty}, _opts) do - Algora.Admin.alert("Notify bounty: #{inspect(bounty)}", :error) + def notify_bounty(%{owner: owner, bounty: bounty}, _opts) do + Algora.Admin.alert( + "New contract offer: #{AlgoraWeb.Endpoint.url()}/#{owner.handle}/contracts/#{bounty.id}", + :critical + ) + {:ok, nil} end diff --git a/lib/algora/payments/payments.ex b/lib/algora/payments/payments.ex index a7a27bec3..059ca1ecc 100644 --- a/lib/algora/payments/payments.ex +++ b/lib/algora/payments/payments.ex @@ -9,6 +9,7 @@ defmodule Algora.Payments do alias Algora.Bounties alias Algora.Bounties.Bounty alias Algora.Bounties.Claim + alias Algora.Bounties.Jobs.PromptPayoutConnect alias Algora.Bounties.Tip alias Algora.Jobs.JobPosting alias Algora.MoneyUtils @@ -737,7 +738,7 @@ defmodule Algora.Payments do {:error, :no_active_account} -> case %{credit_id: credit.id} - |> Bounties.Jobs.PromptPayoutConnect.new() + |> PromptPayoutConnect.new() |> Oban.insert() do {:ok, _job} -> {:cont, :ok} error -> {:halt, error} @@ -761,4 +762,81 @@ defmodule Algora.Payments do end end) end + + def process_release( + %Stripe.Charge{id: charge_id, captured: true, payment_intent: payment_intent_id}, + group_id, + amount, + recipient + ) do + Repo.transact(fn -> + tx = Repo.get_by(Transaction, group_id: group_id, type: :charge, status: :succeeded) + + user = Repo.get_by(User, id: tx.user_id) + bounty = Repo.get_by(Bounty, id: tx.bounty_id) + + Algora.Admin.alert( + "Release #{amount} escrow to #{recipient.handle} for #{AlgoraWeb.Endpoint.url()}/#{user.handle}/contracts/#{bounty.id}", + :critical + ) + + debit_id = Nanoid.generate() + credit_id = Nanoid.generate() + + with {:ok, debit0} <- Repo.fetch_by(Transaction, group_id: group_id, type: :debit, status: :requires_release), + {:ok, _} <- + debit0 + |> change(%{ + net_amount: Money.sub!(debit0.net_amount, amount), + gross_amount: Money.sub!(debit0.gross_amount, amount) + }) + |> Repo.update(), + {:ok, credit0} <- Repo.fetch_by(Transaction, group_id: group_id, type: :credit, status: :requires_release), + {:ok, _} <- + credit0 + |> change(%{ + net_amount: Money.add!(credit0.net_amount, amount), + gross_amount: Money.add!(credit0.gross_amount, amount) + }) + |> Repo.update(), + {:ok, _debit} <- + Repo.insert(%Transaction{ + id: debit_id, + provider: "stripe", + provider_id: charge_id, + provider_charge_id: charge_id, + provider_payment_intent_id: payment_intent_id, + type: :debit, + status: :succeeded, + succeeded_at: DateTime.utc_now(), + bounty_id: tx.bounty_id, + user_id: tx.user_id, + gross_amount: amount, + net_amount: amount, + total_fee: Money.zero(:USD), + linked_transaction_id: credit_id, + group_id: group_id + }), + {:ok, _credit} <- + Repo.insert(%Transaction{ + id: credit_id, + provider: "stripe", + provider_id: charge_id, + provider_charge_id: charge_id, + provider_payment_intent_id: payment_intent_id, + type: :credit, + status: :initialized, + succeeded_at: DateTime.utc_now(), + bounty_id: tx.bounty_id, + user_id: recipient.id, + gross_amount: amount, + net_amount: amount, + total_fee: Money.zero(:USD), + linked_transaction_id: debit_id, + group_id: group_id + }) do + {:ok, nil} + end + end) + end end diff --git a/lib/algora_web/live/contract_live.ex b/lib/algora_web/live/contract_live.ex index d692aa7e6..7a0135fff 100644 --- a/lib/algora_web/live/contract_live.ex +++ b/lib/algora_web/live/contract_live.ex @@ -12,6 +12,7 @@ defmodule AlgoraWeb.ContractLive do alias Algora.Chat alias Algora.Organizations.Member alias Algora.Payments + alias Algora.Payments.Transaction alias Algora.Repo alias Algora.Types.USD alias Algora.Util @@ -235,8 +236,21 @@ defmodule AlgoraWeb.ContractLive do @impl true def handle_event("release", %{"tx_id" => tx_id}, socket) do + tx = Repo.get(Transaction, tx_id) + + release_changeset = + ReleaseBountyForm.changeset(%ReleaseBountyForm{}, %{ + amount: tx.net_amount, + hours: + tx.net_amount + |> Money.to_decimal() + |> Decimal.mult(socket.assigns.bounty.hours_per_week) + |> Decimal.div(Money.to_decimal(socket.assigns.bounty.amount)) + }) + {:noreply, socket + |> assign(:release_form, to_form(release_changeset)) |> assign(:show_release_modal, true) |> assign(:tx_id, tx_id)} end @@ -352,11 +366,15 @@ defmodule AlgoraWeb.ContractLive do changeset = ReleaseBountyForm.changeset(%ReleaseBountyForm{}, params) case apply_action(changeset, :save) do - {:ok, _data} -> + {:ok, data} -> with tx when not is_nil(tx) <- Enum.find(socket.assigns.transactions, &(&1.id == socket.assigns.tx_id)), {:ok, charge} <- Algora.PSP.Charge.retrieve(tx.provider_charge_id), - {:ok, _} <- Algora.Payments.process_charge("charge.succeeded", charge, tx.group_id) do - {:noreply, socket |> assign(:show_release_modal, false) |> put_flash(:info, "Funds released!")} + {:ok, _} <- Algora.Payments.process_release(charge, tx.group_id, data.amount, socket.assigns.contractor) do + {:noreply, + socket + |> assign(:show_release_modal, false) + |> put_flash(:info, "Funds released!") + |> assign_transactions()} else {:error, reason} -> Logger.error("Failed to release funds: #{inspect(reason)}") @@ -458,7 +476,7 @@ defmodule AlgoraWeb.ContractLive do <% end %> <%= if @can_create_bounty && @transactions == [] do %> - <.button phx-click="reward"> + <.button phx-click="reward" variant="secondary"> Make payment <% end %> @@ -1048,12 +1066,26 @@ defmodule AlgoraWeb.ContractLive do defp assign_transactions(socket) do transactions = [ - user_id: socket.assigns.bounty.owner.id, status: [:succeeded, :requires_capture, :requires_release], bounty_id: socket.assigns.bounty.id ] |> Payments.list_transactions() - |> Enum.filter(&(&1.type == :charge or &1.status in [:succeeded, :requires_release])) + |> Enum.filter(fn tx -> Money.positive?(tx.net_amount) end) + |> Enum.filter(fn tx -> + cond do + socket.assigns.can_create_bounty -> + tx.type == :charge or + (tx.type == :debit and tx.status in [:succeeded, :requires_release]) + + socket.assigns.current_user.id == socket.assigns.contractor.id -> + tx.type == :charge or + (tx.type == :debit and tx.status == :requires_release) or + (tx.type == :credit and tx.status == :succeeded) + + true -> + false + end + end) balance = calculate_balance(transactions) volume = calculate_volume(transactions) diff --git a/lib/algora_web/live/org/dashboard_live.ex b/lib/algora_web/live/org/dashboard_live.ex index e19aacb44..2f1622ec9 100644 --- a/lib/algora_web/live/org/dashboard_live.ex +++ b/lib/algora_web/live/org/dashboard_live.ex @@ -82,6 +82,12 @@ defmodule AlgoraWeb.Org.DashboardLive do matches = Algora.Settings.get_org_matches(previewed_user) + contributions = + matches + |> Enum.map(& &1.user.id) + |> Algora.Workspace.list_user_contributions() + |> Enum.group_by(& &1.user.id) + admins_last_active = Algora.Admin.admins_last_active() developers = @@ -106,6 +112,7 @@ defmodule AlgoraWeb.Org.DashboardLive do |> assign(:contributors, contributors) |> assign(:previewed_user, previewed_user) |> assign(:matches, matches) + |> assign(:contributions, contributions) |> assign(:developers, developers) |> assign(:has_more_bounties, false) |> assign(:oauth_url, Github.authorize_url(%{socket_id: socket.id})) @@ -398,6 +405,7 @@ defmodule AlgoraWeb.Org.DashboardLive do <.match_card match={match} contract_for_user={contract_for_user(@contracts, match.user)} + contributions={@contributions[match.user.id]} current_org={@current_org} /> <% end %> @@ -1329,7 +1337,7 @@ defmodule AlgoraWeb.Org.DashboardLive do defp match_card(assigns) do ~H""" -
    +
    @@ -1404,7 +1412,54 @@ defmodule AlgoraWeb.Org.DashboardLive do )}
    -
    +
    + <%= for {owner, contributions} <- aggregate_contributions(@contributions) |> Enum.take(3) do %> + <.link + href={"https://github.com/#{owner.provider_login}/#{List.first(contributions).repository.name}/pulls?q=author%3A#{@match.user.provider_login}+is%3Amerged+"} + target="_blank" + rel="noopener" + class="flex items-center gap-3 rounded-xl pr-2 bg-card/50 border border-border/50 hover:border-border transition-all" + > + {owner.name} +
    + + + {if owner.type == :organization do + owner.name + else + List.first(contributions).repository.name + end} + + <%= if tech = List.first(List.first(contributions).repository.tech_stack) do %> + + {tech} + + <% end %> + +
    + + <.icon name="tabler-star-filled" class="h-4 w-4 mr-1" /> + {Algora.Util.format_number_compact( + max(owner.stargazers_count, total_stars(contributions)) + )} + + + <.icon name="tabler-git-pull-request" class="h-4 w-4 mr-1" /> + {Algora.Util.format_number_compact(total_contributions(contributions))} + +
    +
    + + <% end %> +
    + <%!--
    <%= for {project, total_earned} <- @match.projects |> Enum.take(2) do %> <.link navigate={User.url(project)} @@ -1437,7 +1492,7 @@ defmodule AlgoraWeb.Org.DashboardLive do
    <% end %> -
    +
    --%>
    @@ -2071,4 +2126,24 @@ defmodule AlgoraWeb.Org.DashboardLive do defp format_number(n) when n >= 1_000_000, do: "#{Float.round(n / 1_000_000, 1)}M" defp format_number(n) when n >= 1_000, do: "#{Float.round(n / 1_000, 1)}K" defp format_number(n), do: to_string(n) + + defp aggregate_contributions(contributions) do + groups = Enum.group_by(contributions, fn c -> c.repository.user end) + + contributions + |> Enum.map(fn c -> {c.repository.user, groups[c.repository.user]} end) + |> Enum.uniq_by(fn {owner, _} -> owner.id end) + end + + defp total_stars(contributions) do + contributions + |> Enum.map(& &1.repository.stargazers_count) + |> Enum.sum() + end + + defp total_contributions(contributions) do + contributions + |> Enum.map(& &1.contribution_count) + |> Enum.sum() + end end From dee45b897c5d606309dc50fb251b8e3c0954612e Mon Sep 17 00:00:00 2001 From: zafer Date: Fri, 2 May 2025 22:25:15 +0300 Subject: [PATCH 5/5] update assign_transactions --- lib/algora_web/live/contract_live.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/algora_web/live/contract_live.ex b/lib/algora_web/live/contract_live.ex index 7a0135fff..e7952e5c9 100644 --- a/lib/algora_web/live/contract_live.ex +++ b/lib/algora_web/live/contract_live.ex @@ -1078,9 +1078,7 @@ defmodule AlgoraWeb.ContractLive do (tx.type == :debit and tx.status in [:succeeded, :requires_release]) socket.assigns.current_user.id == socket.assigns.contractor.id -> - tx.type == :charge or - (tx.type == :debit and tx.status == :requires_release) or - (tx.type == :credit and tx.status == :succeeded) + tx.type == :credit and tx.status == :succeeded true -> false