Skip to content

Commit 2446934

Browse files
committed
Merge branch 'main' of github.com:algora-io/algora into feat/contracts
2 parents c69dda6 + 22f6771 commit 2446934

File tree

18 files changed

+726
-58
lines changed

18 files changed

+726
-58
lines changed

assets/js/app.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,4 +766,22 @@ window.addEventListener("phx:open_popup", (e: CustomEvent) => {
766766
}
767767
});
768768

769+
// Add event listener for storing session values
770+
window.addEventListener("phx:store-session", (event) => {
771+
const token = document
772+
.querySelector('meta[name="csrf-token"]')
773+
.getAttribute("content");
774+
775+
console.log(event.detail);
776+
777+
fetch("/store-session", {
778+
method: "POST",
779+
headers: {
780+
"Content-Type": "application/json",
781+
"X-CSRF-Token": token,
782+
},
783+
body: JSON.stringify(event.detail),
784+
});
785+
});
786+
769787
export default Hooks;

lib/algora/accounts/accounts.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ defmodule Algora.Accounts do
412412
end
413413

414414
def register_org(params) do
415-
params |> User.org_registration_changeset() |> Repo.insert()
415+
params |> User.org_registration_changeset() |> Repo.insert(returning: true)
416416
end
417417

418418
def auto_join_orgs(user) do
@@ -443,10 +443,10 @@ defmodule Algora.Accounts do
443443
orgs
444444
end
445445

446-
def get_or_register_user(email) do
446+
def get_or_register_user(email, attr \\ %{}) do
447447
res =
448448
case get_user_by_email(email) do
449-
nil -> register_org(%{email: email})
449+
nil -> attr |> Map.put(:email, email) |> register_org()
450450
user -> {:ok, user}
451451
end
452452

lib/algora/accounts/schemas/user.ex

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ defmodule Algora.Accounts.User do
114114

115115
def org_registration_changeset(params) do
116116
%User{}
117-
|> cast(params, [:email])
117+
|> cast(params, [:email, :display_name, :type])
118118
|> generate_id()
119119
|> validate_required([:email])
120-
|> validate_email()
120+
|> validate_unique_email()
121121
end
122122

123123
@doc """
@@ -161,7 +161,7 @@ defmodule Algora.Accounts.User do
161161
|> generate_id()
162162
|> validate_required([:email, :handle])
163163
|> validate_handle()
164-
|> validate_email()
164+
|> validate_unique_email()
165165
|> unique_constraint(:email)
166166
|> unique_constraint(:handle)
167167
|> put_assoc(:identities, [identity_changeset])
@@ -219,7 +219,7 @@ defmodule Algora.Accounts.User do
219219
|> generate_id()
220220
|> validate_required([:email, :handle])
221221
|> validate_handle()
222-
|> validate_email()
222+
|> validate_unique_email()
223223
|> unique_constraint(:email)
224224
|> unique_constraint(:handle)
225225
else
@@ -249,7 +249,7 @@ defmodule Algora.Accounts.User do
249249
])
250250
|> generate_id()
251251
|> validate_required([:type, :handle, :email])
252-
|> validate_email()
252+
|> validate_unique_email()
253253
|> unique_constraint(:handle)
254254
|> unique_constraint(:email)
255255
end
@@ -286,11 +286,16 @@ defmodule Algora.Accounts.User do
286286
cast(user, params, [:email, :signup_token])
287287
end
288288

289-
defp validate_email(changeset) do
289+
def validate_email(changeset) do
290290
changeset
291291
|> validate_required([:email])
292292
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
293293
|> validate_length(:email, max: 160)
294+
end
295+
296+
def validate_unique_email(changeset) do
297+
changeset
298+
|> validate_email()
294299
|> unsafe_validate_unique(:email, Algora.Repo)
295300
|> unique_constraint(:email)
296301
end

lib/algora/jobs/jobs.ex

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
defmodule Algora.Jobs do
2+
@moduledoc false
3+
4+
import Ecto.Changeset
5+
import Ecto.Query
6+
7+
alias Algora.Accounts
8+
alias Algora.Bounties.LineItem
9+
alias Algora.Jobs.JobApplication
10+
alias Algora.Jobs.JobPosting
11+
alias Algora.Payments
12+
alias Algora.Payments.Transaction
13+
alias Algora.Repo
14+
alias Algora.Util
15+
16+
require Logger
17+
18+
def price, do: Money.new(:USD, 499, no_fraction_if_integer: true)
19+
20+
def list_jobs(opts \\ []) do
21+
JobPosting
22+
|> where([j], j.status == :active)
23+
|> order_by([j], desc: j.inserted_at)
24+
|> maybe_filter_by_tech_stack(opts[:tech_stack])
25+
|> maybe_limit(opts[:limit])
26+
|> Repo.all()
27+
|> Repo.preload(:user)
28+
end
29+
30+
def create_job_posting(attrs) do
31+
%JobPosting{}
32+
|> JobPosting.changeset(attrs)
33+
|> Repo.insert()
34+
end
35+
36+
defp maybe_filter_by_tech_stack(query, nil), do: query
37+
defp maybe_filter_by_tech_stack(query, []), do: query
38+
39+
defp maybe_filter_by_tech_stack(query, tech_stack) do
40+
where(query, [j], fragment("? && ?", j.tech_stack, ^tech_stack))
41+
end
42+
43+
defp maybe_limit(query, nil), do: query
44+
defp maybe_limit(query, limit), do: limit(query, ^limit)
45+
46+
@spec create_payment_session(job_posting: JobPosting.t()) ::
47+
{:ok, String.t()} | {:error, atom()}
48+
def create_payment_session(job_posting) do
49+
line_items = [%LineItem{amount: price(), title: "Job posting - #{job_posting.company_name}"}]
50+
51+
gross_amount = LineItem.gross_amount(line_items)
52+
group_id = Nanoid.generate()
53+
54+
Repo.transact(fn ->
55+
with {:ok, user} <-
56+
Accounts.get_or_register_user(job_posting.email, %{
57+
type: :organization,
58+
display_name: job_posting.company_name
59+
}),
60+
{:ok, _charge} <-
61+
%Transaction{}
62+
|> change(%{
63+
id: Nanoid.generate(),
64+
provider: "stripe",
65+
type: :charge,
66+
status: :initialized,
67+
user_id: user.id,
68+
job_id: job_posting.id,
69+
gross_amount: gross_amount,
70+
net_amount: gross_amount,
71+
total_fee: Money.zero(:USD),
72+
line_items: Util.normalize_struct(line_items),
73+
group_id: group_id,
74+
idempotency_key: "session-#{Nanoid.generate()}"
75+
})
76+
|> Algora.Validations.validate_positive(:gross_amount)
77+
|> Algora.Validations.validate_positive(:net_amount)
78+
|> foreign_key_constraint(:user_id)
79+
|> unique_constraint([:idempotency_key])
80+
|> Repo.insert(),
81+
{:ok, session} <-
82+
Payments.create_stripe_session(
83+
user,
84+
Enum.map(line_items, &LineItem.to_stripe/1),
85+
%{
86+
description: "Job posting - #{job_posting.company_name}",
87+
metadata: %{"version" => Payments.metadata_version(), "group_id" => group_id}
88+
},
89+
success_url: "#{AlgoraWeb.Endpoint.url()}/jobs?status=paid",
90+
cancel_url: "#{AlgoraWeb.Endpoint.url()}/jobs?status=canceled"
91+
) do
92+
{:ok, session.url}
93+
end
94+
end)
95+
end
96+
97+
def create_application(job_id, user) do
98+
%JobApplication{}
99+
|> JobApplication.changeset(%{job_id: job_id, user_id: user.id})
100+
|> Repo.insert()
101+
end
102+
103+
def list_user_applications(user) do
104+
JobApplication
105+
|> where([a], a.user_id == ^user.id)
106+
|> select([a], a.job_id)
107+
|> Repo.all()
108+
|> MapSet.new()
109+
end
110+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
defmodule Algora.Jobs.JobApplication do
2+
@moduledoc false
3+
use Algora.Schema
4+
5+
alias Algora.Accounts.User
6+
alias Algora.Jobs.JobPosting
7+
8+
typed_schema "job_applications" do
9+
field :status, Ecto.Enum, values: [:pending], null: false, default: :pending
10+
11+
belongs_to :job, JobPosting, null: false
12+
belongs_to :user, User, null: false
13+
14+
timestamps()
15+
end
16+
17+
def changeset(job_application, attrs) do
18+
job_application
19+
|> cast(attrs, [:status, :job_id, :user_id])
20+
|> generate_id()
21+
|> validate_required([:status, :job_id, :user_id])
22+
|> unique_constraint([:job_id, :user_id])
23+
|> foreign_key_constraint(:job_id)
24+
|> foreign_key_constraint(:user_id)
25+
end
26+
end
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
defmodule Algora.Jobs.JobPosting do
2+
@moduledoc false
3+
use Algora.Schema
4+
5+
alias Algora.Accounts.User
6+
7+
typed_schema "job_postings" do
8+
field :title, :string
9+
field :description, :string
10+
field :tech_stack, {:array, :string}, default: []
11+
field :url, :string
12+
field :company_name, :string
13+
field :company_url, :string
14+
field :email, :string
15+
field :status, Ecto.Enum, values: [:initialized, :processing, :active, :expired], null: false, default: :initialized
16+
field :expires_at, :utc_datetime_usec
17+
18+
belongs_to :user, User, null: false
19+
20+
timestamps()
21+
end
22+
23+
def changeset(job_posting, attrs) do
24+
job_posting
25+
|> cast(attrs, [
26+
:title,
27+
:description,
28+
:tech_stack,
29+
:url,
30+
:company_name,
31+
:company_url,
32+
:email,
33+
:status,
34+
:expires_at,
35+
:user_id
36+
])
37+
|> generate_id()
38+
|> validate_required([:url, :company_name, :company_url, :email])
39+
|> User.validate_email()
40+
|> foreign_key_constraint(:user)
41+
end
42+
end

lib/algora/payments/payments.ex

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule Algora.Payments do
1010
alias Algora.Bounties.Bounty
1111
alias Algora.Bounties.Claim
1212
alias Algora.Bounties.Tip
13+
alias Algora.Jobs.JobPosting
1314
alias Algora.MoneyUtils
1415
alias Algora.Payments.Account
1516
alias Algora.Payments.Customer
@@ -37,7 +38,10 @@ defmodule Algora.Payments do
3738
user :: User.t(),
3839
line_items :: [PSP.Session.line_item_data()],
3940
payment_intent_data :: PSP.Session.payment_intent_data(),
40-
opts :: Keyword.t()
41+
opts :: [
42+
{:success_url, String.t()},
43+
{:cancel_url, String.t()}
44+
]
4145
) ::
4246
{:ok, PSP.session()} | {:error, PSP.error()}
4347
def create_stripe_session(user, line_items, payment_intent_data, opts \\ []) do
@@ -645,12 +649,20 @@ defmodule Algora.Payments do
645649

646650
tip_ids = txs |> Enum.map(& &1.tip_id) |> Enum.reject(&is_nil/1) |> Enum.uniq()
647651
claim_ids = txs |> Enum.map(& &1.claim_id) |> Enum.reject(&is_nil/1) |> Enum.uniq()
652+
job_ids = txs |> Enum.map(& &1.job_id) |> Enum.reject(&is_nil/1) |> Enum.uniq()
648653

649654
Repo.update_all(from(b in Bounty, where: b.id in ^auto_bounty_ids), set: [status: :paid])
650655
Repo.update_all(from(t in Tip, where: t.id in ^tip_ids), set: [status: :paid])
651656
# TODO: add and use a new "paid" status for claims
652657
Repo.update_all(from(c in Claim, where: c.id in ^claim_ids), set: [status: :approved])
653658

659+
{_, job_postings} =
660+
Repo.update_all(from(j in JobPosting, where: j.id in ^job_ids, select: j), set: [status: :processing])
661+
662+
for job <- job_postings do
663+
Algora.Admin.alert("Job payment received! #{job.company_name} #{job.email} #{job.url}", :info)
664+
end
665+
654666
auto_txs =
655667
Enum.filter(txs, fn tx ->
656668
bounty = bounties[tx.bounty_id]

lib/algora/payments/schemas/transaction.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ defmodule Algora.Payments.Transaction do
4040
belongs_to :claim, Algora.Bounties.Claim
4141
belongs_to :bounty, Algora.Bounties.Bounty
4242
belongs_to :tip, Algora.Bounties.Tip
43+
belongs_to :job, Algora.Jobs.JobPosting
4344
belongs_to :linked_transaction, Algora.Payments.Transaction
4445

4546
has_many :activities, {"transaction_activities", Activity}, foreign_key: :assoc_id

lib/algora/workspace/workspace.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,10 @@ defmodule Algora.Workspace do
461461
end
462462
end
463463

464+
def ensure_repo_tech_stack(_token, %{tech_stack: tech_stack}) when tech_stack != [] do
465+
{:ok, tech_stack}
466+
end
467+
464468
def ensure_repo_tech_stack(token, repository) do
465469
with {:ok, languages} <- Github.list_repository_languages(token, repository.user.provider_login, repository.name) do
466470
top_languages =

lib/algora_web/components/core_components.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ defmodule AlgoraWeb.CoreComponents do
595595
</div>
596596
</div>
597597
<% body -> %>
598-
<p class="text-[0.8125rem] flex items-center gap-3 font-semibold">
598+
<p class="pr-4 text-[0.8125rem] flex items-center gap-3 font-semibold">
599599
<.icon :if={@kind == :info} name="tabler-circle-check" class="h-6 w-6 text-success" />
600600
<.icon
601601
:if={@kind == :warning}
@@ -1236,11 +1236,12 @@ defmodule AlgoraWeb.CoreComponents do
12361236
attr :title, :string, default: nil
12371237
attr :subtitle, :string, default: nil
12381238
attr :link, :string, default: nil
1239+
attr :class, :string, default: nil
12391240
slot :inner_block
12401241

12411242
def section(assigns) do
12421243
~H"""
1243-
<div class="relative h-full">
1244+
<div class={classes(["relative h-full", @class])}>
12441245
<div :if={@title} class="flex items-end justify-between pb-2">
12451246
<div class="flex flex-col space-y-1.5">
12461247
<h2 class="text-2xl font-semibold leading-none tracking-tight">{@title}</h2>

0 commit comments

Comments
 (0)