Skip to content

Commit 2079dbe

Browse files
committed
feat: add contexts for job interviews and matches
1 parent 75909d1 commit 2079dbe

File tree

6 files changed

+351
-4
lines changed

6 files changed

+351
-4
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
defmodule Algora.Interviews do
2+
@moduledoc """
3+
The Interviews context.
4+
"""
5+
6+
import Ecto.Query, warn: false
7+
8+
alias Algora.Interviews.JobInterview
9+
alias Algora.Repo
10+
11+
@doc """
12+
Returns the list of job interviews.
13+
14+
## Examples
15+
16+
iex> list_job_interviews()
17+
[%JobInterview{}, ...]
18+
19+
"""
20+
def list_job_interviews do
21+
Repo.all(JobInterview)
22+
end
23+
24+
@doc """
25+
Returns the list of job interviews grouped by organization.
26+
Groups by the job posting's user (which represents the organization).
27+
28+
## Examples
29+
30+
iex> list_job_interviews_by_org()
31+
%{org_id => [%JobInterview{}, ...]}
32+
33+
"""
34+
def list_job_interviews_by_org do
35+
JobInterview
36+
|> join(:inner, [ji], jp in assoc(ji, :job_posting))
37+
|> join(:inner, [ji, jp], org in assoc(jp, :user))
38+
|> preload([ji, jp, org], job_posting: {jp, user: org})
39+
|> preload(:user)
40+
|> Repo.all()
41+
|> Enum.group_by(fn interview -> interview.job_posting.user_id end)
42+
end
43+
44+
@doc """
45+
Gets a single job interview.
46+
47+
Raises `Ecto.NoResultsError` if the Job interview does not exist.
48+
49+
## Examples
50+
51+
iex> get_job_interview!(123)
52+
%JobInterview{}
53+
54+
iex> get_job_interview!(456)
55+
** (Ecto.NoResultsError)
56+
57+
"""
58+
def get_job_interview!(id), do: Repo.get!(JobInterview, id)
59+
60+
@doc """
61+
Creates a job interview.
62+
63+
## Examples
64+
65+
iex> create_job_interview(%{field: value})
66+
{:ok, %JobInterview{}}
67+
68+
iex> create_job_interview(%{field: bad_value})
69+
{:error, %Ecto.Changeset{}}
70+
71+
"""
72+
def create_job_interview(attrs \\ %{}) do
73+
%JobInterview{}
74+
|> JobInterview.changeset(attrs)
75+
|> Repo.insert()
76+
end
77+
78+
@doc """
79+
Updates a job interview.
80+
81+
## Examples
82+
83+
iex> update_job_interview(job_interview, %{field: new_value})
84+
{:ok, %JobInterview{}}
85+
86+
iex> update_job_interview(job_interview, %{field: bad_value})
87+
{:error, %Ecto.Changeset{}}
88+
89+
"""
90+
def update_job_interview(%JobInterview{} = job_interview, attrs) do
91+
job_interview
92+
|> JobInterview.changeset(attrs)
93+
|> Repo.update()
94+
end
95+
96+
@doc """
97+
Deletes a job interview.
98+
99+
## Examples
100+
101+
iex> delete_job_interview(job_interview)
102+
{:ok, %JobInterview{}}
103+
104+
iex> delete_job_interview(job_interview)
105+
{:error, %Ecto.Changeset{}}
106+
107+
"""
108+
def delete_job_interview(%JobInterview{} = job_interview) do
109+
Repo.delete(job_interview)
110+
end
111+
112+
@doc """
113+
Returns an `%Ecto.Changeset{}` for tracking job interview changes.
114+
115+
## Examples
116+
117+
iex> change_job_interview(job_interview)
118+
%Ecto.Changeset{data: %JobInterview{}}
119+
120+
"""
121+
def change_job_interview(%JobInterview{} = job_interview, attrs \\ %{}) do
122+
JobInterview.changeset(job_interview, attrs)
123+
end
124+
125+
@doc """
126+
Creates a job interview with the given status.
127+
128+
## Examples
129+
130+
iex> create_interview_with_status(user_id, job_posting_id, :scheduled)
131+
{:ok, %JobInterview{}}
132+
133+
iex> create_interview_with_status(user_id, job_posting_id, :completed, "Great candidate!")
134+
{:ok, %JobInterview{}}
135+
136+
"""
137+
def create_interview_with_status(user_id, job_posting_id, status, notes \\ nil) do
138+
attrs = %{
139+
user_id: user_id,
140+
job_posting_id: job_posting_id,
141+
status: status,
142+
notes: notes
143+
}
144+
145+
attrs =
146+
case status do
147+
:scheduled ->
148+
Map.put(attrs, :scheduled_at, DateTime.utc_now())
149+
150+
:completed ->
151+
attrs
152+
|> Map.put(:scheduled_at, DateTime.utc_now())
153+
|> Map.put(:completed_at, DateTime.utc_now())
154+
155+
_ ->
156+
attrs
157+
end
158+
159+
create_job_interview(attrs)
160+
end
161+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule Algora.Interviews.JobInterview do
2+
@moduledoc false
3+
use Algora.Schema
4+
5+
import Ecto.Changeset
6+
7+
typed_schema "job_interviews" do
8+
field :status, Ecto.Enum, values: [:scheduled, :completed, :cancelled, :no_show]
9+
field :notes, :string
10+
field :scheduled_at, :utc_datetime_usec
11+
field :completed_at, :utc_datetime_usec
12+
13+
belongs_to :user, Algora.Accounts.User
14+
belongs_to :job_posting, Algora.Jobs.JobPosting
15+
16+
timestamps()
17+
end
18+
19+
def changeset(job_interview, attrs) do
20+
job_interview
21+
|> cast(attrs, [:user_id, :job_posting_id, :status, :notes, :scheduled_at, :completed_at])
22+
|> validate_required([:user_id, :job_posting_id, :status])
23+
|> validate_inclusion(:status, [:scheduled, :completed, :cancelled, :no_show])
24+
|> foreign_key_constraint(:user_id)
25+
|> foreign_key_constraint(:job_posting_id)
26+
|> generate_id()
27+
end
28+
end

lib/algora/jobs/jobs.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ defmodule Algora.Jobs do
66

77
alias Algora.Accounts.User
88
alias Algora.Bounties.LineItem
9+
alias Algora.Interviews.JobInterview
910
alias Algora.Jobs.JobApplication
1011
alias Algora.Jobs.JobPosting
12+
alias Algora.Matches.JobMatch
1113
alias Algora.Payments
1214
alias Algora.Payments.Transaction
1315
alias Algora.Repo
1416
alias Algora.Util
15-
alias AlgoraCloud.Interviews.JobInterview
16-
alias AlgoraCloud.Matches.JobMatch
1717

1818
require Logger
1919

lib/algora/jobs/schemas/job_posting.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ defmodule Algora.Jobs.JobPosting do
2525
field :system_tags, {:array, :string}, default: []
2626

2727
belongs_to :user, User, null: false
28-
has_many :interviews, AlgoraCloud.Interviews.JobInterview, foreign_key: :job_posting_id
29-
has_many :matches, AlgoraCloud.Matches.JobMatch, foreign_key: :job_posting_id
28+
has_many :interviews, Algora.Interviews.JobInterview, foreign_key: :job_posting_id
29+
has_many :matches, Algora.Matches.JobMatch, foreign_key: :job_posting_id
3030

3131
timestamps()
3232
end

lib/algora/matches/matches.ex

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
defmodule Algora.Matches do
2+
@moduledoc false
3+
4+
import Ecto.Query, warn: false
5+
6+
alias Algora.Jobs.JobPosting
7+
alias Algora.Matches.JobMatch
8+
alias Algora.Repo
9+
10+
def list_job_matches(opts \\ []) do
11+
order_by_clause = opts[:order_by] || [desc: :inserted_at]
12+
13+
JobMatch
14+
|> filter_by_job_posting_id(opts[:job_posting_id])
15+
|> filter_by_user_id(opts[:user_id])
16+
|> filter_by_status(opts[:status])
17+
|> order_by(^order_by_clause)
18+
|> maybe_preload(opts[:preload])
19+
|> Repo.all()
20+
end
21+
22+
def get_job_match!(id) do
23+
JobMatch
24+
|> preload([:user, :job_posting])
25+
|> Repo.get!(id)
26+
end
27+
28+
def get_job_match(user_id, job_posting_id) do
29+
JobMatch
30+
|> where([m], m.user_id == ^user_id and m.job_posting_id == ^job_posting_id)
31+
|> preload([:user, :job_posting])
32+
|> Repo.one()
33+
end
34+
35+
def create_job_match(attrs \\ %{}) do
36+
%JobMatch{}
37+
|> JobMatch.changeset(attrs)
38+
|> Repo.insert()
39+
end
40+
41+
def fetch_job_matches(job_posting_id) do
42+
job = Repo.get!(JobPosting, job_posting_id)
43+
44+
job_countries =
45+
job.regions
46+
|> Enum.flat_map(&Algora.PSP.ConnectCountries.get_countries/1)
47+
|> Enum.concat(job.countries)
48+
|> Enum.uniq()
49+
50+
[
51+
limit: 3,
52+
tech_stack: job.tech_stack,
53+
has_min_compensation: true,
54+
system_tags: job.system_tags,
55+
sort_by: [{"countries", job_countries}]
56+
]
57+
|> Algora.Cloud.list_top_matches()
58+
|> Algora.Settings.load_matches_2()
59+
end
60+
61+
def create_job_matches(job_posting_id) do
62+
job_posting_id
63+
|> fetch_job_matches()
64+
|> Enum.map(fn match -> match.user.id end)
65+
|> then(&create_job_matches(job_posting_id, &1))
66+
end
67+
68+
def create_job_matches(job_posting_id, user_ids) do
69+
matches =
70+
Enum.map(user_ids, fn user_id ->
71+
%{
72+
id: Nanoid.generate(),
73+
user_id: user_id,
74+
job_posting_id: job_posting_id,
75+
inserted_at: DateTime.truncate(DateTime.utc_now(), :microsecond),
76+
updated_at: DateTime.truncate(DateTime.utc_now(), :microsecond)
77+
}
78+
end)
79+
80+
Repo.transaction(fn ->
81+
# Delete existing matches for this job posting
82+
Repo.delete_all(from(m in JobMatch, where: m.job_posting_id == ^job_posting_id))
83+
84+
# Insert new matches
85+
case Repo.insert_all(JobMatch, matches, on_conflict: :nothing) do
86+
{count, _} -> count
87+
error -> Repo.rollback(error)
88+
end
89+
end)
90+
end
91+
92+
def update_job_match(%JobMatch{} = job_match, attrs) do
93+
job_match
94+
|> JobMatch.changeset(attrs)
95+
|> Repo.update()
96+
end
97+
98+
def delete_job_match(%JobMatch{} = job_match) do
99+
Repo.delete(job_match)
100+
end
101+
102+
def change_job_match(%JobMatch{} = job_match, attrs \\ %{}) do
103+
JobMatch.changeset(job_match, attrs)
104+
end
105+
106+
# Private helper functions
107+
defp filter_by_job_posting_id(query, nil), do: query
108+
109+
defp filter_by_job_posting_id(query, job_posting_id) do
110+
where(query, [m], m.job_posting_id == ^job_posting_id)
111+
end
112+
113+
defp filter_by_user_id(query, nil), do: query
114+
115+
defp filter_by_user_id(query, user_id) do
116+
where(query, [m], m.user_id == ^user_id)
117+
end
118+
119+
defp filter_by_status(query, nil), do: query
120+
121+
defp filter_by_status(query, status) do
122+
where(query, [m], m.status == ^status)
123+
end
124+
125+
defp maybe_preload(query, nil), do: query
126+
127+
defp maybe_preload(query, preload_list) do
128+
preload(query, ^preload_list)
129+
end
130+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule Algora.Matches.JobMatch do
2+
@moduledoc false
3+
use Algora.Schema
4+
5+
import Ecto.Changeset
6+
7+
typed_schema "job_matches" do
8+
field :status, Ecto.Enum, values: [:pending, :accepted, :rejected], default: :pending
9+
field :score, :decimal
10+
field :notes, :string
11+
12+
belongs_to :user, Algora.Accounts.User
13+
belongs_to :job_posting, Algora.Jobs.JobPosting
14+
15+
timestamps()
16+
end
17+
18+
def changeset(job_match, attrs) do
19+
job_match
20+
|> cast(attrs, [:user_id, :job_posting_id, :status, :score, :notes])
21+
|> validate_required([:user_id, :job_posting_id])
22+
|> validate_inclusion(:status, [:pending, :accepted, :rejected])
23+
|> foreign_key_constraint(:user_id)
24+
|> foreign_key_constraint(:job_posting_id)
25+
|> unique_constraint([:user_id, :job_posting_id])
26+
|> generate_id()
27+
end
28+
end

0 commit comments

Comments
 (0)