commit cfcbde418e34349e4a3ece2baa3d01c2b2da3376
parent 2c8deac62dbd767a4aa4bf99cedec63dc8a2c7f2
Author: srfsh <dev@srf.sh>
Date: Tue, 16 Aug 2022 13:31:27 +0300
Zenflows{Test,}.VF.Proposal: init
Diffstat:
5 files changed, 581 insertions(+), 0 deletions(-)
diff --git a/src/zenflows/vf/proposal.ex b/src/zenflows/vf/proposal.ex
@@ -23,6 +23,7 @@ Published requests or offers, sometimes with what is expected in return.
use Zenflows.DB.Schema
alias Zenflows.VF.{
+ Intent,
ProposedIntent,
SpatialThing,
Validate,
@@ -35,6 +36,9 @@ alias Zenflows.VF.{
unit_based: boolean(),
note: String.t() | nil,
eligible_location: SpatialThing.t() | nil,
+ publishes: [ProposedIntent.t()],
+ primary_intents: [Intent.t()],
+ reciprocal_intents: [Intent.t()],
}
schema "vf_proposal" do
@@ -47,6 +51,10 @@ schema "vf_proposal" do
timestamps()
has_many :publishes, ProposedIntent, foreign_key: :published_in_id
+ many_to_many :primary_intents, Intent, join_through: ProposedIntent,
+ join_keys: [published_in_id: :id, publishes_id: :id], join_where: [reciprocal: false]
+ many_to_many :reciprocal_intents, Intent, join_through: ProposedIntent,
+ join_keys: [published_in_id: :id, publishes_id: :id], join_where: [reciprocal: true]
end
@cast ~w[name has_beginning has_end unit_based note eligible_location_id]a
diff --git a/src/zenflows/vf/proposal/domain.ex b/src/zenflows/vf/proposal/domain.ex
@@ -0,0 +1,101 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule Zenflows.VF.Proposal.Domain do
+@moduledoc "Domain logic of Proposals."
+
+alias Ecto.Multi
+alias Zenflows.DB.Repo
+alias Zenflows.VF.Proposal
+
+@typep repo() :: Ecto.Repo.t()
+@typep chgset() :: Ecto.Changeset.t()
+@typep id() :: Zenflows.DB.Schema.id()
+@typep params() :: Zenflows.DB.Schema.params()
+
+@spec one(repo(), id()) :: {:ok, Proposal.t()} | {:error, String.t()}
+def one(repo \\ Repo, id) do
+ one_by(repo, id: id)
+end
+
+@spec one_by(repo(), map() | Keyword.t())
+ :: {:ok, Proposal.t()} | {:error, String.t()}
+def one_by(repo \\ Repo, clauses) do
+ case repo.get_by(Proposal, clauses) do
+ nil -> {:error, "not found"}
+ found -> {:ok, found}
+ end
+end
+
+@spec create(params()) :: {:ok, Proposal.t()} | {:error, chgset()}
+def create(params) do
+ Multi.new()
+ |> Multi.insert(:insert, Proposal.chgset(params))
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{insert: p}} -> {:ok, p}
+ {:error, _, cset, _} -> {:error, cset}
+ end
+end
+
+@spec update(id(), params()) ::
+ {:ok, Proposal.t()} | {:error, String.t() | chgset()}
+def update(id, params) do
+ Multi.new()
+ |> Multi.put(:id, id)
+ |> Multi.run(:one, &one_by/2)
+ |> Multi.update(:update, &Proposal.chgset(&1.one, params))
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{update: p}} -> {:ok, p}
+ {:error, _, msg_or_cset, _} -> {:error, msg_or_cset}
+ end
+end
+
+@spec delete(id())
+ :: {:ok, Proposal.t()} | {:error, String.t() | chgset()}
+def delete(id) do
+ Multi.new()
+ |> Multi.put(:id, id)
+ |> Multi.run(:one, &one_by/2)
+ |> Multi.delete(:delete, &(&1.one))
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{delete: pi}} -> {:ok, pi}
+ {:error, _, msg_or_cset, _} -> {:error, msg_or_cset}
+ end
+end
+
+@spec preload(Proposal.t(), :eligible_location | :publishes
+ | :primary_intents | :reciprocal_intents)
+ :: Proposal.t()
+def preload(prop, :eligible_location) do
+ Repo.preload(prop, :eligible_location)
+end
+
+def preload(prop, :publishes) do
+ Repo.preload(prop, :publishes)
+end
+
+def preload(prop, :primary_intents) do
+ Repo.preload(prop, :primary_intents)
+end
+
+def preload(prop, :reciprocal_intents) do
+ Repo.preload(prop, :reciprocal_intents)
+end
+end
diff --git a/src/zenflows/vf/proposal/resolv.ex b/src/zenflows/vf/proposal/resolv.ex
@@ -0,0 +1,94 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule Zenflows.VF.Proposal.Resolv do
+@moduledoc "Resolvers of Proposal."
+
+alias Zenflows.VF.Proposal.Domain
+
+def eligible_location(prop, _, _) do
+ prop = Domain.preload(prop, :eligible_location)
+ {:ok, prop.eligible_location}
+end
+
+def publishes(prop, _, _) do
+ prop = Domain.preload(prop, :publishes)
+ {:ok, prop.publishes}
+end
+
+def primary_intents(prop, _, _) do
+ prop = Domain.preload(prop, :primary_intents)
+ {:ok, prop.primary_intents}
+end
+
+def reciprocal_intents(prop, _, _) do
+ prop = Domain.preload(prop, :reciprocal_intents)
+ {:ok, prop.reciprocal_intents}
+end
+
+def proposal(%{id: id}, _) do
+ Domain.one(id)
+end
+
+def proposals(_params, _) do
+ {:ok, %{
+ edges: [],
+ page_info: %{
+ has_previous_page: false,
+ has_next_page: false,
+ },
+ }}
+end
+
+def offers(_params, _) do
+ {:ok, %{
+ edges: [],
+ page_info: %{
+ has_previous_page: false,
+ has_next_page: false,
+ },
+ }}
+end
+
+def requests(_params, _) do
+ {:ok, %{
+ edges: [],
+ page_info: %{
+ has_previous_page: false,
+ has_next_page: false,
+ },
+ }}
+end
+
+def create_proposal(%{proposal: params}, _) do
+ with {:ok, prop} <- Domain.create(params) do
+ {:ok, %{proposal: prop}}
+ end
+end
+
+def update_proposal(%{proposal: %{id: id} = params}, _) do
+ with {:ok, prop} <- Domain.update(id, params) do
+ {:ok, %{proposal: prop}}
+ end
+end
+
+def delete_proposal(%{id: id}, _) do
+ with {:ok, _} <- Domain.delete(id) do
+ {:ok, true}
+ end
+end
+end
diff --git a/test/vf/proposal/domain.test.exs b/test/vf/proposal/domain.test.exs
@@ -0,0 +1,164 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule ZenflowsTest.VF.Proposal.Domain do
+use ZenflowsTest.Help.EctoCase, async: true
+
+alias Zenflows.VF.{
+ Proposal,
+ Proposal.Domain,
+ SpatialThing,
+}
+
+setup do
+ %{
+ params: %{
+ name: Factory.str("name"),
+ note: Factory.str("note"),
+ has_beginning: Factory.now(),
+ has_end: Factory.now(),
+ unit_based: Factory.bool(),
+ eligible_location_id: Factory.insert!(:spatial_thing).id,
+ },
+ inserted: Factory.insert!(:proposal),
+ id: Factory.id(),
+ }
+end
+
+describe "one/1" do
+ test "with good id: finds the Proposal", %{inserted: %{id: id}} do
+ assert {:ok, %Proposal{}} = Domain.one(id)
+ end
+
+ test "with bad id: doesn't find the Proposal", %{id: id} do
+ assert {:error, "not found"} = Domain.one(id)
+ end
+end
+
+describe "create/1" do
+ test "with good params: creates a Proposal", %{params: params} do
+ assert {:ok, %Proposal{} = prop} = Domain.create(params)
+ assert prop.name == params.name
+ assert prop.note == params.note
+ assert prop.has_beginning == params.has_beginning
+ assert prop.has_end == params.has_end
+ assert prop.unit_based == params.unit_based
+ assert prop.eligible_location_id == params.eligible_location_id
+ end
+
+ test "with empty params: creates a Proposal" do
+ assert {:ok, %Proposal{} = prop} = Domain.create(%{})
+ assert prop.name == nil
+ assert prop.note == nil
+ assert prop.has_beginning == nil
+ assert prop.has_end == nil
+ assert prop.unit_based == false # since it defaults
+ assert prop.eligible_location_id == nil
+ end
+end
+
+describe "update/2" do
+ test "with good params: updates the Proposal", %{params: params, inserted: old} do
+ assert {:ok, %Proposal{} = new} = Domain.update(old.id, params)
+
+ assert new.name == params.name
+ assert new.note == params.note
+ assert new.has_beginning == params.has_beginning
+ assert new.has_end == params.has_end
+ assert new.unit_based == params.unit_based
+ assert new.eligible_location_id == params.eligible_location_id
+ end
+
+ test "with bad params: doesn't update the Proposal", %{inserted: old} do
+ assert {:ok, %Proposal{} = new} = Domain.update(old.id, %{})
+
+ assert new.name == old.name
+ assert new.note == old.note
+ assert new.has_beginning == old.has_beginning
+ assert new.has_end == old.has_end
+ assert new.unit_based == old.unit_based
+ assert new.eligible_location_id == old.eligible_location_id
+ end
+end
+
+describe "delete/1" do
+ test "with good id: deletes the Proposal", %{inserted: %{id: id}} do
+ assert {:ok, %Proposal{id: ^id}} = Domain.delete(id)
+ assert {:error, "not found"} = Domain.one(id)
+ end
+
+ test "with bad id: doesn't delete the Proposal", %{id: id} do
+ assert {:error, "not found"} = Domain.delete(id)
+ end
+end
+
+describe "preload/2" do
+ test "preloads `:eligible_location`", %{inserted: %{id: id}} do
+ assert {:ok, prop} = Domain.one(id)
+ prop = Domain.preload(prop, :eligible_location)
+ assert %SpatialThing{} = prop.eligible_location
+ end
+
+ test "preloads `:publishes`", %{inserted: %{id: id}} do
+ assert {:ok, prop} = Domain.one(id)
+
+ assert [] = Domain.preload(prop, :publishes).publishes
+
+ left =
+ Enum.map(0..9, fn _ ->
+ Factory.insert!(:proposed_intent, %{published_in: prop}).id
+ end)
+ right =
+ Domain.preload(prop, :publishes)
+ |> Map.fetch!(:publishes)
+ |> Enum.map(& &1.id)
+ assert left -- right == []
+ end
+
+ test "preloads `:primary_intents`", %{inserted: %{id: id}} do
+ assert {:ok, prop} = Domain.one(id)
+
+ assert [] = Domain.preload(prop, :primary_intents).primary_intents
+
+ left =
+ Enum.map(0..9, fn _ ->
+ Factory.insert!(:proposed_intent, %{published_in: prop, reciprocal: false}).publishes_id
+ end)
+ right =
+ Domain.preload(prop, :primary_intents)
+ |> Map.fetch!(:primary_intents)
+ |> Enum.map(& &1.id)
+ assert left -- right == []
+ end
+
+ test "preloads `:reciprocal_intents`", %{inserted: %{id: id}} do
+ assert {:ok, prop} = Domain.one(id)
+
+ assert [] = Domain.preload(prop, :reciprocal_intents).reciprocal_intents
+
+ left =
+ Enum.map(0..9, fn _ ->
+ Factory.insert!(:proposed_intent, %{published_in: prop, reciprocal: true}).publishes_id
+ end)
+ right =
+ Domain.preload(prop, :reciprocal_intents)
+ |> Map.fetch!(:reciprocal_intents)
+ |> Enum.map(& &1.id)
+ assert left -- right == []
+ end
+end
+end
diff --git a/test/vf/proposal/type.test.exs b/test/vf/proposal/type.test.exs
@@ -0,0 +1,214 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule ZenflowsTest.VF.Proposal.Type do
+use ZenflowsTest.Help.AbsinCase, async: true
+
+setup do
+ %{
+ params: %{
+ "name" => Factory.str("name"),
+ "note" => Factory.str("note"),
+ "hasBeginning" => Factory.iso_now(),
+ "hasEnd" => Factory.iso_now(),
+ "unitBased" => Factory.bool(),
+ "eligibleLocation" => Factory.insert!(:spatial_thing).id,
+ },
+ inserted: Factory.insert!(:proposal),
+ }
+end
+
+@frag """
+fragment proposal on Proposal {
+ id
+ name
+ note
+ hasBeginning
+ hasEnd
+ unitBased
+ eligibleLocation {id}
+}
+"""
+describe "Query" do
+ test "proposal", %{inserted: prop} do
+ assert %{data: %{"proposal" => data}} =
+ run!("""
+ #{@frag}
+ query ($id: ID!) {
+ proposal(id: $id) {...proposal}
+ }
+ """, vars: %{"id" => prop.id})
+
+ assert data["id"] == prop.id
+ assert data["name"] == prop.name
+ assert data["note"] == prop.note
+ assert data["unitBased"] == prop.unit_based
+ assert data["eligibleLocation"]["id"] == prop.eligible_location_id
+ assert {:ok, has_beginning, 0} = DateTime.from_iso8601(data["hasBeginning"])
+ assert DateTime.compare(DateTime.utc_now(), has_beginning) != :lt
+ assert {:ok, has_end, 0} = DateTime.from_iso8601(data["hasEnd"])
+ assert DateTime.compare(DateTime.utc_now(), has_end) != :lt
+ end
+
+ test "proposals" do
+ assert %{data: %{"proposals" => data}} =
+ run!("""
+ #{@frag}
+ query {
+ proposals {
+ pageInfo {
+ startCursor
+ endCursor
+ hasPreviousPage
+ hasNextPage
+ totalCount
+ pageLimit
+ }
+ edges {
+ cursor
+ node {...proposal}
+ }
+ }
+ }
+ """)
+ assert %{
+ "pageInfo" => %{
+ "startCursor" => nil,
+ "endCursor" => nil,
+ "hasPreviousPage" => false,
+ "hasNextPage" => false,
+ "totalCount" => nil,
+ "pageLimit" => nil,
+ },
+ "edges" => [],
+ } = data
+ end
+
+ test "offers" do
+ assert %{data: %{"offers" => data}} =
+ run!("""
+ #{@frag}
+ query {
+ offers {
+ pageInfo {
+ startCursor
+ endCursor
+ hasPreviousPage
+ hasNextPage
+ totalCount
+ pageLimit
+ }
+ edges {
+ cursor
+ node {...proposal}
+ }
+ }
+ }
+ """)
+ assert %{
+ "pageInfo" => %{
+ "startCursor" => nil,
+ "endCursor" => nil,
+ "hasPreviousPage" => false,
+ "hasNextPage" => false,
+ "totalCount" => nil,
+ "pageLimit" => nil,
+ },
+ "edges" => [],
+ } = data
+ end
+
+ test "requests" do
+ assert %{data: %{"requests" => data}} =
+ run!("""
+ #{@frag}
+ query {
+ requests {
+ pageInfo {
+ startCursor
+ endCursor
+ hasPreviousPage
+ hasNextPage
+ totalCount
+ pageLimit
+ }
+ edges {
+ cursor
+ node {...proposal}
+ }
+ }
+ }
+ """)
+ assert %{
+ "pageInfo" => %{
+ "startCursor" => nil,
+ "endCursor" => nil,
+ "hasPreviousPage" => false,
+ "hasNextPage" => false,
+ "totalCount" => nil,
+ "pageLimit" => nil,
+ },
+ "edges" => [],
+ } = data
+ end
+end
+
+describe "Mutation" do
+ test "createProposal", %{params: params} do
+ assert %{data: %{"createProposal" => %{"proposal" => data}}} =
+ run!("""
+ #{@frag}
+ mutation ($proposal: ProposalCreateParams!) {
+ createProposal(proposal: $proposal) {
+ proposal {...proposal}
+ }
+ }
+ """, vars: %{"proposal" => params})
+
+ assert {:ok, _} = Zenflows.DB.ID.cast(data["id"])
+ keys = ~w[name note unitBased hasBeginning hasEnd]
+ assert Map.take(data, keys) == Map.take(params, keys)
+ assert data["eligibleLocation"]["id"] == params["eligibleLocation"]
+ end
+
+ test "updateProposal", %{params: params, inserted: prop} do
+ assert %{data: %{"updateProposal" => %{"proposal" => data}}} =
+ run!("""
+ #{@frag}
+ mutation ($proposal: ProposalUpdateParams!) {
+ updateProposal(proposal: $proposal) {
+ proposal {...proposal}
+ }
+ }
+ """, vars: %{"proposal" => params |> Map.put("id", prop.id)})
+
+ assert data["id"] == prop.id
+ keys = ~w[name note unitBased hasBeginning hasEnd]
+ assert Map.take(data, keys) == Map.take(params, keys)
+ assert data["eligibleLocation"]["id"] == params["eligibleLocation"]
+ end
+
+ test "deleteProposal", %{inserted: %{id: id}} do
+ assert %{data: %{"deleteProposal" => true}} =
+ run!("""
+ mutation ($id: ID!) {
+ deleteProposal(id: $id)
+ }
+ """, vars: %{"id" => id})
+ end
+end
+end