Skip to content

Commit 58ef2c5

Browse files
committed
activities (with has_many on_replace: :ignore)
1 parent 205a56a commit 58ef2c5

File tree

13 files changed

+369
-4
lines changed

13 files changed

+369
-4
lines changed

lib/algora/accounts/schemas/identity.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ defmodule Algora.Accounts.Identity do
1515
field :provider_id, :string
1616
field :provider_meta, :map
1717

18+
has_many :activities, {"identity_activities", Algora.Activities.Activity}, foreign_key: :assoc_id
19+
1820
belongs_to :user, User
1921

2022
timestamps()

lib/algora/accounts/schemas/user.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ defmodule Algora.Accounts.User do
8585
has_many :connected_installations, Installation, foreign_key: :connected_user_id
8686
has_many :contractor_contracts, Contract, foreign_key: :contractor_id
8787
has_many :client_contracts, Contract, foreign_key: :client_id
88+
has_many :activities, {"account_activities", Algora.Activities.Activity}, foreign_key: :assoc_id
8889

8990
has_one :customer, Algora.Payments.Customer, foreign_key: :user_id
9091

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
defmodule Algora.Activities do
2+
@moduledoc false
3+
import Ecto.Changeset
4+
import Ecto.Query
5+
6+
alias Algora.Activities.Activity
7+
alias Algora.Repo
8+
9+
@tables [
10+
:account_activities,
11+
:contract_activities
12+
]
13+
14+
def base_query do
15+
[head | tail] = @tables
16+
17+
query = head |> to_string() |> base_query()
18+
19+
Enum.reduce(tail, query, fn table_name, acc ->
20+
new_query = table_name |> to_string() |> base_query()
21+
union_all(new_query, ^acc)
22+
end)
23+
end
24+
25+
def base_query(table_name) do
26+
from(_e in {table_name, Activity})
27+
end
28+
29+
def base_query_for_user(user_id) do
30+
[head | tail] = [:client_contracts, :contractor_contracts]
31+
first_query = base_query_for_user(user_id, head)
32+
33+
Enum.reduce(tail, first_query, fn relation_name, acc ->
34+
new_query = base_query_for_user(user_id, relation_name)
35+
union_all(new_query, ^acc)
36+
end)
37+
end
38+
39+
def base_query_for_user(user_id, name) do
40+
from u in Algora.Accounts.User,
41+
where: u.id == ^user_id,
42+
join: c in assoc(u, ^name),
43+
join: a in assoc(c, :activities),
44+
select: a
45+
end
46+
47+
def all(table_name) when is_binary(table_name) do
48+
table_name
49+
|> base_query()
50+
|> order_by(desc: :inserted_at)
51+
|> Repo.all()
52+
end
53+
54+
def all(target) when is_map(target) do
55+
Repo.all(Ecto.assoc(target, :activities))
56+
end
57+
58+
def all do
59+
base_query()
60+
|> order_by(fragment("inserted_at DESC"))
61+
|> limit(40)
62+
|> Repo.all()
63+
end
64+
65+
def all_for_user(user_id) do
66+
user_id
67+
|> base_query_for_user()
68+
|> order_by(fragment("inserted_at DESC"))
69+
|> limit(40)
70+
|> Repo.all()
71+
end
72+
73+
def insert(target, activity) do
74+
target
75+
|> Activity.build_activity(activity)
76+
|> Algora.Repo.insert()
77+
end
78+
end
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
defmodule Algora.Activities.Activity do
2+
@moduledoc false
3+
use Algora.Schema
4+
5+
require Protocol
6+
7+
@activity_types ~w{
8+
contract_paid
9+
contract_prepaid
10+
contract_created
11+
contract_renewed
12+
transaction_status_change
13+
transaction_created
14+
transaction_failed
15+
transaction_processed
16+
}a
17+
18+
typed_schema "activities" do
19+
field :assoc_id, :string
20+
field :type, Ecto.Enum, values: @activity_types
21+
field :visibility, Ecto.Enum, values: [:public, :private, :internal], default: :internal
22+
field :template, :string
23+
field :meta, :map, default: %{}
24+
field :changes, :map, default: %{}
25+
field :trace_id, :string
26+
field :notify_users, {:array, :string}, default: []
27+
28+
belongs_to :user, Algora.Accounts.User
29+
belongs_to :previous_event, __MODULE__
30+
31+
timestamps()
32+
end
33+
34+
@doc false
35+
def changeset(activity, attrs) do
36+
activity
37+
|> cast(attrs, [:type, :visibility, :template, :meta, :changes, :trace_id, :user_id, :previous_event_id])
38+
|> validate_required([:type])
39+
|> foreign_key_constraint(:assoc_id)
40+
|> foreign_key_constraint(:user_id)
41+
|> foreign_key_constraint(:previous_event_id)
42+
|> generate_id()
43+
end
44+
45+
def build_activity(target, %{meta: %struct{}} = activity) when struct in [Stripe.Error] do
46+
build_activity(target, %{activity | meta: Algora.Utils.normalize_struct(struct)})
47+
end
48+
49+
def build_activity(target, activity) do
50+
target
51+
|> Ecto.build_assoc(:activities)
52+
|> Algora.Activities.Activity.changeset(activity)
53+
end
54+
55+
def put_activity(target, activity) do
56+
put_activity(change(target), target, activity)
57+
end
58+
59+
def put_activiies(target, activities) do
60+
put_activities(change(target), target, activities)
61+
end
62+
63+
def put_activity(changeset, target, activity) do
64+
put_activities(changeset, target, [activity])
65+
end
66+
67+
def put_activities(%Ecto.Changeset{changes: changes} = changeset, target, activities) do
68+
put_assoc(
69+
changeset,
70+
:activities,
71+
Enum.map(activities, fn activity ->
72+
build_activity(target, put_changes(activity, changes))
73+
end)
74+
)
75+
end
76+
77+
defp put_changes(activity, changes) do
78+
changes = Map.delete(changes, :activities)
79+
Map.put(activity, :changes, changes)
80+
end
81+
end

lib/algora/bounties/schemas/bounty.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ defmodule Algora.Bounties.Bounty do
1616
has_many :attempts, Algora.Bounties.Attempt
1717
has_many :claims, Algora.Bounties.Claim
1818
has_many :transactions, Algora.Payments.Transaction
19+
has_many :activities, {"bounty_activities", Algora.Activities.Activity}, foreign_key: :assoc_id
1920

2021
timestamps()
2122
end

lib/algora/contracts/contracts.ex

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
defmodule Algora.Contracts do
22
@moduledoc false
3+
import Algora.Activities.Activity, only: [put_activity: 2, put_activity: 3]
34
import Ecto.Changeset
45
import Ecto.Query
56

7+
alias Algora.Activities
68
alias Algora.Contracts.Contract
79
alias Algora.Contracts.Timesheet
810
alias Algora.FeeTier
@@ -170,6 +172,12 @@ defmodule Algora.Contracts do
170172
total_fee: total_fee,
171173
line_items: line_items
172174
})
175+
|> put_assoc(
176+
:contract,
177+
put_activity(contract, %{
178+
type: :transaction_created
179+
})
180+
)
173181
|> Algora.Validations.validate_positive(:gross_amount)
174182
|> Algora.Validations.validate_positive(:net_amount)
175183
|> Algora.Validations.validate_positive(:total_fee)
@@ -196,6 +204,12 @@ defmodule Algora.Contracts do
196204
total_fee: Money.zero(:USD),
197205
linked_transaction_id: linked_transaction_id
198206
})
207+
|> put_assoc(
208+
:contract,
209+
put_activity(contract, %{
210+
type: :transaction_created
211+
})
212+
)
199213
|> Algora.Validations.validate_positive(:gross_amount)
200214
|> Algora.Validations.validate_positive(:net_amount)
201215
|> foreign_key_constraint(:original_contract_id)
@@ -221,6 +235,12 @@ defmodule Algora.Contracts do
221235
user_id: contract.contractor_id,
222236
linked_transaction_id: linked_transaction_id
223237
})
238+
|> put_assoc(
239+
:contract,
240+
put_activity(contract, %{
241+
type: :transaction_created
242+
})
243+
)
224244
|> Algora.Validations.validate_positive(:gross_amount)
225245
|> Algora.Validations.validate_positive(:net_amount)
226246
|> foreign_key_constraint(:original_contract_id)
@@ -245,6 +265,12 @@ defmodule Algora.Contracts do
245265
timesheet_id: contract.timesheet.id,
246266
user_id: contract.contractor_id
247267
})
268+
|> put_assoc(
269+
:contract,
270+
put_activity(contract, %{
271+
type: :transaction_created
272+
})
273+
)
248274
|> Algora.Validations.validate_positive(:gross_amount)
249275
|> Algora.Validations.validate_positive(:net_amount)
250276
|> foreign_key_constraint(:original_contract_id)
@@ -377,7 +403,16 @@ defmodule Algora.Contracts do
377403
with {:ok, txs} <- initialize_prepayment_transaction(contract),
378404
{:ok, invoice} <- maybe_generate_invoice(contract, txs.charge),
379405
{:ok, _invoice} <- maybe_pay_invoice(contract, invoice, txs) do
406+
Activities.insert(contract, %{type: :contract_prepaid})
407+
380408
{:ok, txs}
409+
else
410+
error ->
411+
Activities.insert(contract, %{
412+
type: :contract_prepayment_failed
413+
})
414+
415+
error
381416
end
382417
end
383418

@@ -483,6 +518,7 @@ defmodule Algora.Contracts do
483518

484519
{:error, error} ->
485520
update_transaction_status(transaction, {:error, error})
521+
Activities.insert(contract, %{type: :contract_prepayment_failed})
486522
{:error, error}
487523
end
488524
end
@@ -493,6 +529,13 @@ defmodule Algora.Contracts do
493529
provider_meta: Util.normalize_struct(%{error: error}),
494530
status: :failed
495531
})
532+
|> put_assoc(
533+
:contract,
534+
put_activity(transaction.contract, %{
535+
type: :transaction_status_change,
536+
meta: %{status: :failed, transaction_id: transaction.id}
537+
})
538+
)
496539
|> Repo.update()
497540
end
498541

@@ -504,12 +547,26 @@ defmodule Algora.Contracts do
504547
status: status,
505548
succeeded_at: if(status == :succeeded, do: DateTime.utc_now())
506549
})
550+
|> put_assoc(
551+
:contract,
552+
put_activity(transaction.contract, %{
553+
type: :transaction_status_change,
554+
meta: %{status: status, transaction_id: transaction.id}
555+
})
556+
)
507557
|> Repo.update()
508558
end
509559

510560
defp mark_contract_as_paid(contract) do
511561
contract
512562
|> change(%{status: :paid})
563+
|> put_activity(contract, %{
564+
type: :contract_paid,
565+
meta: %{},
566+
template: "",
567+
trace_id: Nanoid.generate(),
568+
notify_users: []
569+
})
513570
|> Repo.update()
514571
end
515572

@@ -527,6 +584,10 @@ defmodule Algora.Contracts do
527584
hourly_rate: contract.hourly_rate,
528585
hours_per_week: contract.hours_per_week
529586
})
587+
|> put_activity(contract, %{
588+
type: :contract_renewed,
589+
trace_id: Nanoid.generate()
590+
})
530591
|> Repo.insert()
531592
end
532593

@@ -601,6 +662,7 @@ defmodule Algora.Contracts do
601662
on: tt.original_contract_id == c.original_contract_id,
602663
as: :tt
603664
)
665+
|> join(:left, [c], act in assoc(c, :activities), as: :act)
604666
|> select_merge([ta: ta, tt: tt], %{
605667
amount_credited: Algora.SQL.money_or_zero(ta.amount_credited),
606668
amount_debited: Algora.SQL.money_or_zero(ta.amount_debited),
@@ -611,11 +673,12 @@ defmodule Algora.Contracts do
611673
total_transferred: Algora.SQL.money_or_zero(tt.total_transferred),
612674
total_withdrawn: Algora.SQL.money_or_zero(tt.total_withdrawn)
613675
})
614-
|> preload([ts: ts, txs: txs, cl: cl, ct: ct, cu: cu, dpm: dpm],
676+
|> preload([ts: ts, txs: txs, cl: cl, ct: ct, cu: cu, dpm: dpm, act: act],
615677
timesheet: ts,
616678
transactions: txs,
617679
client: {cl, customer: {cu, default_payment_method: dpm}},
618-
contractor: ct
680+
contractor: ct,
681+
activities: act
619682
)
620683
|> Repo.all()
621684
|> Enum.map(&Contract.after_load/1)

lib/algora/contracts/schemas/contract.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule Algora.Contracts.Contract do
33
use Algora.Schema
44

55
alias Algora.Accounts.User
6+
alias Algora.Activities.Activity
67
alias Algora.Contracts.Contract
78
alias Algora.MoneyUtils
89

@@ -36,6 +37,8 @@ defmodule Algora.Contracts.Contract do
3637
has_many :reviews, Algora.Reviews.Review
3738
has_one :timesheet, Algora.Contracts.Timesheet
3839

40+
has_many :activities, {"contract_activities", Activity}, foreign_key: :assoc_id, on_replace: :ignore
41+
3942
timestamps()
4043
end
4144

lib/algora/payments/schemas/transaction.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule Algora.Payments.Transaction do
22
@moduledoc false
33
use Algora.Schema
44

5+
alias Algora.Activities.Activity
56
alias Algora.Contracts.Contract
67
alias Algora.Types.Money
78

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ defmodule Algora.MixProject do
3838
# Type `mix help deps` for examples and options.
3939
defp deps do
4040
[
41+
{:ecto, github: "lastcanal/ecto", branch: "has_many_on_replace_ignore", override: true},
4142
{:phoenix, "~> 1.7.11"},
4243
{:phoenix_ecto, "~> 4.5"},
4344
{:ecto_sql, "~> 3.10"},

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
1717
"digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"},
1818
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
19-
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
19+
"ecto": {:git, "https://github.com/lastcanal/ecto.git", "c730389a3965004dca68a8a173ddd6d102b15b4f", [branch: "has_many_on_replace_ignore"]},
2020
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
2121
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
2222
"ex_aws": {:hex, :ex_aws, "2.5.8", "0393cfbc5e4a9e7017845451a015d836a670397100aa4c86901980e2a2c5f7d4", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8f79777b7932168956c8cc3a6db41f5783aa816eb50de356aed3165a71e5f8c3"},

0 commit comments

Comments
 (0)