defmodule TarakanWeb.BountyLive.Show do @moduledoc "One contract: target, reward, claims, and sponsor/moderator controls." use TarakanWeb, :live_view import TarakanWeb.BountyComponents alias Tarakan.Market alias Tarakan.Market.Bounty alias Tarakan.Policy @impl true def mount(%{"public_id" => public_id}, _session, socket) do bounty = Market.get_bounty!(public_id) {:ok, socket |> assign(:page_title, "Contract: #{bounty.title}") |> assign(:meta_description, meta_description(bounty)) |> assign(:canonical_path, ~p"/bounties/#{bounty.public_id}") |> assign_bounty(bounty)} end @impl true def handle_event("claim", _params, socket) do case Market.claim_bounty(socket.assigns.current_scope, socket.assigns.bounty) do {:ok, _claim} -> {:noreply, socket |> put_flash(:info, "Contract claimed - the job is yours for the next two hours.") |> reload_bounty()} {:error, reason} -> {:noreply, put_flash(socket, :error, claim_error_message(reason))} end end def handle_event("cancel", _params, socket) do case Market.cancel_bounty(socket.assigns.current_scope, socket.assigns.bounty) do {:ok, _bounty} -> {:noreply, socket |> put_flash(:info, "Contract cancelled. Escrowed funds were returned.") |> reload_bounty()} {:error, reason} -> {:noreply, put_flash(socket, :error, cancel_error_message(reason))} end end def handle_event("settle", _params, socket) do case Market.settle_bounty(socket.assigns.current_scope, socket.assigns.bounty) do {:ok, bounty} -> message = if bounty.funding == "fiat", do: "Contract settled. Payout is pending manual processing.", else: "Contract settled. Credits were paid to the winner." {:noreply, socket |> put_flash(:info, message) |> reload_bounty()} {:error, reason} -> {:noreply, put_flash(socket, :error, settle_error_message(reason))} end end def handle_event("mark_paid", _params, socket) do case Market.mark_paid(socket.assigns.current_scope, socket.assigns.bounty) do {:ok, _bounty} -> {:noreply, socket |> put_flash(:info, "Contract marked paid.") |> reload_bounty()} {:error, _reason} -> {:noreply, put_flash(socket, :error, "The contract could not be marked paid.")} end end defp reload_bounty(socket) do assign_bounty(socket, Market.get_bounty!(socket.assigns.bounty.public_id)) end defp assign_bounty(socket, bounty) do scope = socket.assigns.current_scope account_id = scope && scope.account_id claims = Enum.sort_by(bounty.claims, & &1.id, :desc) my_claim = if account_id, do: Enum.find(claims, &(&1.account_id == account_id)), else: nil socket |> assign(:bounty, bounty) |> assign(:claims, claims) |> assign(:claim_count, length(claims)) |> assign(:my_claim, my_claim) |> assign(:sponsor?, account_id != nil and account_id == bounty.sponsor_account_id) |> assign(:winner?, account_id != nil and account_id == bounty.winner_account_id) |> assign(:moderator?, scope != nil and Policy.moderator?(scope)) |> assign(:admin?, scope != nil and Policy.admin?(scope)) |> assign(:can_claim?, can_claim?(scope, bounty)) |> assign(:can_cancel?, can_cancel?(scope, bounty)) end defp can_claim?(nil, _bounty), do: false defp can_claim?(scope, %Bounty{} = bounty) do scope.account_id != nil and scope.account_state == "active" and bounty.status == "open" and scope.account_id != bounty.sponsor_account_id end defp can_cancel?(nil, _bounty), do: false defp can_cancel?(scope, %Bounty{} = bounty) do bounty.status in ["pending_funding", "open"] and bounty.claims == [] and (scope.account_id == bounty.sponsor_account_id or Policy.moderator?(scope)) end defp meta_description(bounty) do "#{amount_label(bounty)} contract on #{target_label(bounty)}: #{bounty.title}" |> String.replace(~r/\s+/, " ") |> String.slice(0, 160) end defp claim_error_message(:own_bounty), do: "You cannot claim your own contract." defp claim_error_message(:not_open), do: "This contract is no longer open." defp claim_error_message(:claim_limit), do: "You already hold too many active claims." defp claim_error_message(:unauthorized), do: "Your account cannot claim contracts." defp claim_error_message(_reason), do: "The contract could not be claimed. Try again shortly." defp cancel_error_message(:has_claims), do: "A contract with claims cannot be cancelled." defp cancel_error_message(:invalid_state), do: "This contract can no longer be cancelled." defp cancel_error_message(:unauthorized), do: "You cannot cancel this contract." defp cancel_error_message(_reason), do: "The contract could not be cancelled. Try again shortly." defp settle_error_message(:no_winning_claim), do: "No submitted or accepted claim to settle." defp settle_error_message(:invalid_state), do: "This contract is not awaiting settlement." defp settle_error_message(_reason), do: "The contract could not be settled. Try again shortly." @impl true def render(assigns) do ~H"""
<.notch_badge class="bg-signal text-ground">Contract {status_label(@bounty.status)}

{amount_label(@bounty)}

{@bounty.title}

Target
<%= if path = target_path(@bounty) do %> <.link navigate={path} class="text-signal hover:underline"> {target_label(@bounty)} <% else %> {target_label(@bounty)} <% end %>
Sponsor
<.link navigate={~p"/#{@bounty.sponsor_account.handle}"} class="text-signal hover:underline" > @{@bounty.sponsor_account.handle}
Funding
{if @bounty.funding == "fiat", do: "card escrow", else: "credits"}
Expires
{Calendar.strftime(@bounty.expires_at, "%Y-%m-%d")}
Claims
{@claim_count}
Winner
<.link navigate={~p"/#{@bounty.winner_account.handle}"} class="text-signal hover:underline" > @{@bounty.winner_account.handle}

{@bounty.description}

Payout pending - the platform processes fiat payouts manually. You'll be contacted at your account email.
Payout pending - escrowed funds are being released to the winner.
<.link :if={not @can_claim? and @bounty.status == "open" and @current_scope == nil} id="bounty-login-to-claim" navigate={~p"/accounts/log-in"} class="border-2 border-signal px-5 py-2 font-display text-xs uppercase tracking-[0.12em] text-signal transition hover:bg-signal hover:text-ground" > Log in to claim

Claims

  • @{claim.account.handle} ยท <.link navigate={~p"/jobs/#{claim.review_task.id}"} class="text-signal hover:underline" >job {claim.status}

Your claim is backed by <.link navigate={~p"/jobs/#{@my_claim.review_task.id}"} class="font-semibold text-signal hover:underline" > this job - submit your work there. Settlement follows acceptance.

""" end end