diff --git a/lib/pearl/accounts/roles/permissions.ex b/lib/pearl/accounts/roles/permissions.ex
index b5d5240..b920541 100644
--- a/lib/pearl/accounts/roles/permissions.ex
+++ b/lib/pearl/accounts/roles/permissions.ex
@@ -10,6 +10,8 @@ defmodule Pearl.Accounts.Roles.Permissions do
"staffs" => ["show", "edit", "roles_edit"],
"challenges" => ["show", "edit", "delete"],
"companies" => ["edit"],
+ "tickets" => ["edit"],
+ "discount_codes" => ["edit"],
"enrolments" => ["show", "edit"],
"products" => ["show", "edit", "delete"],
"purchases" => ["show", "redeem", "refund"],
diff --git a/lib/pearl/accounts/user.ex b/lib/pearl/accounts/user.ex
index f9dba67..f95f248 100644
--- a/lib/pearl/accounts/user.ex
+++ b/lib/pearl/accounts/user.ex
@@ -35,6 +35,10 @@ defmodule Pearl.Accounts.User do
schema "users" do
field :name, :string
field :email, :string
+ # field :notes, :string
+ # field :university, :string
+ # field :course, :string
+ # field :code, :string
field :handle, :string
field :picture, Pearl.Uploaders.UserPicture.Type
field :password, :string, virtual: true, redact: true
diff --git a/lib/pearl/discount_codes.ex b/lib/pearl/discount_codes.ex
new file mode 100644
index 0000000..5228e81
--- /dev/null
+++ b/lib/pearl/discount_codes.ex
@@ -0,0 +1,137 @@
+defmodule Pearl.DiscountCodes do
+ @moduledoc """
+ The DiscountCodes context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Pearl.Repo
+
+ alias Pearl.DiscountCodes.DiscountCode
+
+ @doc """
+ Returns the list of discount_codes.
+
+ ## Examples
+
+ iex> list_discount_codes()
+ [%DiscountCode{}, ...]
+
+ """
+ def list_discount_codes(params \\ %{}) do
+ DiscountCode
+ |> preload(:ticket_types)
+ |> Flop.validate_and_run(params, for: DiscountCode)
+ end
+
+ @doc """
+ Gets a single discount_code.
+
+ Raises `Ecto.NoResultsError` if the Discount code does not exist.
+
+ ## Examples
+
+ iex> get_discount_code!(123)
+ %DiscountCode{}
+
+ iex> get_discount_code!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_discount_code!(id) do
+ DiscountCode
+ |> Repo.get!(id)
+ |> Repo.preload(:ticket_types)
+ end
+
+ @doc """
+ Creates a discount_code.
+
+ ## Examples
+
+ iex> create_discount_code(%{field: value})
+ {:ok, %DiscountCode{}}
+
+ iex> create_discount_code(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_discount_code(attrs) do
+ %DiscountCode{}
+ |> Repo.preload(:ticket_types)
+ |> DiscountCode.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a discount_code.
+
+ ## Examples
+
+ iex> update_discount_code(discount_code, %{field: new_value})
+ {:ok, %DiscountCode{}}
+
+ iex> update_discount_code(discount_code, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_discount_code(%DiscountCode{} = discount_code, attrs) do
+ discount_code
+ |> Repo.preload(:ticket_types)
+ |> DiscountCode.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a discount_code.
+
+ ## Examples
+
+ iex> delete_discount_code(discount_code)
+ {:ok, %DiscountCode{}}
+
+ iex> delete_discount_code(discount_code)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_discount_code(%DiscountCode{} = discount_code) do
+ Repo.delete(discount_code)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking discount_code changes.
+
+ ## Examples
+
+ iex> change_discount_code(discount_code)
+ %Ecto.Changeset{data: %DiscountCode{}}
+
+ """
+ def change_discount_code(%DiscountCode{} = discount_code, attrs \\ %{}) do
+ DiscountCode.changeset(discount_code, attrs)
+ end
+
+ @doc """
+ Updates a discount code's ticket types.
+
+ ## Examples
+
+ iex> upsert_discount_code_ticket_types(discount_code, ["id1", "id2"])
+ {:ok, %DiscountCode{}}
+
+ iex> upsert_discount_code_ticket_types(discount_code, ["id1", "id2"])
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def upsert_discount_code_ticket_types(%DiscountCode{} = discount_code, ticket_type_ids) do
+ ids = ticket_type_ids || []
+
+ ticket_types =
+ Pearl.Tickets.TicketType
+ |> where([t], t.id in ^ids)
+ |> Repo.all()
+
+ discount_code
+ |> DiscountCode.changeset_update_ticket_types(ticket_types)
+ |> Repo.update()
+ end
+end
diff --git a/lib/pearl/discount_codes/discount_code.ex b/lib/pearl/discount_codes/discount_code.ex
new file mode 100644
index 0000000..03b0e17
--- /dev/null
+++ b/lib/pearl/discount_codes/discount_code.ex
@@ -0,0 +1,50 @@
+defmodule Pearl.DiscountCodes.DiscountCode do
+ @moduledoc """
+ Module for the Discount Code
+ """
+
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ alias Pearl.Tickets.TicketType
+
+ @derive {
+ Flop.Schema,
+ filterable: [:code, :active],
+ sortable: [:code, :amount, :active, :inserted_at],
+ default_limit: 25
+ }
+
+ @required_fields ~w(code amount active usage_limit)a
+ @optional_fields ~w()a
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+
+ schema "discount_codes" do
+ field :code, :string
+ field :amount, :float
+ field :active, :boolean, default: false
+ field :usage_limit, :integer
+
+ many_to_many :ticket_types, TicketType,
+ join_through: "discount_codes_ticket_types",
+ on_replace: :delete
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(discount_code, attrs) do
+ discount_code
+ |> cast(attrs, @required_fields ++ @optional_fields)
+ |> validate_required(@required_fields)
+ end
+
+ @doc false
+ def changeset_update_ticket_types(discount_code, ticket_types) do
+ discount_code
+ |> cast(%{}, @required_fields ++ @optional_fields)
+ |> put_assoc(:ticket_types, ticket_types)
+ end
+end
diff --git a/lib/pearl/perks.ex b/lib/pearl/perks.ex
new file mode 100644
index 0000000..39a50c9
--- /dev/null
+++ b/lib/pearl/perks.ex
@@ -0,0 +1,44 @@
+defmodule Pearl.Perks do
+ @moduledoc """
+ Context for Perks
+ """
+ use Pearl.Context
+ alias Pearl.Tickets.Perk
+
+ def list_perks do
+ Repo.all(Perk)
+ end
+
+ def get_perk!(id) do
+ Perk
+ |> Repo.get!(id)
+ end
+
+ def create_perk(attrs \\ %{}) do
+ %Perk{}
+ |> Perk.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ def change_perk(%Perk{} = perk, attrs \\ %{}) do
+ Perk.changeset(perk, attrs)
+ end
+
+ def update_perk(%Perk{} = perk, attrs) do
+ perk
+ |> Perk.changeset(attrs)
+ |> Repo.update()
+ end
+
+ def archive_perk(%Perk{} = perk) do
+ perk
+ |> Perk.changeset(%{active: false})
+ |> Repo.update()
+ end
+
+ def unarchive_perk(%Perk{} = perk) do
+ perk
+ |> Perk.changeset(%{active: true})
+ |> Repo.update()
+ end
+end
diff --git a/lib/pearl/ticket_types.ex b/lib/pearl/ticket_types.ex
new file mode 100644
index 0000000..779af8b
--- /dev/null
+++ b/lib/pearl/ticket_types.ex
@@ -0,0 +1,196 @@
+defmodule Pearl.TicketTypes do
+ @moduledoc """
+ The Ticket Types context
+ """
+ use Pearl.Context
+
+ import Ecto.Query, warn: false
+ alias Pearl.Repo
+
+ alias Pearl.Tickets.TicketType
+
+ @doc """
+ Returns the list of ticket types.
+
+ ## Examples
+
+ iex> list_ticket_types()
+ [%TicketType{}, ...]
+
+ """
+ def list_ticket_types do
+ TicketType
+ |> order_by(:priority)
+ |> Repo.all()
+ end
+
+ @doc """
+ Returns the list of active ticket_types.
+
+ ## Examples
+
+ iex> list_active_ticket_types()
+ [%TicketType{}, ...]
+
+ """
+
+ def list_active_ticket_types do
+ TicketType
+ |> where([t], t.active == true)
+ |> order_by(:priority)
+ |> Repo.all()
+ end
+
+ @doc """
+ Gets a single ticket type.
+
+ Raises `Ecto.NoResultsError` if the TicketType does not exist.
+
+ ## Examples
+
+ iex> get_ticket_type!(123)
+ %TicketType{}
+
+ iex> get_ticket_type!(321)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_ticket_type!(id) do
+ TicketType
+ |> Repo.get!(id)
+ |> Repo.preload(:perks)
+ end
+
+ @doc """
+ Creates a ticket type.
+
+ ## Examples
+
+ iex> create_ticket_type(%{field: value})
+ {:ok, %TicketType{}}
+
+ iex> create_ticket_type(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_ticket_type(attrs \\ %{}) do
+ %TicketType{}
+ |> TicketType.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a ticket type.
+
+ ## Examples
+
+ iex> update_ticket_type(ticket_type, %{field: new_value})
+ {:ok, %TicketType{}}
+
+ iex> update_ticket_type(ticket_type, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_ticket_type(%TicketType{} = ticket_type, attrs) do
+ ticket_type
+ |> TicketType.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a ticket type.
+
+ ## Examples
+
+ iex> delete_ticket_type(ticket_type)
+ {:ok, %TicketType{}}
+
+ iex> delete_ticket_type(ticket_type)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_ticket_type(%TicketType{} = ticket_type) do
+ Repo.delete(ticket_type)
+ end
+
+ @doc """
+ Archives a ticket type.
+
+ iex> archive_ticket_type(ticket_type)
+ {:ok, %TicketType{}}
+
+ iex> archive_ticket_type(ticket_type)
+ {:error, %Ecto.Changeset{}}
+ """
+ def archive_ticket_type(%TicketType{} = ticket_type) do
+ ticket_type
+ |> TicketType.changeset(%{active: false})
+ |> Repo.update()
+ end
+
+ @doc """
+ Unarchives a ticket type.
+
+ iex> unarchive_ticket_type(ticket_type)
+ {:ok, %TicketType{}}
+
+ iex> unarchive_ticket_type(ticket_type)
+ {:error, %Ecto.Changeset{}}
+ """
+ def unarchive_ticket_type(%TicketType{} = ticket_type) do
+ ticket_type
+ |> TicketType.changeset(%{active: true})
+ |> Repo.update()
+ end
+
+ @doc """
+ Returns the next priority a ticket type should have.
+
+ ## Examples
+
+ iex> get_next_ticket_type_priority()
+ 5
+ """
+ def get_next_ticket_type_priority do
+ (Repo.aggregate(from(t in TicketType), :max, :priority) || -1) + 1
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking ticket types changes.
+
+ ## Examples
+
+ iex> change_ticket_type(ticket_type)
+ %Ecto.Changeset{data: %TicketType{}}
+
+ """
+ def change_ticket_type(%TicketType{} = ticket_type, attrs \\ %{}) do
+ TicketType.changeset(ticket_type, attrs)
+ end
+
+ @doc """
+ Updates a ticket type's perks.
+
+ ## Examples
+
+ iex> upsert_ticket_type_perks(ticket_type, ["id1", "id2"])
+ {:ok, %TicketType{}}
+
+ iex> upsert_ticket_type_perks(ticket_type, ["id1", "id2"])
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def upsert_ticket_type_perks(%TicketType{} = ticket_type, perk_ids) do
+ ids = perk_ids || []
+
+ perks =
+ Pearl.Tickets.Perk
+ |> where([p], p.id in ^ids)
+ |> Repo.all()
+
+ ticket_type
+ |> Repo.preload(:perks)
+ |> TicketType.changeset_update_perks(perks)
+ |> Repo.update()
+ end
+end
diff --git a/lib/pearl/tickets.ex b/lib/pearl/tickets.ex
new file mode 100644
index 0000000..3863b31
--- /dev/null
+++ b/lib/pearl/tickets.ex
@@ -0,0 +1,227 @@
+defmodule Pearl.Tickets do
+ @moduledoc """
+ The Tickets context.
+ """
+ use Pearl.Context
+
+ import Ecto.Query, warn: false
+ alias Pearl.Repo
+
+ alias Pearl.Tickets.Ticket
+
+ @doc """
+ Returns the list of tickets.
+
+ ## Examples
+
+ iex> list_tickets()
+ [%Ticket{}, ...]
+
+ """
+ def list_tickets do
+ Ticket
+ |> Repo.all()
+ end
+
+ def list_tickets(opts) when is_list(opts) do
+ Ticket
+ |> apply_filters(opts)
+ |> Repo.all()
+ end
+
+ def list_tickets(params) do
+ Ticket
+ |> join(:left, [t], u in assoc(t, :user), as: :user)
+ |> join(:left, [t], tt in assoc(t, :ticket_type), as: :ticket_type)
+ |> preload([user: u, ticket_type: tt], user: u, ticket_type: tt)
+ |> Flop.validate_and_run(params, for: Ticket)
+ end
+
+ def list_tickets(%{} = params, opts) when is_list(opts) do
+ Ticket
+ |> apply_filters(opts)
+ |> Flop.validate_and_run(params, for: Ticket)
+ end
+
+ @doc """
+ Gets a single ticket.
+
+ Raises `Ecto.NoResultsError` if the Ticket does not exist.
+
+ ## Examples
+
+ iex> get_ticket!(123)
+ %Ticket{}
+
+ iex> get_ticket!(321)
+ ** (Ecto.NoResultsError)
+
+ """
+
+ def get_ticket!(id) do
+ Ticket
+ |> preload([:user, :ticket_type])
+ |> Repo.get!(id)
+ end
+
+ @doc """
+ Creates a ticket.
+
+ ## Examples
+
+ iex> create_ticket(%{field: value})
+ {:ok, %Ticket{}}
+
+ iex> create_ticket(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_ticket(attrs \\ %{}) do
+ %Ticket{}
+ |> Ticket.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a ticket.
+
+ ## Examples
+
+ iex> update_ticket(ticket, %{field: new_value})
+ {:ok, %Ticket{}}
+
+ iex> update_ticket(ticket, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_ticket(%Ticket{} = ticket, attrs) do
+ ticket
+ |> Ticket.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a ticket.
+
+ ## Examples
+
+ iex> delete_ticket(ticket)
+ {:ok, %Ticket{}}
+
+ iex> delete_ticket(ticket)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_ticket(%Ticket{} = ticket) do
+ Repo.delete(ticket)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking ticket changes.
+
+ ## Examples
+
+ iex> change_ticket(ticket)
+ %Ecto.Changeset{data: %Ticket{}}
+
+ """
+ def change_ticket(%Ticket{} = ticket, attrs \\ %{}) do
+ Ticket.changeset(ticket, attrs)
+ end
+
+ alias Pearl.Tickets.Perk
+
+ @doc """
+ Returns the list of perks.
+
+ ## Examples
+
+ iex> list_perks()
+ [%Perk{}, ...]
+
+ """
+ def list_perks do
+ Repo.all(Perk)
+ end
+
+ @doc """
+ Gets a single perk.
+
+ Raises `Ecto.NoResultsError` if the Perk does not exist.
+
+ ## Examples
+
+ iex> get_perk!(123)
+ %Perk{}
+
+ iex> get_perk!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_perk!(id), do: Repo.get!(Perk, id)
+
+ @doc """
+ Creates a perk.
+
+ ## Examples
+
+ iex> create_perk(%{field: value})
+ {:ok, %Perk{}}
+
+ iex> create_perk(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_perk(attrs) do
+ %Perk{}
+ |> Perk.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a perk.
+
+ ## Examples
+
+ iex> update_perk(perk, %{field: new_value})
+ {:ok, %Perk{}}
+
+ iex> update_perk(perk, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_perk(%Perk{} = perk, attrs) do
+ perk
+ |> Perk.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a perk.
+
+ ## Examples
+
+ iex> delete_perk(perk)
+ {:ok, %Perk{}}
+
+ iex> delete_perk(perk)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_perk(%Perk{} = perk) do
+ Repo.delete(perk)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking perk changes.
+
+ ## Examples
+
+ iex> change_perk(perk)
+ %Ecto.Changeset{data: %Perk{}}
+
+ """
+ def change_perk(%Perk{} = perk, attrs \\ %{}) do
+ Perk.changeset(perk, attrs)
+ end
+end
diff --git a/lib/pearl/tickets/perk.ex b/lib/pearl/tickets/perk.ex
new file mode 100644
index 0000000..dcac7cb
--- /dev/null
+++ b/lib/pearl/tickets/perk.ex
@@ -0,0 +1,31 @@
+defmodule Pearl.Tickets.Perk do
+ @moduledoc """
+ Perks for Ticket Types.
+ """
+
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ @required_fields ~w(name description icon color active)a
+
+ schema "perks" do
+ field :name, :string
+ field :description, :string
+ field :icon, :string
+ field :color, :string
+ field :active, :boolean
+
+ many_to_many :ticket_types, Pearl.Tickets.TicketType, join_through: "ticket_types_perks"
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(perk, attrs) do
+ perk
+ |> cast(attrs, @required_fields)
+ |> validate_required(@required_fields)
+ end
+end
diff --git a/lib/pearl/tickets/ticket.ex b/lib/pearl/tickets/ticket.ex
new file mode 100644
index 0000000..52ea106
--- /dev/null
+++ b/lib/pearl/tickets/ticket.ex
@@ -0,0 +1,53 @@
+defmodule Pearl.Tickets.Ticket do
+ @moduledoc """
+ Tickets to access the event.
+ """
+
+ use Pearl.Schema
+
+ alias Pearl.Accounts.User
+ alias Pearl.Repo
+ alias Pearl.Tickets.TicketType
+
+ @derive {
+ Flop.Schema,
+ filterable: [:paid, :user_name],
+ sortable: [:paid, :inserted_at, :ticket_type],
+ default_limit: 11,
+ join_fields: [
+ ticket_type: [
+ binding: :ticket_type,
+ field: :name,
+ path: [:ticket_type, :name],
+ ecto_type: :string
+ ],
+ user_name: [
+ binding: :user,
+ field: :name,
+ path: [:user, :name],
+ ecto_type: :string
+ ]
+ ]
+ }
+
+ @required_fields ~w(paid user_id ticket_type_id)a
+
+ schema "tickets" do
+ field :paid, :boolean
+
+ belongs_to :user, User
+ belongs_to :ticket_type, TicketType, on_replace: :delete
+
+ timestamps(type: :utc_datetime)
+ end
+
+ def changeset(ticket, attrs) do
+ ticket
+ |> cast(attrs, @required_fields)
+ |> validate_required(@required_fields)
+ |> unique_constraint(:user_id)
+ |> cast_assoc(:user, with: &User.profile_changeset/2)
+ |> unsafe_validate_unique(:user_id, Repo)
+ |> foreign_key_constraint(:ticket_type_id)
+ end
+end
diff --git a/lib/pearl/tickets/ticket_type.ex b/lib/pearl/tickets/ticket_type.ex
new file mode 100644
index 0000000..bd06c32
--- /dev/null
+++ b/lib/pearl/tickets/ticket_type.ex
@@ -0,0 +1,48 @@
+defmodule Pearl.Tickets.TicketType do
+ @moduledoc """
+ Ticket types for Tickets.
+ """
+ use Pearl.Schema
+
+ alias Pearl.DiscountCodes.DiscountCode
+ alias Pearl.Tickets.Perk
+ alias Pearl.Tickets.Ticket
+
+ @required_fields ~w(name priority price active)a
+ @optional_fields ~w()a
+
+ @derive {Flop.Schema, sortable: [:priority], filterable: []}
+
+ schema "ticket_types" do
+ field :name, :string
+ field :priority, :integer
+ field :price, :float
+ field :active, :boolean
+ field :product_key, :binary_id
+
+ has_many :tickets, Ticket
+
+ many_to_many :perks, Perk,
+ join_through: "ticket_types_perks",
+ on_replace: :delete
+
+ many_to_many :discount_codes, DiscountCode,
+ join_through: "discount_codes_ticket_types",
+ on_replace: :delete
+
+ timestamps(type: :utc_datetime)
+ end
+
+ def changeset(ticket_type, attrs) do
+ ticket_type
+ |> cast(attrs, @required_fields ++ @optional_fields)
+ |> validate_required(@required_fields)
+ |> foreign_key_constraint(:tickets)
+ end
+
+ def changeset_update_perks(ticket_type, perks) do
+ ticket_type
+ |> cast(%{}, @required_fields ++ @optional_fields)
+ |> put_assoc(:perks, perks)
+ end
+end
diff --git a/lib/pearl_web/components/core_components.ex b/lib/pearl_web/components/core_components.ex
index 16eaa71..e0a2a64 100644
--- a/lib/pearl_web/components/core_components.ex
+++ b/lib/pearl_web/components/core_components.ex
@@ -295,7 +295,7 @@ defmodule PearlWeb.CoreComponents do
name={@name}
value="true"
checked={@checked}
- class={"rounded border-zinc-300 text-accent focus:ring-0 #{@class}"}
+ class={"rounded border-zinc-300 text-primary focus:ring-0 #{@class}"}
{@rest}
/>
{@label}
diff --git a/lib/pearl_web/config.ex b/lib/pearl_web/config.ex
index e2b9699..a01c883 100644
--- a/lib/pearl_web/config.ex
+++ b/lib/pearl_web/config.ex
@@ -153,6 +153,20 @@ defmodule PearlWeb.Config do
url: "/dashboard/companies",
scope: %{"companies" => ["edit"]}
},
+ %{
+ key: :tickets,
+ title: "Tickets",
+ icon: "hero-ticket",
+ url: "/dashboard/tickets",
+ scope: %{"tickets" => ["edit"]}
+ },
+ %{
+ key: :discount_codes,
+ title: "Discount Codes",
+ icon: "hero-tag",
+ url: "/dashboard/discount_codes",
+ scope: %{"discount_codes" => ["edit"]}
+ },
%{
key: :store,
title: "Store",
diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex
new file mode 100644
index 0000000..429274a
--- /dev/null
+++ b/lib/pearl_web/live/backoffice/discount_codes_live/form_component.ex
@@ -0,0 +1,168 @@
+defmodule PearlWeb.Backoffice.DiscountCodesLive.FormComponent do
+ use PearlWeb, :live_component
+
+ alias Pearl.DiscountCodes
+ alias Pearl.TicketTypes
+
+ import PearlWeb.Components.Forms
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.header>
+ {@title}
+ <:subtitle>
+ {gettext("Manage discount codes for tickets.")}
+
+
+
+ <.simple_form
+ for={@form}
+ id="discount-code-form"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ autocomplete="off"
+ >
+ <.field
+ field={@form[:code]}
+ type="text"
+ label="Code"
+ required
+ />
+
+ <.field
+ field={@form[:amount]}
+ type="number"
+ label="Discount (%)"
+ required
+ />
+
+ <.field
+ field={@form[:usage_limit]}
+ type="number"
+ label="Usage Limit"
+ required
+ />
+
+ <.field
+ field={@form[:active]}
+ type="checkbox"
+ label="Active"
+ />
+
+
+
+ Ticket Types
+
+
+ <%= for ticket_type <- @ticket_types do %>
+
+
+ {ticket_type.name}
+
+ <% end %>
+
+
+
+
+ <:actions>
+ <.backoffice_button phx-disable-with="Saving...">Save Discount Code
+
+
+
+ """
+ end
+
+ @impl true
+ def mount(socket) do
+ {:ok, socket}
+ end
+
+ @impl true
+ def update(%{discount_code: discount_code} = assigns, socket) do
+ ticket_types = TicketTypes.list_ticket_types()
+
+ selected_ids =
+ case discount_code.ticket_types do
+ %Ecto.Association.NotLoaded{} -> []
+ ticket_types when is_list(ticket_types) -> Enum.map(ticket_types, & &1.id)
+ _ -> []
+ end
+
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign(:ticket_types, ticket_types)
+ |> assign(:selected_ticket_type_ids, selected_ids)
+ |> assign_new(:form, fn ->
+ to_form(DiscountCodes.change_discount_code(discount_code))
+ end)}
+ end
+
+ @impl true
+ def handle_event("validate", %{"discount_code" => discount_code_params}, socket) do
+ changeset =
+ DiscountCodes.change_discount_code(socket.assigns.discount_code, discount_code_params)
+
+ selected_ids =
+ case discount_code_params do
+ %{"ticket_type_ids" => ids} when is_list(ids) ->
+ ids |> Enum.reject(&(&1 == "" or is_nil(&1)))
+
+ _ ->
+ []
+ end
+
+ {:noreply,
+ socket
+ |> assign(form: to_form(changeset, action: :validate))
+ |> assign(selected_ticket_type_ids: selected_ids)}
+ end
+
+ def handle_event("save", %{"discount_code" => discount_code_params}, socket) do
+ save_discount_code(socket, socket.assigns.action, discount_code_params)
+ end
+
+ defp save_discount_code(socket, :edit, discount_code_params) do
+ ticket_type_ids =
+ Map.get(discount_code_params, "ticket_type_ids", []) |> Enum.reject(&(&1 == ""))
+
+ with {:ok, discount_code} <-
+ DiscountCodes.update_discount_code(socket.assigns.discount_code, discount_code_params),
+ {:ok, _discount_code} <-
+ DiscountCodes.upsert_discount_code_ticket_types(discount_code, ticket_type_ids) do
+ {:noreply,
+ socket
+ |> put_flash(:info, "Discount code updated successfully")
+ |> push_patch(to: socket.assigns.patch)}
+ else
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp save_discount_code(socket, :new, discount_code_params) do
+ ticket_type_ids =
+ Map.get(discount_code_params, "ticket_type_ids", []) |> Enum.reject(&(&1 == ""))
+
+ with {:ok, discount_code} <- DiscountCodes.create_discount_code(discount_code_params),
+ {:ok, _discount_code} <-
+ DiscountCodes.upsert_discount_code_ticket_types(discount_code, ticket_type_ids) do
+ {:noreply,
+ socket
+ |> put_flash(:info, "Discount code created successfully")
+ |> push_patch(to: socket.assigns.patch)}
+ else
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+end
diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/index.ex b/lib/pearl_web/live/backoffice/discount_codes_live/index.ex
new file mode 100644
index 0000000..cd77167
--- /dev/null
+++ b/lib/pearl_web/live/backoffice/discount_codes_live/index.ex
@@ -0,0 +1,54 @@
+defmodule PearlWeb.Backoffice.DiscountCodesLive.Index do
+ use PearlWeb, :backoffice_view
+
+ import PearlWeb.Components.Table
+
+ alias Pearl.DiscountCodes
+ alias Pearl.DiscountCodes.DiscountCode
+
+ on_mount {PearlWeb.StaffRoles, index: %{"discount_codes" => ["edit"]}}
+
+ def mount(_params, _session, socket) do
+ {:ok, socket}
+ end
+
+ def handle_params(params, _url, socket) do
+ case DiscountCodes.list_discount_codes(params) do
+ {:ok, {discount_codes, meta}} ->
+ {:noreply,
+ socket
+ |> assign(:current_page, :discount_codes)
+ |> assign(:meta, meta)
+ |> assign(:params, params)
+ |> stream(:discount_codes, discount_codes, reset: true)
+ |> apply_action(socket.assigns.live_action, params)}
+
+ {:error, _} ->
+ {:noreply, socket}
+ end
+ end
+
+ defp apply_action(socket, :index, _params) do
+ socket
+ |> assign(:page_title, "Listing Discount Codes")
+ end
+
+ defp apply_action(socket, :new, _params) do
+ socket
+ |> assign(:page_title, "New Discount Code")
+ |> assign(:discount_code, %DiscountCode{})
+ end
+
+ defp apply_action(socket, :edit, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Edit Discount Code")
+ |> assign(:discount_code, DiscountCodes.get_discount_code!(id))
+ end
+
+ def handle_event("delete", %{"id" => id}, socket) do
+ discount_code = DiscountCodes.get_discount_code!(id)
+ {:ok, _} = DiscountCodes.delete_discount_code(discount_code)
+
+ {:noreply, stream_delete(socket, :discount_codes, discount_code)}
+ end
+end
diff --git a/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex
new file mode 100644
index 0000000..18e94f9
--- /dev/null
+++ b/lib/pearl_web/live/backoffice/discount_codes_live/index.html.heex
@@ -0,0 +1,93 @@
+<.page title="Discount Codes">
+ <:actions>
+
+ <.ensure_permissions user={@current_user} permissions={%{"discount_codes" => ["edit"]}}>
+ <.link patch={~p"/dashboard/discount_codes/new"}>
+ <.backoffice_button>
+ New Discount
+
+
+
+
+
+
+
+ <.table
+ id="discount_codes-table"
+ items={@streams.discount_codes}
+ meta={@meta}
+ params={@params}
+ >
+ <:col :let={{_id, discount_code}} field={:code} label="Code">
+ {discount_code.code}
+
+ <:col :let={{_id, discount_code}} sortable field={:amount} label="Amount (%)">
+ {discount_code.amount}
+
+ <:col :let={{_id, discount_code}} sortable field={:active} label="Active">
+ <.input
+ type="checkbox"
+ name="active"
+ value="true"
+ checked={discount_code.active}
+ disabled
+ class="text-wine"
+ />
+
+ <:col :let={{_id, discount_code}} field={:usage_limit} label="Usage Limit">
+ {discount_code.usage_limit}
+
+ <:col :let={{_id, discount_code}} field={:ticket_types} label="Ticket Types">
+
+ <%= for ticket_type <- discount_code.ticket_types do %>
+
+ {ticket_type.name}
+
+ <% end %>
+ <%= if Enum.empty?(discount_code.ticket_types) do %>
+ No ticket types
+ <% end %>
+
+
+ <:action :let={{id, discount_code}}>
+ <.ensure_permissions user={@current_user} permissions={%{"discount_codes" => ["edit"]}}>
+
+ <.link patch={~p"/dashboard/discount_codes/#{discount_code.id}/edit"}>
+ <.icon name="hero-pencil" class="w-5 h-5" />
+
+ <.link
+ phx-click={JS.push("delete", value: %{id: discount_code.id}) |> hide("##{id}")}
+ data-confirm="Are you sure?"
+ >
+ <.icon name="hero-trash" class="w-5 h-5" />
+
+
+
+
+
+
+
+
+<.modal
+ :if={@live_action in [:new, :edit]}
+ id="discount_code-modal"
+ show
+ on_cancel={JS.patch(~p"/dashboard/discount_codes")}
+>
+ <.live_component
+ module={PearlWeb.Backoffice.DiscountCodesLive.FormComponent}
+ id={@discount_code.id || :new}
+ title={@page_title}
+ current_user={@current_user}
+ action={@live_action}
+ discount_code={@discount_code}
+ patch={~p"/dashboard/discount_codes"}
+ />
+
diff --git a/lib/pearl_web/live/backoffice/tickets_live/form_component.ex b/lib/pearl_web/live/backoffice/tickets_live/form_component.ex
new file mode 100644
index 0000000..95f3efd
--- /dev/null
+++ b/lib/pearl_web/live/backoffice/tickets_live/form_component.ex
@@ -0,0 +1,97 @@
+defmodule PearlWeb.Backoffice.TicketsLive.FormComponent do
+ use PearlWeb, :live_component
+
+ alias Pearl.Tickets
+ alias Pearl.TicketTypes
+
+ import PearlWeb.Components.Forms
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.header>
+ {@title}
+ <:subtitle>
+ {gettext("Tickets of the users.")}
+
+
+
+ <.simple_form
+ for={@form}
+ id="ticket-form"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ autocomplete="off"
+ >
+
+
+ <.field
+ field={@form[:ticket_type_id]}
+ type="select"
+ options={ticket_type_options(@ticket_types)}
+ label="Ticket Type"
+ wrapper_class="pr-2"
+ required
+ />
+ <.field
+ field={@form[:paid]}
+ type="checkbox"
+ label="Paid"
+ wrapper_class=""
+ />
+
+
+ <:actions>
+ <.backoffice_button phx-disable-with="Saving...">Save Ticket
+
+
+
+ """
+ end
+
+ @impl true
+ def mount(socket) do
+ {:ok, socket}
+ end
+
+ @impl true
+ def update(%{ticket: ticket} = assigns, socket) do
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign(:ticket, ticket)
+ |> assign(:ticket_types, TicketTypes.list_ticket_types())
+ |> assign_new(:form, fn ->
+ to_form(Tickets.change_ticket(ticket))
+ end)}
+ end
+
+ @impl true
+ def handle_event("validate", %{"ticket" => ticket_params}, socket) do
+ changeset = Tickets.change_ticket(socket.assigns.ticket, ticket_params)
+ {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
+ end
+
+ def handle_event("save", %{"ticket" => ticket_params}, socket) do
+ save_ticket(socket, ticket_params)
+ end
+
+ defp save_ticket(socket, ticket_params) do
+ case Tickets.update_ticket(socket.assigns.ticket, ticket_params) do
+ {:ok, _ticket} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "Ticket updated successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp ticket_type_options(ticket_types) do
+ Enum.map(ticket_types, &{&1.name, &1.id})
+ end
+end
diff --git a/lib/pearl_web/live/backoffice/tickets_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/index.ex
new file mode 100644
index 0000000..11a2056
--- /dev/null
+++ b/lib/pearl_web/live/backoffice/tickets_live/index.ex
@@ -0,0 +1,82 @@
+defmodule PearlWeb.Backoffice.TicketsLive.Index do
+ use PearlWeb, :backoffice_view
+
+ import PearlWeb.Components.{Table, TableSearch}
+
+ alias Pearl.{Perks, Tickets, TicketTypes}
+ alias Pearl.Tickets.{Perk, TicketType}
+
+ on_mount {PearlWeb.StaffRoles, index: %{"tickets" => ["edit"]}}
+
+ def mount(_params, _session, socket) do
+ {:ok, socket}
+ end
+
+ def handle_params(params, _url, socket) do
+ case Tickets.list_tickets(params) do
+ {:ok, {tickets, meta}} ->
+ {:noreply,
+ socket
+ |> assign(:current_page, :tickets)
+ |> assign(:meta, meta)
+ |> assign(:params, params)
+ |> stream(:tickets, tickets, reset: true)
+ |> apply_action(socket.assigns.live_action, params)}
+
+ {:error, _} ->
+ {:noreply, socket}
+ end
+ end
+
+ defp apply_action(socket, :index, _params) do
+ socket
+ |> assign(:page_title, "Listing Tickets")
+ end
+
+ defp apply_action(socket, :edit, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Edit Ticket")
+ |> assign(:ticket, Tickets.get_ticket!(id))
+ end
+
+ defp apply_action(socket, :ticket_types, _params) do
+ socket
+ |> assign(:page_title, "Listing Ticket Types")
+ end
+
+ defp apply_action(socket, :ticket_types_edit, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Edit Ticket Type")
+ |> assign(:ticket_type, TicketTypes.get_ticket_type!(id))
+ end
+
+ defp apply_action(socket, :ticket_types_new, _params) do
+ socket
+ |> assign(:page_title, "New Ticket Type")
+ |> assign(:ticket_type, %TicketType{})
+ end
+
+ defp apply_action(socket, :perks, _params) do
+ socket
+ |> assign(:page_title, "Listing Perks")
+ end
+
+ defp apply_action(socket, :perks_new, _params) do
+ socket
+ |> assign(:page_title, "New Perk")
+ |> assign(:perk, %Perk{})
+ end
+
+ defp apply_action(socket, :perks_edit, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Edit Perk")
+ |> assign(:perk, Perks.get_perk!(id))
+ end
+
+ def handle_event("delete", %{"id" => id}, socket) do
+ ticket = Tickets.get_ticket!(id)
+ {:ok, _} = Tickets.delete_ticket(ticket)
+
+ {:noreply, stream_delete(socket, :tickets, ticket)}
+ end
+end
diff --git a/lib/pearl_web/live/backoffice/tickets_live/index.html.heex b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex
new file mode 100644
index 0000000..75a3d1a
--- /dev/null
+++ b/lib/pearl_web/live/backoffice/tickets_live/index.html.heex
@@ -0,0 +1,144 @@
+<.page title="Tickets">
+ <:actions>
+
+ <.table_search
+ id="ticket-table-name-search"
+ params={@params}
+ field={:user_name}
+ path={~p"/dashboard/tickets"}
+ placeholder={gettext("Search for users")}
+ />
+ <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}>
+ <.link patch={~p"/dashboard/tickets/ticket_types"}>
+ <.backoffice_button>
+ <.icon name="hero-inbox-stack" class="w-5 h-5" />
+
+
+
+
+
+
+
+ <.table id="tickets-table" items={@streams.tickets} meta={@meta} params={@params}>
+ <:col :let={{_id, ticket}} label="User">
+ {ticket.user.name}
+
+ <:col :let={{_id, ticket}} sortable field={:paid} label="Paid">
+ <.input
+ type="checkbox"
+ name="active"
+ value="true"
+ checked={ticket.paid}
+ disabled
+ class="text-wine"
+ />
+
+ <:col :let={{_id, ticket}} sortable field={:ticket_type} label="Ticket Type">
+ {ticket.ticket_type.name}
+ <%= if not ticket.ticket_type.active do %>
+
+ Inactive
+
+ <% end %>
+
+ <:action :let={{id, ticket}}>
+ <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}>
+
+ <.link patch={~p"/dashboard/tickets/#{ticket.id}/edit"}>
+ <.icon name="hero-pencil" class="w-5 h-5" />
+
+ <.link
+ phx-click={JS.push("delete", value: %{id: ticket.id}) |> hide("##{id}")}
+ data-confirm="Are you sure?"
+ >
+ <.icon name="hero-trash" class="w-5 h-5" />
+
+
+
+
+
+
+
+
+<.modal
+ :if={@live_action in [:new, :edit]}
+ id="ticket-modal"
+ show
+ on_cancel={JS.patch(~p"/dashboard/tickets")}
+>
+ <.live_component
+ module={PearlWeb.Backoffice.TicketsLive.FormComponent}
+ id={@ticket.id || :new}
+ title={@page_title}
+ current_user={@current_user}
+ action={@live_action}
+ ticket={@ticket}
+ patch={~p"/dashboard/tickets"}
+ />
+
+
+<.modal
+ :if={@live_action in [:ticket_types]}
+ id="ticket-types-modal"
+ show
+ on_cancel={JS.patch(~p"/dashboard/tickets")}
+>
+ <.live_component
+ module={PearlWeb.Backoffice.TicketsLive.TicketTypesLive.Index}
+ id="list-ticket-types"
+ title={@page_title}
+ current_user={@current_user}
+ action={@live_action}
+ patch={~p"/dashboard/tickets/ticket_types"}
+ />
+
+
+<.modal
+ :if={@live_action in [:ticket_types_edit, :ticket_types_new]}
+ id="ticket-types-form-modal"
+ show
+ on_cancel={JS.navigate(~p"/dashboard/tickets/ticket_types")}
+>
+ <.live_component
+ module={PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent}
+ id={@ticket_type.id || :new}
+ title={@page_title}
+ current_user={@current_user}
+ action={@live_action}
+ ticket_type={@ticket_type}
+ patch={~p"/dashboard/tickets/ticket_types"}
+ />
+
+
+<.modal
+ :if={@live_action in [:perks]}
+ id="perks-modal"
+ show
+ on_cancel={JS.patch(~p"/dashboard/tickets/ticket_types/")}
+>
+ <.live_component
+ module={PearlWeb.Backoffice.TicketsLive.TicketTypesLive.PerksLive.Index}
+ id="list-perks"
+ title={@page_title}
+ current_user={@current_user}
+ action={@live_action}
+ patch={~p"/dashboard/tickets/ticket_types/perks"}
+ />
+
+
+<.modal
+ :if={@live_action in [:perks_edit, :perks_new]}
+ id="perks-modal"
+ show
+ on_cancel={JS.navigate(~p"/dashboard/tickets/ticket_types/perks")}
+>
+ <.live_component
+ module={PearlWeb.Backoffice.TicketsLive.TicketTypesLive.PerksLive.FormComponent}
+ id={@perk.id || :new}
+ title={@page_title}
+ current_user={@current_user}
+ action={@live_action}
+ perk={@perk}
+ patch={~p"/dashboard/tickets/ticket_types/perks/"}
+ />
+
diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex
new file mode 100644
index 0000000..6ac62ac
--- /dev/null
+++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/form_component.ex
@@ -0,0 +1,143 @@
+defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.FormComponent do
+ use PearlWeb, :live_component
+
+ alias Pearl.Perks
+ alias Pearl.TicketTypes
+
+ import PearlWeb.Components.Forms
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.header>
+ {@title}
+ <:subtitle>
+ {gettext("Ticket types for the event.")}
+
+
+
+ <.simple_form
+ for={@form}
+ id="ticket-type-form"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ >
+ <.field field={@form[:name]} type="text" label="Name" required />
+ <.field field={@form[:description]} type="textarea" label="Description" />
+ <.field field={@form[:price]} type="number" label="Price" required />
+ <.field field={@form[:product_key]} type="text" label="Product Key" required />
+
+
+ Perks
+
+
+ <%= for perk <- @perks do %>
+
+
+ {perk.name}
+
+ <% end %>
+
+
+
+ <:actions>
+ <.backoffice_button phx-disable-with="Saving...">Save Ticket Type
+
+
+
+ """
+ end
+
+ @impl true
+ def mount(socket) do
+ {:ok, socket}
+ end
+
+ @impl true
+ def update(%{ticket_type: ticket_type} = assigns, socket) do
+ perks = Perks.list_perks()
+
+ selected_ids =
+ case ticket_type.perks do
+ %Ecto.Association.NotLoaded{} -> []
+ perks when is_list(perks) -> Enum.map(perks, & &1.id)
+ _ -> []
+ end
+
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign(:perks, perks)
+ |> assign(:selected_perks_ids, selected_ids)
+ |> assign_new(:form, fn ->
+ to_form(TicketTypes.change_ticket_type(ticket_type))
+ end)}
+ end
+
+ @impl true
+ def handle_event("validate", %{"ticket_type" => ticket_type_params}, socket) do
+ changeset = TicketTypes.change_ticket_type(socket.assigns.ticket_type, ticket_type_params)
+
+ selected_ids =
+ case ticket_type_params do
+ %{"perk_ids" => ids} when is_list(ids) ->
+ ids |> Enum.reject(&(&1 == "" or is_nil(&1)))
+
+ _ ->
+ []
+ end
+
+ {:noreply,
+ socket
+ |> assign(form: to_form(changeset, action: :validate))
+ |> assign(selected_perks_ids: selected_ids)}
+ end
+
+ def handle_event("save", %{"ticket_type" => ticket_type_params}, socket) do
+ save_ticket_type(socket, socket.assigns.action, ticket_type_params)
+ end
+
+ defp save_ticket_type(socket, :ticket_types_edit, ticket_type_params) do
+ perk_ids = Map.get(ticket_type_params, "perk_ids", []) |> Enum.reject(&(&1 == ""))
+
+ with {:ok, ticket_type} <-
+ TicketTypes.update_ticket_type(socket.assigns.ticket_type, ticket_type_params),
+ {:ok, _ticket_type} <- TicketTypes.upsert_ticket_type_perks(ticket_type, perk_ids) do
+ {:noreply,
+ socket
+ |> put_flash(:info, "Ticket type updated successfully")
+ |> push_patch(to: socket.assigns.patch)}
+ else
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp save_ticket_type(socket, :ticket_types_new, ticket_type_params) do
+ perk_ids = Map.get(ticket_type_params, "perk_ids", []) |> Enum.reject(&(&1 == ""))
+
+ with {:ok, ticket_type} <-
+ TicketTypes.create_ticket_type(
+ ticket_type_params
+ |> Map.put("priority", TicketTypes.get_next_ticket_type_priority())
+ |> Map.put("active", true)
+ ),
+ {:ok, _ticket_type} <- TicketTypes.upsert_ticket_type_perks(ticket_type, perk_ids) do
+ {:noreply,
+ socket
+ |> put_flash(:info, "Ticket type created successfully")
+ |> push_patch(to: socket.assigns.patch)}
+ else
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+end
diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex
new file mode 100644
index 0000000..a662fa3
--- /dev/null
+++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/index.ex
@@ -0,0 +1,98 @@
+defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.Index do
+ use PearlWeb, :live_component
+
+ alias Pearl.TicketTypes
+ import PearlWeb.Components.EnsurePermissions
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.page title={@title}>
+ <:actions>
+ <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}>
+ <.link navigate={~p"/dashboard/tickets/ticket_types/new"}>
+ <.backoffice_button>New Ticket Type
+
+ <.link navigate={~p"/dashboard/tickets/ticket_types/perks"}>
+ <.backoffice_button>Perks
+
+
+
+
+ ticket_type.id}
+ class="even:bg-lightShade/20 dark:even:bg-darkShade/20 py-4 px-4 flex flex-row justify-between"
+ >
+
+ <.icon name="hero-bars-3" class="w-5 h-5 handle cursor-pointer ml-4" />
+ {ticket_type.name}
+ <%= if not ticket_type.active do %>
+
+ Inactive
+
+ <% end %>
+
+
+ <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}>
+ <.link navigate={~p"/dashboard/tickets/ticket_types/#{ticket_type.id}/edit"}>
+ <.icon name="hero-pencil" class="w-5 h-4" />
+
+ <.link
+ phx-click={JS.push("toggle_archive", value: %{id: ticket_type.id})}
+ data-confirm="Are you sure?"
+ phx-target={@myself}
+ >
+ <%= if not ticket_type.active do %>
+ <.icon name="hero-archive-box-arrow-down" class="w-5 h-5" />
+ <% else %>
+ <.icon name="hero-archive-box" class="w-5 h-5" />
+ <% end %>
+
+
+
+
+
+
+
+ """
+ end
+
+ @impl true
+ def mount(socket) do
+ {:ok,
+ socket
+ |> stream(:ticket_types, TicketTypes.list_ticket_types())}
+ end
+
+ def handle_event("update-sorting", %{"ids" => ids}, socket) do
+ ids
+ |> Enum.with_index(0)
+ |> Enum.each(fn {"ticket_type-" <> id, index} ->
+ id
+ |> TicketTypes.get_ticket_type!()
+ |> TicketTypes.update_ticket_type(%{priority: index})
+ end)
+
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event("toggle_archive", %{"id" => id}, socket) do
+ ticket_type = TicketTypes.get_ticket_type!(id)
+
+ if ticket_type.active do
+ {:ok, _} = TicketTypes.archive_ticket_type(ticket_type)
+ else
+ {:ok, _} = TicketTypes.unarchive_ticket_type(ticket_type)
+ end
+
+ {:noreply, socket |> stream(:ticket_types, TicketTypes.list_ticket_types())}
+ end
+end
diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex
new file mode 100644
index 0000000..65842c1
--- /dev/null
+++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/form_component.ex
@@ -0,0 +1,86 @@
+defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.PerksLive.FormComponent do
+ use PearlWeb, :live_component
+
+ alias Pearl.Perks
+
+ import PearlWeb.Components.Forms
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.header>
+ {@title}
+ <:subtitle>
+ {gettext("Perks for the ticket types.")}
+
+
+
+ <.simple_form
+ for={@form}
+ id="perks-form"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ >
+ <.field field={@form[:name]} type="text" label="Name" required />
+ <.field field={@form[:description]} type="textarea" label="Description" />
+ <.field field={@form[:icon]} type="text" label="Icon" />
+ <.field field={@form[:color]} type="text" label="Color" />
+ <:actions>
+ <.backoffice_button phx-disable-with="Saving...">Save Perk
+
+
+
+ """
+ end
+
+ @impl true
+ def update(%{perk: perk} = assigns, socket) do
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign_new(:form, fn ->
+ to_form(Perks.change_perk(perk))
+ end)}
+ end
+
+ @impl true
+ def handle_event("validate", %{"perk" => perk_params}, socket) do
+ changeset = Perks.change_perk(socket.assigns.perk, perk_params)
+ {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
+ end
+
+ def handle_event("save", %{"perk" => perk_params}, socket) do
+ save_perk(socket, socket.assigns.action, perk_params)
+ end
+
+ defp save_perk(socket, :perks_edit, perk_params) do
+ case Perks.update_perk(socket.assigns.perk, perk_params) do
+ {:ok, _perk} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "Perk updated successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp save_perk(socket, :perks_new, perk_params) do
+ case Perks.create_perk(
+ perk_params
+ |> Map.put("active", true)
+ ) do
+ {:ok, _perk} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "Perk created successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+end
diff --git a/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/index.ex b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/index.ex
new file mode 100644
index 0000000..1e3697b
--- /dev/null
+++ b/lib/pearl_web/live/backoffice/tickets_live/ticket_types_live/perks_live/index.ex
@@ -0,0 +1,81 @@
+defmodule PearlWeb.Backoffice.TicketsLive.TicketTypesLive.PerksLive.Index do
+ use PearlWeb, :live_component
+
+ alias Pearl.Perks
+ import PearlWeb.Components.EnsurePermissions
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.page title={@title}>
+ <:actions>
+ <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}>
+ <.link navigate={~p"/dashboard/tickets/ticket_types/perks/new"}>
+ <.backoffice_button>New Perk
+
+
+
+
+ perk.id}
+ class="even:bg-lightShade/20 dark:even:bg-darkShade/20 py-4 px-4 flex flex-row justify-between"
+ >
+
+ {perk.name}
+ <%= if not perk.active do %>
+
+ Inactive
+
+ <% end %>
+
+
+ <.ensure_permissions user={@current_user} permissions={%{"tickets" => ["edit"]}}>
+ <.link navigate={~p"/dashboard/tickets/ticket_types/perks/#{perk.id}/edit"}>
+ <.icon name="hero-pencil" class="w-5 h-4" />
+
+ <.link
+ phx-click={JS.push("toggle_archive", value: %{id: perk.id})}
+ data-confirm="Are you sure?"
+ phx-target={@myself}
+ >
+ <%= if not perk.active do %>
+ <.icon name="hero-archive-box-arrow-down" class="w-5 h-5" />
+ <% else %>
+ <.icon name="hero-archive-box" class="w-5 h-5" />
+ <% end %>
+
+
+
+
+
+
+
+ """
+ end
+
+ @impl true
+ def mount(socket) do
+ {:ok,
+ socket
+ |> stream(:perks, Perks.list_perks())}
+ end
+
+ @impl true
+ def handle_event("toggle_archive", %{"id" => id}, socket) do
+ perk = Perks.get_perk!(id)
+
+ if perk.active do
+ {:ok, _} = Perks.archive_perk(perk)
+ else
+ {:ok, _} = Perks.unarchive_perk(perk)
+ end
+
+ {:noreply, socket |> stream(:perks, Perks.list_perks())}
+ end
+end
diff --git a/lib/pearl_web/router.ex b/lib/pearl_web/router.ex
index 7c82390..af60bb3 100644
--- a/lib/pearl_web/router.ex
+++ b/lib/pearl_web/router.ex
@@ -228,6 +228,29 @@ defmodule PearlWeb.Router do
end
end
+ scope "/tickets", TicketsLive do
+ live "/", Index, :index
+ live "/:id/edit", Index, :edit
+
+ scope "/ticket_types" do
+ live "/", Index, :ticket_types
+ live "/new", Index, :ticket_types_new
+ live "/:id/edit", Index, :ticket_types_edit
+
+ scope "/perks" do
+ live "/", Index, :perks
+ live "/new", Index, :perks_new
+ live "/:id/edit", Index, :perks_edit
+ end
+ end
+ end
+
+ scope "/discount_codes", DiscountCodesLive do
+ live "/", Index, :index
+ live "/new", Index, :new
+ live "/:id/edit", Index, :edit
+ end
+
scope "/schedule", ScheduleLive do
live "/edit", Index, :edit_schedule
diff --git a/priv/repo/migrations/20251120141024_add_ticket_types.exs b/priv/repo/migrations/20251120141024_add_ticket_types.exs
new file mode 100644
index 0000000..9587b1c
--- /dev/null
+++ b/priv/repo/migrations/20251120141024_add_ticket_types.exs
@@ -0,0 +1,16 @@
+defmodule Pearl.Repo.Migrations.AddTicketTypes do
+ use Ecto.Migration
+
+ def change do
+ create table(:ticket_types, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+ add :priority, :integer
+ add :name, :string
+ add :price, :float
+ add :active, :boolean
+ add :product_key, :binary_id
+
+ timestamps(type: :utc_datetime)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20251120141058_add_tickets.exs b/priv/repo/migrations/20251120141058_add_tickets.exs
new file mode 100644
index 0000000..1ad61e1
--- /dev/null
+++ b/priv/repo/migrations/20251120141058_add_tickets.exs
@@ -0,0 +1,19 @@
+defmodule Pearl.Repo.Migrations.AddTickets do
+ use Ecto.Migration
+
+ def change do
+ create table(:tickets, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+ add :paid, :boolean, null: false
+ add :user_id, references(:users, type: :binary_id, on_delete: :nothing), null: false
+
+ add :ticket_type_id, references(:ticket_types, type: :binary_id, on_delete: :nothing),
+ null: false
+
+ timestamps(type: :utc_datetime)
+ end
+
+ create unique_index(:tickets, [:user_id])
+ create index(:tickets, [:ticket_type_id])
+ end
+end
diff --git a/priv/repo/migrations/20251128122529_create_discount_codes.exs b/priv/repo/migrations/20251128122529_create_discount_codes.exs
new file mode 100644
index 0000000..3f0962e
--- /dev/null
+++ b/priv/repo/migrations/20251128122529_create_discount_codes.exs
@@ -0,0 +1,15 @@
+defmodule Pearl.Repo.Migrations.CreateDiscountCodes do
+ use Ecto.Migration
+
+ def change do
+ create table(:discount_codes, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+ add :code, :string
+ add :amount, :float
+ add :active, :boolean, default: false, null: false
+ add :usage_limit, :integer
+
+ timestamps(type: :utc_datetime)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs b/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs
new file mode 100644
index 0000000..5a4177e
--- /dev/null
+++ b/priv/repo/migrations/20251128124926_create_discount_codes_ticket_types.exs
@@ -0,0 +1,18 @@
+defmodule Pearl.Repo.Migrations.CreateDiscountCodesTicketTypes do
+ use Ecto.Migration
+
+ def change do
+ create table(:discount_codes_ticket_types, primary_key: false) do
+ add :discount_code_id,
+ references(:discount_codes, type: :binary_id, on_delete: :delete_all), null: false
+
+ add :ticket_type_id,
+ references(:ticket_types, type: :binary_id, on_delete: :delete_all),
+ null: false
+ end
+
+ create index(:discount_codes_ticket_types, [:discount_code_id])
+ create index(:discount_codes_ticket_types, [:ticket_type_id])
+ create unique_index(:discount_codes_ticket_types, [:discount_code_id, :ticket_type_id])
+ end
+end
diff --git a/priv/repo/migrations/20251202172259_create_perks.exs b/priv/repo/migrations/20251202172259_create_perks.exs
new file mode 100644
index 0000000..ea99c53
--- /dev/null
+++ b/priv/repo/migrations/20251202172259_create_perks.exs
@@ -0,0 +1,16 @@
+defmodule Pearl.Repo.Migrations.CreatePerks do
+ use Ecto.Migration
+
+ def change do
+ create table(:perks, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+ add :name, :string
+ add :description, :string
+ add :icon, :string
+ add :color, :string
+ add :active, :boolean
+
+ timestamps(type: :utc_datetime)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20251202172629_create_ticket_types_perks.exs b/priv/repo/migrations/20251202172629_create_ticket_types_perks.exs
new file mode 100644
index 0000000..7ee2dfa
--- /dev/null
+++ b/priv/repo/migrations/20251202172629_create_ticket_types_perks.exs
@@ -0,0 +1,18 @@
+defmodule Pearl.Repo.Migrations.CreateTicketTypesPerks do
+ use Ecto.Migration
+
+ def change do
+ create table(:ticket_types_perks, primary_key: false) do
+ add :perk_id,
+ references(:perks, type: :binary_id, on_delete: :delete_all)
+
+ add :ticket_type_id,
+ references(:ticket_types, type: :binary_id, on_delete: :delete_all),
+ null: false
+ end
+
+ create index(:ticket_types_perks, [:perk_id])
+ create index(:ticket_types_perks, [:ticket_type_id])
+ create unique_index(:ticket_types_perks, [:perk_id, :ticket_type_id])
+ end
+end
diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs
index cf725c6..6309f3d 100644
--- a/priv/repo/seeds.exs
+++ b/priv/repo/seeds.exs
@@ -21,7 +21,9 @@ defmodule Pearl.Repo.Seeds do
"companies.exs",
"activities.exs",
"slots.exs",
- "teams.exs"
+ "teams.exs",
+ "tickets.exs",
+ "discount_codes.exs"
]
|> Enum.each(fn file ->
Code.require_file("#{@seeds_dir}/#{file}")
diff --git a/priv/repo/seeds/discount_codes.exs b/priv/repo/seeds/discount_codes.exs
new file mode 100644
index 0000000..248b1c7
--- /dev/null
+++ b/priv/repo/seeds/discount_codes.exs
@@ -0,0 +1,77 @@
+alias Pearl.Repo
+alias Pearl.DiscountCodes.DiscountCode
+alias Pearl.Tickets.TicketType
+
+ticket_types = Repo.all(TicketType)
+
+if Enum.empty?(ticket_types) do
+ IO.puts("No ticket types found. Please run ticket_types seeds first.")
+else
+ normal = Enum.find(ticket_types, &(&1.name == "Normal"))
+ fullpass = Enum.find(ticket_types, &(&1.name == "FullPass"))
+ fullpass_hotel = Enum.find(ticket_types, &(&1.name == "FullPass+Hotel"))
+ student = Enum.find(ticket_types, &(&1.name == "Student"))
+ early_bird = Enum.find(ticket_types, &(&1.name == "Early Bird"))
+
+ discount_codes = [
+ %{
+ code: "EARLYBIRD2025",
+ amount: 10,
+ active: true,
+ ticket_type_ids: [normal, early_bird] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id),
+ usage_limit: 50
+ },
+ %{
+ code: "STUDENT50",
+ amount: 20,
+ active: true,
+ ticket_type_ids: [student] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id),
+ usage_limit: 20
+ },
+ %{
+ code: "FULLPASS20",
+ amount: 100,
+ active: true,
+ ticket_type_ids: [fullpass, fullpass_hotel] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id),
+ usage_limit: 12
+ },
+ %{
+ code: "ALLACCESS",
+ amount: 100,
+ active: true,
+ ticket_type_ids: ticket_types |> Enum.map(& &1.id),
+ usage_limit: 3
+ },
+ %{
+ code: "SPONSOR25",
+ amount: 100,
+ active: true,
+ ticket_type_ids: [normal, fullpass] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id),
+ usage_limit: 1
+ },
+ %{
+ code: "EXPIRED2024",
+ amount: 33,
+ active: false,
+ ticket_type_ids: [normal] |> Enum.reject(&is_nil/1) |> Enum.map(& &1.id),
+ usage_limit: 10
+ }
+ ]
+
+ Enum.each(discount_codes, fn attrs ->
+ case Repo.get_by(DiscountCode, code: attrs.code) do
+ nil ->
+ case Pearl.DiscountCodes.create_discount_code(attrs) do
+ {:ok, _} ->
+ IO.puts("Created discount code: #{attrs.code}")
+ {:error, changeset} ->
+ IO.puts("Failed to create discount code #{attrs.code}: #{inspect(changeset.errors)}")
+ end
+
+ existing ->
+ IO.puts("Discount code already exists: #{existing.code}")
+ end
+ end)
+
+ IO.puts("Discount codes seeded successfully!")
+end
diff --git a/priv/repo/seeds/tickets.exs b/priv/repo/seeds/tickets.exs
new file mode 100644
index 0000000..c3fb138
--- /dev/null
+++ b/priv/repo/seeds/tickets.exs
@@ -0,0 +1,128 @@
+defmodule Pearl.Repo.Seeds.Tickets do
+ import Ecto.Query
+
+ alias Pearl.Accounts.User
+ alias Pearl.{Perks, Repo, Tickets, TicketTypes}
+ alias Pearl.Tickets.{Perk, Ticket, TicketType}
+
+ @perks [
+ %{name: "Entry", description: "Entrada nos 4 dias do evento", icon: "hero-ticket", color: "#F9808D", active: true},
+ %{name: "Meals", description: "Refeições durante todo o evento", icon: "hero-beaker", color: "#505936", active: true},
+ %{name: "Accommodation", description: "Estadia no Pavilhão", icon: "hero-star", color: "#9AB7C1", active: true},
+ %{name: "Premium Accommodation", description: "Estadia na Pousada da Juventude", icon: "hero-gift", color: "#D89ED0", active: true}
+ ]
+
+ @ticket_types [
+ %{name: "Bilhete 1", description: "A nice ticket", price: 32, active: true, product_key: "XxXxX", priority: 0, perks: ["Entry"]},
+ %{name: "Bilhete 2", description: "A much nicer ticket", price: 33, active: true, product_key: "XxXxX", priority: 1, perks: ["Entry", "Meals"]},
+ %{name: "Bilhete 3", description: "An awesome ticket", price: 38, active: true, product_key: "XxXxX", priority: 2, perks: ["Entry", "Meals", "Accommodation"]},
+ %{name: "Bilhete 4", description: "Absolutely magnificent ticket", price: 45, product_key: "XxXxX", active: true, priority: 3, perks: ["Entry", "Meals", "Premium Accommodation"]}
+ ]
+
+ def run do
+ seed_perks()
+ seed_ticket_types()
+ seed_tickets()
+ end
+
+ defp seed_perks do
+ case Repo.all(Perk) do
+ [] ->
+ Enum.each(@perks, &insert_perk/1)
+ Mix.shell().info("Seeded perks successfully.")
+
+ _ ->
+ Mix.shell().info("Found perks, skipping seeding.")
+ end
+ end
+
+ defp insert_perk(attrs) do
+ case Perks.create_perk(attrs) do
+ {:ok, _perk} ->
+ nil
+
+ {:error, _changeset} ->
+ Mix.shell().error("Failed to insert perk: #{attrs.name}")
+ end
+ end
+
+ defp seed_ticket_types do
+ case Repo.all(TicketType) do
+ [] ->
+ Enum.each(@ticket_types, &insert_ticket_type/1)
+ Mix.shell().info("Seeded ticket types successfully.")
+
+ _ ->
+ Mix.shell().info("Found ticket types, skipping seeding.")
+ end
+ end
+
+ defp insert_ticket_type(attrs) do
+ {perk_names, ticket_type_attrs} = Map.pop(attrs, :perks, [])
+
+ case TicketTypes.create_ticket_type(ticket_type_attrs) do
+ {:ok, ticket_type} ->
+ perk_ids =
+ Repo.all(from p in Perk, where: p.name in ^perk_names, select: p.id)
+
+ case TicketTypes.upsert_ticket_type_perks(ticket_type, perk_ids) do
+ {:ok, _ticket_type} ->
+ nil
+
+ {:error, _changeset} ->
+ Mix.shell().error("Failed to associate perks for ticket type: #{attrs.name}")
+ end
+
+ {:error, _changeset} ->
+ Mix.shell().error("Failed to insert ticket type: #{attrs.name}")
+ end
+ end
+
+ defp seed_tickets do
+ case Repo.all(Ticket) do
+ [] ->
+ users = Repo.all(from u in User, where: u.type == :attendee, limit: 20)
+
+ if Enum.empty?(users) do
+ Mix.shell().error("No attendee users found. Please create users first.")
+ else
+ ticket_types = Repo.all(TicketType)
+
+ empty_ticket_types?(ticket_types, users)
+ end
+
+ _ ->
+ Mix.shell().info("Found tickets, skipping seeding.")
+ end
+ end
+
+ defp empty_ticket_types?(ticket_types, users) do
+ if Enum.empty?(ticket_types) do
+ Mix.shell().error("No ticket types found. Please run ticket types seed first.")
+ else
+ users
+ |> Enum.with_index()
+ |> Enum.each(fn {user, index} ->
+ ticket_type = Enum.at(ticket_types, rem(index, length(ticket_types)))
+
+ insert_ticket(%{
+ user_id: user.id,
+ ticket_type_id: ticket_type.id,
+ paid: rem(index, 3) != 0
+ })
+ end)
+ end
+ end
+
+ defp insert_ticket(attrs) do
+ case Tickets.create_ticket(attrs) do
+ {:ok, _ticket} ->
+ nil
+
+ {:error, changeset} ->
+ Mix.shell().error("Failed to insert ticket for user #{attrs.user_id}: #{inspect(changeset.errors)}")
+ end
+ end
+end
+
+Pearl.Repo.Seeds.Tickets.run()
diff --git a/priv/static/images/starts.svg b/priv/static/images/starts.svg
index a29bf59..d890a90 100644
--- a/priv/static/images/starts.svg
+++ b/priv/static/images/starts.svg
@@ -1,8 +1,8 @@
-
-
+
+
diff --git a/test/pearl/discount_codes_test.exs b/test/pearl/discount_codes_test.exs
new file mode 100644
index 0000000..5784df2
--- /dev/null
+++ b/test/pearl/discount_codes_test.exs
@@ -0,0 +1,73 @@
+defmodule Pearl.DiscountCodesTest do
+ use Pearl.DataCase
+
+ alias Pearl.DiscountCodes
+
+ describe "discount_codes" do
+ alias Pearl.DiscountCodes.DiscountCode
+
+ import Pearl.DiscountCodesFixtures
+
+ @invalid_attrs %{active: nil, code: nil, amount: nil, usage_limit: nil}
+
+ test "list_discount_codes/0 returns all discount_codes" do
+ _discount_code = discount_code_fixture()
+ assert {:ok, {[_discount_code], _meta}} = DiscountCodes.list_discount_codes()
+ end
+
+ test "get_discount_code!/1 returns the discount_code with given id" do
+ discount_code = discount_code_fixture()
+ assert DiscountCodes.get_discount_code!(discount_code.id) == discount_code
+ end
+
+ test "create_discount_code/1 with valid data creates a discount_code" do
+ valid_attrs = %{active: true, code: "some code", amount: 42, usage_limit: 1}
+
+ assert {:ok, %DiscountCode{} = discount_code} =
+ DiscountCodes.create_discount_code(valid_attrs)
+
+ assert discount_code.active == true
+ assert discount_code.code == "some code"
+ assert discount_code.amount == 42
+ end
+
+ test "create_discount_code/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = DiscountCodes.create_discount_code(@invalid_attrs)
+ end
+
+ test "update_discount_code/2 with valid data updates the discount_code" do
+ discount_code = discount_code_fixture()
+ update_attrs = %{active: false, code: "some updated code", amount: 43, usage_limit: 2}
+
+ assert {:ok, %DiscountCode{} = discount_code} =
+ DiscountCodes.update_discount_code(discount_code, update_attrs)
+
+ assert discount_code.active == false
+ assert discount_code.code == "some updated code"
+ assert discount_code.amount == 43
+ end
+
+ test "update_discount_code/2 with invalid data returns error changeset" do
+ discount_code = discount_code_fixture()
+
+ assert {:error, %Ecto.Changeset{}} =
+ DiscountCodes.update_discount_code(discount_code, @invalid_attrs)
+
+ assert discount_code == DiscountCodes.get_discount_code!(discount_code.id)
+ end
+
+ test "delete_discount_code/1 deletes the discount_code" do
+ discount_code = discount_code_fixture()
+ assert {:ok, %DiscountCode{}} = DiscountCodes.delete_discount_code(discount_code)
+
+ assert_raise Ecto.NoResultsError, fn ->
+ DiscountCodes.get_discount_code!(discount_code.id)
+ end
+ end
+
+ test "change_discount_code/1 returns a discount_code changeset" do
+ discount_code = discount_code_fixture()
+ assert %Ecto.Changeset{} = DiscountCodes.change_discount_code(discount_code)
+ end
+ end
+end
diff --git a/test/pearl/tickets_test.exs b/test/pearl/tickets_test.exs
new file mode 100644
index 0000000..49b130c
--- /dev/null
+++ b/test/pearl/tickets_test.exs
@@ -0,0 +1,80 @@
+defmodule Pearl.TicketsTest do
+ use Pearl.DataCase
+
+ alias Pearl.Tickets
+
+ describe "perks" do
+ alias Pearl.Tickets.Perk
+
+ import Pearl.TicketsFixtures
+
+ @invalid_attrs %{name: nil, description: nil, color: nil, icon: nil, active: nil}
+
+ test "list_perks/0 returns all perks" do
+ perk = perk_fixture()
+ assert Tickets.list_perks() == [perk]
+ end
+
+ test "get_perk!/1 returns the perk with given id" do
+ perk = perk_fixture()
+ assert Tickets.get_perk!(perk.id) == perk
+ end
+
+ test "create_perk/1 with valid data creates a perk" do
+ valid_attrs = %{
+ name: "some name",
+ description: "some description",
+ color: "some color",
+ icon: "some icon",
+ active: true
+ }
+
+ assert {:ok, %Perk{} = perk} = Tickets.create_perk(valid_attrs)
+ assert perk.name == "some name"
+ assert perk.description == "some description"
+ assert perk.color == "some color"
+ assert perk.icon == "some icon"
+ assert perk.active == true
+ end
+
+ test "create_perk/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Tickets.create_perk(@invalid_attrs)
+ end
+
+ test "update_perk/2 with valid data updates the perk" do
+ perk = perk_fixture()
+
+ update_attrs = %{
+ name: "some updated name",
+ description: "some updated description",
+ color: "some updated color",
+ icon: "some updated icon",
+ active: true
+ }
+
+ assert {:ok, %Perk{} = perk} = Tickets.update_perk(perk, update_attrs)
+ assert perk.name == "some updated name"
+ assert perk.description == "some updated description"
+ assert perk.color == "some updated color"
+ assert perk.icon == "some updated icon"
+ assert perk.active == true
+ end
+
+ test "update_perk/2 with invalid data returns error changeset" do
+ perk = perk_fixture()
+ assert {:error, %Ecto.Changeset{}} = Tickets.update_perk(perk, @invalid_attrs)
+ assert perk == Tickets.get_perk!(perk.id)
+ end
+
+ test "delete_perk/1 deletes the perk" do
+ perk = perk_fixture()
+ assert {:ok, %Perk{}} = Tickets.delete_perk(perk)
+ assert_raise Ecto.NoResultsError, fn -> Tickets.get_perk!(perk.id) end
+ end
+
+ test "change_perk/1 returns a perk changeset" do
+ perk = perk_fixture()
+ assert %Ecto.Changeset{} = Tickets.change_perk(perk)
+ end
+ end
+end
diff --git a/test/support/fixtures/discount_codes_fixtures.ex b/test/support/fixtures/discount_codes_fixtures.ex
new file mode 100644
index 0000000..f3d14ae
--- /dev/null
+++ b/test/support/fixtures/discount_codes_fixtures.ex
@@ -0,0 +1,23 @@
+defmodule Pearl.DiscountCodesFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `Pearl.DiscountCodes` context.
+ """
+
+ @doc """
+ Generate a discount_code.
+ """
+ def discount_code_fixture(attrs \\ %{}) do
+ {:ok, discount_code} =
+ attrs
+ |> Enum.into(%{
+ active: true,
+ amount: 42,
+ code: "some code",
+ usage_limit: 100
+ })
+ |> Pearl.DiscountCodes.create_discount_code()
+
+ discount_code
+ end
+end
diff --git a/test/support/fixtures/tickets_fixtures.ex b/test/support/fixtures/tickets_fixtures.ex
new file mode 100644
index 0000000..85c8b60
--- /dev/null
+++ b/test/support/fixtures/tickets_fixtures.ex
@@ -0,0 +1,24 @@
+defmodule Pearl.TicketsFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `Pearl.Tickets` context.
+ """
+
+ @doc """
+ Generate a perk.
+ """
+ def perk_fixture(attrs \\ %{}) do
+ {:ok, perk} =
+ attrs
+ |> Enum.into(%{
+ color: "some color",
+ description: "some description",
+ icon: "some icon",
+ name: "some name",
+ active: true
+ })
+ |> Pearl.Tickets.create_perk()
+
+ perk
+ end
+end