Skip to content

Commit dfa6267

Browse files
committed
add subscription payment drawer
1 parent 4840156 commit dfa6267

File tree

6 files changed

+207
-43
lines changed

6 files changed

+207
-43
lines changed

lib/algora/accounts/schemas/user.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ defmodule Algora.Accounts.User do
4040
field :priority, :integer, default: 0
4141
field :fee_pct, :integer, default: 9
4242
field :fee_pct_prev, :integer, default: 9
43+
field :subscription_price, Money
4344
field :seeded, :boolean, default: false
4445
field :activated, :boolean, default: false
4546
field :max_open_attempts, :integer, default: 3

lib/algora/jobs/jobs.ex

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,10 @@ defmodule Algora.Jobs do
1111
alias Algora.Payments
1212
alias Algora.Payments.Transaction
1313
alias Algora.Repo
14-
alias Algora.Settings
1514
alias Algora.Util
1615

1716
require Logger
1817

19-
def price, do: Money.new(:USD, 2_270, no_fraction_if_integer: true)
20-
2118
def list_jobs(opts \\ []) do
2219
JobPosting
2320
|> where([j], j.status == :active)
@@ -51,16 +48,18 @@ defmodule Algora.Jobs do
5148
defp maybe_limit(query, nil), do: query
5249
defp maybe_limit(query, limit), do: limit(query, ^limit)
5350

54-
@spec create_payment_session(job_posting: JobPosting.t()) ::
51+
@spec create_payment_session(JobPosting.t(), Money.t()) ::
5552
{:ok, String.t()} | {:error, atom()}
56-
def create_payment_session(job_posting) do
57-
{amount, description} = get_job_price_and_description(job_posting.user_id)
58-
53+
def create_payment_session(job_posting, amount) do
5954
line_items = [
6055
%LineItem{
6156
amount: amount,
6257
title: "Job posting - #{job_posting.company_name}",
63-
description: description
58+
description: "Annual subscription"
59+
},
60+
%LineItem{
61+
amount: Money.mult!(amount, Decimal.new("0.04")),
62+
title: "Processing fee (4%)"
6463
}
6564
]
6665

@@ -144,15 +143,4 @@ defmodule Algora.Jobs do
144143
|> preload(:user)
145144
|> Repo.all()
146145
end
147-
148-
def get_job_price_and_description(org_id) do
149-
case Settings.get_org_job_settings(org_id) do
150-
%{amount: amount, description: description} ->
151-
{amount, description}
152-
153-
nil ->
154-
# default values
155-
{price(), "1 month"}
156-
end
157-
end
158146
end

lib/algora/settings/settings.ex

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -164,19 +164,4 @@ defmodule Algora.Settings do
164164
def set_featured_transactions(ids) when is_list(ids) do
165165
set("featured_transactions", %{"ids" => ids})
166166
end
167-
168-
def get_org_job_settings(org_id) when is_binary(org_id) do
169-
case get("org_job_settings:#{org_id}") do
170-
%{"amount" => amount, "description" => description} when is_binary(description) ->
171-
%{amount: Algora.MoneyUtils.deserialize(amount), description: description}
172-
173-
_ ->
174-
nil
175-
end
176-
end
177-
178-
def set_org_job_settings(org_id, amount, description)
179-
when is_binary(org_id) and is_binary(description) and is_struct(amount, Money) do
180-
set("org_job_settings:#{org_id}", %{"amount" => Algora.MoneyUtils.serialize(amount), "description" => description})
181-
end
182167
end

lib/algora_web/live/org/job_live.ex

Lines changed: 189 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ defmodule AlgoraWeb.Org.JobLive do
3232
|> assign(:job, job)
3333
|> assign(:show_import_drawer, false)
3434
|> assign(:show_share_drawer, false)
35+
|> assign(:show_payment_drawer, false)
36+
|> assign(:payment_form, to_form(%{"payment_type" => "stripe"}, as: :payment))
3537
|> assign(:current_tab, tab)
3638
|> assign(:bounty_form, to_form(BountyForm.changeset(%BountyForm{}, %{})))
3739
|> assign(:tip_form, to_form(TipForm.changeset(%TipForm{}, %{})))
@@ -55,12 +57,6 @@ defmodule AlgoraWeb.Org.JobLive do
5557
{:ok, push_navigate(socket, to: ~p"/#{handle}/jobs/#{id}/applicants")}
5658
end
5759

58-
@impl true
59-
def handle_params(%{"tab" => "activate", "org_handle" => handle, "id" => id}, _uri, socket) do
60-
Algora.Admin.alert("Activation request received for #{AlgoraWeb.Endpoint.url()}/#{handle}/jobs/#{id}", :info)
61-
{:noreply, redirect(socket, external: AlgoraWeb.Constants.get(:calendar_url))}
62-
end
63-
6460
@impl true
6561
def handle_params(%{"tab" => tab}, _uri, socket) do
6662
{:noreply, assign(socket, :current_tab, tab)}
@@ -354,7 +350,7 @@ defmodule AlgoraWeb.Org.JobLive do
354350
</div>
355351
<div class="flex flex-col justify-center items-center text-center">
356352
<.button
357-
patch={~p"/#{@current_org.handle}/jobs/#{@job.id}/activate"}
353+
phx-click="toggle_payment_drawer"
358354
variant="none"
359355
class="group bg-emerald-900/10 text-emerald-300 transition-colors duration-75 hover:bg-emerald-800/10 hover:text-emerald-300 hover:drop-shadow-[0_1px_5px_#34d39980] focus:bg-emerald-800/10 focus:text-emerald-300 focus:outline-none focus:drop-shadow-[0_1px_5px_#34d39980] border border-emerald-400/40 hover:border-emerald-400/50 focus:border-emerald-400/50 h-[8rem]"
360356
size="xl"
@@ -475,12 +471,17 @@ defmodule AlgoraWeb.Org.JobLive do
475471
</div>
476472
</.drawer_content>
477473
</.drawer>
474+
475+
{payment_drawer(assigns)}
478476
"""
479477
end
480478

481479
@impl true
482480
def handle_event("activate_subscription", _params, socket) do
483-
case Jobs.create_payment_session(%{socket.assigns.job | email: socket.assigns.current_user.email}) do
481+
case Jobs.create_payment_session(
482+
%{socket.assigns.job | email: socket.assigns.current_user.email},
483+
socket.assigns.current_org.subscription_price
484+
) do
484485
{:ok, url} ->
485486
Algora.Admin.alert("Payment session created for job posting: #{socket.assigns.job.company_name}", :info)
486487
{:noreply, redirect(socket, external: url)}
@@ -491,6 +492,16 @@ defmodule AlgoraWeb.Org.JobLive do
491492
end
492493
end
493494

495+
@impl true
496+
def handle_event("toggle_payment_drawer", _, socket) do
497+
socket =
498+
if socket.assigns.current_org.subscription_price,
499+
do: assign(socket, :show_payment_drawer, !socket.assigns.show_payment_drawer),
500+
else: redirect(socket, external: AlgoraWeb.Constants.get(:calendar_url))
501+
502+
{:noreply, socket}
503+
end
504+
494505
@impl true
495506
def handle_event("toggle_import_drawer", _, socket) do
496507
{:noreply, assign(socket, :show_import_drawer, !socket.assigns.show_import_drawer)}
@@ -617,6 +628,35 @@ defmodule AlgoraWeb.Org.JobLive do
617628
{:noreply, assign(socket, :show_share_drawer, false)}
618629
end
619630

631+
@impl true
632+
def handle_event("close_payment_drawer", _, socket) do
633+
{:noreply, assign(socket, :show_payment_drawer, false)}
634+
end
635+
636+
@impl true
637+
def handle_event("process_payment", %{"payment" => %{"payment_type" => "stripe"}}, socket) do
638+
# Mock data for demonstration
639+
case Jobs.create_payment_session(
640+
%{socket.assigns.job | email: socket.assigns.current_user.email},
641+
socket.assigns.current_org.subscription_price
642+
) do
643+
{:ok, url} ->
644+
{:noreply, redirect(socket, external: url)}
645+
646+
{:error, _reason} ->
647+
{:noreply, put_flash(socket, :error, "Something went wrong. Please try again.")}
648+
end
649+
end
650+
651+
@impl true
652+
def handle_event("process_payment", %{"payment" => %{"payment_type" => "wire"}}, socket) do
653+
# Mock successful wire initiation
654+
{:noreply,
655+
socket
656+
|> put_flash(:info, "Wire transfer details have been sent to your email")
657+
|> assign(:show_payment_drawer, false)}
658+
end
659+
620660
@impl true
621661
def handle_event(_event, _params, socket) do
622662
{:noreply, socket}
@@ -1429,4 +1469,145 @@ defmodule AlgoraWeb.Org.JobLive do
14291469
</.button>
14301470
"""
14311471
end
1472+
1473+
defp payment_drawer(assigns) do
1474+
~H"""
1475+
<.drawer show={@show_payment_drawer} on_cancel={JS.push("close_payment_drawer")} direction="right">
1476+
<.drawer_header>
1477+
<.drawer_title>Activate Subscription</.drawer_title>
1478+
<.drawer_description>
1479+
Choose your preferred payment method to activate your annual subscription
1480+
</.drawer_description>
1481+
</.drawer_header>
1482+
1483+
<.drawer_content :if={@current_org.subscription_price} class="mt-4">
1484+
<.form for={@payment_form} phx-submit="process_payment">
1485+
<div class="space-y-6">
1486+
<div class="grid grid-cols-2 gap-4" phx-update="ignore" id="payment-form-tabs">
1487+
<%= for {label, value} <- [{"Stripe", "stripe"}, {"Wire Transfer", "wire"}] do %>
1488+
<label class={[
1489+
"group relative flex cursor-pointer rounded-lg px-3 py-2 shadow-sm focus:outline-none",
1490+
"border-2 bg-background transition-all duration-200 hover:border-primary hover:bg-primary/10",
1491+
"border-border has-[:checked]:border-primary has-[:checked]:bg-primary/10"
1492+
]}>
1493+
<.input
1494+
type="radio"
1495+
name="payment[payment_type]"
1496+
checked={@payment_form[:payment_type].value == value}
1497+
value={value}
1498+
class="sr-only"
1499+
phx-click={
1500+
%JS{}
1501+
|> JS.hide(to: "#payment-details [data-tab]:not([data-tab=#{value}])")
1502+
|> JS.show(to: "#payment-details [data-tab=#{value}]")
1503+
}
1504+
/>
1505+
<span class="flex flex-1 items-center justify-between">
1506+
<span class="text-sm font-medium">{label}</span>
1507+
<.icon
1508+
name="tabler-check"
1509+
class="invisible size-5 text-primary group-has-[:checked]:visible"
1510+
/>
1511+
</span>
1512+
</label>
1513+
<% end %>
1514+
</div>
1515+
1516+
<div id="payment-details">
1517+
<div data-tab="stripe">
1518+
<.card>
1519+
<.card_header>
1520+
<.card_title>Stripe Payment</.card_title>
1521+
<.card_description>Pay with credit card or ACH using Stripe</.card_description>
1522+
</.card_header>
1523+
<.card_content>
1524+
<div class="space-y-4">
1525+
<div class="flex justify-between items-center">
1526+
<span class="text-sm text-muted-foreground">Annual Subscription</span>
1527+
<span class="font-semibold font-display">
1528+
{Money.to_string!(@current_org.subscription_price)}
1529+
</span>
1530+
</div>
1531+
<div class="flex justify-between items-center">
1532+
<span class="text-sm text-muted-foreground">Processing Fee (4%)</span>
1533+
<span class="font-semibold font-display">
1534+
{Money.to_string!(
1535+
Money.mult!(@current_org.subscription_price, Decimal.new("0.04"))
1536+
)}
1537+
</span>
1538+
</div>
1539+
<div class="border-t pt-4 flex justify-between items-center">
1540+
<span class="font-semibold">Total</span>
1541+
<span class="font-semibold font-display">
1542+
{Money.to_string!(
1543+
Money.mult!(@current_org.subscription_price, Decimal.new("1.04"))
1544+
)}
1545+
</span>
1546+
</div>
1547+
</div>
1548+
</.card_content>
1549+
</.card>
1550+
1551+
<div class="pt-4 flex justify-end gap-4">
1552+
<.button variant="secondary" phx-click="close_payment_drawer" type="button">
1553+
Cancel
1554+
</.button>
1555+
<.button type="submit">
1556+
Continue to checkout
1557+
</.button>
1558+
</div>
1559+
</div>
1560+
1561+
<div data-tab="wire" class="hidden">
1562+
<.card>
1563+
<.card_header>
1564+
<.card_title>Wire Transfer Details</.card_title>
1565+
<.card_description>Send payment to the following account</.card_description>
1566+
</.card_header>
1567+
<.card_content>
1568+
<div class="space-y-4">
1569+
<div class="grid grid-cols-2 gap-2 text-sm">
1570+
<span class="text-muted-foreground">Bank Name:</span>
1571+
<span class="font-medium">Silicon Valley Bank</span>
1572+
1573+
<span class="text-muted-foreground">Account Name:</span>
1574+
<span class="font-medium">Algora Inc</span>
1575+
1576+
<span class="text-muted-foreground">Account Number:</span>
1577+
<span class="font-medium">XXXX-XXXX-1234</span>
1578+
1579+
<span class="text-muted-foreground">Routing Number:</span>
1580+
<span class="font-medium">XXXXXX123</span>
1581+
1582+
<span class="text-muted-foreground">SWIFT Code:</span>
1583+
<span class="font-medium">SVBKUS6S</span>
1584+
1585+
<span class="text-muted-foreground">Amount:</span>
1586+
<span class="font-medium font-display">
1587+
{Money.to_string!(@current_org.subscription_price)}
1588+
</span>
1589+
</div>
1590+
</div>
1591+
<p class="text-sm text-muted-foreground pt-4">
1592+
You will receive an invoice and receipt via email once you confirm
1593+
</p>
1594+
</.card_content>
1595+
</.card>
1596+
1597+
<div class="pt-4 flex justify-end gap-4">
1598+
<.button variant="secondary" phx-click="close_payment_drawer" type="button">
1599+
Cancel
1600+
</.button>
1601+
<.button type="submit">
1602+
I have wired
1603+
</.button>
1604+
</div>
1605+
</div>
1606+
</div>
1607+
</div>
1608+
</.form>
1609+
</.drawer_content>
1610+
</.drawer>
1611+
"""
1612+
end
14321613
end

lib/algora_web/live/org/jobs_live.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ defmodule AlgoraWeb.Org.JobsLive do
179179
with {:ok, user} <-
180180
Accounts.get_or_register_user(params["email"], %{type: :organization, display_name: params["company_name"]}),
181181
{:ok, job} <- params |> Map.put("user_id", user.id) |> Jobs.create_job_posting(),
182-
{:ok, url} <- Jobs.create_payment_session(job) do
182+
{:ok, url} <- Jobs.create_payment_session(job, socket.assigns.current_org.subscription_price) do
183183
Algora.Admin.alert("Job posting initialized: #{job.company_name}", :info)
184184
{:noreply, redirect(socket, external: url)}
185185
else
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
defmodule Algora.Repo.Migrations.AddSubscriptionPriceToUsers do
2+
use Ecto.Migration
3+
4+
def change do
5+
alter table(:users) do
6+
add :subscription_price, :money_with_currency
7+
end
8+
end
9+
end

0 commit comments

Comments
 (0)